@calmlens/sdk 0.16.3 → 0.17.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/README.md +398 -163
- package/cjs/BrowserUpload-DZ0Vbmxi.js +10 -0
- package/cjs/BrowserUpload-uZjVFGfP.d.ts +11 -0
- package/cjs/CalmLensAppClient-CV_K26Ce.d.ts +40 -0
- package/cjs/CalmLensAppClient-DBGmaDlG.js +89 -0
- package/cjs/CalmLensConnection-K1TAD1Z-.js +659 -0
- package/cjs/CalmLensServerClient-C-pxiz0C.js +645 -0
- package/cjs/CalmLensServerClient-JtP687a3.d.ts +235 -0
- package/cjs/CalmLensTypes-9Vt16pc7.d.ts +40400 -0
- package/cjs/Classification-94kSYWZw-CMnQKW13.d.ts +5873 -0
- package/cjs/CryptoUtils-B_x8lMIx-YBW1-bvL.js +42 -0
- package/cjs/FileIcons-Ob003MdS.js +175 -0
- package/cjs/Jurisdiction-FusEXb4U-zVs21tDv.js +91 -0
- package/cjs/ModerationContext-9lGm-Mna-CcmbRnOy.d.ts +67 -0
- package/cjs/ModerationContextLimits-Dj1YAl3G.js +1311 -0
- package/cjs/ReactUpload-DxtFMr06.js +54 -0
- package/cjs/ReactUpload-nZahpmyg.d.ts +17 -0
- package/cjs/TestTriggers-By07LR31-CmK0yTyR.d.ts +271 -0
- package/cjs/TestTriggers-DcArt0Nu-CvoaONUB.js +1326 -0
- package/cjs/UploadController-BBkN4Ni6.js +618 -0
- package/{esm/index-C9Xw1aid.d.ts → cjs/WebhookPayload-2aL31v0J.d.ts} +131 -155
- package/cjs/app.d.ts +7 -44
- package/cjs/app.js +11 -89
- package/cjs/elements.d.ts +88 -0
- package/cjs/elements.js +555 -0
- package/cjs/file-icons.d.ts +67 -0
- package/cjs/file-icons.js +10 -0
- package/cjs/index-CI4BzN4W.d.ts +252 -0
- package/cjs/index.d.ts +6 -4
- package/cjs/index.js +9 -4
- package/cjs/react-native.d.ts +56 -0
- package/cjs/react-native.js +495 -0
- package/cjs/react.d.ts +30 -0
- package/cjs/react.js +370 -0
- package/cjs/server.d.ts +12 -191
- package/cjs/server.js +18 -378
- package/cjs/test-inputs.d.ts +2 -2
- package/cjs/test-inputs.js +9 -264
- package/cjs/testing.d.ts +285 -190
- package/cjs/testing.js +2393 -1203
- package/cjs/ui-CgZpgpNo.d.ts +526 -0
- package/cjs/ui-KceuoVCM.js +583 -0
- package/cjs/ui.d.ts +2 -0
- package/cjs/ui.js +27 -0
- package/cjs/vue.d.ts +490 -0
- package/cjs/vue.js +419 -0
- package/esm/BrowserUpload-D7-zNmc0.js +5 -0
- package/esm/BrowserUpload-uZjVFGfP.d.ts +11 -0
- package/esm/CalmLensAppClient-CV_K26Ce.d.ts +40 -0
- package/esm/CalmLensAppClient-FVMwrHlH.js +84 -0
- package/esm/CalmLensConnection-iyNQHA8L.js +582 -0
- package/esm/CalmLensServerClient-JtP687a3.d.ts +235 -0
- package/esm/CalmLensServerClient-fQEYKLw4.js +586 -0
- package/esm/CalmLensTypes-9Vt16pc7.d.ts +40400 -0
- package/esm/Classification-94kSYWZw-CMnQKW13.d.ts +5873 -0
- package/esm/CryptoUtils-B_x8lMIx-DhuWGzqn.js +25 -0
- package/esm/FileIcons-M6dzjFxd.js +128 -0
- package/esm/Jurisdiction-FusEXb4U-1ov6m5yE.js +46 -0
- package/esm/ModerationContext-9lGm-Mna-CcmbRnOy.d.ts +67 -0
- package/esm/ModerationContextLimits-D1x_WAtP.js +1114 -0
- package/esm/ReactUpload-D6yBCkcw.js +37 -0
- package/esm/ReactUpload-nZahpmyg.d.ts +17 -0
- package/esm/TestTriggers-By07LR31-CmK0yTyR.d.ts +271 -0
- package/esm/TestTriggers-DcArt0Nu-s0AQZuWE.js +1266 -0
- package/esm/UploadController-txp5NuzH.js +589 -0
- package/{cjs/index-C9Xw1aid.d.ts → esm/WebhookPayload-2aL31v0J.d.ts} +131 -155
- package/esm/app.d.ts +7 -44
- package/esm/app.js +3 -86
- package/esm/elements.d.ts +88 -0
- package/esm/elements.js +528 -0
- package/esm/file-icons.d.ts +67 -0
- package/esm/file-icons.js +1 -0
- package/esm/index-CI4BzN4W.d.ts +252 -0
- package/esm/index.d.ts +6 -4
- package/esm/index.js +1 -1
- package/esm/react-native.d.ts +56 -0
- package/esm/react-native.js +467 -0
- package/esm/react.d.ts +30 -0
- package/esm/react.js +343 -0
- package/esm/server.d.ts +12 -191
- package/esm/server.js +3 -369
- package/esm/styles.css +147 -0
- package/esm/svelte/CalmLensMedia.svelte +134 -0
- package/esm/svelte/CalmLensUploader.svelte +175 -0
- package/esm/svelte/Icon.svelte +7 -0
- package/esm/svelte/index.d.ts +37 -0
- package/esm/svelte/index.js +6 -0
- package/esm/test-inputs.d.ts +2 -2
- package/esm/test-inputs.js +1 -256
- package/esm/testing.d.ts +285 -190
- package/esm/testing.js +2329 -1122
- package/esm/ui-Ca_SpJXf.js +458 -0
- package/esm/ui-CgZpgpNo.d.ts +526 -0
- package/esm/ui.d.ts +2 -0
- package/esm/ui.js +3 -0
- package/esm/vue.d.ts +490 -0
- package/esm/vue.js +392 -0
- package/package.json +110 -11
- package/cjs/CalmLensConnection-BNUHp_AQ.js +0 -234
- package/cjs/CalmLensUtils-o171ownK.js +0 -699
- package/cjs/CalmLensVersion-DwhgxQK5.js +0 -480
- package/cjs/CalmLensWebhooks-BKrq30Yp.js +0 -201
- package/cjs/Classification-BNhgwGEm-DmA9VEuE.d.ts +0 -4806
- package/cjs/TestTriggers-CRKtulyp-vrzEq2wm.d.ts +0 -44
- package/cjs/WebhookPayload-CJoIkGBD.d.ts +0 -11753
- package/esm/CalmLensConnection-XTqmlwzG.js +0 -193
- package/esm/CalmLensUtils-DsF8Aa_x.js +0 -574
- package/esm/CalmLensVersion-DcifRKsx.js +0 -415
- package/esm/CalmLensWebhooks-BvGyk5d3.js +0 -148
- package/esm/Classification-BNhgwGEm-DmA9VEuE.d.ts +0 -4806
- package/esm/TestTriggers-CRKtulyp-vrzEq2wm.d.ts +0 -44
- package/esm/WebhookPayload-CJoIkGBD.d.ts +0 -11753
package/cjs/testing.js
CHANGED
|
@@ -1,34 +1,42 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
-
key = keys[i];
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp$1(to, key, {
|
|
13
|
-
get: ((k) => from[k]).bind(null, key),
|
|
14
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp$1(target, "default", {
|
|
20
|
-
value: mod,
|
|
21
|
-
enumerable: true
|
|
22
|
-
}) : target, mod));
|
|
23
|
-
//#endregion
|
|
24
|
-
const require_CalmLensVersion = require("./CalmLensVersion-DwhgxQK5.js");
|
|
25
|
-
const require_CalmLensWebhooks = require("./CalmLensWebhooks-BKrq30Yp.js");
|
|
26
|
-
const require_CalmLensUtils = require("./CalmLensUtils-o171ownK.js");
|
|
2
|
+
const require_Jurisdiction_FusEXb4U = require("./Jurisdiction-FusEXb4U-zVs21tDv.js");
|
|
3
|
+
const require_ModerationContextLimits = require("./ModerationContextLimits-Dj1YAl3G.js");
|
|
4
|
+
const require_CalmLensServerClient = require("./CalmLensServerClient-C-pxiz0C.js");
|
|
5
|
+
const require_CalmLensConnection = require("./CalmLensConnection-K1TAD1Z-.js");
|
|
6
|
+
const require_CryptoUtils_B_x8lMIx = require("./CryptoUtils-B_x8lMIx-YBW1-bvL.js");
|
|
7
|
+
const require_CalmLensAppClient = require("./CalmLensAppClient-DBGmaDlG.js");
|
|
8
|
+
const require_UploadController = require("./UploadController-BBkN4Ni6.js");
|
|
9
|
+
const require_TestTriggers_DcArt0Nu = require("./TestTriggers-DcArt0Nu-CvoaONUB.js");
|
|
27
10
|
let zod = require("zod");
|
|
28
|
-
zod = __toESM(zod, 1);
|
|
29
|
-
|
|
11
|
+
zod = require_Jurisdiction_FusEXb4U.__toESM(zod, 1);
|
|
12
|
+
// src/contract/MediaTestTriggers.js
|
|
13
|
+
const MEDIA_TEST_SIGNALS = [...require_TestTriggers_DcArt0Nu.CLASSIFICATION_CATEGORIES, "violence.graphic"];
|
|
14
|
+
const HEX_WORDS = [
|
|
15
|
+
"zero",
|
|
16
|
+
"one",
|
|
17
|
+
"two",
|
|
18
|
+
"three",
|
|
19
|
+
"four",
|
|
20
|
+
"five",
|
|
21
|
+
"six",
|
|
22
|
+
"seven",
|
|
23
|
+
"eight",
|
|
24
|
+
"nine",
|
|
25
|
+
"alpha",
|
|
26
|
+
"bravo",
|
|
27
|
+
"charlie",
|
|
28
|
+
"delta",
|
|
29
|
+
"echo",
|
|
30
|
+
"foxtrot"
|
|
31
|
+
];
|
|
32
|
+
/** Read this aloud, or render it as large, high-contrast text in a video frame. */
|
|
33
|
+
async function generateMediaTestCode(signal, testInputKey) {
|
|
34
|
+
if (!MEDIA_TEST_SIGNALS.includes(signal)) throw new RangeError("Unsupported media test signal");
|
|
35
|
+
return `Calm Lens test ${[...await require_TestTriggers_DcArt0Nu.getTestInputTag("media", signal, testInputKey)].map((character) => HEX_WORDS[Number.parseInt(character, 16)]).join(" ")} ${signal.replace(/[.-]/g, " ")} end test`;
|
|
36
|
+
}
|
|
37
|
+
// src/contract/Ids-aHyP9ZcF.js
|
|
30
38
|
var __defProp = Object.defineProperty;
|
|
31
|
-
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
39
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod, mod.exports), cb = null), mod.exports);
|
|
32
40
|
var __exportAll = (all, no_symbols) => {
|
|
33
41
|
let target = {};
|
|
34
42
|
for (var name in all) __defProp(target, name, {
|
|
@@ -38,98 +46,6 @@ var __exportAll = (all, no_symbols) => {
|
|
|
38
46
|
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
39
47
|
return target;
|
|
40
48
|
};
|
|
41
|
-
const MODERATION_CATEGORY_DEFINITIONS = {
|
|
42
|
-
sexual: {
|
|
43
|
-
summary: "Sexual acts, explicit sexual anatomy, sexual services, sexual assault depictions and clearly erotic material.",
|
|
44
|
-
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.",
|
|
45
|
-
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.",
|
|
46
|
-
docsHref: "/docs/guides/moderation-categories#sexual-content"
|
|
47
|
-
},
|
|
48
|
-
violence: {
|
|
49
|
-
summary: "Physical attacks, injury, death, threats against others, and planning or encouraging physical violence.",
|
|
50
|
-
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.",
|
|
51
|
-
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.",
|
|
52
|
-
docsHref: "/docs/guides/moderation-categories#violence"
|
|
53
|
-
},
|
|
54
|
-
"self-harm": {
|
|
55
|
-
summary: "Intentional self-injury, suicide, preparation, methods or encouragement; text also includes current or recurring self-harm thoughts.",
|
|
56
|
-
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.",
|
|
57
|
-
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.",
|
|
58
|
-
docsHref: "/docs/guides/moderation-categories#self-harm"
|
|
59
|
-
},
|
|
60
|
-
harassment: {
|
|
61
|
-
summary: "Targeted personal abuse, humiliation, bullying or serious intimidation; dislike, ordinary criticism and neutral reporting alone are excluded.",
|
|
62
|
-
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.",
|
|
63
|
-
exclusions: "Expressions of dislike or anger, such as \"I hate you\" or \"i hate u\", disagreement, ordinary criticism of conduct or competence, untargeted profanity and neutral reporting of abuse do not qualify on their own. Dislike can still qualify when accompanied by personal degradation, threats, serious intimidation or sustained bullying.",
|
|
64
|
-
docsHref: "/docs/guides/moderation-categories#harassment-and-bullying"
|
|
65
|
-
},
|
|
66
|
-
hate: {
|
|
67
|
-
summary: "Identity-based abuse, dehumanization, discriminatory exclusion or threats against people because of protected characteristics.",
|
|
68
|
-
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.",
|
|
69
|
-
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.",
|
|
70
|
-
docsHref: "/docs/guides/moderation-categories#hate-and-identity-abuse"
|
|
71
|
-
},
|
|
72
|
-
"criminal-activity": {
|
|
73
|
-
summary: "Planning, assistance, instructions, encouragement or facilitation of a concrete criminal or seriously dangerous act, fraud and scams, and illegal drugs or contraband.",
|
|
74
|
-
definition: "covers planning, requests for assistance, instructions, encouragement or facilitation of a concrete criminal or seriously dangerous act. A request can qualify even when no act occurs and a later reply refuses it. Dangerous activity requires a concrete serious risk of physical harm. It also covers fraud, scams and phishing aimed at the reader, illegal drug use, dealing, production or trafficking, and contraband such as stolen goods, counterfeit money or documents and smuggled or illegally traded weapons.",
|
|
75
|
-
exclusions: "Neutral reporting, prevention, lawful safety advice, fiction without criminal assistance, and vague dangerous references do not qualify. Medical, pharmaceutical, clinical, research and educational content does not qualify, including prescriptions, dosages, side effects, controlled medicines used as prescribed, pharmacies, hospitals and drug manufacturing by pharmaceutical companies. Lawful alcohol and tobacco, harm reduction, addiction recovery and drug-policy discussion do not qualify either. Masks, computers, payment cards, detention or ordinary tools do not establish criminal facilitation by themselves. A clear visual demonstration of how to carry out a crime can qualify.",
|
|
76
|
-
docsHref: "/docs/guides/moderation-categories#criminal-and-dangerous-activity"
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
const MODERATION_QUALIFIERS = [
|
|
80
|
-
"sexual-nudity",
|
|
81
|
-
"sexual-activity",
|
|
82
|
-
"sexual-explicit-language",
|
|
83
|
-
"sexual-solicitation",
|
|
84
|
-
"sexual-historical",
|
|
85
|
-
"sexual-documentary",
|
|
86
|
-
"sexual-educational",
|
|
87
|
-
"sexual-fictional",
|
|
88
|
-
"sexual-support-prevention",
|
|
89
|
-
"violence-attack",
|
|
90
|
-
"violence-threat",
|
|
91
|
-
"violence-preparation",
|
|
92
|
-
"violence-encouragement",
|
|
93
|
-
"violence-injury",
|
|
94
|
-
"violence-violent-death",
|
|
95
|
-
"violence-natural-death",
|
|
96
|
-
"violence-graphic",
|
|
97
|
-
"violence-historical",
|
|
98
|
-
"violence-documentary",
|
|
99
|
-
"violence-educational",
|
|
100
|
-
"violence-fictional",
|
|
101
|
-
"violence-support-prevention",
|
|
102
|
-
"self-harm-thoughts",
|
|
103
|
-
"self-harm-preparation",
|
|
104
|
-
"self-harm-act",
|
|
105
|
-
"self-harm-methods",
|
|
106
|
-
"self-harm-encouragement",
|
|
107
|
-
"self-harm-documentary",
|
|
108
|
-
"self-harm-educational",
|
|
109
|
-
"self-harm-fictional",
|
|
110
|
-
"self-harm-support-prevention",
|
|
111
|
-
"harassment-abuse",
|
|
112
|
-
"harassment-intimidation",
|
|
113
|
-
"harassment-threat",
|
|
114
|
-
"harassment-sexual",
|
|
115
|
-
"harassment-encouragement",
|
|
116
|
-
"harassment-fictional",
|
|
117
|
-
"hate-abuse",
|
|
118
|
-
"hate-dehumanization",
|
|
119
|
-
"hate-inferiority",
|
|
120
|
-
"hate-exclusion",
|
|
121
|
-
"hate-threat",
|
|
122
|
-
"hate-historical",
|
|
123
|
-
"hate-fictional",
|
|
124
|
-
"criminal-activity-demonstration",
|
|
125
|
-
"criminal-activity-preparation",
|
|
126
|
-
"criminal-activity-request",
|
|
127
|
-
"criminal-activity-instructions",
|
|
128
|
-
"criminal-activity-promotion",
|
|
129
|
-
"criminal-activity-substances",
|
|
130
|
-
"criminal-activity-fraud",
|
|
131
|
-
"criminal-activity-fictional"
|
|
132
|
-
];
|
|
133
49
|
function getEnumValues(entries) {
|
|
134
50
|
const numericValues = Object.values(entries).filter((v) => typeof v === "number");
|
|
135
51
|
return Object.entries(entries).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v]) => v);
|
|
@@ -462,6 +378,17 @@ function prefixIssues(path, issues) {
|
|
|
462
378
|
function unwrapMessage(message) {
|
|
463
379
|
return typeof message === "string" ? message : message?.message;
|
|
464
380
|
}
|
|
381
|
+
/** The leading size checks of `defChecks` that set an upper bound, each paired with it. */
|
|
382
|
+
function sizeCaps(defChecks) {
|
|
383
|
+
const caps = [];
|
|
384
|
+
for (const ch of defChecks ?? []) {
|
|
385
|
+
const d = ch._zod.def;
|
|
386
|
+
if (!/_(length|size)$|_equals$/.test(d.check)) break;
|
|
387
|
+
const max = d.maximum ?? d.length ?? d.size;
|
|
388
|
+
if (max >= 0) caps.push([max, ch]);
|
|
389
|
+
}
|
|
390
|
+
return caps;
|
|
391
|
+
}
|
|
465
392
|
function attachSchema(issues, start, inst) {
|
|
466
393
|
var _a;
|
|
467
394
|
for (let i = start; i < issues.length; i++) (_a = issues[i]).schema ?? (_a.schema = inst);
|
|
@@ -560,6 +487,7 @@ function hide(inst, key, value) {
|
|
|
560
487
|
return own(inst, key, value, false);
|
|
561
488
|
}
|
|
562
489
|
/** Adds members a table derives from the instance: each builds on first read and shadows as own data, and assignment shadows the same way, as when these were own properties. */
|
|
490
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
563
491
|
function derived(computes, table) {
|
|
564
492
|
for (const key in computes) {
|
|
565
493
|
const compute = computes[key];
|
|
@@ -709,6 +637,7 @@ function newError(Definition) {
|
|
|
709
637
|
}
|
|
710
638
|
return new Definition();
|
|
711
639
|
}
|
|
640
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
712
641
|
function $constructor(name, initializer, proto, params) {
|
|
713
642
|
const zodProto = {};
|
|
714
643
|
function Internals(def) {
|
|
@@ -837,8 +766,8 @@ const initializer$1 = (inst, def) => {
|
|
|
837
766
|
});
|
|
838
767
|
}
|
|
839
768
|
};
|
|
840
|
-
const $ZodError = $constructor("$ZodError", initializer$1);
|
|
841
|
-
const $ZodRealError = $constructor("$ZodError", initializer$1, void 0, { Parent: Error });
|
|
769
|
+
const $ZodError = /* @__PURE__ */ $constructor("$ZodError", initializer$1);
|
|
770
|
+
const $ZodRealError = /* @__PURE__ */ $constructor("$ZodError", initializer$1, void 0, { Parent: Error });
|
|
842
771
|
/** Get-or-create `obj[key]` as an own data property. A path segment naming an inherited member
|
|
843
772
|
* ("toString", "constructor") would otherwise read through to the prototype, and assigning
|
|
844
773
|
* "__proto__" would hit the setter instead of creating a key. */
|
|
@@ -1772,7 +1701,7 @@ var Doc = class {
|
|
|
1772
1701
|
const version = {
|
|
1773
1702
|
major: 4,
|
|
1774
1703
|
minor: 6,
|
|
1775
|
-
patch:
|
|
1704
|
+
patch: 7
|
|
1776
1705
|
};
|
|
1777
1706
|
const $ZodType = /*@__PURE__*/ $constructor("$ZodType", (inst, def) => {
|
|
1778
1707
|
var _a;
|
|
@@ -2464,6 +2393,28 @@ const $ZodDate = /*@__PURE__*/ $constructor("$ZodDate", (inst, def) => {
|
|
|
2464
2393
|
return payload;
|
|
2465
2394
|
};
|
|
2466
2395
|
});
|
|
2396
|
+
function capSize(inst, size) {
|
|
2397
|
+
const caps = sizeCaps(inst._zod.def.checks);
|
|
2398
|
+
if (!caps.length) return;
|
|
2399
|
+
const cap = Math.min(...caps.map((c) => c[0]));
|
|
2400
|
+
const parse = inst._zod.parse;
|
|
2401
|
+
inst._zod.parse = (payload, ctx) => {
|
|
2402
|
+
const n = size(payload.value);
|
|
2403
|
+
if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
|
|
2404
|
+
for (const [max, ch] of sizeCaps(inst._zod.def.checks)) if (n > max && ch._zod.def.when(payload)) {
|
|
2405
|
+
const start = payload.issues.length;
|
|
2406
|
+
ch._zod.check(payload);
|
|
2407
|
+
payload.issues[start].continue = false;
|
|
2408
|
+
attachSchema(payload.issues, start, inst);
|
|
2409
|
+
return payload;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
return parse(payload, ctx);
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
const arraySize = (input) => Array.isArray(input) ? input.length : -1;
|
|
2416
|
+
const mapSize = (input) => input instanceof Map ? input.size : -1;
|
|
2417
|
+
const setSize = (input) => input instanceof Set ? input.size : -1;
|
|
2467
2418
|
function handleArrayResult(result, final, index) {
|
|
2468
2419
|
if (result.issues.length) final.issues.push(...prefixIssues(index, result.issues));
|
|
2469
2420
|
final.value[index] = result.value;
|
|
@@ -2501,6 +2452,7 @@ const $ZodArray = /*@__PURE__*/ $constructor("$ZodArray", (inst, def) => {
|
|
|
2501
2452
|
if (proms.length) return Promise.all(proms).then(() => payload);
|
|
2502
2453
|
return payload;
|
|
2503
2454
|
};
|
|
2455
|
+
capSize(inst, arraySize);
|
|
2504
2456
|
});
|
|
2505
2457
|
function handlePropertyResult(result, final, key, input, optin, optout) {
|
|
2506
2458
|
const isPresent = key in input;
|
|
@@ -2596,8 +2548,8 @@ const $ZodObject = /*@__PURE__*/ $constructor("$ZodObject", (inst, def) => {
|
|
|
2596
2548
|
Object.defineProperty(def, "shape", { get });
|
|
2597
2549
|
}
|
|
2598
2550
|
const _normalized = cached(() => normalizeDef(def));
|
|
2599
|
-
defineLazyInternal(inst, "propValues", (zod
|
|
2600
|
-
const shape = zod
|
|
2551
|
+
defineLazyInternal(inst, "propValues", (zod) => {
|
|
2552
|
+
const shape = zod.def.shape;
|
|
2601
2553
|
const propValues = {};
|
|
2602
2554
|
for (const key in shape) {
|
|
2603
2555
|
const field = shape[key]._zod;
|
|
@@ -2609,7 +2561,7 @@ const $ZodObject = /*@__PURE__*/ $constructor("$ZodObject", (inst, def) => {
|
|
|
2609
2561
|
}
|
|
2610
2562
|
return propValues;
|
|
2611
2563
|
});
|
|
2612
|
-
const isObject$
|
|
2564
|
+
const isObject$1 = isObject;
|
|
2613
2565
|
const catchall = def.catchall;
|
|
2614
2566
|
let value;
|
|
2615
2567
|
const memo = globalConfig.memoizer;
|
|
@@ -2617,7 +2569,7 @@ const $ZodObject = /*@__PURE__*/ $constructor("$ZodObject", (inst, def) => {
|
|
|
2617
2569
|
inst._zod.parse = (payload, ctx) => {
|
|
2618
2570
|
value ?? (value = _normalized.value);
|
|
2619
2571
|
const input = payload.value;
|
|
2620
|
-
if (!isObject$
|
|
2572
|
+
if (!isObject$1(input)) {
|
|
2621
2573
|
payload.issues.push({
|
|
2622
2574
|
expected: "object",
|
|
2623
2575
|
code: "invalid_type",
|
|
@@ -2747,7 +2699,7 @@ const $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
2747
2699
|
return doc.compile();
|
|
2748
2700
|
};
|
|
2749
2701
|
let fastpass;
|
|
2750
|
-
const isObject$
|
|
2702
|
+
const isObject$2 = isObject;
|
|
2751
2703
|
const jit = !globalConfig.jitless;
|
|
2752
2704
|
const fastEnabled = jit && allowsEval.value;
|
|
2753
2705
|
const catchall = def.catchall;
|
|
@@ -2755,7 +2707,7 @@ const $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
2755
2707
|
inst._zod.parse = (payload, ctx) => {
|
|
2756
2708
|
value ?? (value = _normalized.value);
|
|
2757
2709
|
const input = payload.value;
|
|
2758
|
-
if (!isObject$
|
|
2710
|
+
if (!isObject$2(input)) {
|
|
2759
2711
|
payload.issues.push({
|
|
2760
2712
|
expected: "object",
|
|
2761
2713
|
code: "invalid_type",
|
|
@@ -2793,14 +2745,14 @@ function handleUnionResults(results, final, inst, ctx) {
|
|
|
2793
2745
|
}
|
|
2794
2746
|
const $ZodUnion = /*@__PURE__*/ $constructor("$ZodUnion", (inst, def) => {
|
|
2795
2747
|
$ZodType.init(inst, def);
|
|
2796
|
-
defineLazyInternal(inst, "optin", (zod
|
|
2797
|
-
defineLazyInternal(inst, "optout", (zod
|
|
2798
|
-
defineLazyInternal(inst, "values", (zod
|
|
2799
|
-
if (zod
|
|
2800
|
-
});
|
|
2801
|
-
defineLazyInternal(inst, "pattern", (zod
|
|
2802
|
-
if (zod
|
|
2803
|
-
const patterns = zod
|
|
2748
|
+
defineLazyInternal(inst, "optin", (zod) => zod.def.options.some((o) => o._zod.optin === "defaulted") ? "defaulted" : zod.def.options.some((o) => o._zod.optin !== void 0) ? "optional" : void 0);
|
|
2749
|
+
defineLazyInternal(inst, "optout", (zod) => zod.def.options.some((o) => o._zod.optout === "optional") ? "optional" : void 0);
|
|
2750
|
+
defineLazyInternal(inst, "values", (zod) => {
|
|
2751
|
+
if (zod.def.options.every((o) => o._zod.values)) return new Set(zod.def.options.flatMap((option) => Array.from(option._zod.values)));
|
|
2752
|
+
});
|
|
2753
|
+
defineLazyInternal(inst, "pattern", (zod) => {
|
|
2754
|
+
if (zod.def.options.every((o) => o._zod.pattern)) {
|
|
2755
|
+
const patterns = zod.def.options.map((o) => o._zod.pattern);
|
|
2804
2756
|
return new RegExp(`^(${patterns.map((p) => cleanRegex(p.source)).join("|")})$`);
|
|
2805
2757
|
}
|
|
2806
2758
|
});
|
|
@@ -2891,19 +2843,19 @@ const $ZodDiscriminatedUnion = /*@__PURE__*/ $constructor("$ZodDiscriminatedUnio
|
|
|
2891
2843
|
def.inclusive = false;
|
|
2892
2844
|
$ZodUnion.init(inst, def);
|
|
2893
2845
|
const _super = inst._zod.parse;
|
|
2894
|
-
defineLazyInternal(inst, "propValues", (zod
|
|
2846
|
+
defineLazyInternal(inst, "propValues", (zod) => {
|
|
2895
2847
|
const propValues = {};
|
|
2896
2848
|
let undefinedCount = 0;
|
|
2897
|
-
for (const option of zod
|
|
2849
|
+
for (const option of zod.def.options) {
|
|
2898
2850
|
const pv = option._zod.propValues;
|
|
2899
|
-
if (!pv || Object.keys(pv).length === 0) throw new Error(`Invalid discriminated union option at index "${zod
|
|
2900
|
-
if (pv[zod
|
|
2851
|
+
if (!pv || Object.keys(pv).length === 0) throw new Error(`Invalid discriminated union option at index "${zod.def.options.indexOf(option)}"`);
|
|
2852
|
+
if (pv[zod.def.discriminator]?.has(void 0)) undefinedCount++;
|
|
2901
2853
|
for (const [k, v] of Object.entries(pv)) {
|
|
2902
2854
|
if (!Object.prototype.hasOwnProperty.call(propValues, k)) assignProp(propValues, k, /* @__PURE__ */ new Set());
|
|
2903
2855
|
for (const val of v) propValues[k].add(val);
|
|
2904
2856
|
}
|
|
2905
2857
|
}
|
|
2906
|
-
if (!zod
|
|
2858
|
+
if (!zod.def.unionFallback && undefinedCount > 1) propValues[zod.def.discriminator]?.delete(void 0);
|
|
2907
2859
|
return propValues;
|
|
2908
2860
|
});
|
|
2909
2861
|
def.options.forEach((option, i) => {
|
|
@@ -3333,6 +3285,7 @@ const $ZodMap = /*@__PURE__*/ $constructor("$ZodMap", (inst, def) => {
|
|
|
3333
3285
|
if (proms.length) return Promise.all(proms).then(() => payload);
|
|
3334
3286
|
return payload;
|
|
3335
3287
|
};
|
|
3288
|
+
capSize(inst, mapSize);
|
|
3336
3289
|
});
|
|
3337
3290
|
function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
|
|
3338
3291
|
if (keyResult.issues.length) {
|
|
@@ -3392,6 +3345,7 @@ const $ZodSet = /*@__PURE__*/ $constructor("$ZodSet", (inst, def) => {
|
|
|
3392
3345
|
if (proms.length) return Promise.all(proms).then(() => payload);
|
|
3393
3346
|
return payload;
|
|
3394
3347
|
};
|
|
3348
|
+
capSize(inst, setSize);
|
|
3395
3349
|
});
|
|
3396
3350
|
function handleSetResult(result, final) {
|
|
3397
3351
|
if (result.issues.length) final.issues.push(...result.issues);
|
|
@@ -3402,8 +3356,8 @@ const $ZodEnum = /*@__PURE__*/ $constructor("$ZodEnum", (inst, def) => {
|
|
|
3402
3356
|
const values = getEnumValues(def.entries);
|
|
3403
3357
|
const valuesSet = new Set(values);
|
|
3404
3358
|
inst._zod.values = valuesSet;
|
|
3405
|
-
defineLazyInternal(inst, "pattern", (zod
|
|
3406
|
-
const patternValues = getEnumValues(zod
|
|
3359
|
+
defineLazyInternal(inst, "pattern", (zod) => {
|
|
3360
|
+
const patternValues = getEnumValues(zod.def.entries).filter((k) => propertyKeyTypes.has(typeof k));
|
|
3407
3361
|
return new RegExp(patternValues.length ? `^(${patternValues.map((o) => escapeRegex(o.toString())).join("|")})$` : "^[^\\s\\S]$");
|
|
3408
3362
|
});
|
|
3409
3363
|
inst._zod.parse = (payload, _ctx) => {
|
|
@@ -3422,8 +3376,8 @@ const $ZodLiteral = /*@__PURE__*/ $constructor("$ZodLiteral", (inst, def) => {
|
|
|
3422
3376
|
$ZodType.init(inst, def);
|
|
3423
3377
|
const values = new Set(def.values);
|
|
3424
3378
|
inst._zod.values = values;
|
|
3425
|
-
defineLazyInternal(inst, "pattern", (zod
|
|
3426
|
-
const vals = zod
|
|
3379
|
+
defineLazyInternal(inst, "pattern", (zod) => {
|
|
3380
|
+
const vals = zod.def.values;
|
|
3427
3381
|
return new RegExp(vals.length ? `^(${vals.map((o) => typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)).join("|")})$` : "^[^\\s\\S]$");
|
|
3428
3382
|
});
|
|
3429
3383
|
inst._zod.parse = (payload, _ctx) => {
|
|
@@ -3474,14 +3428,14 @@ function handleOptionalResult(payload, result) {
|
|
|
3474
3428
|
}
|
|
3475
3429
|
const $ZodOptional = /*@__PURE__*/ $constructor("$ZodOptional", (inst, def) => {
|
|
3476
3430
|
$ZodType.init(inst, def);
|
|
3477
|
-
defineLazyInternal(inst, "optin", (zod
|
|
3431
|
+
defineLazyInternal(inst, "optin", (zod) => zod.def.innerType._zod.optin === "defaulted" ? "defaulted" : "optional");
|
|
3478
3432
|
inst._zod.optout = "optional";
|
|
3479
|
-
defineLazyInternal(inst, "values", (zod
|
|
3480
|
-
const values = zod
|
|
3433
|
+
defineLazyInternal(inst, "values", (zod) => {
|
|
3434
|
+
const values = zod.def.innerType._zod.values;
|
|
3481
3435
|
return values ? /* @__PURE__ */ new Set([...values, void 0]) : void 0;
|
|
3482
3436
|
});
|
|
3483
|
-
defineLazyInternal(inst, "pattern", (zod
|
|
3484
|
-
const pattern = zod
|
|
3437
|
+
defineLazyInternal(inst, "pattern", (zod) => {
|
|
3438
|
+
const pattern = zod.def.innerType._zod.pattern;
|
|
3485
3439
|
return pattern ? new RegExp(`^(${cleanRegex(pattern.source)})?$`) : void 0;
|
|
3486
3440
|
});
|
|
3487
3441
|
inst._zod.parse = (payload, ctx) => {
|
|
@@ -3499,22 +3453,22 @@ const $ZodOptional = /*@__PURE__*/ $constructor("$ZodOptional", (inst, def) => {
|
|
|
3499
3453
|
});
|
|
3500
3454
|
const $ZodExactOptional = /*@__PURE__*/ $constructor("$ZodExactOptional", (inst, def) => {
|
|
3501
3455
|
$ZodOptional.init(inst, def);
|
|
3502
|
-
defineLazyInternal(inst, "values", (zod
|
|
3503
|
-
defineLazyInternal(inst, "pattern", (zod
|
|
3456
|
+
defineLazyInternal(inst, "values", (zod) => zod.def.innerType._zod.values);
|
|
3457
|
+
defineLazyInternal(inst, "pattern", (zod) => zod.def.innerType._zod.pattern);
|
|
3504
3458
|
inst._zod.parse = (payload, ctx) => {
|
|
3505
3459
|
return def.innerType._zod.run(payload, ctx);
|
|
3506
3460
|
};
|
|
3507
3461
|
});
|
|
3508
3462
|
const $ZodNullable = /*@__PURE__*/ $constructor("$ZodNullable", (inst, def) => {
|
|
3509
3463
|
$ZodType.init(inst, def);
|
|
3510
|
-
defineLazyInternal(inst, "optin", (zod
|
|
3511
|
-
defineLazyInternal(inst, "optout", (zod
|
|
3512
|
-
defineLazyInternal(inst, "pattern", (zod
|
|
3513
|
-
const pattern = zod
|
|
3464
|
+
defineLazyInternal(inst, "optin", (zod) => zod.def.innerType._zod.optin);
|
|
3465
|
+
defineLazyInternal(inst, "optout", (zod) => zod.def.innerType._zod.optout);
|
|
3466
|
+
defineLazyInternal(inst, "pattern", (zod) => {
|
|
3467
|
+
const pattern = zod.def.innerType._zod.pattern;
|
|
3514
3468
|
return pattern ? new RegExp(`^(${cleanRegex(pattern.source)}|null)$`) : void 0;
|
|
3515
3469
|
});
|
|
3516
|
-
defineLazyInternal(inst, "values", (zod
|
|
3517
|
-
return zod
|
|
3470
|
+
defineLazyInternal(inst, "values", (zod) => {
|
|
3471
|
+
return zod.def.innerType._zod.values ? /* @__PURE__ */ new Set([...zod.def.innerType._zod.values, null]) : void 0;
|
|
3518
3472
|
});
|
|
3519
3473
|
inst._zod.parse = (payload, ctx) => {
|
|
3520
3474
|
if (payload.value === null) return payload;
|
|
@@ -3524,7 +3478,7 @@ const $ZodNullable = /*@__PURE__*/ $constructor("$ZodNullable", (inst, def) => {
|
|
|
3524
3478
|
const $ZodDefault = /*@__PURE__*/ $constructor("$ZodDefault", (inst, def) => {
|
|
3525
3479
|
$ZodType.init(inst, def);
|
|
3526
3480
|
inst._zod.optin = "defaulted";
|
|
3527
|
-
defineLazyInternal(inst, "values", (zod
|
|
3481
|
+
defineLazyInternal(inst, "values", (zod) => zod.def.innerType._zod.values);
|
|
3528
3482
|
inst._zod.parse = (payload, ctx) => {
|
|
3529
3483
|
if (ctx.direction === "backward") return def.innerType._zod.run(payload, ctx);
|
|
3530
3484
|
if (payload.value === void 0) {
|
|
@@ -3546,7 +3500,7 @@ function handleDefaultResult(payload, def) {
|
|
|
3546
3500
|
const $ZodPrefault = /*@__PURE__*/ $constructor("$ZodPrefault", (inst, def) => {
|
|
3547
3501
|
$ZodType.init(inst, def);
|
|
3548
3502
|
inst._zod.optin = "defaulted";
|
|
3549
|
-
defineLazyInternal(inst, "values", (zod
|
|
3503
|
+
defineLazyInternal(inst, "values", (zod) => zod.def.innerType._zod.values);
|
|
3550
3504
|
inst._zod.parse = (payload, ctx) => {
|
|
3551
3505
|
if (ctx.direction === "backward") return def.innerType._zod.run(payload, ctx);
|
|
3552
3506
|
if (payload.value === void 0) payload.value = def.defaultValue;
|
|
@@ -3555,8 +3509,8 @@ const $ZodPrefault = /*@__PURE__*/ $constructor("$ZodPrefault", (inst, def) => {
|
|
|
3555
3509
|
});
|
|
3556
3510
|
const $ZodNonOptional = /*@__PURE__*/ $constructor("$ZodNonOptional", (inst, def) => {
|
|
3557
3511
|
$ZodType.init(inst, def);
|
|
3558
|
-
defineLazyInternal(inst, "values", (zod
|
|
3559
|
-
const v = zod
|
|
3512
|
+
defineLazyInternal(inst, "values", (zod) => {
|
|
3513
|
+
const v = zod.def.innerType._zod.values;
|
|
3560
3514
|
return v ? new Set([...v].filter((x) => x !== void 0)) : void 0;
|
|
3561
3515
|
});
|
|
3562
3516
|
inst._zod.parse = (payload, ctx) => {
|
|
@@ -3603,9 +3557,9 @@ function handleCatchResult(payload, result, def, ctx) {
|
|
|
3603
3557
|
}
|
|
3604
3558
|
const $ZodCatch = /*@__PURE__*/ $constructor("$ZodCatch", (inst, def) => {
|
|
3605
3559
|
$ZodType.init(inst, def);
|
|
3606
|
-
defineLazyInternal(inst, "optin", (zod
|
|
3607
|
-
defineLazyInternal(inst, "optout", (zod
|
|
3608
|
-
defineLazyInternal(inst, "values", (zod
|
|
3560
|
+
defineLazyInternal(inst, "optin", (zod) => zod.def.innerType._zod.optin === "defaulted" ? "defaulted" : "optional");
|
|
3561
|
+
defineLazyInternal(inst, "optout", (zod) => zod.def.innerType._zod.optout);
|
|
3562
|
+
defineLazyInternal(inst, "values", (zod) => zod.def.innerType._zod.values);
|
|
3609
3563
|
inst._zod.parse = (payload, ctx) => {
|
|
3610
3564
|
if (ctx.direction === "backward") return def.innerType._zod.run(payload, ctx);
|
|
3611
3565
|
const result = def.innerType._zod.run({
|
|
@@ -3633,10 +3587,10 @@ const $ZodNaN = /*@__PURE__*/ $constructor("$ZodNaN", (inst, def) => {
|
|
|
3633
3587
|
});
|
|
3634
3588
|
const $ZodPipe = /*@__PURE__*/ $constructor("$ZodPipe", (inst, def) => {
|
|
3635
3589
|
$ZodType.init(inst, def);
|
|
3636
|
-
defineLazyInternal(inst, "values", (zod
|
|
3637
|
-
defineLazyInternal(inst, "optin", (zod
|
|
3638
|
-
defineLazyInternal(inst, "optout", (zod
|
|
3639
|
-
defineLazyInternal(inst, "propValues", (zod
|
|
3590
|
+
defineLazyInternal(inst, "values", (zod) => zod.def.in._zod.values);
|
|
3591
|
+
defineLazyInternal(inst, "optin", (zod) => zod.def.in._zod.optin);
|
|
3592
|
+
defineLazyInternal(inst, "optout", (zod) => zod.def.out._zod.optout);
|
|
3593
|
+
defineLazyInternal(inst, "propValues", (zod) => zod.def.in._zod.propValues);
|
|
3640
3594
|
inst._zod.parse = (payload, ctx) => {
|
|
3641
3595
|
if (ctx.direction === "backward") {
|
|
3642
3596
|
const right = def.out._zod.run(payload, ctx);
|
|
@@ -3660,10 +3614,10 @@ function handlePipeResult(left, next, ctx) {
|
|
|
3660
3614
|
}
|
|
3661
3615
|
const $ZodCodec = /*@__PURE__*/ $constructor("$ZodCodec", (inst, def) => {
|
|
3662
3616
|
$ZodType.init(inst, def);
|
|
3663
|
-
defineLazyInternal(inst, "values", (zod
|
|
3664
|
-
defineLazyInternal(inst, "optin", (zod
|
|
3665
|
-
defineLazyInternal(inst, "optout", (zod
|
|
3666
|
-
defineLazyInternal(inst, "propValues", (zod
|
|
3617
|
+
defineLazyInternal(inst, "values", (zod) => zod.def.in._zod.values);
|
|
3618
|
+
defineLazyInternal(inst, "optin", (zod) => zod.def.in._zod.optin);
|
|
3619
|
+
defineLazyInternal(inst, "optout", (zod) => zod.def.out._zod.optout);
|
|
3620
|
+
defineLazyInternal(inst, "propValues", (zod) => zod.def.in._zod.propValues);
|
|
3667
3621
|
inst._zod.parse = (payload, ctx) => {
|
|
3668
3622
|
if ((ctx.direction || "forward") === "forward") {
|
|
3669
3623
|
const left = def.in._zod.run(payload, ctx);
|
|
@@ -3706,10 +3660,10 @@ const $ZodPreprocess = /*@__PURE__*/ $constructor("$ZodPreprocess", (inst, def)
|
|
|
3706
3660
|
});
|
|
3707
3661
|
const $ZodReadonly = /*@__PURE__*/ $constructor("$ZodReadonly", (inst, def) => {
|
|
3708
3662
|
$ZodType.init(inst, def);
|
|
3709
|
-
defineLazyInternal(inst, "propValues", (zod
|
|
3710
|
-
defineLazyInternal(inst, "values", (zod
|
|
3711
|
-
defineLazyInternal(inst, "optin", (zod
|
|
3712
|
-
defineLazyInternal(inst, "optout", (zod
|
|
3663
|
+
defineLazyInternal(inst, "propValues", (zod) => zod.def.innerType._zod.propValues);
|
|
3664
|
+
defineLazyInternal(inst, "values", (zod) => zod.def.innerType._zod.values);
|
|
3665
|
+
defineLazyInternal(inst, "optin", (zod) => zod.def.innerType?._zod?.optin);
|
|
3666
|
+
defineLazyInternal(inst, "optout", (zod) => zod.def.innerType?._zod?.optout);
|
|
3713
3667
|
inst._zod.parse = (payload, ctx) => {
|
|
3714
3668
|
if (ctx.direction === "backward") return def.innerType._zod.run(payload, ctx);
|
|
3715
3669
|
const result = def.innerType._zod.run(payload, ctx);
|
|
@@ -3880,10 +3834,10 @@ const $ZodLazy = /*@__PURE__*/ $constructor("$ZodLazy", (inst, def) => {
|
|
|
3880
3834
|
if (!d._cachedInner) d._cachedInner = def.getter();
|
|
3881
3835
|
return d._cachedInner;
|
|
3882
3836
|
});
|
|
3883
|
-
defineLazyInternal(inst, "pattern", (zod
|
|
3884
|
-
defineLazyInternal(inst, "propValues", (zod
|
|
3885
|
-
defineLazyInternal(inst, "optin", (zod
|
|
3886
|
-
defineLazyInternal(inst, "optout", (zod
|
|
3837
|
+
defineLazyInternal(inst, "pattern", (zod) => zod.innerType?._zod?.pattern);
|
|
3838
|
+
defineLazyInternal(inst, "propValues", (zod) => zod.innerType?._zod?.propValues);
|
|
3839
|
+
defineLazyInternal(inst, "optin", (zod) => zod.innerType?._zod?.optin ?? void 0);
|
|
3840
|
+
defineLazyInternal(inst, "optout", (zod) => zod.innerType?._zod?.optout ?? void 0);
|
|
3887
3841
|
inst._zod.parse = (payload, ctx) => {
|
|
3888
3842
|
return inst._zod.innerType._zod.run(payload, ctx);
|
|
3889
3843
|
};
|
|
@@ -7877,122 +7831,6 @@ ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
|
7877
7831
|
...schemas_exports,
|
|
7878
7832
|
...checks_exports
|
|
7879
7833
|
});
|
|
7880
|
-
const ZOD_META_KEY = "__zod_meta__";
|
|
7881
|
-
const createMetaType = (type) => {
|
|
7882
|
-
return Object.assign((value) => {
|
|
7883
|
-
return {
|
|
7884
|
-
type,
|
|
7885
|
-
data: value ?? {}
|
|
7886
|
-
};
|
|
7887
|
-
}, type);
|
|
7888
|
-
};
|
|
7889
|
-
const metaStore = (meta) => {
|
|
7890
|
-
const metaMap = meta.reduce((acc, meta) => {
|
|
7891
|
-
if (meta) acc[meta.type.id] = meta;
|
|
7892
|
-
return acc;
|
|
7893
|
-
}, {});
|
|
7894
|
-
return { [ZOD_META_KEY]: {
|
|
7895
|
-
itemMap: metaMap,
|
|
7896
|
-
get itemList() {
|
|
7897
|
-
return Object.values(metaMap);
|
|
7898
|
-
}
|
|
7899
|
-
} };
|
|
7900
|
-
};
|
|
7901
|
-
const docPropertyInfo = createMetaType({ id: "docPropertyInfo" });
|
|
7902
|
-
/**
|
|
7903
|
-
* Optional analyses a request can turn on with `processingOptions.features.<id>`. The catalog is
|
|
7904
|
-
* the single source for the request and result schemas, cache keys and metering. Features are
|
|
7905
|
-
* chosen per request only; there is no project-level default.
|
|
7906
|
-
*/
|
|
7907
|
-
/** How mature a feature is. Beta results may change shape. */
|
|
7908
|
-
const PROCESSING_FEATURE_STAGES = ["beta", "ga"];
|
|
7909
|
-
const createFeatureOptionsSchemas = (booleanSchema) => {
|
|
7910
|
-
const enabled = booleanSchema.meta(metaStore([docPropertyInfo({ description: "Whether to run the feature. Every feature is off unless this is true." })]));
|
|
7911
|
-
return {
|
|
7912
|
-
ocr: zod.object({ enabled }).strict().meta(metaStore([docPropertyInfo({
|
|
7913
|
-
description: "Extract text onto the image asset and run an additional text moderation pass. Applies to images, including those extracted from multimedia documents and websites.",
|
|
7914
|
-
namedType: "OcrFeatureOptions"
|
|
7915
|
-
})])),
|
|
7916
|
-
personDetection: zod.object({ enabled }).strict().meta(metaStore([docPropertyInfo({
|
|
7917
|
-
description: "Beta. Locate people in images, including people whose faces are not visible. Results are informational and never change the moderation decision.",
|
|
7918
|
-
namedType: "PersonDetectionFeatureOptions"
|
|
7919
|
-
})])),
|
|
7920
|
-
faceDetection: zod.object({
|
|
7921
|
-
enabled,
|
|
7922
|
-
ageEstimation: booleanSchema.optional().meta(metaStore([docPropertyInfo({
|
|
7923
|
-
description: "Also estimate an age range for each face",
|
|
7924
|
-
defaultValue: false
|
|
7925
|
-
})]))
|
|
7926
|
-
}).strict().meta(metaStore([docPropertyInfo({
|
|
7927
|
-
description: "Beta. Locate faces in images and optionally estimate an age range for each. Faces are not identified or stored as biometric templates. Results are informational and never change the moderation decision.",
|
|
7928
|
-
namedType: "FaceDetectionFeatureOptions"
|
|
7929
|
-
})]))
|
|
7930
|
-
};
|
|
7931
|
-
};
|
|
7932
|
-
const createProcessingFeaturesSchema = (booleanSchema) => {
|
|
7933
|
-
const options = createFeatureOptionsSchemas(booleanSchema);
|
|
7934
|
-
return zod.object({
|
|
7935
|
-
ocr: options.ocr.nullish(),
|
|
7936
|
-
personDetection: options.personDetection.nullish(),
|
|
7937
|
-
faceDetection: options.faceDetection.nullish()
|
|
7938
|
-
}).strict().meta(metaStore([docPropertyInfo({
|
|
7939
|
-
description: "Optional analyses to run on this asset, keyed by feature. Unknown features are rejected.",
|
|
7940
|
-
namedType: "ProcessingFeatures"
|
|
7941
|
-
})]));
|
|
7942
|
-
};
|
|
7943
|
-
createProcessingFeaturesSchema(zod.boolean());
|
|
7944
|
-
/** Position and size as fractions of the image width and height, from 0 to 1. */
|
|
7945
|
-
const BOUNDING_BOX_SCHEMA = zod.object({
|
|
7946
|
-
left: zod.number(),
|
|
7947
|
-
top: zod.number(),
|
|
7948
|
-
width: zod.number().min(0),
|
|
7949
|
-
height: zod.number().min(0)
|
|
7950
|
-
}).strict().meta(metaStore([docPropertyInfo({
|
|
7951
|
-
description: "Position and size as fractions of the image width and height, from 0 to 1. `left` and `top` can be slightly negative when the subject extends past the edge.",
|
|
7952
|
-
namedType: "BoundingBox"
|
|
7953
|
-
})]));
|
|
7954
|
-
const DETECTION_CONFIDENCE_SCHEMA = zod.number().min(0).max(1).describe("Detection confidence from 0 to 1");
|
|
7955
|
-
const AGE_RANGE_SCHEMA = zod.object({
|
|
7956
|
-
low: zod.number().int().nonnegative(),
|
|
7957
|
-
high: zod.number().int().nonnegative()
|
|
7958
|
-
}).strict().meta(metaStore([docPropertyInfo({
|
|
7959
|
-
description: "Estimated age range in years. Estimates can be off by several years.",
|
|
7960
|
-
namedType: "AgeRange"
|
|
7961
|
-
})]));
|
|
7962
|
-
const DETECTED_PERSON_SCHEMA = zod.object({
|
|
7963
|
-
box: BOUNDING_BOX_SCHEMA,
|
|
7964
|
-
confidence: DETECTION_CONFIDENCE_SCHEMA
|
|
7965
|
-
}).strict().meta(metaStore([docPropertyInfo({
|
|
7966
|
-
description: "A detected person",
|
|
7967
|
-
namedType: "DetectedPerson"
|
|
7968
|
-
})]));
|
|
7969
|
-
const DETECTED_FACE_SCHEMA = zod.object({
|
|
7970
|
-
box: BOUNDING_BOX_SCHEMA,
|
|
7971
|
-
confidence: DETECTION_CONFIDENCE_SCHEMA,
|
|
7972
|
-
ageRange: AGE_RANGE_SCHEMA.optional()
|
|
7973
|
-
}).strict().meta(metaStore([docPropertyInfo({
|
|
7974
|
-
description: "A detected face",
|
|
7975
|
-
namedType: "DetectedFace"
|
|
7976
|
-
})]));
|
|
7977
|
-
const NOT_EVALUATED_FEATURE_RESULT_SCHEMA = zod.object({
|
|
7978
|
-
status: zod.literal("not-evaluated"),
|
|
7979
|
-
stage: zod.enum(PROCESSING_FEATURE_STAGES),
|
|
7980
|
-
reason: zod.enum(["not-supported", "unavailable"])
|
|
7981
|
-
}).strict();
|
|
7982
|
-
const createFeatureResultSchema = (shape) => zod.discriminatedUnion("status", [zod.object({
|
|
7983
|
-
status: zod.literal("evaluated"),
|
|
7984
|
-
stage: zod.enum(PROCESSING_FEATURE_STAGES),
|
|
7985
|
-
...shape
|
|
7986
|
-
}).strict(), NOT_EVALUATED_FEATURE_RESULT_SCHEMA]);
|
|
7987
|
-
const PERSON_DETECTION_RESULT_SCHEMA = createFeatureResultSchema({ people: zod.array(DETECTED_PERSON_SCHEMA) });
|
|
7988
|
-
const FACE_DETECTION_RESULT_SCHEMA = createFeatureResultSchema({ faces: zod.array(DETECTED_FACE_SCHEMA) });
|
|
7989
|
-
const PROCESSING_FEATURE_RESULTS_SCHEMA = zod.object({
|
|
7990
|
-
personDetection: PERSON_DETECTION_RESULT_SCHEMA.optional(),
|
|
7991
|
-
faceDetection: FACE_DETECTION_RESULT_SCHEMA.optional()
|
|
7992
|
-
}).strict().meta(metaStore([docPropertyInfo({
|
|
7993
|
-
description: "Results of the optional features requested in `processingOptions.features`, keyed by feature",
|
|
7994
|
-
namedType: "ProcessingFeatureResults"
|
|
7995
|
-
})]));
|
|
7996
7834
|
(/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
7997
7835
|
const CHARS_GLOBAL_BACKSLASH_SUPPORTED_RX = /[\0\b\t\n\r\x1a"'\\]/g;
|
|
7998
7836
|
const CHARS_ESCAPE_BACKSLASH_SUPPORTED_MAP = {
|
|
@@ -8049,166 +7887,106 @@ const PROCESSING_FEATURE_RESULTS_SCHEMA = zod.object({
|
|
|
8049
7887
|
}
|
|
8050
7888
|
module.exports = escapeString;
|
|
8051
7889
|
})))();
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
7890
|
+
/**
|
|
7891
|
+
* The prefix of every kind of object customers can see, so an ID says what it identifies:
|
|
7892
|
+
* `proj_8fK2mQx7Lp4NvB3tRz9Y` is a project. Rows that never leave the backend (usage meters, upload
|
|
7893
|
+
* token uses, retained variants, terms acceptances, notices) keep their own internal IDs.
|
|
7894
|
+
*/
|
|
7895
|
+
const ID_PREFIXES = {
|
|
7896
|
+
organisation: "org",
|
|
7897
|
+
project: "proj",
|
|
7898
|
+
asset: "asset",
|
|
7899
|
+
folder: "fldr",
|
|
7900
|
+
lens: "lens",
|
|
7901
|
+
apiKey: "key",
|
|
7902
|
+
uploadPreset: "preset",
|
|
7903
|
+
connector: "conn",
|
|
7904
|
+
connectorDelivery: "dlv",
|
|
7905
|
+
hashList: "hl",
|
|
7906
|
+
hashListEntry: "hle",
|
|
7907
|
+
policy: "pol",
|
|
7908
|
+
event: "evt",
|
|
7909
|
+
user: "user",
|
|
7910
|
+
member: "mem",
|
|
7911
|
+
invitation: "inv",
|
|
7912
|
+
supportTicket: "tkt",
|
|
7913
|
+
supportTicketMessage: "msg",
|
|
7914
|
+
humanReview: "rev",
|
|
7915
|
+
appeal: "apl",
|
|
7916
|
+
complianceCase: "case",
|
|
7917
|
+
safetyHold: "hold",
|
|
7918
|
+
creditPurchase: "cp",
|
|
7919
|
+
oauthClient: "oac",
|
|
7920
|
+
waitlistEntry: "wl"
|
|
8058
7921
|
};
|
|
8059
|
-
const
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
7922
|
+
const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
7923
|
+
const ID_BODY = `[0-9A-Za-z]{20}`;
|
|
7924
|
+
const ID_PREFIX_VALUES = new Set(Object.values(ID_PREFIXES));
|
|
7925
|
+
const patterns = /* @__PURE__ */ new Map();
|
|
7926
|
+
/** Matches one ID with `prefix`, or any registered prefix when none is given. */
|
|
7927
|
+
const idPattern = (prefix) => {
|
|
7928
|
+
const key = prefix ?? "";
|
|
7929
|
+
let pattern = patterns.get(key);
|
|
7930
|
+
if (!pattern) {
|
|
7931
|
+
const prefixes = prefix ? prefix : [...ID_PREFIX_VALUES].join("|");
|
|
7932
|
+
pattern = new RegExp(`^(?:${prefixes})_${ID_BODY}$`);
|
|
7933
|
+
patterns.set(key, pattern);
|
|
7934
|
+
}
|
|
7935
|
+
return pattern;
|
|
8067
7936
|
};
|
|
8068
|
-
|
|
8069
|
-
const
|
|
8070
|
-
"
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
"
|
|
8088
|
-
"
|
|
8089
|
-
"
|
|
8090
|
-
"
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
status: zod.literal("not-evaluated"),
|
|
8122
|
-
reason: CLASSIFICATION_NOT_EVALUATED_REASON_SCHEMA,
|
|
8123
|
-
qualifiers: CLASSIFICATION_QUALIFIER_RESULTS_SCHEMA.optional()
|
|
8124
|
-
}).strict()
|
|
8125
|
-
]);
|
|
8126
|
-
const CATEGORIES_SCHEMA = zod.object({
|
|
8127
|
-
sexual: CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
|
|
8128
|
-
violence: CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
|
|
8129
|
-
"self-harm": CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
|
|
8130
|
-
harassment: CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
|
|
8131
|
-
hate: CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
|
|
8132
|
-
"criminal-activity": CLASSIFICATION_CATEGORY_REPORT_SCHEMA
|
|
8133
|
-
}).strict();
|
|
8134
|
-
/** What a moderation policy does with a category: nothing, alert, hide or purge. */
|
|
8135
|
-
const FLAGGED_CONTENT_ACTION_SCHEMA = zod.enum([
|
|
8136
|
-
"ignore",
|
|
8137
|
-
"alert",
|
|
8138
|
-
"hide",
|
|
8139
|
-
"purge"
|
|
8140
|
-
]);
|
|
8141
|
-
/** What the project's policy made of the report when the asset was decided. */
|
|
8142
|
-
const CLASSIFICATION_REPORT_POLICY_SCHEMA = zod.object({
|
|
8143
|
-
score: zod.number().min(0).max(1).describe("The highest category policy score."),
|
|
8144
|
-
categories: zod.partialRecord(CLASSIFICATION_CATEGORY_SCHEMA, zod.object({
|
|
8145
|
-
score: zod.number().min(0).max(1).describe("How strongly the content meets the policy, from 0 to 1: the category score, lowered by allowances that cover the content and raised to the presence score of a restriction."),
|
|
8146
|
-
action: FLAGGED_CONTENT_ACTION_SCHEMA.describe("What the policy does with this category. `ignore` when the policy score is below the category threshold.")
|
|
8147
|
-
}).strict()).describe("Each assessed category's policy score and action. Unassessed categories are left out.")
|
|
8148
|
-
}).strict();
|
|
8149
|
-
const CLASSIFICATION_LABEL_SCHEMA = zod.object({
|
|
8150
|
-
description: saneStringField(),
|
|
8151
|
-
score: zod.number(),
|
|
8152
|
-
topicalityScore: zod.number().nullish(),
|
|
8153
|
-
likelihood: LIKELIHOOD_SCHEMA,
|
|
8154
|
-
knowledgeBaseId: saneStringField().nullish()
|
|
8155
|
-
});
|
|
8156
|
-
zod.object({
|
|
8157
|
-
processedAt: zod.number(),
|
|
8158
|
-
categories: CATEGORIES_SCHEMA,
|
|
8159
|
-
score: zod.number(),
|
|
8160
|
-
notes: saneStringField().nullish(),
|
|
8161
|
-
media: zod.object({
|
|
8162
|
-
durationSeconds: zod.number().positive(),
|
|
8163
|
-
frameIntervalSeconds: zod.number().positive().optional(),
|
|
8164
|
-
sampledFrames: zod.number().int().nonnegative(),
|
|
8165
|
-
audioTranscribed: zod.boolean(),
|
|
8166
|
-
segments: zod.array(zod.object({
|
|
8167
|
-
source: zod.enum([
|
|
8168
|
-
"frame",
|
|
8169
|
-
"speech",
|
|
8170
|
-
"frame-text",
|
|
8171
|
-
"clip"
|
|
8172
|
-
]),
|
|
8173
|
-
startSeconds: zod.number().nonnegative(),
|
|
8174
|
-
endSeconds: zod.number().nonnegative(),
|
|
8175
|
-
categories: CATEGORIES_SCHEMA
|
|
8176
|
-
}).strict())
|
|
8177
|
-
}).strict().optional(),
|
|
8178
|
-
labels: zod.array(CLASSIFICATION_LABEL_SCHEMA).nullish(),
|
|
8179
|
-
features: PROCESSING_FEATURE_RESULTS_SCHEMA.optional(),
|
|
8180
|
-
policy: CLASSIFICATION_REPORT_POLICY_SCHEMA.optional().describe("What the project's moderation policy made of this report when the asset was decided. Absent until then.")
|
|
8181
|
-
}).strict();
|
|
8182
|
-
//#endregion
|
|
8183
|
-
//#region src/contract/MediaTestTriggers.js
|
|
8184
|
-
const MEDIA_TEST_SIGNALS = [...CLASSIFICATION_CATEGORIES, "violence.graphic"];
|
|
8185
|
-
const HEX_WORDS = [
|
|
8186
|
-
"zero",
|
|
8187
|
-
"one",
|
|
8188
|
-
"two",
|
|
8189
|
-
"three",
|
|
8190
|
-
"four",
|
|
8191
|
-
"five",
|
|
8192
|
-
"six",
|
|
8193
|
-
"seven",
|
|
8194
|
-
"eight",
|
|
8195
|
-
"nine",
|
|
8196
|
-
"alpha",
|
|
8197
|
-
"bravo",
|
|
8198
|
-
"charlie",
|
|
8199
|
-
"delta",
|
|
8200
|
-
"echo",
|
|
8201
|
-
"foxtrot"
|
|
8202
|
-
];
|
|
8203
|
-
/** Read this aloud, or render it as large, high-contrast text in a video frame. */
|
|
8204
|
-
function generateMediaTestCode(signal, testInputKey) {
|
|
8205
|
-
if (!MEDIA_TEST_SIGNALS.includes(signal)) throw new Error("Unsupported media test signal");
|
|
8206
|
-
const suffix = testInputKey.toLowerCase();
|
|
8207
|
-
if (!/^[a-f0-9]{8}$/.test(suffix)) throw new Error("Expected an eight-character hexadecimal test input key");
|
|
8208
|
-
return `Calm Lens test ${[...suffix].map((character) => HEX_WORDS[Number.parseInt(character, 16)]).join(" ")} ${signal.replace(/[.-]/g, " ")} end test`;
|
|
8209
|
-
}
|
|
8210
|
-
//#endregion
|
|
8211
|
-
//#region src/contract/Permissions-B5dqc8aj.js
|
|
7937
|
+
/** Base62 characters from a CSPRNG, without modulo bias (bytes 248-255 are redrawn). */
|
|
7938
|
+
const randomBase62 = (length) => {
|
|
7939
|
+
let result = "";
|
|
7940
|
+
while (result.length < length) {
|
|
7941
|
+
const bytes = crypto.getRandomValues(new Uint8Array(length * 2));
|
|
7942
|
+
for (const byte of bytes) {
|
|
7943
|
+
if (byte >= 248) continue;
|
|
7944
|
+
result += ALPHABET[byte % 62];
|
|
7945
|
+
if (result.length === length) break;
|
|
7946
|
+
}
|
|
7947
|
+
}
|
|
7948
|
+
return result;
|
|
7949
|
+
};
|
|
7950
|
+
/** A new random ID, such as `newId("proj")` → `proj_8fK2mQx7Lp4NvB3tRz9Y`. */
|
|
7951
|
+
const newId = (prefix) => `${prefix}_${randomBase62(20)}`;
|
|
7952
|
+
/** Whether `value` is an ID with `prefix` (or with any registered prefix). */
|
|
7953
|
+
const isId = (value, prefix) => typeof value === "string" && idPattern(prefix).test(value);
|
|
7954
|
+
/** Example IDs for docs, OpenAPI and SDK fixtures: fixed, so generated output stays stable. */
|
|
7955
|
+
const EXAMPLE_IDS = {
|
|
7956
|
+
org: "org_3hQ7tLk2Vb9XpR4mWc8N",
|
|
7957
|
+
proj: "proj_8fK2mQx7Lp4NvB3tRz9Y",
|
|
7958
|
+
asset: "asset_5Gm2Rk8XqT3wLp9VbN4c",
|
|
7959
|
+
fldr: "fldr_7Nc4Wq2Lm8Xt5Rk9Bv3P",
|
|
7960
|
+
lens: "lens_2Xr9Lp4Vm7Qk3Tb8Nw5C",
|
|
7961
|
+
key: "key_9Pt3Wm6Rk2Lx8Qv4Nb7C",
|
|
7962
|
+
preset: "preset_4Lk8Qw2Xm9Rt5Vb3Nc7P",
|
|
7963
|
+
conn: "conn_6Rb2Xk9Lm4Qt7Wp3Nv8C",
|
|
7964
|
+
dlv: "dlv_3Vm8Qk2Xt7Lp9Rb4Nw6C",
|
|
7965
|
+
hl: "hl_5Xk3Rm9Lq2Vt8Wb4Np7C",
|
|
7966
|
+
hle: "hle_8Qm4Xk2Rt9Lv3Wb7Np5C",
|
|
7967
|
+
pol: "pol_2Wk7Rm4Xq9Lt3Vb8Np6C",
|
|
7968
|
+
evt: "evt_7Lm3Xq8Rk2Vt9Wb4Np5C",
|
|
7969
|
+
user: "user_4Rk9Xm2Lq7Vt3Wb8Np6C",
|
|
7970
|
+
mem: "mem_9Xm4Rk7Lq2Vt8Wb3Np5C",
|
|
7971
|
+
inv: "inv_3Qk8Xm2Rt9Lv4Wb7Np6C",
|
|
7972
|
+
tkt: "tkt_6Xm2Rk9Lq4Vt7Wb3Np8C",
|
|
7973
|
+
msg: "msg_2Rk7Xm4Lq9Vt3Wb8Np5C",
|
|
7974
|
+
rev: "rev_8Xm3Rk2Lq7Vt9Wb4Np6C",
|
|
7975
|
+
apl: "apl_4Qk9Xm7Rt2Lv8Wb3Np5C",
|
|
7976
|
+
case: "case_7Xm2Rk4Lq9Vt3Wb8Np6C",
|
|
7977
|
+
hold: "hold_3Rk8Xm2Lq7Vt4Wb9Np5C",
|
|
7978
|
+
cp: "cp_9Qk4Xm7Rt3Lv8Wb2Np6C",
|
|
7979
|
+
oac: "oac_5Xm9Rk3Lq2Vt7Wb4Np8C",
|
|
7980
|
+
wl: "wl_2Qk7Xm4Rt9Lv3Wb8Np5C"
|
|
7981
|
+
};
|
|
7982
|
+
/**
|
|
7983
|
+
* A string field holding an ID with `prefix`: a string schema with a pattern, so OpenAPI and the
|
|
7984
|
+
* docs show the pattern and the example. Parsed values are typed `string`, not `Id<P>`: typing
|
|
7985
|
+
* every stored model's IDs by prefix is a separate change (it surfaces about a thousand call sites
|
|
7986
|
+
* across the packages that build records from plain strings).
|
|
7987
|
+
*/
|
|
7988
|
+
const idField = (prefix) => zod.string().regex(idPattern(prefix), { message: `Must be an ID starting with "${prefix}_"` }).meta({ examples: [EXAMPLE_IDS[prefix]] });
|
|
7989
|
+
// src/contract/Permissions-DjKQmOVp.js
|
|
8212
7990
|
const ACTION_SCHEMA = zod.enum([
|
|
8213
7991
|
"assets:read",
|
|
8214
7992
|
"assets:download",
|
|
@@ -8234,6 +8012,8 @@ const ACTION_SCHEMA = zod.enum([
|
|
|
8234
8012
|
"lenses:write",
|
|
8235
8013
|
"upload-presets:read",
|
|
8236
8014
|
"upload-presets:write",
|
|
8015
|
+
"hash-lists:read",
|
|
8016
|
+
"hash-lists:write",
|
|
8237
8017
|
"reviews:read",
|
|
8238
8018
|
"reviews:write",
|
|
8239
8019
|
"project:manage"
|
|
@@ -8246,6 +8026,7 @@ const PROJECT_ACTIONS = [...[
|
|
|
8246
8026
|
"metadata",
|
|
8247
8027
|
"lenses",
|
|
8248
8028
|
"upload-presets",
|
|
8029
|
+
"hash-lists",
|
|
8249
8030
|
"reviews"
|
|
8250
8031
|
].flatMap((feature) => [`${feature}:read`, `${feature}:write`]), "project:manage"];
|
|
8251
8032
|
const isProjectAction = (action) => PROJECT_ACTIONS.includes(action);
|
|
@@ -8253,7 +8034,7 @@ const isProjectAction = (action) => PROJECT_ACTIONS.includes(action);
|
|
|
8253
8034
|
const actionsImplying = (action) => !isProjectAction(action) || action === "project:manage" ? [] : action.endsWith(":read") ? [action.replace(/:read$/, ":write"), "project:manage"] : ["project:manage"];
|
|
8254
8035
|
const GRANT_SCHEMA = zod.object({
|
|
8255
8036
|
actions: zod.array(ACTION_SCHEMA),
|
|
8256
|
-
folderId:
|
|
8037
|
+
folderId: idField("fldr").nullable(),
|
|
8257
8038
|
includeSubfolders: zod.boolean()
|
|
8258
8039
|
}).strict().refine((g) => g.folderId === null || !g.actions.some(isProjectAction), "Project configuration permissions require project scope");
|
|
8259
8040
|
const GRANTS_SCHEMA = zod.array(GRANT_SCHEMA).max(100);
|
|
@@ -8276,15 +8057,15 @@ const permits = (grants, action, folderId, folders) => grants.some((grant) => {
|
|
|
8276
8057
|
}
|
|
8277
8058
|
return false;
|
|
8278
8059
|
});
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
zod.
|
|
8060
|
+
/**
|
|
8061
|
+
* What a person can let an AI assistant or the CLI do for them. `read` looks at assets, folders,
|
|
8062
|
+
* hash lists and events; `write` also creates, changes and deletes assets, folders and hash lists.
|
|
8063
|
+
* Members, API keys, connectors, sharing, downloads and project settings are never delegated.
|
|
8064
|
+
*/
|
|
8065
|
+
const AGENT_SCOPE_SCHEMA = zod.enum(["read", "write"]);
|
|
8066
|
+
zod.array(AGENT_SCOPE_SCHEMA).min(1).max(2);
|
|
8285
8067
|
zod.enum(["active", "banned"]);
|
|
8286
|
-
|
|
8287
|
-
//#region src/testing/MockApiKey.ts
|
|
8068
|
+
// src/testing/MockApiKey.ts
|
|
8288
8069
|
const MOCK_API_KEY_PREFIX = "sk_test_";
|
|
8289
8070
|
const toBase64Url = (value) => btoa(value).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
8290
8071
|
const fromBase64Url = (value) => {
|
|
@@ -8300,133 +8081,322 @@ const getMockApiKeyGrants = (apiKey) => {
|
|
|
8300
8081
|
return [];
|
|
8301
8082
|
}
|
|
8302
8083
|
};
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8084
|
+
// src/testing/MockClassification.ts
|
|
8085
|
+
/** The parent categories, in the API's order. */
|
|
8086
|
+
const MOCK_CATEGORIES = [
|
|
8087
|
+
"sexual",
|
|
8088
|
+
"violence",
|
|
8089
|
+
"self-harm",
|
|
8090
|
+
"harassment",
|
|
8091
|
+
"hate",
|
|
8092
|
+
"criminal-activity",
|
|
8093
|
+
"personal-information",
|
|
8094
|
+
"profanity"
|
|
8095
|
+
];
|
|
8096
|
+
/** Each category's qualifiers. */
|
|
8097
|
+
const MOCK_QUALIFIERS = {
|
|
8098
|
+
"sexual": [
|
|
8099
|
+
"sexual-pornographic-media",
|
|
8100
|
+
"sexual-nudity",
|
|
8101
|
+
"sexual-activity",
|
|
8102
|
+
"sexual-explicit-language",
|
|
8103
|
+
"sexual-assault-media",
|
|
8104
|
+
"sexual-non-consensual",
|
|
8105
|
+
"sexual-intimate-image-abuse",
|
|
8106
|
+
"sexual-extreme",
|
|
8107
|
+
"sexual-minors",
|
|
8108
|
+
"sexual-solicitation",
|
|
8109
|
+
"sexual-paid",
|
|
8110
|
+
"sexual-adult-promotion",
|
|
8111
|
+
"sexual-revealing-clothing",
|
|
8112
|
+
"sexual-suggestive",
|
|
8113
|
+
"sexual-non-erotic-nudity",
|
|
8114
|
+
"sexual-text",
|
|
8115
|
+
"sexual-historical",
|
|
8116
|
+
"sexual-documentary",
|
|
8117
|
+
"sexual-educational",
|
|
8118
|
+
"sexual-discussion",
|
|
8119
|
+
"sexual-fictional",
|
|
8120
|
+
"sexual-support-prevention"
|
|
8121
|
+
],
|
|
8122
|
+
"violence": [
|
|
8123
|
+
"violence-high-impact",
|
|
8124
|
+
"violence-attack",
|
|
8125
|
+
"violence-threat",
|
|
8126
|
+
"violence-preparation",
|
|
8127
|
+
"violence-encouragement",
|
|
8128
|
+
"violence-wish",
|
|
8129
|
+
"violence-coded-incitement",
|
|
8130
|
+
"violence-extremism",
|
|
8131
|
+
"violence-injury",
|
|
8132
|
+
"violence-violent-death",
|
|
8133
|
+
"violence-natural-death",
|
|
8134
|
+
"violence-graphic",
|
|
8135
|
+
"violence-decapitation-act",
|
|
8136
|
+
"violence-gore",
|
|
8137
|
+
"violence-weapon",
|
|
8138
|
+
"violence-sport",
|
|
8139
|
+
"violence-text",
|
|
8140
|
+
"violence-historical",
|
|
8141
|
+
"violence-documentary",
|
|
8142
|
+
"violence-educational",
|
|
8143
|
+
"violence-discussion",
|
|
8144
|
+
"violence-fictional",
|
|
8145
|
+
"violence-fantasy",
|
|
8146
|
+
"violence-support-prevention"
|
|
8147
|
+
],
|
|
8148
|
+
"self-harm": [
|
|
8149
|
+
"self-harm-age-restricted",
|
|
8150
|
+
"self-harm-thoughts",
|
|
8151
|
+
"self-harm-preparation",
|
|
8152
|
+
"self-harm-act",
|
|
8153
|
+
"self-harm-methods",
|
|
8154
|
+
"self-harm-encouragement",
|
|
8155
|
+
"self-harm-historical",
|
|
8156
|
+
"self-harm-documentary",
|
|
8157
|
+
"self-harm-educational",
|
|
8158
|
+
"self-harm-fictional",
|
|
8159
|
+
"self-harm-support-prevention"
|
|
8160
|
+
],
|
|
8161
|
+
"harassment": [
|
|
8162
|
+
"harassment-abuse",
|
|
8163
|
+
"harassment-mild",
|
|
8164
|
+
"harassment-bullying",
|
|
8165
|
+
"harassment-public-target",
|
|
8166
|
+
"harassment-threat",
|
|
8167
|
+
"harassment-sexual",
|
|
8168
|
+
"harassment-encouragement",
|
|
8169
|
+
"harassment-historical",
|
|
8170
|
+
"harassment-documentary",
|
|
8171
|
+
"harassment-educational",
|
|
8172
|
+
"harassment-fictional",
|
|
8173
|
+
"harassment-support-prevention"
|
|
8174
|
+
],
|
|
8175
|
+
"hate": [
|
|
8176
|
+
"hate-nazi-symbols-public-display",
|
|
8177
|
+
"hate-nazi-insignia-public-display",
|
|
8178
|
+
"hate-nazi-genocide-public-peace",
|
|
8179
|
+
"hate-nazi-genocide-denial-or-approval",
|
|
8180
|
+
"hate-adjudicated-nazi-crimes-denial",
|
|
8181
|
+
"hate-abuse",
|
|
8182
|
+
"hate-slurs",
|
|
8183
|
+
"hate-dehumanization",
|
|
8184
|
+
"hate-inferiority",
|
|
8185
|
+
"hate-exclusion",
|
|
8186
|
+
"hate-symbol",
|
|
8187
|
+
"hate-hostility",
|
|
8188
|
+
"hate-threat",
|
|
8189
|
+
"hate-personal",
|
|
8190
|
+
"hate-historical",
|
|
8191
|
+
"hate-documentary",
|
|
8192
|
+
"hate-educational",
|
|
8193
|
+
"hate-fictional",
|
|
8194
|
+
"hate-counterspeech",
|
|
8195
|
+
"hate-support-prevention"
|
|
8196
|
+
],
|
|
8197
|
+
"criminal-activity": [
|
|
8198
|
+
"criminal-activity-demonstration",
|
|
8199
|
+
"criminal-activity-preparation",
|
|
8200
|
+
"criminal-activity-request",
|
|
8201
|
+
"criminal-activity-instructions",
|
|
8202
|
+
"criminal-activity-promotion",
|
|
8203
|
+
"criminal-activity-coded-incitement",
|
|
8204
|
+
"criminal-activity-substances",
|
|
8205
|
+
"criminal-activity-grooming",
|
|
8206
|
+
"criminal-activity-csam",
|
|
8207
|
+
"criminal-activity-alcohol",
|
|
8208
|
+
"criminal-activity-tobacco",
|
|
8209
|
+
"criminal-activity-gambling",
|
|
8210
|
+
"criminal-activity-fraud",
|
|
8211
|
+
"criminal-activity-sexual-solicitation",
|
|
8212
|
+
"criminal-activity-graffiti",
|
|
8213
|
+
"criminal-activity-historical",
|
|
8214
|
+
"criminal-activity-documentary",
|
|
8215
|
+
"criminal-activity-educational",
|
|
8216
|
+
"criminal-activity-discussion",
|
|
8217
|
+
"criminal-activity-fictional",
|
|
8218
|
+
"criminal-activity-support-prevention"
|
|
8219
|
+
],
|
|
8220
|
+
"personal-information": [
|
|
8221
|
+
"personal-information-contact",
|
|
8222
|
+
"personal-information-email",
|
|
8223
|
+
"personal-information-phone",
|
|
8224
|
+
"personal-information-government-id",
|
|
8225
|
+
"personal-information-financial",
|
|
8226
|
+
"personal-information-location",
|
|
8227
|
+
"personal-information-identifiers",
|
|
8228
|
+
"personal-information-credentials",
|
|
8229
|
+
"personal-information-request",
|
|
8230
|
+
"personal-information-self-disclosure",
|
|
8231
|
+
"personal-information-third-party",
|
|
8232
|
+
"personal-information-historical",
|
|
8233
|
+
"personal-information-documentary",
|
|
8234
|
+
"personal-information-educational",
|
|
8235
|
+
"personal-information-fictional",
|
|
8236
|
+
"personal-information-support-prevention"
|
|
8237
|
+
],
|
|
8238
|
+
"profanity": [
|
|
8239
|
+
"profanity-mild",
|
|
8240
|
+
"profanity-strong",
|
|
8241
|
+
"profanity-slurs",
|
|
8242
|
+
"profanity-obfuscated",
|
|
8243
|
+
"profanity-historical",
|
|
8244
|
+
"profanity-documentary",
|
|
8245
|
+
"profanity-educational",
|
|
8246
|
+
"profanity-fictional",
|
|
8247
|
+
"profanity-support-prevention"
|
|
8248
|
+
]
|
|
8341
8249
|
};
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8250
|
+
// src/testing/MockDecision.ts
|
|
8251
|
+
const OUTCOMES = [
|
|
8252
|
+
"pending",
|
|
8253
|
+
"failed",
|
|
8254
|
+
"allowed",
|
|
8255
|
+
"limited",
|
|
8256
|
+
"age-restricted",
|
|
8257
|
+
"blocked",
|
|
8258
|
+
"purged"
|
|
8259
|
+
];
|
|
8260
|
+
/** The policy action that gives each decided status. */
|
|
8261
|
+
const STATUS_ACTION = {
|
|
8262
|
+
allowed: "allow",
|
|
8263
|
+
limited: "limit",
|
|
8264
|
+
"age-restricted": "age-restrict",
|
|
8265
|
+
blocked: "block",
|
|
8266
|
+
purged: "purge"
|
|
8267
|
+
};
|
|
8268
|
+
/** The likelihood the API reports for a score (ClassificationCategories.ts, scoreToLikelihood). */
|
|
8269
|
+
const scoreToLikelihood = (score) => score >= .8 ? "very-likely" : score >= .6 ? "likely" : score >= .4 ? "possible" : score >= .2 ? "unlikely" : "very-unlikely";
|
|
8270
|
+
const isScore = (score) => typeof score === "number" && Number.isFinite(score) && score >= 0 && score <= 1;
|
|
8271
|
+
/**
|
|
8272
|
+
* A moderation decision for `mockOptions.moderate` or `completeProcessing`, built with
|
|
8273
|
+
* `mockDecision(status)`: flag the categories it found, with their scores and qualifiers, and
|
|
8274
|
+
* mark the categories it couldn't assess. Each call checks its names and scores and throws
|
|
8275
|
+
* `sdk/invalid-argument` for one CalmLens doesn't have.
|
|
8276
|
+
*/
|
|
8277
|
+
var MockDecision = class {
|
|
8278
|
+
#flags = /* @__PURE__ */ new Map();
|
|
8279
|
+
#notEvaluated = /* @__PURE__ */ new Set();
|
|
8280
|
+
constructor(status) {
|
|
8281
|
+
if (!OUTCOMES.includes(status)) throw require_ModerationContextLimits.invalidArgument(`status must be one of ${OUTCOMES.join(", ")}`);
|
|
8282
|
+
this.status = status;
|
|
8367
8283
|
}
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8284
|
+
/**
|
|
8285
|
+
* Records findings in `category` with its `score` (0 to 1) and, optionally, scores for its
|
|
8286
|
+
* qualifiers, such as `{ "violence-threat": 0.9 }`. The policy acts on a category at 0.5 and
|
|
8287
|
+
* above, so `flaggedCategories` lists it there unless the status is `allowed`.
|
|
8288
|
+
*/
|
|
8289
|
+
flag(category, score, qualifiers = {}) {
|
|
8290
|
+
this.#checkCategory(category);
|
|
8291
|
+
if (this.status === "pending" || this.status === "failed") throw require_ModerationContextLimits.invalidArgument(`a ${this.status} decision has no findings`);
|
|
8292
|
+
if (this.#flags.has(category) || this.#notEvaluated.has(category)) throw require_ModerationContextLimits.invalidArgument(`${category} is already in this decision`);
|
|
8293
|
+
if (!isScore(score)) throw require_ModerationContextLimits.invalidArgument(`${category}'s score must be from 0 to 1`);
|
|
8294
|
+
const known = MOCK_QUALIFIERS[category];
|
|
8295
|
+
for (const [qualifier, qualifierScore] of Object.entries(qualifiers)) {
|
|
8296
|
+
if (!known.includes(qualifier)) throw require_ModerationContextLimits.invalidArgument(`${qualifier} is not a qualifier of ${category}`);
|
|
8297
|
+
if (!isScore(qualifierScore)) throw require_ModerationContextLimits.invalidArgument(`${qualifier}'s score must be from 0 to 1`);
|
|
8298
|
+
}
|
|
8299
|
+
this.#flags.set(category, {
|
|
8300
|
+
score,
|
|
8301
|
+
qualifiers: Object.entries(qualifiers)
|
|
8302
|
+
});
|
|
8303
|
+
return this;
|
|
8371
8304
|
}
|
|
8372
|
-
|
|
8373
|
-
|
|
8305
|
+
/** Marks `category` as not assessed (`not-evaluated` for reason `not-supported`). */
|
|
8306
|
+
notEvaluated(category) {
|
|
8307
|
+
this.#checkCategory(category);
|
|
8308
|
+
if (this.status === "pending" || this.status === "failed") throw require_ModerationContextLimits.invalidArgument(`a ${this.status} decision has no findings`);
|
|
8309
|
+
if (this.#flags.has(category) || this.#notEvaluated.has(category)) throw require_ModerationContextLimits.invalidArgument(`${category} is already in this decision`);
|
|
8310
|
+
this.#notEvaluated.add(category);
|
|
8311
|
+
return this;
|
|
8374
8312
|
}
|
|
8375
|
-
|
|
8376
|
-
|
|
8313
|
+
#checkCategory(category) {
|
|
8314
|
+
if (!MOCK_CATEGORIES.includes(category)) throw require_ModerationContextLimits.invalidArgument(`${category} is not a CalmLens category`);
|
|
8377
8315
|
}
|
|
8378
|
-
|
|
8379
|
-
|
|
8316
|
+
/**
|
|
8317
|
+
* The report a decided asset gets, as the API sends it: every category (unflagged ones
|
|
8318
|
+
* evaluated with score 0), policy-adjusted results and the automated action and
|
|
8319
|
+
* `flaggedCategories`. `undefined` for `pending` and `failed`.
|
|
8320
|
+
*/
|
|
8321
|
+
report() {
|
|
8322
|
+
const status = this.status;
|
|
8323
|
+
if (status === "pending" || status === "failed") return void 0;
|
|
8324
|
+
const action = (score) => status !== "allowed" && score >= .5 ? STATUS_ACTION[status] : "allow";
|
|
8325
|
+
const evaluated = MOCK_CATEGORIES.filter((category) => !this.#notEvaluated.has(category)).map((category) => [category, this.#flags.get(category)?.score ?? 0]);
|
|
8326
|
+
const categories = Object.fromEntries(MOCK_CATEGORIES.map((category) => {
|
|
8327
|
+
if (this.#notEvaluated.has(category)) return [category, {
|
|
8328
|
+
status: "not-evaluated",
|
|
8329
|
+
reason: "not-supported"
|
|
8330
|
+
}];
|
|
8331
|
+
const flag = this.#flags.get(category);
|
|
8332
|
+
const score = flag?.score ?? 0;
|
|
8333
|
+
return [category, {
|
|
8334
|
+
status: "evaluated",
|
|
8335
|
+
likelihood: scoreToLikelihood(score),
|
|
8336
|
+
score,
|
|
8337
|
+
...flag?.qualifiers.length && { qualifiers: Object.fromEntries(flag.qualifiers.map(([qualifier, someScore]) => [qualifier, {
|
|
8338
|
+
status: "evaluated",
|
|
8339
|
+
someScore,
|
|
8340
|
+
likelihood: scoreToLikelihood(someScore)
|
|
8341
|
+
}])) }
|
|
8342
|
+
}];
|
|
8343
|
+
}));
|
|
8344
|
+
return {
|
|
8345
|
+
categories: Object.fromEntries(evaluated.map(([category, score]) => [category, {
|
|
8346
|
+
score,
|
|
8347
|
+
action: action(score)
|
|
8348
|
+
}])),
|
|
8349
|
+
explanationComplete: true,
|
|
8350
|
+
source: "policy",
|
|
8351
|
+
automatedAction: STATUS_ACTION[status],
|
|
8352
|
+
regions: [],
|
|
8353
|
+
flaggedCategories: evaluated.filter(([, score]) => action(score) !== "allow").map(([category]) => category),
|
|
8354
|
+
raw: { categories }
|
|
8355
|
+
};
|
|
8380
8356
|
}
|
|
8381
8357
|
};
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
const createPage = (items, query) => {
|
|
8406
|
-
const pageSize = query.pageSize ?? 10;
|
|
8407
|
-
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) throw require_CalmLensVersion.invalidArgument("Invalid page size");
|
|
8408
|
-
const ordering = query.ordering?.length ? [...query.ordering] : [{
|
|
8409
|
-
field: "createdAt",
|
|
8410
|
-
direction: "DESC"
|
|
8411
|
-
}];
|
|
8412
|
-
if (ordering.some(({ field }) => field !== "createdAt" && field !== "id")) throw require_CalmLensVersion.invalidArgument("Invalid ordering");
|
|
8358
|
+
/** Starts a moderation decision with this status; see `MockDecision`. */
|
|
8359
|
+
const mockDecision = (status) => new MockDecision(status);
|
|
8360
|
+
/** The default `moderate`: queued work stays pending until `completeProcessing`, which allows it. */
|
|
8361
|
+
const defaultModerate = ({ trigger, queued }) => queued && trigger !== "complete" ? "pending" : "allowed";
|
|
8362
|
+
// src/testing/MockUtils.ts
|
|
8363
|
+
const toBase64Utf8 = (value) => btoa(Array.from(new TextEncoder().encode(value), (byte) => String.fromCharCode(byte)).join(""));
|
|
8364
|
+
const fromBase64Utf8 = (value) => new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(atob(value), (char) => char.charCodeAt(0)));
|
|
8365
|
+
/**
|
|
8366
|
+
* One keyset page like the API's (packages/shared-backend/src/CursorPagination.ts): ordered by
|
|
8367
|
+
* `ordering` (newest first by default) with an `id` tie-breaker added, starting after the
|
|
8368
|
+
* cursor's values. The cursor is tied to `scope` (the filter, folder or parent) and the
|
|
8369
|
+
* ordering, so it only continues the query it came from. `name` and `id` are compared by code
|
|
8370
|
+
* point, like the database.
|
|
8371
|
+
*/
|
|
8372
|
+
const createPage = (items, query, { scope, fail, defaultPageSize = 50, defaultOrdering = [{
|
|
8373
|
+
field: "createdAt",
|
|
8374
|
+
direction: "desc"
|
|
8375
|
+
}] }) => {
|
|
8376
|
+
const pageSize = query.pageSize ?? defaultPageSize;
|
|
8377
|
+
if (typeof pageSize !== "number" || !Number.isFinite(pageSize) || pageSize < 1) throw fail("validation/too-small", "pageSize");
|
|
8378
|
+
if (pageSize > 100) throw fail("validation/too-big", "pageSize");
|
|
8379
|
+
if (!Number.isInteger(pageSize)) throw require_ModerationContextLimits.invalidArgument("Invalid page size");
|
|
8380
|
+
const ordering = query.ordering?.length ? [...query.ordering] : [...defaultOrdering];
|
|
8413
8381
|
if (!ordering.some(({ field }) => field === "id")) ordering.push({
|
|
8414
8382
|
field: "id",
|
|
8415
8383
|
direction: ordering[0].direction
|
|
8416
8384
|
});
|
|
8385
|
+
const signature = JSON.stringify([scope, ordering]);
|
|
8417
8386
|
let remaining = orderItems(items, ordering);
|
|
8418
8387
|
if (query.cursor) {
|
|
8419
8388
|
let values;
|
|
8420
8389
|
try {
|
|
8421
|
-
|
|
8422
|
-
|
|
8390
|
+
const parsed = JSON.parse(fromBase64Utf8(query.cursor));
|
|
8391
|
+
values = parsed?.values;
|
|
8392
|
+
if (parsed?.scope !== signature || !Array.isArray(values) || values.length !== ordering.length || values.some((value, i) => STRING_FIELDS.has(ordering[i].field) ? typeof value !== "string" : typeof value !== "number" || !Number.isFinite(value))) throw new Error();
|
|
8423
8393
|
} catch {
|
|
8424
|
-
throw
|
|
8394
|
+
throw fail("validation/invalid-cursor");
|
|
8425
8395
|
}
|
|
8426
8396
|
remaining = remaining.filter((item) => {
|
|
8427
8397
|
for (const [i, rule] of ordering.entries()) {
|
|
8428
8398
|
const result = compareValues(item[rule.field], values[i]);
|
|
8429
|
-
if (result) return rule.direction === "
|
|
8399
|
+
if (result) return rule.direction === "desc" ? result < 0 : result > 0;
|
|
8430
8400
|
}
|
|
8431
8401
|
return false;
|
|
8432
8402
|
});
|
|
@@ -8439,26 +8409,31 @@ const createPage = (items, query) => {
|
|
|
8439
8409
|
pageSize,
|
|
8440
8410
|
hasMore,
|
|
8441
8411
|
ordering,
|
|
8442
|
-
nextCursor: hasMore && last ?
|
|
8412
|
+
nextCursor: hasMore && last ? toBase64Utf8(JSON.stringify({
|
|
8413
|
+
scope: signature,
|
|
8414
|
+
values: ordering.map(({ field }) => last[field])
|
|
8415
|
+
})) : null
|
|
8443
8416
|
};
|
|
8444
8417
|
};
|
|
8418
|
+
const STRING_FIELDS = /* @__PURE__ */ new Set(["id", "name"]);
|
|
8445
8419
|
const compareValues = (left, right) => {
|
|
8446
8420
|
if (left === right) return 0;
|
|
8447
8421
|
if (left === void 0 || left === null) return -1;
|
|
8448
8422
|
if (right === void 0 || right === null) return 1;
|
|
8449
8423
|
if (typeof left === "number" && typeof right === "number") return left - right;
|
|
8450
|
-
|
|
8424
|
+
const [a, b] = [String(left), String(right)];
|
|
8425
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
8451
8426
|
};
|
|
8452
8427
|
/** Sorts like the API's `ordering`; without one, newest first. */
|
|
8453
8428
|
const orderItems = (items, ordering) => {
|
|
8454
8429
|
const rules = ordering?.length ? ordering : [{
|
|
8455
8430
|
field: "createdAt",
|
|
8456
|
-
direction: "
|
|
8431
|
+
direction: "desc"
|
|
8457
8432
|
}];
|
|
8458
8433
|
return [...items].sort((a, b) => {
|
|
8459
8434
|
for (const rule of rules) {
|
|
8460
8435
|
const result = compareValues(a[rule.field], b[rule.field]);
|
|
8461
|
-
if (result !== 0) return rule.direction === "
|
|
8436
|
+
if (result !== 0) return rule.direction === "desc" ? -result : result;
|
|
8462
8437
|
}
|
|
8463
8438
|
return 0;
|
|
8464
8439
|
});
|
|
@@ -8474,7 +8449,7 @@ const parseFilterValue = (raw) => {
|
|
|
8474
8449
|
return value;
|
|
8475
8450
|
};
|
|
8476
8451
|
const readField = (item, path) => path.split(".").reduce((value, key) => value && typeof value === "object" ? value[key] : void 0, item);
|
|
8477
|
-
const unsupportedFilter = (expression) =>
|
|
8452
|
+
const unsupportedFilter = (expression) => require_ModerationContextLimits.invalidArgument(`MockCalmLensServerClient cannot evaluate the filter "${expression}". It supports ==, !=, <, <=, >, >=, =in= and =out=, joined by ; (and) or , (or) and grouped with parentheses.`);
|
|
8478
8453
|
const matchesComparison = (item, comparison) => {
|
|
8479
8454
|
const match = COMPARISON.exec(comparison.trim());
|
|
8480
8455
|
if (!match) throw unsupportedFilter(comparison);
|
|
@@ -8498,7 +8473,7 @@ const matchesComparison = (item, comparison) => {
|
|
|
8498
8473
|
default: return compareValues(actual, values[0]) <= 0;
|
|
8499
8474
|
}
|
|
8500
8475
|
};
|
|
8501
|
-
/** Splits on `separator` outside quotes and parentheses
|
|
8476
|
+
/** Splits on `separator` outside quotes and parentheses. */
|
|
8502
8477
|
const splitTopLevel = (text, separator) => {
|
|
8503
8478
|
const parts = [];
|
|
8504
8479
|
let depth = 0;
|
|
@@ -8520,778 +8495,1993 @@ const splitTopLevel = (text, separator) => {
|
|
|
8520
8495
|
parts.push(current);
|
|
8521
8496
|
return parts;
|
|
8522
8497
|
};
|
|
8498
|
+
/** The expression inside `( … )` when the parentheses enclose all of `text`, else undefined. */
|
|
8499
|
+
const unwrapGroup = (text) => {
|
|
8500
|
+
if (!text.startsWith("(") || !text.endsWith(")")) return void 0;
|
|
8501
|
+
let depth = 0;
|
|
8502
|
+
let quote = null;
|
|
8503
|
+
for (const [index, char] of [...text].entries()) if (quote) {
|
|
8504
|
+
if (char === quote) quote = null;
|
|
8505
|
+
} else if (char === "\"" || char === "'") quote = char;
|
|
8506
|
+
else if (char === "(") depth++;
|
|
8507
|
+
else if (char === ")" && --depth === 0) return index === text.length - 1 ? text.slice(1, -1) : void 0;
|
|
8508
|
+
};
|
|
8509
|
+
/** Evaluates `,` (or) of `;` (and) terms, where a term is a comparison or a `( … )` group. */
|
|
8510
|
+
const matchesExpression = (item, expression) => splitTopLevel(expression, ",").some((alternative) => splitTopLevel(alternative, ";").every((term) => {
|
|
8511
|
+
const trimmed = term.trim();
|
|
8512
|
+
if (!trimmed) throw unsupportedFilter(expression);
|
|
8513
|
+
const group = unwrapGroup(trimmed);
|
|
8514
|
+
return group === void 0 ? matchesComparison(item, trimmed) : matchesExpression(item, group);
|
|
8515
|
+
}));
|
|
8523
8516
|
/**
|
|
8524
|
-
* Applies an RSQL `filter` like the API
|
|
8525
|
-
* (
|
|
8517
|
+
* Applies an RSQL `filter` like the API: comparisons joined by `;` (and), which binds tighter
|
|
8518
|
+
* than `,` (or), and grouped with parentheses. Anything it can't evaluate throws instead of
|
|
8519
|
+
* returning wrong results.
|
|
8526
8520
|
*/
|
|
8527
8521
|
const filterAssets = (items, filter) => {
|
|
8528
8522
|
if (!filter) return items;
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
return items.filter((item) => alternatives.some((conditions) => conditions.every((condition) => matchesComparison(item, condition))));
|
|
8523
|
+
if (items.length === 0) matchesExpression({}, filter);
|
|
8524
|
+
return items.filter((item) => matchesExpression(item, filter));
|
|
8532
8525
|
};
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
const
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8526
|
+
const MAX_SEARCH_TOKENS = 8;
|
|
8527
|
+
/** The keyword fields the API searches, in the order it reports matches. */
|
|
8528
|
+
const searchFieldValues = (asset) => [
|
|
8529
|
+
["name", asset.name],
|
|
8530
|
+
["id", asset.id],
|
|
8531
|
+
["externalId", asset.externalId ?? ""],
|
|
8532
|
+
["description", asset.description ?? ""],
|
|
8533
|
+
["tags", asset.tags?.join(" ") ?? ""],
|
|
8534
|
+
["type", asset.type],
|
|
8535
|
+
["content.format", asset.content?.format ?? ""],
|
|
8536
|
+
["content.extension", asset.content?.extension ?? ""],
|
|
8537
|
+
["classification.moderation.status", asset.classification.moderation.status]
|
|
8538
|
+
];
|
|
8539
|
+
/** The API's keyword score for the whole lowercased query. */
|
|
8540
|
+
const keywordScore = (values, query) => {
|
|
8541
|
+
const id = values.get("id");
|
|
8542
|
+
const externalId = values.get("externalId");
|
|
8543
|
+
const name = values.get("name");
|
|
8544
|
+
if (id === query) return 1e3;
|
|
8545
|
+
if (externalId === query) return 950;
|
|
8546
|
+
if (name === query) return 900;
|
|
8547
|
+
if (id.startsWith(query)) return 800;
|
|
8548
|
+
if (externalId.startsWith(query)) return 750;
|
|
8549
|
+
if (name.startsWith(query)) return 700;
|
|
8550
|
+
if (name.includes(query)) return 600;
|
|
8551
|
+
if (values.get("tags").includes(query)) return 500;
|
|
8552
|
+
return 100;
|
|
8542
8553
|
};
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8554
|
+
/**
|
|
8555
|
+
* Keyword search as the API runs it: every whitespace-separated token (case-insensitive, at
|
|
8556
|
+
* most 8) must appear in some searchable field. Results are ranked by the API's score, then
|
|
8557
|
+
* newest first.
|
|
8558
|
+
*/
|
|
8559
|
+
const searchAssets = (assets, query, limit) => {
|
|
8560
|
+
const normalized = query.trim().toLowerCase();
|
|
8561
|
+
const tokens = [...new Set(normalized.split(/\s+/).filter(Boolean))].slice(0, MAX_SEARCH_TOKENS);
|
|
8562
|
+
if (tokens.length === 0) return [];
|
|
8563
|
+
return assets.map((asset) => {
|
|
8564
|
+
const fields = searchFieldValues(asset).map(([field, value]) => [field, value.toLowerCase()]);
|
|
8565
|
+
return {
|
|
8566
|
+
asset,
|
|
8567
|
+
matches: tokens.every((token) => fields.some(([, value]) => value.includes(token))),
|
|
8568
|
+
score: keywordScore(new Map(fields), normalized),
|
|
8569
|
+
matchedFields: fields.filter(([, value]) => tokens.some((token) => value.includes(token))).map(([field]) => field)
|
|
8570
|
+
};
|
|
8571
|
+
}).filter((result) => result.matches).sort((a, b) => b.score - a.score || b.asset.createdAt - a.asset.createdAt).slice(0, limit).map(({ asset, score, matchedFields }) => ({
|
|
8572
|
+
asset,
|
|
8573
|
+
score,
|
|
8574
|
+
matchedFields
|
|
8575
|
+
}));
|
|
8576
|
+
};
|
|
8577
|
+
// src/testing/MockCalmLensApi.ts
|
|
8578
|
+
const REVIEW_DECISIONS = [
|
|
8579
|
+
"allowed",
|
|
8580
|
+
"limited",
|
|
8581
|
+
"age-restricted",
|
|
8582
|
+
"blocked",
|
|
8583
|
+
"purged"
|
|
8584
|
+
];
|
|
8585
|
+
const MAX_PAGE_SIZE = 100;
|
|
8586
|
+
const MAX_DECISION_NOTE_LENGTH = 1499;
|
|
8587
|
+
/** How the API names a reviewer acting through an unnamed API key. */
|
|
8588
|
+
const API_KEY_REVIEWER_NAME = "An API key";
|
|
8589
|
+
const MAX_SEARCH_LIMIT = 20;
|
|
8590
|
+
const MAX_SEARCH_QUERY_LENGTH = 200;
|
|
8591
|
+
/**
|
|
8592
|
+
* The ThumbHash the mock gives every classified image, standing in for one computed from its
|
|
8593
|
+
* pixels. The `placeholder` resource serves it as a PNG, like the CDN.
|
|
8594
|
+
*/
|
|
8595
|
+
const MOCK_THUMBHASH = "1QcSHQRnh493V4dIh4eXh1h4kJUI";
|
|
8596
|
+
/** Like the API: 1 to 255 printable ASCII characters, once HTTP has trimmed the header value. */
|
|
8597
|
+
const isValidIdempotencyKey = (key) => key.length > 0 && key.length <= 255 && /^[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?$/.test(key);
|
|
8598
|
+
/** Like the CDN, content a browser could run as a document is served as a download. */
|
|
8599
|
+
const isActiveContentType = (type) => {
|
|
8600
|
+
const normalized = type?.toLowerCase().split(";", 1)[0]?.trim();
|
|
8601
|
+
return Boolean(normalized && ([
|
|
8602
|
+
"image/svg+xml",
|
|
8603
|
+
"application/xhtml+xml",
|
|
8604
|
+
"text/html",
|
|
8605
|
+
"application/xml",
|
|
8606
|
+
"text/xml"
|
|
8607
|
+
].includes(normalized) || normalized.endsWith("+xml")));
|
|
8608
|
+
};
|
|
8609
|
+
/** The asset snapshot events carry (getEventAsset in the API). */
|
|
8610
|
+
const toEventAsset = (asset) => ({
|
|
8611
|
+
id: asset.id,
|
|
8612
|
+
externalId: asset.externalId ?? null,
|
|
8613
|
+
uploadPresetId: asset.uploadPresetId ?? null,
|
|
8614
|
+
upload: { status: asset.upload.status },
|
|
8615
|
+
classification: {
|
|
8616
|
+
status: asset.classification.status,
|
|
8617
|
+
moderation: { status: asset.classification.moderation.status }
|
|
8618
|
+
},
|
|
8619
|
+
content: { status: asset.content.status }
|
|
8620
|
+
});
|
|
8621
|
+
/** A `moderate` answer or `completeProcessing` decision as a checked `MockDecision`. */
|
|
8622
|
+
const toDecision = (value) => {
|
|
8623
|
+
if (value instanceof MockDecision) return value;
|
|
8624
|
+
if (typeof value === "string") return new MockDecision(value);
|
|
8625
|
+
throw require_ModerationContextLimits.invalidArgument("a decision must be a status or a mockDecision(...)");
|
|
8626
|
+
};
|
|
8627
|
+
/** An asset's portal link, as event facts carry it. */
|
|
8628
|
+
const assetNameFact = (asset) => ({
|
|
8629
|
+
key: "asset-name",
|
|
8630
|
+
value: asset.name,
|
|
8631
|
+
url: `https://calmlens.com/go/asset/${asset.id}`
|
|
8632
|
+
});
|
|
8633
|
+
/** Removing content removes the placeholder and average colour with it. */
|
|
8634
|
+
const withoutPlaceholder = (image) => image ? {
|
|
8635
|
+
...image,
|
|
8636
|
+
thumbhash: null,
|
|
8637
|
+
averageColor: null
|
|
8638
|
+
} : image;
|
|
8639
|
+
/** An asset's type from its content type, or `website` for a URL. */
|
|
8640
|
+
const inferAssetType = (contentType) => {
|
|
8641
|
+
if (contentType === void 0) return "website";
|
|
8642
|
+
const type = contentType.toLowerCase();
|
|
8643
|
+
if (type.startsWith("image/")) return "image";
|
|
8644
|
+
if (type.startsWith("video/")) return "video";
|
|
8645
|
+
if (type.startsWith("audio/")) return "audio";
|
|
8646
|
+
if (type.startsWith("text/")) return "plaintext-document";
|
|
8647
|
+
if (type.includes("pdf") || type.includes("document")) return "multimedia-document";
|
|
8648
|
+
return "file";
|
|
8649
|
+
};
|
|
8650
|
+
/** The API's answer to a request it refuses: thrown by handlers, sent as a JSON error body. */
|
|
8651
|
+
var ApiFailure = class {
|
|
8652
|
+
constructor(status, code, message, details) {
|
|
8653
|
+
this.status = status;
|
|
8654
|
+
this.code = code;
|
|
8655
|
+
this.message = message;
|
|
8656
|
+
this.details = details;
|
|
8657
|
+
}
|
|
8658
|
+
};
|
|
8659
|
+
const fail = (code, { message, details } = {}) => new ApiFailure(require_ModerationContextLimits.API_ERROR_DEFINITIONS[code].status, code, message ?? require_ModerationContextLimits.API_ERROR_DEFINITIONS[code].message, details);
|
|
8660
|
+
/** A request the API's schema refuses: `validation/<zod issue>` with HTTP 400 and the field. */
|
|
8661
|
+
const invalidField = (code, path) => new ApiFailure(400, code, `Invalid ${path}`, { path });
|
|
8662
|
+
/** The client methods that send a request, the one it is recorded under first. */
|
|
8663
|
+
const operationsOf = (request) => {
|
|
8664
|
+
const { method, segments, query, cdn } = request;
|
|
8665
|
+
if (cdn) return ["cdn.serve"];
|
|
8666
|
+
const [root] = segments;
|
|
8667
|
+
if (root === "projects") {
|
|
8668
|
+
switch (segments.slice(2).join("/")) {
|
|
8669
|
+
case "events": return ["events.list", "events.pages"];
|
|
8670
|
+
case "lenses": return ["ensureLenses", "ensureLens"];
|
|
8671
|
+
case "folders": return method === "GET" ? ["folders.list"] : ["folders.create"];
|
|
8672
|
+
case "hash-lists": return method === "GET" ? ["hashLists.list"] : ["hashLists.create"];
|
|
8673
|
+
case "assets/search": return ["assets.search"];
|
|
8674
|
+
case "assets/batch": return ["assets.batch"];
|
|
8675
|
+
case "assets":
|
|
8676
|
+
if (method === "GET") return ["assets.list", "assets.iterate"];
|
|
8677
|
+
if (query.get("queue") === "true") return ["assets.create"];
|
|
8678
|
+
return isTextUpload(request) ? ["assets.classify", "text.classify"] : ["assets.classify"];
|
|
8679
|
+
}
|
|
8680
|
+
return [];
|
|
8556
8681
|
}
|
|
8557
|
-
|
|
8682
|
+
if (root === "upload-presets") {
|
|
8683
|
+
if (segments[3] === void 0) return [query.get("queue") === "true" ? "assets.create" : "assets.classify"];
|
|
8684
|
+
return segments[4] === "part" ? ["assets.uploadPart"] : ["assets.retrieve"];
|
|
8685
|
+
}
|
|
8686
|
+
const id = segments[1];
|
|
8687
|
+
const action = segments[2];
|
|
8688
|
+
if (root === "folders" && id !== void 0) {
|
|
8689
|
+
if (action === "move") return ["folders.move"];
|
|
8690
|
+
return method === "GET" ? ["folders.retrieve"] : method === "PATCH" ? ["folders.update"] : ["folders.delete"];
|
|
8691
|
+
}
|
|
8692
|
+
if (root === "hash-lists" && id !== void 0) {
|
|
8693
|
+
if (action === "entries") return method === "GET" ? ["hashLists.listEntries"] : ["hashLists.addEntry"];
|
|
8694
|
+
return method === "GET" ? ["hashLists.retrieve"] : method === "PATCH" ? ["hashLists.update"] : ["hashLists.delete"];
|
|
8695
|
+
}
|
|
8696
|
+
if (root === "hash-list-entries" && id !== void 0) return ["hashLists.removeEntry"];
|
|
8697
|
+
if (root !== "assets" || id === void 0) return [];
|
|
8698
|
+
switch (action) {
|
|
8699
|
+
case void 0: return method === "GET" ? ["assets.retrieve", "assets.waitForDecision"] : method === "PATCH" ? ["assets.update"] : ["assets.delete"];
|
|
8700
|
+
case "content": return ["assets.deleteContent"];
|
|
8701
|
+
case "children": return ["assets.listChildren", "assets.iterateChildren"];
|
|
8702
|
+
case "signed-url": return ["assets.createSignedUrl"];
|
|
8703
|
+
case "part": return ["assets.uploadPart"];
|
|
8704
|
+
case "move": return ["assets.move"];
|
|
8705
|
+
case "retry": return ["assets.retry"];
|
|
8706
|
+
case "explain": return ["assets.explain"];
|
|
8707
|
+
case "decision": return ["assets.decide"];
|
|
8708
|
+
}
|
|
8709
|
+
return [];
|
|
8710
|
+
};
|
|
8711
|
+
const isTextUpload = (request) => request.headers.get("x-calmlens-upload") === "content" && request.headers.get("content-type") === "text/plain";
|
|
8712
|
+
/** Like the API: a new folder is 201 Created, a queued batch 202 Accepted, everything else 200. */
|
|
8713
|
+
const SUCCESS_STATUS = {
|
|
8714
|
+
"folders.create": 201,
|
|
8715
|
+
"hashLists.create": 201,
|
|
8716
|
+
"assets.batch": 202
|
|
8717
|
+
};
|
|
8718
|
+
const decodeSegment = (segment) => {
|
|
8719
|
+
try {
|
|
8720
|
+
return decodeURIComponent(segment);
|
|
8721
|
+
} catch {
|
|
8722
|
+
return segment;
|
|
8723
|
+
}
|
|
8724
|
+
};
|
|
8725
|
+
const createMonotonicClock = () => {
|
|
8726
|
+
let last = 0;
|
|
8727
|
+
return () => {
|
|
8728
|
+
last = Math.max(Date.now(), last + 1);
|
|
8729
|
+
return last;
|
|
8730
|
+
};
|
|
8558
8731
|
};
|
|
8559
8732
|
/**
|
|
8560
|
-
*
|
|
8561
|
-
*
|
|
8562
|
-
* client's, so error handling can be tested without a network.
|
|
8733
|
+
* The in-memory CalmLens API and CDN one or more mock clients share: storage, events, lenses,
|
|
8734
|
+
* idempotency keys, upload presets, the API key's grants, queued failures and recorded requests.
|
|
8563
8735
|
*/
|
|
8564
|
-
var
|
|
8565
|
-
/** Accepts the same options as `CalmLensServerClient` (validated the same way), plus `mockOptions`. */
|
|
8736
|
+
var MockCalmLensApi = class {
|
|
8566
8737
|
constructor(options) {
|
|
8738
|
+
this.calls = [];
|
|
8567
8739
|
this.lenses = /* @__PURE__ */ new Map();
|
|
8568
8740
|
this.eventFeed = [];
|
|
8569
|
-
this.
|
|
8741
|
+
this.openUploads = /* @__PURE__ */ new Map();
|
|
8570
8742
|
this.idempotentResults = /* @__PURE__ */ new Map();
|
|
8571
|
-
this.requestCount = 0;
|
|
8572
8743
|
this.pendingFailures = [];
|
|
8573
|
-
this.
|
|
8574
|
-
|
|
8744
|
+
this.requestCount = 0;
|
|
8745
|
+
this.waitedMs = 0;
|
|
8746
|
+
this.hashLists = /* @__PURE__ */ new Map();
|
|
8747
|
+
this.hashListEntries = /* @__PURE__ */ new Map();
|
|
8748
|
+
this.pageFailure = (code, path) => code === "validation/invalid-cursor" ? fail(require_ModerationContextLimits.API_ERROR_CODES.INVALID_CURSOR) : invalidField(code, path ?? "pageSize");
|
|
8749
|
+
this.options = options;
|
|
8575
8750
|
this.projectId = options.projectId;
|
|
8576
8751
|
this.apiKey = options.apiKey;
|
|
8577
|
-
this.
|
|
8578
|
-
this.
|
|
8579
|
-
this.
|
|
8580
|
-
this.
|
|
8581
|
-
this.
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
}
|
|
8587
|
-
/** Keeps the API key out of `console.log` and `util.inspect`. */
|
|
8588
|
-
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
8589
|
-
return `MockCalmLensServerClient { projectId: ${JSON.stringify(this.projectId)} }`;
|
|
8590
|
-
}
|
|
8591
|
-
/**
|
|
8592
|
-
* Makes the next call (or the next call to `operation`) throw `failure` instead of running,
|
|
8593
|
-
* as if the request never completed. Queue several to fail several calls in a row.
|
|
8594
|
-
*/
|
|
8595
|
-
failNextRequest(failure = "transport-error", { operation } = {}) {
|
|
8752
|
+
this.grants = options.grants;
|
|
8753
|
+
this.databaseStorage = options.databaseStorage;
|
|
8754
|
+
this.imageStorage = options.imageStorage;
|
|
8755
|
+
this.createId = options.createId;
|
|
8756
|
+
this.now = options.now;
|
|
8757
|
+
}
|
|
8758
|
+
/** Queues a failure for the next request, or the next one `operation` sends. */
|
|
8759
|
+
failNextRequest(failure, options = {}) {
|
|
8760
|
+
if (options.retryAfter !== void 0 && !(Number.isInteger(options.retryAfter) && options.retryAfter >= 0)) throw require_ModerationContextLimits.invalidArgument("retryAfter must be a whole number of seconds");
|
|
8596
8761
|
this.pendingFailures.push({
|
|
8597
8762
|
failure,
|
|
8598
|
-
|
|
8763
|
+
...options
|
|
8599
8764
|
});
|
|
8600
8765
|
}
|
|
8601
|
-
/**
|
|
8602
|
-
|
|
8603
|
-
return
|
|
8766
|
+
/** A `fetch` that answers a client's requests from memory; app clients pass their session. */
|
|
8767
|
+
createFetch(endpoints, session) {
|
|
8768
|
+
return ((input, init) => this.handle(input, init, endpoints(), session));
|
|
8604
8769
|
}
|
|
8605
8770
|
/**
|
|
8606
|
-
* Adds an event to the feed `events.list` reads, as if CalmLens had recorded it
|
|
8607
|
-
* stored event
|
|
8771
|
+
* Adds an event to the feed `events.list` reads, as if CalmLens had recorded it, and returns
|
|
8772
|
+
* the stored event.
|
|
8608
8773
|
*/
|
|
8609
|
-
enqueueEvent(input) {
|
|
8774
|
+
async enqueueEvent(input) {
|
|
8775
|
+
const asset = input.asset === void 0 && input.assetId ? await this.databaseStorage.getAsset(input.assetId) : void 0;
|
|
8776
|
+
return this.pushEvent(asset ? {
|
|
8777
|
+
...input,
|
|
8778
|
+
asset: toEventAsset(asset)
|
|
8779
|
+
} : input);
|
|
8780
|
+
}
|
|
8781
|
+
/** Adds an event to the feed, filling the fields the API always sets. */
|
|
8782
|
+
pushEvent(input) {
|
|
8610
8783
|
const now = this.now();
|
|
8611
8784
|
const event = {
|
|
8612
|
-
id: this.createId(),
|
|
8785
|
+
id: this.createId("evt"),
|
|
8613
8786
|
projectId: this.projectId,
|
|
8614
8787
|
severity: "low",
|
|
8615
8788
|
title: input.type,
|
|
8789
|
+
description: null,
|
|
8790
|
+
assetId: null,
|
|
8791
|
+
asset: null,
|
|
8616
8792
|
facts: [],
|
|
8617
8793
|
createdAt: now,
|
|
8618
|
-
updatedAt: now,
|
|
8619
8794
|
...input
|
|
8620
8795
|
};
|
|
8621
8796
|
this.eventFeed.push(event);
|
|
8622
8797
|
return event;
|
|
8623
8798
|
}
|
|
8624
|
-
/**
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
"content-type": "application/json",
|
|
8635
|
-
"x-request-id": requestId
|
|
8636
|
-
})
|
|
8637
|
-
});
|
|
8799
|
+
/** Applies a reviewer's decision to a stored asset (see `MockCalmLensServerClient.decide`). */
|
|
8800
|
+
async decide(assetId, decision) {
|
|
8801
|
+
if (!REVIEW_DECISIONS.includes(decision)) throw require_ModerationContextLimits.invalidArgument(`decision must be one of ${REVIEW_DECISIONS.join(", ")}`);
|
|
8802
|
+
let asset;
|
|
8803
|
+
try {
|
|
8804
|
+
asset = await this.findOwnAsset(assetId);
|
|
8805
|
+
} catch (failure) {
|
|
8806
|
+
throw this.toError(failure);
|
|
8807
|
+
}
|
|
8808
|
+
return structuredClone(await this.recordDecision(asset, decision));
|
|
8638
8809
|
}
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8810
|
+
/**
|
|
8811
|
+
* Finishes processing a pending asset as the pipeline would (see
|
|
8812
|
+
* `MockCalmLensServerClient.completeProcessing`): with `decision`, or else what `moderate`
|
|
8813
|
+
* returns for the `complete` trigger.
|
|
8814
|
+
*/
|
|
8815
|
+
async completeProcessing(assetId, decision) {
|
|
8816
|
+
let asset;
|
|
8817
|
+
try {
|
|
8818
|
+
asset = await this.findOwnAsset(assetId);
|
|
8819
|
+
} catch (failure) {
|
|
8820
|
+
throw this.toError(failure);
|
|
8821
|
+
}
|
|
8822
|
+
if (asset.classification.status !== "processing") throw require_ModerationContextLimits.invalidArgument(`asset ${assetId} isn't processing`);
|
|
8823
|
+
const settled = decision === void 0 ? void 0 : toDecision(decision);
|
|
8824
|
+
return structuredClone(await this.moderateStored(asset, "complete", settled));
|
|
8644
8825
|
}
|
|
8645
|
-
|
|
8646
|
-
|
|
8826
|
+
/**
|
|
8827
|
+
* `POST /assets/:assetId/decision`, like the API: an asset still uploading or processing can't
|
|
8828
|
+
* be decided, one pending moderation can only be blocked or purged, a decision must change the
|
|
8829
|
+
* status and purged content can only be blocked. Mock API keys have no name, so the reviewer is
|
|
8830
|
+
* "An API key".
|
|
8831
|
+
*/
|
|
8832
|
+
async decideAsset(assetId, body) {
|
|
8833
|
+
const { decision, note: rawNote } = body;
|
|
8834
|
+
if (decision === "purged") {
|
|
8835
|
+
if (body.confirmPurge !== true) throw invalidField("validation/invalid-value", "confirmPurge");
|
|
8836
|
+
} else if (!REVIEW_DECISIONS.includes(decision)) throw invalidField("validation/invalid-value", "decision");
|
|
8837
|
+
if (typeof rawNote !== "string") throw invalidField("validation/invalid-type", "note");
|
|
8838
|
+
const note = rawNote.trim();
|
|
8839
|
+
if (!note) throw invalidField("validation/too-small", "note");
|
|
8840
|
+
if (note.length > MAX_DECISION_NOTE_LENGTH) throw invalidField("validation/too-big", "note");
|
|
8841
|
+
const asset = await this.findOwnAsset(assetId);
|
|
8842
|
+
if (asset.upload.status === "uploading" || asset.classification.status === "processing") throw fail(require_ModerationContextLimits.API_ERROR_CODES.ASSET_PROCESSING);
|
|
8843
|
+
const status = asset.classification.moderation.status;
|
|
8844
|
+
if (decision !== "purged" && decision !== "blocked" && status === "pending") throw fail(require_ModerationContextLimits.API_ERROR_CODES.ASSET_DECISION_NOT_ALLOWED);
|
|
8845
|
+
if (status === decision) throw fail(require_ModerationContextLimits.API_ERROR_CODES.ASSET_DECISION_UNCHANGED);
|
|
8846
|
+
if (decision !== "purged" && decision !== "blocked" && status === "purged") throw fail(require_ModerationContextLimits.API_ERROR_CODES.ASSET_CONTENT_PURGED);
|
|
8847
|
+
return this.recordDecision(asset, decision, {
|
|
8848
|
+
note,
|
|
8849
|
+
reviewerName: API_KEY_REVIEWER_NAME
|
|
8850
|
+
});
|
|
8647
8851
|
}
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8852
|
+
/**
|
|
8853
|
+
* Sets the reviewer's decision: the moderation status, a manual review and the
|
|
8854
|
+
* `asset.<status>` event (after `asset.content-removed` when purging). The test hook records no
|
|
8855
|
+
* reviewer or note; the API's handler records both.
|
|
8856
|
+
*/
|
|
8857
|
+
async recordDecision(asset, decision, review) {
|
|
8858
|
+
const decided = await this.applyOutcome(asset, decision, "requested");
|
|
8859
|
+
const now = this.now();
|
|
8860
|
+
decided.classification.moderation.manualReviews = [...decided.classification.moderation.manualReviews ?? [], {
|
|
8861
|
+
id: this.createId("rev"),
|
|
8862
|
+
decision,
|
|
8863
|
+
note: review?.note ?? null,
|
|
8864
|
+
...review && {
|
|
8865
|
+
reviewerName: review.reviewerName,
|
|
8866
|
+
reviewerEmail: null
|
|
8867
|
+
},
|
|
8868
|
+
reviewedAt: now,
|
|
8869
|
+
appliedToAsset: true
|
|
8870
|
+
}];
|
|
8871
|
+
const stored = await this.databaseStorage.updateAsset(decided);
|
|
8872
|
+
if (decision === "purged") this.recordLifecycleEvent("asset.content-removed", stored);
|
|
8873
|
+
this.pushEvent({
|
|
8874
|
+
type: `asset.${decision}`,
|
|
8875
|
+
severity: decision === "allowed" || decision === "limited" ? "low" : "high",
|
|
8876
|
+
title: `asset.${decision}`,
|
|
8877
|
+
assetId: stored.id,
|
|
8878
|
+
asset: toEventAsset(stored),
|
|
8879
|
+
facts: [
|
|
8880
|
+
{
|
|
8881
|
+
key: "decided-by",
|
|
8882
|
+
value: "reviewer"
|
|
8883
|
+
},
|
|
8884
|
+
{
|
|
8885
|
+
key: "asset-type",
|
|
8886
|
+
value: stored.type
|
|
8887
|
+
},
|
|
8888
|
+
{
|
|
8889
|
+
key: "asset-name",
|
|
8890
|
+
value: stored.name,
|
|
8891
|
+
url: `https://calmlens.com/go/asset/${stored.id}`
|
|
8892
|
+
},
|
|
8893
|
+
...review ? [{
|
|
8894
|
+
key: "reviewer-user-id",
|
|
8895
|
+
value: null
|
|
8896
|
+
}, {
|
|
8897
|
+
key: "reviewer-name",
|
|
8898
|
+
value: review.reviewerName
|
|
8899
|
+
}] : [],
|
|
8900
|
+
{
|
|
8901
|
+
key: "note",
|
|
8902
|
+
value: review?.note ?? null
|
|
8903
|
+
}
|
|
8904
|
+
]
|
|
8905
|
+
});
|
|
8906
|
+
return stored;
|
|
8653
8907
|
}
|
|
8654
|
-
|
|
8908
|
+
/** The `CalmLensError` the real client throws for an API failure, for helpers that send none. */
|
|
8909
|
+
toError(failure) {
|
|
8910
|
+
if (!(failure instanceof ApiFailure)) return failure;
|
|
8911
|
+
const requestId = `mock-request-${++this.requestCount}`;
|
|
8912
|
+
return new require_ModerationContextLimits.CalmLensError(failure.code, {
|
|
8913
|
+
message: failure.message,
|
|
8914
|
+
status: failure.status,
|
|
8915
|
+
details: failure.details ?? {},
|
|
8916
|
+
requestId,
|
|
8917
|
+
headers: new Headers({
|
|
8918
|
+
"content-type": "application/json",
|
|
8919
|
+
"x-request-id": requestId
|
|
8920
|
+
})
|
|
8921
|
+
});
|
|
8922
|
+
}
|
|
8923
|
+
async handle(input, init, endpoints, session) {
|
|
8924
|
+
if (init?.signal?.aborted) throw init.signal.reason;
|
|
8925
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
8926
|
+
const cdn = url.startsWith(`${endpoints.cdnBaseUrl}/`);
|
|
8927
|
+
const base = cdn ? endpoints.cdnBaseUrl : endpoints.baseUrl;
|
|
8928
|
+
if (!url.startsWith(`${base}/`)) throw new TypeError(`MockCalmLensServerClient does not serve ${url}`);
|
|
8929
|
+
const relative = url.slice(base.length);
|
|
8930
|
+
const queryIndex = relative.indexOf("?");
|
|
8931
|
+
const path = queryIndex === -1 ? relative : relative.slice(0, queryIndex);
|
|
8932
|
+
const headers = new Headers(init?.headers);
|
|
8933
|
+
const bytes = init?.body === void 0 || init.body === null ? /* @__PURE__ */ new Uint8Array(0) : new Uint8Array(await new Response(init.body).arrayBuffer());
|
|
8934
|
+
const request = {
|
|
8935
|
+
method: (init?.method ?? "GET").toUpperCase(),
|
|
8936
|
+
url,
|
|
8937
|
+
segments: path.split("/").filter(Boolean).map(decodeSegment),
|
|
8938
|
+
query: new URLSearchParams(queryIndex === -1 ? "" : relative.slice(queryIndex + 1)),
|
|
8939
|
+
headers,
|
|
8940
|
+
bytes,
|
|
8941
|
+
cdn
|
|
8942
|
+
};
|
|
8943
|
+
const operations = operationsOf(request);
|
|
8944
|
+
const call = {
|
|
8945
|
+
operation: operations[0] ?? "unknown",
|
|
8946
|
+
method: request.method,
|
|
8947
|
+
url,
|
|
8948
|
+
path: `/${request.segments.join("/")}`,
|
|
8949
|
+
query: Object.fromEntries(request.query),
|
|
8950
|
+
headers: Object.fromEntries(headers),
|
|
8951
|
+
body: this.describeBody(request),
|
|
8952
|
+
status: void 0
|
|
8953
|
+
};
|
|
8655
8954
|
this.calls.push({
|
|
8656
|
-
|
|
8657
|
-
|
|
8955
|
+
call,
|
|
8956
|
+
operations
|
|
8658
8957
|
});
|
|
8659
|
-
const index = this.pendingFailures.findIndex((pending) => pending.operation === void 0 || pending.operation
|
|
8958
|
+
const index = this.pendingFailures.findIndex((pending) => pending.operation === void 0 || operations.includes(pending.operation));
|
|
8660
8959
|
if (index !== -1) {
|
|
8661
|
-
const [
|
|
8662
|
-
|
|
8960
|
+
const [{ failure, details, retryAfter }] = this.pendingFailures.splice(index, 1);
|
|
8961
|
+
if (typeof failure === "string" && failure in require_ModerationContextLimits.API_ERROR_DEFINITIONS) return this.answered(call, this.failureResponse(fail(failure, { details }), url, retryAfter));
|
|
8962
|
+
throw this.toFailureError(failure);
|
|
8663
8963
|
}
|
|
8964
|
+
const response = cdn ? await this.serveCdn(request) : await this.answer(request, operations[0], session);
|
|
8965
|
+
return this.answered(call, response);
|
|
8664
8966
|
}
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8967
|
+
answered(call, response) {
|
|
8968
|
+
call.status = response.status;
|
|
8969
|
+
return response;
|
|
8970
|
+
}
|
|
8971
|
+
/** What `fetch` throws for a simulated network failure, as the real client reports it. */
|
|
8972
|
+
toFailureError(failure) {
|
|
8973
|
+
if (failure === "transport-error") return /* @__PURE__ */ new TypeError("Simulated network failure");
|
|
8974
|
+
if (failure === "timeout") {
|
|
8975
|
+
const cause = /* @__PURE__ */ new Error("The operation timed out.");
|
|
8976
|
+
cause.name = "TimeoutError";
|
|
8977
|
+
return new require_ModerationContextLimits.CalmLensError(require_ModerationContextLimits.SDK_ERROR_CODES.TIMEOUT, {
|
|
8978
|
+
message: "Request timed out",
|
|
8979
|
+
cause
|
|
8980
|
+
});
|
|
8981
|
+
}
|
|
8982
|
+
return failure;
|
|
8983
|
+
}
|
|
8984
|
+
describeBody({ bytes, headers }) {
|
|
8985
|
+
if (bytes.byteLength === 0) return void 0;
|
|
8986
|
+
const type = headers.get("content-type") ?? "";
|
|
8987
|
+
const text = () => new TextDecoder().decode(bytes);
|
|
8988
|
+
if (type === "application/json") try {
|
|
8989
|
+
return JSON.parse(text());
|
|
8990
|
+
} catch {
|
|
8991
|
+
return text();
|
|
8992
|
+
}
|
|
8993
|
+
return type.startsWith("text/") ? text() : bytes.slice();
|
|
8994
|
+
}
|
|
8995
|
+
response(status, body, url, headers, requestId = `mock-request-${++this.requestCount}`) {
|
|
8996
|
+
const response = new Response(body, {
|
|
8668
8997
|
status,
|
|
8669
8998
|
headers: {
|
|
8670
|
-
|
|
8671
|
-
"
|
|
8999
|
+
...headers,
|
|
9000
|
+
"X-Request-Id": requestId
|
|
8672
9001
|
}
|
|
8673
9002
|
});
|
|
8674
9003
|
Object.defineProperty(response, "url", { value: url });
|
|
8675
9004
|
return response;
|
|
8676
9005
|
}
|
|
8677
|
-
|
|
8678
|
-
return {
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
}
|
|
8716
|
-
const requestId = `mock-request-${++this.requestCount}`;
|
|
8717
|
-
const isHead = init?.method?.toUpperCase() === "HEAD";
|
|
8718
|
-
const response = new Response(isHead ? null : bytes, {
|
|
8719
|
-
status: 200,
|
|
8720
|
-
headers: {
|
|
8721
|
-
"Content-Type": storedContent.type ?? "application/octet-stream",
|
|
8722
|
-
"Content-Length": String(byteLength),
|
|
8723
|
-
"Cache-Control": asset.visibility === "public" ? "public, max-age=31536000" : "private, no-store",
|
|
8724
|
-
ETag: `"${asset.id}-${asset.updatedAt}"`,
|
|
8725
|
-
"X-Request-Id": requestId,
|
|
8726
|
-
"X-Mock-CalmLens-CDN-URL": url
|
|
8727
|
-
}
|
|
9006
|
+
jsonResponse(status, body, url, headers = {}, requestId) {
|
|
9007
|
+
return this.response(status, JSON.stringify(body), url, {
|
|
9008
|
+
"Content-Type": "application/json",
|
|
9009
|
+
...headers
|
|
9010
|
+
}, requestId);
|
|
9011
|
+
}
|
|
9012
|
+
/** The API's error body: `{ code, message, requestId, details }`, `requestId` as in the header. */
|
|
9013
|
+
failureResponse(failure, url, retryAfter) {
|
|
9014
|
+
const requestId = `mock-request-${++this.requestCount}`;
|
|
9015
|
+
return this.jsonResponse(failure.status, {
|
|
9016
|
+
code: failure.code,
|
|
9017
|
+
message: failure.message,
|
|
9018
|
+
requestId,
|
|
9019
|
+
details: failure.details ?? {}
|
|
9020
|
+
}, url, retryAfter === void 0 ? {} : { "Retry-After": String(retryAfter) }, requestId);
|
|
9021
|
+
}
|
|
9022
|
+
/** Answers an API request: the API key, idempotency, then the route. */
|
|
9023
|
+
async answer(request, operation, session) {
|
|
9024
|
+
const { url, headers } = request;
|
|
9025
|
+
if (!(request.segments[0] === "upload-presets") && headers.get("x-api-key") !== this.apiKey) return this.failureResponse(fail(require_ModerationContextLimits.API_ERROR_CODES.INVALID_API_KEY), url);
|
|
9026
|
+
const key = headers.get("idempotency-key")?.replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, "");
|
|
9027
|
+
const upload = headers.get("x-calmlens-upload");
|
|
9028
|
+
const fingerprint = JSON.stringify([
|
|
9029
|
+
request.method,
|
|
9030
|
+
request.segments,
|
|
9031
|
+
[...request.query].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
|
|
9032
|
+
headers.get("content-type"),
|
|
9033
|
+
upload,
|
|
9034
|
+
upload ? request.bytes.byteLength : new TextDecoder().decode(request.bytes)
|
|
9035
|
+
]);
|
|
9036
|
+
if (key !== void 0) {
|
|
9037
|
+
if (!isValidIdempotencyKey(key)) return this.failureResponse(fail(require_ModerationContextLimits.API_ERROR_CODES.INVALID_IDEMPOTENCY_KEY), url);
|
|
9038
|
+
const saved = this.idempotentResults.get(key);
|
|
9039
|
+
if (saved) {
|
|
9040
|
+
if (saved.fingerprint !== fingerprint) return this.failureResponse(fail(require_ModerationContextLimits.API_ERROR_CODES.IDEMPOTENCY_KEY_REUSED), url);
|
|
9041
|
+
return this.response(saved.status, saved.body, url, {
|
|
9042
|
+
"Content-Type": "application/json",
|
|
9043
|
+
"Idempotent-Replayed": "true"
|
|
8728
9044
|
});
|
|
8729
|
-
Object.defineProperty(response, "url", { value: url });
|
|
8730
|
-
return response;
|
|
8731
9045
|
}
|
|
8732
|
-
}
|
|
9046
|
+
}
|
|
9047
|
+
let answer;
|
|
9048
|
+
try {
|
|
9049
|
+
answer = await this.route(request, session);
|
|
9050
|
+
} catch (failure) {
|
|
9051
|
+
if (failure instanceof ApiFailure) return this.failureResponse(failure, url);
|
|
9052
|
+
throw failure;
|
|
9053
|
+
}
|
|
9054
|
+
const [status, body] = answer;
|
|
9055
|
+
const json = JSON.stringify(body ?? {});
|
|
9056
|
+
const finalStatus = operation ? SUCCESS_STATUS[operation] ?? status : status;
|
|
9057
|
+
if (key !== void 0) this.idempotentResults.set(key, {
|
|
9058
|
+
fingerprint,
|
|
9059
|
+
status: finalStatus,
|
|
9060
|
+
body: json
|
|
9061
|
+
});
|
|
9062
|
+
return this.response(finalStatus, json, url, { "Content-Type": "application/json" });
|
|
9063
|
+
}
|
|
9064
|
+
async route(request, session) {
|
|
9065
|
+
const { method, segments } = request;
|
|
9066
|
+
const [root, id, action, ...rest] = segments;
|
|
9067
|
+
if (root === "projects") {
|
|
9068
|
+
if (id !== this.projectId) throw fail(require_ModerationContextLimits.API_ERROR_CODES.API_KEY_PROJECT_MISMATCH);
|
|
9069
|
+
switch (`${method} ${segments.slice(2).join("/")}`) {
|
|
9070
|
+
case "POST assets": return [200, await this.createAsset(request)];
|
|
9071
|
+
case "GET assets": return [200, await this.listAssets(request.query)];
|
|
9072
|
+
case "GET assets/search": return [200, await this.searchAssets(request.query)];
|
|
9073
|
+
case "POST assets/batch": return [202, await this.batch(this.json(request))];
|
|
9074
|
+
case "GET events": return [200, this.listEvents(request.query)];
|
|
9075
|
+
case "GET folders": return [200, await this.listFolders(request.query)];
|
|
9076
|
+
case "POST folders": return [201, await this.createFolder(this.json(request))];
|
|
9077
|
+
case "GET hash-lists": return [200, this.listHashLists(request.query)];
|
|
9078
|
+
case "POST hash-lists": return [201, this.createHashList(this.json(request))];
|
|
9079
|
+
case "PUT lenses": return [200, this.ensureLenses(this.json(request))];
|
|
9080
|
+
}
|
|
9081
|
+
}
|
|
9082
|
+
if (root === "upload-presets" && id !== void 0 && action === "assets") return [200, await this.presetRoute(request, id, rest, session)];
|
|
9083
|
+
if (root === "assets" && id !== void 0 && rest.length === 0) switch (`${method} ${action ?? ""}`) {
|
|
9084
|
+
case "GET ": return [200, await this.findOwnAsset(id)];
|
|
9085
|
+
case "PATCH ": return [200, await this.updateAsset(id, this.json(request))];
|
|
9086
|
+
case "DELETE ": return [200, await this.deleteAsset(id)];
|
|
9087
|
+
case "GET children": return [200, await this.listChildren(id, request.query)];
|
|
9088
|
+
case "POST part": return [200, await this.uploadPart(id, request)];
|
|
9089
|
+
case "POST signed-url": return [200, await this.createSignedUrl(id, this.json(request))];
|
|
9090
|
+
case "POST move": return [200, await this.moveAsset(id, this.json(request))];
|
|
9091
|
+
case "POST retry": return [200, await this.retryAsset(id)];
|
|
9092
|
+
case "POST explain": return [200, await this.explainAsset(id, this.json(request))];
|
|
9093
|
+
case "POST decision": return [200, await this.decideAsset(id, this.json(request))];
|
|
9094
|
+
case "DELETE content": return [200, await this.removeContent(await this.findOwnAsset(id))];
|
|
9095
|
+
}
|
|
9096
|
+
if (root === "folders" && id !== void 0 && rest.length === 0) switch (`${method} ${action ?? ""}`) {
|
|
9097
|
+
case "GET ": return [200, await this.findOwnFolder(id)];
|
|
9098
|
+
case "PATCH ": return [200, await this.updateFolder(id, this.json(request))];
|
|
9099
|
+
case "DELETE ": return [200, await this.deleteFolder(id)];
|
|
9100
|
+
case "POST move": return [200, await this.moveFolder(id, this.json(request))];
|
|
9101
|
+
}
|
|
9102
|
+
if (root === "hash-lists" && id !== void 0 && rest.length === 0) switch (`${method} ${action ?? ""}`) {
|
|
9103
|
+
case "GET ": return [200, this.findHashList(id)];
|
|
9104
|
+
case "PATCH ": return [200, this.updateHashList(id, this.json(request))];
|
|
9105
|
+
case "DELETE ": return [200, this.deleteHashList(id)];
|
|
9106
|
+
case "GET entries": return [200, this.listHashListEntries(id, request.query)];
|
|
9107
|
+
case "POST entries": return this.addHashListEntry(id, this.json(request));
|
|
9108
|
+
}
|
|
9109
|
+
if (root === "hash-list-entries" && id !== void 0 && method === "DELETE" && !action) {
|
|
9110
|
+
if (!this.hashListEntries.delete(id)) throw fail(require_ModerationContextLimits.API_ERROR_CODES.HASH_LIST_ENTRY_NOT_FOUND);
|
|
9111
|
+
return [200, {}];
|
|
9112
|
+
}
|
|
9113
|
+
throw fail(require_ModerationContextLimits.API_ERROR_CODES.ROUTE_NOT_FOUND, { message: `No route for ${method} /${segments.join("/")}` });
|
|
8733
9114
|
}
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
9115
|
+
createHashList(body) {
|
|
9116
|
+
const { name, description, action } = body;
|
|
9117
|
+
if (typeof name !== "string" || !name.trim() || action !== "flag" && action !== "block") throw fail(require_ModerationContextLimits.API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "A hash list needs a name and an action of flag or block" });
|
|
9118
|
+
const now = this.now();
|
|
9119
|
+
const list = {
|
|
9120
|
+
id: this.createId("hl"),
|
|
9121
|
+
projectId: this.projectId,
|
|
9122
|
+
name: name.trim(),
|
|
9123
|
+
description: typeof description === "string" ? description : null,
|
|
9124
|
+
action,
|
|
9125
|
+
createdAt: now,
|
|
9126
|
+
updatedAt: now
|
|
8746
9127
|
};
|
|
9128
|
+
this.hashLists.set(list.id, list);
|
|
9129
|
+
return list;
|
|
8747
9130
|
}
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
9131
|
+
listHashLists(query) {
|
|
9132
|
+
return createPage([...this.hashLists.values()], this.pageQuery(query), {
|
|
9133
|
+
scope: `hash-lists:${this.projectId}`,
|
|
9134
|
+
fail: this.pageFailure
|
|
9135
|
+
});
|
|
9136
|
+
}
|
|
9137
|
+
findHashList(hashListId) {
|
|
9138
|
+
const list = this.hashLists.get(hashListId);
|
|
9139
|
+
if (!list) throw fail(require_ModerationContextLimits.API_ERROR_CODES.HASH_LIST_NOT_FOUND);
|
|
9140
|
+
return list;
|
|
9141
|
+
}
|
|
9142
|
+
updateHashList(hashListId, body) {
|
|
9143
|
+
const updated = {
|
|
9144
|
+
...this.findHashList(hashListId),
|
|
9145
|
+
...typeof body.name === "string" ? { name: body.name.trim() } : {},
|
|
9146
|
+
...body.description !== void 0 ? { description: body.description ?? null } : {},
|
|
9147
|
+
...body.action === "flag" || body.action === "block" ? { action: body.action } : {},
|
|
9148
|
+
updatedAt: this.now()
|
|
8755
9149
|
};
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
9150
|
+
this.hashLists.set(hashListId, updated);
|
|
9151
|
+
return updated;
|
|
9152
|
+
}
|
|
9153
|
+
deleteHashList(hashListId) {
|
|
9154
|
+
this.findHashList(hashListId);
|
|
9155
|
+
this.hashLists.delete(hashListId);
|
|
9156
|
+
for (const [id, entry] of this.hashListEntries) if (entry.hashListId === hashListId) this.hashListEntries.delete(id);
|
|
9157
|
+
return {};
|
|
9158
|
+
}
|
|
9159
|
+
listHashListEntries(hashListId, query) {
|
|
9160
|
+
this.findHashList(hashListId);
|
|
9161
|
+
const pageSize = this.integer(query, "pageSize", 50);
|
|
9162
|
+
const entries = [...this.hashListEntries.values()].filter((entry) => entry.hashListId === hashListId).reverse();
|
|
9163
|
+
const cursor = query.get("cursor");
|
|
9164
|
+
const start = cursor ? Number(atob(cursor)) : 0;
|
|
9165
|
+
if (!Number.isInteger(start) || start < 0) throw fail(require_ModerationContextLimits.API_ERROR_CODES.INVALID_CURSOR);
|
|
9166
|
+
const items = entries.slice(start, start + pageSize);
|
|
9167
|
+
const hasMore = start + pageSize < entries.length;
|
|
8761
9168
|
return {
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
9169
|
+
items,
|
|
9170
|
+
hasMore,
|
|
9171
|
+
nextCursor: hasMore ? btoa(String(start + pageSize)) : null,
|
|
9172
|
+
pageSize,
|
|
9173
|
+
ordering: [{
|
|
9174
|
+
field: "createdAt",
|
|
9175
|
+
direction: "desc"
|
|
9176
|
+
}, {
|
|
9177
|
+
field: "id",
|
|
9178
|
+
direction: "desc"
|
|
9179
|
+
}]
|
|
8765
9180
|
};
|
|
8766
9181
|
}
|
|
8767
|
-
async
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
if (!
|
|
8776
|
-
|
|
9182
|
+
async addHashListEntry(hashListId, body) {
|
|
9183
|
+
const list = this.findHashList(hashListId);
|
|
9184
|
+
let hash = typeof body.hash === "string" ? body.hash.trim().toLowerCase() : void 0;
|
|
9185
|
+
let quality = typeof body.quality === "number" ? body.quality : null;
|
|
9186
|
+
const assetId = typeof body.assetId === "string" ? body.assetId : void 0;
|
|
9187
|
+
if (hash === void 0 === (assetId === void 0)) throw fail(require_ModerationContextLimits.API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Send either hash or assetId" });
|
|
9188
|
+
if (assetId) {
|
|
9189
|
+
const image = (await this.findOwnAsset(assetId)).content.image;
|
|
9190
|
+
if (!image?.perceptualHash) throw fail(require_ModerationContextLimits.API_ERROR_CODES.HASH_LIST_ASSET_NOT_HASHED);
|
|
9191
|
+
hash = image.perceptualHash;
|
|
9192
|
+
quality = image.perceptualHashQuality ?? null;
|
|
8777
9193
|
}
|
|
9194
|
+
if (!/^[0-9a-f]{64}$/.test(hash)) throw fail(require_ModerationContextLimits.API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Use a PDQ hash of 64 hexadecimal characters" });
|
|
9195
|
+
const existing = [...this.hashListEntries.values()].find((entry) => entry.hashListId === list.id && entry.hash === hash);
|
|
9196
|
+
if (existing) return [200, existing];
|
|
9197
|
+
const entry = {
|
|
9198
|
+
id: this.createId("hle"),
|
|
9199
|
+
projectId: this.projectId,
|
|
9200
|
+
hashListId: list.id,
|
|
9201
|
+
hash,
|
|
9202
|
+
quality,
|
|
9203
|
+
label: typeof body.label === "string" ? body.label : null,
|
|
9204
|
+
sourceAssetId: assetId ?? null,
|
|
9205
|
+
createdAt: this.now()
|
|
9206
|
+
};
|
|
9207
|
+
this.hashListEntries.set(entry.id, entry);
|
|
9208
|
+
return [201, entry];
|
|
8778
9209
|
}
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
9210
|
+
/** The request's JSON body, or `{}` without one. */
|
|
9211
|
+
json(request) {
|
|
9212
|
+
if (request.bytes.byteLength === 0) return {};
|
|
9213
|
+
try {
|
|
9214
|
+
const body = JSON.parse(new TextDecoder().decode(request.bytes));
|
|
9215
|
+
return body && typeof body === "object" ? body : {};
|
|
9216
|
+
} catch {
|
|
9217
|
+
throw new ApiFailure(400, "validation/invalid-json", "Invalid JSON body");
|
|
8786
9218
|
}
|
|
8787
9219
|
}
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
9220
|
+
/** A whole number from the query, or `fallback` when absent; other values fail like zod. */
|
|
9221
|
+
integer(query, name, fallback) {
|
|
9222
|
+
const raw = query.get(name);
|
|
9223
|
+
if (raw === null) return fallback;
|
|
9224
|
+
const value = Number(raw);
|
|
9225
|
+
if (raw.trim() === "" || !Number.isFinite(value)) throw invalidField("validation/invalid-type", name);
|
|
9226
|
+
if (!Number.isInteger(value)) throw invalidField("validation/invalid-type", name);
|
|
9227
|
+
return value;
|
|
9228
|
+
}
|
|
9229
|
+
/** `ordering[i][field]` and `ordering[i][direction]` pairs, as qs parses them. */
|
|
9230
|
+
ordering(query) {
|
|
9231
|
+
const ordering = [];
|
|
9232
|
+
for (const [key, value] of query) {
|
|
9233
|
+
const match = /^ordering\[(\d+)\]\[(field|direction)\]$/.exec(key);
|
|
9234
|
+
if (!match) continue;
|
|
9235
|
+
const index = Number(match[1]);
|
|
9236
|
+
ordering[index] = {
|
|
9237
|
+
...ordering[index],
|
|
9238
|
+
[match[2]]: value
|
|
9239
|
+
};
|
|
9240
|
+
}
|
|
9241
|
+
return ordering.filter(Boolean);
|
|
8793
9242
|
}
|
|
8794
|
-
|
|
9243
|
+
pageQuery(query) {
|
|
9244
|
+
const ordering = this.ordering(query);
|
|
8795
9245
|
return {
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
* otherwise; `mode` is accepted but there is no semantic search.
|
|
8800
|
-
*/
|
|
8801
|
-
search: async (options) => {
|
|
8802
|
-
this.begin("assets.search", options);
|
|
8803
|
-
const query = typeof options?.query === "string" ? options.query.trim() : "";
|
|
8804
|
-
if (!query || query.length > 200) throw this.invalidPayload("query must be 1 to 200 characters");
|
|
8805
|
-
const limit = options.limit ?? 10;
|
|
8806
|
-
const needle = query.toLowerCase();
|
|
8807
|
-
const fields = [
|
|
8808
|
-
"name",
|
|
8809
|
-
"id",
|
|
8810
|
-
"externalId",
|
|
8811
|
-
"description"
|
|
8812
|
-
];
|
|
8813
|
-
return {
|
|
8814
|
-
query,
|
|
8815
|
-
items: (await this.databaseStorage.listAssets(this.projectId)).map((asset) => {
|
|
8816
|
-
const matchedFields = [];
|
|
8817
|
-
for (const field of fields) if (String(asset[field] ?? "").toLowerCase().includes(needle)) matchedFields.push(field);
|
|
8818
|
-
if (asset.tags?.some((tag) => tag.toLowerCase().includes(needle))) matchedFields.push("tags");
|
|
8819
|
-
return {
|
|
8820
|
-
asset,
|
|
8821
|
-
score: fields.some((field) => String(asset[field] ?? "").toLowerCase() === needle) ? 1 : .5,
|
|
8822
|
-
matchedFields
|
|
8823
|
-
};
|
|
8824
|
-
}).filter((result) => result.matchedFields.length > 0).sort((a, b) => b.score - a.score).slice(0, limit),
|
|
8825
|
-
semanticStatus: "disabled"
|
|
8826
|
-
};
|
|
8827
|
-
},
|
|
8828
|
-
/** Lists assets with the real client's paging, `ordering`, `folderId` and `filter`. */
|
|
8829
|
-
list: async (options = {}) => {
|
|
8830
|
-
this.begin("assets.list", options);
|
|
8831
|
-
let assets = await this.databaseStorage.listAssets(this.projectId);
|
|
8832
|
-
if (options.folderId !== void 0) assets = assets.filter((asset) => (asset.folderId ?? null) === options.folderId);
|
|
8833
|
-
assets = filterAssets(assets, options.filter);
|
|
8834
|
-
return createPage(assets, options);
|
|
8835
|
-
},
|
|
8836
|
-
iterate: (options = {}) => this.iteratePages((cursor) => this.assets.list({
|
|
8837
|
-
...options,
|
|
8838
|
-
cursor
|
|
8839
|
-
})),
|
|
8840
|
-
retrieve: async (options) => {
|
|
8841
|
-
this.begin("assets.retrieve", options);
|
|
8842
|
-
return this.findOwnAsset(options?.assetId);
|
|
8843
|
-
},
|
|
8844
|
-
listChildren: async (childrenOptions = {}) => {
|
|
8845
|
-
this.begin("assets.listChildren", childrenOptions);
|
|
8846
|
-
const { assetId, ...options } = childrenOptions;
|
|
8847
|
-
require_CalmLensUtils.encodePathSegment(assetId, "Asset ID");
|
|
8848
|
-
const assets = await this.databaseStorage.listAssets(this.projectId);
|
|
8849
|
-
return createPage(assets.filter((asset) => asset.parentId === assetId), options);
|
|
8850
|
-
},
|
|
8851
|
-
iterateChildren: (options) => {
|
|
8852
|
-
require_CalmLensUtils.encodePathSegment(options?.assetId, "Asset ID");
|
|
8853
|
-
return this.iteratePages((cursor) => this.assets.listChildren({
|
|
8854
|
-
...options,
|
|
8855
|
-
cursor
|
|
8856
|
-
}));
|
|
8857
|
-
},
|
|
8858
|
-
update: async (options) => {
|
|
8859
|
-
this.begin("assets.update", options);
|
|
8860
|
-
const { assetId, ...body } = options;
|
|
8861
|
-
const asset = await this.findOwnAsset(assetId);
|
|
8862
|
-
const changes = Object.fromEntries(Object.entries(body).filter(([, value]) => value !== void 0));
|
|
8863
|
-
return this.databaseStorage.updateAsset({
|
|
8864
|
-
...asset,
|
|
8865
|
-
...changes,
|
|
8866
|
-
updatedAt: this.now()
|
|
8867
|
-
});
|
|
8868
|
-
},
|
|
8869
|
-
create: async (options) => {
|
|
8870
|
-
this.begin("assets.create", options);
|
|
8871
|
-
return this.withIdempotency("assets.create", options, () => this.createAsset(options, true));
|
|
8872
|
-
},
|
|
8873
|
-
moderate: async (options) => {
|
|
8874
|
-
this.begin("assets.moderate", options);
|
|
8875
|
-
return this.moderateAsset(options, "assets.moderate");
|
|
8876
|
-
},
|
|
8877
|
-
/** Continues a multipart upload: parts must arrive in order while it is `uploading`. */
|
|
8878
|
-
uploadPart: async (options) => {
|
|
8879
|
-
this.begin("assets.uploadPart", options);
|
|
8880
|
-
const { assetId, part } = options;
|
|
8881
|
-
require_CalmLensUtils.encodePathSegment(assetId, "Asset ID");
|
|
8882
|
-
if (!part) throw require_CalmLensVersion.invalidArgument("Part options are required");
|
|
8883
|
-
return this.withIdempotency("assets.uploadPart", options, async () => {
|
|
8884
|
-
const asset = await this.findOwnAsset(assetId);
|
|
8885
|
-
const validation = require_CalmLensUtils.validateAssetPayload(part, {
|
|
8886
|
-
multipart: true,
|
|
8887
|
-
isFinal: part.isFinal
|
|
8888
|
-
});
|
|
8889
|
-
if (!validation.ok) throw this.invalidPayload(validation.error);
|
|
8890
|
-
if (asset.processing.status !== "uploading") throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.UPLOAD_NOT_OPEN);
|
|
8891
|
-
const expectedPartNumber = (asset.processing.uploadPartsReceived ?? 1) + 1;
|
|
8892
|
-
if (part.partNumber !== expectedPartNumber) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.INVALID_UPLOAD_PART_NUMBER, { details: { expectedPartNumber } });
|
|
8893
|
-
await this.imageStorage.putPart(assetId, part);
|
|
8894
|
-
const queue = options.queue ?? this.uploadQueues.get(assetId) ?? false;
|
|
8895
|
-
if (part.isFinal) this.uploadQueues.delete(assetId);
|
|
8896
|
-
const now = this.now();
|
|
8897
|
-
const received = {
|
|
8898
|
-
...asset,
|
|
8899
|
-
processing: {
|
|
8900
|
-
...asset.processing,
|
|
8901
|
-
uploadPartsReceived: part.partNumber
|
|
8902
|
-
},
|
|
8903
|
-
updatedAt: now
|
|
8904
|
-
};
|
|
8905
|
-
return this.databaseStorage.updateAsset(part.isFinal ? await this.applyOutcome(received, queue ? this.queuedOutcome : this.analyzedOutcome) : received);
|
|
8906
|
-
});
|
|
8907
|
-
},
|
|
8908
|
-
createSignedUrl: async (signedUrlOptions) => {
|
|
8909
|
-
this.begin("assets.createSignedUrl", signedUrlOptions);
|
|
8910
|
-
const { assetId, ...options } = signedUrlOptions;
|
|
8911
|
-
const asset = await this.findOwnAsset(assetId);
|
|
8912
|
-
const expiresInSeconds = options.expiresInSeconds ?? 900;
|
|
8913
|
-
const resource = options.resource ?? "content";
|
|
8914
|
-
const transform = require_CalmLensUtils.transformSelector(options.transform);
|
|
8915
|
-
const selector = (options.lens ? `&lens=${encodeURIComponent(options.lens)}` : "") + (transform ? `&transform=${encodeURIComponent(transform)}` : "");
|
|
8916
|
-
return {
|
|
8917
|
-
url: `${this.signedUrlBaseUrl}/assets/${asset.id}/${resource}?expiresInSeconds=${expiresInSeconds}${selector}`,
|
|
8918
|
-
expiresAt: this.now() + expiresInSeconds * 1e3
|
|
8919
|
-
};
|
|
8920
|
-
},
|
|
8921
|
-
move: async (options) => {
|
|
8922
|
-
this.begin("assets.move", options);
|
|
8923
|
-
const { assetId, folderId } = options;
|
|
8924
|
-
const asset = await this.findOwnAsset(assetId);
|
|
8925
|
-
if (folderId !== null && folderId !== void 0) {
|
|
8926
|
-
if (!await this.resolveFolder(folderId)) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.FOLDER_NOT_FOUND);
|
|
8927
|
-
}
|
|
8928
|
-
return this.databaseStorage.updateAsset({
|
|
8929
|
-
...asset,
|
|
8930
|
-
folderId: folderId ?? null,
|
|
8931
|
-
updatedAt: this.now()
|
|
8932
|
-
});
|
|
8933
|
-
},
|
|
8934
|
-
retry: async (options) => {
|
|
8935
|
-
this.begin("assets.retry", options);
|
|
8936
|
-
const asset = await this.findOwnAsset(options?.assetId);
|
|
8937
|
-
if (asset.processing.status !== "failed") throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.ASSET_NOT_RETRYABLE);
|
|
8938
|
-
if (asset.content.status === "removed") throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.ASSET_CONTENT_UNAVAILABLE);
|
|
8939
|
-
return this.databaseStorage.updateAsset(await this.applyOutcome(asset, "pending"));
|
|
8940
|
-
},
|
|
8941
|
-
/** Waits like the real client, checking the mock's storage for a decision. */
|
|
8942
|
-
waitForDecision: async (options) => {
|
|
8943
|
-
this.begin("assets.waitForDecision", options);
|
|
8944
|
-
const { assetId, ...waitOptions } = options;
|
|
8945
|
-
return require_CalmLensUtils.pollForDecision(() => this.findOwnAsset(assetId), waitOptions, require_CalmLensUtils.sleep);
|
|
8946
|
-
},
|
|
8947
|
-
deleteContent: async (options) => {
|
|
8948
|
-
this.begin("assets.deleteContent", options);
|
|
8949
|
-
const asset = await this.findOwnAsset(options?.assetId);
|
|
8950
|
-
await this.imageStorage.deleteContent(asset.id);
|
|
8951
|
-
const now = this.now();
|
|
8952
|
-
return this.databaseStorage.updateAsset({
|
|
8953
|
-
...asset,
|
|
8954
|
-
content: asset.content.status === "removed" ? asset.content : {
|
|
8955
|
-
...asset.content,
|
|
8956
|
-
status: "removed",
|
|
8957
|
-
removalReason: "requested",
|
|
8958
|
-
updatedAt: now,
|
|
8959
|
-
extractedText: null
|
|
8960
|
-
},
|
|
8961
|
-
updatedAt: now
|
|
8962
|
-
});
|
|
8963
|
-
},
|
|
8964
|
-
delete: async (options) => {
|
|
8965
|
-
this.begin("assets.delete", options);
|
|
8966
|
-
const asset = await this.findOwnAsset(options?.assetId);
|
|
8967
|
-
await this.imageStorage.deleteContent(asset.id);
|
|
8968
|
-
await this.databaseStorage.deleteAsset(asset.id);
|
|
8969
|
-
}
|
|
9246
|
+
pageSize: this.integer(query, "pageSize", 50),
|
|
9247
|
+
cursor: query.get("cursor") ?? void 0,
|
|
9248
|
+
ordering: ordering.length ? ordering : void 0
|
|
8970
9249
|
};
|
|
8971
9250
|
}
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
return require_CalmLensWebhooks.signUploadTokenWithApiKey(this.apiKey, options, {
|
|
8977
|
-
now: this.now(),
|
|
8978
|
-
nonce: this.createId()
|
|
8979
|
-
});
|
|
8980
|
-
} };
|
|
9251
|
+
async findOwnAsset(assetId) {
|
|
9252
|
+
const asset = await this.databaseStorage.getAsset(assetId);
|
|
9253
|
+
if (!asset || asset.projectId !== this.projectId) throw fail(require_ModerationContextLimits.API_ERROR_CODES.ASSET_NOT_FOUND);
|
|
9254
|
+
return asset;
|
|
8981
9255
|
}
|
|
8982
|
-
|
|
9256
|
+
async listAssets(query) {
|
|
9257
|
+
let assets = await this.databaseStorage.listAssets(this.projectId);
|
|
9258
|
+
const folderId = query.get("folderId");
|
|
9259
|
+
if (folderId !== null) assets = assets.filter((asset) => (asset.folderId ?? null) === folderId);
|
|
9260
|
+
const filter = query.get("filter") ?? void 0;
|
|
9261
|
+
assets = filterAssets(assets, filter);
|
|
9262
|
+
return createPage(assets, this.pageQuery(query), {
|
|
9263
|
+
scope: JSON.stringify([
|
|
9264
|
+
this.projectId,
|
|
9265
|
+
filter ?? null,
|
|
9266
|
+
folderId
|
|
9267
|
+
]),
|
|
9268
|
+
fail: this.pageFailure
|
|
9269
|
+
});
|
|
9270
|
+
}
|
|
9271
|
+
/**
|
|
9272
|
+
* Keyword search like the API's: every whitespace-separated token must appear,
|
|
9273
|
+
* case-insensitively, in the asset's name, ID, external ID, description, tags, type, content
|
|
9274
|
+
* format or extension, or moderation status. There is no semantic search.
|
|
9275
|
+
*/
|
|
9276
|
+
async searchAssets(params) {
|
|
9277
|
+
const query = (params.get("query") ?? "").trim();
|
|
9278
|
+
if (!query) throw invalidField("validation/too-small", "query");
|
|
9279
|
+
if (query.length > MAX_SEARCH_QUERY_LENGTH) throw invalidField("validation/too-big", "query");
|
|
9280
|
+
const limit = this.integer(params, "limit", 10);
|
|
9281
|
+
if (limit < 1) throw invalidField("validation/too-small", "limit");
|
|
9282
|
+
if (limit > MAX_SEARCH_LIMIT) throw invalidField("validation/too-big", "limit");
|
|
8983
9283
|
return {
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
},
|
|
8988
|
-
retrieve: async (options) => {
|
|
8989
|
-
this.begin("folders.retrieve", options);
|
|
8990
|
-
return this.findOwnFolder(options?.folderId);
|
|
8991
|
-
},
|
|
8992
|
-
create: async (options) => {
|
|
8993
|
-
this.begin("folders.create", options);
|
|
8994
|
-
if (!options?.name) throw this.invalidPayload("Folder name is required");
|
|
8995
|
-
if (options.parentId) await this.findOwnFolder(options.parentId);
|
|
8996
|
-
await this.assertUniqueFolderName(options.name, options.parentId ?? null);
|
|
8997
|
-
const folder = {
|
|
8998
|
-
id: this.createId(),
|
|
8999
|
-
projectId: this.projectId,
|
|
9000
|
-
parentId: options.parentId ?? null,
|
|
9001
|
-
name: options.name,
|
|
9002
|
-
createdAt: this.now(),
|
|
9003
|
-
updatedAt: this.now()
|
|
9004
|
-
};
|
|
9005
|
-
return this.databaseStorage.createFolder ? await this.databaseStorage.createFolder(folder) : folder;
|
|
9006
|
-
},
|
|
9007
|
-
update: async (options) => {
|
|
9008
|
-
this.begin("folders.update", options);
|
|
9009
|
-
const { folderId, ...body } = options;
|
|
9010
|
-
const folder = await this.findOwnFolder(folderId);
|
|
9011
|
-
if (body.name !== void 0 && body.name !== folder.name) await this.assertUniqueFolderName(body.name, folder.parentId ?? null, folderId);
|
|
9012
|
-
const updated = {
|
|
9013
|
-
...folder,
|
|
9014
|
-
...body.name !== void 0 ? { name: body.name } : {},
|
|
9015
|
-
updatedAt: this.now()
|
|
9016
|
-
};
|
|
9017
|
-
return this.databaseStorage.updateFolder ? await this.databaseStorage.updateFolder(updated) : updated;
|
|
9018
|
-
},
|
|
9019
|
-
delete: async (options) => {
|
|
9020
|
-
this.begin("folders.delete", options);
|
|
9021
|
-
const { folderId } = options;
|
|
9022
|
-
await this.findOwnFolder(folderId);
|
|
9023
|
-
const hasAssets = (await this.databaseStorage.listAssets(this.projectId)).some((asset) => asset.folderId === folderId);
|
|
9024
|
-
const hasChildren = (this.databaseStorage.listFolders ? await this.databaseStorage.listFolders(this.projectId) : []).some((f) => f.parentId === folderId);
|
|
9025
|
-
if (hasAssets || hasChildren) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.FOLDER_NOT_EMPTY, { details: {
|
|
9026
|
-
hasAssets,
|
|
9027
|
-
hasChildren
|
|
9028
|
-
} });
|
|
9029
|
-
await this.databaseStorage.deleteFolder?.(folderId);
|
|
9030
|
-
},
|
|
9031
|
-
move: async (options) => {
|
|
9032
|
-
this.begin("folders.move", options);
|
|
9033
|
-
const { folderId, parentId } = options;
|
|
9034
|
-
const folder = await this.findOwnFolder(folderId);
|
|
9035
|
-
const targetParentId = parentId ?? null;
|
|
9036
|
-
if (targetParentId) {
|
|
9037
|
-
let candidate = await this.findOwnFolder(targetParentId);
|
|
9038
|
-
const visited = /* @__PURE__ */ new Set();
|
|
9039
|
-
while (candidate) {
|
|
9040
|
-
if (candidate.id === folderId || visited.has(candidate.id)) throw this.apiError(require_CalmLensVersion.API_ERROR_CODES.FOLDER_PARENT_CYCLE);
|
|
9041
|
-
visited.add(candidate.id);
|
|
9042
|
-
candidate = candidate.parentId ? await this.resolveFolder(candidate.parentId) : void 0;
|
|
9043
|
-
}
|
|
9044
|
-
}
|
|
9045
|
-
if (targetParentId !== folder.parentId) await this.assertUniqueFolderName(folder.name, targetParentId, folderId);
|
|
9046
|
-
const updated = {
|
|
9047
|
-
...folder,
|
|
9048
|
-
parentId: targetParentId,
|
|
9049
|
-
updatedAt: this.now()
|
|
9050
|
-
};
|
|
9051
|
-
return this.databaseStorage.updateFolder ? await this.databaseStorage.updateFolder(updated) : updated;
|
|
9052
|
-
}
|
|
9284
|
+
query,
|
|
9285
|
+
items: searchAssets(await this.databaseStorage.listAssets(this.projectId), query, limit),
|
|
9286
|
+
semanticStatus: "disabled"
|
|
9053
9287
|
};
|
|
9054
9288
|
}
|
|
9055
|
-
async
|
|
9056
|
-
this.
|
|
9057
|
-
|
|
9058
|
-
return this.
|
|
9289
|
+
async listChildren(assetId, query) {
|
|
9290
|
+
const parent = await this.findOwnAsset(assetId);
|
|
9291
|
+
const assets = await this.databaseStorage.listAssets(this.projectId);
|
|
9292
|
+
return createPage(assets.filter((asset) => asset.parentId === parent.id), this.pageQuery(query), {
|
|
9293
|
+
scope: parent.id,
|
|
9294
|
+
fail: this.pageFailure
|
|
9295
|
+
});
|
|
9059
9296
|
}
|
|
9060
|
-
|
|
9061
|
-
const
|
|
9062
|
-
if (
|
|
9063
|
-
|
|
9064
|
-
id: this.createId(),
|
|
9065
|
-
projectId: this.projectId,
|
|
9066
|
-
userId: null,
|
|
9067
|
-
...options,
|
|
9068
|
-
createdAt: this.now(),
|
|
9069
|
-
updatedAt: this.now()
|
|
9070
|
-
};
|
|
9071
|
-
this.lenses.set(lens.identifier, lens);
|
|
9072
|
-
return lens;
|
|
9297
|
+
async updateAsset(assetId, body) {
|
|
9298
|
+
const asset = await this.findOwnAsset(assetId);
|
|
9299
|
+
if (body.externalId) {
|
|
9300
|
+
if ((await this.databaseStorage.listAssets(this.projectId)).some((other) => other.id !== asset.id && other.externalId === body.externalId)) throw fail(require_ModerationContextLimits.API_ERROR_CODES.DUPLICATE_EXTERNAL_ID);
|
|
9073
9301
|
}
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9302
|
+
return this.databaseStorage.updateAsset({
|
|
9303
|
+
...asset,
|
|
9304
|
+
...body,
|
|
9305
|
+
updatedAt: this.now()
|
|
9306
|
+
});
|
|
9307
|
+
}
|
|
9308
|
+
async deleteAsset(assetId) {
|
|
9309
|
+
const asset = await this.findOwnAsset(assetId);
|
|
9310
|
+
await this.imageStorage.deleteContent(asset.id);
|
|
9311
|
+
await this.databaseStorage.deleteAsset(asset.id);
|
|
9312
|
+
this.recordLifecycleEvent("asset.deleted", asset);
|
|
9313
|
+
}
|
|
9314
|
+
async moveAsset(assetId, body) {
|
|
9315
|
+
const asset = await this.findOwnAsset(assetId);
|
|
9316
|
+
const folderId = body.folderId ?? null;
|
|
9317
|
+
if (folderId !== null && !await this.resolveFolder(folderId)) throw fail(require_ModerationContextLimits.API_ERROR_CODES.FOLDER_NOT_FOUND);
|
|
9318
|
+
return this.databaseStorage.updateAsset({
|
|
9319
|
+
...asset,
|
|
9320
|
+
folderId,
|
|
9321
|
+
updatedAt: this.now()
|
|
9322
|
+
});
|
|
9323
|
+
}
|
|
9324
|
+
async createSignedUrl(assetId, body) {
|
|
9325
|
+
const asset = await this.findOwnAsset(assetId);
|
|
9326
|
+
const expiresInSeconds = body.expiresInSeconds ?? 900;
|
|
9327
|
+
const resource = body.resource ?? "rendered";
|
|
9328
|
+
const selector = (body.lens ? `&lens=${encodeURIComponent(String(body.lens))}` : "") + (body.transform ? `&transform=${encodeURIComponent(String(body.transform))}` : "");
|
|
9329
|
+
return {
|
|
9330
|
+
url: `${this.options.signedUrlBaseUrl}/assets/${asset.id}/${resource}?expiresInSeconds=${expiresInSeconds}${selector}`,
|
|
9331
|
+
expiresAt: this.now() + expiresInSeconds * 1e3
|
|
9332
|
+
};
|
|
9333
|
+
}
|
|
9334
|
+
async retryAsset(assetId) {
|
|
9335
|
+
const asset = await this.findOwnAsset(assetId);
|
|
9336
|
+
if (asset.classification.status !== "failed" || asset.classification.moderation.status !== "pending") throw fail(require_ModerationContextLimits.API_ERROR_CODES.ASSET_NOT_RETRYABLE);
|
|
9337
|
+
if (asset.content.status === "removed") throw fail(require_ModerationContextLimits.API_ERROR_CODES.ASSET_CONTENT_UNAVAILABLE);
|
|
9338
|
+
const retried = await this.databaseStorage.updateAsset(await this.applyOutcome(asset, "pending"));
|
|
9339
|
+
this.recordLifecycleEvent("asset.retried", retried);
|
|
9340
|
+
return this.moderateStored(retried, "retry");
|
|
9341
|
+
}
|
|
9342
|
+
async explainAsset(assetId, body) {
|
|
9343
|
+
const asset = await this.findOwnAsset(assetId);
|
|
9344
|
+
if (asset.classification.status !== "completed") throw fail(require_ModerationContextLimits.API_ERROR_CODES.ASSET_NOT_DECIDED);
|
|
9345
|
+
if (!body.rerun) {
|
|
9346
|
+
if (asset.classification.moderation?.explanationComplete !== false) return asset;
|
|
9347
|
+
return this.databaseStorage.updateAsset({
|
|
9348
|
+
...asset,
|
|
9349
|
+
updatedAt: this.now(),
|
|
9350
|
+
classification: {
|
|
9351
|
+
...asset.classification,
|
|
9352
|
+
moderation: {
|
|
9353
|
+
...asset.classification.moderation,
|
|
9354
|
+
explanationComplete: true
|
|
9355
|
+
}
|
|
9356
|
+
}
|
|
9357
|
+
});
|
|
9083
9358
|
}
|
|
9084
|
-
throw
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
} });
|
|
9359
|
+
if (asset.classification.moderation.manualReviews.length) throw fail(require_ModerationContextLimits.API_ERROR_CODES.ASSET_REVIEWED);
|
|
9360
|
+
if (asset.content.status === "removed") throw fail(require_ModerationContextLimits.API_ERROR_CODES.ASSET_CONTENT_UNAVAILABLE);
|
|
9361
|
+
const rerun = await this.databaseStorage.updateAsset(await this.applyOutcome(asset, "pending"));
|
|
9362
|
+
return this.moderateStored(rerun, "rerun");
|
|
9089
9363
|
}
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
if (
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
}
|
|
9107
|
-
const asset = await run();
|
|
9108
|
-
this.idempotentResults.set(key, {
|
|
9109
|
-
fingerprint,
|
|
9110
|
-
asset
|
|
9364
|
+
/**
|
|
9365
|
+
* Checks every asset like the API (an ID that is not an asset ID is
|
|
9366
|
+
* `validation/invalid-format`, an unknown
|
|
9367
|
+
* one `asset/not-found`, and either refuses the whole batch), then applies the operation at
|
|
9368
|
+
* once instead of in the background: `delete` deletes each asset with its stored child
|
|
9369
|
+
* assets and records `asset.deleted` with the asset as it was; `remove-content` removes
|
|
9370
|
+
* the content like `deleteContent`. Mock assets carry no safety holds, so none is skipped.
|
|
9371
|
+
*/
|
|
9372
|
+
async batch(body) {
|
|
9373
|
+
const assetIds = Array.isArray(body.assetIds) ? body.assetIds : [];
|
|
9374
|
+
const operation = body.operation;
|
|
9375
|
+
if (assetIds.length === 0) throw invalidField("validation/too-small", "assetIds");
|
|
9376
|
+
if (assetIds.length > 500) throw invalidField("validation/too-big", "assetIds");
|
|
9377
|
+
if (operation !== "delete" && operation !== "remove-content") throw invalidField("validation/invalid-value", "operation");
|
|
9378
|
+
assetIds.forEach((assetId, index) => {
|
|
9379
|
+
if (typeof assetId !== "string" || !isId(assetId, "asset")) throw invalidField("validation/invalid-format", `assetIds.${index}`);
|
|
9111
9380
|
});
|
|
9112
|
-
|
|
9381
|
+
const assets = [];
|
|
9382
|
+
for (const assetId of new Set(assetIds)) assets.push(await this.findOwnAsset(assetId));
|
|
9383
|
+
for (const asset of assets) if (operation === "delete") {
|
|
9384
|
+
await this.deleteAssetTree(asset);
|
|
9385
|
+
this.recordLifecycleEvent("asset.deleted", asset);
|
|
9386
|
+
} else await this.removeContent(asset);
|
|
9387
|
+
return { queued: true };
|
|
9113
9388
|
}
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
if (
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9389
|
+
/**
|
|
9390
|
+
* Removes an asset's content like the API: the record and report stay; stored content,
|
|
9391
|
+
* extracted text and the placeholder go. Content already removed keeps its reason, and an
|
|
9392
|
+
* `asset.content-removed` event is recorded either way.
|
|
9393
|
+
*/
|
|
9394
|
+
async removeContent(asset) {
|
|
9395
|
+
await this.imageStorage.deleteContent(asset.id);
|
|
9396
|
+
let removed = asset;
|
|
9397
|
+
if (asset.content.status === "removed") {
|
|
9398
|
+
if (asset.content.image?.thumbhash) removed = await this.databaseStorage.updateAsset({
|
|
9399
|
+
...asset,
|
|
9400
|
+
content: {
|
|
9401
|
+
...asset.content,
|
|
9402
|
+
image: withoutPlaceholder(asset.content.image)
|
|
9403
|
+
}
|
|
9404
|
+
});
|
|
9405
|
+
} else {
|
|
9406
|
+
const now = this.now();
|
|
9407
|
+
removed = await this.databaseStorage.updateAsset({
|
|
9408
|
+
...asset,
|
|
9409
|
+
context: null,
|
|
9410
|
+
content: {
|
|
9411
|
+
...asset.content,
|
|
9412
|
+
status: "removed",
|
|
9413
|
+
removalReason: "requested",
|
|
9414
|
+
updatedAt: now,
|
|
9415
|
+
extractedText: null,
|
|
9416
|
+
fileMetadata: null,
|
|
9417
|
+
image: withoutPlaceholder(asset.content.image)
|
|
9418
|
+
},
|
|
9419
|
+
updatedAt: now
|
|
9420
|
+
});
|
|
9421
|
+
}
|
|
9422
|
+
this.recordLifecycleEvent("asset.content-removed", removed);
|
|
9423
|
+
return removed;
|
|
9424
|
+
}
|
|
9425
|
+
/** Deletes an asset, its stored child assets and their content. */
|
|
9426
|
+
async deleteAssetTree(asset) {
|
|
9427
|
+
const assets = await this.databaseStorage.listAssets(this.projectId);
|
|
9428
|
+
const deleting = /* @__PURE__ */ new Set([asset.id]);
|
|
9429
|
+
for (let added = true; added;) {
|
|
9430
|
+
added = false;
|
|
9431
|
+
for (const other of assets) if (other.parentId && deleting.has(other.parentId) && !deleting.has(other.id)) {
|
|
9432
|
+
deleting.add(other.id);
|
|
9433
|
+
added = true;
|
|
9129
9434
|
}
|
|
9130
9435
|
}
|
|
9131
|
-
|
|
9132
|
-
this.
|
|
9133
|
-
|
|
9436
|
+
for (const id of deleting) {
|
|
9437
|
+
await this.imageStorage.deleteContent(id);
|
|
9438
|
+
await this.databaseStorage.deleteAsset(id);
|
|
9439
|
+
}
|
|
9440
|
+
}
|
|
9441
|
+
/** The upload fields of `POST .../assets`: raw uploads carry them in the query. */
|
|
9442
|
+
uploadFields(request) {
|
|
9443
|
+
const upload = request.headers.get("x-calmlens-upload");
|
|
9444
|
+
if (upload !== "content" && upload !== "part") return this.json(request);
|
|
9445
|
+
const { query } = request;
|
|
9446
|
+
const text = (name) => query.get(name) ?? void 0;
|
|
9447
|
+
const parsed = (name) => {
|
|
9448
|
+
const raw = query.get(name);
|
|
9449
|
+
if (raw === null) return void 0;
|
|
9450
|
+
try {
|
|
9451
|
+
return JSON.parse(raw);
|
|
9452
|
+
} catch {
|
|
9453
|
+
throw invalidField("validation/invalid-json", name);
|
|
9454
|
+
}
|
|
9455
|
+
};
|
|
9456
|
+
const total = query.get("totalSizeInBytes");
|
|
9457
|
+
return {
|
|
9458
|
+
name: query.get("name") ?? "",
|
|
9459
|
+
description: text("description"),
|
|
9460
|
+
processingOptions: parsed("processingOptions"),
|
|
9461
|
+
visibility: text("visibility"),
|
|
9462
|
+
metadata: parsed("metadata"),
|
|
9463
|
+
authorId: text("authorId"),
|
|
9464
|
+
context: parsed("context"),
|
|
9465
|
+
externalId: text("externalId"),
|
|
9466
|
+
externalUrl: text("externalUrl"),
|
|
9467
|
+
folderId: text("folderId"),
|
|
9468
|
+
uploadPresetId: text("uploadPresetId"),
|
|
9469
|
+
contentType: query.get("contentType") ?? request.headers.get("content-type") ?? "",
|
|
9470
|
+
bytes: request.bytes,
|
|
9471
|
+
part: upload === "part" ? {
|
|
9472
|
+
partNumber: this.integer(query, "partNumber", 1),
|
|
9473
|
+
isFinal: query.get("isFinal") === "true",
|
|
9474
|
+
totalSizeInBytes: total === null ? void 0 : this.integer(query, "totalSizeInBytes", 0)
|
|
9475
|
+
} : void 0
|
|
9476
|
+
};
|
|
9477
|
+
}
|
|
9478
|
+
async createAsset(request) {
|
|
9479
|
+
return this.storeAsset(this.uploadFields(request), request.query.get("queue") === "true");
|
|
9480
|
+
}
|
|
9481
|
+
async storeAsset(fields, queue) {
|
|
9482
|
+
if (!fields.name) throw fail(require_ModerationContextLimits.API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Asset name is required" });
|
|
9483
|
+
const { contentType, part } = fields;
|
|
9484
|
+
const bytes = fields.bytes ?? /* @__PURE__ */ new Uint8Array(0);
|
|
9485
|
+
if (contentType !== void 0 && require_ModerationContextLimits.isRejectedUploadMimeType(contentType)) throw fail(require_ModerationContextLimits.API_ERROR_CODES.INVALID_UPLOAD_MIME_TYPE, {
|
|
9486
|
+
message: require_ModerationContextLimits.getRejectedUploadMimeTypeError(contentType),
|
|
9487
|
+
details: { mimeType: contentType }
|
|
9488
|
+
});
|
|
9489
|
+
const type = inferAssetType(fields.url === void 0 ? contentType ?? "" : void 0);
|
|
9490
|
+
if (part) {
|
|
9491
|
+
if (part.partNumber > 1) throw fail(require_ModerationContextLimits.API_ERROR_CODES.USE_UPLOAD_PART);
|
|
9492
|
+
const partSize = bytes.byteLength;
|
|
9493
|
+
const sizeError = (part.totalSizeInBytes === void 0 ? void 0 : this.multipartImageSizeError(type, part.totalSizeInBytes, true)) ?? (part.isFinal ? this.multipartTotalSizeError(part.totalSizeInBytes, partSize, true) ?? this.multipartImageSizeError(type, partSize, true) : void 0);
|
|
9494
|
+
if (sizeError) throw fail(sizeError);
|
|
9495
|
+
const validation = require_CalmLensConnection.validateAssetPayload({ uint8Array: bytes }, {
|
|
9134
9496
|
multipart: true,
|
|
9135
|
-
isFinal:
|
|
9497
|
+
isFinal: part.isFinal
|
|
9136
9498
|
});
|
|
9137
|
-
if (!validation.ok) throw
|
|
9499
|
+
if (!validation.ok) throw fail(require_ModerationContextLimits.API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: validation.error });
|
|
9500
|
+
if (!part.isFinal) {
|
|
9501
|
+
const totalError = this.multipartTotalSizeError(part.totalSizeInBytes, partSize, false);
|
|
9502
|
+
if (totalError) throw fail(totalError);
|
|
9503
|
+
}
|
|
9138
9504
|
}
|
|
9139
|
-
if (
|
|
9140
|
-
|
|
9505
|
+
if (fields.folderId) await this.findOwnFolder(fields.folderId);
|
|
9506
|
+
if (fields.externalId) {
|
|
9507
|
+
if ((await this.databaseStorage.listAssets(this.projectId)).some((asset) => asset.externalId === fields.externalId)) throw fail(require_ModerationContextLimits.API_ERROR_CODES.DUPLICATE_EXTERNAL_ID);
|
|
9141
9508
|
}
|
|
9142
|
-
const id = this.createId();
|
|
9509
|
+
const id = this.createId("asset");
|
|
9143
9510
|
const now = this.now();
|
|
9144
|
-
const isOpenUpload =
|
|
9145
|
-
const
|
|
9511
|
+
const isOpenUpload = part !== void 0 && !part.isFinal;
|
|
9512
|
+
const processingOptions = fields.processingOptions ?? { keepContentAfterProcessing: false };
|
|
9513
|
+
const keepContent = processingOptions.keepContentAfterProcessing === true;
|
|
9514
|
+
const hasContent = fields.url === void 0;
|
|
9515
|
+
const contextItems = require_ModerationContextLimits.selectModerationContextItems(fields.context?.items ?? []);
|
|
9146
9516
|
const asset = {
|
|
9147
9517
|
id,
|
|
9148
9518
|
projectId: this.projectId,
|
|
9149
|
-
|
|
9519
|
+
parentId: null,
|
|
9520
|
+
folderId: fields.folderId ?? null,
|
|
9521
|
+
uploadPresetId: fields.uploadPresetId ?? null,
|
|
9150
9522
|
createdAt: now,
|
|
9151
9523
|
updatedAt: now,
|
|
9152
|
-
name:
|
|
9153
|
-
description:
|
|
9154
|
-
externalId:
|
|
9155
|
-
externalUrl:
|
|
9156
|
-
type
|
|
9157
|
-
metadata:
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
uploadPartsReceived: "part" in options ? options.part.partNumber ?? 1 : null
|
|
9164
|
-
},
|
|
9165
|
-
moderation: {
|
|
9166
|
-
status: "pending",
|
|
9167
|
-
action: null,
|
|
9168
|
-
note: null,
|
|
9169
|
-
reviewerAction: null,
|
|
9170
|
-
reviewerNote: null,
|
|
9171
|
-
reviewerUserId: null,
|
|
9172
|
-
reviewedAt: null,
|
|
9173
|
-
decidedAt: null,
|
|
9174
|
-
report: null
|
|
9175
|
-
},
|
|
9524
|
+
name: fields.name,
|
|
9525
|
+
description: fields.description ?? null,
|
|
9526
|
+
externalId: fields.externalId ?? null,
|
|
9527
|
+
externalUrl: fields.url ?? fields.externalUrl ?? null,
|
|
9528
|
+
type,
|
|
9529
|
+
metadata: fields.metadata ?? null,
|
|
9530
|
+
authorId: fields.authorId ?? null,
|
|
9531
|
+
context: contextItems.length ? { items: contextItems } : null,
|
|
9532
|
+
tags: null,
|
|
9533
|
+
processingOptions,
|
|
9534
|
+
visibility: fields.visibility ?? "private",
|
|
9176
9535
|
content: {
|
|
9177
9536
|
status: keepContent ? "stored" : "temporary",
|
|
9178
9537
|
removalReason: null,
|
|
9179
9538
|
updatedAt: now,
|
|
9180
9539
|
safetyHold: null,
|
|
9181
|
-
format:
|
|
9540
|
+
format: hasContent ? contentType ?? null : null,
|
|
9182
9541
|
extension: null,
|
|
9183
|
-
sizeInBytes:
|
|
9542
|
+
sizeInBytes: hasContent ? bytes.byteLength : null,
|
|
9184
9543
|
checksums: null,
|
|
9185
9544
|
image: null,
|
|
9186
|
-
|
|
9187
|
-
|
|
9545
|
+
extractedText: null,
|
|
9546
|
+
fileMetadata: null
|
|
9547
|
+
},
|
|
9548
|
+
upload: {
|
|
9549
|
+
status: isOpenUpload ? "uploading" : "completed",
|
|
9550
|
+
updatedAt: now,
|
|
9551
|
+
partsReceived: isOpenUpload ? part.partNumber : null
|
|
9552
|
+
},
|
|
9553
|
+
classification: {
|
|
9554
|
+
status: isOpenUpload ? "pending" : "processing",
|
|
9555
|
+
failureReason: null,
|
|
9556
|
+
updatedAt: now,
|
|
9557
|
+
completedAt: null,
|
|
9558
|
+
moderation: {
|
|
9559
|
+
status: "pending",
|
|
9560
|
+
manualReviews: [],
|
|
9561
|
+
flaggedCategories: [],
|
|
9562
|
+
regions: []
|
|
9563
|
+
}
|
|
9564
|
+
}
|
|
9565
|
+
};
|
|
9566
|
+
if (part) {
|
|
9567
|
+
await this.imageStorage.putPart(id, {
|
|
9568
|
+
uint8Array: bytes,
|
|
9569
|
+
type: contentType,
|
|
9570
|
+
partNumber: part.partNumber,
|
|
9571
|
+
isFinal: part.isFinal
|
|
9572
|
+
});
|
|
9573
|
+
if (isOpenUpload) this.openUploads.set(id, {
|
|
9574
|
+
queue,
|
|
9575
|
+
totalSizeInBytes: part.totalSizeInBytes
|
|
9576
|
+
});
|
|
9577
|
+
} else if (hasContent) await this.imageStorage.putContent(id, {
|
|
9578
|
+
uint8Array: bytes,
|
|
9579
|
+
type: contentType ?? ""
|
|
9580
|
+
});
|
|
9581
|
+
if (isOpenUpload) return this.databaseStorage.createAsset(asset);
|
|
9582
|
+
const decision = await this.decisionFor(asset, "upload", queue, hasContent ? bytes : null);
|
|
9583
|
+
const stored = await this.databaseStorage.createAsset(decision.status === "pending" ? asset : await this.decided(asset, decision));
|
|
9584
|
+
this.recordUploaded(asset);
|
|
9585
|
+
this.recordProcessingEvents(stored, decision);
|
|
9586
|
+
return stored;
|
|
9587
|
+
}
|
|
9588
|
+
/** Continues a multipart upload: parts must arrive in order while it is `uploading`. */
|
|
9589
|
+
async uploadPart(assetId, request) {
|
|
9590
|
+
const { query, bytes } = request;
|
|
9591
|
+
const partNumber = this.integer(query, "partNumber", 0);
|
|
9592
|
+
const isFinal = query.get("isFinal") === "true";
|
|
9593
|
+
const asset = await this.findOwnAsset(assetId);
|
|
9594
|
+
if (asset.upload.status !== "uploading") throw fail(require_ModerationContextLimits.API_ERROR_CODES.UPLOAD_NOT_OPEN);
|
|
9595
|
+
const expectedPartNumber = (asset.upload.partsReceived ?? 0) + 1;
|
|
9596
|
+
if (partNumber !== expectedPartNumber) throw fail(require_ModerationContextLimits.API_ERROR_CODES.INVALID_UPLOAD_PART_NUMBER, {
|
|
9597
|
+
message: `Expected part ${expectedPartNumber}`,
|
|
9598
|
+
details: {
|
|
9599
|
+
expectedPartNumber,
|
|
9600
|
+
receivedPartNumber: partNumber
|
|
9601
|
+
}
|
|
9602
|
+
});
|
|
9603
|
+
const received = (asset.content.sizeInBytes ?? 0) + bytes.byteLength;
|
|
9604
|
+
const upload = this.openUploads.get(assetId);
|
|
9605
|
+
const sizeError = this.multipartImageSizeError(asset.type, received, isFinal) ?? this.multipartTotalSizeError(upload?.totalSizeInBytes, received, isFinal);
|
|
9606
|
+
if (sizeError) throw fail(sizeError);
|
|
9607
|
+
const validation = require_CalmLensConnection.validateAssetPayload({ uint8Array: bytes }, {
|
|
9608
|
+
multipart: true,
|
|
9609
|
+
isFinal
|
|
9610
|
+
});
|
|
9611
|
+
if (!validation.ok) throw fail(require_ModerationContextLimits.API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: validation.error });
|
|
9612
|
+
await this.imageStorage.putPart(assetId, {
|
|
9613
|
+
uint8Array: bytes,
|
|
9614
|
+
partNumber,
|
|
9615
|
+
isFinal
|
|
9616
|
+
});
|
|
9617
|
+
const queueOverride = query.get("queue");
|
|
9618
|
+
const queue = queueOverride === null ? upload?.queue ?? false : queueOverride === "true";
|
|
9619
|
+
if (isFinal) this.openUploads.delete(assetId);
|
|
9620
|
+
const now = this.now();
|
|
9621
|
+
const next = {
|
|
9622
|
+
...asset,
|
|
9623
|
+
updatedAt: now,
|
|
9624
|
+
content: {
|
|
9625
|
+
...asset.content,
|
|
9626
|
+
sizeInBytes: received
|
|
9627
|
+
},
|
|
9628
|
+
upload: {
|
|
9629
|
+
...asset.upload,
|
|
9630
|
+
updatedAt: now,
|
|
9631
|
+
partsReceived: isFinal ? null : partNumber
|
|
9188
9632
|
}
|
|
9189
9633
|
};
|
|
9190
|
-
if (
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9634
|
+
if (!isFinal) return this.databaseStorage.updateAsset(next);
|
|
9635
|
+
const processing = await this.applyOutcome(next, "pending");
|
|
9636
|
+
const decision = await this.decisionFor(processing, "upload", queue);
|
|
9637
|
+
const stored = await this.databaseStorage.updateAsset(decision.status === "pending" ? processing : await this.decided(processing, decision));
|
|
9638
|
+
this.recordUploaded(processing);
|
|
9639
|
+
this.recordProcessingEvents(stored, decision);
|
|
9640
|
+
return stored;
|
|
9641
|
+
}
|
|
9642
|
+
/** Like the API: image uploads in parts total 10 MiB (checked at the last part) to 20 MiB. */
|
|
9643
|
+
multipartImageSizeError(type, sizeInBytes, isFinal) {
|
|
9644
|
+
if (type !== "image") return void 0;
|
|
9645
|
+
if (sizeInBytes > require_ModerationContextLimits.MAX_IMAGE_UPLOAD_BYTES) return require_ModerationContextLimits.API_ERROR_CODES.MULTIPART_IMAGE_TOO_LARGE;
|
|
9646
|
+
if (isFinal && sizeInBytes < require_ModerationContextLimits.MIN_MULTIPART_IMAGE_UPLOAD_BYTES) return require_ModerationContextLimits.API_ERROR_CODES.MULTIPART_IMAGE_TOO_SMALL;
|
|
9647
|
+
}
|
|
9648
|
+
/** Like the API: the parts may not overshoot a declared total, and must reach it at the end. */
|
|
9649
|
+
multipartTotalSizeError(totalSizeInBytes, receivedSizeInBytes, isFinal) {
|
|
9650
|
+
if (totalSizeInBytes === void 0) return void 0;
|
|
9651
|
+
return receivedSizeInBytes > totalSizeInBytes || isFinal && receivedSizeInBytes !== totalSizeInBytes ? require_ModerationContextLimits.API_ERROR_CODES.UPLOAD_SIZE_MISMATCH : void 0;
|
|
9652
|
+
}
|
|
9653
|
+
/**
|
|
9654
|
+
* Asks `moderate` how to decide `asset`, with its content: `bytes` when the caller has them,
|
|
9655
|
+
* otherwise the stored parts in order (none once the content is removed).
|
|
9656
|
+
*/
|
|
9657
|
+
async decisionFor(asset, trigger, queued, bytes) {
|
|
9658
|
+
let data = bytes;
|
|
9659
|
+
if (data === void 0) {
|
|
9660
|
+
const stored = asset.content.status === "removed" ? void 0 : await this.imageStorage.getContent(asset.id);
|
|
9661
|
+
const parts = stored ? [...stored.parts].sort(([a], [b]) => a - b).map(([, part]) => part) : [];
|
|
9662
|
+
data = stored ? new Uint8Array(await new Blob(parts).arrayBuffer()) : null;
|
|
9195
9663
|
}
|
|
9196
|
-
|
|
9664
|
+
const content = {
|
|
9665
|
+
bytes: data,
|
|
9666
|
+
text: data && asset.type === "plaintext-document" ? new TextDecoder().decode(data) : null
|
|
9667
|
+
};
|
|
9668
|
+
return toDecision(await this.options.moderate({
|
|
9669
|
+
asset: structuredClone(asset),
|
|
9670
|
+
content,
|
|
9671
|
+
trigger,
|
|
9672
|
+
queued
|
|
9673
|
+
}));
|
|
9674
|
+
}
|
|
9675
|
+
/** `asset` decided as `decision` says, with its report. */
|
|
9676
|
+
decided(asset, decision) {
|
|
9677
|
+
return this.applyOutcome(asset, decision.status, "policy", decision.report());
|
|
9678
|
+
}
|
|
9679
|
+
/**
|
|
9680
|
+
* Stores a processing asset's `decision` and records its events; a `pending` decision leaves it
|
|
9681
|
+
* processing.
|
|
9682
|
+
*/
|
|
9683
|
+
async settle(asset, decision) {
|
|
9684
|
+
if (decision.status === "pending") return asset;
|
|
9685
|
+
const stored = await this.databaseStorage.updateAsset(await this.decided(asset, decision));
|
|
9686
|
+
this.recordProcessingEvents(stored, decision);
|
|
9687
|
+
return stored;
|
|
9688
|
+
}
|
|
9689
|
+
/** Asks `moderate` (unless `decision` is given) and settles a processing asset. */
|
|
9690
|
+
async moderateStored(asset, trigger, decision) {
|
|
9691
|
+
return this.settle(asset, decision ?? await this.decisionFor(asset, trigger, true));
|
|
9692
|
+
}
|
|
9693
|
+
/** `asset.uploaded`, as the API records it when an upload completes (UploadAssetUtils.ts). */
|
|
9694
|
+
recordUploaded(asset) {
|
|
9695
|
+
this.pushEvent({
|
|
9696
|
+
type: "asset.uploaded",
|
|
9697
|
+
severity: "low",
|
|
9698
|
+
assetId: asset.id,
|
|
9699
|
+
asset: toEventAsset(asset),
|
|
9700
|
+
facts: [
|
|
9701
|
+
{
|
|
9702
|
+
key: "asset-type",
|
|
9703
|
+
value: asset.type
|
|
9704
|
+
},
|
|
9705
|
+
assetNameFact(asset),
|
|
9706
|
+
{
|
|
9707
|
+
key: "file-format",
|
|
9708
|
+
value: asset.content.format ?? null
|
|
9709
|
+
}
|
|
9710
|
+
]
|
|
9711
|
+
});
|
|
9197
9712
|
}
|
|
9198
9713
|
/**
|
|
9199
|
-
*
|
|
9200
|
-
*
|
|
9714
|
+
* The events the pipeline records when it settles an asset (AssetClassification.ts and
|
|
9715
|
+
* AssetErrorAlert.ts): `asset.classified` and the automated `asset.<status>`, or
|
|
9716
|
+
* `asset.failed`.
|
|
9201
9717
|
*/
|
|
9202
|
-
|
|
9718
|
+
recordProcessingEvents(asset, decision) {
|
|
9719
|
+
if (decision.status === "pending") return;
|
|
9720
|
+
const event = {
|
|
9721
|
+
assetId: asset.id,
|
|
9722
|
+
asset: toEventAsset(asset)
|
|
9723
|
+
};
|
|
9724
|
+
if (decision.status === "failed") {
|
|
9725
|
+
this.pushEvent({
|
|
9726
|
+
...event,
|
|
9727
|
+
type: "asset.failed",
|
|
9728
|
+
severity: "high",
|
|
9729
|
+
facts: [
|
|
9730
|
+
assetNameFact(asset),
|
|
9731
|
+
{
|
|
9732
|
+
key: "asset-type",
|
|
9733
|
+
value: asset.type
|
|
9734
|
+
},
|
|
9735
|
+
{
|
|
9736
|
+
key: "previous-status",
|
|
9737
|
+
value: "processing"
|
|
9738
|
+
},
|
|
9739
|
+
{
|
|
9740
|
+
key: "failure-reason",
|
|
9741
|
+
value: asset.classification.failureReason ?? "processing-error"
|
|
9742
|
+
}
|
|
9743
|
+
]
|
|
9744
|
+
});
|
|
9745
|
+
return;
|
|
9746
|
+
}
|
|
9747
|
+
const flaggedCategories = decision.report()?.flaggedCategories ?? [];
|
|
9748
|
+
this.pushEvent({
|
|
9749
|
+
...event,
|
|
9750
|
+
type: "asset.classified",
|
|
9751
|
+
severity: "low",
|
|
9752
|
+
facts: [
|
|
9753
|
+
{
|
|
9754
|
+
key: "flagged-categories",
|
|
9755
|
+
value: flaggedCategories
|
|
9756
|
+
},
|
|
9757
|
+
{
|
|
9758
|
+
key: "moderation-status",
|
|
9759
|
+
value: decision.status
|
|
9760
|
+
},
|
|
9761
|
+
{
|
|
9762
|
+
key: "asset-type",
|
|
9763
|
+
value: asset.type
|
|
9764
|
+
},
|
|
9765
|
+
assetNameFact(asset)
|
|
9766
|
+
]
|
|
9767
|
+
});
|
|
9768
|
+
this.pushEvent({
|
|
9769
|
+
...event,
|
|
9770
|
+
type: `asset.${decision.status}`,
|
|
9771
|
+
severity: decision.status === "allowed" || decision.status === "limited" ? "low" : "high",
|
|
9772
|
+
facts: [
|
|
9773
|
+
{
|
|
9774
|
+
key: "decided-by",
|
|
9775
|
+
value: "automated"
|
|
9776
|
+
},
|
|
9777
|
+
{
|
|
9778
|
+
key: "flagged-categories",
|
|
9779
|
+
value: flaggedCategories
|
|
9780
|
+
},
|
|
9781
|
+
{
|
|
9782
|
+
key: "asset-type",
|
|
9783
|
+
value: asset.type
|
|
9784
|
+
},
|
|
9785
|
+
assetNameFact(asset)
|
|
9786
|
+
]
|
|
9787
|
+
});
|
|
9788
|
+
}
|
|
9789
|
+
/**
|
|
9790
|
+
* Moves an asset to `outcome` like the pipeline would: a decision finishes processing, `purged`
|
|
9791
|
+
* deletes the content (with `purgeReason`, `policy` for the pipeline), and finished processing
|
|
9792
|
+
* removes content kept only temporarily (`keepContentAfterProcessing` off). A classified image
|
|
9793
|
+
* gets a ThumbHash for its placeholder, which goes with purged content.
|
|
9794
|
+
*/
|
|
9795
|
+
async applyOutcome(asset, outcome, purgeReason = "policy", report) {
|
|
9203
9796
|
const now = this.now();
|
|
9204
|
-
const decided = outcome
|
|
9205
|
-
const
|
|
9206
|
-
const
|
|
9797
|
+
const decided = outcome !== "pending" && outcome !== "failed";
|
|
9798
|
+
const classificationStatus = decided ? "completed" : outcome === "failed" ? "failed" : "processing";
|
|
9799
|
+
const purge = outcome === "purged" && asset.content.status !== "removed";
|
|
9800
|
+
const removeContent = purge || classificationStatus === "completed" && asset.content.status === "temporary";
|
|
9207
9801
|
if (removeContent) await this.imageStorage.deleteContent(asset.id);
|
|
9802
|
+
const image = decided && asset.type === "image" && asset.content.status !== "removed" && !purge ? {
|
|
9803
|
+
width: null,
|
|
9804
|
+
height: null,
|
|
9805
|
+
perceptualHash: null,
|
|
9806
|
+
perceptualHashQuality: null,
|
|
9807
|
+
...asset.content.image,
|
|
9808
|
+
thumbhash: MOCK_THUMBHASH,
|
|
9809
|
+
averageColor: asset.content.image?.averageColor ?? "#7f7f7f"
|
|
9810
|
+
} : outcome === "purged" ? withoutPlaceholder(asset.content.image) : asset.content.image;
|
|
9208
9811
|
return {
|
|
9209
9812
|
...asset,
|
|
9210
9813
|
updatedAt: now,
|
|
9211
|
-
|
|
9212
|
-
...asset.processing,
|
|
9213
|
-
status: processingStatus,
|
|
9214
|
-
updatedAt: now
|
|
9215
|
-
},
|
|
9216
|
-
moderation: {
|
|
9217
|
-
...asset.moderation,
|
|
9218
|
-
status: decided ? outcome : "pending",
|
|
9219
|
-
decidedAt: decided ? now : null
|
|
9220
|
-
},
|
|
9814
|
+
...removeContent && { context: null },
|
|
9221
9815
|
content: removeContent ? {
|
|
9222
9816
|
...asset.content,
|
|
9223
9817
|
status: "removed",
|
|
9224
|
-
removalReason: "retention",
|
|
9818
|
+
removalReason: purge ? purgeReason : "retention",
|
|
9225
9819
|
updatedAt: now,
|
|
9226
|
-
extractedText: null
|
|
9227
|
-
|
|
9820
|
+
extractedText: null,
|
|
9821
|
+
fileMetadata: null,
|
|
9822
|
+
image: withoutPlaceholder(image)
|
|
9823
|
+
} : {
|
|
9824
|
+
...asset.content,
|
|
9825
|
+
image
|
|
9826
|
+
},
|
|
9827
|
+
upload: {
|
|
9828
|
+
...asset.upload,
|
|
9829
|
+
status: "completed",
|
|
9830
|
+
updatedAt: now
|
|
9831
|
+
},
|
|
9832
|
+
classification: {
|
|
9833
|
+
...asset.classification,
|
|
9834
|
+
status: classificationStatus,
|
|
9835
|
+
failureReason: classificationStatus === "failed" ? "processing-error" : null,
|
|
9836
|
+
updatedAt: now,
|
|
9837
|
+
completedAt: decided ? now : null,
|
|
9838
|
+
moderation: {
|
|
9839
|
+
...asset.classification.moderation,
|
|
9840
|
+
status: decided ? outcome : "pending",
|
|
9841
|
+
...report
|
|
9842
|
+
}
|
|
9843
|
+
}
|
|
9228
9844
|
};
|
|
9229
9845
|
}
|
|
9846
|
+
/** Adds an `asset.deleted`, `asset.content-removed` or `asset.retried` event, like the API. */
|
|
9847
|
+
recordLifecycleEvent(type, asset) {
|
|
9848
|
+
this.pushEvent({
|
|
9849
|
+
type,
|
|
9850
|
+
severity: "low",
|
|
9851
|
+
assetId: asset.id,
|
|
9852
|
+
asset: toEventAsset(asset),
|
|
9853
|
+
facts: [{
|
|
9854
|
+
key: "asset-name",
|
|
9855
|
+
value: asset.name,
|
|
9856
|
+
...type === "asset.deleted" ? {} : { url: `https://calmlens.com/go/asset/${asset.id}` }
|
|
9857
|
+
}, {
|
|
9858
|
+
key: "asset-type",
|
|
9859
|
+
value: asset.type
|
|
9860
|
+
}]
|
|
9861
|
+
});
|
|
9862
|
+
}
|
|
9863
|
+
/**
|
|
9864
|
+
* `/upload-presets/:id/assets[/:assetId[/part]]`: uploads through the preset, with its rules
|
|
9865
|
+
* applied in the API's order (token, content type, size), and checks of its uploads.
|
|
9866
|
+
*/
|
|
9867
|
+
async presetRoute(request, uploadPresetId, rest, session) {
|
|
9868
|
+
if (!session) throw fail(require_ModerationContextLimits.API_ERROR_CODES.UPLOAD_PRESET_NOT_FOUND);
|
|
9869
|
+
const { preset } = session;
|
|
9870
|
+
const toUploaded = (asset) => ({
|
|
9871
|
+
id: asset.id,
|
|
9872
|
+
name: asset.name,
|
|
9873
|
+
uploadPresetId,
|
|
9874
|
+
createdAt: asset.createdAt,
|
|
9875
|
+
...preset.showDecisions !== false && { status: asset.classification.moderation.status }
|
|
9876
|
+
});
|
|
9877
|
+
const [assetId, action] = rest;
|
|
9878
|
+
if (assetId !== void 0) {
|
|
9879
|
+
if (!session.uploads.has(assetId)) throw fail(require_ModerationContextLimits.API_ERROR_CODES.ASSET_NOT_FOUND);
|
|
9880
|
+
const asset = await this.findOwnAsset(assetId);
|
|
9881
|
+
if (request.method === "POST" && action === "part") return toUploaded(await this.uploadPart(assetId, request));
|
|
9882
|
+
if (request.method === "GET" && action === void 0) return toUploaded(asset);
|
|
9883
|
+
throw new ApiFailure(404, "not-found", "No such route");
|
|
9884
|
+
}
|
|
9885
|
+
if (request.method !== "POST") throw new ApiFailure(404, "not-found", "No such route");
|
|
9886
|
+
const fields = this.uploadFields(request);
|
|
9887
|
+
const token = request.headers.get("authorization");
|
|
9888
|
+
if (preset.requireToken && !token) throw fail(require_ModerationContextLimits.API_ERROR_CODES.UPLOAD_PRESET_TOKEN_REQUIRED);
|
|
9889
|
+
if (preset.allowedContentTypes && !require_UploadController.isFileAccepted({
|
|
9890
|
+
name: fields.name,
|
|
9891
|
+
type: fields.contentType ?? ""
|
|
9892
|
+
}, preset.allowedContentTypes.join(","))) throw fail(require_ModerationContextLimits.API_ERROR_CODES.UPLOAD_PRESET_CONTENT_TYPE_NOT_ALLOWED);
|
|
9893
|
+
const size = fields.part?.totalSizeInBytes ?? fields.bytes?.byteLength;
|
|
9894
|
+
if (preset.maxFileSizeInBytes !== void 0 && size !== void 0 && size > preset.maxFileSizeInBytes) throw fail(require_ModerationContextLimits.API_ERROR_CODES.UPLOAD_PRESET_TOO_LARGE);
|
|
9895
|
+
const asset = await this.storeAsset({
|
|
9896
|
+
...fields,
|
|
9897
|
+
uploadPresetId,
|
|
9898
|
+
visibility: preset.visibility ?? "public",
|
|
9899
|
+
folderId: preset.folderId ?? void 0,
|
|
9900
|
+
processingOptions: { keepContentAfterProcessing: preset.keepContentAfterProcessing ?? true }
|
|
9901
|
+
}, request.query.get("queue") === "true");
|
|
9902
|
+
session.uploads.add(asset.id);
|
|
9903
|
+
return toUploaded(asset);
|
|
9904
|
+
}
|
|
9905
|
+
/**
|
|
9906
|
+
* Reads the feed like the API: without `cursor`, no events and a `nextCursor` for "now"; with
|
|
9907
|
+
* `cursor`, up to `pageSize` events added since, oldest first. `nextCursor` is never null: it
|
|
9908
|
+
* is where to keep polling from.
|
|
9909
|
+
*/
|
|
9910
|
+
listEvents(query) {
|
|
9911
|
+
const pageSize = this.integer(query, "pageSize", 50);
|
|
9912
|
+
if (pageSize < 1) throw invalidField("validation/too-small", "pageSize");
|
|
9913
|
+
if (pageSize > MAX_PAGE_SIZE) throw invalidField("validation/too-big", "pageSize");
|
|
9914
|
+
const cursor = query.get("cursor");
|
|
9915
|
+
if (!cursor) return {
|
|
9916
|
+
items: [],
|
|
9917
|
+
nextCursor: `mock_${this.eventFeed.length}`,
|
|
9918
|
+
hasMore: false
|
|
9919
|
+
};
|
|
9920
|
+
const match = /^mock_(\d+)$/.exec(cursor);
|
|
9921
|
+
const position = match ? Number(match[1]) : NaN;
|
|
9922
|
+
if (!Number.isSafeInteger(position) || position > this.eventFeed.length) throw fail(require_ModerationContextLimits.API_ERROR_CODES.INVALID_CURSOR);
|
|
9923
|
+
const items = this.eventFeed.slice(position, position + pageSize);
|
|
9924
|
+
const next = position + items.length;
|
|
9925
|
+
return {
|
|
9926
|
+
items,
|
|
9927
|
+
nextCursor: `mock_${next}`,
|
|
9928
|
+
hasMore: next < this.eventFeed.length
|
|
9929
|
+
};
|
|
9930
|
+
}
|
|
9931
|
+
/**
|
|
9932
|
+
* Lists folders like the API: one keyset page of a parent's children, optionally matching
|
|
9933
|
+
* `search`, by name unless `ordering` says otherwise, with the folders above them.
|
|
9934
|
+
*/
|
|
9935
|
+
async listFolders(query) {
|
|
9936
|
+
const all = await this.databaseStorage.listFolders?.(this.projectId) ?? [];
|
|
9937
|
+
const parentId = query.get("parentId");
|
|
9938
|
+
const search = query.get("search")?.trim() || null;
|
|
9939
|
+
const ordering = this.folderOrdering(query);
|
|
9940
|
+
const matching = all.filter((folder) => (folder.parentId ?? null) === parentId && (!search || folder.name.toLowerCase().includes(search.toLowerCase())));
|
|
9941
|
+
const page = createPage(matching, {
|
|
9942
|
+
cursor: query.get("cursor") ?? void 0,
|
|
9943
|
+
pageSize: this.integer(query, "pageSize", 50),
|
|
9944
|
+
ordering
|
|
9945
|
+
}, {
|
|
9946
|
+
scope: JSON.stringify([
|
|
9947
|
+
this.projectId,
|
|
9948
|
+
parentId,
|
|
9949
|
+
search
|
|
9950
|
+
]),
|
|
9951
|
+
fail: this.pageFailure,
|
|
9952
|
+
defaultOrdering: [{
|
|
9953
|
+
field: "name",
|
|
9954
|
+
direction: "asc"
|
|
9955
|
+
}]
|
|
9956
|
+
});
|
|
9957
|
+
const folders = page.items;
|
|
9958
|
+
const ids = /* @__PURE__ */ new Set([...parentId ? [parentId] : [], ...folders.map((folder) => folder.id)]);
|
|
9959
|
+
const byId = new Map(all.map((folder) => [folder.id, folder]));
|
|
9960
|
+
for (const id of ids) {
|
|
9961
|
+
const parent = byId.get(id)?.parentId;
|
|
9962
|
+
if (parent) ids.add(parent);
|
|
9963
|
+
}
|
|
9964
|
+
return {
|
|
9965
|
+
items: folders,
|
|
9966
|
+
ancestors: all.filter((folder) => ids.has(folder.id) && !folders.some((item) => item.id === folder.id)),
|
|
9967
|
+
nextCursor: page.nextCursor,
|
|
9968
|
+
pageSize: page.pageSize,
|
|
9969
|
+
hasMore: page.hasMore,
|
|
9970
|
+
ordering: page.ordering
|
|
9971
|
+
};
|
|
9972
|
+
}
|
|
9973
|
+
/** The folder list's `ordering`: at most one rule, by `name` or `createdAt`. */
|
|
9974
|
+
folderOrdering(query) {
|
|
9975
|
+
const ordering = this.ordering(query);
|
|
9976
|
+
if (ordering.length > 1) throw invalidField("validation/too-big", "ordering");
|
|
9977
|
+
for (const [index, rule] of ordering.entries()) {
|
|
9978
|
+
if (rule.field !== "name" && rule.field !== "createdAt") throw invalidField("validation/invalid-value", `ordering.${index}.field`);
|
|
9979
|
+
if (rule.direction !== "asc" && rule.direction !== "desc") throw invalidField("validation/invalid-value", `ordering.${index}.direction`);
|
|
9980
|
+
}
|
|
9981
|
+
return ordering;
|
|
9982
|
+
}
|
|
9983
|
+
async createFolder(body) {
|
|
9984
|
+
const name = body.name;
|
|
9985
|
+
if (typeof name !== "string" || !name) throw fail(require_ModerationContextLimits.API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Folder name is required" });
|
|
9986
|
+
const parentId = body.parentId ?? null;
|
|
9987
|
+
if (parentId) await this.findOwnFolder(parentId);
|
|
9988
|
+
await this.assertUniqueFolderName(name, parentId);
|
|
9989
|
+
const now = this.now();
|
|
9990
|
+
const folder = {
|
|
9991
|
+
id: this.createId("fldr"),
|
|
9992
|
+
projectId: this.projectId,
|
|
9993
|
+
parentId,
|
|
9994
|
+
name,
|
|
9995
|
+
createdAt: now,
|
|
9996
|
+
updatedAt: now
|
|
9997
|
+
};
|
|
9998
|
+
return this.databaseStorage.createFolder ? await this.databaseStorage.createFolder(folder) : folder;
|
|
9999
|
+
}
|
|
10000
|
+
async updateFolder(folderId, body) {
|
|
10001
|
+
const folder = await this.findOwnFolder(folderId);
|
|
10002
|
+
const name = body.name;
|
|
10003
|
+
if (name !== void 0 && name !== folder.name) await this.assertUniqueFolderName(name, folder.parentId ?? null, folderId);
|
|
10004
|
+
const updated = {
|
|
10005
|
+
...folder,
|
|
10006
|
+
...name !== void 0 ? { name } : {},
|
|
10007
|
+
updatedAt: this.now()
|
|
10008
|
+
};
|
|
10009
|
+
return this.databaseStorage.updateFolder ? await this.databaseStorage.updateFolder(updated) : updated;
|
|
10010
|
+
}
|
|
10011
|
+
async deleteFolder(folderId) {
|
|
10012
|
+
await this.findOwnFolder(folderId);
|
|
10013
|
+
const hasAssets = (await this.databaseStorage.listAssets(this.projectId)).some((asset) => asset.folderId === folderId);
|
|
10014
|
+
const hasChildren = (await this.databaseStorage.listFolders?.(this.projectId) ?? []).some((folder) => folder.parentId === folderId);
|
|
10015
|
+
if (hasAssets || hasChildren) throw fail(require_ModerationContextLimits.API_ERROR_CODES.FOLDER_NOT_EMPTY, { details: {
|
|
10016
|
+
hasAssets,
|
|
10017
|
+
hasChildren
|
|
10018
|
+
} });
|
|
10019
|
+
await this.databaseStorage.deleteFolder?.(folderId);
|
|
10020
|
+
}
|
|
10021
|
+
async moveFolder(folderId, body) {
|
|
10022
|
+
const folder = await this.findOwnFolder(folderId);
|
|
10023
|
+
const targetParentId = body.parentId ?? null;
|
|
10024
|
+
if (targetParentId) {
|
|
10025
|
+
let candidate = await this.findOwnFolder(targetParentId);
|
|
10026
|
+
const visited = /* @__PURE__ */ new Set();
|
|
10027
|
+
while (candidate) {
|
|
10028
|
+
if (candidate.id === folderId || visited.has(candidate.id)) throw fail(require_ModerationContextLimits.API_ERROR_CODES.FOLDER_PARENT_CYCLE);
|
|
10029
|
+
visited.add(candidate.id);
|
|
10030
|
+
candidate = candidate.parentId ? await this.resolveFolder(candidate.parentId) : void 0;
|
|
10031
|
+
}
|
|
10032
|
+
}
|
|
10033
|
+
if (targetParentId !== folder.parentId) await this.assertUniqueFolderName(folder.name, targetParentId, folderId);
|
|
10034
|
+
const updated = {
|
|
10035
|
+
...folder,
|
|
10036
|
+
parentId: targetParentId,
|
|
10037
|
+
updatedAt: this.now()
|
|
10038
|
+
};
|
|
10039
|
+
return this.databaseStorage.updateFolder ? await this.databaseStorage.updateFolder(updated) : updated;
|
|
10040
|
+
}
|
|
9230
10041
|
async resolveFolder(folderId) {
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
if (!folder || folder.projectId !== this.projectId) return;
|
|
9234
|
-
return folder;
|
|
10042
|
+
const folder = await this.databaseStorage.getFolder?.(folderId);
|
|
10043
|
+
return folder && folder.projectId === this.projectId ? folder : void 0;
|
|
9235
10044
|
}
|
|
9236
10045
|
async findOwnFolder(folderId) {
|
|
9237
|
-
require_CalmLensUtils.encodePathSegment(folderId, "Folder ID");
|
|
9238
10046
|
const folder = await this.resolveFolder(folderId);
|
|
9239
|
-
if (!folder) throw
|
|
10047
|
+
if (!folder) throw fail(require_ModerationContextLimits.API_ERROR_CODES.FOLDER_NOT_FOUND);
|
|
9240
10048
|
return folder;
|
|
9241
10049
|
}
|
|
9242
10050
|
async assertUniqueFolderName(name, parentId, excludeFolderId) {
|
|
9243
|
-
if ((
|
|
10051
|
+
if ((await this.databaseStorage.listFolders?.(this.projectId) ?? []).find((folder) => folder.id !== excludeFolderId && (folder.parentId ?? null) === parentId && folder.name === name)) throw fail(require_ModerationContextLimits.API_ERROR_CODES.DUPLICATE_FOLDER_NAME, { message: `A folder named "${name}" already exists in this location.` });
|
|
9244
10052
|
}
|
|
9245
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
10053
|
+
/** Saves each lens in order, like the API: new ones, or changes with a greater version. */
|
|
10054
|
+
ensureLenses(body) {
|
|
10055
|
+
const lenses = Array.isArray(body.lenses) ? body.lenses : [];
|
|
10056
|
+
if (lenses.length === 0) throw invalidField("validation/too-small", "lenses");
|
|
10057
|
+
return { lenses: lenses.map((lens) => this.ensureLens(lens)) };
|
|
9249
10058
|
}
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
10059
|
+
ensureLens(options) {
|
|
10060
|
+
const existing = this.lenses.get(options.identifier);
|
|
10061
|
+
if (!existing) {
|
|
10062
|
+
const now = this.now();
|
|
10063
|
+
const lens = {
|
|
10064
|
+
id: this.createId("lens"),
|
|
10065
|
+
projectId: this.projectId,
|
|
10066
|
+
...options,
|
|
10067
|
+
createdAt: now,
|
|
10068
|
+
updatedAt: now
|
|
10069
|
+
};
|
|
10070
|
+
this.lenses.set(lens.identifier, lens);
|
|
10071
|
+
return lens;
|
|
10072
|
+
}
|
|
10073
|
+
if (existing.name === options.name && (existing.description ?? null) === (options.description ?? null) && existing.type === options.type && existing.active === options.active && require_CalmLensServerClient.stringifyJsonCanonical(existing.transforms ?? null) === require_CalmLensServerClient.stringifyJsonCanonical(options.transforms ?? null) && existing.version === options.version) return existing;
|
|
10074
|
+
if (options.version > existing.version) {
|
|
10075
|
+
const lens = {
|
|
10076
|
+
...existing,
|
|
10077
|
+
...options,
|
|
10078
|
+
updatedAt: this.now()
|
|
10079
|
+
};
|
|
10080
|
+
this.lenses.set(lens.identifier, lens);
|
|
10081
|
+
return lens;
|
|
10082
|
+
}
|
|
10083
|
+
throw fail(require_ModerationContextLimits.API_ERROR_CODES.LENS_VERSION_CONFLICT, { details: {
|
|
10084
|
+
currentVersion: existing.version,
|
|
10085
|
+
minimumVersion: existing.version + 1,
|
|
10086
|
+
requestedVersion: options.version
|
|
10087
|
+
} });
|
|
10088
|
+
}
|
|
10089
|
+
/**
|
|
10090
|
+
* Serves stored content like the CDN (`/assets/:projectId/:asset/:resource`): `200` with
|
|
10091
|
+
* `Content-Type`, `Content-Length`, `Cache-Control`, `Cache-Tag`, the CDN's protection headers
|
|
10092
|
+
* and, for images, `ETag` and `Expires`; or a JSON error body with `404`/`403`. A
|
|
10093
|
+
* `placeholder` is the asset's ThumbHash as a PNG.
|
|
10094
|
+
*/
|
|
10095
|
+
async serveCdn(request) {
|
|
10096
|
+
const { url, segments, query } = request;
|
|
10097
|
+
const [root, projectId, assetIdentifier, resource] = segments;
|
|
10098
|
+
const notFound = () => this.jsonResponse(404, {
|
|
10099
|
+
code: "asset/not-found",
|
|
10100
|
+
message: "Asset not found"
|
|
10101
|
+
}, url);
|
|
10102
|
+
if (root !== "assets" || projectId !== this.projectId || !assetIdentifier) return notFound();
|
|
10103
|
+
const asset = (await this.databaseStorage.listAssets(this.projectId)).find((asset) => asset.id === assetIdentifier || asset.externalId === assetIdentifier);
|
|
10104
|
+
if (!asset) return notFound();
|
|
10105
|
+
const notAvailable = () => this.jsonResponse(403, {
|
|
10106
|
+
code: "asset/not-available",
|
|
10107
|
+
message: "Asset is not available"
|
|
10108
|
+
}, url);
|
|
10109
|
+
const grants = request.headers.get("x-api-key") === this.apiKey ? this.grants : [];
|
|
10110
|
+
const folders = await this.databaseStorage.listFolders?.(this.projectId) ?? [];
|
|
10111
|
+
const canDownload = () => permits(grants, "assets:download", asset.folderId ?? null, folders);
|
|
10112
|
+
const status = asset.classification.moderation.status;
|
|
10113
|
+
const isHead = request.method === "HEAD";
|
|
10114
|
+
if (resource === "placeholder") {
|
|
10115
|
+
const thumbhash = asset.content.image?.thumbhash;
|
|
10116
|
+
if (!thumbhash || status === "purged") return notAvailable();
|
|
10117
|
+
const png = Uint8Array.from(atob(require_UploadController.placeholderHashToDataURL(thumbhash).split(",", 2)[1]), (char) => char.charCodeAt(0));
|
|
10118
|
+
return this.cdnResponse(isHead, png, url, {
|
|
10119
|
+
"Content-Type": "image/png",
|
|
10120
|
+
"Cache-Control": "public, max-age=3600",
|
|
10121
|
+
"Cache-Tag": `asset-${asset.id}`,
|
|
10122
|
+
ETag: `"${thumbhash}"`
|
|
10123
|
+
});
|
|
10124
|
+
}
|
|
10125
|
+
const storedContent = await this.imageStorage.getContent(asset.id);
|
|
10126
|
+
if (asset.upload.status === "uploading" || status === "purged" || status === "blocked" || asset.classification.status === "failed" || asset.content.status === "removed" || !storedContent) return notAvailable();
|
|
10127
|
+
let claims = {};
|
|
10128
|
+
try {
|
|
10129
|
+
claims = JSON.parse(query.get("viewerClaims") ?? request.headers.get("X-CalmLens-Viewer-Claims") ?? "{}");
|
|
10130
|
+
} catch {
|
|
10131
|
+
return this.jsonResponse(400, { code: "viewer/invalid-claims" }, url);
|
|
10132
|
+
}
|
|
10133
|
+
if (status === "age-restricted" && claims.ageConfirmed !== true) return this.jsonResponse(403, { code: "viewer/age-confirmation-required" }, url);
|
|
10134
|
+
const isPrivate = asset.visibility !== "public";
|
|
10135
|
+
if (isPrivate && !canDownload()) return this.jsonResponse(403, {
|
|
10136
|
+
code: "asset/api-key-missing-permission",
|
|
10137
|
+
message: "This API key needs assets:download permission for this asset’s folder."
|
|
10138
|
+
}, url);
|
|
10139
|
+
const parts = [...storedContent.parts.entries()].sort(([left], [right]) => left - right).map(([, value]) => value);
|
|
10140
|
+
const bytes = new Uint8Array(parts.reduce((sum, value) => sum + value.byteLength, 0));
|
|
10141
|
+
let offset = 0;
|
|
10142
|
+
for (const part of parts) {
|
|
10143
|
+
bytes.set(part, offset);
|
|
10144
|
+
offset += part.byteLength;
|
|
9254
10145
|
}
|
|
9255
|
-
|
|
10146
|
+
const contentType = storedContent.type ?? "application/octet-stream";
|
|
10147
|
+
const isImage = asset.type === "image";
|
|
10148
|
+
return this.cdnResponse(isHead, bytes, url, {
|
|
10149
|
+
"Content-Type": contentType,
|
|
10150
|
+
"Cache-Control": Object.keys(claims).length ? "private, no-store" : `${isPrivate ? "private" : "public"}, ${isImage ? "max-age=31536000, immutable" : "max-age=3600"}`,
|
|
10151
|
+
"Cache-Tag": `asset-${asset.id}`,
|
|
10152
|
+
...isImage ? {
|
|
10153
|
+
ETag: `"${asset.id}"`,
|
|
10154
|
+
Expires: new Date(this.now() + 31536e6).toUTCString()
|
|
10155
|
+
} : {},
|
|
10156
|
+
"X-Content-Type-Options": "nosniff",
|
|
10157
|
+
"Content-Security-Policy": "sandbox; default-src 'none'",
|
|
10158
|
+
...isActiveContentType(contentType) ? { "Content-Disposition": "attachment" } : {}
|
|
10159
|
+
});
|
|
10160
|
+
}
|
|
10161
|
+
cdnResponse(isHead, bytes, url, headers) {
|
|
10162
|
+
return this.response(200, isHead ? null : bytes, url, {
|
|
10163
|
+
...headers,
|
|
10164
|
+
"Content-Length": String(bytes.byteLength),
|
|
10165
|
+
"X-Mock-CalmLens-CDN-URL": url
|
|
10166
|
+
});
|
|
9256
10167
|
}
|
|
9257
10168
|
};
|
|
9258
|
-
|
|
9259
|
-
|
|
9260
|
-
|
|
10169
|
+
// src/testing/MockStorage.ts
|
|
10170
|
+
var InMemoryMockImageStorage = class {
|
|
10171
|
+
constructor() {
|
|
10172
|
+
this.content = /* @__PURE__ */ new Map();
|
|
10173
|
+
}
|
|
10174
|
+
async putContent(assetId, content) {
|
|
10175
|
+
const validation = require_CalmLensConnection.validateAssetPayload(content);
|
|
10176
|
+
if (!validation.ok) throw new Error(validation.error);
|
|
10177
|
+
const bytes = await require_CalmLensConnection.assetDataToUint8Array(require_CalmLensConnection.assetToData(content));
|
|
10178
|
+
this.content.set(assetId, {
|
|
10179
|
+
assetId,
|
|
10180
|
+
parts: /* @__PURE__ */ new Map([[1, bytes]]),
|
|
10181
|
+
type: content.type
|
|
10182
|
+
});
|
|
10183
|
+
}
|
|
10184
|
+
async putPart(assetId, part) {
|
|
10185
|
+
const validation = require_CalmLensConnection.validateAssetPayload(part, {
|
|
10186
|
+
multipart: true,
|
|
10187
|
+
isFinal: part.isFinal
|
|
10188
|
+
});
|
|
10189
|
+
if (!validation.ok) throw new Error(validation.error);
|
|
10190
|
+
const bytes = await require_CalmLensConnection.assetDataToUint8Array(require_CalmLensConnection.assetToData(part));
|
|
10191
|
+
const existing = this.content.get(assetId) ?? {
|
|
10192
|
+
assetId,
|
|
10193
|
+
parts: /* @__PURE__ */ new Map(),
|
|
10194
|
+
type: part.type
|
|
10195
|
+
};
|
|
10196
|
+
existing.parts.set(part.partNumber ?? 1, bytes);
|
|
10197
|
+
existing.type = part.type ?? existing.type;
|
|
10198
|
+
this.content.set(assetId, existing);
|
|
10199
|
+
}
|
|
10200
|
+
async getContent(assetId) {
|
|
10201
|
+
return this.content.get(assetId);
|
|
10202
|
+
}
|
|
10203
|
+
async deleteContent(assetId) {
|
|
10204
|
+
this.content.delete(assetId);
|
|
10205
|
+
}
|
|
10206
|
+
};
|
|
10207
|
+
var InMemoryMockDatabaseStorage = class {
|
|
10208
|
+
constructor() {
|
|
10209
|
+
this.assets = /* @__PURE__ */ new Map();
|
|
10210
|
+
this.folders = /* @__PURE__ */ new Map();
|
|
10211
|
+
}
|
|
10212
|
+
async createAsset(asset) {
|
|
10213
|
+
this.assets.set(asset.id, asset);
|
|
10214
|
+
return asset;
|
|
10215
|
+
}
|
|
10216
|
+
async updateAsset(asset) {
|
|
10217
|
+
this.assets.set(asset.id, asset);
|
|
10218
|
+
return asset;
|
|
10219
|
+
}
|
|
10220
|
+
async getAsset(assetId) {
|
|
10221
|
+
return this.assets.get(assetId);
|
|
10222
|
+
}
|
|
10223
|
+
async listAssets(projectId) {
|
|
10224
|
+
return [...this.assets.values()].filter((asset) => asset.projectId === projectId);
|
|
10225
|
+
}
|
|
10226
|
+
async deleteAsset(assetId) {
|
|
10227
|
+
this.assets.delete(assetId);
|
|
10228
|
+
}
|
|
10229
|
+
async createFolder(folder) {
|
|
10230
|
+
this.folders.set(folder.id, folder);
|
|
10231
|
+
return folder;
|
|
10232
|
+
}
|
|
10233
|
+
async updateFolder(folder) {
|
|
10234
|
+
this.folders.set(folder.id, folder);
|
|
10235
|
+
return folder;
|
|
10236
|
+
}
|
|
10237
|
+
async getFolder(folderId) {
|
|
10238
|
+
return this.folders.get(folderId);
|
|
10239
|
+
}
|
|
10240
|
+
async listFolders(projectId) {
|
|
10241
|
+
return [...this.folders.values()].filter((folder) => folder.projectId === projectId).sort((a, b) => a.name.localeCompare(b.name));
|
|
10242
|
+
}
|
|
10243
|
+
async deleteFolder(folderId) {
|
|
10244
|
+
this.folders.delete(folderId);
|
|
10245
|
+
}
|
|
10246
|
+
};
|
|
10247
|
+
// src/testing/MockCalmLensServerClient.ts
|
|
10248
|
+
const apis = /* @__PURE__ */ new WeakMap();
|
|
10249
|
+
/** The in-memory API a mock server client runs against, for `MockCalmLensAppClient`. */
|
|
10250
|
+
const getMockApi = (client) => {
|
|
10251
|
+
const api = apis.get(client);
|
|
10252
|
+
if (!api) throw require_ModerationContextLimits.invalidConfig("mockOptions.server must be a MockCalmLensServerClient");
|
|
10253
|
+
return api;
|
|
10254
|
+
};
|
|
10255
|
+
/**
|
|
10256
|
+
* Options that make a real client send its requests to `api`: waits only move the mock's clock,
|
|
10257
|
+
* multipart size checks are the in-memory API's, and nothing is retried unless the options say so.
|
|
10258
|
+
*/
|
|
10259
|
+
const mockClientOptions = (api, endpoints, session) => {
|
|
10260
|
+
const fetch = api.createFetch(endpoints, session);
|
|
10261
|
+
const hooks = {
|
|
10262
|
+
sleep: async (ms, signal) => {
|
|
10263
|
+
await require_CalmLensConnection.sleep(0, signal);
|
|
10264
|
+
api.waitedMs += ms;
|
|
10265
|
+
},
|
|
10266
|
+
now: () => api.waitedMs,
|
|
10267
|
+
uploadTokenTime: () => ({
|
|
10268
|
+
now: api.now(),
|
|
10269
|
+
nonce: require_ModerationContextLimits.randomUuid()
|
|
10270
|
+
}),
|
|
10271
|
+
deferUploadChecks: true,
|
|
10272
|
+
defaultCdnBaseUrl: "mock://calmlens/v1"
|
|
10273
|
+
};
|
|
10274
|
+
return {
|
|
10275
|
+
fetch,
|
|
10276
|
+
uploadFetch: fetch,
|
|
10277
|
+
cdnFetch: fetch,
|
|
10278
|
+
[require_CalmLensConnection.MOCK_HOOKS]: hooks
|
|
10279
|
+
};
|
|
10280
|
+
};
|
|
10281
|
+
/**
|
|
10282
|
+
* The real `CalmLensServerClient`, answered by an in-memory CalmLens API and CDN instead of the
|
|
10283
|
+
* network: every method runs the real client's code, and the in-memory API answers its requests
|
|
10284
|
+
* with the API's statuses, bodies, error bodies and headers. Mock helpers decide assets, add
|
|
10285
|
+
* events, change the API key's grants, fail requests and list the requests sent.
|
|
10286
|
+
*/
|
|
10287
|
+
var MockCalmLensServerClient = class extends require_CalmLensServerClient.CalmLensServerClient {
|
|
10288
|
+
#api;
|
|
10289
|
+
#projectId;
|
|
10290
|
+
/**
|
|
10291
|
+
* Accepts the same options as `CalmLensServerClient` (validated the same way), plus
|
|
10292
|
+
* `mockOptions`. `maxRetries` defaults to 0, so a failed request fails once; retry backoff and
|
|
10293
|
+
* `assets.waitForDecision` take no real time.
|
|
10294
|
+
*/
|
|
10295
|
+
constructor(options) {
|
|
10296
|
+
require_ModerationContextLimits.validateServerClientOptions(options);
|
|
10297
|
+
const { mockOptions = {}, ...clientOptions } = options;
|
|
10298
|
+
const api = new MockCalmLensApi({
|
|
10299
|
+
projectId: options.projectId,
|
|
10300
|
+
apiKey: options.apiKey,
|
|
10301
|
+
grants: getMockApiKeyGrants(options.apiKey),
|
|
10302
|
+
databaseStorage: mockOptions.databaseStorage ?? new InMemoryMockDatabaseStorage(),
|
|
10303
|
+
imageStorage: mockOptions.imageStorage ?? new InMemoryMockImageStorage(),
|
|
10304
|
+
createId: mockOptions.createId ?? newId,
|
|
10305
|
+
now: mockOptions.now ?? createMonotonicClock(),
|
|
10306
|
+
moderate: mockOptions.moderate ?? defaultModerate,
|
|
10307
|
+
signedUrlBaseUrl: mockOptions.signedUrlBaseUrl ?? "mock://calmlens"
|
|
10308
|
+
});
|
|
10309
|
+
let endpoints;
|
|
10310
|
+
super({
|
|
10311
|
+
maxRetries: 0,
|
|
10312
|
+
...clientOptions,
|
|
10313
|
+
dangerouslyAllowBrowser: true,
|
|
10314
|
+
...mockClientOptions(api, () => endpoints)
|
|
10315
|
+
});
|
|
10316
|
+
const { baseUrl, cdnBaseUrl } = this.toJSON();
|
|
10317
|
+
endpoints = {
|
|
10318
|
+
baseUrl,
|
|
10319
|
+
cdnBaseUrl
|
|
10320
|
+
};
|
|
10321
|
+
this.#api = api;
|
|
10322
|
+
this.#projectId = options.projectId;
|
|
10323
|
+
this.databaseStorage = api.databaseStorage;
|
|
10324
|
+
this.imageStorage = api.imageStorage;
|
|
10325
|
+
apis.set(this, api);
|
|
10326
|
+
}
|
|
10327
|
+
/** Keeps the API key out of `console.log` and `util.inspect`. */
|
|
10328
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
10329
|
+
return `MockCalmLensServerClient { projectId: ${JSON.stringify(this.#projectId)} }`;
|
|
10330
|
+
}
|
|
10331
|
+
/**
|
|
10332
|
+
* Every request sent to the mock's API and CDN, oldest first, by this client and the
|
|
10333
|
+
* `MockCalmLensAppClient`s that use it, each with the client method that sent it.
|
|
10334
|
+
*/
|
|
10335
|
+
get calls() {
|
|
10336
|
+
return this.#api.calls.map(({ call }) => call);
|
|
10337
|
+
}
|
|
10338
|
+
/**
|
|
10339
|
+
* Requests `operation` sends, oldest first. A request several methods send matches each of
|
|
10340
|
+
* them: `callsTo("text.classify")` also lists `assets.classify` text uploads, and
|
|
10341
|
+
* `callsTo("assets.waitForDecision")` the `assets.retrieve` requests it polls with.
|
|
10342
|
+
*/
|
|
10343
|
+
callsTo(operation) {
|
|
10344
|
+
return this.#api.calls.filter(({ operations }) => operations.includes(operation)).map(({ call }) => call);
|
|
10345
|
+
}
|
|
10346
|
+
/** Forgets the requests recorded so far, so `calls` lists only later ones. */
|
|
10347
|
+
clearCalls() {
|
|
10348
|
+
this.#api.calls.length = 0;
|
|
10349
|
+
}
|
|
10350
|
+
/**
|
|
10351
|
+
* Makes the next request (or the next one `operation` sends) fail instead of reaching the
|
|
10352
|
+
* in-memory API. Each queued failure fails one request, so a retry takes the next queued
|
|
10353
|
+
* failure or reaches the API. An API error code answers with that error and its HTTP status,
|
|
10354
|
+
* with `details` and a `Retry-After` of `retryAfter` seconds when given.
|
|
10355
|
+
*/
|
|
10356
|
+
failNextRequest(failure = "transport-error", options = {}) {
|
|
10357
|
+
this.#api.failNextRequest(failure, options);
|
|
10358
|
+
}
|
|
10359
|
+
/**
|
|
10360
|
+
* Changes the grants of the mock's API key, as editing the key in the portal would: later
|
|
10361
|
+
* requests, such as `cdn.serve`, use them. The key itself stays the same, so idempotency keys
|
|
10362
|
+
* it used still replay.
|
|
10363
|
+
*/
|
|
10364
|
+
setApiKeyGrants(grants) {
|
|
10365
|
+
this.#api.grants = getMockApiKeyGrants(createMockApiKey(grants));
|
|
10366
|
+
}
|
|
10367
|
+
/**
|
|
10368
|
+
* Adds an event to the feed `events.list` reads, as if CalmLens had recorded it. Returns the
|
|
10369
|
+
* stored event; pass it to `toWebhookEventPayload` to build a matching webhook body.
|
|
10370
|
+
*/
|
|
10371
|
+
enqueueEvent(input) {
|
|
10372
|
+
return this.#api.enqueueEvent(input);
|
|
10373
|
+
}
|
|
10374
|
+
/**
|
|
10375
|
+
* Applies a reviewer's decision to a stored asset, as the portal or API would: the moderation
|
|
10376
|
+
* status changes (`purged` also removes the content, with reason `requested` and an
|
|
10377
|
+
* `asset.content-removed` event), the review joins `manualReviews`, and an `asset.<decision>`
|
|
10378
|
+
* event with a `decided-by: reviewer` fact joins the events feed. Rejects with
|
|
10379
|
+
* `asset/not-found` for an unknown asset. It sends no request.
|
|
10380
|
+
*/
|
|
10381
|
+
decide(assetId, decision) {
|
|
10382
|
+
return this.#api.decide(assetId, decision);
|
|
10383
|
+
}
|
|
10384
|
+
/**
|
|
10385
|
+
* Finishes processing a pending asset as the moderation pipeline would: with `decision`, or
|
|
10386
|
+
* else what `mockOptions.moderate` returns for the `complete` trigger (`allowed` by default).
|
|
10387
|
+
* The asset gets the decision's report, and `asset.classified` and the automated
|
|
10388
|
+
* `asset.<status>` (or `asset.failed`) join the events feed. No review is recorded, so the
|
|
10389
|
+
* asset can still be moderated again. Rejects with `asset/not-found` for an unknown asset and
|
|
10390
|
+
* `sdk/invalid-argument` for one that isn't processing. It sends no request.
|
|
10391
|
+
*/
|
|
10392
|
+
completeProcessing(assetId, decision) {
|
|
10393
|
+
return this.#api.completeProcessing(assetId, decision);
|
|
10394
|
+
}
|
|
10395
|
+
};
|
|
10396
|
+
// src/testing/MockCalmLensAppClient.ts
|
|
10397
|
+
const MOCK_PROJECT_ID = "proj_0000000000000000c1a5";
|
|
10398
|
+
/**
|
|
10399
|
+
* The real `CalmLensAppClient`, answered by the in-memory API of a `MockCalmLensServerClient`.
|
|
10400
|
+
* The API applies `mockOptions.preset` to this client's uploads like CalmLens applies an upload
|
|
10401
|
+
* preset: its token, content type and size rules with the API's error codes, and its visibility,
|
|
10402
|
+
* folder, content retention and decision display. The client can check and continue only the
|
|
10403
|
+
* uploads it made.
|
|
10404
|
+
*/
|
|
10405
|
+
var MockCalmLensAppClient = class extends require_CalmLensAppClient.CalmLensAppClient {
|
|
10406
|
+
#uploadPresetId;
|
|
10407
|
+
constructor(options) {
|
|
10408
|
+
require_ModerationContextLimits.validateAppClientOptions(options);
|
|
10409
|
+
const { mockOptions = {}, ...clientOptions } = options;
|
|
10410
|
+
const server = mockOptions.server ?? new MockCalmLensServerClient({
|
|
10411
|
+
apiKey: createMockApiKey([{
|
|
10412
|
+
actions: [
|
|
10413
|
+
"assets:create",
|
|
10414
|
+
"assets:read",
|
|
10415
|
+
"assets:update"
|
|
10416
|
+
],
|
|
10417
|
+
folderId: null,
|
|
10418
|
+
includeSubfolders: true
|
|
10419
|
+
}]),
|
|
10420
|
+
projectId: MOCK_PROJECT_ID,
|
|
10421
|
+
mockOptions: { moderate: mockOptions.moderate }
|
|
10422
|
+
});
|
|
10423
|
+
let endpoints;
|
|
10424
|
+
const { cdnFetch: _cdnFetch, ...requestOptions } = mockClientOptions(getMockApi(server), () => endpoints, {
|
|
10425
|
+
preset: mockOptions.preset ?? {},
|
|
10426
|
+
uploads: /* @__PURE__ */ new Set()
|
|
10427
|
+
});
|
|
10428
|
+
super({
|
|
10429
|
+
maxRetries: 0,
|
|
10430
|
+
...clientOptions,
|
|
10431
|
+
...requestOptions
|
|
10432
|
+
});
|
|
10433
|
+
endpoints = {
|
|
10434
|
+
baseUrl: this.toJSON().baseUrl,
|
|
10435
|
+
cdnBaseUrl: server.toJSON().cdnBaseUrl
|
|
10436
|
+
};
|
|
10437
|
+
this.server = server;
|
|
10438
|
+
this.#uploadPresetId = options.uploadPresetId;
|
|
10439
|
+
}
|
|
10440
|
+
/** Keeps tokens out of `console.log` and `util.inspect`. */
|
|
10441
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
10442
|
+
return `MockCalmLensAppClient { uploadPresetId: ${JSON.stringify(this.#uploadPresetId)} }`;
|
|
10443
|
+
}
|
|
10444
|
+
};
|
|
10445
|
+
// src/testing/MockWebhooks.ts
|
|
10446
|
+
/**
|
|
10447
|
+
* The JSON body of a webhook delivery for an event, such as one from `events.list`: the event in
|
|
10448
|
+
* the same shape `events.list` returns it. Fields that are not part of that shape are left out.
|
|
10449
|
+
*/
|
|
9261
10450
|
const toWebhookEventPayload = (event) => ({
|
|
9262
10451
|
id: event.id,
|
|
9263
|
-
type: event.type,
|
|
9264
|
-
createdAt: event.createdAt,
|
|
9265
10452
|
projectId: event.projectId,
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
|
|
10453
|
+
type: event.type,
|
|
10454
|
+
severity: event.severity,
|
|
10455
|
+
title: event.title,
|
|
10456
|
+
description: event.description ?? null,
|
|
10457
|
+
assetId: event.assetId ?? null,
|
|
10458
|
+
asset: event.asset ?? null,
|
|
10459
|
+
facts: event.facts,
|
|
10460
|
+
createdAt: event.createdAt
|
|
9274
10461
|
});
|
|
9275
10462
|
/** `v1=<hex HMAC-SHA256 of "<timestamp>.<body>">`, the `x-calmlens-signature` header value. */
|
|
9276
|
-
const createWebhookSignatureHeader = async (secret, timestamp, body) => `v1=${await
|
|
10463
|
+
const createWebhookSignatureHeader = async (secret, timestamp, body) => `v1=${await require_CryptoUtils_B_x8lMIx.hmacSha256Hex(secret, `${timestamp}.${body}`)}`;
|
|
9277
10464
|
/** Headers for one signed delivery of `body`, as CalmLens sends them. */
|
|
9278
10465
|
const createWebhookHeaders = async (options) => {
|
|
9279
10466
|
const timestamp = options.timestampSeconds ?? Math.floor(Date.now() / 1e3);
|
|
9280
10467
|
return {
|
|
9281
10468
|
"content-type": "application/json",
|
|
9282
|
-
[
|
|
9283
|
-
[
|
|
9284
|
-
[
|
|
10469
|
+
[require_CalmLensServerClient.WEBHOOK_DELIVERY_HEADER]: options.deliveryId,
|
|
10470
|
+
[require_CalmLensServerClient.WEBHOOK_TIMESTAMP_HEADER]: String(timestamp),
|
|
10471
|
+
[require_CalmLensServerClient.WEBHOOK_SIGNATURE_HEADER]: await createWebhookSignatureHeader(options.secret, timestamp, options.body)
|
|
9285
10472
|
};
|
|
9286
10473
|
};
|
|
9287
|
-
//#endregion
|
|
9288
10474
|
exports.InMemoryMockDatabaseStorage = InMemoryMockDatabaseStorage;
|
|
9289
10475
|
exports.InMemoryMockImageStorage = InMemoryMockImageStorage;
|
|
9290
10476
|
exports.MEDIA_TEST_SIGNALS = MEDIA_TEST_SIGNALS;
|
|
9291
10477
|
exports.MOCK_API_KEY_PREFIX = MOCK_API_KEY_PREFIX;
|
|
10478
|
+
exports.MockCalmLensAppClient = MockCalmLensAppClient;
|
|
9292
10479
|
exports.MockCalmLensServerClient = MockCalmLensServerClient;
|
|
10480
|
+
exports.MockDecision = MockDecision;
|
|
9293
10481
|
exports.createMockApiKey = createMockApiKey;
|
|
9294
10482
|
exports.createWebhookHeaders = createWebhookHeaders;
|
|
9295
10483
|
exports.createWebhookSignatureHeader = createWebhookSignatureHeader;
|
|
9296
10484
|
exports.generateMediaTestCode = generateMediaTestCode;
|
|
10485
|
+
exports.mockDecision = mockDecision;
|
|
10486
|
+
exports.signUploadToken = require_CalmLensServerClient.signUploadTokenWithApiKey;
|
|
9297
10487
|
exports.toWebhookEventPayload = toWebhookEventPayload;
|