@mediaviz/sdk 1.8.0-dev.92 → 1.8.0-dev.96
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 +457 -7
- package/dist/sdk.d.cts +80 -5
- package/dist/sdk.d.ts +80 -5
- package/dist/sdk.esm.d.ts +80 -5
- package/dist/sdk.esm.js +451 -8
- package/dist/sdk.umd.js +457 -7
- package/package.json +1 -1
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 {
|
|
472
|
-
const {
|
|
473
|
-
const {
|
|
913
|
+
const { WebhookConsumer, selectResultColumns, WEBHOOK_MODEL_COLUMNS, WEBHOOK_OUTCOME_MODELS } = requireConsumer();
|
|
914
|
+
const { InMemoryWebhookStore } = requireStore();
|
|
915
|
+
const { signWebhookPayload, verifyWebhookSignature } = requireSigning();
|
|
474
916
|
|
|
475
|
-
src = {
|
|
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
|
|
922
|
+
var _webhooks = /*@__PURE__*/getDefaultExportFromCjs(srcExports);
|
|
481
923
|
|
|
482
924
|
// Auto-generated — do not edit
|
|
483
|
-
const {
|
|
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; }
|
|
@@ -1210,7 +1652,7 @@ function stripUndef$3(o) { const r = {}; for (const k in o) if (o[k] !== undefin
|
|
|
1210
1652
|
class Projects {
|
|
1211
1653
|
constructor(ctx) { this._ctx = ctx; }
|
|
1212
1654
|
|
|
1213
|
-
async createProjectAndRun(name,
|
|
1655
|
+
async createProjectAndRun(name, type, private_ = undefined, description = undefined, directory = undefined, photoUploadVector = undefined, thumbnail = undefined, runName = undefined, { outcomes, models } = {}) {
|
|
1214
1656
|
this._ctx.requireTokens();
|
|
1215
1657
|
let path = `/api/v1/project_outcome/`;
|
|
1216
1658
|
const query = new URLSearchParams();
|
|
@@ -1903,6 +2345,7 @@ class MediaViz {
|
|
|
1903
2345
|
this.search = new Search(_ctx);
|
|
1904
2346
|
this.subscription = new Subscription(_ctx);
|
|
1905
2347
|
this.users = new Users(_ctx);
|
|
2348
|
+
this.webhooks = new WebhookConsumer(_ctx, this.subscription);
|
|
1906
2349
|
}
|
|
1907
2350
|
|
|
1908
2351
|
async authenticate() {
|
|
@@ -1940,6 +2383,7 @@ exports.CuratedAlbums = CuratedAlbums;
|
|
|
1940
2383
|
exports.CustomAlbums = CustomAlbums;
|
|
1941
2384
|
exports.EmailTokens = EmailTokens;
|
|
1942
2385
|
exports.Health = Health;
|
|
2386
|
+
exports.InMemoryWebhookStore = InMemoryWebhookStore;
|
|
1943
2387
|
exports.Keywords = Keywords;
|
|
1944
2388
|
exports.MediaViz = MediaViz;
|
|
1945
2389
|
exports.NotFoundError = NotFoundError;
|
|
@@ -1959,4 +2403,10 @@ exports.ServerError = ServerError;
|
|
|
1959
2403
|
exports.Subscription = Subscription;
|
|
1960
2404
|
exports.Users = Users;
|
|
1961
2405
|
exports.ValidationError = ValidationError;
|
|
2406
|
+
exports.WEBHOOK_MODEL_COLUMNS = WEBHOOK_MODEL_COLUMNS;
|
|
2407
|
+
exports.WEBHOOK_OUTCOME_MODELS = WEBHOOK_OUTCOME_MODELS;
|
|
2408
|
+
exports.WebhookConsumer = WebhookConsumer;
|
|
1962
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
|
|
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 {
|
|
@@ -467,7 +541,7 @@ export class Photos {
|
|
|
467
541
|
}
|
|
468
542
|
|
|
469
543
|
export class Projects {
|
|
470
|
-
createProjectAndRun(name: string,
|
|
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>;
|
|
471
545
|
markProjectUploadComplete(projectTableName: string, options?: { skippedFileCount?: number | number[] }): Promise<any>;
|
|
472
546
|
checkProjectStatus(projectTableName: string): Promise<any>;
|
|
473
547
|
getProjectPrelimModelRequestTemplate(projectTableName: string): Promise<Record<string, any>>;
|
|
@@ -500,8 +574,8 @@ export class Search {
|
|
|
500
574
|
}
|
|
501
575
|
|
|
502
576
|
export class Subscription {
|
|
503
|
-
createSubscription(projectTableName: string, callbackUrl: string, targets: string[]): Promise<
|
|
504
|
-
verifySubscription(subscriptionId: string): Promise<
|
|
577
|
+
createSubscription(projectTableName: string, callbackUrl: string, targets: string[]): Promise<SubscriptionActivated>;
|
|
578
|
+
verifySubscription(subscriptionId: string): Promise<SubscriptionActivated>;
|
|
505
579
|
listSubscriptions(): Promise<SubscriptionDisplay[]>;
|
|
506
580
|
listSubscriptionEvents(subscriptionId: string, options?: { since?: string | string[], limit?: number | number[] }): Promise<EventPage>;
|
|
507
581
|
updateSubscription(subscriptionId: string, body?: { callbackUrl?: string, targets?: string[] }): Promise<SubscriptionDisplay>;
|
|
@@ -546,5 +620,6 @@ export class MediaViz {
|
|
|
546
620
|
readonly search: Search;
|
|
547
621
|
readonly subscription: Subscription;
|
|
548
622
|
readonly users: Users;
|
|
623
|
+
readonly webhooks: WebhookConsumer;
|
|
549
624
|
}
|
|
550
625
|
|