From 2473ade4ad73a558e9bebb4eba8cf96f79dbc849 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 4 May 2026 20:37:27 +0200 Subject: [PATCH] =?UTF-8?q?Address=20Codex=20audit=20P1.6=20=E2=80=94=20co?= =?UTF-8?q?nnection=20presets=20+=20dead=20`autoReauthOn`=20removal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new orca presets in `arts/active-app/presets/`: - `applyConnectionsReauthOnIdentityChange` — listens to `SESSION_EVENT_IDENTITY_CHANGED` and calls `App.connections.reauthenticateAll()`. Closes the canonical motivating scenario for orca: "chat connected with the previous user's credentials" can no longer happen with this preset wired. - `applyConnectionsCloseOnRevoke` — listens to `SESSION_EVENT_REVOKED` and calls `App.connections.closeAll('session-revoked')`, leaving no socket alive carrying revoked credentials. `applyStandardOrca` now picks both up automatically when `App.connections` is declared, and the index barrel re-exports the new shapes. Removes the dead `autoReauthOn` config — declared on `EngineConnectionsOptions` but never read by any runtime code: - field removed from `connection/types.ts` - `CONNECTION_AUTO_REAUTH_*` constants removed from `connection/consts.ts` - `ConnectionAutoReauthOn` / `ConnectionAutoReauthTarget` types removed - unused test import removed from `connection.test.ts` - connection README rewritten: orca preset is now the canonical bridge, per-connection `session: { ... }` documented as the manual / standalone alternative - demo route artifact-docs.ts and aapp page updated to use `applyStandardOrca(App)` instead of `autoReauthOn: 'standard'` The per-connection `session-wiring.ts` mechanism stays as-is — it's useful for connections that live outside an App composition or that need a custom `ConnectionSessionSource`. README now spells out the two paths: orca preset for App-composed apps, per-connection `session` for manual control. Suite: 1482 / 1482 (+4 from this commit: 3 preset behaviour tests + 1 `applyStandardOrca` connection wiring test). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../presets/connections-close-on-revoke.ts | 42 +++++++++ .../connections-reauth-on-identity-change.ts | 47 ++++++++++ src/arts/active-app/presets/index.ts | 8 ++ src/arts/active-app/presets/standard.ts | 11 +++ src/arts/active-app/test/presets.test.ts | 90 ++++++++++++++++++ src/arts/connection/README.md | 92 +++++++++++-------- src/arts/connection/consts.ts | 4 - src/arts/connection/test/connection.test.ts | 1 - src/arts/connection/types.ts | 11 --- src/web/routes/active/_data/artifact-docs.ts | 53 ++++------- src/web/routes/active/docs/aapp/+page.svelte | 4 +- 11 files changed, 275 insertions(+), 88 deletions(-) create mode 100644 src/arts/active-app/presets/connections-close-on-revoke.ts create mode 100644 src/arts/active-app/presets/connections-reauth-on-identity-change.ts diff --git a/src/arts/active-app/presets/connections-close-on-revoke.ts b/src/arts/active-app/presets/connections-close-on-revoke.ts new file mode 100644 index 0000000..2863547 --- /dev/null +++ b/src/arts/active-app/presets/connections-close-on-revoke.ts @@ -0,0 +1,42 @@ +import { ORCA_ON_ERROR_CONTINUE, ORCA_STAGE_MAIN, orcaError, orcaSuccess } from '$orca'; +import { SESSION_EVENT_REVOKED } from '$session'; +import type { ActiveConnections } from '$connection/types'; +import type { ActiveAppCore } from '../types.ts'; + +const ACTION_ID = 'connections.close-on-revoke'; +const TOKEN_CLOSED = 'connections:closed-on-revoke'; +const CLOSE_REASON = 'session-revoked'; + +/** + * Shape this preset requires from `App`. Only `closeAll()` is needed. + */ +export interface ConnectionsCloseOnRevokeApp extends ActiveAppCore { + readonly connections: Pick; +} + +/** + * Registers an orca action that closes every active connection when + * the session is revoked. Used together with + * `applyCacheClearOnRevoke` to ensure that a logout / forced sign-out + * leaves no live socket carrying the revoked identity's credentials. + * + * Returns a detach function. Calling it unregisters the action. + */ +export function applyConnectionsCloseOnRevoke( + App: ConnectionsCloseOnRevokeApp +): () => void { + return App.Orca.onEvent(SESSION_EVENT_REVOKED, { + id: ACTION_ID, + stage: ORCA_STAGE_MAIN, + provides: [TOKEN_CLOSED], + onError: ORCA_ON_ERROR_CONTINUE, + action: async () => { + try { + App.connections.closeAll(CLOSE_REASON); + return orcaSuccess({ emits: [TOKEN_CLOSED] }); + } catch (error) { + return orcaError(error); + } + } + }); +} diff --git a/src/arts/active-app/presets/connections-reauth-on-identity-change.ts b/src/arts/active-app/presets/connections-reauth-on-identity-change.ts new file mode 100644 index 0000000..f07d508 --- /dev/null +++ b/src/arts/active-app/presets/connections-reauth-on-identity-change.ts @@ -0,0 +1,47 @@ +import { ORCA_ON_ERROR_CONTINUE, ORCA_STAGE_MAIN, orcaError, orcaSuccess } from '$orca'; +import { SESSION_EVENT_IDENTITY_CHANGED } from '$session'; +import type { ActiveConnections } from '$connection/types'; +import type { ActiveAppCore } from '../types.ts'; + +const ACTION_ID = 'connections.reauth-on-identity-change'; +const TOKEN_REAUTHENTICATED = 'connections:reauthenticated-on-identity'; + +/** + * Shape this preset requires from `App`. Only the + * `reauthenticateAll()` method is actually invoked, so apps can + * inject any compatible adapter — no need to expose the full + * `ActiveConnections` surface. + */ +export interface ConnectionsReauthOnIdentityChangeApp extends ActiveAppCore { + readonly connections: Pick; +} + +/** + * Registers an orca action that asks every active connection to + * reauthenticate when the session's actor identity changes. Pairs with + * `applyCacheClearOnIdentityChange` and + * `applyPermInvalidateOnIdentityChange` to flush stale state from the + * previous user before any new request flies — the canonical motivator + * scenario for orca: "chat connected with the previous user's + * credentials" can no longer happen with this preset registered. + * + * Returns a detach function. Calling it unregisters the action. + */ +export function applyConnectionsReauthOnIdentityChange( + App: ConnectionsReauthOnIdentityChangeApp +): () => void { + return App.Orca.onEvent(SESSION_EVENT_IDENTITY_CHANGED, { + id: ACTION_ID, + stage: ORCA_STAGE_MAIN, + provides: [TOKEN_REAUTHENTICATED], + onError: ORCA_ON_ERROR_CONTINUE, + action: async () => { + try { + await App.connections.reauthenticateAll(); + return orcaSuccess({ emits: [TOKEN_REAUTHENTICATED] }); + } catch (error) { + return orcaError(error); + } + } + }); +} diff --git a/src/arts/active-app/presets/index.ts b/src/arts/active-app/presets/index.ts index 5da1e5a..5b6a747 100644 --- a/src/arts/active-app/presets/index.ts +++ b/src/arts/active-app/presets/index.ts @@ -24,6 +24,14 @@ export { applyCacheClearOnRevoke, type CacheClearOnRevokeApp } from './cache-clear-on-revoke.ts'; +export { + applyConnectionsCloseOnRevoke, + type ConnectionsCloseOnRevokeApp +} from './connections-close-on-revoke.ts'; +export { + applyConnectionsReauthOnIdentityChange, + type ConnectionsReauthOnIdentityChangeApp +} from './connections-reauth-on-identity-change.ts'; export { applyPermInvalidateOnIdentityChange, type PermInvalidateOnIdentityChangeApp diff --git a/src/arts/active-app/presets/standard.ts b/src/arts/active-app/presets/standard.ts index f818bcd..f866daa 100644 --- a/src/arts/active-app/presets/standard.ts +++ b/src/arts/active-app/presets/standard.ts @@ -1,8 +1,11 @@ import type { ActiveCache } from '$cache/types'; +import type { ActiveConnections } from '$connection/types'; import type { ActivePerms } from '$perm/types'; import type { ActiveAppCore } from '../types.ts'; import { applyCacheClearOnIdentityChange } from './cache-clear-on-identity-change.ts'; import { applyCacheClearOnRevoke } from './cache-clear-on-revoke.ts'; +import { applyConnectionsCloseOnRevoke } from './connections-close-on-revoke.ts'; +import { applyConnectionsReauthOnIdentityChange } from './connections-reauth-on-identity-change.ts'; import { applyPermInvalidateOnIdentityChange } from './perm-invalidate-on-identity-change.ts'; /** @@ -15,6 +18,7 @@ import { applyPermInvalidateOnIdentityChange } from './perm-invalidate-on-identi export interface StandardOrcaApp extends ActiveAppCore { readonly cache?: Pick; readonly perm?: Pick; + readonly connections?: Pick; } /** @@ -37,6 +41,13 @@ export function applyStandardOrca(App: StandardOrcaApp): () => void { const permApp = App as StandardOrcaApp & { perm: NonNullable }; detachers.push(applyPermInvalidateOnIdentityChange(permApp)); } + if (App.connections !== undefined) { + const connApp = App as StandardOrcaApp & { + connections: NonNullable; + }; + detachers.push(applyConnectionsReauthOnIdentityChange(connApp)); + detachers.push(applyConnectionsCloseOnRevoke(connApp)); + } return () => { for (let i = detachers.length - 1; i >= 0; i--) detachers[i](); diff --git a/src/arts/active-app/test/presets.test.ts b/src/arts/active-app/test/presets.test.ts index 985348b..388bf49 100644 --- a/src/arts/active-app/test/presets.test.ts +++ b/src/arts/active-app/test/presets.test.ts @@ -12,6 +12,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { applyCacheClearOnIdentityChange, applyCacheClearOnRevoke, + applyConnectionsCloseOnRevoke, + applyConnectionsReauthOnIdentityChange, applyPermInvalidateOnIdentityChange, applyStandardOrca } from '../presets/index.ts'; @@ -24,6 +26,7 @@ import type { EngineLogger } from '$logger'; import type { EngineBus } from '$bus'; import type { ActiveTimers } from '$timer'; import type { ActiveCache } from '$cache/types'; +import type { ActiveConnections } from '$connection/types'; import type { ActivePerms } from '$perm/types'; interface CoreState { @@ -164,6 +167,74 @@ describe('applyCacheClearOnRevoke', () => { }); }); +describe('applyConnectionsReauthOnIdentityChange', () => { + let core: CoreState; + + beforeEach(() => { + core = buildCore(); + }); + afterEach(() => { + disposeCore(core); + }); + + it('reauthenticates connections when SESSION_EVENT_IDENTITY_CHANGED is published', async () => { + const reauthenticateAll = vi.fn(() => Promise.resolve([])); + const connections = { reauthenticateAll } as unknown as ActiveConnections; + + applyConnectionsReauthOnIdentityChange({ Orca: core.orca, connections }); + + core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload); + await flush(); + + expect(reauthenticateAll).toHaveBeenCalledTimes(1); + }); + + it('records action failure in run trace when reauthenticateAll() rejects', async () => { + const error = new Error('reauth failed'); + const connections = { + reauthenticateAll: vi.fn(() => Promise.reject(error)) + } as unknown as ActiveConnections; + + applyConnectionsReauthOnIdentityChange({ Orca: core.orca, connections }); + + core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload); + await flush(); + + const runs = core.orca.recentRuns(); + expect(runs).toHaveLength(1); + expect(runs[0].actions[0].status).toBe('error'); + expect(runs[0].actions[0].error).toBe(error); + }); +}); + +describe('applyConnectionsCloseOnRevoke', () => { + let core: CoreState; + + beforeEach(() => { + core = buildCore(); + }); + afterEach(() => { + disposeCore(core); + }); + + it('closes connections when SESSION_EVENT_REVOKED is published', async () => { + const closeAll = vi.fn(); + const connections = { closeAll } as unknown as ActiveConnections; + + applyConnectionsCloseOnRevoke({ Orca: core.orca, connections }); + + const revokePayload = { + ...samplePayload, + event: 'session.lifecycle.revoked' + }; + core.bus.publish(SESSION_EVENT_REVOKED, revokePayload); + await flush(); + + expect(closeAll).toHaveBeenCalledTimes(1); + expect(closeAll).toHaveBeenCalledWith('session-revoked'); + }); +}); + describe('applyStandardOrca', () => { let core: CoreState; @@ -189,6 +260,25 @@ describe('applyStandardOrca', () => { expect(permInvalidate).toHaveBeenCalledTimes(1); }); + it('registers connection reauth + close when connections is present', async () => { + const reauthenticateAll = vi.fn(() => Promise.resolve([])); + const closeAll = vi.fn(); + const connections = { reauthenticateAll, closeAll } as unknown as ActiveConnections; + + applyStandardOrca({ Orca: core.orca, connections }); + + core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload); + await flush(); + expect(reauthenticateAll).toHaveBeenCalledTimes(1); + + core.bus.publish(SESSION_EVENT_REVOKED, { + ...samplePayload, + event: 'session.lifecycle.revoked' + }); + await flush(); + expect(closeAll).toHaveBeenCalledTimes(1); + }); + it('skips cache reactions when cache is absent', async () => { const permInvalidate = vi.fn(); const perm = { invalidate: permInvalidate } as unknown as ActivePerms; diff --git a/src/arts/connection/README.md b/src/arts/connection/README.md index 85d80b3..5fb3cdd 100644 --- a/src/arts/connection/README.md +++ b/src/arts/connection/README.md @@ -80,32 +80,47 @@ App inyecta: - `App.Logger`, como `Logger` común de `$libs/logr`. - `App.Timers`, para reconexión, heartbeat y timeouts de ack. -- `App.Bus`, para escuchar eventos públicos de aplicación cuando - `autoReauthOn` lo active. -The registry decides whether to subscribe to a session source. Wire one -explicitly when needed: +### Reacción a cambios de identidad + +El patrón canónico para reaccionar a cambios de sesión vive en los +**presets de `arts/active-app`**, no dentro del registry de +conexiones: ```ts +import { applyStandardOrca } from '$active-app/presets'; + const App = createActiveApp({ services: { - connections: defineActiveConnections({ - autoReauthOn: 'standard' - }), - session: defineActiveSession({ ... }) + connections: defineActiveConnections({}), + session: defineActiveSession({ ... }), + cache: defineActiveCache({ ... }), + perm: defineActivePerms({ ... }) } }); + +applyStandardOrca(App); +// → registra applyConnectionsReauthOnIdentityChange y +// applyConnectionsCloseOnRevoke entre otros, todos vía orca. ``` -`standard` listens to `SESSION_EVENT_IDENTITY_CHANGED`. Each connection -still decides whether to react via its own `session` option. +`applyConnectionsReauthOnIdentityChange` escucha +`SESSION_EVENT_IDENTITY_CHANGED` y llama +`App.connections.reauthenticateAll()`. `applyConnectionsCloseOnRevoke` +escucha `SESSION_EVENT_REVOKED` y llama `App.connections.closeAll()`. +Apps que prefieran granularidad pueden llamar a los presets +individuales en lugar del agregador. -Cada conexión decide si usa la sesión: +### Sesión per-connection (modo manual) + +Para casos donde una conexión concreta tiene un `ConnectionSessionSource` +propio (ajeno al `App.session` global), o para usos standalone sin +orca, cada conexión sigue aceptando `session` en sus opciones: ```ts const Main = Connections.createConnection('main', { transport: createWebSocketTransport({ url: '/realtime' }), - auth: () => ({ token: App.Sess?.current?.credential }), + auth: () => ({ token: App.session?.current?.credential }), session: { enabled: true, reauthOnRefresh: true, @@ -114,15 +129,17 @@ const Main = Connections.createConnection('main', { }); ``` -Sin `autoReauthOn`, `conn` no reacciona automáticamente a cambios de identidad -aunque exista `App.Bus`. Sin `session.enabled`, una conexión concreta tampoco -se reautentica ni se desconecta por eventos de identidad. +Si la conexión declara `session.enabled`, su lógica interna +(`session-wiring.ts`) escucha el `onChange` del source y reacciona +con `reauthenticate()` / `disconnect()`. La reautenticación necesita +`auth`: define qué credencial nueva se envía cuando el source dice +"identidad cambió". Sin `auth` no se puede emitir un frame de reauth +y debe resolverse con reconnect/disconnect manual. -La reautenticación necesita también `auth`. `autoReauthOn` y `session.enabled` -solo dicen cuándo reaccionar; `auth` dice qué credencial nueva se envía. Si la -conexión no define `auth`, un cambio de usuario no puede producir un frame de -reauth y debe resolverse con reconnect/disconnect manual o con una política de -sesión que cierre la conexión. +Las dos vías (preset orca a nivel App y `session` per-connection) +coexisten. La regla práctica: usa el preset cuando uses `arts/orca` y +`arts/session`; usa `session` per-connection para casos standalone o +cuando la conexión vive fuera del ciclo App. Cuando `Timers` no se inyecta, `createEngineConnections()` crea un scheduler privado con el mismo logger. Los diagnósticos del scheduler salen bajo la @@ -333,26 +350,29 @@ El resultado de auth es tagged: await Main.reauthenticate(); // { ok: true } | { ok: false, reason, error? } ``` -With `autoReauthOn` on the registry, `session.enabled` on the connection -and `auth` defined, the session source bound to the App's bus can: - -- reauthenticate when `session` publishes `SESSION_EVENT_IDENTITY_CHANGED` - on adoption, refresh, or external change; -- disconnect when that event represents expiration / revocation and - `disconnectOnExpire !== false`. - -The full flow is: +The full flow with the orca preset (`applyStandardOrca` or +`applyConnectionsReauthOnIdentityChange` / +`applyConnectionsCloseOnRevoke`) is: ```txt -session -> SESSION_EVENT_IDENTITY_CHANGED -connection -> reauth/disconnect when autoReauthOn + connection.session + - connection.auth allow it +session -> SESSION_EVENT_IDENTITY_CHANGED on App.Bus +orca -> connections-reauth-on-identity action runs + -> App.connections.reauthenticateAll() + -> each connection calls auth() with the new credential +session -> SESSION_EVENT_REVOKED on App.Bus +orca -> connections-close-on-revoke action runs + -> App.connections.closeAll('session-revoked') ``` -`connection` does not subscribe to `session.*` directly: the wiring is the -session source the registry creates from `App.Bus`. If you want different -behaviour, register a custom orca action or call -`Connections.reauthenticateAll()` / `closeAll()` explicitly. +The connection art does not subscribe to `session.*` events directly: +the orca preset is the canonical bridge. For standalone setups (a +connection that lives outside an App composition or that needs a +custom session source), each connection still accepts +`session: { enabled, reauthOnRefresh, disconnectOnExpire, ... }` and +its internal `session-wiring` listens to the source's `onChange`. +Both routes coexist: pick the orca preset when using `arts/orca` + +`arts/session`; pick the per-connection `session` option for manual +control. `conn` no crea sesiones ni decide permisos. En servidor, los joins/sends de un canal deben validarse con `auth/sess/perm`. diff --git a/src/arts/connection/consts.ts b/src/arts/connection/consts.ts index d322f3b..baacf62 100644 --- a/src/arts/connection/consts.ts +++ b/src/arts/connection/consts.ts @@ -142,10 +142,6 @@ export const CONNECTION_METHOD_REAUTHENTICATE = 'reauthenticate'; export const CONNECTION_METHOD_JOIN = 'join'; export const CONNECTION_METHOD_LEAVE = 'leave'; -export const CONNECTION_AUTO_REAUTH_NONE = 'none'; -export const CONNECTION_AUTO_REAUTH_STANDARD = 'standard'; -export const CONNECTION_AUTO_REAUTH_USER_IDENTITY_CHANGE = 'userIdentityChange'; - export const CONNECTION_FRAME_TYPE_AUTH = 'connection.auth'; export const CONNECTION_FRAME_TYPE_JOIN = 'connection.join'; export const CONNECTION_FRAME_TYPE_LEAVE = 'connection.leave'; diff --git a/src/arts/connection/test/connection.test.ts b/src/arts/connection/test/connection.test.ts index 9aca9ae..2aff932 100644 --- a/src/arts/connection/test/connection.test.ts +++ b/src/arts/connection/test/connection.test.ts @@ -11,7 +11,6 @@ import { CONNECTION_STATE_FAILED, CONNECTION_STATE_OPEN, CONNECTION_STATE_RECONNECTING, - CONNECTION_AUTO_REAUTH_STANDARD, CONNECTION_SESSION_EVENT_EXPIRED, CONNECTION_SESSION_EVENT_REVOKED, createEngineConnections, diff --git a/src/arts/connection/types.ts b/src/arts/connection/types.ts index 19915c7..52fd70a 100644 --- a/src/arts/connection/types.ts +++ b/src/arts/connection/types.ts @@ -27,9 +27,6 @@ import type { CONNECTION_CONNECT_REASON_DISPOSED, CONNECTION_CONNECT_REASON_RECONNECT_EXHAUSTED, CONNECTION_CONNECT_REASON_TRANSPORT_ERROR, - CONNECTION_AUTO_REAUTH_NONE, - CONNECTION_AUTO_REAUTH_STANDARD, - CONNECTION_AUTO_REAUTH_USER_IDENTITY_CHANGE, CONNECTION_SEND_REASON_BUFFER_FULL, CONNECTION_SEND_REASON_CLOSED, CONNECTION_SEND_REASON_INVALID_FRAME, @@ -365,19 +362,11 @@ export interface EngineConnectionsOptions { */ readonly timers: TimerScheduler; readonly defaults?: Partial; - readonly autoReauthOn?: ConnectionAutoReauthOn; readonly session?: ConnectionSessionSource; } export type ActiveConnectionsOptions = EngineConnectionsOptions; -export type ConnectionAutoReauthTarget = typeof CONNECTION_AUTO_REAUTH_USER_IDENTITY_CHANGE; - -export type ConnectionAutoReauthOn = - | typeof CONNECTION_AUTO_REAUTH_NONE - | typeof CONNECTION_AUTO_REAUTH_STANDARD - | readonly ConnectionAutoReauthTarget[]; - export interface ConnectionSessionChange { readonly event: string; readonly current?: unknown | null; diff --git a/src/web/routes/active/_data/artifact-docs.ts b/src/web/routes/active/_data/artifact-docs.ts index 07016f8..0788ebe 100644 --- a/src/web/routes/active/_data/artifact-docs.ts +++ b/src/web/routes/active/_data/artifact-docs.ts @@ -1284,15 +1284,10 @@ const artifactApis = { purpose: 'Shared runtime dependencies.', notes: 'App injects Logger and Timers automatically.' }, - { - name: 'autoReauthOn', - purpose: 'Reauth policy on session-source events.', - notes: 'Defaults to none. "standard" listens to identity-change events from the supplied ConnectionSessionSource.' - }, { name: 'session', - purpose: 'ConnectionSessionSource (abstract { onChange } interface).', - notes: 'Application code builds it from App.session and passes it through; the connection art does not couple to App.Bus directly.' + purpose: 'ConnectionSessionSource (abstract { onChange } interface) for advanced/manual wiring.', + notes: 'Optional. The canonical pattern wires reauth/close as orca actions via applyConnectionsReauthOnIdentityChange / applyConnectionsCloseOnRevoke (or applyStandardOrca) — this option is for standalone / per-connection setups outside the App composition.' } ] }, @@ -1891,7 +1886,7 @@ const App = createActiveApp({ cache: defineActiveCache({}), perm: defineActivePerm({ endpoint: '/api/perm' }), session: defineActiveSession({ onRefresh, onRevoke }), - connections: defineActiveConnections({ autoReauthOn: 'standard' }) + connections: defineActiveConnections({}) } }); @@ -1902,12 +1897,6 @@ const Chat = App.connections.createConnection('chat', { auth: () => { const credential = App.session?.current?.credential; return credential ? { accessToken: credential.accessToken } : null; - }, - session: { - enabled: true, - reauthOnChange: true, - reauthOnRefresh: true, - disconnectOnExpire: true } }); @@ -1915,13 +1904,14 @@ const Chat = App.connections.createConnection('chat', { App.session.adoptServer(nextSessionFromServer); // 1. App.session updates state, then publishes SESSION_EVENT_IDENTITY_CHANGED on App.Bus. -// 2. Orca runs the registered actions: -// - cache-clear-on-identity action calls App.cache.clear() -// - perm-invalidate-on-identity action calls App.perm.invalidate() -// 3. Chat sees the change via its ConnectionSessionSource (built from App.session) -// and calls auth(), sending a fresh credential frame because the registry's -// autoReauthOn and the connection's session/auth options all opted in. -// 4. If the session expires or is revoked, Chat disconnects instead.` +// 2. Orca runs the registered actions in a single trace: +// - cache-clear-on-identity -> App.cache.clear() +// - perm-invalidate-on-identity -> App.perm.invalidate() +// - connections-reauth-on-identity -> App.connections.reauthenticateAll() +// which calls each connection's auth() with the fresh credential. +// 3. If the session is revoked, the same orca pipeline runs +// cache-clear-on-revoke and connections-close-on-revoke, leaving no +// socket alive carrying the revoked credentials.` } } ], @@ -3800,13 +3790,13 @@ Timers.cancelScope('profile');` overview: [ 'Connections is a registry of named realtime connections. A connection is not the engine: EngineConnections owns all connection state; each Connection owns transport, channels, heartbeat, reconnect and request/reply.', 'The transport contract is pluggable. Browser WebSocket is one transport; tests and demos can use mock transports without changing the connection runtime.', - 'When composed through App via defineActiveConnections, the registry receives Timers and Logger from the core. Identity tracking is decoupled: the connection consumes a ConnectionSessionSource (an abstract { onChange } interface) — typically built from App.session — and the autoReauthOn option declares which source events trigger reauth.' + 'When composed through App via defineActiveConnections, the registry receives Timers and Logger from the core. Identity tracking is decoupled: the canonical wiring uses orca presets (applyConnectionsReauthOnIdentityChange / applyConnectionsCloseOnRevoke) to bridge App.session events to the registry. Per-connection ConnectionSessionSource remains available for standalone or manual setups.' ], dynamics: [ 'Create one registry, then create named connections inside it. The registry tracks all names and aggregate state; each connection owns its transport lifecycle, channel collection, send buffer, heartbeat timers and reconnect strategy.', 'Transports emit open/message/close/failure signals. The connection translates those into framework states, schedules heartbeat and reconnect through Timers, and routes logs through the shared Logger/diagnostic constants.', 'Channels are scoped streams over a connection. They can join, leave, send and request. After reconnect, auto-join channels rejoin so feature code does not rebuild subscriptions manually.', - 'Identity reauth is opt-in at three levels: the registry must enable autoReauthOn, each connection must enable its own session option, and the connection must provide auth. Without those, identity events are observable but inert.' + 'Identity reauth is wired through the orca presets in $active-app/presets — applyStandardOrca(App) registers the canonical reactions (cache clear, perm invalidate, connections reauth on identity change, connections close on revoke). Each connection still needs its own auth() callback to produce a credential frame. Per-connection session options remain available for standalone or manual setups outside the App composition.' ], commonMistakes: [ { @@ -3828,7 +3818,7 @@ Timers.cancelScope('profile');` { name: 'forgetting app-event reauth behavior', purpose: 'Connections can keep old identity after login/logout/refresh.', - notes: 'Enable registry autoReauthOn, each connection session option and connection auth, or reconnect/disconnect manually.' + notes: 'Wire the orca preset (applyStandardOrca or applyConnectionsReauthOnIdentityChange / applyConnectionsCloseOnRevoke) so identity changes flow through reauthenticateAll/closeAll. For standalone connections without orca, use the per-connection session option.' } ], quickStart: { @@ -3836,10 +3826,12 @@ Timers.cancelScope('profile');` code: `const App = createActiveApp({ services: { session: defineActiveSession({ ... }), - connections: defineActiveConnections({ autoReauthOn: 'standard' }) + connections: defineActiveConnections({}) } }); +applyStandardOrca(App); // wires reauth-on-identity / close-on-revoke + const Updates = App.connections.createConnection('updates', { transport: createWebSocketTransport({ url: '/ws' }), auth: () => { @@ -3847,12 +3839,7 @@ const Updates = App.connections.createConnection('updates', { return credential ? { accessToken: credential.accessToken } : null; }, heartbeat: { enabled: true }, - reconnect: { enabled: true }, - session: { - enabled: true, - reauthOnChange: true, - disconnectOnExpire: true - } + reconnect: { enabled: true } }); await Updates.connect(); @@ -3945,8 +3932,8 @@ await Chat.connect();` { title: 'Session-aware reauth', body: [ - 'Connections consume an abstract ConnectionSessionSource (a { onChange } interface). The application builds the source from App.session and passes it through defineActiveConnections({ session: ... }) — the connection art does not import session or bus internals.', - 'When autoReauthOn is enabled at the registry level, identity-change events from the source trigger a reauth attempt on each connection that has session.enabled and an auth provider. Without auth there is no credential payload to send, so the connection can only be observed or manually reconnected.' + 'The canonical pattern wires reauth through orca: applyStandardOrca(App) (or the individual applyConnectionsReauthOnIdentityChange / applyConnectionsCloseOnRevoke) calls reauthenticateAll() / closeAll() on App.connections when session events fire. Each connection still needs its own auth() callback to produce the credential frame.', + 'For standalone or per-connection setups outside the App composition, the registry / connection still accepts ConnectionSessionSource ({ onChange }) via the session option. The internal session-wiring listens to onChange and reacts directly. This is the manual / advanced path.' ] }, { diff --git a/src/web/routes/active/docs/aapp/+page.svelte b/src/web/routes/active/docs/aapp/+page.svelte index 76fbffc..96ab9b4 100644 --- a/src/web/routes/active/docs/aapp/+page.svelte +++ b/src/web/routes/active/docs/aapp/+page.svelte @@ -29,9 +29,7 @@ App.lang.setLocale('es-MX'); // notifies Format and Frontend via the locale sour const App = createActiveApp({ services: { sium: defineEngineSium({}), // validation engine - connections: defineActiveConnections({ // realtime registry - autoReauthOn: 'standard' - }), + connections: defineActiveConnections({}), // realtime registry auth: defineActiveAuth({ initial: data.auth }), perm: defineActivePerm({ endpoint: '/api/perm' }), session: defineActiveSession({