@receiz/sdk 93.1.0 → 94.0.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 +95 -0
- package/dist/identity.d.ts +172 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +853 -0
- package/dist/index.d.ts +554 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +452 -1
- package/docs/app-registration-token-lifecycle.md +1 -1
- package/docs/identity-login-and-recovery.md +122 -0
- package/docs/proof-memory-and-projections.md +127 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,93 @@
|
|
|
1
|
-
|
|
1
|
+
import { buildReceizIdContinueRequest, createReceizIdIdentity, projectReceizIdentityAccount, readReceizIdentityArtifact, signReceizIdentityLoginProof, verifyReceizIdentityLoginProof, } from "./identity.js";
|
|
2
|
+
export * from "./identity.js";
|
|
3
|
+
export const RECEIZ_SDK_VERSION = "94.0.0";
|
|
2
4
|
export const RECEIZ_DEFAULT_BASE_URL = "https://receiz.com";
|
|
3
5
|
export const RECEIZ_WEBHOOK_SIGNATURE_HEADER = "x-receiz-signature";
|
|
4
6
|
export const RECEIZ_WEBHOOK_TIMESTAMP_HEADER = "x-receiz-timestamp";
|
|
7
|
+
export const RECEIZ_ASSET_MANIFEST_SCHEMA = {
|
|
8
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
9
|
+
$id: "https://receiz.com/standards/receiz.asset-manifest.schema.v1.json",
|
|
10
|
+
title: "Receiz Asset Manifest",
|
|
11
|
+
type: "object",
|
|
12
|
+
additionalProperties: true,
|
|
13
|
+
required: ["schema", "assetId", "assetType", "proof", "links"],
|
|
14
|
+
properties: {
|
|
15
|
+
schema: { const: "receiz.asset_manifest.v1" },
|
|
16
|
+
assetId: { type: "string", minLength: 1 },
|
|
17
|
+
assetType: {
|
|
18
|
+
enum: ["proof_object", "sports_card", "signal_card", "wallet_note", "market_certificate", "profile_original", "document"],
|
|
19
|
+
},
|
|
20
|
+
proof: {
|
|
21
|
+
type: "object",
|
|
22
|
+
required: ["kind"],
|
|
23
|
+
additionalProperties: true,
|
|
24
|
+
properties: {
|
|
25
|
+
kind: { type: "string", minLength: 1 },
|
|
26
|
+
verifyUrl: { type: "string" },
|
|
27
|
+
kaiPulseEternal: { type: ["number", "string"] },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
owner: { type: ["object", "null"], additionalProperties: true },
|
|
31
|
+
media: { type: ["object", "null"], additionalProperties: true },
|
|
32
|
+
appends: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
33
|
+
settlement: { type: ["object", "null"], additionalProperties: true },
|
|
34
|
+
links: { type: "object", additionalProperties: { type: "string" } },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
export const RECEIZ_SPORTS_CARD_MANIFEST_SCHEMA = {
|
|
38
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
39
|
+
$id: "https://receiz.com/standards/receiz.sports-card-manifest.schema.v1.json",
|
|
40
|
+
title: "Receiz Sports Arena Card Manifest",
|
|
41
|
+
type: "object",
|
|
42
|
+
additionalProperties: true,
|
|
43
|
+
required: ["schema", "sport", "collectibleId", "claimHash", "card", "ownership", "valueBasis", "links"],
|
|
44
|
+
properties: {
|
|
45
|
+
schema: { const: "receiz.sports_arena.card_manifest.v1" },
|
|
46
|
+
sport: { type: "string", minLength: 1 },
|
|
47
|
+
collectibleId: { type: "string", minLength: 1 },
|
|
48
|
+
claimHash: { type: "string", minLength: 1 },
|
|
49
|
+
card: { type: "object", additionalProperties: true },
|
|
50
|
+
ownership: { type: "object", additionalProperties: true },
|
|
51
|
+
valueBasis: { type: "object", additionalProperties: true },
|
|
52
|
+
appendSummary: { type: ["object", "null"], additionalProperties: true },
|
|
53
|
+
eventProofSummary: { type: ["object", "null"], additionalProperties: true },
|
|
54
|
+
links: { type: "object", additionalProperties: { type: "string" } },
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
export const RECEIZ_WEBHOOK_EVENT_SCHEMA = {
|
|
58
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
59
|
+
$id: "https://receiz.com/standards/receiz.webhook-event.schema.v1.json",
|
|
60
|
+
title: "Receiz Webhook Event",
|
|
61
|
+
type: "object",
|
|
62
|
+
additionalProperties: true,
|
|
63
|
+
required: ["schema", "id", "type", "createdAt", "data"],
|
|
64
|
+
properties: {
|
|
65
|
+
schema: { const: "receiz.webhook_event.v1" },
|
|
66
|
+
id: { type: "string", minLength: 1 },
|
|
67
|
+
type: {
|
|
68
|
+
enum: [
|
|
69
|
+
"asset.created",
|
|
70
|
+
"asset.transferred",
|
|
71
|
+
"proof.appended",
|
|
72
|
+
"sports_card.scored",
|
|
73
|
+
"event_proof.created",
|
|
74
|
+
"wallet.ledger_entry",
|
|
75
|
+
"note.claimed",
|
|
76
|
+
"market.trade",
|
|
77
|
+
"profile.asset.visible_changed",
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
createdAt: { type: "string", minLength: 1 },
|
|
81
|
+
data: { type: "object", additionalProperties: true },
|
|
82
|
+
actor: { type: ["object", "null"], additionalProperties: true },
|
|
83
|
+
signature: { type: ["object", "null"], additionalProperties: true },
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
export const RECEIZ_SCHEMAS = {
|
|
87
|
+
assetManifest: RECEIZ_ASSET_MANIFEST_SCHEMA,
|
|
88
|
+
sportsCardManifest: RECEIZ_SPORTS_CARD_MANIFEST_SCHEMA,
|
|
89
|
+
webhookEvent: RECEIZ_WEBHOOK_EVENT_SCHEMA,
|
|
90
|
+
};
|
|
5
91
|
export class ReceizHttpError extends Error {
|
|
6
92
|
status;
|
|
7
93
|
payload;
|
|
@@ -179,6 +265,13 @@ export class ReceizClient {
|
|
|
179
265
|
downloadNote: (noteId) => this.delegated(`/api/connect/payments/notes/${encodePathSegment(noteId)}/download`),
|
|
180
266
|
};
|
|
181
267
|
identity = {
|
|
268
|
+
createReceizId: createReceizIdIdentity,
|
|
269
|
+
readArtifact: readReceizIdentityArtifact,
|
|
270
|
+
projectAccount: projectReceizIdentityAccount,
|
|
271
|
+
signLoginProof: signReceizIdentityLoginProof,
|
|
272
|
+
verifyLoginProof: verifyReceizIdentityLoginProof,
|
|
273
|
+
buildReceizIdContinueRequest,
|
|
274
|
+
continueReceizId: (identity, options = {}) => this.continueReceizId(identity, options),
|
|
182
275
|
openIdConfiguration: () => this.request("/.well-known/openid-configuration"),
|
|
183
276
|
oauthAuthorizationServerMetadata: () => this.request("/.well-known/oauth-authorization-server"),
|
|
184
277
|
jwks: () => this.request("/api/oidc/jwks"),
|
|
@@ -227,6 +320,16 @@ export class ReceizClient {
|
|
|
227
320
|
isAsset: isReceizAssetManifest,
|
|
228
321
|
isSportsCard: isReceizSportsCardManifest,
|
|
229
322
|
isWebhookEvent: isReceizWebhookEvent,
|
|
323
|
+
projectAsset: projectReceizAssetManifest,
|
|
324
|
+
projectSportsCard: projectReceizSportsCardManifest,
|
|
325
|
+
};
|
|
326
|
+
schemas = RECEIZ_SCHEMAS;
|
|
327
|
+
projections = {
|
|
328
|
+
assetManifest: projectReceizAssetManifest,
|
|
329
|
+
sportsCardManifest: projectReceizSportsCardManifest,
|
|
330
|
+
};
|
|
331
|
+
proofMemory = {
|
|
332
|
+
createRegister: createReceizProofRegister,
|
|
230
333
|
};
|
|
231
334
|
constructor(options = {}) {
|
|
232
335
|
this.baseUrl = trimTrailingSlash(options.baseUrl ?? RECEIZ_DEFAULT_BASE_URL);
|
|
@@ -290,6 +393,10 @@ export class ReceizClient {
|
|
|
290
393
|
throw new Error("receiz_access_token_required");
|
|
291
394
|
return this.request(path, { ...options, bearerToken: this.accessToken });
|
|
292
395
|
}
|
|
396
|
+
async continueReceizId(identity, options = {}) {
|
|
397
|
+
const body = await buildReceizIdContinueRequest(identity, options);
|
|
398
|
+
return this.request("/api/auth/receiz-id/continue", { method: "POST", body });
|
|
399
|
+
}
|
|
293
400
|
authorizeUrl(options) {
|
|
294
401
|
const url = new URL(`${this.baseUrl}/api/oidc/authorize`);
|
|
295
402
|
url.searchParams.set("response_type", options.responseType ?? "code");
|
|
@@ -417,6 +524,350 @@ export function isReceizWebhookEvent(value) {
|
|
|
417
524
|
return false;
|
|
418
525
|
}
|
|
419
526
|
}
|
|
527
|
+
function stringField(record, key) {
|
|
528
|
+
if (!record)
|
|
529
|
+
return null;
|
|
530
|
+
const value = record[key];
|
|
531
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
532
|
+
}
|
|
533
|
+
function numberField(record, key) {
|
|
534
|
+
if (!record)
|
|
535
|
+
return null;
|
|
536
|
+
const value = record[key];
|
|
537
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
538
|
+
return value;
|
|
539
|
+
if (typeof value === "string" && value.trim() && Number.isFinite(Number(value)))
|
|
540
|
+
return Number(value);
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
function firstString(...values) {
|
|
544
|
+
for (const value of values) {
|
|
545
|
+
if (typeof value === "string" && value.trim())
|
|
546
|
+
return value.trim();
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
function objectField(record, key) {
|
|
551
|
+
if (!record)
|
|
552
|
+
return null;
|
|
553
|
+
const value = record[key];
|
|
554
|
+
return isRecord(value) ? value : null;
|
|
555
|
+
}
|
|
556
|
+
function optionalCount(value) {
|
|
557
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
558
|
+
return value;
|
|
559
|
+
if (typeof value === "string" && Number.isFinite(Number(value)))
|
|
560
|
+
return Number(value);
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
function formatUsdCents(value) {
|
|
564
|
+
if (typeof value !== "string" && typeof value !== "number")
|
|
565
|
+
return null;
|
|
566
|
+
const numeric = Number(value);
|
|
567
|
+
if (!Number.isFinite(numeric))
|
|
568
|
+
return null;
|
|
569
|
+
return `$${(numeric / 100).toFixed(2)}`;
|
|
570
|
+
}
|
|
571
|
+
function proofCreatedAt(proof) {
|
|
572
|
+
return firstString(stringField(proof, "ts"), stringField(proof, "createdAt"));
|
|
573
|
+
}
|
|
574
|
+
function proofKaiUpulse(proof) {
|
|
575
|
+
return firstString(stringField(proof, "kaiUpulse"), stringField(proof, "kaiPulse"), stringField(proof, "kaiPulseEternal")) ?? numberField(proof, "kaiPulseEternal");
|
|
576
|
+
}
|
|
577
|
+
function addProjectionRow(rows, label, value, href) {
|
|
578
|
+
if (value === null || value === undefined || value === "")
|
|
579
|
+
return;
|
|
580
|
+
rows.push({ label, value: String(value), href: href ?? null });
|
|
581
|
+
}
|
|
582
|
+
export function projectReceizAssetManifest(value) {
|
|
583
|
+
const manifest = assertReceizAssetManifest(value);
|
|
584
|
+
const owner = manifest.owner ?? null;
|
|
585
|
+
const media = manifest.media ?? null;
|
|
586
|
+
const settlement = manifest.settlement ?? null;
|
|
587
|
+
const verifyUrl = firstString(manifest.links.verify, manifest.proof.verifyUrl, manifest.proof.verifyPath);
|
|
588
|
+
const primaryUrl = firstString(manifest.links.open, manifest.links.public, manifest.links.verify, verifyUrl);
|
|
589
|
+
const mediaUrl = firstString(stringField(media, "url"), stringField(media, "src"), stringField(media, "imageUrl"));
|
|
590
|
+
const ownerLabel = firstString(stringField(owner, "displayName"), stringField(owner, "ownerLabel"), stringField(owner, "username"), stringField(owner, "receizSubject"));
|
|
591
|
+
const title = firstString(stringField(media, "title"), stringField(media, "name"), manifest.assetId) ?? manifest.assetId;
|
|
592
|
+
const proofKind = manifest.proof.kind;
|
|
593
|
+
const settlementState = firstString(stringField(settlement, "state"), stringField(settlement, "status"));
|
|
594
|
+
const rows = [];
|
|
595
|
+
addProjectionRow(rows, "Asset ID", manifest.assetId);
|
|
596
|
+
addProjectionRow(rows, "Asset Type", manifest.assetType);
|
|
597
|
+
addProjectionRow(rows, "Proof Kind", proofKind);
|
|
598
|
+
addProjectionRow(rows, "Verify", verifyUrl, verifyUrl);
|
|
599
|
+
addProjectionRow(rows, "Kai Pulse", proofKaiUpulse(manifest.proof));
|
|
600
|
+
addProjectionRow(rows, "Appends", manifest.appends?.length ?? 0);
|
|
601
|
+
addProjectionRow(rows, "Settlement", settlementState);
|
|
602
|
+
addProjectionRow(rows, "Owner", ownerLabel);
|
|
603
|
+
return {
|
|
604
|
+
schema: "receiz.sdk.asset_manifest_projection.v1",
|
|
605
|
+
assetId: manifest.assetId,
|
|
606
|
+
assetType: manifest.assetType,
|
|
607
|
+
title,
|
|
608
|
+
subtitle: ownerLabel ? `${manifest.assetType} / ${ownerLabel}` : manifest.assetType,
|
|
609
|
+
proofKind,
|
|
610
|
+
verifyUrl,
|
|
611
|
+
primaryUrl,
|
|
612
|
+
mediaUrl,
|
|
613
|
+
ownerLabel,
|
|
614
|
+
appendCount: manifest.appends?.length ?? 0,
|
|
615
|
+
settlementState,
|
|
616
|
+
rows,
|
|
617
|
+
manifest,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
export function projectReceizSportsCardManifest(value) {
|
|
621
|
+
const manifest = assertReceizSportsCardManifest(value);
|
|
622
|
+
const card = manifest.card;
|
|
623
|
+
const ownership = manifest.ownership;
|
|
624
|
+
const valueBasis = manifest.valueBasis;
|
|
625
|
+
const appendSummary = manifest.appendSummary ?? null;
|
|
626
|
+
const eventProofSummary = manifest.eventProofSummary ?? null;
|
|
627
|
+
const title = firstString(stringField(card, "playerName"), stringField(card, "athleteName"), stringField(card, "name"), manifest.collectibleId) ?? manifest.collectibleId;
|
|
628
|
+
const rarity = firstString(stringField(card, "rarity"), stringField(card, "tier"), stringField(card, "edition"));
|
|
629
|
+
const team = firstString(stringField(card, "team"), stringField(card, "club"));
|
|
630
|
+
const verifyUrl = firstString(manifest.links.verify, manifest.links.proof, manifest.links.asset);
|
|
631
|
+
const eventProofUrl = firstString(manifest.links.eventProof, stringField(eventProofSummary, "latestEventProofUrl"), stringField(eventProofSummary, "latestProofUrl"));
|
|
632
|
+
const mediaUrl = firstString(stringField(card, "deterministicImageUrl"), stringField(card, "imageUrl"), stringField(card, "mediaUrl"));
|
|
633
|
+
const ownerLabel = firstString(stringField(ownership, "ownerLabel"), stringField(ownership, "displayName"), stringField(ownership, "ownerSubject"));
|
|
634
|
+
const scoreLabel = firstString(stringField(valueBasis, "score"), stringField(valueBasis, "valueScore"));
|
|
635
|
+
const valueLabel = firstString(stringField(valueBasis, "valueLabel"), stringField(valueBasis, "reserveLabel"), formatUsdCents(valueBasis.reserveUsdCents));
|
|
636
|
+
const appendCount = optionalCount(appendSummary?.count);
|
|
637
|
+
const eventProofCount = optionalCount(eventProofSummary?.count);
|
|
638
|
+
const rows = [];
|
|
639
|
+
addProjectionRow(rows, "Collectible ID", manifest.collectibleId);
|
|
640
|
+
addProjectionRow(rows, "Claim Hash", manifest.claimHash);
|
|
641
|
+
addProjectionRow(rows, "Sport", manifest.sport);
|
|
642
|
+
addProjectionRow(rows, "Owner", ownerLabel);
|
|
643
|
+
addProjectionRow(rows, "Score", scoreLabel);
|
|
644
|
+
addProjectionRow(rows, "Reserve", valueLabel);
|
|
645
|
+
addProjectionRow(rows, "Kai Upulse", firstString(stringField(valueBasis, "kaiUpulse"), stringField(valueBasis, "kaiPulse")));
|
|
646
|
+
addProjectionRow(rows, "Appends", appendCount);
|
|
647
|
+
addProjectionRow(rows, "Event Proofs", eventProofCount);
|
|
648
|
+
addProjectionRow(rows, "Latest Event", stringField(eventProofSummary, "latestEventType"));
|
|
649
|
+
addProjectionRow(rows, "Proof", verifyUrl, verifyUrl);
|
|
650
|
+
return {
|
|
651
|
+
schema: "receiz.sdk.sports_card_manifest_projection.v1",
|
|
652
|
+
sport: manifest.sport,
|
|
653
|
+
collectibleId: manifest.collectibleId,
|
|
654
|
+
claimHash: manifest.claimHash,
|
|
655
|
+
title,
|
|
656
|
+
subtitle: [manifest.sport, team, rarity].filter(Boolean).join(" / "),
|
|
657
|
+
verifyUrl,
|
|
658
|
+
eventProofUrl,
|
|
659
|
+
mediaUrl,
|
|
660
|
+
ownerLabel,
|
|
661
|
+
scoreLabel,
|
|
662
|
+
valueLabel,
|
|
663
|
+
appendCount,
|
|
664
|
+
eventProofCount,
|
|
665
|
+
rows,
|
|
666
|
+
manifest,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function normalizeKaiForCompare(value) {
|
|
670
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
671
|
+
return String(value);
|
|
672
|
+
if (typeof value !== "string")
|
|
673
|
+
return null;
|
|
674
|
+
const trimmed = value.trim();
|
|
675
|
+
if (!trimmed)
|
|
676
|
+
return null;
|
|
677
|
+
return trimmed.replace(/^\+/, "");
|
|
678
|
+
}
|
|
679
|
+
function compareNumericTextDesc(leftValue, rightValue) {
|
|
680
|
+
const left = normalizeKaiForCompare(leftValue);
|
|
681
|
+
const right = normalizeKaiForCompare(rightValue);
|
|
682
|
+
if (!left && !right)
|
|
683
|
+
return 0;
|
|
684
|
+
if (!left)
|
|
685
|
+
return 1;
|
|
686
|
+
if (!right)
|
|
687
|
+
return -1;
|
|
688
|
+
const [leftWholeRaw = "", leftFractionRaw = ""] = left.split(".");
|
|
689
|
+
const [rightWholeRaw = "", rightFractionRaw = ""] = right.split(".");
|
|
690
|
+
const leftWhole = leftWholeRaw.replace(/^0+/, "") || "0";
|
|
691
|
+
const rightWhole = rightWholeRaw.replace(/^0+/, "") || "0";
|
|
692
|
+
if (leftWhole.length !== rightWhole.length)
|
|
693
|
+
return rightWhole.length - leftWhole.length;
|
|
694
|
+
if (leftWhole !== rightWhole)
|
|
695
|
+
return rightWhole.localeCompare(leftWhole);
|
|
696
|
+
const maxFractionLength = Math.max(leftFractionRaw.length, rightFractionRaw.length);
|
|
697
|
+
const leftFraction = leftFractionRaw.padEnd(maxFractionLength, "0");
|
|
698
|
+
const rightFraction = rightFractionRaw.padEnd(maxFractionLength, "0");
|
|
699
|
+
return rightFraction.localeCompare(leftFraction);
|
|
700
|
+
}
|
|
701
|
+
function compareRegisterEntries(left, right) {
|
|
702
|
+
const kai = compareNumericTextDesc(left.kaiUpulse, right.kaiUpulse);
|
|
703
|
+
if (kai !== 0)
|
|
704
|
+
return kai;
|
|
705
|
+
const leftTime = left.createdAt ? Date.parse(left.createdAt) : Number.NaN;
|
|
706
|
+
const rightTime = right.createdAt ? Date.parse(right.createdAt) : Number.NaN;
|
|
707
|
+
if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime)
|
|
708
|
+
return rightTime - leftTime;
|
|
709
|
+
if (Number.isFinite(leftTime) && !Number.isFinite(rightTime))
|
|
710
|
+
return -1;
|
|
711
|
+
if (!Number.isFinite(leftTime) && Number.isFinite(rightTime))
|
|
712
|
+
return 1;
|
|
713
|
+
return left.id.localeCompare(right.id);
|
|
714
|
+
}
|
|
715
|
+
function mergeJsonPreservingExisting(existing, incoming) {
|
|
716
|
+
const merged = { ...existing };
|
|
717
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
718
|
+
if (merged[key] === undefined)
|
|
719
|
+
merged[key] = value;
|
|
720
|
+
}
|
|
721
|
+
return merged;
|
|
722
|
+
}
|
|
723
|
+
function mergeOptionalJsonPreservingExisting(existing, incoming) {
|
|
724
|
+
if (existing && incoming)
|
|
725
|
+
return mergeJsonPreservingExisting(existing, incoming);
|
|
726
|
+
return existing ?? incoming ?? null;
|
|
727
|
+
}
|
|
728
|
+
function normalizeRegisterEntry(value) {
|
|
729
|
+
const record = ensureRecord(value, "ReceizProofRegisterEntry");
|
|
730
|
+
const issues = [];
|
|
731
|
+
const id = ensureString(record, "id", issues);
|
|
732
|
+
const kind = ensureString(record, "kind", issues);
|
|
733
|
+
if (!isRecord(record.payload))
|
|
734
|
+
issues.push("payload must be an object");
|
|
735
|
+
const createdAt = record.createdAt === null || record.createdAt === undefined ? null : ensureString(record, "createdAt", issues);
|
|
736
|
+
const kaiUpulse = record.kaiUpulse;
|
|
737
|
+
if (kaiUpulse !== null && kaiUpulse !== undefined && typeof kaiUpulse !== "string" && typeof kaiUpulse !== "number") {
|
|
738
|
+
issues.push("kaiUpulse must be a string, number, or null");
|
|
739
|
+
}
|
|
740
|
+
const proof = record.proof;
|
|
741
|
+
if (proof !== null && proof !== undefined && !isRecord(proof))
|
|
742
|
+
issues.push("proof must be an object or null");
|
|
743
|
+
const projection = record.projection;
|
|
744
|
+
if (projection !== null && projection !== undefined && !isRecord(projection))
|
|
745
|
+
issues.push("projection must be an object or null");
|
|
746
|
+
failIfIssues("ReceizProofRegisterEntry", issues);
|
|
747
|
+
return {
|
|
748
|
+
id: id ?? "",
|
|
749
|
+
kind: kind ?? "",
|
|
750
|
+
createdAt,
|
|
751
|
+
kaiUpulse: kaiUpulse === undefined ? null : kaiUpulse,
|
|
752
|
+
proof: isRecord(proof) ? proof : null,
|
|
753
|
+
payload: record.payload,
|
|
754
|
+
projection: isRecord(projection) ? projection : null,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function mergeRegisterEntry(existing, incoming) {
|
|
758
|
+
return {
|
|
759
|
+
id: existing.id,
|
|
760
|
+
kind: existing.kind,
|
|
761
|
+
createdAt: existing.createdAt ?? incoming.createdAt ?? null,
|
|
762
|
+
kaiUpulse: existing.kaiUpulse ?? incoming.kaiUpulse ?? null,
|
|
763
|
+
proof: mergeOptionalJsonPreservingExisting(existing.proof, incoming.proof),
|
|
764
|
+
payload: mergeJsonPreservingExisting(existing.payload, incoming.payload),
|
|
765
|
+
projection: mergeOptionalJsonPreservingExisting(existing.projection, incoming.projection),
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function registerEntryFromAssetManifest(manifest) {
|
|
769
|
+
const projection = projectReceizAssetManifest(manifest);
|
|
770
|
+
return {
|
|
771
|
+
id: `asset:${manifest.assetId}`,
|
|
772
|
+
kind: "asset_manifest",
|
|
773
|
+
createdAt: proofCreatedAt(manifest.proof),
|
|
774
|
+
kaiUpulse: proofKaiUpulse(manifest.proof),
|
|
775
|
+
proof: manifest.proof,
|
|
776
|
+
payload: manifest,
|
|
777
|
+
projection: projection,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function registerEntryFromSportsCardManifest(manifest) {
|
|
781
|
+
const projection = projectReceizSportsCardManifest(manifest);
|
|
782
|
+
return {
|
|
783
|
+
id: `sports-card:${manifest.collectibleId}:${manifest.claimHash}`,
|
|
784
|
+
kind: "sports_card_manifest",
|
|
785
|
+
createdAt: firstString(stringField(manifest.appendSummary ?? null, "latestObservedAt"), stringField(manifest.ownership, "updatedAt")),
|
|
786
|
+
kaiUpulse: firstString(stringField(manifest.valueBasis, "kaiUpulse"), stringField(manifest.valueBasis, "kaiPulse")),
|
|
787
|
+
proof: {
|
|
788
|
+
claimHash: manifest.claimHash,
|
|
789
|
+
latestAppendHash: stringField(manifest.appendSummary ?? null, "latestAppendHash"),
|
|
790
|
+
latestProofHash: stringField(manifest.eventProofSummary ?? null, "latestProofHash"),
|
|
791
|
+
},
|
|
792
|
+
payload: manifest,
|
|
793
|
+
projection: projection,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function registerEntryFromWebhookEvent(event) {
|
|
797
|
+
const proofBundle = objectField(event.data, "proofBundle");
|
|
798
|
+
const assetId = stringField(event.data, "assetId");
|
|
799
|
+
const appendId = stringField(event.data, "appendId");
|
|
800
|
+
return {
|
|
801
|
+
id: `webhook:${event.id}`,
|
|
802
|
+
kind: event.type,
|
|
803
|
+
createdAt: event.createdAt,
|
|
804
|
+
kaiUpulse: proofKaiUpulse(proofBundle),
|
|
805
|
+
proof: proofBundle,
|
|
806
|
+
payload: event,
|
|
807
|
+
projection: {
|
|
808
|
+
eventId: event.id,
|
|
809
|
+
type: event.type,
|
|
810
|
+
assetId,
|
|
811
|
+
appendId,
|
|
812
|
+
verifyUrl: stringField(proofBundle, "verifyUrl"),
|
|
813
|
+
},
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
export class ReceizProofRegister {
|
|
817
|
+
ownerId;
|
|
818
|
+
createdAt;
|
|
819
|
+
entriesById = new Map();
|
|
820
|
+
constructor(input = {}) {
|
|
821
|
+
this.ownerId = input.ownerId ?? null;
|
|
822
|
+
this.createdAt = input.createdAt ?? new Date().toISOString();
|
|
823
|
+
for (const entry of input.entries ?? [])
|
|
824
|
+
this.append(entry);
|
|
825
|
+
}
|
|
826
|
+
append(entry) {
|
|
827
|
+
const normalized = normalizeRegisterEntry(entry);
|
|
828
|
+
const existing = this.entriesById.get(normalized.id);
|
|
829
|
+
this.entriesById.set(normalized.id, existing ? mergeRegisterEntry(existing, normalized) : normalized);
|
|
830
|
+
return this;
|
|
831
|
+
}
|
|
832
|
+
admitAssetManifest(value) {
|
|
833
|
+
return this.append(registerEntryFromAssetManifest(assertReceizAssetManifest(value)));
|
|
834
|
+
}
|
|
835
|
+
admitSportsCardManifest(value) {
|
|
836
|
+
return this.append(registerEntryFromSportsCardManifest(assertReceizSportsCardManifest(value)));
|
|
837
|
+
}
|
|
838
|
+
admitWebhookEvent(value) {
|
|
839
|
+
return this.append(registerEntryFromWebhookEvent(assertReceizWebhookEvent(value)));
|
|
840
|
+
}
|
|
841
|
+
has(entryId) {
|
|
842
|
+
return this.entriesById.has(entryId);
|
|
843
|
+
}
|
|
844
|
+
entries() {
|
|
845
|
+
return [...this.entriesById.values()].sort(compareRegisterEntries);
|
|
846
|
+
}
|
|
847
|
+
snapshot() {
|
|
848
|
+
const entries = this.entries();
|
|
849
|
+
const head = entries[0] ?? null;
|
|
850
|
+
return {
|
|
851
|
+
schema: "receiz.sdk.proof_register.v1",
|
|
852
|
+
ownerId: this.ownerId,
|
|
853
|
+
createdAt: this.createdAt,
|
|
854
|
+
updatedAt: new Date().toISOString(),
|
|
855
|
+
head: {
|
|
856
|
+
entryId: head?.id ?? null,
|
|
857
|
+
kaiUpulse: head?.kaiUpulse ?? null,
|
|
858
|
+
createdAt: head?.createdAt ?? null,
|
|
859
|
+
count: entries.length,
|
|
860
|
+
},
|
|
861
|
+
entries,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
toJSON() {
|
|
865
|
+
return this.snapshot();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
export function createReceizProofRegister(input = {}) {
|
|
869
|
+
return new ReceizProofRegister(input);
|
|
870
|
+
}
|
|
420
871
|
function bodyToString(body) {
|
|
421
872
|
if (typeof body === "string")
|
|
422
873
|
return body;
|
|
@@ -17,7 +17,7 @@ const authorizeUrl = receiz.identity.authorizeUrl({
|
|
|
17
17
|
});
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Receiz Connect uses standard OIDC mechanics for app onboarding and token lifecycle
|
|
20
|
+
Receiz Connect is delegated API access. It uses standard OIDC mechanics for app onboarding and token lifecycle, but it is not the identity proof root. Receiz ID, Receiz Key, Identity Record, and Identity Seal remain the identity primitives; Connect tokens authorize scoped API calls after that proof boundary.
|
|
21
21
|
|
|
22
22
|
Lifecycle:
|
|
23
23
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Identity Login And Recovery
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
npm install @receiz/sdk
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
buildReceizIdContinueRequest,
|
|
10
|
+
createReceizClient,
|
|
11
|
+
createReceizIdIdentity,
|
|
12
|
+
projectReceizIdentityAccount,
|
|
13
|
+
readReceizIdentityArtifact,
|
|
14
|
+
signReceizIdentityLoginProof,
|
|
15
|
+
verifyReceizIdentityLoginProof,
|
|
16
|
+
} from "@receiz/sdk";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Receiz ID, Receiz Key, Identity Record, and Identity Seal are identity primitives. They carry proof-bearing account continuity. Your app should verify and project them locally first, then ask Receiz only for verified additions or delegated API access.
|
|
20
|
+
|
|
21
|
+
Receiz ID is the default login path. It does not replace PBI/passkey, email magic-link recovery, or OIDC/Connect. PBI/passkey can still create or recover a Receiz account, email can still recover the account, Receiz Key / Identity Record / Identity Seal can still restore it locally, and OIDC/Connect still delegates API access after identity proof. These rails bind to the same account; they are not parallel account models.
|
|
22
|
+
|
|
23
|
+
## Create A Fresh Receiz ID
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
const identity = await createReceizIdIdentity({
|
|
27
|
+
username: "builder",
|
|
28
|
+
displayName: "Builder",
|
|
29
|
+
deviceName: "Builder's device",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const account = await projectReceizIdentityAccount(identity.keyFile);
|
|
33
|
+
|
|
34
|
+
if (!account.portableStateVerified) {
|
|
35
|
+
throw new Error("Receiz ID account-state proof failed");
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Persist the returned identity artifact in your app's durable proof memory. It is not a temporary session object.
|
|
40
|
+
|
|
41
|
+
## Login With A Receiz Key Or Identity Image
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
const keyFile = await readReceizIdentityArtifact(fileOrImageBytes);
|
|
45
|
+
const account = await projectReceizIdentityAccount(keyFile);
|
|
46
|
+
|
|
47
|
+
if (!account.portableStateVerified) {
|
|
48
|
+
throw new Error("Receiz identity artifact proof failed");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
renderSignedInShell({
|
|
52
|
+
userId: account.owner.uid,
|
|
53
|
+
username: account.owner.username,
|
|
54
|
+
displayName: account.owner.displayName,
|
|
55
|
+
domains: account.domains,
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`readReceizIdentityArtifact` accepts Receiz Key JSON, Identity Record image bytes, or Identity Seal image bytes. The projection is local verified truth; do not block first paint on a server rediscovery pass.
|
|
60
|
+
|
|
61
|
+
## Prove Control To Your App
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const proof = await signReceizIdentityLoginProof({
|
|
65
|
+
keyFile,
|
|
66
|
+
challengeText: "YOUR_APP_LOGIN_V1\nnonce=server-issued-nonce",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const ok = await verifyReceizIdentityLoginProof({
|
|
70
|
+
keyFile,
|
|
71
|
+
challengeB64Url: proof.challengeB64Url,
|
|
72
|
+
signatureB64Url: proof.signatureB64Url,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!ok) throw new Error("Receiz identity signature failed");
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Use a fresh challenge from your own app server when you need server-side session issuance. The proof verifies ownership of the Receiz identity key; it does not make your app the authority over Receiz truth.
|
|
79
|
+
|
|
80
|
+
## Publish Continuity To Receiz
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const receiz = createReceizClient({ baseUrl: "https://receiz.com" });
|
|
84
|
+
|
|
85
|
+
const response = await receiz.identity.continueReceizId(identity, {
|
|
86
|
+
next: "/profile",
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This calls the existing Receiz ID continuation route. It publishes the same signed proof the Receiz app uses for real account creation/recovery. Use it when your app needs Receiz infrastructure to append global continuity or issue a Receiz session.
|
|
91
|
+
|
|
92
|
+
When a user has already recovered with PBI/passkey or email, use Receiz ID continuation as the bound default for the same account. Do not remove the recovery rail from your app just because the Receiz ID path is default.
|
|
93
|
+
|
|
94
|
+
For server-side publication, build the request explicitly:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const body = await buildReceizIdContinueRequest(identity, {
|
|
98
|
+
next: "/profile",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await fetch("https://receiz.com/api/auth/receiz-id/continue", {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { "content-type": "application/json" },
|
|
104
|
+
body: JSON.stringify(body),
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Connect After Proof Login
|
|
109
|
+
|
|
110
|
+
Use Connect only when your app needs delegated Receiz API access.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const connectUrl = receiz.identity.authorizeUrl({
|
|
114
|
+
clientId: process.env.RECEIZ_CLIENT_ID!,
|
|
115
|
+
redirectUri: "https://app.example.com/auth/receiz/callback",
|
|
116
|
+
codeChallenge,
|
|
117
|
+
scope: ["openid", "profile", "receiz:wallet.transfer"],
|
|
118
|
+
state,
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Connect tokens authorize API calls beneath the identity primitive. They are permission artifacts, not the proof root.
|