@calmlens/sdk 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -15
- package/cjs/CalmLensConnection-CYtBP-vu.js +516 -0
- package/cjs/CalmLensError-CKwMKV0S.js +362 -0
- package/cjs/CalmLensUtils-Dg3s4loH.js +367 -0
- package/cjs/CalmLensWebhooks-BTPxKvcD.js +201 -0
- package/cjs/{CalmLensTypes-BZ-nxhr_.d.ts → WebhookPayload-Cgb32aSH.d.ts} +5424 -3850
- package/cjs/app.d.ts +43 -0
- package/cjs/app.js +91 -0
- package/cjs/index-BgmWj3KL.d.ts +350 -0
- package/cjs/index.d.ts +3 -382
- package/cjs/index.js +5 -816
- package/cjs/server.d.ts +202 -0
- package/cjs/server.js +371 -0
- package/cjs/src-Dsc8Pn6v.js +45 -0
- package/cjs/testing.d.ts +54 -18
- package/cjs/testing.js +190 -62
- package/esm/CalmLensConnection-wLNy0OIP.js +475 -0
- package/esm/CalmLensError-CihGG26K.js +321 -0
- package/esm/CalmLensUtils-Cx32j2zt.js +260 -0
- package/esm/CalmLensWebhooks-DShmAal4.js +148 -0
- package/esm/{CalmLensTypes-BZ-nxhr_.d.ts → WebhookPayload-Cgb32aSH.d.ts} +5424 -3850
- package/esm/app.d.ts +43 -0
- package/esm/app.js +87 -0
- package/esm/index-BgmWj3KL.d.ts +350 -0
- package/esm/index.d.ts +3 -382
- package/esm/index.js +3 -807
- package/esm/server.d.ts +202 -0
- package/esm/server.js +361 -0
- package/esm/src-CJhrILvM.js +22 -0
- package/esm/testing.d.ts +54 -18
- package/esm/testing.js +140 -12
- package/package.json +23 -1
- package/cjs/CalmLensWebhooks-CH2PaPs0.js +0 -784
- package/esm/CalmLensWebhooks-3Otbubyf.js +0 -593
package/esm/server.d.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { $ as SearchAssetsOptions, A as DeleteAssetContentOptions, At as AssetSafetyHold, B as Lens, C as CdnAssetUrlOptions, Ct as EventType, D as CreateAssetSignedUrlOptions, Dt as AssetSearchMatchField, E as ConnectionOptions, Et as LensType, F as IdempotencyOptions, Ft as MetadataItem, G as ModerateTextOptions, H as ListAssetChildrenOptions, I as ImageTransform, It as PreviewImage, J as MultipartUploadOptions, K as MoveAssetOptions, L as ImageTransformOptions, Lt as ProcessingOptions, M as EnsureLensOptions, Mt as DocumentData, N as EnsureLensesOptions, Nt as ExtractedText, O as CreateFolderOptions, Ot as AssetSearchQuery, P as FolderIdOptions, Pt as ImageData, Q as RetryAssetOptions, R as IterateAssetChildrenOptions, S as CalmLensServerClientOptions, St as EventSeverity, T as CdnTransform, Tt as Page, U as ListAssetsOptions, V as LensTransform, W as ListEventsOptions, X as RequestOptions, Y as MultipartUploadPartOptions, Z as RetrieveAssetOptions, _ as AssetSearchResult, _t as TypedEvent, a as ClassificationLabel, at as UpdateFolderOptions, b as AssetVisibility, bt as EventFactValues, c as ClassificationReport, ct as UploadedAsset, d as Asset, dt as WebhookPayloadInput, et as SignUploadTokenOptions, f as AssetChecksums, ft as GetEventsResponse, g as AssetSearchResponse, gt as EventAsset, h as AssetPage, i as ClassificationCategoryReport, it as UpdateAssetOptions, j as DeleteAssetOptions, jt as AssetSafetyHoldReason, k as CreateMultipartUploadPartOptions, l as Likelihood, lt as VerifyWebhookSignatureOptions, m as AssetContentOptions, mt as Folder, n as ClassificationCategories, nt as SignedUrl, o as ClassificationNotEvaluatedReason, ot as UploadAssetPartOptions, p as AssetChildrenPage, pt as PostAssetSignedUrlResponse, q as MoveFolderOptions, r as ClassificationCategory, rt as SubmitAssetOptions, s as ClassificationQualifierResults, st as UploadPresetSubmitAssetOptions, t as WebhookEventPayload, tt as SignedUploadToken, u as ModerationQualifier, ut as WebhookHeadersInput, v as AssetStatus, vt as EventCategory, w as CdnServeOptions, wt as Order, x as CalmLensAppClientOptions, xt as EventFacts, y as AssetType, yt as EventFact, z as IterateAssetsOptions } from "./WebhookPayload-Cgb32aSH.js";
|
|
2
|
+
import { _ as ProcessingFeatureStage, a as CalmLensErrorDetails, c as ApiErrorCode, d as AgeRange, f as BoundingBox, g as ProcessingFeatureResults, h as ProcessingFeatureId, i as CalmLensErrorCode, l as FlaggedContentAction, m as DetectedPerson, n as CALMLENS_ERROR_CODES, o as CalmLensErrorOptions, p as DetectedFace, r as CalmLensError, s as SdkErrorCode, t as SDK_VERSION, u as FlaggedContentPolicy, v as ProcessingFeatures } from "./index-BgmWj3KL.js";
|
|
3
|
+
//#region src/client/CalmLensServerClient.d.ts
|
|
4
|
+
interface LensCache {
|
|
5
|
+
readonly ensuredLenses: Map<string, Lens>;
|
|
6
|
+
readonly pendingEnsureLenses: Map<string, Promise<void>>;
|
|
7
|
+
}
|
|
8
|
+
declare const inspectSymbol: unique symbol;
|
|
9
|
+
/**
|
|
10
|
+
* The CalmLens API client for your server, authenticated with a project API key. Create one per
|
|
11
|
+
* project and reuse it; it holds no connections. Every method takes one options object and
|
|
12
|
+
* throws `CalmLensError` on failure. Browsers and apps upload with `CalmLensAppClient` instead.
|
|
13
|
+
*/
|
|
14
|
+
declare class CalmLensServerClient {
|
|
15
|
+
#private;
|
|
16
|
+
/**
|
|
17
|
+
* Verifies that a webhook delivery came from CalmLens: checks the `v1=` HMAC-SHA256 in
|
|
18
|
+
* `x-calmlens-signature` over `"<x-calmlens-timestamp>.<raw body>"` and rejects timestamps
|
|
19
|
+
* more than `toleranceSeconds` (default 300) from now, which stops replayed deliveries.
|
|
20
|
+
* Also exported as `verifyWebhookSignature`; `parseWebhookEvent` verifies and parses.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const payload = await request.text();
|
|
25
|
+
* const isValid = await CalmLensServerClient.verifyWebhookSignature({
|
|
26
|
+
* payload,
|
|
27
|
+
* headers: request.headers,
|
|
28
|
+
* secret: process.env.CALMLENS_WEBHOOK_SECRET!,
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
static verifyWebhookSignature(options: VerifyWebhookSignatureOptions): Promise<boolean>;
|
|
33
|
+
constructor(options: CalmLensServerClientOptions, lenses?: LensCache);
|
|
34
|
+
/**
|
|
35
|
+
* A client with different cancellation, deadlines or retries for the calls made through it,
|
|
36
|
+
* sharing this client's credentials and lens cache. This client is unchanged.
|
|
37
|
+
*/
|
|
38
|
+
withRequestOptions(options: RequestOptions): CalmLensServerClient;
|
|
39
|
+
/** Keeps the API key out of `console.log`, `util.inspect` and `JSON.stringify`. */
|
|
40
|
+
[inspectSymbol](): string;
|
|
41
|
+
toJSON(): {
|
|
42
|
+
projectId: string;
|
|
43
|
+
baseUrl: string;
|
|
44
|
+
cdnBaseUrl: string;
|
|
45
|
+
};
|
|
46
|
+
private get projectPath();
|
|
47
|
+
private request;
|
|
48
|
+
private submitAsset;
|
|
49
|
+
get cdn(): {
|
|
50
|
+
/**
|
|
51
|
+
* Builds a CDN URL for asset content or a preview, without a request. Private assets
|
|
52
|
+
* need a signed URL (`assets.createSignedUrl`) or an authenticated request.
|
|
53
|
+
*/
|
|
54
|
+
getAssetUrl: ({ assetIdentifier, ...options }: CdnAssetUrlOptions) => string;
|
|
55
|
+
/**
|
|
56
|
+
* Fetches asset content or a preview from the CDN with the client's API key, and returns
|
|
57
|
+
* the `Response` as soon as headers arrive, so the body streams. Non-2xx responses are
|
|
58
|
+
* returned, not thrown; `timeoutMs` covers only the wait for headers.
|
|
59
|
+
*/
|
|
60
|
+
serve: ({ assetIdentifier, init, ...urlOptions }: CdnServeOptions) => Promise<Response>;
|
|
61
|
+
};
|
|
62
|
+
get text(): {
|
|
63
|
+
/**
|
|
64
|
+
* Moderates text and waits for the decision. Sends `text` as `text/plain` content named
|
|
65
|
+
* `text` unless you pass `name`.
|
|
66
|
+
*/
|
|
67
|
+
moderate: (options: ModerateTextOptions) => Promise<Asset>;
|
|
68
|
+
};
|
|
69
|
+
/** Assets: uploads, moderation and management. */
|
|
70
|
+
get assets(): {
|
|
71
|
+
/** Searches accessible assets by keyword or a natural-language description. */
|
|
72
|
+
search: (options: Partial<AssetSearchQuery> & {
|
|
73
|
+
query: string;
|
|
74
|
+
}) => Promise<AssetSearchResponse>;
|
|
75
|
+
/** Lists one page of the project's assets, with optional filter and ordering. */
|
|
76
|
+
list: (options?: ListAssetsOptions) => Promise<AssetPage>;
|
|
77
|
+
/**
|
|
78
|
+
* Yields every asset matching `options`, fetching pages as needed. Assets created or
|
|
79
|
+
* deleted while iterating can shift pages, so an asset may be skipped or repeated.
|
|
80
|
+
*/
|
|
81
|
+
iterate: (options?: IterateAssetsOptions) => AsyncGenerator<Asset, void, undefined>;
|
|
82
|
+
/** Retrieves an asset with its status and moderation report. */
|
|
83
|
+
retrieve: ({ assetId }: RetrieveAssetOptions) => Promise<Asset>;
|
|
84
|
+
/** Updates an asset's editable fields. */
|
|
85
|
+
update: ({ assetId, ...body }: UpdateAssetOptions) => Promise<Asset>;
|
|
86
|
+
/** Lists one page of the assets extracted from a website or multimedia document. */
|
|
87
|
+
listChildren: ({ assetId, ...options }?: ListAssetChildrenOptions) => Promise<AssetChildrenPage>;
|
|
88
|
+
/** Yields every child of an asset, fetching pages as needed. */
|
|
89
|
+
iterateChildren: (options: IterateAssetChildrenOptions) => AsyncGenerator<Asset, void, undefined>;
|
|
90
|
+
/**
|
|
91
|
+
* Uploads an asset and queues it for moderation, returning immediately with status
|
|
92
|
+
* `pending` (or `uploading` for the first multipart part). Retried safely with an
|
|
93
|
+
* `Idempotency-Key`.
|
|
94
|
+
*/
|
|
95
|
+
create: (options: SubmitAssetOptions) => Promise<Asset>;
|
|
96
|
+
/**
|
|
97
|
+
* Uploads an asset and waits for its moderation decision. Audio and video still finish
|
|
98
|
+
* in the background. Retried safely with an `Idempotency-Key`.
|
|
99
|
+
*/
|
|
100
|
+
moderate: (options: SubmitAssetOptions) => Promise<Asset>;
|
|
101
|
+
/**
|
|
102
|
+
* Uploads the next part of a multipart upload. The upload's queue mode, chosen when it
|
|
103
|
+
* started, applies when the final part arrives; pass `queue` to override it.
|
|
104
|
+
*/
|
|
105
|
+
uploadPart: ({ assetId, ...options }: UploadAssetPartOptions) => Promise<Asset>;
|
|
106
|
+
/**
|
|
107
|
+
* Creates a short-lived signed CDN URL, so browsers can load private content without
|
|
108
|
+
* an API key.
|
|
109
|
+
*/
|
|
110
|
+
createSignedUrl: ({ assetId, ...options }: CreateAssetSignedUrlOptions) => Promise<PostAssetSignedUrlResponse>;
|
|
111
|
+
/** Moves an asset into a folder, or to the project root with `folderId: null`. */
|
|
112
|
+
move: ({ assetId, folderId }: MoveAssetOptions) => Promise<Asset>;
|
|
113
|
+
/**
|
|
114
|
+
* Processes an asset that failed (status `error`) again. It returns to `pending` and is
|
|
115
|
+
* moderated like a new upload; its content must still be stored.
|
|
116
|
+
*/
|
|
117
|
+
retry: ({ assetId }: RetryAssetOptions) => Promise<Asset>;
|
|
118
|
+
/**
|
|
119
|
+
* Removes an asset's stored content (source, preview and retained extracted text) while
|
|
120
|
+
* keeping the asset record, metadata and moderation report. The returned asset has
|
|
121
|
+
* `storageDeletedAt` set; the CDN no longer serves it.
|
|
122
|
+
*/
|
|
123
|
+
deleteContent: ({ assetId }: DeleteAssetContentOptions) => Promise<Asset>;
|
|
124
|
+
/** Deletes an asset record together with its stored content. */
|
|
125
|
+
delete: ({ assetId }: DeleteAssetOptions) => Promise<void>;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Signs upload tokens on your server, so a browser or app can upload through an upload preset
|
|
129
|
+
* that requires one. Signing happens locally with your API key: it makes no request.
|
|
130
|
+
*/
|
|
131
|
+
get uploadTokens(): {
|
|
132
|
+
/**
|
|
133
|
+
* Signs a single-use token for one upload through `uploadPresetId`. Give `token` to the
|
|
134
|
+
* browser or app, which passes it to `CalmLensAppClient` as `token`.
|
|
135
|
+
*/
|
|
136
|
+
sign: (options: SignUploadTokenOptions) => Promise<SignedUploadToken>;
|
|
137
|
+
};
|
|
138
|
+
private buildAssets;
|
|
139
|
+
get events(): {
|
|
140
|
+
/**
|
|
141
|
+
* Lists project events after a cursor, oldest first. Call without `after` to get a cursor
|
|
142
|
+
* for "now", then poll with the returned `cursor`. Webhook deliveries carry the same events
|
|
143
|
+
* reshaped as `{ id, type, createdAt, projectId, data }`.
|
|
144
|
+
*/
|
|
145
|
+
list: (options?: ListEventsOptions) => Promise<GetEventsResponse>;
|
|
146
|
+
/**
|
|
147
|
+
* Yields pages of events after `after` until no more are waiting. Save each page's
|
|
148
|
+
* `cursor` to resume later. Without `after`, yields one empty page whose cursor is "now".
|
|
149
|
+
*/
|
|
150
|
+
pages: (options?: ListEventsOptions) => AsyncGenerator<GetEventsResponse, void, undefined>;
|
|
151
|
+
};
|
|
152
|
+
private iterateEventPages;
|
|
153
|
+
private iteratePages;
|
|
154
|
+
get folders(): {
|
|
155
|
+
/** Lists every folder in the project. */
|
|
156
|
+
list: () => Promise<Folder[]>;
|
|
157
|
+
retrieve: ({ folderId }: FolderIdOptions) => Promise<Folder>;
|
|
158
|
+
/** Creates a folder at the project root or inside `parentId`. */
|
|
159
|
+
create: (options: CreateFolderOptions) => Promise<Folder>;
|
|
160
|
+
update: ({ folderId, ...body }: UpdateFolderOptions) => Promise<Folder>;
|
|
161
|
+
/** Deletes an empty folder; throws `folder/not-empty` otherwise. */
|
|
162
|
+
delete: ({ folderId }: FolderIdOptions) => Promise<void>;
|
|
163
|
+
/** Moves a folder under `parentId`, or to the project root with `parentId: null`. */
|
|
164
|
+
move: ({ folderId, parentId }: MoveFolderOptions) => Promise<Folder>;
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Create a named lens at any positive version, or update it with a greater version.
|
|
168
|
+
* Supplying the same definition and version is idempotent.
|
|
169
|
+
*/
|
|
170
|
+
ensureLens(options: EnsureLensOptions): Promise<Lens>;
|
|
171
|
+
/**
|
|
172
|
+
* Create or update several named lenses in one API request.
|
|
173
|
+
* Each lens follows the same versioning and idempotency rules as ensureLens().
|
|
174
|
+
*/
|
|
175
|
+
ensureLenses({ lenses }: EnsureLensesOptions): Promise<Lens[]>;
|
|
176
|
+
private submitEnsureLenses;
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/client/CalmLensWebhooks.d.ts
|
|
180
|
+
export declare const WEBHOOK_DELIVERY_HEADER = "x-calmlens-delivery";
|
|
181
|
+
export declare const WEBHOOK_TIMESTAMP_HEADER = "x-calmlens-timestamp";
|
|
182
|
+
export declare const WEBHOOK_SIGNATURE_HEADER = "x-calmlens-signature";
|
|
183
|
+
/** Deliveries whose timestamp is further than this from the receiver's clock are rejected. */
|
|
184
|
+
export declare const WEBHOOK_SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
185
|
+
/**
|
|
186
|
+
* Verifies a webhook delivery: `x-calmlens-signature` must contain a `v1=` HMAC-SHA256 of
|
|
187
|
+
* `"<x-calmlens-timestamp>.<raw body>"`, and the timestamp must be within `toleranceSeconds`
|
|
188
|
+
* (default 300) of now. Returns `false` for anything missing, malformed or stale, and throws
|
|
189
|
+
* `sdk/invalid-argument` when `payload` is not a raw body (for example, parsed JSON).
|
|
190
|
+
*/
|
|
191
|
+
export declare function verifyWebhookSignature(options: VerifyWebhookSignatureOptions): Promise<boolean>;
|
|
192
|
+
/**
|
|
193
|
+
* Verifies a webhook delivery and parses its body. Throws `sdk/invalid-webhook-signature`
|
|
194
|
+
* when the signature or timestamp does not verify, and `sdk/invalid-webhook-payload` when a
|
|
195
|
+
* verified body is not a webhook event. Narrow the result on `type` for typed `data.facts`.
|
|
196
|
+
*
|
|
197
|
+
* Event types and fact keys added to CalmLens after this SDK version are returned as they are,
|
|
198
|
+
* so keep a default branch when you switch on `type`.
|
|
199
|
+
*/
|
|
200
|
+
export declare function parseWebhookEvent(options: VerifyWebhookSignatureOptions): Promise<WebhookEventPayload>;
|
|
201
|
+
//#endregion
|
|
202
|
+
export { type AgeRange, type ApiErrorCode, type Asset, type AssetChecksums, type AssetChildrenPage, type AssetContentOptions, type AssetPage, type AssetSafetyHold, type AssetSafetyHoldReason, type AssetSearchMatchField, type AssetSearchQuery, type AssetSearchResponse, type AssetSearchResult, type AssetStatus, type AssetType, type AssetVisibility, type BoundingBox, CALMLENS_ERROR_CODES, type CalmLensAppClientOptions, CalmLensError, type CalmLensErrorCode, type CalmLensErrorDetails, type CalmLensErrorOptions, CalmLensServerClient, type CalmLensServerClientOptions, type CdnAssetUrlOptions, type CdnServeOptions, type CdnTransform, type ClassificationCategories, type ClassificationCategory, type ClassificationCategoryReport, type ClassificationLabel, type ClassificationNotEvaluatedReason, type ClassificationQualifierResults, type ClassificationReport, type ConnectionOptions, type CreateAssetSignedUrlOptions, type CreateFolderOptions, type CreateMultipartUploadPartOptions, type DeleteAssetContentOptions, type DeleteAssetOptions, type DetectedFace, type DetectedPerson, type DocumentData, type EnsureLensOptions, type EnsureLensesOptions, type EventAsset, type EventCategory, type EventFact, type EventFactValues, type EventFacts, type EventSeverity, type EventType, type GetEventsResponse as EventsPage, type ExtractedText, type FlaggedContentAction, type FlaggedContentPolicy, type Folder, type FolderIdOptions, type IdempotencyOptions, type ImageData, type ImageTransform, type ImageTransformOptions, type IterateAssetChildrenOptions, type IterateAssetsOptions, type Lens, type LensTransform, type LensType, type Likelihood, type ListAssetChildrenOptions, type ListAssetsOptions, type ListEventsOptions, type MetadataItem, type ModerateTextOptions, type ModerationQualifier, type MoveAssetOptions, type MoveFolderOptions, type MultipartUploadOptions, type MultipartUploadPartOptions, type Order, type Page, type PreviewImage, type ProcessingFeatureId, type ProcessingFeatureResults, type ProcessingFeatureStage, type ProcessingFeatures, type ProcessingOptions, type RequestOptions, type RetrieveAssetOptions, type RetryAssetOptions, SDK_VERSION, type SdkErrorCode, type SearchAssetsOptions, type SignUploadTokenOptions, type SignedUploadToken, type SignedUrl, type SubmitAssetOptions, type TypedEvent, type UpdateAssetOptions, type UpdateFolderOptions, type UploadAssetPartOptions, type UploadPresetSubmitAssetOptions, type UploadedAsset, type VerifyWebhookSignatureOptions, type WebhookEventPayload, type WebhookHeadersInput, type WebhookPayloadInput };
|
package/esm/server.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { a as invalidConfig, i as invalidArgument, n as CalmLensError, r as SDK_ERROR_CODES, t as CALMLENS_ERROR_CODES } from "./CalmLensError-CihGG26K.js";
|
|
2
|
+
import { i as isReactNativeRuntime, r as isBrowserRuntime, t as SDK_VERSION } from "./src-CJhrILvM.js";
|
|
3
|
+
import { a as parseWebhookEvent, i as WEBHOOK_TIMESTAMP_HEADER, l as stringifyJsonCanonical, n as WEBHOOK_SIGNATURE_HEADER, o as verifyWebhookSignature, r as WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, s as signUploadTokenWithApiKey, t as WEBHOOK_DELIVERY_HEADER } from "./CalmLensWebhooks-DShmAal4.js";
|
|
4
|
+
import { a as toTextAssetOptions, c as validateRequestOptions, l as validateServerClientOptions, n as encodePathSegment, o as transformSelector, t as appendCdnSelector } from "./CalmLensUtils-Cx32j2zt.js";
|
|
5
|
+
import { a as newIdempotencyKey, i as invalidPayload, n as expectAsset, o as prepareSubmitAssetOptions, s as resolveConnectionOptions, t as CalmLensConnection } from "./CalmLensConnection-wLNy0OIP.js";
|
|
6
|
+
//#region src/client/CalmLensServerClient.ts
|
|
7
|
+
const getEnsureLensCacheKey = (options) => stringifyJsonCanonical({
|
|
8
|
+
identifier: options.identifier,
|
|
9
|
+
name: options.name,
|
|
10
|
+
description: options.description ?? null,
|
|
11
|
+
type: options.type,
|
|
12
|
+
active: options.active,
|
|
13
|
+
transforms: options.transforms ?? null,
|
|
14
|
+
version: options.version
|
|
15
|
+
});
|
|
16
|
+
const requireId = (value, name) => encodePathSegment(value, name);
|
|
17
|
+
const inspectSymbol = Symbol.for("nodejs.util.inspect.custom");
|
|
18
|
+
/**
|
|
19
|
+
* The CalmLens API client for your server, authenticated with a project API key. Create one per
|
|
20
|
+
* project and reuse it; it holds no connections. Every method takes one options object and
|
|
21
|
+
* throws `CalmLensError` on failure. Browsers and apps upload with `CalmLensAppClient` instead.
|
|
22
|
+
*/
|
|
23
|
+
var CalmLensServerClient = class CalmLensServerClient {
|
|
24
|
+
#apiKey;
|
|
25
|
+
#settings;
|
|
26
|
+
#lenses;
|
|
27
|
+
#connection;
|
|
28
|
+
/**
|
|
29
|
+
* Verifies that a webhook delivery came from CalmLens: checks the `v1=` HMAC-SHA256 in
|
|
30
|
+
* `x-calmlens-signature` over `"<x-calmlens-timestamp>.<raw body>"` and rejects timestamps
|
|
31
|
+
* more than `toleranceSeconds` (default 300) from now, which stops replayed deliveries.
|
|
32
|
+
* Also exported as `verifyWebhookSignature`; `parseWebhookEvent` verifies and parses.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const payload = await request.text();
|
|
37
|
+
* const isValid = await CalmLensServerClient.verifyWebhookSignature({
|
|
38
|
+
* payload,
|
|
39
|
+
* headers: request.headers,
|
|
40
|
+
* secret: process.env.CALMLENS_WEBHOOK_SECRET!,
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
static async verifyWebhookSignature(options) {
|
|
45
|
+
return verifyWebhookSignature(options);
|
|
46
|
+
}
|
|
47
|
+
constructor(options, lenses) {
|
|
48
|
+
validateServerClientOptions(options);
|
|
49
|
+
if ((isBrowserRuntime() || isReactNativeRuntime()) && !options.dangerouslyAllowBrowser) throw invalidConfig("CalmLensServerClient is running in a browser or app, where anyone could read the API key. Call CalmLens from your server, upload from the browser or app with CalmLensAppClient, or set dangerouslyAllowBrowser: true for trusted internal tools.");
|
|
50
|
+
const { apiKey, ...rest } = options;
|
|
51
|
+
this.#apiKey = apiKey;
|
|
52
|
+
this.#settings = {
|
|
53
|
+
...resolveConnectionOptions(rest),
|
|
54
|
+
cdnBaseUrl: (rest.cdnBaseUrl ?? `https://cdn.calmlens.com/v1`).replace(/\/+$/, "")
|
|
55
|
+
};
|
|
56
|
+
this.#lenses = lenses ?? {
|
|
57
|
+
ensuredLenses: /* @__PURE__ */ new Map(),
|
|
58
|
+
pendingEnsureLenses: /* @__PURE__ */ new Map()
|
|
59
|
+
};
|
|
60
|
+
this.#connection = new CalmLensConnection(this.#settings, apiKey);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A client with different cancellation, deadlines or retries for the calls made through it,
|
|
64
|
+
* sharing this client's credentials and lens cache. This client is unchanged.
|
|
65
|
+
*/
|
|
66
|
+
withRequestOptions(options) {
|
|
67
|
+
validateRequestOptions(options);
|
|
68
|
+
return new CalmLensServerClient({
|
|
69
|
+
...this.#settings,
|
|
70
|
+
...options,
|
|
71
|
+
apiKey: this.#apiKey
|
|
72
|
+
}, this.#lenses);
|
|
73
|
+
}
|
|
74
|
+
/** Keeps the API key out of `console.log`, `util.inspect` and `JSON.stringify`. */
|
|
75
|
+
[inspectSymbol]() {
|
|
76
|
+
const { projectId, baseUrl } = this.#settings;
|
|
77
|
+
return `CalmLensServerClient { projectId: ${JSON.stringify(projectId)}, baseUrl: ${JSON.stringify(baseUrl)} }`;
|
|
78
|
+
}
|
|
79
|
+
toJSON() {
|
|
80
|
+
const { projectId, baseUrl, cdnBaseUrl } = this.#settings;
|
|
81
|
+
return {
|
|
82
|
+
projectId,
|
|
83
|
+
baseUrl,
|
|
84
|
+
cdnBaseUrl
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
get projectPath() {
|
|
88
|
+
return `/projects/${encodeURIComponent(this.#settings.projectId)}`;
|
|
89
|
+
}
|
|
90
|
+
request(method, path, options = {}) {
|
|
91
|
+
return this.#connection.json(method, path, options);
|
|
92
|
+
}
|
|
93
|
+
async submitAsset(options, queue) {
|
|
94
|
+
const body = prepareSubmitAssetOptions(options);
|
|
95
|
+
const uploadsPath = `${this.projectPath}/assets`;
|
|
96
|
+
if ("content" in body || "part" in body) return expectAsset(await this.#connection.submitRawUpload(uploadsPath, body, queue));
|
|
97
|
+
const { idempotencyKey, ...fields } = body;
|
|
98
|
+
const data = await this.#connection.json("POST", uploadsPath, {
|
|
99
|
+
query: { queue },
|
|
100
|
+
body: fields,
|
|
101
|
+
idempotencyKey: idempotencyKey ?? newIdempotencyKey()
|
|
102
|
+
});
|
|
103
|
+
return expectAsset(data);
|
|
104
|
+
}
|
|
105
|
+
get cdn() {
|
|
106
|
+
return {
|
|
107
|
+
/**
|
|
108
|
+
* Builds a CDN URL for asset content or a preview, without a request. Private assets
|
|
109
|
+
* need a signed URL (`assets.createSignedUrl`) or an authenticated request.
|
|
110
|
+
*/
|
|
111
|
+
getAssetUrl: ({ assetIdentifier, ...options }) => {
|
|
112
|
+
const resource = options.resource ?? "content";
|
|
113
|
+
const { projectId } = this.#settings;
|
|
114
|
+
const url = `${this.#settings.cdnBaseUrl}/assets/${encodeURIComponent(projectId)}/${requireId(assetIdentifier, "Asset identifier")}/${resource}`;
|
|
115
|
+
return appendCdnSelector(url, options);
|
|
116
|
+
},
|
|
117
|
+
/**
|
|
118
|
+
* Fetches asset content or a preview from the CDN with the client's API key, and returns
|
|
119
|
+
* the `Response` as soon as headers arrive, so the body streams. Non-2xx responses are
|
|
120
|
+
* returned, not thrown; `timeoutMs` covers only the wait for headers.
|
|
121
|
+
*/
|
|
122
|
+
serve: async ({ assetIdentifier, init, ...urlOptions }) => {
|
|
123
|
+
const url = this.cdn.getAssetUrl({
|
|
124
|
+
assetIdentifier,
|
|
125
|
+
...urlOptions
|
|
126
|
+
});
|
|
127
|
+
const method = init?.method?.toUpperCase() ?? "GET";
|
|
128
|
+
const { response } = await this.#connection.transport.send({
|
|
129
|
+
method,
|
|
130
|
+
url,
|
|
131
|
+
init,
|
|
132
|
+
timeoutMs: this.#connection.timeoutMs,
|
|
133
|
+
deadline: "headers",
|
|
134
|
+
fetch: this.#settings.cdnFetch
|
|
135
|
+
}, { allowHttpErrors: true });
|
|
136
|
+
return response;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
get text() {
|
|
141
|
+
return {
|
|
142
|
+
/**
|
|
143
|
+
* Moderates text and waits for the decision. Sends `text` as `text/plain` content named
|
|
144
|
+
* `text` unless you pass `name`.
|
|
145
|
+
*/
|
|
146
|
+
moderate: async (options) => this.buildAssets().moderate(toTextAssetOptions(options)) };
|
|
147
|
+
}
|
|
148
|
+
/** Assets: uploads, moderation and management. */
|
|
149
|
+
get assets() {
|
|
150
|
+
return this.buildAssets();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Signs upload tokens on your server, so a browser or app can upload through an upload preset
|
|
154
|
+
* that requires one. Signing happens locally with your API key: it makes no request.
|
|
155
|
+
*/
|
|
156
|
+
get uploadTokens() {
|
|
157
|
+
return {
|
|
158
|
+
/**
|
|
159
|
+
* Signs a single-use token for one upload through `uploadPresetId`. Give `token` to the
|
|
160
|
+
* browser or app, which passes it to `CalmLensAppClient` as `token`.
|
|
161
|
+
*/
|
|
162
|
+
sign: async (options) => {
|
|
163
|
+
return signUploadTokenWithApiKey(this.#apiKey, options);
|
|
164
|
+
} };
|
|
165
|
+
}
|
|
166
|
+
buildAssets() {
|
|
167
|
+
return {
|
|
168
|
+
/** Searches accessible assets by keyword or a natural-language description. */
|
|
169
|
+
search: async (options) => this.request("GET", `${this.projectPath}/assets/search`, { query: options }),
|
|
170
|
+
/** Lists one page of the project's assets, with optional filter and ordering. */
|
|
171
|
+
list: async (options = {}) => this.request("GET", `${this.projectPath}/assets/page`, { query: {
|
|
172
|
+
...options,
|
|
173
|
+
pageIndex: options.pageIndex ?? 0
|
|
174
|
+
} }),
|
|
175
|
+
/**
|
|
176
|
+
* Yields every asset matching `options`, fetching pages as needed. Assets created or
|
|
177
|
+
* deleted while iterating can shift pages, so an asset may be skipped or repeated.
|
|
178
|
+
*/
|
|
179
|
+
iterate: (options = {}) => this.iteratePages((pageIndex) => this.buildAssets().list({
|
|
180
|
+
...options,
|
|
181
|
+
pageIndex
|
|
182
|
+
})),
|
|
183
|
+
/** Retrieves an asset with its status and moderation report. */
|
|
184
|
+
retrieve: async ({ assetId }) => this.request("GET", `/assets/${requireId(assetId, "Asset ID")}`),
|
|
185
|
+
/** Updates an asset's editable fields. */
|
|
186
|
+
update: async ({ assetId, ...body }) => this.request("PATCH", `/assets/${requireId(assetId, "Asset ID")}`, { body }),
|
|
187
|
+
/** Lists one page of the assets extracted from a website or multimedia document. */
|
|
188
|
+
listChildren: async ({ assetId, ...options } = {}) => this.request("GET", `/assets/${requireId(assetId, "Asset ID")}/children`, { query: {
|
|
189
|
+
...options,
|
|
190
|
+
pageIndex: options.pageIndex ?? 0
|
|
191
|
+
} }),
|
|
192
|
+
/** Yields every child of an asset, fetching pages as needed. */
|
|
193
|
+
iterateChildren: (options) => {
|
|
194
|
+
requireId(options?.assetId, "Asset ID");
|
|
195
|
+
return this.iteratePages((pageIndex) => this.buildAssets().listChildren({
|
|
196
|
+
...options,
|
|
197
|
+
pageIndex
|
|
198
|
+
}));
|
|
199
|
+
},
|
|
200
|
+
/**
|
|
201
|
+
* Uploads an asset and queues it for moderation, returning immediately with status
|
|
202
|
+
* `pending` (or `uploading` for the first multipart part). Retried safely with an
|
|
203
|
+
* `Idempotency-Key`.
|
|
204
|
+
*/
|
|
205
|
+
create: async (options) => this.submitAsset(options, true),
|
|
206
|
+
/**
|
|
207
|
+
* Uploads an asset and waits for its moderation decision. Audio and video still finish
|
|
208
|
+
* in the background. Retried safely with an `Idempotency-Key`.
|
|
209
|
+
*/
|
|
210
|
+
moderate: async (options) => {
|
|
211
|
+
if (options && "content" in options && "plainText" in options.content && (typeof options.content.plainText !== "string" || !options.content.plainText.trim())) throw invalidPayload("Text to moderate must be a non-empty string");
|
|
212
|
+
return this.submitAsset(options, false);
|
|
213
|
+
},
|
|
214
|
+
/**
|
|
215
|
+
* Uploads the next part of a multipart upload. The upload's queue mode, chosen when it
|
|
216
|
+
* started, applies when the final part arrives; pass `queue` to override it.
|
|
217
|
+
*/
|
|
218
|
+
uploadPart: async ({ assetId, ...options }) => {
|
|
219
|
+
const encodedAssetId = requireId(assetId, "Asset ID");
|
|
220
|
+
return expectAsset(await this.#connection.uploadPart(`/assets/${encodedAssetId}/part`, options));
|
|
221
|
+
},
|
|
222
|
+
/**
|
|
223
|
+
* Creates a short-lived signed CDN URL, so browsers can load private content without
|
|
224
|
+
* an API key.
|
|
225
|
+
*/
|
|
226
|
+
createSignedUrl: async ({ assetId, ...options }) => {
|
|
227
|
+
const encodedAssetId = requireId(assetId, "Asset ID");
|
|
228
|
+
if (options.lens && options.transform) throw invalidArgument("Specify either a lens or a transform, not both");
|
|
229
|
+
return this.request("POST", `/assets/${encodedAssetId}/signed-url`, { body: {
|
|
230
|
+
resource: options.resource ?? "content",
|
|
231
|
+
expiresInSeconds: options.expiresInSeconds ?? 900,
|
|
232
|
+
lens: options.lens,
|
|
233
|
+
transform: transformSelector(options.transform)
|
|
234
|
+
} });
|
|
235
|
+
},
|
|
236
|
+
/** Moves an asset into a folder, or to the project root with `folderId: null`. */
|
|
237
|
+
move: async ({ assetId, folderId }) => this.request("POST", `/assets/${requireId(assetId, "Asset ID")}/move`, { body: { folderId } }),
|
|
238
|
+
/**
|
|
239
|
+
* Processes an asset that failed (status `error`) again. It returns to `pending` and is
|
|
240
|
+
* moderated like a new upload; its content must still be stored.
|
|
241
|
+
*/
|
|
242
|
+
retry: async ({ assetId }) => this.request("POST", `/assets/${requireId(assetId, "Asset ID")}/retry`),
|
|
243
|
+
/**
|
|
244
|
+
* Removes an asset's stored content (source, preview and retained extracted text) while
|
|
245
|
+
* keeping the asset record, metadata and moderation report. The returned asset has
|
|
246
|
+
* `storageDeletedAt` set; the CDN no longer serves it.
|
|
247
|
+
*/
|
|
248
|
+
deleteContent: async ({ assetId }) => this.request("DELETE", `/assets/${requireId(assetId, "Asset ID")}/content`),
|
|
249
|
+
/** Deletes an asset record together with its stored content. */
|
|
250
|
+
delete: async ({ assetId }) => {
|
|
251
|
+
await this.request("DELETE", `/assets/${requireId(assetId, "Asset ID")}`);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
get events() {
|
|
256
|
+
return {
|
|
257
|
+
/**
|
|
258
|
+
* Lists project events after a cursor, oldest first. Call without `after` to get a cursor
|
|
259
|
+
* for "now", then poll with the returned `cursor`. Webhook deliveries carry the same events
|
|
260
|
+
* reshaped as `{ id, type, createdAt, projectId, data }`.
|
|
261
|
+
*/
|
|
262
|
+
list: async (options = {}) => this.request("GET", `${this.projectPath}/events`, { query: { ...options } }),
|
|
263
|
+
/**
|
|
264
|
+
* Yields pages of events after `after` until no more are waiting. Save each page's
|
|
265
|
+
* `cursor` to resume later. Without `after`, yields one empty page whose cursor is "now".
|
|
266
|
+
*/
|
|
267
|
+
pages: (options = {}) => this.iterateEventPages(options)
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
async *iterateEventPages(options) {
|
|
271
|
+
let after = options.after;
|
|
272
|
+
for (;;) {
|
|
273
|
+
const page = await this.events.list({
|
|
274
|
+
...options,
|
|
275
|
+
after
|
|
276
|
+
});
|
|
277
|
+
yield page;
|
|
278
|
+
if (!page.hasMore || page.cursor === after) return;
|
|
279
|
+
after = page.cursor;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async *iteratePages(listPage) {
|
|
283
|
+
for (let pageIndex = 0;; pageIndex++) {
|
|
284
|
+
const page = await listPage(pageIndex);
|
|
285
|
+
yield* page.items;
|
|
286
|
+
if (!page.hasMore || page.items.length === 0) return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
get folders() {
|
|
290
|
+
return {
|
|
291
|
+
/** Lists every folder in the project. */
|
|
292
|
+
list: async () => (await this.request("GET", `${this.projectPath}/folders`)).folders,
|
|
293
|
+
retrieve: async ({ folderId }) => this.request("GET", `/folders/${requireId(folderId, "Folder ID")}`),
|
|
294
|
+
/** Creates a folder at the project root or inside `parentId`. */
|
|
295
|
+
create: async (options) => this.request("POST", `${this.projectPath}/folders`, { body: options }),
|
|
296
|
+
update: async ({ folderId, ...body }) => this.request("PATCH", `/folders/${requireId(folderId, "Folder ID")}`, { body }),
|
|
297
|
+
/** Deletes an empty folder; throws `folder/not-empty` otherwise. */
|
|
298
|
+
delete: async ({ folderId }) => {
|
|
299
|
+
await this.request("DELETE", `/folders/${requireId(folderId, "Folder ID")}`);
|
|
300
|
+
},
|
|
301
|
+
/** Moves a folder under `parentId`, or to the project root with `parentId: null`. */
|
|
302
|
+
move: async ({ folderId, parentId }) => this.request("POST", `/folders/${requireId(folderId, "Folder ID")}/move`, { body: { parentId } })
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Create a named lens at any positive version, or update it with a greater version.
|
|
307
|
+
* Supplying the same definition and version is idempotent.
|
|
308
|
+
*/
|
|
309
|
+
async ensureLens(options) {
|
|
310
|
+
if (!options?.identifier) throw invalidArgument("Lens identifier is required");
|
|
311
|
+
const [lens] = await this.ensureLenses({ lenses: [options] });
|
|
312
|
+
return lens;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Create or update several named lenses in one API request.
|
|
316
|
+
* Each lens follows the same versioning and idempotency rules as ensureLens().
|
|
317
|
+
*/
|
|
318
|
+
async ensureLenses({ lenses }) {
|
|
319
|
+
if (!Array.isArray(lenses) || lenses.length === 0) throw invalidArgument("At least one lens is required");
|
|
320
|
+
if (lenses.some((option) => !option?.identifier)) throw invalidArgument("Lens identifier is required");
|
|
321
|
+
const { ensuredLenses, pendingEnsureLenses } = this.#lenses;
|
|
322
|
+
const missingOptions = [];
|
|
323
|
+
const missingKeys = /* @__PURE__ */ new Set();
|
|
324
|
+
for (const option of lenses) {
|
|
325
|
+
const cacheKey = getEnsureLensCacheKey(option);
|
|
326
|
+
if (!ensuredLenses.has(cacheKey) && !missingKeys.has(cacheKey)) {
|
|
327
|
+
missingKeys.add(cacheKey);
|
|
328
|
+
missingOptions.push(option);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (missingOptions.length > 0) {
|
|
332
|
+
const batchCacheKey = JSON.stringify([...missingKeys].sort());
|
|
333
|
+
let pending = pendingEnsureLenses.get(batchCacheKey);
|
|
334
|
+
if (!pending) {
|
|
335
|
+
pending = this.submitEnsureLenses(missingOptions);
|
|
336
|
+
pendingEnsureLenses.set(batchCacheKey, pending);
|
|
337
|
+
pending.then(() => pendingEnsureLenses.delete(batchCacheKey), () => pendingEnsureLenses.delete(batchCacheKey));
|
|
338
|
+
}
|
|
339
|
+
await pending;
|
|
340
|
+
}
|
|
341
|
+
return lenses.map((option) => {
|
|
342
|
+
const lens = ensuredLenses.get(getEnsureLensCacheKey(option));
|
|
343
|
+
if (!lens) throw new CalmLensError(SDK_ERROR_CODES.INVALID_RESPONSE, { message: `CalmLens did not return lens "${option.identifier}"` });
|
|
344
|
+
return lens;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
async submitEnsureLenses(options) {
|
|
348
|
+
const result = await this.request("PUT", `${this.projectPath}/lenses`, { body: { lenses: options } });
|
|
349
|
+
const returned = Array.isArray(result?.lenses) ? result.lenses : [];
|
|
350
|
+
if (returned.length !== options.length) throw new CalmLensError(SDK_ERROR_CODES.INVALID_RESPONSE, { message: `Expected ${options.length} lenses but CalmLens returned ${returned.length}` });
|
|
351
|
+
const { ensuredLenses } = this.#lenses;
|
|
352
|
+
for (const [index, lens] of returned.entries()) {
|
|
353
|
+
const optionsForLens = options[index];
|
|
354
|
+
if (lens?.identifier !== optionsForLens.identifier) throw new CalmLensError(SDK_ERROR_CODES.INVALID_RESPONSE, { message: `Expected lens "${optionsForLens.identifier}" at position ${index}` });
|
|
355
|
+
for (const [cacheKey, cachedLens] of ensuredLenses) if (cachedLens.identifier === optionsForLens.identifier) ensuredLenses.delete(cacheKey);
|
|
356
|
+
ensuredLenses.set(getEnsureLensCacheKey(optionsForLens), lens);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
//#endregion
|
|
361
|
+
export { CALMLENS_ERROR_CODES, CalmLensError, CalmLensServerClient, SDK_VERSION, WEBHOOK_DELIVERY_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, WEBHOOK_TIMESTAMP_HEADER, parseWebhookEvent, verifyWebhookSignature };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import "./CalmLensError-CihGG26K.js";
|
|
2
|
+
//#endregion
|
|
3
|
+
//#region src/client/CalmLensVersion.ts
|
|
4
|
+
/** This SDK's version, from its package.json. */
|
|
5
|
+
const SDK_VERSION = "0.12.0";
|
|
6
|
+
const runtimeName = () => {
|
|
7
|
+
const runtime = globalThis;
|
|
8
|
+
if (isReactNativeRuntime()) return "react-native";
|
|
9
|
+
if (runtime.Bun?.version) return `bun/${runtime.Bun.version}`;
|
|
10
|
+
if (runtime.Deno?.version?.deno) return `deno/${runtime.Deno.version.deno}`;
|
|
11
|
+
if (runtime.navigator?.userAgent === "Cloudflare-Workers") return "workerd";
|
|
12
|
+
if (runtime.process?.versions?.node) return `node/${runtime.process.versions.node}`;
|
|
13
|
+
return "unknown";
|
|
14
|
+
};
|
|
15
|
+
/** The `User-Agent` the SDK sends from server runtimes, e.g. `calmlens-js/0.10.0 (node/22.12.0)`. */
|
|
16
|
+
const getUserAgent = () => `calmlens-js/${SDK_VERSION} (${runtimeName()})`;
|
|
17
|
+
/** True in a web page, where API keys would be visible to every visitor. */
|
|
18
|
+
const isBrowserRuntime = () => typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
|
|
19
|
+
/** True in a React Native app, where an API key ships inside the app for anyone to extract. */
|
|
20
|
+
const isReactNativeRuntime = () => globalThis.navigator?.product === "ReactNative";
|
|
21
|
+
//#endregion
|
|
22
|
+
export { isReactNativeRuntime as i, getUserAgent as n, isBrowserRuntime as r, SDK_VERSION as t };
|