@calimero-network/mero-js 23.8.1 → 23.9.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/README.md +95 -0
- package/dist/account/account-admin.d.ts +82 -0
- package/dist/account/account-admin.js +360 -0
- package/dist/account/application-id.d.ts +48 -0
- package/dist/account/application-id.js +83 -0
- package/dist/account/bootstrap-from-invitation.d.ts +117 -0
- package/dist/account/bootstrap-from-invitation.js +86 -0
- package/dist/account/create-context.d.ts +105 -0
- package/dist/account/create-context.js +237 -0
- package/dist/account/govern.d.ts +24 -0
- package/dist/account/govern.js +50 -0
- package/dist/account/index.d.ts +18 -0
- package/dist/account/index.js +10 -0
- package/dist/account/join-as-account.d.ts +23 -0
- package/dist/account/join-as-account.js +34 -0
- package/dist/account/join-with-node.d.ts +124 -0
- package/dist/account/join-with-node.js +165 -0
- package/dist/account/node-admin.d.ts +20 -0
- package/dist/account/node-admin.js +33 -0
- package/dist/account/relay-from-invitation.d.ts +157 -0
- package/dist/account/relay-from-invitation.js +216 -0
- package/dist/account/session.d.ts +214 -0
- package/dist/account/session.js +789 -0
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +4302 -2970
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/index.mjs +4292 -2970
- package/dist/index.mjs.map +4 -4
- package/dist/namespace-op/namespace-op.d.ts +6 -1
- package/dist/namespace-op/namespace-op.js +6 -1
- package/package.json +6 -3
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving a relay from an INVITATION rather than from an account.
|
|
3
|
+
*
|
|
4
|
+
* # The gap this closes
|
|
5
|
+
*
|
|
6
|
+
* `ConnectButtonAccount` finds a relay by asking the cloud what serves the
|
|
7
|
+
* *account* (`CloudClient.getAccountRelays`). A freshly enrolled account is a
|
|
8
|
+
* member of nothing, so that read correctly answers `[]` and the button
|
|
9
|
+
* correctly refuses to connect: there is nowhere to write, and a typed guess
|
|
10
|
+
* would be a node that refuses every warrant.
|
|
11
|
+
*
|
|
12
|
+
* But a newcomer is not stuck — they are holding an invitation, and an
|
|
13
|
+
* invitation names a namespace. `CloudClient.getNamespaceRouting` answers which
|
|
14
|
+
* nodes serve *that namespace*, and it is deliberately anonymous: a joiner has
|
|
15
|
+
* no cloud session, and needing one is exactly what this path exists to avoid.
|
|
16
|
+
* So the bootstrap order is namespace first, account second — admission is what
|
|
17
|
+
* makes `getAccountRelays` answer at all, not the other way round.
|
|
18
|
+
*
|
|
19
|
+
* # Two sources, one intersection, and why it must not be skipped
|
|
20
|
+
*
|
|
21
|
+
* The invitation's `admitters` list sits INSIDE the body the group admin
|
|
22
|
+
* signed, so it is authorization: merod answers `403` to an `/admit` from a node
|
|
23
|
+
* that list does not name, whatever else is true of it. The cloud's list is
|
|
24
|
+
* every node currently assigned to the namespace — it cannot know which
|
|
25
|
+
* invitation the caller holds, so it cannot filter by the signed list, and a
|
|
26
|
+
* node assigned *after* the invitation was minted looks perfect in every field
|
|
27
|
+
* the cloud reports and still refuses the claim.
|
|
28
|
+
*
|
|
29
|
+
* Intersecting here is therefore not belt-and-braces: it is the check that
|
|
30
|
+
* turns an unexplainable `403` into "your invitation names none of these nodes,
|
|
31
|
+
* ask for a fresh one". It is never relaxed to make a flow work.
|
|
32
|
+
*
|
|
33
|
+
* # `servable` and `canAdmit` are advisory; `writable` is information
|
|
34
|
+
*
|
|
35
|
+
* `servable` and `canAdmit` are derived from assignment heartbeats, and a lapsed
|
|
36
|
+
* heartbeat is not the same fact as a node that will refuse. Measured against
|
|
37
|
+
* prod: the cloud reported `servable: false, canAdmit: false, fresh: false` for a
|
|
38
|
+
* node whose `/admin-api/health` answered `{"status":"alive"}` and which had just
|
|
39
|
+
* minted the invitation being claimed. Gating on them meant refusing to knock on
|
|
40
|
+
* an open door, so a node with an address is tried and the NODE decides —
|
|
41
|
+
* `admit_join` re-verifies the invitation, the signature and the admitters list,
|
|
42
|
+
* so asking loosens nothing and a refusal names its own cause. `stale` on the
|
|
43
|
+
* result says the attempt rests on advisory data.
|
|
44
|
+
*
|
|
45
|
+
* `writable` — and its per-node form, `canExecute` — is about `CAN_AUTHOR_ON_BEHALF`,
|
|
46
|
+
* which admission does **not** require: relaying a join the joiner signed is not
|
|
47
|
+
* authoring on anyone's behalf. So a non-writable namespace is not a refusal
|
|
48
|
+
* here; it is reported on the result, because it is precisely why the writes
|
|
49
|
+
* would fail *after* connecting, and a caller that cannot tell the two apart
|
|
50
|
+
* sends the user to the wrong place.
|
|
51
|
+
*/
|
|
52
|
+
import { CloudClient, } from '../cloud/index.js';
|
|
53
|
+
/**
|
|
54
|
+
* Normalise a hex account for comparison: lower-cased, `0x` stripped.
|
|
55
|
+
*
|
|
56
|
+
* Both sides are hex for the same 32 bytes, and a spelling mismatch would
|
|
57
|
+
* present as "your invitation does not name this node" — a refusal that looks
|
|
58
|
+
* like policy and is really formatting.
|
|
59
|
+
*/
|
|
60
|
+
export function normaliseAccount(account) {
|
|
61
|
+
if (!account)
|
|
62
|
+
return null;
|
|
63
|
+
const trimmed = account.trim().toLowerCase();
|
|
64
|
+
const bare = trimmed.startsWith('0x') ? trimmed.slice(2) : trimmed;
|
|
65
|
+
return bare.length > 0 ? bare : null;
|
|
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
|
+
/** Whether the invitation's signed list names this node. */
|
|
83
|
+
function isInvited(node, invited) {
|
|
84
|
+
if (invited.size === 0)
|
|
85
|
+
return true;
|
|
86
|
+
const account = normaliseAccount(node.account);
|
|
87
|
+
return account !== null && invited.has(account);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a relay to bootstrap through, from an invitation and nothing else.
|
|
91
|
+
*
|
|
92
|
+
* Reads the cloud's routing for the namespace, intersects it with the
|
|
93
|
+
* invitation's signed `admitters`, and returns the node to use — or which step
|
|
94
|
+
* stopped it and what that means. It performs no admission and mints no keys:
|
|
95
|
+
* the credential passed in is the one the wallet already certified.
|
|
96
|
+
*/
|
|
97
|
+
export async function resolveRelayFromInvitation(input) {
|
|
98
|
+
const cloud = input.cloud ??
|
|
99
|
+
new CloudClient({
|
|
100
|
+
cloudBaseUrl: input.cloudBaseUrl,
|
|
101
|
+
routingCredential: { credential: input.credential, deviceSecret: input.deviceSecret },
|
|
102
|
+
});
|
|
103
|
+
let nodes;
|
|
104
|
+
let servable;
|
|
105
|
+
try {
|
|
106
|
+
const routing = await cloud.getNamespaceRouting(input.namespaceId);
|
|
107
|
+
nodes = routing.nodes;
|
|
108
|
+
servable = routing.servable;
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
// Named as its own step: nothing has been learned about the namespace, so
|
|
112
|
+
// reporting this as "no nodes serve it" would be a claim the read never
|
|
113
|
+
// made — and would send the user to ask for a new invitation over what is
|
|
114
|
+
// usually a refused proof or a network blip.
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
step: 'admitters-lookup',
|
|
118
|
+
reason: `The cloud could not be asked which nodes serve namespace ${input.namespaceId}: ` +
|
|
119
|
+
`${e instanceof Error ? e.message : String(e)}. Nothing is known about the ` +
|
|
120
|
+
'invitation yet — this is the lookup failing, not the invitation.',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
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
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
step: 'no-nodes',
|
|
149
|
+
reason: 'The cloud lists no nodes serving this namespace, so there is no hosted node to be ' +
|
|
150
|
+
'admitted through. The namespace has no fleet assignment — whoever invited you is ' +
|
|
151
|
+
'running their own node, and admission has to go through a node the cloud routes to.',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const invited = new Set((input.admitters ?? [])
|
|
155
|
+
.map(normaliseAccount)
|
|
156
|
+
.filter((a) => a !== null));
|
|
157
|
+
const named = nodes.filter((n) => isInvited(n, invited));
|
|
158
|
+
if (named.length === 0) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
step: 'not-invited',
|
|
162
|
+
reason: `${nodes.length} node${nodes.length === 1 ? '' : 's'} serve this namespace, but your ` +
|
|
163
|
+
'invitation names none of them. The signed admitter list is a snapshot from when the ' +
|
|
164
|
+
'invitation was minted, so a node assigned since is healthy and will still refuse the ' +
|
|
165
|
+
'claim. Ask for a fresh invitation.',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// `canAdmit` and `servable` are ADVISORY here, not gates.
|
|
169
|
+
//
|
|
170
|
+
// They are derived from assignment heartbeats, and a lapsed heartbeat is not
|
|
171
|
+
// the same fact as a node that will refuse: measured against prod, the cloud
|
|
172
|
+
// reported `servable: false, canAdmit: false, fresh: false` for a node whose
|
|
173
|
+
// `/admin-api/health` answered `{"status":"alive"}` and which had just minted
|
|
174
|
+
// the invitation being claimed. Refusing on those flags meant refusing to
|
|
175
|
+
// knock on a door that was open.
|
|
176
|
+
//
|
|
177
|
+
// So a node with an address is tried, and the node itself decides. Its answer
|
|
178
|
+
// is the authoritative one: `admit_join` re-verifies the invitation, the
|
|
179
|
+
// signature and the admitters list, so nothing is loosened by asking — the
|
|
180
|
+
// worst case is a 403 that names its own cause, which is strictly better than
|
|
181
|
+
// a client-side guess that names a heartbeat.
|
|
182
|
+
const usable = named.filter((n) => n.canAdmit);
|
|
183
|
+
const candidates = usable.length > 0 ? usable : named;
|
|
184
|
+
const advisory = usable.length === 0 || !servable;
|
|
185
|
+
const addressable = candidates.filter((n) => typeof n.relayUrl === 'string' && n.relayUrl.length > 0);
|
|
186
|
+
if (addressable.length === 0) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
step: 'no-relay-url',
|
|
190
|
+
reason: 'A node your invitation names can admit you, but the cloud knows no address for it ' +
|
|
191
|
+
'yet, so there would be nothing to connect through afterwards. Try again shortly.',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// Prefer a node that can do both legs — admit now and author later — so the
|
|
195
|
+
// session lands on one node where possible. Preference only: admission is the
|
|
196
|
+
// step that cannot proceed without a node, and authorship is not on the
|
|
197
|
+
// invitation's list at all, so insisting would refuse a perfectly good
|
|
198
|
+
// admitter for a permission admission never needed.
|
|
199
|
+
const chosen = addressable.find((n) => n.canExecute) ?? addressable.find((n) => n.fresh) ?? addressable[0];
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
relayUrl: chosen.relayUrl,
|
|
203
|
+
// The cloud's ready-made URL when it gave one, so the path is never rebuilt
|
|
204
|
+
// from a guess; the fallback is the documented route on the same origin.
|
|
205
|
+
admitUrl: chosen.admitUrl ??
|
|
206
|
+
`${chosen.relayUrl.replace(/\/+$/, '')}/admin-api/namespaces/${input.namespaceId}/admit`,
|
|
207
|
+
admitterAccount: chosen.account,
|
|
208
|
+
peerId: chosen.peerId,
|
|
209
|
+
writable: chosen.canExecute,
|
|
210
|
+
// Stale when the node's own heartbeat lapsed OR when the cloud's summary
|
|
211
|
+
// disagrees with trying it at all. Both mean the same thing to a caller:
|
|
212
|
+
// this was attempted on advisory data, so if the admit fails that is why.
|
|
213
|
+
stale: !chosen.fresh || advisory,
|
|
214
|
+
via: 'cloud',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { type MeroClient } from '../transport/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a delegated connection needs to be rebuilt after a reload.
|
|
4
|
+
*
|
|
5
|
+
* `deviceSecret` is here because the device key is what signs warrants, and a
|
|
6
|
+
* reload that could not sign would be a dead session. It is a per-tab secret for
|
|
7
|
+
* a certificate the account can revoke — not the account root, which never
|
|
8
|
+
* leaves the wallet's origin.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* What the wallet certified: an account, a device it trusts, and the key that
|
|
12
|
+
* device signs with. Everything except somewhere to send the result.
|
|
13
|
+
*
|
|
14
|
+
* Split out of {@link DelegatedSession} because it is reachable one step
|
|
15
|
+
* earlier. An account that has just enrolled holds exactly this and no relay —
|
|
16
|
+
* it is a member of nothing, so the cloud has no relay to name — and this is the
|
|
17
|
+
* input that resolves one from an invitation instead.
|
|
18
|
+
*/
|
|
19
|
+
export interface DelegatedCredential {
|
|
20
|
+
/** The author's account id, hex. */
|
|
21
|
+
account: string;
|
|
22
|
+
/** The author's `AccountProof<DeviceCert>`, hex borsh. */
|
|
23
|
+
credential: string;
|
|
24
|
+
/** The certified device's ed25519 signing secret, hex. */
|
|
25
|
+
deviceSecret: string;
|
|
26
|
+
}
|
|
27
|
+
export interface DelegatedSession extends DelegatedCredential {
|
|
28
|
+
/**
|
|
29
|
+
* The relay to write through — a node origin, or `null` for none yet.
|
|
30
|
+
*
|
|
31
|
+
* **`null` is a logged-in state, not a broken one.** A brand-new account is a
|
|
32
|
+
* member of nothing, so the cloud names no relay for it — and that is the
|
|
33
|
+
* normal condition of an account that has just been created, because being
|
|
34
|
+
* signed in is how you come to be invited in the first place. Refusing to
|
|
35
|
+
* connect without a relay locked a new account out of the only path that
|
|
36
|
+
* would earn it one.
|
|
37
|
+
*
|
|
38
|
+
* What it does NOT mean is that writes quietly do nothing. There is no
|
|
39
|
+
* client at all without a URL ({@link buildDelegatedClient} returns `null`),
|
|
40
|
+
* so `mero` stays `null`, the session is authenticated, and anything reaching
|
|
41
|
+
* for `rpc.execute` is told the relay is missing. A placeholder URL would be
|
|
42
|
+
* the one unacceptable answer: it would look connected and refuse every
|
|
43
|
+
* warrant.
|
|
44
|
+
*/
|
|
45
|
+
relayUrl: string | null;
|
|
46
|
+
/**
|
|
47
|
+
* The relay's executor account, hex, when the app knows it — the cloud's
|
|
48
|
+
* machine page names it beside the relay's address.
|
|
49
|
+
*
|
|
50
|
+
* Founding a namespace through a relay names the relay's account in the
|
|
51
|
+
* warrant, and a namespace that does not exist yet cannot be asked for it.
|
|
52
|
+
* Without this the account learns it from a namespace it is already in, so a
|
|
53
|
+
* brand-new account would have to join one first; with it, the account founds
|
|
54
|
+
* on `relayUrl` directly.
|
|
55
|
+
*/
|
|
56
|
+
executorAccount?: string | null;
|
|
57
|
+
}
|
|
58
|
+
export declare function readDelegatedSession(): DelegatedSession | null;
|
|
59
|
+
export declare function saveDelegatedSession(s: DelegatedSession): void;
|
|
60
|
+
export declare function clearDelegatedSession(): void;
|
|
61
|
+
/**
|
|
62
|
+
* The enrolled credential, whether or not a relay was ever found for it.
|
|
63
|
+
*
|
|
64
|
+
* A connected session is authoritative: it carries the same three fields and a
|
|
65
|
+
* relay, so it is read first and the standalone record is the fallback. Without
|
|
66
|
+
* that order a stale record from an earlier enrolment could out-rank the account
|
|
67
|
+
* actually connected.
|
|
68
|
+
*/
|
|
69
|
+
export declare function readDelegatedCredential(): DelegatedCredential | null;
|
|
70
|
+
export declare function saveDelegatedCredential(c: DelegatedCredential): void;
|
|
71
|
+
export declare function clearDelegatedCredential(): void;
|
|
72
|
+
/**
|
|
73
|
+
* A warrant nonce is spent on the node, so the counter must outlive the page.
|
|
74
|
+
*
|
|
75
|
+
* In `localStorage`, not session: a reload that restarted from zero would
|
|
76
|
+
* replay a spent nonce and be refused. Keyed by relay and context because the
|
|
77
|
+
* ledger the node keeps is per `(context, author device)`.
|
|
78
|
+
*
|
|
79
|
+
* This is the client-side half of core #4018. The node can answer "where does
|
|
80
|
+
* my sequence stand" — `POST /contexts/{id}/warrant-nonce` with the caller's own
|
|
81
|
+
* `authorProof` — but that route is mounted on the protected router, so a
|
|
82
|
+
* delegated client on a relay cannot reach it. Until it moves, holding the
|
|
83
|
+
* counter locally is the only option, and losing storage means burning refusals
|
|
84
|
+
* until the counter catches up.
|
|
85
|
+
*/
|
|
86
|
+
export declare function persistedNonces(relayUrl: string): {
|
|
87
|
+
next: () => Promise<bigint>;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Record that `nonce` is spent for this device, so later intents start above
|
|
91
|
+
* it. A creation warrant's nonce is spent in the NEW context's own window; one
|
|
92
|
+
* sequence per relay covers that context like any other.
|
|
93
|
+
*/
|
|
94
|
+
export declare function markContextNonceSpent(relayUrl: string, _contextId: string, nonce: bigint): void;
|
|
95
|
+
/**
|
|
96
|
+
* Pin a relay's device signing key, hex (32 bytes), learned out of band.
|
|
97
|
+
*
|
|
98
|
+
* `login()` binds the session to this key, and it must never be read from the
|
|
99
|
+
* relay being logged in to — the answering party would choose what the device
|
|
100
|
+
* signs about. Until the cloud publishes it (mdma#312), it comes from the
|
|
101
|
+
* relay's operator.
|
|
102
|
+
*/
|
|
103
|
+
export declare function pinRelayNodeKey(relayUrl: string, nodeKey: string): void;
|
|
104
|
+
/** The pinned key for a relay, or `null` if none was pinned. */
|
|
105
|
+
export declare function readPinnedRelayNodeKey(relayUrl: string): string | null;
|
|
106
|
+
/**
|
|
107
|
+
* One attempt at a relay's node key, and what became of it:
|
|
108
|
+
*
|
|
109
|
+
* - `learned`: verified (or already pinned), and pinned now;
|
|
110
|
+
* - `unavailable`: something on the way did not answer — the relay, the release
|
|
111
|
+
* mirror, the quote library's download. Nothing was decided; try again.
|
|
112
|
+
* - `refused`: the relay answered and its proof did not hold — a quote that is
|
|
113
|
+
* not of a trusted image, not bound to this request, a mock from a hosted
|
|
114
|
+
* relay. Trying again cannot change that answer into a yes, and must not.
|
|
115
|
+
*/
|
|
116
|
+
export type RelayNodeKeyAttempt = {
|
|
117
|
+
kind: 'learned';
|
|
118
|
+
nodeKey: string;
|
|
119
|
+
} | {
|
|
120
|
+
kind: 'unavailable';
|
|
121
|
+
error: unknown;
|
|
122
|
+
} | {
|
|
123
|
+
kind: 'refused';
|
|
124
|
+
error: unknown;
|
|
125
|
+
};
|
|
126
|
+
export declare function attemptRelayNodeKey(relayUrl: string): Promise<RelayNodeKeyAttempt>;
|
|
127
|
+
/**
|
|
128
|
+
* The relay's node key: the pinned one, or else learned from the relay's TEE
|
|
129
|
+
* attestation in one attempt; `null` when that attempt did not learn it, for
|
|
130
|
+
* whatever reason. Callers that must end up with the key use
|
|
131
|
+
* {@link learnRelayNodeKey}.
|
|
132
|
+
*/
|
|
133
|
+
export declare function resolveRelayNodeKey(relayUrl: string): Promise<string | null>;
|
|
134
|
+
/**
|
|
135
|
+
* How long to wait before each retry of an unavailable relay key: quickly at
|
|
136
|
+
* first, for a blip or a cold cache, then every 30 s for as long as it takes.
|
|
137
|
+
*/
|
|
138
|
+
export declare const RELAY_NODE_KEY_RETRY_MS: readonly number[];
|
|
139
|
+
/**
|
|
140
|
+
* The relay's node key, however long the way to it is down.
|
|
141
|
+
*
|
|
142
|
+
* One failed fetch of the release used to cost the whole session: no key, so no
|
|
143
|
+
* relay session, so every admin read and every event stream unauthenticated
|
|
144
|
+
* until a reload. A transport failure here is now waited out with backoff
|
|
145
|
+
* ({@link RELAY_NODE_KEY_RETRY_MS}) until the key is learned or `signal` aborts.
|
|
146
|
+
*
|
|
147
|
+
* A refusal ends it at once with `null`. A quote that did not verify is an
|
|
148
|
+
* answer, and asking again is not how it becomes a different one.
|
|
149
|
+
*/
|
|
150
|
+
export declare function learnRelayNodeKey(relayUrl: string, options?: {
|
|
151
|
+
signal?: AbortSignal;
|
|
152
|
+
}): Promise<string | null>;
|
|
153
|
+
/** Forget what was learned about which methods read. For tests. */
|
|
154
|
+
export declare function forgetMethodKinds(): void;
|
|
155
|
+
/**
|
|
156
|
+
* Build the client a delegated session talks through.
|
|
157
|
+
*
|
|
158
|
+
* The result is a relay-transport `MeroClient`. Its `rpc.execute` takes the same
|
|
159
|
+
* named arguments a node client's does, which is what lets a generated ABI
|
|
160
|
+
* client — and every hook that only calls `execute` — work unchanged. What it
|
|
161
|
+
* also has is `admin` and `events`, pointed at the relay and authenticated as
|
|
162
|
+
* the account by the device certificate, so the caller-scoped reads answer this
|
|
163
|
+
* account's own contexts and namespaces rather than the node's.
|
|
164
|
+
*/
|
|
165
|
+
export declare function buildDelegatedClient(s: DelegatedSession, _contextId: string | null): MeroClient | null;
|
|
166
|
+
export interface RelayMap {
|
|
167
|
+
namespaces: Record<string, string>;
|
|
168
|
+
contexts: Record<string, string>;
|
|
169
|
+
}
|
|
170
|
+
export declare function readRelayMap(account: string): RelayMap;
|
|
171
|
+
/** Record the relay that admitted this account to a namespace (and, when known, the context it came for). */
|
|
172
|
+
export declare function rememberRelay(account: string, relayUrl: string, at: {
|
|
173
|
+
namespaceId?: string;
|
|
174
|
+
contextId?: string;
|
|
175
|
+
}): void;
|
|
176
|
+
/** The relay that serves a context, if this account has learned it. */
|
|
177
|
+
export declare function relayForContext(account: string, contextId: string): string | null;
|
|
178
|
+
/**
|
|
179
|
+
* A session moving to `next`, keeping the relay's executor account the previous
|
|
180
|
+
* one knew when both are the same relay.
|
|
181
|
+
*
|
|
182
|
+
* A join hands back a session built from the admitting node alone, which has no
|
|
183
|
+
* executor on it. When that node is the relay the account was already on — the
|
|
184
|
+
* one the cloud assigned it, say — dropping the executor would cost nothing
|
|
185
|
+
* today (the joined namespace can answer for it) but leaves the session knowing
|
|
186
|
+
* less than it did. On a different relay the old executor is the wrong account,
|
|
187
|
+
* so it is left behind.
|
|
188
|
+
*/
|
|
189
|
+
export declare function carryExecutorAccount(prev: DelegatedSession | null, next: DelegatedSession): DelegatedSession;
|
|
190
|
+
/** Every relay this account is known to use: the session's, and each in the map. */
|
|
191
|
+
export declare function knownRelays(s: DelegatedSession): string[];
|
|
192
|
+
type ListedContext = Awaited<ReturnType<MeroClient['admin']['getContexts']>>['contexts'][number];
|
|
193
|
+
/**
|
|
194
|
+
* This account's contexts on every relay it knows, each tagged with the relay
|
|
195
|
+
* that listed it — and that relay recorded as the one serving it.
|
|
196
|
+
*
|
|
197
|
+
* Each relay answers only for itself and only for this caller, so the union is
|
|
198
|
+
* exactly the account's contexts across relays. A relay that cannot be reached
|
|
199
|
+
* or logged in to is skipped (its contexts simply do not appear), not fatal.
|
|
200
|
+
*/
|
|
201
|
+
export declare function listDelegatedContexts(s: DelegatedSession): Promise<Array<ListedContext & {
|
|
202
|
+
relayUrl: string;
|
|
203
|
+
}>>;
|
|
204
|
+
type ListedNamespace = Awaited<ReturnType<MeroClient['admin']['listNamespaces']>>[number];
|
|
205
|
+
/**
|
|
206
|
+
* This account's namespaces on every relay it knows, each tagged with the relay
|
|
207
|
+
* that listed it and recorded as that namespace's relay. Same rules as
|
|
208
|
+
* {@link listDelegatedContexts}: each relay answers for itself and this caller
|
|
209
|
+
* only (`namespace:list-own`); an unreachable relay is skipped.
|
|
210
|
+
*/
|
|
211
|
+
export declare function listDelegatedNamespaces(s: DelegatedSession): Promise<Array<ListedNamespace & {
|
|
212
|
+
relayUrl: string;
|
|
213
|
+
}>>;
|
|
214
|
+
export {};
|