@stigmer/server 3.22.1 → 3.23.1-dev.20260923075205
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/dist/artifactstorage/artifact-storage.d.ts +29 -31
- package/dist/artifactstorage/artifact-storage.d.ts.map +1 -1
- package/dist/artifactstorage/artifact-storage.js +5 -7
- package/dist/artifactstorage/artifact-storage.js.map +1 -1
- package/dist/artifactstorage/r2-storage.d.ts +12 -7
- package/dist/artifactstorage/r2-storage.d.ts.map +1 -1
- package/dist/artifactstorage/r2-storage.js +6 -14
- package/dist/artifactstorage/r2-storage.js.map +1 -1
- package/dist/authorization/authorizer.d.ts +4 -8
- package/dist/authorization/authorizer.d.ts.map +1 -1
- package/dist/authorization/authorizer.js +5 -9
- package/dist/authorization/authorizer.js.map +1 -1
- package/dist/authorization/derived-tuples.d.ts.map +1 -1
- package/dist/authorization/derived-tuples.js +10 -25
- package/dist/authorization/derived-tuples.js.map +1 -1
- package/dist/authorization/evaluator.d.ts +7 -8
- package/dist/authorization/evaluator.d.ts.map +1 -1
- package/dist/authorization/evaluator.js +4 -20
- package/dist/authorization/evaluator.js.map +1 -1
- package/dist/authorization/list-read-scope.d.ts.map +1 -1
- package/dist/authorization/list-read-scope.js +5 -9
- package/dist/authorization/list-read-scope.js.map +1 -1
- package/dist/authorization/model/agent.d.ts.map +1 -1
- package/dist/authorization/model/agent.js +4 -5
- package/dist/authorization/model/agent.js.map +1 -1
- package/dist/authorization/model/agent_instance.d.ts.map +1 -1
- package/dist/authorization/model/agent_instance.js +2 -2
- package/dist/authorization/model/agent_instance.js.map +1 -1
- package/dist/authorization/model/mcp_server.d.ts.map +1 -1
- package/dist/authorization/model/mcp_server.js +2 -2
- package/dist/authorization/model/mcp_server.js.map +1 -1
- package/dist/authorization/model/plugin.d.ts.map +1 -1
- package/dist/authorization/model/plugin.js +3 -4
- package/dist/authorization/model/plugin.js.map +1 -1
- package/dist/authorization/model/rewrite.d.ts +8 -10
- package/dist/authorization/model/rewrite.d.ts.map +1 -1
- package/dist/authorization/model/rewrite.js +0 -3
- package/dist/authorization/model/rewrite.js.map +1 -1
- package/dist/authorization/model/skill.d.ts.map +1 -1
- package/dist/authorization/model/skill.js +2 -2
- package/dist/authorization/model/skill.js.map +1 -1
- package/dist/authorization/model/workflow.d.ts.map +1 -1
- package/dist/authorization/model/workflow.js +2 -2
- package/dist/authorization/model/workflow.js.map +1 -1
- package/dist/authorization/model/workflow_instance.d.ts.map +1 -1
- package/dist/authorization/model/workflow_instance.js +2 -2
- package/dist/authorization/model/workflow_instance.js.map +1 -1
- package/dist/authorization/organization-directory.d.ts.map +1 -1
- package/dist/authorization/organization-directory.js +2 -3
- package/dist/authorization/organization-directory.js.map +1 -1
- package/dist/authorization/store-test-kit.d.ts +0 -6
- package/dist/authorization/store-test-kit.d.ts.map +1 -1
- package/dist/authorization/store-test-kit.js +16 -62
- package/dist/authorization/store-test-kit.js.map +1 -1
- package/dist/authorization/tuples.d.ts +13 -32
- package/dist/authorization/tuples.d.ts.map +1 -1
- package/dist/authorization/tuples.js +3 -11
- package/dist/authorization/tuples.js.map +1 -1
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +49 -24
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/config.d.ts +4 -3
- package/dist/boot/config.d.ts.map +1 -1
- package/dist/boot/config.js.map +1 -1
- package/dist/boot/skill-transfer-origin.d.ts +52 -0
- package/dist/boot/skill-transfer-origin.d.ts.map +1 -0
- package/dist/boot/skill-transfer-origin.js +54 -0
- package/dist/boot/skill-transfer-origin.js.map +1 -0
- package/dist/domain/agent/controller.d.ts.map +1 -1
- package/dist/domain/agent/controller.js +4 -4
- package/dist/domain/agent/controller.js.map +1 -1
- package/dist/domain/agentchannel/controller.d.ts.map +1 -1
- package/dist/domain/agentchannel/controller.js +3 -1
- package/dist/domain/agentchannel/controller.js.map +1 -1
- package/dist/domain/agentexecution/controller.d.ts.map +1 -1
- package/dist/domain/agentexecution/controller.js +3 -1
- package/dist/domain/agentexecution/controller.js.map +1 -1
- package/dist/domain/agentinstance/controller.d.ts.map +1 -1
- package/dist/domain/agentinstance/controller.js +3 -4
- package/dist/domain/agentinstance/controller.js.map +1 -1
- package/dist/domain/agentshare/constants.d.ts +6 -11
- package/dist/domain/agentshare/constants.d.ts.map +1 -1
- package/dist/domain/agentshare/constants.js +8 -16
- package/dist/domain/agentshare/constants.js.map +1 -1
- package/dist/domain/agentshare/controller.d.ts.map +1 -1
- package/dist/domain/agentshare/controller.js +3 -1
- package/dist/domain/agentshare/controller.js.map +1 -1
- package/dist/domain/agentshare/steps.d.ts +18 -25
- package/dist/domain/agentshare/steps.d.ts.map +1 -1
- package/dist/domain/agentshare/steps.js +41 -142
- package/dist/domain/agentshare/steps.js.map +1 -1
- package/dist/domain/environment/controller.d.ts.map +1 -1
- package/dist/domain/environment/controller.js +2 -3
- package/dist/domain/environment/controller.js.map +1 -1
- package/dist/domain/executioncontext/controller.d.ts.map +1 -1
- package/dist/domain/executioncontext/controller.js +2 -1
- package/dist/domain/executioncontext/controller.js.map +1 -1
- package/dist/domain/mcpserver/controller.d.ts.map +1 -1
- package/dist/domain/mcpserver/controller.js +3 -4
- package/dist/domain/mcpserver/controller.js.map +1 -1
- package/dist/domain/plugin/controller.d.ts +3 -7
- package/dist/domain/plugin/controller.d.ts.map +1 -1
- package/dist/domain/plugin/controller.js +9 -11
- package/dist/domain/plugin/controller.js.map +1 -1
- package/dist/domain/plugin/push.d.ts +7 -5
- package/dist/domain/plugin/push.d.ts.map +1 -1
- package/dist/domain/plugin/push.js +8 -14
- package/dist/domain/plugin/push.js.map +1 -1
- package/dist/domain/schedule/controller.d.ts.map +1 -1
- package/dist/domain/schedule/controller.js +3 -1
- package/dist/domain/schedule/controller.js.map +1 -1
- package/dist/domain/session/controller.d.ts.map +1 -1
- package/dist/domain/session/controller.js +3 -2
- package/dist/domain/session/controller.js.map +1 -1
- package/dist/domain/skill/constants.d.ts +26 -3
- package/dist/domain/skill/constants.d.ts.map +1 -1
- package/dist/domain/skill/constants.js +26 -3
- package/dist/domain/skill/constants.js.map +1 -1
- package/dist/domain/skill/controller.d.ts +6 -11
- package/dist/domain/skill/controller.d.ts.map +1 -1
- package/dist/domain/skill/controller.js +33 -24
- package/dist/domain/skill/controller.js.map +1 -1
- package/dist/domain/skill/push.d.ts +2 -2
- package/dist/domain/skill/push.d.ts.map +1 -1
- package/dist/domain/skill/push.js +2 -2
- package/dist/domain/skill/push.js.map +1 -1
- package/dist/domain/skill/transfer/handler.d.ts +17 -9
- package/dist/domain/skill/transfer/handler.d.ts.map +1 -1
- package/dist/domain/skill/transfer/handler.js +19 -8
- package/dist/domain/skill/transfer/handler.js.map +1 -1
- package/dist/domain/skill/transfer/slots.d.ts +26 -41
- package/dist/domain/skill/transfer/slots.d.ts.map +1 -1
- package/dist/domain/skill/transfer/slots.js +80 -87
- package/dist/domain/skill/transfer/slots.js.map +1 -1
- package/dist/domain/skill/transfer/staging.d.ts +48 -0
- package/dist/domain/skill/transfer/staging.d.ts.map +1 -0
- package/dist/domain/skill/transfer/staging.js +127 -0
- package/dist/domain/skill/transfer/staging.js.map +1 -0
- package/dist/domain/workflow/agent-call-references.d.ts +54 -0
- package/dist/domain/workflow/agent-call-references.d.ts.map +1 -0
- package/dist/domain/workflow/agent-call-references.js +97 -0
- package/dist/domain/workflow/agent-call-references.js.map +1 -0
- package/dist/domain/workflow/controller.d.ts.map +1 -1
- package/dist/domain/workflow/controller.js +14 -6
- package/dist/domain/workflow/controller.js.map +1 -1
- package/dist/domain/workflow/registry/data/task-kind-registry.json +1 -1
- package/dist/domain/workflow/validation/task-config-constraints.d.ts +10 -1
- package/dist/domain/workflow/validation/task-config-constraints.d.ts.map +1 -1
- package/dist/domain/workflow/validation/task-config-constraints.js +3 -2
- package/dist/domain/workflow/validation/task-config-constraints.js.map +1 -1
- package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
- package/dist/domain/workflowexecution/controller.js +4 -2
- package/dist/domain/workflowexecution/controller.js.map +1 -1
- package/dist/domain/workflowinstance/controller.d.ts.map +1 -1
- package/dist/domain/workflowinstance/controller.js +3 -4
- package/dist/domain/workflowinstance/controller.js.map +1 -1
- package/dist/extensions/authorization-queries.d.ts +6 -5
- package/dist/extensions/authorization-queries.d.ts.map +1 -1
- package/dist/extensions/list-read-scope.d.ts +1 -1
- package/dist/extensions/resource-authorization.d.ts +3 -2
- package/dist/extensions/resource-authorization.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/apiresource-meta.d.ts +12 -3
- package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
- package/dist/pipeline/apiresource-meta.js +21 -10
- package/dist/pipeline/apiresource-meta.js.map +1 -1
- package/dist/pipeline/steps/authorization-tuples.d.ts +9 -3
- package/dist/pipeline/steps/authorization-tuples.d.ts.map +1 -1
- package/dist/pipeline/steps/authorization-tuples.js +9 -11
- package/dist/pipeline/steps/authorization-tuples.js.map +1 -1
- package/dist/pipeline/steps/references.d.ts +178 -20
- package/dist/pipeline/steps/references.d.ts.map +1 -1
- package/dist/pipeline/steps/references.js +344 -43
- package/dist/pipeline/steps/references.js.map +1 -1
- package/dist/pipeline/steps/resolve-artifact-source.d.ts +11 -8
- package/dist/pipeline/steps/resolve-artifact-source.d.ts.map +1 -1
- package/dist/pipeline/steps/resolve-artifact-source.js +8 -4
- package/dist/pipeline/steps/resolve-artifact-source.js.map +1 -1
- package/dist/query/search/controller.js +0 -1
- package/dist/query/search/controller.js.map +1 -1
- package/dist/query/search/criteria.d.ts +1 -5
- package/dist/query/search/criteria.d.ts.map +1 -1
- package/dist/query/search/criteria.js +3 -13
- package/dist/query/search/criteria.js.map +1 -1
- package/dist/query/search/handler.d.ts +5 -4
- package/dist/query/search/handler.d.ts.map +1 -1
- package/dist/query/search/handler.js +7 -6
- package/dist/query/search/handler.js.map +1 -1
- package/dist/query/search/query-store.d.ts.map +1 -1
- package/dist/query/search/query-store.js +0 -2
- package/dist/query/search/query-store.js.map +1 -1
- package/dist/store/interface.d.ts +3 -6
- package/dist/store/interface.d.ts.map +1 -1
- package/dist/store/interface.js.map +1 -1
- package/dist/store/postgres/migrations.d.ts +3 -1
- package/dist/store/postgres/migrations.d.ts.map +1 -1
- package/dist/store/postgres/migrations.js +35 -1
- package/dist/store/postgres/migrations.js.map +1 -1
- package/dist/store/postgres/store.d.ts.map +1 -1
- package/dist/store/postgres/store.js +4 -9
- package/dist/store/postgres/store.js.map +1 -1
- package/dist/store/public-visibility-retired.d.ts +67 -0
- package/dist/store/public-visibility-retired.d.ts.map +1 -0
- package/dist/store/public-visibility-retired.js +55 -0
- package/dist/store/public-visibility-retired.js.map +1 -0
- package/dist/store/sqlite/migrations.d.ts +3 -1
- package/dist/store/sqlite/migrations.d.ts.map +1 -1
- package/dist/store/sqlite/migrations.js +36 -1
- package/dist/store/sqlite/migrations.js.map +1 -1
- package/dist/store/sqlite/store.d.ts.map +1 -1
- package/dist/store/sqlite/store.js +4 -8
- package/dist/store/sqlite/store.js.map +1 -1
- package/package.json +6 -6
- package/src/artifactstorage/__tests__/artifact-storage.test.ts +36 -33
- package/src/artifactstorage/__tests__/r2-storage.test.ts +11 -13
- package/src/artifactstorage/artifact-storage.ts +37 -33
- package/src/artifactstorage/r2-storage.ts +6 -15
- package/src/authorization/README.md +24 -26
- package/src/authorization/__tests__/authorizer.test.ts +10 -9
- package/src/authorization/__tests__/derived-tuples.test.ts +4 -11
- package/src/authorization/__tests__/evaluator.test.ts +14 -73
- package/src/authorization/__tests__/facts.test.ts +2 -2
- package/src/authorization/__tests__/fixtures/fga/agent-instance-creation.fga.yaml +19 -27
- package/src/authorization/__tests__/fixtures/fga/blueprint-private-visibility.fga.yaml +6 -98
- package/src/authorization/__tests__/fixtures/fga/default-instance-inheritance.fga.yaml +4 -61
- package/src/authorization/__tests__/fixtures/fga/mcp-server-authoring.fga.yaml +1 -15
- package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +1 -21
- package/src/authorization/__tests__/fixtures/fga/platform-visibility.fga.yaml +1 -25
- package/src/authorization/__tests__/fixtures/fga/workflow-execution-sharing.fga.yaml +1 -3
- package/src/authorization/__tests__/list-read-scope.test.ts +18 -17
- package/src/authorization/__tests__/store-tests.test.ts +2 -6
- package/src/authorization/__tests__/tuples.test.ts +12 -11
- package/src/authorization/authorizer.ts +4 -9
- package/src/authorization/derived-tuples.ts +14 -31
- package/src/authorization/evaluator.ts +8 -44
- package/src/authorization/list-read-scope.ts +5 -11
- package/src/authorization/model/agent.ts +2 -5
- package/src/authorization/model/agent_instance.ts +0 -2
- package/src/authorization/model/mcp_server.ts +0 -2
- package/src/authorization/model/plugin.ts +1 -4
- package/src/authorization/model/rewrite.ts +8 -13
- package/src/authorization/model/skill.ts +0 -2
- package/src/authorization/model/workflow.ts +0 -2
- package/src/authorization/model/workflow_instance.ts +0 -2
- package/src/authorization/organization-directory.ts +1 -3
- package/src/authorization/store-test-kit.ts +16 -75
- package/src/authorization/tuples.ts +16 -39
- package/src/boot/__tests__/skill-transfer-origin.test.ts +72 -0
- package/src/boot/compose.ts +62 -25
- package/src/boot/config.ts +4 -3
- package/src/boot/skill-transfer-origin.ts +76 -0
- package/src/domain/agent/__tests__/agent.test.ts +11 -4
- package/src/domain/agent/controller.ts +7 -7
- package/src/domain/agentchannel/__tests__/agentchannel.test.ts +223 -48
- package/src/domain/agentchannel/controller.ts +6 -1
- package/src/domain/agentexecution/controller.ts +6 -1
- package/src/domain/agentinstance/controller.ts +6 -12
- package/src/domain/agentshare/__tests__/agentshare.test.ts +196 -172
- package/src/domain/agentshare/__tests__/create-authorization.test.ts +16 -31
- package/src/domain/agentshare/constants.ts +14 -26
- package/src/domain/agentshare/controller.ts +6 -1
- package/src/domain/agentshare/steps.ts +41 -194
- package/src/domain/environment/controller.ts +5 -8
- package/src/domain/executioncontext/controller.ts +5 -1
- package/src/domain/mcpserver/__tests__/mcpserver.test.ts +115 -28
- package/src/domain/mcpserver/__tests__/oauth-handshake.test.ts +53 -18
- package/src/domain/mcpserver/controller.ts +6 -12
- package/src/domain/plugin/controller.ts +16 -25
- package/src/domain/plugin/push.ts +8 -16
- package/src/domain/schedule/controller.ts +6 -1
- package/src/domain/session/controller.ts +12 -4
- package/src/domain/skill/__tests__/skill.test.ts +5 -3
- package/src/domain/skill/__tests__/slots.test.ts +113 -60
- package/src/domain/skill/__tests__/staging.test.ts +304 -0
- package/src/domain/skill/constants.ts +28 -3
- package/src/domain/skill/controller.ts +44 -42
- package/src/domain/skill/push.ts +3 -3
- package/src/domain/skill/transfer/handler.ts +33 -15
- package/src/domain/skill/transfer/slots.ts +89 -100
- package/src/domain/skill/transfer/staging.ts +191 -0
- package/src/domain/workflow/__tests__/agent-call-references.test.ts +222 -0
- package/src/domain/workflow/agent-call-references.ts +157 -0
- package/src/domain/workflow/controller.ts +28 -10
- package/src/domain/workflow/registry/data/task-kind-registry.json +1 -1
- package/src/domain/workflow/validation/task-config-constraints.ts +7 -3
- package/src/domain/workflowexecution/controller.ts +7 -2
- package/src/domain/workflowinstance/controller.ts +6 -12
- package/src/extensions/__tests__/built-in-authorization-composed.test.ts +32 -19
- package/src/extensions/__tests__/extension-composition.test.ts +4 -4
- package/src/extensions/__tests__/list-read-scope-composed.test.ts +9 -6
- package/src/extensions/authorization-queries.ts +6 -5
- package/src/extensions/list-read-scope.ts +1 -1
- package/src/extensions/resource-authorization.ts +3 -5
- package/src/index.ts +0 -1
- package/src/pipeline/__tests__/supports-visibility.test.ts +112 -0
- package/src/pipeline/apiresource-meta.ts +21 -10
- package/src/pipeline/steps/__tests__/authorization-tuples.test.ts +18 -31
- package/src/pipeline/steps/__tests__/references.test.ts +703 -0
- package/src/pipeline/steps/authorization-tuples.ts +9 -11
- package/src/pipeline/steps/references.ts +578 -70
- package/src/pipeline/steps/resolve-artifact-source.ts +20 -13
- package/src/query/search/__tests__/criteria.test.ts +0 -4
- package/src/query/search/__tests__/query-store.test.ts +11 -40
- package/src/query/search/controller.ts +0 -1
- package/src/query/search/criteria.ts +0 -14
- package/src/query/search/handler.ts +6 -7
- package/src/query/search/query-store.ts +0 -2
- package/src/store/__tests__/public-visibility-retired.test.ts +127 -0
- package/src/store/__tests__/store-contract.ts +6 -51
- package/src/store/interface.ts +3 -6
- package/src/store/postgres/__tests__/migrations.test.ts +187 -6
- package/src/store/postgres/__tests__/store-contract.test.ts +0 -2
- package/src/store/postgres/migrations.ts +48 -1
- package/src/store/postgres/store.ts +4 -11
- package/src/store/public-visibility-retired.ts +119 -0
- package/src/store/sqlite/__tests__/migrations.test.ts +191 -5
- package/src/store/sqlite/migrations.ts +45 -1
- package/src/store/sqlite/store.ts +4 -10
- package/src/transport/registry/__tests__/lanes.test.ts +1 -1
- package/dist/pipeline/steps/visibility-gates.d.ts +0 -27
- package/dist/pipeline/steps/visibility-gates.d.ts.map +0 -1
- package/dist/pipeline/steps/visibility-gates.js +0 -139
- package/dist/pipeline/steps/visibility-gates.js.map +0 -1
- package/src/authorization/__tests__/fixtures/fga/cross-org-share-creation.fga.yaml +0 -98
- package/src/authorization/__tests__/fixtures/fga/public-visibility-setter.fga.yaml +0 -52
- package/src/pipeline/steps/__tests__/visibility-gates.test.ts +0 -187
- package/src/pipeline/steps/visibility-gates.ts +0 -174
|
@@ -85,7 +85,10 @@ import {
|
|
|
85
85
|
TARGET_RESOURCE_KEY,
|
|
86
86
|
newLoadTargetStep,
|
|
87
87
|
} from "../../pipeline/steps/load-target.js";
|
|
88
|
-
import {
|
|
88
|
+
import {
|
|
89
|
+
newNormalizeReferencesStep,
|
|
90
|
+
newValidateReferencesStep,
|
|
91
|
+
} from "../../pipeline/steps/references.js";
|
|
89
92
|
import {
|
|
90
93
|
newCleanupIamPoliciesStep,
|
|
91
94
|
newCreateAuthorizationTuplesStep,
|
|
@@ -208,7 +211,8 @@ async function createSession(
|
|
|
208
211
|
.addStep(newCheckDuplicateStep(deps.store))
|
|
209
212
|
.addStep(newBuildNewStateStep())
|
|
210
213
|
.addStep(newGuardReservedLabelsStep(deps.authorizer))
|
|
211
|
-
.addStep(newNormalizeReferencesStep())
|
|
214
|
+
.addStep(newNormalizeReferencesStep())
|
|
215
|
+
.addStep(newValidateReferencesStep(deps.store));
|
|
212
216
|
// The ratified pre-side-effect gate slot (blueprint 03 §3a; O4; Q2
|
|
213
217
|
// ruling — see the create doc comment). Empty in OSS.
|
|
214
218
|
for (const step of stepsForSlot<typeof SessionSchema>(
|
|
@@ -499,7 +503,9 @@ async function list(
|
|
|
499
503
|
newAuthorizeStep(SessionQueryController.method.list, deps.authorizer),
|
|
500
504
|
)
|
|
501
505
|
.addStep(newValidateProtoStep())
|
|
502
|
-
.addStep(
|
|
506
|
+
.addStep(
|
|
507
|
+
newListAllSessionsStep(deps.store, deps.logger, deps.listReadScope),
|
|
508
|
+
)
|
|
503
509
|
.build()
|
|
504
510
|
.execute(reqCtx);
|
|
505
511
|
return requireListResult(reqCtx.get(LIST_RESULT_KEY));
|
|
@@ -559,7 +565,9 @@ async function listByChannel(
|
|
|
559
565
|
)
|
|
560
566
|
.addStep(newValidateProtoStep())
|
|
561
567
|
.addStep(newAuthorizeChannelAccessStep(deps.authorizer))
|
|
562
|
-
.addStep(
|
|
568
|
+
.addStep(
|
|
569
|
+
newFilterByChannelStep(deps.store, deps.logger, deps.listReadScope),
|
|
570
|
+
)
|
|
563
571
|
.build()
|
|
564
572
|
.execute(reqCtx);
|
|
565
573
|
return requireListResult(reqCtx.get(LIST_RESULT_KEY));
|
|
@@ -434,7 +434,7 @@ describe("transfer lane (#675) — mint → PUT → push-by-ref → download", (
|
|
|
434
434
|
);
|
|
435
435
|
});
|
|
436
436
|
|
|
437
|
-
it("a ref whose slot was minted but never uploaded is rejected at push", async () => {
|
|
437
|
+
it("a ref whose slot was minted but never uploaded is rejected at push — the one condition every driver can answer", async () => {
|
|
438
438
|
const minted = await command.createArtifactUploadUrl({ org: ORG, sizeBytes: 1024n });
|
|
439
439
|
const err = await expectCode(
|
|
440
440
|
command.push({ org: ORG, artifactUploadRef: minted.artifactUploadRef }),
|
|
@@ -442,7 +442,7 @@ describe("transfer lane (#675) — mint → PUT → push-by-ref → download", (
|
|
|
442
442
|
"empty slot",
|
|
443
443
|
);
|
|
444
444
|
expect(err.rawMessage).toBe(
|
|
445
|
-
"artifact_upload_ref not usable: upload reference
|
|
445
|
+
"artifact_upload_ref not usable: upload reference unknown or expired — request a new upload URL via createArtifactUploadUrl",
|
|
446
446
|
);
|
|
447
447
|
});
|
|
448
448
|
|
|
@@ -499,7 +499,9 @@ describe("transfer lane (#675) — mint → PUT → push-by-ref → download", (
|
|
|
499
499
|
const minted = await query.getArtifactDownloadUrl({
|
|
500
500
|
artifactStorageKey: pushed.status!.artifactStorageKey,
|
|
501
501
|
});
|
|
502
|
-
|
|
502
|
+
// The floor of the URL's validity, the same on every driver; the local
|
|
503
|
+
// lane's content-hash URL simply outlives it.
|
|
504
|
+
expect(minted.ttlSeconds).toBe(3600);
|
|
503
505
|
expect(minted.sizeBytes).toBe(BigInt(artifact.length));
|
|
504
506
|
|
|
505
507
|
const resp = await fetch(minted.url);
|
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pins the upload-slot registry against Go's slots_test.go
|
|
3
|
-
*
|
|
4
|
-
* (
|
|
5
|
-
*
|
|
2
|
+
* Pins the upload-slot registry against Go's slots_test.go, keyed by the
|
|
3
|
+
* domain's staging key: reservation bounds and key shape, TTL expiry
|
|
4
|
+
* (injected clock), exact-declared-size enforcement (short AND long
|
|
5
|
+
* bodies), single upload per key, staged-file placement under the driver
|
|
6
|
+
* root, expiry sweep on reservation, the boot-time staging wipe, and the
|
|
7
|
+
* staging directory's recreation after the driver's delete pruned it.
|
|
6
8
|
*/
|
|
7
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdtempSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from "node:fs";
|
|
8
18
|
import { tmpdir } from "node:os";
|
|
9
19
|
import path from "node:path";
|
|
10
20
|
import { Readable } from "node:stream";
|
|
11
21
|
|
|
12
22
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
13
23
|
|
|
24
|
+
import { STAGING_KEY_PREFIX } from "../constants.js";
|
|
14
25
|
import {
|
|
15
26
|
SizeMismatchError,
|
|
16
27
|
SlotConsumedError,
|
|
17
|
-
SlotEmptyError,
|
|
18
28
|
SlotUnknownError,
|
|
19
29
|
UploadSlots,
|
|
20
30
|
} from "../transfer/slots.js";
|
|
31
|
+
import { newUploadRef, stagingKeyOf } from "../transfer/staging.js";
|
|
21
32
|
|
|
22
33
|
const TTL_MS = 15 * 60 * 1000;
|
|
23
34
|
const MAX_SIZE = 100 * 1024 * 1024;
|
|
24
35
|
|
|
25
|
-
let
|
|
36
|
+
let root: string;
|
|
26
37
|
let stagingDir: string;
|
|
27
38
|
let nowMs: number;
|
|
28
39
|
let slots: UploadSlots;
|
|
@@ -31,101 +42,143 @@ function body(text: string): Readable {
|
|
|
31
42
|
return Readable.from([Buffer.from(text)]);
|
|
32
43
|
}
|
|
33
44
|
|
|
45
|
+
/** A fresh, well-formed staging key — what the domain's mint hands the registry. */
|
|
46
|
+
function freshKey(): string {
|
|
47
|
+
const key = stagingKeyOf(newUploadRef());
|
|
48
|
+
if (key === undefined) throw new Error("a minted ref always has a key");
|
|
49
|
+
return key;
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
beforeEach(() => {
|
|
35
|
-
|
|
36
|
-
stagingDir = path.join(
|
|
53
|
+
root = mkdtempSync(path.join(tmpdir(), "skill-slots-test-"));
|
|
54
|
+
stagingDir = path.join(root, STAGING_KEY_PREFIX);
|
|
37
55
|
nowMs = 1_000_000;
|
|
38
|
-
slots = new UploadSlots(
|
|
56
|
+
slots = new UploadSlots(
|
|
57
|
+
root,
|
|
58
|
+
STAGING_KEY_PREFIX,
|
|
59
|
+
TTL_MS,
|
|
60
|
+
MAX_SIZE,
|
|
61
|
+
() => nowMs,
|
|
62
|
+
);
|
|
39
63
|
});
|
|
40
64
|
|
|
41
65
|
afterEach(() => {
|
|
42
|
-
rmSync(
|
|
66
|
+
rmSync(root, { recursive: true, force: true });
|
|
43
67
|
});
|
|
44
68
|
|
|
45
69
|
describe("UploadSlots", () => {
|
|
46
|
-
it("
|
|
47
|
-
|
|
48
|
-
expect(ref).toMatch(/^sau_[0-9a-f]{32}$/);
|
|
49
|
-
expect(ttlMs).toBe(TTL_MS);
|
|
70
|
+
it("reserves a key with the registry TTL", () => {
|
|
71
|
+
expect(slots.reserve(freshKey(), 1024)).toEqual({ ttlMs: TTL_MS });
|
|
50
72
|
});
|
|
51
73
|
|
|
52
|
-
it("rejects out-of-bounds declarations at
|
|
53
|
-
expect(() => slots.
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
it("rejects out-of-bounds declarations at reservation", () => {
|
|
75
|
+
expect(() => slots.reserve(freshKey(), 0)).toThrow(
|
|
76
|
+
"declared size 0 outside (0, 104857600]",
|
|
77
|
+
);
|
|
78
|
+
expect(() => slots.reserve(freshKey(), -1)).toThrow(
|
|
79
|
+
`declared size -1 outside (0, ${MAX_SIZE}]`,
|
|
80
|
+
);
|
|
81
|
+
expect(() => slots.reserve(freshKey(), MAX_SIZE + 1)).toThrow(
|
|
56
82
|
`declared size ${MAX_SIZE + 1} outside (0, ${MAX_SIZE}]`,
|
|
57
83
|
);
|
|
58
84
|
});
|
|
59
85
|
|
|
60
|
-
it("
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
expect(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
expect(
|
|
86
|
+
it("refuses a key outside the staging prefix or not shaped like a staged archive", () => {
|
|
87
|
+
expect(() => slots.reserve("skills/deadbeef.zip", 1)).toThrow(
|
|
88
|
+
`staging key skills/deadbeef.zip is outside ${STAGING_KEY_PREFIX}`,
|
|
89
|
+
);
|
|
90
|
+
expect(() =>
|
|
91
|
+
slots.reserve(`${STAGING_KEY_PREFIX}../escape.zip`, 1),
|
|
92
|
+
).toThrow("does not name a staged archive");
|
|
93
|
+
expect(() => slots.reserve(`${STAGING_KEY_PREFIX}notes.txt`, 1)).toThrow(
|
|
94
|
+
"does not name a staged archive",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("refuses to reserve the same key twice", () => {
|
|
99
|
+
const key = freshKey();
|
|
100
|
+
slots.reserve(key, 1);
|
|
101
|
+
expect(() => slots.reserve(key, 1)).toThrow(
|
|
102
|
+
`staging key ${key} is already reserved`,
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("stages a received body at the key under the root — where the driver reads it", async () => {
|
|
107
|
+
const key = freshKey();
|
|
108
|
+
slots.reserve(key, 5);
|
|
109
|
+
await slots.receive(key, body("hello"));
|
|
110
|
+
expect(readFileSync(path.join(root, key), "utf8")).toBe("hello");
|
|
68
111
|
});
|
|
69
112
|
|
|
70
|
-
it("rejects receive on an
|
|
71
|
-
await expect(slots.receive(
|
|
113
|
+
it("rejects receive on an unreserved key", async () => {
|
|
114
|
+
await expect(slots.receive(freshKey(), body("x"))).rejects.toThrow(
|
|
115
|
+
SlotUnknownError,
|
|
116
|
+
);
|
|
72
117
|
});
|
|
73
118
|
|
|
74
|
-
it("rejects receive on an expired
|
|
75
|
-
const
|
|
119
|
+
it("rejects receive on an expired key", async () => {
|
|
120
|
+
const key = freshKey();
|
|
121
|
+
slots.reserve(key, 1);
|
|
76
122
|
nowMs += TTL_MS + 1;
|
|
77
|
-
await expect(slots.receive(
|
|
123
|
+
await expect(slots.receive(key, body("x"))).rejects.toThrow(
|
|
124
|
+
SlotUnknownError,
|
|
125
|
+
);
|
|
78
126
|
});
|
|
79
127
|
|
|
80
|
-
it("rejects a second upload to the same
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
await
|
|
128
|
+
it("rejects a second upload to the same key", async () => {
|
|
129
|
+
const key = freshKey();
|
|
130
|
+
slots.reserve(key, 1);
|
|
131
|
+
await slots.receive(key, body("x"));
|
|
132
|
+
await expect(slots.receive(key, body("x"))).rejects.toThrow(
|
|
133
|
+
SlotConsumedError,
|
|
134
|
+
);
|
|
84
135
|
});
|
|
85
136
|
|
|
86
137
|
it("rejects a short body, naming both sizes, and stages nothing", async () => {
|
|
87
|
-
const
|
|
88
|
-
|
|
138
|
+
const key = freshKey();
|
|
139
|
+
slots.reserve(key, 10);
|
|
140
|
+
await expect(slots.receive(key, body("short"))).rejects.toThrow(
|
|
89
141
|
"upload size mismatch: received 5 bytes, declared 10",
|
|
90
142
|
);
|
|
91
|
-
await expect(slots.receive(
|
|
143
|
+
await expect(slots.receive(key, body("short"))).rejects.toBeInstanceOf(
|
|
144
|
+
SizeMismatchError,
|
|
145
|
+
);
|
|
92
146
|
expect(readdirSync(stagingDir)).toEqual([]);
|
|
93
147
|
});
|
|
94
148
|
|
|
95
149
|
it("rejects an over-declaration body without buffering it whole (declared+1 proof)", async () => {
|
|
96
|
-
const
|
|
97
|
-
|
|
150
|
+
const key = freshKey();
|
|
151
|
+
slots.reserve(key, 3);
|
|
152
|
+
await expect(slots.receive(key, body("longer than three"))).rejects.toThrow(
|
|
98
153
|
"upload size mismatch: received 4 bytes, declared 3",
|
|
99
154
|
);
|
|
100
155
|
});
|
|
101
156
|
|
|
102
|
-
it("
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
it("rejects consume on an expired ref", async () => {
|
|
108
|
-
const { ref } = slots.mint(1);
|
|
109
|
-
await slots.receive(ref, body("x"));
|
|
110
|
-
nowMs += TTL_MS + 1;
|
|
111
|
-
await expect(slots.consume(ref)).rejects.toThrow(SlotUnknownError);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("sweeps expired slots and their staged files on mint", async () => {
|
|
115
|
-
const { ref } = slots.mint(1);
|
|
116
|
-
await slots.receive(ref, body("x"));
|
|
157
|
+
it("sweeps expired slots and their staged files on reservation", async () => {
|
|
158
|
+
const key = freshKey();
|
|
159
|
+
slots.reserve(key, 1);
|
|
160
|
+
await slots.receive(key, body("x"));
|
|
117
161
|
expect(readdirSync(stagingDir)).toHaveLength(1);
|
|
118
162
|
nowMs += TTL_MS + 1;
|
|
119
|
-
slots.
|
|
163
|
+
slots.reserve(freshKey(), 1);
|
|
120
164
|
expect(readdirSync(stagingDir)).toEqual([]);
|
|
121
165
|
});
|
|
122
166
|
|
|
167
|
+
it("recreates the staging directory when the driver's delete pruned it between uploads", async () => {
|
|
168
|
+
const key = freshKey();
|
|
169
|
+
slots.reserve(key, 1);
|
|
170
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
171
|
+
await slots.receive(key, body("x"));
|
|
172
|
+
expect(readFileSync(path.join(root, key), "utf8")).toBe("x");
|
|
173
|
+
});
|
|
174
|
+
|
|
123
175
|
it("wipes the staging directory at construction (orphans are unreachable)", () => {
|
|
124
|
-
const
|
|
176
|
+
const orphanRoot = path.join(root, "orphan-root");
|
|
177
|
+
const orphanDir = path.join(orphanRoot, STAGING_KEY_PREFIX);
|
|
125
178
|
mkdirSync(orphanDir, { recursive: true });
|
|
126
|
-
const orphan = path.join(orphanDir, "
|
|
179
|
+
const orphan = path.join(orphanDir, "deadbeef.zip");
|
|
127
180
|
writeFileSync(orphan, "stale bytes");
|
|
128
|
-
new UploadSlots(
|
|
181
|
+
new UploadSlots(orphanRoot, STAGING_KEY_PREFIX, TTL_MS, MAX_SIZE);
|
|
129
182
|
expect(existsSync(orphan)).toBe(false);
|
|
130
183
|
expect(readdirSync(orphanDir)).toEqual([]);
|
|
131
184
|
});
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the archive staging port (transfer/staging.ts): the reference ⇄
|
|
3
|
+
* staging-key mapping and its refusals; mint handing the driver the
|
|
4
|
+
* domain's key and the slot TTL; consume reading then deleting, with the
|
|
5
|
+
* delete best-effort and loud; the one "unknown or expired" condition for
|
|
6
|
+
* a malformed, missing or already-consumed reference; a driver fault
|
|
7
|
+
* propagating untouched; the download capability's floor TTL. First over
|
|
8
|
+
* a recording fake driver (what any bucket sees), then over the real local
|
|
9
|
+
* driver with its slot registry, wired exactly as boot/compose.ts wires it
|
|
10
|
+
* — the two drivers must be indistinguishable above this module.
|
|
11
|
+
*/
|
|
12
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { Readable } from "node:stream";
|
|
16
|
+
|
|
17
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
ArtifactStorageNotFoundError,
|
|
21
|
+
LocalArtifactStorage,
|
|
22
|
+
} from "../../../artifactstorage/artifact-storage.js";
|
|
23
|
+
import type { ArtifactStorage } from "../../../artifactstorage/artifact-storage.js";
|
|
24
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
25
|
+
import { SKILL_ARTIFACTS_PATH_PREFIX } from "../../../transport/constants.js";
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_SLOT_TTL_MS,
|
|
28
|
+
DOWNLOAD_URL_TTL_MS,
|
|
29
|
+
MAX_ZIP_SIZE,
|
|
30
|
+
STAGING_KEY_PREFIX,
|
|
31
|
+
UPLOADS_SEGMENT,
|
|
32
|
+
} from "../constants.js";
|
|
33
|
+
import { transferServeUrl, uploadUrl } from "../transfer/handler.js";
|
|
34
|
+
import { SlotUnknownError, UploadSlots } from "../transfer/slots.js";
|
|
35
|
+
import {
|
|
36
|
+
newArchiveStaging,
|
|
37
|
+
newUploadRef,
|
|
38
|
+
stagingKeyOf,
|
|
39
|
+
uploadRefOf,
|
|
40
|
+
} from "../transfer/staging.js";
|
|
41
|
+
import type { ArchiveStaging } from "../transfer/staging.js";
|
|
42
|
+
|
|
43
|
+
const logLines: string[] = [];
|
|
44
|
+
const logger = createLogger({
|
|
45
|
+
level: "error",
|
|
46
|
+
pretty: false,
|
|
47
|
+
write: (line) => {
|
|
48
|
+
logLines.push(line);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("the reference ⇄ staging key mapping", () => {
|
|
53
|
+
it("mints sau_-prefixed 128-bit hex references whose key sits under the staging prefix", () => {
|
|
54
|
+
const ref = newUploadRef();
|
|
55
|
+
expect(ref).toMatch(/^sau_[0-9a-f]{32}$/);
|
|
56
|
+
const key = stagingKeyOf(ref);
|
|
57
|
+
expect(key).toBe(`${STAGING_KEY_PREFIX}${ref.slice(4)}.zip`);
|
|
58
|
+
expect(uploadRefOf(key!)).toBe(ref);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("answers no key for a reference this server could not have minted", () => {
|
|
62
|
+
for (const bad of [
|
|
63
|
+
"",
|
|
64
|
+
"sau_",
|
|
65
|
+
"sau_nope",
|
|
66
|
+
"sau_" + "g".repeat(32),
|
|
67
|
+
"sau_" + "0".repeat(31),
|
|
68
|
+
"skl_" + "0".repeat(32),
|
|
69
|
+
"sau_" + "A".repeat(32),
|
|
70
|
+
]) {
|
|
71
|
+
expect(stagingKeyOf(bad), bad).toBeUndefined();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("refuses to derive a reference from a foreign key", () => {
|
|
76
|
+
expect(() => uploadRefOf("skills/deadbeef.zip")).toThrow(
|
|
77
|
+
"is not a staging key this domain minted",
|
|
78
|
+
);
|
|
79
|
+
expect(() => uploadRefOf(`${STAGING_KEY_PREFIX}notes.txt`)).toThrow(
|
|
80
|
+
"is not a staging key this domain minted",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ─── Over a recording fake: what a bucket driver is asked to do ──────────
|
|
86
|
+
|
|
87
|
+
interface Recorded {
|
|
88
|
+
presignPut: Array<{ key: string; declaredSizeBytes: number; ttlMs: number }>;
|
|
89
|
+
downloads: string[];
|
|
90
|
+
deletes: string[];
|
|
91
|
+
signed: Array<{ key: string; expiresInMs: number; downloadFilename: string }>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function fakeDriver(
|
|
95
|
+
options: { failDelete?: boolean; failDownload?: Error } = {},
|
|
96
|
+
): {
|
|
97
|
+
driver: ArtifactStorage;
|
|
98
|
+
blobs: Map<string, Uint8Array>;
|
|
99
|
+
recorded: Recorded;
|
|
100
|
+
} {
|
|
101
|
+
const blobs = new Map<string, Uint8Array>();
|
|
102
|
+
const recorded: Recorded = {
|
|
103
|
+
presignPut: [],
|
|
104
|
+
downloads: [],
|
|
105
|
+
deletes: [],
|
|
106
|
+
signed: [],
|
|
107
|
+
};
|
|
108
|
+
const driver: ArtifactStorage = {
|
|
109
|
+
upload: (key, data) => {
|
|
110
|
+
blobs.set(key, data);
|
|
111
|
+
return Promise.resolve();
|
|
112
|
+
},
|
|
113
|
+
download: (key) => {
|
|
114
|
+
recorded.downloads.push(key);
|
|
115
|
+
if (options.failDownload) return Promise.reject(options.failDownload);
|
|
116
|
+
const data = blobs.get(key);
|
|
117
|
+
return data === undefined
|
|
118
|
+
? Promise.reject(new ArtifactStorageNotFoundError(key))
|
|
119
|
+
: Promise.resolve(data);
|
|
120
|
+
},
|
|
121
|
+
size: (key) => Promise.resolve(blobs.get(key)?.length ?? 0),
|
|
122
|
+
presignPut: (key, declaredSizeBytes, ttlMs) => {
|
|
123
|
+
recorded.presignPut.push({ key, declaredSizeBytes, ttlMs });
|
|
124
|
+
return Promise.resolve({
|
|
125
|
+
url: `https://bucket.test/${key}?sig=put`,
|
|
126
|
+
ttlMs: ttlMs / 2,
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
getSignedUrl: (key, expiresInMs, downloadFilename) => {
|
|
130
|
+
recorded.signed.push({ key, expiresInMs, downloadFilename });
|
|
131
|
+
return Promise.resolve(`https://bucket.test/${key}?sig=get`);
|
|
132
|
+
},
|
|
133
|
+
delete: (key) => {
|
|
134
|
+
recorded.deletes.push(key);
|
|
135
|
+
if (options.failDelete)
|
|
136
|
+
return Promise.reject(new Error("bucket refused the delete"));
|
|
137
|
+
blobs.delete(key);
|
|
138
|
+
return Promise.resolve();
|
|
139
|
+
},
|
|
140
|
+
exists: (key) => Promise.resolve(blobs.has(key)),
|
|
141
|
+
health: () => Promise.resolve(),
|
|
142
|
+
};
|
|
143
|
+
return { driver, blobs, recorded };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
describe("newArchiveStaging over a bucket-shaped driver", () => {
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
logLines.length = 0;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("mint hands the driver the domain's key and the slot TTL, and returns the driver's URL and granted TTL", async () => {
|
|
152
|
+
const { driver, recorded } = fakeDriver();
|
|
153
|
+
const staging = newArchiveStaging(driver, logger);
|
|
154
|
+
const minted = await staging.mint(1234);
|
|
155
|
+
|
|
156
|
+
expect(recorded.presignPut).toEqual([
|
|
157
|
+
{
|
|
158
|
+
key: stagingKeyOf(minted.ref),
|
|
159
|
+
declaredSizeBytes: 1234,
|
|
160
|
+
ttlMs: DEFAULT_SLOT_TTL_MS,
|
|
161
|
+
},
|
|
162
|
+
]);
|
|
163
|
+
expect(minted.url).toBe(
|
|
164
|
+
`https://bucket.test/${stagingKeyOf(minted.ref)}?sig=put`,
|
|
165
|
+
);
|
|
166
|
+
expect(minted.ttlMs).toBe(DEFAULT_SLOT_TTL_MS / 2);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("mint refuses a declaration outside the archive ceiling before any driver call", async () => {
|
|
170
|
+
const { driver, recorded } = fakeDriver();
|
|
171
|
+
const staging = newArchiveStaging(driver, logger);
|
|
172
|
+
await expect(staging.mint(0)).rejects.toThrow(
|
|
173
|
+
`declared size 0 outside (0, ${MAX_ZIP_SIZE}]`,
|
|
174
|
+
);
|
|
175
|
+
await expect(staging.mint(MAX_ZIP_SIZE + 1)).rejects.toThrow("outside");
|
|
176
|
+
expect(recorded.presignPut).toEqual([]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("consume reads the staged key, deletes it, and a replay is unknown", async () => {
|
|
180
|
+
const { driver, blobs, recorded } = fakeDriver();
|
|
181
|
+
const staging = newArchiveStaging(driver, logger);
|
|
182
|
+
const minted = await staging.mint(5);
|
|
183
|
+
const key = stagingKeyOf(minted.ref)!;
|
|
184
|
+
blobs.set(key, Buffer.from("hello"));
|
|
185
|
+
|
|
186
|
+
expect(Buffer.from(await staging.consume(minted.ref)).toString()).toBe(
|
|
187
|
+
"hello",
|
|
188
|
+
);
|
|
189
|
+
expect(recorded.downloads).toEqual([key]);
|
|
190
|
+
expect(recorded.deletes).toEqual([key]);
|
|
191
|
+
await expect(staging.consume(minted.ref)).rejects.toThrow(SlotUnknownError);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("a malformed or never-uploaded reference is one condition: unknown or expired", async () => {
|
|
195
|
+
const { driver, recorded } = fakeDriver();
|
|
196
|
+
const staging = newArchiveStaging(driver, logger);
|
|
197
|
+
await expect(staging.consume("not-a-ref")).rejects.toThrow(
|
|
198
|
+
"upload reference unknown or expired",
|
|
199
|
+
);
|
|
200
|
+
// Nothing shaped like a key reaches the driver for a malformed ref.
|
|
201
|
+
expect(recorded.downloads).toEqual([]);
|
|
202
|
+
|
|
203
|
+
const minted = await staging.mint(5);
|
|
204
|
+
await expect(staging.consume(minted.ref)).rejects.toThrow(SlotUnknownError);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("a failed delete is loud and does not fail the consume", async () => {
|
|
208
|
+
const { driver, blobs } = fakeDriver({ failDelete: true });
|
|
209
|
+
const staging = newArchiveStaging(driver, logger);
|
|
210
|
+
const minted = await staging.mint(5);
|
|
211
|
+
blobs.set(stagingKeyOf(minted.ref)!, Buffer.from("hello"));
|
|
212
|
+
|
|
213
|
+
expect(Buffer.from(await staging.consume(minted.ref)).toString()).toBe(
|
|
214
|
+
"hello",
|
|
215
|
+
);
|
|
216
|
+
expect(
|
|
217
|
+
logLines.some((line) =>
|
|
218
|
+
line.includes("failed to delete consumed staged artifact"),
|
|
219
|
+
),
|
|
220
|
+
).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("a driver fault on download propagates as itself, never as an unusable reference", async () => {
|
|
224
|
+
const fault = new Error("bucket unreachable");
|
|
225
|
+
const { driver } = fakeDriver({ failDownload: fault });
|
|
226
|
+
const staging = newArchiveStaging(driver, logger);
|
|
227
|
+
const minted = await staging.mint(5);
|
|
228
|
+
await expect(staging.consume(minted.ref)).rejects.toBe(fault);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("downloadUrl signs the stored key for the floor TTL and reports it", async () => {
|
|
232
|
+
const { driver, recorded } = fakeDriver();
|
|
233
|
+
const staging = newArchiveStaging(driver, logger);
|
|
234
|
+
const capability = await staging.downloadUrl("skills/abc.zip");
|
|
235
|
+
expect(recorded.signed).toEqual([
|
|
236
|
+
{
|
|
237
|
+
key: "skills/abc.zip",
|
|
238
|
+
expiresInMs: DOWNLOAD_URL_TTL_MS,
|
|
239
|
+
downloadFilename: "",
|
|
240
|
+
},
|
|
241
|
+
]);
|
|
242
|
+
expect(capability).toEqual({
|
|
243
|
+
url: "https://bucket.test/skills/abc.zip?sig=get",
|
|
244
|
+
ttlMs: DOWNLOAD_URL_TTL_MS,
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// ─── Over the real local driver: the slot registry behind the same port ──
|
|
250
|
+
|
|
251
|
+
describe("newArchiveStaging over LocalArtifactStorage and its slot registry", () => {
|
|
252
|
+
const BASE_URL = "http://localhost:8080";
|
|
253
|
+
let root: string;
|
|
254
|
+
let slots: UploadSlots;
|
|
255
|
+
let staging: ArchiveStaging;
|
|
256
|
+
|
|
257
|
+
beforeEach(() => {
|
|
258
|
+
root = mkdtempSync(path.join(tmpdir(), "archive-staging-local-"));
|
|
259
|
+
slots = new UploadSlots(root, STAGING_KEY_PREFIX, 60_000, 1024 * 1024);
|
|
260
|
+
const driver = new LocalArtifactStorage(root, transferServeUrl(BASE_URL), {
|
|
261
|
+
reserve: (key, declaredSizeBytes) => {
|
|
262
|
+
const { ttlMs } = slots.reserve(key, declaredSizeBytes);
|
|
263
|
+
return { url: uploadUrl(BASE_URL, uploadRefOf(key)), ttlMs };
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
staging = newArchiveStaging(driver, logger);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
afterEach(() => {
|
|
270
|
+
rmSync(root, { recursive: true, force: true });
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("mint → lane receive → consume round-trips the bytes and retires the reference", async () => {
|
|
274
|
+
const bytes = Buffer.from("staged artifact bytes");
|
|
275
|
+
const minted = await staging.mint(bytes.length);
|
|
276
|
+
|
|
277
|
+
// The URL is the lane's wire shape and the ref is its last segment —
|
|
278
|
+
// exactly how the lane handler dispatches a PUT.
|
|
279
|
+
expect(minted.url).toBe(
|
|
280
|
+
`${BASE_URL}${SKILL_ARTIFACTS_PATH_PREFIX}${UPLOADS_SEGMENT}${minted.ref}`,
|
|
281
|
+
);
|
|
282
|
+
// The registry's slot TTL governs, not the domain's ask.
|
|
283
|
+
expect(minted.ttlMs).toBe(60_000);
|
|
284
|
+
|
|
285
|
+
await slots.receive(stagingKeyOf(minted.ref)!, Readable.from(bytes));
|
|
286
|
+
expect(Buffer.from(await staging.consume(minted.ref))).toEqual(bytes);
|
|
287
|
+
await expect(staging.consume(minted.ref)).rejects.toThrow(SlotUnknownError);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("a reserved-but-never-uploaded reference is unknown, exactly as on a bucket", async () => {
|
|
291
|
+
const minted = await staging.mint(4);
|
|
292
|
+
await expect(staging.consume(minted.ref)).rejects.toThrow(
|
|
293
|
+
"upload reference unknown or expired",
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("downloadUrl renders the lane's own GET route with the floor TTL", async () => {
|
|
298
|
+
const capability = await staging.downloadUrl("skills/abc.zip");
|
|
299
|
+
expect(capability).toEqual({
|
|
300
|
+
url: `${BASE_URL}${SKILL_ARTIFACTS_PATH_PREFIX}/skills/abc.zip`,
|
|
301
|
+
ttlMs: DOWNLOAD_URL_TTL_MS,
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
@@ -27,10 +27,34 @@ export const MAX_SKILL_MD_SIZE = 1 * 1024 * 1024;
|
|
|
27
27
|
/**
|
|
28
28
|
* Upload-slot lifetime — generous enough for a 100MB upload on a slow
|
|
29
29
|
* link, short enough that abandoned slots don't accumulate. Go:
|
|
30
|
-
* transfer.DefaultSlotTTL (15 minutes).
|
|
30
|
+
* transfer.DefaultSlotTTL (15 minutes). The same lifetime is asked of a
|
|
31
|
+
* bucket driver's presigned PUT, so the reference and the URL it names
|
|
32
|
+
* expire together whichever driver serves them.
|
|
31
33
|
*/
|
|
32
34
|
export const DEFAULT_SLOT_TTL_MS = 15 * 60 * 1000;
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Download capability lifetime — the floor of a minted download URL's
|
|
38
|
+
* validity, reported as ttl_seconds. A bucket driver signs its GET for
|
|
39
|
+
* exactly this long, the lifetime the cloud edition already grants
|
|
40
|
+
* execution-artifact downloads; the local lane's URL is a content-hash
|
|
41
|
+
* capability that never expires, so the floor holds there trivially. An
|
|
42
|
+
* hour outlives any mount the runner performs while staying short enough
|
|
43
|
+
* that a leaked URL is not a standing grant.
|
|
44
|
+
*/
|
|
45
|
+
export const DOWNLOAD_URL_TTL_MS = 60 * 60 * 1000;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Where staged uploads live in the blob store, under the skill store's own
|
|
49
|
+
* prefix: `skills/staging/<hex>.zip`. The retired cloud service staged
|
|
50
|
+
* under this exact prefix, so the bucket lifecycle rule that sweeps
|
|
51
|
+
* abandoned uploads is written for it, and on the local driver the boot
|
|
52
|
+
* wipe targets the same directory. Every staged object is a random
|
|
53
|
+
* 128-bit key, so its exposure through the download lane is the same
|
|
54
|
+
* capability the upload reference already grants.
|
|
55
|
+
*/
|
|
56
|
+
export const STAGING_KEY_PREFIX = "skills/staging/";
|
|
57
|
+
|
|
34
58
|
/**
|
|
35
59
|
* Random capability-token size: 16 bytes = 128 bits of entropy, matching
|
|
36
60
|
* the unguessability of the content-hash download keys. Go: refByteLen.
|
|
@@ -53,8 +77,9 @@ export const UPLOADS_SEGMENT = "/uploads/";
|
|
|
53
77
|
|
|
54
78
|
/**
|
|
55
79
|
* Downloads are restricted to the skill artifact store's own keys
|
|
56
|
-
* ("skills/<hash>.zip"
|
|
57
|
-
*
|
|
80
|
+
* ("skills/<hash>.zip", and the staged uploads under STAGING_KEY_PREFIX);
|
|
81
|
+
* everything else under the storage root — should the two ever share one
|
|
82
|
+
* — stays unreachable from this lane.
|
|
58
83
|
*/
|
|
59
84
|
export const DOWNLOAD_KEY_PREFIX = "skills/";
|
|
60
85
|
|