nostr-bray 3.4.2 → 3.4.3
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 +18 -0
- package/dist/bunker-context.js +20 -5
- package/dist/cli/index.js +2 -0
- package/dist/heartwood-context.d.ts +53 -7
- package/dist/heartwood-context.js +120 -22
- package/dist/index.js +4 -1
- package/package.json +3 -3
package/dist/bunker-context.d.ts
CHANGED
|
@@ -16,6 +16,24 @@ import { BunkerSigner } from 'nostr-tools/nip46';
|
|
|
16
16
|
import { SimplePool } from 'nostr-tools/pool';
|
|
17
17
|
import type { PublicIdentity, SignFn } from './types.js';
|
|
18
18
|
import type { SigningContext } from './signing-context.js';
|
|
19
|
+
/** Bound the NIP-46 connect handshake. nostr-tools' BunkerSigner has no
|
|
20
|
+
* per-request timeout, so an offline signer or unreachable relays make the
|
|
21
|
+
* connect hang forever. A hardware signer (e.g. Heartwood) may also wait on
|
|
22
|
+
* a physical button press -- roughly a 30-second window -- so this must sit
|
|
23
|
+
* comfortably above that, not at or under it. */
|
|
24
|
+
export declare const CONNECT_TIMEOUT_MS = 60000;
|
|
25
|
+
/** Bound individual signer round-trips (sign_event, get_public_key, nip44).
|
|
26
|
+
* Same button-press headroom as {@link CONNECT_TIMEOUT_MS}. */
|
|
27
|
+
export declare const REQUEST_TIMEOUT_MS = 60000;
|
|
28
|
+
/**
|
|
29
|
+
* Thrown by {@link withTimeout} when the wrapped promise does not settle in
|
|
30
|
+
* time. A distinct class (rather than a plain `Error`) lets callers tell a
|
|
31
|
+
* timeout apart from a genuine rejection -- e.g. `HeartwoodContext.probe()`
|
|
32
|
+
* treats a timed-out request differently from a denied one.
|
|
33
|
+
*/
|
|
34
|
+
export declare class TimeoutError extends Error {
|
|
35
|
+
constructor(message: string);
|
|
36
|
+
}
|
|
19
37
|
/**
|
|
20
38
|
* Reject if `promise` does not settle within `ms`. Converts a silent NIP-46
|
|
21
39
|
* hang into a catchable error so callers can fail fast or retry with backoff
|
package/dist/bunker-context.js
CHANGED
|
@@ -21,10 +21,25 @@ import { readStateFile, writeStateFile } from './state.js';
|
|
|
21
21
|
useWebSocketImplementation(WebSocket);
|
|
22
22
|
/** Bound the NIP-46 connect handshake. nostr-tools' BunkerSigner has no
|
|
23
23
|
* per-request timeout, so an offline signer or unreachable relays make the
|
|
24
|
-
* connect hang forever.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
24
|
+
* connect hang forever. A hardware signer (e.g. Heartwood) may also wait on
|
|
25
|
+
* a physical button press -- roughly a 30-second window -- so this must sit
|
|
26
|
+
* comfortably above that, not at or under it. */
|
|
27
|
+
export const CONNECT_TIMEOUT_MS = 60_000;
|
|
28
|
+
/** Bound individual signer round-trips (sign_event, get_public_key, nip44).
|
|
29
|
+
* Same button-press headroom as {@link CONNECT_TIMEOUT_MS}. */
|
|
30
|
+
export const REQUEST_TIMEOUT_MS = 60_000;
|
|
31
|
+
/**
|
|
32
|
+
* Thrown by {@link withTimeout} when the wrapped promise does not settle in
|
|
33
|
+
* time. A distinct class (rather than a plain `Error`) lets callers tell a
|
|
34
|
+
* timeout apart from a genuine rejection -- e.g. `HeartwoodContext.probe()`
|
|
35
|
+
* treats a timed-out request differently from a denied one.
|
|
36
|
+
*/
|
|
37
|
+
export class TimeoutError extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'TimeoutError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
28
43
|
/**
|
|
29
44
|
* Reject if `promise` does not settle within `ms`. Converts a silent NIP-46
|
|
30
45
|
* hang into a catchable error so callers can fail fast or retry with backoff
|
|
@@ -34,7 +49,7 @@ const REQUEST_TIMEOUT_MS = 30_000;
|
|
|
34
49
|
export function withTimeout(promise, ms, label) {
|
|
35
50
|
let timer;
|
|
36
51
|
const timeout = new Promise((_resolve, reject) => {
|
|
37
|
-
timer = setTimeout(() => reject(new
|
|
52
|
+
timer = setTimeout(() => reject(new TimeoutError(`${label} timed out after ${ms}ms`)), ms);
|
|
38
53
|
});
|
|
39
54
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
40
55
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -382,6 +382,8 @@ if (config.bunkerUri) {
|
|
|
382
382
|
if (hw)
|
|
383
383
|
ctx = hw;
|
|
384
384
|
console.error(`Connected to ${hw ? 'Heartwood' : 'bunker'} — signing as ${ctx.activeNpub}`);
|
|
385
|
+
if (hw && !hw.listingAvailable)
|
|
386
|
+
console.error(hw.listingUnavailableMessage);
|
|
385
387
|
}
|
|
386
388
|
else {
|
|
387
389
|
ctx = new IdentityContext(config.secretKey, config.secretFormat, { forbidPubkeys: config.forbidPubkeys });
|
|
@@ -9,22 +9,68 @@ import { BunkerContext } from './bunker-context.js';
|
|
|
9
9
|
import type { LinkageProof } from 'nsec-tree';
|
|
10
10
|
import type { PublicIdentity } from './types.js';
|
|
11
11
|
import type { ExtendedSigningContext } from './signing-context.js';
|
|
12
|
+
/**
|
|
13
|
+
* Why {@link HeartwoodContext.listIdentities} is unavailable for this
|
|
14
|
+
* pairing, set by {@link HeartwoodContext.probe} when it could not confirm
|
|
15
|
+
* identity listing works.
|
|
16
|
+
*
|
|
17
|
+
* - `'denied'` -- the device (or a strict-policy slot) refused
|
|
18
|
+
* `heartwood_list_identities` with an unauthorised/denied error.
|
|
19
|
+
* - `'pending-approval'` -- the probe's `heartwood_list_identities` request
|
|
20
|
+
* timed out, most likely because it is awaiting a button press on the
|
|
21
|
+
* device right now.
|
|
22
|
+
*/
|
|
23
|
+
export type HeartwoodListingUnavailableReason = 'denied' | 'pending-approval';
|
|
12
24
|
export declare class HeartwoodContext extends BunkerContext implements ExtendedSigningContext {
|
|
25
|
+
/** Whether `listIdentities()` (and thus features that depend on the
|
|
26
|
+
* device's identity list) is expected to work for this pairing. `true`
|
|
27
|
+
* unless `probe()` could not confirm listing works. */
|
|
28
|
+
listingAvailable: boolean;
|
|
29
|
+
/** Set by `probe()` alongside `listingAvailable = false`; explains why. */
|
|
30
|
+
listingUnavailableReason?: HeartwoodListingUnavailableReason;
|
|
31
|
+
/** Human-readable explanation of `listingUnavailableReason`, suitable for
|
|
32
|
+
* logging or surfacing to a caller. Undefined when listing is available. */
|
|
33
|
+
listingUnavailableMessage?: string;
|
|
13
34
|
/**
|
|
14
35
|
* Probe a connected BunkerContext for Heartwood extensions.
|
|
15
|
-
* Returns a HeartwoodContext if extensions are available, null otherwise.
|
|
16
36
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
37
|
+
* Returns `null` when the remote signer does not speak the Heartwood
|
|
38
|
+
* dialect at all (method unknown/unsupported, or a response that doesn't
|
|
39
|
+
* look like an identity list) -- the base context stays a plain bunker,
|
|
40
|
+
* silently, as before.
|
|
41
|
+
*
|
|
42
|
+
* Returns a `HeartwoodContext` in every other case where the device IS a
|
|
43
|
+
* Heartwood but identity listing could not be confirmed:
|
|
44
|
+
* - the request was refused by policy (`unauthorised`/denied -- e.g. a
|
|
45
|
+
* strict slot that doesn't list `heartwood_list_identities`), or
|
|
46
|
+
* - the request timed out, most likely because it's awaiting a button
|
|
47
|
+
* press on the device right now.
|
|
48
|
+
*
|
|
49
|
+
* In both of those cases `listingAvailable` is set to `false` and
|
|
50
|
+
* `listingUnavailableReason` / `listingUnavailableMessage` explain why.
|
|
51
|
+
* Callers that only check "is this a Heartwood" (`hw !== null` /
|
|
52
|
+
* `instanceof HeartwoodContext`) see no change in behaviour.
|
|
19
53
|
*/
|
|
20
54
|
static probe(base: BunkerContext): Promise<HeartwoodContext | null>;
|
|
21
|
-
/**
|
|
55
|
+
/** Re-class `base` as a HeartwoodContext with listing marked unavailable. */
|
|
56
|
+
private static upgrade;
|
|
57
|
+
/** Derive a child identity by purpose and index on the Heartwood device.
|
|
58
|
+
* Does not depend on `heartwood_list_identities` -- works even when
|
|
59
|
+
* `listingAvailable` is `false`. */
|
|
22
60
|
derive(purpose: string, index: number): Promise<PublicIdentity>;
|
|
23
|
-
/** Derive a named persona on the Heartwood device.
|
|
61
|
+
/** Derive a named persona on the Heartwood device.
|
|
62
|
+
* Does not depend on `heartwood_list_identities` -- works even when
|
|
63
|
+
* `listingAvailable` is `false`. */
|
|
24
64
|
derivePersona(name: string, index: number): Promise<PublicIdentity>;
|
|
25
|
-
/** List all known identities on the Heartwood device.
|
|
65
|
+
/** List all known identities on the Heartwood device.
|
|
66
|
+
*
|
|
67
|
+
* Throws immediately, without a device round trip, when `probe()` already
|
|
68
|
+
* established that listing isn't available for this pairing -- retrying
|
|
69
|
+
* would just re-hit the same denial or wait out the same timeout. */
|
|
26
70
|
listIdentities(): Promise<PublicIdentity[]>;
|
|
27
|
-
/** Switch the active identity on the Heartwood device.
|
|
71
|
+
/** Switch the active identity on the Heartwood device.
|
|
72
|
+
* Does not depend on `heartwood_list_identities` -- works even when
|
|
73
|
+
* `listingAvailable` is `false`. */
|
|
28
74
|
switch(purposeOrName: string, index?: number): Promise<void>;
|
|
29
75
|
/** Create a linkage proof on the Heartwood device. */
|
|
30
76
|
prove(mode?: 'blind' | 'full'): Promise<LinkageProof>;
|
|
@@ -5,50 +5,148 @@
|
|
|
5
5
|
* extension methods for identity derivation, switching, proofs, and recovery.
|
|
6
6
|
* HeartwoodContext auto-detects these extensions by probing the remote signer.
|
|
7
7
|
*/
|
|
8
|
-
import { BunkerContext } from './bunker-context.js';
|
|
8
|
+
import { BunkerContext, withTimeout, TimeoutError, REQUEST_TIMEOUT_MS } from './bunker-context.js';
|
|
9
|
+
/** Matches an error whose text indicates the remote signer does not know the method at all. */
|
|
10
|
+
function isMethodUnknownError(message) {
|
|
11
|
+
return /unsupported method|method not found|unknown method|no such method|not implemented/i.test(message);
|
|
12
|
+
}
|
|
13
|
+
/** Matches an error whose text indicates the request was understood but refused by policy. */
|
|
14
|
+
function isUnauthorisedError(message) {
|
|
15
|
+
return /unauthori[sz]ed|denied|not permitted|forbidden/i.test(message);
|
|
16
|
+
}
|
|
17
|
+
/** How long a Heartwood request runs before we hint that it's probably
|
|
18
|
+
* waiting on a device button press. Well under REQUEST_TIMEOUT_MS so the
|
|
19
|
+
* hint has a chance to show before the request either completes or times out. */
|
|
20
|
+
const APPROVAL_HINT_MS = 3_000;
|
|
21
|
+
/**
|
|
22
|
+
* Wrap a Heartwood NIP-46 request with the standard 60s timeout, and log a
|
|
23
|
+
* one-off hint to stderr if it's still pending after {@link APPROVAL_HINT_MS}.
|
|
24
|
+
* bray is an MCP server over stdio, so this MUST go to stderr (console.error,
|
|
25
|
+
* same channel every other log line in this codebase uses) and never stdout,
|
|
26
|
+
* which carries the JSON-RPC transport.
|
|
27
|
+
*/
|
|
28
|
+
function withApprovalHint(promise, label) {
|
|
29
|
+
const hint = setTimeout(() => {
|
|
30
|
+
console.error(`Waiting for approval on your Heartwood (${label})...`);
|
|
31
|
+
}, APPROVAL_HINT_MS);
|
|
32
|
+
return withTimeout(promise, REQUEST_TIMEOUT_MS, label).finally(() => clearTimeout(hint));
|
|
33
|
+
}
|
|
34
|
+
/** Extract a human-readable message from whatever `sendRequest` rejected with.
|
|
35
|
+
* NIP-46 error responses reject with a bare string (the signer's `error`
|
|
36
|
+
* field); our own code (timeouts, network failures) rejects with an `Error`. */
|
|
37
|
+
function errorMessage(e) {
|
|
38
|
+
if (typeof e === 'string')
|
|
39
|
+
return e;
|
|
40
|
+
if (e instanceof Error)
|
|
41
|
+
return e.message;
|
|
42
|
+
return String(e);
|
|
43
|
+
}
|
|
9
44
|
export class HeartwoodContext extends BunkerContext {
|
|
45
|
+
/** Whether `listIdentities()` (and thus features that depend on the
|
|
46
|
+
* device's identity list) is expected to work for this pairing. `true`
|
|
47
|
+
* unless `probe()` could not confirm listing works. */
|
|
48
|
+
listingAvailable = true;
|
|
49
|
+
/** Set by `probe()` alongside `listingAvailable = false`; explains why. */
|
|
50
|
+
listingUnavailableReason;
|
|
51
|
+
/** Human-readable explanation of `listingUnavailableReason`, suitable for
|
|
52
|
+
* logging or surfacing to a caller. Undefined when listing is available. */
|
|
53
|
+
listingUnavailableMessage;
|
|
10
54
|
/**
|
|
11
55
|
* Probe a connected BunkerContext for Heartwood extensions.
|
|
12
|
-
* Returns a HeartwoodContext if extensions are available, null otherwise.
|
|
13
56
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
57
|
+
* Returns `null` when the remote signer does not speak the Heartwood
|
|
58
|
+
* dialect at all (method unknown/unsupported, or a response that doesn't
|
|
59
|
+
* look like an identity list) -- the base context stays a plain bunker,
|
|
60
|
+
* silently, as before.
|
|
61
|
+
*
|
|
62
|
+
* Returns a `HeartwoodContext` in every other case where the device IS a
|
|
63
|
+
* Heartwood but identity listing could not be confirmed:
|
|
64
|
+
* - the request was refused by policy (`unauthorised`/denied -- e.g. a
|
|
65
|
+
* strict slot that doesn't list `heartwood_list_identities`), or
|
|
66
|
+
* - the request timed out, most likely because it's awaiting a button
|
|
67
|
+
* press on the device right now.
|
|
68
|
+
*
|
|
69
|
+
* In both of those cases `listingAvailable` is set to `false` and
|
|
70
|
+
* `listingUnavailableReason` / `listingUnavailableMessage` explain why.
|
|
71
|
+
* Callers that only check "is this a Heartwood" (`hw !== null` /
|
|
72
|
+
* `instanceof HeartwoodContext`) see no change in behaviour.
|
|
16
73
|
*/
|
|
17
74
|
static async probe(base) {
|
|
75
|
+
// Cast to access protected signer — safe because HeartwoodContext extends BunkerContext
|
|
76
|
+
const signer = base.signer;
|
|
77
|
+
let result;
|
|
18
78
|
try {
|
|
19
|
-
|
|
20
|
-
const signer = base.signer;
|
|
21
|
-
const result = await signer.sendRequest('heartwood_list_identities', []);
|
|
22
|
-
if (!isHeartwoodIdentitiesResponse(result))
|
|
23
|
-
return null;
|
|
24
|
-
// Re-class the base context as HeartwoodContext
|
|
25
|
-
return Object.setPrototypeOf(base, HeartwoodContext.prototype);
|
|
79
|
+
result = await withTimeout(signer.sendRequest('heartwood_list_identities', []), REQUEST_TIMEOUT_MS, 'heartwood probe (heartwood_list_identities)');
|
|
26
80
|
}
|
|
27
|
-
catch {
|
|
81
|
+
catch (e) {
|
|
82
|
+
const message = errorMessage(e);
|
|
83
|
+
if (e instanceof TimeoutError) {
|
|
84
|
+
return HeartwoodContext.upgrade(base, 'pending-approval', 'Heartwood is awaiting approval on the device for identity listing ' +
|
|
85
|
+
'(heartwood_list_identities timed out) -- approve the pending request on the device.');
|
|
86
|
+
}
|
|
87
|
+
if (isUnauthorisedError(message)) {
|
|
88
|
+
return HeartwoodContext.upgrade(base, 'denied', 'Heartwood denied identity listing for this pairing; allow heartwood_list_identities for this app in Sapwood.');
|
|
89
|
+
}
|
|
90
|
+
if (isMethodUnknownError(message))
|
|
91
|
+
return null;
|
|
92
|
+
// Anything else is genuinely ambiguous -- keep the previous, conservative
|
|
93
|
+
// behaviour of treating it as "not a Heartwood" rather than guessing.
|
|
28
94
|
return null;
|
|
29
95
|
}
|
|
96
|
+
if (!isHeartwoodIdentitiesResponse(result))
|
|
97
|
+
return null;
|
|
98
|
+
// Re-class the base context as HeartwoodContext. Object.setPrototypeOf
|
|
99
|
+
// only swaps the prototype -- it does NOT re-run class field
|
|
100
|
+
// initialisers on an object that was constructed as a plain
|
|
101
|
+
// BunkerContext, so `listingAvailable` must be set explicitly here.
|
|
102
|
+
const hw = Object.setPrototypeOf(base, HeartwoodContext.prototype);
|
|
103
|
+
hw.listingAvailable = true;
|
|
104
|
+
hw.listingUnavailableReason = undefined;
|
|
105
|
+
hw.listingUnavailableMessage = undefined;
|
|
106
|
+
return hw;
|
|
107
|
+
}
|
|
108
|
+
/** Re-class `base` as a HeartwoodContext with listing marked unavailable. */
|
|
109
|
+
static upgrade(base, reason, message) {
|
|
110
|
+
const hw = Object.setPrototypeOf(base, HeartwoodContext.prototype);
|
|
111
|
+
hw.listingAvailable = false;
|
|
112
|
+
hw.listingUnavailableReason = reason;
|
|
113
|
+
hw.listingUnavailableMessage = message;
|
|
114
|
+
return hw;
|
|
30
115
|
}
|
|
31
|
-
/** Derive a child identity by purpose and index on the Heartwood device.
|
|
116
|
+
/** Derive a child identity by purpose and index on the Heartwood device.
|
|
117
|
+
* Does not depend on `heartwood_list_identities` -- works even when
|
|
118
|
+
* `listingAvailable` is `false`. */
|
|
32
119
|
async derive(purpose, index) {
|
|
33
|
-
const result = await this.signer.sendRequest('heartwood_derive', [purpose, String(index)]);
|
|
120
|
+
const result = await withApprovalHint(this.signer.sendRequest('heartwood_derive', [purpose, String(index)]), 'heartwood derive');
|
|
34
121
|
return JSON.parse(result);
|
|
35
122
|
}
|
|
36
|
-
/** Derive a named persona on the Heartwood device.
|
|
123
|
+
/** Derive a named persona on the Heartwood device.
|
|
124
|
+
* Does not depend on `heartwood_list_identities` -- works even when
|
|
125
|
+
* `listingAvailable` is `false`. */
|
|
37
126
|
async derivePersona(name, index) {
|
|
38
|
-
const result = await this.signer.sendRequest('heartwood_derive_persona', [name, String(index)]);
|
|
127
|
+
const result = await withApprovalHint(this.signer.sendRequest('heartwood_derive_persona', [name, String(index)]), 'heartwood derive persona');
|
|
39
128
|
return JSON.parse(result);
|
|
40
129
|
}
|
|
41
|
-
/** List all known identities on the Heartwood device.
|
|
130
|
+
/** List all known identities on the Heartwood device.
|
|
131
|
+
*
|
|
132
|
+
* Throws immediately, without a device round trip, when `probe()` already
|
|
133
|
+
* established that listing isn't available for this pairing -- retrying
|
|
134
|
+
* would just re-hit the same denial or wait out the same timeout. */
|
|
42
135
|
async listIdentities() {
|
|
43
|
-
|
|
136
|
+
if (!this.listingAvailable) {
|
|
137
|
+
throw new Error(this.listingUnavailableMessage ?? 'Heartwood identity listing is unavailable for this pairing.');
|
|
138
|
+
}
|
|
139
|
+
const result = await withApprovalHint(this.signer.sendRequest('heartwood_list_identities', []), 'heartwood list identities');
|
|
44
140
|
return JSON.parse(result);
|
|
45
141
|
}
|
|
46
|
-
/** Switch the active identity on the Heartwood device.
|
|
142
|
+
/** Switch the active identity on the Heartwood device.
|
|
143
|
+
* Does not depend on `heartwood_list_identities` -- works even when
|
|
144
|
+
* `listingAvailable` is `false`. */
|
|
47
145
|
async switch(purposeOrName, index) {
|
|
48
146
|
const params = index !== undefined
|
|
49
147
|
? [purposeOrName, String(index)]
|
|
50
148
|
: [purposeOrName];
|
|
51
|
-
const raw = await this.signer.sendRequest('heartwood_switch', params);
|
|
149
|
+
const raw = await withApprovalHint(this.signer.sendRequest('heartwood_switch', params), 'heartwood switch');
|
|
52
150
|
// Parse response — device may return { npub } object or a bare npub string
|
|
53
151
|
const { decode, npubEncode } = await import('nostr-tools/nip19');
|
|
54
152
|
let npub;
|
|
@@ -80,13 +178,13 @@ export class HeartwoodContext extends BunkerContext {
|
|
|
80
178
|
}
|
|
81
179
|
/** Create a linkage proof on the Heartwood device. */
|
|
82
180
|
async prove(mode = 'blind') {
|
|
83
|
-
const result = await this.signer.sendRequest('heartwood_create_proof', [this.activePublicKeyHex, mode]);
|
|
181
|
+
const result = await withApprovalHint(this.signer.sendRequest('heartwood_create_proof', [this.activePublicKeyHex, mode]), 'heartwood create proof');
|
|
84
182
|
return JSON.parse(result);
|
|
85
183
|
}
|
|
86
184
|
/** Recover identities by scanning derived keys on the Heartwood device. */
|
|
87
185
|
async recover(lookahead) {
|
|
88
186
|
const params = lookahead !== undefined ? [String(lookahead)] : [];
|
|
89
|
-
const result = await this.signer.sendRequest('heartwood_recover', params);
|
|
187
|
+
const result = await withApprovalHint(this.signer.sendRequest('heartwood_recover', params), 'heartwood recover');
|
|
90
188
|
return JSON.parse(result);
|
|
91
189
|
}
|
|
92
190
|
}
|
package/dist/index.js
CHANGED
|
@@ -79,8 +79,11 @@ async function establishBunkerInBackground(base) {
|
|
|
79
79
|
// Probe for Heartwood extensions and upgrade the prototype in place so
|
|
80
80
|
// tools that check `ctx instanceof HeartwoodContext` see it.
|
|
81
81
|
const hw = await HeartwoodContext.probe(base);
|
|
82
|
-
if (hw)
|
|
82
|
+
if (hw) {
|
|
83
83
|
console.error(`Heartwood extensions detected -- ${base.activeNpub}`);
|
|
84
|
+
if (!hw.listingAvailable)
|
|
85
|
+
console.error(hw.listingUnavailableMessage);
|
|
86
|
+
}
|
|
84
87
|
}
|
|
85
88
|
catch (e) {
|
|
86
89
|
console.error('Heartwood probe failed (non-fatal):', e.message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nostr-bray",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.3",
|
|
4
4
|
"mcpName": "dev.forgesworn/bray",
|
|
5
5
|
"description": "Trust-aware Nostr for AI and humans. Three dimensions of trust -- verification, proximity, and access -- woven into every interaction.",
|
|
6
6
|
"type": "module",
|
|
@@ -178,9 +178,9 @@
|
|
|
178
178
|
"devDependencies": {
|
|
179
179
|
"@types/node": "^25.5.0",
|
|
180
180
|
"@types/ws": "^8.18.1",
|
|
181
|
-
"@vitest/coverage-v8": "^
|
|
181
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
182
182
|
"typescript": "^6.0.2",
|
|
183
|
-
"vitest": "^
|
|
183
|
+
"vitest": "^5.0.0",
|
|
184
184
|
"yaml": "^2.9.0"
|
|
185
185
|
},
|
|
186
186
|
"publishConfig": {
|