harness-dispatch 0.7.8 → 0.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/CHANGELOG.md +706 -1
- package/README.md +35 -5
- package/config.default.yaml +30 -2
- package/dist/auth.d.ts +29 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +58 -5
- package/dist/auth.js.map +1 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +449 -21
- package/dist/bin.js.map +1 -1
- package/dist/breaker-store.d.ts +25 -1
- package/dist/breaker-store.d.ts.map +1 -1
- package/dist/breaker-store.js +193 -25
- package/dist/breaker-store.js.map +1 -1
- package/dist/client-register.d.ts +147 -0
- package/dist/client-register.d.ts.map +1 -0
- package/dist/client-register.js +380 -0
- package/dist/client-register.js.map +1 -0
- package/dist/config/validation.d.ts +29 -0
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +88 -0
- package/dist/config/validation.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +55 -2
- package/dist/config.js.map +1 -1
- package/dist/dispatch-log.d.ts +14 -0
- package/dist/dispatch-log.d.ts.map +1 -1
- package/dist/dispatch-log.js +3 -0
- package/dist/dispatch-log.js.map +1 -1
- package/dist/dispatchers/generic-cli.d.ts.map +1 -1
- package/dist/dispatchers/generic-cli.js +35 -0
- package/dist/dispatchers/generic-cli.js.map +1 -1
- package/dist/dispatchers/openai-compatible.d.ts +20 -4
- package/dist/dispatchers/openai-compatible.d.ts.map +1 -1
- package/dist/dispatchers/openai-compatible.js +145 -17
- package/dist/dispatchers/openai-compatible.js.map +1 -1
- package/dist/dispatchers/shared/rate-limit-headers.d.ts +4 -2
- package/dist/dispatchers/shared/rate-limit-headers.d.ts.map +1 -1
- package/dist/dispatchers/shared/rate-limit-headers.js +4 -2
- package/dist/dispatchers/shared/rate-limit-headers.js.map +1 -1
- package/dist/http/answer-stream.d.ts +52 -0
- package/dist/http/answer-stream.d.ts.map +1 -0
- package/dist/http/answer-stream.js +47 -0
- package/dist/http/answer-stream.js.map +1 -0
- package/dist/http/parse.d.ts.map +1 -1
- package/dist/http/parse.js +41 -0
- package/dist/http/parse.js.map +1 -1
- package/dist/http/server.d.ts.map +1 -1
- package/dist/http/server.js +102 -9
- package/dist/http/server.js.map +1 -1
- package/dist/jobs/store.d.ts.map +1 -1
- package/dist/jobs/store.js +45 -4
- package/dist/jobs/store.js.map +1 -1
- package/dist/jobs.d.ts +24 -0
- package/dist/jobs.d.ts.map +1 -1
- package/dist/jobs.js +101 -1
- package/dist/jobs.js.map +1 -1
- package/dist/mcp/config-hot-reload.d.ts.map +1 -1
- package/dist/mcp/config-hot-reload.js +10 -0
- package/dist/mcp/config-hot-reload.js.map +1 -1
- package/dist/mcp/near-miss-guard.d.ts +45 -0
- package/dist/mcp/near-miss-guard.d.ts.map +1 -0
- package/dist/mcp/near-miss-guard.js +98 -0
- package/dist/mcp/near-miss-guard.js.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +36 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-schemas.d.ts.map +1 -1
- package/dist/mcp/tool-schemas.js +21 -4
- package/dist/mcp/tool-schemas.js.map +1 -1
- package/dist/mcp/tools.d.ts +32 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +41 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp-clients.d.ts +63 -0
- package/dist/mcp-clients.d.ts.map +1 -0
- package/dist/mcp-clients.js +117 -0
- package/dist/mcp-clients.js.map +1 -0
- package/dist/near-miss.d.ts +63 -0
- package/dist/near-miss.d.ts.map +1 -0
- package/dist/near-miss.js +132 -0
- package/dist/near-miss.js.map +1 -0
- package/dist/quota.d.ts +31 -1
- package/dist/quota.d.ts.map +1 -1
- package/dist/quota.js +68 -6
- package/dist/quota.js.map +1 -1
- package/dist/route-policy.d.ts +10 -0
- package/dist/route-policy.d.ts.map +1 -1
- package/dist/route-policy.js +20 -2
- package/dist/route-policy.js.map +1 -1
- package/dist/router.d.ts +61 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +165 -29
- package/dist/router.js.map +1 -1
- package/dist/safety.d.ts.map +1 -1
- package/dist/safety.js +31 -1
- package/dist/safety.js.map +1 -1
- package/dist/status.d.ts +30 -21
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +152 -8
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +43 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/working-dir.d.ts.map +1 -1
- package/dist/working-dir.js +16 -0
- package/dist/working-dir.js.map +1 -1
- package/dist/workspace-resolve.d.ts.map +1 -1
- package/dist/workspace-resolve.js +119 -15
- package/dist/workspace-resolve.js.map +1 -1
- package/dist/workspaces.d.ts +83 -0
- package/dist/workspaces.d.ts.map +1 -1
- package/dist/workspaces.js +288 -15
- package/dist/workspaces.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -55,14 +55,24 @@ timeout — or to the server itself restarting mid-run.**
|
|
|
55
55
|
|
|
56
56
|
Needs Node.js `>=22.22.2` (so current LTS works) and at least one harness or endpoint.
|
|
57
57
|
|
|
58
|
+
`git` is optional but recommended: dispatch works without it, but the
|
|
59
|
+
`workspace` tool shells out to git to diff and apply an isolated run's changes,
|
|
60
|
+
and the `git_worktree` isolation policy needs it. `doctor` reports whether it
|
|
61
|
+
found one.
|
|
62
|
+
|
|
58
63
|
```bash
|
|
59
64
|
npm install -g harness-dispatch
|
|
60
65
|
harness-dispatch configure --yes
|
|
61
66
|
harness-dispatch doctor --live
|
|
62
67
|
```
|
|
63
68
|
|
|
64
|
-
`configure --yes` detects installed harnesses
|
|
65
|
-
|
|
69
|
+
`configure --yes` detects installed harnesses, writes `config.yaml`, and then
|
|
70
|
+
offers to register this server with each MCP client it finds (Claude Code,
|
|
71
|
+
Cursor) — showing you what it would write, and what is already there, before
|
|
72
|
+
changing anything. `--no-clients` skips the offer and prints a snippet to paste
|
|
73
|
+
instead; `harness-dispatch connect` does the same registration later on its own,
|
|
74
|
+
and `connect --remove` undoes it. Without
|
|
75
|
+
`--yes` configure previews and writes nothing. `doctor` then checks the whole chain:
|
|
66
76
|
binary, config load, harness detection, auth and billing classification, route
|
|
67
77
|
readiness. `--live` goes further and routes one tiny real prompt, so you see a
|
|
68
78
|
completion before wiring anything into your agent. The live probe never touches paid or
|
|
@@ -343,6 +353,10 @@ set `billing_kind:` / `paid_usage_possible:` explicitly once you know it.
|
|
|
343
353
|
harness-dispatch # stdio MCP
|
|
344
354
|
harness-dispatch configure # detect harnesses and prepare config
|
|
345
355
|
harness-dispatch configure --print # inspect generated config YAML
|
|
356
|
+
harness-dispatch connect # register with the MCP clients you have
|
|
357
|
+
harness-dispatch connect --clients cursor # no prompt; ids from the listing it prints
|
|
358
|
+
harness-dispatch connect --dev # point clients at THIS checkout's build
|
|
359
|
+
harness-dispatch connect --remove # take the entry back out
|
|
346
360
|
harness-dispatch doctor # validate install, auth, config, and routes
|
|
347
361
|
harness-dispatch doctor --live # run one eligible live routed probe
|
|
348
362
|
harness-dispatch doctor --live --allow-paid
|
|
@@ -371,7 +385,7 @@ version bump.
|
|
|
371
385
|
| `dispatch` | Always starts new routed coding work — one task to the best-fit harness, or a fanout to several for independent opinions. Every call runs as a background job from the first moment: a fast task returns its full result inline (`completed: true`), a slow one returns `completed: false` plus a `jobId` to check on. Nothing is ever lost to a timeout — including the MCP call's own. |
|
|
372
386
|
| `job_status` | Checks work started by `dispatch`. Pass the `jobId` it returned to get a `partialOutput` tail while running and the full `result` once done; omit `jobId` to list recent background dispatches (compact, newest first). |
|
|
373
387
|
| `cancel_job` | Stops work started by `dispatch` — a wrong turn, a wrong directory, a superseded run. A job still waiting for a slot stops outright; a running one tears down within about a second (poll `job_status` to see it land), killing the agent CLI and its children. Files it already changed are **not** reverted, and a cancelled run is not counted as a route failure. |
|
|
374
|
-
| `retry_job` | Re-runs a finished job's task from its own record — same prompt (as the delegate saw it), files, working directory, hints and workspace policy. Pass `service` to send the retry to a different route, which is the usual reason to retry: the task was fine and the route was not
|
|
388
|
+
| `retry_job` | Re-runs a finished job's task from its own record — same prompt (as the delegate saw it), files, working directory, hints and workspace policy. Pass `service` to send the retry to a different route, which is the usual reason to retry: the task was fine and the route was not — the original's model is left behind when the new route does not declare it, reported as `droppedModel`. Returns a new jobId; the original is untouched. |
|
|
375
389
|
| `workspace` | For a job that ran with `workspacePolicy: "copy"` or `"git_worktree"`, the agent's changes live in an isolated workspace and were **never** applied to your project. `action: "diff"` returns the real patch; `"apply"` applies it (refusing when your project has uncommitted changes, since the patch was built against a clean base — `force: true` overrides); `"discard"` deletes the workspace. The full patch is always written to the job directory, so `git apply` by hand is available either way. |
|
|
376
390
|
| `usage` | Per-route call counts, quota, billing kind, and breaker state — check this before passing an unfamiliar `hints.model`/`service`/`models` value, since those are not validated. Pass `listModels: <route id>` to fetch that `openai_compatible` route's live `GET /models` catalog instead of (or alongside) the summary. |
|
|
377
391
|
|
|
@@ -383,7 +397,11 @@ carries a `warning` field saying so.
|
|
|
383
397
|
immediately, then waits up to `graceSeconds` (default 25) for it to finish. Within the
|
|
384
398
|
window you get the complete result inline, exactly as if the call had blocked. Past it
|
|
385
399
|
you get the `jobId` — call `job_status` with that `jobId` to see a `partialOutput` tail
|
|
386
|
-
while it runs and the full `result` once `completed`.
|
|
400
|
+
while it runs and the full `result` once `completed`. Expect the `jobId` path to be
|
|
401
|
+
ordinary rather than exceptional: real agent-CLI work regularly runs for minutes, so on
|
|
402
|
+
this maintainer's install a little over half of live dispatches finish past the default
|
|
403
|
+
window. Treat a `completed: false` as the normal shape of a substantial task, not as a
|
|
404
|
+
sign anything went wrong. Because the run never depends on
|
|
387
405
|
the MCP call staying open, a client-side timeout costs you the inline reply, never the
|
|
388
406
|
work. Background runs default to a generous 60-minute ceiling meant only to catch a
|
|
389
407
|
genuinely hung process (stuck waiting on input, a stalled network call), not to cap
|
|
@@ -440,9 +458,21 @@ Status is exposed as resources:
|
|
|
440
458
|
|
|
441
459
|
Endpoints:
|
|
442
460
|
|
|
461
|
+
- `GET /health` — liveness, and the **only** route served without a token, so a
|
|
462
|
+
deploy gate or container probe can ask without being handed a credential. It
|
|
463
|
+
answers `{"status","service","version"}` and nothing else: no routes, no
|
|
464
|
+
endpoints, no quota, no config.
|
|
443
465
|
- `POST /mcp` for streamable HTTP MCP
|
|
466
|
+
- `POST /v1/chat/completions` with `stream: true` sends the answer as SSE
|
|
467
|
+
deltas. An endpoint route streams its text as it arrives; a CLI harness emits
|
|
468
|
+
protocol on stdout, so its answer is sent once, at completion — the deltas
|
|
469
|
+
never carry harness protocol either way. **Streaming creates no job record**,
|
|
470
|
+
so unlike the non-streaming call there is no `jobId` to poll and an
|
|
471
|
+
interrupted stream cannot be recovered. Use the non-streaming form for work
|
|
472
|
+
you would mind losing.
|
|
444
473
|
- `GET /v1/status` — full route/quota/billing/breaker detail (same shape as
|
|
445
|
-
`harness-dispatch://status.json`)
|
|
474
|
+
`harness-dispatch://status.json`). Authenticated, because that answer is not
|
|
475
|
+
for strangers.
|
|
446
476
|
- `GET /v1/usage` — per-route call counts, quota, billing kind, and breaker state only
|
|
447
477
|
- `GET /v1/models` — OpenAI-style model list; each entry's `id` is a route id you can
|
|
448
478
|
pass as `model` in `/v1/chat/completions`
|
package/config.default.yaml
CHANGED
|
@@ -47,7 +47,23 @@ clis:
|
|
|
47
47
|
# line; {{tokens}} are substituted at dispatch time (see the reference at
|
|
48
48
|
# the bottom of this file).
|
|
49
49
|
protocol:
|
|
50
|
-
|
|
50
|
+
# The prompt goes on STDIN, not in argv.
|
|
51
|
+
#
|
|
52
|
+
# `claude -p` accepts either; argv was the original choice and it is the
|
|
53
|
+
# one that carries every command-line defect this project has fixed. On
|
|
54
|
+
# Windows a .CMD/.bat target is re-spawned through cmd.exe, which caps
|
|
55
|
+
# the whole command line at 8,191 characters — so a long prompt was
|
|
56
|
+
# refused before it reached the harness at all, and the machinery
|
|
57
|
+
# deciding where that boundary sits had to replicate cross-spawn's own
|
|
58
|
+
# escaping to know. generic-cli.ts skips that check entirely when
|
|
59
|
+
# `stdin: true` (the guard is `if (!protocol.stdin)`), because a prompt
|
|
60
|
+
# on stdin has no such limit. codex_cli has always worked this way.
|
|
61
|
+
#
|
|
62
|
+
# Verified live: a 21,670-character prompt — 2.65x the 8,191-character ceiling, and
|
|
63
|
+
# measured at 20,064 characters as argv, which the old form refused
|
|
64
|
+
# outright — dispatched and answered.
|
|
65
|
+
stdin: true
|
|
66
|
+
args: ["-p", "--output-format", "json", "{{safety}}", "{{model}}"]
|
|
51
67
|
model: { flag: "--model" }
|
|
52
68
|
safety:
|
|
53
69
|
read_only: ["--allowedTools", "Read", "--permission-mode", "plan"]
|
|
@@ -206,7 +222,19 @@ clis:
|
|
|
206
222
|
protocol:
|
|
207
223
|
# -p is Cursor's boolean "print mode" flag; the prompt itself is the
|
|
208
224
|
# trailing positional {{prompt}}.
|
|
209
|
-
|
|
225
|
+
# Prompt on STDIN, for the same reason as claude_code_cli above: a
|
|
226
|
+
# prompt in argv is measured against cmd.exe's 8,191-character ceiling
|
|
227
|
+
# and escaped to get there, and this route is a `cursor-agent.CMD`
|
|
228
|
+
# PowerShell wrapper — the one that failed at ~9k characters and drove
|
|
229
|
+
# two releases of escaping work. generic-cli.ts skips the check
|
|
230
|
+
# entirely when `stdin: true`.
|
|
231
|
+
#
|
|
232
|
+
# Verified live before changing: `cursor-agent -p --trust
|
|
233
|
+
# --output-format json` with the prompt piped in returns
|
|
234
|
+
# {"type":"result","subtype":"success","is_error":false,...,"result":"pong"}.
|
|
235
|
+
# --trust is required or it refuses with a workspace-trust prompt.
|
|
236
|
+
stdin: true
|
|
237
|
+
args: ["-p", "--trust", "{{safety}}", "{{working_dir}}", "--output-format", "json", "{{model}}"]
|
|
210
238
|
# Only read_only gets extra flags; the other profiles run print mode as
|
|
211
239
|
# before. --trust is unrelated to capability — it just suppresses the
|
|
212
240
|
# workspace-trust prompt, which would hang a non-interactive run.
|
package/dist/auth.d.ts
CHANGED
|
@@ -2,8 +2,37 @@ export declare function authDir(): string;
|
|
|
2
2
|
export declare function tokenPath(): string;
|
|
3
3
|
export declare function generateHttpToken(): string;
|
|
4
4
|
export declare function readHttpToken(): Promise<string | null>;
|
|
5
|
+
/**
|
|
6
|
+
* The token as it is on disk RIGHT NOW, for a running server to consult.
|
|
7
|
+
*
|
|
8
|
+
* A server read the token once at startup and held it forever, so `auth
|
|
9
|
+
* rotate` was a lie in both directions: an acceptance pass measured the old
|
|
10
|
+
* token still returning 200 after rotation, and the newly issued one being
|
|
11
|
+
* rejected with 401. Invalidating the old token is the entire reason anyone
|
|
12
|
+
* rotates a credential, so telling the user it rotated while the leaked value
|
|
13
|
+
* kept working is the worst possible outcome.
|
|
14
|
+
*
|
|
15
|
+
* Synchronous because it is consulted on the authorization path of every
|
|
16
|
+
* request, which is not async. The file is a few dozen bytes on local disk and
|
|
17
|
+
* the read is guarded by an mtime check in the caller.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readHttpTokenSync(): string | null;
|
|
20
|
+
/** Modification time of the token file, or 0 when there isn't one. */
|
|
21
|
+
export declare function httpTokenMtimeMs(): number;
|
|
5
22
|
export declare function ensureHttpToken(): Promise<string>;
|
|
6
23
|
export declare function rotateHttpToken(): Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Whether a request carries the expected bearer token.
|
|
26
|
+
*
|
|
27
|
+
* A `token` of null means NO AUTH IS CONFIGURED, and every request is
|
|
28
|
+
* authorized — this function fails open, deliberately, and a caller that
|
|
29
|
+
* cannot guarantee a token must not rely on it to deny anything. The HTTP
|
|
30
|
+
* server can: it calls `ensureHttpToken()` at startup and falls back to the
|
|
31
|
+
* token it read from disk on refresh, so null never reaches here from there.
|
|
32
|
+
*
|
|
33
|
+
* Spelled out because the fail-open branch is one line and reads like a
|
|
34
|
+
* guard clause rather than the policy decision it is.
|
|
35
|
+
*/
|
|
7
36
|
export declare function isAuthorized(authorizationHeader: string | string[] | undefined, token: string | null): boolean;
|
|
8
37
|
export declare function maskToken(token: string): string;
|
|
9
38
|
//# sourceMappingURL=auth.d.ts.map
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AASA,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAS5D;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAOvD;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAKvD;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AASA,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAS5D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CASjD;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,IAAI,MAAM,CAMzC;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAOvD;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAKvD;AAyBD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAC1B,mBAAmB,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAClD,KAAK,EAAE,MAAM,GAAG,IAAI,GACnB,OAAO,CAOT;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG/C"}
|
package/dist/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { promises as fs } from "node:fs";
|
|
2
|
+
import { promises as fs, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { stateRoot } from "./state-dir.js";
|
|
5
5
|
const TOKEN_ENV = "HARNESS_DISPATCH_HTTP_TOKEN";
|
|
@@ -24,6 +24,41 @@ export async function readHttpToken() {
|
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The token as it is on disk RIGHT NOW, for a running server to consult.
|
|
29
|
+
*
|
|
30
|
+
* A server read the token once at startup and held it forever, so `auth
|
|
31
|
+
* rotate` was a lie in both directions: an acceptance pass measured the old
|
|
32
|
+
* token still returning 200 after rotation, and the newly issued one being
|
|
33
|
+
* rejected with 401. Invalidating the old token is the entire reason anyone
|
|
34
|
+
* rotates a credential, so telling the user it rotated while the leaked value
|
|
35
|
+
* kept working is the worst possible outcome.
|
|
36
|
+
*
|
|
37
|
+
* Synchronous because it is consulted on the authorization path of every
|
|
38
|
+
* request, which is not async. The file is a few dozen bytes on local disk and
|
|
39
|
+
* the read is guarded by an mtime check in the caller.
|
|
40
|
+
*/
|
|
41
|
+
export function readHttpTokenSync() {
|
|
42
|
+
const fromEnv = process.env[TOKEN_ENV];
|
|
43
|
+
if (fromEnv)
|
|
44
|
+
return fromEnv;
|
|
45
|
+
try {
|
|
46
|
+
const token = readFileSync(tokenPath(), "utf-8").trim();
|
|
47
|
+
return token || null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Modification time of the token file, or 0 when there isn't one. */
|
|
54
|
+
export function httpTokenMtimeMs() {
|
|
55
|
+
try {
|
|
56
|
+
return statSync(tokenPath()).mtimeMs;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
27
62
|
export async function ensureHttpToken() {
|
|
28
63
|
const existing = await readHttpToken();
|
|
29
64
|
if (existing)
|
|
@@ -42,19 +77,37 @@ export async function rotateHttpToken() {
|
|
|
42
77
|
/**
|
|
43
78
|
* Constant-time string compare. `value === expected` short-circuits on the
|
|
44
79
|
* first mismatching byte — a textbook timing side channel for guessing a
|
|
45
|
-
* bearer token one byte at a time.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
80
|
+
* bearer token one byte at a time.
|
|
81
|
+
*
|
|
82
|
+
* On a length mismatch it still does comparison work rather than returning
|
|
83
|
+
* early. That work is now sized by `expected`, not by `value`: the previous
|
|
84
|
+
* version compared the caller-supplied buffer against ITSELF, so its cost
|
|
85
|
+
* scaled with the length an attacker chose, and the comment claiming "a length
|
|
86
|
+
* mismatch alone doesn't leak timing info either" asserted a property the code
|
|
87
|
+
* did not have. The remaining signal is the same for every wrong length, which
|
|
88
|
+
* is what that sentence was meant to say.
|
|
48
89
|
*/
|
|
49
90
|
function safeEqual(value, expected) {
|
|
50
91
|
const valueBuf = Buffer.from(value, "utf8");
|
|
51
92
|
const expectedBuf = Buffer.from(expected, "utf8");
|
|
52
93
|
if (valueBuf.length !== expectedBuf.length) {
|
|
53
|
-
timingSafeEqual(
|
|
94
|
+
timingSafeEqual(expectedBuf, expectedBuf);
|
|
54
95
|
return false;
|
|
55
96
|
}
|
|
56
97
|
return timingSafeEqual(valueBuf, expectedBuf);
|
|
57
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Whether a request carries the expected bearer token.
|
|
101
|
+
*
|
|
102
|
+
* A `token` of null means NO AUTH IS CONFIGURED, and every request is
|
|
103
|
+
* authorized — this function fails open, deliberately, and a caller that
|
|
104
|
+
* cannot guarantee a token must not rely on it to deny anything. The HTTP
|
|
105
|
+
* server can: it calls `ensureHttpToken()` at startup and falls back to the
|
|
106
|
+
* token it read from disk on refresh, so null never reaches here from there.
|
|
107
|
+
*
|
|
108
|
+
* Spelled out because the fail-open branch is one line and reads like a
|
|
109
|
+
* guard clause rather than the policy decision it is.
|
|
110
|
+
*/
|
|
58
111
|
export function isAuthorized(authorizationHeader, token) {
|
|
59
112
|
if (token === null)
|
|
60
113
|
return true;
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/bin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;GAEG;
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;GAEG;AAg8BH,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAgK1D"}
|