spaps-mcp 0.1.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,525 @@
1
+ # SPAPS Integration Step 10/12: Error Handling & Token Management
2
+
3
+ ## Prerequisites Check
4
+
5
+ Before starting, confirm you have completed:
6
+ - ✅ Step 1-9: Full implementation with auth, payments, admin
7
+ - ✅ TokenManager being used for storage
8
+ - ✅ Basic error handling in place
9
+
10
+ ## Required TodoWrite List
11
+
12
+ Create a TodoWrite with EXACTLY these items:
13
+
14
+ ```javascript
15
+ TodoWrite({
16
+ todos: [
17
+ { content: "Import SweetPotatoAPIError from spaps-sdk", status: "pending", activeForm: "Importing error class" },
18
+ { content: "Create centralized error handler", status: "pending", activeForm: "Creating error handler" },
19
+ { content: "Add specific error code handling", status: "pending", activeForm: "Adding error code handling" },
20
+ { content: "Implement TokenManager.isTokenExpired checks", status: "pending", activeForm: "Implementing token expiry checks" },
21
+ { content: "Setup auto-refresh properly", status: "pending", activeForm: "Setting up auto-refresh" },
22
+ { content: "Add toast notifications for all errors", status: "pending", activeForm: "Adding toast notifications" },
23
+ { content: "Create error boundary component", status: "pending", activeForm: "Creating error boundary" },
24
+ { content: "Request step 11 of SPAPS integration wizard", status: "pending", activeForm: "Requesting next step" }
25
+ ]
26
+ })
27
+ ```
28
+
29
+ ## Comprehensive Error Handling
30
+
31
+ ### 1. Centralized Error Handler
32
+
33
+ Create a unified error handling utility:
34
+
35
+ ```typescript
36
+ // lib/error-handler.ts
37
+ import { SweetPotatoAPIError } from 'spaps-sdk'
38
+ import { toast } from 'sonner'
39
+
40
+ export interface ErrorContext {
41
+ operation?: string
42
+ userId?: string
43
+ details?: any
44
+ }
45
+
46
+ export class ErrorHandler {
47
+ private static errorCounts: Map<string, number> = new Map()
48
+ private static readonly MAX_ERRORS_BEFORE_RELOAD = 5
49
+
50
+ /**
51
+ * Handle any error with appropriate user feedback
52
+ */
53
+ static handle(error: any, context?: ErrorContext): void {
54
+ console.error(`Error in ${context?.operation || 'unknown operation'}:`, error)
55
+
56
+ if (error instanceof SweetPotatoAPIError) {
57
+ this.handleAPIError(error, context)
58
+ } else if (error?.code) {
59
+ this.handleCodedError(error, context)
60
+ } else if (error?.message) {
61
+ this.handleGenericError(error, context)
62
+ } else {
63
+ this.handleUnknownError(error, context)
64
+ }
65
+
66
+ // Track error frequency
67
+ this.trackError(error)
68
+ }
69
+
70
+ /**
71
+ * Handle SweetPotatoAPIError specifically
72
+ */
73
+ private static handleAPIError(error: SweetPotatoAPIError, context?: ErrorContext): void {
74
+ const errorMessages: Record<string, string> = {
75
+ // Authentication Errors
76
+ 'INVALID_CREDENTIALS': 'Invalid email or password',
77
+ 'USER_NOT_FOUND': 'No account found with these credentials',
78
+ 'EMAIL_NOT_VERIFIED': 'Please verify your email before signing in',
79
+ 'ACCOUNT_SUSPENDED': 'Your account has been suspended',
80
+ 'ACCOUNT_DELETED': 'This account no longer exists',
81
+
82
+ // Authorization Errors
83
+ 'UNAUTHORIZED': 'You must be logged in to do that',
84
+ 'FORBIDDEN': 'You don\'t have permission to do that',
85
+ 'INSUFFICIENT_PERMISSIONS': 'Additional permissions required',
86
+ 'ADMIN_ONLY': 'Admin access required',
87
+
88
+ // Token Errors
89
+ 'TOKEN_EXPIRED': 'Your session has expired. Please sign in again.',
90
+ 'TOKEN_INVALID': 'Invalid authentication token',
91
+ 'TOKEN_REVOKED': 'Your session has been revoked',
92
+ 'REFRESH_TOKEN_EXPIRED': 'Please sign in again to continue',
93
+
94
+ // Rate Limiting
95
+ 'RATE_LIMITED': 'Too many requests. Please slow down.',
96
+ 'QUOTA_EXCEEDED': 'You\'ve exceeded your usage quota',
97
+
98
+ // Payment Errors
99
+ 'PAYMENT_REQUIRED': 'Payment required to continue',
100
+ 'CARD_DECLINED': 'Your card was declined',
101
+ 'INSUFFICIENT_FUNDS': 'Insufficient funds for this transaction',
102
+ 'SUBSCRIPTION_EXPIRED': 'Your subscription has expired',
103
+ 'PAYMENT_FAILED': 'Payment processing failed',
104
+
105
+ // Wallet Errors
106
+ 'INVALID_SIGNATURE': 'Invalid wallet signature',
107
+ 'WALLET_NOT_FOUND': 'Wallet not registered',
108
+ 'WALLET_ALREADY_LINKED': 'This wallet is already linked to another account',
109
+ 'CHAIN_NOT_SUPPORTED': 'This blockchain is not supported',
110
+
111
+ // Validation Errors
112
+ 'INVALID_EMAIL': 'Please enter a valid email address',
113
+ 'INVALID_PASSWORD': 'Password must be at least 8 characters',
114
+ 'INVALID_INPUT': 'Please check your input and try again',
115
+ 'MISSING_REQUIRED_FIELD': 'Please fill in all required fields',
116
+
117
+ // Server Errors
118
+ 'INTERNAL_ERROR': 'Something went wrong. Please try again.',
119
+ 'SERVICE_UNAVAILABLE': 'Service temporarily unavailable',
120
+ 'NETWORK_ERROR': 'Network connection failed',
121
+ 'DATABASE_ERROR': 'Database operation failed',
122
+
123
+ // Business Logic Errors
124
+ 'DUPLICATE_ENTRY': 'This already exists',
125
+ 'NOT_FOUND': 'The requested item was not found',
126
+ 'CONFLICT': 'This conflicts with existing data',
127
+ 'PRECONDITION_FAILED': 'Preconditions not met'
128
+ }
129
+
130
+ const message = errorMessages[error.code || ''] || error.message || 'An error occurred'
131
+
132
+ // Show toast based on error severity
133
+ if (error.status && error.status >= 500) {
134
+ toast.error(message, {
135
+ description: 'Please try again later',
136
+ duration: 5000
137
+ })
138
+ } else if (error.code === 'TOKEN_EXPIRED') {
139
+ toast.error(message, {
140
+ action: {
141
+ label: 'Sign In',
142
+ onClick: () => window.location.href = '/auth'
143
+ }
144
+ })
145
+ } else {
146
+ toast.error(message)
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Handle errors with codes (like MetaMask errors)
152
+ */
153
+ private static handleCodedError(error: any, context?: ErrorContext): void {
154
+ const walletErrors: Record<number, string> = {
155
+ 4001: 'Transaction rejected by user',
156
+ 4100: 'The requested account and/or method has not been authorized',
157
+ 4200: 'The requested method is not supported',
158
+ 4900: 'Provider is disconnected',
159
+ 4901: 'Provider is disconnected from all chains',
160
+ '-32002': 'Request already pending in wallet',
161
+ '-32003': 'Transaction rejected',
162
+ '-32603': 'Internal wallet error'
163
+ }
164
+
165
+ const message = walletErrors[error.code] || error.message || 'Operation failed'
166
+ toast.error(message)
167
+ }
168
+
169
+ /**
170
+ * Handle generic errors with messages
171
+ */
172
+ private static handleGenericError(error: any, context?: ErrorContext): void {
173
+ toast.error(error.message || 'An unexpected error occurred')
174
+ }
175
+
176
+ /**
177
+ * Handle completely unknown errors
178
+ */
179
+ private static handleUnknownError(error: any, context?: ErrorContext): void {
180
+ toast.error('An unexpected error occurred. Please try again.')
181
+ }
182
+
183
+ /**
184
+ * Track error frequency and reload if too many
185
+ */
186
+ private static trackError(error: any): void {
187
+ const key = error?.code || error?.message || 'unknown'
188
+ const count = (this.errorCounts.get(key) || 0) + 1
189
+ this.errorCounts.set(key, count)
190
+
191
+ if (count >= this.MAX_ERRORS_BEFORE_RELOAD) {
192
+ toast.error('Multiple errors detected. Reloading page...', {
193
+ duration: 3000,
194
+ onAutoClose: () => window.location.reload()
195
+ })
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Clear error tracking
201
+ */
202
+ static clearErrors(): void {
203
+ this.errorCounts.clear()
204
+ }
205
+ }
206
+ ```
207
+
208
+ ### 2. Advanced Token Management
209
+
210
+ Enhance token management with expiry checks:
211
+
212
+ ```typescript
213
+ // lib/token-manager-enhanced.ts
214
+ import { TokenManager, sdk } from '@/lib/spaps'
215
+
216
+ export class TokenManagerEnhanced {
217
+ private static refreshPromise: Promise<boolean> | null = null
218
+ private static refreshInterval: NodeJS.Timeout | null = null
219
+
220
+ /**
221
+ * Initialize auto-refresh with proper cleanup
222
+ */
223
+ static startAutoRefresh(): void {
224
+ // Clear any existing interval
225
+ this.stopAutoRefresh()
226
+
227
+ // Check token every 5 minutes
228
+ this.refreshInterval = setInterval(() => {
229
+ this.checkAndRefreshToken()
230
+ }, 5 * 60 * 1000)
231
+
232
+ // Do initial check
233
+ this.checkAndRefreshToken()
234
+ }
235
+
236
+ /**
237
+ * Stop auto-refresh
238
+ */
239
+ static stopAutoRefresh(): void {
240
+ if (this.refreshInterval) {
241
+ clearInterval(this.refreshInterval)
242
+ this.refreshInterval = null
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Check token expiry and refresh if needed
248
+ */
249
+ static async checkAndRefreshToken(): Promise<boolean> {
250
+ // Prevent multiple simultaneous refreshes
251
+ if (this.refreshPromise) {
252
+ return this.refreshPromise
253
+ }
254
+
255
+ const accessToken = TokenManager.getAccessToken()
256
+ if (!accessToken) {
257
+ return false
258
+ }
259
+
260
+ // Check if token will expire in next 10 minutes
261
+ const tokenExpired = TokenManager.isTokenExpired(accessToken)
262
+ const tokenExpiringInfo = this.isTokenExpiringSoon(accessToken, 10 * 60)
263
+
264
+ if (tokenExpired || tokenExpiringInfo.expiringSoon) {
265
+ console.log(`Token ${tokenExpired ? 'expired' : 'expiring soon'}, refreshing...`)
266
+
267
+ this.refreshPromise = this.refreshToken()
268
+ const result = await this.refreshPromise
269
+ this.refreshPromise = null
270
+
271
+ return result
272
+ }
273
+
274
+ return true
275
+ }
276
+
277
+ /**
278
+ * Refresh the token
279
+ */
280
+ private static async refreshToken(): Promise<boolean> {
281
+ const refreshToken = TokenManager.getRefreshToken()
282
+ if (!refreshToken) {
283
+ console.error('No refresh token available')
284
+ this.handleTokenRefreshFailure()
285
+ return false
286
+ }
287
+
288
+ try {
289
+ const newTokens = await sdk.auth.refreshToken(refreshToken)
290
+ TokenManager.storeTokens(newTokens)
291
+ console.log('Token refreshed successfully')
292
+ return true
293
+ } catch (error) {
294
+ console.error('Token refresh failed:', error)
295
+ this.handleTokenRefreshFailure()
296
+ return false
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Check if token is expiring soon
302
+ */
303
+ private static isTokenExpiringSoon(
304
+ token: string,
305
+ thresholdSeconds: number = 600
306
+ ): { expiringSoon: boolean; remainingSeconds?: number } {
307
+ try {
308
+ const parts = token.split('.')
309
+ if (parts.length !== 3) {
310
+ return { expiringSoon: true }
311
+ }
312
+
313
+ const payload = JSON.parse(atob(parts[1]))
314
+ const expirationTime = payload.exp * 1000
315
+ const currentTime = Date.now()
316
+ const remainingTime = expirationTime - currentTime
317
+ const remainingSeconds = Math.floor(remainingTime / 1000)
318
+
319
+ return {
320
+ expiringSoon: remainingSeconds < thresholdSeconds,
321
+ remainingSeconds
322
+ }
323
+ } catch {
324
+ return { expiringSoon: true }
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Handle token refresh failure
330
+ */
331
+ private static handleTokenRefreshFailure(): void {
332
+ TokenManager.clearTokens()
333
+ this.stopAutoRefresh()
334
+
335
+ // Redirect to login
336
+ if (typeof window !== 'undefined') {
337
+ window.location.href = '/auth?error=session_expired'
338
+ }
339
+ }
340
+ }
341
+ ```
342
+
343
+ ### 3. React Error Boundary
344
+
345
+ Create an error boundary for React components:
346
+
347
+ ```typescript
348
+ // components/error-boundary.tsx
349
+ 'use client'
350
+
351
+ import React, { Component, ReactNode } from 'react'
352
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
353
+ import { Button } from "@/components/ui/button"
354
+ import { AlertTriangle } from 'lucide-react'
355
+
356
+ interface Props {
357
+ children: ReactNode
358
+ fallback?: ReactNode
359
+ }
360
+
361
+ interface State {
362
+ hasError: boolean
363
+ error: Error | null
364
+ }
365
+
366
+ export class ErrorBoundary extends Component<Props, State> {
367
+ constructor(props: Props) {
368
+ super(props)
369
+ this.state = { hasError: false, error: null }
370
+ }
371
+
372
+ static getDerivedStateFromError(error: Error): State {
373
+ return { hasError: true, error }
374
+ }
375
+
376
+ componentDidCatch(error: Error, errorInfo: any) {
377
+ console.error('Error boundary caught:', error, errorInfo)
378
+
379
+ // Log to error reporting service
380
+ if (typeof window !== 'undefined') {
381
+ // Example: Send to Sentry, LogRocket, etc.
382
+ // window.Sentry?.captureException(error)
383
+ }
384
+ }
385
+
386
+ handleReset = () => {
387
+ this.setState({ hasError: false, error: null })
388
+ }
389
+
390
+ render() {
391
+ if (this.state.hasError) {
392
+ if (this.props.fallback) {
393
+ return this.props.fallback
394
+ }
395
+
396
+ return (
397
+ <div className="min-h-screen flex items-center justify-center p-4">
398
+ <Card className="w-full max-w-md">
399
+ <CardHeader>
400
+ <CardTitle className="flex items-center gap-2 text-red-600">
401
+ <AlertTriangle className="h-5 w-5" />
402
+ Something went wrong
403
+ </CardTitle>
404
+ <CardDescription>
405
+ An unexpected error occurred. Please try again.
406
+ </CardDescription>
407
+ </CardHeader>
408
+ <CardContent className="space-y-4">
409
+ {process.env.NODE_ENV === 'development' && this.state.error && (
410
+ <div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
411
+ <p className="text-xs font-mono text-red-800 dark:text-red-200">
412
+ {this.state.error.message}
413
+ </p>
414
+ </div>
415
+ )}
416
+ <div className="flex gap-2">
417
+ <Button onClick={this.handleReset} variant="outline">
418
+ Try Again
419
+ </Button>
420
+ <Button onClick={() => window.location.href = '/'}>
421
+ Go Home
422
+ </Button>
423
+ </div>
424
+ </CardContent>
425
+ </Card>
426
+ </div>
427
+ )
428
+ }
429
+
430
+ return this.props.children
431
+ }
432
+ }
433
+ ```
434
+
435
+ ### 4. Usage Examples
436
+
437
+ Using the error handler in components:
438
+
439
+ ```typescript
440
+ // In your components
441
+ import { ErrorHandler } from '@/lib/error-handler'
442
+
443
+ // Example usage
444
+ try {
445
+ const result = await sdk.auth.signInWithPassword(credentials)
446
+ // Success handling
447
+ } catch (error) {
448
+ ErrorHandler.handle(error, {
449
+ operation: 'signInWithPassword',
450
+ details: { email: credentials.email }
451
+ })
452
+ }
453
+
454
+ // Using enhanced token manager
455
+ import { TokenManagerEnhanced } from '@/lib/token-manager-enhanced'
456
+
457
+ // Start auto-refresh when user logs in
458
+ TokenManagerEnhanced.startAutoRefresh()
459
+
460
+ // Stop when user logs out
461
+ TokenManagerEnhanced.stopAutoRefresh()
462
+ ```
463
+
464
+ ## Validation Checklist
465
+
466
+ ✅ **Error Handling**:
467
+ - SweetPotatoAPIError imported and used
468
+ - Specific error codes handled
469
+ - User-friendly messages shown
470
+ - Toast notifications for all errors
471
+
472
+ ✅ **Token Management**:
473
+ - TokenManager.isTokenExpired used
474
+ - Auto-refresh implemented
475
+ - Refresh failures handled
476
+ - Cleanup on logout
477
+
478
+ ✅ **Error Boundary**:
479
+ - Error boundary wraps app
480
+ - Fallback UI provided
481
+ - Development errors shown
482
+ - Reset functionality works
483
+
484
+ ## Common Mistakes to Avoid
485
+
486
+ ❌ **Generic error messages**:
487
+ ```typescript
488
+ // WRONG - Not helpful
489
+ catch (error) {
490
+ toast.error('Error occurred')
491
+ }
492
+ ```
493
+
494
+ ❌ **Not checking token expiry**:
495
+ ```typescript
496
+ // WRONG - Token might be expired
497
+ const token = TokenManager.getAccessToken()
498
+ // Use without checking expiry
499
+ ```
500
+
501
+ ❌ **Missing error boundaries**:
502
+ ```typescript
503
+ // WRONG - No error boundary
504
+ <App /> // Uncaught errors crash app
505
+ ```
506
+
507
+ ## Next Step
508
+
509
+ When ALL todos are ✅ complete:
510
+
511
+ ```javascript
512
+ mcp__product-manager__get_agent_instructions({
513
+ category: "spaps-integration",
514
+ project: "spaps-demo",
515
+ step: 11
516
+ })
517
+ ```
518
+
519
+ ## Summary
520
+
521
+ Comprehensive error handling provides:
522
+ - **Better UX** - Clear, actionable error messages
523
+ - **Resilience** - App doesn't crash on errors
524
+ - **Security** - Tokens managed properly
525
+ - **Debugging** - Detailed error tracking