@ultimat3/realtime 29.3.2 → 30.0.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.
@@ -0,0 +1,386 @@
1
+ // `deviceSocket()`: a declared, long-lived socket for a client that is not a browser — a device
2
+ // agent that dials out and receives work. A factory over `route`: the value IS a websocket mount
3
+ // (`path`, `fetch`, `websocket`, the shape `@ultimat3/http`'s `WebSocketMount` reads), plus
4
+ // `request()`, which reaches one connected device from any process the framework booted.
5
+
6
+ import type { Actor, Clock } from '@ultimat3/core';
7
+ import { ConfigInvalidError, finiteOption, onShutdown, systemClock } from '@ultimat3/core';
8
+ import { DeviceBus } from './device-bus';
9
+ import { declarerFile } from './device-declarer';
10
+ import {
11
+ DEVICE_MAX_DEADLINE_MS,
12
+ DeviceFrameTooLargeError,
13
+ DeviceOfflineError,
14
+ } from './device-errors';
15
+ import { type DeviceFrameHandler, DeviceHost, type DeviceWs } from './device-host';
16
+ import {
17
+ answerDeviceUpgrade,
18
+ type DeviceTokenResolver,
19
+ type DeviceUpgradeTarget,
20
+ tenantOf,
21
+ } from './device-upgrade';
22
+ import { bytesOverLimit, DEVICE_FRAME_MAX_BYTES } from './device-wire';
23
+ import type { Transport } from './fanout';
24
+ import type { JsonValue } from './json';
25
+ import { PrincipalSockets } from './principal-sockets';
26
+ import { AcceptBudget, type Rng, type Scheduler, timeoutScheduler } from './thundering-herd';
27
+
28
+ export const DEFAULT_DEVICE_HEARTBEAT_MS = 15_000;
29
+ export const DEFAULT_DEVICE_SOCKETS_PER_ACTOR = 10;
30
+ export const DEFAULT_DEVICE_REQUEST_DEADLINE_MS = 30_000;
31
+ /** How long `resolveToken` may hold an upgrade before it is answered 503. */
32
+ export const DEVICE_AUTH_DEADLINE_MS = 5_000;
33
+ /** Upgrades parked in `resolveToken` at once, per declaration per node; past it, shed. */
34
+ export const DEVICE_MAX_PENDING_UPGRADES = 256;
35
+ const MIN_HEARTBEAT_MS = 1_000;
36
+ const MAX_HEARTBEAT_MS = 300_000;
37
+ /** Accepts per second during a reconnect herd, and the burst before that bites. */
38
+ const ACCEPT_PER_SECOND = 100;
39
+ /** The drain's defaults: the sync node's, so a fleet restart spreads devices as it spreads tabs. */
40
+ const DRAIN_GRACE_MS = 5_000;
41
+ const DRAIN_SPREAD_MS = 30_000;
42
+
43
+ export interface DeviceSocketOptions {
44
+ /** `'/link'` — lowercase segments, never under `/_x` or `/api`, which the framework serves. */
45
+ readonly path: string;
46
+ readonly resolveToken: DeviceTokenResolver;
47
+ readonly onFrame: DeviceFrameHandler;
48
+ readonly maxSocketsPerActor?: number | undefined;
49
+ /** How often a device must send a frame (a `{ type: 'beat' }` is free); silent for two, it is closed. */
50
+ readonly heartbeatMs?: number | undefined;
51
+ /** Browser origins admitted, exactly. Empty (the default): an upgrade with an `Origin` is refused. */
52
+ readonly allowedOrigins?: readonly string[] | undefined;
53
+ }
54
+
55
+ /** What a process hands a declaration so it can reach devices, and hold them when it serves. */
56
+ export interface DeviceAttachOptions {
57
+ readonly transport: Transport;
58
+ readonly clock?: Clock | undefined;
59
+ readonly rng?: Rng | undefined;
60
+ readonly schedule?: Scheduler | undefined;
61
+ /** This process accepts device sockets (the web role); `false` only addresses them. */
62
+ readonly serve: boolean;
63
+ /** Register the drain on core's lifecycle. `false` for a host that drains by hand (a test). */
64
+ readonly lifecycle?: boolean | undefined;
65
+ }
66
+
67
+ export interface DeviceAttachment {
68
+ drain(options?: { graceMs?: number; spreadMs?: number }): Promise<void>;
69
+ stop(): void;
70
+ }
71
+
72
+ export interface DeviceRequestOptions {
73
+ readonly deadlineMs?: number | undefined;
74
+ }
75
+
76
+ export interface DeviceSocket {
77
+ readonly path: string;
78
+ fetch(request: Request, server: DeviceUpgradeTarget): Promise<Response | undefined>;
79
+ readonly websocket: {
80
+ open(ws: DeviceWs): void;
81
+ message(ws: DeviceWs, message: string | Uint8Array): void;
82
+ close(ws: DeviceWs): void;
83
+ readonly maxPayloadLength: number;
84
+ readonly idleTimeout: number;
85
+ };
86
+ /**
87
+ * One frame to one device, its answer back — from an action, a job, any role. Scoped to the
88
+ * caller's tenant (`ctx.actor.orgId`): a device registered under another one is `X_DEVICE_OFFLINE`,
89
+ * exactly as an unknown one is.
90
+ */
91
+ request(
92
+ ctx: { readonly actor: Actor },
93
+ deviceId: string,
94
+ frame: JsonValue,
95
+ options?: DeviceRequestOptions,
96
+ ): Promise<JsonValue>;
97
+ attach(options: DeviceAttachOptions): Promise<DeviceAttachment>;
98
+ /** Sockets this process holds for this declaration. */
99
+ readonly connected: number;
100
+ }
101
+
102
+ /** Each path's declaration, the generation of the scan that evaluated it, and its module's file. */
103
+ const declared = new Map<
104
+ string,
105
+ { readonly link: DeviceSocket; readonly generation: number; readonly by: string | null }
106
+ >();
107
+ /**
108
+ * Bumped by `x dev` before each rescan, never by a container (one scan per process). A module the
109
+ * rescan re-evaluates declares its path again: in a NEW generation that is the same declaration,
110
+ * edited, and replaces the old value; inside one generation it is a second module claiming the
111
+ * path, and so is a DIFFERENT module in any generation — the holder was not re-evaluated, only
112
+ * evicted modules are (`app-reload-graph.ts`), so taking its path would silently swap its handler.
113
+ */
114
+ let generation = 0;
115
+
116
+ /** Every declaration this process imported, in declaration order — what a boot attaches. */
117
+ export function declaredDeviceSockets(): readonly DeviceSocket[] {
118
+ return [...declared.values()].map((entry) => entry.link);
119
+ }
120
+
121
+ /** `x dev`'s rescan seam: a path declared from here on replaces its earlier generation's value. */
122
+ export function beginDeviceSocketGeneration(): void {
123
+ generation += 1;
124
+ }
125
+
126
+ /** Test seam: forget every declaration, the state a process boots in — `resetChannelTransport`'s twin. */
127
+ export function forgetDeviceSockets(): void {
128
+ declared.clear();
129
+ }
130
+
131
+ const PATH = /^\/[a-z0-9][a-z0-9_-]*(?:\/[a-z0-9][a-z0-9_-]*)*$/;
132
+
133
+ function invalid(path: string, cause: string, fix: string): ConfigInvalidError {
134
+ return new ConfigInvalidError({
135
+ cause: `deviceSocket("${path}"): ${cause}`,
136
+ fix,
137
+ meta: { path },
138
+ });
139
+ }
140
+
141
+ function screenPath(path: string, by: string | null): string {
142
+ const shown = PATH.test(path) ? path : '<invalid>';
143
+ if (!PATH.test(path)) {
144
+ throw invalid(
145
+ shown,
146
+ 'the path must be lowercase segments with no parameter or trailing slash',
147
+ "deviceSocket({ path: '/link', … })",
148
+ );
149
+ }
150
+ if (path === '/_x' || path.startsWith('/_x/') || path === '/api' || path.startsWith('/api/')) {
151
+ throw invalid(
152
+ path,
153
+ 'the framework serves /_x and /api itself',
154
+ "deviceSocket({ path: '/link', … })",
155
+ );
156
+ }
157
+ const held = declared.get(path);
158
+ const elsewhere = held?.by != null && by !== null && held.by !== by;
159
+ if (held?.generation === generation || elsewhere) {
160
+ throw invalid(
161
+ path,
162
+ elsewhere
163
+ ? `${held.by} already declares this path, and ${by} declares it again`
164
+ : 'another deviceSocket already declares this path',
165
+ `grep -rn "path: '${path}'" apps/ # then keep one declaration; moved it? x stop && x dev`,
166
+ );
167
+ }
168
+ return path;
169
+ }
170
+
171
+ function screenHeartbeat(value: number | undefined): number {
172
+ const ms = finiteOption('deviceSocket', 'heartbeatMs', value ?? DEFAULT_DEVICE_HEARTBEAT_MS);
173
+ if (ms >= MIN_HEARTBEAT_MS && ms <= MAX_HEARTBEAT_MS) return ms;
174
+ throw invalid(
175
+ '',
176
+ `heartbeatMs must be between ${MIN_HEARTBEAT_MS} and ${MAX_HEARTBEAT_MS}, not ${ms}`,
177
+ `deviceSocket({ heartbeatMs: ${DEFAULT_DEVICE_HEARTBEAT_MS}, … })`,
178
+ );
179
+ }
180
+
181
+ function screenOrigins(origins: readonly string[] | undefined): readonly string[] {
182
+ return Object.freeze(
183
+ (origins ?? []).map((origin) => {
184
+ const parsed = URL.parse(origin)?.origin;
185
+ if (parsed === undefined || parsed === 'null') {
186
+ throw invalid(
187
+ '',
188
+ `allowedOrigins holds ${JSON.stringify(origin)}, which is no origin`,
189
+ "deviceSocket({ allowedOrigins: ['https://www.example.com'], … })",
190
+ );
191
+ }
192
+ return parsed;
193
+ }),
194
+ );
195
+ }
196
+
197
+ function screenDeadline(value: number | undefined): number {
198
+ const ms = finiteOption(
199
+ 'deviceLink.request',
200
+ 'deadlineMs',
201
+ value ?? DEFAULT_DEVICE_REQUEST_DEADLINE_MS,
202
+ );
203
+ if (ms > 0 && ms <= DEVICE_MAX_DEADLINE_MS) return ms;
204
+ throw invalid(
205
+ '',
206
+ `deadlineMs must be above 0 and at most ${DEVICE_MAX_DEADLINE_MS}, not ${ms}`,
207
+ `deviceLink.request(ctx, deviceId, frame, { deadlineMs: ${DEFAULT_DEVICE_REQUEST_DEADLINE_MS} })`,
208
+ );
209
+ }
210
+
211
+ const newId = (): string => crypto.randomUUID().replaceAll('-', '');
212
+
213
+ export function deviceSocket(options: DeviceSocketOptions): DeviceSocket {
214
+ const by = declarerFile([import.meta.path]);
215
+ const path = screenPath(options.path, by);
216
+ const heartbeatMs = screenHeartbeat(options.heartbeatMs);
217
+ const allowedOrigins = screenOrigins(options.allowedOrigins);
218
+ // `PrincipalSockets` refuses a count below 1 or a fraction; the NaN screen is this one.
219
+ const principals = new PrincipalSockets(
220
+ finiteOption(
221
+ 'deviceSocket',
222
+ 'maxSocketsPerActor',
223
+ options.maxSocketsPerActor ?? DEFAULT_DEVICE_SOCKETS_PER_ACTOR,
224
+ ),
225
+ );
226
+ const parked = { count: 0 };
227
+ let bus: DeviceBus | null = null;
228
+ let host: DeviceHost | null = null;
229
+ let accepting = false;
230
+ let accept = new AcceptBudget({ perSecond: ACCEPT_PER_SECOND, burst: ACCEPT_PER_SECOND * 2 });
231
+ let rng: Rng = Math.random;
232
+ let clock: Clock = systemClock;
233
+ let schedule: Scheduler = timeoutScheduler;
234
+
235
+ const link: DeviceSocket = {
236
+ path,
237
+ fetch: (request, server) =>
238
+ answerDeviceUpgrade(
239
+ {
240
+ path,
241
+ allowedOrigins,
242
+ resolveToken: options.resolveToken,
243
+ accept,
244
+ principals,
245
+ rng,
246
+ clock,
247
+ schedule,
248
+ authDeadlineMs: DEVICE_AUTH_DEADLINE_MS,
249
+ maxPendingUpgrades: DEVICE_MAX_PENDING_UPGRADES,
250
+ accepting: () => accepting && host !== null,
251
+ newSocketId: newId,
252
+ parked,
253
+ },
254
+ request,
255
+ server,
256
+ ),
257
+ websocket: {
258
+ open: (ws) => {
259
+ if (host === null) ws.close(1013, 'not serving');
260
+ else host.open(ws);
261
+ },
262
+ message: (ws, message) => host?.message(ws, message),
263
+ close: (ws) => {
264
+ if (host === null) principals.release(ws.data.socketId);
265
+ else host.close(ws);
266
+ },
267
+ maxPayloadLength: DEVICE_FRAME_MAX_BYTES,
268
+ // Bun's own idle cut sits past the beat sweep's, so the sweep (which says why) decides first.
269
+ idleTimeout: Math.ceil((heartbeatMs * 3) / 1_000),
270
+ },
271
+ async request(ctx, deviceId, frame, requestOptions = {}) {
272
+ const deadlineMs = screenDeadline(requestOptions.deadlineMs);
273
+ const over = bytesOverLimit(JSON.stringify({ type: 'request', id: newId(), frame }));
274
+ if (over !== null) {
275
+ throw new DeviceFrameTooLargeError({ path, bytes: over, limit: DEVICE_FRAME_MAX_BYTES });
276
+ }
277
+ if (bus === null) {
278
+ throw new DeviceOfflineError({
279
+ path,
280
+ deviceId,
281
+ reason:
282
+ 'this process never attached the declaration to a bus (the boot attaches every declared deviceSocket)',
283
+ });
284
+ }
285
+ return await bus.request(tenantOf(ctx.actor), deviceId, frame, deadlineMs);
286
+ },
287
+ async attach(attachOptions) {
288
+ if (bus !== null) {
289
+ throw invalid(
290
+ path,
291
+ 'it is already attached in this process',
292
+ 'x doctor --json # one boot attaches each declaration once',
293
+ );
294
+ }
295
+ clock = attachOptions.clock ?? systemClock;
296
+ rng = attachOptions.rng ?? Math.random;
297
+ schedule = attachOptions.schedule ?? timeoutScheduler;
298
+ accept = new AcceptBudget({
299
+ perSecond: ACCEPT_PER_SECOND,
300
+ burst: ACCEPT_PER_SECOND * 2,
301
+ clock,
302
+ });
303
+ const nodeId = newId();
304
+ const ownBus = new DeviceBus({
305
+ transport: attachOptions.transport,
306
+ path,
307
+ nodeId,
308
+ schedule,
309
+ clock,
310
+ newId,
311
+ });
312
+ const ownHost = attachOptions.serve
313
+ ? new DeviceHost({
314
+ path,
315
+ heartbeatMs,
316
+ onFrame: options.onFrame,
317
+ bus: ownBus,
318
+ principals,
319
+ clock,
320
+ rng,
321
+ schedule,
322
+ })
323
+ : null;
324
+ await ownBus.start(ownHost);
325
+ bus = ownBus;
326
+ host = ownHost;
327
+ accepting = ownHost !== null;
328
+ let cancelSweep: () => void = () => undefined;
329
+ const arm = (): void => {
330
+ cancelSweep = schedule(() => {
331
+ ownHost?.sweep();
332
+ arm();
333
+ }, heartbeatMs);
334
+ };
335
+ if (ownHost !== null) arm();
336
+ const drain = async (
337
+ drainOptions: { graceMs?: number; spreadMs?: number } = {},
338
+ ): Promise<void> => {
339
+ accepting = false;
340
+ await ownHost?.drain({
341
+ graceMs: finiteOption(
342
+ 'deviceLink.drain',
343
+ 'graceMs',
344
+ drainOptions.graceMs ?? DRAIN_GRACE_MS,
345
+ ),
346
+ spreadMs: finiteOption(
347
+ 'deviceLink.drain',
348
+ 'spreadMs',
349
+ drainOptions.spreadMs ?? DRAIN_SPREAD_MS,
350
+ ),
351
+ });
352
+ };
353
+ // The sync node's two phases: stop taking sockets, then hand the held ones to the fleet.
354
+ const hooks =
355
+ attachOptions.lifecycle === false || ownHost === null
356
+ ? []
357
+ : [
358
+ onShutdown(
359
+ `realtime:device:${path}:accept`,
360
+ () => {
361
+ accepting = false;
362
+ },
363
+ { phase: 'accept' },
364
+ ),
365
+ onShutdown(`realtime:device:${path}:drain`, () => drain(), { phase: 'inflight' }),
366
+ ];
367
+ return {
368
+ drain,
369
+ stop: () => {
370
+ for (const unregister of hooks) unregister();
371
+ cancelSweep();
372
+ accepting = false;
373
+ ownBus.stop();
374
+ bus = null;
375
+ host = null;
376
+ },
377
+ };
378
+ },
379
+ get connected() {
380
+ return host?.count ?? 0;
381
+ },
382
+ };
383
+ // `set` on a key it holds keeps its place: a reload re-declares a path, never reorders it.
384
+ declared.set(path, { link, generation, by });
385
+ return link;
386
+ }
@@ -0,0 +1,218 @@
1
+ // Whether a device may have a socket, decided entirely BEFORE `server.upgrade`: origin, load, the
2
+ // bearer token (bounded by a deadline, so a token service that hangs cannot park upgrades without
3
+ // end), expiry and the per-actor cap. A refused upgrade never costs a websocket.
4
+
5
+ import type { Actor, Clock } from '@ultimat3/core';
6
+ import { reportError } from '@ultimat3/core';
7
+ import {
8
+ deviceAuthUnavailable,
9
+ deviceOriginRefused,
10
+ deviceSocketLimit,
11
+ deviceUnauthenticated,
12
+ } from './device-errors';
13
+ import type { PrincipalSockets } from './principal-sockets';
14
+ import { toWireError } from './sync-protocol';
15
+ import type { AcceptBudget, Rng, Scheduler } from './thundering-herd';
16
+
17
+ /** What a token resolves to: who the device acts as, which device it is, and until when. */
18
+ export interface DeviceGrant {
19
+ readonly actor: Actor;
20
+ /** The app's own id for the device — what `request(ctx, deviceId, …)` addresses. */
21
+ readonly deviceId: string;
22
+ /** Epoch ms. Past it the upgrade is refused, and an open socket is closed by the sweep. */
23
+ readonly expiresAt?: number | undefined;
24
+ }
25
+
26
+ /** How the upgrade arrived, as far as a token decision may read it — never the raw headers. */
27
+ export interface DeviceUpgradeFacts {
28
+ /** The peer's socket address; `null` when the host cannot say. */
29
+ readonly address: string | null;
30
+ readonly userAgent: string | null;
31
+ readonly path: string;
32
+ }
33
+
34
+ /** `null` for a malformed, unknown, revoked or expired token: all one indistinguishable 401. */
35
+ export type DeviceTokenResolver = (
36
+ token: string,
37
+ facts: DeviceUpgradeFacts,
38
+ ) => Promise<DeviceGrant | null> | DeviceGrant | null;
39
+
40
+ /** What the upgrade hands the socket. The grant rides here: this socket kind has no re-auth. */
41
+ export interface DeviceWsData {
42
+ readonly socketId: string;
43
+ readonly grant: DeviceGrant;
44
+ /** `grant.actor.orgId`, or `''` for an actor with none — the key every lookup is scoped by. */
45
+ readonly tenant: string;
46
+ }
47
+
48
+ /** Structural view of `Bun.serve`'s server, as much of one as the upgrade reads. */
49
+ export interface DeviceUpgradeTarget {
50
+ upgrade(request: Request, options: { data: DeviceWsData }): boolean;
51
+ requestIP(request: Request): { readonly address: string } | null;
52
+ }
53
+
54
+ export interface DeviceUpgradeDeps {
55
+ readonly path: string;
56
+ readonly allowedOrigins: readonly string[];
57
+ readonly resolveToken: DeviceTokenResolver;
58
+ readonly accept: AcceptBudget;
59
+ readonly principals: PrincipalSockets;
60
+ readonly rng: Rng;
61
+ readonly clock: Clock;
62
+ readonly schedule: Scheduler;
63
+ readonly authDeadlineMs: number;
64
+ readonly maxPendingUpgrades: number;
65
+ /** Bound to a bus and not draining. Asked again after the token await. */
66
+ accepting(): boolean;
67
+ newSocketId(): string;
68
+ /** Upgrades parked in `resolveToken` right now — read and moved by this function alone. */
69
+ readonly parked: { count: number };
70
+ }
71
+
72
+ /** The tenant key of an actor: its org, or `''` for an app with no tenancy. */
73
+ export const tenantOf = (actor: Actor): string => actor.orgId ?? '';
74
+
75
+ /** The per-actor cap key: an actor's id within its tenant, never request text. */
76
+ const principalOf = (actor: Actor): string =>
77
+ `actor:${JSON.stringify([tenantOf(actor), actor.id])}`;
78
+
79
+ /** `Authorization: Bearer <token>`, scheme case-insensitive; anything else is no token. */
80
+ export function bearerToken(header: string | null): string | null {
81
+ const match = header === null ? null : /^Bearer[ ]+([^ ]+)[ ]*$/i.exec(header);
82
+ return match?.[1] ?? null;
83
+ }
84
+
85
+ const SHED_STATUS = 503;
86
+
87
+ /** `undefined`: the upgrade took and Bun owns the connection. Every other answer is `no-store`. */
88
+ export async function answerDeviceUpgrade(
89
+ deps: DeviceUpgradeDeps,
90
+ request: Request,
91
+ server: DeviceUpgradeTarget,
92
+ ): Promise<Response | undefined> {
93
+ // A browser says where it is; a device does not. An Origin is a page — refused before anything
94
+ // is spent, unless the declaration lists that exact origin.
95
+ const origin = request.headers.get('origin');
96
+ if (origin !== null && !deps.allowedOrigins.includes(URL.parse(origin)?.origin ?? '')) {
97
+ return refusal(403, deviceOriginRefused(deps.path, deps.allowedOrigins));
98
+ }
99
+ const token = bearerToken(request.headers.get('authorization'));
100
+ if (token === null) {
101
+ return refusal(
102
+ 401,
103
+ deviceUnauthenticated(deps.path, 'no Authorization: Bearer token'),
104
+ 'Bearer',
105
+ );
106
+ }
107
+ if (!deps.accepting() || deps.parked.count >= deps.maxPendingUpgrades) return shed(deps);
108
+ if (!deps.accept.tryAccept()) return shed(deps);
109
+ const facts: DeviceUpgradeFacts = Object.freeze({
110
+ address: server.requestIP(request)?.address ?? null,
111
+ userAgent: request.headers.get('user-agent'),
112
+ path: deps.path,
113
+ });
114
+ const decided = await decide(deps, token, facts);
115
+ if (decided instanceof Response) {
116
+ deps.accept.refund();
117
+ return decided;
118
+ }
119
+ // Asked again: the await above was app code, and SIGTERM may have landed while it ran.
120
+ if (!deps.accepting()) {
121
+ deps.accept.refund();
122
+ return shed(deps);
123
+ }
124
+ const data: DeviceWsData = {
125
+ socketId: deps.newSocketId(),
126
+ grant: decided,
127
+ tenant: tenantOf(decided.actor),
128
+ };
129
+ // No await between this and `server.upgrade`, so a herd of one actor cannot all pass one count.
130
+ if (!deps.principals.admit(data.socketId, principalOf(decided.actor))) {
131
+ deps.accept.refund();
132
+ return refusal(429, deviceSocketLimit(deps.path, deps.principals.max));
133
+ }
134
+ let upgraded = false;
135
+ try {
136
+ upgraded = server.upgrade(request, { data });
137
+ } finally {
138
+ if (!upgraded) {
139
+ deps.principals.release(data.socketId);
140
+ deps.accept.refund();
141
+ }
142
+ }
143
+ return upgraded
144
+ ? undefined
145
+ : new Response('expected websocket', { status: 426, headers: NO_STORE });
146
+ }
147
+
148
+ /** The grant, or the refusal: a resolver that raises or outlives the deadline is a 503, not a 401. */
149
+ async function decide(
150
+ deps: DeviceUpgradeDeps,
151
+ token: string,
152
+ facts: DeviceUpgradeFacts,
153
+ ): Promise<DeviceGrant | Response> {
154
+ deps.parked.count += 1;
155
+ let cancel: () => void = () => undefined;
156
+ try {
157
+ const deadline = new Promise<'deadline'>((resolve) => {
158
+ cancel = deps.schedule(() => resolve('deadline'), deps.authDeadlineMs);
159
+ });
160
+ const grant = await Promise.race([Promise.resolve(deps.resolveToken(token, facts)), deadline]);
161
+ if (grant === 'deadline') {
162
+ return refusal(
163
+ SHED_STATUS,
164
+ deviceAuthUnavailable(`no answer within ${deps.authDeadlineMs}ms`),
165
+ );
166
+ }
167
+ if (grant === null) {
168
+ return refusal(
169
+ 401,
170
+ deviceUnauthenticated(deps.path, 'resolveToken() resolved no grant'),
171
+ 'Bearer error="invalid_token"',
172
+ );
173
+ }
174
+ if (grant.expiresAt !== undefined && !(grant.expiresAt > deps.clock.now().getTime())) {
175
+ return refusal(
176
+ 401,
177
+ deviceUnauthenticated(deps.path, 'the grant resolveToken() returned has expired'),
178
+ 'Bearer error="invalid_token"',
179
+ );
180
+ }
181
+ if (typeof grant.deviceId !== 'string' || grant.deviceId.length === 0) {
182
+ return refusal(
183
+ 401,
184
+ deviceUnauthenticated(deps.path, 'the grant resolveToken() returned names no deviceId'),
185
+ 'Bearer error="invalid_token"',
186
+ );
187
+ }
188
+ return grant;
189
+ } catch (error) {
190
+ reportError(error, { source: 'realtime', scope: { operation: 'device.resolve_token' } });
191
+ return refusal(SHED_STATUS, deviceAuthUnavailable('see the node log for the cause'));
192
+ } finally {
193
+ cancel();
194
+ deps.parked.count -= 1;
195
+ }
196
+ }
197
+
198
+ const NO_STORE = { 'cache-control': 'no-store' } as const;
199
+
200
+ /** The sync node's refusal body, and the bearer challenge `@ultimat3/http`'s mounts send. */
201
+ function refusal(status: number, error: unknown, challenge?: string): Response {
202
+ return new Response(JSON.stringify({ error: toWireError(error) }), {
203
+ status,
204
+ headers: {
205
+ 'content-type': 'application/json',
206
+ ...NO_STORE,
207
+ ...(challenge === undefined ? {} : { 'www-authenticate': challenge }),
208
+ },
209
+ });
210
+ }
211
+
212
+ /** Load shed with a jittered delay — refusing without one only moves the herd next door. */
213
+ function shed(deps: DeviceUpgradeDeps): Response {
214
+ return new Response('retry', {
215
+ status: SHED_STATUS,
216
+ headers: { ...NO_STORE, 'retry-after-ms': String(deps.accept.retryAfterMs(deps.rng)) },
217
+ });
218
+ }