@bwalletx/connect 0.0.0-stage → 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.
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ CHALLENGE_LIFETIME_MS: () => CHALLENGE_LIFETIME_MS,
24
+ LOGIN_PROTOCOL: () => LOGIN_PROTOCOL,
25
+ createChallenge: () => createChallenge,
26
+ isIdentityKey: () => isIdentityKey,
27
+ loginMessage: () => loginMessage,
28
+ memoryStore: () => memoryStore,
29
+ originAllowed: () => originAllowed,
30
+ verifySignIn: () => verifySignIn
31
+ });
32
+ module.exports = __toCommonJS(server_exports);
33
+ var import_sdk = require("@bsv/sdk");
34
+ var LOGIN_PROTOCOL = [2, "bwallet sign in"];
35
+ var CHALLENGE_LIFETIME_MS = 2 * 60 * 1e3;
36
+ var warned = false;
37
+ function memoryStore(opts = {}) {
38
+ if (!opts.quiet && !warned) {
39
+ warned = true;
40
+ console.warn("[@bwalletx/connect] Using the in-memory nonce store. It is for development only; pass a shared store (Redis, database) in production.");
41
+ }
42
+ const m = /* @__PURE__ */ new Map();
43
+ return {
44
+ put(nonce, c) {
45
+ const now = Date.now();
46
+ for (const [k, v] of m) if (v.expiresAt < now) m.delete(k);
47
+ m.set(nonce, c);
48
+ },
49
+ take(nonce) {
50
+ const c = m.get(nonce) ?? null;
51
+ m.delete(nonce);
52
+ return c;
53
+ }
54
+ };
55
+ }
56
+ var defaultStore = null;
57
+ var storeOrDefault = (s) => s ?? (defaultStore ??= memoryStore());
58
+ function isIdentityKey(x) {
59
+ if (typeof x !== "string" || !/^0[23][0-9a-fA-F]{64}$/.test(x)) return false;
60
+ try {
61
+ import_sdk.PublicKey.fromString(x);
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+ function normalOrigin(origin) {
68
+ const u = new URL(origin);
69
+ if (u.origin === "null" || u.origin !== origin.replace(/\/$/, "")) {
70
+ throw new Error(`origin must be scheme://host[:port], got ${origin}`);
71
+ }
72
+ return u.origin;
73
+ }
74
+ function loginMessage(origin, nonce, expiresAt) {
75
+ return [
76
+ `Sign in to ${new URL(origin).host} with bWallet`,
77
+ "",
78
+ "This proves you hold this wallet. It does not spend anything.",
79
+ "",
80
+ `Origin: ${origin}`,
81
+ `Nonce: ${nonce}`,
82
+ `Expires: ${new Date(expiresAt).toISOString()}`
83
+ ].join("\n");
84
+ }
85
+ function randomNonce() {
86
+ const b = new Uint8Array(16);
87
+ globalThis.crypto.getRandomValues(b);
88
+ let s = "";
89
+ for (const x of b) s += String.fromCharCode(x);
90
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
91
+ }
92
+ async function createChallenge(opts) {
93
+ if (!isIdentityKey(opts.identityKey)) throw new Error("identityKey must be a compressed public key (66 hex chars)");
94
+ const origin = normalOrigin(opts.origin);
95
+ const nonce = randomNonce();
96
+ const expiresAt = Date.now() + CHALLENGE_LIFETIME_MS;
97
+ const message = loginMessage(origin, nonce, expiresAt);
98
+ await storeOrDefault(opts.store).put(nonce, { identityKey: opts.identityKey.toLowerCase(), origin, message, expiresAt });
99
+ return { nonce, message, protocolID: LOGIN_PROTOCOL, keyID: nonce, expiresAt };
100
+ }
101
+ async function verifySignIn(opts) {
102
+ if (typeof opts.nonce !== "string" || opts.nonce.length > 64) return null;
103
+ const c = await storeOrDefault(opts.store).take(opts.nonce);
104
+ if (!c || Date.now() > c.expiresAt) return null;
105
+ let origin;
106
+ try {
107
+ origin = normalOrigin(opts.origin);
108
+ } catch {
109
+ return null;
110
+ }
111
+ if (c.origin !== origin) return null;
112
+ if (!isIdentityKey(opts.identityKey)) return null;
113
+ const identityKey = opts.identityKey.toLowerCase();
114
+ if (c.identityKey !== identityKey) return null;
115
+ const sig = opts.signature;
116
+ if (!Array.isArray(sig) || sig.length < 8 || sig.length > 80 || !sig.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)) {
117
+ return null;
118
+ }
119
+ try {
120
+ const { valid } = await new import_sdk.ProtoWallet("anyone").verifySignature({
121
+ protocolID: LOGIN_PROTOCOL,
122
+ keyID: opts.nonce,
123
+ counterparty: identityKey,
124
+ data: Array.from(new TextEncoder().encode(c.message)),
125
+ signature: sig
126
+ });
127
+ return valid ? identityKey : null;
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+ function originAllowed(requestOrigin, origin) {
133
+ try {
134
+ return !!requestOrigin && requestOrigin === normalOrigin(origin);
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+ // Annotate the CommonJS export names for ESM import in node:
140
+ 0 && (module.exports = {
141
+ CHALLENGE_LIFETIME_MS,
142
+ LOGIN_PROTOCOL,
143
+ createChallenge,
144
+ isIdentityKey,
145
+ loginMessage,
146
+ memoryStore,
147
+ originAllowed,
148
+ verifySignIn
149
+ });
150
+ //# sourceMappingURL=server.cjs.map
@@ -0,0 +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":[]}
@@ -0,0 +1,64 @@
1
+ import { WalletProtocol } from '@bsv/sdk';
2
+
3
+ /** Security level 2, a protocol name used for nothing else. Keep it constant. */
4
+ declare const LOGIN_PROTOCOL: WalletProtocol;
5
+ declare const CHALLENGE_LIFETIME_MS: number;
6
+ interface PendingChallenge {
7
+ identityKey: string;
8
+ origin: string;
9
+ message: string;
10
+ expiresAt: number;
11
+ }
12
+ /**
13
+ * Where challenges wait between the two routes. Use Redis or a database table in production.
14
+ * `take` must read and delete in one step, so each nonce can be tried once.
15
+ */
16
+ interface NonceStore {
17
+ put(nonce: string, c: PendingChallenge): Promise<void> | void;
18
+ take(nonce: string): Promise<PendingChallenge | null> | PendingChallenge | null;
19
+ }
20
+ /**
21
+ * In-memory store. For development and tests only: it is lost on restart and not shared between
22
+ * server processes or serverless instances. Expired entries are pruned on each put.
23
+ */
24
+ declare function memoryStore(opts?: {
25
+ quiet?: boolean;
26
+ }): NonceStore;
27
+ declare function isIdentityKey(x: unknown): x is string;
28
+ /** The text the user signs. Lines joined with \n, no trailing newline. */
29
+ declare function loginMessage(origin: string, nonce: string, expiresAt: number): string;
30
+ interface CreateChallengeOptions {
31
+ /** YOUR site's origin from config, e.g. "https://example.com". Never take it from the request. */
32
+ origin: string;
33
+ /** The identity key the browser claims. Checked again at verify. */
34
+ identityKey: unknown;
35
+ /** Default: a process-wide in-memory store (dev only). */
36
+ store?: NonceStore;
37
+ }
38
+ interface ChallengeResponse {
39
+ nonce: string;
40
+ message: string;
41
+ protocolID: WalletProtocol;
42
+ keyID: string;
43
+ expiresAt: number;
44
+ }
45
+ /** Step 1. Throws if identityKey is not a valid compressed public key. Send the result to the browser. */
46
+ declare function createChallenge(opts: CreateChallengeOptions): Promise<ChallengeResponse>;
47
+ interface VerifySignInOptions {
48
+ /** YOUR site's origin from config. Must equal the origin the challenge was made for. */
49
+ origin: string;
50
+ /** From the request body. */
51
+ identityKey: unknown;
52
+ nonce: unknown;
53
+ signature: unknown;
54
+ store?: NonceStore;
55
+ }
56
+ /**
57
+ * Step 2. Returns the proven identity key (lowercase hex), or null.
58
+ * The nonce is deleted before anything is checked, so each challenge can be tried once.
59
+ */
60
+ declare function verifySignIn(opts: VerifySignInOptions): Promise<string | null>;
61
+ /** True when a request's Origin header is exactly your configured origin. Check it on both routes. */
62
+ declare function originAllowed(requestOrigin: string | null | undefined, origin: string): boolean;
63
+
64
+ export { CHALLENGE_LIFETIME_MS, type ChallengeResponse, type CreateChallengeOptions, LOGIN_PROTOCOL, type NonceStore, type PendingChallenge, type VerifySignInOptions, createChallenge, isIdentityKey, loginMessage, memoryStore, originAllowed, verifySignIn };
@@ -0,0 +1,64 @@
1
+ import { WalletProtocol } from '@bsv/sdk';
2
+
3
+ /** Security level 2, a protocol name used for nothing else. Keep it constant. */
4
+ declare const LOGIN_PROTOCOL: WalletProtocol;
5
+ declare const CHALLENGE_LIFETIME_MS: number;
6
+ interface PendingChallenge {
7
+ identityKey: string;
8
+ origin: string;
9
+ message: string;
10
+ expiresAt: number;
11
+ }
12
+ /**
13
+ * Where challenges wait between the two routes. Use Redis or a database table in production.
14
+ * `take` must read and delete in one step, so each nonce can be tried once.
15
+ */
16
+ interface NonceStore {
17
+ put(nonce: string, c: PendingChallenge): Promise<void> | void;
18
+ take(nonce: string): Promise<PendingChallenge | null> | PendingChallenge | null;
19
+ }
20
+ /**
21
+ * In-memory store. For development and tests only: it is lost on restart and not shared between
22
+ * server processes or serverless instances. Expired entries are pruned on each put.
23
+ */
24
+ declare function memoryStore(opts?: {
25
+ quiet?: boolean;
26
+ }): NonceStore;
27
+ declare function isIdentityKey(x: unknown): x is string;
28
+ /** The text the user signs. Lines joined with \n, no trailing newline. */
29
+ declare function loginMessage(origin: string, nonce: string, expiresAt: number): string;
30
+ interface CreateChallengeOptions {
31
+ /** YOUR site's origin from config, e.g. "https://example.com". Never take it from the request. */
32
+ origin: string;
33
+ /** The identity key the browser claims. Checked again at verify. */
34
+ identityKey: unknown;
35
+ /** Default: a process-wide in-memory store (dev only). */
36
+ store?: NonceStore;
37
+ }
38
+ interface ChallengeResponse {
39
+ nonce: string;
40
+ message: string;
41
+ protocolID: WalletProtocol;
42
+ keyID: string;
43
+ expiresAt: number;
44
+ }
45
+ /** Step 1. Throws if identityKey is not a valid compressed public key. Send the result to the browser. */
46
+ declare function createChallenge(opts: CreateChallengeOptions): Promise<ChallengeResponse>;
47
+ interface VerifySignInOptions {
48
+ /** YOUR site's origin from config. Must equal the origin the challenge was made for. */
49
+ origin: string;
50
+ /** From the request body. */
51
+ identityKey: unknown;
52
+ nonce: unknown;
53
+ signature: unknown;
54
+ store?: NonceStore;
55
+ }
56
+ /**
57
+ * Step 2. Returns the proven identity key (lowercase hex), or null.
58
+ * The nonce is deleted before anything is checked, so each challenge can be tried once.
59
+ */
60
+ declare function verifySignIn(opts: VerifySignInOptions): Promise<string | null>;
61
+ /** True when a request's Origin header is exactly your configured origin. Check it on both routes. */
62
+ declare function originAllowed(requestOrigin: string | null | undefined, origin: string): boolean;
63
+
64
+ export { CHALLENGE_LIFETIME_MS, type ChallengeResponse, type CreateChallengeOptions, LOGIN_PROTOCOL, type NonceStore, type PendingChallenge, type VerifySignInOptions, createChallenge, isIdentityKey, loginMessage, memoryStore, originAllowed, verifySignIn };
package/dist/server.js ADDED
@@ -0,0 +1,118 @@
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
+ }
108
+ export {
109
+ CHALLENGE_LIFETIME_MS,
110
+ LOGIN_PROTOCOL,
111
+ createChallenge,
112
+ isIdentityKey,
113
+ loginMessage,
114
+ memoryStore,
115
+ originAllowed,
116
+ verifySignIn
117
+ };
118
+ //# sourceMappingURL=server.js.map
@@ -0,0 +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":[]}
package/package.json CHANGED
@@ -1,6 +1,72 @@
1
1
  {
2
2
  "name": "@bwalletx/connect",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Sign in with bWalletX: wallet discovery, phone pairing and server-side challenge verification",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "require": {
18
+ "types": "./dist/index.d.cts",
19
+ "default": "./dist/index.cjs"
20
+ }
21
+ },
22
+ "./server": {
23
+ "import": {
24
+ "types": "./dist/server.d.ts",
25
+ "default": "./dist/server.js"
26
+ },
27
+ "require": {
28
+ "types": "./dist/server.d.cts",
29
+ "default": "./dist/server.cjs"
30
+ }
31
+ },
32
+ "./browser": "./dist/bwalletx-connect.global.js",
33
+ "./package.json": "./package.json"
34
+ },
35
+ "unpkg": "./dist/bwalletx-connect.global.js",
36
+ "jsdelivr": "./dist/bwalletx-connect.global.js",
37
+ "files": [
38
+ "dist",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "keywords": [
43
+ "bsv",
44
+ "brc-100",
45
+ "wallet",
46
+ "bwalletx",
47
+ "sign-in",
48
+ "authentication"
49
+ ],
50
+ "engines": {
51
+ "node": ">=20"
52
+ },
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "typecheck": "tsc --noEmit",
56
+ "test": "vitest run",
57
+ "test:live": "LIVE_RELAY=1 vitest run test/live-relay.test.ts",
58
+ "prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
59
+ },
60
+ "peerDependencies": {
61
+ "@bsv/sdk": ">=2.2.0 <4"
62
+ },
63
+ "devDependencies": {
64
+ "@bsv/sdk": "^2.8.11",
65
+ "@types/node": "^26.6.4",
66
+ "@types/ws": "^8.18.2",
67
+ "tsup": "^8.5.1",
68
+ "typescript": "^5.9.3",
69
+ "vitest": "^5.0.3",
70
+ "ws": "^8.22.0"
71
+ }
72
+ }