@calmlens/sdk 0.0.0-stage → 0.5.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 +554 -2
- package/bin/calmlens.js +1323 -0
- package/cjs/CalmLensTypes-DG_kHVqY.d.ts +3521 -0
- package/cjs/CalmLensUtils-0QaxbI3A.js +551 -0
- package/cjs/index.d.ts +2741 -0
- package/cjs/index.js +1206 -0
- package/cjs/package.json +3 -0
- package/cjs/testing.d.ts +149 -0
- package/cjs/testing.js +702 -0
- package/esm/CalmLensTypes-DG_kHVqY.d.ts +3521 -0
- package/esm/CalmLensUtils-BQEgxGWk.js +410 -0
- package/esm/index.d.ts +2741 -0
- package/esm/index.js +1196 -0
- package/esm/package.json +3 -0
- package/esm/testing.d.ts +149 -0
- package/esm/testing.js +696 -0
- package/package.json +88 -4
package/README.md
CHANGED
|
@@ -1,3 +1,555 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @calmlens/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official JavaScript/TypeScript SDK for CalmLens API
|
|
4
|
+
|
|
5
|
+
CalmLens provides content moderation and classification for text, images, documents and websites, plus optional storage and CDN delivery. This SDK makes it easy to integrate CalmLens into your JavaScript/TypeScript applications.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @calmlens/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Every public SDK operation accepts one options object. This keeps call sites stable as optional fields are added.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
Create an API key under **Developer → API Keys** in the CalmLens portal. The same page shows your **Project ID**. Keep both on your server.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { CalmLensClient } from "@calmlens/sdk";
|
|
21
|
+
|
|
22
|
+
const client = new CalmLensClient({
|
|
23
|
+
apiKey: process.env.CALMLENS_API_KEY!,
|
|
24
|
+
projectId: process.env.CALMLENS_PROJECT_ID!,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Text (comments, captions, bios): waits for the decision
|
|
28
|
+
const comment = await client.text.moderate({ text: "Great photo!", externalId: "comment-42" });
|
|
29
|
+
|
|
30
|
+
if (comment.status === "approved") {
|
|
31
|
+
// publish it; if comment.report?.flagged is true, also queue it for review
|
|
32
|
+
} else {
|
|
33
|
+
// "rejected": don't publish it
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Images and documents: queued, resolved by webhook or assets.retrieve()
|
|
37
|
+
const image = await client.assets.create({
|
|
38
|
+
name: "photo.jpg",
|
|
39
|
+
content: { data: file, type: "image/jpeg" },
|
|
40
|
+
externalId: "post-7",
|
|
41
|
+
});
|
|
42
|
+
console.log(image.status); // "pending"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| `status` | `report.flagged` | Meaning |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| `approved` | `false` | Safe to show. |
|
|
48
|
+
| `approved` | `true` | Show it, but your policy asked to be alerted. |
|
|
49
|
+
| `rejected` | `true` | Your policy hid or purged it. Don't show it. |
|
|
50
|
+
| `pending` | — | Still being classified. |
|
|
51
|
+
| `error` | — | Processing failed. |
|
|
52
|
+
|
|
53
|
+
See [How CalmLens works](https://calmlens.com/docs/how-it-works) and [Testing your integration](https://calmlens.com/docs/guides/testing) for test inputs that trigger rejections safely.
|
|
54
|
+
|
|
55
|
+
## API Reference
|
|
56
|
+
|
|
57
|
+
### CalmLensClient
|
|
58
|
+
|
|
59
|
+
The main client class for interacting with the CalmLens API.
|
|
60
|
+
|
|
61
|
+
#### Constructor
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
new CalmLensClient(options: CalmLensClientOptions)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Options:**
|
|
68
|
+
- `apiKey` (string, required): Your CalmLens API key
|
|
69
|
+
- `projectId` (string, required): Your CalmLens project ID
|
|
70
|
+
- `baseUrl` (string, optional): API base URL (defaults to `https://api.calmlens.com/v1`)
|
|
71
|
+
- `cdnBaseUrl` (string, optional): CDN base URL (defaults to `https://cdn.calmlens.com/v1`)
|
|
72
|
+
|
|
73
|
+
#### Methods
|
|
74
|
+
|
|
75
|
+
##### assets.create(options)
|
|
76
|
+
|
|
77
|
+
Queue an asset for processing and return the created asset immediately.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
async assets.create(options: SubmitAssetOptions): Promise<Asset>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Options:**
|
|
84
|
+
- `name` (string, required): Name of the asset
|
|
85
|
+
- `content` (optional): Binary or plain-text content and its concrete MIME type, e.g. `{ plainText: "hello", type: "text/plain" }` or `{ data: file, type: file.type }`
|
|
86
|
+
- `url` (string, optional): External URL to fetch instead of sending content
|
|
87
|
+
- `part` (optional): First part of a sequential multipart upload
|
|
88
|
+
- `description` (string, optional): Description of the asset
|
|
89
|
+
- `processingOptions.keepContentAfterProcessing` (boolean, optional): Retain content after processing
|
|
90
|
+
- `visibility` (string, optional): "public" or "private"
|
|
91
|
+
|
|
92
|
+
**Example:**
|
|
93
|
+
```typescript
|
|
94
|
+
// Submit from file content
|
|
95
|
+
const asset = await client.assets.create({
|
|
96
|
+
name: "document.pdf",
|
|
97
|
+
content: {
|
|
98
|
+
base64: "JVBERi0xLjQKJcfs...",
|
|
99
|
+
type: "application/pdf",
|
|
100
|
+
},
|
|
101
|
+
description: "Important document",
|
|
102
|
+
visibility: "private"
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Submit from URL
|
|
106
|
+
const asset = await client.assets.create({
|
|
107
|
+
name: "website-content",
|
|
108
|
+
url: "https://example.com/page",
|
|
109
|
+
description: "Web page to moderate"
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
##### assets.moderate(options)
|
|
114
|
+
|
|
115
|
+
Submit an asset and wait for classification to finish. Accepts the same options as `assets.create()` for text, images, documents, and URLs.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
async assets.moderate(options: SubmitAssetOptions): Promise<Asset>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
##### assets.retrieve(options)
|
|
122
|
+
|
|
123
|
+
Retrieve details of a specific asset.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
async assets.retrieve(options: { assetId: string }): Promise<Asset>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Example:**
|
|
130
|
+
```typescript
|
|
131
|
+
const asset = await client.assets.retrieve({ assetId: "asset-uuid-here" });
|
|
132
|
+
console.log(asset.status);
|
|
133
|
+
console.log(asset.report); // Classification results
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
##### ensureLens(options)
|
|
137
|
+
|
|
138
|
+
Create a project-scoped named lens, or update it safely. A lens can start at any positive version. Repeating the exact same version and definition is idempotent; any other update must use a version greater than the current one. A stale or conflicting version throws `lens/version-conflict`.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const lens = await client.ensureLens({
|
|
142
|
+
identifier: "picmagic-normal",
|
|
143
|
+
name: "PicMagic normal",
|
|
144
|
+
type: "image",
|
|
145
|
+
active: true,
|
|
146
|
+
version: 1,
|
|
147
|
+
transforms: [
|
|
148
|
+
{
|
|
149
|
+
type: "imageTransform",
|
|
150
|
+
options: { fit: "scale-down", width: 1024, format: "png" },
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const image = await client.cdn.serve({
|
|
156
|
+
assetIdentifier: assetId,
|
|
157
|
+
lens: lens.identifier,
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
For several lenses, use `ensureLenses()` to perform all ensures in one request. Each lens keeps the same independent versioning rules.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
const [normal, uhd] = await client.ensureLenses({ lenses: [normalLens, uhdLens] });
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The CDN uses named lenses with an `@` prefix in URLs, such as `/content/@picmagic-normal`. The SDK adds it automatically when you supply `lens`. Use `transform` for one-off direct transforms, for example `client.cdn.serve({ assetIdentifier: assetId, transform: "imageTransform[format=png,width=1024]" })`. Supply either `lens` or `transform`, not both. The resolved lens version is part of CalmLens's CDN cache and ETag identity, so changing a lens with the next version invalidates prior derived output.
|
|
168
|
+
|
|
169
|
+
##### assets.list(options)
|
|
170
|
+
|
|
171
|
+
List assets in your project with pagination.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
async assets.list(options: {
|
|
175
|
+
pageIndex?: number;
|
|
176
|
+
pageSize?: number;
|
|
177
|
+
ordering?: Array<{ field: string; direction: "ASC" | "DESC" }>;
|
|
178
|
+
filter?: string | null;
|
|
179
|
+
}): Promise<{
|
|
180
|
+
items: Asset[];
|
|
181
|
+
total: number;
|
|
182
|
+
hasMore: boolean;
|
|
183
|
+
pageIndex: number;
|
|
184
|
+
pageSize: number;
|
|
185
|
+
}>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Example:**
|
|
189
|
+
```typescript
|
|
190
|
+
const result = await client.assets.list({
|
|
191
|
+
pageIndex: 0,
|
|
192
|
+
pageSize: 20
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
console.log(`Found ${result.total} assets`);
|
|
196
|
+
result.items.forEach(asset => {
|
|
197
|
+
console.log(`${asset.name}: ${asset.status}`);
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
##### assets.listChildren(options)
|
|
202
|
+
|
|
203
|
+
List assets extracted from a website or multimedia document.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const children = await client.assets.listChildren({
|
|
207
|
+
assetId: parentAssetId,
|
|
208
|
+
pageIndex: 0,
|
|
209
|
+
pageSize: 20,
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
##### assets.createSignedUrl(options)
|
|
214
|
+
|
|
215
|
+
Create a short-lived URL for private CDN content or previews.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const signed = await client.assets.createSignedUrl({
|
|
219
|
+
assetId,
|
|
220
|
+
resource: "preview",
|
|
221
|
+
lens: "thumbnail",
|
|
222
|
+
expiresInSeconds: 900,
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
##### assets.move(options)
|
|
227
|
+
|
|
228
|
+
Move an asset into a folder, or to the project root by passing `null`.
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// Move asset to a folder
|
|
232
|
+
await client.assets.move({
|
|
233
|
+
assetId: "asset-uuid",
|
|
234
|
+
folderId: "folder-uuid",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Move asset to project root
|
|
238
|
+
await client.assets.move({
|
|
239
|
+
assetId: "asset-uuid",
|
|
240
|
+
folderId: null,
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
##### assets.retry(options)
|
|
245
|
+
|
|
246
|
+
Process an asset that failed (status `error`) again. It returns to `pending` and is moderated like a
|
|
247
|
+
new upload, so handle the result the same way (webhook or polling). Its content must still be stored:
|
|
248
|
+
otherwise this throws `asset/content-unavailable`, and you need to upload it again.
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
const asset = await client.assets.retry({ assetId: "asset-uuid" });
|
|
252
|
+
// asset.status === "pending"
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
##### folders.list()
|
|
256
|
+
|
|
257
|
+
List all folders in the current project.
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
const folders = await client.folders.list();
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
##### folders.retrieve(options)
|
|
264
|
+
|
|
265
|
+
Retrieve details of a specific folder by ID.
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
const folder = await client.folders.retrieve({ folderId: "folder-uuid" });
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
##### folders.create(options)
|
|
272
|
+
|
|
273
|
+
Create a folder at the project root or nested under another folder.
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// Create root folder
|
|
277
|
+
const marketing = await client.folders.create({ name: "Marketing" });
|
|
278
|
+
|
|
279
|
+
// Create nested subfolder
|
|
280
|
+
const launch = await client.folders.create({
|
|
281
|
+
name: "Launch 2026",
|
|
282
|
+
parentId: marketing.id,
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
##### folders.update(options)
|
|
287
|
+
|
|
288
|
+
Rename an existing folder.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const updated = await client.folders.update({
|
|
292
|
+
folderId: "folder-uuid",
|
|
293
|
+
name: "New Name",
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
##### folders.move(options)
|
|
298
|
+
|
|
299
|
+
Move a folder under another folder, or to the project root by passing `parentId: null`. Moving a folder into itself or a descendant throws `folder/parent-cycle`.
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// Move folder to another parent
|
|
303
|
+
await client.folders.move({
|
|
304
|
+
folderId: child.id,
|
|
305
|
+
parentId: anotherParent.id,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Move folder to project root
|
|
309
|
+
await client.folders.move({
|
|
310
|
+
folderId: child.id,
|
|
311
|
+
parentId: null,
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
##### folders.delete(options)
|
|
316
|
+
|
|
317
|
+
Delete an empty folder. If the folder still contains assets or child folders, this throws `folder/not-empty`.
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
await client.folders.delete({ folderId: "folder-uuid" });
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
##### cdn.getAssetUrl(options) and cdn.serve(options)
|
|
324
|
+
|
|
325
|
+
`getAssetUrl` constructs a CDN URL without fetching it. `serve` fetches the resource and sends the client's API key.
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
const url = client.cdn.getAssetUrl({
|
|
329
|
+
assetIdentifier: assetId,
|
|
330
|
+
resource: "content",
|
|
331
|
+
lens: "thumbnail",
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const response = await client.cdn.serve({
|
|
335
|
+
assetIdentifier: assetId,
|
|
336
|
+
resource: "content",
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Private CDN content cannot be read directly with API keys. Use a short-lived signed URL for application clients.
|
|
341
|
+
|
|
342
|
+
##### Multipart upload flow
|
|
343
|
+
|
|
344
|
+
Use `create` with `part` to start multipart uploads, and `uploadPart` for subsequent chunks. `partNumber` defaults to `1` and `isFinal` defaults to `false` when starting an upload.
|
|
345
|
+
|
|
346
|
+
Image upload sizes use distinct direct and multipart paths:
|
|
347
|
+
|
|
348
|
+
- Direct image uploads must be smaller than `10MB`.
|
|
349
|
+
- Multipart image uploads must total between `10MB` and `20MB`, inclusive. CalmLens accumulates the uploaded parts and rejects a final image outside that range; callers do not need to provide a total-size field.
|
|
350
|
+
- Retained multipart image originals remain in private backing storage. Public or authorized delivery and transformations use CalmLens CDN URLs.
|
|
351
|
+
|
|
352
|
+
Each non-final multipart part must be at least `5MiB`; only the final part may be smaller. If you send base64, split the original bytes first, then base64-encode each part. Do not split one full base64 string into substrings.
|
|
353
|
+
|
|
354
|
+
Payloads can be sent as `{ plainText: string }` (text content only), `{ base64: string }`, `{ uint8Array: number[] | Uint8Array }`, `{ buffer: Buffer | ArrayBuffer }`, `{ data: Blob | Response | ReadableStream }`. Use `{ data: file }` for `File` objects. Upload metadata is URL query data and the SDK sends the binary payload as the raw request body without constructing multipart form data. Multipart `ReadableStream` uploads must include the exact `sizeInBytes` so CalmLens can stream each part without buffering it.
|
|
355
|
+
|
|
356
|
+
Use a concrete asset MIME type such as `image/png`, `application/pdf`, or `text/plain`. The SDK rejects ambiguous/container request MIME types such as `application/octet-stream` and `multipart/*`.
|
|
357
|
+
|
|
358
|
+
**Example (queued finalization):**
|
|
359
|
+
```typescript
|
|
360
|
+
const uploading = await client.assets.create({
|
|
361
|
+
name: "large-image.png",
|
|
362
|
+
part: {
|
|
363
|
+
uint8Array: firstPartBytes,
|
|
364
|
+
type: "image/png"
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
await client.assets.uploadPart({
|
|
369
|
+
assetId: uploading.id,
|
|
370
|
+
part: {
|
|
371
|
+
base64: secondPartBase64,
|
|
372
|
+
partNumber: 2,
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const completed = await client.assets.uploadPart({
|
|
377
|
+
assetId: uploading.id,
|
|
378
|
+
part: {
|
|
379
|
+
base64: finalPartBase64,
|
|
380
|
+
partNumber: 3,
|
|
381
|
+
isFinal: true,
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Example (single-call synchronous analysis):**
|
|
387
|
+
```typescript
|
|
388
|
+
const analyzed = await client.assets.moderate({
|
|
389
|
+
name: "large-image.png",
|
|
390
|
+
part: {
|
|
391
|
+
base64: finalPartBase64,
|
|
392
|
+
type: "image/png",
|
|
393
|
+
isFinal: true
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
console.log(analyzed.status);
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
#### Static Methods
|
|
400
|
+
|
|
401
|
+
##### verifyWebhookSignature(options)
|
|
402
|
+
|
|
403
|
+
Verify that a webhook delivery came from CalmLens and is fresh. Also exported as a standalone `verifyWebhookSignature` function.
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
static async verifyWebhookSignature(options: {
|
|
407
|
+
payload: string | object; // raw request body (pass the string, not parsed JSON)
|
|
408
|
+
signature: string | null | undefined; // x-calmlens-signature header
|
|
409
|
+
timestamp: string | number | null | undefined; // x-calmlens-timestamp header
|
|
410
|
+
secret: string;
|
|
411
|
+
toleranceSeconds?: number; // default 300
|
|
412
|
+
now?: Date | number; // default Date.now(), for tests
|
|
413
|
+
}): Promise<boolean>
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
It returns `true` only when the `x-calmlens-signature` header contains a `v1=` HMAC-SHA256 of `"<timestamp>.<raw body>"` (one match in a comma-separated list is enough) and the timestamp is within `toleranceSeconds` of now.
|
|
417
|
+
|
|
418
|
+
**Example:**
|
|
419
|
+
```typescript
|
|
420
|
+
// In your webhook handler (Fetch API Request)
|
|
421
|
+
const payload = await request.text();
|
|
422
|
+
const isValid = await CalmLensClient.verifyWebhookSignature({
|
|
423
|
+
payload,
|
|
424
|
+
signature: request.headers.get('x-calmlens-signature'),
|
|
425
|
+
timestamp: request.headers.get('x-calmlens-timestamp'),
|
|
426
|
+
secret: WEBHOOK_SECRET,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
if (!isValid) {
|
|
430
|
+
return new Response('Unauthorized', { status: 401 });
|
|
431
|
+
}
|
|
432
|
+
const event: WebhookEventPayload = JSON.parse(payload);
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
## Asset Types
|
|
436
|
+
|
|
437
|
+
Assets can be of the following types:
|
|
438
|
+
- `image` - Images (JPEG, PNG, GIF, WebP, etc.)
|
|
439
|
+
- `video` - Video files (MP4, WebM, etc.)
|
|
440
|
+
- `audio` - Audio files (MP3, WAV, etc.)
|
|
441
|
+
- `plaintext-document` - Plain-text formats such as TXT, JSON, CSV, and YAML
|
|
442
|
+
- `multimedia-document` - Documents such as PDF, DOCX, DOC, RTF, and ODT
|
|
443
|
+
- `website` - Web pages and URLs
|
|
444
|
+
- `file` - Other supported binary files
|
|
445
|
+
|
|
446
|
+
## Asset Status
|
|
447
|
+
|
|
448
|
+
Assets progress through these statuses:
|
|
449
|
+
- `uploading` - Multipart upload is accepting more parts
|
|
450
|
+
- `pending` - Asset is being processed
|
|
451
|
+
- `approved` - Processing completed and delivery is allowed; an `alert` action can still leave `report.flagged` set to `true`
|
|
452
|
+
- `rejected` - A hide, purge, child-asset, or manual rejection decision was applied
|
|
453
|
+
- `error` - Processing failed
|
|
454
|
+
|
|
455
|
+
## Moderation Reports
|
|
456
|
+
|
|
457
|
+
Completed reports always contain the six parent categories: `sexual`, `violence`, `self-harm`, `harassment`, `hate`, and `criminal-activity`. Category-specific detail is nested under `qualifiers`, for example `violence.qualifiers.graphic`. Reports have no `subcategories` field or dedicated minor-safety placeholder. Retired generic qualifier IDs are rejected.
|
|
458
|
+
|
|
459
|
+
Always narrow on a category's `status` before reading `score`. An `evaluated` or `partial` result includes `score` and `likelihood`; a `not-evaluated` result includes a reason and represents unknown coverage, not a safe zero score.
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
const violence = asset.report?.categories.violence;
|
|
463
|
+
if (violence?.status === 'evaluated' || violence?.status === 'partial') {
|
|
464
|
+
console.log(violence.score);
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
See the [moderation report guide](https://calmlens.com/docs/guides/moderation-reports) for aggregation, provider coverage, OCR merging, and policy actions. General Sexual Content moderation does not estimate age or provide dedicated CSAM detection.
|
|
469
|
+
|
|
470
|
+
## Asset Visibility
|
|
471
|
+
|
|
472
|
+
- `public` - Retained content can be requested directly from the CDN
|
|
473
|
+
- `private` - Retained content requires a signed URL or an authenticated owner session
|
|
474
|
+
|
|
475
|
+
## Error Handling
|
|
476
|
+
|
|
477
|
+
The SDK throws errors for common issues:
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
try {
|
|
481
|
+
const asset = await client.assets.create({
|
|
482
|
+
name: "test.jpg",
|
|
483
|
+
// Missing required content, URL, or multipart part
|
|
484
|
+
});
|
|
485
|
+
} catch (error) {
|
|
486
|
+
console.error(error.message); // "Either file content, URL, or multipart part is required"
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Public API failures are normalized to `CalmLensError`. Use its stable `code` property for control flow and `details` for diagnostics.
|
|
491
|
+
|
|
492
|
+
## Testing
|
|
493
|
+
|
|
494
|
+
`@calmlens/sdk/testing` exports a matching object-parameter `MockCalmLensClient`, in-memory storage implementations, and mock API key helpers for local unit testing without network requests.
|
|
495
|
+
|
|
496
|
+
The main `@calmlens/sdk` entry point does not load or export testing helpers to keep production bundles minimal. Keep `/testing` imports in test files.
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
import { MockCalmLensClient, createMockApiKey } from "@calmlens/sdk/testing";
|
|
500
|
+
|
|
501
|
+
const client = new MockCalmLensClient({
|
|
502
|
+
apiKey: createMockApiKey(),
|
|
503
|
+
projectId: crypto.randomUUID(),
|
|
504
|
+
});
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
For configuration options (`mockOptions`), deterministic timestamps and IDs, storage inspection, and testing recipes, see the [JavaScript SDK Testing Guide](https://calmlens.com/docs/sdk/javascript#testing).
|
|
508
|
+
|
|
509
|
+
## Webhooks
|
|
510
|
+
|
|
511
|
+
A `webhook` connector POSTs each event to your URL as JSON:
|
|
512
|
+
|
|
513
|
+
```json
|
|
514
|
+
{
|
|
515
|
+
"id": "…",
|
|
516
|
+
"type": "asset.flagged",
|
|
517
|
+
"createdAt": 1760000000000,
|
|
518
|
+
"projectId": "…",
|
|
519
|
+
"data": { "severity": "high", "title": "…", "description": null, "assetId": "…", "asset": null, "metadata": null }
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
Each delivery has these headers:
|
|
524
|
+
|
|
525
|
+
| Header | Value |
|
|
526
|
+
|---|---|
|
|
527
|
+
| `x-calmlens-delivery` | Delivery ID (the same across retries) |
|
|
528
|
+
| `x-calmlens-timestamp` | Unix time in seconds when the delivery was signed |
|
|
529
|
+
| `x-calmlens-signature` | `v1=<lowercase hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>` |
|
|
530
|
+
|
|
531
|
+
Verify the exact raw request body with `verifyWebhookSignature` before parsing it; re-serializing parsed JSON can change the signed bytes. Deliveries more than 5 minutes old (or ahead of your clock) are rejected to prevent replays. The body type is exported as `WebhookEventPayload`; narrow it on `type` to get that event type's typed `data.facts` (`EventType` and `EventFacts<Type>` are exported too, and [the webhooks guide](https://calmlens.com/docs/guides/webhooks#event-types) lists every type). The header names are exported as `WEBHOOK_SIGNATURE_HEADER`, `WEBHOOK_TIMESTAMP_HEADER` and `WEBHOOK_DELIVERY_HEADER`.
|
|
532
|
+
|
|
533
|
+
## Command-line tool
|
|
534
|
+
|
|
535
|
+
The SDK installs a `calmlens` command. Webhook connectors must use public HTTPS URLs, so during development use it to forward events to a local handler instead. Each event is sent with the same body, headers and signature as a real delivery:
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
npx calmlens listen --forward-to http://localhost:3000/api/webhooks/calmlens --env-file .env.local
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
The CLI reads `CALMLENS_API_KEY`, `CALMLENS_PROJECT_ID` and `CALMLENS_WEBHOOK_SECRET` from the environment or `--env-file`; without a secret it prints a generated one for your app. `npx calmlens trigger [harassment|hate|criminal-activity|clean]` moderates a harmless test input so events appear (one upload each). Run `npx calmlens --help` for all options. To install the command globally, use `npm install -g calmlens`: the same package, published under a shorter name.
|
|
542
|
+
|
|
543
|
+
The same feed is available in code: `client.events.list({ after, limit })` returns events oldest first with a `cursor` to pass back.
|
|
544
|
+
|
|
545
|
+
## Releasing
|
|
546
|
+
|
|
547
|
+
Stable releases are published automatically from `main` when packed SDK contents differ from npm `latest` and an SDK Changeset is present. Contributors must run `bun run changeset` to choose the release type and write release notes. CI updates and commits this package's `CHANGELOG.md` automatically. See the repository's [.changeset release guide](../../.changeset/README.md).
|
|
548
|
+
|
|
549
|
+
### Request budgets and errors
|
|
550
|
+
|
|
551
|
+
Set `timeoutMs` (default 120000) or `signal` on the client. Use `client.withRequestOptions({ signal, timeoutMs: 30000 })` to scope them to an operation without changing the original client. Multipart state is shared between scoped clients. Deadlines include response-body reads; authenticated default REST, upload and CDN requests reject redirects. Custom non-Fetch backends own their redirect/cancellation policies.
|
|
552
|
+
|
|
553
|
+
`CalmLensError` exposes `code`, optional HTTP `status`, `details` and native `cause`. Transport failures use `sdk/transport-error`, deadlines `sdk/timeout`, cancellation `sdk/cancelled`, malformed successful uploads `sdk/invalid-response`, and unstructured HTTP failures `sdk/http-error`. API error codes/messages are preserved. Core model types (`Asset`, `Folder`, `Lens`, `AssetPage`, `EventsPage`, `ClassificationReport`) and request types are exported from the package root.
|
|
554
|
+
|
|
555
|
+
`text.moderate` is a convenience wrapper around asset moderation: it supplies `text/plain` content and defaults the asset name to `text`. `assets.create` queues processing; `assets.moderate` waits for classification. The old asset analyze method is removed.
|