uniweb 0.12.35 → 0.12.37
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 +2 -2
- package/partials/agents.md +11 -11
- package/src/backend/client.js +78 -24
- package/src/backend/foundation-bring-along.js +229 -0
- package/src/backend/payment-handoff.js +105 -0
- package/src/backend/site-sync.js +2 -2
- package/src/commands/build.js +39 -35
- package/src/commands/clone.js +3 -3
- package/src/commands/deploy.js +95 -424
- package/src/commands/export.js +5 -3
- package/src/commands/publish.js +285 -95
- package/src/commands/pull.js +7 -5
- package/src/commands/push.js +8 -6
- package/src/commands/register.js +13 -5
- package/src/commands/rename.js +3 -2
- package/src/commands/runtime.js +1 -1
- package/src/commands/status.js +24 -5
- package/src/framework-index.json +5 -5
- package/src/index.js +63 -48
- package/src/utils/asset-upload.js +3 -3
- package/src/utils/code-upload.js +43 -3
- package/src/utils/config.js +30 -5
- package/src/utils/registry-auth.js +84 -33
|
@@ -35,6 +35,11 @@ export function getAuthDir() {
|
|
|
35
35
|
return join(homedir(), '.uniweb')
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** Normalize a backend URL to a bare origin (for stamping on the session). */
|
|
39
|
+
function normOrigin(u) {
|
|
40
|
+
try { return new URL(u).origin } catch { return u }
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
/** True when a stored session's `expiresAt` is in the past (absent → never expires). */
|
|
39
44
|
export function isExpired(auth) {
|
|
40
45
|
if (!auth?.expiresAt) return false
|
|
@@ -128,7 +133,7 @@ export async function loginToRegistry({ apiBase, username, password } = {}) {
|
|
|
128
133
|
// the shared isExpired() works unchanged. Tolerant of the pre-#2 flat body
|
|
129
134
|
// (no `account` key) — token + expiry still resolve, identity is just skipped.
|
|
130
135
|
const account = data.account || {}
|
|
131
|
-
const record = { token: data.token }
|
|
136
|
+
const record = { token: data.token, origin: normOrigin(apiBase) }
|
|
132
137
|
if (data.expires_at) record.expiresAt = data.expires_at
|
|
133
138
|
if (account.uuid) record.uuid = account.uuid
|
|
134
139
|
if (account.username) record.username = account.username
|
|
@@ -244,7 +249,7 @@ async function loginViaTokenPaste({ apiBase, nonInteractive }) {
|
|
|
244
249
|
}, { onCancel: () => { console.log('\nLogin cancelled.'); process.exit(0) } })
|
|
245
250
|
if (!token) process.exit(1)
|
|
246
251
|
const account = await fetchMe({ apiBase, token }) // throws if the token is invalid
|
|
247
|
-
const record = { token }
|
|
252
|
+
const record = { token, origin: normOrigin(apiBase) }
|
|
248
253
|
if (account?.uuid) record.uuid = account.uuid
|
|
249
254
|
if (account?.username) record.username = account.username
|
|
250
255
|
if (account?.handle) record.handle = account.handle
|
|
@@ -265,35 +270,50 @@ async function openBrowser(url) {
|
|
|
265
270
|
}
|
|
266
271
|
}
|
|
267
272
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
273
|
+
/**
|
|
274
|
+
* One-shot browser loopback — the reusable primitive behind both `uniweb login`
|
|
275
|
+
* (token-in-redirect) and `uniweb publish`'s payment handoff (done-signal).
|
|
276
|
+
*
|
|
277
|
+
* Hosts a one-shot `127.0.0.1` server on an ephemeral port, opens the browser to
|
|
278
|
+
* a URL built from that port, and resolves once the browser is redirected back
|
|
279
|
+
* to `/callback`. The value never leaves browser→localhost. Provider-agnostic:
|
|
280
|
+
* the CALLER supplies the URL to open (given the loopback redirect URI) and a
|
|
281
|
+
* validator that inspects the callback query — so the same tested server serves
|
|
282
|
+
* any "open a page, wait for it to come back" flow.
|
|
283
|
+
*
|
|
284
|
+
* @param {object} o
|
|
285
|
+
* @param {(redirectUri: string) => string} o.buildUrl - the URL to open, given the loopback /callback URI
|
|
286
|
+
* @param {(params: URLSearchParams) => ({ value: any } | { error: string })} o.validate
|
|
287
|
+
* - inspect the callback query; return `{ value }` to succeed or `{ error }` to fail
|
|
288
|
+
* @param {number} [o.timeoutMs=120000]
|
|
289
|
+
* @param {string} [o.openingLabel] - the "opening…" line
|
|
290
|
+
* @param {string} [o.waitingLabel] - the "waiting…" line
|
|
291
|
+
* @param {string} [o.okTitle='Done'] - success page heading
|
|
292
|
+
* @param {string} [o.errTitle='Something went wrong'] - failure page heading
|
|
293
|
+
* @returns {Promise<any>} the validated `value`
|
|
294
|
+
* @throws on validation error, timeout, or a loopback server error
|
|
295
|
+
*/
|
|
296
|
+
export async function awaitBrowserCallback({
|
|
297
|
+
buildUrl,
|
|
298
|
+
validate,
|
|
299
|
+
timeoutMs = 120000,
|
|
300
|
+
openingLabel = 'Opening your browser…',
|
|
301
|
+
waitingLabel,
|
|
302
|
+
okTitle = 'Done',
|
|
303
|
+
errTitle = 'Something went wrong',
|
|
304
|
+
} = {}) {
|
|
280
305
|
const result = await new Promise((resolve) => {
|
|
281
306
|
const server = createServer((req, res) => {
|
|
282
307
|
const u = new URL(req.url, 'http://127.0.0.1')
|
|
283
308
|
if (u.pathname !== '/callback') { res.writeHead(404); res.end('Not found'); return }
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
const gotState = u.searchParams.get('state')
|
|
287
|
-
const failed = !!error || !token || gotState !== state
|
|
309
|
+
const verdict = validate(u.searchParams) || { error: 'no result from the callback.' }
|
|
310
|
+
const failed = !!verdict.error
|
|
288
311
|
res.writeHead(failed ? 400 : 200, { 'Content-Type': 'text/html' })
|
|
289
312
|
res.end(`<!doctype html><html><body style="font-family:system-ui,sans-serif;text-align:center;padding:60px">`
|
|
290
|
-
+ `<h2 style="color:${failed ? '#dc2626' : '#16a34a'}">${failed ?
|
|
313
|
+
+ `<h2 style="color:${failed ? '#dc2626' : '#16a34a'}">${failed ? errTitle : okTitle}</h2>`
|
|
291
314
|
+ `<p>You can close this tab and return to your terminal.</p></body></html>`)
|
|
292
315
|
cleanup()
|
|
293
|
-
|
|
294
|
-
else if (gotState !== state) resolve({ error: 'state mismatch — please try again.' })
|
|
295
|
-
else if (!token) resolve({ error: 'no token returned by the callback.' })
|
|
296
|
-
else resolve({ token })
|
|
316
|
+
resolve(failed ? { error: verdict.error } : { value: verdict.value })
|
|
297
317
|
})
|
|
298
318
|
let timer
|
|
299
319
|
function cleanup() { clearTimeout(timer); server.close() }
|
|
@@ -301,20 +321,51 @@ async function loginViaBrowser({ apiBase }) {
|
|
|
301
321
|
server.listen(0, '127.0.0.1', async () => {
|
|
302
322
|
const { port } = server.address()
|
|
303
323
|
const redirectUri = `http://127.0.0.1:${port}/callback`
|
|
304
|
-
const
|
|
305
|
-
console.log(
|
|
306
|
-
console.log(` \x1b[2m${
|
|
307
|
-
const opened = await openBrowser(
|
|
324
|
+
const url = buildUrl(redirectUri)
|
|
325
|
+
console.log(`\x1b[36m→\x1b[0m ${openingLabel}`)
|
|
326
|
+
console.log(` \x1b[2m${url}\x1b[0m`)
|
|
327
|
+
const opened = await openBrowser(url)
|
|
308
328
|
if (!opened) console.log('\x1b[33m⚠\x1b[0m Could not open a browser automatically — open the URL above.')
|
|
309
|
-
console.log(
|
|
329
|
+
console.log(`\x1b[2m${waitingLabel || `Waiting (${Math.round(timeoutMs / 1000)}s)…`}\x1b[0m`)
|
|
310
330
|
})
|
|
311
|
-
timer = setTimeout(() => { server.close(); resolve({ error:
|
|
331
|
+
timer = setTimeout(() => { server.close(); resolve({ error: `timed out (${Math.round(timeoutMs / 1000)}s).` }) }, timeoutMs)
|
|
312
332
|
})
|
|
313
|
-
|
|
314
333
|
if (result.error) throw new Error(result.error)
|
|
334
|
+
return result.value
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Browser / social — loopback OAuth against the backend's /dev/auth/authorize.
|
|
338
|
+
// The CLI hosts a one-shot 127.0.0.1 server (awaitBrowserCallback), opens the
|
|
339
|
+
// browser to authorize, and the backend (after the Google dance) 302s back to
|
|
340
|
+
// the loopback with the token (or an error). state is a CSRF nonce echoed back
|
|
341
|
+
// and verified. The token never leaves browser→localhost. Gated by
|
|
342
|
+
// BROWSER_AVAILABLE until the endpoint is live.
|
|
343
|
+
async function loginViaBrowser({ apiBase }) {
|
|
344
|
+
if (!BROWSER_AVAILABLE) {
|
|
345
|
+
throw new Error('browser/social login for the new backend isn’t available yet — use --password or --token-paste.')
|
|
346
|
+
}
|
|
347
|
+
const base = apiBase.replace(/\/$/, '')
|
|
348
|
+
const state = randomBytes(16).toString('hex')
|
|
349
|
+
|
|
350
|
+
const token = await awaitBrowserCallback({
|
|
351
|
+
buildUrl: (redirectUri) =>
|
|
352
|
+
`${base}/dev/auth/authorize?redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}`,
|
|
353
|
+
validate: (params) => {
|
|
354
|
+
if (params.get('error')) return { error: params.get('error') }
|
|
355
|
+
if (params.get('state') !== state) return { error: 'state mismatch — please try again.' }
|
|
356
|
+
const tok = params.get('token')
|
|
357
|
+
if (!tok) return { error: 'no token returned by the callback.' }
|
|
358
|
+
return { value: tok }
|
|
359
|
+
},
|
|
360
|
+
openingLabel: 'Opening your browser to sign in…',
|
|
361
|
+
waitingLabel: 'Waiting for sign-in to complete (120s)…',
|
|
362
|
+
okTitle: 'Login successful',
|
|
363
|
+
errTitle: 'Login failed',
|
|
364
|
+
})
|
|
365
|
+
|
|
315
366
|
let account = null
|
|
316
|
-
try { account = await fetchMe({ apiBase, token
|
|
317
|
-
const record = { token:
|
|
367
|
+
try { account = await fetchMe({ apiBase, token }) } catch { /* identity optional; token is valid */ }
|
|
368
|
+
const record = { token, origin: normOrigin(apiBase) }
|
|
318
369
|
if (account?.uuid) record.uuid = account.uuid
|
|
319
370
|
if (account?.username) record.username = account.username
|
|
320
371
|
if (account?.handle) record.handle = account.handle
|
|
@@ -359,7 +410,7 @@ export async function runRegistryLogin({ apiBase, args = [] } = {}) {
|
|
|
359
410
|
console.error(`\x1b[31m✗\x1b[0m Token rejected by ${apiBase}: ${err.message}`)
|
|
360
411
|
process.exit(1)
|
|
361
412
|
}
|
|
362
|
-
const record = { token: tokenFlag }
|
|
413
|
+
const record = { token: tokenFlag, origin: normOrigin(apiBase) }
|
|
363
414
|
if (account?.uuid) record.uuid = account.uuid
|
|
364
415
|
if (account?.username) record.username = account.username
|
|
365
416
|
if (account?.handle) record.handle = account.handle
|