magic-spells 0.1.0 → 0.2.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/README.md +41 -17
- package/dist/{chunk-S5IUP7G2.js → chunk-4Y3SKWF7.js} +1 -1
- package/dist/{chunk-YT2Z756Z.js → chunk-ZC4NCQBG.js} +750 -50
- package/dist/main.js +618 -64
- package/dist/{operations-P3DUAVPP.js → operations-VJZEQHSW.js} +2 -2
- package/dist/{src-4S3XHPWY.js → src-56NZ53TG.js} +7 -234
- package/dist/src-DJYPPJFM.js +16 -0
- package/dist/src-KJGFPRQP.js +17 -0
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -1,31 +1,85 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AccountClient,
|
|
3
|
+
GRANT_ITEM,
|
|
2
4
|
MagicError,
|
|
5
|
+
authorizeUrl,
|
|
3
6
|
buildContext,
|
|
4
7
|
cell,
|
|
5
8
|
configFilePath,
|
|
6
9
|
contextFor,
|
|
7
10
|
createCredentialStore,
|
|
8
11
|
createMcpServer,
|
|
12
|
+
createPkce,
|
|
9
13
|
exitCodeFor,
|
|
10
14
|
forgetProductKey,
|
|
15
|
+
forgetSessions,
|
|
16
|
+
grantExpired,
|
|
17
|
+
grantFromToken,
|
|
18
|
+
grantIdleExpiry,
|
|
19
|
+
isMagicError,
|
|
11
20
|
jsonMode,
|
|
21
|
+
loadCliConfig,
|
|
12
22
|
manualItem,
|
|
13
23
|
maskKey,
|
|
24
|
+
mintSessions,
|
|
14
25
|
parseGlobals,
|
|
15
26
|
processIO,
|
|
16
27
|
productConfig,
|
|
17
28
|
productEnvVar,
|
|
29
|
+
productSessions,
|
|
30
|
+
randomState,
|
|
18
31
|
readConfigFile,
|
|
32
|
+
readField,
|
|
33
|
+
readGrant,
|
|
34
|
+
readIndex,
|
|
19
35
|
readPackageVersion,
|
|
36
|
+
readStr,
|
|
37
|
+
rec,
|
|
38
|
+
remintFor,
|
|
20
39
|
render,
|
|
21
40
|
renderError,
|
|
22
41
|
resolveCredential,
|
|
42
|
+
revokeSessions,
|
|
23
43
|
runDoctorChecks,
|
|
24
44
|
runProductCli,
|
|
45
|
+
runWithRemint,
|
|
46
|
+
saveGrant,
|
|
25
47
|
saveProductKey,
|
|
48
|
+
sessionItem,
|
|
49
|
+
startLoopback,
|
|
50
|
+
storeSessions,
|
|
26
51
|
toMagicError,
|
|
27
|
-
usage
|
|
28
|
-
|
|
52
|
+
usage,
|
|
53
|
+
waitForDevice
|
|
54
|
+
} from "./chunk-ZC4NCQBG.js";
|
|
55
|
+
|
|
56
|
+
// src/browser.ts
|
|
57
|
+
import { spawn } from "node:child_process";
|
|
58
|
+
import { createInterface } from "node:readline/promises";
|
|
59
|
+
function browserIsLocal(env, platform) {
|
|
60
|
+
if (env.SSH_CONNECTION) return false;
|
|
61
|
+
if (platform === "linux") return Boolean(env.DISPLAY || env.WAYLAND_DISPLAY);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
function openBrowser(url) {
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
const platform = process.platform;
|
|
67
|
+
const command = platform === "darwin" ? "open" : platform === "win32" ? "rundll32" : "xdg-open";
|
|
68
|
+
const args = platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
69
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
70
|
+
child.once("error", () => resolve(false));
|
|
71
|
+
child.once("close", (code) => resolve(code === 0));
|
|
72
|
+
child.unref();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async function confirmOnTty(question) {
|
|
76
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
77
|
+
try {
|
|
78
|
+
return /^y(es)?$/i.test((await rl.question(question)).trim());
|
|
79
|
+
} finally {
|
|
80
|
+
rl.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
29
83
|
|
|
30
84
|
// src/version.ts
|
|
31
85
|
var cached;
|
|
@@ -93,9 +147,10 @@ async function doctor(shell, argv) {
|
|
|
93
147
|
}
|
|
94
148
|
rejectWorkspace("doctor", shell.opts);
|
|
95
149
|
noMoreArgs("doctor", argv);
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
150
|
+
const products = await loadProducts(shell);
|
|
151
|
+
const checks = [...shellChecks(shell), ...await accountChecks(shell)];
|
|
152
|
+
const signedIn = await anyCredential(shell, products);
|
|
153
|
+
for (const product of products) checks.push(...await productChecks(shell, product, signedIn));
|
|
99
154
|
const ok = checks.every((c) => c.ok);
|
|
100
155
|
if (jsonMode(shell.opts, shell.io)) render({ ok, checks }, shell.opts, shell.io);
|
|
101
156
|
else printHuman(shell, checks);
|
|
@@ -123,7 +178,79 @@ function shellChecks(shell) {
|
|
|
123
178
|
}
|
|
124
179
|
];
|
|
125
180
|
}
|
|
126
|
-
async function
|
|
181
|
+
async function accountChecks(shell) {
|
|
182
|
+
const grant = await readGrant(shell.store);
|
|
183
|
+
let accountUrl;
|
|
184
|
+
try {
|
|
185
|
+
accountUrl = loadCliConfig({
|
|
186
|
+
products: [],
|
|
187
|
+
env: shell.env,
|
|
188
|
+
file: shell.readConfig()
|
|
189
|
+
}).accountUrl;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
const e = toMagicError(err);
|
|
192
|
+
return [{ name: "account", product: "account", ok: false, code: e.code, detail: e.message }];
|
|
193
|
+
}
|
|
194
|
+
const results = [];
|
|
195
|
+
try {
|
|
196
|
+
const res = await fetch(accountUrl, {
|
|
197
|
+
method: "HEAD",
|
|
198
|
+
signal: AbortSignal.timeout(REACH_TIMEOUT_MS)
|
|
199
|
+
});
|
|
200
|
+
results.push({
|
|
201
|
+
name: "account",
|
|
202
|
+
product: "account",
|
|
203
|
+
ok: true,
|
|
204
|
+
detail: `${accountUrl} answers (HTTP ${res.status})`
|
|
205
|
+
});
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const e = toMagicError(networkish(err), {
|
|
208
|
+
product: "account",
|
|
209
|
+
networkHint: "Check MAGIC_ACCOUNT_URL and your network connection."
|
|
210
|
+
});
|
|
211
|
+
const detail = e.hint ? `${e.message} \u2014 ${e.hint}` : e.message;
|
|
212
|
+
results.push(
|
|
213
|
+
grant ? { name: "account", product: "account", ok: false, code: e.code, detail } : { name: "account", product: "account", ok: true, warn: true, detail }
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (!grant) {
|
|
217
|
+
results.push({
|
|
218
|
+
name: "account grant",
|
|
219
|
+
product: "account",
|
|
220
|
+
ok: true,
|
|
221
|
+
warn: true,
|
|
222
|
+
detail: "Not signed in (run `magic login`); product keys only."
|
|
223
|
+
});
|
|
224
|
+
} else if (grantExpired(grant)) {
|
|
225
|
+
results.push({
|
|
226
|
+
name: "account grant",
|
|
227
|
+
product: "account",
|
|
228
|
+
ok: false,
|
|
229
|
+
code: "not_logged_in",
|
|
230
|
+
detail: "Your sign-in has ended \u2014 run `magic login`."
|
|
231
|
+
});
|
|
232
|
+
} else {
|
|
233
|
+
const who = grant.user ? `${grant.user.display_name} <${grant.user.email}>` : "signed in";
|
|
234
|
+
results.push({
|
|
235
|
+
name: "account grant",
|
|
236
|
+
product: "account",
|
|
237
|
+
ok: true,
|
|
238
|
+
detail: `${who} until ${grant.expires_at.slice(0, 10)} (idle sign-out after ${grantIdleExpiry(grant).slice(0, 10)})`,
|
|
239
|
+
data: { user: grant.user ?? null, expires_at: grant.expires_at }
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return results;
|
|
243
|
+
}
|
|
244
|
+
async function anyCredential(shell, products) {
|
|
245
|
+
if (await readGrant(shell.store)) return true;
|
|
246
|
+
if (Object.keys(await readIndex(shell.store)).length > 0) return true;
|
|
247
|
+
for (const p of products) {
|
|
248
|
+
if (shell.env[productEnvVar(p.id, "KEY")]?.trim()) return true;
|
|
249
|
+
if (await shell.store.locate(manualItem(p.id)) !== null) return true;
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
async function productChecks(shell, product, signedIn) {
|
|
127
254
|
const id = product.id;
|
|
128
255
|
const networkHint = `Check ${productEnvVar(id, "URL")} and your network connection.`;
|
|
129
256
|
const fail = (name, err) => {
|
|
@@ -160,22 +287,39 @@ async function productChecks(shell, product) {
|
|
|
160
287
|
}
|
|
161
288
|
let session;
|
|
162
289
|
try {
|
|
163
|
-
|
|
290
|
+
const workspace = config.defaultWorkspace[id];
|
|
291
|
+
session = await resolveCredential(product, {
|
|
292
|
+
env: shell.env,
|
|
293
|
+
store: shell.store,
|
|
294
|
+
...workspace ? { workspace } : {}
|
|
295
|
+
});
|
|
164
296
|
} catch (err) {
|
|
165
|
-
|
|
297
|
+
const failed = fail(`${id} key`, err);
|
|
298
|
+
const soft = failed.code === "usage" || failed.code === "not_logged_in" && signedIn;
|
|
299
|
+
results.push(soft ? { ...failed, ok: true, warn: true } : failed);
|
|
166
300
|
return results;
|
|
167
301
|
}
|
|
302
|
+
let ctx = contextFor(product, config, session);
|
|
303
|
+
if (session.source === "account" && Date.parse(session.expires_at ?? "") <= Date.now()) {
|
|
304
|
+
try {
|
|
305
|
+
ctx = await remintFor(product, shell.store)(ctx) ?? ctx;
|
|
306
|
+
} catch (err) {
|
|
307
|
+
results.push(fail(`${id} key`, err));
|
|
308
|
+
return results;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
168
311
|
results.push({
|
|
169
312
|
name: `${id} key`,
|
|
170
313
|
product: id,
|
|
171
314
|
ok: true,
|
|
172
|
-
detail: `${maskKey(session.key, product.credential)} from ${session.source
|
|
315
|
+
detail: `${maskKey(ctx.session.key, product.credential)} from ${keySource(session.source)}${session.workspace ? ` (workspace ${session.workspace.handle})` : ""}`
|
|
173
316
|
});
|
|
174
|
-
results.push(
|
|
175
|
-
...await runDoctorChecks(product.doctor ?? [], contextFor(product, config, session), id)
|
|
176
|
-
);
|
|
317
|
+
results.push(...await runDoctorChecks(product.doctor ?? [], ctx, id));
|
|
177
318
|
return results;
|
|
178
319
|
}
|
|
320
|
+
function keySource(source) {
|
|
321
|
+
return source === "env" ? "the environment" : source === "account" ? "your sign-in" : "a pasted key";
|
|
322
|
+
}
|
|
179
323
|
function networkish(err) {
|
|
180
324
|
if (err instanceof Error && err.cause instanceof Error) {
|
|
181
325
|
return new Error(`${err.message}: ${err.cause.message}`);
|
|
@@ -193,15 +337,23 @@ function printHuman(shell, checks) {
|
|
|
193
337
|
}
|
|
194
338
|
|
|
195
339
|
// src/commands/login.ts
|
|
196
|
-
var LOGIN_HELP = `Usage: magic login --product
|
|
340
|
+
var LOGIN_HELP = `Usage: magic login [--device] [--product <product>] [--yes]
|
|
341
|
+
magic login --product-key [<product>]
|
|
197
342
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
343
|
+
Signs in to your Magic Spells account, then mints a short-lived key in each product
|
|
344
|
+
for every workspace you belong to. Opens your browser when one is on this machine;
|
|
345
|
+
otherwise (or with --device) prints a code to enter on any device.
|
|
346
|
+
|
|
347
|
+
--device sign in with a code instead of a browser on this machine
|
|
348
|
+
--product <product> mint (or re-mint) one product's keys
|
|
349
|
+
--yes, -y replace an existing sign-in without asking
|
|
350
|
+
--product-key store a pasted product key instead (from a hidden prompt or
|
|
351
|
+
stdin; for CI). Without <product>, the key's prefix picks it.
|
|
201
352
|
`;
|
|
202
353
|
var LOGOUT_HELP = `Usage: magic logout [--product <product>]
|
|
203
354
|
|
|
204
|
-
|
|
355
|
+
Revokes your product keys and your sign-in, and deletes every stored credential.
|
|
356
|
+
With --product, only that product's keys (you stay signed in).
|
|
205
357
|
`;
|
|
206
358
|
async function login(shell, argv) {
|
|
207
359
|
const args = [...argv];
|
|
@@ -210,12 +362,276 @@ async function login(shell, argv) {
|
|
|
210
362
|
return 0;
|
|
211
363
|
}
|
|
212
364
|
rejectWorkspace("login", shell.opts);
|
|
213
|
-
if (
|
|
365
|
+
if (takeSwitch(args, "product-key")) return productKeyLogin(shell, args);
|
|
366
|
+
const device = takeSwitch(args, "device");
|
|
367
|
+
const only = takeValue(args, "product");
|
|
368
|
+
noMoreArgs("login", args);
|
|
369
|
+
if (only) chooseEntries(shell, [only]);
|
|
370
|
+
const all = await loadProducts(shell);
|
|
371
|
+
const targets = all.filter((p) => p.sessions && (!only || p.id === only));
|
|
372
|
+
if (only && targets.length === 0) {
|
|
214
373
|
throw usage(
|
|
215
|
-
|
|
216
|
-
|
|
374
|
+
`${only} does not mint keys from your account.`,
|
|
375
|
+
`Run \`magic login --product-key ${only}\` to store a key.`
|
|
217
376
|
);
|
|
218
377
|
}
|
|
378
|
+
const config = loadCliConfig({
|
|
379
|
+
products: all.map((p) => ({ id: p.id, apiUrl: p.apiUrl })),
|
|
380
|
+
env: shell.env,
|
|
381
|
+
file: shell.readConfig()
|
|
382
|
+
});
|
|
383
|
+
const interrupt = interruptSignal(shell);
|
|
384
|
+
try {
|
|
385
|
+
const old = await readGrant(shell.store);
|
|
386
|
+
const live = old && !grantExpired(old) ? old : null;
|
|
387
|
+
let grant;
|
|
388
|
+
let previous = null;
|
|
389
|
+
if (only && live) {
|
|
390
|
+
grant = live;
|
|
391
|
+
} else {
|
|
392
|
+
if (live) await confirmReplace(shell, live);
|
|
393
|
+
previous = old ? await snapshot(shell, all, old) : null;
|
|
394
|
+
const token = await signIn(shell, config, device, interrupt.signal);
|
|
395
|
+
grant = grantFromToken(token, config.accountUrl);
|
|
396
|
+
await saveGrant(shell.store, grant);
|
|
397
|
+
}
|
|
398
|
+
const minted = await mintAll(shell, config, targets, grant, interrupt.signal);
|
|
399
|
+
grant = await rememberUser(shell, config, minted, grant);
|
|
400
|
+
if (previous) await retire(shell, config, previous, minted);
|
|
401
|
+
const result = loginResult(grant, minted);
|
|
402
|
+
report(shell, result);
|
|
403
|
+
if (jsonMode(shell.opts, shell.io)) render(result, shell.opts, shell.io);
|
|
404
|
+
return 0;
|
|
405
|
+
} finally {
|
|
406
|
+
interrupt.dispose();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function interruptSignal(shell) {
|
|
410
|
+
if (shell.login.interrupt) return { signal: shell.login.interrupt, dispose: () => {
|
|
411
|
+
} };
|
|
412
|
+
const controller = new AbortController();
|
|
413
|
+
const onSigint = () => controller.abort();
|
|
414
|
+
process.once("SIGINT", onSigint);
|
|
415
|
+
return { signal: controller.signal, dispose: () => process.off("SIGINT", onSigint) };
|
|
416
|
+
}
|
|
417
|
+
async function confirmReplace(shell, grant) {
|
|
418
|
+
if (shell.opts.yes) return;
|
|
419
|
+
const who = grant.user ? `${grant.user.display_name} (${grant.user.email})` : "another person";
|
|
420
|
+
if (!shell.confirm) {
|
|
421
|
+
throw usage(`Already logged in as ${who}.`, "Re-run with --yes to replace that sign-in.");
|
|
422
|
+
}
|
|
423
|
+
if (!await shell.confirm(`Already logged in as ${who}. Replace that sign-in? [y/N] `)) {
|
|
424
|
+
throw cancelled();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function signIn(shell, config, forceDevice, signal) {
|
|
428
|
+
const client = new AccountClient({
|
|
429
|
+
accountUrl: config.accountUrl,
|
|
430
|
+
clientId: config.accountClientId
|
|
431
|
+
});
|
|
432
|
+
void client.clientId;
|
|
433
|
+
const local = browserIsLocal(shell.env, shell.platform);
|
|
434
|
+
return !forceDevice && local ? loopbackSignIn(shell, client, signal) : deviceSignIn(shell, client, local, signal);
|
|
435
|
+
}
|
|
436
|
+
async function loopbackSignIn(shell, client, signal) {
|
|
437
|
+
const pkce = createPkce();
|
|
438
|
+
const state = randomState();
|
|
439
|
+
const loopback = await startLoopback({
|
|
440
|
+
state,
|
|
441
|
+
signal,
|
|
442
|
+
...shell.login.loopbackTimeoutMs !== void 0 ? { timeoutMs: shell.login.loopbackTimeoutMs } : {},
|
|
443
|
+
exchange: (code, redirectUri, exchangeSignal) => client.exchangeCode({ code, verifier: pkce.verifier, redirectUri, signal: exchangeSignal })
|
|
444
|
+
});
|
|
445
|
+
const url = authorizeUrl({
|
|
446
|
+
accountUrl: client.accountUrl,
|
|
447
|
+
clientId: client.clientId,
|
|
448
|
+
redirectUri: loopback.redirectUri,
|
|
449
|
+
state,
|
|
450
|
+
challenge: pkce.challenge
|
|
451
|
+
});
|
|
452
|
+
if (await shell.openBrowser(url)) {
|
|
453
|
+
note(shell, "Opened your browser to sign in to Magic Spells. Waiting for you to finish\u2026");
|
|
454
|
+
} else {
|
|
455
|
+
shell.io.err(`Open this page to sign in to Magic Spells:
|
|
456
|
+
|
|
457
|
+
${url}
|
|
458
|
+
|
|
459
|
+
`);
|
|
460
|
+
}
|
|
461
|
+
return loopback.result;
|
|
462
|
+
}
|
|
463
|
+
async function deviceSignIn(shell, client, browserLocal, signal) {
|
|
464
|
+
const start = await client.startDevice(signal);
|
|
465
|
+
const bold = (t) => shell.opts.color === false ? t : `\x1B[1m${t}\x1B[22m`;
|
|
466
|
+
const big = start.user_code.split("").join(" ");
|
|
467
|
+
shell.io.err(
|
|
468
|
+
`To sign in, open this page on any device:
|
|
469
|
+
|
|
470
|
+
${start.verification_uri}
|
|
471
|
+
|
|
472
|
+
and enter the code:
|
|
473
|
+
|
|
474
|
+
${bold(big)}
|
|
475
|
+
|
|
476
|
+
`
|
|
477
|
+
);
|
|
478
|
+
if (browserLocal && start.verification_uri_complete) {
|
|
479
|
+
if (await shell.openBrowser(start.verification_uri_complete)) {
|
|
480
|
+
note(shell, "Opened your browser.");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const countdown = shell.io.isTTY() && !shell.opts.quiet;
|
|
484
|
+
if (!countdown) {
|
|
485
|
+
note(shell, `Waiting for approval (the code expires in ${minutes(start.expires_in)})\u2026`);
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
return await waitForDevice(client, start, {
|
|
489
|
+
signal,
|
|
490
|
+
...shell.login.sleep ? { sleep: shell.login.sleep } : {},
|
|
491
|
+
...shell.login.now ? { now: shell.login.now } : {},
|
|
492
|
+
...countdown ? { onTick: (s) => shell.io.err(`\rWaiting for approval\u2026 ${clock(s)} left `) } : {}
|
|
493
|
+
});
|
|
494
|
+
} finally {
|
|
495
|
+
if (countdown) shell.io.err("\r\x1B[K");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function mintAll(shell, config, products, grant, signal) {
|
|
499
|
+
const answers = await Promise.all(
|
|
500
|
+
products.map(async (product) => {
|
|
501
|
+
try {
|
|
502
|
+
const res = await mintSessions(product, {
|
|
503
|
+
apiUrl: config.products[product.id].apiUrl,
|
|
504
|
+
grant: grant.access_token,
|
|
505
|
+
signal
|
|
506
|
+
});
|
|
507
|
+
return { product, res };
|
|
508
|
+
} catch (err) {
|
|
509
|
+
return { product, error: toMagicError(err, { product: product.id }) };
|
|
510
|
+
}
|
|
511
|
+
})
|
|
512
|
+
);
|
|
513
|
+
const out = [];
|
|
514
|
+
for (const a of answers) {
|
|
515
|
+
if (!a.res) {
|
|
516
|
+
out.push({ product: a.product, error: a.error });
|
|
517
|
+
} else if (a.res.sessions.length === 0) {
|
|
518
|
+
out.push({
|
|
519
|
+
product: a.product,
|
|
520
|
+
error: new MagicError("permission_denied", `You have no ${a.product.title} workspaces.`, {
|
|
521
|
+
product: a.product.id,
|
|
522
|
+
details: { reason: "no_workspaces" }
|
|
523
|
+
})
|
|
524
|
+
});
|
|
525
|
+
} else {
|
|
526
|
+
const entry = await storeSessions(shell.store, a.product.id, a.res);
|
|
527
|
+
out.push({
|
|
528
|
+
product: a.product,
|
|
529
|
+
entry,
|
|
530
|
+
sessions: a.res.sessions,
|
|
531
|
+
...a.res.user ? { user: a.res.user } : {}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return out;
|
|
536
|
+
}
|
|
537
|
+
async function rememberUser(shell, config, minted, grant) {
|
|
538
|
+
let user = minted.find((m) => m.user)?.user ?? grant.user;
|
|
539
|
+
for (const m of minted) {
|
|
540
|
+
if (user || !m.sessions) continue;
|
|
541
|
+
try {
|
|
542
|
+
const op = (await m.product.load()).find((o) => o.name === `${m.product.id}.whoami`);
|
|
543
|
+
if (!op) continue;
|
|
544
|
+
const ctx = contextFor(m.product, config, m.sessions[0]);
|
|
545
|
+
const identity = await runWithRemint(
|
|
546
|
+
op,
|
|
547
|
+
op.input.parse({}),
|
|
548
|
+
ctx,
|
|
549
|
+
remintFor(m.product, shell.store)
|
|
550
|
+
);
|
|
551
|
+
const u = rec(readField(identity, "user"));
|
|
552
|
+
const email = readStr(u.email);
|
|
553
|
+
if (email) {
|
|
554
|
+
user = {
|
|
555
|
+
id: readStr(u.id) ?? "",
|
|
556
|
+
display_name: readStr(u.display_name) ?? readStr(u.name) ?? email,
|
|
557
|
+
email
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
} catch {
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const touched = {
|
|
564
|
+
...grant,
|
|
565
|
+
...user ? { user } : {},
|
|
566
|
+
...minted.some((m) => m.sessions) ? { last_used_at: (/* @__PURE__ */ new Date()).toISOString() } : {}
|
|
567
|
+
};
|
|
568
|
+
await saveGrant(shell.store, touched);
|
|
569
|
+
return touched;
|
|
570
|
+
}
|
|
571
|
+
async function snapshot(shell, all, grant) {
|
|
572
|
+
const sessions = [];
|
|
573
|
+
for (const product of all) {
|
|
574
|
+
const s = await productSessions(shell.store, product.id);
|
|
575
|
+
if (s.length > 0) sessions.push({ product, sessions: s });
|
|
576
|
+
}
|
|
577
|
+
return { grant, sessions };
|
|
578
|
+
}
|
|
579
|
+
async function retire(shell, config, previous, minted) {
|
|
580
|
+
for (const { product, sessions } of previous.sessions) {
|
|
581
|
+
await revokeProduct(product, sessions, config).catch(() => {
|
|
582
|
+
});
|
|
583
|
+
if (!minted.some((m) => m.product.id === product.id && m.sessions)) {
|
|
584
|
+
await forgetSessions(shell.store, product.id);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
await new AccountClient({ accountUrl: previous.grant.account_url }).revoke(previous.grant.access_token).catch(() => {
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
function loginResult(grant, minted) {
|
|
591
|
+
const products = {};
|
|
592
|
+
for (const m of minted) {
|
|
593
|
+
products[m.product.id] = m.entry ? { workspaces: m.entry.workspaces.map((w) => w.handle), default: m.entry.default ?? null } : { error: m.error.toJSON() };
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
account: {
|
|
597
|
+
user: grant.user ?? null,
|
|
598
|
+
expires_at: grant.expires_at,
|
|
599
|
+
account_url: grant.account_url
|
|
600
|
+
},
|
|
601
|
+
products
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function report(shell, result) {
|
|
605
|
+
if (shell.opts.quiet) return;
|
|
606
|
+
const u = result.account.user;
|
|
607
|
+
const lines = [u ? `Logged in as ${u.display_name} (${u.email}).` : "Logged in."];
|
|
608
|
+
const width = Math.max(...Object.keys(result.products).map((id) => id.length));
|
|
609
|
+
for (const [id, p] of Object.entries(result.products)) {
|
|
610
|
+
let line;
|
|
611
|
+
if ("error" in p) {
|
|
612
|
+
line = `skipped: ${p.error.message}${p.error.hint ? ` ${p.error.hint}` : ""}`;
|
|
613
|
+
} else {
|
|
614
|
+
const n = p.workspaces.length;
|
|
615
|
+
const which = `${n} workspace${n === 1 ? "" : "s"}: ${p.workspaces.join(", ")}`;
|
|
616
|
+
line = p.default ? `${which} (default)` : `${which} (use --workspace to pick one)`;
|
|
617
|
+
}
|
|
618
|
+
lines.push(` ${id.padEnd(width)} ${line}`);
|
|
619
|
+
}
|
|
620
|
+
shell.io.err(`${lines.join("\n")}
|
|
621
|
+
`);
|
|
622
|
+
}
|
|
623
|
+
function note(shell, line) {
|
|
624
|
+
if (!shell.opts.quiet) shell.io.err(`${line}
|
|
625
|
+
`);
|
|
626
|
+
}
|
|
627
|
+
function minutes(seconds) {
|
|
628
|
+
const m = Math.round(seconds / 60);
|
|
629
|
+
return m === 1 ? "1 minute" : `${m} minutes`;
|
|
630
|
+
}
|
|
631
|
+
function clock(seconds) {
|
|
632
|
+
return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
|
|
633
|
+
}
|
|
634
|
+
async function productKeyLogin(shell, args) {
|
|
219
635
|
if (args.some((a) => a.includes("_"))) {
|
|
220
636
|
throw usage(
|
|
221
637
|
"Keys are never read from arguments.",
|
|
@@ -241,6 +657,21 @@ async function login(shell, argv) {
|
|
|
241
657
|
);
|
|
242
658
|
return 0;
|
|
243
659
|
}
|
|
660
|
+
function byPrefix(products, key) {
|
|
661
|
+
const match = products.find((p) => key.startsWith(p.credential));
|
|
662
|
+
if (match) return match;
|
|
663
|
+
throw usage(
|
|
664
|
+
"The key prefix matches no product.",
|
|
665
|
+
`Known prefixes: ${products.map((p) => `${p.credential} (${p.id})`).join(", ")}.`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
function envShadowNote(shell, product) {
|
|
669
|
+
const envVar = productEnvVar(product.id, "KEY");
|
|
670
|
+
if (shell.env[envVar]?.trim() && !shell.opts.quiet) {
|
|
671
|
+
shell.io.err(`note: ${envVar} is set and takes precedence over the stored key.
|
|
672
|
+
`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
244
675
|
async function logout(shell, argv) {
|
|
245
676
|
const args = [...argv];
|
|
246
677
|
if (wantsHelp(args)) {
|
|
@@ -250,28 +681,65 @@ async function logout(shell, argv) {
|
|
|
250
681
|
rejectWorkspace("logout", shell.opts);
|
|
251
682
|
const one = takeValue(args, "product");
|
|
252
683
|
noMoreArgs("logout", args);
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
684
|
+
const chosen = await loadProducts(shell, one ? [one] : void 0);
|
|
685
|
+
const grant = await readGrant(shell.store);
|
|
686
|
+
const result = { removed: [], revoked: [], account: "none" };
|
|
687
|
+
const errors = {};
|
|
688
|
+
for (const product of chosen) {
|
|
689
|
+
const sessions = await productSessions(shell.store, product.id);
|
|
690
|
+
if (sessions.length > 0 && product.sessions) {
|
|
691
|
+
try {
|
|
692
|
+
await revokeProduct(product, sessions, productConfig(product, shell));
|
|
693
|
+
result.revoked.push(product.id);
|
|
694
|
+
} catch (err) {
|
|
695
|
+
errors[product.id] = toMagicError(err, { product: product.id }).toJSON();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const items = await forgetSessions(shell.store, product.id);
|
|
699
|
+
const hadManual = await shell.store.locate(manualItem(product.id)) !== null;
|
|
256
700
|
await forgetProductKey(product, shell.store);
|
|
701
|
+
if (items.length > 0 || hadManual) result.removed.push(product.id);
|
|
257
702
|
envShadowNote(shell, product);
|
|
258
703
|
}
|
|
259
|
-
|
|
704
|
+
if (one) {
|
|
705
|
+
result.account = grant ? "kept" : "none";
|
|
706
|
+
} else if (grant) {
|
|
707
|
+
try {
|
|
708
|
+
result.account = await new AccountClient({ accountUrl: grant.account_url }).revoke(
|
|
709
|
+
grant.access_token
|
|
710
|
+
);
|
|
711
|
+
} catch (err) {
|
|
712
|
+
result.account = "failed";
|
|
713
|
+
errors.account = toMagicError(err, { product: "account" }).toJSON();
|
|
714
|
+
}
|
|
715
|
+
await shell.store.delete(GRANT_ITEM);
|
|
716
|
+
}
|
|
717
|
+
if (Object.keys(errors).length > 0) result.errors = errors;
|
|
718
|
+
if (!shell.opts.quiet) {
|
|
719
|
+
if (result.account === "unsupported") {
|
|
720
|
+
shell.io.err(
|
|
721
|
+
"note: Account does not offer grant revocation yet; the sign-in was forgotten here.\n"
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
for (const [id, e] of Object.entries(errors)) {
|
|
725
|
+
shell.io.err(`note: ${id}: could not revoke server-side (${e.message}); forgotten here.
|
|
726
|
+
`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
render(result, shell.opts, shell.io);
|
|
260
730
|
return 0;
|
|
261
731
|
}
|
|
262
|
-
function
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
shell.io.err(`note: ${envVar} is set and takes precedence over the stored key.
|
|
274
|
-
`);
|
|
732
|
+
async function revokeProduct(product, sessions, config) {
|
|
733
|
+
for (const s of sessions) {
|
|
734
|
+
try {
|
|
735
|
+
await revokeSessions(product, { apiUrl: config.products[product.id].apiUrl, key: s.key });
|
|
736
|
+
return;
|
|
737
|
+
} catch (err) {
|
|
738
|
+
if (isMagicError(err) && (err.code === "auth_invalid" || err.code === "auth_expired")) {
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
throw err;
|
|
742
|
+
}
|
|
275
743
|
}
|
|
276
744
|
}
|
|
277
745
|
|
|
@@ -295,7 +763,13 @@ async function buildMcp(shell, argv, signal) {
|
|
|
295
763
|
try {
|
|
296
764
|
const module = await entry.load();
|
|
297
765
|
const operations = await module.load();
|
|
298
|
-
|
|
766
|
+
if (operations.length === 0 && !module.mcp && !ids?.includes(entry.id)) continue;
|
|
767
|
+
products.push({
|
|
768
|
+
module,
|
|
769
|
+
operations,
|
|
770
|
+
context: contextSource(shell, module, signal),
|
|
771
|
+
remint: remintFor(module, shell.store)
|
|
772
|
+
});
|
|
299
773
|
} catch (err) {
|
|
300
774
|
skip(entry.id, err);
|
|
301
775
|
}
|
|
@@ -374,12 +848,24 @@ function guardStdout() {
|
|
|
374
848
|
// src/commands/whoami.ts
|
|
375
849
|
async function whoami(shell, argv) {
|
|
376
850
|
if (wantsHelp(argv)) {
|
|
377
|
-
shell.io.err(
|
|
851
|
+
shell.io.err(
|
|
852
|
+
"Usage: magic whoami [--json]\n\nYour account, and your workspaces and keys in each product.\n"
|
|
853
|
+
);
|
|
378
854
|
return 0;
|
|
379
855
|
}
|
|
380
856
|
rejectWorkspace("whoami", shell.opts);
|
|
381
857
|
noMoreArgs("whoami", argv);
|
|
382
|
-
const
|
|
858
|
+
const grant = await readGrant(shell.store);
|
|
859
|
+
const result = {
|
|
860
|
+
account: grant ? {
|
|
861
|
+
user: grant.user ?? null,
|
|
862
|
+
grant_id: grant.grant_id,
|
|
863
|
+
expires_at: grant.expires_at,
|
|
864
|
+
idle_expires_at: grantIdleExpiry(grant),
|
|
865
|
+
account_url: grant.account_url
|
|
866
|
+
} : null,
|
|
867
|
+
products: {}
|
|
868
|
+
};
|
|
383
869
|
let firstError;
|
|
384
870
|
for (const product of await loadProducts(shell)) {
|
|
385
871
|
const entry = await identify(shell, product);
|
|
@@ -389,36 +875,87 @@ async function whoami(shell, argv) {
|
|
|
389
875
|
if (jsonMode(shell.opts, shell.io)) render(result, shell.opts, shell.io);
|
|
390
876
|
else printHuman2(shell, result);
|
|
391
877
|
if (firstError) return exitCodeFor(firstError.code);
|
|
392
|
-
if (Object.values(result.products).every((p) => p.source === "none")) {
|
|
393
|
-
if (!shell.opts.quiet) shell.io.err("
|
|
878
|
+
if (!result.account && Object.values(result.products).every((p) => p.source === "none")) {
|
|
879
|
+
if (!shell.opts.quiet) shell.io.err("Not logged in. Run `magic login`.\n");
|
|
394
880
|
return exitCodeFor("not_logged_in");
|
|
395
881
|
}
|
|
396
882
|
return 0;
|
|
397
883
|
}
|
|
398
884
|
async function identify(shell, product) {
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
885
|
+
const id = product.id;
|
|
886
|
+
const sessions = await productSessions(shell.store, id);
|
|
887
|
+
const index = (await readIndex(shell.store))[id];
|
|
888
|
+
const envSet = Boolean(shell.env[productEnvVar(id, "KEY")]?.trim());
|
|
889
|
+
const manualAt = await shell.store.locate(manualItem(id));
|
|
890
|
+
const source = envSet ? "env" : sessions.length > 0 ? "account" : manualAt ? "manual" : "none";
|
|
891
|
+
const stored = source === "account" ? sessions[0].workspace.handle : null;
|
|
892
|
+
const base = {
|
|
893
|
+
source,
|
|
894
|
+
stored_in: source === "account" ? storeLabel(await shell.store.locate(sessionItem(id, stored))) : source === "manual" ? storeLabel(manualAt) : null,
|
|
895
|
+
key: null,
|
|
896
|
+
default: index?.default ?? null,
|
|
897
|
+
workspaces: workspaces(product, sessions, index?.workspaces ?? []),
|
|
898
|
+
identity: null
|
|
899
|
+
};
|
|
900
|
+
if (source === "none") return { identity: base };
|
|
901
|
+
let session;
|
|
902
|
+
const config = productConfig(product, shell);
|
|
903
|
+
try {
|
|
904
|
+
const workspace = config.defaultWorkspace[id];
|
|
905
|
+
session = await resolveCredential(product, {
|
|
906
|
+
env: shell.env,
|
|
907
|
+
store: shell.store,
|
|
908
|
+
...workspace ? { workspace } : {}
|
|
909
|
+
});
|
|
910
|
+
} catch (err) {
|
|
911
|
+
const error = toMagicError(err, { product: id });
|
|
912
|
+
if (error.code === "usage") return { identity: base };
|
|
913
|
+
return { identity: { ...base, error: error.toJSON() }, error };
|
|
914
|
+
}
|
|
915
|
+
const identity = { ...base, key: maskKey(session.key, product.credential) };
|
|
402
916
|
try {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const identity = await op.run(op.input.parse({}), contextFor(product, config, session));
|
|
409
|
-
return { identity: { source, key, identity } };
|
|
917
|
+
const op = (await product.load()).find((o) => o.name === `${id}.whoami`);
|
|
918
|
+
if (!op) return { identity };
|
|
919
|
+
const ctx = contextFor(product, config, session);
|
|
920
|
+
const who = await runWithRemint(op, op.input.parse({}), ctx, remintFor(product, shell.store));
|
|
921
|
+
return { identity: { ...identity, identity: who } };
|
|
410
922
|
} catch (err) {
|
|
411
|
-
const error = toMagicError(err, { product:
|
|
412
|
-
return { identity: {
|
|
923
|
+
const error = toMagicError(err, { product: id });
|
|
924
|
+
return { identity: { ...identity, error: error.toJSON() }, error };
|
|
413
925
|
}
|
|
414
926
|
}
|
|
927
|
+
function workspaces(product, sessions, listed) {
|
|
928
|
+
return listed.map((w) => {
|
|
929
|
+
const s = sessions.find((x) => x.workspace?.handle === w.handle);
|
|
930
|
+
return {
|
|
931
|
+
handle: w.handle,
|
|
932
|
+
name: w.name,
|
|
933
|
+
role: w.role,
|
|
934
|
+
key: s ? maskKey(s.key, product.credential) : null,
|
|
935
|
+
expires_at: s?.expires_at ?? null
|
|
936
|
+
};
|
|
937
|
+
});
|
|
938
|
+
}
|
|
415
939
|
function printHuman2(shell, result) {
|
|
416
940
|
const lines = [];
|
|
941
|
+
const a = result.account;
|
|
942
|
+
if (a) {
|
|
943
|
+
lines.push(a.user ? `${a.user.display_name} <${a.user.email}>` : "Signed in");
|
|
944
|
+
lines.push(
|
|
945
|
+
` signed in until ${day(a.expires_at)}; ends after ${day(a.idle_expires_at)} unused`
|
|
946
|
+
);
|
|
947
|
+
} else {
|
|
948
|
+
lines.push("Not signed in to a Magic Spells account.");
|
|
949
|
+
}
|
|
950
|
+
lines.push("");
|
|
417
951
|
for (const [id, p] of Object.entries(result.products)) {
|
|
418
|
-
const rows = [
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
952
|
+
const rows = [["source", p.source]];
|
|
953
|
+
if (p.key) rows.push(["key", p.key]);
|
|
954
|
+
for (const w of p.workspaces) {
|
|
955
|
+
const mark = w.handle === p.default ? " (default)" : "";
|
|
956
|
+
const expires = w.expires_at ? `, key expires ${w.expires_at}` : "";
|
|
957
|
+
rows.push([w.handle, `${w.name} \xB7 ${w.role}${w.key ? ` \xB7 ${w.key}` : ""}${mark}${expires}`]);
|
|
958
|
+
}
|
|
422
959
|
if (p.identity && typeof p.identity === "object") {
|
|
423
960
|
for (const [k, v] of Object.entries(p.identity)) rows.push([k, cell(v)]);
|
|
424
961
|
}
|
|
@@ -428,13 +965,26 @@ function printHuman2(shell, result) {
|
|
|
428
965
|
}
|
|
429
966
|
shell.io.out(lines.join("\n"));
|
|
430
967
|
}
|
|
968
|
+
function day(iso) {
|
|
969
|
+
return iso.slice(0, 10);
|
|
970
|
+
}
|
|
431
971
|
|
|
432
972
|
// src/products.ts
|
|
433
973
|
var PRODUCTS = [
|
|
434
974
|
{
|
|
435
975
|
id: "pyramid",
|
|
436
976
|
title: "Pyramid \u2014 projects and tasks",
|
|
437
|
-
load: async () => (await import("./src-
|
|
977
|
+
load: async () => (await import("./src-56NZ53TG.js")).default
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
id: "sites",
|
|
981
|
+
title: "Sites \u2014 sign-in only until its commands arrive",
|
|
982
|
+
load: async () => (await import("./src-DJYPPJFM.js")).default
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
id: "marketing",
|
|
986
|
+
title: "Marketing \u2014 sign-in only until its commands arrive",
|
|
987
|
+
load: async () => (await import("./src-KJGFPRQP.js")).default
|
|
438
988
|
}
|
|
439
989
|
];
|
|
440
990
|
|
|
@@ -484,10 +1034,10 @@ async function readAll(stream) {
|
|
|
484
1034
|
|
|
485
1035
|
// src/main.ts
|
|
486
1036
|
var COMMANDS = {
|
|
487
|
-
login: { summary: "
|
|
488
|
-
logout: { summary: "forget
|
|
489
|
-
whoami: { summary: "
|
|
490
|
-
doctor: { summary: "check the install, the
|
|
1037
|
+
login: { summary: "sign in and mint product keys: login [--device] [--product <p>]", run: login },
|
|
1038
|
+
logout: { summary: "revoke and forget credentials: logout [--product <p>]", run: logout },
|
|
1039
|
+
whoami: { summary: "your account and your workspaces in each product", run: whoami },
|
|
1040
|
+
doctor: { summary: "check the install, the sign-in and each product", run: doctor },
|
|
491
1041
|
mcp: { summary: "serve every product\u2019s tools over stdio: mcp [--products a,b]", run: serveMcp }
|
|
492
1042
|
};
|
|
493
1043
|
function help(products) {
|
|
@@ -544,7 +1094,11 @@ async function run(argv, deps = {}) {
|
|
|
544
1094
|
entries: products,
|
|
545
1095
|
opts,
|
|
546
1096
|
readConfig,
|
|
547
|
-
readSecret: deps.readSecret ?? readSecret
|
|
1097
|
+
readSecret: deps.readSecret ?? readSecret,
|
|
1098
|
+
openBrowser: deps.openBrowser ?? openBrowser,
|
|
1099
|
+
confirm: deps.confirm !== void 0 ? deps.confirm : process.stdin.isTTY ? confirmOnTty : null,
|
|
1100
|
+
platform: deps.platform ?? process.platform,
|
|
1101
|
+
login: deps.login ?? {}
|
|
548
1102
|
};
|
|
549
1103
|
return await command.run(shell, parsed.rest);
|
|
550
1104
|
} catch (err) {
|