workerdeck 0.23.0 → 1.1.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/cli.d.mts +1 -1
- package/build/cli.mjs +20 -6
- package/build/cli.mjs.map +1 -1
- package/build/{guard-DkovUj7o.mjs → guard-CGPVzJ3Q.mjs} +2 -27
- package/build/guard-CGPVzJ3Q.mjs.map +1 -0
- package/build/index.d.mts +36 -327
- package/build/index.mjs +2 -2
- package/build/{instance-iBl_fOW2.mjs → instance-CPSmCpPb.mjs} +98 -399
- package/build/instance-CPSmCpPb.mjs.map +1 -0
- package/package.json +13 -13
- package/build/guard-DkovUj7o.mjs.map +0 -1
- package/build/instance-iBl_fOW2.mjs.map +0 -1
package/build/index.d.mts
CHANGED
|
@@ -2,41 +2,21 @@ import { Authenticator, WorkerServer, WorkerServerOptions } from "@workerdeck/se
|
|
|
2
2
|
import { KeyObject } from "node:crypto";
|
|
3
3
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
4
4
|
import { ProfileInfo, SessionNotification } from "@workerdeck/protocol";
|
|
5
|
-
|
|
6
5
|
//#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
6
|
type ApnsEnvironment = 'development' | 'production';
|
|
20
7
|
type ApnsConfig = {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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`). */
|
|
8
|
+
keyFile: string;
|
|
9
|
+
keyId: string;
|
|
10
|
+
teamId: string;
|
|
26
11
|
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
12
|
production?: boolean;
|
|
31
13
|
};
|
|
32
14
|
type ApnsRequest = {
|
|
33
15
|
deviceToken: string;
|
|
34
16
|
environment: ApnsEnvironment;
|
|
35
|
-
payload: unknown;
|
|
36
|
-
priority?: 5 | 10;
|
|
17
|
+
payload: unknown;
|
|
18
|
+
priority?: 5 | 10;
|
|
37
19
|
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
20
|
collapseId?: string;
|
|
41
21
|
};
|
|
42
22
|
type ApnsResult = {
|
|
@@ -46,152 +26,49 @@ type ApnsResult = {
|
|
|
46
26
|
ok: false;
|
|
47
27
|
status: number;
|
|
48
28
|
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
29
|
unregistered: boolean;
|
|
52
30
|
};
|
|
53
31
|
type ApnsClient = {
|
|
54
32
|
send(request: ApnsRequest): Promise<ApnsResult>;
|
|
55
33
|
close(): void;
|
|
56
34
|
};
|
|
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
35
|
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?: {
|
|
36
|
+
declare function createApnsClient(config: ApnsConfig, key: KeyObject, options?: {
|
|
70
37
|
hosts?: Record<ApnsEnvironment, string>;
|
|
71
38
|
retryDelayMs?: number;
|
|
72
39
|
}): ApnsClient;
|
|
73
40
|
//#endregion
|
|
74
41
|
//#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
42
|
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
43
|
secret?: string;
|
|
115
|
-
/** Default 'workerdeck_session'. No `__Host-` prefix — it requires
|
|
116
|
-
* `Secure`, and plain-HTTP localhost is the primary deployment. */
|
|
117
44
|
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
45
|
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
46
|
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
47
|
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
48
|
throttle?: {
|
|
147
49
|
windowMs?: number;
|
|
148
50
|
maxFailuresPerIp?: number;
|
|
149
51
|
maxFailuresGlobal?: number;
|
|
150
52
|
};
|
|
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
53
|
sessions?: CliSessionStore;
|
|
157
54
|
};
|
|
158
|
-
/** One session-table row as it is handed to a store; the key is opaque here. */
|
|
159
55
|
type StoredSession = {
|
|
160
56
|
expiresAt: number;
|
|
161
57
|
};
|
|
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
58
|
type CliSessionStore = {
|
|
169
|
-
|
|
170
|
-
save(entries: [string, StoredSession][]): void;
|
|
59
|
+
initial?: Iterable<[string, StoredSession]>;
|
|
60
|
+
save(entries: [string, StoredSession][]): void;
|
|
171
61
|
flush?(): Promise<void>;
|
|
172
62
|
};
|
|
173
|
-
/** What `authenticate` hands the worker server as the request principal. */
|
|
174
63
|
type CliPrincipal = {
|
|
175
|
-
|
|
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`). */
|
|
64
|
+
via: 'header' | 'cookie' | 'open';
|
|
179
65
|
canManageProfiles: true;
|
|
180
66
|
};
|
|
181
67
|
type CliAuth = {
|
|
182
68
|
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
69
|
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
70
|
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
71
|
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
72
|
loginPage(req: IncomingMessage): {
|
|
196
73
|
action: string;
|
|
197
74
|
field: string;
|
|
@@ -201,63 +78,16 @@ type CliAuth = {
|
|
|
201
78
|
declare function createCliAuth(options?: CliAuthOptions): CliAuth;
|
|
202
79
|
//#endregion
|
|
203
80
|
//#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
81
|
type WorkerDeckConfig = WorkerServerOptions & {
|
|
211
82
|
port?: number;
|
|
212
|
-
host?: string;
|
|
213
|
-
auth?: CliAuthOptions;
|
|
83
|
+
host?: string;
|
|
84
|
+
auth?: CliAuthOptions;
|
|
214
85
|
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
86
|
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. */
|
|
87
|
+
insecureHosts?: string[];
|
|
231
88
|
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
89
|
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
90
|
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
91
|
apns?: ApnsConfig;
|
|
262
92
|
};
|
|
263
93
|
type CliFlags = {
|
|
@@ -283,67 +113,32 @@ type CliFlags = {
|
|
|
283
113
|
version?: boolean;
|
|
284
114
|
};
|
|
285
115
|
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
116
|
declare function parseArgs(argv: string[]): CliFlags;
|
|
292
117
|
type LoadedConfig = {
|
|
293
|
-
|
|
118
|
+
path: string | null;
|
|
294
119
|
options: WorkerDeckConfig;
|
|
295
120
|
};
|
|
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
121
|
declare function loadConfigFile(explicit?: string, cwd?: string): Promise<LoadedConfig>;
|
|
303
122
|
declare function isLoopback(host: string): boolean;
|
|
304
123
|
type ResolvedConfig = {
|
|
305
124
|
port: number;
|
|
306
|
-
host: string;
|
|
307
|
-
authKey?: string;
|
|
308
|
-
auth: CliAuthOptions;
|
|
125
|
+
host: string;
|
|
126
|
+
authKey?: string;
|
|
127
|
+
auth: CliAuthOptions;
|
|
309
128
|
stateDir: string | null;
|
|
310
|
-
configPath: string | null;
|
|
129
|
+
configPath: string | null;
|
|
311
130
|
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
131
|
generateAuthKey: boolean;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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. */
|
|
132
|
+
allowedHosts: Set<string> | null;
|
|
133
|
+
webRoot?: string;
|
|
134
|
+
web: boolean;
|
|
328
135
|
corsOrigins: string[];
|
|
329
|
-
/** APNs forwarder settings with `keyFile` made absolute, or undefined for an
|
|
330
|
-
* instance that does not push. */
|
|
331
136
|
apns?: ApnsConfig;
|
|
332
137
|
open: boolean;
|
|
333
138
|
options: WorkerServerOptions;
|
|
334
139
|
};
|
|
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
140
|
declare function defaultStateDir(configPath: string | null): string;
|
|
344
|
-
/** Hostname out of a Host header, minus the port and any IPv6 brackets. */
|
|
345
141
|
declare function hostnameOf(hostHeader: string): string;
|
|
346
|
-
/** 127.0.0.0/8, ::1, and the names that mean them. */
|
|
347
142
|
declare function isLoopbackHostname(hostname: string): boolean;
|
|
348
143
|
declare function resolveInstanceConfig(flags: CliFlags, loaded: LoadedConfig, env?: NodeJS.ProcessEnv, cwd?: string): ResolvedConfig;
|
|
349
144
|
//#endregion
|
|
@@ -351,28 +146,13 @@ declare function resolveInstanceConfig(flags: CliFlags, loaded: LoadedConfig, en
|
|
|
351
146
|
type Instance = {
|
|
352
147
|
server: WorkerServer;
|
|
353
148
|
url: string;
|
|
354
|
-
port: number;
|
|
149
|
+
port: number;
|
|
355
150
|
closed: Promise<void>;
|
|
151
|
+
/** 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;
|
|
402
|
-
source: 'stored' | 'created' | '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
|
-
|
|
412
|
-
warn?: (message: string) => void;
|
|
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
|
-
|
|
430
|
-
field: string;
|
|
431
|
-
error?: string;
|
|
432
|
-
redirectTo?: string;
|
|
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
|
-
|
|
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
|
-
|
|
500
|
-
handleRequest: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
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;
|
|
516
|
-
stateDir: string | null;
|
|
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-
|
|
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-
|
|
1
|
+
import { n as runGuard } from "./guard-CGPVzJ3Q.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-CPSmCpPb.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 };
|