@llui/agent 0.0.29
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/LICENSE +21 -0
- package/README.md +210 -0
- package/dist/client/agentConfirm.d.ts +60 -0
- package/dist/client/agentConfirm.d.ts.map +1 -0
- package/dist/client/agentConfirm.js +66 -0
- package/dist/client/agentConfirm.js.map +1 -0
- package/dist/client/agentConnect.d.ts +125 -0
- package/dist/client/agentConnect.d.ts.map +1 -0
- package/dist/client/agentConnect.js +114 -0
- package/dist/client/agentConnect.js.map +1 -0
- package/dist/client/agentLog.d.ts +51 -0
- package/dist/client/agentLog.d.ts.map +1 -0
- package/dist/client/agentLog.js +53 -0
- package/dist/client/agentLog.js.map +1 -0
- package/dist/client/effect-handler.d.ts +15 -0
- package/dist/client/effect-handler.d.ts.map +1 -0
- package/dist/client/effect-handler.js +146 -0
- package/dist/client/effect-handler.js.map +1 -0
- package/dist/client/effects.d.ts +27 -0
- package/dist/client/effects.d.ts.map +1 -0
- package/dist/client/effects.js +2 -0
- package/dist/client/effects.js.map +1 -0
- package/dist/client/factory.d.ts +47 -0
- package/dist/client/factory.d.ts.map +1 -0
- package/dist/client/factory.js +105 -0
- package/dist/client/factory.js.map +1 -0
- package/dist/client/index.d.ts +7 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/rpc/describe-context.d.ts +10 -0
- package/dist/client/rpc/describe-context.d.ts.map +1 -0
- package/dist/client/rpc/describe-context.js +8 -0
- package/dist/client/rpc/describe-context.js.map +1 -0
- package/dist/client/rpc/describe-visible-content.d.ts +22 -0
- package/dist/client/rpc/describe-visible-content.d.ts.map +1 -0
- package/dist/client/rpc/describe-visible-content.js +66 -0
- package/dist/client/rpc/describe-visible-content.js.map +1 -0
- package/dist/client/rpc/get-state.d.ts +15 -0
- package/dist/client/rpc/get-state.d.ts.map +1 -0
- package/dist/client/rpc/get-state.js +37 -0
- package/dist/client/rpc/get-state.js.map +1 -0
- package/dist/client/rpc/list-actions.d.ts +27 -0
- package/dist/client/rpc/list-actions.d.ts.map +1 -0
- package/dist/client/rpc/list-actions.js +38 -0
- package/dist/client/rpc/list-actions.js.map +1 -0
- package/dist/client/rpc/query-dom.d.ts +20 -0
- package/dist/client/rpc/query-dom.d.ts.map +1 -0
- package/dist/client/rpc/query-dom.js +37 -0
- package/dist/client/rpc/query-dom.js.map +1 -0
- package/dist/client/rpc/send-message.d.ts +28 -0
- package/dist/client/rpc/send-message.d.ts.map +1 -0
- package/dist/client/rpc/send-message.js +40 -0
- package/dist/client/rpc/send-message.js.map +1 -0
- package/dist/client/uuid.d.ts +2 -0
- package/dist/client/uuid.d.ts.map +1 -0
- package/dist/client/uuid.js +24 -0
- package/dist/client/uuid.js.map +1 -0
- package/dist/client/ws-client.d.ts +44 -0
- package/dist/client/ws-client.d.ts.map +1 -0
- package/dist/client/ws-client.js +176 -0
- package/dist/client/ws-client.js.map +1 -0
- package/dist/protocol.d.ts +319 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +6 -0
- package/dist/protocol.js.map +1 -0
- package/dist/server/audit.d.ts +6 -0
- package/dist/server/audit.d.ts.map +1 -0
- package/dist/server/audit.js +6 -0
- package/dist/server/audit.js.map +1 -0
- package/dist/server/factory.d.ts +10 -0
- package/dist/server/factory.d.ts.map +1 -0
- package/dist/server/factory.js +69 -0
- package/dist/server/factory.js.map +1 -0
- package/dist/server/http/mint.d.ts +23 -0
- package/dist/server/http/mint.d.ts.map +1 -0
- package/dist/server/http/mint.js +63 -0
- package/dist/server/http/mint.js.map +1 -0
- package/dist/server/http/resume.d.ts +14 -0
- package/dist/server/http/resume.d.ts.map +1 -0
- package/dist/server/http/resume.js +89 -0
- package/dist/server/http/resume.js.map +1 -0
- package/dist/server/http/revoke.d.ts +11 -0
- package/dist/server/http/revoke.d.ts.map +1 -0
- package/dist/server/http/revoke.js +24 -0
- package/dist/server/http/revoke.js.map +1 -0
- package/dist/server/http/router.d.ts +13 -0
- package/dist/server/http/router.d.ts.map +1 -0
- package/dist/server/http/router.js +28 -0
- package/dist/server/http/router.js.map +1 -0
- package/dist/server/http/sessions.d.ts +8 -0
- package/dist/server/http/sessions.d.ts.map +1 -0
- package/dist/server/http/sessions.js +27 -0
- package/dist/server/http/sessions.js.map +1 -0
- package/dist/server/identity.d.ts +8 -0
- package/dist/server/identity.d.ts.map +1 -0
- package/dist/server/identity.js +41 -0
- package/dist/server/identity.js.map +1 -0
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +6 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/lap/confirm-result.d.ts +14 -0
- package/dist/server/lap/confirm-result.d.ts.map +1 -0
- package/dist/server/lap/confirm-result.js +60 -0
- package/dist/server/lap/confirm-result.js.map +1 -0
- package/dist/server/lap/describe.d.ts +22 -0
- package/dist/server/lap/describe.d.ts.map +1 -0
- package/dist/server/lap/describe.js +67 -0
- package/dist/server/lap/describe.js.map +1 -0
- package/dist/server/lap/forward.d.ts +24 -0
- package/dist/server/lap/forward.d.ts.map +1 -0
- package/dist/server/lap/forward.js +68 -0
- package/dist/server/lap/forward.js.map +1 -0
- package/dist/server/lap/message.d.ts +14 -0
- package/dist/server/lap/message.d.ts.map +1 -0
- package/dist/server/lap/message.js +97 -0
- package/dist/server/lap/message.js.map +1 -0
- package/dist/server/lap/router.d.ts +4 -0
- package/dist/server/lap/router.d.ts.map +1 -0
- package/dist/server/lap/router.js +37 -0
- package/dist/server/lap/router.js.map +1 -0
- package/dist/server/lap/wait.d.ts +14 -0
- package/dist/server/lap/wait.d.ts.map +1 -0
- package/dist/server/lap/wait.js +35 -0
- package/dist/server/lap/wait.js.map +1 -0
- package/dist/server/options.d.ts +41 -0
- package/dist/server/options.d.ts.map +1 -0
- package/dist/server/options.js +2 -0
- package/dist/server/options.js.map +1 -0
- package/dist/server/rate-limit.d.ts +14 -0
- package/dist/server/rate-limit.d.ts.map +1 -0
- package/dist/server/rate-limit.js +43 -0
- package/dist/server/rate-limit.js.map +1 -0
- package/dist/server/token-store.d.ts +27 -0
- package/dist/server/token-store.d.ts.map +1 -0
- package/dist/server/token-store.js +55 -0
- package/dist/server/token-store.js.map +1 -0
- package/dist/server/token.d.ts +24 -0
- package/dist/server/token.d.ts.map +1 -0
- package/dist/server/token.js +77 -0
- package/dist/server/token.js.map +1 -0
- package/dist/server/ws/pairing-registry.d.ts +53 -0
- package/dist/server/ws/pairing-registry.d.ts.map +1 -0
- package/dist/server/ws/pairing-registry.js +205 -0
- package/dist/server/ws/pairing-registry.js.map +1 -0
- package/dist/server/ws/upgrade.d.ts +23 -0
- package/dist/server/ws/upgrade.d.ts.map +1 -0
- package/dist/server/ws/upgrade.js +81 -0
- package/dist/server/ws/upgrade.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
const PREFIX = 'llui-agent_';
|
|
3
|
+
function toKeyBuffer(key) {
|
|
4
|
+
const buf = typeof key === 'string' ? Buffer.from(key, 'utf8') : Buffer.from(key);
|
|
5
|
+
if (buf.length < 32)
|
|
6
|
+
throw new Error('signingKey must be at least 32 bytes');
|
|
7
|
+
return buf;
|
|
8
|
+
}
|
|
9
|
+
function b64url(buf) {
|
|
10
|
+
return buf.toString('base64url');
|
|
11
|
+
}
|
|
12
|
+
function b64urlDecode(s) {
|
|
13
|
+
try {
|
|
14
|
+
return Buffer.from(s, 'base64url');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Serialize a payload to `llui-agent_<base64url(json)>.<base64url(hmac)>`.
|
|
22
|
+
* See spec §6.1.
|
|
23
|
+
*/
|
|
24
|
+
export function signToken(payload, key) {
|
|
25
|
+
const keyBuf = toKeyBuffer(key);
|
|
26
|
+
const jsonBuf = Buffer.from(JSON.stringify(payload), 'utf8');
|
|
27
|
+
const payloadPart = b64url(jsonBuf);
|
|
28
|
+
const mac = createHmac('sha256', keyBuf).update(payloadPart).digest();
|
|
29
|
+
const sigPart = b64url(mac);
|
|
30
|
+
return (PREFIX + payloadPart + '.' + sigPart);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Verify the signature, parse the payload, and check expiry.
|
|
34
|
+
*/
|
|
35
|
+
export function verifyToken(token, key, nowSec = Math.floor(Date.now() / 1000)) {
|
|
36
|
+
if (!token.startsWith(PREFIX))
|
|
37
|
+
return { kind: 'invalid', reason: 'malformed' };
|
|
38
|
+
const body = token.slice(PREFIX.length);
|
|
39
|
+
const dot = body.indexOf('.');
|
|
40
|
+
if (dot < 0)
|
|
41
|
+
return { kind: 'invalid', reason: 'malformed' };
|
|
42
|
+
const payloadPart = body.slice(0, dot);
|
|
43
|
+
const sigPart = body.slice(dot + 1);
|
|
44
|
+
const sigBuf = b64urlDecode(sigPart);
|
|
45
|
+
if (!sigBuf)
|
|
46
|
+
return { kind: 'invalid', reason: 'malformed' };
|
|
47
|
+
const keyBuf = toKeyBuffer(key);
|
|
48
|
+
const expected = createHmac('sha256', keyBuf).update(payloadPart).digest();
|
|
49
|
+
if (expected.length !== sigBuf.length || !timingSafeEqual(expected, sigBuf)) {
|
|
50
|
+
return { kind: 'invalid', reason: 'bad-signature' };
|
|
51
|
+
}
|
|
52
|
+
const jsonBuf = b64urlDecode(payloadPart);
|
|
53
|
+
if (!jsonBuf)
|
|
54
|
+
return { kind: 'invalid', reason: 'malformed' };
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(jsonBuf.toString('utf8'));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return { kind: 'invalid', reason: 'malformed' };
|
|
61
|
+
}
|
|
62
|
+
if (!isTokenPayload(parsed))
|
|
63
|
+
return { kind: 'invalid', reason: 'malformed' };
|
|
64
|
+
if (parsed.exp <= nowSec)
|
|
65
|
+
return { kind: 'invalid', reason: 'expired' };
|
|
66
|
+
return { kind: 'ok', payload: parsed };
|
|
67
|
+
}
|
|
68
|
+
function isTokenPayload(x) {
|
|
69
|
+
if (!x || typeof x !== 'object')
|
|
70
|
+
return false;
|
|
71
|
+
const o = x;
|
|
72
|
+
return (typeof o.tid === 'string' &&
|
|
73
|
+
typeof o.iat === 'number' &&
|
|
74
|
+
typeof o.exp === 'number' &&
|
|
75
|
+
o.scope === 'agent');
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/server/token.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHmac, timingSafeEqual } from 'node:crypto'\nimport type { AgentToken } from '../protocol.js'\n\nexport type TokenPayload = {\n tid: string\n iat: number\n exp: number\n scope: 'agent'\n}\n\nexport type VerifyResult =\n | { kind: 'ok'; payload: TokenPayload }\n | { kind: 'invalid'; reason: 'malformed' | 'bad-signature' | 'expired' }\n\nconst PREFIX = 'llui-agent_'\n\nfunction toKeyBuffer(key: string | Uint8Array): Buffer {\n const buf = typeof key === 'string' ? Buffer.from(key, 'utf8') : Buffer.from(key)\n if (buf.length < 32) throw new Error('signingKey must be at least 32 bytes')\n return buf\n}\n\nfunction b64url(buf: Buffer): string {\n return buf.toString('base64url')\n}\n\nfunction b64urlDecode(s: string): Buffer | null {\n try {\n return Buffer.from(s, 'base64url')\n } catch {\n return null\n }\n}\n\n/**\n * Serialize a payload to `llui-agent_<base64url(json)>.<base64url(hmac)>`.\n * See spec §6.1.\n */\nexport function signToken(payload: TokenPayload, key: string | Uint8Array): AgentToken {\n const keyBuf = toKeyBuffer(key)\n const jsonBuf = Buffer.from(JSON.stringify(payload), 'utf8')\n const payloadPart = b64url(jsonBuf)\n const mac = createHmac('sha256', keyBuf).update(payloadPart).digest()\n const sigPart = b64url(mac)\n return (PREFIX + payloadPart + '.' + sigPart) as AgentToken\n}\n\n/**\n * Verify the signature, parse the payload, and check expiry.\n */\nexport function verifyToken(\n token: string,\n key: string | Uint8Array,\n nowSec: number = Math.floor(Date.now() / 1000),\n): VerifyResult {\n if (!token.startsWith(PREFIX)) return { kind: 'invalid', reason: 'malformed' }\n const body = token.slice(PREFIX.length)\n const dot = body.indexOf('.')\n if (dot < 0) return { kind: 'invalid', reason: 'malformed' }\n\n const payloadPart = body.slice(0, dot)\n const sigPart = body.slice(dot + 1)\n const sigBuf = b64urlDecode(sigPart)\n if (!sigBuf) return { kind: 'invalid', reason: 'malformed' }\n\n const keyBuf = toKeyBuffer(key)\n const expected = createHmac('sha256', keyBuf).update(payloadPart).digest()\n if (expected.length !== sigBuf.length || !timingSafeEqual(expected, sigBuf)) {\n return { kind: 'invalid', reason: 'bad-signature' }\n }\n\n const jsonBuf = b64urlDecode(payloadPart)\n if (!jsonBuf) return { kind: 'invalid', reason: 'malformed' }\n let parsed: unknown\n try {\n parsed = JSON.parse(jsonBuf.toString('utf8'))\n } catch {\n return { kind: 'invalid', reason: 'malformed' }\n }\n\n if (!isTokenPayload(parsed)) return { kind: 'invalid', reason: 'malformed' }\n if (parsed.exp <= nowSec) return { kind: 'invalid', reason: 'expired' }\n return { kind: 'ok', payload: parsed }\n}\n\nfunction isTokenPayload(x: unknown): x is TokenPayload {\n if (!x || typeof x !== 'object') return false\n const o = x as Record<string, unknown>\n return (\n typeof o.tid === 'string' &&\n typeof o.iat === 'number' &&\n typeof o.exp === 'number' &&\n o.scope === 'agent'\n )\n}\n"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ClientFrame, ServerFrame, HelloFrame, LogEntry } from '../../protocol.js';
|
|
2
|
+
/**
|
|
3
|
+
* Thin abstraction over a WebSocket so the registry is testable with
|
|
4
|
+
* a fake EventEmitter-style mock.
|
|
5
|
+
*/
|
|
6
|
+
export interface PairingConnection {
|
|
7
|
+
send(frame: ServerFrame): void;
|
|
8
|
+
onFrame(handler: (f: ClientFrame) => void): void;
|
|
9
|
+
onClose(handler: () => void): void;
|
|
10
|
+
close(): void;
|
|
11
|
+
}
|
|
12
|
+
export type RpcError = {
|
|
13
|
+
code: 'paused' | 'invalid' | 'timeout' | 'schema-error' | 'internal' | string;
|
|
14
|
+
detail?: string;
|
|
15
|
+
};
|
|
16
|
+
export type RpcOptions = {
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Tracks live browser pairings and correlates rpc requests with replies.
|
|
21
|
+
* One instance per server; shared by all LAP handlers + the upgrade
|
|
22
|
+
* handler. Spec §10.4–§10.5.
|
|
23
|
+
*/
|
|
24
|
+
export declare class WsPairingRegistry {
|
|
25
|
+
private pairings;
|
|
26
|
+
private now;
|
|
27
|
+
private onLogAppend;
|
|
28
|
+
constructor(opts?: {
|
|
29
|
+
now?: () => number;
|
|
30
|
+
onLogAppend?: (tid: string, entry: LogEntry) => void;
|
|
31
|
+
});
|
|
32
|
+
register(tid: string, conn: PairingConnection): void;
|
|
33
|
+
unregister(tid: string): void;
|
|
34
|
+
isPaired(tid: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Send a ServerFrame to the paired browser connection, if one is live.
|
|
37
|
+
* No-op when unpaired or closed.
|
|
38
|
+
*/
|
|
39
|
+
notify(tid: string, frame: ServerFrame): void;
|
|
40
|
+
getHello(tid: string): HelloFrame | null;
|
|
41
|
+
rpc(tid: string, tool: string, args: unknown, opts?: RpcOptions): Promise<unknown>;
|
|
42
|
+
waitForConfirm(tid: string, confirmId: string, timeoutMs: number): Promise<{
|
|
43
|
+
outcome: 'confirmed' | 'user-cancelled';
|
|
44
|
+
stateAfter?: unknown;
|
|
45
|
+
}>;
|
|
46
|
+
waitForChange(tid: string, path: string | undefined, timeoutMs: number): Promise<{
|
|
47
|
+
status: 'changed' | 'timeout';
|
|
48
|
+
stateAfter: unknown;
|
|
49
|
+
}>;
|
|
50
|
+
private handleClientFrame;
|
|
51
|
+
private handleClose;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=pairing-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing-registry.d.ts","sourceRoot":"","sources":["../../../src/server/ws/pairing-registry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAEvF;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IAC9B,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI,CAAA;IAChD,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IAClC,KAAK,IAAI,IAAI,CAAA;CACd;AA4BD,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,GAAG,UAAU,GAAG,MAAM,CAAA;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE/C;;;;GAIG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,GAAG,CAAc;IACzB,OAAO,CAAC,WAAW,CAAiD;gBAGlE,IAAI,GAAE;QACJ,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;QAClB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAA;KAChD;IAMR,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI;IAcpD,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAM7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAK9B;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI;IAU7C,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAIlC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,GAAE,UAAe,GAAG,OAAO,CAAC,OAAO,CAAC;IA6BtF,cAAc,CAClB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,OAAO,EAAE,WAAW,GAAG,gBAAgB,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAiBvE,aAAa,CACjB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,MAAM,EAAE,SAAS,GAAG,SAAS,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAC;IAiBlE,OAAO,CAAC,iBAAiB;IAmDzB,OAAO,CAAC,WAAW;CAqBpB"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Tracks live browser pairings and correlates rpc requests with replies.
|
|
4
|
+
* One instance per server; shared by all LAP handlers + the upgrade
|
|
5
|
+
* handler. Spec §10.4–§10.5.
|
|
6
|
+
*/
|
|
7
|
+
export class WsPairingRegistry {
|
|
8
|
+
pairings = new Map();
|
|
9
|
+
now;
|
|
10
|
+
onLogAppend;
|
|
11
|
+
constructor(opts = {}) {
|
|
12
|
+
this.now = opts.now ?? (() => Date.now());
|
|
13
|
+
this.onLogAppend = opts.onLogAppend ?? null;
|
|
14
|
+
}
|
|
15
|
+
register(tid, conn) {
|
|
16
|
+
const p = {
|
|
17
|
+
conn,
|
|
18
|
+
hello: null,
|
|
19
|
+
pendingRpc: new Map(),
|
|
20
|
+
pendingConfirm: new Map(),
|
|
21
|
+
pendingWait: [],
|
|
22
|
+
closed: false,
|
|
23
|
+
};
|
|
24
|
+
this.pairings.set(tid, p);
|
|
25
|
+
conn.onFrame((frame) => this.handleClientFrame(tid, frame));
|
|
26
|
+
conn.onClose(() => this.handleClose(tid));
|
|
27
|
+
}
|
|
28
|
+
unregister(tid) {
|
|
29
|
+
const p = this.pairings.get(tid);
|
|
30
|
+
if (!p)
|
|
31
|
+
return;
|
|
32
|
+
this.handleClose(tid);
|
|
33
|
+
}
|
|
34
|
+
isPaired(tid) {
|
|
35
|
+
const p = this.pairings.get(tid);
|
|
36
|
+
return !!p && !p.closed;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Send a ServerFrame to the paired browser connection, if one is live.
|
|
40
|
+
* No-op when unpaired or closed.
|
|
41
|
+
*/
|
|
42
|
+
notify(tid, frame) {
|
|
43
|
+
const p = this.pairings.get(tid);
|
|
44
|
+
if (!p || p.closed)
|
|
45
|
+
return;
|
|
46
|
+
try {
|
|
47
|
+
p.conn.send(frame);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// connection may have dropped between isPaired and notify; ignore
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
getHello(tid) {
|
|
54
|
+
return this.pairings.get(tid)?.hello ?? null;
|
|
55
|
+
}
|
|
56
|
+
async rpc(tid, tool, args, opts = {}) {
|
|
57
|
+
const p = this.pairings.get(tid);
|
|
58
|
+
if (!p || p.closed) {
|
|
59
|
+
const err = { code: 'paused' };
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
const id = randomUUID();
|
|
63
|
+
const timeoutMs = opts.timeoutMs ?? 15_000;
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const entry = {
|
|
66
|
+
resolve,
|
|
67
|
+
reject,
|
|
68
|
+
timer: setTimeout(() => {
|
|
69
|
+
p.pendingRpc.delete(id);
|
|
70
|
+
reject({ code: 'timeout' });
|
|
71
|
+
}, timeoutMs),
|
|
72
|
+
};
|
|
73
|
+
p.pendingRpc.set(id, entry);
|
|
74
|
+
const frame = { t: 'rpc', id, tool, args };
|
|
75
|
+
try {
|
|
76
|
+
p.conn.send(frame);
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
p.pendingRpc.delete(id);
|
|
80
|
+
if (entry.timer)
|
|
81
|
+
clearTimeout(entry.timer);
|
|
82
|
+
reject({ code: 'internal', detail: String(e) });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async waitForConfirm(tid, confirmId, timeoutMs) {
|
|
87
|
+
const p = this.pairings.get(tid);
|
|
88
|
+
if (!p || p.closed) {
|
|
89
|
+
return { outcome: 'user-cancelled' };
|
|
90
|
+
}
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
const entry = {
|
|
93
|
+
resolve,
|
|
94
|
+
timer: setTimeout(() => {
|
|
95
|
+
p.pendingConfirm.delete(confirmId);
|
|
96
|
+
resolve({ outcome: 'user-cancelled' });
|
|
97
|
+
}, timeoutMs),
|
|
98
|
+
};
|
|
99
|
+
p.pendingConfirm.set(confirmId, entry);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async waitForChange(tid, path, timeoutMs) {
|
|
103
|
+
const p = this.pairings.get(tid);
|
|
104
|
+
if (!p || p.closed)
|
|
105
|
+
return { status: 'timeout', stateAfter: null };
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
const entry = {
|
|
108
|
+
path,
|
|
109
|
+
resolve,
|
|
110
|
+
timer: setTimeout(() => {
|
|
111
|
+
const idx = p.pendingWait.indexOf(entry);
|
|
112
|
+
if (idx >= 0)
|
|
113
|
+
p.pendingWait.splice(idx, 1);
|
|
114
|
+
resolve({ status: 'timeout', stateAfter: null });
|
|
115
|
+
}, timeoutMs),
|
|
116
|
+
};
|
|
117
|
+
p.pendingWait.push(entry);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
handleClientFrame(tid, frame) {
|
|
121
|
+
const p = this.pairings.get(tid);
|
|
122
|
+
if (!p || p.closed)
|
|
123
|
+
return;
|
|
124
|
+
switch (frame.t) {
|
|
125
|
+
case 'hello': {
|
|
126
|
+
p.hello = frame;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'rpc-reply': {
|
|
130
|
+
const e = p.pendingRpc.get(frame.id);
|
|
131
|
+
if (!e)
|
|
132
|
+
break;
|
|
133
|
+
p.pendingRpc.delete(frame.id);
|
|
134
|
+
if (e.timer)
|
|
135
|
+
clearTimeout(e.timer);
|
|
136
|
+
e.resolve(frame.result);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'rpc-error': {
|
|
140
|
+
const e = p.pendingRpc.get(frame.id);
|
|
141
|
+
if (!e)
|
|
142
|
+
break;
|
|
143
|
+
p.pendingRpc.delete(frame.id);
|
|
144
|
+
if (e.timer)
|
|
145
|
+
clearTimeout(e.timer);
|
|
146
|
+
e.reject({ code: frame.code, detail: frame.detail });
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 'confirm-resolved': {
|
|
150
|
+
const e = p.pendingConfirm.get(frame.confirmId);
|
|
151
|
+
if (!e)
|
|
152
|
+
break;
|
|
153
|
+
p.pendingConfirm.delete(frame.confirmId);
|
|
154
|
+
if (e.timer)
|
|
155
|
+
clearTimeout(e.timer);
|
|
156
|
+
e.resolve({ outcome: frame.outcome, stateAfter: frame.stateAfter });
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case 'state-update': {
|
|
160
|
+
for (let i = p.pendingWait.length - 1; i >= 0; i--) {
|
|
161
|
+
const w = p.pendingWait[i];
|
|
162
|
+
if (w === undefined)
|
|
163
|
+
continue;
|
|
164
|
+
if (w.path === undefined || w.path === frame.path || frame.path.startsWith(w.path)) {
|
|
165
|
+
p.pendingWait.splice(i, 1);
|
|
166
|
+
if (w.timer)
|
|
167
|
+
clearTimeout(w.timer);
|
|
168
|
+
w.resolve({ status: 'changed', stateAfter: frame.stateAfter });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case 'log-append': {
|
|
174
|
+
this.onLogAppend?.(tid, frame.entry);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
handleClose(tid) {
|
|
180
|
+
const p = this.pairings.get(tid);
|
|
181
|
+
if (!p)
|
|
182
|
+
return;
|
|
183
|
+
p.closed = true;
|
|
184
|
+
for (const [, e] of p.pendingRpc) {
|
|
185
|
+
if (e.timer)
|
|
186
|
+
clearTimeout(e.timer);
|
|
187
|
+
e.reject({ code: 'paused' });
|
|
188
|
+
}
|
|
189
|
+
p.pendingRpc.clear();
|
|
190
|
+
for (const [, e] of p.pendingConfirm) {
|
|
191
|
+
if (e.timer)
|
|
192
|
+
clearTimeout(e.timer);
|
|
193
|
+
e.resolve({ outcome: 'user-cancelled' });
|
|
194
|
+
}
|
|
195
|
+
p.pendingConfirm.clear();
|
|
196
|
+
for (const w of p.pendingWait) {
|
|
197
|
+
if (w.timer)
|
|
198
|
+
clearTimeout(w.timer);
|
|
199
|
+
w.resolve({ status: 'timeout', stateAfter: null });
|
|
200
|
+
}
|
|
201
|
+
p.pendingWait.length = 0;
|
|
202
|
+
this.pairings.delete(tid);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=pairing-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing-registry.js","sourceRoot":"","sources":["../../../src/server/ws/pairing-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from 'node:crypto'\nimport type { ClientFrame, ServerFrame, HelloFrame, LogEntry } from '../../protocol.js'\n\n/**\n * Thin abstraction over a WebSocket so the registry is testable with\n * a fake EventEmitter-style mock.\n */\nexport interface PairingConnection {\n send(frame: ServerFrame): void\n onFrame(handler: (f: ClientFrame) => void): void\n onClose(handler: () => void): void\n close(): void\n}\n\ntype RpcEntry = {\n resolve: (result: unknown) => void\n reject: (err: RpcError) => void\n timer: ReturnType<typeof setTimeout> | null\n}\n\ntype ConfirmEntry = {\n resolve: (r: { outcome: 'confirmed' | 'user-cancelled'; stateAfter?: unknown }) => void\n timer: ReturnType<typeof setTimeout> | null\n}\n\ntype WaitEntry = {\n path: string | undefined\n resolve: (r: { status: 'changed' | 'timeout'; stateAfter: unknown }) => void\n timer: ReturnType<typeof setTimeout> | null\n}\n\ntype Pairing = {\n conn: PairingConnection\n hello: HelloFrame | null\n pendingRpc: Map<string, RpcEntry>\n pendingConfirm: Map<string, ConfirmEntry>\n pendingWait: WaitEntry[]\n closed: boolean\n}\n\nexport type RpcError = {\n code: 'paused' | 'invalid' | 'timeout' | 'schema-error' | 'internal' | string\n detail?: string\n}\n\nexport type RpcOptions = { timeoutMs?: number }\n\n/**\n * Tracks live browser pairings and correlates rpc requests with replies.\n * One instance per server; shared by all LAP handlers + the upgrade\n * handler. Spec §10.4–§10.5.\n */\nexport class WsPairingRegistry {\n private pairings = new Map<string, Pairing>()\n private now: () => number\n private onLogAppend: ((tid: string, entry: LogEntry) => void) | null\n\n constructor(\n opts: {\n now?: () => number\n onLogAppend?: (tid: string, entry: LogEntry) => void\n } = {},\n ) {\n this.now = opts.now ?? (() => Date.now())\n this.onLogAppend = opts.onLogAppend ?? null\n }\n\n register(tid: string, conn: PairingConnection): void {\n const p: Pairing = {\n conn,\n hello: null,\n pendingRpc: new Map(),\n pendingConfirm: new Map(),\n pendingWait: [],\n closed: false,\n }\n this.pairings.set(tid, p)\n conn.onFrame((frame) => this.handleClientFrame(tid, frame))\n conn.onClose(() => this.handleClose(tid))\n }\n\n unregister(tid: string): void {\n const p = this.pairings.get(tid)\n if (!p) return\n this.handleClose(tid)\n }\n\n isPaired(tid: string): boolean {\n const p = this.pairings.get(tid)\n return !!p && !p.closed\n }\n\n /**\n * Send a ServerFrame to the paired browser connection, if one is live.\n * No-op when unpaired or closed.\n */\n notify(tid: string, frame: ServerFrame): void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return\n try {\n p.conn.send(frame)\n } catch {\n // connection may have dropped between isPaired and notify; ignore\n }\n }\n\n getHello(tid: string): HelloFrame | null {\n return this.pairings.get(tid)?.hello ?? null\n }\n\n async rpc(tid: string, tool: string, args: unknown, opts: RpcOptions = {}): Promise<unknown> {\n const p = this.pairings.get(tid)\n if (!p || p.closed) {\n const err: RpcError = { code: 'paused' }\n throw err\n }\n const id = randomUUID()\n const timeoutMs = opts.timeoutMs ?? 15_000\n return new Promise((resolve, reject) => {\n const entry: RpcEntry = {\n resolve,\n reject,\n timer: setTimeout(() => {\n p.pendingRpc.delete(id)\n reject({ code: 'timeout' } as RpcError)\n }, timeoutMs),\n }\n p.pendingRpc.set(id, entry)\n const frame: ServerFrame = { t: 'rpc', id, tool, args }\n try {\n p.conn.send(frame)\n } catch (e) {\n p.pendingRpc.delete(id)\n if (entry.timer) clearTimeout(entry.timer)\n reject({ code: 'internal', detail: String(e) } as RpcError)\n }\n })\n }\n\n async waitForConfirm(\n tid: string,\n confirmId: string,\n timeoutMs: number,\n ): Promise<{ outcome: 'confirmed' | 'user-cancelled'; stateAfter?: unknown }> {\n const p = this.pairings.get(tid)\n if (!p || p.closed) {\n return { outcome: 'user-cancelled' }\n }\n return new Promise((resolve) => {\n const entry: ConfirmEntry = {\n resolve,\n timer: setTimeout(() => {\n p.pendingConfirm.delete(confirmId)\n resolve({ outcome: 'user-cancelled' })\n }, timeoutMs),\n }\n p.pendingConfirm.set(confirmId, entry)\n })\n }\n\n async waitForChange(\n tid: string,\n path: string | undefined,\n timeoutMs: number,\n ): Promise<{ status: 'changed' | 'timeout'; stateAfter: unknown }> {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return { status: 'timeout', stateAfter: null }\n return new Promise((resolve) => {\n const entry: WaitEntry = {\n path,\n resolve,\n timer: setTimeout(() => {\n const idx = p.pendingWait.indexOf(entry)\n if (idx >= 0) p.pendingWait.splice(idx, 1)\n resolve({ status: 'timeout', stateAfter: null })\n }, timeoutMs),\n }\n p.pendingWait.push(entry)\n })\n }\n\n private handleClientFrame(tid: string, frame: ClientFrame): void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return\n switch (frame.t) {\n case 'hello': {\n p.hello = frame\n break\n }\n case 'rpc-reply': {\n const e = p.pendingRpc.get(frame.id)\n if (!e) break\n p.pendingRpc.delete(frame.id)\n if (e.timer) clearTimeout(e.timer)\n e.resolve(frame.result)\n break\n }\n case 'rpc-error': {\n const e = p.pendingRpc.get(frame.id)\n if (!e) break\n p.pendingRpc.delete(frame.id)\n if (e.timer) clearTimeout(e.timer)\n e.reject({ code: frame.code, detail: frame.detail } as RpcError)\n break\n }\n case 'confirm-resolved': {\n const e = p.pendingConfirm.get(frame.confirmId)\n if (!e) break\n p.pendingConfirm.delete(frame.confirmId)\n if (e.timer) clearTimeout(e.timer)\n e.resolve({ outcome: frame.outcome, stateAfter: frame.stateAfter })\n break\n }\n case 'state-update': {\n for (let i = p.pendingWait.length - 1; i >= 0; i--) {\n const w = p.pendingWait[i]\n if (w === undefined) continue\n if (w.path === undefined || w.path === frame.path || frame.path.startsWith(w.path)) {\n p.pendingWait.splice(i, 1)\n if (w.timer) clearTimeout(w.timer)\n w.resolve({ status: 'changed', stateAfter: frame.stateAfter })\n }\n }\n break\n }\n case 'log-append': {\n this.onLogAppend?.(tid, frame.entry)\n break\n }\n }\n }\n\n private handleClose(tid: string): void {\n const p = this.pairings.get(tid)\n if (!p) return\n p.closed = true\n for (const [, e] of p.pendingRpc) {\n if (e.timer) clearTimeout(e.timer)\n e.reject({ code: 'paused' } as RpcError)\n }\n p.pendingRpc.clear()\n for (const [, e] of p.pendingConfirm) {\n if (e.timer) clearTimeout(e.timer)\n e.resolve({ outcome: 'user-cancelled' })\n }\n p.pendingConfirm.clear()\n for (const w of p.pendingWait) {\n if (w.timer) clearTimeout(w.timer)\n w.resolve({ status: 'timeout', stateAfter: null })\n }\n p.pendingWait.length = 0\n this.pairings.delete(tid)\n }\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import type { Duplex } from 'node:stream';
|
|
3
|
+
import type { WsPairingRegistry } from './pairing-registry.js';
|
|
4
|
+
import type { TokenStore } from '../token-store.js';
|
|
5
|
+
import type { AuditSink } from '../audit.js';
|
|
6
|
+
export type UpgradeDeps = {
|
|
7
|
+
signingKey: string | Uint8Array;
|
|
8
|
+
tokenStore: TokenStore;
|
|
9
|
+
registry: WsPairingRegistry;
|
|
10
|
+
auditSink: AuditSink;
|
|
11
|
+
now?: () => number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Returns a handler for `server.on('upgrade', ...)`. Validates the token
|
|
15
|
+
* from the query string, attaches to the registry, wires frame/close
|
|
16
|
+
* routing. Unauthorized paths/tokens get a bare HTTP error response on
|
|
17
|
+
* the raw socket (per RFC 6455 the response must be sent before the
|
|
18
|
+
* socket is torn down).
|
|
19
|
+
*
|
|
20
|
+
* Spec §10.2, §10.4.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createWsUpgradeHandler(deps: UpgradeDeps): (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
|
|
23
|
+
//# sourceMappingURL=upgrade.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../../src/server/ws/upgrade.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,uBAAuB,CAAA;AACjF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAI5C,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,EAAE,MAAM,GAAG,UAAU,CAAA;IAC/B,UAAU,EAAE,UAAU,CAAA;IACtB,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,SAAS,EAAE,SAAS,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,WAAW,IAI9C,KAAK,eAAe,EAAE,QAAQ,MAAM,EAAE,MAAM,MAAM,KAAG,IAAI,CAoElE"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
import { verifyToken } from '../token.js';
|
|
3
|
+
/**
|
|
4
|
+
* Returns a handler for `server.on('upgrade', ...)`. Validates the token
|
|
5
|
+
* from the query string, attaches to the registry, wires frame/close
|
|
6
|
+
* routing. Unauthorized paths/tokens get a bare HTTP error response on
|
|
7
|
+
* the raw socket (per RFC 6455 the response must be sent before the
|
|
8
|
+
* socket is torn down).
|
|
9
|
+
*
|
|
10
|
+
* Spec §10.2, §10.4.
|
|
11
|
+
*/
|
|
12
|
+
export function createWsUpgradeHandler(deps) {
|
|
13
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
14
|
+
const now = deps.now ?? (() => Date.now());
|
|
15
|
+
return (req, socket, head) => {
|
|
16
|
+
// Path check
|
|
17
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
18
|
+
if (url.pathname !== '/agent/ws') {
|
|
19
|
+
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
|
20
|
+
socket.destroy();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Token — try query string first, then Authorization header
|
|
24
|
+
let token = url.searchParams.get('token');
|
|
25
|
+
if (!token) {
|
|
26
|
+
const auth = req.headers['authorization'];
|
|
27
|
+
if (typeof auth === 'string' && auth.startsWith('Bearer ')) {
|
|
28
|
+
token = auth.slice('Bearer '.length);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!token) {
|
|
32
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
33
|
+
socket.destroy();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const verified = verifyToken(token, deps.signingKey);
|
|
37
|
+
if (verified.kind !== 'ok') {
|
|
38
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
39
|
+
socket.destroy();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const { tid } = verified.payload;
|
|
43
|
+
// Perform upgrade, wire to registry
|
|
44
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
45
|
+
const conn = {
|
|
46
|
+
send(frame) {
|
|
47
|
+
ws.send(JSON.stringify(frame));
|
|
48
|
+
},
|
|
49
|
+
onFrame(handler) {
|
|
50
|
+
ws.on('message', (data) => {
|
|
51
|
+
const raw = typeof data === 'string' ? data : data.toString('utf8');
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
handler(parsed);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Ignore malformed frames.
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
onClose(handler) {
|
|
62
|
+
ws.on('close', handler);
|
|
63
|
+
},
|
|
64
|
+
close() {
|
|
65
|
+
ws.close();
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
deps.registry.register(tid, conn);
|
|
69
|
+
// Transition status: browser WS is now live, waiting for Claude's first call.
|
|
70
|
+
void deps.tokenStore.markAwaitingClaude(tid, now());
|
|
71
|
+
void deps.auditSink.write({
|
|
72
|
+
at: now(),
|
|
73
|
+
tid,
|
|
74
|
+
uid: null,
|
|
75
|
+
event: 'claim',
|
|
76
|
+
detail: { transport: 'ws' },
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=upgrade.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../../src/server/ws/upgrade.ts"],"names":[],"mappings":"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","sourcesContent":["import { WebSocketServer } from 'ws'\nimport type { WebSocket } from 'ws'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport type { WsPairingRegistry, PairingConnection } from './pairing-registry.js'\nimport type { TokenStore } from '../token-store.js'\nimport type { AuditSink } from '../audit.js'\nimport { verifyToken } from '../token.js'\nimport type { ClientFrame, ServerFrame } from '../../protocol.js'\n\nexport type UpgradeDeps = {\n signingKey: string | Uint8Array\n tokenStore: TokenStore\n registry: WsPairingRegistry\n auditSink: AuditSink\n now?: () => number\n}\n\n/**\n * Returns a handler for `server.on('upgrade', ...)`. Validates the token\n * from the query string, attaches to the registry, wires frame/close\n * routing. Unauthorized paths/tokens get a bare HTTP error response on\n * the raw socket (per RFC 6455 the response must be sent before the\n * socket is torn down).\n *\n * Spec §10.2, §10.4.\n */\nexport function createWsUpgradeHandler(deps: UpgradeDeps) {\n const wss = new WebSocketServer({ noServer: true })\n const now = deps.now ?? (() => Date.now())\n\n return (req: IncomingMessage, socket: Duplex, head: Buffer): void => {\n // Path check\n const url = new URL(req.url ?? '/', 'http://localhost')\n if (url.pathname !== '/agent/ws') {\n socket.write('HTTP/1.1 404 Not Found\\r\\n\\r\\n')\n socket.destroy()\n return\n }\n\n // Token — try query string first, then Authorization header\n let token = url.searchParams.get('token')\n if (!token) {\n const auth = req.headers['authorization']\n if (typeof auth === 'string' && auth.startsWith('Bearer ')) {\n token = auth.slice('Bearer '.length)\n }\n }\n if (!token) {\n socket.write('HTTP/1.1 401 Unauthorized\\r\\n\\r\\n')\n socket.destroy()\n return\n }\n\n const verified = verifyToken(token, deps.signingKey)\n if (verified.kind !== 'ok') {\n socket.write('HTTP/1.1 401 Unauthorized\\r\\n\\r\\n')\n socket.destroy()\n return\n }\n const { tid } = verified.payload\n\n // Perform upgrade, wire to registry\n wss.handleUpgrade(req, socket, head, (ws: WebSocket) => {\n const conn: PairingConnection = {\n send(frame: ServerFrame) {\n ws.send(JSON.stringify(frame))\n },\n onFrame(handler) {\n ws.on('message', (data: Buffer | string) => {\n const raw = typeof data === 'string' ? data : data.toString('utf8')\n try {\n const parsed = JSON.parse(raw) as ClientFrame\n handler(parsed)\n } catch {\n // Ignore malformed frames.\n }\n })\n },\n onClose(handler) {\n ws.on('close', handler)\n },\n close() {\n ws.close()\n },\n }\n deps.registry.register(tid, conn)\n\n // Transition status: browser WS is now live, waiting for Claude's first call.\n void deps.tokenStore.markAwaitingClaude(tid, now())\n void deps.auditSink.write({\n at: now(),\n tid,\n uid: null,\n event: 'claim',\n detail: { transport: 'ws' },\n })\n })\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@llui/agent",
|
|
3
|
+
"version": "0.0.29",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"exports": {
|
|
7
|
+
"./server": {
|
|
8
|
+
"types": "./dist/server/index.d.ts",
|
|
9
|
+
"import": "./dist/server/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./client": {
|
|
12
|
+
"types": "./dist/client/index.d.ts",
|
|
13
|
+
"import": "./dist/client/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./protocol": {
|
|
16
|
+
"types": "./dist/protocol.d.ts",
|
|
17
|
+
"import": "./dist/protocol.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"ws": "^8.18.0",
|
|
25
|
+
"@llui/dom": "0.0.29",
|
|
26
|
+
"@llui/effects": "0.0.9"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"@types/ws": "^8.5.13"
|
|
31
|
+
},
|
|
32
|
+
"description": "LLui Agent — LAP server + browser client runtime for driving LLui apps from LLM clients",
|
|
33
|
+
"keywords": [
|
|
34
|
+
"llui",
|
|
35
|
+
"agent",
|
|
36
|
+
"llm",
|
|
37
|
+
"mcp",
|
|
38
|
+
"lap"
|
|
39
|
+
],
|
|
40
|
+
"author": "Franco Ponticelli <franco.ponticelli@gmail.com>",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/fponticelli/llui.git",
|
|
45
|
+
"directory": "packages/agent"
|
|
46
|
+
},
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/fponticelli/llui/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/fponticelli/llui/tree/main/packages/agent#readme",
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc -p tsconfig.build.json",
|
|
53
|
+
"check": "tsc --noEmit",
|
|
54
|
+
"lint": "eslint src",
|
|
55
|
+
"test": "vitest run"
|
|
56
|
+
}
|
|
57
|
+
}
|