@calmlens/sdk 0.12.0 → 0.14.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 (37) hide show
  1. package/README.md +54 -37
  2. package/cjs/CalmLensConnection-Bth5fJyp.js +234 -0
  3. package/cjs/CalmLensUtils-DNdmYeX6.js +699 -0
  4. package/cjs/{CalmLensError-CKwMKV0S.js → CalmLensVersion-Bs0DreBV.js} +114 -1
  5. package/cjs/{CalmLensWebhooks-BTPxKvcD.js → CalmLensWebhooks-6thkvr_F.js} +9 -9
  6. package/cjs/WebhookPayload-vy8HNMaK.d.ts +15834 -0
  7. package/cjs/app.d.ts +3 -3
  8. package/cjs/app.js +10 -10
  9. package/cjs/{index-BgmWj3KL.d.ts → index-Dz340UQ9.d.ts} +29 -1
  10. package/cjs/index.d.ts +3 -3
  11. package/cjs/index.js +4 -5
  12. package/cjs/server.d.ts +16 -7
  13. package/cjs/server.js +35 -21
  14. package/cjs/testing.d.ts +21 -42
  15. package/cjs/testing.js +181 -94
  16. package/esm/CalmLensConnection-DrCOL9h9.js +193 -0
  17. package/esm/{CalmLensUtils-Cx32j2zt.js → CalmLensUtils-BQo79TA1.js} +316 -2
  18. package/esm/{CalmLensError-CihGG26K.js → CalmLensVersion-DPnL_Ebv.js} +91 -2
  19. package/esm/{CalmLensWebhooks-DShmAal4.js → CalmLensWebhooks-CWlD-f_P.js} +2 -2
  20. package/esm/WebhookPayload-vy8HNMaK.d.ts +15834 -0
  21. package/esm/app.d.ts +3 -3
  22. package/esm/app.js +4 -4
  23. package/esm/{index-BgmWj3KL.d.ts → index-Dz340UQ9.d.ts} +29 -1
  24. package/esm/index.d.ts +3 -3
  25. package/esm/index.js +1 -3
  26. package/esm/server.d.ts +16 -7
  27. package/esm/server.js +24 -10
  28. package/esm/testing.d.ts +21 -42
  29. package/esm/testing.js +152 -65
  30. package/package.json +1 -1
  31. package/cjs/CalmLensConnection-CYtBP-vu.js +0 -516
  32. package/cjs/CalmLensUtils-Dg3s4loH.js +0 -367
  33. package/cjs/WebhookPayload-Cgb32aSH.d.ts +0 -10187
  34. package/cjs/src-Dsc8Pn6v.js +0 -45
  35. package/esm/CalmLensConnection-wLNy0OIP.js +0 -475
  36. package/esm/WebhookPayload-Cgb32aSH.d.ts +0 -10187
  37. package/esm/src-CJhrILvM.js +0 -22
package/cjs/testing.js CHANGED
@@ -21,9 +21,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- const require_CalmLensError = require("./CalmLensError-CKwMKV0S.js");
25
- const require_CalmLensWebhooks = require("./CalmLensWebhooks-BTPxKvcD.js");
26
- const require_CalmLensUtils = require("./CalmLensUtils-Dg3s4loH.js");
24
+ const require_CalmLensVersion = require("./CalmLensVersion-Bs0DreBV.js");
25
+ const require_CalmLensWebhooks = require("./CalmLensWebhooks-6thkvr_F.js");
26
+ const require_CalmLensUtils = require("./CalmLensUtils-DNdmYeX6.js");
27
27
  let zod = require("zod");
28
28
  zod = __toESM(zod);
29
29
  require("zod/v4");
@@ -60,58 +60,63 @@ const MODERATION_CATEGORY_DEFINITIONS = {
60
60
  docsHref: "/docs/guides/moderation-categories#hate-and-identity-abuse"
61
61
  },
