@calimero-network/mero-js 25.0.1 → 25.0.3
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 +2 -3
- package/dist/cloud/cloud-client.js +2 -3
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +51 -48
- package/dist/index.cjs.map +2 -2
- package/dist/index.mjs +51 -48
- package/dist/index.mjs.map +2 -2
- package/dist/mero-js.d.ts +6 -0
- package/dist/mero-js.js +53 -6
- package/package.json +1 -1
|
@@ -29,10 +29,8 @@ export declare class NoRelayError extends Error {
|
|
|
29
29
|
* cloud's routing, intersected with the invitation's signed admitters, so an
|
|
30
30
|
* invitation the cloud routes to none of them can only fail in their hands.
|
|
31
31
|
*
|
|
32
|
-
* - `not-hosted`
|
|
33
|
-
*
|
|
34
|
-
* relay in it is minted for even when the cloud routes it nowhere: the relay
|
|
35
|
-
* admits through its own `/admit`, and the invitation carries its address.
|
|
32
|
+
* - `not-hosted` - the cloud lists no node holding the namespace. A relay it
|
|
33
|
+
* does not list cannot stand in: an address on the invitation is unsigned.
|
|
36
34
|
* - `no-named-node` — the cloud's nodes are not among the admitters this
|
|
37
35
|
* invitation would name.
|
|
38
36
|
*/
|
|
@@ -26,17 +26,15 @@ export class NoRelayError extends Error {
|
|
|
26
26
|
* cloud's routing, intersected with the invitation's signed admitters, so an
|
|
27
27
|
* invitation the cloud routes to none of them can only fail in their hands.
|
|
28
28
|
*
|
|
29
|
-
* - `not-hosted`
|
|
30
|
-
*
|
|
31
|
-
* relay in it is minted for even when the cloud routes it nowhere: the relay
|
|
32
|
-
* admits through its own `/admit`, and the invitation carries its address.
|
|
29
|
+
* - `not-hosted` - the cloud lists no node holding the namespace. A relay it
|
|
30
|
+
* does not list cannot stand in: an address on the invitation is unsigned.
|
|
33
31
|
* - `no-named-node` — the cloud's nodes are not among the admitters this
|
|
34
32
|
* invitation would name.
|
|
35
33
|
*/
|
|
36
34
|
export class InvitationNotClaimableError extends Error {
|
|
37
35
|
constructor(namespaceId, reason) {
|
|
38
36
|
super(reason === 'not-hosted'
|
|
39
|
-
? `nobody could claim an invitation to ${namespaceId} yet: the
|
|
37
|
+
? `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.`
|
|
40
38
|
: `nobody could claim an invitation to ${namespaceId}: the cloud routes to none of the relays or admins it would name as admitters`);
|
|
41
39
|
this.namespaceId = namespaceId;
|
|
42
40
|
this.reason = reason;
|
|
@@ -264,9 +262,6 @@ export function createAccountAdmin(input, deps = {}) {
|
|
|
264
262
|
const relays = members
|
|
265
263
|
.filter((m) => m.role === 'RelayTee')
|
|
266
264
|
.map((m) => m.identity.toLowerCase());
|
|
267
|
-
// Where the claimant reaches an admitter when the cloud routes them to
|
|
268
|
-
// none: set only then, so a routed namespace stays the cloud's to route.
|
|
269
|
-
let admitterAddrs;
|
|
270
265
|
if (routing) {
|
|
271
266
|
// Who the claimant's client will look for: the admitters this
|
|
272
267
|
// invitation names (the relays, else signGroupInvitation's admins).
|
|
@@ -281,19 +276,9 @@ export function createAccountAdmin(input, deps = {}) {
|
|
|
281
276
|
// Nothing learned: the claimant's own lookup will say what is wrong.
|
|
282
277
|
}
|
|
283
278
|
if (nodes !== null) {
|
|
284
|
-
if (nodes.length === 0)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// relay that founded it and serves this session, a RelayTee
|
|
288
|
-
// member; its own `/admit` takes the join (measured on prod: 200,
|
|
289
|
-
// where the cloud's lookup answered no nodes). The claimant's
|
|
290
|
-
// client cannot find that relay through the cloud, so the
|
|
291
|
-
// invitation says where it is.
|
|
292
|
-
if (relays.length === 0 || !s.relayUrl)
|
|
293
|
-
throw new InvitationNotClaimableError(namespaceId, 'not-hosted');
|
|
294
|
-
admitterAddrs = [s.relayUrl];
|
|
295
|
-
}
|
|
296
|
-
else if (!nodes.some((n) => n.account !== null && named.has(n.account.toLowerCase()))) {
|
|
279
|
+
if (nodes.length === 0)
|
|
280
|
+
throw new InvitationNotClaimableError(namespaceId, 'not-hosted');
|
|
281
|
+
if (!nodes.some((n) => n.account !== null && named.has(n.account.toLowerCase()))) {
|
|
297
282
|
throw new InvitationNotClaimableError(namespaceId, 'no-named-node');
|
|
298
283
|
}
|
|
299
284
|
}
|
|
@@ -303,7 +288,6 @@ export function createAccountAdmin(input, deps = {}) {
|
|
|
303
288
|
inviterAccount: s.account,
|
|
304
289
|
deviceSecret: s.deviceSecret,
|
|
305
290
|
...(relays.length > 0 ? { admitters: relays } : {}),
|
|
306
|
-
...(admitterAddrs ? { admitterAddrs } : {}),
|
|
307
291
|
members: members,
|
|
308
292
|
applicationId: info.targetApplicationId,
|
|
309
293
|
appKey: info.appKey,
|
|
@@ -20,16 +20,11 @@
|
|
|
20
20
|
*
|
|
21
21
|
* # Where the node URL comes from, in preference order
|
|
22
22
|
*
|
|
23
|
-
* 1. one
|
|
24
|
-
* today emits only libp2p multiaddrs on an invitation (`resolve_admitter_addrs`)
|
|
25
|
-
* because a node cannot know the URL its admin API is proxied on, so there is
|
|
26
|
-
* nothing to read off the invitation yet and this is the seam for when there
|
|
27
|
-
* is;
|
|
23
|
+
* 1. one the caller passes in, such as an operator's or a local rig's;
|
|
28
24
|
* 2. the cloud's `/api/cloud/namespaces/{ns}/admitters`, intersected with the
|
|
29
|
-
* invitation's signed `admitters
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* the account writes its own origin there when it mints.
|
|
25
|
+
* invitation's signed `admitters`, see `resolveRelayFromInvitation`.
|
|
26
|
+
*
|
|
27
|
+
* Never from the invitation, signed or not: the inviter may be the attacker.
|
|
33
28
|
*
|
|
34
29
|
* # Nothing here is minted, weakened or worked around
|
|
35
30
|
*
|
|
@@ -90,10 +85,8 @@ export interface BootstrapFromInvitationInput {
|
|
|
90
85
|
/**
|
|
91
86
|
* A node to present the join to, skipping the cloud lookup.
|
|
92
87
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* cloud's admitters lookup answers, and the invitation's signed list is what
|
|
96
|
-
* narrows the answer.
|
|
88
|
+
* For a node the caller already knows (an operator's, a local rig); never one
|
|
89
|
+
* from the invitation. Without it the cloud's lookup answers, narrowed by the signed admitters.
|
|
97
90
|
*/
|
|
98
91
|
readonly nodeUrl?: string;
|
|
99
92
|
/** Point at a cloud other than the hosted manager. For local rigs. */
|
|
@@ -20,16 +20,11 @@
|
|
|
20
20
|
*
|
|
21
21
|
* # Where the node URL comes from, in preference order
|
|
22
22
|
*
|
|
23
|
-
* 1. one
|
|
24
|
-
* today emits only libp2p multiaddrs on an invitation (`resolve_admitter_addrs`)
|
|
25
|
-
* because a node cannot know the URL its admin API is proxied on, so there is
|
|
26
|
-
* nothing to read off the invitation yet and this is the seam for when there
|
|
27
|
-
* is;
|
|
23
|
+
* 1. one the caller passes in, such as an operator's or a local rig's;
|
|
28
24
|
* 2. the cloud's `/api/cloud/namespaces/{ns}/admitters`, intersected with the
|
|
29
|
-
* invitation's signed `admitters
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* the account writes its own origin there when it mints.
|
|
25
|
+
* invitation's signed `admitters`, see `resolveRelayFromInvitation`.
|
|
26
|
+
*
|
|
27
|
+
* Never from the invitation, signed or not: the inviter may be the attacker.
|
|
33
28
|
*
|
|
34
29
|
* # Nothing here is minted, weakened or worked around
|
|
35
30
|
*
|
|
@@ -61,7 +56,6 @@ export async function bootstrapFromInvitation(input) {
|
|
|
61
56
|
// separately: two sources for one fact is how an intersection ends up
|
|
62
57
|
// being performed against the wrong list.
|
|
63
58
|
admitters: input.invitation.invitation.admitters,
|
|
64
|
-
admitterAddrs: input.invitation.admitter_addrs,
|
|
65
59
|
credential: input.credential.credential,
|
|
66
60
|
deviceSecret: input.credential.deviceSecret,
|
|
67
61
|
cloudBaseUrl: input.cloudBaseUrl,
|
|
@@ -89,7 +89,7 @@ export interface FoundedDelegatedNamespace {
|
|
|
89
89
|
haError?: string;
|
|
90
90
|
}
|
|
91
91
|
/** What an app can tell a person whose account the cloud cannot place. */
|
|
92
|
-
export declare const HA_ACCOUNT_NOT_LINKED_MESSAGE = "link this account to your cloud user in the wallet so
|
|
92
|
+
export declare const 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";
|
|
93
93
|
/**
|
|
94
94
|
* What to tell a person for each refusal the cloud names, keyed by its code.
|
|
95
95
|
* Every one leaves the namespace founded; only hosting was refused.
|
|
@@ -197,7 +197,7 @@ async function attestedNodeKey(relay) {
|
|
|
197
197
|
: "the relay's attestation was refused, so founding through it is not possible: it is not a signed locked-read-only release");
|
|
198
198
|
}
|
|
199
199
|
/** What an app can tell a person whose account the cloud cannot place. */
|
|
200
|
-
export const HA_ACCOUNT_NOT_LINKED_MESSAGE = 'link this account to your cloud user in the wallet so
|
|
200
|
+
export const 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';
|
|
201
201
|
/**
|
|
202
202
|
* What to tell a person for each refusal the cloud names, keyed by its code.
|
|
203
203
|
* Every one leaves the namespace founded; only hosting was refused.
|
|
@@ -226,9 +226,8 @@ function blockingNamespace(e) {
|
|
|
226
226
|
* the namespace id) and the cloud bills the user it is linked to.
|
|
227
227
|
*
|
|
228
228
|
* Best-effort: the namespace exists whatever the cloud answers, so a refusal is
|
|
229
|
-
* reported, never thrown.
|
|
230
|
-
* so
|
|
231
|
-
* sentence that says what to do.
|
|
229
|
+
* reported, never thrown. The cloud records the founding relay before its link
|
|
230
|
+
* gate, so invitees find the namespace either way; HA adds fleet hosting.
|
|
232
231
|
*/
|
|
233
232
|
async function enableHaBestEffort(s, namespaceId, salt, relayUrl, deps) {
|
|
234
233
|
try {
|
|
@@ -80,12 +80,8 @@ export interface JoinWithNodeInput {
|
|
|
80
80
|
/**
|
|
81
81
|
* The node to present the join to — its admin API origin.
|
|
82
82
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* see `resolve_admitter_addrs`, a node cannot know the URL its admin API is
|
|
86
|
-
* proxied on — so this is the seam for when invitations carry a claim URL),
|
|
87
|
-
* one resolved from the cloud's `/admitters` lookup, and one an operator
|
|
88
|
-
* simply knows.
|
|
83
|
+
* Resolved from the cloud's `/admitters` lookup or supplied by the caller;
|
|
84
|
+
* never read off the invitation, signed or not: the inviter may be the attacker.
|
|
89
85
|
*/
|
|
90
86
|
readonly nodeUrl: string;
|
|
91
87
|
/**
|
|
@@ -56,11 +56,8 @@ import { CloudClient } from '../cloud/index.js';
|
|
|
56
56
|
*
|
|
57
57
|
* - `admitters-lookup` — the cloud read itself failed (offline, 5xx, a refused
|
|
58
58
|
* routing proof). Nothing has been learned about the namespace.
|
|
59
|
-
* - `no-nodes`
|
|
60
|
-
* invitation
|
|
61
|
-
* has no fleet assignment, so there is no cloud node to admit through; the
|
|
62
|
-
* invitation's admitters are somebody's self-hosted or desktop node, which the
|
|
63
|
-
* cloud neither knows nor routes to.
|
|
59
|
+
* - `no-nodes` - the cloud lists no node to admit through. An address on the
|
|
60
|
+
* invitation is not one: `admitter_addrs` is unsigned.
|
|
64
61
|
* - `not-invited` — nodes serve the namespace, and the invitation names none of
|
|
65
62
|
* them. Terminal for this invitation: ask for a fresh one.
|
|
66
63
|
* - `invited-unreachable` — a named node exists but cannot take a join now (no
|
|
@@ -78,13 +75,10 @@ export interface ResolvedInvitationRelay {
|
|
|
78
75
|
readonly relayUrl: string;
|
|
79
76
|
/** Where to POST the signed join — the cloud's own, never rebuilt when given. */
|
|
80
77
|
readonly admitUrl: string;
|
|
81
|
-
/**
|
|
82
|
-
* The admitter's account: as the cloud reported it, or — for a relay the
|
|
83
|
-
* invitation itself named — the one admitter it names, when it names one.
|
|
84
|
-
*/
|
|
78
|
+
/** The admitter's account, as the cloud reported it. */
|
|
85
79
|
readonly admitterAccount: string | null;
|
|
86
|
-
/** The node's peer id as the cloud reported it
|
|
87
|
-
readonly peerId: string
|
|
80
|
+
/** The node's peer id, as the cloud reported it. */
|
|
81
|
+
readonly peerId: string;
|
|
88
82
|
/**
|
|
89
83
|
* Whether this node can also run delegated writes (`CAN_AUTHOR_ON_BEHALF`).
|
|
90
84
|
*
|
|
@@ -95,11 +89,6 @@ export interface ResolvedInvitationRelay {
|
|
|
95
89
|
readonly writable: boolean;
|
|
96
90
|
/** Whether the chosen node's heartbeat for this namespace has lapsed. */
|
|
97
91
|
readonly stale: boolean;
|
|
98
|
-
/**
|
|
99
|
-
* Who named this relay: the cloud's routing, or the invitation's own
|
|
100
|
-
* `admitter_addrs` when the cloud routed the namespace to no node.
|
|
101
|
-
*/
|
|
102
|
-
readonly via: 'cloud' | 'invitation';
|
|
103
92
|
}
|
|
104
93
|
export interface RelayResolutionFailure {
|
|
105
94
|
readonly ok: false;
|
|
@@ -120,15 +109,6 @@ export interface ResolveRelayFromInvitationInput {
|
|
|
120
109
|
* here that means "nobody may admit", because an invitation cannot say that.
|
|
121
110
|
*/
|
|
122
111
|
readonly admitters?: readonly string[];
|
|
123
|
-
/**
|
|
124
|
-
* The invitation's `admitter_addrs`: where its admitters are reached. Core
|
|
125
|
-
* emits libp2p multiaddrs here; an account minting through mero-react adds
|
|
126
|
-
* its relay's http(s) origin. Only the http(s) entries are of use to a
|
|
127
|
-
* browser, and only when the cloud routes the namespace to no node — the
|
|
128
|
-
* case of a namespace whose founder never linked the account, which the
|
|
129
|
-
* cloud does not route yet whose own relay admits.
|
|
130
|
-
*/
|
|
131
|
-
readonly admitterAddrs?: readonly string[];
|
|
132
112
|
/** The delegated credential — an `AccountProof<DeviceCert>`, hex. */
|
|
133
113
|
readonly credential: string;
|
|
134
114
|
/** The certified device's ed25519 signing secret, hex. */
|
|
@@ -64,21 +64,6 @@ export function normaliseAccount(account) {
|
|
|
64
64
|
const bare = trimmed.startsWith('0x') ? trimmed.slice(2) : trimmed;
|
|
65
65
|
return bare.length > 0 ? bare : null;
|
|
66
66
|
}
|
|
67
|
-
/** The http(s) origins among an invitation's admitter addresses, trailing slash dropped. */
|
|
68
|
-
function httpAdmitterOrigins(addrs) {
|
|
69
|
-
const out = [];
|
|
70
|
-
for (const addr of addrs ?? []) {
|
|
71
|
-
try {
|
|
72
|
-
const url = new URL(addr);
|
|
73
|
-
if (url.protocol === 'http:' || url.protocol === 'https:')
|
|
74
|
-
out.push(addr.replace(/\/+$/, ''));
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
// A multiaddr, or nothing a browser can dial.
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return out;
|
|
81
|
-
}
|
|
82
67
|
/** Whether the invitation's signed list names this node. */
|
|
83
68
|
function isInvited(node, invited) {
|
|
84
69
|
if (invited.size === 0)
|
|
@@ -121,28 +106,6 @@ export async function resolveRelayFromInvitation(input) {
|
|
|
121
106
|
};
|
|
122
107
|
}
|
|
123
108
|
if (nodes.length === 0) {
|
|
124
|
-
// The cloud routes nothing, but the invitation may say where an admitter
|
|
125
|
-
// is: an account's relay writes its origin into `admitter_addrs` when it
|
|
126
|
-
// mints for a namespace the cloud does not host. The relay's `admit_join`
|
|
127
|
-
// re-verifies the invitation and its admitters, so nothing is loosened by
|
|
128
|
-
// knocking there; the account is the invitation's, when it names one.
|
|
129
|
-
const [relayUrl] = httpAdmitterOrigins(input.admitterAddrs);
|
|
130
|
-
if (relayUrl) {
|
|
131
|
-
const admitters = (input.admitters ?? []).map(normaliseAccount).filter((a) => a !== null);
|
|
132
|
-
return {
|
|
133
|
-
ok: true,
|
|
134
|
-
relayUrl,
|
|
135
|
-
admitUrl: `${relayUrl}/admin-api/namespaces/${input.namespaceId}/admit`,
|
|
136
|
-
admitterAccount: admitters.length === 1 ? admitters[0] : null,
|
|
137
|
-
peerId: null,
|
|
138
|
-
// The relay a founder's session runs through executes that founder's
|
|
139
|
-
// warrants, so it authors on behalf; the node's answer to the first
|
|
140
|
-
// write is the authoritative one, as for a cloud-routed node.
|
|
141
|
-
writable: true,
|
|
142
|
-
stale: false,
|
|
143
|
-
via: 'invitation',
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
109
|
return {
|
|
147
110
|
ok: false,
|
|
148
111
|
step: 'no-nodes',
|
|
@@ -211,6 +174,5 @@ export async function resolveRelayFromInvitation(input) {
|
|
|
211
174
|
// disagrees with trying it at all. Both mean the same thing to a caller:
|
|
212
175
|
// this was attempted on advisory data, so if the admit fails that is why.
|
|
213
176
|
stale: !chosen.fresh || advisory,
|
|
214
|
-
via: 'cloud',
|
|
215
177
|
};
|
|
216
178
|
}
|
|
@@ -624,9 +624,8 @@ export declare class CloudClient {
|
|
|
624
624
|
* are answered together rather than across two endpoints — the per-context
|
|
625
625
|
* read needs a context id a joiner does not yet have.
|
|
626
626
|
*
|
|
627
|
-
* An empty `nodes` list is a state, not an error:
|
|
628
|
-
*
|
|
629
|
-
* invitation's other admitters — another admin's node, or a self-hosted peer.
|
|
627
|
+
* An empty `nodes` list is a state, not an error: the cloud has no record of
|
|
628
|
+
* a node holding the namespace, so there is no hosted admitter.
|
|
630
629
|
*/
|
|
631
630
|
getNamespaceRouting(namespaceId: string): Promise<CloudNamespaceRouting>;
|
|
632
631
|
/**
|
|
@@ -404,9 +404,8 @@ export class CloudClient {
|
|
|
404
404
|
* are answered together rather than across two endpoints — the per-context
|
|
405
405
|
* read needs a context id a joiner does not yet have.
|
|
406
406
|
*
|
|
407
|
-
* An empty `nodes` list is a state, not an error:
|
|
408
|
-
*
|
|
409
|
-
* invitation's other admitters — another admin's node, or a self-hosted peer.
|
|
407
|
+
* An empty `nodes` list is a state, not an error: the cloud has no record of
|
|
408
|
+
* a node holding the namespace, so there is no hosted admitter.
|
|
410
409
|
*/
|
|
411
410
|
async getNamespaceRouting(namespaceId) {
|
|
412
411
|
const body = await this.request('GET', `/api/cloud/namespaces/${encodeURIComponent(namespaceId)}/admitters`, {
|