signet-protocol 1.10.1 → 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.
- package/dist/bot-ownership.d.ts +61 -0
- package/dist/bot-ownership.d.ts.map +1 -0
- package/dist/bot-ownership.js +88 -0
- package/dist/bot-ownership.js.map +1 -0
- package/dist/crypto.d.ts +2 -0
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +4 -0
- package/dist/crypto.js.map +1 -1
- package/dist/experimental.d.ts +19 -0
- package/dist/experimental.d.ts.map +1 -0
- package/dist/experimental.js +16 -0
- package/dist/experimental.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/url-auth.d.ts.map +1 -1
- package/dist/url-auth.js +2 -1
- package/dist/url-auth.js.map +1 -1
- package/dist/utils.d.ts +46 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +127 -0
- package/dist/utils.js.map +1 -1
- package/dist/vault-checkpoint.d.ts +50 -0
- package/dist/vault-checkpoint.d.ts.map +1 -0
- package/dist/vault-checkpoint.js +84 -0
- package/dist/vault-checkpoint.js.map +1 -0
- package/dist/vault-keys.d.ts +19 -0
- package/dist/vault-keys.d.ts.map +1 -0
- package/dist/vault-keys.js +57 -0
- package/dist/vault-keys.js.map +1 -0
- package/dist/vault-recovery.d.ts +99 -0
- package/dist/vault-recovery.d.ts.map +1 -0
- package/dist/vault-recovery.js +305 -0
- package/dist/vault-recovery.js.map +1 -0
- package/dist/vault-relay.d.ts +29 -0
- package/dist/vault-relay.d.ts.map +1 -0
- package/dist/vault-relay.js +131 -0
- package/dist/vault-relay.js.map +1 -0
- package/package.json +8 -4
- package/src/bot-ownership.ts +77 -0
- package/src/crypto.ts +5 -0
- package/src/experimental.ts +25 -0
- package/src/index.ts +3 -0
- package/src/url-auth.ts +2 -1
- package/src/utils.ts +122 -0
- package/src/vault-checkpoint.ts +98 -0
- package/src/vault-keys.ts +59 -0
- package/src/vault-recovery.ts +284 -0
- package/src/vault-relay.ts +112 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Shared private-vault checkpoint contract. No relay I/O or application data schema. */
|
|
2
|
+
import { hmac } from '@noble/hashes/hmac.js';
|
|
3
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
4
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
|
|
5
|
+
import { parseVaultPurpose } from './vault-keys.js';
|
|
6
|
+
export const VAULT_EVENT_KIND = 30078;
|
|
7
|
+
export const MAX_VAULT_CHUNKS = 32;
|
|
8
|
+
export const MAX_VAULT_DEVICES = 16;
|
|
9
|
+
export const MAX_VAULT_CONTROL_BYTES = 12 * 1024;
|
|
10
|
+
export const MAX_VAULT_CHUNK_BYTES = 100_000;
|
|
11
|
+
const HEX = /^[0-9a-f]{64}$/;
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
const uint = (v) => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
|
|
14
|
+
const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
15
|
+
/** Publicly computable opaque tag; confidentiality comes from encryption and private keys. */
|
|
16
|
+
export function vaultCheckpointTag(author, publisher) {
|
|
17
|
+
if (!HEX.test(author))
|
|
18
|
+
throw new Error('Invalid vault author');
|
|
19
|
+
if (publisher !== undefined && !HEX.test(publisher))
|
|
20
|
+
throw new Error('Invalid vault publisher');
|
|
21
|
+
return bytesToHex(hmac(sha256, hexToBytes(author), encoder.encode(`signet-vault-route\u0000checkpoint\u0000${publisher ?? '0'}`))).slice(0, 32);
|
|
22
|
+
}
|
|
23
|
+
const CHECKPOINT_TAG = /^[0-9a-f]{32}$/;
|
|
24
|
+
/** The single d-tag of a checkpoint-shaped event, or null. Every checkpoint tag
|
|
25
|
+
* is `vaultCheckpointTag` output: 32 lowercase hex characters. Necessary, not
|
|
26
|
+
* sufficient: the publisher is encrypted, so the shape cannot prove the tag. */
|
|
27
|
+
export function vaultCheckpointShapedTag(event) {
|
|
28
|
+
if (!event || !Array.isArray(event.tags))
|
|
29
|
+
return null;
|
|
30
|
+
const dTags = event.tags.filter((t) => Array.isArray(t) && t[0] === 'd');
|
|
31
|
+
if (dTags.length !== 1 || dTags[0].length !== 2 || typeof dTags[0][1] !== 'string' || !CHECKPOINT_TAG.test(dTags[0][1]))
|
|
32
|
+
return null;
|
|
33
|
+
return dTags[0][1];
|
|
34
|
+
}
|
|
35
|
+
export function vaultContentHash(content) {
|
|
36
|
+
return bytesToHex(sha256(encoder.encode(content)));
|
|
37
|
+
}
|
|
38
|
+
/** Accept no partial checkpoint. A bad reference must never silently lose data. */
|
|
39
|
+
export function parseVaultCheckpoint(raw, expected) {
|
|
40
|
+
if (typeof raw !== 'string' || encoder.encode(raw).length > MAX_VAULT_CONTROL_BYTES)
|
|
41
|
+
return null;
|
|
42
|
+
let v;
|
|
43
|
+
try {
|
|
44
|
+
v = JSON.parse(raw);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (!object(v) || v.v !== 1 || typeof v.purpose !== 'string' || !parseVaultPurpose(v.purpose)
|
|
50
|
+
|| v.purpose !== expected.purpose || v.rotation !== expected.rotation || !uint(v.rotation) || v.rotation > 0xffff_ffff
|
|
51
|
+
|| !uint(v.sequence) || typeof v.revision !== 'string' || !HEX.test(v.revision))
|
|
52
|
+
return null;
|
|
53
|
+
if (!Array.isArray(v.devicePubkeys) || !v.devicePubkeys.length || v.devicePubkeys.length > MAX_VAULT_DEVICES
|
|
54
|
+
|| v.devicePubkeys.some(p => typeof p !== 'string' || !HEX.test(p))
|
|
55
|
+
|| new Set(v.devicePubkeys).size !== v.devicePubkeys.length)
|
|
56
|
+
return null;
|
|
57
|
+
if (v.publisher !== undefined && (typeof v.publisher !== 'string' || !v.devicePubkeys.includes(v.publisher)))
|
|
58
|
+
return null;
|
|
59
|
+
if (!Array.isArray(v.chunks) || !v.chunks.length || v.chunks.length > MAX_VAULT_CHUNKS)
|
|
60
|
+
return null;
|
|
61
|
+
const chunks = [];
|
|
62
|
+
for (const c of v.chunks) {
|
|
63
|
+
if (!object(c) || typeof c.eventId !== 'string' || !HEX.test(c.eventId)
|
|
64
|
+
|| typeof c.author !== 'string' || !v.devicePubkeys.includes(c.author)
|
|
65
|
+
|| typeof c.contentHash !== 'string' || !HEX.test(c.contentHash)
|
|
66
|
+
|| !uint(c.contentBytes) || c.contentBytes === 0 || c.contentBytes > MAX_VAULT_CHUNK_BYTES)
|
|
67
|
+
return null;
|
|
68
|
+
chunks.push({ eventId: c.eventId, author: c.author, contentHash: c.contentHash, contentBytes: c.contentBytes });
|
|
69
|
+
}
|
|
70
|
+
if (new Set(chunks.map(c => c.eventId)).size !== chunks.length)
|
|
71
|
+
return null;
|
|
72
|
+
if (v.nextRotation !== undefined && (v.nextRotation !== v.rotation + 1 || v.nextRotation > 0xffff_ffff))
|
|
73
|
+
return null;
|
|
74
|
+
return { v: 1, purpose: v.purpose, rotation: v.rotation, sequence: v.sequence,
|
|
75
|
+
revision: v.revision, ...(v.publisher === undefined ? {} : { publisher: v.publisher }), devicePubkeys: [...v.devicePubkeys], chunks,
|
|
76
|
+
...(v.nextRotation === undefined ? {} : { nextRotation: v.nextRotation }) };
|
|
77
|
+
}
|
|
78
|
+
/** Integrity check before decrypting. Signature and event-ID verification is also required. */
|
|
79
|
+
export function matchesVaultChunk(ref, event) {
|
|
80
|
+
return event.id === ref.eventId && event.pubkey === ref.author && event.kind === VAULT_EVENT_KIND
|
|
81
|
+
&& encoder.encode(event.content).length === ref.contentBytes
|
|
82
|
+
&& vaultContentHash(event.content) === ref.contentHash;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=vault-checkpoint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault-checkpoint.js","sourceRoot":"","sources":["../src/vault-checkpoint.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Identity, TreeRoot } from 'nsec-tree';
|
|
2
|
+
export type VaultDataset = 'profiles' | 'contacts:owner' | 'contacts:bots' | 'credentials' | 'settings' | {
|
|
3
|
+
dependant: number;
|
|
4
|
+
};
|
|
5
|
+
export declare function vaultPurpose(dataset: VaultDataset): string;
|
|
6
|
+
/** Strict parser: aliases such as dependant-01 would derive a different key. */
|
|
7
|
+
export declare function parseVaultPurpose(purpose: string): VaultDataset | null;
|
|
8
|
+
/** Suitable for Heartwood's per-request context; never a registry persona. */
|
|
9
|
+
export declare function vaultKeyContext(dataset: VaultDataset, rotation?: number): {
|
|
10
|
+
purpose: string;
|
|
11
|
+
index: number;
|
|
12
|
+
};
|
|
13
|
+
/** Caller owns and must zeroise the returned child. The supplied root is retained. */
|
|
14
|
+
export declare function deriveVaultIdentity(root: TreeRoot, dataset: VaultDataset, rotation?: number): Identity;
|
|
15
|
+
/** Keep root construction and use in one module instance (roots are opaque).
|
|
16
|
+
* Caller must zeroise the returned child; the temporary root is always destroyed.
|
|
17
|
+
*/
|
|
18
|
+
export declare function vaultIdentityFromMnemonic(mnemonic: string, dataset: VaultDataset, rotation?: number): Identity;
|
|
19
|
+
//# sourceMappingURL=vault-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault-keys.d.ts","sourceRoot":"","sources":["../src/vault-keys.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEnD,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,gBAAgB,GAAG,eAAe,GAAG,aAAa,GAAG,UAAU,GACnG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAA;AAWzB,wBAAgB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAO1D;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAQtE;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,EAAE,QAAQ,SAAI,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAGvG;AAED,sFAAsF;AACtF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,SAAI,GAAG,QAAQ,CAGjG;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,SAAI,GAAG,QAAQ,CAIzG"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Private dataset identities, derived directly from the tree root.
|
|
2
|
+
* Identity ordinals belong in the purpose; the derivation index is rotation.
|
|
3
|
+
* This does not change any existing persona derivation.
|
|
4
|
+
*/
|
|
5
|
+
import { derive, fromMnemonic } from 'nsec-tree';
|
|
6
|
+
const FIXED = new Set(['profiles', 'contacts:owner', 'contacts:bots', 'credentials', 'settings']);
|
|
7
|
+
const MAX_INDEX = 0xffff_ffff;
|
|
8
|
+
function ordinal(value) {
|
|
9
|
+
if (!Number.isInteger(value) || value < 0 || value > MAX_INDEX) {
|
|
10
|
+
throw new Error('Vault ordinal must be an unsigned 32-bit integer');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function vaultPurpose(dataset) {
|
|
14
|
+
if (typeof dataset === 'string' && FIXED.has(dataset))
|
|
15
|
+
return `signet:vault:${dataset}`;
|
|
16
|
+
if (dataset && typeof dataset === 'object' && 'dependant' in dataset) {
|
|
17
|
+
ordinal(dataset.dependant);
|
|
18
|
+
return `signet:vault:contacts:dependant-${dataset.dependant}`;
|
|
19
|
+
}
|
|
20
|
+
throw new Error('Unknown vault dataset');
|
|
21
|
+
}
|
|
22
|
+
/** Strict parser: aliases such as dependant-01 would derive a different key. */
|
|
23
|
+
export function parseVaultPurpose(purpose) {
|
|
24
|
+
if (typeof purpose !== 'string' || !purpose.startsWith('signet:vault:'))
|
|
25
|
+
return null;
|
|
26
|
+
const suffix = purpose.slice('signet:vault:'.length);
|
|
27
|
+
if (FIXED.has(suffix))
|
|
28
|
+
return suffix;
|
|
29
|
+
const match = /^contacts:dependant-(0|[1-9][0-9]*)$/.exec(suffix);
|
|
30
|
+
if (!match)
|
|
31
|
+
return null;
|
|
32
|
+
const dependant = Number(match[1]);
|
|
33
|
+
return Number.isInteger(dependant) && dependant <= MAX_INDEX ? { dependant } : null;
|
|
34
|
+
}
|
|
35
|
+
/** Suitable for Heartwood's per-request context; never a registry persona. */
|
|
36
|
+
export function vaultKeyContext(dataset, rotation = 0) {
|
|
37
|
+
ordinal(rotation);
|
|
38
|
+
return { purpose: vaultPurpose(dataset), index: rotation };
|
|
39
|
+
}
|
|
40
|
+
/** Caller owns and must zeroise the returned child. The supplied root is retained. */
|
|
41
|
+
export function deriveVaultIdentity(root, dataset, rotation = 0) {
|
|
42
|
+
const { purpose, index } = vaultKeyContext(dataset, rotation);
|
|
43
|
+
return derive(root, purpose, index);
|
|
44
|
+
}
|
|
45
|
+
/** Keep root construction and use in one module instance (roots are opaque).
|
|
46
|
+
* Caller must zeroise the returned child; the temporary root is always destroyed.
|
|
47
|
+
*/
|
|
48
|
+
export function vaultIdentityFromMnemonic(mnemonic, dataset, rotation = 0) {
|
|
49
|
+
const root = fromMnemonic(mnemonic);
|
|
50
|
+
try {
|
|
51
|
+
return deriveVaultIdentity(root, dataset, rotation);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
root.destroy();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=vault-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault-keys.js","sourceRoot":"","sources":["../src/vault-keys.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAMhD,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,gBAAgB,EAAE,eAAe,EAAE,aAAa,EAAE,UAAU,CAAC,CAAC,CAAA;AACjG,MAAM,SAAS,GAAG,WAAW,CAAA;AAE7B,SAAS,OAAO,CAAC,KAAa;IAC5B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;QAC/D,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;IACrE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAqB;IAChD,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,gBAAgB,OAAO,EAAE,CAAA;IACvF,IAAI,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,WAAW,IAAI,OAAO,EAAE,CAAC;QACrE,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;QAC1B,OAAO,mCAAmC,OAAO,CAAC,SAAS,EAAE,CAAA;IAC/D,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAA;AAC1C,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC;QAAE,OAAO,IAAI,CAAA;IACpF,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,MAAM,CAAC,CAAA;IACpD,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,MAAsB,CAAA;IACpD,MAAM,KAAK,GAAG,sCAAsC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACjE,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACvB,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAClC,OAAO,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,SAAS,IAAI,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AACrF,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,eAAe,CAAC,OAAqB,EAAE,QAAQ,GAAG,CAAC;IACjE,OAAO,CAAC,QAAQ,CAAC,CAAA;IACjB,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAA;AAC5D,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,mBAAmB,CAAC,IAAc,EAAE,OAAqB,EAAE,QAAQ,GAAG,CAAC;IACrF,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IAC7D,OAAO,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,CAAA;AACrC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,yBAAyB,CAAC,QAAgB,EAAE,OAAqB,EAAE,QAAQ,GAAG,CAAC;IAC7F,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAA;IACnC,IAAI,CAAC;QAAC,OAAO,mBAAmB,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAA;IAAC,CAAC;YACnD,CAAC;QAAC,IAAI,CAAC,OAAO,EAAE,CAAA;IAAC,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { NostrEvent } from './types.js';
|
|
2
|
+
import type { VaultCheckpoint } 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 declare const VAULT_CLOCK_TOLERANCE_SECONDS = 300;
|
|
6
|
+
type Ready = Extract<VaultReadResult, {
|
|
7
|
+
state: 'ready';
|
|
8
|
+
}>;
|
|
9
|
+
export type VaultReadResult = {
|
|
10
|
+
state: 'absent';
|
|
11
|
+
}
|
|
12
|
+
/** `failedRelays`, when known, names the relays that did not answer. */
|
|
13
|
+
| {
|
|
14
|
+
state: 'unavailable';
|
|
15
|
+
failedRelays?: string[];
|
|
16
|
+
} | {
|
|
17
|
+
state: 'unusable';
|
|
18
|
+
reason: 'checkpoint' | 'rollback' | 'chunk' | 'revision';
|
|
19
|
+
} | {
|
|
20
|
+
state: 'ready';
|
|
21
|
+
plaintext: string;
|
|
22
|
+
checkpoint: VaultCheckpoint;
|
|
23
|
+
event: NostrEvent;
|
|
24
|
+
};
|
|
25
|
+
export interface VaultReader {
|
|
26
|
+
/** Must distinguish an empty reachable query from relay failure (throw).
|
|
27
|
+
* An error carrying `failedRelays: string[]` is reported in the result. */
|
|
28
|
+
checkpoints(author: string, dTag?: string): Promise<NostrEvent[]>;
|
|
29
|
+
/** Exact ID query. Missing data is a failed restore, never an empty dataset. */
|
|
30
|
+
chunk(eventId: string): Promise<NostrEvent | null>;
|
|
31
|
+
/** Open the v2 envelope only; sender is the vault key, not the chunk signer. */
|
|
32
|
+
open(content: string, vaultAuthor: string): Promise<string | null>;
|
|
33
|
+
}
|
|
34
|
+
/** Read one rotation. Callers may fall back to legacy ONLY on `absent`.
|
|
35
|
+
* Invalid, incomplete, offline and rolled-back backups are distinguishable from
|
|
36
|
+
* never-migrated accounts. No state is persisted or marked canonical here.
|
|
37
|
+
* `now` (unix seconds, default the local clock) bounds created_at from above.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readVaultSnapshot(reader: VaultReader, expected: {
|
|
40
|
+
author: string;
|
|
41
|
+
purpose: string;
|
|
42
|
+
rotation: number;
|
|
43
|
+
minSequence?: number;
|
|
44
|
+
publisher?: string;
|
|
45
|
+
now?: number;
|
|
46
|
+
}): Promise<VaultReadResult>;
|
|
47
|
+
/** Read the newest rotation's legacy (publisher-less) checkpoint. Rotations are
|
|
48
|
+
* contiguous from zero; see `newestRotation` for the revocation boundary. A
|
|
49
|
+
* `nextRotation` pointer (always rotation + 1) to a rotation with no authentic
|
|
50
|
+
* checkpoint is damage, not a never-migrated account.
|
|
51
|
+
*
|
|
52
|
+
* Any key present in `floors`, whatever its value (including 0), counts as a
|
|
53
|
+
* rotation this caller has already reached: the walk refuses to land below
|
|
54
|
+
* the highest such key. Callers must not pre-fill a floor for a rotation they
|
|
55
|
+
* have not actually reached, or a legitimate read of that rotation would be
|
|
56
|
+
* refused as a rollback.
|
|
57
|
+
*/
|
|
58
|
+
export declare function readVaultRotations(resolve: (rotation: number) => Promise<{
|
|
59
|
+
reader: VaultReader;
|
|
60
|
+
author: string;
|
|
61
|
+
}>, purpose: string, floors?: Readonly<Record<number, number>>, now?: number): Promise<VaultReadResult>;
|
|
62
|
+
export type VaultHeadsResult = Exclude<VaultReadResult, {
|
|
63
|
+
state: 'ready';
|
|
64
|
+
}> | {
|
|
65
|
+
state: 'ready';
|
|
66
|
+
snapshots: Ready[];
|
|
67
|
+
};
|
|
68
|
+
interface HeadsExpected {
|
|
69
|
+
author: string;
|
|
70
|
+
purpose: string;
|
|
71
|
+
rotation: number;
|
|
72
|
+
sequenceFloors?: Readonly<Record<string, number>>;
|
|
73
|
+
now?: number;
|
|
74
|
+
}
|
|
75
|
+
/** Mergeable per-device heads prevent last-writer replacement across devices.
|
|
76
|
+
* Discovery queries only the private vault author. No device roster is public.
|
|
77
|
+
* Reads one rotation strictly: events without a checkpoint-shaped d-tag are
|
|
78
|
+
* ignored, then every remaining head must be valid, and more than 16 fails.
|
|
79
|
+
*/
|
|
80
|
+
export declare function readVaultHeads(reader: VaultReader, expected: HeadsExpected): Promise<VaultHeadsResult>;
|
|
81
|
+
/** Read the newest rotation's merged per-device heads. Rotation is a
|
|
82
|
+
* revocation boundary (see `newestRotation`): heads of a superseded rotation
|
|
83
|
+
* are never merged, opened or counted, so the rotating writer must carry every
|
|
84
|
+
* head's state into the new rotation before publishing there. Floors from
|
|
85
|
+
* `resolve` apply to the rotation that is read.
|
|
86
|
+
*
|
|
87
|
+
* `minRotation` is the highest rotation this caller has already reached. If
|
|
88
|
+
* every relay withholds the true newest rotation's successor, `newestRotation`
|
|
89
|
+
* cannot distinguish that from a vault that never rotated further, and would
|
|
90
|
+
* otherwise report a lower rotation's heads as `ready`; a caller-tracked floor
|
|
91
|
+
* on the walk itself is what catches that withholding.
|
|
92
|
+
*/
|
|
93
|
+
export declare function readVaultHeadRotations(resolve: (rotation: number) => Promise<{
|
|
94
|
+
reader: VaultReader;
|
|
95
|
+
author: string;
|
|
96
|
+
sequenceFloors?: Readonly<Record<string, number>>;
|
|
97
|
+
}>, purpose: string, now?: number, minRotation?: number): Promise<VaultHeadsResult>;
|
|
98
|
+
export {};
|
|
99
|
+
//# sourceMappingURL=vault-recovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault-recovery.d.ts","sourceRoot":"","sources":["../src/vault-recovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|