@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,1114 @@
1
+ import { r as VIEWER_CLAIMS_SCHEMA } from "./Jurisdiction-FusEXb4U-1ov6m5yE.js";
2
+ // src/client/CalmLensErrorCodes.ts
3
+ /** Error codes the CalmLens API documents, by name. */
4
+ const API_ERROR_CODES = {
5
+ INVALID_VIEWER_CLAIMS: "viewer/invalid-claims",
6
+ AGE_CONFIRMATION_REQUIRED: "viewer/age-confirmation-required",
7
+ INVALID_VIEWER_REGION: "viewer/invalid-region",
8
+ VIEWER_REGION_REQUIRED: "viewer/region-required",
9
+ REGIONAL_EVIDENCE_REQUIRED: "asset/regional-evidence-required",
10
+ INVALID_TYPE: "validation/invalid-type",
11
+ TOO_SMALL: "validation/too-small",
12
+ TOO_BIG: "validation/too-big",
13
+ INVALID_FORMAT: "validation/invalid-format",
14
+ INVALID_VALUE: "validation/invalid-value",
15
+ UNKNOWN_FIELD: "validation/unknown-field",
16
+ UNSUPPORTED_CONTENT_TYPE: "validation/unsupported-content-type",
17
+ INVALID_FILTER: "validation/invalid-filter",
18
+ INVALID_ORDERING_FIELD: "validation/invalid-ordering-field",
19
+ ROUTE_NOT_FOUND: "misc/route-not-found",
20
+ INTERNAL_ERROR: "misc/internal-error",
21
+ UNAUTHENTICATED: "auth/unauthenticated",
22
+ TOKEN_EXPIRED: "auth/token-expired",
23
+ INVALID_TOKEN: "auth/invalid-token",
24
+ SEARCH_UNAVAILABLE: "search/unavailable",
25
+ INVALID_TRANSFORM: "asset/invalid-transform",
26
+ INVALID_TRANSFORM_OPTIONS: "asset/invalid-transform-options",
27
+ REMOTE_URL_REDIRECT_LIMIT: "asset/remote-url-redirect-limit",
28
+ COPYRIGHT_CONTENT_HELD: "copyright/content-held",
29
+ DOWNLOAD_PERMISSION_REQUIRED: "asset/api-key-missing-permission",
30
+ INVALID_CURSOR: "validation/invalid-cursor",
31
+ MISSING_TOKEN: "auth/missing-token",
32
+ INVALID_API_KEY: "auth/invalid-api-key",
33
+ API_KEY_PROJECT_MISMATCH: "auth/project-mismatch",
34
+ API_KEY_NOT_ACCEPTED: "auth/api-key-not-accepted",
35
+ PERMISSION_DENIED: "auth/permission-denied",
36
+ FILE_TOO_LARGE: "upload/file-too-large",
37
+ MULTIPART_IMAGE_TOO_SMALL: "upload/multipart-image-too-small",
38
+ MULTIPART_IMAGE_TOO_LARGE: "upload/multipart-image-too-large",
39
+ IMAGE_EXCEEDED_MAX_DIMENSIONS: "upload/image-exceeded-max-dimensions",
40
+ UPLOAD_LIMIT_EXCEEDED: "upload/limit-exceeded",
41
+ SPEND_LIMIT_REACHED: "upload/spend-limit-reached",
42
+ INVALID_UPLOAD_MIME_TYPE: "upload/invalid-mime-type",
43
+ INVALID_UPLOAD_PAYLOAD: "upload/invalid-payload",
44
+ USE_UPLOAD_PART: "upload/use-upload-part",
45
+ UPLOAD_NOT_OPEN: "upload/not-open",
46
+ INVALID_UPLOAD_PART_NUMBER: "upload/invalid-part-number",
47
+ UPLOAD_MISSING: "upload/missing",
48
+ UPLOAD_SIZE_MISMATCH: "upload/size-mismatch",
49
+ INVALID_IDEMPOTENCY_KEY: "idempotency/invalid-key",
50
+ IDEMPOTENCY_KEY_REUSED: "idempotency/key-reused",
51
+ IDEMPOTENT_REQUEST_IN_PROGRESS: "idempotency/request-in-progress",
52
+ ASSET_NOT_FOUND: "asset/not-found",
53
+ PROJECT_NOT_FOUND: "project/not-found",
54
+ ASSET_REVIEW_CHANGED: "asset/review-changed",
55
+ ASSET_REVIEW_NOTE_REQUIRED: "asset/review-note-required",
56
+ ASSET_UPDATE_FAILED: "asset/update-failed",
57
+ ASSET_SAFETY_HELD: "asset/safety-held",
58
+ ASSET_PROCESSING: "asset/processing",
59
+ ASSET_DECISION_NOT_ALLOWED: "asset/decision-not-allowed",
60
+ ASSET_DECISION_UNCHANGED: "asset/decision-unchanged",
61
+ ASSET_NOT_RETRYABLE: "asset/not-retryable",
62
+ ASSET_CONTENT_UNAVAILABLE: "asset/content-unavailable",
63
+ ASSET_CONTENT_PURGED: "asset/content-purged",
64
+ ASSET_NOT_DECIDED: "asset/not-decided",
65
+ ASSET_POLICY_CHANGED: "asset/policy-changed",
66
+ ASSET_REVIEWED: "asset/reviewed",
67
+ SUPPORT_TICKET_NOT_FOUND: "support/ticket-not-found",
68
+ DUPLICATE_EXTERNAL_ID: "asset/duplicate-external-id",
69
+ FOLDER_NOT_FOUND: "folder/not-found",
70
+ DUPLICATE_FOLDER_NAME: "folder/duplicate-name",
71
+ FOLDER_NOT_EMPTY: "folder/not-empty",
72
+ FOLDER_PARENT_CYCLE: "folder/parent-cycle",
73
+ LENS_VERSION_CONFLICT: "lens/version-conflict",
74
+ LENS_IDENTIFIER_RESERVED: "lens/identifier-reserved",
75
+ HASH_LIST_NOT_FOUND: "hash-list/not-found",
76
+ HASH_LIST_ENTRY_NOT_FOUND: "hash-list/entry-not-found",
77
+ HASH_LIST_LIMIT_REACHED: "hash-list/limit-reached",
78
+ HASH_LIST_ENTRY_LIMIT_REACHED: "hash-list/entry-limit-reached",
79
+ HASH_LIST_ASSET_NOT_HASHED: "hash-list/asset-not-hashed",
80
+ UPLOAD_PRESET_NOT_FOUND: "upload-preset/not-found",
81
+ UPLOAD_PRESET_DISABLED: "upload-preset/disabled",
82
+ UPLOAD_PRESET_TOKEN_REQUIRED: "upload-preset/token-required",
83
+ REMOTE_URL_NOT_ALLOWED: "upload/remote-url-not-allowed",
84
+ WEBSITE_RATE_LIMIT_EXCEEDED: "upload/website-rate-limit-exceeded",
85
+ UPLOAD_PRESET_CONTENT_TYPE_NOT_ALLOWED: "upload-preset/content-type-not-allowed",
86
+ UPLOAD_PRESET_TOO_LARGE: "upload-preset/too-large",
87
+ UPLOAD_PRESET_DAILY_LIMIT_REACHED: "upload-preset/daily-limit-reached",
88
+ UPLOAD_PRESET_ORIGIN_NOT_ALLOWED: "upload-preset/origin-not-allowed",
89
+ UPLOAD_PRESET_FIELD_NOT_ALLOWED: "upload-preset/field-not-allowed",
90
+ UPLOAD_TOKEN_INVALID: "upload-token/invalid",
91
+ UPLOAD_TOKEN_EXPIRED: "upload-token/expired",
92
+ UPLOAD_TOKEN_USED: "upload-token/used",
93
+ INVALID_REMOTE_URL: "asset/invalid-remote-url",
94
+ REMOTE_URL_UNAVAILABLE: "asset/remote-url-unavailable",
95
+ APPEAL_NOT_FOUND: "appeal/not-found",
96
+ APPEAL_JUDGMENT_NOT_FOUND: "appeal/judgment-not-found",
97
+ APPEAL_ASSET_NOT_FOUND: "appeal/asset-not-found",
98
+ APPEAL_PERMISSION_DENIED: "appeal/permission-denied",
99
+ APPEAL_ALREADY_REVIEWED: "appeal/already-reviewed",
100
+ APPEAL_DECISION_CHANGED: "appeal/decision-changed",
101
+ APPEAL_INVALID_ACTION: "appeal/invalid-action",
102
+ APPEAL_SAFETY_HELD: "appeal/safety-held",
103
+ APPEAL_CONTENT_UNAVAILABLE: "appeal/content-unavailable"
104
+ };
105
+ /** The HTTP status and default message the API returns with each documented error code. */
106
+ const API_ERROR_DEFINITIONS = {
107
+ "viewer/invalid-claims": {
108
+ status: 400,
109
+ message: "Supply a viewerClaims object with an optional region and boolean ageConfirmed."
110
+ },
111
+ "viewer/age-confirmation-required": {
112
+ status: 403,
113
+ message: "Your application has not confirmed age-restricted delivery for this viewer."
114
+ },
115
+ "viewer/invalid-region": {
116
+ status: 400,
117
+ message: "Supply the viewer's ISO country code, such as DE or GB."
118
+ },
119
+ "viewer/region-required": {
120
+ status: 403,
121
+ message: "Supply the viewer region when regional delivery controls apply."
122
+ },
123
+ "asset/regional-evidence-required": {
124
+ status: 403,
125
+ message: "This asset needs moderation evidence for the newly served jurisdiction."
126
+ },
127
+ "validation/invalid-type": {
128
+ status: 400,
129
+ message: "A field has the wrong type. details.issues lists each problem."
130
+ },
131
+ "validation/too-small": {
132
+ status: 400,
133
+ message: "A value is below its minimum. details.issues lists each problem."
134
+ },
135
+ "validation/too-big": {
136
+ status: 400,
137
+ message: "A value is above its maximum. details.issues lists each problem."
138
+ },
139
+ "validation/invalid-format": {
140
+ status: 400,
141
+ message: "A string is not in the required format, such as an ID or URL. details.issues lists each problem."
142
+ },
143
+ "validation/invalid-value": {
144
+ status: 400,
145
+ message: "A field has a value it cannot take. details.issues lists each problem."
146
+ },
147
+ "validation/unknown-field": {
148
+ status: 400,
149
+ message: "The request has a field this endpoint does not accept."
150
+ },
151
+ "validation/unsupported-content-type": {
152
+ status: 415,
153
+ message: "Send the request body as application/json."
154
+ },
155
+ "validation/invalid-filter": {
156
+ status: 400,
157
+ message: "The filter has invalid syntax or names a field that can't be filtered."
158
+ },
159
+ "validation/invalid-ordering-field": {
160
+ status: 400,
161
+ message: "The list cannot be ordered by this field."
162
+ },
163
+ "misc/route-not-found": {
164
+ status: 404,
165
+ message: "No endpoint matches this method and path."
166
+ },
167
+ "misc/internal-error": {
168
+ status: 500,
169
+ message: "An internal error occurred. Retry later, and quote requestId to support."
170
+ },
171
+ "auth/unauthenticated": {
172
+ status: 401,
173
+ message: "Authentication required."
174
+ },
175
+ "auth/token-expired": {
176
+ status: 401,
177
+ message: "The session token has expired. Sign in again."
178
+ },
179
+ "auth/invalid-token": {
180
+ status: 401,
181
+ message: "The session token is invalid. Sign in again."
182
+ },
183
+ "search/unavailable": {
184
+ status: 503,
185
+ message: "Search is temporarily unavailable. Retry later."
186
+ },
187
+ "asset/invalid-transform": {
188
+ status: 400,
189
+ message: "The transform names an unknown type or is not a valid selector."
190
+ },
191
+ "asset/invalid-transform-options": {
192
+ status: 400,
193
+ message: "The transform's options are invalid for its type."
194
+ },
195
+ "asset/remote-url-redirect-limit": {
196
+ status: 400,
197
+ message: "The remote URL redirected too many times."
198
+ },
199
+ "copyright/content-held": {
200
+ status: 409,
201
+ message: "The asset's content is held for a copyright claim and cannot be changed."
202
+ },
203
+ "asset/api-key-missing-permission": {
204
+ status: 403,
205
+ message: "This API key needs assets:download permission for this asset's folder."
206
+ },
207
+ "validation/invalid-cursor": {
208
+ status: 400,
209
+ message: "The cursor is invalid or belongs to a different query."
210
+ },
211
+ "auth/missing-token": {
212
+ status: 401,
213
+ message: "Authentication required. Send your project API key in the X-Api-Key header."
214
+ },
215
+ "auth/invalid-api-key": {
216
+ status: 401,
217
+ message: "The API key is invalid, revoked or expired. Create a key under Developer → API Keys."
218
+ },
219
+ "auth/project-mismatch": {
220
+ status: 403,
221
+ message: "This API key belongs to a different project. Use a key created for the project that owns this resource."
222
+ },
223
+ "auth/api-key-not-accepted": {
224
+ status: 403,
225
+ message: "API keys are only accepted by the public API at /v1."
226
+ },
227
+ "auth/permission-denied": {
228
+ status: 403,
229
+ message: "This API key lacks a permission the request needs, such as assets:delete for an asset's folder, or the resource belongs to another project."
230
+ },
231
+ "upload/file-too-large": {
232
+ status: 400,
233
+ message: "File size exceeds limit. Maximum direct image size is 10 MiB, audio/video size is 50 MiB, and other files is 5,000 MiB."
234
+ },
235
+ "upload/multipart-image-too-small": {
236
+ status: 400,
237
+ message: "Multipart image uploads must total at least 10 MiB."
238
+ },
239
+ "upload/multipart-image-too-large": {
240
+ status: 400,
241
+ message: "Multipart image uploads cannot exceed 20 MiB."
242
+ },
243
+ "upload/image-exceeded-max-dimensions": {
244
+ status: 400,
245
+ message: "Image exceeds maximum dimensions. Maximum dimensions are 12,000px."
246
+ },
247
+ "upload/limit-exceeded": {
248
+ status: 403,
249
+ message: "Upload limit exceeded. This project's organisation has used its free uploads. Set up billing to continue uploading."
250
+ },
251
+ "upload/spend-limit-reached": {
252
+ status: 402,
253
+ message: "Uploads to this project are paused by its organisation's usage limits. See details.reason; an owner can review them in Billing."
254
+ },
255
+ "upload/invalid-mime-type": {
256
+ status: 400,
257
+ 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."
258
+ },
259
+ "upload/invalid-payload": {
260
+ status: 400,
261
+ message: "Upload payload is invalid."
262
+ },
263
+ "upload/use-upload-part": {
264
+ status: 409,
265
+ message: "Use uploadPart for multipart upload parts after the first part"
266
+ },
267
+ "upload/not-open": {
268
+ status: 409,
269
+ message: "Asset is not accepting additional parts"
270
+ },
271
+ "upload/invalid-part-number": {
272
+ status: 409,
273
+ message: "Multipart upload part number is invalid."
274
+ },
275
+ "upload/missing": {
276
+ status: 409,
277
+ message: "Multipart upload state is missing"
278
+ },
279
+ "upload/size-mismatch": {
280
+ status: 400,
281
+ message: "The uploaded parts do not add up to the totalSizeInBytes declared when the multipart upload started."
282
+ },
283
+ "idempotency/invalid-key": {
284
+ status: 400,
285
+ message: "The Idempotency-Key header must be 1 to 255 printable ASCII characters, such as a UUID."
286
+ },
287
+ "idempotency/key-reused": {
288
+ status: 409,
289
+ message: "This Idempotency-Key was already used for a different request. Use a new key for each distinct request."
290
+ },
291
+ "idempotency/request-in-progress": {
292
+ status: 409,
293
+ message: "A request with this Idempotency-Key is still being processed. Retry after the Retry-After delay to receive its result."
294
+ },
295
+ "asset/not-found": {
296
+ status: 404,
297
+ message: "Asset not found"
298
+ },
299
+ "project/not-found": {
300
+ status: 404,
301
+ message: "Project not found"
302
+ },
303
+ "asset/review-changed": {
304
+ status: 409,
305
+ message: "The asset changed during review. Refresh and review the current decision."
306
+ },
307
+ "asset/review-note-required": {
308
+ status: 400,
309
+ message: "A note is required when changing the decision."
310
+ },
311
+ "asset/update-failed": {
312
+ status: 500,
313
+ message: "Failed to update asset"
314
+ },
315
+ "asset/safety-held": {
316
+ status: 423,
317
+ message: "Asset access is restricted by a safety hold."
318
+ },
319
+ "asset/processing": {
320
+ status: 409,
321
+ message: "Wait for the asset to finish uploading and processing before deciding it."
322
+ },
323
+ "asset/decision-not-allowed": {
324
+ status: 409,
325
+ message: "This asset has no moderation decision yet, so it can only be blocked or purged. Allow, limit or age-restrict it once moderation has decided it."
326
+ },
327
+ "asset/decision-unchanged": {
328
+ status: 409,
329
+ message: "The asset already has this moderation status."
330
+ },
331
+ "asset/not-retryable": {
332
+ status: 409,
333
+ message: "Only assets that failed to process (status error) can be retried."
334
+ },
335
+ "asset/content-unavailable": {
336
+ status: 409,
337
+ message: "This asset's content is no longer stored, so it can't be processed again. Upload it again instead."
338
+ },
339
+ "asset/content-purged": {
340
+ status: 409,
341
+ message: "This asset's content was purged, so it can only be blocked. Upload it again to have it moderated afresh."
342
+ },
343
+ "asset/not-decided": {
344
+ status: 409,
345
+ message: "This asset has no automatic moderation decision to explain yet."
346
+ },
347
+ "asset/policy-changed": {
348
+ status: 409,
349
+ message: "The project's moderation policy has changed since this decision, so it can't be explained as made. Re-run moderation with the current policy instead."
350
+ },
351
+ "asset/reviewed": {
352
+ status: 409,
353
+ message: "A reviewer has decided this asset, so its moderation can't be re-run."
354
+ },
355
+ "support/ticket-not-found": {
356
+ status: 404,
357
+ message: "Support ticket not found."
358
+ },
359
+ "asset/duplicate-external-id": {
360
+ status: 409,
361
+ message: "An asset with this externalId already exists in the project."
362
+ },
363
+ "folder/not-found": {
364
+ status: 404,
365
+ message: "Folder not found"
366
+ },
367
+ "folder/duplicate-name": {
368
+ status: 409,
369
+ message: "A folder with this name already exists in this location."
370
+ },
371
+ "folder/not-empty": {
372
+ status: 409,
373
+ message: "Folder must be empty before it can be deleted."
374
+ },
375
+ "folder/parent-cycle": {
376
+ status: 409,
377
+ message: "A folder cannot be moved inside itself or one of its descendants."
378
+ },
379
+ "lens/version-conflict": {
380
+ status: 409,
381
+ message: "Lens updates require a version greater than the current version."
382
+ },
383
+ "lens/identifier-reserved": {
384
+ status: 409,
385
+ message: "The identifier belongs to a built-in lens (thumbnail, sm, md, lg, xl or 2xl). Give the lens another identifier."
386
+ },
387
+ "hash-list/not-found": {
388
+ status: 404,
389
+ message: "Hash list not found. Check the list ID in the portal."
390
+ },
391
+ "hash-list/entry-not-found": {
392
+ status: 404,
393
+ message: "Hash list entry not found."
394
+ },
395
+ "hash-list/limit-reached": {
396
+ status: 409,
397
+ message: "This project has the most hash lists it can hold. Delete a list to add another."
398
+ },
399
+ "hash-list/entry-limit-reached": {
400
+ status: 409,
401
+ message: "This project's hash lists hold the most entries they can. Remove entries to add more."
402
+ },
403
+ "hash-list/asset-not-hashed": {
404
+ status: 409,
405
+ message: "This asset has no perceptual hash. Only image assets that finished processing can be added."
406
+ },
407
+ "upload-preset/not-found": {
408
+ status: 404,
409
+ message: "Upload preset not found. Check the preset ID in the portal."
410
+ },
411
+ "upload-preset/disabled": {
412
+ status: 403,
413
+ message: "This upload preset is paused. Turn it on in the portal to accept uploads."
414
+ },
415
+ "upload-preset/token-required": {
416
+ status: 401,
417
+ 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."
418
+ },
419
+ "upload/remote-url-not-allowed": {
420
+ status: 403,
421
+ message: "This project doesn't accept URLs from this domain. Change the allowed or blocked domains under Project settings."
422
+ },
423
+ "upload/website-rate-limit-exceeded": {
424
+ status: 429,
425
+ message: "This project has submitted too many websites in the last minute. Wait for the Retry-After period, or lower the pace of submissions."
426
+ },
427
+ "upload-preset/content-type-not-allowed": {
428
+ status: 415,
429
+ message: "This upload preset doesn't accept this file type."
430
+ },
431
+ "upload-preset/too-large": {
432
+ status: 413,
433
+ message: "This file is larger than the upload preset allows."
434
+ },
435
+ "upload-preset/daily-limit-reached": {
436
+ status: 429,
437
+ message: "You have reached this upload preset's daily upload limit. Try again after midnight UTC."
438
+ },
439
+ "upload-preset/origin-not-allowed": {
440
+ status: 403,
441
+ message: "This site isn't allowed to upload through this upload preset."
442
+ },
443
+ "upload-preset/field-not-allowed": {
444
+ status: 400,
445
+ message: "This field is set by the upload preset or the upload token and can't be changed by the upload."
446
+ },
447
+ "upload-token/invalid": {
448
+ status: 401,
449
+ message: "The upload token is invalid. Sign a new one on your server with an active API key for this project."
450
+ },
451
+ "upload-token/expired": {
452
+ status: 401,
453
+ message: "The upload token has expired. Sign a new one on your server."
454
+ },
455
+ "upload-token/used": {
456
+ status: 409,
457
+ message: "The upload token has already been used. Each token uploads one asset."
458
+ },
459
+ "asset/invalid-remote-url": {
460
+ status: 400,
461
+ message: "Remote URLs must use HTTP or HTTPS and cannot target private networks, localhost names or URL credentials."
462
+ },
463
+ "asset/remote-url-unavailable": {
464
+ status: 422,
465
+ message: "The URL responded with an error status. Check that it is publicly reachable."
466
+ },
467
+ "appeal/not-found": {
468
+ status: 404,
469
+ message: "The appeal does not exist in this project."
470
+ },
471
+ "appeal/judgment-not-found": {
472
+ status: 404,
473
+ message: "The judgment event is unavailable, belongs to another project or cannot supply an asset decision. Supply explicit context instead."
474
+ },
475
+ "appeal/asset-not-found": {
476
+ status: 404,
477
+ message: "The linked asset does not exist in this project."
478
+ },
479
+ "appeal/permission-denied": {
480
+ status: 403,
481
+ message: "You cannot review this project's appeals."
482
+ },
483
+ "appeal/already-reviewed": {
484
+ status: 409,
485
+ message: "A reviewer already resolved this appeal."
486
+ },
487
+ "appeal/decision-changed": {
488
+ status: 409,
489
+ message: "The linked asset's decision changed after the appealed decision. Submit a new appeal for the current decision."
490
+ },
491
+ "appeal/invalid-action": {
492
+ status: 400,
493
+ message: "The decision's action is not valid for this appeal."
494
+ },
495
+ "appeal/safety-held": {
496
+ status: 409,
497
+ message: "A safety hold prevents this appeal from being submitted or applied."
498
+ },
499
+ "appeal/content-unavailable": {
500
+ status: 409,
501
+ message: "The appeal's content is unavailable, or the asset's content was purged, so it can only be blocked."
502
+ }
503
+ };
504
+ // src/client/CalmLensError.ts
505
+ /** Codes for failures the SDK detects itself, before or instead of an API error response. */
506
+ const SDK_ERROR_CODES = {
507
+ /** The request could not be completed: DNS, TLS, a refused or reset connection, a truncated body. */
508
+ TRANSPORT_ERROR: "sdk/transport-error",
509
+ /** The request did not finish within `timeoutMs`. */
510
+ TIMEOUT: "sdk/timeout",
511
+ /** The caller's `AbortSignal` cancelled the request. */
512
+ CANCELLED: "sdk/cancelled",
513
+ /** A successful response whose body is not the JSON the operation returns. */
514
+ INVALID_RESPONSE: "sdk/invalid-response",
515
+ /** An HTTP failure without a CalmLens error body, for example from a proxy, or a redirect. */
516
+ HTTP_ERROR: "sdk/http-error",
517
+ /** A response body larger than `maxResponseBytes`. */
518
+ RESPONSE_TOO_LARGE: "sdk/response-too-large",
519
+ /** A method argument is missing or invalid, for example an empty asset ID. */
520
+ INVALID_ARGUMENT: "sdk/invalid-argument",
521
+ /** The client options are invalid, for example a missing API key. */
522
+ INVALID_CONFIG: "sdk/invalid-config",
523
+ /** No `fetch` implementation is available in this runtime. */
524
+ NO_FETCH: "sdk/no-fetch",
525
+ /** A webhook delivery whose signature or timestamp did not verify. */
526
+ INVALID_WEBHOOK_SIGNATURE: "sdk/invalid-webhook-signature",
527
+ /** A verified webhook delivery whose body is not a webhook event. */
528
+ INVALID_WEBHOOK_PAYLOAD: "sdk/invalid-webhook-payload"
529
+ };
530
+ /** Every error code the SDK can throw: the documented API codes and the SDK's own `sdk/*` codes. */
531
+ const CALMLENS_ERROR_CODES = {
532
+ ...API_ERROR_CODES,
533
+ ...SDK_ERROR_CODES
534
+ };
535
+ const BRAND = Symbol.for("calmlens.sdk.CalmLensError");
536
+ const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
537
+ 429,
538
+ 502,
539
+ 503,
540
+ 504
541
+ ]);
542
+ const RETRYABLE_CODES = /* @__PURE__ */ new Set([
543
+ SDK_ERROR_CODES.TRANSPORT_ERROR,
544
+ SDK_ERROR_CODES.TIMEOUT,
545
+ API_ERROR_CODES.IDEMPOTENT_REQUEST_IN_PROGRESS
546
+ ]);
547
+ /**
548
+ * Every failure the SDK reports, whether the API returned an error or the request never
549
+ * completed. Branch on `code`; `status`, `requestId`, `headers` and `retryAfter` describe the
550
+ * HTTP response when there was one, and `cause` holds the underlying error.
551
+ *
552
+ * `instanceof CalmLensError` and `CalmLensError.isCalmLensError(value)` both recognise errors
553
+ * from any copy of the SDK, so they work when ESM and CommonJS builds are loaded side by side.
554
+ */
555
+ var CalmLensError = class CalmLensError extends Error {
556
+ constructor(code, options = {}) {
557
+ const id = typeof code === "string" ? code : code.id;
558
+ const defaultMessage = typeof code === "string" ? void 0 : code.defaultMessage;
559
+ super(`[calmlens] ${options.message ?? defaultMessage ?? id}`, { cause: options.cause });
560
+ this.name = "CalmLensError";
561
+ this.code = id;
562
+ this.status = options.status;
563
+ this.details = options.details;
564
+ this.requestId = options.requestId;
565
+ this.headers = options.headers;
566
+ this.retryAfter = options.retryAfter;
567
+ Object.defineProperty(this, BRAND, { value: true });
568
+ }
569
+ /** True for any `CalmLensError`, including one created by another copy or build of the SDK. */
570
+ static isCalmLensError(value) {
571
+ return typeof value === "object" && value !== null && value[BRAND] === true;
572
+ }
573
+ static [Symbol.hasInstance](value) {
574
+ return CalmLensError.isCalmLensError(value);
575
+ }
576
+ /** The resource does not exist or is not visible to this API key. */
577
+ isNotFound() {
578
+ return this.status === 404;
579
+ }
580
+ /**
581
+ * Whether repeating the same request may succeed: transport failures, timeouts (a request's
582
+ * `timeoutMs` or `waitForDecision`'s deadline), throttling (429), gateway errors (502, 503, 504)
583
+ * and idempotent requests still in progress. Only repeat a create or upload with the same
584
+ * `idempotencyKey`.
585
+ */
586
+ isRetryable() {
587
+ return RETRYABLE_CODES.has(this.code) || this.status !== void 0 && RETRYABLE_STATUSES.has(this.status);
588
+ }
589
+ };
590
+ const invalidArgument = (message, details) => new CalmLensError(SDK_ERROR_CODES.INVALID_ARGUMENT, {
591
+ message,
592
+ details
593
+ });
594
+ const invalidConfig = (message) => new CalmLensError(SDK_ERROR_CODES.INVALID_CONFIG, { message });
595
+ // src/contract/SharedConstants-CyiUWW66.js
596
+ const IMPERSONATION_COOKIE_NAME = "CL_impersonation";
597
+ const DAY_MS = 864e5;
598
+ const YEAR_MS = 365 * DAY_MS;
599
+ const FIRST_PARTY_COOKIES = {
600
+ ACCEPTED_COOKIES_VERSION: {
601
+ name: "CL_acceptedCookiesVersion",
602
+ description: "Records your cookie choice: the version of this cookie list you answered and whether you allowed functional and analytics cookies",
603
+ expirationDescription: "1 year",
604
+ expirationMs: YEAR_MS,
605
+ type: "essential",
606
+ providerName: "CalmLens"
607
+ },
608
+ EVER_LOGGED_IN: {
609
+ name: "CL_everLoggedIn",
610
+ description: "Remembers which sign-in method you last used on this device so the sign-in page can suggest it",
611
+ expirationDescription: "10 years",
612
+ type: "functional",
613
+ providerName: "CalmLens"
614
+ },
615
+ ID_TOKEN: {
616
+ name: "CL_idToken",
617
+ description: "Authentication token that keeps you signed in (HttpOnly)",
618
+ expirationDescription: "When the token expires (about 1 hour); renewed while you are signed in",
619
+ type: "essential",
620
+ providerName: "CalmLens"
621
+ },
622
+ ID_TOKEN_EXPIRES_AT: {
623
+ name: "CL_idTokenExpiresAt",
624
+ description: "When the current authentication token expires, so it can be renewed in time",
625
+ expirationDescription: "10 years, or until you sign out",
626
+ type: "essential",
627
+ providerName: "CalmLens"
628
+ },
629
+ REFRESH_TOKEN: {
630
+ name: "CL_refreshToken",
631
+ description: "Token used to renew your sign-in without asking for your password again (HttpOnly)",
632
+ expirationDescription: "10 years, or until you sign out",
633
+ type: "essential",
634
+ providerName: "CalmLens"
635
+ },
636
+ LOGGED_IN: {
637
+ name: "CL_loggedIn",
638
+ description: "Indicates that you are signed in, so pages can show the signed-in view",
639
+ expirationDescription: "10 years, or until you sign out",
640
+ type: "essential",
641
+ providerName: "CalmLens"
642
+ },
643
+ IMPERSONATION: {
644
+ name: IMPERSONATION_COOKIE_NAME,
645
+ description: "Staff support access only: lets an authorised CalmLens staff member view the service as a specific user for support, security, safety or legal reasons. Only honoured for staff accounts and never set for other users",
646
+ expirationDescription: "Session",
647
+ type: "essential",
648
+ providerName: "CalmLens"
649
+ },
650
+ MAINTENANCE_BYPASS: {
651
+ name: "CL_maintenanceBypass",
652
+ description: "Staff only: allows authorised staff to use the site while it is in maintenance mode (HttpOnly)",
653
+ expirationDescription: "1 hour",
654
+ expirationMs: 36e5,
655
+ type: "essential",
656
+ providerName: "CalmLens"
657
+ },
658
+ WAITLIST_BYPASS: {
659
+ name: "CL_waitlistBypass",
660
+ description: "Staff only: allows authorised staff to use the site while access is limited to the waitlist (HttpOnly)",
661
+ expirationDescription: "1 hour",
662
+ expirationMs: 36e5,
663
+ type: "essential",
664
+ providerName: "CalmLens"
665
+ },
666
+ SIGN_IN_CODE: {
667
+ name: "CL_signInCode",
668
+ description: "Records that you entered the sign-in code while access is limited to the waitlist, so you can sign in (HttpOnly)",
669
+ expirationDescription: "30 days",
670
+ expirationMs: 30 * DAY_MS,
671
+ type: "essential",
672
+ providerName: "CalmLens"
673
+ },
674
+ SIGN_IN_CODE_ACCEPTED: {
675
+ name: "CL_signInCodeAccepted",
676
+ description: "Indicates that you entered the sign-in code, so the sign-in page shows the sign-in options",
677
+ expirationDescription: "30 days",
678
+ expirationMs: 30 * DAY_MS,
679
+ type: "essential",
680
+ providerName: "CalmLens"
681
+ },
682
+ PRE_RENDERED_WIDTH: {
683
+ name: "CL_preRenderedWidth",
684
+ description: "Your browser's viewport width, so pages are rendered with the right layout for your screen",
685
+ expirationDescription: "30 days",
686
+ expirationMs: 30 * DAY_MS,
687
+ type: "essential",
688
+ providerName: "CalmLens"
689
+ },
690
+ SELECTED_PROJECT_ID: {
691
+ name: "CL_selectedProjectId",
692
+ description: "Remembers the project you last selected in the portal",
693
+ expirationDescription: "1 year",
694
+ expirationMs: YEAR_MS,
695
+ type: "functional",
696
+ providerName: "CalmLens"
697
+ },
698
+ SELECTED_ORGANISATION_ID: {
699
+ name: "CL_selectedOrganisationId",
700
+ description: "Remembers the organisation you last selected in the portal",
701
+ expirationDescription: "1 year",
702
+ expirationMs: YEAR_MS,
703
+ type: "functional",
704
+ providerName: "CalmLens"
705
+ },
706
+ ASSET_DASHBOARD_STATE: {
707
+ name: "CL_assetDashboardState",
708
+ description: "Remembers your asset dashboard view (view mode, filters, sorting and folder)",
709
+ expirationDescription: "1 year",
710
+ expirationMs: YEAR_MS,
711
+ type: "functional",
712
+ providerName: "CalmLens"
713
+ },
714
+ ASSET_VIEW: {
715
+ name: "CL_assetView",
716
+ description: "Remembers whether you chose the grid or table view for an asset's sub-assets (one cookie per asset, named CL_assetView_<assetId>)",
717
+ expirationDescription: "30 days",
718
+ expirationMs: 30 * DAY_MS,
719
+ type: "functional",
720
+ providerName: "CalmLens",
721
+ isPrefix: true
722
+ },
723
+ PORTAL_GUIDANCE_HIDDEN: {
724
+ name: "CL_portalGuidanceHidden",
725
+ description: "Remembers whether you hid next-step tips for a project",
726
+ expirationDescription: "1 year",
727
+ expirationMs: YEAR_MS,
728
+ type: "functional",
729
+ providerName: "CalmLens",
730
+ isPrefix: true
731
+ },
732
+ PORTAL_LAYOUT_SPLIT_PANE: {
733
+ name: "CL_portalLayoutSplitPane",
734
+ description: "Remembers the width you set for the portal sidebar",
735
+ expirationDescription: "1 year",
736
+ expirationMs: YEAR_MS,
737
+ type: "functional",
738
+ providerName: "CalmLens"
739
+ },
740
+ DOCS_UI_LIBRARY: {
741
+ name: "CL_docsUiLibrary",
742
+ description: "Remembers the UI library you selected for documentation examples",
743
+ expirationDescription: "1 year",
744
+ expirationMs: YEAR_MS,
745
+ type: "functional",
746
+ providerName: "CalmLens"
747
+ },
748
+ DOCS_LANGUAGE: {
749
+ name: "CL_docsLanguage",
750
+ description: "Remembers the programming language you selected for documentation examples",
751
+ expirationDescription: "1 year",
752
+ expirationMs: YEAR_MS,
753
+ type: "functional",
754
+ providerName: "CalmLens"
755
+ },
756
+ SEARCH_SDK_LANGUAGE: {
757
+ name: "CL_searchSdkLanguage",
758
+ description: "Remembers the SDK you chose to filter documentation search results by",
759
+ expirationDescription: "1 year",
760
+ expirationMs: YEAR_MS,
761
+ type: "functional",
762
+ providerName: "CalmLens"
763
+ }
764
+ };
765
+ const THIRD_PARTY_COOKIES = {
766
+ GOOGLE_ANALYTICS: {
767
+ name: "_ga",
768
+ description: "Google Analytics distinguishes visitors and maintains session state, only with analytics consent",
769
+ expirationDescription: "2 years",
770
+ type: "analytics",
771
+ providerName: "Google Analytics",
772
+ isPrefix: true
773
+ },
774
+ GOOGLE_AD_CLICK: {
775
+ name: "_gcl",
776
+ description: "Links consented ad clicks to signups; no personalised advertising",
777
+ expirationDescription: "Up to 90 days",
778
+ type: "marketing",
779
+ providerName: "Google",
780
+ isPrefix: true
781
+ },
782
+ GOOGLE_CAMPAIGN: {
783
+ name: "_gac",
784
+ description: "Stores consented campaign attribution",
785
+ expirationDescription: "Up to 90 days",
786
+ type: "marketing",
787
+ providerName: "Google",
788
+ isPrefix: true
789
+ }
790
+ };
791
+ [...Object.values(FIRST_PARTY_COOKIES), ...Object.values(THIRD_PARTY_COOKIES)];
792
+ const MAX_IMAGE_UPLOAD_BYTES = /* @__PURE__ */ (() => 20971520)();
793
+ const MIN_MULTIPART_IMAGE_UPLOAD_BYTES = /* @__PURE__ */ (() => 10485760)();
794
+ const REJECTED_UPLOAD_MIME_TYPE_SET = /* @__PURE__ */ new Set([
795
+ "application/octet-stream",
796
+ "multipart/form-data",
797
+ "multipart/mixed",
798
+ "multipart/alternative",
799
+ "multipart/related",
800
+ "multipart/signed",
801
+ "multipart/encrypted"
802
+ ]);
803
+ const normalizeUploadMimeType = (mimeType) => mimeType.toLowerCase().split(";")[0]?.trim() || mimeType.toLowerCase().trim();
804
+ const isRejectedUploadMimeType = (mimeType) => REJECTED_UPLOAD_MIME_TYPE_SET.has(normalizeUploadMimeType(mimeType));
805
+ 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.`;
806
+ const DOMAIN = "calmlens.com";
807
+ // src/client/CalmLensUtils.ts
808
+ const assertHttpUrl = (name, value) => {
809
+ if (value === void 0) return;
810
+ let url;
811
+ try {
812
+ url = new URL(value);
813
+ } catch {}
814
+ if (!url || url.protocol !== "https:" && url.protocol !== "http:") throw invalidConfig(`${name} must be an absolute http(s) URL`);
815
+ };
816
+ const assertInteger = (name, value, minimum) => {
817
+ 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`);
818
+ };
819
+ const configPathSegment = (value, name) => {
820
+ try {
821
+ encodePathSegment(value, name);
822
+ } catch (error) {
823
+ throw invalidConfig(error.message.replace(/^\[calmlens\] /, ""));
824
+ }
825
+ };
826
+ /** Checks the options both clients share: where requests go and how they're sent. */
827
+ const validateConnectionOptions = (options) => {
828
+ if ("requestBackend" in options) throw invalidConfig("requestBackend is no longer supported; pass a fetch implementation as fetch");
829
+ validateRequestOptions(options);
830
+ assertInteger("maxResponseBytes", options.maxResponseBytes, 1);
831
+ for (const name of ["fetch", "uploadFetch"]) if (options[name] !== void 0 && typeof options[name] !== "function") throw invalidConfig(`${name} must be a function`);
832
+ assertHttpUrl("baseUrl", options.baseUrl);
833
+ };
834
+ /**
835
+ * Checks server client options before the client sets anything up. `CalmLensServerClient` and
836
+ * `MockCalmLensServerClient` share it so both reject the same configuration.
837
+ */
838
+ const validateServerClientOptions = (options) => {
839
+ if (!options || typeof options !== "object") throw invalidConfig("Client options are required");
840
+ if (!options.apiKey || typeof options.apiKey !== "string") throw invalidConfig("API key is required");
841
+ if (!options.projectId) throw invalidConfig("Project ID is required");
842
+ configPathSegment(options.projectId, "Project ID");
843
+ validateConnectionOptions(options);
844
+ if (options.cdnFetch !== void 0 && typeof options.cdnFetch !== "function") throw invalidConfig("cdnFetch must be a function");
845
+ assertHttpUrl("cdnBaseUrl", options.cdnBaseUrl);
846
+ };
847
+ /** Checks app client options before the client sets anything up. */
848
+ const validateAppClientOptions = (options) => {
849
+ if (!options || typeof options !== "object") throw invalidConfig("Client options are required");
850
+ 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.");
851
+ if (!options.uploadPresetId) throw invalidConfig("Upload preset ID is required");
852
+ configPathSegment(options.uploadPresetId, "Upload preset ID");
853
+ 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");
854
+ validateConnectionOptions(options);
855
+ };
856
+ /** Checks the options `withRequestOptions` accepts. */
857
+ const validateRequestOptions = (options) => {
858
+ assertInteger("timeoutMs", options.timeoutMs, 1);
859
+ assertInteger("uploadTimeoutMs", options.uploadTimeoutMs, 1);
860
+ assertInteger("maxRetries", options.maxRetries, 0);
861
+ };
862
+ const toTextAssetOptions = ({ text, name, ...options }) => {
863
+ if (typeof text !== "string" || !text.trim()) throw new CalmLensError(API_ERROR_CODES.INVALID_UPLOAD_PAYLOAD, { message: "Text to classify must be a non-empty string" });
864
+ return {
865
+ ...options,
866
+ name: name ?? "text",
867
+ content: {
868
+ plainText: text,
869
+ type: "text/plain"
870
+ }
871
+ };
872
+ };
873
+ /**
874
+ * A random UUID v4. React Native has no `crypto` global, so this falls back to
875
+ * `crypto.getRandomValues` and then to `Math.random`; the SDK only uses it for idempotency keys,
876
+ * which need to be unique rather than secret.
877
+ */
878
+ const randomUuid = () => {
879
+ const crypto = globalThis.crypto;
880
+ if (typeof crypto?.randomUUID === "function") return crypto.randomUUID();
881
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
882
+ if (typeof crypto?.getRandomValues === "function") crypto.getRandomValues(bytes);
883
+ else for (let index = 0; index < bytes.length; index++) bytes[index] = Math.random() * 256;
884
+ bytes[6] = bytes[6] & 15 | 64;
885
+ bytes[8] = bytes[8] & 63 | 128;
886
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
887
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
888
+ };
889
+ /**
890
+ * Encodes an identifier as one URL path segment. `.` and `..` are rejected because URL
891
+ * resolution would treat them as relative segments and send the request to another route.
892
+ */
893
+ const encodePathSegment = (value, name) => {
894
+ if (typeof value !== "string" || !value) throw invalidArgument(`${name} is required`);
895
+ if (value === "." || value === "..") throw invalidArgument(`${name} cannot be "." or ".."`);
896
+ return encodeURIComponent(value);
897
+ };
898
+ const TRANSFORM_OPTION_NAME = /^[a-z][a-zA-Z0-9]*$/;
899
+ const TRANSFORM_COLOUR_KEYS = /* @__PURE__ */ new Set([
900
+ "background",
901
+ "border.color",
902
+ "trim.border.color"
903
+ ]);
904
+ const HEX_COLOUR = /^#((?:[0-9a-f]{3}){1,2}|(?:[0-9a-f]{4}){1,2})$/i;
905
+ /** Percent-encodes everything except RFC 3986 unreserved characters (`A-Z a-z 0-9 - . _ ~`). */
906
+ const encodeTransformComponent = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
907
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
908
+ const appendTransformOptions = (pairs, key, value) => {
909
+ if (value === void 0 || value === null) return;
910
+ if (isPlainObject(value)) {
911
+ for (const child of Object.keys(value).sort()) {
912
+ if (!TRANSFORM_OPTION_NAME.test(child)) throw invalidArgument(`Transform option "${key}.${child}" is not a valid name`);
913
+ appendTransformOptions(pairs, `${key}.${child}`, value[child]);
914
+ }
915
+ return;
916
+ }
917
+ let text;
918
+ if (typeof value === "string") {
919
+ const hex = TRANSFORM_COLOUR_KEYS.has(key) ? HEX_COLOUR.exec(value) : null;
920
+ text = hex ? hex[1].toLowerCase() : value;
921
+ } else if (typeof value === "boolean") text = String(value);
922
+ else if (typeof value === "number" && Number.isFinite(value)) text = String(value);
923
+ else throw invalidArgument(`Transform option "${key}" must be a string, number, boolean or object`);
924
+ pairs.push(`${key}=${encodeTransformComponent(text)}`);
925
+ };
926
+ /**
927
+ * Serializes structured transforms as a CDN selector, one path segment per step, such as
928
+ * `fit=cover,width=640/blur=12`. Option keys are sorted so equal transforms share a URL, nested
929
+ * keys are dotted (`border.color`), colours are hex without `#`, and values are percent-encoded.
930
+ */
931
+ const serializeTransform = (transform) => {
932
+ const steps = Array.isArray(transform) ? transform : [transform];
933
+ if (steps.length === 0) throw invalidArgument("Transform needs at least one step");
934
+ return steps.map((step) => {
935
+ if (!isPlainObject(step)) throw invalidArgument("Transform steps must be objects");
936
+ if (step.type !== "imageTransform") throw invalidArgument("Transform type must be \"imageTransform\"");
937
+ if (step.options !== void 0 && step.options !== null && !isPlainObject(step.options)) throw invalidArgument("Transform options must be an object");
938
+ const pairs = [];
939
+ for (const key of Object.keys(step.options ?? {}).sort()) {
940
+ if (!TRANSFORM_OPTION_NAME.test(key)) throw invalidArgument(`Transform option "${key}" is not a valid name`);
941
+ appendTransformOptions(pairs, key, step.options[key]);
942
+ }
943
+ if (pairs.length === 0) throw invalidArgument("Transform steps need at least one option");
944
+ return pairs.join(",");
945
+ }).join("/");
946
+ };
947
+ /** Returns the selector string the API and CDN accept for a string or structured transform. */
948
+ const transformSelector = (transform) => transform === void 0 || typeof transform === "string" ? transform : serializeTransform(transform);
949
+ const DEFAULT_CDN_BASE_URL = `https://cdn.${DOMAIN}/v1`;
950
+ const CDN_RESOURCES = /* @__PURE__ */ new Set([
951
+ "original",
952
+ "rendered",
953
+ "placeholder"
954
+ ]);
955
+ /** The longest signed URL lifetime the API accepts: 24 hours. */
956
+ const MAX_SIGNED_URL_SECONDS = 86400;
957
+ /** The operations `assets.batch` runs. */
958
+ const ASSET_BATCH_OPERATIONS = ["delete", "remove-content"];
959
+ /** Refuses a batch the API would reject for its size, an ID or the operation. */
960
+ const validateBatchAssetsOptions = ({ assetIds, operation }) => {
961
+ if (!Array.isArray(assetIds) || assetIds.length < 1 || assetIds.length > 500) throw invalidArgument(`assetIds must list 1 to 500 asset IDs`);
962
+ if (assetIds.some((assetId) => typeof assetId !== "string" || !assetId)) throw invalidArgument("Every asset ID must be a non-empty string");
963
+ if (!ASSET_BATCH_OPERATIONS.includes(operation)) throw invalidArgument(`operation must be one of ${ASSET_BATCH_OPERATIONS.join(", ")}`);
964
+ };
965
+ const validateSignedUrlOptions = (options) => {
966
+ const { resource = "rendered", lens, transform, expiresInSeconds } = options;
967
+ if (resource !== "original" && resource !== "rendered") throw invalidArgument("A signed URL's resource must be original or rendered");
968
+ if (resource === "original" && (lens !== void 0 || transform !== void 0)) throw invalidArgument("Lenses and transforms apply only to the rendered resource");
969
+ if (expiresInSeconds !== void 0 && (!Number.isInteger(expiresInSeconds) || expiresInSeconds < 1 || expiresInSeconds > MAX_SIGNED_URL_SECONDS)) throw invalidArgument("expiresInSeconds must be a whole number from 1 to 86400");
970
+ };
971
+ /**
972
+ * Builds an asset's CDN URL without a client: no API key and no request, so it works in browsers.
973
+ * `CalmLensServerClient`'s `cdn.getAssetUrl` uses it too.
974
+ *
975
+ * `resource` is `rendered` (default), `original` or `placeholder`. Only `rendered` takes a lens or
976
+ * transform, and it serves images in the best format the browser accepts. A
977
+ * placeholder is a small blurred PNG that the CDN serves publicly for every asset whatever its
978
+ * visibility or moderation status, without requiring age confirmation.
979
+ */
980
+ const getAssetUrl = ({ projectId, assetIdentifier, cdnBaseUrl = DEFAULT_CDN_BASE_URL, resource = "rendered", ...options }) => {
981
+ if (!CDN_RESOURCES.has(resource)) throw invalidArgument("resource must be original, rendered or placeholder");
982
+ if (resource === "original" && (options.lens !== void 0 || options.transform !== void 0)) throw invalidArgument("Lenses and transforms apply only to the rendered resource");
983
+ if (resource === "placeholder" && (options.lens !== void 0 || options.transform !== void 0)) throw invalidArgument("A placeholder takes no lens or transform");
984
+ const url = `${cdnBaseUrl.replace(/\/+$/, "")}/assets/${encodePathSegment(projectId, "Project ID")}/${encodePathSegment(assetIdentifier, "Asset identifier")}/${resource}`;
985
+ return appendCdnSelector(url, options);
986
+ };
987
+ /**
988
+ * Appends a CDN lens (`/@name`) followed by any direct transform selector to an asset URL, and
989
+ * optional JSON `viewerClaims` when supplied.
990
+ */
991
+ const appendCdnSelector = (url, options) => {
992
+ const path = appendCdnPathSelector(url, options);
993
+ const query = new URLSearchParams();
994
+ if (options.viewerClaims !== void 0) {
995
+ const claims = VIEWER_CLAIMS_SCHEMA.safeParse(options.viewerClaims);
996
+ if (!claims.success) throw invalidArgument("Invalid viewerClaims");
997
+ query.set("viewerClaims", JSON.stringify(claims.data));
998
+ }
999
+ return query.size ? `${path}?${query}` : path;
1000
+ };
1001
+ const appendCdnPathSelector = (url, options) => {
1002
+ if (options.lens) url = `${url}/@${encodePathSegment(options.lens, "Lens")}`;
1003
+ if (!options.transform) return url;
1004
+ if (typeof options.transform !== "string") return `${url}/${serializeTransform(options.transform)}`;
1005
+ const transform = options.transform.replace(/^\/+/, "");
1006
+ if (transform.includes("..") || transform.includes("?") || transform.includes("#")) throw invalidArgument("Transform cannot contain \"..\", \"?\" or \"#\"");
1007
+ return `${url}/${encodeURI(transform)}`;
1008
+ };
1009
+ const appendQueryValue = (parts, key, value) => {
1010
+ if (value === void 0 || value === null) return;
1011
+ if (Array.isArray(value)) {
1012
+ for (const [index, item] of value.entries()) appendQueryValue(parts, `${key}[${index}]`, item);
1013
+ return;
1014
+ }
1015
+ if (typeof value === "object") {
1016
+ for (const [field, item] of Object.entries(value)) appendQueryValue(parts, `${key}[${field}]`, item);
1017
+ return;
1018
+ }
1019
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
1020
+ };
1021
+ /**
1022
+ * Serialises a query object the way the API parses it (qs bracket notation), for example
1023
+ * `ordering[0][field]=createdAt&ordering[0][direction]=desc`. Every key and value is
1024
+ * percent-encoded and missing values are omitted.
1025
+ */
1026
+ const stringifyQuery = (query) => {
1027
+ const parts = [];
1028
+ for (const [key, value] of Object.entries(query)) appendQueryValue(parts, key, value);
1029
+ return parts.join("&");
1030
+ };
1031
+ /**
1032
+ * Whether an asset has a moderation decision (`classification.moderation.status` is no longer
1033
+ * `pending`) or failed processing; `assets.waitForDecision` returns at the first such state.
1034
+ */
1035
+ const isAssetDecided = (asset) => asset.classification.moderation.status !== "pending" || asset.classification.status === "failed";
1036
+ const DECISION_POLL_DELAY_FACTOR = 1.5;
1037
+ const DECISION_POLL_MAX_DELAY_MS = 5e3;
1038
+ const DEFAULT_DECISION_TIMEOUT_MS = 6e4;
1039
+ /**
1040
+ * Retrieves an asset until it has a decision, waiting 0.5s between checks and 1.5 times longer
1041
+ * each time, up to 5s. Rejects with `sdk/timeout` once `timeoutMs` passes without one.
1042
+ */
1043
+ const pollForDecision = async (retrieve, { timeoutMs = DEFAULT_DECISION_TIMEOUT_MS, signal }, sleep, now = Date.now) => {
1044
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw invalidArgument("timeoutMs must be a non-negative number");
1045
+ const deadline = now() + timeoutMs;
1046
+ let delay = 500;
1047
+ for (;;) {
1048
+ if (signal?.aborted) throw new CalmLensError(SDK_ERROR_CODES.CANCELLED, { message: "Request cancelled" });
1049
+ const asset = await retrieve();
1050
+ if (isAssetDecided(asset)) return asset;
1051
+ const remaining = deadline - now();
1052
+ if (remaining <= 0) throw new CalmLensError(SDK_ERROR_CODES.TIMEOUT, {
1053
+ message: `Asset ${asset.id} had no moderation decision after ${timeoutMs}ms (processing ${asset.classification.status}, moderation ${asset.classification.moderation.status})`,
1054
+ details: {
1055
+ assetId: asset.id,
1056
+ classificationStatus: asset.classification.status,
1057
+ moderationStatus: asset.classification.moderation.status
1058
+ }
1059
+ });
1060
+ await sleep(Math.min(delay, remaining), signal);
1061
+ delay = Math.min(DECISION_POLL_MAX_DELAY_MS, delay * DECISION_POLL_DELAY_FACTOR);
1062
+ }
1063
+ };
1064
+ // src/client/CalmLensVersion.ts
1065
+ /** This SDK's version, from its package.json. */
1066
+ const SDK_VERSION = "0.18.0";
1067
+ const runtimeName = () => {
1068
+ const runtime = globalThis;
1069
+ if (isReactNativeRuntime()) return "react-native";
1070
+ if (runtime.Bun?.version) return `bun/${runtime.Bun.version}`;
1071
+ if (runtime.Deno?.version?.deno) return `deno/${runtime.Deno.version.deno}`;
1072
+ if (runtime.navigator?.userAgent === "Cloudflare-Workers") return "workerd";
1073
+ if (runtime.process?.versions?.node) return `node/${runtime.process.versions.node}`;
1074
+ return "unknown";
1075
+ };
1076
+ /** The `User-Agent` the SDK sends from server runtimes, e.g. `calmlens-js/0.10.0 (node/22.12.0)`. */
1077
+ const getUserAgent = () => `calmlens-js/${SDK_VERSION} (${runtimeName()})`;
1078
+ /** True in a web page, where API keys would be visible to every visitor. */
1079
+ const isBrowserRuntime = () => typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
1080
+ /** True in a React Native app, where an API key ships inside the app for anyone to extract. */
1081
+ const isReactNativeRuntime = () => globalThis.navigator?.product === "ReactNative";
1082
+ // src/contract/ModerationContextLimits-BIMUlk2e.js
1083
+ /** The most recent earlier posts CalmLens uses. */
1084
+ const MODERATION_CONTEXT_USED_ITEMS = 5;
1085
+ /**
1086
+ * The most characters of earlier posts CalmLens uses. The newest post always fits, since no post
1087
+ * may be longer.
1088
+ */
1089
+ const MODERATION_CONTEXT_USED_CHARACTERS = 2e3;
1090
+ /** Unicode code points, as every SDK counts characters. */
1091
+ const countModerationContextCharacters = (text) => {
1092
+ let count = 0;
1093
+ for (const _ of text) count++;
1094
+ return count;
1095
+ };
1096
+ /**
1097
+ * The earlier posts CalmLens uses: the most recent ones, newest first, while at most
1098
+ * `MODERATION_CONTEXT_USED_ITEMS` and `MODERATION_CONTEXT_USED_CHARACTERS` allow, returned oldest
1099
+ * first. The newest post is always kept, so an over-long one is left for validation to reject.
1100
+ */
1101
+ const selectModerationContextItems = (items) => {
1102
+ const selected = [];
1103
+ let characters = 0;
1104
+ for (let index = items.length - 1; index >= 0; index--) {
1105
+ if (selected.length === 5) break;
1106
+ const item = items[index];
1107
+ const length = countModerationContextCharacters(item.text);
1108
+ if (selected.length && characters + length > 2e3) break;
1109
+ selected.unshift(item);
1110
+ characters += length;
1111
+ }
1112
+ return selected;
1113
+ };
1114
+ export { invalidArgument as A, MAX_IMAGE_UPLOAD_BYTES as C, CALMLENS_ERROR_CODES as D, isRejectedUploadMimeType as E, API_ERROR_CODES as M, API_ERROR_DEFINITIONS as N, CalmLensError as O, DOMAIN as S, getRejectedUploadMimeTypeError as T, validateAppClientOptions as _, getUserAgent as a, validateServerClientOptions as b, DEFAULT_CDN_BASE_URL as c, pollForDecision as d, randomUuid as f, transformSelector as g, toTextAssetOptions as h, SDK_VERSION as i, invalidConfig as j, SDK_ERROR_CODES as k, encodePathSegment as l, stringifyQuery as m, MODERATION_CONTEXT_USED_ITEMS as n, isBrowserRuntime as o, serializeTransform as p, selectModerationContextItems as r, isReactNativeRuntime as s, MODERATION_CONTEXT_USED_CHARACTERS as t, getAssetUrl as u, validateBatchAssetsOptions as v, MIN_MULTIPART_IMAGE_UPLOAD_BYTES as w, validateSignedUrlOptions as x, validateRequestOptions as y };