@calmlens/sdk 0.10.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -16
- package/cjs/CalmLensConnection-CYtBP-vu.js +516 -0
- package/cjs/CalmLensError-CKwMKV0S.js +362 -0
- package/cjs/CalmLensUtils-Dg3s4loH.js +367 -0
- package/cjs/CalmLensWebhooks-BTPxKvcD.js +201 -0
- package/cjs/{CalmLensTypes-DjYFaTyh.d.ts → WebhookPayload-Cgb32aSH.d.ts} +5423 -3844
- package/cjs/app.d.ts +43 -0
- package/cjs/app.js +91 -0
- package/cjs/index-BgmWj3KL.d.ts +350 -0
- package/cjs/index.d.ts +3 -382
- package/cjs/index.js +5 -816
- package/cjs/server.d.ts +202 -0
- package/cjs/server.js +371 -0
- package/cjs/src-Dsc8Pn6v.js +45 -0
- package/cjs/testing.d.ts +54 -18
- package/cjs/testing.js +191 -62
- package/esm/CalmLensConnection-wLNy0OIP.js +475 -0
- package/esm/CalmLensError-CihGG26K.js +321 -0
- package/esm/CalmLensUtils-Cx32j2zt.js +260 -0
- package/esm/CalmLensWebhooks-DShmAal4.js +148 -0
- package/esm/{CalmLensTypes-DjYFaTyh.d.ts → WebhookPayload-Cgb32aSH.d.ts} +5423 -3844
- package/esm/app.d.ts +43 -0
- package/esm/app.js +87 -0
- package/esm/index-BgmWj3KL.d.ts +350 -0
- package/esm/index.d.ts +3 -382
- package/esm/index.js +3 -807
- package/esm/server.d.ts +202 -0
- package/esm/server.js +361 -0
- package/esm/src-CJhrILvM.js +22 -0
- package/esm/testing.d.ts +54 -18
- package/esm/testing.js +142 -13
- package/package.json +23 -1
- package/cjs/CalmLensWebhooks-DJnaLpRy.js +0 -741
- package/esm/CalmLensWebhooks-DLUozliX.js +0 -556
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
//#region src/client/CalmLensErrorCodes.ts
|
|
2
|
+
/** Error codes the CalmLens API documents, by name. */
|
|
3
|
+
const API_ERROR_CODES = {
|
|
4
|
+
MISSING_TOKEN: "auth/missing-token",
|
|
5
|
+
INVALID_API_KEY: "auth/invalid-api-key",
|
|
6
|
+
API_KEY_PROJECT_MISMATCH: "auth/project-mismatch",
|
|
7
|
+
API_KEY_NOT_ACCEPTED: "auth/api-key-not-accepted",
|
|
8
|
+
FILE_TOO_LARGE: "upload/file-too-large",
|
|
9
|
+
IMAGE_REQUIRES_MULTIPART_UPLOAD: "upload/image-requires-multipart-upload",
|
|
10
|
+
MULTIPART_IMAGE_TOO_SMALL: "upload/multipart-image-too-small",
|
|
11
|
+
MULTIPART_IMAGE_TOO_LARGE: "upload/multipart-image-too-large",
|
|
12
|
+
IMAGE_EXCEEDED_MAX_DIMENSIONS: "upload/image-exceeded-max-dimensions",
|
|
13
|
+
UPLOAD_LIMIT_EXCEEDED: "upload/limit-exceeded",
|
|
14
|
+
INVALID_UPLOAD_MIME_TYPE: "upload/invalid-mime-type",
|
|
15
|
+
INVALID_EVENT_CURSOR: "events/invalid-cursor",
|
|
16
|
+
INVALID_UPLOAD_PAYLOAD: "upload/invalid-payload",
|
|
17
|
+
USE_UPLOAD_PART: "upload/use-upload-part",
|
|
18
|
+
UPLOAD_NOT_OPEN: "upload/not-open",
|
|
19
|
+
INVALID_UPLOAD_PART_NUMBER: "upload/invalid-part-number",
|
|
20
|
+
UPLOAD_MISSING: "upload/missing",
|
|
21
|
+
UPLOAD_SIZE_MISMATCH: "upload/size-mismatch",
|
|
22
|
+
INVALID_IDEMPOTENCY_KEY: "idempotency/invalid-key",
|
|
23
|
+
IDEMPOTENCY_KEY_REUSED: "idempotency/key-reused",
|
|
24
|
+
IDEMPOTENT_REQUEST_IN_PROGRESS: "idempotency/request-in-progress",
|
|
25
|
+
ASSET_NOT_FOUND: "asset/not-found",
|
|
26
|
+
PROJECT_NOT_FOUND: "project/not-found",
|
|
27
|
+
ASSET_UPDATE_FAILED: "asset/update-failed",
|
|
28
|
+
ASSET_SAFETY_HELD: "asset/safety-held",
|
|
29
|
+
ASSET_NOT_RETRYABLE: "asset/not-retryable",
|
|
30
|
+
ASSET_CONTENT_UNAVAILABLE: "asset/content-unavailable",
|
|
31
|
+
SUPPORT_TICKET_NOT_FOUND: "support/ticket-not-found",
|
|
32
|
+
DUPLICATE_EXTERNAL_ID: "asset/duplicate-external-id",
|
|
33
|
+
FOLDER_NOT_FOUND: "folder/not-found",
|
|
34
|
+
DUPLICATE_FOLDER_NAME: "folder/duplicate-name",
|
|
35
|
+
FOLDER_NOT_EMPTY: "folder/not-empty",
|
|
36
|
+
FOLDER_PARENT_CYCLE: "folder/parent-cycle",
|
|
37
|
+
LENS_VERSION_CONFLICT: "lens/version-conflict",
|
|
38
|
+
UPLOAD_PRESET_NOT_FOUND: "upload-preset/not-found",
|
|
39
|
+
UPLOAD_PRESET_DISABLED: "upload-preset/disabled",
|
|
40
|
+
UPLOAD_PRESET_TOKEN_REQUIRED: "upload-preset/token-required",
|
|
41
|
+
UPLOAD_PRESET_CONTENT_TYPE_NOT_ALLOWED: "upload-preset/content-type-not-allowed",
|
|
42
|
+
UPLOAD_PRESET_TOO_LARGE: "upload-preset/too-large",
|
|
43
|
+
UPLOAD_PRESET_DAILY_LIMIT_REACHED: "upload-preset/daily-limit-reached",
|
|
44
|
+
UPLOAD_PRESET_ORIGIN_NOT_ALLOWED: "upload-preset/origin-not-allowed",
|
|
45
|
+
UPLOAD_PRESET_FIELD_NOT_ALLOWED: "upload-preset/field-not-allowed",
|
|
46
|
+
UPLOAD_TOKEN_INVALID: "upload-token/invalid",
|
|
47
|
+
UPLOAD_TOKEN_EXPIRED: "upload-token/expired",
|
|
48
|
+
UPLOAD_TOKEN_USED: "upload-token/used"
|
|
49
|
+
};
|
|
50
|
+
/** The HTTP status and default message the API returns with each documented error code. */
|
|
51
|
+
const API_ERROR_DEFINITIONS = {
|
|
52
|
+
"auth/missing-token": {
|
|
53
|
+
status: 401,
|
|
54
|
+
message: "Authentication required. Send your project API key in the X-Api-Key header."
|
|
55
|
+
},
|
|
56
|
+
"auth/invalid-api-key": {
|
|
57
|
+
status: 401,
|
|
58
|
+
message: "The API key is invalid, revoked or expired. Create a key under Developer → API Keys."
|
|
59
|
+
},
|
|
60
|
+
"auth/project-mismatch": {
|
|
61
|
+
status: 403,
|
|
62
|
+
message: "This API key belongs to a different project. Use a key created for the project that owns this resource."
|
|
63
|
+
},
|
|
64
|
+
"auth/api-key-not-accepted": {
|
|
65
|
+
status: 403,
|
|
66
|
+
message: "API keys are only accepted by the public API at /v1."
|
|
67
|
+
},
|
|
68
|
+
"upload/file-too-large": {
|
|
69
|
+
status: 400,
|
|
70
|
+
message: "File size exceeds limit. Maximum direct image size is 10MB, audio/video size is 50MiB, and other files is 5GB."
|
|
71
|
+
},
|
|
72
|
+
"upload/image-requires-multipart-upload": {
|
|
73
|
+
status: 400,
|
|
74
|
+
message: "Images of 10MB or more must use a multipart upload."
|
|
75
|
+
},
|
|
76
|
+
"upload/multipart-image-too-small": {
|
|
77
|
+
status: 400,
|
|
78
|
+
message: "Multipart image uploads must total at least 10MB."
|
|
79
|
+
},
|
|
80
|
+
"upload/multipart-image-too-large": {
|
|
81
|
+
status: 400,
|
|
82
|
+
message: "Multipart image uploads cannot exceed 20MB."
|
|
83
|
+
},
|
|
84
|
+
"upload/image-exceeded-max-dimensions": {
|
|
85
|
+
status: 400,
|
|
86
|
+
message: "Image exceeds maximum dimensions. Maximum dimensions are 12,000px."
|
|
87
|
+
},
|
|
88
|
+
"upload/limit-exceeded": {
|
|
89
|
+
status: 403,
|
|
90
|
+
message: "Upload limit exceeded. You have used all your free uploads. Please upgrade to Plus or Pro for unlimited uploads."
|
|
91
|
+
},
|
|
92
|
+
"upload/invalid-mime-type": {
|
|
93
|
+
status: 400,
|
|
94
|
+
message: "Upload file type is not supported. Uploads must use a concrete file MIME type that CalmLens can store and process, such as image/png, image/jpeg, application/pdf, or text/plain."
|
|
95
|
+
},
|
|
96
|
+
"events/invalid-cursor": {
|
|
97
|
+
status: 400,
|
|
98
|
+
message: "The events cursor is invalid. Use the cursor from a previous response, or omit it to start from now."
|
|
99
|
+
},
|
|
100
|
+
"upload/invalid-payload": {
|
|
101
|
+
status: 400,
|
|
102
|
+
message: "Upload payload is invalid."
|
|
103
|
+
},
|
|
104
|
+
"upload/use-upload-part": {
|
|
105
|
+
status: 409,
|
|
106
|
+
message: "Use uploadPart for multipart upload parts after the first part"
|
|
107
|
+
},
|
|
108
|
+
"upload/not-open": {
|
|
109
|
+
status: 409,
|
|
110
|
+
message: "Asset is not accepting additional parts"
|
|
111
|
+
},
|
|
112
|
+
"upload/invalid-part-number": {
|
|
113
|
+
status: 409,
|
|
114
|
+
message: "Multipart upload part number is invalid."
|
|
115
|
+
},
|
|
116
|
+
"upload/missing": {
|
|
117
|
+
status: 409,
|
|
118
|
+
message: "Multipart upload state is missing"
|
|
119
|
+
},
|
|
120
|
+
"upload/size-mismatch": {
|
|
121
|
+
status: 400,
|
|
122
|
+
message: "The uploaded parts do not add up to the totalSizeInBytes declared when the multipart upload started."
|
|
123
|
+
},
|
|
124
|
+
"idempotency/invalid-key": {
|
|
125
|
+
status: 400,
|
|
126
|
+
message: "The Idempotency-Key header must be 1 to 255 printable ASCII characters, such as a UUID."
|
|
127
|
+
},
|
|
128
|
+
"idempotency/key-reused": {
|
|
129
|
+
status: 409,
|
|
130
|
+
message: "This Idempotency-Key was already used for a different request. Use a new key for each distinct request."
|
|
131
|
+
},
|
|
132
|
+
"idempotency/request-in-progress": {
|
|
133
|
+
status: 409,
|
|
134
|
+
message: "A request with this Idempotency-Key is still being processed. Retry after the Retry-After delay to receive its result."
|
|
135
|
+
},
|
|
136
|
+
"asset/not-found": {
|
|
137
|
+
status: 404,
|
|
138
|
+
message: "Asset not found"
|
|
139
|
+
},
|
|
140
|
+
"project/not-found": {
|
|
141
|
+
status: 404,
|
|
142
|
+
message: "Project not found"
|
|
143
|
+
},
|
|
144
|
+
"asset/update-failed": {
|
|
145
|
+
status: 500,
|
|
146
|
+
message: "Failed to update asset"
|
|
147
|
+
},
|
|
148
|
+
"asset/safety-held": {
|
|
149
|
+
status: 423,
|
|
150
|
+
message: "Asset access is restricted by a safety hold."
|
|
151
|
+
},
|
|
152
|
+
"asset/not-retryable": {
|
|
153
|
+
status: 409,
|
|
154
|
+
message: "Only assets that failed to process (status error) can be retried."
|
|
155
|
+
},
|
|
156
|
+
"asset/content-unavailable": {
|
|
157
|
+
status: 409,
|
|
158
|
+
message: "This asset's content is no longer stored, so it can't be processed again. Upload it again instead."
|
|
159
|
+
},
|
|
160
|
+
"support/ticket-not-found": {
|
|
161
|
+
status: 404,
|
|
162
|
+
message: "Support ticket not found."
|
|
163
|
+
},
|
|
164
|
+
"asset/duplicate-external-id": {
|
|
165
|
+
status: 409,
|
|
166
|
+
message: "An asset with this externalId already exists in the project."
|
|
167
|
+
},
|
|
168
|
+
"folder/not-found": {
|
|
169
|
+
status: 404,
|
|
170
|
+
message: "Folder not found"
|
|
171
|
+
},
|
|
172
|
+
"folder/duplicate-name": {
|
|
173
|
+
status: 409,
|
|
174
|
+
message: "A folder with this name already exists in this location."
|
|
175
|
+
},
|
|
176
|
+
"folder/not-empty": {
|
|
177
|
+
status: 409,
|
|
178
|
+
message: "Folder must be empty before it can be deleted."
|
|
179
|
+
},
|
|
180
|
+
"folder/parent-cycle": {
|
|
181
|
+
status: 409,
|
|
182
|
+
message: "A folder cannot be moved inside itself or one of its descendants."
|
|
183
|
+
},
|
|
184
|
+
"lens/version-conflict": {
|
|
185
|
+
status: 409,
|
|
186
|
+
message: "Lens updates require a version greater than the current version."
|
|
187
|
+
},
|
|
188
|
+
"upload-preset/not-found": {
|
|
189
|
+
status: 404,
|
|
190
|
+
message: "Upload preset not found. Check the preset ID in the portal."
|
|
191
|
+
},
|
|
192
|
+
"upload-preset/disabled": {
|
|
193
|
+
status: 403,
|
|
194
|
+
message: "This upload preset is paused. Turn it on in the portal to accept uploads."
|
|
195
|
+
},
|
|
196
|
+
"upload-preset/token-required": {
|
|
197
|
+
status: 401,
|
|
198
|
+
message: "This upload preset only accepts uploads with an upload token. Sign one on your server with uploadTokens.sign and pass it to the client."
|
|
199
|
+
},
|
|
200
|
+
"upload-preset/content-type-not-allowed": {
|
|
201
|
+
status: 415,
|
|
202
|
+
message: "This upload preset doesn't accept this file type."
|
|
203
|
+
},
|
|
204
|
+
"upload-preset/too-large": {
|
|
205
|
+
status: 413,
|
|
206
|
+
message: "This file is larger than the upload preset allows."
|
|
207
|
+
},
|
|
208
|
+
"upload-preset/daily-limit-reached": {
|
|
209
|
+
status: 429,
|
|
210
|
+
message: "This upload preset has reached its daily upload limit. Try again after midnight UTC, or raise the limit in the portal."
|
|
211
|
+
},
|
|
212
|
+
"upload-preset/origin-not-allowed": {
|
|
213
|
+
status: 403,
|
|
214
|
+
message: "This site isn't allowed to upload through this upload preset."
|
|
215
|
+
},
|
|
216
|
+
"upload-preset/field-not-allowed": {
|
|
217
|
+
status: 400,
|
|
218
|
+
message: "This field is set by the upload preset or the upload token and can't be changed by the upload."
|
|
219
|
+
},
|
|
220
|
+
"upload-token/invalid": {
|
|
221
|
+
status: 401,
|
|
222
|
+
message: "The upload token is invalid. Sign a new one on your server with an active API key for this project."
|
|
223
|
+
},
|
|
224
|
+
"upload-token/expired": {
|
|
225
|
+
status: 401,
|
|
226
|
+
message: "The upload token has expired. Sign a new one on your server."
|
|
227
|
+
},
|
|
228
|
+
"upload-token/used": {
|
|
229
|
+
status: 409,
|
|
230
|
+
message: "The upload token has already been used. Each token uploads one asset."
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/client/CalmLensError.ts
|
|
235
|
+
/** Codes for failures the SDK detects itself, before or instead of an API error response. */
|
|
236
|
+
const SDK_ERROR_CODES = {
|
|
237
|
+
/** The request could not be completed: DNS, TLS, a refused or reset connection, a truncated body. */
|
|
238
|
+
TRANSPORT_ERROR: "sdk/transport-error",
|
|
239
|
+
/** The request did not finish within `timeoutMs`. */
|
|
240
|
+
TIMEOUT: "sdk/timeout",
|
|
241
|
+
/** The caller's `AbortSignal` cancelled the request. */
|
|
242
|
+
CANCELLED: "sdk/cancelled",
|
|
243
|
+
/** A successful response whose body is not the JSON the operation returns. */
|
|
244
|
+
INVALID_RESPONSE: "sdk/invalid-response",
|
|
245
|
+
/** An HTTP failure without a CalmLens error body, for example from a proxy. */
|
|
246
|
+
HTTP_ERROR: "sdk/http-error",
|
|
247
|
+
/** A response body larger than `maxResponseBytes`. */
|
|
248
|
+
RESPONSE_TOO_LARGE: "sdk/response-too-large",
|
|
249
|
+
/** A method argument is missing or invalid, for example an empty asset ID. */
|
|
250
|
+
INVALID_ARGUMENT: "sdk/invalid-argument",
|
|
251
|
+
/** The client options are invalid, for example a missing API key. */
|
|
252
|
+
INVALID_CONFIG: "sdk/invalid-config",
|
|
253
|
+
/** No `fetch` implementation is available in this runtime. */
|
|
254
|
+
NO_FETCH: "sdk/no-fetch",
|
|
255
|
+
/** A webhook delivery whose signature or timestamp did not verify. */
|
|
256
|
+
INVALID_WEBHOOK_SIGNATURE: "sdk/invalid-webhook-signature",
|
|
257
|
+
/** A verified webhook delivery whose body is not a webhook event. */
|
|
258
|
+
INVALID_WEBHOOK_PAYLOAD: "sdk/invalid-webhook-payload"
|
|
259
|
+
};
|
|
260
|
+
/** Every error code the SDK can throw: the documented API codes and the SDK's own `sdk/*` codes. */
|
|
261
|
+
const CALMLENS_ERROR_CODES = {
|
|
262
|
+
...API_ERROR_CODES,
|
|
263
|
+
...SDK_ERROR_CODES
|
|
264
|
+
};
|
|
265
|
+
const BRAND = Symbol.for("calmlens.sdk.CalmLensError");
|
|
266
|
+
const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
267
|
+
429,
|
|
268
|
+
502,
|
|
269
|
+
503,
|
|
270
|
+
504
|
|
271
|
+
]);
|
|
272
|
+
const RETRYABLE_CODES = /* @__PURE__ */ new Set([SDK_ERROR_CODES.TRANSPORT_ERROR, API_ERROR_CODES.IDEMPOTENT_REQUEST_IN_PROGRESS]);
|
|
273
|
+
/**
|
|
274
|
+
* Every failure the SDK reports, whether the API returned an error or the request never
|
|
275
|
+
* completed. Branch on `code`; `status`, `requestId`, `headers` and `retryAfter` describe the
|
|
276
|
+
* HTTP response when there was one, and `cause` holds the underlying error.
|
|
277
|
+
*
|
|
278
|
+
* `instanceof CalmLensError` and `CalmLensError.isCalmLensError(value)` both recognise errors
|
|
279
|
+
* from any copy of the SDK, so they work when ESM and CommonJS builds are loaded side by side.
|
|
280
|
+
*/
|
|
281
|
+
var CalmLensError = class CalmLensError extends Error {
|
|
282
|
+
constructor(code, options = {}) {
|
|
283
|
+
const id = typeof code === "string" ? code : code.id;
|
|
284
|
+
const defaultMessage = typeof code === "string" ? void 0 : code.defaultMessage;
|
|
285
|
+
super(`[calmlens] ${options.message ?? defaultMessage ?? id}`, { cause: options.cause });
|
|
286
|
+
this.name = "CalmLensError";
|
|
287
|
+
this.code = id;
|
|
288
|
+
this.status = options.status;
|
|
289
|
+
this.details = options.details;
|
|
290
|
+
this.requestId = options.requestId;
|
|
291
|
+
this.headers = options.headers;
|
|
292
|
+
this.retryAfter = options.retryAfter;
|
|
293
|
+
Object.defineProperty(this, BRAND, { value: true });
|
|
294
|
+
}
|
|
295
|
+
/** True for any `CalmLensError`, including one created by another copy or build of the SDK. */
|
|
296
|
+
static isCalmLensError(value) {
|
|
297
|
+
return typeof value === "object" && value !== null && value[BRAND] === true;
|
|
298
|
+
}
|
|
299
|
+
static [Symbol.hasInstance](value) {
|
|
300
|
+
return CalmLensError.isCalmLensError(value);
|
|
301
|
+
}
|
|
302
|
+
/** The resource does not exist or is not visible to this API key. */
|
|
303
|
+
isNotFound() {
|
|
304
|
+
return this.status === 404;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Whether repeating the same request may succeed: transport failures, throttling (429),
|
|
308
|
+
* gateway errors (502, 503, 504) and idempotent requests still in progress. Only repeat a
|
|
309
|
+
* create or upload with the same `idempotencyKey`.
|
|
310
|
+
*/
|
|
311
|
+
isRetryable() {
|
|
312
|
+
return RETRYABLE_CODES.has(this.code) || this.status !== void 0 && RETRYABLE_STATUSES.has(this.status);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const invalidArgument = (message, details) => new CalmLensError(SDK_ERROR_CODES.INVALID_ARGUMENT, {
|
|
316
|
+
message,
|
|
317
|
+
details
|
|
318
|
+
});
|
|
319
|
+
const invalidConfig = (message) => new CalmLensError(SDK_ERROR_CODES.INVALID_CONFIG, { message });
|
|
320
|
+
//#endregion
|
|
321
|
+
export { invalidConfig as a, invalidArgument as i, CalmLensError as n, API_ERROR_CODES as o, SDK_ERROR_CODES as r, API_ERROR_DEFINITIONS as s, CALMLENS_ERROR_CODES as t };
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { a as invalidConfig, i as invalidArgument, n as CalmLensError, o as API_ERROR_CODES } from "./CalmLensError-CihGG26K.js";
|
|
2
|
+
const MAX_IMAGE_UPLOAD_BYTES = /* @__PURE__ */ (() => 20971520)();
|
|
3
|
+
const MIN_MULTIPART_IMAGE_UPLOAD_BYTES = /* @__PURE__ */ (() => 10485760)();
|
|
4
|
+
const REJECTED_UPLOAD_MIME_TYPE_SET = /* @__PURE__ */ new Set([
|
|
5
|
+
"application/octet-stream",
|
|
6
|
+
"multipart/form-data",
|
|
7
|
+
"multipart/mixed",
|
|
8
|
+
"multipart/alternative",
|
|
9
|
+
"multipart/related",
|
|
10
|
+
"multipart/signed",
|
|
11
|
+
"multipart/encrypted"
|
|
12
|
+
]);
|
|
13
|
+
const normalizeUploadMimeType = (mimeType) => mimeType.toLowerCase().split(";")[0]?.trim() || mimeType.toLowerCase().trim();
|
|
14
|
+
const isRejectedUploadMimeType = (mimeType) => REJECTED_UPLOAD_MIME_TYPE_SET.has(normalizeUploadMimeType(mimeType));
|
|
15
|
+
const getRejectedUploadMimeTypeError = (mimeType) => `${normalizeUploadMimeType(mimeType)} is not a valid upload file type. Uploads must use a concrete file MIME type that CalmLens can store and process, such as image/png, image/jpeg, application/pdf, or text/plain.`;
|
|
16
|
+
const DOMAIN = "calmlens.com";
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region ../shared/src/AssetPayload.ts
|
|
19
|
+
const uint8ArrayInputToNumbers = (value) => {
|
|
20
|
+
if (Array.isArray(value)) return value;
|
|
21
|
+
return Array.from(value);
|
|
22
|
+
};
|
|
23
|
+
const bufferInputToBytes = (value) => {
|
|
24
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
25
|
+
return Uint8Array.from(value);
|
|
26
|
+
};
|
|
27
|
+
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.";
|
|
28
|
+
const MULTIPART_PART_TOO_SMALL_ERROR = `Multipart upload parts must be at least 5MiB except the final part.`;
|
|
29
|
+
const getRawBase64 = (value) => {
|
|
30
|
+
const markerIndex = value.indexOf(";base64,");
|
|
31
|
+
if (markerIndex === -1) return value;
|
|
32
|
+
return value.slice(markerIndex + 8);
|
|
33
|
+
};
|
|
34
|
+
const validateBase64PayloadString = (payload, options) => {
|
|
35
|
+
const raw = getRawBase64(payload.base64).replace(/\s/g, "");
|
|
36
|
+
if (options?.multipart && raw.length % 4 !== 0) return {
|
|
37
|
+
ok: false,
|
|
38
|
+
error: BASE64_PART_BOUNDARY_ERROR
|
|
39
|
+
};
|
|
40
|
+
return { ok: true };
|
|
41
|
+
};
|
|
42
|
+
const isReadableStream = (value) => typeof value === "object" && value !== null && "getReader" in value && typeof value.getReader === "function";
|
|
43
|
+
const assetToData = (payload) => {
|
|
44
|
+
if ("plainText" in payload && payload.plainText !== void 0) return new TextEncoder().encode(payload.plainText);
|
|
45
|
+
if ("data" in payload && payload.data) return payload.data;
|
|
46
|
+
if ("buffer" in payload) {
|
|
47
|
+
if (payload.buffer instanceof ArrayBuffer) return payload.buffer;
|
|
48
|
+
return bufferInputToBytes(payload.buffer);
|
|
49
|
+
}
|
|
50
|
+
if (payload.uint8Array !== void 0) return Uint8Array.from(uint8ArrayInputToNumbers(payload.uint8Array));
|
|
51
|
+
if (payload.base64 === void 0) throw new Error("Invalid asset payload");
|
|
52
|
+
const raw = getRawBase64(payload.base64);
|
|
53
|
+
if (typeof Buffer !== "undefined") return Uint8Array.from(Buffer.from(raw, "base64"));
|
|
54
|
+
const binary = atob(raw);
|
|
55
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
56
|
+
};
|
|
57
|
+
const assetDataToUint8Array = async (data) => {
|
|
58
|
+
if (data instanceof Response) return new Uint8Array(await data.arrayBuffer());
|
|
59
|
+
if (isReadableStream(data)) return new Uint8Array(await new Response(data).arrayBuffer());
|
|
60
|
+
if (data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
|
|
61
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
62
|
+
return data;
|
|
63
|
+
};
|
|
64
|
+
const getAssetPayloadByteLength = (payload) => {
|
|
65
|
+
if ("plainText" in payload && payload.plainText !== void 0) return new TextEncoder().encode(payload.plainText).byteLength;
|
|
66
|
+
if ("data" in payload && payload.data) {
|
|
67
|
+
if (payload.data instanceof Blob) return payload.data.size;
|
|
68
|
+
if (payload.data instanceof Response) {
|
|
69
|
+
const contentLength = payload.data.headers.get("content-length");
|
|
70
|
+
return contentLength ? Number(contentLength) : void 0;
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if ("buffer" in payload && payload.buffer) return payload.buffer.byteLength;
|
|
75
|
+
if (payload.uint8Array !== void 0) {
|
|
76
|
+
if (Array.isArray(payload.uint8Array)) return payload.uint8Array.length;
|
|
77
|
+
return payload.uint8Array.byteLength;
|
|
78
|
+
}
|
|
79
|
+
if (payload.base64 === void 0) return;
|
|
80
|
+
const raw = getRawBase64(payload.base64).replace(/\s/g, "");
|
|
81
|
+
const padding = raw.endsWith("==") ? 2 : raw.endsWith("=") ? 1 : 0;
|
|
82
|
+
return Math.max(0, Math.floor(raw.length * 3 / 4) - padding);
|
|
83
|
+
};
|
|
84
|
+
const validateAssetPayload = (payload, options) => {
|
|
85
|
+
if ("base64" in payload && payload.base64 !== void 0) {
|
|
86
|
+
const validation = validateBase64PayloadString(payload, options);
|
|
87
|
+
if (!validation.ok) return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: validation.error
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const byteLength = getAssetPayloadByteLength(payload);
|
|
93
|
+
if (options?.multipart && !options.isFinal && byteLength !== void 0 && byteLength < 5242880) return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error: MULTIPART_PART_TOO_SMALL_ERROR
|
|
96
|
+
};
|
|
97
|
+
return { ok: true };
|
|
98
|
+
};
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/client/CalmLensUtils.ts
|
|
101
|
+
const assertHttpUrl = (name, value) => {
|
|
102
|
+
if (value === void 0) return;
|
|
103
|
+
let url;
|
|
104
|
+
try {
|
|
105
|
+
url = new URL(value);
|
|
106
|
+
} catch {}
|
|
107
|
+
if (!url || url.protocol !== "https:" && url.protocol !== "http:") throw invalidConfig(`${name} must be an absolute http(s) URL`);
|
|
108
|
+
};
|
|
109
|
+
const assertInteger = (name, value, minimum) => {
|
|
110
|
+
if (value !== void 0 && (!Number.isSafeInteger(value) || value < minimum)) throw invalidConfig(minimum === 0 ? `${name} must be a non-negative integer` : `${name} must be a positive integer`);
|
|
111
|
+
};
|
|
112
|
+
const configPathSegment = (value, name) => {
|
|
113
|
+
try {
|
|
114
|
+
encodePathSegment(value, name);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw invalidConfig(error.message.replace(/^\[calmlens\] /, ""));
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
/** Checks the options both clients share: where requests go and how they're sent. */
|
|
120
|
+
const validateConnectionOptions = (options) => {
|
|
121
|
+
if ("requestBackend" in options) throw invalidConfig("requestBackend is no longer supported; pass a fetch implementation as fetch");
|
|
122
|
+
validateRequestOptions(options);
|
|
123
|
+
assertInteger("maxResponseBytes", options.maxResponseBytes, 1);
|
|
124
|
+
for (const name of ["fetch", "uploadFetch"]) if (options[name] !== void 0 && typeof options[name] !== "function") throw invalidConfig(`${name} must be a function`);
|
|
125
|
+
assertHttpUrl("baseUrl", options.baseUrl);
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Checks server client options before the client sets anything up. `CalmLensServerClient` and
|
|
129
|
+
* `MockCalmLensServerClient` share it so both reject the same configuration.
|
|
130
|
+
*/
|
|
131
|
+
const validateServerClientOptions = (options) => {
|
|
132
|
+
if (!options || typeof options !== "object") throw invalidConfig("Client options are required");
|
|
133
|
+
if (!options.apiKey || typeof options.apiKey !== "string") throw invalidConfig("API key is required");
|
|
134
|
+
if (!options.projectId) throw invalidConfig("Project ID is required");
|
|
135
|
+
configPathSegment(options.projectId, "Project ID");
|
|
136
|
+
validateConnectionOptions(options);
|
|
137
|
+
if (options.cdnFetch !== void 0 && typeof options.cdnFetch !== "function") throw invalidConfig("cdnFetch must be a function");
|
|
138
|
+
assertHttpUrl("cdnBaseUrl", options.cdnBaseUrl);
|
|
139
|
+
};
|
|
140
|
+
/** Checks app client options before the client sets anything up. */
|
|
141
|
+
const validateAppClientOptions = (options) => {
|
|
142
|
+
if (!options || typeof options !== "object") throw invalidConfig("Client options are required");
|
|
143
|
+
if ("apiKey" in options && options.apiKey !== void 0) throw invalidConfig("CalmLensAppClient never takes an API key, which anyone using the browser or app could read. Upload through uploadPresetId, and keep the API key in CalmLensServerClient on your server.");
|
|
144
|
+
if (!options.uploadPresetId) throw invalidConfig("Upload preset ID is required");
|
|
145
|
+
configPathSegment(options.uploadPresetId, "Upload preset ID");
|
|
146
|
+
if (options.token !== void 0 && typeof options.token !== "string" && typeof options.token !== "function") throw invalidConfig("token must be a string or a function that returns one");
|
|
147
|
+
validateConnectionOptions(options);
|
|
148
|
+
};
|
|
149
|
+
/** Checks the options `withRequestOptions` accepts. */
|
|
150
|
+
const validateRequestOptions = (options) => {
|
|
151
|
+
assertInteger("timeoutMs", options.timeoutMs, 1);
|
|
152
|
+
assertInteger("uploadTimeoutMs", options.uploadTimeoutMs, 1);
|
|
153
|
+
assertInteger("maxRetries", options.maxRetries, 0);
|
|
154
|
+
};
|
|
155
|
+
const toTextAssetOptions = ({ text, name, ...options }) => {
|
|
156
|
+
if (typeof text !== "string" || !text.trim()) throw new CalmLensError(API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Text to moderate must be a non-empty string" });
|
|
157
|
+
return {
|
|
158
|
+
...options,
|
|
159
|
+
name: name ?? "text",
|
|
160
|
+
content: {
|
|
161
|
+
plainText: text,
|
|
162
|
+
type: "text/plain"
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* A random UUID v4. React Native has no `crypto` global, so this falls back to
|
|
168
|
+
* `crypto.getRandomValues` and then to `Math.random`; the SDK only uses it for idempotency keys,
|
|
169
|
+
* which need to be unique rather than secret.
|
|
170
|
+
*/
|
|
171
|
+
const randomUuid = () => {
|
|
172
|
+
const crypto = globalThis.crypto;
|
|
173
|
+
if (typeof crypto?.randomUUID === "function") return crypto.randomUUID();
|
|
174
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
175
|
+
if (typeof crypto?.getRandomValues === "function") crypto.getRandomValues(bytes);
|
|
176
|
+
else for (let index = 0; index < bytes.length; index++) bytes[index] = Math.random() * 256;
|
|
177
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
178
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
179
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
180
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Encodes an identifier as one URL path segment. `.` and `..` are rejected because URL
|
|
184
|
+
* resolution would treat them as relative segments and send the request to another route.
|
|
185
|
+
*/
|
|
186
|
+
const encodePathSegment = (value, name) => {
|
|
187
|
+
if (typeof value !== "string" || !value) throw invalidArgument(`${name} is required`);
|
|
188
|
+
if (value === "." || value === "..") throw invalidArgument(`${name} cannot be "." or ".."`);
|
|
189
|
+
return encodeURIComponent(value);
|
|
190
|
+
};
|
|
191
|
+
const TRANSFORM_TYPE_PATTERN = /^[A-Za-z0-9_]+$/;
|
|
192
|
+
/** Percent-encodes everything except RFC 3986 unreserved characters (`A-Z a-z 0-9 - . _ ~`). */
|
|
193
|
+
const encodeTransformComponent = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
194
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
195
|
+
const appendTransformOptions = (pairs, key, value) => {
|
|
196
|
+
if (value === void 0 || value === null) return;
|
|
197
|
+
if (isPlainObject(value)) {
|
|
198
|
+
for (const child of Object.keys(value).sort()) appendTransformOptions(pairs, `${key}[${child}]`, value[child]);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
let text;
|
|
202
|
+
if (typeof value === "string") text = value;
|
|
203
|
+
else if (typeof value === "boolean") text = String(value);
|
|
204
|
+
else if (typeof value === "number" && Number.isFinite(value)) text = String(value);
|
|
205
|
+
else throw invalidArgument(`Transform option "${key}" must be a string, number, boolean or object`);
|
|
206
|
+
pairs.push(`${encodeTransformComponent(key)}=${encodeTransformComponent(text)}`);
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* Serializes structured transforms as a CDN selector, such as
|
|
210
|
+
* `imageTransform[format=png,width=1024]`. Option keys are sorted so equal transforms share a
|
|
211
|
+
* URL; nested keys (`border[color]`) and values are percent-encoded.
|
|
212
|
+
*/
|
|
213
|
+
const serializeTransform = (transform) => {
|
|
214
|
+
const steps = Array.isArray(transform) ? transform : [transform];
|
|
215
|
+
if (steps.length === 0) throw invalidArgument("Transform needs at least one step");
|
|
216
|
+
return steps.map((step) => {
|
|
217
|
+
if (!isPlainObject(step)) throw invalidArgument("Transform steps must be objects");
|
|
218
|
+
if (typeof step.type !== "string" || !TRANSFORM_TYPE_PATTERN.test(step.type)) throw invalidArgument("Transform type must contain only letters, digits and underscores");
|
|
219
|
+
if (step.options !== void 0 && step.options !== null && !isPlainObject(step.options)) throw invalidArgument("Transform options must be an object");
|
|
220
|
+
const pairs = [];
|
|
221
|
+
for (const key of Object.keys(step.options ?? {}).sort()) appendTransformOptions(pairs, key, step.options[key]);
|
|
222
|
+
return `${step.type}[${pairs.join(",")}]`;
|
|
223
|
+
}).join(",");
|
|
224
|
+
};
|
|
225
|
+
/** Returns the selector string the API and CDN accept for a string or structured transform. */
|
|
226
|
+
const transformSelector = (transform) => transform === void 0 || typeof transform === "string" ? transform : serializeTransform(transform);
|
|
227
|
+
/** Appends a CDN lens (`/@name`) or direct transform selector to an asset URL. */
|
|
228
|
+
const appendCdnSelector = (url, options) => {
|
|
229
|
+
if (options.lens && options.transform) throw invalidArgument("Specify either a lens or a transform, not both");
|
|
230
|
+
if (options.lens) return `${url}/@${encodePathSegment(options.lens, "Lens")}`;
|
|
231
|
+
if (!options.transform) return url;
|
|
232
|
+
if (typeof options.transform !== "string") return `${url}/${serializeTransform(options.transform)}`;
|
|
233
|
+
const transform = options.transform.replace(/^\/+/, "");
|
|
234
|
+
if (transform.includes("..") || transform.includes("?") || transform.includes("#")) throw invalidArgument("Transform cannot contain \"..\", \"?\" or \"#\"");
|
|
235
|
+
return `${url}/${encodeURI(transform).replace(/%5B/g, "[").replace(/%5D/g, "]")}`;
|
|
236
|
+
};
|
|
237
|
+
const appendQueryValue = (parts, key, value) => {
|
|
238
|
+
if (value === void 0 || value === null) return;
|
|
239
|
+
if (Array.isArray(value)) {
|
|
240
|
+
for (const [index, item] of value.entries()) appendQueryValue(parts, `${key}[${index}]`, item);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (typeof value === "object") {
|
|
244
|
+
for (const [field, item] of Object.entries(value)) appendQueryValue(parts, `${key}[${field}]`, item);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Serialises a query object the way the API parses it (qs bracket notation), for example
|
|
251
|
+
* `ordering[0][field]=createdAt&ordering[0][direction]=DESC`. Every key and value is
|
|
252
|
+
* percent-encoded and missing values are omitted.
|
|
253
|
+
*/
|
|
254
|
+
const stringifyQuery = (query) => {
|
|
255
|
+
const parts = [];
|
|
256
|
+
for (const [key, value] of Object.entries(query)) appendQueryValue(parts, key, value);
|
|
257
|
+
return parts.join("&");
|
|
258
|
+
};
|
|
259
|
+
//#endregion
|
|
260
|
+
export { getRejectedUploadMimeTypeError as _, toTextAssetOptions as a, validateRequestOptions as c, assetToData as d, getAssetPayloadByteLength as f, MIN_MULTIPART_IMAGE_UPLOAD_BYTES as g, MAX_IMAGE_UPLOAD_BYTES as h, stringifyQuery as i, validateServerClientOptions as l, DOMAIN as m, encodePathSegment as n, transformSelector as o, validateAssetPayload as p, randomUuid as r, validateAppClientOptions as s, appendCdnSelector as t, assetDataToUint8Array as u, isRejectedUploadMimeType as v };
|