@xrpl-wallet-kit/auth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # @xrpl-wallet-kit/auth
2
+
3
+ Sign-In with Wallet helpers for XRPL Wallet Kit.
4
+
5
+ This package is intentionally split from `@xrpl-wallet-kit/core`: core signs messages, while auth handles nonce, message formatting, server verification, auth state, and sign-out. The main entry is client-safe. XRPL verifier dependencies live behind the `./verifiers` subpath and should only be imported on the server.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @xrpl-wallet-kit/auth @xrpl-wallet-kit/core
11
+ ```
12
+
13
+ For server-side XRPL verification, install the optional peers in your server package:
14
+
15
+ ```bash
16
+ npm install ripple-keypairs verify-xrpl-signature xrpl
17
+ ```
18
+
19
+ ## Client Usage
20
+
21
+ ```ts
22
+ import { createWalletAuth, formatAuthMessage } from "@xrpl-wallet-kit/auth";
23
+
24
+ const auth = createWalletAuth(manager, {
25
+ async getNonce() {
26
+ const res = await fetch("/api/auth/nonce", { credentials: "include" });
27
+ const body = await res.json();
28
+ return body.nonce;
29
+ },
30
+ createMessage(params) {
31
+ return formatAuthMessage(params);
32
+ },
33
+ async verify(params) {
34
+ const res = await fetch("/api/auth/verify", {
35
+ method: "POST",
36
+ credentials: "include",
37
+ headers: { "content-type": "application/json" },
38
+ body: JSON.stringify(params)
39
+ });
40
+ return res.ok;
41
+ },
42
+ async signOut() {
43
+ await fetch("/api/auth/signout", { method: "POST", credentials: "include" });
44
+ }
45
+ }, {
46
+ domain: window.location.host,
47
+ uri: window.location.origin,
48
+ chainId: "xrpl:0",
49
+ statement: "Sign in to Example App"
50
+ });
51
+
52
+ auth.on("change", (state) => {
53
+ console.log(state.status, state.address);
54
+ });
55
+
56
+ await auth.signIn();
57
+ ```
58
+
59
+ The dApp does not branch by wallet id. `manager.signMessage()` delegates to the active adapter and returns the normalized proof shape:
60
+
61
+ ```ts
62
+ {
63
+ signatureKind: "signature" | "signedTx",
64
+ proof: string,
65
+ signature?: string,
66
+ txBlob?: string,
67
+ publicKey?: string
68
+ }
69
+ ```
70
+
71
+ ## Pre-Issued Nonce
72
+
73
+ If your dApp already issued a nonce before the user clicks sign-in, return that nonce from `getNonce()`. This is common in SSR pages, legacy apps, or flows where the nonce is rendered into the page by the server.
74
+
75
+ ```ts
76
+ import { createWalletAuth, formatAuthMessage } from "@xrpl-wallet-kit/auth";
77
+
78
+ const preIssuedNonce = window.__AUTH_NONCE__;
79
+
80
+ const auth = createWalletAuth(manager, {
81
+ async getNonce() {
82
+ return preIssuedNonce;
83
+ },
84
+ createMessage(params) {
85
+ return formatAuthMessage(params);
86
+ },
87
+ async verify(params) {
88
+ const res = await fetch("/api/auth/verify", {
89
+ method: "POST",
90
+ credentials: "include",
91
+ headers: { "content-type": "application/json" },
92
+ body: JSON.stringify(params)
93
+ });
94
+ return res.ok;
95
+ }
96
+ }, {
97
+ domain: window.location.host,
98
+ uri: window.location.origin,
99
+ chainId: "xrpl:0",
100
+ statement: "Sign in to Example App"
101
+ });
102
+
103
+ await auth.signIn();
104
+ ```
105
+
106
+ The server must still generate, store, validate, and invalidate the nonce. Do not treat a browser-generated nonce as trusted authentication state.
107
+
108
+ ```js
109
+ app.get("/login", (req, res) => {
110
+ const nonce = createSecureNonce();
111
+ req.session.authNonce = nonce;
112
+ res.render("login", { authNonce: nonce });
113
+ });
114
+
115
+ app.post("/api/auth/verify", async (req, res) => {
116
+ const parsed = parseAuthMessage(req.body.message);
117
+
118
+ if (parsed.nonce !== req.session.authNonce) {
119
+ return res.status(400).json({ ok: false, error: "Invalid nonce" });
120
+ }
121
+
122
+ // Verify req.body proof here, then create the app session.
123
+ req.session.authNonce = null;
124
+ res.json({ ok: true });
125
+ });
126
+ ```
127
+
128
+ ## Server Verification
129
+
130
+ ```ts
131
+ import { createXrplSignatureVerifier } from "@xrpl-wallet-kit/auth/verifiers";
132
+
133
+ const verifier = createXrplSignatureVerifier({
134
+ nodeUrl: "wss://xrplcluster.com"
135
+ });
136
+
137
+ const ok = await verifier.verify({
138
+ address: body.address,
139
+ message: body.message,
140
+ signatureKind: body.signatureKind,
141
+ proof: body.proof,
142
+ signature: body.signature,
143
+ txBlob: body.txBlob,
144
+ publicKey: body.publicKey
145
+ });
146
+ ```
147
+
148
+ For `signatureKind: "signature"`, `publicKey` should be provided by the wallet/sign result. Ledger lookup via `account_info.account_data.PublicKey` is only a fallback. Do not use `RegularKey` as a public key.
149
+
150
+ For `signatureKind: "signedTx"`, the verifier checks the signed transaction blob, signer address, transaction `Account`, and first memo text against the original auth message.
151
+
152
+ ## Legacy HTML / jQuery Pattern
153
+
154
+ Legacy pages can use the same API from the browser bundle or ESM build. Keep the server verification endpoint separate:
155
+
156
+ ```js
157
+ async function signInWithWalletKit(manager) {
158
+ const auth = XRPLWalletKitAuth.createWalletAuth(manager, {
159
+ async getNonce() {
160
+ return (await fetch("/api/auth/nonce", { credentials: "include" }).then((r) => r.json())).nonce;
161
+ },
162
+ createMessage(params) {
163
+ return XRPLWalletKitAuth.formatAuthMessage(params);
164
+ },
165
+ async verify(params) {
166
+ const response = await fetch("/api/auth/verify", {
167
+ method: "POST",
168
+ credentials: "include",
169
+ headers: { "content-type": "application/json" },
170
+ body: JSON.stringify(params)
171
+ });
172
+ return response.ok;
173
+ }
174
+ }, {
175
+ domain: location.host,
176
+ uri: location.origin,
177
+ chainId: "xrpl:0"
178
+ });
179
+
180
+ return auth.signIn();
181
+ }
182
+ ```
183
+
184
+ Do not verify signatures in browser-only legacy code. The nonce must be generated, stored, validated, and invalidated by the server.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { WalletAuth, WalletAuthAdapter, WalletAuthManager, WalletAuthOptions } from "./types";
2
+ export declare function createWalletAuth(manager: WalletAuthManager, adapter: WalletAuthAdapter, options?: WalletAuthOptions): WalletAuth;
3
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAA2B,iBAAiB,EAAE,iBAAiB,EAA2C,MAAM,SAAS,CAAC;AAKrK,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,iBAAiB,EAC1B,OAAO,GAAE,iBAAsB,GAC9B,UAAU,CAEZ"}
package/dist/auth.js ADDED
@@ -0,0 +1,145 @@
1
+ import { WalletKitError, WalletKitErrorCode } from "@xrpl-wallet-kit/core";
2
+ import { isWalletAuthSignInResult } from "./types";
3
+ const DEFAULT_EXPIRES_IN_SECONDS = 3600;
4
+ export function createWalletAuth(manager, adapter, options = {}) {
5
+ return new WalletAuthController(manager, adapter, options);
6
+ }
7
+ class WalletAuthController {
8
+ constructor(manager, adapter, defaults) {
9
+ this.manager = manager;
10
+ this.adapter = adapter;
11
+ this.defaults = defaults;
12
+ this.state = {
13
+ status: "unauthenticated",
14
+ address: null,
15
+ error: null
16
+ };
17
+ this.listeners = new Set();
18
+ this.destroyed = false;
19
+ this.signing = false;
20
+ }
21
+ getState() {
22
+ return { ...this.state };
23
+ }
24
+ async signIn(options = {}) {
25
+ this.ensureActive();
26
+ if (this.signing)
27
+ throw new Error("A sign-in request is already in progress.");
28
+ const merged = { ...this.defaults, ...options };
29
+ const initialSession = this.manager.getSession();
30
+ const account = this.manager.getAccount();
31
+ if (!account)
32
+ throw new Error("No active wallet session. Connect a wallet first.");
33
+ if (!this.manager.getCapabilities()?.signMessage) {
34
+ throw new Error("Active wallet adapter does not support message signing.");
35
+ }
36
+ this.signing = true;
37
+ this.setState({ status: "loading", address: account.address, error: null });
38
+ try {
39
+ const nonce = await this.adapter.getNonce();
40
+ const issuedAt = new Date();
41
+ const expirationTime = new Date(issuedAt.getTime() + (merged.expiresIn ?? DEFAULT_EXPIRES_IN_SECONDS) * 1000);
42
+ const message = this.adapter.createMessage({
43
+ address: account.address,
44
+ nonce,
45
+ domain: merged.domain ?? resolveDefaultDomain(),
46
+ uri: merged.uri ?? resolveDefaultUri(),
47
+ chainId: merged.chainId,
48
+ statement: merged.statement,
49
+ issuedAt: issuedAt.toISOString(),
50
+ expirationTime: expirationTime.toISOString(),
51
+ version: "1"
52
+ });
53
+ if (this.manager.getSession() !== initialSession) {
54
+ throw new Error("Wallet session changed during sign-in.");
55
+ }
56
+ const signResult = await this.manager.signMessage({ message, account });
57
+ if (!isWalletAuthSignInResult(signResult)) {
58
+ throw new Error("Wallet did not return a verifiable signature proof.");
59
+ }
60
+ const verifyOk = await this.adapter.verify({
61
+ message,
62
+ signatureKind: signResult.signatureKind,
63
+ proof: signResult.proof,
64
+ signature: signResult.signature,
65
+ txBlob: signResult.txBlob,
66
+ address: account.address,
67
+ publicKey: signResult.publicKey,
68
+ raw: signResult.raw
69
+ });
70
+ if (!verifyOk)
71
+ throw new Error("Authentication rejected by server.");
72
+ const result = {
73
+ address: account.address,
74
+ message,
75
+ signatureKind: signResult.signatureKind,
76
+ proof: signResult.proof,
77
+ signature: signResult.signature,
78
+ txBlob: signResult.txBlob,
79
+ publicKey: signResult.publicKey,
80
+ raw: signResult.raw
81
+ };
82
+ this.setState({ status: "authenticated", address: account.address, error: null });
83
+ return result;
84
+ }
85
+ catch (error) {
86
+ this.setState({ status: "error", address: null, error });
87
+ throw normalizeAuthError(error);
88
+ }
89
+ finally {
90
+ this.signing = false;
91
+ }
92
+ }
93
+ async signOut() {
94
+ try {
95
+ await this.adapter.signOut?.();
96
+ }
97
+ finally {
98
+ this.setState({ status: "unauthenticated", address: null, error: null });
99
+ }
100
+ }
101
+ on(event, handler) {
102
+ this.ensureActive();
103
+ if (event !== "change")
104
+ return () => { };
105
+ this.listeners.add(handler);
106
+ return () => this.off(event, handler);
107
+ }
108
+ off(event, handler) {
109
+ if (event !== "change")
110
+ return;
111
+ this.listeners.delete(handler);
112
+ }
113
+ destroy() {
114
+ this.destroyed = true;
115
+ this.listeners.clear();
116
+ }
117
+ setState(state) {
118
+ this.state = state;
119
+ for (const listener of this.listeners)
120
+ listener(this.getState());
121
+ }
122
+ ensureActive() {
123
+ if (this.destroyed)
124
+ throw new Error("WalletAuth instance has been destroyed.");
125
+ }
126
+ }
127
+ function resolveDefaultDomain() {
128
+ if (typeof window !== "undefined" && window.location.host)
129
+ return window.location.host;
130
+ return "localhost";
131
+ }
132
+ function resolveDefaultUri() {
133
+ if (typeof window !== "undefined" && window.location.origin)
134
+ return window.location.origin;
135
+ return "http://localhost";
136
+ }
137
+ function normalizeAuthError(error) {
138
+ if (error instanceof WalletKitError)
139
+ return error;
140
+ const message = error instanceof Error ? error.message : String(error);
141
+ if (/reject|denied|cancelled|canceled|closed/i.test(message)) {
142
+ return new WalletKitError(WalletKitErrorCode.SIGN_REJECTED, "Signing request was rejected", { cause: error });
143
+ }
144
+ return error;
145
+ }
@@ -0,0 +1,5 @@
1
+ export { createWalletAuth } from "./auth";
2
+ export { formatAuthMessage, parseAuthMessage, validateAuthMessage } from "./message";
3
+ export { generateNonce } from "./nonce";
4
+ export type { ParsedWalletAuthMessage, SignatureVerifier, WalletAuth, WalletAuthAdapter, WalletAuthChangeHandler, WalletAuthManager, WalletAuthMessageParams, WalletAuthOptions, WalletAuthSignInResult, WalletAuthState, WalletAuthStatus, WalletAuthVerifyParams } from "./types";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,YAAY,EACV,uBAAuB,EACvB,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,uBAAuB,EACvB,iBAAiB,EACjB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EACf,gBAAgB,EAChB,sBAAsB,EACvB,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createWalletAuth } from "./auth";
2
+ export { formatAuthMessage, parseAuthMessage, validateAuthMessage } from "./message";
3
+ export { generateNonce } from "./nonce";
@@ -0,0 +1,18 @@
1
+ import type { ParsedWalletAuthMessage, WalletAuthMessageParams } from "./types";
2
+ export interface WalletAuthValidationOptions {
3
+ expectedDomain?: string;
4
+ expectedUri?: string;
5
+ expectedAddress?: string;
6
+ now?: Date;
7
+ maxAgeSeconds?: number;
8
+ isNonceUsed?: (nonce: string) => boolean | Promise<boolean>;
9
+ }
10
+ export interface WalletAuthValidationResult {
11
+ valid: boolean;
12
+ errors: string[];
13
+ message?: ParsedWalletAuthMessage;
14
+ }
15
+ export declare function formatAuthMessage(params: WalletAuthMessageParams): string;
16
+ export declare function parseAuthMessage(message: string): ParsedWalletAuthMessage;
17
+ export declare function validateAuthMessage(message: string, options?: WalletAuthValidationOptions): Promise<WalletAuthValidationResult>;
18
+ //# sourceMappingURL=message.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../src/message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEhF,MAAM,WAAW,2BAA2B;IAC1C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7D;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,uBAAuB,CAAC;CACnC;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,uBAAuB,GAAG,MAAM,CAsBzE;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,uBAAuB,CAsCzE;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,2BAAgC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAwCzI"}
@@ -0,0 +1,107 @@
1
+ export function formatAuthMessage(params) {
2
+ const lines = [
3
+ `${params.domain} wants you to sign in with your wallet:`,
4
+ params.address,
5
+ ""
6
+ ];
7
+ if (params.statement) {
8
+ lines.push(params.statement, "");
9
+ }
10
+ lines.push(`URI: ${params.uri}`, `Version: ${params.version}`, `Nonce: ${params.nonce}`, `Issued At: ${params.issuedAt}`);
11
+ if (params.chainId)
12
+ lines.push(`Chain ID: ${params.chainId}`);
13
+ if (params.expirationTime)
14
+ lines.push(`Expiration Time: ${params.expirationTime}`);
15
+ return lines.join("\n");
16
+ }
17
+ export function parseAuthMessage(message) {
18
+ const lines = message.split(/\r?\n/);
19
+ const header = lines[0] ?? "";
20
+ const domain = header.endsWith(" wants you to sign in with your wallet:")
21
+ ? header.slice(0, -" wants you to sign in with your wallet:".length)
22
+ : "";
23
+ const address = lines[1] ?? "";
24
+ const fields = new Map();
25
+ const statementLines = [];
26
+ let inStatement = false;
27
+ for (let index = 2; index < lines.length; index += 1) {
28
+ const line = lines[index] ?? "";
29
+ const fieldMatch = /^([A-Za-z ]+):\s*(.*)$/.exec(line);
30
+ if (fieldMatch && ["URI", "Version", "Nonce", "Issued At", "Chain ID", "Expiration Time"].includes(fieldMatch[1])) {
31
+ fields.set(fieldMatch[1], fieldMatch[2]);
32
+ inStatement = false;
33
+ continue;
34
+ }
35
+ if (line.length > 0 || inStatement) {
36
+ statementLines.push(line);
37
+ inStatement = true;
38
+ }
39
+ }
40
+ const statement = trimBlankLines(statementLines).join("\n") || undefined;
41
+ return {
42
+ domain,
43
+ address,
44
+ statement,
45
+ uri: fields.get("URI") ?? "",
46
+ version: fields.get("Version") ?? "",
47
+ nonce: fields.get("Nonce") ?? "",
48
+ issuedAt: fields.get("Issued At") ?? "",
49
+ chainId: fields.get("Chain ID") || undefined,
50
+ expirationTime: fields.get("Expiration Time") || undefined
51
+ };
52
+ }
53
+ export async function validateAuthMessage(message, options = {}) {
54
+ const parsed = parseAuthMessage(message);
55
+ const errors = [];
56
+ const now = options.now ?? new Date();
57
+ if (!parsed.domain)
58
+ errors.push("Missing or invalid domain.");
59
+ if (!parsed.address)
60
+ errors.push("Missing address.");
61
+ if (!parsed.uri)
62
+ errors.push("Missing URI.");
63
+ if (!parsed.version)
64
+ errors.push("Missing version.");
65
+ if (!parsed.nonce)
66
+ errors.push("Missing nonce.");
67
+ if (!parsed.issuedAt || Number.isNaN(Date.parse(parsed.issuedAt)))
68
+ errors.push("Missing or invalid issuedAt.");
69
+ if (options.expectedDomain && parsed.domain !== options.expectedDomain)
70
+ errors.push("Domain does not match.");
71
+ if (options.expectedUri && parsed.uri !== options.expectedUri)
72
+ errors.push("URI does not match.");
73
+ if (options.expectedAddress && parsed.address !== options.expectedAddress)
74
+ errors.push("Address does not match.");
75
+ if (parsed.expirationTime) {
76
+ const expiresAt = Date.parse(parsed.expirationTime);
77
+ if (Number.isNaN(expiresAt)) {
78
+ errors.push("Invalid expirationTime.");
79
+ }
80
+ else if (expiresAt <= now.getTime()) {
81
+ errors.push("Message has expired.");
82
+ }
83
+ }
84
+ if (parsed.issuedAt && !Number.isNaN(Date.parse(parsed.issuedAt)) && options.maxAgeSeconds !== undefined) {
85
+ const issuedAt = Date.parse(parsed.issuedAt);
86
+ if (now.getTime() - issuedAt > options.maxAgeSeconds * 1000) {
87
+ errors.push("Message is older than maxAgeSeconds.");
88
+ }
89
+ }
90
+ if (options.isNonceUsed && parsed.nonce && await options.isNonceUsed(parsed.nonce)) {
91
+ errors.push("Nonce has already been used.");
92
+ }
93
+ return {
94
+ valid: errors.length === 0,
95
+ errors,
96
+ message: parsed
97
+ };
98
+ }
99
+ function trimBlankLines(lines) {
100
+ let start = 0;
101
+ let end = lines.length;
102
+ while (start < end && lines[start] === "")
103
+ start += 1;
104
+ while (end > start && lines[end - 1] === "")
105
+ end -= 1;
106
+ return lines.slice(start, end);
107
+ }
@@ -0,0 +1,5 @@
1
+ export interface GenerateNonceOptions {
2
+ bytes?: number;
3
+ }
4
+ export declare function generateNonce(options?: GenerateNonceOptions): string;
5
+ //# sourceMappingURL=nonce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nonce.d.ts","sourceRoot":"","sources":["../src/nonce.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAexE"}
package/dist/nonce.js ADDED
@@ -0,0 +1,13 @@
1
+ export function generateNonce(options = {}) {
2
+ const byteLength = Math.max(16, options.bytes ?? 24);
3
+ const cryptoApi = globalThis.crypto;
4
+ if (cryptoApi?.randomUUID && byteLength <= 16) {
5
+ return cryptoApi.randomUUID().replace(/-/g, "");
6
+ }
7
+ if (!cryptoApi?.getRandomValues) {
8
+ throw new Error("Secure random nonce generation requires crypto.getRandomValues().");
9
+ }
10
+ const bytes = new Uint8Array(byteLength);
11
+ cryptoApi.getRandomValues(bytes);
12
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
13
+ }
@@ -0,0 +1,71 @@
1
+ import type { SignatureKind, SignMessageResult, WalletManager } from "@xrpl-wallet-kit/core";
2
+ export interface WalletAuthMessageParams {
3
+ address: string;
4
+ nonce: string;
5
+ domain: string;
6
+ uri: string;
7
+ chainId?: string;
8
+ statement?: string;
9
+ issuedAt: string;
10
+ expirationTime?: string;
11
+ version: string;
12
+ }
13
+ export interface ParsedWalletAuthMessage extends WalletAuthMessageParams {
14
+ }
15
+ export interface WalletAuthVerifyParams {
16
+ message: string;
17
+ signatureKind: SignatureKind;
18
+ proof: string;
19
+ signature?: string;
20
+ txBlob?: string;
21
+ address: string;
22
+ publicKey?: string;
23
+ raw?: unknown;
24
+ }
25
+ export interface WalletAuthAdapter {
26
+ getNonce(): Promise<string>;
27
+ createMessage(params: WalletAuthMessageParams): string;
28
+ verify(params: WalletAuthVerifyParams): Promise<boolean>;
29
+ signOut?(): Promise<void>;
30
+ }
31
+ export type WalletAuthStatus = "unauthenticated" | "loading" | "authenticated" | "error";
32
+ export interface WalletAuthState {
33
+ status: WalletAuthStatus;
34
+ address: string | null;
35
+ error: unknown | null;
36
+ }
37
+ export interface WalletAuthOptions {
38
+ domain?: string;
39
+ uri?: string;
40
+ chainId?: string;
41
+ statement?: string;
42
+ expiresIn?: number;
43
+ }
44
+ export interface WalletAuthSignInResult {
45
+ address: string;
46
+ message: string;
47
+ signatureKind: SignatureKind;
48
+ proof: string;
49
+ signature?: string;
50
+ txBlob?: string;
51
+ publicKey?: string;
52
+ raw?: unknown;
53
+ }
54
+ export type WalletAuthChangeHandler = (state: WalletAuthState) => void;
55
+ export interface WalletAuth {
56
+ getState(): WalletAuthState;
57
+ signIn(options?: WalletAuthOptions): Promise<WalletAuthSignInResult>;
58
+ signOut(): Promise<void>;
59
+ on(event: "change", handler: WalletAuthChangeHandler): () => void;
60
+ off(event: "change", handler: WalletAuthChangeHandler): void;
61
+ destroy(): void;
62
+ }
63
+ export interface SignatureVerifier {
64
+ verify(params: WalletAuthVerifyParams): Promise<boolean>;
65
+ }
66
+ export interface WalletAuthManager extends Pick<WalletManager, "getAccount" | "getCapabilities" | "getSession" | "signMessage"> {
67
+ }
68
+ export declare function isWalletAuthSignInResult(value: SignMessageResult): value is SignMessageResult & {
69
+ proof: string;
70
+ };
71
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ export function isWalletAuthSignInResult(value) {
2
+ return typeof value.proof === "string" && value.proof.trim().length > 0;
3
+ }
@@ -0,0 +1,3 @@
1
+ export { createXrplSignatureVerifier } from "./xrpl";
2
+ export type { XrplSignatureVerifierOptions } from "./xrpl";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/verifiers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,QAAQ,CAAC;AACrD,YAAY,EAAE,4BAA4B,EAAE,MAAM,QAAQ,CAAC"}
@@ -0,0 +1 @@
1
+ export { createXrplSignatureVerifier } from "./xrpl";
@@ -0,0 +1,54 @@
1
+ import type { SignatureVerifier } from "../types";
2
+ interface RippleKeypairsModule {
3
+ verify(messageHex: string, signature: string, publicKey: string): boolean;
4
+ deriveAddress(publicKey: string): string;
5
+ }
6
+ interface VerifyXrplSignatureModule {
7
+ verifySignature(txBlob: string): {
8
+ signatureValid?: boolean;
9
+ signedBy?: string;
10
+ } | boolean;
11
+ }
12
+ interface XrplModule {
13
+ decode(txBlob: string): XrplDecodedTransaction;
14
+ Client?: new (url: string) => XrplClient;
15
+ }
16
+ interface XrplClient {
17
+ connect(): Promise<void>;
18
+ disconnect(): Promise<void>;
19
+ request(request: {
20
+ command: "account_info";
21
+ account: string;
22
+ }): Promise<{
23
+ result?: {
24
+ account_data?: {
25
+ PublicKey?: string;
26
+ };
27
+ };
28
+ account_data?: {
29
+ PublicKey?: string;
30
+ };
31
+ }>;
32
+ }
33
+ interface XrplDecodedTransaction {
34
+ Account?: string;
35
+ Memos?: Array<{
36
+ Memo?: {
37
+ MemoData?: string;
38
+ };
39
+ }>;
40
+ }
41
+ export interface XrplSignatureVerifierOptions {
42
+ nodeUrl?: string;
43
+ hashMessage?: (message: string) => string;
44
+ nodeTimeout?: number;
45
+ dependencies?: {
46
+ rippleKeypairs?: RippleKeypairsModule;
47
+ verifyXrplSignature?: VerifyXrplSignatureModule;
48
+ xrpl?: XrplModule;
49
+ loadPeer?: <T>(name: string) => Promise<T>;
50
+ };
51
+ }
52
+ export declare function createXrplSignatureVerifier(options?: XrplSignatureVerifierOptions): SignatureVerifier;
53
+ export {};
54
+ //# sourceMappingURL=xrpl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xrpl.d.ts","sourceRoot":"","sources":["../../src/verifiers/xrpl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAA0B,MAAM,UAAU,CAAC;AAE1E,UAAU,oBAAoB;IAC5B,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1E,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1C;AAED,UAAU,yBAAyB;IACjC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG;QAAE,cAAc,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;CAC5F;AAED,UAAU,UAAU;IAClB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,sBAAsB,CAAC;IAC/C,MAAM,CAAC,EAAE,KAAK,GAAG,EAAE,MAAM,KAAK,UAAU,CAAC;CAC1C;AAED,UAAU,UAAU;IAClB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,cAAc,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,MAAM,CAAC,EAAE;YAAE,YAAY,CAAC,EAAE;gBAAE,SAAS,CAAC,EAAE,MAAM,CAAA;aAAE,CAAA;SAAE,CAAC;QAAC,YAAY,CAAC,EAAE;YAAE,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;CACxK;AAED,UAAU,sBAAsB;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,4BAA4B;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE;QACb,cAAc,CAAC,EAAE,oBAAoB,CAAC;QACtC,mBAAmB,CAAC,EAAE,yBAAyB,CAAC;QAChD,IAAI,CAAC,EAAE,UAAU,CAAC;QAClB,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;KAC5C,CAAC;CACH;AAID,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,4BAAiC,GAAG,iBAAiB,CAOzG"}
@@ -0,0 +1,111 @@
1
+ const PEER_ERROR = "Install ripple-keypairs, verify-xrpl-signature, and xrpl to use @xrpl-wallet-kit/auth/verifiers.";
2
+ export function createXrplSignatureVerifier(options = {}) {
3
+ return {
4
+ async verify(params) {
5
+ if (params.signatureKind === "signedTx")
6
+ return verifySignedTransaction(params, options);
7
+ return verifyCompactSignature(params, options);
8
+ }
9
+ };
10
+ }
11
+ async function verifyCompactSignature(params, options) {
12
+ const signature = params.signature ?? params.proof;
13
+ if (!signature)
14
+ return false;
15
+ const rippleKeypairs = options.dependencies?.rippleKeypairs ?? await loadPeer("ripple-keypairs", options);
16
+ const publicKey = params.publicKey ?? await resolveLedgerPublicKey(params.address, options);
17
+ if (!publicKey)
18
+ throw new Error("Cannot resolve public key for address.");
19
+ const messageHex = options.hashMessage ? options.hashMessage(params.message) : utf8ToHex(params.message);
20
+ try {
21
+ if (!rippleKeypairs.verify(messageHex, signature, publicKey))
22
+ return false;
23
+ return rippleKeypairs.deriveAddress(publicKey) === params.address;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ async function verifySignedTransaction(params, options) {
30
+ const txBlob = params.txBlob ?? params.proof;
31
+ if (!txBlob)
32
+ return false;
33
+ const verifyModule = options.dependencies?.verifyXrplSignature ?? await loadPeer("verify-xrpl-signature", options);
34
+ const xrpl = options.dependencies?.xrpl ?? await loadPeer("xrpl", options);
35
+ const verifyResult = verifyModule.verifySignature(txBlob);
36
+ if (typeof verifyResult === "boolean") {
37
+ if (!verifyResult)
38
+ return false;
39
+ }
40
+ else {
41
+ if (verifyResult.signatureValid === false)
42
+ return false;
43
+ if (verifyResult.signedBy && verifyResult.signedBy !== params.address)
44
+ return false;
45
+ }
46
+ const tx = xrpl.decode(txBlob);
47
+ if (tx.Account !== params.address)
48
+ return false;
49
+ return extractFirstMemoText(tx) === params.message;
50
+ }
51
+ async function resolveLedgerPublicKey(address, options) {
52
+ if (!options.nodeUrl)
53
+ return undefined;
54
+ const xrpl = options.dependencies?.xrpl ?? await loadPeer("xrpl", options);
55
+ if (!xrpl.Client)
56
+ throw new Error(PEER_ERROR);
57
+ const client = new xrpl.Client(options.nodeUrl);
58
+ try {
59
+ await withTimeout(client.connect(), options.nodeTimeout ?? 5000);
60
+ const response = await withTimeout(client.request({ command: "account_info", account: address }), options.nodeTimeout ?? 5000);
61
+ return response.result?.account_data?.PublicKey ?? response.account_data?.PublicKey;
62
+ }
63
+ finally {
64
+ await client.disconnect().catch(() => undefined);
65
+ }
66
+ }
67
+ async function loadPeer(name, options) {
68
+ try {
69
+ if (options.dependencies?.loadPeer)
70
+ return await options.dependencies.loadPeer(name);
71
+ const mod = await import(name);
72
+ const maybeDefault = mod;
73
+ return (maybeDefault.default && typeof maybeDefault.default === "object" ? maybeDefault.default : mod);
74
+ }
75
+ catch (error) {
76
+ const peerError = new Error(PEER_ERROR);
77
+ peerError.cause = error;
78
+ throw peerError;
79
+ }
80
+ }
81
+ function utf8ToHex(value) {
82
+ const bytes = new TextEncoder().encode(value);
83
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("").toUpperCase();
84
+ }
85
+ function hexToUtf8(value) {
86
+ const normalized = value.length % 2 === 0 ? value : `0${value}`;
87
+ const bytes = new Uint8Array(normalized.length / 2);
88
+ for (let index = 0; index < bytes.length; index += 1) {
89
+ bytes[index] = Number.parseInt(normalized.slice(index * 2, index * 2 + 2), 16);
90
+ }
91
+ return new TextDecoder().decode(bytes);
92
+ }
93
+ function extractFirstMemoText(tx) {
94
+ const memoData = tx.Memos?.[0]?.Memo?.MemoData;
95
+ return memoData ? hexToUtf8(memoData) : undefined;
96
+ }
97
+ async function withTimeout(promise, timeoutMs) {
98
+ let timer;
99
+ try {
100
+ return await Promise.race([
101
+ promise,
102
+ new Promise((_, reject) => {
103
+ timer = setTimeout(() => reject(new Error(`XRPL request timed out after ${timeoutMs}ms.`)), timeoutMs);
104
+ })
105
+ ]);
106
+ }
107
+ finally {
108
+ if (timer)
109
+ clearTimeout(timer);
110
+ }
111
+ }
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@xrpl-wallet-kit/auth",
3
+ "version": "0.1.0",
4
+ "description": "Sign-In with Wallet helpers for XRPL Wallet Kit.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./verifiers": {
16
+ "types": "./dist/verifiers/index.d.ts",
17
+ "import": "./dist/verifiers/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json"
26
+ },
27
+ "dependencies": {
28
+ "@xrpl-wallet-kit/core": "0.1.0"
29
+ },
30
+ "peerDependencies": {
31
+ "ripple-keypairs": "^2.0.0",
32
+ "verify-xrpl-signature": "^9.2.0",
33
+ "xrpl": "^4.0.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "ripple-keypairs": {
37
+ "optional": true
38
+ },
39
+ "verify-xrpl-signature": {
40
+ "optional": true
41
+ },
42
+ "xrpl": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "devDependencies": {
47
+ "ripple-keypairs": "^2.0.0",
48
+ "verify-xrpl-signature": "^9.2.0",
49
+ "xrpl": "^4.0.0"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/XRPDomains/xrpl-wallet-kit.git"
57
+ },
58
+ "homepage": "https://github.com/XRPDomains/xrpl-wallet-kit#readme",
59
+ "bugs": {
60
+ "url": "https://github.com/XRPDomains/xrpl-wallet-kit/issues"
61
+ },
62
+ "keywords": [
63
+ "xrpl",
64
+ "wallet",
65
+ "wallet-auth",
66
+ "sign-in",
67
+ "web3"
68
+ ]
69
+ }