@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.
Files changed (119) hide show
  1. package/README.md +39 -0
  2. package/dist/agent-handoff-contracts.d.ts +12 -0
  3. package/dist/agent-handoff-contracts.d.ts.map +1 -0
  4. package/dist/agent-handoff-contracts.js +18 -0
  5. package/dist/agent-handoff-contracts.js.map +1 -0
  6. package/dist/agent-handoff.d.ts +2 -11
  7. package/dist/agent-handoff.d.ts.map +1 -1
  8. package/dist/agent-handoff.js +1 -18
  9. package/dist/agent-handoff.js.map +1 -1
  10. package/dist/agent-trace.d.ts +1 -5
  11. package/dist/agent-trace.d.ts.map +1 -1
  12. package/dist/agent-trace.js.map +1 -1
  13. package/dist/agent.d.ts +1 -0
  14. package/dist/agent.d.ts.map +1 -1
  15. package/dist/agent.js +358 -215
  16. package/dist/agent.js.map +1 -1
  17. package/dist/api-stability.d.ts.map +1 -1
  18. package/dist/api-stability.js +36 -25
  19. package/dist/api-stability.js.map +1 -1
  20. package/dist/artifact.d.ts +32 -9
  21. package/dist/artifact.d.ts.map +1 -1
  22. package/dist/artifact.js +428 -82
  23. package/dist/artifact.js.map +1 -1
  24. package/dist/catalog.d.ts +42 -3
  25. package/dist/catalog.d.ts.map +1 -1
  26. package/dist/catalog.js +269 -7
  27. package/dist/catalog.js.map +1 -1
  28. package/dist/contracts.d.ts +8 -0
  29. package/dist/contracts.d.ts.map +1 -0
  30. package/dist/contracts.js +2 -0
  31. package/dist/contracts.js.map +1 -0
  32. package/dist/generate-text.d.ts.map +1 -1
  33. package/dist/generate-text.js +6 -3
  34. package/dist/generate-text.js.map +1 -1
  35. package/dist/google.d.ts +1 -60
  36. package/dist/google.d.ts.map +1 -1
  37. package/dist/google.js +0 -198
  38. package/dist/google.js.map +1 -1
  39. package/dist/index.d.ts +13 -7
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +7 -4
  42. package/dist/index.js.map +1 -1
  43. package/dist/live-agent.js +1 -1
  44. package/dist/live-agent.js.map +1 -1
  45. package/dist/middleware.d.ts +16 -0
  46. package/dist/middleware.d.ts.map +1 -1
  47. package/dist/middleware.js +476 -110
  48. package/dist/middleware.js.map +1 -1
  49. package/dist/model-capabilities.d.ts +66 -0
  50. package/dist/model-capabilities.d.ts.map +1 -0
  51. package/dist/model-capabilities.js +136 -0
  52. package/dist/model-capabilities.js.map +1 -0
  53. package/dist/node.d.ts +8 -0
  54. package/dist/node.d.ts.map +1 -0
  55. package/dist/node.js +8 -0
  56. package/dist/node.js.map +1 -0
  57. package/dist/observability.d.ts +57 -10
  58. package/dist/observability.d.ts.map +1 -1
  59. package/dist/observability.js +535 -114
  60. package/dist/observability.js.map +1 -1
  61. package/dist/pricing.d.ts +7 -0
  62. package/dist/pricing.d.ts.map +1 -0
  63. package/dist/pricing.js +2 -0
  64. package/dist/pricing.js.map +1 -0
  65. package/dist/provider-resources.d.ts +61 -0
  66. package/dist/provider-resources.d.ts.map +1 -0
  67. package/dist/provider-resources.js +199 -0
  68. package/dist/provider-resources.js.map +1 -0
  69. package/dist/runner.js +1 -1
  70. package/dist/runner.js.map +1 -1
  71. package/dist/runtime-entry.d.ts +15 -0
  72. package/dist/runtime-entry.d.ts.map +1 -0
  73. package/dist/runtime-entry.js +14 -0
  74. package/dist/runtime-entry.js.map +1 -0
  75. package/dist/runtime.d.ts +9 -1
  76. package/dist/runtime.d.ts.map +1 -1
  77. package/dist/runtime.js +35 -8
  78. package/dist/runtime.js.map +1 -1
  79. package/dist/secure-id-node.d.ts +2 -0
  80. package/dist/secure-id-node.d.ts.map +1 -0
  81. package/dist/secure-id-node.js +3 -0
  82. package/dist/secure-id-node.js.map +1 -0
  83. package/dist/secure-id.d.ts.map +1 -1
  84. package/dist/secure-id.js +6 -2
  85. package/dist/secure-id.js.map +1 -1
  86. package/dist/testing.d.ts +4 -0
  87. package/dist/testing.d.ts.map +1 -0
  88. package/dist/testing.js +4 -0
  89. package/dist/testing.js.map +1 -0
  90. package/dist/types.d.ts +74 -6
  91. package/dist/types.d.ts.map +1 -1
  92. package/dist/ui-entry.d.ts +6 -0
  93. package/dist/ui-entry.d.ts.map +1 -0
  94. package/dist/ui-entry.js +5 -0
  95. package/dist/ui-entry.js.map +1 -0
  96. package/dist/ui.js +1 -1
  97. package/dist/ui.js.map +1 -1
  98. package/dist/workflow-observability.d.ts +13 -0
  99. package/dist/workflow-observability.d.ts.map +1 -0
  100. package/dist/workflow-observability.js +186 -0
  101. package/dist/workflow-observability.js.map +1 -0
  102. package/dist/workflow-state-contracts.d.ts +76 -0
  103. package/dist/workflow-state-contracts.d.ts.map +1 -0
  104. package/dist/workflow-state-contracts.js +49 -0
  105. package/dist/workflow-state-contracts.js.map +1 -0
  106. package/dist/workflow-state-service.d.ts +3 -34
  107. package/dist/workflow-state-service.d.ts.map +1 -1
  108. package/dist/workflow-state-service.js +2 -2
  109. package/dist/workflow-state-service.js.map +1 -1
  110. package/dist/workflow.d.ts +53 -42
  111. package/dist/workflow.d.ts.map +1 -1
  112. package/dist/workflow.js +258 -125
  113. package/dist/workflow.js.map +1 -1
  114. package/dist/workflows-entry.d.ts +9 -0
  115. package/dist/workflows-entry.d.ts.map +1 -0
  116. package/dist/workflows-entry.js +5 -0
  117. package/dist/workflows-entry.js.map +1 -0
  118. package/package.json +42 -1
  119. 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 "./secure-id.js";
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 && artifact.schemaVersion > ARTIFACT_SCHEMA_VERSION) {
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 (typeof artifact.id !== "string" ||
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 bytesFromBinaryInput = (data) => {
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 base64Bytes = (value) => {
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
- return new Uint8Array(Buffer.from(normalized, "base64"));
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
- return normalizeArtifactRecord(JSON.parse(value));
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
- const integrity = enrichArtifactMetadata(input);
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
- return {
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 buffer = typeof data === "string"
250
- ? Buffer.from(data, "utf8")
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
- return {
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 = new Uint8Array(Buffer.from(artifact.data, "base64"));
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: `Artifact "${lookup.id}" binary blob was not found.`
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 artifact = normalizeArtifactRecord(JSON.parse(await fs.readFile(metadataPath, "utf8")));
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
- issues.push({
398
- type: "missing-blob",
399
- path: metadataPath,
400
- artifact,
401
- message: `Artifact "${artifact.id}" has no blobPath.`
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({ directory: options.directory });
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
- const bytes = bytesFromBinaryInput(input.data);
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), null, 2));
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
- const bytes = bytesFromBinaryInput(input.data);
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 data = await fs.readFile(resolveFileArtifactBlobPath(options.directory, artifact));
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 content = await fs.readFile(path.join(options.directory, entry), "utf8");
705
- const artifact = normalizeArtifactRecord(JSON.parse(content));
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 !== undefined && options.existing) {
826
- const canonicalExisting = parseArtifactJson(getRecordField(loadStatement.get([artifactKey(artifact)]), ["artifact_json", "artifactJson"]));
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) === 0) {
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
- const bytes = bytesFromBinaryInput(input.data);
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: input.sha256 ?? sha256Digest(bytes),
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: new Uint8Array(Buffer.from(artifact.data, "base64"))
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 !== undefined && saveOptions.existing) {
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
- const bytes = bytesFromBinaryInput(input.data);
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: input.sha256 ?? sha256Digest(bytes),
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: new Uint8Array(Buffer.from(artifact.data, "base64"))
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
  }