workerdeck 2.3.0 → 2.5.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.mjs +10 -2
- package/build/cli.mjs.map +1 -1
- package/build/index.d.mts +5 -1
- package/build/index.mjs +1 -1
- package/build/{instance-Bdf5e-lQ.mjs → instance-DP3af4H-.mjs} +55 -6
- package/build/instance-DP3af4H-.mjs.map +1 -0
- package/package.json +4 -4
- package/build/instance-Bdf5e-lQ.mjs.map +0 -1
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-DP3af4H-.mjs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -33,7 +33,15 @@ Options
|
|
|
33
33
|
none generated — and, while unauthenticated, also
|
|
34
34
|
accepted as a Host header (repeatable; config:
|
|
35
35
|
insecureHosts). Names the host alone, no port.
|
|
36
|
-
--profile <name=dir> Claude config dir a session may run under (repeatable)
|
|
36
|
+
--profile <name=dir> Claude config dir a session may run under (repeatable). Declared
|
|
37
|
+
this way a profile is fixed: it cannot be changed over the API.
|
|
38
|
+
--profile-root <path> directory a profile created at runtime may point its config dir at
|
|
39
|
+
(repeatable; default: your home directory). Narrow this to pin
|
|
40
|
+
exactly which credential stores the dashboard may reach for.
|
|
41
|
+
--no-profile-store refuse runtime profile management entirely — /v1/profiles stays
|
|
42
|
+
read-only and profiles come from --profile and the config file
|
|
43
|
+
alone. Profiles created at runtime are stored in
|
|
44
|
+
<state-dir>/profiles.json, so --no-parking-store implies this.
|
|
37
45
|
--cwd-root <path> restrict session cwds to this root (repeatable,
|
|
38
46
|
WORKERDECK_CWD_ROOTS as a ':'-separated list)
|
|
39
47
|
--fs-root <path> narrow which host directories /v1/fs serves
|
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 './lib/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 --shell let an operator run '!' shell commands on the host from\n the composer, in the session's cwd. Off by default and\n its own switch: unlike an agent's Bash tool, a '!'\n command goes through no permission prompt at all.\n Never offered to a scoped principal.\n --approval-timeout <d>\n how long a permission prompt or a question may wait for\n an answer before the engine denies it (300000, 30s, 5m;\n config: approvalTimeoutMs). Default 'none' — a prompt\n waits as long as the session lives. A session may\n override it per request.\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 --cors-origin <o> browser origin allowed to call /v1 cross-origin, for a\n dashboard served elsewhere (repeatable, exact origin, no\n wildcard; config: corsOrigins). Refused without auth. The\n key is still required — this only permits the call.\n --no-keep-awake don't hold the machine awake while a session is waiting\n on it (config: keepAwake: false). By default a turn in\n flight, or a session blocked on an approval, blocks idle\n sleep — a laptop sleeping mid-turn drops the socket and\n burns the turn. The screen is never kept on, and the\n lock releases the moment nothing is waiting.\n --no-web don't serve the web dashboard (config: web: false).\n /v1 and the auth routes are unchanged; everything else\n 404s. For a gateway reached only from the VS Code\n extension, the phone, or another host's dashboard.\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\nfunction openInBrowser(url: string): void {\n const command = 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}\n\nfunction line(text: string): void {\n process.stdout.write(`${text}\\n`)\n}\n\nasync function main(argv: string[]): Promise<number> {\n if (argv[0] === 'guard') {\n const { runGuard } = await import('./lib/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) {\n openInBrowser(instance.url)\n }\n\n // A second signal must always be able to kill a shutdown that is taking too long. That used to work only by\n // accident — the second call re-entered `instance.close()` and got an immediate callback out of an already-closed\n // http server — so it evaporated the moment close stopped being idempotent-by-luck. Make it a real path.\n let shuttingDown = false\n const shutdown = (signal: string): void => {\n if (shuttingDown) {\n process.stdout.write(`\\n[workerdeck] ${signal} again — terminating now\\n`)\n process.exit(130)\n }\n shuttingDown = true\n line(`\\n[workerdeck] ${signal} — shutting down (press again to stop now)`)\n instance\n .drain({\n onProgress: (report) => {\n if (report.working.length > 0) {\n line(`[workerdeck] waiting for ${report.working.length} session(s) to finish the current turn`)\n }\n // Named, not waited for: nothing about shutting down answers a permission prompt.\n for (const id of report.awaitingHuman) {\n line(`[workerdeck] session ${id} is waiting on an approval — not waiting for it`)\n }\n if (report.timedOut) {\n line(`[workerdeck] ${report.working.length} session(s) still running — stopping anyway`)\n } else if (report.working.length === 0) {\n line('[workerdeck] all turns finished')\n }\n },\n })\n .catch(() => undefined)\n .then(() => instance.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: until then 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) {\n process.exit(code)\n }\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(`[workerdeck] ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`)\n process.exit(1)\n })\n"],"mappings":"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"}
|
|
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 './lib/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). Declared\n this way a profile is fixed: it cannot be changed over the API.\n --profile-root <path> directory a profile created at runtime may point its config dir at\n (repeatable; default: your home directory). Narrow this to pin\n exactly which credential stores the dashboard may reach for.\n --no-profile-store refuse runtime profile management entirely — /v1/profiles stays\n read-only and profiles come from --profile and the config file\n alone. Profiles created at runtime are stored in\n <state-dir>/profiles.json, so --no-parking-store implies this.\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 --shell let an operator run '!' shell commands on the host from\n the composer, in the session's cwd. Off by default and\n its own switch: unlike an agent's Bash tool, a '!'\n command goes through no permission prompt at all.\n Never offered to a scoped principal.\n --approval-timeout <d>\n how long a permission prompt or a question may wait for\n an answer before the engine denies it (300000, 30s, 5m;\n config: approvalTimeoutMs). Default 'none' — a prompt\n waits as long as the session lives. A session may\n override it per request.\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 --cors-origin <o> browser origin allowed to call /v1 cross-origin, for a\n dashboard served elsewhere (repeatable, exact origin, no\n wildcard; config: corsOrigins). Refused without auth. The\n key is still required — this only permits the call.\n --no-keep-awake don't hold the machine awake while a session is waiting\n on it (config: keepAwake: false). By default a turn in\n flight, or a session blocked on an approval, blocks idle\n sleep — a laptop sleeping mid-turn drops the socket and\n burns the turn. The screen is never kept on, and the\n lock releases the moment nothing is waiting.\n --no-web don't serve the web dashboard (config: web: false).\n /v1 and the auth routes are unchanged; everything else\n 404s. For a gateway reached only from the VS Code\n extension, the phone, or another host's dashboard.\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\nfunction openInBrowser(url: string): void {\n const command = 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}\n\nfunction line(text: string): void {\n process.stdout.write(`${text}\\n`)\n}\n\nasync function main(argv: string[]): Promise<number> {\n if (argv[0] === 'guard') {\n const { runGuard } = await import('./lib/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) {\n openInBrowser(instance.url)\n }\n\n // A second signal must always be able to kill a shutdown that is taking too long. That used to work only by\n // accident — the second call re-entered `instance.close()` and got an immediate callback out of an already-closed\n // http server — so it evaporated the moment close stopped being idempotent-by-luck. Make it a real path.\n let shuttingDown = false\n const shutdown = (signal: string): void => {\n if (shuttingDown) {\n process.stdout.write(`\\n[workerdeck] ${signal} again — terminating now\\n`)\n process.exit(130)\n }\n shuttingDown = true\n line(`\\n[workerdeck] ${signal} — shutting down (press again to stop now)`)\n instance\n .drain({\n onProgress: (report) => {\n if (report.working.length > 0) {\n line(`[workerdeck] waiting for ${report.working.length} session(s) to finish the current turn`)\n }\n // Named, not waited for: nothing about shutting down answers a permission prompt.\n for (const id of report.awaitingHuman) {\n line(`[workerdeck] session ${id} is waiting on an approval — not waiting for it`)\n }\n if (report.timedOut) {\n line(`[workerdeck] ${report.working.length} session(s) still running — stopping anyway`)\n } else if (report.working.length === 0) {\n line('[workerdeck] all turns finished')\n }\n },\n })\n .catch(() => undefined)\n .then(() => instance.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: until then 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) {\n process.exit(code)\n }\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(`[workerdeck] ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`)\n process.exit(1)\n })\n"],"mappings":"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"}
|
package/build/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Authenticator, WorkerServer, WorkerServerOptions } from "@workerdeck/server";
|
|
2
2
|
import { KeyObject } from "node:crypto";
|
|
3
|
-
import { ProfileInfo, SessionNotification } from "@workerdeck/protocol";
|
|
3
|
+
import { ProfileInfo, SessionNotification, SessionNotificationType } from "@workerdeck/protocol";
|
|
4
4
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
5
5
|
//#region src/apns/client.d.ts
|
|
6
6
|
type ApnsEnvironment = 'development' | 'production';
|
|
@@ -101,6 +101,8 @@ type CliFlags = {
|
|
|
101
101
|
profiles: ProfileInfo[];
|
|
102
102
|
cwdRoots: string[];
|
|
103
103
|
fsRoots: string[];
|
|
104
|
+
profileRoots: string[];
|
|
105
|
+
profileStore?: boolean;
|
|
104
106
|
fsWrite?: boolean;
|
|
105
107
|
shell?: boolean;
|
|
106
108
|
allowedOrigins: string[];
|
|
@@ -142,6 +144,7 @@ type ResolvedConfig = {
|
|
|
142
144
|
corsOrigins: string[];
|
|
143
145
|
apns?: ApnsConfig;
|
|
144
146
|
open: boolean;
|
|
147
|
+
profileStore: boolean;
|
|
145
148
|
options: WorkerServerOptions;
|
|
146
149
|
};
|
|
147
150
|
declare function defaultStateDir(configPath: string | null): string;
|
|
@@ -203,6 +206,7 @@ type DeviceRecord = {
|
|
|
203
206
|
bundleId?: string;
|
|
204
207
|
platform?: string;
|
|
205
208
|
liveActivityStartToken?: string;
|
|
209
|
+
notify?: SessionNotificationType[];
|
|
206
210
|
updatedAt: number;
|
|
207
211
|
};
|
|
208
212
|
type DeviceRegistry = {
|
package/build/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { n as runGuard } from "./guard-CGPVzJ3Q.mjs";
|
|
2
|
-
import { C as createActivityRegistry, S as createDeviceRoute, _ as createApnsForwarder, a as ConfigError, b as createApnsRoute, 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 createApnsClient, w as createActivityRoute, x as createDeviceRegistry, y as loadApnsKey } from "./instance-
|
|
2
|
+
import { C as createActivityRegistry, S as createDeviceRoute, _ as createApnsForwarder, a as ConfigError, b as createApnsRoute, 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 createApnsClient, w as createActivityRoute, x as createDeviceRegistry, y as loadApnsKey } from "./instance-DP3af4H-.mjs";
|
|
3
3
|
export { ConfigError, buildPush, createActivityRegistry, createActivityRoute, createApnsClient, createApnsForwarder, createApnsRoute, createAuthSessionStore, createCliAuth, createDeviceRegistry, createDeviceRoute, createHostGuard, defaultStateDir, hostnameOf, isLoopback, isLoopbackHostname, loadApnsKey, loadConfigFile, materializeAuthKey, parseArgs, renderLoginPage, resolveInstanceConfig, resolveWebRoot, runGuard, startInstance };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createReadStream, existsSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
3
|
-
import { createFileSessionStore, createWorkerServer } from "@workerdeck/server";
|
|
3
|
+
import { createFileProfileStore, createFileSessionStore, createWorkerServer } from "@workerdeck/server";
|
|
4
4
|
import { dashboardDir } from "@workerdeck/web";
|
|
5
5
|
import { chmod, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
6
6
|
import { createHash, createHmac, createPrivateKey, randomBytes, sign, timingSafeEqual } from "node:crypto";
|
|
@@ -205,12 +205,33 @@ function createActivityRoute(registry, authenticate) {
|
|
|
205
205
|
}
|
|
206
206
|
//#endregion
|
|
207
207
|
//#region src/apns/devices.ts
|
|
208
|
+
const NOTIFY_EVENTS = [
|
|
209
|
+
"permission_requested",
|
|
210
|
+
"turn_completed",
|
|
211
|
+
"session_error",
|
|
212
|
+
"session_closed"
|
|
213
|
+
];
|
|
214
|
+
const DEFAULT_NOTIFY = [
|
|
215
|
+
"permission_requested",
|
|
216
|
+
"turn_completed",
|
|
217
|
+
"session_error"
|
|
218
|
+
];
|
|
208
219
|
const FILENAME = "apns-devices.json";
|
|
209
220
|
const TOKEN_PATTERN = /^[0-9a-fA-F]{32,200}$/;
|
|
210
221
|
const MAX_BODY_BYTES = 4096;
|
|
211
222
|
function isEnvironment(value) {
|
|
212
223
|
return value === "development" || value === "production";
|
|
213
224
|
}
|
|
225
|
+
function isNotifyList(value) {
|
|
226
|
+
return Array.isArray(value) && value.every((entry) => NOTIFY_EVENTS.includes(entry));
|
|
227
|
+
}
|
|
228
|
+
function sameNotify(a, b) {
|
|
229
|
+
if (a === void 0 || b === void 0) return a === b;
|
|
230
|
+
return a.length === b.length && a.every((entry, index) => entry === b[index]);
|
|
231
|
+
}
|
|
232
|
+
function wantsNotification(device, type) {
|
|
233
|
+
return (device.notify ?? DEFAULT_NOTIFY).includes(type);
|
|
234
|
+
}
|
|
214
235
|
async function createDeviceRegistry(options) {
|
|
215
236
|
const path = options.dir === null ? null : join(options.dir, FILENAME);
|
|
216
237
|
const devices = /* @__PURE__ */ new Map();
|
|
@@ -242,7 +263,7 @@ async function createDeviceRegistry(options) {
|
|
|
242
263
|
...record,
|
|
243
264
|
updatedAt: Date.now()
|
|
244
265
|
});
|
|
245
|
-
if (existing !== void 0 && existing.environment === record.environment && existing.hostId === record.hostId && existing.liveActivityStartToken === record.liveActivityStartToken) return;
|
|
266
|
+
if (existing !== void 0 && existing.environment === record.environment && existing.hostId === record.hostId && existing.liveActivityStartToken === record.liveActivityStartToken && sameNotify(existing.notify, record.notify)) return;
|
|
246
267
|
await persist();
|
|
247
268
|
},
|
|
248
269
|
async clearStartToken(token) {
|
|
@@ -313,15 +334,22 @@ function createDeviceRoute(registry, authenticate) {
|
|
|
313
334
|
respondJson(res, 400, { error: "liveActivityStartToken must be a hex token or null" });
|
|
314
335
|
return true;
|
|
315
336
|
}
|
|
337
|
+
const notifyGiven = Object.hasOwn(body, "notify");
|
|
338
|
+
if (notifyGiven && !isNotifyList(body.notify)) {
|
|
339
|
+
respondJson(res, 400, { error: `notify must be an array of ${NOTIFY_EVENTS.join(", ")}` });
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
316
342
|
const previous = registry.list().find((record) => record.token === token);
|
|
317
343
|
const liveActivityStartToken = startTokenGiven ? startToken ?? void 0 : previous?.liveActivityStartToken;
|
|
344
|
+
const notify = notifyGiven ? body.notify : previous?.notify;
|
|
318
345
|
await registry.register({
|
|
319
346
|
token,
|
|
320
347
|
environment: body.environment,
|
|
321
348
|
hostId: optionalString(body.hostId),
|
|
322
349
|
bundleId: optionalString(body.bundleId),
|
|
323
350
|
platform: optionalString(body.platform),
|
|
324
|
-
...liveActivityStartToken === void 0 ? {} : { liveActivityStartToken }
|
|
351
|
+
...liveActivityStartToken === void 0 ? {} : { liveActivityStartToken },
|
|
352
|
+
...notify === void 0 ? {} : { notify }
|
|
325
353
|
});
|
|
326
354
|
respondJson(res, 200, {
|
|
327
355
|
registered: true,
|
|
@@ -798,6 +826,12 @@ function oneLine(text, limit = BODY_LIMIT) {
|
|
|
798
826
|
function collapseKey(sessionId) {
|
|
799
827
|
return createHash("sha256").update(sessionId).digest("base64url").slice(0, 32);
|
|
800
828
|
}
|
|
829
|
+
const COLLAPSE_PREFIX = {
|
|
830
|
+
permission_requested: "p",
|
|
831
|
+
turn_completed: "t",
|
|
832
|
+
session_error: "e",
|
|
833
|
+
session_closed: "c"
|
|
834
|
+
};
|
|
801
835
|
function titleFor(notification, name) {
|
|
802
836
|
switch (notification.type) {
|
|
803
837
|
case "permission_requested": return `Approval needed — ${name}`;
|
|
@@ -847,7 +881,7 @@ function buildPush(notification, hostId) {
|
|
|
847
881
|
payload,
|
|
848
882
|
priority: permission || notification.type === "session_error" ? 10 : 5,
|
|
849
883
|
expiration: permission && notification.request?.expiresAt !== void 0 ? Math.floor(notification.request.expiresAt / 1e3) : Math.floor(Date.now() / 1e3) + 3600,
|
|
850
|
-
|
|
884
|
+
collapseId: `${COLLAPSE_PREFIX[notification.type]}:${collapseKey(notification.sessionId)}`
|
|
851
885
|
};
|
|
852
886
|
}
|
|
853
887
|
async function createApnsForwarder(options) {
|
|
@@ -869,7 +903,7 @@ async function createApnsForwarder(options) {
|
|
|
869
903
|
const fallbackEnvironment = options.config.production === false ? "development" : "production";
|
|
870
904
|
const chains = /* @__PURE__ */ new Map();
|
|
871
905
|
const deliver = async (notification) => {
|
|
872
|
-
const devices = registry.list();
|
|
906
|
+
const devices = registry.list().filter((device) => wantsNotification(device, notification.type));
|
|
873
907
|
if (devices.length === 0) return;
|
|
874
908
|
await Promise.all(devices.map(async (device) => {
|
|
875
909
|
const push = buildPush(notification, device.hostId);
|
|
@@ -1650,6 +1684,7 @@ function parsePort(raw, source) {
|
|
|
1650
1684
|
function parseArgs(argv) {
|
|
1651
1685
|
const flags = {
|
|
1652
1686
|
profiles: [],
|
|
1687
|
+
profileRoots: [],
|
|
1653
1688
|
cwdRoots: [],
|
|
1654
1689
|
fsRoots: [],
|
|
1655
1690
|
allowedOrigins: [],
|
|
@@ -1705,6 +1740,13 @@ function parseArgs(argv) {
|
|
|
1705
1740
|
});
|
|
1706
1741
|
break;
|
|
1707
1742
|
}
|
|
1743
|
+
case "--profile-root":
|
|
1744
|
+
flags.profileRoots.push(resolve(next(i, arg)));
|
|
1745
|
+
i++;
|
|
1746
|
+
break;
|
|
1747
|
+
case "--no-profile-store":
|
|
1748
|
+
flags.profileStore = false;
|
|
1749
|
+
break;
|
|
1708
1750
|
case "--cwd-root":
|
|
1709
1751
|
flags.cwdRoots.push(resolve(next(i, arg)));
|
|
1710
1752
|
i++;
|
|
@@ -1880,6 +1922,10 @@ function resolveInstanceConfig(flags, loaded, env = process.env, cwd = process.c
|
|
|
1880
1922
|
};
|
|
1881
1923
|
if (flags.profiles.length) options.profiles = flags.profiles;
|
|
1882
1924
|
if (flags.approvalTimeoutMs !== void 0) options.approvalTimeoutMs = flags.approvalTimeoutMs;
|
|
1925
|
+
if (flags.profileStore === false) delete options.profileStore;
|
|
1926
|
+
const manageProfiles = flags.profileStore !== false;
|
|
1927
|
+
if (manageProfiles && !options.allowedConfigDirRoots) options.allowedConfigDirRoots = flags.profileRoots.length ? flags.profileRoots : [homedir()];
|
|
1928
|
+
const profileStore = manageProfiles && options.profileStore === void 0;
|
|
1883
1929
|
return {
|
|
1884
1930
|
port,
|
|
1885
1931
|
host,
|
|
@@ -1896,6 +1942,7 @@ function resolveInstanceConfig(flags, loaded, env = process.env, cwd = process.c
|
|
|
1896
1942
|
corsOrigins,
|
|
1897
1943
|
apns: resolveApns(loaded),
|
|
1898
1944
|
open: flags.open ?? false,
|
|
1945
|
+
profileStore,
|
|
1899
1946
|
options
|
|
1900
1947
|
};
|
|
1901
1948
|
}
|
|
@@ -2254,8 +2301,10 @@ async function startInstance(config, options = {}) {
|
|
|
2254
2301
|
process.stderr.write(`[workerdeck] parked-session store ${context.op} failed for ${context.path}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2255
2302
|
}
|
|
2256
2303
|
});
|
|
2304
|
+
const profileStore = config.profileStore && config.stateDir ? createFileProfileStore(join(config.stateDir, "profiles.json")) : void 0;
|
|
2257
2305
|
const server = createWorkerServer({
|
|
2258
2306
|
...config.options,
|
|
2307
|
+
...profileStore ? { profileStore } : {},
|
|
2259
2308
|
checkCredentials: config.options.checkCredentials ?? true,
|
|
2260
2309
|
parking,
|
|
2261
2310
|
notifications: apns === void 0 ? config.options.notifications : {
|
|
@@ -2340,4 +2389,4 @@ async function startInstance(config, options = {}) {
|
|
|
2340
2389
|
//#endregion
|
|
2341
2390
|
export { createActivityRegistry as C, createDeviceRoute as S, createApnsForwarder as _, ConfigError as a, createApnsRoute as b, isLoopback as c, parseArgs as d, resolveInstanceConfig as f, buildPush as g, materializeAuthKey as h, renderLoginPage as i, isLoopbackHostname as l, createAuthSessionStore as m, resolveWebRoot as n, defaultStateDir as o, createCliAuth as p, startInstance as r, hostnameOf as s, createHostGuard as t, loadConfigFile as u, createApnsClient as v, createActivityRoute as w, createDeviceRegistry as x, loadApnsKey as y };
|
|
2342
2391
|
|
|
2343
|
-
//# sourceMappingURL=instance-
|
|
2392
|
+
//# sourceMappingURL=instance-DP3af4H-.mjs.map
|