@receiz/sdk 97.0.0 → 97.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.
- package/README.md +64 -5
- package/dist/cli.js +106 -2
- package/dist/index.d.ts +322 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +520 -13
- package/docs/app-runtime-commerce-cloud.md +127 -0
- package/docs/app-state-public-projections.md +82 -7
- package/docs/cli-and-conformance.md +28 -0
- package/package.json +1 -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.1.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,180 @@ 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 usdToCents(value) {
|
|
272
|
+
const trimmed = value.trim();
|
|
273
|
+
if (!/^\d+(\.\d{1,2})?$/.test(trimmed))
|
|
274
|
+
throw new ReceizValidationError("receiz.commerce.amountUsd", ["amountUsd must be a positive USD decimal"]);
|
|
275
|
+
const [whole = "0", fraction = ""] = trimmed.split(".");
|
|
276
|
+
return BigInt(whole) * 100n + BigInt((fraction + "00").slice(0, 2));
|
|
277
|
+
}
|
|
278
|
+
function centsToUsd(value) {
|
|
279
|
+
const sign = value < 0n ? "-" : "";
|
|
280
|
+
const abs = value < 0n ? -value : value;
|
|
281
|
+
return `${sign}${abs / 100n}.${String(abs % 100n).padStart(2, "0")}`;
|
|
282
|
+
}
|
|
283
|
+
function stringToCents(value) {
|
|
284
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
285
|
+
return BigInt(Math.max(0, Math.trunc(value)));
|
|
286
|
+
if (typeof value === "bigint")
|
|
287
|
+
return value < 0n ? 0n : value;
|
|
288
|
+
if (typeof value === "string" && /^\d+$/.test(value.trim()))
|
|
289
|
+
return BigInt(value.trim());
|
|
290
|
+
return 0n;
|
|
291
|
+
}
|
|
292
|
+
function sandboxProducts(count) {
|
|
293
|
+
return Array.from({ length: Math.max(0, count) }, (_, index) => ({
|
|
294
|
+
id: `sandbox_product_${index + 1}`,
|
|
295
|
+
title: `Sandbox Product ${index + 1}`,
|
|
296
|
+
amountUsd: centsToUsd(BigInt(index + 1) * 1200n),
|
|
297
|
+
proofObject: {
|
|
298
|
+
schema: "receiz.sdk.sandbox.proof_object.v1",
|
|
299
|
+
objectId: `sandbox_product_${index + 1}`,
|
|
300
|
+
},
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
export async function verifyReceizWebhookRequest(request, options) {
|
|
304
|
+
const signature = request.headers.get(RECEIZ_WEBHOOK_SIGNATURE_HEADER) ?? "";
|
|
305
|
+
const timestamp = request.headers.get(RECEIZ_WEBHOOK_TIMESTAMP_HEADER) ?? "";
|
|
306
|
+
const body = await request.clone().text();
|
|
307
|
+
const verifyInput = {
|
|
308
|
+
secret: options.secret,
|
|
309
|
+
timestamp,
|
|
310
|
+
body,
|
|
311
|
+
signature,
|
|
312
|
+
};
|
|
313
|
+
if (options.toleranceSeconds !== undefined)
|
|
314
|
+
verifyInput.toleranceSeconds = options.toleranceSeconds;
|
|
315
|
+
if (options.nowMs !== undefined)
|
|
316
|
+
verifyInput.nowMs = options.nowMs;
|
|
317
|
+
return verifyReceizWebhookSignature(verifyInput);
|
|
318
|
+
}
|
|
319
|
+
function resolveReceizAppStateProjectionRecord(input) {
|
|
320
|
+
if (!input || !isRecord(input))
|
|
321
|
+
return null;
|
|
322
|
+
const looksLikeResponse = typeof input.ok === "boolean" &&
|
|
323
|
+
Object.prototype.hasOwnProperty.call(input, "record") &&
|
|
324
|
+
!Object.prototype.hasOwnProperty.call(input, "sourceUrl") &&
|
|
325
|
+
!Object.prototype.hasOwnProperty.call(input, "data");
|
|
326
|
+
if (looksLikeResponse) {
|
|
327
|
+
const response = input;
|
|
328
|
+
return response.record && isRecord(response.record) ? response.record : null;
|
|
329
|
+
}
|
|
330
|
+
return input;
|
|
331
|
+
}
|
|
332
|
+
function receizAppStateRestoreError(record, options) {
|
|
333
|
+
if (!record)
|
|
334
|
+
return "app_state_not_found";
|
|
335
|
+
if (options.schema && record.schema !== options.schema)
|
|
336
|
+
return "app_state_schema_mismatch";
|
|
337
|
+
if (options.state && record.state !== options.state)
|
|
338
|
+
return "app_state_state_mismatch";
|
|
339
|
+
if (!isRecord(record.data))
|
|
340
|
+
return "app_state_data_missing";
|
|
341
|
+
if (options.requiredDataKey && !Object.prototype.hasOwnProperty.call(record.data, options.requiredDataKey)) {
|
|
342
|
+
return "app_state_required_data_missing";
|
|
343
|
+
}
|
|
344
|
+
return "app_state_data_unavailable";
|
|
345
|
+
}
|
|
346
|
+
export function extractReceizAppStateData(input, options = {}) {
|
|
347
|
+
const record = resolveReceizAppStateProjectionRecord(input);
|
|
348
|
+
if (!record)
|
|
349
|
+
return null;
|
|
350
|
+
if (options.schema && record.schema !== options.schema)
|
|
351
|
+
return null;
|
|
352
|
+
if (options.state && record.state !== options.state)
|
|
353
|
+
return null;
|
|
354
|
+
if (!isRecord(record.data))
|
|
355
|
+
return null;
|
|
356
|
+
if (options.requiredDataKey && !Object.prototype.hasOwnProperty.call(record.data, options.requiredDataKey))
|
|
357
|
+
return null;
|
|
358
|
+
return record.data;
|
|
359
|
+
}
|
|
360
|
+
export function restoreReceizAppStateProjection(input, options = {}) {
|
|
361
|
+
const record = resolveReceizAppStateProjectionRecord(input);
|
|
362
|
+
const data = extractReceizAppStateData(input, options);
|
|
363
|
+
if (!record || !data) {
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
record,
|
|
367
|
+
data: null,
|
|
368
|
+
error: receizAppStateRestoreError(record, options),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
ok: true,
|
|
373
|
+
record,
|
|
374
|
+
data,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
203
377
|
function isRecord(value) {
|
|
204
378
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
205
379
|
}
|
|
@@ -367,6 +541,8 @@ export class ReceizClient {
|
|
|
367
541
|
baseUrl;
|
|
368
542
|
accessToken;
|
|
369
543
|
fetchImpl;
|
|
544
|
+
doctor = (options = {}) => this.runDoctor(options);
|
|
545
|
+
capabilities = (options = {}) => this.inspectCapabilities(options);
|
|
370
546
|
verification = {
|
|
371
547
|
verifyArtifact: (file) => this.verifyArtifact(file),
|
|
372
548
|
sealArtifact: (file, options) => this.sealArtifact(file, options),
|
|
@@ -380,13 +556,26 @@ export class ReceizClient {
|
|
|
380
556
|
registryFeed: (feed, options = {}) => this.publishAppState(feed, options),
|
|
381
557
|
};
|
|
382
558
|
appState = {
|
|
383
|
-
publish: (
|
|
559
|
+
publish: (input, options = {}) => this.publishAppStateInput(input, options),
|
|
384
560
|
publishRecord: (record, options = {}) => this.publishAppStateRecord(record, options),
|
|
561
|
+
resolve: (input) => this.resolveAppState(input),
|
|
385
562
|
byUrl: (url) => this.readAppStateByUrl(url),
|
|
386
563
|
byHost: (host) => this.readAppStateByHost(host),
|
|
387
564
|
byCreator: (receizId) => this.readAppStateByCreator(receizId),
|
|
388
565
|
byNamespace: (namespace) => this.readAppStateByNamespace(namespace),
|
|
389
566
|
byId: (id) => this.readAppStateById(id),
|
|
567
|
+
restoreByUrl: (url, options = {}) => this.restoreAppStateByUrl(url, options),
|
|
568
|
+
restoreByHost: (host, options = {}) => this.restoreAppStateByHost(host, options),
|
|
569
|
+
restoreById: (id, options = {}) => this.restoreAppStateById(id, options),
|
|
570
|
+
createRecord: createReceizAppStateRecord,
|
|
571
|
+
createTenantRecord: createReceizTenantAppStateRecord,
|
|
572
|
+
createFeed: createReceizAppStateFeed,
|
|
573
|
+
createPublicStoreRecord: createReceizPublicStoreStateRecord,
|
|
574
|
+
urlFromHost: receizAppStateUrlFromHost,
|
|
575
|
+
namespaceFromUrl: receizAppStateNamespaceFromUrl,
|
|
576
|
+
objectIdFromUrl: receizAppStateObjectIdFromUrl,
|
|
577
|
+
extractData: extractReceizAppStateData,
|
|
578
|
+
restoreProjection: restoreReceizAppStateProjection,
|
|
390
579
|
};
|
|
391
580
|
sports = {
|
|
392
581
|
conformance: () => this.sportsConformance(),
|
|
@@ -488,12 +677,51 @@ export class ReceizClient {
|
|
|
488
677
|
bootstrap: (username) => this.request(`/api/connect/login/bootstrap/${encodePathSegment(username)}`),
|
|
489
678
|
authorizeUrl: (options) => this.authorizeUrl(options),
|
|
490
679
|
};
|
|
680
|
+
commerce = {
|
|
681
|
+
oneClickCheckout: (body) => this.oneClickCheckout(body),
|
|
682
|
+
};
|
|
491
683
|
payments = {
|
|
492
684
|
embeddedCheckout: (body) => this.request("/api/payments/embed/checkout", { method: "POST", body }),
|
|
493
685
|
embeddedNoteClaim: (body) => this.request("/api/payments/embed/note-claim", { method: "POST", body }),
|
|
494
686
|
};
|
|
687
|
+
media = {
|
|
688
|
+
upload: (file, options = {}) => this.uploadMedia(file, options),
|
|
689
|
+
objectUrl: (query) => appendQuery(`${this.baseUrl}/api/storage/object`, query),
|
|
690
|
+
};
|
|
691
|
+
domains = {
|
|
692
|
+
normalizeHost: normalizeReceizHost,
|
|
693
|
+
tenantUrl: receizAppStateUrlFromHost,
|
|
694
|
+
namespace: receizAppStateNamespaceFromUrl,
|
|
695
|
+
objectId: receizAppStateObjectIdFromUrl,
|
|
696
|
+
resolveTenant: (host, options = {}) => this.restoreAppStateByHost(host, options),
|
|
697
|
+
reserveSubdomain: (body, options = {}) => this.delegated("/api/connect/domains/subdomains/reserve", {
|
|
698
|
+
method: "POST",
|
|
699
|
+
body: bodyWithIdempotency(body, options.idempotencyKey),
|
|
700
|
+
headers: headersWithIdempotency(undefined, options.idempotencyKey),
|
|
701
|
+
}),
|
|
702
|
+
verifyCustomDomain: (body, options = {}) => this.delegated("/api/connect/domains/custom/verify", {
|
|
703
|
+
method: "POST",
|
|
704
|
+
body: bodyWithIdempotency(body, options.idempotencyKey),
|
|
705
|
+
headers: headersWithIdempotency(undefined, options.idempotencyKey),
|
|
706
|
+
}),
|
|
707
|
+
status: (query = {}) => this.delegated(appendQuery("/api/connect/domains/status", query)),
|
|
708
|
+
};
|
|
709
|
+
events = {
|
|
710
|
+
subscribe: (body) => this.eventSubscribe(body),
|
|
711
|
+
};
|
|
712
|
+
proof = {
|
|
713
|
+
query: (query) => this.delegated("/api/connect/proof/query", { method: "POST", body: query }),
|
|
714
|
+
};
|
|
715
|
+
jobs = {
|
|
716
|
+
enqueue: (body) => this.delegated("/api/connect/jobs", {
|
|
717
|
+
method: "POST",
|
|
718
|
+
body: bodyWithIdempotency(body, body.idempotencyKey),
|
|
719
|
+
headers: headersWithIdempotency(undefined, body.idempotencyKey),
|
|
720
|
+
}),
|
|
721
|
+
};
|
|
495
722
|
webhooks = {
|
|
496
723
|
sign: createReceizWebhookSignature,
|
|
724
|
+
verify: verifyReceizWebhookRequest,
|
|
497
725
|
verifySignature: verifyReceizWebhookSignature,
|
|
498
726
|
signaturePayload: buildReceizWebhookSignaturePayload,
|
|
499
727
|
assertEvent: assertReceizWebhookEvent,
|
|
@@ -513,6 +741,33 @@ export class ReceizClient {
|
|
|
513
741
|
assetManifest: projectReceizAssetManifest,
|
|
514
742
|
sportsCardManifest: projectReceizSportsCardManifest,
|
|
515
743
|
};
|
|
744
|
+
sandbox = {
|
|
745
|
+
seedStore: (options) => {
|
|
746
|
+
const tenantHost = normalizeReceizHost(options.tenantHost);
|
|
747
|
+
return {
|
|
748
|
+
ok: true,
|
|
749
|
+
schema: "receiz.sdk.sandbox.store.v1",
|
|
750
|
+
tenantHost,
|
|
751
|
+
state: {
|
|
752
|
+
brand: tenantHost,
|
|
753
|
+
products: sandboxProducts(options.products ?? 3),
|
|
754
|
+
rewards: [{ id: "sandbox_reward_1", title: "Sandbox reward", type: "credit" }],
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
},
|
|
758
|
+
wallet: (options = {}) => ({
|
|
759
|
+
ok: true,
|
|
760
|
+
userId: "sandbox_customer",
|
|
761
|
+
balanceUsdCents: options.balanceUsdCents ?? "10000",
|
|
762
|
+
balancePhiMicro: options.balancePhiMicro ?? "100000000",
|
|
763
|
+
}),
|
|
764
|
+
checkout: (options) => ({
|
|
765
|
+
ok: options.outcome !== "failure",
|
|
766
|
+
checkoutSessionId: "sandbox_checkout",
|
|
767
|
+
status: options.outcome === "failure" ? "failed" : "succeeded",
|
|
768
|
+
amountUsd: options.amountUsd,
|
|
769
|
+
}),
|
|
770
|
+
};
|
|
516
771
|
proofMemory = {
|
|
517
772
|
createRegister: createReceizProofRegister,
|
|
518
773
|
createMemory: createReceizProofMemory,
|
|
@@ -572,22 +827,124 @@ export class ReceizClient {
|
|
|
572
827
|
async readPublicProofByUrl(url) {
|
|
573
828
|
return this.request(appendQuery("/api/public-proof/by-url", { url }));
|
|
574
829
|
}
|
|
830
|
+
async inspectCapabilities(options = {}) {
|
|
831
|
+
const tenantHost = options.tenantHost ? normalizeReceizHost(options.tenantHost) : undefined;
|
|
832
|
+
const scopes = options.scopes ?? [];
|
|
833
|
+
const delegatedAvailable = Boolean(this.accessToken);
|
|
834
|
+
const configured = (available, reason) => ({
|
|
835
|
+
available,
|
|
836
|
+
status: available ? "available" : "missing",
|
|
837
|
+
...(reason ? { reason } : {}),
|
|
838
|
+
...(!available ? { fixUrl: `${this.baseUrl}/developers` } : {}),
|
|
839
|
+
});
|
|
840
|
+
return {
|
|
841
|
+
ok: true,
|
|
842
|
+
schema: "receiz.sdk.capabilities.v1",
|
|
843
|
+
baseUrl: this.baseUrl,
|
|
844
|
+
...(tenantHost ? { tenantHost } : {}),
|
|
845
|
+
scopes,
|
|
846
|
+
capabilities: {
|
|
847
|
+
identity: configured(true),
|
|
848
|
+
wallet: configured(delegatedAvailable, "Connect bearer token required for wallet reads."),
|
|
849
|
+
payments: configured(delegatedAvailable, "Connect bearer token required for checkout/session routes."),
|
|
850
|
+
proofStore: configured(true),
|
|
851
|
+
media: configured(delegatedAvailable, "Connect bearer token required for media uploads."),
|
|
852
|
+
domains: configured(Boolean(tenantHost), "tenantHost is required for tenant domain helpers."),
|
|
853
|
+
twin: configured(delegatedAvailable, "Connect bearer token required for Twin routes."),
|
|
854
|
+
commerce: configured(delegatedAvailable && Boolean(tenantHost), "Connect bearer token and tenantHost are required for commerce helpers."),
|
|
855
|
+
rewards: configured(delegatedAvailable && Boolean(tenantHost), "Connect bearer token and tenantHost are required for reward routes."),
|
|
856
|
+
events: configured(delegatedAvailable, "Connect bearer token required for event subscriptions."),
|
|
857
|
+
search: configured(delegatedAvailable, "Connect bearer token required for private proof search."),
|
|
858
|
+
permissions: configured(delegatedAvailable && Boolean(tenantHost), "Connect bearer token and tenantHost are required for tenant permissions."),
|
|
859
|
+
jobs: configured(delegatedAvailable, "Connect bearer token required for durable jobs."),
|
|
860
|
+
},
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
async runDoctor(options = {}) {
|
|
864
|
+
const capabilities = await this.inspectCapabilities(options);
|
|
865
|
+
const missing = [];
|
|
866
|
+
const warnings = [];
|
|
867
|
+
const fixes = [];
|
|
868
|
+
if (!this.accessToken) {
|
|
869
|
+
missing.push({
|
|
870
|
+
code: "missing_access_token",
|
|
871
|
+
message: "Create the Receiz client with a delegated Connect/OIDC bearer token before using wallet, media, commerce, events, domains, or jobs.",
|
|
872
|
+
fixUrl: `${this.baseUrl}/developers/connect`,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
if (!options.tenantHost) {
|
|
876
|
+
warnings.push({
|
|
877
|
+
code: "missing_tenant_host",
|
|
878
|
+
message: "Pass tenantHost so app-state, commerce, domain, permission, and customer-session helpers can scope correctly.",
|
|
879
|
+
fixUrl: `${this.baseUrl}/developers/app-state`,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
if (!options.callbackUrl) {
|
|
883
|
+
warnings.push({
|
|
884
|
+
code: "missing_callback_url",
|
|
885
|
+
message: "Pass callbackUrl to verify your Connect/OIDC redirect configuration before users hit the authorize screen.",
|
|
886
|
+
fixUrl: `${this.baseUrl}/developers/connect`,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
const scopes = new Set(options.scopes ?? []);
|
|
890
|
+
for (const requiredScope of ["openid", "profile", "receiz:record"]) {
|
|
891
|
+
if (!scopes.has(requiredScope)) {
|
|
892
|
+
warnings.push({
|
|
893
|
+
code: "missing_scope",
|
|
894
|
+
message: `Recommended scope missing: ${requiredScope}.`,
|
|
895
|
+
fixUrl: `${this.baseUrl}/developers/apps`,
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
fixes.push(...missing, ...warnings);
|
|
900
|
+
return {
|
|
901
|
+
ok: missing.length === 0,
|
|
902
|
+
schema: "receiz.sdk.doctor.v1",
|
|
903
|
+
baseUrl: this.baseUrl,
|
|
904
|
+
...(capabilities.tenantHost ? { tenantHost: capabilities.tenantHost } : {}),
|
|
905
|
+
capabilities: capabilities.capabilities,
|
|
906
|
+
missing,
|
|
907
|
+
warnings,
|
|
908
|
+
fixes,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
575
911
|
async publishAppState(feed, options = {}) {
|
|
576
|
-
const requestOptions = {
|
|
577
|
-
|
|
578
|
-
|
|
912
|
+
const requestOptions = {
|
|
913
|
+
method: "POST",
|
|
914
|
+
body: feed,
|
|
915
|
+
headers: headersWithIdempotency(undefined, options.idempotencyKey),
|
|
916
|
+
};
|
|
917
|
+
if (options.signature) {
|
|
918
|
+
const headers = headersWithIdempotency(requestOptions.headers, options.idempotencyKey);
|
|
919
|
+
headers.set("x-public-proof-feed-signature", options.signature);
|
|
920
|
+
requestOptions.headers = headers;
|
|
921
|
+
}
|
|
579
922
|
return this.request("/api/public-proof/registry/feed", requestOptions);
|
|
580
923
|
}
|
|
924
|
+
async publishAppStateInput(input, options = {}) {
|
|
925
|
+
if (isReceizAppStateFeed(input)) {
|
|
926
|
+
return this.publishAppState(input, options);
|
|
927
|
+
}
|
|
928
|
+
const record = createReceizTenantAppStateRecord(input);
|
|
929
|
+
const publishOptions = {};
|
|
930
|
+
if (options.signature)
|
|
931
|
+
publishOptions.signature = options.signature;
|
|
932
|
+
const idempotencyKey = options.idempotencyKey ?? input.idempotencyKey;
|
|
933
|
+
if (idempotencyKey)
|
|
934
|
+
publishOptions.idempotencyKey = idempotencyKey;
|
|
935
|
+
const feedOptions = { schema: RECEIZ_PUBLIC_STORE_STATE_FEED_SCHEMA };
|
|
936
|
+
if (record.namespace)
|
|
937
|
+
feedOptions.namespace = record.namespace;
|
|
938
|
+
if (record.externalCreatorId)
|
|
939
|
+
feedOptions.externalCreatorId = record.externalCreatorId;
|
|
940
|
+
return this.publishAppState(createReceizAppStateFeed([record], feedOptions), publishOptions);
|
|
941
|
+
}
|
|
581
942
|
async publishAppStateRecord(record, options = {}) {
|
|
582
|
-
const
|
|
583
|
-
const feed = {
|
|
943
|
+
const feed = createReceizAppStateFeed([record], {
|
|
584
944
|
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;
|
|
945
|
+
...(options.namespace ? { namespace: options.namespace } : {}),
|
|
946
|
+
...(options.externalCreatorId ? { externalCreatorId: options.externalCreatorId } : {}),
|
|
947
|
+
});
|
|
591
948
|
return this.publishAppState(feed, options.signature ? { signature: options.signature } : {});
|
|
592
949
|
}
|
|
593
950
|
async readAppStateByUrl(url) {
|
|
@@ -605,6 +962,156 @@ export class ReceizClient {
|
|
|
605
962
|
async readAppStateById(id) {
|
|
606
963
|
return this.request(`/api/public-proof/${encodePathSegment(id)}`);
|
|
607
964
|
}
|
|
965
|
+
async restoreAppStateByUrl(url, options = {}) {
|
|
966
|
+
const response = await this.readAppStateByUrl(url);
|
|
967
|
+
return restoreReceizAppStateProjection(response, options);
|
|
968
|
+
}
|
|
969
|
+
async restoreAppStateByHost(host, options = {}) {
|
|
970
|
+
return this.restoreAppStateByUrl(receizAppStateUrlFromHost(host), options);
|
|
971
|
+
}
|
|
972
|
+
async restoreAppStateById(id, options = {}) {
|
|
973
|
+
try {
|
|
974
|
+
const response = await this.readAppStateById(id);
|
|
975
|
+
return restoreReceizAppStateProjection(response, options);
|
|
976
|
+
}
|
|
977
|
+
catch (error) {
|
|
978
|
+
if (error instanceof ReceizHttpError && error.status === 404) {
|
|
979
|
+
return {
|
|
980
|
+
ok: false,
|
|
981
|
+
record: null,
|
|
982
|
+
data: null,
|
|
983
|
+
error: "app_state_not_found",
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
throw error;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
async resolveAppState(input) {
|
|
990
|
+
const options = {
|
|
991
|
+
...(input.schema ? { schema: input.schema } : {}),
|
|
992
|
+
...(input.state ? { state: input.state } : {}),
|
|
993
|
+
...(input.requiredDataKey ? { requiredDataKey: input.requiredDataKey } : {}),
|
|
994
|
+
};
|
|
995
|
+
if (input.id)
|
|
996
|
+
return this.restoreAppStateById(input.id, options);
|
|
997
|
+
if (input.url)
|
|
998
|
+
return this.restoreAppStateByUrl(input.url, options);
|
|
999
|
+
if (input.host || input.tenantHost)
|
|
1000
|
+
return this.restoreAppStateByHost(input.host ?? input.tenantHost ?? "", options);
|
|
1001
|
+
if (input.namespace) {
|
|
1002
|
+
const response = await this.readAppStateByNamespace(input.namespace);
|
|
1003
|
+
const records = response.records;
|
|
1004
|
+
return { ok: response.ok, record: records[0] ?? null, data: null, records };
|
|
1005
|
+
}
|
|
1006
|
+
const creator = input.creatorReceizId ?? input.receizId;
|
|
1007
|
+
if (creator) {
|
|
1008
|
+
const response = await this.readAppStateByCreator(creator);
|
|
1009
|
+
const records = response.records;
|
|
1010
|
+
return { ok: response.ok, record: records[0] ?? null, data: null, records };
|
|
1011
|
+
}
|
|
1012
|
+
throw new ReceizValidationError("receiz.appState.resolve", ["id, url, host, tenantHost, namespace, creatorReceizId, or receizId is required"]);
|
|
1013
|
+
}
|
|
1014
|
+
async oneClickCheckout(body) {
|
|
1015
|
+
const tenantHost = normalizeReceizHost(body.tenantHost);
|
|
1016
|
+
const totalUsdCents = usdToCents(body.amountUsd);
|
|
1017
|
+
const walletFirst = body.walletFirst !== false;
|
|
1018
|
+
const cardFallback = body.cardFallback !== false;
|
|
1019
|
+
const wallet = walletFirst ? await this.delegated("/api/connect/wallet/me") : null;
|
|
1020
|
+
const walletAvailable = walletFirst ? stringToCents(wallet?.balanceUsdCents) : 0n;
|
|
1021
|
+
const walletApplied = walletFirst ? (walletAvailable > totalUsdCents ? totalUsdCents : walletAvailable) : 0n;
|
|
1022
|
+
const cardDelta = totalUsdCents - walletApplied;
|
|
1023
|
+
let checkoutSession = null;
|
|
1024
|
+
if (cardDelta > 0n) {
|
|
1025
|
+
if (!cardFallback) {
|
|
1026
|
+
const response = {
|
|
1027
|
+
ok: false,
|
|
1028
|
+
tenantHost,
|
|
1029
|
+
funding: {
|
|
1030
|
+
totalUsdCents: String(totalUsdCents),
|
|
1031
|
+
walletAppliedUsdCents: String(walletApplied),
|
|
1032
|
+
cardDeltaUsdCents: String(cardDelta),
|
|
1033
|
+
walletFirst,
|
|
1034
|
+
cardFallback,
|
|
1035
|
+
},
|
|
1036
|
+
wallet,
|
|
1037
|
+
checkoutSession: null,
|
|
1038
|
+
proofObject: null,
|
|
1039
|
+
events: [{ type: "checkout.card_delta_required", cardDeltaUsdCents: String(cardDelta) }],
|
|
1040
|
+
error: "card_delta_required",
|
|
1041
|
+
};
|
|
1042
|
+
if (body.orderId)
|
|
1043
|
+
response.orderId = body.orderId;
|
|
1044
|
+
return response;
|
|
1045
|
+
}
|
|
1046
|
+
const checkoutBody = {
|
|
1047
|
+
amountUsd: centsToUsd(cardDelta),
|
|
1048
|
+
currency: body.currency ?? "usd",
|
|
1049
|
+
uiMode: "embedded",
|
|
1050
|
+
description: `Receiz one-click checkout for ${tenantHost}`,
|
|
1051
|
+
tenantHost,
|
|
1052
|
+
};
|
|
1053
|
+
if (body.orderId)
|
|
1054
|
+
checkoutBody.referenceId = body.orderId;
|
|
1055
|
+
if (body.customerEmail)
|
|
1056
|
+
checkoutBody.customerEmail = body.customerEmail;
|
|
1057
|
+
if (body.successUrl)
|
|
1058
|
+
checkoutBody.successUrl = body.successUrl;
|
|
1059
|
+
if (body.cancelUrl)
|
|
1060
|
+
checkoutBody.cancelUrl = body.cancelUrl;
|
|
1061
|
+
if (body.cart)
|
|
1062
|
+
checkoutBody.cart = body.cart;
|
|
1063
|
+
checkoutSession = await this.delegated("/api/connect/payments/checkout", {
|
|
1064
|
+
method: "POST",
|
|
1065
|
+
body: bodyWithIdempotency(checkoutBody, body.idempotencyKey),
|
|
1066
|
+
headers: headersWithIdempotency(undefined, body.idempotencyKey),
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
const response = {
|
|
1070
|
+
ok: true,
|
|
1071
|
+
tenantHost,
|
|
1072
|
+
funding: {
|
|
1073
|
+
totalUsdCents: String(totalUsdCents),
|
|
1074
|
+
walletAppliedUsdCents: String(walletApplied),
|
|
1075
|
+
cardDeltaUsdCents: String(cardDelta),
|
|
1076
|
+
walletFirst,
|
|
1077
|
+
cardFallback,
|
|
1078
|
+
},
|
|
1079
|
+
wallet,
|
|
1080
|
+
checkoutSession,
|
|
1081
|
+
proofObject: checkoutSession?.proofBundle ?? null,
|
|
1082
|
+
events: [
|
|
1083
|
+
{ type: "wallet.summary.read", walletAppliedUsdCents: String(walletApplied) },
|
|
1084
|
+
...(checkoutSession ? [{ type: "checkout.session.created", checkoutSessionId: checkoutSession.checkoutSessionId ?? null }] : []),
|
|
1085
|
+
],
|
|
1086
|
+
};
|
|
1087
|
+
if (body.orderId)
|
|
1088
|
+
response.orderId = body.orderId;
|
|
1089
|
+
return response;
|
|
1090
|
+
}
|
|
1091
|
+
async uploadMedia(file, options = {}) {
|
|
1092
|
+
const form = new FormData();
|
|
1093
|
+
form.set("file", file, options.filename ?? "receiz-media");
|
|
1094
|
+
if (options.tenantHost)
|
|
1095
|
+
form.set("tenantHost", normalizeReceizHost(options.tenantHost));
|
|
1096
|
+
if (options.purpose)
|
|
1097
|
+
form.set("purpose", options.purpose);
|
|
1098
|
+
if (options.metadata)
|
|
1099
|
+
form.set("metadata", JSON.stringify(options.metadata));
|
|
1100
|
+
if (options.idempotencyKey)
|
|
1101
|
+
form.set("idempotencyKey", options.idempotencyKey);
|
|
1102
|
+
return this.delegated("/api/connect/media/upload", {
|
|
1103
|
+
method: "POST",
|
|
1104
|
+
body: form,
|
|
1105
|
+
headers: headersWithIdempotency(undefined, options.idempotencyKey),
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
async eventSubscribe(body) {
|
|
1109
|
+
return this.delegated("/api/connect/events/subscribe", {
|
|
1110
|
+
method: "POST",
|
|
1111
|
+
body: bodyWithIdempotency(body, body.idempotencyKey),
|
|
1112
|
+
headers: headersWithIdempotency(undefined, body.idempotencyKey),
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
608
1115
|
async sportsConformance() {
|
|
609
1116
|
return this.request("/api/game/sports/conformance");
|
|
610
1117
|
}
|