@calimero-network/mero-js 25.3.3 → 26.0.1
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/join-with-node.d.ts +7 -12
- package/dist/account/join-with-node.js +7 -12
- package/dist/account/session.d.ts +3 -6
- package/dist/account/session.js +3 -6
- package/dist/admin-api/admin-client.d.ts +2 -133
- package/dist/admin-api/admin-client.js +1 -226
- package/dist/admin-api/admin-types.d.ts +0 -146
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +0 -262
- package/dist/index.cjs.map +2 -2
- package/dist/index.mjs +0 -262
- package/dist/index.mjs.map +2 -2
- package/dist/relay/index.d.ts +2 -2
- package/dist/relay/index.js +1 -1
- package/dist/relay/nonce-source.d.ts +4 -127
- package/dist/relay/nonce-source.js +2 -154
- package/dist/relay/relay-client.d.ts +3 -3
- package/package.json +1 -1
package/dist/relay/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { RelayClient, IntentRefusedError } from './relay-client.js';
|
|
2
2
|
export type { RelayClientConfig, RelayExecutor, RelayDescription, IntentResult, CreationDescription, CreateContextInput, CreatedContext, GovernanceDescription, GovernInput, GovernResult, FoundNamespaceInput, FoundedNamespace, } from './relay-client.js';
|
|
3
|
-
export { createMemoryNonceSource, createLocalStorageNonceSource,
|
|
4
|
-
export type { NonceSource
|
|
3
|
+
export { createMemoryNonceSource, createLocalStorageNonceSource, } from './nonce-source.js';
|
|
4
|
+
export type { NonceSource } from './nonce-source.js';
|
package/dist/relay/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { RelayClient, IntentRefusedError } from './relay-client.js';
|
|
2
|
-
export { createMemoryNonceSource, createLocalStorageNonceSource,
|
|
2
|
+
export { createMemoryNonceSource, createLocalStorageNonceSource, } from './nonce-source.js';
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
* is the one you have to ask for. It is also why a source may jump: reserving a
|
|
23
23
|
* block and losing the tail is the correct trade.
|
|
24
24
|
*/
|
|
25
|
-
import type { WarrantNonceState } from '../admin-api/admin-types.js';
|
|
26
25
|
/** Produces the next nonce for one author device. Must never go backwards. */
|
|
27
26
|
export interface NonceSource {
|
|
28
27
|
next(): Promise<bigint>;
|
|
@@ -30,10 +29,10 @@ export interface NonceSource {
|
|
|
30
29
|
* Raise the floor of the sequence to `floor`, if this source can record it.
|
|
31
30
|
*
|
|
32
31
|
* Optional, and additive: a source that does not implement it still works
|
|
33
|
-
* everywhere a `NonceSource` is taken
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
32
|
+
* everywhere a `NonceSource` is taken. Implementing it is what lets a caller
|
|
33
|
+
* that learns a higher spent nonce out of band — a refusal naming the nonce,
|
|
34
|
+
* say — skip ahead, and lets a *persistent* source remember that jump across
|
|
35
|
+
* a cold start.
|
|
37
36
|
*
|
|
38
37
|
* Never lowers: a smaller `floor` than the source's own position is ignored.
|
|
39
38
|
*/
|
|
@@ -71,125 +70,3 @@ export declare function createLocalStorageNonceSource(key: string, storage?: {
|
|
|
71
70
|
getItem(k: string): string | null;
|
|
72
71
|
setItem(k: string, v: string): void;
|
|
73
72
|
}): NonceSource;
|
|
74
|
-
/** A node (or anything else) that can answer where a device's sequence stands. */
|
|
75
|
-
export interface WarrantNonceLookup {
|
|
76
|
-
getWarrantNonce(contextId: string, authorDeviceKey: string): Promise<WarrantNonceState>;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* A lookup a delegated client can actually use.
|
|
80
|
-
*
|
|
81
|
-
* {@link WarrantNonceLookup} names the device key because the admin route takes
|
|
82
|
-
* it in the path. The delegated route does not: it reads the key from the
|
|
83
|
-
* certificate it verifies, so the only way to ask is to present that
|
|
84
|
-
* certificate. This adapter bridges the two — it satisfies the interface, and
|
|
85
|
-
* ignores the device key argument because the credential already determines it.
|
|
86
|
-
*
|
|
87
|
-
* That the argument is ignored is not an oversight to tidy away. The whole
|
|
88
|
-
* point of the delegated route is that the device key is not caller-supplied:
|
|
89
|
-
* a key passed alongside the proof would be a second spelling of a fact the
|
|
90
|
-
* signature already fixes, and two spellings can differ. The adapter therefore
|
|
91
|
-
* has nothing to check it against, and pretending otherwise would invent a
|
|
92
|
-
* comparison the protocol deliberately does not make.
|
|
93
|
-
*
|
|
94
|
-
* Use with {@link createRecoveringNonceSource} exactly as an `AdminApiClient`
|
|
95
|
-
* would be used — on a relay, this is the form that works.
|
|
96
|
-
*/
|
|
97
|
-
export declare function authorNonceLookup(options: {
|
|
98
|
-
/** Anything that can reach the node's admin path; an `AdminApiClient` does. */
|
|
99
|
-
client: {
|
|
100
|
-
getWarrantNonceAsAuthor(contextId: string, authorProof: string): Promise<WarrantNonceState>;
|
|
101
|
-
};
|
|
102
|
-
/** Hex borsh `AccountProof<DeviceCert>` — the same one the relay client sends. */
|
|
103
|
-
authorProof: string;
|
|
104
|
-
}): WarrantNonceLookup;
|
|
105
|
-
/** Thrown when every node asked says this device has spent `u64::MAX` here. */
|
|
106
|
-
export declare class WarrantNonceExhaustedError extends Error {
|
|
107
|
-
readonly contextId: string;
|
|
108
|
-
readonly authorDeviceKey: string;
|
|
109
|
-
name: string;
|
|
110
|
-
constructor(contextId: string, authorDeviceKey: string);
|
|
111
|
-
}
|
|
112
|
-
/** What {@link createRecoveringNonceSource} needs to know. */
|
|
113
|
-
export interface RecoveringNonceSourceOptions {
|
|
114
|
-
/**
|
|
115
|
-
* The node(s) to ask. Pass an {@link AdminApiClient} — it satisfies this
|
|
116
|
-
* structurally — or several, one per relay this author writes through.
|
|
117
|
-
*
|
|
118
|
-
* With more than one, the **highest** answer wins. Nonce state folds per peer,
|
|
119
|
-
* so a node can only be behind a client whose warrants it has not applied,
|
|
120
|
-
* never ahead of the truth; taking the highest is therefore the only reading
|
|
121
|
-
* that cannot hand out a nonce somebody has already spent.
|
|
122
|
-
*/
|
|
123
|
-
lookups: WarrantNonceLookup | readonly WarrantNonceLookup[];
|
|
124
|
-
contextId: string;
|
|
125
|
-
/** The author device's SIGNING key, base58 or hex. Not its `deviceId`. */
|
|
126
|
-
authorDeviceKey: string;
|
|
127
|
-
/**
|
|
128
|
-
* The client's own counter, folded in with `max`. Defaults to an in-memory
|
|
129
|
-
* one starting at 1.
|
|
130
|
-
*
|
|
131
|
-
* Pass {@link createLocalStorageNonceSource} to keep both halves: the local
|
|
132
|
-
* counter carries the sequence forward between calls, the node supplies the
|
|
133
|
-
* floor when that counter has been cleared.
|
|
134
|
-
*/
|
|
135
|
-
local?: NonceSource;
|
|
136
|
-
/**
|
|
137
|
-
* What to do when no node can be reached on the cold-start lookup.
|
|
138
|
-
*
|
|
139
|
-
* `'throw'` (the default) refuses to mint. That is the conservative reading:
|
|
140
|
-
* a source that quietly fell back to a cleared local counter would replay
|
|
141
|
-
* spent nonces, which is the exact failure this exists to prevent, and it
|
|
142
|
-
* would do it invisibly. `'local'` mints from the local counter anyway —
|
|
143
|
-
* correct only when that counter is known to be intact and the app would
|
|
144
|
-
* rather write offline than not at all.
|
|
145
|
-
*/
|
|
146
|
-
onLookupFailure?: 'throw' | 'local';
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* A nonce source that asks the node where the sequence stands before its first
|
|
150
|
-
* mint, then carries on locally.
|
|
151
|
-
*
|
|
152
|
-
* # Why this is not the default
|
|
153
|
-
*
|
|
154
|
-
* It costs a round trip and an admin credential, and an author that never lost
|
|
155
|
-
* its counter gains nothing from it. It earns its place in exactly one
|
|
156
|
-
* situation, which is common rather than exotic: a client whose local state is
|
|
157
|
-
* gone. A browser keyholder lives in storage that is partitioned per top-level
|
|
158
|
-
* site and cleared on a schedule it does not control, so for it, losing the
|
|
159
|
-
* counter is the normal lifecycle. Without this, such a client either restarts
|
|
160
|
-
* at 1 — replaying every nonce it already spent, each write refused, which
|
|
161
|
-
* reads to the user as the app silently not saving — or guesses upward, one
|
|
162
|
-
* burnt round trip per wrong guess.
|
|
163
|
-
*
|
|
164
|
-
* # What it does
|
|
165
|
-
*
|
|
166
|
-
* The lookup happens once, lazily, on the first {@link NonceSource.next}: a
|
|
167
|
-
* source that is constructed and never used costs nothing, and an app that
|
|
168
|
-
* builds its relay client at startup does not pay a network call for it. The
|
|
169
|
-
* answer becomes a **floor**, and every nonce handed out afterwards is
|
|
170
|
-
* `max(localCounter, floor)`. It never goes backwards from either input, which
|
|
171
|
-
* is the one property a nonce source owes its caller.
|
|
172
|
-
*
|
|
173
|
-
* If the local counter is the one that is ahead, it wins — the node may simply
|
|
174
|
-
* not have applied this client's recent warrants yet.
|
|
175
|
-
*
|
|
176
|
-
* The floor is also pushed into the local source through
|
|
177
|
-
* {@link NonceSource.advanceTo} when it has one, so a persistent counter
|
|
178
|
-
* remembers the jump.
|
|
179
|
-
*
|
|
180
|
-
* # Exhaustion
|
|
181
|
-
*
|
|
182
|
-
* A node reporting no `nextNonce` means `u64::MAX` is spent and this device can
|
|
183
|
-
* never write here again. `next()` rejects with
|
|
184
|
-
* {@link WarrantNonceExhaustedError} rather than guessing, because every guess
|
|
185
|
-
* from there is refused forever.
|
|
186
|
-
*
|
|
187
|
-
* # On an older node
|
|
188
|
-
*
|
|
189
|
-
* The route is new and absent from core `master` builds, which answer 404 —
|
|
190
|
-
* {@link AdminApiClient.getWarrantNonce} surfaces that as
|
|
191
|
-
* `WarrantNonceRouteUnavailableError`. It reaches the caller as a lookup
|
|
192
|
-
* failure, so `onLookupFailure` decides: refuse to mint, or fall back to the
|
|
193
|
-
* local counter.
|
|
194
|
-
*/
|
|
195
|
-
export declare function createRecoveringNonceSource(options: RecoveringNonceSourceOptions): NonceSource;
|
|
@@ -77,8 +77,8 @@ export function createLocalStorageNonceSource(key, storage) {
|
|
|
77
77
|
// already written would replay. But the client cannot know, and 1 is the only
|
|
78
78
|
// defensible start, so it is what a fresh key gets. This is why the doc above
|
|
79
79
|
// tells apps to key per device: a device whose storage was cleared should be
|
|
80
|
-
// re-paired rather than resumed
|
|
81
|
-
// the
|
|
80
|
+
// re-paired rather than resumed. No node serves a "where does my sequence
|
|
81
|
+
// stand" lookup, so the local counter is the only record there is.
|
|
82
82
|
const lastHandedOut = () => {
|
|
83
83
|
const raw = store.getItem(key);
|
|
84
84
|
if (raw === null || raw === '')
|
|
@@ -109,155 +109,3 @@ export function createLocalStorageNonceSource(key, storage) {
|
|
|
109
109
|
},
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
|
-
/**
|
|
113
|
-
* A lookup a delegated client can actually use.
|
|
114
|
-
*
|
|
115
|
-
* {@link WarrantNonceLookup} names the device key because the admin route takes
|
|
116
|
-
* it in the path. The delegated route does not: it reads the key from the
|
|
117
|
-
* certificate it verifies, so the only way to ask is to present that
|
|
118
|
-
* certificate. This adapter bridges the two — it satisfies the interface, and
|
|
119
|
-
* ignores the device key argument because the credential already determines it.
|
|
120
|
-
*
|
|
121
|
-
* That the argument is ignored is not an oversight to tidy away. The whole
|
|
122
|
-
* point of the delegated route is that the device key is not caller-supplied:
|
|
123
|
-
* a key passed alongside the proof would be a second spelling of a fact the
|
|
124
|
-
* signature already fixes, and two spellings can differ. The adapter therefore
|
|
125
|
-
* has nothing to check it against, and pretending otherwise would invent a
|
|
126
|
-
* comparison the protocol deliberately does not make.
|
|
127
|
-
*
|
|
128
|
-
* Use with {@link createRecoveringNonceSource} exactly as an `AdminApiClient`
|
|
129
|
-
* would be used — on a relay, this is the form that works.
|
|
130
|
-
*/
|
|
131
|
-
export function authorNonceLookup(options) {
|
|
132
|
-
return {
|
|
133
|
-
getWarrantNonce: (contextId, _authorDeviceKey) => options.client.getWarrantNonceAsAuthor(contextId, options.authorProof),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
/** Thrown when every node asked says this device has spent `u64::MAX` here. */
|
|
137
|
-
export class WarrantNonceExhaustedError extends Error {
|
|
138
|
-
constructor(contextId, authorDeviceKey) {
|
|
139
|
-
super(`author device ${authorDeviceKey} has spent every nonce in context ${contextId}; it must re-key to write again`);
|
|
140
|
-
this.contextId = contextId;
|
|
141
|
-
this.authorDeviceKey = authorDeviceKey;
|
|
142
|
-
this.name = 'WarrantNonceExhaustedError';
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* A nonce source that asks the node where the sequence stands before its first
|
|
147
|
-
* mint, then carries on locally.
|
|
148
|
-
*
|
|
149
|
-
* # Why this is not the default
|
|
150
|
-
*
|
|
151
|
-
* It costs a round trip and an admin credential, and an author that never lost
|
|
152
|
-
* its counter gains nothing from it. It earns its place in exactly one
|
|
153
|
-
* situation, which is common rather than exotic: a client whose local state is
|
|
154
|
-
* gone. A browser keyholder lives in storage that is partitioned per top-level
|
|
155
|
-
* site and cleared on a schedule it does not control, so for it, losing the
|
|
156
|
-
* counter is the normal lifecycle. Without this, such a client either restarts
|
|
157
|
-
* at 1 — replaying every nonce it already spent, each write refused, which
|
|
158
|
-
* reads to the user as the app silently not saving — or guesses upward, one
|
|
159
|
-
* burnt round trip per wrong guess.
|
|
160
|
-
*
|
|
161
|
-
* # What it does
|
|
162
|
-
*
|
|
163
|
-
* The lookup happens once, lazily, on the first {@link NonceSource.next}: a
|
|
164
|
-
* source that is constructed and never used costs nothing, and an app that
|
|
165
|
-
* builds its relay client at startup does not pay a network call for it. The
|
|
166
|
-
* answer becomes a **floor**, and every nonce handed out afterwards is
|
|
167
|
-
* `max(localCounter, floor)`. It never goes backwards from either input, which
|
|
168
|
-
* is the one property a nonce source owes its caller.
|
|
169
|
-
*
|
|
170
|
-
* If the local counter is the one that is ahead, it wins — the node may simply
|
|
171
|
-
* not have applied this client's recent warrants yet.
|
|
172
|
-
*
|
|
173
|
-
* The floor is also pushed into the local source through
|
|
174
|
-
* {@link NonceSource.advanceTo} when it has one, so a persistent counter
|
|
175
|
-
* remembers the jump.
|
|
176
|
-
*
|
|
177
|
-
* # Exhaustion
|
|
178
|
-
*
|
|
179
|
-
* A node reporting no `nextNonce` means `u64::MAX` is spent and this device can
|
|
180
|
-
* never write here again. `next()` rejects with
|
|
181
|
-
* {@link WarrantNonceExhaustedError} rather than guessing, because every guess
|
|
182
|
-
* from there is refused forever.
|
|
183
|
-
*
|
|
184
|
-
* # On an older node
|
|
185
|
-
*
|
|
186
|
-
* The route is new and absent from core `master` builds, which answer 404 —
|
|
187
|
-
* {@link AdminApiClient.getWarrantNonce} surfaces that as
|
|
188
|
-
* `WarrantNonceRouteUnavailableError`. It reaches the caller as a lookup
|
|
189
|
-
* failure, so `onLookupFailure` decides: refuse to mint, or fall back to the
|
|
190
|
-
* local counter.
|
|
191
|
-
*/
|
|
192
|
-
export function createRecoveringNonceSource(options) {
|
|
193
|
-
const lookups = Array.isArray(options.lookups)
|
|
194
|
-
? options.lookups
|
|
195
|
-
: [options.lookups];
|
|
196
|
-
if (lookups.length === 0) {
|
|
197
|
-
throw new Error('a recovering nonce source needs at least one node to ask');
|
|
198
|
-
}
|
|
199
|
-
const local = options.local ?? createMemoryNonceSource(1);
|
|
200
|
-
const onLookupFailure = options.onLookupFailure ?? 'throw';
|
|
201
|
-
let floor = null;
|
|
202
|
-
// Memoised so concurrent first calls ask once. Cleared on failure, since a
|
|
203
|
-
// node that was unreachable a moment ago may answer the next call — caching
|
|
204
|
-
// the failure would make one flaky request poison the whole session.
|
|
205
|
-
let pending = null;
|
|
206
|
-
const resolveFloor = async () => {
|
|
207
|
-
const answers = await Promise.allSettled(lookups.map((lookup) => lookup.getWarrantNonce(options.contextId, options.authorDeviceKey)));
|
|
208
|
-
let highest = null;
|
|
209
|
-
let exhausted = false;
|
|
210
|
-
for (const answer of answers) {
|
|
211
|
-
if (answer.status !== 'fulfilled')
|
|
212
|
-
continue;
|
|
213
|
-
const state = answer.value;
|
|
214
|
-
if (state.kind === 'exhausted') {
|
|
215
|
-
exhausted = true;
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
if (highest === null || state.nextNonce > highest)
|
|
219
|
-
highest = state.nextNonce;
|
|
220
|
-
}
|
|
221
|
-
// Exhaustion is only decisive when nothing usable came back. One node
|
|
222
|
-
// saturated while another still has room is not a thing the ledger can
|
|
223
|
-
// produce — the sequence is the device's, not the node's — but if it ever
|
|
224
|
-
// is, the usable answer is the actionable one.
|
|
225
|
-
if (highest === null && exhausted) {
|
|
226
|
-
throw new WarrantNonceExhaustedError(options.contextId, options.authorDeviceKey);
|
|
227
|
-
}
|
|
228
|
-
if (highest === null) {
|
|
229
|
-
const first = answers.find((a) => a.status === 'rejected');
|
|
230
|
-
const reason = first && first.status === 'rejected' ? first.reason : undefined;
|
|
231
|
-
if (onLookupFailure === 'local') {
|
|
232
|
-
floor = 0n;
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
throw reason instanceof Error
|
|
236
|
-
? reason
|
|
237
|
-
: new Error(`no node could answer where the warrant-nonce sequence stands: ${String(reason)}`);
|
|
238
|
-
}
|
|
239
|
-
floor = highest;
|
|
240
|
-
await local.advanceTo?.(highest);
|
|
241
|
-
};
|
|
242
|
-
return {
|
|
243
|
-
async next() {
|
|
244
|
-
if (floor === null) {
|
|
245
|
-
pending ?? (pending = resolveFloor().finally(() => {
|
|
246
|
-
pending = null;
|
|
247
|
-
}));
|
|
248
|
-
await pending;
|
|
249
|
-
}
|
|
250
|
-
const mine = await local.next();
|
|
251
|
-
const value = floor !== null && floor > mine ? floor : mine;
|
|
252
|
-
// Advance the floor past what was just handed out, so a local source that
|
|
253
|
-
// is behind cannot re-offer this number on the next call.
|
|
254
|
-
floor = value + 1n;
|
|
255
|
-
return value;
|
|
256
|
-
},
|
|
257
|
-
async advanceTo(wanted) {
|
|
258
|
-
if (floor === null || wanted > floor)
|
|
259
|
-
floor = wanted;
|
|
260
|
-
await local.advanceTo?.(wanted);
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
|
-
}
|
|
@@ -76,9 +76,9 @@ export interface RelayClientConfig {
|
|
|
76
76
|
/**
|
|
77
77
|
* Where nonces come from. See {@link NonceSource} — a reset replays.
|
|
78
78
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
79
|
+
* Persist it (`createLocalStorageNonceSource` in a browser): the node keeps
|
|
80
|
+
* the spent ledger but serves no lookup of it, so the client's own counter is
|
|
81
|
+
* the only record of where the sequence stands.
|
|
82
82
|
*/
|
|
83
83
|
nonces: NonceSource;
|
|
84
84
|
/**
|