@konemono/nostr-login 1.7.11

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.
Files changed (43) hide show
  1. package/.prettierrc.json +13 -0
  2. package/README.md +167 -0
  3. package/dist/const/index.d.ts +1 -0
  4. package/dist/iife-module.d.ts +1 -0
  5. package/dist/index.d.ts +27 -0
  6. package/dist/index.esm.js +18 -0
  7. package/dist/index.esm.js.map +1 -0
  8. package/dist/modules/AuthNostrService.d.ts +84 -0
  9. package/dist/modules/BannerManager.d.ts +20 -0
  10. package/dist/modules/ModalManager.d.ts +25 -0
  11. package/dist/modules/Nip46.d.ts +56 -0
  12. package/dist/modules/Nostr.d.ts +34 -0
  13. package/dist/modules/NostrExtensionService.d.ts +17 -0
  14. package/dist/modules/NostrParams.d.ts +8 -0
  15. package/dist/modules/Popup.d.ts +7 -0
  16. package/dist/modules/ProcessManager.d.ts +10 -0
  17. package/dist/modules/Signer.d.ts +9 -0
  18. package/dist/modules/index.d.ts +8 -0
  19. package/dist/types.d.ts +72 -0
  20. package/dist/unpkg.js +17 -0
  21. package/dist/utils/index.d.ts +27 -0
  22. package/dist/utils/nip44.d.ts +9 -0
  23. package/index.html +30 -0
  24. package/package.json +28 -0
  25. package/rollup.config.js +55 -0
  26. package/src/const/index.ts +1 -0
  27. package/src/iife-module.ts +81 -0
  28. package/src/index.ts +347 -0
  29. package/src/modules/AuthNostrService.ts +756 -0
  30. package/src/modules/BannerManager.ts +146 -0
  31. package/src/modules/ModalManager.ts +635 -0
  32. package/src/modules/Nip46.ts +441 -0
  33. package/src/modules/Nostr.ts +107 -0
  34. package/src/modules/NostrExtensionService.ts +99 -0
  35. package/src/modules/NostrParams.ts +18 -0
  36. package/src/modules/Popup.ts +27 -0
  37. package/src/modules/ProcessManager.ts +67 -0
  38. package/src/modules/Signer.ts +25 -0
  39. package/src/modules/index.ts +8 -0
  40. package/src/types.ts +124 -0
  41. package/src/utils/index.ts +326 -0
  42. package/src/utils/nip44.ts +185 -0
  43. package/tsconfig.json +15 -0
