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,560 @@
1
+ # SPAPS Integration Step 6/12: Magic Link Authentication
2
+
3
+ ## Prerequisites Check
4
+
5
+ Before starting, confirm you have completed:
6
+ - ✅ Step 1-5: Full setup with email and wallet authentication
7
+ - ✅ TokenManager working properly
8
+ - ✅ Error handling implemented
9
+
10
+ ## Required TodoWrite List
11
+
12
+ Create a TodoWrite with EXACTLY these items:
13
+
14
+ ```javascript
15
+ TodoWrite({
16
+ todos: [
17
+ { content: "Add magic link tab to auth component", status: "pending", activeForm: "Adding magic link tab" },
18
+ { content: "Create magic link request form", status: "pending", activeForm: "Creating magic link form" },
19
+ { content: "Implement requestMagicLink method", status: "pending", activeForm: "Implementing requestMagicLink" },
20
+ { content: "Add redirect_url parameter", status: "pending", activeForm: "Adding redirect URL" },
21
+ { content: "Handle success/error states", status: "pending", activeForm: "Handling response states" },
22
+ { content: "Create callback page for magic link", status: "pending", activeForm: "Creating callback page" },
23
+ { content: "Show user feedback with toast", status: "pending", activeForm: "Adding user feedback" },
24
+ { content: "Request step 7 of SPAPS integration wizard", status: "pending", activeForm: "Requesting next step" }
25
+ ]
26
+ })
27
+ ```
28
+
29
+ ## Magic Link Implementation
30
+
31
+ ### Critical Parameter: redirect_url
32
+
33
+ ⚠️ **IMPORTANT**: The `requestMagicLink` method requires BOTH `email` and `redirect_url` parameters:
34
+
35
+ ```typescript
36
+ // ✅ CORRECT - Both parameters required
37
+ await sdk.auth.requestMagicLink({
38
+ email: 'user@example.com',
39
+ redirect_url: 'http://localhost:3000/auth/callback' // REQUIRED!
40
+ })
41
+
42
+ // ❌ WRONG - Missing redirect_url will cause errors
43
+ await sdk.auth.requestMagicLink({
44
+ email: 'user@example.com' // Missing redirect_url!
45
+ })
46
+
47
+ // ❌ WRONG - Wrong parameter name
48
+ await sdk.auth.requestMagicLink({
49
+ email: 'user@example.com',
50
+ redirectUrl: 'http://localhost:3000/auth/callback' // Wrong! Use redirect_url
51
+ })
52
+ ```
53
+
54
+ **What redirect_url does:**
55
+ - Where the user goes after clicking the magic link
56
+ - Must be a complete URL (including protocol)
57
+ - Usually your app's callback page
58
+ - Example: `http://localhost:3000/auth/callback`
59
+
60
+ ### 1. Magic Link Request Form
61
+
62
+ Add to your existing auth component or create new:
63
+
64
+ ```typescript
65
+ // components/auth/magic-link-form.tsx
66
+ 'use client'
67
+
68
+ import { useState } from 'react'
69
+ import { Button } from "@/components/ui/button"
70
+ import { Input } from "@/components/ui/input"
71
+ import { Label } from "@/components/ui/label"
72
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
73
+ import { toast } from "sonner"
74
+ import { sdk } from '@/lib/spaps'
75
+ import { SweetPotatoAPIError } from 'spaps-sdk'
76
+ import { Mail, Loader2 } from 'lucide-react'
77
+
78
+ export function MagicLinkForm() {
79
+ const [email, setEmail] = useState('')
80
+ const [isLoading, setIsLoading] = useState(false)
81
+ const [isSent, setIsSent] = useState(false)
82
+
83
+ const handleMagicLink = async (e: React.FormEvent) => {
84
+ e.preventDefault()
85
+
86
+ if (!email) {
87
+ toast.error('Please enter your email address')
88
+ return
89
+ }
90
+
91
+ setIsLoading(true)
92
+ setIsSent(false)
93
+
94
+ try {
95
+ // ✅ CORRECT: Use requestMagicLink with proper parameters
96
+ // The redirect_url MUST be a complete URL where users go after clicking the link
97
+ await sdk.auth.requestMagicLink({
98
+ email: email.trim(),
99
+ redirect_url: `${window.location.origin}/auth/callback` // Complete URL required
100
+ })
101
+
102
+ setIsSent(true)
103
+ toast.success('Magic link sent! Check your email.')
104
+
105
+ // Optional: Clear form after success
106
+ setTimeout(() => {
107
+ setEmail('')
108
+ setIsSent(false)
109
+ }, 30000) // Reset after 30 seconds
110
+
111
+ } catch (error) {
112
+ if (error instanceof SweetPotatoAPIError) {
113
+ switch (error.code) {
114
+ case 'INVALID_EMAIL':
115
+ toast.error('Please enter a valid email address')
116
+ break
117
+ case 'USER_NOT_FOUND':
118
+ toast.error('No account found with this email')
119
+ break
120
+ case 'RATE_LIMITED':
121
+ toast.error('Too many requests. Please wait before trying again.')
122
+ break
123
+ case 'EMAIL_PROVIDER_ERROR':
124
+ toast.error('Failed to send email. Please try again.')
125
+ break
126
+ default:
127
+ toast.error(error.message || 'Failed to send magic link')
128
+ }
129
+ } else {
130
+ toast.error('An unexpected error occurred')
131
+ }
132
+ console.error('Magic link error:', error)
133
+ } finally {
134
+ setIsLoading(false)
135
+ }
136
+ }
137
+
138
+ const handleResend = () => {
139
+ setIsSent(false)
140
+ handleMagicLink(new Event('submit') as any)
141
+ }
142
+
143
+ return (
144
+ <Card className="w-[400px]">
145
+ <CardHeader>
146
+ <CardTitle>Magic Link Sign In</CardTitle>
147
+ <CardDescription>
148
+ We'll email you a secure link to sign in instantly
149
+ </CardDescription>
150
+ </CardHeader>
151
+ <CardContent>
152
+ <form onSubmit={handleMagicLink} className="space-y-4">
153
+ {!isSent ? (
154
+ <>
155
+ <div className="space-y-2">
156
+ <Label htmlFor="magic-email">Email Address</Label>
157
+ <Input
158
+ id="magic-email"
159
+ type="email"
160
+ value={email}
161
+ onChange={(e) => setEmail(e.target.value)}
162
+ placeholder="your@email.com"
163
+ required
164
+ disabled={isLoading}
165
+ />
166
+ </div>
167
+
168
+ <Button
169
+ type="submit"
170
+ className="w-full"
171
+ disabled={isLoading}
172
+ >
173
+ {isLoading ? (
174
+ <>
175
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
176
+ Sending...
177
+ </>
178
+ ) : (
179
+ <>
180
+ <Mail className="mr-2 h-4 w-4" />
181
+ Send Magic Link
182
+ </>
183
+ )}
184
+ </Button>
185
+ </>
186
+ ) : (
187
+ <div className="space-y-4">
188
+ <div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
189
+ <p className="text-sm text-green-800 dark:text-green-200">
190
+ ✅ Magic link sent to <strong>{email}</strong>
191
+ </p>
192
+ <p className="text-xs text-green-600 dark:text-green-400 mt-2">
193
+ Check your email and click the link to sign in.
194
+ </p>
195
+ </div>
196
+
197
+ <Button
198
+ variant="outline"
199
+ className="w-full"
200
+ onClick={handleResend}
201
+ >
202
+ Resend Magic Link
203
+ </Button>
204
+ </div>
205
+ )}
206
+
207
+ <p className="text-xs text-muted-foreground text-center">
208
+ {isSent
209
+ ? "Didn't receive it? Check your spam folder or resend."
210
+ : "No password needed. Just click the link in your email."}
211
+ </p>
212
+ </form>
213
+ </CardContent>
214
+ </Card>
215
+ )
216
+ }
217
+ ```
218
+
219
+ ### 2. Magic Link Callback Page
220
+
221
+ Create a page to handle the magic link callback:
222
+
223
+ ```typescript
224
+ // app/auth/callback/page.tsx
225
+ 'use client'
226
+
227
+ import { useEffect, useState } from 'react'
228
+ import { useRouter, useSearchParams } from 'next/navigation'
229
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
230
+ import { toast } from "sonner"
231
+ import { sdk, TokenManager } from '@/lib/spaps'
232
+ import { Loader2 } from 'lucide-react'
233
+
234
+ export default function MagicLinkCallback() {
235
+ const router = useRouter()
236
+ const searchParams = useSearchParams()
237
+ const [status, setStatus] = useState<'verifying' | 'success' | 'error'>('verifying')
238
+ const [error, setError] = useState<string>('')
239
+
240
+ useEffect(() => {
241
+ const verifyMagicLink = async () => {
242
+ const token = searchParams.get('token')
243
+ const email = searchParams.get('email')
244
+
245
+ if (!token) {
246
+ setStatus('error')
247
+ setError('Invalid magic link. Token missing.')
248
+ return
249
+ }
250
+
251
+ try {
252
+ // Verify the magic link token
253
+ const authResponse = await sdk.auth.verifyMagicLink({
254
+ token,
255
+ email: email || undefined
256
+ })
257
+
258
+ // Store tokens
259
+ TokenManager.storeTokens(authResponse)
260
+
261
+ // Setup auto-refresh
262
+ TokenManager.autoRefreshToken(sdk)
263
+
264
+ setStatus('success')
265
+ toast.success('Successfully signed in!')
266
+
267
+ // Redirect after short delay
268
+ setTimeout(() => {
269
+ router.push('/dashboard') // Or wherever you want
270
+ }, 2000)
271
+
272
+ } catch (error: any) {
273
+ setStatus('error')
274
+ setError(error.message || 'Failed to verify magic link')
275
+ toast.error('Magic link verification failed')
276
+ console.error('Magic link verification error:', error)
277
+ }
278
+ }
279
+
280
+ verifyMagicLink()
281
+ }, [searchParams, router])
282
+
283
+ return (
284
+ <div className="min-h-screen flex items-center justify-center p-4">
285
+ <Card className="w-[400px]">
286
+ <CardHeader>
287
+ <CardTitle>
288
+ {status === 'verifying' && 'Verifying Magic Link...'}
289
+ {status === 'success' && 'Success!'}
290
+ {status === 'error' && 'Verification Failed'}
291
+ </CardTitle>
292
+ <CardDescription>
293
+ {status === 'verifying' && 'Please wait while we sign you in'}
294
+ {status === 'success' && 'You have been successfully authenticated'}
295
+ {status === 'error' && 'There was a problem with your magic link'}
296
+ </CardDescription>
297
+ </CardHeader>
298
+ <CardContent>
299
+ {status === 'verifying' && (
300
+ <div className="flex justify-center py-8">
301
+ <Loader2 className="h-8 w-8 animate-spin text-primary" />
302
+ </div>
303
+ )}
304
+
305
+ {status === 'success' && (
306
+ <div className="text-center py-8">
307
+ <div className="text-green-600 text-5xl mb-4">✓</div>
308
+ <p className="text-sm text-muted-foreground">
309
+ Redirecting to your dashboard...
310
+ </p>
311
+ </div>
312
+ )}
313
+
314
+ {status === 'error' && (
315
+ <div className="space-y-4">
316
+ <div className="text-center py-4">
317
+ <div className="text-red-600 text-5xl mb-4">✗</div>
318
+ <p className="text-sm text-red-600">
319
+ {error}
320
+ </p>
321
+ </div>
322
+ <div className="space-y-2">
323
+ <Button
324
+ variant="outline"
325
+ className="w-full"
326
+ onClick={() => router.push('/auth')}
327
+ >
328
+ Back to Sign In
329
+ </Button>
330
+ <Button
331
+ className="w-full"
332
+ onClick={() => router.push('/auth?tab=magic')}
333
+ >
334
+ Request New Magic Link
335
+ </Button>
336
+ </div>
337
+ </div>
338
+ )}
339
+ </CardContent>
340
+ </Card>
341
+ </div>
342
+ )
343
+ }
344
+ ```
345
+
346
+ ### 3. Integration with Tabs
347
+
348
+ Add magic link to your main auth component:
349
+
350
+ ```typescript
351
+ // components/auth/auth-tabs.tsx
352
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
353
+ import { EmailAuthForm } from './email-auth-form'
354
+ import { WalletAuthForm } from './wallet-auth-form'
355
+ import { MagicLinkForm } from './magic-link-form'
356
+
357
+ export function AuthTabs() {
358
+ return (
359
+ <Tabs defaultValue="email" className="w-[400px]">
360
+ <TabsList className="grid w-full grid-cols-3">
361
+ <TabsTrigger value="email">Email</TabsTrigger>
362
+ <TabsTrigger value="wallet">Wallet</TabsTrigger>
363
+ <TabsTrigger value="magic">Magic Link</TabsTrigger>
364
+ </TabsList>
365
+
366
+ <TabsContent value="email">
367
+ <EmailAuthForm />
368
+ </TabsContent>
369
+
370
+ <TabsContent value="wallet">
371
+ <WalletAuthForm />
372
+ </TabsContent>
373
+
374
+ <TabsContent value="magic">
375
+ <MagicLinkForm />
376
+ </TabsContent>
377
+ </Tabs>
378
+ )
379
+ }
380
+ ```
381
+
382
+ ## Method Signatures
383
+
384
+ ```typescript
385
+ // Request magic link
386
+ sdk.auth.requestMagicLink({
387
+ email: string,
388
+ redirect_url?: string // Where to redirect after verification
389
+ }): Promise<void>
390
+
391
+ // Verify magic link (on callback page)
392
+ sdk.auth.verifyMagicLink({
393
+ token: string,
394
+ email?: string // Optional, for extra validation
395
+ }): Promise<AuthResponse>
396
+ ```
397
+
398
+ ## Complete Working Example
399
+
400
+ Here's the complete flow with all required pieces:
401
+
402
+ ### Step 1: Request Magic Link
403
+ ```typescript
404
+ // In your component
405
+ const handleMagicLink = async () => {
406
+ try {
407
+ // ✅ CORRECT: Both email and redirect_url required
408
+ await sdk.auth.requestMagicLink({
409
+ email: 'test@example.com',
410
+ redirect_url: 'http://localhost:3000/auth/callback' // MUST be complete URL
411
+ })
412
+
413
+ console.log('Magic link sent successfully!')
414
+ // Show success UI to user
415
+
416
+ } catch (error) {
417
+ console.error('Failed to send magic link:', error)
418
+ }
419
+ }
420
+ ```
421
+
422
+ ### Step 2: User Clicks Link in Email
423
+ The magic link will look like:
424
+ ```
425
+ http://localhost:3000/auth/callback?token=abc123&email=test@example.com
426
+ ```
427
+
428
+ ### Step 3: Callback Page Handles Verification
429
+ ```typescript
430
+ // app/auth/callback/page.tsx
431
+ const token = searchParams.get('token')
432
+ const email = searchParams.get('email')
433
+
434
+ // ✅ CORRECT: Verify the magic link
435
+ const authResponse = await sdk.auth.verifyMagicLink({
436
+ token: token!,
437
+ email: email || undefined // Optional but recommended
438
+ })
439
+
440
+ // Store tokens and redirect
441
+ TokenManager.storeTokens(authResponse)
442
+ router.push('/dashboard')
443
+ ```
444
+
445
+ ### Step 4: URLs Must Match
446
+ ⚠️ **CRITICAL**: The redirect_url in your request MUST match your callback page route:
447
+
448
+ ```typescript
449
+ // If your callback page is at app/auth/callback/page.tsx
450
+ // Then redirect_url MUST be:
451
+ redirect_url: `${window.location.origin}/auth/callback`
452
+
453
+ // NOT these:
454
+ redirect_url: '/auth/callback' // Missing protocol/domain
455
+ redirect_url: 'http://localhost:3000/callback' // Wrong path
456
+ redirect_url: `${window.location.origin}/login` // Wrong path
457
+ ```
458
+
459
+ ## Validation Checklist
460
+
461
+ ✅ **Implementation**:
462
+ - `requestMagicLink` method called correctly
463
+ - `redirect_url` parameter included
464
+ - Success state shows confirmation
465
+ - Callback page handles verification
466
+
467
+ ✅ **User Experience**:
468
+ - Clear success message after sending
469
+ - Loading states during request
470
+ - Resend option available
471
+ - Helpful error messages
472
+
473
+ ✅ **Error Handling**:
474
+ - Invalid email format
475
+ - User not found
476
+ - Rate limiting
477
+ - Email provider errors
478
+
479
+ ✅ **Testing**:
480
+ - Can request magic link
481
+ - Email shows as sent
482
+ - Callback page loads
483
+ - Tokens stored after verification
484
+
485
+ ## Common Mistakes to Avoid
486
+
487
+ ❌ **Forgetting redirect URL**:
488
+ ```typescript
489
+ // WRONG - Missing redirect_url
490
+ await sdk.auth.requestMagicLink({ email })
491
+ ```
492
+
493
+ ❌ **Not handling sent state**:
494
+ ```typescript
495
+ // WRONG - No feedback that email was sent
496
+ await sdk.auth.requestMagicLink(...)
497
+ // User doesn't know if it worked
498
+ ```
499
+
500
+ ❌ **Missing callback page**:
501
+ ```typescript
502
+ // WRONG - Redirect URL points to non-existent page
503
+ redirect_url: '/auth/callback' // But page doesn't exist!
504
+ ```
505
+
506
+ ## Testing Magic Links
507
+
508
+ ### Local Development
509
+
510
+ In local mode, SPAPS will:
511
+ 1. Log the magic link to console (no real email sent)
512
+ 2. You can copy the link from terminal
513
+ 3. Open in browser to test callback
514
+
515
+ ### Test Flow
516
+
517
+ 1. Enter email: `test@example.com`
518
+ 2. Click "Send Magic Link"
519
+ 3. Check terminal for link (local mode)
520
+ 4. Open link in browser
521
+ 5. Should authenticate and redirect
522
+
523
+ ### Email Templates
524
+
525
+ SPAPS uses default email templates. In production, you can customize:
526
+ - Subject line
527
+ - Email body
528
+ - Branding
529
+ - Link expiration time
530
+
531
+ ## Security Considerations
532
+
533
+ ⚠️ **Important Security Notes**:
534
+ - Magic links expire after 15 minutes
535
+ - Single use only (can't reuse link)
536
+ - Rate limited to prevent abuse
537
+ - Verify token server-side
538
+ - Use HTTPS in production
539
+
540
+ ## Next Step
541
+
542
+ When ALL todos are ✅ complete and magic links work:
543
+
544
+ ```javascript
545
+ mcp__product-manager__get_agent_instructions({
546
+ category: "spaps-integration",
547
+ project: "spaps-demo",
548
+ step: 7
549
+ })
550
+ ```
551
+
552
+ ## Summary
553
+
554
+ Magic link authentication provides:
555
+ - **Passwordless login** - No passwords to remember
556
+ - **Better security** - No password to steal
557
+ - **Simple UX** - Just click the email link
558
+ - **Mobile friendly** - Works great on phones
559
+
560
+ Remember to test the full flow from request to callback!