@symbo.ls/cli 2.33.34 → 2.33.35
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/bin/login.js +214 -48
- package/package.json +5 -5
package/bin/login.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import inquirer from 'inquirer'
|
|
4
4
|
import chalk from 'chalk'
|
|
5
|
+
import crypto from 'crypto'
|
|
6
|
+
import { spawn } from 'child_process'
|
|
5
7
|
import { program } from './program.js'
|
|
6
8
|
import { getApiUrl, saveCliConfig, loadCliConfig } from '../helpers/config.js'
|
|
7
9
|
import { CredentialManager } from '../helpers/credentialManager.js'
|
|
@@ -10,7 +12,7 @@ function websiteFromApi(apiBaseUrl) {
|
|
|
10
12
|
try {
|
|
11
13
|
const u = new URL(apiBaseUrl)
|
|
12
14
|
const host = u.host
|
|
13
|
-
if (apiBaseUrl.startsWith('http://localhost')) return '
|
|
15
|
+
if (apiBaseUrl.startsWith('http://localhost')) return 'https://2dd379117f83.ngrok-free.app'
|
|
14
16
|
if (host === 'api.dev.symbols.app') return 'https://dev.symbols.app'
|
|
15
17
|
if (host === 'api.staging.symbols.app') return 'https://staging.symbols.app'
|
|
16
18
|
if (host === 'api.test.symbols.app') return 'https://test.symbols.app'
|
|
@@ -23,6 +25,82 @@ function websiteFromApi(apiBaseUrl) {
|
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
function openBrowser(url) {
|
|
29
|
+
try {
|
|
30
|
+
const platform = process.platform
|
|
31
|
+
if (platform === 'darwin') {
|
|
32
|
+
spawn('open', [url], { stdio: 'ignore', detached: true }).unref()
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
if (platform === 'win32') {
|
|
36
|
+
// Use cmd's "start" to open default browser
|
|
37
|
+
spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref()
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
// linux, etc.
|
|
41
|
+
spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref()
|
|
42
|
+
return true
|
|
43
|
+
} catch (_) {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractAuthFromResponse(data) {
|
|
49
|
+
const user = data?.data?.user || data?.user
|
|
50
|
+
const tokens = data?.data?.tokens || data?.tokens || {}
|
|
51
|
+
const accessToken =
|
|
52
|
+
tokens?.accessToken ||
|
|
53
|
+
data?.token ||
|
|
54
|
+
data?.accessToken ||
|
|
55
|
+
data?.jwt ||
|
|
56
|
+
data?.data?.token ||
|
|
57
|
+
data?.data?.accessToken ||
|
|
58
|
+
data?.data?.jwt
|
|
59
|
+
const refreshToken = tokens?.refreshToken || null
|
|
60
|
+
const accessTokenExp = tokens?.accessTokenExp?.expiresAt || tokens?.accessTokenExp || null
|
|
61
|
+
return { user, accessToken, refreshToken, accessTokenExp }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function randomVerifier(len = 64) {
|
|
65
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'
|
|
66
|
+
let out = ''
|
|
67
|
+
for (let i = 0; i < len; i++) out += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
68
|
+
return out
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function base64urlFromBuffer(buf) {
|
|
72
|
+
return Buffer.from(buf)
|
|
73
|
+
.toString('base64')
|
|
74
|
+
.replace(/\+/g, '-')
|
|
75
|
+
.replace(/\//g, '_')
|
|
76
|
+
.replace(/=+$/g, '')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sha256Base64url(input) {
|
|
80
|
+
const digest = crypto.createHash('sha256').update(input).digest()
|
|
81
|
+
return base64urlFromBuffer(digest)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function extractSessionStatus(data) {
|
|
85
|
+
return (
|
|
86
|
+
data?.status ||
|
|
87
|
+
data?.data?.status ||
|
|
88
|
+
data?.state ||
|
|
89
|
+
data?.data?.state ||
|
|
90
|
+
null
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function extractConfirmToken(data) {
|
|
95
|
+
return (
|
|
96
|
+
data?.access_token ||
|
|
97
|
+
data?.data?.access_token ||
|
|
98
|
+
data?.token ||
|
|
99
|
+
data?.data?.token ||
|
|
100
|
+
null
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
26
104
|
program
|
|
27
105
|
.command('login')
|
|
28
106
|
.description('Sign in to Symbols')
|
|
@@ -33,7 +111,7 @@ program
|
|
|
33
111
|
|
|
34
112
|
// Prompt for credentials
|
|
35
113
|
const currentConfig = loadCliConfig()
|
|
36
|
-
const
|
|
114
|
+
const first = await inquirer.prompt([
|
|
37
115
|
{
|
|
38
116
|
type: 'input',
|
|
39
117
|
name: 'apiBaseUrl',
|
|
@@ -42,72 +120,160 @@ program
|
|
|
42
120
|
validate: input => /^https?:\/\//.test(input) || '❌ Please enter a valid URL'
|
|
43
121
|
},
|
|
44
122
|
{
|
|
45
|
-
type: '
|
|
46
|
-
name: '
|
|
47
|
-
message: '
|
|
48
|
-
|
|
123
|
+
type: 'list',
|
|
124
|
+
name: 'method',
|
|
125
|
+
message: 'Sign in method:',
|
|
126
|
+
choices: [
|
|
127
|
+
{ name: 'Email + Password', value: 'password' },
|
|
128
|
+
// Uses plugin-style PKCE session flow so providers don't need localhost redirects
|
|
129
|
+
{ name: 'Continue with Google (opens browser)', value: 'google_browser' },
|
|
130
|
+
{ name: 'Continue with GitHub (opens browser)', value: 'github_browser' },
|
|
131
|
+
],
|
|
132
|
+
default: 'password',
|
|
49
133
|
},
|
|
50
|
-
{
|
|
51
|
-
type: 'password',
|
|
52
|
-
name: 'password',
|
|
53
|
-
message: 'Password:',
|
|
54
|
-
validate: input => input.length >= 6 || '❌ Password must be at least 6 characters'
|
|
55
|
-
}
|
|
56
134
|
])
|
|
57
135
|
|
|
58
136
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
137
|
+
let data
|
|
138
|
+
let apiBaseUrl = first.apiBaseUrl
|
|
139
|
+
|
|
140
|
+
if (first.method === 'password') {
|
|
141
|
+
const answers = await inquirer.prompt([
|
|
142
|
+
{
|
|
143
|
+
type: 'input',
|
|
144
|
+
name: 'email',
|
|
145
|
+
message: 'Email:',
|
|
146
|
+
validate: input => input.includes('@') || '❌ Please enter a valid email address'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: 'password',
|
|
150
|
+
name: 'password',
|
|
151
|
+
message: 'Password:',
|
|
152
|
+
validate: input => input.length >= 6 || '❌ Password must be at least 6 characters'
|
|
153
|
+
}
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
// Make login request
|
|
157
|
+
console.log(chalk.dim('\nAuthenticating...'))
|
|
158
|
+
const response = await fetch(`${apiBaseUrl}/core/auth/login`, {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
161
|
+
body: JSON.stringify({ email: answers.email, password: answers.password })
|
|
69
162
|
})
|
|
70
|
-
})
|
|
71
163
|
|
|
72
|
-
|
|
164
|
+
data = await response.json()
|
|
165
|
+
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
const msg = data?.message || data?.error || `Authentication failed (${response.status})`
|
|
168
|
+
const err = new Error(msg)
|
|
169
|
+
err.response = { status: response.status, data }
|
|
170
|
+
throw err
|
|
171
|
+
}
|
|
172
|
+
} else if (first.method === 'google_browser' || first.method === 'github_browser') {
|
|
173
|
+
const sessionId = crypto.randomUUID()
|
|
174
|
+
const codeVerifier = randomVerifier(64)
|
|
175
|
+
const codeChallenge = sha256Base64url(codeVerifier)
|
|
176
|
+
|
|
177
|
+
console.log(chalk.dim('\nCreating secure sign-in session...'))
|
|
178
|
+
const sessionResp = await fetch(`${apiBaseUrl}/core/auth/session`, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: { 'Content-Type': 'application/json' },
|
|
181
|
+
body: JSON.stringify({
|
|
182
|
+
session_id: sessionId,
|
|
183
|
+
code_challenge: codeChallenge,
|
|
184
|
+
plugin_info: { version: 'cli', figma_env: 'cli' }
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const sessionData = await sessionResp.json().catch(async () => ({ message: await sessionResp.text() }))
|
|
189
|
+
if (!sessionResp.ok) {
|
|
190
|
+
const msg = sessionData?.message || sessionData?.error || `Failed to create session (${sessionResp.status})`
|
|
191
|
+
throw new Error(msg)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const website = websiteFromApi(apiBaseUrl)
|
|
195
|
+
const signinUrl = `${website.replace(/\/+$/,'')}/signin?session=${encodeURIComponent(sessionId)}`
|
|
196
|
+
|
|
197
|
+
console.log(chalk.dim('\nOpening your browser to complete sign-in...'))
|
|
198
|
+
if (!openBrowser(signinUrl)) {
|
|
199
|
+
console.log(chalk.yellow('\nCould not auto-open a browser. Please open this URL manually:\n'))
|
|
200
|
+
console.log(chalk.cyan(signinUrl) + '\n')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.log(chalk.gray('Waiting for you to finish signing in (Ctrl+C to cancel)...'))
|
|
204
|
+
|
|
205
|
+
const startedAt = Date.now()
|
|
206
|
+
const timeoutMs = 3 * 60 * 1000
|
|
207
|
+
const pollMs = 1500
|
|
208
|
+
|
|
209
|
+
// Poll until server says session is ready_for_confirm
|
|
210
|
+
// (Website login/register will attach ?session=... to mark the session ready)
|
|
211
|
+
while (true) {
|
|
212
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
213
|
+
throw new Error('Timed out waiting for sign-in to complete in the browser')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const statusResp = await fetch(
|
|
217
|
+
`${apiBaseUrl}/core/auth/session/status?session=${encodeURIComponent(sessionId)}`
|
|
218
|
+
)
|
|
219
|
+
const statusData = await statusResp.json().catch(async () => ({ message: await statusResp.text() }))
|
|
220
|
+
|
|
221
|
+
if (!statusResp.ok) {
|
|
222
|
+
const msg = statusData?.message || statusData?.error || `Failed to check session status (${statusResp.status})`
|
|
223
|
+
throw new Error(msg)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const status = extractSessionStatus(statusData)
|
|
227
|
+
if (status === 'ready_for_confirm') break
|
|
228
|
+
if (status === 'expired' || status === 'revoked' || status === 'invalid') {
|
|
229
|
+
throw new Error(`Session became unusable (${status})`)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await new Promise(resolve => setTimeout(resolve, pollMs))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log(chalk.dim('Confirming session...'))
|
|
236
|
+
const confirmResp = await fetch(`${apiBaseUrl}/core/auth/session/confirm`, {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
headers: { 'Content-Type': 'application/json' },
|
|
239
|
+
body: JSON.stringify({ session_id: sessionId, code_verifier: codeVerifier })
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const confirmData = await confirmResp.json().catch(async () => ({ message: await confirmResp.text() }))
|
|
243
|
+
if (!confirmResp.ok) {
|
|
244
|
+
const msg = confirmData?.message || confirmData?.error || `Failed to confirm session (${confirmResp.status})`
|
|
245
|
+
throw new Error(msg)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const token = extractConfirmToken(confirmData)
|
|
249
|
+
if (!token) {
|
|
250
|
+
throw new Error('Sign-in succeeded but no access token was returned by the server')
|
|
251
|
+
}
|
|
73
252
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
throw err
|
|
253
|
+
// Normalize shape to reuse existing credential save logic
|
|
254
|
+
data = { token }
|
|
255
|
+
} else {
|
|
256
|
+
throw new Error('Unknown login method')
|
|
79
257
|
}
|
|
80
258
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const tokens = data?.data?.tokens || data?.tokens || {}
|
|
84
|
-
const token =
|
|
85
|
-
tokens?.accessToken ||
|
|
86
|
-
data?.token ||
|
|
87
|
-
data?.accessToken ||
|
|
88
|
-
data?.jwt ||
|
|
89
|
-
data?.data?.token ||
|
|
90
|
-
data?.data?.accessToken ||
|
|
91
|
-
data?.data?.jwt
|
|
92
|
-
if (!token) {
|
|
259
|
+
const { user, accessToken, refreshToken, accessTokenExp } = extractAuthFromResponse(data)
|
|
260
|
+
if (!accessToken) {
|
|
93
261
|
throw new Error('Login succeeded but no token was returned by the server')
|
|
94
262
|
}
|
|
95
|
-
const refreshToken = tokens?.refreshToken || null
|
|
96
|
-
const accessTokenExp = tokens?.accessTokenExp?.expiresAt || null
|
|
97
263
|
|
|
98
264
|
// Save credentials
|
|
99
265
|
const credManager = new CredentialManager()
|
|
100
266
|
credManager.saveCredentials({
|
|
101
|
-
apiBaseUrl
|
|
102
|
-
authToken:
|
|
267
|
+
apiBaseUrl,
|
|
268
|
+
authToken: accessToken,
|
|
103
269
|
refreshToken,
|
|
104
270
|
authTokenExpiresAt: accessTokenExp,
|
|
105
271
|
userId: user?.id || data?.userId,
|
|
106
|
-
email: user?.email ||
|
|
272
|
+
email: user?.email || null
|
|
107
273
|
})
|
|
108
274
|
|
|
109
275
|
// Persist API base URL to local config for this project as well
|
|
110
|
-
saveCliConfig({ apiBaseUrl
|
|
276
|
+
saveCliConfig({ apiBaseUrl })
|
|
111
277
|
|
|
112
278
|
console.log(chalk.green('\n✨ Successfully logged in!'))
|
|
113
279
|
console.log(chalk.white('\nYou can now use Symbols CLI commands:'))
|
|
@@ -120,7 +286,7 @@ program
|
|
|
120
286
|
console.log(chalk.dim('For more commands, run: smbls --help\n'))
|
|
121
287
|
|
|
122
288
|
} catch (error) {
|
|
123
|
-
const website = websiteFromApi(
|
|
289
|
+
const website = websiteFromApi(first.apiBaseUrl)
|
|
124
290
|
console.log(chalk.red('\n❌ Login failed'))
|
|
125
291
|
console.log(chalk.white('\nError:'))
|
|
126
292
|
console.log(chalk.yellow(error.message || 'Unknown error'))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/cli",
|
|
3
|
-
"version": "2.33.
|
|
3
|
+
"version": "2.33.35",
|
|
4
4
|
"description": "Fetch your Symbols configuration",
|
|
5
5
|
"main": "bin/fetch.js",
|
|
6
6
|
"author": "Symbols",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"vpatch": "npm version patch && npm publish"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@symbo.ls/fetch": "^2.33.
|
|
19
|
-
"@symbo.ls/init": "^2.33.
|
|
20
|
-
"@symbo.ls/socket": "^2.33.
|
|
18
|
+
"@symbo.ls/fetch": "^2.33.35",
|
|
19
|
+
"@symbo.ls/init": "^2.33.35",
|
|
20
|
+
"@symbo.ls/socket": "^2.33.35",
|
|
21
21
|
"chalk": "^5.4.1",
|
|
22
22
|
"chokidar": "^4.0.3",
|
|
23
23
|
"commander": "^13.1.0",
|
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
"socket.io-client": "^4.8.1",
|
|
29
29
|
"v8-compile-cache": "^2.4.0"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "3ad860e75a24c41afaac79d639f7a9162c2f4c81"
|
|
32
32
|
}
|