Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Checks

# Lint, unit tests, and the type-check + build gate on every PR that touches
# the ipaas frontend. Unlike the E2E workflows, none of this needs live
# credentials or a reachable backend, so it runs on every push too for
# branch-health visibility.
#
# Not yet a required status check. To make it required: add
# "Checks / Lint, test, build check" to branch protection rules under
# Settings → Branches → Required status checks.

on:
pull_request:
branches:
- main
- devant-migration
paths:
- 'ipaas/**'
push:
branches:
- main
- devant-migration
paths:
- 'ipaas/**'

jobs:
checks:
name: Lint, test, build check
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
cache-dependency-path: ipaas/pnpm-lock.yaml

- name: Install dependencies
working-directory: ipaas
run: pnpm install --frozen-lockfile

- name: Lint
working-directory: ipaas
run: pnpm lint

- name: Unit tests
working-directory: ipaas
run: pnpm test

- name: Type check + build
working-directory: ipaas
run: pnpm build:check
23 changes: 12 additions & 11 deletions ipaas/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ Every feature follows a strict four-layer separation. Violating these boundaries
┌─────────────────────────────────────────────────┐
│ pages/ components/ │ UI only
│ Import from: hooks/, types/, constants/, │
│ utils/, assets/, auth/ (OAuth
state utilities only — see below) │
│ utils/, assets/, auth/oauthState
(OAuth CSRF state — see below)
└───────────────┬─────────────────────────────────┘
│ imports
┌───────────────▼─────────────────────────────────┐
Expand All @@ -42,20 +42,21 @@ utils/ ← same
config/ ← same
```

**The single most important rule**: components and pages must never import directly from `api/`, `auth/tokenManager` (data functions), or any backend transport. All data access goes through `hooks/`.
**The single most important rule**: components and pages must never import directly from `api/`, `auth/tokenManager`, or any backend transport. All data access goes through `hooks/`.

---

## The one accepted exception

`auth/tokenManager.ts` exports two categories of functions:

| Category | Examples | Used in |
|---|---|---|
| OAuth CSRF state (pure local storage utilities) | `generateAndSaveGitHubState`, `validateAndClearGitHubState`, `validateAndClearOIDCState`, `getAndClearRedirectUrl` | pages — acceptable |
| Token/data access | `getOrgUuidFromToken`, `authenticatedFetch` | hooks only, via `useOrgUuid()` |

Pages may import the OAuth CSRF helpers directly because they are pure client-side state utilities with no cache semantics, not data access.
OAuth CSRF state (pure local storage/sessionStorage utilities — no token or network access)
lives in its own module, `auth/oauthState.ts`, separate from `auth/tokenManager.ts` (token/data
access, e.g. `getOrgUuidFromToken`, `authenticatedFetch` — hooks only, via `useOrgUuid()`).
Pages may import `auth/oauthState.ts` directly (`generateAndSaveGitHubState`,
`validateAndClearGitHubState`, `validateAndClearOIDCState`, `getAndClearRedirectUrl`) because
it holds nothing but pure client-side state utilities with no cache semantics, not data access.
`auth/tokenManager.ts` itself stays off-limits to pages with no per-file exception — splitting
the CSRF helpers into their own file makes this an allowlist by construction rather than an
ESLint exception list that has to be kept in sync by hand.

---

Expand Down
11 changes: 11 additions & 0 deletions ipaas/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ pnpm dev:cloud # Cloud variant
pnpm dev:icp # ICP variant
```

## Running unit tests

```bash
pnpm test # run once (CI-friendly)
pnpm test:watch # watch mode
```

Vitest, no live backend or credentials needed. Co-located as `<file>.test.ts` next to the
source file (e.g. `src/utils/identifyIntegration.test.ts`). `#api`/`#product` resolve to
`wip` in tests, same as the IDE (see `vitest.config.ts`).

## Running e2e tests

```bash
Expand Down
57 changes: 57 additions & 0 deletions ipaas/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';

// `group` patterns are matched gitignore-style: a leading `#` starts a comment
// unless escaped, so the alias must be written as `\#api`, not `#api`.
const NO_DIRECT_API_PATTERN = {
group: ['\\#api', '\\#api/*'],
message: 'UI code must not import api/ directly — call a hook from src/hooks/ instead. See AGENTS.md (the four-layer architecture).',
};