62
62
  "criminal-activity": {
63
- summary: "Planning, assistance, instructions, encouragement or facilitation of a concrete criminal or seriously dangerous act.",
64
- definition: "covers planning, requests for assistance, instructions, encouragement or facilitation of a concrete criminal or seriously dangerous act. A request can qualify even when no act occurs and a later reply refuses it. Dangerous activity requires a concrete serious risk of physical harm.",
65
- exclusions: "Neutral reporting, prevention, lawful safety advice, fiction without criminal assistance, and vague dangerous references do not qualify. Masks, computers, payment cards, detention or ordinary tools do not establish criminal facilitation by themselves. A clear visual demonstration of how to carry out a crime can qualify.",
63
+ summary: "Planning, assistance, instructions, encouragement or facilitation of a concrete criminal or seriously dangerous act, and illegal drugs or contraband.",
64
+ definition: "covers planning, requests for assistance, instructions, encouragement or facilitation of a concrete criminal or seriously dangerous act. A request can qualify even when no act occurs and a later reply refuses it. Dangerous activity requires a concrete serious risk of physical harm. It also covers illegal drug use, dealing, production or trafficking, and contraband such as stolen goods, counterfeit money or documents and smuggled or illegally traded weapons.",
65
+ exclusions: "Neutral reporting, prevention, lawful safety advice, fiction without criminal assistance, and vague dangerous references do not qualify. Medical, pharmaceutical, clinical, research and educational content does not qualify, including prescriptions, dosages, side effects, controlled medicines used as prescribed, pharmacies, hospitals and drug manufacturing by pharmaceutical companies. Lawful alcohol and tobacco, harm reduction, addiction recovery and drug-policy discussion do not qualify either. Masks, computers, payment cards, detention or ordinary tools do not establish criminal facilitation by themselves. A clear visual demonstration of how to carry out a crime can qualify.",
66
66
  docsHref: "/docs/guides/moderation-categories#criminal-and-dangerous-activity"
67
67
  }
68
68
  };
69
69
  //#endregion
70
70
  //#region ../shared/src/ModerationPolicyDimensions.ts
71
71
  const MODERATION_QUALIFIERS = [
72
- "nudity",
72
+ "sexual-nudity",
73
73
  "sexual-activity",
74
- "explicit-sexual-language",
74
+ "sexual-explicit-language",
75
75
  "sexual-solicitation",
76
- "physical-attack",
77
- "violent-threat",
78
- "violent-preparation",
79
- "violent-encouragement",
80
- "injury",
81
- "death",
82
- "graphic",
76
+ "sexual-historical",
77
+ "sexual-documentary",
78
+ "sexual-fictional",
79
+ "sexual-support-prevention",
80
+ "violence-attack",
81
+ "violence-threat",
82
+ "violence-preparation",
83
+ "violence-encouragement",
84
+ "violence-injury",
85
+ "violence-death",
86
+ "violence-graphic",
87
+ "violence-historical",
88
+ "violence-documentary",
89
+ "violence-fictional",
90
+ "violence-support-prevention",
83
91
  "self-harm-thoughts",
84
92
  "self-harm-preparation",
85
93
  "self-harm-act",
86
94
  "self-harm-methods",
87
95
  "self-harm-encouragement",
88
- "personal-abuse",
89
- "targeted-intimidation",
96
+ "self-harm-documentary",
97
+ "self-harm-fictional",
98
+ "self-harm-support-prevention",
99
+ "harassment-abuse",
100
+ "harassment-intimidation",
90
101
  "harassment-threat",
91
- "sexual-harassment",
102
+ "harassment-sexual",
92
103
  "harassment-encouragement",
93
- "identity-abuse",
94
- "dehumanization",
95
- "identity-inferiority",
96
- "discriminatory-exclusion",
104
+ "harassment-fictional",
105
+ "hate-abuse",
106
+ "hate-dehumanization",
107
+ "hate-inferiority",
108
+ "hate-exclusion",
97
109
  "hate-threat",
98
- "criminal-demonstration",
99
- "criminal-preparation",
100
- "criminal-request",
101
- "criminal-instructions",
102
- "criminal-promotion",
103
- "historical",
104
- "documentary",
105
- "fictional",
106
- "support-prevention"
110
+ "hate-historical",
111
+ "hate-fictional",
112
+ "criminal-activity-demonstration",
113
+ "criminal-activity-preparation",
114
+ "criminal-activity-request",
115
+ "criminal-activity-instructions",
116
+ "criminal-activity-promotion",
117
+ "criminal-activity-substances",
118
+ "criminal-activity-fictional"
107
119
  ];
108
- const SHARED_QUALIFIERS = [
109
- "historical",
110
- "documentary",
111
- "fictional",
112
- "support-prevention"
113
- ];
114
- [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS];
115
120
  //#endregion
116
121
  //#region ../../node_modules/zod-meta/esm/index.js
117
122
  const ZOD_META_KEY = "__zod_meta__";
@@ -276,7 +281,11 @@ const LIKELIHOOD_SCHEMA = zod.enum([
276
281
  "likely",
277
282
  "very-likely"
278
283
  ]);