@@ -0,0 +1,67 @@
1
+ import { EventEmitter } from 'tseep';
2
+ import { CALL_TIMEOUT } from '../const';
3
+
4
+ class ProcessManager extends EventEmitter {
5
+ private callCount: number = 0;
6
+ private callTimer: NodeJS.Timeout | undefined;
7
+
8
+ constructor() {
9
+ super();
10
+ }
11
+
12
+ public onAuthUrl() {
13
+ if (Boolean(this.callTimer)) {
14
+ clearTimeout(this.callTimer);
15
+ }
16
+ }
17
+
18
+ public onIframeUrl() {
19
+ if (Boolean(this.callTimer)) {
20
+ clearTimeout(this.callTimer);
21
+ }
22
+ }
23
+
24
+ public async wait<T>(cb: () => Promise<T>): Promise<T> {
25
+ // FIXME only allow 1 parallel req
26
+
27
+ if (!this.callTimer) {
28
+ this.callTimer = setTimeout(() => this.emit('onCallTimeout'), CALL_TIMEOUT);
29
+ }
30
+
31
+ if (!this.callCount) {
32
+ this.emit('onCallStart');
33
+ }
34
+
35
+ this.callCount++;
36
+
37
+ let error;
38
+ let result;
39
+
40
+ try {
41
+ result = await cb();
42
+ } catch (e) {
43
+ error = e;
44
+ }
45
+
46
+ this.callCount--;
47
+
48
+ this.emit('onCallEnd');
49
+
50
+ if (this.callTimer) {
51
+ clearTimeout(this.callTimer);
52
+ }
53
+
54
+ this.callTimer = undefined;
55
+
56
+ if (error) {
57
+ throw error;
58
+ }
59
+
60
+ // we can't return undefined bcs an exception is
61
+ // thrown above on error
62
+ // @ts-ignore
63
+ return result;
64
+ }
65
+ }
66
+
67
+ export default ProcessManager;
@@ -0,0 +1,25 @@
1
+ import { NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk';
2
+ import { Nip44 } from '../utils/nip44';
3
+ import { getPublicKey } from 'nostr-tools';
4
+
5
+ export class PrivateKeySigner extends NDKPrivateKeySigner {
6
+ private nip44: Nip44 = new Nip44();
7
+ private _pubkey: string;
8
+
9
+ constructor(privateKey: string) {
10
+ super(privateKey);
11
+ this._pubkey = getPublicKey(privateKey);
12
+ }
13
+
14
+ get pubkey() {
15
+ return this._pubkey;
16
+ }
17
+
18
+ encryptNip44(recipient: NDKUser, value: string): Promise<string> {
19
+ return Promise.resolve(this.nip44.encrypt(this.privateKey!, recipient.pubkey, value));
20
+ }
21
+
22
+ decryptNip44(sender: NDKUser, value: string): Promise<string> {
23
+ return Promise.resolve(this.nip44.decrypt(this.privateKey!, sender.pubkey, value));
24
+ }
25
+ }
@@ -0,0 +1,8 @@
1
+ export { default as BannerManager } from './BannerManager';
2
+ export { default as AuthNostrService } from './AuthNostrService';
3
+ export { default as ModalManager } from './ModalManager';
4
+ export { default as Nostr } from './Nostr';
5
+ export { default as NostrExtensionService } from './NostrExtensionService';
6
+ export { default as NostrParams } from './NostrParams';
7
+ export { default as Popup } from './Popup';
8
+ export { default as ProcessManager } from './ProcessManager';
package/src/types.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { Info, AuthMethod, ConnectionString, RecentType, BannerNotify } from 'nostr-login-components';
2
+
3
+ export interface NostrLoginAuthOptions {
4
+ localNsec?: string;
5
+ relays?: string[];
6
+ type: 'login' | 'signup' | 'logout';
7
+ method?: AuthMethod;
8
+ pubkey?: string;
9
+ otpData?: string;
10
+ name?: string;
11
+ }
12
+
13
+ // NOTE: must be a subset of CURRENT_MODULE enum
14
+ export type StartScreens =
15
+ | 'welcome'
16
+ | 'welcome-login'
17
+ | 'welcome-signup'
18
+ | 'signup'
19
+ | 'local-signup'
20
+ | 'login'
21
+ | 'otp'
22
+ | 'connect'
23
+ | 'login-bunker-url'
24
+ | 'login-read-only'
25
+ | 'connection-string'
26
+ | 'switch-account'
27
+ | 'import';
28
+
29
+ export interface NostrLoginOptions {
30
+ // optional
31
+ theme?: string;
32
+ startScreen?: StartScreens;
33
+ bunkers?: string;
34
+ onAuth?: (npub: string, options: NostrLoginAuthOptions) => void;
35
+ perms?: string;
36
+ darkMode?: boolean;
37
+
38
+ // do not show the banner, modals must be `launch`-ed
39
+ noBanner?: boolean;
40
+
41
+ // forward reqs to this bunker origin for testing
42
+ devOverrideBunkerOrigin?: string;
43
+
44
+ // deprecated, use methods=['local']
45
+ // use local signup instead of nostr connect
46
+ localSignup?: boolean;
47
+
48
+ // allowed auth methods
49
+ methods?: AuthMethod[];
50
+
51
+ // otp endpoints
52
+ otpRequestUrl?: string;
53
+ otpReplyUrl?: string;
54
+
55
+ // welcome screen's title/desc
56
+ title?: string;
57
+ description?: string;
58
+
59
+ // comma-separated list of relays added
60
+ // to relay list of new profiles created with local signup
61
+ signupRelays?: string;
62
+
63
+ // relay list to override hardcoded `OUTBOX_RELAYS` constant
64
+ outboxRelays?: string[];
65
+
66
+ // dev mode
67
+ dev?: boolean;
68
+
69
+ // use start.njump.me instead of local signup
70
+ signupNstart?: boolean;
71
+
72
+ // list of npubs to auto/suggest-follow on signup
73
+ followNpubs?: string;
74
+
75
+ // when method call auth needed, instead of showing
76
+ // the modal, we start waiting for incoming nip46
77
+ // connection and send the nostrconnect string using
78
+ // nlNeedAuth event
79
+ customNostrConnect?: boolean;
80
+ }
81
+
82
+ export interface IBanner {
83
+ userInfo?: Info | null;
84
+ titleBanner?: string;
85
+ isLoading?: boolean;
86
+ listNotifies?: string[];
87
+ accounts?: Info[];
88
+ isOpen?: boolean;
89
+ darkMode?: boolean;
90
+ notify?: BannerNotify;
91
+ }
92
+
93
+ export type TypeBanner = IBanner & HTMLElement;
94
+
95
+ export interface IModal {
96
+ authUrl?: string;
97
+ iframeUrl?: string;
98
+ isLoading?: boolean;
99
+ isOTP?: boolean;
100
+ isLoadingExtension?: boolean;
101
+ localSignup?: boolean;
102
+ signupNjump?: boolean;
103
+ njumpIframe?: string;
104
+ authMethods?: AuthMethod[];
105
+ hasExtension?: boolean;
106
+ hasOTP?: boolean;
107
+ error?: string;
108
+ signupNameIsAvailable?: string | boolean;
109
+ loginIsGood?: string | boolean;
110
+ recents?: RecentType[];
111
+ accounts?: Info[];
112
+ darkMode?: boolean;
113
+ welcomeTitle?: string;
114
+ welcomeDescription?: string;
115
+ connectionString?: string;
116
+ connectionStringServices?: ConnectionString[];
117
+ }
118
+
119
+ export type TypeModal = IModal & HTMLElement;
120
+
121
+ export interface Response {
122
+ result?: string;
123
+ error?: string;
124
+ }
@@ -0,0 +1,326 @@
1
+ import { Info, RecentType } from 'nostr-login-components';
2
+ import NDK, { NDKEvent, NDKRelaySet, NDKSigner, NDKUser } from '@nostr-dev-kit/ndk';
3
+ import { generatePrivateKey } from 'nostr-tools';
4
+ import { NostrLoginOptions } from '../types';
5
+
6
+ const LOCAL_STORE_KEY = '__nostrlogin_nip46';
7
+ const LOGGED_IN_ACCOUNTS = '__nostrlogin_accounts';
8
+ const RECENT_ACCOUNTS = '__nostrlogin_recent';
9
+ const OUTBOX_RELAYS = ['wss://purplepag.es', 'wss://relay.nos.social', 'wss://user.kindpag.es', 'wss://relay.damus.io', 'wss://nos.lol'];
10
+ const DEFAULT_SIGNUP_RELAYS = ['wss://relay.damus.io/', 'wss://nos.lol/', 'wss://relay.primal.net/'];
11
+
12
+ export const localStorageSetItem = (key: string, value: string) => {
13
+ localStorage.setItem(key, value);
14
+ };
15
+
16
+ export const localStorageGetItem = (key: string) => {
17
+ const value = window.localStorage.getItem(key);
18
+
19
+ if (value) {
20
+ try {
21
+ return JSON.parse(value);
22
+ } catch {}
23
+ }
24
+
25
+ return null;
26
+ };
27
+
28
+ export const localStorageRemoveItem = (key: string) => {
29
+ localStorage.removeItem(key);
30
+ };
31
+
32
+ export const fetchProfile = async (info: Info, profileNdk: NDK) => {
33
+ const user = new NDKUser({ pubkey: info.pubkey });
34
+
35
+ user.ndk = profileNdk;
36
+
37
+ return await user.fetchProfile();
38
+ };
39
+
40
+ export const prepareSignupRelays = (signupRelays?: string) => {
41
+ const relays = (signupRelays || '')
42
+ .split(',')
43
+ .map(r => r.trim())
44
+ .filter(r => r.startsWith('ws'));
45
+ if (!relays.length) relays.push(...DEFAULT_SIGNUP_RELAYS);
46
+ return relays;
47
+ };
48
+
49
+ export const createProfile = async (info: Info, profileNdk: NDK, signer: NDKSigner, signupRelays?: string, outboxRelays?: string[]) => {
50
+ const meta = {
51
+ name: info.name,
52
+ };
53
+
54
+ const profileEvent = new NDKEvent(profileNdk, {
55
+ kind: 0,
56
+ created_at: Math.floor(Date.now() / 1000),
57
+ pubkey: info.pubkey,
58
+ content: JSON.stringify(meta),
59
+ tags: [],
60
+ });
61
+ if (window.location.hostname) profileEvent.tags.push(['client', window.location.hostname]);
62
+
63
+ const relaysEvent = new NDKEvent(profileNdk, {
64
+ kind: 10002,
65
+ created_at: Math.floor(Date.now() / 1000),
66
+ pubkey: info.pubkey,
67
+ content: '',
68
+ tags: [],
69
+ });
70
+
71
+ const relays = prepareSignupRelays(signupRelays)
72
+ for (const r of relays) {
73
+ relaysEvent.tags.push(['r', r]);
74
+ }
75
+
76
+ await profileEvent.sign(signer);
77
+ console.log('signed profile', profileEvent);
78
+ await relaysEvent.sign(signer);
79
+ console.log('signed relays', relaysEvent);
80
+
81
+ const outboxRelaysFinal = outboxRelays && outboxRelays.length ? outboxRelays : OUTBOX_RELAYS;
82
+
83
+ await profileEvent.publish(NDKRelaySet.fromRelayUrls(outboxRelaysFinal, profileNdk));
84
+ console.log('published profile', profileEvent);
85
+ await relaysEvent.publish(NDKRelaySet.fromRelayUrls(outboxRelaysFinal, profileNdk));
86
+ console.log('published relays', relaysEvent);
87
+ };
88
+
89
+ export const bunkerUrlToInfo = (bunkerUrl: string, sk = ''): Info => {
90
+ const url = new URL(bunkerUrl);
91
+
92
+ return {
93
+ pubkey: '',
94
+ signerPubkey: url.hostname || url.pathname.split('//')[1],
95
+ sk: sk || generatePrivateKey(),
96
+ relays: url.searchParams.getAll('relay'),
97
+ token: url.searchParams.get('secret') || '',
98
+ authMethod: 'connect',
99
+ };
100
+ };
101
+
102
+ export const isBunkerUrl = (value: string) => value.startsWith('bunker://');
103
+
104
+ export const getBunkerUrl = async (value: string, optionsModal: NostrLoginOptions) => {
105
+ if (!value) {
106
+ return '';
107
+ }
108
+
109
+ if (isBunkerUrl(value)) {
110
+ return value;
111
+ }
112
+
113
+ if (value.includes('@')) {
114
+ const [name, domain] = value.toLocaleLowerCase().split('@');
115
+ const origin = optionsModal.devOverrideBunkerOrigin || `https://${domain}`;
116
+ const bunkerUrl = `${origin}/.well-known/nostr.json?name=_`;
117
+ const userUrl = `${origin}/.well-known/nostr.json?name=${name}`;
118
+ const bunker = await fetch(bunkerUrl);
119
+ const bunkerData = await bunker.json();
120
+ const bunkerPubkey = bunkerData.names['_'];
121
+ const bunkerRelays = bunkerData.nip46[bunkerPubkey];
122
+ const user = await fetch(userUrl);
123
+ const userData = await user.json();
124
+ const userPubkey = userData.names[name];
125
+ // console.log({
126
+ // bunkerData, userData, bunkerPubkey, bunkerRelays, userPubkey,
127
+ // name, domain, origin
128
+ // })
129
+ if (!bunkerRelays.length) {
130
+ throw new Error('Bunker relay not provided');
131
+ }
132
+
133
+ return `bunker://${userPubkey}?relay=${bunkerRelays[0]}`;
134
+ }
135
+
136
+ throw new Error('Invalid user name or bunker url');
137
+ };
138
+
139
+ export const checkNip05 = async (nip05: string) => {
140
+ let available = false;
141
+ let error = '';
142
+ let pubkey = '';
143
+ await (async () => {
144
+ if (!nip05 || !nip05.includes('@')) return;
145
+
146
+ const [name, domain] = nip05.toLocaleLowerCase().split('@');
147
+ if (!name) return;
148
+
149
+ const REGEXP = new RegExp(/^[\w-.]+@([\w-]+\.)+[\w-]{2,8}$/g);
150
+ if (!REGEXP.test(nip05)) {
151
+ error = 'Invalid name';
152
+ return;
153
+ }
154
+
155
+ if (!domain) {
156
+ error = 'Select service';
157
+ return;
158
+ }
159
+
160
+ const url = `https://${domain}/.well-known/nostr.json?name=${name.toLowerCase()}`;
161
+ try {
162
+ const r = await fetch(url);
163
+ const d = await r.json();
164
+ if (d.names[name]) {
165
+ pubkey = d.names[name];
166
+ return;
167
+ }
168
+ } catch {}
169
+
170
+ available = true;
171
+ })();
172
+
173
+ return {
174
+ available,
175
+ taken: pubkey != '',
176
+ error,
177
+ pubkey,
178
+ };
179
+ };
180
+
181
+ const upgradeInfo = (info: Info | RecentType) => {
182
+ if ('typeAuthMethod' in info) delete info['typeAuthMethod'];
183
+
184
+ if (!info.authMethod) {
185
+ if ('extension' in info && info['extension']) info.authMethod = 'extension';
186
+ else if ('readOnly' in info && info['readOnly']) info.authMethod = 'readOnly';
187
+ else info.authMethod = 'connect';
188
+ }
189
+
190
+ if (info.nip05 && isBunkerUrl(info.nip05)) {
191
+ info.bunkerUrl = info.nip05;
192
+ info.nip05 = '';
193
+ }
194
+
195
+ if (info.authMethod === 'connect' && !info.signerPubkey) {
196
+ info.signerPubkey = info.pubkey;
197
+ }
198
+ };
199
+
200
+ export const localStorageAddAccount = (info: Info) => {
201
+ // make current
202
+ localStorageSetItem(LOCAL_STORE_KEY, JSON.stringify(info));
203
+
204
+ const loggedInAccounts: Info[] = localStorageGetItem(LOGGED_IN_ACCOUNTS) || [];
205
+ const recentAccounts: RecentType[] = localStorageGetItem(RECENT_ACCOUNTS) || [];
206
+
207
+ // upgrade first
208
+ loggedInAccounts.forEach(a => upgradeInfo(a));
209
+ recentAccounts.forEach(a => upgradeInfo(a));
210
+
211
+ // upsert new info into accounts
212
+ const accounts: Info[] = loggedInAccounts;
213
+ const index = loggedInAccounts.findIndex((el: Info) => el.pubkey === info.pubkey && el.authMethod === info.authMethod);
214
+ if (index !== -1) {
215
+ accounts[index] = info;
216
+ } else {
217
+ accounts.push(info);
218
+ }
219
+
220
+ // remove new info from recent
221
+ const recents = recentAccounts.filter(el => el.pubkey !== info.pubkey || el.authMethod !== info.authMethod);
222
+
223
+ localStorageSetItem(RECENT_ACCOUNTS, JSON.stringify(recents));
224
+ localStorageSetItem(LOGGED_IN_ACCOUNTS, JSON.stringify(accounts));
225
+ };
226
+
227
+ export const localStorageRemoveCurrentAccount = () => {
228
+ const user: Info = localStorageGetItem(LOCAL_STORE_KEY);
229
+ if (!user) return;
230
+
231
+ // make sure it's valid
232
+ upgradeInfo(user);
233
+
234
+ // remove secret fields
235
+ const recentUser: RecentType = { ...user };
236
+
237
+ // make sure session keys are dropped
238
+ // @ts-ignore
239
+ delete recentUser['sk'];
240
+ // @ts-ignore
241
+ delete recentUser['otpData'];
242
+
243
+ // get accounts and recent
244
+ const loggedInAccounts: Info[] = localStorageGetItem(LOGGED_IN_ACCOUNTS) || [];
245
+ const recentsAccounts: RecentType[] = localStorageGetItem(RECENT_ACCOUNTS) || [];
246
+
247
+ // upgrade first
248
+ loggedInAccounts.forEach(a => upgradeInfo(a));
249
+ recentsAccounts.forEach(a => upgradeInfo(a));
250
+
251
+ const recents: RecentType[] = recentsAccounts;
252
+ if (recentUser.authMethod === 'connect' && recentUser.bunkerUrl && recentUser.bunkerUrl.includes('secret=')) {
253
+ console.log('nostr login bunker conn with a secret not saved to recent');
254
+ } else if (recentUser.authMethod === 'local') {
255
+ console.log('nostr login temporary local keys not save to recent');
256
+ } else {
257
+ // upsert to recent
258
+ const index = recentsAccounts.findIndex((el: RecentType) => el.pubkey === recentUser.pubkey && el.authMethod === recentUser.authMethod);
259
+ if (index !== -1) {
260
+ recents[index] = recentUser;
261
+ } else {
262
+ recents.push(recentUser);
263
+ }
264
+ }
265
+
266
+ // remove from accounts
267
+ const accounts = loggedInAccounts.filter(el => el.pubkey !== user.pubkey || el.authMethod !== user.authMethod);
268
+
269
+ // update accounts and recent, clear current
270
+ localStorageSetItem(RECENT_ACCOUNTS, JSON.stringify(recents));
271
+ localStorageSetItem(LOGGED_IN_ACCOUNTS, JSON.stringify(accounts));
272
+ localStorageRemoveItem(LOCAL_STORE_KEY);
273
+ };
274
+
275
+ export const localStorageRemoveRecent = (user: RecentType) => {
276
+ const recentsAccounts: RecentType[] = localStorageGetItem(RECENT_ACCOUNTS) || [];
277
+ recentsAccounts.forEach(a => upgradeInfo(a));
278
+ const recents = recentsAccounts.filter(el => el.pubkey !== user.pubkey || el.authMethod !== user.authMethod);
279
+ localStorageSetItem(RECENT_ACCOUNTS, JSON.stringify(recents));
280
+ };
281
+
282
+ export const localStorageGetRecents = (): RecentType[] => {
283
+ const recents: RecentType[] = localStorageGetItem(RECENT_ACCOUNTS) || [];
284
+ recents.forEach(r => upgradeInfo(r));
285
+ return recents;
286
+ };
287
+
288
+ export const localStorageGetAccounts = (): Info[] => {
289
+ const accounts: Info[] = localStorageGetItem(LOGGED_IN_ACCOUNTS) || [];
290
+ accounts.forEach(a => upgradeInfo(a));
291
+ return accounts;
292
+ };
293
+
294
+ export const localStorageGetCurrent = (): Info | null => {
295
+ const info = localStorageGetItem(LOCAL_STORE_KEY);
296
+ if (info) upgradeInfo(info);
297
+ return info;
298
+ };
299
+
300
+ export const setDarkMode = (dark: boolean) => {
301
+ localStorageSetItem('nl-dark-mode', dark ? 'true' : 'false');
302
+ };
303
+
304
+ export const getDarkMode = (opt: NostrLoginOptions) => {
305
+ const getDarkModeLocal = localStorage.getItem('nl-dark-mode');
306
+
307
+ if (getDarkModeLocal) {
308
+ // user already changed it
309
+ return Boolean(JSON.parse(getDarkModeLocal));
310
+ } else if (opt.darkMode !== undefined) {
311
+ // app provided an option
312
+ return opt.darkMode;
313
+ } else {
314
+ // auto-detect
315
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
316
+ return true;
317
+ } else {
318
+ return false;
319
+ }
320
+ }
321
+ };
322
+
323
+ export const getIcon = async () => {
324
+ // FIXME look at meta tags or manifest
325
+ return document.location.origin + '/favicon.ico';
326
+ };