workerdeck 0.0.0 → 0.7.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,453 @@
1
+ import { Authenticator, WorkerServer, WorkerServerOptions } from "@workerdeck/server";
2
+ import { KeyObject } from "node:crypto";
3
+ import { IncomingMessage, ServerResponse } from "node:http";
4
+ import { ProfileInfo, SessionNotification } from "@workerdeck/protocol";
5
+
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
+ type ApnsEnvironment = 'development' | 'production';
20
+ 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`). */
26
+ 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
+ production?: boolean;
31
+ };
32
+ type ApnsRequest = {
33
+ deviceToken: string;
34
+ 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". */
37
+ 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
+ collapseId?: string;
41
+ };
42
+ type ApnsResult = {
43
+ ok: true;
44
+ apnsId?: string;
45
+ } | {
46
+ ok: false;
47
+ status: number;
48
+ 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
+ unregistered: boolean;
52
+ };
53
+ type ApnsClient = {
54
+ send(request: ApnsRequest): Promise<ApnsResult>;
55
+ close(): void;
56
+ };
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
+ 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. Nothing in
65
+ * production should pass this — the real endpoints are not configurable, and
66
+ * an operator who could redirect them could exfiltrate every push. */
67
+
68
+ options?: {
69
+ hosts?: Record<ApnsEnvironment, string>;
70
+ }): ApnsClient;
71
+ //#endregion
72
+ //#region src/auth.d.ts
73
+ /**
74
+ * Gateway auth for the turnkey CLI: one shared operator secret, two transports.
75
+ *
76
+ * Services present the secret itself on every request (`x-workerdeck-key`,
77
+ * or `Authorization: Bearer`). The dashboard cannot: the SPA is prebuilt to
78
+ * call `location.origin + '/v1'` with no headers, and a browser WebSocket
79
+ * handshake carries no custom headers at all. So browsers POST the secret once
80
+ * to `/auth/login`, get an HttpOnly cookie naming a server-side session, and
81
+ * the cookie rides same-origin REST and the WS upgrade automatically. That
82
+ * automatic ride is also the threat: the cookie is ambient authority, and the
83
+ * WS handshake is exempt from CORS, so cross-origin misuse is fenced off by an
84
+ * explicit Origin check here — not by the browser.
85
+ *
86
+ * This file guards the operator's own gateway and nothing else. It never sees
87
+ * an Anthropic credential — those are resolved by the SDK/CLI from the
88
+ * operator's environment (root CLAUDE.md, auth red lines).
89
+ */
90
+ type CliAuthOptions = {
91
+ /**
92
+ * The shared operator secret. Unset disables auth entirely (the CLI then
93
+ * refuses to bind anything but loopback — enforced by the caller, not here).
94
+ * An empty or short value is a config accident, not a choice: anything under
95
+ * 12 characters throws rather than standing up a guessable gateway.
96
+ */
97
+ secret?: string;
98
+ /** Default 'workerdeck_session'. No `__Host-` prefix — it requires
99
+ * `Secure`, and plain-HTTP localhost is the primary deployment. */
100
+ cookieName?: string;
101
+ /**
102
+ * Browser session lifetime, default 7 days. Fixed, not sliding: the auth
103
+ * hooks only see the request, so a renewed cookie has nowhere to ride — and
104
+ * for a dashboard whose whole login is retyping one secret, a periodic
105
+ * re-login is cheaper than a refresh endpoint. Expiry (or a process restart —
106
+ * sessions are in-memory by design) simply lands the operator back on the
107
+ * login page.
108
+ */
109
+ ttlMs?: number;
110
+ /**
111
+ * Trust `x-forwarded-proto` / `x-forwarded-host` / `x-forwarded-for` from
112
+ * exactly one reverse proxy in front of this process; the *last* value of
113
+ * each is used (the one the proxy set, the only position a client cannot
114
+ * forge). Off by default: these are attacker-writable headers on a directly
115
+ * exposed port. Behind a TLS-terminating proxy this must be on, or the
116
+ * `Secure` cookie flag is skipped and the Origin check computes `http://`
117
+ * where the browser says `https://` and rejects the dashboard's own writes.
118
+ */
119
+ trustProxy?: boolean;
120
+ /**
121
+ * Origins accepted in addition to the request's own (scheme + Host). Needed
122
+ * when the proxy rewrites Host so the external origin no longer matches what
123
+ * this process sees. Entries must be full origins ('https://ops.example.com');
124
+ * invalid ones throw at startup rather than silently never matching.
125
+ */
126
+ allowedOrigins?: string[];
127
+ /** Login throttle tuning; defaults: 15 min window, 10 failures per IP, 100
128
+ * globally. Exposed mainly so tests need not wait out real windows. */
129
+ throttle?: {
130
+ windowMs?: number;
131
+ maxFailuresPerIp?: number;
132
+ maxFailuresGlobal?: number;
133
+ };
134
+ };
135
+ /** What `authenticate` hands the worker server as the request principal. */
136
+ type CliPrincipal = {
137
+ /** Which transport authenticated the request; 'open' when auth is disabled. */via: 'header' | 'cookie' | 'open';
138
+ /** One secret, one trust level: whoever holds it is the operator, so the
139
+ * dashboard may manage provider profiles (still bounded by the server's
140
+ * `allowedConfigDirRoots`). */
141
+ canManageProfiles: true;
142
+ };
143
+ type CliAuth = {
144
+ enabled: boolean;
145
+ /** Hand straight to `createWorkerServer({ authenticate })` — it guards both
146
+ * REST and the WS upgrade, which is exactly why the Origin policy lives in it. */
147
+ authenticate: Authenticator;
148
+ /** Claims `/auth` and everything under it (login/logout/status); returns true
149
+ * when it consumed the request. The static host must call this first. */
150
+ handleAuthRequest(req: IncomingMessage, res: ServerResponse): boolean | Promise<boolean>;
151
+ /** Cookie-only check for the static host: login page or SPA? Gating the SPA
152
+ * shell is UX, not security — every byte of data sits behind `authenticate`. */
153
+ hasValidSession(req: IncomingMessage): boolean;
154
+ /** What the login page needs to render for this request. The endpoint path,
155
+ * field name, and the `?auth=` redirect params are all this module's wire
156
+ * format, so the page learns them here instead of hardcoding them. */
157
+ loginPage(req: IncomingMessage): {
158
+ action: string;
159
+ field: string;
160
+ error?: string;
161
+ };
162
+ };
163
+ declare function createCliAuth(options?: CliAuthOptions): CliAuth;
164
+ //#endregion
165
+ //#region src/config.d.ts
166
+ /**
167
+ * What a `workerdeck.config.mjs` default-exports: the server options, plus
168
+ * the few instance-level settings that aren't the server's business. Keeping
169
+ * them in one object means a deployment is one file, not a file plus a
170
+ * memorised command line.
171
+ */
172
+ type WorkerDeckConfig = WorkerServerOptions & {
173
+ port?: number;
174
+ host?: string; /** Built-in shared-secret auth. Ignored entirely if you supply `authenticate`. */
175
+ auth?: CliAuthOptions; /** Where parked sessions are persisted; null disables durable parking. */
176
+ stateDir?: string | null;
177
+ /**
178
+ * Host header values accepted when running *without* auth. Defaults to the
179
+ * loopback names; see `resolveInstanceConfig` for why this exists at all.
180
+ */
181
+ allowedHosts?: string[];
182
+ /**
183
+ * Bind hosts that may serve without auth. One declaration, two effects:
184
+ * binding a listed host waives the auth requirement (no key demanded, none
185
+ * generated), and while unauthenticated every entry is also accepted as a
186
+ * Host header, so the operator states the intent once. Entries name a host,
187
+ * never an endpoint — a port is rejected — and match the bind host literally
188
+ * and case-insensitively: nothing is inferred from DNS or the network, and
189
+ * `0.0.0.0` means the all-interfaces bind itself, not "any host". When auth
190
+ * is on this widens nothing.
191
+ */
192
+ insecureHosts?: string[]; /** Serve a dashboard build from here instead of the bundled one. */
193
+ webRoot?: string;
194
+ /**
195
+ * Forward session notifications to Apple Push Notification service, for the
196
+ * iOS app. Absent turns the forwarder off entirely — including its
197
+ * `/apns/devices` route, so a gateway without this answers 404 there and the
198
+ * app quietly stops asking.
199
+ *
200
+ * Lives here rather than in `packages/server` on purpose: this is the only
201
+ * place in the project that holds a push credential, and the OSS gateway
202
+ * stays credential-free. `keyFile` is a path, never key contents.
203
+ */
204
+ apns?: ApnsConfig;
205
+ };
206
+ type CliFlags = {
207
+ config?: string;
208
+ port?: number;
209
+ host?: string;
210
+ authKey?: string;
211
+ profiles: ProfileInfo[];
212
+ cwdRoots: string[];
213
+ fsRoots: string[];
214
+ fsWrite?: boolean;
215
+ allowedOrigins: string[];
216
+ allowedHosts: string[];
217
+ insecureHosts: string[];
218
+ trustProxy?: boolean;
219
+ stateDir?: string;
220
+ parking?: boolean;
221
+ insecure?: boolean;
222
+ open?: boolean;
223
+ help?: boolean;
224
+ version?: boolean;
225
+ };
226
+ declare class ConfigError extends Error {}
227
+ /**
228
+ * Hand-rolled rather than a dependency: the CLI's whole value is that `npx
229
+ * workerdeck` pulls down a small tree, and an arg parser is a hundred lines
230
+ * of it.
231
+ */
232
+ declare function parseArgs(argv: string[]): CliFlags;
233
+ type LoadedConfig = {
234
+ /** Absolute path of the file that was loaded, or null if there wasn't one. */path: string | null;
235
+ options: WorkerDeckConfig;
236
+ };
237
+ /**
238
+ * Explicit `--config` must exist — a typo that silently starts a default
239
+ * instance is worse than a failure. An implicit one is looked up in cwd only:
240
+ * walking parent directories would make what a given command does depend on
241
+ * where it was run from.
242
+ */
243
+ declare function loadConfigFile(explicit?: string, cwd?: string): Promise<LoadedConfig>;
244
+ declare function isLoopback(host: string): boolean;
245
+ type ResolvedConfig = {
246
+ port: number;
247
+ host: string; /** Shared secret, or undefined for an unauthenticated instance. */
248
+ authKey?: string; /** Everything else the built-in auth takes (proxy trust, extra origins). */
249
+ auth: CliAuthOptions; /** Where parked sessions and other instance state live; null disables durable parking. */
250
+ stateDir: string | null;
251
+ configPath: string | null; /** True when the config file supplied its own `authenticate` — built-in auth stands down. */
252
+ hostAuthenticates: boolean;
253
+ /**
254
+ * Auth is required here but no key was supplied: `startInstance` must
255
+ * materialize one (stored under `stateDir`, ephemeral without one). This is a
256
+ * *promise* rather than a key because resolution is pure and synchronous while
257
+ * reading a key file is I/O — and the promise is load-bearing: `allowedHosts`
258
+ * is already null on the strength of it, so `startInstance` refuses to serve
259
+ * if materialization ever fails to arm the built-in auth.
260
+ */
261
+ generateAuthKey: boolean;
262
+ /**
263
+ * Host header values to accept, or null to accept any. Non-null only for an
264
+ * unauthenticated instance — see `resolveInstanceConfig`.
265
+ */
266
+ allowedHosts: Set<string> | null; /** Dashboard build to serve; resolved from the package when unset. */
267
+ webRoot?: string;
268
+ /** APNs forwarder settings with `keyFile` made absolute, or undefined for an
269
+ * instance that does not push. */
270
+ apns?: ApnsConfig;
271
+ open: boolean;
272
+ options: WorkerServerOptions;
273
+ };
274
+ /**
275
+ * Durable parking is on by default because this is a long-lived instance: a
276
+ * turnkey tool that silently drops parked work on every restart is the wrong
277
+ * default. The store writes whole transcripts in plaintext, so it goes beside
278
+ * the config file (or under the home directory) rather than anywhere temporary,
279
+ * and one directory serves exactly one instance — the store is single-process
280
+ * by design, which the single-port model already implies.
281
+ */
282
+ declare function defaultStateDir(configPath: string | null): string;
283
+ /** Hostname out of a Host header, minus the port and any IPv6 brackets. */
284
+ declare function hostnameOf(hostHeader: string): string;
285
+ /** 127.0.0.0/8, ::1, and the names that mean them. */
286
+ declare function isLoopbackHostname(hostname: string): boolean;
287
+ declare function resolveInstanceConfig(flags: CliFlags, loaded: LoadedConfig, env?: NodeJS.ProcessEnv, cwd?: string): ResolvedConfig;
288
+ //#endregion
289
+ //#region src/instance.d.ts
290
+ type Instance = {
291
+ server: WorkerServer;
292
+ url: string;
293
+ port: number; /** Resolves when the instance stops serving. */
294
+ closed: Promise<void>;
295
+ close: () => Promise<void>;
296
+ };
297
+ /**
298
+ * The dashboard comes from `@workerdeck/web`, which ships it prebuilt and
299
+ * exports the path to it. Depending on the package rather than vendoring a copy
300
+ * means one dashboard, versioned in lockstep with everything else.
301
+ *
302
+ * In a checkout that directory only exists once the app has been built — dev
303
+ * never builds — so the miss is worth a real message rather than a stack trace
304
+ * from the static host.
305
+ */
306
+ declare function resolveWebRoot(): string;
307
+ /**
308
+ * The Host-header gate for an unauthenticated instance. `allowedHosts` is null
309
+ * whenever auth is on, and then this is the identity function — with a
310
+ * credential in play a rebound origin holds no cookie and fails `authenticate`
311
+ * anyway. Loopback *names* are what's checked, not the socket: the attacker in
312
+ * this scenario controls DNS, so the connection genuinely arrives on 127.0.0.1;
313
+ * what they cannot control is the name the victim's browser writes into Host.
314
+ */
315
+ declare function createHostGuard(allowedHosts: Set<string> | null): (req: IncomingMessage) => boolean;
316
+ declare function startInstance(config: ResolvedConfig, options?: {
317
+ quiet?: boolean;
318
+ }): Promise<Instance>;
319
+ //#endregion
320
+ //#region src/guard.d.ts
321
+ declare function runGuard(argv: string[]): Promise<number>;
322
+ //#endregion
323
+ //#region src/auth-key.d.ts
324
+ /**
325
+ * Materializes the key that `resolveInstanceConfig` promised via
326
+ * `generateAuthKey`: resolution is pure and synchronous, reading a key file is
327
+ * I/O, so the two halves meet here at startup. The contract is that this
328
+ * function returns a usable secret or throws — it never returns "no key",
329
+ * because the resolved config has already stood down the Host-header guard on
330
+ * the strength of the promise, and a silent miss would serve an open gateway
331
+ * that reports itself authenticated.
332
+ *
333
+ * The key persists under `stateDir` so a restart does not un-pair every client
334
+ * that stored it (the iOS app keeps it in its keychain). No `stateDir` means
335
+ * nothing durable to write, so the key is ephemeral per run — the banner says
336
+ * so. This is the gateway's own operator secret and nothing else: Anthropic
337
+ * credentials never pass through here (root CLAUDE.md, auth red lines).
338
+ */
339
+ type MaterializedAuthKey = {
340
+ key: string; /** 'stored' reused the file, 'created' wrote a new one, 'ephemeral' had nowhere to write. */
341
+ source: 'stored' | 'created' | 'ephemeral'; /** Where the key lives, or null when ephemeral. */
342
+ path: string | null;
343
+ };
344
+ declare function materializeAuthKey(stateDir: string | null, options?: {
345
+ warn?: (message: string) => void;
346
+ }): Promise<MaterializedAuthKey>;
347
+ //#endregion
348
+ //#region src/login-page.d.ts
349
+ /**
350
+ * The login page is the CLI's, not the dashboard's. That split is deliberate:
351
+ * the SPA ships prebuilt and is also served straight from vite in dev, so making
352
+ * it aware of an auth scheme that only exists in the turnkey instance would
353
+ * couple two things that are otherwise independent. An unauthenticated document
354
+ * request gets this instead of index.html; nothing in the SPA changes.
355
+ *
356
+ * Self-contained by necessity — it renders before any bundled asset is worth
357
+ * fetching, and it must not depend on the app it is gating.
358
+ */
359
+ type LoginPageOptions = {
360
+ /** Where the form POSTs. Comes from the auth module, not hardcoded here. */action: string; /** Form field name carrying the secret. */
361
+ field: string; /** Shown when a previous attempt failed. */
362
+ error?: string; /** Where to send the browser after a successful login. */
363
+ redirectTo?: string; /** Field name carrying the post-login redirect. */
364
+ redirectField?: string;
365
+ };
366
+ declare function renderLoginPage(options: LoginPageOptions): string;
367
+ //#endregion
368
+ //#region src/apns/devices.d.ts
369
+ /**
370
+ * The device-token registry, and the route that fills it.
371
+ *
372
+ * This has to live *somewhere*, and the point of the webhooks-first decision is
373
+ * that it is not `packages/server`: the OSS gateway stays credential-free and
374
+ * knows nothing about APNs. So the turnkey CLI mounts its own route through the
375
+ * server's `fallback` hook — the same seam that serves the dashboard — and
376
+ * registration lands on the gateway's own origin, behind the auth key that
377
+ * already guards everything else.
378
+ *
379
+ * Storage is a JSON file under the state dir with the same 0600 posture as
380
+ * `auth-key`. Device tokens are not secrets in the way an auth key is, but they
381
+ * are a list of which phones belong to the operator, and there is no reason for
382
+ * every user on the machine to read it.
383
+ */
384
+ type DeviceRecord = {
385
+ /** Hex APNs device token. */token: string;
386
+ environment: ApnsEnvironment;
387
+ /**
388
+ * Opaque to us: the client's own id for this gateway, echoed back in every
389
+ * payload. It is what lets an app configured with two gateways tell which one
390
+ * woke it — we store the string and never interpret it.
391
+ */
392
+ hostId?: string;
393
+ bundleId?: string;
394
+ platform?: string;
395
+ updatedAt: number;
396
+ };
397
+ type DeviceRegistry = {
398
+ list(): DeviceRecord[];
399
+ register(record: Omit<DeviceRecord, 'updatedAt'>): Promise<void>;
400
+ remove(token: string): Promise<void>;
401
+ };
402
+ /**
403
+ * `dir` null keeps the registry in memory: a restart then forgets every token,
404
+ * which is survivable because the app re-registers on launch, but it does mean
405
+ * an instance with no state dir goes quiet until each phone is next opened.
406
+ */
407
+ declare function createDeviceRegistry(options: {
408
+ dir: string | null;
409
+ onError?: (error: unknown, context: {
410
+ op: string;
411
+ path: string;
412
+ }) => void;
413
+ }): Promise<DeviceRegistry>;
414
+ /**
415
+ * `POST /apns/devices` to register a token, `DELETE /apns/devices` to drop one.
416
+ * Returns true when it consumed the request.
417
+ *
418
+ * Deliberately outside `/v1`: this is the forwarder's own surface, not part of
419
+ * the protocol `packages/protocol` defines, and a client that finds a 404 here
420
+ * has simply reached a gateway running without push configured.
421
+ *
422
+ * DELETE exists so removing a gateway from the app can stop its pushes. Without
423
+ * it a forgotten server keeps buzzing a phone that no longer has any way to act
424
+ * on what it says.
425
+ */
426
+ declare function createDeviceRoute(registry: DeviceRegistry, authenticate: (req: IncomingMessage) => unknown): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
427
+ //#endregion
428
+ //#region src/apns/forwarder.d.ts
429
+ type ApnsForwarder = {
430
+ /** Hand to `createWorkerServer({ notifications: { onNotification } })`. */onNotification: (notification: SessionNotification) => void; /** Mount ahead of the static host; true when it consumed the request. */
431
+ handleRequest: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>; /** How many devices are registered, for the startup banner. */
432
+ deviceCount: () => number;
433
+ close: () => void;
434
+ };
435
+ /**
436
+ * Build the push for one notification.
437
+ *
438
+ * The payload carries routing and nothing else — `sessionId` to deep-link,
439
+ * `requestId` because a lock-screen Approve has nothing to POST to without it,
440
+ * and `hostId` so a client with two gateways knows which one this came from.
441
+ * Everything else the app fetches over REST the moment it opens; a transcript
442
+ * has no business in a 4 KB envelope.
443
+ */
444
+ declare function buildPush(notification: SessionNotification, hostId: string | undefined): Omit<ApnsRequest, 'deviceToken' | 'environment'>;
445
+ declare function createApnsForwarder(options: {
446
+ config: ApnsConfig; /** Where the device registry is persisted; null keeps it in memory. */
447
+ stateDir: string | null; /** Guards `/apns/devices` — the instance's own `authenticate`. */
448
+ authenticate: (req: IncomingMessage) => unknown;
449
+ warn?: (message: string) => void;
450
+ }): Promise<ApnsForwarder>;
451
+ //#endregion
452
+ export { type ApnsClient, type ApnsConfig, type ApnsEnvironment, type ApnsForwarder, type ApnsRequest, type ApnsResult, type CliAuth, type CliAuthOptions, type CliFlags, type CliPrincipal, ConfigError, type DeviceRecord, type DeviceRegistry, type Instance, type LoadedConfig, type LoginPageOptions, type MaterializedAuthKey, type ResolvedConfig, type WorkerDeckConfig, buildPush, createApnsClient, createApnsForwarder, createCliAuth, createDeviceRegistry, createDeviceRoute, createHostGuard, defaultStateDir, hostnameOf, isLoopback, isLoopbackHostname, loadApnsKey, loadConfigFile, materializeAuthKey, parseArgs, renderLoginPage, resolveInstanceConfig, resolveWebRoot, runGuard, startInstance };
453
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1,3 @@
1
+ import { n as runGuard } from "./guard-D3VN855w.mjs";
2
+ import { _ as createDeviceRegistry, a as ConfigError, b as loadApnsKey, c as isLoopback, d as parseArgs, f as resolveInstanceConfig, g as createApnsForwarder, h as buildPush, i as renderLoginPage, l as isLoopbackHostname, m as materializeAuthKey, n as resolveWebRoot, o as defaultStateDir, p as createCliAuth, r as startInstance, s as hostnameOf, t as createHostGuard, u as loadConfigFile, v as createDeviceRoute, y as createApnsClient } from "./instance-DbIqlX-e.mjs";
3
+ export { ConfigError, buildPush, createApnsClient, createApnsForwarder, createCliAuth, createDeviceRegistry, createDeviceRoute, createHostGuard, defaultStateDir, hostnameOf, isLoopback, isLoopbackHostname, loadApnsKey, loadConfigFile, materializeAuthKey, parseArgs, renderLoginPage, resolveInstanceConfig, resolveWebRoot, runGuard, startInstance };