@mediaviz/sdk 1.8.0-dev.91 → 1.8.0-dev.95

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/dist/sdk.cjs CHANGED
@@ -461,6 +461,448 @@ function requireTypes () {
461
461
  return types;
462
462
  }
463
463
 
464
+ var src$1;
465
+ var hasRequiredSrc$1;
466
+
467
+ function requireSrc$1 () {
468
+ if (hasRequiredSrc$1) return src$1;
469
+ hasRequiredSrc$1 = 1;
470
+
471
+ const { OAuthClient } = requireClient();
472
+ const { OAuthError } = requireErrors();
473
+ const { OAuthErrorCode } = requireTypes();
474
+
475
+ src$1 = { OAuthClient, OAuthError, OAuthErrorCode };
476
+ return src$1;
477
+ }
478
+
479
+ var srcExports$1 = requireSrc$1();
480
+ var _oauth = /*@__PURE__*/getDefaultExportFromCjs(srcExports$1);
481
+
482
+ // Auto-generated — do not edit
483
+ const { OAuthClient, OAuthError, OAuthErrorCode } = _oauth;
484
+
485
+ var signing;
486
+ var hasRequiredSigning;
487
+
488
+ function requireSigning () {
489
+ if (hasRequiredSigning) return signing;
490
+ hasRequiredSigning = 1;
491
+
492
+ // HMAC scheme shared with the MediaViz producer: X-Signature = 'sha256=' +
493
+ // hex(HMAC-SHA256(secret, `${X-Timestamp}.` + rawBody)). The signing string is
494
+ // the timestamp joined to the *raw request body bytes* — never re-serialized
495
+ // JSON. Verification accepts the current and (during rotation) previous
496
+ // secret, rejects timestamps outside the skew window, and compares digests in
497
+ // constant time. Web Crypto only, so the same code runs in Node >= 18 and
498
+ // browsers.
499
+
500
+ const _encoder = new TextEncoder();
501
+
502
+ async function signWebhookPayload(secret, timestamp, rawBody) {
503
+ const key = await globalThis.crypto.subtle.importKey(
504
+ 'raw', _encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
505
+ const payload = _concat(_encoder.encode(`${timestamp}.`), toBytes(rawBody));
506
+ const digest = await globalThis.crypto.subtle.sign('HMAC', key, payload);
507
+ return 'sha256=' + _hex(new Uint8Array(digest));
508
+ }
509
+
510
+ async function verifyWebhookSignature(secretCurrent, secretPrevious, headers, rawBody, options = {}) {
511
+ const h = lowerHeaders(headers);
512
+ const timestamp = h['x-timestamp'];
513
+ const signature = h['x-signature'];
514
+ if (!timestamp || !signature) return false;
515
+ if (!/^-?\d+$/.test(String(timestamp))) return false;
516
+ const skewToleranceS = options.skewToleranceS ?? 300;
517
+ const now = options.now ?? Math.floor(Date.now() / 1000);
518
+ if (Math.abs(now - Number(timestamp)) > skewToleranceS) return false;
519
+ for (const secret of [secretCurrent, secretPrevious]) {
520
+ if (!secret) continue;
521
+ const expected = await signWebhookPayload(secret, String(timestamp), rawBody);
522
+ if (_constantTimeEqual(expected, String(signature))) return true;
523
+ }
524
+ return false;
525
+ }
526
+
527
+ // helpers
528
+ function toBytes(rawBody) {
529
+ if (typeof rawBody === 'string') return _encoder.encode(rawBody);
530
+ if (rawBody instanceof Uint8Array) return rawBody;
531
+ if (rawBody instanceof ArrayBuffer) return new Uint8Array(rawBody);
532
+ throw new TypeError('rawBody must be a string, Uint8Array, or ArrayBuffer');
533
+ }
534
+
535
+ function lowerHeaders(headers) {
536
+ const out = {};
537
+ const entries = typeof headers?.entries === 'function' ? headers.entries() : Object.entries(headers ?? {});
538
+ for (const [k, v] of entries) out[String(k).toLowerCase()] = v;
539
+ return out;
540
+ }
541
+
542
+ function _concat(a, b) {
543
+ const out = new Uint8Array(a.length + b.length);
544
+ out.set(a, 0);
545
+ out.set(b, a.length);
546
+ return out;
547
+ }
548
+
549
+ function _hex(bytes) {
550
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
551
+ }
552
+
553
+ function _constantTimeEqual(a, b) {
554
+ if (a.length !== b.length) return false;
555
+ let diff = 0;
556
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
557
+ return diff === 0;
558
+ }
559
+
560
+ signing = { signWebhookPayload, verifyWebhookSignature, toBytes, lowerHeaders };
561
+ return signing;
562
+ }
563
+
564
+ var store;
565
+ var hasRequiredStore;
566
+
567
+ function requireStore () {
568
+ if (hasRequiredStore) return store;
569
+ hasRequiredStore = 1;
570
+
571
+ // Persistence seam for the webhook consumer: dedupe ledger, signing secrets,
572
+ // and pull cursors. The default in-memory store suits a single long-lived
573
+ // process (worker, dev server); multi-process consumers supply an object with
574
+ // the same shape backed by Redis or a database.
575
+
576
+ const MAX_SEEN_ENTRIES = 100000;
577
+
578
+ class InMemoryWebhookStore {
579
+ constructor() {
580
+ this._seen = new Map(); // eventId -> expiresAt (ms)
581
+ this._secrets = new Map(); // subscriptionId -> {current, previous, previousExpiresAt}
582
+ this._cursors = new Map(); // subscriptionId -> cursor
583
+ }
584
+
585
+ markSeen(eventId, ttlS) {
586
+ const now = Date.now();
587
+ const expiresAt = this._seen.get(eventId);
588
+ if (expiresAt !== undefined && expiresAt > now) return false;
589
+ this._seen.delete(eventId);
590
+ this._seen.set(eventId, now + ttlS * 1000);
591
+ this._prune(now);
592
+ return true;
593
+ }
594
+
595
+ unmarkSeen(eventId) {
596
+ this._seen.delete(eventId);
597
+ }
598
+
599
+ saveSecret(subscriptionId, secret) {
600
+ this._secrets.set(subscriptionId, { current: secret, previous: null, previousExpiresAt: 0 });
601
+ }
602
+
603
+ rotateSecret(subscriptionId, newSecret, windowS) {
604
+ const entry = this._secrets.get(subscriptionId);
605
+ this._secrets.set(subscriptionId, {
606
+ current: newSecret,
607
+ previous: entry?.current ?? null,
608
+ previousExpiresAt: Date.now() + windowS * 1000,
609
+ });
610
+ }
611
+
612
+ getSecrets(subscriptionId) {
613
+ const entry = this._secrets.get(subscriptionId);
614
+ if (!entry) return [null, null];
615
+ const previous = entry.previous && entry.previousExpiresAt > Date.now() ? entry.previous : null;
616
+ return [entry.current, previous];
617
+ }
618
+
619
+ getCursor(subscriptionId) {
620
+ return this._cursors.get(subscriptionId) ?? null;
621
+ }
622
+
623
+ setCursor(subscriptionId, cursor) {
624
+ this._cursors.set(subscriptionId, cursor);
625
+ }
626
+
627
+ _prune(now) {
628
+ if (this._seen.size <= MAX_SEEN_ENTRIES) return;
629
+ for (const [id, expiresAt] of this._seen) {
630
+ if (expiresAt <= now) this._seen.delete(id);
631
+ if (this._seen.size <= MAX_SEEN_ENTRIES) return;
632
+ }
633
+ // still over the cap: evict oldest insertions
634
+ for (const id of this._seen.keys()) {
635
+ this._seen.delete(id);
636
+ if (this._seen.size <= MAX_SEEN_ENTRIES) return;
637
+ }
638
+ }
639
+ }
640
+
641
+ store = { InMemoryWebhookStore };
642
+ return store;
643
+ }
644
+
645
+ var consumer;
646
+ var hasRequiredConsumer;
647
+
648
+ function requireConsumer () {
649
+ if (hasRequiredConsumer) return consumer;
650
+ hasRequiredConsumer = 1;
651
+
652
+ const { verifyWebhookSignature, toBytes, lowerHeaders } = requireSigning();
653
+ const { InMemoryWebhookStore } = requireStore();
654
+
655
+ // Photo columns each producer model writes — used to select a model's results
656
+ // out of a fetched PhotoDisplay. Mirrors the producer vocabulary; personhood is
657
+ // predicate-based and writes no columns.
658
+ const WEBHOOK_MODEL_COLUMNS = {
659
+ blur: ['blur_value'],
660
+ colors: ['main_color_palette', 'color_averages', 'hue_values', 'light_values', 'saturation_values'],
661
+ face_recognition: ['number_of_faces', 'bounding_boxes_from_faces_model'],
662
+ image_classification: ['labels_from_classifications_model', 'feature_extraction_output'],
663
+ image_comparison: ['average_hash'],
664
+ image_describe: ['topic'],
665
+ ocr: ['ocr_output'],
666
+ similarity: ['similar_photo_ids_high', 'similar_photo_ids_medium', 'similar_photo_ids_low'],
667
+ evidence: ['evidence_score', 'aesthetic_score', 'face_score', 'content_score', 'similarity_set_ranking'],
668
+ personhood: [],
669
+ };
670
+
671
+ const WEBHOOK_OUTCOME_MODELS = {
672
+ curation: ['blur', 'colors', 'face_recognition', 'image_classification', 'image_comparison', 'similarity', 'evidence'],
673
+ similarity: ['image_classification', 'image_comparison', 'similarity'],
674
+ persons: ['face_recognition', 'personhood'],
675
+ };
676
+
677
+ // Client-side counterpart of the MediaViz webhook producer, giving photo-by-photo
678
+ // progress as a project upload is analyzed.
679
+ //
680
+ // Receiver half (no auth needed): classify inbound POSTs, echo the subscription
681
+ // verification challenge, verify signed `target.completed` deliveries (HMAC +
682
+ // timestamp skew), dedupe on event_id, and dispatch each new event to a single
683
+ // onEvent callback. `handleRequest` is framework-agnostic glue — adapt it to
684
+ // express/lambda/etc. in a few lines by feeding it the raw body and headers.
685
+ //
686
+ // API half (requires an authenticated client): subscription lifecycle
687
+ // orchestration — register/confirm/rotate store the signing secret the moment
688
+ // the producer issues it — plus history pulls that share the push dedupe
689
+ // ledger, and out-of-band result fetches (webhooks carry a pointer, never the
690
+ // result itself).
691
+ class WebhookConsumer {
692
+ constructor(ctx, subscriptionApi, options = {}) {
693
+ this._ctx = ctx;
694
+ this._api = subscriptionApi;
695
+ this.store = options.store ?? new InMemoryWebhookStore();
696
+ this.skewToleranceS = options.skewToleranceS ?? 300;
697
+ this.rotationWindowS = options.rotationWindowS ?? 86400;
698
+ this.dedupeTtlS = options.dedupeTtlS ?? 604800;
699
+ this.pageLimit = options.pageLimit ?? 100;
700
+ this._onEvent = options.onEvent ?? null;
701
+ }
702
+
703
+ // ── receiver half ─────────────────────────────────────────────────────────
704
+
705
+ onEvent(fn) {
706
+ this._onEvent = fn;
707
+ return fn;
708
+ }
709
+
710
+ useStore(store) {
711
+ this.store = store;
712
+ }
713
+
714
+ classifyMessage(body) {
715
+ const type = body?.event_type;
716
+ if (type === 'subscription.verification') return 'verification';
717
+ if (type === 'target.completed') return 'delivery';
718
+ return 'unknown';
719
+ }
720
+
721
+ handleChallenge(body) {
722
+ return { challenge: body.challenge };
723
+ }
724
+
725
+ async handleDelivery(headers, rawBody) {
726
+ let event;
727
+ try {
728
+ event = this._parseDelivery(rawBody);
729
+ } catch {
730
+ return 'bad_signature';
731
+ }
732
+ const [current, previous] = this.store.getSecrets(String(event.subscription_id));
733
+ const ok = await verifyWebhookSignature(current, previous, headers, rawBody, { skewToleranceS: this.skewToleranceS });
734
+ if (!ok) return 'bad_signature';
735
+ return this._dispatch(event);
736
+ }
737
+
738
+ async handleRequest(headers, rawBody) {
739
+ let body;
740
+ try {
741
+ body = JSON.parse(_text(rawBody));
742
+ } catch {
743
+ return { status: 400, body: { detail: 'invalid JSON' } };
744
+ }
745
+ switch (this.classifyMessage(body)) {
746
+ case 'verification':
747
+ return { status: 200, body: this.handleChallenge(body) };
748
+ case 'delivery': {
749
+ const ack = await this.handleDelivery(lowerHeaders(headers), rawBody);
750
+ if (ack === 'bad_signature') return { status: 401, body: { detail: 'bad signature' } };
751
+ return { status: 200, body: { status: ack } };
752
+ }
753
+ default:
754
+ return { status: 400, body: { detail: 'unrecognized event_type' } };
755
+ }
756
+ }
757
+
758
+ // ── API half ──────────────────────────────────────────────────────────────
759
+
760
+ async register(projectTableName, callbackUrl, targets) {
761
+ const res = await this._api.createSubscription(projectTableName, callbackUrl, targets);
762
+ this._saveSecretFrom(res);
763
+ return res;
764
+ }
765
+
766
+ async confirm(subscriptionId) {
767
+ const res = await this._api.verifySubscription(subscriptionId);
768
+ this._saveSecretFrom(res);
769
+ return res;
770
+ }
771
+
772
+ async rotateSecret(subscriptionId) {
773
+ const res = await this._api.rotateSubscriptionSecret(subscriptionId);
774
+ this.store.rotateSecret(String(subscriptionId), res.signing_secret, this.rotationWindowS);
775
+ return res.signing_secret;
776
+ }
777
+
778
+ async updateCallback(subscriptionId, callbackUrl) {
779
+ // A callback change resets the subscription to pending_verification and
780
+ // pauses push until the new URL passes the challenge — confirm right away.
781
+ await this._api.updateSubscription(subscriptionId, { callbackUrl });
782
+ return this.confirm(subscriptionId);
783
+ }
784
+
785
+ async disable(subscriptionId) {
786
+ return this._api.deleteSubscription(subscriptionId);
787
+ }
788
+
789
+ async listSubscriptions() {
790
+ return this._api.listSubscriptions();
791
+ }
792
+
793
+ async pullEvents(subscriptionId, { since, limit } = {}) {
794
+ return this._api.listSubscriptionEvents(subscriptionId, { since, limit: limit ?? this.pageLimit });
795
+ }
796
+
797
+ async reconcile(subscriptionId, projectTableName) {
798
+ const id = String(subscriptionId);
799
+ let cursor = this.store.getCursor(id);
800
+ let pulled = 0;
801
+ let dispatched = 0;
802
+ for (;;) {
803
+ const page = await this._api.listSubscriptionEvents(id, { since: cursor ?? undefined, limit: this.pageLimit });
804
+ const events = page?.events ?? [];
805
+ pulled += events.length;
806
+ for (const ev of events) {
807
+ const ack = await this._dispatch({
808
+ event_id: ev.event_id,
809
+ event_type: 'target.completed',
810
+ schema_version: '1',
811
+ subscription_id: id,
812
+ project_table_name: projectTableName,
813
+ photo_id: ev.photo_id,
814
+ target: ev.target,
815
+ completed_at: ev.completed_at,
816
+ result_location: this._ctx.baseUrl + _photoPath(projectTableName, ev.photo_id),
817
+ });
818
+ if (ack === 'handled') dispatched += 1;
819
+ }
820
+ if (page?.next_cursor && page.next_cursor !== cursor) {
821
+ cursor = page.next_cursor;
822
+ this.store.setCursor(id, cursor);
823
+ }
824
+ if (!events.length || events.length < this.pageLimit) break;
825
+ }
826
+ return { pulled, dispatched };
827
+ }
828
+
829
+ async fetchResult(event) {
830
+ this._ctx.requireTokens();
831
+ const path = _photoPath(event.project_table_name, event.photo_id);
832
+ const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
833
+ return { photo: data, selected: selectResultColumns(data, event.target) };
834
+ }
835
+
836
+ saveSecret(subscriptionId, secret) {
837
+ this.store.saveSecret(String(subscriptionId), secret);
838
+ }
839
+
840
+ // ── internal ──────────────────────────────────────────────────────────────
841
+
842
+ _saveSecretFrom(res) {
843
+ if (res?.signing_secret && res?.subscription_id != null) {
844
+ this.store.saveSecret(String(res.subscription_id), res.signing_secret);
845
+ }
846
+ }
847
+
848
+ _parseDelivery(rawBody) {
849
+ const event = JSON.parse(_text(rawBody));
850
+ for (const key of ['event_id', 'subscription_id', 'target']) {
851
+ if (typeof event?.[key] !== 'string' || !event[key]) throw new Error(`missing ${key}`);
852
+ }
853
+ if (!Number.isInteger(event.photo_id)) throw new Error('photo_id must be an integer');
854
+ return event;
855
+ }
856
+
857
+ async _dispatch(event) {
858
+ if (!this.store.markSeen(String(event.event_id), this.dedupeTtlS)) return 'duplicate';
859
+ if (this._onEvent) {
860
+ try {
861
+ await this._onEvent(event);
862
+ } catch (e) {
863
+ // release the dedupe claim so the producer's retry redelivers
864
+ this.store.unmarkSeen(String(event.event_id));
865
+ throw e;
866
+ }
867
+ }
868
+ return 'handled';
869
+ }
870
+ }
871
+
872
+ // helpers
873
+ function selectResultColumns(photo, target) {
874
+ if (target === 'all') return null;
875
+ const sep = String(target).indexOf(':');
876
+ const kind = sep === -1 ? String(target) : String(target).slice(0, sep);
877
+ const name = sep === -1 ? '' : String(target).slice(sep + 1);
878
+ let columns;
879
+ if (kind === 'model') {
880
+ columns = WEBHOOK_MODEL_COLUMNS[name];
881
+ if (!columns) throw new Error(`unknown model '${name}'`);
882
+ } else if (kind === 'outcome') {
883
+ const models = WEBHOOK_OUTCOME_MODELS[name];
884
+ if (!models) throw new Error(`unknown outcome '${name}'`);
885
+ columns = [...new Set(models.flatMap((m) => WEBHOOK_MODEL_COLUMNS[m]))];
886
+ } else {
887
+ throw new Error(`unknown target grammar '${target}' (expected 'model:<name>', 'outcome:<name>', or 'all')`);
888
+ }
889
+ const selected = {};
890
+ for (const c of columns) selected[c] = photo?.[c] ?? null;
891
+ return selected;
892
+ }
893
+
894
+ function _photoPath(projectTableName, photoId) {
895
+ return `/api/v1/photos/${encodeURIComponent(projectTableName)}/${encodeURIComponent(String(photoId))}`;
896
+ }
897
+
898
+ function _text(rawBody) {
899
+ return typeof rawBody === 'string' ? rawBody : new TextDecoder().decode(toBytes(rawBody));
900
+ }
901
+
902
+ consumer = { WebhookConsumer, selectResultColumns, WEBHOOK_MODEL_COLUMNS, WEBHOOK_OUTCOME_MODELS };
903
+ return consumer;
904
+ }
905
+
464
906
  var src;
465
907
  var hasRequiredSrc;
466
908
 
@@ -468,19 +910,19 @@ function requireSrc () {
468
910
  if (hasRequiredSrc) return src;
469
911
  hasRequiredSrc = 1;
470
912
 
471
- const { OAuthClient } = requireClient();
472
- const { OAuthError } = requireErrors();
473
- const { OAuthErrorCode } = requireTypes();
913
+ const { WebhookConsumer, selectResultColumns, WEBHOOK_MODEL_COLUMNS, WEBHOOK_OUTCOME_MODELS } = requireConsumer();
914
+ const { InMemoryWebhookStore } = requireStore();
915
+ const { signWebhookPayload, verifyWebhookSignature } = requireSigning();
474
916
 
475
- src = { OAuthClient, OAuthError, OAuthErrorCode };
917
+ src = { WebhookConsumer, InMemoryWebhookStore, signWebhookPayload, verifyWebhookSignature, selectResultColumns, WEBHOOK_MODEL_COLUMNS, WEBHOOK_OUTCOME_MODELS };
476
918
  return src;
477
919
  }
478
920
 
479
921
  var srcExports = requireSrc();
480
- var _oauth = /*@__PURE__*/getDefaultExportFromCjs(srcExports);
922
+ var _webhooks = /*@__PURE__*/getDefaultExportFromCjs(srcExports);
481
923
 
482
924
  // Auto-generated — do not edit
483
- const { OAuthClient, OAuthError, OAuthErrorCode } = _oauth;
925
+ const { WebhookConsumer, InMemoryWebhookStore, signWebhookPayload, verifyWebhookSignature, selectResultColumns, WEBHOOK_MODEL_COLUMNS, WEBHOOK_OUTCOME_MODELS } = _webhooks;
484
926
 
485
927
  class AiModelCredits {
486
928
  constructor(ctx) { this._ctx = ctx; }
@@ -1196,6 +1638,13 @@ class Person {
1196
1638
  const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
1197
1639
  return data;
1198
1640
  }
1641
+
1642
+ async removePerson(projectTableName, personId) {
1643
+ this._ctx.requireTokens();
1644
+ const path = `/api/v1/person/${encodeURIComponent(projectTableName)}/${encodeURIComponent(personId)}`;
1645
+ const { data } = await this._ctx.client.request(path, 'DELETE', this._ctx.accessToken, this._ctx.refreshToken);
1646
+ return data;
1647
+ }
1199
1648
  }
1200
1649
 
1201
1650
  function stripUndef$3(o) { const r = {}; for (const k in o) if (o[k] !== undefined) r[k] = o[k]; return r; }
@@ -1203,7 +1652,7 @@ function stripUndef$3(o) { const r = {}; for (const k in o) if (o[k] !== undefin
1203
1652
  class Projects {
1204
1653
  constructor(ctx) { this._ctx = ctx; }
1205
1654
 
1206
- async createProjectAndRun(name, private_ = undefined, type = undefined, description = undefined, directory = undefined, photoUploadVector = undefined, thumbnail = undefined, runName = undefined, { outcomes, models } = {}) {
1655
+ async createProjectAndRun(name, type, private_ = undefined, description = undefined, directory = undefined, photoUploadVector = undefined, thumbnail = undefined, runName = undefined, { outcomes, models } = {}) {
1207
1656
  this._ctx.requireTokens();
1208
1657
  let path = `/api/v1/project_outcome/`;
1209
1658
  const query = new URLSearchParams();
@@ -1896,6 +2345,7 @@ class MediaViz {
1896
2345
  this.search = new Search(_ctx);
1897
2346
  this.subscription = new Subscription(_ctx);
1898
2347
  this.users = new Users(_ctx);
2348
+ this.webhooks = new WebhookConsumer(_ctx, this.subscription);
1899
2349
  }
1900
2350
 
1901
2351
  async authenticate() {
@@ -1933,6 +2383,7 @@ exports.CuratedAlbums = CuratedAlbums;
1933
2383
  exports.CustomAlbums = CustomAlbums;
1934
2384
  exports.EmailTokens = EmailTokens;
1935
2385
  exports.Health = Health;
2386
+ exports.InMemoryWebhookStore = InMemoryWebhookStore;
1936
2387
  exports.Keywords = Keywords;
1937
2388
  exports.MediaViz = MediaViz;
1938
2389
  exports.NotFoundError = NotFoundError;
@@ -1952,4 +2403,10 @@ exports.ServerError = ServerError;
1952
2403
  exports.Subscription = Subscription;
1953
2404
  exports.Users = Users;
1954
2405
  exports.ValidationError = ValidationError;
2406
+ exports.WEBHOOK_MODEL_COLUMNS = WEBHOOK_MODEL_COLUMNS;
2407
+ exports.WEBHOOK_OUTCOME_MODELS = WEBHOOK_OUTCOME_MODELS;
2408
+ exports.WebhookConsumer = WebhookConsumer;
1955
2409
  exports.handleResponse = handleResponse;
2410
+ exports.selectResultColumns = selectResultColumns;
2411
+ exports.signWebhookPayload = signWebhookPayload;
2412
+ exports.verifyWebhookSignature = verifyWebhookSignature;
package/dist/sdk.d.cts CHANGED
@@ -60,6 +60,79 @@ export class ServerError extends ApiError {
60
60
 
61
61
  export function handleResponse(response: Response): Promise<any>;
62
62
 
63
+ export type WebhookAck = 'handled' | 'duplicate' | 'bad_signature';
64
+
65
+ export interface WebhookDeliveryEvent {
66
+ event_id: string;
67
+ event_type: string;
68
+ schema_version?: string;
69
+ subscription_id: string;
70
+ project_table_name: string;
71
+ photo_id: number;
72
+ target: string;
73
+ completed_at?: string;
74
+ result_location?: string;
75
+ [key: string]: any;
76
+ }
77
+
78
+ export interface WebhookStoreLike {
79
+ markSeen(eventId: string, ttlS: number): boolean;
80
+ unmarkSeen(eventId: string): void;
81
+ saveSecret(subscriptionId: string, secret: string): void;
82
+ rotateSecret(subscriptionId: string, newSecret: string, windowS: number): void;
83
+ getSecrets(subscriptionId: string): [string | null, string | null];
84
+ getCursor(subscriptionId: string): string | null;
85
+ setCursor(subscriptionId: string, cursor: string): void;
86
+ }
87
+
88
+ export class InMemoryWebhookStore implements WebhookStoreLike {
89
+ markSeen(eventId: string, ttlS: number): boolean;
90
+ unmarkSeen(eventId: string): void;
91
+ saveSecret(subscriptionId: string, secret: string): void;
92
+ rotateSecret(subscriptionId: string, newSecret: string, windowS: number): void;
93
+ getSecrets(subscriptionId: string): [string | null, string | null];
94
+ getCursor(subscriptionId: string): string | null;
95
+ setCursor(subscriptionId: string, cursor: string): void;
96
+ }
97
+
98
+ export class WebhookConsumer {
99
+ constructor(ctx: any, subscriptionApi: any, options?: {
100
+ store?: WebhookStoreLike;
101
+ onEvent?: (event: WebhookDeliveryEvent) => void | Promise<void>;
102
+ skewToleranceS?: number;
103
+ rotationWindowS?: number;
104
+ dedupeTtlS?: number;
105
+ pageLimit?: number;
106
+ });
107
+ store: WebhookStoreLike;
108
+ skewToleranceS: number;
109
+ rotationWindowS: number;
110
+ dedupeTtlS: number;
111
+ pageLimit: number;
112
+ onEvent(fn: (event: WebhookDeliveryEvent) => void | Promise<void>): (event: WebhookDeliveryEvent) => void | Promise<void>;
113
+ useStore(store: WebhookStoreLike): void;
114
+ classifyMessage(body: any): 'verification' | 'delivery' | 'unknown';
115
+ handleChallenge(body: any): { challenge: string };
116
+ handleDelivery(headers: any, rawBody: string | Uint8Array | ArrayBuffer): Promise<WebhookAck>;
117
+ handleRequest(headers: any, rawBody: string | Uint8Array | ArrayBuffer): Promise<{ status: number; body: any }>;
118
+ register(projectTableName: string, callbackUrl: string, targets: string[]): Promise<any>;
119
+ confirm(subscriptionId: string): Promise<any>;
120
+ rotateSecret(subscriptionId: string): Promise<string>;
121
+ updateCallback(subscriptionId: string, callbackUrl: string): Promise<any>;
122
+ disable(subscriptionId: string): Promise<any>;
123
+ listSubscriptions(): Promise<any>;
124
+ pullEvents(subscriptionId: string, options?: { since?: string; limit?: number }): Promise<any>;
125
+ reconcile(subscriptionId: string, projectTableName: string): Promise<{ pulled: number; dispatched: number }>;
126
+ fetchResult(event: WebhookDeliveryEvent): Promise<{ photo: any; selected: Record<string, any> | null }>;
127
+ saveSecret(subscriptionId: string, secret: string): void;
128
+ }
129
+
130
+ export function signWebhookPayload(secret: string, timestamp: string, rawBody: string | Uint8Array | ArrayBuffer): Promise<string>;
131
+ export function verifyWebhookSignature(secretCurrent: string | null, secretPrevious: string | null, headers: any, rawBody: string | Uint8Array | ArrayBuffer, options?: { skewToleranceS?: number; now?: number }): Promise<boolean>;
132
+ export function selectResultColumns(photo: any, target: string): Record<string, any> | null;
133
+ export const WEBHOOK_MODEL_COLUMNS: Record<string, string[]>;
134
+ export const WEBHOOK_OUTCOME_MODELS: Record<string, string[]>;
135
+
63
136
  // ── response schemas ──
64
137
  export interface AlbumDisplay {
65
138
  name?: string;
@@ -255,7 +328,7 @@ export interface PhotoPersonWithS3Link {
255
328
  export interface ProjectRunDisplay {
256
329
  name: string;
257
330
  private?: boolean;
258
- type?: number;
331
+ type: number;
259
332
  description?: string;
260
333
  directory?: string;
261
334
  photo_upload_vector?: number;
@@ -290,6 +363,7 @@ export interface ProjectRunDisplay {
290
363
  evidence_model?: boolean;
291
364
  insights_model?: boolean;
292
365
  normalize?: boolean;
366
+ is_continuous_upload?: boolean;
293
367
  }
294
368
 
295
369
  export interface PullEvent {
@@ -430,7 +504,7 @@ export class OauthAuthorization {
430
504
  export class OauthToken {
431
505
  token(body: { grantType: string, code: string, redirectUri: string, clientId: string, codeVerifier: string, refreshToken: string, clientSecret: string }): Promise<Record<string, any>>;
432
506
  adminRevokeUserTokens(userId: number): Promise<Record<string, any>>;
433
- revoke(body: { token: string, tokenTypeHint: string, clientId: string }): Promise<any>;
507
+ revoke(body: { token: string, tokenTypeHint: string, clientId: string }): Promise<Record<string, any>>;
434
508
  }
435
509
 
436
510
  export class OauthLogin {
@@ -447,6 +521,7 @@ export class Person {
447
521
  getAllPersonNamesFromProject(projectTableName: string): Promise<any>;
448
522
  getPhotoIdsByPersons(projectTableName: string, options?: { personId?: string[] | string[][] }): Promise<any>;
449
523
  getAllPersonsFromPhoto(projectTableName: string, photoId: number): Promise<PhotoPersonLight[]>;
524
+ removePerson(projectTableName: string, personId: string): Promise<Record<string, any>>;
450
525
  }
451
526
 
452
527
  export class Photoupload {
@@ -466,7 +541,7 @@ export class Photos {
466
541
  }
467
542
 
468
543
  export class Projects {
469
- createProjectAndRun(name: string, private_?: boolean, type?: number, description?: string, directory?: string, photoUploadVector?: number, thumbnail?: string, runName?: string, options?: { outcomes?: any, models?: any }): Promise<ProjectRunDisplay>;
544
+ createProjectAndRun(name: string, type: number, private_?: boolean, description?: string, directory?: string, photoUploadVector?: number, thumbnail?: string, runName?: string, options?: { outcomes?: any, models?: any }): Promise<ProjectRunDisplay>;
470
545
  markProjectUploadComplete(projectTableName: string, options?: { skippedFileCount?: number | number[] }): Promise<any>;
471
546
  checkProjectStatus(projectTableName: string): Promise<any>;
472
547
  getProjectPrelimModelRequestTemplate(projectTableName: string): Promise<Record<string, any>>;
@@ -499,8 +574,8 @@ export class Search {
499
574
  }
500
575
 
501
576
  export class Subscription {
502
- createSubscription(projectTableName: string, callbackUrl: string, targets: string[]): Promise<any>;
503
- verifySubscription(subscriptionId: string): Promise<any>;
577
+ createSubscription(projectTableName: string, callbackUrl: string, targets: string[]): Promise<SubscriptionActivated>;
578
+ verifySubscription(subscriptionId: string): Promise<SubscriptionActivated>;
504
579
  listSubscriptions(): Promise<SubscriptionDisplay[]>;
505
580
  listSubscriptionEvents(subscriptionId: string, options?: { since?: string | string[], limit?: number | number[] }): Promise<EventPage>;
506
581
  updateSubscription(subscriptionId: string, body?: { callbackUrl?: string, targets?: string[] }): Promise<SubscriptionDisplay>;
@@ -545,5 +620,6 @@ export class MediaViz {
545
620
  readonly search: Search;
546
621
  readonly subscription: Subscription;
547
622
  readonly users: Users;
623
+ readonly webhooks: WebhookConsumer;
548
624
  }
549
625