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.
@@ -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._def;
550
- const name = def?.typeName;
551
- if ((name === "ZodOptional" || name === "ZodNullable" || name === "ZodDefault") && def?.innerType) {
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._def;
586
+ const def = defOf(type);
566
587
  let kind = "string";
567
588
  let values;
568
- switch (def?.typeName) {
569
- case "ZodBoolean":
589
+ switch (def?.type) {
590
+ case "boolean":
570
591
  kind = "boolean";
571
592
  break;
572
- case "ZodNumber":
593
+ case "number":
573
594
  kind = "number";
574
595
  break;
575
- case "ZodArray": {
576
- const el = def.type ? baseType(def.type).type : void 0;
577
- const elName = el?._def?.typeName;
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 "ZodEnum":
601
+ case "enum":
582
602
  kind = "enum";
583
- if (Array.isArray(def.values)) values = def.values;
603
+ if (def.entries) values = Object.values(def.entries).map(String);
584
604
  break;
585
- case "ZodNativeEnum":
586
- kind = "enum";
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
- return `Set ${productEnvVar(spec.id, "KEY")} or run \`magic login --product-key\` with a ${spec.credential}\u2026 key${where}.`;
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 (!session) {
746
- throw new MagicError("not_logged_in", `No ${spec.title} key found.`, {
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
- hint: loginHint(spec)
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
- checkPrefix(spec, session.key);
752
- return session;
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 session = requiresSession ? await resolveCredential(product, { env: deps.env ?? process.env, store: deps.store }) : null;
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.run(input, ctx);
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
- if (!has("workspace")) {
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.run(parsed.data, ctx);
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
  };