@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/esm/testing.js CHANGED
@@ -1,6 +1,6 @@
1
- import { i as invalidArgument, n as CalmLensError, o as API_ERROR_CODES, r as SDK_ERROR_CODES, s as API_ERROR_DEFINITIONS } from "./CalmLensError-CihGG26K.js";
2
- import { c as hmacSha256Hex, i as WEBHOOK_TIMESTAMP_HEADER, l as stringifyJsonCanonical, n as WEBHOOK_SIGNATURE_HEADER, s as signUploadTokenWithApiKey, t as WEBHOOK_DELIVERY_HEADER } from "./CalmLensWebhooks-DShmAal4.js";
3
- import { _ as getRejectedUploadMimeTypeError, a as toTextAssetOptions, d as assetToData, f as getAssetPayloadByteLength, l as validateServerClientOptions, n as encodePathSegment, o as transformSelector, p as validateAssetPayload, t as appendCdnSelector, u as assetDataToUint8Array, v as isRejectedUploadMimeType } from "./CalmLensUtils-Cx32j2zt.js";
1
+ import { c as invalidArgument, d as API_ERROR_DEFINITIONS, o as CalmLensError, s as SDK_ERROR_CODES, u as API_ERROR_CODES } from "./CalmLensVersion-DPnL_Ebv.js";
2
+ import { c as hmacSha256Hex, i as WEBHOOK_TIMESTAMP_HEADER, l as stringifyJsonCanonical, n as WEBHOOK_SIGNATURE_HEADER, s as signUploadTokenWithApiKey, t as WEBHOOK_DELIVERY_HEADER } from "./CalmLensWebhooks-CWlD-f_P.js";
3
+ import { b as getRejectedUploadMimeTypeError, f as sleep, g as validateAssetPayload, h as getAssetPayloadByteLength, m as assetToData, n as encodePathSegment, o as toTextAssetOptions, p as assetDataToUint8Array, r as pollForDecision, s as transformSelector, t as appendCdnSelector, u as validateServerClientOptions, x as isRejectedUploadMimeType } from "./CalmLensUtils-BQo79TA1.js";
4
4
  import * as zod from "zod";
5
5
  import "zod/v4";
6
6
  //#region ../shared/src/ModerationCategoryDefinitions.ts
@@ -36,58 +36,63 @@ const MODERATION_CATEGORY_DEFINITIONS = {
36
36
  docsHref: "/docs/guides/moderation-categories#hate-and-identity-abuse"
37
37
  },
38
38
  "criminal-activity": {
39
- summary: "Planning, assistance, instructions, encouragement or facilitation of a concrete criminal or seriously dangerous act.",
40
- 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.",
41
- 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.",
39
+ summary: "Planning, assistance, instructions, encouragement or facilitation of a concrete criminal or seriously dangerous act, and illegal drugs or contraband.",
40
+ 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.",
41
+ 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.",
42
42
  docsHref: "/docs/guides/moderation-categories#criminal-and-dangerous-activity"
43
43
  }
44
44
  };
45
45
  //#endregion
46
46
  //#region ../shared/src/ModerationPolicyDimensions.ts
47
47
  const MODERATION_QUALIFIERS = [
48
- "nudity",
48
+ "sexual-nudity",
49
49
  "sexual-activity",
50
- "explicit-sexual-language",
50
+ "sexual-explicit-language",
51
51
  "sexual-solicitation",
52
- "physical-attack",
53
- "violent-threat",
54
- "violent-preparation",
55
- "violent-encouragement",
56
- "injury",
57
- "death",
58
- "graphic",
52
+ "sexual-historical",
53
+ "sexual-documentary",
54
+ "sexual-fictional",
55
+ "sexual-support-prevention",
56
+ "violence-attack",
57
+ "violence-threat",
58
+ "violence-preparation",
59
+ "violence-encouragement",
60
+ "violence-injury",
61
+ "violence-death",
62
+ "violence-graphic",
63
+ "violence-historical",
64
+ "violence-documentary",
65
+ "violence-fictional",
66
+ "violence-support-prevention",
59
67
  "self-harm-thoughts",
60
68
  "self-harm-preparation",
61
69
  "self-harm-act",
62
70
  "self-harm-methods",
63
71
  "self-harm-encouragement",
64
- "personal-abuse",
65
- "targeted-intimidation",
72
+ "self-harm-documentary",
73
+ "self-harm-fictional",
74
+ "self-harm-support-prevention",
75
+ "harassment-abuse",
76
+ "harassment-intimidation",
66
77
  "harassment-threat",
67
- "sexual-harassment",
78
+ "harassment-sexual",
68
79
  "harassment-encouragement",
69
- "identity-abuse",
70
- "dehumanization",
71
- "identity-inferiority",
72
- "discriminatory-exclusion",
80
+ "harassment-fictional",
81
+ "hate-abuse",
82
+ "hate-dehumanization",
83
+ "hate-inferiority",
84
+ "hate-exclusion",
73
85
  "hate-threat",
74
- "criminal-demonstration",
75
- "criminal-preparation",
76
- "criminal-request",
77
- "criminal-instructions",
78
- "criminal-promotion",
79
- "historical",
80
- "documentary",
81
- "fictional",
82
- "support-prevention"
86
+ "hate-historical",
87
+ "hate-fictional",
88
+ "criminal-activity-demonstration",
89
+ "criminal-activity-preparation",
90
+ "criminal-activity-request",
91
+ "criminal-activity-instructions",
92
+ "criminal-activity-promotion",
93
+ "criminal-activity-substances",
94
+ "criminal-activity-fictional"
83
95
  ];
