@bwalletx/connect 0.3.0 → 0.4.1

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/dist/server.d.cts CHANGED
@@ -1,4 +1,206 @@
1
- import { WalletProtocol } from '@bsv/sdk';
1
+ import { ChainTracker, WalletProtocol } from '@bsv/sdk';
2
+
3
+ /**
4
+ * Shared NonceStore adapters. No database driver is imported: you pass your own client or query
5
+ * function. Every adapter makes `take` read-and-delete in one step, so each nonce works once.
6
+ */
7
+
8
+ /**
9
+ * The Redis commands used. Fits @upstash/redis and node-redis v4+ as they are
10
+ * (`set(key, value, { ex | EX })`, `getdel` / `getDel`). For ioredis pass `flavor: 'ioredis'`.
11
+ */
12
+ interface RedisLike {
13
+ set(key: string, value: string, ...args: unknown[]): Promise<unknown>;
14
+ getdel?(key: string): Promise<unknown>;
15
+ getDel?(key: string): Promise<unknown>;
16
+ }
17
+ interface RedisStoreOptions {
18
+ client: RedisLike;
19
+ /** Key prefix. Default 'bwalletx:nonce:'. */
20
+ prefix?: string;
21
+ /** 'ioredis' uses positional `set(key, value, 'EX', s)`. Default 'object' (Upstash, node-redis). */
22
+ flavor?: 'object' | 'ioredis';
23
+ }
24
+ /** Redis / Upstash / Vercel KV. Needs Redis 6.2+ for GETDEL. */
25
+ declare function redisStore(opts: RedisStoreOptions): NonceStore;
26
+ /** `query(sql, params)` resolving to `{ rows }` (node-postgres, @vercel/postgres, Neon) or a bare array (postgres.js `sql.unsafe`). */
27
+ type PgQuery = (sql: string, params: unknown[]) => Promise<{
28
+ rows: unknown[];
29
+ } | unknown[]>;
30
+ interface PgStoreOptions {
31
+ query: PgQuery;
32
+ /** Table name (letters, digits, _ and one optional schema dot). Default 'bwalletx_nonces'. */
33
+ table?: string;
34
+ }
35
+ /** Run this once (a migration) before using pgStore. */
36
+ declare function pgNonceTableSql(table?: string): string;
37
+ /** Postgres (e.g. self-hosted Supabase). Single-use via DELETE ... RETURNING. */
38
+ declare function pgStore(opts: PgStoreOptions): NonceStore;
39
+
40
+ /**
41
+ * Signed session tokens: base64url(JSON payload) + "." + base64url(HMAC-SHA256).
42
+ * WebCrypto only, so it runs in Node 20+, Vercel Edge and Cloudflare Workers.
43
+ * There is no default secret: pass one (32+ chars) or every call throws.
44
+ */
45
+ interface SessionPayload {
46
+ /** The proven identity key (lowercase hex). */
47
+ sub: string;
48
+ /** Issued at, seconds since the epoch. */
49
+ iat: number;
50
+ /** Expires at, seconds since the epoch. */
51
+ exp: number;
52
+ /** Your app's user id, if the identity key is linked to one. */
53
+ uid?: string;
54
+ handle?: string;
55
+ paymail?: string;
56
+ [claim: string]: unknown;
57
+ }
58
+ declare const MIN_SECRET_LENGTH = 32;
59
+ interface SignSessionOptions {
60
+ secret: string;
61
+ /** Lifetime in seconds. Default 7 days. */
62
+ maxAge?: number;
63
+ /** For tests. */
64
+ now?: number;
65
+ }
66
+ /** Sign a session. `claims.sub` (identity key) is required. */
67
+ declare function signSession(claims: Omit<SessionPayload, 'iat' | 'exp'> & Partial<Pick<SessionPayload, 'iat' | 'exp'>>, opts: SignSessionOptions): Promise<string>;
68
+ /** The payload if the token is authentic and unexpired, else null. Never throws on bad input (only on a missing secret). */
69
+ declare function verifySession(token: unknown, opts: {
70
+ secret: string;
71
+ now?: number;
72
+ }): Promise<SessionPayload | null>;
73
+ interface CookieOptions {
74
+ name: string;
75
+ maxAge: number;
76
+ secure?: boolean;
77
+ sameSite?: 'Lax' | 'Strict' | 'None';
78
+ path?: string;
79
+ domain?: string;
80
+ }
81
+ /** A Set-Cookie header value. Always HttpOnly. */
82
+ declare function serializeCookie(value: string, o: CookieOptions): string;
83
+ /** Read one cookie from a Cookie header. */
84
+ declare function readCookie(header: string | null | undefined, name: string): string | null;
85
+
86
+ /**
87
+ * Server side: check that a transaction really pays what you expected before you deliver anything.
88
+ * Read-only: it never broadcasts, signs or retries a spend.
89
+ */
90
+
91
+ interface ExpectedOutput {
92
+ /** Locking script hex. Give this or `address`. */
93
+ script?: string;
94
+ /** A P2PKH address. */
95
+ address?: string;
96
+ /** Minimum satoshis this output must carry. */
97
+ satoshis: number;
98
+ }
99
+ type TxStatus = {
100
+ state: 'confirmed';
101
+ confirmations: number;
102
+ source: 'woc' | 'arc';
103
+ } | {
104
+ state: 'mempool';
105
+ source: 'woc' | 'arc';
106
+ detail?: string;
107
+ } | {
108
+ state: 'not-found' | 'rejected';
109
+ source: 'woc' | 'arc';
110
+ detail?: string;
111
+ } | {
112
+ state: 'unchecked';
113
+ };
114
+ interface VerifyPaymentOptions {
115
+ /** A txid (fetched from WhatsOnChain), raw tx / BEEF / Atomic BEEF as hex, or BEEF bytes. */
116
+ tx: string | number[] | Uint8Array;
117
+ /** Every one must be matched by a different output. */
118
+ expected: ExpectedOutput[];
119
+ network?: 'main' | 'test';
120
+ /** Where to ask for status. Default 'woc'. 'none' skips the network (BEEF only). */
121
+ status?: 'woc' | 'arc' | 'none';
122
+ /** ARC base URL, e.g. https://arc.taal.com. Needed when status is 'arc'. */
123
+ arcUrl?: string;
124
+ /** Sent as Bearer. Read it from env; never hardcode. */
125
+ arcApiKey?: string;
126
+ /** Fail unless the network has seen the tx (mempool or confirmed). Default false. */
127
+ requireSeen?: boolean;
128
+ /** Minimum confirmations. Default 0. */
129
+ minConfirmations?: number;
130
+ /** Also run SPV (merkle proofs + scripts) on a BEEF with this chain tracker. */
131
+ spv?: ChainTracker;
132
+ /** Injected fetch (tests, custom agents). Default globalThis.fetch. */
133
+ fetch?: typeof fetch;
134
+ }
135
+ interface VerifyPaymentResult {
136
+ ok: true;
137
+ txid: string;
138
+ /** For each expected output, the index of the output that paid it. */
139
+ matched: {
140
+ vout: number;
141
+ satoshis: number;
142
+ script: string;
143
+ }[];
144
+ status: TxStatus;
145
+ }
146
+ type PaymentErrorCode = 'BAD_TX' | 'TX_NOT_FOUND' | 'OUTPUT_MISSING' | 'NOT_SEEN' | 'REJECTED' | 'SPV_FAILED' | 'NETWORK' | 'BAD_EXPECTED';
147
+ declare class PaymentVerificationError extends Error {
148
+ readonly code: PaymentErrorCode;
149
+ readonly txid?: string | undefined;
150
+ constructor(code: PaymentErrorCode, message: string, txid?: string | undefined);
151
+ }
152
+ /**
153
+ * Throws PaymentVerificationError (with a `code`) unless every expected output is paid by a
154
+ * distinct output of the transaction with at least the expected satoshis.
155
+ */
156
+ declare function verifyPayment(o: VerifyPaymentOptions): Promise<VerifyPaymentResult>;
157
+
158
+ /**
159
+ * Link a proven identity key to a user in YOUR auth system (Supabase, next-auth, your own table).
160
+ * You supply the storage callbacks; this module only holds the rules.
161
+ */
162
+ interface LinkCallbacks {
163
+ /** The user id this identity key is linked to, or null. */
164
+ findUserByIdentityKey(identityKey: string): Promise<string | null> | string | null;
165
+ /** Save the link. Make (identity_key) unique in your table so a race cannot double-link. */
166
+ attach(userId: string, identityKey: string): Promise<void> | void;
167
+ }
168
+ type LinkResult = {
169
+ status: 'linked' | 'already-linked';
170
+ userId: string;
171
+ identityKey: string;
172
+ };
173
+ declare class IdentityLinkConflictError extends Error {
174
+ readonly identityKey: string;
175
+ readonly otherUserId: string;
176
+ readonly code = "IDENTITY_LINKED_TO_OTHER_USER";
177
+ constructor(identityKey: string, otherUserId: string);
178
+ }
179
+ /**
180
+ * Link `identityKey` (already verified with verifySignIn) to the signed-in `userId`.
181
+ * Throws IdentityLinkConflictError if it belongs to someone else. Idempotent for the same user.
182
+ */
183
+ declare function linkIdentity(userId: string, identityKey: string, cb: LinkCallbacks): Promise<LinkResult>;
184
+ interface SignInOrLinkOptions extends LinkCallbacks {
185
+ /** The user already signed in some other way (e.g. HandCash, email), if any. */
186
+ currentUserId?: string | null;
187
+ /** Make a new user for an unknown identity key. Without it, unknown keys return status 'unknown'. */
188
+ createUser?(identityKey: string): Promise<string> | string;
189
+ }
190
+ type SignInOrLinkResult = {
191
+ status: 'signed-in' | 'linked' | 'already-linked' | 'created';
192
+ userId: string;
193
+ identityKey: string;
194
+ } | {
195
+ status: 'unknown';
196
+ userId: null;
197
+ identityKey: string;
198
+ };
199
+ /**
200
+ * The usual verify-route logic: signed in already → link; known key → that user; else create (or report unknown).
201
+ * Throws IdentityLinkConflictError when a signed-in user tries to link a key owned by another user.
202
+ */
203
+ declare function signInOrLink(identityKey: string, o: SignInOrLinkOptions): Promise<SignInOrLinkResult>;
2
204
 
