@bwalletx/connect 0.3.0 → 0.5.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +145 -4
  3. package/assets/protocol/401.jpg +0 -0
  4. package/assets/protocol/401.mp4 +0 -0
  5. package/assets/protocol/402.jpg +0 -0
  6. package/assets/protocol/402.mp4 +0 -0
  7. package/assets/protocol/403.jpg +0 -0
  8. package/assets/protocol/403.mp4 +0 -0
  9. package/dist/access-Z6xQwKVF.d.cts +118 -0
  10. package/dist/access-Z6xQwKVF.d.ts +118 -0
  11. package/dist/bwalletx-connect.global.js +41 -3
  12. package/dist/bwalletx-connect.global.js.map +1 -1
  13. package/dist/{chunk-NNMUSWPG.js → chunk-CVLXFNAG.js} +123 -1
  14. package/dist/chunk-CVLXFNAG.js.map +1 -0
  15. package/dist/chunk-GF6GUDZL.js +404 -0
  16. package/dist/chunk-GF6GUDZL.js.map +1 -0
  17. package/dist/chunk-MWD4BCO5.js +182 -0
  18. package/dist/chunk-MWD4BCO5.js.map +1 -0
  19. package/dist/chunk-QRXFL66W.js +652 -0
  20. package/dist/chunk-QRXFL66W.js.map +1 -0
  21. package/dist/element.cjs +209 -0
  22. package/dist/element.cjs.map +1 -1
  23. package/dist/element.js +2 -1
  24. package/dist/element.js.map +1 -1
  25. package/dist/index.cjs +706 -5
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +66 -62
  28. package/dist/index.d.ts +66 -62
  29. package/dist/index.js +63 -3
  30. package/dist/index.js.map +1 -1
  31. package/dist/next.cjs +783 -0
  32. package/dist/next.cjs.map +1 -0
  33. package/dist/next.d.cts +98 -0
  34. package/dist/next.d.ts +98 -0
  35. package/dist/next.js +198 -0
  36. package/dist/next.js.map +1 -0
  37. package/dist/react.cjs +795 -36
  38. package/dist/react.cjs.map +1 -1
  39. package/dist/react.d.cts +74 -28
  40. package/dist/react.d.ts +74 -28
  41. package/dist/react.js +177 -4
  42. package/dist/react.js.map +1 -1
  43. package/dist/server.cjs +742 -10
  44. package/dist/server.cjs.map +1 -1
  45. package/dist/server.d.cts +314 -2
  46. package/dist/server.d.ts +314 -2
  47. package/dist/server.js +82 -107
  48. package/dist/server.js.map +1 -1
  49. package/dist/wallets-VOKIKBbO.d.cts +250 -0
  50. package/dist/wallets-VOKIKBbO.d.ts +250 -0
  51. package/package.json +32 -6
  52. package/dist/chunk-NNMUSWPG.js.map +0 -1
