@schlessera/brain-ui-server 0.34.0 → 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.
- package/README.md +28 -0
- package/dist/activity/notify.d.ts.map +1 -1
- package/dist/activity/notify.js +18 -8
- package/dist/activity/notify.js.map +1 -1
- package/dist/activity/push-sender.d.ts +7 -3
- package/dist/activity/push-sender.d.ts.map +1 -1
- package/dist/activity/push-sender.js +45 -11
- package/dist/activity/push-sender.js.map +1 -1
- package/dist/activity/query.d.ts.map +1 -1
- package/dist/activity/query.js +8 -0
- package/dist/activity/query.js.map +1 -1
- package/dist/activity/recorder.d.ts +11 -1
- package/dist/activity/recorder.d.ts.map +1 -1
- package/dist/activity/recorder.js +52 -2
- package/dist/activity/recorder.js.map +1 -1
- package/dist/activity/runtime.d.ts.map +1 -1
- package/dist/activity/runtime.js +2 -5
- package/dist/activity/runtime.js.map +1 -1
- package/dist/activity/sql.d.ts +34 -0
- package/dist/activity/sql.d.ts.map +1 -0
- package/dist/activity/sql.js +114 -0
- package/dist/activity/sql.js.map +1 -0
- package/dist/activity/store.d.ts +7 -0
- package/dist/activity/store.d.ts.map +1 -1
- package/dist/activity/store.js +48 -113
- package/dist/activity/store.js.map +1 -1
- package/dist/activity/stream.d.ts +3 -1
- package/dist/activity/stream.d.ts.map +1 -1
- package/dist/activity/stream.js +32 -17
- package/dist/activity/stream.js.map +1 -1
- package/dist/app-env.d.ts +8 -0
- package/dist/app-env.d.ts.map +1 -0
- package/dist/app-env.js +2 -0
- package/dist/app-env.js.map +1 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +28 -3
- package/dist/app.js.map +1 -1
- package/dist/brain/client.d.ts +4 -2
- package/dist/brain/client.d.ts.map +1 -1
- package/dist/brain/client.js +60 -20
- package/dist/brain/client.js.map +1 -1
- package/dist/brain/types.d.ts +9 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/cron/run-job.d.ts.map +1 -1
- package/dist/cron/run-job.js +60 -10
- package/dist/cron/run-job.js.map +1 -1
- package/dist/db/principals.d.ts +72 -0
- package/dist/db/principals.d.ts.map +1 -0
- package/dist/db/principals.js +294 -0
- package/dist/db/principals.js.map +1 -0
- package/dist/files/walker.d.ts.map +1 -1
- package/dist/files/walker.js +6 -1
- package/dist/files/walker.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts +37 -9
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +148 -74
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/passkeys.d.ts +9 -7
- package/dist/middleware/passkeys.d.ts.map +1 -1
- package/dist/middleware/passkeys.js +25 -7
- package/dist/middleware/passkeys.js.map +1 -1
- package/dist/middleware/principals.d.ts +18 -0
- package/dist/middleware/principals.d.ts.map +1 -0
- package/dist/middleware/principals.js +138 -0
- package/dist/middleware/principals.js.map +1 -0
- package/dist/middleware/require-owner.d.ts +5 -0
- package/dist/middleware/require-owner.d.ts.map +1 -0
- package/dist/middleware/require-owner.js +10 -0
- package/dist/middleware/require-owner.js.map +1 -0
- package/dist/observability/loggers.js +1 -1
- package/dist/observability/loggers.js.map +1 -1
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/activity.js +3 -0
- package/dist/routes/activity.js.map +1 -1
- package/dist/routes/brain.d.ts.map +1 -1
- package/dist/routes/brain.js +102 -63
- package/dist/routes/brain.js.map +1 -1
- package/dist/routes/files.d.ts.map +1 -1
- package/dist/routes/files.js +7 -2
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/push.d.ts +61 -2
- package/dist/routes/push.d.ts.map +1 -1
- package/dist/routes/push.js +8 -3
- package/dist/routes/push.js.map +1 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +30 -4
- package/dist/routes/sessions.js.map +1 -1
- package/dist/share/staging.d.ts.map +1 -1
- package/dist/share/staging.js +11 -4
- package/dist/share/staging.js.map +1 -1
- package/dist/ws/bridge.d.ts.map +1 -1
- package/dist/ws/bridge.js +6 -2
- package/dist/ws/bridge.js.map +1 -1
- package/dist/ws/clients.d.ts +9 -2
- package/dist/ws/clients.d.ts.map +1 -1
- package/dist/ws/clients.js +44 -6
- package/dist/ws/clients.js.map +1 -1
- package/dist/ws/connection.d.ts +7 -4
- package/dist/ws/connection.d.ts.map +1 -1
- package/dist/ws/connection.js +83 -12
- package/dist/ws/connection.js.map +1 -1
- package/dist/ws/dispatch.d.ts +12 -1
- package/dist/ws/dispatch.d.ts.map +1 -1
- package/dist/ws/dispatch.js +46 -6
- package/dist/ws/dispatch.js.map +1 -1
- package/dist/ws/history.d.ts.map +1 -1
- package/dist/ws/history.js +7 -5
- package/dist/ws/history.js.map +1 -1
- package/dist/ws/host.d.ts +22 -0
- package/dist/ws/host.d.ts.map +1 -1
- package/dist/ws/host.js +39 -1
- package/dist/ws/host.js.map +1 -1
- package/dist/ws/run-session.d.ts +7 -7
- package/dist/ws/run-session.d.ts.map +1 -1
- package/dist/ws/run-session.js +208 -52
- package/dist/ws/run-session.js.map +1 -1
- package/dist/ws/shrink.d.ts +3 -1
- package/dist/ws/shrink.d.ts.map +1 -1
- package/dist/ws/shrink.js +46 -16
- package/dist/ws/shrink.js.map +1 -1
- package/dist/ws/turns.d.ts +58 -3
- package/dist/ws/turns.d.ts.map +1 -1
- package/dist/ws/turns.js +101 -2
- package/dist/ws/turns.js.map +1 -1
- package/migrations/011_principals.sql +13 -0
- package/migrations/012_activity_principals.sql +12 -0
- package/migrations/013_push_subscription_principals.sql +7 -0
- package/migrations/014_invalidate_legacy_sessions.sql +14 -0
- package/package.json +3 -3
- package/src/activity/notify.ts +16 -7
- package/src/activity/push-sender.ts +79 -16
- package/src/activity/query.ts +8 -0
- package/src/activity/recorder.ts +68 -3
- package/src/activity/runtime.ts +2 -5
- package/src/activity/sql.ts +122 -0
- package/src/activity/store.ts +73 -155
- package/src/activity/stream.ts +37 -17
- package/src/app-env.ts +8 -0
- package/src/app.ts +33 -4
- package/src/brain/client.ts +61 -20
- package/src/brain/types.ts +10 -0
- package/src/cron/run-job.ts +41 -17
- package/src/db/principals.ts +452 -0
- package/src/files/walker.ts +5 -1
- package/src/index.ts +6 -1
- package/src/middleware/auth.ts +241 -81
- package/src/middleware/passkeys.ts +35 -14
- package/src/middleware/principals.ts +189 -0
- package/src/middleware/require-owner.ts +13 -0
- package/src/observability/loggers.ts +1 -1
- package/src/routes/activity.ts +3 -0
- package/src/routes/brain.ts +107 -70
- package/src/routes/files.ts +10 -5
- package/src/routes/push.ts +11 -5
- package/src/routes/sessions.ts +45 -20
- package/src/share/staging.ts +11 -4
- package/src/ws/bridge.ts +14 -2
- package/src/ws/clients.ts +52 -7
- package/src/ws/connection.ts +98 -20
- package/src/ws/dispatch.ts +66 -6
- package/src/ws/history.ts +7 -5
- package/src/ws/host.ts +59 -1
- package/src/ws/run-session.ts +247 -74
- package/src/ws/shrink.ts +48 -14
- package/src/ws/turns.ts +136 -5
package/src/ws/turns.ts
CHANGED
|
@@ -11,12 +11,31 @@ import type {
|
|
|
11
11
|
ChatImageAttachment,
|
|
12
12
|
ClientEnvironment,
|
|
13
13
|
} from "@schlessera/brain-ui-sdk/protocol";
|
|
14
|
+
import type { TurnRecorder } from "../activity/recorder.js";
|
|
15
|
+
|
|
16
|
+
const authorizationContextBrand: unique symbol = Symbol("AuthorizationContext");
|
|
17
|
+
|
|
18
|
+
/** Mutable admission decision shared by every asynchronous path from one socket. */
|
|
19
|
+
export interface AuthorizationContext {
|
|
20
|
+
readonly [authorizationContextBrand]: true;
|
|
21
|
+
readonly principalId: string;
|
|
22
|
+
readonly expiresAt: number;
|
|
23
|
+
valid: boolean;
|
|
24
|
+
/** Keep this context discoverable until the returned release is called. */
|
|
25
|
+
retain(): () => void;
|
|
26
|
+
/** Release the reference acquired when the coordinator opened this context. */
|
|
27
|
+
release(): void;
|
|
28
|
+
}
|
|
14
29
|
|
|
15
30
|
export interface QueuedFollowUp {
|
|
31
|
+
principalId: string;
|
|
32
|
+
authorization: AuthorizationContext;
|
|
16
33
|
text: string;
|
|
17
34
|
attachments: ChatImageAttachment[];
|
|
18
35
|
/** Device snapshot taken when the message was sent, not when it runs. */
|
|
19
36
|
client?: ClientEnvironment;
|
|
37
|
+
/** Queue-owned authorization lease, transferred to the runner on dequeue. */
|
|
38
|
+
releaseAuthorization: () => void;
|
|
20
39
|
}
|
|
21
40
|
|
|
22
41
|
/**
|
|
@@ -30,7 +49,9 @@ export interface QueuedFollowUp {
|
|
|
30
49
|
* on the wire.) The client snapshot is a handful of short fields and is not
|
|
31
50
|
* worth walking.
|
|
32
51
|
*/
|
|
33
|
-
export function queuedFollowUpBytes(
|
|
52
|
+
export function queuedFollowUpBytes(
|
|
53
|
+
entry: Pick<QueuedFollowUp, "text" | "attachments">
|
|
54
|
+
): number {
|
|
34
55
|
let bytes = Buffer.byteLength(entry.text, "utf-8");
|
|
35
56
|
for (const attachment of entry.attachments) {
|
|
36
57
|
bytes += attachment.data.length;
|
|
@@ -39,7 +60,7 @@ export function queuedFollowUpBytes(entry: QueuedFollowUp): number {
|
|
|
39
60
|
}
|
|
40
61
|
|
|
41
62
|
/** Total bytes currently parked in a session's follow-up queue. */
|
|
42
|
-
export function queuedBytes(turn: RunningTurn): number {
|
|
63
|
+
export function queuedBytes(turn: Pick<RunningTurn, "queue">): number {
|
|
43
64
|
let bytes = 0;
|
|
44
65
|
for (const entry of turn.queue) bytes += queuedFollowUpBytes(entry);
|
|
45
66
|
return bytes;
|
|
@@ -51,6 +72,16 @@ export function queuedBytes(turn: RunningTurn): number {
|
|
|
51
72
|
* same slot (same sessionId) after the current one resolves.
|
|
52
73
|
*/
|
|
53
74
|
export interface RunningTurn {
|
|
75
|
+
/** Principal responsible for the CURRENT turn in this session slot. */
|
|
76
|
+
principalId: string;
|
|
77
|
+
authorization: AuthorizationContext;
|
|
78
|
+
/** Activity recorder for the CURRENT turn, once startup reaches the backend. */
|
|
79
|
+
recorder?: TurnRecorder;
|
|
80
|
+
/**
|
|
81
|
+
* Cancelling actors accepted before the recorder exists (while billing is
|
|
82
|
+
* resolving). Drained into immutable activity events at recorder creation.
|
|
83
|
+
*/
|
|
84
|
+
pendingCancellationPrincipalIds: string[];
|
|
54
85
|
sessionId: string | null; // null until session_info resolves it (new session)
|
|
55
86
|
/** Host-minted id of the CURRENT turn in this slot; re-minted per queued follow-up. */
|
|
56
87
|
turnId: string;
|
|
@@ -88,7 +119,10 @@ export interface PendingApproval {
|
|
|
88
119
|
* locks; an approval must survive that and reappear, not silently die.
|
|
89
120
|
*/
|
|
90
121
|
request: PermissionRequest;
|
|
91
|
-
resolve: (
|
|
122
|
+
resolve: (
|
|
123
|
+
decision: PermissionDecision,
|
|
124
|
+
response?: { principalId: string; always?: boolean }
|
|
125
|
+
) => void;
|
|
92
126
|
}
|
|
93
127
|
|
|
94
128
|
export interface PendingAskUser {
|
|
@@ -132,12 +166,17 @@ export class TurnCoordinator {
|
|
|
132
166
|
readonly running = new Set<RunningTurn>();
|
|
133
167
|
readonly bySession = new Map<string, RunningTurn>();
|
|
134
168
|
startingSessions = 0;
|
|
169
|
+
/** Reserve a known session before asynchronous backend routing. */
|
|
170
|
+
readonly startingBySession = new Map<string, { queue: QueuedFollowUp[]; cancelled: boolean }>();
|
|
135
171
|
|
|
136
172
|
readonly pendingApprovals = new Map<string, PendingApproval>();
|
|
137
173
|
readonly pendingAskUser = new Map<string, PendingAskUser>();
|
|
138
174
|
readonly pendingLocation = new Map<string, PendingLocation>();
|
|
139
175
|
readonly pendingMask = new Map<string, PendingMask>();
|
|
140
176
|
|
|
177
|
+
/** Every authorization context with at least one live owner or async lease. */
|
|
178
|
+
readonly authorizationRegistry = new Map<AuthorizationContext, number>();
|
|
179
|
+
|
|
141
180
|
private locationCounter = 0;
|
|
142
181
|
private maskCounter = 0;
|
|
143
182
|
|
|
@@ -154,9 +193,89 @@ export class TurnCoordinator {
|
|
|
154
193
|
return this.running.size > 0;
|
|
155
194
|
}
|
|
156
195
|
|
|
196
|
+
/** Construct and synchronously register a context with its initial reference. */
|
|
197
|
+
openAuthorization(input: {
|
|
198
|
+
principalId: string;
|
|
199
|
+
expiresAt: number;
|
|
200
|
+
valid: boolean;
|
|
201
|
+
}): AuthorizationContext {
|
|
202
|
+
let initialReleased = false;
|
|
203
|
+
const releaseReference = () => {
|
|
204
|
+
const count = this.authorizationRegistry.get(authorization);
|
|
205
|
+
if (count === undefined) return;
|
|
206
|
+
if (count === 1) this.authorizationRegistry.delete(authorization);
|
|
207
|
+
else this.authorizationRegistry.set(authorization, count - 1);
|
|
208
|
+
};
|
|
209
|
+
const authorization: AuthorizationContext = {
|
|
210
|
+
[authorizationContextBrand]: true,
|
|
211
|
+
...input,
|
|
212
|
+
retain: () => {
|
|
213
|
+
const count = this.authorizationRegistry.get(authorization);
|
|
214
|
+
if (count === undefined) {
|
|
215
|
+
throw new Error("Cannot retain a released authorization context");
|
|
216
|
+
}
|
|
217
|
+
this.authorizationRegistry.set(authorization, count + 1);
|
|
218
|
+
let released = false;
|
|
219
|
+
return () => {
|
|
220
|
+
if (released) return;
|
|
221
|
+
released = true;
|
|
222
|
+
releaseReference();
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
release: () => {
|
|
226
|
+
if (initialReleased) return;
|
|
227
|
+
initialReleased = true;
|
|
228
|
+
releaseReference();
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
this.authorizationRegistry.set(authorization, 1);
|
|
232
|
+
return authorization;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Add principals whose registered authorization context has expired. */
|
|
236
|
+
collectExpiredPrincipalIds(now: number, ids: Set<string>): void {
|
|
237
|
+
for (const authorization of this.authorizationRegistry.keys()) {
|
|
238
|
+
if (authorization.valid && authorization.expiresAt <= now) {
|
|
239
|
+
ids.add(authorization.principalId);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Invalidate every registered context belonging to one of these principals. */
|
|
245
|
+
invalidateAuthorizations(principalIds: ReadonlySet<string>): void {
|
|
246
|
+
for (const authorization of this.authorizationRegistry.keys()) {
|
|
247
|
+
if (principalIds.has(authorization.principalId)) authorization.valid = false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Record revocation without aborting running work. Unstarted queued work is
|
|
253
|
+
* removed per sender, while a current turn merely carries the invalid marker.
|
|
254
|
+
*/
|
|
255
|
+
revokePrincipals(principalIds: ReadonlySet<string>): RunningTurn[] {
|
|
256
|
+
const affectedRunning: RunningTurn[] = [];
|
|
257
|
+
this.invalidateAuthorizations(principalIds);
|
|
258
|
+
for (const turn of this.running) {
|
|
259
|
+
if (principalIds.has(turn.principalId)) {
|
|
260
|
+
affectedRunning.push(turn);
|
|
261
|
+
}
|
|
262
|
+
turn.queue = turn.queue.filter((entry) => {
|
|
263
|
+
if (!principalIds.has(entry.principalId)) return true;
|
|
264
|
+
entry.releaseAuthorization();
|
|
265
|
+
return false;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return affectedRunning;
|
|
269
|
+
}
|
|
270
|
+
|
|
157
271
|
/** Cancel every running turn (used on shutdown). */
|
|
158
272
|
cancelAll(reason: string): boolean {
|
|
159
|
-
|
|
273
|
+
const hadWork = this.running.size > 0 || this.startingBySession.size > 0;
|
|
274
|
+
for (const starting of this.startingBySession.values()) {
|
|
275
|
+
starting.cancelled = true;
|
|
276
|
+
starting.queue.length = 0;
|
|
277
|
+
}
|
|
278
|
+
if (!hadWork) return false;
|
|
160
279
|
for (const turn of [...this.running]) this.cancelTurn(turn, reason);
|
|
161
280
|
return true;
|
|
162
281
|
}
|
|
@@ -164,12 +283,18 @@ export class TurnCoordinator {
|
|
|
164
283
|
/** Cancel a session's current turn and drop its queued follow-ups. */
|
|
165
284
|
cancelTurn(turn: RunningTurn, reason: string): void {
|
|
166
285
|
turn.cancelled = true;
|
|
167
|
-
turn.queue.
|
|
286
|
+
for (const entry of turn.queue.splice(0)) entry.releaseAuthorization();
|
|
168
287
|
clearTimeout(turn.timeoutHandle);
|
|
169
288
|
turn.abortController.abort();
|
|
170
289
|
this.drainPendingForTurn(turn, reason);
|
|
171
290
|
}
|
|
172
291
|
|
|
292
|
+
/** Record now, or retain the actor across the pre-recorder startup window. */
|
|
293
|
+
recordCancellation(turn: RunningTurn, principalId: string): void {
|
|
294
|
+
if (turn.recorder) turn.recorder.recordCancellation(principalId);
|
|
295
|
+
else turn.pendingCancellationPrincipalIds.push(principalId);
|
|
296
|
+
}
|
|
297
|
+
|
|
173
298
|
/** Resolve/reject every pending interactive request belonging to one turn. */
|
|
174
299
|
drainPendingForTurn(turn: RunningTurn, reason: string): void {
|
|
175
300
|
for (const [id, p] of this.pendingApprovals) {
|
|
@@ -238,7 +363,13 @@ export class TurnCoordinator {
|
|
|
238
363
|
for (const turn of [...this.running]) {
|
|
239
364
|
clearTimeout(turn.timeoutHandle);
|
|
240
365
|
turn.abortController.abort();
|
|
366
|
+
for (const entry of turn.queue.splice(0)) entry.releaseAuthorization();
|
|
367
|
+
}
|
|
368
|
+
for (const starting of this.startingBySession.values()) {
|
|
369
|
+
starting.cancelled = true;
|
|
370
|
+
starting.queue.length = 0;
|
|
241
371
|
}
|
|
372
|
+
this.startingBySession.clear();
|
|
242
373
|
this.running.clear();
|
|
243
374
|
this.bySession.clear();
|
|
244
375
|
this.pendingApprovals.clear();
|