@calmlens/sdk 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/testing.js CHANGED
@@ -1,5 +1,245 @@
1
- import { C as _objectSpread2, S as _asyncToGenerator, _ as assetDataToUint8Array, a as validateClientOptions, g as stringifyJsonCanonical, h as PUBLIC_ERROR_CODES, i as toTextAssetOptions, l as calmLensError, m as isRejectedUploadMimeType, n as encodePathSegment, o as _objectWithoutProperties, p as getRejectedUploadMimeTypeError, t as appendCdnSelector, v as assetToData, x as validateAssetPayload, y as getAssetPayloadByteLength } from "./CalmLensUtils-O1xk_Ts0.js";
1
+ import { D as assetDataToUint8Array, E as stringifyJsonCanonical, O as assetToData, T as isRejectedUploadMimeType, _ as invalidArgument, b as API_ERROR_DEFINITIONS, c as appendCdnSelector, d as toTextAssetOptions, f as validateClientOptions, g as SDK_ERROR_CODES, h as CalmLensError, i as WEBHOOK_TIMESTAMP_HEADER, j as validateAssetPayload, k as getAssetPayloadByteLength, l as encodePathSegment, n as WEBHOOK_SIGNATURE_HEADER, s as hmacSha256Hex, t as WEBHOOK_DELIVERY_HEADER, w as getRejectedUploadMimeTypeError, y as API_ERROR_CODES } from "./CalmLensWebhooks-DLUozliX.js";
2
2
  import * as zod from "zod";
3
+ import "zod/v4";
4
+ //#region ../shared/src/ModerationCategoryDefinitions.ts
5
+ const MODERATION_CATEGORY_DEFINITIONS = {
6
+ sexual: {
7
+ summary: "Sexual acts, explicit sexual anatomy, sexual services, sexual assault depictions and clearly erotic material.",
8
+ definition: "covers sexual acts, explicit sexual anatomy, sexual services and clearly erotic material. Descriptions of sexual assault can qualify without erotic intent. Fictional, illustrated and generated depictions can also qualify.",
9
+ exclusions: "Ordinary affection, swimwear, fitness, fashion, non-erotic medical diagrams and general sexual-health education do not qualify on their own. A person accepting money or wearing revealing clothing does not establish sexual content without a qualifying act, display or offer.",
10
+ docsHref: "/docs/guides/moderation-categories#sexual-content"
11
+ },
12
+ violence: {
13
+ summary: "Physical attacks, injury, death, threats against others, and planning or encouraging physical violence.",
14
+ definition: "covers physical attacks, bodily injury, death and threats against others. Accidental or medical injuries and natural death also count; an attacker or intent to harm is not required. It also includes planning, requesting, encouraging or praising physical violence. A depicted or described act can qualify in fiction or news reporting even when the author condemns it.",
15
+ exclusions: "Solely intentional self-injury or suicide belongs under self-harm. Property damage alone, figurative language, abstract discussion of violence, injury or death and merely possessing a weapon do not establish violence. Consensual sport or training without injury or abuse is excluded.",
16
+ docsHref: "/docs/guides/moderation-categories#violence"
17
+ },
18
+ "self-harm": {
19
+ summary: "Intentional self-injury, suicide, preparation, methods or encouragement; text also includes current or recurring self-harm thoughts.",
20
+ definition: "covers intentional self-injury, suicide, current self-harm risk, preparation, methods and encouragement. For text, it also includes current or recurring self-harm thoughts and an explicitly expressed desire to die. Asking for support does not cancel a separate disclosure of current risk or behavior.",
21
+ exclusions: "For images, require evidence of a self-directed act, preparation, harmful methods or encouragement. An injury or covered body does not establish that harm was self-inflicted. General sadness, neutral statistics, prevention, recovery and healed scars alone are excluded.",
22
+ docsHref: "/docs/guides/moderation-categories#self-harm"
23
+ },
24
+ harassment: {
25
+ summary: "Targeted personal abuse, humiliation, bullying or serious intimidation; ordinary criticism and neutral reporting are excluded.",
26
+ definition: "covers personal abuse, humiliation, bullying and serious intimidation directed at a person or identifiable group. The target can be named, implied or defined by an activity or affiliation. A protected identity is not required.",
27
+ exclusions: "Disagreement, ordinary criticism of conduct or competence, untargeted profanity and neutral reporting of abuse do not qualify without a separate personal attack. Criticism can still qualify when it also contains personal degradation.",
28
+ docsHref: "/docs/guides/moderation-categories#harassment-and-bullying"
29
+ },
30
+ hate: {
31
+ summary: "Identity-based abuse, dehumanization, discriminatory exclusion or threats against people because of protected characteristics.",
32
+ definition: "covers attacks on people because of race, ethnicity, nationality, religion, caste, sex, gender identity, sexual orientation or disability. It includes dehumanization, asserted inferiority, discriminatory exclusion, identity-based abuse and threats.",
33
+ exclusions: "Both the protected target and the identity-based attack must be supported. Identity mentions, cultural clothing, religious imagery, criticism of beliefs or an unrelated threat do not establish hate. Neutral reporting, condemnation and opposition to hate are excluded unless a separate attack is expressed or endorsed.",
34
+ docsHref: "/docs/guides/moderation-categories#hate-and-identity-abuse"
35
+ },
36
+ "criminal-activity": {
37
+ summary: "Planning, assistance, instructions, encouragement or facilitation of a concrete criminal or seriously dangerous act.",
38
+ 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.",
39
+ 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.",
40
+ docsHref: "/docs/guides/moderation-categories#criminal-and-dangerous-activity"
41
+ }
42
+ };
43
+ //#endregion
44
+ //#region ../shared/src/ModerationPolicyDimensions.ts
45
+ const MODERATION_QUALIFIERS = [
46
+ "nudity",
47
+ "sexual-activity",
48
+ "explicit-sexual-language",
49
+ "sexual-solicitation",
50
+ "physical-attack",
51
+ "violent-threat",
52
+ "violent-preparation",
53
+ "violent-encouragement",
54
+ "injury",
55
+ "death",
56
+ "graphic",
57
+ "self-harm-thoughts",
58
+ "self-harm-preparation",
59
+ "self-harm-act",
60
+ "self-harm-methods",
61
+ "self-harm-encouragement",
62
+ "personal-abuse",
63
+ "targeted-intimidation",
64
+ "harassment-threat",
65
+ "sexual-harassment",
66
+ "harassment-encouragement",
67
+ "identity-abuse",
68
+ "dehumanization",
69
+ "identity-inferiority",
70
+ "discriminatory-exclusion",
71
+ "hate-threat",
72
+ "criminal-demonstration",
73
+ "criminal-preparation",
74
+ "criminal-request",
75
+ "criminal-instructions",
76
+ "criminal-promotion",
77
+ "historical",
78
+ "documentary",
79
+ "fictional",
80
+ "support-prevention"
81
+ ];
82
+ const SHARED_QUALIFIERS = [
83
+ "historical",
84
+ "documentary",
85
+ "fictional",
86
+ "support-prevention"
87
+ ];
88
+ [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS];
89
+ //#endregion
90
+ //#region ../../node_modules/zod-meta/esm/index.js
91
+ const createMetaType = (type) => {
92
+ return Object.assign((value) => {
93
+ return {
94
+ type,
95
+ data: value ?? {}
96
+ };
97
+ }, type);
98
+ };
99
+ createMetaType({ id: "docPropertyInfo" });
100
+ //#endregion
101
+ //#region ../shared/src/ZodUtils.ts
102
+ const SIZE_LIMITS = {
103
+ small: 64,
104
+ medium: 200,
105
+ large: 1500,
106
+ xlarge: 1e4,
107
+ token: 1e4
108
+ };
109
+ const saneStringField = (options) => {
110
+ return zod.string().transform((value) => value.trim()).refine((value) => {
111
+ if (options?.minWidth && value.length < options.minWidth) return false;
112
+ return true;
113
+ }, "Not long enough").refine((value) => {
114
+ const sizeLimit = options?.maxLength ?? SIZE_LIMITS[options?.type ?? "medium"];
115
+ return typeof value === "string" && value.length < sizeLimit;
116
+ }, "Too long");
117
+ };
118
+ const CLASSIFICATION_INPUT_TYPE_SCHEMA = zod.enum(["text", "image"]);
119
+ const CLASSIFICATION_CATEGORIES = [
120
+ "sexual",
121
+ "violence",
122
+ "self-harm",
123
+ "harassment",
124
+ "hate",
125
+ "criminal-activity"
126
+ ];
127
+ const CLASSIFICATION_CATEGORY_SCHEMA = zod.enum(CLASSIFICATION_CATEGORIES);
128
+ zod.object({
129
+ category: CLASSIFICATION_CATEGORY_SCHEMA,
130
+ displayName: saneStringField(),
131
+ description: saneStringField(),
132
+ supportedInputTypes: zod.array(CLASSIFICATION_INPUT_TYPE_SCHEMA).min(1)
133
+ });
134
+ MODERATION_CATEGORY_DEFINITIONS.sexual.summary, MODERATION_CATEGORY_DEFINITIONS.violence.summary, MODERATION_CATEGORY_DEFINITIONS["self-harm"].summary, MODERATION_CATEGORY_DEFINITIONS.harassment.summary, MODERATION_CATEGORY_DEFINITIONS.hate.summary, MODERATION_CATEGORY_DEFINITIONS["criminal-activity"].summary;
135
+ const LIKELIHOOD_SCHEMA = zod.enum([
136
+ "very-unlikely",
137
+ "unlikely",
138
+ "possible",
139
+ "likely",
140
+ "very-likely"
141
+ ]);
142
+ const CLASSIFICATION_NOT_EVALUATED_REASON_SCHEMA = zod.enum(["not-supported", "unavailable"]);
143
+ const CLASSIFICATION_QUALIFIER_RESULTS_SCHEMA = zod.partialRecord(zod.enum(MODERATION_QUALIFIERS), zod.discriminatedUnion("status", [zod.object({
144
+ status: zod.literal("evaluated"),
145
+ score: zod.number().min(0).max(1),
146
+ scopeScore: zod.number().min(0).max(1).optional(),
147
+ likelihood: LIKELIHOOD_SCHEMA
148
+ }).strict().refine(({ score, scopeScore }) => scopeScore === void 0 || scopeScore <= score, {
149
+ message: "Only confidence cannot exceed Any confidence",
150
+ path: ["scopeScore"]
151
+ }), zod.object({
152
+ status: zod.literal("not-evaluated"),
153
+ reason: CLASSIFICATION_NOT_EVALUATED_REASON_SCHEMA
154
+ }).strict()]));
155
+ const CLASSIFICATION_CATEGORY_REPORT_SCHEMA = zod.discriminatedUnion("status", [
156
+ zod.object({
157
+ status: zod.literal("evaluated"),
158
+ likelihood: LIKELIHOOD_SCHEMA,
159
+ score: zod.number().min(0).max(1),
160
+ qualifiers: CLASSIFICATION_QUALIFIER_RESULTS_SCHEMA.optional()
161
+ }).strict(),
162
+ zod.object({
163
+ status: zod.literal("partial"),
164
+ likelihood: LIKELIHOOD_SCHEMA,
165
+ score: zod.number().min(0).max(1),
166
+ qualifiers: CLASSIFICATION_QUALIFIER_RESULTS_SCHEMA.optional()
167
+ }).strict(),
168
+ zod.object({
169
+ status: zod.literal("not-evaluated"),
170
+ reason: CLASSIFICATION_NOT_EVALUATED_REASON_SCHEMA,
171
+ qualifiers: CLASSIFICATION_QUALIFIER_RESULTS_SCHEMA.optional()
172
+ }).strict()
173
+ ]);
174
+ const CATEGORIES_SCHEMA = zod.object({
175
+ sexual: CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
176
+ violence: CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
177
+ "self-harm": CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
178
+ harassment: CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
179
+ hate: CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
180
+ "criminal-activity": CLASSIFICATION_CATEGORY_REPORT_SCHEMA
181
+ }).strict();
182
+ const CLASSIFICATION_LABEL_SCHEMA = zod.object({
183
+ description: saneStringField(),
184
+ score: zod.number(),
185
+ topicalityScore: zod.number().nullish(),
186
+ likelihood: LIKELIHOOD_SCHEMA,
187
+ knowledgeBaseId: saneStringField().nullish()
188
+ });
189
+ zod.object({
190
+ processedAt: zod.number(),
191
+ categories: CATEGORIES_SCHEMA,
192
+ flagged: zod.boolean(),
193
+ score: zod.number(),
194
+ notes: saneStringField().nullish(),
195
+ media: zod.object({
196
+ durationSeconds: zod.number().positive(),
197
+ frameIntervalSeconds: zod.number().positive().optional(),
198
+ sampledFrames: zod.number().int().nonnegative(),
199
+ audioTranscribed: zod.boolean(),
200
+ segments: zod.array(zod.object({
201
+ source: zod.enum([
202
+ "frame",
203
+ "speech",
204
+ "frame-text",
205
+ "clip"
206
+ ]),
207
+ startSeconds: zod.number().nonnegative(),
208
+ endSeconds: zod.number().nonnegative(),
209
+ categories: CATEGORIES_SCHEMA
210
+ }).strict())
211
+ }).strict().optional(),
212
+ labels: zod.array(CLASSIFICATION_LABEL_SCHEMA).nullish()
213
+ }).strict();
214
+ //#endregion
215
+ //#region ../shared/src/MediaTestTriggers.ts
216
+ const MEDIA_TEST_SIGNALS = [...CLASSIFICATION_CATEGORIES, "violence.graphic"];
217
+ const HEX_WORDS = [
218
+ "zero",
219
+ "one",
220
+ "two",
221
+ "three",
222
+ "four",
223
+ "five",
224
+ "six",
225
+ "seven",
226
+ "eight",
227
+ "nine",
228
+ "alpha",
229
+ "bravo",
230
+ "charlie",
231
+ "delta",
232
+ "echo",
233
+ "foxtrot"
234
+ ];
235
+ /** Read this aloud, or render it as large, high-contrast text in a video frame. */
236
+ function generateMediaTestCode(signal, projectId) {
237
+ if (!MEDIA_TEST_SIGNALS.includes(signal)) throw new Error("Unsupported media test signal");
238
+ const suffix = projectId.slice(-8).toLowerCase();
239
+ if (!/^[a-f0-9]{8}$/.test(suffix)) throw new Error("Expected a project ID with an eight-character hexadecimal suffix");
240
+ return `Calm Lens test ${[...suffix].map((character) => HEX_WORDS[Number.parseInt(character, 16)]).join(" ")} ${signal.replace(/[.-]/g, " ")} end test`;
241
+ }
242
+ //#endregion
3
243
  //#region ../shared/src/Permissions.ts
