@calmlens/sdk 0.16.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +425 -181
  2. package/bin/calmlens.js +16016 -0
  3. package/bin/package.json +1 -0
  4. package/cjs/BrowserUpload-BT091eOB.d.ts +11 -0
  5. package/cjs/BrowserUpload-CQwXEGFv.js +10 -0
  6. package/cjs/CalmLensAppClient-CHQGu-Do.js +89 -0
  7. package/cjs/CalmLensAppClient-CV_K26Ce.d.ts +40 -0
  8. package/cjs/CalmLensConnection-B8Bu2j3y.js +659 -0
  9. package/cjs/CalmLensServerClient-C5o2A8MW.js +645 -0
  10. package/cjs/CalmLensServerClient-JtP687a3.d.ts +235 -0
  11. package/cjs/CalmLensTypes-9Vt16pc7.d.ts +40400 -0
  12. package/cjs/Classification-94kSYWZw-CMnQKW13.d.ts +5873 -0
  13. package/cjs/CryptoUtils-B_x8lMIx-YBW1-bvL.js +42 -0
  14. package/cjs/FileIcons-Ob003MdS.js +175 -0
  15. package/cjs/Jurisdiction-FusEXb4U-zVs21tDv.js +91 -0
  16. package/cjs/ModerationContext-9lGm-Mna-CcmbRnOy.d.ts +67 -0
  17. package/cjs/ModerationContextLimits-DelWfmiJ.js +1311 -0
  18. package/cjs/ReactUpload-CAyCsyG6.d.ts +17 -0
  19. package/cjs/ReactUpload-Cfg5JYUq.js +54 -0
  20. package/cjs/TestTriggers-By07LR31-CmK0yTyR.d.ts +271 -0
  21. package/cjs/TestTriggers-DTc_iMJF-BjMPq4_3.js +1327 -0
  22. package/cjs/UploadController-BGO1eyzb.js +618 -0
  23. package/{esm/index-C9Xw1aid.d.ts → cjs/WebhookPayload-2aL31v0J.d.ts} +131 -155
  24. package/cjs/app.d.ts +7 -44
  25. package/cjs/app.js +11 -89
  26. package/cjs/elements.d.ts +88 -0
  27. package/cjs/elements.js +555 -0
  28. package/cjs/file-icons.d.ts +67 -0
  29. package/cjs/file-icons.js +10 -0
  30. package/cjs/index-CI4BzN4W.d.ts +252 -0
  31. package/cjs/index.d.ts +6 -4
  32. package/cjs/index.js +9 -4
  33. package/cjs/react-native.d.ts +56 -0
  34. package/cjs/react-native.js +495 -0
  35. package/cjs/react.d.ts +30 -0
  36. package/cjs/react.js +370 -0
  37. package/cjs/server.d.ts +12 -191
  38. package/cjs/server.js +18 -378
  39. package/cjs/test-inputs.d.ts +2 -2
  40. package/cjs/test-inputs.js +9 -264
  41. package/cjs/testing.d.ts +285 -190
  42. package/cjs/testing.js +2393 -1203
  43. package/cjs/ui-CC1GOoMg.js +583 -0
  44. package/cjs/ui-wIiMfqMB.d.ts +526 -0
  45. package/cjs/ui.d.ts +2 -0
  46. package/cjs/ui.js +27 -0
  47. package/cjs/vue.d.ts +490 -0
  48. package/cjs/vue.js +419 -0
  49. package/esm/BrowserUpload-BT091eOB.d.ts +11 -0
  50. package/esm/BrowserUpload-D2xBWqhm.js +5 -0
  51. package/esm/CalmLensAppClient-CV_K26Ce.d.ts +40 -0
  52. package/esm/CalmLensAppClient-DuFas_dW.js +84 -0
  53. package/esm/CalmLensConnection-MxwwLXdl.js +582 -0
  54. package/esm/CalmLensServerClient-D80_mKpW.js +586 -0
  55. package/esm/CalmLensServerClient-JtP687a3.d.ts +235 -0
  56. package/esm/CalmLensTypes-9Vt16pc7.d.ts +40400 -0
  57. package/esm/Classification-94kSYWZw-CMnQKW13.d.ts +5873 -0
  58. package/esm/CryptoUtils-B_x8lMIx-DhuWGzqn.js +25 -0
  59. package/esm/FileIcons-M6dzjFxd.js +128 -0
  60. package/esm/Jurisdiction-FusEXb4U-1ov6m5yE.js +46 -0
  61. package/esm/ModerationContext-9lGm-Mna-CcmbRnOy.d.ts +67 -0
  62. package/esm/ModerationContextLimits-BaX2QnU8.js +1114 -0
  63. package/esm/ReactUpload-CAyCsyG6.d.ts +17 -0
  64. package/esm/ReactUpload-CL2XC8WS.js +37 -0
  65. package/esm/TestTriggers-By07LR31-CmK0yTyR.d.ts +271 -0
  66. package/esm/TestTriggers-DTc_iMJF-HfB533Gx.js +1267 -0
  67. package/esm/UploadController-qjoYGCwx.js +589 -0
  68. package/{cjs/index-C9Xw1aid.d.ts → esm/WebhookPayload-2aL31v0J.d.ts} +131 -155
  69. package/esm/app.d.ts +7 -44
  70. package/esm/app.js +3 -86
  71. package/esm/elements.d.ts +88 -0
  72. package/esm/elements.js +528 -0
  73. package/esm/file-icons.d.ts +67 -0
  74. package/esm/file-icons.js +1 -0
  75. package/esm/index-CI4BzN4W.d.ts +252 -0
  76. package/esm/index.d.ts +6 -4
  77. package/esm/index.js +1 -1
  78. package/esm/react-native.d.ts +56 -0
  79. package/esm/react-native.js +467 -0
  80. package/esm/react.d.ts +30 -0
  81. package/esm/react.js +343 -0
  82. package/esm/server.d.ts +12 -191
  83. package/esm/server.js +3 -369
  84. package/esm/styles.css +147 -0
  85. package/esm/svelte/CalmLensMedia.svelte +134 -0
  86. package/esm/svelte/CalmLensUploader.svelte +175 -0
  87. package/esm/svelte/Icon.svelte +7 -0
  88. package/esm/svelte/index.d.ts +37 -0
  89. package/esm/svelte/index.js +6 -0
  90. package/esm/test-inputs.d.ts +2 -2
  91. package/esm/test-inputs.js +1 -256
  92. package/esm/testing.d.ts +285 -190
  93. package/esm/testing.js +2329 -1122
  94. package/esm/ui-CCY3yXFn.js +458 -0
  95. package/esm/ui-wIiMfqMB.d.ts +526 -0
  96. package/esm/ui.d.ts +2 -0
  97. package/esm/ui.js +3 -0
  98. package/esm/vue.d.ts +490 -0
  99. package/esm/vue.js +392 -0
  100. package/package.json +118 -13
  101. package/cjs/CalmLensConnection-BNUHp_AQ.js +0 -234
  102. package/cjs/CalmLensUtils-o171ownK.js +0 -699
  103. package/cjs/CalmLensVersion-DwhgxQK5.js +0 -480
  104. package/cjs/CalmLensWebhooks-BKrq30Yp.js +0 -201
  105. package/cjs/Classification-BNhgwGEm-DmA9VEuE.d.ts +0 -4806
  106. package/cjs/TestTriggers-CRKtulyp-vrzEq2wm.d.ts +0 -44
  107. package/cjs/WebhookPayload-CJoIkGBD.d.ts +0 -11753
  108. package/esm/CalmLensConnection-XTqmlwzG.js +0 -193
  109. package/esm/CalmLensUtils-DsF8Aa_x.js +0 -574
  110. package/esm/CalmLensVersion-DcifRKsx.js +0 -415
  111. package/esm/CalmLensWebhooks-BvGyk5d3.js +0 -148
  112. package/esm/Classification-BNhgwGEm-DmA9VEuE.d.ts +0 -4806
  113. package/esm/TestTriggers-CRKtulyp-vrzEq2wm.d.ts +0 -44
  114. package/esm/WebhookPayload-CJoIkGBD.d.ts +0 -11753
