@bananalink-sdk/protocol 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +604 -0
  2. package/dist/chunk-32OWUOZ3.js +308 -0
  3. package/dist/chunk-32OWUOZ3.js.map +1 -0
  4. package/dist/chunk-65HNHRJK.cjs +123 -0
  5. package/dist/chunk-65HNHRJK.cjs.map +1 -0
  6. package/dist/chunk-7KYDLL3B.js +480 -0
  7. package/dist/chunk-7KYDLL3B.js.map +1 -0
  8. package/dist/chunk-A6FLEJ7R.cjs +62 -0
  9. package/dist/chunk-A6FLEJ7R.cjs.map +1 -0
  10. package/dist/chunk-CUJK7ZTS.js +217 -0
  11. package/dist/chunk-CUJK7ZTS.js.map +1 -0
  12. package/dist/chunk-GI3BUPIH.cjs +236 -0
  13. package/dist/chunk-GI3BUPIH.cjs.map +1 -0
  14. package/dist/chunk-JXHV66Q4.js +106 -0
  15. package/dist/chunk-JXHV66Q4.js.map +1 -0
  16. package/dist/chunk-KNGZKGRS.cjs +552 -0
  17. package/dist/chunk-KNGZKGRS.cjs.map +1 -0
  18. package/dist/chunk-LELPCIE7.js +840 -0
  19. package/dist/chunk-LELPCIE7.js.map +1 -0
  20. package/dist/chunk-MCZG7QEM.cjs +310 -0
  21. package/dist/chunk-MCZG7QEM.cjs.map +1 -0
  22. package/dist/chunk-TCVKC227.js +56 -0
  23. package/dist/chunk-TCVKC227.js.map +1 -0
  24. package/dist/chunk-VXLUSU5B.cjs +856 -0
  25. package/dist/chunk-VXLUSU5B.cjs.map +1 -0
  26. package/dist/chunk-WCQVDF3K.js +12 -0
  27. package/dist/chunk-WCQVDF3K.js.map +1 -0
  28. package/dist/chunk-WGEGR3DF.cjs +15 -0
  29. package/dist/chunk-WGEGR3DF.cjs.map +1 -0
  30. package/dist/client-session-claim-3QF3noOr.d.ts +197 -0
  31. package/dist/client-session-claim-C4lUik3b.d.cts +197 -0
  32. package/dist/core-DMhuNfoz.d.cts +62 -0
  33. package/dist/core-DMhuNfoz.d.ts +62 -0
  34. package/dist/crypto/providers/noble-provider.cjs +14 -0
  35. package/dist/crypto/providers/noble-provider.cjs.map +1 -0
  36. package/dist/crypto/providers/noble-provider.d.cts +30 -0
  37. package/dist/crypto/providers/noble-provider.d.ts +30 -0
  38. package/dist/crypto/providers/noble-provider.js +5 -0
  39. package/dist/crypto/providers/noble-provider.js.map +1 -0
  40. package/dist/crypto/providers/node-provider.cjs +308 -0
  41. package/dist/crypto/providers/node-provider.cjs.map +1 -0
  42. package/dist/crypto/providers/node-provider.d.cts +32 -0
  43. package/dist/crypto/providers/node-provider.d.ts +32 -0
  44. package/dist/crypto/providers/node-provider.js +306 -0
  45. package/dist/crypto/providers/node-provider.js.map +1 -0
  46. package/dist/crypto/providers/quickcrypto-provider.cjs +339 -0
  47. package/dist/crypto/providers/quickcrypto-provider.cjs.map +1 -0
  48. package/dist/crypto/providers/quickcrypto-provider.d.cts +34 -0
  49. package/dist/crypto/providers/quickcrypto-provider.d.ts +34 -0
  50. package/dist/crypto/providers/quickcrypto-provider.js +337 -0
  51. package/dist/crypto/providers/quickcrypto-provider.js.map +1 -0
  52. package/dist/crypto/providers/webcrypto-provider.cjs +310 -0
  53. package/dist/crypto/providers/webcrypto-provider.cjs.map +1 -0
  54. package/dist/crypto/providers/webcrypto-provider.d.cts +30 -0
  55. package/dist/crypto/providers/webcrypto-provider.d.ts +30 -0
  56. package/dist/crypto/providers/webcrypto-provider.js +308 -0
  57. package/dist/crypto/providers/webcrypto-provider.js.map +1 -0
  58. package/dist/crypto-BUS06Qz-.d.cts +40 -0
  59. package/dist/crypto-BUS06Qz-.d.ts +40 -0
  60. package/dist/crypto-export.cjs +790 -0
  61. package/dist/crypto-export.cjs.map +1 -0
  62. package/dist/crypto-export.d.cts +257 -0
  63. package/dist/crypto-export.d.ts +257 -0
  64. package/dist/crypto-export.js +709 -0
  65. package/dist/crypto-export.js.map +1 -0
  66. package/dist/crypto-provider-deYoVIxi.d.cts +36 -0
  67. package/dist/crypto-provider-deYoVIxi.d.ts +36 -0
  68. package/dist/index.cjs +615 -0
  69. package/dist/index.cjs.map +1 -0
  70. package/dist/index.d.cts +379 -0
  71. package/dist/index.d.ts +379 -0
  72. package/dist/index.js +504 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/schemas-export.cjs +294 -0
  75. package/dist/schemas-export.cjs.map +1 -0
  76. package/dist/schemas-export.d.cts +1598 -0
  77. package/dist/schemas-export.d.ts +1598 -0
  78. package/dist/schemas-export.js +5 -0
  79. package/dist/schemas-export.js.map +1 -0
  80. package/dist/siwe-export.cjs +237 -0
  81. package/dist/siwe-export.cjs.map +1 -0
  82. package/dist/siwe-export.d.cts +27 -0
  83. package/dist/siwe-export.d.ts +27 -0
  84. package/dist/siwe-export.js +228 -0
  85. package/dist/siwe-export.js.map +1 -0
  86. package/dist/testing.cjs +54 -0
  87. package/dist/testing.cjs.map +1 -0
  88. package/dist/testing.d.cts +20 -0
  89. package/dist/testing.d.ts +20 -0
  90. package/dist/testing.js +51 -0
  91. package/dist/testing.js.map +1 -0
  92. package/dist/validation-export.cjs +359 -0
  93. package/dist/validation-export.cjs.map +1 -0
  94. package/dist/validation-export.d.cts +3 -0
  95. package/dist/validation-export.d.ts +3 -0
  96. package/dist/validation-export.js +6 -0
  97. package/dist/validation-export.js.map +1 -0
  98. package/dist/validators-export.cjs +73 -0
  99. package/dist/validators-export.cjs.map +1 -0
  100. package/dist/validators-export.d.cts +37 -0
  101. package/dist/validators-export.d.ts +37 -0
  102. package/dist/validators-export.js +4 -0
  103. package/dist/validators-export.js.map +1 -0
  104. package/package.json +140 -0
  105. package/src/constants/index.ts +205 -0
  106. package/src/crypto/context.ts +228 -0
  107. package/src/crypto/diagnostics.ts +772 -0
  108. package/src/crypto/errors.ts +114 -0
  109. package/src/crypto/index.ts +89 -0
  110. package/src/crypto/payload-handler.ts +102 -0
  111. package/src/crypto/providers/compliance-provider.ts +579 -0
  112. package/src/crypto/providers/factory.ts +204 -0
  113. package/src/crypto/providers/index.ts +44 -0
  114. package/src/crypto/providers/noble-provider.ts +392 -0
  115. package/src/crypto/providers/node-provider.ts +433 -0
  116. package/src/crypto/providers/quickcrypto-provider.ts +483 -0
  117. package/src/crypto/providers/registry.ts +129 -0
  118. package/src/crypto/providers/webcrypto-provider.ts +364 -0
  119. package/src/crypto/session-security.ts +185 -0
  120. package/src/crypto/types.ts +93 -0
  121. package/src/crypto/utils.ts +190 -0
  122. package/src/crypto-export.ts +21 -0
  123. package/src/index.ts +38 -0
  124. package/src/schemas/auth.ts +60 -0
  125. package/src/schemas/client-messages.ts +57 -0
  126. package/src/schemas/core.ts +144 -0
  127. package/src/schemas/crypto.ts +65 -0
  128. package/src/schemas/discovery.ts +79 -0
  129. package/src/schemas/index.ts +239 -0
  130. package/src/schemas/relay-messages.ts +45 -0
  131. package/src/schemas/wallet-messages.ts +177 -0
  132. package/src/schemas-export.ts +23 -0
  133. package/src/siwe-export.ts +27 -0
  134. package/src/testing.ts +71 -0
  135. package/src/types/auth.ts +60 -0
  136. package/src/types/client-messages.ts +84 -0
  137. package/src/types/core.ts +131 -0
  138. package/src/types/crypto-provider.ts +264 -0
  139. package/src/types/crypto.ts +90 -0
  140. package/src/types/discovery.ts +50 -0
  141. package/src/types/errors.ts +87 -0
  142. package/src/types/index.ts +197 -0
  143. package/src/types/post-auth-operations.ts +363 -0
  144. package/src/types/providers.ts +72 -0
  145. package/src/types/relay-messages.ts +60 -0
  146. package/src/types/request-lifecycle.ts +161 -0
  147. package/src/types/signing-operations.ts +99 -0
  148. package/src/types/wallet-messages.ts +251 -0
  149. package/src/utils/client-session-claim.ts +188 -0
  150. package/src/utils/index.ts +54 -0
  151. package/src/utils/public-keys.ts +49 -0
  152. package/src/utils/siwe.ts +362 -0
  153. package/src/utils/url-decoding.ts +126 -0
  154. package/src/utils/url-encoding.ts +144 -0
  155. package/src/utils/wallet-session-claim.ts +188 -0
  156. package/src/validation-export.ts +32 -0
  157. package/src/validators/index.ts +222 -0
  158. package/src/validators-export.ts +8 -0
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Protocol utility functions
3
+ *
4
+ * This module provides utility functions that are specific to the BananaLink protocol.
5
+ * SDK-specific utilities are implemented in their respective packages.
6
+ */
7
+
8
+ import { generateUUID } from '../crypto/utils';
9
+
10
+ /**
11
+ * Generate a cryptographically secure session ID in RFC 4122 UUID v4 format
12
+ *
13
+ * This function delegates to the protocol crypto layer for mobile-safe UUID generation.
14
+ * For direct access to UUID generation, prefer importing from @bananalink-sdk/protocol/crypto.
15
+ *
16
+ * @returns UUID v4 string (e.g., "550e8400-e29b-41d4-a716-446655440000")
17
+ *
18
+ * @example
19
+ * const sessionId = generateSessionId();
20
+ * // => "a3bb189e-8bf9-4bdc-9f16-9f48a6a1e3e7"
21
+ */
22
+ export function generateSessionId(): string {
23
+ return generateUUID();
24
+ }
25
+
26
+ // Protocol-specific utility that might be needed across packages
27
+ export function isValidProtocolVersion(version: string): boolean {
28
+ // Only support version "1" for now
29
+ return version === '1';
30
+ }
31
+
32
+ // Protocol-specific session ID validation
33
+ export function isValidSessionId(sessionId: string): boolean {
34
+ // Session IDs should be valid UUIDs
35
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
36
+ return uuidRegex.test(sessionId);
37
+ }
38
+
39
+ // Export public key compression utilities
40
+ export * from './public-keys';
41
+
42
+ // Export URL encoding and decoding utilities
43
+ export * from './url-encoding';
44
+ export * from './url-decoding';
45
+
46
+ // Note: SIWE utilities have been moved to '@bananalink-sdk/protocol/siwe'
47
+ // For ERC-4361 message construction and validation, import from '@bananalink-sdk/protocol/siwe'
48
+ // Type-only definitions (SIWEFields, SIWEMessageOptions) remain available from main export
49
+
50
+ // Export wallet session claiming utilities
51
+ export * from './wallet-session-claim';
52
+
53
+ // Export client session claiming utilities
54
+ export * from './client-session-claim';
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Public key compression utilities for compact URL encoding
3
+ *
4
+ * These utilities convert public keys between standard base64 format
5
+ * and compact base64url format for use in QR codes and URLs.
6
+ */
7
+
8
+ /**
9
+ * Compress a public key for compact URLs
10
+ * Removes AES-GCM: prefix and uses base64url without padding
11
+ *
12
+ * @param publicKey - Public key in format "AES-GCM:base64string" or "base64string"
13
+ * @returns Compressed public key in base64url format without padding
14
+ */
15
+ export function compressPublicKey(publicKey: string): string {
16
+ // Remove AES-GCM: prefix if present
17
+ const keyWithoutPrefix = publicKey.startsWith('AES-GCM:')
18
+ ? publicKey.slice(8)
19
+ : publicKey;
20
+
21
+ // Convert base64 to base64url (replace + with -, / with _, remove padding)
22
+ return keyWithoutPrefix
23
+ .replace(/\+/g, '-')
24
+ .replace(/\//g, '_')
25
+ .replace(/=+$/, '');
26
+ }
27
+
28
+ /**
29
+ * Decompress a public key from compact format
30
+ * Adds back AES-GCM: prefix and restores base64 padding
31
+ *
32
+ * @param compressedKey - Compressed public key in base64url format
33
+ * @returns Public key in format "AES-GCM:base64string"
34
+ */
35
+ export function decompressPublicKey(compressedKey: string): string {
36
+ // Convert base64url back to base64
37
+ let base64Key = compressedKey
38
+ .replace(/-/g, '+')
39
+ .replace(/_/g, '/');
40
+
41
+ // Add padding if needed
42
+ const padding = base64Key.length % 4;
43
+ if (padding > 0) {
44
+ base64Key += '='.repeat(4 - padding);
45
+ }
46
+
47
+ // Add AES-GCM prefix if not already present
48
+ return base64Key.startsWith('AES-GCM:') ? base64Key : `AES-GCM:${base64Key}`;
49
+ }
@@ -0,0 +1,362 @@
1
+ /**
2
+ * SIWE (Sign-In with Ethereum) message construction utilities
3
+ * Implements ERC-4361 compliant message formatting
4
+ */
5
+
6
+ import type { SIWEFields } from '../types/core';
7
+
8
+ /**
9
+ * Options for SIWE message construction
10
+ */
11
+ export interface SIWEMessageOptions extends Partial<SIWEFields> {
12
+ domain: string;
13
+ address: string;
14
+ uri: string;
15
+ chainId: number;
16
+ nonce: string;
17
+ statement?: string;
18
+ }
19
+
20
+ /**
21
+ * Construct a SIWE message according to ERC-4361 specification
22
+ * @param options - SIWE message options
23
+ * @returns Formatted SIWE message string
24
+ */
25
+ export function constructSIWEMessage(options: SIWEMessageOptions): string {
26
+ const {
27
+ domain,
28
+ address,
29
+ statement,
30
+ uri,
31
+ version = '1',
32
+ chainId,
33
+ nonce,
34
+ issuedAt = new Date().toISOString(),
35
+ expirationTime,
36
+ notBefore,
37
+ requestId,
38
+ resources,
39
+ scheme,
40
+ } = options;
41
+
42
+ // Validate required fields
43
+ if (!domain || !address || !uri || !chainId || !nonce) {
44
+ throw new Error('Missing required SIWE fields: domain, address, uri, chainId, nonce');
45
+ }
46
+
47
+ // Validate address format
48
+ if (!isValidEthereumAddress(address)) {
49
+ throw new Error('Invalid Ethereum address format');
50
+ }
51
+
52
+ // Construct message parts according to ERC-4361
53
+ const parts: string[] = [];
54
+
55
+ // Header: domain wants you to sign in
56
+ const header = scheme ? `${scheme}://${domain}` : domain;
57
+ parts.push(`${header} wants you to sign in with your Ethereum account:`);
58
+ parts.push(address);
59
+ parts.push(''); // Empty line
60
+
61
+ // Optional statement
62
+ if (statement) {
63
+ parts.push(statement);
64
+ parts.push(''); // Empty line
65
+ }
66
+
67
+ // Required fields
68
+ parts.push(`URI: ${uri}`);
69
+ parts.push(`Version: ${version}`);
70
+ parts.push(`Chain ID: ${chainId}`);
71
+ parts.push(`Nonce: ${nonce}`);
72
+ parts.push(`Issued At: ${issuedAt}`);
73
+
74
+ // Optional fields
75
+ if (expirationTime) {
76
+ parts.push(`Expiration Time: ${expirationTime}`);
77
+ }
78
+ if (notBefore) {
79
+ parts.push(`Not Before: ${notBefore}`);
80
+ }
81
+ if (requestId) {
82
+ parts.push(`Request ID: ${requestId}`);
83
+ }
84
+
85
+ // Resources
86
+ if (resources && resources.length > 0) {
87
+ parts.push('Resources:');
88
+ resources.forEach(resource => {
89
+ parts.push(`- ${resource}`);
90
+ });
91
+ }
92
+
93
+ return parts.join('\n');
94
+ }
95
+
96
+ /**
97
+ * Parse a SIWE message string into its component fields
98
+ * @param message - SIWE message string
99
+ * @returns Parsed SIWE fields
100
+ */
101
+ export function parseSIWEMessage(message: string): SIWEFields {
102
+ const lines = message.split('\n');
103
+
104
+ if (lines.length < 6) {
105
+ throw new Error('Invalid SIWE message format: insufficient lines');
106
+ }
107
+
108
+ // Parse header
109
+ const headerMatch = lines[0].match(/^((?<scheme>[^:]+):\/\/)?(?<domain>.+) wants you to sign in with your Ethereum account:$/);
110
+ if (!headerMatch) {
111
+ throw new Error('Invalid SIWE message header');
112
+ }
113
+
114
+ const fields: SIWEFields = {
115
+ scheme: headerMatch.groups?.scheme,
116
+ domain: headerMatch.groups?.domain || '',
117
+ address: lines[1],
118
+ statement: undefined,
119
+ uri: '',
120
+ version: '1',
121
+ chainId: 0,
122
+ nonce: '',
123
+ issuedAt: '',
124
+ };
125
+
126
+ // Find where the required fields start
127
+ let fieldIndex = 2;
128
+
129
+ // Check for optional statement (non-empty lines before URI)
130
+ const statementLines: string[] = [];
131
+ while (fieldIndex < lines.length && !lines[fieldIndex].startsWith('URI: ')) {
132
+ if (lines[fieldIndex]) {
133
+ statementLines.push(lines[fieldIndex]);
134
+ }
135
+ fieldIndex++;
136
+ }
137
+
138
+ if (statementLines.length > 0) {
139
+ // Remove trailing empty line if present
140
+ if (statementLines[statementLines.length - 1] === '') {
141
+ statementLines.pop();
142
+ }
143
+ fields.statement = statementLines.join('\n');
144
+ }
145
+
146
+ // Parse required and optional fields
147
+ for (let i = fieldIndex; i < lines.length; i++) {
148
+ const line = lines[i];
149
+
150
+ if (line.startsWith('URI: ')) {
151
+ fields.uri = line.substring(5);
152
+ } else if (line.startsWith('Version: ')) {
153
+ fields.version = line.substring(9);
154
+ } else if (line.startsWith('Chain ID: ')) {
155
+ fields.chainId = parseInt(line.substring(10), 10);
156
+ } else if (line.startsWith('Nonce: ')) {
157
+ fields.nonce = line.substring(7);
158
+ } else if (line.startsWith('Issued At: ')) {
159
+ fields.issuedAt = line.substring(11);
160
+ } else if (line.startsWith('Expiration Time: ')) {
161
+ fields.expirationTime = line.substring(17);
162
+ } else if (line.startsWith('Not Before: ')) {
163
+ fields.notBefore = line.substring(12);
164
+ } else if (line.startsWith('Request ID: ')) {
165
+ fields.requestId = line.substring(12);
166
+ } else if (line === 'Resources:') {
167
+ // Parse resources list
168
+ fields.resources = [];
169
+ i++;
170
+ while (i < lines.length && lines[i].startsWith('- ')) {
171
+ fields.resources.push(lines[i].substring(2));
172
+ i++;
173
+ }
174
+ i--; // Adjust for the outer loop increment
175
+ }
176
+ }
177
+
178
+ // Validate required fields
179
+ if (!fields.uri || !fields.nonce || !fields.chainId || !fields.issuedAt) {
180
+ throw new Error('Missing required SIWE fields in message');
181
+ }
182
+
183
+ return fields;
184
+ }
185
+
186
+ /**
187
+ * Create a BananaLink-specific SIWE message for session authentication
188
+ * @param sessionId - Session identifier
189
+ * @param address - Ethereum address
190
+ * @param domain - dApp domain
191
+ * @param chainId - Chain ID
192
+ * @param nonce - Session nonce
193
+ * @param statement - Optional custom statement
194
+ * @returns Formatted SIWE message
195
+ */
196
+ export function createBananaLinkSIWEMessage(
197
+ sessionId: string,
198
+ address: string,
199
+ domain: string,
200
+ chainId: number,
201
+ nonce: string,
202
+ statement?: string
203
+ ): string {
204
+ return constructSIWEMessage({
205
+ domain,
206
+ address,
207
+ statement: statement || `Sign in to ${domain}`,
208
+ uri: `bananalink://session/${sessionId}`,
209
+ chainId,
210
+ nonce,
211
+ version: '1',
212
+ issuedAt: new Date().toISOString(),
213
+ });
214
+ }
215
+
216
+ /**
217
+ * Validate an Ethereum address format
218
+ * @param address - Address to validate
219
+ * @returns True if valid Ethereum address
220
+ */
221
+ export function isValidEthereumAddress(address: string): boolean {
222
+ return /^0x[a-fA-F0-9]{40}$/.test(address);
223
+ }
224
+
225
+ // NOTE: Nonce generation moved to @bananalink-sdk/crypto package to avoid circular dependencies
226
+ // SDKs should use crypto package to generate nonces before constructing SIWE messages
227
+
228
+ /**
229
+ * Validate SIWE message timestamp
230
+ * @param timestamp - ISO 8601 timestamp
231
+ * @param maxAge - Maximum age in milliseconds (default: 5 minutes)
232
+ * @returns True if timestamp is within valid range
233
+ */
234
+ export function validateSIWETimestamp(timestamp: string, maxAge: number = 5 * 60 * 1000): boolean {
235
+ try {
236
+ const messageTime = new Date(timestamp).getTime();
237
+ const now = Date.now();
238
+ const age = Math.abs(now - messageTime);
239
+ return age <= maxAge;
240
+ } catch {
241
+ return false;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Validate SIWE message expiration
247
+ * @param expirationTime - Optional expiration timestamp
248
+ * @returns True if message has not expired
249
+ */
250
+ export function validateSIWEExpiration(expirationTime?: string): boolean {
251
+ // Empty string should be treated as invalid (check before falsy check)
252
+ if (expirationTime === '') {
253
+ return false;
254
+ }
255
+
256
+ if (!expirationTime) {
257
+ return true; // No expiration set (undefined or null)
258
+ }
259
+
260
+ try {
261
+ const expiry = new Date(expirationTime).getTime();
262
+ // Check for Invalid Date
263
+ if (isNaN(expiry)) {
264
+ return false;
265
+ }
266
+ return Date.now() < expiry;
267
+ } catch {
268
+ return false;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Validate SIWE message not-before time
274
+ * @param notBefore - Optional not-before timestamp
275
+ * @returns True if current time is after not-before time
276
+ */
277
+ export function validateSIWENotBefore(notBefore?: string): boolean {
278
+ // Empty string should be treated as invalid (check before falsy check)
279
+ if (notBefore === '') {
280
+ return false;
281
+ }
282
+
283
+ if (!notBefore) {
284
+ return true; // No not-before restriction (undefined or null)
285
+ }
286
+
287
+ try {
288
+ const notBeforeTime = new Date(notBefore).getTime();
289
+ // Check for Invalid Date
290
+ if (isNaN(notBeforeTime)) {
291
+ return false;
292
+ }
293
+ return Date.now() >= notBeforeTime;
294
+ } catch {
295
+ return false;
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Comprehensive SIWE message validation
301
+ * @param fields - SIWE fields to validate
302
+ * @param options - Validation options
303
+ * @returns Validation result with any errors
304
+ */
305
+ export function validateSIWEMessage(
306
+ fields: SIWEFields,
307
+ options: {
308
+ expectedDomain?: string;
309
+ expectedChainId?: number;
310
+ maxAge?: number;
311
+ } = {}
312
+ ): { valid: boolean; errors: string[] } {
313
+ const errors: string[] = [];
314
+
315
+ // Check required fields
316
+ if (!fields.domain) errors.push('Missing domain');
317
+ if (!fields.address) errors.push('Missing address');
318
+ if (!fields.uri) errors.push('Missing URI');
319
+ if (!fields.chainId) errors.push('Missing chain ID');
320
+ if (!fields.nonce) errors.push('Missing nonce');
321
+ if (!fields.issuedAt) errors.push('Missing issued at timestamp');
322
+
323
+ // Validate address format
324
+ if (fields.address && !isValidEthereumAddress(fields.address)) {
325
+ errors.push('Invalid Ethereum address format');
326
+ }
327
+
328
+ // Validate version
329
+ if (fields.version !== '1') {
330
+ errors.push(`Invalid SIWE version: ${fields.version} (must be 1)`);
331
+ }
332
+
333
+ // Domain validation
334
+ if (options.expectedDomain && fields.domain !== options.expectedDomain) {
335
+ errors.push(`Domain mismatch: expected ${options.expectedDomain}, got ${fields.domain}`);
336
+ }
337
+
338
+ // Chain ID validation
339
+ if (options.expectedChainId && fields.chainId !== options.expectedChainId) {
340
+ errors.push(`Chain ID mismatch: expected ${options.expectedChainId}, got ${fields.chainId}`);
341
+ }
342
+
343
+ // Timestamp validation
344
+ if (!validateSIWETimestamp(fields.issuedAt, options.maxAge)) {
345
+ errors.push('Message timestamp is too old or invalid');
346
+ }
347
+
348
+ // Expiration validation
349
+ if (!validateSIWEExpiration(fields.expirationTime)) {
350
+ errors.push('Message has expired');
351
+ }
352
+
353
+ // Not-before validation
354
+ if (!validateSIWENotBefore(fields.notBefore)) {
355
+ errors.push('Message is not yet valid (not-before time not reached)');
356
+ }
357
+
358
+ return {
359
+ valid: errors.length === 0,
360
+ errors,
361
+ };
362
+ }
@@ -0,0 +1,126 @@
1
+ import type { QRPayload } from '../types/discovery';
2
+ import { decompressPublicKey } from './public-keys';
3
+
4
+ /**
5
+ * Provider shortcodes reverse mapping for compact URL decoding
6
+ */
7
+ const PROVIDER_SHORTCODES_REVERSE: Record<string, string> = {
8
+ 'u': 'websocket', // Decode legacy 'u' to new 'websocket' name
9
+ 'w': 'websocket',
10
+ 'p': 'pusher',
11
+ 'a': 'ably',
12
+ };
13
+
14
+ /**
15
+ * Environment shortcodes reverse mapping for relay URL decoding
16
+ */
17
+ const RELAY_SHORTCODES_REVERSE: Record<string, string> = {
18
+ 'd': 'wss://relay.dev.banana.link/v1',
19
+ 's': 'wss://relay.staging.banana.link/v1',
20
+ };
21
+
22
+ /**
23
+ * Safely decode a URL parameter value
24
+ * Uses decodeURIComponent to restore special characters
25
+ */
26
+ export function decodeUrlParameter(value: string): string {
27
+ return decodeURIComponent(value);
28
+ }
29
+
30
+ /**
31
+ * Parse a query string with properly decoded parameters
32
+ */
33
+ export function parseQueryString(query: string): Record<string, string> {
34
+ const params: Record<string, string> = {};
35
+
36
+ // Remove leading ? if present
37
+ const cleanQuery = query.startsWith('?') ? query.slice(1) : query;
38
+
39
+ if (!cleanQuery) {
40
+ return params;
41
+ }
42
+
43
+ for (const pair of cleanQuery.split('&')) {
44
+ const [key, ...valueParts] = pair.split('=');
45
+ if (key) {
46
+ const value = valueParts.join('='); // Handle values that contain =
47
+ params[decodeUrlParameter(key)] = decodeUrlParameter(value);
48
+ }
49
+ }
50
+
51
+ return params;
52
+ }
53
+
54
+ /**
55
+ * Parse a connection string and extract the QRPayload with proper decoding
56
+ * Supports both full format and compact format
57
+ */
58
+ export function decodeConnectionString(connectionString: string): QRPayload {
59
+ let url: URL;
60
+
61
+ try {
62
+ url = new URL(connectionString);
63
+ } catch {
64
+ throw new Error(`Invalid connection string format: ${connectionString}`);
65
+ }
66
+
67
+ // Parse query parameters using our safe decoder
68
+ const params = parseQueryString(url.search);
69
+
70
+ // Support both compact and full parameter formats
71
+ const sessionId = params.sessionId || params.s; // full or compact
72
+ let publicKey = params.publicKey || params.key || params.k; // full, universal, or compact
73
+ let providerId = params.providerId || params.provider || params.p; // full, alt, or compact
74
+ let relayUrl = params.relay || params.r; // full or compact
75
+
76
+ if (!sessionId || !publicKey) {
77
+ throw new Error('Missing required parameters: sessionId and publicKey');
78
+ }
79
+
80
+ // If we have a compact format public key (params.k), decompress it
81
+ if (params.k && !params.publicKey && !params.key) {
82
+ publicKey = decompressPublicKey(params.k);
83
+ }
84
+
85
+ // If we have a compact format provider, expand it
86
+ if (params.p && !params.providerId && !params.provider) {
87
+ providerId = PROVIDER_SHORTCODES_REVERSE[params.p] || params.p;
88
+ }
89
+
90
+ // Handle relay URL decompression:
91
+ // - No relay parameter = production (default)
92
+ // - Shortcode (d, s) = expand to full environment URL
93
+ // - Full URL = use as-is
94
+ if (!relayUrl) {
95
+ // No relay parameter means production (most common case)
96
+ relayUrl = 'wss://relay.banana.link/v1';
97
+ } else if (RELAY_SHORTCODES_REVERSE[relayUrl]) {
98
+ // Environment shortcode - expand to full URL
99
+ relayUrl = RELAY_SHORTCODES_REVERSE[relayUrl];
100
+ }
101
+ // else: already a full URL, use as-is
102
+
103
+ // Ensure public key has AES-GCM prefix for backward compatibility
104
+ if (publicKey && !publicKey.startsWith('AES-GCM:')) {
105
+ publicKey = `AES-GCM:${publicKey}`;
106
+ }
107
+
108
+ return {
109
+ sessionId,
110
+ publicKey,
111
+ providerId: providerId || undefined,
112
+ relayUrl: relayUrl || undefined,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Validate that a string is a valid connection string
118
+ */
119
+ export function isValidConnectionString(connectionString: string): boolean {
120
+ try {
121
+ decodeConnectionString(connectionString);
122
+ return true;
123
+ } catch {
124
+ return false;
125
+ }
126
+ }