279
- const CLASSIFICATION_NOT_EVALUATED_REASON_SCHEMA = zod.enum(["not-supported", "unavailable"]);
284
+ const CLASSIFICATION_NOT_EVALUATED_REASON_SCHEMA = zod.enum([
285
+ "not-supported",
286
+ "unavailable",
287
+ "not-requested"
288
+ ]);
280
289
  const CLASSIFICATION_QUALIFIER_RESULTS_SCHEMA = zod.partialRecord(zod.enum(MODERATION_QUALIFIERS), zod.discriminatedUnion("status", [zod.object({
281
290
  status: zod.literal("evaluated"),
282
291
  score: zod.number().min(0).max(1),
@@ -326,7 +335,6 @@ const CLASSIFICATION_LABEL_SCHEMA = zod.object({
326
335
  zod.object({
327
336
  processedAt: zod.number(),
328
337
  categories: CATEGORIES_SCHEMA,
329
- flagged: zod.boolean(),
330
338
  score: zod.number(),
331
339
  notes: saneStringField().nullish(),
332
340
  media: zod.object({
@@ -404,6 +412,8 @@ const ACTION_SCHEMA = zod.enum([
404
412
  "lenses:write",
405
413
  "upload-presets:read",
406
414
  "upload-presets:write",
415
+ "reviews:read",
416
+ "reviews:write",
407
417
  "project:manage"
408
418
  ]);
409
419
  const PROJECT_ACTIONS = [...[
@@ -413,7 +423,8 @@ const PROJECT_ACTIONS = [...[
413
423
  "connectors",
414
424
  "metadata",
415
425
  "lenses",
416
- "upload-presets"
426
+ "upload-presets",
427
+ "reviews"
417
428
  ].flatMap((feature) => [`${feature}:read`, `${feature}:write`]), "project:manage"];
418
429
  const isProjectAction = (action) => PROJECT_ACTIONS.includes(action);
419
430
  /** The actions that include `action`: its feature's write action and project:manage. */
@@ -426,7 +437,7 @@ const GRANT_SCHEMA = zod.object({
426
437
  const GRANTS_SCHEMA = zod.array(GRANT_SCHEMA).max(100);
427
438
  zod.enum([
428
439
  "admin",
429
- "member",
440
+ "editor",
430
441
  "viewer",
431
442
  "custom"
432
443
  ]);
@@ -572,8 +583,8 @@ const inferAssetType = (options) => {
572
583
  const createPage = (items, query) => {
573
584
  const pageIndex = query.pageIndex ?? 0;
574
585
  const pageSize = query.pageSize ?? 10;
575
- if (!Number.isInteger(pageIndex) || pageIndex < 0) throw require_CalmLensError.invalidArgument("pageIndex must be a non-negative integer");
576
- if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) throw require_CalmLensError.invalidArgument("pageSize must be an integer from 1 to 100");
586
+ if (!Number.isInteger(pageIndex) || pageIndex < 0) throw require_CalmLensVersion.invalidArgument("pageIndex must be a non-negative integer");
587
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) throw require_CalmLensVersion.invalidArgument("pageSize must be an integer from 1 to 100");
577
588
  const start = pageIndex * pageSize;
578
589
  return {
579
590
  items: items.slice(start, start + pageSize),
@@ -616,7 +627,7 @@ const parseFilterValue = (raw) => {
616
627
  return value;
617
628
  };
618
629
  const readField = (item, path) => path.split(".").reduce((value, key) => value && typeof value === "object" ? value[key] : void 0, item);
619
- const unsupportedFilter = (expression) => require_CalmLensError.invalidArgument(`MockCalmLensServerClient cannot evaluate the filter "${expression}". It supports ==, !=, <, <=, >, >=, =in= and =out=, joined by ; (and) or , (or), without grouping parentheses.`);
630
+ const unsupportedFilter = (expression) => require_CalmLensVersion.invalidArgument(`MockCalmLensServerClient cannot evaluate the filter "${expression}". It supports ==, !=, <, <=, >, >=, =in= and =out=, joined by ; (and) or , (or), without grouping parentheses.`);
620
631
  const matchesComparison = (item, comparison) => {
621
632
  const match = COMPARISON.exec(comparison.trim());
622
633
  if (!match) throw unsupportedFilter(comparison);
@@ -684,14 +695,14 @@ const describeBinary = (value) => {
684
695
  };
685
696
  const MAX_EVENTS_LIMIT = 100;
686
697
  const toFailureError = (failure) => {
687
- if (failure === "transport-error") return new require_CalmLensError.CalmLensError(require_CalmLensError.SDK_ERROR_CODES.TRANSPORT_ERROR, {
698
+ if (failure === "transport-error") return new require_CalmLensVersion.CalmLensError(require_CalmLensVersion.SDK_ERROR_CODES.TRANSPORT_ERROR, {
688
699
  message: "Request failed",
689
700
  cause: /* @__PURE__ */ new TypeError("Simulated network failure")
690
701
  });
691
702
  if (failure === "timeout") {
692
703
  const cause = /* @__PURE__ */ new Error("The operation timed out.");
693
704
  cause.name = "TimeoutError";
694
- return new require_CalmLensError.CalmLensError(require_CalmLensError.SDK_ERROR_CODES.TIMEOUT, {
705
+ return new require_CalmLensVersion.CalmLensError(require_CalmLensVersion.SDK_ERROR_CODES.TIMEOUT, {
695
706
  message: "Request timed out",
696
707
  cause
697
708
  });
@@ -720,8 +731,8 @@ var MockCalmLensServerClient = class {
720
731
  this.imageStorage = options.mockOptions?.imageStorage ?? new InMemoryMockImageStorage();
721
732
  this.createId = options.mockOptions?.createId ?? crypto.randomUUID.bind(crypto);
722
733
  this.now = options.mockOptions?.now ?? Date.now;
723
- this.queuedStatus = options.mockOptions?.queuedStatus ?? "pending";
724
- this.analyzedStatus = options.mockOptions?.analyzedStatus ?? "approved";
734
+ this.queuedOutcome = options.mockOptions?.queuedOutcome ?? "pending";
735
+ this.analyzedOutcome = options.mockOptions?.analyzedOutcome ?? "approved";
725
736
  this.apiKeyGrants = getMockApiKeyGrants(options.apiKey);
726
737
  this.signedUrlBaseUrl = options.mockOptions?.signedUrlBaseUrl ?? "mock://calmlens";
727
738
  this.cdnBaseUrl = (options.cdnBaseUrl ?? "mock://calmlens/v1").replace(/\/+$/, "");
@@ -765,9 +776,9 @@ var MockCalmLensServerClient = class {
765
776
  }
766
777
  /** An API error as the real client throws it: status, default message and request ID. */
767
778
  apiError(code, options = {}) {
768
- const definition = require_CalmLensError.API_ERROR_DEFINITIONS[code];
779
+ const definition = require_CalmLensVersion.API_ERROR_DEFINITIONS[code];
769
780
  const requestId = `mock-request-${++this.requestCount}`;
770
- return new require_CalmLensError.CalmLensError(code, {
781
+ return new require_CalmLensVersion.CalmLensError(code, {
771
782
  message: options.message ?? definition.message,
772
783
  status: definition.status,
773
784
  details: options.details,
@@ -779,18 +790,18 @@ var MockCalmLensServerClient = class {
779
790
  });
780
791
  }
781
792
  assertValidUploadMimeType(mimeType) {
782
- if (require_CalmLensUtils.isRejectedUploadMimeType(mimeType)) throw this.apiError(require_CalmLensError.API_ERROR_CODES.INVALID_UPLOAD_MIME_TYPE, {
793
+ if (require_CalmLensUtils.isRejectedUploadMimeType(mimeType)) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.INVALID_UPLOAD_MIME_TYPE, {
783
794
  message: require_CalmLensUtils.getRejectedUploadMimeTypeError(mimeType),
784
795
  details: { mimeType }
785
796
  });
786
797
  }
787
798
  invalidPayload(message) {
788
- return this.apiError(require_CalmLensError.API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message });
799
+ return this.apiError(require_CalmLensVersion.API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message });
789
800
  }
790
801
  async findOwnAsset(assetId) {
791
802
  require_CalmLensUtils.encodePathSegment(assetId, "Asset ID");
792
803
  const asset = await this.databaseStorage.getAsset(assetId);
793
- if (!asset || asset.projectId !== this.projectId) throw this.apiError(require_CalmLensError.API_ERROR_CODES.ASSET_NOT_FOUND);
804
+ if (!asset || asset.projectId !== this.projectId) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.ASSET_NOT_FOUND);
794
805
  return asset;
795
806
  }
796
807
  begin(operation, options) {
@@ -889,7 +900,7 @@ var MockCalmLensServerClient = class {
889
900
  }
890
901
  listEvents(options) {
891
902
  const limit = options.limit ?? DEFAULT_EVENTS_LIMIT;
892
- if (!Number.isInteger(limit) || limit < 1 || limit > MAX_EVENTS_LIMIT) throw this.apiError(require_CalmLensError.API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: `limit must be an integer from 1 to ${MAX_EVENTS_LIMIT}` });
903
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_EVENTS_LIMIT) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: `limit must be an integer from 1 to ${MAX_EVENTS_LIMIT}` });
893
904
  if (!options.after) return {
894
905
  events: [],
895
906
  cursor: `mock_${this.eventFeed.length}`,
@@ -897,7 +908,7 @@ var MockCalmLensServerClient = class {
897
908
  };
898
909
  const match = /^mock_(\d+)$/.exec(options.after);
899
910
  const position = match ? Number(match[1]) : NaN;
900
- if (!Number.isSafeInteger(position) || position > this.eventFeed.length) throw this.apiError(require_CalmLensError.API_ERROR_CODES.INVALID_EVENT_CURSOR);
911
+ if (!Number.isSafeInteger(position) || position > this.eventFeed.length) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.INVALID_EVENT_CURSOR);
901
912
  const events = this.eventFeed.slice(position, position + limit);
902
913
  const next = position + events.length;
903
914
  return {
@@ -1025,7 +1036,7 @@ var MockCalmLensServerClient = class {
1025
1036
  this.begin("assets.uploadPart", options);
1026
1037
  const { assetId, part } = options;
1027
1038
  require_CalmLensUtils.encodePathSegment(assetId, "Asset ID");
1028
- if (!part) throw require_CalmLensError.invalidArgument("Part options are required");
1039
+ if (!part) throw require_CalmLensVersion.invalidArgument("Part options are required");
1029
1040
  return this.withIdempotency("assets.uploadPart", options, async () => {
1030
1041
  const asset = await this.findOwnAsset(assetId);
1031
1042
  const validation = require_CalmLensUtils.validateAssetPayload(part, {
@@ -1033,25 +1044,29 @@ var MockCalmLensServerClient = class {
1033
1044
  isFinal: part.isFinal
1034
1045
  });
1035
1046
  if (!validation.ok) throw this.invalidPayload(validation.error);
1036
- if (asset.status !== "uploading") throw this.apiError(require_CalmLensError.API_ERROR_CODES.UPLOAD_NOT_OPEN);
1037
- const expectedPartNumber = (asset.uploadPartsReceived ?? 1) + 1;
1038
- if (part.partNumber !== expectedPartNumber) throw this.apiError(require_CalmLensError.API_ERROR_CODES.INVALID_UPLOAD_PART_NUMBER, { details: { expectedPartNumber } });
1047
+ if (asset.processing.status !== "uploading") throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.UPLOAD_NOT_OPEN);
1048
+ const expectedPartNumber = (asset.processing.uploadPartsReceived ?? 1) + 1;
1049
+ if (part.partNumber !== expectedPartNumber) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.INVALID_UPLOAD_PART_NUMBER, { details: { expectedPartNumber } });
1039
1050
  await this.imageStorage.putPart(assetId, part);
1040
1051
  const queue = options.queue ?? this.uploadQueues.get(assetId) ?? false;
1041
1052
  if (part.isFinal) this.uploadQueues.delete(assetId);
1042
- return this.databaseStorage.updateAsset({
1053
+ const now = this.now();
1054
+ const received = {
1043
1055
  ...asset,
1044
- status: part.isFinal ? queue ? this.queuedStatus : this.analyzedStatus : "uploading",
1045
- uploadPartsReceived: part.partNumber,
1046
- updatedAt: this.now()
1047
- });
1056
+ processing: {
1057
+ ...asset.processing,
1058
+ uploadPartsReceived: part.partNumber
1059
+ },
1060
+ updatedAt: now
1061
+ };
1062
+ return this.databaseStorage.updateAsset(part.isFinal ? await this.applyOutcome(received, queue ? this.queuedOutcome : this.analyzedOutcome) : received);
1048
1063
  });
1049
1064
  },
1050
1065
  createSignedUrl: async (signedUrlOptions) => {
1051
1066
  this.begin("assets.createSignedUrl", signedUrlOptions);
1052
1067
  const { assetId, ...options } = signedUrlOptions;
1053
1068
  const asset = await this.findOwnAsset(assetId);
1054
- if (options.lens && options.transform) throw require_CalmLensError.invalidArgument("Specify either a lens or a transform, not both");
1069
+ if (options.lens && options.transform) throw require_CalmLensVersion.invalidArgument("Specify either a lens or a transform, not both");
1055
1070
  const expiresInSeconds = options.expiresInSeconds ?? 900;
1056
1071
  const resource = options.resource ?? "content";
1057
1072
  const transform = require_CalmLensUtils.transformSelector(options.transform);
@@ -1066,7 +1081,7 @@ var MockCalmLensServerClient = class {
1066
1081
  const { assetId, folderId } = options;
1067
1082
  const asset = await this.findOwnAsset(assetId);
1068
1083
  if (folderId !== null && folderId !== void 0) {
1069
- if (!await this.resolveFolder(folderId)) throw this.apiError(require_CalmLensError.API_ERROR_CODES.FOLDER_NOT_FOUND);
1084
+ if (!await this.resolveFolder(folderId)) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.FOLDER_NOT_FOUND);
1070
1085
  }
1071
1086
  return this.databaseStorage.updateAsset({
1072
1087
  ...asset,
@@ -1077,23 +1092,31 @@ var MockCalmLensServerClient = class {
1077
1092
  retry: async (options) => {
1078
1093
  this.begin("assets.retry", options);
1079
1094
  const asset = await this.findOwnAsset(options?.assetId);
1080
- if (asset.status !== "error") throw this.apiError(require_CalmLensError.API_ERROR_CODES.ASSET_NOT_RETRYABLE);
1081
- if (asset.storageDeletedAt) throw this.apiError(require_CalmLensError.API_ERROR_CODES.ASSET_CONTENT_UNAVAILABLE);
1082
- return this.databaseStorage.updateAsset({
1083
- ...asset,
1084
- status: "pending",
1085
- updatedAt: this.now()
1086
- });
1095
+ if (asset.processing.status !== "failed") throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.ASSET_NOT_RETRYABLE);
1096
+ if (asset.content.status === "removed") throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.ASSET_CONTENT_UNAVAILABLE);
1097
+ return this.databaseStorage.updateAsset(await this.applyOutcome(asset, "pending"));
1098
+ },
1099
+ /** Waits like the real client, checking the mock's storage for a decision. */
1100
+ waitForDecision: async (options) => {
1101
+ this.begin("assets.waitForDecision", options);
1102
+ const { assetId, ...waitOptions } = options;
1103
+ return require_CalmLensUtils.pollForDecision(() => this.findOwnAsset(assetId), waitOptions, require_CalmLensUtils.sleep);
1087
1104
  },
1088
1105
  deleteContent: async (options) => {
1089
1106
  this.begin("assets.deleteContent", options);
1090
1107
  const asset = await this.findOwnAsset(options?.assetId);
1091
1108
  await this.imageStorage.deleteContent(asset.id);
1109
+ const now = this.now();
1092
1110
  return this.databaseStorage.updateAsset({
1093
1111
  ...asset,
1094
- extractedText: null,
1095
- storageDeletedAt: asset.storageDeletedAt ?? this.now(),
1096
- updatedAt: this.now()
1112
+ content: asset.content.status === "removed" ? asset.content : {
1113
+ ...asset.content,
1114
+ status: "removed",
1115
+ removalReason: "requested",
1116
+ updatedAt: now,
1117
+ extractedText: null
1118
+ },
1119
+ updatedAt: now
1097
1120
  });
1098
1121
  },
1099
1122
  delete: async (options) => {
@@ -1157,7 +1180,7 @@ var MockCalmLensServerClient = class {
1157
1180
  await this.findOwnFolder(folderId);
1158
1181
  const hasAssets = (await this.databaseStorage.listAssets(this.projectId)).some((asset) => asset.folderId === folderId);
1159
1182
  const hasChildren = (this.databaseStorage.listFolders ? await this.databaseStorage.listFolders(this.projectId) : []).some((f) => f.parentId === folderId);
1160
- if (hasAssets || hasChildren) throw this.apiError(require_CalmLensError.API_ERROR_CODES.FOLDER_NOT_EMPTY, { details: {
1183
+ if (hasAssets || hasChildren) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.FOLDER_NOT_EMPTY, { details: {
1161
1184
  hasAssets,
1162
1185
  hasChildren
1163
1186
  } });
@@ -1172,7 +1195,7 @@ var MockCalmLensServerClient = class {
1172
1195
  let candidate = await this.findOwnFolder(targetParentId);
1173
1196
  const visited = /* @__PURE__ */ new Set();
1174
1197
  while (candidate) {
1175
- if (candidate.id === folderId || visited.has(candidate.id)) throw this.apiError(require_CalmLensError.API_ERROR_CODES.FOLDER_PARENT_CYCLE);
1198
+ if (candidate.id === folderId || visited.has(candidate.id)) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.FOLDER_PARENT_CYCLE);
1176
1199
  visited.add(candidate.id);
1177
1200
  candidate = candidate.parentId ? await this.resolveFolder(candidate.parentId) : void 0;
1178
1201
  }
@@ -1189,7 +1212,7 @@ var MockCalmLensServerClient = class {
1189
1212
  }
1190
1213
  async ensureLens(options) {
1191
1214
  this.begin("ensureLens", options);
1192
- if (!options?.identifier) throw require_CalmLensError.invalidArgument("Lens identifier is required");
1215
+ if (!options?.identifier) throw require_CalmLensVersion.invalidArgument("Lens identifier is required");
1193
1216
  return this.ensureLensDefinition(options);
1194
1217
  }
1195
1218
  ensureLensDefinition(options) {
@@ -1216,7 +1239,7 @@ var MockCalmLensServerClient = class {
1216
1239
  this.lenses.set(lens.identifier, lens);
1217
1240
  return lens;
1218
1241
  }
1219
- throw this.apiError(require_CalmLensError.API_ERROR_CODES.LENS_VERSION_CONFLICT, { details: {
1242
+ throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.LENS_VERSION_CONFLICT, { details: {
1220
1243
  currentVersion: existing.version,
1221
1244
  minimumVersion: existing.version + 1,
1222
1245
  requestedVersion: options.version
@@ -1224,19 +1247,19 @@ var MockCalmLensServerClient = class {
1224
1247
  }
1225
1248
  async ensureLenses(options) {
1226
1249
  this.begin("ensureLenses", options);
1227
- if (!Array.isArray(options?.lenses) || options.lenses.length === 0) throw require_CalmLensError.invalidArgument("At least one lens is required");
1228
- if (options.lenses.some((lens) => !lens?.identifier)) throw require_CalmLensError.invalidArgument("Lens identifier is required");
1250
+ if (!Array.isArray(options?.lenses) || options.lenses.length === 0) throw require_CalmLensVersion.invalidArgument("At least one lens is required");
1251
+ if (options.lenses.some((lens) => !lens?.identifier)) throw require_CalmLensVersion.invalidArgument("Lens identifier is required");
1229
1252
  return options.lenses.map((option) => this.ensureLensDefinition(option));
1230
1253
  }
1231
1254
  /** Replays the first result for a reused idempotency key, like the API. */
1232
1255
  async withIdempotency(operation, options, run) {
1233
1256
  const key = options?.idempotencyKey;
1234
1257
  if (key === void 0) return run();
1235
- if (!key || key.length > 255 || !/^[\x21-\x7e]+$/.test(key)) throw this.apiError(require_CalmLensError.API_ERROR_CODES.INVALID_IDEMPOTENCY_KEY);
1258
+ if (!key || key.length > 255 || !/^[\x21-\x7e]+$/.test(key)) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.INVALID_IDEMPOTENCY_KEY);
1236
1259
  const fingerprint = `${operation}:${require_CalmLensWebhooks.stringifyJsonCanonical(JSON.parse(JSON.stringify(options, (_key, value) => describeBinary(value))))}`;
1237
1260
  const saved = this.idempotentResults.get(key);
1238
1261
  if (saved) {
1239
- if (saved.fingerprint !== fingerprint) throw this.apiError(require_CalmLensError.API_ERROR_CODES.IDEMPOTENCY_KEY_REUSED);
1262
+ if (saved.fingerprint !== fingerprint) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.IDEMPOTENCY_KEY_REUSED);
1240
1263
  return saved.asset;
1241
1264
  }
1242
1265
  const asset = await run();
@@ -1251,11 +1274,11 @@ var MockCalmLensServerClient = class {
1251
1274
  return this.withIdempotency(operation, options, () => this.createAsset(options, false));
1252
1275
  }
1253
1276
  async createAsset(options, queue) {
1254
- if (!options || typeof options !== "object") throw require_CalmLensError.invalidArgument("Asset options are required");
1277
+ if (!options || typeof options !== "object") throw require_CalmLensVersion.invalidArgument("Asset options are required");
1255
1278
  if (!("content" in options) && !("url" in options) && !("part" in options)) throw this.invalidPayload("Either file content, URL, or multipart part is required");
1256
1279
  if (!options.name) throw this.invalidPayload("Asset name is required");
1257
1280
  if (!("part" in options) && "isFinal" in options) throw this.invalidPayload("isFinal is only valid for multipart uploads");
1258
- if ("part" in options && options.part.partNumber && options.part.partNumber > 1) throw this.apiError(require_CalmLensError.API_ERROR_CODES.USE_UPLOAD_PART);
1281
+ if ("part" in options && options.part.partNumber && options.part.partNumber > 1) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.USE_UPLOAD_PART);
1259
1282
  if ("content" in options) {
1260
1283
  this.assertValidUploadMimeType(options.content.type);
1261
1284
  if (!("plainText" in options.content)) {
@@ -1272,11 +1295,12 @@ var MockCalmLensServerClient = class {
1272
1295
  if (!validation.ok) throw this.invalidPayload(validation.error);
1273
1296
  }
1274
1297
  if (options.externalId) {
1275
- if ((await this.databaseStorage.listAssets(this.projectId)).some((asset) => asset.externalId === options.externalId)) throw this.apiError(require_CalmLensError.API_ERROR_CODES.DUPLICATE_EXTERNAL_ID);
1298
+ if ((await this.databaseStorage.listAssets(this.projectId)).some((asset) => asset.externalId === options.externalId)) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.DUPLICATE_EXTERNAL_ID);
1276
1299
  }
1277
1300
  const id = this.createId();
1278
1301
  const now = this.now();
1279
1302
  const isOpenUpload = "part" in options && !options.part.isFinal;
1303
+ const keepContent = options.processingOptions?.keepContentAfterProcessing === true;
1280
1304
  const asset = {
1281
1305
  id,
1282
1306
  projectId: this.projectId,
@@ -1289,11 +1313,37 @@ var MockCalmLensServerClient = class {
1289
1313
  externalUrl: "url" in options ? options.url : "content" in options ? options.externalUrl : void 0,
1290
1314
  type: inferAssetType(options),
1291
1315
  metadata: options.metadata,
1292
- status: isOpenUpload ? "uploading" : queue ? this.queuedStatus : this.analyzedStatus,
1293
1316
  processingOptions: options.processingOptions,
1294
1317
  visibility: options.visibility ?? "private",
1295
- sizeInBytes: this.getSizeInBytes(options),
1296
- uploadPartsReceived: "part" in options ? options.part.partNumber ?? 1 : void 0
1318
+ processing: {
1319
+ status: isOpenUpload ? "uploading" : "processing",
1320
+ updatedAt: now,
1321
+ uploadPartsReceived: "part" in options ? options.part.partNumber ?? 1 : null
1322
+ },
1323
+ moderation: {
1324
+ status: "pending",
1325
+ action: null,
1326
+ note: null,
1327
+ reviewerAction: null,
1328
+ reviewerNote: null,
1329
+ reviewerUserId: null,
1330
+ reviewedAt: null,
1331
+ decidedAt: null,
1332
+ report: null
1333
+ },
1334
+ content: {
1335
+ status: keepContent ? "stored" : "temporary",
1336
+ removalReason: null,
1337
+ updatedAt: now,
1338
+ safetyHold: null,
1339
+ format: this.getContentFormat(options),
1340
+ extension: null,
1341
+ sizeInBytes: this.getSizeInBytes(options) ?? null,
1342
+ checksums: null,
1343
+ image: null,
1344
+ previewImage: null,
1345
+ extractedText: null
1346
+ }
1297
1347
  };
1298
1348
  if ("content" in options) {
1299
1349
  if (!("plainText" in options.content)) await this.imageStorage.putContent(id, options.content);
@@ -1301,7 +1351,39 @@ var MockCalmLensServerClient = class {
1301
1351
  await this.imageStorage.putPart(id, options.part);
1302
1352
  if (isOpenUpload) this.uploadQueues.set(id, queue);
1303
1353
  }
1304
- return this.databaseStorage.createAsset(asset);
1354
+ return this.databaseStorage.createAsset(isOpenUpload ? asset : await this.applyOutcome(asset, queue ? this.queuedOutcome : this.analyzedOutcome));
1355
+ }
1356
+ /**
1357
+ * Moves an asset to `outcome` like the pipeline would: a decision finishes processing, and
1358
+ * finished processing removes content kept only temporarily (`keepContentAfterProcessing` off).
1359
+ */
1360
+ async applyOutcome(asset, outcome) {
1361
+ const now = this.now();
1362
+ const decided = outcome === "approved" || outcome === "rejected";
1363
+ const processingStatus = decided ? "done" : outcome === "failed" ? "failed" : "processing";
1364
+ const removeContent = processingStatus === "done" && asset.content.status === "temporary";
1365
+ if (removeContent) await this.imageStorage.deleteContent(asset.id);
1366
+ return {
1367
+ ...asset,
1368
+ updatedAt: now,
1369
+ processing: {
1370
+ ...asset.processing,
1371
+ status: processingStatus,
1372
+ updatedAt: now
1373
+ },
1374
+ moderation: {
1375
+ ...asset.moderation,
1376
+ status: decided ? outcome : "pending",
1377
+ decidedAt: decided ? now : null
1378
+ },
1379
+ content: removeContent ? {
1380
+ ...asset.content,
1381
+ status: "removed",
1382
+ removalReason: "retention",
1383
+ updatedAt: now,
1384
+ extractedText: null
1385
+ } : asset.content
1386
+ };
1305
1387
  }
1306
1388
  async resolveFolder(folderId) {
1307
1389
  if (!this.databaseStorage.getFolder) return void 0;
@@ -1312,11 +1394,16 @@ var MockCalmLensServerClient = class {
1312
1394
  async findOwnFolder(folderId) {
1313
1395
  require_CalmLensUtils.encodePathSegment(folderId, "Folder ID");
1314
1396
  const folder = await this.resolveFolder(folderId);
1315
- if (!folder) throw this.apiError(require_CalmLensError.API_ERROR_CODES.FOLDER_NOT_FOUND);
1397
+ if (!folder) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.FOLDER_NOT_FOUND);
1316
1398
  return folder;
1317
1399
  }
1318
1400
  async assertUniqueFolderName(name, parentId, excludeFolderId) {
1319
- if ((this.databaseStorage.listFolders ? await this.databaseStorage.listFolders(this.projectId) : []).find((f) => f.id !== excludeFolderId && f.parentId === (parentId ?? null) && f.name === name)) throw this.apiError(require_CalmLensError.API_ERROR_CODES.DUPLICATE_FOLDER_NAME, { message: `A folder named "${name}" already exists in this location.` });
1401
+ if ((this.databaseStorage.listFolders ? await this.databaseStorage.listFolders(this.projectId) : []).find((f) => f.id !== excludeFolderId && f.parentId === (parentId ?? null) && f.name === name)) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.DUPLICATE_FOLDER_NAME, { message: `A folder named "${name}" already exists in this location.` });
1402
+ }
1403
+ getContentFormat(options) {
1404
+ if ("content" in options) return options.content.type;
1405
+ if ("part" in options) return options.part.type;
1406
+ return null;
1320
1407
  }
1321
1408
  getSizeInBytes(options) {
1322
1409
  if ("content" in options) {