package/dist/server.d.ts CHANGED
@@ -1,4 +1,316 @@
1
- import { WalletProtocol } from '@bsv/sdk';
1
+ import { ChainTracker, WalletProtocol } from '@bsv/sdk';
2
+ import { b as AccessResponse, i as AccessKind, d as ACCESS_PROTOCOL, a as AccessRequirement } from './access-Z6xQwKVF.js';
3
+ export { l as ACCESS_CHALLENGE_LIFETIME_S, c as ACCESS_ERROR_STATUS, e as ACCESS_VERSION, f as AccessChallenge, A as AccessError, g as AccessErrorBody, h as AccessErrorCode, H as HoldingProof, M as MAX_HOLDING_PROOFS, P as PROTOCOL_MEDIA, T as TokenRequirement, j as accessErrorMessage, m as atLeastRaw, k as canonicalJson, n as challengeBytes, o as kindFor, q as normaliseRequirement, r as parseAccessChallenge, p as protocolMediaUrl, t as tokenNeedRaw } from './access-Z6xQwKVF.js';
4
+
5
+ /**
6
+ * Shared NonceStore adapters. No database driver is imported: you pass your own client or query
7
+ * function. Every adapter makes `take` read-and-delete in one step, so each nonce works once.
8
+ */
9
+
10
+ /**
11
+ * The Redis commands used. Fits @upstash/redis and node-redis v4+ as they are
12
+ * (`set(key, value, { ex | EX })`, `getdel` / `getDel`). For ioredis pass `flavor: 'ioredis'`.
13
+ */
14
+ interface RedisLike {
15
+ set(key: string, value: string, ...args: unknown[]): Promise<unknown>;
16
+ getdel?(key: string): Promise<unknown>;
17
+ getDel?(key: string): Promise<unknown>;
18
+ }
19
+ interface RedisStoreOptions {
20
+ client: RedisLike;
21
+ /** Key prefix. Default 'bwalletx:nonce:'. */
22
+ prefix?: string;
23
+ /** 'ioredis' uses positional `set(key, value, 'EX', s)`. Default 'object' (Upstash, node-redis). */
24
+ flavor?: 'object' | 'ioredis';
25
+ }
26
+ /** Redis / Upstash / Vercel KV. Needs Redis 6.2+ for GETDEL. */
27
+ declare function redisStore(opts: RedisStoreOptions): NonceStore;
28
+ /** `query(sql, params)` resolving to `{ rows }` (node-postgres, @vercel/postgres, Neon) or a bare array (postgres.js `sql.unsafe`). */
29
+ type PgQuery = (sql: string, params: unknown[]) => Promise<{
30
+ rows: unknown[];
31
+ } | unknown[]>;
32
+ interface PgStoreOptions {
33
+ query: PgQuery;
34
+ /** Table name (letters, digits, _ and one optional schema dot). Default 'bwalletx_nonces'. */
35
+ table?: string;
36
+ }
37
+ /** Run this once (a migration) before using pgStore. */
38
+ declare function pgNonceTableSql(table?: string): string;
39
+ /** Postgres (e.g. self-hosted Supabase). Single-use via DELETE ... RETURNING. */
40
+ declare function pgStore(opts: PgStoreOptions): NonceStore;
41
+
42
+ /**
43
+ * Signed session tokens: base64url(JSON payload) + "." + base64url(HMAC-SHA256).
44
+ * WebCrypto only, so it runs in Node 20+, Vercel Edge and Cloudflare Workers.
45
+ * There is no default secret: pass one (32+ chars) or every call throws.
46
+ */
47
+ interface SessionPayload {
48
+ /** The proven identity key (lowercase hex). */
49
+ sub: string;
50
+ /** Issued at, seconds since the epoch. */
51
+ iat: number;
52
+ /** Expires at, seconds since the epoch. */
53
+ exp: number;
54
+ /** Your app's user id, if the identity key is linked to one. */
55
+ uid?: string;
56
+ handle?: string;
57
+ paymail?: string;
58
+ [claim: string]: unknown;
59
+ }
60
+ declare const MIN_SECRET_LENGTH = 32;
61
+ interface SignSessionOptions {
62
+ secret: string;
63
+ /** Lifetime in seconds. Default 7 days. */
64
+ maxAge?: number;
65
+ /** For tests. */
66
+ now?: number;
67
+ }
68
+ /** Sign a session. `claims.sub` (identity key) is required. */
69
+ declare function signSession(claims: Omit<SessionPayload, 'iat' | 'exp'> & Partial<Pick<SessionPayload, 'iat' | 'exp'>>, opts: SignSessionOptions): Promise<string>;
70
+ /** The payload if the token is authentic and unexpired, else null. Never throws on bad input (only on a missing secret). */
71
+ declare function verifySession(token: unknown, opts: {
72
+ secret: string;
73
+ now?: number;
74
+ }): Promise<SessionPayload | null>;
75
+ interface CookieOptions {
76
+ name: string;
77
+ maxAge: number;
78
+ secure?: boolean;
79
+ sameSite?: 'Lax' | 'Strict' | 'None';
80
+ path?: string;
81
+ domain?: string;
82
+ }
83
+ /** A Set-Cookie header value. Always HttpOnly. */
84
+ declare function serializeCookie(value: string, o: CookieOptions): string;
85
+ /** Read one cookie from a Cookie header. */
86
+ declare function readCookie(header: string | null | undefined, name: string): string | null;
87
+
88
+ /**
89
+ * Server side: check that a transaction really pays what you expected before you deliver anything.
90
+ * Read-only: it never broadcasts, signs or retries a spend.
91
+ */
92
+
93
+ interface ExpectedOutput {
94
+ /** Locking script hex. Give this or `address`. */
95
+ script?: string;
96
+ /** A P2PKH address. */
97
+ address?: string;
98
+ /** Minimum satoshis this output must carry. */
99
+ satoshis: number;
100
+ }
101
+ type TxStatus = {
102
+ state: 'confirmed';
103
+ confirmations: number;
104
+ source: 'woc' | 'arc';
105
+ } | {
106
+ state: 'mempool';
107
+ source: 'woc' | 'arc';
108
+ detail?: string;
109
+ } | {
110
+ state: 'not-found' | 'rejected';
111
+ source: 'woc' | 'arc';
112
+ detail?: string;
113
+ } | {
114
+ state: 'unchecked';
115
+ };
116
+ interface VerifyPaymentOptions {
117
+ /** A txid (fetched from WhatsOnChain), raw tx / BEEF / Atomic BEEF as hex, or BEEF bytes. */
118
+ tx: string | number[] | Uint8Array;
119
+ /** Every one must be matched by a different output. */
120
+ expected: ExpectedOutput[];
121
+ network?: 'main' | 'test';
122
+ /** Where to ask for status. Default 'woc'. 'none' skips the network (BEEF only). */
123
+ status?: 'woc' | 'arc' | 'none';
124
+ /** ARC base URL, e.g. https://arc.taal.com. Needed when status is 'arc'. */
125
+ arcUrl?: string;
126
+ /** Sent as Bearer. Read it from env; never hardcode. */
127
+ arcApiKey?: string;
128
+ /** Fail unless the network has seen the tx (mempool or confirmed). Default false. */
129
+ requireSeen?: boolean;
130
+ /** Minimum confirmations. Default 0. */
131
+ minConfirmations?: number;
132
+ /** Also run SPV (merkle proofs + scripts) on a BEEF with this chain tracker. */
133
+ spv?: ChainTracker;
134
+ /** Injected fetch (tests, custom agents). Default globalThis.fetch. */
135
+ fetch?: typeof fetch;
136
+ }
137
+ interface VerifyPaymentResult {
138
+ ok: true;
139
+ txid: string;
140
+ /** For each expected output, the index of the output that paid it. */
141
+ matched: {
142
+ vout: number;
143
+ satoshis: number;
144
+ script: string;
145
+ }[];
146
+ status: TxStatus;
147
+ }
148
+ type PaymentErrorCode = 'BAD_TX' | 'TX_NOT_FOUND' | 'OUTPUT_MISSING' | 'NOT_SEEN' | 'REJECTED' | 'SPV_FAILED' | 'NETWORK' | 'BAD_EXPECTED';
149
+ declare class PaymentVerificationError extends Error {
150
+ readonly code: PaymentErrorCode;
151
+ readonly txid?: string | undefined;
152
+ constructor(code: PaymentErrorCode, message: string, txid?: string | undefined);
153
+ }
154
+ /**
155
+ * Throws PaymentVerificationError (with a `code`) unless every expected output is paid by a
156
+ * distinct output of the transaction with at least the expected satoshis.
157
+ */
158
+ declare function verifyPayment(o: VerifyPaymentOptions): Promise<VerifyPaymentResult>;
159
+
160
+ /**
161
+ * Link a proven identity key to a user in YOUR auth system (Supabase, next-auth, your own table).
162
+ * You supply the storage callbacks; this module only holds the rules.
163
+ */
164
+ interface LinkCallbacks {
165
+ /** The user id this identity key is linked to, or null. */
166
+ findUserByIdentityKey(identityKey: string): Promise<string | null> | string | null;
167
+ /** Save the link. Make (identity_key) unique in your table so a race cannot double-link. */
168
+ attach(userId: string, identityKey: string): Promise<void> | void;
169
+ }
170
+ type LinkResult = {
171
+ status: 'linked' | 'already-linked';
172
+ userId: string;
173
+ identityKey: string;
174
+ };
175
+ declare class IdentityLinkConflictError extends Error {
176
+ readonly identityKey: string;
177
+ readonly otherUserId: string;
178
+ readonly code = "IDENTITY_LINKED_TO_OTHER_USER";
179
+ constructor(identityKey: string, otherUserId: string);
180
+ }
181
+ /**
182
+ * Link `identityKey` (already verified with verifySignIn) to the signed-in `userId`.
183
+ * Throws IdentityLinkConflictError if it belongs to someone else. Idempotent for the same user.
184
+ */
185
+ declare function linkIdentity(userId: string, identityKey: string, cb: LinkCallbacks): Promise<LinkResult>;
186
+ interface SignInOrLinkOptions extends LinkCallbacks {
187
+ /** The user already signed in some other way (e.g. HandCash, email), if any. */
188
+ currentUserId?: string | null;
189
+ /** Make a new user for an unknown identity key. Without it, unknown keys return status 'unknown'. */
190
+ createUser?(identityKey: string): Promise<string> | string;
191
+ }
192
+ type SignInOrLinkResult = {
193
+ status: 'signed-in' | 'linked' | 'already-linked' | 'created';
194
+ userId: string;
195
+ identityKey: string;
196
+ } | {
197
+ status: 'unknown';
198
+ userId: null;
199
+ identityKey: string;
200
+ };
201
+ /**
202
+ * The usual verify-route logic: signed in already → link; known key → that user; else create (or report unknown).
203
+ * Throws IdentityLinkConflictError when a signed-in user tries to link a key owned by another user.
204
+ */
205
+ declare function signInOrLink(identityKey: string, o: SignInOrLinkOptions): Promise<SignInOrLinkResult>;
206
+
207
+ interface IdentityStatus {
208
+ /** bit-sign identity level: 0 none, 1 Basic … 4 Sovereign (Veriff). */
209
+ level: number;
210
+ /** A live KYC credential for this identity key. */
211
+ kyc: boolean;
212
+ }
213
+ /**
214
+ * Where verifyAccess reads the chain and identity data. Every method THROWS when it cannot answer;
215
+ * a throw becomes `indexer_unavailable`, never "holds nothing".
216
+ */
217
+ interface AccessResolver {
218
+ /** Confirmed + unspent raw balance of `tokenId` ('bsv21:…') over these P2PKH addresses. */
219
+ tokenBalance(tokenId: string, addresses: string[]): Promise<string>;
220
+ /** Total supply, raw. Needed only for minBps requirements. */
221
+ tokenSupply?(tokenId: string): Promise<string>;
222
+ /** Identity level and KYC for a key. Needed only for identityLevel / kyc requirements. */
223
+ identity?(identityKey: string, response: AccessResponse): Promise<IdentityStatus>;
224
+ }
225
+ type FetchLike = typeof fetch;
226
+ interface OneSatResolverOptions {
227
+ /** Default 'https://api.1sat.app/1sat'. */
228
+ base?: string;
229
+ fetch?: FetchLike;
230
+ timeoutMs?: number;
231
+ }
232
+ /** BSV-21 balances and supply from the 1Sat indexer (the one bit-sign's token rooms use). */
233
+ declare function oneSatResolver(o?: OneSatResolverOptions): AccessResolver;
234
+ interface BitSignResolverOptions {
235
+ /** Default 'https://bit-sign.online'. */
236
+ base?: string;
237
+ fetch?: FetchLike;
238
+ timeoutMs?: number;
239
+ /** Token reads. Default: oneSatResolver(). */
240
+ tokens?: AccessResolver;
241
+ }
242
+ /**
243
+ * Identity level / KYC from bit-sign's POST /api/access/verify, which checks the signed response
244
+ * itself (the user's signature is their consent to disclose those facts to this origin).
245
+ * Token reads go to `tokens` (default the 1Sat indexer).
246
+ */
247
+ declare function bitSignResolver(o?: BitSignResolverOptions): AccessResolver;
248
+ interface CreateAccessChallengeOptions {
249
+ /** YOUR origin from config. Never from the request. */
250
+ origin: string;
251
+ /** The requirement, from YOUR config. Never from the request. */
252
+ require: AccessRequirement;
253
+ /** A shared store (redisStore / pgStore). The same stores sign-in uses. */
254
+ store: NonceStore;
255
+ /** The identity key the browser says it is (optional). If given, a different signer is wrong_account. */
256
+ identityKey?: unknown;
257
+ /** Default: '403' when a token is required, else '401'. */
258
+ kind?: AccessKind;
259
+ lifetimeS?: number;
260
+ now?: number;
261
+ }
262
+ interface AccessChallengeResponse {
263
+ /** The canonical JSON the wallet signs. Pass it to the wallet unchanged. */
264
+ challenge: string;
265
+ nonce: string;
266
+ kind: AccessKind;
267
+ protocolID: typeof ACCESS_PROTOCOL;
268
+ keyID: string;
269
+ exp: number;
270
+ }
271
+ declare function createAccessChallenge(o: CreateAccessChallengeOptions): Promise<AccessChallengeResponse>;
272
+ /** The P2PKH address a holding proof establishes, or null if the proof does not verify. */
273
+ declare function verifyHoldingProof(challenge: string, p: unknown): string | null;
274
+ interface VerifyAccessOptions {
275
+ origin: string;
276
+ /** The body the browser posted (AccessResponse). Untrusted. */
277
+ response: unknown;
278
+ store: NonceStore;
279
+ resolver: AccessResolver;
280
+ /**
281
+ * The requirement YOUR config says this gate needs. If given, the challenge must carry exactly it
282
+ * (defence in depth: the challenge came from your store, but configs change).
283
+ */
284
+ require?: AccessRequirement;
285
+ now?: number;
286
+ }
287
+ interface AccessGrant {
288
+ ok: true;
289
+ identityKey: string;
290
+ kind: AccessKind;
291
+ require: AccessRequirement;
292
+ /** Proven addresses holding the token (store them to re-check later). */
293
+ addresses: string[];
294
+ have: {
295
+ tokenRaw?: string;
296
+ needRaw?: string;
297
+ identityLevel?: number;
298
+ kyc?: boolean;
299
+ };
300
+ }
301
+ /**
302
+ * Verify a wallet's response. Returns the grant, or throws AccessError with a protocol code.
303
+ * The nonce is consumed first, so each challenge can be tried once.
304
+ */
305
+ declare function verifyAccess(o: VerifyAccessOptions): Promise<AccessGrant>;
306
+ /**
307
+ * Re-check a token requirement over already-proven addresses (for periodic re-verification).
308
+ * Returns { have, need } or throws AccessError('insufficient_tokens' | 'indexer_unavailable').
309
+ */
310
+ declare function checkTokenHolding(require: AccessRequirement, addresses: string[], resolver: AccessResolver): Promise<{
311
+ have: string;
312
+ need: string;
313
+ }>;
2
314
 
