signet-protocol 1.10.0 → 1.11.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.
Files changed (57) hide show
  1. package/dist/bonds.d.ts +8 -3
  2. package/dist/bonds.d.ts.map +1 -1
  3. package/dist/bonds.js +17 -6
  4. package/dist/bonds.js.map +1 -1
  5. package/dist/bot-ownership.d.ts +61 -0
  6. package/dist/bot-ownership.d.ts.map +1 -0
  7. package/dist/bot-ownership.js +88 -0
  8. package/dist/bot-ownership.js.map +1 -0
  9. package/dist/crypto.d.ts +2 -0
  10. package/dist/crypto.d.ts.map +1 -1
  11. package/dist/crypto.js +4 -0
  12. package/dist/crypto.js.map +1 -1
  13. package/dist/experimental.d.ts +19 -0
  14. package/dist/experimental.d.ts.map +1 -0
  15. package/dist/experimental.js +16 -0
  16. package/dist/experimental.js.map +1 -0
  17. package/dist/index.js +3 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/types.d.ts +7 -1
  20. package/dist/types.d.ts.map +1 -1
  21. package/dist/types.js.map +1 -1
  22. package/dist/url-auth.d.ts.map +1 -1
  23. package/dist/url-auth.js +2 -1
  24. package/dist/url-auth.js.map +1 -1
  25. package/dist/utils.d.ts +46 -0
  26. package/dist/utils.d.ts.map +1 -1
  27. package/dist/utils.js +127 -0
  28. package/dist/utils.js.map +1 -1
  29. package/dist/vault-checkpoint.d.ts +50 -0
  30. package/dist/vault-checkpoint.d.ts.map +1 -0
  31. package/dist/vault-checkpoint.js +84 -0
  32. package/dist/vault-checkpoint.js.map +1 -0
  33. package/dist/vault-keys.d.ts +19 -0
  34. package/dist/vault-keys.d.ts.map +1 -0
  35. package/dist/vault-keys.js +57 -0
  36. package/dist/vault-keys.js.map +1 -0
  37. package/dist/vault-recovery.d.ts +99 -0
  38. package/dist/vault-recovery.d.ts.map +1 -0
  39. package/dist/vault-recovery.js +305 -0
  40. package/dist/vault-recovery.js.map +1 -0
  41. package/dist/vault-relay.d.ts +29 -0
  42. package/dist/vault-relay.d.ts.map +1 -0
  43. package/dist/vault-relay.js +131 -0
  44. package/dist/vault-relay.js.map +1 -0
  45. package/package.json +10 -5
  46. package/src/bonds.ts +18 -6
  47. package/src/bot-ownership.ts +77 -0
  48. package/src/crypto.ts +5 -0
  49. package/src/experimental.ts +25 -0
  50. package/src/index.ts +4 -1
  51. package/src/types.ts +8 -2
  52. package/src/url-auth.ts +2 -1
  53. package/src/utils.ts +122 -0
  54. package/src/vault-checkpoint.ts +98 -0
  55. package/src/vault-keys.ts +59 -0
  56. package/src/vault-recovery.ts +284 -0
  57. package/src/vault-relay.ts +112 -0
