workerdeck 0.23.0 → 1.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.
package/build/index.d.mts CHANGED
@@ -4,39 +4,20 @@ import { IncomingMessage, ServerResponse } from "node:http";
4
4
  import { ProfileInfo, SessionNotification } from "@workerdeck/protocol";
5
5
 
6
6
  //#region src/apns/client.d.ts
7
- /**
8
- * A minimal APNs provider client: an HTTP/2 POST carrying an ES256 JWT.
9
- *
10
- * Hand-rolled rather than a dependency, because that is the whole of the
11
- * protocol and the published CLI's zero-runtime-dep posture is worth more than
12
- * the eighty lines. Note `fetch`/undici will not do: it does not speak HTTP/2,
13
- * and APNs accepts nothing else — hence `node:http2` directly.
14
- *
15
- * Token authentication, not certificates: one `.p8` serves every app in the team
16
- * and both environments, and it does not expire. Certificates are per-app,
17
- * per-environment, and expire annually.
18
- */
19
7
  type ApnsEnvironment = 'development' | 'production';
20
8
  type ApnsConfig = {
21
- /** Path to the `.p8`. A path, never the contents — a secret pasted into a
22
- * config file is a secret in every backup of that file. */
23
- keyFile: string; /** The 10-character Key ID shown beside the key in the developer portal. */
24
- keyId: string; /** The 10-character Team ID from the top right of the portal. */
25
- teamId: string; /** The APNs topic, which is the app's bundle id (`bi.atomic.workerdeck.ios`). */
9
+ keyFile: string;
10
+ keyId: string;
11
+ teamId: string;
26
12
  topic: string;
27
- /** Environment for a device that registered without naming one. Devices that
28
- * do name one are always routed by their own answer — see the note on
29
- * `ApnsEnvironment` below. */
30
13
  production?: boolean;
31
14
  };
32
15
  type ApnsRequest = {
33
16
  deviceToken: string;
34
17
  environment: ApnsEnvironment;
35
- payload: unknown; /** 10 for something a person is waiting on, 5 for everything else. */
36
- priority?: 5 | 10; /** Unix seconds after which Apple stops trying. 0 means "one attempt". */
18
+ payload: unknown;
19
+ priority?: 5 | 10;
37
20
  expiration?: number;
38
- /** Later pushes with the same id replace an undelivered earlier one. Max 64
39
- * bytes, so never pass a raw identifier of unbounded length. */
40
21
  collapseId?: string;
41
22
  };