3
315
  /** Security level 2, a protocol name used for nothing else. Keep it constant. */
4
316
  declare const LOGIN_PROTOCOL: WalletProtocol;
@@ -61,4 +373,4 @@ declare function verifySignIn(opts: VerifySignInOptions): Promise<string | null>
61
373
  /** True when a request's Origin header is exactly your configured origin. Check it on both routes. */
62
374
  declare function originAllowed(requestOrigin: string | null | undefined, origin: string): boolean;
63
375
 
64
- export { CHALLENGE_LIFETIME_MS, type ChallengeResponse, type CreateChallengeOptions, LOGIN_PROTOCOL, type NonceStore, type PendingChallenge, type VerifySignInOptions, createChallenge, isIdentityKey, loginMessage, memoryStore, originAllowed, verifySignIn };
376
+ export { ACCESS_PROTOCOL, type AccessChallengeResponse, type AccessGrant, AccessKind, AccessRequirement, type AccessResolver, AccessResponse, type BitSignResolverOptions, CHALLENGE_LIFETIME_MS, type ChallengeResponse, type CookieOptions, type CreateAccessChallengeOptions, type CreateChallengeOptions, type ExpectedOutput, IdentityLinkConflictError, type IdentityStatus, LOGIN_PROTOCOL, type LinkCallbacks, type LinkResult, MIN_SECRET_LENGTH, type NonceStore, type OneSatResolverOptions, PaymentVerificationError, type PendingChallenge, type PgQuery, type PgStoreOptions, type RedisLike, type RedisStoreOptions, type SessionPayload, type SignInOrLinkOptions, type SignInOrLinkResult, type SignSessionOptions, type TxStatus, type VerifyAccessOptions, type VerifyPaymentOptions, type VerifyPaymentResult, type VerifySignInOptions, bitSignResolver, checkTokenHolding, createAccessChallenge, createChallenge, isIdentityKey, linkIdentity, loginMessage, memoryStore, oneSatResolver, originAllowed, pgNonceTableSql, pgStore, readCookie, redisStore, serializeCookie, signInOrLink, signSession, verifyAccess, verifyHoldingProof, verifyPayment, verifySession, verifySignIn };
package/dist/server.js CHANGED
@@ -1,118 +1,93 @@
1
- // src/server.ts
2
- import { ProtoWallet, PublicKey } from "@bsv/sdk";
3
- var LOGIN_PROTOCOL = [2, "bwallet sign in"];
4
- var CHALLENGE_LIFETIME_MS = 2 * 60 * 1e3;
5
- var warned = false;
6
- function memoryStore(opts = {}) {
7
- if (!opts.quiet && !warned) {
8
- warned = true;
9
- console.warn("[@bwalletx/connect] Using the in-memory nonce store. It is for development only; pass a shared store (Redis, database) in production.");
10
- }
11
- const m = /* @__PURE__ */ new Map();
12
- return {
13
- put(nonce, c) {
14
- const now = Date.now();
15
- for (const [k, v] of m) if (v.expiresAt < now) m.delete(k);
16
- m.set(nonce, c);
17
- },
18
- take(nonce) {
19
- const c = m.get(nonce) ?? null;
20
- m.delete(nonce);
21
- return c;
22
- }
23
- };
24
- }
25
- var defaultStore = null;
26
- var storeOrDefault = (s) => s ?? (defaultStore ??= memoryStore());
27
- function isIdentityKey(x) {
28
- if (typeof x !== "string" || !/^0[23][0-9a-fA-F]{64}$/.test(x)) return false;
29
- try {
30
- PublicKey.fromString(x);
31
- return true;
32
- } catch {
33
- return false;
34
- }
35
- }
36
- function normalOrigin(origin) {
37
- const u = new URL(origin);
38
- if (u.origin === "null" || u.origin !== origin.replace(/\/$/, "")) {
39
- throw new Error(`origin must be scheme://host[:port], got ${origin}`);
40
- }
41
- return u.origin;
42
- }
43
- function loginMessage(origin, nonce, expiresAt) {
44
- return [
45
- `Sign in to ${new URL(origin).host} with bWallet`,
46
- "",
47
- "This proves you hold this wallet. It does not spend anything.",
48
- "",
49
- `Origin: ${origin}`,
50
- `Nonce: ${nonce}`,
51
- `Expires: ${new Date(expiresAt).toISOString()}`
52
- ].join("\n");
53
- }
54
- function randomNonce() {
55
- const b = new Uint8Array(16);
56
- globalThis.crypto.getRandomValues(b);
57
- let s = "";
58
- for (const x of b) s += String.fromCharCode(x);
59
- return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
60
- }
61
- async function createChallenge(opts) {
62
- if (!isIdentityKey(opts.identityKey)) throw new Error("identityKey must be a compressed public key (66 hex chars)");
63
- const origin = normalOrigin(opts.origin);
64
- const nonce = randomNonce();
65
- const expiresAt = Date.now() + CHALLENGE_LIFETIME_MS;
66
- const message = loginMessage(origin, nonce, expiresAt);
67
- await storeOrDefault(opts.store).put(nonce, { identityKey: opts.identityKey.toLowerCase(), origin, message, expiresAt });
68
- return { nonce, message, protocolID: LOGIN_PROTOCOL, keyID: nonce, expiresAt };
69
- }
70
- async function verifySignIn(opts) {
71
- if (typeof opts.nonce !== "string" || opts.nonce.length > 64) return null;
72
- const c = await storeOrDefault(opts.store).take(opts.nonce);
73
- if (!c || Date.now() > c.expiresAt) return null;
74
- let origin;
75
- try {
76
- origin = normalOrigin(opts.origin);
77
- } catch {
78
- return null;
79
- }
80
- if (c.origin !== origin) return null;
81
- if (!isIdentityKey(opts.identityKey)) return null;
82
- const identityKey = opts.identityKey.toLowerCase();
83
- if (c.identityKey !== identityKey) return null;
84
- const sig = opts.signature;
85
- if (!Array.isArray(sig) || sig.length < 8 || sig.length > 80 || !sig.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)) {
86
- return null;
87
- }
88
- try {
89
- const { valid } = await new ProtoWallet("anyone").verifySignature({
90
- protocolID: LOGIN_PROTOCOL,
91
- keyID: opts.nonce,
92
- counterparty: identityKey,
93
- data: Array.from(new TextEncoder().encode(c.message)),
94
- signature: sig
95
- });
96
- return valid ? identityKey : null;
97
- } catch {
98
- return null;
99
- }
100
- }
101
- function originAllowed(requestOrigin, origin) {
102
- try {
103
- return !!requestOrigin && requestOrigin === normalOrigin(origin);
104
- } catch {
105
- return false;
106
- }
107
- }
1
+ import {
2
+ CHALLENGE_LIFETIME_MS,
3
+ IdentityLinkConflictError,
4
+ LOGIN_PROTOCOL,
5
+ MIN_SECRET_LENGTH,
6
+ PaymentVerificationError,
7
+ bitSignResolver,
8
+ checkTokenHolding,
9
+ createAccessChallenge,
10
+ createChallenge,
11
+ isIdentityKey,
12
+ linkIdentity,
13
+ loginMessage,
14
+ memoryStore,
15
+ oneSatResolver,
16
+ originAllowed,
17
+ pgNonceTableSql,
18
+ pgStore,
19
+ readCookie,
20
+ redisStore,
21
+ serializeCookie,
22
+ signInOrLink,
23
+ signSession,
24
+ verifyAccess,
25
+ verifyHoldingProof,
26
+ verifyPayment,
27
+ verifySession,
28
+ verifySignIn
29
+ } from "./chunk-QRXFL66W.js";
30
+ import {
31
+ ACCESS_CHALLENGE_LIFETIME_S,
32
+ ACCESS_ERROR_STATUS,
33
+ ACCESS_PROTOCOL,
34
+ ACCESS_VERSION,
35
+ AccessError,
36
+ MAX_HOLDING_PROOFS,
37
+ PROTOCOL_MEDIA,
38
+ accessErrorMessage,
39
+ atLeastRaw,
40
+ canonicalJson,
41
+ challengeBytes,
42
+ kindFor,
43
+ normaliseRequirement,
44
+ parseAccessChallenge,
45
+ protocolMediaUrl,
46
+ tokenNeedRaw
47
+ } from "./chunk-MWD4BCO5.js";
108
48
  export {
49
+ ACCESS_CHALLENGE_LIFETIME_S,
50
+ ACCESS_ERROR_STATUS,
51
+ ACCESS_PROTOCOL,
52
+ ACCESS_VERSION,
53
+ AccessError,
109
54
  CHALLENGE_LIFETIME_MS,
55
+ IdentityLinkConflictError,
110
56
  LOGIN_PROTOCOL,
57
+ MAX_HOLDING_PROOFS,
58
+ MIN_SECRET_LENGTH,
59
+ PROTOCOL_MEDIA,
60
+ PaymentVerificationError,
61
+ accessErrorMessage,
62
+ atLeastRaw,
63
+ bitSignResolver,
64
+ canonicalJson,
65
+ challengeBytes,
66
+ checkTokenHolding,
67
+ createAccessChallenge,
111
68
  createChallenge,
112
69
  isIdentityKey,
70
+ kindFor,
71
+ linkIdentity,
113
72
  loginMessage,
114
73
  memoryStore,
74
+ normaliseRequirement,
75
+ oneSatResolver,
115
76
  originAllowed,
77
+ parseAccessChallenge,
78
+ pgNonceTableSql,
79
+ pgStore,
80
+ protocolMediaUrl,
81
+ readCookie,
82
+ redisStore,
83
+ serializeCookie,
84
+ signInOrLink,
85
+ signSession,
86
+ tokenNeedRaw,
87
+ verifyAccess,
88
+ verifyHoldingProof,
89
+ verifyPayment,
90
+ verifySession,
116
91
  verifySignIn
117
92
  };
