@calmlens/sdk 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +156 -60
- package/cjs/CalmLensTypes-BfweJTMS.d.ts +8584 -0
- package/cjs/CalmLensWebhooks-DJnaLpRy.js +741 -0
- package/cjs/index.d.ts +276 -7525
- package/cjs/index.js +662 -1184
- package/cjs/testing.d.ts +83 -8
- package/cjs/testing.js +991 -596
- package/esm/CalmLensTypes-BfweJTMS.d.ts +8584 -0
- package/esm/CalmLensWebhooks-DLUozliX.js +556 -0
- package/esm/index.d.ts +276 -7525
- package/esm/index.js +648 -1168
- package/esm/testing.d.ts +83 -8
- package/esm/testing.js +960 -592
- package/package.json +24 -18
- package/cjs/CalmLensTypes-CZqyni4p.d.ts +0 -3633
- package/cjs/CalmLensUtils-CArkVfj0.js +0 -637
- package/esm/CalmLensTypes-CZqyni4p.d.ts +0 -3633
- package/esm/CalmLensUtils-Bnikzonc.js +0 -472
package/esm/testing.js
CHANGED
|
@@ -1,5 +1,245 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
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
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
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")
|
|
269
|
-
|
|
270
|
-
cause
|
|
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.
|
|
284
|
-
this.
|
|
285
|
-
this.
|
|
286
|
-
this.
|
|
287
|
-
this.
|
|
288
|
-
this.
|
|
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 =
|
|
291
|
-
this.cdnBaseUrl = (
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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: (
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}()
|
|
729
|
+
Object.defineProperty(response, "url", { value: url });
|
|
730
|
+
return response;
|
|
731
|
+
}
|
|
375
732
|
};
|
|
376
733
|
}
|
|
377
734
|
get events() {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
587
|
-
|
|
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:
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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 =
|
|
1045
|
+
const lens = {
|
|
723
1046
|
id: this.createId(),
|
|
724
|
-
projectId: this.
|
|
725
|
-
userId: null
|
|
726
|
-
|
|
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 && (
|
|
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 =
|
|
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
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
return
|
|
823
|
-
|
|
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
|
-
|
|
833
|
-
|
|
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 };
|