// Flat config does not merge `no-restricted-imports` option objects across
// matching blocks — the last matching block's value wins outright. Every
// block below that sets this rule must therefore repeat this pattern, or
// files it covers would silently lose the "never a literal product folder"
// restriction set by the repo-wide block.
const NO_LITERAL_PRODUCT_FOLDER_PATTERN = {
group: ['**/api/wip/*', '**/api/cloud/*', '**/api/icp/*'],
message: "Import product API functions via the '#api/<domain>' alias, never a literal product folder — see AGENTS.md.",
};

export default [
{ ignores: ['dist', 'playwright-report', 'test-results'] },
js.configs.recommended,
Expand All @@ -42,4 +59,44 @@ export default [
'@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '^_', argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_', ignoreRestSiblings: true }],
},
},
// Everywhere: go through the #api alias, never a literal product folder
// (src/api/_check.ts is exempt — it intentionally imports every product's
// real files to assert them against contracts.ts at compile time).
{
files: ['src/**/*.{ts,tsx}'],
ignores: ['src/api/**'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [NO_LITERAL_PRODUCT_FOLDER_PATTERN],
},
],
},
},
// UI layer (pages/components/layouts/contexts): no direct api/ access and no
// direct auth/tokenManager data access — all server state and token data
// must flow through src/hooks/. See AGENTS.md, src/pages/AGENTS.md,
// src/components/AGENTS.md.
//
// No per-file exception for the OAuth CSRF helpers: they live in their own
// module, src/auth/oauthState.ts, which this block does not restrict. That
// makes the allowed surface an allowlist by construction (oauthState.ts's
// exports) rather than a denylist of tokenManager.ts's other exports that
// would silently go stale if tokenManager.ts gained a new export.
{
files: ['src/pages/**/*.{ts,tsx}', 'src/components/**/*.{ts,tsx}', 'src/layouts/**/*.{ts,tsx}', 'src/contexts/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
NO_DIRECT_API_PATTERN,
NO_LITERAL_PRODUCT_FOLDER_PATTERN,
{ group: ['**/auth/tokenManager'], message: 'auth/tokenManager is raw token/data access — use a hook (useAuth, useOrgUuid, ...) instead. The OAuth CSRF helpers are in the separate src/auth/oauthState module (see src/pages/AGENTS.md), which is not restricted.' },
],
},
],
},
},
];
6 changes: 5 additions & 1 deletion ipaas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"build:cloud": "PRODUCT=cloud vite build",
"build:icp": "PRODUCT=icp vite build",
"build:check": "tsc -b && PRODUCT=wip vite build",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
Expand Down Expand Up @@ -57,12 +59,14 @@
"eslint-plugin-react-refresh": "0.4.24",
"globals": "16.4.0",
"googleapis": "^173.0.0",
"jsdom": "^29.1.1",
"prettier": "^3.8.1",
"rollup-plugin-visualizer": "6.0.5",
"sass": "^1.99.0",
"tsx": "^4.22.4",
"typescript": "5.9.3",
"typescript-eslint": "8.45.0",
"vite": "7.1.12"
"vite": "7.1.12",
"vitest": "^4.1.9"
}
}
2 changes: 1 addition & 1 deletion ipaas/src/auth/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
getRefreshToken,
revokeToken,
setOnAuthFailure,
generateAndSaveOIDCState,
generatePKCE,
saveCodeVerifier,
getAndClearCodeVerifier,
Expand All @@ -40,6 +39,7 @@ import {
saveOidcAuthMetadata,
clearOidcAuthMetadata,
} from './tokenManager';
import { generateAndSaveOIDCState } from './oauthState';

const USER_KEY = 'icp_user';

Expand Down
2 changes: 1 addition & 1 deletion ipaas/src/auth/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { useAuth } from './AuthContext';
import { useAccessControl } from '../contexts/AccessControlContext';
import { fetchOrgPermissions } from '#api/auth';
import { loginUrl, forceChangePasswordUrl } from '../paths';
import { saveRedirectUrl } from './tokenManager';
import { saveRedirectUrl } from './oauthState';
import { Permissions } from '../constants/permissions';

