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 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-Bdf5e-lQ.mjs";
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-Bdf5e-lQ.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-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
- ...notification.type === "turn_completed" ? { collapseId: `t:${collapseKey(notification.sessionId)}` } : {}
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-Bdf5e-lQ.mjs.map
2392
+ //# sourceMappingURL=instance-DP3af4H-.mjs.map