3
205
  /** Security level 2, a protocol name used for nothing else. Keep it constant. */
4
206
  declare const LOGIN_PROTOCOL: WalletProtocol;
@@ -61,4 +263,4 @@ declare function verifySignIn(opts: VerifySignInOptions): Promise<string | null>
61
263
  /** True when a request's Origin header is exactly your configured origin. Check it on both routes. */
62
264
  declare function originAllowed(requestOrigin: string | null | undefined, origin: string): boolean;
63
265
 
64
- export { CHALLENGE_LIFETIME_MS, type ChallengeResponse, type CreateChallengeOptions, LOGIN_PROTOCOL, type NonceStore, type PendingChallenge, type VerifySignInOptions, createChallenge, isIdentityKey, loginMessage, memoryStore, originAllowed, verifySignIn };
266
+ export { CHALLENGE_LIFETIME_MS, type ChallengeResponse, type CookieOptions, type CreateChallengeOptions, type ExpectedOutput, IdentityLinkConflictError, LOGIN_PROTOCOL, type LinkCallbacks, type LinkResult, MIN_SECRET_LENGTH, type NonceStore, PaymentVerificationError, type PendingChallenge, type PgQuery, type PgStoreOptions, type RedisLike, type RedisStoreOptions, type SessionPayload, type SignInOrLinkOptions, type SignInOrLinkResult, type SignSessionOptions, type TxStatus, type VerifyPaymentOptions, type VerifyPaymentResult, type VerifySignInOptions, createChallenge, isIdentityKey, linkIdentity, loginMessage, memoryStore, originAllowed, pgNonceTableSql, pgStore, readCookie, redisStore, serializeCookie, signInOrLink, signSession, verifyPayment, verifySession, verifySignIn };
package/dist/server.d.ts CHANGED
@@ -1,4 +1,206 @@
1
- import { WalletProtocol } from '@bsv/sdk';
1
+ import { ChainTracker, WalletProtocol } from '@bsv/sdk';
2
+
3
+ /**
4
+ * Shared NonceStore adapters. No database driver is imported: you pass your own client or query
5
+ * function. Every adapter makes `take` read-and-delete in one step, so each nonce works once.
6
+ */
7
+
8
+ /**
9
+ * The Redis commands used. Fits @upstash/redis and node-redis v4+ as they are
10
+ * (`set(key, value, { ex | EX })`, `getdel` / `getDel`). For ioredis pass `flavor: 'ioredis'`.
11
+ */
12
+ interface RedisLike {
13
+ set(key: string, value: string, ...args: unknown[]): Promise<unknown>;
14
+ getdel?(key: string): Promise<unknown>;
15
+ getDel?(key: string): Promise<unknown>;
16
+ }
17
+ interface RedisStoreOptions {
18
+ client: RedisLike;
19
+ /** Key prefix. Default 'bwalletx:nonce:'. */
20
+ prefix?: string;
21
+ /** 'ioredis' uses positional `set(key, value, 'EX', s)`. Default 'object' (Upstash, node-redis). */
22
+ flavor?: 'object' | 'ioredis';
23
+ }
24
+ /** Redis / Upstash / Vercel KV. Needs Redis 6.2+ for GETDEL. */
25
+ declare function redisStore(opts: RedisStoreOptions): NonceStore;
26
+ /** `query(sql, params)` resolving to `{ rows }` (node-postgres, @vercel/postgres, Neon) or a bare array (postgres.js `sql.unsafe`). */
27
+ type PgQuery = (sql: string, params: unknown[]) => Promise<{
28
+ rows: unknown[];
29
+ } | unknown[]>;
30
+ interface PgStoreOptions {
31
+ query: PgQuery;
32
+ /** Table name (letters, digits, _ and one optional schema dot). Default 'bwalletx_nonces'. */
33
+ table?: string;
34
+ }
35
+ /** Run this once (a migration) before using pgStore. */
36
+ declare function pgNonceTableSql(table?: string): string;
37
+ /** Postgres (e.g. self-hosted Supabase). Single-use via DELETE ... RETURNING. */
38
+ declare function pgStore(opts: PgStoreOptions): NonceStore;
39
+
40
+ /**
41
+ * Signed session tokens: base64url(JSON payload) + "." + base64url(HMAC-SHA256).
42
+ * WebCrypto only, so it runs in Node 20+, Vercel Edge and Cloudflare Workers.
43
+ * There is no default secret: pass one (32+ chars) or every call throws.
44
+ */
45
+ interface SessionPayload {
46
+ /** The proven identity key (lowercase hex). */
47
+ sub: string;
48
+ /** Issued at, seconds since the epoch. */
49
+ iat: number;
50
+ /** Expires at, seconds since the epoch. */
51
+ exp: number;
52
+ /** Your app's user id, if the identity key is linked to one. */
53
+ uid?: string;
54
+ handle?: string;
55
+ paymail?: string;
56
+ [claim: string]: unknown;
57
+ }
58
+ declare const MIN_SECRET_LENGTH = 32;
59
+ interface SignSessionOptions {
60
+ secret: string;
61
+ /** Lifetime in seconds. Default 7 days. */
62
+ maxAge?: number;
63
+ /** For tests. */
64
+ now?: number;
65
+ }
66
+ /** Sign a session. `claims.sub` (identity key) is required. */
67
+ declare function signSession(claims: Omit<SessionPayload, 'iat' | 'exp'> & Partial<Pick<SessionPayload, 'iat' | 'exp'>>, opts: SignSessionOptions): Promise<string>;
68
+ /** The payload if the token is authentic and unexpired, else null. Never throws on bad input (only on a missing secret). */
69
+ declare function verifySession(token: unknown, opts: {
70
+ secret: string;
71
+ now?: number;
72
+ }): Promise<SessionPayload | null>;
73
+ interface CookieOptions {
74
+ name: string;
75
+ maxAge: number;
76
+ secure?: boolean;
77
+ sameSite?: 'Lax' | 'Strict' | 'None';
78
+ path?: string;
79
+ domain?: string;
80
+ }
81
+ /** A Set-Cookie header value. Always HttpOnly. */
82
+ declare function serializeCookie(value: string, o: CookieOptions): string;
83
+ /** Read one cookie from a Cookie header. */
84
+ declare function readCookie(header: string | null | undefined, name: string): string | null;
85
+
86
+ /**
87
+ * Server side: check that a transaction really pays what you expected before you deliver anything.
88
+ * Read-only: it never broadcasts, signs or retries a spend.
89
+ */
90
+
91
+ interface ExpectedOutput {
92
+ /** Locking script hex. Give this or `address`. */
93
+ script?: string;
94
+ /** A P2PKH address. */
95
+ address?: string;
96
+ /** Minimum satoshis this output must carry. */
97
+ satoshis: number;
98
+ }
99
+ type TxStatus = {
100
+ state: 'confirmed';
101
+ confirmations: number;
102
+ source: 'woc' | 'arc';
103
+ } | {
104
+ state: 'mempool';
105
+ source: 'woc' | 'arc';
106
+ detail?: string;
107
+ } | {
108
+ state: 'not-found' | 'rejected';
109
+ source: 'woc' | 'arc';
110
+ detail?: string;
111
+ } | {
112
+ state: 'unchecked';
113
+ };
114
+ interface VerifyPaymentOptions {
115
+ /** A txid (fetched from WhatsOnChain), raw tx / BEEF / Atomic BEEF as hex, or BEEF bytes. */
116
+ tx: string | number[] | Uint8Array;
117
+ /** Every one must be matched by a different output. */
118
+ expected: ExpectedOutput[];
119
+ network?: 'main' | 'test';
120
+ /** Where to ask for status. Default 'woc'. 'none' skips the network (BEEF only). */
121
+ status?: 'woc' | 'arc' | 'none';
122
+ /** ARC base URL, e.g. https://arc.taal.com. Needed when status is 'arc'. */
123
+ arcUrl?: string;
124
+ /** Sent as Bearer. Read it from env; never hardcode. */
125
+ arcApiKey?: string;
126
+ /** Fail unless the network has seen the tx (mempool or confirmed). Default false. */
127
+ requireSeen?: boolean;
128
+ /** Minimum confirmations. Default 0. */
129
+ minConfirmations?: number;
130
+ /** Also run SPV (merkle proofs + scripts) on a BEEF with this chain tracker. */
131
+ spv?: ChainTracker;
132
+ /** Injected fetch (tests, custom agents). Default globalThis.fetch. */
133
+ fetch?: typeof fetch;
134
+ }
135
+ interface VerifyPaymentResult {
136
+ ok: true;
137
+ txid: string;
138
+ /** For each expected output, the index of the output that paid it. */
139
+ matched: {
140
+ vout: number;
141
+ satoshis: number;
142
+ script: string;
143
+ }[];
144
+ status: TxStatus;
145
+ }
146
+ type PaymentErrorCode = 'BAD_TX' | 'TX_NOT_FOUND' | 'OUTPUT_MISSING' | 'NOT_SEEN' | 'REJECTED' | 'SPV_FAILED' | 'NETWORK' | 'BAD_EXPECTED';
147
+ declare class PaymentVerificationError extends Error {
148
+ readonly code: PaymentErrorCode;
149
+ readonly txid?: string | undefined;
150
+ constructor(code: PaymentErrorCode, message: string, txid?: string | undefined);
151
+ }
152
+ /**
153
+ * Throws PaymentVerificationError (with a `code`) unless every expected output is paid by a
154
+ * distinct output of the transaction with at least the expected satoshis.
155
+ */
156
+ declare function verifyPayment(o: VerifyPaymentOptions): Promise<VerifyPaymentResult>;
157
+
158
+ /**
159
+ * Link a proven identity key to a user in YOUR auth system (Supabase, next-auth, your own table).
160
+ * You supply the storage callbacks; this module only holds the rules.
161
+ */
162
+ interface LinkCallbacks {
163
+ /** The user id this identity key is linked to, or null. */
164
+ findUserByIdentityKey(identityKey: string): Promise<string | null> | string | null;
165
+ /** Save the link. Make (identity_key) unique in your table so a race cannot double-link. */
166
+ attach(userId: string, identityKey: string): Promise<void> | void;
167
+ }
168
+ type LinkResult = {
169
+ status: 'linked' | 'already-linked';
170
+ userId: string;
171
+ identityKey: string;
172
+ };
173
+ declare class IdentityLinkConflictError extends Error {
174
+ readonly identityKey: string;
175
+ readonly otherUserId: string;
176
+ readonly code = "IDENTITY_LINKED_TO_OTHER_USER";
177
+ constructor(identityKey: string, otherUserId: string);
178
+ }
179
+ /**
180
+ * Link `identityKey` (already verified with verifySignIn) to the signed-in `userId`.
181
+ * Throws IdentityLinkConflictError if it belongs to someone else. Idempotent for the same user.
182
+ */
183
+ declare function linkIdentity(userId: string, identityKey: string, cb: LinkCallbacks): Promise<LinkResult>;
184
+ interface SignInOrLinkOptions extends LinkCallbacks {
185
+ /** The user already signed in some other way (e.g. HandCash, email), if any. */
186
+ currentUserId?: string | null;
187
+ /** Make a new user for an unknown identity key. Without it, unknown keys return status 'unknown'. */
188
+ createUser?(identityKey: string): Promise<string> | string;
189
+ }
190
+ type SignInOrLinkResult = {
191
+ status: 'signed-in' | 'linked' | 'already-linked' | 'created';
192
+ userId: string;
193
+ identityKey: string;
194
+ } | {
195
+ status: 'unknown';
196
+ userId: null;
197
+ identityKey: string;
198
+ };
199
+ /**
200
+ * The usual verify-route logic: signed in already → link; known key → that user; else create (or report unknown).
201
+ * Throws IdentityLinkConflictError when a signed-in user tries to link a key owned by another user.
202
+ */
203
+ declare function signInOrLink(identityKey: string, o: SignInOrLinkOptions): Promise<SignInOrLinkResult>;
2
204
 
