@schlessera/brain-ui-server 0.34.1 → 0.35.0

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 (169) hide show
  1. package/README.md +28 -0
  2. package/dist/activity/notify.d.ts.map +1 -1
  3. package/dist/activity/notify.js +18 -8
  4. package/dist/activity/notify.js.map +1 -1
  5. package/dist/activity/push-sender.d.ts +7 -3
  6. package/dist/activity/push-sender.d.ts.map +1 -1
  7. package/dist/activity/push-sender.js +45 -11
  8. package/dist/activity/push-sender.js.map +1 -1
  9. package/dist/activity/query.d.ts.map +1 -1
  10. package/dist/activity/query.js +8 -0
  11. package/dist/activity/query.js.map +1 -1
  12. package/dist/activity/recorder.d.ts +11 -1
  13. package/dist/activity/recorder.d.ts.map +1 -1
  14. package/dist/activity/recorder.js +52 -2
  15. package/dist/activity/recorder.js.map +1 -1
  16. package/dist/activity/runtime.d.ts.map +1 -1
  17. package/dist/activity/runtime.js +2 -5
  18. package/dist/activity/runtime.js.map +1 -1
  19. package/dist/activity/sql.d.ts +3 -3
  20. package/dist/activity/sql.d.ts.map +1 -1
  21. package/dist/activity/sql.js +7 -5
  22. package/dist/activity/sql.js.map +1 -1
  23. package/dist/activity/store.d.ts +7 -0
  24. package/dist/activity/store.d.ts.map +1 -1
  25. package/dist/activity/store.js +15 -3
  26. package/dist/activity/store.js.map +1 -1
  27. package/dist/activity/stream.d.ts +3 -1
  28. package/dist/activity/stream.d.ts.map +1 -1
  29. package/dist/activity/stream.js +32 -17
  30. package/dist/activity/stream.js.map +1 -1
  31. package/dist/app-env.d.ts +8 -0
  32. package/dist/app-env.d.ts.map +1 -0
  33. package/dist/app-env.js +2 -0
  34. package/dist/app-env.js.map +1 -0
  35. package/dist/app.d.ts.map +1 -1
  36. package/dist/app.js +28 -3
  37. package/dist/app.js.map +1 -1
  38. package/dist/brain/client.d.ts +4 -2
  39. package/dist/brain/client.d.ts.map +1 -1
  40. package/dist/brain/client.js +60 -20
  41. package/dist/brain/client.js.map +1 -1
  42. package/dist/brain/types.d.ts +9 -0
  43. package/dist/brain/types.d.ts.map +1 -1
  44. package/dist/cron/run-job.d.ts.map +1 -1
  45. package/dist/cron/run-job.js +60 -10
  46. package/dist/cron/run-job.js.map +1 -1
  47. package/dist/db/principals.d.ts +72 -0
  48. package/dist/db/principals.d.ts.map +1 -0
  49. package/dist/db/principals.js +294 -0
  50. package/dist/db/principals.js.map +1 -0
  51. package/dist/files/walker.d.ts.map +1 -1
  52. package/dist/files/walker.js +6 -1
  53. package/dist/files/walker.js.map +1 -1
  54. package/dist/index.d.ts +4 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/middleware/auth.d.ts +37 -9
  59. package/dist/middleware/auth.d.ts.map +1 -1
  60. package/dist/middleware/auth.js +148 -74
  61. package/dist/middleware/auth.js.map +1 -1
  62. package/dist/middleware/passkeys.d.ts +9 -7
  63. package/dist/middleware/passkeys.d.ts.map +1 -1
  64. package/dist/middleware/passkeys.js +25 -7
  65. package/dist/middleware/passkeys.js.map +1 -1
  66. package/dist/middleware/principals.d.ts +18 -0
  67. package/dist/middleware/principals.d.ts.map +1 -0
  68. package/dist/middleware/principals.js +138 -0
  69. package/dist/middleware/principals.js.map +1 -0
  70. package/dist/middleware/require-owner.d.ts +5 -0
  71. package/dist/middleware/require-owner.d.ts.map +1 -0
  72. package/dist/middleware/require-owner.js +10 -0
  73. package/dist/middleware/require-owner.js.map +1 -0
  74. package/dist/observability/loggers.js +1 -1
  75. package/dist/observability/loggers.js.map +1 -1
  76. package/dist/routes/activity.d.ts.map +1 -1
  77. package/dist/routes/activity.js +3 -0
  78. package/dist/routes/activity.js.map +1 -1
  79. package/dist/routes/brain.d.ts.map +1 -1
  80. package/dist/routes/brain.js +102 -63
  81. package/dist/routes/brain.js.map +1 -1
  82. package/dist/routes/files.d.ts.map +1 -1
  83. package/dist/routes/files.js +7 -2
  84. package/dist/routes/files.js.map +1 -1
  85. package/dist/routes/push.d.ts +61 -2
  86. package/dist/routes/push.d.ts.map +1 -1
  87. package/dist/routes/push.js +8 -3
  88. package/dist/routes/push.js.map +1 -1
  89. package/dist/routes/sessions.d.ts.map +1 -1
  90. package/dist/routes/sessions.js +30 -4
  91. package/dist/routes/sessions.js.map +1 -1
  92. package/dist/share/staging.d.ts.map +1 -1
  93. package/dist/share/staging.js +11 -4
  94. package/dist/share/staging.js.map +1 -1
  95. package/dist/ws/bridge.d.ts.map +1 -1
  96. package/dist/ws/bridge.js +6 -2
  97. package/dist/ws/bridge.js.map +1 -1
  98. package/dist/ws/clients.d.ts +9 -2
  99. package/dist/ws/clients.d.ts.map +1 -1
  100. package/dist/ws/clients.js +44 -6
  101. package/dist/ws/clients.js.map +1 -1
  102. package/dist/ws/connection.d.ts +7 -4
  103. package/dist/ws/connection.d.ts.map +1 -1
  104. package/dist/ws/connection.js +83 -12
  105. package/dist/ws/connection.js.map +1 -1
  106. package/dist/ws/dispatch.d.ts +12 -1
  107. package/dist/ws/dispatch.d.ts.map +1 -1
  108. package/dist/ws/dispatch.js +46 -6
  109. package/dist/ws/dispatch.js.map +1 -1
  110. package/dist/ws/history.d.ts.map +1 -1
  111. package/dist/ws/history.js +7 -5
  112. package/dist/ws/history.js.map +1 -1
  113. package/dist/ws/host.d.ts +22 -0
  114. package/dist/ws/host.d.ts.map +1 -1
  115. package/dist/ws/host.js +39 -1
  116. package/dist/ws/host.js.map +1 -1
  117. package/dist/ws/run-session.d.ts +7 -7
  118. package/dist/ws/run-session.d.ts.map +1 -1
  119. package/dist/ws/run-session.js +208 -52
  120. package/dist/ws/run-session.js.map +1 -1
  121. package/dist/ws/shrink.d.ts +3 -1
  122. package/dist/ws/shrink.d.ts.map +1 -1
  123. package/dist/ws/shrink.js +46 -16
  124. package/dist/ws/shrink.js.map +1 -1
  125. package/dist/ws/turns.d.ts +58 -3
  126. package/dist/ws/turns.d.ts.map +1 -1
  127. package/dist/ws/turns.js +101 -2
  128. package/dist/ws/turns.js.map +1 -1
  129. package/migrations/011_principals.sql +13 -0
  130. package/migrations/012_activity_principals.sql +12 -0
  131. package/migrations/013_push_subscription_principals.sql +7 -0
  132. package/migrations/014_invalidate_legacy_sessions.sql +14 -0
  133. package/package.json +3 -3
  134. package/src/activity/notify.ts +16 -7
  135. package/src/activity/push-sender.ts +79 -16
  136. package/src/activity/query.ts +8 -0
  137. package/src/activity/recorder.ts +68 -3
  138. package/src/activity/runtime.ts +2 -5
  139. package/src/activity/sql.ts +7 -5
  140. package/src/activity/store.ts +32 -1
  141. package/src/activity/stream.ts +37 -17
  142. package/src/app-env.ts +8 -0
  143. package/src/app.ts +33 -4
  144. package/src/brain/client.ts +61 -20
  145. package/src/brain/types.ts +10 -0
  146. package/src/cron/run-job.ts +41 -17
  147. package/src/db/principals.ts +452 -0
  148. package/src/files/walker.ts +5 -1
  149. package/src/index.ts +6 -1
  150. package/src/middleware/auth.ts +241 -81
  151. package/src/middleware/passkeys.ts +35 -14
  152. package/src/middleware/principals.ts +189 -0
  153. package/src/middleware/require-owner.ts +13 -0
  154. package/src/observability/loggers.ts +1 -1
  155. package/src/routes/activity.ts +3 -0
  156. package/src/routes/brain.ts +107 -70
  157. package/src/routes/files.ts +10 -5
  158. package/src/routes/push.ts +11 -5
  159. package/src/routes/sessions.ts +45 -20
  160. package/src/share/staging.ts +11 -4
  161. package/src/ws/bridge.ts +14 -2
  162. package/src/ws/clients.ts +52 -7
  163. package/src/ws/connection.ts +98 -20
  164. package/src/ws/dispatch.ts +66 -6
  165. package/src/ws/history.ts +7 -5
  166. package/src/ws/host.ts +59 -1
  167. package/src/ws/run-session.ts +247 -74
  168. package/src/ws/shrink.ts +48 -14
  169. package/src/ws/turns.ts +136 -5
