@receiz/sdk 97.0.0 → 97.2.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.
- package/README.md +98 -5
- package/dist/cli.js +106 -2
- package/dist/index.d.ts +558 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +864 -13
- package/dist/react.d.ts +80 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +200 -0
- package/docs/app-runtime-commerce-cloud.md +236 -0
- package/docs/app-state-public-projections.md +82 -7
- package/docs/cli-and-conformance.md +28 -0
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildReceizIdContinueRequest, createReceizIdIdentity, projectReceizIdentityAccount, readReceizIdentityArtifact, signReceizIdentityLoginProof, verifyReceizIdentityLoginProof, } from "./identity.js";
|
|
2
2
|
export * from "./identity.js";
|
|
3
|
-
export const RECEIZ_SDK_VERSION = "97.
|
|
3
|
+
export const RECEIZ_SDK_VERSION = "97.2.0";
|
|
4
4
|
export const RECEIZ_DEFAULT_BASE_URL = "https://receiz.com";
|
|
5
5
|
export const RECEIZ_WEBHOOK_SIGNATURE_HEADER = "x-receiz-signature";
|
|
6
6
|
export const RECEIZ_WEBHOOK_TIMESTAMP_HEADER = "x-receiz-timestamp";
|
|
@@ -200,6 +200,193 @@ export function createReceizAppStateRecord(input) {
|
|
|
200
200
|
};
|
|
201
201
|
return record;
|
|
202
202
|
}
|
|
203
|
+
export function createReceizAppStateFeed(records, options = {}) {
|
|
204
|
+
const feed = {
|
|
205
|
+
schema: options.schema ?? RECEIZ_APP_STATE_FEED_SCHEMA,
|
|
206
|
+
records: records.map((record) => createReceizAppStateRecord(record)),
|
|
207
|
+
};
|
|
208
|
+
if (options.namespace)
|
|
209
|
+
feed.namespace = options.namespace;
|
|
210
|
+
if (options.externalCreatorId)
|
|
211
|
+
feed.externalCreatorId = options.externalCreatorId;
|
|
212
|
+
return feed;
|
|
213
|
+
}
|
|
214
|
+
export function createReceizPublicStoreStateRecord(input) {
|
|
215
|
+
return createReceizAppStateRecord({
|
|
216
|
+
...input,
|
|
217
|
+
platform: input.platform ?? "Receiz.app Commerce Cloud",
|
|
218
|
+
schema: input.schema ?? RECEIZ_PUBLIC_STORE_STATE_PROJECTION_SCHEMA,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
export function createReceizTenantAppStateRecord(input) {
|
|
222
|
+
const sourceUrl = input.sourceUrl ?? receizAppStateUrlFromHost(input.tenantHost);
|
|
223
|
+
const data = input.data ?? {
|
|
224
|
+
...input.state,
|
|
225
|
+
...(input.schemaVersion !== undefined ? { schemaVersion: input.schemaVersion } : {}),
|
|
226
|
+
tenantHost: normalizeReceizHost(input.tenantHost),
|
|
227
|
+
};
|
|
228
|
+
const record = {
|
|
229
|
+
sourceUrl,
|
|
230
|
+
state: input.projectionState ?? (input.visibility === "private" ? "private" : "published"),
|
|
231
|
+
platform: input.platform ?? "Receiz App Runtime",
|
|
232
|
+
schema: input.schema ?? RECEIZ_PUBLIC_STORE_STATE_PROJECTION_SCHEMA,
|
|
233
|
+
data,
|
|
234
|
+
};
|
|
235
|
+
if (input.id)
|
|
236
|
+
record.id = input.id;
|
|
237
|
+
const externalCreatorId = input.externalCreatorId ?? input.creatorReceizId;
|
|
238
|
+
if (externalCreatorId)
|
|
239
|
+
record.externalCreatorId = externalCreatorId;
|
|
240
|
+
if (input.title)
|
|
241
|
+
record.title = input.title;
|
|
242
|
+
record.namespace = input.namespace ?? receizAppStateNamespaceFromUrl(sourceUrl);
|
|
243
|
+
record.record = input.record ?? input.state;
|
|
244
|
+
return createReceizAppStateRecord(record);
|
|
245
|
+
}
|
|
246
|
+
function isReceizAppStateFeed(value) {
|
|
247
|
+
return isRecord(value) && Array.isArray(value.records);
|
|
248
|
+
}
|
|
249
|
+
export function normalizeReceizHost(hostOrUrl) {
|
|
250
|
+
const trimmed = hostOrUrl.trim();
|
|
251
|
+
if (!trimmed)
|
|
252
|
+
throw new ReceizValidationError("receiz.domains.normalizeHost", ["host is required"]);
|
|
253
|
+
try {
|
|
254
|
+
return new URL(/^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`).hostname.toLowerCase();
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return trimmed.replace(/^https?:\/\//i, "").split(/[/?#]/)[0]?.toLowerCase() ?? trimmed.toLowerCase();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function bodyWithIdempotency(body, idempotencyKey) {
|
|
261
|
+
if (!idempotencyKey || Object.prototype.hasOwnProperty.call(body, "idempotencyKey"))
|
|
262
|
+
return body;
|
|
263
|
+
return { ...body, idempotencyKey };
|
|
264
|
+
}
|
|
265
|
+
function headersWithIdempotency(headers, idempotencyKey) {
|
|
266
|
+
const next = new Headers(headers);
|
|
267
|
+
if (idempotencyKey)
|
|
268
|
+
next.set("idempotency-key", idempotencyKey);
|
|
269
|
+
return next;
|
|
270
|
+
}
|
|
271
|
+
function queryParams(input) {
|
|
272
|
+
const params = {};
|
|
273
|
+
for (const [key, value] of Object.entries(input)) {
|
|
274
|
+
if (value === null ||
|
|
275
|
+
value === undefined ||
|
|
276
|
+
typeof value === "string" ||
|
|
277
|
+
typeof value === "number" ||
|
|
278
|
+
typeof value === "boolean") {
|
|
279
|
+
params[key] = value;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return params;
|
|
283
|
+
}
|
|
284
|
+
function usdToCents(value) {
|
|
285
|
+
const trimmed = value.trim();
|
|
286
|
+
if (!/^\d+(\.\d{1,2})?$/.test(trimmed))
|
|
287
|
+
throw new ReceizValidationError("receiz.commerce.amountUsd", ["amountUsd must be a positive USD decimal"]);
|
|
288
|
+
const [whole = "0", fraction = ""] = trimmed.split(".");
|
|
289
|
+
return BigInt(whole) * 100n + BigInt((fraction + "00").slice(0, 2));
|
|
290
|
+
}
|
|
291
|
+
function centsToUsd(value) {
|
|
292
|
+
const sign = value < 0n ? "-" : "";
|
|
293
|
+
const abs = value < 0n ? -value : value;
|
|
294
|
+
return `${sign}${abs / 100n}.${String(abs % 100n).padStart(2, "0")}`;
|
|
295
|
+
}
|
|
296
|
+
function stringToCents(value) {
|
|
297
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
298
|
+
return BigInt(Math.max(0, Math.trunc(value)));
|
|
299
|
+
if (typeof value === "bigint")
|
|
300
|
+
return value < 0n ? 0n : value;
|
|
301
|
+
if (typeof value === "string" && /^\d+$/.test(value.trim()))
|
|
302
|
+
return BigInt(value.trim());
|
|
303
|
+
return 0n;
|
|
304
|
+
}
|
|
305
|
+
function sandboxProducts(count) {
|
|
306
|
+
return Array.from({ length: Math.max(0, count) }, (_, index) => ({
|
|
307
|
+
id: `sandbox_product_${index + 1}`,
|
|
308
|
+
title: `Sandbox Product ${index + 1}`,
|
|
309
|
+
amountUsd: centsToUsd(BigInt(index + 1) * 1200n),
|
|
310
|
+
proofObject: {
|
|
311
|
+
schema: "receiz.sdk.sandbox.proof_object.v1",
|
|
312
|
+
objectId: `sandbox_product_${index + 1}`,
|
|
313
|
+
},
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
export async function verifyReceizWebhookRequest(request, options) {
|
|
317
|
+
const signature = request.headers.get(RECEIZ_WEBHOOK_SIGNATURE_HEADER) ?? "";
|
|
318
|
+
const timestamp = request.headers.get(RECEIZ_WEBHOOK_TIMESTAMP_HEADER) ?? "";
|
|
319
|
+
const body = await request.clone().text();
|
|
320
|
+
const verifyInput = {
|
|
321
|
+
secret: options.secret,
|
|
322
|
+
timestamp,
|
|
323
|
+
body,
|
|
324
|
+
signature,
|
|
325
|
+
};
|
|
326
|
+
if (options.toleranceSeconds !== undefined)
|
|
327
|
+
verifyInput.toleranceSeconds = options.toleranceSeconds;
|
|
328
|
+
if (options.nowMs !== undefined)
|
|
329
|
+
verifyInput.nowMs = options.nowMs;
|
|
330
|
+
return verifyReceizWebhookSignature(verifyInput);
|
|
331
|
+
}
|
|
332
|
+
function resolveReceizAppStateProjectionRecord(input) {
|
|
333
|
+
if (!input || !isRecord(input))
|
|
334
|
+
return null;
|
|
335
|
+
const looksLikeResponse = typeof input.ok === "boolean" &&
|
|
336
|
+
Object.prototype.hasOwnProperty.call(input, "record") &&
|
|
337
|
+
!Object.prototype.hasOwnProperty.call(input, "sourceUrl") &&
|
|
338
|
+
!Object.prototype.hasOwnProperty.call(input, "data");
|
|
339
|
+
if (looksLikeResponse) {
|
|
340
|
+
const response = input;
|
|
341
|
+
return response.record && isRecord(response.record) ? response.record : null;
|
|
342
|
+
}
|
|
343
|
+
return input;
|
|
344
|
+
}
|
|
345
|
+
function receizAppStateRestoreError(record, options) {
|
|
346
|
+
if (!record)
|
|
347
|
+
return "app_state_not_found";
|
|
348
|
+
if (options.schema && record.schema !== options.schema)
|
|
349
|
+
return "app_state_schema_mismatch";
|
|
350
|
+
if (options.state && record.state !== options.state)
|
|
351
|
+
return "app_state_state_mismatch";
|
|
352
|
+
if (!isRecord(record.data))
|
|
353
|
+
return "app_state_data_missing";
|
|
354
|
+
if (options.requiredDataKey && !Object.prototype.hasOwnProperty.call(record.data, options.requiredDataKey)) {
|
|
355
|
+
return "app_state_required_data_missing";
|
|
356
|
+
}
|
|
357
|
+
return "app_state_data_unavailable";
|
|
358
|
+
}
|
|
359
|
+
export function extractReceizAppStateData(input, options = {}) {
|
|
360
|
+
const record = resolveReceizAppStateProjectionRecord(input);
|
|
361
|
+
if (!record)
|
|
362
|
+
return null;
|
|
363
|
+
if (options.schema && record.schema !== options.schema)
|
|
364
|
+
return null;
|
|
365
|
+
if (options.state && record.state !== options.state)
|
|
366
|
+
return null;
|
|
367
|
+
if (!isRecord(record.data))
|
|
368
|
+
return null;
|
|
369
|
+
if (options.requiredDataKey && !Object.prototype.hasOwnProperty.call(record.data, options.requiredDataKey))
|
|
370
|
+
return null;
|
|
371
|
+
return record.data;
|
|
372
|
+
}
|
|
373
|
+
export function restoreReceizAppStateProjection(input, options = {}) {
|
|
374
|
+
const record = resolveReceizAppStateProjectionRecord(input);
|
|
375
|
+
const data = extractReceizAppStateData(input, options);
|
|
376
|
+
if (!record || !data) {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
record,
|
|
380
|
+
data: null,
|
|
381
|
+
error: receizAppStateRestoreError(record, options),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
ok: true,
|
|
386
|
+
record,
|
|
387
|
+
data,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
203
390
|
function isRecord(value) {
|
|
204
391
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
205
392
|
}
|
|
@@ -367,6 +554,8 @@ export class ReceizClient {
|
|
|
367
554
|
baseUrl;
|
|
368
555
|
accessToken;
|
|
369
556
|
fetchImpl;
|
|
557
|
+
doctor = (options = {}) => this.runDoctor(options);
|
|
558
|
+
capabilities = (options = {}) => this.inspectCapabilities(options);
|
|
370
559
|
verification = {
|
|
371
560
|
verifyArtifact: (file) => this.verifyArtifact(file),
|
|
372
561
|
sealArtifact: (file, options) => this.sealArtifact(file, options),
|
|
@@ -380,13 +569,26 @@ export class ReceizClient {
|
|
|
380
569
|
registryFeed: (feed, options = {}) => this.publishAppState(feed, options),
|
|
381
570
|
};
|
|
382
571
|
appState = {
|
|
383
|
-
publish: (
|
|
572
|
+
publish: (input, options = {}) => this.publishAppStateInput(input, options),
|
|
384
573
|
publishRecord: (record, options = {}) => this.publishAppStateRecord(record, options),
|
|
574
|
+
resolve: (input) => this.resolveAppState(input),
|
|
385
575
|
byUrl: (url) => this.readAppStateByUrl(url),
|
|
386
576
|
byHost: (host) => this.readAppStateByHost(host),
|
|
387
577
|
byCreator: (receizId) => this.readAppStateByCreator(receizId),
|
|
388
578
|
byNamespace: (namespace) => this.readAppStateByNamespace(namespace),
|
|
389
579
|
byId: (id) => this.readAppStateById(id),
|
|
580
|
+
restoreByUrl: (url, options = {}) => this.restoreAppStateByUrl(url, options),
|
|
581
|
+
restoreByHost: (host, options = {}) => this.restoreAppStateByHost(host, options),
|
|
582
|
+
restoreById: (id, options = {}) => this.restoreAppStateById(id, options),
|
|
583
|
+
createRecord: createReceizAppStateRecord,
|
|
584
|
+
createTenantRecord: createReceizTenantAppStateRecord,
|
|
585
|
+
createFeed: createReceizAppStateFeed,
|
|
586
|
+
createPublicStoreRecord: createReceizPublicStoreStateRecord,
|
|
587
|
+
urlFromHost: receizAppStateUrlFromHost,
|
|
588
|
+
namespaceFromUrl: receizAppStateNamespaceFromUrl,
|
|
589
|
+
objectIdFromUrl: receizAppStateObjectIdFromUrl,
|
|
590
|
+
extractData: extractReceizAppStateData,
|
|
591
|
+
restoreProjection: restoreReceizAppStateProjection,
|
|
390
592
|
};
|
|
391
593
|
sports = {
|
|
392
594
|
conformance: () => this.sportsConformance(),
|
|
@@ -488,12 +690,154 @@ export class ReceizClient {
|
|
|
488
690
|
bootstrap: (username) => this.request(`/api/connect/login/bootstrap/${encodePathSegment(username)}`),
|
|
489
691
|
authorizeUrl: (options) => this.authorizeUrl(options),
|
|
490
692
|
};
|
|
693
|
+
customers = {
|
|
694
|
+
session: (body, options = {}) => this.delegatedWrite("/api/connect/customers/session", body, options),
|
|
695
|
+
portal: (body) => this.delegatedWrite("/api/connect/customers/portal", body),
|
|
696
|
+
orders: (query = {}) => this.delegated(appendQuery("/api/connect/customers/orders", queryParams(query))),
|
|
697
|
+
rewards: (query = {}) => this.delegated(appendQuery("/api/connect/customers/rewards", queryParams(query))),
|
|
698
|
+
assets: (query = {}) => this.delegated(appendQuery("/api/connect/customers/assets", queryParams(query))),
|
|
699
|
+
};
|
|
700
|
+
merchants = {
|
|
701
|
+
onboard: (body, options = {}) => this.delegatedWrite("/api/connect/merchants/onboard", body, options),
|
|
702
|
+
profile: (query = {}) => this.delegated(appendQuery("/api/connect/merchants/profile", query)),
|
|
703
|
+
capabilities: (query = {}) => this.delegated(appendQuery("/api/connect/merchants/capabilities", query)),
|
|
704
|
+
};
|
|
705
|
+
commerce = {
|
|
706
|
+
oneClickCheckout: (body) => this.oneClickCheckout(body),
|
|
707
|
+
refunds: {
|
|
708
|
+
create: (body, options = {}) => this.delegatedWrite("/api/connect/commerce/refunds", body, options),
|
|
709
|
+
},
|
|
710
|
+
subscriptions: {
|
|
711
|
+
create: (body, options = {}) => this.delegatedWrite("/api/connect/commerce/subscriptions", body, options),
|
|
712
|
+
cancel: (subscriptionId, body, options = {}) => this.delegatedWrite(`/api/connect/commerce/subscriptions/${encodePathSegment(subscriptionId)}/cancel`, body, options),
|
|
713
|
+
},
|
|
714
|
+
shipping: {
|
|
715
|
+
quote: (body) => this.delegatedWrite("/api/connect/commerce/shipping/quote", body),
|
|
716
|
+
update: (body, options = {}) => this.delegatedWrite("/api/connect/commerce/shipping/update", body, options),
|
|
717
|
+
},
|
|
718
|
+
tax: {
|
|
719
|
+
quote: (body) => this.delegatedWrite("/api/connect/commerce/tax/quote", body),
|
|
720
|
+
},
|
|
721
|
+
discounts: {
|
|
722
|
+
create: (body, options = {}) => this.delegatedWrite("/api/connect/commerce/discounts", body, options),
|
|
723
|
+
redeem: (body, options = {}) => this.delegatedWrite("/api/connect/commerce/discounts/redeem", body, options),
|
|
724
|
+
},
|
|
725
|
+
giftCards: {
|
|
726
|
+
issue: (body, options = {}) => this.delegatedWrite("/api/connect/commerce/gift-cards", body, options),
|
|
727
|
+
redeem: (body, options = {}) => this.delegatedWrite("/api/connect/commerce/gift-cards/redeem", body, options),
|
|
728
|
+
},
|
|
729
|
+
accessPasses: {
|
|
730
|
+
issue: (body, options = {}) => this.delegatedWrite("/api/connect/commerce/access-passes", body, options),
|
|
731
|
+
verify: (body) => this.delegatedWrite("/api/connect/commerce/access-passes/verify", body),
|
|
732
|
+
},
|
|
733
|
+
inventory: {
|
|
734
|
+
reserve: (body, options = {}) => this.delegatedWrite("/api/connect/commerce/inventory/reserve", body, options),
|
|
735
|
+
adjust: (body, options = {}) => this.delegatedWrite("/api/connect/commerce/inventory/adjust", body, options),
|
|
736
|
+
},
|
|
737
|
+
fulfillment: {
|
|
738
|
+
update: (body, options = {}) => this.delegatedWrite("/api/connect/commerce/fulfillment", body, options),
|
|
739
|
+
},
|
|
740
|
+
payouts: {
|
|
741
|
+
create: (body, options = {}) => this.delegatedWrite("/api/connect/commerce/payouts", body, options),
|
|
742
|
+
status: (query = {}) => this.delegated(appendQuery("/api/connect/commerce/payouts/status", query)),
|
|
743
|
+
},
|
|
744
|
+
};
|
|
491
745
|
payments = {
|
|
492
746
|
embeddedCheckout: (body) => this.request("/api/payments/embed/checkout", { method: "POST", body }),
|
|
493
747
|
embeddedNoteClaim: (body) => this.request("/api/payments/embed/note-claim", { method: "POST", body }),
|
|
494
748
|
};
|
|
749
|
+
media = {
|
|
750
|
+
upload: (file, options = {}) => this.uploadMedia(file, options),
|
|
751
|
+
transform: (body, options = {}) => this.delegatedWrite("/api/connect/media/transform", body, options),
|
|
752
|
+
objectUrl: (query) => appendQuery(`${this.baseUrl}/api/storage/object`, query),
|
|
753
|
+
};
|
|
754
|
+
domains = {
|
|
755
|
+
normalizeHost: normalizeReceizHost,
|
|
756
|
+
tenantUrl: receizAppStateUrlFromHost,
|
|
757
|
+
namespace: receizAppStateNamespaceFromUrl,
|
|
758
|
+
objectId: receizAppStateObjectIdFromUrl,
|
|
759
|
+
resolveTenant: (host, options = {}) => this.restoreAppStateByHost(host, options),
|
|
760
|
+
reserveSubdomain: (body, options = {}) => this.delegated("/api/connect/domains/subdomains/reserve", {
|
|
761
|
+
method: "POST",
|
|
762
|
+
body: bodyWithIdempotency(body, options.idempotencyKey),
|
|
763
|
+
headers: headersWithIdempotency(undefined, options.idempotencyKey),
|
|
764
|
+
}),
|
|
765
|
+
verifyCustomDomain: (body, options = {}) => this.delegated("/api/connect/domains/custom/verify", {
|
|
766
|
+
method: "POST",
|
|
767
|
+
body: bodyWithIdempotency(body, options.idempotencyKey),
|
|
768
|
+
headers: headersWithIdempotency(undefined, options.idempotencyKey),
|
|
769
|
+
}),
|
|
770
|
+
status: (query = {}) => this.delegated(appendQuery("/api/connect/domains/status", query)),
|
|
771
|
+
};
|
|
772
|
+
events = {
|
|
773
|
+
subscribe: (body) => this.eventSubscribe(body),
|
|
774
|
+
replay: (body) => this.delegatedWrite("/api/connect/events/replay", body),
|
|
775
|
+
list: (query = {}) => this.delegated(appendQuery("/api/connect/events", queryParams(query))),
|
|
776
|
+
ack: (eventId, body = {}) => this.delegatedWrite(`/api/connect/events/${encodePathSegment(eventId)}/ack`, body),
|
|
777
|
+
};
|
|
778
|
+
proof = {
|
|
779
|
+
query: (query) => this.delegated("/api/connect/proof/query", { method: "POST", body: query }),
|
|
780
|
+
};
|
|
781
|
+
jobs = {
|
|
782
|
+
enqueue: (body) => this.delegated("/api/connect/jobs", {
|
|
783
|
+
method: "POST",
|
|
784
|
+
body: bodyWithIdempotency(body, body.idempotencyKey),
|
|
785
|
+
headers: headersWithIdempotency(undefined, body.idempotencyKey),
|
|
786
|
+
}),
|
|
787
|
+
status: (jobId) => this.delegated(`/api/connect/jobs/${encodePathSegment(jobId)}`),
|
|
788
|
+
cancel: (jobId, body = {}) => this.delegatedWrite(`/api/connect/jobs/${encodePathSegment(jobId)}/cancel`, body),
|
|
789
|
+
list: (query = {}) => this.delegated(appendQuery("/api/connect/jobs", queryParams(query))),
|
|
790
|
+
};
|
|
791
|
+
permissions = {
|
|
792
|
+
grant: (body, options = {}) => this.delegatedWrite("/api/connect/permissions/grants", body, options),
|
|
793
|
+
revoke: (body, options = {}) => this.delegatedWrite("/api/connect/permissions/revoke", body, options),
|
|
794
|
+
check: (body) => this.delegatedWrite("/api/connect/permissions/check", body),
|
|
795
|
+
roles: (query = {}) => this.delegated(appendQuery("/api/connect/permissions/roles", query)),
|
|
796
|
+
};
|
|
797
|
+
audit = {
|
|
798
|
+
append: (body, options = {}) => this.delegatedWrite("/api/connect/audit/events", body, options),
|
|
799
|
+
query: (query = {}) => this.delegated(appendQuery("/api/connect/audit/events", queryParams(query))),
|
|
800
|
+
export: (query = {}) => this.delegated(appendQuery("/api/connect/audit/export", queryParams(query))),
|
|
801
|
+
};
|
|
802
|
+
risk = {
|
|
803
|
+
scorePayment: (body) => this.delegatedWrite("/api/connect/risk/payment", body),
|
|
804
|
+
scoreAccountRecovery: (body) => this.delegatedWrite("/api/connect/risk/account-recovery", body),
|
|
805
|
+
velocity: (body) => this.delegatedWrite("/api/connect/risk/velocity", body),
|
|
806
|
+
proofActivity: (body) => this.delegatedWrite("/api/connect/risk/proof-activity", body),
|
|
807
|
+
};
|
|
808
|
+
compliance = {
|
|
809
|
+
exportOrders: (query) => this.delegated(appendQuery("/api/connect/compliance/orders", queryParams(query))),
|
|
810
|
+
exportTaxes: (query) => this.delegated(appendQuery("/api/connect/compliance/taxes", queryParams(query))),
|
|
811
|
+
exportPayouts: (query) => this.delegated(appendQuery("/api/connect/compliance/payouts", queryParams(query))),
|
|
812
|
+
exportCustomers: (query) => this.delegated(appendQuery("/api/connect/compliance/customers", queryParams(query))),
|
|
813
|
+
exportAudit: (query) => this.delegated(appendQuery("/api/connect/compliance/audit", queryParams(query))),
|
|
814
|
+
};
|
|
815
|
+
portability = {
|
|
816
|
+
exportStore: (query) => this.delegated(appendQuery("/api/connect/portability/store/export", query)),
|
|
817
|
+
importStore: (body, options = {}) => this.delegatedWrite("/api/connect/portability/store/import", body, options),
|
|
818
|
+
};
|
|
819
|
+
search = {
|
|
820
|
+
query: (body) => this.delegatedWrite("/api/connect/search/query", body),
|
|
821
|
+
products: (body) => this.delegatedWrite("/api/connect/search/products", body),
|
|
822
|
+
pages: (body) => this.delegatedWrite("/api/connect/search/pages", body),
|
|
823
|
+
blog: (body) => this.delegatedWrite("/api/connect/search/blog", body),
|
|
824
|
+
orders: (body) => this.delegatedWrite("/api/connect/search/orders", body),
|
|
825
|
+
customers: (body) => this.delegatedWrite("/api/connect/search/customers", body),
|
|
826
|
+
proofObjects: (body) => this.delegatedWrite("/api/connect/search/proof-objects", body),
|
|
827
|
+
};
|
|
828
|
+
notifications = {
|
|
829
|
+
send: (body, options = {}) => this.delegatedWrite("/api/connect/notifications/send", body, options),
|
|
830
|
+
subscribe: (body, options = {}) => this.delegatedWrite("/api/connect/notifications/subscribe", body, options),
|
|
831
|
+
templates: (query = {}) => this.delegated(appendQuery("/api/connect/notifications/templates", query)),
|
|
832
|
+
};
|
|
833
|
+
releases = {
|
|
834
|
+
pin: (body, options = {}) => this.delegatedWrite("/api/connect/releases/pin", body, options),
|
|
835
|
+
check: (query = {}) => this.delegated(appendQuery("/api/connect/releases/check", query)),
|
|
836
|
+
supported: () => this.request("/api/releases/rails/supported"),
|
|
837
|
+
};
|
|
495
838
|
webhooks = {
|
|
496
839
|
sign: createReceizWebhookSignature,
|
|
840
|
+
verify: verifyReceizWebhookRequest,
|
|
497
841
|
verifySignature: verifyReceizWebhookSignature,
|
|
498
842
|
signaturePayload: buildReceizWebhookSignaturePayload,
|
|
499
843
|
assertEvent: assertReceizWebhookEvent,
|
|
@@ -513,6 +857,33 @@ export class ReceizClient {
|
|
|
513
857
|
assetManifest: projectReceizAssetManifest,
|
|
514
858
|
sportsCardManifest: projectReceizSportsCardManifest,
|
|
515
859
|
};
|
|
860
|
+
sandbox = {
|
|
861
|
+
seedStore: (options) => {
|
|
862
|
+
const tenantHost = normalizeReceizHost(options.tenantHost);
|
|
863
|
+
return {
|
|
864
|
+
ok: true,
|
|
865
|
+
schema: "receiz.sdk.sandbox.store.v1",
|
|
866
|
+
tenantHost,
|
|
867
|
+
state: {
|
|
868
|
+
brand: tenantHost,
|
|
869
|
+
products: sandboxProducts(options.products ?? 3),
|
|
870
|
+
rewards: [{ id: "sandbox_reward_1", title: "Sandbox reward", type: "credit" }],
|
|
871
|
+
},
|
|
872
|
+
};
|
|
873
|
+
},
|
|
874
|
+
wallet: (options = {}) => ({
|
|
875
|
+
ok: true,
|
|
876
|
+
userId: "sandbox_customer",
|
|
877
|
+
balanceUsdCents: options.balanceUsdCents ?? "10000",
|
|
878
|
+
balancePhiMicro: options.balancePhiMicro ?? "100000000",
|
|
879
|
+
}),
|
|
880
|
+
checkout: (options) => ({
|
|
881
|
+
ok: options.outcome !== "failure",
|
|
882
|
+
checkoutSessionId: "sandbox_checkout",
|
|
883
|
+
status: options.outcome === "failure" ? "failed" : "succeeded",
|
|
884
|
+
amountUsd: options.amountUsd,
|
|
885
|
+
}),
|
|
886
|
+
};
|
|
516
887
|
proofMemory = {
|
|
517
888
|
createRegister: createReceizProofRegister,
|
|
518
889
|
createMemory: createReceizProofMemory,
|
|
@@ -521,6 +892,12 @@ export class ReceizClient {
|
|
|
521
892
|
assertSnapshot: assertReceizProofRegisterSnapshot,
|
|
522
893
|
additionsQuery: receizProofMemoryAdditionsQuery,
|
|
523
894
|
};
|
|
895
|
+
offline = {
|
|
896
|
+
createQueue: createReceizOfflineProofQueue,
|
|
897
|
+
createInMemoryStorage: createReceizInMemoryOfflineProofQueueStorage,
|
|
898
|
+
createLocalStorage: createReceizLocalStorageOfflineProofQueueStorage,
|
|
899
|
+
assertSnapshot: assertReceizOfflineProofQueueSnapshot,
|
|
900
|
+
};
|
|
524
901
|
constructor(options = {}) {
|
|
525
902
|
this.baseUrl = trimTrailingSlash(options.baseUrl ?? RECEIZ_DEFAULT_BASE_URL);
|
|
526
903
|
this.accessToken = options.accessToken;
|
|
@@ -572,22 +949,131 @@ export class ReceizClient {
|
|
|
572
949
|
async readPublicProofByUrl(url) {
|
|
573
950
|
return this.request(appendQuery("/api/public-proof/by-url", { url }));
|
|
574
951
|
}
|
|
952
|
+
async inspectCapabilities(options = {}) {
|
|
953
|
+
const tenantHost = options.tenantHost ? normalizeReceizHost(options.tenantHost) : undefined;
|
|
954
|
+
const scopes = options.scopes ?? [];
|
|
955
|
+
const delegatedAvailable = Boolean(this.accessToken);
|
|
956
|
+
const configured = (available, reason) => ({
|
|
957
|
+
available,
|
|
958
|
+
status: available ? "available" : "missing",
|
|
959
|
+
...(reason ? { reason } : {}),
|
|
960
|
+
...(!available ? { fixUrl: `${this.baseUrl}/developers` } : {}),
|
|
961
|
+
});
|
|
962
|
+
return {
|
|
963
|
+
ok: true,
|
|
964
|
+
schema: "receiz.sdk.capabilities.v1",
|
|
965
|
+
baseUrl: this.baseUrl,
|
|
966
|
+
...(tenantHost ? { tenantHost } : {}),
|
|
967
|
+
scopes,
|
|
968
|
+
capabilities: {
|
|
969
|
+
identity: configured(true),
|
|
970
|
+
wallet: configured(delegatedAvailable, "Connect bearer token required for wallet reads."),
|
|
971
|
+
payments: configured(delegatedAvailable, "Connect bearer token required for checkout/session routes."),
|
|
972
|
+
proofStore: configured(true),
|
|
973
|
+
media: configured(delegatedAvailable, "Connect bearer token required for media uploads."),
|
|
974
|
+
domains: configured(Boolean(tenantHost), "tenantHost is required for tenant domain helpers."),
|
|
975
|
+
twin: configured(delegatedAvailable, "Connect bearer token required for Twin routes."),
|
|
976
|
+
commerce: configured(delegatedAvailable && Boolean(tenantHost), "Connect bearer token and tenantHost are required for commerce helpers."),
|
|
977
|
+
rewards: configured(delegatedAvailable && Boolean(tenantHost), "Connect bearer token and tenantHost are required for reward routes."),
|
|
978
|
+
events: configured(delegatedAvailable, "Connect bearer token required for event subscriptions."),
|
|
979
|
+
search: configured(delegatedAvailable, "Connect bearer token required for private proof search."),
|
|
980
|
+
permissions: configured(delegatedAvailable && Boolean(tenantHost), "Connect bearer token and tenantHost are required for tenant permissions."),
|
|
981
|
+
jobs: configured(delegatedAvailable, "Connect bearer token required for durable jobs."),
|
|
982
|
+
audit: configured(delegatedAvailable && Boolean(tenantHost), "Connect bearer token and tenantHost are required for sealed tenant audit events."),
|
|
983
|
+
risk: configured(delegatedAvailable && Boolean(tenantHost), "Connect bearer token and tenantHost are required for risk signals."),
|
|
984
|
+
compliance: configured(delegatedAvailable && Boolean(tenantHost), "Connect bearer token and tenantHost are required for compliance exports."),
|
|
985
|
+
portability: configured(delegatedAvailable && Boolean(tenantHost), "Connect bearer token and tenantHost are required for store portability."),
|
|
986
|
+
notifications: configured(delegatedAvailable && Boolean(tenantHost), "Connect bearer token and tenantHost are required for notifications."),
|
|
987
|
+
offline: configured(true),
|
|
988
|
+
releases: configured(delegatedAvailable && Boolean(tenantHost), "Connect bearer token and tenantHost are required for release rail pinning."),
|
|
989
|
+
},
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
async runDoctor(options = {}) {
|
|
993
|
+
const capabilities = await this.inspectCapabilities(options);
|
|
994
|
+
const missing = [];
|
|
995
|
+
const warnings = [];
|
|
996
|
+
const fixes = [];
|
|
997
|
+
if (!this.accessToken) {
|
|
998
|
+
missing.push({
|
|
999
|
+
code: "missing_access_token",
|
|
1000
|
+
message: "Create the Receiz client with a delegated Connect/OIDC bearer token before using wallet, media, commerce, events, domains, or jobs.",
|
|
1001
|
+
fixUrl: `${this.baseUrl}/developers/connect`,
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
if (!options.tenantHost) {
|
|
1005
|
+
warnings.push({
|
|
1006
|
+
code: "missing_tenant_host",
|
|
1007
|
+
message: "Pass tenantHost so app-state, commerce, domain, permission, and customer-session helpers can scope correctly.",
|
|
1008
|
+
fixUrl: `${this.baseUrl}/developers/app-state`,
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
if (!options.callbackUrl) {
|
|
1012
|
+
warnings.push({
|
|
1013
|
+
code: "missing_callback_url",
|
|
1014
|
+
message: "Pass callbackUrl to verify your Connect/OIDC redirect configuration before users hit the authorize screen.",
|
|
1015
|
+
fixUrl: `${this.baseUrl}/developers/connect`,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
const scopes = new Set(options.scopes ?? []);
|
|
1019
|
+
for (const requiredScope of ["openid", "profile", "receiz:record"]) {
|
|
1020
|
+
if (!scopes.has(requiredScope)) {
|
|
1021
|
+
warnings.push({
|
|
1022
|
+
code: "missing_scope",
|
|
1023
|
+
message: `Recommended scope missing: ${requiredScope}.`,
|
|
1024
|
+
fixUrl: `${this.baseUrl}/developers/apps`,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
fixes.push(...missing, ...warnings);
|
|
1029
|
+
return {
|
|
1030
|
+
ok: missing.length === 0,
|
|
1031
|
+
schema: "receiz.sdk.doctor.v1",
|
|
1032
|
+
baseUrl: this.baseUrl,
|
|
1033
|
+
...(capabilities.tenantHost ? { tenantHost: capabilities.tenantHost } : {}),
|
|
1034
|
+
capabilities: capabilities.capabilities,
|
|
1035
|
+
missing,
|
|
1036
|
+
warnings,
|
|
1037
|
+
fixes,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
575
1040
|
async publishAppState(feed, options = {}) {
|
|
576
|
-
const requestOptions = {
|
|
577
|
-
|
|
578
|
-
|
|
1041
|
+
const requestOptions = {
|
|
1042
|
+
method: "POST",
|
|
1043
|
+
body: feed,
|
|
1044
|
+
headers: headersWithIdempotency(undefined, options.idempotencyKey),
|
|
1045
|
+
};
|
|
1046
|
+
if (options.signature) {
|
|
1047
|
+
const headers = headersWithIdempotency(requestOptions.headers, options.idempotencyKey);
|
|
1048
|
+
headers.set("x-public-proof-feed-signature", options.signature);
|
|
1049
|
+
requestOptions.headers = headers;
|
|
1050
|
+
}
|
|
579
1051
|
return this.request("/api/public-proof/registry/feed", requestOptions);
|
|
580
1052
|
}
|
|
1053
|
+
async publishAppStateInput(input, options = {}) {
|
|
1054
|
+
if (isReceizAppStateFeed(input)) {
|
|
1055
|
+
return this.publishAppState(input, options);
|
|
1056
|
+
}
|
|
1057
|
+
const record = createReceizTenantAppStateRecord(input);
|
|
1058
|
+
const publishOptions = {};
|
|
1059
|
+
if (options.signature)
|
|
1060
|
+
publishOptions.signature = options.signature;
|
|
1061
|
+
const idempotencyKey = options.idempotencyKey ?? input.idempotencyKey;
|
|
1062
|
+
if (idempotencyKey)
|
|
1063
|
+
publishOptions.idempotencyKey = idempotencyKey;
|
|
1064
|
+
const feedOptions = { schema: RECEIZ_PUBLIC_STORE_STATE_FEED_SCHEMA };
|
|
1065
|
+
if (record.namespace)
|
|
1066
|
+
feedOptions.namespace = record.namespace;
|
|
1067
|
+
if (record.externalCreatorId)
|
|
1068
|
+
feedOptions.externalCreatorId = record.externalCreatorId;
|
|
1069
|
+
return this.publishAppState(createReceizAppStateFeed([record], feedOptions), publishOptions);
|
|
1070
|
+
}
|
|
581
1071
|
async publishAppStateRecord(record, options = {}) {
|
|
582
|
-
const
|
|
583
|
-
const feed = {
|
|
1072
|
+
const feed = createReceizAppStateFeed([record], {
|
|
584
1073
|
schema: options.feedSchema ?? RECEIZ_APP_STATE_FEED_SCHEMA,
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
feed.namespace = options.namespace;
|
|
589
|
-
if (options.externalCreatorId)
|
|
590
|
-
feed.externalCreatorId = options.externalCreatorId;
|
|
1074
|
+
...(options.namespace ? { namespace: options.namespace } : {}),
|
|
1075
|
+
...(options.externalCreatorId ? { externalCreatorId: options.externalCreatorId } : {}),
|
|
1076
|
+
});
|
|
591
1077
|
return this.publishAppState(feed, options.signature ? { signature: options.signature } : {});
|
|
592
1078
|
}
|
|
593
1079
|
async readAppStateByUrl(url) {
|
|
@@ -605,6 +1091,156 @@ export class ReceizClient {
|
|
|
605
1091
|
async readAppStateById(id) {
|
|
606
1092
|
return this.request(`/api/public-proof/${encodePathSegment(id)}`);
|
|
607
1093
|
}
|
|
1094
|
+
async restoreAppStateByUrl(url, options = {}) {
|
|
1095
|
+
const response = await this.readAppStateByUrl(url);
|
|
1096
|
+
return restoreReceizAppStateProjection(response, options);
|
|
1097
|
+
}
|
|
1098
|
+
async restoreAppStateByHost(host, options = {}) {
|
|
1099
|
+
return this.restoreAppStateByUrl(receizAppStateUrlFromHost(host), options);
|
|
1100
|
+
}
|
|
1101
|
+
async restoreAppStateById(id, options = {}) {
|
|
1102
|
+
try {
|
|
1103
|
+
const response = await this.readAppStateById(id);
|
|
1104
|
+
return restoreReceizAppStateProjection(response, options);
|
|
1105
|
+
}
|
|
1106
|
+
catch (error) {
|
|
1107
|
+
if (error instanceof ReceizHttpError && error.status === 404) {
|
|
1108
|
+
return {
|
|
1109
|
+
ok: false,
|
|
1110
|
+
record: null,
|
|
1111
|
+
data: null,
|
|
1112
|
+
error: "app_state_not_found",
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
throw error;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
async resolveAppState(input) {
|
|
1119
|
+
const options = {
|
|
1120
|
+
...(input.schema ? { schema: input.schema } : {}),
|
|
1121
|
+
...(input.state ? { state: input.state } : {}),
|
|
1122
|
+
...(input.requiredDataKey ? { requiredDataKey: input.requiredDataKey } : {}),
|
|
1123
|
+
};
|
|
1124
|
+
if (input.id)
|
|
1125
|
+
return this.restoreAppStateById(input.id, options);
|
|
1126
|
+
if (input.url)
|
|
1127
|
+
return this.restoreAppStateByUrl(input.url, options);
|
|
1128
|
+
if (input.host || input.tenantHost)
|
|
1129
|
+
return this.restoreAppStateByHost(input.host ?? input.tenantHost ?? "", options);
|
|
1130
|
+
if (input.namespace) {
|
|
1131
|
+
const response = await this.readAppStateByNamespace(input.namespace);
|
|
1132
|
+
const records = response.records;
|
|
1133
|
+
return { ok: response.ok, record: records[0] ?? null, data: null, records };
|
|
1134
|
+
}
|
|
1135
|
+
const creator = input.creatorReceizId ?? input.receizId;
|
|
1136
|
+
if (creator) {
|
|
1137
|
+
const response = await this.readAppStateByCreator(creator);
|
|
1138
|
+
const records = response.records;
|
|
1139
|
+
return { ok: response.ok, record: records[0] ?? null, data: null, records };
|
|
1140
|
+
}
|
|
1141
|
+
throw new ReceizValidationError("receiz.appState.resolve", ["id, url, host, tenantHost, namespace, creatorReceizId, or receizId is required"]);
|
|
1142
|
+
}
|
|
1143
|
+
async oneClickCheckout(body) {
|
|
1144
|
+
const tenantHost = normalizeReceizHost(body.tenantHost);
|
|
1145
|
+
const totalUsdCents = usdToCents(body.amountUsd);
|
|
1146
|
+
const walletFirst = body.walletFirst !== false;
|
|
1147
|
+
const cardFallback = body.cardFallback !== false;
|
|
1148
|
+
const wallet = walletFirst ? await this.delegated("/api/connect/wallet/me") : null;
|
|
1149
|
+
const walletAvailable = walletFirst ? stringToCents(wallet?.balanceUsdCents) : 0n;
|
|
1150
|
+
const walletApplied = walletFirst ? (walletAvailable > totalUsdCents ? totalUsdCents : walletAvailable) : 0n;
|
|
1151
|
+
const cardDelta = totalUsdCents - walletApplied;
|
|
1152
|
+
let checkoutSession = null;
|
|
1153
|
+
if (cardDelta > 0n) {
|
|
1154
|
+
if (!cardFallback) {
|
|
1155
|
+
const response = {
|
|
1156
|
+
ok: false,
|
|
1157
|
+
tenantHost,
|
|
1158
|
+
funding: {
|
|
1159
|
+
totalUsdCents: String(totalUsdCents),
|
|
1160
|
+
walletAppliedUsdCents: String(walletApplied),
|
|
1161
|
+
cardDeltaUsdCents: String(cardDelta),
|
|
1162
|
+
walletFirst,
|
|
1163
|
+
cardFallback,
|
|
1164
|
+
},
|
|
1165
|
+
wallet,
|
|
1166
|
+
checkoutSession: null,
|
|
1167
|
+
proofObject: null,
|
|
1168
|
+
events: [{ type: "checkout.card_delta_required", cardDeltaUsdCents: String(cardDelta) }],
|
|
1169
|
+
error: "card_delta_required",
|
|
1170
|
+
};
|
|
1171
|
+
if (body.orderId)
|
|
1172
|
+
response.orderId = body.orderId;
|
|
1173
|
+
return response;
|
|
1174
|
+
}
|
|
1175
|
+
const checkoutBody = {
|
|
1176
|
+
amountUsd: centsToUsd(cardDelta),
|
|
1177
|
+
currency: body.currency ?? "usd",
|
|
1178
|
+
uiMode: "embedded",
|
|
1179
|
+
description: `Receiz one-click checkout for ${tenantHost}`,
|
|
1180
|
+
tenantHost,
|
|
1181
|
+
};
|
|
1182
|
+
if (body.orderId)
|
|
1183
|
+
checkoutBody.referenceId = body.orderId;
|
|
1184
|
+
if (body.customerEmail)
|
|
1185
|
+
checkoutBody.customerEmail = body.customerEmail;
|
|
1186
|
+
if (body.successUrl)
|
|
1187
|
+
checkoutBody.successUrl = body.successUrl;
|
|
1188
|
+
if (body.cancelUrl)
|
|
1189
|
+
checkoutBody.cancelUrl = body.cancelUrl;
|
|
1190
|
+
if (body.cart)
|
|
1191
|
+
checkoutBody.cart = body.cart;
|
|
1192
|
+
checkoutSession = await this.delegated("/api/connect/payments/checkout", {
|
|
1193
|
+
method: "POST",
|
|
1194
|
+
body: bodyWithIdempotency(checkoutBody, body.idempotencyKey),
|
|
1195
|
+
headers: headersWithIdempotency(undefined, body.idempotencyKey),
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
const response = {
|
|
1199
|
+
ok: true,
|
|
1200
|
+
tenantHost,
|
|
1201
|
+
funding: {
|
|
1202
|
+
totalUsdCents: String(totalUsdCents),
|
|
1203
|
+
walletAppliedUsdCents: String(walletApplied),
|
|
1204
|
+
cardDeltaUsdCents: String(cardDelta),
|
|
1205
|
+
walletFirst,
|
|
1206
|
+
cardFallback,
|
|
1207
|
+
},
|
|
1208
|
+
wallet,
|
|
1209
|
+
checkoutSession,
|
|
1210
|
+
proofObject: checkoutSession?.proofBundle ?? null,
|
|
1211
|
+
events: [
|
|
1212
|
+
{ type: "wallet.summary.read", walletAppliedUsdCents: String(walletApplied) },
|
|
1213
|
+
...(checkoutSession ? [{ type: "checkout.session.created", checkoutSessionId: checkoutSession.checkoutSessionId ?? null }] : []),
|
|
1214
|
+
],
|
|
1215
|
+
};
|
|
1216
|
+
if (body.orderId)
|
|
1217
|
+
response.orderId = body.orderId;
|
|
1218
|
+
return response;
|
|
1219
|
+
}
|
|
1220
|
+
async uploadMedia(file, options = {}) {
|
|
1221
|
+
const form = new FormData();
|
|
1222
|
+
form.set("file", file, options.filename ?? "receiz-media");
|
|
1223
|
+
if (options.tenantHost)
|
|
1224
|
+
form.set("tenantHost", normalizeReceizHost(options.tenantHost));
|
|
1225
|
+
if (options.purpose)
|
|
1226
|
+
form.set("purpose", options.purpose);
|
|
1227
|
+
if (options.metadata)
|
|
1228
|
+
form.set("metadata", JSON.stringify(options.metadata));
|
|
1229
|
+
if (options.idempotencyKey)
|
|
1230
|
+
form.set("idempotencyKey", options.idempotencyKey);
|
|
1231
|
+
return this.delegated("/api/connect/media/upload", {
|
|
1232
|
+
method: "POST",
|
|
1233
|
+
body: form,
|
|
1234
|
+
headers: headersWithIdempotency(undefined, options.idempotencyKey),
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
async eventSubscribe(body) {
|
|
1238
|
+
return this.delegated("/api/connect/events/subscribe", {
|
|
1239
|
+
method: "POST",
|
|
1240
|
+
body: bodyWithIdempotency(body, body.idempotencyKey),
|
|
1241
|
+
headers: headersWithIdempotency(undefined, body.idempotencyKey),
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
608
1244
|
async sportsConformance() {
|
|
609
1245
|
return this.request("/api/game/sports/conformance");
|
|
610
1246
|
}
|
|
@@ -616,6 +1252,14 @@ export class ReceizClient {
|
|
|
616
1252
|
throw new Error("receiz_access_token_required");
|
|
617
1253
|
return this.request(path, { ...options, bearerToken: this.accessToken });
|
|
618
1254
|
}
|
|
1255
|
+
async delegatedWrite(path, body = {}, options = {}) {
|
|
1256
|
+
const idempotencyKey = options.idempotencyKey ?? (typeof body.idempotencyKey === "string" ? body.idempotencyKey : undefined);
|
|
1257
|
+
return this.delegated(path, {
|
|
1258
|
+
method: "POST",
|
|
1259
|
+
body: bodyWithIdempotency(body, idempotencyKey),
|
|
1260
|
+
headers: headersWithIdempotency(undefined, idempotencyKey),
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
619
1263
|
async delegatedBlob(path, options = {}) {
|
|
620
1264
|
if (!this.accessToken)
|
|
621
1265
|
throw new Error("receiz_access_token_required");
|
|
@@ -1450,6 +2094,213 @@ export function receizProofMemoryAdditionsQuery(value, limit) {
|
|
|
1450
2094
|
...(limit === undefined ? {} : { limit }),
|
|
1451
2095
|
};
|
|
1452
2096
|
}
|
|
2097
|
+
function normalizeOfflineQueueItem(value) {
|
|
2098
|
+
const record = ensureRecord(value, "ReceizOfflineProofQueueItem");
|
|
2099
|
+
const issues = [];
|
|
2100
|
+
const id = ensureString(record, "id", issues);
|
|
2101
|
+
const kind = ensureString(record, "kind", issues);
|
|
2102
|
+
if (!isRecord(record.payload))
|
|
2103
|
+
issues.push("payload must be an object");
|
|
2104
|
+
const attempts = record.attempts;
|
|
2105
|
+
if (attempts !== undefined && (typeof attempts !== "number" || !Number.isInteger(attempts) || attempts < 0)) {
|
|
2106
|
+
issues.push("attempts must be a non-negative integer when present");
|
|
2107
|
+
}
|
|
2108
|
+
const item = {
|
|
2109
|
+
id: id ?? "",
|
|
2110
|
+
kind: (kind ?? ""),
|
|
2111
|
+
payload: isRecord(record.payload) ? record.payload : {},
|
|
2112
|
+
createdAt: typeof record.createdAt === "string" && record.createdAt.trim() ? record.createdAt : new Date().toISOString(),
|
|
2113
|
+
attempts: typeof attempts === "number" ? attempts : 0,
|
|
2114
|
+
};
|
|
2115
|
+
if (typeof record.idempotencyKey === "string" && record.idempotencyKey.trim())
|
|
2116
|
+
item.idempotencyKey = record.idempotencyKey;
|
|
2117
|
+
if (record.lastError === null || isRecord(record.lastError))
|
|
2118
|
+
item.lastError = record.lastError;
|
|
2119
|
+
failIfIssues("ReceizOfflineProofQueueItem", issues);
|
|
2120
|
+
return item;
|
|
2121
|
+
}
|
|
2122
|
+
export function assertReceizOfflineProofQueueSnapshot(value) {
|
|
2123
|
+
const record = ensureRecord(value, "ReceizOfflineProofQueueSnapshot");
|
|
2124
|
+
const issues = [];
|
|
2125
|
+
if (record.schema !== "receiz.sdk.offline_proof_queue.v1")
|
|
2126
|
+
issues.push("schema must be receiz.sdk.offline_proof_queue.v1");
|
|
2127
|
+
const ownerId = record.ownerId;
|
|
2128
|
+
if (ownerId !== null && ownerId !== undefined && typeof ownerId !== "string") {
|
|
2129
|
+
issues.push("ownerId must be a string or null when present");
|
|
2130
|
+
}
|
|
2131
|
+
const createdAt = ensureString(record, "createdAt", issues);
|
|
2132
|
+
const updatedAt = ensureString(record, "updatedAt", issues);
|
|
2133
|
+
if (!Array.isArray(record.pending))
|
|
2134
|
+
issues.push("pending must be an array");
|
|
2135
|
+
if (!Array.isArray(record.settled))
|
|
2136
|
+
issues.push("settled must be an array");
|
|
2137
|
+
if (!Array.isArray(record.failed))
|
|
2138
|
+
issues.push("failed must be an array");
|
|
2139
|
+
failIfIssues("ReceizOfflineProofQueueSnapshot", issues);
|
|
2140
|
+
return {
|
|
2141
|
+
schema: "receiz.sdk.offline_proof_queue.v1",
|
|
2142
|
+
ownerId: typeof ownerId === "string" || ownerId === null ? ownerId : null,
|
|
2143
|
+
createdAt: createdAt ?? "",
|
|
2144
|
+
updatedAt: updatedAt ?? "",
|
|
2145
|
+
pending: Array.isArray(record.pending) ? record.pending.map(normalizeOfflineQueueItem) : [],
|
|
2146
|
+
settled: Array.isArray(record.settled) ? record.settled.map(normalizeOfflineQueueItem) : [],
|
|
2147
|
+
failed: Array.isArray(record.failed) ? record.failed.map(normalizeOfflineQueueItem) : [],
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
function parseOfflineProofQueueStorageValue(value) {
|
|
2151
|
+
if (value === null || value === undefined)
|
|
2152
|
+
return null;
|
|
2153
|
+
if (typeof value === "string") {
|
|
2154
|
+
const trimmed = value.trim();
|
|
2155
|
+
if (!trimmed)
|
|
2156
|
+
return null;
|
|
2157
|
+
return assertReceizOfflineProofQueueSnapshot(JSON.parse(trimmed));
|
|
2158
|
+
}
|
|
2159
|
+
return assertReceizOfflineProofQueueSnapshot(value);
|
|
2160
|
+
}
|
|
2161
|
+
export class ReceizOfflineProofQueue {
|
|
2162
|
+
ownerId;
|
|
2163
|
+
createdAt;
|
|
2164
|
+
pendingItems;
|
|
2165
|
+
settledItems;
|
|
2166
|
+
failedItems;
|
|
2167
|
+
storage;
|
|
2168
|
+
constructor(options = {}) {
|
|
2169
|
+
const snapshot = options.snapshot ? assertReceizOfflineProofQueueSnapshot(options.snapshot) : null;
|
|
2170
|
+
this.ownerId = options.ownerId ?? snapshot?.ownerId ?? null;
|
|
2171
|
+
this.createdAt = snapshot?.createdAt ?? new Date().toISOString();
|
|
2172
|
+
this.pendingItems = snapshot?.pending ? [...snapshot.pending] : [];
|
|
2173
|
+
this.settledItems = snapshot?.settled ? [...snapshot.settled] : [];
|
|
2174
|
+
this.failedItems = snapshot?.failed ? [...snapshot.failed] : [];
|
|
2175
|
+
this.storage = options.storage ?? null;
|
|
2176
|
+
}
|
|
2177
|
+
enqueue(item) {
|
|
2178
|
+
const normalized = normalizeOfflineQueueItem(item);
|
|
2179
|
+
if (this.pendingItems.some((pending) => pending.id === normalized.id))
|
|
2180
|
+
return this;
|
|
2181
|
+
if (this.settledItems.some((settled) => settled.id === normalized.id))
|
|
2182
|
+
return this;
|
|
2183
|
+
this.pendingItems.push(normalized);
|
|
2184
|
+
return this;
|
|
2185
|
+
}
|
|
2186
|
+
snapshot() {
|
|
2187
|
+
return {
|
|
2188
|
+
schema: "receiz.sdk.offline_proof_queue.v1",
|
|
2189
|
+
ownerId: this.ownerId,
|
|
2190
|
+
createdAt: this.createdAt,
|
|
2191
|
+
updatedAt: new Date().toISOString(),
|
|
2192
|
+
pending: [...this.pendingItems],
|
|
2193
|
+
settled: [...this.settledItems],
|
|
2194
|
+
failed: [...this.failedItems],
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
async flush() {
|
|
2198
|
+
const snapshot = this.snapshot();
|
|
2199
|
+
if (this.storage)
|
|
2200
|
+
await this.storage.write(snapshot);
|
|
2201
|
+
return snapshot;
|
|
2202
|
+
}
|
|
2203
|
+
async clear() {
|
|
2204
|
+
this.pendingItems = [];
|
|
2205
|
+
this.settledItems = [];
|
|
2206
|
+
this.failedItems = [];
|
|
2207
|
+
if (this.storage?.remove)
|
|
2208
|
+
await this.storage.remove();
|
|
2209
|
+
}
|
|
2210
|
+
async replay(client) {
|
|
2211
|
+
const originalPending = [...this.pendingItems];
|
|
2212
|
+
let settled = 0;
|
|
2213
|
+
let failed = 0;
|
|
2214
|
+
this.pendingItems = [];
|
|
2215
|
+
for (const item of originalPending) {
|
|
2216
|
+
try {
|
|
2217
|
+
await replayOfflineProofQueueItem(client, item);
|
|
2218
|
+
this.settledItems.push({ ...item, attempts: (item.attempts ?? 0) + 1, lastError: null });
|
|
2219
|
+
settled += 1;
|
|
2220
|
+
}
|
|
2221
|
+
catch (error) {
|
|
2222
|
+
const failedItem = {
|
|
2223
|
+
...item,
|
|
2224
|
+
attempts: (item.attempts ?? 0) + 1,
|
|
2225
|
+
lastError: errorDetail(error),
|
|
2226
|
+
};
|
|
2227
|
+
this.failedItems.push(failedItem);
|
|
2228
|
+
failed += 1;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
await this.flush();
|
|
2232
|
+
return { ok: failed === 0, settled, failed };
|
|
2233
|
+
}
|
|
2234
|
+
toJSON() {
|
|
2235
|
+
return this.snapshot();
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
async function replayOfflineProofQueueItem(client, item) {
|
|
2239
|
+
if (item.kind === "app_state.publish") {
|
|
2240
|
+
return client.appState.publish(item.payload, item.idempotencyKey ? { idempotencyKey: item.idempotencyKey } : {});
|
|
2241
|
+
}
|
|
2242
|
+
if (item.kind === "connect.record") {
|
|
2243
|
+
return client.request("/api/connect/record", {
|
|
2244
|
+
method: "POST",
|
|
2245
|
+
body: bodyWithIdempotency(item.payload, item.idempotencyKey),
|
|
2246
|
+
headers: headersWithIdempotency(undefined, item.idempotencyKey),
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
if (item.kind === "webhook.event") {
|
|
2250
|
+
return client.request("/api/connect/webhooks/replay", {
|
|
2251
|
+
method: "POST",
|
|
2252
|
+
body: bodyWithIdempotency(item.payload, item.idempotencyKey),
|
|
2253
|
+
headers: headersWithIdempotency(undefined, item.idempotencyKey),
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
return client.request("/api/connect/offline/replay", {
|
|
2257
|
+
method: "POST",
|
|
2258
|
+
body: bodyWithIdempotency({ kind: item.kind, payload: item.payload }, item.idempotencyKey),
|
|
2259
|
+
headers: headersWithIdempotency(undefined, item.idempotencyKey),
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
export async function createReceizOfflineProofQueue(options = {}) {
|
|
2263
|
+
const storedSnapshot = options.storage ? parseOfflineProofQueueStorageValue(await options.storage.read()) : null;
|
|
2264
|
+
const queueOptions = {
|
|
2265
|
+
ownerId: options.ownerId ?? storedSnapshot?.ownerId ?? null,
|
|
2266
|
+
};
|
|
2267
|
+
if (options.storage)
|
|
2268
|
+
queueOptions.storage = options.storage;
|
|
2269
|
+
const snapshot = options.snapshot ?? storedSnapshot;
|
|
2270
|
+
if (snapshot)
|
|
2271
|
+
queueOptions.snapshot = snapshot;
|
|
2272
|
+
return new ReceizOfflineProofQueue(queueOptions);
|
|
2273
|
+
}
|
|
2274
|
+
export function createReceizInMemoryOfflineProofQueueStorage(initialValue = null) {
|
|
2275
|
+
let storedText = typeof initialValue === "string"
|
|
2276
|
+
? initialValue
|
|
2277
|
+
: initialValue
|
|
2278
|
+
? JSON.stringify(assertReceizOfflineProofQueueSnapshot(initialValue))
|
|
2279
|
+
: null;
|
|
2280
|
+
return {
|
|
2281
|
+
read: () => storedText,
|
|
2282
|
+
write: (snapshot) => {
|
|
2283
|
+
storedText = JSON.stringify(assertReceizOfflineProofQueueSnapshot(snapshot));
|
|
2284
|
+
},
|
|
2285
|
+
remove: () => {
|
|
2286
|
+
storedText = null;
|
|
2287
|
+
},
|
|
2288
|
+
readText: () => storedText,
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
export function createReceizLocalStorageOfflineProofQueueStorage(key = "receiz:offline-proof-queue:v1", storage = globalThis.localStorage) {
|
|
2292
|
+
if (!storage)
|
|
2293
|
+
throw new Error("receiz_local_storage_unavailable");
|
|
2294
|
+
return {
|
|
2295
|
+
read: () => storage.getItem(key),
|
|
2296
|
+
write: (snapshot) => {
|
|
2297
|
+
storage.setItem(key, JSON.stringify(assertReceizOfflineProofQueueSnapshot(snapshot)));
|
|
2298
|
+
},
|
|
2299
|
+
remove: () => {
|
|
2300
|
+
storage.removeItem(key);
|
|
2301
|
+
},
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
1453
2304
|
function bodyToString(body) {
|
|
1454
2305
|
if (typeof body === "string")
|
|
1455
2306
|
return body;
|