applesauce-core 2.2.0 → 3.0.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/event-store/event-set.d.ts +2 -0
- package/dist/event-store/event-set.js +15 -3
- package/dist/event-store/event-store.d.ts +54 -24
- package/dist/event-store/event-store.js +139 -47
- package/dist/event-store/interface.d.ts +36 -18
- package/dist/helpers/calendar-event.d.ts +36 -0
- package/dist/helpers/calendar-event.js +110 -0
- package/dist/helpers/calendar-rsvp.d.ts +15 -0
- package/dist/helpers/calendar-rsvp.js +38 -0
- package/dist/helpers/calendar.d.ts +6 -0
- package/dist/helpers/calendar.js +11 -0
- package/dist/helpers/encrypted-content-cache.js +16 -12
- package/dist/helpers/encrypted-content.d.ts +18 -10
- package/dist/helpers/encrypted-content.js +11 -3
- package/dist/helpers/event-cache.d.ts +15 -0
- package/dist/helpers/event-cache.js +32 -0
- package/dist/helpers/event.d.ts +1 -1
- package/dist/helpers/event.js +1 -1
- package/dist/helpers/expiration.js +1 -2
- package/dist/helpers/gift-wraps.d.ts +33 -13
- package/dist/helpers/gift-wraps.js +154 -58
- package/dist/helpers/hidden-content.d.ts +3 -3
- package/dist/helpers/hidden-content.js +2 -2
- package/dist/helpers/hidden-tags.d.ts +5 -10
- package/dist/helpers/hidden-tags.js +3 -3
- package/dist/helpers/highlight.d.ts +45 -0
- package/dist/helpers/highlight.js +76 -0
- package/dist/helpers/index.d.ts +8 -0
- package/dist/helpers/index.js +8 -0
- package/dist/helpers/pointers.js +1 -1
- package/dist/helpers/poll.d.ts +46 -0
- package/dist/helpers/poll.js +78 -0
- package/dist/helpers/stream-chat.d.ts +4 -0
- package/dist/helpers/stream-chat.js +9 -0
- package/dist/helpers/stream.d.ts +31 -0
- package/dist/helpers/stream.js +81 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +1 -0
- package/dist/models/blossom.d.ts +2 -1
- package/dist/models/blossom.js +4 -2
- package/dist/models/calendar.d.ts +6 -0
- package/dist/models/calendar.js +15 -0
- package/dist/models/common.d.ts +14 -10
- package/dist/models/common.js +47 -76
- package/dist/models/contacts.d.ts +1 -1
- package/dist/models/contacts.js +4 -2
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/models/mailboxes.d.ts +2 -1
- package/dist/models/mailboxes.js +4 -2
- package/dist/models/mutes.d.ts +3 -2
- package/dist/models/mutes.js +4 -2
- package/dist/models/profile.d.ts +2 -1
- package/dist/models/profile.js +4 -2
- package/package.json +1 -1
- package/dist/event-store/common.d.ts +0 -1
- package/dist/event-store/common.js +0 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
import { AddressPointer, EventPointer, ProfilePointer } from "nostr-tools/nip19";
|
|
3
|
+
export declare const CALENDAR_EVENT_RSVP_KIND = 31925;
|
|
4
|
+
export type RSVPStatus = "accepted" | "declined" | "tentative";
|
|
5
|
+
export type RSVPFreeBusy = "free" | "busy";
|
|
6
|
+
/** Gets the RSVP status from a calendar event RSVP */
|
|
7
|
+
export declare function getRSVPStatus(event: NostrEvent): RSVPStatus | undefined;
|
|
8
|
+
/** Gets the free/busy status from a calendar event RSVP (will be undefined if the RSVP is declined) */
|
|
9
|
+
export declare function getRSVPFreeBusy(event: NostrEvent): RSVPFreeBusy | undefined;
|
|
10
|
+
/** Gets the referenced calendar event coordinate that the RSVP is responding to */
|
|
11
|
+
export declare function getRSVPAddressPointer(event: NostrEvent): AddressPointer | undefined;
|
|
12
|
+
/** Gets the referenced calendar event pointer that the RSVP is responding to */
|
|
13
|
+
export declare function getRSVPEventPointer(event: NostrEvent): EventPointer | undefined;
|
|
14
|
+
/** Gets the profile pointer that the RSVP is responding to */
|
|
15
|
+
export declare function getRSVPProfilePointer(event: NostrEvent): ProfilePointer | undefined;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getTagValue } from "./event-tags.js";
|
|
2
|
+
import { getAddressPointerFromATag, getEventPointerFromETag, getProfilePointerFromPTag } from "./pointers.js";
|
|
3
|
+
import { isATag, isETag, isPTag } from "./tags.js";
|
|
4
|
+
export const CALENDAR_EVENT_RSVP_KIND = 31925;
|
|
5
|
+
/** Gets the RSVP status from a calendar event RSVP */
|
|
6
|
+
export function getRSVPStatus(event) {
|
|
7
|
+
const status = getTagValue(event, "status");
|
|
8
|
+
return status && ["accepted", "declined", "tentative"].includes(status) ? status : undefined;
|
|
9
|
+
}
|
|
10
|
+
/** Gets the free/busy status from a calendar event RSVP (will be undefined if the RSVP is declined) */
|
|
11
|
+
export function getRSVPFreeBusy(event) {
|
|
12
|
+
const status = getRSVPStatus(event);
|
|
13
|
+
if (status === "declined")
|
|
14
|
+
return undefined;
|
|
15
|
+
const fb = getTagValue(event, "fb");
|
|
16
|
+
return fb && ["free", "busy"].includes(fb) ? fb : undefined;
|
|
17
|
+
}
|
|
18
|
+
/** Gets the referenced calendar event coordinate that the RSVP is responding to */
|
|
19
|
+
export function getRSVPAddressPointer(event) {
|
|
20
|
+
const tag = event.tags.find(isATag);
|
|
21
|
+
if (!tag)
|
|
22
|
+
return undefined;
|
|
23
|
+
return getAddressPointerFromATag(tag);
|
|
24
|
+
}
|
|
25
|
+
/** Gets the referenced calendar event pointer that the RSVP is responding to */
|
|
26
|
+
export function getRSVPEventPointer(event) {
|
|
27
|
+
const tag = event.tags.find(isETag);
|
|
28
|
+
if (!tag)
|
|
29
|
+
return undefined;
|
|
30
|
+
return getEventPointerFromETag(tag);
|
|
31
|
+
}
|
|
32
|
+
/** Gets the profile pointer that the RSVP is responding to */
|
|
33
|
+
export function getRSVPProfilePointer(event) {
|
|
34
|
+
const tag = event.tags.find(isPTag);
|
|
35
|
+
if (!tag)
|
|
36
|
+
return undefined;
|
|
37
|
+
return getProfilePointerFromPTag(tag);
|
|
38
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
import { AddressPointer } from "nostr-tools/nip19";
|
|
3
|
+
/** Gets the title of a calendar */
|
|
4
|
+
export declare function getCalendarTitle(event: NostrEvent): string | undefined;
|
|
5
|
+
/** Gets the address pointers to all the events on the calendar */
|
|
6
|
+
export declare function getCalendarAddressPointers(event: NostrEvent): AddressPointer[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getTagValue } from "./event-tags.js";
|
|
2
|
+
import { getAddressPointerFromATag } from "./pointers.js";
|
|
3
|
+
import { isATag } from "./tags.js";
|
|
4
|
+
/** Gets the title of a calendar */
|
|
5
|
+
export function getCalendarTitle(event) {
|
|
6
|
+
return getTagValue(event, "title");
|
|
7
|
+
}
|
|
8
|
+
/** Gets the address pointers to all the events on the calendar */
|
|
9
|
+
export function getCalendarAddressPointers(event) {
|
|
10
|
+
return event.tags.filter(isATag).map(getAddressPointerFromATag);
|
|
11
|
+
}
|
|
@@ -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) =>
|
|
57
|
+
map((gift) => getGiftWrapSeal(gift)),
|
|
58
58
|
// Look for gift wraps with locked seals
|
|
59
|
-
filter((
|
|
59
|
+
filter((seal) => seal !== undefined && isEncryptedContentLocked(seal)),
|
|
60
60
|
// Only attempt to unlock seals once
|
|
61
|
-
distinct((
|
|
61
|
+
distinct((seal) => seal.id),
|
|
62
62
|
// Get encrypted content from storage
|
|
63
|
-
mergeMap((
|
|
63
|
+
mergeMap((seal) =>
|
|
64
64
|
// Wait for storage to be available
|
|
65
|
-
storage$.pipe(switchMap((storage) => combineLatest([of(
|
|
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 ([
|
|
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
|
-
|
|
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]) => [
|
|
111
|
+
map(([gift, storage]) => [getGiftWrapSeal(gift), storage]),
|
|
108
112
|
// Make sure the seal is defined
|
|
109
|
-
filter(([
|
|
113
|
+
filter(([seal]) => seal !== undefined),
|
|
110
114
|
// Make sure seal is unlocked and not from cache
|
|
111
|
-
filter(([
|
|
115
|
+
filter(([seal]) => !isEncryptedContentLocked(seal) && !isEncryptedContentFromCache(seal)),
|
|
112
116
|
// Only persist the seal once
|
|
113
117
|
distinct(([seal]) => seal.id))
|
|
114
|
-
.subscribe(async ([
|
|
118
|
+
.subscribe(async ([seal, storage]) => {
|
|
115
119
|
if (!seal)
|
|
116
120
|
return;
|
|
117
121
|
try {
|
|
@@ -10,18 +10,26 @@ export interface EncryptedContentSigner {
|
|
|
10
10
|
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export declare function setEncryptedContentEncryptionMethod(kind: number, method: "nip04" | "nip44"): number;
|
|
17
|
-
/** Returns either nip04 or nip44 encryption methods depending on event kind */
|
|
18
|
-
export declare function getEncryptedContentEncryptionMethods(kind: number, signer: EncryptedContentSigner): {
|
|
13
|
+
export type EncryptionMethod = "nip04" | "nip44";
|
|
14
|
+
/** A pair of encryption methods for encrypting and decrypting event content */
|
|
15
|
+
export interface EncryptionMethods {
|
|
19
16
|
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
|
20
17
|
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
}
|
|
19
|
+
/** Various event kinds that can have encrypted content and which encryption method they use */
|
|
20
|
+
export declare const EventContentEncryptionMethod: Record<number, EncryptionMethod>;
|
|
21
|
+
/** Sets the encryption method that is used for the contents of a specific event kind */
|
|
22
|
+
export declare function setEncryptedContentEncryptionMethod(kind: number, method: EncryptionMethod): number;
|
|
23
|
+
/**
|
|
24
|
+
* Returns either nip04 or nip44 encryption methods depending on event kind
|
|
25
|
+
* @param kind The event kind to get the encryption method for
|
|
26
|
+
* @param signer The signer to use to get the encryption methods
|
|
27
|
+
* @param override The encryption method to use instead of the default
|
|
28
|
+
* @throws If the event kind does not support encrypted content
|
|
29
|
+
* @throws If the signer does not support the encryption method
|
|
30
|
+
* @returns The encryption methods for the event kind
|
|
31
|
+
*/
|
|
32
|
+
export declare function getEncryptedContentEncryptionMethods(kind: number, signer: EncryptedContentSigner, override?: EncryptionMethod): EncryptionMethods;
|
|
25
33
|
/** Checks if an event can have encrypted content */
|
|
26
34
|
export declare function canHaveEncryptedContent(kind: number): boolean;
|
|
27
35
|
/** Checks if an event has encrypted content */
|
|
@@ -13,9 +13,17 @@ export function setEncryptedContentEncryptionMethod(kind, method) {
|
|
|
13
13
|
EventContentEncryptionMethod[kind] = method;
|
|
14
14
|
return kind;
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Returns either nip04 or nip44 encryption methods depending on event kind
|
|
18
|
+
* @param kind The event kind to get the encryption method for
|
|
19
|
+
* @param signer The signer to use to get the encryption methods
|
|
20
|
+
* @param override The encryption method to use instead of the default
|
|
21
|
+
* @throws If the event kind does not support encrypted content
|
|
22
|
+
* @throws If the signer does not support the encryption method
|
|
23
|
+
* @returns The encryption methods for the event kind
|
|
24
|
+
*/
|
|
25
|
+
export function getEncryptedContentEncryptionMethods(kind, signer, override) {
|
|
26
|
+
const method = override ?? EventContentEncryptionMethod[kind];
|
|
19
27
|
if (!method)
|
|
20
28
|
throw new Error(`Event kind ${kind} does not support encrypted content`);
|
|
21
29
|
const encryption = signer[method];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
import { IEventStoreStreams } from "../event-store/interface.js";
|
|
3
|
+
/**
|
|
4
|
+
* Setups a process to write batches of new events from an event store to a cache
|
|
5
|
+
* @param eventStore - The event store to read from
|
|
6
|
+
* @param write - The function to write the events to the cache
|
|
7
|
+
* @param opts - The options for the process
|
|
8
|
+
* @param opts.batchTime - The time to wait before writing a batch (default: 5 seconds)
|
|
9
|
+
* @param opts.maxBatchSize - The maximum number of events to write in a batch
|
|
10
|
+
* @returns A function to stop the process
|
|
11
|
+
*/
|
|
12
|
+
export declare function presistEventsToCache(eventStore: IEventStoreStreams, write: (events: NostrEvent[]) => Promise<void>, opts?: {
|
|
13
|
+
maxBatchSize?: number;
|
|
14
|
+
batchTime?: number;
|
|
15
|
+
}): () => void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { bufferTime, filter } from "rxjs";
|
|
2
|
+
import { logger } from "../logger.js";
|
|
3
|
+
import { isFromCache } from "./index.js";
|
|
4
|
+
const log = logger.extend("event-cache");
|
|
5
|
+
/**
|
|
6
|
+
* Setups a process to write batches of new events from an event store to a cache
|
|
7
|
+
* @param eventStore - The event store to read from
|
|
8
|
+
* @param write - The function to write the events to the cache
|
|
9
|
+
* @param opts - The options for the process
|
|
10
|
+
* @param opts.batchTime - The time to wait before writing a batch (default: 5 seconds)
|
|
11
|
+
* @param opts.maxBatchSize - The maximum number of events to write in a batch
|
|
12
|
+
* @returns A function to stop the process
|
|
13
|
+
*/
|
|
14
|
+
export function presistEventsToCache(eventStore, write, opts) {
|
|
15
|
+
const time = opts?.batchTime ?? 5_000;
|
|
16
|
+
// Save all new events to the cache
|
|
17
|
+
const sub = eventStore.insert$
|
|
18
|
+
.pipe(
|
|
19
|
+
// Only select events that are not from the cache
|
|
20
|
+
filter((e) => !isFromCache(e)),
|
|
21
|
+
// Buffer events for 5 seconds
|
|
22
|
+
opts?.maxBatchSize ? bufferTime(time, undefined, opts?.maxBatchSize ?? 100) : bufferTime(time),
|
|
23
|
+
// Only select buffers with events
|
|
24
|
+
filter((b) => b.length > 0))
|
|
25
|
+
.subscribe((events) => {
|
|
26
|
+
// Save all new events to the cache
|
|
27
|
+
write(events)
|
|
28
|
+
.then(() => log(`Saved ${events.length} events to cache`))
|
|
29
|
+
.catch((e) => log(`Failed to save ${events.length} events to cache`, e));
|
|
30
|
+
});
|
|
31
|
+
return () => sub.unsubscribe();
|
|
32
|
+
}
|
package/dist/helpers/event.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export declare function getParentEventStore<T extends object>(event: T): IEventS
|
|
|
41
41
|
export declare function notifyEventUpdate(event: NostrEvent): void;
|
|
42
42
|
/**
|
|
43
43
|
* Returns the replaceable identifier for a replaceable event
|
|
44
|
-
* @throws
|
|
44
|
+
* @throws {Error} if the event is not addressable or missing the "d" tag
|
|
45
45
|
*/
|
|
46
46
|
export declare function getReplaceableIdentifier(event: NostrEvent): string;
|
|
47
47
|
/** Checks if an event is a NIP-70 protected event */
|
package/dist/helpers/event.js
CHANGED
|
@@ -87,7 +87,7 @@ export function notifyEventUpdate(event) {
|
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
89
|
* Returns the replaceable identifier for a replaceable event
|
|
90
|
-
* @throws
|
|
90
|
+
* @throws {Error} if the event is not addressable or missing the "d" tag
|
|
91
91
|
*/
|
|
92
92
|
export function getReplaceableIdentifier(event) {
|
|
93
93
|
if (!isAddressableKind(event.kind))
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { getOrComputeCachedValue } from "./cache.js";
|
|
2
2
|
import { unixNow } from "./time.js";
|
|
3
|
-
import { getTagValue } from "./event-tags.js";
|
|
4
3
|
export const ExpirationTimestampSymbol = Symbol("expiration-timestamp");
|
|
5
4
|
/** Returns the NIP-40 expiration timestamp for an event */
|
|
6
5
|
export function getExpirationTimestamp(event) {
|
|
7
6
|
return getOrComputeCachedValue(event, ExpirationTimestampSymbol, () => {
|
|
8
|
-
const expiration =
|
|
7
|
+
const expiration = event.tags.find((t) => t[0] === "expiration")?.[1];
|
|
9
8
|
return expiration ? parseInt(expiration) : undefined;
|
|
10
9
|
});
|
|
11
10
|
}
|
|
@@ -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
|
-
|
|
7
|
-
export declare const
|
|
8
|
-
/**
|
|
9
|
-
export declare
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
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 {
|
|
2
|
+
import { EventSet } from "../event-store/event-set.js";
|
|
3
3
|
import { getEncryptedContent, isEncryptedContentLocked, lockEncryptedContent, unlockEncryptedContent, } from "./encrypted-content.js";
|
|
4
|
-
import {
|
|
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
|
-
*
|
|
40
|
-
*
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 ||
|
|
138
|
+
if (!seal || isSealLocked(seal))
|
|
70
139
|
return true;
|
|
71
140
|
else
|
|
72
141
|
return false;
|
|
73
142
|
}
|
|
74
143
|
}
|
|
75
|
-
/**
|
|
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
|
-
//
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EncryptedContentSigner, getEncryptedContentEncryptionMethods } from "./encrypted-content.js";
|
|
1
|
+
import { EncryptedContentSigner, EncryptionMethod, getEncryptedContentEncryptionMethods } from "./encrypted-content.js";
|
|
2
2
|
export declare const HiddenContentSymbol: symbol;
|
|
3
3
|
export interface HiddenContentSigner extends EncryptedContentSigner {
|
|
4
4
|
}
|
|
@@ -6,7 +6,7 @@ export declare const getHiddenContentEncryptionMethods: typeof getEncryptedConte
|
|
|
6
6
|
/** Various event kinds that can have hidden content */
|
|
7
7
|
export declare const HiddenContentKinds: Set<number>;
|
|
8
8
|
/** Sets the encryption method for hidden content on a kind */
|
|
9
|
-
export declare function setHiddenContentEncryptionMethod(kind: number, method:
|
|
9
|
+
export declare function setHiddenContentEncryptionMethod(kind: number, method: EncryptionMethod): number;
|
|
10
10
|
/** Checks if an event can have hidden content */
|
|
11
11
|
export declare function canHaveHiddenContent(kind: number): boolean;
|
|
12
12
|
/** Checks if an event has hidden content */
|
|
@@ -31,7 +31,7 @@ export declare function unlockHiddenContent<T extends {
|
|
|
31
31
|
kind: number;
|
|
32
32
|
pubkey: string;
|
|
33
33
|
content: string;
|
|
34
|
-
}>(event: T, signer: EncryptedContentSigner): Promise<string>;
|
|
34
|
+
}>(event: T, signer: EncryptedContentSigner, override?: EncryptionMethod): Promise<string>;
|
|
35
35
|
/**
|
|
36
36
|
* Sets the hidden content on an event and updates it if its part of an event store
|
|
37
37
|
* @throws
|
|
@@ -34,10 +34,10 @@ export function getHiddenContent(event) {
|
|
|
34
34
|
* @param signer A signer to use to decrypt the content
|
|
35
35
|
* @throws
|
|
36
36
|
*/
|
|
37
|
-
export async function unlockHiddenContent(event, signer) {
|
|
37
|
+
export async function unlockHiddenContent(event, signer, override) {
|
|
38
38
|
if (!canHaveHiddenContent(event.kind))
|
|
39
39
|
throw new Error("Event kind does not support hidden content");
|
|
40
|
-
const encryption = getEncryptedContentEncryptionMethods(event.kind, signer);
|
|
40
|
+
const encryption = getEncryptedContentEncryptionMethods(event.kind, signer, override);
|
|
41
41
|
const plaintext = await encryption.decrypt(event.pubkey, event.content);
|
|
42
42
|
setHiddenContentCache(event, plaintext);
|
|
43
43
|
return plaintext;
|