@domino-sdk/relay 0.1.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.
@@ -0,0 +1,1056 @@
1
+ import {
2
+ attemptSchema,
3
+ entitlementSchema,
4
+ errorSchema,
5
+ evidenceSchema,
6
+ identifier,
7
+ memberListSchema,
8
+ memberPageSchema,
9
+ memberProgressSchema,
10
+ pickupAvailabilitySchema,
11
+ pickupSelectionSchema,
12
+ publishedReleaseSchema,
13
+ questDescriptionSchema,
14
+ quizEvidenceSchema,
15
+ releaseSchema,
16
+ reviewListSchema,
17
+ reviewModeSchema,
18
+ reviewPageSchema,
19
+ settingsValuesSchema,
20
+ snapshotSchema,
21
+ templateSchema
22
+ } from "./chunk-C2KOMNLG.js";
23
+
24
+ // src/catalog.ts
25
+ import { z } from "zod";
26
+ var interactionSchema = z.enum([
27
+ "photo",
28
+ "quiz",
29
+ "claim",
30
+ "staff",
31
+ "automatic"
32
+ ]);
33
+ function questInteraction(trigger) {
34
+ return trigger.kind === "automatic" ? "automatic" : trigger.actor === "staff" ? "staff" : trigger.input === "none" ? "claim" : trigger.input;
35
+ }
36
+ var questTypeVersionSchema = z.object({
37
+ id: identifier,
38
+ version: identifier,
39
+ code: z.string(),
40
+ definition: questDescriptionSchema,
41
+ provider: reviewModeSchema,
42
+ createdAt: z.number()
43
+ });
44
+ var collectionSlotSchema = z.object({ id: identifier, title: z.string().trim().min(1).max(120) }).strict();
45
+ var questCollectionSchema = collectionSlotSchema.extend({
46
+ quests: z.array(identifier).max(100).refine(
47
+ (v) => new Set(v).size === v.length,
48
+ "A collection can contain a quest only once"
49
+ )
50
+ });
51
+ var questInstanceSchema = z.object({
52
+ id: identifier,
53
+ title: z.string().trim().min(1).max(120),
54
+ typeVersion: identifier,
55
+ settings: settingsValuesSchema,
56
+ requires: z.array(identifier).max(30),
57
+ status: z.enum(["active", "paused", "archived"])
58
+ }).strict();
59
+ var questDraftChangesSchema = z.object({
60
+ quests: z.array(
61
+ questInstanceSchema.extend({ expectedRelease: identifier.nullable() })
62
+ ).max(50),
63
+ collections: z.array(questCollectionSchema).max(50)
64
+ }).strict();
65
+ var draftBase = z.object({
66
+ id: identifier,
67
+ revision: z.number().int().positive(),
68
+ baseRevision: z.number().int().nonnegative(),
69
+ changes: questDraftChangesSchema
70
+ });
71
+ var questDraftSchema = z.discriminatedUnion("status", [
72
+ draftBase.extend({ status: z.literal("draft") }),
73
+ draftBase.extend({
74
+ status: z.literal("published"),
75
+ releaseIds: z.array(identifier)
76
+ })
77
+ ]);
78
+ var questCatalogSchema = z.object({
79
+ revision: z.number().int().nonnegative(),
80
+ types: z.array(questTypeVersionSchema),
81
+ collections: z.array(questCollectionSchema),
82
+ quests: z.array(questInstanceSchema),
83
+ drafts: z.array(questDraftSchema),
84
+ releases: z.array(publishedReleaseSchema).default([]),
85
+ supportedInteractions: z.array(interactionSchema)
86
+ });
87
+ var catalogDeploymentSchema = z.object({
88
+ expectedRevision: z.number().int().nonnegative(),
89
+ types: z.array(
90
+ z.object({
91
+ code: z.string().min(1).max(1e6),
92
+ provider: reviewModeSchema
93
+ })
94
+ ).max(50).default([]),
95
+ collections: z.array(collectionSlotSchema).max(50).default([]),
96
+ supportedInteractions: z.array(interactionSchema).min(1).optional()
97
+ }).strict();
98
+ var catalogDeploymentPreviewSchema = z.object({
99
+ revision: z.number().int().nonnegative(),
100
+ types: z.array(questTypeVersionSchema),
101
+ collections: z.array(collectionSlotSchema),
102
+ supportedInteractions: z.array(interactionSchema)
103
+ });
104
+ var saveQuestDraftSchema = z.object({
105
+ id: identifier,
106
+ expectedRevision: z.number().int().nonnegative(),
107
+ baseRevision: z.number().int().nonnegative(),
108
+ changes: questDraftChangesSchema
109
+ }).strict();
110
+ var publishQuestDraftSchema = z.object({ id: identifier, expectedRevision: z.number().int().positive() }).strict();
111
+ var questViewSchema = memberProgressSchema.shape.quests.element.extend(
112
+ {
113
+ release: identifier,
114
+ interaction: interactionSchema,
115
+ lifecycle: z.enum(["active", "paused", "archived"]),
116
+ rewards: z.array(z.object({ label: z.string() }))
117
+ }
118
+ );
119
+ var collectionViewSchema = z.object({
120
+ id: identifier,
121
+ title: z.string(),
122
+ quests: z.array(questViewSchema)
123
+ });
124
+ var questExperienceSchema = z.object({
125
+ member: identifier,
126
+ quests: z.array(questViewSchema),
127
+ collections: z.array(collectionViewSchema)
128
+ });
129
+ var questPublicationPreviewSchema = z.object({
130
+ draft: questDraftSchema,
131
+ releases: z.array(publishedReleaseSchema),
132
+ affectedCollections: z.array(questCollectionSchema)
133
+ });
134
+
135
+ // src/errors.ts
136
+ var RelayError = class extends Error {
137
+ constructor(message, status, missing = [], code) {
138
+ super(message);
139
+ this.status = status;
140
+ this.missing = missing;
141
+ this.code = code;
142
+ }
143
+ status;
144
+ missing;
145
+ code;
146
+ };
147
+
148
+ // src/submissions.ts
149
+ import { z as z2 } from "zod";
150
+ var common = {
151
+ id: identifier,
152
+ quest: identifier,
153
+ release: identifier.optional()
154
+ };
155
+ var claimInputSchema = z2.union([
156
+ quizEvidenceSchema,
157
+ z2.object({ kind: z2.literal("none") }).strict()
158
+ ]);
159
+ var draftSchema = z2.union([
160
+ claimInputSchema,
161
+ z2.object({ kind: z2.literal("photo"), file: z2.instanceof(Blob) })
162
+ ]);
163
+ var recordSchema = z2.union([
164
+ z2.object({
165
+ ...common,
166
+ stage: z2.literal("review"),
167
+ draft: draftSchema.nullable()
168
+ }),
169
+ z2.object({ ...common, stage: z2.literal("upload"), file: z2.instanceof(Blob) }),
170
+ z2.object({
171
+ ...common,
172
+ stage: z2.literal("claim"),
173
+ evidence: identifier,
174
+ file: z2.instanceof(Blob).optional()
175
+ }),
176
+ z2.object({
177
+ ...common,
178
+ stage: z2.literal("submitted"),
179
+ evidence: identifier,
180
+ attempt: identifier
181
+ }),
182
+ z2.object({ ...common, stage: z2.literal("claim"), input: claimInputSchema }),
183
+ z2.object({
184
+ ...common,
185
+ stage: z2.literal("submitted"),
186
+ input: claimInputSchema,
187
+ attempt: identifier
188
+ })
189
+ ]);
190
+ function submissions(transport, configuredStore, namespace) {
191
+ async function open(quest, options = {
192
+ input: "photo"
193
+ }) {
194
+ if (!configuredStore)
195
+ throw new Error(
196
+ "Submissions require durable storage. Use createBrowserRelayClient or provide a SubmissionStore."
197
+ );
198
+ const store = configuredStore;
199
+ identifier.parse(quest);
200
+ const session = await transport.session();
201
+ if (session?.principal.kind !== "member")
202
+ throw new Error("Sign in to submit an activity");
203
+ const sessionId = session.id;
204
+ const key = JSON.stringify([
205
+ "relay-submission-v1",
206
+ namespace,
207
+ session.organization,
208
+ session.environment,
209
+ session.project,
210
+ session.principal.member,
211
+ quest
212
+ ]);
213
+ async function currentSession() {
214
+ const current = await transport.session();
215
+ if (current?.id !== sessionId)
216
+ throw new Error(
217
+ "Your session changed. Reopen the submission before continuing."
218
+ );
219
+ }
220
+ async function read() {
221
+ const saved = await store.read(key);
222
+ if (saved === void 0 || saved === null) return null;
223
+ const parsed = recordSchema.safeParse(saved);
224
+ if (!parsed.success || parsed.data.quest !== quest)
225
+ throw new Error(
226
+ "Saved submission could not be read. Do not create another claim until it is recovered."
227
+ );
228
+ const record = parsed.data;
229
+ const input = record.stage === "review" ? record.draft?.kind : "input" in record ? record.input.kind : "photo";
230
+ if (!options.recoverPrevious && input !== options.input)
231
+ throw new Error(
232
+ "Saved submission uses a different input type. Reopen it with its original type to recover it."
233
+ );
234
+ return record;
235
+ }
236
+ function claim(record) {
237
+ return transport.claim(
238
+ {
239
+ quest,
240
+ actionId: record.id,
241
+ ...record.release ? { release: record.release } : {},
242
+ ..."evidence" in record ? { evidence: record.evidence } : record.input.kind === "quiz" ? { input: record.input } : {}
243
+ },
244
+ sessionId
245
+ );
246
+ }
247
+ async function resumeRecord(saved) {
248
+ if (saved.stage === "review")
249
+ throw new QuestUpdatedError(
250
+ "This quest changed. Review it before submitting again.",
251
+ "quest-updated",
252
+ saved.draft
253
+ );
254
+ let record = saved;
255
+ if (record.stage === "upload") {
256
+ const evidence = await transport.upload(
257
+ record.file,
258
+ record.id,
259
+ sessionId
260
+ );
261
+ record = {
262
+ id: record.id,
263
+ quest,
264
+ ...record.release ? { release: record.release } : {},
265
+ file: record.file,
266
+ stage: "claim",
267
+ evidence: evidence.id
268
+ };
269
+ await store.write(key, record);
270
+ }
271
+ let attempt;
272
+ try {
273
+ attempt = await claim(record);
274
+ } catch (error) {
275
+ if (error instanceof RelayError && error.status === 409 && (error.code === "quest-updated" || error.code === "stale-quiz") && record.stage === "claim") {
276
+ const draft = "input" in record ? record.input : "file" in record && record.file ? { kind: "photo", file: record.file } : null;
277
+ await store.write(
278
+ key,
279
+ error.code === "quest-updated" ? {
280
+ id: record.id,
281
+ quest,
282
+ release: record.release,
283
+ stage: "review",
284
+ draft
285
+ } : null
286
+ );
287
+ throw new QuestUpdatedError(error.message, error.code, draft);
288
+ }
289
+ throw error;
290
+ }
291
+ await store.write(key, {
292
+ ...record,
293
+ stage: "submitted",
294
+ attempt: attempt.id
295
+ });
296
+ if (attempt.status === "failed")
297
+ attempt = await transport.retry(attempt.id, sessionId);
298
+ return attempt;
299
+ }
300
+ async function submit(record) {
301
+ return store.exclusive(key, async () => {
302
+ await currentSession();
303
+ const existing = await read();
304
+ if (existing?.stage === "review" && existing.release === record.release)
305
+ throw new QuestUpdatedError(
306
+ "Review the latest quest before submitting again.",
307
+ "quest-updated",
308
+ existing.draft
309
+ );
310
+ if (existing && existing.stage !== "submitted" && existing.stage !== "review")
311
+ throw new Error(
312
+ "A submission is unfinished. Resume it before choosing new input."
313
+ );
314
+ if (existing?.stage === "submitted") {
315
+ const previous = await claim(existing);
316
+ if (previous.status === "failed")
317
+ return transport.retry(previous.id, sessionId);
318
+ if (previous.status !== "rejected") return previous;
319
+ }
320
+ await store.write(key, record);
321
+ return resumeRecord(record);
322
+ });
323
+ }
324
+ const handle = {
325
+ async status() {
326
+ await currentSession();
327
+ const record = await read();
328
+ if (!record) return { kind: "empty" };
329
+ if (record.stage === "review") return { kind: "needs-review" };
330
+ return record.stage === "submitted" ? { kind: "submitted", attempt: record.attempt } : { kind: "pending", stage: record.stage };
331
+ },
332
+ async resume() {
333
+ return store.exclusive(key, async () => {
334
+ await currentSession();
335
+ const record = await read();
336
+ if (!record)
337
+ throw new Error("There is no saved submission to resume");
338
+ return resumeRecord(record);
339
+ });
340
+ }
341
+ };
342
+ if (options.input === "quiz")
343
+ return {
344
+ ...handle,
345
+ submit(answers, version) {
346
+ const input = quizEvidenceSchema.parse({
347
+ kind: "quiz",
348
+ answers,
349
+ ...version || options.release ? { release: version?.release ?? options.release } : {}
350
+ });
351
+ return submit({
352
+ id: crypto.randomUUID(),
353
+ ...options.release ? { release: options.release } : {},
354
+ quest,
355
+ stage: "claim",
356
+ input
357
+ });
358
+ }
359
+ };
360
+ if (options.input === "none")
361
+ return {
362
+ ...handle,
363
+ submit() {
364
+ return submit({
365
+ id: crypto.randomUUID(),
366
+ ...options.release ? { release: options.release } : {},
367
+ quest,
368
+ stage: "claim",
369
+ input: { kind: "none" }
370
+ });
371
+ }
372
+ };
373
+ return {
374
+ ...handle,
375
+ async submit(file) {
376
+ if (!["image/jpeg", "image/png"].includes(file.type) || file.size === 0 || file.size > 5 * 1024 * 1024)
377
+ throw new Error("Choose a JPG or PNG photo under 5 MB");
378
+ const bytes = new Uint8Array(await file.slice(0, 4).arrayBuffer());
379
+ const signature = file.type === "image/png" ? [137, 80, 78, 71] : [255, 216, 255];
380
+ if (!signature.every((byte, index) => bytes[index] === byte))
381
+ throw new Error("Image content does not match its file type");
382
+ return submit({
383
+ id: crypto.randomUUID(),
384
+ ...options.release ? { release: options.release } : {},
385
+ quest,
386
+ stage: "upload",
387
+ file
388
+ });
389
+ }
390
+ };
391
+ }
392
+ return open;
393
+ }
394
+ var QuestUpdatedError = class extends RelayError {
395
+ constructor(message, code, draft) {
396
+ super(message, 409, [], code);
397
+ this.draft = draft;
398
+ }
399
+ draft;
400
+ };
401
+
402
+ // src/quest-experience.ts
403
+ function reconcileQuestDraft(view, draft) {
404
+ if (!draft) return null;
405
+ if (draft.kind === "photo")
406
+ return view.interaction === "photo" ? draft : null;
407
+ if (draft.kind === "none") return view.interaction === "claim" ? draft : null;
408
+ if (view.interaction !== "quiz") return null;
409
+ const answers = Object.fromEntries(
410
+ (view.presentation.quiz ?? []).flatMap((q) => {
411
+ const answer = draft.answers[q.id];
412
+ return q.choices.some((c) => c.id === answer) ? [[q.id, answer]] : [];
413
+ })
414
+ );
415
+ return { kind: "quiz", release: view.release, answers };
416
+ }
417
+ function questActions(submission, latest) {
418
+ return async (view) => {
419
+ async function run(task) {
420
+ try {
421
+ return { kind: "submitted", attempt: await task() };
422
+ } catch (error) {
423
+ if (!(error instanceof QuestUpdatedError)) throw error;
424
+ const current = await latest(view.quest).catch(() => {
425
+ throw error;
426
+ });
427
+ return {
428
+ kind: "quest-updated",
429
+ latest: current,
430
+ originalDraft: error.draft,
431
+ draft: reconcileQuestDraft(current, error.draft)
432
+ };
433
+ }
434
+ }
435
+ if (view.lifecycle !== "active" || view.interaction === "staff" || view.interaction === "automatic") {
436
+ const handle = await submission(view.quest, {
437
+ input: "none",
438
+ release: view.release,
439
+ recoverPrevious: true
440
+ });
441
+ return {
442
+ kind: "status",
443
+ view,
444
+ status: handle.status,
445
+ resume: () => run(handle.resume)
446
+ };
447
+ }
448
+ switch (view.interaction) {
449
+ case "photo": {
450
+ const handle = await submission(view.quest, {
451
+ input: "photo",
452
+ release: view.release,
453
+ recoverPrevious: true
454
+ });
455
+ return {
456
+ kind: "photo",
457
+ view,
458
+ status: handle.status,
459
+ resume: () => run(handle.resume),
460
+ submit: (file) => run(() => handle.submit(file))
461
+ };
462
+ }
463
+ case "quiz": {
464
+ const handle = await submission(view.quest, {
465
+ input: "quiz",
466
+ release: view.release,
467
+ recoverPrevious: true
468
+ });
469
+ return {
470
+ kind: "quiz",
471
+ view,
472
+ status: handle.status,
473
+ resume: () => run(handle.resume),
474
+ submit: (answers) => run(() => handle.submit(answers, { release: view.release }))
475
+ };
476
+ }
477
+ case "claim": {
478
+ const handle = await submission(view.quest, {
479
+ input: "none",
480
+ release: view.release,
481
+ recoverPrevious: true
482
+ });
483
+ return {
484
+ kind: "claim",
485
+ view,
486
+ status: handle.status,
487
+ resume: () => run(handle.resume),
488
+ submit: () => run(handle.submit)
489
+ };
490
+ }
491
+ }
492
+ };
493
+ }
494
+
495
+ // src/progress.ts
496
+ function observeProgress(transport, listener, options = {}, onDispose = () => {
497
+ }) {
498
+ const interval = options.intervalMs ?? 1500;
499
+ const maximum = options.maxBackoffMs ?? 3e4;
500
+ if (!Number.isFinite(interval) || interval <= 0 || !Number.isFinite(maximum) || maximum < interval)
501
+ throw new Error(
502
+ "Progress polling requires a positive interval and a maximum at least as large"
503
+ );
504
+ let disposed = false;
505
+ let controller;
506
+ let timer;
507
+ let sessionId;
508
+ let failures = 0;
509
+ function clear() {
510
+ controller?.abort();
511
+ if (timer !== void 0) clearTimeout(timer);
512
+ sessionId = void 0;
513
+ if (!disposed) listener({ kind: "loading" });
514
+ }
515
+ async function refresh() {
516
+ if (disposed) return;
517
+ controller?.abort();
518
+ if (timer !== void 0) clearTimeout(timer);
519
+ const request = new AbortController();
520
+ controller = request;
521
+ const current = () => !disposed && !request.signal.aborted;
522
+ try {
523
+ const session = await transport.session(request.signal);
524
+ if (!current()) return;
525
+ if (sessionId !== (session?.id ?? null)) {
526
+ sessionId = session?.id ?? null;
527
+ listener({ kind: "session", session });
528
+ }
529
+ if (session?.principal.kind === "member") {
530
+ const progress = await transport.progress(session.id, request.signal);
531
+ if (!current()) return;
532
+ const confirmed = await transport.session(request.signal);
533
+ if (!current()) return;
534
+ if (confirmed?.id !== session.id || progress.member !== session.principal.member) {
535
+ sessionId = void 0;
536
+ listener({ kind: "loading" });
537
+ return;
538
+ }
539
+ listener({ kind: "ready", session: confirmed, progress });
540
+ }
541
+ failures = 0;
542
+ } catch (error) {
543
+ if (!current()) return;
544
+ failures = Math.min(failures + 1, 20);
545
+ sessionId = void 0;
546
+ listener({
547
+ kind: "error",
548
+ error: error instanceof Error ? error : new Error("Could not refresh progress")
549
+ });
550
+ } finally {
551
+ if (current())
552
+ timer = setTimeout(
553
+ () => void refresh(),
554
+ Math.min(maximum, interval * 2 ** failures)
555
+ );
556
+ }
557
+ }
558
+ const observer = {
559
+ refresh,
560
+ clear,
561
+ dispose() {
562
+ if (disposed) return;
563
+ disposed = true;
564
+ controller?.abort();
565
+ if (timer !== void 0) clearTimeout(timer);
566
+ onDispose();
567
+ }
568
+ };
569
+ return observer;
570
+ }
571
+
572
+ // src/index.ts
573
+ import { z as z7 } from "zod";
574
+
575
+ // src/auth.ts
576
+ import { z as z3 } from "zod";
577
+ var authScopeSchema = z3.object({
578
+ organization: identifier,
579
+ project: identifier,
580
+ environment: z3.enum(["test", "live"])
581
+ }).strict();
582
+ var principalSchema = z3.discriminatedUnion("kind", [
583
+ z3.object({ kind: z3.literal("member"), member: identifier }).strict(),
584
+ z3.object({ kind: z3.literal("staff"), subject: identifier }).strict(),
585
+ z3.object({ kind: z3.literal("operator"), subject: identifier }).strict()
586
+ ]);
587
+ var sessionSchema = authScopeSchema.extend({
588
+ id: identifier,
589
+ principal: principalSchema,
590
+ expiresAt: z3.number()
591
+ });
592
+ var exchangeSchema = authScopeSchema.extend({
593
+ identity: z3.object({ issuer: identifier, subject: z3.string().min(1).max(200) }).strict(),
594
+ role: z3.enum(["member", "staff", "operator"])
595
+ }).strict();
596
+ var exchangeResultSchema = z3.object({
597
+ token: z3.string(),
598
+ session: sessionSchema
599
+ });
600
+
601
+ // src/wallet.ts
602
+ import { z as z4 } from "zod";
603
+ var solanaAddressSchema = z4.string().min(32).max(44).regex(/^[1-9A-HJ-NP-Za-km-z]+$/);
604
+ var walletConnectionSchema = z4.object({
605
+ chain: z4.literal("solana"),
606
+ address: solanaAddressSchema,
607
+ verifiedAt: z4.number().int().nonnegative(),
608
+ revision: z4.number().int().positive()
609
+ }).strict();
610
+ var walletChallengeInputSchema = z4.object({
611
+ chain: z4.literal("solana"),
612
+ address: solanaAddressSchema
613
+ }).strict();
614
+ var walletChallengeSchema = walletChallengeInputSchema.extend({
615
+ id: z4.string().uuid(),
616
+ message: z4.string(),
617
+ expiresAt: z4.number()
618
+ });
619
+ var walletProofSchema = z4.object({
620
+ challenge: z4.string().uuid(),
621
+ signature: z4.string().min(64).max(88).regex(/^[1-9A-HJ-NP-Za-km-z]+$/)
622
+ }).strict();
623
+ var solanaBalanceSchema = z4.object({
624
+ address: solanaAddressSchema,
625
+ network: z4.literal("mainnet-beta"),
626
+ lamports: z4.string().regex(/^(0|[1-9][0-9]*)$/),
627
+ slot: z4.number().int().nonnegative().safe(),
628
+ observedAt: z4.number().int().nonnegative()
629
+ }).strict();
630
+
631
+ // src/collection.ts
632
+ import { z as z5 } from "zod";
633
+ var collectionCodeSchema = z5.string().regex(/^relay:collect:[a-f0-9]{64}$/);
634
+ var collectionPassSchema = z5.object({
635
+ code: collectionCodeSchema,
636
+ expiresAt: z5.number(),
637
+ entitlement: identifier
638
+ });
639
+ var collectionResolveSchema = z5.object({ code: collectionCodeSchema }).strict();
640
+ var collectionBeginSchema = collectionResolveSchema.extend({
641
+ actionId: identifier,
642
+ revision: z5.number().int().positive(),
643
+ selection: pickupSelectionSchema.optional()
644
+ });
645
+ var collectionPreviewSchema = z5.object({
646
+ entitlement: entitlementSchema,
647
+ inventory: pickupAvailabilitySchema,
648
+ expiresAt: z5.number()
649
+ });
650
+
651
+ // src/configuration.ts
652
+ function questConfiguration(release) {
653
+ return release.configuration ?? {
654
+ defaults: Object.fromEntries(
655
+ Object.entries(release.fields).map(([key, field]) => [
656
+ key,
657
+ field.default
658
+ ])
659
+ ),
660
+ overrides: { ...release.values },
661
+ preserved: Object.keys(release.values)
662
+ };
663
+ }
664
+
665
+ // src/deployments.ts
666
+ import { z as z6 } from "zod";
667
+ var projectDeploymentSchema = catalogDeploymentSchema.omit({ expectedRevision: true }).extend({
668
+ releases: z6.array(releaseSchema).max(50).default([])
669
+ }).refine(
670
+ (value) => value.releases.length > 0 || value.types.length > 0 || value.collections.length > 0 || value.supportedInteractions !== void 0,
671
+ "No resources configured. Add quests, questTypes, collections, or supportedInteractions to relay.json."
672
+ );
673
+ var deploymentPreviewSchema = z6.object({
674
+ id: identifier,
675
+ baseRevision: z6.number().int().nonnegative(),
676
+ releases: z6.array(publishedReleaseSchema),
677
+ types: z6.array(questTypeVersionSchema),
678
+ collections: z6.array(questCollectionSchema),
679
+ supportedInteractions: z6.array(interactionSchema)
680
+ });
681
+ var publishDeploymentSchema = z6.object({ id: identifier }).strict();
682
+ var deploymentResultSchema = deploymentPreviewSchema.extend({
683
+ revision: z6.number().int().positive()
684
+ });
685
+
686
+ // src/index.ts
687
+ function createRelayClient(options) {
688
+ async function send(path, init = {}) {
689
+ const headers = new Headers(init.headers);
690
+ if (options.token) headers.set("Authorization", `Bearer ${options.token}`);
691
+ if (options.organization)
692
+ headers.set("X-Relay-Organization", options.organization);
693
+ if (options.project) headers.set("X-Relay-Project", options.project);
694
+ if (options.environment)
695
+ headers.set("X-Relay-Environment", options.environment);
696
+ if (init.method && init.method !== "GET") headers.set("X-Relay-CSRF", "1");
697
+ const response = await fetch(options.baseUrl + path, {
698
+ ...init,
699
+ signal: init.signal ? AbortSignal.any([init.signal, AbortSignal.timeout(15e3)]) : AbortSignal.timeout(15e3),
700
+ headers
701
+ });
702
+ if (!response.ok) {
703
+ const value = await response.json().catch(() => null);
704
+ const error = errorSchema.safeParse(value);
705
+ throw new RelayError(
706
+ error.success ? error.data.error : `Request failed (${response.status})`,
707
+ response.status,
708
+ error.success ? error.data.missing : [],
709
+ error.success ? error.data.code : void 0
710
+ );
711
+ }
712
+ return response;
713
+ }
714
+ async function request(path, schema, init = {}) {
715
+ const response = await send(path, init);
716
+ const value = await response.json();
717
+ return schema.parse(value);
718
+ }
719
+ async function walletRequest(path, schema, init = {}) {
720
+ for (let attempt = 0; ; attempt++) {
721
+ let response;
722
+ try {
723
+ response = await send(path, init);
724
+ } catch (error) {
725
+ const transient = error instanceof RelayError && [500, 502, 503, 504].includes(error.status) || error instanceof TypeError || error instanceof DOMException && error.name === "TimeoutError";
726
+ if (!transient || attempt === 2) throw error;
727
+ await new Promise(
728
+ (resolve) => setTimeout(resolve, attempt === 0 ? 250 : 750)
729
+ );
730
+ continue;
731
+ }
732
+ const value = await response.json();
733
+ return schema.parse(value);
734
+ }
735
+ }
736
+ function query(path, values) {
737
+ const params = new URLSearchParams();
738
+ for (const [key, value] of Object.entries(values))
739
+ if (value !== void 0) params.set(key, String(value));
740
+ return path + "?" + params;
741
+ }
742
+ function json(body) {
743
+ return {
744
+ method: "POST",
745
+ headers: { "Content-Type": "application/json" },
746
+ body: JSON.stringify(body)
747
+ };
748
+ }
749
+ const submission = submissions(
750
+ {
751
+ session: () => request("/v1/auth/session", sessionSchema.nullable()),
752
+ upload: (file, action, session) => request("/v1/me/evidence", evidenceSchema, {
753
+ method: "POST",
754
+ headers: {
755
+ "Content-Type": file.type,
756
+ "X-Relay-Upload-ID": action,
757
+ "X-Relay-Session": session
758
+ },
759
+ body: file
760
+ }),
761
+ claim: (input, session) => request("/v1/me/attempts", attemptSchema, {
762
+ ...json(input),
763
+ headers: {
764
+ "Content-Type": "application/json",
765
+ "X-Relay-Session": session
766
+ }
767
+ }),
768
+ retry: (attempt, session) => request(`/v1/attempts/${attempt}/retry`, attemptSchema, {
769
+ ...json({}),
770
+ headers: {
771
+ "Content-Type": "application/json",
772
+ "X-Relay-Session": session
773
+ }
774
+ })
775
+ },
776
+ options.submissionStore,
777
+ options.baseUrl
778
+ );
779
+ const observers = /* @__PURE__ */ new Set();
780
+ async function changeSession(task) {
781
+ for (const observer of observers) observer.clear();
782
+ try {
783
+ return await task();
784
+ } finally {
785
+ for (const observer of observers) void observer.refresh();
786
+ }
787
+ }
788
+ const experience = () => request("/v1/me/experience", questExperienceSchema);
789
+ async function getQuest(id) {
790
+ const quest = (await experience()).quests.find((q) => q.quest === id);
791
+ if (!quest) throw new RelayError("Quest not found", 404);
792
+ return quest;
793
+ }
794
+ return {
795
+ quests: {
796
+ get: getQuest,
797
+ retry: async (view) => {
798
+ if (!view.attempt || view.attempt.status !== "failed")
799
+ throw new Error("There is no failed attempt to retry.");
800
+ const session = await request(
801
+ "/v1/auth/session",
802
+ sessionSchema.nullable()
803
+ );
804
+ if (session?.principal.kind !== "member")
805
+ throw new Error("Sign in to retry verification.");
806
+ return request(`/v1/attempts/${view.attempt.id}/retry`, attemptSchema, {
807
+ ...json({}),
808
+ headers: {
809
+ "Content-Type": "application/json",
810
+ "X-Relay-Session": session.id
811
+ }
812
+ });
813
+ },
814
+ experience,
815
+ collection: async (id) => {
816
+ const collection = (await experience()).collections.find(
817
+ (c) => c.id === id
818
+ );
819
+ if (!collection) throw new RelayError("Collection not found", 404);
820
+ return collection;
821
+ },
822
+ open: questActions(submission, getQuest),
823
+ observe: (listener, options2) => {
824
+ const observer = observeProgress(
825
+ {
826
+ session: (signal) => request("/v1/auth/session", sessionSchema.nullable(), { signal }),
827
+ progress: (session, signal) => request("/v1/me/experience", questExperienceSchema, {
828
+ signal,
829
+ headers: { "X-Relay-Session": session }
830
+ })
831
+ },
832
+ listener,
833
+ options2,
834
+ () => observers.delete(observer)
835
+ );
836
+ observers.add(observer);
837
+ observer.clear();
838
+ void observer.refresh();
839
+ return { refresh: observer.refresh, dispose: observer.dispose };
840
+ }
841
+ },
842
+ wallet: {
843
+ connection: () => walletRequest("/v1/me/wallet", walletConnectionSchema.nullable()),
844
+ challenge: (input, session) => walletRequest("/v1/me/wallet/challenge", walletChallengeSchema, {
845
+ ...json(walletChallengeInputSchema.parse(input)),
846
+ headers: {
847
+ "Content-Type": "application/json",
848
+ "X-Relay-Session": session
849
+ }
850
+ }),
851
+ verify: (input, session) => walletRequest("/v1/me/wallet/verify", walletConnectionSchema, {
852
+ ...json(walletProofSchema.parse(input)),
853
+ headers: {
854
+ "Content-Type": "application/json",
855
+ "X-Relay-Session": session
856
+ }
857
+ })
858
+ },
859
+ auth: {
860
+ providers: () => request(
861
+ "/v1/auth/providers",
862
+ z7.object({
863
+ providers: z7.array(
864
+ z7.object({
865
+ provider: z7.enum(["google", "apple", "x", "discord"]),
866
+ configured: z7.boolean()
867
+ })
868
+ ),
869
+ email: z7.boolean(),
870
+ demo: z7.boolean()
871
+ })
872
+ ),
873
+ start: (provider, mode) => request(
874
+ "/v1/auth/start",
875
+ z7.object({ url: z7.string() }),
876
+ json({ provider, mode })
877
+ ),
878
+ emailStart: (email) => request(
879
+ "/v1/auth/email/start",
880
+ z7.object({ challenge: z7.string() }),
881
+ json({ email })
882
+ ),
883
+ emailVerify: (challenge, code) => changeSession(
884
+ () => request(
885
+ "/v1/auth/email/verify",
886
+ sessionSchema,
887
+ json({ challenge, code })
888
+ )
889
+ ),
890
+ connections: () => request(
891
+ "/v1/auth/connections",
892
+ z7.array(
893
+ z7.object({
894
+ provider: z7.string(),
895
+ subject: z7.string(),
896
+ signInEnabled: z7.boolean()
897
+ })
898
+ )
899
+ ),
900
+ session: () => request("/v1/auth/session", sessionSchema.nullable()),
901
+ demoSignIn: (code) => changeSession(
902
+ () => request("/v1/auth/demo", sessionSchema, json({ code }))
903
+ ),
904
+ signOut: () => changeSession(
905
+ () => request("/v1/auth/logout", z7.object({ ok: z7.boolean() }), json({}))
906
+ ),
907
+ exchange: (input) => request("/v1/auth/exchange", exchangeResultSchema, json(input))
908
+ },
909
+ me: {
910
+ observeProgress: (listener, options2) => {
911
+ const observer = observeProgress(
912
+ {
913
+ session: (signal) => request("/v1/auth/session", sessionSchema.nullable(), { signal }),
914
+ progress: (session, signal) => request("/v1/me/progress", memberProgressSchema, {
915
+ signal,
916
+ headers: { "X-Relay-Session": session }
917
+ })
918
+ },
919
+ listener,
920
+ options2,
921
+ () => observers.delete(observer)
922
+ );
923
+ observers.add(observer);
924
+ observer.clear();
925
+ void observer.refresh();
926
+ return { refresh: observer.refresh, dispose: observer.dispose };
927
+ },
928
+ submission,
929
+ collectionPass: (entitlement, session) => request("/v1/me/collection-pass", collectionPassSchema, {
930
+ ...json({ entitlement: identifier.parse(entitlement) }),
931
+ headers: {
932
+ "Content-Type": "application/json",
933
+ "X-Relay-Session": session
934
+ }
935
+ }),
936
+ linkPass: (code) => request("/v1/me/pass", attemptSchema, json({ code })),
937
+ progress: () => request("/v1/me/progress", memberProgressSchema),
938
+ claim: (input, options2) => request("/v1/me/attempts", attemptSchema, {
939
+ ...json(input),
940
+ headers: {
941
+ "Content-Type": "application/json",
942
+ ...options2 ? { "X-Relay-Session": options2.session } : {}
943
+ }
944
+ }),
945
+ upload: (file) => request("/v1/me/evidence", evidenceSchema, {
946
+ method: "POST",
947
+ headers: { "Content-Type": file.type },
948
+ body: file
949
+ })
950
+ },
951
+ reviews: {
952
+ list: (options2 = {}, requestOptions) => request(
953
+ query("/v1/reviews", reviewListSchema.parse(options2)),
954
+ reviewPageSchema,
955
+ requestOptions
956
+ )
957
+ },
958
+ members: {
959
+ list: (options2 = {}, requestOptions) => request(
960
+ query("/v1/members", memberListSchema.parse(options2)),
961
+ memberPageSchema,
962
+ requestOptions
963
+ )
964
+ },
965
+ evidence: {
966
+ async download(id, requestOptions) {
967
+ const response = await send(
968
+ `/v1/evidence/${identifier.parse(id)}`,
969
+ requestOptions
970
+ );
971
+ const type = response.headers.get("Content-Type");
972
+ if (type !== "image/jpeg" && type !== "image/png")
973
+ throw new RelayError("Unexpected evidence content type", 502);
974
+ return response.blob();
975
+ }
976
+ },
977
+ lookupMember: (issuer, subject) => request(
978
+ "/v1/members/lookup",
979
+ z7.object({ member: z7.string() }),
980
+ json({ issuer, subject })
981
+ ),
982
+ journeyTemplate: () => request("/v1/journey-template", z7.array(templateSchema)),
983
+ template: () => request("/v1/template", templateSchema),
984
+ snapshot: () => request("/v1/snapshot", snapshotSchema),
985
+ publish: (release) => request("/v1/releases", publishedReleaseSchema, json(release)),
986
+ publishBatch: (releases) => request(
987
+ "/v1/releases/batch",
988
+ z7.array(publishedReleaseSchema),
989
+ json({ releases })
990
+ ),
991
+ progress: (member) => request(
992
+ `/v1/members/${encodeURIComponent(member)}/progress`,
993
+ memberProgressSchema
994
+ ),
995
+ beginHandover: (id, input) => request(`/v1/entitlements/${id}/begin`, entitlementSchema, json(input)),
996
+ confirmHandover: (id, input) => request(`/v1/entitlements/${id}/confirm`, entitlementSchema, json(input)),
997
+ upload: (member, file) => request(
998
+ `/v1/evidence?member=${encodeURIComponent(member)}`,
999
+ evidenceSchema,
1000
+ {
1001
+ method: "POST",
1002
+ headers: { "Content-Type": file.type },
1003
+ body: file
1004
+ }
1005
+ ),
1006
+ submit: (input) => request("/v1/attempts", attemptSchema, json(input)),
1007
+ retry: (id) => request(`/v1/attempts/${id}/retry`, attemptSchema, json({})),
1008
+ decide: (id, input) => request(`/v1/attempts/${id}/review`, attemptSchema, json(input))
1009
+ };
1010
+ }
1011
+
1012
+ export {
1013
+ interactionSchema,
1014
+ questInteraction,
1015
+ questTypeVersionSchema,
1016
+ collectionSlotSchema,
1017
+ questCollectionSchema,
1018
+ questInstanceSchema,
1019
+ questDraftChangesSchema,
1020
+ questDraftSchema,
1021
+ questCatalogSchema,
1022
+ catalogDeploymentSchema,
1023
+ catalogDeploymentPreviewSchema,
1024
+ saveQuestDraftSchema,
1025
+ publishQuestDraftSchema,
1026
+ questViewSchema,
1027
+ collectionViewSchema,
1028
+ questExperienceSchema,
1029
+ questPublicationPreviewSchema,
1030
+ RelayError,
1031
+ QuestUpdatedError,
1032
+ reconcileQuestDraft,
1033
+ questActions,
1034
+ authScopeSchema,
1035
+ principalSchema,
1036
+ sessionSchema,
1037
+ exchangeSchema,
1038
+ exchangeResultSchema,
1039
+ solanaAddressSchema,
1040
+ walletConnectionSchema,
1041
+ walletChallengeInputSchema,
1042
+ walletChallengeSchema,
1043
+ walletProofSchema,
1044
+ solanaBalanceSchema,
1045
+ collectionCodeSchema,
1046
+ collectionPassSchema,
1047
+ collectionResolveSchema,
1048
+ collectionBeginSchema,
1049
+ collectionPreviewSchema,
1050
+ questConfiguration,
1051
+ projectDeploymentSchema,
1052
+ deploymentPreviewSchema,
1053
+ publishDeploymentSchema,
1054
+ deploymentResultSchema,
1055
+ createRelayClient
1056
+ };