@calimero-network/mero-js 26.2.1 → 26.2.2
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/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.cjs
CHANGED
|
@@ -2830,6 +2830,9 @@ var STATE_PARAM = "link_state";
|
|
|
2830
2830
|
var LIFETIME_MS = 10 * 60 * 1e3;
|
|
2831
2831
|
var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
|
|
2832
2832
|
var MAX_PENDING = 20;
|
|
2833
|
+
var VERIFIER_BYTES = 32;
|
|
2834
|
+
var VERIFIER_HEX = /^[0-9a-f]{64}$/;
|
|
2835
|
+
var LINK_GRANT_USE = "account-link-grant";
|
|
2833
2836
|
function readPending(key, now) {
|
|
2834
2837
|
try {
|
|
2835
2838
|
const raw = localStorage.getItem(key);
|
|
@@ -2852,10 +2855,7 @@ function writePending(key, pending) {
|
|
|
2852
2855
|
function beginState(key, payload) {
|
|
2853
2856
|
const now = Date.now();
|
|
2854
2857
|
const pending = readPending(key, now);
|
|
2855
|
-
const
|
|
2856
|
-
const state = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
|
|
2857
|
-
""
|
|
2858
|
-
);
|
|
2858
|
+
const state = hex(crypto.getRandomValues(new Uint8Array(16)));
|
|
2859
2859
|
pending[state] = { ...payload, expiresAt: now + LIFETIME_MS };
|
|
2860
2860
|
writePending(key, Object.fromEntries(Object.entries(pending).slice(-MAX_PENDING)));
|
|
2861
2861
|
return state;
|
|
@@ -2869,16 +2869,41 @@ function takeState(key, state) {
|
|
|
2869
2869
|
writePending(key, pending);
|
|
2870
2870
|
return entry;
|
|
2871
2871
|
}
|
|
2872
|
-
function
|
|
2872
|
+
async function linkChallenge(verifier) {
|
|
2873
|
+
const bytes = Uint8Array.from(verifier.match(/../g) ?? [], (b) => parseInt(b, 16));
|
|
2874
|
+
return hex(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)));
|
|
2875
|
+
}
|
|
2876
|
+
async function beginLinkState(account, returnTo) {
|
|
2873
2877
|
if (!ACCOUNT_HEX.test(account)) {
|
|
2874
2878
|
throw new Error("accountId must be 64 lowercase hex characters");
|
|
2875
2879
|
}
|
|
2876
2880
|
const url2 = new URL(returnTo);
|
|
2877
|
-
|
|
2878
|
-
|
|
2881
|
+
const verifier = hex(crypto.getRandomValues(new Uint8Array(VERIFIER_BYTES)));
|
|
2882
|
+
const challenge = await linkChallenge(verifier);
|
|
2883
|
+
url2.searchParams.set(STATE_PARAM, beginState(KEY, { account, verifier }));
|
|
2884
|
+
return { callbackUrl: url2.toString(), challenge };
|
|
2885
|
+
}
|
|
2886
|
+
function takeLinkVerifier(search, account) {
|
|
2887
|
+
const pending = takeState(
|
|
2888
|
+
KEY,
|
|
2889
|
+
new URLSearchParams(search).get(STATE_PARAM)
|
|
2890
|
+
);
|
|
2891
|
+
if (!pending || pending.account !== account) return null;
|
|
2892
|
+
return typeof pending.verifier === "string" && VERIFIER_HEX.test(pending.verifier) ? pending.verifier : null;
|
|
2879
2893
|
}
|
|
2880
|
-
function
|
|
2881
|
-
|
|
2894
|
+
function linkGrantClaims(grant) {
|
|
2895
|
+
try {
|
|
2896
|
+
const body = grant.split(".")[0].replace(/-/g, "+").replace(/_/g, "/");
|
|
2897
|
+
const claims = JSON.parse(
|
|
2898
|
+
new TextDecoder().decode(Uint8Array.from(atob(body), (c) => c.charCodeAt(0)))
|
|
2899
|
+
);
|
|
2900
|
+
const { use, sub, acc, chal } = claims;
|
|
2901
|
+
if (use !== LINK_GRANT_USE) return null;
|
|
2902
|
+
if (typeof sub !== "string" || typeof acc !== "string" || typeof chal !== "string") return null;
|
|
2903
|
+
return { login: sub, account: acc, challenge: chal };
|
|
2904
|
+
} catch {
|
|
2905
|
+
return null;
|
|
2906
|
+
}
|
|
2882
2907
|
}
|
|
2883
2908
|
function withoutLinkState(search) {
|
|
2884
2909
|
const params = new URLSearchParams(search);
|
|
@@ -10096,21 +10121,23 @@ function base643(bytes) {
|
|
|
10096
10121
|
|
|
10097
10122
|
// src/cloud/link-redirect.ts
|
|
10098
10123
|
var DEFAULT_CLOUD_WEB_URL = "https://cloud.calimero.network";
|
|
10099
|
-
function cloudLinkUrl(options) {
|
|
10124
|
+
async function cloudLinkUrl(options) {
|
|
10100
10125
|
const url2 = new URL(options.cloudWebUrl ?? DEFAULT_CLOUD_WEB_URL);
|
|
10126
|
+
const { callbackUrl, challenge } = await beginLinkState(options.accountId, options.returnTo);
|
|
10101
10127
|
url2.searchParams.set("link-account", options.accountId);
|
|
10102
|
-
url2.searchParams.set("
|
|
10128
|
+
url2.searchParams.set("challenge", challenge);
|
|
10129
|
+
url2.searchParams.set("callback-url", callbackUrl);
|
|
10103
10130
|
return url2.toString();
|
|
10104
10131
|
}
|
|
10105
|
-
function beginCloudLink(options) {
|
|
10106
|
-
window.location.assign(cloudLinkUrl(options));
|
|
10132
|
+
async function beginCloudLink(options) {
|
|
10133
|
+
window.location.assign(await cloudLinkUrl(options));
|
|
10107
10134
|
throw new Error("redirecting to the cloud to authorise this link");
|
|
10108
10135
|
}
|
|
10109
10136
|
function readCloudLinkCallback(location2 = window.location) {
|
|
10110
10137
|
const params = new URLSearchParams(location2.hash.replace(/^#/, ""));
|
|
10111
10138
|
const error = params.get("error");
|
|
10112
10139
|
if (error) {
|
|
10113
|
-
|
|
10140
|
+
takeLinkVerifier(location2.search ?? "", null);
|
|
10114
10141
|
stripFragment();
|
|
10115
10142
|
throw new Error(
|
|
10116
10143
|
error === "denied" ? "the account link was declined at the cloud" : `the cloud refused this link: ${error}`
|
|
@@ -10119,14 +10146,16 @@ function readCloudLinkCallback(location2 = window.location) {
|
|
|
10119
10146
|
const grant = params.get("grant");
|
|
10120
10147
|
const account = params.get("account");
|
|
10121
10148
|
if (!grant || !account) return null;
|
|
10122
|
-
|
|
10149
|
+
const verifier = takeLinkVerifier(location2.search ?? "", account);
|
|
10150
|
+
const claims = linkGrantClaims(grant);
|
|
10151
|
+
if (!verifier || claims?.account !== account) {
|
|
10123
10152
|
stripFragment();
|
|
10124
10153
|
throw new Error(
|
|
10125
10154
|
"this account link answers a request this browser did not make - start the link again from here"
|
|
10126
10155
|
);
|
|
10127
10156
|
}
|
|
10128
10157
|
stripFragment();
|
|
10129
|
-
return { grant, account };
|
|
10158
|
+
return { grant, account, login: claims.login, verifier };
|
|
10130
10159
|
}
|
|
10131
10160
|
function stripFragment() {
|
|
10132
10161
|
if (typeof history === "undefined" || !history.replaceState) return;
|
|
@@ -10143,7 +10172,11 @@ async function completeCloudLink(options) {
|
|
|
10143
10172
|
cloudBaseUrl: options.cloudBaseUrl,
|
|
10144
10173
|
fetch: options.fetch
|
|
10145
10174
|
});
|
|
10146
|
-
return cloud.linkAccountWithGrant({
|
|
10175
|
+
return cloud.linkAccountWithGrant({
|
|
10176
|
+
grant: options.grant,
|
|
10177
|
+
verifier: options.verifier,
|
|
10178
|
+
signer: root.signer
|
|
10179
|
+
});
|
|
10147
10180
|
}
|
|
10148
10181
|
|
|
10149
10182
|
// src/cloud/enrol-redirect.ts
|
|
@@ -11987,11 +12020,13 @@ var CloudClient = class {
|
|
|
11987
12020
|
* Static, so it needs no client and no session: there is nothing to
|
|
11988
12021
|
* authenticate as yet.
|
|
11989
12022
|
*/
|
|
11990
|
-
static accountLinkHandoff(options) {
|
|
12023
|
+
static async accountLinkHandoff(options) {
|
|
11991
12024
|
const portal = options.portalUrl.replace(/\/+$/, "");
|
|
12025
|
+
const { callbackUrl, challenge } = await beginLinkState(options.accountId, options.callbackUrl);
|
|
11992
12026
|
const query = new URLSearchParams({
|
|
11993
12027
|
"link-account": options.accountId,
|
|
11994
|
-
|
|
12028
|
+
challenge,
|
|
12029
|
+
"callback-url": callbackUrl
|
|
11995
12030
|
});
|
|
11996
12031
|
return { url: `${portal}/?${query.toString()}`, accountId: options.accountId };
|
|
11997
12032
|
}
|
|
@@ -12022,11 +12057,15 @@ var CloudClient = class {
|
|
|
12022
12057
|
const accountId = params.get("account");
|
|
12023
12058
|
const error = params.get("error");
|
|
12024
12059
|
const out = {};
|
|
12025
|
-
if (error)
|
|
12026
|
-
if (grant
|
|
12027
|
-
|
|
12060
|
+
if (error) takeLinkVerifier(search, null);
|
|
12061
|
+
if (grant) {
|
|
12062
|
+
const verifier = takeLinkVerifier(search, accountId);
|
|
12063
|
+
const claims = linkGrantClaims(grant);
|
|
12064
|
+
if (!verifier || claims?.account !== accountId) return { error: "unexpected_callback" };
|
|
12065
|
+
out.grant = grant;
|
|
12066
|
+
out.login = claims.login;
|
|
12067
|
+
out.verifier = verifier;
|
|
12028
12068
|
}
|
|
12029
|
-
if (grant) out.grant = grant;
|
|
12030
12069
|
if (accountId) out.accountId = accountId;
|
|
12031
12070
|
if (error) out.error = error;
|
|
12032
12071
|
return out;
|
|
@@ -12041,24 +12080,40 @@ var CloudClient = class {
|
|
|
12041
12080
|
*
|
|
12042
12081
|
* The signature is an ordinary account-link signature with the grant as the
|
|
12043
12082
|
* nonce, so the same {@link signAccountLink} every other link path uses signs
|
|
12044
|
-
* this one.
|
|
12045
|
-
*
|
|
12083
|
+
* this one. The grant is bound to the verifier of the browser that asked for
|
|
12084
|
+
* it, and is refused here, before the root signs, unless it carries that
|
|
12085
|
+
* verifier's challenge and names this root's account.
|
|
12046
12086
|
*
|
|
12047
12087
|
* A `403` means the grant was bad, expired, already spent, or signed by a key
|
|
12048
12088
|
* that names a different account; `409` that the account belongs to another
|
|
12049
12089
|
* login; `402` that the plan is full.
|
|
12050
12090
|
*/
|
|
12051
12091
|
async linkAccountWithGrant(options) {
|
|
12092
|
+
if (typeof options.verifier !== "string" || !VERIFIER_HEX.test(options.verifier)) {
|
|
12093
|
+
throw new Error("no verifier for this grant - start the link again from this browser");
|
|
12094
|
+
}
|
|
12095
|
+
const claims = linkGrantClaims(options.grant);
|
|
12096
|
+
if (claims?.challenge !== await linkChallenge(options.verifier)) {
|
|
12097
|
+
throw new Error(
|
|
12098
|
+
"this grant was not issued to this link request - start the link again from this browser"
|
|
12099
|
+
);
|
|
12100
|
+
}
|
|
12052
12101
|
const { signer, publicKey, accountId } = await resolveRootPair(
|
|
12053
12102
|
options.rootSecret,
|
|
12054
12103
|
options.signer
|
|
12055
12104
|
);
|
|
12105
|
+
if (claims.account !== accountId) {
|
|
12106
|
+
throw new Error(
|
|
12107
|
+
`this grant is for account ${claims.account}, but the root here derives ${accountId} - it was minted for a different account`
|
|
12108
|
+
);
|
|
12109
|
+
}
|
|
12056
12110
|
const body = await this.request(
|
|
12057
12111
|
"POST",
|
|
12058
12112
|
"/api/cloud/accounts/link",
|
|
12059
12113
|
{
|
|
12060
12114
|
body: {
|
|
12061
12115
|
grant: options.grant,
|
|
12116
|
+
verifier: options.verifier,
|
|
12062
12117
|
root_public_key: publicKey,
|
|
12063
12118
|
signature: await signAccountLink({
|
|
12064
12119
|
signer,
|