@llui/agent 0.0.32 → 0.0.35
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/README.md +82 -1
- package/dist/client/agentConfirm.d.ts +48 -18
- package/dist/client/agentConfirm.d.ts.map +1 -1
- package/dist/client/agentConfirm.js +28 -25
- package/dist/client/agentConfirm.js.map +1 -1
- package/dist/client/agentConnect.d.ts +95 -34
- package/dist/client/agentConnect.d.ts.map +1 -1
- package/dist/client/agentConnect.js +85 -47
- package/dist/client/agentConnect.js.map +1 -1
- package/dist/client/agentLog.d.ts +31 -14
- package/dist/client/agentLog.d.ts.map +1 -1
- package/dist/client/agentLog.js +39 -20
- package/dist/client/agentLog.js.map +1 -1
- package/dist/client/effect-handler.d.ts +23 -0
- package/dist/client/effect-handler.d.ts.map +1 -1
- package/dist/client/effect-handler.js +185 -126
- package/dist/client/effect-handler.js.map +1 -1
- package/dist/client/effects.d.ts +13 -2
- package/dist/client/effects.d.ts.map +1 -1
- package/dist/client/effects.js.map +1 -1
- package/dist/client/factory.d.ts +55 -3
- package/dist/client/factory.d.ts.map +1 -1
- package/dist/client/factory.js +30 -5
- package/dist/client/factory.js.map +1 -1
- package/dist/client/rpc/describe-visible-content.d.ts +18 -5
- package/dist/client/rpc/describe-visible-content.d.ts.map +1 -1
- package/dist/client/rpc/describe-visible-content.js +112 -7
- package/dist/client/rpc/describe-visible-content.js.map +1 -1
- package/dist/client/rpc/list-actions.d.ts +52 -2
- package/dist/client/rpc/list-actions.d.ts.map +1 -1
- package/dist/client/rpc/list-actions.js +187 -5
- package/dist/client/rpc/list-actions.js.map +1 -1
- package/dist/client/rpc/query-state.d.ts +32 -0
- package/dist/client/rpc/query-state.d.ts.map +1 -0
- package/dist/client/rpc/query-state.js +82 -0
- package/dist/client/rpc/query-state.js.map +1 -0
- package/dist/client/rpc/send-message.d.ts +2 -0
- package/dist/client/rpc/send-message.d.ts.map +1 -1
- package/dist/client/rpc/send-message.js +119 -9
- package/dist/client/rpc/send-message.js.map +1 -1
- package/dist/client/rpc/would-dispatch.d.ts +66 -0
- package/dist/client/rpc/would-dispatch.d.ts.map +1 -0
- package/dist/client/rpc/would-dispatch.js +21 -0
- package/dist/client/rpc/would-dispatch.js.map +1 -0
- package/dist/client/ws-client.d.ts +3 -1
- package/dist/client/ws-client.d.ts.map +1 -1
- package/dist/client/ws-client.js +29 -0
- package/dist/client/ws-client.js.map +1 -1
- package/dist/codecs.d.ts +107 -0
- package/dist/codecs.d.ts.map +1 -0
- package/dist/codecs.js +166 -0
- package/dist/codecs.js.map +1 -0
- package/dist/protocol.d.ts +172 -12
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +7 -1
- package/dist/protocol.js.map +1 -1
- package/dist/server/cloudflare/durable-object.d.ts +11 -4
- package/dist/server/cloudflare/durable-object.d.ts.map +1 -1
- package/dist/server/cloudflare/durable-object.js.map +1 -1
- package/dist/server/cloudflare/index.d.ts +8 -4
- package/dist/server/cloudflare/index.d.ts.map +1 -1
- package/dist/server/cloudflare/index.js +8 -4
- package/dist/server/cloudflare/index.js.map +1 -1
- package/dist/server/cloudflare/worker.d.ts +10 -2
- package/dist/server/cloudflare/worker.d.ts.map +1 -1
- package/dist/server/cloudflare/worker.js +13 -6
- package/dist/server/cloudflare/worker.js.map +1 -1
- package/dist/server/core-entry.d.ts +2 -2
- package/dist/server/core-entry.d.ts.map +1 -1
- package/dist/server/core-entry.js +1 -1
- package/dist/server/core-entry.js.map +1 -1
- package/dist/server/core.d.ts +1 -3
- package/dist/server/core.d.ts.map +1 -1
- package/dist/server/core.js +13 -12
- package/dist/server/core.js.map +1 -1
- package/dist/server/factory.d.ts +1 -1
- package/dist/server/factory.d.ts.map +1 -1
- package/dist/server/factory.js +1 -2
- package/dist/server/factory.js.map +1 -1
- package/dist/server/http/mint.d.ts +6 -1
- package/dist/server/http/mint.d.ts.map +1 -1
- package/dist/server/http/mint.js +14 -6
- package/dist/server/http/mint.js.map +1 -1
- package/dist/server/http/resume.d.ts +3 -1
- package/dist/server/http/resume.d.ts.map +1 -1
- package/dist/server/http/resume.js +9 -7
- package/dist/server/http/resume.js.map +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/lap/confirm-result.d.ts +0 -1
- package/dist/server/lap/confirm-result.d.ts.map +1 -1
- package/dist/server/lap/confirm-result.js +1 -1
- package/dist/server/lap/confirm-result.js.map +1 -1
- package/dist/server/lap/describe.d.ts +13 -2
- package/dist/server/lap/describe.d.ts.map +1 -1
- package/dist/server/lap/describe.js +23 -6
- package/dist/server/lap/describe.js.map +1 -1
- package/dist/server/lap/forward.d.ts +13 -1
- package/dist/server/lap/forward.d.ts.map +1 -1
- package/dist/server/lap/forward.js +75 -1
- package/dist/server/lap/forward.js.map +1 -1
- package/dist/server/lap/message.d.ts +0 -1
- package/dist/server/lap/message.d.ts.map +1 -1
- package/dist/server/lap/message.js +1 -1
- package/dist/server/lap/message.js.map +1 -1
- package/dist/server/lap/observe.d.ts +0 -1
- package/dist/server/lap/observe.d.ts.map +1 -1
- package/dist/server/lap/observe.js +1 -1
- package/dist/server/lap/observe.js.map +1 -1
- package/dist/server/lap/router.d.ts.map +1 -1
- package/dist/server/lap/router.js +7 -1
- package/dist/server/lap/router.js.map +1 -1
- package/dist/server/lap/wait.d.ts +0 -1
- package/dist/server/lap/wait.d.ts.map +1 -1
- package/dist/server/lap/wait.js +1 -1
- package/dist/server/lap/wait.js.map +1 -1
- package/dist/server/options.d.ts +7 -5
- package/dist/server/options.d.ts.map +1 -1
- package/dist/server/options.js.map +1 -1
- package/dist/server/token-store.d.ts +22 -0
- package/dist/server/token-store.d.ts.map +1 -1
- package/dist/server/token-store.js +24 -0
- package/dist/server/token-store.js.map +1 -1
- package/dist/server/token.d.ts +32 -17
- package/dist/server/token.d.ts.map +1 -1
- package/dist/server/token.js +40 -103
- package/dist/server/token.js.map +1 -1
- package/dist/server/web/upgrade.d.ts +1 -1
- package/dist/server/web/upgrade.js +1 -1
- package/dist/server/web/upgrade.js.map +1 -1
- package/dist/server/ws/pairing-registry.d.ts +22 -6
- package/dist/server/ws/pairing-registry.d.ts.map +1 -1
- package/dist/server/ws/pairing-registry.js +49 -0
- package/dist/server/ws/pairing-registry.js.map +1 -1
- package/dist/server/ws/upgrade.d.ts +0 -1
- package/dist/server/ws/upgrade.d.ts.map +1 -1
- package/dist/server/ws/upgrade.js +12 -4
- package/dist/server/ws/upgrade.js.map +1 -1
- package/dist/state-diff.d.ts +52 -0
- package/dist/state-diff.d.ts.map +1 -0
- package/dist/state-diff.js +119 -0
- package/dist/state-diff.js.map +1 -0
- package/package.json +7 -3
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type { TokenRecord } from '../protocol.js';
|
|
2
2
|
/**
|
|
3
3
|
* Append-only, read-friendly storage for token records. See spec §10.3.
|
|
4
|
+
*
|
|
5
|
+
* Tokens are looked up by `tokenHash` (SHA-256 of the presented bearer
|
|
6
|
+
* value) on every authenticated request. The `tid` index is kept for
|
|
7
|
+
* the resume / revoke / sessions surfaces — those operate on session
|
|
8
|
+
* IDs the user can see and copy.
|
|
4
9
|
*/
|
|
5
10
|
export interface TokenStore {
|
|
6
11
|
create(record: TokenRecord): Promise<void>;
|
|
7
12
|
findByTid(tid: string): Promise<TokenRecord | null>;
|
|
13
|
+
/**
|
|
14
|
+
* Look up a record by the SHA-256 hash of its bearer token. Returns
|
|
15
|
+
* `null` when the hash isn't in the store (the typical "this token
|
|
16
|
+
* isn't ours / has been revoked / never existed" case).
|
|
17
|
+
*/
|
|
18
|
+
findByTokenHash(tokenHash: string): Promise<TokenRecord | null>;
|
|
8
19
|
listByIdentity(uid: string): Promise<TokenRecord[]>;
|
|
9
20
|
touch(tid: string, now: number): Promise<void>;
|
|
10
21
|
markPendingResume(tid: string, until: number): Promise<void>;
|
|
@@ -12,16 +23,27 @@ export interface TokenStore {
|
|
|
12
23
|
markAwaitingClaude(tid: string, now: number): Promise<void>;
|
|
13
24
|
markActive(tid: string, label: string, now: number): Promise<void>;
|
|
14
25
|
revoke(tid: string): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Replace the bearer token's hash and bump expiry. Used by the
|
|
28
|
+
* resume-claim flow: the old token is invalidated (its hash is no
|
|
29
|
+
* longer indexed) and a freshly-minted opaque token takes its
|
|
30
|
+
* place. The `tid` stays stable so existing audit / pairing state
|
|
31
|
+
* carries over.
|
|
32
|
+
*/
|
|
33
|
+
rotateTokenHash(tid: string, newTokenHash: string, expiresAt: number): Promise<void>;
|
|
15
34
|
}
|
|
16
35
|
export declare class InMemoryTokenStore implements TokenStore {
|
|
17
36
|
private byTid;
|
|
37
|
+
private tidByTokenHash;
|
|
18
38
|
create(record: TokenRecord): Promise<void>;
|
|
19
39
|
findByTid(tid: string): Promise<TokenRecord | null>;
|
|
40
|
+
findByTokenHash(tokenHash: string): Promise<TokenRecord | null>;
|
|
20
41
|
listByIdentity(uid: string): Promise<TokenRecord[]>;
|
|
21
42
|
touch(tid: string, now: number): Promise<void>;
|
|
22
43
|
markPendingResume(tid: string, until: number): Promise<void>;
|
|
23
44
|
markAwaitingClaude(tid: string, now: number): Promise<void>;
|
|
24
45
|
markActive(tid: string, label: string, now: number): Promise<void>;
|
|
25
46
|
revoke(tid: string): Promise<void>;
|
|
47
|
+
rotateTokenHash(tid: string, newTokenHash: string, expiresAt: number): Promise<void>;
|
|
26
48
|
}
|
|
27
49
|
//# sourceMappingURL=token-store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../src/server/token-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEjD
|
|
1
|
+
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../src/server/token-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEjD;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1C,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IACnD;;;;OAIG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC/D,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAA;IACnD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9C,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5D,+FAA+F;IAC/F,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3D,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC;;;;;;OAMG;IACH,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACrF;AAED,qBAAa,kBAAmB,YAAW,UAAU;IACnD,OAAO,CAAC,KAAK,CAAiC;IAI9C,OAAO,CAAC,cAAc,CAA4B;IAE5C,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAK1C,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAKnD,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAO/D,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAQnD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM9C,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5D,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM3D,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYlE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUlC,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAO3F"}
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
export class InMemoryTokenStore {
|
|
2
2
|
byTid = new Map();
|
|
3
|
+
// Secondary index for the auth hot path. Kept in sync with `byTid`
|
|
4
|
+
// on `create`. Persistent stores would index this column at schema
|
|
5
|
+
// time; the in-memory map is the same idea minus the DB.
|
|
6
|
+
tidByTokenHash = new Map();
|
|
3
7
|
async create(record) {
|
|
4
8
|
this.byTid.set(record.tid, { ...record });
|
|
9
|
+
this.tidByTokenHash.set(record.tokenHash, record.tid);
|
|
5
10
|
}
|
|
6
11
|
async findByTid(tid) {
|
|
7
12
|
const r = this.byTid.get(tid);
|
|
8
13
|
return r ? { ...r } : null;
|
|
9
14
|
}
|
|
15
|
+
async findByTokenHash(tokenHash) {
|
|
16
|
+
const tid = this.tidByTokenHash.get(tokenHash);
|
|
17
|
+
if (!tid)
|
|
18
|
+
return null;
|
|
19
|
+
const r = this.byTid.get(tid);
|
|
20
|
+
return r ? { ...r } : null;
|
|
21
|
+
}
|
|
10
22
|
async listByIdentity(uid) {
|
|
11
23
|
const out = [];
|
|
12
24
|
for (const r of this.byTid.values()) {
|
|
@@ -50,6 +62,18 @@ export class InMemoryTokenStore {
|
|
|
50
62
|
if (!r)
|
|
51
63
|
return;
|
|
52
64
|
this.byTid.set(tid, { ...r, status: 'revoked', pendingResumeUntil: null });
|
|
65
|
+
// Drop the hash index entry so revoked tokens fail at the auth
|
|
66
|
+
// boundary even if the bearer leaks. The byTid record stays for
|
|
67
|
+
// audit / replay purposes.
|
|
68
|
+
this.tidByTokenHash.delete(r.tokenHash);
|
|
69
|
+
}
|
|
70
|
+
async rotateTokenHash(tid, newTokenHash, expiresAt) {
|
|
71
|
+
const r = this.byTid.get(tid);
|
|
72
|
+
if (!r)
|
|
73
|
+
return;
|
|
74
|
+
this.tidByTokenHash.delete(r.tokenHash);
|
|
75
|
+
this.byTid.set(tid, { ...r, tokenHash: newTokenHash, expiresAt });
|
|
76
|
+
this.tidByTokenHash.set(newTokenHash, tid);
|
|
53
77
|
}
|
|
54
78
|
}
|
|
55
79
|
//# sourceMappingURL=token-store.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../src/server/token-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../src/server/token-store.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TokenRecord } from '../protocol.js'\n\n/**\n * Append-only, read-friendly storage for token records. See spec §10.3.\n *\n * Tokens are looked up by `tokenHash` (SHA-256 of the presented bearer\n * value) on every authenticated request. The `tid` index is kept for\n * the resume / revoke / sessions surfaces — those operate on session\n * IDs the user can see and copy.\n */\nexport interface TokenStore {\n create(record: TokenRecord): Promise<void>\n findByTid(tid: string): Promise<TokenRecord | null>\n /**\n * Look up a record by the SHA-256 hash of its bearer token. Returns\n * `null` when the hash isn't in the store (the typical \"this token\n * isn't ours / has been revoked / never existed\" case).\n */\n findByTokenHash(tokenHash: string): Promise<TokenRecord | null>\n listByIdentity(uid: string): Promise<TokenRecord[]>\n touch(tid: string, now: number): Promise<void>\n markPendingResume(tid: string, until: number): Promise<void>\n /** Transition to awaiting-claude: browser WS is connected, waiting for Claude's first call. */\n markAwaitingClaude(tid: string, now: number): Promise<void>\n markActive(tid: string, label: string, now: number): Promise<void>\n revoke(tid: string): Promise<void>\n /**\n * Replace the bearer token's hash and bump expiry. Used by the\n * resume-claim flow: the old token is invalidated (its hash is no\n * longer indexed) and a freshly-minted opaque token takes its\n * place. The `tid` stays stable so existing audit / pairing state\n * carries over.\n */\n rotateTokenHash(tid: string, newTokenHash: string, expiresAt: number): Promise<void>\n}\n\nexport class InMemoryTokenStore implements TokenStore {\n private byTid = new Map<string, TokenRecord>()\n // Secondary index for the auth hot path. Kept in sync with `byTid`\n // on `create`. Persistent stores would index this column at schema\n // time; the in-memory map is the same idea minus the DB.\n private tidByTokenHash = new Map<string, string>()\n\n async create(record: TokenRecord): Promise<void> {\n this.byTid.set(record.tid, { ...record })\n this.tidByTokenHash.set(record.tokenHash, record.tid)\n }\n\n async findByTid(tid: string): Promise<TokenRecord | null> {\n const r = this.byTid.get(tid)\n return r ? { ...r } : null\n }\n\n async findByTokenHash(tokenHash: string): Promise<TokenRecord | null> {\n const tid = this.tidByTokenHash.get(tokenHash)\n if (!tid) return null\n const r = this.byTid.get(tid)\n return r ? { ...r } : null\n }\n\n async listByIdentity(uid: string): Promise<TokenRecord[]> {\n const out: TokenRecord[] = []\n for (const r of this.byTid.values()) {\n if (r.uid === uid) out.push({ ...r })\n }\n return out\n }\n\n async touch(tid: string, now: number): Promise<void> {\n const r = this.byTid.get(tid)\n if (!r) return\n this.byTid.set(tid, { ...r, lastSeenAt: now })\n }\n\n async markPendingResume(tid: string, until: number): Promise<void> {\n const r = this.byTid.get(tid)\n if (!r) return\n this.byTid.set(tid, { ...r, status: 'pending-resume', pendingResumeUntil: until })\n }\n\n async markAwaitingClaude(tid: string, now: number): Promise<void> {\n const r = this.byTid.get(tid)\n if (!r) return\n this.byTid.set(tid, { ...r, status: 'awaiting-claude', lastSeenAt: now })\n }\n\n async markActive(tid: string, label: string, now: number): Promise<void> {\n const r = this.byTid.get(tid)\n if (!r) return\n this.byTid.set(tid, {\n ...r,\n status: 'active',\n label,\n lastSeenAt: now,\n pendingResumeUntil: null,\n })\n }\n\n async revoke(tid: string): Promise<void> {\n const r = this.byTid.get(tid)\n if (!r) return\n this.byTid.set(tid, { ...r, status: 'revoked', pendingResumeUntil: null })\n // Drop the hash index entry so revoked tokens fail at the auth\n // boundary even if the bearer leaks. The byTid record stays for\n // audit / replay purposes.\n this.tidByTokenHash.delete(r.tokenHash)\n }\n\n async rotateTokenHash(tid: string, newTokenHash: string, expiresAt: number): Promise<void> {\n const r = this.byTid.get(tid)\n if (!r) return\n this.tidByTokenHash.delete(r.tokenHash)\n this.byTid.set(tid, { ...r, tokenHash: newTokenHash, expiresAt })\n this.tidByTokenHash.set(newTokenHash, tid)\n }\n}\n"]}
|
package/dist/server/token.d.ts
CHANGED
|
@@ -1,28 +1,43 @@
|
|
|
1
1
|
import type { AgentToken } from '../protocol.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Result of looking up a presented token. The `expired` reason is
|
|
4
|
+
* returned by the verify path when the token's record exists but its
|
|
5
|
+
* hard-expiry has passed; `unknown` covers both "no record" and
|
|
6
|
+
* "wrong hash" so a probe-by-hash leak surface is uniform.
|
|
7
|
+
*/
|
|
8
8
|
export type VerifyResult = {
|
|
9
9
|
kind: 'ok';
|
|
10
|
-
|
|
10
|
+
tid: string;
|
|
11
11
|
} | {
|
|
12
12
|
kind: 'invalid';
|
|
13
|
-
reason: 'malformed' | '
|
|
13
|
+
reason: 'malformed' | 'unknown' | 'expired';
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* Mint an opaque random bearer token + the SHA-256 hash the server
|
|
17
|
+
* stores as a lookup key. Tokens are 32 bytes of CSPRNG entropy (256
|
|
18
|
+
* bits) base64url-encoded with the `llui-agent_` prefix — total
|
|
19
|
+
* 54–55 chars, vs the previous JWT format's ~250.
|
|
20
|
+
*
|
|
21
|
+
* The token itself never persists; only the hash does. A leaked store
|
|
22
|
+
* therefore does not compromise live tokens, since the bearer secret
|
|
23
|
+
* isn't recoverable from the hash. This matches the standard "session
|
|
24
|
+
* cookie / API key" pattern.
|
|
25
|
+
*
|
|
26
|
+
* The opaque form is the only token format the server understands as
|
|
27
|
+
* of 0.0.35. The previous HMAC-signed JWT format is gone; clients
|
|
28
|
+
* carrying old tokens will fail with `unknown` on first call and need
|
|
29
|
+
* to remint. See CHANGELOG.
|
|
20
30
|
*/
|
|
21
|
-
export declare function
|
|
31
|
+
export declare function mintToken(): Promise<{
|
|
32
|
+
token: AgentToken;
|
|
33
|
+
tokenHash: string;
|
|
34
|
+
}>;
|
|
22
35
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
36
|
+
* Compute the SHA-256 hash of a presented bearer token. Returns `null`
|
|
37
|
+
* when the prefix is missing — the verify path uses that to fail-fast
|
|
38
|
+
* on garbage-shaped Authorization headers without a crypto round-trip.
|
|
39
|
+
* Hash is hex-encoded for portability across stores (Postgres `text`,
|
|
40
|
+
* KV string, etc.).
|
|
26
41
|
*/
|
|
27
|
-
export declare function
|
|
42
|
+
export declare function tokenHashOf(token: string): Promise<string | null>;
|
|
28
43
|
//# sourceMappingURL=token.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../src/server/token.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../src/server/token.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAKhD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC3B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,CAAA;CAAE,CAAA;AAEpE;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAMnF;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGvE"}
|
package/dist/server/token.js
CHANGED
|
@@ -1,117 +1,54 @@
|
|
|
1
1
|
const PREFIX = 'llui-agent_';
|
|
2
|
+
const TOKEN_BYTES = 32;
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Mint an opaque random bearer token + the SHA-256 hash the server
|
|
5
|
+
* stores as a lookup key. Tokens are 32 bytes of CSPRNG entropy (256
|
|
6
|
+
* bits) base64url-encoded with the `llui-agent_` prefix — total
|
|
7
|
+
* 54–55 chars, vs the previous JWT format's ~250.
|
|
8
|
+
*
|
|
9
|
+
* The token itself never persists; only the hash does. A leaked store
|
|
10
|
+
* therefore does not compromise live tokens, since the bearer secret
|
|
11
|
+
* isn't recoverable from the hash. This matches the standard "session
|
|
12
|
+
* cookie / API key" pattern.
|
|
13
|
+
*
|
|
14
|
+
* The opaque form is the only token format the server understands as
|
|
15
|
+
* of 0.0.35. The previous HMAC-signed JWT format is gone; clients
|
|
16
|
+
* carrying old tokens will fail with `unknown` on first call and need
|
|
17
|
+
* to remint. See CHANGELOG.
|
|
9
18
|
*/
|
|
10
|
-
function
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
return
|
|
16
|
-
}
|
|
17
|
-
function toKeyBytes(key) {
|
|
18
|
-
if (typeof key === 'string') {
|
|
19
|
-
if (key.length < 32)
|
|
20
|
-
throw new Error('signingKey must be at least 32 bytes');
|
|
21
|
-
}
|
|
22
|
-
else if (key.byteLength < 32) {
|
|
23
|
-
throw new Error('signingKey must be at least 32 bytes');
|
|
24
|
-
}
|
|
25
|
-
return toBytes(key);
|
|
19
|
+
export async function mintToken() {
|
|
20
|
+
const bytes = new Uint8Array(TOKEN_BYTES);
|
|
21
|
+
crypto.getRandomValues(bytes);
|
|
22
|
+
const token = (PREFIX + toBase64Url(bytes));
|
|
23
|
+
const tokenHash = await sha256Hex(token);
|
|
24
|
+
return { token, tokenHash };
|
|
26
25
|
}
|
|
27
26
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
27
|
+
* Compute the SHA-256 hash of a presented bearer token. Returns `null`
|
|
28
|
+
* when the prefix is missing — the verify path uses that to fail-fast
|
|
29
|
+
* on garbage-shaped Authorization headers without a crypto round-trip.
|
|
30
|
+
* Hash is hex-encoded for portability across stores (Postgres `text`,
|
|
31
|
+
* KV string, etc.).
|
|
32
32
|
*/
|
|
33
|
-
async function
|
|
34
|
-
|
|
33
|
+
export async function tokenHashOf(token) {
|
|
34
|
+
if (!token.startsWith(PREFIX))
|
|
35
|
+
return null;
|
|
36
|
+
return sha256Hex(token);
|
|
37
|
+
}
|
|
38
|
+
async function sha256Hex(s) {
|
|
39
|
+
const bytes = new TextEncoder().encode(s);
|
|
40
|
+
const buf = await crypto.subtle.digest('SHA-256', bytes);
|
|
41
|
+
const arr = new Uint8Array(buf);
|
|
42
|
+
let out = '';
|
|
43
|
+
for (let i = 0; i < arr.length; i++) {
|
|
44
|
+
out += arr[i].toString(16).padStart(2, '0');
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
35
47
|
}
|
|
36
48
|
function toBase64Url(bytes) {
|
|
37
|
-
// btoa needs a binary string; build it manually to avoid ArrayBuffer/Uint8Array quirks.
|
|
38
49
|
let bin = '';
|
|
39
50
|
for (let i = 0; i < bytes.byteLength; i++)
|
|
40
51
|
bin += String.fromCharCode(bytes[i]);
|
|
41
52
|
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
42
53
|
}
|
|
43
|
-
function fromBase64Url(s) {
|
|
44
|
-
try {
|
|
45
|
-
const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (s.length % 4)) % 4);
|
|
46
|
-
const bin = atob(b64);
|
|
47
|
-
const buf = new ArrayBuffer(bin.length);
|
|
48
|
-
const bytes = new Uint8Array(buf);
|
|
49
|
-
for (let i = 0; i < bin.length; i++)
|
|
50
|
-
bytes[i] = bin.charCodeAt(i);
|
|
51
|
-
return bytes;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Serialize a payload to `llui-agent_<base64url(json)>.<base64url(hmac)>`.
|
|
59
|
-
* See spec §6.1. Async because WebCrypto's HMAC sign/verify is the
|
|
60
|
-
* cross-runtime standard; Node, Cloudflare, Deno, and Bun all expose
|
|
61
|
-
* `crypto.subtle` identically.
|
|
62
|
-
*/
|
|
63
|
-
export async function signToken(payload, key) {
|
|
64
|
-
const cryptoKey = await importHmacKey(key, ['sign']);
|
|
65
|
-
const jsonBytes = toBytes(JSON.stringify(payload));
|
|
66
|
-
const payloadPart = toBase64Url(jsonBytes);
|
|
67
|
-
const macBuf = await crypto.subtle.sign('HMAC', cryptoKey, toBytes(payloadPart));
|
|
68
|
-
const sigPart = toBase64Url(new Uint8Array(macBuf));
|
|
69
|
-
return (PREFIX + payloadPart + '.' + sigPart);
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Verify the signature, parse the payload, and check expiry.
|
|
73
|
-
* `crypto.subtle.verify` does the constant-time compare internally,
|
|
74
|
-
* so we don't need a separate `timingSafeEqual`.
|
|
75
|
-
*/
|
|
76
|
-
export async function verifyToken(token, key, nowSec = Math.floor(Date.now() / 1000)) {
|
|
77
|
-
if (!token.startsWith(PREFIX))
|
|
78
|
-
return { kind: 'invalid', reason: 'malformed' };
|
|
79
|
-
const body = token.slice(PREFIX.length);
|
|
80
|
-
const dot = body.indexOf('.');
|
|
81
|
-
if (dot < 0)
|
|
82
|
-
return { kind: 'invalid', reason: 'malformed' };
|
|
83
|
-
const payloadPart = body.slice(0, dot);
|
|
84
|
-
const sigPart = body.slice(dot + 1);
|
|
85
|
-
const sigBytes = fromBase64Url(sigPart);
|
|
86
|
-
if (!sigBytes)
|
|
87
|
-
return { kind: 'invalid', reason: 'malformed' };
|
|
88
|
-
const cryptoKey = await importHmacKey(key, ['verify']);
|
|
89
|
-
const ok = await crypto.subtle.verify('HMAC', cryptoKey, sigBytes, toBytes(payloadPart));
|
|
90
|
-
if (!ok)
|
|
91
|
-
return { kind: 'invalid', reason: 'bad-signature' };
|
|
92
|
-
const jsonBytes = fromBase64Url(payloadPart);
|
|
93
|
-
if (!jsonBytes)
|
|
94
|
-
return { kind: 'invalid', reason: 'malformed' };
|
|
95
|
-
let parsed;
|
|
96
|
-
try {
|
|
97
|
-
parsed = JSON.parse(new TextDecoder().decode(jsonBytes));
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return { kind: 'invalid', reason: 'malformed' };
|
|
101
|
-
}
|
|
102
|
-
if (!isTokenPayload(parsed))
|
|
103
|
-
return { kind: 'invalid', reason: 'malformed' };
|
|
104
|
-
if (parsed.exp <= nowSec)
|
|
105
|
-
return { kind: 'invalid', reason: 'expired' };
|
|
106
|
-
return { kind: 'ok', payload: parsed };
|
|
107
|
-
}
|
|
108
|
-
function isTokenPayload(x) {
|
|
109
|
-
if (!x || typeof x !== 'object')
|
|
110
|
-
return false;
|
|
111
|
-
const o = x;
|
|
112
|
-
return (typeof o.tid === 'string' &&
|
|
113
|
-
typeof o.iat === 'number' &&
|
|
114
|
-
typeof o.exp === 'number' &&
|
|
115
|
-
o.scope === 'agent');
|
|
116
|
-
}
|
|
117
54
|
//# sourceMappingURL=token.js.map
|
package/dist/server/token.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/server/token.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/server/token.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG,aAAa,CAAA;AAC5B,MAAM,WAAW,GAAG,EAAE,CAAA;AAYtB;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,WAAW,CAAC,CAAA;IACzC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;IAC7B,MAAM,KAAK,GAAG,CAAC,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAe,CAAA;IACzD,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAA;IACxC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;AAC7B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,KAAa;IAC7C,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAA;IAC1C,OAAO,SAAS,CAAC,KAAK,CAAC,CAAA;AACzB,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,CAAS;IAChC,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IACzC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;IACxD,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,CAAA;IAC/B,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IAC9C,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAS,WAAW,CAAC,KAAiB;IACpC,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,UAAU,EAAE,CAAC,EAAE;QAAE,GAAG,IAAI,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAA;IAChF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AAC7E,CAAC","sourcesContent":["import type { AgentToken } from '../protocol.js'\n\nconst PREFIX = 'llui-agent_'\nconst TOKEN_BYTES = 32\n\n/**\n * Result of looking up a presented token. The `expired` reason is\n * returned by the verify path when the token's record exists but its\n * hard-expiry has passed; `unknown` covers both \"no record\" and\n * \"wrong hash\" so a probe-by-hash leak surface is uniform.\n */\nexport type VerifyResult =\n | { kind: 'ok'; tid: string }\n | { kind: 'invalid'; reason: 'malformed' | 'unknown' | 'expired' }\n\n/**\n * Mint an opaque random bearer token + the SHA-256 hash the server\n * stores as a lookup key. Tokens are 32 bytes of CSPRNG entropy (256\n * bits) base64url-encoded with the `llui-agent_` prefix — total\n * 54–55 chars, vs the previous JWT format's ~250.\n *\n * The token itself never persists; only the hash does. A leaked store\n * therefore does not compromise live tokens, since the bearer secret\n * isn't recoverable from the hash. This matches the standard \"session\n * cookie / API key\" pattern.\n *\n * The opaque form is the only token format the server understands as\n * of 0.0.35. The previous HMAC-signed JWT format is gone; clients\n * carrying old tokens will fail with `unknown` on first call and need\n * to remint. See CHANGELOG.\n */\nexport async function mintToken(): Promise<{ token: AgentToken; tokenHash: string }> {\n const bytes = new Uint8Array(TOKEN_BYTES)\n crypto.getRandomValues(bytes)\n const token = (PREFIX + toBase64Url(bytes)) as AgentToken\n const tokenHash = await sha256Hex(token)\n return { token, tokenHash }\n}\n\n/**\n * Compute the SHA-256 hash of a presented bearer token. Returns `null`\n * when the prefix is missing — the verify path uses that to fail-fast\n * on garbage-shaped Authorization headers without a crypto round-trip.\n * Hash is hex-encoded for portability across stores (Postgres `text`,\n * KV string, etc.).\n */\nexport async function tokenHashOf(token: string): Promise<string | null> {\n if (!token.startsWith(PREFIX)) return null\n return sha256Hex(token)\n}\n\nasync function sha256Hex(s: string): Promise<string> {\n const bytes = new TextEncoder().encode(s)\n const buf = await crypto.subtle.digest('SHA-256', bytes)\n const arr = new Uint8Array(buf)\n let out = ''\n for (let i = 0; i < arr.length; i++) {\n out += arr[i]!.toString(16).padStart(2, '0')\n }\n return out\n}\n\nfunction toBase64Url(bytes: Uint8Array): string {\n let bin = ''\n for (let i = 0; i < bytes.byteLength; i++) bin += String.fromCharCode(bytes[i]!)\n return btoa(bin).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n"]}
|
|
@@ -13,7 +13,7 @@ export declare function extractToken(req: Request): string | null;
|
|
|
13
13
|
*
|
|
14
14
|
* Usage:
|
|
15
15
|
* ```ts
|
|
16
|
-
* const agent = createLluiAgentCore(
|
|
16
|
+
* const agent = createLluiAgentCore()
|
|
17
17
|
* export default {
|
|
18
18
|
* async fetch(req, env) {
|
|
19
19
|
* const url = new URL(req.url)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../../src/server/web/upgrade.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentCoreHandle } from '../core.js'\nimport { createWHATWGPairingConnection } from './adapter.js'\n\n/**\n * Extract the bearer token from a LAP WebSocket upgrade request.\n * Accepts the token on either `?token=` or `Authorization: Bearer` —\n * query-string is the common pattern because browsers can't set\n * arbitrary headers on WebSocket construction.\n */\nexport function extractToken(req: Request): string | null {\n const url = new URL(req.url)\n const q = url.searchParams.get('token')\n if (q) return q\n const auth = req.headers.get('authorization')\n if (auth?.startsWith('Bearer ')) return auth.slice('Bearer '.length)\n return null\n}\n\n/**\n * Cloudflare Workers handler. Accepts a WebSocket upgrade using\n * `WebSocketPair`, validates the token via\n * `agent.acceptConnection`, and returns the 101 upgrade Response.\n *\n * Usage:\n * ```ts\n * const agent = createLluiAgentCore(
|
|
1
|
+
{"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../../src/server/web/upgrade.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentCoreHandle } from '../core.js'\nimport { createWHATWGPairingConnection } from './adapter.js'\n\n/**\n * Extract the bearer token from a LAP WebSocket upgrade request.\n * Accepts the token on either `?token=` or `Authorization: Bearer` —\n * query-string is the common pattern because browsers can't set\n * arbitrary headers on WebSocket construction.\n */\nexport function extractToken(req: Request): string | null {\n const url = new URL(req.url)\n const q = url.searchParams.get('token')\n if (q) return q\n const auth = req.headers.get('authorization')\n if (auth?.startsWith('Bearer ')) return auth.slice('Bearer '.length)\n return null\n}\n\n/**\n * Cloudflare Workers handler. Accepts a WebSocket upgrade using\n * `WebSocketPair`, validates the token via\n * `agent.acceptConnection`, and returns the 101 upgrade Response.\n *\n * Usage:\n * ```ts\n * const agent = createLluiAgentCore()\n * export default {\n * async fetch(req, env) {\n * const url = new URL(req.url)\n * if (url.pathname === '/agent/ws') return handleCloudflareUpgrade(req, agent)\n * return (await agent.router(req)) ?? new Response('Not Found', { status: 404 })\n * },\n * }\n * ```\n */\nexport async function handleCloudflareUpgrade(\n req: Request,\n agent: AgentCoreHandle,\n): Promise<Response> {\n if (req.headers.get('upgrade') !== 'websocket') {\n return new Response('Expected upgrade: websocket', { status: 426 })\n }\n const token = extractToken(req)\n if (!token) return new Response('Unauthorized', { status: 401 })\n\n // `WebSocketPair` is a Cloudflare Workers global. We reference it\n // through `globalThis` so importing this module in non-CF runtimes\n // (e.g. during type-checking on Node) doesn't crash.\n const Pair = (\n globalThis as unknown as { WebSocketPair?: new () => { 0: WebSocket; 1: WebSocket } }\n ).WebSocketPair\n if (!Pair) {\n return new Response('WebSocketPair unavailable in this runtime', { status: 501 })\n }\n const pair = new Pair()\n const client = pair[0]\n const server = pair[1]!\n // `accept()` on the server half is Cloudflare-specific — it tells\n // the runtime the Worker will handle the WebSocket itself.\n ;(server as unknown as { accept: () => void }).accept()\n\n const conn = createWHATWGPairingConnection(server)\n const result = await agent.acceptConnection(token, conn)\n if (!result.ok) {\n conn.close()\n return new Response(result.code, { status: result.status })\n }\n\n // `webSocket` on ResponseInit is Cloudflare-specific; cast to satisfy\n // the standard lib types.\n return new Response(null, { status: 101, webSocket: client } as ResponseInit & {\n webSocket: WebSocket\n })\n}\n\n/**\n * Deno handler. Uses `Deno.upgradeWebSocket(req)` to produce the\n * response + socket pair, then plugs the socket into the registry.\n *\n * Usage:\n * ```ts\n * Deno.serve(async (req) => {\n * const url = new URL(req.url)\n * if (url.pathname === '/agent/ws') return handleDenoUpgrade(req, agent)\n * return (await agent.router(req)) ?? new Response('Not Found', { status: 404 })\n * })\n * ```\n */\nexport async function handleDenoUpgrade(req: Request, agent: AgentCoreHandle): Promise<Response> {\n const token = extractToken(req)\n if (!token) return new Response('Unauthorized', { status: 401 })\n\n const Deno_ = (\n globalThis as unknown as {\n Deno?: {\n upgradeWebSocket: (req: Request) => { socket: WebSocket; response: Response }\n }\n }\n ).Deno\n if (!Deno_) {\n return new Response('Deno.upgradeWebSocket unavailable in this runtime', { status: 501 })\n }\n\n const { socket, response } = Deno_.upgradeWebSocket(req)\n const conn = createWHATWGPairingConnection(socket)\n\n // Deno opens the socket asynchronously; validate the token first,\n // then register on `open` so frames aren't missed.\n socket.addEventListener(\n 'open',\n () => {\n void agent.acceptConnection(token, conn).then((result) => {\n if (!result.ok) conn.close()\n })\n },\n { once: true },\n )\n\n return response\n}\n"]}
|
|
@@ -56,6 +56,14 @@ export interface PairingRegistry {
|
|
|
56
56
|
* close fire synchronously. Returns an unsubscribe function.
|
|
57
57
|
*/
|
|
58
58
|
onClose(tid: string, handler: () => void): () => void;
|
|
59
|
+
/**
|
|
60
|
+
* Read the most recent `n` log entries for a tid (newest first).
|
|
61
|
+
* Backed by an in-memory ring buffer populated as the registry
|
|
62
|
+
* sees `log-append` frames; capped per-tid to bound memory across
|
|
63
|
+
* long-lived sessions. Drained on close. Returns an empty array
|
|
64
|
+
* for unknown tids.
|
|
65
|
+
*/
|
|
66
|
+
getRecentLog(tid: string, n: number): LogEntry[];
|
|
59
67
|
/**
|
|
60
68
|
* Send a typed rpc frame and await its matching reply. See
|
|
61
69
|
* `./rpc.ts::rpc` for the full contract.
|
|
@@ -72,18 +80,26 @@ export interface PairingRegistry {
|
|
|
72
80
|
stateAfter: unknown;
|
|
73
81
|
}>;
|
|
74
82
|
}
|
|
75
|
-
/**
|
|
76
|
-
* Single-process in-memory registry. Correct for Node/Bun/Deno/Deno
|
|
77
|
-
* Deploy — anywhere the server process can hold a long-lived
|
|
78
|
-
* WebSocket. Not suitable for stateless Worker isolates; use the
|
|
79
|
-
* Durable Object registry for Cloudflare.
|
|
80
|
-
*/
|
|
81
83
|
export declare class InMemoryPairingRegistry implements PairingRegistry {
|
|
82
84
|
private pairings;
|
|
83
85
|
private onLogAppend;
|
|
86
|
+
/**
|
|
87
|
+
* Per-tid ring buffer of recent log entries. Populated as the
|
|
88
|
+
* registry sees `log-append` frames; trimmed to RECENT_LOG_CAP.
|
|
89
|
+
* The agent reads this via `describe_recent_actions` to introspect
|
|
90
|
+
* its own activity history with stateDiffs intact.
|
|
91
|
+
*/
|
|
92
|
+
private recentLog;
|
|
84
93
|
constructor(opts?: {
|
|
85
94
|
onLogAppend?: (tid: string, entry: LogEntry) => void;
|
|
86
95
|
});
|
|
96
|
+
/**
|
|
97
|
+
* Read the most recent `n` log entries for a tid, newest-first. Returns
|
|
98
|
+
* an empty array when the tid is unknown or has no recorded activity.
|
|
99
|
+
* Drained from the in-memory ring buffer; entries older than
|
|
100
|
+
* RECENT_LOG_CAP have already been trimmed.
|
|
101
|
+
*/
|
|
102
|
+
getRecentLog(tid: string, n: number): LogEntry[];
|
|
87
103
|
register(tid: string, conn: PairingConnection): void;
|
|
88
104
|
unregister(tid: string): void;
|
|
89
105
|
isPaired(tid: string): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pairing-registry.d.ts","sourceRoot":"","sources":["../../../src/server/ws/pairing-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACvF,OAAO,EAIL,KAAK,UAAU,EACf,KAAK,QAAQ,EACd,MAAM,UAAU,CAAA;AAEjB,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAA;AAEpC;;;;;GAKG;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;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAA;AAE7D;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAe;IAE9B,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACpD,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAAA;IACxC,gEAAgE;IAChE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IAC3C;;;;;OAKG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,MAAM,IAAI,CAAA;IAC5D;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"pairing-registry.d.ts","sourceRoot":"","sources":["../../../src/server/ws/pairing-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,12 +5,43 @@ import { rpc as rpcHelper, waitForConfirm as waitForConfirmHelper, waitForChange
|
|
|
5
5
|
* WebSocket. Not suitable for stateless Worker isolates; use the
|
|
6
6
|
* Durable Object registry for Cloudflare.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Per-tid cap on the recent-log ring buffer. Sized to cover a few
|
|
10
|
+
* minutes of agent activity at typical dispatch rates without
|
|
11
|
+
* growing unboundedly for long-lived sessions. Reads via
|
|
12
|
+
* `getRecentLog` clamp to this; agents asking for more get whatever
|
|
13
|
+
* the buffer currently holds.
|
|
14
|
+
*/
|
|
15
|
+
const RECENT_LOG_CAP = 100;
|
|
8
16
|
export class InMemoryPairingRegistry {
|
|
9
17
|
pairings = new Map();
|
|
10
18
|
onLogAppend;
|
|
19
|
+
/**
|
|
20
|
+
* Per-tid ring buffer of recent log entries. Populated as the
|
|
21
|
+
* registry sees `log-append` frames; trimmed to RECENT_LOG_CAP.
|
|
22
|
+
* The agent reads this via `describe_recent_actions` to introspect
|
|
23
|
+
* its own activity history with stateDiffs intact.
|
|
24
|
+
*/
|
|
25
|
+
recentLog = new Map();
|
|
11
26
|
constructor(opts = {}) {
|
|
12
27
|
this.onLogAppend = opts.onLogAppend ?? null;
|
|
13
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Read the most recent `n` log entries for a tid, newest-first. Returns
|
|
31
|
+
* an empty array when the tid is unknown or has no recorded activity.
|
|
32
|
+
* Drained from the in-memory ring buffer; entries older than
|
|
33
|
+
* RECENT_LOG_CAP have already been trimmed.
|
|
34
|
+
*/
|
|
35
|
+
getRecentLog(tid, n) {
|
|
36
|
+
const buf = this.recentLog.get(tid);
|
|
37
|
+
if (!buf || buf.length === 0)
|
|
38
|
+
return [];
|
|
39
|
+
const count = Math.min(Math.max(0, Math.floor(n)), buf.length);
|
|
40
|
+
if (count === 0)
|
|
41
|
+
return [];
|
|
42
|
+
// Buffer is append-order; return the tail reversed so newest is first.
|
|
43
|
+
return buf.slice(-count).reverse();
|
|
44
|
+
}
|
|
14
45
|
register(tid, conn) {
|
|
15
46
|
const p = {
|
|
16
47
|
conn,
|
|
@@ -77,6 +108,19 @@ export class InMemoryPairingRegistry {
|
|
|
77
108
|
return;
|
|
78
109
|
}
|
|
79
110
|
if (frame.t === 'log-append') {
|
|
111
|
+
// Push into the ring buffer for `describe_recent_actions`,
|
|
112
|
+
// capped to RECENT_LOG_CAP. The audit-sink callback runs
|
|
113
|
+
// alongside; both are independent observers of the same
|
|
114
|
+
// log-append stream.
|
|
115
|
+
let buf = this.recentLog.get(tid);
|
|
116
|
+
if (!buf) {
|
|
117
|
+
buf = [];
|
|
118
|
+
this.recentLog.set(tid, buf);
|
|
119
|
+
}
|
|
120
|
+
buf.push(frame.entry);
|
|
121
|
+
if (buf.length > RECENT_LOG_CAP) {
|
|
122
|
+
buf.splice(0, buf.length - RECENT_LOG_CAP);
|
|
123
|
+
}
|
|
80
124
|
this.onLogAppend?.(tid, frame.entry);
|
|
81
125
|
return;
|
|
82
126
|
}
|
|
@@ -131,6 +175,11 @@ export class InMemoryPairingRegistry {
|
|
|
131
175
|
p.closeHandlers.clear();
|
|
132
176
|
p.subscribers.clear();
|
|
133
177
|
this.pairings.delete(tid);
|
|
178
|
+
// Drop the recent-log ring buffer — once the pairing is gone,
|
|
179
|
+
// `describe_recent_actions` will reject anyway (paused/revoked
|
|
180
|
+
// gates run before the registry lookup), but holding the entries
|
|
181
|
+
// would leak memory across reconnects.
|
|
182
|
+
this.recentLog.delete(tid);
|
|
134
183
|
}
|
|
135
184
|
}
|
|
136
185
|
/**
|