@sema-agent/server 7.135.0 → 7.137.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/MIGRATION.md +237 -2
- package/USAGE.md +146 -41
- package/deploy/sema-up/README.md +15 -1
- package/deploy/sema-up/chart/templates/NOTES.txt +1 -1
- package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +7 -1
- package/deploy/sema-up/chart/values.yaml +1 -1
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +7 -1
- package/dist/approval-card.d.ts +60 -4
- package/dist/approval-card.js +18 -1
- package/dist/approval-content-kind.d.ts +7 -0
- package/dist/approval-content-kind.js +8 -1
- package/dist/approval-gate-currency.d.ts +48 -6
- package/dist/approval-gate-currency.js +16 -9
- package/dist/approval-hmac.d.ts +13 -1
- package/dist/approval-hmac.js +1 -1
- package/dist/approval.js +3 -3
- package/dist/auth-bridge.d.ts +34 -5
- package/dist/auth-bridge.js +22 -5
- package/dist/boot/budget-tracing.d.ts +1 -2
- package/dist/boot/budget-tracing.js +2 -2
- package/dist/boot/execution-env.d.ts +24 -1
- package/dist/boot/execution-env.js +69 -6
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +15 -1
- package/dist/boot/resolve-spec.d.ts +4 -0
- package/dist/boot/resolve-spec.js +24 -22
- package/dist/boot/runner-deps.js +4 -2
- package/dist/boot/stage-03-store-backend.d.ts +1 -1
- package/dist/boot/stage-04-stores.d.ts +2 -2
- package/dist/boot/stage-05-execution-env.d.ts +4 -2
- package/dist/boot/stage-05-execution-env.js +11 -2
- package/dist/boot/stage-06-runners.d.ts +4 -2
- package/dist/boot/stage-06-runners.js +5 -1
- package/dist/boot/stage-07-capability-layer.d.ts +5 -2
- package/dist/boot/stage-07-capability-layer.js +1 -1
- package/dist/boot/stage-08-reapers.d.ts +5 -2
- package/dist/boot/stage-08-reapers.js +2 -1
- package/dist/boot/stage-09-leader.d.ts +5 -2
- package/dist/boot/stage-10-http-server.d.ts +5 -2
- package/dist/boot/stage-10-http-server.js +3 -2
- package/dist/boot/stores.d.ts +1 -1
- package/dist/boot/stores.js +2 -3
- package/dist/boot/web-admin.js +10 -17
- package/dist/brain.js +12 -3
- package/dist/broker/broker.d.ts +89 -0
- package/dist/broker/broker.js +287 -0
- package/dist/broker/constants.d.ts +39 -0
- package/dist/broker/constants.js +15 -0
- package/dist/broker/http1.d.ts +111 -0
- package/dist/broker/http1.js +256 -0
- package/dist/broker/lease-book.d.ts +87 -0
- package/dist/broker/lease-book.js +142 -0
- package/dist/broker/lease-file.d.ts +60 -0
- package/dist/broker/lease-file.js +127 -0
- package/dist/broker/main.d.ts +2 -0
- package/dist/broker/main.js +20 -0
- package/dist/budget.d.ts +3 -3
- package/dist/budget.js +4 -0
- package/dist/capabilities/composite-members.d.ts +2 -1
- package/dist/capabilities/composite-members.js +2 -1
- package/dist/capabilities/engine-capabilities.js +1 -0
- package/dist/capabilities/mcp-probe.d.ts +5 -4
- package/dist/capabilities/scenarios.d.ts +23 -8
- package/dist/capabilities/scenarios.js +13 -8
- package/dist/capabilities/skills.d.ts +28 -5
- package/dist/capabilities/skills.js +8 -0
- package/dist/capability-facts.d.ts +3 -7
- package/dist/capability-facts.js +3 -5
- package/dist/center-credential-renewer.js +5 -1
- package/dist/center-credential.d.ts +18 -4
- package/dist/center-credential.js +18 -3
- package/dist/checkpoint-ctx-seal-m28.d.ts +3 -10
- package/dist/checkpoint-ctx-seal-m28.js +1 -1
- package/dist/checkpoint-identity.d.ts +3 -3
- package/dist/config-catalog.js +21 -16
- package/dist/config-d-migration.js +1 -1
- package/dist/config-invariants.d.ts +1 -1
- package/dist/config-invariants.js +8 -0
- package/dist/config-lkg.d.ts +0 -38
- package/dist/config-lkg.js +1 -83
- package/dist/config-types.d.ts +21 -4
- package/dist/config.d.ts +1 -1
- package/dist/config.js +60 -16
- package/dist/deployment-governance.d.ts +2 -2
- package/dist/deployment-governance.js +7 -9
- package/dist/deployment-shape.d.ts +1 -1
- package/dist/device-ws-protocol.d.ts +1 -0
- package/dist/device-ws-protocol.js +2 -1
- package/dist/elicitation.d.ts +8 -7
- package/dist/elicitation.js +9 -8
- package/dist/file-history-disclosure.d.ts +8 -4
- package/dist/file-history-disclosure.js +24 -6
- package/dist/fleet-client.d.ts +32 -1
- package/dist/fleet-client.js +24 -4
- package/dist/fleet-lease.d.ts +7 -3
- package/dist/fleet-lease.js +31 -23
- package/dist/hooks/classic-stations.d.ts +11 -54
- package/dist/hooks/hook-llm.js +14 -8
- package/dist/hooks/hook-runner.d.ts +4 -4
- package/dist/hooks/hook-runner.js +2 -2
- package/dist/http/admission.js +10 -7
- package/dist/http/approval-sweeps.js +4 -3
- package/dist/http/caller-trust.d.ts +2 -1
- package/dist/http/caller-trust.js +1 -3
- package/dist/http/checkpoint-ctx.d.ts +76 -16
- package/dist/http/checkpoint-ctx.js +45 -1
- package/dist/http/dispatch.js +24 -19
- package/dist/http/durable-run-leg.d.ts +5 -1
- package/dist/http/durable-run-leg.js +1 -1
- package/dist/http/health-self-report.d.ts +2 -2
- package/dist/http/mcp-remote-text.d.ts +5 -0
- package/dist/http/mcp-remote-text.js +7 -0
- package/dist/http/principal-gate.d.ts +15 -10
- package/dist/http/principal-gate.js +48 -9
- package/dist/http/resume-ctx-read.d.ts +2 -16
- package/dist/http/resume-ctx-read.js +2 -3
- package/dist/http/resume-legs.d.ts +2 -20
- package/dist/http/resume-legs.js +52 -38
- package/dist/http/route-ctx.d.ts +8 -2
- package/dist/http/routes/approvals-assistant.d.ts +0 -2
- package/dist/http/routes/approvals-assistant.js +65 -16
- package/dist/http/routes/capabilities.js +11 -1
- package/dist/http/routes/mcp-reconnect.d.ts +1 -1
- package/dist/http/routes/mcp-reconnect.js +5 -7
- package/dist/http/routes/runs.js +4 -3
- package/dist/http/routes/session-sync.js +70 -12
- package/dist/http/routes/sessions.js +20 -15
- package/dist/http/routes/tasks.js +8 -6
- package/dist/http/server.d.ts +4 -0
- package/dist/http/stored-body-replay.d.ts +3 -3
- package/dist/http/stored-body-replay.js +6 -1
- package/dist/http/submit-ctx.js +1 -1
- package/dist/http/wire-types.d.ts +57 -30
- package/dist/identity-source.d.ts +65 -0
- package/dist/identity-source.js +45 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/memory-store-refusal.d.ts +3 -3
- package/dist/model-entry-refusal.d.ts +2 -0
- package/dist/model-entry-refusal.js +11 -0
- package/dist/model-route-gap.d.ts +49 -4
- package/dist/model-route-gap.js +34 -1
- package/dist/model-select.d.ts +6 -17
- package/dist/model-select.js +0 -3
- package/dist/namespaced-rule-name.d.ts +1 -1
- package/dist/object-store-buckets.d.ts +13 -0
- package/dist/object-store-buckets.js +3 -0
- package/dist/observability/fail-open.d.ts +19 -7
- package/dist/observability/fail-open.js +19 -7
- package/dist/observability/metrics.js +2 -2
- package/dist/observability/principal-context.d.ts +4 -1
- package/dist/observability/principal-context.js +6 -1
- package/dist/observability/run-terminal.js +1 -0
- package/dist/operator-ask.d.ts +2 -2
- package/dist/org-delivery-gate.js +1 -1
- package/dist/org-memory-admission.d.ts +2 -2
- package/dist/org-memory-admission.js +39 -2
- package/dist/parked-decide.d.ts +4 -3
- package/dist/parked-decide.js +29 -9
- package/dist/peer-directory.d.ts +2 -1
- package/dist/peer-directory.js +7 -5
- package/dist/permission-rule-list.d.ts +2 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +8 -0
- package/dist/plugins/approval-ask-store-sql.js +3 -0
- package/dist/plugins/blob-backend.js +2 -1
- package/dist/plugins/checkpoint-index-columns.d.ts +50 -1
- package/dist/plugins/checkpoint-index-columns.js +28 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +46 -12
- package/dist/plugins/checkpoint-store-sql.js +64 -35
- package/dist/plugins/file-history-store-sql.d.ts +6 -0
- package/dist/plugins/file-history-store-sql.js +37 -37
- package/dist/plugins/file-run-store.js +8 -2
- package/dist/plugins/k8s-exec-protocol.d.ts +69 -13
- package/dist/plugins/k8s-exec-protocol.js +116 -28
- package/dist/plugins/k8s-pod-delete-failure.d.ts +25 -0
- package/dist/plugins/k8s-pod-delete-failure.js +17 -0
- package/dist/plugins/k8s-pod-reaper.d.ts +112 -0
- package/dist/plugins/k8s-pod-reaper.js +122 -0
- package/dist/plugins/local-checkpoint-store.d.ts +3 -6
- package/dist/plugins/local-checkpoint-store.js +7 -6
- package/dist/plugins/memory-run-store.js +2 -0
- package/dist/plugins/one-time-migrations.d.ts +49 -37
- package/dist/plugins/one-time-migrations.js +87 -128
- package/dist/plugins/permission-rule-store-file.js +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +17 -17
- package/dist/plugins/permission-rule-store-sql.js +16 -17
- package/dist/plugins/pg-pool.js +4 -0
- package/dist/plugins/pg-session-storage.js +2 -2
- package/dist/plugins/remote-env-adb.d.ts +3 -0
- package/dist/plugins/remote-env-adb.js +1 -0
- package/dist/plugins/remote-env-device.d.ts +3 -0
- package/dist/plugins/remote-env-device.js +4 -0
- package/dist/plugins/remote-env-e2b.d.ts +3 -0
- package/dist/plugins/remote-env-e2b.js +1 -0
- package/dist/plugins/remote-env-file-error.d.ts +8 -1
- package/dist/plugins/remote-env-file-error.js +7 -1
- package/dist/plugins/remote-env-host.d.ts +7 -1
- package/dist/plugins/remote-env-host.js +7 -1
- package/dist/plugins/remote-env-k8s.d.ts +154 -14
- package/dist/plugins/remote-env-k8s.js +300 -21
- package/dist/plugins/remote-env-local-docker.d.ts +3 -0
- package/dist/plugins/remote-env-local-docker.js +1 -0
- package/dist/plugins/remote-env-ssh.d.ts +3 -0
- package/dist/plugins/remote-env-ssh.js +1 -0
- package/dist/plugins/run-store-sql.js +10 -4
- package/dist/plugins/s3-answer.d.ts +15 -0
- package/dist/plugins/s3-answer.js +5 -0
- package/dist/plugins/session-ownership.d.ts +17 -3
- package/dist/plugins/session-ownership.js +19 -5
- package/dist/plugins/snapshot-bucket-presence.d.ts +39 -4
- package/dist/plugins/snapshot-bucket-presence.js +35 -7
- package/dist/plugins/sql-errors.d.ts +60 -0
- package/dist/plugins/sql-errors.js +64 -1
- package/dist/plugins/store-contracts.d.ts +10 -1
- package/dist/plugins/store-fault.d.ts +4 -0
- package/dist/plugins/store-fault.js +10 -0
- package/dist/plugins/tidb-pool.js +8 -1
- package/dist/plugins/tidb-session-store.js +2 -2
- package/dist/plugins/web-search.d.ts +1 -1
- package/dist/plugins/web-search.js +2 -2
- package/dist/request-declaration-refusal.d.ts +94 -0
- package/dist/request-declaration-refusal.js +78 -0
- package/dist/request-image-urls.d.ts +6 -0
- package/dist/request-image-urls.js +27 -0
- package/dist/request-key-closure.d.ts +5 -0
- package/dist/request-key-closure.js +5 -2
- package/dist/request-value-closure.d.ts +31 -2
- package/dist/request-value-closure.js +7 -1
- package/dist/rule-entry-list.d.ts +3 -2
- package/dist/rule-entry-list.js +2 -2
- package/dist/rules-consent.js +7 -4
- package/dist/run-local.d.ts +2 -2
- package/dist/runs.js +2 -1
- package/dist/runtime-caps-resolver.js +1 -1
- package/dist/runtime-governance.d.ts +5 -5
- package/dist/sandbox-git-lease.d.ts +49 -0
- package/dist/sandbox-git-lease.js +60 -0
- package/dist/sandbox-git-plane.d.ts +84 -0
- package/dist/sandbox-git-plane.js +105 -0
- package/dist/sandbox-pkg-source.d.ts +28 -1
- package/dist/sandbox-pkg-source.js +1 -2
- package/dist/sealed-key.d.ts +19 -0
- package/dist/sealed-key.js +47 -4
- package/dist/security.d.ts +16 -4
- package/dist/security.js +8 -3
- package/dist/server-secret-env.d.ts +6 -0
- package/dist/server-secret-env.js +14 -0
- package/dist/session-error-wire.d.ts +14 -6
- package/dist/session-error-wire.js +2 -1
- package/dist/session-incarnation.d.ts +16 -5
- package/dist/session-incarnation.js +7 -4
- package/dist/session-policy-wire.d.ts +1 -1
- package/dist/session-policy-wire.js +2 -2
- package/dist/session-row.d.ts +3 -17
- package/dist/session-row.js +4 -0
- package/dist/session-sync.d.ts +15 -3
- package/dist/session-sync.js +5 -2
- package/dist/spec-fields.d.ts +43 -13
- package/dist/spec-fields.js +33 -12
- package/dist/stored-request-prompt-profile-m30.d.ts +49 -0
- package/dist/stored-request-prompt-profile-m30.js +25 -0
- package/dist/task-a2a.d.ts +52 -81
- package/dist/task-a2a.js +219 -89
- package/dist/task-mcp.d.ts +7 -15
- package/dist/task-mcp.js +16 -51
- package/dist/task-settings.d.ts +2 -2
- package/dist/tool-approval.d.ts +35 -12
- package/dist/tool-approval.js +59 -39
- package/dist/tool-rule-names.d.ts +5 -0
- package/dist/trace/core-keyset-guard.d.ts +5 -5
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/exit-seat.js +37 -13
- package/dist/trace/ledger-sink.d.ts +4 -5
- package/dist/trace/ledger-sink.js +3 -7
- package/dist/trace/project.d.ts +37 -3
- package/dist/trace/project.js +20 -6
- package/dist/trace/redact.d.ts +40 -12
- package/dist/trace/redact.js +332 -128
- package/dist/trace/self-minted-wire.d.ts +641 -0
- package/dist/trace/self-minted-wire.js +201 -0
- package/dist/trace/wire-projection-faces.d.ts +5 -3
- package/dist/trace/wire-projection-faces.js +7 -3
- package/dist/url-credential-refusal.d.ts +52 -0
- package/dist/url-credential-refusal.js +14 -0
- package/dist/url-refusal.d.ts +13 -4
- package/dist/url-refusal.js +8 -2
- package/dist/web-admin-client/apply-effective.js +0 -1
- package/dist/web-admin-client/center-request.d.ts +20 -2
- package/dist/web-admin-client/center-request.js +40 -5
- package/dist/web-admin-client/http-client.js +6 -6
- package/dist/web-admin-client/types.d.ts +5 -5
- package/dist/workspace-space.d.ts +27 -0
- package/dist/workspace-space.js +26 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +11 -5
- package/package.json +4 -3
- package/scripts/drain-check-local.mjs +2 -1
- package/scripts/drain-check.mysql.sql +3 -1
- package/scripts/drain-check.pg.sql +3 -1
- package/skills/schedule.md +1 -1
- package/wire-changes/server@7.104.0.json +134 -0
- package/wire-changes/server@7.105.0.json +53 -0
- package/wire-changes/server@7.106.0.json +475 -0
- package/wire-changes/server@7.107.0.json +64 -0
- package/wire-changes/server@7.108.0.json +153 -0
- package/wire-changes/server@7.109.0.json +46 -0
- package/wire-changes/server@7.110.0.json +90 -0
- package/wire-changes/server@7.111.0.json +86 -0
- package/wire-changes/server@7.112.0.json +60 -0
- package/wire-changes/server@7.113.0.json +26 -0
- package/wire-changes/server@7.114.0.json +26 -0
- package/wire-changes/server@7.115.0.json +97 -0
- package/wire-changes/server@7.116.0.json +218 -0
- package/wire-changes/server@7.116.1.json +26 -0
- package/wire-changes/server@7.117.0.json +65 -0
- package/wire-changes/server@7.118.0.json +129 -0
- package/wire-changes/server@7.119.0.json +65 -0
- package/wire-changes/server@7.120.0.json +89 -0
- package/wire-changes/server@7.121.0.json +219 -0
- package/wire-changes/server@7.122.0.json +54 -0
- package/wire-changes/server@7.123.0.json +26 -0
- package/wire-changes/server@7.124.0.json +336 -0
- package/wire-changes/server@7.124.1.json +26 -0
- package/wire-changes/server@7.125.0.json +111 -0
- package/wire-changes/server@7.126.0.json +285 -0
- package/wire-changes/server@7.127.0.json +77 -0
- package/wire-changes/server@7.127.1.json +26 -0
- package/wire-changes/server@7.127.2.json +53 -0
- package/wire-changes/server@7.128.0.json +26 -0
- package/wire-changes/server@7.129.0.json +106 -0
- package/wire-changes/server@7.129.1.json +26 -0
- package/wire-changes/server@7.130.0.json +26 -0
- package/wire-changes/server@7.131.0.json +118 -0
- package/wire-changes/server@7.132.0.json +226 -0
- package/wire-changes/server@7.133.0.json +1155 -0
- package/wire-changes/server@7.134.0.json +107 -0
- package/wire-changes/server@7.135.0.json +174 -0
- package/wire-changes/server@7.136.0.json +448 -0
- package/wire-changes/server@7.137.0.json +825 -0
- package/dist/url-credential.d.ts +0 -52
- package/dist/url-credential.js +0 -49
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
export const MAX_HEAD_BYTES = 16 * 1024;
|
|
2
|
+
const CHUNK_LINE_MAX = 64;
|
|
3
|
+
const PIECE_MAX = 64 * 1024;
|
|
4
|
+
const HIGH_WATER = 1024 * 1024;
|
|
5
|
+
export class WireError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
rule;
|
|
8
|
+
constructor(status, rule) {
|
|
9
|
+
super(rule);
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.rule = rule;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class ByteReader {
|
|
15
|
+
stream;
|
|
16
|
+
bufs = [];
|
|
17
|
+
len = 0;
|
|
18
|
+
ended = false;
|
|
19
|
+
failure;
|
|
20
|
+
wake;
|
|
21
|
+
constructor(stream) {
|
|
22
|
+
this.stream = stream;
|
|
23
|
+
stream.on("data", (d) => {
|
|
24
|
+
this.bufs.push(d);
|
|
25
|
+
this.len += d.length;
|
|
26
|
+
if (this.len > HIGH_WATER)
|
|
27
|
+
stream.pause();
|
|
28
|
+
this.notify();
|
|
29
|
+
});
|
|
30
|
+
stream.on("end", () => {
|
|
31
|
+
this.ended = true;
|
|
32
|
+
this.notify();
|
|
33
|
+
});
|
|
34
|
+
stream.on("close", () => {
|
|
35
|
+
this.ended = true;
|
|
36
|
+
this.notify();
|
|
37
|
+
});
|
|
38
|
+
stream.on("error", (e) => {
|
|
39
|
+
this.failure = e;
|
|
40
|
+
this.notify();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
notify() {
|
|
44
|
+
const w = this.wake;
|
|
45
|
+
this.wake = undefined;
|
|
46
|
+
w?.();
|
|
47
|
+
}
|
|
48
|
+
wait() {
|
|
49
|
+
return new Promise((r) => (this.wake = r));
|
|
50
|
+
}
|
|
51
|
+
take(n) {
|
|
52
|
+
const all = this.bufs.length === 1 ? this.bufs[0] : Buffer.concat(this.bufs);
|
|
53
|
+
const out = all.subarray(0, n);
|
|
54
|
+
const rest = all.subarray(n);
|
|
55
|
+
this.bufs = rest.length > 0 ? [rest] : [];
|
|
56
|
+
this.len = rest.length;
|
|
57
|
+
if (this.len < HIGH_WATER / 2 && this.stream.isPaused())
|
|
58
|
+
this.stream.resume();
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
async readUntil(delim, max) {
|
|
62
|
+
for (;;) {
|
|
63
|
+
const all = this.bufs.length === 1 ? this.bufs[0] : Buffer.concat(this.bufs);
|
|
64
|
+
this.bufs = all.length > 0 ? [all] : [];
|
|
65
|
+
const at = all.indexOf(delim, 0, "latin1");
|
|
66
|
+
if (at >= 0 && at + delim.length <= max)
|
|
67
|
+
return this.take(at + delim.length);
|
|
68
|
+
if (all.length > max)
|
|
69
|
+
throw new WireError(400, "a head or line is longer than allowed");
|
|
70
|
+
if (this.failure)
|
|
71
|
+
throw this.failure;
|
|
72
|
+
if (this.ended)
|
|
73
|
+
return undefined;
|
|
74
|
+
await this.wait();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async readSome(max) {
|
|
78
|
+
for (;;) {
|
|
79
|
+
if (this.len > 0)
|
|
80
|
+
return this.take(Math.min(max, this.len));
|
|
81
|
+
if (this.failure)
|
|
82
|
+
throw this.failure;
|
|
83
|
+
if (this.ended)
|
|
84
|
+
return undefined;
|
|
85
|
+
await this.wait();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async readExact(n) {
|
|
89
|
+
for (;;) {
|
|
90
|
+
if (this.len >= n)
|
|
91
|
+
return this.take(n);
|
|
92
|
+
if (this.failure)
|
|
93
|
+
throw this.failure;
|
|
94
|
+
if (this.ended)
|
|
95
|
+
throw new WireError(400, "the stream ended inside a declared body");
|
|
96
|
+
await this.wait();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const TOKEN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
101
|
+
const REQUEST_LINE = /^([!#$%&'*+.^_`|~0-9A-Za-z-]+) ([\x21-\x7e]+) HTTP\/1\.1$/;
|
|
102
|
+
const STATUS_LINE = /^HTTP\/1\.[01] ([1-5][0-9]{2})(?: ([\t\x20-\x7e\x80-\xff]*))?$/;
|
|
103
|
+
const FIELD_VALUE = /^[\t\x20-\x7e\x80-\xff]*$/;
|
|
104
|
+
function splitHead(buf) {
|
|
105
|
+
const text = buf.subarray(0, buf.length - 4).toString("latin1");
|
|
106
|
+
const lines = text.split("\r\n");
|
|
107
|
+
for (const l of lines)
|
|
108
|
+
if (l.includes("\r") || l.includes("\n"))
|
|
109
|
+
throw new WireError(400, "a bare CR or LF in the head");
|
|
110
|
+
const [first, ...rest] = lines;
|
|
111
|
+
const headers = rest.map((line) => {
|
|
112
|
+
if (line.startsWith(" ") || line.startsWith("\t"))
|
|
113
|
+
throw new WireError(400, "a folded header line");
|
|
114
|
+
const c = line.indexOf(":");
|
|
115
|
+
const name = c > 0 ? line.slice(0, c) : "";
|
|
116
|
+
if (!TOKEN.test(name))
|
|
117
|
+
throw new WireError(400, "a header line without a token name");
|
|
118
|
+
const value = line.slice(c + 1).replace(/^[ \t]+|[ \t]+$/g, "");
|
|
119
|
+
if (!FIELD_VALUE.test(value))
|
|
120
|
+
throw new WireError(400, "a control character in a header value");
|
|
121
|
+
return [name.toLowerCase(), value];
|
|
122
|
+
});
|
|
123
|
+
return { first: first ?? "", headers };
|
|
124
|
+
}
|
|
125
|
+
export function parseRequestHead(buf) {
|
|
126
|
+
const { first, headers } = splitHead(buf);
|
|
127
|
+
const m = REQUEST_LINE.exec(first);
|
|
128
|
+
if (!m)
|
|
129
|
+
throw new WireError(400, "the request line is not `<method> <target> HTTP/1.1`");
|
|
130
|
+
return { method: m[1], target: m[2], headers };
|
|
131
|
+
}
|
|
132
|
+
export function parseResponseHead(buf) {
|
|
133
|
+
const { first, headers } = splitHead(buf);
|
|
134
|
+
const m = STATUS_LINE.exec(first);
|
|
135
|
+
if (!m)
|
|
136
|
+
throw new WireError(502, "the upstream status line is malformed");
|
|
137
|
+
return { status: Number(m[1]), reason: m[2] ?? "", headers };
|
|
138
|
+
}
|
|
139
|
+
export const valuesOf = (headers, name) => headers.filter(([n]) => n === name).map(([, v]) => v);
|
|
140
|
+
export const ROUTE_PATH = /^\/o\/(0|[1-9][0-9]?)\/([A-Za-z0-9][A-Za-z0-9._-]{0,99})\/([A-Za-z0-9][A-Za-z0-9._-]{0,99})\/(info\/refs|git-upload-pack|git-receive-pack)$/;
|
|
141
|
+
export const UPSTREAM_PATH = /^\/([A-Za-z0-9][A-Za-z0-9._-]{0,99})\/([A-Za-z0-9][A-Za-z0-9._-]{0,99})\/(info\/refs|git-upload-pack|git-receive-pack)$/;
|
|
142
|
+
export const INFO_REFS_QUERIES = { "service=git-upload-pack": "git-upload-pack", "service=git-receive-pack": "git-receive-pack" };
|
|
143
|
+
const PACK_CONTENT_TYPE = {
|
|
144
|
+
"git-upload-pack": "application/x-git-upload-pack-request",
|
|
145
|
+
"git-receive-pack": "application/x-git-receive-pack-request",
|
|
146
|
+
};
|
|
147
|
+
export const FORWARDED_REQUEST_HEADERS = new Set(["accept", "accept-encoding", "content-type", "content-encoding", "git-protocol", "user-agent", "pragma", "cache-control"]);
|
|
148
|
+
export const FORWARDED_RESPONSE_HEADERS = new Set(["content-type", "content-encoding", "cache-control", "pragma", "expires", "date"]);
|
|
149
|
+
const no = (rule, status = 400) => ({ ok: false, status, rule });
|
|
150
|
+
export function judgeRequest(head, hasEntry) {
|
|
151
|
+
if (!head.target.startsWith("/"))
|
|
152
|
+
return no("the request target is not origin-form");
|
|
153
|
+
const q = head.target.indexOf("?");
|
|
154
|
+
const path = q < 0 ? head.target : head.target.slice(0, q);
|
|
155
|
+
const query = q < 0 ? undefined : head.target.slice(q + 1);
|
|
156
|
+
const m = ROUTE_PATH.exec(path);
|
|
157
|
+
if (!m)
|
|
158
|
+
return no("the path is not /o/<i>/<owner>/<repo>/<git endpoint> (raw, never decoded: no percent-escape)");
|
|
159
|
+
const i = Number(m[1]);
|
|
160
|
+
if (!hasEntry(i))
|
|
161
|
+
return no("the path names no configured origin");
|
|
162
|
+
const endpoint = m[4];
|
|
163
|
+
let service;
|
|
164
|
+
if (endpoint === "info/refs") {
|
|
165
|
+
if (head.method !== "GET")
|
|
166
|
+
return no("info/refs takes GET only");
|
|
167
|
+
service = query !== undefined && Object.hasOwn(INFO_REFS_QUERIES, query) ? INFO_REFS_QUERIES[query] : undefined;
|
|
168
|
+
if (service === undefined)
|
|
169
|
+
return no("info/refs takes exactly service=git-upload-pack or service=git-receive-pack");
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
if (head.method !== "POST")
|
|
173
|
+
return no("a pack endpoint takes POST only");
|
|
174
|
+
if (query !== undefined)
|
|
175
|
+
return no("a pack endpoint takes no query");
|
|
176
|
+
const ct = valuesOf(head.headers, "content-type");
|
|
177
|
+
if (ct.length !== 1 || ct[0] !== PACK_CONTENT_TYPE[endpoint])
|
|
178
|
+
return no(`${endpoint} takes exactly one Content-Type: ${PACK_CONTENT_TYPE[endpoint]}`);
|
|
179
|
+
}
|
|
180
|
+
if (valuesOf(head.headers, "trailer").length > 0)
|
|
181
|
+
return no("a Trailer header");
|
|
182
|
+
const cl = valuesOf(head.headers, "content-length");
|
|
183
|
+
const te = valuesOf(head.headers, "transfer-encoding");
|
|
184
|
+
let framing;
|
|
185
|
+
if (head.method === "GET") {
|
|
186
|
+
if (te.length > 0)
|
|
187
|
+
return no("a GET with Transfer-Encoding");
|
|
188
|
+
if (cl.length > 1 || (cl.length === 1 && cl[0] !== "0"))
|
|
189
|
+
return no("a GET with a body");
|
|
190
|
+
framing = { kind: "none" };
|
|
191
|
+
}
|
|
192
|
+
else if (cl.length === 1 && te.length === 0 && /^[0-9]{1,15}$/.test(cl[0])) {
|
|
193
|
+
framing = { kind: "length", n: Number(cl[0]) };
|
|
194
|
+
}
|
|
195
|
+
else if (te.length === 1 && cl.length === 0 && te[0].toLowerCase() === "chunked") {
|
|
196
|
+
framing = { kind: "chunked" };
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
return no("a POST needs exactly one Content-Length (one decimal) or exactly one Transfer-Encoding: chunked");
|
|
200
|
+
}
|
|
201
|
+
const expect = valuesOf(head.headers, "expect");
|
|
202
|
+
if (expect.length > 1 || (expect.length === 1 && expect[0].toLowerCase() !== "100-continue"))
|
|
203
|
+
return no("an Expect other than 100-continue", 417);
|
|
204
|
+
const ce = valuesOf(head.headers, "content-encoding");
|
|
205
|
+
if (ce.length > 1 || (ce.length === 1 && !["gzip", "identity"].includes(ce[0].toLowerCase())))
|
|
206
|
+
return no("Content-Encoding other than gzip / identity");
|
|
207
|
+
return { ok: true, route: { i, owner: m[2], repo: m[3], endpoint, ...(service !== undefined ? { service } : {}) }, framing, expectContinue: expect.length === 1 && framing.kind !== "none" };
|
|
208
|
+
}
|
|
209
|
+
export function judgeResponseFraming(head) {
|
|
210
|
+
if (head.status === 204 || head.status === 304)
|
|
211
|
+
return { kind: "none" };
|
|
212
|
+
const cl = valuesOf(head.headers, "content-length");
|
|
213
|
+
const te = valuesOf(head.headers, "transfer-encoding");
|
|
214
|
+
if (cl.length > 0 && te.length > 0)
|
|
215
|
+
return { problem: "the upstream response carries both Content-Length and Transfer-Encoding" };
|
|
216
|
+
if (cl.length > 1)
|
|
217
|
+
return { problem: "the upstream response repeats Content-Length" };
|
|
218
|
+
if (cl.length === 1)
|
|
219
|
+
return /^[0-9]{1,15}$/.test(cl[0]) ? { kind: "length", n: Number(cl[0]) } : { problem: "the upstream Content-Length is not one decimal" };
|
|
220
|
+
if (te.length > 1 || (te.length === 1 && te[0].toLowerCase() !== "chunked"))
|
|
221
|
+
return { problem: "the upstream Transfer-Encoding is not exactly chunked" };
|
|
222
|
+
return te.length === 1 ? { kind: "chunked" } : { kind: "close" };
|
|
223
|
+
}
|
|
224
|
+
export async function* readChunked(reader, trailers, failStatus) {
|
|
225
|
+
for (;;) {
|
|
226
|
+
const line = await reader.readUntil("\r\n", CHUNK_LINE_MAX);
|
|
227
|
+
if (line === undefined)
|
|
228
|
+
throw new WireError(failStatus, "the stream ended inside a chunked body");
|
|
229
|
+
const size = line.subarray(0, line.length - 2).toString("latin1");
|
|
230
|
+
if (!/^[0-9A-Fa-f]{1,8}$/.test(size))
|
|
231
|
+
throw new WireError(failStatus, "a malformed chunk-size line");
|
|
232
|
+
let left = parseInt(size, 16);
|
|
233
|
+
if (left === 0)
|
|
234
|
+
break;
|
|
235
|
+
while (left > 0) {
|
|
236
|
+
const piece = await reader.readSome(Math.min(left, PIECE_MAX));
|
|
237
|
+
if (piece === undefined)
|
|
238
|
+
throw new WireError(failStatus, "the stream ended inside a chunk");
|
|
239
|
+
left -= piece.length;
|
|
240
|
+
yield piece;
|
|
241
|
+
}
|
|
242
|
+
const crlf = await reader.readExact(2).catch(() => undefined);
|
|
243
|
+
if (crlf === undefined || crlf.toString("latin1") !== "\r\n")
|
|
244
|
+
throw new WireError(failStatus, "a chunk not followed by CRLF");
|
|
245
|
+
}
|
|
246
|
+
for (;;) {
|
|
247
|
+
const line = await reader.readUntil("\r\n", MAX_HEAD_BYTES);
|
|
248
|
+
if (line === undefined)
|
|
249
|
+
throw new WireError(failStatus, "the stream ended inside the trailer section");
|
|
250
|
+
if (line.length === 2)
|
|
251
|
+
return;
|
|
252
|
+
if (trailers === "refuse")
|
|
253
|
+
throw new WireError(failStatus, "a trailer field");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=http1.js.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** The monotonic clock the law runs on (production: {@link systemMonotonicClock}; the pins drive a hand clock). */
|
|
2
|
+
export interface MonotonicClock {
|
|
3
|
+
nowNs(): bigint;
|
|
4
|
+
setTimer(delayMs: number, fn: () => void): unknown;
|
|
5
|
+
clearTimer(handle: unknown): void;
|
|
6
|
+
}
|
|
7
|
+
export declare const systemMonotonicClock: MonotonicClock;
|
|
8
|
+
/** One exchange the broker attached a credential to — cut by {@link AuthenticatedExchange.destroy} (both sockets, no graceful close). */
|
|
9
|
+
export interface AuthenticatedExchange {
|
|
10
|
+
destroy(): void;
|
|
11
|
+
}
|
|
12
|
+
/** What the request path needs of an entry: where it goes and what to say when git must be told no. Never the credential. */
|
|
13
|
+
export interface LeaseEntryView {
|
|
14
|
+
readonly origin: string;
|
|
15
|
+
readonly unusableMessage: string;
|
|
16
|
+
/** Present on a `ready` entry: the sentence for "the git host refused the credential we attached". */
|
|
17
|
+
readonly rejectedMessage?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Log events of the book (closed set; no event carries a credential). */
|
|
20
|
+
export type LeaseBookEvent = {
|
|
21
|
+
event: "lease_loaded";
|
|
22
|
+
i: number;
|
|
23
|
+
leaseId: string;
|
|
24
|
+
ttlMs: number;
|
|
25
|
+
} | {
|
|
26
|
+
event: "lease_withdrawn";
|
|
27
|
+
i: number;
|
|
28
|
+
reason: string;
|
|
29
|
+
destroyed: number;
|
|
30
|
+
} | {
|
|
31
|
+
event: "lease_expired";
|
|
32
|
+
i: number;
|
|
33
|
+
leaseId: string;
|
|
34
|
+
destroyed: number;
|
|
35
|
+
} | {
|
|
36
|
+
event: "lease_rejected";
|
|
37
|
+
problem: string;
|
|
38
|
+
destroyed: number;
|
|
39
|
+
};
|
|
40
|
+
export declare class LeaseBook {
|
|
41
|
+
private readonly clock;
|
|
42
|
+
private readonly log;
|
|
43
|
+
/** The pinned origin table (set by the first good file). */
|
|
44
|
+
private table?;
|
|
45
|
+
/** Per entry: the last good file's entry (messages / origin answer even after the file went bad). */
|
|
46
|
+
private entries;
|
|
47
|
+
/** Per entry: the current ready lease, if any. */
|
|
48
|
+
private readonly current;
|
|
49
|
+
/** `stopAt` of every (entry, leaseId) ever seen — rule 1: a leaseId is armed once. */
|
|
50
|
+
private readonly seen;
|
|
51
|
+
/** Per entry: the exchanges holding its credential right now. */
|
|
52
|
+
private readonly held;
|
|
53
|
+
/** The last file parsed and passed rule 4. */
|
|
54
|
+
private good;
|
|
55
|
+
constructor(opts: {
|
|
56
|
+
clock: MonotonicClock;
|
|
57
|
+
log: (e: LeaseBookEvent) => void;
|
|
58
|
+
});
|
|
59
|
+
/** Load one lease file text. A refused file withdraws everything (rule 3) and leaves the pinned table as it was. */
|
|
60
|
+
load(text: string): {
|
|
61
|
+
ok: true;
|
|
62
|
+
} | {
|
|
63
|
+
ok: false;
|
|
64
|
+
problem: string;
|
|
65
|
+
};
|
|
66
|
+
/** The entry's route answer (origin + sentences), from the last good file; `undefined` for an index the pinned table does not have. */
|
|
67
|
+
view(i: number): LeaseEntryView | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Rule 2, and the hold of rule 3 in the same synchronous step: the entry usable ⇒ the `Authorization` value + a release, and `exchange` is
|
|
70
|
+
* now cut whenever the entry stops being usable; not usable ⇒ `undefined` (forward without a credential).
|
|
71
|
+
*/
|
|
72
|
+
authorize(i: number, exchange: AuthenticatedExchange): {
|
|
73
|
+
authorization: string;
|
|
74
|
+
release(): void;
|
|
75
|
+
} | undefined;
|
|
76
|
+
/** Readiness: the last file parsed (and kept the pinned table) ∧ every ready entry's current lease has not reached `stopAt`. */
|
|
77
|
+
ready(): boolean;
|
|
78
|
+
private tableProblem;
|
|
79
|
+
private arm;
|
|
80
|
+
/** Every current lease whose stopAt has passed expires now (before anything new is installed or authorized). */
|
|
81
|
+
private expireDue;
|
|
82
|
+
private expire;
|
|
83
|
+
/** The entry has no lease any more: clear its timer, cut its exchanges. Returns how many were cut. */
|
|
84
|
+
private dropLease;
|
|
85
|
+
private cut;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=lease-book.d.ts.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { parseLeaseFile } from "./lease-file.js";
|
|
2
|
+
export const systemMonotonicClock = {
|
|
3
|
+
nowNs: () => process.hrtime.bigint(),
|
|
4
|
+
setTimer: (delayMs, fn) => setTimeout(fn, delayMs),
|
|
5
|
+
clearTimer: (handle) => clearTimeout(handle),
|
|
6
|
+
};
|
|
7
|
+
const NS_PER_MS = 1000000n;
|
|
8
|
+
export class LeaseBook {
|
|
9
|
+
clock;
|
|
10
|
+
log;
|
|
11
|
+
table;
|
|
12
|
+
entries = [];
|
|
13
|
+
current = new Map();
|
|
14
|
+
seen = new Map();
|
|
15
|
+
held = new Map();
|
|
16
|
+
good = false;
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
this.clock = opts.clock;
|
|
19
|
+
this.log = opts.log;
|
|
20
|
+
}
|
|
21
|
+
load(text) {
|
|
22
|
+
this.expireDue();
|
|
23
|
+
const parsed = parseLeaseFile(text);
|
|
24
|
+
const problem = !parsed.ok ? parsed.problem : this.tableProblem(parsed.file.origins);
|
|
25
|
+
if (problem !== undefined || !parsed.ok) {
|
|
26
|
+
const why = problem ?? "unreadable";
|
|
27
|
+
this.good = false;
|
|
28
|
+
let destroyed = 0;
|
|
29
|
+
for (const i of [...this.current.keys()])
|
|
30
|
+
destroyed += this.dropLease(i);
|
|
31
|
+
this.log({ event: "lease_rejected", problem: why, destroyed });
|
|
32
|
+
return { ok: false, problem: why };
|
|
33
|
+
}
|
|
34
|
+
this.table ??= parsed.file.origins.map((e) => e.origin);
|
|
35
|
+
this.entries = [...parsed.file.origins];
|
|
36
|
+
this.good = true;
|
|
37
|
+
for (const entry of parsed.file.origins) {
|
|
38
|
+
if (entry.state === "withdrawn") {
|
|
39
|
+
if (this.current.has(entry.i))
|
|
40
|
+
this.log({ event: "lease_withdrawn", i: entry.i, reason: entry.reason, destroyed: this.dropLease(entry.i) });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const key = `${entry.i}\u0000${entry.leaseId}`;
|
|
44
|
+
const known = this.seen.get(key);
|
|
45
|
+
const stopAt = known ?? this.clock.nowNs() + BigInt(entry.ttlMs) * NS_PER_MS;
|
|
46
|
+
if (known === undefined) {
|
|
47
|
+
this.seen.set(key, stopAt);
|
|
48
|
+
this.log({ event: "lease_loaded", i: entry.i, leaseId: entry.leaseId, ttlMs: entry.ttlMs });
|
|
49
|
+
}
|
|
50
|
+
const was = this.current.get(entry.i);
|
|
51
|
+
if (was !== undefined && was.entry.leaseId === entry.leaseId) {
|
|
52
|
+
was.entry = entry;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (was?.timer !== undefined)
|
|
56
|
+
this.clock.clearTimer(was.timer);
|
|
57
|
+
const lease = { entry, stopAt };
|
|
58
|
+
this.current.set(entry.i, lease);
|
|
59
|
+
this.arm(entry.i, lease);
|
|
60
|
+
}
|
|
61
|
+
this.expireDue();
|
|
62
|
+
return { ok: true };
|
|
63
|
+
}
|
|
64
|
+
view(i) {
|
|
65
|
+
const e = this.entries[i];
|
|
66
|
+
if (e === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
return e.state === "ready" ? { origin: e.origin, unusableMessage: e.unusableMessage, rejectedMessage: e.rejectedMessage } : { origin: e.origin, unusableMessage: e.unusableMessage };
|
|
69
|
+
}
|
|
70
|
+
authorize(i, exchange) {
|
|
71
|
+
this.expireDue();
|
|
72
|
+
const lease = this.good ? this.current.get(i) : undefined;
|
|
73
|
+
if (lease === undefined || this.clock.nowNs() >= lease.stopAt)
|
|
74
|
+
return undefined;
|
|
75
|
+
let set = this.held.get(i);
|
|
76
|
+
if (set === undefined)
|
|
77
|
+
this.held.set(i, (set = new Set()));
|
|
78
|
+
set.add(exchange);
|
|
79
|
+
const authorization = `Basic ${Buffer.from(`${lease.entry.username}:${lease.entry.secret}`, "utf8").toString("base64")}`;
|
|
80
|
+
return { authorization, release: () => void this.held.get(i)?.delete(exchange) };
|
|
81
|
+
}
|
|
82
|
+
ready() {
|
|
83
|
+
if (!this.good)
|
|
84
|
+
return false;
|
|
85
|
+
const now = this.clock.nowNs();
|
|
86
|
+
for (const e of this.entries) {
|
|
87
|
+
if (e.state !== "ready")
|
|
88
|
+
continue;
|
|
89
|
+
const lease = this.current.get(e.i);
|
|
90
|
+
if (lease === undefined || now >= lease.stopAt)
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
tableProblem(origins) {
|
|
96
|
+
if (this.table === undefined)
|
|
97
|
+
return undefined;
|
|
98
|
+
if (origins.length !== this.table.length || origins.some((e, i) => e.origin !== this.table[i])) {
|
|
99
|
+
return "the lease file's origin table differs from the one this pod was built with";
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
arm(i, lease) {
|
|
104
|
+
const leftMs = Number((lease.stopAt - this.clock.nowNs() + NS_PER_MS - 1n) / NS_PER_MS);
|
|
105
|
+
lease.timer = this.clock.setTimer(Math.max(0, leftMs), () => {
|
|
106
|
+
if (this.current.get(i) !== lease)
|
|
107
|
+
return;
|
|
108
|
+
if (this.clock.nowNs() < lease.stopAt)
|
|
109
|
+
return this.arm(i, lease);
|
|
110
|
+
this.expire(i, lease);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
expireDue() {
|
|
114
|
+
const now = this.clock.nowNs();
|
|
115
|
+
for (const [i, lease] of [...this.current])
|
|
116
|
+
if (now >= lease.stopAt)
|
|
117
|
+
this.expire(i, lease);
|
|
118
|
+
}
|
|
119
|
+
expire(i, lease) {
|
|
120
|
+
if (lease.timer !== undefined)
|
|
121
|
+
this.clock.clearTimer(lease.timer);
|
|
122
|
+
this.current.delete(i);
|
|
123
|
+
this.log({ event: "lease_expired", i, leaseId: lease.entry.leaseId, destroyed: this.cut(i) });
|
|
124
|
+
}
|
|
125
|
+
dropLease(i) {
|
|
126
|
+
const lease = this.current.get(i);
|
|
127
|
+
if (lease?.timer !== undefined)
|
|
128
|
+
this.clock.clearTimer(lease.timer);
|
|
129
|
+
this.current.delete(i);
|
|
130
|
+
return this.cut(i);
|
|
131
|
+
}
|
|
132
|
+
cut(i) {
|
|
133
|
+
const set = this.held.get(i);
|
|
134
|
+
if (set === undefined)
|
|
135
|
+
return 0;
|
|
136
|
+
this.held.delete(i);
|
|
137
|
+
for (const x of set)
|
|
138
|
+
x.destroy();
|
|
139
|
+
return set.size;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=lease-book.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `broker.json` v1 — the lease file the server writes (the pod's lease Secret, `writeSandboxLease`) and the broker reads (its mounted
|
|
3
|
+
* volume). ONE parser for both sides: the server runs it on every file before sending it, the broker on every file it loads — a file one
|
|
4
|
+
* side would refuse is a file the other never sees.
|
|
5
|
+
*
|
|
6
|
+
* Shape (closed — any other key, any other value form is a refusal; server and broker are one build):
|
|
7
|
+
* `{ "v": 1, "origins": [ <entry i = 0>, <entry i = 1>, … ] }`, 1–16 entries, `i` = the entry's position, origins pairwise distinct and each
|
|
8
|
+
* exactly a URL origin (`scheme://host[:port]`: lower case, no default port, no path / query / userinfo — `new URL(o).origin === o`).
|
|
9
|
+
* · `ready`: `leaseId` (fresh on every server write), `ttlMs` (integer, 0 < ttlMs ≤ {@link LEASE_TTL_MAX_MS}), `username` (no `:`),
|
|
10
|
+
* `secret`, `credentialId`, `version`, `unusableMessage`, `rejectedMessage`. A `ready` entry is https — a credential never rides http.
|
|
11
|
+
* · `withdrawn`: `reason` (a word), `unusableMessage`.
|
|
12
|
+
* Messages are single-line and at most {@link LEASE_MESSAGE_MAX_CHARS} characters (they go into git's `ERR` line and an HTTP body).
|
|
13
|
+
*
|
|
14
|
+
* A refusal names the key and the entry, never a value.
|
|
15
|
+
*/
|
|
16
|
+
export declare const LEASE_FILE_VERSION = 1;
|
|
17
|
+
/** The longest lease the broker accepts (5 minutes, its own monotonic clock). */
|
|
18
|
+
export declare const LEASE_TTL_MAX_MS = 300000;
|
|
19
|
+
/** Origins one pod can be routed to. */
|
|
20
|
+
export declare const LEASE_ORIGINS_MAX = 16;
|
|
21
|
+
export declare const LEASE_MESSAGE_MAX_CHARS = 512;
|
|
22
|
+
export interface ReadyLeaseEntry {
|
|
23
|
+
readonly i: number;
|
|
24
|
+
readonly origin: string;
|
|
25
|
+
readonly state: "ready";
|
|
26
|
+
readonly leaseId: string;
|
|
27
|
+
readonly ttlMs: number;
|
|
28
|
+
readonly username: string;
|
|
29
|
+
readonly secret: string;
|
|
30
|
+
readonly credentialId: string;
|
|
31
|
+
readonly version: number;
|
|
32
|
+
readonly unusableMessage: string;
|
|
33
|
+
readonly rejectedMessage: string;
|
|
34
|
+
}
|
|
35
|
+
export interface WithdrawnLeaseEntry {
|
|
36
|
+
readonly i: number;
|
|
37
|
+
readonly origin: string;
|
|
38
|
+
readonly state: "withdrawn";
|
|
39
|
+
readonly reason: string;
|
|
40
|
+
readonly unusableMessage: string;
|
|
41
|
+
}
|
|
42
|
+
export type LeaseEntry = ReadyLeaseEntry | WithdrawnLeaseEntry;
|
|
43
|
+
export interface LeaseFile {
|
|
44
|
+
readonly v: typeof LEASE_FILE_VERSION;
|
|
45
|
+
readonly origins: readonly LeaseEntry[];
|
|
46
|
+
}
|
|
47
|
+
export type LeaseParse = {
|
|
48
|
+
readonly ok: true;
|
|
49
|
+
readonly file: LeaseFile;
|
|
50
|
+
} | {
|
|
51
|
+
readonly ok: false;
|
|
52
|
+
readonly problem: string;
|
|
53
|
+
};
|
|
54
|
+
/** Parse and judge one lease file text. Never throws. */
|
|
55
|
+
export declare function parseLeaseFile(text: string): LeaseParse;
|
|
56
|
+
/** The file text the server sends (the same object a successful {@link parseLeaseFile} returns). */
|
|
57
|
+
export declare function serializeLeaseFile(file: LeaseFile): string;
|
|
58
|
+
/** Is `o` exactly a URL origin (the one form the file carries)? */
|
|
59
|
+
export declare function isUrlOrigin(o: string): boolean;
|
|
60
|
+
//# sourceMappingURL=lease-file.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export const LEASE_FILE_VERSION = 1;
|
|
2
|
+
export const LEASE_TTL_MAX_MS = 300_000;
|
|
3
|
+
export const LEASE_ORIGINS_MAX = 16;
|
|
4
|
+
export const LEASE_MESSAGE_MAX_CHARS = 512;
|
|
5
|
+
const READY_KEYS = ["i", "origin", "state", "leaseId", "ttlMs", "username", "secret", "credentialId", "version", "unusableMessage", "rejectedMessage"];
|
|
6
|
+
const WITHDRAWN_KEYS = ["i", "origin", "state", "reason", "unusableMessage"];
|
|
7
|
+
const LEASE_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
8
|
+
const REASON_WORD = /^[a-z][a-z0-9_]{0,63}$/;
|
|
9
|
+
const CONTROL = /[\u0000-\u001f\u007f]/;
|
|
10
|
+
class Refusal extends Error {
|
|
11
|
+
}
|
|
12
|
+
const refuse = (problem) => {
|
|
13
|
+
throw new Refusal(problem);
|
|
14
|
+
};
|
|
15
|
+
export function parseLeaseFile(text) {
|
|
16
|
+
let raw;
|
|
17
|
+
try {
|
|
18
|
+
raw = JSON.parse(text);
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
return { ok: false, problem: `the lease file is not JSON (${e instanceof Error ? e.name : "parse error"})` };
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return { ok: true, file: judgeFile(raw) };
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
if (e instanceof Refusal)
|
|
28
|
+
return { ok: false, problem: e.message };
|
|
29
|
+
throw e;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function serializeLeaseFile(file) {
|
|
33
|
+
return JSON.stringify(file);
|
|
34
|
+
}
|
|
35
|
+
export function isUrlOrigin(o) {
|
|
36
|
+
if (!URL.canParse(o))
|
|
37
|
+
return false;
|
|
38
|
+
const u = new URL(o);
|
|
39
|
+
return (u.protocol === "https:" || u.protocol === "http:") && u.origin === o;
|
|
40
|
+
}
|
|
41
|
+
function judgeFile(raw) {
|
|
42
|
+
const top = asRecord(raw, "the lease file");
|
|
43
|
+
exactKeys(top, ["v", "origins"], "the lease file");
|
|
44
|
+
if (top.v !== LEASE_FILE_VERSION)
|
|
45
|
+
refuse(`the lease file's \`v\` is not ${LEASE_FILE_VERSION}`);
|
|
46
|
+
const list = top.origins;
|
|
47
|
+
if (!Array.isArray(list) || list.length === 0 || list.length > LEASE_ORIGINS_MAX)
|
|
48
|
+
refuse(`\`origins\` must hold 1–${LEASE_ORIGINS_MAX} entries`);
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const origins = list.map((item, at) => {
|
|
51
|
+
const entry = judgeEntry(item, at);
|
|
52
|
+
if (seen.has(entry.origin))
|
|
53
|
+
refuse(`entry ${at}: its origin repeats an earlier entry's`);
|
|
54
|
+
seen.add(entry.origin);
|
|
55
|
+
return entry;
|
|
56
|
+
});
|
|
57
|
+
return { v: LEASE_FILE_VERSION, origins };
|
|
58
|
+
}
|
|
59
|
+
function judgeEntry(item, at) {
|
|
60
|
+
const where = `entry ${at}`;
|
|
61
|
+
const e = asRecord(item, where);
|
|
62
|
+
if (e.i !== at)
|
|
63
|
+
refuse(`${where}: \`i\` is not its position (${at})`);
|
|
64
|
+
if (typeof e.origin !== "string" || !isUrlOrigin(e.origin))
|
|
65
|
+
refuse(`${where}: \`origin\` is not exactly a URL origin (scheme://host[:port], lower case, no default port, no path)`);
|
|
66
|
+
const origin = e.origin;
|
|
67
|
+
const unusableMessage = message(e.unusableMessage, `${where}: \`unusableMessage\``);
|
|
68
|
+
switch (e.state) {
|
|
69
|
+
case "ready": {
|
|
70
|
+
exactKeys(e, READY_KEYS, where);
|
|
71
|
+
if (!origin.startsWith("https://"))
|
|
72
|
+
refuse(`${where}: a \`ready\` entry's origin must be https (a credential never rides plain http)`);
|
|
73
|
+
if (typeof e.leaseId !== "string" || !LEASE_ID.test(e.leaseId))
|
|
74
|
+
refuse(`${where}: \`leaseId\` is not 1–128 of [A-Za-z0-9_-]`);
|
|
75
|
+
if (typeof e.ttlMs !== "number" || !Number.isInteger(e.ttlMs) || e.ttlMs <= 0 || e.ttlMs > LEASE_TTL_MAX_MS)
|
|
76
|
+
refuse(`${where}: \`ttlMs\` is not an integer in (0, ${LEASE_TTL_MAX_MS}]`);
|
|
77
|
+
if (typeof e.username !== "string" || e.username === "" || e.username.includes(":") || CONTROL.test(e.username))
|
|
78
|
+
refuse(`${where}: \`username\` is not a non-empty string without \`:\` or control characters`);
|
|
79
|
+
if (typeof e.secret !== "string" || e.secret === "" || CONTROL.test(e.secret))
|
|
80
|
+
refuse(`${where}: \`secret\` is not a non-empty string without control characters`);
|
|
81
|
+
if (typeof e.credentialId !== "string")
|
|
82
|
+
refuse(`${where}: \`credentialId\` is not a string`);
|
|
83
|
+
if (typeof e.version !== "number" || !Number.isInteger(e.version) || e.version < 0)
|
|
84
|
+
refuse(`${where}: \`version\` is not a non-negative integer`);
|
|
85
|
+
return {
|
|
86
|
+
i: at,
|
|
87
|
+
origin,
|
|
88
|
+
state: "ready",
|
|
89
|
+
leaseId: e.leaseId,
|
|
90
|
+
ttlMs: e.ttlMs,
|
|
91
|
+
username: e.username,
|
|
92
|
+
secret: e.secret,
|
|
93
|
+
credentialId: e.credentialId,
|
|
94
|
+
version: e.version,
|
|
95
|
+
unusableMessage,
|
|
96
|
+
rejectedMessage: message(e.rejectedMessage, `${where}: \`rejectedMessage\``),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
case "withdrawn":
|
|
100
|
+
exactKeys(e, WITHDRAWN_KEYS, where);
|
|
101
|
+
if (typeof e.reason !== "string" || !REASON_WORD.test(e.reason))
|
|
102
|
+
refuse(`${where}: \`reason\` is not a word`);
|
|
103
|
+
return { i: at, origin, state: "withdrawn", reason: e.reason, unusableMessage };
|
|
104
|
+
default:
|
|
105
|
+
return refuse(`${where}: \`state\` is neither "ready" nor "withdrawn"`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function asRecord(v, where) {
|
|
109
|
+
if (typeof v !== "object" || v === null || Array.isArray(v))
|
|
110
|
+
refuse(`${where} is not an object`);
|
|
111
|
+
return v;
|
|
112
|
+
}
|
|
113
|
+
function exactKeys(o, keys, where) {
|
|
114
|
+
const allowed = new Set(keys);
|
|
115
|
+
for (const k of Object.keys(o))
|
|
116
|
+
if (!allowed.has(k))
|
|
117
|
+
refuse(`${where}: unknown key ${JSON.stringify(k.slice(0, 40))}`);
|
|
118
|
+
for (const k of keys)
|
|
119
|
+
if (!Object.hasOwn(o, k))
|
|
120
|
+
refuse(`${where}: missing key \`${k}\``);
|
|
121
|
+
}
|
|
122
|
+
function message(v, where) {
|
|
123
|
+
if (typeof v !== "string" || v === "" || v.length > LEASE_MESSAGE_MAX_CHARS || CONTROL.test(v))
|
|
124
|
+
refuse(`${where} is not a single-line string of 1–${LEASE_MESSAGE_MAX_CHARS} characters`);
|
|
125
|
+
return v;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=lease-file.js.map
|