@go-labs-sg/event-tech 0.9.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 +25 -1
  2. package/dist/index.js +326 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  `@go-labs-sg/event-tech` provides the `event-tech` and `et` commands for working with Event Tech through its permission-aware API.
4
4
 
5
+ ## First step: update before use
6
+
7
+ Before starting a task, **update to the latest published release first**, including when an AI agent runs this CLI:
8
+
9
+ ```bash
10
+ bun add --global @go-labs-sg/event-tech@latest
11
+ et version
12
+ et --help
13
+ ```
14
+
15
+ Run this sequence once per task, not before every command. Re-read the updated help before choosing commands or flags. If updating fails, stop and report the failure rather than silently using an outdated CLI. If the user explicitly requested a pinned version or a development/staging build, preserve that version instead of replacing it with the public release.
16
+
17
+ New installation? Follow the runtime and installation instructions below to install Bun first, then return to this step.
18
+
5
19
  ## Runtime and installation
6
20
 
7
21
  The CLI requires [Bun](https://bun.com/docs/installation) 1.4.0 or newer. npm distributes the package, while the installed command runs under Bun.
@@ -41,6 +55,16 @@ bun add --global @go-labs-sg/event-tech
41
55
  et version
42
56
  ```
43
57
 
58
+ ## Secure credential storage
59
+
60
+ `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.
61
+
62
+ 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.
63
+
64
+ 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.
65
+
66
+ See the [shared authentication contract](../../cli-auth/README.md) for recovery and verification limits.
67
+
44
68
  ## Authentication
45
69
 
46
70
  ```sh
@@ -51,7 +75,7 @@ et auth whoami
51
75
 
52
76
  `et auth login` opens Event Tech in the system browser. Use the normal Event Tech Google-backed web login if needed, verify the displayed code and device, then click **Authorize CLI**. An existing browser session skips another Google login but never skips explicit CLI approval.
53
77
 
54
- The terminal polls automatically and stores only Event Tech-issued credentials through `Bun.secrets`. It does not store Google credentials and has no API-key or environment-variable authentication fallback. Use `et auth logout` to revoke the remote session and remove its local credential. For SSH and containers, pass `--no-browser` and open the printed URL elsewhere.
78
+ The terminal polls automatically and stores only Event Tech-issued credentials through `Bun.secrets`. It does not store Google credentials and has no raw API-key or token-environment authentication fallback. Use `et auth logout` to revoke the remote session and remove its local credential. For SSH and containers, pass `--no-browser` and open the printed URL elsewhere.
55
79
 
56
80
  Use `EVENT_TECH_API_URL` or `--api-url` to target another HTTPS deployment. Plain HTTP is allowed only for localhost development.
57
81
 
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
  // ../../cli-auth/src/client/credential-store.ts
474
476
  import { createHash as createHash2, randomUUID } from "crypto";
@@ -1483,6 +1485,8 @@ function datetime(args) {
1483
1485
  }
1484
1486
  var anyString = /^[\s\S]{0,}$/;
1485
1487
  var number = /^-?\d+(?:\.\d+)?$/;
1488
+ var boolean = /^(?:true|false)$/i;
1489
+ var _null = /^null$/i;
1486
1490
  var lowercase = /^[^A-Z]*$/;
1487
1491
  var uppercase = /^[^a-z]*$/;
1488
1492
 
@@ -2364,6 +2368,43 @@ var $ZodNumberFormat = /* @__PURE__ */ $constructor("$ZodNumberFormat", (inst, d
2364
2368
  $ZodCheckNumberFormat.init(inst, def);
2365
2369
  $ZodNumber.init(inst, def);
2366
2370
  });
2371
+ var $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
2372
+ $ZodType.init(inst, def);
2373
+ inst._zod.pattern = boolean;
2374
+ inst._zod.parse = (payload, _ctx) => {
2375
+ if (def.coerce)
2376
+ try {
2377
+ payload.value = Boolean(payload.value);
2378
+ } catch (_) {}
2379
+ const input = payload.value;
2380
+ if (typeof input === "boolean")
2381
+ return payload;
2382
+ payload.issues.push({
2383
+ expected: "boolean",
2384
+ code: "invalid_type",
2385
+ input,
2386
+ inst
2387
+ });
2388
+ return payload;
2389
+ };
2390
+ });
2391
+ var $ZodNull = /* @__PURE__ */ $constructor("$ZodNull", (inst, def) => {
2392
+ $ZodType.init(inst, def);
2393
+ inst._zod.pattern = _null;
2394
+ inst._zod.values = new Set([null]);
2395
+ inst._zod.parse = (payload, _ctx) => {
2396
+ const input = payload.value;
2397
+ if (input === null)
2398
+ return payload;
2399
+ payload.issues.push({
2400
+ expected: "null",
2401
+ code: "invalid_type",
2402
+ input,
2403
+ inst
2404
+ });
2405
+ return payload;
2406
+ };
2407
+ });
2367
2408
  var $ZodUnknown = /* @__PURE__ */ $constructor("$ZodUnknown", (inst, def) => {
2368
2409
  $ZodType.init(inst, def);
2369
2410
  inst._zod.parse = (payload) => payload;
@@ -3901,6 +3942,18 @@ function _int(Class, params) {
3901
3942
  ...normalizeParams(params)
3902
3943
  });
3903
3944
  }
3945
+ function _boolean(Class, params) {
3946
+ return new Class({
3947
+ type: "boolean",
3948
+ ...normalizeParams(params)
3949
+ });
3950
+ }
3951
+ function _null2(Class, params) {
3952
+ return new Class({
3953
+ type: "null",
3954
+ ...normalizeParams(params)
3955
+ });
3956
+ }
3904
3957
  function _unknown(Class) {
3905
3958
  return new Class({
3906
3959
  type: "unknown"
@@ -4783,6 +4836,18 @@ var numberProcessor = (schema, ctx, _json, params) => {
4783
4836
  json.allOf = [...json.allOf ?? [], ...rest.map((m) => ({ multipleOf: m }))];
4784
4837
  }
4785
4838
  };
4839
+ var booleanProcessor = (_schema, _ctx, json, _params) => {
4840
+ json.type = "boolean";
4841
+ };
4842
+ var nullProcessor = (_schema, ctx, json, _params) => {
4843
+ if (ctx.target === "openapi-3.0") {
4844
+ json.type = "string";
4845
+ json.nullable = true;
4846
+ json.enum = [null];
4847
+ } else {
4848
+ json.type = "null";
4849
+ }
4850
+ };
4786
4851
  var neverProcessor = (_schema, _ctx, json, _params) => {
4787
4852
  json.not = {};
4788
4853
  };
@@ -5577,6 +5642,22 @@ var ZodNumberFormat = /* @__PURE__ */ $constructor("ZodNumberFormat", (inst, def
5577
5642
  function int(params) {
5578
5643
  return _int(ZodNumberFormat, params);
5579
5644
  }
5645
+ var ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
5646
+ $ZodBoolean.init(inst, def);
5647
+ ZodType.init(inst, def);
5648
+ inst._zod.processJSONSchema = (ctx, json, params) => booleanProcessor(inst, ctx, json, params);
5649
+ });
5650
+ function boolean2(params) {
5651
+ return _boolean(ZodBoolean, params);
5652
+ }
5653
+ var ZodNull = /* @__PURE__ */ $constructor("ZodNull", (inst, def) => {
5654
+ $ZodNull.init(inst, def);
5655
+ ZodType.init(inst, def);
5656
+ inst._zod.processJSONSchema = (ctx, json, params) => nullProcessor(inst, ctx, json, params);
5657
+ });
5658
+ function _null3(params) {
5659
+ return _null2(ZodNull, params);
5660
+ }
5580
5661
  var ZodUnknown = /* @__PURE__ */ $constructor("ZodUnknown", (inst, def) => {
5581
5662
  $ZodUnknown.init(inst, def);
5582
5663
  ZodType.init(inst, def);
@@ -5995,6 +6076,9 @@ var storedCliCredentialsSchema = object({
5995
6076
  session: cliSessionIdentitySchema
5996
6077
  }).strict();
5997
6078
  var cliProductSchema = _enum(cliProducts);
6079
+ // ../../cli-auth/src/client/secret-backend.ts
6080
+ import { execFile } from "child_process";
6081
+
5998
6082
  // ../../cli-auth/src/client/runtime.ts
5999
6083
  var MINIMUM_BUN_VERSION = [1, 4, 0];
6000
6084
  var parseBunVersion = (version) => {
@@ -6020,27 +6104,145 @@ var assertBunRuntime = (runtime = globalThis.Bun) => {
6020
6104
  }
6021
6105
  };
6022
6106
 
6023
- // ../../cli-auth/src/client/credential-store.ts
6107
+ // ../../cli-auth/src/client/storage-error.ts
6108
+ var guidance = {
6109
+ "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.",
6110
+ "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.",
6111
+ "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.",
6112
+ "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.",
6113
+ locked: "Unlock your own native credential collection/session before retrying. The CLI will not wait indefinitely for an unlock prompt.",
6114
+ 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.",
6115
+ "invalid-response": "The native credential backend returned an invalid response. Check your own keyring/session and configuration; no credential output was disclosed.",
6116
+ "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.",
6117
+ "write-failed": "The native credential backend could not persist credentials. Check your own keyring/session write permissions and availability; no plaintext fallback is used.",
6118
+ "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."
6119
+ };
6120
+
6024
6121
  class CliCredentialStoreUnavailableError extends Error {
6025
6122
  code = "CLI_CREDENTIAL_STORE_UNAVAILABLE";
6026
- constructor(product, options) {
6027
- 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);
6123
+ reason;
6124
+ constructor(product, reason = "read-failed") {
6125
+ super(`Secure credential storage is unavailable for the ${product} CLI [CLI_CREDENTIAL_STORE_UNAVAILABLE:${reason}]. ${guidance[reason]}`);
6028
6126
  this.name = "CliCredentialStoreUnavailableError";
6029
- }
6127
+ this.reason = reason;
6128
+ }
6129
+ }
6130
+ var classifyCredentialStoreFailure = (message, fallback) => {
6131
+ if (/libsecret.*(not found|not available|cannot open|load)|dlopen|shared (object|library).*not found/i.test(message))
6132
+ return "missing-library";
6133
+ if (/(d-?bus|user bus|session bus).*(denied|not permitted)|(?:denied|not permitted).*(d-?bus|user bus|session bus)/i.test(message))
6134
+ return "bus-denied";
6135
+ if (/locked|interaction.*not allowed|user.*cancel/i.test(message))
6136
+ return "locked";
6137
+ if (/ServiceUnknown|org\.freedesktop\.secrets.*(not provided|no owner|not available)|secret.?service.*(unavailable|not running)/i.test(message))
6138
+ return "service-unavailable";
6139
+ if (/d-?bus|user bus|session bus|autolaunch/i.test(message))
6140
+ return "bus-unavailable";
6141
+ return fallback;
6142
+ };
6143
+
6144
+ class CliCredentialPersistenceError extends Error {
6145
+ remoteRevoked;
6146
+ localRestored;
6147
+ phase;
6148
+ code = "CLI_CREDENTIAL_PERSISTENCE_FAILED";
6149
+ constructor(remoteRevoked, localRestored, phase) {
6150
+ 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."}`);
6151
+ this.remoteRevoked = remoteRevoked;
6152
+ this.localRestored = localRestored;
6153
+ this.phase = phase;
6154
+ this.name = "CliCredentialPersistenceError";
6155
+ }
6156
+ }
6157
+ var getCliStorageError = (error) => {
6158
+ if (error instanceof CliCredentialStoreUnavailableError)
6159
+ return {
6160
+ code: error.code,
6161
+ message: error.message,
6162
+ details: { reason: error.reason }
6163
+ };
6164
+ if (error instanceof CliCredentialPersistenceError)
6165
+ return {
6166
+ code: error.code,
6167
+ message: error.message,
6168
+ details: {
6169
+ remoteRevoked: error.remoteRevoked,
6170
+ localRestored: error.localRestored,
6171
+ phase: error.phase
6172
+ }
6173
+ };
6174
+ return;
6175
+ };
6176
+
6177
+ // ../../cli-auth/src/client/secret-backend.ts
6178
+ var timeoutMs = 1e4;
6179
+ var maxBuffer = 64 * 1024;
6180
+ var runBackend = (product, executable, args, input, failure) => new Promise((resolve, reject) => {
6181
+ const child = execFile(executable, args, {
6182
+ timeout: timeoutMs,
6183
+ killSignal: "SIGKILL",
6184
+ maxBuffer,
6185
+ windowsHide: true
6186
+ }, (error, stdout, stderr) => {
6187
+ if (error) {
6188
+ const reason = error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" ? "invalid-response" : error.killed ? "timeout" : classifyCredentialStoreFailure(`${stdout}
6189
+ ${stderr}`, failure);
6190
+ reject(new CliCredentialStoreUnavailableError(product, reason));
6191
+ return;
6192
+ }
6193
+ resolve(stdout);
6194
+ });
6195
+ child.stdin?.on("error", () => {});
6196
+ child.stdin?.end(input);
6197
+ });
6198
+ var nativeWorker = `
6199
+ try {
6200
+ const { operation, input } = JSON.parse(await Bun.stdin.text());
6201
+ const value = await Bun.secrets[operation](input);
6202
+ process.stdout.write(JSON.stringify({ value: value ?? null }));
6203
+ } catch (error) {
6204
+ const message = error instanceof Error ? error.message : "Credential backend failed";
6205
+ // libsecret's bus transport errors can omit the D-Bus context.
6206
+ const context = process.platform === "linux" && /^Could not connect:/i.test(message) ? "D-Bus: " : "";
6207
+ process.stderr.write(context + message);
6208
+ process.exitCode = 1;
6030
6209
  }
6031
- var useSecrets = async (product, operation) => {
6210
+ `;
6211
+ var nativeResponse = object({ value: union([string2(), boolean2(), _null3()]) }).strict();
6212
+ var createIsolatedBunSecrets = (product) => {
6213
+ const run = async (operation, input) => {
6214
+ assertBunRuntime();
6215
+ 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");
6216
+ try {
6217
+ return nativeResponse.parse(JSON.parse(output)).value;
6218
+ } catch {
6219
+ throw new CliCredentialStoreUnavailableError(product, "invalid-response");
6220
+ }
6221
+ };
6222
+ return {
6223
+ get: async (input) => {
6224
+ const value = await run("get", input);
6225
+ if (value === null || typeof value === "string")
6226
+ return value;
6227
+ throw new CliCredentialStoreUnavailableError(product, "invalid-response");
6228
+ },
6229
+ set: async (input) => {
6230
+ await run("set", input);
6231
+ },
6232
+ delete: async (input) => await run("delete", input) === true
6233
+ };
6234
+ };
6235
+
6236
+ // ../../cli-auth/src/client/credential-store.ts
6237
+ var runSecretOperation = async (product, operation, fallback = "read-failed") => {
6032
6238
  try {
6033
6239
  return await operation();
6034
6240
  } catch (cause) {
6035
6241
  if (cause instanceof CliCredentialStoreUnavailableError)
6036
6242
  throw cause;
6037
- throw new CliCredentialStoreUnavailableError(product, { cause });
6243
+ throw new CliCredentialStoreUnavailableError(product, classifyCredentialStoreFailure(cause instanceof Error ? cause.message : "", fallback));
6038
6244
  }
6039
6245
  };
6040
- var defaultSecrets = () => {
6041
- assertBunRuntime();
6042
- return Bun.secrets;
6043
- };
6044
6246
  var refreshLockTimeoutMs = 5 * 60 * 1000;
6045
6247
  var refreshLockRetryMs = 50;
6046
6248
  var getCredentialLockDirectoryPath = () => {
@@ -6112,18 +6314,38 @@ var acquireRefreshLock = async (path) => {
6112
6314
  }
6113
6315
  throw new Error("Timed out waiting for another CLI process to refresh credentials. Retry the command.");
6114
6316
  };
6317
+ var resolveCliCredentialAccount = (environment = process.env) => environment.GO_LABS_CLI_CREDENTIAL_ACCOUNT?.trim() || "default";
6115
6318
  var createBunCredentialStore = ({
6116
- account = "default",
6319
+ account = resolveCliCredentialAccount(),
6117
6320
  product,
6118
- secrets = defaultSecrets()
6321
+ secrets = createIsolatedBunSecrets(product)
6119
6322
  }) => {
6120
6323
  const service = `go-labs-cli:${product}`;
6121
6324
  return {
6325
+ preflight: async () => {
6326
+ const input = { name: `probe-${randomUUID()}`, service };
6327
+ const value = `go-labs-non-secret-probe:${randomUUID()}`;
6328
+ try {
6329
+ await runSecretOperation(product, () => secrets.set({ ...input, value }), "write-failed");
6330
+ if (await runSecretOperation(product, () => secrets.get(input)) !== value) {
6331
+ throw new CliCredentialStoreUnavailableError(product, "write-failed");
6332
+ }
6333
+ } catch (error) {
6334
+ await secrets.delete(input).catch(() => {
6335
+ return;
6336
+ });
6337
+ throw error;
6338
+ }
6339
+ await runSecretOperation(product, () => secrets.delete(input), "delete-failed");
6340
+ if (await runSecretOperation(product, () => secrets.get(input)) !== null) {
6341
+ throw new CliCredentialStoreUnavailableError(product, "delete-failed");
6342
+ }
6343
+ },
6122
6344
  delete: async () => {
6123
- await useSecrets(product, () => secrets.delete({ name: account, service }));
6345
+ await runSecretOperation(product, () => secrets.delete({ name: account, service }), "delete-failed");
6124
6346
  },
6125
6347
  get: async () => {
6126
- const value = await useSecrets(product, () => secrets.get({ name: account, service }));
6348
+ const value = await runSecretOperation(product, () => secrets.get({ name: account, service }));
6127
6349
  if (value === null)
6128
6350
  return null;
6129
6351
  try {
@@ -6134,11 +6356,11 @@ var createBunCredentialStore = ({
6134
6356
  },
6135
6357
  set: async (credentials) => {
6136
6358
  const parsed = storedCliCredentialsSchema.parse(credentials);
6137
- await useSecrets(product, () => secrets.set({
6359
+ await runSecretOperation(product, () => secrets.set({
6138
6360
  name: account,
6139
6361
  service,
6140
6362
  value: JSON.stringify(parsed)
6141
- }));
6363
+ }), "write-failed");
6142
6364
  },
6143
6365
  withCredentialLock: async (operation) => {
6144
6366
  const refreshLockPath = await getCredentialLockPath(product, account);
@@ -6159,6 +6381,7 @@ var createBunCredentialStore = ({
6159
6381
  };
6160
6382
  // ../../cli-auth/src/client/login.ts
6161
6383
  import { hostname } from "os";
6384
+ import { isDeepStrictEqual } from "util";
6162
6385
  var normalizeCliAuthIssuer = (value) => {
6163
6386
  const url = new URL(value);
6164
6387
  url.hash = "";
@@ -6221,12 +6444,15 @@ var openSystemBrowser = async (url) => {
6221
6444
  "-NoProfile",
6222
6445
  "-NonInteractive",
6223
6446
  "-Command",
6224
- "Start-Process -FilePath $args[0]",
6225
- url
6447
+ "Start-Process -FilePath $env:GO_LABS_CLI_VERIFICATION_URI -ErrorAction Stop"
6226
6448
  ] : ["xdg-open", url];
6227
6449
  try {
6228
- const process2 = Bun.spawn(command, { stderr: "ignore", stdout: "ignore" });
6229
- return await process2.exited === 0;
6450
+ const browserProcess = Bun.spawn(command, {
6451
+ env: process.platform === "win32" ? { ...process.env, GO_LABS_CLI_VERIFICATION_URI: url } : process.env,
6452
+ stderr: "ignore",
6453
+ stdout: "ignore"
6454
+ });
6455
+ return await browserProcess.exited === 0;
6230
6456
  } catch {
6231
6457
  return false;
6232
6458
  }
@@ -6244,6 +6470,8 @@ var loginWithBrowser = async ({
6244
6470
  store
6245
6471
  }) => {
6246
6472
  const baseUrl = normalizeCliAuthIssuer(apiBaseUrl);
6473
+ if (store.preflight)
6474
+ await withCredentialLock(store, () => store.preflight?.() ?? Promise.resolve());
6247
6475
  const pkce = createPkcePair();
6248
6476
  const beginResponse = await postJson(fetchImplementation, `${baseUrl}/api/cli-auth/device`, {
6249
6477
  clientName,
@@ -6271,7 +6499,19 @@ var loginWithBrowser = async ({
6271
6499
  if (response.ok) {
6272
6500
  const issued = tokenResponseSchema.parse(payload);
6273
6501
  const credentials = toStoredCredentials(issued, baseUrl, now());
6274
- await withCredentialLock(store, () => store.set(credentials));
6502
+ try {
6503
+ await withCredentialLock(store, () => persistIssuedCredentials({
6504
+ store,
6505
+ credentials,
6506
+ apiBaseUrl: baseUrl,
6507
+ fetch: fetchImplementation,
6508
+ phase: "login"
6509
+ }));
6510
+ } catch (error) {
6511
+ if (error instanceof CliCredentialPersistenceError)
6512
+ throw error;
6513
+ throw new CliCredentialPersistenceError(await revokeIssuedCredentials(fetchImplementation, baseUrl, credentials), false, "login");
6514
+ }
6275
6515
  return credentials;
6276
6516
  }
6277
6517
  const error = deviceTokenErrorSchema.safeParse(payload);
@@ -6291,6 +6531,55 @@ var loginWithBrowser = async ({
6291
6531
  }
6292
6532
  throw new Error("CLI authentication expired. Run auth login again.");
6293
6533
  };
6534
+ var revokeIssuedCredentials = async (fetchImplementation, apiBaseUrl, credentials) => {
6535
+ try {
6536
+ const response = await postJson(fetchImplementation, `${normalizeCliAuthIssuer(apiBaseUrl)}/api/cli-auth/revoke`, { refreshToken: credentials.refreshToken }, { signal: AbortSignal.timeout(refreshRequestTimeoutMs) });
6537
+ return response.ok;
6538
+ } catch {
6539
+ return false;
6540
+ }
6541
+ };
6542
+ var persistIssuedCredentials = async ({
6543
+ store,
6544
+ credentials,
6545
+ previous,
6546
+ apiBaseUrl,
6547
+ fetch: fetchImplementation,
6548
+ phase
6549
+ }) => {
6550
+ let prior = previous;
6551
+ try {
6552
+ if (phase === "login")
6553
+ prior = await store.get();
6554
+ await store.set(credentials);
6555
+ const saved = await store.get();
6556
+ if (!isDeepStrictEqual(saved, credentials))
6557
+ throw new Error("Credential write could not be verified");
6558
+ return;
6559
+ } catch {
6560
+ const remoteRevoked = await revokeIssuedCredentials(fetchImplementation, apiBaseUrl, credentials);
6561
+ let localRestored = false;
6562
+ try {
6563
+ const current = await store.get();
6564
+ if (phase === "login" && prior !== undefined) {
6565
+ if (isDeepStrictEqual(current, prior))
6566
+ localRestored = true;
6567
+ else if (current === null || current.refreshToken === credentials.refreshToken) {
6568
+ if (prior)
6569
+ await store.set(prior);
6570
+ else
6571
+ await store.delete();
6572
+ localRestored = isDeepStrictEqual(await store.get(), prior);
6573
+ }
6574
+ } else if (phase === "refresh" && (!current || current.refreshToken === credentials.refreshToken || current.refreshToken === prior?.refreshToken)) {
6575
+ if (current)
6576
+ await store.delete();
6577
+ localRestored = await store.get() === null;
6578
+ }
6579
+ } catch {}
6580
+ throw new CliCredentialPersistenceError(remoteRevoked, localRestored, phase);
6581
+ }
6582
+ };
6294
6583
  var refreshes = new WeakMap;
6295
6584
  var refreshRequestTimeoutMs = 30000;
6296
6585
  var isInvalidGrantResponse = (payload) => {
@@ -6345,7 +6634,14 @@ var refreshAccessToken = async ({
6345
6634
  throw new Error("Unable to refresh the CLI session because the API returned invalid credentials. Credentials were kept; retry the command.");
6346
6635
  }
6347
6636
  const stored = toStoredCredentials(issued, normalizeCliAuthIssuer(apiBaseUrl), now());
6348
- await store.set(stored);
6637
+ await persistIssuedCredentials({
6638
+ store,
6639
+ credentials: stored,
6640
+ previous: credentials,
6641
+ apiBaseUrl,
6642
+ fetch: fetchImplementation,
6643
+ phase: "refresh"
6644
+ });
6349
6645
  return stored.accessToken;
6350
6646
  };
6351
6647
  var refreshWithCurrentCredentials = async ({
@@ -6509,6 +6805,8 @@ var api2 = new Proxy({}, {
6509
6805
  // src/commands/index.ts
6510
6806
  var usage = `Event Tech CLI
6511
6807
 
6808
+ First step before each task: update to the latest published release with 'bun add --global @go-labs-sg/event-tech@latest', then run 'et version' and 'et --help' before using commands. Update once per task. If updating fails, stop and report it. Preserve an explicitly requested pinned or development/staging version.
6809
+
6512
6810
  Usage:
6513
6811
  et auth login [--no-browser]
6514
6812
  et auth status
@@ -6582,6 +6880,11 @@ var errorCode = (error) => {
6582
6880
  return "INTERNAL_ERROR";
6583
6881
  };
6584
6882
  var writeError = (runtime, error) => {
6883
+ const storageError = getCliStorageError(error);
6884
+ if (storageError) {
6885
+ runtime.stderr.write(serialize({ ok: false, error: storageError }));
6886
+ return;
6887
+ }
6585
6888
  const message = error instanceof Error ? sanitizeErrorMessage(error.message) : "Unknown error.";
6586
6889
  runtime.stderr.write(serialize({ ok: false, error: { code: errorCode(error), message } }));
6587
6890
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/event-tech",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Event Tech CLI for managing Event Tech resources through the Go Labs API.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",