nostr-bray 1.26.1 → 1.27.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/bunker-context.d.ts +2 -0
- package/dist/bunker-context.js +17 -3
- package/dist/bunker.d.ts +1 -0
- package/dist/bunker.js +24 -6
- package/dist/state.d.ts +4 -0
- package/dist/state.js +23 -0
- package/package.json +1 -1
package/dist/bunker-context.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface BunkerConfig {
|
|
|
20
20
|
}
|
|
21
21
|
/** Parse a bunker:// URI */
|
|
22
22
|
export declare function parseBunkerUri(uri: string): BunkerConfig;
|
|
23
|
+
/** Resolve the client secret key: URI secret > cached > generate & persist. */
|
|
24
|
+
export declare function resolveClientKey(config: BunkerConfig, stateDir?: string): Uint8Array;
|
|
23
25
|
export declare class BunkerContext implements SigningContext {
|
|
24
26
|
protected signer: BunkerSigner;
|
|
25
27
|
protected pool: SimplePool;
|
package/dist/bunker-context.js
CHANGED
|
@@ -14,6 +14,7 @@ import { useWebSocketImplementation, SimplePool } from 'nostr-tools/pool';
|
|
|
14
14
|
import { generateSecretKey } from 'nostr-tools/pure';
|
|
15
15
|
import { npubEncode } from 'nostr-tools/nip19';
|
|
16
16
|
import WebSocket from 'ws';
|
|
17
|
+
import { readStateFile, writeStateFile } from './state.js';
|
|
17
18
|
useWebSocketImplementation(WebSocket);
|
|
18
19
|
/** Parse a bunker:// URI */
|
|
19
20
|
export function parseBunkerUri(uri) {
|
|
@@ -27,6 +28,21 @@ export function parseBunkerUri(uri) {
|
|
|
27
28
|
}
|
|
28
29
|
return { pubkey, relays, secret };
|
|
29
30
|
}
|
|
31
|
+
const CLIENT_KEYS_FILE = 'client-keys.json';
|
|
32
|
+
/** Resolve the client secret key: URI secret > cached > generate & persist. */
|
|
33
|
+
export function resolveClientKey(config, stateDir) {
|
|
34
|
+
if (config.secret) {
|
|
35
|
+
return Buffer.from(config.secret, 'hex');
|
|
36
|
+
}
|
|
37
|
+
const cache = readStateFile(CLIENT_KEYS_FILE, stateDir);
|
|
38
|
+
if (cache[config.pubkey]) {
|
|
39
|
+
return Buffer.from(cache[config.pubkey], 'hex');
|
|
40
|
+
}
|
|
41
|
+
const sk = generateSecretKey();
|
|
42
|
+
cache[config.pubkey] = Buffer.from(sk).toString('hex');
|
|
43
|
+
writeStateFile(CLIENT_KEYS_FILE, cache, stateDir);
|
|
44
|
+
return sk;
|
|
45
|
+
}
|
|
30
46
|
export class BunkerContext {
|
|
31
47
|
signer;
|
|
32
48
|
pool;
|
|
@@ -40,9 +56,7 @@ export class BunkerContext {
|
|
|
40
56
|
/** Connect to a remote bunker. Blocks until the connection is established. */
|
|
41
57
|
static async connect(uri, timeoutMs = 15_000) {
|
|
42
58
|
const config = parseBunkerUri(uri);
|
|
43
|
-
const clientSk = config
|
|
44
|
-
? Buffer.from(config.secret, 'hex')
|
|
45
|
-
: generateSecretKey();
|
|
59
|
+
const clientSk = resolveClientKey(config);
|
|
46
60
|
const pool = new SimplePool();
|
|
47
61
|
const signer = BunkerSigner.fromBunker(clientSk, { pubkey: config.pubkey, relays: config.relays, secret: null }, { pool });
|
|
48
62
|
// Connect and verify
|
package/dist/bunker.d.ts
CHANGED
package/dist/bunker.js
CHANGED
|
@@ -14,17 +14,26 @@ import { finalizeEvent, getPublicKey, generateSecretKey } from 'nostr-tools/pure
|
|
|
14
14
|
import { npubEncode } from 'nostr-tools/nip19';
|
|
15
15
|
import { useWebSocketImplementation, SimplePool } from 'nostr-tools/pool';
|
|
16
16
|
import WebSocket from 'ws';
|
|
17
|
+
import { readStateFile, writeStateFile } from './state.js';
|
|
17
18
|
useWebSocketImplementation(WebSocket);
|
|
18
19
|
export function startBunker(opts) {
|
|
19
20
|
const { ctx, relays, quiet } = opts;
|
|
20
|
-
const authorizedKeys = new Set(opts.authorizedKeys ?? []);
|
|
21
21
|
const log = quiet ? () => { } : (...args) => console.error('[bunker]', ...args);
|
|
22
|
-
//
|
|
22
|
+
// Bunker keypair — must be computed first so we can load persisted approvals
|
|
23
23
|
const bunkerSk = opts.bunkerKeyHex
|
|
24
24
|
? Buffer.from(opts.bunkerKeyHex, 'hex')
|
|
25
25
|
: generateSecretKey();
|
|
26
26
|
const bunkerPk = getPublicKey(bunkerSk);
|
|
27
27
|
const bunkerNpub = npubEncode(bunkerPk);
|
|
28
|
+
// Authorised clients: CLI flag controls the gate, persisted approvals supplement
|
|
29
|
+
const APPROVALS_FILE = 'approved-clients.json';
|
|
30
|
+
const persisted = readStateFile(APPROVALS_FILE, opts.stateDir);
|
|
31
|
+
const bunkerApprovals = persisted[bunkerPk] ?? [];
|
|
32
|
+
const cliAuthorizedKeys = new Set(opts.authorizedKeys ?? []);
|
|
33
|
+
const authorizedKeys = new Set([
|
|
34
|
+
...cliAuthorizedKeys,
|
|
35
|
+
...bunkerApprovals,
|
|
36
|
+
]);
|
|
28
37
|
const pool = new SimplePool();
|
|
29
38
|
// Subscribe to kind 24133 events addressed to us
|
|
30
39
|
const sub = pool.subscribeMany(relays, { kinds: [24133], '#p': [bunkerPk] }, {
|
|
@@ -39,8 +48,8 @@ export function startBunker(opts) {
|
|
|
39
48
|
});
|
|
40
49
|
async function handleRequest(event) {
|
|
41
50
|
const clientPk = event.pubkey;
|
|
42
|
-
// Check authorization
|
|
43
|
-
if (
|
|
51
|
+
// Check authorization — gate only activates when CLI --authorized-keys were set
|
|
52
|
+
if (cliAuthorizedKeys.size > 0 && !authorizedKeys.has(clientPk)) {
|
|
44
53
|
log(`Rejected request from unauthorized key: ${clientPk.slice(0, 12)}...`);
|
|
45
54
|
return;
|
|
46
55
|
}
|
|
@@ -60,6 +69,15 @@ export function startBunker(opts) {
|
|
|
60
69
|
let error;
|
|
61
70
|
switch (request.method) {
|
|
62
71
|
case 'connect':
|
|
72
|
+
// Persist newly-approved client
|
|
73
|
+
if (!bunkerApprovals.includes(clientPk)) {
|
|
74
|
+
bunkerApprovals.push(clientPk);
|
|
75
|
+
authorizedKeys.add(clientPk);
|
|
76
|
+
const current = readStateFile(APPROVALS_FILE, opts.stateDir);
|
|
77
|
+
current[bunkerPk] = bunkerApprovals;
|
|
78
|
+
writeStateFile(APPROVALS_FILE, current, opts.stateDir);
|
|
79
|
+
log(`Approved and persisted client: ${clientPk.slice(0, 12)}...`);
|
|
80
|
+
}
|
|
63
81
|
result = 'ack';
|
|
64
82
|
break;
|
|
65
83
|
case 'ping':
|
|
@@ -188,8 +206,8 @@ export function startBunker(opts) {
|
|
|
188
206
|
log(`Bunker started: ${bunkerNpub}`);
|
|
189
207
|
log(`URI: ${bunkerUrl}`);
|
|
190
208
|
log(`Signing as: ${ctx.activeNpub}`);
|
|
191
|
-
if (
|
|
192
|
-
log(`Authorized keys: ${authorizedKeys.size}`);
|
|
209
|
+
if (cliAuthorizedKeys.size > 0) {
|
|
210
|
+
log(`Authorized keys: ${authorizedKeys.size} (${cliAuthorizedKeys.size} CLI + ${bunkerApprovals.length} persisted)`);
|
|
193
211
|
}
|
|
194
212
|
else {
|
|
195
213
|
log('WARNING: No authorized keys set — accepting requests from anyone');
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Read and parse a JSON state file. Returns `{}` if missing or corrupt. */
|
|
2
|
+
export declare function readStateFile<T = Record<string, unknown>>(name: string, stateDir?: string): T;
|
|
3
|
+
/** Write a JSON state file with 0600 permissions. Creates directory if needed. */
|
|
4
|
+
export declare function writeStateFile(name: string, data: unknown, stateDir?: string): void;
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
const DEFAULT_STATE_DIR = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'bray');
|
|
5
|
+
/** Read and parse a JSON state file. Returns `{}` if missing or corrupt. */
|
|
6
|
+
export function readStateFile(name, stateDir = DEFAULT_STATE_DIR) {
|
|
7
|
+
const path = join(stateDir, name);
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Write a JSON state file with 0600 permissions. Creates directory if needed. */
|
|
16
|
+
export function writeStateFile(name, data, stateDir = DEFAULT_STATE_DIR) {
|
|
17
|
+
if (!existsSync(stateDir)) {
|
|
18
|
+
mkdirSync(stateDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
const path = join(stateDir, name);
|
|
21
|
+
writeFileSync(path, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
22
|
+
chmodSync(path, 0o600);
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nostr-bray",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
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",
|