118
93
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import { ProtoWallet, PublicKey, type WalletProtocol } from '@bsv/sdk';\n\n/** Security level 2, a protocol name used for nothing else. Keep it constant. */\nexport const LOGIN_PROTOCOL: WalletProtocol = [2, 'bwallet sign in'];\nexport const CHALLENGE_LIFETIME_MS = 2 * 60 * 1000;\n\nexport interface PendingChallenge {\n identityKey: string;\n origin: string;\n message: string;\n expiresAt: number;\n}\n\n/**\n * Where challenges wait between the two routes. Use Redis or a database table in production.\n * `take` must read and delete in one step, so each nonce can be tried once.\n */\nexport interface NonceStore {\n put(nonce: string, c: PendingChallenge): Promise<void> | void;\n take(nonce: string): Promise<PendingChallenge | null> | PendingChallenge | null;\n}\n\nlet warned = false;\n\n/**\n * In-memory store. For development and tests only: it is lost on restart and not shared between\n * server processes or serverless instances. Expired entries are pruned on each put.\n */\nexport function memoryStore(opts: { quiet?: boolean } = {}): NonceStore {\n if (!opts.quiet && !warned) {\n warned = true;\n console.warn('[@bwalletx/connect] Using the in-memory nonce store. It is for development only; pass a shared store (Redis, database) in production.');\n }\n const m = new Map<string, PendingChallenge>();\n return {\n put(nonce, c) {\n const now = Date.now();\n for (const [k, v] of m) if (v.expiresAt < now) m.delete(k);\n m.set(nonce, c);\n },\n take(nonce) {\n const c = m.get(nonce) ?? null;\n m.delete(nonce);\n return c;\n },\n };\n}\n\nlet defaultStore: NonceStore | null = null;\nconst storeOrDefault = (s?: NonceStore) => s ?? (defaultStore ??= memoryStore());\n\nexport function isIdentityKey(x: unknown): x is string {\n if (typeof x !== 'string' || !/^0[23][0-9a-fA-F]{64}$/.test(x)) return false;\n try {\n PublicKey.fromString(x);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction normalOrigin(origin: string): string {\n const u = new URL(origin);\n if (u.origin === 'null' || u.origin !== origin.replace(/\\/$/, '')) {\n throw new Error(`origin must be scheme://host[:port], got ${origin}`);\n }\n return u.origin;\n}\n\n/** The text the user signs. Lines joined with \\n, no trailing newline. */\nexport function loginMessage(origin: string, nonce: string, expiresAt: number): string {\n return [\n `Sign in to ${new URL(origin).host} with bWallet`,\n '',\n 'This proves you hold this wallet. It does not spend anything.',\n '',\n `Origin: ${origin}`,\n `Nonce: ${nonce}`,\n `Expires: ${new Date(expiresAt).toISOString()}`,\n ].join('\\n');\n}\n\nfunction randomNonce(): string {\n const b = new Uint8Array(16);\n globalThis.crypto.getRandomValues(b);\n let s = '';\n for (const x of b) s += String.fromCharCode(x);\n return btoa(s).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nexport interface CreateChallengeOptions {\n /** YOUR site's origin from config, e.g. \"https://example.com\". Never take it from the request. */\n origin: string;\n /** The identity key the browser claims. Checked again at verify. */\n identityKey: unknown;\n /** Default: a process-wide in-memory store (dev only). */\n store?: NonceStore;\n}\n\nexport interface ChallengeResponse {\n nonce: string;\n message: string;\n protocolID: WalletProtocol;\n keyID: string;\n expiresAt: number;\n}\n\n/** Step 1. Throws if identityKey is not a valid compressed public key. Send the result to the browser. */\nexport async function createChallenge(opts: CreateChallengeOptions): Promise<ChallengeResponse> {\n if (!isIdentityKey(opts.identityKey)) throw new Error('identityKey must be a compressed public key (66 hex chars)');\n const origin = normalOrigin(opts.origin);\n const nonce = randomNonce();\n const expiresAt = Date.now() + CHALLENGE_LIFETIME_MS;\n const message = loginMessage(origin, nonce, expiresAt);\n await storeOrDefault(opts.store).put(nonce, { identityKey: opts.identityKey.toLowerCase(), origin, message, expiresAt });\n return { nonce, message, protocolID: LOGIN_PROTOCOL, keyID: nonce, expiresAt };\n}\n\nexport interface VerifySignInOptions {\n /** YOUR site's origin from config. Must equal the origin the challenge was made for. */\n origin: string;\n /** From the request body. */\n identityKey: unknown;\n nonce: unknown;\n signature: unknown;\n store?: NonceStore;\n}\n\n/**\n * Step 2. Returns the proven identity key (lowercase hex), or null.\n * The nonce is deleted before anything is checked, so each challenge can be tried once.\n */\nexport async function verifySignIn(opts: VerifySignInOptions): Promise<string | null> {\n if (typeof opts.nonce !== 'string' || opts.nonce.length > 64) return null;\n const c = await storeOrDefault(opts.store).take(opts.nonce);\n if (!c || Date.now() > c.expiresAt) return null;\n let origin: string;\n try {\n origin = normalOrigin(opts.origin);\n } catch {\n return null;\n }\n if (c.origin !== origin) return null;\n if (!isIdentityKey(opts.identityKey)) return null;\n const identityKey = opts.identityKey.toLowerCase();\n if (c.identityKey !== identityKey) return null;\n\n const sig = opts.signature;\n if (!Array.isArray(sig) || sig.length < 8 || sig.length > 80 || !sig.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)) {\n return null;\n }\n try {\n const { valid } = await new ProtoWallet('anyone').verifySignature({\n protocolID: LOGIN_PROTOCOL,\n keyID: opts.nonce,\n counterparty: identityKey,\n data: Array.from(new TextEncoder().encode(c.message)),\n signature: sig as number[],\n });\n return valid ? identityKey : null;\n } catch {\n return null; // @bsv/sdk throws on a bad signature instead of returning valid: false\n }\n}\n\n/** True when a request's Origin header is exactly your configured origin. Check it on both routes. */\nexport function originAllowed(requestOrigin: string | null | undefined, origin: string): boolean {\n try {\n return !!requestOrigin && requestOrigin === normalOrigin(origin);\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}