@maravilla-labs/platform 0.14.0 → 0.15.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.
@@ -133,12 +133,80 @@ interface ResizeOpts {
133
133
  format: ImageFormat;
134
134
  quality?: number;
135
135
  strip_metadata?: boolean;
136
+ /**
137
+ * Crop strategy when BOTH `width` and `height` are given (the output is
138
+ * cropped to fill the exact box rather than letterboxed):
139
+ * - `'center'` — crop around the geometric center.
140
+ * - `'focal'` — crop around the source's focal point. The server injects
141
+ * the focal point from the persisted {@link FocalPointRecord} (set via
142
+ * `detectFaces` or `setFocalPoint`); if none exists it falls back to
143
+ * center.
144
+ * Omit for the default aspect-preserving fit (no crop).
145
+ */
146
+ crop?: 'focal' | 'center';
147
+ /**
148
+ * Explicit focal point for `crop: 'focal'`, normalized `0..=1`. Server-
149
+ * injected — callers normally omit this and let the platform resolve the
150
+ * stored focal point for `src_key`. Present here so the wire type mirrors
151
+ * the Rust `ResizeOpts` exactly.
152
+ */
153
+ focal?: FocalPoint;
136
154
  }
137
155
  /** Options for `transforms.ocr`. */
138
156
  interface OcrOpts {
139
157
  /** Tesseract language code(s). Defaults to `"eng"` server-side when omitted. */
140
158
  lang?: string;
141
159
  }
160
+ /**
161
+ * Options for `transforms.detectFaces` — run face detection over an image and
162
+ * persist the resulting focal point (the center of the highest-scoring face,
163
+ * or the centroid of all faces) as a {@link FocalPointRecord} keyed on the
164
+ * source. The derived output is a JSON artifact listing the detected boxes.
165
+ */
166
+ interface DetectFacesOpts {
167
+ /** Minimum detector confidence to keep a face, `0..=1`. Defaults to `0.6` server-side. */
168
+ min_score?: number;
169
+ /** Cap on the number of faces retained (highest-scoring first). Defaults to `32` server-side. */
170
+ max_faces?: number;
171
+ /** Trade accuracy for latency (smaller detector input). Defaults to `false` server-side. */
172
+ fast?: boolean;
173
+ }
174
+ /** A focal point in normalized image coordinates, each component `0..=1`. */
175
+ interface FocalPoint {
176
+ x: number;
177
+ y: number;
178
+ }
179
+ /** A single detected face box, normalized to `0..1` of the image dimensions. */
180
+ interface FaceBox {
181
+ x: number;
182
+ y: number;
183
+ w: number;
184
+ h: number;
185
+ /** Detector confidence, `0..1`. */
186
+ score: number;
187
+ }
188
+ /**
189
+ * Persisted focal-point record for a source image — the result of
190
+ * `detectFaces` or a manual `setFocalPoint`. Returned by `getFocalPoint`.
191
+ */
192
+ interface FocalPointRecord {
193
+ /** Storage key of the source image this focal point belongs to. */
194
+ src_key: string;
195
+ /** The resolved focal point, normalized `0..=1`. */
196
+ focal: FocalPoint;
197
+ /** Whether the point came from face detection or a manual override. */
198
+ source: 'faces' | 'manual';
199
+ /** Number of faces detected (`null` until a detection has run for this key). */
200
+ face_count: number | null;
201
+ /** The detected face boxes, when `source === 'faces'`. */
202
+ faces?: FaceBox[];
203
+ /** Content hash of the source bytes the detection ran against. */
204
+ src_hash?: string;
205
+ /** Version tag of the detector that produced this record. */
206
+ detector_version?: string;
207
+ /** Unix-epoch milliseconds the record was last written. */
208
+ updated_at: number;
209
+ }
142
210
  /**
143
211
  * LibreOffice-supported output container for `doc_convert`. Each value
144
212
  * maps to a `--convert-to` filter and a derived-key extension.
@@ -263,6 +331,8 @@ type TransformSpec = ({
263
331
  } & ExtractAudioOpts) | ({
264
332
  kind: 'resize';
265
333
  } & ResizeOpts) | ({
334
+ kind: 'detect_faces';
335
+ } & DetectFacesOpts) | ({
266
336
  kind: 'ocr';
267
337
  } & OcrOpts) | ({
268
338
  kind: 'doc_to_pdf';
@@ -320,6 +390,30 @@ interface TransformsService {
320
390
  resize(srcKey: string, opts: ResizeOpts): Promise<JobHandle>;
321
391
  probe(srcKey: string): Promise<MediaInfo>;
322
392
  ocr(srcKey: string, opts?: OcrOpts | null): Promise<JobHandle>;
393
+ /**
394
+ * Run face detection over an image and persist the resulting focal point as
395
+ * a {@link FocalPointRecord}. Resolves to a `JobHandle` whose `output_key`
396
+ * holds a JSON artifact of the detected {@link FaceBox | boxes} once
397
+ * complete. Subsequent `resize` calls with `crop: 'focal'` on the same
398
+ * source pick up the stored focal point automatically.
399
+ */
400
+ detectFaces(srcKey: string, opts?: DetectFacesOpts | null): Promise<JobHandle>;
401
+ /**
402
+ * Manually set (or override) the focal point for a source image, normalized
403
+ * `0..=1`. Persists a {@link FocalPointRecord} with `source: 'manual'`.
404
+ * Resolves to `null`.
405
+ */
406
+ setFocalPoint(srcKey: string, point: FocalPoint): Promise<null>;
407
+ /**
408
+ * Read the persisted {@link FocalPointRecord} for a source, or `null` when
409
+ * none has been set (neither `detectFaces` nor `setFocalPoint` has run).
410
+ */
411
+ getFocalPoint(srcKey: string): Promise<FocalPointRecord | null>;
412
+ /**
413
+ * Remove the persisted focal point for a source. Resolves to `true` when a
414
+ * record was deleted, `false` when there was nothing to clear.
415
+ */
416
+ clearFocalPoint(srcKey: string): Promise<boolean>;
323
417
  /** Convert a LibreOffice-supported document to PDF. */
