@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.cjs
CHANGED
|
@@ -10191,15 +10191,70 @@ function base643(bytes) {
|
|
|
10191
10191
|
return btoa(binary);
|
|
10192
10192
|
}
|
|
10193
10193
|
|
|
10194
|
+
// src/cloud/link-state.ts
|
|
10195
|
+
var KEY2 = "calimero.cloud.link.pending";
|
|
10196
|
+
var STATE_PARAM = "link_state";
|
|
10197
|
+
var LIFETIME_MS = 10 * 60 * 1e3;
|
|
10198
|
+
var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
|
|
10199
|
+
var MAX_PENDING = 20;
|
|
10200
|
+
function readPending(now) {
|
|
10201
|
+
try {
|
|
10202
|
+
const raw = localStorage.getItem(KEY2);
|
|
10203
|
+
const pending = raw ? JSON.parse(raw) : {};
|
|
10204
|
+
return Object.fromEntries(
|
|
10205
|
+
Object.entries(pending).filter(
|
|
10206
|
+
([, p]) => p && typeof p.expiresAt === "number" && p.expiresAt > now
|
|
10207
|
+
)
|
|
10208
|
+
);
|
|
10209
|
+
} catch {
|
|
10210
|
+
return {};
|
|
10211
|
+
}
|
|
10212
|
+
}
|
|
10213
|
+
function writePending(pending) {
|
|
10214
|
+
try {
|
|
10215
|
+
localStorage.setItem(KEY2, JSON.stringify(pending));
|
|
10216
|
+
} catch {
|
|
10217
|
+
}
|
|
10218
|
+
}
|
|
10219
|
+
function beginLinkState(account, returnTo) {
|
|
10220
|
+
if (!ACCOUNT_HEX.test(account)) {
|
|
10221
|
+
throw new Error("accountId must be 64 lowercase hex characters");
|
|
10222
|
+
}
|
|
10223
|
+
const url = new URL(returnTo);
|
|
10224
|
+
const now = Date.now();
|
|
10225
|
+
const pending = readPending(now);
|
|
10226
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
10227
|
+
const state = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
|
|
10228
|
+
""
|
|
10229
|
+
);
|
|
10230
|
+
pending[state] = { account, expiresAt: now + LIFETIME_MS };
|
|
10231
|
+
writePending(Object.fromEntries(Object.entries(pending).slice(-MAX_PENDING)));
|
|
10232
|
+
url.searchParams.set(STATE_PARAM, state);
|
|
10233
|
+
return url.toString();
|
|
10234
|
+
}
|
|
10235
|
+
function takeLinkState(search) {
|
|
10236
|
+
const state = new URLSearchParams(search).get(STATE_PARAM);
|
|
10237
|
+
const pending = readPending(Date.now());
|
|
10238
|
+
if (!state || !Object.prototype.hasOwnProperty.call(pending, state))
|
|
10239
|
+
return null;
|
|
10240
|
+
const entry = pending[state];
|
|
10241
|
+
delete pending[state];
|
|
10242
|
+
writePending(pending);
|
|
10243
|
+
return entry;
|
|
10244
|
+
}
|
|
10245
|
+
function withoutLinkState(search) {
|
|
10246
|
+
const params = new URLSearchParams(search);
|
|
10247
|
+
params.delete(STATE_PARAM);
|
|
10248
|
+
const rest = params.toString();
|
|
10249
|
+
return rest ? `?${rest}` : "";
|
|
10250
|
+
}
|
|
10251
|
+
|
|
10194
10252
|
// src/cloud/link-redirect.ts
|
|
10195
10253
|
var DEFAULT_CLOUD_WEB_URL = "https://cloud.calimero.network";
|
|
10196
10254
|
function cloudLinkUrl(options) {
|
|
10197
|
-
if (!/^[0-9a-f]{64}$/.test(options.accountId)) {
|
|
10198
|
-
throw new Error("accountId must be 64 lowercase hex characters");
|
|
10199
|
-
}
|
|
10200
10255
|
const url = new URL(options.cloudWebUrl ?? DEFAULT_CLOUD_WEB_URL);
|
|
10201
10256
|
url.searchParams.set("link-account", options.accountId);
|
|
10202
|
-
url.searchParams.set("callback-url", options.returnTo);
|
|
10257
|
+
url.searchParams.set("callback-url", beginLinkState(options.accountId, options.returnTo));
|
|
10203
10258
|
return url.toString();
|
|
10204
10259
|
}
|
|
10205
10260
|
function beginCloudLink(options) {
|
|
@@ -10210,6 +10265,7 @@ function readCloudLinkCallback(location2 = window.location) {
|
|
|
10210
10265
|
const params = new URLSearchParams(location2.hash.replace(/^#/, ""));
|
|
10211
10266
|
const error = params.get("error");
|
|
10212
10267
|
if (error) {
|
|
10268
|
+
takeLinkState(location2.search ?? "");
|
|
10213
10269
|
stripFragment();
|
|
10214
10270
|
throw new Error(
|
|
10215
10271
|
error === "denied" ? "the account link was declined at the cloud" : `the cloud refused this link: ${error}`
|
|
@@ -10218,12 +10274,18 @@ function readCloudLinkCallback(location2 = window.location) {
|
|
|
10218
10274
|
const grant = params.get("grant");
|
|
10219
10275
|
const account = params.get("account");
|
|
10220
10276
|
if (!grant || !account) return null;
|
|
10277
|
+
if (takeLinkState(location2.search ?? "")?.account !== account) {
|
|
10278
|
+
stripFragment();
|
|
10279
|
+
throw new Error(
|
|
10280
|
+
"this account link answers a request this browser did not make - start the link again from here"
|
|
10281
|
+
);
|
|
10282
|
+
}
|
|
10221
10283
|
stripFragment();
|
|
10222
10284
|
return { grant, account };
|
|
10223
10285
|
}
|
|
10224
10286
|
function stripFragment() {
|
|
10225
10287
|
if (typeof history === "undefined" || !history.replaceState) return;
|
|
10226
|
-
history.replaceState(null, "", location.pathname + location.search);
|
|
10288
|
+
history.replaceState(null, "", location.pathname + withoutLinkState(location.search));
|
|
10227
10289
|
}
|
|
10228
10290
|
async function completeCloudLink(options) {
|
|
10229
10291
|
const root = await resolveRoot(options.root);
|
|
@@ -10243,7 +10305,7 @@ async function completeCloudLink(options) {
|
|
|
10243
10305
|
var DEVICE_PARAM = "enrol-device";
|
|
10244
10306
|
var KEM_PARAM = "enrol-kem";
|
|
10245
10307
|
var CALLBACK_PARAM = "callback-url";
|
|
10246
|
-
var
|
|
10308
|
+
var STATE_PARAM2 = "state";
|
|
10247
10309
|
function deviceEnrolmentUrl(options) {
|
|
10248
10310
|
assertHex(options.devicePublicKey, "devicePublicKey");
|
|
10249
10311
|
assertHex(options.kemPublicKey, "kemPublicKey");
|
|
@@ -10253,7 +10315,7 @@ function deviceEnrolmentUrl(options) {
|
|
|
10253
10315
|
url.searchParams.set(DEVICE_PARAM, options.devicePublicKey);
|
|
10254
10316
|
url.searchParams.set(KEM_PARAM, options.kemPublicKey);
|
|
10255
10317
|
url.searchParams.set(CALLBACK_PARAM, options.returnTo);
|
|
10256
|
-
if (options.state) url.searchParams.set(
|
|
10318
|
+
if (options.state) url.searchParams.set(STATE_PARAM2, options.state);
|
|
10257
10319
|
return url.toString();
|
|
10258
10320
|
}
|
|
10259
10321
|
function beginDeviceEnrolment(options) {
|
|
@@ -10360,17 +10422,6 @@ function normaliseAccount(account) {
|
|
|
10360
10422
|
const bare = trimmed.startsWith("0x") ? trimmed.slice(2) : trimmed;
|
|
10361
10423
|
return bare.length > 0 ? bare : null;
|
|
10362
10424
|
}
|
|
10363
|
-
function httpAdmitterOrigins(addrs) {
|
|
10364
|
-
const out = [];
|
|
10365
|
-
for (const addr of addrs ?? []) {
|
|
10366
|
-
try {
|
|
10367
|
-
const url = new URL(addr);
|
|
10368
|
-
if (url.protocol === "http:" || url.protocol === "https:") out.push(addr.replace(/\/+$/, ""));
|
|
10369
|
-
} catch {
|
|
10370
|
-
}
|
|
10371
|
-
}
|
|
10372
|
-
return out;
|
|
10373
|
-
}
|
|
10374
10425
|
function isInvited(node, invited) {
|
|
10375
10426
|
if (invited.size === 0) return true;
|
|
10376
10427
|
const account = normaliseAccount(node.account);
|
|
@@ -10395,23 +10446,6 @@ async function resolveRelayFromInvitation(input) {
|
|
|
10395
10446
|
};
|
|
10396
10447
|
}
|
|
10397
10448
|
if (nodes.length === 0) {
|
|
10398
|
-
const [relayUrl] = httpAdmitterOrigins(input.admitterAddrs);
|
|
10399
|
-
if (relayUrl) {
|
|
10400
|
-
const admitters = (input.admitters ?? []).map(normaliseAccount).filter((a) => a !== null);
|
|
10401
|
-
return {
|
|
10402
|
-
ok: true,
|
|
10403
|
-
relayUrl,
|
|
10404
|
-
admitUrl: `${relayUrl}/admin-api/namespaces/${input.namespaceId}/admit`,
|
|
10405
|
-
admitterAccount: admitters.length === 1 ? admitters[0] : null,
|
|
10406
|
-
peerId: null,
|
|
10407
|
-
// The relay a founder's session runs through executes that founder's
|
|
10408
|
-
// warrants, so it authors on behalf; the node's answer to the first
|
|
10409
|
-
// write is the authoritative one, as for a cloud-routed node.
|
|
10410
|
-
writable: true,
|
|
10411
|
-
stale: false,
|
|
10412
|
-
via: "invitation"
|
|
10413
|
-
};
|
|
10414
|
-
}
|
|
10415
10449
|
return {
|
|
10416
10450
|
ok: false,
|
|
10417
10451
|
step: "no-nodes",
|
|
@@ -10455,8 +10489,7 @@ async function resolveRelayFromInvitation(input) {
|
|
|
10455
10489
|
// Stale when the node's own heartbeat lapsed OR when the cloud's summary
|
|
10456
10490
|
// disagrees with trying it at all. Both mean the same thing to a caller:
|
|
10457
10491
|
// this was attempted on advisory data, so if the admit fails that is why.
|
|
10458
|
-
stale: !chosen.fresh || advisory
|
|
10459
|
-
via: "cloud"
|
|
10492
|
+
stale: !chosen.fresh || advisory
|
|
10460
10493
|
};
|
|
10461
10494
|
}
|
|
10462
10495
|
|
|
@@ -10742,7 +10775,6 @@ async function bootstrapFromInvitation(input) {
|
|
|
10742
10775
|
// separately: two sources for one fact is how an intersection ends up
|
|
10743
10776
|
// being performed against the wrong list.
|
|
10744
10777
|
admitters: input.invitation.invitation.admitters,
|
|
10745
|
-
admitterAddrs: input.invitation.admitter_addrs,
|
|
10746
10778
|
credential: input.credential.credential,
|
|
10747
10779
|
deviceSecret: input.credential.deviceSecret,
|
|
10748
10780
|
cloudBaseUrl: input.cloudBaseUrl
|
|
@@ -10961,7 +10993,7 @@ async function attestedNodeKey(relay) {
|
|
|
10961
10993
|
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"
|
|
10962
10994
|
);
|
|
10963
10995
|
}
|
|
10964
|
-
var HA_ACCOUNT_NOT_LINKED_MESSAGE = "link this account to your cloud user in the wallet so
|
|
10996
|
+
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";
|
|
10965
10997
|
var HA_REFUSAL_MESSAGES = {
|
|
10966
10998
|
account_not_linked: HA_ACCOUNT_NOT_LINKED_MESSAGE,
|
|
10967
10999
|
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",
|
|
@@ -11066,7 +11098,7 @@ var NoRelayError = class extends Error {
|
|
|
11066
11098
|
var InvitationNotClaimableError = class extends Error {
|
|
11067
11099
|
constructor(namespaceId, reason) {
|
|
11068
11100
|
super(
|
|
11069
|
-
reason === "not-hosted" ? `nobody could claim an invitation to ${namespaceId} yet: the
|
|
11101
|
+
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`
|
|
11070
11102
|
);
|
|
11071
11103
|
this.namespaceId = namespaceId;
|
|
11072
11104
|
this.reason = reason;
|
|
@@ -11254,7 +11286,6 @@ function createAccountAdmin(input, deps = {}) {
|
|
|
11254
11286
|
async createNamespaceInvitation(namespaceId) {
|
|
11255
11287
|
const [{ members }, info] = await Promise.all([relay().listGroupMembers(namespaceId), relay().getGroupInfo(namespaceId)]);
|
|
11256
11288
|
const relays = members.filter((m) => m.role === "RelayTee").map((m) => m.identity.toLowerCase());
|
|
11257
|
-
let admitterAddrs;
|
|
11258
11289
|
if (routing) {
|
|
11259
11290
|
const named = new Set(
|
|
11260
11291
|
relays.length > 0 ? relays : members.filter((m) => m.role === "Admin").map((m) => m.identity.toLowerCase())
|
|
@@ -11265,10 +11296,8 @@ function createAccountAdmin(input, deps = {}) {
|
|
|
11265
11296
|
} catch {
|
|
11266
11297
|
}
|
|
11267
11298
|
if (nodes !== null) {
|
|
11268
|
-
if (nodes.length === 0)
|
|
11269
|
-
|
|
11270
|
-
admitterAddrs = [s.relayUrl];
|
|
11271
|
-
} else if (!nodes.some((n) => n.account !== null && named.has(n.account.toLowerCase()))) {
|
|
11299
|
+
if (nodes.length === 0) throw new InvitationNotClaimableError(namespaceId, "not-hosted");
|
|
11300
|
+
if (!nodes.some((n) => n.account !== null && named.has(n.account.toLowerCase()))) {
|
|
11272
11301
|
throw new InvitationNotClaimableError(namespaceId, "no-named-node");
|
|
11273
11302
|
}
|
|
11274
11303
|
}
|
|
@@ -11278,7 +11307,6 @@ function createAccountAdmin(input, deps = {}) {
|
|
|
11278
11307
|
inviterAccount: s.account,
|
|
11279
11308
|
deviceSecret: s.deviceSecret,
|
|
11280
11309
|
...relays.length > 0 ? { admitters: relays } : {},
|
|
11281
|
-
...admitterAddrs ? { admitterAddrs } : {},
|
|
11282
11310
|
members,
|
|
11283
11311
|
applicationId: info.targetApplicationId,
|
|
11284
11312
|
appKey: info.appKey
|
|
@@ -11385,7 +11413,6 @@ function joinOf(row) {
|
|
|
11385
11413
|
reportedAt: typeof join.reported_at === "string" ? join.reported_at : null
|
|
11386
11414
|
};
|
|
11387
11415
|
}
|
|
11388
|
-
var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
|
|
11389
11416
|
function accountHexOrNull(value) {
|
|
11390
11417
|
return typeof value === "string" && ACCOUNT_HEX.test(value) ? value : null;
|
|
11391
11418
|
}
|
|
@@ -11659,9 +11686,8 @@ var CloudClient = class {
|
|
|
11659
11686
|
* are answered together rather than across two endpoints — the per-context
|
|
11660
11687
|
* read needs a context id a joiner does not yet have.
|
|
11661
11688
|
*
|
|
11662
|
-
* An empty `nodes` list is a state, not an error:
|
|
11663
|
-
*
|
|
11664
|
-
* invitation's other admitters — another admin's node, or a self-hosted peer.
|
|
11689
|
+
* An empty `nodes` list is a state, not an error: the cloud has no record of
|
|
11690
|
+
* a node holding the namespace, so there is no hosted admitter.
|
|
11665
11691
|
*/
|
|
11666
11692
|
async getNamespaceRouting(namespaceId) {
|
|
11667
11693
|
const body = await this.request("GET", `/api/cloud/namespaces/${encodeURIComponent(namespaceId)}/admitters`, {
|
|
@@ -12114,7 +12140,7 @@ var CloudClient = class {
|
|
|
12114
12140
|
const portal = options.portalUrl.replace(/\/+$/, "");
|
|
12115
12141
|
const query = new URLSearchParams({
|
|
12116
12142
|
"link-account": options.accountId,
|
|
12117
|
-
"callback-url": options.callbackUrl
|
|
12143
|
+
"callback-url": beginLinkState(options.accountId, options.callbackUrl)
|
|
12118
12144
|
});
|
|
12119
12145
|
return { url: `${portal}/?${query.toString()}`, accountId: options.accountId };
|
|
12120
12146
|
}
|
|
@@ -12124,14 +12150,19 @@ var CloudClient = class {
|
|
|
12124
12150
|
* Returns `{}` when there is nothing to read, so it is safe to call on every
|
|
12125
12151
|
* load — which is how a redirected app finds out it has been sent back.
|
|
12126
12152
|
*
|
|
12153
|
+
* A grant is only returned when it answers a handoff this browser began, for the
|
|
12154
|
+
* account that handoff named; anything else comes back as
|
|
12155
|
+
* `error: 'unexpected_callback'`, because the redirect URL can be forwarded.
|
|
12156
|
+
*
|
|
12127
12157
|
* @param href defaults to the current location, when there is one.
|
|
12128
12158
|
*/
|
|
12129
12159
|
static readAccountLinkCallback(href) {
|
|
12130
12160
|
const source = href ?? (typeof window === "undefined" ? "" : window.location.href);
|
|
12131
12161
|
if (!source) return {};
|
|
12132
12162
|
let hash;
|
|
12163
|
+
let search;
|
|
12133
12164
|
try {
|
|
12134
|
-
hash = new URL(source)
|
|
12165
|
+
({ hash, search } = new URL(source));
|
|
12135
12166
|
} catch {
|
|
12136
12167
|
return {};
|
|
12137
12168
|
}
|
|
@@ -12140,6 +12171,10 @@ var CloudClient = class {
|
|
|
12140
12171
|
const accountId = params.get("account");
|
|
12141
12172
|
const error = params.get("error");
|
|
12142
12173
|
const out = {};
|
|
12174
|
+
if (error) takeLinkState(search);
|
|
12175
|
+
if (grant && takeLinkState(search)?.account !== accountId) {
|
|
12176
|
+
return { error: "unexpected_callback" };
|
|
12177
|
+
}
|
|
12143
12178
|
if (grant) out.grant = grant;
|
|
12144
12179
|
if (accountId) out.accountId = accountId;
|
|
12145
12180
|
if (error) out.error = error;
|