@@ -1,11 +1,28 @@
1
1
  import type { Logger } from "@opentelemetry/api-logs";
2
2
  import type { AgentBackend, PermissionDecision, PermissionRequest, AskUserResult, LocationFix } from "@schlessera/brain-ui-sdk/server";
3
3
  import type { AskUserQuestion, ChatImageAttachment, ClientEnvironment } from "@schlessera/brain-ui-sdk/protocol";
4
+ import type { TurnRecorder } from "../activity/recorder.js";
5
+ declare const authorizationContextBrand: unique symbol;
6
+ /** Mutable admission decision shared by every asynchronous path from one socket. */
7
+ export interface AuthorizationContext {
8
+ readonly [authorizationContextBrand]: true;
9
+ readonly principalId: string;
10
+ readonly expiresAt: number;
11
+ valid: boolean;
12
+ /** Keep this context discoverable until the returned release is called. */
13
+ retain(): () => void;
14
+ /** Release the reference acquired when the coordinator opened this context. */
15
+ release(): void;
16
+ }
4
17
  export interface QueuedFollowUp {
18
+ principalId: string;
19
+ authorization: AuthorizationContext;
5
20
  text: string;
6
21
  attachments: ChatImageAttachment[];
7
22
  /** Device snapshot taken when the message was sent, not when it runs. */
8
23
  client?: ClientEnvironment;
24
+ /** Queue-owned authorization lease, transferred to the runner on dequeue. */
25
+ releaseAuthorization: () => void;
9
26
  }