@@ -0,0 +1,305 @@
1
+ import { verifyEvent } from './crypto.js';
2
+ import { MAX_VAULT_CHUNKS, MAX_VAULT_CONTROL_BYTES, VAULT_EVENT_KIND, matchesVaultChunk, parseVaultCheckpoint, vaultCheckpointShapedTag, vaultCheckpointTag, vaultContentHash } from './vault-checkpoint.js';
3
+ /** A checkpoint dated further than this beyond the reader's clock is rejected,
4
+ * so one device with a bad clock cannot outrank every later honest head. */
5
+ export const VAULT_CLOCK_TOLERANCE_SECONDS = 300;
6
+ const MAX_VAULT_HEADS = 16;
7
+ /** Rotations recovery will walk. A vault rotated more than 32 times reads
8
+ * `unusable`; there is no way past it without raising this bound. */
9
+ const MAX_ROTATION_HOPS = 32;
10
+ const encoder = new TextEncoder();
11
+ const nowSeconds = () => Math.floor(Date.now() / 1000);
12
+ /** Carry the failed relay list from a `VaultRelayError` (or any error with a
13
+ * string-array `failedRelays`) into the result. */
14
+ function unavailable(error) {
15
+ const failed = error?.failedRelays;
16
+ return Array.isArray(failed) && failed.every(f => typeof f === 'string')
17
+ ? { state: 'unavailable', failedRelays: [...failed] } : { state: 'unavailable' };
18
+ }
19
+ function checkNow(now) {
20
+ if (!Number.isSafeInteger(now) || now < 0)
21
+ throw new Error('Vault reader clock must be a non-negative integer');
22
+ return now;
23
+ }
24
+ function checkMinRotation(minRotation) {
25
+ if (!Number.isSafeInteger(minRotation) || minRotation < 0) {
26
+ throw new Error('Vault reader minRotation must be a non-negative integer');
27
+ }
28
+ return minRotation;
29
+ }
30
+ /** The highest rotation number a caller's floors already name, ignoring any
31
+ * non-integer or negative key. -1 when `floors` names none, so a rotation-0
32
+ * result is never treated as a rollback when nothing has been seen yet. */
33
+ function highestFloorRotation(floors) {
34
+ let highest = -1;
35
+ for (const key of Object.keys(floors)) {
36
+ const rotation = Number(key);
37
+ if (Number.isInteger(rotation) && rotation >= 0 && rotation > highest)
38
+ highest = rotation;
39
+ }
40
+ return highest;
41
+ }
42
+ /** Ascending (created_at, id): the order used for every newest/tie decision.
43
+ * Of two events with the same created_at, the lower ID is the newer head,
44
+ * matching NIP-01 replaceable-event resolution. */
45
+ const older = (a, b) => a.created_at - b.created_at || (a.id < b.id ? 1 : a.id > b.id ? -1 : 0);
46
+ /** Structural, bound and signature checks shared by every checkpoint read. */
47
+ async function authenticCheckpointEvent(event, author, now) {
48
+ if (!event || event.kind !== VAULT_EVENT_KIND || event.pubkey !== author || typeof event.id !== 'string'
49
+ || typeof event.content !== 'string' || encoder.encode(event.content).length > MAX_VAULT_CONTROL_BYTES
50
+ || !Number.isSafeInteger(event.created_at) || event.created_at < 0
51
+ || event.created_at > now + VAULT_CLOCK_TOLERANCE_SECONDS)
52
+ return null;
53
+ const tag = vaultCheckpointShapedTag(event);
54
+ return tag !== null && await verifyEvent(event) ? tag : null;
55
+ }
56
+ /** Read one rotation. Callers may fall back to legacy ONLY on `absent`.
57
+ * Invalid, incomplete, offline and rolled-back backups are distinguishable from
58
+ * never-migrated accounts. No state is persisted or marked canonical here.
59
+ * `now` (unix seconds, default the local clock) bounds created_at from above.
60
+ */
61
+ export async function readVaultSnapshot(reader, expected) {
62
+ const now = checkNow(expected.now ?? nowSeconds());
63
+ const tag = vaultCheckpointTag(expected.author, expected.publisher);
64
+ let candidates;
65
+ try {
66
+ candidates = await reader.checkpoints(expected.author, tag);
67
+ }
68
+ catch (e) {
69
+ return unavailable(e);
70
+ }
71
+ if (!candidates.length)
72
+ return { state: 'absent' };
73
+ if (candidates.length > 128)
74
+ return { state: 'unusable', reason: 'checkpoint' };
75
+ // A corrupt newer event does not hide a valid older event. Once a valid signed
76
+ // event is chosen, however, failed decrypt/schema is not a licence to downgrade.
77
+ const valid = [];
78
+ for (const event of candidates) {
79
+ if (await authenticCheckpointEvent(event, expected.author, now) === tag)
80
+ valid.push(event);
81
+ }
82
+ valid.sort((a, b) => older(b, a));
83
+ const event = valid[0];
84
+ if (!event)
85
+ return { state: 'unusable', reason: 'checkpoint' };
86
+ let raw;
87
+ try {
88
+ raw = await reader.open(event.content, expected.author);
89
+ }
90
+ catch {
91
+ return { state: 'unusable', reason: 'checkpoint' };
92
+ }
93
+ const checkpoint = raw === null ? null : parseVaultCheckpoint(raw, expected);
94
+ if (!checkpoint || checkpoint.publisher !== expected.publisher)
95
+ return { state: 'unusable', reason: 'checkpoint' };
96
+ if (checkpoint.sequence < (expected.minSequence ?? 0))
97
+ return { state: 'unusable', reason: 'rollback' };
98
+ const parts = [];
99
+ let totalBytes = 0;
100
+ for (const ref of checkpoint.chunks) {
101
+ let chunk;
102
+ try {
103
+ chunk = await reader.chunk(ref.eventId);
104
+ }
105
+ catch (e) {
106
+ return unavailable(e);
107
+ }
108
+ if (!chunk || typeof chunk.content !== 'string' || !matchesVaultChunk(ref, chunk) || !await verifyEvent(chunk)) {
109
+ return { state: 'unusable', reason: 'chunk' };
110
+ }
111
+ let part;
112
+ try {
113
+ part = await reader.open(chunk.content, expected.author);
114
+ }
115
+ catch {
116
+ return { state: 'unusable', reason: 'chunk' };
117
+ }
118
+ if (part === null)
119
+ return { state: 'unusable', reason: 'chunk' };
120
+ totalBytes += encoder.encode(part).length;
121
+ if (totalBytes > MAX_VAULT_CHUNKS * 65536)
122
+ return { state: 'unusable', reason: 'chunk' };
123
+ parts.push(part);
124
+ }
125
+ const plaintext = parts.join('');
126
+ if (vaultContentHash(plaintext) !== checkpoint.revision)
127
+ return { state: 'unusable', reason: 'revision' };
128
+ return { state: 'ready', plaintext, checkpoint, event };
129
+ }
130
+ /** Whether a rotation holds any authentic checkpoint-shaped event (optionally
131
+ * under one exact tag). Contents are never opened, so nothing written under a
132
+ * superseded key is decrypted, parsed or counted. */
133
+ async function rotationPresent(reader, author, now, tag) {
134
+ let events;
135
+ try {
136
+ events = await reader.checkpoints(author, tag);
137
+ }
138
+ catch (e) {
139
+ return unavailable(e);
140
+ }
141
+ for (const event of events.slice(0, 1024)) {
142
+ const found = await authenticCheckpointEvent(event, author, now);
143
+ if (found !== null && (tag === undefined || found === tag))
144
+ return 'present';
145
+ }
146
+ return 'absent';
147
+ }
148
+ /** Find the newest rotation: the first rotation whose successor holds no
149
+ * authentic checkpoint. Rotation is a revocation boundary: once rotation n + 1
150
+ * exists, rotation n is never read (only rotation n + 1's key can sign there),
151
+ * so a holder of a retired key can neither inject heads nor block recovery by
152
+ * writing to the old rotation, and cannot hide the successor by overwriting
153
+ * the pointer. Costs one extra resolve and query for the successor.
154
+ *
155
+ * A rotation revokes only a holder of THAT rotation's key, not the tree root:
156
+ * a holder of the root key can derive every rotation regardless, so rotation
157
+ * is a boundary against a retired per-rotation key, not against root
158
+ * compromise. This walk can find a rotation older than one a caller has
159
+ * already read (every relay withholding the successor); callers that track
160
+ * the highest rotation they have seen must reject a lower result as a
161
+ * rollback rather than trust it, since "not found" here never means "does
162
+ * not exist". */
163
+ async function newestRotation(resolve, now, tagFor) {
164
+ let rotation = 0;
165
+ let context;
166
+ try {
167
+ context = await resolve(0);
168
+ }
169
+ catch (e) {
170
+ return unavailable(e);
171
+ }
172
+ for (let hop = 0; hop < MAX_ROTATION_HOPS; hop++) {
173
+ let next;
174
+ try {
175
+ next = await resolve(rotation + 1);
176
+ }
177
+ catch (e) {
178
+ return unavailable(e);
179
+ }
180
+ const probe = await rotationPresent(next.reader, next.author, now, tagFor?.(next.author));
181
+ if (probe === 'absent')
182
+ return { rotation, context };
183
+ if (probe !== 'present')
184
+ return probe;
185
+ rotation++;
186
+ context = next;
187
+ }
188
+ return { state: 'unusable', reason: 'checkpoint' };
189
+ }
190
+ /** Read the newest rotation's legacy (publisher-less) checkpoint. Rotations are
191
+ * contiguous from zero; see `newestRotation` for the revocation boundary. A
192
+ * `nextRotation` pointer (always rotation + 1) to a rotation with no authentic
193
+ * checkpoint is damage, not a never-migrated account.
194
+ *
195
+ * Any key present in `floors`, whatever its value (including 0), counts as a
196
+ * rotation this caller has already reached: the walk refuses to land below
197
+ * the highest such key. Callers must not pre-fill a floor for a rotation they
198
+ * have not actually reached, or a legitimate read of that rotation would be
199
+ * refused as a rollback.
200
+ */
201
+ export async function readVaultRotations(resolve, purpose, floors = {}, now = nowSeconds()) {
202
+ checkNow(now);
203
+ const found = await newestRotation(resolve, now, author => vaultCheckpointTag(author));
204
+ if (!('context' in found))
205
+ return found;
206
+ const { rotation, context } = found;
207
+ if (rotation < highestFloorRotation(floors))
208
+ return { state: 'unusable', reason: 'rollback' };
209
+ const result = await readVaultSnapshot(context.reader, {
210
+ author: context.author, purpose, rotation, minSequence: floors[rotation], now,
211
+ });
212
+ if (result.state === 'absent')
213
+ return rotation > 0 ? { state: 'unusable', reason: 'checkpoint' } : result;
214
+ if (result.state === 'ready' && result.checkpoint.nextRotation !== undefined)
215
+ return { state: 'unusable', reason: 'checkpoint' };
216
+ return result;
217
+ }
218
+ /** Mergeable per-device heads prevent last-writer replacement across devices.
219
+ * Discovery queries only the private vault author. No device roster is public.
220
+ * Reads one rotation strictly: events without a checkpoint-shaped d-tag are
221
+ * ignored, then every remaining head must be valid, and more than 16 fails.
222
+ */
223
+ export async function readVaultHeads(reader, expected) {
224
+ const now = checkNow(expected.now ?? nowSeconds());
225
+ let events;
226
+ try {
227
+ events = await reader.checkpoints(expected.author);
228
+ }
229
+ catch (e) {
230
+ return unavailable(e);
231
+ }
232
+ const shaped = events.filter(e => !!e && e.pubkey === expected.author && e.kind === VAULT_EVENT_KIND
233
+ && vaultCheckpointShapedTag(e) !== null);
234
+ if (!shaped.length)
235
+ return { state: 'absent' };
236
+ if (shaped.length > 128)
237
+ return { state: 'unusable', reason: 'checkpoint' };
238
+ const heads = new Map();
239
+ for (const event of shaped) {
240
+ const tag = await authenticCheckpointEvent(event, expected.author, now);
241
+ if (tag === null)
242
+ continue;
243
+ const previous = heads.get(tag);
244
+ if (!previous || older(previous, event) < 0)
245
+ heads.set(tag, event);
246
+ }
247
+ if (!heads.size || heads.size > MAX_VAULT_HEADS)
248
+ return { state: 'unusable', reason: 'checkpoint' };
249
+ const snapshots = [];
250
+ for (const [tag, event] of heads) {
251
+ let raw;
252
+ try {
253
+ raw = await reader.open(event.content, expected.author);
254
+ }
255
+ catch {
256
+ return { state: 'unusable', reason: 'checkpoint' };
257
+ }
258
+ const checkpoint = raw === null ? null : parseVaultCheckpoint(raw, expected);
259
+ if (!checkpoint || tag !== vaultCheckpointTag(expected.author, checkpoint.publisher))
260
+ return { state: 'unusable', reason: 'checkpoint' };
261
+ const result = await readVaultSnapshot({ ...reader, checkpoints: async () => [event],
262
+ open: (content, author) => content === event.content ? Promise.resolve(raw) : reader.open(content, author),
263
+ }, { author: expected.author, purpose: expected.purpose, rotation: expected.rotation, now,
264
+ publisher: checkpoint.publisher, minSequence: expected.sequenceFloors?.[checkpoint.publisher ?? 'legacy'] });
265
+ if (result.state !== 'ready')
266
+ return result;
267
+ snapshots.push(result);
268
+ }
269
+ for (const [publisher, floor] of Object.entries(expected.sequenceFloors ?? {})) {
270
+ if (floor > 0 && !snapshots.some(s => (s.checkpoint.publisher ?? 'legacy') === publisher))
271
+ return { state: 'unusable', reason: 'rollback' };
272
+ }
273
+ snapshots.sort((a, b) => older(a.event, b.event));
274
+ return { state: 'ready', snapshots };
275
+ }
276
+ /** Read the newest rotation's merged per-device heads. Rotation is a
277
+ * revocation boundary (see `newestRotation`): heads of a superseded rotation
278
+ * are never merged, opened or counted, so the rotating writer must carry every
279
+ * head's state into the new rotation before publishing there. Floors from
280
+ * `resolve` apply to the rotation that is read.
281
+ *
282
+ * `minRotation` is the highest rotation this caller has already reached. If
283
+ * every relay withholds the true newest rotation's successor, `newestRotation`
284
+ * cannot distinguish that from a vault that never rotated further, and would
285
+ * otherwise report a lower rotation's heads as `ready`; a caller-tracked floor
286
+ * on the walk itself is what catches that withholding.
287
+ */
288
+ export async function readVaultHeadRotations(resolve, purpose, now = nowSeconds(), minRotation = 0) {
289
+ checkNow(now);
290
+ checkMinRotation(minRotation);
291
+ const found = await newestRotation(resolve, now);
292
+ if (!('context' in found))
293
+ return found;
294
+ const { rotation, context } = found;
295
+ if (rotation < minRotation)
296
+ return { state: 'unusable', reason: 'rollback' };
297
+ const result = await readVaultHeads(context.reader, { ...context, purpose, rotation, now });
298
+ if (result.state === 'absent')
299
+ return rotation > 0 ? { state: 'unusable', reason: 'checkpoint' } : result;
300
+ if (result.state === 'ready' && result.snapshots.some(s => s.checkpoint.nextRotation !== undefined)) {
301
+ return { state: 'unusable', reason: 'checkpoint' };
302
+ }
303
+ return result;
304
+ }
305
+ //# sourceMappingURL=vault-recovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vault-recovery.js","sourceRoot":"","sources":["../src/vault-recovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { RelayClient, type NostrFilter } from './relay.js';
2
+ import type { NostrEvent } from './types.js';
3
+ import type { VaultReader } from './vault-recovery.js';
4
+ /** Matching events one relay may contribute to one query. More fails that relay. */
5
+ export declare const MAX_VAULT_EVENTS_PER_RELAY = 128;
6
+ /** A relay query that did not complete on every relay. `failedRelays` names the
7
+ * relays that errored, timed out or exceeded the cap. */
8
+ export declare class VaultRelayError extends Error {
9
+ readonly failedRelays: string[];
10
+ constructor(message: string, failedRelays: string[]);
11
+ }
12
+ /** EOSE is required: a timed-out empty query is not evidence of absence.
13
+ * Events are filtered against `filter` (and `accept`, when given) and
14
+ * de-duplicated before the per-relay cap applies, so unrelated events cannot
15
+ * crowd out the requested ones. */
16
+ export declare function fetchVaultEvents(relay: Pick<RelayClient, 'subscribe' | 'closeSubscription'>, filter: NostrFilter, timeoutMs?: number, accept?: (event: NostrEvent) => boolean): Promise<NostrEvent[]>;
17
+ /** Relay-backed reader. At most eight valid relay URLs are queried.
18
+ *
19
+ * A checkpoint query, empty or not, completes only when EVERY queried relay
20
+ * answered with EOSE: one relay's "nothing" is not evidence of absence, and one
21
+ * relay's heads may omit a device head another relay holds. Otherwise it throws
22
+ * a `VaultRelayError` naming the failed relays, and recovery reports
23
+ * `unavailable` with `failedRelays`. A permanently dead configured relay keeps
24
+ * reads unavailable until it is removed from the set. A chunk fetch by exact
25
+ * ID succeeds as soon as any relay returns it (its hash is pinned); "not found"
26
+ * again needs every relay. Checkpoint queries drop events without a
27
+ * checkpoint-shaped d-tag before the per-relay cap. */
28
+ export declare function createVaultRelayReader(relays: readonly string[], open: VaultReader['open']): VaultReader;
29
+ //# sourceMappingURL=vault-relay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vault-relay.d.ts","sourceRoot":"","sources":["../src/vault-relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAGtD,oFAAoF;AACpF,eAAO,MAAM,0BAA0B,MAAM,CAAA;AA2B7C;yDACyD;AACzD,qBAAa,eAAgB,SAAQ,KAAK;IACX,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE;gBAAhD,OAAO,EAAE,MAAM,EAAW,YAAY,EAAE,MAAM,EAAE;CAI7D;AAED;;;mCAGmC;AACnC,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,mBAAmB,CAAC,EAC1F,MAAM,EAAE,WAAW,EAAE,SAAS,SAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAsBxG;AAED;;;;;;;;;;uDAUuD;AACvD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,WAAW,CA6BxG"}
@@ -0,0 +1,131 @@
1
+ import { RelayClient } from './relay.js';
2
+ import { VAULT_EVENT_KIND, vaultCheckpointShapedTag } from './vault-checkpoint.js';
3
+ /** Matching events one relay may contribute to one query. More fails that relay. */
4
+ export const MAX_VAULT_EVENTS_PER_RELAY = 128;
5
+ const MAX_VAULT_RELAYS = 8;
6
+ /** Must agree with the RelayClient constructor: a URL accepted here but rejected
7
+ * there would be skipped silently. Plain ws:// is loopback-only (localhost and
8
+ * 127.0.0.1; RelayClient does not accept [::1]). */
9
+ function validRelay(raw) {
10
+ try {
11
+ const url = new URL(raw);
12
+ return !url.username && !url.password && !url.hash && (url.protocol === 'wss:'
13
+ || (url.protocol === 'ws:' && ['localhost', '127.0.0.1'].includes(url.hostname)));
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ /** Client-side re-check of the relay filter. Relays are untrusted, so an event
20
+ * outside the requested author/kind/id/d-tag is dropped before it counts
21
+ * towards any limit. */
22
+ function matchesFilter(filter, event) {
23
+ if (!event || typeof event.id !== 'string' || typeof event.pubkey !== 'string' || !Array.isArray(event.tags))
24
+ return false;
25
+ if (filter.ids && !filter.ids.includes(event.id))
26
+ return false;
27
+ if (filter.authors && !filter.authors.includes(event.pubkey))
28
+ return false;
29
+ if (filter.kinds && !filter.kinds.includes(event.kind))
30
+ return false;
31
+ const dTags = filter['#d'];
32
+ if (dTags && !event.tags.some(t => Array.isArray(t) && t[0] === 'd' && dTags.includes(t[1])))
33
+ return false;
34
+ return true;
35
+ }
36
+ /** A relay query that did not complete on every relay. `failedRelays` names the
37
+ * relays that errored, timed out or exceeded the cap. */
38
+ export class VaultRelayError extends Error {
39
+ failedRelays;
40
+ constructor(message, failedRelays) {
41
+ super(message);
42
+ this.failedRelays = failedRelays;
43
+ this.name = 'VaultRelayError';
44
+ }
45
+ }
46
+ /** EOSE is required: a timed-out empty query is not evidence of absence.
47
+ * Events are filtered against `filter` (and `accept`, when given) and
48
+ * de-duplicated before the per-relay cap applies, so unrelated events cannot
49
+ * crowd out the requested ones. */
50
+ export function fetchVaultEvents(relay, filter, timeoutMs = 10000, accept) {
51
+ return new Promise((resolve, reject) => {
52
+ const events = new Map();
53
+ let subId;
54
+ let finished = false;
55
+ const finish = (error) => {
56
+ if (finished)
57
+ return;
58
+ finished = true;
59
+ clearTimeout(timer);
60
+ if (subId)
61
+ relay.closeSubscription(subId);
62
+ if (error)
63
+ reject(error);
64
+ else
65
+ resolve([...events.values()]);
66
+ };
67
+ const timer = setTimeout(() => finish(new Error('Vault query did not complete')), timeoutMs);
68
+ try {
69
+ subId = relay.subscribe([filter], event => {
70
+ if (finished || !matchesFilter(filter, event) || (accept && !accept(event)) || events.has(event.id))
71
+ return;
72
+ if (events.size >= MAX_VAULT_EVENTS_PER_RELAY)
73
+ finish(new Error('Vault query exceeds event limit'));
74
+ else
75
+ events.set(event.id, event);
76
+ }, () => finish());
77
+ if (finished)
78
+ relay.closeSubscription(subId);
79
+ }
80
+ catch {
81
+ finish(new Error('Vault query failed'));
82
+ }
83
+ });
84
+ }
85
+ /** Relay-backed reader. At most eight valid relay URLs are queried.
86
+ *
87
+ * A checkpoint query, empty or not, completes only when EVERY queried relay
88
+ * answered with EOSE: one relay's "nothing" is not evidence of absence, and one
89
+ * relay's heads may omit a device head another relay holds. Otherwise it throws
90
+ * a `VaultRelayError` naming the failed relays, and recovery reports
91
+ * `unavailable` with `failedRelays`. A permanently dead configured relay keeps
92
+ * reads unavailable until it is removed from the set. A chunk fetch by exact
93
+ * ID succeeds as soon as any relay returns it (its hash is pinned); "not found"
94
+ * again needs every relay. Checkpoint queries drop events without a
95
+ * checkpoint-shaped d-tag before the per-relay cap. */
96
+ export function createVaultRelayReader(relays, open) {
97
+ const urls = [...new Set(relays.filter(validRelay))].slice(0, MAX_VAULT_RELAYS);
98
+ const query = async (filter, accept, enough) => {
99
+ if (!urls.length)
100
+ throw new VaultRelayError('No valid vault relays', []);
101
+ const events = new Map();
102
+ const failed = [];
103
+ await Promise.all(urls.map(async (url) => {
104
+ let relay;
105
+ try {
106
+ relay = new RelayClient(url);
107
+ await relay.connect();
108
+ const found = await fetchVaultEvents(relay, filter, undefined, accept);
109
+ for (const e of found)
110
+ events.set(e.id, e);
111
+ }
112
+ catch {
113
+ failed.push(url);
114
+ }
115
+ finally {
116
+ relay?.disconnect();
117
+ }
118
+ }));
119
+ const result = [...events.values()];
120
+ if (failed.length && !(enough && enough(result))) {
121
+ throw new VaultRelayError(`Vault relays did not all answer: ${failed.length} of ${urls.length} failed`, failed.sort());
122
+ }
123
+ return result;
124
+ };
125
+ return {
126
+ checkpoints: (author, dTag) => query({ kinds: [VAULT_EVENT_KIND], authors: [author], ...(dTag ? { '#d': [dTag] } : {}), limit: MAX_VAULT_EVENTS_PER_RELAY }, event => vaultCheckpointShapedTag(event) !== null),
127
+ chunk: async (id) => (await query({ ids: [id], limit: 1 }, undefined, found => found.some(e => e.id === id))).find(e => e.id === id) ?? null,
128
+ open,
129
+ };
130
+ }
131
+ //# sourceMappingURL=vault-relay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vault-relay.js","sourceRoot":"","sources":["../src/vault-relay.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signet-protocol",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Decentralised identity verification protocol for Nostr",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,6 +9,10 @@
9
9
  ".": {
10
10
  "import": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts"
12
+ },
13
+ "./experimental": {
14
+ "import": "./dist/experimental.js",
15
+ "types": "./dist/experimental.d.ts"
12
16
  }
13
17
  },
14
18
  "files": [
@@ -24,7 +28,8 @@
24
28
  "test:watch": "vitest",
25
29
  "typecheck": "tsc --noEmit",
26
30
  "clean": "rm -rf dist",
27
- "prepare": "npm run build"
31
+ "prepare": "npm run build",
32
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
28
33
  },
29
34
  "keywords": [
30
35
  "nostr",
@@ -54,20 +59,20 @@
54
59
  "dependencies": {
55
60
  "@forgesworn/range-proof": "^2.0.0",
56
61
  "@forgesworn/ring-sig": "^3.0.0",
57
- "@forgesworn/shamir-words": "^1.0.0",
62
+ "@forgesworn/shamir-words": "^1.2.0-alpha.1",
58
63
  "@noble/curves": "^2.0.1",
59
64
  "@noble/hashes": "^2.0.1",
60
65
  "@scure/bip39": "^2.0.1",
61
66
  "jurisdiction-kit": "^1.0.0",
62
67
  "nostr-attestations": "^2.3.1",
63
- "nsec-tree": "^1.4.1",
68
+ "nsec-tree": "^1.6.0-alpha.1",
64
69
  "spoken-token": "^2.0.3"
65
70
  },
66
71
  "devDependencies": {
67
72
  "@types/node": "^25.3.3",
68
73
  "tsx": "^4.21.0",
69
74
  "typescript": "^6.0.2",
70
- "vitest": "^4.1.4"
75
+ "vitest": "^5.0.0"
71
76
  },
72
77
  "funding": {
73
78
  "type": "lightning",
package/src/bonds.ts CHANGED
@@ -1,5 +1,5 @@
1
- // Proof-of-Reserve Bond Attestation
2
- // Non-custodial bond system: verifiers prove Bitcoin address control via BIP-322
1
+ // Bond declaration with BIP-322 address-control proof.
2
+ // This does not prove funds, lock value, or make a bond slashable.
3
3
 
4
4
  import { BOND_DOMAIN_SEPARATOR, DEFAULT_BOND_MAX_AGE_SECS, VALID_BOND_ADDRESS_TYPES } from './constants.js';
5
5
  import { getTagValue } from './validation.js';
@@ -44,7 +44,7 @@ export interface VerifyBondProofOptions {
44
44
  verifier?: BIP322Verifier;
45
45
  /** Maximum age in seconds (default: DEFAULT_BOND_MAX_AGE_SECS) */
46
46
  maxAgeSecs?: number;
47
- /** Minimum required amount in satoshis */
47
+ /** Minimum declared amount in satoshis. This does not verify funds. */
48
48
  requiredSats?: number;
49
49
  /** Override current time (unix seconds, for testing) */
50
50
  now?: number;
@@ -84,7 +84,7 @@ export async function verifyBondProof(
84
84
  const fresh = ageSecs <= maxAgeSecs;
85
85
 
86
86
  // Check threshold
87
- const meetsThreshold = opts.requiredSats != null
87
+ const declaredAmountMeetsThreshold = opts.requiredSats != null
88
88
  ? proof.amountSats >= opts.requiredSats
89
89
  : null;
90
90
 
@@ -126,7 +126,9 @@ export async function verifyBondProof(
126
126
  signatureValid,
127
127
  fresh,
128
128
  ageSecs,
129
- meetsThreshold,
129
+ declaredAmountMeetsThreshold,
130
+ fundsVerified: false,
131
+ meetsThreshold: declaredAmountMeetsThreshold,
130
132
  errors,
131
133
  };
132
134
  }
@@ -185,10 +187,14 @@ export function parseBondProof(event: NostrEvent | { tags: string[][] }): BondPr
185
187
  };
186
188
  }
187
189
 
188
- /** Check whether a bond proof meets a required amount threshold */
190
+ /**
191
+ * Check policy compliance using independently verified funds evidence.
192
+ * Safe by default: BIP-322 plus a declared amount is never enough on its own.
193
+ */
189
194
  export function checkBondCompliance(
190
195
  proof: BondProof | null,
191
196
  requiredSats: number,
197
+ opts: { fundsVerified?: boolean } = {},
192
198
  ): { meets: boolean; reason?: string } {
193
199
  if (!proof) {
194
200
  return { meets: false, reason: 'No bond proof provided' };
@@ -199,5 +205,11 @@ export function checkBondCompliance(
199
205
  reason: `Bond amount ${proof.amountSats} sats is below required ${requiredSats} sats`,
200
206
  };
201
207
  }
208
+ if (opts.fundsVerified !== true) {
209
+ return {
210
+ meets: false,
211
+ reason: 'BIP-322 proves address control only; independent locked-funds verification is required',
212
+ };
213
+ }
202
214
  return { meets: true };
203
215
  }