magic-spells 0.1.0 → 0.2.0

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