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
12 changes: 11 additions & 1 deletion .ai/targets/AUTH.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
## SCOPE
- Package: `packages/auth/`
- Layer: L1 SDK (zero @hai3 dependencies)
- Purpose: headless authentication contract (types only)
- Purpose: headless authentication contract (types only). Redux integration lives in @cyberfabric/framework (auth plugin).

## CRITICAL RULES
- REQUIRED: Only TypeScript interfaces and type aliases. No runtime code.
Expand Down Expand Up @@ -47,6 +47,16 @@
- Primary API: `canAccess(query)` with action + resource + optional record.
- NOT roles-first: roles are metadata inside AuthPermissions, not the primary check.

## REDUX STATE INTEGRATION
- The auth plugin in @cyberfabric/framework projects AuthProvider state into Redux.
- `AuthSessionMeta` — token-safe snapshot type (`kind` + optional `expiresAt`). Used when `redux.includeTokens` is `false` (default). Consumers who need tokens call `app.auth.getSession()`.
- `isFullAuthSession(session)` — type guard distinguishing `AuthSession` from `AuthSessionMeta`. For bearer sessions, checks for the `token` field.
- `RootStateWithAuth` — root state extension type providing `state['auth/session']` and `state['auth/permissions']`.
- Known limitations:
- `subscribe()` is optional on AuthProvider. Without it, Redux auth state only updates on explicit actions (syncAuth, loginAction, etc.).
- Stale state is possible between provider mutations and the next sync.
- Permissions are fetched automatically on sync if `provider.getPermissions` exists, but can also be fetched independently via `fetchPermissions()`.

