@zhivex-ai/core 1.5.0 → 1.7.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/README.md +39 -0
- package/dist/agent-handoff-contracts.d.ts +12 -0
- package/dist/agent-handoff-contracts.d.ts.map +1 -0
- package/dist/agent-handoff-contracts.js +18 -0
- package/dist/agent-handoff-contracts.js.map +1 -0
- package/dist/agent-handoff.d.ts +2 -11
- package/dist/agent-handoff.d.ts.map +1 -1
- package/dist/agent-handoff.js +1 -18
- package/dist/agent-handoff.js.map +1 -1
- package/dist/agent-trace.d.ts +1 -5
- package/dist/agent-trace.d.ts.map +1 -1
- package/dist/agent-trace.js.map +1 -1
- package/dist/agent.d.ts +1 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +358 -215
- package/dist/agent.js.map +1 -1
- package/dist/api-stability.d.ts.map +1 -1
- package/dist/api-stability.js +36 -25
- package/dist/api-stability.js.map +1 -1
- package/dist/artifact.d.ts +32 -9
- package/dist/artifact.d.ts.map +1 -1
- package/dist/artifact.js +428 -82
- package/dist/artifact.js.map +1 -1
- package/dist/catalog.d.ts +42 -3
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +269 -7
- package/dist/catalog.js.map +1 -1
- package/dist/contracts.d.ts +8 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +2 -0
- package/dist/contracts.js.map +1 -0
- package/dist/generate-text.d.ts.map +1 -1
- package/dist/generate-text.js +6 -3
- package/dist/generate-text.js.map +1 -1
- package/dist/google.d.ts +1 -60
- package/dist/google.d.ts.map +1 -1
- package/dist/google.js +0 -198
- package/dist/google.js.map +1 -1
- package/dist/index.d.ts +13 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/live-agent.js +1 -1
- package/dist/live-agent.js.map +1 -1
- package/dist/middleware.d.ts +16 -0
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +476 -110
- package/dist/middleware.js.map +1 -1
- package/dist/model-capabilities.d.ts +66 -0
- package/dist/model-capabilities.d.ts.map +1 -0
- package/dist/model-capabilities.js +136 -0
- package/dist/model-capabilities.js.map +1 -0
- package/dist/node.d.ts +8 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +8 -0
- package/dist/node.js.map +1 -0
- package/dist/observability.d.ts +57 -10
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +535 -114
- package/dist/observability.js.map +1 -1
- package/dist/pricing.d.ts +7 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +2 -0
- package/dist/pricing.js.map +1 -0
- package/dist/provider-resources.d.ts +61 -0
- package/dist/provider-resources.d.ts.map +1 -0
- package/dist/provider-resources.js +199 -0
- package/dist/provider-resources.js.map +1 -0
- package/dist/runner.js +1 -1
- package/dist/runner.js.map +1 -1
- package/dist/runtime-entry.d.ts +15 -0
- package/dist/runtime-entry.d.ts.map +1 -0
- package/dist/runtime-entry.js +14 -0
- package/dist/runtime-entry.js.map +1 -0
- package/dist/runtime.d.ts +9 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +35 -8
- package/dist/runtime.js.map +1 -1
- package/dist/secure-id-node.d.ts +2 -0
- package/dist/secure-id-node.d.ts.map +1 -0
- package/dist/secure-id-node.js +3 -0
- package/dist/secure-id-node.js.map +1 -0
- package/dist/secure-id.d.ts.map +1 -1
- package/dist/secure-id.js +6 -2
- package/dist/secure-id.js.map +1 -1
- package/dist/testing.d.ts +4 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +4 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +74 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/ui-entry.d.ts +6 -0
- package/dist/ui-entry.d.ts.map +1 -0
- package/dist/ui-entry.js +5 -0
- package/dist/ui-entry.js.map +1 -0
- package/dist/ui.js +1 -1
- package/dist/ui.js.map +1 -1
- package/dist/workflow-observability.d.ts +13 -0
- package/dist/workflow-observability.d.ts.map +1 -0
- package/dist/workflow-observability.js +186 -0
- package/dist/workflow-observability.js.map +1 -0
- package/dist/workflow-state-contracts.d.ts +76 -0
- package/dist/workflow-state-contracts.d.ts.map +1 -0
- package/dist/workflow-state-contracts.js +49 -0
- package/dist/workflow-state-contracts.js.map +1 -0
- package/dist/workflow-state-service.d.ts +3 -34
- package/dist/workflow-state-service.d.ts.map +1 -1
- package/dist/workflow-state-service.js +2 -2
- package/dist/workflow-state-service.js.map +1 -1
- package/dist/workflow.d.ts +53 -42
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +258 -125
- package/dist/workflow.js.map +1 -1
- package/dist/workflows-entry.d.ts +9 -0
- package/dist/workflows-entry.d.ts.map +1 -0
- package/dist/workflows-entry.js +5 -0
- package/dist/workflows-entry.js.map +1 -0
- package/package.json +42 -1
- package/secure-id-internal.d.ts +1 -0
package/dist/artifact.js
CHANGED
|
@@ -3,11 +3,20 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { ConflictError, ValidationError } from "./errors.js";
|
|
5
5
|
import { assertPostgresClient } from "./postgres-client.js";
|
|
6
|
-
import { createSecureId } from "
|
|
6
|
+
import { createSecureId } from "#secure-id";
|
|
7
7
|
import { canonicalStoreFileStem, canonicalStoreKey, ensurePrivateDirectory, writePrivateFile } from "./store-security.js";
|
|
8
8
|
const randomId = createSecureId;
|
|
9
9
|
const cloneJson = (value) => JSON.parse(JSON.stringify(value));
|
|
10
|
+
const MIB = 1024 * 1024;
|
|
10
11
|
export const ARTIFACT_SCHEMA_VERSION = 1;
|
|
12
|
+
export const DEFAULT_ARTIFACT_SERVICE_LIMITS = Object.freeze({
|
|
13
|
+
maxJsonBytes: MIB,
|
|
14
|
+
maxTextBytes: MIB,
|
|
15
|
+
maxBase64Bytes: 16 * MIB,
|
|
16
|
+
maxBinaryBytes: 16 * MIB,
|
|
17
|
+
maxMetadataBytes: 64 * 1024,
|
|
18
|
+
maxRecordBytes: 24 * MIB
|
|
19
|
+
});
|
|
11
20
|
const artifactParts = (input) => [input.appName, input.userId, input.sessionId, input.id];
|
|
12
21
|
const artifactKey = (input) => canonicalStoreKey("artifact", artifactParts(input));
|
|
13
22
|
const legacyArtifactKey = (input) => `${input.appName}:${input.userId}:${input.sessionId}:${input.id}`;
|
|
@@ -16,25 +25,141 @@ const matchesArtifactLookup = (artifact, input) => artifact.appName === input.ap
|
|
|
16
25
|
artifact.sessionId === input.sessionId &&
|
|
17
26
|
artifact.id === input.id;
|
|
18
27
|
const identifierPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
28
|
+
const artifactEncodings = new Set(["json", "text", "base64"]);
|
|
29
|
+
const artifactStorageModes = new Set(["json", "binary"]);
|
|
30
|
+
const positiveSafeIntegerLimit = (value, fallback, name) => {
|
|
31
|
+
const resolved = value ?? fallback;
|
|
32
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0) {
|
|
33
|
+
throw new ValidationError(`The "${name}" artifact limit must be a positive safe integer.`);
|
|
34
|
+
}
|
|
35
|
+
return resolved;
|
|
36
|
+
};
|
|
37
|
+
export const resolveArtifactServiceLimits = (limits = {}) => ({
|
|
38
|
+
maxJsonBytes: positiveSafeIntegerLimit(limits.maxJsonBytes, DEFAULT_ARTIFACT_SERVICE_LIMITS.maxJsonBytes, "maxJsonBytes"),
|
|
39
|
+
maxTextBytes: positiveSafeIntegerLimit(limits.maxTextBytes, DEFAULT_ARTIFACT_SERVICE_LIMITS.maxTextBytes, "maxTextBytes"),
|
|
40
|
+
maxBase64Bytes: positiveSafeIntegerLimit(limits.maxBase64Bytes, DEFAULT_ARTIFACT_SERVICE_LIMITS.maxBase64Bytes, "maxBase64Bytes"),
|
|
41
|
+
maxBinaryBytes: positiveSafeIntegerLimit(limits.maxBinaryBytes, DEFAULT_ARTIFACT_SERVICE_LIMITS.maxBinaryBytes, "maxBinaryBytes"),
|
|
42
|
+
maxMetadataBytes: positiveSafeIntegerLimit(limits.maxMetadataBytes, DEFAULT_ARTIFACT_SERVICE_LIMITS.maxMetadataBytes, "maxMetadataBytes"),
|
|
43
|
+
maxRecordBytes: positiveSafeIntegerLimit(limits.maxRecordBytes, DEFAULT_ARTIFACT_SERVICE_LIMITS.maxRecordBytes, "maxRecordBytes")
|
|
44
|
+
});
|
|
45
|
+
const jsonText = (value, fieldName) => {
|
|
46
|
+
try {
|
|
47
|
+
const serialized = JSON.stringify(value);
|
|
48
|
+
if (serialized === undefined) {
|
|
49
|
+
throw new ValidationError(`Artifact ${fieldName} must be JSON-serializable.`);
|
|
50
|
+
}
|
|
51
|
+
return serialized;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof ValidationError) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
throw new ValidationError(`Artifact ${fieldName} must be JSON-serializable.`, { cause: error });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const utf8Bytes = (value) => Buffer.byteLength(value, "utf8");
|
|
61
|
+
const assertByteLimit = (actual, maximum, fieldName) => {
|
|
62
|
+
if (actual > maximum) {
|
|
63
|
+
throw new ValidationError(`Artifact ${fieldName} is ${actual} bytes and exceeds the ${maximum}-byte limit.`);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const validateRequiredString = (value, fieldName) => {
|
|
67
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
68
|
+
throw new ValidationError(`Artifact "${fieldName}" must be a non-empty string.`);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const validateOptionalString = (value, fieldName) => {
|
|
72
|
+
if (value !== undefined) {
|
|
73
|
+
validateRequiredString(value, fieldName);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const validateArtifactLookup = (input) => {
|
|
77
|
+
validateRequiredString(input.appName, "appName");
|
|
78
|
+
validateRequiredString(input.userId, "userId");
|
|
79
|
+
validateRequiredString(input.sessionId, "sessionId");
|
|
80
|
+
validateRequiredString(input.id, "id");
|
|
81
|
+
};
|
|
82
|
+
const validateArtifactListInput = (input) => {
|
|
83
|
+
validateRequiredString(input.appName, "appName");
|
|
84
|
+
validateRequiredString(input.userId, "userId");
|
|
85
|
+
validateRequiredString(input.sessionId, "sessionId");
|
|
86
|
+
validateOptionalString(input.workflowRunId, "workflowRunId");
|
|
87
|
+
validateOptionalString(input.workflowStepId, "workflowStepId");
|
|
88
|
+
validateOptionalString(input.agentRunId, "agentRunId");
|
|
89
|
+
};
|
|
90
|
+
const validateRevision = (revision, fieldName = "revision") => {
|
|
91
|
+
if (revision !== undefined && (!Number.isSafeInteger(revision) || revision < 0)) {
|
|
92
|
+
throw new ValidationError(`Artifact "${fieldName}" must be a non-negative safe integer.`);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const externalBlobReference = (metadata) => {
|
|
96
|
+
const externalBlob = metadata?.externalBlob;
|
|
97
|
+
if (!externalBlob || typeof externalBlob !== "object" || Array.isArray(externalBlob)) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const reference = externalBlob;
|
|
101
|
+
return reference.managedBy === "application" &&
|
|
102
|
+
typeof reference.uri === "string" &&
|
|
103
|
+
reference.uri.length > 0 &&
|
|
104
|
+
!/[\u0000-\u001f\u007f]/.test(reference.uri)
|
|
105
|
+
? { uri: reference.uri }
|
|
106
|
+
: undefined;
|
|
107
|
+
};
|
|
19
108
|
export const normalizeArtifactRecord = (value) => {
|
|
20
109
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
21
110
|
throw new ValidationError("ArtifactRecord must be an object.");
|
|
22
111
|
}
|
|
23
112
|
const artifact = value;
|
|
24
|
-
if (artifact.schemaVersion !== undefined &&
|
|
113
|
+
if (artifact.schemaVersion !== undefined &&
|
|
114
|
+
(!Number.isSafeInteger(artifact.schemaVersion) || artifact.schemaVersion < 0 || artifact.schemaVersion > ARTIFACT_SCHEMA_VERSION)) {
|
|
25
115
|
throw new ValidationError(`Unsupported ArtifactRecord schemaVersion ${artifact.schemaVersion}.`);
|
|
26
116
|
}
|
|
27
|
-
if (
|
|
28
|
-
typeof artifact.appName !== "string" ||
|
|
29
|
-
typeof artifact.userId !== "string" ||
|
|
30
|
-
typeof artifact.sessionId !== "string" ||
|
|
31
|
-
typeof artifact.name !== "string" ||
|
|
32
|
-
typeof artifact.contentType !== "string" ||
|
|
33
|
-
typeof artifact.createdAt !== "number" ||
|
|
34
|
-
typeof artifact.updatedAt !== "number" ||
|
|
35
|
-
!("data" in artifact)) {
|
|
117
|
+
if (!("data" in artifact)) {
|
|
36
118
|
throw new ValidationError("ArtifactRecord is missing required fields.");
|
|
37
119
|
}
|
|
120
|
+
validateRequiredString(artifact.id, "id");
|
|
121
|
+
validateRequiredString(artifact.appName, "appName");
|
|
122
|
+
validateRequiredString(artifact.userId, "userId");
|
|
123
|
+
validateRequiredString(artifact.sessionId, "sessionId");
|
|
124
|
+
validateRequiredString(artifact.name, "name");
|
|
125
|
+
validateRequiredString(artifact.contentType, "contentType");
|
|
126
|
+
validateOptionalString(artifact.workflowRunId, "workflowRunId");
|
|
127
|
+
validateOptionalString(artifact.workflowStepId, "workflowStepId");
|
|
128
|
+
validateOptionalString(artifact.agentRunId, "agentRunId");
|
|
129
|
+
if (artifact.revision !== undefined && (!Number.isSafeInteger(artifact.revision) || artifact.revision < 1)) {
|
|
130
|
+
throw new ValidationError('Artifact "revision" must be a positive safe integer.');
|
|
131
|
+
}
|
|
132
|
+
if (!Number.isSafeInteger(artifact.createdAt) || artifact.createdAt < 0) {
|
|
133
|
+
throw new ValidationError('Artifact "createdAt" must be a non-negative safe integer.');
|
|
134
|
+
}
|
|
135
|
+
if (!Number.isSafeInteger(artifact.updatedAt) || artifact.updatedAt < artifact.createdAt) {
|
|
136
|
+
throw new ValidationError('Artifact "updatedAt" must be a safe integer greater than or equal to "createdAt".');
|
|
137
|
+
}
|
|
138
|
+
if (artifact.encoding !== undefined && !artifactEncodings.has(artifact.encoding)) {
|
|
139
|
+
throw new ValidationError('Artifact "encoding" must be "json", "text", or "base64".');
|
|
140
|
+
}
|
|
141
|
+
if (artifact.storageMode !== undefined && !artifactStorageModes.has(artifact.storageMode)) {
|
|
142
|
+
throw new ValidationError('Artifact "storageMode" must be "json" or "binary".');
|
|
143
|
+
}
|
|
144
|
+
if (artifact.metadata !== undefined && (!artifact.metadata || typeof artifact.metadata !== "object" || Array.isArray(artifact.metadata))) {
|
|
145
|
+
throw new ValidationError('Artifact "metadata" must be a JSON object.');
|
|
146
|
+
}
|
|
147
|
+
if ((artifact.storageMode ?? "json") !== "binary" &&
|
|
148
|
+
(artifact.encoding === "base64" || artifact.encoding === "text") &&
|
|
149
|
+
typeof artifact.data !== "string") {
|
|
150
|
+
throw new ValidationError(`Artifact data must be a string when "encoding" is "${artifact.encoding}".`);
|
|
151
|
+
}
|
|
152
|
+
if ((artifact.storageMode ?? "json") === "binary" && artifact.data !== null) {
|
|
153
|
+
throw new ValidationError('Artifact data must be null when "storageMode" is "binary".');
|
|
154
|
+
}
|
|
155
|
+
if (artifact.blobPath !== undefined && (artifact.storageMode !== "binary" || artifact.data !== null)) {
|
|
156
|
+
throw new ValidationError('Artifact "blobPath" is only valid for separate binary storage.');
|
|
157
|
+
}
|
|
158
|
+
validateArtifactMetadata({ size: artifact.size, sha256: artifact.sha256 });
|
|
159
|
+
jsonText(artifact.data, "data");
|
|
160
|
+
if (artifact.metadata !== undefined) {
|
|
161
|
+
jsonText(artifact.metadata, "metadata");
|
|
162
|
+
}
|
|
38
163
|
return {
|
|
39
164
|
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
40
165
|
revision: typeof artifact.revision === "number" ? artifact.revision : 1,
|
|
@@ -65,7 +190,9 @@ export const migrateArtifactRecord = (value, targetVersion = ARTIFACT_SCHEMA_VER
|
|
|
65
190
|
return normalizeArtifactRecord(value);
|
|
66
191
|
};
|
|
67
192
|
const cloneArtifact = (artifact) => cloneJson(normalizeArtifactRecord(artifact));
|
|
68
|
-
const
|
|
193
|
+
const binaryInputByteLength = (data) => typeof data === "string" ? utf8Bytes(data) : data.byteLength;
|
|
194
|
+
const bytesFromBinaryInput = (data, maxBytes) => {
|
|
195
|
+
assertByteLimit(binaryInputByteLength(data), maxBytes, "binary data");
|
|
69
196
|
const buffer = typeof data === "string"
|
|
70
197
|
? Buffer.from(data, "utf8")
|
|
71
198
|
: data instanceof Uint8Array
|
|
@@ -83,6 +210,7 @@ const resolveBinarySha256 = (data, expectedSha256) => {
|
|
|
83
210
|
return actualSha256;
|
|
84
211
|
};
|
|
85
212
|
const assertExpectedRevision = (current, expectedRevision, resource) => {
|
|
213
|
+
validateRevision(expectedRevision, "expectedRevision");
|
|
86
214
|
if (expectedRevision !== undefined && (current?.revision ?? 0) !== expectedRevision) {
|
|
87
215
|
throw new ConflictError(`${resource} revision conflict.`);
|
|
88
216
|
}
|
|
@@ -103,14 +231,74 @@ const validateArtifactMetadata = (input) => {
|
|
|
103
231
|
throw new ValidationError('The "sha256" artifact option must be a 64-character hexadecimal digest.');
|
|
104
232
|
}
|
|
105
233
|
};
|
|
106
|
-
const
|
|
234
|
+
const encodedBase64Length = (decodedBytes) => 4 * Math.ceil(decodedBytes / 3);
|
|
235
|
+
const base64Bytes = (value, maxBytes) => {
|
|
236
|
+
assertByteLimit(utf8Bytes(value), encodedBase64Length(maxBytes), "base64 data");
|
|
107
237
|
const normalized = value.replace(/\s/g, "");
|
|
108
238
|
if (normalized.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
|
|
109
239
|
throw new ValidationError('Artifact data must be valid base64 when "encoding" is "base64".');
|
|
110
240
|
}
|
|
111
|
-
|
|
241
|
+
const padding = normalized.endsWith("==") ? 2 : normalized.endsWith("=") ? 1 : 0;
|
|
242
|
+
const decodedBytes = normalized.length === 0 ? 0 : (normalized.length / 4) * 3 - padding;
|
|
243
|
+
assertByteLimit(decodedBytes, maxBytes, "decoded base64 data");
|
|
244
|
+
const bytes = new Uint8Array(Buffer.from(normalized, "base64"));
|
|
245
|
+
if (bytes.byteLength !== decodedBytes) {
|
|
246
|
+
throw new ValidationError('Artifact data must be valid base64 when "encoding" is "base64".');
|
|
247
|
+
}
|
|
248
|
+
return bytes;
|
|
249
|
+
};
|
|
250
|
+
const validateArtifactSaveInput = (input, limits, internal = {}) => {
|
|
251
|
+
validateRequiredString(input.appName, "appName");
|
|
252
|
+
validateRequiredString(input.userId, "userId");
|
|
253
|
+
validateRequiredString(input.sessionId, "sessionId");
|
|
254
|
+
if (input.id !== undefined) {
|
|
255
|
+
validateRequiredString(input.id, "id");
|
|
256
|
+
}
|
|
257
|
+
validateOptionalString(input.workflowRunId, "workflowRunId");
|
|
258
|
+
validateOptionalString(input.workflowStepId, "workflowStepId");
|
|
259
|
+
validateOptionalString(input.agentRunId, "agentRunId");
|
|
260
|
+
validateRequiredString(input.name, "name");
|
|
261
|
+
validateRequiredString(input.contentType, "contentType");
|
|
262
|
+
validateRevision(input.expectedRevision, "expectedRevision");
|
|
263
|
+
if (input.encoding !== undefined && !artifactEncodings.has(input.encoding)) {
|
|
264
|
+
throw new ValidationError('Artifact "encoding" must be "json", "text", or "base64".');
|
|
265
|
+
}
|
|
266
|
+
if (input.storageMode !== undefined && !artifactStorageModes.has(input.storageMode)) {
|
|
267
|
+
throw new ValidationError('Artifact "storageMode" must be "json" or "binary".');
|
|
268
|
+
}
|
|
269
|
+
if (input.metadata !== undefined && (!input.metadata || typeof input.metadata !== "object" || Array.isArray(input.metadata))) {
|
|
270
|
+
throw new ValidationError('Artifact "metadata" must be a JSON object.');
|
|
271
|
+
}
|
|
272
|
+
if (input.metadata !== undefined) {
|
|
273
|
+
assertByteLimit(utf8Bytes(jsonText(input.metadata, "metadata")), limits.maxMetadataBytes, "metadata");
|
|
274
|
+
}
|
|
275
|
+
if ((input.storageMode ?? "json") === "binary") {
|
|
276
|
+
if (input.data !== null) {
|
|
277
|
+
throw new ValidationError('Artifact data must be null when "storageMode" is "binary".');
|
|
278
|
+
}
|
|
279
|
+
if (!internal.managedBinary && !externalBlobReference(input.metadata)) {
|
|
280
|
+
throw new ValidationError('Binary artifact metadata must contain an application-managed externalBlob reference.');
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (input.encoding === "base64") {
|
|
285
|
+
if (typeof input.data !== "string") {
|
|
286
|
+
throw new ValidationError('Artifact data must be a string when "encoding" is "base64".');
|
|
287
|
+
}
|
|
288
|
+
base64Bytes(input.data, limits.maxBase64Bytes);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (input.encoding === "text" && typeof input.data !== "string") {
|
|
292
|
+
throw new ValidationError('Artifact data must be a string when "encoding" is "text".');
|
|
293
|
+
}
|
|
294
|
+
if (typeof input.data === "string") {
|
|
295
|
+
assertByteLimit(utf8Bytes(input.data), limits.maxTextBytes, "text data");
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
assertByteLimit(utf8Bytes(jsonText(input.data, "data")), limits.maxJsonBytes, "JSON data");
|
|
299
|
+
}
|
|
112
300
|
};
|
|
113
|
-
const enrichArtifactMetadata = (input) => {
|
|
301
|
+
const enrichArtifactMetadata = (input, limits) => {
|
|
114
302
|
validateArtifactMetadata(input);
|
|
115
303
|
if (input.encoding !== "base64" || typeof input.data !== "string") {
|
|
116
304
|
return {
|
|
@@ -118,7 +306,7 @@ const enrichArtifactMetadata = (input) => {
|
|
|
118
306
|
sha256: input.sha256
|
|
119
307
|
};
|
|
120
308
|
}
|
|
121
|
-
const bytes = base64Bytes(input.data);
|
|
309
|
+
const bytes = base64Bytes(input.data, limits.maxBase64Bytes);
|
|
122
310
|
const actualSha256 = sha256Digest(bytes);
|
|
123
311
|
if (input.size !== undefined && input.size !== bytes.byteLength) {
|
|
124
312
|
throw new ValidationError('The "size" artifact option does not match the decoded base64 data.');
|
|
@@ -131,6 +319,27 @@ const enrichArtifactMetadata = (input) => {
|
|
|
131
319
|
sha256: actualSha256
|
|
132
320
|
};
|
|
133
321
|
};
|
|
322
|
+
const validateArtifactRecordLimits = (artifact, limits) => {
|
|
323
|
+
assertByteLimit(utf8Bytes(jsonText(artifact, "record")), limits.maxRecordBytes, "record");
|
|
324
|
+
if (artifact.metadata !== undefined) {
|
|
325
|
+
assertByteLimit(utf8Bytes(jsonText(artifact.metadata, "metadata")), limits.maxMetadataBytes, "metadata");
|
|
326
|
+
}
|
|
327
|
+
if (artifact.storageMode !== "binary") {
|
|
328
|
+
if (artifact.encoding === "base64") {
|
|
329
|
+
if (typeof artifact.data !== "string") {
|
|
330
|
+
throw new ValidationError('Artifact data must be a string when "encoding" is "base64".');
|
|
331
|
+
}
|
|
332
|
+
base64Bytes(artifact.data, limits.maxBase64Bytes);
|
|
333
|
+
}
|
|
334
|
+
else if (typeof artifact.data === "string") {
|
|
335
|
+
assertByteLimit(utf8Bytes(artifact.data), limits.maxTextBytes, "text data");
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
assertByteLimit(utf8Bytes(jsonText(artifact.data, "data")), limits.maxJsonBytes, "JSON data");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return artifact;
|
|
342
|
+
};
|
|
134
343
|
const validateIdentifier = (value, fieldName) => {
|
|
135
344
|
if (!identifierPattern.test(value)) {
|
|
136
345
|
throw new ValidationError(`The "${fieldName}" option must match the SQL identifier pattern [A-Za-z_][A-Za-z0-9_]*.`);
|
|
@@ -149,14 +358,15 @@ const getRecordField = (value, candidates) => {
|
|
|
149
358
|
}
|
|
150
359
|
return undefined;
|
|
151
360
|
};
|
|
152
|
-
const parseArtifactJson = (value) => {
|
|
361
|
+
const parseArtifactJson = (value, limits) => {
|
|
153
362
|
if (!value) {
|
|
154
363
|
return undefined;
|
|
155
364
|
}
|
|
156
365
|
if (typeof value === "string") {
|
|
157
|
-
|
|
366
|
+
assertByteLimit(utf8Bytes(value), limits.maxRecordBytes, "record");
|
|
367
|
+
return validateArtifactRecordLimits(normalizeArtifactRecord(JSON.parse(value)), limits);
|
|
158
368
|
}
|
|
159
|
-
return normalizeArtifactRecord(value);
|
|
369
|
+
return validateArtifactRecordLimits(normalizeArtifactRecord(value), limits);
|
|
160
370
|
};
|
|
161
371
|
const prepareSqliteStatement = (db, sql) => {
|
|
162
372
|
if (typeof db.prepare === "function") {
|
|
@@ -183,10 +393,11 @@ const ensurePostgresTable = (() => {
|
|
|
183
393
|
await initialization;
|
|
184
394
|
};
|
|
185
395
|
})();
|
|
186
|
-
const createArtifact = (input, existing, internal) => {
|
|
187
|
-
|
|
396
|
+
const createArtifact = (input, limits, existing, internal) => {
|
|
397
|
+
validateArtifactSaveInput(input, limits, internal);
|
|
398
|
+
const integrity = enrichArtifactMetadata(input, limits);
|
|
188
399
|
const now = Date.now();
|
|
189
|
-
|
|
400
|
+
const artifact = {
|
|
190
401
|
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
191
402
|
revision: existing ? existing.revision + 1 : 1,
|
|
192
403
|
id: input.id ?? existing?.id ?? randomId("art"),
|
|
@@ -206,8 +417,10 @@ const createArtifact = (input, existing, internal) => {
|
|
|
206
417
|
blobPath: internal?.blobPath,
|
|
207
418
|
metadata: input.metadata ? cloneJson(input.metadata) : undefined,
|
|
208
419
|
createdAt: existing?.createdAt ?? now,
|
|
209
|
-
updatedAt: now
|
|
420
|
+
updatedAt: existing ? Math.max(now, existing.updatedAt + 1) : now
|
|
210
421
|
};
|
|
422
|
+
assertByteLimit(utf8Bytes(jsonText(artifact, "record")), limits.maxRecordBytes, "record");
|
|
423
|
+
return artifact;
|
|
211
424
|
};
|
|
212
425
|
const matchesListInput = (artifact, input) => artifact.appName === input.appName &&
|
|
213
426
|
artifact.userId === input.userId &&
|
|
@@ -244,25 +457,26 @@ const lookupFromArtifact = (artifact) => ({
|
|
|
244
457
|
sessionId: artifact.sessionId,
|
|
245
458
|
id: artifact.id
|
|
246
459
|
});
|
|
247
|
-
export const createBase64ArtifactData = (input) => {
|
|
460
|
+
export const createBase64ArtifactData = (input, limits = {}) => {
|
|
248
461
|
const data = typeof input === "object" && "data" in input ? input.data : input;
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
: data instanceof Uint8Array
|
|
252
|
-
? Buffer.from(data)
|
|
253
|
-
: Buffer.from(data);
|
|
462
|
+
const resolvedLimits = resolveArtifactServiceLimits(limits);
|
|
463
|
+
const buffer = Buffer.from(bytesFromBinaryInput(data, resolvedLimits.maxBase64Bytes));
|
|
254
464
|
return {
|
|
255
465
|
data: buffer.toString("base64"),
|
|
256
466
|
encoding: "base64",
|
|
257
467
|
size: buffer.byteLength
|
|
258
468
|
};
|
|
259
469
|
};
|
|
260
|
-
export const createExternalArtifactReference = (input) => {
|
|
470
|
+
export const createExternalArtifactReference = (input, limits = {}) => {
|
|
471
|
+
const resolvedLimits = resolveArtifactServiceLimits(limits);
|
|
261
472
|
validateArtifactMetadata(input);
|
|
262
|
-
if (!input.uri) {
|
|
473
|
+
if (!input.uri || /[\u0000-\u001f\u007f]/.test(input.uri)) {
|
|
263
474
|
throw new ValidationError('The "uri" external artifact reference option is required.');
|
|
264
475
|
}
|
|
265
|
-
|
|
476
|
+
if (input.metadata !== undefined && (!input.metadata || typeof input.metadata !== "object" || Array.isArray(input.metadata))) {
|
|
477
|
+
throw new ValidationError('Artifact "metadata" must be a JSON object.');
|
|
478
|
+
}
|
|
479
|
+
const reference = {
|
|
266
480
|
data: null,
|
|
267
481
|
storageMode: "binary",
|
|
268
482
|
size: input.size,
|
|
@@ -275,14 +489,19 @@ export const createExternalArtifactReference = (input) => {
|
|
|
275
489
|
}
|
|
276
490
|
}
|
|
277
491
|
};
|
|
492
|
+
assertByteLimit(utf8Bytes(jsonText(reference.metadata, "metadata")), resolvedLimits.maxMetadataBytes, "metadata");
|
|
493
|
+
return reference;
|
|
278
494
|
};
|
|
279
|
-
export const verifyArtifactRecordIntegrity = (record, data) => {
|
|
495
|
+
export const verifyArtifactRecordIntegrity = (record, data, limits = {}) => {
|
|
280
496
|
const artifact = normalizeArtifactRecord(record);
|
|
497
|
+
const resolvedLimits = resolveArtifactServiceLimits(limits);
|
|
281
498
|
const issues = [];
|
|
282
499
|
let bytes = data;
|
|
500
|
+
let decodedInlineBase64 = false;
|
|
283
501
|
if (!bytes && artifact.encoding === "base64" && typeof artifact.data === "string") {
|
|
284
502
|
try {
|
|
285
|
-
bytes =
|
|
503
|
+
bytes = base64Bytes(artifact.data, resolvedLimits.maxBase64Bytes);
|
|
504
|
+
decodedInlineBase64 = true;
|
|
286
505
|
}
|
|
287
506
|
catch {
|
|
288
507
|
issues.push({
|
|
@@ -291,6 +510,18 @@ export const verifyArtifactRecordIntegrity = (record, data) => {
|
|
|
291
510
|
});
|
|
292
511
|
}
|
|
293
512
|
}
|
|
513
|
+
if (bytes && !decodedInlineBase64) {
|
|
514
|
+
try {
|
|
515
|
+
assertByteLimit(bytes.byteLength, resolvedLimits.maxBinaryBytes, "binary data");
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
issues.push({
|
|
519
|
+
type: "metadata-invalid",
|
|
520
|
+
message: error instanceof Error ? error.message : `Artifact "${artifact.id}" binary data exceeds its limit.`
|
|
521
|
+
});
|
|
522
|
+
bytes = undefined;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
294
525
|
if (bytes) {
|
|
295
526
|
if (artifact.size !== undefined && artifact.size !== bytes.byteLength) {
|
|
296
527
|
issues.push({
|
|
@@ -318,7 +549,7 @@ export const verifyArtifactRecordIntegrity = (record, data) => {
|
|
|
318
549
|
issues
|
|
319
550
|
};
|
|
320
551
|
};
|
|
321
|
-
export const verifyArtifactIntegrity = async (service, lookup) => {
|
|
552
|
+
export const verifyArtifactIntegrity = async (service, lookup, limits = {}) => {
|
|
322
553
|
const artifact = await service.loadArtifact(lookup);
|
|
323
554
|
if (!artifact) {
|
|
324
555
|
return {
|
|
@@ -332,18 +563,21 @@ export const verifyArtifactIntegrity = async (service, lookup) => {
|
|
|
332
563
|
if (artifact.storageMode === "binary") {
|
|
333
564
|
const binary = await service.loadBinaryArtifact(lookup);
|
|
334
565
|
if (!binary) {
|
|
566
|
+
const external = externalBlobReference(artifact.metadata);
|
|
335
567
|
return {
|
|
336
568
|
ok: false,
|
|
337
569
|
artifact,
|
|
338
570
|
issues: [{
|
|
339
|
-
type: "missing-blob",
|
|
340
|
-
message:
|
|
571
|
+
type: external ? "external-data-unavailable" : "missing-blob",
|
|
572
|
+
message: external
|
|
573
|
+
? `Artifact "${lookup.id}" uses application-managed external data at "${external.uri}".`
|
|
574
|
+
: `Artifact "${lookup.id}" binary blob was not found.`
|
|
341
575
|
}]
|
|
342
576
|
};
|
|
343
577
|
}
|
|
344
|
-
return verifyArtifactRecordIntegrity(binary.artifact, binary.data);
|
|
578
|
+
return verifyArtifactRecordIntegrity(binary.artifact, binary.data, limits);
|
|
345
579
|
}
|
|
346
|
-
return verifyArtifactRecordIntegrity(artifact);
|
|
580
|
+
return verifyArtifactRecordIntegrity(artifact, undefined, limits);
|
|
347
581
|
};
|
|
348
582
|
const listFilesRecursive = async (directory) => {
|
|
349
583
|
try {
|
|
@@ -365,6 +599,7 @@ const listFilesRecursive = async (directory) => {
|
|
|
365
599
|
}
|
|
366
600
|
};
|
|
367
601
|
export const inspectFileArtifactStore = async (options) => {
|
|
602
|
+
const limits = resolveArtifactServiceLimits(options.limits);
|
|
368
603
|
const artifacts = new Map();
|
|
369
604
|
const issues = [];
|
|
370
605
|
const referencedBlobPaths = new Set();
|
|
@@ -383,7 +618,9 @@ export const inspectFileArtifactStore = async (options) => {
|
|
|
383
618
|
}
|
|
384
619
|
const metadataPath = path.join(options.directory, entry);
|
|
385
620
|
try {
|
|
386
|
-
const
|
|
621
|
+
const stat = await fs.stat(metadataPath);
|
|
622
|
+
assertByteLimit(stat.size, limits.maxRecordBytes, "record");
|
|
623
|
+
const artifact = validateArtifactRecordLimits(normalizeArtifactRecord(JSON.parse(await fs.readFile(metadataPath, "utf8"))), limits);
|
|
387
624
|
const fullBlobPath = resolveFileArtifactBlobPath(options.directory, artifact);
|
|
388
625
|
const key = artifactKey(artifact);
|
|
389
626
|
if (!artifacts.has(key) || entry === fileNameForArtifact(artifact)) {
|
|
@@ -394,12 +631,14 @@ export const inspectFileArtifactStore = async (options) => {
|
|
|
394
631
|
}
|
|
395
632
|
if (artifact.storageMode === "binary") {
|
|
396
633
|
if (!artifact.blobPath) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
634
|
+
if (!externalBlobReference(artifact.metadata)) {
|
|
635
|
+
issues.push({
|
|
636
|
+
type: "missing-blob",
|
|
637
|
+
path: metadataPath,
|
|
638
|
+
artifact,
|
|
639
|
+
message: `Artifact "${artifact.id}" has no blobPath.`
|
|
640
|
+
});
|
|
641
|
+
}
|
|
403
642
|
}
|
|
404
643
|
else {
|
|
405
644
|
try {
|
|
@@ -467,7 +706,10 @@ export const cleanupFileArtifactStore = async (options) => {
|
|
|
467
706
|
export const pruneFileArtifactStore = async (options) => {
|
|
468
707
|
const now = options.now ?? Date.now();
|
|
469
708
|
const dryRun = options.dryRun ?? true;
|
|
470
|
-
const inspection = await inspectFileArtifactStore({
|
|
709
|
+
const inspection = await inspectFileArtifactStore({
|
|
710
|
+
directory: options.directory,
|
|
711
|
+
limits: options.limits
|
|
712
|
+
});
|
|
471
713
|
const sorted = inspection.artifacts.sort((left, right) => right.updatedAt - left.updatedAt || artifactKey(left).localeCompare(artifactKey(right)));
|
|
472
714
|
const keepByCount = new Set(options.keepLast === undefined ? [] : sorted.slice(0, Math.max(0, options.keepLast)).map((artifact) => artifactKey(artifact)));
|
|
473
715
|
const shouldDelete = (artifact) => !keepByCount.has(artifactKey(artifact)) &&
|
|
@@ -498,7 +740,8 @@ export const pruneFileArtifactStore = async (options) => {
|
|
|
498
740
|
deletedBlobPaths
|
|
499
741
|
};
|
|
500
742
|
};
|
|
501
|
-
export const createInMemoryArtifactService = () => {
|
|
743
|
+
export const createInMemoryArtifactService = (options = {}) => {
|
|
744
|
+
const limits = resolveArtifactServiceLimits(options.limits);
|
|
502
745
|
const artifacts = new Map();
|
|
503
746
|
const binaryData = new Map();
|
|
504
747
|
return {
|
|
@@ -510,9 +753,10 @@ export const createInMemoryArtifactService = () => {
|
|
|
510
753
|
sessionId: input.sessionId,
|
|
511
754
|
id
|
|
512
755
|
};
|
|
756
|
+
validateArtifactLookup(lookup);
|
|
513
757
|
const existing = artifacts.get(artifactKey(lookup));
|
|
514
758
|
assertExpectedRevision(existing, input.expectedRevision, "ArtifactRecord");
|
|
515
|
-
const artifact = createArtifact({ ...input, id }, existing);
|
|
759
|
+
const artifact = createArtifact({ ...input, id }, limits, existing);
|
|
516
760
|
artifacts.set(artifactKey(lookup), cloneArtifact(artifact));
|
|
517
761
|
binaryData.delete(artifactKey(lookup));
|
|
518
762
|
return cloneArtifact(artifact);
|
|
@@ -525,7 +769,8 @@ export const createInMemoryArtifactService = () => {
|
|
|
525
769
|
sessionId: input.sessionId,
|
|
526
770
|
id
|
|
527
771
|
};
|
|
528
|
-
|
|
772
|
+
validateArtifactLookup(lookup);
|
|
773
|
+
const bytes = bytesFromBinaryInput(input.data, limits.maxBinaryBytes);
|
|
529
774
|
const sha256 = resolveBinarySha256(bytes, input.sha256);
|
|
530
775
|
const existing = artifacts.get(artifactKey(lookup));
|
|
531
776
|
assertExpectedRevision(existing, input.expectedRevision, "ArtifactRecord");
|
|
@@ -537,16 +782,18 @@ export const createInMemoryArtifactService = () => {
|
|
|
537
782
|
size: bytes.byteLength,
|
|
538
783
|
sha256,
|
|
539
784
|
storageMode: "binary"
|
|
540
|
-
}, existing);
|
|
785
|
+
}, limits, existing, { managedBinary: true });
|
|
541
786
|
artifacts.set(artifactKey(lookup), cloneArtifact(artifact));
|
|
542
787
|
binaryData.set(artifactKey(lookup), new Uint8Array(bytes));
|
|
543
788
|
return cloneArtifact(artifact);
|
|
544
789
|
},
|
|
545
790
|
loadArtifact(input) {
|
|
791
|
+
validateArtifactLookup(input);
|
|
546
792
|
const artifact = artifacts.get(artifactKey(input));
|
|
547
793
|
return artifact ? cloneArtifact(artifact) : undefined;
|
|
548
794
|
},
|
|
549
795
|
loadBinaryArtifact(input) {
|
|
796
|
+
validateArtifactLookup(input);
|
|
550
797
|
const artifact = artifacts.get(artifactKey(input));
|
|
551
798
|
const data = binaryData.get(artifactKey(input));
|
|
552
799
|
if (!artifact || !data) {
|
|
@@ -558,26 +805,32 @@ export const createInMemoryArtifactService = () => {
|
|
|
558
805
|
};
|
|
559
806
|
},
|
|
560
807
|
listArtifacts(input) {
|
|
808
|
+
validateArtifactListInput(input);
|
|
561
809
|
return [...artifacts.values()]
|
|
562
810
|
.filter((artifact) => matchesListInput(artifact, input))
|
|
563
811
|
.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id))
|
|
564
812
|
.map(cloneArtifact);
|
|
565
813
|
},
|
|
566
814
|
deleteArtifact(input) {
|
|
815
|
+
validateArtifactLookup(input);
|
|
567
816
|
artifacts.delete(artifactKey(input));
|
|
568
817
|
binaryData.delete(artifactKey(input));
|
|
569
818
|
}
|
|
570
819
|
};
|
|
571
820
|
};
|
|
572
821
|
export const createFileArtifactService = (options) => {
|
|
822
|
+
const limits = resolveArtifactServiceLimits(options.limits);
|
|
573
823
|
const filePath = (input) => path.join(options.directory, fileNameForArtifact(input));
|
|
574
824
|
const binaryPath = (input) => path.join(options.directory, blobPathForArtifact(input));
|
|
575
825
|
const legacyFilePath = (input) => path.join(options.directory, legacyFileNameForArtifact(input));
|
|
576
826
|
const load = async (input) => {
|
|
827
|
+
validateArtifactLookup(input);
|
|
577
828
|
for (const candidate of [filePath(input), legacyFilePath(input)]) {
|
|
578
829
|
try {
|
|
830
|
+
const stat = await fs.stat(candidate);
|
|
831
|
+
assertByteLimit(stat.size, limits.maxRecordBytes, "record");
|
|
579
832
|
const content = await fs.readFile(candidate, "utf8");
|
|
580
|
-
const artifact = normalizeArtifactRecord(JSON.parse(content));
|
|
833
|
+
const artifact = validateArtifactRecordLimits(normalizeArtifactRecord(JSON.parse(content)), limits);
|
|
581
834
|
if (!matchesArtifactLookup(artifact, input)) {
|
|
582
835
|
continue;
|
|
583
836
|
}
|
|
@@ -594,7 +847,7 @@ export const createFileArtifactService = (options) => {
|
|
|
594
847
|
};
|
|
595
848
|
const save = async (artifact) => {
|
|
596
849
|
await ensurePrivateDirectory(options.directory);
|
|
597
|
-
await writePrivateFile(filePath(artifact), JSON.stringify(cloneArtifact(artifact)
|
|
850
|
+
await writePrivateFile(filePath(artifact), JSON.stringify(cloneArtifact(artifact)));
|
|
598
851
|
try {
|
|
599
852
|
const legacyArtifact = normalizeArtifactRecord(JSON.parse(await fs.readFile(legacyFilePath(artifact), "utf8")));
|
|
600
853
|
if (matchesArtifactLookup(legacyArtifact, artifact)) {
|
|
@@ -624,9 +877,10 @@ export const createFileArtifactService = (options) => {
|
|
|
624
877
|
sessionId: input.sessionId,
|
|
625
878
|
id
|
|
626
879
|
};
|
|
880
|
+
validateArtifactLookup(lookup);
|
|
627
881
|
const existing = await load(lookup);
|
|
628
882
|
assertExpectedRevision(existing, input.expectedRevision, "ArtifactRecord");
|
|
629
|
-
const artifact = createArtifact({ ...input, id }, existing);
|
|
883
|
+
const artifact = createArtifact({ ...input, id }, limits, existing);
|
|
630
884
|
await save(artifact);
|
|
631
885
|
const existingBlob = binaryPath(lookup);
|
|
632
886
|
await fs.unlink(existingBlob).catch((error) => {
|
|
@@ -644,7 +898,8 @@ export const createFileArtifactService = (options) => {
|
|
|
644
898
|
sessionId: input.sessionId,
|
|
645
899
|
id
|
|
646
900
|
};
|
|
647
|
-
|
|
901
|
+
validateArtifactLookup(lookup);
|
|
902
|
+
const bytes = bytesFromBinaryInput(input.data, limits.maxBinaryBytes);
|
|
648
903
|
const sha256 = resolveBinarySha256(bytes, input.sha256);
|
|
649
904
|
const blobPath = blobPathForArtifact(lookup);
|
|
650
905
|
const existing = await load(lookup);
|
|
@@ -657,7 +912,7 @@ export const createFileArtifactService = (options) => {
|
|
|
657
912
|
size: bytes.byteLength,
|
|
658
913
|
sha256,
|
|
659
914
|
storageMode: "binary"
|
|
660
|
-
}, existing, { blobPath });
|
|
915
|
+
}, limits, existing, { blobPath, managedBinary: true });
|
|
661
916
|
await ensurePrivateDirectory(path.dirname(binaryPath(lookup)));
|
|
662
917
|
await writePrivateFile(binaryPath(lookup), bytes);
|
|
663
918
|
await save(artifact);
|
|
@@ -672,7 +927,11 @@ export const createFileArtifactService = (options) => {
|
|
|
672
927
|
return undefined;
|
|
673
928
|
}
|
|
674
929
|
try {
|
|
675
|
-
const
|
|
930
|
+
const resolvedBlobPath = resolveFileArtifactBlobPath(options.directory, artifact);
|
|
931
|
+
const stat = await fs.stat(resolvedBlobPath);
|
|
932
|
+
assertByteLimit(stat.size, limits.maxBinaryBytes, "binary data");
|
|
933
|
+
const data = await fs.readFile(resolvedBlobPath);
|
|
934
|
+
assertByteLimit(data.byteLength, limits.maxBinaryBytes, "binary data");
|
|
676
935
|
return {
|
|
677
936
|
artifact,
|
|
678
937
|
data: new Uint8Array(data)
|
|
@@ -686,6 +945,7 @@ export const createFileArtifactService = (options) => {
|
|
|
686
945
|
}
|
|
687
946
|
},
|
|
688
947
|
async listArtifacts(input) {
|
|
948
|
+
validateArtifactListInput(input);
|
|
689
949
|
let entries;
|
|
690
950
|
try {
|
|
691
951
|
entries = await fs.readdir(options.directory);
|
|
@@ -701,8 +961,11 @@ export const createFileArtifactService = (options) => {
|
|
|
701
961
|
if (!entry.endsWith(".json")) {
|
|
702
962
|
continue;
|
|
703
963
|
}
|
|
704
|
-
const
|
|
705
|
-
const
|
|
964
|
+
const metadataPath = path.join(options.directory, entry);
|
|
965
|
+
const stat = await fs.stat(metadataPath);
|
|
966
|
+
assertByteLimit(stat.size, limits.maxRecordBytes, "record");
|
|
967
|
+
const content = await fs.readFile(metadataPath, "utf8");
|
|
968
|
+
const artifact = validateArtifactRecordLimits(normalizeArtifactRecord(JSON.parse(content)), limits);
|
|
706
969
|
resolveFileArtifactBlobPath(options.directory, artifact);
|
|
707
970
|
if (matchesListInput(artifact, input)) {
|
|
708
971
|
const key = artifactKey(artifact);
|
|
@@ -714,6 +977,7 @@ export const createFileArtifactService = (options) => {
|
|
|
714
977
|
return [...artifacts.values()].sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id));
|
|
715
978
|
},
|
|
716
979
|
async deleteArtifact(input) {
|
|
980
|
+
validateArtifactLookup(input);
|
|
717
981
|
const artifact = await load(input);
|
|
718
982
|
await fs.unlink(filePath(input)).catch((error) => {
|
|
719
983
|
if (error.code !== "ENOENT") {
|
|
@@ -745,6 +1009,7 @@ export const createFileArtifactService = (options) => {
|
|
|
745
1009
|
};
|
|
746
1010
|
};
|
|
747
1011
|
export const createSqliteArtifactService = (options) => {
|
|
1012
|
+
const limits = resolveArtifactServiceLimits(options.limits);
|
|
748
1013
|
const tableName = validateIdentifier(options.tableName ?? "zhivex_artifacts", "tableName");
|
|
749
1014
|
options.db.exec(`
|
|
750
1015
|
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
@@ -795,6 +1060,23 @@ export const createSqliteArtifactService = (options) => {
|
|
|
795
1060
|
agent_run_id = excluded.agent_run_id,
|
|
796
1061
|
artifact_json = excluded.artifact_json,
|
|
797
1062
|
updated_at_ms = excluded.updated_at_ms
|
|
1063
|
+
`);
|
|
1064
|
+
const insertCasStatement = prepareSqliteStatement(options.db, `
|
|
1065
|
+
INSERT INTO ${tableName} (
|
|
1066
|
+
artifact_key,
|
|
1067
|
+
app_name,
|
|
1068
|
+
user_id,
|
|
1069
|
+
session_id,
|
|
1070
|
+
artifact_id,
|
|
1071
|
+
workflow_run_id,
|
|
1072
|
+
workflow_step_id,
|
|
1073
|
+
agent_run_id,
|
|
1074
|
+
artifact_json,
|
|
1075
|
+
created_at_ms,
|
|
1076
|
+
updated_at_ms
|
|
1077
|
+
)
|
|
1078
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1079
|
+
ON CONFLICT(artifact_key) DO NOTHING
|
|
798
1080
|
`);
|
|
799
1081
|
const updateCasStatement = prepareSqliteStatement(options.db, `
|
|
800
1082
|
UPDATE ${tableName}
|
|
@@ -812,9 +1094,10 @@ export const createSqliteArtifactService = (options) => {
|
|
|
812
1094
|
`);
|
|
813
1095
|
const deleteStatement = prepareSqliteStatement(options.db, `DELETE FROM ${tableName} WHERE artifact_key = ?`);
|
|
814
1096
|
const load = (input) => {
|
|
1097
|
+
validateArtifactLookup(input);
|
|
815
1098
|
for (const key of [artifactKey(input), legacyArtifactKey(input)]) {
|
|
816
1099
|
const row = loadStatement.get([key]);
|
|
817
|
-
const artifact = parseArtifactJson(getRecordField(row, ["artifact_json", "artifactJson"]));
|
|
1100
|
+
const artifact = parseArtifactJson(getRecordField(row, ["artifact_json", "artifactJson"]), limits);
|
|
818
1101
|
if (artifact && matchesArtifactLookup(artifact, input)) {
|
|
819
1102
|
return artifact;
|
|
820
1103
|
}
|
|
@@ -822,8 +1105,26 @@ export const createSqliteArtifactService = (options) => {
|
|
|
822
1105
|
return undefined;
|
|
823
1106
|
};
|
|
824
1107
|
const save = (artifact, options) => {
|
|
825
|
-
if (options?.expectedRevision
|
|
826
|
-
const
|
|
1108
|
+
if (options?.expectedRevision === 0 && !options.existing) {
|
|
1109
|
+
const result = insertCasStatement.run([
|
|
1110
|
+
artifactKey(lookupFromArtifact(artifact)),
|
|
1111
|
+
artifact.appName,
|
|
1112
|
+
artifact.userId,
|
|
1113
|
+
artifact.sessionId,
|
|
1114
|
+
artifact.id,
|
|
1115
|
+
artifact.workflowRunId ?? null,
|
|
1116
|
+
artifact.workflowStepId ?? null,
|
|
1117
|
+
artifact.agentRunId ?? null,
|
|
1118
|
+
JSON.stringify(artifact),
|
|
1119
|
+
artifact.createdAt,
|
|
1120
|
+
artifact.updatedAt
|
|
1121
|
+
]);
|
|
1122
|
+
if (sqliteMutationCount(result) !== 1) {
|
|
1123
|
+
throw new ConflictError("ArtifactRecord revision conflict.");
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
else if (options?.expectedRevision !== undefined && options.existing) {
|
|
1127
|
+
const canonicalExisting = parseArtifactJson(getRecordField(loadStatement.get([artifactKey(artifact)]), ["artifact_json", "artifactJson"]), limits);
|
|
827
1128
|
if (canonicalExisting && matchesArtifactLookup(canonicalExisting, artifact)) {
|
|
828
1129
|
const result = updateCasStatement.run([
|
|
829
1130
|
artifact.appName,
|
|
@@ -838,12 +1139,12 @@ export const createSqliteArtifactService = (options) => {
|
|
|
838
1139
|
artifactKey(lookupFromArtifact(artifact)),
|
|
839
1140
|
options.existing.updatedAt
|
|
840
1141
|
]);
|
|
841
|
-
if (sqliteMutationCount(result)
|
|
1142
|
+
if (sqliteMutationCount(result) !== 1) {
|
|
842
1143
|
throw new ConflictError("ArtifactRecord revision conflict.");
|
|
843
1144
|
}
|
|
844
1145
|
}
|
|
845
1146
|
else {
|
|
846
|
-
const legacy = parseArtifactJson(getRecordField(loadStatement.get([legacyArtifactKey(artifact)]), ["artifact_json", "artifactJson"]));
|
|
1147
|
+
const legacy = parseArtifactJson(getRecordField(loadStatement.get([legacyArtifactKey(artifact)]), ["artifact_json", "artifactJson"]), limits);
|
|
847
1148
|
if (!legacy || !matchesArtifactLookup(legacy, artifact) || legacy.updatedAt !== options.existing.updatedAt) {
|
|
848
1149
|
throw new ConflictError("ArtifactRecord revision conflict.");
|
|
849
1150
|
}
|
|
@@ -877,7 +1178,7 @@ export const createSqliteArtifactService = (options) => {
|
|
|
877
1178
|
artifact.updatedAt
|
|
878
1179
|
]);
|
|
879
1180
|
}
|
|
880
|
-
const legacy = parseArtifactJson(getRecordField(loadStatement.get([legacyArtifactKey(artifact)]), ["artifact_json", "artifactJson"]));
|
|
1181
|
+
const legacy = parseArtifactJson(getRecordField(loadStatement.get([legacyArtifactKey(artifact)]), ["artifact_json", "artifactJson"]), limits);
|
|
881
1182
|
if (legacy && matchesArtifactLookup(legacy, artifact)) {
|
|
882
1183
|
deleteStatement.run([legacyArtifactKey(artifact)]);
|
|
883
1184
|
}
|
|
@@ -886,6 +1187,7 @@ export const createSqliteArtifactService = (options) => {
|
|
|
886
1187
|
return {
|
|
887
1188
|
saveArtifact(input) {
|
|
888
1189
|
const id = input.id ?? randomId("art");
|
|
1190
|
+
validateArtifactLookup({ appName: input.appName, userId: input.userId, sessionId: input.sessionId, id });
|
|
889
1191
|
const existing = load({
|
|
890
1192
|
appName: input.appName,
|
|
891
1193
|
userId: input.userId,
|
|
@@ -893,14 +1195,16 @@ export const createSqliteArtifactService = (options) => {
|
|
|
893
1195
|
id
|
|
894
1196
|
});
|
|
895
1197
|
assertExpectedRevision(existing, input.expectedRevision, "ArtifactRecord");
|
|
896
|
-
return save(createArtifact({ ...input, id }, existing), {
|
|
1198
|
+
return save(createArtifact({ ...input, id }, limits, existing), {
|
|
897
1199
|
existing,
|
|
898
1200
|
expectedRevision: input.expectedRevision
|
|
899
1201
|
});
|
|
900
1202
|
},
|
|
901
1203
|
saveBinaryArtifact(input) {
|
|
902
1204
|
const id = input.id ?? randomId("art");
|
|
903
|
-
|
|
1205
|
+
validateArtifactLookup({ appName: input.appName, userId: input.userId, sessionId: input.sessionId, id });
|
|
1206
|
+
const bytes = bytesFromBinaryInput(input.data, limits.maxBinaryBytes);
|
|
1207
|
+
const sha256 = resolveBinarySha256(bytes, input.sha256);
|
|
904
1208
|
const existing = load({
|
|
905
1209
|
appName: input.appName,
|
|
906
1210
|
userId: input.userId,
|
|
@@ -914,9 +1218,9 @@ export const createSqliteArtifactService = (options) => {
|
|
|
914
1218
|
data: Buffer.from(bytes).toString("base64"),
|
|
915
1219
|
encoding: "base64",
|
|
916
1220
|
size: bytes.byteLength,
|
|
917
|
-
sha256
|
|
1221
|
+
sha256,
|
|
918
1222
|
storageMode: "json"
|
|
919
|
-
}, existing), {
|
|
1223
|
+
}, limits, existing), {
|
|
920
1224
|
existing,
|
|
921
1225
|
expectedRevision: input.expectedRevision
|
|
922
1226
|
});
|
|
@@ -931,10 +1235,11 @@ export const createSqliteArtifactService = (options) => {
|
|
|
931
1235
|
}
|
|
932
1236
|
return {
|
|
933
1237
|
artifact,
|
|
934
|
-
data:
|
|
1238
|
+
data: base64Bytes(artifact.data, limits.maxBase64Bytes)
|
|
935
1239
|
};
|
|
936
1240
|
},
|
|
937
1241
|
listArtifacts(input) {
|
|
1242
|
+
validateArtifactListInput(input);
|
|
938
1243
|
const params = [
|
|
939
1244
|
input.appName,
|
|
940
1245
|
input.userId,
|
|
@@ -949,7 +1254,7 @@ export const createSqliteArtifactService = (options) => {
|
|
|
949
1254
|
const rows = listStatement.all?.(params) ?? [];
|
|
950
1255
|
const artifacts = new Map();
|
|
951
1256
|
for (const row of rows) {
|
|
952
|
-
const artifact = parseArtifactJson(getRecordField(row, ["artifact_json", "artifactJson"]));
|
|
1257
|
+
const artifact = parseArtifactJson(getRecordField(row, ["artifact_json", "artifactJson"]), limits);
|
|
953
1258
|
if (artifact) {
|
|
954
1259
|
artifacts.set(artifactKey(artifact), artifact);
|
|
955
1260
|
}
|
|
@@ -957,8 +1262,9 @@ export const createSqliteArtifactService = (options) => {
|
|
|
957
1262
|
return [...artifacts.values()];
|
|
958
1263
|
},
|
|
959
1264
|
deleteArtifact(input) {
|
|
1265
|
+
validateArtifactLookup(input);
|
|
960
1266
|
deleteStatement.run([artifactKey(input)]);
|
|
961
|
-
const legacy = parseArtifactJson(getRecordField(loadStatement.get([legacyArtifactKey(input)]), ["artifact_json", "artifactJson"]));
|
|
1267
|
+
const legacy = parseArtifactJson(getRecordField(loadStatement.get([legacyArtifactKey(input)]), ["artifact_json", "artifactJson"]), limits);
|
|
962
1268
|
if (legacy && matchesArtifactLookup(legacy, input)) {
|
|
963
1269
|
deleteStatement.run([legacyArtifactKey(input)]);
|
|
964
1270
|
}
|
|
@@ -967,6 +1273,7 @@ export const createSqliteArtifactService = (options) => {
|
|
|
967
1273
|
};
|
|
968
1274
|
export const createPostgresArtifactService = (options) => {
|
|
969
1275
|
assertPostgresClient(options.client);
|
|
1276
|
+
const limits = resolveArtifactServiceLimits(options.limits);
|
|
970
1277
|
const tableName = validateIdentifier(options.tableName ?? "zhivex_artifacts", "tableName");
|
|
971
1278
|
const createSql = `
|
|
972
1279
|
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
@@ -984,10 +1291,11 @@ export const createPostgresArtifactService = (options) => {
|
|
|
984
1291
|
)
|
|
985
1292
|
`;
|
|
986
1293
|
const load = async (input) => {
|
|
1294
|
+
validateArtifactLookup(input);
|
|
987
1295
|
await ensurePostgresTable(options.client, tableName, createSql);
|
|
988
1296
|
for (const key of [artifactKey(input), legacyArtifactKey(input)]) {
|
|
989
1297
|
const result = await options.client.query(`SELECT artifact_json FROM ${tableName} WHERE artifact_key = $1`, [key]);
|
|
990
|
-
const artifact = parseArtifactJson(getRecordField(result.rows[0], ["artifact_json", "artifactJson"]));
|
|
1298
|
+
const artifact = parseArtifactJson(getRecordField(result.rows[0], ["artifact_json", "artifactJson"]), limits);
|
|
991
1299
|
if (artifact && matchesArtifactLookup(artifact, input)) {
|
|
992
1300
|
return artifact;
|
|
993
1301
|
}
|
|
@@ -996,7 +1304,40 @@ export const createPostgresArtifactService = (options) => {
|
|
|
996
1304
|
};
|
|
997
1305
|
const save = async (artifact, saveOptions) => {
|
|
998
1306
|
await ensurePostgresTable(options.client, tableName, createSql);
|
|
999
|
-
if (saveOptions?.expectedRevision
|
|
1307
|
+
if (saveOptions?.expectedRevision === 0 && !saveOptions.existing) {
|
|
1308
|
+
const result = await options.client.query(`INSERT INTO ${tableName} (
|
|
1309
|
+
artifact_key,
|
|
1310
|
+
app_name,
|
|
1311
|
+
user_id,
|
|
1312
|
+
session_id,
|
|
1313
|
+
artifact_id,
|
|
1314
|
+
workflow_run_id,
|
|
1315
|
+
workflow_step_id,
|
|
1316
|
+
agent_run_id,
|
|
1317
|
+
artifact_json,
|
|
1318
|
+
created_at_ms,
|
|
1319
|
+
updated_at_ms
|
|
1320
|
+
)
|
|
1321
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11)
|
|
1322
|
+
ON CONFLICT(artifact_key) DO NOTHING
|
|
1323
|
+
RETURNING artifact_json`, [
|
|
1324
|
+
artifactKey(lookupFromArtifact(artifact)),
|
|
1325
|
+
artifact.appName,
|
|
1326
|
+
artifact.userId,
|
|
1327
|
+
artifact.sessionId,
|
|
1328
|
+
artifact.id,
|
|
1329
|
+
artifact.workflowRunId ?? null,
|
|
1330
|
+
artifact.workflowStepId ?? null,
|
|
1331
|
+
artifact.agentRunId ?? null,
|
|
1332
|
+
JSON.stringify(artifact),
|
|
1333
|
+
artifact.createdAt,
|
|
1334
|
+
artifact.updatedAt
|
|
1335
|
+
]);
|
|
1336
|
+
if (result.rows.length === 0) {
|
|
1337
|
+
throw new ConflictError("ArtifactRecord revision conflict.");
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
else if (saveOptions?.expectedRevision !== undefined && saveOptions.existing) {
|
|
1000
1341
|
const result = await options.client.query(`UPDATE ${tableName}
|
|
1001
1342
|
SET app_name = $2,
|
|
1002
1343
|
user_id = $3,
|
|
@@ -1094,7 +1435,7 @@ export const createPostgresArtifactService = (options) => {
|
|
|
1094
1435
|
]);
|
|
1095
1436
|
}
|
|
1096
1437
|
const legacyResult = await options.client.query(`SELECT artifact_json FROM ${tableName} WHERE artifact_key = $1`, [legacyArtifactKey(artifact)]);
|
|
1097
|
-
const legacy = parseArtifactJson(getRecordField(legacyResult.rows[0], ["artifact_json", "artifactJson"]));
|
|
1438
|
+
const legacy = parseArtifactJson(getRecordField(legacyResult.rows[0], ["artifact_json", "artifactJson"]), limits);
|
|
1098
1439
|
if (legacy && matchesArtifactLookup(legacy, artifact)) {
|
|
1099
1440
|
await options.client.query(`DELETE FROM ${tableName} WHERE artifact_key = $1`, [legacyArtifactKey(artifact)]);
|
|
1100
1441
|
}
|
|
@@ -1103,6 +1444,7 @@ export const createPostgresArtifactService = (options) => {
|
|
|
1103
1444
|
return {
|
|
1104
1445
|
async saveArtifact(input) {
|
|
1105
1446
|
const id = input.id ?? randomId("art");
|
|
1447
|
+
validateArtifactLookup({ appName: input.appName, userId: input.userId, sessionId: input.sessionId, id });
|
|
1106
1448
|
const existing = await load({
|
|
1107
1449
|
appName: input.appName,
|
|
1108
1450
|
userId: input.userId,
|
|
@@ -1110,14 +1452,16 @@ export const createPostgresArtifactService = (options) => {
|
|
|
1110
1452
|
id
|
|
1111
1453
|
});
|
|
1112
1454
|
assertExpectedRevision(existing, input.expectedRevision, "ArtifactRecord");
|
|
1113
|
-
return save(createArtifact({ ...input, id }, existing), {
|
|
1455
|
+
return save(createArtifact({ ...input, id }, limits, existing), {
|
|
1114
1456
|
existing,
|
|
1115
1457
|
expectedRevision: input.expectedRevision
|
|
1116
1458
|
});
|
|
1117
1459
|
},
|
|
1118
1460
|
async saveBinaryArtifact(input) {
|
|
1119
1461
|
const id = input.id ?? randomId("art");
|
|
1120
|
-
|
|
1462
|
+
validateArtifactLookup({ appName: input.appName, userId: input.userId, sessionId: input.sessionId, id });
|
|
1463
|
+
const bytes = bytesFromBinaryInput(input.data, limits.maxBinaryBytes);
|
|
1464
|
+
const sha256 = resolveBinarySha256(bytes, input.sha256);
|
|
1121
1465
|
const existing = await load({
|
|
1122
1466
|
appName: input.appName,
|
|
1123
1467
|
userId: input.userId,
|
|
@@ -1131,9 +1475,9 @@ export const createPostgresArtifactService = (options) => {
|
|
|
1131
1475
|
data: Buffer.from(bytes).toString("base64"),
|
|
1132
1476
|
encoding: "base64",
|
|
1133
1477
|
size: bytes.byteLength,
|
|
1134
|
-
sha256
|
|
1478
|
+
sha256,
|
|
1135
1479
|
storageMode: "json"
|
|
1136
|
-
}, existing), {
|
|
1480
|
+
}, limits, existing), {
|
|
1137
1481
|
existing,
|
|
1138
1482
|
expectedRevision: input.expectedRevision
|
|
1139
1483
|
});
|
|
@@ -1148,10 +1492,11 @@ export const createPostgresArtifactService = (options) => {
|
|
|
1148
1492
|
}
|
|
1149
1493
|
return {
|
|
1150
1494
|
artifact,
|
|
1151
|
-
data:
|
|
1495
|
+
data: base64Bytes(artifact.data, limits.maxBase64Bytes)
|
|
1152
1496
|
};
|
|
1153
1497
|
},
|
|
1154
1498
|
async listArtifacts(input) {
|
|
1499
|
+
validateArtifactListInput(input);
|
|
1155
1500
|
await ensurePostgresTable(options.client, tableName, createSql);
|
|
1156
1501
|
const result = await options.client.query(`SELECT artifact_json FROM ${tableName}
|
|
1157
1502
|
WHERE app_name = $1
|
|
@@ -1170,7 +1515,7 @@ export const createPostgresArtifactService = (options) => {
|
|
|
1170
1515
|
]);
|
|
1171
1516
|
const artifacts = new Map();
|
|
1172
1517
|
for (const row of result.rows) {
|
|
1173
|
-
const artifact = parseArtifactJson(getRecordField(row, ["artifact_json", "artifactJson"]));
|
|
1518
|
+
const artifact = parseArtifactJson(getRecordField(row, ["artifact_json", "artifactJson"]), limits);
|
|
1174
1519
|
if (artifact) {
|
|
1175
1520
|
artifacts.set(artifactKey(artifact), artifact);
|
|
1176
1521
|
}
|
|
@@ -1178,10 +1523,11 @@ export const createPostgresArtifactService = (options) => {
|
|
|
1178
1523
|
return [...artifacts.values()];
|
|
1179
1524
|
},
|
|
1180
1525
|
async deleteArtifact(input) {
|
|
1526
|
+
validateArtifactLookup(input);
|
|
1181
1527
|
await ensurePostgresTable(options.client, tableName, createSql);
|
|
1182
1528
|
await options.client.query(`DELETE FROM ${tableName} WHERE artifact_key = $1`, [artifactKey(input)]);
|
|
1183
1529
|
const legacyResult = await options.client.query(`SELECT artifact_json FROM ${tableName} WHERE artifact_key = $1`, [legacyArtifactKey(input)]);
|
|
1184
|
-
const legacy = parseArtifactJson(getRecordField(legacyResult.rows[0], ["artifact_json", "artifactJson"]));
|
|
1530
|
+
const legacy = parseArtifactJson(getRecordField(legacyResult.rows[0], ["artifact_json", "artifactJson"]), limits);
|
|
1185
1531
|
if (legacy && matchesArtifactLookup(legacy, input)) {
|
|
1186
1532
|
await options.client.query(`DELETE FROM ${tableName} WHERE artifact_key = $1`, [legacyArtifactKey(input)]);
|
|
1187
1533
|
}
|