Address Codex audit P1.6 — connection presets + dead `autoReauthOn` removal

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) <noreply@anthropic.com>
master
dev 5 months ago
parent c0ed619372
commit 2473ade4ad

@ -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<ActiveConnections, 'closeAll'>;
}
/**
* 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);
}
}
});
}

@ -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<ActiveConnections, 'reauthenticateAll'>;
}
/**
* 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);
}
}
});
}

@ -24,6 +24,14 @@ export {
applyCacheClearOnRevoke, applyCacheClearOnRevoke,
type CacheClearOnRevokeApp type CacheClearOnRevokeApp
} from './cache-clear-on-revoke.ts'; } 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 { export {
applyPermInvalidateOnIdentityChange, applyPermInvalidateOnIdentityChange,
type PermInvalidateOnIdentityChangeApp type PermInvalidateOnIdentityChangeApp

@ -1,8 +1,11 @@
import type { ActiveCache } from '$cache/types'; import type { ActiveCache } from '$cache/types';
import type { ActiveConnections } from '$connection/types';
import type { ActivePerms } from '$perm/types'; import type { ActivePerms } from '$perm/types';
import type { ActiveAppCore } from '../types.ts'; import type { ActiveAppCore } from '../types.ts';
import { applyCacheClearOnIdentityChange } from './cache-clear-on-identity-change.ts'; import { applyCacheClearOnIdentityChange } from './cache-clear-on-identity-change.ts';
import { applyCacheClearOnRevoke } from './cache-clear-on-revoke.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'; 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 { export interface StandardOrcaApp extends ActiveAppCore {
readonly cache?: Pick<ActiveCache, 'clear'>; readonly cache?: Pick<ActiveCache, 'clear'>;
readonly perm?: Pick<ActivePerms, 'invalidate'>; readonly perm?: Pick<ActivePerms, 'invalidate'>;
readonly connections?: Pick<ActiveConnections, 'reauthenticateAll' | 'closeAll'>;
} }
/** /**
@ -37,6 +41,13 @@ export function applyStandardOrca(App: StandardOrcaApp): () => void {
const permApp = App as StandardOrcaApp & { perm: NonNullable<StandardOrcaApp['perm']> }; const permApp = App as StandardOrcaApp & { perm: NonNullable<StandardOrcaApp['perm']> };
detachers.push(applyPermInvalidateOnIdentityChange(permApp)); detachers.push(applyPermInvalidateOnIdentityChange(permApp));
} }
if (App.connections !== undefined) {
const connApp = App as StandardOrcaApp & {
connections: NonNullable<StandardOrcaApp['connections']>;
};
detachers.push(applyConnectionsReauthOnIdentityChange(connApp));
detachers.push(applyConnectionsCloseOnRevoke(connApp));
}
return () => { return () => {
for (let i = detachers.length - 1; i >= 0; i--) detachers[i](); for (let i = detachers.length - 1; i >= 0; i--) detachers[i]();

@ -12,6 +12,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { import {
applyCacheClearOnIdentityChange, applyCacheClearOnIdentityChange,
applyCacheClearOnRevoke, applyCacheClearOnRevoke,
applyConnectionsCloseOnRevoke,
applyConnectionsReauthOnIdentityChange,
applyPermInvalidateOnIdentityChange, applyPermInvalidateOnIdentityChange,
applyStandardOrca applyStandardOrca
} from '../presets/index.ts'; } from '../presets/index.ts';
@ -24,6 +26,7 @@ import type { EngineLogger } from '$logger';
import type { EngineBus } from '$bus'; import type { EngineBus } from '$bus';
import type { ActiveTimers } from '$timer'; import type { ActiveTimers } from '$timer';
import type { ActiveCache } from '$cache/types'; import type { ActiveCache } from '$cache/types';
import type { ActiveConnections } from '$connection/types';
import type { ActivePerms } from '$perm/types'; import type { ActivePerms } from '$perm/types';
interface CoreState { 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', () => { describe('applyStandardOrca', () => {
let core: CoreState; let core: CoreState;
@ -189,6 +260,25 @@ describe('applyStandardOrca', () => {
expect(permInvalidate).toHaveBeenCalledTimes(1); 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 () => { it('skips cache reactions when cache is absent', async () => {
const permInvalidate = vi.fn(); const permInvalidate = vi.fn();
const perm = { invalidate: permInvalidate } as unknown as ActivePerms; const perm = { invalidate: permInvalidate } as unknown as ActivePerms;

@ -80,32 +80,47 @@ App inyecta:
- `App.Logger`, como `Logger` común de `$libs/logr`. - `App.Logger`, como `Logger` común de `$libs/logr`.
- `App.Timers`, para reconexión, heartbeat y timeouts de ack. - `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 ### Reacción a cambios de identidad
explicitly when needed:
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 ```ts
import { applyStandardOrca } from '$active-app/presets';
const App = createActiveApp({ const App = createActiveApp({
services: { services: {
connections: defineActiveConnections({ connections: defineActiveConnections({}),
autoReauthOn: 'standard' session: defineActiveSession({ ... }),
}), cache: defineActiveCache({ ... }),
session: defineActiveSession({ ... }) perm: defineActivePerms({ ... })
} }
}); });
applyStandardOrca(App);
// → registra applyConnectionsReauthOnIdentityChange y
// applyConnectionsCloseOnRevoke entre otros, todos vía orca.
``` ```
`standard` listens to `SESSION_EVENT_IDENTITY_CHANGED`. Each connection `applyConnectionsReauthOnIdentityChange` escucha
still decides whether to react via its own `session` option. `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 ```ts
const Main = Connections.createConnection('main', { const Main = Connections.createConnection('main', {
transport: createWebSocketTransport({ url: '/realtime' }), transport: createWebSocketTransport({ url: '/realtime' }),
auth: () => ({ token: App.Sess?.current?.credential }), auth: () => ({ token: App.session?.current?.credential }),
session: { session: {
enabled: true, enabled: true,
reauthOnRefresh: true, reauthOnRefresh: true,
@ -114,15 +129,17 @@ const Main = Connections.createConnection('main', {
}); });
``` ```
Sin `autoReauthOn`, `conn` no reacciona automáticamente a cambios de identidad Si la conexión declara `session.enabled`, su lógica interna
aunque exista `App.Bus`. Sin `session.enabled`, una conexión concreta tampoco (`session-wiring.ts`) escucha el `onChange` del source y reacciona
se reautentica ni se desconecta por eventos de identidad. 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` Las dos vías (preset orca a nivel App y `session` per-connection)
solo dicen cuándo reaccionar; `auth` dice qué credencial nueva se envía. Si la coexisten. La regla práctica: usa el preset cuando uses `arts/orca` y
conexión no define `auth`, un cambio de usuario no puede producir un frame de `arts/session`; usa `session` per-connection para casos standalone o
reauth y debe resolverse con reconnect/disconnect manual o con una política de cuando la conexión vive fuera del ciclo App.
sesión que cierre la conexión.
Cuando `Timers` no se inyecta, `createEngineConnections()` crea un scheduler Cuando `Timers` no se inyecta, `createEngineConnections()` crea un scheduler
privado con el mismo logger. Los diagnósticos del scheduler salen bajo la 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? } await Main.reauthenticate(); // { ok: true } | { ok: false, reason, error? }
``` ```
With `autoReauthOn` on the registry, `session.enabled` on the connection The full flow with the orca preset (`applyStandardOrca` or
and `auth` defined, the session source bound to the App's bus can: `applyConnectionsReauthOnIdentityChange` /
`applyConnectionsCloseOnRevoke`) is:
- 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:
```txt ```txt
session -> SESSION_EVENT_IDENTITY_CHANGED session -> SESSION_EVENT_IDENTITY_CHANGED on App.Bus
connection -> reauth/disconnect when autoReauthOn + connection.session + orca -> connections-reauth-on-identity action runs
connection.auth allow it -> 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 The connection art does not subscribe to `session.*` events directly:
session source the registry creates from `App.Bus`. If you want different the orca preset is the canonical bridge. For standalone setups (a
behaviour, register a custom orca action or call connection that lives outside an App composition or that needs a
`Connections.reauthenticateAll()` / `closeAll()` explicitly. 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 `conn` no crea sesiones ni decide permisos. En servidor, los joins/sends de un
canal deben validarse con `auth/sess/perm`. canal deben validarse con `auth/sess/perm`.

@ -142,10 +142,6 @@ export const CONNECTION_METHOD_REAUTHENTICATE = 'reauthenticate';
export const CONNECTION_METHOD_JOIN = 'join'; export const CONNECTION_METHOD_JOIN = 'join';
export const CONNECTION_METHOD_LEAVE = 'leave'; 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_AUTH = 'connection.auth';
export const CONNECTION_FRAME_TYPE_JOIN = 'connection.join'; export const CONNECTION_FRAME_TYPE_JOIN = 'connection.join';
export const CONNECTION_FRAME_TYPE_LEAVE = 'connection.leave'; export const CONNECTION_FRAME_TYPE_LEAVE = 'connection.leave';

@ -11,7 +11,6 @@ import {
CONNECTION_STATE_FAILED, CONNECTION_STATE_FAILED,
CONNECTION_STATE_OPEN, CONNECTION_STATE_OPEN,
CONNECTION_STATE_RECONNECTING, CONNECTION_STATE_RECONNECTING,
CONNECTION_AUTO_REAUTH_STANDARD,
CONNECTION_SESSION_EVENT_EXPIRED, CONNECTION_SESSION_EVENT_EXPIRED,
CONNECTION_SESSION_EVENT_REVOKED, CONNECTION_SESSION_EVENT_REVOKED,
createEngineConnections, createEngineConnections,

@ -27,9 +27,6 @@ import type {
CONNECTION_CONNECT_REASON_DISPOSED, CONNECTION_CONNECT_REASON_DISPOSED,
CONNECTION_CONNECT_REASON_RECONNECT_EXHAUSTED, CONNECTION_CONNECT_REASON_RECONNECT_EXHAUSTED,
CONNECTION_CONNECT_REASON_TRANSPORT_ERROR, 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_BUFFER_FULL,
CONNECTION_SEND_REASON_CLOSED, CONNECTION_SEND_REASON_CLOSED,
CONNECTION_SEND_REASON_INVALID_FRAME, CONNECTION_SEND_REASON_INVALID_FRAME,
@ -365,19 +362,11 @@ export interface EngineConnectionsOptions {
*/ */
readonly timers: TimerScheduler; readonly timers: TimerScheduler;
readonly defaults?: Partial<ConnectionOptions>; readonly defaults?: Partial<ConnectionOptions>;
readonly autoReauthOn?: ConnectionAutoReauthOn;
readonly session?: ConnectionSessionSource; readonly session?: ConnectionSessionSource;
} }
export type ActiveConnectionsOptions = EngineConnectionsOptions; 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 { export interface ConnectionSessionChange {
readonly event: string; readonly event: string;
readonly current?: unknown | null; readonly current?: unknown | null;

@ -1284,15 +1284,10 @@ const artifactApis = {
purpose: 'Shared runtime dependencies.', purpose: 'Shared runtime dependencies.',
notes: 'App injects Logger and Timers automatically.' 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', name: 'session',
purpose: 'ConnectionSessionSource (abstract { onChange } interface).', purpose: 'ConnectionSessionSource (abstract { onChange } interface) for advanced/manual wiring.',
notes: 'Application code builds it from App.session and passes it through; the connection art does not couple to App.Bus directly.' 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({}), cache: defineActiveCache({}),
perm: defineActivePerm({ endpoint: '/api/perm' }), perm: defineActivePerm({ endpoint: '/api/perm' }),
session: defineActiveSession<User, ChatCredential>({ onRefresh, onRevoke }), session: defineActiveSession<User, ChatCredential>({ onRefresh, onRevoke }),
connections: defineActiveConnections({ autoReauthOn: 'standard' }) connections: defineActiveConnections({})
} }
}); });
@ -1902,12 +1897,6 @@ const Chat = App.connections.createConnection('chat', {
auth: () => { auth: () => {
const credential = App.session?.current?.credential; const credential = App.session?.current?.credential;
return credential ? { accessToken: credential.accessToken } : null; 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); App.session.adoptServer(nextSessionFromServer);
// 1. App.session updates state, then publishes SESSION_EVENT_IDENTITY_CHANGED on App.Bus. // 1. App.session updates state, then publishes SESSION_EVENT_IDENTITY_CHANGED on App.Bus.
// 2. Orca runs the registered actions: // 2. Orca runs the registered actions in a single trace:
// - cache-clear-on-identity action calls App.cache.clear() // - cache-clear-on-identity -> App.cache.clear()
// - perm-invalidate-on-identity action calls App.perm.invalidate() // - perm-invalidate-on-identity -> App.perm.invalidate()
// 3. Chat sees the change via its ConnectionSessionSource (built from App.session) // - connections-reauth-on-identity -> App.connections.reauthenticateAll()
// and calls auth(), sending a fresh credential frame because the registry's // which calls each connection's auth() with the fresh credential.
// autoReauthOn and the connection's session/auth options all opted in. // 3. If the session is revoked, the same orca pipeline runs
// 4. If the session expires or is revoked, Chat disconnects instead.` // 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: [ 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.', '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.', '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: [ 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.', '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.', '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.', '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: [ commonMistakes: [
{ {
@ -3828,7 +3818,7 @@ Timers.cancelScope('profile');`
{ {
name: 'forgetting app-event reauth behavior', name: 'forgetting app-event reauth behavior',
purpose: 'Connections can keep old identity after login/logout/refresh.', 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: { quickStart: {
@ -3836,10 +3826,12 @@ Timers.cancelScope('profile');`
code: `const App = createActiveApp({ code: `const App = createActiveApp({
services: { services: {
session: defineActiveSession({ ... }), session: defineActiveSession({ ... }),
connections: defineActiveConnections({ autoReauthOn: 'standard' }) connections: defineActiveConnections({})
} }
}); });
applyStandardOrca(App); // wires reauth-on-identity / close-on-revoke
const Updates = App.connections.createConnection('updates', { const Updates = App.connections.createConnection('updates', {
transport: createWebSocketTransport({ url: '/ws' }), transport: createWebSocketTransport({ url: '/ws' }),
auth: () => { auth: () => {
@ -3847,12 +3839,7 @@ const Updates = App.connections.createConnection('updates', {
return credential ? { accessToken: credential.accessToken } : null; return credential ? { accessToken: credential.accessToken } : null;
}, },
heartbeat: { enabled: true }, heartbeat: { enabled: true },
reconnect: { enabled: true }, reconnect: { enabled: true }
session: {
enabled: true,
reauthOnChange: true,
disconnectOnExpire: true
}
}); });
await Updates.connect(); await Updates.connect();
@ -3945,8 +3932,8 @@ await Chat.connect();`
{ {
title: 'Session-aware reauth', title: 'Session-aware reauth',
body: [ 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.', '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.',
'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.' '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.'
] ]
}, },
{ {

@ -29,9 +29,7 @@ App.lang.setLocale('es-MX'); // notifies Format and Frontend via the locale sour
const App = createActiveApp({ const App = createActiveApp({
services: { services: {
sium: defineEngineSium({}), // validation engine sium: defineEngineSium({}), // validation engine
connections: defineActiveConnections({ // realtime registry connections: defineActiveConnections({}), // realtime registry
autoReauthOn: 'standard'
}),
auth: defineActiveAuth({ initial: data.auth }), auth: defineActiveAuth({ initial: data.auth }),
perm: defineActivePerm({ endpoint: '/api/perm' }), perm: defineActivePerm({ endpoint: '/api/perm' }),
session: defineActiveSession({ session: defineActiveSession({

Loading…
Cancel
Save

Powered by TurnKey Linux.