applesauce-relay 0.0.0-next-20250806165639 → 0.0.0-next-20250815164532

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.
@@ -0,0 +1,65 @@
1
+ export function createClientSpy(addOnMessageListener) {
2
+ let queue = { type: "empty" };
3
+ addOnMessageListener((message) => {
4
+ switch (queue.type) {
5
+ case "empty": {
6
+ queue = {
7
+ type: "message",
8
+ items: [message],
9
+ };
10
+ break;
11
+ }
12
+ case "message": {
13
+ queue.items.push(message);
14
+ break;
15
+ }
16
+ case "resolver": {
17
+ const item = queue.items.shift();
18
+ if (item) {
19
+ const resolve = item[0];
20
+ resolve(message);
21
+ }
22
+ if (queue.items.length <= 0) {
23
+ queue = { type: "empty" };
24
+ }
25
+ }
26
+ }
27
+ });
28
+ return {
29
+ __createClientSpy: true,
30
+ next: () => new Promise((resolve, reject) => {
31
+ switch (queue.type) {
32
+ case "empty": {
33
+ queue = {
34
+ type: "resolver",
35
+ items: [[resolve, reject]],
36
+ };
37
+ break;
38
+ }
39
+ case "message": {
40
+ const msg = queue.items.shift();
41
+ if (msg) {
42
+ resolve(msg);
43
+ }
44
+ if (queue.items.length <= 0) {
45
+ queue = { type: "empty" };
46
+ }
47
+ break;
48
+ }
49
+ case "resolver": {
50
+ queue.items.push([resolve, reject]);
51
+ break;
52
+ }
53
+ }
54
+ }),
55
+ dispose: () => {
56
+ if (queue.type === "resolver") {
57
+ for (const item of queue.items) {
58
+ const reject = item[1];
59
+ reject(new Error());
60
+ }
61
+ }
62
+ queue = { type: "empty" };
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,3 @@
1
+ export declare class TimeoutError extends Error {
2
+ }
3
+ export declare function withTimeout<T>(task: Promise<T> | (() => Promise<T>), message?: string, timeout?: number): Promise<T>;
@@ -0,0 +1,8 @@
1
+ export class TimeoutError extends Error {
2
+ }
3
+ export function withTimeout(task, message, timeout = 1000) {
4
+ return Promise.race([
5
+ typeof task === "function" ? task() : task,
6
+ new Promise((_, reject) => setTimeout(() => reject(new TimeoutError(message)), timeout)),
7
+ ]);
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as exports from "../index.js";
3
+ describe("exports", () => {
4
+ it("should export the expected functions", () => {
5
+ expect(Object.keys(exports).sort()).toMatchInlineSnapshot(`
6
+ [
7
+ "completeOnEose",
8
+ "markFromRelay",
9
+ "onlyEvents",
10
+ "storeEvents",
11
+ "toEventStore",
12
+ ]
13
+ `);
14
+ });
15
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import { EventStore } from "applesauce-core";
2
+ import { describe, expect, it } from "vitest";
3
+ import { FakeUser } from "../../__tests__/fake-user.js";
4
+ import { of } from "rxjs";
5
+ import { subscribeSpyTo } from "@hirez_io/observer-spy";
6
+ import { toEventStore } from "../to-event-store.js";
7
+ const user = new FakeUser();
8
+ describe("toEventStore", () => {
9
+ it("should remove duplicate events", () => {
10
+ const eventStore = new EventStore();
11
+ const event = user.note("original content");
12
+ const source = of(event, { ...event }, { ...event }, { ...event });
13
+ const spy = subscribeSpyTo(source.pipe(toEventStore(eventStore)));
14
+ expect(spy.getValuesLength()).toBe(1);
15
+ expect(spy.getValueAt(0).length).toBe(1);
16
+ expect(spy.getValueAt(0)[0]).toBe(event);
17
+ });
18
+ });
package/dist/relay.d.ts CHANGED
@@ -1,16 +1,20 @@
1
1
  import { logger } from "applesauce-core";
2
2
  import { type Filter, type NostrEvent } from "nostr-tools";
3
- import { BehaviorSubject, Observable } from "rxjs";
4
- import { WebSocketSubject, WebSocketSubjectConfig } from "rxjs/webSocket";
5
3
  import { RelayInformation } from "nostr-tools/nip11";
4
+ import { BehaviorSubject, MonoTypeOperatorFunction, Observable, RepeatConfig, RetryConfig, Subject } from "rxjs";
5
+ import { WebSocketSubject, WebSocketSubjectConfig } from "rxjs/webSocket";
6
6
  import { AuthSigner, FilterInput, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
7
7
  /** An error that is thrown when a REQ is closed from the relay side */
8
8
  export declare class ReqCloseError extends Error {
9
9
  }
10
10
  export type RelayOptions = {
11
+ /** Custom WebSocket implementation */
11
12
  WebSocket?: WebSocketSubjectConfig<any>["WebSocketCtor"];
13
+ /** How long to wait for an EOSE message (default 10s) */
12
14
  eoseTimeout?: number;
15
+ /** How long to wait for an OK message from the relay (default 10s) */
13
16
  eventTimeout?: number;
17
+ /** How long to keep the connection alive after nothing is subscribed (default 30s) */
14
18
  keepAlive?: number;
15
19
  };
16
20
  export declare class Relay implements IRelay {
@@ -50,6 +54,12 @@ export declare class Relay implements IRelay {
50
54
  protected _nip11: RelayInformation | null;
51
55
  /** An observable that emits the limitations for the relay */
52
56
  limitations$: Observable<RelayInformation["limitation"] | null>;
57
+ /** An observable that emits when underlying websocket is opened */
58
+ open$: Subject<Event>;
59
+ /** An observable that emits when underlying websocket is closed */
60
+ close$: Subject<CloseEvent>;
61
+ /** An observable that emits when underlying websocket is closing due to unsubscription */
62
+ closing$: Subject<void>;
53
63
  get connected(): boolean;
54
64
  get challenge(): string | null;
55
65
  get notices(): string[];
@@ -87,6 +97,10 @@ export declare class Relay implements IRelay {
87
97
  auth(event: NostrEvent): Promise<PublishResponse>;
88
98
  /** Authenticate with the relay using a signer */
89
99
  authenticate(signer: AuthSigner): Promise<PublishResponse>;
100
+ /** Internal operator for creating the retry() operator */
101
+ protected customRetryOperator<T extends unknown = unknown>(times: number | RetryConfig): MonoTypeOperatorFunction<T>;
102
+ /** Internal operator for creating the repeat() operator */
103
+ protected customRepeatOperator<T extends unknown = unknown>(times: boolean | number | RepeatConfig | undefined): MonoTypeOperatorFunction<T>;
90
104
  /** Creates a REQ that retries when relay errors ( default 3 retries ) */
91
105
  subscription(filters: Filter | Filter[], opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
92
106
  /** Makes a single request that retires on errors and completes on EOSE */
package/dist/relay.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { logger } from "applesauce-core";
2
+ import { ensureHttpURL } from "applesauce-core/helpers";
2
3
  import { simpleTimeout } from "applesauce-core/observable";
3
4
  import { nanoid } from "nanoid";
4
5
  import { nip42 } from "nostr-tools";
5
- import { BehaviorSubject, catchError, combineLatest, defer, endWith, filter, finalize, from, ignoreElements, isObservable, lastValueFrom, map, merge, mergeMap, mergeWith, NEVER, of, retry, scan, share, shareReplay, Subject, switchMap, take, takeUntil, tap, throwError, timeout, timer, } from "rxjs";
6
+ import { BehaviorSubject, catchError, combineLatest, defer, endWith, filter, finalize, from, identity, ignoreElements, isObservable, lastValueFrom, map, merge, mergeMap, mergeWith, NEVER, of, repeat, retry, scan, share, shareReplay, Subject, switchMap, take, takeUntil, tap, throwError, timeout, timer, } from "rxjs";
6
7
  import { webSocket } from "rxjs/webSocket";
7
- import { ensureHttpURL } from "applesauce-core/helpers";
8
8
  import { completeOnEose } from "./operators/complete-on-eose.js";
9
9
  import { markFromRelay } from "./operators/mark-from-relay.js";
10
10
  /** An error that is thrown when a REQ is closed from the relay side */
@@ -47,6 +47,12 @@ export class Relay {
47
47
  _nip11 = null;
48
48
  /** An observable that emits the limitations for the relay */
49
49
  limitations$;
50
+ /** An observable that emits when underlying websocket is opened */
51
+ open$ = new Subject();
52
+ /** An observable that emits when underlying websocket is closed */
53
+ close$ = new Subject();
54
+ /** An observable that emits when underlying websocket is closing due to unsubscription */
55
+ closing$ = new Subject();
50
56
  // sync state
51
57
  get connected() {
52
58
  return this.connected$.value;
@@ -107,28 +113,28 @@ export class Relay {
107
113
  this.authenticated$ = this.authenticationResponse$.pipe(map((response) => response?.ok === true));
108
114
  /** Use the static method to create a new reconnect method for this relay */
109
115
  this.reconnectTimer = Relay.createReconnectTimer(url);
116
+ // Subscribe to open and close events
117
+ this.open$.subscribe(() => {
118
+ this.log("Connected");
119
+ this.connected$.next(true);
120
+ this.attempts$.next(0);
121
+ this.error$.next(null);
122
+ this.resetState();
123
+ });
124
+ this.close$.subscribe((event) => {
125
+ this.log("Disconnected");
126
+ this.connected$.next(false);
127
+ this.attempts$.next(this.attempts$.value + 1);
128
+ this.resetState();
129
+ // Start the reconnect timer if the connection was not closed cleanly
130
+ if (!event.wasClean)
131
+ this.startReconnectTimer(event);
132
+ });
110
133
  this.socket = webSocket({
111
134
  url,
112
- openObserver: {
113
- next: () => {
114
- this.log("Connected");
115
- this.connected$.next(true);
116
- this.attempts$.next(0);
117
- this.error$.next(null);
118
- this.resetState();
119
- },
120
- },
121
- closeObserver: {
122
- next: (event) => {
123
- this.log("Disconnected");
124
- this.connected$.next(false);
125
- this.attempts$.next(this.attempts$.value + 1);
126
- this.resetState();
127
- // Start the reconnect timer if the connection was not closed cleanly
128
- if (!event.wasClean)
129
- this.startReconnectTimer(event);
130
- },
131
- },
135
+ openObserver: this.open$,
136
+ closeObserver: this.close$,
137
+ closingObserver: this.closing$,
132
138
  WebSocketCtor: opts?.WebSocket,
133
139
  });
134
140
  // Create an observable to fetch the NIP-11 information document
@@ -207,7 +213,7 @@ export class Relay {
207
213
  }
208
214
  /** Wait for authentication state, make connection and then wait for authentication if required */
209
215
  waitForAuth(
210
- // NOTE: require BehaviorSubject so it always has a value
216
+ // NOTE: require BehaviorSubject or shareReplay so it always has a value
211
217
  requireAuth, observable) {
212
218
  return combineLatest([requireAuth, this.authenticated$]).pipe(
213
219
  // Once the auth state is known, make a connection and watch for auth challenges
@@ -245,21 +251,26 @@ export class Relay {
245
251
  // Convert filters input into an observable, if its a normal value merge it with NEVER so it never completes
246
252
  const input = isObservable(filters) ? filters : merge(of(filters), NEVER);
247
253
  // Create an observable that completes when the upstream observable completes
248
- const complete = input.pipe(ignoreElements(), endWith(null));
254
+ const filtersComplete = input.pipe(ignoreElements(), endWith(null));
249
255
  // Create an observable that filters responses from the relay to just the ones for this REQ
250
- const messages = this.socket.pipe(filter((m) => Array.isArray(m) && (m[0] === "EVENT" || m[0] === "CLOSED" || m[0] === "EOSE") && m[1] === id));
256
+ const messages = this.socket.pipe(filter((m) => Array.isArray(m) && (m[0] === "EVENT" || m[0] === "CLOSED" || m[0] === "EOSE") && m[1] === id),
257
+ // Singleton (prevents the .pipe() operator later from sending two REQ messages )
258
+ share());
251
259
  // Create an observable that controls sending the filters and closing the REQ
252
260
  const control = input.pipe(
253
261
  // Send the filters when they change
254
262
  tap((filters) => this.socket.next(Array.isArray(filters) ? ["REQ", id, ...filters] : ["REQ", id, filters])),
255
- // Close the req when unsubscribed
263
+ // Send the CLOSE message when unsubscribed or input completes
256
264
  finalize(() => this.socket.next(["CLOSE", id])),
257
265
  // Once filters have been sent, switch to listening for messages
258
266
  switchMap(() => messages));
259
267
  // Start the watch tower with the observables
260
268
  const observable = merge(this.watchTower, control).pipe(
269
+ // Complete the subscription when the control observable completes
270
+ // This is to work around the fact that merge() waits for both observables to complete
271
+ takeUntil(messages.pipe(ignoreElements(), endWith(true))),
261
272
  // Complete the subscription when the input is completed
262
- takeUntil(complete),
273
+ takeUntil(filtersComplete),
263
274
  // Map the messages to events, EOSE, or throw an error
264
275
  map((message) => {
265
276
  if (message[0] === "EOSE")
@@ -294,15 +305,20 @@ export class Relay {
294
305
  }
295
306
  /** Send an EVENT or AUTH message and return an observable of PublishResponse that completes or errors */
296
307
  event(event, verb = "EVENT") {
297
- const base = defer(() => {
308
+ const messages = defer(() => {
298
309
  // Send event when subscription starts
299
310
  this.socket.next([verb, event]);
300
311
  return this.socket.pipe(filter((m) => m[0] === "OK" && m[1] === event.id),
301
312
  // format OK message
302
313
  map((m) => ({ ok: m[2], message: m[3], from: this.url })));
303
- });
314
+ }).pipe(
315
+ // Singleton (prevents the .pipe() operator later from sending two EVENT messages )
316
+ share());
304
317
  // Start the watch tower and add complete operators
305
- const observable = merge(this.watchTower, base).pipe(
318
+ const observable = merge(this.watchTower, messages).pipe(
319
+ // Complete the subscription when the messages observable completes
320
+ // This is to work around the fact that merge() waits for both observables to complete
321
+ takeUntil(messages.pipe(ignoreElements(), endWith(true))),
306
322
  // complete on first value
307
323
  take(1),
308
324
  // listen for OK auth-required
@@ -339,19 +355,45 @@ export class Relay {
339
355
  const start = p instanceof Promise ? from(p) : of(p);
340
356
  return lastValueFrom(start.pipe(switchMap((event) => this.auth(event))));
341
357
  }
358
+ /** Internal operator for creating the retry() operator */
359
+ customRetryOperator(times) {
360
+ if (typeof times === "number")
361
+ return retry(times);
362
+ else
363
+ return retry(times);
364
+ }
365
+ /** Internal operator for creating the repeat() operator */
366
+ customRepeatOperator(times) {
367
+ if (times === false || times === undefined)
368
+ return identity;
369
+ else if (times === true)
370
+ return repeat();
371
+ else if (typeof times === "number")
372
+ return repeat(times);
373
+ else
374
+ return repeat(times);
375
+ }
342
376
  /** Creates a REQ that retries when relay errors ( default 3 retries ) */
343
377
  subscription(filters, opts) {
344
378
  return this.req(filters, opts?.id).pipe(
345
379
  // Retry on connection errors
346
- retry({ count: opts?.retries ?? 3, resetOnSuccess: true }));
380
+ this.customRetryOperator(opts?.retries ?? 3),
381
+ // Create reconnect logic (repeat operator)
382
+ this.customRepeatOperator(opts?.reconnect),
383
+ // Single subscription
384
+ share());
347
385
  }
348
386
  /** Makes a single request that retires on errors and completes on EOSE */
349
387
  request(filters, opts) {
350
388
  return this.req(filters, opts?.id).pipe(
351
389
  // Retry on connection errors
352
- retry(opts?.retries ?? 3),
390
+ this.customRetryOperator(opts?.retries ?? 3),
391
+ // Create reconnect logic (repeat operator)
392
+ this.customRepeatOperator(opts?.reconnect),
353
393
  // Complete when EOSE is received
354
- completeOnEose());
394
+ completeOnEose(),
395
+ // Single subscription
396
+ share());
355
397
  }
356
398
  /** Publishes an event to the relay and retries when relay errors or responds with auth-required ( default 3 retries ) */
357
399
  publish(event, opts) {
@@ -362,7 +404,11 @@ export class Relay {
362
404
  return of(result);
363
405
  }),
364
406
  // Retry the publish until it succeeds or the number of retries is reached
365
- retry(opts?.retries ?? 3)));
407
+ this.customRetryOperator(opts?.retries ?? 3),
408
+ // Create reconnect logic (repeat operator)
409
+ this.customRepeatOperator(opts?.reconnect),
410
+ // Single subscription
411
+ share()));
366
412
  }
367
413
  /** Force close the connection */
368
414
  close() {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ const IncomingREQ = z.tuple([z.literal("REQ"), z.string(), z.array(z.any())]);
3
+ const IncomingEVENT = z.tuple([z.literal("EVENT"), z.any()]);
4
+ const IncomingCLOSE = z.tuple([z.literal("CLOSE"), z.string()]);
5
+ const IncomingAUTH = z.tuple([z.literal("AUTH"), z.any()]);
6
+ const OutgoingEVENT = z.tuple([z.literal("EVENT"), z.string(), z.any()]);
7
+ const OutgoingNOTICE = z.tuple([z.literal("NOTICE"), z.string()]);
8
+ const OutgoingCLOSE = z.tuple([z.literal("CLOSE"), z.string()]);
9
+ const OutgoingAUTH = z.tuple([z.literal("AUTH"), z.string()]);
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type EventTemplate, type Filter, type NostrEvent } from "nostr-tools";
2
- import { Observable } from "rxjs";
2
+ import { Observable, repeat, retry } from "rxjs";
3
3
  import { WebSocketSubject } from "rxjs/webSocket";
4
4
  export type SubscriptionResponse = NostrEvent | "EOSE";
5
5
  export type PublishResponse = {
@@ -8,16 +8,35 @@ export type PublishResponse = {
8
8
  from: string;
9
9
  };
10
10
  export type MultiplexWebSocket<T = any> = Pick<WebSocketSubject<T>, "multiplex">;
11
+ /** Options for the publish method on the pool and relay */
11
12
  export type PublishOptions = {
12
- retries?: number;
13
- };
14
- export type RequestOptions = {
15
- id?: string;
16
- retries?: number;
13
+ /**
14
+ * Number of times to retry the publish. default is 3
15
+ * @see https://rxjs.dev/api/index/function/retry
16
+ */
17
+ retries?: number | Parameters<typeof retry>[0];
18
+ /**
19
+ * Whether to reconnect when socket is closed. A number of times or true for infinite. default is false
20
+ * @see https://rxjs.dev/api/index/function/repeat
21
+ */
22
+ reconnect?: boolean | Parameters<typeof repeat>[0];
17
23
  };
24
+ /** Options for the request method on the pool and relay */
25
+ export type RequestOptions = SubscriptionOptions;
26
+ /** Options for the subscription method on the pool and relay */
18
27
  export type SubscriptionOptions = {
28
+ /** Custom REQ id for the subscription */
19
29
  id?: string;
20
- retries?: number;
30
+ /**
31
+ * Number of times to retry a request. default is 3
32
+ * @see https://rxjs.dev/api/index/function/retry
33
+ */
34
+ retries?: number | Parameters<typeof retry>[0];
35
+ /**
36
+ * Whether to reconnect when socket is closed. A number of times or true for infinite. default is false
37
+ * @see https://rxjs.dev/api/index/function/repeat
38
+ */
39
+ reconnect?: boolean | Parameters<typeof repeat>[0];
21
40
  };
22
41
  export type AuthSigner = {
23
42
  signEvent: (event: EventTemplate) => NostrEvent | Promise<NostrEvent>;
@@ -47,19 +66,11 @@ export interface IRelay extends MultiplexWebSocket {
47
66
  /** Authenticate with the relay using a signer */
48
67
  authenticate(signer: AuthSigner): Promise<PublishResponse>;
49
68
  /** Send an EVENT message with retries */
50
- publish(event: NostrEvent, opts?: {
51
- retries?: number;
52
- }): Promise<PublishResponse>;
69
+ publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse>;
53
70
  /** Send a REQ message with retries */
54
- request(filters: FilterInput, opts?: {
55
- id?: string;
56
- retries?: number;
57
- }): Observable<NostrEvent>;
71
+ request(filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
58
72
  /** Open a subscription with retries */
59
- subscription(filters: FilterInput, opts?: {
60
- id?: string;
61
- retries?: number;
62
- }): Observable<SubscriptionResponse>;
73
+ subscription(filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
63
74
  }
64
75
  export interface IGroup {
65
76
  /** Send a REQ message */
@@ -67,19 +78,11 @@ export interface IGroup {
67
78
  /** Send an EVENT message */
68
79
  event(event: NostrEvent): Observable<PublishResponse>;
69
80
  /** Send an EVENT message with retries */
70
- publish(event: NostrEvent, opts?: {
71
- retries?: number;
72
- }): Promise<PublishResponse[]>;
81
+ publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse[]>;
73
82
  /** Send a REQ message with retries */
74
- request(filters: FilterInput, opts?: {
75
- id?: string;
76
- retries?: number;
77
- }): Observable<NostrEvent>;
83
+ request(filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
78
84
  /** Open a subscription with retries */
79
- subscription(filters: FilterInput, opts?: {
80
- id?: string;
81
- retries?: number;
82
- }): Observable<SubscriptionResponse>;
85
+ subscription(filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
83
86
  }
84
87
  export interface IPool {
85
88
  /** Get or create a relay */
@@ -93,17 +96,9 @@ export interface IPool {
93
96
  /** Send an EVENT message */
94
97
  event(relays: string[], event: NostrEvent): Observable<PublishResponse>;
95
98
  /** Send an EVENT message to relays with retries */
96
- publish(relays: string[], event: NostrEvent, opts?: {
97
- retries?: number;
98
- }): Promise<PublishResponse[]>;
99
+ publish(relays: string[], event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse[]>;
99
100
  /** Send a REQ message to relays with retries */
100
- request(relays: string[], filters: FilterInput, opts?: {
101
- id?: string;
102
- retries?: number;
103
- }): Observable<NostrEvent>;
101
+ request(relays: string[], filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
104
102
  /** Open a subscription to relays with retries */
105
- subscription(relays: string[], filters: FilterInput, opts?: {
106
- id?: string;
107
- retries?: number;
108
- }): Observable<SubscriptionResponse>;
103
+ subscription(relays: string[], filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
109
104
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-relay",
3
- "version": "0.0.0-next-20250806165639",
3
+ "version": "0.0.0-next-20250815164532",
4
4
  "description": "nostr relay communication framework built on rxjs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -54,14 +54,14 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@noble/hashes": "^1.7.1",
57
- "applesauce-core": "0.0.0-next-20250806165639",
57
+ "applesauce-core": "0.0.0-next-20250815164532",
58
58
  "nanoid": "^5.0.9",
59
59
  "nostr-tools": "^2.13",
60
60
  "rxjs": "^7.8.1"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@hirez_io/observer-spy": "^2.2.0",
64
- "applesauce-signers": "0.0.0-next-20250806165639",
64
+ "applesauce-signers": "0.0.0-next-20250815164532",
65
65
  "@vitest/expect": "^3.1.1",
66
66
  "typescript": "^5.7.3",
67
67
  "vitest": "^3.2.3",