@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.
- package/README.md +604 -0
- package/dist/chunk-32OWUOZ3.js +308 -0
- package/dist/chunk-32OWUOZ3.js.map +1 -0
- package/dist/chunk-65HNHRJK.cjs +123 -0
- package/dist/chunk-65HNHRJK.cjs.map +1 -0
- package/dist/chunk-7KYDLL3B.js +480 -0
- package/dist/chunk-7KYDLL3B.js.map +1 -0
- package/dist/chunk-A6FLEJ7R.cjs +62 -0
- package/dist/chunk-A6FLEJ7R.cjs.map +1 -0
- package/dist/chunk-CUJK7ZTS.js +217 -0
- package/dist/chunk-CUJK7ZTS.js.map +1 -0
- package/dist/chunk-GI3BUPIH.cjs +236 -0
- package/dist/chunk-GI3BUPIH.cjs.map +1 -0
- package/dist/chunk-JXHV66Q4.js +106 -0
- package/dist/chunk-JXHV66Q4.js.map +1 -0
- package/dist/chunk-KNGZKGRS.cjs +552 -0
- package/dist/chunk-KNGZKGRS.cjs.map +1 -0
- package/dist/chunk-LELPCIE7.js +840 -0
- package/dist/chunk-LELPCIE7.js.map +1 -0
- package/dist/chunk-MCZG7QEM.cjs +310 -0
- package/dist/chunk-MCZG7QEM.cjs.map +1 -0
- package/dist/chunk-TCVKC227.js +56 -0
- package/dist/chunk-TCVKC227.js.map +1 -0
- package/dist/chunk-VXLUSU5B.cjs +856 -0
- package/dist/chunk-VXLUSU5B.cjs.map +1 -0
- package/dist/chunk-WCQVDF3K.js +12 -0
- package/dist/chunk-WCQVDF3K.js.map +1 -0
- package/dist/chunk-WGEGR3DF.cjs +15 -0
- package/dist/chunk-WGEGR3DF.cjs.map +1 -0
- package/dist/client-session-claim-3QF3noOr.d.ts +197 -0
- package/dist/client-session-claim-C4lUik3b.d.cts +197 -0
- package/dist/core-DMhuNfoz.d.cts +62 -0
- package/dist/core-DMhuNfoz.d.ts +62 -0
- package/dist/crypto/providers/noble-provider.cjs +14 -0
- package/dist/crypto/providers/noble-provider.cjs.map +1 -0
- package/dist/crypto/providers/noble-provider.d.cts +30 -0
- package/dist/crypto/providers/noble-provider.d.ts +30 -0
- package/dist/crypto/providers/noble-provider.js +5 -0
- package/dist/crypto/providers/noble-provider.js.map +1 -0
- package/dist/crypto/providers/node-provider.cjs +308 -0
- package/dist/crypto/providers/node-provider.cjs.map +1 -0
- package/dist/crypto/providers/node-provider.d.cts +32 -0
- package/dist/crypto/providers/node-provider.d.ts +32 -0
- package/dist/crypto/providers/node-provider.js +306 -0
- package/dist/crypto/providers/node-provider.js.map +1 -0
- package/dist/crypto/providers/quickcrypto-provider.cjs +339 -0
- package/dist/crypto/providers/quickcrypto-provider.cjs.map +1 -0
- package/dist/crypto/providers/quickcrypto-provider.d.cts +34 -0
- package/dist/crypto/providers/quickcrypto-provider.d.ts +34 -0
- package/dist/crypto/providers/quickcrypto-provider.js +337 -0
- package/dist/crypto/providers/quickcrypto-provider.js.map +1 -0
- package/dist/crypto/providers/webcrypto-provider.cjs +310 -0
- package/dist/crypto/providers/webcrypto-provider.cjs.map +1 -0
- package/dist/crypto/providers/webcrypto-provider.d.cts +30 -0
- package/dist/crypto/providers/webcrypto-provider.d.ts +30 -0
- package/dist/crypto/providers/webcrypto-provider.js +308 -0
- package/dist/crypto/providers/webcrypto-provider.js.map +1 -0
- package/dist/crypto-BUS06Qz-.d.cts +40 -0
- package/dist/crypto-BUS06Qz-.d.ts +40 -0
- package/dist/crypto-export.cjs +790 -0
- package/dist/crypto-export.cjs.map +1 -0
- package/dist/crypto-export.d.cts +257 -0
- package/dist/crypto-export.d.ts +257 -0
- package/dist/crypto-export.js +709 -0
- package/dist/crypto-export.js.map +1 -0
- package/dist/crypto-provider-deYoVIxi.d.cts +36 -0
- package/dist/crypto-provider-deYoVIxi.d.ts +36 -0
- package/dist/index.cjs +615 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +379 -0
- package/dist/index.d.ts +379 -0
- package/dist/index.js +504 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas-export.cjs +294 -0
- package/dist/schemas-export.cjs.map +1 -0
- package/dist/schemas-export.d.cts +1598 -0
- package/dist/schemas-export.d.ts +1598 -0
- package/dist/schemas-export.js +5 -0
- package/dist/schemas-export.js.map +1 -0
- package/dist/siwe-export.cjs +237 -0
- package/dist/siwe-export.cjs.map +1 -0
- package/dist/siwe-export.d.cts +27 -0
- package/dist/siwe-export.d.ts +27 -0
- package/dist/siwe-export.js +228 -0
- package/dist/siwe-export.js.map +1 -0
- package/dist/testing.cjs +54 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +20 -0
- package/dist/testing.d.ts +20 -0
- package/dist/testing.js +51 -0
- package/dist/testing.js.map +1 -0
- package/dist/validation-export.cjs +359 -0
- package/dist/validation-export.cjs.map +1 -0
- package/dist/validation-export.d.cts +3 -0
- package/dist/validation-export.d.ts +3 -0
- package/dist/validation-export.js +6 -0
- package/dist/validation-export.js.map +1 -0
- package/dist/validators-export.cjs +73 -0
- package/dist/validators-export.cjs.map +1 -0
- package/dist/validators-export.d.cts +37 -0
- package/dist/validators-export.d.ts +37 -0
- package/dist/validators-export.js +4 -0
- package/dist/validators-export.js.map +1 -0
- package/package.json +140 -0
- package/src/constants/index.ts +205 -0
- package/src/crypto/context.ts +228 -0
- package/src/crypto/diagnostics.ts +772 -0
- package/src/crypto/errors.ts +114 -0
- package/src/crypto/index.ts +89 -0
- package/src/crypto/payload-handler.ts +102 -0
- package/src/crypto/providers/compliance-provider.ts +579 -0
- package/src/crypto/providers/factory.ts +204 -0
- package/src/crypto/providers/index.ts +44 -0
- package/src/crypto/providers/noble-provider.ts +392 -0
- package/src/crypto/providers/node-provider.ts +433 -0
- package/src/crypto/providers/quickcrypto-provider.ts +483 -0
- package/src/crypto/providers/registry.ts +129 -0
- package/src/crypto/providers/webcrypto-provider.ts +364 -0
- package/src/crypto/session-security.ts +185 -0
- package/src/crypto/types.ts +93 -0
- package/src/crypto/utils.ts +190 -0
- package/src/crypto-export.ts +21 -0
- package/src/index.ts +38 -0
- package/src/schemas/auth.ts +60 -0
- package/src/schemas/client-messages.ts +57 -0
- package/src/schemas/core.ts +144 -0
- package/src/schemas/crypto.ts +65 -0
- package/src/schemas/discovery.ts +79 -0
- package/src/schemas/index.ts +239 -0
- package/src/schemas/relay-messages.ts +45 -0
- package/src/schemas/wallet-messages.ts +177 -0
- package/src/schemas-export.ts +23 -0
- package/src/siwe-export.ts +27 -0
- package/src/testing.ts +71 -0
- package/src/types/auth.ts +60 -0
- package/src/types/client-messages.ts +84 -0
- package/src/types/core.ts +131 -0
- package/src/types/crypto-provider.ts +264 -0
- package/src/types/crypto.ts +90 -0
- package/src/types/discovery.ts +50 -0
- package/src/types/errors.ts +87 -0
- package/src/types/index.ts +197 -0
- package/src/types/post-auth-operations.ts +363 -0
- package/src/types/providers.ts +72 -0
- package/src/types/relay-messages.ts +60 -0
- package/src/types/request-lifecycle.ts +161 -0
- package/src/types/signing-operations.ts +99 -0
- package/src/types/wallet-messages.ts +251 -0
- package/src/utils/client-session-claim.ts +188 -0
- package/src/utils/index.ts +54 -0
- package/src/utils/public-keys.ts +49 -0
- package/src/utils/siwe.ts +362 -0
- package/src/utils/url-decoding.ts +126 -0
- package/src/utils/url-encoding.ts +144 -0
- package/src/utils/wallet-session-claim.ts +188 -0
- package/src/validation-export.ts +32 -0
- package/src/validators/index.ts +222 -0
- 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
|
+
}
|