10
27
  /**
11
28
  * Bytes one queued entry keeps alive in this process until its turn runs.
@@ -18,15 +35,25 @@ export interface QueuedFollowUp {
18
35
  * on the wire.) The client snapshot is a handful of short fields and is not
19
36
  * worth walking.
20
37
  */
21
- export declare function queuedFollowUpBytes(entry: QueuedFollowUp): number;
38
+ export declare function queuedFollowUpBytes(entry: Pick<QueuedFollowUp, "text" | "attachments">): number;
22
39
  /** Total bytes currently parked in a session's follow-up queue. */
23
- export declare function queuedBytes(turn: RunningTurn): number;
40
+ export declare function queuedBytes(turn: Pick<RunningTurn, "queue">): number;
24
41
  /**
25
42
  * One RUNNING session slot. `abortController`/`timeoutHandle` belong to the
26
43
  * turn currently executing; a queued follow-up runs as the next turn in the
27
44
  * same slot (same sessionId) after the current one resolves.
28
45
  */
29
46
  export interface RunningTurn {
47
+ /** Principal responsible for the CURRENT turn in this session slot. */
48
+ principalId: string;
49
+ authorization: AuthorizationContext;
50
+ /** Activity recorder for the CURRENT turn, once startup reaches the backend. */
51
+ recorder?: TurnRecorder;
52
+ /**
53
+ * Cancelling actors accepted before the recorder exists (while billing is
54
+ * resolving). Drained into immutable activity events at recorder creation.
55
+ */
56
+ pendingCancellationPrincipalIds: string[];
30
57
  sessionId: string | null;
31
58
  /** Host-minted id of the CURRENT turn in this slot; re-minted per queued follow-up. */
32
59
  turnId: string;
@@ -60,7 +87,10 @@ export interface PendingApproval {
60
87
  * locks; an approval must survive that and reappear, not silently die.
61
88
  */
62
89
  request: PermissionRequest;
63
- resolve: (decision: PermissionDecision) => void;
90
+ resolve: (decision: PermissionDecision, response?: {
91
+ principalId: string;
92
+ always?: boolean;
93
+ }) => void;
64
94
  }
65
95
  export interface PendingAskUser {
66
96
  turn: RunningTurn;
@@ -99,20 +129,44 @@ export declare class TurnCoordinator {
99
129
  readonly running: Set<RunningTurn>;
100
130
  readonly bySession: Map<string, RunningTurn>;
101
131
  startingSessions: number;
132
+ /** Reserve a known session before asynchronous backend routing. */
133
+ readonly startingBySession: Map<string, {
134
+ queue: QueuedFollowUp[];
135
+ cancelled: boolean;
136
+ }>;
102
137
  readonly pendingApprovals: Map<string, PendingApproval>;
103
138
  readonly pendingAskUser: Map<string, PendingAskUser>;
104
139
  readonly pendingLocation: Map<string, PendingLocation>;
105
140
  readonly pendingMask: Map<string, PendingMask>;
141
+ /** Every authorization context with at least one live owner or async lease. */
142
+ readonly authorizationRegistry: Map<AuthorizationContext, number>;
106
143
  private locationCounter;
107
144
  private maskCounter;
108
145
  nextLocationRequestId(): string;
109
146
  nextMaskRequestId(): string;
110
147
  /** True while any session has a running turn. */
111
148
  isTurnActive(): boolean;
149
+ /** Construct and synchronously register a context with its initial reference. */
150
+ openAuthorization(input: {
151
+ principalId: string;
152
+ expiresAt: number;
153
+ valid: boolean;
154
+ }): AuthorizationContext;
155
+ /** Add principals whose registered authorization context has expired. */
156
+ collectExpiredPrincipalIds(now: number, ids: Set<string>): void;
157
+ /** Invalidate every registered context belonging to one of these principals. */
158
+ invalidateAuthorizations(principalIds: ReadonlySet<string>): void;
159
+ /**
160
+ * Record revocation without aborting running work. Unstarted queued work is
161
+ * removed per sender, while a current turn merely carries the invalid marker.
162
+ */
163
+ revokePrincipals(principalIds: ReadonlySet<string>): RunningTurn[];
112
164
  /** Cancel every running turn (used on shutdown). */
113
165
  cancelAll(reason: string): boolean;
114
166
  /** Cancel a session's current turn and drop its queued follow-ups. */
115
167
  cancelTurn(turn: RunningTurn, reason: string): void;
168
+ /** Record now, or retain the actor across the pre-recorder startup window. */
169
+ recordCancellation(turn: RunningTurn, principalId: string): void;
116
170
  /** Resolve/reject every pending interactive request belonging to one turn. */
117
171
  drainPendingForTurn(turn: RunningTurn, reason: string): void;
118
172
  /**
@@ -140,4 +194,5 @@ export declare class TurnCoordinator {
140
194
  /** Test seam: abort everything and clear all turn/pending state. */
141
195
  reset(): void;
142
196
  }
197
+ export {};
143
198
  //# sourceMappingURL=turns.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"turns.d.ts","sourceRoot":"","sources":["../../src/ws/turns.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"turns.d.ts","sourceRoot":"","sources":["../../src/ws/turns.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EACV,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACZ,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D,QAAA,MAAM,yBAAyB,EAAE,OAAO,MAAuC,CAAC;AAEhF,oFAAoF;AACpF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,CAAC,yBAAyB,CAAC,EAAE,IAAI,CAAC;IAC3C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,2EAA2E;IAC3E,MAAM,IAAI,MAAM,IAAI,CAAC;IACrB,+EAA+E;IAC/E,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,oBAAoB,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,yEAAyE;IACzE,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,6EAA6E;IAC7E,oBAAoB,EAAE,MAAM,IAAI,CAAC;CAClC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,aAAa,CAAC,GAClD,MAAM,CAMR;AAED,mEAAmE;AACnE,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,GAAG,MAAM,CAIpE;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,oBAAoB,CAAC;IACpC,gFAAgF;IAChF,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB;;;OAGG;IACH,+BAA+B,EAAE,MAAM,EAAE,CAAC;IAC1C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,uFAAuF;IACvF,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,YAAY,CAAC;IACtB,eAAe,EAAE,eAAe,CAAC;IACjC,aAAa,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;IAC7C,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,UAAU,EAAE,SAAS,GAAG,OAAO,GAAG,WAAW,GAAG,IAAI,CAAC;IACrD;;;;OAIG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAKD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,OAAO,EAAE,iBAAiB,CAAC;IAC3B,OAAO,EAAE,CACP,QAAQ,EAAE,kBAAkB,EAC5B,QAAQ,CAAC,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,KACjD,IAAI,CAAC;CACX;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,OAAO,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IACzC,MAAM,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACvC,MAAM,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,CAAC;IACnC,MAAM,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAC9B;AAED;;;;;GAKG;AACH,qBAAa,eAAe;IAC1B;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,QAAQ,CAAC,OAAO,mBAA0B;IAC1C,QAAQ,CAAC,SAAS,2BAAkC;IACpD,gBAAgB,SAAK;IACrB,mEAAmE;IACnE,QAAQ,CAAC,iBAAiB;eAA4B,cAAc,EAAE;mBAAa,OAAO;OAAM;IAEhG,QAAQ,CAAC,gBAAgB,+BAAsC;IAC/D,QAAQ,CAAC,cAAc,8BAAqC;IAC5D,QAAQ,CAAC,eAAe,+BAAsC;IAC9D,QAAQ,CAAC,WAAW,2BAAkC;IAEtD,+EAA+E;IAC/E,QAAQ,CAAC,qBAAqB,oCAA2C;IAEzE,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,WAAW,CAAK;IAExB,qBAAqB,IAAI,MAAM;IAI/B,iBAAiB,IAAI,MAAM;IAI3B,iDAAiD;IACjD,YAAY,IAAI,OAAO;IAIvB,iFAAiF;IACjF,iBAAiB,CAAC,KAAK,EAAE;QACvB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,OAAO,CAAC;KAChB,GAAG,oBAAoB;IAkCxB,yEAAyE;IACzE,0BAA0B,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI;IAQ/D,gFAAgF;IAChF,wBAAwB,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI;IAMjE;;;OAGG;IACH,gBAAgB,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,WAAW,EAAE;IAgBlE,oDAAoD;IACpD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAWlC,sEAAsE;IACtE,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAQnD,8EAA8E;IAC9E,kBAAkB,CAAC,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAKhE,8EAA8E;IAC9E,mBAAmB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAc5D;;;;;;;;;OASG;IACH,uBAAuB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAahE;;;;;;;OAOG;IACH,mBAAmB,CAAC,CAAC,SAAS;QAAE,IAAI,EAAE,WAAW,CAAA;KAAE,EACjD,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EACnB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,WAAW,GAChB,OAAO;IAaV,oEAAoE;IACpE,KAAK,IAAI,IAAI;CAkBd"}
package/dist/ws/turns.js CHANGED
@@ -1,3 +1,4 @@
1
+ const authorizationContextBrand = Symbol("AuthorizationContext");
1
2
  /**
2
3
  * Bytes one queued entry keeps alive in this process until its turn runs.
3
4
  *
@@ -39,10 +40,14 @@ export class TurnCoordinator {
39
40
  running = new Set();
40
41
  bySession = new Map();
41
42
  startingSessions = 0;
43
+ /** Reserve a known session before asynchronous backend routing. */
44
+ startingBySession = new Map();
42
45
  pendingApprovals = new Map();
43
46
  pendingAskUser = new Map();
44
47
  pendingLocation = new Map();
45
48
  pendingMask = new Map();
49
+ /** Every authorization context with at least one live owner or async lease. */
50
+ authorizationRegistry = new Map();
46
51
  locationCounter = 0;
47
52
  maskCounter = 0;
48
53
  nextLocationRequestId() {
@@ -55,9 +60,88 @@ export class TurnCoordinator {
55
60
  isTurnActive() {
56
61
  return this.running.size > 0;
57
62
  }
63
+ /** Construct and synchronously register a context with its initial reference. */
64
+ openAuthorization(input) {
65
+ let initialReleased = false;
66
+ const releaseReference = () => {
67
+ const count = this.authorizationRegistry.get(authorization);
68
+ if (count === undefined)
69
+ return;
70
+ if (count === 1)
71
+ this.authorizationRegistry.delete(authorization);
72
+ else
73
+ this.authorizationRegistry.set(authorization, count - 1);
74
+ };
75
+ const authorization = {
76
+ [authorizationContextBrand]: true,
77
+ ...input,
78
+ retain: () => {
79
+ const count = this.authorizationRegistry.get(authorization);
80
+ if (count === undefined) {
81
+ throw new Error("Cannot retain a released authorization context");
82
+ }
83
+ this.authorizationRegistry.set(authorization, count + 1);
84
+ let released = false;
85
+ return () => {
86
+ if (released)
87
+ return;
88
+ released = true;
89
+ releaseReference();
90
+ };
91
+ },
92
+ release: () => {
93
+ if (initialReleased)
94
+ return;
95
+ initialReleased = true;
96
+ releaseReference();
97
+ },
98
+ };
99
+ this.authorizationRegistry.set(authorization, 1);
100
+ return authorization;
101
+ }
102
+ /** Add principals whose registered authorization context has expired. */
103
+ collectExpiredPrincipalIds(now, ids) {
104
+ for (const authorization of this.authorizationRegistry.keys()) {
105
+ if (authorization.valid && authorization.expiresAt <= now) {
106
+ ids.add(authorization.principalId);
107
+ }
108
+ }
109
+ }
110
+ /** Invalidate every registered context belonging to one of these principals. */
111
+ invalidateAuthorizations(principalIds) {
112
+ for (const authorization of this.authorizationRegistry.keys()) {
113
+ if (principalIds.has(authorization.principalId))
114
+ authorization.valid = false;
115
+ }
116
+ }
117
+ /**
118
+ * Record revocation without aborting running work. Unstarted queued work is
119
+ * removed per sender, while a current turn merely carries the invalid marker.
120
+ */
121
+ revokePrincipals(principalIds) {
122
+ const affectedRunning = [];
123
+ this.invalidateAuthorizations(principalIds);
124
+ for (const turn of this.running) {
125
+ if (principalIds.has(turn.principalId)) {
126
+ affectedRunning.push(turn);
127
+ }
128
+ turn.queue = turn.queue.filter((entry) => {
129
+ if (!principalIds.has(entry.principalId))
130
+ return true;
131
+ entry.releaseAuthorization();
132
+ return false;
133
+ });
134
+ }
135
+ return affectedRunning;
136
+ }
58
137
  /** Cancel every running turn (used on shutdown). */
59
138
  cancelAll(reason) {
60
- if (this.running.size === 0)
139
+ const hadWork = this.running.size > 0 || this.startingBySession.size > 0;
140
+ for (const starting of this.startingBySession.values()) {
141
+ starting.cancelled = true;
142
+ starting.queue.length = 0;
143
+ }
144
+ if (!hadWork)
61
145
  return false;
62
146
  for (const turn of [...this.running])
63
147
  this.cancelTurn(turn, reason);
@@ -66,11 +150,19 @@ export class TurnCoordinator {
66
150
  /** Cancel a session's current turn and drop its queued follow-ups. */
67
151
  cancelTurn(turn, reason) {
68
152
  turn.cancelled = true;
69
- turn.queue.length = 0;
153
+ for (const entry of turn.queue.splice(0))
154
+ entry.releaseAuthorization();
70
155
  clearTimeout(turn.timeoutHandle);
71
156
  turn.abortController.abort();
72
157
  this.drainPendingForTurn(turn, reason);
73
158
  }
159
+ /** Record now, or retain the actor across the pre-recorder startup window. */
160
+ recordCancellation(turn, principalId) {
161
+ if (turn.recorder)
162
+ turn.recorder.recordCancellation(principalId);
163
+ else
164
+ turn.pendingCancellationPrincipalIds.push(principalId);
165
+ }
74
166
  /** Resolve/reject every pending interactive request belonging to one turn. */
75
167
  drainPendingForTurn(turn, reason) {
76
168
  for (const [id, p] of this.pendingApprovals) {
@@ -136,7 +228,14 @@ export class TurnCoordinator {
136
228
  for (const turn of [...this.running]) {
137
229
  clearTimeout(turn.timeoutHandle);
138
230
  turn.abortController.abort();
231
+ for (const entry of turn.queue.splice(0))
232
+ entry.releaseAuthorization();
233
+ }
234
+ for (const starting of this.startingBySession.values()) {
235
+ starting.cancelled = true;
236
+ starting.queue.length = 0;
139
237
  }
238
+ this.startingBySession.clear();
140
239
  this.running.clear();
141
240
  this.bySession.clear();
142
241
  this.pendingApprovals.clear();
@@ -1 +1 @@
1
- {"version":3,"file":"turns.js","sourceRoot":"","sources":["../../src/ws/turns.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"turns.js","sourceRoot":"","sources":["../../src/ws/turns.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ CREATE TABLE principals (
2
+ id TEXT PRIMARY KEY NOT NULL, -- 22 chars base64url, server-generated
3
+ kind TEXT NOT NULL CHECK (kind IN ('owner','agent','ambient','system')),
4
+ auth_method TEXT NOT NULL CHECK (auth_method IN ('password','passkey','delegated','ambient')),
5
+ label TEXT NOT NULL, -- ≤64 chars, never rendered as HTML
6
+ credential_id TEXT, -- passkey_credentials.id, else NULL
7
+ created_by TEXT, -- minting principal, for delegation
8
+ created_at INTEGER NOT NULL,
9
+ expires_at INTEGER NOT NULL,
10
+ last_seen_at INTEGER,
11
+ revoked_at INTEGER
12
+ );
13
+ CREATE INDEX idx_principals_live ON principals(revoked_at, expires_at);
@@ -0,0 +1,12 @@
1
+ -- Principal attribution for activity spans and durable run rollups.
2
+ --
3
+ -- Deliberately NO backfill and no owner default: activity recorded before
4
+ -- this migration remains NULL (= unattributed). Rollups snapshot the live
5
+ -- principal's label and kind when they are first written because principal
6
+ -- rows expire and are pruned while activity_run_rollups are retained.
7
+ ALTER TABLE activity_spans ADD COLUMN principal_id TEXT;
8
+
9
+ ALTER TABLE activity_run_rollups ADD COLUMN principal_id TEXT;
10
+ ALTER TABLE activity_run_rollups ADD COLUMN principal_label TEXT;
11
+ ALTER TABLE activity_run_rollups ADD COLUMN principal_kind TEXT
12
+ CHECK (principal_kind IN ('owner', 'agent', 'ambient', 'system'));
@@ -0,0 +1,7 @@
1
+ -- Legacy subscriptions deliberately remain NULL and therefore inert until an
2
+ -- authenticated client re-registers the endpoint for its current principal.
3
+ ALTER TABLE push_subscriptions
4
+ ADD COLUMN principal_id TEXT REFERENCES principals(id) ON DELETE SET NULL;
5
+
6
+ CREATE INDEX idx_push_subscriptions_principal
7
+ ON push_subscriptions(principal_id);
@@ -0,0 +1,14 @@
1
+ -- Advance the legacy v1 cookie epoch once at upgrade so every pre-upgrade
2
+ -- cookie stays invalid if the deployment rolls back to the v1 verifier.
3
+ INSERT INTO settings (key, value, updated_at)
4
+ VALUES ('auth.sessionsEpoch', '1', CAST(strftime('%s', 'now') AS INTEGER) * 1000)
5
+ ON CONFLICT(key) DO UPDATE SET
6
+ value = CASE
7
+ WHEN json_valid(value) THEN CASE
8
+ WHEN json_type(value) = 'integer' AND CAST(value AS INTEGER) >= 0
9
+ THEN CAST(CAST(value AS INTEGER) + 1 AS TEXT)
10
+ ELSE '1'
11
+ END
12
+ ELSE '1'
13
+ END,
14
+ updated_at = excluded.updated_at;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schlessera/brain-ui-server",
3
- "version": "0.34.1",
3
+ "version": "0.35.0",
4
4
  "description": "brain-kit chat-UI server: Hono app factory, WebSocket turn coordinator, auth (password/passkeys/tailscale/proxy), session catalog, and brain/files/voice routes",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -54,8 +54,8 @@
54
54
  "dependencies": {
55
55
  "@opentelemetry/api": "^1.9.1",
56
56
  "@opentelemetry/api-logs": "0.221.0",
57
- "@schlessera/brain-render-template": "0.34.1",
58
- "@schlessera/brain-ui-sdk": "0.34.1",
57
+ "@schlessera/brain-render-template": "0.35.0",
58
+ "@schlessera/brain-ui-sdk": "0.35.0",
59
59
  "@simplewebauthn/server": "^13.3.2",
60
60
  "fflate": "^0.8.2",
61
61
  "gray-matter": "^4.0.3",
@@ -23,7 +23,7 @@ import type { Database } from "bun:sqlite";
23
23
  import type { Logger } from "@opentelemetry/api-logs";
24
24
  import { isFailureOutcome } from "@schlessera/brain-ui-sdk/protocol";
25
25
 
26
- import { getSetting } from "../db/settings.js";
26
+ import { getSetting, setSetting } from "../db/settings.js";
27
27
  import type { ActivityStore, SpanRow } from "./store.js";
28
28
 
29
29
  export type IntentKind = "failure" | "completion" | "stuck";
@@ -76,7 +76,7 @@ const DEFAULT_STUCK_THRESHOLD_MS = 45 * 60 * 1000;
76
76
 
77
77
  export function createActivityNotifier(deps: ActivityNotifierDeps): ActivityNotifier {
78
78
  const { db, store, isWatched, log } = deps;
79
- let cursor = latestCursor(db);
79
+ const cursorKey = "activity.notify.cursor";
80
80
  /** Runs already flagged as stuck this process lifetime (tag also guards). */
81
81
  const stuckFlagged = new Set<string>();
82
82
 
@@ -97,6 +97,9 @@ export function createActivityNotifier(deps: ActivityNotifierDeps): ActivityNoti
97
97
  suppressed?: boolean;
98
98
  }): void {
99
99
  const now = Date.now();
100
+ // Replay on first upgrade must not resurrect an acknowledged notification.
101
+ if (db.query("SELECT id FROM notification_intents WHERE run_id = ? AND kind = ? LIMIT 1")
102
+ .get(input.runId, input.kind)) return;
100
103
  // Tag dedupe: one live (unacknowledged) intent per tag — repeats coalesce.
101
104
  const existing = db
102
105
  .query(
@@ -180,11 +183,17 @@ export function createActivityNotifier(deps: ActivityNotifierDeps): ActivityNoti
180
183
  // The head is read BEFORE the detection query so advancing past
181
184
  // non-terminal changes can never skip a terminal write committed
182
185
  // in between; a change seen twice is absorbed by the tag dedupe.
183
- const head = latestCursor(db);
184
- const rows = store.terminalRootChangesSince(cursor, 500);
185
- for (const { span } of rows) intentForSpan(span);
186
- const lastSeen = rows.length > 0 ? rows[rows.length - 1]!.changeId : cursor;
187
- cursor = rows.length === 500 ? lastSeen : Math.max(head, lastSeen);
186
+ db.transaction(() => {
187
+ const saved = getSetting<unknown>(db, cursorKey, 0, log);
188
+ const cursor = typeof saved === "number" && Number.isSafeInteger(saved) && saved >= 0 ? saved : 0;
189
+ const head = latestCursor(db);
190
+ const rows = store.terminalRootChangesSince(cursor, 500);
191
+ for (const { span } of rows) intentForSpan(span);
192
+ const lastSeen = rows.length > 0 ? rows[rows.length - 1]!.changeId : cursor;
193
+ // Commit intents and progress together. A crash retries the entire
194
+ // batch; another notifier reads the committed cursor under this lock.
195
+ setSetting(db, cursorKey, rows.length === 500 ? lastSeen : Math.max(head, lastSeen));
196
+ }).immediate();
188
197
 
189
198
  // Watchdog: an over-threshold LIVE root run is stuck — a signal, not
190
199
  // an outcome. Threshold from settings, per-job override supported.
@@ -27,13 +27,21 @@ export interface PushSubscriptionRow {
27
27
  label: string | null;
28
28
  createdAt: number;
29
29
  lastUsedAt: number | null;
30
+ principalId: string | null;
30
31
  }
31
32
 
32
33
  export interface PushSender {
33
34
  publicKey(): string;
34
- subscribe(sub: { endpoint: string; keys: { p256dh: string; auth: string } }, label?: string): void;
35
- unsubscribe(endpoint: string): boolean;
36
- subscriptions(): PushSubscriptionRow[];
35
+ /** Bind (or re-bind) an endpoint only while the registering principal is live. */
36
+ subscribe(
37
+ sub: { endpoint: string; keys: { p256dh: string; auth: string } },
38
+ principalId: string,
39
+ label?: string
40
+ ): boolean;
41
+ unsubscribe(endpoint: string, principalId: string): boolean;
42
+ subscriptions(principalId?: string): PushSubscriptionRow[];
43
+ /** Make one revoked principal's endpoints inert without deleting browser state. */
44
+ unbindPrincipal(principalId: string): number;
37
45
  /** Deliver pending intents to every subscription. Returns sends attempted. */
38
46
  deliverPending(notifier: ActivityNotifier): Promise<number>;
39
47
  }
@@ -86,9 +94,9 @@ export function createPushSender(
86
94
  return vapid();
87
95
  }
88
96
 
89
- function rows(): PushSubscriptionRow[] {
97
+ function mapRows(dbRows: any[]): PushSubscriptionRow[] {
90
98
  return (
91
- db.query("SELECT * FROM push_subscriptions ORDER BY created_at").all() as any[]
99
+ dbRows
92
100
  ).map((r) => ({
93
101
  endpoint: r.endpoint,
94
102
  p256dh: r.p256dh,
@@ -96,33 +104,88 @@ export function createPushSender(
96
104
  label: r.label,
97
105
  createdAt: r.created_at,
98
106
  lastUsedAt: r.last_used_at,
107
+ principalId: r.principal_id,
99
108
  }));
100
109
  }
101
110
 
111
+ function rows(principalId?: string): PushSubscriptionRow[] {
112
+ const dbRows = principalId === undefined
113
+ ? db.query("SELECT * FROM push_subscriptions ORDER BY created_at").all()
114
+ : db
115
+ .query(
116
+ `SELECT * FROM push_subscriptions
117
+ WHERE principal_id = ? ORDER BY created_at`
118
+ )
119
+ .all(principalId);
120
+ return mapRows(dbRows as any[]);
121
+ }
122
+
123
+ function deliverableRows(now: number): PushSubscriptionRow[] {
124
+ return mapRows(
125
+ db
126
+ .query(
127
+ `SELECT s.* FROM push_subscriptions s
128
+ JOIN principals p ON p.id = s.principal_id
129
+ WHERE p.revoked_at IS NULL
130
+ AND typeof(p.expires_at) = 'integer'
131
+ AND p.expires_at > ?
132
+ ORDER BY s.created_at`
133
+ )
134
+ .all(now) as any[]
135
+ );
136
+ }
137
+
102
138
  return {
103
139
  publicKey() {
104
140
  return vapid().publicKey;
105
141
  },
106
142
 
107
- subscribe(sub, label) {
108
- db.query(
109
- `INSERT INTO push_subscriptions (endpoint, p256dh, auth, label, created_at)
110
- VALUES (?, ?, ?, ?, ?)
111
- ON CONFLICT(endpoint) DO UPDATE SET p256dh = excluded.p256dh, auth = excluded.auth`
112
- ).run(sub.endpoint, sub.keys.p256dh, sub.keys.auth, label ?? null, Date.now());
143
+ subscribe(sub, principalId, label) {
144
+ const now = Date.now();
145
+ return db.query(
146
+ `INSERT INTO push_subscriptions
147
+ (endpoint, p256dh, auth, label, created_at, principal_id)
148
+ SELECT ?, ?, ?, ?, ?, id FROM principals
149
+ WHERE id = ? AND revoked_at IS NULL
150
+ AND typeof(expires_at) = 'integer' AND expires_at > ?
151
+ ON CONFLICT(endpoint) DO UPDATE SET
152
+ p256dh = excluded.p256dh,
153
+ auth = excluded.auth,
154
+ label = excluded.label,
155
+ principal_id = excluded.principal_id`
156
+ ).run(
157
+ sub.endpoint,
158
+ sub.keys.p256dh,
159
+ sub.keys.auth,
160
+ label ?? null,
161
+ now,
162
+ principalId,
163
+ now
164
+ ).changes > 0;
113
165
  },
114
166
 
115
- unsubscribe(endpoint) {
116
- return db.query("DELETE FROM push_subscriptions WHERE endpoint = ?").run(endpoint)
117
- .changes > 0;
167
+ unsubscribe(endpoint, principalId) {
168
+ return db
169
+ .query(
170
+ "DELETE FROM push_subscriptions WHERE endpoint = ? AND principal_id = ?"
171
+ )
172
+ .run(endpoint, principalId).changes > 0;
118
173
  },
119
174
 
120
175
  subscriptions: rows,
121
176
 
177
+ unbindPrincipal(principalId) {
178
+ return db
179
+ .query(
180
+ "UPDATE push_subscriptions SET principal_id = NULL WHERE principal_id = ?"
181
+ )
182
+ .run(principalId).changes;
183
+ },
184
+
122
185
  async deliverPending(notifier) {
123
186
  const pending = notifier.pending();
124
187
  if (pending.length === 0) return 0;
125
- if (rows().length === 0) {
188
+ if (deliverableRows(Date.now()).length === 0) {
126
189
  // No devices: the inbox already has it; nothing to deliver. Leave
127
190
  // the intents pending so a device subscribing later still gets them
128
191
  // if they are fresh, and the boot sweep can retry.
@@ -134,7 +197,7 @@ export function createPushSender(
134
197
  for (const intent of pending) {
135
198
  // Re-read per intent: an endpoint pruned as dead during the previous
136
199
  // intent's pass must not be attempted again in this one.
137
- const subs = rows();
200
+ const subs = deliverableRows(Date.now());
138
201
  if (subs.length === 0) break;
139
202
  // Minimized payload; the tag coalesces repeats per run across devices.
140
203
  const payload = JSON.stringify({
@@ -50,6 +50,9 @@ export function runActivityQuery(
50
50
  name: r.name,
51
51
  jobName: r.jobName,
52
52
  sessionId: r.sessionId,
53
+ principalId: r.principalId,
54
+ principalLabel: r.principalLabel,
55
+ principalKind: r.principalKind,
53
56
  startedAt: iso(r.startedAt),
54
57
  outcome: r.outcome,
55
58
  durationMs: r.durationMs,
@@ -89,6 +92,7 @@ export function runActivityQuery(
89
92
  parent: s.parentSpanId,
90
93
  name: s.name,
91
94
  kind: s.kind,
95
+ principalId: s.principalId,
92
96
  startedAt: iso(s.startedAt),
93
97
  endedAt: s.endedAt ? iso(s.endedAt) : null,
94
98
  outcome: s.outcome,
@@ -112,6 +116,9 @@ export function runActivityQuery(
112
116
  rollup: {
113
117
  name: rollup.name,
114
118
  jobName: rollup.jobName,
119
+ principalId: rollup.principalId,
120
+ principalLabel: rollup.principalLabel,
121
+ principalKind: rollup.principalKind,
115
122
  outcome: rollup.outcome,
116
123
  startedAt: iso(rollup.startedAt),
117
124
  durationMs: rollup.durationMs,
@@ -173,6 +180,7 @@ function liveSummary(span: SpanRow) {
173
180
  name: span.name,
174
181
  jobName: span.jobName,
175
182
  sessionId: span.sessionId,
183
+ principalId: span.principalId,
176
184
  startedAt: iso(span.startedAt),
177
185
  elapsedMs: Date.now() - span.startedAt,
178
186
  };