swapped-commerce-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ import type {
2
+ RequiredSwappedConfig,
3
+ ApiResponse,
4
+ } from '../types/common'
5
+ import {
6
+ SwappedError,
7
+ SwappedAuthenticationError,
8
+ SwappedValidationError,
9
+ SwappedRateLimitError,
10
+ SwappedNotFoundError,
11
+ } from './errors'
12
+ import { withRetry } from './retry'
13
+
14
+ /**
15
+ * HTTP method types
16
+ */
17
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
18
+
19
+ /**
20
+ * Base URL for Swapped Commerce API
21
+ */
22
+ const BASE_URL = 'https://pay-api.swapped.com'
23
+
24
+ /**
25
+ * Pure function for building URL with query parameters
26
+ */
27
+ export function buildUrl(
28
+ baseUrl: string,
29
+ path: string,
30
+ params?: Readonly<Record<string, unknown>>
31
+ ): string {
32
+ const url = new URL(path, baseUrl)
33
+
34
+ if (params) {
35
+ for (const [key, value] of Object.entries(params)) {
36
+ if (value !== undefined && value !== null) {
37
+ url.searchParams.append(key, String(value))
38
+ }
39
+ }
40
+ }
41
+
42
+ return url.toString()
43
+ }
44
+
45
+ /**
46
+ * Pure function for creating request configuration
47
+ */
48
+ export function createRequestConfig(
49
+ config: RequiredSwappedConfig,
50
+ method: HttpMethod,
51
+ path: string,
52
+ body?: unknown
53
+ ): RequestInit {
54
+ const headers: Record<string, string> = {
55
+ 'X-API-Key': config.apiKey,
56
+ 'Content-Type': 'application/json',
57
+ 'Accept': 'application/json',
58
+ }
59
+
60
+ const requestInit: RequestInit = {
61
+ method,
62
+ headers,
63
+ }
64
+
65
+ if (body !== undefined) {
66
+ requestInit.body = JSON.stringify(body)
67
+ }
68
+
69
+ return requestInit
70
+ }
71
+
72
+ /**
73
+ * Pure function for handling error responses
74
+ */
75
+ async function handleErrorResponse(
76
+ response: Response
77
+ ): Promise<SwappedError> {
78
+ let errorData: {
79
+ message?: string
80
+ code?: string
81
+ details?: Readonly<Record<string, unknown>>
82
+ } = {}
83
+
84
+ try {
85
+ const text = await response.text()
86
+ if (text) {
87
+ errorData = JSON.parse(text) as typeof errorData
88
+ }
89
+ } catch {
90
+ // If parsing fails, use empty object
91
+ }
92
+
93
+ const message = errorData.message ?? response.statusText
94
+
95
+ switch (response.status) {
96
+ case 401:
97
+ return new SwappedAuthenticationError(message)
98
+ case 400:
99
+ return new SwappedValidationError(message, errorData.details)
100
+ case 404:
101
+ return new SwappedNotFoundError(message)
102
+ case 429:
103
+ return new SwappedRateLimitError(message)
104
+ default:
105
+ return new SwappedError(
106
+ message,
107
+ response.status,
108
+ errorData.code,
109
+ errorData.details
110
+ )
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Pure function for making HTTP requests with retry logic
116
+ */
117
+ export async function request<T>(
118
+ config: RequiredSwappedConfig,
119
+ method: HttpMethod,
120
+ path: string,
121
+ options?: {
122
+ readonly body?: unknown
123
+ readonly params?: Readonly<Record<string, unknown>>
124
+ }
125
+ ): Promise<ApiResponse<T>> {
126
+ const url = buildUrl(BASE_URL, path, options?.params)
127
+ const requestConfig = createRequestConfig(config, method, path, options?.body)
128
+
129
+ // Create abort controller for timeout
130
+ const controller = new AbortController()
131
+ const timeoutId = setTimeout(() => {
132
+ controller.abort()
133
+ }, config.timeout)
134
+
135
+ try {
136
+ const response = await withRetry(
137
+ async () => {
138
+ const res = await fetch(url, {
139
+ ...requestConfig,
140
+ signal: controller.signal,
141
+ })
142
+
143
+ if (!res.ok) {
144
+ throw await handleErrorResponse(res)
145
+ }
146
+
147
+ return res
148
+ },
149
+ {
150
+ maxRetries: config.retries,
151
+ timeout: config.timeout,
152
+ }
153
+ )
154
+
155
+ const data = (await response.json()) as ApiResponse<T>
156
+ return data
157
+ } catch (error) {
158
+ if (error instanceof SwappedError) {
159
+ throw error
160
+ }
161
+
162
+ // Handle abort/timeout
163
+ if (error instanceof Error && error.name === 'AbortError') {
164
+ throw new SwappedError(
165
+ `Request timeout after ${config.timeout}ms`,
166
+ 408,
167
+ 'TIMEOUT_ERROR'
168
+ )
169
+ }
170
+
171
+ // Handle network errors
172
+ throw new SwappedError(
173
+ error instanceof Error ? error.message : 'Network error occurred',
174
+ 0,
175
+ 'NETWORK_ERROR'
176
+ )
177
+ } finally {
178
+ clearTimeout(timeoutId)
179
+ }
180
+ }
@@ -0,0 +1,57 @@
1
+ import { SwappedError } from './errors'
2
+
3
+ /**
4
+ * Retry configuration
5
+ */
6
+ export interface RetryConfig {
7
+ readonly maxRetries: number
8
+ readonly timeout: number
9
+ }
10
+
11
+ /**
12
+ * Pure function for retrying with exponential backoff
13
+ *
14
+ * @param fn - Function to retry
15
+ * @param config - Retry configuration
16
+ * @returns Promise that resolves with the function result or rejects after all retries
17
+ */
18
+ export async function withRetry<T>(
19
+ fn: () => Promise<T>,
20
+ config: RetryConfig
21
+ ): Promise<T> {
22
+ let lastError: Error | undefined
23
+
24
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
25
+ try {
26
+ return await fn()
27
+ } catch (error) {
28
+ lastError = error as Error
29
+
30
+ // Don't retry on client errors (4xx) except 429 (rate limit)
31
+ if (
32
+ error instanceof SwappedError &&
33
+ error.statusCode !== undefined &&
34
+ error.statusCode >= 400 &&
35
+ error.statusCode < 500 &&
36
+ error.statusCode !== 429
37
+ ) {
38
+ throw error
39
+ }
40
+
41
+ // Wait before retry (exponential backoff)
42
+ if (attempt < config.maxRetries) {
43
+ const delayMs = Math.pow(2, attempt) * 1000
44
+ await delay(delayMs)
45
+ }
46
+ }
47
+ }
48
+
49
+ throw lastError ?? new Error('Request failed after retries')
50
+ }
51
+
52
+ /**
53
+ * Delay helper function
54
+ */
55
+ function delay(ms: number): Promise<void> {
56
+ return new Promise((resolve) => setTimeout(resolve, ms))
57
+ }
@@ -0,0 +1,82 @@
1
+ import type { WebhookEvent } from '../types/webhooks'
2
+
3
+ /**
4
+ * Pure function for verifying webhook signature using Web Crypto API
5
+ *
6
+ * @param payload - Raw webhook payload string
7
+ * @param signature - Signature from X-Signature header
8
+ * @param secret - Webhook secret key
9
+ * @returns Promise that resolves to true if signature is valid
10
+ */
11
+ export async function verifyWebhookSignature(
12
+ payload: string,
13
+ signature: string,
14
+ secret: string
15
+ ): Promise<boolean> {
16
+ try {
17
+ // Import the secret key
18
+ const encoder = new TextEncoder()
19
+ const keyData = encoder.encode(secret)
20
+ const key = await crypto.subtle.importKey(
21
+ 'raw',
22
+ keyData,
23
+ { name: 'HMAC', hash: 'SHA-256' },
24
+ false,
25
+ ['sign']
26
+ )
27
+
28
+ // Sign the payload
29
+ const payloadData = encoder.encode(payload)
30
+ const signatureBuffer = await crypto.subtle.sign(
31
+ 'HMAC',
32
+ key,
33
+ payloadData
34
+ )
35
+
36
+ // Convert signature to hex string
37
+ const signatureArray = Array.from(new Uint8Array(signatureBuffer))
38
+ const expectedSignature = signatureArray
39
+ .map((b) => b.toString(16).padStart(2, '0'))
40
+ .join('')
41
+
42
+ // Compare signatures using constant-time comparison
43
+ return constantTimeEqual(expectedSignature, signature)
44
+ } catch {
45
+ return false
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Constant-time string comparison to prevent timing attacks
51
+ */
52
+ function constantTimeEqual(a: string, b: string): boolean {
53
+ if (a.length !== b.length) {
54
+ return false
55
+ }
56
+
57
+ let result = 0
58
+ for (let i = 0; i < a.length; i++) {
59
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i)
60
+ }
61
+
62
+ return result === 0
63
+ }
64
+
65
+ /**
66
+ * Pure function for parsing webhook event payload
67
+ *
68
+ * @param payload - JSON string payload
69
+ * @returns Parsed webhook event
70
+ * @throws Error if payload is invalid JSON
71
+ */
72
+ export function parseWebhookEvent(payload: string): WebhookEvent {
73
+ try {
74
+ return JSON.parse(payload) as WebhookEvent
75
+ } catch (error) {
76
+ throw new Error(
77
+ `Failed to parse webhook payload: ${
78
+ error instanceof Error ? error.message : 'Unknown error'
79
+ }`
80
+ )
81
+ }
82
+ }