324
418
  docToPdf(srcKey: string, opts?: DocToPdfOpts | null): Promise<JobHandle>;
325
419
  /** Render a single page of a document as a raster thumbnail. */
@@ -367,6 +461,16 @@ interface TransformsPatternSpec {
367
461
  /** Sugar for `resize` arrays — same shape, same semantics. */
368
462
  variants?: ResizeOpts[];
369
463
  ocr?: OcrOpts | OcrOpts[];
464
+ /**
465
+ * Run face detection on matching uploads and persist a focal point so
466
+ * `resize`/`variants` entries with `crop: 'focal'` crop around faces. Pass
467
+ * `true` for detector defaults, or an object to tune the detection.
468
+ */
469
+ detectFaces?: boolean | {
470
+ min_score?: number;
471
+ max_faces?: number;
472
+ fast?: boolean;
473
+ };
370
474
  }
371
475
  /**
372
476
  * Top-level `transforms` block. Keys are glob path patterns matched against
@@ -396,4 +500,4 @@ interface TransformsPatternSpec {
396
500
  */
397
501
  type TransformsConfig = Record<string, TransformsPatternSpec>;
398
502
 
399
- export { type DetachAudioOpts as D, type ExtractFramesOpts as E, type FramesetManifest as F, type ImageFormat as I, type JobStatus as J, type MediaInfo as M, type OcrOpts as O, type QrCodeSpec as Q, type ResizeOpts as R, type TransformsConfig as T, type VideoFormat as V, type TransformsPatternSpec as a, type TranscodeOpts as b, type ThumbnailOpts as c, type ExtractAudioOpts as d, type DocFormat as e, type DocToPdfOpts as f, type DocThumbnailOpts as g, type DocConvertOpts as h, type DocToMarkdownOpts as i, type DocToHtmlOpts as j, type ImageRef as k, type DocReplaceImagesOpts as l, type DocInsertQrCodeOpts as m, type QrPayload as n, type DocTemplateMergeOpts as o, type TransformSpec as p, type JobHandle as q, type JobStatusResponse as r, type TransformsService as s, keyFor as t, transforms as u };
503
+ export { type DetachAudioOpts as D, type ExtractFramesOpts as E, type FramesetManifest as F, type ImageFormat as I, type JobStatus as J, type MediaInfo as M, type OcrOpts as O, type QrCodeSpec as Q, type ResizeOpts as R, type TransformsConfig as T, type VideoFormat as V, type TransformsPatternSpec as a, type TranscodeOpts as b, type ThumbnailOpts as c, type ExtractAudioOpts as d, type DetectFacesOpts as e, type FocalPoint as f, type FaceBox as g, type FocalPointRecord as h, type DocFormat as i, type DocToPdfOpts as j, type DocThumbnailOpts as k, type DocConvertOpts as l, type DocToMarkdownOpts as m, type DocToHtmlOpts as n, type ImageRef as o, type DocReplaceImagesOpts as p, type DocInsertQrCodeOpts as q, type QrPayload as r, type DocTemplateMergeOpts as s, type TransformSpec as t, type JobHandle as u, type JobStatusResponse as v, type TransformsService as w, keyFor as x, transforms as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maravilla-labs/platform",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Universal platform client for Maravilla runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/config.ts CHANGED
@@ -393,6 +393,46 @@ export interface SessionConfigDefinition {
393
393
  export interface SecurityConfig {
394
394
  password_policy?: PasswordPolicyDefinition;
395
395
  session?: SessionConfigDefinition;
396
+ signup?: SignupProtectionConfig;
397
+ }
398
+
399
+ /**
400
+ * Bot protection for the registration endpoints, applied to both the hosted
401
+ * `/_auth/register` form and the `POST /api/platform/auth/register` JSON API.
402
+ *
403
+ * These layers are not equally strong:
404
+ *
405
+ * - `honeypot` (and its submit-timing check) stops untargeted form spam — bots
406
+ * that scrape the HTML and fill every field. Anyone who looks at the page
407
+ * defeats it. It is free, so it is on by default, but it is a filter, not a
408
+ * gate.
409
+ * - `rate_limit` bounds how fast anyone can register. On by default.
410
+ * - `challenge` is the only layer that imposes a real per-attempt cost. `"pow"`
411
+ * makes the client burn CPU; `"turnstile"` / `"hcaptcha"` outsource the
412
+ * judgement to a third party. Off by default.
413
+ *
414
+ * A server-to-server caller cannot solve a browser challenge, so it presents
415
+ * `signup_secret` in the `x-maravilla-signup-secret` header instead. Without
416
+ * that header the JSON endpoint enforces exactly the same challenge as the
417
+ * form — otherwise a bot would simply POST the JSON endpoint to escape.
418
+ */
419
+ export interface SignupProtectionConfig {
420
+ /** Default `"none"`. */
421
+ challenge?: 'none' | 'pow' | 'turnstile' | 'hcaptcha';
422
+ /** Leading zero bits a proof-of-work solution must have. Default 18. */
423
+ pow_difficulty?: number;
424
+ /** Default `true`. */
425
+ honeypot?: boolean;
426
+ /** Default `true`. */
427
+ rate_limit?: boolean;
428
+ /** Prefer `{ env: "VAR_NAME" }` or `"${env.VAR_NAME}"`. */
429
+ signup_secret?: SecretRef;
430
+ /** Required when `challenge` is `"turnstile"`. */
431
+ turnstile_secret?: SecretRef;
432
+ /** Required when `challenge` is `"hcaptcha"`. */
433
+ hcaptcha_secret?: SecretRef;
434
+ /** Public widget key, rendered into the page. Not a secret. */
435
+ captcha_site_key?: string;
396
436
  }
397
437
 
398
438
  // ── Branding ──
@@ -1,5 +1,5 @@
1
1
  import type { KvNamespace, KvListResult, Database, DbDocument, DbFindOptions, Storage, RealtimeService, PresenceService, AuthService, AuthCaller, AuthUser, AuthSession, AuthField, RegisterOptions, LoginOptions, CreateManagedUserOptions, UserListFilter, UserListResponse, UpdateUserOptions, Relation, AddRelationOptions, ListRelationsOptions, PolicyExplain, CanCheck, ConnectedClient, ApiKeyInfo, LoginSession, PolicyService, VectorIndexSpec, VectorIndexDescriptor, VectorQueryWithFilter, VectorSearchHit, IndexSpec, IndexDescriptor, Workflows, WorkflowHandle, WorkflowRun, WorkflowStepRecord, BrowserClient, FramesClient, BrowserJobHandle, ScreenshotRequest, PdfRequest, FramesRenderRequest } from './types.js';
2
- import type { TransformsService, TranscodeOpts, ExtractFramesOpts, ExtractAudioOpts, ThumbnailOpts, ResizeOpts, OcrOpts, DocToPdfOpts, DocThumbnailOpts, DocConvertOpts, DocToMarkdownOpts, DocToHtmlOpts, DocReplaceImagesOpts, DocInsertQrCodeOpts, DocTemplateMergeOpts, JobHandle, JobStatusResponse, MediaInfo } from './transforms.js';
2
+ import type { TransformsService, TranscodeOpts, ExtractFramesOpts, ExtractAudioOpts, ThumbnailOpts, ResizeOpts, OcrOpts, DetectFacesOpts, FocalPoint, FocalPointRecord, DocToPdfOpts, DocThumbnailOpts, DocConvertOpts, DocToMarkdownOpts, DocToHtmlOpts, DocReplaceImagesOpts, DocInsertQrCodeOpts, DocTemplateMergeOpts, JobHandle, JobStatusResponse, MediaInfo } from './transforms.js';
3
3
  import { RemoteMediaService } from './media.js';
4
4
  import { getRequestAuthHeader } from './request-scope.js';
5
5
 
@@ -133,20 +133,32 @@ class RemoteDatabase implements Database {
133
133
  return result.id;
134
134
  }
135
135
 
136
- async updateOne(collection: string, filter: any, update: any): Promise<{ modified: number }> {
136
+ async updateOne(
137
+ collection: string,
138
+ filter: any,
139
+ update: any,
140
+ ): Promise<{ modified: number; matched: number; matchedCount: number; modifiedCount: number }> {
137
141
  const response = await this.fetch(`${this.baseUrl}/api/db/${collection}/update`, {
138
142
  method: 'POST',
139
143
  body: JSON.stringify({ filter, update }),
140
144
  });
141
- return response.json() as Promise<{ modified: number }>;
142
- }
143
-
144
- async deleteOne(collection: string, filter: any): Promise<{ deleted: number }> {
145
+ return response.json() as Promise<{
146
+ modified: number;
147
+ matched: number;
148
+ matchedCount: number;
149
+ modifiedCount: number;
150
+ }>;
151
+ }
152
+
153
+ async deleteOne(
154
+ collection: string,
155
+ filter: any,
156
+ ): Promise<{ deleted: number; deletedCount: number }> {
145
157
  const response = await this.fetch(`${this.baseUrl}/api/db/${collection}/delete`, {
146
158
  method: 'DELETE',
147
159
  body: JSON.stringify(filter),
148
160
  });
149
- return response.json() as Promise<{ deleted: number }>;
161
+ return response.json() as Promise<{ deleted: number; deletedCount: number }>;
150
162
  }
151
163
 
152
164
  async deleteMany(collection: string, filter: any): Promise<{ deleted: number }> {
@@ -1300,6 +1312,49 @@ class RemoteTransformsService implements TransformsService {
1300
1312
  return this.post<JobHandle>('/ocr', { srcKey, opts: opts ?? {} });
1301
1313
  }
1302
1314
 
1315
+ detectFaces(srcKey: string, opts?: DetectFacesOpts | null): Promise<JobHandle> {
1316
+ return this.post<JobHandle>('/detect-faces', { srcKey, opts: opts ?? {} });
1317
+ }
1318
+
1319
+ async setFocalPoint(srcKey: string, point: FocalPoint): Promise<null> {
1320
+ const res = await fetch(`${this.baseUrl}/api/media/transforms/focal-point`, {
1321
+ method: 'PUT',
1322
+ headers: { 'Content-Type': 'application/json', ...this.headers, ...getRequestAuthHeader() },
1323
+ body: JSON.stringify({ srcKey, x: point.x, y: point.y }),
1324
+ });
1325
+ if (!res.ok) {
1326
+ const text = await res.text();
1327
+ throw new Error(`Focal-point set error (${res.status}): ${text}`);
1328
+ }
1329
+ return null;
1330
+ }
1331
+
1332
+ async getFocalPoint(srcKey: string): Promise<FocalPointRecord | null> {
1333
+ const res = await fetch(
1334
+ `${this.baseUrl}/api/media/transforms/focal-point?srcKey=${encodeURIComponent(srcKey)}`,
1335
+ { method: 'GET', headers: { ...this.headers, ...getRequestAuthHeader() } },
1336
+ );
1337
+ if (res.status === 404) return null;
1338
+ if (!res.ok) {
1339
+ const text = await res.text();
1340
+ throw new Error(`Focal-point lookup error (${res.status}): ${text}`);
1341
+ }
1342
+ return res.json() as Promise<FocalPointRecord>;
1343
+ }
1344
+
1345
+ async clearFocalPoint(srcKey: string): Promise<boolean> {
1346
+ const res = await fetch(
1347
+ `${this.baseUrl}/api/media/transforms/focal-point?srcKey=${encodeURIComponent(srcKey)}`,
1348
+ { method: 'DELETE', headers: { ...this.headers, ...getRequestAuthHeader() } },
1349
+ );
1350
+ if (!res.ok) {
1351
+ const text = await res.text();
1352
+ throw new Error(`Focal-point clear error (${res.status}): ${text}`);
1353
+ }
1354
+ const data = (await res.json()) as { cleared: boolean };
1355
+ return data.cleared;
1356
+ }
1357
+
1303
1358
  docToPdf(srcKey: string, opts?: DocToPdfOpts | null): Promise<JobHandle> {
1304
1359
  return this.post<JobHandle>('/doc_to_pdf', { srcKey, opts: opts ?? {} });
1305
1360
  }
package/src/transforms.ts CHANGED
@@ -187,6 +187,24 @@ export interface ResizeOpts {
187
187
  format: ImageFormat;
188
188
  quality?: number;
189
189
  strip_metadata?: boolean;
190
+ /**
191
+ * Crop strategy when BOTH `width` and `height` are given (the output is
192
+ * cropped to fill the exact box rather than letterboxed):
193
+ * - `'center'` — crop around the geometric center.
194
+ * - `'focal'` — crop around the source's focal point. The server injects
195
+ * the focal point from the persisted {@link FocalPointRecord} (set via
196
+ * `detectFaces` or `setFocalPoint`); if none exists it falls back to
197
+ * center.
198
+ * Omit for the default aspect-preserving fit (no crop).
199
+ */
200
+ crop?: 'focal' | 'center';
201
+ /**
202
+ * Explicit focal point for `crop: 'focal'`, normalized `0..=1`. Server-
203
+ * injected — callers normally omit this and let the platform resolve the
204
+ * stored focal point for `src_key`. Present here so the wire type mirrors
205
+ * the Rust `ResizeOpts` exactly.
206
+ */
207
+ focal?: FocalPoint;
190
208
  }
191
209
 
192
210
  /** Options for `transforms.ocr`. */
@@ -195,6 +213,60 @@ export interface OcrOpts {
195
213
  lang?: string;
196
214
  }
197
215
 
216
+ /**
217
+ * Options for `transforms.detectFaces` — run face detection over an image and
218
+ * persist the resulting focal point (the center of the highest-scoring face,
219
+ * or the centroid of all faces) as a {@link FocalPointRecord} keyed on the
220
+ * source. The derived output is a JSON artifact listing the detected boxes.
221
+ */
222
+ export interface DetectFacesOpts {
223
+ /** Minimum detector confidence to keep a face, `0..=1`. Defaults to `0.6` server-side. */
224
+ min_score?: number;
225
+ /** Cap on the number of faces retained (highest-scoring first). Defaults to `32` server-side. */
226
+ max_faces?: number;
227
+ /** Trade accuracy for latency (smaller detector input). Defaults to `false` server-side. */
228
+ fast?: boolean;
229
+ }
230
+
231
+ /** A focal point in normalized image coordinates, each component `0..=1`. */
232
+ export interface FocalPoint {
233
+ x: number;
234
+ y: number;
235
+ }
236
+
237
+ /** A single detected face box, normalized to `0..1` of the image dimensions. */
238
+ export interface FaceBox {
239
+ x: number;
240
+ y: number;
241
+ w: number;
242
+ h: number;
243
+ /** Detector confidence, `0..1`. */
244
+ score: number;
245
+ }
246
+
247
+ /**
248
+ * Persisted focal-point record for a source image — the result of
249
+ * `detectFaces` or a manual `setFocalPoint`. Returned by `getFocalPoint`.
250
+ */
251
+ export interface FocalPointRecord {
252
+ /** Storage key of the source image this focal point belongs to. */
253
+ src_key: string;
254
+ /** The resolved focal point, normalized `0..=1`. */
255
+ focal: FocalPoint;
256
+ /** Whether the point came from face detection or a manual override. */
257
+ source: 'faces' | 'manual';
258
+ /** Number of faces detected (`null` until a detection has run for this key). */
259
+ face_count: number | null;
260
+ /** The detected face boxes, when `source === 'faces'`. */
261
+ faces?: FaceBox[];
262
+ /** Content hash of the source bytes the detection ran against. */
263
+ src_hash?: string;
264
+ /** Version tag of the detector that produced this record. */
265
+ detector_version?: string;
266
+ /** Unix-epoch milliseconds the record was last written. */
267
+ updated_at: number;
268
+ }
269
+
198
270
  /**
199
271
  * LibreOffice-supported output container for `doc_convert`. Each value
200
272
  * maps to a `--convert-to` filter and a derived-key extension.
@@ -326,6 +398,7 @@ export type TransformSpec =
326
398
  | ({ kind: 'extract_frames' } & ExtractFramesOpts)
327
399
  | ({ kind: 'extract_audio' } & ExtractAudioOpts)
328
400
  | ({ kind: 'resize' } & ResizeOpts)
401
+ | ({ kind: 'detect_faces' } & DetectFacesOpts)
329
402
  | ({ kind: 'ocr' } & OcrOpts)
330
403
  | ({ kind: 'doc_to_pdf' } & DocToPdfOpts)
331
404
  | ({ kind: 'doc_thumbnail' } & DocThumbnailOpts)
@@ -380,6 +453,30 @@ export interface TransformsService {
380
453
  resize(srcKey: string, opts: ResizeOpts): Promise<JobHandle>;
381
454
  probe(srcKey: string): Promise<MediaInfo>;
382
455
  ocr(srcKey: string, opts?: OcrOpts | null): Promise<JobHandle>;
456
+ /**
457
+ * Run face detection over an image and persist the resulting focal point as
458
+ * a {@link FocalPointRecord}. Resolves to a `JobHandle` whose `output_key`
459
+ * holds a JSON artifact of the detected {@link FaceBox | boxes} once
460
+ * complete. Subsequent `resize` calls with `crop: 'focal'` on the same
461
+ * source pick up the stored focal point automatically.
462
+ */
463
+ detectFaces(srcKey: string, opts?: DetectFacesOpts | null): Promise<JobHandle>;
464
+ /**
465
+ * Manually set (or override) the focal point for a source image, normalized
466
+ * `0..=1`. Persists a {@link FocalPointRecord} with `source: 'manual'`.
467
+ * Resolves to `null`.
468
+ */
469
+ setFocalPoint(srcKey: string, point: FocalPoint): Promise<null>;
470
+ /**
471
+ * Read the persisted {@link FocalPointRecord} for a source, or `null` when
472
+ * none has been set (neither `detectFaces` nor `setFocalPoint` has run).
473
+ */
474
+ getFocalPoint(srcKey: string): Promise<FocalPointRecord | null>;
475
+ /**
476
+ * Remove the persisted focal point for a source. Resolves to `true` when a
477
+ * record was deleted, `false` when there was nothing to clear.
478
+ */
479
+ clearFocalPoint(srcKey: string): Promise<boolean>;
383
480
  /** Convert a LibreOffice-supported document to PDF. */
384
481
  docToPdf(srcKey: string, opts?: DocToPdfOpts | null): Promise<JobHandle>;
385
482
  /** Render a single page of a document as a raster thumbnail. */
@@ -452,6 +549,9 @@ function outputExtension(spec: TransformSpec): string {
452
549
  const fmt = (spec as { format?: ImageFormat }).format ?? 'jpg';
453
550
  return fmt;
454
551
  }
552
+ case 'detect_faces':
553
+ // Output key is the JSON artifact listing the detected face boxes.
554
+ return 'json';
455
555
  case 'ocr':
456
556
  return 'txt';
457
557
  case 'doc_to_pdf':
@@ -533,6 +633,12 @@ export interface TransformsPatternSpec {
533
633
  /** Sugar for `resize` arrays — same shape, same semantics. */
534
634
  variants?: ResizeOpts[];
535
635
  ocr?: OcrOpts | OcrOpts[];
636
+ /**
637
+ * Run face detection on matching uploads and persist a focal point so
638
+ * `resize`/`variants` entries with `crop: 'focal'` crop around faces. Pass
639
+ * `true` for detector defaults, or an object to tune the detection.
640
+ */
641
+ detectFaces?: boolean | { min_score?: number; max_faces?: number; fast?: boolean };
536
642
  }
537
643
 
538
644
  /**
package/src/types.ts CHANGED
@@ -206,37 +206,46 @@ export interface Database {
206
206
 
207
207
  /**
208
208
  * Update a single document in a collection.
209
- *
209
+ *
210
210
  * @param collection - The collection name
211
211
  * @param filter - Query filter to match the document to update
212
212
  * @param update - Update operations (MongoDB-style with $set, $inc, etc.)
213
- * @returns Promise that resolves to an object with the number of modified documents
214
- *
213
+ * @returns Promise that resolves to real matched/modified counts `modified`
214
+ * is 0 when the filter matched nothing, so a conditional filter plus an
215
+ * atomic operator is safe for enforcement (quotas, redemption caps, seats)
216
+ *
215
217
  * @example
216
218
  * ```typescript
217
- * const result = await db.updateOne('users',
218
- * { email: 'john@example.com' },
219
- * { $set: { lastLogin: new Date() }, $inc: { loginCount: 1 } }
219
+ * // Atomic redemption-cap enforcement: only one of N concurrent checkouts
220
+ * // can win each remaining slot — no read-then-act needed.
221
+ * const result = await db.updateOne('discount_codes',
222
+ * { id, redeemedCount: { $lt: maxRedemptions } },
223
+ * { $inc: { redeemedCount: 1 } }
220
224
  * );
221
- * console.log(`Modified ${result.modified} documents`);
225
+ * if (result.modified === 0) throw new Error('code exhausted');
222
226
  * ```
223
227
  */
224
- updateOne(collection: string, filter: any, update: any): Promise<{ modified: number }>;
228
+ updateOne(
229
+ collection: string,
230
+ filter: any,
231
+ update: any,
232
+ ): Promise<{ modified: number; matched: number; matchedCount: number; modifiedCount: number }>;
225
233
 
226
234
  /**
227
235
  * Delete a single document from a collection.
228
- *
236
+ *
229
237
  * @param collection - The collection name
230
238
  * @param filter - Query filter to match the document to delete
231
- * @returns Promise that resolves to an object with the number of deleted documents
232
- *
239
+ * @returns Promise that resolves to the real number of deleted documents
240
+ * (0 when the filter matched nothing)
241
+ *
233
242
  * @example
234
243
  * ```typescript
235
244
  * const result = await db.deleteOne('posts', { _id: postId });
236
245
  * console.log(`Deleted ${result.deleted} documents`);
237
246
  * ```
238
247
  */
239
- deleteOne(collection: string, filter: any): Promise<{ deleted: number }>;
248
+ deleteOne(collection: string, filter: any): Promise<{ deleted: number; deletedCount: number }>;
240
249
 
241
250
  /**
242
251
  * Delete multiple documents from a collection.