4
244
  const ACTION_SCHEMA = zod.enum([
5
245
  "assets:read",
@@ -55,11 +295,10 @@ const permits = (grants, action, folderId, folders) => grants.some((grant) => {
55
295
  let current = folderId;
56
296
  const visited = /* @__PURE__ */ new Set();
57
297
  while (current && !visited.has(current)) {
58
- var _folders$find$parentI, _folders$find;
59
298
  if (current === grant.folderId) return true;
60
299
  if (!grant.includeSubfolders) return false;
61
300
  visited.add(current);
62
- current = (_folders$find$parentI = (_folders$find = folders.find((folder) => folder.id === current)) === null || _folders$find === void 0 ? void 0 : _folders$find.parentId) !== null && _folders$find$parentI !== void 0 ? _folders$find$parentI : null;
301
+ current = folders.find((folder) => folder.id === current)?.parentId ?? null;
63
302
  }
64
303
  return false;
65
304
  });
@@ -83,7 +322,7 @@ const getMockApiKeyGrants = (apiKey) => {
83
322
  if (!apiKey.startsWith("sk_test_")) return [];
84
323
  try {
85
324
  return GRANTS_SCHEMA.parse(JSON.parse(fromBase64Url(apiKey.slice(8))));
86
- } catch (_unused) {
325
+ } catch {
87
326
  return [];
88
327
  }
89
328
  };
@@ -93,50 +332,37 @@ var InMemoryMockImageStorage = class {
93
332
  constructor() {
94
333
  this.content = /* @__PURE__ */ new Map();
95
334
  }
96
- putContent(assetId, content) {
97
- var _this = this;
98
- return _asyncToGenerator(function* () {
99
- const validation = validateAssetPayload(content);
100
- if (!validation.ok) throw new Error(validation.error);
101
- const bytes = yield assetDataToUint8Array(assetToData(content));
102
- _this.content.set(assetId, {
103
- assetId,
104
- parts: /* @__PURE__ */ new Map([[1, bytes]]),
105
- type: content.type
106
- });
107
- })();
335
+ async putContent(assetId, content) {
336
+ const validation = validateAssetPayload(content);
337
+ if (!validation.ok) throw new Error(validation.error);
338
+ const bytes = await assetDataToUint8Array(assetToData(content));
339
+ this.content.set(assetId, {
340
+ assetId,
341
+ parts: /* @__PURE__ */ new Map([[1, bytes]]),
342
+ type: content.type
343
+ });
108
344
  }
109
- putPart(assetId, part) {
110
- var _this2 = this;
111
- return _asyncToGenerator(function* () {
112
- var _this$content$get, _part$partNumber, _part$type;
113
- const validation = validateAssetPayload(part, {
114
- multipart: true,
115
- isFinal: part.isFinal
116
- });
117
- if (!validation.ok) throw new Error(validation.error);
118
- const bytes = yield assetDataToUint8Array(assetToData(part));
119
- const existing = (_this$content$get = _this2.content.get(assetId)) !== null && _this$content$get !== void 0 ? _this$content$get : {
120
- assetId,
121
- parts: /* @__PURE__ */ new Map(),
122
- type: part.type
123
- };
124
- existing.parts.set((_part$partNumber = part.partNumber) !== null && _part$partNumber !== void 0 ? _part$partNumber : 1, bytes);
125
- existing.type = (_part$type = part.type) !== null && _part$type !== void 0 ? _part$type : existing.type;
126
- _this2.content.set(assetId, existing);
127
- })();
128
- }
129
- getContent(assetId) {
130
- var _this3 = this;
131
- return _asyncToGenerator(function* () {
132
- return _this3.content.get(assetId);
133
- })();
134
- }
135
- deleteContent(assetId) {
136
- var _this4 = this;
137
- return _asyncToGenerator(function* () {
138
- _this4.content.delete(assetId);
139
- })();
345
+ async putPart(assetId, part) {
346
+ const validation = validateAssetPayload(part, {
347
+ multipart: true,
348
+ isFinal: part.isFinal
349
+ });
350
+ if (!validation.ok) throw new Error(validation.error);
351
+ const bytes = await assetDataToUint8Array(assetToData(part));
352
+ const existing = this.content.get(assetId) ?? {
353
+ assetId,
354
+ parts: /* @__PURE__ */ new Map(),
355
+ type: part.type
356
+ };
357
+ existing.parts.set(part.partNumber ?? 1, bytes);
358
+ existing.type = part.type ?? existing.type;
359
+ this.content.set(assetId, existing);
360
+ }
361
+ async getContent(assetId) {
362
+ return this.content.get(assetId);
363
+ }
364
+ async deleteContent(assetId) {
365
+ this.content.delete(assetId);
140
366
  }
141
367
  };
142
368
  var InMemoryMockDatabaseStorage = class {
@@ -144,69 +370,39 @@ var InMemoryMockDatabaseStorage = class {
144
370
  this.assets = /* @__PURE__ */ new Map();
145
371
  this.folders = /* @__PURE__ */ new Map();
146
372
  }
147
- createAsset(asset) {
148
- var _this5 = this;
149
- return _asyncToGenerator(function* () {
150
- _this5.assets.set(asset.id, asset);
151
- return asset;
152
- })();
153
- }
154
- updateAsset(asset) {
155
- var _this6 = this;
156
- return _asyncToGenerator(function* () {
157
- _this6.assets.set(asset.id, asset);
158
- return asset;
159
- })();
160
- }
161
- getAsset(assetId) {
162
- var _this7 = this;
163
- return _asyncToGenerator(function* () {
164
- return _this7.assets.get(assetId);
165
- })();
166
- }
167
- listAssets(projectId) {
168
- var _this8 = this;
169
- return _asyncToGenerator(function* () {
170
- return [..._this8.assets.values()].filter((asset) => asset.projectId === projectId);
171
- })();
172
- }
173
- deleteAsset(assetId) {
174
- var _this9 = this;
175
- return _asyncToGenerator(function* () {
176
- _this9.assets.delete(assetId);
177
- })();
178
- }
179
- createFolder(folder) {
180
- var _this10 = this;
181
- return _asyncToGenerator(function* () {
182
- _this10.folders.set(folder.id, folder);
183
- return folder;
184
- })();
185
- }
186
- updateFolder(folder) {
187
- var _this11 = this;
188
- return _asyncToGenerator(function* () {
189
- _this11.folders.set(folder.id, folder);
190
- return folder;
191
- })();
192
- }
193
- getFolder(folderId) {
194
- var _this12 = this;
195
- return _asyncToGenerator(function* () {
196
- return _this12.folders.get(folderId);
197
- })();
198
- }
199
- listFolders(projectId) {
200
- var _this13 = this;
201
- return _asyncToGenerator(function* () {
202
- return [..._this13.folders.values()].filter((folder) => folder.projectId === projectId).sort((a, b) => a.name.localeCompare(b.name));
203
- })();
204
- }
205
- deleteFolder(folderId) {
206
- var _this14 = this;
207
- return _asyncToGenerator(function* () {
208
- _this14.folders.delete(folderId);
209
- })();
373
+ async createAsset(asset) {
374
+ this.assets.set(asset.id, asset);
375
+ return asset;
376
+ }
377
+ async updateAsset(asset) {
378
+ this.assets.set(asset.id, asset);
379
+ return asset;
380
+ }
381
+ async getAsset(assetId) {
382
+ return this.assets.get(assetId);
383
+ }
384
+ async listAssets(projectId) {
385
+ return [...this.assets.values()].filter((asset) => asset.projectId === projectId);
386
+ }
387
+ async deleteAsset(assetId) {
388
+ this.assets.delete(assetId);
389
+ }
390
+ async createFolder(folder) {
391
+ this.folders.set(folder.id, folder);
392
+ return folder;
393
+ }
394
+ async updateFolder(folder) {
395
+ this.folders.set(folder.id, folder);
396
+ return folder;
397
+ }
398
+ async getFolder(folderId) {
399
+ return this.folders.get(folderId);
400
+ }
401
+ async listFolders(projectId) {
402
+ return [...this.folders.values()].filter((folder) => folder.projectId === projectId).sort((a, b) => a.name.localeCompare(b.name));
403
+ }
404
+ async deleteFolder(folderId) {
405
+ this.folders.delete(folderId);
210
406
  }
211
407
  };
212
408
  //#endregion
@@ -233,62 +429,164 @@ const inferAssetType = (options) => {
233
429
  return "file";
234
430
  };
235
431
  const createPage = (items, query) => {
236
- var _query$pageIndex, _query$pageSize;
237
- const pageIndex = (_query$pageIndex = query.pageIndex) !== null && _query$pageIndex !== void 0 ? _query$pageIndex : 0;
238
- const pageSize = (_query$pageSize = query.pageSize) !== null && _query$pageSize !== void 0 ? _query$pageSize : 10;
432
+ const pageIndex = query.pageIndex ?? 0;
433
+ const pageSize = query.pageSize ?? 10;
434
+ if (!Number.isInteger(pageIndex) || pageIndex < 0) throw invalidArgument("pageIndex must be a non-negative integer");
435
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) throw invalidArgument("pageSize must be an integer from 1 to 100");
239
436
  const start = pageIndex * pageSize;
240
437
  return {
241
438
  items: items.slice(start, start + pageSize),
242
439
  total: items.length,
243
440
  pageIndex,
244
441
  pageSize,
245
- ordering: [],
442
+ ordering: query.ordering ?? [],
246
443
  hasMore: start + pageSize < items.length
247
444
  };
248
445
  };
446
+ const compareValues = (left, right) => {
447
+ if (left === right) return 0;
448
+ if (left === void 0 || left === null) return -1;
449
+ if (right === void 0 || right === null) return 1;
450
+ if (typeof left === "number" && typeof right === "number") return left - right;
451
+ return String(left).localeCompare(String(right));
452
+ };
453
+ /** Sorts like the API's `ordering`; without one, newest first. */
454
+ const orderItems = (items, ordering) => {
455
+ const rules = ordering?.length ? ordering : [{
456
+ field: "createdAt",
457
+ direction: "DESC"
458
+ }];
459
+ return [...items].sort((a, b) => {
460
+ for (const rule of rules) {
461
+ const result = compareValues(a[rule.field], b[rule.field]);
462
+ if (result !== 0) return rule.direction === "DESC" ? -result : result;
463
+ }
464
+ return 0;
465
+ });
466
+ };
467
+ const COMPARISON = /^([A-Za-z][\w.]*)(==|!=|=in=|=out=|>=|<=|=gt=|=ge=|=lt=|=le=|>|<)(.+)$/;
468
+ const parseFilterValue = (raw) => {
469
+ const value = raw.trim();
470
+ if (/^".*"$/.test(value) || /^'.*'$/.test(value)) return value.slice(1, -1);
471
+ if (value === "null") return null;
472
+ if (value === "true") return true;
473
+ if (value === "false") return false;
474
+ if (value !== "" && !Number.isNaN(Number(value))) return Number(value);
475
+ return value;
476
+ };
477
+ const readField = (item, path) => path.split(".").reduce((value, key) => value && typeof value === "object" ? value[key] : void 0, item);
478
+ const unsupportedFilter = (expression) => invalidArgument(`MockCalmLensClient cannot evaluate the filter "${expression}". It supports ==, !=, <, <=, >, >=, =in= and =out=, joined by ; (and) or , (or), without grouping parentheses.`);
479
+ const matchesComparison = (item, comparison) => {
480
+ const match = COMPARISON.exec(comparison.trim());
481
+ if (!match) throw unsupportedFilter(comparison);
482
+ const [, field, operator, rawValue] = match;
483
+ const actual = readField(item, field);
484
+ const isList = operator === "=in=" || operator === "=out=";
485
+ if (isList && !/^\(.*\)$/.test(rawValue.trim())) throw unsupportedFilter(comparison);
486
+ const values = isList ? rawValue.trim().slice(1, -1).split(",").map(parseFilterValue) : [parseFilterValue(rawValue)];
487
+ const equals = (expected) => Array.isArray(actual) ? actual.includes(expected) : expected === null ? actual === null || actual === void 0 : actual === expected;
488
+ switch (operator) {
489
+ case "==": return equals(values[0]);
490
+ case "!=": return !equals(values[0]);
491
+ case "=in=": return values.some(equals);
492
+ case "=out=": return !values.some(equals);
493
+ case ">":
494
+ case "=gt=": return compareValues(actual, values[0]) > 0;
495
+ case ">=":
496
+ case "=ge=": return compareValues(actual, values[0]) >= 0;
497
+ case "<":
498
+ case "=lt=": return compareValues(actual, values[0]) < 0;
499
+ default: return compareValues(actual, values[0]) <= 0;
500
+ }
501
+ };
502
+ /** Splits on `separator` outside quotes and parentheses; grouping parentheses are refused. */
503
+ const splitTopLevel = (text, separator) => {
504
+ const parts = [];
505
+ let depth = 0;
506
+ let quote = null;
507
+ let current = "";
508
+ for (const char of text) {
509
+ if (quote) {
510
+ if (char === quote) quote = null;
511
+ } else if (char === "\"" || char === "'") quote = char;
512
+ else if (char === "(") depth++;
513
+ else if (char === ")") depth--;
514
+ else if (char === separator && depth === 0) {
515
+ parts.push(current);
516
+ current = "";
517
+ continue;
518
+ }
519
+ current += char;
520
+ }
521
+ parts.push(current);
522
+ return parts;
523
+ };
524
+ /**
525
+ * Applies an RSQL `filter` like the API for simple expressions: comparisons joined by `;`
526
+ * (and) and `,` (or). Anything it can't evaluate throws instead of returning wrong results.
527
+ */
528
+ const filterAssets = (items, filter) => {
529
+ if (!filter) return items;
530
+ const alternatives = splitTopLevel(filter, ",").map((group) => splitTopLevel(group, ";"));
531
+ for (const condition of alternatives.flat()) if (condition.trim().startsWith("(")) throw unsupportedFilter(condition);
532
+ return items.filter((item) => alternatives.some((conditions) => conditions.every((condition) => matchesComparison(item, condition))));
533
+ };
249
534
  //#endregion
250
535
  //#region src/testing/MockCalmLensClient.ts
251
- const _excluded = ["assetIdentifier"];
252
- const _excluded2 = ["assetIdentifier"];
253
- const _excluded3 = ["assetId"];
254
- const _excluded4 = ["assetId"];
255
- const _excluded5 = ["assetId"];
256
- const _excluded6 = ["folderId"];
257
- const assertValidUploadMimeType = (mimeType) => {
258
- if (isRejectedUploadMimeType(mimeType)) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_MIME_TYPE, {
259
- message: getRejectedUploadMimeTypeError(mimeType),
260
- details: { mimeType }
261
- });
536
+ const DEFAULT_EVENTS_LIMIT = 50;
537
+ /** Stands in for binary payloads when comparing idempotent requests, like the API's size check. */
538
+ const describeBinary = (value) => {
539
+ if (value instanceof Blob) return `[blob:${value.size}]`;
540
+ if (value instanceof ArrayBuffer) return `[bytes:${value.byteLength}]`;
541
+ if (ArrayBuffer.isView(value)) return `[bytes:${value.byteLength}]`;
542
+ return value;
262
543
  };
544
+ const MAX_EVENTS_LIMIT = 100;
263
545
  const toFailureError = (failure) => {
264
- if (failure === "transport-error") return calmLensError({ id: "sdk/transport-error" }, {
546
+ if (failure === "transport-error") return new CalmLensError(SDK_ERROR_CODES.TRANSPORT_ERROR, {
265
547
  message: "Request failed",
266
548
  cause: /* @__PURE__ */ new TypeError("Simulated network failure")
267
549
  });
268
- if (failure === "timeout") return calmLensError({ id: "sdk/timeout" }, {
269
- message: "Request aborted",
270
- cause: new DOMException("The operation timed out.", "TimeoutError")
271
- });
550
+ if (failure === "timeout") {
551
+ const cause = /* @__PURE__ */ new Error("The operation timed out.");
552
+ cause.name = "TimeoutError";
553
+ return new CalmLensError(SDK_ERROR_CODES.TIMEOUT, {
554
+ message: "Request timed out",
555
+ cause
556
+ });
557
+ }
272
558
  return failure;
273
559
  };
560
+ /**
561
+ * An in-memory stand-in for `CalmLensClient` with the same methods, validation and error
562
+ * codes. API errors carry the HTTP status, a `requestId` and response headers like the real
563
+ * client's, so error handling can be tested without a network.
564
+ */
274
565
  var MockCalmLensClient = class {
275
566
  /** Accepts the same options as `CalmLensClient` (validated the same way), plus `mockOptions`. */
276
567
  constructor(options) {
277
- var _options$mockOptions$, _options$mockOptions, _options$mockOptions$2, _options$mockOptions2, _options$mockOptions$3, _options$mockOptions3, _options$mockOptions$4, _options$mockOptions4, _options$mockOptions$5, _options$mockOptions5, _options$mockOptions$6, _options$mockOptions6, _options$mockOptions$7, _options$mockOptions7, _options$cdnBaseUrl;
278
- this.options = options;
279
568
  this.lenses = /* @__PURE__ */ new Map();
569
+ this.eventFeed = [];
570
+ this.uploadQueues = /* @__PURE__ */ new Map();
571
+ this.idempotentResults = /* @__PURE__ */ new Map();
572
+ this.requestCount = 0;
280
573
  this.pendingFailures = [];
281
574
  this.calls = [];
282
575
  validateClientOptions(options);
283
- this.databaseStorage = (_options$mockOptions$ = (_options$mockOptions = options.mockOptions) === null || _options$mockOptions === void 0 ? void 0 : _options$mockOptions.databaseStorage) !== null && _options$mockOptions$ !== void 0 ? _options$mockOptions$ : new InMemoryMockDatabaseStorage();
284
- this.imageStorage = (_options$mockOptions$2 = (_options$mockOptions2 = options.mockOptions) === null || _options$mockOptions2 === void 0 ? void 0 : _options$mockOptions2.imageStorage) !== null && _options$mockOptions$2 !== void 0 ? _options$mockOptions$2 : new InMemoryMockImageStorage();
285
- this.createId = (_options$mockOptions$3 = (_options$mockOptions3 = options.mockOptions) === null || _options$mockOptions3 === void 0 ? void 0 : _options$mockOptions3.createId) !== null && _options$mockOptions$3 !== void 0 ? _options$mockOptions$3 : crypto.randomUUID.bind(crypto);
286
- this.now = (_options$mockOptions$4 = (_options$mockOptions4 = options.mockOptions) === null || _options$mockOptions4 === void 0 ? void 0 : _options$mockOptions4.now) !== null && _options$mockOptions$4 !== void 0 ? _options$mockOptions$4 : Date.now;
287
- this.queuedStatus = (_options$mockOptions$5 = (_options$mockOptions5 = options.mockOptions) === null || _options$mockOptions5 === void 0 ? void 0 : _options$mockOptions5.queuedStatus) !== null && _options$mockOptions$5 !== void 0 ? _options$mockOptions$5 : "pending";
288
- this.analyzedStatus = (_options$mockOptions$6 = (_options$mockOptions6 = options.mockOptions) === null || _options$mockOptions6 === void 0 ? void 0 : _options$mockOptions6.analyzedStatus) !== null && _options$mockOptions$6 !== void 0 ? _options$mockOptions$6 : "approved";
576
+ this.projectId = options.projectId;
577
+ this.databaseStorage = options.mockOptions?.databaseStorage ?? new InMemoryMockDatabaseStorage();
578
+ this.imageStorage = options.mockOptions?.imageStorage ?? new InMemoryMockImageStorage();
579
+ this.createId = options.mockOptions?.createId ?? crypto.randomUUID.bind(crypto);
580
+ this.now = options.mockOptions?.now ?? Date.now;
581
+ this.queuedStatus = options.mockOptions?.queuedStatus ?? "pending";
582
+ this.analyzedStatus = options.mockOptions?.analyzedStatus ?? "approved";
289
583
  this.apiKeyGrants = getMockApiKeyGrants(options.apiKey);
290
- this.signedUrlBaseUrl = (_options$mockOptions$7 = (_options$mockOptions7 = options.mockOptions) === null || _options$mockOptions7 === void 0 ? void 0 : _options$mockOptions7.signedUrlBaseUrl) !== null && _options$mockOptions$7 !== void 0 ? _options$mockOptions$7 : "mock://calmlens";
291
- this.cdnBaseUrl = (_options$cdnBaseUrl = options.cdnBaseUrl) !== null && _options$cdnBaseUrl !== void 0 ? _options$cdnBaseUrl : "mock://calmlens/v1";
584
+ this.signedUrlBaseUrl = options.mockOptions?.signedUrlBaseUrl ?? "mock://calmlens";
585
+ this.cdnBaseUrl = (options.cdnBaseUrl ?? "mock://calmlens/v1").replace(/\/+$/, "");
586
+ }
587
+ /** Keeps the API key out of `console.log` and `util.inspect`. */
588
+ [Symbol.for("nodejs.util.inspect.custom")]() {
589
+ return `MockCalmLensClient { projectId: ${JSON.stringify(this.projectId)} }`;
292
590
  }
293
591
  /**
294
592
  * Makes the next call (or the next call to `operation`) throw `failure` instead of running,
@@ -304,14 +602,54 @@ var MockCalmLensClient = class {
304
602
  callsTo(operation) {
305
603
  return this.calls.filter((call) => call.operation === operation);
306
604
  }
307
- findOwnAsset(assetId) {
308
- var _this = this;
309
- return _asyncToGenerator(function* () {
310
- if (!assetId) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Asset ID is required" });
311
- const asset = yield _this.databaseStorage.getAsset(assetId);
312
- if (!asset || asset.projectId !== _this.options.projectId) throw calmLensError(PUBLIC_ERROR_CODES.ASSET_NOT_FOUND);
313
- return asset;
314
- })();
605
+ /**
606
+ * Adds an event to the feed `events.list` reads, as if CalmLens had recorded it. Returns the
607
+ * stored event; pass it to `toWebhookEventPayload` to build a matching webhook body.
608
+ */
609
+ enqueueEvent(input) {
610
+ const now = this.now();
611
+ const event = {
612
+ id: this.createId(),
613
+ projectId: this.projectId,
614
+ severity: "low",
615
+ title: input.type,
616
+ facts: [],
617
+ createdAt: now,
618
+ updatedAt: now,
619
+ ...input
620
+ };
621
+ this.eventFeed.push(event);
622
+ return event;
623
+ }
624
+ /** An API error as the real client throws it: status, default message and request ID. */
625
+ apiError(code, options = {}) {
626
+ const definition = API_ERROR_DEFINITIONS[code];
627
+ const requestId = `mock-request-${++this.requestCount}`;
628
+ return new CalmLensError(code, {
629
+ message: options.message ?? definition.message,
630
+ status: definition.status,
631
+ details: options.details,
632
+ requestId,
633
+ headers: new Headers({
634
+ "content-type": "application/json",
635
+ "x-request-id": requestId
636
+ })
637
+ });
638
+ }
639
+ assertValidUploadMimeType(mimeType) {
640
+ if (isRejectedUploadMimeType(mimeType)) throw this.apiError(API_ERROR_CODES.INVALID_UPLOAD_MIME_TYPE, {
641
+ message: getRejectedUploadMimeTypeError(mimeType),
642
+ details: { mimeType }
643
+ });
644
+ }
645
+ invalidPayload(message) {
646
+ return this.apiError(API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message });
647
+ }
648
+ async findOwnAsset(assetId) {
649
+ encodePathSegment(assetId, "Asset ID");
650
+ const asset = await this.databaseStorage.getAsset(assetId);
651
+ if (!asset || asset.projectId !== this.projectId) throw this.apiError(API_ERROR_CODES.ASSET_NOT_FOUND);
652
+ return asset;
315
653
  }
316
654
  begin(operation, options) {
317
655
  this.calls.push({
@@ -324,515 +662,545 @@ var MockCalmLensClient = class {
324
662
  throw toFailureError(pending.failure);
325
663
  }
326
664
  }
665
+ jsonResponse(status, body, url) {
666
+ const requestId = `mock-request-${++this.requestCount}`;
667
+ const response = new Response(JSON.stringify(body), {
668
+ status,
669
+ headers: {
670
+ "content-type": "application/json",
671
+ "x-request-id": requestId
672
+ }
673
+ });
674
+ Object.defineProperty(response, "url", { value: url });
675
+ return response;
676
+ }
327
677
  get cdn() {
328
- var _this2 = this;
329
678
  return {
330
- getAssetUrl: (_ref) => {
331
- var _options$resource;
332
- let { assetIdentifier } = _ref, options = _objectWithoutProperties(_ref, _excluded);
333
- if (!assetIdentifier) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Asset identifier is required" });
334
- const resource = (_options$resource = options.resource) !== null && _options$resource !== void 0 ? _options$resource : "content";
335
- const url = `${this.cdnBaseUrl.replace(/\/$/, "")}/assets/${encodeURIComponent(this.options.projectId)}/${encodePathSegment(assetIdentifier, "Asset identifier")}/${resource}`;
679
+ getAssetUrl: ({ assetIdentifier, ...options }) => {
680
+ const resource = options.resource ?? "content";
681
+ const url = `${this.cdnBaseUrl}/assets/${encodeURIComponent(this.projectId)}/${encodePathSegment(assetIdentifier, "Asset identifier")}/${resource}`;
336
682
  return appendCdnSelector(url, options);
337
683
  },
338
- serve: function() {
339
- var _ref2 = _asyncToGenerator(function* (serveOptions) {
340
- var _asset$folderId, _await$this$databaseS, _this$databaseStorage, _this$databaseStorage2, _storedContent$type;
341
- _this2.begin("cdn.serve", serveOptions);
342
- const { assetIdentifier } = serveOptions, options = _objectWithoutProperties(serveOptions, _excluded2);
343
- if (!assetIdentifier) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Asset identifier is required" });
344
- const asset = (yield _this2.databaseStorage.listAssets(_this2.options.projectId)).find((asset) => asset.id === assetIdentifier || asset.externalId === assetIdentifier);
345
- if (!asset) return new Response("Asset not found", { status: 404 });
346
- if (asset.visibility !== "public" && !permits(_this2.apiKeyGrants, "assets:download", (_asset$folderId = asset.folderId) !== null && _asset$folderId !== void 0 ? _asset$folderId : null, (_await$this$databaseS = yield (_this$databaseStorage = (_this$databaseStorage2 = _this2.databaseStorage).listFolders) === null || _this$databaseStorage === void 0 ? void 0 : _this$databaseStorage.call(_this$databaseStorage2, _this2.options.projectId)) !== null && _await$this$databaseS !== void 0 ? _await$this$databaseS : [])) return new Response(JSON.stringify({
347
- code: "asset/api-key-missing-permission",
348
- message: "This API key needs assets:download permission for this asset’s folder."
349
- }), {
350
- status: 403,
351
- headers: { "Content-Type": "application/json" }
352
- });
353
- const storedContent = yield _this2.imageStorage.getContent(asset.id);
354
- if (!storedContent) return new Response("Asset content not found", { status: 404 });
355
- const parts = [...storedContent.parts.entries()].sort(([left], [right]) => left - right).map(([, value]) => value);
356
- const byteLength = parts.reduce((sum, value) => sum + value.byteLength, 0);
357
- const bytes = new Uint8Array(byteLength);
358
- let offset = 0;
359
- for (const part of parts) {
360
- bytes.set(part, offset);
361
- offset += part.byteLength;
684
+ /**
685
+ * Serves stored content like the CDN: `200` with `Content-Type`, `Content-Length`,
686
+ * `ETag` and `X-Request-Id`, or a JSON error body with `404`/`403`. Never throws for
687
+ * HTTP errors, like the real client.
688
+ */
689
+ serve: async (serveOptions) => {
690
+ this.begin("cdn.serve", serveOptions);
691
+ const { assetIdentifier, init, ...options } = serveOptions;
692
+ const url = this.cdn.getAssetUrl({
693
+ assetIdentifier,
694
+ ...options
695
+ });
696
+ const asset = (await this.databaseStorage.listAssets(this.projectId)).find((asset) => asset.id === assetIdentifier || asset.externalId === assetIdentifier);
697
+ const notFound = () => this.jsonResponse(404, {
698
+ code: "asset/not-found",
699
+ message: "Asset not found"
700
+ }, url);
701
+ if (!asset) return notFound();
702
+ if (asset.visibility !== "public" && !permits(this.apiKeyGrants, "assets:download", asset.folderId ?? null, await this.databaseStorage.listFolders?.(this.projectId) ?? [])) return this.jsonResponse(403, {
703
+ code: "asset/api-key-missing-permission",
704
+ message: "This API key needs assets:download permission for this asset’s folder."
705
+ }, url);
706
+ const storedContent = await this.imageStorage.getContent(asset.id);
707
+ if (!storedContent) return notFound();
708
+ const parts = [...storedContent.parts.entries()].sort(([left], [right]) => left - right).map(([, value]) => value);
709
+ const byteLength = parts.reduce((sum, value) => sum + value.byteLength, 0);
710
+ const bytes = new Uint8Array(byteLength);
711
+ let offset = 0;
712
+ for (const part of parts) {
713
+ bytes.set(part, offset);
714
+ offset += part.byteLength;
715
+ }
716
+ const requestId = `mock-request-${++this.requestCount}`;
717
+ const isHead = init?.method?.toUpperCase() === "HEAD";
718
+ const response = new Response(isHead ? null : bytes, {
719
+ status: 200,
720
+ headers: {
721
+ "Content-Type": storedContent.type ?? "application/octet-stream",
722
+ "Content-Length": String(byteLength),
723
+ "Cache-Control": asset.visibility === "public" ? "public, max-age=31536000" : "private, no-store",
724
+ ETag: `"${asset.id}-${asset.updatedAt}"`,
725
+ "X-Request-Id": requestId,
726
+ "X-Mock-CalmLens-CDN-URL": url
362
727
  }
363
- return new Response(bytes, {
364
- status: 200,
365
- headers: {
366
- "Content-Type": (_storedContent$type = storedContent.type) !== null && _storedContent$type !== void 0 ? _storedContent$type : "application/octet-stream",
367
- "X-Mock-CalmLens-CDN-URL": _this2.cdn.getAssetUrl(_objectSpread2({ assetIdentifier }, options))
368
- }
369
- });
370
728
  });
371
- return function serve(_x) {
372
- return _ref2.apply(this, arguments);
373
- };
374
- }()
729
+ Object.defineProperty(response, "url", { value: url });
730
+ return response;
731
+ }
375
732
  };
376
733
  }
377
734
  get events() {
378
- var _this3 = this;
379
- return {
380
- /** The mock does not record events; it always returns an empty page at "now". */
381
- list: function() {
382
- var _ref3 = _asyncToGenerator(function* (options = {}) {
383
- var _options$after;
384
- _this3.begin("events.list", options);
385
- return {
386
- events: [],
387
- cursor: (_options$after = options.after) !== null && _options$after !== void 0 ? _options$after : `${_this3.now()}_`,
388
- hasMore: false
389
- };
735
+ return {
736
+ /**
737
+ * Reads the mock feed like the API: without `after`, no events and a cursor for "now";
738
+ * with `after`, up to `limit` events added since, oldest first. Add events with
739
+ * `enqueueEvent`.
740
+ */
741
+ list: async (options = {}) => {
742
+ this.begin("events.list", options);
743
+ return this.listEvents(options);
744
+ },
745
+ pages: (options = {}) => this.iterateEventPages(options)
746
+ };
747
+ }
748
+ listEvents(options) {
749
+ const limit = options.limit ?? DEFAULT_EVENTS_LIMIT;
750
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_EVENTS_LIMIT) throw this.apiError(API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: `limit must be an integer from 1 to ${MAX_EVENTS_LIMIT}` });
751
+ if (!options.after) return {
752
+ events: [],
753
+ cursor: `mock_${this.eventFeed.length}`,
754
+ hasMore: false
755
+ };
756
+ const match = /^mock_(\d+)$/.exec(options.after);
757
+ const position = match ? Number(match[1]) : NaN;
758
+ if (!Number.isSafeInteger(position) || position > this.eventFeed.length) throw this.apiError(API_ERROR_CODES.INVALID_EVENT_CURSOR);
759
+ const events = this.eventFeed.slice(position, position + limit);
760
+ const next = position + events.length;
761
+ return {
762
+ events,
763
+ cursor: `mock_${next}`,
764
+ hasMore: next < this.eventFeed.length
765
+ };
766
+ }
767
+ async *iterateEventPages(options) {
768
+ let after = options.after;
769
+ for (;;) {
770
+ const page = await this.events.list({
771
+ ...options,
772
+ after
390
773
  });
391
- return function list() {
392
- return _ref3.apply(this, arguments);
393
- };
394
- }() };
774
+ yield page;
775
+ if (!page.hasMore || page.cursor === after) return;
776
+ after = page.cursor;
777
+ }
778
+ }
779
+ async *iteratePages(listPage) {
780
+ for (let pageIndex = 0;; pageIndex++) {
781
+ const page = await listPage(pageIndex);
782
+ yield* page.items;
783
+ if (!page.hasMore || page.items.length === 0) return;
784
+ }
395
785
  }
396
786
  get text() {
397
- var _this4 = this;
398
- return { moderate: function() {
399
- var _ref4 = _asyncToGenerator(function* (options) {
400
- _this4.begin("text.moderate", options);
401
- return _this4.moderateAsset(toTextAssetOptions(options));
402
- });
403
- return function moderate(_x2) {
404
- return _ref4.apply(this, arguments);
405
- };
406
- }() };
787
+ return { moderate: async (options) => {
788
+ this.begin("text.moderate", options);
789
+ return this.moderateAsset(toTextAssetOptions(options), "text.moderate");
790
+ } };
407
791
  }
408
792
  get assets() {
409
- var _this5 = this;
410
793
  return {
411
- list: function() {
412
- var _ref5 = _asyncToGenerator(function* (options = {}) {
413
- var _options$pageIndex;
414
- _this5.begin("assets.list", options);
415
- const assets = yield _this5.databaseStorage.listAssets(_this5.options.projectId);
416
- return createPage(assets, _objectSpread2(_objectSpread2({}, options), {}, { pageIndex: (_options$pageIndex = options.pageIndex) !== null && _options$pageIndex !== void 0 ? _options$pageIndex : 0 }));
417
- });
418
- return function list() {
419
- return _ref5.apply(this, arguments);
420
- };
421
- }(),
422
- retrieve: function() {
423
- var _ref6 = _asyncToGenerator(function* (options) {
424
- _this5.begin("assets.retrieve", options);
425
- const { assetId } = options;
426
- if (!assetId) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Asset ID is required" });
427
- const asset = yield _this5.databaseStorage.getAsset(assetId);
428
- if (!asset) throw calmLensError(PUBLIC_ERROR_CODES.ASSET_NOT_FOUND);
429
- return asset;
430
- });
431
- return function retrieve(_x3) {
432
- return _ref6.apply(this, arguments);
433
- };
434
- }(),
435
- listChildren: function() {
436
- var _ref7 = _asyncToGenerator(function* (childrenOptions = {}) {
437
- var _options$pageIndex2;
438
- _this5.begin("assets.listChildren", childrenOptions);
439
- const { assetId } = childrenOptions, options = _objectWithoutProperties(childrenOptions, _excluded3);
440
- if (!assetId) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Asset ID is required" });
441
- const assets = yield _this5.databaseStorage.listAssets(_this5.options.projectId);
442
- return createPage(assets.filter((asset) => asset.parentId === assetId), _objectSpread2(_objectSpread2({}, options), {}, { pageIndex: (_options$pageIndex2 = options.pageIndex) !== null && _options$pageIndex2 !== void 0 ? _options$pageIndex2 : 0 }));
443
- });
444
- return function listChildren() {
445
- return _ref7.apply(this, arguments);
794
+ /**
795
+ * Matches `query` (case-insensitively) against each asset's name, ID, external ID,
796
+ * description and tags, best match first. Mock scores are 1 for an exact match and 0.5
797
+ * otherwise; `mode` is accepted but there is no semantic search.
798
+ */
799
+ search: async (options) => {
800
+ this.begin("assets.search", options);
801
+ const query = typeof options?.query === "string" ? options.query.trim() : "";
802
+ if (!query || query.length > 200) throw this.invalidPayload("query must be 1 to 200 characters");
803
+ const limit = options.limit ?? 10;
804
+ const needle = query.toLowerCase();
805
+ const fields = [
806
+ "name",
807
+ "id",
808
+ "externalId",
809
+ "description"
810
+ ];
811
+ return {
812
+ query,
813
+ items: (await this.databaseStorage.listAssets(this.projectId)).map((asset) => {
814
+ const matchedFields = [];
815
+ for (const field of fields) if (String(asset[field] ?? "").toLowerCase().includes(needle)) matchedFields.push(field);
816
+ if (asset.tags?.some((tag) => tag.toLowerCase().includes(needle))) matchedFields.push("tags");
817
+ return {
818
+ asset,
819
+ score: fields.some((field) => String(asset[field] ?? "").toLowerCase() === needle) ? 1 : .5,
820
+ matchedFields
821
+ };
822
+ }).filter((result) => result.matchedFields.length > 0).sort((a, b) => b.score - a.score).slice(0, limit),
823
+ semanticStatus: "disabled"
446
824
  };
447
- }(),
448
- update: function() {
449
- var _ref8 = _asyncToGenerator(function* (options) {
450
- _this5.begin("assets.update", options);
451
- const { assetId } = options, body = _objectWithoutProperties(options, _excluded4);
452
- if (!assetId) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Asset ID is required" });
453
- const asset = yield _this5.databaseStorage.getAsset(assetId);
454
- if (!asset || asset.projectId !== _this5.options.projectId) throw calmLensError(PUBLIC_ERROR_CODES.ASSET_NOT_FOUND);
455
- const changes = Object.fromEntries(Object.entries(body).filter(([, value]) => value !== void 0));
456
- return _this5.databaseStorage.updateAsset(_objectSpread2(_objectSpread2(_objectSpread2({}, asset), changes), {}, { updatedAt: _this5.now() }));
825
+ },
826
+ /** Lists assets with the real client's paging, `ordering`, `folderId` and `filter`. */
827
+ list: async (options = {}) => {
828
+ this.begin("assets.list", options);
829
+ let assets = await this.databaseStorage.listAssets(this.projectId);
830
+ if (options.folderId !== void 0) assets = assets.filter((asset) => (asset.folderId ?? null) === options.folderId);
831
+ assets = filterAssets(assets, options.filter);
832
+ return createPage(orderItems(assets, options.ordering), {
833
+ ...options,
834
+ pageIndex: options.pageIndex ?? 0
457
835
  });
458
- return function update(_x4) {
459
- return _ref8.apply(this, arguments);
460
- };
461
- }(),
462
- create: function() {
463
- var _ref9 = _asyncToGenerator(function* (options) {
464
- _this5.begin("assets.create", options);
465
- return _this5.createAsset(options, _this5.queuedStatus);
836
+ },
837
+ iterate: (options = {}) => this.iteratePages((pageIndex) => this.assets.list({
838
+ ...options,
839
+ pageIndex
840
+ })),
841
+ retrieve: async (options) => {
842
+ this.begin("assets.retrieve", options);
843
+ return this.findOwnAsset(options?.assetId);
844
+ },
845
+ listChildren: async (childrenOptions = {}) => {
846
+ this.begin("assets.listChildren", childrenOptions);
847
+ const { assetId, ...options } = childrenOptions;
848
+ encodePathSegment(assetId, "Asset ID");
849
+ const assets = await this.databaseStorage.listAssets(this.projectId);
850
+ return createPage(orderItems(assets.filter((asset) => asset.parentId === assetId), options.ordering), {
851
+ ...options,
852
+ pageIndex: options.pageIndex ?? 0
466
853
  });
467
- return function create(_x5) {
468
- return _ref9.apply(this, arguments);
469
- };
470
- }(),
471
- moderate: function() {
472
- var _ref10 = _asyncToGenerator(function* (options) {
473
- _this5.begin("assets.moderate", options);
474
- return _this5.moderateAsset(options);
854
+ },
855
+ iterateChildren: (options) => {
856
+ encodePathSegment(options?.assetId, "Asset ID");
857
+ return this.iteratePages((pageIndex) => this.assets.listChildren({
858
+ ...options,
859
+ pageIndex
860
+ }));
861
+ },
862
+ update: async (options) => {
863
+ this.begin("assets.update", options);
864
+ const { assetId, ...body } = options;
865
+ const asset = await this.findOwnAsset(assetId);
866
+ const changes = Object.fromEntries(Object.entries(body).filter(([, value]) => value !== void 0));
867
+ return this.databaseStorage.updateAsset({
868
+ ...asset,
869
+ ...changes,
870
+ updatedAt: this.now()
475
871
  });
476
- return function moderate(_x6) {
477
- return _ref10.apply(this, arguments);
478
- };
479
- }(),
480
- uploadPart: function() {
481
- var _ref11 = _asyncToGenerator(function* (options) {
482
- var _asset$uploadPartsRec;
483
- _this5.begin("assets.uploadPart", options);
484
- const { assetId, part } = options;
485
- if (!assetId) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Asset ID is required" });
486
- if (!part) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Part options are required" });
487
- const asset = yield _this5.databaseStorage.getAsset(assetId);
488
- if (!asset) throw calmLensError(PUBLIC_ERROR_CODES.ASSET_NOT_FOUND);
872
+ },
873
+ create: async (options) => {
874
+ this.begin("assets.create", options);
875
+ return this.withIdempotency("assets.create", options, () => this.createAsset(options, true));
876
+ },
877
+ moderate: async (options) => {
878
+ this.begin("assets.moderate", options);
879
+ return this.moderateAsset(options, "assets.moderate");
880
+ },
881
+ /** Continues a multipart upload: parts must arrive in order while it is `uploading`. */
882
+ uploadPart: async (options) => {
883
+ this.begin("assets.uploadPart", options);
884
+ const { assetId, part } = options;
885
+ encodePathSegment(assetId, "Asset ID");
886
+ if (!part) throw invalidArgument("Part options are required");
887
+ return this.withIdempotency("assets.uploadPart", options, async () => {
888
+ const asset = await this.findOwnAsset(assetId);
489
889
  const validation = validateAssetPayload(part, {
490
890
  multipart: true,
491
891
  isFinal: part.isFinal
492
892
  });
493
- if (!validation.ok) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: validation.error });
494
- yield _this5.imageStorage.putPart(assetId, part);
495
- const updated = _objectSpread2(_objectSpread2({}, asset), {}, {
496
- status: part.isFinal ? _this5.queuedStatus : "uploading",
497
- uploadPartsReceived: Math.max((_asset$uploadPartsRec = asset.uploadPartsReceived) !== null && _asset$uploadPartsRec !== void 0 ? _asset$uploadPartsRec : 0, part.partNumber),
498
- updatedAt: _this5.now()
893
+ if (!validation.ok) throw this.invalidPayload(validation.error);
894
+ if (asset.status !== "uploading") throw this.apiError(API_ERROR_CODES.UPLOAD_NOT_OPEN);
895
+ const expectedPartNumber = (asset.uploadPartsReceived ?? 1) + 1;
896
+ if (part.partNumber !== expectedPartNumber) throw this.apiError(API_ERROR_CODES.INVALID_UPLOAD_PART_NUMBER, { details: { expectedPartNumber } });
897
+ await this.imageStorage.putPart(assetId, part);
898
+ const queue = options.queue ?? this.uploadQueues.get(assetId) ?? false;
899
+ if (part.isFinal) this.uploadQueues.delete(assetId);
900
+ return this.databaseStorage.updateAsset({
901
+ ...asset,
902
+ status: part.isFinal ? queue ? this.queuedStatus : this.analyzedStatus : "uploading",
903
+ uploadPartsReceived: part.partNumber,
904
+ updatedAt: this.now()
499
905
  });
500
- return _this5.databaseStorage.updateAsset(updated);
501
- });
502
- return function uploadPart(_x7) {
503
- return _ref11.apply(this, arguments);
504
- };
505
- }(),
506
- createSignedUrl: function() {
507
- var _ref12 = _asyncToGenerator(function* (signedUrlOptions) {
508
- var _options$expiresInSec, _options$resource2;
509
- _this5.begin("assets.createSignedUrl", signedUrlOptions);
510
- const { assetId } = signedUrlOptions, options = _objectWithoutProperties(signedUrlOptions, _excluded5);
511
- if (!assetId) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Asset ID is required" });
512
- if (!(yield _this5.databaseStorage.getAsset(assetId))) throw calmLensError(PUBLIC_ERROR_CODES.ASSET_NOT_FOUND);
513
- const expiresInSeconds = (_options$expiresInSec = options.expiresInSeconds) !== null && _options$expiresInSec !== void 0 ? _options$expiresInSec : 900;
514
- const resource = (_options$resource2 = options.resource) !== null && _options$resource2 !== void 0 ? _options$resource2 : "content";
515
- if (options.lens && options.transform) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Specify either a lens or a transform, not both" });
516
- const selector = options.lens ? `&lens=${encodeURIComponent(options.lens)}` : options.transform ? `&transform=${encodeURIComponent(options.transform)}` : "";
517
- return {
518
- url: `${_this5.signedUrlBaseUrl}/assets/${assetId}/${resource}?expiresInSeconds=${expiresInSeconds}${selector}`,
519
- expiresAt: _this5.now() + expiresInSeconds * 1e3
520
- };
521
906
  });
522
- return function createSignedUrl(_x8) {
523
- return _ref12.apply(this, arguments);
524
- };
525
- }(),
526
- move: function() {
527
- var _ref13 = _asyncToGenerator(function* (options) {
528
- _this5.begin("assets.move", options);
529
- const { assetId, folderId } = options;
530
- if (!assetId) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Asset ID is required" });
531
- const asset = yield _this5.databaseStorage.getAsset(assetId);
532
- if (!asset || asset.projectId !== _this5.options.projectId) throw calmLensError(PUBLIC_ERROR_CODES.ASSET_NOT_FOUND);
533
- if (folderId !== null && folderId !== void 0) {
534
- if (!(yield _this5.resolveFolder(folderId))) throw calmLensError(PUBLIC_ERROR_CODES.FOLDER_NOT_FOUND);
535
- }
536
- const updated = _objectSpread2(_objectSpread2({}, asset), {}, {
537
- folderId: folderId !== null && folderId !== void 0 ? folderId : null,
538
- updatedAt: _this5.now()
539
- });
540
- return _this5.databaseStorage.updateAsset(updated);
541
- });
542
- return function move(_x9) {
543
- return _ref13.apply(this, arguments);
907
+ },
908
+ createSignedUrl: async (signedUrlOptions) => {
909
+ this.begin("assets.createSignedUrl", signedUrlOptions);
910
+ const { assetId, ...options } = signedUrlOptions;
911
+ const asset = await this.findOwnAsset(assetId);
912
+ if (options.lens && options.transform) throw invalidArgument("Specify either a lens or a transform, not both");
913
+ const expiresInSeconds = options.expiresInSeconds ?? 900;
914
+ const resource = options.resource ?? "content";
915
+ const selector = options.lens ? `&lens=${encodeURIComponent(options.lens)}` : options.transform ? `&transform=${encodeURIComponent(options.transform)}` : "";
916
+ return {
917
+ url: `${this.signedUrlBaseUrl}/assets/${asset.id}/${resource}?expiresInSeconds=${expiresInSeconds}${selector}`,
918
+ expiresAt: this.now() + expiresInSeconds * 1e3
544
919
  };
545
- }(),
546
- retry: function() {
547
- var _ref14 = _asyncToGenerator(function* (options) {
548
- _this5.begin("assets.retry", options);
549
- const { assetId } = options;
550
- if (!assetId) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Asset ID is required" });
551
- const asset = yield _this5.databaseStorage.getAsset(assetId);
552
- if (!asset || asset.projectId !== _this5.options.projectId) throw calmLensError(PUBLIC_ERROR_CODES.ASSET_NOT_FOUND);
553
- if (asset.status !== "error") throw calmLensError(PUBLIC_ERROR_CODES.ASSET_NOT_RETRYABLE);
554
- return _this5.databaseStorage.updateAsset(_objectSpread2(_objectSpread2({}, asset), {}, {
555
- status: "pending",
556
- updatedAt: _this5.now()
557
- }));
920
+ },
921
+ move: async (options) => {
922
+ this.begin("assets.move", options);
923
+ const { assetId, folderId } = options;
924
+ const asset = await this.findOwnAsset(assetId);
925
+ if (folderId !== null && folderId !== void 0) {
926
+ if (!await this.resolveFolder(folderId)) throw this.apiError(API_ERROR_CODES.FOLDER_NOT_FOUND);
927
+ }
928
+ return this.databaseStorage.updateAsset({
929
+ ...asset,
930
+ folderId: folderId ?? null,
931
+ updatedAt: this.now()
558
932
  });
559
- return function retry(_x10) {
560
- return _ref14.apply(this, arguments);
561
- };
562
- }(),
563
- deleteContent: function() {
564
- var _ref15 = _asyncToGenerator(function* (options) {
565
- var _asset$storageDeleted;
566
- _this5.begin("assets.deleteContent", options);
567
- const asset = yield _this5.findOwnAsset(options.assetId);
568
- yield _this5.imageStorage.deleteContent(asset.id);
569
- return _this5.databaseStorage.updateAsset(_objectSpread2(_objectSpread2({}, asset), {}, {
570
- extractedText: null,
571
- storageDeletedAt: (_asset$storageDeleted = asset.storageDeletedAt) !== null && _asset$storageDeleted !== void 0 ? _asset$storageDeleted : _this5.now(),
572
- updatedAt: _this5.now()
573
- }));
933
+ },
934
+ retry: async (options) => {
935
+ this.begin("assets.retry", options);
936
+ const asset = await this.findOwnAsset(options?.assetId);
937
+ if (asset.status !== "error") throw this.apiError(API_ERROR_CODES.ASSET_NOT_RETRYABLE);
938
+ if (asset.storageDeletedAt) throw this.apiError(API_ERROR_CODES.ASSET_CONTENT_UNAVAILABLE);
939
+ return this.databaseStorage.updateAsset({
940
+ ...asset,
941
+ status: "pending",
942
+ updatedAt: this.now()
574
943
  });
575
- return function deleteContent(_x11) {
576
- return _ref15.apply(this, arguments);
577
- };
578
- }(),
579
- delete: function() {
580
- var _ref16 = _asyncToGenerator(function* (options) {
581
- _this5.begin("assets.delete", options);
582
- const asset = yield _this5.findOwnAsset(options.assetId);
583
- yield _this5.imageStorage.deleteContent(asset.id);
584
- yield _this5.databaseStorage.deleteAsset(asset.id);
944
+ },
945
+ deleteContent: async (options) => {
946
+ this.begin("assets.deleteContent", options);
947
+ const asset = await this.findOwnAsset(options?.assetId);
948
+ await this.imageStorage.deleteContent(asset.id);
949
+ return this.databaseStorage.updateAsset({
950
+ ...asset,
951
+ extractedText: null,
952
+ storageDeletedAt: asset.storageDeletedAt ?? this.now(),
953
+ updatedAt: this.now()
585
954
  });
586
- return function _delete(_x12) {
587
- return _ref16.apply(this, arguments);
588
- };
589
- }()
955
+ },
956
+ delete: async (options) => {
957
+ this.begin("assets.delete", options);
958
+ const asset = await this.findOwnAsset(options?.assetId);
959
+ await this.imageStorage.deleteContent(asset.id);
960
+ await this.databaseStorage.deleteAsset(asset.id);
961
+ }
590
962
  };
591
963
  }
592
964
  get folders() {
593
- var _this6 = this;
594
965
  return {
595
- list: function() {
596
- var _ref17 = _asyncToGenerator(function* () {
597
- _this6.begin("folders.list", void 0);
598
- return [..._this6.databaseStorage.listFolders ? yield _this6.databaseStorage.listFolders(_this6.options.projectId) : []].sort((a, b) => a.name.localeCompare(b.name));
599
- });
600
- return function list() {
601
- return _ref17.apply(this, arguments);
602
- };
603
- }(),
604
- retrieve: function() {
605
- var _ref18 = _asyncToGenerator(function* (options) {
606
- _this6.begin("folders.retrieve", options);
607
- const { folderId } = options;
608
- if (!folderId) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Folder ID is required" });
609
- const folder = yield _this6.resolveFolder(folderId);
610
- if (!folder) throw calmLensError(PUBLIC_ERROR_CODES.FOLDER_NOT_FOUND);
611
- return folder;
612
- });
613
- return function retrieve(_x13) {
614
- return _ref18.apply(this, arguments);
615
- };
616
- }(),
617
- create: function() {
618
- var _ref19 = _asyncToGenerator(function* (options) {
619
- var _options$parentId, _options$parentId2;
620
- _this6.begin("folders.create", options);
621
- if (!options.name) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Folder name is required" });
622
- if (options.parentId) {
623
- if (!(yield _this6.resolveFolder(options.parentId))) throw calmLensError(PUBLIC_ERROR_CODES.FOLDER_NOT_FOUND);
624
- }
625
- yield _this6.assertUniqueFolderName(options.name, (_options$parentId = options.parentId) !== null && _options$parentId !== void 0 ? _options$parentId : null);
626
- const folder = {
627
- id: _this6.createId(),
628
- projectId: _this6.options.projectId,
629
- parentId: (_options$parentId2 = options.parentId) !== null && _options$parentId2 !== void 0 ? _options$parentId2 : null,
630
- name: options.name,
631
- createdAt: _this6.now(),
632
- updatedAt: _this6.now()
633
- };
634
- if (_this6.databaseStorage.createFolder) return yield _this6.databaseStorage.createFolder(folder);
635
- return folder;
636
- });
637
- return function create(_x14) {
638
- return _ref19.apply(this, arguments);
639
- };
640
- }(),
641
- update: function() {
642
- var _ref20 = _asyncToGenerator(function* (options) {
643
- _this6.begin("folders.update", options);
644
- const { folderId } = options, body = _objectWithoutProperties(options, _excluded6);
645
- if (!folderId) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Folder ID is required" });
646
- const folder = yield _this6.resolveFolder(folderId);
647
- if (!folder) throw calmLensError(PUBLIC_ERROR_CODES.FOLDER_NOT_FOUND);
648
- if (body.name !== void 0 && body.name !== folder.name) {
649
- var _folder$parentId;
650
- yield _this6.assertUniqueFolderName(body.name, (_folder$parentId = folder.parentId) !== null && _folder$parentId !== void 0 ? _folder$parentId : null, folderId);
651
- }
652
- const updated = _objectSpread2(_objectSpread2(_objectSpread2({}, folder), body.name !== void 0 ? { name: body.name } : {}), {}, { updatedAt: _this6.now() });
653
- if (_this6.databaseStorage.updateFolder) return yield _this6.databaseStorage.updateFolder(updated);
654
- return updated;
655
- });
656
- return function update(_x15) {
657
- return _ref20.apply(this, arguments);
966
+ list: async () => {
967
+ this.begin("folders.list", void 0);
968
+ return [...this.databaseStorage.listFolders ? await this.databaseStorage.listFolders(this.projectId) : []].sort((a, b) => a.name.localeCompare(b.name));
969
+ },
970
+ retrieve: async (options) => {
971
+ this.begin("folders.retrieve", options);
972
+ return this.findOwnFolder(options?.folderId);
973
+ },
974
+ create: async (options) => {
975
+ this.begin("folders.create", options);
976
+ if (!options?.name) throw this.invalidPayload("Folder name is required");
977
+ if (options.parentId) await this.findOwnFolder(options.parentId);
978
+ await this.assertUniqueFolderName(options.name, options.parentId ?? null);
979
+ const folder = {
980
+ id: this.createId(),
981
+ projectId: this.projectId,
982
+ parentId: options.parentId ?? null,
983
+ name: options.name,
984
+ createdAt: this.now(),
985
+ updatedAt: this.now()
658
986
  };
659
- }(),
660
- delete: function() {
661
- var _ref21 = _asyncToGenerator(function* (options) {
662
- _this6.begin("folders.delete", options);
663
- const { folderId } = options;
664
- if (!folderId) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Folder ID is required" });
665
- if (!(yield _this6.resolveFolder(folderId))) throw calmLensError(PUBLIC_ERROR_CODES.FOLDER_NOT_FOUND);
666
- const hasAssets = (yield _this6.databaseStorage.listAssets(_this6.options.projectId)).some((asset) => asset.folderId === folderId);
667
- const hasChildren = (_this6.databaseStorage.listFolders ? yield _this6.databaseStorage.listFolders(_this6.options.projectId) : []).some((f) => f.parentId === folderId);
668
- if (hasAssets || hasChildren) throw calmLensError(PUBLIC_ERROR_CODES.FOLDER_NOT_EMPTY, { details: {
669
- hasAssets,
670
- hasChildren
671
- } });
672
- if (_this6.databaseStorage.deleteFolder) yield _this6.databaseStorage.deleteFolder(folderId);
673
- });
674
- return function _delete(_x16) {
675
- return _ref21.apply(this, arguments);
987
+ return this.databaseStorage.createFolder ? await this.databaseStorage.createFolder(folder) : folder;
988
+ },
989
+ update: async (options) => {
990
+ this.begin("folders.update", options);
991
+ const { folderId, ...body } = options;
992
+ const folder = await this.findOwnFolder(folderId);
993
+ if (body.name !== void 0 && body.name !== folder.name) await this.assertUniqueFolderName(body.name, folder.parentId ?? null, folderId);
994
+ const updated = {
995
+ ...folder,
996
+ ...body.name !== void 0 ? { name: body.name } : {},
997
+ updatedAt: this.now()
676
998
  };
677
- }(),
678
- move: function() {
679
- var _ref22 = _asyncToGenerator(function* (options) {
680
- _this6.begin("folders.move", options);
681
- const { folderId, parentId } = options;
682
- if (!folderId) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Folder ID is required" });
683
- const folder = yield _this6.resolveFolder(folderId);
684
- if (!folder) throw calmLensError(PUBLIC_ERROR_CODES.FOLDER_NOT_FOUND);
685
- const targetParentId = parentId !== null && parentId !== void 0 ? parentId : null;
686
- if (targetParentId) {
687
- const parent = yield _this6.resolveFolder(targetParentId);
688
- if (!parent) throw calmLensError(PUBLIC_ERROR_CODES.FOLDER_NOT_FOUND);
689
- let candidate = parent;
690
- const visited = /* @__PURE__ */ new Set();
691
- while (candidate) {
692
- if (candidate.id === folderId || visited.has(candidate.id)) throw calmLensError(PUBLIC_ERROR_CODES.FOLDER_PARENT_CYCLE);
693
- visited.add(candidate.id);
694
- candidate = candidate.parentId ? yield _this6.resolveFolder(candidate.parentId) : void 0;
695
- }
999
+ return this.databaseStorage.updateFolder ? await this.databaseStorage.updateFolder(updated) : updated;
1000
+ },
1001
+ delete: async (options) => {
1002
+ this.begin("folders.delete", options);
1003
+ const { folderId } = options;
1004
+ await this.findOwnFolder(folderId);
1005
+ const hasAssets = (await this.databaseStorage.listAssets(this.projectId)).some((asset) => asset.folderId === folderId);
1006
+ const hasChildren = (this.databaseStorage.listFolders ? await this.databaseStorage.listFolders(this.projectId) : []).some((f) => f.parentId === folderId);
1007
+ if (hasAssets || hasChildren) throw this.apiError(API_ERROR_CODES.FOLDER_NOT_EMPTY, { details: {
1008
+ hasAssets,
1009
+ hasChildren
1010
+ } });
1011
+ await this.databaseStorage.deleteFolder?.(folderId);
1012
+ },
1013
+ move: async (options) => {
1014
+ this.begin("folders.move", options);
1015
+ const { folderId, parentId } = options;
1016
+ const folder = await this.findOwnFolder(folderId);
1017
+ const targetParentId = parentId ?? null;
1018
+ if (targetParentId) {
1019
+ let candidate = await this.findOwnFolder(targetParentId);
1020
+ const visited = /* @__PURE__ */ new Set();
1021
+ while (candidate) {
1022
+ if (candidate.id === folderId || visited.has(candidate.id)) throw this.apiError(API_ERROR_CODES.FOLDER_PARENT_CYCLE);
1023
+ visited.add(candidate.id);
1024
+ candidate = candidate.parentId ? await this.resolveFolder(candidate.parentId) : void 0;
696
1025
  }
697
- if (targetParentId !== folder.parentId) yield _this6.assertUniqueFolderName(folder.name, targetParentId, folderId);
698
- const updated = _objectSpread2(_objectSpread2({}, folder), {}, {
699
- parentId: targetParentId,
700
- updatedAt: _this6.now()
701
- });
702
- if (_this6.databaseStorage.updateFolder) return yield _this6.databaseStorage.updateFolder(updated);
703
- return updated;
704
- });
705
- return function move(_x17) {
706
- return _ref22.apply(this, arguments);
1026
+ }
1027
+ if (targetParentId !== folder.parentId) await this.assertUniqueFolderName(folder.name, targetParentId, folderId);
1028
+ const updated = {
1029
+ ...folder,
1030
+ parentId: targetParentId,
1031
+ updatedAt: this.now()
707
1032
  };
708
- }()
1033
+ return this.databaseStorage.updateFolder ? await this.databaseStorage.updateFolder(updated) : updated;
1034
+ }
709
1035
  };
710
1036
  }
711
- ensureLens(options) {
712
- var _this7 = this;
713
- return _asyncToGenerator(function* () {
714
- _this7.begin("ensureLens", options);
715
- return _this7.ensureLensDefinition(options);
716
- })();
1037
+ async ensureLens(options) {
1038
+ this.begin("ensureLens", options);
1039
+ if (!options?.identifier) throw invalidArgument("Lens identifier is required");
1040
+ return this.ensureLensDefinition(options);
717
1041
  }
718
1042
  ensureLensDefinition(options) {
719
- var _existing$description, _options$description, _existing$transforms, _options$transforms;
720
1043
  const existing = this.lenses.get(options.identifier);
721
1044
  if (!existing) {
722
- const lens = _objectSpread2(_objectSpread2({
1045
+ const lens = {
723
1046
  id: this.createId(),
724
- projectId: this.options.projectId,
725
- userId: null
726
- }, options), {}, {
1047
+ projectId: this.projectId,
1048
+ userId: null,
1049
+ ...options,
727
1050
  createdAt: this.now(),
728
1051
  updatedAt: this.now()
729
- });
1052
+ };
730
1053
  this.lenses.set(lens.identifier, lens);
731
1054
  return lens;
732
1055
  }
733
- if (existing.name === options.name && ((_existing$description = existing.description) !== null && _existing$description !== void 0 ? _existing$description : null) === ((_options$description = options.description) !== null && _options$description !== void 0 ? _options$description : null) && existing.type === options.type && existing.active === options.active && stringifyJsonCanonical((_existing$transforms = existing.transforms) !== null && _existing$transforms !== void 0 ? _existing$transforms : null) === stringifyJsonCanonical((_options$transforms = options.transforms) !== null && _options$transforms !== void 0 ? _options$transforms : null) && existing.version === options.version) return existing;
1056
+ if (existing.name === options.name && (existing.description ?? null) === (options.description ?? null) && existing.type === options.type && existing.active === options.active && stringifyJsonCanonical(existing.transforms ?? null) === stringifyJsonCanonical(options.transforms ?? null) && existing.version === options.version) return existing;
734
1057
  if (options.version > existing.version) {
735
- const lens = _objectSpread2(_objectSpread2(_objectSpread2({}, existing), options), {}, { updatedAt: this.now() });
1058
+ const lens = {
1059
+ ...existing,
1060
+ ...options,
1061
+ updatedAt: this.now()
1062
+ };
736
1063
  this.lenses.set(lens.identifier, lens);
737
1064
  return lens;
738
1065
  }
739
- throw calmLensError(PUBLIC_ERROR_CODES.LENS_VERSION_CONFLICT, { details: {
1066
+ throw this.apiError(API_ERROR_CODES.LENS_VERSION_CONFLICT, { details: {
740
1067
  currentVersion: existing.version,
741
1068
  minimumVersion: existing.version + 1,
742
1069
  requestedVersion: options.version
743
1070
  } });
744
1071
  }
745
- ensureLenses(options) {
746
- var _this8 = this;
747
- return _asyncToGenerator(function* () {
748
- _this8.begin("ensureLenses", options);
749
- if (options.lenses.length === 0) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "At least one lens is required" });
750
- return options.lenses.map((option) => _this8.ensureLensDefinition(option));
751
- })();
752
- }
753
- moderateAsset(options) {
754
- var _this9 = this;
755
- return _asyncToGenerator(function* () {
756
- if ("content" in options && "plainText" in options.content && (typeof options.content.plainText !== "string" || !options.content.plainText.trim())) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Text to moderate must be a non-empty string" });
757
- return _this9.createAsset(options, _this9.analyzedStatus);
758
- })();
759
- }
760
- createAsset(options, status) {
761
- var _this10 = this;
762
- return _asyncToGenerator(function* () {
763
- var _options$folderId, _options$visibility, _options$part$partNum;
764
- if (!("content" in options) && !("url" in options) && !("part" in options)) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Either file content, URL, or multipart part is required" });
765
- if (!options.name) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Asset name is required" });
766
- if (!("part" in options) && "isFinal" in options) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "isFinal is only valid for multipart uploads" });
767
- if ("part" in options && options.part.partNumber && options.part.partNumber > 1) throw calmLensError(PUBLIC_ERROR_CODES.USE_UPLOAD_PART);
768
- if ("content" in options) {
769
- assertValidUploadMimeType(options.content.type);
770
- if (!("plainText" in options.content)) {
771
- const validation = validateAssetPayload(options.content);
772
- if (!validation.ok) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: validation.error });
773
- }
774
- }
775
- if ("part" in options) {
776
- assertValidUploadMimeType(options.part.type);
777
- const validation = validateAssetPayload(options.part, {
778
- multipart: true,
779
- isFinal: options.part.isFinal
780
- });
781
- if (!validation.ok) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: validation.error });
1072
+ async ensureLenses(options) {
1073
+ this.begin("ensureLenses", options);
1074
+ if (!Array.isArray(options?.lenses) || options.lenses.length === 0) throw invalidArgument("At least one lens is required");
1075
+ if (options.lenses.some((lens) => !lens?.identifier)) throw invalidArgument("Lens identifier is required");
1076
+ return options.lenses.map((option) => this.ensureLensDefinition(option));
1077
+ }
1078
+ /** Replays the first result for a reused idempotency key, like the API. */
1079
+ async withIdempotency(operation, options, run) {
1080
+ const key = options?.idempotencyKey;
1081
+ if (key === void 0) return run();
1082
+ if (!key || key.length > 255 || !/^[\x21-\x7e]+$/.test(key)) throw this.apiError(API_ERROR_CODES.INVALID_IDEMPOTENCY_KEY);
1083
+ const fingerprint = `${operation}:${stringifyJsonCanonical(JSON.parse(JSON.stringify(options, (_key, value) => describeBinary(value))))}`;
1084
+ const saved = this.idempotentResults.get(key);
1085
+ if (saved) {
1086
+ if (saved.fingerprint !== fingerprint) throw this.apiError(API_ERROR_CODES.IDEMPOTENCY_KEY_REUSED);
1087
+ return saved.asset;
1088
+ }
1089
+ const asset = await run();
1090
+ this.idempotentResults.set(key, {
1091
+ fingerprint,
1092
+ asset
1093
+ });
1094
+ return asset;
1095
+ }
1096
+ async moderateAsset(options, operation) {
1097
+ if (options && "content" in options && "plainText" in options.content && (typeof options.content.plainText !== "string" || !options.content.plainText.trim())) throw this.invalidPayload("Text to moderate must be a non-empty string");
1098
+ return this.withIdempotency(operation, options, () => this.createAsset(options, false));
1099
+ }
1100
+ async createAsset(options, queue) {
1101
+ if (!options || typeof options !== "object") throw invalidArgument("Asset options are required");
1102
+ if (!("content" in options) && !("url" in options) && !("part" in options)) throw this.invalidPayload("Either file content, URL, or multipart part is required");
1103
+ if (!options.name) throw this.invalidPayload("Asset name is required");
1104
+ if (!("part" in options) && "isFinal" in options) throw this.invalidPayload("isFinal is only valid for multipart uploads");
1105
+ if ("part" in options && options.part.partNumber && options.part.partNumber > 1) throw this.apiError(API_ERROR_CODES.USE_UPLOAD_PART);
1106
+ if ("content" in options) {
1107
+ this.assertValidUploadMimeType(options.content.type);
1108
+ if (!("plainText" in options.content)) {
1109
+ const validation = validateAssetPayload(options.content);
1110
+ if (!validation.ok) throw this.invalidPayload(validation.error);
782
1111
  }
783
- const id = _this10.createId();
784
- const now = _this10.now();
785
- const type = inferAssetType(options);
786
- const sizeInBytes = _this10.getSizeInBytes(options);
787
- const asset = {
788
- id,
789
- projectId: _this10.options.projectId,
790
- folderId: (_options$folderId = options.folderId) !== null && _options$folderId !== void 0 ? _options$folderId : null,
791
- createdAt: now,
792
- updatedAt: now,
793
- name: options.name,
794
- description: options.description,
795
- externalId: options.externalId,
796
- externalUrl: "url" in options ? options.url : "content" in options ? options.externalUrl : void 0,
797
- type,
798
- metadata: options.metadata,
799
- status: "part" in options && !options.part.isFinal ? "uploading" : status,
800
- processingOptions: options.processingOptions,
801
- visibility: (_options$visibility = options.visibility) !== null && _options$visibility !== void 0 ? _options$visibility : "private",
802
- sizeInBytes,
803
- uploadPartsReceived: "part" in options ? (_options$part$partNum = options.part.partNumber) !== null && _options$part$partNum !== void 0 ? _options$part$partNum : 1 : void 0
804
- };
805
- if ("content" in options) {
806
- if (!("plainText" in options.content)) yield _this10.imageStorage.putContent(id, options.content);
807
- } else if ("part" in options) yield _this10.imageStorage.putPart(id, options.part);
808
- return _this10.databaseStorage.createAsset(asset);
809
- })();
810
- }
811
- resolveFolder(folderId) {
812
- var _this11 = this;
813
- return _asyncToGenerator(function* () {
814
- if (!_this11.databaseStorage.getFolder) return void 0;
815
- const folder = yield _this11.databaseStorage.getFolder(folderId);
816
- if (!folder || folder.projectId !== _this11.options.projectId) return;
817
- return folder;
818
- })();
819
- }
820
- assertUniqueFolderName(name, parentId, excludeFolderId) {
821
- var _this12 = this;
822
- return _asyncToGenerator(function* () {
823
- if ((_this12.databaseStorage.listFolders ? yield _this12.databaseStorage.listFolders(_this12.options.projectId) : []).find((f) => f.id !== excludeFolderId && f.parentId === (parentId !== null && parentId !== void 0 ? parentId : null) && f.name === name)) throw calmLensError(PUBLIC_ERROR_CODES.DUPLICATE_FOLDER_NAME, { message: `A folder named "${name}" already exists in this location.` });
824
- })();
1112
+ }
1113
+ if ("part" in options) {
1114
+ this.assertValidUploadMimeType(options.part.type);
1115
+ const validation = validateAssetPayload(options.part, {
1116
+ multipart: true,
1117
+ isFinal: options.part.isFinal
1118
+ });
1119
+ if (!validation.ok) throw this.invalidPayload(validation.error);
1120
+ }
1121
+ if (options.externalId) {
1122
+ if ((await this.databaseStorage.listAssets(this.projectId)).some((asset) => asset.externalId === options.externalId)) throw this.apiError(API_ERROR_CODES.DUPLICATE_EXTERNAL_ID);
1123
+ }
1124
+ const id = this.createId();
1125
+ const now = this.now();
1126
+ const isOpenUpload = "part" in options && !options.part.isFinal;
1127
+ const asset = {
1128
+ id,
1129
+ projectId: this.projectId,
1130
+ folderId: options.folderId ?? null,
1131
+ createdAt: now,
1132
+ updatedAt: now,
1133
+ name: options.name,
1134
+ description: options.description,
1135
+ externalId: options.externalId,
1136
+ externalUrl: "url" in options ? options.url : "content" in options ? options.externalUrl : void 0,
1137
+ type: inferAssetType(options),
1138
+ metadata: options.metadata,
1139
+ status: isOpenUpload ? "uploading" : queue ? this.queuedStatus : this.analyzedStatus,
1140
+ processingOptions: options.processingOptions,
1141
+ visibility: options.visibility ?? "private",
1142
+ sizeInBytes: this.getSizeInBytes(options),
1143
+ uploadPartsReceived: "part" in options ? options.part.partNumber ?? 1 : void 0
1144
+ };
1145
+ if ("content" in options) {
1146
+ if (!("plainText" in options.content)) await this.imageStorage.putContent(id, options.content);
1147
+ } else if ("part" in options) {
1148
+ await this.imageStorage.putPart(id, options.part);
1149
+ if (isOpenUpload) this.uploadQueues.set(id, queue);
1150
+ }
1151
+ return this.databaseStorage.createAsset(asset);
1152
+ }
1153
+ async resolveFolder(folderId) {
1154
+ if (!this.databaseStorage.getFolder) return void 0;
1155
+ const folder = await this.databaseStorage.getFolder(folderId);
1156
+ if (!folder || folder.projectId !== this.projectId) return;
1157
+ return folder;
1158
+ }
1159
+ async findOwnFolder(folderId) {
1160
+ encodePathSegment(folderId, "Folder ID");
1161
+ const folder = await this.resolveFolder(folderId);
1162
+ if (!folder) throw this.apiError(API_ERROR_CODES.FOLDER_NOT_FOUND);
1163
+ return folder;
1164
+ }
1165
+ async assertUniqueFolderName(name, parentId, excludeFolderId) {
1166
+ 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.` });
825
1167
  }
826
1168
  getSizeInBytes(options) {
827
1169
  if ("content" in options) {
828
1170
  if ("plainText" in options.content) return new TextEncoder().encode(options.content.plainText).byteLength;
829
1171
  return getAssetPayloadByteLength(options.content);
830
1172
  }
831
- if ("part" in options) {
832
- var _options$sizeInBytes;
833
- return (_options$sizeInBytes = options.sizeInBytes) !== null && _options$sizeInBytes !== void 0 ? _options$sizeInBytes : getAssetPayloadByteLength(options.part);
834
- }
1173
+ if ("part" in options) return options.part.sizeInBytes ?? options.sizeInBytes ?? getAssetPayloadByteLength(options.part);
1174
+ }
1175
+ };
1176
+ //#endregion
1177
+ //#region src/testing/MockWebhooks.ts
1178
+ /** The JSON body of a webhook delivery for a stored event (or an event from `events.list`). */
1179
+ const toWebhookEventPayload = (event) => ({
1180
+ id: event.id,
1181
+ type: event.type,
1182
+ createdAt: event.createdAt,
1183
+ projectId: event.projectId,
1184
+ data: {
1185
+ severity: event.severity,
1186
+ title: event.title,
1187
+ description: event.description ?? null,
1188
+ assetId: event.assetId ?? null,
1189
+ asset: event.asset ?? null,
1190
+ facts: event.facts
835
1191
  }
1192
+ });
1193
+ /** `v1=<hex HMAC-SHA256 of "<timestamp>.<body>">`, the `x-calmlens-signature` header value. */
1194
+ const createWebhookSignatureHeader = async (secret, timestamp, body) => `v1=${await hmacSha256Hex(secret, `${timestamp}.${body}`)}`;
1195
+ /** Headers for one signed delivery of `body`, as CalmLens sends them. */
1196
+ const createWebhookHeaders = async (options) => {
1197
+ const timestamp = options.timestampSeconds ?? Math.floor(Date.now() / 1e3);
1198
+ return {
1199
+ "content-type": "application/json",
1200
+ [WEBHOOK_DELIVERY_HEADER]: options.deliveryId,
1201
+ [WEBHOOK_TIMESTAMP_HEADER]: String(timestamp),
1202
+ [WEBHOOK_SIGNATURE_HEADER]: await createWebhookSignatureHeader(options.secret, timestamp, options.body)
1203
+ };
836
1204
  };
837
1205
  //#endregion
838
- export { InMemoryMockDatabaseStorage, InMemoryMockImageStorage, MOCK_API_KEY_PREFIX, MockCalmLensClient, createMockApiKey };
1206
+ export { InMemoryMockDatabaseStorage, InMemoryMockImageStorage, MEDIA_TEST_SIGNALS, MOCK_API_KEY_PREFIX, MockCalmLensClient, createMockApiKey, createWebhookHeaders, createWebhookSignatureHeader, generateMediaTestCode, toWebhookEventPayload };