applesauce-core 2.2.0 → 2.3.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.
@@ -79,4 +79,6 @@ export declare class EventSet implements IEventSet {
79
79
  getEventsForFilters(filters: Filter[]): Set<NostrEvent>;
80
80
  /** Remove the oldest events that are not claimed */
81
81
  prune(limit?: number): number;
82
+ /** Resets the event set */
83
+ reset(): void;
82
84
  }
@@ -344,4 +344,14 @@ export class EventSet {
344
344
  }
345
345
  return removed;
346
346
  }
347
+ /** Resets the event set */
348
+ reset() {
349
+ this.events.clear();
350
+ this.kinds.clear();
351
+ this.authors.clear();
352
+ this.tags.clear();
353
+ this.created_at = [];
354
+ this.replaceable.clear();
355
+ this.claims = new WeakMap();
356
+ }
347
357
  }
@@ -3,7 +3,7 @@ import { catchError, combineLatest, distinct, EMPTY, filter, isObservable, map,
3
3
  import { logger } from "../logger.js";
4
4
  import { canHaveEncryptedContent, getEncryptedContent, isEncryptedContentLocked, setEncryptedContentCache, } from "./encrypted-content.js";
5
5
  import { notifyEventUpdate } from "./event.js";
6
- import { getGiftWrapSeal } from "./gift-wraps.js";
6
+ import { getGiftWrapSeal, getSealGiftWrap, getSealRumor } from "./gift-wraps.js";
7
7
  /** A symbol that is used to mark encrypted content as being from a cache */
8
8
  export const EncryptedContentFromCacheSymbol = Symbol.for("encrypted-content-from-cache");
9
9
  /** Marks the encrypted content as being from a cache */
@@ -54,25 +54,29 @@ export function persistEncryptedContent(eventStore, storage, fallback) {
54
54
  // Look for gift wraps that are unlocked
55
55
  filter((e) => e.kind === kinds.GiftWrap && !isEncryptedContentLocked(e)),
56
56
  // Get the seal event
57
- map((gift) => [gift, getGiftWrapSeal(gift)]),
57
+ map((gift) => getGiftWrapSeal(gift)),
58
58
  // Look for gift wraps with locked seals
59
- filter(([_gift, seal]) => seal !== undefined && isEncryptedContentLocked(seal)),
59
+ filter((seal) => seal !== undefined && isEncryptedContentLocked(seal)),
60
60
  // Only attempt to unlock seals once
61
- distinct(([_gift, seal]) => seal.id),
61
+ distinct((seal) => seal.id),
62
62
  // Get encrypted content from storage
63
- mergeMap(([gift, seal]) =>
63
+ mergeMap((seal) =>
64
64
  // Wait for storage to be available
65
- storage$.pipe(switchMap((storage) => combineLatest([of(gift), of(seal), getItem(storage, seal)])), catchError((error) => {
65
+ storage$.pipe(switchMap((storage) => combineLatest([of(seal), getItem(storage, seal)])), catchError((error) => {
66
66
  log(`Failed to restore encrypted content for ${seal.id}`, error);
67
67
  return EMPTY;
68
68
  }))))
69
- .subscribe(async ([gift, seal, content]) => {
69
+ .subscribe(async ([seal, content]) => {
70
70
  if (!seal || !content)
71
71
  return;
72
72
  markEncryptedContentFromCache(seal);
73
73
  setEncryptedContentCache(seal, content);
74
+ // Parse the rumor event
75
+ getSealRumor(seal);
74
76
  // Trigger an update to the gift wrap event
75
- notifyEventUpdate(gift);
77
+ const gift = getSealGiftWrap(seal);
78
+ if (gift)
79
+ notifyEventUpdate(gift);
76
80
  log(`Restored encrypted content for ${seal.id}`);
77
81
  });
78
82
  // Persist encrypted content when it is updated or inserted
@@ -104,14 +108,14 @@ export function persistEncryptedContent(eventStore, storage, fallback) {
104
108
  // Look for gift wraps that are unlocked
105
109
  filter(([event]) => event.kind === kinds.GiftWrap && !isEncryptedContentLocked(event)),
106
110
  // Get the seal event
107
- map(([gift, storage]) => [gift, getGiftWrapSeal(gift), storage]),
111
+ map(([gift, storage]) => [getGiftWrapSeal(gift), storage]),
108
112
  // Make sure the seal is defined
109
- filter(([_gift, seal]) => seal !== undefined),
113
+ filter(([seal]) => seal !== undefined),
110
114
  // Make sure seal is unlocked and not from cache
111
- filter(([_gift, seal]) => !isEncryptedContentLocked(seal) && !isEncryptedContentFromCache(seal)),
115
+ filter(([seal]) => !isEncryptedContentLocked(seal) && !isEncryptedContentFromCache(seal)),
112
116
  // Only persist the seal once
113
117
  distinct(([seal]) => seal.id))
114
- .subscribe(async ([_gift, seal, storage]) => {
118
+ .subscribe(async ([seal, storage]) => {
115
119
  if (!seal)
116
120
  return;
117
121
  try {
@@ -1,26 +1,46 @@
1
1
  import { NostrEvent, UnsignedEvent } from "nostr-tools";
2
+ import { EventSet } from "../event-store/event-set.js";
2
3
  import { EncryptedContentSigner } from "./encrypted-content.js";
4
+ /**
5
+ * An internal event set to keep track of seals and rumors
6
+ * This is intentially isolated from the main applications event store so to prevent seals and rumors from being leaked
7
+ */
8
+ export declare const internalGiftWrapEvents: EventSet;
3
9
  export type Rumor = UnsignedEvent & {
4
10
  id: string;
5
11
  };
6
- export declare const GiftWrapSealSymbol: unique symbol;
7
- export declare const GiftWrapRumorSymbol: unique symbol;
8
- /** Returns the unsigned seal event in a gift-wrap event */
9
- export declare function getGiftWrapSeal(gift: NostrEvent): NostrEvent | undefined;
12
+ /** Used to store a reference to the seal event on gift wraps (downstream) or the seal event on rumors (upstream[]) */
13
+ export declare const SealSymbol: unique symbol;
14
+ /** Used to store a reference to the rumor on seals (downstream) */
15
+ export declare const RumorSymbol: unique symbol;
16
+ /** Used to store a reference to the parent gift wrap event on seals (upstream) */
17
+ export declare const GiftWrapSymbol: unique symbol;
18
+ /** Checks if an event is a rumor (normal event with "id" and no "sig") */
19
+ export declare function isRumor(event: any): event is Rumor;
20
+ /** Returns all the parent gift wraps for a seal event */
21
+ export declare function getSealGiftWrap(seal: NostrEvent): NostrEvent | undefined;
22
+ /** Returns all the parent seals for a rumor event */
23
+ export declare function getRumorSeals(rumor: Rumor): NostrEvent[];
24
+ /** Returns all the parent gift wraps for a rumor event */
25
+ export declare function getRumorGiftWraps(rumor: Rumor): NostrEvent[];
10
26
  /** Checks if a seal event is locked */
11
27
  export declare function isSealLocked(seal: NostrEvent): boolean;
12
28
  /** Gets the rumor from a seal event */
13
29
  export declare function getSealRumor(seal: NostrEvent): Rumor | undefined;
14
- /**
15
- * Returns the unsigned event in the gift-wrap seal
16
- * @throws {Error} If the author of the rumor event does not match the author of the seal
17
- */
30
+ /** Returns the seal event in a gift-wrap event */
31
+ export declare function getGiftWrapSeal(gift: NostrEvent): NostrEvent | undefined;
32
+ /** Returns the unsigned rumor in the gift-wrap */
18
33
  export declare function getGiftWrapRumor(gift: NostrEvent): Rumor | undefined;
19
- /** Checks if an event is a rumor (normal event with "id" and no "sig") */
20
- export declare function isRumor(event: any): event is Rumor;
21
34
  /** Returns if a gift-wrap event or gift-wrap seal is locked */
22
35
  export declare function isGiftWrapLocked(gift: NostrEvent): boolean;
23
- /** Unlocks and returns the unsigned seal event in a gift-wrap */
24
- export declare function unlockGiftWrap(gift: NostrEvent, signer: EncryptedContentSigner): Promise<UnsignedEvent>;
25
- /** Locks a gift-wrap event by removing its cached seal and encrypted content */
36
+ /**
37
+ * Unlocks a seal event and returns the rumor event
38
+ * @throws {Error} If the author of the rumor event does not match the author of the seal
39
+ */
40
+ export declare function unlockSeal(seal: NostrEvent, signer: EncryptedContentSigner): Promise<Rumor>;
41
+ /**
42
+ * Unlocks and returns the unsigned seal event in a gift-wrap
43
+ * @throws {Error} If the author of the rumor event does not match the author of the seal
44
+ */
45
+ export declare function unlockGiftWrap(gift: NostrEvent, signer: EncryptedContentSigner): Promise<Rumor>;
26
46
  export declare function lockGiftWrap(gift: NostrEvent): void;
@@ -1,51 +1,31 @@
1
1
  import { verifyEvent } from "nostr-tools";
2
- import { getOrComputeCachedValue } from "./cache.js";
2
+ import { EventSet } from "../event-store/event-set.js";
3
3
  import { getEncryptedContent, isEncryptedContentLocked, lockEncryptedContent, unlockEncryptedContent, } from "./encrypted-content.js";
4
- import { isEvent, notifyEventUpdate } from "./event.js";
5
- export const GiftWrapSealSymbol = Symbol.for("gift-wrap-seal");
6
- export const GiftWrapRumorSymbol = Symbol.for("gift-wrap-rumor");
7
- /** Returns the unsigned seal event in a gift-wrap event */
8
- export function getGiftWrapSeal(gift) {
9
- if (isEncryptedContentLocked(gift))
10
- return undefined;
11
- const plaintext = getEncryptedContent(gift);
12
- if (!plaintext)
13
- return undefined;
14
- return getOrComputeCachedValue(gift, GiftWrapSealSymbol, () => {
15
- const seal = JSON.parse(plaintext);
16
- verifyEvent(seal);
17
- return seal;
18
- });
19
- }
20
- /** Checks if a seal event is locked */
21
- export function isSealLocked(seal) {
22
- return isEncryptedContentLocked(seal);
23
- }
24
- /** Gets the rumor from a seal event */
25
- export function getSealRumor(seal) {
26
- if (isEncryptedContentLocked(seal))
27
- return undefined;
28
- const plaintext = getEncryptedContent(seal);
29
- if (!plaintext)
30
- return undefined;
31
- return getOrComputeCachedValue(seal, GiftWrapRumorSymbol, () => {
32
- const rumor = JSON.parse(plaintext);
33
- if (rumor.pubkey !== seal.pubkey)
34
- throw new Error("Seal author does not match rumor author");
35
- return rumor;
36
- });
37
- }
4
+ import { notifyEventUpdate } from "./event.js";
38
5
  /**
39
- * Returns the unsigned event in the gift-wrap seal
40
- * @throws {Error} If the author of the rumor event does not match the author of the seal
6
+ * An internal event set to keep track of seals and rumors
7
+ * This is intentially isolated from the main applications event store so to prevent seals and rumors from being leaked
41
8
  */
42
- export function getGiftWrapRumor(gift) {
43
- if (isEncryptedContentLocked(gift))
44
- return undefined;
45
- const seal = getGiftWrapSeal(gift);
46
- if (!seal || isSealLocked(seal))
47
- return undefined;
48
- return getOrComputeCachedValue(gift, GiftWrapRumorSymbol, () => getSealRumor(seal));
9
+ export const internalGiftWrapEvents = new EventSet();
10
+ /** Used to store a reference to the seal event on gift wraps (downstream) or the seal event on rumors (upstream[]) */
11
+ export const SealSymbol = Symbol.for("seal");
12
+ /** Used to store a reference to the rumor on seals (downstream) */
13
+ export const RumorSymbol = Symbol.for("rumor");
14
+ /** Used to store a reference to the parent gift wrap event on seals (upstream) */
15
+ export const GiftWrapSymbol = Symbol.for("gift-wrap");
16
+ /** Adds a parent reference to a seal or rumor */
17
+ function addParentSealReference(rumor, seal) {
18
+ const parents = Reflect.get(rumor, SealSymbol);
19
+ if (!parents)
20
+ Reflect.set(rumor, SealSymbol, new Set([seal]));
21
+ else
22
+ parents.add(seal);
23
+ }
24
+ /** Removes a parent reference from a seal or rumor */
25
+ function removeParentSealReference(rumor, seal) {
26
+ const parents = Reflect.get(rumor, SealSymbol);
27
+ if (parents)
28
+ parents.delete(seal);
49
29
  }
50
30
  /** Checks if an event is a rumor (normal event with "id" and no "sig") */
51
31
  export function isRumor(event) {
@@ -60,41 +40,157 @@ export function isRumor(event) {
60
40
  typeof event.created_at === "number" &&
61
41
  event.created_at > 0);
62
42
  }
43
+ /** Returns all the parent gift wraps for a seal event */
44
+ export function getSealGiftWrap(seal) {
45
+ return Reflect.get(seal, GiftWrapSymbol);
46
+ }
47
+ /** Returns all the parent seals for a rumor event */
48
+ export function getRumorSeals(rumor) {
49
+ let set = Reflect.get(rumor, SealSymbol);
50
+ if (!set) {
51
+ set = new Set();
52
+ Reflect.set(rumor, SealSymbol, set);
53
+ }
54
+ return Array.from(set);
55
+ }
56
+ /** Returns all the parent gift wraps for a rumor event */
57
+ export function getRumorGiftWraps(rumor) {
58
+ const giftWraps = new Set();
59
+ const seals = getRumorSeals(rumor);
60
+ for (const seal of seals) {
61
+ const upstream = getSealGiftWrap(seal);
62
+ if (upstream)
63
+ giftWraps.add(upstream);
64
+ }
65
+ return Array.from(giftWraps);
66
+ }
67
+ /** Checks if a seal event is locked */
68
+ export function isSealLocked(seal) {
69
+ return isEncryptedContentLocked(seal);
70
+ }
71
+ /** Gets the rumor from a seal event */
72
+ export function getSealRumor(seal) {
73
+ // Returned cached rumor if it exists (downstream)
74
+ const cached = Reflect.get(seal, RumorSymbol);
75
+ if (cached)
76
+ return cached;
77
+ // Get the encrypted content plaintext
78
+ const plaintext = getEncryptedContent(seal);
79
+ if (!plaintext)
80
+ return undefined;
81
+ let rumor = JSON.parse(plaintext);
82
+ // Check if the rumor event already exists in the internal event set
83
+ const existing = internalGiftWrapEvents.getEvent(rumor.id);
84
+ if (existing)
85
+ // Reuse the existing rumor instance
86
+ rumor = existing;
87
+ else
88
+ // Add to the internal event set
89
+ internalGiftWrapEvents.add(rumor);
90
+ // Save a reference to the parent seal event
91
+ addParentSealReference(rumor, seal);
92
+ // Save a reference to the rumor on the seal (downstream)
93
+ Reflect.set(seal, RumorSymbol, rumor);
94
+ return rumor;
95
+ }
96
+ /** Returns the seal event in a gift-wrap event */
97
+ export function getGiftWrapSeal(gift) {
98
+ // Returned cached seal if it exists (downstream)
99
+ const cached = Reflect.get(gift, SealSymbol);
100
+ if (cached)
101
+ return cached;
102
+ // Get the encrypted content plaintext
103
+ const plaintext = getEncryptedContent(gift);
104
+ if (!plaintext)
105
+ return undefined;
106
+ let seal = JSON.parse(plaintext);
107
+ // Check if the seal event already exists in the internal event set
108
+ const existing = internalGiftWrapEvents.getEvent(seal.id);
109
+ if (existing) {
110
+ // Reuse the existing seal instance
111
+ seal = existing;
112
+ }
113
+ else {
114
+ // Verify the seal event
115
+ verifyEvent(seal);
116
+ // Add to the internal event set
117
+ internalGiftWrapEvents.add(seal);
118
+ // Set the reference to the parent gift wrap event (upstream)
119
+ Reflect.set(seal, GiftWrapSymbol, gift);
120
+ }
121
+ // Save a reference to the seal on the gift wrap (downstream)
122
+ Reflect.set(gift, SealSymbol, seal);
123
+ return seal;
124
+ }
125
+ /** Returns the unsigned rumor in the gift-wrap */
126
+ export function getGiftWrapRumor(gift) {
127
+ const seal = getGiftWrapSeal(gift);
128
+ if (!seal)
129
+ return undefined;
130
+ return getSealRumor(seal);
131
+ }
63
132
  /** Returns if a gift-wrap event or gift-wrap seal is locked */
64
133
  export function isGiftWrapLocked(gift) {
65
134
  if (isEncryptedContentLocked(gift))
66
135
  return true;
67
136
  else {
68
137
  const seal = getGiftWrapSeal(gift);
69
- if (!seal || isEncryptedContentLocked(seal))
138
+ if (!seal || isSealLocked(seal))
70
139
  return true;
71
140
  else
72
141
  return false;
73
142
  }
74
143
  }
75
- /** Unlocks and returns the unsigned seal event in a gift-wrap */
144
+ /**
145
+ * Unlocks a seal event and returns the rumor event
146
+ * @throws {Error} If the author of the rumor event does not match the author of the seal
147
+ */
148
+ export async function unlockSeal(seal, signer) {
149
+ if (isEncryptedContentLocked(seal))
150
+ await unlockEncryptedContent(seal, seal.pubkey, signer);
151
+ // Parse the rumor event
152
+ const rumor = getSealRumor(seal);
153
+ if (!rumor)
154
+ throw new Error("Failed to read rumor in gift wrap");
155
+ // Check if the seal and rumor authors match
156
+ if (rumor.pubkey !== seal.pubkey)
157
+ throw new Error("Seal author does not match rumor author");
158
+ return rumor;
159
+ }
160
+ /**
161
+ * Unlocks and returns the unsigned seal event in a gift-wrap
162
+ * @throws {Error} If the author of the rumor event does not match the author of the seal
163
+ */
76
164
  export async function unlockGiftWrap(gift, signer) {
77
165
  // First unlock the gift-wrap event
78
166
  if (isEncryptedContentLocked(gift))
79
167
  await unlockEncryptedContent(gift, gift.pubkey, signer);
80
- // Next get and unlock the seal
168
+ // Get the seal event
81
169
  const seal = getGiftWrapSeal(gift);
82
170
  if (!seal)
83
171
  throw new Error("Failed to read seal in gift wrap");
84
- if (isEncryptedContentLocked(seal))
85
- await unlockEncryptedContent(seal, seal.pubkey, signer);
86
- // Finally get the rumor event
87
- const rumor = getGiftWrapRumor(gift);
88
- if (!rumor)
89
- throw new Error("Failed to read rumor in gift wrap");
172
+ // Unlock the seal event
173
+ const rumor = await unlockSeal(seal, signer);
90
174
  // if the event has been added to an event store, notify it
91
- if (isEvent(gift))
92
- notifyEventUpdate(gift);
175
+ notifyEventUpdate(gift);
93
176
  return rumor;
94
177
  }
95
- /** Locks a gift-wrap event by removing its cached seal and encrypted content */
96
178
  export function lockGiftWrap(gift) {
97
- Reflect.deleteProperty(gift, GiftWrapSealSymbol);
98
- Reflect.deleteProperty(gift, GiftWrapRumorSymbol);
179
+ const seal = getGiftWrapSeal(gift);
180
+ if (seal) {
181
+ const rumor = getSealRumor(seal);
182
+ // Remove the rumors parent seal reference (upstream)
183
+ if (rumor)
184
+ removeParentSealReference(rumor, seal);
185
+ // Remove the seal's parent gift wrap reference (upstream)
186
+ Reflect.deleteProperty(seal, GiftWrapSymbol);
187
+ // Remove the seal's rumor reference (downstream)
188
+ Reflect.deleteProperty(seal, RumorSymbol);
189
+ // Lock the seal's encrypted content
190
+ lockEncryptedContent(seal);
191
+ }
192
+ // Remove the gift wrap's seal reference (downstream)
193
+ Reflect.deleteProperty(gift, SealSymbol);
194
+ // Lock the gift wrap's encrypted content
99
195
  lockEncryptedContent(gift);
100
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",