-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathinject.ts
More file actions
176 lines (146 loc) · 6.45 KB
/
Copy pathinject.ts
File metadata and controls
176 lines (146 loc) · 6.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/**
* inject.ts — Page-context script exposing window.nostr (NIP-07) and window.webln
*
* Runs in MAIN world (page context). Cannot use ES module imports.
* All NIP-07 and WebLN methods are thin message-passing wrappers — no crypto happens here.
*
* @see https://github.com/nostr-protocol/nips/blob/master/07.md — NIP-07: window.nostr capability for web browsers
*/
export {}; // make this a module for declare global
interface PendingEntry {
resolve: (value: unknown) => void;
reject: (reason: Error) => void;
timeoutId: ReturnType<typeof setTimeout>;
}
interface NostrNip04 {
encrypt: (pubkey: string, plaintext: string) => Promise<string>;
decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
}
interface NostrNip44 {
encrypt: (pubkey: string, plaintext: string) => Promise<string>;
decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
}
interface SignedEvent {
id: string;
pubkey: string;
created_at: number;
kind: number;
tags: string[][];
content: string;
sig: string;
}
interface WebLNProvider {
enabled: boolean;
enable(): Promise<void>;
getInfo(): Promise<{ node: { alias: string; pubkey: string } }>;
sendPayment(paymentRequest: string): Promise<{ preimage: string }>;
makeInvoice(args: { amount: number; defaultMemo?: string }): Promise<{ paymentRequest: string }>;
getBalance(): Promise<{ balance: number }>;
}
interface NostrProvider {
getPublicKey: () => Promise<string>;
signEvent: (event: Record<string, unknown>) => Promise<SignedEvent>;
getRelays: () => Promise<Record<string, { read: boolean; write: boolean }>>;
nip04: NostrNip04;
nip44: NostrNip44;
}
declare global {
interface Window {
__nostrWotInjected?: boolean;
nostr: NostrProvider;
webln?: WebLNProvider;
}
}
(() => {
// Guard against double injection
if (window.__nostrWotInjected) return;
window.__nostrWotInjected = true;
// Crypto-random request ID generator (prevents response spoofing from page scripts)
function randomId(): string {
const buf = new Uint8Array(16);
crypto.getRandomValues(buf);
return Array.from(buf, b => b.toString(16).padStart(2, '0')).join('');
}
// ── Channel factory ──
// Eliminates duplication across NIP-07 and WebLN pending maps and call functions.
function createChannel(msgType: string, responseType: string, timeoutMs: number) {
const pending = new Map<string, PendingEntry>();
function call(method: string, params?: unknown): Promise<unknown> {
return new Promise((resolve, reject) => {
const id = randomId();
const timeoutId = setTimeout(() => {
pending.delete(id);
reject(new Error(`${msgType} timeout: ${method}`));
}, timeoutMs);
pending.set(id, { resolve, reject, timeoutId });
window.postMessage({ type: msgType, id, method, params }, window.location.origin);
});
}
function handleResponse(ev: MessageEvent): void {
if (ev.data?.type !== responseType) return;
const entry = pending.get(ev.data.id);
if (!entry) return;
clearTimeout(entry.timeoutId);
pending.delete(ev.data.id);
if (ev.data.error) entry.reject(new Error(ev.data.error));
else entry.resolve(ev.data.result);
}
return { call, handleResponse };
}
const nip07 = createChannel('NIP07_REQUEST', 'NIP07_RESPONSE', 120_000);
const webln = createChannel('WEBLN_REQUEST', 'WEBLN_RESPONSE', 120_000);
// Single message listener for all channels
window.addEventListener('message', async (event: MessageEvent) => {
if (event.source !== window) return;
// Route responses to the correct channel
nip07.handleResponse(event);
webln.handleResponse(event);
// Account changed notification from background
if (event.data?.type === 'NOSTR_ACCOUNT_CHANGED') {
window.dispatchEvent(new CustomEvent('nostr:accountChanged', {
detail: { pubkey: event.data.pubkey }
}));
return;
}
});
// ── Expose APIs ──
window.nostr = window.nostr || {} as NostrProvider;
// NIP-07 signer methods
window.nostr.getPublicKey = () => nip07.call('getPublicKey', {}) as Promise<string>;
window.nostr.signEvent = (event) => nip07.call('signEvent', { event }) as Promise<SignedEvent>;
window.nostr.getRelays = () => nip07.call('getRelays', {}) as Promise<Record<string, { read: boolean; write: boolean }>>;
window.nostr.nip04 = {
encrypt: (pubkey, plaintext) => nip07.call('nip04Encrypt', { pubkey, plaintext }) as Promise<string>,
decrypt: (pubkey, ciphertext) => nip07.call('nip04Decrypt', { pubkey, ciphertext }) as Promise<string>
};
window.nostr.nip44 = {
encrypt: (pubkey, plaintext) => nip07.call('nip44Encrypt', { pubkey, plaintext }) as Promise<string>,
decrypt: (pubkey, ciphertext) => nip07.call('nip44Decrypt', { pubkey, ciphertext }) as Promise<string>
};
// WebLN Lightning wallet API
let weblnEnabled = false;
window.webln = {
enabled: false,
async enable() {
await webln.call('enable', {});
weblnEnabled = true;
window.webln!.enabled = true;
},
getInfo: () => webln.call('getInfo', {}) as Promise<{ node: { alias: string; pubkey: string } }>,
sendPayment: (paymentRequest: string) => {
if (!weblnEnabled) return Promise.reject(new Error('WebLN not enabled. Call webln.enable() first.'));
return webln.call('sendPayment', { paymentRequest }) as Promise<{ preimage: string }>;
},
makeInvoice: (args: { amount: number; defaultMemo?: string }) => {
if (!weblnEnabled) return Promise.reject(new Error('WebLN not enabled. Call webln.enable() first.'));
return webln.call('makeInvoice', args) as Promise<{ paymentRequest: string }>;
},
getBalance: () => {
if (!weblnEnabled) return Promise.reject(new Error('WebLN not enabled. Call webln.enable() first.'));
return webln.call('getBalance', {}) as Promise<{ balance: number }>;
},
};
window.dispatchEvent(new CustomEvent('webln-ready'));
// Notify page that APIs are ready
window.dispatchEvent(new CustomEvent('nostr-wot-ready'));
})();