@calmlens/sdk 0.9.1 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +156 -60
- package/cjs/CalmLensTypes-DjYFaTyh.d.ts +8608 -0
- package/cjs/CalmLensWebhooks-DJnaLpRy.js +741 -0
- package/cjs/index.d.ts +276 -7525
- package/cjs/index.js +672 -1185
- package/cjs/testing.d.ts +83 -8
- package/cjs/testing.js +991 -596
- package/esm/CalmLensTypes-DjYFaTyh.d.ts +8608 -0
- package/esm/CalmLensWebhooks-DLUozliX.js +556 -0
- package/esm/index.d.ts +276 -7525
- package/esm/index.js +658 -1169
- package/esm/testing.d.ts +83 -8
- package/esm/testing.js +960 -592
- package/package.json +24 -18
- package/cjs/CalmLensTypes-CZqyni4p.d.ts +0 -3633
- package/cjs/CalmLensUtils-B6vwJijv.js +0 -657
- package/esm/CalmLensTypes-CZqyni4p.d.ts +0 -3633
- package/esm/CalmLensUtils-O1xk_Ts0.js +0 -492
package/esm/index.js
CHANGED
|
@@ -1,401 +1,335 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
import * as zod from "zod";
|
|
3
|
-
import "zod/v4";
|
|
4
|
-
import { Api, FetchRequestBackend, RequestCoalescingMiddleware, RequestEvent, getErrorResponse, isRequestError } from "api-def";
|
|
5
|
-
//#region ../shared/src/ModerationCategoryDefinitions.ts
|
|
6
|
-
const MODERATION_CATEGORY_DEFINITIONS = {
|
|
7
|
-
sexual: {
|
|
8
|
-
summary: "Sexual acts, explicit sexual anatomy, sexual services, sexual assault depictions and clearly erotic material.",
|
|
9
|
-
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.",
|
|
10
|
-
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.",
|
|
11
|
-
docsHref: "/docs/guides/moderation-categories#sexual-content"
|
|
12
|
-
},
|
|
13
|
-
violence: {
|
|
14
|
-
summary: "Physical attacks, injury, death, threats against others, and planning or encouraging physical violence.",
|
|
15
|
-
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.",
|
|
16
|
-
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.",
|
|
17
|
-
docsHref: "/docs/guides/moderation-categories#violence"
|
|
18
|
-
},
|
|
19
|
-
"self-harm": {
|
|
20
|
-
summary: "Intentional self-injury, suicide, preparation, methods or encouragement; text also includes current or recurring self-harm thoughts.",
|
|
21
|
-
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.",
|
|
22
|
-
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.",
|
|
23
|
-
docsHref: "/docs/guides/moderation-categories#self-harm"
|
|
24
|
-
},
|
|
25
|
-
harassment: {
|
|
26
|
-
summary: "Targeted personal abuse, humiliation, bullying or serious intimidation; ordinary criticism and neutral reporting are excluded.",
|
|
27
|
-
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.",
|
|
28
|
-
exclusions: "Disagreement, ordinary criticism of conduct or competence, untargeted profanity and neutral reporting of abuse do not qualify without a separate personal attack. Criticism can still qualify when it also contains personal degradation.",
|
|
29
|
-
docsHref: "/docs/guides/moderation-categories#harassment-and-bullying"
|
|
30
|
-
},
|
|
31
|
-
hate: {
|
|
32
|
-
summary: "Identity-based abuse, dehumanization, discriminatory exclusion or threats against people because of protected characteristics.",
|
|
33
|
-
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.",
|
|
34
|
-
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.",
|
|
35
|
-
docsHref: "/docs/guides/moderation-categories#hate-and-identity-abuse"
|
|
36
|
-
},
|
|
37
|
-
"criminal-activity": {
|
|
38
|
-
summary: "Planning, assistance, instructions, encouragement or facilitation of a concrete criminal or seriously dangerous act.",
|
|
39
|
-
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.",
|
|
40
|
-
exclusions: "Neutral reporting, prevention, lawful safety advice, fiction without criminal assistance, and vague dangerous references do not qualify. Masks, computers, payment cards, detention or ordinary tools do not establish criminal facilitation by themselves. A clear visual demonstration of how to carry out a crime can qualify.",
|
|
41
|
-
docsHref: "/docs/guides/moderation-categories#criminal-and-dangerous-activity"
|
|
42
|
-
}
|
|
43
|
-
};
|
|
1
|
+
import { A as getRawBase64, C as MIN_MULTIPART_IMAGE_UPLOAD_BYTES, E as stringifyJsonCanonical, O as assetToData, S as MAX_IMAGE_UPLOAD_BYTES, T as isRejectedUploadMimeType, _ as invalidArgument, a as parseWebhookEvent, c as appendCdnSelector, d as toTextAssetOptions, f as validateClientOptions, g as SDK_ERROR_CODES, h as CalmLensError, i as WEBHOOK_TIMESTAMP_HEADER, j as validateAssetPayload, k as getAssetPayloadByteLength, l as encodePathSegment, m as CALMLENS_ERROR_CODES, n as WEBHOOK_SIGNATURE_HEADER, o as verifyWebhookSignature, p as validateRequestOptions, r as WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, t as WEBHOOK_DELIVERY_HEADER, u as stringifyQuery, v as invalidConfig, w as getRejectedUploadMimeTypeError, y as API_ERROR_CODES } from "./CalmLensWebhooks-DLUozliX.js";
|
|
44
2
|
//#endregion
|
|
45
|
-
//#region
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
"injury",
|
|
56
|
-
"death",
|
|
57
|
-
"graphic",
|
|
58
|
-
"self-harm-thoughts",
|
|
59
|
-
"self-harm-preparation",
|
|
60
|
-
"self-harm-act",
|
|
61
|
-
"self-harm-methods",
|
|
62
|
-
"self-harm-encouragement",
|
|
63
|
-
"personal-abuse",
|
|
64
|
-
"targeted-intimidation",
|
|
65
|
-
"harassment-threat",
|
|
66
|
-
"sexual-harassment",
|
|
67
|
-
"harassment-encouragement",
|
|
68
|
-
"identity-abuse",
|
|
69
|
-
"dehumanization",
|
|
70
|
-
"identity-inferiority",
|
|
71
|
-
"discriminatory-exclusion",
|
|
72
|
-
"hate-threat",
|
|
73
|
-
"criminal-demonstration",
|
|
74
|
-
"criminal-preparation",
|
|
75
|
-
"criminal-request",
|
|
76
|
-
"criminal-instructions",
|
|
77
|
-
"criminal-promotion",
|
|
78
|
-
"historical",
|
|
79
|
-
"documentary",
|
|
80
|
-
"fictional",
|
|
81
|
-
"support-prevention"
|
|
82
|
-
];
|
|
83
|
-
const SHARED_QUALIFIERS = [
|
|
84
|
-
"historical",
|
|
85
|
-
"documentary",
|
|
86
|
-
"fictional",
|
|
87
|
-
"support-prevention"
|
|
88
|
-
];
|
|
89
|
-
[...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS], [...SHARED_QUALIFIERS];
|
|
90
|
-
//#endregion
|
|
91
|
-
//#region ../../node_modules/zod-meta/esm/index.js
|
|
92
|
-
const createMetaType = (type) => {
|
|
93
|
-
return Object.assign((value) => {
|
|
94
|
-
return {
|
|
95
|
-
type,
|
|
96
|
-
data: value !== null && value !== void 0 ? value : {}
|
|
97
|
-
};
|
|
98
|
-
}, type);
|
|
3
|
+
//#region src/client/CalmLensVersion.ts
|
|
4
|
+
/** This SDK's version, from its package.json. */
|
|
5
|
+
const SDK_VERSION = "0.10.1";
|
|
6
|
+
const runtimeName = () => {
|
|
7
|
+
const runtime = globalThis;
|
|
8
|
+
if (runtime.Bun?.version) return `bun/${runtime.Bun.version}`;
|
|
9
|
+
if (runtime.Deno?.version?.deno) return `deno/${runtime.Deno.version.deno}`;
|
|
10
|
+
if (runtime.navigator?.userAgent === "Cloudflare-Workers") return "workerd";
|
|
11
|
+
if (runtime.process?.versions?.node) return `node/${runtime.process.versions.node}`;
|
|
12
|
+
return "unknown";
|
|
99
13
|
};
|
|
100
|
-
|
|
14
|
+
/** The `User-Agent` the SDK sends from server runtimes, e.g. `calmlens-js/0.10.0 (node/22.12.0)`. */
|
|
15
|
+
const getUserAgent = () => `calmlens-js/${SDK_VERSION} (${runtimeName()})`;
|
|
16
|
+
/** True in a web page, where API keys would be visible to every visitor. */
|
|
17
|
+
const isBrowserRuntime = () => typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
|
|
101
18
|
//#endregion
|
|
102
|
-
//#region
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return true;
|
|
114
|
-
}, "Not long enough").refine((value) => {
|
|
115
|
-
var _options$maxLength, _options$type;
|
|
116
|
-
const sizeLimit = (_options$maxLength = options === null || options === void 0 ? void 0 : options.maxLength) !== null && _options$maxLength !== void 0 ? _options$maxLength : SIZE_LIMITS[(_options$type = options === null || options === void 0 ? void 0 : options.type) !== null && _options$type !== void 0 ? _options$type : "medium"];
|
|
117
|
-
return typeof value === "string" && value.length < sizeLimit;
|
|
118
|
-
}, "Too long");
|
|
119
|
-
};
|
|
120
|
-
const CLASSIFICATION_INPUT_TYPE_SCHEMA = zod.enum(["text", "image"]);
|
|
121
|
-
const CLASSIFICATION_CATEGORIES = [
|
|
122
|
-
"sexual",
|
|
123
|
-
"violence",
|
|
124
|
-
"self-harm",
|
|
125
|
-
"harassment",
|
|
126
|
-
"hate",
|
|
127
|
-
"criminal-activity"
|
|
128
|
-
];
|
|
129
|
-
const CLASSIFICATION_CATEGORY_SCHEMA = zod.enum(CLASSIFICATION_CATEGORIES);
|
|
130
|
-
zod.object({
|
|
131
|
-
category: CLASSIFICATION_CATEGORY_SCHEMA,
|
|
132
|
-
displayName: saneStringField(),
|
|
133
|
-
description: saneStringField(),
|
|
134
|
-
supportedInputTypes: zod.array(CLASSIFICATION_INPUT_TYPE_SCHEMA).min(1)
|
|
135
|
-
});
|
|
136
|
-
MODERATION_CATEGORY_DEFINITIONS.sexual.summary, MODERATION_CATEGORY_DEFINITIONS.violence.summary, MODERATION_CATEGORY_DEFINITIONS["self-harm"].summary, MODERATION_CATEGORY_DEFINITIONS.harassment.summary, MODERATION_CATEGORY_DEFINITIONS.hate.summary, MODERATION_CATEGORY_DEFINITIONS["criminal-activity"].summary;
|
|
137
|
-
const LIKELIHOOD_SCHEMA = zod.enum([
|
|
138
|
-
"very-unlikely",
|
|
139
|
-
"unlikely",
|
|
140
|
-
"possible",
|
|
141
|
-
"likely",
|
|
142
|
-
"very-likely"
|
|
19
|
+
//#region src/client/CalmLensTransport.ts
|
|
20
|
+
/** Retry delays grow from this base, doubling per attempt, with jitter. */
|
|
21
|
+
const RETRY_BASE_DELAY_MS = 500;
|
|
22
|
+
const RETRY_MAX_DELAY_MS = 8e3;
|
|
23
|
+
/** A `Retry-After` longer than this is returned to the caller instead of waited out. */
|
|
24
|
+
const RETRY_AFTER_MAX_MS = 6e4;
|
|
25
|
+
const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
26
|
+
429,
|
|
27
|
+
502,
|
|
28
|
+
503,
|
|
29
|
+
504
|
|
143
30
|
]);
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
likelihood: LIKELIHOOD_SCHEMA
|
|
150
|
-
}).strict().refine(({ score, scopeScore }) => scopeScore === void 0 || scopeScore <= score, {
|
|
151
|
-
message: "Only confidence cannot exceed Any confidence",
|
|
152
|
-
path: ["scopeScore"]
|
|
153
|
-
}), zod.object({
|
|
154
|
-
status: zod.literal("not-evaluated"),
|
|
155
|
-
reason: CLASSIFICATION_NOT_EVALUATED_REASON_SCHEMA
|
|
156
|
-
}).strict()]));
|
|
157
|
-
const CLASSIFICATION_CATEGORY_REPORT_SCHEMA = zod.discriminatedUnion("status", [
|
|
158
|
-
zod.object({
|
|
159
|
-
status: zod.literal("evaluated"),
|
|
160
|
-
likelihood: LIKELIHOOD_SCHEMA,
|
|
161
|
-
score: zod.number().min(0).max(1),
|
|
162
|
-
qualifiers: CLASSIFICATION_QUALIFIER_RESULTS_SCHEMA.optional()
|
|
163
|
-
}).strict(),
|
|
164
|
-
zod.object({
|
|
165
|
-
status: zod.literal("partial"),
|
|
166
|
-
likelihood: LIKELIHOOD_SCHEMA,
|
|
167
|
-
score: zod.number().min(0).max(1),
|
|
168
|
-
qualifiers: CLASSIFICATION_QUALIFIER_RESULTS_SCHEMA.optional()
|
|
169
|
-
}).strict(),
|
|
170
|
-
zod.object({
|
|
171
|
-
status: zod.literal("not-evaluated"),
|
|
172
|
-
reason: CLASSIFICATION_NOT_EVALUATED_REASON_SCHEMA,
|
|
173
|
-
qualifiers: CLASSIFICATION_QUALIFIER_RESULTS_SCHEMA.optional()
|
|
174
|
-
}).strict()
|
|
175
|
-
]);
|
|
176
|
-
const CATEGORIES_SCHEMA = zod.object({
|
|
177
|
-
sexual: CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
|
|
178
|
-
violence: CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
|
|
179
|
-
"self-harm": CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
|
|
180
|
-
harassment: CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
|
|
181
|
-
hate: CLASSIFICATION_CATEGORY_REPORT_SCHEMA,
|
|
182
|
-
"criminal-activity": CLASSIFICATION_CATEGORY_REPORT_SCHEMA
|
|
183
|
-
}).strict();
|
|
184
|
-
const CLASSIFICATION_LABEL_SCHEMA = zod.object({
|
|
185
|
-
description: saneStringField(),
|
|
186
|
-
score: zod.number(),
|
|
187
|
-
topicalityScore: zod.number().nullish(),
|
|
188
|
-
likelihood: LIKELIHOOD_SCHEMA,
|
|
189
|
-
knowledgeBaseId: saneStringField().nullish()
|
|
190
|
-
});
|
|
191
|
-
zod.object({
|
|
192
|
-
processedAt: zod.number(),
|
|
193
|
-
categories: CATEGORIES_SCHEMA,
|
|
194
|
-
flagged: zod.boolean(),
|
|
195
|
-
score: zod.number(),
|
|
196
|
-
notes: saneStringField().nullish(),
|
|
197
|
-
media: zod.object({
|
|
198
|
-
durationSeconds: zod.number().positive(),
|
|
199
|
-
frameIntervalSeconds: zod.number().positive().optional(),
|
|
200
|
-
sampledFrames: zod.number().int().nonnegative(),
|
|
201
|
-
audioTranscribed: zod.boolean(),
|
|
202
|
-
segments: zod.array(zod.object({
|
|
203
|
-
source: zod.enum([
|
|
204
|
-
"frame",
|
|
205
|
-
"speech",
|
|
206
|
-
"frame-text",
|
|
207
|
-
"clip"
|
|
208
|
-
]),
|
|
209
|
-
startSeconds: zod.number().nonnegative(),
|
|
210
|
-
endSeconds: zod.number().nonnegative(),
|
|
211
|
-
categories: CATEGORIES_SCHEMA
|
|
212
|
-
}).strict())
|
|
213
|
-
}).strict().optional(),
|
|
214
|
-
labels: zod.array(CLASSIFICATION_LABEL_SCHEMA).nullish()
|
|
215
|
-
}).strict();
|
|
216
|
-
//#endregion
|
|
217
|
-
//#region ../shared/src/MediaTestTriggers.ts
|
|
218
|
-
const MEDIA_TEST_SIGNALS = [...CLASSIFICATION_CATEGORIES, "violence.graphic"];
|
|
219
|
-
const HEX_WORDS = [
|
|
220
|
-
"zero",
|
|
221
|
-
"one",
|
|
222
|
-
"two",
|
|
223
|
-
"three",
|
|
224
|
-
"four",
|
|
225
|
-
"five",
|
|
226
|
-
"six",
|
|
227
|
-
"seven",
|
|
228
|
-
"eight",
|
|
229
|
-
"nine",
|
|
230
|
-
"alpha",
|
|
231
|
-
"bravo",
|
|
232
|
-
"charlie",
|
|
233
|
-
"delta",
|
|
234
|
-
"echo",
|
|
235
|
-
"foxtrot"
|
|
236
|
-
];
|
|
237
|
-
/** Read this aloud, or render it as large, high-contrast text in a video frame. */
|
|
238
|
-
function generateMediaTestCode(signal, projectId) {
|
|
239
|
-
if (!MEDIA_TEST_SIGNALS.includes(signal)) throw new Error("Unsupported media test signal");
|
|
240
|
-
const suffix = projectId.slice(-8).toLowerCase();
|
|
241
|
-
if (!/^[a-f0-9]{8}$/.test(suffix)) throw new Error("Expected a project ID with an eight-character hexadecimal suffix");
|
|
242
|
-
return `Calm Lens test ${[...suffix].map((character) => HEX_WORDS[Number.parseInt(character, 16)]).join(" ")} ${signal.replace(/[.-]/g, " ")} end test`;
|
|
243
|
-
}
|
|
244
|
-
//#endregion
|
|
245
|
-
//#region ../shared/src/CryptoUtils.ts
|
|
246
|
-
const encoder = new TextEncoder();
|
|
247
|
-
/** Lowercase hex of `bytes`, two characters per byte. */
|
|
248
|
-
const bytesToHex = (bytes) => Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
249
|
-
(function() {
|
|
250
|
-
var _ref = _asyncToGenerator(function* (value) {
|
|
251
|
-
return bytesToHex(yield crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
252
|
-
});
|
|
253
|
-
return function sha256Hex(_x) {
|
|
254
|
-
return _ref.apply(this, arguments);
|
|
255
|
-
};
|
|
256
|
-
})();
|
|
257
|
-
/** HMAC-SHA256 of `message` keyed with `secret` (both UTF-8 encoded), as lowercase hex. */
|
|
258
|
-
const hmacSha256Hex = function() {
|
|
259
|
-
var _ref2 = _asyncToGenerator(function* (secret, message) {
|
|
260
|
-
const key = yield crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
261
|
-
name: "HMAC",
|
|
262
|
-
hash: "SHA-256"
|
|
263
|
-
}, false, ["sign"]);
|
|
264
|
-
return bytesToHex(yield crypto.subtle.sign("HMAC", key, encoder.encode(message)));
|
|
265
|
-
});
|
|
266
|
-
return function hmacSha256Hex(_x2, _x3) {
|
|
267
|
-
return _ref2.apply(this, arguments);
|
|
268
|
-
};
|
|
269
|
-
}();
|
|
31
|
+
const timeoutReason = () => {
|
|
32
|
+
const reason = /* @__PURE__ */ new Error("The operation timed out.");
|
|
33
|
+
reason.name = "TimeoutError";
|
|
34
|
+
return reason;
|
|
35
|
+
};
|
|
270
36
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
37
|
+
* Links the callers' signals and a per-attempt deadline. `AbortSignal.any` holds its sources
|
|
38
|
+
* weakly, so a long-lived caller signal doesn't collect a listener per streamed response;
|
|
39
|
+
* runtimes without it get explicit listeners that `release` removes.
|
|
273
40
|
*/
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
41
|
+
const createAttemptSignal = (signals, timeoutMs) => {
|
|
42
|
+
const callers = signals.filter((signal) => signal !== void 0);
|
|
43
|
+
const deadline = new AbortController();
|
|
44
|
+
let timedOut = false;
|
|
45
|
+
let signal;
|
|
46
|
+
let onAbort;
|
|
47
|
+
if (typeof AbortSignal.any === "function") signal = AbortSignal.any([...callers, deadline.signal]);
|
|
48
|
+
else {
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
signal = controller.signal;
|
|
51
|
+
onAbort = (event) => controller.abort(event.target.reason);
|
|
52
|
+
for (const source of [...callers, deadline.signal]) if (source.aborted) controller.abort(source.reason);
|
|
53
|
+
else source.addEventListener("abort", onAbort, { once: true });
|
|
54
|
+
}
|
|
55
|
+
let timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
|
|
56
|
+
timedOut = true;
|
|
57
|
+
deadline.abort(timeoutReason());
|
|
58
|
+
}, timeoutMs);
|
|
290
59
|
return {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
60
|
+
signal,
|
|
61
|
+
isTimedOut: () => timedOut,
|
|
62
|
+
isCancelled: () => callers.some((caller) => caller.aborted),
|
|
63
|
+
clearDeadline: () => {
|
|
64
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
65
|
+
timer = void 0;
|
|
66
|
+
},
|
|
67
|
+
release: () => {
|
|
68
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
69
|
+
timer = void 0;
|
|
70
|
+
if (onAbort) for (const caller of callers) caller.removeEventListener("abort", onAbort);
|
|
302
71
|
}
|
|
303
72
|
};
|
|
304
73
|
};
|
|
305
|
-
/** `v1=<hex HMAC-SHA256 of "<timestamp>.<body>">`, the `x-calmlens-signature` header value. */
|
|
306
|
-
const createWebhookSignatureHeader = function() {
|
|
307
|
-
var _ref = _asyncToGenerator(function* (secret, timestamp, body) {
|
|
308
|
-
return `v1=${yield hmacSha256Hex(secret, `${timestamp}.${body}`)}`;
|
|
309
|
-
});
|
|
310
|
-
return function createWebhookSignatureHeader(_x, _x2, _x3) {
|
|
311
|
-
return _ref.apply(this, arguments);
|
|
312
|
-
};
|
|
313
|
-
}();
|
|
314
|
-
/** Headers for one signed delivery of `body`. */
|
|
315
|
-
const createWebhookHeaders = function() {
|
|
316
|
-
var _ref2 = _asyncToGenerator(function* (options) {
|
|
317
|
-
var _options$timestampSec;
|
|
318
|
-
const timestamp = (_options$timestampSec = options.timestampSeconds) !== null && _options$timestampSec !== void 0 ? _options$timestampSec : Math.floor(Date.now() / 1e3);
|
|
319
|
-
return {
|
|
320
|
-
"content-type": "application/json",
|
|
321
|
-
[WEBHOOK_DELIVERY_HEADER]: options.deliveryId,
|
|
322
|
-
[WEBHOOK_TIMESTAMP_HEADER]: String(timestamp),
|
|
323
|
-
[WEBHOOK_SIGNATURE_HEADER]: yield createWebhookSignatureHeader(options.secret, timestamp, options.body)
|
|
324
|
-
};
|
|
325
|
-
});
|
|
326
|
-
return function createWebhookHeaders(_x4) {
|
|
327
|
-
return _ref2.apply(this, arguments);
|
|
328
|
-
};
|
|
329
|
-
}();
|
|
330
74
|
/**
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
* (default 300) of now. Returns `false` for anything missing or malformed.
|
|
75
|
+
* A redirect the SDK must not follow: it would send the API key to another origin. Browsers report
|
|
76
|
+
* a manual redirect as an opaque response; other runtimes return the 3xx with its Location.
|
|
334
77
|
*/
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
78
|
+
const isRedirect = (response) => response.type === "opaqueredirect" || response.status >= 300 && response.status < 400 && response.headers.has("location");
|
|
79
|
+
/** Turns anything thrown while sending or reading a request into a `CalmLensError`. */
|
|
80
|
+
const toFailure = (cause, attempt) => {
|
|
81
|
+
if (CalmLensError.isCalmLensError(cause)) return cause;
|
|
82
|
+
if (attempt.isCancelled()) return new CalmLensError(SDK_ERROR_CODES.CANCELLED, {
|
|
83
|
+
message: "Request cancelled",
|
|
84
|
+
cause
|
|
85
|
+
});
|
|
86
|
+
if (attempt.isTimedOut()) return new CalmLensError(SDK_ERROR_CODES.TIMEOUT, {
|
|
87
|
+
message: "Request timed out",
|
|
88
|
+
cause
|
|
89
|
+
});
|
|
90
|
+
return new CalmLensError(SDK_ERROR_CODES.TRANSPORT_ERROR, {
|
|
91
|
+
message: "Request failed",
|
|
92
|
+
cause
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
96
|
+
if (signal?.aborted) {
|
|
97
|
+
reject(new CalmLensError(SDK_ERROR_CODES.CANCELLED, { message: "Request cancelled" }));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const onAbort = () => {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
reject(new CalmLensError(SDK_ERROR_CODES.CANCELLED, { message: "Request cancelled" }));
|
|
103
|
+
};
|
|
104
|
+
const timer = setTimeout(() => {
|
|
105
|
+
signal?.removeEventListener("abort", onAbort);
|
|
106
|
+
resolve();
|
|
107
|
+
}, ms);
|
|
108
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
109
|
+
});
|
|
110
|
+
/** Exponential backoff with jitter: half the delay is fixed, the other half random. */
|
|
111
|
+
const getRetryDelayMs = (attempt, random = Math.random) => {
|
|
112
|
+
const delay = Math.min(RETRY_MAX_DELAY_MS, RETRY_BASE_DELAY_MS * 2 ** attempt);
|
|
113
|
+
return Math.round(delay / 2 + random() * (delay / 2));
|
|
114
|
+
};
|
|
115
|
+
/** Parses `Retry-After` as seconds or an HTTP date, in seconds from `now`. */
|
|
116
|
+
const parseRetryAfter = (value, now = Date.now()) => {
|
|
117
|
+
if (!value) return void 0;
|
|
118
|
+
const trimmed = value.trim();
|
|
119
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed);
|
|
120
|
+
const date = Date.parse(trimmed);
|
|
121
|
+
if (Number.isNaN(date)) return void 0;
|
|
122
|
+
return Math.max(0, Math.ceil((date - now) / 1e3));
|
|
123
|
+
};
|
|
124
|
+
/** Bodies that can be sent again unchanged. Streams are consumed by the first attempt. */
|
|
125
|
+
const isReplayableBody = (body) => body === void 0 || body === null || typeof body === "string" || body instanceof Blob || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || body instanceof URLSearchParams;
|
|
126
|
+
const isApiErrorPayload = (value) => !!value && typeof value === "object" && typeof value.code === "string";
|
|
127
|
+
/** Reads at most `limit` bytes of a body, failing with `sdk/response-too-large` beyond that. */
|
|
128
|
+
const readBody = async (response, limit) => {
|
|
129
|
+
const declared = Number(response.headers.get("content-length"));
|
|
130
|
+
const tooLarge = () => new CalmLensError(SDK_ERROR_CODES.RESPONSE_TOO_LARGE, {
|
|
131
|
+
message: `Response body exceeds ${limit} bytes`,
|
|
132
|
+
status: response.status,
|
|
133
|
+
requestId: response.headers.get("x-request-id") ?? void 0,
|
|
134
|
+
headers: response.headers
|
|
135
|
+
});
|
|
136
|
+
if (Number.isFinite(declared) && declared > limit) {
|
|
137
|
+
await response.body?.cancel().catch(() => {});
|
|
138
|
+
throw tooLarge();
|
|
139
|
+
}
|
|
140
|
+
if (!response.body) return /* @__PURE__ */ new Uint8Array(0);
|
|
141
|
+
const reader = response.body.getReader();
|
|
142
|
+
const chunks = [];
|
|
143
|
+
let total = 0;
|
|
144
|
+
for (;;) {
|
|
145
|
+
const { done, value } = await reader.read();
|
|
146
|
+
if (done) break;
|
|
147
|
+
total += value.byteLength;
|
|
148
|
+
if (total > limit) {
|
|
149
|
+
await reader.cancel().catch(() => {});
|
|
150
|
+
throw tooLarge();
|
|
151
|
+
}
|
|
152
|
+
chunks.push(value);
|
|
153
|
+
}
|
|
154
|
+
const bytes = new Uint8Array(total);
|
|
155
|
+
let offset = 0;
|
|
156
|
+
for (const chunk of chunks) {
|
|
157
|
+
bytes.set(chunk, offset);
|
|
158
|
+
offset += chunk.byteLength;
|
|
159
|
+
}
|
|
160
|
+
return bytes;
|
|
161
|
+
};
|
|
162
|
+
const responseInfo = (response) => ({
|
|
163
|
+
status: response.status,
|
|
164
|
+
requestId: response.headers.get("x-request-id") ?? void 0,
|
|
165
|
+
headers: response.headers,
|
|
166
|
+
retryAfter: parseRetryAfter(response.headers.get("retry-after"))
|
|
167
|
+
});
|
|
168
|
+
/** The `CalmLensError` for a non-2xx response whose body has been read. */
|
|
169
|
+
const toHttpError = (response, bytes) => {
|
|
170
|
+
let data;
|
|
171
|
+
try {
|
|
172
|
+
data = JSON.parse(new TextDecoder().decode(bytes));
|
|
173
|
+
} catch {}
|
|
174
|
+
if (isApiErrorPayload(data)) {
|
|
175
|
+
const { code, message, details, traceId } = data;
|
|
176
|
+
return new CalmLensError(code, {
|
|
177
|
+
message: message ?? code,
|
|
178
|
+
...responseInfo(response),
|
|
179
|
+
details: details || traceId ? {
|
|
180
|
+
...details,
|
|
181
|
+
...traceId ? { traceId } : {}
|
|
182
|
+
} : void 0
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return new CalmLensError(SDK_ERROR_CODES.HTTP_ERROR, {
|
|
186
|
+
message: `HTTP ${response.status}`,
|
|
187
|
+
...responseInfo(response)
|
|
352
188
|
});
|
|
353
|
-
|
|
354
|
-
|
|
189
|
+
};
|
|
190
|
+
/** Sends requests with the client's credentials, deadlines, retries and response limits. */
|
|
191
|
+
var CalmLensTransport = class {
|
|
192
|
+
constructor(settings) {
|
|
193
|
+
this.settings = settings;
|
|
194
|
+
}
|
|
195
|
+
headersFor(request) {
|
|
196
|
+
const headers = new Headers(request.init?.headers);
|
|
197
|
+
for (const [name, value] of Object.entries(request.headers ?? {})) headers.set(name, value);
|
|
198
|
+
if (!headers.has("accept") && request.deadline === "body") headers.set("accept", "application/json");
|
|
199
|
+
headers.set("x-api-key", this.settings.apiKey);
|
|
200
|
+
if (!isBrowserRuntime()) headers.set("user-agent", getUserAgent());
|
|
201
|
+
if (request.idempotencyKey) headers.set("idempotency-key", request.idempotencyKey);
|
|
202
|
+
return Object.fromEntries(headers.entries());
|
|
203
|
+
}
|
|
204
|
+
canRetry(request) {
|
|
205
|
+
return this.settings.maxRetries > 0 && (request.method === "GET" || request.method === "HEAD" || !!request.idempotencyKey) && isReplayableBody(request.body);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Sends a request and returns the response, retrying transport failures, 429, 502–504 and
|
|
209
|
+
* `idempotency/request-in-progress` when the request is safe to repeat. Non-2xx responses
|
|
210
|
+
* throw unless `allowHttpErrors` is set. Call `release` once the body has been read.
|
|
211
|
+
*/
|
|
212
|
+
async send(request, { allowHttpErrors = false } = {}) {
|
|
213
|
+
const fetchImpl = request.fetch ?? this.settings.fetch;
|
|
214
|
+
const retryable = this.canRetry(request);
|
|
215
|
+
const headers = this.headersFor(request);
|
|
216
|
+
for (let attemptNumber = 0;; attemptNumber++) {
|
|
217
|
+
const canRetryAgain = retryable && attemptNumber < this.settings.maxRetries;
|
|
218
|
+
const attempt = createAttemptSignal([this.settings.signal, request.init?.signal ?? void 0], request.timeoutMs);
|
|
219
|
+
let response;
|
|
220
|
+
try {
|
|
221
|
+
attempt.signal.throwIfAborted();
|
|
222
|
+
response = await fetchImpl(request.url, {
|
|
223
|
+
...request.init,
|
|
224
|
+
method: request.method,
|
|
225
|
+
headers,
|
|
226
|
+
body: request.body,
|
|
227
|
+
redirect: "manual",
|
|
228
|
+
signal: attempt.signal,
|
|
229
|
+
duplex: "half"
|
|
230
|
+
});
|
|
231
|
+
if (isRedirect(response)) {
|
|
232
|
+
await response.body?.cancel().catch(() => {});
|
|
233
|
+
throw new CalmLensError(SDK_ERROR_CODES.TRANSPORT_ERROR, { message: `Unexpected redirect (HTTP ${response.status})` });
|
|
234
|
+
}
|
|
235
|
+
} catch (cause) {
|
|
236
|
+
attempt.release();
|
|
237
|
+
const failure = toFailure(cause, attempt);
|
|
238
|
+
if (canRetryAgain && failure.code === SDK_ERROR_CODES.TRANSPORT_ERROR) {
|
|
239
|
+
await sleep(getRetryDelayMs(attemptNumber), this.settings.signal);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
throw failure;
|
|
243
|
+
}
|
|
244
|
+
if (request.deadline === "headers") attempt.clearDeadline();
|
|
245
|
+
if (response.ok) return {
|
|
246
|
+
response,
|
|
247
|
+
attempt
|
|
248
|
+
};
|
|
249
|
+
if (allowHttpErrors) {
|
|
250
|
+
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
251
|
+
const delayMs = retryAfter !== void 0 ? retryAfter * 1e3 : getRetryDelayMs(attemptNumber);
|
|
252
|
+
if (!canRetryAgain || !RETRYABLE_STATUSES.has(response.status) || delayMs > RETRY_AFTER_MAX_MS) return {
|
|
253
|
+
response,
|
|
254
|
+
attempt
|
|
255
|
+
};
|
|
256
|
+
attempt.release();
|
|
257
|
+
await response.body?.cancel().catch(() => {});
|
|
258
|
+
await sleep(delayMs, this.settings.signal);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
let error;
|
|
262
|
+
try {
|
|
263
|
+
error = toHttpError(response, await readBody(response, this.settings.maxResponseBytes));
|
|
264
|
+
} catch (cause) {
|
|
265
|
+
error = toFailure(cause, attempt);
|
|
266
|
+
} finally {
|
|
267
|
+
attempt.release();
|
|
268
|
+
}
|
|
269
|
+
const retryDelayMs = error.retryAfter !== void 0 ? error.retryAfter * 1e3 : getRetryDelayMs(attemptNumber);
|
|
270
|
+
if (canRetryAgain && retryDelayMs <= RETRY_AFTER_MAX_MS && (error.status !== void 0 && RETRYABLE_STATUSES.has(error.status) || error.code === API_ERROR_CODES.IDEMPOTENT_REQUEST_IN_PROGRESS)) {
|
|
271
|
+
await sleep(retryDelayMs, this.settings.signal);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/** Sends a request and parses its JSON response body. */
|
|
278
|
+
async json(request) {
|
|
279
|
+
const { response, attempt } = await this.send(request);
|
|
280
|
+
try {
|
|
281
|
+
const bytes = await readBody(response, this.settings.maxResponseBytes);
|
|
282
|
+
if (bytes.byteLength === 0 && (response.status === 204 || request.method === "DELETE")) return;
|
|
283
|
+
try {
|
|
284
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
285
|
+
} catch (cause) {
|
|
286
|
+
throw new CalmLensError(SDK_ERROR_CODES.INVALID_RESPONSE, {
|
|
287
|
+
message: `Invalid JSON response (HTTP ${response.status})`,
|
|
288
|
+
...responseInfo(response),
|
|
289
|
+
cause
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
} catch (cause) {
|
|
293
|
+
throw toFailure(cause, attempt);
|
|
294
|
+
} finally {
|
|
295
|
+
attempt.release();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
355
299
|
//#endregion
|
|
356
300
|
//#region src/client/CalmLensClient.ts
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
const _excluded4 = ["assetIdentifier", "init"];
|
|
361
|
-
const _excluded5 = ["assetId"];
|
|
362
|
-
const _excluded6 = ["assetId"];
|
|
363
|
-
const _excluded7 = ["assetId"];
|
|
364
|
-
const _excluded8 = ["folderId"];
|
|
301
|
+
const DEFAULT_TIMEOUT_MS = 12e4;
|
|
302
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
303
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 33554432;
|
|
365
304
|
const toBlobPartBytes = (value) => {
|
|
366
305
|
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
367
306
|
return Uint8Array.from(value);
|
|
368
307
|
};
|
|
369
308
|
const normalizePayloadForRequest = (payload) => {
|
|
370
|
-
if ("base64" in payload && payload.base64) return
|
|
309
|
+
if ("base64" in payload && payload.base64) return {
|
|
310
|
+
...payload,
|
|
311
|
+
base64: getRawBase64(payload.base64)
|
|
312
|
+
};
|
|
371
313
|
if ("uint8Array" in payload && payload.uint8Array) {
|
|
372
|
-
const { uint8Array
|
|
373
|
-
return
|
|
314
|
+
const { uint8Array, ...rest } = payload;
|
|
315
|
+
return {
|
|
316
|
+
...rest,
|
|
317
|
+
data: new Blob([toBlobPartBytes(uint8Array)])
|
|
318
|
+
};
|
|
374
319
|
}
|
|
375
320
|
if ("buffer" in payload && payload.buffer) {
|
|
376
|
-
const { buffer
|
|
377
|
-
return
|
|
321
|
+
const { buffer, ...rest } = payload;
|
|
322
|
+
return {
|
|
323
|
+
...rest,
|
|
324
|
+
data: new Blob([toBlobPartBytes(buffer)])
|
|
325
|
+
};
|
|
378
326
|
}
|
|
379
327
|
return payload;
|
|
380
328
|
};
|
|
381
|
-
const normalizeMetadataForMultipart = (metadata) => {
|
|
382
|
-
if (!Array.isArray(metadata)) return metadata;
|
|
383
|
-
return Object.fromEntries(metadata.map((item, index) => [index, item]));
|
|
384
|
-
};
|
|
385
|
-
const normalizeMetadataOptionsForRequest = (options) => {
|
|
386
|
-
if (!("metadata" in options) || !Array.isArray(options.metadata)) return options;
|
|
387
|
-
return _objectSpread2(_objectSpread2({}, options), {}, { metadata: normalizeMetadataForMultipart(options.metadata) });
|
|
388
|
-
};
|
|
389
|
-
const normalizeAssetOptionsForRequest = (options) => {
|
|
390
|
-
const normalizedOptions = normalizeMetadataOptionsForRequest(options);
|
|
391
|
-
if ("part" in normalizedOptions) return _objectSpread2(_objectSpread2({}, normalizedOptions), {}, { part: normalizePayloadForRequest(normalizedOptions.part) });
|
|
392
|
-
if ("content" in normalizedOptions && !("plainText" in normalizedOptions.content)) return _objectSpread2(_objectSpread2({}, normalizedOptions), {}, { content: normalizePayloadForRequest(normalizedOptions.content) });
|
|
393
|
-
return normalizedOptions;
|
|
394
|
-
};
|
|
395
329
|
const toRawUploadBody = (payload) => {
|
|
396
330
|
const data = assetToData(normalizePayloadForRequest(payload));
|
|
397
331
|
if (data instanceof Response) {
|
|
398
|
-
if (!data.body) throw
|
|
332
|
+
if (!data.body) throw new CalmLensError(API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Response upload data must have a body" });
|
|
399
333
|
return data.body;
|
|
400
334
|
}
|
|
401
335
|
if (data instanceof ArrayBuffer || data instanceof Blob) return data;
|
|
@@ -406,913 +340,468 @@ const setOptionalQueryValue = (params, key, value) => {
|
|
|
406
340
|
if (value !== void 0 && value !== null) params.set(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
407
341
|
};
|
|
408
342
|
const assertValidUploadMimeType = (mimeType) => {
|
|
409
|
-
if (isRejectedUploadMimeType(mimeType)) throw
|
|
343
|
+
if (isRejectedUploadMimeType(mimeType)) throw new CalmLensError(API_ERROR_CODES.INVALID_UPLOAD_MIME_TYPE, {
|
|
410
344
|
message: getRejectedUploadMimeTypeError(mimeType),
|
|
411
345
|
details: { mimeType }
|
|
412
346
|
});
|
|
413
347
|
};
|
|
414
348
|
const isImageMimeType = (mimeType) => mimeType.toLowerCase().startsWith("image/");
|
|
349
|
+
const invalidPayload = (message) => new CalmLensError(API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message });
|
|
415
350
|
const prepareSubmitAssetOptions = (options) => {
|
|
416
|
-
if (!
|
|
417
|
-
if (!options
|
|
418
|
-
if (!
|
|
419
|
-
if ("part" in options &&
|
|
351
|
+
if (!options || typeof options !== "object") throw invalidArgument("Asset options are required");
|
|
352
|
+
if (!("content" in options) && !("url" in options) && !("part" in options)) throw invalidPayload("Either file content, URL, or multipart part is required");
|
|
353
|
+
if (!options.name) throw invalidPayload("Asset name is required");
|
|
354
|
+
if (!("part" in options) && "isFinal" in options) throw invalidPayload("isFinal is only valid for multipart uploads");
|
|
355
|
+
if ("part" in options && options.part.partNumber && options.part.partNumber > 1) throw new CalmLensError(API_ERROR_CODES.USE_UPLOAD_PART, { message: "Use uploadPart for multipart upload parts after the first part" });
|
|
420
356
|
if ("content" in options) {
|
|
421
357
|
assertValidUploadMimeType(options.content.type);
|
|
422
358
|
if (!("plainText" in options.content)) {
|
|
423
359
|
const validation = validateAssetPayload(options.content);
|
|
424
|
-
if (!validation.ok) throw
|
|
360
|
+
if (!validation.ok) throw invalidPayload(validation.error);
|
|
425
361
|
}
|
|
426
362
|
}
|
|
427
363
|
if ("part" in options) {
|
|
428
|
-
var _options$part$sizeInB;
|
|
429
364
|
assertValidUploadMimeType(options.part.type);
|
|
430
|
-
const partSizeInBytes =
|
|
365
|
+
const partSizeInBytes = options.part.sizeInBytes ?? getAssetPayloadByteLength(options.part);
|
|
431
366
|
if (isImageMimeType(options.part.type) && partSizeInBytes !== void 0) {
|
|
432
|
-
if (partSizeInBytes > MAX_IMAGE_UPLOAD_BYTES) throw
|
|
433
|
-
if (options.part.isFinal && partSizeInBytes < MIN_MULTIPART_IMAGE_UPLOAD_BYTES) throw
|
|
367
|
+
if (partSizeInBytes > MAX_IMAGE_UPLOAD_BYTES) throw new CalmLensError(API_ERROR_CODES.MULTIPART_IMAGE_TOO_LARGE, { message: "Multipart image uploads cannot exceed 20MB." });
|
|
368
|
+
if (options.part.isFinal && partSizeInBytes < MIN_MULTIPART_IMAGE_UPLOAD_BYTES) throw new CalmLensError(API_ERROR_CODES.MULTIPART_IMAGE_TOO_SMALL, { message: "Multipart image uploads must total at least 10MB." });
|
|
434
369
|
}
|
|
435
370
|
const validation = validateAssetPayload(options.part, {
|
|
436
371
|
multipart: true,
|
|
437
372
|
isFinal: options.part.isFinal
|
|
438
373
|
});
|
|
439
|
-
if (!validation.ok) throw
|
|
374
|
+
if (!validation.ok) throw invalidPayload(validation.error);
|
|
440
375
|
}
|
|
441
376
|
return options;
|
|
442
377
|
};
|
|
443
|
-
const getEnsureLensCacheKey = (options) => {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
return _objectSpread2(_objectSpread2({}, init), {}, {
|
|
459
|
-
redirect: "error",
|
|
460
|
-
headers: Object.fromEntries(headers.entries())
|
|
461
|
-
});
|
|
462
|
-
};
|
|
463
|
-
const requestSignal = (options, signal) => {
|
|
464
|
-
var _options$timeoutMs;
|
|
465
|
-
const signals = [
|
|
466
|
-
options.signal,
|
|
467
|
-
signal,
|
|
468
|
-
AbortSignal.timeout((_options$timeoutMs = options.timeoutMs) !== null && _options$timeoutMs !== void 0 ? _options$timeoutMs : 12e4)
|
|
469
|
-
].filter((value) => value != null);
|
|
470
|
-
return AbortSignal.any(signals);
|
|
471
|
-
};
|
|
472
|
-
/** Copies a fully read body into a new Response, so later JSON parsing cannot hit the network. */
|
|
473
|
-
const toBufferedResponse = function() {
|
|
474
|
-
var _ref = _asyncToGenerator(function* (response) {
|
|
475
|
-
const body = response.status === 204 || response.status === 304 ? null : yield response.arrayBuffer();
|
|
476
|
-
const headers = new Headers(response.headers);
|
|
477
|
-
headers.delete("content-encoding");
|
|
478
|
-
headers.delete("content-length");
|
|
479
|
-
const buffered = new Response(body, {
|
|
480
|
-
status: response.status,
|
|
481
|
-
statusText: response.statusText,
|
|
482
|
-
headers
|
|
483
|
-
});
|
|
484
|
-
Object.defineProperty(buffered, "url", { value: response.url });
|
|
485
|
-
return buffered;
|
|
486
|
-
});
|
|
487
|
-
return function toBufferedResponse(_x) {
|
|
488
|
-
return _ref.apply(this, arguments);
|
|
489
|
-
};
|
|
490
|
-
}();
|
|
491
|
-
/**
|
|
492
|
-
* Wraps fetch with the client's deadline, cancellation and redirect policy. Every failure,
|
|
493
|
-
* including body reads, decompression and truncated bodies when `bufferBody` is set, becomes a
|
|
494
|
-
* `CalmLensError` (`sdk/transport-error`, `sdk/timeout` or `sdk/cancelled`) with the original
|
|
495
|
-
* error as its `cause`.
|
|
496
|
-
*/
|
|
497
|
-
const safeFetch = (fetchImpl, options, { bufferBody }) => function() {
|
|
498
|
-
var _ref2 = _asyncToGenerator(function* (input, init) {
|
|
499
|
-
var _init$signal;
|
|
500
|
-
const signal = requestSignal(options, (_init$signal = init === null || init === void 0 ? void 0 : init.signal) !== null && _init$signal !== void 0 ? _init$signal : input instanceof Request ? input.signal : void 0);
|
|
501
|
-
try {
|
|
502
|
-
signal.throwIfAborted();
|
|
503
|
-
const response = yield fetchImpl(input, _objectSpread2(_objectSpread2({}, init), {}, {
|
|
504
|
-
redirect: "error",
|
|
505
|
-
signal
|
|
506
|
-
}));
|
|
507
|
-
return bufferBody ? yield toBufferedResponse(response) : response;
|
|
508
|
-
} catch (cause) {
|
|
509
|
-
var _signal$reason;
|
|
510
|
-
throw calmLensError({ id: signal.aborted ? ((_signal$reason = signal.reason) === null || _signal$reason === void 0 ? void 0 : _signal$reason.name) === "TimeoutError" ? "sdk/timeout" : "sdk/cancelled" : "sdk/transport-error" }, {
|
|
511
|
-
message: signal.aborted ? "Request aborted" : "Request failed",
|
|
512
|
-
cause
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
});
|
|
516
|
-
return function(_x2, _x3) {
|
|
517
|
-
return _ref2.apply(this, arguments);
|
|
518
|
-
};
|
|
519
|
-
}();
|
|
520
|
-
const createApi = (options) => {
|
|
521
|
-
return new Api({
|
|
522
|
-
name: "Calm Lens",
|
|
523
|
-
baseUrl: options.baseUrl,
|
|
524
|
-
config: { queryHandling: { stringify: stringifyQuery } },
|
|
525
|
-
middleware: [
|
|
526
|
-
RequestCoalescingMiddleware(),
|
|
527
|
-
{ beforeSend: (context) => {
|
|
528
|
-
context.updateHeaders({ "X-Api-Key": options.apiKey });
|
|
529
|
-
} },
|
|
530
|
-
{ [RequestEvent.ERROR]: (context) => {
|
|
531
|
-
if (context.error) throw normalizeApiError(context.error);
|
|
532
|
-
} }
|
|
533
|
-
],
|
|
534
|
-
requestBackend: options.requestBackend ? Object.assign(Object.create(options.requestBackend), "fetch" in options.requestBackend && typeof options.requestBackend.fetch === "function" ? { fetch: safeFetch(options.requestBackend.fetch, options, { bufferBody: true }) } : {}) : new FetchRequestBackend(safeFetch(globalThis.fetch, options, { bufferBody: true }))
|
|
535
|
-
});
|
|
536
|
-
};
|
|
537
|
-
const isApiErrorPayload = (value) => {
|
|
538
|
-
return !!value && typeof value === "object" && typeof value.code === "string";
|
|
539
|
-
};
|
|
540
|
-
const normalizeApiError = (error) => {
|
|
541
|
-
let cause = error;
|
|
542
|
-
const seen = /* @__PURE__ */ new Set();
|
|
543
|
-
while (cause instanceof Error && !seen.has(cause)) {
|
|
544
|
-
seen.add(cause);
|
|
545
|
-
if (cause instanceof CalmLensError) return cause;
|
|
546
|
-
if (cause.name === "AbortError" || cause.name === "TimeoutError") return calmLensError({ id: cause.name === "TimeoutError" ? "sdk/timeout" : "sdk/cancelled" }, {
|
|
547
|
-
message: "Request aborted",
|
|
548
|
-
cause
|
|
549
|
-
});
|
|
550
|
-
cause = cause.cause;
|
|
551
|
-
}
|
|
552
|
-
const response = error instanceof Error ? getErrorResponse(error) : void 0;
|
|
553
|
-
if (error instanceof Error && isRequestError(error) && isApiErrorPayload(response === null || response === void 0 ? void 0 : response.data)) {
|
|
554
|
-
const { code, message, details, traceId } = response.data;
|
|
555
|
-
return calmLensError({
|
|
556
|
-
id: code,
|
|
557
|
-
defaultMessage: message
|
|
558
|
-
}, {
|
|
559
|
-
message,
|
|
560
|
-
status: response === null || response === void 0 ? void 0 : response.status,
|
|
561
|
-
cause: error,
|
|
562
|
-
details: _objectSpread2(_objectSpread2({}, details), {}, {
|
|
563
|
-
traceId,
|
|
564
|
-
network: true
|
|
565
|
-
})
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
if (error instanceof CalmLensError) return error;
|
|
569
|
-
return calmLensError({ id: response ? response.ok ? "sdk/invalid-response" : "sdk/http-error" : "sdk/transport-error" }, {
|
|
570
|
-
message: response ? `HTTP ${response.status}` : error.message,
|
|
571
|
-
status: response === null || response === void 0 ? void 0 : response.status,
|
|
572
|
-
cause: error
|
|
573
|
-
});
|
|
574
|
-
};
|
|
575
|
-
const createEndpoints = (api) => {
|
|
576
|
-
const getFolders = api.endpoint().paramsOf().responseOf().build({
|
|
577
|
-
id: "getFolders",
|
|
578
|
-
method: "get",
|
|
579
|
-
path: "/projects/:projectId/folders"
|
|
580
|
-
});
|
|
581
|
-
const getFolder = api.endpoint().paramsOf().responseOf().build({
|
|
582
|
-
id: "getFolder",
|
|
583
|
-
method: "get",
|
|
584
|
-
path: "/folders/:folderId"
|
|
585
|
-
});
|
|
586
|
-
const postFolder = api.endpoint().paramsOf().bodyOf().responseOf().build({
|
|
587
|
-
id: "postFolder",
|
|
588
|
-
method: "post",
|
|
589
|
-
path: "/projects/:projectId/folders"
|
|
590
|
-
});
|
|
591
|
-
const patchFolder = api.endpoint().paramsOf().bodyOf().responseOf().build({
|
|
592
|
-
id: "patchFolder",
|
|
593
|
-
method: "patch",
|
|
594
|
-
path: "/folders/:folderId"
|
|
595
|
-
});
|
|
596
|
-
const deleteFolder = api.endpoint().paramsOf().responseOf().build({
|
|
597
|
-
id: "deleteFolder",
|
|
598
|
-
method: "delete",
|
|
599
|
-
path: "/folders/:folderId"
|
|
600
|
-
});
|
|
601
|
-
const moveFolder = api.endpoint().paramsOf().bodyOf().responseOf().build({
|
|
602
|
-
id: "moveFolder",
|
|
603
|
-
method: "post",
|
|
604
|
-
path: "/folders/:folderId/move"
|
|
605
|
-
});
|
|
606
|
-
const getAssetsPage = api.endpoint().queryOf().paramsOf().responseOf().build({
|
|
607
|
-
id: "getAssetsPage",
|
|
608
|
-
method: "get",
|
|
609
|
-
path: "/projects/:projectId/assets/page"
|
|
610
|
-
});
|
|
611
|
-
const searchAssets = api.endpoint().paramsOf().queryOf().responseOf().build({
|
|
612
|
-
id: "searchAssets",
|
|
613
|
-
method: "get",
|
|
614
|
-
path: "/projects/:projectId/assets/search"
|
|
615
|
-
});
|
|
616
|
-
const getAsset = api.endpoint().paramsOf().responseOf().build({
|
|
617
|
-
id: "getAsset",
|
|
618
|
-
method: "get",
|
|
619
|
-
path: "/assets/:assetId"
|
|
620
|
-
});
|
|
621
|
-
const getAssetChildren = api.endpoint().paramsOf().queryOf().responseOf().build({
|
|
622
|
-
id: "getAssetChildren",
|
|
623
|
-
method: "get",
|
|
624
|
-
path: "/assets/:assetId/children"
|
|
625
|
-
});
|
|
626
|
-
const patchAsset = api.endpoint().paramsOf().bodyOf().responseOf().build({
|
|
627
|
-
id: "patchAsset",
|
|
628
|
-
method: "patch",
|
|
629
|
-
path: "/assets/:assetId"
|
|
630
|
-
});
|
|
631
|
-
const postUploadAsset = api.endpoint().bodyOf({ encoding: "multipart/form-data" }).paramsOf().queryOf().responseOf().build({
|
|
632
|
-
id: "postUploadAsset",
|
|
633
|
-
method: "post",
|
|
634
|
-
path: "/projects/:projectId/assets"
|
|
635
|
-
});
|
|
636
|
-
const postAssetSignedUrl = api.endpoint().bodyOf().paramsOf().responseOf().build({
|
|
637
|
-
id: "postAssetSignedUrl",
|
|
638
|
-
method: "post",
|
|
639
|
-
path: "/assets/:assetId/signed-url"
|
|
640
|
-
});
|
|
641
|
-
const postAssetPart = api.endpoint().bodyOf({ encoding: "multipart/form-data" }).paramsOf().queryOf().responseOf().build({
|
|
642
|
-
id: "postAssetPart",
|
|
643
|
-
method: "post",
|
|
644
|
-
path: "/assets/:assetId/part"
|
|
645
|
-
});
|
|
646
|
-
const moveAsset = api.endpoint().paramsOf().bodyOf().responseOf().build({
|
|
647
|
-
id: "moveAsset",
|
|
648
|
-
method: "post",
|
|
649
|
-
path: "/assets/:assetId/move"
|
|
650
|
-
});
|
|
651
|
-
const retryAsset = api.endpoint().paramsOf().responseOf().build({
|
|
652
|
-
id: "retryAsset",
|
|
653
|
-
method: "post",
|
|
654
|
-
path: "/assets/:assetId/retry"
|
|
655
|
-
});
|
|
656
|
-
const deleteAsset = api.endpoint().paramsOf().responseOf().build({
|
|
657
|
-
id: "deleteAsset",
|
|
658
|
-
method: "delete",
|
|
659
|
-
path: "/assets/:assetId"
|
|
660
|
-
});
|
|
661
|
-
const deleteAssetContent = api.endpoint().paramsOf().responseOf().build({
|
|
662
|
-
id: "deleteAssetContent",
|
|
663
|
-
method: "delete",
|
|
664
|
-
path: "/assets/:assetId/content"
|
|
665
|
-
});
|
|
666
|
-
const getEvents = api.endpoint().paramsOf().queryOf().responseOf().build({
|
|
667
|
-
id: "getEvents",
|
|
668
|
-
method: "get",
|
|
669
|
-
path: "/projects/:projectId/events"
|
|
670
|
-
});
|
|
671
|
-
const ensureLens = api.endpoint().bodyOf().paramsOf().responseOf().build({
|
|
672
|
-
id: "ensureLens",
|
|
673
|
-
method: "put",
|
|
674
|
-
path: "/lenses/:lensIdentifier"
|
|
675
|
-
});
|
|
676
|
-
const ensureLenses = api.endpoint().bodyOf().paramsOf().responseOf().build({
|
|
677
|
-
id: "ensureLenses",
|
|
678
|
-
method: "put",
|
|
679
|
-
path: "/projects/:projectId/lenses"
|
|
680
|
-
});
|
|
681
|
-
return encodePathParams({
|
|
682
|
-
getFolders,
|
|
683
|
-
getFolder,
|
|
684
|
-
postFolder,
|
|
685
|
-
patchFolder,
|
|
686
|
-
deleteFolder,
|
|
687
|
-
moveFolder,
|
|
688
|
-
getAssetsPage,
|
|
689
|
-
searchAssets,
|
|
690
|
-
getAsset,
|
|
691
|
-
getAssetChildren,
|
|
692
|
-
patchAsset,
|
|
693
|
-
postUploadAsset,
|
|
694
|
-
postAssetSignedUrl,
|
|
695
|
-
postAssetPart,
|
|
696
|
-
moveAsset,
|
|
697
|
-
retryAsset,
|
|
698
|
-
deleteAsset,
|
|
699
|
-
deleteAssetContent,
|
|
700
|
-
ensureLens,
|
|
701
|
-
ensureLenses,
|
|
702
|
-
getEvents
|
|
703
|
-
});
|
|
704
|
-
};
|
|
378
|
+
const getEnsureLensCacheKey = (options) => stringifyJsonCanonical({
|
|
379
|
+
identifier: options.identifier,
|
|
380
|
+
name: options.name,
|
|
381
|
+
description: options.description ?? null,
|
|
382
|
+
type: options.type,
|
|
383
|
+
active: options.active,
|
|
384
|
+
transforms: options.transforms ?? null,
|
|
385
|
+
version: options.version
|
|
386
|
+
});
|
|
387
|
+
const newIdempotencyKey = () => crypto.randomUUID();
|
|
388
|
+
/** Text content is small, so it keeps the API deadline; binary files may be arbitrarily large. */
|
|
389
|
+
const isTextPayload = (payload) => "plainText" in payload;
|
|
390
|
+
const requireId = (value, name) => encodePathSegment(value, name);
|
|
391
|
+
const isAsset = (data) => !!data && typeof data === "object" && typeof data.id === "string" && !!data.id && typeof data.status === "string";
|
|
392
|
+
const inspectSymbol = Symbol.for("nodejs.util.inspect.custom");
|
|
705
393
|
/**
|
|
706
|
-
*
|
|
707
|
-
*
|
|
394
|
+
* The CalmLens API client. Create one per project and reuse it; it holds no connections.
|
|
395
|
+
* Every method takes one options object and throws `CalmLensError` on failure.
|
|
708
396
|
*/
|
|
709
|
-
const encodePathParams = (endpoints) => {
|
|
710
|
-
for (const endpoint of Object.values(endpoints)) {
|
|
711
|
-
const submit = endpoint.submit.bind(endpoint);
|
|
712
|
-
endpoint.submit = (config) => submit(_objectSpread2(_objectSpread2({}, config), {}, { params: config.params && Object.fromEntries(Object.entries(config.params).map(([key, value]) => [key, encodePathSegment(value, key)])) }));
|
|
713
|
-
}
|
|
714
|
-
return endpoints;
|
|
715
|
-
};
|
|
716
397
|
var CalmLensClient = class CalmLensClient {
|
|
398
|
+
#apiKey;
|
|
399
|
+
#state;
|
|
400
|
+
#transport;
|
|
717
401
|
/**
|
|
718
402
|
* Verifies that a webhook delivery came from CalmLens: checks the `v1=` HMAC-SHA256 in
|
|
719
403
|
* `x-calmlens-signature` over `"<x-calmlens-timestamp>.<raw body>"` and rejects timestamps
|
|
720
404
|
* more than `toleranceSeconds` (default 300) from now, which stops replayed deliveries.
|
|
721
|
-
*
|
|
722
|
-
* @param options.payload - The raw request body, exactly as received (verify before parsing)
|
|
723
|
-
* @param options.signature - The 'x-calmlens-signature' header value
|
|
724
|
-
* @param options.timestamp - The 'x-calmlens-timestamp' header value
|
|
725
|
-
* @param options.secret - The connector's signing secret
|
|
726
|
-
* @param options.toleranceSeconds - Allowed clock distance in seconds (default 300)
|
|
727
|
-
* @param options.now - Override the current time (Date or epoch milliseconds), for tests
|
|
728
|
-
* @returns Promise<boolean> - True if the signature is valid and fresh, false otherwise
|
|
405
|
+
* Also exported as `verifyWebhookSignature`; `parseWebhookEvent` verifies and parses.
|
|
729
406
|
*
|
|
730
407
|
* @example
|
|
731
|
-
* ```
|
|
732
|
-
* // In your webhook handler (e.g. a Fetch API Request)
|
|
408
|
+
* ```ts
|
|
733
409
|
* const payload = await request.text();
|
|
734
410
|
* const isValid = await CalmLensClient.verifyWebhookSignature({
|
|
735
411
|
* payload,
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
* secret: WEBHOOK_SECRET,
|
|
412
|
+
* headers: request.headers,
|
|
413
|
+
* secret: process.env.CALMLENS_WEBHOOK_SECRET!,
|
|
739
414
|
* });
|
|
740
|
-
*
|
|
741
|
-
* if (!isValid) {
|
|
742
|
-
* return new Response('Unauthorized', { status: 401 });
|
|
743
|
-
* }
|
|
744
|
-
* const event = JSON.parse(payload); // { id, type, createdAt, projectId, data }
|
|
745
415
|
* ```
|
|
746
416
|
*/
|
|
747
|
-
static verifyWebhookSignature(options) {
|
|
748
|
-
return
|
|
749
|
-
return verifyWebhookSignature(options);
|
|
750
|
-
})();
|
|
417
|
+
static async verifyWebhookSignature(options) {
|
|
418
|
+
return verifyWebhookSignature(options);
|
|
751
419
|
}
|
|
752
|
-
constructor(options) {
|
|
753
|
-
var _this$options$baseUrl, _this$options$cdnBase;
|
|
754
|
-
this.options = options;
|
|
755
|
-
this.ensuredLenses = /* @__PURE__ */ new Map();
|
|
756
|
-
this.pendingEnsureLenses = /* @__PURE__ */ new Map();
|
|
757
|
-
this.multipartImageUploadSizes = /* @__PURE__ */ new Map();
|
|
758
|
-
this.multipartUploadQueues = /* @__PURE__ */ new Map();
|
|
420
|
+
constructor(options, state) {
|
|
759
421
|
validateClientOptions(options);
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
this
|
|
763
|
-
this
|
|
764
|
-
|
|
422
|
+
if (isBrowserRuntime() && !options.dangerouslyAllowBrowser) throw invalidConfig("CalmLensClient is running in a browser, where every visitor could read the API key. Call CalmLens from your server, or set dangerouslyAllowBrowser: true for trusted internal tools.");
|
|
423
|
+
const { apiKey, ...rest } = options;
|
|
424
|
+
this.#apiKey = apiKey;
|
|
425
|
+
this.#state = {
|
|
426
|
+
options: {
|
|
427
|
+
...rest,
|
|
428
|
+
baseUrl: (rest.baseUrl ?? `https://api.calmlens.com/v1`).replace(/\/+$/, ""),
|
|
429
|
+
cdnBaseUrl: (rest.cdnBaseUrl ?? `https://cdn.calmlens.com/v1`).replace(/\/+$/, "")
|
|
430
|
+
},
|
|
431
|
+
ensuredLenses: state?.ensuredLenses ?? /* @__PURE__ */ new Map(),
|
|
432
|
+
pendingEnsureLenses: state?.pendingEnsureLenses ?? /* @__PURE__ */ new Map()
|
|
433
|
+
};
|
|
434
|
+
const resolveFetch = () => {
|
|
435
|
+
const fetchImpl = this.#state.options.fetch ?? globalThis.fetch;
|
|
436
|
+
if (typeof fetchImpl !== "function") throw new CalmLensError(SDK_ERROR_CODES.NO_FETCH, { message: "No fetch implementation is available; pass one as the fetch option" });
|
|
437
|
+
return fetchImpl;
|
|
438
|
+
};
|
|
439
|
+
this.#transport = new CalmLensTransport({
|
|
440
|
+
apiKey,
|
|
441
|
+
fetch: ((input, init) => resolveFetch()(input, init)),
|
|
442
|
+
signal: rest.signal,
|
|
443
|
+
maxRetries: rest.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
444
|
+
maxResponseBytes: rest.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES
|
|
445
|
+
});
|
|
765
446
|
}
|
|
766
|
-
/**
|
|
447
|
+
/**
|
|
448
|
+
* A client with different cancellation, deadlines or retries for the calls made through it,
|
|
449
|
+
* sharing this client's credentials and lens cache. This client is unchanged.
|
|
450
|
+
*/
|
|
767
451
|
withRequestOptions(options) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
452
|
+
validateRequestOptions(options);
|
|
453
|
+
return new CalmLensClient({
|
|
454
|
+
...this.#state.options,
|
|
455
|
+
...options,
|
|
456
|
+
apiKey: this.#apiKey
|
|
457
|
+
}, this.#state);
|
|
772
458
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
if (!fetchImpl) throw calmLensError(SDK_ERRORS.NO_CDN_FETCH);
|
|
778
|
-
return safeFetch(fetchImpl, this.options, { bufferBody: true });
|
|
459
|
+
/** Keeps the API key out of `console.log`, `util.inspect` and `JSON.stringify`. */
|
|
460
|
+
[inspectSymbol]() {
|
|
461
|
+
const { projectId, baseUrl } = this.#state.options;
|
|
462
|
+
return `CalmLensClient { projectId: ${JSON.stringify(projectId)}, baseUrl: ${JSON.stringify(baseUrl)} }`;
|
|
779
463
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
return
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
setOptionalQueryValue(query, "description", options.description);
|
|
788
|
-
setOptionalQueryValue(query, "processingOptions", options.processingOptions);
|
|
789
|
-
setOptionalQueryValue(query, "visibility", options.visibility);
|
|
790
|
-
setOptionalQueryValue(query, "metadata", options.metadata);
|
|
791
|
-
setOptionalQueryValue(query, "externalId", options.externalId);
|
|
792
|
-
setOptionalQueryValue(query, "externalUrl", options.externalUrl);
|
|
793
|
-
setOptionalQueryValue(query, "folderId", options.folderId);
|
|
794
|
-
const isMultipart = "part" in options;
|
|
795
|
-
const payload = isMultipart ? options.part : options.content;
|
|
796
|
-
query.set("contentType", payload.type);
|
|
797
|
-
setOptionalQueryValue(query, "sizeInBytes", (_payload$sizeInBytes = payload.sizeInBytes) !== null && _payload$sizeInBytes !== void 0 ? _payload$sizeInBytes : getAssetPayloadByteLength(payload));
|
|
798
|
-
if (isMultipart) {
|
|
799
|
-
var _part$partNumber, _part$isFinal, _part$sizeInBytes;
|
|
800
|
-
const part = payload;
|
|
801
|
-
const multipartOptions = options;
|
|
802
|
-
query.set("partNumber", String((_part$partNumber = part.partNumber) !== null && _part$partNumber !== void 0 ? _part$partNumber : 1));
|
|
803
|
-
query.set("isFinal", String((_part$isFinal = part.isFinal) !== null && _part$isFinal !== void 0 ? _part$isFinal : false));
|
|
804
|
-
setOptionalQueryValue(query, "sizeInBytes", (_part$sizeInBytes = part.sizeInBytes) !== null && _part$sizeInBytes !== void 0 ? _part$sizeInBytes : multipartOptions.sizeInBytes);
|
|
805
|
-
}
|
|
806
|
-
const baseUrl = _this.options.baseUrl.replace(/\/$/, "");
|
|
807
|
-
const path = isMultipart ? "assets/multipart-upload" : "assets/upload";
|
|
808
|
-
const url = `${baseUrl}/projects/${encodeURIComponent(_this.options.projectId)}/${path}?${query}`;
|
|
809
|
-
const response = yield _this.getUploadFetch()(url, {
|
|
810
|
-
method: "POST",
|
|
811
|
-
headers: {
|
|
812
|
-
"Content-Type": payload.type,
|
|
813
|
-
"X-Api-Key": _this.options.apiKey,
|
|
814
|
-
"X-CalmLens-Upload": isMultipart ? "part" : "content"
|
|
815
|
-
},
|
|
816
|
-
body: toRawUploadBody(payload),
|
|
817
|
-
duplex: "half"
|
|
818
|
-
});
|
|
819
|
-
const asset = yield _this.parseRawUploadResponse(response);
|
|
820
|
-
if (isMultipart && asset.status === "uploading") {
|
|
821
|
-
_this.multipartUploadQueues.set(asset.id, queue);
|
|
822
|
-
if (isImageMimeType(payload.type)) {
|
|
823
|
-
var _asset$sizeInBytes;
|
|
824
|
-
_this.multipartImageUploadSizes.set(asset.id, (_asset$sizeInBytes = asset.sizeInBytes) !== null && _asset$sizeInBytes !== void 0 ? _asset$sizeInBytes : 0);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
return asset;
|
|
828
|
-
})();
|
|
464
|
+
toJSON() {
|
|
465
|
+
const { projectId, baseUrl, cdnBaseUrl } = this.#state.options;
|
|
466
|
+
return {
|
|
467
|
+
projectId,
|
|
468
|
+
baseUrl,
|
|
469
|
+
cdnBaseUrl
|
|
470
|
+
};
|
|
829
471
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
return _asyncToGenerator(function* () {
|
|
833
|
-
var _part$isFinal2, _part$sizeInBytes2;
|
|
834
|
-
const query = new URLSearchParams({
|
|
835
|
-
queue: String(queue),
|
|
836
|
-
partNumber: String(part.partNumber),
|
|
837
|
-
isFinal: String((_part$isFinal2 = part.isFinal) !== null && _part$isFinal2 !== void 0 ? _part$isFinal2 : false)
|
|
838
|
-
});
|
|
839
|
-
setOptionalQueryValue(query, "sizeInBytes", (_part$sizeInBytes2 = part.sizeInBytes) !== null && _part$sizeInBytes2 !== void 0 ? _part$sizeInBytes2 : getAssetPayloadByteLength(part));
|
|
840
|
-
const url = `${_this2.options.baseUrl.replace(/\/$/, "")}/assets/${encodePathSegment(assetId, "Asset ID")}/part?${query}`;
|
|
841
|
-
const response = yield _this2.getUploadFetch()(url, {
|
|
842
|
-
method: "POST",
|
|
843
|
-
headers: {
|
|
844
|
-
"X-Api-Key": _this2.options.apiKey,
|
|
845
|
-
"X-CalmLens-Upload": "part"
|
|
846
|
-
},
|
|
847
|
-
body: toRawUploadBody(part),
|
|
848
|
-
duplex: "half"
|
|
849
|
-
});
|
|
850
|
-
const asset = yield _this2.parseRawUploadResponse(response);
|
|
851
|
-
if (asset.status === "uploading") {
|
|
852
|
-
var _asset$sizeInBytes2;
|
|
853
|
-
_this2.multipartImageUploadSizes.set(asset.id, (_asset$sizeInBytes2 = asset.sizeInBytes) !== null && _asset$sizeInBytes2 !== void 0 ? _asset$sizeInBytes2 : 0);
|
|
854
|
-
} else {
|
|
855
|
-
_this2.multipartImageUploadSizes.delete(assetId);
|
|
856
|
-
_this2.multipartUploadQueues.delete(assetId);
|
|
857
|
-
}
|
|
858
|
-
return asset;
|
|
859
|
-
})();
|
|
472
|
+
get timeoutMs() {
|
|
473
|
+
return this.#state.options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
860
474
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
475
|
+
apiUrl(path, query) {
|
|
476
|
+
const search = query ? stringifyQuery(query) : "";
|
|
477
|
+
return `${this.#state.options.baseUrl}${path}${search ? `?${search}` : ""}`;
|
|
478
|
+
}
|
|
479
|
+
get projectPath() {
|
|
480
|
+
return `/projects/${encodeURIComponent(this.#state.options.projectId)}`;
|
|
481
|
+
}
|
|
482
|
+
request(method, path, { query, body } = {}) {
|
|
483
|
+
return this.#transport.json({
|
|
484
|
+
method,
|
|
485
|
+
url: this.apiUrl(path, query),
|
|
486
|
+
headers: body === void 0 ? void 0 : { "Content-Type": "application/json" },
|
|
487
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
488
|
+
timeoutMs: this.timeoutMs,
|
|
489
|
+
deadline: "body"
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
uploadRequest(url, payload, headers, idempotencyKey) {
|
|
493
|
+
const options = this.#state.options;
|
|
494
|
+
return this.#transport.json({
|
|
495
|
+
method: "POST",
|
|
496
|
+
url,
|
|
497
|
+
headers,
|
|
498
|
+
body: toRawUploadBody(payload),
|
|
499
|
+
idempotencyKey,
|
|
500
|
+
timeoutMs: isTextPayload(payload) ? this.timeoutMs : options.uploadTimeoutMs,
|
|
501
|
+
deadline: "body",
|
|
502
|
+
fetch: options.uploadFetch
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async submitRawAssetUpload(options, queue) {
|
|
506
|
+
const query = new URLSearchParams();
|
|
507
|
+
query.set("name", options.name);
|
|
508
|
+
query.set("queue", String(queue));
|
|
509
|
+
setOptionalQueryValue(query, "description", options.description);
|
|
510
|
+
setOptionalQueryValue(query, "processingOptions", options.processingOptions);
|
|
511
|
+
setOptionalQueryValue(query, "visibility", options.visibility);
|
|
512
|
+
setOptionalQueryValue(query, "metadata", options.metadata);
|
|
513
|
+
setOptionalQueryValue(query, "externalId", options.externalId);
|
|
514
|
+
setOptionalQueryValue(query, "externalUrl", options.externalUrl);
|
|
515
|
+
setOptionalQueryValue(query, "folderId", options.folderId);
|
|
516
|
+
const isMultipart = "part" in options;
|
|
517
|
+
const payload = isMultipart ? options.part : options.content;
|
|
518
|
+
query.set("contentType", payload.type);
|
|
519
|
+
setOptionalQueryValue(query, "sizeInBytes", payload.sizeInBytes ?? getAssetPayloadByteLength(payload));
|
|
520
|
+
if (isMultipart) {
|
|
521
|
+
const part = payload;
|
|
522
|
+
const multipartOptions = options;
|
|
523
|
+
query.set("partNumber", String(part.partNumber ?? 1));
|
|
524
|
+
query.set("isFinal", String(part.isFinal ?? false));
|
|
525
|
+
setOptionalQueryValue(query, "sizeInBytes", part.sizeInBytes ?? multipartOptions.sizeInBytes);
|
|
526
|
+
setOptionalQueryValue(query, "totalSizeInBytes", multipartOptions.totalSizeInBytes);
|
|
527
|
+
}
|
|
528
|
+
const path = isMultipart ? "assets/multipart-upload" : "assets/upload";
|
|
529
|
+
const data = await this.uploadRequest(this.apiUrl(`${this.projectPath}/${path}?${query}`), payload, {
|
|
530
|
+
"Content-Type": payload.type,
|
|
531
|
+
"X-CalmLens-Upload": isMultipart ? "part" : "content"
|
|
532
|
+
}, options.idempotencyKey ?? newIdempotencyKey());
|
|
533
|
+
return this.expectAsset(data);
|
|
534
|
+
}
|
|
535
|
+
expectAsset(data) {
|
|
536
|
+
if (!isAsset(data)) throw new CalmLensError(SDK_ERROR_CODES.INVALID_RESPONSE, {
|
|
537
|
+
message: "Expected an asset response",
|
|
538
|
+
status: 200
|
|
539
|
+
});
|
|
540
|
+
return data;
|
|
541
|
+
}
|
|
542
|
+
async submitAsset(options, queue) {
|
|
543
|
+
const body = prepareSubmitAssetOptions(options);
|
|
544
|
+
if ("content" in body || "part" in body) return this.submitRawAssetUpload(body, queue);
|
|
545
|
+
const { idempotencyKey, ...fields } = body;
|
|
546
|
+
const data = await this.#transport.json({
|
|
547
|
+
method: "POST",
|
|
548
|
+
url: this.apiUrl(`${this.projectPath}/assets`, { queue }),
|
|
549
|
+
headers: { "Content-Type": "application/json" },
|
|
550
|
+
body: JSON.stringify(fields),
|
|
551
|
+
idempotencyKey: idempotencyKey ?? newIdempotencyKey(),
|
|
552
|
+
timeoutMs: this.timeoutMs,
|
|
553
|
+
deadline: "body"
|
|
554
|
+
});
|
|
555
|
+
return this.expectAsset(data);
|
|
902
556
|
}
|
|
903
557
|
get cdn() {
|
|
904
|
-
var _this3 = this;
|
|
905
558
|
return {
|
|
906
559
|
/**
|
|
907
|
-
*
|
|
908
|
-
*
|
|
560
|
+
* Builds a CDN URL for asset content or a preview, without a request. Private assets
|
|
561
|
+
* need a signed URL (`assets.createSignedUrl`) or an authenticated request.
|
|
909
562
|
*/
|
|
910
|
-
getAssetUrl: (
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
if (!assetIdentifier) throw calmLensError({ id: "upload/invalid-payload" }, { message: "Asset identifier is required" });
|
|
914
|
-
const resource = (_options$resource = options.resource) !== null && _options$resource !== void 0 ? _options$resource : "content";
|
|
915
|
-
const url = `${this.options.cdnBaseUrl.replace(/\/$/, "")}/assets/${encodeURIComponent(this.options.projectId)}/${encodePathSegment(assetIdentifier, "Asset identifier")}/${resource}`;
|
|
563
|
+
getAssetUrl: ({ assetIdentifier, ...options }) => {
|
|
564
|
+
const resource = options.resource ?? "content";
|
|
565
|
+
const url = `${this.#state.options.cdnBaseUrl}/assets/${encodeURIComponent(this.#state.options.projectId)}/${requireId(assetIdentifier, "Asset identifier")}/${resource}`;
|
|
916
566
|
return appendCdnSelector(url, options);
|
|
917
567
|
},
|
|
918
568
|
/**
|
|
919
|
-
*
|
|
920
|
-
*
|
|
569
|
+
* Fetches asset content or a preview from the CDN with the client's API key, and returns
|
|
570
|
+
* the `Response` as soon as headers arrive, so the body streams. Non-2xx responses are
|
|
571
|
+
* returned, not thrown; `timeoutMs` covers only the wait for headers.
|
|
921
572
|
*/
|
|
922
|
-
serve:
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const fetchImpl = (_this$options$cdnFetc = _this3.options.cdnFetch) !== null && _this$options$cdnFetc !== void 0 ? _this$options$cdnFetc : globalThis.fetch;
|
|
927
|
-
if (!fetchImpl) throw calmLensError(SDK_ERRORS.NO_CDN_FETCH);
|
|
928
|
-
return safeFetch(fetchImpl, _this3.options, { bufferBody: false })(_this3.cdn.getAssetUrl(_objectSpread2({ assetIdentifier }, urlOptions)), withApiKeyHeader(init, _this3.options.apiKey));
|
|
573
|
+
serve: async ({ assetIdentifier, init, ...urlOptions }) => {
|
|
574
|
+
const url = this.cdn.getAssetUrl({
|
|
575
|
+
assetIdentifier,
|
|
576
|
+
...urlOptions
|
|
929
577
|
});
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
578
|
+
const method = init?.method?.toUpperCase() ?? "GET";
|
|
579
|
+
const { response } = await this.#transport.send({
|
|
580
|
+
method,
|
|
581
|
+
url,
|
|
582
|
+
init,
|
|
583
|
+
timeoutMs: this.timeoutMs,
|
|
584
|
+
deadline: "headers",
|
|
585
|
+
fetch: this.#state.options.cdnFetch
|
|
586
|
+
}, { allowHttpErrors: true });
|
|
587
|
+
return response;
|
|
588
|
+
}
|
|
934
589
|
};
|
|
935
590
|
}
|
|
936
591
|
get text() {
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
return _ref7.apply(this, arguments);
|
|
944
|
-
};
|
|
945
|
-
}() };
|
|
592
|
+
return {
|
|
593
|
+
/**
|
|
594
|
+
* Moderates text and waits for the decision. Sends `text` as `text/plain` content named
|
|
595
|
+
* `text` unless you pass `name`.
|
|
596
|
+
*/
|
|
597
|
+
moderate: async (options) => this.assets.moderate(toTextAssetOptions(options)) };
|
|
946
598
|
}
|
|
947
599
|
get assets() {
|
|
948
|
-
var _this5 = this;
|
|
949
600
|
return {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
return function search(_x6) {
|
|
958
|
-
return _ref8.apply(this, arguments);
|
|
959
|
-
};
|
|
960
|
-
}(),
|
|
601
|
+
/** Searches accessible assets by keyword or a natural-language description. */
|
|
602
|
+
search: async (options) => this.request("GET", `${this.projectPath}/assets/search`, { query: options }),
|
|
603
|
+
/** Lists one page of the project's assets, with optional filter and ordering. */
|
|
604
|
+
list: async (options = {}) => this.request("GET", `${this.projectPath}/assets/page`, { query: {
|
|
605
|
+
...options,
|
|
606
|
+
pageIndex: options.pageIndex ?? 0
|
|
607
|
+
} }),
|
|
961
608
|
/**
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
* @returns Promise with paginated assets
|
|
609
|
+
* Yields every asset matching `options`, fetching pages as needed. Assets created or
|
|
610
|
+
* deleted while iterating can shift pages, so an asset may be skipped or repeated.
|
|
965
611
|
*/
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
});
|
|
988
|
-
return function retrieve(_x7) {
|
|
989
|
-
return _ref10.apply(this, arguments);
|
|
990
|
-
};
|
|
991
|
-
}(),
|
|
992
|
-
update: function() {
|
|
993
|
-
var _ref12 = _asyncToGenerator(function* (_ref11) {
|
|
994
|
-
let { assetId } = _ref11, body = _objectWithoutProperties(_ref11, _excluded5);
|
|
995
|
-
if (!assetId) throw calmLensError({ id: "upload/invalid-payload" }, { message: "Asset ID is required" });
|
|
996
|
-
return (yield _this5.endpoints.patchAsset.submit({
|
|
997
|
-
params: { assetId },
|
|
998
|
-
body
|
|
999
|
-
})).data;
|
|
1000
|
-
});
|
|
1001
|
-
return function update(_x8) {
|
|
1002
|
-
return _ref12.apply(this, arguments);
|
|
1003
|
-
};
|
|
1004
|
-
}(),
|
|
612
|
+
iterate: (options = {}) => this.iteratePages((pageIndex) => this.assets.list({
|
|
613
|
+
...options,
|
|
614
|
+
pageIndex
|
|
615
|
+
})),
|
|
616
|
+
/** Retrieves an asset with its status and moderation report. */
|
|
617
|
+
retrieve: async ({ assetId }) => this.request("GET", `/assets/${requireId(assetId, "Asset ID")}`),
|
|
618
|
+
/** Updates an asset's editable fields. */
|
|
619
|
+
update: async ({ assetId, ...body }) => this.request("PATCH", `/assets/${requireId(assetId, "Asset ID")}`, { body }),
|
|
620
|
+
/** Lists one page of the assets extracted from a website or multimedia document. */
|
|
621
|
+
listChildren: async ({ assetId, ...options } = {}) => this.request("GET", `/assets/${requireId(assetId, "Asset ID")}/children`, { query: {
|
|
622
|
+
...options,
|
|
623
|
+
pageIndex: options.pageIndex ?? 0
|
|
624
|
+
} }),
|
|
625
|
+
/** Yields every child of an asset, fetching pages as needed. */
|
|
626
|
+
iterateChildren: (options) => {
|
|
627
|
+
requireId(options?.assetId, "Asset ID");
|
|
628
|
+
return this.iteratePages((pageIndex) => this.assets.listChildren({
|
|
629
|
+
...options,
|
|
630
|
+
pageIndex
|
|
631
|
+
}));
|
|
632
|
+
},
|
|
1005
633
|
/**
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1009
|
-
* @returns Promise with paginated child assets
|
|
634
|
+
* Uploads an asset and queues it for moderation, returning immediately with status
|
|
635
|
+
* `pending` (or `uploading` for the first multipart part). Retried safely with an
|
|
636
|
+
* `Idempotency-Key`.
|
|
1010
637
|
*/
|
|
1011
|
-
|
|
1012
|
-
var _ref14 = _asyncToGenerator(function* (_ref13 = {}) {
|
|
1013
|
-
var _options$pageIndex2;
|
|
1014
|
-
let { assetId } = _ref13, options = _objectWithoutProperties(_ref13, _excluded6);
|
|
1015
|
-
if (!assetId) throw calmLensError({ id: "upload/invalid-payload" }, { message: "Asset ID is required" });
|
|
1016
|
-
return (yield _this5.endpoints.getAssetChildren.submit({
|
|
1017
|
-
params: { assetId },
|
|
1018
|
-
query: _objectSpread2(_objectSpread2({}, options), {}, { pageIndex: (_options$pageIndex2 = options.pageIndex) !== null && _options$pageIndex2 !== void 0 ? _options$pageIndex2 : 0 })
|
|
1019
|
-
})).data;
|
|
1020
|
-
});
|
|
1021
|
-
return function listChildren() {
|
|
1022
|
-
return _ref14.apply(this, arguments);
|
|
1023
|
-
};
|
|
1024
|
-
}(),
|
|
638
|
+
create: async (options) => this.submitAsset(options, true),
|
|
1025
639
|
/**
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
* @param options - Asset submission options
|
|
1029
|
-
* @returns Promise with created asset
|
|
640
|
+
* Uploads an asset and waits for its moderation decision. Audio and video still finish
|
|
641
|
+
* in the background. Retried safely with an `Idempotency-Key`.
|
|
1030
642
|
*/
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
return (yield _this5.endpoints.postUploadAsset.submit({
|
|
1036
|
-
body: normalizeAssetOptionsForRequest(body),
|
|
1037
|
-
params: { projectId: _this5.options.projectId },
|
|
1038
|
-
query: { queue: true }
|
|
1039
|
-
})).data;
|
|
1040
|
-
});
|
|
1041
|
-
return function create(_x9) {
|
|
1042
|
-
return _ref15.apply(this, arguments);
|
|
1043
|
-
};
|
|
1044
|
-
}(),
|
|
643
|
+
moderate: async (options) => {
|
|
644
|
+
if (options && "content" in options && "plainText" in options.content && (typeof options.content.plainText !== "string" || !options.content.plainText.trim())) throw invalidPayload("Text to moderate must be a non-empty string");
|
|
645
|
+
return this.submitAsset(options, false);
|
|
646
|
+
},
|
|
1045
647
|
/**
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1048
|
-
* This may take longer but ensures the asset is fully processed.
|
|
1049
|
-
* @param options - Asset submission options
|
|
1050
|
-
* @returns Promise with created and analyzed asset
|
|
648
|
+
* Uploads the next part of a multipart upload. The upload's queue mode, chosen when it
|
|
649
|
+
* started, applies when the final part arrives; pass `queue` to override it.
|
|
1051
650
|
*/
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
body: normalizeAssetOptionsForRequest(body),
|
|
1059
|
-
params: { projectId: _this5.options.projectId },
|
|
1060
|
-
query: { queue: false }
|
|
1061
|
-
})).data;
|
|
651
|
+
uploadPart: async ({ assetId, part, queue, idempotencyKey }) => {
|
|
652
|
+
const encodedAssetId = requireId(assetId, "Asset ID");
|
|
653
|
+
if (!part) throw invalidArgument("Part options are required");
|
|
654
|
+
const validation = validateAssetPayload(part, {
|
|
655
|
+
multipart: true,
|
|
656
|
+
isFinal: part.isFinal
|
|
1062
657
|
});
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
/**
|
|
1068
|
-
* Upload a part for a multipart upload
|
|
1069
|
-
* @param assetId - The asset ID
|
|
1070
|
-
* @param part - The part to upload
|
|
1071
|
-
* @returns Promise with asset data
|
|
1072
|
-
*/
|
|
1073
|
-
uploadPart: function() {
|
|
1074
|
-
var _ref17 = _asyncToGenerator(function* ({ assetId, part }) {
|
|
1075
|
-
var _part$sizeInBytes3, _this$multipartUpload;
|
|
1076
|
-
if (!assetId) throw calmLensError({ id: "upload/invalid-payload" }, { message: "Asset ID is required" });
|
|
1077
|
-
if (!part) throw calmLensError({ id: "upload/invalid-payload" }, { message: "Part options are required" });
|
|
1078
|
-
const validation = validateAssetPayload(part, {
|
|
1079
|
-
multipart: true,
|
|
1080
|
-
isFinal: part.isFinal
|
|
1081
|
-
});
|
|
1082
|
-
if (!validation.ok) throw calmLensError(PUBLIC_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: validation.error });
|
|
1083
|
-
const existingImageSizeInBytes = _this5.multipartImageUploadSizes.get(assetId);
|
|
1084
|
-
const partSizeInBytes = (_part$sizeInBytes3 = part.sizeInBytes) !== null && _part$sizeInBytes3 !== void 0 ? _part$sizeInBytes3 : getAssetPayloadByteLength(part);
|
|
1085
|
-
if (existingImageSizeInBytes !== void 0 && partSizeInBytes !== void 0) {
|
|
1086
|
-
const totalSizeInBytes = existingImageSizeInBytes + partSizeInBytes;
|
|
1087
|
-
if (totalSizeInBytes > MAX_IMAGE_UPLOAD_BYTES) throw calmLensError(PUBLIC_ERROR_CODES.MULTIPART_IMAGE_TOO_LARGE);
|
|
1088
|
-
if (part.isFinal && totalSizeInBytes < MIN_MULTIPART_IMAGE_UPLOAD_BYTES) throw calmLensError(PUBLIC_ERROR_CODES.MULTIPART_IMAGE_TOO_SMALL);
|
|
1089
|
-
}
|
|
1090
|
-
return yield _this5.submitRawAssetPart(assetId, part, (_this$multipartUpload = _this5.multipartUploadQueues.get(assetId)) !== null && _this$multipartUpload !== void 0 ? _this$multipartUpload : false);
|
|
658
|
+
if (!validation.ok) throw invalidPayload(validation.error);
|
|
659
|
+
const query = new URLSearchParams({
|
|
660
|
+
partNumber: String(part.partNumber),
|
|
661
|
+
isFinal: String(part.isFinal ?? false)
|
|
1091
662
|
});
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
};
|
|
1095
|
-
|
|
663
|
+
if (queue !== void 0) query.set("queue", String(queue));
|
|
664
|
+
setOptionalQueryValue(query, "sizeInBytes", part.sizeInBytes ?? getAssetPayloadByteLength(part));
|
|
665
|
+
const data = await this.uploadRequest(this.apiUrl(`/assets/${encodedAssetId}/part?${query}`), part, { "X-CalmLens-Upload": "part" }, idempotencyKey ?? newIdempotencyKey());
|
|
666
|
+
return this.expectAsset(data);
|
|
667
|
+
},
|
|
1096
668
|
/**
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
* auth cookies or API keys to the CDN on every media request.
|
|
669
|
+
* Creates a short-lived signed CDN URL, so browsers can load private content without
|
|
670
|
+
* an API key.
|
|
1100
671
|
*/
|
|
1101
|
-
createSignedUrl:
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
},
|
|
1114
|
-
params: { assetId }
|
|
1115
|
-
})).data;
|
|
1116
|
-
});
|
|
1117
|
-
return function createSignedUrl(_x12) {
|
|
1118
|
-
return _ref19.apply(this, arguments);
|
|
1119
|
-
};
|
|
1120
|
-
}(),
|
|
1121
|
-
move: function() {
|
|
1122
|
-
var _ref20 = _asyncToGenerator(function* ({ assetId, folderId }) {
|
|
1123
|
-
if (!assetId) throw calmLensError({ id: "upload/invalid-payload" }, { message: "Asset ID is required" });
|
|
1124
|
-
return (yield _this5.endpoints.moveAsset.submit({
|
|
1125
|
-
params: { assetId },
|
|
1126
|
-
body: { folderId }
|
|
1127
|
-
})).data;
|
|
1128
|
-
});
|
|
1129
|
-
return function move(_x13) {
|
|
1130
|
-
return _ref20.apply(this, arguments);
|
|
1131
|
-
};
|
|
1132
|
-
}(),
|
|
672
|
+
createSignedUrl: async ({ assetId, ...options }) => {
|
|
673
|
+
const encodedAssetId = requireId(assetId, "Asset ID");
|
|
674
|
+
if (options.lens && options.transform) throw invalidArgument("Specify either a lens or a transform, not both");
|
|
675
|
+
return this.request("POST", `/assets/${encodedAssetId}/signed-url`, { body: {
|
|
676
|
+
resource: options.resource ?? "content",
|
|
677
|
+
expiresInSeconds: options.expiresInSeconds ?? 900,
|
|
678
|
+
lens: options.lens,
|
|
679
|
+
transform: options.transform
|
|
680
|
+
} });
|
|
681
|
+
},
|
|
682
|
+
/** Moves an asset into a folder, or to the project root with `folderId: null`. */
|
|
683
|
+
move: async ({ assetId, folderId }) => this.request("POST", `/assets/${requireId(assetId, "Asset ID")}/move`, { body: { folderId } }),
|
|
1133
684
|
/**
|
|
1134
685
|
* Processes an asset that failed (status `error`) again. It returns to `pending` and is
|
|
1135
686
|
* moderated like a new upload; its content must still be stored.
|
|
1136
687
|
*/
|
|
1137
|
-
retry:
|
|
1138
|
-
var _ref21 = _asyncToGenerator(function* ({ assetId }) {
|
|
1139
|
-
if (!assetId) throw calmLensError({ id: "upload/invalid-payload" }, { message: "Asset ID is required" });
|
|
1140
|
-
return (yield _this5.endpoints.retryAsset.submit({ params: { assetId } })).data;
|
|
1141
|
-
});
|
|
1142
|
-
return function retry(_x14) {
|
|
1143
|
-
return _ref21.apply(this, arguments);
|
|
1144
|
-
};
|
|
1145
|
-
}(),
|
|
688
|
+
retry: async ({ assetId }) => this.request("POST", `/assets/${requireId(assetId, "Asset ID")}/retry`),
|
|
1146
689
|
/**
|
|
1147
690
|
* Removes an asset's stored content (source, preview and retained extracted text) while
|
|
1148
691
|
* keeping the asset record, metadata and moderation report. The returned asset has
|
|
1149
692
|
* `storageDeletedAt` set; the CDN no longer serves it.
|
|
1150
693
|
*/
|
|
1151
|
-
deleteContent:
|
|
1152
|
-
var _ref22 = _asyncToGenerator(function* ({ assetId }) {
|
|
1153
|
-
if (!assetId) throw calmLensError({ id: "upload/invalid-payload" }, { message: "Asset ID is required" });
|
|
1154
|
-
return (yield _this5.endpoints.deleteAssetContent.submit({ params: { assetId } })).data;
|
|
1155
|
-
});
|
|
1156
|
-
return function deleteContent(_x15) {
|
|
1157
|
-
return _ref22.apply(this, arguments);
|
|
1158
|
-
};
|
|
1159
|
-
}(),
|
|
694
|
+
deleteContent: async ({ assetId }) => this.request("DELETE", `/assets/${requireId(assetId, "Asset ID")}/content`),
|
|
1160
695
|
/** Deletes an asset record together with its stored content. */
|
|
1161
|
-
delete:
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
yield _this5.endpoints.deleteAsset.submit({ params: { assetId } });
|
|
1165
|
-
});
|
|
1166
|
-
return function _delete(_x16) {
|
|
1167
|
-
return _ref23.apply(this, arguments);
|
|
1168
|
-
};
|
|
1169
|
-
}()
|
|
696
|
+
delete: async ({ assetId }) => {
|
|
697
|
+
await this.request("DELETE", `/assets/${requireId(assetId, "Asset ID")}`);
|
|
698
|
+
}
|
|
1170
699
|
};
|
|
1171
700
|
}
|
|
1172
701
|
get events() {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
702
|
+
return {
|
|
703
|
+
/**
|
|
704
|
+
* Lists project events after a cursor, oldest first. Call without `after` to get a cursor
|
|
705
|
+
* for "now", then poll with the returned `cursor`. Webhook deliveries carry the same events
|
|
706
|
+
* reshaped as `{ id, type, createdAt, projectId, data }`.
|
|
707
|
+
*/
|
|
708
|
+
list: async (options = {}) => this.request("GET", `${this.projectPath}/events`, { query: { ...options } }),
|
|
709
|
+
/**
|
|
710
|
+
* Yields pages of events after `after` until no more are waiting. Save each page's
|
|
711
|
+
* `cursor` to resume later. Without `after`, yields one empty page whose cursor is "now".
|
|
712
|
+
*/
|
|
713
|
+
pages: (options = {}) => this.iterateEventPages(options)
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
async *iterateEventPages(options) {
|
|
717
|
+
let after = options.after;
|
|
718
|
+
for (;;) {
|
|
719
|
+
const page = await this.events.list({
|
|
720
|
+
...options,
|
|
721
|
+
after
|
|
1186
722
|
});
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
}
|
|
723
|
+
yield page;
|
|
724
|
+
if (!page.hasMore || page.cursor === after) return;
|
|
725
|
+
after = page.cursor;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async *iteratePages(listPage) {
|
|
729
|
+
for (let pageIndex = 0;; pageIndex++) {
|
|
730
|
+
const page = await listPage(pageIndex);
|
|
731
|
+
yield* page.items;
|
|
732
|
+
if (!page.hasMore || page.items.length === 0) return;
|
|
733
|
+
}
|
|
1191
734
|
}
|
|
1192
735
|
get folders() {
|
|
1193
|
-
var _this7 = this;
|
|
1194
736
|
return {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
});
|
|
1208
|
-
return function retrieve(_x17) {
|
|
1209
|
-
return _ref26.apply(this, arguments);
|
|
1210
|
-
};
|
|
1211
|
-
}(),
|
|
1212
|
-
create: function() {
|
|
1213
|
-
var _ref27 = _asyncToGenerator(function* (options) {
|
|
1214
|
-
return (yield _this7.endpoints.postFolder.submit({
|
|
1215
|
-
params: { projectId: _this7.options.projectId },
|
|
1216
|
-
body: options
|
|
1217
|
-
})).data;
|
|
1218
|
-
});
|
|
1219
|
-
return function create(_x18) {
|
|
1220
|
-
return _ref27.apply(this, arguments);
|
|
1221
|
-
};
|
|
1222
|
-
}(),
|
|
1223
|
-
update: function() {
|
|
1224
|
-
var _ref29 = _asyncToGenerator(function* (_ref28) {
|
|
1225
|
-
let { folderId } = _ref28, body = _objectWithoutProperties(_ref28, _excluded8);
|
|
1226
|
-
if (!folderId) throw calmLensError({ id: "upload/invalid-payload" }, { message: "Folder ID is required" });
|
|
1227
|
-
return (yield _this7.endpoints.patchFolder.submit({
|
|
1228
|
-
params: { folderId },
|
|
1229
|
-
body
|
|
1230
|
-
})).data;
|
|
1231
|
-
});
|
|
1232
|
-
return function update(_x19) {
|
|
1233
|
-
return _ref29.apply(this, arguments);
|
|
1234
|
-
};
|
|
1235
|
-
}(),
|
|
1236
|
-
delete: function() {
|
|
1237
|
-
var _ref30 = _asyncToGenerator(function* ({ folderId }) {
|
|
1238
|
-
if (!folderId) throw calmLensError({ id: "upload/invalid-payload" }, { message: "Folder ID is required" });
|
|
1239
|
-
yield _this7.endpoints.deleteFolder.submit({ params: { folderId } });
|
|
1240
|
-
});
|
|
1241
|
-
return function _delete(_x20) {
|
|
1242
|
-
return _ref30.apply(this, arguments);
|
|
1243
|
-
};
|
|
1244
|
-
}(),
|
|
1245
|
-
move: function() {
|
|
1246
|
-
var _ref31 = _asyncToGenerator(function* ({ folderId, parentId }) {
|
|
1247
|
-
if (!folderId) throw calmLensError({ id: "upload/invalid-payload" }, { message: "Folder ID is required" });
|
|
1248
|
-
return (yield _this7.endpoints.moveFolder.submit({
|
|
1249
|
-
params: { folderId },
|
|
1250
|
-
body: { parentId }
|
|
1251
|
-
})).data;
|
|
1252
|
-
});
|
|
1253
|
-
return function move(_x21) {
|
|
1254
|
-
return _ref31.apply(this, arguments);
|
|
1255
|
-
};
|
|
1256
|
-
}()
|
|
737
|
+
/** Lists every folder in the project. */
|
|
738
|
+
list: async () => (await this.request("GET", `${this.projectPath}/folders`)).folders,
|
|
739
|
+
retrieve: async ({ folderId }) => this.request("GET", `/folders/${requireId(folderId, "Folder ID")}`),
|
|
740
|
+
/** Creates a folder at the project root or inside `parentId`. */
|
|
741
|
+
create: async (options) => this.request("POST", `${this.projectPath}/folders`, { body: options }),
|
|
742
|
+
update: async ({ folderId, ...body }) => this.request("PATCH", `/folders/${requireId(folderId, "Folder ID")}`, { body }),
|
|
743
|
+
/** Deletes an empty folder; throws `folder/not-empty` otherwise. */
|
|
744
|
+
delete: async ({ folderId }) => {
|
|
745
|
+
await this.request("DELETE", `/folders/${requireId(folderId, "Folder ID")}`);
|
|
746
|
+
},
|
|
747
|
+
/** Moves a folder under `parentId`, or to the project root with `parentId: null`. */
|
|
748
|
+
move: async ({ folderId, parentId }) => this.request("POST", `/folders/${requireId(folderId, "Folder ID")}/move`, { body: { parentId } })
|
|
1257
749
|
};
|
|
1258
750
|
}
|
|
1259
751
|
/**
|
|
1260
752
|
* Create a named lens at any positive version, or update it with a greater version.
|
|
1261
753
|
* Supplying the same definition and version is idempotent.
|
|
1262
754
|
*/
|
|
1263
|
-
ensureLens(options) {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
const [lens] = yield _this8.ensureLenses({ lenses: [options] });
|
|
1268
|
-
return lens;
|
|
1269
|
-
})();
|
|
755
|
+
async ensureLens(options) {
|
|
756
|
+
if (!options?.identifier) throw invalidArgument("Lens identifier is required");
|
|
757
|
+
const [lens] = await this.ensureLenses({ lenses: [options] });
|
|
758
|
+
return lens;
|
|
1270
759
|
}
|
|
1271
760
|
/**
|
|
1272
761
|
* Create or update several named lenses in one API request.
|
|
1273
762
|
* Each lens follows the same versioning and idempotency rules as ensureLens().
|
|
1274
763
|
*/
|
|
1275
|
-
ensureLenses({ lenses }) {
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
missingOptions.push(option);
|
|
1287
|
-
}
|
|
764
|
+
async ensureLenses({ lenses }) {
|
|
765
|
+
if (!Array.isArray(lenses) || lenses.length === 0) throw invalidArgument("At least one lens is required");
|
|
766
|
+
if (lenses.some((option) => !option?.identifier)) throw invalidArgument("Lens identifier is required");
|
|
767
|
+
const { ensuredLenses, pendingEnsureLenses } = this.#state;
|
|
768
|
+
const missingOptions = [];
|
|
769
|
+
const missingKeys = /* @__PURE__ */ new Set();
|
|
770
|
+
for (const option of lenses) {
|
|
771
|
+
const cacheKey = getEnsureLensCacheKey(option);
|
|
772
|
+
if (!ensuredLenses.has(cacheKey) && !missingKeys.has(cacheKey)) {
|
|
773
|
+
missingKeys.add(cacheKey);
|
|
774
|
+
missingOptions.push(option);
|
|
1288
775
|
}
|
|
1289
|
-
|
|
776
|
+
}
|
|
777
|
+
if (missingOptions.length > 0) {
|
|
1290
778
|
const batchCacheKey = JSON.stringify([...missingKeys].sort());
|
|
1291
|
-
let pending =
|
|
779
|
+
let pending = pendingEnsureLenses.get(batchCacheKey);
|
|
1292
780
|
if (!pending) {
|
|
1293
|
-
pending =
|
|
1294
|
-
|
|
1295
|
-
pending.then(() =>
|
|
781
|
+
pending = this.submitEnsureLenses(missingOptions);
|
|
782
|
+
pendingEnsureLenses.set(batchCacheKey, pending);
|
|
783
|
+
pending.then(() => pendingEnsureLenses.delete(batchCacheKey), () => pendingEnsureLenses.delete(batchCacheKey));
|
|
1296
784
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
785
|
+
await pending;
|
|
786
|
+
}
|
|
787
|
+
return lenses.map((option) => {
|
|
788
|
+
const lens = ensuredLenses.get(getEnsureLensCacheKey(option));
|
|
789
|
+
if (!lens) throw new CalmLensError(SDK_ERROR_CODES.INVALID_RESPONSE, { message: `CalmLens did not return lens "${option.identifier}"` });
|
|
790
|
+
return lens;
|
|
791
|
+
});
|
|
1300
792
|
}
|
|
1301
|
-
submitEnsureLenses(options) {
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
_this10.ensuredLenses.set(getEnsureLensCacheKey(optionsForLens), lens);
|
|
1313
|
-
}
|
|
1314
|
-
})();
|
|
793
|
+
async submitEnsureLenses(options) {
|
|
794
|
+
const result = await this.request("PUT", `${this.projectPath}/lenses`, { body: { lenses: options } });
|
|
795
|
+
const returned = Array.isArray(result?.lenses) ? result.lenses : [];
|
|
796
|
+
if (returned.length !== options.length) throw new CalmLensError(SDK_ERROR_CODES.INVALID_RESPONSE, { message: `Expected ${options.length} lenses but CalmLens returned ${returned.length}` });
|
|
797
|
+
const { ensuredLenses } = this.#state;
|
|
798
|
+
for (const [index, lens] of returned.entries()) {
|
|
799
|
+
const optionsForLens = options[index];
|
|
800
|
+
if (lens?.identifier !== optionsForLens.identifier) throw new CalmLensError(SDK_ERROR_CODES.INVALID_RESPONSE, { message: `Expected lens "${optionsForLens.identifier}" at position ${index}` });
|
|
801
|
+
for (const [cacheKey, cachedLens] of ensuredLenses) if (cachedLens.identifier === optionsForLens.identifier) ensuredLenses.delete(cacheKey);
|
|
802
|
+
ensuredLenses.set(getEnsureLensCacheKey(optionsForLens), lens);
|
|
803
|
+
}
|
|
1315
804
|
}
|
|
1316
805
|
};
|
|
1317
806
|
//#endregion
|
|
1318
|
-
export { CalmLensClient, CalmLensError,
|
|
807
|
+
export { CALMLENS_ERROR_CODES, CalmLensClient, CalmLensError, SDK_VERSION, WEBHOOK_DELIVERY_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, WEBHOOK_TIMESTAMP_HEADER, parseWebhookEvent, verifyWebhookSignature };
|