@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.
Files changed (145) hide show
  1. package/README.md +82 -1
  2. package/dist/client/agentConfirm.d.ts +48 -18
  3. package/dist/client/agentConfirm.d.ts.map +1 -1
  4. package/dist/client/agentConfirm.js +28 -25
  5. package/dist/client/agentConfirm.js.map +1 -1
  6. package/dist/client/agentConnect.d.ts +95 -34
  7. package/dist/client/agentConnect.d.ts.map +1 -1
  8. package/dist/client/agentConnect.js +85 -47
  9. package/dist/client/agentConnect.js.map +1 -1
  10. package/dist/client/agentLog.d.ts +31 -14
  11. package/dist/client/agentLog.d.ts.map +1 -1
  12. package/dist/client/agentLog.js +39 -20
  13. package/dist/client/agentLog.js.map +1 -1
  14. package/dist/client/effect-handler.d.ts +23 -0
  15. package/dist/client/effect-handler.d.ts.map +1 -1
  16. package/dist/client/effect-handler.js +185 -126
  17. package/dist/client/effect-handler.js.map +1 -1
  18. package/dist/client/effects.d.ts +13 -2
  19. package/dist/client/effects.d.ts.map +1 -1
  20. package/dist/client/effects.js.map +1 -1
  21. package/dist/client/factory.d.ts +55 -3
  22. package/dist/client/factory.d.ts.map +1 -1
  23. package/dist/client/factory.js +30 -5
  24. package/dist/client/factory.js.map +1 -1
  25. package/dist/client/rpc/describe-visible-content.d.ts +18 -5
  26. package/dist/client/rpc/describe-visible-content.d.ts.map +1 -1
  27. package/dist/client/rpc/describe-visible-content.js +112 -7
  28. package/dist/client/rpc/describe-visible-content.js.map +1 -1
  29. package/dist/client/rpc/list-actions.d.ts +52 -2
  30. package/dist/client/rpc/list-actions.d.ts.map +1 -1
  31. package/dist/client/rpc/list-actions.js +187 -5
  32. package/dist/client/rpc/list-actions.js.map +1 -1
  33. package/dist/client/rpc/query-state.d.ts +32 -0
  34. package/dist/client/rpc/query-state.d.ts.map +1 -0
  35. package/dist/client/rpc/query-state.js +82 -0
  36. package/dist/client/rpc/query-state.js.map +1 -0
  37. package/dist/client/rpc/send-message.d.ts +2 -0
  38. package/dist/client/rpc/send-message.d.ts.map +1 -1
  39. package/dist/client/rpc/send-message.js +119 -9
  40. package/dist/client/rpc/send-message.js.map +1 -1
  41. package/dist/client/rpc/would-dispatch.d.ts +66 -0
  42. package/dist/client/rpc/would-dispatch.d.ts.map +1 -0
  43. package/dist/client/rpc/would-dispatch.js +21 -0
  44. package/dist/client/rpc/would-dispatch.js.map +1 -0
  45. package/dist/client/ws-client.d.ts +3 -1
  46. package/dist/client/ws-client.d.ts.map +1 -1
  47. package/dist/client/ws-client.js +29 -0
  48. package/dist/client/ws-client.js.map +1 -1
  49. package/dist/codecs.d.ts +107 -0
  50. package/dist/codecs.d.ts.map +1 -0
  51. package/dist/codecs.js +166 -0
  52. package/dist/codecs.js.map +1 -0
  53. package/dist/protocol.d.ts +172 -12
  54. package/dist/protocol.d.ts.map +1 -1
  55. package/dist/protocol.js +7 -1
  56. package/dist/protocol.js.map +1 -1
  57. package/dist/server/cloudflare/durable-object.d.ts +11 -4
  58. package/dist/server/cloudflare/durable-object.d.ts.map +1 -1
  59. package/dist/server/cloudflare/durable-object.js.map +1 -1
  60. package/dist/server/cloudflare/index.d.ts +8 -4
  61. package/dist/server/cloudflare/index.d.ts.map +1 -1
  62. package/dist/server/cloudflare/index.js +8 -4
  63. package/dist/server/cloudflare/index.js.map +1 -1
  64. package/dist/server/cloudflare/worker.d.ts +10 -2
  65. package/dist/server/cloudflare/worker.d.ts.map +1 -1
  66. package/dist/server/cloudflare/worker.js +13 -6
  67. package/dist/server/cloudflare/worker.js.map +1 -1
  68. package/dist/server/core-entry.d.ts +2 -2
  69. package/dist/server/core-entry.d.ts.map +1 -1
  70. package/dist/server/core-entry.js +1 -1
  71. package/dist/server/core-entry.js.map +1 -1
  72. package/dist/server/core.d.ts +1 -3
  73. package/dist/server/core.d.ts.map +1 -1
  74. package/dist/server/core.js +13 -12
  75. package/dist/server/core.js.map +1 -1
  76. package/dist/server/factory.d.ts +1 -1
  77. package/dist/server/factory.d.ts.map +1 -1
  78. package/dist/server/factory.js +1 -2
  79. package/dist/server/factory.js.map +1 -1
  80. package/dist/server/http/mint.d.ts +6 -1
  81. package/dist/server/http/mint.d.ts.map +1 -1
  82. package/dist/server/http/mint.js +14 -6
  83. package/dist/server/http/mint.js.map +1 -1
  84. package/dist/server/http/resume.d.ts +3 -1
  85. package/dist/server/http/resume.d.ts.map +1 -1
  86. package/dist/server/http/resume.js +9 -7
  87. package/dist/server/http/resume.js.map +1 -1
  88. package/dist/server/index.d.ts +2 -2
  89. package/dist/server/index.d.ts.map +1 -1
  90. package/dist/server/index.js +1 -1
  91. package/dist/server/index.js.map +1 -1
  92. package/dist/server/lap/confirm-result.d.ts +0 -1
  93. package/dist/server/lap/confirm-result.d.ts.map +1 -1
  94. package/dist/server/lap/confirm-result.js +1 -1
  95. package/dist/server/lap/confirm-result.js.map +1 -1
  96. package/dist/server/lap/describe.d.ts +13 -2
  97. package/dist/server/lap/describe.d.ts.map +1 -1
  98. package/dist/server/lap/describe.js +23 -6
  99. package/dist/server/lap/describe.js.map +1 -1
  100. package/dist/server/lap/forward.d.ts +13 -1
  101. package/dist/server/lap/forward.d.ts.map +1 -1
  102. package/dist/server/lap/forward.js +75 -1
  103. package/dist/server/lap/forward.js.map +1 -1
  104. package/dist/server/lap/message.d.ts +0 -1
  105. package/dist/server/lap/message.d.ts.map +1 -1
  106. package/dist/server/lap/message.js +1 -1
  107. package/dist/server/lap/message.js.map +1 -1
  108. package/dist/server/lap/observe.d.ts +0 -1
  109. package/dist/server/lap/observe.d.ts.map +1 -1
  110. package/dist/server/lap/observe.js +1 -1
  111. package/dist/server/lap/observe.js.map +1 -1
  112. package/dist/server/lap/router.d.ts.map +1 -1
  113. package/dist/server/lap/router.js +7 -1
  114. package/dist/server/lap/router.js.map +1 -1
  115. package/dist/server/lap/wait.d.ts +0 -1
  116. package/dist/server/lap/wait.d.ts.map +1 -1
  117. package/dist/server/lap/wait.js +1 -1
  118. package/dist/server/lap/wait.js.map +1 -1
  119. package/dist/server/options.d.ts +7 -5
  120. package/dist/server/options.d.ts.map +1 -1
  121. package/dist/server/options.js.map +1 -1
  122. package/dist/server/token-store.d.ts +22 -0
  123. package/dist/server/token-store.d.ts.map +1 -1
  124. package/dist/server/token-store.js +24 -0
  125. package/dist/server/token-store.js.map +1 -1
  126. package/dist/server/token.d.ts +32 -17
  127. package/dist/server/token.d.ts.map +1 -1
  128. package/dist/server/token.js +40 -103
  129. package/dist/server/token.js.map +1 -1
  130. package/dist/server/web/upgrade.d.ts +1 -1
  131. package/dist/server/web/upgrade.js +1 -1
  132. package/dist/server/web/upgrade.js.map +1 -1
  133. package/dist/server/ws/pairing-registry.d.ts +22 -6
  134. package/dist/server/ws/pairing-registry.d.ts.map +1 -1
  135. package/dist/server/ws/pairing-registry.js +49 -0
  136. package/dist/server/ws/pairing-registry.js.map +1 -1
  137. package/dist/server/ws/upgrade.d.ts +0 -1
  138. package/dist/server/ws/upgrade.d.ts.map +1 -1
  139. package/dist/server/ws/upgrade.js +12 -4
  140. package/dist/server/ws/upgrade.js.map +1 -1
  141. package/dist/state-diff.d.ts +52 -0
  142. package/dist/state-diff.d.ts.map +1 -0
  143. package/dist/state-diff.js +119 -0
  144. package/dist/state-diff.js.map +1 -0
  145. 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;;GAEG;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,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;CACnC;AAED,qBAAa,kBAAmB,YAAW,UAAU;IACnD,OAAO,CAAC,KAAK,CAAiC;IAExC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1C,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAKnD,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;CAKzC"}
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":"AAiBA,MAAM,OAAO,kBAAkB;IACrB,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAA;IAE9C,KAAK,CAAC,MAAM,CAAC,MAAmB;QAC9B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,GAAG,MAAM,EAAE,CAAC,CAAA;IAC3C,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,GAAW;QACzB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7B,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IAC5B,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,GAAW;QAC9B,MAAM,GAAG,GAAkB,EAAE,CAAA;QAC7B,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YACpC,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG;gBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAA;QACvC,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,GAAW,EAAE,GAAW;QAClC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,CAAC,CAAC;YAAE,OAAM;QACd,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAA;IAChD,CAAC;IAED,KAAK,CAAC,iBAAiB,CAAC,GAAW,EAAE,KAAa;QAChD,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,CAAC,CAAC;YAAE,OAAM;QACd,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC,CAAA;IACpF,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,GAAW,EAAE,GAAW;QAC/C,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,CAAC,CAAC;YAAE,OAAM;QACd,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,iBAAiB,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAA;IAC3E,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,GAAW,EAAE,KAAa,EAAE,GAAW;QACtD,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,CAAC,CAAC;YAAE,OAAM;QACd,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE;YAClB,GAAG,CAAC;YACJ,MAAM,EAAE,QAAQ;YAChB,KAAK;YACL,UAAU,EAAE,GAAG;YACf,kBAAkB,EAAE,IAAI;SACzB,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,CAAC,CAAC;YAAE,OAAM;QACd,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5E,CAAC;CACF","sourcesContent":["import type { TokenRecord } from '../protocol.js'\n\n/**\n * Append-only, read-friendly storage for token records. See spec §10.3.\n */\nexport interface TokenStore {\n create(record: TokenRecord): Promise<void>\n findByTid(tid: 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\nexport class InMemoryTokenStore implements TokenStore {\n private byTid = new Map<string, TokenRecord>()\n\n async create(record: TokenRecord): Promise<void> {\n this.byTid.set(record.tid, { ...record })\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 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 }\n}\n"]}
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"]}
@@ -1,28 +1,43 @@
1
1
  import type { AgentToken } from '../protocol.js';
2
- export type TokenPayload = {
3
- tid: string;
4
- iat: number;
5
- exp: number;
6
- scope: 'agent';
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
- payload: TokenPayload;
10
+ tid: string;
11
11
  } | {
12
12
  kind: 'invalid';
13
- reason: 'malformed' | 'bad-signature' | 'expired';
13
+ reason: 'malformed' | 'unknown' | 'expired';
14
14
  };
15
15
  /**
16
- * Serialize a payload to `llui-agent_<base64url(json)>.<base64url(hmac)>`.
17
- * See spec §6.1. Async because WebCrypto's HMAC sign/verify is the
18
- * cross-runtime standard; Node, Cloudflare, Deno, and Bun all expose
19
- * `crypto.subtle` identically.
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 signToken(payload: TokenPayload, key: string | Uint8Array): Promise<AgentToken>;
31
+ export declare function mintToken(): Promise<{
32
+ token: AgentToken;
33
+ tokenHash: string;
34
+ }>;
22
35
  /**
23
- * Verify the signature, parse the payload, and check expiry.
24
- * `crypto.subtle.verify` does the constant-time compare internally,
25
- * so we don't need a separate `timingSafeEqual`.
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 verifyToken(token: string, key: string | Uint8Array, nowSec?: number): Promise<VerifyResult>;
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;AAEhD,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,WAAW,GAAG,eAAe,GAAG,SAAS,CAAA;CAAE,CAAA;AAiE1E;;;;;GAKG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,MAAM,GAAG,UAAU,GACvB,OAAO,CAAC,UAAU,CAAC,CAOrB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GAAG,UAAU,EACxB,MAAM,GAAE,MAAsC,GAC7C,OAAO,CAAC,YAAY,CAAC,CA2BvB"}
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"}
@@ -1,117 +1,54 @@
1
1
  const PREFIX = 'llui-agent_';
2
+ const TOKEN_BYTES = 32;
2
3
  /**
3
- * Normalize key + payload to `Uint8Array<ArrayBuffer>`, the shape
4
- * WebCrypto wants. Newer TS lib types parameterize `Uint8Array` over the
5
- * underlying buffer, and `TextEncoder.encode()` returns
6
- * `Uint8Array<ArrayBufferLike>` which `crypto.subtle.*` won't accept
7
- * directly. A one-shot copy is cheap (HMAC inputs are bytes-small) and
8
- * keeps the types honest without `as BufferSource` scattered at call sites.
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 toBytes(input) {
11
- const raw = typeof input === 'string' ? new TextEncoder().encode(input) : input;
12
- const buf = new ArrayBuffer(raw.byteLength);
13
- const out = new Uint8Array(buf);
14
- out.set(raw);
15
- return out;
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
- * Import a signing key as a WebCrypto `CryptoKey`. Done per call so the
29
- * caller doesn't have to pre-import and pass it around; the cost is a
30
- * microtask per sign/verify, which is negligible for our call volume
31
- * (tokens verified once per LAP HTTP request).
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 importHmacKey(key, usages) {
34
- return crypto.subtle.importKey('raw', toKeyBytes(key), { name: 'HMAC', hash: 'SHA-256' }, false, usages);
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
@@ -1 +1 @@
1
- {"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/server/token.ts"],"names":[],"mappings":"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","sourcesContent":["import 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\n/**\n * Normalize key + payload to `Uint8Array<ArrayBuffer>`, the shape\n * WebCrypto wants. Newer TS lib types parameterize `Uint8Array` over the\n * underlying buffer, and `TextEncoder.encode()` returns\n * `Uint8Array<ArrayBufferLike>` which `crypto.subtle.*` won't accept\n * directly. A one-shot copy is cheap (HMAC inputs are bytes-small) and\n * keeps the types honest without `as BufferSource` scattered at call sites.\n */\nfunction toBytes(input: string | Uint8Array): Uint8Array<ArrayBuffer> {\n const raw = typeof input === 'string' ? new TextEncoder().encode(input) : input\n const buf = new ArrayBuffer(raw.byteLength)\n const out = new Uint8Array(buf)\n out.set(raw)\n return out\n}\n\nfunction toKeyBytes(key: string | Uint8Array): Uint8Array<ArrayBuffer> {\n if (typeof key === 'string') {\n if (key.length < 32) throw new Error('signingKey must be at least 32 bytes')\n } else if (key.byteLength < 32) {\n throw new Error('signingKey must be at least 32 bytes')\n }\n return toBytes(key)\n}\n\n/**\n * Import a signing key as a WebCrypto `CryptoKey`. Done per call so the\n * caller doesn't have to pre-import and pass it around; the cost is a\n * microtask per sign/verify, which is negligible for our call volume\n * (tokens verified once per LAP HTTP request).\n */\nasync function importHmacKey(key: string | Uint8Array, usages: KeyUsage[]): Promise<CryptoKey> {\n return crypto.subtle.importKey(\n 'raw',\n toKeyBytes(key),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n usages,\n )\n}\n\nfunction toBase64Url(bytes: Uint8Array): string {\n // btoa needs a binary string; build it manually to avoid ArrayBuffer/Uint8Array quirks.\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\nfunction fromBase64Url(s: string): Uint8Array<ArrayBuffer> | null {\n try {\n const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (s.length % 4)) % 4)\n const bin = atob(b64)\n const buf = new ArrayBuffer(bin.length)\n const bytes = new Uint8Array(buf)\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)\n return bytes\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. Async because WebCrypto's HMAC sign/verify is the\n * cross-runtime standard; Node, Cloudflare, Deno, and Bun all expose\n * `crypto.subtle` identically.\n */\nexport async function signToken(\n payload: TokenPayload,\n key: string | Uint8Array,\n): Promise<AgentToken> {\n const cryptoKey = await importHmacKey(key, ['sign'])\n const jsonBytes = toBytes(JSON.stringify(payload))\n const payloadPart = toBase64Url(jsonBytes)\n const macBuf = await crypto.subtle.sign('HMAC', cryptoKey, toBytes(payloadPart))\n const sigPart = toBase64Url(new Uint8Array(macBuf))\n return (PREFIX + payloadPart + '.' + sigPart) as AgentToken\n}\n\n/**\n * Verify the signature, parse the payload, and check expiry.\n * `crypto.subtle.verify` does the constant-time compare internally,\n * so we don't need a separate `timingSafeEqual`.\n */\nexport async function verifyToken(\n token: string,\n key: string | Uint8Array,\n nowSec: number = Math.floor(Date.now() / 1000),\n): Promise<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 sigBytes = fromBase64Url(sigPart)\n if (!sigBytes) return { kind: 'invalid', reason: 'malformed' }\n\n const cryptoKey = await importHmacKey(key, ['verify'])\n const ok = await crypto.subtle.verify('HMAC', cryptoKey, sigBytes, toBytes(payloadPart))\n if (!ok) return { kind: 'invalid', reason: 'bad-signature' }\n\n const jsonBytes = fromBase64Url(payloadPart)\n if (!jsonBytes) return { kind: 'invalid', reason: 'malformed' }\n let parsed: unknown\n try {\n parsed = JSON.parse(new TextDecoder().decode(jsonBytes))\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"]}
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({ signingKey: env.AGENT_KEY })
16
+ * const agent = createLluiAgentCore()
17
17
  * export default {
18
18
  * async fetch(req, env) {
19
19
  * const url = new URL(req.url)
@@ -22,7 +22,7 @@ export function extractToken(req) {
22
22
  *
23
23
  * Usage:
24
24
  * ```ts
25
- * const agent = createLluiAgentCore({ signingKey: env.AGENT_KEY })
25
+ * const agent = createLluiAgentCore()
26
26
  * export default {
27
27
  * async fetch(req, env) {
28
28
  * 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({ signingKey: env.AGENT_KEY })\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"]}
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":"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"}
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
  /**