## STOP CONDITIONS
- Adding runtime code (classes, functions, side effects).
- Adding @cyberfabric/* or third-party dependencies.
Expand Down
14 changes: 14 additions & 0 deletions .ai/targets/EVENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@
- UICORE format: "uicore/domainId/eventName".
- Actions use imperative names; events use past-tense names.

## FRAMEWORK-LEVEL EVENTS
- Framework plugins emit events with the `domain/subdomain/eventName` naming pattern.
- Auth session events:
- `auth/session/state-changed` — from provider `subscribe()` or after operations.
- `auth/session/sync-requested` — imperative sync trigger.
- `auth/session/login-requested` — login trigger.
- `auth/session/logout-requested` — logout trigger.
- `auth/session/refresh-requested` — refresh trigger.
- Auth permissions events:
- `auth/permissions/fetch-requested` — permissions fetch.
- `auth/permissions/changed` — permissions changed.
- Mock events use `mock/` prefix (e.g., `mock/mode-changed`).
- MFE-internal events use `mfe/` prefix (see MFE EVENT NAMING).

## DOMAIN FILE STRUCTURE
- REQUIRED: Split events into domain files (one file per domain).
- REQUIRED: Each domain file defines local DOMAIN_ID.
Expand Down
18 changes: 17 additions & 1 deletion .ai/targets/FRAMEWORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const store = configureStore({ ... }); // FORBIDDEN
| `routing()` | routeRegistry, URL sync | screensets |
| `i18n()` | i18nRegistry, setLanguage | - |
| `effects()` | Core effect coordination | - |
| `auth()` | app.auth (AuthRuntime) | @cyberfabric/auth |
| `auth()` | app.auth, auth/session slice, auth/permissions slice, auth actions | effects |

## RUNTIME EXTENSIONS
- Plugins expose runtime APIs on `app` via `provides.app`.
Expand Down Expand Up @@ -81,6 +81,22 @@ declare module '../types' {
- 401 refresh+retry: calls `provider.refresh()` on first 401, deduplicates concurrent refreshes, retries with new token.
- Custom transport: pass `transport` option to override default `hai3ApiTransport()` binding.

## AUTH REDUX INTEGRATION
- Config: `auth({ provider, redux: { includeTokens: false } })` — `includeTokens` defaults to `false`.
- Slices:
- `auth/session` — `{ status, session, error, lastSyncAt, capabilities }` (initial status: `'loading'`).
- `auth/permissions` — `{ permissions, loading, error }`.
- Actions (pure functions emitting events):
- `syncAuth()` — force re-check auth state.
- `loginAction(input)` — trigger login flow.
- `logoutAction()` — trigger logout.
- `refreshAuth()` — refresh session then sync.
- `fetchPermissions()` — fetch permissions independently.
- Module augmentation: `HAI3Actions` is extended from `auth.ts` via `declare module '../types'`, not statically in `types.ts`.
- Redux is a **projection** of AuthProvider state — provider is the source of truth.
- `useAuth()` for React components, `app.auth.getSession()` for imperative code.
- Without `provider.subscribe()`, Redux auth state only updates on explicit actions.

## CUSTOM PLUGINS
```typescript
// GOOD: Follow plugin contract
Expand Down
13 changes: 13 additions & 0 deletions .ai/targets/REACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const app = createHAI3().use(screensets()).build();
| `useScreenTranslations()` | Load screen translations | `{ isLoaded, error }` |
| `useNavigation()` | Navigation utilities | `{ navigateToScreen, currentScreen }` |
| `useTheme()` | Theme utilities | `{ currentTheme, themes, setTheme }` |
| `useAuth()` | Auth state & actions | `{ status, isAuthenticated, session, permissions, sync, login, logout, refresh, fetchPermissions }` |

## SCREEN TRANSLATIONS
```tsx
Expand All @@ -75,6 +76,18 @@ function HomeScreen() {
export default HomeScreen;
```

## AUTH HOOK
```tsx
const { isAuthenticated, status, permissions, login, logout } = useAuth();

if (!isAuthenticated) {
return <LoginButton onClick={() => login({ type: 'oauth', payload: {} })} />;
}
return <UserMenu onLogout={logout} roles={permissions?.roles} />;
```
- Safe to use when auth plugin is not registered (returns unauthenticated defaults).
- Redux is a projection of AuthProvider state; use `app.auth.getSession()` for imperative token access.

## STOP CONDITIONS
- Using hooks outside HAI3Provider.
- Using raw react-redux instead of provided hooks.
Expand Down
8 changes: 8 additions & 0 deletions .ai/targets/STORE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,19 @@
- Type safety via module augmentation on `RootState`.
- One store instance via `createStore()` singleton.

## FRAMEWORK-LEVEL SLICES
- `layout/*` — layout domain slices (header, footer, menu, sidebar, screen, popup, overlay).
- `app/*` — application-level slices.
- `mock` — mock mode control.
- `auth/session` — auth session state (`{ status, session, error, lastSyncAt, capabilities }`).
- `auth/permissions` — RBAC permissions (`{ permissions, loading, error }`).

## SLICE NAMING CONVENTION
- REQUIRED: Use template literal with screenset ID: `${SCREENSET_ID}/domain`.
- REQUIRED: Slice `.name` must match the state key exactly.
- FORBIDDEN: Hardcoded string slice names.
- FORBIDDEN: Global/shared slices outside layout domains.
- Framework-level prefixes: `layout/`, `app/`, `mock`, `auth/`.

## SLICE REGISTRATION
```typescript
Expand Down
85 changes: 85 additions & 0 deletions architecture/explorations/2026-04-10-auth-redux-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Exploration: Auth Redux Integration

Date: 2026-04-10

## Research question

How should the auth plugin project AuthProvider state into Redux for reactive UI consumption, while preserving the provider as the single source of truth?

## Scope

In scope:

- Redux state shape for auth session and permissions
- Token safety in serialized state
- Sync strategy between AuthProvider and Redux
- Action design for auth operations
- Type safety via module augmentation

Out of scope:

- AuthProvider contract changes (already defined in @cyberfabric/auth)
- Transport binding changes (already handled by auth plugin)
- React component design beyond the useAuth hook

## Findings

### 1. Why Redux instead of a registry pattern

Auth state is fundamentally different from registries (themes, i18n, screensets):

- **Async with failure modes**: auth operations (checkAuth, login, refresh) can fail, need loading/error tracking.
- **Derived state**: `isAuthenticated` is derived from `status`, permissions require separate fetch cycles.
- **Reactive updates**: UI components need to re-render when auth state changes (login, logout, token refresh, permission changes).
- **Already established pattern**: Redux slices with effects already handle this in layout and mock domains.

A registry would require inventing a subscription mechanism that Redux already provides via `useAppSelector`.

### 2. Token safety: AuthSessionMeta vs sentinel values

Two approaches were considered for keeping tokens out of Redux state:

- **Sentinel values**: Store the full `AuthSession` but replace token fields with a sentinel string (e.g., `"[REDACTED]"`). Simpler, but consumers might accidentally use the sentinel as a real token, and TypeScript cannot distinguish redacted from real sessions.
- **AuthSessionMeta type**: A separate type containing only `kind` and optional `expiresAt`. TypeScript enforces the distinction at compile time via `isFullAuthSession()` type guard.

`AuthSessionMeta` was chosen because:

- Type safety prevents accidental token usage from Redux state.
- The `isFullAuthSession()` type guard makes the check explicit and compiler-enforced.
- Cookie and custom sessions have no mandatory sensitive fields, so the guard handles all session kinds correctly.
- Consumers who need actual tokens have a clear path: call `app.auth.getSession()`.

### 3. SRP: two slices for independent update cycles

Auth session and permissions are separated into `auth/session` and `auth/permissions` because:

- Permissions can change independently (role assignment) without re-checking the session.
- Session refreshes do not necessarily change permissions.
- Separate loading/error states prevent UI flicker (e.g., permissions loading should not show the login screen).
- Independent slices enable fine-grained `useAppSelector` subscriptions (a component that only reads permissions does not re-render on session changes).

### 4. Sync strategy

Two sync modes operate in parallel:

- **Reactive (provider.subscribe)**: When the provider supports `subscribe()`, state changes are pushed immediately via the `auth/session/state-changed` event. This is the preferred path for real-time auth state.
- **Imperative (actions)**: `syncAuth()`, `loginAction()`, `logoutAction()`, `refreshAuth()`, and `fetchPermissions()` trigger explicit state transitions. These work regardless of whether `subscribe()` is implemented.

Since `subscribe()` is optional on AuthProvider, the imperative path is always available as a fallback. Without `subscribe()`, Redux auth state only updates when actions are explicitly called.

### 5. Module augmentation for HAI3Actions

Auth actions are declared on `HAI3Actions` via module augmentation from `plugins/auth.ts`, not statically in `types.ts`. This follows the plugin composition principle from ADR 0003: plugins declare their own type contributions. The `types.ts` file defines the base `HAI3Actions` interface as an extensible empty contract; each plugin extends it.

## Known limitations and trade-offs

- **Stale state window**: Between a provider mutation (e.g., external token revocation) and the next sync, Redux state may be stale. Mitigated by `provider.subscribe()` when available.
- **No automatic retry**: If `syncAuth()` fails, the error is stored but no automatic retry occurs. Consumers must call `syncAuth()` again.
- **Permissions auto-fetch coupling**: On sync, permissions are automatically fetched if `provider.getPermissions` exists. This may cause unnecessary fetches if permissions rarely change. `fetchPermissions()` provides an independent path.
- **Initial status is loading**: The `auth/session` slice starts with `status: 'loading'` to prevent a flash of unauthenticated UI before the first sync completes.

## References

- ADR 0002: Event-driven Flux dataflow
- ADR 0003: Plugin-based framework composition
- Exploration: 2026-03-27-authprovider-architecture-research.md
34 changes: 33 additions & 1 deletion packages/framework/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const headlessApp = createHAI3()
| `i18n()` | i18nRegistry, setLanguage action | - |
| `effects()` | Core effect coordination | - |
| `mock()` | mockSlice, toggleMockMode action | effects |
| `auth()` | app.auth, auth/session slice, auth/permissions slice, auth actions | effects |

### Mock Mode Control

Expand Down Expand Up @@ -86,6 +87,37 @@ const app = createHAI3()

Services register mock plugins using `registerPlugin()` in their constructor. The framework automatically manages plugin activation based on mock mode state.

### Auth Redux State

The `auth()` plugin projects AuthProvider state into two Redux slices:

- `auth/session` — `{ status, session, error, lastSyncAt, capabilities }` (initial status: `'loading'`)
- `auth/permissions` — `{ permissions, loading, error }`

Auth actions (pure functions emitting events):

| Action | Purpose |
|--------|---------|
| `syncAuth()` | Force re-check auth state |
| `loginAction(input)` | Trigger login flow |
| `logoutAction()` | Trigger logout |
| `refreshAuth()` | Refresh session then sync |
| `fetchPermissions()` | Fetch permissions independently |

Config: `auth({ provider, redux: { includeTokens: false } })` — when `includeTokens` is `false` (default), session data in Redux uses `AuthSessionMeta` (kind + expiresAt only). Use `app.auth.getSession()` for token access.

```typescript
import { createHAI3, effects, auth } from '@cyberfabric/framework';

const app = createHAI3()
.use(effects())
.use(auth({ provider: myAuthProvider }))
.build();

// Imperative: app.auth.getSession()
// React: useAuth() hook from @cyberfabric/react
```

### Built Application

After calling `.build()`, access registries, actions, and the MFE-enabled screensetsRegistry:
Expand Down Expand Up @@ -296,7 +328,7 @@ const menu = useAppSelector((state: RootStateWithLayout) => state.layout.menu);
- `presets` - Available presets (full, minimal, headless)

### Plugins
- `screensets`, `themes`, `layout`, `microfrontends`, `i18n`, `effects`, `mock`
- `screensets`, `themes`, `layout`, `microfrontends`, `i18n`, `effects`, `mock`, `auth`

### Registries
- `createThemeRegistry` - Theme registry factory
Expand Down
Loading