@sema-agent/server 7.151.0 → 7.152.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 +266 -0
- package/README.md +15 -3
- package/README.zh-CN.md +10 -2
- package/USAGE.md +93 -64
- package/deploy/sema-up/README.md +5 -4
- package/deploy/sema-up/cluster-up.sh +6 -6
- package/deploy/sema-up/kube-up.sh +5 -5
- package/deploy/sema-up/lib/flagshape.sh +36 -8
- package/deploy/sema-up/machines.example.yaml +3 -1
- package/deploy/sema-up/sema-up.sh +22 -16
- package/dist/approval-ask-audit-store.d.ts +1 -1
- package/dist/approval-ask-machine.d.ts +4 -10
- package/dist/approval-ask-retention.d.ts +1 -1
- package/dist/approval-card.d.ts +10 -12
- package/dist/approval-gate-currency.d.ts +8 -8
- package/dist/approval-gate-currency.js +3 -4
- package/dist/approval-hmac.d.ts +17 -1
- package/dist/approval-hmac.js +7 -1
- package/dist/approval-queue-kind.d.ts +1 -1
- package/dist/approval.d.ts +2 -2
- package/dist/approval.js +2 -2
- package/dist/bind-host.d.ts +1 -3
- package/dist/bind-host.js +2 -3
- package/dist/boot/coordinators.d.ts +1 -1
- package/dist/boot/coordinators.js +9 -10
- package/dist/boot/device-lane.js +2 -0
- package/dist/boot/execution-env.d.ts +1 -0
- package/dist/boot/execution-env.js +31 -5
- package/dist/boot/leader.d.ts +31 -30
- package/dist/boot/leader.js +38 -28
- package/dist/boot/leg-path-adjudication.d.ts +40 -0
- package/dist/boot/leg-path-adjudication.js +18 -0
- package/dist/boot/parked-revive-gate.js +3 -15
- package/dist/boot/reapers.d.ts +1 -1
- package/dist/boot/reapers.js +1 -1
- package/dist/boot/resolve-spec.js +12 -26
- package/dist/boot/runner-deps.d.ts +39 -35
- package/dist/boot/runner-deps.js +92 -89
- package/dist/boot/shutdown.d.ts +6 -0
- package/dist/boot/shutdown.js +10 -4
- package/dist/boot/stage-02-web-admin-pull.js +4 -5
- package/dist/boot/stage-05-execution-env.js +2 -1
- package/dist/boot/stage-06-runners.d.ts +2 -0
- package/dist/boot/stage-06-runners.js +10 -4
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.js +1 -1
- package/dist/boot/stage-08-reapers.d.ts +1 -0
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-09-leader.js +2 -2
- package/dist/boot/stage-10-http-server.d.ts +8 -0
- package/dist/boot/stage-10-http-server.js +11 -15
- package/dist/boot/stores.js +7 -4
- package/dist/boot/web-admin.js +2 -7
- package/dist/capabilities/approvals-push.d.ts +3 -3
- package/dist/capabilities/scenarios.d.ts +28 -13
- package/dist/capabilities/scenarios.js +2 -2
- package/dist/capability-facts.d.ts +22 -20
- package/dist/capability-facts.js +18 -34
- package/dist/card-rule-offers.d.ts +32 -0
- package/dist/card-rule-offers.js +130 -0
- package/dist/config-catalog.js +12 -11
- package/dist/config-dir-trust.js +4 -2
- package/dist/config-invariants.d.ts +18 -7
- package/dist/config-invariants.js +51 -8
- package/dist/config-lkg.d.ts +5 -19
- package/dist/config-lkg.js +8 -62
- package/dist/config-provider.d.ts +1 -1
- package/dist/config-root-migration.d.ts +2 -2
- package/dist/config-types.d.ts +28 -38
- package/dist/config.d.ts +7 -5
- package/dist/config.js +55 -16
- package/dist/control-socket.d.ts +51 -0
- package/dist/control-socket.js +115 -0
- package/dist/deployment-governance.d.ts +2 -1
- package/dist/device-ws-hub.d.ts +7 -0
- package/dist/device-ws-hub.js +7 -1
- package/dist/elicitation.js +2 -2
- package/dist/entry-problems.d.ts +40 -3
- package/dist/entry-problems.js +3 -1
- package/dist/execution-env-refusal.d.ts +8 -2
- package/dist/execution-env-refusal.js +3 -0
- package/dist/fleet/engine-signal.d.ts +12 -29
- package/dist/fleet/engine-signal.js +1 -7
- package/dist/fleet-client.js +2 -5
- package/dist/gate-settlement.d.ts +1 -8
- package/dist/gate-settlement.js +0 -4
- package/dist/history-anchor.js +1 -1
- package/dist/hitl-delivery.d.ts +2 -3
- package/dist/hitl-delivery.js +0 -2
- package/dist/hooks/hook-llm.js +4 -2
- package/dist/hooks/hook-runner.d.ts +3 -2
- package/dist/hooks/hook-runner.js +16 -1
- package/dist/host-decision.d.ts +11 -11
- package/dist/http/active-run-conflict.d.ts +35 -2
- package/dist/http/active-run-conflict.js +48 -19
- package/dist/http/approval-sweeps.d.ts +1 -1
- package/dist/http/approval-sweeps.js +5 -2
- package/dist/http/checkpoint-ctx.d.ts +2 -0
- package/dist/http/checkpoint-ctx.js +1 -0
- package/dist/http/dispatch.d.ts +6 -2
- package/dist/http/dispatch.js +25 -5
- package/dist/http/durable-run-leg.js +0 -1
- package/dist/http/health-reader.d.ts +16 -9
- package/dist/http/health-reader.js +2 -1
- package/dist/http/health-self-report.js +1 -1
- package/dist/http/owner-access.d.ts +2 -3
- package/dist/http/owner-access.js +3 -4
- package/dist/http/principal-gate.d.ts +49 -21
- package/dist/http/principal-gate.js +61 -11
- package/dist/http/request-channel.d.ts +23 -0
- package/dist/http/request-channel.js +9 -0
- package/dist/http/request-guards.js +17 -2
- package/dist/http/resume-ctx-read.js +20 -3
- package/dist/http/resume-legs.d.ts +2 -2
- package/dist/http/resume-legs.js +111 -41
- package/dist/http/route-ctx.d.ts +73 -7
- package/dist/http/route-ctx.js +17 -0
- package/dist/http/route-table.d.ts +4 -0
- package/dist/http/route-table.js +4 -0
- package/dist/http/routes/admin-config-refresh.js +3 -3
- package/dist/http/routes/admin-drain.d.ts +3 -0
- package/dist/http/routes/admin-drain.js +7 -7
- package/dist/http/routes/admin-memory-optout.js +3 -3
- package/dist/http/routes/adoption.js +3 -5
- package/dist/http/routes/approvals-assistant.d.ts +2 -1
- package/dist/http/routes/approvals-assistant.js +95 -48
- package/dist/http/routes/capabilities.d.ts +4 -0
- package/dist/http/routes/capabilities.js +30 -5
- package/dist/http/routes/config-catalog.js +2 -2
- package/dist/http/routes/devices.js +1 -1
- package/dist/http/routes/diagnostics.d.ts +3 -3
- package/dist/http/routes/diagnostics.js +19 -7
- package/dist/http/routes/images.js +10 -10
- package/dist/http/routes/memory-bundle.js +3 -3
- package/dist/http/routes/memory-compliance.js +3 -3
- package/dist/http/routes/memory-consolidation.js +2 -2
- package/dist/http/routes/memory-origin.js +3 -3
- package/dist/http/routes/memory-policy.js +2 -2
- package/dist/http/routes/observability.js +3 -3
- package/dist/http/routes/retention-ops.js +3 -3
- package/dist/http/routes/rules.js +5 -5
- package/dist/http/routes/run-permission-mode.d.ts +2 -1
- package/dist/http/routes/run-permission-mode.js +2 -2
- package/dist/http/routes/runs.d.ts +4 -9
- package/dist/http/routes/runs.js +73 -33
- package/dist/http/routes/session-sync.d.ts +18 -6
- package/dist/http/routes/session-sync.js +38 -36
- package/dist/http/routes/sessions.js +20 -9
- package/dist/http/routes/tasks.js +28 -37
- package/dist/http/routes/workflows.js +1 -1
- package/dist/http/server.d.ts +9 -3
- package/dist/http/server.js +15 -3
- package/dist/http/submit-ctx.d.ts +1 -1
- package/dist/http/submit-ctx.js +2 -0
- package/dist/human-facing-args.d.ts +33 -0
- package/dist/human-facing-args.js +33 -0
- package/dist/index.d.ts +1 -1
- package/dist/k8s-api-credentials.d.ts +29 -0
- package/dist/k8s-api-credentials.js +25 -0
- package/dist/lane-home-probe.d.ts +1 -1
- package/dist/lane-home-probe.js +2 -1
- package/dist/leader/diffup.d.ts +19 -0
- package/dist/leader/diffup.js +7 -0
- package/dist/leader/leader.d.ts +114 -10
- package/dist/leader/leader.js +69 -5
- package/dist/leader/repair-wire.js +2 -2
- package/dist/leader/run-entry.d.ts +2 -1
- package/dist/leader/run-entry.js +1 -1
- package/dist/leader/wire.d.ts +42 -132
- package/dist/leader/wire.js +81 -67
- package/dist/memory-scope.d.ts +20 -9
- package/dist/memory-scope.js +15 -3
- package/dist/model-entry-headers.d.ts +9 -10
- package/dist/model-entry-headers.js +0 -4
- package/dist/model-entry-refusal.d.ts +7 -4
- package/dist/model-entry-refusal.js +11 -5
- package/dist/model-plane-face.d.ts +1 -1
- package/dist/model-plane-face.js +3 -1
- package/dist/model-route-endpoint.d.ts +13 -7
- package/dist/model-route-endpoint.js +14 -11
- package/dist/model-select.d.ts +22 -0
- package/dist/model-select.js +9 -0
- package/dist/model-unreachable-notice.d.ts +11 -0
- package/dist/model-unreachable-notice.js +4 -4
- package/dist/object-store-buckets.d.ts +640 -6
- package/dist/object-store-buckets.js +166 -0
- package/dist/observability/fail-open.d.ts +20 -16
- package/dist/observability/fail-open.js +20 -16
- package/dist/observability/logger.js +3 -1
- package/dist/operator-roster.d.ts +68 -0
- package/dist/operator-roster.js +32 -0
- package/dist/orchestration/workflow-park-decision.d.ts +1 -1
- package/dist/outbound-dispatcher.d.ts +71 -8
- package/dist/outbound-dispatcher.js +48 -7
- package/dist/owner-faces.d.ts +1 -1
- package/dist/parked-decide.d.ts +2 -2
- package/dist/permission-mode-admission.d.ts +59 -26
- package/dist/permission-mode-admission.js +37 -10
- package/dist/plugins/approval-ask-store-file.d.ts +4 -0
- package/dist/plugins/approval-ask-store-file.js +3 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +4 -0
- package/dist/plugins/approval-ask-store-memory.js +7 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +26 -2
- package/dist/plugins/approval-ask-store-sql.js +12 -1
- package/dist/plugins/blob-backend.d.ts +83 -7
- package/dist/plugins/blob-backend.js +57 -6
- package/dist/plugins/checkpoint-index-columns.d.ts +20 -16
- package/dist/plugins/checkpoint-index-columns.js +3 -6
- package/dist/plugins/checkpoint-store-face.d.ts +119 -61
- package/dist/plugins/checkpoint-store-face.js +134 -55
- package/dist/plugins/landed-attendants.d.ts +2 -2
- package/dist/plugins/local-checkpoint-store.d.ts +13 -3
- package/dist/plugins/local-checkpoint-store.js +38 -49
- package/dist/plugins/one-time-migrations.d.ts +51 -21
- package/dist/plugins/one-time-migrations.js +67 -45
- package/dist/plugins/pg-pool.js +11 -9
- package/dist/plugins/remote-env-host.d.ts +29 -10
- package/dist/plugins/remote-env-host.js +99 -19
- package/dist/plugins/remote-env-k8s.d.ts +68 -2
- package/dist/plugins/remote-env-k8s.js +107 -24
- package/dist/plugins/replica-registration.d.ts +1 -1
- package/dist/plugins/replica-registration.js +1 -1
- package/dist/plugins/s3-presign.d.ts +23 -0
- package/dist/plugins/s3-presign.js +37 -3
- package/dist/plugins/scheduler-support.d.ts +2 -2
- package/dist/plugins/scheduler-support.js +0 -3
- package/dist/plugins/schema-indexes.js +2 -0
- package/dist/plugins/send-file-ledger.d.ts +2 -2
- package/dist/plugins/send-file-ledger.js +2 -2
- package/dist/plugins/send-user-file.d.ts +3 -4
- package/dist/plugins/send-user-file.js +56 -52
- package/dist/plugins/session-blob-claim-token.d.ts +15 -0
- package/dist/plugins/session-blob-claim-token.js +20 -0
- package/dist/plugins/session-blob-namespace.d.ts +46 -0
- package/dist/plugins/session-blob-namespace.js +116 -0
- package/dist/plugins/snapshot-bucket-presence.d.ts +25 -2
- package/dist/plugins/snapshot-bucket-presence.js +8 -2
- package/dist/plugins/sql-json-column.d.ts +1 -1
- package/dist/plugins/store-backend.d.ts +44 -2
- package/dist/plugins/store-backend.js +75 -53
- package/dist/plugins/task-attachment-store.d.ts +2 -1
- package/dist/plugins/task-attachment-store.js +5 -1
- package/dist/plugins/tidb-pool.js +19 -24
- package/dist/plugins/workspace-custody.d.ts +4 -3
- package/dist/question.d.ts +2 -1
- package/dist/question.js +5 -13
- package/dist/request-form.d.ts +6 -1
- package/dist/request-form.js +4 -1
- package/dist/rule-issue-reading.d.ts +1 -1
- package/dist/rules-consent.d.ts +1 -1
- package/dist/run-local.js +8 -9
- package/dist/runs.d.ts +17 -8
- package/dist/runs.js +15 -2
- package/dist/runtime-governance.d.ts +2 -2
- package/dist/sema-config-dir.d.ts +2 -1
- package/dist/session-sync.d.ts +27 -11
- package/dist/session-sync.js +15 -7
- package/dist/stores-sql/blob-index-sql.d.ts +30 -9
- package/dist/stores-sql/blob-index-sql.js +40 -21
- package/dist/stores-sql/checkpoint-store-sql.d.ts +54 -27
- package/dist/stores-sql/checkpoint-store-sql.js +70 -146
- package/dist/stores-sql/file-history-store-sql.d.ts +64 -16
- package/dist/stores-sql/file-history-store-sql.js +100 -28
- package/dist/stores-sql/retention-store-sql.js +3 -9
- package/dist/task-settings.d.ts +6 -5
- package/dist/task-settings.js +3 -3
- package/dist/tool-approval.d.ts +342 -518
- package/dist/tool-approval.js +498 -1207
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/ledger-events.d.ts +3 -3
- package/dist/trace/project.d.ts +2 -3
- package/dist/trace/redact.js +3 -2
- package/dist/trace/self-minted-wire.d.ts +287 -157
- package/dist/trace/self-minted-wire.js +93 -48
- package/dist/web-admin-client/apply-effective.js +2 -2
- package/dist/web-admin-client/read-warnings.d.ts +2 -2
- package/dist/web-admin-client/read-warnings.js +1 -1
- package/dist/web-admin-client/types.d.ts +1 -1
- package/dist/workspace-space.d.ts +10 -4
- package/dist/workspace-space.js +4 -1
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +1 -1
- package/package.json +6 -2
- package/scripts/embed-minimal.mjs +3 -3
- package/wire-changes/server@7.152.0.json +900 -0
- package/dist/config-d-migration.d.ts +0 -37
- package/dist/config-d-migration.js +0 -60
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
import { type ObjectStoreOp } from "../object-store-buckets.js";
|
|
2
|
+
/** The signed URL's lifetime under row `op` — the ONE reading of the table's TTL column. A row with no `knob` has a fixed TTL: the table's number, and a
|
|
3
|
+
* call that brings one is refused (its number would be a second source). A knob row takes `requested` (the configured value the call brings) clamped to
|
|
4
|
+
* the row's `maxSec` (the row's policy cap) — a call that brings none, or a value that is not a positive number of seconds (`0` is no lifetime at all),
|
|
5
|
+
* takes the row's `defaultSec` — the reading the `SESSION_SNAPSHOT_TTL_SEC` loader gives its `0` (any other non-positive spelling no longer loads: the configuration is refused) — and a row with no default needs a positive one.
|
|
6
|
+
* Every row, one answer for the SigV4 ceiling ({@link SIGV4_PRESIGN_MAX_SEC}): a lifetime over it is refused, never clamped (the knobs are refused above it
|
|
7
|
+
* when the configuration loads, so a call that gets here is a defect). */
|
|
8
|
+
export declare function rowTtlSec(op: ObjectStoreOp, requested: number | undefined): number;
|
|
1
9
|
export interface S3PresignParams {
|
|
10
|
+
/** The object-store action table row this request is (its action must be `method`). */
|
|
11
|
+
op: ObjectStoreOp;
|
|
2
12
|
/** Base endpoint, e.g. `http://minio.minio.svc.cluster.local:9000` (the host the SANDBOX will reach). */
|
|
3
13
|
endpoint: string;
|
|
4
14
|
bucket: string;
|
|
@@ -8,6 +18,7 @@ export interface S3PresignParams {
|
|
|
8
18
|
accessKey: string;
|
|
9
19
|
secretKey: string;
|
|
10
20
|
region?: string;
|
|
21
|
+
/** The configured TTL (seconds) of a knob row; a fixed-TTL row takes none (see {@link rowTtlSec}). */
|
|
11
22
|
expiresSec?: number;
|
|
12
23
|
/** Override the signing clock (tests). Defaults to now. */
|
|
13
24
|
now?: Date;
|
|
@@ -33,6 +44,8 @@ export declare function presignS3BucketUrl(p: S3BucketPresignParams): string;
|
|
|
33
44
|
/** ListObjectsV2 presign(D-1 孤儿对象 GC,clay 拍 2026-07-28):GET `/<bucket>/?list-type=2&prefix=…`。
|
|
34
45
|
* bucket 级 GET 是**列举能力**——只给 server 端 sweep 用,URL 绝不外发(与 copy 面同纪律)。 */
|
|
35
46
|
export declare function presignS3ListUrl(p: {
|
|
47
|
+
/** The object-store action table row this listing is (a LIST row). */
|
|
48
|
+
op: ObjectStoreOp;
|
|
36
49
|
endpoint: string;
|
|
37
50
|
bucket: string;
|
|
38
51
|
prefix: string;
|
|
@@ -45,6 +58,8 @@ export declare function presignS3ListUrl(p: {
|
|
|
45
58
|
now?: Date;
|
|
46
59
|
}): string;
|
|
47
60
|
export interface S3CopyPresignParams {
|
|
61
|
+
/** The object-store action table row this copy is (a COPY row). */
|
|
62
|
+
op: ObjectStoreOp;
|
|
48
63
|
/** INTERNAL endpoint — CopyObject is a SERVER-side request with adapter creds; never hand this URL out. */
|
|
49
64
|
endpoint: string;
|
|
50
65
|
/** Destination coordinates (the PUT target). */
|
|
@@ -69,4 +84,12 @@ export declare function presignS3CopyUrl(p: S3CopyPresignParams): {
|
|
|
69
84
|
url: string;
|
|
70
85
|
headers: Record<string, string>;
|
|
71
86
|
};
|
|
87
|
+
/** The URL of an object on an anonymous-read bucket (no signature, no expiry): `<endpoint>/<bucket>/<key>`, each key segment URI-encoded. `op` names
|
|
88
|
+
* the table's anonymous GET row; any other row is refused. */
|
|
89
|
+
export declare function buildPublicObjectUrl(p: {
|
|
90
|
+
op: ObjectStoreOp;
|
|
91
|
+
endpoint: string;
|
|
92
|
+
bucket: string;
|
|
93
|
+
key: string;
|
|
94
|
+
}): string;
|
|
72
95
|
//# sourceMappingURL=s3-presign.d.ts.map
|
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
import { createHash, createHmac } from "node:crypto";
|
|
2
|
+
import { SIGV4_PRESIGN_MAX_SEC, objectStoreActionOf } from "../object-store-buckets.js";
|
|
3
|
+
function assertRow(op, verb, signing) {
|
|
4
|
+
const row = objectStoreActionOf(op);
|
|
5
|
+
if (row.action !== verb)
|
|
6
|
+
throw new Error(`object-store seat: ${op} is a ${row.action} row — refusing to build a ${verb} request under it`);
|
|
7
|
+
if ((row.ttl === "unsigned") !== (signing === "anonymous")) {
|
|
8
|
+
throw new Error(`object-store seat: ${op} is ${row.ttl === "unsigned" ? "an anonymous" : "a signed"} row — refusing to build ${signing === "anonymous" ? "an anonymous" : "a signed"} URL under it`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function rowTtlSec(op, requested) {
|
|
12
|
+
const ttl = objectStoreActionOf(op).ttl;
|
|
13
|
+
if (ttl === "unsigned")
|
|
14
|
+
throw new Error(`object-store seat: ${op} is unsigned — it has no URL lifetime`);
|
|
15
|
+
if (ttl.knob === undefined) {
|
|
16
|
+
if (requested !== undefined)
|
|
17
|
+
throw new Error(`object-store seat: ${op} has a fixed TTL (${ttl.defaultSec} s, the table's) — refusing a call-site TTL of ${requested} s`);
|
|
18
|
+
return ttl.defaultSec;
|
|
19
|
+
}
|
|
20
|
+
const sec = requested !== undefined && requested > 0 ? requested : ttl.defaultSec;
|
|
21
|
+
if (sec === undefined)
|
|
22
|
+
throw new Error(`object-store seat: ${op} takes its TTL from ${ttl.knob} or its call — none was given`);
|
|
23
|
+
if (sec > SIGV4_PRESIGN_MAX_SEC) {
|
|
24
|
+
throw new Error(`object-store seat: ${op} was asked for a ${sec} s URL — a SigV4 presigned URL lives at most ${SIGV4_PRESIGN_MAX_SEC} s (7 days); ${ttl.knob} is refused above that when the configuration loads`);
|
|
25
|
+
}
|
|
26
|
+
return ttl.maxSec !== undefined ? Math.min(sec, ttl.maxSec) : sec;
|
|
27
|
+
}
|
|
2
28
|
function uriEncode(s, encodeSlash) {
|
|
3
29
|
let out = "";
|
|
4
30
|
for (const ch of Buffer.from(s, "utf8")) {
|
|
@@ -57,18 +83,21 @@ function presignCanonical(p) {
|
|
|
57
83
|
return `${url.origin}${canonicalUri}?${canonicalQuery}&X-Amz-Signature=${signature}`;
|
|
58
84
|
}
|
|
59
85
|
export function presignS3Url(p) {
|
|
86
|
+
assertRow(p.op, p.method, "signed");
|
|
87
|
+
const expiresSec = rowTtlSec(p.op, p.expiresSec);
|
|
60
88
|
if (p.key === "") {
|
|
61
89
|
throw new Error("presignS3Url: refusing to sign an empty key (single-object contract) — use presignS3BucketUrl for bucket-level operations");
|
|
62
90
|
}
|
|
63
91
|
if (p.key.startsWith("/") || p.key.split("/").some((seg) => seg === "." || seg === "..")) {
|
|
64
92
|
throw new Error(`presignS3Url: refusing to sign a key with a traversal segment: ${p.key}`);
|
|
65
93
|
}
|
|
66
|
-
return presignCanonical({ ...p, canonicalUri: `/${uriEncode(p.bucket, true)}/${uriEncode(p.key, false)}` });
|
|
94
|
+
return presignCanonical({ ...p, expiresSec, canonicalUri: `/${uriEncode(p.bucket, true)}/${uriEncode(p.key, false)}` });
|
|
67
95
|
}
|
|
68
96
|
export function presignS3BucketUrl(p) {
|
|
69
97
|
return presignCanonical({ ...p, canonicalUri: `/${uriEncode(p.bucket, true)}/` });
|
|
70
98
|
}
|
|
71
99
|
export function presignS3ListUrl(p) {
|
|
100
|
+
assertRow(p.op, "LIST", "signed");
|
|
72
101
|
return presignCanonical({
|
|
73
102
|
endpoint: p.endpoint,
|
|
74
103
|
canonicalUri: `/${uriEncode(p.bucket, true)}/`,
|
|
@@ -76,7 +105,7 @@ export function presignS3ListUrl(p) {
|
|
|
76
105
|
accessKey: p.accessKey,
|
|
77
106
|
secretKey: p.secretKey,
|
|
78
107
|
...(p.region ? { region: p.region } : {}),
|
|
79
|
-
|
|
108
|
+
expiresSec: rowTtlSec(p.op, p.expiresSec),
|
|
80
109
|
...(p.now ? { now: p.now } : {}),
|
|
81
110
|
extraQuery: {
|
|
82
111
|
"list-type": "2",
|
|
@@ -87,6 +116,7 @@ export function presignS3ListUrl(p) {
|
|
|
87
116
|
});
|
|
88
117
|
}
|
|
89
118
|
export function presignS3CopyUrl(p) {
|
|
119
|
+
assertRow(p.op, "COPY", "signed");
|
|
90
120
|
for (const [label, key] of [
|
|
91
121
|
["destination", p.key],
|
|
92
122
|
["source", p.sourceKey],
|
|
@@ -105,10 +135,14 @@ export function presignS3CopyUrl(p) {
|
|
|
105
135
|
accessKey: p.accessKey,
|
|
106
136
|
secretKey: p.secretKey,
|
|
107
137
|
...(p.region !== undefined ? { region: p.region } : {}),
|
|
108
|
-
|
|
138
|
+
expiresSec: rowTtlSec(p.op, p.expiresSec),
|
|
109
139
|
...(p.now !== undefined ? { now: p.now } : {}),
|
|
110
140
|
signedHeaders: { "x-amz-copy-source": copySource },
|
|
111
141
|
});
|
|
112
142
|
return { url, headers: { "x-amz-copy-source": copySource } };
|
|
113
143
|
}
|
|
144
|
+
export function buildPublicObjectUrl(p) {
|
|
145
|
+
assertRow(p.op, "GET", "anonymous");
|
|
146
|
+
return `${p.endpoint.replace(/\/+$/, "")}/${p.bucket}/${p.key.split("/").map(encodeURIComponent).join("/")}`;
|
|
147
|
+
}
|
|
114
148
|
//# sourceMappingURL=s3-presign.js.map
|
|
@@ -47,8 +47,8 @@ export interface SchedulerStoreLog {
|
|
|
47
47
|
* 的 `record` 可能是任意内容 —— 只报下标):
|
|
48
48
|
* · `unreadable-file` ⇒ warn(整份文件不是 JSON 数组:读成零条,写者一律不写 —— 见 `mutateSchedulerStore`);
|
|
49
49
|
* · `malformed-record` ⇒ warn(这一条读不懂、被跳过;下一个写者落盘时它就没了);
|
|
50
|
-
* · `stale-session-record` ⇒ info(会话链上过期 ≥ 7 天的记录,读时丢、下一个写者落盘)
|
|
51
|
-
*
|
|
50
|
+
* · `stale-session-record` ⇒ info(会话链上过期 ≥ 7 天的记录,读时丢、下一个写者落盘)。
|
|
51
|
+
* (settings-schema 26.0.0 删了调度店的读时重铸一步与它的收据臂:仍按会话链键存的 durable 记录逐字读出、不告警 —— 没有日志可写。)
|
|
52
52
|
*/
|
|
53
53
|
export declare function logSchedulerWarnings(log: SchedulerStoreLog, path: string, warnings: readonly SchedulerParseWarning[]): void;
|
|
54
54
|
/**
|
|
@@ -75,9 +75,6 @@ export function logSchedulerWarnings(log, path, warnings) {
|
|
|
75
75
|
case "stale-session-record":
|
|
76
76
|
log.info("scheduler_store_record_stale", { path, index: w.index, id: w.id, note: "a session-chained record whose next run is ≥ 7 days past — dropped on read (the next write persists the drop)" });
|
|
77
77
|
break;
|
|
78
|
-
case "one-time-migration":
|
|
79
|
-
log.info("scheduler_store_record_migrated", { path, index: w.index, migration: w.receipt.migration, appliedAs: w.receipt.appliedAs, note: "settings-schema one-time migration rewrote this record on read (the next write persists it)" });
|
|
80
|
-
break;
|
|
81
78
|
default: {
|
|
82
79
|
const unhandled = w;
|
|
83
80
|
throw new Error(`scheduler store: unknown read warning ${JSON.stringify(unhandled)}`);
|
|
@@ -20,6 +20,8 @@ export const CENTRAL_SCHEMA_INDEXES = [
|
|
|
20
20
|
{ table: "file_history_boundary_file", name: "idx_file_history_bf_key", columns: ["scope", "key_hash"] },
|
|
21
21
|
{ table: "file_history_boundary", name: "uq_file_history_boundary_seq", columns: ["scope", "publish_seq"], unique: true },
|
|
22
22
|
{ table: "file_history_blob", name: "idx_file_history_blob_created", columns: ["created_at"] },
|
|
23
|
+
{ table: "file_history_blob_grant", name: "idx_file_history_blob_grant_granted", columns: ["granted_at"] },
|
|
24
|
+
{ table: "file_history_blob_grant", name: "idx_file_history_blob_grant_blob", columns: ["blob_hash"] },
|
|
23
25
|
{ table: "workflow_run", name: "idx_wfrun_scope_created", columns: ["scope", "created_at_ms"] },
|
|
24
26
|
{ table: "workflow_run", name: "idx_wfrun_scope_status", columns: ["scope", "status"] },
|
|
25
27
|
{ table: "workflow_agent_session", name: "idx_wfagentsess_run", columns: ["run_id"] },
|
|
@@ -55,8 +55,8 @@ export declare class FileSendFileLedger implements SendFileLedger {
|
|
|
55
55
|
}
|
|
56
56
|
export interface LedgerRecordingDeps {
|
|
57
57
|
ledger: SendFileLedger;
|
|
58
|
-
/** Best-effort reclaim of the uploaded object when the record fails (issuer.deleteObject). */
|
|
59
|
-
deleteObject: (bucket: string, key: string) => Promise<void>;
|
|
58
|
+
/** Best-effort reclaim of the uploaded object when the record fails (issuer.deleteObject; `track` = the link's track). */
|
|
59
|
+
deleteObject: (bucket: string, key: string, track: IssuedFileLink["track"]) => Promise<void>;
|
|
60
60
|
warn?: (msg: string, meta?: Record<string, unknown>) => void;
|
|
61
61
|
now?: () => number;
|
|
62
62
|
newId?: () => string;
|
|
@@ -130,10 +130,10 @@ export function withLedgerRecording(send, deps) {
|
|
|
130
130
|
let reclaimed = true;
|
|
131
131
|
try {
|
|
132
132
|
try {
|
|
133
|
-
await deps.deleteObject(link.bucket, link.key);
|
|
133
|
+
await deps.deleteObject(link.bucket, link.key, link.track);
|
|
134
134
|
}
|
|
135
135
|
catch {
|
|
136
|
-
await deps.deleteObject(link.bucket, link.key);
|
|
136
|
+
await deps.deleteObject(link.bucket, link.key, link.track);
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
catch (de) {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** SigV4 physical ceiling (AWS: X-Amz-Expires max 604800s = 7 days). */
|
|
2
|
-
export declare const PRESIGN_MAX_TTL_SEC = 604800;
|
|
3
1
|
/**
|
|
4
2
|
* Multi-tenant governance (clay ruling 2026-07-14, blackboard [ref]② variant a with a HASHED segment): every
|
|
5
3
|
* object key carries a per-tenant SCOPE SEGMENT so tenants can be listed/revoked by prefix on the store itself,
|
|
@@ -102,8 +100,9 @@ export interface SendUserFileIssuer {
|
|
|
102
100
|
* semantics as issue() — prepare and finalize share ONE key, so the segment is fixed at prepare time. */
|
|
103
101
|
prepareDirectUpload(filename: string | undefined, ttlSec?: number, scope?: string): PreparedDirectUpload;
|
|
104
102
|
/** Best-effort object reclaim (ledger fail-loud path: an uploaded object whose ledger record failed would be
|
|
105
|
-
* an unmanageable orphan — delete it). Signed DELETE against the INTERNAL endpoint with adapter creds.
|
|
106
|
-
|
|
103
|
+
* an unmanageable orphan — delete it). Signed DELETE against the INTERNAL endpoint with adapter creds. `track` = the link's track
|
|
104
|
+
* (which object-store action row the delete is: the permanent track's bucket or the private one). */
|
|
105
|
+
deleteObject(bucket: string, key: string, track: IssuedFileLink["track"]): Promise<void>;
|
|
107
106
|
/** The public endpoint web clients need for rendering context (bootstrap/capabilities透出). */
|
|
108
107
|
readonly publicEndpoint: string | undefined;
|
|
109
108
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHmac } from "node:crypto";
|
|
2
2
|
import { uuidv7 } from "@sema-agent/core";
|
|
3
3
|
import { stripControlAndBidi } from "@sema-agent/settings-schema";
|
|
4
|
-
import { presignS3CopyUrl, presignS3Url } from "./s3-presign.js";
|
|
4
|
+
import { buildPublicObjectUrl, presignS3CopyUrl, presignS3Url, rowTtlSec } from "./s3-presign.js";
|
|
5
|
+
import { SENDFILE_STAGING_SEGMENT } from "../object-store-buckets.js";
|
|
5
6
|
import { OBJECT_STORE_IO_TIMEOUT_MS } from "./blob-backend.js";
|
|
6
7
|
import { createEgressFetch } from "../egress-redirect.js";
|
|
7
|
-
export const PRESIGN_MAX_TTL_SEC = 604_800;
|
|
8
8
|
export function scopeSegment(secretKey, scope) {
|
|
9
9
|
if (scope === undefined || scope === "")
|
|
10
10
|
return "_";
|
|
@@ -30,23 +30,27 @@ export function sanitizeSendFileName(name) {
|
|
|
30
30
|
export function createSendUserFileIssuer(cfg, deps) {
|
|
31
31
|
const f = createEgressFetch("declare the final object-store address as MINIO_ENDPOINT / S3_ENDPOINT", deps?.fetchImpl);
|
|
32
32
|
const bounded = () => ({ signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
const trackOps = (track) => track === "public"
|
|
34
|
+
? { put: "sendfile/public:PUT", copy: "sendfile/public:COPY", head: "sendfile/public:HEAD", delete: "sendfile/public:DELETE" }
|
|
35
|
+
: { put: "sendfile/private:PUT", copy: "sendfile/private:COPY", head: "sendfile/private:HEAD", delete: "sendfile/private:DELETE" };
|
|
36
|
+
const upload = async (op, bucket, key, bytes) => {
|
|
37
|
+
if (deps?.upload)
|
|
38
|
+
return deps.upload(bucket, key, bytes);
|
|
39
|
+
const url = presignS3Url({
|
|
40
|
+
op,
|
|
41
|
+
endpoint: cfg.endpoint,
|
|
42
|
+
bucket,
|
|
43
|
+
key,
|
|
44
|
+
method: "PUT",
|
|
45
|
+
accessKey: cfg.accessKey,
|
|
46
|
+
secretKey: cfg.secretKey,
|
|
47
|
+
...(cfg.region !== undefined ? { region: cfg.region } : {}),
|
|
48
|
+
...(deps?.now !== undefined ? { now: deps.now() } : {}),
|
|
49
49
|
});
|
|
50
|
+
const res = await f(url, { method: "PUT", body: Buffer.from(bytes), ...bounded() });
|
|
51
|
+
if (!res.ok)
|
|
52
|
+
throw new Error(`SendUserFile upload failed: ${res.status} ${res.statusText} (bucket=${bucket})`);
|
|
53
|
+
};
|
|
50
54
|
const issue = async (bytes, filename, ttlSec, scope) => {
|
|
51
55
|
if (cfg.publicEndpoint === undefined) {
|
|
52
56
|
throw new Error("SendUserFile is not configured for public links: set S3_PUBLIC_ENDPOINT (the S3/MinIO base URL users can reach; falls back to S3_ENDPOINT when that is already public)");
|
|
@@ -60,16 +64,15 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
60
64
|
const scopeField = scope !== undefined && scope !== "" ? { scope } : {};
|
|
61
65
|
if (ttl === 0) {
|
|
62
66
|
const key = `${seg}/${uuidv7()}/${name}`;
|
|
63
|
-
await upload(cfg.publicBucket, key, bytes);
|
|
64
|
-
const url =
|
|
67
|
+
await upload(trackOps("public").put, cfg.publicBucket, key, bytes);
|
|
68
|
+
const url = buildPublicObjectUrl({ op: "sendfile/public:GET", endpoint: publicBase, bucket: cfg.publicBucket, key });
|
|
65
69
|
return { url, filename: name, size: bytes.byteLength, ttlSec: 0, track: "public", bucket: cfg.publicBucket, key, ...scopeField };
|
|
66
70
|
}
|
|
67
|
-
|
|
68
|
-
throw new Error(`SendUserFile ttl ${ttl}s exceeds the SigV4 presign ceiling (${PRESIGN_MAX_TTL_SEC}s = 7 days) — use ttl=0 for a permanent link`);
|
|
69
|
-
}
|
|
71
|
+
const expiresSec = rowTtlSec("sendfile/private:GET", ttl);
|
|
70
72
|
const key = `${cfg.privateKeyPrefix}${seg}/${uuidv7()}/${name}`;
|
|
71
|
-
await upload(cfg.privateBucket, key, bytes);
|
|
73
|
+
await upload(trackOps("presigned").put, cfg.privateBucket, key, bytes);
|
|
72
74
|
const url = presignS3Url({
|
|
75
|
+
op: "sendfile/private:GET",
|
|
73
76
|
endpoint: publicBase,
|
|
74
77
|
bucket: cfg.privateBucket,
|
|
75
78
|
key,
|
|
@@ -77,10 +80,10 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
77
80
|
accessKey: cfg.accessKey,
|
|
78
81
|
secretKey: cfg.secretKey,
|
|
79
82
|
...(cfg.region !== undefined ? { region: cfg.region } : {}),
|
|
80
|
-
expiresSec
|
|
83
|
+
expiresSec,
|
|
81
84
|
...(deps?.now !== undefined ? { now: deps.now() } : {}),
|
|
82
85
|
});
|
|
83
|
-
return { url, filename: name, size: bytes.byteLength, ttlSec:
|
|
86
|
+
return { url, filename: name, size: bytes.byteLength, ttlSec: expiresSec, track: "presigned", bucket: cfg.privateBucket, key, ...scopeField };
|
|
84
87
|
};
|
|
85
88
|
const prepareDirectUpload = (filename, ttlSec, scope) => {
|
|
86
89
|
if (cfg.publicEndpoint === undefined) {
|
|
@@ -89,19 +92,20 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
89
92
|
const ttl = ttlSec ?? cfg.defaultTtlSec;
|
|
90
93
|
if (!Number.isInteger(ttl) || ttl < 0)
|
|
91
94
|
throw new Error(`SendUserFile ttl must be a non-negative INTEGER of seconds (0 = permanent; SigV4 X-Amz-Expires is integer-only); got ${ttl}`);
|
|
92
|
-
if (ttl >
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
+
if (ttl > 0)
|
|
96
|
+
rowTtlSec("sendfile/private:GET", ttl);
|
|
95
97
|
const name = sanitizeSendFileName(filename);
|
|
96
98
|
const publicBase = cfg.publicEndpoint.replace(/\/+$/, "");
|
|
97
99
|
const putBase = (cfg.sandboxPutEndpoint ?? cfg.publicEndpoint).replace(/\/+$/, "");
|
|
98
100
|
const bucket = ttl === 0 ? cfg.publicBucket : cfg.privateBucket;
|
|
101
|
+
const finalOps = trackOps(ttl === 0 ? "public" : "presigned");
|
|
99
102
|
const seg = scopeSegment(cfg.secretKey, scope);
|
|
100
103
|
const scopeField = scope !== undefined && scope !== "" ? { scope } : {};
|
|
101
104
|
const key = ttl === 0 ? `${seg}/${uuidv7()}/${name}` : `${cfg.privateKeyPrefix}${seg}/${uuidv7()}/${name}`;
|
|
102
105
|
const stagingBucket = cfg.privateBucket;
|
|
103
|
-
const stagingKey = `${cfg.privateKeyPrefix}
|
|
106
|
+
const stagingKey = `${cfg.privateKeyPrefix}${SENDFILE_STAGING_SEGMENT}${seg}/${uuidv7()}`;
|
|
104
107
|
const putUrl = presignS3Url({
|
|
108
|
+
op: "sendfile/staging:PUT",
|
|
105
109
|
endpoint: putBase,
|
|
106
110
|
bucket: stagingBucket,
|
|
107
111
|
key: stagingKey,
|
|
@@ -109,13 +113,13 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
109
113
|
accessKey: cfg.accessKey,
|
|
110
114
|
secretKey: cfg.secretKey,
|
|
111
115
|
...(cfg.region !== undefined ? { region: cfg.region } : {}),
|
|
112
|
-
expiresSec: 120,
|
|
113
116
|
...(deps?.now !== undefined ? { now: deps.now() } : {}),
|
|
114
117
|
});
|
|
115
118
|
const verifySize = async (maxBytes) => {
|
|
116
119
|
if (deps?.verify)
|
|
117
120
|
return deps.verify(bucket, key, maxBytes);
|
|
118
|
-
const sign = (method, b, k) => presignS3Url({
|
|
121
|
+
const sign = (op, method, b, k) => presignS3Url({
|
|
122
|
+
op,
|
|
119
123
|
endpoint: cfg.endpoint,
|
|
120
124
|
bucket: b,
|
|
121
125
|
key: k,
|
|
@@ -123,11 +127,10 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
123
127
|
accessKey: cfg.accessKey,
|
|
124
128
|
secretKey: cfg.secretKey,
|
|
125
129
|
...(cfg.region !== undefined ? { region: cfg.region } : {}),
|
|
126
|
-
expiresSec: 60,
|
|
127
130
|
});
|
|
128
|
-
const reclaim = async (b, k) => {
|
|
131
|
+
const reclaim = async (op, b, k) => {
|
|
129
132
|
try {
|
|
130
|
-
const r = await f(sign("DELETE", b, k), { method: "DELETE", ...bounded() });
|
|
133
|
+
const r = await f(sign(op, "DELETE", b, k), { method: "DELETE", ...bounded() });
|
|
131
134
|
return r.ok || r.status === 404;
|
|
132
135
|
}
|
|
133
136
|
catch {
|
|
@@ -137,28 +140,29 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
137
140
|
const outcome = (ok, what) => (ok ? `${what} reclaimed` : `${what} reclaim FAILED (orphan; operator cleanup needed)`);
|
|
138
141
|
let head;
|
|
139
142
|
try {
|
|
140
|
-
head = await f(sign("HEAD", stagingBucket, stagingKey), { method: "HEAD", ...bounded() });
|
|
143
|
+
head = await f(sign("sendfile/staging:HEAD", "HEAD", stagingBucket, stagingKey), { method: "HEAD", ...bounded() });
|
|
141
144
|
}
|
|
142
145
|
catch (e) {
|
|
143
|
-
const r = await reclaim(stagingBucket, stagingKey);
|
|
146
|
+
const r = await reclaim("sendfile/staging:DELETE", stagingBucket, stagingKey);
|
|
144
147
|
throw new Error(`SendUserFile upload verification failed: store unreachable during HEAD (${e instanceof Error ? e.message : String(e)}) — ${outcome(r, "staged object")}`);
|
|
145
148
|
}
|
|
146
149
|
if (head.status === 404)
|
|
147
150
|
throw new Error("SendUserFile upload verification failed: object not found after upload (HEAD 404)");
|
|
148
151
|
if (!head.ok) {
|
|
149
|
-
const r = await reclaim(stagingBucket, stagingKey);
|
|
152
|
+
const r = await reclaim("sendfile/staging:DELETE", stagingBucket, stagingKey);
|
|
150
153
|
throw new Error(`SendUserFile upload verification failed: store error (HEAD ${head.status}) — ${outcome(r, "staged object")}`);
|
|
151
154
|
}
|
|
152
155
|
const staged = Number.parseInt(head.headers.get("content-length") ?? "", 10);
|
|
153
156
|
if (!Number.isFinite(staged) || staged < 0) {
|
|
154
|
-
const r = await reclaim(stagingBucket, stagingKey);
|
|
157
|
+
const r = await reclaim("sendfile/staging:DELETE", stagingBucket, stagingKey);
|
|
155
158
|
throw new Error(`SendUserFile upload verification failed: store returned no content-length — ${outcome(r, "staged object")}`);
|
|
156
159
|
}
|
|
157
160
|
if (staged > maxBytes) {
|
|
158
|
-
await reclaim(stagingBucket, stagingKey);
|
|
161
|
+
await reclaim("sendfile/staging:DELETE", stagingBucket, stagingKey);
|
|
159
162
|
throw new Error(`file exceeds the ${maxBytes >= 1024 * 1024 ? `${Math.floor(maxBytes / (1024 * 1024))} MiB` : `${maxBytes} byte`} per-file cap (uploaded object was ${staged} bytes — deleted)`);
|
|
160
163
|
}
|
|
161
164
|
const copy = presignS3CopyUrl({
|
|
165
|
+
op: finalOps.copy,
|
|
162
166
|
endpoint: cfg.endpoint,
|
|
163
167
|
bucket,
|
|
164
168
|
key,
|
|
@@ -167,7 +171,6 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
167
171
|
accessKey: cfg.accessKey,
|
|
168
172
|
secretKey: cfg.secretKey,
|
|
169
173
|
...(cfg.region !== undefined ? { region: cfg.region } : {}),
|
|
170
|
-
expiresSec: 60,
|
|
171
174
|
...(deps?.now !== undefined ? { now: deps.now() } : {}),
|
|
172
175
|
});
|
|
173
176
|
try {
|
|
@@ -177,40 +180,41 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
177
180
|
throw new Error(`copy returned ${res.status}`);
|
|
178
181
|
}
|
|
179
182
|
catch (e) {
|
|
180
|
-
const r1 = await reclaim(stagingBucket, stagingKey);
|
|
181
|
-
const r2 = await reclaim(bucket, key);
|
|
183
|
+
const r1 = await reclaim("sendfile/staging:DELETE", stagingBucket, stagingKey);
|
|
184
|
+
const r2 = await reclaim(finalOps.delete, bucket, key);
|
|
182
185
|
throw new Error(`SendUserFile upload verification failed: staging promotion failed (${e instanceof Error ? e.message : String(e)}) — ${outcome(r1 && r2, "uploaded object")}`);
|
|
183
186
|
}
|
|
184
|
-
await reclaim(stagingBucket, stagingKey);
|
|
187
|
+
await reclaim("sendfile/staging:DELETE", stagingBucket, stagingKey);
|
|
185
188
|
let fin;
|
|
186
189
|
try {
|
|
187
|
-
fin = await f(sign("HEAD", bucket, key), { method: "HEAD", ...bounded() });
|
|
190
|
+
fin = await f(sign(finalOps.head, "HEAD", bucket, key), { method: "HEAD", ...bounded() });
|
|
188
191
|
}
|
|
189
192
|
catch (e) {
|
|
190
|
-
const r = await reclaim(bucket, key);
|
|
193
|
+
const r = await reclaim(finalOps.delete, bucket, key);
|
|
191
194
|
throw new Error(`SendUserFile upload verification failed: store unreachable during final HEAD (${e instanceof Error ? e.message : String(e)}) — ${outcome(r, "object")}`);
|
|
192
195
|
}
|
|
193
196
|
if (!fin.ok) {
|
|
194
|
-
const r = await reclaim(bucket, key);
|
|
197
|
+
const r = await reclaim(finalOps.delete, bucket, key);
|
|
195
198
|
throw new Error(`SendUserFile upload verification failed: final object HEAD ${fin.status} — ${outcome(r, "object")}`);
|
|
196
199
|
}
|
|
197
200
|
const size = Number.parseInt(fin.headers.get("content-length") ?? "", 10);
|
|
198
201
|
if (!Number.isFinite(size) || size < 0) {
|
|
199
|
-
const r = await reclaim(bucket, key);
|
|
202
|
+
const r = await reclaim(finalOps.delete, bucket, key);
|
|
200
203
|
throw new Error(`SendUserFile upload verification failed: store returned no content-length — ${outcome(r, "object")}`);
|
|
201
204
|
}
|
|
202
205
|
if (size > maxBytes) {
|
|
203
|
-
await reclaim(bucket, key);
|
|
206
|
+
await reclaim(finalOps.delete, bucket, key);
|
|
204
207
|
throw new Error(`file exceeds the ${maxBytes >= 1024 * 1024 ? `${Math.floor(maxBytes / (1024 * 1024))} MiB` : `${maxBytes} byte`} per-file cap (uploaded object was ${size} bytes — deleted)`);
|
|
205
208
|
}
|
|
206
209
|
return size;
|
|
207
210
|
};
|
|
208
211
|
const finalize = (size) => {
|
|
209
212
|
if (ttl === 0) {
|
|
210
|
-
const url =
|
|
213
|
+
const url = buildPublicObjectUrl({ op: "sendfile/public:GET", endpoint: publicBase, bucket, key });
|
|
211
214
|
return { url, filename: name, size, ttlSec: 0, track: "public", bucket, key, ...scopeField };
|
|
212
215
|
}
|
|
213
216
|
const url = presignS3Url({
|
|
217
|
+
op: "sendfile/private:GET",
|
|
214
218
|
endpoint: publicBase,
|
|
215
219
|
bucket,
|
|
216
220
|
key,
|
|
@@ -225,10 +229,11 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
225
229
|
};
|
|
226
230
|
return { putUrl, verifySize, finalize };
|
|
227
231
|
};
|
|
228
|
-
const deleteObject = async (bucket, key) => {
|
|
232
|
+
const deleteObject = async (bucket, key, track) => {
|
|
229
233
|
if (deps?.remove)
|
|
230
234
|
return deps.remove(bucket, key);
|
|
231
235
|
const url = presignS3Url({
|
|
236
|
+
op: trackOps(track).delete,
|
|
232
237
|
endpoint: cfg.endpoint,
|
|
233
238
|
bucket,
|
|
234
239
|
key,
|
|
@@ -236,7 +241,6 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
236
241
|
accessKey: cfg.accessKey,
|
|
237
242
|
secretKey: cfg.secretKey,
|
|
238
243
|
...(cfg.region !== undefined ? { region: cfg.region } : {}),
|
|
239
|
-
expiresSec: 60,
|
|
240
244
|
...(deps?.now !== undefined ? { now: deps.now() } : {}),
|
|
241
245
|
});
|
|
242
246
|
const res = await f(url, { method: "DELETE", ...bounded() });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This database's claim token on the session blob namespace (`session-blob-namespace.ts`): one row, minted once per database (absent ⇒ the first writer's
|
|
3
|
+
* stays; every replica of the database reads the same), never rewritten. The store's open reads it (`StoreBackend.ensureSchema()`) and hands it to the
|
|
4
|
+
* namespace's claim; nothing else reads or writes the table.
|
|
5
|
+
*/
|
|
6
|
+
import type { SqlDialect, SqlExec } from "./sql-driver.js";
|
|
7
|
+
/** The table that keeps this database's claim token (one row, `id = 1`). */
|
|
8
|
+
export declare const SESSION_BLOB_NAMESPACE_CLAIM_TABLE = "blob_namespace_claim";
|
|
9
|
+
/** The create statement (both dialects; idempotent). The central schema statements read it (`tidb-pool.ts` / `pg-pool.ts`). */
|
|
10
|
+
export declare function sessionBlobNamespaceClaimDdl(dialect: SqlDialect): string;
|
|
11
|
+
/** This database's claim token: minted if absent, then read (the store's open; a failure is the open's failure). */
|
|
12
|
+
export declare function ensureSessionBlobClaimToken(db: SqlExec & {
|
|
13
|
+
readonly dialect: SqlDialect;
|
|
14
|
+
}): Promise<string>;
|
|
15
|
+
//# sourceMappingURL=session-blob-claim-token.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export const SESSION_BLOB_NAMESPACE_CLAIM_TABLE = "blob_namespace_claim";
|
|
3
|
+
export function sessionBlobNamespaceClaimDdl(dialect) {
|
|
4
|
+
return dialect === "mysql"
|
|
5
|
+
? `CREATE TABLE IF NOT EXISTS ${SESSION_BLOB_NAMESPACE_CLAIM_TABLE} (id SMALLINT NOT NULL PRIMARY KEY, claim_token VARCHAR(64) NOT NULL, minted_at_ms BIGINT NOT NULL) COLLATE utf8mb4_bin`
|
|
6
|
+
: `CREATE TABLE IF NOT EXISTS ${SESSION_BLOB_NAMESPACE_CLAIM_TABLE} (id SMALLINT NOT NULL PRIMARY KEY, claim_token VARCHAR(64) COLLATE "C" NOT NULL, minted_at_ms BIGINT NOT NULL)`;
|
|
7
|
+
}
|
|
8
|
+
const MINT_SQL = {
|
|
9
|
+
mysql: `INSERT INTO ${SESSION_BLOB_NAMESPACE_CLAIM_TABLE} (id, claim_token, minted_at_ms) VALUES (1, ?, ?) ON DUPLICATE KEY UPDATE id = id`,
|
|
10
|
+
pg: `INSERT INTO ${SESSION_BLOB_NAMESPACE_CLAIM_TABLE} (id, claim_token, minted_at_ms) VALUES (1, $1, $2) ON CONFLICT (id) DO NOTHING`,
|
|
11
|
+
};
|
|
12
|
+
export async function ensureSessionBlobClaimToken(db) {
|
|
13
|
+
await db.query(MINT_SQL[db.dialect], [randomUUID(), Date.now()]);
|
|
14
|
+
const { rows } = await db.query(`SELECT claim_token FROM ${SESSION_BLOB_NAMESPACE_CLAIM_TABLE} WHERE id = 1`);
|
|
15
|
+
const token = rows[0]?.claim_token;
|
|
16
|
+
if (typeof token !== "string" || token === "")
|
|
17
|
+
throw new Error(`${SESSION_BLOB_NAMESPACE_CLAIM_TABLE} holds no claim token after the mint`);
|
|
18
|
+
return token;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=session-blob-claim-token.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type SessionBlobStoreConfig } from "./blob-backend.js";
|
|
2
|
+
import { StoreBootRefusalError } from "./store-contracts.js";
|
|
3
|
+
/** After a create-only write, how long to wait before reading the claim back (once per namespace, on its first claim). */
|
|
4
|
+
export declare const SESSION_BLOB_CLAIM_SETTLE_MS = 1000;
|
|
5
|
+
/** One attempt's reading (closed set). */
|
|
6
|
+
export type SessionBlobClaimReading = {
|
|
7
|
+
readonly state: "mine";
|
|
8
|
+
} | {
|
|
9
|
+
readonly state: "foreign";
|
|
10
|
+
readonly holder: string;
|
|
11
|
+
} | {
|
|
12
|
+
readonly state: "unproven";
|
|
13
|
+
readonly detail: string;
|
|
14
|
+
};
|
|
15
|
+
/** The store open's refusal: the namespace is another database's (or the store was opened without binding this database's token). */
|
|
16
|
+
export declare class SessionBlobNamespaceRefusedError extends StoreBootRefusalError {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
/** The live namespace seat of one database over one bucket × prefix. */
|
|
20
|
+
export interface SessionBlobNamespace {
|
|
21
|
+
/** The bucket config the namespace's ports are built from (`createSessionBlobBackend`). */
|
|
22
|
+
readonly store: SessionBlobStoreConfig;
|
|
23
|
+
/** Bind this database's claim token (`ensureSessionBlobClaimToken`, read by the store's `ensureSchema()`). */
|
|
24
|
+
bind(token: string): void;
|
|
25
|
+
/** One attempt of the claim under `token` (no retry). */
|
|
26
|
+
claimOnce(token: string): Promise<SessionBlobClaimReading>;
|
|
27
|
+
/** Boot, under the bound token, one attempt: `mine` ⇒ resolves; `foreign` ⇒ refuses; `unproven` ⇒ resolves through the registered F-class arm
|
|
28
|
+
* (deletes stay refused until a later reading is `mine`: {@link assertOwned}). */
|
|
29
|
+
establish(): Promise<void>;
|
|
30
|
+
/** Every delete's gate: reads the claim afresh (never remembered) and resolves only when it names this database; rejects otherwise — a namespace with no
|
|
31
|
+
* bound token (the store's `ensureSchema()` never ran) has nothing to prove with. */
|
|
32
|
+
assertOwned(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export interface SessionBlobNamespaceOptions {
|
|
35
|
+
/** Test seam; production = the global fetch (through the egress-redirect rule). */
|
|
36
|
+
readonly fetchImpl?: typeof fetch;
|
|
37
|
+
/** Test seam for the settle wait; production = a timer. */
|
|
38
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
/** The refusal when another database holds the namespace (the one sentence; the boot refusal and a refused delete both say it). */
|
|
41
|
+
export declare function buildSessionBlobNamespaceForeignText(store: SessionBlobStoreConfig, holder: string, mine: string): string;
|
|
42
|
+
/** A delete's refusal when the claim could not be read or written (the delete is retried by its sweep / release on its next call, and goes through once
|
|
43
|
+
* the claim reads as this database's). */
|
|
44
|
+
export declare function buildSessionBlobNamespaceUnprovenText(store: SessionBlobStoreConfig, detail: string): string;
|
|
45
|
+
export declare function createSessionBlobNamespace(store: SessionBlobStoreConfig, opts?: SessionBlobNamespaceOptions): SessionBlobNamespace;
|
|
46
|
+
//# sourceMappingURL=session-blob-namespace.d.ts.map
|