export default function ProtectedRoute(): JSX.Element {
Expand Down
69 changes: 69 additions & 0 deletions ipaas/src/auth/oauthState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
* OAuth CSRF state — pure localStorage/sessionStorage utilities, no token or
* network access. Deliberately a separate module from tokenManager.ts (which
* holds actual token/data access): pages and components may import from here
* directly (see src/pages/AGENTS.md, src/components/AGENTS.md). Keeping this
* file's export surface limited to exactly these helpers — rather than
* allow-listing names out of tokenManager.ts — means a future addition to
* tokenManager.ts (e.g. a new data-access function) can never become
* importable from UI code by accident; it would have to be added here
* explicitly first.
*/

const REDIRECT_URL_KEY = 'icp_redirect_url';
const OIDC_STATE_KEY = 'icp_oidc_state';

export function saveRedirectUrl(url: string): void {
localStorage.setItem(REDIRECT_URL_KEY, url);
}

export function getAndClearRedirectUrl(): string | null {
const url = localStorage.getItem(REDIRECT_URL_KEY);
localStorage.removeItem(REDIRECT_URL_KEY);
return url;
}

export function generateAndSaveOIDCState(): string {
const state = crypto.randomUUID();
localStorage.setItem(OIDC_STATE_KEY, state);
return state;
}

export function validateAndClearOIDCState(state: string): boolean {
const savedState = localStorage.getItem(OIDC_STATE_KEY);
localStorage.removeItem(OIDC_STATE_KEY);
return savedState === state;
}

// GitHub OAuth CSRF state — sessionStorage so it's scoped to the initiating tab
const GITHUB_OAUTH_STATE_KEY = 'icp_github_oauth_state';

export function generateAndSaveGitHubState(): string {
const state = crypto.randomUUID();
sessionStorage.setItem(GITHUB_OAUTH_STATE_KEY, state);
return state;
}

export function validateAndClearGitHubState(state: string): boolean {
const saved = sessionStorage.getItem(GITHUB_OAUTH_STATE_KEY);
sessionStorage.removeItem(GITHUB_OAUTH_STATE_KEY);
return saved !== null && saved === state;
}
39 changes: 0 additions & 39 deletions ipaas/src/auth/tokenManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ const ACCESS_TOKEN_KEY = 'icp_auth_token';
const REFRESH_TOKEN_KEY = 'icp_refresh_token';
const TOKEN_EXPIRES_AT_KEY = 'icp_token_expires_at';
const REFRESH_TOKEN_EXPIRES_AT_KEY = 'icp_refresh_token_expires_at';
const REDIRECT_URL_KEY = 'icp_redirect_url';
const OIDC_STATE_KEY = 'icp_oidc_state';
const OIDC_AUTH_MODE_KEY = 'icp_auth_mode';
const OIDC_ORG_HANDLE_KEY = 'icp_org_handle';

Expand Down Expand Up @@ -415,43 +413,6 @@ export async function revokeToken(): Promise<void> {
}
}

export function saveRedirectUrl(url: string): void {
localStorage.setItem(REDIRECT_URL_KEY, url);
}

export function getAndClearRedirectUrl(): string | null {
const url = localStorage.getItem(REDIRECT_URL_KEY);
localStorage.removeItem(REDIRECT_URL_KEY);
return url;
}

export function generateAndSaveOIDCState(): string {
const state = crypto.randomUUID();
localStorage.setItem(OIDC_STATE_KEY, state);
return state;
}

export function validateAndClearOIDCState(state: string): boolean {
const savedState = localStorage.getItem(OIDC_STATE_KEY);
localStorage.removeItem(OIDC_STATE_KEY);
return savedState === state;
}

// GitHub OAuth CSRF state — sessionStorage so it's scoped to the initiating tab
const GITHUB_OAUTH_STATE_KEY = 'icp_github_oauth_state';

export function generateAndSaveGitHubState(): string {
const state = crypto.randomUUID();
sessionStorage.setItem(GITHUB_OAUTH_STATE_KEY, state);
return state;
}

export function validateAndClearGitHubState(state: string): boolean {
const saved = sessionStorage.getItem(GITHUB_OAUTH_STATE_KEY);
sessionStorage.removeItem(GITHUB_OAUTH_STATE_KEY);
return saved !== null && saved === state;
}

// ---------------------------------------------------------------------------
// PKCE helpers
// ---------------------------------------------------------------------------
Expand Down
6 changes: 5 additions & 1 deletion ipaas/src/components/AiCopilot/ApiChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Divider, St
import { AlertCircle, CheckCircle2, ChevronDown, Terminal } from '@wso2/oxygen-ui-icons-react';
import { useState } from 'react';
import type { JSX } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light';
import { prism } from 'react-syntax-highlighter/dist/esm/styles/prism';
import json from 'react-syntax-highlighter/dist/esm/languages/prism/json';
import type { ApiChatExecutionResult } from '../../types/copilot';

// Only JSON is ever rendered here (API execution payloads).
SyntaxHighlighter.registerLanguage('json', json);

interface ParsedResult {
resource?: { method?: string; inputs?: { requestBody?: unknown } };
output?: { code?: number; path?: string; headers?: Record<string, unknown>; body?: unknown };
Expand Down
Loading
Loading