@domino-sdk/relay 0.13.0 → 0.14.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.
@@ -15,12 +15,12 @@ import {
15
15
  referralSummarySchema,
16
16
  trackerArtifactSchema,
17
17
  validateAuthoringIdentity
18
- } from "./chunk-XOEREIXI.js";
18
+ } from "./chunk-CJARNIVP.js";
19
19
  import {
20
20
  authScopeSchema,
21
21
  exchangeResultSchema,
22
22
  sessionSchema
23
- } from "./chunk-VDWQL6HV.js";
23
+ } from "./chunk-I2CCMGN6.js";
24
24
  import {
25
25
  accountConnectionSchema,
26
26
  attemptSchema,
@@ -45,7 +45,7 @@ import {
45
45
  snapshotSchema,
46
46
  storedReviewModeSchema,
47
47
  templateSchema
48
- } from "./chunk-3UOTI4IU.js";
48
+ } from "./chunk-FUQYKTOJ.js";
49
49
 
50
50
  // src/point-ledgers.ts
51
51
  import { z } from "zod";
@@ -267,15 +267,17 @@ function submissions(transport, configuredStore, namespace) {
267
267
  async function open(quest, options = {
268
268
  input: "photo"
269
269
  }) {
270
- if (!configuredStore)
270
+ if (!configuredStore) {
271
271
  throw new Error(
272
272
  "Submissions require durable storage. Use createBrowserRelayClient or provide a SubmissionStore."
273
273
  );
274
+ }
274
275
  const store = configuredStore;
275
276
  identifier.parse(quest);
276
277
  const session = await transport.session();
277
- if (session?.principal.kind !== "member")
278
+ if (session?.principal.kind !== "member") {
278
279
  throw new Error("Sign in to submit an activity");
280
+ }
279
281
  const sessionId = session.id;
280
282
  const key = JSON.stringify([
281
283
  "relay-submission-v1",
@@ -288,25 +290,30 @@ function submissions(transport, configuredStore, namespace) {
288
290
  ]);
289
291
  async function currentSession() {
290
292
  const current = await transport.session();
291
- if (current?.id !== sessionId)
293
+ if (current?.id !== sessionId) {
292
294
  throw new Error(
293
295
  "Your session changed. Reopen the submission before continuing."
294
296
  );
297
+ }
295
298
  }
296
299
  async function read() {
297
300
  const saved = await store.read(key);
298
- if (saved === void 0 || saved === null) return null;
301
+ if (saved === void 0 || saved === null) {
302
+ return null;
303
+ }
299
304
  const parsed = recordSchema.safeParse(saved);
300
- if (!parsed.success || parsed.data.quest !== quest)
305
+ if (!parsed.success || parsed.data.quest !== quest) {
301
306
  throw new Error(
302
307
  "Saved submission could not be read. Do not create another claim until it is recovered."
303
308
  );
309
+ }
304
310
  const record = parsed.data;
305
311
  const input = record.stage === "review" ? record.draft?.kind : "input" in record ? record.input.kind : "photo";
306
- if (!options.recoverPrevious && input !== options.input)
312
+ if (!options.recoverPrevious && input !== options.input) {
307
313
  throw new Error(
308
314
  "Saved submission uses a different input type. Reopen it with its original type to recover it."
309
315
  );
316
+ }
310
317
  return record;
311
318
  }
312
319
  function claim(record) {
@@ -321,12 +328,13 @@ function submissions(transport, configuredStore, namespace) {
321
328
  );
322
329
  }
323
330
  async function resumeRecord(saved) {
324
- if (saved.stage === "review")
331
+ if (saved.stage === "review") {
325
332
  throw new QuestUpdatedError(
326
333
  "This quest changed. Review it before submitting again.",
327
334
  "quest-updated",
328
335
  saved.draft
329
336
  );
337
+ }
330
338
  let record = saved;
331
339
  if (record.stage === "upload") {
332
340
  const evidence = await transport.upload(
@@ -369,30 +377,35 @@ function submissions(transport, configuredStore, namespace) {
369
377
  stage: "submitted",
370
378
  attempt: attempt.id
371
379
  });
372
- if (attempt.status === "failed")
380
+ if (attempt.status === "failed") {
373
381
  attempt = await transport.retry(attempt.id, sessionId);
382
+ }
374
383
  return attempt;
375
384
  }
376
385
  async function submit(record) {
377
386
  return store.exclusive(key, async () => {
378
387
  await currentSession();
379
388
  const existing = await read();
380
- if (existing?.stage === "review" && existing.release === record.release)
389
+ if (existing?.stage === "review" && existing.release === record.release) {
381
390
  throw new QuestUpdatedError(
382
391
  "Review the latest quest before submitting again.",
383
392
  "quest-updated",
384
393
  existing.draft
385
394
  );
386
- if (existing && existing.stage !== "submitted" && existing.stage !== "review")
395
+ }
396
+ if (existing && existing.stage !== "submitted" && existing.stage !== "review") {
387
397
  throw new Error(
388
398
  "A submission is unfinished. Resume it before choosing new input."
389
399
  );
400
+ }
390
401
  if (existing?.stage === "submitted") {
391
402
  const previous = await claim(existing);
392
- if (previous.status === "failed")
403
+ if (previous.status === "failed") {
393
404
  return transport.retry(previous.id, sessionId);
394
- if (previous.status !== "rejected" && !(options.repeatable && previous.status === "accepted"))
405
+ }
406
+ if (previous.status !== "rejected" && !(options.repeatable && previous.status === "accepted")) {
395
407
  return previous;
408
+ }
396
409
  }
397
410
  await store.write(key, record);
398
411
  return resumeRecord(record);
@@ -403,37 +416,46 @@ function submissions(transport, configuredStore, namespace) {
403
416
  return store.exclusive(key, async () => {
404
417
  await currentSession();
405
418
  const record = await read();
406
- if (!record) return;
407
- if (record.stage !== "submitted")
419
+ if (!record) {
420
+ return;
421
+ }
422
+ if (record.stage !== "submitted") {
408
423
  throw new Error(
409
424
  "Resume the unfinished submission before starting over."
410
425
  );
426
+ }
411
427
  const attempt = await claim(record);
412
- if (attempt.status !== "failed" && attempt.status !== "rejected" && !(options.repeatable && attempt.status === "accepted"))
428
+ if (attempt.status !== "failed" && attempt.status !== "rejected" && !(options.repeatable && attempt.status === "accepted")) {
413
429
  throw new Error(
414
430
  "Only a failed or rejected submission can be discarded."
415
431
  );
432
+ }
416
433
  await store.write(key, null);
417
434
  });
418
435
  },
419
436
  async status() {
420
437
  await currentSession();
421
438
  const record = await read();
422
- if (!record) return { kind: "empty" };
423
- if (record.stage === "review") return { kind: "needs-review" };
439
+ if (!record) {
440
+ return { kind: "empty" };
441
+ }
442
+ if (record.stage === "review") {
443
+ return { kind: "needs-review" };
444
+ }
424
445
  return record.stage === "submitted" ? { kind: "submitted", attempt: record.attempt } : { kind: "pending", stage: record.stage };
425
446
  },
426
447
  async resume() {
427
448
  return store.exclusive(key, async () => {
428
449
  await currentSession();
429
450
  const record = await read();
430
- if (!record)
451
+ if (!record) {
431
452
  throw new Error("There is no saved submission to resume");
453
+ }
432
454
  return resumeRecord(record);
433
455
  });
434
456
  }
435
457
  };
436
- if (options.input === "quiz")
458
+ if (options.input === "quiz") {
437
459
  return {
438
460
  ...handle,
439
461
  submit(answers, version) {
@@ -451,7 +473,8 @@ function submissions(transport, configuredStore, namespace) {
451
473
  });
452
474
  }
453
475
  };
454
- if (options.input === "none")
476
+ }
477
+ if (options.input === "none") {
455
478
  return {
456
479
  ...handle,
457
480
  submit() {
@@ -464,15 +487,18 @@ function submissions(transport, configuredStore, namespace) {
464
487
  });
465
488
  }
466
489
  };
490
+ }
467
491
  return {
468
492
  ...handle,
469
493
  async submit(file) {
470
- if (!["image/jpeg", "image/png"].includes(file.type) || file.size === 0 || file.size > 5 * 1024 * 1024)
494
+ if (!["image/jpeg", "image/png"].includes(file.type) || file.size === 0 || file.size > 5 * 1024 * 1024) {
471
495
  throw new Error("Choose a JPG or PNG photo under 5 MB");
496
+ }
472
497
  const bytes = new Uint8Array(await file.slice(0, 4).arrayBuffer());
473
498
  const signature = file.type === "image/png" ? [137, 80, 78, 71] : [255, 216, 255];
474
- if (!signature.every((byte, index) => bytes[index] === byte))
499
+ if (!signature.every((byte, index) => bytes[index] === byte)) {
475
500
  throw new Error("Image content does not match its file type");
501
+ }
476
502
  return submit({
477
503
  id: crypto.randomUUID(),
478
504
  ...options.release ? { release: options.release } : {},
@@ -495,11 +521,18 @@ var QuestUpdatedError = class extends RelayError {
495
521
 
496
522
  // src/quest-experience.ts
497
523
  function reconcileQuestDraft(view, draft) {
498
- if (!draft) return null;
499
- if (draft.kind === "photo")
524
+ if (!draft) {
525
+ return null;
526
+ }
527
+ if (draft.kind === "photo") {
500
528
  return view.interaction === "photo" ? draft : null;
501
- if (draft.kind === "none") return view.interaction === "claim" ? draft : null;
502
- if (view.interaction !== "quiz") return null;
529
+ }
530
+ if (draft.kind === "none") {
531
+ return view.interaction === "claim" ? draft : null;
532
+ }
533
+ if (view.interaction !== "quiz") {
534
+ return null;
535
+ }
503
536
  const answers = Object.fromEntries(
504
537
  (view.presentation.quiz ?? []).flatMap((q) => {
505
538
  const answer = draft.answers[q.id];
@@ -514,7 +547,9 @@ function questActions(submission, latest) {
514
547
  try {
515
548
  return { kind: "submitted", attempt: await task() };
516
549
  } catch (error) {
517
- if (!(error instanceof QuestUpdatedError)) throw error;
550
+ if (!(error instanceof QuestUpdatedError)) {
551
+ throw error;
552
+ }
518
553
  const current = await latest(view.quest).catch(() => {
519
554
  throw error;
520
555
  });
@@ -599,10 +634,11 @@ function observeProgress(transport, listener, options = {}, onDispose = () => {
599
634
  }) {
600
635
  const interval = options.intervalMs ?? 1500;
601
636
  const maximum = options.maxBackoffMs ?? 3e4;
602
- if (!Number.isFinite(interval) || interval <= 0 || !Number.isFinite(maximum) || maximum < interval)
637
+ if (!Number.isFinite(interval) || interval <= 0 || !Number.isFinite(maximum) || maximum < interval) {
603
638
  throw new Error(
604
639
  "Progress polling requires a positive interval and a maximum at least as large"
605
640
  );
641
+ }
606
642
  let disposed = false;
607
643
  let controller;
608
644
  let timer;
@@ -610,29 +646,43 @@ function observeProgress(transport, listener, options = {}, onDispose = () => {
610
646
  let failures = 0;
611
647
  function clear() {
612
648
  controller?.abort();
613
- if (timer !== void 0) clearTimeout(timer);
649
+ if (timer !== void 0) {
650
+ clearTimeout(timer);
651
+ }
614
652
  sessionId = void 0;
615
- if (!disposed) listener({ kind: "loading" });
653
+ if (!disposed) {
654
+ listener({ kind: "loading" });
655
+ }
616
656
  }
617
657
  async function refresh() {
618
- if (disposed) return;
658
+ if (disposed) {
659
+ return;
660
+ }
619
661
  controller?.abort();
620
- if (timer !== void 0) clearTimeout(timer);
662
+ if (timer !== void 0) {
663
+ clearTimeout(timer);
664
+ }
621
665
  const request = new AbortController();
622
666
  controller = request;
623
667
  const current = () => !disposed && !request.signal.aborted;
624
668
  try {
625
669
  const session = await transport.session(request.signal);
626
- if (!current()) return;
670
+ if (!current()) {
671
+ return;
672
+ }
627
673
  if (sessionId !== (session?.id ?? null)) {
628
674
  sessionId = session?.id ?? null;
629
675
  listener({ kind: "session", session });
630
676
  }
631
677
  if (session?.principal.kind === "member") {
632
678
  const progress = await transport.progress(session.id, request.signal);
633
- if (!current()) return;
679
+ if (!current()) {
680
+ return;
681
+ }
634
682
  const confirmed = await transport.session(request.signal);
635
- if (!current()) return;
683
+ if (!current()) {
684
+ return;
685
+ }
636
686
  if (confirmed?.id !== session.id || progress.member !== session.principal.member) {
637
687
  sessionId = void 0;
638
688
  listener({ kind: "loading" });
@@ -642,7 +692,9 @@ function observeProgress(transport, listener, options = {}, onDispose = () => {
642
692
  }
643
693
  failures = 0;
644
694
  } catch (error) {
645
- if (!current()) return;
695
+ if (!current()) {
696
+ return;
697
+ }
646
698
  failures = Math.min(failures + 1, 20);
647
699
  sessionId = void 0;
648
700
  listener({
@@ -650,21 +702,26 @@ function observeProgress(transport, listener, options = {}, onDispose = () => {
650
702
  error: error instanceof Error ? error : new Error("Could not refresh progress")
651
703
  });
652
704
  } finally {
653
- if (current())
705
+ if (current()) {
654
706
  timer = setTimeout(
655
707
  () => void refresh(),
656
708
  Math.min(maximum, interval * 2 ** failures)
657
709
  );
710
+ }
658
711
  }
659
712
  }
660
713
  const observer = {
661
714
  refresh,
662
715
  clear,
663
716
  dispose() {
664
- if (disposed) return;
717
+ if (disposed) {
718
+ return;
719
+ }
665
720
  disposed = true;
666
721
  controller?.abort();
667
- if (timer !== void 0) clearTimeout(timer);
722
+ if (timer !== void 0) {
723
+ clearTimeout(timer);
724
+ }
668
725
  onDispose();
669
726
  }
670
727
  };
@@ -796,13 +853,21 @@ var participantExchangeSchema = z8.object({
796
853
  function createRelayClient(options) {
797
854
  async function send(path, init = {}) {
798
855
  const headers = new Headers(init.headers);
799
- if (options.token) headers.set("Authorization", `Bearer ${options.token}`);
800
- if (options.organization)
856
+ if (options.token) {
857
+ headers.set("Authorization", `Bearer ${options.token}`);
858
+ }
859
+ if (options.organization) {
801
860
  headers.set("X-Relay-Organization", options.organization);
802
- if (options.project) headers.set("X-Relay-Project", options.project);
803
- if (options.environment)
861
+ }
862
+ if (options.project) {
863
+ headers.set("X-Relay-Project", options.project);
864
+ }
865
+ if (options.environment) {
804
866
  headers.set("X-Relay-Environment", options.environment);
805
- if (init.method && init.method !== "GET") headers.set("X-Relay-CSRF", "1");
867
+ }
868
+ if (init.method && init.method !== "GET") {
869
+ headers.set("X-Relay-CSRF", "1");
870
+ }
806
871
  const response = await fetch(options.baseUrl + path, {
807
872
  ...init,
808
873
  signal: init.signal ? AbortSignal.any([init.signal, AbortSignal.timeout(15e3)]) : AbortSignal.timeout(15e3),
@@ -832,7 +897,9 @@ function createRelayClient(options) {
832
897
  response = await send(path, init);
833
898
  } catch (error) {
834
899
  const transient = error instanceof RelayError && [500, 502, 503, 504].includes(error.status) || error instanceof TypeError || error instanceof DOMException && error.name === "TimeoutError";
835
- if (!transient || attempt === 2) throw error;
900
+ if (!transient || attempt === 2) {
901
+ throw error;
902
+ }
836
903
  await new Promise(
837
904
  (resolve) => setTimeout(resolve, attempt === 0 ? 250 : 750)
838
905
  );
@@ -844,8 +911,11 @@ function createRelayClient(options) {
844
911
  }
845
912
  function query(path, values) {
846
913
  const params = new URLSearchParams();
847
- for (const [key, value] of Object.entries(values))
848
- if (value !== void 0) params.set(key, String(value));
914
+ for (const [key, value] of Object.entries(values)) {
915
+ if (value !== void 0) {
916
+ params.set(key, String(value));
917
+ }
918
+ }
849
919
  return path + "?" + params;
850
920
  }
851
921
  function json(body) {
@@ -887,11 +957,15 @@ function createRelayClient(options) {
887
957
  );
888
958
  const observers = /* @__PURE__ */ new Set();
889
959
  async function changeSession(task) {
890
- for (const observer of observers) observer.clear();
960
+ for (const observer of observers) {
961
+ observer.clear();
962
+ }
891
963
  try {
892
964
  return await task();
893
965
  } finally {
894
- for (const observer of observers) void observer.refresh();
966
+ for (const observer of observers) {
967
+ void observer.refresh();
968
+ }
895
969
  }
896
970
  }
897
971
  const experience = () => request("/v1/me/experience", questExperienceSchema);
@@ -899,7 +973,9 @@ function createRelayClient(options) {
899
973
  const quest = (await experience()).quests.find(
900
974
  (q) => q.quest === id || q.key === id
901
975
  );
902
- if (!quest) throw new RelayError("Quest not found", 404);
976
+ if (!quest) {
977
+ throw new RelayError("Quest not found", 404);
978
+ }
903
979
  return quest;
904
980
  }
905
981
  return {
@@ -928,18 +1004,21 @@ function createRelayClient(options) {
928
1004
  quests: {
929
1005
  get: getQuest,
930
1006
  retry: async (view) => {
931
- if (view.trigger.kind === "automatic")
1007
+ if (view.trigger.kind === "automatic") {
932
1008
  throw new Error(
933
1009
  "Automatic quests recover without participant retries."
934
1010
  );
935
- if (!view.attempt || view.attempt.status !== "failed")
1011
+ }
1012
+ if (!view.attempt || view.attempt.status !== "failed") {
936
1013
  throw new Error("There is no failed attempt to retry.");
1014
+ }
937
1015
  const session = await request(
938
1016
  "/v1/auth/session",
939
1017
  sessionSchema.nullable()
940
1018
  );
941
- if (session?.principal.kind !== "member")
1019
+ if (session?.principal.kind !== "member") {
942
1020
  throw new Error("Sign in to retry verification.");
1021
+ }
943
1022
  return request(`/v1/attempts/${view.attempt.id}/retry`, attemptSchema, {
944
1023
  ...json({}),
945
1024
  headers: {
@@ -953,7 +1032,9 @@ function createRelayClient(options) {
953
1032
  const collection = (await experience()).collections.find(
954
1033
  (c) => c.id === id || c.key === id
955
1034
  );
956
- if (!collection) throw new RelayError("Collection not found", 404);
1035
+ if (!collection) {
1036
+ throw new RelayError("Collection not found", 404);
1037
+ }
957
1038
  return collection;
958
1039
  },
959
1040
  open: questActions(submission, getQuest),
@@ -1114,8 +1195,9 @@ function createRelayClient(options) {
1114
1195
  requestOptions
1115
1196
  );
1116
1197
  const type = response.headers.get("Content-Type");
1117
- if (type !== "image/jpeg" && type !== "image/png")
1198
+ if (type !== "image/jpeg" && type !== "image/png") {
1118
1199
  throw new RelayError("Unexpected evidence content type", 502);
1200
+ }
1119
1201
  return response.blob();
1120
1202
  }
1121
1203
  },