workerdeck 0.0.0 → 0.6.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/LICENSE +21 -0
- package/README.md +98 -9
- package/build/cli.d.mts +1 -0
- package/build/cli.mjs +122 -0
- package/build/cli.mjs.map +1 -0
- package/build/guard-D3VN855w.mjs +241 -0
- package/build/guard-D3VN855w.mjs.map +1 -0
- package/build/index.d.mts +286 -0
- package/build/index.mjs +3 -0
- package/build/instance-kupxU5UD.mjs +1019 -0
- package/build/instance-kupxU5UD.mjs.map +1 -0
- package/package.json +55 -12
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { Authenticator, WorkerServer, WorkerServerOptions } from "@workerdeck/server";
|
|
2
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import { ProfileInfo } from "@workerdeck/protocol";
|
|
4
|
+
|
|
5
|
+
//#region src/auth.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Gateway auth for the turnkey CLI: one shared operator secret, two transports.
|
|
8
|
+
*
|
|
9
|
+
* Services present the secret itself on every request (`x-workerdeck-key`,
|
|
10
|
+
* or `Authorization: Bearer`). The dashboard cannot: the SPA is prebuilt to
|
|
11
|
+
* call `location.origin + '/v1'` with no headers, and a browser WebSocket
|
|
12
|
+
* handshake carries no custom headers at all. So browsers POST the secret once
|
|
13
|
+
* to `/auth/login`, get an HttpOnly cookie naming a server-side session, and
|
|
14
|
+
* the cookie rides same-origin REST and the WS upgrade automatically. That
|
|
15
|
+
* automatic ride is also the threat: the cookie is ambient authority, and the
|
|
16
|
+
* WS handshake is exempt from CORS, so cross-origin misuse is fenced off by an
|
|
17
|
+
* explicit Origin check here — not by the browser.
|
|
18
|
+
*
|
|
19
|
+
* This file guards the operator's own gateway and nothing else. It never sees
|
|
20
|
+
* an Anthropic credential — those are resolved by the SDK/CLI from the
|
|
21
|
+
* operator's environment (root CLAUDE.md, auth red lines).
|
|
22
|
+
*/
|
|
23
|
+
type CliAuthOptions = {
|
|
24
|
+
/**
|
|
25
|
+
* The shared operator secret. Unset disables auth entirely (the CLI then
|
|
26
|
+
* refuses to bind anything but loopback — enforced by the caller, not here).
|
|
27
|
+
* An empty or short value is a config accident, not a choice: anything under
|
|
28
|
+
* 12 characters throws rather than standing up a guessable gateway.
|
|
29
|
+
*/
|
|
30
|
+
secret?: string;
|
|
31
|
+
/** Default 'workerdeck_session'. No `__Host-` prefix — it requires
|
|
32
|
+
* `Secure`, and plain-HTTP localhost is the primary deployment. */
|
|
33
|
+
cookieName?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Browser session lifetime, default 7 days. Fixed, not sliding: the auth
|
|
36
|
+
* hooks only see the request, so a renewed cookie has nowhere to ride — and
|
|
37
|
+
* for a dashboard whose whole login is retyping one secret, a periodic
|
|
38
|
+
* re-login is cheaper than a refresh endpoint. Expiry (or a process restart —
|
|
39
|
+
* sessions are in-memory by design) simply lands the operator back on the
|
|
40
|
+
* login page.
|
|
41
|
+
*/
|
|
42
|
+
ttlMs?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Trust `x-forwarded-proto` / `x-forwarded-host` / `x-forwarded-for` from
|
|
45
|
+
* exactly one reverse proxy in front of this process; the *last* value of
|
|
46
|
+
* each is used (the one the proxy set, the only position a client cannot
|
|
47
|
+
* forge). Off by default: these are attacker-writable headers on a directly
|
|
48
|
+
* exposed port. Behind a TLS-terminating proxy this must be on, or the
|
|
49
|
+
* `Secure` cookie flag is skipped and the Origin check computes `http://`
|
|
50
|
+
* where the browser says `https://` and rejects the dashboard's own writes.
|
|
51
|
+
*/
|
|
52
|
+
trustProxy?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Origins accepted in addition to the request's own (scheme + Host). Needed
|
|
55
|
+
* when the proxy rewrites Host so the external origin no longer matches what
|
|
56
|
+
* this process sees. Entries must be full origins ('https://ops.example.com');
|
|
57
|
+
* invalid ones throw at startup rather than silently never matching.
|
|
58
|
+
*/
|
|
59
|
+
allowedOrigins?: string[];
|
|
60
|
+
/** Login throttle tuning; defaults: 15 min window, 10 failures per IP, 100
|
|
61
|
+
* globally. Exposed mainly so tests need not wait out real windows. */
|
|
62
|
+
throttle?: {
|
|
63
|
+
windowMs?: number;
|
|
64
|
+
maxFailuresPerIp?: number;
|
|
65
|
+
maxFailuresGlobal?: number;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
/** What `authenticate` hands the worker server as the request principal. */
|
|
69
|
+
type CliPrincipal = {
|
|
70
|
+
/** Which transport authenticated the request; 'open' when auth is disabled. */via: 'header' | 'cookie' | 'open';
|
|
71
|
+
/** One secret, one trust level: whoever holds it is the operator, so the
|
|
72
|
+
* dashboard may manage provider profiles (still bounded by the server's
|
|
73
|
+
* `allowedConfigDirRoots`). */
|
|
74
|
+
canManageProfiles: true;
|
|
75
|
+
};
|
|
76
|
+
type CliAuth = {
|
|
77
|
+
enabled: boolean;
|
|
78
|
+
/** Hand straight to `createWorkerServer({ authenticate })` — it guards both
|
|
79
|
+
* REST and the WS upgrade, which is exactly why the Origin policy lives in it. */
|
|
80
|
+
authenticate: Authenticator;
|
|
81
|
+
/** Claims `/auth` and everything under it (login/logout/status); returns true
|
|
82
|
+
* when it consumed the request. The static host must call this first. */
|
|
83
|
+
handleAuthRequest(req: IncomingMessage, res: ServerResponse): boolean | Promise<boolean>;
|
|
84
|
+
/** Cookie-only check for the static host: login page or SPA? Gating the SPA
|
|
85
|
+
* shell is UX, not security — every byte of data sits behind `authenticate`. */
|
|
86
|
+
hasValidSession(req: IncomingMessage): boolean;
|
|
87
|
+
/** What the login page needs to render for this request. The endpoint path,
|
|
88
|
+
* field name, and the `?auth=` redirect params are all this module's wire
|
|
89
|
+
* format, so the page learns them here instead of hardcoding them. */
|
|
90
|
+
loginPage(req: IncomingMessage): {
|
|
91
|
+
action: string;
|
|
92
|
+
field: string;
|
|
93
|
+
error?: string;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
declare function createCliAuth(options?: CliAuthOptions): CliAuth;
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/config.d.ts
|
|
99
|
+
/**
|
|
100
|
+
* What a `workerdeck.config.mjs` default-exports: the server options, plus
|
|
101
|
+
* the few instance-level settings that aren't the server's business. Keeping
|
|
102
|
+
* them in one object means a deployment is one file, not a file plus a
|
|
103
|
+
* memorised command line.
|
|
104
|
+
*/
|
|
105
|
+
type WorkerDeckConfig = WorkerServerOptions & {
|
|
106
|
+
port?: number;
|
|
107
|
+
host?: string; /** Built-in shared-secret auth. Ignored entirely if you supply `authenticate`. */
|
|
108
|
+
auth?: CliAuthOptions; /** Where parked sessions are persisted; null disables durable parking. */
|
|
109
|
+
stateDir?: string | null;
|
|
110
|
+
/**
|
|
111
|
+
* Host header values accepted when running *without* auth. Defaults to the
|
|
112
|
+
* loopback names; see `resolveInstanceConfig` for why this exists at all.
|
|
113
|
+
*/
|
|
114
|
+
allowedHosts?: string[];
|
|
115
|
+
/**
|
|
116
|
+
* Bind hosts that may serve without auth. One declaration, two effects:
|
|
117
|
+
* binding a listed host waives the auth requirement (no key demanded, none
|
|
118
|
+
* generated), and while unauthenticated every entry is also accepted as a
|
|
119
|
+
* Host header, so the operator states the intent once. Entries name a host,
|
|
120
|
+
* never an endpoint — a port is rejected — and match the bind host literally
|
|
121
|
+
* and case-insensitively: nothing is inferred from DNS or the network, and
|
|
122
|
+
* `0.0.0.0` means the all-interfaces bind itself, not "any host". When auth
|
|
123
|
+
* is on this widens nothing.
|
|
124
|
+
*/
|
|
125
|
+
insecureHosts?: string[]; /** Serve a dashboard build from here instead of the bundled one. */
|
|
126
|
+
webRoot?: string;
|
|
127
|
+
};
|
|
128
|
+
type CliFlags = {
|
|
129
|
+
config?: string;
|
|
130
|
+
port?: number;
|
|
131
|
+
host?: string;
|
|
132
|
+
authKey?: string;
|
|
133
|
+
profiles: ProfileInfo[];
|
|
134
|
+
cwdRoots: string[];
|
|
135
|
+
allowedOrigins: string[];
|
|
136
|
+
allowedHosts: string[];
|
|
137
|
+
insecureHosts: string[];
|
|
138
|
+
trustProxy?: boolean;
|
|
139
|
+
stateDir?: string;
|
|
140
|
+
parking?: boolean;
|
|
141
|
+
insecure?: boolean;
|
|
142
|
+
open?: boolean;
|
|
143
|
+
help?: boolean;
|
|
144
|
+
version?: boolean;
|
|
145
|
+
};
|
|
146
|
+
declare class ConfigError extends Error {}
|
|
147
|
+
/**
|
|
148
|
+
* Hand-rolled rather than a dependency: the CLI's whole value is that `npx
|
|
149
|
+
* workerdeck` pulls down a small tree, and an arg parser is a hundred lines
|
|
150
|
+
* of it.
|
|
151
|
+
*/
|
|
152
|
+
declare function parseArgs(argv: string[]): CliFlags;
|
|
153
|
+
type LoadedConfig = {
|
|
154
|
+
/** Absolute path of the file that was loaded, or null if there wasn't one. */path: string | null;
|
|
155
|
+
options: WorkerDeckConfig;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Explicit `--config` must exist — a typo that silently starts a default
|
|
159
|
+
* instance is worse than a failure. An implicit one is looked up in cwd only:
|
|
160
|
+
* walking parent directories would make what a given command does depend on
|
|
161
|
+
* where it was run from.
|
|
162
|
+
*/
|
|
163
|
+
declare function loadConfigFile(explicit?: string, cwd?: string): Promise<LoadedConfig>;
|
|
164
|
+
declare function isLoopback(host: string): boolean;
|
|
165
|
+
type ResolvedConfig = {
|
|
166
|
+
port: number;
|
|
167
|
+
host: string; /** Shared secret, or undefined for an unauthenticated instance. */
|
|
168
|
+
authKey?: string; /** Everything else the built-in auth takes (proxy trust, extra origins). */
|
|
169
|
+
auth: CliAuthOptions; /** Where parked sessions and other instance state live; null disables durable parking. */
|
|
170
|
+
stateDir: string | null;
|
|
171
|
+
configPath: string | null; /** True when the config file supplied its own `authenticate` — built-in auth stands down. */
|
|
172
|
+
hostAuthenticates: boolean;
|
|
173
|
+
/**
|
|
174
|
+
* Auth is required here but no key was supplied: `startInstance` must
|
|
175
|
+
* materialize one (stored under `stateDir`, ephemeral without one). This is a
|
|
176
|
+
* *promise* rather than a key because resolution is pure and synchronous while
|
|
177
|
+
* reading a key file is I/O — and the promise is load-bearing: `allowedHosts`
|
|
178
|
+
* is already null on the strength of it, so `startInstance` refuses to serve
|
|
179
|
+
* if materialization ever fails to arm the built-in auth.
|
|
180
|
+
*/
|
|
181
|
+
generateAuthKey: boolean;
|
|
182
|
+
/**
|
|
183
|
+
* Host header values to accept, or null to accept any. Non-null only for an
|
|
184
|
+
* unauthenticated instance — see `resolveInstanceConfig`.
|
|
185
|
+
*/
|
|
186
|
+
allowedHosts: Set<string> | null; /** Dashboard build to serve; resolved from the package when unset. */
|
|
187
|
+
webRoot?: string;
|
|
188
|
+
open: boolean;
|
|
189
|
+
options: WorkerServerOptions;
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Durable parking is on by default because this is a long-lived instance: a
|
|
193
|
+
* turnkey tool that silently drops parked work on every restart is the wrong
|
|
194
|
+
* default. The store writes whole transcripts in plaintext, so it goes beside
|
|
195
|
+
* the config file (or under the home directory) rather than anywhere temporary,
|
|
196
|
+
* and one directory serves exactly one instance — the store is single-process
|
|
197
|
+
* by design, which the single-port model already implies.
|
|
198
|
+
*/
|
|
199
|
+
declare function defaultStateDir(configPath: string | null): string;
|
|
200
|
+
/** Hostname out of a Host header, minus the port and any IPv6 brackets. */
|
|
201
|
+
declare function hostnameOf(hostHeader: string): string;
|
|
202
|
+
/** 127.0.0.0/8, ::1, and the names that mean them. */
|
|
203
|
+
declare function isLoopbackHostname(hostname: string): boolean;
|
|
204
|
+
declare function resolveInstanceConfig(flags: CliFlags, loaded: LoadedConfig, env?: NodeJS.ProcessEnv, cwd?: string): ResolvedConfig;
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/instance.d.ts
|
|
207
|
+
type Instance = {
|
|
208
|
+
server: WorkerServer;
|
|
209
|
+
url: string;
|
|
210
|
+
port: number; /** Resolves when the instance stops serving. */
|
|
211
|
+
closed: Promise<void>;
|
|
212
|
+
close: () => Promise<void>;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* The dashboard comes from `@workerdeck/web`, which ships it prebuilt and
|
|
216
|
+
* exports the path to it. Depending on the package rather than vendoring a copy
|
|
217
|
+
* means one dashboard, versioned in lockstep with everything else.
|
|
218
|
+
*
|
|
219
|
+
* In a checkout that directory only exists once the app has been built — dev
|
|
220
|
+
* never builds — so the miss is worth a real message rather than a stack trace
|
|
221
|
+
* from the static host.
|
|
222
|
+
*/
|
|
223
|
+
declare function resolveWebRoot(): string;
|
|
224
|
+
/**
|
|
225
|
+
* The Host-header gate for an unauthenticated instance. `allowedHosts` is null
|
|
226
|
+
* whenever auth is on, and then this is the identity function — with a
|
|
227
|
+
* credential in play a rebound origin holds no cookie and fails `authenticate`
|
|
228
|
+
* anyway. Loopback *names* are what's checked, not the socket: the attacker in
|
|
229
|
+
* this scenario controls DNS, so the connection genuinely arrives on 127.0.0.1;
|
|
230
|
+
* what they cannot control is the name the victim's browser writes into Host.
|
|
231
|
+
*/
|
|
232
|
+
declare function createHostGuard(allowedHosts: Set<string> | null): (req: IncomingMessage) => boolean;
|
|
233
|
+
declare function startInstance(config: ResolvedConfig, options?: {
|
|
234
|
+
quiet?: boolean;
|
|
235
|
+
}): Promise<Instance>;
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/guard.d.ts
|
|
238
|
+
declare function runGuard(argv: string[]): Promise<number>;
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/auth-key.d.ts
|
|
241
|
+
/**
|
|
242
|
+
* Materializes the key that `resolveInstanceConfig` promised via
|
|
243
|
+
* `generateAuthKey`: resolution is pure and synchronous, reading a key file is
|
|
244
|
+
* I/O, so the two halves meet here at startup. The contract is that this
|
|
245
|
+
* function returns a usable secret or throws — it never returns "no key",
|
|
246
|
+
* because the resolved config has already stood down the Host-header guard on
|
|
247
|
+
* the strength of the promise, and a silent miss would serve an open gateway
|
|
248
|
+
* that reports itself authenticated.
|
|
249
|
+
*
|
|
250
|
+
* The key persists under `stateDir` so a restart does not un-pair every client
|
|
251
|
+
* that stored it (the iOS app keeps it in its keychain). No `stateDir` means
|
|
252
|
+
* nothing durable to write, so the key is ephemeral per run — the banner says
|
|
253
|
+
* so. This is the gateway's own operator secret and nothing else: Anthropic
|
|
254
|
+
* credentials never pass through here (root CLAUDE.md, auth red lines).
|
|
255
|
+
*/
|
|
256
|
+
type MaterializedAuthKey = {
|
|
257
|
+
key: string; /** 'stored' reused the file, 'created' wrote a new one, 'ephemeral' had nowhere to write. */
|
|
258
|
+
source: 'stored' | 'created' | 'ephemeral'; /** Where the key lives, or null when ephemeral. */
|
|
259
|
+
path: string | null;
|
|
260
|
+
};
|
|
261
|
+
declare function materializeAuthKey(stateDir: string | null, options?: {
|
|
262
|
+
warn?: (message: string) => void;
|
|
263
|
+
}): Promise<MaterializedAuthKey>;
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region src/login-page.d.ts
|
|
266
|
+
/**
|
|
267
|
+
* The login page is the CLI's, not the dashboard's. That split is deliberate:
|
|
268
|
+
* the SPA ships prebuilt and is also served straight from vite in dev, so making
|
|
269
|
+
* it aware of an auth scheme that only exists in the turnkey instance would
|
|
270
|
+
* couple two things that are otherwise independent. An unauthenticated document
|
|
271
|
+
* request gets this instead of index.html; nothing in the SPA changes.
|
|
272
|
+
*
|
|
273
|
+
* Self-contained by necessity — it renders before any bundled asset is worth
|
|
274
|
+
* fetching, and it must not depend on the app it is gating.
|
|
275
|
+
*/
|
|
276
|
+
type LoginPageOptions = {
|
|
277
|
+
/** Where the form POSTs. Comes from the auth module, not hardcoded here. */action: string; /** Form field name carrying the secret. */
|
|
278
|
+
field: string; /** Shown when a previous attempt failed. */
|
|
279
|
+
error?: string; /** Where to send the browser after a successful login. */
|
|
280
|
+
redirectTo?: string; /** Field name carrying the post-login redirect. */
|
|
281
|
+
redirectField?: string;
|
|
282
|
+
};
|
|
283
|
+
declare function renderLoginPage(options: LoginPageOptions): string;
|
|
284
|
+
//#endregion
|
|
285
|
+
export { type CliAuth, type CliAuthOptions, type CliFlags, type CliPrincipal, ConfigError, type Instance, type LoadedConfig, type LoginPageOptions, type MaterializedAuthKey, type ResolvedConfig, type WorkerDeckConfig, createCliAuth, createHostGuard, defaultStateDir, hostnameOf, isLoopback, isLoopbackHostname, loadConfigFile, materializeAuthKey, parseArgs, renderLoginPage, resolveInstanceConfig, resolveWebRoot, runGuard, startInstance };
|
|
286
|
+
//# sourceMappingURL=index.d.mts.map
|
package/build/index.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { n as runGuard } from "./guard-D3VN855w.mjs";
|
|
2
|
+
import { a as ConfigError, c as isLoopback, d as parseArgs, f as resolveInstanceConfig, i as renderLoginPage, l as isLoopbackHostname, m as materializeAuthKey, n as resolveWebRoot, o as defaultStateDir, p as createCliAuth, r as startInstance, s as hostnameOf, t as createHostGuard, u as loadConfigFile } from "./instance-kupxU5UD.mjs";
|
|
3
|
+
export { ConfigError, createCliAuth, createHostGuard, defaultStateDir, hostnameOf, isLoopback, isLoopbackHostname, loadConfigFile, materializeAuthKey, parseArgs, renderLoginPage, resolveInstanceConfig, resolveWebRoot, runGuard, startInstance };
|