@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,144 @@
|
|
|
1
|
+
import type { QRPayload } from '../types/discovery';
|
|
2
|
+
import { DEEPLINK_SCHEME } from '../constants';
|
|
3
|
+
import { compressPublicKey } from './public-keys';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Provider shortcodes for compact URLs
|
|
7
|
+
*/
|
|
8
|
+
const PROVIDER_SHORTCODES: Record<string, string> = {
|
|
9
|
+
'uwebsockets': 'u', // Legacy - API may still return this
|
|
10
|
+
'websocket': 'w',
|
|
11
|
+
'pusher': 'p',
|
|
12
|
+
'ably': 'a',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Production relay URLs that should be omitted in compact format (most common case)
|
|
17
|
+
*/
|
|
18
|
+
const PRODUCTION_RELAY_URLS = [
|
|
19
|
+
'wss://relay.banana.link/v1',
|
|
20
|
+
'https://relay.banana.link/v1',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Environment shortcodes for non-production relay URLs
|
|
25
|
+
*/
|
|
26
|
+
const RELAY_SHORTCODES: Record<string, string> = {
|
|
27
|
+
'wss://relay.dev.banana.link/v1': 'd',
|
|
28
|
+
'https://relay.dev.banana.link/v1': 'd',
|
|
29
|
+
'wss://relay.staging.banana.link/v1': 's', // Future staging environment
|
|
30
|
+
'https://relay.staging.banana.link/v1': 's',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Safely encode a URL parameter value
|
|
35
|
+
* Uses encodeURIComponent to handle special characters like +, /, =
|
|
36
|
+
*/
|
|
37
|
+
export function encodeUrlParameter(value: string): string {
|
|
38
|
+
return encodeURIComponent(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build a query string with properly encoded parameters
|
|
43
|
+
*/
|
|
44
|
+
export function buildQueryString(params: Record<string, string | undefined>): string {
|
|
45
|
+
const pairs: string[] = [];
|
|
46
|
+
|
|
47
|
+
for (const [key, value] of Object.entries(params)) {
|
|
48
|
+
if (value !== undefined) {
|
|
49
|
+
pairs.push(`${encodeUrlParameter(key)}=${encodeUrlParameter(value)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return pairs.join('&');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a compact deep link URL with shortened parameters
|
|
58
|
+
* Uses single-letter parameters to minimize QR code size
|
|
59
|
+
*/
|
|
60
|
+
export function encodeCompactConnectionString(payload: QRPayload): string {
|
|
61
|
+
const params: Record<string, string | undefined> = {
|
|
62
|
+
s: payload.sessionId, // sessionId -> s
|
|
63
|
+
k: compressPublicKey(payload.publicKey), // publicKey -> k (compressed)
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Add optional parameters with shortcodes
|
|
67
|
+
if (payload.providerId) {
|
|
68
|
+
params.p = PROVIDER_SHORTCODES[payload.providerId] || payload.providerId;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Handle relay URL compression:
|
|
72
|
+
// - Production: omit entirely (most common case)
|
|
73
|
+
// - Development/Staging: use shortcode (r=d, r=s)
|
|
74
|
+
// - Custom: use full URL
|
|
75
|
+
if (payload.relayUrl) {
|
|
76
|
+
if (PRODUCTION_RELAY_URLS.includes(payload.relayUrl)) {
|
|
77
|
+
// Production relay - omit parameter entirely for maximum compression
|
|
78
|
+
// This will be the most common case, so QR codes will be smallest
|
|
79
|
+
} else if (RELAY_SHORTCODES[payload.relayUrl]) {
|
|
80
|
+
// Known environment (dev/staging) - use shortcode
|
|
81
|
+
params.r = RELAY_SHORTCODES[payload.relayUrl];
|
|
82
|
+
} else {
|
|
83
|
+
// Custom relay URL - include full URL
|
|
84
|
+
params.r = payload.relayUrl;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const queryString = buildQueryString(params);
|
|
89
|
+
return `${DEEPLINK_SCHEME}://connect?${queryString}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create a deep link URL with properly encoded parameters
|
|
94
|
+
* Uses full parameter names for readability
|
|
95
|
+
*/
|
|
96
|
+
export function encodeConnectionString(payload: QRPayload): string {
|
|
97
|
+
const params: Record<string, string | undefined> = {
|
|
98
|
+
sessionId: payload.sessionId,
|
|
99
|
+
publicKey: payload.publicKey,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Add optional parameters
|
|
103
|
+
if (payload.providerId) {
|
|
104
|
+
params.providerId = payload.providerId;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (payload.relayUrl) {
|
|
108
|
+
params.relay = payload.relayUrl;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const queryString = buildQueryString(params);
|
|
112
|
+
return `${DEEPLINK_SCHEME}://connect?${queryString}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a universal link (HTTPS) with properly encoded parameters
|
|
117
|
+
*/
|
|
118
|
+
export function encodeUniversalLink(payload: QRPayload, options: {
|
|
119
|
+
baseUrl?: string;
|
|
120
|
+
fallbackUrl?: string;
|
|
121
|
+
} = {}): string {
|
|
122
|
+
const {
|
|
123
|
+
baseUrl = 'https://banana.link',
|
|
124
|
+
fallbackUrl = 'https://banana.link/download',
|
|
125
|
+
} = options;
|
|
126
|
+
|
|
127
|
+
const params: Record<string, string | undefined> = {
|
|
128
|
+
sessionId: payload.sessionId,
|
|
129
|
+
key: payload.publicKey, // Use 'key' instead of 'publicKey' for universal links
|
|
130
|
+
fallback: fallbackUrl,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Add optional parameters
|
|
134
|
+
if (payload.providerId) {
|
|
135
|
+
params.provider = payload.providerId;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (payload.relayUrl) {
|
|
139
|
+
params.relay = payload.relayUrl;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const queryString = buildQueryString(params);
|
|
143
|
+
return `${baseUrl}/connect?${queryString}`;
|
|
144
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session claiming mechanism utilities for BananaLink protocol v2.0
|
|
3
|
+
* Implements secure session ownership and reconnection logic
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { WalletSessionClaim, WalletMessageEnvelope } from '../types/wallet-messages';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Wallet session claim storage interface
|
|
10
|
+
* Implementations should provide secure storage for wallet session claims
|
|
11
|
+
*/
|
|
12
|
+
export interface WalletSessionClaimStorage {
|
|
13
|
+
store(sessionId: string, claim: WalletSessionClaim): Promise<void>;
|
|
14
|
+
retrieve(sessionId: string): Promise<WalletSessionClaim | null>;
|
|
15
|
+
remove(sessionId: string): Promise<void>;
|
|
16
|
+
clear(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Wallet session claim manager for handling claim generation and validation
|
|
21
|
+
*/
|
|
22
|
+
export class WalletSessionClaimManager {
|
|
23
|
+
constructor(private storage: WalletSessionClaimStorage) {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate a new wallet session claim with provided nonce
|
|
27
|
+
* @param sessionId - Session identifier
|
|
28
|
+
* @param sessionNonce - Pre-generated cryptographically secure nonce (use @bananalink-sdk/crypto)
|
|
29
|
+
* @returns Generated wallet session claim
|
|
30
|
+
*/
|
|
31
|
+
async generateClaim(sessionId: string, sessionNonce: string): Promise<WalletSessionClaim> {
|
|
32
|
+
const claim: WalletSessionClaim = {
|
|
33
|
+
sessionNonce,
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Store the claim for future reconnection
|
|
38
|
+
await this.storage.store(sessionId, claim);
|
|
39
|
+
|
|
40
|
+
return claim;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Retrieve an existing wallet session claim
|
|
45
|
+
* @param sessionId - Session identifier
|
|
46
|
+
* @returns Stored wallet session claim or null if not found
|
|
47
|
+
*/
|
|
48
|
+
async getClaim(sessionId: string): Promise<WalletSessionClaim | null> {
|
|
49
|
+
return this.storage.retrieve(sessionId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validate a wallet session claim for reconnection
|
|
54
|
+
* @param claim - Claim to validate
|
|
55
|
+
* @param storedClaim - Previously stored claim
|
|
56
|
+
* @returns Validation result
|
|
57
|
+
*/
|
|
58
|
+
validateReconnectionClaim(
|
|
59
|
+
claim: WalletSessionClaim,
|
|
60
|
+
storedClaim: WalletSessionClaim
|
|
61
|
+
): { valid: boolean; reason?: string } {
|
|
62
|
+
// Check nonce match
|
|
63
|
+
if (claim.sessionNonce !== storedClaim.sessionNonce) {
|
|
64
|
+
return {
|
|
65
|
+
valid: false,
|
|
66
|
+
reason: 'Session nonce mismatch - invalid reconnection attempt',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check timestamp validity (prevent very old reconnection attempts)
|
|
71
|
+
const MAX_RECONNECTION_AGE = 24 * 60 * 60 * 1000; // 24 hours
|
|
72
|
+
const claimAge = Date.now() - claim.timestamp;
|
|
73
|
+
if (claimAge > MAX_RECONNECTION_AGE) {
|
|
74
|
+
return {
|
|
75
|
+
valid: false,
|
|
76
|
+
reason: 'Session claim too old - please create a new session',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { valid: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Remove a wallet session claim (on session end)
|
|
85
|
+
* @param sessionId - Session identifier
|
|
86
|
+
*/
|
|
87
|
+
async removeClaim(sessionId: string): Promise<void> {
|
|
88
|
+
await this.storage.remove(sessionId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Clear all stored claims
|
|
93
|
+
*/
|
|
94
|
+
async clearAllClaims(): Promise<void> {
|
|
95
|
+
await this.storage.clear();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// NOTE: Nonce generation moved to @bananalink-sdk/crypto package to avoid circular dependencies
|
|
100
|
+
// SDKs should use crypto.generateSessionNonce() before creating session claims
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Create a wallet message envelope with wallet session claim
|
|
104
|
+
* @param sessionId - Session identifier
|
|
105
|
+
* @param claim - Wallet session claim
|
|
106
|
+
* @param payload - Message payload
|
|
107
|
+
* @returns Complete wallet message envelope
|
|
108
|
+
*/
|
|
109
|
+
export function createWalletMessageEnvelope(
|
|
110
|
+
sessionId: string,
|
|
111
|
+
claim: WalletSessionClaim,
|
|
112
|
+
payload: WalletMessageEnvelope['payload']
|
|
113
|
+
): WalletMessageEnvelope {
|
|
114
|
+
return {
|
|
115
|
+
sessionId,
|
|
116
|
+
walletSessionClaim: claim,
|
|
117
|
+
payload,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Strip wallet session claim from wallet message for forwarding to dApp
|
|
123
|
+
* @param envelope - Original wallet message envelope
|
|
124
|
+
* @returns Message without wallet session claim
|
|
125
|
+
*/
|
|
126
|
+
export function stripWalletSessionClaim(
|
|
127
|
+
envelope: WalletMessageEnvelope
|
|
128
|
+
): Omit<WalletMessageEnvelope, 'walletSessionClaim'> {
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
130
|
+
const { walletSessionClaim, ...messageWithoutClaim } = envelope;
|
|
131
|
+
return messageWithoutClaim;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Validate session claim timestamp for replay attack prevention
|
|
136
|
+
* @param timestamp - Claim timestamp
|
|
137
|
+
* @param maxAge - Maximum age in milliseconds (default: 30 seconds)
|
|
138
|
+
* @returns True if timestamp is within valid range
|
|
139
|
+
*/
|
|
140
|
+
export function validateClaimTimestamp(
|
|
141
|
+
timestamp: number,
|
|
142
|
+
maxAge: number = 30 * 1000
|
|
143
|
+
): boolean {
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
const age = Math.abs(now - timestamp);
|
|
146
|
+
return age <= maxAge;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Validate wallet message envelope
|
|
151
|
+
* @param envelope - Message envelope to validate
|
|
152
|
+
* @returns Validation result
|
|
153
|
+
*/
|
|
154
|
+
export function validateWalletMessageEnvelope(
|
|
155
|
+
envelope: WalletMessageEnvelope
|
|
156
|
+
): { valid: boolean; errors: string[] } {
|
|
157
|
+
const errors: string[] = [];
|
|
158
|
+
|
|
159
|
+
// Check required fields
|
|
160
|
+
if (!envelope.sessionId) {
|
|
161
|
+
errors.push('Missing session ID');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!envelope.walletSessionClaim) {
|
|
165
|
+
errors.push('Missing wallet session claim');
|
|
166
|
+
} else {
|
|
167
|
+
// Validate claim structure
|
|
168
|
+
if (!envelope.walletSessionClaim.sessionNonce) {
|
|
169
|
+
errors.push('Missing session nonce in wallet session claim');
|
|
170
|
+
}
|
|
171
|
+
if (!envelope.walletSessionClaim.timestamp) {
|
|
172
|
+
errors.push('Missing timestamp in wallet session claim');
|
|
173
|
+
} else if (!validateClaimTimestamp(envelope.walletSessionClaim.timestamp)) {
|
|
174
|
+
errors.push('Wallet session claim timestamp is too old or invalid');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!envelope.payload) {
|
|
179
|
+
errors.push('Missing message payload');
|
|
180
|
+
} else if (!envelope.payload.type) {
|
|
181
|
+
errors.push('Missing payload type');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
valid: errors.length === 0,
|
|
186
|
+
errors,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full Zod validation export
|
|
3
|
+
*
|
|
4
|
+
* Complete runtime validation with Zod schemas (~48KB bundle)
|
|
5
|
+
* For lightweight validation, use @bananalink-sdk/protocol/validators
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export all schemas
|
|
9
|
+
export * from './schemas';
|
|
10
|
+
|
|
11
|
+
// Re-export lightweight validators for convenience
|
|
12
|
+
// Note: EncryptedPayload type conflict resolved by explicit schema export
|
|
13
|
+
export {
|
|
14
|
+
isValidAddress,
|
|
15
|
+
isValidUrl,
|
|
16
|
+
isValidTimestamp,
|
|
17
|
+
isValidChainId,
|
|
18
|
+
isValidNonce,
|
|
19
|
+
isValidSessionId,
|
|
20
|
+
isValidBase64,
|
|
21
|
+
isValidHex,
|
|
22
|
+
isValidMessageType,
|
|
23
|
+
isValidEncryptionAlgorithm,
|
|
24
|
+
isValidDomain,
|
|
25
|
+
isValidPublicKey,
|
|
26
|
+
isValidEncryptedPayload,
|
|
27
|
+
isValidSIWEMessage,
|
|
28
|
+
createValidationResult,
|
|
29
|
+
hasRequiredFields,
|
|
30
|
+
type ValidationResult,
|
|
31
|
+
type SIWEMessage
|
|
32
|
+
} from './validators';
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight validation utilities for BananaLink protocol types
|
|
3
|
+
*
|
|
4
|
+
* Zero dependencies, minimal bundle size (~3KB)
|
|
5
|
+
* Suitable for browsers, React Native, and Node.js
|
|
6
|
+
*
|
|
7
|
+
* For full Zod-based validation, use: @bananalink-sdk/protocol/validation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate Ethereum address format (0x + 40 hex characters)
|
|
12
|
+
*/
|
|
13
|
+
export function isValidAddress(address: unknown): address is string {
|
|
14
|
+
return typeof address === 'string' && /^0x[0-9a-fA-F]{40}$/.test(address);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate URL format
|
|
19
|
+
*/
|
|
20
|
+
export function isValidUrl(url: unknown): url is string {
|
|
21
|
+
if (typeof url !== 'string') return false;
|
|
22
|
+
try {
|
|
23
|
+
new URL(url);
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate ISO 8601 timestamp format
|
|
32
|
+
*/
|
|
33
|
+
export function isValidTimestamp(timestamp: unknown): timestamp is string {
|
|
34
|
+
if (typeof timestamp !== 'string') return false;
|
|
35
|
+
try {
|
|
36
|
+
const date = new Date(timestamp);
|
|
37
|
+
return !isNaN(date.getTime()) && timestamp === date.toISOString();
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate chain ID (positive integer)
|
|
45
|
+
*/
|
|
46
|
+
export function isValidChainId(chainId: unknown): chainId is number {
|
|
47
|
+
return typeof chainId === 'number' && Number.isInteger(chainId) && chainId > 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validate nonce format (64 hex characters = 32 bytes)
|
|
52
|
+
* Protocol v2.0 standard
|
|
53
|
+
*/
|
|
54
|
+
export function isValidNonce(nonce: unknown): nonce is string {
|
|
55
|
+
return typeof nonce === 'string' && /^[a-fA-F0-9]{64}$/.test(nonce);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate session ID format (UUID v4)
|
|
60
|
+
*/
|
|
61
|
+
export function isValidSessionId(sessionId: unknown): sessionId is string {
|
|
62
|
+
return (
|
|
63
|
+
typeof sessionId === 'string' &&
|
|
64
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(sessionId)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate base64 string format
|
|
70
|
+
* Accepts standard base64 (with padding) and base64url variants
|
|
71
|
+
*/
|
|
72
|
+
export function isValidBase64(str: unknown): str is string {
|
|
73
|
+
if (typeof str !== 'string' || str.length === 0) return false;
|
|
74
|
+
|
|
75
|
+
// RFC 4648 base64 pattern: A-Z, a-z, 0-9, +, /, = (padding)
|
|
76
|
+
// Also accepts base64url variant: A-Z, a-z, 0-9, -, _
|
|
77
|
+
const base64Pattern = /^[A-Za-z0-9+/\-_]+(={0,2})$/;
|
|
78
|
+
|
|
79
|
+
if (!base64Pattern.test(str)) return false;
|
|
80
|
+
|
|
81
|
+
// Validate padding - must be 0, 1, or 2 '=' characters at the end
|
|
82
|
+
// Length must be multiple of 4 for standard base64 with padding
|
|
83
|
+
const paddingMatch = str.match(/=*$/);
|
|
84
|
+
const padding = paddingMatch ? paddingMatch[0].length : 0;
|
|
85
|
+
|
|
86
|
+
// For base64url (no padding) or standard base64 (proper padding)
|
|
87
|
+
return padding === 0 || (padding <= 2 && (str.length % 4 === 0));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate hex string format
|
|
92
|
+
*/
|
|
93
|
+
export function isValidHex(hex: unknown): hex is string {
|
|
94
|
+
return typeof hex === 'string' && /^(0x)?[0-9a-fA-F]+$/.test(hex);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validation result with detailed error information
|
|
99
|
+
*/
|
|
100
|
+
export interface ValidationResult<T = unknown> {
|
|
101
|
+
valid: boolean;
|
|
102
|
+
error?: string;
|
|
103
|
+
data?: T;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Validate message type enum
|
|
108
|
+
*/
|
|
109
|
+
export function isValidMessageType(type: unknown): type is 'auth' | 'tx' | 'sign' {
|
|
110
|
+
return type === 'auth' || type === 'tx' || type === 'sign';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validate encryption algorithm
|
|
115
|
+
*/
|
|
116
|
+
export function isValidEncryptionAlgorithm(algo: unknown): algo is 'AES-GCM' | 'plaintext' {
|
|
117
|
+
return algo === 'AES-GCM' || algo === 'plaintext';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validate domain name format
|
|
122
|
+
*/
|
|
123
|
+
export function isValidDomain(domain: unknown): domain is string {
|
|
124
|
+
if (typeof domain !== 'string' || domain.length === 0) return false;
|
|
125
|
+
// Basic domain validation (not comprehensive, but sufficient for protocol)
|
|
126
|
+
return /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(
|
|
127
|
+
domain
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Validate public key format (algorithm:base64)
|
|
133
|
+
*/
|
|
134
|
+
export function isValidPublicKey(publicKey: unknown): publicKey is string {
|
|
135
|
+
if (typeof publicKey !== 'string') return false;
|
|
136
|
+
const parts = publicKey.split(':');
|
|
137
|
+
if (parts.length !== 2) return false;
|
|
138
|
+
const [algorithm, key] = parts;
|
|
139
|
+
return (
|
|
140
|
+
(algorithm === 'AES-GCM' || algorithm === 'cleartext') &&
|
|
141
|
+
(algorithm === 'cleartext' || isValidBase64(key))
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Validate encrypted payload structure
|
|
147
|
+
*/
|
|
148
|
+
export interface EncryptedPayload {
|
|
149
|
+
iv: string;
|
|
150
|
+
ciphertext: string;
|
|
151
|
+
mac: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function isValidEncryptedPayload(payload: unknown): payload is EncryptedPayload {
|
|
155
|
+
if (!payload || typeof payload !== 'object') return false;
|
|
156
|
+
const p = payload as Record<string, unknown>;
|
|
157
|
+
return (
|
|
158
|
+
isValidBase64(p.iv) &&
|
|
159
|
+
isValidBase64(p.ciphertext) &&
|
|
160
|
+
isValidBase64(p.mac)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create a validation result
|
|
166
|
+
*/
|
|
167
|
+
export function createValidationResult<T = unknown>(
|
|
168
|
+
valid: boolean,
|
|
169
|
+
data?: T,
|
|
170
|
+
error?: string
|
|
171
|
+
): ValidationResult<T> {
|
|
172
|
+
if (valid) {
|
|
173
|
+
return { valid: true, data };
|
|
174
|
+
}
|
|
175
|
+
return { valid: false, error };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Validate required fields are present and non-empty
|
|
180
|
+
*/
|
|
181
|
+
export function hasRequiredFields<T extends object>(
|
|
182
|
+
obj: unknown,
|
|
183
|
+
fields: (keyof T)[]
|
|
184
|
+
): obj is T {
|
|
185
|
+
if (!obj || typeof obj !== 'object') return false;
|
|
186
|
+
const o = obj as Record<string, unknown>;
|
|
187
|
+
return fields.every(
|
|
188
|
+
(field) =>
|
|
189
|
+
field in o &&
|
|
190
|
+
o[field as string] !== null &&
|
|
191
|
+
o[field as string] !== undefined &&
|
|
192
|
+
o[field as string] !== ''
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Validate SIWE message structure (minimal check)
|
|
198
|
+
*/
|
|
199
|
+
export interface SIWEMessage {
|
|
200
|
+
domain: string;
|
|
201
|
+
address: string;
|
|
202
|
+
uri: string;
|
|
203
|
+
version: string;
|
|
204
|
+
chainId: number;
|
|
205
|
+
nonce: string;
|
|
206
|
+
issuedAt: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function isValidSIWEMessage(message: unknown): message is SIWEMessage {
|
|
210
|
+
if (!message || typeof message !== 'object') return false;
|
|
211
|
+
const m = message as Record<string, unknown>;
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
isValidDomain(m.domain) &&
|
|
215
|
+
isValidAddress(m.address) &&
|
|
216
|
+
isValidUrl(m.uri) &&
|
|
217
|
+
m.version === '1' &&
|
|
218
|
+
isValidChainId(m.chainId) &&
|
|
219
|
+
isValidNonce(m.nonce) &&
|
|
220
|
+
isValidTimestamp(m.issuedAt)
|
|
221
|
+
);
|
|
222
|
+
}
|