@calimero-network/mero-js 25.0.2 → 25.0.4
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/account/account-admin.d.ts +2 -4
- package/dist/account/account-admin.js +6 -22
- package/dist/account/bootstrap-from-invitation.d.ts +6 -13
- package/dist/account/bootstrap-from-invitation.js +4 -10
- package/dist/account/create-context.d.ts +1 -1
- package/dist/account/create-context.js +3 -4
- package/dist/account/join-with-node.d.ts +2 -6
- package/dist/account/relay-from-invitation.d.ts +5 -25
- package/dist/account/relay-from-invitation.js +0 -38
- package/dist/cloud/cloud-client.d.ts +7 -4
- package/dist/cloud/cloud-client.js +15 -6
- package/dist/cloud/link-redirect.d.ts +4 -3
- package/dist/cloud/link-redirect.js +11 -8
- package/dist/cloud/link-state.d.ts +13 -0
- package/dist/cloud/link-state.js +60 -0
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +87 -52
- package/dist/index.cjs.map +4 -4
- package/dist/index.mjs +87 -52
- package/dist/index.mjs.map +4 -4
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -9906,15 +9906,70 @@ function base643(bytes) {
|
|
|
9906
9906
|
return btoa(binary);
|
|
9907
9907
|
}
|
|
9908
9908
|
|
|
9909
|
+
// src/cloud/link-state.ts
|
|
9910
|
+
var KEY2 = "calimero.cloud.link.pending";
|
|
9911
|
+
var STATE_PARAM = "link_state";
|
|
9912
|
+
var LIFETIME_MS = 10 * 60 * 1e3;
|
|
9913
|
+
var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
|
|
9914
|
+
var MAX_PENDING = 20;
|
|
9915
|
+
function readPending(now) {
|
|
9916
|
+
try {
|
|
9917
|
+
const raw = localStorage.getItem(KEY2);
|
|
9918
|
+
const pending = raw ? JSON.parse(raw) : {};
|
|
9919
|
+
return Object.fromEntries(
|
|
9920
|
+
Object.entries(pending).filter(
|
|
9921
|
+
([, p]) => p && typeof p.expiresAt === "number" && p.expiresAt > now
|
|
9922
|
+
)
|
|
9923
|
+
);
|
|
9924
|
+
} catch {
|
|
9925
|
+
return {};
|
|
9926
|
+
}
|
|
9927
|
+
}
|
|
9928
|
+
function writePending(pending) {
|
|
9929
|
+
try {
|
|
9930
|
+
localStorage.setItem(KEY2, JSON.stringify(pending));
|
|
9931
|
+
} catch {
|
|
9932
|
+
}
|
|
9933
|
+
}
|
|
9934
|
+
function beginLinkState(account, returnTo) {
|
|
9935
|
+
if (!ACCOUNT_HEX.test(account)) {
|
|
9936
|
+
throw new Error("accountId must be 64 lowercase hex characters");
|
|
9937
|
+
}
|
|
9938
|
+
const url = new URL(returnTo);
|
|
9939
|
+
const now = Date.now();
|
|
9940
|
+
const pending = readPending(now);
|
|
9941
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
9942
|
+
const state = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
|
|
9943
|
+
""
|
|
9944
|
+
);
|
|
9945
|
+
pending[state] = { account, expiresAt: now + LIFETIME_MS };
|
|
9946
|
+
writePending(Object.fromEntries(Object.entries(pending).slice(-MAX_PENDING)));
|
|
9947
|
+
url.searchParams.set(STATE_PARAM, state);
|
|
9948
|
+
return url.toString();
|
|
9949
|
+
}
|
|
9950
|
+
function takeLinkState(search) {
|
|
9951
|
+
const state = new URLSearchParams(search).get(STATE_PARAM);
|
|
9952
|
+
const pending = readPending(Date.now());
|
|
9953
|
+
if (!state || !Object.prototype.hasOwnProperty.call(pending, state))
|
|
9954
|
+
return null;
|
|
9955
|
+
const entry = pending[state];
|
|
9956
|
+
delete pending[state];
|
|
9957
|
+
writePending(pending);
|
|
9958
|
+
return entry;
|
|
9959
|
+
}
|
|
9960
|
+
function withoutLinkState(search) {
|
|
9961
|
+
const params = new URLSearchParams(search);
|
|
9962
|
+
params.delete(STATE_PARAM);
|
|
9963
|
+
const rest = params.toString();
|
|
9964
|
+
return rest ? `?${rest}` : "";
|
|
9965
|
+
}
|
|
9966
|
+
|
|
9909
9967
|
// src/cloud/link-redirect.ts
|
|
9910
9968
|
var DEFAULT_CLOUD_WEB_URL = "https://cloud.calimero.network";
|
|
9911
9969
|
function cloudLinkUrl(options) {
|
|
9912
|
-
if (!/^[0-9a-f]{64}$/.test(options.accountId)) {
|
|
9913
|
-
throw new Error("accountId must be 64 lowercase hex characters");
|
|
9914
|
-
}
|
|
9915
9970
|
const url = new URL(options.cloudWebUrl ?? DEFAULT_CLOUD_WEB_URL);
|
|
9916
9971
|
url.searchParams.set("link-account", options.accountId);
|
|
9917
|
-
url.searchParams.set("callback-url", options.returnTo);
|
|
9972
|
+
url.searchParams.set("callback-url", beginLinkState(options.accountId, options.returnTo));
|
|
9918
9973
|
return url.toString();
|
|
9919
9974
|
}
|
|
9920
9975
|
function beginCloudLink(options) {
|
|
@@ -9925,6 +9980,7 @@ function readCloudLinkCallback(location2 = window.location) {
|
|
|
9925
9980
|
const params = new URLSearchParams(location2.hash.replace(/^#/, ""));
|
|
9926
9981
|
const error = params.get("error");
|
|
9927
9982
|
if (error) {
|
|
9983
|
+
takeLinkState(location2.search ?? "");
|
|
9928
9984
|
stripFragment();
|
|
9929
9985
|
throw new Error(
|
|
9930
9986
|
error === "denied" ? "the account link was declined at the cloud" : `the cloud refused this link: ${error}`
|
|
@@ -9933,12 +9989,18 @@ function readCloudLinkCallback(location2 = window.location) {
|
|
|
9933
9989
|
const grant = params.get("grant");
|
|
9934
9990
|
const account = params.get("account");
|
|
9935
9991
|
if (!grant || !account) return null;
|
|
9992
|
+
if (takeLinkState(location2.search ?? "")?.account !== account) {
|
|
9993
|
+
stripFragment();
|
|
9994
|
+
throw new Error(
|
|
9995
|
+
"this account link answers a request this browser did not make - start the link again from here"
|
|
9996
|
+
);
|
|
9997
|
+
}
|
|
9936
9998
|
stripFragment();
|
|
9937
9999
|
return { grant, account };
|
|
9938
10000
|
}
|
|
9939
10001
|
function stripFragment() {
|
|
9940
10002
|
if (typeof history === "undefined" || !history.replaceState) return;
|
|
9941
|
-
history.replaceState(null, "", location.pathname + location.search);
|
|
10003
|
+
history.replaceState(null, "", location.pathname + withoutLinkState(location.search));
|
|
9942
10004
|
}
|
|
9943
10005
|
async function completeCloudLink(options) {
|
|
9944
10006
|
const root = await resolveRoot(options.root);
|
|
@@ -9958,7 +10020,7 @@ async function completeCloudLink(options) {
|
|
|
9958
10020
|
var DEVICE_PARAM = "enrol-device";
|
|
9959
10021
|
var KEM_PARAM = "enrol-kem";
|
|
9960
10022
|
var CALLBACK_PARAM = "callback-url";
|
|
9961
|
-
var
|
|
10023
|
+
var STATE_PARAM2 = "state";
|
|
9962
10024
|
function deviceEnrolmentUrl(options) {
|
|
9963
10025
|
assertHex(options.devicePublicKey, "devicePublicKey");
|
|
9964
10026
|
assertHex(options.kemPublicKey, "kemPublicKey");
|
|
@@ -9968,7 +10030,7 @@ function deviceEnrolmentUrl(options) {
|
|
|
9968
10030
|
url.searchParams.set(DEVICE_PARAM, options.devicePublicKey);
|
|
9969
10031
|
url.searchParams.set(KEM_PARAM, options.kemPublicKey);
|
|
9970
10032
|
url.searchParams.set(CALLBACK_PARAM, options.returnTo);
|
|
9971
|
-
if (options.state) url.searchParams.set(
|
|
10033
|
+
if (options.state) url.searchParams.set(STATE_PARAM2, options.state);
|
|
9972
10034
|
return url.toString();
|
|
9973
10035
|
}
|
|
9974
10036
|
function beginDeviceEnrolment(options) {
|
|
@@ -10075,17 +10137,6 @@ function normaliseAccount(account) {
|
|
|
10075
10137
|
const bare = trimmed.startsWith("0x") ? trimmed.slice(2) : trimmed;
|
|
10076
10138
|
return bare.length > 0 ? bare : null;
|
|
10077
10139
|
}
|
|
10078
|
-
function httpAdmitterOrigins(addrs) {
|
|
10079
|
-
const out = [];
|
|
10080
|
-
for (const addr of addrs ?? []) {
|
|
10081
|
-
try {
|
|
10082
|
-
const url = new URL(addr);
|
|
10083
|
-
if (url.protocol === "http:" || url.protocol === "https:") out.push(addr.replace(/\/+$/, ""));
|
|
10084
|
-
} catch {
|
|
10085
|
-
}
|
|
10086
|
-
}
|
|
10087
|
-
return out;
|
|
10088
|
-
}
|
|
10089
10140
|
function isInvited(node, invited) {
|
|
10090
10141
|
if (invited.size === 0) return true;
|
|
10091
10142
|
const account = normaliseAccount(node.account);
|
|
@@ -10110,23 +10161,6 @@ async function resolveRelayFromInvitation(input) {
|
|
|
10110
10161
|
};
|
|
10111
10162
|
}
|
|
10112
10163
|
if (nodes.length === 0) {
|
|
10113
|
-
const [relayUrl] = httpAdmitterOrigins(input.admitterAddrs);
|
|
10114
|
-
if (relayUrl) {
|
|
10115
|
-
const admitters = (input.admitters ?? []).map(normaliseAccount).filter((a) => a !== null);
|
|
10116
|
-
return {
|
|
10117
|
-
ok: true,
|
|
10118
|
-
relayUrl,
|
|
10119
|
-
admitUrl: `${relayUrl}/admin-api/namespaces/${input.namespaceId}/admit`,
|
|
10120
|
-
admitterAccount: admitters.length === 1 ? admitters[0] : null,
|
|
10121
|
-
peerId: null,
|
|
10122
|
-
// The relay a founder's session runs through executes that founder's
|
|
10123
|
-
// warrants, so it authors on behalf; the node's answer to the first
|
|
10124
|
-
// write is the authoritative one, as for a cloud-routed node.
|
|
10125
|
-
writable: true,
|
|
10126
|
-
stale: false,
|
|
10127
|
-
via: "invitation"
|
|
10128
|
-
};
|
|
10129
|
-
}
|
|
10130
10164
|
return {
|
|
10131
10165
|
ok: false,
|
|
10132
10166
|
step: "no-nodes",
|
|
@@ -10170,8 +10204,7 @@ async function resolveRelayFromInvitation(input) {
|
|
|
10170
10204
|
// Stale when the node's own heartbeat lapsed OR when the cloud's summary
|
|
10171
10205
|
// disagrees with trying it at all. Both mean the same thing to a caller:
|
|
10172
10206
|
// this was attempted on advisory data, so if the admit fails that is why.
|
|
10173
|
-
stale: !chosen.fresh || advisory
|
|
10174
|
-
via: "cloud"
|
|
10207
|
+
stale: !chosen.fresh || advisory
|
|
10175
10208
|
};
|
|
10176
10209
|
}
|
|
10177
10210
|
|
|
@@ -10457,7 +10490,6 @@ async function bootstrapFromInvitation(input) {
|
|
|
10457
10490
|
// separately: two sources for one fact is how an intersection ends up
|
|
10458
10491
|
// being performed against the wrong list.
|
|
10459
10492
|
admitters: input.invitation.invitation.admitters,
|
|
10460
|
-
admitterAddrs: input.invitation.admitter_addrs,
|
|
10461
10493
|
credential: input.credential.credential,
|
|
10462
10494
|
deviceSecret: input.credential.deviceSecret,
|
|
10463
10495
|
cloudBaseUrl: input.cloudBaseUrl
|
|
@@ -10676,7 +10708,7 @@ async function attestedNodeKey(relay) {
|
|
|
10676
10708
|
key.kind === "unavailable" ? "the relay's signing key could not be learned: its attestation did not answer, try again" : "the relay's attestation was refused, so founding through it is not possible: it is not a signed locked-read-only release"
|
|
10677
10709
|
);
|
|
10678
10710
|
}
|
|
10679
|
-
var HA_ACCOUNT_NOT_LINKED_MESSAGE = "link this account to your cloud user in the wallet so
|
|
10711
|
+
var HA_ACCOUNT_NOT_LINKED_MESSAGE = "link this account to your cloud user in the wallet so the cloud can host this namespace on its fleet";
|
|
10680
10712
|
var HA_REFUSAL_MESSAGES = {
|
|
10681
10713
|
account_not_linked: HA_ACCOUNT_NOT_LINKED_MESSAGE,
|
|
10682
10714
|
account_linked_to_several_users: "this account is linked to more than one cloud user, so the cloud cannot tell whose plan hosts the namespace: unlink it from all but one in the wallet",
|
|
@@ -10781,7 +10813,7 @@ var NoRelayError = class extends Error {
|
|
|
10781
10813
|
var InvitationNotClaimableError = class extends Error {
|
|
10782
10814
|
constructor(namespaceId, reason) {
|
|
10783
10815
|
super(
|
|
10784
|
-
reason === "not-hosted" ? `nobody could claim an invitation to ${namespaceId} yet: the
|
|
10816
|
+
reason === "not-hosted" ? `nobody could claim an invitation to ${namespaceId} yet: the cloud lists no node holding the namespace, so an invitee with no node of their own has no relay to be admitted through.` : `nobody could claim an invitation to ${namespaceId}: the cloud routes to none of the relays or admins it would name as admitters`
|
|
10785
10817
|
);
|
|
10786
10818
|
this.namespaceId = namespaceId;
|
|
10787
10819
|
this.reason = reason;
|
|
@@ -10969,7 +11001,6 @@ function createAccountAdmin(input, deps = {}) {
|
|
|
10969
11001
|
async createNamespaceInvitation(namespaceId) {
|
|
10970
11002
|
const [{ members }, info] = await Promise.all([relay().listGroupMembers(namespaceId), relay().getGroupInfo(namespaceId)]);
|
|
10971
11003
|
const relays = members.filter((m) => m.role === "RelayTee").map((m) => m.identity.toLowerCase());
|
|
10972
|
-
let admitterAddrs;
|
|
10973
11004
|
if (routing) {
|
|
10974
11005
|
const named = new Set(
|
|
10975
11006
|
relays.length > 0 ? relays : members.filter((m) => m.role === "Admin").map((m) => m.identity.toLowerCase())
|
|
@@ -10980,10 +11011,8 @@ function createAccountAdmin(input, deps = {}) {
|
|
|
10980
11011
|
} catch {
|
|
10981
11012
|
}
|
|
10982
11013
|
if (nodes !== null) {
|
|
10983
|
-
if (nodes.length === 0)
|
|
10984
|
-
|
|
10985
|
-
admitterAddrs = [s.relayUrl];
|
|
10986
|
-
} else if (!nodes.some((n) => n.account !== null && named.has(n.account.toLowerCase()))) {
|
|
11014
|
+
if (nodes.length === 0) throw new InvitationNotClaimableError(namespaceId, "not-hosted");
|
|
11015
|
+
if (!nodes.some((n) => n.account !== null && named.has(n.account.toLowerCase()))) {
|
|
10987
11016
|
throw new InvitationNotClaimableError(namespaceId, "no-named-node");
|
|
10988
11017
|
}
|
|
10989
11018
|
}
|
|
@@ -10993,7 +11022,6 @@ function createAccountAdmin(input, deps = {}) {
|
|
|
10993
11022
|
inviterAccount: s.account,
|
|
10994
11023
|
deviceSecret: s.deviceSecret,
|
|
10995
11024
|
...relays.length > 0 ? { admitters: relays } : {},
|
|
10996
|
-
...admitterAddrs ? { admitterAddrs } : {},
|
|
10997
11025
|
members,
|
|
10998
11026
|
applicationId: info.targetApplicationId,
|
|
10999
11027
|
appKey: info.appKey
|
|
@@ -11100,7 +11128,6 @@ function joinOf(row) {
|
|
|
11100
11128
|
reportedAt: typeof join.reported_at === "string" ? join.reported_at : null
|
|
11101
11129
|
};
|
|
11102
11130
|
}
|
|
11103
|
-
var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
|
|
11104
11131
|
function accountHexOrNull(value) {
|
|
11105
11132
|
return typeof value === "string" && ACCOUNT_HEX.test(value) ? value : null;
|
|
11106
11133
|
}
|
|
@@ -11374,9 +11401,8 @@ var CloudClient = class {
|
|
|
11374
11401
|
* are answered together rather than across two endpoints — the per-context
|
|
11375
11402
|
* read needs a context id a joiner does not yet have.
|
|
11376
11403
|
*
|
|
11377
|
-
* An empty `nodes` list is a state, not an error:
|
|
11378
|
-
*
|
|
11379
|
-
* invitation's other admitters — another admin's node, or a self-hosted peer.
|
|
11404
|
+
* An empty `nodes` list is a state, not an error: the cloud has no record of
|
|
11405
|
+
* a node holding the namespace, so there is no hosted admitter.
|
|
11380
11406
|
*/
|
|
11381
11407
|
async getNamespaceRouting(namespaceId) {
|
|
11382
11408
|
const body = await this.request("GET", `/api/cloud/namespaces/${encodeURIComponent(namespaceId)}/admitters`, {
|
|
@@ -11829,7 +11855,7 @@ var CloudClient = class {
|
|
|
11829
11855
|
const portal = options.portalUrl.replace(/\/+$/, "");
|
|
11830
11856
|
const query = new URLSearchParams({
|
|
11831
11857
|
"link-account": options.accountId,
|
|
11832
|
-
"callback-url": options.callbackUrl
|
|
11858
|
+
"callback-url": beginLinkState(options.accountId, options.callbackUrl)
|
|
11833
11859
|
});
|
|
11834
11860
|
return { url: `${portal}/?${query.toString()}`, accountId: options.accountId };
|
|
11835
11861
|
}
|
|
@@ -11839,14 +11865,19 @@ var CloudClient = class {
|
|
|
11839
11865
|
* Returns `{}` when there is nothing to read, so it is safe to call on every
|
|
11840
11866
|
* load — which is how a redirected app finds out it has been sent back.
|
|
11841
11867
|
*
|
|
11868
|
+
* A grant is only returned when it answers a handoff this browser began, for the
|
|
11869
|
+
* account that handoff named; anything else comes back as
|
|
11870
|
+
* `error: 'unexpected_callback'`, because the redirect URL can be forwarded.
|
|
11871
|
+
*
|
|
11842
11872
|
* @param href defaults to the current location, when there is one.
|
|
11843
11873
|
*/
|
|
11844
11874
|
static readAccountLinkCallback(href) {
|
|
11845
11875
|
const source = href ?? (typeof window === "undefined" ? "" : window.location.href);
|
|
11846
11876
|
if (!source) return {};
|
|
11847
11877
|
let hash;
|
|
11878
|
+
let search;
|
|
11848
11879
|
try {
|
|
11849
|
-
hash = new URL(source)
|
|
11880
|
+
({ hash, search } = new URL(source));
|
|
11850
11881
|
} catch {
|
|
11851
11882
|
return {};
|
|
11852
11883
|
}
|
|
@@ -11855,6 +11886,10 @@ var CloudClient = class {
|
|
|
11855
11886
|
const accountId = params.get("account");
|
|
11856
11887
|
const error = params.get("error");
|
|
11857
11888
|
const out = {};
|
|
11889
|
+
if (error) takeLinkState(search);
|
|
11890
|
+
if (grant && takeLinkState(search)?.account !== accountId) {
|
|
11891
|
+
return { error: "unexpected_callback" };
|
|
11892
|
+
}
|
|
11858
11893
|
if (grant) out.grant = grant;
|
|
11859
11894
|
if (accountId) out.accountId = accountId;
|
|
11860
11895
|
if (error) out.error = error;
|