@rubytech/create-maxy 0.4.4 → 0.4.6
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/package.json
CHANGED
|
@@ -1,8 +1,53 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
3
|
+
import { loginAnthropic, type OAuthCredentials } from '@mariozechner/pi-ai'
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
5
|
+
import { resolve } from 'node:path'
|
|
6
|
+
import { homedir } from 'node:os'
|
|
3
7
|
|
|
4
|
-
//
|
|
5
|
-
|
|
8
|
+
// --- Active OAuth session (same pattern as v1 gateway/server-methods/auth.ts) ---
|
|
9
|
+
|
|
10
|
+
type OAuthSession = {
|
|
11
|
+
authUrl: string | null
|
|
12
|
+
codeResolver: ((code: string) => void) | null
|
|
13
|
+
codePromise: Promise<string> | null
|
|
14
|
+
completed: boolean
|
|
15
|
+
error: string | null
|
|
16
|
+
credentials: OAuthCredentials | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let activeSession: OAuthSession | null = null
|
|
20
|
+
|
|
21
|
+
function resetSession(): void {
|
|
22
|
+
activeSession = null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// --- Claude CLI credentials ---
|
|
26
|
+
|
|
27
|
+
const CLAUDE_CREDS_PATH = resolve(homedir(), '.claude/.credentials.json')
|
|
28
|
+
|
|
29
|
+
function writeClaudeCredentials(creds: OAuthCredentials): void {
|
|
30
|
+
const claudeDir = resolve(homedir(), '.claude')
|
|
31
|
+
mkdirSync(claudeDir, { recursive: true })
|
|
32
|
+
|
|
33
|
+
let data: Record<string, unknown> = {}
|
|
34
|
+
if (existsSync(CLAUDE_CREDS_PATH)) {
|
|
35
|
+
try {
|
|
36
|
+
data = JSON.parse(readFileSync(CLAUDE_CREDS_PATH, 'utf-8'))
|
|
37
|
+
} catch { /* start fresh */ }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
data.claudeAiOauth = {
|
|
41
|
+
...(typeof data.claudeAiOauth === 'object' && data.claudeAiOauth ? data.claudeAiOauth : {}),
|
|
42
|
+
accessToken: creds.access,
|
|
43
|
+
refreshToken: creds.refresh,
|
|
44
|
+
expiresAt: creds.expires,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
writeFileSync(CLAUDE_CREDS_PATH, JSON.stringify(data, null, 2), { mode: 0o600 })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- Endpoints ---
|
|
6
51
|
|
|
7
52
|
/**
|
|
8
53
|
* GET /api/onboarding/claude-auth
|
|
@@ -26,68 +71,118 @@ export async function GET() {
|
|
|
26
71
|
|
|
27
72
|
/**
|
|
28
73
|
* POST /api/onboarding/claude-auth
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
74
|
+
* Three actions:
|
|
75
|
+
* - {} — start OAuth flow, returns authUrl
|
|
76
|
+
* - { code: "..." } — submit the auth code
|
|
77
|
+
* - { action: "wait" } — poll for completion
|
|
33
78
|
*/
|
|
34
|
-
export async function POST() {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
79
|
+
export async function POST(req: Request) {
|
|
80
|
+
let body: { code?: string; action?: string } = {}
|
|
81
|
+
try {
|
|
82
|
+
body = await req.json()
|
|
83
|
+
} catch { /* empty body = start flow */ }
|
|
40
84
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
85
|
+
// --- Submit code ---
|
|
86
|
+
if (body.code) {
|
|
87
|
+
if (!activeSession?.codeResolver) {
|
|
88
|
+
return NextResponse.json(
|
|
89
|
+
{ error: 'No active OAuth session. Click "Sign in with Claude" to start again.' },
|
|
90
|
+
{ status: 400 },
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
activeSession.codeResolver(body.code.trim())
|
|
95
|
+
|
|
96
|
+
return NextResponse.json({ message: 'Code received — verifying...' })
|
|
97
|
+
}
|
|
48
98
|
|
|
49
|
-
|
|
99
|
+
// --- Poll for completion ---
|
|
100
|
+
if (body.action === 'wait') {
|
|
101
|
+
if (!activeSession) {
|
|
102
|
+
return NextResponse.json({ completed: false, error: 'No active session.' })
|
|
103
|
+
}
|
|
50
104
|
|
|
51
|
-
|
|
52
|
-
|
|
105
|
+
// Poll for up to 5 seconds
|
|
106
|
+
const start = Date.now()
|
|
107
|
+
while (Date.now() - start < 5000) {
|
|
108
|
+
if (activeSession.completed) {
|
|
109
|
+
if (activeSession.error) {
|
|
110
|
+
const error = activeSession.error
|
|
111
|
+
resetSession()
|
|
112
|
+
return NextResponse.json({ completed: true, authenticated: false, error })
|
|
113
|
+
}
|
|
53
114
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const match = output.match(/(https:\/\/claude\.ai\/oauth\/authorize[^\s]+)/)
|
|
57
|
-
if (match) {
|
|
58
|
-
resolved = true
|
|
59
|
-
// Remove code=true& from the URL — that triggers the fallback flow
|
|
60
|
-
// which shows an auth code. Without it, the flow completes automatically.
|
|
61
|
-
const autoUrl = match[1].replace(/code=true&?/, '').replace(/\?&/, '?')
|
|
62
|
-
resolve(NextResponse.json({ authUrl: autoUrl }))
|
|
115
|
+
resetSession()
|
|
116
|
+
return NextResponse.json({ completed: true, authenticated: true })
|
|
63
117
|
}
|
|
118
|
+
await new Promise(r => setTimeout(r, 500))
|
|
64
119
|
}
|
|
65
120
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
tryResolve()
|
|
69
|
-
})
|
|
121
|
+
return NextResponse.json({ completed: false })
|
|
122
|
+
}
|
|
70
123
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
tryResolve()
|
|
74
|
-
})
|
|
124
|
+
// --- Start OAuth flow ---
|
|
125
|
+
resetSession()
|
|
75
126
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
127
|
+
activeSession = {
|
|
128
|
+
authUrl: null,
|
|
129
|
+
codeResolver: null,
|
|
130
|
+
codePromise: null,
|
|
131
|
+
completed: false,
|
|
132
|
+
error: null,
|
|
133
|
+
credentials: null,
|
|
134
|
+
}
|
|
81
135
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
resolved = true
|
|
86
|
-
resolve(NextResponse.json(
|
|
87
|
-
{ error: 'Could not start auth flow. Is Claude Code installed?' },
|
|
88
|
-
{ status: 500 },
|
|
89
|
-
))
|
|
90
|
-
}
|
|
91
|
-
}, 15000)
|
|
136
|
+
// Set up code promise (resolved when user submits code via POST { code })
|
|
137
|
+
activeSession.codePromise = new Promise<string>((resolve) => {
|
|
138
|
+
activeSession!.codeResolver = resolve
|
|
92
139
|
})
|
|
140
|
+
|
|
141
|
+
// Start OAuth flow in background (same as v1)
|
|
142
|
+
const oauthPromise = loginAnthropic(
|
|
143
|
+
// onAuthUrl — called when we have the URL
|
|
144
|
+
(url: string) => {
|
|
145
|
+
if (activeSession) {
|
|
146
|
+
activeSession.authUrl = url
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
// onPromptCode — called when the library needs the code
|
|
150
|
+
async (): Promise<string> => {
|
|
151
|
+
if (!activeSession?.codePromise) {
|
|
152
|
+
throw new Error('OAuth session expired')
|
|
153
|
+
}
|
|
154
|
+
return activeSession.codePromise
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// Handle completion in background
|
|
159
|
+
oauthPromise
|
|
160
|
+
.then((credentials) => {
|
|
161
|
+
if (activeSession) {
|
|
162
|
+
activeSession.credentials = credentials
|
|
163
|
+
activeSession.completed = true
|
|
164
|
+
writeClaudeCredentials(credentials)
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
.catch((err) => {
|
|
168
|
+
if (activeSession) {
|
|
169
|
+
activeSession.error = err instanceof Error ? err.message : String(err)
|
|
170
|
+
activeSession.completed = true
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// Wait for the auth URL to appear
|
|
175
|
+
await new Promise(r => setTimeout(r, 500))
|
|
176
|
+
if (!activeSession?.authUrl) {
|
|
177
|
+
await new Promise(r => setTimeout(r, 1500))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!activeSession?.authUrl) {
|
|
181
|
+
return NextResponse.json(
|
|
182
|
+
{ error: 'Failed to start OAuth flow.' },
|
|
183
|
+
{ status: 500 },
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return NextResponse.json({ authUrl: activeSession.authUrl })
|
|
93
188
|
}
|
|
@@ -19,6 +19,7 @@ export default function AdminPage() {
|
|
|
19
19
|
const [pinError, setPinError] = useState('')
|
|
20
20
|
const [showPin, setShowPin] = useState(false)
|
|
21
21
|
const [authUrl, setAuthUrl] = useState<string | null>(null)
|
|
22
|
+
const [authCode, setAuthCode] = useState('')
|
|
22
23
|
const [authLoading, setAuthLoading] = useState(false)
|
|
23
24
|
const [sessionKey, setSessionKey] = useState<string | null>(null)
|
|
24
25
|
const [messages, setMessages] = useState<Message[]>([])
|
|
@@ -274,8 +275,6 @@ export default function AdminPage() {
|
|
|
274
275
|
if (data.authUrl) {
|
|
275
276
|
setAuthUrl(data.authUrl)
|
|
276
277
|
window.open(data.authUrl, '_blank')
|
|
277
|
-
// Start polling for auth completion
|
|
278
|
-
pollForAuth()
|
|
279
278
|
} else if (data.error) {
|
|
280
279
|
setPinError(data.error)
|
|
281
280
|
}
|
|
@@ -285,18 +284,45 @@ export default function AdminPage() {
|
|
|
285
284
|
setAuthLoading(false)
|
|
286
285
|
}
|
|
287
286
|
|
|
288
|
-
async function
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
287
|
+
async function submitCode(e: FormEvent) {
|
|
288
|
+
e.preventDefault()
|
|
289
|
+
if (!authCode.trim()) return
|
|
290
|
+
setAuthLoading(true)
|
|
291
|
+
setPinError('')
|
|
292
|
+
try {
|
|
293
|
+
// Submit the code
|
|
294
|
+
await fetch('/api/onboarding/claude-auth', {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
297
|
+
body: JSON.stringify({ code: authCode.trim() }),
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
// Poll for completion
|
|
301
|
+
for (let i = 0; i < 12; i++) {
|
|
302
|
+
const waitRes = await fetch('/api/onboarding/claude-auth', {
|
|
303
|
+
method: 'POST',
|
|
304
|
+
headers: { 'Content-Type': 'application/json' },
|
|
305
|
+
body: JSON.stringify({ action: 'wait' }),
|
|
306
|
+
})
|
|
307
|
+
const waitData = await waitRes.json()
|
|
308
|
+
|
|
309
|
+
if (waitData.completed) {
|
|
310
|
+
if (waitData.authenticated) {
|
|
311
|
+
setAppState('enter-pin')
|
|
312
|
+
return
|
|
313
|
+
} else {
|
|
314
|
+
setPinError(waitData.error || 'Authentication failed. Check the code and try again.')
|
|
315
|
+
setAuthLoading(false)
|
|
316
|
+
return
|
|
317
|
+
}
|
|
297
318
|
}
|
|
298
|
-
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
setPinError('Timed out waiting for verification. Try again.')
|
|
322
|
+
} catch {
|
|
323
|
+
setPinError('Could not verify code.')
|
|
299
324
|
}
|
|
325
|
+
setAuthLoading(false)
|
|
300
326
|
}
|
|
301
327
|
|
|
302
328
|
return (
|
|
@@ -318,15 +344,29 @@ export default function AdminPage() {
|
|
|
318
344
|
) : (
|
|
319
345
|
<>
|
|
320
346
|
<p className="chat-intro" style={{ fontSize: '14px', marginTop: 0 }}>
|
|
321
|
-
|
|
347
|
+
Sign in on the Anthropic page, then paste the code here.
|
|
322
348
|
</p>
|
|
323
|
-
<
|
|
324
|
-
<
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
349
|
+
<form onSubmit={submitCode}>
|
|
350
|
+
<div className="pin-input-row">
|
|
351
|
+
<input
|
|
352
|
+
type="text"
|
|
353
|
+
value={authCode}
|
|
354
|
+
onChange={e => setAuthCode(e.target.value)}
|
|
355
|
+
placeholder="Paste authentication code"
|
|
356
|
+
className="chat-input"
|
|
357
|
+
autoFocus
|
|
358
|
+
autoComplete="off"
|
|
359
|
+
/>
|
|
360
|
+
<button type="submit" className="chat-send" disabled={!authCode.trim() || authLoading}>
|
|
361
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
362
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
363
|
+
<polyline points="12 5 19 12 12 19" />
|
|
364
|
+
</svg>
|
|
365
|
+
</button>
|
|
366
|
+
</div>
|
|
367
|
+
</form>
|
|
328
368
|
<a href={authUrl} target="_blank" rel="noopener noreferrer" className="auth-retry-link">
|
|
329
|
-
|
|
369
|
+
Try again
|
|
330
370
|
</a>
|
|
331
371
|
</>
|
|
332
372
|
)}
|