42
23
  type ApnsResult = {
@@ -46,152 +27,49 @@ type ApnsResult = {
46
27
  ok: false;
47
28
  status: number;
48
29
  reason: string;
49
- /** The token is dead: drop it from the registry rather than retrying it
50
- * forever. The app re-registers on its next launch anyway. */
51
30
  unregistered: boolean;
52
31
  };
53
32
  type ApnsClient = {
54
33
  send(request: ApnsRequest): Promise<ApnsResult>;
55
34
  close(): void;
56
35
  };
57
- /**
58
- * Load and sanity-check the auth key. Done once at startup rather than at the
59
- * first push, so a mistyped path is a launch error with a clear message instead
60
- * of a notification that silently never arrives.
61
- */
62
36
  declare function loadApnsKey(keyFile: string): Promise<KeyObject>;
63
- declare function createApnsClient(config: ApnsConfig, key: KeyObject,
64
- /** Test seam: point the two environments at a local HTTP/2 server, and
65
- * shorten the redial pause so a test does not sit out the real one. Nothing
66
- * in production should pass this — the real endpoints are not configurable,
67
- * and an operator who could redirect them could exfiltrate every push. */
68
-
69
- options?: {
37
+ declare function createApnsClient(config: ApnsConfig, key: KeyObject, options?: {
70
38
  hosts?: Record<ApnsEnvironment, string>;
71
39
  retryDelayMs?: number;
72
40
  }): ApnsClient;
73
41
  //#endregion
74
42
  //#region src/auth/auth.d.ts
75
- /**
76
- * Gateway auth for the turnkey CLI: one shared operator secret, three transports.
77
- *
78
- * Services present the secret itself on every request (`x-workerdeck-key`,
79
- * or `Authorization: Bearer`). The dashboard *served by this gateway* cannot:
80
- * it calls `location.origin + '/v1'` with no headers, and a browser WebSocket
81
- * handshake carries no custom headers at all. So browsers POST the secret once
82
- * to `/auth/login`, get an HttpOnly cookie naming a server-side session, and
83
- * the cookie rides same-origin REST and the WS upgrade automatically. That
84
- * automatic ride is also the threat: the cookie is ambient authority, and the
85
- * WS handshake is exempt from CORS, so cross-origin misuse is fenced off by an
86
- * explicit Origin check here — not by the browser.
87
- *
88
- * The third transport exists for a dashboard served *elsewhere* attaching to
89
- * this gateway: it holds the key (the operator typed it in) and can put it on
90
- * REST as a header, but still cannot put it on the WS handshake — and the
91
- * cookie is another origin's, so it does not ride either. Such a client passes
92
- * the key as `?key=` on the upgrade URL, and `querySecret` below accepts it
93
- * **on upgrades only**.
94
- *
95
- * That is a deliberate, narrow concession, and its cost should be understood
96
- * rather than rediscovered: a key in a query string is a permanent, replayable
97
- * credential sitting in reverse-proxy access logs, where a header would not be.
98
- * It is confined to upgrades so a leaked URL buys an attach and nothing else,
99
- * and the seam it arrives through (`ClientOptions.buildWsUrl`) is the same one
100
- * a short-lived minted ticket would use — swapping to one later needs no client
101
- * change.
102
- *
103
- * This file guards the operator's own gateway and nothing else. It never sees
104
- * an Anthropic credential — those are resolved by the SDK/CLI from the
105
- * operator's environment (root CLAUDE.md, auth red lines).
106
- */
107
43
  type CliAuthOptions = {
108
- /**
109
- * The shared operator secret. Unset disables auth entirely (the CLI then
110
- * refuses to bind anything but loopback — enforced by the caller, not here).
111
- * An empty or short value is a config accident, not a choice: anything under
112
- * 12 characters throws rather than standing up a guessable gateway.
113
- */
114
44
  secret?: string;
115
- /** Default 'workerdeck_session'. No `__Host-` prefix — it requires
116
- * `Secure`, and plain-HTTP localhost is the primary deployment. */
117
45
  cookieName?: string;
118
- /**
119
- * Browser session lifetime, default 7 days. Fixed, not sliding: the auth
120
- * hooks only see the request, so a renewed cookie has nowhere to ride — and
121
- * for a dashboard whose whole login is retyping one secret, a periodic
122
- * re-login is cheaper than a refresh endpoint. Expiry simply lands the
123
- * operator back on the login page — as does a restart, unless a `sessions`
124
- * store is supplied (the CLI supplies one whenever it has a state dir).
125
- */
126
46
  ttlMs?: number;
127
- /**
128
- * Trust `x-forwarded-proto` / `x-forwarded-host` / `x-forwarded-for` from
129
- * exactly one reverse proxy in front of this process; the *last* value of
130
- * each is used (the one the proxy set, the only position a client cannot
131
- * forge). Off by default: these are attacker-writable headers on a directly
132
- * exposed port. Behind a TLS-terminating proxy this must be on, or the
133
- * `Secure` cookie flag is skipped and the Origin check computes `http://`
134
- * where the browser says `https://` and rejects the dashboard's own writes.
135
- */
136
47
  trustProxy?: boolean;
137
- /**
138
- * Origins accepted in addition to the request's own (scheme + Host). Needed
139
- * when the proxy rewrites Host so the external origin no longer matches what
140
- * this process sees. Entries must be full origins ('https://ops.example.com');
141
- * invalid ones throw at startup rather than silently never matching.
142
- */
143
48
  allowedOrigins?: string[];
144
- /** Login throttle tuning; defaults: 15 min window, 10 failures per IP, 100
145
- * globally. Exposed mainly so tests need not wait out real windows. */
146
49
  throttle?: {
147
50
  windowMs?: number;
148
51
  maxFailuresPerIp?: number;
149
52
  maxFailuresGlobal?: number;
150
53
  };
151
- /**
152
- * Makes browser logins survive a restart. Absent, the session table is
153
- * in-memory and a restart signs every browser out — see the table's own note.
154
- * `createAuthSessionStore` is the CLI's file-backed implementation.
155
- */
156
54
  sessions?: CliSessionStore;
157
55
  };
158
- /** One session-table row as it is handed to a store; the key is opaque here. */
159
56
  type StoredSession = {
160
57
  expiresAt: number;
161
58
  };
162
- /**
163
- * The durability seam for browser logins. Deliberately narrow and
164
- * fire-and-forget: the auth paths are synchronous, so a store may not make them
165
- * wait, and a store that cannot write must degrade to "logins do not survive a
166
- * restart" rather than refuse a login.
167
- */
168
59
  type CliSessionStore = {
169
- /** Rows recovered at startup, already pruned of expired ones. */initial?: Iterable<[string, StoredSession]>; /** The whole live table after a mutation. Must not throw. */
170
- save(entries: [string, StoredSession][]): void; /** Resolves once queued writes have landed — for tests and shutdown. */
60
+ initial?: Iterable<[string, StoredSession]>;
61
+ save(entries: [string, StoredSession][]): void;
171
62
  flush?(): Promise<void>;
172
63
  };
173
- /** What `authenticate` hands the worker server as the request principal. */
174
64
  type CliPrincipal = {
175
- /** Which transport authenticated the request; 'open' when auth is disabled. */via: 'header' | 'cookie' | 'open';
176
- /** One secret, one trust level: whoever holds it is the operator, so the
177
- * dashboard may manage provider profiles (still bounded by the server's
178
- * `allowedConfigDirRoots`). */
65
+ via: 'header' | 'cookie' | 'open';
179
66
  canManageProfiles: true;
180
67
  };
181
68
  type CliAuth = {
182
69
  enabled: boolean;
183
- /** Hand straight to `createWorkerServer({ authenticate })` — it guards both
184
- * REST and the WS upgrade, which is exactly why the Origin policy lives in it. */
185
70
  authenticate: Authenticator;
186
- /** Claims `/auth` and everything under it (login/logout/status); returns true
187
- * when it consumed the request. The static host must call this first. */
188
71
  handleAuthRequest(req: IncomingMessage, res: ServerResponse): boolean | Promise<boolean>;
189
- /** Cookie-only check for the static host: login page or SPA? Gating the SPA
190
- * shell is UX, not security — every byte of data sits behind `authenticate`. */
191
72
  hasValidSession(req: IncomingMessage): boolean;
192
- /** What the login page needs to render for this request. The endpoint path,
193
- * field name, and the `?auth=` redirect params are all this module's wire
194
- * format, so the page learns them here instead of hardcoding them. */
195
73
  loginPage(req: IncomingMessage): {
196
74
  action: string;
197
75
  field: string;
@@ -201,63 +79,16 @@ type CliAuth = {
201
79
  declare function createCliAuth(options?: CliAuthOptions): CliAuth;
202
80
  //#endregion
203
81
  //#region src/config.d.ts
204
- /**
205
- * What a `workerdeck.config.mjs` default-exports: the server options, plus
206
- * the few instance-level settings that aren't the server's business. Keeping
207
- * them in one object means a deployment is one file, not a file plus a
208
- * memorised command line.
209
- */
210
82
  type WorkerDeckConfig = WorkerServerOptions & {
211
83
  port?: number;
212
- host?: string; /** Built-in shared-secret auth. Ignored entirely if you supply `authenticate`. */
213
- auth?: CliAuthOptions; /** Where parked sessions are persisted; null disables durable parking. */
84
+ host?: string;
85
+ auth?: CliAuthOptions;
214
86
  stateDir?: string | null;
215
- /**
216
- * Host header values accepted when running *without* auth. Defaults to the
217
- * loopback names; see `resolveInstanceConfig` for why this exists at all.
218
- */
219
87
  allowedHosts?: string[];
220
- /**
221
- * Bind hosts that may serve without auth. One declaration, two effects:
222
- * binding a listed host waives the auth requirement (no key demanded, none
223
- * generated), and while unauthenticated every entry is also accepted as a
224
- * Host header, so the operator states the intent once. Entries name a host,
225
- * never an endpoint — a port is rejected — and match the bind host literally
226
- * and case-insensitively: nothing is inferred from DNS or the network, and
227
- * `0.0.0.0` means the all-interfaces bind itself, not "any host". When auth
228
- * is on this widens nothing.
229
- */
230
- insecureHosts?: string[]; /** Serve a dashboard build from here instead of the bundled one. */
88
+ insecureHosts?: string[];
231
89
  webRoot?: string;
232
- /**
233
- * Serve the web dashboard at all. Default true — being turnkey is the point
234
- * of this package.
235
- *
236
- * `false` makes the instance a bare gateway: `/v1` and the auth routes answer
237
- * exactly as before, everything else 404s, and the dashboard build is never
238
- * even looked for (so a checkout with no `@workerdeck/web` build can still
239
- * run one). For an operator who reaches this gateway only from the VS Code
240
- * extension, the phone, or another machine's dashboard, the served copy is
241
- * surface they were not using.
242
- */
243
90
  web?: boolean;
244
- /**
245
- * Browser origins allowed to call this gateway cross-origin — for a dashboard
246
- * served somewhere else. Exact origins (`https://deck.example`), never a
247
- * wildcard. Refused unless auth is on: CORS on an open gateway would let any
248
- * allowlisted page drive it with no credential at all.
249
- */
250
91
  corsOrigins?: string[];
251
- /**
252
- * Forward session notifications to Apple Push Notification service, for the
253
- * iOS app. Absent turns the forwarder off entirely — including its
254
- * `/apns/devices` route, so a gateway without this answers 404 there and the
255
- * app quietly stops asking.
256
- *
257
- * Lives here rather than in `packages/server` on purpose: this is the only
258
- * place in the project that holds a push credential, and the OSS gateway
259
- * stays credential-free. `keyFile` is a path, never key contents.
260
- */
261
92
  apns?: ApnsConfig;
262
93
  };
263
94
  type CliFlags = {
@@ -283,67 +114,32 @@ type CliFlags = {
283
114
  version?: boolean;
284
115
  };
285
116
  declare class ConfigError extends Error {}
286
- /**
287
- * Hand-rolled rather than a dependency: the CLI's whole value is that `npx
288
- * workerdeck` pulls down a small tree, and an arg parser is a hundred lines
289
- * of it.
290
- */
291
117
  declare function parseArgs(argv: string[]): CliFlags;
292
118
  type LoadedConfig = {
293
- /** Absolute path of the file that was loaded, or null if there wasn't one. */path: string | null;
119
+ path: string | null;
294
120
  options: WorkerDeckConfig;
295
121
  };
296
- /**
297
- * Explicit `--config` must exist — a typo that silently starts a default
298
- * instance is worse than a failure. An implicit one is looked up in cwd only:
299
- * walking parent directories would make what a given command does depend on
300
- * where it was run from.
301
- */
302
122
  declare function loadConfigFile(explicit?: string, cwd?: string): Promise<LoadedConfig>;
303
123
  declare function isLoopback(host: string): boolean;
304
124
  type ResolvedConfig = {
305
125
  port: number;
306
- host: string; /** Shared secret, or undefined for an unauthenticated instance. */
307
- authKey?: string; /** Everything else the built-in auth takes (proxy trust, extra origins). */
308
- auth: CliAuthOptions; /** Where parked sessions and other instance state live; null disables durable parking. */
126
+ host: string;
127
+ authKey?: string;
128
+ auth: CliAuthOptions;
309
129
  stateDir: string | null;
310
- configPath: string | null; /** True when the config file supplied its own `authenticate` — built-in auth stands down. */
130
+ configPath: string | null;
311
131
  hostAuthenticates: boolean;
312
- /**
313
- * Auth is required here but no key was supplied: `startInstance` must
314
- * materialize one (stored under `stateDir`, ephemeral without one). This is a
315
- * *promise* rather than a key because resolution is pure and synchronous while
316
- * reading a key file is I/O — and the promise is load-bearing: `allowedHosts`
317
- * is already null on the strength of it, so `startInstance` refuses to serve
318
- * if materialization ever fails to arm the built-in auth.
319
- */
320
132
  generateAuthKey: boolean;
321
- /**
322
- * Host header values to accept, or null to accept any. Non-null only for an
323
- * unauthenticated instance — see `resolveInstanceConfig`.
324
- */
325
- allowedHosts: Set<string> | null; /** Dashboard build to serve; resolved from the package when unset. */
326
- webRoot?: string; /** Whether to serve the dashboard at all. False makes this a bare gateway. */
327
- web: boolean; /** Browser origins allowed to call this gateway cross-origin. Empty = off. */
133
+ allowedHosts: Set<string> | null;
134
+ webRoot?: string;
135
+ web: boolean;
328
136
  corsOrigins: string[];
329
- /** APNs forwarder settings with `keyFile` made absolute, or undefined for an
330
- * instance that does not push. */
331
137
  apns?: ApnsConfig;
332
138
  open: boolean;
333
139
  options: WorkerServerOptions;
334
140
  };
335
- /**
336
- * Durable parking is on by default because this is a long-lived instance: a
337
- * turnkey tool that silently drops parked work on every restart is the wrong
338
- * default. The store writes whole transcripts in plaintext, so it goes beside
339
- * the config file (or under the home directory) rather than anywhere temporary,
340
- * and one directory serves exactly one instance — the store is single-process
341
- * by design, which the single-port model already implies.
342
- */
343
141
  declare function defaultStateDir(configPath: string | null): string;
344
- /** Hostname out of a Host header, minus the port and any IPv6 brackets. */
345
142
  declare function hostnameOf(hostHeader: string): string;
346
- /** 127.0.0.0/8, ::1, and the names that mean them. */
347
143
  declare function isLoopbackHostname(hostname: string): boolean;
348
144
  declare function resolveInstanceConfig(flags: CliFlags, loaded: LoadedConfig, env?: NodeJS.ProcessEnv, cwd?: string): ResolvedConfig;
349
145
  //#endregion
@@ -351,28 +147,12 @@ declare function resolveInstanceConfig(flags: CliFlags, loaded: LoadedConfig, en
351
147
  type Instance = {
352
148
  server: WorkerServer;
353
149
  url: string;
354
- port: number; /** Resolves when the instance stops serving. */
355
- closed: Promise<void>;
150
+ port: number;
151
+ closed: Promise<void>; /** Let running turns finish before `close()`. See `WorkerServer.drain`. */
152
+ drain: WorkerServer['drain'];
356
153
  close: () => Promise<void>;
357
154
  };
358
- /**
359
- * The dashboard comes from `@workerdeck/web`, which ships it prebuilt and
360
- * exports the path to it. Depending on the package rather than vendoring a copy
361
- * means one dashboard, versioned in lockstep with everything else.
362
- *
363
- * In a checkout that directory only exists once the app has been built — dev
364
- * never builds — so the miss is worth a real message rather than a stack trace
365
- * from the static host.
366
- */
367
155
  declare function resolveWebRoot(): string;
368
- /**
369
- * The Host-header gate for an unauthenticated instance. `allowedHosts` is null
370
- * whenever auth is on, and then this is the identity function — with a
371
- * credential in play a rebound origin holds no cookie and fails `authenticate`
372
- * anyway. Loopback *names* are what's checked, not the socket: the attacker in
373
- * this scenario controls DNS, so the connection genuinely arrives on 127.0.0.1;
374
- * what they cannot control is the name the victim's browser writes into Host.
375
- */
376
156
  declare function createHostGuard(allowedHosts: Set<string> | null): (req: IncomingMessage) => boolean;
377
157
  declare function startInstance(config: ResolvedConfig, options?: {
378
158
  quiet?: boolean;
@@ -382,24 +162,9 @@ declare function startInstance(config: ResolvedConfig, options?: {
382
162
  declare function runGuard(argv: string[]): Promise<number>;
383
163
  //#endregion
384
164
  //#region src/auth/auth-key.d.ts
385
- /**
386
- * Materializes the key that `resolveInstanceConfig` promised via
387
- * `generateAuthKey`: resolution is pure and synchronous, reading a key file is
388
- * I/O, so the two halves meet here at startup. The contract is that this
389
- * function returns a usable secret or throws — it never returns "no key",
390
- * because the resolved config has already stood down the Host-header guard on
391
- * the strength of the promise, and a silent miss would serve an open gateway
392
- * that reports itself authenticated.
393
- *
394
- * The key persists under `stateDir` so a restart does not un-pair every client
395
- * that stored it (the iOS app keeps it in its keychain). No `stateDir` means
396
- * nothing durable to write, so the key is ephemeral per run — the banner says
397
- * so. This is the gateway's own operator secret and nothing else: Anthropic
398
- * credentials never pass through here (root CLAUDE.md, auth red lines).
399
- */
400
165
  type MaterializedAuthKey = {
401
- key: string; /** 'stored' reused the file, 'created' wrote a new one, 'ephemeral' had nowhere to write. */
402
- source: 'stored' | 'created' | 'ephemeral'; /** Where the key lives, or null when ephemeral. */
166
+ key: string;
167
+ source: 'stored' | 'created' | 'ephemeral';
403
168
  path: string | null;
404
169
  };
405
170
  declare function materializeAuthKey(stateDir: string | null, options?: {
@@ -408,56 +173,26 @@ declare function materializeAuthKey(stateDir: string | null, options?: {
408
173
  //#endregion
409
174
  //#region src/auth/auth-sessions.d.ts
410
175
  type AuthSessionStoreOptions = {
411
- /** Where `<stateDir>/auth-sessions.json` lives. */stateDir: string; /** Defaults to a `[workerdeck]` line on stderr. */
412
- warn?: (message: string) => void; /** Test seam; defaults to `Date.now`. */
176
+ stateDir: string;
177
+ warn?: (message: string) => void;
413
178
  now?: () => number;
414
179
  };
415
180
  declare function createAuthSessionStore(options: AuthSessionStoreOptions): Promise<CliSessionStore>;
416
181
  //#endregion
417
182
  //#region src/auth/login-page.d.ts
418
- /**
419
- * The login page is the CLI's, not the dashboard's. That split is deliberate:
420
- * the SPA ships prebuilt and is also served straight from vite in dev, so making
421
- * it aware of an auth scheme that only exists in the turnkey instance would
422
- * couple two things that are otherwise independent. An unauthenticated document
423
- * request gets this instead of index.html; nothing in the SPA changes.
424
- *
425
- * Self-contained by necessity — it renders before any bundled asset is worth
426
- * fetching, and it must not depend on the app it is gating.
427
- */
428
183
  type LoginPageOptions = {
429
- /** Where the form POSTs. Comes from the auth module, not hardcoded here. */action: string; /** Form field name carrying the secret. */
430
- field: string; /** Shown when a previous attempt failed. */
431
- error?: string; /** Where to send the browser after a successful login. */
432
- redirectTo?: string; /** Field name carrying the post-login redirect. */
184
+ action: string;
185
+ field: string;
186
+ error?: string;
187
+ redirectTo?: string;
433
188
  redirectField?: string;
434
189
  };
435
190
  declare function renderLoginPage(options: LoginPageOptions): string;
436
191
  //#endregion
437
192
  //#region src/apns/devices.d.ts
438
- /**
439
- * The device-token registry, and the route that fills it.
440
- *
441
- * This has to live *somewhere*, and the point of the webhooks-first decision is
442
- * that it is not `packages/server`: the OSS gateway stays credential-free and
443
- * knows nothing about APNs. So the turnkey CLI mounts its own route through the
444
- * server's `fallback` hook — the same seam that serves the dashboard — and
445
- * registration lands on the gateway's own origin, behind the auth key that
446
- * already guards everything else.
447
- *
448
- * Storage is a JSON file under the state dir with the same 0600 posture as
449
- * `auth-key`. Device tokens are not secrets in the way an auth key is, but they
450
- * are a list of which phones belong to the operator, and there is no reason for
451
- * every user on the machine to read it.
452
- */
453
193
  type DeviceRecord = {
454
- /** Hex APNs device token. */token: string;
194
+ token: string;
455
195
  environment: ApnsEnvironment;
456
- /**
457
- * Opaque to us: the client's own id for this gateway, echoed back in every
458
- * payload. It is what lets an app configured with two gateways tell which one
459
- * woke it — we store the string and never interpret it.
460
- */
461
196
  hostId?: string;
462
197
  bundleId?: string;
463
198
  platform?: string;
@@ -468,11 +203,6 @@ type DeviceRegistry = {
468
203
  register(record: Omit<DeviceRecord, 'updatedAt'>): Promise<void>;
469
204
  remove(token: string): Promise<void>;
470
205
  };
471
- /**
472
- * `dir` null keeps the registry in memory: a restart then forgets every token,
473
- * which is survivable because the app re-registers on launch, but it does mean
474
- * an instance with no state dir goes quiet until each phone is next opened.
475
- */
476
206
  declare function createDeviceRegistry(options: {
477
207
  dir: string | null;
478
208
  onError?: (error: unknown, context: {
@@ -480,40 +210,19 @@ declare function createDeviceRegistry(options: {
480
210
  path: string;
481
211
  }) => void;
482
212
  }): Promise<DeviceRegistry>;
483
- /**
484
- * `POST /apns/devices` to register a token, `DELETE /apns/devices` to drop one.
485
- * Returns true when it consumed the request.
486
- *
487
- * Deliberately outside `/v1`: this is the forwarder's own surface, not part of
488
- * the protocol `packages/protocol` defines, and a client that finds a 404 here
489
- * has simply reached a gateway running without push configured.
490
- *
491
- * DELETE exists so removing a gateway from the app can stop its pushes. Without
492
- * it a forgotten server keeps buzzing a phone that no longer has any way to act
493
- * on what it says.
494
- */
495
213
  declare function createDeviceRoute(registry: DeviceRegistry, authenticate: (req: IncomingMessage) => unknown): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
496
214
  //#endregion
497
215
  //#region src/apns/forwarder.d.ts
498
216
  type ApnsForwarder = {
499
- /** Hand to `createWorkerServer({ notifications: { onNotification } })`. */onNotification: (notification: SessionNotification) => void; /** Mount ahead of the static host; true when it consumed the request. */
500
- handleRequest: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>; /** How many devices are registered, for the startup banner. */
217
+ onNotification: (notification: SessionNotification) => void;
218
+ handleRequest: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
501
219
  deviceCount: () => number;
502
220
  close: () => void;
503
221
  };
504
- /**
505
- * Build the push for one notification.
506
- *
507
- * The payload carries routing and nothing else — `sessionId` to deep-link,
508
- * `requestId` because a lock-screen Approve has nothing to POST to without it,
509
- * and `hostId` so a client with two gateways knows which one this came from.
510
- * Everything else the app fetches over REST the moment it opens; a transcript
511
- * has no business in a 4 KB envelope.
512
- */
513
222
  declare function buildPush(notification: SessionNotification, hostId: string | undefined): Omit<ApnsRequest, 'deviceToken' | 'environment'>;
514
223
  declare function createApnsForwarder(options: {
515
- config: ApnsConfig; /** Where the device registry is persisted; null keeps it in memory. */
516
- stateDir: string | null; /** Guards `/apns/devices` — the instance's own `authenticate`. */
224
+ config: ApnsConfig;
225
+ stateDir: string | null;
517
226
  authenticate: (req: IncomingMessage) => unknown;
518
227
  warn?: (message: string) => void;
519
228
  }): Promise<ApnsForwarder>;
package/build/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { n as runGuard } from "./guard-DkovUj7o.mjs";
2
- import { _ as createApnsForwarder, a as ConfigError, b as createApnsClient, c as isLoopback, d as parseArgs, f as resolveInstanceConfig, g as buildPush, h as materializeAuthKey, i as renderLoginPage, l as isLoopbackHostname, m as createAuthSessionStore, n as resolveWebRoot, o as defaultStateDir, p as createCliAuth, r as startInstance, s as hostnameOf, t as createHostGuard, u as loadConfigFile, v as createDeviceRegistry, x as loadApnsKey, y as createDeviceRoute } from "./instance-iBl_fOW2.mjs";
1
+ import { n as runGuard } from "./guard-BJNUnO3H.mjs";
2
+ import { _ as createApnsForwarder, a as ConfigError, b as createApnsClient, c as isLoopback, d as parseArgs, f as resolveInstanceConfig, g as buildPush, h as materializeAuthKey, i as renderLoginPage, l as isLoopbackHostname, m as createAuthSessionStore, n as resolveWebRoot, o as defaultStateDir, p as createCliAuth, r as startInstance, s as hostnameOf, t as createHostGuard, u as loadConfigFile, v as createDeviceRegistry, x as loadApnsKey, y as createDeviceRoute } from "./instance-5sdv2Rrw.mjs";
3
3
  export { ConfigError, buildPush, createApnsClient, createApnsForwarder, createAuthSessionStore, createCliAuth, createDeviceRegistry, createDeviceRoute, createHostGuard, defaultStateDir, hostnameOf, isLoopback, isLoopbackHostname, loadApnsKey, loadConfigFile, materializeAuthKey, parseArgs, renderLoginPage, resolveInstanceConfig, resolveWebRoot, runGuard, startInstance };