@go-labs-sg/gosh 0.10.1 → 0.11.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.
Files changed (3) hide show
  1. package/README.md +10 -0
  2. package/dist/index.js +326 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -41,6 +41,16 @@ bun add --global @go-labs-sg/gosh
41
41
  gosh version
42
42
  ```
43
43
 
44
+ ## Secure credential storage
45
+
46
+ `GO_LABS_CLI_CREDENTIAL_ACCOUNT` selects the native credential-store account for this CLI. The default is `default`, preserving existing production logins. The downloadable staging launcher sets it to `staging` so a staging login does not replace the production login; keep the same account value for login and subsequent commands. The launcher also sets `GO_LABS_CLI_SKIP_DOTENV=1`, which skips the CLI's explicit current-directory `.env` load while leaving normal production behavior unchanged.
47
+
48
+ Browser sessions use `Bun.secrets`: macOS Keychain, Windows Credential Manager, or Linux Secret Service. Linux requires libsecret, a reachable user D-Bus session, and an unlocked Secret Service collection. SSH and containers must provide those native prerequisites themselves; `--no-browser` only lets approval happen on another device. Denied D-Bus access is a host permissions/session issue, not something the CLI bypasses.
49
+
50
+ Before requesting approval, login verifies write/read/delete capability with a unique non-secret probe. Native operations have a 10-second process deadline and sanitized errors. Issued credentials are read back and verified; failed persistence attempts remote revocation and reports local and remote cleanup separately. There is no plaintext fallback or external credential-helper configuration.
51
+
52
+ See the [shared authentication contract](../../cli-auth/README.md) for recovery and verification limits.
53
+
44
54
  ## Authentication
45
55
 
46
56
  ```bash
package/dist/index.js CHANGED
@@ -468,7 +468,9 @@ var require_dist = __commonJS(function(exports, module) {
468
468
  // src/load-env.ts
469
469
  var import_dotenv = __toESM(require_dist(), 1);
470
470
  import path from "path";
471
- import_dotenv.config({ path: path.resolve(process.cwd(), ".env"), quiet: true });
471
+ if (process.env.GO_LABS_CLI_SKIP_DOTENV !== "1") {
472
+ import_dotenv.config({ path: path.resolve(process.cwd(), ".env"), quiet: true });
473
+ }
472
474
 
473
475
  // src/cli.ts
474
476
  import { readFileSync } from "fs";
@@ -1486,6 +1488,8 @@ function datetime(args) {
1486
1488
  }
1487
1489
  var anyString = /^[\s\S]{0,}$/;
1488
1490
  var number = /^-?\d+(?:\.\d+)?$/;
1491
+ var boolean = /^(?:true|false)$/i;
1492
+ var _null = /^null$/i;
1489
1493
  var lowercase = /^[^A-Z]*$/;
1490
1494
  var uppercase = /^[^a-z]*$/;
1491
1495
 
@@ -2367,6 +2371,43 @@ var $ZodNumberFormat = /* @__PURE__ */ $constructor("$ZodNumberFormat", (inst, d
2367
2371
  $ZodCheckNumberFormat.init(inst, def);
2368
2372
  $ZodNumber.init(inst, def);
2369
2373
  });
2374
+ var $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
2375
+ $ZodType.init(inst, def);
2376
+ inst._zod.pattern = boolean;
2377
+ inst._zod.parse = (payload, _ctx) => {
2378
+ if (def.coerce)
2379
+ try {
2380
+ payload.value = Boolean(payload.value);
2381
+ } catch (_) {}
2382
+ const input = payload.value;
2383
+ if (typeof input === "boolean")
2384
+ return payload;
2385
+ payload.issues.push({
2386
+ expected: "boolean",
2387
+ code: "invalid_type",
2388
+ input,
2389
+ inst
2390
+ });
2391
+ return payload;
2392
+ };
2393
+ });
2394
+ var $ZodNull = /* @__PURE__ */ $constructor("$ZodNull", (inst, def) => {
2395
+ $ZodType.init(inst, def);
2396
+ inst._zod.pattern = _null;
2397
+ inst._zod.values = new Set([null]);
2398
+ inst._zod.parse = (payload, _ctx) => {
2399
+ const input = payload.value;
2400
+ if (input === null)
2401
+ return payload;
2402
+ payload.issues.push({
2403
+ expected: "null",
2404
+ code: "invalid_type",
2405
+ input,
2406
+ inst
2407
+ });
2408
+ return payload;
2409
+ };
2410
+ });
2370
2411
  var $ZodUnknown = /* @__PURE__ */ $constructor("$ZodUnknown", (inst, def) => {
2371
2412
  $ZodType.init(inst, def);
2372
2413
  inst._zod.parse = (payload) => payload;
@@ -3904,6 +3945,18 @@ function _int(Class, params) {
3904
3945
  ...normalizeParams(params)
3905
3946
  });
3906
3947
  }