3
205
  /** Security level 2, a protocol name used for nothing else. Keep it constant. */
4
206
  declare const LOGIN_PROTOCOL: WalletProtocol;
@@ -61,4 +263,4 @@ declare function verifySignIn(opts: VerifySignInOptions): Promise<string | null>
61
263
  /** True when a request's Origin header is exactly your configured origin. Check it on both routes. */
62
264
  declare function originAllowed(requestOrigin: string | null | undefined, origin: string): boolean;
63
265
 
64
- export { CHALLENGE_LIFETIME_MS, type ChallengeResponse, type CreateChallengeOptions, LOGIN_PROTOCOL, type NonceStore, type PendingChallenge, type VerifySignInOptions, createChallenge, isIdentityKey, loginMessage, memoryStore, originAllowed, verifySignIn };
266
+ export { CHALLENGE_LIFETIME_MS, type ChallengeResponse, type CookieOptions, type CreateChallengeOptions, type ExpectedOutput, IdentityLinkConflictError, LOGIN_PROTOCOL, type LinkCallbacks, type LinkResult, MIN_SECRET_LENGTH, type NonceStore, PaymentVerificationError, type PendingChallenge, type PgQuery, type PgStoreOptions, type RedisLike, type RedisStoreOptions, type SessionPayload, type SignInOrLinkOptions, type SignInOrLinkResult, type SignSessionOptions, type TxStatus, type VerifyPaymentOptions, type VerifyPaymentResult, type VerifySignInOptions, createChallenge, isIdentityKey, linkIdentity, loginMessage, memoryStore, originAllowed, pgNonceTableSql, pgStore, readCookie, redisStore, serializeCookie, signInOrLink, signSession, verifyPayment, verifySession, verifySignIn };
package/dist/server.js CHANGED
@@ -1,118 +1,47 @@
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
+ createChallenge,
8
+ isIdentityKey,
9
+ linkIdentity,
10
+ loginMessage,
11
+ memoryStore,
12
+ originAllowed,
13
+ pgNonceTableSql,
14
+ pgStore,
15
+ readCookie,
16
+ redisStore,
17
+ serializeCookie,
18
+ signInOrLink,
19
+ signSession,
20
+ verifyPayment,
21
+ verifySession,
22
+ verifySignIn
23
+ } from "./chunk-7NAKOJGZ.js";
108
24
  export {
109
25
  CHALLENGE_LIFETIME_MS,
26
+ IdentityLinkConflictError,
110
27
  LOGIN_PROTOCOL,
28
+ MIN_SECRET_LENGTH,
29
+ PaymentVerificationError,
111
30
  createChallenge,
112
31
  isIdentityKey,
32
+ linkIdentity,
113
33
  loginMessage,
114
34
  memoryStore,
115
35
  originAllowed,
36
+ pgNonceTableSql,
37
+ pgStore,
38
+ readCookie,
39
+ redisStore,
40
+ serializeCookie,
41
+ signInOrLink,
42
+ signSession,
43
+ verifyPayment,
44
+ verifySession,
116
45
  verifySignIn
117
46
  };
118
47
  //# 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":[]}