@calmlens/sdk 0.11.0 → 0.13.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 +73 -16
- package/cjs/CalmLensConnection-DzRDCMOP.js +233 -0
- package/cjs/CalmLensUtils-vX5H_vMn.js +699 -0
- package/cjs/CalmLensVersion-DjhsJ1gj.js +410 -0
- package/cjs/CalmLensWebhooks-hVquX5L1.js +201 -0
- package/cjs/WebhookPayload-B7CgSmI5.d.ts +11928 -0
- package/cjs/app.d.ts +43 -0
- package/cjs/app.js +91 -0
- package/cjs/index-BywTQbRq.d.ts +352 -0
- package/cjs/index.d.ts +3 -382
- package/cjs/index.js +4 -816
- package/cjs/server.d.ts +210 -0
- package/cjs/server.js +384 -0
- package/cjs/testing.d.ts +59 -18
- package/cjs/testing.js +245 -99
- package/esm/CalmLensConnection-D8VsTdsA.js +192 -0
- package/esm/CalmLensUtils-B4i3Z2He.js +574 -0
- package/esm/CalmLensVersion-DK4BMEST.js +345 -0
- package/esm/CalmLensWebhooks-DyCoEvpf.js +148 -0
- package/esm/WebhookPayload-B7CgSmI5.d.ts +11928 -0
- package/esm/app.d.ts +43 -0
- package/esm/app.js +87 -0
- package/esm/index-BywTQbRq.d.ts +352 -0
- package/esm/index.d.ts +3 -382
- package/esm/index.js +1 -807
- package/esm/server.d.ts +210 -0
- package/esm/server.js +374 -0
- package/esm/testing.d.ts +59 -18
- package/esm/testing.js +195 -49
- package/package.json +23 -1
- package/cjs/CalmLensTypes-BZ-nxhr_.d.ts +0 -8613
- package/cjs/CalmLensWebhooks-CH2PaPs0.js +0 -784
- package/esm/CalmLensTypes-BZ-nxhr_.d.ts +0 -8613
- package/esm/CalmLensWebhooks-3Otbubyf.js +0 -593
|
@@ -0,0 +1,410 @@
|
|
|
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
|
+
SPEND_LIMIT_REACHED: "upload/spend-limit-reached",
|
|
15
|
+
INVALID_UPLOAD_MIME_TYPE: "upload/invalid-mime-type",
|
|
16
|
+
INVALID_EVENT_CURSOR: "events/invalid-cursor",
|
|
17
|
+
INVALID_UPLOAD_PAYLOAD: "upload/invalid-payload",
|
|
18
|
+
USE_UPLOAD_PART: "upload/use-upload-part",
|
|
19
|
+
UPLOAD_NOT_OPEN: "upload/not-open",
|
|
20
|
+
INVALID_UPLOAD_PART_NUMBER: "upload/invalid-part-number",
|
|
21
|
+
UPLOAD_MISSING: "upload/missing",
|
|
22
|
+
UPLOAD_SIZE_MISMATCH: "upload/size-mismatch",
|
|
23
|
+
INVALID_IDEMPOTENCY_KEY: "idempotency/invalid-key",
|
|
24
|
+
IDEMPOTENCY_KEY_REUSED: "idempotency/key-reused",
|
|
25
|
+
IDEMPOTENT_REQUEST_IN_PROGRESS: "idempotency/request-in-progress",
|
|
26
|
+
ASSET_NOT_FOUND: "asset/not-found",
|
|
27
|
+
PROJECT_NOT_FOUND: "project/not-found",
|
|
28
|
+
ASSET_UPDATE_FAILED: "asset/update-failed",
|
|
29
|
+
ASSET_SAFETY_HELD: "asset/safety-held",
|
|
30
|
+
ASSET_NOT_RETRYABLE: "asset/not-retryable",
|
|
31
|
+
ASSET_CONTENT_UNAVAILABLE: "asset/content-unavailable",
|
|
32
|
+
SUPPORT_TICKET_NOT_FOUND: "support/ticket-not-found",
|
|
33
|
+
DUPLICATE_EXTERNAL_ID: "asset/duplicate-external-id",
|
|
34
|
+
FOLDER_NOT_FOUND: "folder/not-found",
|
|
35
|
+
DUPLICATE_FOLDER_NAME: "folder/duplicate-name",
|
|
36
|
+
FOLDER_NOT_EMPTY: "folder/not-empty",
|
|
37
|
+
FOLDER_PARENT_CYCLE: "folder/parent-cycle",
|
|
38
|
+
LENS_VERSION_CONFLICT: "lens/version-conflict",
|
|
39
|
+
UPLOAD_PRESET_NOT_FOUND: "upload-preset/not-found",
|
|
40
|
+
UPLOAD_PRESET_DISABLED: "upload-preset/disabled",
|
|
41
|
+
UPLOAD_PRESET_TOKEN_REQUIRED: "upload-preset/token-required",
|
|
42
|
+
UPLOAD_PRESET_CONTENT_TYPE_NOT_ALLOWED: "upload-preset/content-type-not-allowed",
|
|
43
|
+
UPLOAD_PRESET_TOO_LARGE: "upload-preset/too-large",
|
|
44
|
+
UPLOAD_PRESET_DAILY_LIMIT_REACHED: "upload-preset/daily-limit-reached",
|
|
45
|
+
UPLOAD_PRESET_ORIGIN_NOT_ALLOWED: "upload-preset/origin-not-allowed",
|
|
46
|
+
UPLOAD_PRESET_FIELD_NOT_ALLOWED: "upload-preset/field-not-allowed",
|
|
47
|
+
UPLOAD_TOKEN_INVALID: "upload-token/invalid",
|
|
48
|
+
UPLOAD_TOKEN_EXPIRED: "upload-token/expired",
|
|
49
|
+
UPLOAD_TOKEN_USED: "upload-token/used"
|
|
50
|
+
};
|
|
51
|
+
/** The HTTP status and default message the API returns with each documented error code. */
|
|
52
|
+
const API_ERROR_DEFINITIONS = {
|
|
53
|
+
"auth/missing-token": {
|
|
54
|
+
status: 401,
|
|
55
|
+
message: "Authentication required. Send your project API key in the X-Api-Key header."
|
|
56
|
+
},
|
|
57
|
+
"auth/invalid-api-key": {
|
|
58
|
+
status: 401,
|
|
59
|
+
message: "The API key is invalid, revoked or expired. Create a key under Developer → API Keys."
|
|
60
|
+
},
|
|
61
|
+
"auth/project-mismatch": {
|
|
62
|
+
status: 403,
|
|
63
|
+
message: "This API key belongs to a different project. Use a key created for the project that owns this resource."
|
|
64
|
+
},
|
|
65
|
+
"auth/api-key-not-accepted": {
|
|
66
|
+
status: 403,
|
|
67
|
+
message: "API keys are only accepted by the public API at /v1."
|
|
68
|
+
},
|
|
69
|
+
"upload/file-too-large": {
|
|
70
|
+
status: 400,
|
|
71
|
+
message: "File size exceeds limit. Maximum direct image size is 10MB, audio/video size is 50MiB, and other files is 5GB."
|
|
72
|
+
},
|
|
73
|
+
"upload/image-requires-multipart-upload": {
|
|
74
|
+
status: 400,
|
|
75
|
+
message: "Images of 10MB or more must use a multipart upload."
|
|
76
|
+
},
|
|
77
|
+
"upload/multipart-image-too-small": {
|
|
78
|
+
status: 400,
|
|
79
|
+
message: "Multipart image uploads must total at least 10MB."
|
|
80
|
+
},
|
|
81
|
+
"upload/multipart-image-too-large": {
|
|
82
|
+
status: 400,
|
|
83
|
+
message: "Multipart image uploads cannot exceed 20MB."
|
|
84
|
+
},
|
|
85
|
+
"upload/image-exceeded-max-dimensions": {
|
|
86
|
+
status: 400,
|
|
87
|
+
message: "Image exceeds maximum dimensions. Maximum dimensions are 12,000px."
|
|
88
|
+
},
|
|
89
|
+
"upload/limit-exceeded": {
|
|
90
|
+
status: 403,
|
|
91
|
+
message: "Upload limit exceeded. You have used all your free uploads. Please upgrade to Plus or Pro for unlimited uploads."
|
|
92
|
+
},
|
|
93
|
+
"upload/spend-limit-reached": {
|
|
94
|
+
status: 402,
|
|
95
|
+
message: "The project owner's spend limit for this billing period has been reached. Raise the limit in Billing to resume uploads."
|
|
96
|
+
},
|
|
97
|
+
"upload/invalid-mime-type": {
|
|
98
|
+
status: 400,
|
|
99
|
+
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."
|
|
100
|
+
},
|
|
101
|
+
"events/invalid-cursor": {
|
|
102
|
+
status: 400,
|
|
103
|
+
message: "The events cursor is invalid. Use the cursor from a previous response, or omit it to start from now."
|
|
104
|
+
},
|
|
105
|
+
"upload/invalid-payload": {
|
|
106
|
+
status: 400,
|
|
107
|
+
message: "Upload payload is invalid."
|
|
108
|
+
},
|
|
109
|
+
"upload/use-upload-part": {
|
|
110
|
+
status: 409,
|
|
111
|
+
message: "Use uploadPart for multipart upload parts after the first part"
|
|
112
|
+
},
|
|
113
|
+
"upload/not-open": {
|
|
114
|
+
status: 409,
|
|
115
|
+
message: "Asset is not accepting additional parts"
|
|
116
|
+
},
|
|
117
|
+
"upload/invalid-part-number": {
|
|
118
|
+
status: 409,
|
|
119
|
+
message: "Multipart upload part number is invalid."
|
|
120
|
+
},
|
|
121
|
+
"upload/missing": {
|
|
122
|
+
status: 409,
|
|
123
|
+
message: "Multipart upload state is missing"
|
|
124
|
+
},
|
|
125
|
+
"upload/size-mismatch": {
|
|
126
|
+
status: 400,
|
|
127
|
+
message: "The uploaded parts do not add up to the totalSizeInBytes declared when the multipart upload started."
|
|
128
|
+
},
|
|
129
|
+
"idempotency/invalid-key": {
|
|
130
|
+
status: 400,
|
|
131
|
+
message: "The Idempotency-Key header must be 1 to 255 printable ASCII characters, such as a UUID."
|
|
132
|
+
},
|
|
133
|
+
"idempotency/key-reused": {
|
|
134
|
+
status: 409,
|
|
135
|
+
message: "This Idempotency-Key was already used for a different request. Use a new key for each distinct request."
|
|
136
|
+
},
|
|
137
|
+
"idempotency/request-in-progress": {
|
|
138
|
+
status: 409,
|
|
139
|
+
message: "A request with this Idempotency-Key is still being processed. Retry after the Retry-After delay to receive its result."
|
|
140
|
+
},
|
|
141
|
+
"asset/not-found": {
|
|
142
|
+
status: 404,
|
|
143
|
+
message: "Asset not found"
|
|
144
|
+
},
|
|
145
|
+
"project/not-found": {
|
|
146
|
+
status: 404,
|
|
147
|
+
message: "Project not found"
|
|
148
|
+
},
|
|
149
|
+
"asset/update-failed": {
|
|
150
|
+
status: 500,
|
|
151
|
+
message: "Failed to update asset"
|
|
152
|
+
},
|
|
153
|
+
"asset/safety-held": {
|
|
154
|
+
status: 423,
|
|
155
|
+
message: "Asset access is restricted by a safety hold."
|
|
156
|
+
},
|
|
157
|
+
"asset/not-retryable": {
|
|
158
|
+
status: 409,
|
|
159
|
+
message: "Only assets that failed to process (status error) can be retried."
|
|
160
|
+
},
|
|
161
|
+
"asset/content-unavailable": {
|
|
162
|
+
status: 409,
|
|
163
|
+
message: "This asset's content is no longer stored, so it can't be processed again. Upload it again instead."
|
|
164
|
+
},
|
|
165
|
+
"support/ticket-not-found": {
|
|
166
|
+
status: 404,
|
|
167
|
+
message: "Support ticket not found."
|
|
168
|
+
},
|
|
169
|
+
"asset/duplicate-external-id": {
|
|
170
|
+
status: 409,
|
|
171
|
+
message: "An asset with this externalId already exists in the project."
|
|
172
|
+
},
|
|
173
|
+
"folder/not-found": {
|
|
174
|
+
status: 404,
|
|
175
|
+
message: "Folder not found"
|
|
176
|
+
},
|
|
177
|
+
"folder/duplicate-name": {
|
|
178
|
+
status: 409,
|
|
179
|
+
message: "A folder with this name already exists in this location."
|
|
180
|
+
},
|
|
181
|
+
"folder/not-empty": {
|
|
182
|
+
status: 409,
|
|
183
|
+
message: "Folder must be empty before it can be deleted."
|
|
184
|
+
},
|
|
185
|
+
"folder/parent-cycle": {
|
|
186
|
+
status: 409,
|
|
187
|
+
message: "A folder cannot be moved inside itself or one of its descendants."
|
|
188
|
+
},
|
|
189
|
+
"lens/version-conflict": {
|
|
190
|
+
status: 409,
|
|
191
|
+
message: "Lens updates require a version greater than the current version."
|
|
192
|
+
},
|
|
193
|
+
"upload-preset/not-found": {
|
|
194
|
+
status: 404,
|
|
195
|
+
message: "Upload preset not found. Check the preset ID in the portal."
|
|
196
|
+
},
|
|
197
|
+
"upload-preset/disabled": {
|
|
198
|
+
status: 403,
|
|
199
|
+
message: "This upload preset is paused. Turn it on in the portal to accept uploads."
|
|
200
|
+
},
|
|
201
|
+
"upload-preset/token-required": {
|
|
202
|
+
status: 401,
|
|
203
|
+
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."
|
|
204
|
+
},
|
|
205
|
+
"upload-preset/content-type-not-allowed": {
|
|
206
|
+
status: 415,
|
|
207
|
+
message: "This upload preset doesn't accept this file type."
|
|
208
|
+
},
|
|
209
|
+
"upload-preset/too-large": {
|
|
210
|
+
status: 413,
|
|
211
|
+
message: "This file is larger than the upload preset allows."
|
|
212
|
+
},
|
|
213
|
+
"upload-preset/daily-limit-reached": {
|
|
214
|
+
status: 429,
|
|
215
|
+
message: "This upload preset has reached its daily upload limit. Try again after midnight UTC, or raise the limit in the portal."
|
|
216
|
+
},
|
|
217
|
+
"upload-preset/origin-not-allowed": {
|
|
218
|
+
status: 403,
|
|
219
|
+
message: "This site isn't allowed to upload through this upload preset."
|
|
220
|
+
},
|
|
221
|
+
"upload-preset/field-not-allowed": {
|
|
222
|
+
status: 400,
|
|
223
|
+
message: "This field is set by the upload preset or the upload token and can't be changed by the upload."
|
|
224
|
+
},
|
|
225
|
+
"upload-token/invalid": {
|
|
226
|
+
status: 401,
|
|
227
|
+
message: "The upload token is invalid. Sign a new one on your server with an active API key for this project."
|
|
228
|
+
},
|
|
229
|
+
"upload-token/expired": {
|
|
230
|
+
status: 401,
|
|
231
|
+
message: "The upload token has expired. Sign a new one on your server."
|
|
232
|
+
},
|
|
233
|
+
"upload-token/used": {
|
|
234
|
+
status: 409,
|
|
235
|
+
message: "The upload token has already been used. Each token uploads one asset."
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/client/CalmLensError.ts
|
|
240
|
+
/** Codes for failures the SDK detects itself, before or instead of an API error response. */
|
|
241
|
+
const SDK_ERROR_CODES = {
|
|
242
|
+
/** The request could not be completed: DNS, TLS, a refused or reset connection, a truncated body. */
|
|
243
|
+
TRANSPORT_ERROR: "sdk/transport-error",
|
|
244
|
+
/** The request did not finish within `timeoutMs`. */
|
|
245
|
+
TIMEOUT: "sdk/timeout",
|
|
246
|
+
/** The caller's `AbortSignal` cancelled the request. */
|
|
247
|
+
CANCELLED: "sdk/cancelled",
|
|
248
|
+
/** A successful response whose body is not the JSON the operation returns. */
|
|
249
|
+
INVALID_RESPONSE: "sdk/invalid-response",
|
|
250
|
+
/** An HTTP failure without a CalmLens error body, for example from a proxy. */
|
|
251
|
+
HTTP_ERROR: "sdk/http-error",
|
|
252
|
+
/** A response body larger than `maxResponseBytes`. */
|
|
253
|
+
RESPONSE_TOO_LARGE: "sdk/response-too-large",
|
|
254
|
+
/** A method argument is missing or invalid, for example an empty asset ID. */
|
|
255
|
+
INVALID_ARGUMENT: "sdk/invalid-argument",
|
|
256
|
+
/** The client options are invalid, for example a missing API key. */
|
|
257
|
+
INVALID_CONFIG: "sdk/invalid-config",
|
|
258
|
+
/** No `fetch` implementation is available in this runtime. */
|
|
259
|
+
NO_FETCH: "sdk/no-fetch",
|
|
260
|
+
/** A webhook delivery whose signature or timestamp did not verify. */
|
|
261
|
+
INVALID_WEBHOOK_SIGNATURE: "sdk/invalid-webhook-signature",
|
|
262
|
+
/** A verified webhook delivery whose body is not a webhook event. */
|
|
263
|
+
INVALID_WEBHOOK_PAYLOAD: "sdk/invalid-webhook-payload"
|
|
264
|
+
};
|
|
265
|
+
/** Every error code the SDK can throw: the documented API codes and the SDK's own `sdk/*` codes. */
|
|
266
|
+
const CALMLENS_ERROR_CODES = {
|
|
267
|
+
...API_ERROR_CODES,
|
|
268
|
+
...SDK_ERROR_CODES
|
|
269
|
+
};
|
|
270
|
+
const BRAND = Symbol.for("calmlens.sdk.CalmLensError");
|
|
271
|
+
const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
272
|
+
429,
|
|
273
|
+
502,
|
|
274
|
+
503,
|
|
275
|
+
504
|
|
276
|
+
]);
|
|
277
|
+
const RETRYABLE_CODES = /* @__PURE__ */ new Set([SDK_ERROR_CODES.TRANSPORT_ERROR, API_ERROR_CODES.IDEMPOTENT_REQUEST_IN_PROGRESS]);
|
|
278
|
+
/**
|
|
279
|
+
* Every failure the SDK reports, whether the API returned an error or the request never
|
|
280
|
+
* completed. Branch on `code`; `status`, `requestId`, `headers` and `retryAfter` describe the
|
|
281
|
+
* HTTP response when there was one, and `cause` holds the underlying error.
|
|
282
|
+
*
|
|
283
|
+
* `instanceof CalmLensError` and `CalmLensError.isCalmLensError(value)` both recognise errors
|
|
284
|
+
* from any copy of the SDK, so they work when ESM and CommonJS builds are loaded side by side.
|
|
285
|
+
*/
|
|
286
|
+
var CalmLensError = class CalmLensError extends Error {
|
|
287
|
+
constructor(code, options = {}) {
|
|
288
|
+
const id = typeof code === "string" ? code : code.id;
|
|
289
|
+
const defaultMessage = typeof code === "string" ? void 0 : code.defaultMessage;
|
|
290
|
+
super(`[calmlens] ${options.message ?? defaultMessage ?? id}`, { cause: options.cause });
|
|
291
|
+
this.name = "CalmLensError";
|
|
292
|
+
this.code = id;
|
|
293
|
+
this.status = options.status;
|
|
294
|
+
this.details = options.details;
|
|
295
|
+
this.requestId = options.requestId;
|
|
296
|
+
this.headers = options.headers;
|
|
297
|
+
this.retryAfter = options.retryAfter;
|
|
298
|
+
Object.defineProperty(this, BRAND, { value: true });
|
|
299
|
+
}
|
|
300
|
+
/** True for any `CalmLensError`, including one created by another copy or build of the SDK. */
|
|
301
|
+
static isCalmLensError(value) {
|
|
302
|
+
return typeof value === "object" && value !== null && value[BRAND] === true;
|
|
303
|
+
}
|
|
304
|
+
static [Symbol.hasInstance](value) {
|
|
305
|
+
return CalmLensError.isCalmLensError(value);
|
|
306
|
+
}
|
|
307
|
+
/** The resource does not exist or is not visible to this API key. */
|
|
308
|
+
isNotFound() {
|
|
309
|
+
return this.status === 404;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Whether repeating the same request may succeed: transport failures, throttling (429),
|
|
313
|
+
* gateway errors (502, 503, 504) and idempotent requests still in progress. Only repeat a
|
|
314
|
+
* create or upload with the same `idempotencyKey`.
|
|
315
|
+
*/
|
|
316
|
+
isRetryable() {
|
|
317
|
+
return RETRYABLE_CODES.has(this.code) || this.status !== void 0 && RETRYABLE_STATUSES.has(this.status);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
const invalidArgument = (message, details) => new CalmLensError(SDK_ERROR_CODES.INVALID_ARGUMENT, {
|
|
321
|
+
message,
|
|
322
|
+
details
|
|
323
|
+
});
|
|
324
|
+
const invalidConfig = (message) => new CalmLensError(SDK_ERROR_CODES.INVALID_CONFIG, { message });
|
|
325
|
+
//#endregion
|
|
326
|
+
//#region src/client/CalmLensVersion.ts
|
|
327
|
+
/** This SDK's version, from its package.json. */
|
|
328
|
+
const SDK_VERSION = "0.13.0";
|
|
329
|
+
const runtimeName = () => {
|
|
330
|
+
const runtime = globalThis;
|
|
331
|
+
if (isReactNativeRuntime()) return "react-native";
|
|
332
|
+
if (runtime.Bun?.version) return `bun/${runtime.Bun.version}`;
|
|
333
|
+
if (runtime.Deno?.version?.deno) return `deno/${runtime.Deno.version.deno}`;
|
|
334
|
+
if (runtime.navigator?.userAgent === "Cloudflare-Workers") return "workerd";
|
|
335
|
+
if (runtime.process?.versions?.node) return `node/${runtime.process.versions.node}`;
|
|
336
|
+
return "unknown";
|
|
337
|
+
};
|
|
338
|
+
/** The `User-Agent` the SDK sends from server runtimes, e.g. `calmlens-js/0.10.0 (node/22.12.0)`. */
|
|
339
|
+
const getUserAgent = () => `calmlens-js/${SDK_VERSION} (${runtimeName()})`;
|
|
340
|
+
/** True in a web page, where API keys would be visible to every visitor. */
|
|
341
|
+
const isBrowserRuntime = () => typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
|
|
342
|
+
/** True in a React Native app, where an API key ships inside the app for anyone to extract. */
|
|
343
|
+
const isReactNativeRuntime = () => globalThis.navigator?.product === "ReactNative";
|
|
344
|
+
//#endregion
|
|
345
|
+
Object.defineProperty(exports, "API_ERROR_CODES", {
|
|
346
|
+
enumerable: true,
|
|
347
|
+
get: function() {
|
|
348
|
+
return API_ERROR_CODES;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
Object.defineProperty(exports, "API_ERROR_DEFINITIONS", {
|
|
352
|
+
enumerable: true,
|
|
353
|
+
get: function() {
|
|
354
|
+
return API_ERROR_DEFINITIONS;
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
Object.defineProperty(exports, "CALMLENS_ERROR_CODES", {
|
|
358
|
+
enumerable: true,
|
|
359
|
+
get: function() {
|
|
360
|
+
return CALMLENS_ERROR_CODES;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
Object.defineProperty(exports, "CalmLensError", {
|
|
364
|
+
enumerable: true,
|
|
365
|
+
get: function() {
|
|
366
|
+
return CalmLensError;
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
Object.defineProperty(exports, "SDK_ERROR_CODES", {
|
|
370
|
+
enumerable: true,
|
|
371
|
+
get: function() {
|
|
372
|
+
return SDK_ERROR_CODES;
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
Object.defineProperty(exports, "SDK_VERSION", {
|
|
376
|
+
enumerable: true,
|
|
377
|
+
get: function() {
|
|
378
|
+
return SDK_VERSION;
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
Object.defineProperty(exports, "getUserAgent", {
|
|
382
|
+
enumerable: true,
|
|
383
|
+
get: function() {
|
|
384
|
+
return getUserAgent;
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
Object.defineProperty(exports, "invalidArgument", {
|
|
388
|
+
enumerable: true,
|
|
389
|
+
get: function() {
|
|
390
|
+
return invalidArgument;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
Object.defineProperty(exports, "invalidConfig", {
|
|
394
|
+
enumerable: true,
|
|
395
|
+
get: function() {
|
|
396
|
+
return invalidConfig;
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
Object.defineProperty(exports, "isBrowserRuntime", {
|
|
400
|
+
enumerable: true,
|
|
401
|
+
get: function() {
|
|
402
|
+
return isBrowserRuntime;
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
Object.defineProperty(exports, "isReactNativeRuntime", {
|
|
406
|
+
enumerable: true,
|
|
407
|
+
get: function() {
|
|
408
|
+
return isReactNativeRuntime;
|
|
409
|
+
}
|
|
410
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
const require_CalmLensVersion = require("./CalmLensVersion-DjhsJ1gj.js");
|
|
2
|
+
const require_CalmLensUtils = require("./CalmLensUtils-vX5H_vMn.js");
|
|
3
|
+
//#region ../shared/src/JsonUtils.ts
|
|
4
|
+
const stringifyJsonCanonical = (value) => JSON.stringify(value, (_key, nestedValue) => {
|
|
5
|
+
if (!nestedValue || typeof nestedValue !== "object" || Array.isArray(nestedValue)) return nestedValue;
|
|
6
|
+
return Object.fromEntries(Object.keys(nestedValue).sort().map((key) => [key, nestedValue[key]]));
|
|
7
|
+
});
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region ../shared/src/CryptoUtils.ts
|
|
10
|
+
const encoder = new TextEncoder();
|
|
11
|
+
/** Lowercase hex of `bytes`, two characters per byte. */
|
|
12
|
+
const bytesToHex = (bytes) => Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
13
|
+
/** SHA-256 of the UTF-8 encoding of `value`, as lowercase hex. */
|
|
14
|
+
const sha256Hex = async (value) => bytesToHex(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
15
|
+
/** HMAC-SHA256 of `message` keyed with `secret` (both UTF-8 encoded), as lowercase hex. */
|
|
16
|
+
const hmacSha256Hex = async (secret, message) => {
|
|
17
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
18
|
+
name: "HMAC",
|
|
19
|
+
hash: "SHA-256"
|
|
20
|
+
}, false, ["sign"]);
|
|
21
|
+
return bytesToHex(await crypto.subtle.sign("HMAC", key, encoder.encode(message)));
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Compares two strings without leaking, through timing, where they first differ. Strings of
|
|
25
|
+
* different lengths are unequal immediately (the length itself is not secret here).
|
|
26
|
+
*/
|
|
27
|
+
const timingSafeEqual = (a, b) => {
|
|
28
|
+
if (a.length !== b.length) return false;
|
|
29
|
+
let difference = 0;
|
|
30
|
+
for (let index = 0; index < a.length; index++) difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
|
|
31
|
+
return difference === 0;
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region ../shared/src/UploadToken.ts
|
|
35
|
+
const UPLOAD_TOKEN_PREFIX = "clut_";
|
|
36
|
+
const UPLOAD_TOKEN_MAX_TTL_SECONDS = 3600;
|
|
37
|
+
const SIGNING_CONTEXT = "calmlens-upload-token:v1:";
|
|
38
|
+
const toBase64Url = (value) => btoa(String.fromCharCode(...new TextEncoder().encode(value))).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
39
|
+
/**
|
|
40
|
+
* The part of an API key a token names so the backend can find the key's hash. The same rule as
|
|
41
|
+
* the stored `keyPrefix`: the fixed `clsk_<region>_` part plus four random characters.
|
|
42
|
+
*/
|
|
43
|
+
const uploadTokenKeyPrefix = (apiKey) => apiKey.substring(0, apiKey.startsWith("clsk_") ? 13 : 8);
|
|
44
|
+
/** The signing secret: the SHA-256 of the API key, which the backend stores instead of the key. */
|
|
45
|
+
const signingSecret = (apiKey) => sha256Hex(apiKey);
|
|
46
|
+
/** Signs `claims` with `apiKey` and returns the token. */
|
|
47
|
+
const signUploadToken = async (claims, apiKey) => {
|
|
48
|
+
const body = toBase64Url(JSON.stringify(claims));
|
|
49
|
+
const signature = await hmacSha256Hex(await signingSecret(apiKey), SIGNING_CONTEXT + body);
|
|
50
|
+
return `${UPLOAD_TOKEN_PREFIX}${body}.${signature}`;
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/client/CalmLensUploadTokens.ts
|
|
54
|
+
/**
|
|
55
|
+
* Signs a single-use upload token with `apiKey`, locally. `CalmLensServerClient` and
|
|
56
|
+
* `MockCalmLensServerClient` share it so both check options the same way.
|
|
57
|
+
*/
|
|
58
|
+
const signUploadTokenWithApiKey = async (apiKey, options, { now = Date.now(), nonce = crypto.randomUUID() } = {}) => {
|
|
59
|
+
if (!options || typeof options !== "object") throw require_CalmLensVersion.invalidArgument("Options are required");
|
|
60
|
+
const { uploadPresetId, externalId, expiresInSeconds = 600 } = options;
|
|
61
|
+
require_CalmLensUtils.encodePathSegment(uploadPresetId, "Upload preset ID");
|
|
62
|
+
if (!Number.isInteger(expiresInSeconds) || expiresInSeconds < 1 || expiresInSeconds > 3600) throw require_CalmLensVersion.invalidArgument(`expiresInSeconds must be a whole number from 1 to ${UPLOAD_TOKEN_MAX_TTL_SECONDS}`);
|
|
63
|
+
if (externalId !== void 0 && (typeof externalId !== "string" || !externalId)) throw require_CalmLensVersion.invalidArgument("externalId must be a non-empty string");
|
|
64
|
+
const expiresAt = now + expiresInSeconds * 1e3;
|
|
65
|
+
return {
|
|
66
|
+
token: await signUploadToken({
|
|
67
|
+
v: 1,
|
|
68
|
+
u: uploadPresetId,
|
|
69
|
+
k: uploadTokenKeyPrefix(apiKey),
|
|
70
|
+
e: expiresAt,
|
|
71
|
+
n: nonce,
|
|
72
|
+
...externalId === void 0 ? {} : { x: externalId }
|
|
73
|
+
}, apiKey),
|
|
74
|
+
expiresAt
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/client/CalmLensWebhooks.ts
|
|
79
|
+
const WEBHOOK_DELIVERY_HEADER = "x-calmlens-delivery";
|
|
80
|
+
const WEBHOOK_TIMESTAMP_HEADER = "x-calmlens-timestamp";
|
|
81
|
+
const WEBHOOK_SIGNATURE_HEADER = "x-calmlens-signature";
|
|
82
|
+
/** Deliveries whose timestamp is further than this from the receiver's clock are rejected. */
|
|
83
|
+
const WEBHOOK_SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
84
|
+
const readHeader = (headers, name) => {
|
|
85
|
+
if (!headers) return void 0;
|
|
86
|
+
if (typeof headers.get === "function") return headers.get(name) ?? void 0;
|
|
87
|
+
const record = headers;
|
|
88
|
+
const key = Object.keys(record).find((candidate) => candidate.toLowerCase() === name);
|
|
89
|
+
const value = key === void 0 ? void 0 : record[key];
|
|
90
|
+
return Array.isArray(value) ? value.join(",") : value ?? void 0;
|
|
91
|
+
};
|
|
92
|
+
/** The raw body as text. Parsed objects are refused: re-serializing JSON changes the signed bytes. */
|
|
93
|
+
const payloadToText = (payload) => {
|
|
94
|
+
if (typeof payload === "string") return payload;
|
|
95
|
+
if (payload instanceof ArrayBuffer || ArrayBuffer.isView(payload)) {
|
|
96
|
+
const view = ArrayBuffer.isView(payload) ? new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength) : new Uint8Array(payload);
|
|
97
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(view);
|
|
98
|
+
}
|
|
99
|
+
throw require_CalmLensVersion.invalidArgument("Webhook payload must be the raw request body as a string, Uint8Array/Buffer or ArrayBuffer, not parsed JSON");
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Verifies a webhook delivery: `x-calmlens-signature` must contain a `v1=` HMAC-SHA256 of
|
|
103
|
+
* `"<x-calmlens-timestamp>.<raw body>"`, and the timestamp must be within `toleranceSeconds`
|
|
104
|
+
* (default 300) of now. Returns `false` for anything missing, malformed or stale, and throws
|
|
105
|
+
* `sdk/invalid-argument` when `payload` is not a raw body (for example, parsed JSON).
|
|
106
|
+
*/
|
|
107
|
+
async function verifyWebhookSignature(options) {
|
|
108
|
+
const { payload, secret, toleranceSeconds = 300, now, headers } = options;
|
|
109
|
+
const body = payloadToText(payload);
|
|
110
|
+
const signature = options.signature ?? readHeader(headers, "x-calmlens-signature");
|
|
111
|
+
const timestamp = options.timestamp ?? readHeader(headers, "x-calmlens-timestamp");
|
|
112
|
+
if (!secret || !signature || timestamp === null || timestamp === void 0) return false;
|
|
113
|
+
const timestampText = String(timestamp).trim();
|
|
114
|
+
if (!/^\d+$/.test(timestampText)) return false;
|
|
115
|
+
const nowMs = now === void 0 ? Date.now() : now instanceof Date ? now.getTime() : now;
|
|
116
|
+
if (Math.abs(Math.floor(nowMs / 1e3) - Number(timestampText)) > toleranceSeconds) return false;
|
|
117
|
+
const candidates = signature.split(",").map((part) => part.trim()).filter((part) => part.startsWith("v1=")).map((part) => part.slice(3).toLowerCase());
|
|
118
|
+
if (candidates.length === 0) return false;
|
|
119
|
+
const expected = await hmacSha256Hex(secret, `${timestampText}.${body}`);
|
|
120
|
+
let matched = false;
|
|
121
|
+
for (const candidate of candidates) if (timingSafeEqual(expected, candidate)) matched = true;
|
|
122
|
+
return matched;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Verifies a webhook delivery and parses its body. Throws `sdk/invalid-webhook-signature`
|
|
126
|
+
* when the signature or timestamp does not verify, and `sdk/invalid-webhook-payload` when a
|
|
127
|
+
* verified body is not a webhook event. Narrow the result on `type` for typed `data.facts`.
|
|
128
|
+
*
|
|
129
|
+
* Event types and fact keys added to CalmLens after this SDK version are returned as they are,
|
|
130
|
+
* so keep a default branch when you switch on `type`.
|
|
131
|
+
*/
|
|
132
|
+
async function parseWebhookEvent(options) {
|
|
133
|
+
if (!await verifyWebhookSignature(options)) throw new require_CalmLensVersion.CalmLensError(require_CalmLensVersion.SDK_ERROR_CODES.INVALID_WEBHOOK_SIGNATURE, { message: "Webhook signature or timestamp did not verify" });
|
|
134
|
+
let event;
|
|
135
|
+
try {
|
|
136
|
+
event = JSON.parse(payloadToText(options.payload));
|
|
137
|
+
} catch (cause) {
|
|
138
|
+
throw new require_CalmLensVersion.CalmLensError(require_CalmLensVersion.SDK_ERROR_CODES.INVALID_WEBHOOK_PAYLOAD, {
|
|
139
|
+
message: "Webhook body is not JSON",
|
|
140
|
+
cause
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const candidate = event;
|
|
144
|
+
if (!candidate || typeof candidate !== "object" || typeof candidate.id !== "string" || typeof candidate.type !== "string" || typeof candidate.projectId !== "string" || !candidate.data || typeof candidate.data !== "object") throw new require_CalmLensVersion.CalmLensError(require_CalmLensVersion.SDK_ERROR_CODES.INVALID_WEBHOOK_PAYLOAD, { message: "Webhook body is not a CalmLens event" });
|
|
145
|
+
return event;
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
Object.defineProperty(exports, "WEBHOOK_DELIVERY_HEADER", {
|
|
149
|
+
enumerable: true,
|
|
150
|
+
get: function() {
|
|
151
|
+
return WEBHOOK_DELIVERY_HEADER;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
Object.defineProperty(exports, "WEBHOOK_SIGNATURE_HEADER", {
|
|
155
|
+
enumerable: true,
|
|
156
|
+
get: function() {
|
|
157
|
+
return WEBHOOK_SIGNATURE_HEADER;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
Object.defineProperty(exports, "WEBHOOK_SIGNATURE_TOLERANCE_SECONDS", {
|
|
161
|
+
enumerable: true,
|
|
162
|
+
get: function() {
|
|
163
|
+
return WEBHOOK_SIGNATURE_TOLERANCE_SECONDS;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
Object.defineProperty(exports, "WEBHOOK_TIMESTAMP_HEADER", {
|
|
167
|
+
enumerable: true,
|
|
168
|
+
get: function() {
|
|
169
|
+
return WEBHOOK_TIMESTAMP_HEADER;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
Object.defineProperty(exports, "hmacSha256Hex", {
|
|
173
|
+
enumerable: true,
|
|
174
|
+
get: function() {
|
|
175
|
+
return hmacSha256Hex;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
Object.defineProperty(exports, "parseWebhookEvent", {
|
|
179
|
+
enumerable: true,
|
|
180
|
+
get: function() {
|
|
181
|
+
return parseWebhookEvent;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
Object.defineProperty(exports, "signUploadTokenWithApiKey", {
|
|
185
|
+
enumerable: true,
|
|
186
|
+
get: function() {
|
|
187
|
+
return signUploadTokenWithApiKey;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
Object.defineProperty(exports, "stringifyJsonCanonical", {
|
|
191
|
+
enumerable: true,
|
|
192
|
+
get: function() {
|
|
193
|
+
return stringifyJsonCanonical;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
Object.defineProperty(exports, "verifyWebhookSignature", {
|
|
197
|
+
enumerable: true,
|
|
198
|
+
get: function() {
|
|
199
|
+
return verifyWebhookSignature;
|
|
200
|
+
}
|
|
201
|
+
});
|