divine-signer 0.1.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/LICENSE ADDED
@@ -0,0 +1,189 @@
1
+ Mozilla Public License Version 2.0
2
+ ==================================
3
+
4
+ 1. Definitions
5
+ --------------
6
+
7
+ 1.1. "Contributor"
8
+ means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software.
9
+
10
+ 1.2. "Contributor Version"
11
+ means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution.
12
+
13
+ 1.3. "Contribution"
14
+ means Covered Software of a particular Contributor.
15
+
16
+ 1.4. "Covered Software"
17
+ means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof.
18
+
19
+ 1.5. "Incompatible With Secondary Licenses"
20
+ means
21
+ (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or
22
+ (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License.
23
+
24
+ 1.6. "Executable Form"
25
+ means any form of the work other than Source Code Form.
26
+
27
+ 1.7. "Larger Work"
28
+ means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software.
29
+
30
+ 1.8. "License"
31
+ means this document.
32
+
33
+ 1.9. "Licensable"
34
+ means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License.
35
+
36
+ 1.10. "Modifications"
37
+ means any of the following:
38
+ (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or
39
+ (b) any new file in Source Code Form that contains any Covered Software.
40
+
41
+ 1.11. "Patent Claims" of a Contributor
42
+ means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version.
43
+
44
+ 1.12. "Secondary License"
45
+ means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses.
46
+
47
+ 1.13. "Source Code Form"
48
+ means the form of the work preferred for making modifications.
49
+
50
+ 1.14. "You" (or "Your")
51
+ means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.
52
+
53
+ 2. License Grants and Conditions
54
+ --------------------------------
55
+
56
+ 2.1. Grants
57
+
58
+ Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:
59
+
60
+ (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and
61
+
62
+ (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version.
63
+
64
+ 2.2. Effective Date
65
+
66
+ The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution.
67
+
68
+ 2.3. Limitations on Grant Scope
69
+
70
+ The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor:
71
+
72
+ (a) for any code that a Contributor has removed from Covered Software; or
73
+
74
+ (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or
75
+
76
+ (c) under Patent Claims infringed by Covered Software in the absence of its Contributions.
77
+
78
+ This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4).
79
+
80
+ 2.4. Subsequent Licenses
81
+
82
+ No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3).
83
+
84
+ 2.5. Representation
85
+
86
+ Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License.
87
+
88
+ 2.6. Fair Use
89
+
90
+ This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents.
91
+
92
+ 2.7. Conditions
93
+
94
+ Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1.
95
+
96
+ 3. Responsibilities
97
+ -------------------
98
+
99
+ 3.1. Distribution of Source Form
100
+
101
+ All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form.
102
+
103
+ 3.2. Distribution of Executable Form
104
+
105
+ If You distribute Covered Software in Executable Form then:
106
+
107
+ (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and
108
+
109
+ (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License.
110
+
111
+ 3.3. Distribution of a Larger Work
112
+
113
+ You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s).
114
+
115
+ 3.4. Notices
116
+
117
+ You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies.
118
+
119
+ 3.5. Application of Additional Terms
120
+
121
+ You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction.
122
+
123
+ 4. Inability to Comply Due to Statute or Regulation
124
+ ---------------------------------------------------
125
+
126
+ If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it.
127
+
128
+ 5. Termination
129
+ -------------
130
+
131
+ 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice.
132
+
133
+ 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.
134
+
135
+ 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination.
136
+
137
+ 6. Disclaimer of Warranty
138
+ -------------------------
139
+
140
+ Covered Software is provided under this License on an "as is" basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer.
141
+
142
+ 7. Limitation of Liability
143
+ ---------------------------
144
+
145
+ Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party's negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You.
146
+
147
+ 8. Litigation
148
+ -------------
149
+
150
+ Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims.
151
+
152
+ 9. Miscellaneous
153
+ ----------------
154
+
155
+ This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor.
156
+
157
+ 10. Versions of the License
158
+ ---------------------------
159
+
160
+ 10.1. New Versions
161
+
162
+ Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number.
163
+
164
+ 10.2. Effect of New Versions
165
+
166
+ You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward.
167
+
168
+ 10.3. Modified Versions
169
+
170
+ If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License).
171
+
172
+ 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
173
+
174
+ If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached.
175
+
176
+ Exhibit A - Source Code Form License Notice
177
+ -------------------------------------------
178
+
179
+ This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
180
+
181
+ If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice.
182
+
183
+ You may add additional accurate notices of copyright ownership.
184
+
185
+ Exhibit B - "Incompatible With Secondary Licenses" Notice
186
+ ----------------------------------------------------------
187
+
188
+ This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0.
189
+
package/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # divine-signer
2
+
3
+ A headless Nostr signer library that gives web apps five authentication paths through one interface. No UI, no framework lock-in — just a `NostrSigner` interface your app programs against while users pick how they want to sign.
4
+
5
+ ## Auth methods
6
+
7
+ | Method | Class | How it works |
8
+ |--------|-------|-------------|
9
+ | **nsec paste** | `NsecSigner` | User pastes a secret key. Signs and encrypts locally via nostr-tools. Simple but security-sensitive. |
10
+ | **NIP-07 extension** | `ExtensionSigner` | Delegates to browser extensions (Alby, nos2x, Soapbox Signer). Keys never leave the extension. |
11
+ | **NIP-46 bunker** | `BunkerNIP44Signer` | Connects to a remote signer via `bunker://` URL over WebSocket relays. |
12
+ | **NIP-46 nostrconnect** | `BunkerNIP44Signer` | QR code flow — user scans with a mobile signer app (Amber, Primal, nsec.app). |
13
+ | **diVine OAuth** | `KeycastHttpSigner` | Email/password login via [diVine](https://divine.video). Signs over HTTP with PKCE, token refresh, and rate-limit retry. |
14
+
15
+ All five implement `NostrSigner`:
16
+
17
+ ```typescript
18
+ interface NostrSigner {
19
+ type: SignerType;
20
+ getPublicKey(): Promise<string>;
21
+ signEvent(event: EventTemplate): Promise<VerifiedEvent>;
22
+ nip04Encrypt(pubkey: string, plaintext: string): Promise<string>;
23
+ nip04Decrypt(pubkey: string, ciphertext: string): Promise<string>;
24
+ nip44Encrypt(pubkey: string, plaintext: string): Promise<string>;
25
+ nip44Decrypt(pubkey: string, ciphertext: string): Promise<string>;
26
+ }
27
+ ```
28
+
29
+ Your app codes against this interface. The user's choice of auth method is invisible to the rest of your stack.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ npm install divine-signer
35
+ ```
36
+
37
+ Or via CDN (no bundler needed):
38
+
39
+ ```html
40
+ <script type="module">
41
+ import { ExtensionSigner, buildOAuthUrl } from "https://esm.sh/divine-signer";
42
+ </script>
43
+ ```
44
+
45
+ Requires `nostr-tools ^2.23.0` as a peer dependency.
46
+
47
+ ## Quick start
48
+
49
+ ### Direct signer usage
50
+
51
+ ```typescript
52
+ import { NsecSigner, ExtensionSigner, BunkerNIP44Signer } from 'divine-signer';
53
+
54
+ // nsec
55
+ const signer = new NsecSigner('nsec1...');
56
+
57
+ // Browser extension
58
+ const signer = new ExtensionSigner();
59
+
60
+ // Bunker URL
61
+ const signer = await BunkerNIP44Signer.fromBunkerUrl('bunker://...');
62
+
63
+ // Then use it — same API regardless of method
64
+ const pubkey = await signer.getPublicKey();
65
+ const signed = await signer.signEvent({ kind: 1, content: 'hello', tags: [], created_at: now });
66
+ const encrypted = await signer.nip44Encrypt(recipientPubkey, 'secret');
67
+ ```
68
+
69
+ ### OAuth flow (diVine)
70
+
71
+ OAuth requires two steps: redirect out, then handle the callback.
72
+
73
+ First, define your storage adapter (tells the library where to persist PKCE state):
74
+
75
+ ```typescript
76
+ import type { OAuthStorage, OAuthConfig } from 'divine-signer';
77
+
78
+ const oauthStorage: OAuthStorage = {
79
+ savePkceState: (s) => localStorage.setItem('my_oauth', JSON.stringify(s)),
80
+ loadPkceState: () => { try { return JSON.parse(localStorage.getItem('my_oauth')!); } catch { return null; } },
81
+ clearPkceState: () => localStorage.removeItem('my_oauth'),
82
+ saveAuthorizationHandle: (h) => localStorage.setItem('my_auth_handle', h),
83
+ loadAuthorizationHandle: () => localStorage.getItem('my_auth_handle'),
84
+ clearAuthorizationHandle: () => localStorage.removeItem('my_auth_handle'),
85
+ };
86
+
87
+ const oauthConfig: OAuthConfig = {
88
+ clientId: 'my-app',
89
+ redirectUri: `${window.location.origin}/auth/callback`,
90
+ storage: oauthStorage,
91
+ };
92
+ ```
93
+
94
+ Start the flow (login page):
95
+
96
+ ```typescript
97
+ import { buildOAuthUrl } from 'divine-signer';
98
+
99
+ const url = await buildOAuthUrl(oauthConfig);
100
+ window.location.href = url; // redirect to diVine
101
+ ```
102
+
103
+ Handle the callback (`/auth/callback` route):
104
+
105
+ ```typescript
106
+ import { exchangeCode } from 'divine-signer';
107
+
108
+ const params = new URLSearchParams(window.location.search);
109
+ const { signer, accessToken, refreshToken } = await exchangeCode(
110
+ params.get('code')!,
111
+ params.get('state')!,
112
+ oauthConfig,
113
+ );
114
+ // signer is a KeycastHttpSigner — use it like any other NostrSigner
115
+ ```
116
+
117
+ ### Session persistence
118
+
119
+ Save and restore sessions across page reloads:
120
+
121
+ ```typescript
122
+ import { createSessionStore, restoreSession } from 'divine-signer';
123
+
124
+ // Create a store backed by localStorage (or any storage with getItem/setItem/removeItem)
125
+ const sessions = createSessionStore(localStorage, 'my_app');
126
+
127
+ // After login, save the session
128
+ sessions.save({ type: 'keycast', accessToken, refreshToken });
129
+ // or: sessions.save({ type: 'extension' });
130
+ // or: sessions.save({ type: 'bunker', bunkerUrl: '...' });
131
+ // or: sessions.save({ type: 'nsec', nsec: '...' });
132
+
133
+ // On page load, restore it
134
+ const stored = sessions.load();
135
+ if (stored) {
136
+ const signer = await restoreSession(stored);
137
+ // signer is ready to use
138
+ }
139
+ ```
140
+
141
+ ### Token refresh (Keycast)
142
+
143
+ The `KeycastHttpSigner` handles token refresh automatically. Hook into it to persist new tokens:
144
+
145
+ ```typescript
146
+ import { KeycastHttpSigner } from 'divine-signer';
147
+
148
+ if (signer instanceof KeycastHttpSigner) {
149
+ signer.onTokenRefresh = ({ accessToken, refreshToken }) => {
150
+ sessions.save({ type: 'keycast', accessToken, refreshToken });
151
+ };
152
+ }
153
+ ```
154
+
155
+ ## API reference
156
+
157
+ ### Signers
158
+
159
+ - `NsecSigner(nsec: string)` — local signing from a secret key
160
+ - `ExtensionSigner()` — delegates to `window.nostr` (NIP-07)
161
+ - `BunkerNIP44Signer.fromBunkerUrl(input, params?, overrideType?, timeout?)` — connect via bunker URL
162
+ - `BunkerNIP44Signer.reconnect(clientSecretKey, bunkerUrl, params?, timeout?)` — restore a bunker session
163
+ - `BunkerNIP44Signer.fromNostrConnect(uri, clientSecretKey, params?, timeoutOrAbort?)` — QR code connect flow
164
+ - `KeycastHttpSigner(token, options?)` — HTTP signing via Keycast API
165
+ - `KeycastAuthError` — thrown on 401/403 (check `error.status`)
166
+
167
+ ### OAuth
168
+
169
+ - `buildOAuthUrl(config, options?)` — returns authorize URL string (caller navigates)
170
+ - `exchangeCode(code, state, config)` — exchanges auth code for `OAuthResult { signer, accessToken, refreshToken? }`
171
+
172
+ ### Session
173
+
174
+ - `createSessionStore(storage, prefix)` — returns `{ save, load, clear }`
175
+ - `restoreSession(stored)` — reconstructs a `NostrSigner` from a `StoredSession`
176
+
177
+ ### Types
178
+
179
+ - `NostrSigner` — the signer interface all methods implement
180
+ - `SignerType` — `'nsec' | 'extension' | 'bunker' | 'nostrconnect' | 'keycast'`
181
+ - `StoredSession` — discriminated union of all persistable session shapes
182
+ - `OAuthStorage` — interface for PKCE state persistence
183
+ - `OAuthConfig` — `{ clientId, redirectUri, apiUrl?, scope?, storage, fetchImpl? }`
184
+ - `TokenRefreshResult` — `{ accessToken, refreshToken }`
185
+
186
+ ## Example
187
+
188
+ - [`examples/vanilla/`](examples/vanilla/) — minimal single-page app wiring all five auth methods, no framework, no CSS, just the API.
189
+ - [privdm](https://github.com/dcadenas/privdm) — a full React app (NIP-17 encrypted DMs) using divine-signer in production.
190
+
191
+ ## Size
192
+
193
+ ~9KB minified / ~3.4KB gzipped (excluding the nostr-tools peer dependency).
194
+
195
+ ## License
196
+
197
+ [MPL-2.0](LICENSE)
@@ -0,0 +1,31 @@
1
+ import { type BunkerSignerParams } from 'nostr-tools/nip46';
2
+ import type { EventTemplate, VerifiedEvent } from 'nostr-tools/pure';
3
+ import type { NostrSigner, SignerType } from './types';
4
+ export declare class BunkerNIP44Signer implements NostrSigner {
5
+ readonly type: SignerType;
6
+ private readonly inner;
7
+ private constructor();
8
+ /** Connect via a bunker:// URL or NIP-05 identifier. */
9
+ static fromBunkerUrl(input: string, params?: BunkerSignerParams, overrideType?: SignerType, connectTimeout?: number): Promise<BunkerNIP44Signer>;
10
+ /** Reconnect to a bunker using a stored client key and bunker URL (session restore). */
11
+ static reconnect(clientSecretKey: Uint8Array, bunkerUrl: string, params?: BunkerSignerParams, connectTimeout?: number): Promise<BunkerNIP44Signer>;
12
+ /**
13
+ * Connect via a nostrconnect:// URI (QR code flow).
14
+ *
15
+ * Implements the connect handshake manually instead of using
16
+ * BunkerSigner.fromURI, which sends a `switch_relays` RPC that
17
+ * signers like Primal don't understand (causing a parse error and
18
+ * potentially disrupting the session). After the handshake we create
19
+ * the signer via BunkerSigner.fromBunker which skips switch_relays.
20
+ */
21
+ static fromNostrConnect(connectionURI: string, clientSecretKey: Uint8Array, params?: BunkerSignerParams, timeoutOrAbort?: number | AbortSignal): Promise<BunkerNIP44Signer>;
22
+ getPublicKey(): Promise<string>;
23
+ signEvent(event: EventTemplate): Promise<VerifiedEvent>;
24
+ nip04Encrypt(pubkey: string, plaintext: string): Promise<string>;
25
+ nip04Decrypt(pubkey: string, ciphertext: string): Promise<string>;
26
+ nip44Encrypt(pubkey: string, plaintext: string): Promise<string>;
27
+ nip44Decrypt(pubkey: string, ciphertext: string): Promise<string>;
28
+ getBunkerUrl(): string;
29
+ close(): Promise<void>;
30
+ }
31
+ //# sourceMappingURL=bunker-signer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bunker-signer.d.ts","sourceRoot":"","sources":["../src/bunker-signer.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAI3B,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEvD,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IAErC,OAAO;IAKP,wDAAwD;WAC3C,aAAa,CACxB,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,kBAAkB,EAC3B,YAAY,CAAC,EAAE,UAAU,EACzB,cAAc,SAAS,GACtB,OAAO,CAAC,iBAAiB,CAAC;IAgB7B,wFAAwF;WAC3E,SAAS,CACpB,eAAe,EAAE,UAAU,EAC3B,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,kBAAkB,EAC3B,cAAc,SAAS,GACtB,OAAO,CAAC,iBAAiB,CAAC;IAe7B;;;;;;;;OAQG;WACU,gBAAgB,CAC3B,aAAa,EAAE,MAAM,EACrB,eAAe,EAAE,UAAU,EAC3B,MAAM,CAAC,EAAE,kBAAkB,EAC3B,cAAc,CAAC,EAAE,MAAM,GAAG,WAAW,GACpC,OAAO,CAAC,iBAAiB,CAAC;IAgFvB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAW/B,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;IAIvD,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIjE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIvE,YAAY,IAAI,MAAM;IAIhB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
@@ -0,0 +1,30 @@
1
+ import type { EventTemplate, VerifiedEvent } from 'nostr-tools/pure';
2
+ import type { NostrSigner } from './types';
3
+ interface WindowNostrCrypto {
4
+ encrypt(pubkey: string, plaintext: string): Promise<string>;
5
+ decrypt(pubkey: string, ciphertext: string): Promise<string>;
6
+ }
7
+ interface WindowNostr {
8
+ getPublicKey(): Promise<string>;
9
+ signEvent(event: EventTemplate): Promise<VerifiedEvent>;
10
+ nip04?: WindowNostrCrypto;
11
+ nip44?: WindowNostrCrypto;
12
+ }
13
+ declare global {
14
+ interface Window {
15
+ nostr?: WindowNostr;
16
+ }
17
+ }
18
+ export declare class ExtensionSigner implements NostrSigner {
19
+ readonly type: "extension";
20
+ private readonly nostr;
21
+ constructor();
22
+ getPublicKey(): Promise<string>;
23
+ signEvent(event: EventTemplate): Promise<VerifiedEvent>;
24
+ nip04Encrypt(pubkey: string, plaintext: string): Promise<string>;
25
+ nip04Decrypt(pubkey: string, ciphertext: string): Promise<string>;
26
+ nip44Encrypt(pubkey: string, plaintext: string): Promise<string>;
27
+ nip44Decrypt(pubkey: string, ciphertext: string): Promise<string>;
28
+ }
29
+ export {};
30
+ //# sourceMappingURL=extension-signer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension-signer.d.ts","sourceRoot":"","sources":["../src/extension-signer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,UAAU,iBAAiB;IACzB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5D,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9D;AAED,UAAU,WAAW;IACnB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAChC,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACxD,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,KAAK,CAAC,EAAE,iBAAiB,CAAC;CAC3B;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,KAAK,CAAC,EAAE,WAAW,CAAC;KACrB;CACF;AAED,qBAAa,eAAgB,YAAW,WAAW;IACjD,QAAQ,CAAC,IAAI,cAAwB;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;;IAS9B,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAI/B,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;IAIvD,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOhE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOjE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOhE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAMxE"}
@@ -0,0 +1,11 @@
1
+ export type { NostrSigner, SignerType } from './types';
2
+ export type { StoredSession, SessionStore, SessionStorage } from './session';
3
+ export type { OAuthStorage, OAuthConfig, OAuthResult } from './oauth';
4
+ export type { TokenRefreshResult } from './keycast-http-signer';
5
+ export { NsecSigner } from './nsec-signer';
6
+ export { ExtensionSigner } from './extension-signer';
7
+ export { BunkerNIP44Signer } from './bunker-signer';
8
+ export { KeycastHttpSigner, KeycastAuthError } from './keycast-http-signer';
9
+ export { createSessionStore, restoreSession } from './session';
10
+ export { buildOAuthUrl, exchangeCode } from './oauth';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC7E,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtE,YAAY,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAGhE,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG5E,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG/D,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,533 @@
1
+ // src/nsec-signer.ts
2
+ import { nip19 } from "nostr-tools";
3
+ import {
4
+ getPublicKey,
5
+ finalizeEvent
6
+ } from "nostr-tools/pure";
7
+ import * as nip04 from "nostr-tools/nip04";
8
+ import * as nip44 from "nostr-tools/nip44";
9
+ var NsecSigner = class {
10
+ constructor(nsec) {
11
+ this.type = "nsec";
12
+ const decoded = nip19.decode(nsec);
13
+ if (decoded.type !== "nsec") {
14
+ throw new Error(`Expected nsec, got ${decoded.type}`);
15
+ }
16
+ this.secretKey = decoded.data;
17
+ }
18
+ async getPublicKey() {
19
+ return getPublicKey(this.secretKey);
20
+ }
21
+ async signEvent(event) {
22
+ return finalizeEvent(event, this.secretKey);
23
+ }
24
+ async nip04Encrypt(pubkey, plaintext) {
25
+ return nip04.encrypt(this.secretKey, pubkey, plaintext);
26
+ }
27
+ async nip04Decrypt(pubkey, ciphertext) {
28
+ return nip04.decrypt(this.secretKey, pubkey, ciphertext);
29
+ }
30
+ async nip44Encrypt(pubkey, plaintext) {
31
+ const convKey = nip44.v2.utils.getConversationKey(this.secretKey, pubkey);
32
+ return nip44.v2.encrypt(plaintext, convKey);
33
+ }
34
+ async nip44Decrypt(pubkey, ciphertext) {
35
+ const convKey = nip44.v2.utils.getConversationKey(this.secretKey, pubkey);
36
+ return nip44.v2.decrypt(ciphertext, convKey);
37
+ }
38
+ };
39
+
40
+ // src/extension-signer.ts
41
+ var ExtensionSigner = class {
42
+ constructor() {
43
+ this.type = "extension";
44
+ if (!window.nostr) {
45
+ throw new Error("No NIP-07 extension found (window.nostr is undefined)");
46
+ }
47
+ this.nostr = window.nostr;
48
+ }
49
+ async getPublicKey() {
50
+ return this.nostr.getPublicKey();
51
+ }
52
+ async signEvent(event) {
53
+ return this.nostr.signEvent(event);
54
+ }
55
+ async nip04Encrypt(pubkey, plaintext) {
56
+ if (!this.nostr.nip04) {
57
+ throw new Error("Extension does not support NIP-04 encryption");
58
+ }
59
+ return this.nostr.nip04.encrypt(pubkey, plaintext);
60
+ }
61
+ async nip04Decrypt(pubkey, ciphertext) {
62
+ if (!this.nostr.nip04) {
63
+ throw new Error("Extension does not support NIP-04 decryption");
64
+ }
65
+ return this.nostr.nip04.decrypt(pubkey, ciphertext);
66
+ }
67
+ async nip44Encrypt(pubkey, plaintext) {
68
+ if (!this.nostr.nip44) {
69
+ throw new Error("Extension does not support NIP-44 encryption");
70
+ }
71
+ return this.nostr.nip44.encrypt(pubkey, plaintext);
72
+ }
73
+ async nip44Decrypt(pubkey, ciphertext) {
74
+ if (!this.nostr.nip44) {
75
+ throw new Error("Extension does not support NIP-44 decryption");
76
+ }
77
+ return this.nostr.nip44.decrypt(pubkey, ciphertext);
78
+ }
79
+ };
80
+
81
+ // src/bunker-signer.ts
82
+ import {
83
+ BunkerSigner,
84
+ parseBunkerInput,
85
+ toBunkerURL
86
+ } from "nostr-tools/nip46";
87
+ import { SimplePool } from "nostr-tools";
88
+ import { getConversationKey, decrypt as decrypt2 } from "nostr-tools/nip44";
89
+ import { generateSecretKey, getPublicKey as getPublicKey2 } from "nostr-tools/pure";
90
+ var BunkerNIP44Signer = class _BunkerNIP44Signer {
91
+ constructor(inner, type) {
92
+ this.inner = inner;
93
+ this.type = type;
94
+ }
95
+ /** Connect via a bunker:// URL or NIP-05 identifier. */
96
+ static async fromBunkerUrl(input, params, overrideType, connectTimeout = 3e4) {
97
+ const bp = await parseBunkerInput(input);
98
+ if (!bp) {
99
+ throw new Error(`Invalid bunker input: ${input}`);
100
+ }
101
+ const clientKey = generateSecretKey();
102
+ const inner = BunkerSigner.fromBunker(clientKey, bp, params);
103
+ await Promise.race([
104
+ inner.connect(),
105
+ new Promise(
106
+ (_, reject) => setTimeout(() => reject(new Error("Bunker connection timed out")), connectTimeout)
107
+ )
108
+ ]);
109
+ return new _BunkerNIP44Signer(inner, overrideType ?? "bunker");
110
+ }
111
+ /** Reconnect to a bunker using a stored client key and bunker URL (session restore). */
112
+ static async reconnect(clientSecretKey, bunkerUrl, params, connectTimeout = 3e4) {
113
+ const bp = await parseBunkerInput(bunkerUrl);
114
+ if (!bp) {
115
+ throw new Error(`Invalid bunker URL: ${bunkerUrl}`);
116
+ }
117
+ const inner = BunkerSigner.fromBunker(clientSecretKey, bp, params);
118
+ await Promise.race([
119
+ inner.connect(),
120
+ new Promise(
121
+ (_, reject) => setTimeout(() => reject(new Error("Bunker connection timed out")), connectTimeout)
122
+ )
123
+ ]);
124
+ return new _BunkerNIP44Signer(inner, "nostrconnect");
125
+ }
126
+ /**
127
+ * Connect via a nostrconnect:// URI (QR code flow).
128
+ *
129
+ * Implements the connect handshake manually instead of using
130
+ * BunkerSigner.fromURI, which sends a `switch_relays` RPC that
131
+ * signers like Primal don't understand (causing a parse error and
132
+ * potentially disrupting the session). After the handshake we create
133
+ * the signer via BunkerSigner.fromBunker which skips switch_relays.
134
+ */
135
+ static async fromNostrConnect(connectionURI, clientSecretKey, params, timeoutOrAbort) {
136
+ const uri = new URL(connectionURI);
137
+ const relays = uri.searchParams.getAll("relay");
138
+ const secret = uri.searchParams.get("secret");
139
+ const clientPubkey = getPublicKey2(clientSecretKey);
140
+ if (relays.length === 0) {
141
+ throw new Error("No relays specified in nostrconnect URI");
142
+ }
143
+ const abort = typeof timeoutOrAbort === "number" ? AbortSignal.timeout(timeoutOrAbort) : timeoutOrAbort;
144
+ const pool = new SimplePool();
145
+ const signerPubkey = await new Promise((resolve, reject) => {
146
+ if (abort?.aborted) {
147
+ reject(new Error("Aborted"));
148
+ return;
149
+ }
150
+ let settled = false;
151
+ const sub = pool.subscribe(
152
+ relays,
153
+ { kinds: [24133], "#p": [clientPubkey], limit: 0 },
154
+ {
155
+ onevent: async (event) => {
156
+ try {
157
+ const convKey = getConversationKey(clientSecretKey, event.pubkey);
158
+ const decrypted = decrypt2(event.content, convKey);
159
+ const response = JSON.parse(decrypted);
160
+ if (response.result === secret) {
161
+ settled = true;
162
+ sub.close();
163
+ resolve(event.pubkey);
164
+ }
165
+ } catch {
166
+ }
167
+ },
168
+ onclose: () => {
169
+ if (!settled) {
170
+ reject(
171
+ new Error(
172
+ "Subscription closed before connection was established"
173
+ )
174
+ );
175
+ }
176
+ },
177
+ abort
178
+ }
179
+ );
180
+ abort?.addEventListener(
181
+ "abort",
182
+ () => {
183
+ if (!settled) {
184
+ settled = true;
185
+ sub.close();
186
+ reject(new Error("Connection timed out"));
187
+ }
188
+ },
189
+ { once: true }
190
+ );
191
+ });
192
+ const bp = { pubkey: signerPubkey, relays, secret: secret || "" };
193
+ const inner = BunkerSigner.fromBunker(clientSecretKey, bp, {
194
+ ...params,
195
+ pool
196
+ });
197
+ return new _BunkerNIP44Signer(inner, "nostrconnect");
198
+ }
199
+ async getPublicKey() {
200
+ return Promise.race([
201
+ this.inner.getPublicKey(),
202
+ new Promise(
203
+ (_, reject) => setTimeout(() => reject(new Error("get_public_key timed out after 20s")), 2e4)
204
+ )
205
+ ]);
206
+ }
207
+ async signEvent(event) {
208
+ return this.inner.signEvent(event);
209
+ }
210
+ async nip04Encrypt(pubkey, plaintext) {
211
+ return this.inner.nip04Encrypt(pubkey, plaintext);
212
+ }
213
+ async nip04Decrypt(pubkey, ciphertext) {
214
+ return this.inner.nip04Decrypt(pubkey, ciphertext);
215
+ }
216
+ async nip44Encrypt(pubkey, plaintext) {
217
+ return this.inner.nip44Encrypt(pubkey, plaintext);
218
+ }
219
+ async nip44Decrypt(pubkey, ciphertext) {
220
+ return this.inner.nip44Decrypt(pubkey, ciphertext);
221
+ }
222
+ getBunkerUrl() {
223
+ return toBunkerURL(this.inner.bp);
224
+ }
225
+ async close() {
226
+ await this.inner.close();
227
+ }
228
+ };
229
+
230
+ // src/keycast-http-signer.ts
231
+ import { verifyEvent } from "nostr-tools/pure";
232
+ var DEFAULT_KEYCAST_API = "https://login.divine.video";
233
+ var KeycastAuthError = class extends Error {
234
+ constructor(status) {
235
+ super(`Keycast auth failed: HTTP ${status}`);
236
+ this.name = "KeycastAuthError";
237
+ this.status = status;
238
+ }
239
+ };
240
+ var KeycastHttpSigner = class {
241
+ constructor(token, options) {
242
+ this.type = "keycast";
243
+ this.cachedPubkey = null;
244
+ this.refreshPromise = null;
245
+ this.onTokenRefresh = null;
246
+ this.token = token;
247
+ this.refreshToken = options?.refreshToken ?? null;
248
+ this.clientId = options?.clientId ?? "privdm";
249
+ this.apiUrl = options?.apiUrl ?? DEFAULT_KEYCAST_API;
250
+ this.fetchImpl = options?.fetchImpl ?? ((...args) => fetch(...args));
251
+ }
252
+ async tryRefreshToken() {
253
+ if (!this.refreshToken) return false;
254
+ if (this.refreshPromise) {
255
+ await this.refreshPromise;
256
+ return true;
257
+ }
258
+ try {
259
+ this.refreshPromise = this.doRefresh();
260
+ await this.refreshPromise;
261
+ return true;
262
+ } catch (e) {
263
+ console.warn("[keycast] Token refresh failed:", e);
264
+ return false;
265
+ } finally {
266
+ this.refreshPromise = null;
267
+ }
268
+ }
269
+ async doRefresh() {
270
+ const res = await this.fetchImpl(`${this.apiUrl}/api/oauth/token`, {
271
+ method: "POST",
272
+ headers: { "Content-Type": "application/json" },
273
+ body: JSON.stringify({
274
+ grant_type: "refresh_token",
275
+ refresh_token: this.refreshToken,
276
+ client_id: this.clientId
277
+ }),
278
+ signal: AbortSignal.timeout(3e4)
279
+ });
280
+ if (!res.ok) {
281
+ this.refreshToken = null;
282
+ throw new KeycastAuthError(res.status);
283
+ }
284
+ const data = await res.json();
285
+ if (!data.access_token) throw new Error("No access_token in refresh response");
286
+ this.token = data.access_token;
287
+ if (data.refresh_token) this.refreshToken = data.refresh_token;
288
+ this.cachedPubkey = null;
289
+ this.onTokenRefresh?.({
290
+ accessToken: data.access_token,
291
+ refreshToken: data.refresh_token ?? this.refreshToken
292
+ });
293
+ }
294
+ async rpc(method, params) {
295
+ const maxRetries = 3;
296
+ for (let attempt = 0; ; attempt++) {
297
+ const res = await this.fetchImpl(`${this.apiUrl}/api/nostr`, {
298
+ method: "POST",
299
+ headers: {
300
+ "Content-Type": "application/json",
301
+ Authorization: `Bearer ${this.token}`
302
+ },
303
+ body: JSON.stringify({ method, params }),
304
+ signal: AbortSignal.timeout(3e4)
305
+ });
306
+ if (res.status === 429 && attempt < maxRetries) {
307
+ const retryAfter = res.headers.get("Retry-After");
308
+ const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * 2 ** attempt;
309
+ console.warn(
310
+ `[keycast] Rate limited on ${method}, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})` + (retryAfter ? `, Retry-After: ${retryAfter}s` : "")
311
+ );
312
+ await new Promise((r) => setTimeout(r, delay));
313
+ continue;
314
+ }
315
+ if ((res.status === 401 || res.status === 403) && attempt === 0) {
316
+ const refreshed = await this.tryRefreshToken();
317
+ if (refreshed) continue;
318
+ throw new KeycastAuthError(res.status);
319
+ }
320
+ if (!res.ok) {
321
+ if (res.status === 401 || res.status === 403) {
322
+ throw new KeycastAuthError(res.status);
323
+ }
324
+ throw new Error(`Keycast RPC failed: HTTP ${res.status}`);
325
+ }
326
+ const data = await res.json();
327
+ if (data.error) {
328
+ throw new Error(`Keycast RPC error: ${data.error}`);
329
+ }
330
+ return data.result;
331
+ }
332
+ }
333
+ async getPublicKey() {
334
+ if (this.cachedPubkey) return this.cachedPubkey;
335
+ const pubkey = await this.rpc("get_public_key", []);
336
+ this.cachedPubkey = pubkey;
337
+ return pubkey;
338
+ }
339
+ async signEvent(event) {
340
+ const pubkey = await this.getPublicKey();
341
+ const unsigned = { ...event, pubkey };
342
+ const result = await this.rpc("sign_event", [JSON.stringify(unsigned)]);
343
+ const signed = typeof result === "string" ? JSON.parse(result) : result;
344
+ if (!verifyEvent(signed)) {
345
+ throw new Error("Keycast returned an invalid signed event");
346
+ }
347
+ return signed;
348
+ }
349
+ async nip04Encrypt(pubkey, plaintext) {
350
+ return await this.rpc("nip04_encrypt", [pubkey, plaintext]);
351
+ }
352
+ async nip04Decrypt(pubkey, ciphertext) {
353
+ return await this.rpc("nip04_decrypt", [pubkey, ciphertext]);
354
+ }
355
+ async nip44Encrypt(pubkey, plaintext) {
356
+ return await this.rpc("nip44_encrypt", [pubkey, plaintext]);
357
+ }
358
+ async nip44Decrypt(pubkey, ciphertext) {
359
+ return await this.rpc("nip44_decrypt", [pubkey, ciphertext]);
360
+ }
361
+ };
362
+
363
+ // src/session.ts
364
+ import { nip19 as nip192 } from "nostr-tools";
365
+ function createSessionStore(storage, prefix) {
366
+ const key = `${prefix}_session`;
367
+ return {
368
+ save(session) {
369
+ storage.setItem(key, JSON.stringify(session));
370
+ },
371
+ load() {
372
+ const json = storage.getItem(key);
373
+ if (!json) return null;
374
+ try {
375
+ const parsed = JSON.parse(json);
376
+ if (!parsed || typeof parsed !== "object") return null;
377
+ const obj = parsed;
378
+ switch (obj.type) {
379
+ case "keycast":
380
+ if (typeof obj.accessToken === "string") {
381
+ return {
382
+ type: "keycast",
383
+ accessToken: obj.accessToken,
384
+ ...typeof obj.refreshToken === "string" ? { refreshToken: obj.refreshToken } : {}
385
+ };
386
+ }
387
+ return null;
388
+ case "bunker":
389
+ if (typeof obj.bunkerUrl === "string") {
390
+ return { type: "bunker", bunkerUrl: obj.bunkerUrl };
391
+ }
392
+ return null;
393
+ case "nostrconnect":
394
+ if (typeof obj.clientNsec === "string" && typeof obj.bunkerUrl === "string") {
395
+ return { type: "nostrconnect", clientNsec: obj.clientNsec, bunkerUrl: obj.bunkerUrl };
396
+ }
397
+ return null;
398
+ case "extension":
399
+ return { type: "extension" };
400
+ case "nsec":
401
+ if (typeof obj.nsec === "string") {
402
+ return { type: "nsec", nsec: obj.nsec };
403
+ }
404
+ return null;
405
+ default:
406
+ return null;
407
+ }
408
+ } catch {
409
+ return null;
410
+ }
411
+ },
412
+ clear() {
413
+ storage.removeItem(key);
414
+ }
415
+ };
416
+ }
417
+ async function restoreSession(session) {
418
+ switch (session.type) {
419
+ case "keycast":
420
+ return new KeycastHttpSigner(session.accessToken, {
421
+ refreshToken: session.refreshToken
422
+ });
423
+ case "extension":
424
+ return new ExtensionSigner();
425
+ case "bunker":
426
+ return BunkerNIP44Signer.fromBunkerUrl(session.bunkerUrl);
427
+ case "nostrconnect": {
428
+ const { type, data } = nip192.decode(session.clientNsec);
429
+ if (type !== "nsec") throw new Error("Invalid client nsec");
430
+ return BunkerNIP44Signer.reconnect(data, session.bunkerUrl);
431
+ }
432
+ case "nsec":
433
+ return new NsecSigner(session.nsec);
434
+ }
435
+ }
436
+
437
+ // src/oauth.ts
438
+ function base64URLEncode(buffer) {
439
+ const bytes = new Uint8Array(buffer);
440
+ let binary = "";
441
+ for (let i = 0; i < bytes.length; i++) {
442
+ binary += String.fromCharCode(bytes[i]);
443
+ }
444
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
445
+ }
446
+ function generateCodeVerifier() {
447
+ const bytes = new Uint8Array(32);
448
+ crypto.getRandomValues(bytes);
449
+ return base64URLEncode(bytes.buffer);
450
+ }
451
+ async function generateCodeChallenge(verifier) {
452
+ const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
453
+ return base64URLEncode(hash);
454
+ }
455
+ async function buildOAuthUrl(config, options) {
456
+ const apiUrl = config.apiUrl ?? "https://login.divine.video";
457
+ const scope = config.scope ?? "policy:full";
458
+ const codeVerifier = generateCodeVerifier();
459
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
460
+ const nonce = generateCodeVerifier().substring(0, 16);
461
+ config.storage.savePkceState({ codeVerifier, nonce });
462
+ const url = new URL("/api/oauth/authorize", apiUrl);
463
+ url.searchParams.set("client_id", config.clientId);
464
+ url.searchParams.set("redirect_uri", config.redirectUri);
465
+ url.searchParams.set("response_type", "code");
466
+ url.searchParams.set("scope", scope);
467
+ url.searchParams.set("code_challenge", codeChallenge);
468
+ url.searchParams.set("code_challenge_method", "S256");
469
+ url.searchParams.set("state", nonce);
470
+ if (options?.defaultRegister) {
471
+ url.searchParams.set("default_register", "true");
472
+ }
473
+ const handle = config.storage.loadAuthorizationHandle();
474
+ if (handle) {
475
+ url.searchParams.set("authorization_handle", handle);
476
+ }
477
+ return url.toString();
478
+ }
479
+ async function exchangeCode(code, state, config) {
480
+ const apiUrl = config.apiUrl ?? "https://login.divine.video";
481
+ const fetchFn = config.fetchImpl ?? ((...args) => fetch(...args));
482
+ const storedState = config.storage.loadPkceState();
483
+ if (!storedState) {
484
+ throw new Error("OAuth session expired. Please try again.");
485
+ }
486
+ if (state !== storedState.nonce) {
487
+ throw new Error("OAuth state mismatch. Please try again.");
488
+ }
489
+ const res = await fetchFn(`${apiUrl}/api/oauth/token`, {
490
+ method: "POST",
491
+ headers: { "Content-Type": "application/json" },
492
+ body: JSON.stringify({
493
+ grant_type: "authorization_code",
494
+ code,
495
+ client_id: config.clientId,
496
+ redirect_uri: config.redirectUri,
497
+ code_verifier: storedState.codeVerifier
498
+ })
499
+ });
500
+ if (!res.ok) {
501
+ const body = await res.json().catch(() => null);
502
+ const msg = body?.error_description ?? body?.error ?? `HTTP ${res.status}`;
503
+ throw new Error(`diVine token exchange failed: ${msg}`);
504
+ }
505
+ const data = await res.json();
506
+ config.storage.clearPkceState();
507
+ if (!data.access_token) {
508
+ throw new Error("diVine token exchange failed: no access_token in response");
509
+ }
510
+ if (data.authorization_handle) {
511
+ config.storage.saveAuthorizationHandle(data.authorization_handle);
512
+ }
513
+ return {
514
+ signer: new KeycastHttpSigner(data.access_token, {
515
+ refreshToken: data.refresh_token,
516
+ clientId: config.clientId,
517
+ apiUrl
518
+ }),
519
+ accessToken: data.access_token,
520
+ refreshToken: data.refresh_token
521
+ };
522
+ }
523
+ export {
524
+ BunkerNIP44Signer,
525
+ ExtensionSigner,
526
+ KeycastAuthError,
527
+ KeycastHttpSigner,
528
+ NsecSigner,
529
+ buildOAuthUrl,
530
+ createSessionStore,
531
+ exchangeCode,
532
+ restoreSession
533
+ };
@@ -0,0 +1,38 @@
1
+ import type { EventTemplate, VerifiedEvent } from 'nostr-tools/pure';
2
+ import type { NostrSigner, SignerType } from './types';
3
+ export declare const DEFAULT_KEYCAST_API = "https://login.divine.video";
4
+ export declare class KeycastAuthError extends Error {
5
+ readonly status: number;
6
+ constructor(status: number);
7
+ }
8
+ export interface TokenRefreshResult {
9
+ accessToken: string;
10
+ refreshToken: string;
11
+ }
12
+ export declare class KeycastHttpSigner implements NostrSigner {
13
+ readonly type: SignerType;
14
+ private token;
15
+ private refreshToken;
16
+ private readonly clientId;
17
+ private readonly apiUrl;
18
+ private readonly fetchImpl;
19
+ private cachedPubkey;
20
+ private refreshPromise;
21
+ onTokenRefresh: ((result: TokenRefreshResult) => void) | null;
22
+ constructor(token: string, options?: {
23
+ refreshToken?: string;
24
+ clientId?: string;
25
+ apiUrl?: string;
26
+ fetchImpl?: typeof fetch;
27
+ });
28
+ private tryRefreshToken;
29
+ private doRefresh;
30
+ private rpc;
31
+ getPublicKey(): Promise<string>;
32
+ signEvent(event: EventTemplate): Promise<VerifiedEvent>;
33
+ nip04Encrypt(pubkey: string, plaintext: string): Promise<string>;
34
+ nip04Decrypt(pubkey: string, ciphertext: string): Promise<string>;
35
+ nip44Encrypt(pubkey: string, plaintext: string): Promise<string>;
36
+ nip44Decrypt(pubkey: string, ciphertext: string): Promise<string>;
37
+ }
38
+ //# sourceMappingURL=keycast-http-signer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keycast-http-signer.d.ts","sourceRoot":"","sources":["../src/keycast-http-signer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEvD,eAAO,MAAM,mBAAmB,+BAA+B,CAAC;AAEhE,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBACZ,MAAM,EAAE,MAAM;CAK3B;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAa;IACtC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,cAAc,CAA8B;IACpD,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC,GAAG,IAAI,CAAQ;gBAEzD,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QACnC,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;KAC1B;YAQa,eAAe;YAqBf,SAAS;YAkCT,GAAG;IA8CX,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAO/B,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;IAcvD,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIjE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAGxE"}
@@ -0,0 +1,14 @@
1
+ import { type EventTemplate, type VerifiedEvent } from 'nostr-tools/pure';
2
+ import type { NostrSigner } from './types';
3
+ export declare class NsecSigner implements NostrSigner {
4
+ readonly type: "nsec";
5
+ private readonly secretKey;
6
+ constructor(nsec: string);
7
+ getPublicKey(): Promise<string>;
8
+ signEvent(event: EventTemplate): Promise<VerifiedEvent>;
9
+ nip04Encrypt(pubkey: string, plaintext: string): Promise<string>;
10
+ nip04Decrypt(pubkey: string, ciphertext: string): Promise<string>;
11
+ nip44Encrypt(pubkey: string, plaintext: string): Promise<string>;
12
+ nip44Decrypt(pubkey: string, ciphertext: string): Promise<string>;
13
+ }
14
+ //# sourceMappingURL=nsec-signer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nsec-signer.d.ts","sourceRoot":"","sources":["../src/nsec-signer.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,qBAAa,UAAW,YAAW,WAAW;IAC5C,QAAQ,CAAC,IAAI,SAAmB;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAa;gBAE3B,IAAI,EAAE,MAAM;IAQlB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAI/B,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;IAIvD,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIjE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKhE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAIxE"}
@@ -0,0 +1,33 @@
1
+ import type { NostrSigner } from './types';
2
+ export interface OAuthStorage {
3
+ savePkceState(state: {
4
+ codeVerifier: string;
5
+ nonce: string;
6
+ }): void;
7
+ loadPkceState(): {
8
+ codeVerifier: string;
9
+ nonce: string;
10
+ } | null;
11
+ clearPkceState(): void;
12
+ saveAuthorizationHandle(handle: string): void;
13
+ loadAuthorizationHandle(): string | null;
14
+ clearAuthorizationHandle(): void;
15
+ }
16
+ export interface OAuthConfig {
17
+ clientId: string;
18
+ redirectUri: string;
19
+ apiUrl?: string;
20
+ scope?: string;
21
+ storage: OAuthStorage;
22
+ fetchImpl?: typeof fetch;
23
+ }
24
+ export interface OAuthResult {
25
+ signer: NostrSigner;
26
+ accessToken: string;
27
+ refreshToken?: string;
28
+ }
29
+ export declare function buildOAuthUrl(config: OAuthConfig, options?: {
30
+ defaultRegister?: boolean;
31
+ }): Promise<string>;
32
+ export declare function exchangeCode(code: string, state: string, config: OAuthConfig): Promise<OAuthResult>;
33
+ //# sourceMappingURL=oauth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,MAAM,WAAW,YAAY;IAC3B,aAAa,CAAC,KAAK,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACpE,aAAa,IAAI;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAChE,cAAc,IAAI,IAAI,CAAC;IACvB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAAC;IACzC,wBAAwB,IAAI,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA0BD,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,GACtC,OAAO,CAAC,MAAM,CAAC,CA6BjB;AAUD,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,WAAW,CAAC,CAoDtB"}
@@ -0,0 +1,31 @@
1
+ import type { NostrSigner } from './types';
2
+ export type StoredSession = {
3
+ type: 'keycast';
4
+ accessToken: string;
5
+ refreshToken?: string;
6
+ } | {
7
+ type: 'bunker';
8
+ bunkerUrl: string;
9
+ } | {
10
+ type: 'nostrconnect';
11
+ clientNsec: string;
12
+ bunkerUrl: string;
13
+ } | {
14
+ type: 'extension';
15
+ } | {
16
+ type: 'nsec';
17
+ nsec: string;
18
+ };
19
+ export interface SessionStorage {
20
+ getItem(key: string): string | null;
21
+ setItem(key: string, value: string): void;
22
+ removeItem(key: string): void;
23
+ }
24
+ export interface SessionStore {
25
+ save(session: StoredSession): void;
26
+ load(): StoredSession | null;
27
+ clear(): void;
28
+ }
29
+ export declare function createSessionStore(storage: SessionStorage, prefix: string): SessionStore;
30
+ export declare function restoreSession(session: StoredSession): Promise<NostrSigner>;
31
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAM3C,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnC,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC;IACnC,IAAI,IAAI,aAAa,GAAG,IAAI,CAAC;IAC7B,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,YAAY,CAuDxF;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAkBjF"}
@@ -0,0 +1,12 @@
1
+ import type { EventTemplate, VerifiedEvent } from 'nostr-tools/pure';
2
+ export type SignerType = 'nsec' | 'extension' | 'bunker' | 'nostrconnect' | 'keycast';
3
+ export interface NostrSigner {
4
+ type: SignerType;
5
+ getPublicKey(): Promise<string>;
6
+ signEvent(event: EventTemplate): Promise<VerifiedEvent>;
7
+ nip04Encrypt(pubkey: string, plaintext: string): Promise<string>;
8
+ nip04Decrypt(pubkey: string, ciphertext: string): Promise<string>;
9
+ nip44Encrypt(pubkey: string, plaintext: string): Promise<string>;
10
+ nip44Decrypt(pubkey: string, ciphertext: string): Promise<string>;
11
+ }
12
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAErE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,cAAc,GAAG,SAAS,CAAC;AAEtF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,UAAU,CAAC;IACjB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAChC,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACxD,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACnE"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "divine-signer",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "package.json"
16
+ ],
17
+ "scripts": {
18
+ "build": "rm -rf dist && esbuild src/index.ts --bundle --format=esm --outfile=dist/index.js --external:nostr-tools --platform=browser --target=es2020 && tsc -p tsconfig.build.json",
19
+ "prepublishOnly": "npm run build",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "description": "Headless Nostr signer — five auth methods, one interface",
25
+ "license": "MPL-2.0",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/divinevideo/divine-signer.git"
29
+ },
30
+ "keywords": [
31
+ "nostr",
32
+ "signer",
33
+ "nip-07",
34
+ "nip-46",
35
+ "bunker",
36
+ "keycast",
37
+ "oauth"
38
+ ],
39
+ "peerDependencies": {
40
+ "nostr-tools": "^2.23.0"
41
+ },
42
+ "devDependencies": {
43
+ "esbuild": "^0.27.3",
44
+ "jsdom": "^25.0.1",
45
+ "nostr-tools": "^2.23.0",
46
+ "typescript": "~5.6.2",
47
+ "vitest": "^2.1.8"
48
+ }
49
+ }