@calimero-network/mero-js 25.3.2 → 26.0.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.
@@ -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, createRecoveringNonceSource, authorNonceLookup, WarrantNonceExhaustedError, } from './nonce-source.js';
4
- export type { NonceSource, WarrantNonceLookup, RecoveringNonceSourceOptions, } from './nonce-source.js';
3
+ export { createMemoryNonceSource, createLocalStorageNonceSource, } from './nonce-source.js';
4
+ export type { NonceSource } from './nonce-source.js';
@@ -1,2 +1,2 @@
1
1
  export { RelayClient, IntentRefusedError } from './relay-client.js';
2
- export { createMemoryNonceSource, createLocalStorageNonceSource, createRecoveringNonceSource, authorNonceLookup, WarrantNonceExhaustedError, } from './nonce-source.js';
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, and {@link createRecoveringNonceSource}
34
- * keeps the floor itself in that case. Implementing it is what lets a
35
- * *persistent* source remember a jump, so a later cold start that cannot
36
- * reach a node resumes above the mark rather than below it.
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 — or handed a recovering source, which asks
81
- // the node what it already spent instead of guessing.
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
- * A client whose storage may be cleared between sessions should wrap its
80
- * counter in `createRecoveringNonceSource`, which asks the node where the
81
- * sequence actually stands before the first mint instead of restarting at 1.
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calimero-network/mero-js",
3
- "version": "25.3.2",
3
+ "version": "26.0.0",
4
4
  "description": "Pure JavaScript SDK for Calimero",
5
5
  "type": "module",
6
6
  "exports": {