create-windy 0.6.14 → 0.6.15
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/cli.js +215 -24
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +58 -8
- package/template/.windy-template.json +2 -2
- package/template/apps/server/src/agent/client-session/attempt-authority.test.ts +162 -0
- package/template/apps/server/src/agent/client-session/attempt-authority.ts +191 -0
- package/template/apps/server/src/agent/client-session/bootstrap.ts +27 -11
- package/template/apps/server/src/agent/client-session/completion.ts +75 -24
- package/template/apps/server/src/agent/client-session/contracts.ts +23 -0
- package/template/apps/server/src/agent/client-session/creation.ts +103 -0
- package/template/apps/server/src/agent/client-session/digest.ts +11 -1
- package/template/apps/server/src/agent/client-session/drizzle-records.ts +150 -0
- package/template/apps/server/src/agent/client-session/drizzle-repository.integration.test.ts +30 -3
- package/template/apps/server/src/agent/client-session/drizzle-repository.ts +23 -132
- package/template/apps/server/src/agent/client-session/finalization.ts +99 -0
- package/template/apps/server/src/agent/client-session/host-auth.ts +22 -10
- package/template/apps/server/src/agent/client-session/host-revocation-composition.test.ts +77 -0
- package/template/apps/server/src/agent/client-session/host-revocation-composition.ts +28 -0
- package/template/apps/server/src/agent/client-session/launch-authority.contract.test.ts +21 -0
- package/template/apps/server/src/agent/client-session/manifest-admission.ts +11 -0
- package/template/apps/server/src/agent/client-session/service.test-support.ts +22 -0
- package/template/apps/server/src/agent/client-session/service.ts +104 -134
- package/template/apps/server/src/agent/client-session/terminal-digest.test.ts +23 -0
- package/template/apps/server/src/agent/client-workflow-artifact/active-blob-references.ts +8 -0
- package/template/apps/server/src/agent/client-workflow-artifact/artifact-builder.ts +60 -0
- package/template/apps/server/src/agent/client-workflow-artifact/authority-current.ts +59 -0
- package/template/apps/server/src/agent/client-workflow-artifact/binding-registry.test.ts +189 -0
- package/template/apps/server/src/agent/client-workflow-artifact/binding-registry.ts +153 -0
- package/template/apps/server/src/agent/client-workflow-artifact/bootstrap.ts +77 -0
- package/template/apps/server/src/agent/client-workflow-artifact/cancellation.ts +47 -0
- package/template/apps/server/src/agent/client-workflow-artifact/completed-recovery.ts +31 -0
- package/template/apps/server/src/agent/client-workflow-artifact/contracts.ts +104 -0
- package/template/apps/server/src/agent/client-workflow-artifact/drizzle-locks.ts +30 -0
- package/template/apps/server/src/agent/client-workflow-artifact/drizzle-mapper.ts +138 -0
- package/template/apps/server/src/agent/client-workflow-artifact/drizzle-repository.ts +346 -0
- package/template/apps/server/src/agent/client-workflow-artifact/error.ts +21 -0
- package/template/apps/server/src/agent/client-workflow-artifact/handler.ts +167 -0
- package/template/apps/server/src/agent/client-workflow-artifact/host-revocation.ts +29 -0
- package/template/apps/server/src/agent/client-workflow-artifact/in-memory-repository.ts +232 -0
- package/template/apps/server/src/agent/client-workflow-artifact/job-port.ts +101 -0
- package/template/apps/server/src/agent/client-workflow-artifact/presentation.ts +41 -0
- package/template/apps/server/src/agent/client-workflow-artifact/recovery.contract.test.ts +253 -0
- package/template/apps/server/src/agent/client-workflow-artifact/release-binding.ts +85 -0
- package/template/apps/server/src/agent/client-workflow-artifact/repository.test.ts +204 -0
- package/template/apps/server/src/agent/client-workflow-artifact/repository.ts +97 -0
- package/template/apps/server/src/agent/client-workflow-artifact/routes-handler.contract.test.ts +201 -0
- package/template/apps/server/src/agent/client-workflow-artifact/routes.ts +161 -0
- package/template/apps/server/src/agent/client-workflow-artifact/runtime-composition.ts +101 -0
- package/template/apps/server/src/agent/client-workflow-artifact/scope-authority.ts +39 -0
- package/template/apps/server/src/agent/client-workflow-artifact/service-authorization.ts +117 -0
- package/template/apps/server/src/agent/client-workflow-artifact/service-support.ts +28 -0
- package/template/apps/server/src/agent/client-workflow-artifact/service.contract.test.ts +345 -0
- package/template/apps/server/src/agent/client-workflow-artifact/service.ts +350 -0
- package/template/apps/server/src/agent/client-workflow-artifact/session-boundary.ts +39 -0
- package/template/apps/server/src/agent/client-workflow-artifact/source-access.contract.test.ts +98 -0
- package/template/apps/server/src/agent/client-workflow-artifact/source-access.ts +80 -0
- package/template/apps/server/src/agent/client-workflow-artifact/takeover.ts +122 -0
- package/template/apps/server/src/agent/client-workflow-artifact/task-authority.contract.test.ts +97 -0
- package/template/apps/server/src/agent/client-workflow-artifact/task-authority.ts +147 -0
- package/template/apps/server/src/agent/client-workflow-artifact/terminal-authority.contract.test.ts +100 -0
- package/template/apps/server/src/agent/client-workflow-artifact/terminal-replay.ts +31 -0
- package/template/apps/server/src/agent/client-workflow-artifact/terminal-seal.ts +211 -0
- package/template/apps/server/src/agent/client-workflow-artifact/workflow-artifact.test-support.ts +173 -0
- package/template/apps/server/src/agent/client-workflow-artifact/workflow-loader.contract.test.ts +254 -0
- package/template/apps/server/src/agent/client-workflow-artifact/workflow-loader.ts +285 -0
- package/template/apps/server/src/agent/run-facade.test-support.ts +17 -4
- package/template/apps/server/src/agent/run-facade.test.ts +16 -0
- package/template/apps/server/src/agent/run-session-authorization.ts +12 -10
- package/template/apps/server/src/agent/runtime-bootstrap-input.ts +5 -1
- package/template/apps/server/src/agent/runtime-bootstrap.ts +33 -33
- package/template/apps/server/src/agent/runtime-public-ports.ts +40 -0
- package/template/apps/server/src/agent/session-invalidation/contracts.ts +25 -26
- package/template/apps/server/src/agent/session-projection-access.ts +31 -19
- package/template/apps/server/src/agent/unavailable-routes.ts +21 -0
- package/template/apps/server/src/agent/workflow-runtime-bootstrap.ts +2 -0
- package/template/apps/server/src/agent-workflow/artifact-source-authorization.ts +12 -6
- package/template/apps/server/src/agent-workflow/command-facade.ts +70 -11
- package/template/apps/server/src/agent-workflow/drizzle-artifact-port.ts +9 -9
- package/template/apps/server/src/agent-workflow/drizzle-delegation-completion.ts +1 -1
- package/template/apps/server/src/agent-workflow/platform-node-adapters.ts +14 -6
- package/template/apps/server/src/agent-workflow/project-launch-authority.contract.test.ts +53 -0
- package/template/apps/server/src/agent-workflow/project-launch-authority.ts +92 -0
- package/template/apps/server/src/agent-workflow/runtime-composition.ts +30 -14
- package/template/apps/server/src/app/agent-runtime-composition.ts +6 -1
- package/template/apps/server/src/app/client-agent-workflow-artifact-binding.ts +14 -0
- package/template/apps/server/src/application-services.ts +6 -0
- package/template/apps/server/src/data-access/scope-containment.test.ts +31 -0
- package/template/apps/server/src/data-access/scope-containment.ts +48 -0
- package/template/apps/server/src/jobs/artifact-publication.test.ts +192 -0
- package/template/apps/server/src/jobs/artifact-publication.ts +56 -0
- package/template/apps/server/src/jobs/artifact-publisher.ts +101 -0
- package/template/apps/server/src/jobs/drizzle-artifact.ts +73 -1
- package/template/apps/server/src/jobs/drizzle-execution-repository.ts +14 -1
- package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +65 -0
- package/template/apps/server/src/jobs/drizzle-repository.integration.test.ts +1 -0
- package/template/apps/server/src/jobs/execution-repository.ts +28 -0
- package/template/apps/server/src/jobs/execution.test.ts +3 -1
- package/template/apps/server/src/jobs/in-memory-execution.ts +16 -0
- package/template/apps/server/src/jobs/in-memory-lease.ts +45 -1
- package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +2 -0
- package/template/apps/server/src/jobs/postgres-cleanup-contract.test.ts +58 -0
- package/template/apps/server/src/jobs/worker.ts +8 -14
- package/template/apps/server/src/module-host/background-ai-unavailable.ts +18 -0
- package/template/apps/server/src/module-host/durable-context.ts +19 -22
- package/template/apps/server/src/module-host/implementation-completeness.test.ts +80 -1
- package/template/apps/server/src/module-host/implementation-completeness.ts +28 -1
- package/template/apps/server/src/module-host/request-context.ts +32 -8
- package/template/apps/server/src/module-host/storage-port.ts +3 -0
- package/template/apps/server/src/module-host/workflow-input-artifact-port.test.ts +104 -0
- package/template/apps/server/src/module-host/workflow-input-artifact-port.ts +229 -0
- package/template/apps/server/src/module-host/workflow-input-materialization.test.ts +163 -0
- package/template/apps/server/src/module-host/workflow-input-materialization.ts +161 -0
- package/template/apps/server/src/workflow/drizzle-repository.integration.test.ts +1 -0
- package/template/docs/architecture/agent-workflow.md +13 -2
- package/template/docs/architecture/durable-jobs.md +7 -0
- package/template/docs/operations/agent-session-memory.md +2 -1
- package/template/docs/operations/agent-session-troubleshooting.md +87 -2
- package/template/docs/operations/agent-workflow-recovery.md +25 -14
- package/template/docs/platform/client-agent-session.md +148 -2
- package/template/packages/ai-client/package.json +2 -1
- package/template/packages/ai-client/src/client-agent/index.ts +1 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/contracts.ts +85 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/http.test.ts +49 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/http.ts +82 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/index.ts +3 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/validation.test.ts +154 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/validation.ts +332 -0
- package/template/packages/database/drizzle/0062_client_agent_workflow_artifact.sql +63 -0
- package/template/packages/database/drizzle/meta/0062_snapshot.json +14839 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/agent-runtime-schema.test.ts +177 -0
- package/template/packages/database/src/schema/client-agent-workflow-artifact.ts +158 -0
- package/template/packages/database/src/schema/index.ts +1 -0
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs/src/handlers.test.ts +10 -0
- package/template/packages/jobs/src/handlers.ts +28 -11
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/ai-client-execution-policy.ts +6 -0
- package/template/packages/modules/src/manifest.ts +2 -0
- package/template/packages/modules/src/system-agent-audit-actions.ts +3 -0
- package/template/packages/modules/src/system-ai-api-bindings.ts +15 -0
- package/template/packages/modules/src/system-audit-actions.ts +15 -0
- package/template/packages/server-sdk/index.ts +52 -0
- package/template/packages/server-sdk/package.json +5 -1
- package/template/packages/server-sdk/src/client-agent-session.ts +13 -1
- package/template/packages/server-sdk/src/client-agent-workflow-artifact.contract.test.ts +113 -0
- package/template/packages/server-sdk/src/client-agent-workflow-artifact.ts +183 -0
- package/template/packages/server-sdk/src/durable-handler.ts +7 -0
- package/template/packages/server-sdk/src/request-context.ts +7 -0
- package/template/packages/server-sdk/src/workflow-input-artifact-port.ts +104 -0
- package/template/packages/server-sdk/src/workflow-input-artifact-validation.contract.test.ts +124 -0
- package/template/packages/server-sdk/src/workflow-input-artifact-validation.ts +347 -0
- package/template/packages/server-sdk/src/workflow-input-materialization-handler.ts +43 -0
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage/src/blob-store-put-once.test.ts +70 -0
- package/template/packages/storage/src/blob-store.ts +9 -0
- package/template/packages/storage/src/local-file-blob-put-once.ts +120 -0
- package/template/packages/storage/src/local-file-blob-store.ts +53 -1
- package/template/packages/storage/src/memory-blob-store.ts +73 -33
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import type { JsonValue } from "@southwind-ai/shared";
|
|
2
|
+
import type {
|
|
3
|
+
ModuleWorkflowInputJsonArtifactV1,
|
|
4
|
+
ModuleWorkflowInputMaterializationSourceV1,
|
|
5
|
+
ModuleWorkflowInputResourceArtifactV1,
|
|
6
|
+
} from "./workflow-input-artifact-port.js";
|
|
7
|
+
|
|
8
|
+
export const MODULE_WORKFLOW_INPUT_MATERIALIZATION_LIMITS_V1 = {
|
|
9
|
+
sources: 32,
|
|
10
|
+
totalBytes: 16 * 1024 * 1024,
|
|
11
|
+
jsonDepth: 32,
|
|
12
|
+
jsonNodes: 50_000,
|
|
13
|
+
arrayItems: 4_096,
|
|
14
|
+
objectProperties: 2_048,
|
|
15
|
+
keyBytes: 256,
|
|
16
|
+
stringBytes: 4 * 1024 * 1024,
|
|
17
|
+
totalStringBytes: 12 * 1024 * 1024,
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
const unsafeKeys = new Set(["__proto__", "constructor", "prototype"]);
|
|
21
|
+
const encoder = new TextEncoder();
|
|
22
|
+
|
|
23
|
+
interface Budget {
|
|
24
|
+
nodes: number;
|
|
25
|
+
stringBytes: number;
|
|
26
|
+
readonly seen: WeakSet<object>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseModuleWorkflowInputMaterializationBeginV1(value: unknown) {
|
|
30
|
+
const input = strictRecord(value, ["idempotencyKey", "sources"]);
|
|
31
|
+
const budget = freshBudget();
|
|
32
|
+
const result = {
|
|
33
|
+
idempotencyKey: text(input.idempotencyKey, 200, budget),
|
|
34
|
+
sources: sources(input.sources, budget),
|
|
35
|
+
};
|
|
36
|
+
assertTotalBytes(result);
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseModuleWorkflowInputMaterializationPayloadV1(
|
|
41
|
+
value: unknown,
|
|
42
|
+
) {
|
|
43
|
+
const input = strictRecord(value, ["schemaVersion", "sources"]);
|
|
44
|
+
if (input.schemaVersion !== 1) fail();
|
|
45
|
+
const budget = freshBudget();
|
|
46
|
+
const result = {
|
|
47
|
+
schemaVersion: 1 as const,
|
|
48
|
+
sources: sources(input.sources, budget),
|
|
49
|
+
};
|
|
50
|
+
assertTotalBytes(result);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseModuleWorkflowInputMaterializationSourceV1(
|
|
55
|
+
value: unknown,
|
|
56
|
+
): ModuleWorkflowInputMaterializationSourceV1 {
|
|
57
|
+
const budget = freshBudget();
|
|
58
|
+
const result = source(value, budget);
|
|
59
|
+
assertTotalBytes(result);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function parseModuleWorkflowInputResourceArtifactV1(
|
|
64
|
+
value: unknown,
|
|
65
|
+
): ModuleWorkflowInputResourceArtifactV1 {
|
|
66
|
+
const budget = freshBudget();
|
|
67
|
+
const result = resourceArtifact(value, budget, false);
|
|
68
|
+
assertTotalBytes(result);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parseModuleWorkflowInputJsonArtifactV1(
|
|
73
|
+
value: unknown,
|
|
74
|
+
): ModuleWorkflowInputJsonArtifactV1 {
|
|
75
|
+
const budget = freshBudget();
|
|
76
|
+
const result = jsonArtifact(value, budget, false);
|
|
77
|
+
assertTotalBytes(result);
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function parseModuleWorkflowInputJsonValueV1(value: unknown): JsonValue {
|
|
82
|
+
const budget = freshBudget();
|
|
83
|
+
const result = json(value, 0, budget);
|
|
84
|
+
assertTotalBytes(result);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sources(value: unknown, budget: Budget) {
|
|
89
|
+
if (
|
|
90
|
+
!Array.isArray(value) ||
|
|
91
|
+
value.length < 1 ||
|
|
92
|
+
value.length > MODULE_WORKFLOW_INPUT_MATERIALIZATION_LIMITS_V1.sources
|
|
93
|
+
) {
|
|
94
|
+
fail();
|
|
95
|
+
}
|
|
96
|
+
return strictArray(value, value.length).map((item) => source(item, budget));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function source(
|
|
100
|
+
value: unknown,
|
|
101
|
+
budget: Budget,
|
|
102
|
+
): ModuleWorkflowInputMaterializationSourceV1 {
|
|
103
|
+
const mode = dataProperty(value, "mode");
|
|
104
|
+
if (mode === "resource") {
|
|
105
|
+
return { mode, ...resourceArtifact(value, budget, true) };
|
|
106
|
+
}
|
|
107
|
+
if (mode === "json") return { mode, ...jsonArtifact(value, budget, true) };
|
|
108
|
+
return fail();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resourceArtifact(value: unknown, budget: Budget, withMode: boolean) {
|
|
112
|
+
const input = artifactRecord(value, withMode, "resource");
|
|
113
|
+
const resource = strictRecord(input.resource, [
|
|
114
|
+
"fileId",
|
|
115
|
+
"purpose",
|
|
116
|
+
"resourceType",
|
|
117
|
+
"resourceId",
|
|
118
|
+
]);
|
|
119
|
+
return {
|
|
120
|
+
...publication(input, budget),
|
|
121
|
+
resource: {
|
|
122
|
+
fileId: text(resource.fileId, 256, budget),
|
|
123
|
+
purpose: text(resource.purpose, 160, budget),
|
|
124
|
+
resourceType: text(resource.resourceType, 160, budget),
|
|
125
|
+
resourceId: text(resource.resourceId, 256, budget),
|
|
126
|
+
},
|
|
127
|
+
} satisfies ModuleWorkflowInputResourceArtifactV1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function jsonArtifact(value: unknown, budget: Budget, withMode: boolean) {
|
|
131
|
+
const input = artifactRecord(value, withMode, "json");
|
|
132
|
+
return {
|
|
133
|
+
...publication(input, budget),
|
|
134
|
+
value: json(input.value, 0, budget),
|
|
135
|
+
} satisfies ModuleWorkflowInputJsonArtifactV1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function artifactRecord(value: unknown, withMode: boolean, mode: string) {
|
|
139
|
+
const common = [
|
|
140
|
+
"publicationKey",
|
|
141
|
+
"kind",
|
|
142
|
+
"filename",
|
|
143
|
+
"retentionPolicyKey",
|
|
144
|
+
"source",
|
|
145
|
+
];
|
|
146
|
+
const specific = mode === "resource" ? "resource" : "value";
|
|
147
|
+
return strictRecord(
|
|
148
|
+
value,
|
|
149
|
+
withMode ? ["mode", ...common, specific] : [...common, specific],
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function publication(input: Record<string, unknown>, budget: Budget) {
|
|
154
|
+
const source = strictRecord(input.source, [
|
|
155
|
+
"kind",
|
|
156
|
+
"id",
|
|
157
|
+
"version",
|
|
158
|
+
"sequence",
|
|
159
|
+
"digest?",
|
|
160
|
+
]);
|
|
161
|
+
if (!Number.isSafeInteger(source.sequence) || Number(source.sequence) < 1)
|
|
162
|
+
fail();
|
|
163
|
+
const filename = text(input.filename, 240, budget);
|
|
164
|
+
if (filename.includes("/") || filename.includes("\\")) fail();
|
|
165
|
+
return {
|
|
166
|
+
publicationKey: text(input.publicationKey, 200, budget),
|
|
167
|
+
kind: text(input.kind, 160, budget),
|
|
168
|
+
filename,
|
|
169
|
+
retentionPolicyKey: text(input.retentionPolicyKey, 256, budget),
|
|
170
|
+
source: {
|
|
171
|
+
kind: text(source.kind, 160, budget),
|
|
172
|
+
id: text(source.id, 256, budget),
|
|
173
|
+
version: text(source.version, 256, budget),
|
|
174
|
+
sequence: Number(source.sequence),
|
|
175
|
+
...(source.digest === undefined
|
|
176
|
+
? {}
|
|
177
|
+
: { digest: digest(source.digest, budget) }),
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function json(value: unknown, depth: number, budget: Budget): JsonValue {
|
|
183
|
+
budget.nodes += 1;
|
|
184
|
+
if (
|
|
185
|
+
budget.nodes > MODULE_WORKFLOW_INPUT_MATERIALIZATION_LIMITS_V1.jsonNodes ||
|
|
186
|
+
depth > MODULE_WORKFLOW_INPUT_MATERIALIZATION_LIMITS_V1.jsonDepth
|
|
187
|
+
) {
|
|
188
|
+
fail();
|
|
189
|
+
}
|
|
190
|
+
if (value === null || typeof value === "boolean") return value;
|
|
191
|
+
if (typeof value === "string") return jsonString(value, budget);
|
|
192
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : fail();
|
|
193
|
+
if (!value || typeof value !== "object" || budget.seen.has(value)) fail();
|
|
194
|
+
budget.seen.add(value);
|
|
195
|
+
if (Array.isArray(value)) {
|
|
196
|
+
if (
|
|
197
|
+
value.length > MODULE_WORKFLOW_INPUT_MATERIALIZATION_LIMITS_V1.arrayItems
|
|
198
|
+
) {
|
|
199
|
+
fail();
|
|
200
|
+
}
|
|
201
|
+
return strictArray(value, value.length).map((item) =>
|
|
202
|
+
json(item, depth + 1, budget),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) fail();
|
|
206
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
207
|
+
const keys = Reflect.ownKeys(descriptors);
|
|
208
|
+
if (
|
|
209
|
+
keys.length >
|
|
210
|
+
MODULE_WORKFLOW_INPUT_MATERIALIZATION_LIMITS_V1.objectProperties ||
|
|
211
|
+
keys.some((key) => typeof key !== "string" || unsafeKeys.has(key))
|
|
212
|
+
) {
|
|
213
|
+
fail();
|
|
214
|
+
}
|
|
215
|
+
const result: Record<string, JsonValue> = {};
|
|
216
|
+
for (const key of keys as string[]) {
|
|
217
|
+
keyText(key, budget);
|
|
218
|
+
const descriptor = descriptors[key];
|
|
219
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, "value")) fail();
|
|
220
|
+
result[key] = json(descriptor.value, depth + 1, budget);
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function strictRecord(value: unknown, keys: readonly string[]) {
|
|
226
|
+
if (
|
|
227
|
+
!value ||
|
|
228
|
+
typeof value !== "object" ||
|
|
229
|
+
Array.isArray(value) ||
|
|
230
|
+
Object.getPrototypeOf(value) !== Object.prototype
|
|
231
|
+
) {
|
|
232
|
+
fail();
|
|
233
|
+
}
|
|
234
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
235
|
+
const actual = Reflect.ownKeys(descriptors);
|
|
236
|
+
const required = keys.filter((key) => !key.endsWith("?"));
|
|
237
|
+
const allowed = new Set(keys.map((key) => key.replace(/\?$/u, "")));
|
|
238
|
+
if (
|
|
239
|
+
actual.some((key) => typeof key !== "string" || !allowed.has(key)) ||
|
|
240
|
+
required.some((key) => !actual.includes(key))
|
|
241
|
+
) {
|
|
242
|
+
fail();
|
|
243
|
+
}
|
|
244
|
+
const result: Record<string, unknown> = {};
|
|
245
|
+
for (const key of actual as string[]) {
|
|
246
|
+
const descriptor = descriptors[key];
|
|
247
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, "value")) fail();
|
|
248
|
+
result[key] = descriptor.value;
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function dataProperty(value: unknown, key: string) {
|
|
254
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail();
|
|
255
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
256
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, "value")) fail();
|
|
257
|
+
return descriptor.value;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function strictArray(value: readonly unknown[], expectedLength: number) {
|
|
261
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) fail();
|
|
262
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
263
|
+
const keys = Reflect.ownKeys(descriptors);
|
|
264
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
265
|
+
if (
|
|
266
|
+
keys.length !== expectedLength + 1 ||
|
|
267
|
+
!lengthDescriptor ||
|
|
268
|
+
!Object.hasOwn(lengthDescriptor, "value") ||
|
|
269
|
+
lengthDescriptor.value !== expectedLength ||
|
|
270
|
+
lengthDescriptor.enumerable
|
|
271
|
+
) {
|
|
272
|
+
fail();
|
|
273
|
+
}
|
|
274
|
+
const result: unknown[] = [];
|
|
275
|
+
for (let index = 0; index < expectedLength; index += 1) {
|
|
276
|
+
const descriptor = descriptors[String(index)];
|
|
277
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, "value")) fail();
|
|
278
|
+
result.push(descriptor.value);
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function text(value: unknown, maxBytes: number, budget: Budget) {
|
|
284
|
+
if (
|
|
285
|
+
typeof value !== "string" ||
|
|
286
|
+
value !== value.trim() ||
|
|
287
|
+
!value ||
|
|
288
|
+
value.includes("\0")
|
|
289
|
+
)
|
|
290
|
+
fail();
|
|
291
|
+
const bytes = encoder.encode(value).byteLength;
|
|
292
|
+
if (bytes > maxBytes) fail();
|
|
293
|
+
budget.stringBytes += bytes;
|
|
294
|
+
if (
|
|
295
|
+
budget.stringBytes >
|
|
296
|
+
MODULE_WORKFLOW_INPUT_MATERIALIZATION_LIMITS_V1.totalStringBytes
|
|
297
|
+
)
|
|
298
|
+
fail();
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function jsonString(value: string, budget: Budget) {
|
|
303
|
+
const bytes = encoder.encode(value).byteLength;
|
|
304
|
+
if (bytes > MODULE_WORKFLOW_INPUT_MATERIALIZATION_LIMITS_V1.stringBytes)
|
|
305
|
+
fail();
|
|
306
|
+
budget.stringBytes += bytes;
|
|
307
|
+
if (
|
|
308
|
+
budget.stringBytes >
|
|
309
|
+
MODULE_WORKFLOW_INPUT_MATERIALIZATION_LIMITS_V1.totalStringBytes
|
|
310
|
+
)
|
|
311
|
+
fail();
|
|
312
|
+
return value;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function keyText(value: string, budget: Budget) {
|
|
316
|
+
if (!value || value.includes("\0") || unsafeKeys.has(value)) fail();
|
|
317
|
+
return text(
|
|
318
|
+
value,
|
|
319
|
+
MODULE_WORKFLOW_INPUT_MATERIALIZATION_LIMITS_V1.keyBytes,
|
|
320
|
+
budget,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function digest(value: unknown, budget: Budget) {
|
|
325
|
+
const result = text(value, 71, budget);
|
|
326
|
+
if (!/^sha256:[0-9a-f]{64}$/u.test(result)) fail();
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function assertTotalBytes(value: unknown) {
|
|
331
|
+
const encoded = JSON.stringify(value);
|
|
332
|
+
if (
|
|
333
|
+
encoded === undefined ||
|
|
334
|
+
encoder.encode(encoded).byteLength >
|
|
335
|
+
MODULE_WORKFLOW_INPUT_MATERIALIZATION_LIMITS_V1.totalBytes
|
|
336
|
+
) {
|
|
337
|
+
fail();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function freshBudget(): Budget {
|
|
342
|
+
return { nodes: 0, stringBytes: 0, seen: new WeakSet() };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function fail(): never {
|
|
346
|
+
throw new Error("WORKFLOW_INPUT_MATERIALIZATION_PAYLOAD_INVALID");
|
|
347
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AuditActionType } from "@southwind-ai/shared";
|
|
2
|
+
import type { ModuleDurableHandlerDefinition } from "./durable-handler.js";
|
|
3
|
+
import {
|
|
4
|
+
moduleWorkflowInputMaterializationHandlerKey,
|
|
5
|
+
requireModuleWorkflowInputArtifactPublicationPortV1,
|
|
6
|
+
type ModuleWorkflowInputMaterializationSourceV1,
|
|
7
|
+
} from "./workflow-input-artifact-port.js";
|
|
8
|
+
import { parseModuleWorkflowInputMaterializationPayloadV1 } from "./workflow-input-artifact-validation.js";
|
|
9
|
+
|
|
10
|
+
export function defineModuleWorkflowInputMaterializationHandlerV1(input: {
|
|
11
|
+
readonly moduleName: string;
|
|
12
|
+
readonly featureKey: string;
|
|
13
|
+
readonly permissionKey: string;
|
|
14
|
+
readonly auditAction: AuditActionType;
|
|
15
|
+
}): ModuleDurableHandlerDefinition<{
|
|
16
|
+
readonly schemaVersion: 1;
|
|
17
|
+
readonly sources: readonly ModuleWorkflowInputMaterializationSourceV1[];
|
|
18
|
+
}> {
|
|
19
|
+
return {
|
|
20
|
+
key: moduleWorkflowInputMaterializationHandlerKey(input.moduleName),
|
|
21
|
+
featureKey: input.featureKey,
|
|
22
|
+
permissionKey: input.permissionKey,
|
|
23
|
+
auditAction: input.auditAction,
|
|
24
|
+
payload: { parse: parsePayload },
|
|
25
|
+
async execute(payload, context) {
|
|
26
|
+
const artifacts = requireModuleWorkflowInputArtifactPublicationPortV1(
|
|
27
|
+
context.workflowInputArtifacts,
|
|
28
|
+
);
|
|
29
|
+
for (const source of payload.sources) {
|
|
30
|
+
if (context.signal.aborted) throw context.signal.reason;
|
|
31
|
+
if (source.mode === "resource") {
|
|
32
|
+
const { mode: _mode, ...publication } = source;
|
|
33
|
+
await artifacts.publishResource(publication);
|
|
34
|
+
} else {
|
|
35
|
+
const { mode: _mode, ...publication } = source;
|
|
36
|
+
await artifacts.publishJson(publication);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const parsePayload = parseModuleWorkflowInputMaterializationPayloadV1;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { artifactRef, BlobStoreError } from "./blob-store.js";
|
|
7
|
+
import { LocalFileBlobStore } from "./local-file-blob-store.js";
|
|
8
|
+
import { MemoryBlobStore } from "./memory-blob-store.js";
|
|
9
|
+
|
|
10
|
+
const roots: string[] = [];
|
|
11
|
+
const stableRef = artifactRef(`blob_${"1".repeat(32)}`);
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await Promise.all(
|
|
15
|
+
roots.splice(0).map((root) => rm(root, { recursive: true })),
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("BlobStore putOnce", () => {
|
|
20
|
+
test("Memory exact replay 返回同一 ref,内容漂移 fail closed", async () => {
|
|
21
|
+
const store = new MemoryBlobStore();
|
|
22
|
+
const first = await store.putOnce!(input("same"));
|
|
23
|
+
const replay = await store.putOnce!(input("same"));
|
|
24
|
+
expect(replay.ref).toBe(first.ref);
|
|
25
|
+
await expect(store.putOnce!(input("drift"))).rejects.toMatchObject({
|
|
26
|
+
code: "BLOB_IDENTITY_CONFLICT",
|
|
27
|
+
} satisfies Partial<BlobStoreError>);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("Local 并发 exact replay 只发布一个稳定 Blob", async () => {
|
|
31
|
+
const store = await localStore();
|
|
32
|
+
const [left, right] = await Promise.all([
|
|
33
|
+
store.putOnce!(input("same")),
|
|
34
|
+
store.putOnce!(input("same")),
|
|
35
|
+
]);
|
|
36
|
+
expect(left.ref).toBe(stableRef);
|
|
37
|
+
expect(right.ref).toBe(stableRef);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("Local blob-only commit unknown 可按同内容补 seal", async () => {
|
|
41
|
+
const { store, root } = await localStoreWithRoot();
|
|
42
|
+
const directory = join(root, "blobs", stableRef.slice(5, 7));
|
|
43
|
+
await mkdir(directory, { recursive: true });
|
|
44
|
+
await writeFile(join(directory, `${stableRef}.blob`), "same");
|
|
45
|
+
expect((await store.putOnce!(input("same"))).ref).toBe(stableRef);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function input(value: string) {
|
|
50
|
+
const bytes = new TextEncoder().encode(value);
|
|
51
|
+
return {
|
|
52
|
+
ref: stableRef,
|
|
53
|
+
body: new Blob([bytes]).stream(),
|
|
54
|
+
expectedByteSize: bytes.byteLength,
|
|
55
|
+
expectedContentHash: {
|
|
56
|
+
algorithm: "sha256" as const,
|
|
57
|
+
value: createHash("sha256").update(bytes).digest("hex"),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function localStore() {
|
|
63
|
+
return (await localStoreWithRoot()).store;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function localStoreWithRoot() {
|
|
67
|
+
const root = await mkdtemp(join(tmpdir(), "windy-blob-once-"));
|
|
68
|
+
roots.push(root);
|
|
69
|
+
return { root, store: new LocalFileBlobStore({ root }) };
|
|
70
|
+
}
|
|
@@ -22,8 +22,16 @@ export interface PutBlobInput {
|
|
|
22
22
|
expectedContentHash?: ContentHash;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface PutBlobOnceInput extends PutBlobInput {
|
|
26
|
+
readonly ref: ArtifactRef;
|
|
27
|
+
readonly expectedByteSize: number;
|
|
28
|
+
readonly expectedContentHash: ContentHash;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
export interface BlobStore {
|
|
26
32
|
put(input: PutBlobInput): Promise<StoredBlob>;
|
|
33
|
+
/** Exact replay returns the original Blob; coordinate or content drift fails closed. */
|
|
34
|
+
putOnce?(input: PutBlobOnceInput): Promise<StoredBlob>;
|
|
27
35
|
inspect(ref: ArtifactRef): Promise<StoredBlob | undefined>;
|
|
28
36
|
open(ref: ArtifactRef): Promise<ReadableBlob | undefined>;
|
|
29
37
|
delete(ref: ArtifactRef): Promise<boolean>;
|
|
@@ -42,6 +50,7 @@ export type BlobStoreErrorCode =
|
|
|
42
50
|
| "INVALID_CONTENT_HASH"
|
|
43
51
|
| "SIZE_MISMATCH"
|
|
44
52
|
| "HASH_MISMATCH"
|
|
53
|
+
| "BLOB_IDENTITY_CONFLICT"
|
|
45
54
|
| "CORRUPT_BLOB_METADATA";
|
|
46
55
|
|
|
47
56
|
export class BlobStoreError extends Error {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { link, open, stat, type FileHandle } from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
BlobStoreError,
|
|
4
|
+
type ArtifactRef,
|
|
5
|
+
type PutBlobOnceInput,
|
|
6
|
+
type StoredBlob,
|
|
7
|
+
} from "./blob-store.js";
|
|
8
|
+
import { createSha256Accumulator } from "./content-hash.js";
|
|
9
|
+
import { writeLocalBlob } from "./local-file-blob-writer.js";
|
|
10
|
+
|
|
11
|
+
export async function installBlobOnce(input: {
|
|
12
|
+
ref: ArtifactRef;
|
|
13
|
+
input: PutBlobOnceInput;
|
|
14
|
+
blob: string;
|
|
15
|
+
temporaryBlob: string;
|
|
16
|
+
now: () => Date;
|
|
17
|
+
}): Promise<StoredBlob> {
|
|
18
|
+
if (await pathExists(input.blob)) return inspectUnsealedBlob(input);
|
|
19
|
+
const candidate = await writeLocalBlob(
|
|
20
|
+
input.temporaryBlob,
|
|
21
|
+
input.ref,
|
|
22
|
+
input.input,
|
|
23
|
+
input.now,
|
|
24
|
+
);
|
|
25
|
+
try {
|
|
26
|
+
await link(input.temporaryBlob, input.blob);
|
|
27
|
+
return candidate;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (!isExists(error)) throw error;
|
|
30
|
+
return inspectUnsealedBlob(input);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function linkOnce(source: string, target: string) {
|
|
35
|
+
try {
|
|
36
|
+
await link(source, target);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (!isExists(error)) throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function requireExactBlob(
|
|
43
|
+
stored: StoredBlob,
|
|
44
|
+
input: PutBlobOnceInput,
|
|
45
|
+
): StoredBlob {
|
|
46
|
+
if (
|
|
47
|
+
stored.ref !== input.ref ||
|
|
48
|
+
stored.byteSize !== input.expectedByteSize ||
|
|
49
|
+
stored.contentHash.algorithm !== input.expectedContentHash.algorithm ||
|
|
50
|
+
stored.contentHash.value !== input.expectedContentHash.value
|
|
51
|
+
) {
|
|
52
|
+
throw new BlobStoreError(
|
|
53
|
+
"BLOB_IDENTITY_CONFLICT",
|
|
54
|
+
`Blob 稳定坐标冲突:${input.ref}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return stored;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function inspectUnsealedBlob(input: {
|
|
61
|
+
ref: ArtifactRef;
|
|
62
|
+
input: PutBlobOnceInput;
|
|
63
|
+
blob: string;
|
|
64
|
+
now: () => Date;
|
|
65
|
+
}) {
|
|
66
|
+
const handle = await open(input.blob, "r");
|
|
67
|
+
try {
|
|
68
|
+
return requireExactBlob(
|
|
69
|
+
{
|
|
70
|
+
ref: input.ref,
|
|
71
|
+
byteSize: (await handle.stat()).size,
|
|
72
|
+
contentHash: { algorithm: "sha256", value: await hashHandle(handle) },
|
|
73
|
+
createdAt: input.now().toISOString(),
|
|
74
|
+
},
|
|
75
|
+
input.input,
|
|
76
|
+
);
|
|
77
|
+
} finally {
|
|
78
|
+
await handle.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function hashHandle(handle: FileHandle): Promise<string> {
|
|
83
|
+
const hash = createSha256Accumulator();
|
|
84
|
+
const chunk = new Uint8Array(64 * 1024);
|
|
85
|
+
let position = 0;
|
|
86
|
+
while (true) {
|
|
87
|
+
const { bytesRead } = await handle.read(
|
|
88
|
+
chunk,
|
|
89
|
+
0,
|
|
90
|
+
chunk.byteLength,
|
|
91
|
+
position,
|
|
92
|
+
);
|
|
93
|
+
if (bytesRead === 0) break;
|
|
94
|
+
hash.update(chunk.subarray(0, bytesRead));
|
|
95
|
+
position += bytesRead;
|
|
96
|
+
}
|
|
97
|
+
return hash.digest().value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
101
|
+
try {
|
|
102
|
+
await stat(path);
|
|
103
|
+
return true;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (isCode(error, "ENOENT")) return false;
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isExists(error: unknown) {
|
|
111
|
+
return isCode(error, "EEXIST");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isCode(error: unknown, code: string) {
|
|
115
|
+
return (
|
|
116
|
+
error instanceof Error &&
|
|
117
|
+
"code" in error &&
|
|
118
|
+
(error as NodeJS.ErrnoException).code === code
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type ArtifactRef,
|
|
16
16
|
type BlobStore,
|
|
17
17
|
type PutBlobInput,
|
|
18
|
+
type PutBlobOnceInput,
|
|
18
19
|
type ReadableBlob,
|
|
19
20
|
type StoredBlob,
|
|
20
21
|
} from "./blob-store.js";
|
|
@@ -23,6 +24,11 @@ import {
|
|
|
23
24
|
validateExpectedSize,
|
|
24
25
|
writeLocalBlob,
|
|
25
26
|
} from "./local-file-blob-writer.js";
|
|
27
|
+
import {
|
|
28
|
+
installBlobOnce,
|
|
29
|
+
linkOnce,
|
|
30
|
+
requireExactBlob,
|
|
31
|
+
} from "./local-file-blob-put-once.js";
|
|
26
32
|
|
|
27
33
|
export interface LocalFileBlobStoreOptions {
|
|
28
34
|
root: string;
|
|
@@ -40,8 +46,54 @@ export class LocalFileBlobStore implements BlobStore {
|
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
async put(input: PutBlobInput): Promise<StoredBlob> {
|
|
49
|
+
return this.write(this.createRef(), input);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async putOnce(input: PutBlobOnceInput): Promise<StoredBlob> {
|
|
53
|
+
const ref = artifactRef(input.ref);
|
|
54
|
+
validateExpectedSize(input.expectedByteSize);
|
|
55
|
+
const paths = this.paths(ref);
|
|
56
|
+
await mkdir(paths.directory, { recursive: true });
|
|
57
|
+
await mkdir(paths.temporaryDirectory, { recursive: true });
|
|
58
|
+
const suffix = randomUUID();
|
|
59
|
+
const temporaryBlob = join(
|
|
60
|
+
paths.temporaryDirectory,
|
|
61
|
+
`${ref}.${suffix}.partial`,
|
|
62
|
+
);
|
|
63
|
+
const temporaryMetadata = join(
|
|
64
|
+
paths.temporaryDirectory,
|
|
65
|
+
`${ref}.${suffix}.json.partial`,
|
|
66
|
+
);
|
|
67
|
+
try {
|
|
68
|
+
const existing = await this.inspect(ref);
|
|
69
|
+
if (existing) return requireExactBlob(existing, input);
|
|
70
|
+
const stored = await installBlobOnce({
|
|
71
|
+
ref,
|
|
72
|
+
input,
|
|
73
|
+
blob: paths.blob,
|
|
74
|
+
temporaryBlob,
|
|
75
|
+
now: this.now,
|
|
76
|
+
});
|
|
77
|
+
await syncDirectory(paths.directory);
|
|
78
|
+
await writeDurably(temporaryMetadata, `${JSON.stringify(stored)}\n`);
|
|
79
|
+
await linkOnce(temporaryMetadata, paths.metadata);
|
|
80
|
+
await syncDirectory(paths.directory);
|
|
81
|
+
const committed = await this.inspect(ref);
|
|
82
|
+
if (!committed) throw corruptMetadata(ref);
|
|
83
|
+
return requireExactBlob(committed, input);
|
|
84
|
+
} finally {
|
|
85
|
+
await Promise.all([
|
|
86
|
+
rm(temporaryBlob, { force: true }),
|
|
87
|
+
rm(temporaryMetadata, { force: true }),
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async write(
|
|
93
|
+
ref: ArtifactRef,
|
|
94
|
+
input: PutBlobInput,
|
|
95
|
+
): Promise<StoredBlob> {
|
|
43
96
|
validateExpectedSize(input.expectedByteSize);
|
|
44
|
-
const ref = this.createRef();
|
|
45
97
|
const paths = this.paths(ref);
|
|
46
98
|
await mkdir(paths.directory, { recursive: true });
|
|
47
99
|
await mkdir(paths.temporaryDirectory, { recursive: true });
|