3948
+ function _boolean(Class, params) {
3949
+ return new Class({
3950
+ type: "boolean",
3951
+ ...normalizeParams(params)
3952
+ });
3953
+ }
3954
+ function _null2(Class, params) {
3955
+ return new Class({
3956
+ type: "null",
3957
+ ...normalizeParams(params)
3958
+ });
3959
+ }
3907
3960
  function _unknown(Class) {
3908
3961
  return new Class({
3909
3962
  type: "unknown"
@@ -4786,6 +4839,18 @@ var numberProcessor = (schema, ctx, _json, params) => {
4786
4839
  json.allOf = [...json.allOf ?? [], ...rest.map((m) => ({ multipleOf: m }))];
4787
4840
  }
4788
4841
  };
4842
+ var booleanProcessor = (_schema, _ctx, json, _params) => {
4843
+ json.type = "boolean";
4844
+ };
4845
+ var nullProcessor = (_schema, ctx, json, _params) => {
4846
+ if (ctx.target === "openapi-3.0") {
4847
+ json.type = "string";
4848
+ json.nullable = true;
4849
+ json.enum = [null];
4850
+ } else {
4851
+ json.type = "null";
4852
+ }
4853
+ };
4789
4854
  var neverProcessor = (_schema, _ctx, json, _params) => {
4790
4855
  json.not = {};
4791
4856
  };
@@ -5580,6 +5645,22 @@ var ZodNumberFormat = /* @__PURE__ */ $constructor("ZodNumberFormat", (inst, def
5580
5645
  function int(params) {
5581
5646
  return _int(ZodNumberFormat, params);
5582
5647
  }
5648
+ var ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
5649
+ $ZodBoolean.init(inst, def);
5650
+ ZodType.init(inst, def);
5651
+ inst._zod.processJSONSchema = (ctx, json, params) => booleanProcessor(inst, ctx, json, params);
5652
+ });
5653
+ function boolean2(params) {
5654
+ return _boolean(ZodBoolean, params);
5655
+ }
5656
+ var ZodNull = /* @__PURE__ */ $constructor("ZodNull", (inst, def) => {
5657
+ $ZodNull.init(inst, def);
5658
+ ZodType.init(inst, def);
5659
+ inst._zod.processJSONSchema = (ctx, json, params) => nullProcessor(inst, ctx, json, params);
5660
+ });
5661
+ function _null3(params) {
5662
+ return _null2(ZodNull, params);
5663
+ }
5583
5664
  var ZodUnknown = /* @__PURE__ */ $constructor("ZodUnknown", (inst, def) => {
5584
5665
  $ZodUnknown.init(inst, def);
5585
5666
  ZodType.init(inst, def);
@@ -5998,6 +6079,9 @@ var storedCliCredentialsSchema = object({
5998
6079
  session: cliSessionIdentitySchema
5999
6080
  }).strict();
6000
6081
  var cliProductSchema = _enum(cliProducts);
6082
+ // ../../cli-auth/src/client/secret-backend.ts
6083
+ import { execFile } from "child_process";
6084
+
6001
6085
  // ../../cli-auth/src/client/runtime.ts
6002
6086
  var MINIMUM_BUN_VERSION = [1, 4, 0];
6003
6087
  var parseBunVersion = (version) => {
@@ -6023,27 +6107,145 @@ var assertBunRuntime = (runtime = globalThis.Bun) => {
6023
6107
  }
6024
6108
  };
6025
6109
 
6026
- // ../../cli-auth/src/client/credential-store.ts
6110
+ // ../../cli-auth/src/client/storage-error.ts
6111
+ var guidance = {
6112
+ "missing-library": "Install the runtime's native credential-store library and ensure your own native keyring/session is accessible and unlocked; Linux desktop storage requires libsecret AND an accessible, unlocked Secret Service.",
6113
+ "bus-unavailable": "The Linux user D-Bus session is absent or unreachable. --no-browser does not provide storage. Start or repair your own user session and native keyring before retrying.",
6114
+ "bus-denied": "Access to the Linux user bus was denied. Fix your own native keyring/session permissions or host policy; do not change bus/socket permissions or borrow another user's bus.",
6115
+ "service-unavailable": "The user bus is reachable but your own Secret Service is unavailable. Start or repair your own native keyring session; do not expose another user's store.",
6116
+ locked: "Unlock your own native credential collection/session before retrying. The CLI will not wait indefinitely for an unlock prompt.",
6117
+ timeout: "The native credential backend did not finish within 10 seconds and its process was terminated. Fix or unlock your own native keyring/session outside the CLI; the CLI will not bypass host permissions.",
6118
+ "invalid-response": "The native credential backend returned an invalid response. Check your own keyring/session and configuration; no credential output was disclosed.",
6119
+ "read-failed": "The native credential backend could not read credentials. Check your own keyring/session availability and permissions; this is not evidence that you are logged out.",
6120
+ "write-failed": "The native credential backend could not persist credentials. Check your own keyring/session write permissions and availability; no plaintext fallback is used.",
6121
+ "delete-failed": "The native credential backend could not delete its entry. Check your own keyring/session permissions and remove any non-secret preflight probe through that backend if necessary."
6122
+ };
6123
+
6027
6124
  class CliCredentialStoreUnavailableError extends Error {
6028
6125
  code = "CLI_CREDENTIAL_STORE_UNAVAILABLE";
6029
- constructor(product, options) {
6030
- super(`Secure credential storage is unavailable for the ${product} CLI. Bun.secrets requires the operating system credential store, including Secret Service on Linux. Browser authentication cannot continue without a supported secure store.`, options);
6126
+ reason;
6127
+ constructor(product, reason = "read-failed") {
6128
+ super(`Secure credential storage is unavailable for the ${product} CLI [CLI_CREDENTIAL_STORE_UNAVAILABLE:${reason}]. ${guidance[reason]}`);
6031
6129
  this.name = "CliCredentialStoreUnavailableError";
6032
- }
6130
+ this.reason = reason;
6131
+ }
6132
+ }
6133
+ var classifyCredentialStoreFailure = (message, fallback) => {
6134
+ if (/libsecret.*(not found|not available|cannot open|load)|dlopen|shared (object|library).*not found/i.test(message))
6135
+ return "missing-library";
6136
+ if (/(d-?bus|user bus|session bus).*(denied|not permitted)|(?:denied|not permitted).*(d-?bus|user bus|session bus)/i.test(message))
6137
+ return "bus-denied";
6138
+ if (/locked|interaction.*not allowed|user.*cancel/i.test(message))
6139
+ return "locked";
6140
+ if (/ServiceUnknown|org\.freedesktop\.secrets.*(not provided|no owner|not available)|secret.?service.*(unavailable|not running)/i.test(message))
6141
+ return "service-unavailable";
6142
+ if (/d-?bus|user bus|session bus|autolaunch/i.test(message))
6143
+ return "bus-unavailable";
6144
+ return fallback;
6145
+ };
6146
+
6147
+ class CliCredentialPersistenceError extends Error {
6148
+ remoteRevoked;
6149
+ localRestored;
6150
+ phase;
6151
+ code = "CLI_CREDENTIAL_PERSISTENCE_FAILED";
6152
+ constructor(remoteRevoked, localRestored, phase) {
6153
+ super(`CLI credentials could not be saved after ${phase} [CLI_CREDENTIAL_PERSISTENCE_FAILED]. ${remoteRevoked ? "The newly issued session was revoked." : "Remote revocation could not be confirmed; revoke the session in the product's session management before retrying."} ${localRestored ? phase === "login" ? "The previous local credentials were preserved." : "The unusable rotated local credentials were removed; sign in again." : "Local cleanup could not be confirmed. Inspect your own native credential backend before retrying."}`);
6154
+ this.remoteRevoked = remoteRevoked;
6155
+ this.localRestored = localRestored;
6156
+ this.phase = phase;
6157
+ this.name = "CliCredentialPersistenceError";
6158
+ }
6159
+ }
6160
+ var getCliStorageError = (error) => {
6161
+ if (error instanceof CliCredentialStoreUnavailableError)
6162
+ return {
6163
+ code: error.code,
6164
+ message: error.message,
6165
+ details: { reason: error.reason }
6166
+ };
6167
+ if (error instanceof CliCredentialPersistenceError)
6168
+ return {
6169
+ code: error.code,
6170
+ message: error.message,
6171
+ details: {
6172
+ remoteRevoked: error.remoteRevoked,
6173
+ localRestored: error.localRestored,
6174
+ phase: error.phase
6175
+ }
6176
+ };
6177
+ return;
6178
+ };
6179
+
6180
+ // ../../cli-auth/src/client/secret-backend.ts
6181
+ var timeoutMs = 1e4;
6182
+ var maxBuffer = 64 * 1024;
6183
+ var runBackend = (product, executable, args, input, failure) => new Promise((resolve, reject) => {
6184
+ const child = execFile(executable, args, {
6185
+ timeout: timeoutMs,
6186
+ killSignal: "SIGKILL",
6187
+ maxBuffer,
6188
+ windowsHide: true
6189
+ }, (error, stdout, stderr) => {
6190
+ if (error) {
6191
+ const reason = error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" ? "invalid-response" : error.killed ? "timeout" : classifyCredentialStoreFailure(`${stdout}
6192
+ ${stderr}`, failure);
6193
+ reject(new CliCredentialStoreUnavailableError(product, reason));
6194
+ return;
6195
+ }
6196
+ resolve(stdout);
6197
+ });
6198
+ child.stdin?.on("error", () => {});
6199
+ child.stdin?.end(input);
6200
+ });
6201
+ var nativeWorker = `
6202
+ try {
6203
+ const { operation, input } = JSON.parse(await Bun.stdin.text());
6204
+ const value = await Bun.secrets[operation](input);
6205
+ process.stdout.write(JSON.stringify({ value: value ?? null }));
6206
+ } catch (error) {
6207
+ const message = error instanceof Error ? error.message : "Credential backend failed";
6208
+ // libsecret's bus transport errors can omit the D-Bus context.
6209
+ const context = process.platform === "linux" && /^Could not connect:/i.test(message) ? "D-Bus: " : "";
6210
+ process.stderr.write(context + message);
6211
+ process.exitCode = 1;
6033
6212
  }
6034
- var useSecrets = async (product, operation) => {
6213
+ `;
6214
+ var nativeResponse = object({ value: union([string2(), boolean2(), _null3()]) }).strict();
6215
+ var createIsolatedBunSecrets = (product) => {
6216
+ const run = async (operation, input) => {
6217
+ assertBunRuntime();
6218
+ const output = await runBackend(product, process.execPath, ["--no-env-file", "--eval", nativeWorker], JSON.stringify({ operation, input }), operation === "set" ? "write-failed" : operation === "delete" ? "delete-failed" : "read-failed");
6219
+ try {
6220
+ return nativeResponse.parse(JSON.parse(output)).value;
6221
+ } catch {
6222
+ throw new CliCredentialStoreUnavailableError(product, "invalid-response");
6223
+ }
6224
+ };
6225
+ return {
6226
+ get: async (input) => {
6227
+ const value = await run("get", input);
6228
+ if (value === null || typeof value === "string")
6229
+ return value;
6230
+ throw new CliCredentialStoreUnavailableError(product, "invalid-response");
6231
+ },
6232
+ set: async (input) => {
6233
+ await run("set", input);
6234
+ },
6235
+ delete: async (input) => await run("delete", input) === true
6236
+ };
6237
+ };
6238
+
6239
+ // ../../cli-auth/src/client/credential-store.ts
6240
+ var runSecretOperation = async (product, operation, fallback = "read-failed") => {
6035
6241
  try {
6036
6242
  return await operation();
6037
6243
  } catch (cause) {
6038
6244
  if (cause instanceof CliCredentialStoreUnavailableError)
6039
6245
  throw cause;
6040
- throw new CliCredentialStoreUnavailableError(product, { cause });
6246
+ throw new CliCredentialStoreUnavailableError(product, classifyCredentialStoreFailure(cause instanceof Error ? cause.message : "", fallback));
6041
6247
  }
6042
6248
  };
6043
- var defaultSecrets = () => {
6044
- assertBunRuntime();
6045
- return Bun.secrets;
6046
- };
6047
6249
  var refreshLockTimeoutMs = 5 * 60 * 1000;
6048
6250
  var refreshLockRetryMs = 50;
6049
6251
  var getCredentialLockDirectoryPath = () => {
@@ -6115,18 +6317,38 @@ var acquireRefreshLock = async (path) => {
6115
6317
  }
6116
6318
  throw new Error("Timed out waiting for another CLI process to refresh credentials. Retry the command.");
6117
6319
  };
6320
+ var resolveCliCredentialAccount = (environment = process.env) => environment.GO_LABS_CLI_CREDENTIAL_ACCOUNT?.trim() || "default";
6118
6321
  var createBunCredentialStore = ({
6119
- account = "default",
6322
+ account = resolveCliCredentialAccount(),
6120
6323
  product,
6121
- secrets = defaultSecrets()
6324
+ secrets = createIsolatedBunSecrets(product)
6122
6325
  }) => {
6123
6326
  const service = `go-labs-cli:${product}`;
6124
6327
  return {
6328
+ preflight: async () => {
6329
+ const input = { name: `probe-${randomUUID()}`, service };
6330
+ const value = `go-labs-non-secret-probe:${randomUUID()}`;
6331
+ try {
6332
+ await runSecretOperation(product, () => secrets.set({ ...input, value }), "write-failed");
6333
+ if (await runSecretOperation(product, () => secrets.get(input)) !== value) {
6334
+ throw new CliCredentialStoreUnavailableError(product, "write-failed");
6335
+ }
6336
+ } catch (error) {
6337
+ await secrets.delete(input).catch(() => {
6338
+ return;
6339
+ });
6340
+ throw error;
6341
+ }
6342
+ await runSecretOperation(product, () => secrets.delete(input), "delete-failed");
6343
+ if (await runSecretOperation(product, () => secrets.get(input)) !== null) {
6344
+ throw new CliCredentialStoreUnavailableError(product, "delete-failed");
6345
+ }
6346
+ },
6125
6347
  delete: async () => {
6126
- await useSecrets(product, () => secrets.delete({ name: account, service }));
6348
+ await runSecretOperation(product, () => secrets.delete({ name: account, service }), "delete-failed");
6127
6349
  },
6128
6350
  get: async () => {
6129
- const value = await useSecrets(product, () => secrets.get({ name: account, service }));
6351
+ const value = await runSecretOperation(product, () => secrets.get({ name: account, service }));
6130
6352
  if (value === null)
6131
6353
  return null;
6132
6354
  try {
@@ -6137,11 +6359,11 @@ var createBunCredentialStore = ({
6137
6359
  },
6138
6360
  set: async (credentials) => {
6139
6361
  const parsed = storedCliCredentialsSchema.parse(credentials);
6140
- await useSecrets(product, () => secrets.set({
6362
+ await runSecretOperation(product, () => secrets.set({
6141
6363
  name: account,
6142
6364
  service,
6143
6365
  value: JSON.stringify(parsed)
6144
- }));
6366
+ }), "write-failed");
6145
6367
  },
6146
6368
  withCredentialLock: async (operation) => {
6147
6369
  const refreshLockPath = await getCredentialLockPath(product, account);
@@ -6162,6 +6384,7 @@ var createBunCredentialStore = ({
6162
6384
  };
6163
6385
  // ../../cli-auth/src/client/login.ts
6164
6386
  import { hostname } from "os";
6387
+ import { isDeepStrictEqual } from "util";
6165
6388
  var normalizeCliAuthIssuer = (value) => {
6166
6389
  const url = new URL(value);
6167
6390
  url.hash = "";
@@ -6224,12 +6447,15 @@ var openSystemBrowser = async (url) => {
6224
6447
  "-NoProfile",
6225
6448
  "-NonInteractive",
6226
6449
  "-Command",
6227
- "Start-Process -FilePath $args[0]",
6228
- url
6450
+ "Start-Process -FilePath $env:GO_LABS_CLI_VERIFICATION_URI -ErrorAction Stop"
6229
6451
  ] : ["xdg-open", url];
6230
6452
  try {
6231
- const process2 = Bun.spawn(command, { stderr: "ignore", stdout: "ignore" });
6232
- return await process2.exited === 0;
6453
+ const browserProcess = Bun.spawn(command, {
6454
+ env: process.platform === "win32" ? { ...process.env, GO_LABS_CLI_VERIFICATION_URI: url } : process.env,
6455
+ stderr: "ignore",
6456
+ stdout: "ignore"
6457
+ });
6458
+ return await browserProcess.exited === 0;
6233
6459
  } catch {
6234
6460
  return false;
6235
6461
  }
@@ -6247,6 +6473,8 @@ var loginWithBrowser = async ({
6247
6473
  store
6248
6474
  }) => {
6249
6475
  const baseUrl = normalizeCliAuthIssuer(apiBaseUrl);
6476
+ if (store.preflight)
6477
+ await withCredentialLock(store, () => store.preflight?.() ?? Promise.resolve());
6250
6478
  const pkce = createPkcePair();
6251
6479
  const beginResponse = await postJson(fetchImplementation, `${baseUrl}/api/cli-auth/device`, {
6252
6480
  clientName,
@@ -6274,7 +6502,19 @@ var loginWithBrowser = async ({
6274
6502
  if (response.ok) {
6275
6503
  const issued = tokenResponseSchema.parse(payload);
6276
6504
  const credentials = toStoredCredentials(issued, baseUrl, now());
6277
- await withCredentialLock(store, () => store.set(credentials));
6505
+ try {
6506
+ await withCredentialLock(store, () => persistIssuedCredentials({
6507
+ store,
6508
+ credentials,
6509
+ apiBaseUrl: baseUrl,
6510
+ fetch: fetchImplementation,
6511
+ phase: "login"
6512
+ }));
6513
+ } catch (error) {
6514
+ if (error instanceof CliCredentialPersistenceError)
6515
+ throw error;
6516
+ throw new CliCredentialPersistenceError(await revokeIssuedCredentials(fetchImplementation, baseUrl, credentials), false, "login");
6517
+ }
6278
6518
  return credentials;
6279
6519
  }
6280
6520
  const error = deviceTokenErrorSchema.safeParse(payload);
@@ -6294,6 +6534,55 @@ var loginWithBrowser = async ({
6294
6534
  }
6295
6535
  throw new Error("CLI authentication expired. Run auth login again.");
6296
6536
  };
6537
+ var revokeIssuedCredentials = async (fetchImplementation, apiBaseUrl, credentials) => {
6538
+ try {
6539
+ const response = await postJson(fetchImplementation, `${normalizeCliAuthIssuer(apiBaseUrl)}/api/cli-auth/revoke`, { refreshToken: credentials.refreshToken }, { signal: AbortSignal.timeout(refreshRequestTimeoutMs) });
6540
+ return response.ok;
6541
+ } catch {
6542
+ return false;
6543
+ }
6544
+ };
6545
+ var persistIssuedCredentials = async ({
6546
+ store,
6547
+ credentials,
6548
+ previous,
6549
+ apiBaseUrl,
6550
+ fetch: fetchImplementation,
6551
+ phase
6552
+ }) => {
6553
+ let prior = previous;
6554
+ try {
6555
+ if (phase === "login")
6556
+ prior = await store.get();
6557
+ await store.set(credentials);
6558
+ const saved = await store.get();
6559
+ if (!isDeepStrictEqual(saved, credentials))
6560
+ throw new Error("Credential write could not be verified");
6561
+ return;
6562
+ } catch {
6563
+ const remoteRevoked = await revokeIssuedCredentials(fetchImplementation, apiBaseUrl, credentials);
6564
+ let localRestored = false;
6565
+ try {
6566
+ const current = await store.get();
6567
+ if (phase === "login" && prior !== undefined) {
6568
+ if (isDeepStrictEqual(current, prior))
6569
+ localRestored = true;
6570
+ else if (current === null || current.refreshToken === credentials.refreshToken) {
6571
+ if (prior)
6572
+ await store.set(prior);
6573
+ else
6574
+ await store.delete();
6575
+ localRestored = isDeepStrictEqual(await store.get(), prior);
6576
+ }
6577
+ } else if (phase === "refresh" && (!current || current.refreshToken === credentials.refreshToken || current.refreshToken === prior?.refreshToken)) {
6578
+ if (current)
6579
+ await store.delete();
6580
+ localRestored = await store.get() === null;
6581
+ }
6582
+ } catch {}
6583
+ throw new CliCredentialPersistenceError(remoteRevoked, localRestored, phase);
6584
+ }
6585
+ };
6297
6586
  var refreshes = new WeakMap;
6298
6587
  var refreshRequestTimeoutMs = 30000;
6299
6588
  var isInvalidGrantResponse = (payload) => {
@@ -6348,7 +6637,14 @@ var refreshAccessToken = async ({
6348
6637
  throw new Error("Unable to refresh the CLI session because the API returned invalid credentials. Credentials were kept; retry the command.");
6349
6638
  }
6350
6639
  const stored = toStoredCredentials(issued, normalizeCliAuthIssuer(apiBaseUrl), now());
6351
- await store.set(stored);
6640
+ await persistIssuedCredentials({
6641
+ store,
6642
+ credentials: stored,
6643
+ previous: credentials,
6644
+ apiBaseUrl,
6645
+ fetch: fetchImplementation,
6646
+ phase: "refresh"
6647
+ });
6352
6648
  return stored.accessToken;
6353
6649
  };
6354
6650
  var refreshWithCurrentCredentials = async ({
@@ -6631,6 +6927,9 @@ var sanitizeValue = (value, seen = new WeakSet) => {
6631
6927
  ]));
6632
6928
  };
6633
6929
  var normalizeError = (error) => {
6930
+ const storageError = getCliStorageError(error);
6931
+ if (storageError)
6932
+ return storageError;
6634
6933
  if (error instanceof CliError) {
6635
6934
  return {
6636
6935
  code: error.code,
@@ -8028,7 +8327,10 @@ var runGoshCli = async (argv, dependencies) => {
8028
8327
  command: "commands"
8029
8328
  }, commandManifest);
8030
8329
  }
8031
- const credentialStore = dependencies.credentialStore ?? createBunCredentialStore({ product: "gosh" });
8330
+ const credentialStore = dependencies.credentialStore ?? createBunCredentialStore({
8331
+ account: resolveCliCredentialAccount(dependencies.runtime.env),
8332
+ product: "gosh"
8333
+ });
8032
8334
  if (first === "auth" && rest[0] === "login" && rest.length === 1) {
8033
8335
  const credentials = await loginWithBrowser({
8034
8336
  apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/gosh",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "GOSH CLI for organizers and AI agents — manage organizations, events, teams, stations, missions, templates, and live operations through the GOSH API.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",