workerdeck 2.7.2 → 2.8.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 +13 -13
- package/build/cli.mjs +15 -15
- package/build/cli.mjs.map +1 -1
- package/build/{guard-CGPVzJ3Q.mjs → guard-C4ybpzbt.mjs} +5 -5
- package/build/guard-C4ybpzbt.mjs.map +1 -0
- package/build/index.mjs +2 -2
- package/build/{instance-DP3af4H-.mjs → instance-BhSGGmoY.mjs} +21 -21
- package/build/instance-BhSGGmoY.mjs.map +1 -0
- package/package.json +8 -8
- package/build/guard-CGPVzJ3Q.mjs.map +0 -1
- package/build/instance-DP3af4H-.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ session gateway **and** the full dashboard, on one port, with nothing to clone.
|
|
|
7
7
|
npx workerdeck
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
That serves `http://127.0.0.1:8787`
|
|
10
|
+
That serves `http://127.0.0.1:8787` - dashboard at the root, API under `/v1` - and persists parked
|
|
11
11
|
sessions under `~/.workerdeck` so a restart doesn't drop them.
|
|
12
12
|
|
|
13
13
|
The `@workerdeck/*` packages are the libraries you embed in your own app. This one is the
|
|
@@ -22,16 +22,16 @@ npx workerdeck --auth-key "$SECRET" --host 0.0.0.0
|
|
|
22
22
|
One secret, two transports:
|
|
23
23
|
|
|
24
24
|
- **Browsers** get a login page, trade the secret for an `HttpOnly` session cookie, and use that
|
|
25
|
-
for everything afterwards
|
|
25
|
+
for everything afterwards - including the WebSocket attach.
|
|
26
26
|
- **Services** send the same secret as an `x-workerdeck-key` header (or
|
|
27
27
|
`Authorization: Bearer`).
|
|
28
28
|
|
|
29
29
|
The cookie is not a convenience. A browser cannot set a header on a WebSocket handshake at all, so
|
|
30
|
-
a cookie is the only credential a tab can present when it attaches to a session
|
|
30
|
+
a cookie is the only credential a tab can present when it attaches to a session - and a cookie only
|
|
31
31
|
rides requests to the origin that set it. That is why the dashboard and the API share a port.
|
|
32
32
|
|
|
33
33
|
Those logins persist in `<state-dir>/auth-sessions.json` (mode 600), so a restart doesn't sign
|
|
34
|
-
every open tab out. Each row is an HMAC of the cookie under the operator secret
|
|
34
|
+
every open tab out. Each row is an HMAC of the cookie under the operator secret - so the file is
|
|
35
35
|
worth nothing on its own, and rotating the secret invalidates every outstanding login.
|
|
36
36
|
|
|
37
37
|
Neither transport authenticates *who* the person is; the secret is a door key. Put an
|
|
@@ -50,11 +50,11 @@ port. `--insecure` overrides that, for when something in front is doing the auth
|
|
|
50
50
|
| `--auth-key <secret>` | `WORKERDECK_AUTH_KEY` | none (no auth) |
|
|
51
51
|
| `--cwd-root <path>` (repeatable) | `WORKERDECK_CWD_ROOTS` (`:`-separated) | unrestricted |
|
|
52
52
|
| `--fs-root <path>` (repeatable) | `WORKERDECK_FS_ROOTS` (`:`-separated) | narrows `/v1/fs`; unset, reading follows `--cwd-root` |
|
|
53
|
-
| `--fs-write` |
|
|
54
|
-
| `--profile <name=dir>` (repeatable) |
|
|
53
|
+
| `--fs-write` | - | off (browse and read only) |
|
|
54
|
+
| `--profile <name=dir>` (repeatable) | - | auto-detected from `~/.claude` |
|
|
55
55
|
| `--state-dir <path>` | `WORKERDECK_STATE_DIR` | beside the config file, else `~/.workerdeck` |
|
|
56
|
-
| `--no-parking-store` |
|
|
57
|
-
| `--config <path>` |
|
|
56
|
+
| `--no-parking-store` | - | durable parking on |
|
|
57
|
+
| `--config <path>` | - | `./workerdeck.config.mjs` |
|
|
58
58
|
| `--insecure`, `--open`, `--help`, `--version` | | |
|
|
59
59
|
|
|
60
60
|
Precedence is narrowest-wins: flags > env > config file > defaults.
|
|
@@ -75,7 +75,7 @@ export default {
|
|
|
75
75
|
}
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
Supplying your own `authenticate` turns the built-in shared-secret auth **off entirely**
|
|
78
|
+
Supplying your own `authenticate` turns the built-in shared-secret auth **off entirely** - one
|
|
79
79
|
hook, one scheme, rather than two paths where only one got audited. If you take it over, you own
|
|
80
80
|
finding a credential the browser can actually present. There's a fuller example in
|
|
81
81
|
[`examples/workerdeck.config.mjs`](https://github.com/workerdeck/workerdeck/blob/master/examples/workerdeck.config.mjs).
|
|
@@ -87,7 +87,7 @@ npx workerdeck guard --wait 300 --allow-parked && systemctl restart workerdeck
|
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
Exits `0` when a restart is safe, `1` while a session is mid-turn, awaiting an approval, or parked
|
|
90
|
-
without durability behind it, and `2` when it couldn't tell
|
|
90
|
+
without durability behind it, and `2` when it couldn't tell - never treating "couldn't tell" as
|
|
91
91
|
safe. `--allow-parked` and `--allow-queued` are you asserting that the `SessionStore` and the
|
|
92
92
|
`QueueAdapter` respectively are durable; they're separate decisions. Point `--url` at any instance
|
|
93
93
|
and authenticate with `--token` or `--header name=value`.
|
|
@@ -95,7 +95,7 @@ and authenticate with `--token` or `--header name=value`.
|
|
|
95
95
|
## Credentials
|
|
96
96
|
|
|
97
97
|
WorkerDeck implements **no Anthropic auth**. The official SDK/CLI resolves credentials from the
|
|
98
|
-
operator's environment, per profile
|
|
98
|
+
operator's environment, per profile - `--auth-key` protects this gateway and nothing else. See the
|
|
99
99
|
project's
|
|
100
100
|
[Auth & Anthropic's terms](https://github.com/workerdeck/workerdeck#auth--anthropics-terms).
|
|
101
101
|
|
|
@@ -113,13 +113,13 @@ MIT
|
|
|
113
113
|
## Rules you cannot infer from the types
|
|
114
114
|
|
|
115
115
|
- **Single origin is load-bearing.** The gateway and the dashboard share one port because a tab
|
|
116
|
-
cannot header a WebSocket handshake, so a cookie is the only credential an attach can carry
|
|
116
|
+
cannot header a WebSocket handshake, so a cookie is the only credential an attach can carry -
|
|
117
117
|
and cookies are per-origin. This is why there is no "dashboard on another port" mode.
|
|
118
118
|
- **Loopback runs keyless; off loopback the CLI generates a key rather than serving open.** Only an
|
|
119
119
|
explicit `--insecure` / `insecureHosts` declaration serves unauthenticated, and those entries
|
|
120
120
|
double as accepted `Host` headers.
|
|
121
121
|
- **Browser logins are keyed by `HMAC(secret, token)`.** That is what makes the session file
|
|
122
|
-
worthless to a reader
|
|
122
|
+
worthless to a reader - and what makes rotating the auth key invalidate every cookie for free.
|
|
123
123
|
- **A config file supplying its own `authenticate` turns the built-in auth off entirely**, rather
|
|
124
124
|
than layering on top of it. Two auth systems agreeing is harder to reason about than one.
|
|
125
125
|
|
package/build/cli.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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-BhSGGmoY.mjs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
//#region src/cli.ts
|
|
8
|
-
const HELP = `workerdeck
|
|
8
|
+
const HELP = `workerdeck - run a workerdeck instance: session gateway + dashboard, one port.
|
|
9
9
|
|
|
10
10
|
Usage
|
|
11
11
|
workerdeck [options]
|
|
@@ -29,8 +29,8 @@ Options
|
|
|
29
29
|
--allowed-host <name> extra Host header accepted when running without auth
|
|
30
30
|
(repeatable; loopback names are always accepted)
|
|
31
31
|
--insecure-host <name>
|
|
32
|
-
bind host that may serve without auth
|
|
33
|
-
none generated
|
|
32
|
+
bind host that may serve without auth - no key demanded,
|
|
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
36
|
--profile <name=dir> Claude config dir a session may run under (repeatable). Declared
|
|
@@ -38,7 +38,7 @@ Options
|
|
|
38
38
|
--profile-root <path> directory a profile created at runtime may point its config dir at
|
|
39
39
|
(repeatable; default: your home directory). Narrow this to pin
|
|
40
40
|
exactly which credential stores the dashboard may reach for.
|
|
41
|
-
--no-profile-store refuse runtime profile management entirely
|
|
41
|
+
--no-profile-store refuse runtime profile management entirely - /v1/profiles stays
|
|
42
42
|
read-only and profiles come from --profile and the config file
|
|
43
43
|
alone. Profiles created at runtime are stored in
|
|
44
44
|
<state-dir>/profiles.json, so --no-parking-store implies this.
|
|
@@ -61,7 +61,7 @@ Options
|
|
|
61
61
|
--approval-timeout <d>
|
|
62
62
|
how long a permission prompt or a question may wait for
|
|
63
63
|
an answer before the engine denies it (300000, 30s, 5m;
|
|
64
|
-
config: approvalTimeoutMs). Default 'none'
|
|
64
|
+
config: approvalTimeoutMs). Default 'none' - a prompt
|
|
65
65
|
waits as long as the session lives. A session may
|
|
66
66
|
override it per request.
|
|
67
67
|
--state-dir <path> where parked sessions are persisted
|
|
@@ -73,11 +73,11 @@ Options
|
|
|
73
73
|
--cors-origin <o> browser origin allowed to call /v1 cross-origin, for a
|
|
74
74
|
dashboard served elsewhere (repeatable, exact origin, no
|
|
75
75
|
wildcard; config: corsOrigins). Refused without auth. The
|
|
76
|
-
key is still required
|
|
76
|
+
key is still required - this only permits the call.
|
|
77
77
|
--no-keep-awake don't hold the machine awake while a session is waiting
|
|
78
78
|
on it (config: keepAwake: false). By default a turn in
|
|
79
79
|
flight, or a session blocked on an approval, blocks idle
|
|
80
|
-
sleep
|
|
80
|
+
sleep - a laptop sleeping mid-turn drops the socket and
|
|
81
81
|
burns the turn. The screen is never kept on, and the
|
|
82
82
|
lock releases the moment nothing is waiting.
|
|
83
83
|
--no-web don't serve the web dashboard (config: web: false).
|
|
@@ -89,8 +89,8 @@ Options
|
|
|
89
89
|
-v, --version print the version
|
|
90
90
|
|
|
91
91
|
Config file
|
|
92
|
-
Options that cannot fit on a command line
|
|
93
|
-
\`createEngineRunner\` are functions
|
|
92
|
+
Options that cannot fit on a command line - \`authenticate\`, \`buildRunnerConfig\`,
|
|
93
|
+
\`createEngineRunner\` are functions - live in workerdeck.config.mjs, which
|
|
94
94
|
default-exports the createWorkerServer options (or a function returning them).
|
|
95
95
|
Flags and env override it. Supplying your own \`authenticate\` turns the built-in
|
|
96
96
|
shared-secret auth off entirely.
|
|
@@ -126,7 +126,7 @@ function line(text) {
|
|
|
126
126
|
}
|
|
127
127
|
async function main(argv) {
|
|
128
128
|
if (argv[0] === "guard") {
|
|
129
|
-
const { runGuard } = await import("./guard-
|
|
129
|
+
const { runGuard } = await import("./guard-C4ybpzbt.mjs").then((n) => n.t);
|
|
130
130
|
return await runGuard(argv.slice(1));
|
|
131
131
|
}
|
|
132
132
|
const flags = parseArgs(argv);
|
|
@@ -145,15 +145,15 @@ async function main(argv) {
|
|
|
145
145
|
let shuttingDown = false;
|
|
146
146
|
const shutdown = (signal) => {
|
|
147
147
|
if (shuttingDown) {
|
|
148
|
-
process.stdout.write(`\n[workerdeck] ${signal} again
|
|
148
|
+
process.stdout.write(`\n[workerdeck] ${signal} again - terminating now\n`);
|
|
149
149
|
process.exit(130);
|
|
150
150
|
}
|
|
151
151
|
shuttingDown = true;
|
|
152
|
-
line(`\n[workerdeck] ${signal}
|
|
152
|
+
line(`\n[workerdeck] ${signal} - shutting down (press again to stop now)`);
|
|
153
153
|
instance.drain({ onProgress: (report) => {
|
|
154
154
|
if (report.working.length > 0) line(`[workerdeck] waiting for ${report.working.length} session(s) to finish the current turn`);
|
|
155
|
-
for (const id of report.awaitingHuman) line(`[workerdeck] session ${id} is waiting on an approval
|
|
156
|
-
if (report.timedOut) line(`[workerdeck] ${report.working.length} session(s) still running
|
|
155
|
+
for (const id of report.awaitingHuman) line(`[workerdeck] session ${id} is waiting on an approval - not waiting for it`);
|
|
156
|
+
if (report.timedOut) line(`[workerdeck] ${report.working.length} session(s) still running - stopping anyway`);
|
|
157
157
|
else if (report.working.length === 0) line("[workerdeck] all turns finished");
|
|
158
158
|
} }).catch(() => void 0).then(() => instance.close()).then(() => process.exit(0)).catch(() => process.exit(1));
|
|
159
159
|
};
|
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). 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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"}
|
|
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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"}
|
|
@@ -155,7 +155,7 @@ async function runGuard(argv) {
|
|
|
155
155
|
const notes = [];
|
|
156
156
|
for (const session of all.filter((s) => BUSY_STATUSES.has(s.status))) reasons.push(`session ${session.id} is ${session.status}`);
|
|
157
157
|
const parked = all.filter((s) => s.status === "parked");
|
|
158
|
-
if (parked.length > 0 && !values["allow-parked"]) reasons.push(`${parked.length} parked session(s)
|
|
158
|
+
if (parked.length > 0 && !values["allow-parked"]) reasons.push(`${parked.length} parked session(s) - pass --allow-parked once the server runs a durable SessionStore, or they are lost on restart`);
|
|
159
159
|
const queue = await get("/queue");
|
|
160
160
|
if (!queue.ok && !("status" in queue) && queue.code !== 404) return {
|
|
161
161
|
error: `GET ${base}/queue → ${queue.detail}`,
|
|
@@ -164,8 +164,8 @@ async function runGuard(argv) {
|
|
|
164
164
|
};
|
|
165
165
|
const stats = queue.ok ? queue.body.stats : void 0;
|
|
166
166
|
if ((stats?.running ?? 0) > 0) reasons.push(`${stats.running} job(s) running`);
|
|
167
|
-
if ((stats?.queued ?? 0) > 0 && !values["allow-queued"]) reasons.push(`${stats.queued} job(s) queued
|
|
168
|
-
if ((stats?.parked ?? 0) > 0 && values["allow-parked"]) notes.push(`${stats.parked} parked job(s): their queue-side records are the QueueAdapter's, not the SessionStore's
|
|
167
|
+
if ((stats?.queued ?? 0) > 0 && !values["allow-queued"]) reasons.push(`${stats.queued} job(s) queued - pass --allow-queued once the server runs a durable QueueAdapter, or they are lost on restart`);
|
|
168
|
+
if ((stats?.parked ?? 0) > 0 && values["allow-parked"]) notes.push(`${stats.parked} parked job(s): their queue-side records are the QueueAdapter's, not the SessionStore's - with the in-memory adapter they never finish`);
|
|
169
169
|
return {
|
|
170
170
|
reasons,
|
|
171
171
|
notes,
|
|
@@ -203,7 +203,7 @@ async function runGuard(argv) {
|
|
|
203
203
|
return 0;
|
|
204
204
|
}
|
|
205
205
|
if (Date.now() + intervalMs > deadline) {
|
|
206
|
-
report("busy
|
|
206
|
+
report("busy - not safe to restart", result);
|
|
207
207
|
return 1;
|
|
208
208
|
}
|
|
209
209
|
report(`busy, waiting up to ${Math.round((deadline - Date.now()) / 1e3)}s`, result);
|
|
@@ -213,4 +213,4 @@ async function runGuard(argv) {
|
|
|
213
213
|
//#endregion
|
|
214
214
|
export { runGuard as n, guard_exports as t };
|
|
215
215
|
|
|
216
|
-
//# sourceMappingURL=guard-
|
|
216
|
+
//# sourceMappingURL=guard-C4ybpzbt.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard-C4ybpzbt.mjs","names":[],"sources":["../src/lib/guard.ts"],"sourcesContent":["import { parseArgs } from 'node:util'\n\nconst BUSY_STATUSES = new Set(['starting', 'running', 'awaiting_approval'])\n\nconst HELP = `usage: workerdeck guard [--url URL] [--token TOKEN] [--header name=value]\n [--wait SECONDS] [--interval SECONDS]\n [--allow-parked] [--allow-queued] [--json]\n\nexit 0 = safe to restart, 1 = busy, 2 = could not tell\n`\n\ntype GetResult =\n | { ok: true; body: Record<string, unknown> }\n | { ok: false; status: 'unreachable' }\n | { ok: false; code?: number; detail: string }\n\ntype Verdict = {\n reasons: string[]\n notes: string[]\n unreachable?: boolean\n error?: string\n sessions?: number\n parked?: number\n}\n\nclass GuardError extends Error {}\n\nexport async function runGuard(argv: string[]): Promise<number> {\n let values: {\n url: string\n token?: string\n header: string[]\n wait: string\n interval: string\n 'allow-parked': boolean\n 'allow-queued': boolean\n json: boolean\n help: boolean\n }\n try {\n values = parseArgs({\n args: argv,\n options: {\n url: {\n type: 'string',\n default: process.env.WORKERDECK_URL ?? 'http://127.0.0.1:8787/v1',\n },\n token: { type: 'string', default: process.env.WORKERDECK_TOKEN },\n header: { type: 'string', multiple: true, default: [] },\n wait: { type: 'string', default: '0' },\n interval: { type: 'string', default: '5' },\n 'allow-parked': { type: 'boolean', default: false },\n 'allow-queued': { type: 'boolean', default: false },\n json: { type: 'boolean', default: false },\n help: { type: 'boolean', default: false },\n },\n }).values as typeof values\n } catch (error) {\n process.stderr.write(`guard: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 2\n }\n\n if (values.help) {\n process.stdout.write(HELP)\n return 0\n }\n\n const base = values.url.replace(/\\/$/, '')\n const headers: Record<string, string> = { accept: 'application/json' }\n if (values.token) {\n headers.authorization = `Bearer ${values.token}`\n }\n\n const seconds = (flag: string, raw: string): number => {\n const value = Number(raw)\n if (!Number.isFinite(value) || value < 0) {\n throw new GuardError(`${flag} must be a non-negative number of seconds`)\n }\n return value\n }\n\n let waitMs: number\n let intervalMs: number\n try {\n for (const entry of values.header) {\n const at = entry.indexOf('=')\n if (at < 1) {\n throw new GuardError(`--header must be name=value, got '${entry}'`)\n }\n headers[entry.slice(0, at).trim()] = entry.slice(at + 1).trim()\n }\n waitMs = seconds('--wait', values.wait) * 1000\n intervalMs = Math.max(seconds('--interval', values.interval), 1) * 1000\n } catch (error) {\n process.stderr.write(`guard: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 2\n }\n\n const get = async (path: string): Promise<GetResult> => {\n let res: Response\n try {\n res = await fetch(base + path, { headers })\n } catch (error) {\n // Nothing listening on the URL we were pointed at, so there is no session to lose.\n const code = (error as { cause?: { code?: string } })?.cause?.code\n if (code === 'ECONNREFUSED') {\n return { ok: false, status: 'unreachable' }\n }\n return { ok: false, detail: String(error instanceof Error ? error.message : error) }\n }\n if (!res.ok) {\n return { ok: false, code: res.status, detail: `HTTP ${res.status}` }\n }\n try {\n return { ok: true, body: (await res.json()) as Record<string, unknown> }\n } catch {\n return { ok: false, code: res.status, detail: 'response was not JSON' }\n }\n }\n\n const inspect = async (): Promise<Verdict> => {\n const sessions = await get('/sessions')\n if (!sessions.ok && 'status' in sessions) {\n return { unreachable: true, reasons: [], notes: [] }\n }\n if (!sessions.ok) {\n return { error: `GET ${base}/sessions → ${sessions.detail}`, reasons: [], notes: [] }\n }\n const listed = sessions.body.sessions\n if (!Array.isArray(listed)) {\n return { error: `GET ${base}/sessions returned no session list`, reasons: [], notes: [] }\n }\n const all = listed as { id: string; status: string }[]\n\n const reasons: string[] = []\n // Worth printing, but never worth blocking a deploy over.\n const notes: string[] = []\n for (const session of all.filter((s) => BUSY_STATUSES.has(s.status))) {\n reasons.push(`session ${session.id} is ${session.status}`)\n }\n const parked = all.filter((s) => s.status === 'parked')\n if (parked.length > 0 && !values['allow-parked']) {\n reasons.push(\n `${parked.length} parked session(s) - pass --allow-parked once the server ` +\n 'runs a durable SessionStore, or they are lost on restart',\n )\n }\n\n // A queue is optional, so a 404 here means this server declares none.\n const queue = await get('/queue')\n if (!queue.ok && !('status' in queue) && queue.code !== 404) {\n return { error: `GET ${base}/queue → ${queue.detail}`, reasons: [], notes: [] }\n }\n const stats = queue.ok ? (queue.body.stats as { running?: number; queued?: number; parked?: number } | undefined) : undefined\n if ((stats?.running ?? 0) > 0) {\n reasons.push(`${stats!.running} job(s) running`)\n }\n if ((stats?.queued ?? 0) > 0 && !values['allow-queued']) {\n reasons.push(\n `${stats!.queued} job(s) queued - pass --allow-queued once the server runs a ` +\n 'durable QueueAdapter, or they are lost on restart',\n )\n }\n if ((stats?.parked ?? 0) > 0 && values['allow-parked']) {\n notes.push(\n `${stats!.parked} parked job(s): their queue-side records are the QueueAdapter's, ` +\n \"not the SessionStore's - with the in-memory adapter they never finish\",\n )\n }\n return { reasons, notes, sessions: all.length, parked: parked.length }\n }\n\n const report = (verdict: string, detail: Verdict): void => {\n if (values.json) {\n process.stdout.write(`${JSON.stringify({ verdict, ...detail })}\\n`)\n return\n }\n const lines = [...detail.reasons.map((reason) => ` - ${reason}`), ...detail.notes.map((note) => ` note: ${note}`)].join('\\n')\n process.stdout.write(`guard: ${verdict}${lines ? `\\n${lines}` : ''}\\n`)\n }\n\n const deadline = Date.now() + waitMs\n for (;;) {\n const result = await inspect()\n if (result.error) {\n report('unknown', { ...result, reasons: [result.error] })\n return 2\n }\n if (result.unreachable) {\n report('safe to restart (nothing listening on the given URL)', result)\n return 0\n }\n if (result.reasons.length === 0) {\n report('safe to restart', result)\n return 0\n }\n if (Date.now() + intervalMs > deadline) {\n report('busy - not safe to restart', result)\n return 1\n }\n report(`busy, waiting up to ${Math.round((deadline - Date.now()) / 1000)}s`, result)\n await new Promise((resolve) => setTimeout(resolve, intervalMs))\n }\n}\n"],"mappings":"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"}
|
package/build/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as runGuard } from "./guard-
|
|
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-
|
|
1
|
+
import { n as runGuard } from "./guard-C4ybpzbt.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-BhSGGmoY.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 };
|
|
@@ -393,7 +393,7 @@ async function loadApnsKey(keyFile) {
|
|
|
393
393
|
} catch (error) {
|
|
394
394
|
throw new Error(`apns: ${keyFile} is not a private key (${error instanceof Error ? error.message : String(error)})`, { cause: error });
|
|
395
395
|
}
|
|
396
|
-
if (key.asymmetricKeyType !== "ec") throw new Error(`apns: ${keyFile} is a ${key.asymmetricKeyType ?? "unknown"} key, not EC
|
|
396
|
+
if (key.asymmetricKeyType !== "ec") throw new Error(`apns: ${keyFile} is a ${key.asymmetricKeyType ?? "unknown"} key, not EC - an APNs auth key is the .p8 downloaded from Keys in the developer portal, not a certificate`);
|
|
397
397
|
return key;
|
|
398
398
|
}
|
|
399
399
|
function createProviderToken(key, keyId, teamId) {
|
|
@@ -834,10 +834,10 @@ const COLLAPSE_PREFIX = {
|
|
|
834
834
|
};
|
|
835
835
|
function titleFor(notification, name) {
|
|
836
836
|
switch (notification.type) {
|
|
837
|
-
case "permission_requested": return `Approval needed
|
|
838
|
-
case "turn_completed": return notification.result?.isError === true ? `Turn failed
|
|
839
|
-
case "session_error": return `Session error
|
|
840
|
-
case "session_closed": return `Session ended
|
|
837
|
+
case "permission_requested": return `Approval needed - ${name}`;
|
|
838
|
+
case "turn_completed": return notification.result?.isError === true ? `Turn failed - ${name}` : name;
|
|
839
|
+
case "session_error": return `Session error - ${name}`;
|
|
840
|
+
case "session_closed": return `Session ended - ${name}`;
|
|
841
841
|
default: return name;
|
|
842
842
|
}
|
|
843
843
|
}
|
|
@@ -1241,7 +1241,7 @@ async function materializeAuthKey(stateDir, options = {}) {
|
|
|
1241
1241
|
if (key !== null) {
|
|
1242
1242
|
try {
|
|
1243
1243
|
const { mode } = await stat(path);
|
|
1244
|
-
if ((mode & 63) !== 0) warn(`auth key file ${path} is readable by other users (mode ${(mode & 511).toString(8)})
|
|
1244
|
+
if ((mode & 63) !== 0) warn(`auth key file ${path} is readable by other users (mode ${(mode & 511).toString(8)}) - run: chmod 600 ${path}`);
|
|
1245
1245
|
} catch {}
|
|
1246
1246
|
return {
|
|
1247
1247
|
key,
|
|
@@ -1295,7 +1295,7 @@ async function createAuthSessionStore(options) {
|
|
|
1295
1295
|
initial = parseSessions(await readFile(path, "utf8"), now());
|
|
1296
1296
|
try {
|
|
1297
1297
|
const { mode } = await stat(path);
|
|
1298
|
-
if ((mode & 63) !== 0) warn(`session file ${path} is readable by other users (mode ${(mode & 511).toString(8)})
|
|
1298
|
+
if ((mode & 63) !== 0) warn(`session file ${path} is readable by other users (mode ${(mode & 511).toString(8)}) - run: chmod 600 ${path}`);
|
|
1299
1299
|
} catch {}
|
|
1300
1300
|
} catch {
|
|
1301
1301
|
initial = [];
|
|
@@ -1353,7 +1353,7 @@ function sha256(value) {
|
|
|
1353
1353
|
function createCliAuth(options = {}) {
|
|
1354
1354
|
const { secret } = options;
|
|
1355
1355
|
const enabled = secret !== void 0;
|
|
1356
|
-
if (secret !== void 0 && secret.length < MIN_SECRET_LENGTH) throw new Error(`createCliAuth: secret must be at least ${MIN_SECRET_LENGTH} characters
|
|
1356
|
+
if (secret !== void 0 && secret.length < MIN_SECRET_LENGTH) throw new Error(`createCliAuth: secret must be at least ${MIN_SECRET_LENGTH} characters - use a long random value, or leave it unset to run without auth on loopback`);
|
|
1357
1357
|
const cookieName = options.cookieName ?? DEFAULT_COOKIE_NAME;
|
|
1358
1358
|
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
1359
1359
|
if (!(ttlMs > 0)) throw new Error("createCliAuth: ttlMs must be positive");
|
|
@@ -1666,7 +1666,7 @@ var ConfigError = class extends Error {};
|
|
|
1666
1666
|
function parseDuration(raw, source) {
|
|
1667
1667
|
if (raw === "none" || raw === "never") return null;
|
|
1668
1668
|
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(raw);
|
|
1669
|
-
if (!match) throw new ConfigError(`${source}: expected a duration like 300000, 30s, 5m
|
|
1669
|
+
if (!match) throw new ConfigError(`${source}: expected a duration like 300000, 30s, 5m - or 'none'; got: ${raw}`);
|
|
1670
1670
|
const scale = {
|
|
1671
1671
|
ms: 1,
|
|
1672
1672
|
s: 1e3,
|
|
@@ -1866,7 +1866,7 @@ function normalizeInsecureHost(raw) {
|
|
|
1866
1866
|
} catch {
|
|
1867
1867
|
throw new ConfigError(`not a host name or address in insecureHosts: ${JSON.stringify(raw)}`);
|
|
1868
1868
|
}
|
|
1869
|
-
if (url.port !== "") throw new ConfigError(`insecureHosts entry ${JSON.stringify(raw)} carries a port
|
|
1869
|
+
if (url.port !== "") throw new ConfigError(`insecureHosts entry ${JSON.stringify(raw)} carries a port - name the host alone; the bind host and the Host header are both compared portless`);
|
|
1870
1870
|
return url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
1871
1871
|
}
|
|
1872
1872
|
function resolveInstanceConfig(flags, loaded, env = process.env, cwd = process.cwd()) {
|
|
@@ -2284,8 +2284,8 @@ async function startInstance(config, options = {}) {
|
|
|
2284
2284
|
...authOptions,
|
|
2285
2285
|
sessions
|
|
2286
2286
|
} : authOptions);
|
|
2287
|
-
if (config.allowedHosts === null && !config.hostAuthenticates && !auth.enabled) throw new Error("refusing to serve: the resolved config expects auth but no shared secret was materialized
|
|
2288
|
-
if (config.corsOrigins.length > 0 && !config.hostAuthenticates && !auth.enabled) throw new Error("refusing to serve: corsOrigins is set but this instance has no auth
|
|
2287
|
+
if (config.allowedHosts === null && !config.hostAuthenticates && !auth.enabled) throw new Error("refusing to serve: the resolved config expects auth but no shared secret was materialized - this instance would be open while believing itself authenticated");
|
|
2288
|
+
if (config.corsOrigins.length > 0 && !config.hostAuthenticates && !auth.enabled) throw new Error("refusing to serve: corsOrigins is set but this instance has no auth - a cross-origin page could drive it with no credential. Set --auth-key.");
|
|
2289
2289
|
const hostAllowed = createHostGuard(config.allowedHosts);
|
|
2290
2290
|
const authenticateHost = (req) => hostAllowed(req) ? auth.authenticate(req) : null;
|
|
2291
2291
|
const apns = config.apns === void 0 ? void 0 : await createApnsForwarder({
|
|
@@ -2349,21 +2349,21 @@ async function startInstance(config, options = {}) {
|
|
|
2349
2349
|
if (config.hostAuthenticates) line(" auth: the config file supplies its own `authenticate`");
|
|
2350
2350
|
else if (generated?.source === "created") {
|
|
2351
2351
|
line(` auth: generated key ${generated.key}`);
|
|
2352
|
-
line(` stored in ${generated.path}
|
|
2352
|
+
line(` stored in ${generated.path} - later starts reuse it without printing it`);
|
|
2353
2353
|
} else if (generated?.source === "ephemeral") {
|
|
2354
2354
|
line(` auth: generated key ${generated.key}`);
|
|
2355
|
-
line(" ephemeral
|
|
2355
|
+
line(" ephemeral - no state dir to keep it, so the next start mints a new one");
|
|
2356
2356
|
} else if (generated?.source === "stored") line(` auth: shared key from ${generated.path}`);
|
|
2357
|
-
else if (auth.enabled) line(" auth: shared key
|
|
2358
|
-
else line(" NO AUTH
|
|
2359
|
-
if (!config.web) line(" dashboard: off
|
|
2360
|
-
if (!config.keepAwake) line(" keep-awake: off
|
|
2357
|
+
else if (auth.enabled) line(" auth: shared key - browsers sign in, services send a header");
|
|
2358
|
+
else line(" NO AUTH - anyone who can reach this port gets a session");
|
|
2359
|
+
if (!config.web) line(" dashboard: off - bare gateway, /v1 and /auth only");
|
|
2360
|
+
if (!config.keepAwake) line(" keep-awake: off - this machine may sleep mid-turn");
|
|
2361
2361
|
if (config.corsOrigins.length) line(` cors: ${config.corsOrigins.join(", ")} may call /v1 (still key-gated)`);
|
|
2362
|
-
line(config.stateDir ? ` parked sessions persist in ${join(config.stateDir, "parked")}` : " parked sessions are in memory only
|
|
2362
|
+
line(config.stateDir ? ` parked sessions persist in ${join(config.stateDir, "parked")}` : " parked sessions are in memory only - a restart drops them");
|
|
2363
2363
|
if (apns) {
|
|
2364
2364
|
const count = apns.deviceCount();
|
|
2365
2365
|
const cards = apns.activity.count();
|
|
2366
|
-
line(` push: APNs forwarder on ${config.apns?.topic}
|
|
2366
|
+
line(` push: APNs forwarder on ${config.apns?.topic} - ${count === 0 ? "no devices registered yet" : `${count} device(s)`}${cards === 0 ? "" : `, ${cards} live card(s)`}`);
|
|
2367
2367
|
}
|
|
2368
2368
|
if (config.configPath) line(` config ${config.configPath}`);
|
|
2369
2369
|
line("");
|
|
@@ -2389,4 +2389,4 @@ async function startInstance(config, options = {}) {
|
|
|
2389
2389
|
//#endregion
|
|
2390
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 };
|
|
2391
2391
|
|
|
2392
|
-
//# sourceMappingURL=instance-
|
|
2392
|
+
//# sourceMappingURL=instance-BhSGGmoY.mjs.map
|