@genex-ai/cli-demo 1.34.2 → 1.34.3-dev.704

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 CHANGED
@@ -67,7 +67,7 @@ as the latest available snapshot.
67
67
  claude,codex,cursor`, or a single custom dir with `--dir`.
68
68
  2. **Authorizes you** — opens the Genex auth site (web) in your browser. If the
69
69
  browser can't open, it prints the URL to open manually.
70
- 3. **Saves your token** — writes `GENEX_TOKEN` to `~/.genex/env` (per-user;
70
+ 3. **Saves your token** — saves an origin-bound credential under `~/.genex/env.origins/` (per-user;
71
71
  reused across projects).
72
72
  4. **Creates the draft project** — `POST /api/projects` provisions a managed
73
73
  Forgejo repo (one per user; no SSH key) and stores the project metadata (id,
@@ -285,7 +285,25 @@ GENEX_AUTH_URL=https://staging.genex.dev genex init
285
285
  The token env file path is configurable the same way: `--env <path>` per
286
286
  command, or `GENEX_ENV_FILE=<path>` for a whole session (an explicit `--env`
287
287
  still wins). Useful for running as a second account — a curator or a test
288
- runner — without touching the default `~/.genex/env`.
288
+ runner — without touching the default profile. Each profile stores credentials
289
+ under `<env-path>.origins/<origin-hash>.json`, and pending sign-ins under
290
+ `<env-path>.auth/<origin-hash>.json`. Dev, production and custom servers coexist.
291
+
292
+ API destinations must be HTTPS origins without paths, queries, fragments or
293
+ embedded credentials. HTTP is supported for exact loopback hosts (`localhost`,
294
+ `127.0.0.1`, `[::1]`). Select custom servers explicitly with `--api-url` (and
295
+ `--auth-url` for their dashboard). Project metadata can select a saved credential
296
+ for its server, but cannot redirect a credential or initiate a new custom login.
297
+ API redirects are refused, including device-code polls.
298
+
299
+ Legacy `GENEX_TOKEN` files remain readable only at the configured CLI origin
300
+ (the installed channel's default or `GENEX_API_URL`), never a metadata-selected
301
+ origin. The first read records that binding permanently; switching channels
302
+ cannot move it. The legacy file remains intact, including hosted-session token
303
+ rotation. For an old custom-server file, set `GENEX_API_URL` to its known issuer
304
+ before the first read, or run `genex auth --api-url <origin>` to sign in again.
305
+ Older CLI versions do not read the new profiles; use the updated CLI for both
306
+ stands.
289
307
 
290
308
  To control which browser is launched, set `GENEX_BROWSER` (or the conventional
291
309
  `BROWSER`) to an opener command. It's parsed like a shell command, so arguments
@@ -346,7 +364,7 @@ The CLI uses a loopback-redirect flow (the same pattern as `gh auth login` and
346
364
  > the exact `state` it received. A missing or mismatched `state` is rejected
347
365
  > (CSRF protection).
348
366
 
349
- 3. The CLI verifies `state`, writes `GENEX_TOKEN=<TOKEN>` to `~/.genex/env`, and
367
+ 3. The CLI verifies `state`, saves the credential in the selected origin/profile, and
350
368
  shows a success page in the browser.
351
369
 
352
370
  If the browser can't be opened, the URL is printed and — when run in an
@@ -7,10 +7,10 @@ import {
7
7
  isRenderMode,
8
8
  sceneSummary,
9
9
  sheetOf
10
- } from "./chunk-OJAXYQ5L.js";
10
+ } from "./chunk-MRV3Q3WU.js";
11
11
  import {
12
12
  getCliVersion
13
- } from "./chunk-5FA2WLM7.js";
13
+ } from "./chunk-AQW7HUPV.js";
14
14
 
15
15
  // src/commands/blender-mcp.ts
16
16
  import fs from "fs/promises";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  c,
3
3
  getTemplatesDir
4
- } from "./chunk-5FA2WLM7.js";
4
+ } from "./chunk-AQW7HUPV.js";
5
5
 
6
6
  // src/lib/blender-serve.ts
7
7
  import { spawn } from "child_process";
@@ -1,9 +1,44 @@
1
+ // src/lib/credential-origin.ts
2
+ import { createHash } from "crypto";
3
+ function normalizeApiOrigin(raw) {
4
+ const url = new URL(raw);
5
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
6
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.search || url.hash || url.pathname.replace(/\/+$/, "") !== "") {
7
+ throw new Error("Genex API URL must be an HTTPS origin (HTTP is allowed on loopback only), without credentials, a path, query or fragment.");
8
+ }
9
+ return url.origin;
10
+ }
11
+ function originKey(apiUrl) {
12
+ return createHash("sha256").update(normalizeApiOrigin(apiUrl)).digest("hex");
13
+ }
14
+ var destinations = /* @__PURE__ */ new Map();
15
+ function bindToken(token, apiUrl) {
16
+ const key = createHash("sha256").update(token).digest("hex");
17
+ const origins = destinations.get(key) ?? /* @__PURE__ */ new Set();
18
+ origins.add(normalizeApiOrigin(apiUrl));
19
+ destinations.set(key, origins);
20
+ return token;
21
+ }
22
+ function assertTokenDestination(token, requestUrl) {
23
+ const key = createHash("sha256").update(token).digest("hex");
24
+ const origins = destinations.get(key);
25
+ const url = new URL(requestUrl);
26
+ if (url.username || url.password || origins && !origins.has(url.origin)) {
27
+ throw new Error("Refusing to send a Genex credential to a different API origin. Run genex auth for the intended server.");
28
+ }
29
+ }
30
+ function assertAuthorizationOrigin(apiUrl, selectedApiUrl) {
31
+ if (normalizeApiOrigin(apiUrl) !== normalizeApiOrigin(selectedApiUrl)) {
32
+ throw new Error(`No saved sign-in for ${normalizeApiOrigin(apiUrl)}. Select that server explicitly with genex auth --api-url before retrying.`);
33
+ }
34
+ }
35
+
1
36
  // src/config.ts
2
37
  import fs from "fs";
3
38
  import os from "os";
4
39
  import path from "path";
5
40
  import { fileURLToPath } from "url";
6
- var RAW_CHANNEL = "latest";
41
+ var RAW_CHANNEL = "dev";
7
42
  var CLI_CHANNEL = RAW_CHANNEL === "dev" ? "dev" : "latest";
8
43
  var STANDS = {
9
44
  prod: { api: "https://api.genex.games", dashboard: "https://genex.games" },
@@ -20,7 +55,6 @@ function getAnimsBase(override) {
20
55
  function getAnimsCacheDir() {
21
56
  return path.join(getGenexDir(), "cache", "anims");
22
57
  }
23
- var ENV_TOKEN_KEY = "GENEX_TOKEN";
24
58
  var AUTH_URL_ENV = "GENEX_AUTH_URL";
25
59
  var API_URL_ENV = "GENEX_API_URL";
26
60
  var ENV_FILE_ENV = "GENEX_ENV_FILE";
@@ -30,7 +64,7 @@ function getAuthUrl(override) {
30
64
  }
31
65
  function getApiUrl(override) {
32
66
  const raw = override || process.env[API_URL_ENV] || DEFAULT_API_URL;
33
- return raw.replace(/\/+$/, "");
67
+ return normalizeApiOrigin(raw);
34
68
  }
35
69
  function getGenexDir() {
36
70
  return path.join(os.homedir(), ".genex");
@@ -107,13 +141,17 @@ var c = {
107
141
  };
108
142
 
109
143
  export {
144
+ normalizeApiOrigin,
145
+ originKey,
146
+ bindToken,
147
+ assertTokenDestination,
148
+ assertAuthorizationOrigin,
110
149
  CLI_CHANNEL,
111
150
  STANDS,
112
151
  DEFAULT_AUTH_URL,
113
152
  DEFAULT_API_URL,
114
153
  getAnimsBase,
115
154
  getAnimsCacheDir,
116
- ENV_TOKEN_KEY,
117
155
  ENV_FILE_ENV,
118
156
  getAuthUrl,
119
157
  getApiUrl,
@@ -1,13 +1,16 @@
1
1
  import {
2
2
  CLI_CHANNEL,
3
3
  ENV_FILE_ENV,
4
- ENV_TOKEN_KEY,
4
+ assertTokenDestination,
5
+ bindToken,
5
6
  c,
6
7
  getApiUrl,
7
8
  getAuthUrl,
8
9
  getCliVersion,
9
- getGenexEnvPath
10
- } from "./chunk-5FA2WLM7.js";
10
+ getGenexEnvPath,
11
+ normalizeApiOrigin,
12
+ originKey
13
+ } from "./chunk-AQW7HUPV.js";
11
14
 
12
15
  // src/lib/terms.ts
13
16
  import readline from "readline";
@@ -396,7 +399,9 @@ async function apiFetch(url, init = {}, opts = {}) {
396
399
  if (assetsMode && !headers.has(ASSETS_MODE_HEADER)) {
397
400
  headers.set(ASSETS_MODE_HEADER, assetsMode);
398
401
  }
399
- const res = await fetch(url, { ...init, headers });
402
+ const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "");
403
+ if (bearer) assertTokenDestination(bearer, url);
404
+ const res = await fetch(url, { ...init, headers, redirect: "error" });
400
405
  if (res.status === 403 && !opts.noTermsWait && await isTermsRefusal(res)) {
401
406
  const log = stderrLogger;
402
407
  reportTermsRefusal(log, await termsRefusalUrl(res));
@@ -490,46 +495,22 @@ async function fetchSignedInEmail(apiUrl, token) {
490
495
  }
491
496
 
492
497
  // src/lib/blender-client.ts
493
- import fs4 from "fs";
494
- import path4 from "path";
498
+ import fs5 from "fs";
499
+ import path5 from "path";
495
500
 
496
501
  // src/lib/store.ts
502
+ import fs4 from "fs/promises";
503
+ import path4 from "path";
504
+
505
+ // src/lib/secret-file.ts
497
506
  import fs3 from "fs/promises";
498
507
  import path3 from "path";
508
+ import { randomUUID } from "crypto";
499
509
 
500
510
  // src/lib/env.ts
501
511
  import fs2 from "fs/promises";
502
512
  import path2 from "path";
503
513
  import { spawn as spawn2 } from "child_process";
504
- async function writeEnvVar(envPath, key, value) {
505
- let content = "";
506
- let existed = false;
507
- try {
508
- content = await fs2.readFile(envPath, "utf8");
509
- existed = true;
510
- } catch {
511
- }
512
- const assignment = `${key}=${formatValue(value)}`;
513
- const keyPattern = new RegExp(
514
- `^(\\s*export\\s+)?${escapeRegExp(key)}=.*$`,
515
- "gm"
516
- );
517
- let next;
518
- let mode;
519
- if (keyPattern.test(content)) {
520
- next = content.replace(keyPattern, assignment);
521
- mode = "updated";
522
- } else {
523
- let prefix = content;
524
- if (prefix.length > 0 && !prefix.endsWith("\n")) prefix += "\n";
525
- next = prefix + assignment + "\n";
526
- mode = existed ? "appended" : "created";
527
- }
528
- await fs2.mkdir(path2.dirname(envPath), { recursive: true });
529
- await fs2.writeFile(envPath, next, { mode: 384 });
530
- await restrictFilePermissions(envPath);
531
- return { mode, path: envPath };
532
- }
533
514
  async function restrictFilePermissions(filePath) {
534
515
  if (process.platform !== "win32") {
535
516
  await fs2.chmod(filePath, 384).catch(() => {
@@ -552,26 +533,31 @@ async function restrictFilePermissions(filePath) {
552
533
  }
553
534
  });
554
535
  }
555
- function formatValue(value) {
556
- if (/[\s#"'$`\\]/.test(value)) {
557
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
536
+
537
+ // src/lib/secret-file.ts
538
+ async function writeSecretFile(file, content, exclusive = false) {
539
+ await fs3.mkdir(path3.dirname(file), { recursive: true, mode: 448 });
540
+ const temporary = `${file}.${randomUUID()}.tmp`;
541
+ try {
542
+ await fs3.writeFile(temporary, content, { mode: 384, flag: "wx" });
543
+ await restrictFilePermissions(temporary);
544
+ if (exclusive) await fs3.link(temporary, file);
545
+ else await fs3.rename(temporary, file);
546
+ } finally {
547
+ await fs3.rm(temporary, { force: true });
558
548
  }
559
- return value;
560
- }
561
- function escapeRegExp(s) {
562
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
563
549
  }
564
550
 
565
551
  // src/lib/store.ts
566
552
  function getProjectMetadataPath(cwd = process.cwd()) {
567
- return path3.join(cwd, ".genex", "project.json");
553
+ return path4.join(cwd, ".genex", "project.json");
568
554
  }
569
555
  function getWorkspacePath(cwd = process.cwd()) {
570
- return path3.join(cwd, ".genex", "workspace.json");
556
+ return path4.join(cwd, ".genex", "workspace.json");
571
557
  }
572
558
  async function readWorkspace(cwd = process.cwd()) {
573
559
  try {
574
- const raw = await fs3.readFile(getWorkspacePath(cwd), "utf8");
560
+ const raw = await fs4.readFile(getWorkspacePath(cwd), "utf8");
575
561
  return JSON.parse(raw);
576
562
  } catch {
577
563
  return null;
@@ -579,39 +565,72 @@ async function readWorkspace(cwd = process.cwd()) {
579
565
  }
580
566
  async function writeWorkspace(meta, cwd = process.cwd()) {
581
567
  const file = getWorkspacePath(cwd);
582
- await fs3.mkdir(path3.dirname(file), { recursive: true });
583
- await fs3.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
584
- await fs3.chmod(file, 384).catch(() => {
568
+ await fs4.mkdir(path4.dirname(file), { recursive: true });
569
+ await fs4.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
570
+ await fs4.chmod(file, 384).catch(() => {
585
571
  });
586
572
  return { path: file };
587
573
  }
588
- async function writeUserToken(token, envPath) {
589
- const { path: written } = await writeEnvVar(getGenexEnvPath(envPath), ENV_TOKEN_KEY, token);
590
- return { path: written };
574
+ function credentialPath(envPath, apiUrl = getApiUrl()) {
575
+ return path4.join(`${getGenexEnvPath(envPath)}.origins`, `${originKey(apiUrl)}.json`);
576
+ }
577
+ async function writeUserToken(token, envPath, apiUrl = getApiUrl()) {
578
+ const origin = normalizeApiOrigin(apiUrl);
579
+ const file = credentialPath(envPath, origin);
580
+ await writeSecretFile(file, JSON.stringify({ apiUrl: origin, token }) + "\n");
581
+ bindToken(token, origin);
582
+ return { path: file };
591
583
  }
592
- async function rotateRejectedEnv(envPath) {
593
- const file = getGenexEnvPath(envPath);
594
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
595
- const aside = `${file}.rejected-${stamp}`;
584
+ async function rotateRejectedEnv(envPath, apiUrl = getApiUrl()) {
585
+ const file = credentialPath(envPath, apiUrl);
586
+ const aside = `${file}.rejected-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
596
587
  try {
597
- await fs3.rename(file, aside);
588
+ await fs4.rename(file, aside);
589
+ await writeSecretFile(file, JSON.stringify({ apiUrl: normalizeApiOrigin(apiUrl), token: null }));
598
590
  return aside;
599
- } catch {
591
+ } catch (error) {
592
+ if (error.code !== "ENOENT") throw error;
593
+ await writeSecretFile(file, JSON.stringify({ apiUrl: normalizeApiOrigin(apiUrl), token: null }));
600
594
  return null;
601
595
  }
602
596
  }
603
- async function readUserToken(envPath) {
604
- const fromGenex = await readTokenFromFile(getGenexEnvPath(envPath));
605
- if (fromGenex) return fromGenex;
606
- if (!envPath && !process.env[ENV_FILE_ENV]) {
607
- return readTokenFromFile(path3.join(process.cwd(), ".env"));
597
+ async function readUserToken(envPath, apiUrl = getApiUrl()) {
598
+ const origin = normalizeApiOrigin(apiUrl);
599
+ const file = credentialPath(envPath, origin);
600
+ try {
601
+ const record = JSON.parse(await fs4.readFile(file, "utf8"));
602
+ return record.apiUrl === origin && typeof record.token === "string" && record.token ? bindToken(record.token, origin) : null;
603
+ } catch (error) {
604
+ if (error.code !== "ENOENT") return null;
605
+ }
606
+ let legacyFile = getGenexEnvPath(envPath);
607
+ let token = await readTokenFromFile(legacyFile);
608
+ if (!token && !envPath && !process.env[ENV_FILE_ENV]) {
609
+ legacyFile = path4.join(process.cwd(), ".env");
610
+ token = await readTokenFromFile(legacyFile);
611
+ }
612
+ if (!token) return null;
613
+ const claim = `${legacyFile}.origins/legacy-origin.json`;
614
+ let claimed;
615
+ try {
616
+ claimed = JSON.parse(await fs4.readFile(claim, "utf8"));
617
+ } catch (error) {
618
+ if (error.code !== "ENOENT") return null;
619
+ if (origin !== getApiUrl()) return null;
620
+ try {
621
+ await writeSecretFile(claim, JSON.stringify({ apiUrl: origin }), true);
622
+ } catch (error2) {
623
+ if (error2.code !== "EEXIST") throw error2;
624
+ }
625
+ claimed = JSON.parse(await fs4.readFile(claim, "utf8"));
608
626
  }
609
- return null;
627
+ if (claimed.apiUrl !== origin) return null;
628
+ return bindToken(token, origin);
610
629
  }
611
630
  async function readTokenFromFile(file) {
612
631
  let content;
613
632
  try {
614
- content = await fs3.readFile(file, "utf8");
633
+ content = await fs4.readFile(file, "utf8");
615
634
  } catch {
616
635
  return null;
617
636
  }
@@ -627,7 +646,7 @@ function stripQuotes(v) {
627
646
  }
628
647
  async function readProject(cwd = process.cwd()) {
629
648
  try {
630
- const raw = await fs3.readFile(getProjectMetadataPath(cwd), "utf8");
649
+ const raw = await fs4.readFile(getProjectMetadataPath(cwd), "utf8");
631
650
  return JSON.parse(raw);
632
651
  } catch {
633
652
  return null;
@@ -636,11 +655,11 @@ async function readProject(cwd = process.cwd()) {
636
655
  var SCRATCH_DIR = ".genex/scratch";
637
656
  async function writeProject(meta, cwd = process.cwd()) {
638
657
  const file = getProjectMetadataPath(cwd);
639
- await fs3.mkdir(path3.dirname(file), { recursive: true });
640
- await fs3.mkdir(path3.join(cwd, SCRATCH_DIR), { recursive: true }).catch(() => {
658
+ await fs4.mkdir(path4.dirname(file), { recursive: true });
659
+ await fs4.mkdir(path4.join(cwd, SCRATCH_DIR), { recursive: true }).catch(() => {
641
660
  });
642
- await fs3.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
643
- await fs3.chmod(file, 384).catch(() => {
661
+ await fs4.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
662
+ await fs4.chmod(file, 384).catch(() => {
644
663
  });
645
664
  return { path: file };
646
665
  }
@@ -678,10 +697,10 @@ function sheetOf(r) {
678
697
  function sheetExt(mime) {
679
698
  return mime === "image/webp" ? "webp" : "png";
680
699
  }
681
- var SEAT_FILE = path4.join(".genex", "blender-seat.json");
700
+ var SEAT_FILE = path5.join(".genex", "blender-seat.json");
682
701
  function readSeatGrant(cwd = process.cwd()) {
683
702
  try {
684
- const raw = JSON.parse(fs4.readFileSync(path4.join(cwd, SEAT_FILE), "utf8"));
703
+ const raw = JSON.parse(fs5.readFileSync(path5.join(cwd, SEAT_FILE), "utf8"));
685
704
  if (typeof raw.url !== "string" || typeof raw.token !== "string") return null;
686
705
  return {
687
706
  url: raw.url,
@@ -694,13 +713,13 @@ function readSeatGrant(cwd = process.cwd()) {
694
713
  }
695
714
  }
696
715
  function writeSeatGrant(grant, cwd = process.cwd()) {
697
- const file = path4.join(cwd, SEAT_FILE);
698
- fs4.mkdirSync(path4.dirname(file), { recursive: true });
699
- fs4.writeFileSync(file, JSON.stringify(grant, null, 2) + "\n", { mode: 384 });
716
+ const file = path5.join(cwd, SEAT_FILE);
717
+ fs5.mkdirSync(path5.dirname(file), { recursive: true });
718
+ fs5.writeFileSync(file, JSON.stringify(grant, null, 2) + "\n", { mode: 384 });
700
719
  }
701
720
  function deleteSeatGrant(cwd = process.cwd()) {
702
721
  try {
703
- fs4.unlinkSync(path4.join(cwd, SEAT_FILE));
722
+ fs5.unlinkSync(path5.join(cwd, SEAT_FILE));
704
723
  } catch {
705
724
  }
706
725
  }
@@ -710,7 +729,7 @@ async function touchCliSeat() {
710
729
  if (Date.now() - lastTouchAt < TOUCH_THROTTLE_MS) return;
711
730
  lastTouchAt = Date.now();
712
731
  try {
713
- const token = await readUserToken();
732
+ const token = await readUserToken(void 0, getApiUrl());
714
733
  if (!token) return;
715
734
  await apiFetch(`${getApiUrl()}/api/blender/seat/touch`, {
716
735
  method: "POST",
@@ -782,7 +801,7 @@ ${json.trace.slice(-4e3)}` : "";
782
801
  }
783
802
  if (seat?.kind === "cli") void touchCliSeat();
784
803
  if (seat?.kind === "hosted" && seat.seatId && route === "/health") {
785
- const token = await readUserToken();
804
+ const token = await readUserToken(void 0, getApiUrl());
786
805
  if (!token) throw new Error("Cannot acknowledge Blender readiness: not signed in");
787
806
  const claim = await apiFetch(`${getApiUrl()}/api/blender/seat/touch`, {
788
807
  method: "POST",
@@ -816,7 +835,7 @@ async function acquireSeat(opts) {
816
835
  const held = readSeatGrant(opts.cwd);
817
836
  if (held) return { kind: "granted", grant: held };
818
837
  if (!hostedBlenderLane()) return { kind: "off" };
819
- const token = opts.token !== void 0 ? opts.token : await readUserToken();
838
+ const token = opts.token !== void 0 ? opts.token : await readUserToken(void 0, getApiUrl());
820
839
  if (!token) return { kind: "refused", reason: "not signed in" };
821
840
  const doFetch = opts.fetchImpl ?? apiFetch;
822
841
  const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
@@ -857,7 +876,7 @@ async function acquireSeat(opts) {
857
876
  }
858
877
  }
859
878
  async function acquireCliSeat(opts) {
860
- const token = opts.token !== void 0 ? opts.token : await readUserToken();
879
+ const token = opts.token !== void 0 ? opts.token : await readUserToken(void 0, getApiUrl());
861
880
  if (!token) return { kind: "refused", reason: "not signed in" };
862
881
  const doFetch = opts.fetchImpl ?? apiFetch;
863
882
  const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
@@ -909,7 +928,7 @@ async function acquireCliSeat(opts) {
909
928
  }
910
929
  }
911
930
  async function releaseCliSeat(opts) {
912
- const token = opts.token !== void 0 ? opts.token : await readUserToken();
931
+ const token = opts.token !== void 0 ? opts.token : await readUserToken(void 0, getApiUrl());
913
932
  if (!token) return { ok: false, reason: "not signed in" };
914
933
  const doFetch = opts.fetchImpl ?? apiFetch;
915
934
  const res = await doFetch(`${getApiUrl()}/api/blender/seat`, {
@@ -944,7 +963,7 @@ export {
944
963
  printedStructuredError,
945
964
  apiFetch,
946
965
  fetchSignedInEmail,
947
- restrictFilePermissions,
966
+ writeSecretFile,
948
967
  readWorkspace,
949
968
  writeWorkspace,
950
969
  writeUserToken,