@@ -0,0 +1,582 @@
1
+ import { A as invalidArgument, C as MAX_IMAGE_UPLOAD_BYTES, E as isRejectedUploadMimeType, M as API_ERROR_CODES, O as CalmLensError, T as getRejectedUploadMimeTypeError, a as getUserAgent, f as randomUuid, k as SDK_ERROR_CODES, m as stringifyQuery, o as isBrowserRuntime, r as selectModerationContextItems, w as MIN_MULTIPART_IMAGE_UPLOAD_BYTES } from "./ModerationContextLimits-BaX2QnU8.js";
2
+ // src/contract/AssetPayload.js
3
+ const uint8ArrayInputToNumbers = (value) => {
4
+ if (Array.isArray(value)) return value;
5
+ return Array.from(value);
6
+ };
7
+ const bufferInputToBytes = (value) => {
8
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
9
+ return Uint8Array.from(value);
10
+ };
11
+ const BASE64_PART_BOUNDARY_ERROR = "Invalid base64 part. For multipart uploads, split the original bytes first, then base64-encode each part. Do not split one full base64 string into substrings.";
12
+ const MULTIPART_PART_TOO_SMALL_ERROR = `Multipart upload parts must be at least 5MiB except the final part.`;
13
+ const getRawBase64 = (value) => {
14
+ const markerIndex = value.indexOf(";base64,");
15
+ if (markerIndex === -1) return value;
16
+ return value.slice(markerIndex + 8);
17
+ };
18
+ const validateBase64PayloadString = (payload, options) => {
19
+ const raw = getRawBase64(payload.base64).replace(/\s/g, "");
20
+ if (options?.multipart && raw.length % 4 !== 0) return {
21
+ ok: false,
22
+ error: BASE64_PART_BOUNDARY_ERROR
23
+ };
24
+ return { ok: true };
25
+ };
26
+ const isReadableStream = (value) => typeof value === "object" && value !== null && "getReader" in value && typeof value.getReader === "function";
27
+ const assetToData = (payload) => {
28
+ if ("plainText" in payload && payload.plainText !== void 0) return new TextEncoder().encode(payload.plainText);
29
+ if ("data" in payload && payload.data) return payload.data;
30
+ if ("buffer" in payload) {
31
+ if (payload.buffer instanceof ArrayBuffer) return payload.buffer;
32
+ return bufferInputToBytes(payload.buffer);
33
+ }
34
+ if (payload.uint8Array !== void 0) return Uint8Array.from(uint8ArrayInputToNumbers(payload.uint8Array));
35
+ if (payload.base64 === void 0) throw new Error("Invalid asset payload");
36
+ const raw = getRawBase64(payload.base64);
37
+ if (typeof Buffer !== "undefined") return Uint8Array.from(Buffer.from(raw, "base64"));
38
+ const binary = atob(raw);
39
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
40
+ };
41
+ const assetDataToUint8Array = async (data) => {
42
+ if (data instanceof Response) return new Uint8Array(await data.arrayBuffer());
43
+ if (isReadableStream(data)) return new Uint8Array(await new Response(data).arrayBuffer());
44
+ if (data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
45
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
46
+ return data;
47
+ };
48
+ const getAssetPayloadByteLength = (payload) => {
49
+ if ("plainText" in payload && payload.plainText !== void 0) return new TextEncoder().encode(payload.plainText).byteLength;
50
+ if ("data" in payload && payload.data) {
51
+ if (payload.data instanceof Blob) return payload.data.size;
52
+ if (payload.data instanceof Response) {
53
+ const contentLength = payload.data.headers.get("content-length");
54
+ return contentLength ? Number(contentLength) : void 0;
55
+ }
56
+ return;
57
+ }
58
+ if ("buffer" in payload && payload.buffer) return payload.buffer.byteLength;
59
+ if (payload.uint8Array !== void 0) {
60
+ if (Array.isArray(payload.uint8Array)) return payload.uint8Array.length;
61
+ return payload.uint8Array.byteLength;
62
+ }
63
+ if (payload.base64 === void 0) return;
64
+ const raw = getRawBase64(payload.base64).replace(/\s/g, "");
65
+ const padding = raw.endsWith("==") ? 2 : raw.endsWith("=") ? 1 : 0;
66
+ return Math.max(0, Math.floor(raw.length * 3 / 4) - padding);
67
+ };
68
+ const validateAssetPayload = (payload, options) => {
69
+ if ("base64" in payload && payload.base64 !== void 0) {
70
+ const validation = validateBase64PayloadString(payload, options);
71
+ if (!validation.ok) return {
72
+ ok: false,
73
+ error: validation.error
74
+ };
75
+ }
76
+ const byteLength = getAssetPayloadByteLength(payload);
77
+ if (options?.multipart && !options.isFinal && byteLength !== void 0 && byteLength < 5242880) return {
78
+ ok: false,
79
+ error: MULTIPART_PART_TOO_SMALL_ERROR
80
+ };
81
+ return { ok: true };
82
+ };
83
+ const RETRY_MAX_DELAY_MS = 8e3;
84
+ const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
85
+ 429,
86
+ 502,
87
+ 503,
88
+ 504
89
+ ]);
90
+ const timeoutReason = () => {
91
+ const reason = /* @__PURE__ */ new Error("The operation timed out.");
92
+ reason.name = "TimeoutError";
93
+ return reason;
94
+ };
95
+ /**
96
+ * Links the callers' signals and a per-attempt deadline. `AbortSignal.any` holds its sources
97
+ * weakly, so a long-lived caller signal doesn't collect a listener per streamed response;
98
+ * runtimes without it get explicit listeners that `release` removes.
99
+ */
100
+ const createAttemptSignal = (signals, timeoutMs) => {
101
+ const callers = signals.filter((signal) => signal !== void 0);
102
+ const deadline = new AbortController();
103
+ let timedOut = false;
104
+ let signal;
105
+ let onAbort;
106
+ if (typeof AbortSignal.any === "function") signal = AbortSignal.any([...callers, deadline.signal]);
107
+ else {
108
+ const controller = new AbortController();
109
+ signal = controller.signal;
110
+ onAbort = (event) => controller.abort(event.target.reason);
111
+ for (const source of [...callers, deadline.signal]) if (source.aborted) controller.abort(source.reason);
112
+ else source.addEventListener("abort", onAbort, { once: true });
113
+ }
114
+ let timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
115
+ timedOut = true;
116
+ deadline.abort(timeoutReason());
117
+ }, timeoutMs);
118
+ return {
119
+ signal,
120
+ isTimedOut: () => timedOut,
121
+ isCancelled: () => callers.some((caller) => caller.aborted),
122
+ clearDeadline: () => {
123
+ if (timer !== void 0) clearTimeout(timer);
124
+ timer = void 0;
125
+ },
126
+ release: () => {
127
+ if (timer !== void 0) clearTimeout(timer);
128
+ timer = void 0;
129
+ if (onAbort) for (const caller of callers) caller.removeEventListener("abort", onAbort);
130
+ }
131
+ };
132
+ };
133
+ /**
134
+ * A redirect the SDK must not follow: it would send the API key to another origin. Browsers report
135
+ * a manual redirect as an opaque response; other runtimes return the 3xx with its Location.
136
+ */
137
+ const isRedirect = (response) => response.type === "opaqueredirect" || response.status >= 300 && response.status < 400 && response.headers.has("location");
138
+ /** Turns anything thrown while sending or reading a request into a `CalmLensError`. */
139
+ const toFailure = (cause, attempt) => {
140
+ if (CalmLensError.isCalmLensError(cause)) return cause;
141
+ if (attempt.isCancelled()) return new CalmLensError(SDK_ERROR_CODES.CANCELLED, {
142
+ message: "Request cancelled",
143
+ cause
144
+ });
145
+ if (attempt.isTimedOut()) return new CalmLensError(SDK_ERROR_CODES.TIMEOUT, {
146
+ message: "Request timed out",
147
+ cause
148
+ });
149
+ return new CalmLensError(SDK_ERROR_CODES.TRANSPORT_ERROR, {
150
+ message: "Request failed",
151
+ cause
152
+ });
153
+ };
154
+ const sleep = (ms, signal) => new Promise((resolve, reject) => {
155
+ if (signal?.aborted) {
156
+ reject(new CalmLensError(SDK_ERROR_CODES.CANCELLED, { message: "Request cancelled" }));
157
+ return;
158
+ }
159
+ const onAbort = () => {
160
+ clearTimeout(timer);
161
+ reject(new CalmLensError(SDK_ERROR_CODES.CANCELLED, { message: "Request cancelled" }));
162
+ };
163
+ const timer = setTimeout(() => {
164
+ signal?.removeEventListener("abort", onAbort);
165
+ resolve();
166
+ }, ms);
167
+ signal?.addEventListener("abort", onAbort, { once: true });
168
+ });
169
+ /** Exponential backoff with jitter: half the delay is fixed, the other half random. */
170
+ const getRetryDelayMs = (attempt, random = Math.random) => {
171
+ const delay = Math.min(RETRY_MAX_DELAY_MS, 500 * 2 ** attempt);
172
+ return Math.round(delay / 2 + random() * (delay / 2));
173
+ };
174
+ /** Parses `Retry-After` as seconds or an HTTP date, in seconds from `now`. */
175
+ const parseRetryAfter = (value, now = Date.now()) => {
176
+ if (!value) return void 0;
177
+ const trimmed = value.trim();
178
+ if (/^\d+$/.test(trimmed)) return Number(trimmed);
179
+ const date = Date.parse(trimmed);
180
+ if (Number.isNaN(date)) return void 0;
181
+ return Math.max(0, Math.ceil((date - now) / 1e3));
182
+ };
183
+ /** Bodies that can be sent again unchanged. Streams are consumed by the first attempt. */
184
+ const isReplayableBody = (body) => body === void 0 || body === null || typeof body === "string" || body instanceof Blob || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || body instanceof URLSearchParams;
185
+ const isApiErrorPayload = (value) => !!value && typeof value === "object" && typeof value.code === "string";
186
+ /** Reads at most `limit` bytes of a body, failing with `sdk/response-too-large` beyond that. */
187
+ const readBody = async (response, limit) => {
188
+ const declared = Number(response.headers.get("content-length"));
189
+ const tooLarge = () => new CalmLensError(SDK_ERROR_CODES.RESPONSE_TOO_LARGE, {
190
+ message: `Response body exceeds ${limit} bytes`,
191
+ status: response.status,
192
+ requestId: response.headers.get("x-request-id") ?? void 0,
193
+ headers: response.headers
194
+ });
195
+ if (Number.isFinite(declared) && declared > limit) {
196
+ await response.body?.cancel().catch(() => {});
197
+ throw tooLarge();
198
+ }
199
+ if (!response.body) return /* @__PURE__ */ new Uint8Array(0);
200
+ const reader = response.body.getReader();
201
+ const chunks = [];
202
+ let total = 0;
203
+ for (;;) {
204
+ const { done, value } = await reader.read();
205
+ if (done) break;
206
+ total += value.byteLength;
207
+ if (total > limit) {
208
+ await reader.cancel().catch(() => {});
209
+ throw tooLarge();
210
+ }
211
+ chunks.push(value);
212
+ }
213
+ const bytes = new Uint8Array(total);
214
+ let offset = 0;
215
+ for (const chunk of chunks) {
216
+ bytes.set(chunk, offset);
217
+ offset += chunk.byteLength;
218
+ }
219
+ return bytes;
220
+ };
221
+ const responseInfo = (response) => ({
222
+ status: response.status,
223
+ requestId: response.headers.get("x-request-id") ?? void 0,
224
+ headers: response.headers,
225
+ retryAfter: parseRetryAfter(response.headers.get("retry-after"))
226
+ });
227
+ /** The `CalmLensError` for a non-2xx response whose body has been read. */
228
+ const toHttpError = (response, bytes) => {
229
+ let data;
230
+ try {
231
+ data = JSON.parse(new TextDecoder().decode(bytes));
232
+ } catch {}
233
+ if (isApiErrorPayload(data)) {
234
+ const { code, message, details, requestId } = data;
235
+ const info = responseInfo(response);
236
+ return new CalmLensError(code, {
237
+ message: message ?? code,
238
+ ...info,
239
+ requestId: info.requestId ?? (typeof requestId === "string" ? requestId : void 0),
240
+ details
241
+ });
242
+ }
243
+ return new CalmLensError(SDK_ERROR_CODES.HTTP_ERROR, {
244
+ message: `HTTP ${response.status}`,
245
+ ...responseInfo(response)
246
+ });
247
+ };
248
+ /** Sends requests with the client's credentials, deadlines, retries and response limits. */
249
+ var CalmLensTransport = class {
250
+ constructor(settings) {
251
+ this.settings = settings;
252
+ this.sleep = settings.sleep ?? sleep;
253
+ }
254
+ headersFor(request) {
255
+ const headers = new Headers(request.init?.headers);
256
+ for (const [name, value] of Object.entries(request.headers ?? {})) headers.set(name, value);
257
+ if (!headers.has("accept") && request.deadline === "body") headers.set("accept", "application/json");
258
+ if (this.settings.apiKey) headers.set("x-api-key", this.settings.apiKey);
259
+ if (!isBrowserRuntime()) headers.set("user-agent", getUserAgent());
260
+ if (request.idempotencyKey) headers.set("idempotency-key", request.idempotencyKey);
261
+ return Object.fromEntries(headers.entries());
262
+ }
263
+ canRetry(request) {
264
+ return this.settings.maxRetries > 0 && (request.method === "GET" || request.method === "HEAD" || !!request.idempotencyKey) && isReplayableBody(request.body);
265
+ }
266
+ /**
267
+ * Sends a request and returns the response, retrying transport failures, timeouts, 429, 502–504 and
268
+ * `idempotency/request-in-progress` when the request is safe to repeat. Non-2xx responses
269
+ * throw unless `allowHttpErrors` is set. Call `release` once the body has been read.
270
+ */
271
+ async send(request, { allowHttpErrors = false } = {}) {
272
+ const fetchImpl = request.fetch ?? this.settings.fetch;
273
+ const retryable = this.canRetry(request);
274
+ const headers = this.headersFor(request);
275
+ for (let attemptNumber = 0;; attemptNumber++) {
276
+ const canRetryAgain = retryable && attemptNumber < this.settings.maxRetries;
277
+ const attempt = createAttemptSignal([this.settings.signal, request.init?.signal ?? void 0], request.timeoutMs);
278
+ let response;
279
+ try {
280
+ if (attempt.signal.aborted) throw attempt.signal.reason;
281
+ response = await fetchImpl(request.url, {
282
+ ...request.init,
283
+ method: request.method,
284
+ headers,
285
+ body: request.body,
286
+ redirect: "manual",
287
+ signal: attempt.signal,
288
+ duplex: "half"
289
+ });
290
+ if (isRedirect(response)) {
291
+ await response.body?.cancel().catch(() => {});
292
+ throw new CalmLensError(SDK_ERROR_CODES.HTTP_ERROR, {
293
+ message: `Unexpected redirect (HTTP ${response.status})`,
294
+ ...responseInfo(response),
295
+ status: response.status || void 0
296
+ });
297
+ }
298
+ } catch (cause) {
299
+ attempt.release();
300
+ const failure = toFailure(cause, attempt);
301
+ if (canRetryAgain && failure.isRetryable()) {
302
+ await this.sleep(getRetryDelayMs(attemptNumber), this.settings.signal);
303
+ continue;
304
+ }
305
+ throw failure;
306
+ }
307
+ if (request.deadline === "headers") attempt.clearDeadline();
308
+ if (response.ok) return {
309
+ response,
310
+ attempt
311
+ };
312
+ if (allowHttpErrors) {
313
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
314
+ const delayMs = retryAfter !== void 0 ? retryAfter * 1e3 : getRetryDelayMs(attemptNumber);
315
+ if (!canRetryAgain || !RETRYABLE_STATUSES.has(response.status) || delayMs > 6e4) return {
316
+ response,
317
+ attempt
318
+ };
319
+ attempt.release();
320
+ await response.body?.cancel().catch(() => {});
321
+ await this.sleep(delayMs, this.settings.signal);
322
+ continue;
323
+ }
324
+ let error;
325
+ try {
326
+ error = toHttpError(response, await readBody(response, this.settings.maxResponseBytes));
327
+ } catch (cause) {
328
+ error = toFailure(cause, attempt);
329
+ } finally {
330
+ attempt.release();
331
+ }
332
+ const retryDelayMs = error.retryAfter !== void 0 ? error.retryAfter * 1e3 : getRetryDelayMs(attemptNumber);
333
+ if (canRetryAgain && retryDelayMs <= 6e4 && error.isRetryable()) {
334
+ await this.sleep(retryDelayMs, this.settings.signal);
335
+ continue;
336
+ }
337
+ throw error;
338
+ }
339
+ }
340
+ /** Sends a request and parses its JSON response body. */
341
+ async json(request) {
342
+ const { response, attempt } = await this.send(request);
343
+ try {
344
+ const bytes = await readBody(response, this.settings.maxResponseBytes);
345
+ if (bytes.byteLength === 0 && (response.status === 204 || request.method === "DELETE")) return;
346
+ try {
347
+ return JSON.parse(new TextDecoder().decode(bytes));
348
+ } catch (cause) {
349
+ throw new CalmLensError(SDK_ERROR_CODES.INVALID_RESPONSE, {
350
+ message: `Invalid JSON response (HTTP ${response.status})`,
351
+ ...responseInfo(response),
352
+ cause
353
+ });
354
+ }
355
+ } catch (cause) {
356
+ throw toFailure(cause, attempt);
357
+ } finally {
358
+ attempt.release();
359
+ }
360
+ }
361
+ };
362
+ /**
363
+ * Where `calmlens/testing` puts its hooks in client options. Not part of the public API:
364
+ * the mock clients run the real clients against their in-memory API with these.
365
+ */
366
+ const MOCK_HOOKS = Symbol.for("@calmlens/sdk.mockHooks");
367
+ /** The mock hooks in client options, if a mock client set them. */
368
+ const getMockHooks = (options) => options[MOCK_HOOKS];
369
+ /**
370
+ * Bytes as a fetch body. They're sent as they are rather than in a Blob: React Native can't
371
+ * build a Blob from bytes, and every fetch accepts a Uint8Array.
372
+ */
373
+ const toBodyBytes = (value) => value.buffer instanceof ArrayBuffer ? value : Uint8Array.from(value);
374
+ const toRawUploadBody = (payload) => {
375
+ const data = assetToData(payload);
376
+ if (data instanceof Response) {
377
+ if (!data.body) throw new CalmLensError(API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Response upload data must have a body" });
378
+ return data.body;
379
+ }
380
+ if (data instanceof ArrayBuffer || data instanceof Blob) return data;
381
+ if ("getReader" in data) return data;
382
+ return toBodyBytes(data);
383
+ };
384
+ const setOptionalQueryValue = (params, key, value) => {
385
+ if (value !== void 0 && value !== null) params.set(key, typeof value === "string" ? value : JSON.stringify(value));
386
+ };
387
+ const assertValidUploadMimeType = (mimeType) => {
388
+ if (isRejectedUploadMimeType(mimeType)) throw new CalmLensError(API_ERROR_CODES.INVALID_UPLOAD_MIME_TYPE, {
389
+ message: getRejectedUploadMimeTypeError(mimeType),
390
+ details: { mimeType }
391
+ });
392
+ };
393
+ const isImageMimeType = (mimeType) => mimeType.toLowerCase().startsWith("image/");
394
+ const invalidPayload = (message) => new CalmLensError(API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message });
395
+ const assertDeclaredPayloadSize = (payload, declared = payload.sizeInBytes) => {
396
+ if (declared === void 0) return;
397
+ const data = "data" in payload ? payload.data : void 0;
398
+ const measured = data instanceof ArrayBuffer || ArrayBuffer.isView(data) ? data.byteLength : getAssetPayloadByteLength(payload);
399
+ if (!Number.isSafeInteger(declared) || declared < 0 || measured !== void 0 && measured !== declared) throw invalidPayload("sizeInBytes must match the payload byte length");
400
+ };
401
+ /** Checks upload options before anything is sent. */
402
+ const prepareSubmitAssetOptions = (options, { deferUploadChecks = false } = {}) => {
403
+ if (!options || typeof options !== "object") throw invalidArgument("Asset options are required");
404
+ if (!("content" in options) && !("url" in options) && !("part" in options)) throw invalidPayload("Either file content, URL, or multipart part is required");
405
+ if (!options.name) throw invalidPayload("Asset name is required");
406
+ if (!("part" in options) && "isFinal" in options) throw invalidPayload("isFinal is only valid for multipart uploads");
407
+ 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" });
408
+ if ("content" in options) {
409
+ assertDeclaredPayloadSize(options.content);
410
+ assertValidUploadMimeType(options.content.type);
411
+ if (!("plainText" in options.content)) {
412
+ const validation = validateAssetPayload(options.content);
413
+ if (!validation.ok) throw invalidPayload(validation.error);
414
+ }
415
+ }
416
+ if ("part" in options) {
417
+ assertDeclaredPayloadSize(options.part, options.part.sizeInBytes ?? options.sizeInBytes);
418
+ assertValidUploadMimeType(options.part.type);
419
+ const partSizeInBytes = options.part.sizeInBytes ?? getAssetPayloadByteLength(options.part);
420
+ if (!deferUploadChecks && isImageMimeType(options.part.type) && partSizeInBytes !== void 0) {
421
+ if (partSizeInBytes > MAX_IMAGE_UPLOAD_BYTES) throw new CalmLensError(API_ERROR_CODES.MULTIPART_IMAGE_TOO_LARGE, { message: "Multipart image uploads cannot exceed 20MB." });
422
+ 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." });
423
+ }
424
+ const validation = validateAssetPayload(options.part, {
425
+ multipart: true,
426
+ isFinal: deferUploadChecks || options.part.isFinal
427
+ });
428
+ if (!validation.ok) throw invalidPayload(validation.error);
429
+ }
430
+ if (options.context !== void 0) {
431
+ if (!options.context || !Array.isArray(options.context.items)) throw invalidArgument("context.items must be an array of earlier posts");
432
+ return {
433
+ ...options,
434
+ context: { items: selectModerationContextItems(options.context.items) }
435
+ };
436
+ }
437
+ return options;
438
+ };
439
+ const newIdempotencyKey = () => randomUuid();
440
+ /** Text content is small, so it keeps the API deadline; binary files may be arbitrarily large. */
441
+ const isTextPayload = (payload) => "plainText" in payload;
442
+ const hasStringStatus = (value) => !!value && typeof value === "object" && typeof value.status === "string";
443
+ const isAsset = (data) => !!data && typeof data === "object" && typeof data.id === "string" && !!data.id && hasStringStatus(data.classification) && hasStringStatus(data.upload) && typeof data.classification?.moderation?.status === "string";
444
+ const isUploadedAsset = (data) => !!data && typeof data === "object" && typeof data.id === "string" && !!data.id;
445
+ const expectUploadedAsset = (data) => {
446
+ if (!isUploadedAsset(data)) throw new CalmLensError(SDK_ERROR_CODES.INVALID_RESPONSE, {
447
+ message: "Expected an uploaded asset response",
448
+ status: 200
449
+ });
450
+ return data;
451
+ };
452
+ const expectAsset = (data) => {
453
+ if (!isAsset(data)) throw new CalmLensError(SDK_ERROR_CODES.INVALID_RESPONSE, {
454
+ message: "Expected an asset response",
455
+ status: 200
456
+ });
457
+ return data;
458
+ };
459
+ /** Fills in the default API base URL and drops trailing slashes. */
460
+ const resolveConnectionOptions = (options) => ({
461
+ ...options,
462
+ baseUrl: (options.baseUrl ?? `https://api.calmlens.com/v1`).replace(/\/+$/, "")
463
+ });
464
+ /** One client's transport, base URL and deadlines, and the requests both clients make. */
465
+ var CalmLensConnection = class {
466
+ constructor(options, apiKey) {
467
+ this.options = options;
468
+ const resolveFetch = () => {
469
+ const fetchImpl = this.options.fetch ?? globalThis.fetch;
470
+ if (typeof fetchImpl !== "function") throw new CalmLensError(SDK_ERROR_CODES.NO_FETCH, { message: "No fetch implementation is available; pass one as the fetch option" });
471
+ return fetchImpl;
472
+ };
473
+ this.hooks = getMockHooks(options);
474
+ this.transport = new CalmLensTransport({
475
+ apiKey,
476
+ sleep: this.hooks?.sleep,
477
+ fetch: ((input, init) => resolveFetch()(input, init)),
478
+ signal: options.signal,
479
+ maxRetries: options.maxRetries ?? 2,
480
+ maxResponseBytes: options.maxResponseBytes ?? 33554432
481
+ });
482
+ }
483
+ /** How the client waits between polls and retries. */
484
+ get sleep() {
485
+ return this.hooks?.sleep ?? sleep;
486
+ }
487
+ /** Whether multipart size checks are left to the API (mock clients only). */
488
+ get deferUploadChecks() {
489
+ return this.hooks?.deferUploadChecks ?? false;
490
+ }
491
+ get timeoutMs() {
492
+ return this.options.timeoutMs ?? 12e4;
493
+ }
494
+ url(path, query) {
495
+ const search = query ? stringifyQuery(query) : "";
496
+ return `${this.options.baseUrl}${path}${search ? `?${search}` : ""}`;
497
+ }
498
+ /** A JSON request whose deadline covers reading the response. */
499
+ json(method, path, { query, body, headers, idempotencyKey } = {}) {
500
+ return this.transport.json({
501
+ method,
502
+ url: this.url(path, query),
503
+ headers: body === void 0 ? headers : {
504
+ ...headers,
505
+ "Content-Type": "application/json"
506
+ },
507
+ body: body === void 0 ? void 0 : JSON.stringify(body),
508
+ idempotencyKey,
509
+ timeoutMs: this.timeoutMs,
510
+ deadline: "body"
511
+ });
512
+ }
513
+ upload(url, payload, headers, idempotencyKey) {
514
+ return this.transport.json({
515
+ method: "POST",
516
+ url,
517
+ headers,
518
+ body: toRawUploadBody(payload),
519
+ idempotencyKey,
520
+ timeoutMs: isTextPayload(payload) ? this.timeoutMs : this.options.uploadTimeoutMs,
521
+ deadline: "body",
522
+ fetch: this.options.uploadFetch
523
+ });
524
+ }
525
+ /**
526
+ * Uploads file content or a first multipart part to `uploadsPath` (`.../assets`), as raw
527
+ * bytes with the options in the query.
528
+ */
529
+ submitRawUpload(uploadsPath, options, queue, authHeaders = {}) {
530
+ const query = new URLSearchParams();
531
+ query.set("name", options.name);
532
+ query.set("queue", String(queue));
533
+ setOptionalQueryValue(query, "description", options.description);
534
+ setOptionalQueryValue(query, "processingOptions", options.processingOptions);
535
+ setOptionalQueryValue(query, "visibility", options.visibility);
536
+ setOptionalQueryValue(query, "metadata", options.metadata);
537
+ setOptionalQueryValue(query, "authorId", options.authorId);
538
+ setOptionalQueryValue(query, "context", options.context);
539
+ setOptionalQueryValue(query, "externalId", options.externalId);
540
+ setOptionalQueryValue(query, "externalUrl", options.externalUrl);
541
+ setOptionalQueryValue(query, "folderId", options.folderId);
542
+ setOptionalQueryValue(query, "uploadPresetId", options.uploadPresetId);
543
+ const isMultipart = "part" in options;
544
+ const payload = isMultipart ? options.part : options.content;
545
+ query.set("contentType", payload.type);
546
+ setOptionalQueryValue(query, "sizeInBytes", payload.sizeInBytes ?? getAssetPayloadByteLength(payload));
547
+ if (isMultipart) {
548
+ const part = payload;
549
+ const multipartOptions = options;
550
+ query.set("partNumber", String(part.partNumber ?? 1));
551
+ query.set("isFinal", String(part.isFinal ?? false));
552
+ setOptionalQueryValue(query, "sizeInBytes", part.sizeInBytes ?? multipartOptions.sizeInBytes);
553
+ setOptionalQueryValue(query, "totalSizeInBytes", multipartOptions.totalSizeInBytes);
554
+ }
555
+ return this.upload(this.url(`${uploadsPath}?${query}`), payload, {
556
+ ...authHeaders,
557
+ "Content-Type": payload.type,
558
+ "X-CalmLens-Upload": isMultipart ? "part" : "content"
559
+ }, options.idempotencyKey ?? newIdempotencyKey());
560
+ }
561
+ /** Uploads the next part of a multipart upload to `partPath` (`.../assets/{id}/part`). */
562
+ uploadPart(partPath, { part, queue, idempotencyKey }, authHeaders = {}) {
563
+ if (!part) throw invalidArgument("Part options are required");
564
+ assertDeclaredPayloadSize(part);
565
+ const validation = validateAssetPayload(part, {
566
+ multipart: true,
567
+ isFinal: this.deferUploadChecks || part.isFinal
568
+ });
569
+ if (!validation.ok) throw invalidPayload(validation.error);
570
+ const query = new URLSearchParams({
571
+ partNumber: String(part.partNumber),
572
+ isFinal: String(part.isFinal ?? false)
573
+ });
574
+ if (queue !== void 0) query.set("queue", String(queue));
575
+ setOptionalQueryValue(query, "sizeInBytes", part.sizeInBytes ?? getAssetPayloadByteLength(part));
576
+ return this.upload(this.url(`${partPath}?${query}`), part, {
577
+ ...authHeaders,
578
+ "X-CalmLens-Upload": "part"
579
+ }, idempotencyKey ?? newIdempotencyKey());
580
+ }
581
+ };
582
+ export { getMockHooks as a, prepareSubmitAssetOptions as c, assetDataToUint8Array as d, assetToData as f, expectUploadedAsset as i, resolveConnectionOptions as l, MOCK_HOOKS as n, invalidPayload as o, validateAssetPayload as p, expectAsset as r, newIdempotencyKey as s, CalmLensConnection as t, sleep as u };