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
|
@@ -25,6 +25,9 @@ var MagicError = class _MagicError extends Error {
|
|
|
25
25
|
return out;
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
|
+
function isMagicError(e) {
|
|
29
|
+
return e instanceof MagicError;
|
|
30
|
+
}
|
|
28
31
|
function exitCodeFor(code) {
|
|
29
32
|
switch (code) {
|
|
30
33
|
case "usage":
|
|
@@ -32,6 +35,7 @@ function exitCodeFor(code) {
|
|
|
32
35
|
case "not_logged_in":
|
|
33
36
|
case "auth_invalid":
|
|
34
37
|
case "auth_expired":
|
|
38
|
+
case "auth_cancelled":
|
|
35
39
|
return 3;
|
|
36
40
|
case "permission_denied":
|
|
37
41
|
return 4;
|
|
@@ -165,6 +169,8 @@ function configDir(opts = {}) {
|
|
|
165
169
|
|
|
166
170
|
// ../kit/src/config.ts
|
|
167
171
|
var DEFAULT_ACCOUNT_URL = "https://account.magicspells.io";
|
|
172
|
+
var ACCOUNT_CLIENT_ID_PLACEHOLDER = "set-magic-cli-client-id";
|
|
173
|
+
var DEFAULT_ACCOUNT_CLIENT_ID = "magic-cli";
|
|
168
174
|
function productEnvVar(id, suffix) {
|
|
169
175
|
return `MAGIC_${id.toUpperCase().replace(/-/g, "_")}_${suffix}`;
|
|
170
176
|
}
|
|
@@ -198,6 +204,7 @@ function loadCliConfig(opts) {
|
|
|
198
204
|
env.MAGIC_ACCOUNT_URL || file?.accountUrl || DEFAULT_ACCOUNT_URL,
|
|
199
205
|
"MAGIC_ACCOUNT_URL"
|
|
200
206
|
),
|
|
207
|
+
accountClientId: env.MAGIC_ACCOUNT_CLIENT_ID?.trim() || DEFAULT_ACCOUNT_CLIENT_ID,
|
|
201
208
|
products,
|
|
202
209
|
defaultWorkspace,
|
|
203
210
|
allowDestructive: env.MAGIC_ALLOW_DESTRUCTIVE === "1",
|
|
@@ -220,6 +227,17 @@ function normalizeUrl(value, source) {
|
|
|
220
227
|
return trimmed;
|
|
221
228
|
}
|
|
222
229
|
|
|
230
|
+
// ../kit/src/read.ts
|
|
231
|
+
function rec(v) {
|
|
232
|
+
return v && typeof v === "object" ? v : {};
|
|
233
|
+
}
|
|
234
|
+
function readStr(v) {
|
|
235
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
236
|
+
}
|
|
237
|
+
function readField(obj, key) {
|
|
238
|
+
return obj && typeof obj === "object" && key in obj ? obj[key] : void 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
223
241
|
// ../kit/src/keychain.ts
|
|
224
242
|
import { execFileSync } from "node:child_process";
|
|
225
243
|
import { chmodSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
@@ -542,13 +560,16 @@ function matchCommand(cmds, argv) {
|
|
|
542
560
|
}
|
|
543
561
|
return void 0;
|
|
544
562
|
}
|
|
563
|
+
function defOf(schema) {
|
|
564
|
+
return schema?.def;
|
|
565
|
+
}
|
|
545
566
|
function baseType(schema) {
|
|
546
567
|
let cur = schema;
|
|
547
568
|
let optional = false;
|
|
548
569
|
for (let guard = 0; guard < 12; guard++) {
|
|
549
|
-
const def = cur
|
|
550
|
-
const name = def?.
|
|
551
|
-
if ((name === "
|
|
570
|
+
const def = defOf(cur);
|
|
571
|
+
const name = def?.type;
|
|
572
|
+
if ((name === "optional" || name === "nullable" || name === "default") && def?.innerType) {
|
|
552
573
|
optional = true;
|
|
553
574
|
cur = def.innerType;
|
|
554
575
|
continue;
|
|
@@ -562,31 +583,27 @@ function describeFields(input) {
|
|
|
562
583
|
if (!shape) return [];
|
|
563
584
|
return Object.entries(shape).map(([key, raw]) => {
|
|
564
585
|
const { type, optional } = baseType(raw);
|
|
565
|
-
const def = type
|
|
586
|
+
const def = defOf(type);
|
|
566
587
|
let kind = "string";
|
|
567
588
|
let values;
|
|
568
|
-
switch (def?.
|
|
569
|
-
case "
|
|
589
|
+
switch (def?.type) {
|
|
590
|
+
case "boolean":
|
|
570
591
|
kind = "boolean";
|
|
571
592
|
break;
|
|
572
|
-
case "
|
|
593
|
+
case "number":
|
|
573
594
|
kind = "number";
|
|
574
595
|
break;
|
|
575
|
-
case "
|
|
576
|
-
const el = def.
|
|
577
|
-
|
|
578
|
-
kind = elName === "ZodObject" ? "json" : "string[]";
|
|
596
|
+
case "array": {
|
|
597
|
+
const el = def.element ? baseType(def.element).type : void 0;
|
|
598
|
+
kind = defOf(el)?.type === "object" ? "json" : "string[]";
|
|
579
599
|
break;
|
|
580
600
|
}
|
|
581
|
-
case "
|
|
601
|
+
case "enum":
|
|
582
602
|
kind = "enum";
|
|
583
|
-
if (
|
|
603
|
+
if (def.entries) values = Object.values(def.entries).map(String);
|
|
584
604
|
break;
|
|
585
|
-
case "
|
|
586
|
-
|
|
587
|
-
break;
|
|
588
|
-
case "ZodObject":
|
|
589
|
-
case "ZodRecord":
|
|
605
|
+
case "object":
|
|
606
|
+
case "record":
|
|
590
607
|
kind = "json";
|
|
591
608
|
break;
|
|
592
609
|
}
|
|
@@ -694,6 +711,533 @@ function usage(message, hint) {
|
|
|
694
711
|
// ../kit/src/cli.ts
|
|
695
712
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
696
713
|
|
|
714
|
+
// ../kit/src/sessions.ts
|
|
715
|
+
import { hostname } from "node:os";
|
|
716
|
+
|
|
717
|
+
// ../kit/src/account.ts
|
|
718
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
719
|
+
var ACCOUNT_PRODUCT = "account";
|
|
720
|
+
var GRANT_PREFIX = "magic_";
|
|
721
|
+
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
722
|
+
var DEFAULT_GRANT_SECONDS = 60 * 24 * 60 * 60;
|
|
723
|
+
function createPkce(bytes = randomBytes(32)) {
|
|
724
|
+
const verifier = bytes.toString("base64url");
|
|
725
|
+
return { verifier, challenge: createHash("sha256").update(verifier).digest("base64url") };
|
|
726
|
+
}
|
|
727
|
+
function randomState() {
|
|
728
|
+
return randomBytes(24).toString("base64url");
|
|
729
|
+
}
|
|
730
|
+
function authorizeUrl(p) {
|
|
731
|
+
const url = new URL("/auth/authorize", p.accountUrl);
|
|
732
|
+
url.searchParams.set("client_id", p.clientId);
|
|
733
|
+
url.searchParams.set("redirect_uri", p.redirectUri);
|
|
734
|
+
url.searchParams.set("state", p.state);
|
|
735
|
+
url.searchParams.set("code_challenge", p.challenge);
|
|
736
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
737
|
+
url.searchParams.set("scope", "basic");
|
|
738
|
+
return url.toString();
|
|
739
|
+
}
|
|
740
|
+
function requireClientId(clientId) {
|
|
741
|
+
if (!clientId || clientId === ACCOUNT_CLIENT_ID_PLACEHOLDER) {
|
|
742
|
+
throw new MagicError("usage", "This build of magic has no Account client id.", {
|
|
743
|
+
product: ACCOUNT_PRODUCT,
|
|
744
|
+
hint: "Set MAGIC_ACCOUNT_CLIENT_ID to the magic application\u2019s client id."
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
return clientId;
|
|
748
|
+
}
|
|
749
|
+
var AccountClient = class {
|
|
750
|
+
accountUrl;
|
|
751
|
+
id;
|
|
752
|
+
fetch;
|
|
753
|
+
constructor(opts) {
|
|
754
|
+
this.accountUrl = opts.accountUrl;
|
|
755
|
+
this.id = opts.clientId;
|
|
756
|
+
this.fetch = opts.fetch ?? fetch;
|
|
757
|
+
}
|
|
758
|
+
get clientId() {
|
|
759
|
+
return requireClientId(this.id);
|
|
760
|
+
}
|
|
761
|
+
/** Exchange a loopback `code` for the grant (`grant_type=authorization_code`). */
|
|
762
|
+
async exchangeCode(p) {
|
|
763
|
+
const res = await this.post(
|
|
764
|
+
"/auth/token",
|
|
765
|
+
{
|
|
766
|
+
grant_type: "authorization_code",
|
|
767
|
+
code: p.code,
|
|
768
|
+
code_verifier: p.verifier,
|
|
769
|
+
client_id: this.clientId,
|
|
770
|
+
redirect_uri: p.redirectUri
|
|
771
|
+
},
|
|
772
|
+
p.signal
|
|
773
|
+
);
|
|
774
|
+
if (!res.ok) throw tokenError(res.status, res.code);
|
|
775
|
+
return readToken(res.body);
|
|
776
|
+
}
|
|
777
|
+
/** Start the device flow (`POST /auth/device`). */
|
|
778
|
+
async startDevice(signal) {
|
|
779
|
+
const res = await this.post(
|
|
780
|
+
"/auth/device",
|
|
781
|
+
{ client_id: this.clientId, scope: "basic" },
|
|
782
|
+
signal
|
|
783
|
+
);
|
|
784
|
+
if (!res.ok) throw tokenError(res.status, res.code);
|
|
785
|
+
const b = res.body;
|
|
786
|
+
if (typeof b.device_code !== "string" || typeof b.user_code !== "string" || typeof b.verification_uri !== "string") {
|
|
787
|
+
throw new MagicError("unknown", "Account answered the device request without a code.", {
|
|
788
|
+
product: ACCOUNT_PRODUCT
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
device_code: b.device_code,
|
|
793
|
+
user_code: b.user_code,
|
|
794
|
+
verification_uri: b.verification_uri,
|
|
795
|
+
...typeof b.verification_uri_complete === "string" ? { verification_uri_complete: b.verification_uri_complete } : {},
|
|
796
|
+
expires_in: positive(b.expires_in, 600),
|
|
797
|
+
interval: positive(b.interval, 5)
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* One device poll. Pending and slow_down come back as values; access_denied
|
|
802
|
+
* (auth_cancelled) and expired_token (auth_expired) throw.
|
|
803
|
+
*/
|
|
804
|
+
async pollDevice(deviceCode, signal) {
|
|
805
|
+
const res = await this.post(
|
|
806
|
+
"/auth/token",
|
|
807
|
+
{ grant_type: DEVICE_GRANT_TYPE, device_code: deviceCode, client_id: this.clientId },
|
|
808
|
+
signal
|
|
809
|
+
);
|
|
810
|
+
if (res.ok) return { status: "approved", token: readToken(res.body) };
|
|
811
|
+
if (res.code === "authorization_pending") return { status: "pending" };
|
|
812
|
+
if (res.code === "slow_down") return { status: "slow_down" };
|
|
813
|
+
throw tokenError(res.status, res.code);
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Revoke the grant (`POST /auth/revoke token=…`). `unsupported` when this Account
|
|
817
|
+
* has no revoke endpoint (404/405); other failures throw.
|
|
818
|
+
*/
|
|
819
|
+
async revoke(token, signal) {
|
|
820
|
+
const res = await this.post("/auth/revoke", { token }, signal);
|
|
821
|
+
if (res.ok) return "revoked";
|
|
822
|
+
if (res.status === 404 || res.status === 405) return "unsupported";
|
|
823
|
+
throw tokenError(res.status, res.code);
|
|
824
|
+
}
|
|
825
|
+
async post(path, form, signal) {
|
|
826
|
+
let res;
|
|
827
|
+
try {
|
|
828
|
+
res = await this.fetch(new URL(path, this.accountUrl), {
|
|
829
|
+
method: "POST",
|
|
830
|
+
headers: {
|
|
831
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
832
|
+
accept: "application/json"
|
|
833
|
+
},
|
|
834
|
+
body: new URLSearchParams(form).toString(),
|
|
835
|
+
...signal ? { signal } : {}
|
|
836
|
+
});
|
|
837
|
+
} catch (err) {
|
|
838
|
+
throw transportError(err, signal);
|
|
839
|
+
}
|
|
840
|
+
const text = await res.text().catch(() => "");
|
|
841
|
+
let body = null;
|
|
842
|
+
try {
|
|
843
|
+
body = text ? JSON.parse(text) : null;
|
|
844
|
+
} catch {
|
|
845
|
+
body = null;
|
|
846
|
+
}
|
|
847
|
+
return { ok: res.ok, status: res.status, code: errorCode(body), body };
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
async function waitForDevice(client, start, opts = {}) {
|
|
851
|
+
const sleep = opts.sleep ?? abortableSleep;
|
|
852
|
+
const now = opts.now ?? Date.now;
|
|
853
|
+
const deadline = now() + start.expires_in * 1e3;
|
|
854
|
+
let interval = start.interval * 1e3;
|
|
855
|
+
for (; ; ) {
|
|
856
|
+
const until = now() + interval;
|
|
857
|
+
while (now() < until) {
|
|
858
|
+
if (opts.signal?.aborted) throw cancelledError();
|
|
859
|
+
opts.onTick?.(Math.max(0, Math.ceil((deadline - now()) / 1e3)));
|
|
860
|
+
await sleep(Math.min(1e3, until - now()), opts.signal).catch((err) => {
|
|
861
|
+
throw opts.signal?.aborted ? cancelledError() : err;
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
if (opts.signal?.aborted) throw cancelledError();
|
|
865
|
+
if (now() >= deadline) throw expiredCodeError();
|
|
866
|
+
const poll = await client.pollDevice(start.device_code, opts.signal);
|
|
867
|
+
if (poll.status === "approved") return poll.token;
|
|
868
|
+
if (poll.status === "slow_down") interval += 5e3;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function abortableSleep(ms, signal) {
|
|
872
|
+
return new Promise((resolve, reject) => {
|
|
873
|
+
if (signal?.aborted) return reject(cancelledError());
|
|
874
|
+
const timer = setTimeout(() => {
|
|
875
|
+
signal?.removeEventListener("abort", onAbort);
|
|
876
|
+
resolve();
|
|
877
|
+
}, ms);
|
|
878
|
+
const onAbort = () => {
|
|
879
|
+
clearTimeout(timer);
|
|
880
|
+
reject(cancelledError());
|
|
881
|
+
};
|
|
882
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
function cancelledError() {
|
|
886
|
+
return new MagicError("auth_cancelled", "Login cancelled.", { product: ACCOUNT_PRODUCT });
|
|
887
|
+
}
|
|
888
|
+
function expiredCodeError() {
|
|
889
|
+
return new MagicError("auth_expired", "The code expired.", {
|
|
890
|
+
product: ACCOUNT_PRODUCT,
|
|
891
|
+
hint: "Run `magic login` again."
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
function errorCode(body) {
|
|
895
|
+
if (!body || typeof body !== "object" || !("error" in body)) return void 0;
|
|
896
|
+
const e = body.error;
|
|
897
|
+
if (typeof e === "string") return e;
|
|
898
|
+
if (e && typeof e === "object" && typeof e.code === "string") {
|
|
899
|
+
return e.code;
|
|
900
|
+
}
|
|
901
|
+
return void 0;
|
|
902
|
+
}
|
|
903
|
+
function tokenError(status, code) {
|
|
904
|
+
const product = ACCOUNT_PRODUCT;
|
|
905
|
+
switch (code) {
|
|
906
|
+
case "access_denied":
|
|
907
|
+
return cancelledError();
|
|
908
|
+
case "expired_token":
|
|
909
|
+
return expiredCodeError();
|
|
910
|
+
case "invalid_client":
|
|
911
|
+
return new MagicError("auth_invalid", "Account does not know this client.", {
|
|
912
|
+
product,
|
|
913
|
+
hint: "Check MAGIC_ACCOUNT_CLIENT_ID and MAGIC_ACCOUNT_URL."
|
|
914
|
+
});
|
|
915
|
+
case "invalid_grant":
|
|
916
|
+
return new MagicError("auth_invalid", "Account refused the sign-in code.", {
|
|
917
|
+
product,
|
|
918
|
+
hint: "Run `magic login` again."
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
if (status === 429 || code === "slow_down" || code === "rate_limited") {
|
|
922
|
+
return new MagicError("rate_limited", "Account is rate limiting sign-ins.", {
|
|
923
|
+
product,
|
|
924
|
+
hint: "Wait a minute and try again."
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
if (status >= 500) {
|
|
928
|
+
return new MagicError("product_unavailable", `Account is unavailable (HTTP ${status}).`, {
|
|
929
|
+
product
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
return new MagicError(
|
|
933
|
+
"auth_invalid",
|
|
934
|
+
`Account refused the request (HTTP ${status}${code ? `, ${code}` : ""}).`,
|
|
935
|
+
{ product }
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
function transportError(err, signal) {
|
|
939
|
+
if (signal?.aborted) return cancelledError();
|
|
940
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
941
|
+
const cause = err instanceof Error && err.cause instanceof Error ? `: ${err.cause.message}` : "";
|
|
942
|
+
if (err instanceof Error && !isNetworkError(err)) {
|
|
943
|
+
return new MagicError("unknown", message, { product: ACCOUNT_PRODUCT });
|
|
944
|
+
}
|
|
945
|
+
return new MagicError("network", `Cannot reach Account (${message}${cause}).`, {
|
|
946
|
+
product: ACCOUNT_PRODUCT,
|
|
947
|
+
hint: "Check MAGIC_ACCOUNT_URL and your network connection."
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
function readToken(body) {
|
|
951
|
+
const b = body ?? {};
|
|
952
|
+
if (typeof b.access_token !== "string" || !b.access_token.startsWith(GRANT_PREFIX)) {
|
|
953
|
+
throw new MagicError("auth_invalid", "Account answered without a magic grant.", {
|
|
954
|
+
product: ACCOUNT_PRODUCT,
|
|
955
|
+
hint: "Check MAGIC_ACCOUNT_URL points at Magic Spells Account."
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
return {
|
|
959
|
+
token_type: "Bearer",
|
|
960
|
+
access_token: b.access_token,
|
|
961
|
+
expires_in: positive(b.expires_in, DEFAULT_GRANT_SECONDS),
|
|
962
|
+
grant_id: typeof b.grant_id === "string" ? b.grant_id : ""
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function positive(v, fallback) {
|
|
966
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : fallback;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// ../kit/src/sessions.ts
|
|
970
|
+
var GRANT_ITEM = "account:grant";
|
|
971
|
+
var SESSIONS_ITEM = "account:sessions";
|
|
972
|
+
var GRANT_IDLE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
973
|
+
function sessionItem(product, handle) {
|
|
974
|
+
return `${product}:${handle}`;
|
|
975
|
+
}
|
|
976
|
+
function grantFromToken(token, accountUrl, now = /* @__PURE__ */ new Date()) {
|
|
977
|
+
return {
|
|
978
|
+
access_token: token.access_token,
|
|
979
|
+
grant_id: token.grant_id,
|
|
980
|
+
token_type: "Bearer",
|
|
981
|
+
obtained_at: now.toISOString(),
|
|
982
|
+
expires_at: new Date(now.getTime() + token.expires_in * 1e3).toISOString(),
|
|
983
|
+
last_used_at: now.toISOString(),
|
|
984
|
+
account_url: accountUrl
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function grantIdleExpiry(grant) {
|
|
988
|
+
const idle = Date.parse(grant.last_used_at) + GRANT_IDLE_MS;
|
|
989
|
+
const hard = Date.parse(grant.expires_at);
|
|
990
|
+
return new Date(Math.min(idle, hard)).toISOString();
|
|
991
|
+
}
|
|
992
|
+
function grantExpired(grant, now = Date.now()) {
|
|
993
|
+
return Date.parse(grantIdleExpiry(grant)) <= now;
|
|
994
|
+
}
|
|
995
|
+
async function readGrant(store) {
|
|
996
|
+
const raw = await store.get(GRANT_ITEM);
|
|
997
|
+
if (!raw) return null;
|
|
998
|
+
try {
|
|
999
|
+
const g = JSON.parse(raw);
|
|
1000
|
+
if (typeof g.access_token !== "string" || !g.access_token.startsWith(GRANT_PREFIX)) return null;
|
|
1001
|
+
return g;
|
|
1002
|
+
} catch {
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
async function saveGrant(store, grant) {
|
|
1007
|
+
await store.set(GRANT_ITEM, JSON.stringify(grant));
|
|
1008
|
+
}
|
|
1009
|
+
async function readIndex(store) {
|
|
1010
|
+
const raw = await store.get(SESSIONS_ITEM);
|
|
1011
|
+
if (!raw) return {};
|
|
1012
|
+
try {
|
|
1013
|
+
const parsed = JSON.parse(raw);
|
|
1014
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1015
|
+
} catch {
|
|
1016
|
+
return {};
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async function saveIndex(store, index) {
|
|
1020
|
+
if (Object.keys(index).length === 0) await store.delete(SESSIONS_ITEM);
|
|
1021
|
+
else await store.set(SESSIONS_ITEM, JSON.stringify(index));
|
|
1022
|
+
}
|
|
1023
|
+
async function readSession(store, product, handle) {
|
|
1024
|
+
const raw = await store.get(sessionItem(product, handle));
|
|
1025
|
+
if (!raw) return null;
|
|
1026
|
+
try {
|
|
1027
|
+
const s = JSON.parse(raw);
|
|
1028
|
+
return typeof s.key === "string" && s.key.length > 0 ? { ...s, product, key: s.key, source: "account" } : null;
|
|
1029
|
+
} catch {
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function sessionLabel(host = hostname()) {
|
|
1034
|
+
return `magic on ${host}`.slice(0, 80);
|
|
1035
|
+
}
|
|
1036
|
+
async function mintSessions(spec, opts) {
|
|
1037
|
+
const path = spec.sessions?.path;
|
|
1038
|
+
if (!path) throw new Error(`${spec.id} declares no sessions route`);
|
|
1039
|
+
const body = { label: opts.label ?? sessionLabel() };
|
|
1040
|
+
if (opts.workspace) body.workspace = opts.workspace;
|
|
1041
|
+
const res = await call(spec, opts, opts.apiUrl + path, {
|
|
1042
|
+
method: "POST",
|
|
1043
|
+
headers: {
|
|
1044
|
+
authorization: `Bearer ${opts.grant}`,
|
|
1045
|
+
"content-type": "application/json",
|
|
1046
|
+
accept: "application/json"
|
|
1047
|
+
},
|
|
1048
|
+
body: JSON.stringify(body)
|
|
1049
|
+
});
|
|
1050
|
+
if (!res.ok) throw sessionError(spec, res.status, res.body);
|
|
1051
|
+
return readSessions(spec, res.body);
|
|
1052
|
+
}
|
|
1053
|
+
async function revokeSessions(spec, opts) {
|
|
1054
|
+
const path = spec.sessions?.path;
|
|
1055
|
+
if (!path) throw new Error(`${spec.id} declares no sessions route`);
|
|
1056
|
+
const res = await call(spec, opts, opts.apiUrl + path, {
|
|
1057
|
+
method: "DELETE",
|
|
1058
|
+
headers: { authorization: `Bearer ${opts.key}`, accept: "application/json" }
|
|
1059
|
+
});
|
|
1060
|
+
if (!res.ok) {
|
|
1061
|
+
throw mapHttpError(res.status, res.body, { product: spec.id, title: spec.title });
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
async function call(spec, opts, url, init) {
|
|
1065
|
+
let res;
|
|
1066
|
+
try {
|
|
1067
|
+
res = await (opts.fetch ?? fetch)(url, {
|
|
1068
|
+
...init,
|
|
1069
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
1070
|
+
});
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
const e = toMagicError(err, {
|
|
1073
|
+
product: spec.id,
|
|
1074
|
+
networkHint: `Check ${productEnvVar(spec.id, "URL")} and your network connection.`
|
|
1075
|
+
});
|
|
1076
|
+
const cause = err instanceof Error && err.cause instanceof Error ? `: ${err.cause.message}` : "";
|
|
1077
|
+
throw cause ? new MagicError(e.code, `${e.message}${cause}`, {
|
|
1078
|
+
product: e.product,
|
|
1079
|
+
...e.hint ? { hint: e.hint } : {}
|
|
1080
|
+
}) : e;
|
|
1081
|
+
}
|
|
1082
|
+
const text = await res.text().catch(() => "");
|
|
1083
|
+
let body = null;
|
|
1084
|
+
try {
|
|
1085
|
+
body = text ? JSON.parse(text) : null;
|
|
1086
|
+
} catch {
|
|
1087
|
+
body = null;
|
|
1088
|
+
}
|
|
1089
|
+
return { ok: res.ok, status: res.status, body };
|
|
1090
|
+
}
|
|
1091
|
+
function sessionError(spec, status, body) {
|
|
1092
|
+
const code = readStr(rec(rec(body).error).code);
|
|
1093
|
+
if (status === 401) {
|
|
1094
|
+
return new MagicError("not_logged_in", "Your Magic Spells sign-in is no longer valid.", {
|
|
1095
|
+
product: spec.id,
|
|
1096
|
+
hint: "Run `magic login`."
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
if (status === 403 && code === "no_workspaces") {
|
|
1100
|
+
return new MagicError("permission_denied", `You have no ${spec.title} workspaces.`, {
|
|
1101
|
+
product: spec.id,
|
|
1102
|
+
details: { reason: "no_workspaces" }
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
if (status === 404) {
|
|
1106
|
+
return new MagicError("not_found", `No ${spec.title} workspace by that handle.`, {
|
|
1107
|
+
product: spec.id,
|
|
1108
|
+
details: { kind: "workspace" }
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
return mapHttpError(status, body, { product: spec.id, title: spec.title });
|
|
1112
|
+
}
|
|
1113
|
+
function readSessions(spec, body) {
|
|
1114
|
+
const b = rec(body);
|
|
1115
|
+
const raw = Array.isArray(b.sessions) ? b.sessions : [];
|
|
1116
|
+
const sessions = [];
|
|
1117
|
+
for (const item of raw) {
|
|
1118
|
+
const s = rec(item);
|
|
1119
|
+
const key = readStr(s.key);
|
|
1120
|
+
const ws = rec(s.workspace);
|
|
1121
|
+
const handle = readStr(ws.handle);
|
|
1122
|
+
if (!key || !key.startsWith(spec.credential) || !handle) {
|
|
1123
|
+
throw new MagicError("unknown", `${spec.title} answered with a malformed session.`, {
|
|
1124
|
+
product: spec.id
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
sessions.push({
|
|
1128
|
+
product: spec.id,
|
|
1129
|
+
key,
|
|
1130
|
+
...readStr(s.key_id) ? { key_id: readStr(s.key_id) } : {},
|
|
1131
|
+
workspace: { id: readStr(ws.id) ?? "", handle, name: readStr(ws.name) ?? handle },
|
|
1132
|
+
...readStr(s.role) ? { role: readStr(s.role) } : {},
|
|
1133
|
+
...readStr(s.expires_at) ? { expires_at: readStr(s.expires_at) } : {},
|
|
1134
|
+
source: "account"
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
const workspaces = (Array.isArray(b.workspaces) ? b.workspaces : []).flatMap(
|
|
1138
|
+
(item) => {
|
|
1139
|
+
const w = rec(item);
|
|
1140
|
+
const handle = readStr(w.handle);
|
|
1141
|
+
if (!handle) return [];
|
|
1142
|
+
return [
|
|
1143
|
+
{
|
|
1144
|
+
id: readStr(w.id) ?? "",
|
|
1145
|
+
handle,
|
|
1146
|
+
name: readStr(w.name) ?? handle,
|
|
1147
|
+
role: readStr(w.role) ?? ""
|
|
1148
|
+
}
|
|
1149
|
+
];
|
|
1150
|
+
}
|
|
1151
|
+
);
|
|
1152
|
+
const u = rec(b.user);
|
|
1153
|
+
const email = readStr(u.email);
|
|
1154
|
+
const user = email ? { id: readStr(u.id) ?? "", display_name: readStr(u.display_name) ?? email, email } : void 0;
|
|
1155
|
+
return { sessions, workspaces, ...user ? { user } : {} };
|
|
1156
|
+
}
|
|
1157
|
+
async function storeSessions(store, product, res, now = /* @__PURE__ */ new Date()) {
|
|
1158
|
+
const index = await readIndex(store);
|
|
1159
|
+
const handles = new Set(res.sessions.map((s) => s.workspace.handle));
|
|
1160
|
+
for (const old of index[product]?.workspaces ?? []) {
|
|
1161
|
+
if (!handles.has(old.handle)) await store.delete(sessionItem(product, old.handle));
|
|
1162
|
+
}
|
|
1163
|
+
for (const s of res.sessions) {
|
|
1164
|
+
await store.set(sessionItem(product, s.workspace.handle), JSON.stringify(s));
|
|
1165
|
+
}
|
|
1166
|
+
const workspaces = res.sessions.map((s) => {
|
|
1167
|
+
const listed = res.workspaces.find((w) => w.handle === s.workspace.handle);
|
|
1168
|
+
return {
|
|
1169
|
+
id: s.workspace.id || listed?.id || "",
|
|
1170
|
+
handle: s.workspace.handle,
|
|
1171
|
+
name: s.workspace.name || listed?.name || s.workspace.handle,
|
|
1172
|
+
role: s.role ?? listed?.role ?? ""
|
|
1173
|
+
};
|
|
1174
|
+
});
|
|
1175
|
+
const entry = {
|
|
1176
|
+
workspaces,
|
|
1177
|
+
...workspaces.length === 1 ? { default: workspaces[0].handle } : {},
|
|
1178
|
+
minted_at: now.toISOString()
|
|
1179
|
+
};
|
|
1180
|
+
index[product] = entry;
|
|
1181
|
+
await saveIndex(store, index);
|
|
1182
|
+
return entry;
|
|
1183
|
+
}
|
|
1184
|
+
async function productSessions(store, product) {
|
|
1185
|
+
const entry = (await readIndex(store))[product];
|
|
1186
|
+
const out = [];
|
|
1187
|
+
for (const w of entry?.workspaces ?? []) {
|
|
1188
|
+
const s = await readSession(store, product, w.handle);
|
|
1189
|
+
if (s) out.push(s);
|
|
1190
|
+
}
|
|
1191
|
+
return out;
|
|
1192
|
+
}
|
|
1193
|
+
async function forgetSessions(store, product) {
|
|
1194
|
+
const index = await readIndex(store);
|
|
1195
|
+
const entry = index[product];
|
|
1196
|
+
if (!entry) return [];
|
|
1197
|
+
const items = entry.workspaces.map((w) => sessionItem(product, w.handle));
|
|
1198
|
+
for (const item of items) await store.delete(item);
|
|
1199
|
+
delete index[product];
|
|
1200
|
+
await saveIndex(store, index);
|
|
1201
|
+
return items;
|
|
1202
|
+
}
|
|
1203
|
+
async function touchGrant(store, grant) {
|
|
1204
|
+
const touched = { ...grant, last_used_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1205
|
+
await saveGrant(store, touched);
|
|
1206
|
+
return touched;
|
|
1207
|
+
}
|
|
1208
|
+
async function remintSession(spec, session, opts) {
|
|
1209
|
+
const grant = await readGrant(opts.store);
|
|
1210
|
+
const handle = session.workspace?.handle;
|
|
1211
|
+
if (!grant || !handle) {
|
|
1212
|
+
throw new MagicError("not_logged_in", `Your ${spec.title} session has expired.`, {
|
|
1213
|
+
product: spec.id,
|
|
1214
|
+
hint: "Run `magic login`."
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
const res = await mintSessions(spec, {
|
|
1218
|
+
apiUrl: opts.apiUrl,
|
|
1219
|
+
grant: grant.access_token,
|
|
1220
|
+
workspace: handle,
|
|
1221
|
+
...opts.fetch ? { fetch: opts.fetch } : {},
|
|
1222
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
1223
|
+
});
|
|
1224
|
+
const fresh = res.sessions.find((s) => s.workspace?.handle === handle);
|
|
1225
|
+
if (!fresh) {
|
|
1226
|
+
throw new MagicError(
|
|
1227
|
+
"not_found",
|
|
1228
|
+
`You no longer reach the ${spec.title} workspace "${handle}".`,
|
|
1229
|
+
{
|
|
1230
|
+
product: spec.id,
|
|
1231
|
+
details: { kind: "workspace" },
|
|
1232
|
+
hint: "Run `magic login` to refresh your workspaces."
|
|
1233
|
+
}
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
await opts.store.set(sessionItem(spec.id, handle), JSON.stringify(fresh));
|
|
1237
|
+
await touchGrant(opts.store, grant);
|
|
1238
|
+
return fresh;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
697
1241
|
// ../kit/src/credentials.ts
|
|
698
1242
|
function manualItem(product) {
|
|
699
1243
|
return `${product}:manual`;
|
|
@@ -719,7 +1263,8 @@ function parseStoredSession(product, value) {
|
|
|
719
1263
|
}
|
|
720
1264
|
function loginHint(spec) {
|
|
721
1265
|
const where = spec.keyHelp ? ` (${spec.keyHelp})` : "";
|
|
722
|
-
|
|
1266
|
+
const manual = `${productEnvVar(spec.id, "KEY")} or run \`magic login --product-key\` with a ${spec.credential}\u2026 key${where}.`;
|
|
1267
|
+
return spec.sessions ? `Run \`magic login\`, or set ${manual}` : `Set ${manual}`;
|
|
723
1268
|
}
|
|
724
1269
|
function checkPrefix(spec, key) {
|
|
725
1270
|
if (!key.startsWith(spec.credential)) {
|
|
@@ -740,16 +1285,40 @@ async function resolveCredential(spec, opts) {
|
|
|
740
1285
|
checkPrefix(spec, envKey);
|
|
741
1286
|
return { product: spec.id, key: envKey, source: "env" };
|
|
742
1287
|
}
|
|
1288
|
+
const minted = (await readIndex(opts.store))[spec.id];
|
|
1289
|
+
const handle = opts.workspace ?? minted?.default;
|
|
1290
|
+
if (minted && handle) {
|
|
1291
|
+
const account = await readSession(opts.store, spec.id, handle);
|
|
1292
|
+
if (account) {
|
|
1293
|
+
checkPrefix(spec, account.key);
|
|
1294
|
+
return account;
|
|
1295
|
+
}
|
|
1296
|
+
if (opts.workspace) {
|
|
1297
|
+
throw new MagicError("not_found", `No ${spec.title} session for workspace "${handle}".`, {
|
|
1298
|
+
product: spec.id,
|
|
1299
|
+
details: { kind: "workspace" },
|
|
1300
|
+
candidates: minted.workspaces.map((w) => w.handle),
|
|
1301
|
+
hint: "Pick one of the listed workspaces, or run `magic login` to refresh them."
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
743
1305
|
const stored = await opts.store.get(manualItem(spec.id));
|
|
744
1306
|
const session = stored ? parseStoredSession(spec.id, stored) : null;
|
|
745
|
-
if (
|
|
746
|
-
|
|
1307
|
+
if (session) {
|
|
1308
|
+
checkPrefix(spec, session.key);
|
|
1309
|
+
return session;
|
|
1310
|
+
}
|
|
1311
|
+
if (minted && minted.workspaces.length > 1) {
|
|
1312
|
+
throw new MagicError("usage", `You have several ${spec.title} workspaces; choose one.`, {
|
|
747
1313
|
product: spec.id,
|
|
748
|
-
|
|
1314
|
+
candidates: minted.workspaces.map((w) => w.handle),
|
|
1315
|
+
hint: "Pass --workspace <handle>, or set defaultWorkspace in the config file."
|
|
749
1316
|
});
|
|
750
1317
|
}
|
|
751
|
-
|
|
752
|
-
|
|
1318
|
+
throw new MagicError("not_logged_in", `No ${spec.title} key found.`, {
|
|
1319
|
+
product: spec.id,
|
|
1320
|
+
hint: loginHint(spec)
|
|
1321
|
+
});
|
|
753
1322
|
}
|
|
754
1323
|
async function saveProductKey(spec, store, key) {
|
|
755
1324
|
const trimmed = key.trim();
|
|
@@ -772,7 +1341,12 @@ function productConfig(product, deps, extra = {}) {
|
|
|
772
1341
|
});
|
|
773
1342
|
}
|
|
774
1343
|
async function buildContext(product, config, deps, requiresSession = true) {
|
|
775
|
-
const
|
|
1344
|
+
const workspace = config.defaultWorkspace[product.id];
|
|
1345
|
+
const session = requiresSession ? await resolveCredential(product, {
|
|
1346
|
+
env: deps.env ?? process.env,
|
|
1347
|
+
store: deps.store,
|
|
1348
|
+
...workspace ? { workspace } : {}
|
|
1349
|
+
}) : null;
|
|
776
1350
|
return contextFor(product, config, session, deps.signal);
|
|
777
1351
|
}
|
|
778
1352
|
function contextFor(product, config, session, signal = new AbortController().signal) {
|
|
@@ -783,6 +1357,27 @@ function contextFor(product, config, session, signal = new AbortController().sig
|
|
|
783
1357
|
signal
|
|
784
1358
|
};
|
|
785
1359
|
}
|
|
1360
|
+
function remintFor(product, store) {
|
|
1361
|
+
return async (ctx) => {
|
|
1362
|
+
if (ctx.session?.source !== "account" || !product.sessions) return null;
|
|
1363
|
+
const fresh = await remintSession(product, ctx.session, {
|
|
1364
|
+
store,
|
|
1365
|
+
apiUrl: ctx.config.products[product.id].apiUrl,
|
|
1366
|
+
signal: ctx.signal
|
|
1367
|
+
});
|
|
1368
|
+
return contextFor(product, ctx.config, fresh, ctx.signal);
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
async function runWithRemint(op, input, ctx, remint) {
|
|
1372
|
+
try {
|
|
1373
|
+
return await op.run(input, ctx);
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
if (!remint || !isMagicError(err) || err.code !== "auth_expired") throw err;
|
|
1376
|
+
const fresh = await remint(ctx);
|
|
1377
|
+
if (!fresh) throw err;
|
|
1378
|
+
return op.run(input, fresh);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
786
1381
|
|
|
787
1382
|
// ../kit/src/cli.ts
|
|
788
1383
|
async function runProductCli(product, argv, deps) {
|
|
@@ -840,7 +1435,7 @@ async function runProductCli(product, argv, deps) {
|
|
|
840
1435
|
if (!config.allowDestructive) throw destructiveDisabled(product.id);
|
|
841
1436
|
}
|
|
842
1437
|
const ctx = await buildContext(product, config, deps, op.meta?.requiresSession !== false);
|
|
843
|
-
const out = await op
|
|
1438
|
+
const out = await runWithRemint(op, input, ctx, remintFor(product, deps.store));
|
|
844
1439
|
render(out, opts, io);
|
|
845
1440
|
return 0;
|
|
846
1441
|
} catch (err) {
|
|
@@ -864,11 +1459,8 @@ function buildInput(cmd, parsed, productFlags, opts) {
|
|
|
864
1459
|
for (const [key, value] of Object.entries(productFlags)) {
|
|
865
1460
|
if (has(key) && raw[key] === void 0) raw[key] = value;
|
|
866
1461
|
}
|
|
867
|
-
if (opts.workspace) {
|
|
868
|
-
|
|
869
|
-
throw usage(`--workspace is not supported by ${cmd.words.join(" ")}`);
|
|
870
|
-
}
|
|
871
|
-
if (raw.workspace === void 0) raw.workspace = opts.workspace;
|
|
1462
|
+
if (opts.workspace && has("workspace") && raw.workspace === void 0) {
|
|
1463
|
+
raw.workspace = opts.workspace;
|
|
872
1464
|
}
|
|
873
1465
|
const result = cmd.op.input.safeParse(coerce(raw, fields));
|
|
874
1466
|
if (!result.success) {
|
|
@@ -926,17 +1518,6 @@ function summaryOf(op) {
|
|
|
926
1518
|
return op.meta?.destructive ? `${op.summary} [destructive]` : op.summary;
|
|
927
1519
|
}
|
|
928
1520
|
|
|
929
|
-
// ../kit/src/read.ts
|
|
930
|
-
function rec(v) {
|
|
931
|
-
return v && typeof v === "object" ? v : {};
|
|
932
|
-
}
|
|
933
|
-
function readStr(v) {
|
|
934
|
-
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
935
|
-
}
|
|
936
|
-
function readField(obj, key) {
|
|
937
|
-
return obj && typeof obj === "object" && key in obj ? obj[key] : void 0;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
1521
|
// ../kit/src/doctor.ts
|
|
941
1522
|
async function runDoctorChecks(checks, ctx, product = "magic") {
|
|
942
1523
|
const results = [];
|
|
@@ -957,6 +1538,102 @@ async function runDoctorChecks(checks, ctx, product = "magic") {
|
|
|
957
1538
|
return results;
|
|
958
1539
|
}
|
|
959
1540
|
|
|
1541
|
+
// ../kit/src/loopback.ts
|
|
1542
|
+
import { timingSafeEqual } from "node:crypto";
|
|
1543
|
+
import { createServer } from "node:http";
|
|
1544
|
+
var LOOPBACK_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
1545
|
+
var CODE_EXCHANGE_TIMEOUT_MS = 30 * 1e3;
|
|
1546
|
+
async function startLoopback(opts) {
|
|
1547
|
+
let settle;
|
|
1548
|
+
const result = new Promise((resolve, reject) => settle = { resolve, reject });
|
|
1549
|
+
result.catch(() => {
|
|
1550
|
+
});
|
|
1551
|
+
let done = false;
|
|
1552
|
+
let exchanging = false;
|
|
1553
|
+
const server = createServer((req, res) => void handle(req, res));
|
|
1554
|
+
const finish = (outcome) => {
|
|
1555
|
+
if (done) return;
|
|
1556
|
+
done = true;
|
|
1557
|
+
clearTimeout(timer);
|
|
1558
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
1559
|
+
server.close();
|
|
1560
|
+
server.closeIdleConnections();
|
|
1561
|
+
if ("value" in outcome) settle.resolve(outcome.value);
|
|
1562
|
+
else settle.reject(outcome.error);
|
|
1563
|
+
};
|
|
1564
|
+
const handle = async (req, res) => {
|
|
1565
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1566
|
+
const state = url.searchParams.get("state");
|
|
1567
|
+
if (done || exchanging || req.method !== "GET" || url.pathname !== "/callback") {
|
|
1568
|
+
return reply(res, 400, "Bad request.");
|
|
1569
|
+
}
|
|
1570
|
+
if (!sameState(state, opts.state)) return reply(res, 400, "Bad request.");
|
|
1571
|
+
const error = url.searchParams.get("error");
|
|
1572
|
+
if (error) {
|
|
1573
|
+
reply(res, 200, "Sign-in cancelled. You can close this tab and return to your terminal.");
|
|
1574
|
+
return finish({
|
|
1575
|
+
error: error === "access_denied" ? cancelledError() : new MagicError("auth_invalid", `Account refused the sign-in (${error}).`, {
|
|
1576
|
+
product: ACCOUNT_PRODUCT
|
|
1577
|
+
})
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
const code = url.searchParams.get("code");
|
|
1581
|
+
if (!code) return reply(res, 400, "Bad request.");
|
|
1582
|
+
exchanging = true;
|
|
1583
|
+
clearTimeout(timer);
|
|
1584
|
+
const signals = [AbortSignal.timeout(CODE_EXCHANGE_TIMEOUT_MS)];
|
|
1585
|
+
if (opts.signal) signals.push(opts.signal);
|
|
1586
|
+
try {
|
|
1587
|
+
const value = await opts.exchange(code, redirectUri, AbortSignal.any(signals));
|
|
1588
|
+
reply(res, 200, "Signed in to Magic Spells. You can close this tab.");
|
|
1589
|
+
finish({ value });
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
reply(res, 400, "Sign-in failed. Return to your terminal for details.");
|
|
1592
|
+
finish({ error: toMagicError(err, { product: ACCOUNT_PRODUCT }) });
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
const onAbort = () => finish({ error: cancelledError() });
|
|
1596
|
+
const timer = setTimeout(
|
|
1597
|
+
() => finish({
|
|
1598
|
+
error: new MagicError("auth_expired", "Sign-in timed out waiting for the browser.", {
|
|
1599
|
+
product: ACCOUNT_PRODUCT,
|
|
1600
|
+
hint: "Run `magic login --device` to sign in with a code instead."
|
|
1601
|
+
})
|
|
1602
|
+
}),
|
|
1603
|
+
opts.timeoutMs ?? LOOPBACK_TIMEOUT_MS
|
|
1604
|
+
);
|
|
1605
|
+
timer.unref?.();
|
|
1606
|
+
await new Promise((resolve, reject) => {
|
|
1607
|
+
server.once("error", reject);
|
|
1608
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1609
|
+
server.off("error", reject);
|
|
1610
|
+
resolve();
|
|
1611
|
+
});
|
|
1612
|
+
}).catch((err) => {
|
|
1613
|
+
clearTimeout(timer);
|
|
1614
|
+
throw toMagicError(err, { product: ACCOUNT_PRODUCT });
|
|
1615
|
+
});
|
|
1616
|
+
const redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
|
|
1617
|
+
if (opts.signal?.aborted) onAbort();
|
|
1618
|
+
else opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1619
|
+
return { redirectUri, result };
|
|
1620
|
+
}
|
|
1621
|
+
function sameState(got, want) {
|
|
1622
|
+
if (got === null) return false;
|
|
1623
|
+
const a = Buffer.from(got);
|
|
1624
|
+
const b = Buffer.from(want);
|
|
1625
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
1626
|
+
}
|
|
1627
|
+
function reply(res, status, text) {
|
|
1628
|
+
res.writeHead(status, {
|
|
1629
|
+
"content-type": "text/plain; charset=utf-8",
|
|
1630
|
+
"cache-control": "no-store",
|
|
1631
|
+
connection: "close"
|
|
1632
|
+
});
|
|
1633
|
+
res.end(`${text}
|
|
1634
|
+
`);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
960
1637
|
// ../kit/src/mcp.ts
|
|
961
1638
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
962
1639
|
import { z } from "zod";
|
|
@@ -988,7 +1665,7 @@ ${text}` : text];
|
|
|
988
1665
|
}
|
|
989
1666
|
function registerProduct(server, p) {
|
|
990
1667
|
for (const op of p.operations) {
|
|
991
|
-
if (onSurface(op, "mcp")) registerTool(server, p.module.id, op, p.context);
|
|
1668
|
+
if (onSurface(op, "mcp")) registerTool(server, p.module.id, op, p.context, p.remint);
|
|
992
1669
|
}
|
|
993
1670
|
p.module.mcp?.register?.(server, p.context);
|
|
994
1671
|
}
|
|
@@ -1000,7 +1677,7 @@ function toolInputShape(op) {
|
|
|
1000
1677
|
yes: z.boolean().optional().describe("Confirm this destructive action (also needs MAGIC_ALLOW_DESTRUCTIVE=1).")
|
|
1001
1678
|
};
|
|
1002
1679
|
}
|
|
1003
|
-
async function callTool(product, op, context, args) {
|
|
1680
|
+
async function callTool(product, op, context, args, remint) {
|
|
1004
1681
|
try {
|
|
1005
1682
|
const ctx = await context();
|
|
1006
1683
|
const { yes, ...rest } = args ?? {};
|
|
@@ -1015,7 +1692,7 @@ async function callTool(product, op, context, args) {
|
|
|
1015
1692
|
}
|
|
1016
1693
|
const parsed = op.input.safeParse(op.meta?.destructive ? rest : args ?? {});
|
|
1017
1694
|
if (!parsed.success) throw invalidInput(product, parsed.error);
|
|
1018
|
-
const result = await op
|
|
1695
|
+
const result = await runWithRemint(op, parsed.data, ctx, remint);
|
|
1019
1696
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1020
1697
|
} catch (err) {
|
|
1021
1698
|
return toToolError(toMagicError(err, { product }));
|
|
@@ -1030,14 +1707,14 @@ function invalidInput(product, error) {
|
|
|
1030
1707
|
...field ? { details: { field } } : {}
|
|
1031
1708
|
});
|
|
1032
1709
|
}
|
|
1033
|
-
function registerTool(server, product, op, context) {
|
|
1710
|
+
function registerTool(server, product, op, context, remint) {
|
|
1034
1711
|
const description = op.detail ? `${op.summary}
|
|
1035
1712
|
|
|
1036
1713
|
${op.detail}` : op.summary;
|
|
1037
1714
|
server.registerTool(
|
|
1038
1715
|
toolName(op),
|
|
1039
1716
|
{ description, inputSchema: toolInputShape(op) },
|
|
1040
|
-
async (args) => callTool(product, op, context, args)
|
|
1717
|
+
async (args) => callTool(product, op, context, args, remint)
|
|
1041
1718
|
);
|
|
1042
1719
|
}
|
|
1043
1720
|
|
|
@@ -1095,12 +1772,35 @@ function readPackageVersion(moduleUrl) {
|
|
|
1095
1772
|
|
|
1096
1773
|
export {
|
|
1097
1774
|
MagicError,
|
|
1775
|
+
isMagicError,
|
|
1098
1776
|
exitCodeFor,
|
|
1099
1777
|
mapHttpError,
|
|
1100
1778
|
toMagicError,
|
|
1101
1779
|
productEnvVar,
|
|
1102
1780
|
configFilePath,
|
|
1103
1781
|
readConfigFile,
|
|
1782
|
+
loadCliConfig,
|
|
1783
|
+
createPkce,
|
|
1784
|
+
randomState,
|
|
1785
|
+
authorizeUrl,
|
|
1786
|
+
AccountClient,
|
|
1787
|
+
waitForDevice,
|
|
1788
|
+
rec,
|
|
1789
|
+
readStr,
|
|
1790
|
+
readField,
|
|
1791
|
+
GRANT_ITEM,
|
|
1792
|
+
sessionItem,
|
|
1793
|
+
grantFromToken,
|
|
1794
|
+
grantIdleExpiry,
|
|
1795
|
+
grantExpired,
|
|
1796
|
+
readGrant,
|
|
1797
|
+
saveGrant,
|
|
1798
|
+
readIndex,
|
|
1799
|
+
mintSessions,
|
|
1800
|
+
revokeSessions,
|
|
1801
|
+
storeSessions,
|
|
1802
|
+
productSessions,
|
|
1803
|
+
forgetSessions,
|
|
1104
1804
|
manualItem,
|
|
1105
1805
|
maskKey,
|
|
1106
1806
|
resolveCredential,
|
|
@@ -1117,12 +1817,12 @@ export {
|
|
|
1117
1817
|
productConfig,
|
|
1118
1818
|
buildContext,
|
|
1119
1819
|
contextFor,
|
|
1820
|
+
remintFor,
|
|
1821
|
+
runWithRemint,
|
|
1120
1822
|
runDoctorChecks,
|
|
1823
|
+
startLoopback,
|
|
1121
1824
|
runProductCli,
|
|
1122
1825
|
createMcpServer,
|
|
1123
1826
|
TtlCache,
|
|
1124
|
-
rec,
|
|
1125
|
-
readStr,
|
|
1126
|
-
readField,
|
|
1127
1827
|
readPackageVersion
|
|
1128
1828
|
};
|