nostr-bray 1.27.3 → 1.27.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # nostr-bray
2
2
 
3
- **Trust-aware Nostr MCP for AI and humans.** 234 tools. Model-agnostic. Works with Claude, ChatGPT, Gemini, Cursor, Windsurf, or any MCP client.
3
+ **Trust-aware Nostr MCP for AI and humans.** 236 tools. Model-agnostic. Works with Claude, ChatGPT, Gemini, Cursor, Windsurf, or any MCP client.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/nostr-bray)](https://www.npmjs.com/package/nostr-bray)
6
6
  [![CI](https://github.com/forgesworn/bray/actions/workflows/ci.yml/badge.svg)](https://github.com/forgesworn/bray/actions/workflows/ci.yml)
@@ -225,6 +225,23 @@ npx nostr-bray --help # see all commands
225
225
 
226
226
  See [llms.txt](./llms.txt) for a concise summary optimised for AI context windows, or [llms-full.txt](./llms-full.txt) for complete tool documentation with parameter details.
227
227
 
228
+ ## Part of the ForgeSworn Toolkit
229
+
230
+ [ForgeSworn](https://forgesworn.dev) builds open-source cryptographic identity, payments, and coordination tools for Nostr.
231
+
232
+ | Library | What it does |
233
+ |---------|-------------|
234
+ | [nsec-tree](https://github.com/forgesworn/nsec-tree) | Deterministic sub-identity derivation |
235
+ | [ring-sig](https://github.com/forgesworn/ring-sig) | SAG/LSAG ring signatures on secp256k1 |
236
+ | [range-proof](https://github.com/forgesworn/range-proof) | Pedersen commitment range proofs |
237
+ | [canary-kit](https://github.com/forgesworn/canary-kit) | Coercion-resistant spoken verification |
238
+ | [spoken-token](https://github.com/forgesworn/spoken-token) | Human-speakable verification tokens |
239
+ | [toll-booth](https://github.com/forgesworn/toll-booth) | L402 payment middleware |
240
+ | [geohash-kit](https://github.com/forgesworn/geohash-kit) | Geohash toolkit with polygon coverage |
241
+ | [nostr-attestations](https://github.com/forgesworn/nostr-attestations) | NIP-VA verifiable attestations |
242
+ | [dominion](https://github.com/forgesworn/dominion) | Epoch-based encrypted access control |
243
+ | [nostr-veil](https://github.com/forgesworn/nostr-veil) | Privacy-preserving Web of Trust |
244
+
228
245
  ## Licence
229
246
 
230
247
  MIT
@@ -5,9 +5,12 @@
5
5
  * remote bunker via encrypted Nostr relay messages.
6
6
  *
7
7
  * Usage:
8
- * BUNKER_URI=bunker://pk?relay=wss://...
9
- * or
10
- * BUNKER_URI=bunker://pk?relay=wss://...&secret=clienthex
8
+ * BUNKER_URI=bunker://<pubkey>?relay=wss://...&relay=wss://...&secret=<pairing-nonce>
9
+ *
10
+ * A bunker URI can list multiple relays; all of them are used. The
11
+ * `secret=` parameter is a NIP-46 pairing nonce (proof-of-URI-possession)
12
+ * that the client echoes to the signer on the initial `connect` request,
13
+ * and is NOT a private key.
11
14
  */
12
15
  import { BunkerSigner } from 'nostr-tools/nip46';
13
16
  import { SimplePool } from 'nostr-tools/pool';
@@ -15,12 +18,46 @@ import type { PublicIdentity, SignFn } from './types.js';
15
18
  import type { SigningContext } from './signing-context.js';
16
19
  export interface BunkerConfig {
17
20
  pubkey: string;
21
+ /**
22
+ * All relays declared in the URI. A bunker URI can (and usually does)
23
+ * list several relays for redundancy; every one must be tried since the
24
+ * remote signer is listening on all of them. Earlier versions of this
25
+ * parser only read the first `?relay=` parameter and discarded the
26
+ * rest, which could produce a silent hang if the first relay was down.
27
+ */
18
28
  relays: string[];
19
- secret?: string;
29
+ /**
30
+ * NIP-46 pairing nonce from the URI's `?secret=` parameter. This is
31
+ * NOT a private key. It is a proof-of-URI-possession token that the
32
+ * client must echo back to the signer in its first `connect` request
33
+ * so the signer knows the client received the URI out-of-band. Any
34
+ * client with the URI can present this value; after the first
35
+ * `connect` the signer TOFU-approves the client for crypto methods
36
+ * and the nonce is not needed again for that specific client.
37
+ *
38
+ * Earlier versions of this interface named this field `secret` without
39
+ * the distinction and the code then used it as a client private key,
40
+ * which was both a hang (wrong client identity, remote signer rejected
41
+ * the unknown client) and a security bug (any URI-holder could
42
+ * impersonate the client on the wire). Do not do that.
43
+ */
44
+ connectSecret?: string;
20
45
  }
21
46
  /** Parse a bunker:// URI */
22
47
  export declare function parseBunkerUri(uri: string): BunkerConfig;
23
- /** Resolve the client secret key: URI secret > cached > generate & persist. */
48
+ /**
49
+ * Resolve the client secret key for a given bunker.
50
+ *
51
+ * Caches a freshly-generated client key per bunker pubkey so that repeat
52
+ * invocations against the same bunker reuse the same client identity,
53
+ * letting the remote signer's TOFU approval persist across reconnects
54
+ * without needing a fresh button press each time.
55
+ *
56
+ * Critically, the URI's `connectSecret` is NEVER used as a client
57
+ * private key here. It's a public pairing nonce (see BunkerConfig docs)
58
+ * and using it as a key would leak the client identity to anyone who
59
+ * has the URI.
60
+ */
24
61
  export declare function resolveClientKey(config: BunkerConfig, stateDir?: string): Uint8Array;
25
62
  export declare class BunkerContext implements SigningContext {
26
63
  protected signer: BunkerSigner;
@@ -29,7 +66,13 @@ export declare class BunkerContext implements SigningContext {
29
66
  private clientSk;
30
67
  protected constructor(signer: BunkerSigner, pool: SimplePool, clientSk: Uint8Array);
31
68
  /** Connect to a remote bunker. Blocks until the connection is established. */
32
- static connect(uri: string, timeoutMs?: number): Promise<BunkerContext>;
69
+ static connect(uri: string, _timeoutMs?: number, stateDir?: string): Promise<BunkerContext>;
70
+ /**
71
+ * Resolve the signing identity pubkey from the remote bunker.
72
+ * Called lazily on first access so startup stays fast (no extra
73
+ * NIP-46 round-trip). The result is cached for subsequent calls.
74
+ */
75
+ resolvePublicKey(): Promise<string>;
33
76
  /** The remote identity's npub */
34
77
  get activeNpub(): string;
35
78
  /** The remote identity's hex pubkey */
@@ -5,9 +5,12 @@
5
5
  * remote bunker via encrypted Nostr relay messages.
6
6
  *
7
7
  * Usage:
8
- * BUNKER_URI=bunker://pk?relay=wss://...
9
- * or
10
- * BUNKER_URI=bunker://pk?relay=wss://...&secret=clienthex
8
+ * BUNKER_URI=bunker://<pubkey>?relay=wss://...&relay=wss://...&secret=<pairing-nonce>
9
+ *
10
+ * A bunker URI can list multiple relays; all of them are used. The
11
+ * `secret=` parameter is a NIP-46 pairing nonce (proof-of-URI-possession)
12
+ * that the client echoes to the signer on the initial `connect` request,
13
+ * and is NOT a private key.
11
14
  */
12
15
  import { BunkerSigner } from 'nostr-tools/nip46';
13
16
  import { useWebSocketImplementation, SimplePool } from 'nostr-tools/pool';
@@ -18,22 +21,34 @@ import { readStateFile, writeStateFile } from './state.js';
18
21
  useWebSocketImplementation(WebSocket);
19
22
  /** Parse a bunker:// URI */
20
23
  export function parseBunkerUri(uri) {
21
- // bunker://<pubkey>?relay=<url>&relay=<url>&secret=<hex>
24
+ // bunker://<pubkey>?relay=<url>&relay=<url>...&secret=<hex>
22
25
  const url = new URL(uri);
23
26
  const pubkey = url.hostname || url.pathname.replace('//', '');
24
27
  const relays = url.searchParams.getAll('relay');
25
- const secret = url.searchParams.get('secret') ?? undefined;
26
- if (!pubkey || relays.length === 0) {
27
- throw new Error('Invalid bunker URI: missing pubkey or relay');
28
+ const connectSecret = url.searchParams.get('secret') ?? undefined;
29
+ if (!pubkey) {
30
+ throw new Error('Invalid bunker URI: missing pubkey');
31
+ }
32
+ if (relays.length === 0) {
33
+ throw new Error('Invalid bunker URI: missing relay parameter(s)');
28
34
  }
29
- return { pubkey, relays, secret };
35
+ return { pubkey, relays, connectSecret };
30
36
  }
31
37
  const CLIENT_KEYS_FILE = 'client-keys.json';
32
- /** Resolve the client secret key: URI secret > cached > generate & persist. */
38
+ /**
39
+ * Resolve the client secret key for a given bunker.
40
+ *
41
+ * Caches a freshly-generated client key per bunker pubkey so that repeat
42
+ * invocations against the same bunker reuse the same client identity,
43
+ * letting the remote signer's TOFU approval persist across reconnects
44
+ * without needing a fresh button press each time.
45
+ *
46
+ * Critically, the URI's `connectSecret` is NEVER used as a client
47
+ * private key here. It's a public pairing nonce (see BunkerConfig docs)
48
+ * and using it as a key would leak the client identity to anyone who
49
+ * has the URI.
50
+ */
33
51
  export function resolveClientKey(config, stateDir) {
34
- if (config.secret) {
35
- return Buffer.from(config.secret, 'hex');
36
- }
37
52
  const cache = readStateFile(CLIENT_KEYS_FILE, stateDir);
38
53
  if (cache[config.pubkey]) {
39
54
  return Buffer.from(cache[config.pubkey], 'hex');
@@ -54,24 +69,52 @@ export class BunkerContext {
54
69
  this.clientSk = clientSk;
55
70
  }
56
71
  /** Connect to a remote bunker. Blocks until the connection is established. */
57
- static async connect(uri, timeoutMs = 15_000) {
72
+ static async connect(uri, _timeoutMs = 15_000, stateDir) {
58
73
  const config = parseBunkerUri(uri);
59
- const clientSk = resolveClientKey(config);
74
+ const clientSk = resolveClientKey(config, stateDir);
60
75
  const pool = new SimplePool();
61
- const signer = BunkerSigner.fromBunker(clientSk, { pubkey: config.pubkey, relays: config.relays, secret: null }, { pool });
62
- // Connect and verify
76
+ const signer = BunkerSigner.fromBunker(clientSk, {
77
+ pubkey: config.pubkey,
78
+ relays: config.relays,
79
+ // The URI pairing nonce -- gets echoed in the `connect` call so
80
+ // the remote signer can TOFU-approve this client. If omitted the
81
+ // signer will still accept the connect but every subsequent
82
+ // request will require a manual approval on the signer side.
83
+ secret: config.connectSecret ?? null,
84
+ }, { pool });
85
+ // Only the `connect` handshake is needed at startup. Ping is
86
+ // redundant (if connect succeeded the signer is alive), and
87
+ // getPublicKey is deferred to first access so startup stays fast.
63
88
  await signer.connect();
64
- await signer.ping();
65
89
  const ctx = new BunkerContext(signer, pool, clientSk);
66
- ctx.pubkeyHex = await signer.getPublicKey();
90
+ // Do NOT use config.pubkey here — that is the bunker's transport
91
+ // key from the URI, not the signing identity. The actual identity
92
+ // pubkey is resolved lazily via getPublicKey() on first access.
67
93
  return ctx;
68
94
  }
95
+ /**
96
+ * Resolve the signing identity pubkey from the remote bunker.
97
+ * Called lazily on first access so startup stays fast (no extra
98
+ * NIP-46 round-trip). The result is cached for subsequent calls.
99
+ */
100
+ async resolvePublicKey() {
101
+ if (!this.pubkeyHex) {
102
+ this.pubkeyHex = await this.signer.getPublicKey();
103
+ }
104
+ return this.pubkeyHex;
105
+ }
69
106
  /** The remote identity's npub */
70
107
  get activeNpub() {
108
+ if (!this.pubkeyHex) {
109
+ throw new Error('pubkey not yet resolved — call resolvePublicKey() first');
110
+ }
71
111
  return npubEncode(this.pubkeyHex);
72
112
  }
73
113
  /** The remote identity's hex pubkey */
74
114
  get activePublicKeyHex() {
115
+ if (!this.pubkeyHex) {
116
+ throw new Error('pubkey not yet resolved — call resolvePublicKey() first');
117
+ }
75
118
  return this.pubkeyHex;
76
119
  }
77
120
  /** Sign an event via the remote bunker */
package/dist/cli.js CHANGED
@@ -24,10 +24,26 @@ import { getCommandHelp } from './help.js';
24
24
  import * as fmt from './format.js';
25
25
  const args = process.argv.slice(2);
26
26
  const command = args[0];
27
- // No command = start MCP server
27
+ // No command = start MCP server.
28
+ //
29
+ // index.js's top-level await sets up the StdioServerTransport (or HTTP
30
+ // transport) and registers tools. Once that chain resolves, the transport
31
+ // keeps the event loop alive on its own, BUT we must prevent cli.ts from
32
+ // falling through to the per-command handlers below (which re-loadConfig
33
+ // and build a second ctx, racing with index.js's state and throwing when
34
+ // run in MCP mode because there is no command to dispatch to).
35
+ //
36
+ // The old version of this block called process.exit(0) after the import,
37
+ // which terminated the MCP server immediately -- before Claude Code's MCP
38
+ // client could send its `initialize` request. The other long-running
39
+ // commands in this file use `await new Promise(() => {})` to hold cli.ts
40
+ // open until SIGINT/SIGTERM; matching that pattern here keeps the MCP
41
+ // server process alive forever without executing any more cli.ts code.
42
+ // SIGINT/SIGTERM are caught by the shutdown handler at the bottom of
43
+ // index.ts which calls ctx.destroy() + pool.close() + process.exit(0).
28
44
  if (!command) {
29
45
  await import('./index.js');
30
- process.exit(0);
46
+ await new Promise(() => { });
31
47
  }
32
48
  // Bunker = NIP-46 remote signer
33
49
  if (command === 'bunker' && !args.includes('--help')) {
package/dist/index.js CHANGED
@@ -29,22 +29,39 @@ const pool = new RelayPool({
29
29
  defaultRelays: config.relays,
30
30
  });
31
31
  const nip65 = new Nip65Manager(pool, config.relays);
32
- // Connect to bunker (with Heartwood auto-detection) or use local key
32
+ // Connect to bunker or use local key.
33
+ //
34
+ // In bunker mode we defer the Heartwood extension probe to a background
35
+ // task rather than blocking startup on it. The probe sends a
36
+ // heartwood_list_identities round-trip to the remote signer, which costs
37
+ // an extra 1-2 seconds on top of the NIP-46 connect and pubkey lookup.
38
+ // Sync startup takes 3-6s total over five relays, and Claude Code's MCP
39
+ // stdio health check will mark the server as "Failed to connect" if the
40
+ // StdioServerTransport isn't serving within its window -- so every second
41
+ // we can shave off startup matters. Heartwood-specific tools
42
+ // (heartwood_derive etc.) still work because nostr-tools' BunkerSigner
43
+ // sends arbitrary NIP-46 method names via sendRequest; the probe only
44
+ // controlled whether to expose those methods as typed instance methods
45
+ // on HeartwoodContext, which is a cosmetic distinction for MCP use.
33
46
  let ctx;
34
47
  if (config.bunkerUri) {
35
48
  const { BunkerContext } = await import('./bunker-context.js');
36
49
  const base = await BunkerContext.connect(config.bunkerUri);
37
- // Probe for Heartwood extensions
38
- const { HeartwoodContext } = await import('./heartwood-context.js');
39
- const hw = await HeartwoodContext.probe(base);
40
- if (hw) {
41
- ctx = hw;
42
- console.error(`Connected to Heartwood — signing as ${hw.activeNpub}`);
43
- }
44
- else {
45
- ctx = base;
46
- console.error(`Connected to bunker — signing as ${base.activeNpub}`);
47
- }
50
+ await base.resolvePublicKey();
51
+ ctx = base;
52
+ console.error(`Connected to bunker — signing as ${base.activeNpub}`);
53
+ (async () => {
54
+ try {
55
+ const { HeartwoodContext } = await import('./heartwood-context.js');
56
+ const hw = await HeartwoodContext.probe(base);
57
+ if (hw) {
58
+ console.error(`Heartwood extensions detected — ${base.activeNpub}`);
59
+ }
60
+ }
61
+ catch (e) {
62
+ console.error('Heartwood probe failed (non-fatal):', e.message);
63
+ }
64
+ })();
48
65
  }
49
66
  else {
50
67
  ctx = new IdentityContext(config.secretKey, config.secretFormat);
@@ -6,6 +6,25 @@ export interface PoolLike {
6
6
  querySync(relays: string[], filter: Filter): Promise<NostrEvent[]>;
7
7
  destroy(): void;
8
8
  }
9
+ /**
10
+ * Summarise a publish attempt into the two boolean flags on {@link PublishResult}.
11
+ *
12
+ * `success` is true when the event reached the network as a majority: at least
13
+ * one relay accepted AND at least 50% of attempted relays accepted. This is the
14
+ * default "did my publish work" signal. Paywalled, whitelisted, or transient
15
+ * failures routinely leave some relays in the rejected list, so requiring every
16
+ * relay to accept produces far too many false negatives.
17
+ *
18
+ * `allAccepted` preserves the strict "every attempted relay accepted" semantic
19
+ * for callers that genuinely need it (e.g. high-assurance publishing to a
20
+ * small curated list of private relays).
21
+ *
22
+ * Zero attempted relays is treated as failure on both flags.
23
+ */
24
+ export declare function summarisePublish(acceptedCount: number, attempted: number): {
25
+ success: boolean;
26
+ allAccepted: boolean;
27
+ };
9
28
  export interface RelayPoolConfig {
10
29
  torProxy?: string;
11
30
  allowClearnet: boolean;
@@ -1,3 +1,26 @@
1
+ /**
2
+ * Summarise a publish attempt into the two boolean flags on {@link PublishResult}.
3
+ *
4
+ * `success` is true when the event reached the network as a majority: at least
5
+ * one relay accepted AND at least 50% of attempted relays accepted. This is the
6
+ * default "did my publish work" signal. Paywalled, whitelisted, or transient
7
+ * failures routinely leave some relays in the rejected list, so requiring every
8
+ * relay to accept produces far too many false negatives.
9
+ *
10
+ * `allAccepted` preserves the strict "every attempted relay accepted" semantic
11
+ * for callers that genuinely need it (e.g. high-assurance publishing to a
12
+ * small curated list of private relays).
13
+ *
14
+ * Zero attempted relays is treated as failure on both flags.
15
+ */
16
+ export function summarisePublish(acceptedCount, attempted) {
17
+ if (attempted <= 0)
18
+ return { success: false, allAccepted: false };
19
+ return {
20
+ success: acceptedCount >= 1 && acceptedCount * 2 >= attempted,
21
+ allAccepted: acceptedCount === attempted,
22
+ };
23
+ }
1
24
  /** Initialise WebSocket and create a real SimplePool — lazy-loaded to avoid side effects at import */
2
25
  async function createRealPool(torProxy) {
3
26
  const { useWebSocketImplementation, SimplePool } = await import('nostr-tools/pool');
@@ -72,7 +95,7 @@ export class RelayPool {
72
95
  const relays = this.getRelays(npub);
73
96
  const writeRelays = relays.write;
74
97
  if (writeRelays.length === 0) {
75
- return { success: false, accepted: [], rejected: [], errors: ['no write relays configured'] };
98
+ return { success: false, allAccepted: false, accepted: [], rejected: [], errors: ['no write relays configured'] };
76
99
  }
77
100
  const promises = pool.publish(writeRelays, event);
78
101
  const accepted = [];
@@ -91,7 +114,7 @@ export class RelayPool {
91
114
  }
92
115
  }
93
116
  return {
94
- success: accepted.length === writeRelays.length,
117
+ ...summarisePublish(accepted.length, writeRelays.length),
95
118
  accepted,
96
119
  rejected,
97
120
  errors,
@@ -101,7 +124,7 @@ export class RelayPool {
101
124
  async publishDirect(relays, event) {
102
125
  const pool = await this.poolReady;
103
126
  if (relays.length === 0) {
104
- return { success: false, accepted: [], rejected: [], errors: ['no relays specified'] };
127
+ return { success: false, allAccepted: false, accepted: [], rejected: [], errors: ['no relays specified'] };
105
128
  }
106
129
  const promises = pool.publish(relays, event);
107
130
  const accepted = [];
@@ -120,7 +143,7 @@ export class RelayPool {
120
143
  }
121
144
  }
122
145
  return {
123
- success: accepted.length === relays.length,
146
+ ...summarisePublish(accepted.length, relays.length),
124
147
  accepted,
125
148
  rejected,
126
149
  errors,
@@ -123,7 +123,12 @@ export async function handlePublishScheduled(pool, npub, queueDir) {
123
123
  published++;
124
124
  }
125
125
  else {
126
- console.error(`Failed to publish ${data.event.id}: rejected by all relays`);
126
+ // success is the majority-quorum flag: treat as failure only when
127
+ // we did not reach at least 50% of attempted relays.
128
+ const reason = result.accepted.length === 0
129
+ ? 'rejected by all relays'
130
+ : `only ${result.accepted.length}/${result.accepted.length + result.rejected.length} relays accepted`;
131
+ console.error(`Failed to publish ${data.event.id}: ${reason}`);
127
132
  failed++;
128
133
  }
129
134
  }
@@ -25,7 +25,7 @@ export function registerSocialTools(server, deps) {
25
25
  maxEntries: deps.veilCacheMax ?? 500,
26
26
  });
27
27
  server.registerTool('social-post', {
28
- description: 'Post a text note (kind 1) signed by the active identity and publish to relays. Returns { id, pubkey, publish: { success, accepted, rejected } }. The most common social action.',
28
+ description: 'Post a text note (kind 1) signed by the active identity and publish to relays. Returns { id, pubkey, publish: { success, allAccepted, accepted, rejected } } where success is the majority-quorum signal (true when at least one relay accepted and at least half of attempted relays accepted) and allAccepted is true only when every attempted relay accepted. The most common social action.',
29
29
  inputSchema: {
30
30
  content: z.string().describe('Text content of the note'),
31
31
  },
package/dist/types.d.ts CHANGED
@@ -21,7 +21,18 @@ export interface RelaySet {
21
21
  }
22
22
  /** Result of a publish operation */
23
23
  export interface PublishResult {
24
+ /**
25
+ * True when the event is reliably published: at least one relay accepted
26
+ * AND at least 50% of attempted relays accepted. Captures both "reached
27
+ * the network" and "reached a majority", which matches real-world
28
+ * expectations where paywalled or whitelisted relays routinely reject.
29
+ */
24
30
  success: boolean;
31
+ /**
32
+ * True when every attempted relay accepted. Use this for high-assurance
33
+ * publishing to a small curated list of private relays.
34
+ */
35
+ allAccepted: boolean;
25
36
  accepted: string[];
26
37
  rejected: string[];
27
38
  errors: string[];
@@ -60,7 +60,15 @@ export interface VaultRotateResult {
60
60
  export declare function handleVaultCreate(ctx: SigningContext, pool: RelayPool, args: {
61
61
  tiers: string[];
62
62
  }): Promise<VaultCreateResult>;
63
- /** Encrypt content with a content key derived from the active identity for a given tier + epoch. */
63
+ /**
64
+ * Encrypt content with the active identity's content key for the given epoch.
65
+ *
66
+ * The `tier` is recorded on the returned result for caller bookkeeping but is
67
+ * not part of the content key derivation itself. dominion-protocol's content
68
+ * keys rotate per-epoch via HKDF(privkey, "epoch:{epochId}"); tier-level
69
+ * access control is implemented by sharing the CK to different subsets of
70
+ * pubkeys (addToTier / addIndividualGrant), not by tier-scoped keys.
71
+ */
64
72
  export declare function handleVaultEncrypt(ctx: SigningContext, args: {
65
73
  content: string;
66
74
  tier: string;
@@ -72,7 +80,12 @@ export declare function handleVaultShare(ctx: SigningContext, pool: RelayPool, a
72
80
  recipients: string[];
73
81
  epoch?: string;
74
82
  }): Promise<VaultShareResult>;
75
- /** Decrypt ciphertext using the content key for the active identity's tier + epoch. */
83
+ /**
84
+ * Decrypt ciphertext using the active identity's content key for the given epoch.
85
+ *
86
+ * `tier` is carried on the result for caller bookkeeping but not used in CK
87
+ * derivation (see handleVaultEncrypt for the same note).
88
+ */
76
89
  export declare function handleVaultRead(ctx: SigningContext, args: {
77
90
  ciphertext: string;
78
91
  tier: string;
@@ -63,7 +63,15 @@ export async function handleVaultCreate(ctx, pool, args) {
63
63
  tiers: Object.keys(config.tiers),
64
64
  };
65
65
  }
66
- /** Encrypt content with a content key derived from the active identity for a given tier + epoch. */
66
+ /**
67
+ * Encrypt content with the active identity's content key for the given epoch.
68
+ *
69
+ * The `tier` is recorded on the returned result for caller bookkeeping but is
70
+ * not part of the content key derivation itself. dominion-protocol's content
71
+ * keys rotate per-epoch via HKDF(privkey, "epoch:{epochId}"); tier-level
72
+ * access control is implemented by sharing the CK to different subsets of
73
+ * pubkeys (addToTier / addIndividualGrant), not by tier-scoped keys.
74
+ */
67
75
  export async function handleVaultEncrypt(ctx, args) {
68
76
  const privkeyHex = Buffer.from(ctx.activePrivateKey).toString('hex');
69
77
  const epoch = args.epoch ?? getCurrentEpochId();
@@ -122,7 +130,12 @@ export async function handleVaultShare(ctx, pool, args) {
122
130
  }
123
131
  return { published, failed, recipients: successfulRecipients };
124
132
  }
125
- /** Decrypt ciphertext using the content key for the active identity's tier + epoch. */
133
+ /**
134
+ * Decrypt ciphertext using the active identity's content key for the given epoch.
135
+ *
136
+ * `tier` is carried on the result for caller bookkeeping but not used in CK
137
+ * derivation (see handleVaultEncrypt for the same note).
138
+ */
126
139
  export async function handleVaultRead(ctx, args) {
127
140
  const privkeyHex = Buffer.from(ctx.activePrivateKey).toString('hex');
128
141
  const ck = deriveContentKey(privkeyHex, args.epoch, args.tier);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nostr-bray",
3
- "version": "1.27.3",
3
+ "version": "1.27.5",
4
4
  "description": "Trust-aware Nostr for AI and humans. Three dimensions of trust -- verification, proximity, and access -- woven into every interaction.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",