@llui/agent 0.0.44 → 0.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/agentConnect.d.ts +53 -1
- package/dist/client/agentConnect.d.ts.map +1 -1
- package/dist/client/agentConnect.js +130 -5
- package/dist/client/agentConnect.js.map +1 -1
- package/dist/client/effect-handler.d.ts +11 -0
- package/dist/client/effect-handler.d.ts.map +1 -1
- package/dist/client/effect-handler.js +26 -5
- package/dist/client/effect-handler.js.map +1 -1
- package/dist/client/effects.d.ts +17 -0
- package/dist/client/effects.d.ts.map +1 -1
- package/dist/client/effects.js.map +1 -1
- package/dist/client/factory.d.ts +53 -1
- package/dist/client/factory.d.ts.map +1 -1
- package/dist/client/factory.js +105 -0
- package/dist/client/factory.js.map +1 -1
- package/dist/server/core.d.ts +19 -0
- package/dist/server/core.d.ts.map +1 -1
- package/dist/server/core.js +35 -2
- package/dist/server/core.js.map +1 -1
- package/dist/server/lap/active.d.ts +19 -0
- package/dist/server/lap/active.d.ts.map +1 -0
- package/dist/server/lap/active.js +25 -0
- package/dist/server/lap/active.js.map +1 -0
- package/dist/server/lap/confirm-result.d.ts.map +1 -1
- package/dist/server/lap/confirm-result.js +4 -1
- package/dist/server/lap/confirm-result.js.map +1 -1
- package/dist/server/lap/describe.d.ts.map +1 -1
- package/dist/server/lap/describe.js +10 -11
- package/dist/server/lap/describe.js.map +1 -1
- package/dist/server/lap/forward.d.ts.map +1 -1
- package/dist/server/lap/forward.js +12 -6
- package/dist/server/lap/forward.js.map +1 -1
- package/dist/server/lap/message.d.ts.map +1 -1
- package/dist/server/lap/message.js +4 -1
- package/dist/server/lap/message.js.map +1 -1
- package/dist/server/lap/observe.d.ts.map +1 -1
- package/dist/server/lap/observe.js +8 -3
- package/dist/server/lap/observe.js.map +1 -1
- package/dist/server/lap/paused.d.ts +30 -0
- package/dist/server/lap/paused.d.ts.map +1 -0
- package/dist/server/lap/paused.js +38 -0
- package/dist/server/lap/paused.js.map +1 -0
- package/dist/server/lap/wait.d.ts.map +1 -1
- package/dist/server/lap/wait.js +4 -1
- package/dist/server/lap/wait.js.map +1 -1
- package/dist/server/token-store.d.ts.map +1 -1
- package/dist/server/token-store.js +7 -0
- package/dist/server/token-store.js.map +1 -1
- package/package.json +1 -1
package/dist/client/factory.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AppHandle } from '@llui/dom';
|
|
2
2
|
import type { AgentEffect } from './effects.js';
|
|
3
3
|
import type { AgentConfirmState } from './agentConfirm.js';
|
|
4
|
-
import type { AgentDocs, AgentContext, MessageAnnotations } from '../protocol.js';
|
|
4
|
+
import type { AgentDocs, AgentContext, AgentToken, MessageAnnotations } from '../protocol.js';
|
|
5
5
|
import { type CodecRegistry } from '../codecs.js';
|
|
6
6
|
/**
|
|
7
7
|
* The shape the compiler emits as `__msgSchema`. Mirrors `MsgField`
|
|
@@ -105,7 +105,59 @@ export type CreateAgentClientOpts<State, Msg> = {
|
|
|
105
105
|
* because cloudflare-vite shadows non-`/cdn-cgi/*` routes.
|
|
106
106
|
*/
|
|
107
107
|
agentBasePath?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Storage adapter for the active session blob. When provided the
|
|
110
|
+
* framework owns the persist/restore loop end-to-end: writes on
|
|
111
|
+
* `MintSucceeded`, reads on `start()` (auto-dispatching
|
|
112
|
+
* `RestoreSession` when a non-expired blob is found), clears on
|
|
113
|
+
* `Disconnect` / `Revoke` / explicit clear effects.
|
|
114
|
+
*
|
|
115
|
+
* Default: `defaultSessionStorage()` — uses `window.sessionStorage`
|
|
116
|
+
* under the key `'llui-agent:session'`. Tab-scoped (survives
|
|
117
|
+
* refresh, dies on tab close), which matches how a single-tab
|
|
118
|
+
* agent connection should behave.
|
|
119
|
+
*
|
|
120
|
+
* Pass `null` to opt out entirely; the framework then emits the
|
|
121
|
+
* `AgentSessionPersist` / `AgentSessionClear` effects unchanged
|
|
122
|
+
* and the host owns storage. Useful for SSR builds where
|
|
123
|
+
* `sessionStorage` is undefined and the host wants to no-op the
|
|
124
|
+
* storage layer.
|
|
125
|
+
*
|
|
126
|
+
* Pass a custom adapter for tests, IndexedDB-backed apps, or
|
|
127
|
+
* environments where `sessionStorage` is unavailable but the
|
|
128
|
+
* persistence semantics are still wanted (e.g. Web Workers).
|
|
129
|
+
*/
|
|
130
|
+
sessionStorage?: AgentSessionStorage | null;
|
|
108
131
|
};
|
|
132
|
+
/**
|
|
133
|
+
* Tab-lifetime persistence for the active agent session. Reads /
|
|
134
|
+
* writes a single blob; the framework synchronizes it with the
|
|
135
|
+
* connect lifecycle so refresh-survival is automatic. Implementations
|
|
136
|
+
* must be synchronous on the read path so `start()` can decide
|
|
137
|
+
* whether to dispatch `RestoreSession` before any UI mounts —
|
|
138
|
+
* otherwise the `idle`-only guard in the reducer might miss the
|
|
139
|
+
* restore when a `Mint` click races the async lookup.
|
|
140
|
+
*/
|
|
141
|
+
export type AgentSessionStorage = {
|
|
142
|
+
read(): PersistedAgentSession | null;
|
|
143
|
+
write(session: PersistedAgentSession): void;
|
|
144
|
+
clear(): void;
|
|
145
|
+
};
|
|
146
|
+
export type PersistedAgentSession = {
|
|
147
|
+
token: AgentToken;
|
|
148
|
+
tid: string;
|
|
149
|
+
lapUrl: string;
|
|
150
|
+
wsUrl: string;
|
|
151
|
+
expiresAt: number;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* The default `AgentSessionStorage` — wraps `window.sessionStorage`
|
|
155
|
+
* under a single key and treats parse / type-mismatch failures as
|
|
156
|
+
* "no session". Returns `null` from the factory when `window` is
|
|
157
|
+
* undefined (SSR/tests), so calling code never has to feature-detect
|
|
158
|
+
* the browser environment itself.
|
|
159
|
+
*/
|
|
160
|
+
export declare function defaultSessionStorage(storageKey?: string): AgentSessionStorage | null;
|
|
109
161
|
export type AgentClient = {
|
|
110
162
|
effectHandler: (effect: AgentEffect) => Promise<void>;
|
|
111
163
|
start(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/client/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAC1D,OAAO,KAAK,EACV,SAAS,EACT,YAAY,
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/client/factory.ts"],"names":[],"mappings":"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"}
|
package/dist/client/factory.js
CHANGED
|
@@ -1,6 +1,82 @@
|
|
|
1
1
|
import { attachWsClient } from './ws-client.js';
|
|
2
2
|
import { createEffectHandler } from './effect-handler.js';
|
|
3
3
|
import { makeDefaultCodecs, encodeForWire, decodeFromWire } from '../codecs.js';
|
|
4
|
+
/**
|
|
5
|
+
* The default `AgentSessionStorage` — wraps `window.sessionStorage`
|
|
6
|
+
* under a single key and treats parse / type-mismatch failures as
|
|
7
|
+
* "no session". Returns `null` from the factory when `window` is
|
|
8
|
+
* undefined (SSR/tests), so calling code never has to feature-detect
|
|
9
|
+
* the browser environment itself.
|
|
10
|
+
*/
|
|
11
|
+
export function defaultSessionStorage(storageKey = 'llui-agent:session') {
|
|
12
|
+
if (typeof window === 'undefined' || typeof window.sessionStorage === 'undefined')
|
|
13
|
+
return null;
|
|
14
|
+
const ss = window.sessionStorage;
|
|
15
|
+
return {
|
|
16
|
+
read: () => {
|
|
17
|
+
let raw;
|
|
18
|
+
try {
|
|
19
|
+
raw = ss.getItem(storageKey);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (raw === null || raw === '')
|
|
25
|
+
return null;
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(raw);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
try {
|
|
32
|
+
ss.removeItem(storageKey);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* ignore */
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (!parsed || typeof parsed !== 'object')
|
|
40
|
+
return null;
|
|
41
|
+
const o = parsed;
|
|
42
|
+
if (typeof o.token !== 'string' ||
|
|
43
|
+
typeof o.tid !== 'string' ||
|
|
44
|
+
typeof o.lapUrl !== 'string' ||
|
|
45
|
+
typeof o.wsUrl !== 'string' ||
|
|
46
|
+
typeof o.expiresAt !== 'number') {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
// `expiresAt` is unix-seconds (server's mint endpoint floors
|
|
50
|
+
// ms→s). Compare to `Date.now() / 1000` so the same-units
|
|
51
|
+
// footgun the host code hit doesn't bite the framework too.
|
|
52
|
+
if (o.expiresAt * 1000 <= Date.now())
|
|
53
|
+
return null;
|
|
54
|
+
return {
|
|
55
|
+
token: o.token,
|
|
56
|
+
tid: o.tid,
|
|
57
|
+
lapUrl: o.lapUrl,
|
|
58
|
+
wsUrl: o.wsUrl,
|
|
59
|
+
expiresAt: o.expiresAt,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
write: (session) => {
|
|
63
|
+
try {
|
|
64
|
+
ss.setItem(storageKey, JSON.stringify(session));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* private mode / quota — non-fatal */
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
clear: () => {
|
|
71
|
+
try {
|
|
72
|
+
ss.removeItem(storageKey);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* ignore */
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
4
80
|
export function createAgentClient(opts) {
|
|
5
81
|
let ws = null;
|
|
6
82
|
let wsClient = null;
|
|
@@ -111,11 +187,21 @@ export function createAgentClient(opts) {
|
|
|
111
187
|
docs: opts.def.agentDocs ?? null,
|
|
112
188
|
schemaHash: opts.def.__schemaHash ?? '',
|
|
113
189
|
});
|
|
190
|
+
// Storage adapter: opt-out with `null`, custom adapter, or default
|
|
191
|
+
// to `sessionStorage` under the canonical key. The framework
|
|
192
|
+
// synchronizes it with the connect lifecycle so refresh-survival
|
|
193
|
+
// is automatic for any host that doesn't explicitly disable it.
|
|
194
|
+
const sessionStorage = opts.sessionStorage === null
|
|
195
|
+
? null
|
|
196
|
+
: opts.sessionStorage !== undefined
|
|
197
|
+
? opts.sessionStorage
|
|
198
|
+
: defaultSessionStorage();
|
|
114
199
|
const effectHandler = createEffectHandler({
|
|
115
200
|
send: (m) => opts.handle.send(m),
|
|
116
201
|
wrapAgentConnect: (m) => opts.slices.wrapConnectMsg(m),
|
|
117
202
|
forward: (payload) => opts.handle.send(payload),
|
|
118
203
|
agentBasePath: opts.agentBasePath,
|
|
204
|
+
sessionStorage,
|
|
119
205
|
openWs: (token, wsUrl) => {
|
|
120
206
|
if (ws)
|
|
121
207
|
ws.close();
|
|
@@ -174,6 +260,25 @@ export function createAgentClient(opts) {
|
|
|
174
260
|
wsClient?.emitStateUpdate('/', encodeForWire(state, codecs));
|
|
175
261
|
});
|
|
176
262
|
}
|
|
263
|
+
// Auto-restore from storage. If a non-expired session blob is
|
|
264
|
+
// present, dispatch RestoreSession synchronously so the connect
|
|
265
|
+
// flow re-enters `pending-claude` before any UI mounts. The
|
|
266
|
+
// reducer's `idle`-only guard means a host that ALSO dispatches
|
|
267
|
+
// its own RestoreSession (legacy hosts that wired storage
|
|
268
|
+
// themselves) won't double-fire — the second call is a no-op.
|
|
269
|
+
if (sessionStorage) {
|
|
270
|
+
const persisted = sessionStorage.read();
|
|
271
|
+
if (persisted) {
|
|
272
|
+
opts.handle.send(opts.slices.wrapConnectMsg({
|
|
273
|
+
type: 'RestoreSession',
|
|
274
|
+
token: persisted.token,
|
|
275
|
+
tid: persisted.tid,
|
|
276
|
+
lapUrl: persisted.lapUrl,
|
|
277
|
+
wsUrl: persisted.wsUrl,
|
|
278
|
+
expiresAt: persisted.expiresAt,
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
177
282
|
installErrorListeners();
|
|
178
283
|
// Catch per-binding throws into drain.errors so a single bad
|
|
179
284
|
// binding doesn't blank the page AND the agent learns about it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/client/factory.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AppHandle } from '@llui/dom'\nimport type { AgentEffect } from './effects.js'\nimport type { AgentConfirmState } from './agentConfirm.js'\nimport type {\n AgentDocs,\n AgentContext,\n LapDrainMeta,\n MessageAnnotations,\n MessageSchemaEntry,\n} from '../protocol.js'\nimport { attachWsClient, type WsLike, type RpcHosts } from './ws-client.js'\nimport { createEffectHandler } from './effect-handler.js'\nimport { makeDefaultCodecs, encodeForWire, decodeFromWire, type CodecRegistry } from '../codecs.js'\n\n/**\n * The shape the compiler emits as `__msgSchema`. Mirrors `MsgField`\n * from `@llui/vite-plugin/src/msg-schema.ts`. Three coexisting forms:\n *\n * 1. Bare primitive: `'string' | 'number' | 'boolean' | 'unknown'`\n * and bare enum: `{enum: [...]}` (values may be string, number,\n * or boolean — the compiler preserves the literal kind so JSON\n * round-trips don't lose type info).\n * 2. Bare nested types: `{kind: 'object', shape}` for inline /\n * followed-via-typeIndex shapes; `{kind: 'array', element}` for\n * `T[]` / `readonly T[]` / `Array<T>`; `{kind: 'discriminated-\n * union', discriminant, variants}` for tagged unions of objects\n * (e.g. `Format = {kind:'exact'} | {kind:'range', min, max}`).\n * The synthesizer recurses to build copy-paste-ready nested\n * examples; the validator walks the same tree.\n * 3. Rich descriptor: wraps any of the above with `{optional?,\n * priority?, hint?}` carrying TS optionality and `@should` hints.\n */\nexport type MsgSchemaBareType =\n | string\n | { enum: ReadonlyArray<string | number | boolean> }\n | { kind: 'object'; shape: Record<string, MsgSchemaField> }\n | { kind: 'array'; element: MsgSchemaBareType }\n | {\n kind: 'discriminated-union'\n discriminant: string\n variants: Record<string, Record<string, MsgSchemaField>>\n }\n\nexport type MsgSchemaField =\n | MsgSchemaBareType\n | {\n type: MsgSchemaBareType\n optional?: boolean\n priority?: 'should'\n hint?: string\n /**\n * Boolean JS expression authored with `@validates(\"expr\")` JSDoc.\n * Has `v` bound to the field value at runtime; the validator\n * compiles it lazily with `new Function('v', 'return (' + src +\n * ')')` and caches the function across calls. Use for invariants\n * the type system can't express — numeric ranges, format\n * predicates, length bounds.\n */\n validates?: string\n }\n\nexport type MsgSchemaShape = {\n discriminant: string\n variants: Record<string, Record<string, MsgSchemaField>>\n}\n\ntype ComponentMetadata = {\n __msgSchema?: unknown\n __stateSchema?: unknown\n __msgAnnotations?: Record<string, MessageAnnotations>\n __schemaHash?: string\n name: string\n agentAffordances?: (state: unknown) => Array<{ type: string; [k: string]: unknown }>\n agentDocs?: AgentDocs\n agentContext?: (state: unknown) => AgentContext\n}\n\nexport type CreateAgentClientOpts<State, Msg> = {\n handle: AppHandle\n def: ComponentMetadata\n appVersion?: string\n rootElement: Element | null\n slices: {\n getConnect: (s: State) => unknown\n getConfirm: (s: State) => AgentConfirmState\n wrapConnectMsg: (m: unknown) => Msg\n wrapConfirmMsg: (m: unknown) => Msg\n /**\n * Optional: wrap an agentLog msg so the client-side activity feed\n * mirrors what Claude is doing. If omitted, outbound log-append\n * frames still go to the server, but the local agent.log slice\n * stays empty (the UI won't show activity).\n */\n wrapLogMsg?: (m: unknown) => Msg\n }\n /**\n * Codec registry for non-JSON-safe values (Date, Blob, Map, …)\n * crossing the LAP boundary. Defaults to `makeDefaultCodecs()`\n * which ships `iso-date` and `epoch-millis`. Provide a custom\n * registry to register additional codecs (e.g. `base64-blob` for\n * file uploads). See `@llui/agent/codecs` for the convention.\n */\n codecs?: CodecRegistry\n /**\n * Base path for agent HTTP endpoints. Default: `'/agent'` (matches\n * the canonical paths in `@llui/vite-plugin`'s dev middleware and\n * `@llui/agent/server`). The mint URL, resume URLs, and revoke URL\n * derive from this so consumers don't have to keep them in sync.\n *\n * Override when:\n * - **Cross-origin agent server**: pass the full base, e.g.\n * `'https://api.example.com/agent'` or `'http://localhost:8787/agent'`.\n * - **`@cloudflare/vite-plugin` in dev**: pass `'/cdn-cgi/agent'`\n * because cloudflare-vite shadows non-`/cdn-cgi/*` routes.\n */\n agentBasePath?: string\n}\n\nexport type AgentClient = {\n effectHandler: (effect: AgentEffect) => Promise<void>\n start(): void\n stop(): void\n}\n\nexport function createAgentClient<State, Msg>(\n opts: CreateAgentClientOpts<State, Msg>,\n): AgentClient {\n let ws: WebSocket | null = null\n let wsClient: ReturnType<typeof attachWsClient> | null = null\n let confirmPollTimer: ReturnType<typeof setInterval> | null = null\n let stateSubscription: (() => void) | null = null\n const resolvedConfirms = new Set<string>()\n\n // Drain-error buffer: populated by persistent `window.error` and\n // `window.unhandledrejection` listeners installed on `start()`. The\n // send-message drain loop consumes and clears it per call so the\n // envelope surfaces only errors that fired during that window.\n const drainErrors: LapDrainMeta['errors'] = []\n let errorListenersInstalled = false\n let onErrorEvt: ((e: ErrorEvent) => void) | null = null\n let onRejectionEvt: ((e: PromiseRejectionEvent) => void) | null = null\n\n function installErrorListeners(): void {\n if (errorListenersInstalled) return\n if (typeof window === 'undefined') return\n onErrorEvt = (e: ErrorEvent) => {\n drainErrors.push({\n kind: 'error',\n message: e.message ?? String(e.error ?? 'unknown error'),\n stack: e.error instanceof Error ? e.error.stack : undefined,\n })\n }\n onRejectionEvt = (e: PromiseRejectionEvent) => {\n const r = e.reason\n const message = r instanceof Error ? r.message : typeof r === 'string' ? r : safeStringify(r)\n drainErrors.push({\n kind: 'unhandledrejection',\n message,\n stack: r instanceof Error ? r.stack : undefined,\n })\n }\n window.addEventListener('error', onErrorEvt)\n window.addEventListener('unhandledrejection', onRejectionEvt)\n errorListenersInstalled = true\n }\n\n function removeErrorListeners(): void {\n if (!errorListenersInstalled) return\n if (typeof window === 'undefined') return\n if (onErrorEvt) window.removeEventListener('error', onErrorEvt)\n if (onRejectionEvt) window.removeEventListener('unhandledrejection', onRejectionEvt)\n onErrorEvt = null\n onRejectionEvt = null\n errorListenersInstalled = false\n }\n\n // Codec registry handles non-JSON-safe values (Date, etc.) crossing\n // the LAP boundary. `getState` encodes outgoing snapshots; `send`\n // decodes incoming agent messages before they hit the reducer. The\n // tagged-value convention is documented in `@llui/agent/codecs`.\n const codecs = opts.codecs ?? makeDefaultCodecs()\n\n // Track the most recent send_message outcome so `describe_context`\n // can prepend a synthetic hint about it. Apps used to roll their\n // own `lastDispatchError` state field; the framework now owns it.\n let lastDispatchOutcome: import('./rpc/describe-context.js').LastDispatchOutcome | null = null\n\n const rpcHost: RpcHosts = {\n getState: () => encodeForWire(opts.handle.getState(), codecs),\n send: (m) => opts.handle.send(decodeFromWire(m, codecs)),\n flush: () => opts.handle.flush(),\n subscribe: (listener) => opts.handle.subscribe(() => listener()),\n getAndClearDrainErrors: () => drainErrors.splice(0, drainErrors.length),\n getMsgAnnotations: () => opts.def.__msgAnnotations ?? null,\n // The compiler-injected message schema. Used by `list_actions` to\n // synthesize payload examples for `@agentOnly` variants that have\n // no live UI binding — the agent should still see them as\n // affordances even though no human can click them.\n getMsgSchema: () => (opts.def.__msgSchema as MsgSchemaShape | undefined) ?? null,\n // Run the reducer in isolation for `would_dispatch`. Wraps the\n // AppHandle's same-named method so the host doesn't need a direct\n // reference to the live ComponentInstance.\n runReducer: (msg) => opts.handle.runReducer(msg),\n // Live binding descriptors: read from the runtime registry that\n // tracks which Msg variants are dispatchable from currently-mounted\n // event handlers. Empty array when the app wasn't compiled with\n // agent metadata (no tagger pass) or has no view bindings yet —\n // both produce the same \"no live affordances\" signal at the agent\n // layer.\n getBindingDescriptors: () => opts.handle.getBindingDescriptors(),\n getAgentAffordances: () => opts.def.agentAffordances ?? null,\n getAgentContext: () => opts.def.agentContext ?? null,\n getLastDispatchOutcome: () => lastDispatchOutcome,\n getRootElement: () => opts.rootElement,\n proposeConfirm: (entry) => {\n opts.handle.send(opts.slices.wrapConfirmMsg({ type: 'Propose', entry }))\n },\n }\n\n // Exposed so the WS client can update on each send_message reply.\n // Closure scope keeps the field write-protected — only the WS layer\n // mutates it; everyone else reads through the getter above.\n const recordDispatchOutcome = (outcome: typeof lastDispatchOutcome): void => {\n lastDispatchOutcome = outcome\n }\n\n const helloBuilder = () => ({\n t: 'hello' as const,\n appName: opts.def.name,\n appVersion: opts.appVersion ?? '0.0.0',\n msgSchema: (opts.def.__msgSchema ?? {}) as Record<string, MessageSchemaEntry>,\n stateSchema: (opts.def.__stateSchema ?? {}) as object,\n affordancesSample: opts.def.agentAffordances\n ? opts.def.agentAffordances(opts.handle.getState())\n : [],\n docs: opts.def.agentDocs ?? null,\n schemaHash: opts.def.__schemaHash ?? '',\n })\n\n const effectHandler = createEffectHandler({\n send: (m) => opts.handle.send(m),\n wrapAgentConnect: (m) => opts.slices.wrapConnectMsg(m),\n forward: (payload) => opts.handle.send(payload),\n agentBasePath: opts.agentBasePath,\n openWs: (token, wsUrl) => {\n if (ws) ws.close()\n ws = new WebSocket(`${wsUrl}?token=${encodeURIComponent(token)}`)\n wsClient = attachWsClient(ws as unknown as WsLike, rpcHost, helloBuilder, {\n onActivated: () => {\n opts.handle.send(opts.slices.wrapConnectMsg({ type: 'ActivatedByClaude' }))\n },\n onLogEntry: opts.slices.wrapLogMsg\n ? (entry) => {\n opts.handle.send(opts.slices.wrapLogMsg!({ type: 'Append', entry }))\n }\n : undefined,\n onDispatchOutcome: recordDispatchOutcome,\n })\n ws.addEventListener('open', () => {\n opts.handle.send(opts.slices.wrapConnectMsg({ type: 'WsOpened' }))\n })\n ws.addEventListener('close', () => {\n opts.handle.send(opts.slices.wrapConnectMsg({ type: 'WsClosed' }))\n })\n },\n closeWs: () => {\n wsClient?.close()\n ws = null\n wsClient = null\n },\n })\n\n const pollConfirms = () => {\n const state = opts.handle.getState() as State\n const confirm = opts.slices.getConfirm(state)\n for (const entry of confirm.pending) {\n if (entry.status === 'pending') continue\n if (resolvedConfirms.has(entry.id)) continue\n resolvedConfirms.add(entry.id)\n if (entry.status === 'approved') {\n wsClient?.resolveConfirm(\n entry.id,\n 'confirmed',\n encodeForWire(opts.handle.getState(), codecs),\n )\n } else if (entry.status === 'rejected') {\n wsClient?.resolveConfirm(entry.id, 'user-cancelled')\n }\n }\n }\n\n return {\n effectHandler,\n start() {\n if (!confirmPollTimer) confirmPollTimer = setInterval(pollConfirms, 200)\n if (!stateSubscription) {\n stateSubscription = opts.handle.subscribe((state) => {\n // Same codec convention as `getState`: outgoing snapshots\n // pass through the encoder so non-JSON-safe values (Date,\n // etc.) become tagged-wire form.\n wsClient?.emitStateUpdate('/', encodeForWire(state, codecs))\n })\n }\n installErrorListeners()\n // Catch per-binding throws into drain.errors so a single bad\n // binding doesn't blank the page AND the agent learns about it.\n // Runtime contract: leaves the binding's `lastValue` unchanged\n // (DOM stays at last-rendered value), continues with siblings,\n // calls this hook once per binding throw.\n opts.handle.setOnBindingError((info) => {\n drainErrors.push({\n kind: 'error',\n message: `[binding ${info.kind}${info.key ? `:${info.key}` : ''}] ${info.message}`,\n stack: info.stack,\n })\n })\n },\n stop() {\n if (confirmPollTimer) clearInterval(confirmPollTimer)\n confirmPollTimer = null\n if (stateSubscription) {\n stateSubscription()\n stateSubscription = null\n }\n removeErrorListeners()\n opts.handle.setOnBindingError(null)\n drainErrors.length = 0\n wsClient?.close()\n },\n }\n}\n\nfunction safeStringify(v: unknown): string {\n try {\n return JSON.stringify(v)\n } catch {\n return String(v)\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/client/factory.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AppHandle } from '@llui/dom'\nimport type { AgentEffect } from './effects.js'\nimport type { AgentConfirmState } from './agentConfirm.js'\nimport type {\n AgentDocs,\n AgentContext,\n AgentToken,\n LapDrainMeta,\n MessageAnnotations,\n MessageSchemaEntry,\n} from '../protocol.js'\nimport { attachWsClient, type WsLike, type RpcHosts } from './ws-client.js'\nimport { createEffectHandler } from './effect-handler.js'\nimport { makeDefaultCodecs, encodeForWire, decodeFromWire, type CodecRegistry } from '../codecs.js'\n\n/**\n * The shape the compiler emits as `__msgSchema`. Mirrors `MsgField`\n * from `@llui/vite-plugin/src/msg-schema.ts`. Three coexisting forms:\n *\n * 1. Bare primitive: `'string' | 'number' | 'boolean' | 'unknown'`\n * and bare enum: `{enum: [...]}` (values may be string, number,\n * or boolean — the compiler preserves the literal kind so JSON\n * round-trips don't lose type info).\n * 2. Bare nested types: `{kind: 'object', shape}` for inline /\n * followed-via-typeIndex shapes; `{kind: 'array', element}` for\n * `T[]` / `readonly T[]` / `Array<T>`; `{kind: 'discriminated-\n * union', discriminant, variants}` for tagged unions of objects\n * (e.g. `Format = {kind:'exact'} | {kind:'range', min, max}`).\n * The synthesizer recurses to build copy-paste-ready nested\n * examples; the validator walks the same tree.\n * 3. Rich descriptor: wraps any of the above with `{optional?,\n * priority?, hint?}` carrying TS optionality and `@should` hints.\n */\nexport type MsgSchemaBareType =\n | string\n | { enum: ReadonlyArray<string | number | boolean> }\n | { kind: 'object'; shape: Record<string, MsgSchemaField> }\n | { kind: 'array'; element: MsgSchemaBareType }\n | {\n kind: 'discriminated-union'\n discriminant: string\n variants: Record<string, Record<string, MsgSchemaField>>\n }\n\nexport type MsgSchemaField =\n | MsgSchemaBareType\n | {\n type: MsgSchemaBareType\n optional?: boolean\n priority?: 'should'\n hint?: string\n /**\n * Boolean JS expression authored with `@validates(\"expr\")` JSDoc.\n * Has `v` bound to the field value at runtime; the validator\n * compiles it lazily with `new Function('v', 'return (' + src +\n * ')')` and caches the function across calls. Use for invariants\n * the type system can't express — numeric ranges, format\n * predicates, length bounds.\n */\n validates?: string\n }\n\nexport type MsgSchemaShape = {\n discriminant: string\n variants: Record<string, Record<string, MsgSchemaField>>\n}\n\ntype ComponentMetadata = {\n __msgSchema?: unknown\n __stateSchema?: unknown\n __msgAnnotations?: Record<string, MessageAnnotations>\n __schemaHash?: string\n name: string\n agentAffordances?: (state: unknown) => Array<{ type: string; [k: string]: unknown }>\n agentDocs?: AgentDocs\n agentContext?: (state: unknown) => AgentContext\n}\n\nexport type CreateAgentClientOpts<State, Msg> = {\n handle: AppHandle\n def: ComponentMetadata\n appVersion?: string\n rootElement: Element | null\n slices: {\n getConnect: (s: State) => unknown\n getConfirm: (s: State) => AgentConfirmState\n wrapConnectMsg: (m: unknown) => Msg\n wrapConfirmMsg: (m: unknown) => Msg\n /**\n * Optional: wrap an agentLog msg so the client-side activity feed\n * mirrors what Claude is doing. If omitted, outbound log-append\n * frames still go to the server, but the local agent.log slice\n * stays empty (the UI won't show activity).\n */\n wrapLogMsg?: (m: unknown) => Msg\n }\n /**\n * Codec registry for non-JSON-safe values (Date, Blob, Map, …)\n * crossing the LAP boundary. Defaults to `makeDefaultCodecs()`\n * which ships `iso-date` and `epoch-millis`. Provide a custom\n * registry to register additional codecs (e.g. `base64-blob` for\n * file uploads). See `@llui/agent/codecs` for the convention.\n */\n codecs?: CodecRegistry\n /**\n * Base path for agent HTTP endpoints. Default: `'/agent'` (matches\n * the canonical paths in `@llui/vite-plugin`'s dev middleware and\n * `@llui/agent/server`). The mint URL, resume URLs, and revoke URL\n * derive from this so consumers don't have to keep them in sync.\n *\n * Override when:\n * - **Cross-origin agent server**: pass the full base, e.g.\n * `'https://api.example.com/agent'` or `'http://localhost:8787/agent'`.\n * - **`@cloudflare/vite-plugin` in dev**: pass `'/cdn-cgi/agent'`\n * because cloudflare-vite shadows non-`/cdn-cgi/*` routes.\n */\n agentBasePath?: string\n /**\n * Storage adapter for the active session blob. When provided the\n * framework owns the persist/restore loop end-to-end: writes on\n * `MintSucceeded`, reads on `start()` (auto-dispatching\n * `RestoreSession` when a non-expired blob is found), clears on\n * `Disconnect` / `Revoke` / explicit clear effects.\n *\n * Default: `defaultSessionStorage()` — uses `window.sessionStorage`\n * under the key `'llui-agent:session'`. Tab-scoped (survives\n * refresh, dies on tab close), which matches how a single-tab\n * agent connection should behave.\n *\n * Pass `null` to opt out entirely; the framework then emits the\n * `AgentSessionPersist` / `AgentSessionClear` effects unchanged\n * and the host owns storage. Useful for SSR builds where\n * `sessionStorage` is undefined and the host wants to no-op the\n * storage layer.\n *\n * Pass a custom adapter for tests, IndexedDB-backed apps, or\n * environments where `sessionStorage` is unavailable but the\n * persistence semantics are still wanted (e.g. Web Workers).\n */\n sessionStorage?: AgentSessionStorage | null\n}\n\n/**\n * Tab-lifetime persistence for the active agent session. Reads /\n * writes a single blob; the framework synchronizes it with the\n * connect lifecycle so refresh-survival is automatic. Implementations\n * must be synchronous on the read path so `start()` can decide\n * whether to dispatch `RestoreSession` before any UI mounts —\n * otherwise the `idle`-only guard in the reducer might miss the\n * restore when a `Mint` click races the async lookup.\n */\nexport type AgentSessionStorage = {\n read(): PersistedAgentSession | null\n write(session: PersistedAgentSession): void\n clear(): void\n}\n\nexport type PersistedAgentSession = {\n token: AgentToken\n tid: string\n lapUrl: string\n wsUrl: string\n expiresAt: number\n}\n\n/**\n * The default `AgentSessionStorage` — wraps `window.sessionStorage`\n * under a single key and treats parse / type-mismatch failures as\n * \"no session\". Returns `null` from the factory when `window` is\n * undefined (SSR/tests), so calling code never has to feature-detect\n * the browser environment itself.\n */\nexport function defaultSessionStorage(\n storageKey: string = 'llui-agent:session',\n): AgentSessionStorage | null {\n if (typeof window === 'undefined' || typeof window.sessionStorage === 'undefined') return null\n const ss = window.sessionStorage\n return {\n read: () => {\n let raw: string | null\n try {\n raw = ss.getItem(storageKey)\n } catch {\n return null\n }\n if (raw === null || raw === '') return null\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n try {\n ss.removeItem(storageKey)\n } catch {\n /* ignore */\n }\n return null\n }\n if (!parsed || typeof parsed !== 'object') return null\n const o = parsed as Record<string, unknown>\n if (\n typeof o.token !== 'string' ||\n typeof o.tid !== 'string' ||\n typeof o.lapUrl !== 'string' ||\n typeof o.wsUrl !== 'string' ||\n typeof o.expiresAt !== 'number'\n ) {\n return null\n }\n // `expiresAt` is unix-seconds (server's mint endpoint floors\n // ms→s). Compare to `Date.now() / 1000` so the same-units\n // footgun the host code hit doesn't bite the framework too.\n if (o.expiresAt * 1000 <= Date.now()) return null\n return {\n token: o.token as AgentToken,\n tid: o.tid,\n lapUrl: o.lapUrl,\n wsUrl: o.wsUrl,\n expiresAt: o.expiresAt,\n }\n },\n write: (session) => {\n try {\n ss.setItem(storageKey, JSON.stringify(session))\n } catch {\n /* private mode / quota — non-fatal */\n }\n },\n clear: () => {\n try {\n ss.removeItem(storageKey)\n } catch {\n /* ignore */\n }\n },\n }\n}\n\nexport type AgentClient = {\n effectHandler: (effect: AgentEffect) => Promise<void>\n start(): void\n stop(): void\n}\n\nexport function createAgentClient<State, Msg>(\n opts: CreateAgentClientOpts<State, Msg>,\n): AgentClient {\n let ws: WebSocket | null = null\n let wsClient: ReturnType<typeof attachWsClient> | null = null\n let confirmPollTimer: ReturnType<typeof setInterval> | null = null\n let stateSubscription: (() => void) | null = null\n const resolvedConfirms = new Set<string>()\n\n // Drain-error buffer: populated by persistent `window.error` and\n // `window.unhandledrejection` listeners installed on `start()`. The\n // send-message drain loop consumes and clears it per call so the\n // envelope surfaces only errors that fired during that window.\n const drainErrors: LapDrainMeta['errors'] = []\n let errorListenersInstalled = false\n let onErrorEvt: ((e: ErrorEvent) => void) | null = null\n let onRejectionEvt: ((e: PromiseRejectionEvent) => void) | null = null\n\n function installErrorListeners(): void {\n if (errorListenersInstalled) return\n if (typeof window === 'undefined') return\n onErrorEvt = (e: ErrorEvent) => {\n drainErrors.push({\n kind: 'error',\n message: e.message ?? String(e.error ?? 'unknown error'),\n stack: e.error instanceof Error ? e.error.stack : undefined,\n })\n }\n onRejectionEvt = (e: PromiseRejectionEvent) => {\n const r = e.reason\n const message = r instanceof Error ? r.message : typeof r === 'string' ? r : safeStringify(r)\n drainErrors.push({\n kind: 'unhandledrejection',\n message,\n stack: r instanceof Error ? r.stack : undefined,\n })\n }\n window.addEventListener('error', onErrorEvt)\n window.addEventListener('unhandledrejection', onRejectionEvt)\n errorListenersInstalled = true\n }\n\n function removeErrorListeners(): void {\n if (!errorListenersInstalled) return\n if (typeof window === 'undefined') return\n if (onErrorEvt) window.removeEventListener('error', onErrorEvt)\n if (onRejectionEvt) window.removeEventListener('unhandledrejection', onRejectionEvt)\n onErrorEvt = null\n onRejectionEvt = null\n errorListenersInstalled = false\n }\n\n // Codec registry handles non-JSON-safe values (Date, etc.) crossing\n // the LAP boundary. `getState` encodes outgoing snapshots; `send`\n // decodes incoming agent messages before they hit the reducer. The\n // tagged-value convention is documented in `@llui/agent/codecs`.\n const codecs = opts.codecs ?? makeDefaultCodecs()\n\n // Track the most recent send_message outcome so `describe_context`\n // can prepend a synthetic hint about it. Apps used to roll their\n // own `lastDispatchError` state field; the framework now owns it.\n let lastDispatchOutcome: import('./rpc/describe-context.js').LastDispatchOutcome | null = null\n\n const rpcHost: RpcHosts = {\n getState: () => encodeForWire(opts.handle.getState(), codecs),\n send: (m) => opts.handle.send(decodeFromWire(m, codecs)),\n flush: () => opts.handle.flush(),\n subscribe: (listener) => opts.handle.subscribe(() => listener()),\n getAndClearDrainErrors: () => drainErrors.splice(0, drainErrors.length),\n getMsgAnnotations: () => opts.def.__msgAnnotations ?? null,\n // The compiler-injected message schema. Used by `list_actions` to\n // synthesize payload examples for `@agentOnly` variants that have\n // no live UI binding — the agent should still see them as\n // affordances even though no human can click them.\n getMsgSchema: () => (opts.def.__msgSchema as MsgSchemaShape | undefined) ?? null,\n // Run the reducer in isolation for `would_dispatch`. Wraps the\n // AppHandle's same-named method so the host doesn't need a direct\n // reference to the live ComponentInstance.\n runReducer: (msg) => opts.handle.runReducer(msg),\n // Live binding descriptors: read from the runtime registry that\n // tracks which Msg variants are dispatchable from currently-mounted\n // event handlers. Empty array when the app wasn't compiled with\n // agent metadata (no tagger pass) or has no view bindings yet —\n // both produce the same \"no live affordances\" signal at the agent\n // layer.\n getBindingDescriptors: () => opts.handle.getBindingDescriptors(),\n getAgentAffordances: () => opts.def.agentAffordances ?? null,\n getAgentContext: () => opts.def.agentContext ?? null,\n getLastDispatchOutcome: () => lastDispatchOutcome,\n getRootElement: () => opts.rootElement,\n proposeConfirm: (entry) => {\n opts.handle.send(opts.slices.wrapConfirmMsg({ type: 'Propose', entry }))\n },\n }\n\n // Exposed so the WS client can update on each send_message reply.\n // Closure scope keeps the field write-protected — only the WS layer\n // mutates it; everyone else reads through the getter above.\n const recordDispatchOutcome = (outcome: typeof lastDispatchOutcome): void => {\n lastDispatchOutcome = outcome\n }\n\n const helloBuilder = () => ({\n t: 'hello' as const,\n appName: opts.def.name,\n appVersion: opts.appVersion ?? '0.0.0',\n msgSchema: (opts.def.__msgSchema ?? {}) as Record<string, MessageSchemaEntry>,\n stateSchema: (opts.def.__stateSchema ?? {}) as object,\n affordancesSample: opts.def.agentAffordances\n ? opts.def.agentAffordances(opts.handle.getState())\n : [],\n docs: opts.def.agentDocs ?? null,\n schemaHash: opts.def.__schemaHash ?? '',\n })\n\n // Storage adapter: opt-out with `null`, custom adapter, or default\n // to `sessionStorage` under the canonical key. The framework\n // synchronizes it with the connect lifecycle so refresh-survival\n // is automatic for any host that doesn't explicitly disable it.\n const sessionStorage =\n opts.sessionStorage === null\n ? null\n : opts.sessionStorage !== undefined\n ? opts.sessionStorage\n : defaultSessionStorage()\n\n const effectHandler = createEffectHandler({\n send: (m) => opts.handle.send(m),\n wrapAgentConnect: (m) => opts.slices.wrapConnectMsg(m),\n forward: (payload) => opts.handle.send(payload),\n agentBasePath: opts.agentBasePath,\n sessionStorage,\n openWs: (token, wsUrl) => {\n if (ws) ws.close()\n ws = new WebSocket(`${wsUrl}?token=${encodeURIComponent(token)}`)\n wsClient = attachWsClient(ws as unknown as WsLike, rpcHost, helloBuilder, {\n onActivated: () => {\n opts.handle.send(opts.slices.wrapConnectMsg({ type: 'ActivatedByClaude' }))\n },\n onLogEntry: opts.slices.wrapLogMsg\n ? (entry) => {\n opts.handle.send(opts.slices.wrapLogMsg!({ type: 'Append', entry }))\n }\n : undefined,\n onDispatchOutcome: recordDispatchOutcome,\n })\n ws.addEventListener('open', () => {\n opts.handle.send(opts.slices.wrapConnectMsg({ type: 'WsOpened' }))\n })\n ws.addEventListener('close', () => {\n opts.handle.send(opts.slices.wrapConnectMsg({ type: 'WsClosed' }))\n })\n },\n closeWs: () => {\n wsClient?.close()\n ws = null\n wsClient = null\n },\n })\n\n const pollConfirms = () => {\n const state = opts.handle.getState() as State\n const confirm = opts.slices.getConfirm(state)\n for (const entry of confirm.pending) {\n if (entry.status === 'pending') continue\n if (resolvedConfirms.has(entry.id)) continue\n resolvedConfirms.add(entry.id)\n if (entry.status === 'approved') {\n wsClient?.resolveConfirm(\n entry.id,\n 'confirmed',\n encodeForWire(opts.handle.getState(), codecs),\n )\n } else if (entry.status === 'rejected') {\n wsClient?.resolveConfirm(entry.id, 'user-cancelled')\n }\n }\n }\n\n return {\n effectHandler,\n start() {\n if (!confirmPollTimer) confirmPollTimer = setInterval(pollConfirms, 200)\n if (!stateSubscription) {\n stateSubscription = opts.handle.subscribe((state) => {\n // Same codec convention as `getState`: outgoing snapshots\n // pass through the encoder so non-JSON-safe values (Date,\n // etc.) become tagged-wire form.\n wsClient?.emitStateUpdate('/', encodeForWire(state, codecs))\n })\n }\n // Auto-restore from storage. If a non-expired session blob is\n // present, dispatch RestoreSession synchronously so the connect\n // flow re-enters `pending-claude` before any UI mounts. The\n // reducer's `idle`-only guard means a host that ALSO dispatches\n // its own RestoreSession (legacy hosts that wired storage\n // themselves) won't double-fire — the second call is a no-op.\n if (sessionStorage) {\n const persisted = sessionStorage.read()\n if (persisted) {\n opts.handle.send(\n opts.slices.wrapConnectMsg({\n type: 'RestoreSession',\n token: persisted.token,\n tid: persisted.tid,\n lapUrl: persisted.lapUrl,\n wsUrl: persisted.wsUrl,\n expiresAt: persisted.expiresAt,\n }),\n )\n }\n }\n installErrorListeners()\n // Catch per-binding throws into drain.errors so a single bad\n // binding doesn't blank the page AND the agent learns about it.\n // Runtime contract: leaves the binding's `lastValue` unchanged\n // (DOM stays at last-rendered value), continues with siblings,\n // calls this hook once per binding throw.\n opts.handle.setOnBindingError((info) => {\n drainErrors.push({\n kind: 'error',\n message: `[binding ${info.kind}${info.key ? `:${info.key}` : ''}] ${info.message}`,\n stack: info.stack,\n })\n })\n },\n stop() {\n if (confirmPollTimer) clearInterval(confirmPollTimer)\n confirmPollTimer = null\n if (stateSubscription) {\n stateSubscription()\n stateSubscription = null\n }\n removeErrorListeners()\n opts.handle.setOnBindingError(null)\n drainErrors.length = 0\n wsClient?.close()\n },\n }\n}\n\nfunction safeStringify(v: unknown): string {\n try {\n return JSON.stringify(v)\n } catch {\n return String(v)\n }\n}\n"]}
|
package/dist/server/core.d.ts
CHANGED
|
@@ -32,6 +32,25 @@ export type CoreOptions = {
|
|
|
32
32
|
* Durable Object that persists across isolates) pass it here.
|
|
33
33
|
*/
|
|
34
34
|
registry?: PairingRegistry;
|
|
35
|
+
/**
|
|
36
|
+
* How long, in milliseconds, a token's record stays in
|
|
37
|
+
* `pending-resume` after the WS pairing closes. During this window
|
|
38
|
+
* the same browser can reconnect with the same bearer token and
|
|
39
|
+
* the WS re-pairs without going through the rotate-on-resume path
|
|
40
|
+
* (`/resume/claim`). The agent's existing token stays valid the
|
|
41
|
+
* whole time, so brief network drops, page reloads, and quick
|
|
42
|
+
* server restarts don't invalidate the agent's session.
|
|
43
|
+
*
|
|
44
|
+
* After the window, LAP calls report `X-LLui-Reconnect: expired`
|
|
45
|
+
* and the record becomes resume-claimable (rotation required).
|
|
46
|
+
* Set to `0` to opt out — the WS close immediately drops the
|
|
47
|
+
* record and any reconnect must go through `/resume/claim`.
|
|
48
|
+
*
|
|
49
|
+
* Default: 60 seconds — long enough for laptop sleep, brief Wi-Fi
|
|
50
|
+
* flicker, and a server restart; short enough that a deliberately-
|
|
51
|
+
* closed tab doesn't keep the record alive forever.
|
|
52
|
+
*/
|
|
53
|
+
pendingResumeGraceMs?: number;
|
|
35
54
|
};
|
|
36
55
|
export type AcceptResult = {
|
|
37
56
|
ok: true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/server/core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAWlF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;IACnC,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/server/core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAWlF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;IACnC,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B;;;;;;;;;;;;;;;;;OAiBG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,aAAa,GAAG,SAAS,CAAA;CAAE,CAAA;AAElE;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IAClD,QAAQ,EAAE,eAAe,CAAA;IACzB,UAAU,EAAE,UAAU,CAAA;IACtB,SAAS,EAAE,SAAS,CAAA;IACpB;;;;;;;;;;OAUG;IACH,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;CACpF,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,WAAgB,GAAG,eAAe,CA2G3E"}
|
package/dist/server/core.js
CHANGED
|
@@ -19,6 +19,7 @@ export function createLluiAgentCore(opts = {}) {
|
|
|
19
19
|
const auditSink = opts.auditSink ?? consoleAuditSink;
|
|
20
20
|
const rateLimiter = opts.rateLimiter ?? defaultRateLimiter({ perBucket: '30/minute' });
|
|
21
21
|
const lapBasePath = opts.lapBasePath ?? '/agent/lap/v1';
|
|
22
|
+
const pendingResumeGraceMs = opts.pendingResumeGraceMs ?? 60_000;
|
|
22
23
|
const registry = opts.registry ??
|
|
23
24
|
new InMemoryPairingRegistry({
|
|
24
25
|
onLogAppend: (tid, entry) => {
|
|
@@ -67,16 +68,48 @@ export function createLluiAgentCore(opts = {}) {
|
|
|
67
68
|
return { ok: false, status: 401, code: 'auth-failed' };
|
|
68
69
|
if (rec.status === 'revoked')
|
|
69
70
|
return { ok: false, status: 403, code: 'revoked' };
|
|
71
|
+
// Reject `pending-resume` records past their grace window — the
|
|
72
|
+
// agent has to go through `/resume/claim` (which rotates the
|
|
73
|
+
// bearer) for those, since the long-gap path can't assume the
|
|
74
|
+
// previous bearer wasn't leaked.
|
|
75
|
+
if (rec.status === 'pending-resume' &&
|
|
76
|
+
rec.pendingResumeUntil !== null &&
|
|
77
|
+
rec.pendingResumeUntil <= Date.now()) {
|
|
78
|
+
return { ok: false, status: 401, code: 'auth-failed' };
|
|
79
|
+
}
|
|
70
80
|
const tid = rec.tid;
|
|
81
|
+
const isRepair = rec.status === 'pending-resume';
|
|
71
82
|
registry.register(tid, conn);
|
|
72
83
|
const nowMs = Date.now();
|
|
73
|
-
|
|
84
|
+
if (isRepair) {
|
|
85
|
+
// Same browser came back within the grace window — re-pair
|
|
86
|
+
// without a token rotation. Claude was already bound; its
|
|
87
|
+
// existing token stays valid and the next LAP call sees a live
|
|
88
|
+
// pairing again. Restore the original label so audit context
|
|
89
|
+
// doesn't show a "reconnected" placeholder bouncing in and out.
|
|
90
|
+
await tokenStore.markActive(tid, rec.label ?? '(reconnected)', nowMs);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
await tokenStore.markAwaitingClaude(tid, nowMs);
|
|
94
|
+
}
|
|
95
|
+
// Hook the close: when the WS drops, transition the record to
|
|
96
|
+
// `pending-resume` with a TTL so the next reconnect within the
|
|
97
|
+
// grace window can re-pair without rotating the token. After
|
|
98
|
+
// grace, LAP calls return `X-LLui-Reconnect: expired` and the
|
|
99
|
+
// agent must call `/resume/claim` to start fresh. The token-
|
|
100
|
+
// store guards the transition so `revoke`/`expired` don't get
|
|
101
|
+
// lifted back into a grace window.
|
|
102
|
+
if (pendingResumeGraceMs > 0) {
|
|
103
|
+
registry.onClose(tid, () => {
|
|
104
|
+
void tokenStore.markPendingResume(tid, Date.now() + pendingResumeGraceMs);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
74
107
|
await auditSink.write({
|
|
75
108
|
at: nowMs,
|
|
76
109
|
tid,
|
|
77
110
|
uid: null,
|
|
78
111
|
event: 'claim',
|
|
79
|
-
detail: { transport: 'ws' },
|
|
112
|
+
detail: { transport: 'ws', repair: isRepair },
|
|
80
113
|
});
|
|
81
114
|
return { ok: true, tid };
|
|
82
115
|
};
|
package/dist/server/core.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/server/core.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime-neutral core of the LLui agent server. Exports everything\n * that works on any runtime with `crypto.subtle` + `Request`/`Response`\n * + long-lived connection primitives — in practice: Node, Bun, Deno,\n * Deno Deploy, Cloudflare Workers + Durable Objects.\n *\n * Intentionally does NOT import the `ws` library or any `node:*`\n * modules. Node-specific wiring lives in `./factory.ts`\n * (`createLluiAgentServer`); web runtimes use `./web/` adapters on\n * top of this core.\n */\nimport type { TokenStore } from './token-store.js'\nimport type { IdentityResolver } from './identity.js'\nimport type { AuditSink } from './audit.js'\nimport type { RateLimiter } from './rate-limit.js'\nimport type { PairingConnection, PairingRegistry } from './ws/pairing-registry.js'\nimport { InMemoryTokenStore } from './token-store.js'\nimport { consoleAuditSink } from './audit.js'\nimport { defaultRateLimiter } from './rate-limit.js'\nimport { createHttpRouter } from './http/router.js'\nimport { createLapRouter } from './lap/router.js'\nimport { InMemoryPairingRegistry } from './ws/pairing-registry.js'\nimport { tokenHashOf } from './token.js'\n\nconst ANONYMOUS_RESOLVER: IdentityResolver = async () => null\n\n/**\n * Options accepted by `createLluiAgentCore`. Strict subset of\n * `ServerOptions` — everything needed to build the router, registry,\n * and accept-connection primitive. The Node factory adds WebSocket\n * upgrade wiring on top.\n */\nexport type CoreOptions = {\n tokenStore?: TokenStore\n identityResolver?: IdentityResolver\n auditSink?: AuditSink\n rateLimiter?: RateLimiter\n lapBasePath?: string\n /**\n * Override the default `InMemoryPairingRegistry`. Web runtimes that\n * need a different pairing implementation (e.g. a Cloudflare\n * Durable Object that persists across isolates) pass it here.\n */\n registry?: PairingRegistry\n}\n\nexport type AcceptResult =\n | { ok: true; tid: string }\n | { ok: false; status: number; code: 'auth-failed' | 'revoked' }\n\n/**\n * Handle returned by `createLluiAgentCore`. Purely runtime-neutral —\n * `router` is a Fetch-style handler, `acceptConnection` is the\n * primitive that runtime-specific WebSocket adapters call after\n * accepting a socket in their native way.\n */\nexport type AgentCoreHandle = {\n router: (req: Request) => Promise<Response | null>\n registry: PairingRegistry\n tokenStore: TokenStore\n auditSink: AuditSink\n /**\n * Validate an agent token and register a `PairingConnection` with\n * the registry. Use this after accepting a WebSocket upgrade via\n * your runtime's native API (e.g. `WebSocketPair` on Cloudflare,\n * `Deno.upgradeWebSocket` on Deno, `server.upgrade` on Bun).\n *\n * On success: marks the token `awaiting-claude`, writes an audit\n * entry, and returns `{ok: true, tid}`. On failure: returns an\n * appropriate HTTP status for the caller to encode into the\n * upgrade response (401 for auth failure, 403 for revoked).\n */\n acceptConnection: (token: string, conn: PairingConnection) => Promise<AcceptResult>\n}\n\n/**\n * Compose the runtime-neutral agent server. The returned handle has\n * everything the LAP HTTP routes and the WebSocket acceptance\n * plumbing need; runtime adapters wire the native upgrade API on\n * top (see `@llui/agent/server` for Node, `@llui/agent/server/web`\n * for WHATWG runtimes).\n */\nexport function createLluiAgentCore(opts: CoreOptions = {}): AgentCoreHandle {\n const tokenStore = opts.tokenStore ?? new InMemoryTokenStore()\n const identityResolver = opts.identityResolver ?? ANONYMOUS_RESOLVER\n const auditSink = opts.auditSink ?? consoleAuditSink\n const rateLimiter = opts.rateLimiter ?? defaultRateLimiter({ perBucket: '30/minute' })\n const lapBasePath = opts.lapBasePath ?? '/agent/lap/v1'\n\n const registry: PairingRegistry =\n opts.registry ??\n new InMemoryPairingRegistry({\n onLogAppend: (tid, entry) => {\n void auditSink.write({\n at: entry.at,\n tid,\n uid: null,\n event: 'lap-call',\n detail: {\n source: 'client-log',\n kind: entry.kind,\n variant: entry.variant,\n intent: entry.intent,\n },\n })\n },\n })\n\n const httpRouter = createHttpRouter({\n tokenStore,\n identityResolver,\n auditSink,\n lapBasePath,\n })\n\n const lapRouter = createLapRouter(\n {\n tokenStore,\n registry,\n auditSink,\n rateLimiter,\n },\n lapBasePath,\n )\n\n const router: AgentCoreHandle['router'] = async (req) => {\n const lapRes = await lapRouter(req)\n if (lapRes) return lapRes\n return httpRouter(req)\n }\n\n const acceptConnection: AgentCoreHandle['acceptConnection'] = async (token, conn) => {\n // Same hash-lookup path as the LAP HTTP routes — keeps the auth\n // story uniform across HTTP and WS surfaces.\n const hash = await tokenHashOf(token)\n if (!hash) return { ok: false, status: 401, code: 'auth-failed' }\n const rec = await tokenStore.findByTokenHash(hash)\n if (!rec) return { ok: false, status: 401, code: 'auth-failed' }\n if (rec.expiresAt <= Date.now()) return { ok: false, status: 401, code: 'auth-failed' }\n if (rec.status === 'revoked') return { ok: false, status: 403, code: 'revoked' }\n const tid = rec.tid\n registry.register(tid, conn)\n const nowMs = Date.now()\n await tokenStore.markAwaitingClaude(tid, nowMs)\n await auditSink.write({\n at: nowMs,\n tid,\n uid: null,\n event: 'claim',\n detail: { transport: 'ws' },\n })\n return { ok: true, tid }\n }\n\n return { router, registry, tokenStore, auditSink, acceptConnection }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/server/core.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime-neutral core of the LLui agent server. Exports everything\n * that works on any runtime with `crypto.subtle` + `Request`/`Response`\n * + long-lived connection primitives — in practice: Node, Bun, Deno,\n * Deno Deploy, Cloudflare Workers + Durable Objects.\n *\n * Intentionally does NOT import the `ws` library or any `node:*`\n * modules. Node-specific wiring lives in `./factory.ts`\n * (`createLluiAgentServer`); web runtimes use `./web/` adapters on\n * top of this core.\n */\nimport type { TokenStore } from './token-store.js'\nimport type { IdentityResolver } from './identity.js'\nimport type { AuditSink } from './audit.js'\nimport type { RateLimiter } from './rate-limit.js'\nimport type { PairingConnection, PairingRegistry } from './ws/pairing-registry.js'\nimport { InMemoryTokenStore } from './token-store.js'\nimport { consoleAuditSink } from './audit.js'\nimport { defaultRateLimiter } from './rate-limit.js'\nimport { createHttpRouter } from './http/router.js'\nimport { createLapRouter } from './lap/router.js'\nimport { InMemoryPairingRegistry } from './ws/pairing-registry.js'\nimport { tokenHashOf } from './token.js'\n\nconst ANONYMOUS_RESOLVER: IdentityResolver = async () => null\n\n/**\n * Options accepted by `createLluiAgentCore`. Strict subset of\n * `ServerOptions` — everything needed to build the router, registry,\n * and accept-connection primitive. The Node factory adds WebSocket\n * upgrade wiring on top.\n */\nexport type CoreOptions = {\n tokenStore?: TokenStore\n identityResolver?: IdentityResolver\n auditSink?: AuditSink\n rateLimiter?: RateLimiter\n lapBasePath?: string\n /**\n * Override the default `InMemoryPairingRegistry`. Web runtimes that\n * need a different pairing implementation (e.g. a Cloudflare\n * Durable Object that persists across isolates) pass it here.\n */\n registry?: PairingRegistry\n /**\n * How long, in milliseconds, a token's record stays in\n * `pending-resume` after the WS pairing closes. During this window\n * the same browser can reconnect with the same bearer token and\n * the WS re-pairs without going through the rotate-on-resume path\n * (`/resume/claim`). The agent's existing token stays valid the\n * whole time, so brief network drops, page reloads, and quick\n * server restarts don't invalidate the agent's session.\n *\n * After the window, LAP calls report `X-LLui-Reconnect: expired`\n * and the record becomes resume-claimable (rotation required).\n * Set to `0` to opt out — the WS close immediately drops the\n * record and any reconnect must go through `/resume/claim`.\n *\n * Default: 60 seconds — long enough for laptop sleep, brief Wi-Fi\n * flicker, and a server restart; short enough that a deliberately-\n * closed tab doesn't keep the record alive forever.\n */\n pendingResumeGraceMs?: number\n}\n\nexport type AcceptResult =\n | { ok: true; tid: string }\n | { ok: false; status: number; code: 'auth-failed' | 'revoked' }\n\n/**\n * Handle returned by `createLluiAgentCore`. Purely runtime-neutral —\n * `router` is a Fetch-style handler, `acceptConnection` is the\n * primitive that runtime-specific WebSocket adapters call after\n * accepting a socket in their native way.\n */\nexport type AgentCoreHandle = {\n router: (req: Request) => Promise<Response | null>\n registry: PairingRegistry\n tokenStore: TokenStore\n auditSink: AuditSink\n /**\n * Validate an agent token and register a `PairingConnection` with\n * the registry. Use this after accepting a WebSocket upgrade via\n * your runtime's native API (e.g. `WebSocketPair` on Cloudflare,\n * `Deno.upgradeWebSocket` on Deno, `server.upgrade` on Bun).\n *\n * On success: marks the token `awaiting-claude`, writes an audit\n * entry, and returns `{ok: true, tid}`. On failure: returns an\n * appropriate HTTP status for the caller to encode into the\n * upgrade response (401 for auth failure, 403 for revoked).\n */\n acceptConnection: (token: string, conn: PairingConnection) => Promise<AcceptResult>\n}\n\n/**\n * Compose the runtime-neutral agent server. The returned handle has\n * everything the LAP HTTP routes and the WebSocket acceptance\n * plumbing need; runtime adapters wire the native upgrade API on\n * top (see `@llui/agent/server` for Node, `@llui/agent/server/web`\n * for WHATWG runtimes).\n */\nexport function createLluiAgentCore(opts: CoreOptions = {}): AgentCoreHandle {\n const tokenStore = opts.tokenStore ?? new InMemoryTokenStore()\n const identityResolver = opts.identityResolver ?? ANONYMOUS_RESOLVER\n const auditSink = opts.auditSink ?? consoleAuditSink\n const rateLimiter = opts.rateLimiter ?? defaultRateLimiter({ perBucket: '30/minute' })\n const lapBasePath = opts.lapBasePath ?? '/agent/lap/v1'\n const pendingResumeGraceMs = opts.pendingResumeGraceMs ?? 60_000\n\n const registry: PairingRegistry =\n opts.registry ??\n new InMemoryPairingRegistry({\n onLogAppend: (tid, entry) => {\n void auditSink.write({\n at: entry.at,\n tid,\n uid: null,\n event: 'lap-call',\n detail: {\n source: 'client-log',\n kind: entry.kind,\n variant: entry.variant,\n intent: entry.intent,\n },\n })\n },\n })\n\n const httpRouter = createHttpRouter({\n tokenStore,\n identityResolver,\n auditSink,\n lapBasePath,\n })\n\n const lapRouter = createLapRouter(\n {\n tokenStore,\n registry,\n auditSink,\n rateLimiter,\n },\n lapBasePath,\n )\n\n const router: AgentCoreHandle['router'] = async (req) => {\n const lapRes = await lapRouter(req)\n if (lapRes) return lapRes\n return httpRouter(req)\n }\n\n const acceptConnection: AgentCoreHandle['acceptConnection'] = async (token, conn) => {\n // Same hash-lookup path as the LAP HTTP routes — keeps the auth\n // story uniform across HTTP and WS surfaces.\n const hash = await tokenHashOf(token)\n if (!hash) return { ok: false, status: 401, code: 'auth-failed' }\n const rec = await tokenStore.findByTokenHash(hash)\n if (!rec) return { ok: false, status: 401, code: 'auth-failed' }\n if (rec.expiresAt <= Date.now()) return { ok: false, status: 401, code: 'auth-failed' }\n if (rec.status === 'revoked') return { ok: false, status: 403, code: 'revoked' }\n // Reject `pending-resume` records past their grace window — the\n // agent has to go through `/resume/claim` (which rotates the\n // bearer) for those, since the long-gap path can't assume the\n // previous bearer wasn't leaked.\n if (\n rec.status === 'pending-resume' &&\n rec.pendingResumeUntil !== null &&\n rec.pendingResumeUntil <= Date.now()\n ) {\n return { ok: false, status: 401, code: 'auth-failed' }\n }\n const tid = rec.tid\n const isRepair = rec.status === 'pending-resume'\n registry.register(tid, conn)\n const nowMs = Date.now()\n if (isRepair) {\n // Same browser came back within the grace window — re-pair\n // without a token rotation. Claude was already bound; its\n // existing token stays valid and the next LAP call sees a live\n // pairing again. Restore the original label so audit context\n // doesn't show a \"reconnected\" placeholder bouncing in and out.\n await tokenStore.markActive(tid, rec.label ?? '(reconnected)', nowMs)\n } else {\n await tokenStore.markAwaitingClaude(tid, nowMs)\n }\n // Hook the close: when the WS drops, transition the record to\n // `pending-resume` with a TTL so the next reconnect within the\n // grace window can re-pair without rotating the token. After\n // grace, LAP calls return `X-LLui-Reconnect: expired` and the\n // agent must call `/resume/claim` to start fresh. The token-\n // store guards the transition so `revoke`/`expired` don't get\n // lifted back into a grace window.\n if (pendingResumeGraceMs > 0) {\n registry.onClose(tid, () => {\n void tokenStore.markPendingResume(tid, Date.now() + pendingResumeGraceMs)\n })\n }\n await auditSink.write({\n at: nowMs,\n tid,\n uid: null,\n event: 'claim',\n detail: { transport: 'ws', repair: isRepair },\n })\n return { ok: true, tid }\n }\n\n return { router, registry, tokenStore, auditSink, acceptConnection }\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TokenStore } from '../token-store.js';
|
|
2
|
+
import type { PairingRegistry } from '../ws/pairing-registry.js';
|
|
3
|
+
import type { TokenRecord } from '../../protocol.js';
|
|
4
|
+
/**
|
|
5
|
+
* Transition a tid to `active` and notify the browser when an LLM
|
|
6
|
+
* makes its first LAP call. Run from every LAP handler that auth's
|
|
7
|
+
* the token, so activation isn't gated by which endpoint the bridge
|
|
8
|
+
* happens to hit first (`describe` was the only one wiring this up
|
|
9
|
+
* historically; the bridge connects via `/observe` so the browser
|
|
10
|
+
* stayed at `awaiting-claude` indefinitely).
|
|
11
|
+
*
|
|
12
|
+
* No-op when the record is already `active` or in any other state —
|
|
13
|
+
* the `awaiting-claude` → `active` transition is the only one we
|
|
14
|
+
* care about here. `pending-resume` reattaches happen in
|
|
15
|
+
* `acceptConnection` (re-pair path); we don't second-guess them
|
|
16
|
+
* from the LAP layer.
|
|
17
|
+
*/
|
|
18
|
+
export declare function ensureActive(tokenStore: TokenStore, registry: PairingRegistry, tid: string, rec: TokenRecord, now: number): Promise<void>;
|
|
19
|
+
//# sourceMappingURL=active.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"active.d.ts","sourceRoot":"","sources":["../../../src/server/lap/active.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEpD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,eAAe,EACzB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAQf"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transition a tid to `active` and notify the browser when an LLM
|
|
3
|
+
* makes its first LAP call. Run from every LAP handler that auth's
|
|
4
|
+
* the token, so activation isn't gated by which endpoint the bridge
|
|
5
|
+
* happens to hit first (`describe` was the only one wiring this up
|
|
6
|
+
* historically; the bridge connects via `/observe` so the browser
|
|
7
|
+
* stayed at `awaiting-claude` indefinitely).
|
|
8
|
+
*
|
|
9
|
+
* No-op when the record is already `active` or in any other state —
|
|
10
|
+
* the `awaiting-claude` → `active` transition is the only one we
|
|
11
|
+
* care about here. `pending-resume` reattaches happen in
|
|
12
|
+
* `acceptConnection` (re-pair path); we don't second-guess them
|
|
13
|
+
* from the LAP layer.
|
|
14
|
+
*/
|
|
15
|
+
export async function ensureActive(tokenStore, registry, tid, rec, now) {
|
|
16
|
+
if (rec.status !== 'awaiting-claude')
|
|
17
|
+
return;
|
|
18
|
+
const label = rec.uid ?? rec.label ?? 'Claude';
|
|
19
|
+
await tokenStore.markActive(tid, label, now);
|
|
20
|
+
// Best-effort — the browser may have closed the WS in the gap;
|
|
21
|
+
// the registry's send is a no-op in that case and the close
|
|
22
|
+
// handler will mark the record `pending-resume` anyway.
|
|
23
|
+
registry.send(tid, { t: 'active' });
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=active.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"active.js","sourceRoot":"","sources":["../../../src/server/lap/active.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,UAAsB,EACtB,QAAyB,EACzB,GAAW,EACX,GAAgB,EAChB,GAAW;IAEX,IAAI,GAAG,CAAC,MAAM,KAAK,iBAAiB;QAAE,OAAM;IAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,IAAI,QAAQ,CAAA;IAC9C,MAAM,UAAU,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,CAAA;IAC5C,+DAA+D;IAC/D,4DAA4D;IAC5D,wDAAwD;IACxD,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAA;AACrC,CAAC","sourcesContent":["import type { TokenStore } from '../token-store.js'\nimport type { PairingRegistry } from '../ws/pairing-registry.js'\nimport type { TokenRecord } from '../../protocol.js'\n\n/**\n * Transition a tid to `active` and notify the browser when an LLM\n * makes its first LAP call. Run from every LAP handler that auth's\n * the token, so activation isn't gated by which endpoint the bridge\n * happens to hit first (`describe` was the only one wiring this up\n * historically; the bridge connects via `/observe` so the browser\n * stayed at `awaiting-claude` indefinitely).\n *\n * No-op when the record is already `active` or in any other state —\n * the `awaiting-claude` → `active` transition is the only one we\n * care about here. `pending-resume` reattaches happen in\n * `acceptConnection` (re-pair path); we don't second-guess them\n * from the LAP layer.\n */\nexport async function ensureActive(\n tokenStore: TokenStore,\n registry: PairingRegistry,\n tid: string,\n rec: TokenRecord,\n now: number,\n): Promise<void> {\n if (rec.status !== 'awaiting-claude') return\n const label = rec.uid ?? rec.label ?? 'Claude'\n await tokenStore.markActive(tid, label, now)\n // Best-effort — the browser may have closed the WS in the gap;\n // the registry's send is a no-op in that case and the close\n // handler will mark the record `pending-resume` anyway.\n registry.send(tid, { t: 'active' })\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"confirm-result.d.ts","sourceRoot":"","sources":["../../../src/server/lap/confirm-result.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"confirm-result.d.ts","sourceRoot":"","sources":["../../../src/server/lap/confirm-result.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAMnD,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,EAAE,UAAU,CAAA;IACtB,QAAQ,EAAE,eAAe,CAAA;IACzB,SAAS,EAAE,SAAS,CAAA;IACpB,WAAW,EAAE,WAAW,CAAA;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB,CAAA;AAED,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,QAAQ,CAAC,CA2DnB"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { verifyAndReadTid } from './describe.js';
|
|
2
|
+
import { buildPausedResponse } from './paused.js';
|
|
3
|
+
import { ensureActive } from './active.js';
|
|
2
4
|
export async function handleLapConfirmResult(req, deps) {
|
|
3
5
|
const auth = await verifyAndReadTid(req, deps.tokenStore);
|
|
4
6
|
if (!auth.ok)
|
|
@@ -7,7 +9,7 @@ export async function handleLapConfirmResult(req, deps) {
|
|
|
7
9
|
if (!rec || rec.status === 'revoked')
|
|
8
10
|
return json({ error: { code: 'revoked' } }, 403);
|
|
9
11
|
if (!deps.registry.isPaired(auth.tid))
|
|
10
|
-
return
|
|
12
|
+
return buildPausedResponse(deps.tokenStore, auth.tid);
|
|
11
13
|
const rlCheck = await deps.rateLimiter.check(auth.tid, 'token');
|
|
12
14
|
if (!rlCheck.allowed) {
|
|
13
15
|
return json({ error: { code: 'rate-limited', retryAfterMs: rlCheck.retryAfterMs } }, 429);
|
|
@@ -23,6 +25,7 @@ export async function handleLapConfirmResult(req, deps) {
|
|
|
23
25
|
// If no resolution arrives in time, we surface 'still-pending'.
|
|
24
26
|
const result = await deps.registry.waitForConfirm(auth.tid, body.confirmId, timeoutMs);
|
|
25
27
|
const nowMs = (deps.now ?? (() => Date.now()))();
|
|
28
|
+
await ensureActive(deps.tokenStore, deps.registry, auth.tid, rec, nowMs);
|
|
26
29
|
if (result.outcome === 'confirmed') {
|
|
27
30
|
await deps.auditSink.write({
|
|
28
31
|
at: nowMs,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"confirm-result.js","sourceRoot":"","sources":["../../../src/server/lap/confirm-result.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"confirm-result.js","sourceRoot":"","sources":["../../../src/server/lap/confirm-result.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TokenStore } from '../token-store.js'\nimport type { PairingRegistry } from '../ws/pairing-registry.js'\nimport type { AuditSink } from '../audit.js'\nimport type { RateLimiter } from '../rate-limit.js'\nimport { verifyAndReadTid } from './describe.js'\nimport { buildPausedResponse } from './paused.js'\nimport { ensureActive } from './active.js'\nimport type { LapConfirmResultRequest, LapConfirmResultResponse } from '../../protocol.js'\n\nexport type LapConfirmResultDeps = {\n tokenStore: TokenStore\n registry: PairingRegistry\n auditSink: AuditSink\n rateLimiter: RateLimiter\n now?: () => number\n}\n\nexport async function handleLapConfirmResult(\n req: Request,\n deps: LapConfirmResultDeps,\n): Promise<Response> {\n const auth = await verifyAndReadTid(req, deps.tokenStore)\n if (!auth.ok) return json({ error: { code: auth.code } }, auth.status)\n\n const rec = await deps.tokenStore.findByTid(auth.tid)\n if (!rec || rec.status === 'revoked') return json({ error: { code: 'revoked' } }, 403)\n if (!deps.registry.isPaired(auth.tid)) return buildPausedResponse(deps.tokenStore, auth.tid)\n\n const rlCheck = await deps.rateLimiter.check(auth.tid, 'token')\n if (!rlCheck.allowed) {\n return json({ error: { code: 'rate-limited', retryAfterMs: rlCheck.retryAfterMs } }, 429)\n }\n\n const body = (await req.json().catch(() => null)) as LapConfirmResultRequest | null\n if (!body || typeof body.confirmId !== 'string') return json({ error: { code: 'invalid' } }, 400)\n const timeoutMs = body.timeoutMs ?? 5_000\n\n // Spec: if the confirm was already resolved during the earlier long-poll on\n // /message, there's no second resolution to wait for. In the current design\n // /confirm-result is ONLY used when /message bailed out early with\n // pending-confirmation. So we call waitForConfirm with the given timeoutMs.\n // If no resolution arrives in time, we surface 'still-pending'.\n const result = await deps.registry.waitForConfirm(auth.tid, body.confirmId, timeoutMs)\n\n const nowMs = (deps.now ?? (() => Date.now()))()\n await ensureActive(deps.tokenStore, deps.registry, auth.tid, rec, nowMs)\n if (result.outcome === 'confirmed') {\n await deps.auditSink.write({\n at: nowMs,\n tid: auth.tid,\n uid: rec.uid,\n event: 'confirm-approved',\n detail: { confirmId: body.confirmId },\n })\n return json(\n { status: 'confirmed', stateAfter: result.stateAfter } satisfies LapConfirmResultResponse,\n 200,\n )\n }\n // user-cancelled OR timeout. WsPairingRegistry returns user-cancelled on timeout too;\n // we distinguish by checking whether the confirm is still in registry.pendingConfirm —\n // but pendingConfirm cleanup happens inside waitForConfirm's timer, so we can't peek.\n // For v1: treat user-cancelled as user-cancelled; treat explicit timeout as timeout by\n // comparing elapsed vs. timeoutMs. Simpler: just return 'still-pending' on the timeout\n // branch to let Claude poll again. Registry returns {outcome: 'user-cancelled'} on\n // both timer and actual cancel — so we can't distinguish. Punt: return 'user-cancelled'\n // (matches registry semantics). Spec §8.2 get_confirm_result allows 'user-cancelled' |\n // 'timeout' | 'still-pending' — a refinement to distinguish is follow-up work.\n await deps.auditSink.write({\n at: nowMs,\n tid: auth.tid,\n uid: rec.uid,\n event: 'confirm-rejected',\n detail: { confirmId: body.confirmId },\n })\n return json(\n { status: 'rejected', reason: 'user-cancelled' } satisfies LapConfirmResultResponse,\n 200,\n )\n}\n\nfunction json(b: unknown, s: number): Response {\n return new Response(JSON.stringify(b), {\n status: s,\n headers: { 'content-type': 'application/json' },\n })\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe.d.ts","sourceRoot":"","sources":["../../../src/server/lap/describe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"describe.d.ts","sourceRoot":"","sources":["../../../src/server/lap/describe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAKnD,MAAM,MAAM,eAAe,GAAG;IAC5B,UAAU,EAAE,UAAU,CAAA;IACtB,QAAQ,EAAE,eAAe,CAAA;IACzB,SAAS,EAAE,SAAS,CAAA;IACpB,WAAW,EAAE,WAAW,CAAA;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB,CAAA;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAiD9F;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,OAAO,EACZ,UAAU,EAAE,UAAU,EACtB,KAAK,GAAE,MAAmB,GACzB,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAUlF"}
|