workerdeck 0.6.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/build/cli.mjs +10 -1
- package/build/cli.mjs.map +1 -1
- package/build/index.d.mts +169 -2
- package/build/index.mjs +2 -2
- package/build/{instance-kupxU5UD.mjs → instance-DbIqlX-e.mjs} +573 -6
- package/build/instance-DbIqlX-e.mjs.map +1 -0
- package/package.json +6 -5
- package/build/instance-kupxU5UD.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -45,6 +45,8 @@ port. `--insecure` overrides that, for when something in front is doing the auth
|
|
|
45
45
|
| `--host <addr>` | `WORKERDECK_HOST` | `127.0.0.1` |
|
|
46
46
|
| `--auth-key <secret>` | `WORKERDECK_AUTH_KEY` | none (no auth) |
|
|
47
47
|
| `--cwd-root <path>` (repeatable) | `WORKERDECK_CWD_ROOTS` (`:`-separated) | unrestricted |
|
|
48
|
+
| `--fs-root <path>` (repeatable) | `WORKERDECK_FS_ROOTS` (`:`-separated) | narrows `/v1/fs`; unset, reading follows `--cwd-root` |
|
|
49
|
+
| `--fs-write` | — | off (browse and read only) |
|
|
48
50
|
| `--profile <name=dir>` (repeatable) | — | auto-detected from `~/.claude` |
|
|
49
51
|
| `--state-dir <path>` | `WORKERDECK_STATE_DIR` | beside the config file, else `~/.workerdeck` |
|
|
50
52
|
| `--no-parking-store` | — | durable parking on |
|
package/build/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as ConfigError, d as parseArgs, f as resolveInstanceConfig, r as startInstance, u as loadConfigFile } from "./instance-
|
|
2
|
+
import { a as ConfigError, d as parseArgs, f as resolveInstanceConfig, r as startInstance, u as loadConfigFile } from "./instance-DbIqlX-e.mjs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -36,6 +36,15 @@ Options
|
|
|
36
36
|
--profile <name=dir> Claude config dir a session may run under (repeatable)
|
|
37
37
|
--cwd-root <path> restrict session cwds to this root (repeatable,
|
|
38
38
|
WORKERDECK_CWD_ROOTS as a ':'-separated list)
|
|
39
|
+
--fs-root <path> narrow which host directories /v1/fs serves
|
|
40
|
+
(repeatable, WORKERDECK_FS_ROOTS as a ':'-separated
|
|
41
|
+
list). Reading otherwise follows --cwd-root: a caller
|
|
42
|
+
who may start a session in a tree can already read it
|
|
43
|
+
through the agent. With neither, the routes 404.
|
|
44
|
+
--fs-write also accept writes over /v1/fs. Its own switch because
|
|
45
|
+
an agent's writes go through the permission flow and a
|
|
46
|
+
PUT does not. Every write is still conditional on the
|
|
47
|
+
hash the client last read.
|
|
39
48
|
--state-dir <path> where parked sessions are persisted
|
|
40
49
|
(default: beside the config file, else ~/.workerdeck)
|
|
41
50
|
--no-parking-store keep parked sessions in memory only; a restart drops them
|
package/build/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { spawn } from 'node:child_process'\nimport { readFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { ConfigError, loadConfigFile, parseArgs, resolveInstanceConfig } from './config.ts'\nimport { startInstance } from './instance.ts'\n\nconst HELP = `workerdeck — run a workerdeck instance: session gateway + dashboard, one port.\n\nUsage\n workerdeck [options]\n workerdeck guard [options] check whether it is safe to restart an instance\n\nOptions\n -p, --port <n> port to listen on (default 8787, WORKERDECK_PORT)\n --host <addr> interface to bind (default 127.0.0.1, WORKERDECK_HOST)\n --auth-key <secret> shared secret, min 12 chars; browsers log in with it,\n services send it as x-workerdeck-key\n (WORKERDECK_AUTH_KEY). Unset = no auth on loopback;\n on any other interface a key is generated instead,\n printed once, and stored in <state-dir>/auth-key for\n later starts to reuse.\n --trust-proxy trust x-forwarded-proto/-host/-for from one reverse\n proxy. Required behind TLS termination, or the session\n cookie loses its Secure flag and the origin check\n computes http:// where the browser says https://.\n --allowed-origin <o> extra origin accepted on browser requests, for when a\n proxy rewrites Host (repeatable)\n --allowed-host <name> extra Host header accepted when running without auth\n (repeatable; loopback names are always accepted)\n --insecure-host <name>\n bind host that may serve without auth — no key demanded,\n none generated — and, while unauthenticated, also\n accepted as a Host header (repeatable; config:\n insecureHosts). Names the host alone, no port.\n --profile <name=dir> Claude config dir a session may run under (repeatable)\n --cwd-root <path> restrict session cwds to this root (repeatable,\n WORKERDECK_CWD_ROOTS as a ':'-separated list)\n --state-dir <path> where parked sessions are persisted\n (default: beside the config file, else ~/.workerdeck)\n --no-parking-store keep parked sessions in memory only; a restart drops them\n -c, --config <path> config file (default: ./workerdeck.config.mjs)\n --insecure allow no-auth on a non-loopback address. Only when\n something in front is doing the authenticating.\n --open open the dashboard in a browser once it is up\n -h, --help show this\n -v, --version print the version\n\nConfig file\n Options that cannot fit on a command line — \\`authenticate\\`, \\`buildRunnerConfig\\`,\n \\`createEngineRunner\\` are functions — live in workerdeck.config.mjs, which\n default-exports the createWorkerServer options (or a function returning them).\n Flags and env override it. Supplying your own \\`authenticate\\` turns the built-in\n shared-secret auth off entirely.\n\nCredentials\n workerdeck implements no Anthropic auth of its own: the official SDK/CLI\n resolves credentials from the environment, per profile. --auth-key protects this\n gateway, nothing else.\n`\n\nasync function readVersion(): Promise<string> {\n // src/cli.ts and build/cli.mjs are both one level under the package root.\n const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')\n try {\n const raw = await readFile(pkgPath, 'utf8')\n return (JSON.parse(raw) as { version?: string }).version ?? 'unknown'\n } catch {\n return 'unknown'\n }\n}\n\n/** Best-effort: a browser that won't open is a convenience missed, not a failure. */\nfunction openInBrowser(url: string): void {\n const command =\n process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'\n try {\n const child = spawn(command, [url], {\n stdio: 'ignore',\n detached: true,\n shell: process.platform === 'win32',\n })\n child.on('error', () => {})\n child.unref()\n } catch {\n // ignored\n }\n}\n\nasync function main(argv: string[]): Promise<number> {\n if (argv[0] === 'guard') {\n const { runGuard } = await import('./guard.ts')\n return await runGuard(argv.slice(1))\n }\n\n const flags = parseArgs(argv)\n if (flags.help) {\n process.stdout.write(HELP)\n return 0\n }\n if (flags.version) {\n process.stdout.write(`${await readVersion()}\\n`)\n return 0\n }\n\n const loaded = await loadConfigFile(flags.config)\n const config = resolveInstanceConfig(flags, loaded)\n const instance = await startInstance(config)\n\n if (config.open) openInBrowser(instance.url)\n\n const shutdown = (signal: string): void => {\n process.stdout.write(`\\n[workerdeck] ${signal} — shutting down\\n`)\n // Parked sessions are already on disk; this is about letting in-flight\n // requests finish rather than dropping sockets on the floor.\n instance\n .close()\n .then(() => process.exit(0))\n .catch(() => process.exit(1))\n }\n process.on('SIGINT', () => shutdown('SIGINT'))\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n\n // Resolves only on close; the process stays up serving.\n await instance.closed\n return 0\n}\n\nmain(process.argv.slice(2))\n .then((code) => {\n if (code !== 0) process.exit(code)\n })\n .catch((error: unknown) => {\n if (error instanceof ConfigError) {\n process.stderr.write(`[workerdeck] ${error.message}\\n`)\n process.exit(2)\n }\n process.stderr.write(\n `[workerdeck] ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`,\n )\n process.exit(1)\n })\n"],"mappings":";;;;;;;AAQA,MAAM,OAAO
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { spawn } from 'node:child_process'\nimport { readFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { ConfigError, loadConfigFile, parseArgs, resolveInstanceConfig } from './config.ts'\nimport { startInstance } from './instance.ts'\n\nconst HELP = `workerdeck — run a workerdeck instance: session gateway + dashboard, one port.\n\nUsage\n workerdeck [options]\n workerdeck guard [options] check whether it is safe to restart an instance\n\nOptions\n -p, --port <n> port to listen on (default 8787, WORKERDECK_PORT)\n --host <addr> interface to bind (default 127.0.0.1, WORKERDECK_HOST)\n --auth-key <secret> shared secret, min 12 chars; browsers log in with it,\n services send it as x-workerdeck-key\n (WORKERDECK_AUTH_KEY). Unset = no auth on loopback;\n on any other interface a key is generated instead,\n printed once, and stored in <state-dir>/auth-key for\n later starts to reuse.\n --trust-proxy trust x-forwarded-proto/-host/-for from one reverse\n proxy. Required behind TLS termination, or the session\n cookie loses its Secure flag and the origin check\n computes http:// where the browser says https://.\n --allowed-origin <o> extra origin accepted on browser requests, for when a\n proxy rewrites Host (repeatable)\n --allowed-host <name> extra Host header accepted when running without auth\n (repeatable; loopback names are always accepted)\n --insecure-host <name>\n bind host that may serve without auth — no key demanded,\n none generated — and, while unauthenticated, also\n accepted as a Host header (repeatable; config:\n insecureHosts). Names the host alone, no port.\n --profile <name=dir> Claude config dir a session may run under (repeatable)\n --cwd-root <path> restrict session cwds to this root (repeatable,\n WORKERDECK_CWD_ROOTS as a ':'-separated list)\n --fs-root <path> narrow which host directories /v1/fs serves\n (repeatable, WORKERDECK_FS_ROOTS as a ':'-separated\n list). Reading otherwise follows --cwd-root: a caller\n who may start a session in a tree can already read it\n through the agent. With neither, the routes 404.\n --fs-write also accept writes over /v1/fs. Its own switch because\n an agent's writes go through the permission flow and a\n PUT does not. Every write is still conditional on the\n hash the client last read.\n --state-dir <path> where parked sessions are persisted\n (default: beside the config file, else ~/.workerdeck)\n --no-parking-store keep parked sessions in memory only; a restart drops them\n -c, --config <path> config file (default: ./workerdeck.config.mjs)\n --insecure allow no-auth on a non-loopback address. Only when\n something in front is doing the authenticating.\n --open open the dashboard in a browser once it is up\n -h, --help show this\n -v, --version print the version\n\nConfig file\n Options that cannot fit on a command line — \\`authenticate\\`, \\`buildRunnerConfig\\`,\n \\`createEngineRunner\\` are functions — live in workerdeck.config.mjs, which\n default-exports the createWorkerServer options (or a function returning them).\n Flags and env override it. Supplying your own \\`authenticate\\` turns the built-in\n shared-secret auth off entirely.\n\nCredentials\n workerdeck implements no Anthropic auth of its own: the official SDK/CLI\n resolves credentials from the environment, per profile. --auth-key protects this\n gateway, nothing else.\n`\n\nasync function readVersion(): Promise<string> {\n // src/cli.ts and build/cli.mjs are both one level under the package root.\n const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')\n try {\n const raw = await readFile(pkgPath, 'utf8')\n return (JSON.parse(raw) as { version?: string }).version ?? 'unknown'\n } catch {\n return 'unknown'\n }\n}\n\n/** Best-effort: a browser that won't open is a convenience missed, not a failure. */\nfunction openInBrowser(url: string): void {\n const command =\n process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'\n try {\n const child = spawn(command, [url], {\n stdio: 'ignore',\n detached: true,\n shell: process.platform === 'win32',\n })\n child.on('error', () => {})\n child.unref()\n } catch {\n // ignored\n }\n}\n\nasync function main(argv: string[]): Promise<number> {\n if (argv[0] === 'guard') {\n const { runGuard } = await import('./guard.ts')\n return await runGuard(argv.slice(1))\n }\n\n const flags = parseArgs(argv)\n if (flags.help) {\n process.stdout.write(HELP)\n return 0\n }\n if (flags.version) {\n process.stdout.write(`${await readVersion()}\\n`)\n return 0\n }\n\n const loaded = await loadConfigFile(flags.config)\n const config = resolveInstanceConfig(flags, loaded)\n const instance = await startInstance(config)\n\n if (config.open) openInBrowser(instance.url)\n\n const shutdown = (signal: string): void => {\n process.stdout.write(`\\n[workerdeck] ${signal} — shutting down\\n`)\n // Parked sessions are already on disk; this is about letting in-flight\n // requests finish rather than dropping sockets on the floor.\n instance\n .close()\n .then(() => process.exit(0))\n .catch(() => process.exit(1))\n }\n process.on('SIGINT', () => shutdown('SIGINT'))\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n\n // Resolves only on close; the process stays up serving.\n await instance.closed\n return 0\n}\n\nmain(process.argv.slice(2))\n .then((code) => {\n if (code !== 0) process.exit(code)\n })\n .catch((error: unknown) => {\n if (error instanceof ConfigError) {\n process.stderr.write(`[workerdeck] ${error.message}\\n`)\n process.exit(2)\n }\n process.stderr.write(\n `[workerdeck] ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`,\n )\n process.exit(1)\n })\n"],"mappings":";;;;;;;AAQA,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+Db,eAAe,cAA+B;CAE5C,MAAM,UAAU,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE,MAAM,eAAe;AACnF,KAAI;EACF,MAAM,MAAM,MAAM,SAAS,SAAS,OAAO;AAC3C,SAAQ,KAAK,MAAM,IAAI,CAA0B,WAAW;SACtD;AACN,SAAO;;;;AAKX,SAAS,cAAc,KAAmB;CACxC,MAAM,UACJ,QAAQ,aAAa,WAAW,SAAS,QAAQ,aAAa,UAAU,UAAU;AACpF,KAAI;EACF,MAAM,QAAQ,MAAM,SAAS,CAAC,IAAI,EAAE;GAClC,OAAO;GACP,UAAU;GACV,OAAO,QAAQ,aAAa;GAC7B,CAAC;AACF,QAAM,GAAG,eAAe,GAAG;AAC3B,QAAM,OAAO;SACP;;AAKV,eAAe,KAAK,MAAiC;AACnD,KAAI,KAAK,OAAO,SAAS;EACvB,MAAM,EAAE,aAAa,MAAM,OAAO,wBAAA,MAAA,MAAA,EAAA,EAAA;AAClC,SAAO,MAAM,SAAS,KAAK,MAAM,EAAE,CAAC;;CAGtC,MAAM,QAAQ,UAAU,KAAK;AAC7B,KAAI,MAAM,MAAM;AACd,UAAQ,OAAO,MAAM,KAAK;AAC1B,SAAO;;AAET,KAAI,MAAM,SAAS;AACjB,UAAQ,OAAO,MAAM,GAAG,MAAM,aAAa,CAAC,IAAI;AAChD,SAAO;;CAIT,MAAM,SAAS,sBAAsB,OAAO,MADvB,eAAe,MAAM,OAAO,CACE;CACnD,MAAM,WAAW,MAAM,cAAc,OAAO;AAE5C,KAAI,OAAO,KAAM,eAAc,SAAS,IAAI;CAE5C,MAAM,YAAY,WAAyB;AACzC,UAAQ,OAAO,MAAM,kBAAkB,OAAO,oBAAoB;AAGlE,WACG,OAAO,CACP,WAAW,QAAQ,KAAK,EAAE,CAAC,CAC3B,YAAY,QAAQ,KAAK,EAAE,CAAC;;AAEjC,SAAQ,GAAG,gBAAgB,SAAS,SAAS,CAAC;AAC9C,SAAQ,GAAG,iBAAiB,SAAS,UAAU,CAAC;AAGhD,OAAM,SAAS;AACf,QAAO;;AAGT,KAAK,QAAQ,KAAK,MAAM,EAAE,CAAC,CACxB,MAAM,SAAS;AACd,KAAI,SAAS,EAAG,SAAQ,KAAK,KAAK;EAClC,CACD,OAAO,UAAmB;AACzB,KAAI,iBAAiB,aAAa;AAChC,UAAQ,OAAO,MAAM,gBAAgB,MAAM,QAAQ,IAAI;AACvD,UAAQ,KAAK,EAAE;;AAEjB,SAAQ,OAAO,MACb,gBAAgB,iBAAiB,QAAS,MAAM,SAAS,MAAM,UAAW,OAAO,MAAM,CAAC,IACzF;AACD,SAAQ,KAAK,EAAE;EACf"}
|
package/build/index.d.mts
CHANGED
|
@@ -1,7 +1,74 @@
|
|
|
1
1
|
import { Authenticator, WorkerServer, WorkerServerOptions } from "@workerdeck/server";
|
|
2
|
+
import { KeyObject } from "node:crypto";
|
|
2
3
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
-
import { ProfileInfo } from "@workerdeck/protocol";
|
|
4
|
+
import { ProfileInfo, SessionNotification } from "@workerdeck/protocol";
|
|
4
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
|
|
5
72
|
//#region src/auth.d.ts
|
|
6
73
|
/**
|
|
7
74
|
* Gateway auth for the turnkey CLI: one shared operator secret, two transports.
|
|
@@ -124,6 +191,17 @@ type WorkerDeckConfig = WorkerServerOptions & {
|
|
|
124
191
|
*/
|
|
125
192
|
insecureHosts?: string[]; /** Serve a dashboard build from here instead of the bundled one. */
|
|
126
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;
|
|
127
205
|
};
|
|
128
206
|
type CliFlags = {
|
|
129
207
|
config?: string;
|
|
@@ -132,6 +210,8 @@ type CliFlags = {
|
|
|
132
210
|
authKey?: string;
|
|
133
211
|
profiles: ProfileInfo[];
|
|
134
212
|
cwdRoots: string[];
|
|
213
|
+
fsRoots: string[];
|
|
214
|
+
fsWrite?: boolean;
|
|
135
215
|
allowedOrigins: string[];
|
|
136
216
|
allowedHosts: string[];
|
|
137
217
|
insecureHosts: string[];
|
|
@@ -185,6 +265,9 @@ type ResolvedConfig = {
|
|
|
185
265
|
*/
|
|
186
266
|
allowedHosts: Set<string> | null; /** Dashboard build to serve; resolved from the package when unset. */
|
|
187
267
|
webRoot?: string;
|
|
268
|
+
/** APNs forwarder settings with `keyFile` made absolute, or undefined for an
|
|
269
|
+
* instance that does not push. */
|
|
270
|
+
apns?: ApnsConfig;
|
|
188
271
|
open: boolean;
|
|
189
272
|
options: WorkerServerOptions;
|
|
190
273
|
};
|
|
@@ -282,5 +365,89 @@ type LoginPageOptions = {
|
|
|
282
365
|
};
|
|
283
366
|
declare function renderLoginPage(options: LoginPageOptions): string;
|
|
284
367
|
//#endregion
|
|
285
|
-
|
|
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 };
|
|
286
453
|
//# sourceMappingURL=index.d.mts.map
|
package/build/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { n as runGuard } from "./guard-D3VN855w.mjs";
|
|
2
|
-
import { a as ConfigError, c as isLoopback, d as parseArgs, f as resolveInstanceConfig, 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 } from "./instance-
|
|
3
|
-
export { ConfigError, createCliAuth, createHostGuard, defaultStateDir, hostnameOf, isLoopback, isLoopbackHostname, loadConfigFile, materializeAuthKey, parseArgs, renderLoginPage, resolveInstanceConfig, resolveWebRoot, runGuard, startInstance };
|
|
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 };
|