@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,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
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Lightweight validators export
3
+ *
4
+ * Zero-dependency validation utilities (~3KB bundle)
5
+ * For Zod-based validation, use @bananalink-sdk/protocol/validation
6
+ */
7
+
8
+ export * from './validators';