84
- const SHARED_QUALIFIERS = [
85
- "historical",
86
- "documentary",
87
- "fictional",
88
- "support-prevention"
89
- ];
90
- [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS];
91
96
  //#endregion
92
97
  //#region ../../node_modules/zod-meta/esm/index.js
93
98
  const ZOD_META_KEY = "__zod_meta__";
@@ -252,7 +257,11 @@ const LIKELIHOOD_SCHEMA = zod.enum([
252
257
  "likely",
253
258
  "very-likely"
254
259
  ]);
255
- const CLASSIFICATION_NOT_EVALUATED_REASON_SCHEMA = zod.enum(["not-supported", "unavailable"]);
260
+ const CLASSIFICATION_NOT_EVALUATED_REASON_SCHEMA = zod.enum([
261
+ "not-supported",
262
+ "unavailable",
263
+ "not-requested"
264
+ ]);
256
265
  const CLASSIFICATION_QUALIFIER_RESULTS_SCHEMA = zod.partialRecord(zod.enum(MODERATION_QUALIFIERS), zod.discriminatedUnion("status", [zod.object({
257
266
  status: zod.literal("evaluated"),
258
267
  score: zod.number().min(0).max(1),
@@ -302,7 +311,6 @@ const CLASSIFICATION_LABEL_SCHEMA = zod.object({
302
311
  zod.object({
303
312
  processedAt: zod.number(),
304
313
  categories: CATEGORIES_SCHEMA,
305
- flagged: zod.boolean(),
306
314
  score: zod.number(),
307
315
  notes: saneStringField().nullish(),
308
316
  media: zod.object({
@@ -380,6 +388,8 @@ const ACTION_SCHEMA = zod.enum([
380
388
  "lenses:write",
381
389
  "upload-presets:read",
382
390
  "upload-presets:write",
391
+ "reviews:read",
392
+ "reviews:write",
383
393
  "project:manage"
384
394
  ]);
385
395
  const PROJECT_ACTIONS = [...[
@@ -389,7 +399,8 @@ const PROJECT_ACTIONS = [...[
389
399
  "connectors",
390
400
  "metadata",
391
401
  "lenses",
392
- "upload-presets"
402
+ "upload-presets",
403
+ "reviews"
393
404
  ].flatMap((feature) => [`${feature}:read`, `${feature}:write`]), "project:manage"];
394
405
  const isProjectAction = (action) => PROJECT_ACTIONS.includes(action);
395
406
  /** The actions that include `action`: its feature's write action and project:manage. */
@@ -402,7 +413,7 @@ const GRANT_SCHEMA = zod.object({
402
413
  const GRANTS_SCHEMA = zod.array(GRANT_SCHEMA).max(100);
403
414
  zod.enum([
404
415
  "admin",
405
- "member",
416
+ "editor",
406
417
  "viewer",
407
418
  "custom"
408
419
  ]);
@@ -696,8 +707,8 @@ var MockCalmLensServerClient = class {
696
707
  this.imageStorage = options.mockOptions?.imageStorage ?? new InMemoryMockImageStorage();
697
708
  this.createId = options.mockOptions?.createId ?? crypto.randomUUID.bind(crypto);
698
709
  this.now = options.mockOptions?.now ?? Date.now;
699
- this.queuedStatus = options.mockOptions?.queuedStatus ?? "pending";
700
- this.analyzedStatus = options.mockOptions?.analyzedStatus ?? "approved";
710
+ this.queuedOutcome = options.mockOptions?.queuedOutcome ?? "pending";
711
+ this.analyzedOutcome = options.mockOptions?.analyzedOutcome ?? "approved";
701
712
  this.apiKeyGrants = getMockApiKeyGrants(options.apiKey);
702
713
  this.signedUrlBaseUrl = options.mockOptions?.signedUrlBaseUrl ?? "mock://calmlens";
703
714
  this.cdnBaseUrl = (options.cdnBaseUrl ?? "mock://calmlens/v1").replace(/\/+$/, "");
@@ -1009,18 +1020,22 @@ var MockCalmLensServerClient = class {
1009
1020
  isFinal: part.isFinal
1010
1021
  });
1011
1022
  if (!validation.ok) throw this.invalidPayload(validation.error);
1012
- if (asset.status !== "uploading") throw this.apiError(API_ERROR_CODES.UPLOAD_NOT_OPEN);
1013
- const expectedPartNumber = (asset.uploadPartsReceived ?? 1) + 1;
1023
+ if (asset.processing.status !== "uploading") throw this.apiError(API_ERROR_CODES.UPLOAD_NOT_OPEN);
1024
+ const expectedPartNumber = (asset.processing.uploadPartsReceived ?? 1) + 1;
1014
1025
  if (part.partNumber !== expectedPartNumber) throw this.apiError(API_ERROR_CODES.INVALID_UPLOAD_PART_NUMBER, { details: { expectedPartNumber } });
1015
1026
  await this.imageStorage.putPart(assetId, part);
1016
1027
  const queue = options.queue ?? this.uploadQueues.get(assetId) ?? false;
1017
1028
  if (part.isFinal) this.uploadQueues.delete(assetId);
1018
- return this.databaseStorage.updateAsset({
1029
+ const now = this.now();
1030
+ const received = {
1019
1031
  ...asset,
1020
- status: part.isFinal ? queue ? this.queuedStatus : this.analyzedStatus : "uploading",
1021
- uploadPartsReceived: part.partNumber,
1022
- updatedAt: this.now()
1023
- });
1032
+ processing: {
1033
+ ...asset.processing,
1034
+ uploadPartsReceived: part.partNumber
1035
+ },
1036
+ updatedAt: now
1037
+ };
1038
+ return this.databaseStorage.updateAsset(part.isFinal ? await this.applyOutcome(received, queue ? this.queuedOutcome : this.analyzedOutcome) : received);
1024
1039
  });
1025
1040
  },
1026
1041
  createSignedUrl: async (signedUrlOptions) => {
@@ -1053,23 +1068,31 @@ var MockCalmLensServerClient = class {
1053
1068
  retry: async (options) => {
1054
1069
  this.begin("assets.retry", options);
1055
1070
  const asset = await this.findOwnAsset(options?.assetId);
1056
- if (asset.status !== "error") throw this.apiError(API_ERROR_CODES.ASSET_NOT_RETRYABLE);
1057
- if (asset.storageDeletedAt) throw this.apiError(API_ERROR_CODES.ASSET_CONTENT_UNAVAILABLE);
1058
- return this.databaseStorage.updateAsset({
1059
- ...asset,
1060
- status: "pending",
1061
- updatedAt: this.now()
1062
- });
1071
+ if (asset.processing.status !== "failed") throw this.apiError(API_ERROR_CODES.ASSET_NOT_RETRYABLE);
1072
+ if (asset.content.status === "removed") throw this.apiError(API_ERROR_CODES.ASSET_CONTENT_UNAVAILABLE);
1073
+ return this.databaseStorage.updateAsset(await this.applyOutcome(asset, "pending"));
1074
+ },
1075
+ /** Waits like the real client, checking the mock's storage for a decision. */
1076
+ waitForDecision: async (options) => {
1077
+ this.begin("assets.waitForDecision", options);
1078
+ const { assetId, ...waitOptions } = options;
1079
+ return pollForDecision(() => this.findOwnAsset(assetId), waitOptions, sleep);
1063
1080
  },
1064
1081
  deleteContent: async (options) => {
1065
1082
  this.begin("assets.deleteContent", options);
1066
1083
  const asset = await this.findOwnAsset(options?.assetId);
1067
1084
  await this.imageStorage.deleteContent(asset.id);
1085
+ const now = this.now();
1068
1086
  return this.databaseStorage.updateAsset({
1069
1087
  ...asset,
1070
- extractedText: null,
1071
- storageDeletedAt: asset.storageDeletedAt ?? this.now(),
1072
- updatedAt: this.now()
1088
+ content: asset.content.status === "removed" ? asset.content : {
1089
+ ...asset.content,
1090
+ status: "removed",
1091
+ removalReason: "requested",
1092
+ updatedAt: now,
1093
+ extractedText: null
1094
+ },
1095
+ updatedAt: now
1073
1096
  });
1074
1097
  },
1075
1098
  delete: async (options) => {
@@ -1253,6 +1276,7 @@ var MockCalmLensServerClient = class {
1253
1276
  const id = this.createId();
1254
1277
  const now = this.now();
1255
1278
  const isOpenUpload = "part" in options && !options.part.isFinal;
1279
+ const keepContent = options.processingOptions?.keepContentAfterProcessing === true;
1256
1280
  const asset = {
1257
1281
  id,
1258
1282
  projectId: this.projectId,
@@ -1265,11 +1289,37 @@ var MockCalmLensServerClient = class {
1265
1289
  externalUrl: "url" in options ? options.url : "content" in options ? options.externalUrl : void 0,
1266
1290
  type: inferAssetType(options),
1267
1291
  metadata: options.metadata,
1268
- status: isOpenUpload ? "uploading" : queue ? this.queuedStatus : this.analyzedStatus,
1269
1292
  processingOptions: options.processingOptions,
1270
1293
  visibility: options.visibility ?? "private",
1271
- sizeInBytes: this.getSizeInBytes(options),
1272
- uploadPartsReceived: "part" in options ? options.part.partNumber ?? 1 : void 0
1294
+ processing: {
1295
+ status: isOpenUpload ? "uploading" : "processing",
1296
+ updatedAt: now,
1297
+ uploadPartsReceived: "part" in options ? options.part.partNumber ?? 1 : null
1298
+ },
1299
+ moderation: {
1300
+ status: "pending",
1301
+ action: null,
1302
+ note: null,
1303
+ reviewerAction: null,
1304
+ reviewerNote: null,
1305
+ reviewerUserId: null,
1306
+ reviewedAt: null,
1307
+ decidedAt: null,
1308
+ report: null
1309
+ },
1310
+ content: {
1311
+ status: keepContent ? "stored" : "temporary",
1312
+ removalReason: null,
1313
+ updatedAt: now,
1314
+ safetyHold: null,
1315
+ format: this.getContentFormat(options),
1316
+ extension: null,
1317
+ sizeInBytes: this.getSizeInBytes(options) ?? null,
1318
+ checksums: null,
1319
+ image: null,
1320
+ previewImage: null,
1321
+ extractedText: null
1322
+ }
1273
1323
  };
1274
1324
  if ("content" in options) {
1275
1325
  if (!("plainText" in options.content)) await this.imageStorage.putContent(id, options.content);
@@ -1277,7 +1327,39 @@ var MockCalmLensServerClient = class {
1277
1327
  await this.imageStorage.putPart(id, options.part);
1278
1328
  if (isOpenUpload) this.uploadQueues.set(id, queue);
1279
1329
  }
1280
- return this.databaseStorage.createAsset(asset);
1330
+ return this.databaseStorage.createAsset(isOpenUpload ? asset : await this.applyOutcome(asset, queue ? this.queuedOutcome : this.analyzedOutcome));
1331
+ }
1332
+ /**
1333
+ * Moves an asset to `outcome` like the pipeline would: a decision finishes processing, and
1334
+ * finished processing removes content kept only temporarily (`keepContentAfterProcessing` off).
1335
+ */
1336
+ async applyOutcome(asset, outcome) {
1337
+ const now = this.now();
1338
+ const decided = outcome === "approved" || outcome === "rejected";
1339
+ const processingStatus = decided ? "done" : outcome === "failed" ? "failed" : "processing";
1340
+ const removeContent = processingStatus === "done" && asset.content.status === "temporary";
1341
+ if (removeContent) await this.imageStorage.deleteContent(asset.id);
1342
+ return {
1343
+ ...asset,
1344
+ updatedAt: now,
1345
+ processing: {
1346
+ ...asset.processing,
1347
+ status: processingStatus,
1348
+ updatedAt: now
1349
+ },
1350
+ moderation: {
1351
+ ...asset.moderation,
1352
+ status: decided ? outcome : "pending",
1353
+ decidedAt: decided ? now : null
1354
+ },
1355
+ content: removeContent ? {
1356
+ ...asset.content,
1357
+ status: "removed",
1358
+ removalReason: "retention",
1359
+ updatedAt: now,
1360
+ extractedText: null
1361
+ } : asset.content
1362
+ };
1281
1363
  }
1282
1364
  async resolveFolder(folderId) {
1283
1365
  if (!this.databaseStorage.getFolder) return void 0;
@@ -1294,6 +1376,11 @@ var MockCalmLensServerClient = class {
1294
1376
  async assertUniqueFolderName(name, parentId, excludeFolderId) {
1295
1377
  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(API_ERROR_CODES.DUPLICATE_FOLDER_NAME, { message: `A folder named "${name}" already exists in this location.` });
1296
1378
  }
1379
+ getContentFormat(options) {
1380
+ if ("content" in options) return options.content.type;
1381
+ if ("part" in options) return options.part.type;
1382
+ return null;
1383
+ }
1297
1384
  getSizeInBytes(options) {
1298
1385
  if ("content" in options) {
1299
1386
  if ("plainText" in options.content) return new TextEncoder().encode(options.content.plainText).byteLength;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calmlens/sdk",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Official CalmLens SDK for JavaScript and TypeScript: content moderation, asset storage and CDN delivery.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://calmlens.com/docs/sdk/javascript",