amorphous-cli 0.1.0 → 0.1.1
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/lib/commands.mjs +2 -1
- package/lib/help.mjs +3 -2
- package/lib/login.mjs +62 -83
- package/package.json +1 -1
package/lib/commands.mjs
CHANGED
|
@@ -167,7 +167,8 @@ async function cmdLogin(rest) {
|
|
|
167
167
|
if (flags.help) return printHelp('login')
|
|
168
168
|
const email = positional[0]
|
|
169
169
|
if (!email) throw new Error('Pass an email.')
|
|
170
|
-
|
|
170
|
+
const code = typeof flags.code === 'string' ? flags.code : undefined
|
|
171
|
+
await loginWithMagicLink(email.trim(), { code })
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
async function cmdCreate(cwd, rest) {
|
package/lib/help.mjs
CHANGED
|
@@ -30,9 +30,10 @@ Run amorphous <command> --help for flags. Pass --json where you need machine out
|
|
|
30
30
|
`
|
|
31
31
|
|
|
32
32
|
export const COMMAND_HELP = {
|
|
33
|
-
login: `login <email>
|
|
33
|
+
login: `login <email> [--code <code>]
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Send a sign-in code to that email, then type it.
|
|
36
|
+
--code skips the prompt when you already have the code.`,
|
|
36
37
|
|
|
37
38
|
list: `list [--view recents|all|starred|archive] [--folder <name>] [--tag <name>] [--search <q>] [--json]
|
|
38
39
|
|
package/lib/login.mjs
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createInterface } from 'node:readline'
|
|
2
2
|
import { createServer as createNetServer } from 'node:net'
|
|
3
3
|
import { createClient } from '@supabase/supabase-js'
|
|
4
4
|
import { loadDotEnv, supabaseAnonKey, supabaseUrl } from './config.mjs'
|
|
5
5
|
import { mailpitBase, sessionFromVerifyUrl, waitForMagicLinkVerifyUrl } from './mailpit.mjs'
|
|
6
6
|
import { saveSession } from './session.mjs'
|
|
7
7
|
|
|
8
|
+
function isLocalSupabase() {
|
|
9
|
+
try {
|
|
10
|
+
const host = new URL(supabaseUrl()).hostname
|
|
11
|
+
return host === '127.0.0.1' || host === 'localhost'
|
|
12
|
+
} catch {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
async function freePort(preferred) {
|
|
9
18
|
if (preferred) {
|
|
10
19
|
const ok = await new Promise((resolve) => {
|
|
@@ -29,6 +38,7 @@ async function freePort(preferred) {
|
|
|
29
38
|
|
|
30
39
|
async function mailpitReachable() {
|
|
31
40
|
if (process.env.AMORPHOUS_AGENT_LOGIN === '0') return false
|
|
41
|
+
if (!isLocalSupabase()) return false
|
|
32
42
|
try {
|
|
33
43
|
const res = await fetch(`${mailpitBase()}/api/v1/messages`, {
|
|
34
44
|
signal: AbortSignal.timeout(1500),
|
|
@@ -54,100 +64,69 @@ async function loginViaMailpit(supabase, email, redirectTo, sinceMs) {
|
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
async function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const { data, error } = await supabase.auth.exchangeCodeForSession(code)
|
|
66
|
-
if (error) throw error
|
|
67
|
-
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
68
|
-
res.end('<p>Signed in. You can close this tab and return to the terminal.</p>')
|
|
69
|
-
server.close()
|
|
70
|
-
resolve(data.session)
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
if (url.pathname === '/callback') {
|
|
74
|
-
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
75
|
-
res.end(`<!DOCTYPE html><html><body><p>Completing sign-in…</p><script>
|
|
76
|
-
const h = new URLSearchParams(location.hash.slice(1));
|
|
77
|
-
const access_token = h.get('access_token');
|
|
78
|
-
const refresh_token = h.get('refresh_token');
|
|
79
|
-
if (access_token) {
|
|
80
|
-
fetch('/token', { method:'POST', headers:{'Content-Type':'application/json'},
|
|
81
|
-
body: JSON.stringify({ access_token, refresh_token }) })
|
|
82
|
-
.then(() => { document.body.innerHTML = '<p>Signed in. Close this tab.</p>'; });
|
|
83
|
-
}
|
|
84
|
-
</script></body></html>`)
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
if (url.pathname === '/token' && req.method === 'POST') {
|
|
88
|
-
let body = ''
|
|
89
|
-
for await (const chunk of req) body += chunk
|
|
90
|
-
const parsed = JSON.parse(body)
|
|
91
|
-
res.writeHead(204)
|
|
92
|
-
res.end()
|
|
93
|
-
server.close()
|
|
94
|
-
resolve({
|
|
95
|
-
access_token: parsed.access_token,
|
|
96
|
-
refresh_token: parsed.refresh_token,
|
|
97
|
-
})
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
res.writeHead(404)
|
|
101
|
-
res.end()
|
|
102
|
-
} catch (e) {
|
|
103
|
-
reject(e)
|
|
104
|
-
}
|
|
105
|
-
})
|
|
106
|
-
server.listen(callbackPort, '127.0.0.1', async () => {
|
|
107
|
-
const { error } = await supabase.auth.signInWithOtp({
|
|
108
|
-
email,
|
|
109
|
-
options: { emailRedirectTo: redirectTo },
|
|
110
|
-
})
|
|
111
|
-
if (error) {
|
|
112
|
-
server.close()
|
|
113
|
-
reject(error)
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
console.error(`Magic link sent to ${email}. Open it (Mailpit ${mailpitBase()}).`)
|
|
117
|
-
console.error(`Waiting for callback on ${redirectTo} …`)
|
|
67
|
+
async function readCode() {
|
|
68
|
+
if (!process.stdin.isTTY) {
|
|
69
|
+
throw new Error('Pass the code with --code.')
|
|
70
|
+
}
|
|
71
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr })
|
|
72
|
+
try {
|
|
73
|
+
const answer = await new Promise((resolve) => {
|
|
74
|
+
rl.question('Code: ', resolve)
|
|
118
75
|
})
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
76
|
+
return String(answer || '').replace(/\s/g, '')
|
|
77
|
+
} finally {
|
|
78
|
+
rl.close()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function finish(tokens) {
|
|
83
|
+
if (!tokens?.access_token || !tokens?.refresh_token) {
|
|
84
|
+
throw new Error('Sign-in did not return a session.')
|
|
85
|
+
}
|
|
86
|
+
saveSession({
|
|
87
|
+
access_token: tokens.access_token,
|
|
88
|
+
refresh_token: tokens.refresh_token,
|
|
89
|
+
})
|
|
90
|
+
console.log('Logged in.')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function verifyEmailCode(supabase, email, code) {
|
|
94
|
+
const token = String(code || '').replace(/\s/g, '')
|
|
95
|
+
if (!/^\d{6,10}$/.test(token)) throw new Error('Enter the code from the email.')
|
|
96
|
+
const { data, error } = await supabase.auth.verifyOtp({
|
|
97
|
+
email,
|
|
98
|
+
token,
|
|
99
|
+
type: 'email',
|
|
124
100
|
})
|
|
101
|
+
if (error) throw error
|
|
102
|
+
return data.session
|
|
125
103
|
}
|
|
126
104
|
|
|
127
|
-
export async function loginWithMagicLink(email) {
|
|
105
|
+
export async function loginWithMagicLink(email, { code } = {}) {
|
|
128
106
|
loadDotEnv()
|
|
129
|
-
const preferred = Number(process.env.AMORPHOUS_CLI_CALLBACK_PORT || 8765)
|
|
130
|
-
const port = await freePort(preferred)
|
|
131
|
-
const redirectTo = `http://127.0.0.1:${port}/callback`
|
|
132
107
|
const supabase = createClient(supabaseUrl(), supabaseAnonKey())
|
|
133
|
-
const sinceMs = Date.now()
|
|
134
108
|
|
|
135
|
-
|
|
136
|
-
|
|
109
|
+
if (!code && (await mailpitReachable())) {
|
|
110
|
+
const preferred = Number(process.env.AMORPHOUS_CLI_CALLBACK_PORT || 8765)
|
|
111
|
+
const port = await freePort(preferred)
|
|
112
|
+
const redirectTo = `http://127.0.0.1:${port}/callback`
|
|
113
|
+
const sinceMs = Date.now()
|
|
137
114
|
const { error } = await supabase.auth.signInWithOtp({
|
|
138
115
|
email,
|
|
139
116
|
options: { emailRedirectTo: redirectTo },
|
|
140
117
|
})
|
|
141
118
|
if (error) throw error
|
|
142
|
-
console.error(`
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
tokens = await loginViaCallbackServer(supabase, email, redirectTo)
|
|
119
|
+
console.error(`Code sent to ${email}. Reading the local mail catcher…`)
|
|
120
|
+
finish(await loginViaMailpit(supabase, email, redirectTo, sinceMs))
|
|
121
|
+
return
|
|
146
122
|
}
|
|
147
123
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
124
|
+
if (!code) {
|
|
125
|
+
const { error } = await supabase.auth.signInWithOtp({ email })
|
|
126
|
+
if (error) throw error
|
|
127
|
+
console.error(`Code sent to ${email}.`)
|
|
128
|
+
code = await readCode()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
finish(await verifyEmailCode(supabase, email, code))
|
|
153
132
|
}
|