nostr-double-ratchet 0.0.37 → 0.0.48
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/README.md +52 -15
- package/dist/AppKeys.d.ts +52 -0
- package/dist/AppKeys.d.ts.map +1 -0
- package/dist/AppKeysManager.d.ts +136 -0
- package/dist/AppKeysManager.d.ts.map +1 -0
- package/dist/Invite.d.ts +7 -7
- package/dist/Invite.d.ts.map +1 -1
- package/dist/Session.d.ts +29 -0
- package/dist/Session.d.ts.map +1 -1
- package/dist/SessionManager.d.ts +46 -22
- package/dist/SessionManager.d.ts.map +1 -1
- package/dist/StorageAdapter.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/inviteUtils.d.ts +122 -0
- package/dist/inviteUtils.d.ts.map +1 -0
- package/dist/nostr-double-ratchet.es.js +2843 -1901
- package/dist/nostr-double-ratchet.umd.js +1 -1
- package/dist/types.d.ts +32 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +20 -2
- package/dist/utils.d.ts.map +1 -1
- package/package.json +5 -19
- package/src/AppKeys.ts +210 -0
- package/src/AppKeysManager.ts +405 -0
- package/src/Invite.ts +69 -89
- package/src/Session.ts +47 -4
- package/src/SessionManager.ts +478 -300
- package/src/StorageAdapter.ts +5 -6
- package/src/index.ts +4 -1
- package/src/inviteUtils.ts +271 -0
- package/src/types.ts +37 -2
- package/src/utils.ts +45 -8
- package/LICENSE +0 -21
- package/dist/UserRecord.d.ts +0 -117
- package/dist/UserRecord.d.ts.map +0 -1
- package/src/UserRecord.ts +0 -338
package/dist/types.d.ts
CHANGED
|
@@ -55,6 +55,14 @@ export type Unsubscribe = () => void;
|
|
|
55
55
|
export type NostrSubscribe = (_filter: Filter, _onEvent: (_e: VerifiedEvent) => void) => Unsubscribe;
|
|
56
56
|
export type EncryptFunction = (_plaintext: string, _pubkey: string) => Promise<string>;
|
|
57
57
|
export type DecryptFunction = (_ciphertext: string, _pubkey: string) => Promise<string>;
|
|
58
|
+
/**
|
|
59
|
+
* Identity key for cryptographic operations.
|
|
60
|
+
* Either a raw private key (Uint8Array) or encrypt/decrypt functions for extension login (NIP-07).
|
|
61
|
+
*/
|
|
62
|
+
export type IdentityKey = Uint8Array | {
|
|
63
|
+
encrypt: EncryptFunction;
|
|
64
|
+
decrypt: DecryptFunction;
|
|
65
|
+
};
|
|
58
66
|
export type NostrPublish = (_event: UnsignedEvent) => Promise<VerifiedEvent>;
|
|
59
67
|
export type Rumor = UnsignedEvent & {
|
|
60
68
|
id: string;
|
|
@@ -74,8 +82,8 @@ export declare const MESSAGE_EVENT_KIND = 1060;
|
|
|
74
82
|
*/
|
|
75
83
|
export declare const INVITE_EVENT_KIND = 30078;
|
|
76
84
|
export declare const INVITE_RESPONSE_KIND = 1059;
|
|
85
|
+
export declare const APP_KEYS_EVENT_KIND = 30078;
|
|
77
86
|
export declare const CHAT_MESSAGE_KIND = 14;
|
|
78
|
-
export declare const MAX_SKIP = 100;
|
|
79
87
|
export type NostrEvent = {
|
|
80
88
|
id: string;
|
|
81
89
|
pubkey: string;
|
|
@@ -85,4 +93,27 @@ export type NostrEvent = {
|
|
|
85
93
|
content: string;
|
|
86
94
|
sig: string;
|
|
87
95
|
};
|
|
96
|
+
/**
|
|
97
|
+
* Payload for reaction messages sent through NDR.
|
|
98
|
+
* Reactions are regular messages with a JSON payload indicating they're a reaction.
|
|
99
|
+
*/
|
|
100
|
+
export interface ReactionPayload {
|
|
101
|
+
type: 'reaction';
|
|
102
|
+
/** ID of the message being reacted to */
|
|
103
|
+
messageId: string;
|
|
104
|
+
/** Emoji or reaction content */
|
|
105
|
+
emoji: string;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Kind constant for reaction inner events
|
|
109
|
+
*/
|
|
110
|
+
export declare const REACTION_KIND = 7;
|
|
111
|
+
/**
|
|
112
|
+
* Kind constant for read/delivery receipt inner events
|
|
113
|
+
*/
|
|
114
|
+
export declare const RECEIPT_KIND = 15;
|
|
115
|
+
/**
|
|
116
|
+
* Kind constant for typing indicator inner events
|
|
117
|
+
*/
|
|
118
|
+
export declare const TYPING_KIND = 25;
|
|
88
119
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEnE,MAAM,MAAM,MAAM,GAAG;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;CACvB,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,UAAU,CAAC;CACxB,CAAA;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,iEAAiE;IACjE,OAAO,EAAE,UAAU,CAAC;IAEpB,iDAAiD;IACjD,0BAA0B,CAAC,EAAE,MAAM,CAAC;IAEpC,8CAA8C;IAC9C,uBAAuB,EAAE,MAAM,CAAC;IAEhC,sDAAsD;IACtD,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B,kGAAkG;IAClG,eAAe,EAAE,OAAO,CAAC;IAEzB,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,UAAU,CAAC;IAE/B,4DAA4D;IAC5D,eAAe,CAAC,EAAE,UAAU,CAAC;IAE7B,uDAAuD;IACvD,yBAAyB,EAAE,MAAM,CAAC;IAElC,6DAA6D;IAC7D,2BAA2B,EAAE,MAAM,CAAC;IAEpC,wDAAwD;IACxD,gCAAgC,EAAE,MAAM,CAAC;IAEzC,wEAAwE;IACxE,WAAW,EAAE;QACX,CAAC,MAAM,EAAE,MAAM,GAAG;YAChB,UAAU,EAAE,UAAU,EAAE,CAAC;YACzB,WAAW,EAAE;gBAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;aAAC,CAAA;SAC9C,CAAC;KACH,CAAC;CACH;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAErC;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,aAAa,KAAK,IAAI,KAAK,WAAW,CAAC;AACrG,MAAM,MAAM,eAAe,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AACvF,MAAM,MAAM,eAAe,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
|
-
import { Rumor, SessionState } from "./types";
|
|
1
|
+
import { Rumor, SessionState, ReactionPayload } from "./types";
|
|
2
2
|
import { Session } from "./Session.ts";
|
|
3
3
|
export declare function serializeSessionState(state: SessionState): string;
|
|
4
4
|
export declare function deserializeSessionState(data: string): SessionState;
|
|
5
5
|
export declare function deepCopyState(s: SessionState): SessionState;
|
|
6
6
|
export declare function createEventStream(session: Session): AsyncGenerator<Rumor, void, unknown>;
|
|
7
7
|
export declare function kdf(input1: Uint8Array, input2?: Uint8Array, numOutputs?: number): Uint8Array[];
|
|
8
|
-
export declare function skippedMessageIndexKey(_nostrSender: string, _number: number): string;
|
|
9
8
|
export declare function getMillisecondTimestamp(event: Rumor): number;
|
|
9
|
+
/**
|
|
10
|
+
* Check if a message content is a reaction payload.
|
|
11
|
+
* @param content The message content to check
|
|
12
|
+
* @returns The parsed ReactionPayload if valid, null otherwise
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseReaction(content: string): ReactionPayload | null;
|
|
15
|
+
/**
|
|
16
|
+
* Check if a message content is a reaction.
|
|
17
|
+
* @param content The message content to check
|
|
18
|
+
* @returns true if the content is a reaction payload
|
|
19
|
+
*/
|
|
20
|
+
export declare function isReaction(content: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Create a reaction payload JSON string.
|
|
23
|
+
* @param messageId The ID of the message being reacted to
|
|
24
|
+
* @param emoji The emoji or reaction content
|
|
25
|
+
* @returns JSON string of the reaction payload
|
|
26
|
+
*/
|
|
27
|
+
export declare function createReactionPayload(messageId: string, emoji: string): string;
|
|
10
28
|
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAMvC,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAkCjE;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAuElE;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,YAAY,GAAG,YAAY,CAgC3D;AAGD,wBAAuB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CA0B/F;AAED,wBAAgB,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,GAAE,UAA+B,EAAE,UAAU,GAAE,MAAU,GAAG,UAAU,EAAE,CAQrH;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,KAAK,UAMnD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAUrE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAO9E"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nostr-double-ratchet",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.48",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"packageManager": "
|
|
5
|
+
"packageManager": "pnpm@9.15.0",
|
|
6
6
|
"description": "Nostr double ratchet library",
|
|
7
7
|
"main": "dist/nostr-double-ratchet.umd.js",
|
|
8
8
|
"module": "dist/nostr-double-ratchet.es.js",
|
|
@@ -12,29 +12,19 @@
|
|
|
12
12
|
"test:once": "vitest run --exclude '**/*.integration.test.ts'",
|
|
13
13
|
"test:integration": "DEBUG='ndk:*' vitest '**/*.integration.test.ts'",
|
|
14
14
|
"build": "vite build && tsc",
|
|
15
|
-
"examples": "cd examples && vite build && tsc",
|
|
16
|
-
"examples-dev": "cd examples && vite && tsc",
|
|
17
|
-
"docs": "typedoc --out docs src/index.ts",
|
|
18
15
|
"lint": "eslint src --ext .ts --fix"
|
|
19
16
|
},
|
|
20
|
-
"repository":
|
|
21
|
-
"type": "git",
|
|
22
|
-
"url": "git+https://github.com/mmalmi/nostr-double-ratchet.git"
|
|
23
|
-
},
|
|
17
|
+
"repository": "https://files.iris.to/#/npub1xndmdgymsf4a34rzr7346vp8qcptxf75pjqweh8naa8rklgxpfqqmfjtce/nostr-double-ratchet",
|
|
24
18
|
"author": "Martti Malmi",
|
|
25
19
|
"license": "MIT",
|
|
26
|
-
"
|
|
27
|
-
"url": "https://github.com/mmalmi/nostr-double-ratchet/issues"
|
|
28
|
-
},
|
|
29
|
-
"homepage": "https://github.com/mmalmi/nostr-double-ratchet",
|
|
30
|
-
"documentation": "https://mmalmi.github.io/nostr-double-ratchet/",
|
|
20
|
+
"homepage": "https://files.iris.to/#/npub1xndmdgymsf4a34rzr7346vp8qcptxf75pjqweh8naa8rklgxpfqqmfjtce/nostr-double-ratchet",
|
|
31
21
|
"files": [
|
|
32
22
|
"src",
|
|
33
23
|
"dist"
|
|
34
24
|
],
|
|
35
25
|
"devDependencies": {
|
|
26
|
+
"@noble/hashes": "^1.3.1",
|
|
36
27
|
"@nostr-dev-kit/ndk": "2.14.23",
|
|
37
|
-
"@types/lodash": "^4.17.17",
|
|
38
28
|
"@types/node": "^22.15.21",
|
|
39
29
|
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
|
40
30
|
"@typescript-eslint/parser": "^8.33.1",
|
|
@@ -42,12 +32,8 @@
|
|
|
42
32
|
"eslint-config-prettier": "^10.1.5",
|
|
43
33
|
"eslint-plugin-prettier": "^5.4.0",
|
|
44
34
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
45
|
-
"lodash": "^4.17.21",
|
|
46
|
-
"react-blurhash": "^0.3.0",
|
|
47
35
|
"tsx": "^4.19.4",
|
|
48
|
-
"typedoc": "^0.28.4",
|
|
49
36
|
"typescript": "^5.8.3",
|
|
50
|
-
"typescript-lru-cache": "^2.0.0",
|
|
51
37
|
"vite": "^6.3.5",
|
|
52
38
|
"vitest": "^3.1.4",
|
|
53
39
|
"ws": "^8.18.2"
|
package/src/AppKeys.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { VerifiedEvent, UnsignedEvent, verifyEvent } from "nostr-tools"
|
|
2
|
+
import { APP_KEYS_EVENT_KIND, NostrSubscribe, Unsubscribe } from "./types"
|
|
3
|
+
|
|
4
|
+
const now = () => Math.round(Date.now() / 1000)
|
|
5
|
+
|
|
6
|
+
// Simplified tag format: ["device", identityPubkey, createdAt]
|
|
7
|
+
type DeviceTag = [
|
|
8
|
+
type: "device",
|
|
9
|
+
identityPubkey: string,
|
|
10
|
+
createdAt: string,
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
const isDeviceTag = (tag: string[]): tag is DeviceTag =>
|
|
14
|
+
tag.length >= 3 &&
|
|
15
|
+
tag[0] === "device" &&
|
|
16
|
+
typeof tag[1] === "string" &&
|
|
17
|
+
typeof tag[2] === "string"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Device identity entry - contains only identity information.
|
|
21
|
+
* identityPubkey serves as the device identifier.
|
|
22
|
+
* Invite crypto material (ephemeral keys, shared secret) is in separate Invite events.
|
|
23
|
+
*/
|
|
24
|
+
export interface DeviceEntry {
|
|
25
|
+
/** Identity public key - also serves as device identifier */
|
|
26
|
+
identityPubkey: string
|
|
27
|
+
createdAt: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Manages a consolidated list of device invites (kind 30078, d-tag "double-ratchet/app-keys").
|
|
32
|
+
* Single atomic event containing all device invites for a user.
|
|
33
|
+
* Uses union merge strategy for conflict resolution.
|
|
34
|
+
*
|
|
35
|
+
* Note: ownerPublicKey is not stored - it's passed to getEvent() when publishing,
|
|
36
|
+
* and NDK's signer sets the correct pubkey during signing anyway.
|
|
37
|
+
*/
|
|
38
|
+
export class AppKeys {
|
|
39
|
+
private devices: Map<string, DeviceEntry> = new Map()
|
|
40
|
+
|
|
41
|
+
constructor(devices: DeviceEntry[] = []) {
|
|
42
|
+
devices.forEach((device) => this.devices.set(device.identityPubkey, device))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates a new device identity entry.
|
|
47
|
+
* Note: This only creates the identity entry. The device must separately
|
|
48
|
+
* create and publish its own Invite event with ephemeral keys.
|
|
49
|
+
*/
|
|
50
|
+
createDeviceEntry(identityPubkey: string): DeviceEntry {
|
|
51
|
+
return {
|
|
52
|
+
identityPubkey,
|
|
53
|
+
createdAt: now(),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
addDevice(device: DeviceEntry): void {
|
|
58
|
+
if (!this.devices.has(device.identityPubkey)) {
|
|
59
|
+
this.devices.set(device.identityPubkey, device)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
removeDevice(identityPubkey: string): void {
|
|
64
|
+
this.devices.delete(identityPubkey)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getDevice(identityPubkey: string): DeviceEntry | undefined {
|
|
68
|
+
return this.devices.get(identityPubkey)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getAllDevices(): DeviceEntry[] {
|
|
72
|
+
return Array.from(this.devices.values())
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getEvent(): UnsignedEvent {
|
|
76
|
+
const deviceTags = this.getAllDevices().map((device) => [
|
|
77
|
+
"device",
|
|
78
|
+
device.identityPubkey,
|
|
79
|
+
String(device.createdAt),
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
kind: APP_KEYS_EVENT_KIND,
|
|
84
|
+
pubkey: "", // Signer will set this
|
|
85
|
+
content: "",
|
|
86
|
+
created_at: now(),
|
|
87
|
+
tags: [
|
|
88
|
+
["d", "double-ratchet/app-keys"],
|
|
89
|
+
["version", "1"],
|
|
90
|
+
...deviceTags,
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static fromEvent(event: VerifiedEvent): AppKeys {
|
|
96
|
+
if (!event.sig) {
|
|
97
|
+
throw new Error("Event is not signed")
|
|
98
|
+
}
|
|
99
|
+
if (!verifyEvent(event)) {
|
|
100
|
+
throw new Error("Event signature is invalid")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Simplified tag format: ["device", identityPubkey, createdAt]
|
|
104
|
+
// Note: "removed" tags are ignored for backwards compatibility with old events
|
|
105
|
+
const devices = event.tags
|
|
106
|
+
.filter(isDeviceTag)
|
|
107
|
+
.map(([, identityPubkey, createdAt]) => ({
|
|
108
|
+
identityPubkey,
|
|
109
|
+
createdAt: parseInt(createdAt, 10) || event.created_at,
|
|
110
|
+
}))
|
|
111
|
+
|
|
112
|
+
return new AppKeys(devices)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
serialize(): string {
|
|
116
|
+
return JSON.stringify({
|
|
117
|
+
devices: this.getAllDevices(),
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
static deserialize(json: string): AppKeys {
|
|
122
|
+
const data = JSON.parse(json) as {
|
|
123
|
+
devices: DeviceEntry[]
|
|
124
|
+
}
|
|
125
|
+
return new AppKeys(data.devices)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
merge(other: AppKeys): AppKeys {
|
|
129
|
+
// Merge devices, preferring the one with earlier createdAt for same identityPubkey
|
|
130
|
+
const mergedDevices = [...this.devices.values(), ...other.devices.values()]
|
|
131
|
+
.reduce((map, device) => {
|
|
132
|
+
const existing = map.get(device.identityPubkey)
|
|
133
|
+
if (!existing || device.createdAt < existing.createdAt) {
|
|
134
|
+
map.set(device.identityPubkey, device)
|
|
135
|
+
}
|
|
136
|
+
return map
|
|
137
|
+
}, new Map<string, DeviceEntry>())
|
|
138
|
+
|
|
139
|
+
return new AppKeys(Array.from(mergedDevices.values()))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Subscribe to AppKeys events from a user.
|
|
144
|
+
* Similar to Invite.fromUser pattern.
|
|
145
|
+
*/
|
|
146
|
+
static fromUser(
|
|
147
|
+
user: string,
|
|
148
|
+
subscribe: NostrSubscribe,
|
|
149
|
+
onAppKeysList: (appKeys: AppKeys) => void
|
|
150
|
+
): Unsubscribe {
|
|
151
|
+
return subscribe(
|
|
152
|
+
{
|
|
153
|
+
kinds: [APP_KEYS_EVENT_KIND],
|
|
154
|
+
authors: [user],
|
|
155
|
+
"#d": ["double-ratchet/app-keys"],
|
|
156
|
+
},
|
|
157
|
+
(event) => {
|
|
158
|
+
if (event.pubkey !== user) return
|
|
159
|
+
try {
|
|
160
|
+
const appKeys = AppKeys.fromEvent(event)
|
|
161
|
+
onAppKeysList(appKeys)
|
|
162
|
+
} catch {
|
|
163
|
+
// Invalid event
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Wait for AppKeys from a user with timeout.
|
|
171
|
+
* Returns the most recent AppKeys received within the timeout, or null.
|
|
172
|
+
* Note: Uses the most recent event by created_at, not merging, since
|
|
173
|
+
* device revocation is determined by absence from the list.
|
|
174
|
+
*/
|
|
175
|
+
static waitFor(
|
|
176
|
+
user: string,
|
|
177
|
+
subscribe: NostrSubscribe,
|
|
178
|
+
timeoutMs = 500
|
|
179
|
+
): Promise<AppKeys | null> {
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
let latest: { list: AppKeys; createdAt: number } | null = null
|
|
182
|
+
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
unsubscribe()
|
|
185
|
+
resolve(latest?.list ?? null)
|
|
186
|
+
}, timeoutMs)
|
|
187
|
+
|
|
188
|
+
const unsubscribe = subscribe(
|
|
189
|
+
{
|
|
190
|
+
kinds: [APP_KEYS_EVENT_KIND],
|
|
191
|
+
authors: [user],
|
|
192
|
+
"#d": ["double-ratchet/app-keys"],
|
|
193
|
+
},
|
|
194
|
+
(event) => {
|
|
195
|
+
if (event.pubkey !== user) return
|
|
196
|
+
try {
|
|
197
|
+
const list = AppKeys.fromEvent(event)
|
|
198
|
+
// Use >= to prefer later-delivered events when timestamps are equal
|
|
199
|
+
// This handles replaceable events created within the same second
|
|
200
|
+
if (!latest || event.created_at >= latest.createdAt) {
|
|
201
|
+
latest = { list, createdAt: event.created_at }
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
// Invalid event
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
}
|