@calimero-network/mero-js 26.2.1 → 26.3.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/dist/admin-api/admin-types.d.ts +5 -0
- package/dist/cloud/cloud-client.d.ts +10 -3
- package/dist/cloud/cloud-client.js +29 -10
- package/dist/cloud/link-redirect.d.ts +22 -13
- package/dist/cloud/link-redirect.js +33 -20
- package/dist/cloud/link-state.d.ts +24 -5
- package/dist/cloud/link-state.js +46 -9
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +80 -25
- package/dist/index.cjs.map +2 -2
- package/dist/index.mjs +80 -25
- package/dist/index.mjs.map +2 -2
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2535,6 +2535,9 @@ var STATE_PARAM = "link_state";
|
|
|
2535
2535
|
var LIFETIME_MS = 10 * 60 * 1e3;
|
|
2536
2536
|
var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
|
|
2537
2537
|
var MAX_PENDING = 20;
|
|
2538
|
+
var VERIFIER_BYTES = 32;
|
|
2539
|
+
var VERIFIER_HEX = /^[0-9a-f]{64}$/;
|
|
2540
|
+
var LINK_GRANT_USE = "account-link-grant";
|
|
2538
2541
|
function readPending(key, now) {
|
|
2539
2542
|
try {
|
|
2540
2543
|
const raw = localStorage.getItem(key);
|
|
@@ -2557,10 +2560,7 @@ function writePending(key, pending) {
|
|
|
2557
2560
|
function beginState(key, payload) {
|
|
2558
2561
|
const now = Date.now();
|
|
2559
2562
|
const pending = readPending(key, now);
|
|
2560
|
-
const
|
|
2561
|
-
const state = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
|
|
2562
|
-
""
|
|
2563
|
-
);
|
|
2563
|
+
const state = hex(crypto.getRandomValues(new Uint8Array(16)));
|
|
2564
2564
|
pending[state] = { ...payload, expiresAt: now + LIFETIME_MS };
|
|
2565
2565
|
writePending(key, Object.fromEntries(Object.entries(pending).slice(-MAX_PENDING)));
|
|
2566
2566
|
return state;
|
|
@@ -2574,16 +2574,41 @@ function takeState(key, state) {
|
|
|
2574
2574
|
writePending(key, pending);
|
|
2575
2575
|
return entry;
|
|
2576
2576
|
}
|
|
2577
|
-
function
|
|
2577
|
+
async function linkChallenge(verifier) {
|
|
2578
|
+
const bytes = Uint8Array.from(verifier.match(/../g) ?? [], (b) => parseInt(b, 16));
|
|
2579
|
+
return hex(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)));
|
|
2580
|
+
}
|
|
2581
|
+
async function beginLinkState(account, returnTo) {
|
|
2578
2582
|
if (!ACCOUNT_HEX.test(account)) {
|
|
2579
2583
|
throw new Error("accountId must be 64 lowercase hex characters");
|
|
2580
2584
|
}
|
|
2581
2585
|
const url2 = new URL(returnTo);
|
|
2582
|
-
|
|
2583
|
-
|
|
2586
|
+
const verifier = hex(crypto.getRandomValues(new Uint8Array(VERIFIER_BYTES)));
|
|
2587
|
+
const challenge = await linkChallenge(verifier);
|
|
2588
|
+
url2.searchParams.set(STATE_PARAM, beginState(KEY, { account, verifier }));
|
|
2589
|
+
return { callbackUrl: url2.toString(), challenge };
|
|
2590
|
+
}
|
|
2591
|
+
function takeLinkVerifier(search, account) {
|
|
2592
|
+
const pending = takeState(
|
|
2593
|
+
KEY,
|
|
2594
|
+
new URLSearchParams(search).get(STATE_PARAM)
|
|
2595
|
+
);
|
|
2596
|
+
if (!pending || pending.account !== account) return null;
|
|
2597
|
+
return typeof pending.verifier === "string" && VERIFIER_HEX.test(pending.verifier) ? pending.verifier : null;
|
|
2584
2598
|
}
|
|
2585
|
-
function
|
|
2586
|
-
|
|
2599
|
+
function linkGrantClaims(grant) {
|
|
2600
|
+
try {
|
|
2601
|
+
const body = grant.split(".")[0].replace(/-/g, "+").replace(/_/g, "/");
|
|
2602
|
+
const claims = JSON.parse(
|
|
2603
|
+
new TextDecoder().decode(Uint8Array.from(atob(body), (c) => c.charCodeAt(0)))
|
|
2604
|
+
);
|
|
2605
|
+
const { use, sub, acc, chal } = claims;
|
|
2606
|
+
if (use !== LINK_GRANT_USE) return null;
|
|
2607
|
+
if (typeof sub !== "string" || typeof acc !== "string" || typeof chal !== "string") return null;
|
|
2608
|
+
return { login: sub, account: acc, challenge: chal };
|
|
2609
|
+
} catch {
|
|
2610
|
+
return null;
|
|
2611
|
+
}
|
|
2587
2612
|
}
|
|
2588
2613
|
function withoutLinkState(search) {
|
|
2589
2614
|
const params = new URLSearchParams(search);
|
|
@@ -9801,21 +9826,23 @@ function base643(bytes) {
|
|
|
9801
9826
|
|
|
9802
9827
|
// src/cloud/link-redirect.ts
|
|
9803
9828
|
var DEFAULT_CLOUD_WEB_URL = "https://cloud.calimero.network";
|
|
9804
|
-
function cloudLinkUrl(options) {
|
|
9829
|
+
async function cloudLinkUrl(options) {
|
|
9805
9830
|
const url2 = new URL(options.cloudWebUrl ?? DEFAULT_CLOUD_WEB_URL);
|
|
9831
|
+
const { callbackUrl, challenge } = await beginLinkState(options.accountId, options.returnTo);
|
|
9806
9832
|
url2.searchParams.set("link-account", options.accountId);
|
|
9807
|
-
url2.searchParams.set("
|
|
9833
|
+
url2.searchParams.set("challenge", challenge);
|
|
9834
|
+
url2.searchParams.set("callback-url", callbackUrl);
|
|
9808
9835
|
return url2.toString();
|
|
9809
9836
|
}
|
|
9810
|
-
function beginCloudLink(options) {
|
|
9811
|
-
window.location.assign(cloudLinkUrl(options));
|
|
9837
|
+
async function beginCloudLink(options) {
|
|
9838
|
+
window.location.assign(await cloudLinkUrl(options));
|
|
9812
9839
|
throw new Error("redirecting to the cloud to authorise this link");
|
|
9813
9840
|
}
|
|
9814
9841
|
function readCloudLinkCallback(location2 = window.location) {
|
|
9815
9842
|
const params = new URLSearchParams(location2.hash.replace(/^#/, ""));
|
|
9816
9843
|
const error = params.get("error");
|
|
9817
9844
|
if (error) {
|
|
9818
|
-
|
|
9845
|
+
takeLinkVerifier(location2.search ?? "", null);
|
|
9819
9846
|
stripFragment();
|
|
9820
9847
|
throw new Error(
|
|
9821
9848
|
error === "denied" ? "the account link was declined at the cloud" : `the cloud refused this link: ${error}`
|
|
@@ -9824,14 +9851,16 @@ function readCloudLinkCallback(location2 = window.location) {
|
|
|
9824
9851
|
const grant = params.get("grant");
|
|
9825
9852
|
const account = params.get("account");
|
|
9826
9853
|
if (!grant || !account) return null;
|
|
9827
|
-
|
|
9854
|
+
const verifier = takeLinkVerifier(location2.search ?? "", account);
|
|
9855
|
+
const claims = linkGrantClaims(grant);
|
|
9856
|
+
if (!verifier || claims?.account !== account) {
|
|
9828
9857
|
stripFragment();
|
|
9829
9858
|
throw new Error(
|
|
9830
9859
|
"this account link answers a request this browser did not make - start the link again from here"
|
|
9831
9860
|
);
|
|
9832
9861
|
}
|
|
9833
9862
|
stripFragment();
|
|
9834
|
-
return { grant, account };
|
|
9863
|
+
return { grant, account, login: claims.login, verifier };
|
|
9835
9864
|
}
|
|
9836
9865
|
function stripFragment() {
|
|
9837
9866
|
if (typeof history === "undefined" || !history.replaceState) return;
|
|
@@ -9848,7 +9877,11 @@ async function completeCloudLink(options) {
|
|
|
9848
9877
|
cloudBaseUrl: options.cloudBaseUrl,
|
|
9849
9878
|
fetch: options.fetch
|
|
9850
9879
|
});
|
|
9851
|
-
return cloud.linkAccountWithGrant({
|
|
9880
|
+
return cloud.linkAccountWithGrant({
|
|
9881
|
+
grant: options.grant,
|
|
9882
|
+
verifier: options.verifier,
|
|
9883
|
+
signer: root.signer
|
|
9884
|
+
});
|
|
9852
9885
|
}
|
|
9853
9886
|
|
|
9854
9887
|
// src/cloud/enrol-redirect.ts
|
|
@@ -11692,11 +11725,13 @@ var CloudClient = class {
|
|
|
11692
11725
|
* Static, so it needs no client and no session: there is nothing to
|
|
11693
11726
|
* authenticate as yet.
|
|
11694
11727
|
*/
|
|
11695
|
-
static accountLinkHandoff(options) {
|
|
11728
|
+
static async accountLinkHandoff(options) {
|
|
11696
11729
|
const portal = options.portalUrl.replace(/\/+$/, "");
|
|
11730
|
+
const { callbackUrl, challenge } = await beginLinkState(options.accountId, options.callbackUrl);
|
|
11697
11731
|
const query = new URLSearchParams({
|
|
11698
11732
|
"link-account": options.accountId,
|
|
11699
|
-
|
|
11733
|
+
challenge,
|
|
11734
|
+
"callback-url": callbackUrl
|
|
11700
11735
|
});
|
|
11701
11736
|
return { url: `${portal}/?${query.toString()}`, accountId: options.accountId };
|
|
11702
11737
|
}
|
|
@@ -11727,11 +11762,15 @@ var CloudClient = class {
|
|
|
11727
11762
|
const accountId = params.get("account");
|
|
11728
11763
|
const error = params.get("error");
|
|
11729
11764
|
const out = {};
|
|
11730
|
-
if (error)
|
|
11731
|
-
if (grant
|
|
11732
|
-
|
|
11765
|
+
if (error) takeLinkVerifier(search, null);
|
|
11766
|
+
if (grant) {
|
|
11767
|
+
const verifier = takeLinkVerifier(search, accountId);
|
|
11768
|
+
const claims = linkGrantClaims(grant);
|
|
11769
|
+
if (!verifier || claims?.account !== accountId) return { error: "unexpected_callback" };
|
|
11770
|
+
out.grant = grant;
|
|
11771
|
+
out.login = claims.login;
|
|
11772
|
+
out.verifier = verifier;
|
|
11733
11773
|
}
|
|
11734
|
-
if (grant) out.grant = grant;
|
|
11735
11774
|
if (accountId) out.accountId = accountId;
|
|
11736
11775
|
if (error) out.error = error;
|
|
11737
11776
|
return out;
|
|
@@ -11746,24 +11785,40 @@ var CloudClient = class {
|
|
|
11746
11785
|
*
|
|
11747
11786
|
* The signature is an ordinary account-link signature with the grant as the
|
|
11748
11787
|
* nonce, so the same {@link signAccountLink} every other link path uses signs
|
|
11749
|
-
* this one.
|
|
11750
|
-
*
|
|
11788
|
+
* this one. The grant is bound to the verifier of the browser that asked for
|
|
11789
|
+
* it, and is refused here, before the root signs, unless it carries that
|
|
11790
|
+
* verifier's challenge and names this root's account.
|
|
11751
11791
|
*
|
|
11752
11792
|
* A `403` means the grant was bad, expired, already spent, or signed by a key
|
|
11753
11793
|
* that names a different account; `409` that the account belongs to another
|
|
11754
11794
|
* login; `402` that the plan is full.
|
|
11755
11795
|
*/
|
|
11756
11796
|
async linkAccountWithGrant(options) {
|
|
11797
|
+
if (typeof options.verifier !== "string" || !VERIFIER_HEX.test(options.verifier)) {
|
|
11798
|
+
throw new Error("no verifier for this grant - start the link again from this browser");
|
|
11799
|
+
}
|
|
11800
|
+
const claims = linkGrantClaims(options.grant);
|
|
11801
|
+
if (claims?.challenge !== await linkChallenge(options.verifier)) {
|
|
11802
|
+
throw new Error(
|
|
11803
|
+
"this grant was not issued to this link request - start the link again from this browser"
|
|
11804
|
+
);
|
|
11805
|
+
}
|
|
11757
11806
|
const { signer, publicKey, accountId } = await resolveRootPair(
|
|
11758
11807
|
options.rootSecret,
|
|
11759
11808
|
options.signer
|
|
11760
11809
|
);
|
|
11810
|
+
if (claims.account !== accountId) {
|
|
11811
|
+
throw new Error(
|
|
11812
|
+
`this grant is for account ${claims.account}, but the root here derives ${accountId} - it was minted for a different account`
|
|
11813
|
+
);
|
|
11814
|
+
}
|
|
11761
11815
|
const body = await this.request(
|
|
11762
11816
|
"POST",
|
|
11763
11817
|
"/api/cloud/accounts/link",
|
|
11764
11818
|
{
|
|
11765
11819
|
body: {
|
|
11766
11820
|
grant: options.grant,
|
|
11821
|
+
verifier: options.verifier,
|
|
11767
11822
|
root_public_key: publicKey,
|
|
11768
11823
|
signature: await signAccountLink({
|
|
11769
11824
|
signer,
|