@alter-ai/cli 0.3.2 → 0.3.3

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 (2) hide show
  1. package/dist/cli.js +532 -130
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -173,7 +173,7 @@ async function maybePrintUpdateBanner(currentVersion, argv2 = process.argv.slice
173
173
  // package.json
174
174
  var package_default = {
175
175
  name: "@alter-ai/cli",
176
- version: "0.3.2",
176
+ version: "0.3.3",
177
177
  description: "Command-line interface for the Alter Vault dev portal \u2014 scripted dashboard automation.",
178
178
  type: "module",
179
179
  bin: {
@@ -216,7 +216,7 @@ var package_default = {
216
216
  tsup: "^8.0.0",
217
217
  tsx: "^4.7.0",
218
218
  typescript: "^5.5.0",
219
- vitest: "^3.0.0"
219
+ vitest: "^4.1.8"
220
220
  },
221
221
  engines: {
222
222
  node: ">=20.10.0"
@@ -224,7 +224,7 @@ var package_default = {
224
224
  };
225
225
 
226
226
  // src/program.ts
227
- import { Command as Command14 } from "commander";
227
+ import { Command as Command15 } from "commander";
228
228
 
229
229
  // src/commands/agents.ts
230
230
  import { Command } from "commander";
@@ -1019,6 +1019,45 @@ var ConnectSession = class {
1019
1019
  return `ConnectSession(url=${this.connectUrl}, expires_in=${this.expiresIn})`;
1020
1020
  }
1021
1021
  };
1022
+ var AuthSession = class {
1023
+ sessionToken;
1024
+ authUrl;
1025
+ expiresIn;
1026
+ expiresAt;
1027
+ constructor(data) {
1028
+ _assertString(data.session_token, "session_token", "AuthSession");
1029
+ _assertString(data.auth_url, "auth_url", "AuthSession");
1030
+ _assertString(data.expires_at, "expires_at", "AuthSession");
1031
+ if (typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in)) {
1032
+ throw new BackendError("AuthSession: 'expires_in' must be a finite number", {
1033
+ field: "expires_in"
1034
+ });
1035
+ }
1036
+ this.sessionToken = data.session_token;
1037
+ this.authUrl = data.auth_url;
1038
+ this.expiresIn = data.expires_in;
1039
+ this.expiresAt = data.expires_at;
1040
+ Object.freeze(this);
1041
+ }
1042
+ toJSON() {
1043
+ return {
1044
+ session_token: this.sessionToken,
1045
+ auth_url: this.authUrl,
1046
+ expires_in: this.expiresIn,
1047
+ expires_at: this.expiresAt
1048
+ };
1049
+ }
1050
+ toString() {
1051
+ return `AuthSession(authUrl=<redacted>, expiresIn=${this.expiresIn}, expiresAt=${JSON.stringify(this.expiresAt)})`;
1052
+ }
1053
+ /**
1054
+ * Custom Node.js inspect output — redacts authUrl/sessionToken so
1055
+ * console.log / util.inspect can't leak them (mirrors TokenResponse).
1056
+ */
1057
+ [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
1058
+ return this.toString();
1059
+ }
1060
+ };
1022
1061
  var ManagedSecretConnectSession = class {
1023
1062
  sessionToken;
1024
1063
  connectUrl;
@@ -1318,7 +1357,8 @@ var AuthResult = class {
1318
1357
  userInfo;
1319
1358
  constructor(data) {
1320
1359
  this.userToken = data.user_token;
1321
- this.userInfo = data.user_info ?? {};
1360
+ const ui = data.user_info;
1361
+ this.userInfo = typeof ui === "object" && ui !== null && !Array.isArray(ui) ? ui : {};
1322
1362
  Object.freeze(this);
1323
1363
  }
1324
1364
  toJSON() {
@@ -1331,6 +1371,17 @@ var AuthResult = class {
1331
1371
  const sub = this.userInfo?.sub ?? "unknown";
1332
1372
  return `AuthResult(sub=${sub})`;
1333
1373
  }
1374
+ /**
1375
+ * Custom Node.js inspect output — redacts the live IDP JWT `userToken` so
1376
+ * console.log / util.inspect can't leak it (mirrors TokenResponse and the
1377
+ * sibling AuthSession). Without this, util.inspect enumerates own fields and
1378
+ * prints the full bearer token. The Python sibling's `__repr__`/`__str__`
1379
+ * redacts the same field — keep parity, and uphold "NEVER expose tokens in
1380
+ * logs".
1381
+ */
1382
+ [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
1383
+ return this.toString();
1384
+ }
1334
1385
  };
1335
1386
  var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
1336
1387
  "authorization",
@@ -2940,8 +2991,11 @@ function _extractAdditionalCredentials(token) {
2940
2991
  return _additionalCredsStore.get(token);
2941
2992
  }
2942
2993
  var _fetch;
2943
- var SDK_VERSION = "0.15.0";
2994
+ var SDK_VERSION = "0.16.0";
2944
2995
  var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
2996
+ var AUTH_POLL_SERVER_WAIT_MS = 25e3;
2997
+ var AUTH_POLL_HTTP_BUFFER_MS = 15e3;
2998
+ var PERMANENT_POLL_STATUSES = /* @__PURE__ */ new Set([400, 401, 403, 404, 422]);
2945
2999
  var HTTP_FORBIDDEN = 403;
2946
3000
  var HTTP_NO_CONTENT2 = 204;
2947
3001
  var HTTP_NOT_FOUND = 404;
@@ -3068,10 +3122,11 @@ var HttpClient = class {
3068
3122
  ...this.#defaultHeaders,
3069
3123
  ...options?.headers
3070
3124
  };
3125
+ const effectiveTimeoutMs = options?.timeoutMs ?? this.#timeoutMs;
3071
3126
  const controller = new AbortController();
3072
3127
  const timeoutId = setTimeout(() => {
3073
3128
  controller.abort(new DOMException("The operation timed out.", "TimeoutError"));
3074
- }, this.#timeoutMs);
3129
+ }, effectiveTimeoutMs);
3075
3130
  const init = {
3076
3131
  method,
3077
3132
  headers: mergedHeaders,
@@ -5194,33 +5249,31 @@ ${effectiveConstraints}`;
5194
5249
  });
5195
5250
  }
5196
5251
  /**
5197
- * Trigger IDP login for end user via browser.
5252
+ * Mint an end-user IDP sign-in session (link-based, headless).
5198
5253
  *
5199
- * Opens the app's configured IDP login page in the user's default browser.
5200
- * Polls for completion and returns the user's IDP JWT token.
5254
+ * The split, pure counterpart to {@link authenticate}. Returns an
5255
+ * {@link AuthSession} carrying the IDP `authUrl` to hand to the end user
5256
+ * and a `sessionToken` to poll with {@link pollAuthSession}. Unlike
5257
+ * `authenticate`, this method:
5201
5258
  *
5202
- * After `authenticate()` completes, the returned `userToken` is installed
5203
- * as the active `userTokenGetter` on this instance and automatically used
5204
- * for subsequent `request()` calls that use identity resolution.
5259
+ * - does **not** open a browser (hand `authUrl` to a remote user over any
5260
+ * channel a Slack DM, an MCP client, a printed link), and
5261
+ * - does **not** install a `userTokenGetter` on the instance, so it is
5262
+ * safe on a shared SDK instance that resolves a JWT per request.
5205
5263
  *
5206
- * .. warning::
5264
+ * `sessionToken` is a persistable handle: a headless consumer can seal it,
5265
+ * poll in a background loop, and resume a pending session after a restart.
5207
5266
  *
5208
- * **`authenticate()` overwrites any constructor-supplied `userTokenGetter`.**
5209
- * If you pass `userTokenGetter` to the `_VaultClient` constructor and then
5210
- * call `authenticate()`, the constructor getter is silently replaced for
5211
- * the lifetime of the instance — a fresh `_VaultClient` instance is the
5212
- * only way to restore the original getter. This is intentional for
5213
- * interactive CLI flows but can surprise web backends that mix both
5214
- * patterns. Prefer one or the other per `_VaultClient` instance.
5267
+ * Scope: requires an app key with the `idp_users:write` scope. Sign-in is
5268
+ * app-initiated (agents do not start user logins).
5215
5269
  *
5216
- * @param options - Optional configuration (timeout in milliseconds, default 300000 = 5 min)
5217
- * @returns AuthResult with userToken and userInfo
5218
- * @throws ConnectTimeoutError if authentication times out
5219
- * @throws AlterSDKError if session creation or authentication fails
5270
+ * @returns AuthSession with `authUrl`, `sessionToken`, `expiresIn`, `expiresAt`.
5271
+ * @throws AlterSDKError if the SDK is closed or the backend rejects the request.
5272
+ * @throws NetworkError if the backend is unreachable.
5273
+ * @throws TimeoutError if the request to the backend times out.
5220
5274
  */
5221
- async authenticate(options) {
5275
+ async createAuthSession() {
5222
5276
  this.#assertNotClosed();
5223
- const timeoutMs = options?.timeout ?? 3e5;
5224
5277
  const actorHeaders = this.#getActorRequestHeaders();
5225
5278
  const sessionPath = "/sdk/auth/session";
5226
5279
  const sessionBodyStr = JSON.stringify({});
@@ -5250,64 +5303,199 @@ ${effectiveConstraints}`;
5250
5303
  }
5251
5304
  this.#cacheActorIdFromResponse(sessionResp);
5252
5305
  await this.#handleErrorResponse(sessionResp);
5253
- const sessionData = await sessionResp.json();
5254
- await __VaultClient.#openBrowserOrPrint(
5255
- sessionData.auth_url,
5256
- "Open this URL to authenticate"
5306
+ let data;
5307
+ try {
5308
+ data = await sessionResp.json();
5309
+ } catch {
5310
+ throw new BackendError("Auth session response body is not valid JSON");
5311
+ }
5312
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
5313
+ throw new BackendError("Auth session response body is not a JSON object");
5314
+ }
5315
+ return new AuthSession(
5316
+ data
5257
5317
  );
5258
- const startTime = Date.now();
5318
+ }
5319
+ /**
5320
+ * Poll an end-user sign-in session to completion.
5321
+ *
5322
+ * The polling half of the link-based sign-in flow. Use this when your code
5323
+ * minted the session itself via {@link createAuthSession} and you need to
5324
+ * block until the user finishes IDP login. {@link authenticate} is the
5325
+ * all-in-one CLI convenience (mint + open browser + poll + install token);
5326
+ * this method is the polling half on its own and — like
5327
+ * {@link createAuthSession} — installs **no** `userTokenGetter`. The caller
5328
+ * decides what to do with the returned token.
5329
+ *
5330
+ * Transient network blips and non-200 responses are retried until the
5331
+ * deadline (a remote user may take minutes to complete login); only a
5332
+ * terminal IDP error, an expired session, or the timeout end the loop.
5333
+ *
5334
+ * Scope: requires an app key with the `idp_users:read` scope.
5335
+ *
5336
+ * @param sessionToken - A session token from {@link createAuthSession}.
5337
+ * @param options - `timeoutMs` (default 300000) and `pollIntervalMs`
5338
+ * (default 2000). The Python SDK's equivalent takes seconds by ecosystem
5339
+ * convention; the two SDKs intentionally differ in unit.
5340
+ * @returns AuthResult with the user's IDP JWT (`userToken`) and `userInfo`.
5341
+ * @throws AlterValueError if `sessionToken` is empty.
5342
+ * @throws AlterSDKError if the SDK is closed or the IDP returned a terminal
5343
+ * error / the session expired.
5344
+ * @throws ConnectTimeoutError if the user did not complete login in time.
5345
+ */
5346
+ async pollAuthSession(sessionToken, options) {
5347
+ this.#assertNotClosed();
5348
+ if (typeof sessionToken !== "string" || sessionToken.trim().length === 0) {
5349
+ throw new AlterValueError("sessionToken must be a non-empty string");
5350
+ }
5351
+ const timeoutMs = options?.timeoutMs ?? 3e5;
5352
+ const pollIntervalMs = options?.pollIntervalMs ?? 2e3;
5353
+ const deadline = Date.now() + timeoutMs;
5259
5354
  while (true) {
5260
- if (Date.now() - startTime > timeoutMs) {
5261
- throw new ConnectTimeoutError(
5262
- "Authentication timed out",
5263
- { timeout: timeoutMs }
5264
- );
5265
- }
5266
- await new Promise((resolve2) => setTimeout(resolve2, 2e3));
5267
- const pollPath = "/sdk/auth/poll";
5268
- const pollBodyStr = JSON.stringify({ session_token: sessionData.session_token });
5269
- const pollHmac = this.#computeHmacHeaders("POST", pollPath, pollBodyStr);
5270
- let pollResp;
5271
- try {
5272
- pollResp = await this.#alterClient.post(pollPath, {
5273
- body: pollBodyStr,
5274
- headers: { ...actorHeaders, ...pollHmac, "Content-Type": "application/json" }
5275
- });
5276
- } catch {
5277
- continue;
5278
- }
5279
- this.#cacheActorIdFromResponse(pollResp);
5280
- if (!pollResp.ok) {
5281
- continue;
5282
- }
5283
- const pollData = await pollResp.json();
5284
- if (pollData.status === "completed") {
5285
- const userToken = pollData.user_token;
5286
- if (!userToken) {
5355
+ this.#assertNotClosed();
5356
+ const remainingBudget = deadline - Date.now();
5357
+ const waitMs = Math.max(
5358
+ 0,
5359
+ Math.min(Math.max(remainingBudget, 0), AUTH_POLL_SERVER_WAIT_MS)
5360
+ );
5361
+ const pollData = await this.#pollAuthSessionOnce(sessionToken, waitMs);
5362
+ if (pollData) {
5363
+ if (pollData.status === "completed") {
5364
+ const userToken = pollData.user_token;
5365
+ if (!userToken) {
5366
+ throw new AlterSDKError(
5367
+ "Authentication completed but no user token was returned by the IDP"
5368
+ );
5369
+ }
5370
+ return new AuthResult({
5371
+ user_token: userToken,
5372
+ user_info: pollData.user_info
5373
+ });
5374
+ }
5375
+ if (pollData.status === "error") {
5287
5376
  throw new AlterSDKError(
5288
- "Authentication completed but no user token was returned by the IDP"
5377
+ `Authentication failed: ${pollData.error_message || "unknown error"}`
5289
5378
  );
5290
5379
  }
5291
- if (this.#userTokenGetter !== null) {
5292
- this.#logger.warn(
5293
- "[alter-sdk] authenticate() replaced a previously-set userTokenGetter on this _VaultClient instance. Subsequent requests will use the authenticate() token, not the constructor getter."
5380
+ if (pollData.status === "expired") {
5381
+ throw new AlterSDKError("Authentication session expired");
5382
+ }
5383
+ if (pollData.status !== "pending") {
5384
+ throw new AlterSDKError(
5385
+ `Unexpected auth poll status from server: ${String(pollData.status)}`
5294
5386
  );
5295
5387
  }
5296
- this.#userTokenGetter = () => userToken;
5297
- return new AuthResult({
5298
- user_token: userToken,
5299
- user_info: pollData.user_info
5300
- });
5301
5388
  }
5302
- if (pollData.status === "error") {
5303
- throw new AlterSDKError(
5304
- pollData.error_message ?? "Authentication failed"
5305
- );
5389
+ const remaining = deadline - Date.now();
5390
+ if (remaining <= 0) break;
5391
+ await new Promise(
5392
+ (resolve2) => setTimeout(resolve2, Math.min(pollIntervalMs, remaining))
5393
+ );
5394
+ }
5395
+ throw new ConnectTimeoutError("Authentication timed out", { timeoutMs });
5396
+ }
5397
+ /**
5398
+ * Single sign-in poll attempt (INTERNAL).
5399
+ *
5400
+ * Returns the parsed poll body, or `null` to signal a transient failure
5401
+ * (network blip or non-200) that the caller should retry. Matches the
5402
+ * Python SDK's resilient poll: a long sign-in poll survives brief server
5403
+ * hiccups rather than aborting the whole flow.
5404
+ */
5405
+ async #pollAuthSessionOnce(sessionToken, waitMs = 0) {
5406
+ const actorHeaders = this.#getActorRequestHeaders();
5407
+ const pollPath = "/sdk/auth/poll";
5408
+ const pollBody = { session_token: sessionToken };
5409
+ if (waitMs > 0) pollBody.wait_ms = waitMs;
5410
+ const pollBodyStr = JSON.stringify(pollBody);
5411
+ const pollHmac = this.#computeHmacHeaders("POST", pollPath, pollBodyStr);
5412
+ const requestTimeoutMs = waitMs > 0 ? waitMs + AUTH_POLL_HTTP_BUFFER_MS : void 0;
5413
+ let pollResp;
5414
+ try {
5415
+ pollResp = await this.#alterClient.post(pollPath, {
5416
+ body: pollBodyStr,
5417
+ headers: { ...actorHeaders, ...pollHmac, "Content-Type": "application/json" },
5418
+ timeoutMs: requestTimeoutMs
5419
+ });
5420
+ } catch (e) {
5421
+ if (e instanceof TypeError || e instanceof Error && (e.name === "TimeoutError" || e.name === "AbortError")) {
5422
+ return null;
5306
5423
  }
5307
- if (pollData.status === "expired") {
5308
- throw new AlterSDKError("Authentication session expired");
5424
+ throw e;
5425
+ }
5426
+ this.#cacheActorIdFromResponse(pollResp);
5427
+ if (!pollResp.ok) {
5428
+ if (PERMANENT_POLL_STATUSES.has(pollResp.status)) {
5429
+ await this.#handleErrorResponse(pollResp);
5309
5430
  }
5431
+ return null;
5432
+ }
5433
+ let parsed;
5434
+ try {
5435
+ parsed = await pollResp.json();
5436
+ } catch {
5437
+ throw new BackendError("Auth session poll response body is not valid JSON");
5438
+ }
5439
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5440
+ throw new BackendError("Auth session poll response body is not a JSON object");
5441
+ }
5442
+ const data = parsed;
5443
+ if (typeof data.status !== "string" || data.status.length === 0) {
5444
+ throw new BackendError(
5445
+ "Auth session poll response missing required 'status' field",
5446
+ { field: "status" }
5447
+ );
5448
+ }
5449
+ return data;
5450
+ }
5451
+ /**
5452
+ * Trigger IDP login for end user via browser (bundled CLI convenience).
5453
+ *
5454
+ * Opens the app's configured IDP login page in the user's default browser.
5455
+ * Polls for completion and returns the user's IDP JWT token.
5456
+ *
5457
+ * After `authenticate()` completes, the returned `userToken` is installed
5458
+ * as the active `userTokenGetter` on this instance and automatically used
5459
+ * for subsequent `request()` calls that use identity resolution.
5460
+ *
5461
+ * This composes the two split primitives: {@link createAuthSession} (mint) +
5462
+ * {@link pollAuthSession} (poll). For headless / remote / multi-tenant
5463
+ * sign-in, call the primitives directly — they open no browser and install
5464
+ * no `userTokenGetter`.
5465
+ *
5466
+ * .. warning::
5467
+ *
5468
+ * **`authenticate()` overwrites any constructor-supplied `userTokenGetter`.**
5469
+ * If you pass `userTokenGetter` to the `_VaultClient` constructor and then
5470
+ * call `authenticate()`, the constructor getter is silently replaced for
5471
+ * the lifetime of the instance — a fresh `_VaultClient` instance is the
5472
+ * only way to restore the original getter. This is intentional for
5473
+ * interactive CLI flows but can surprise web backends that mix both
5474
+ * patterns. Prefer one or the other per `_VaultClient` instance. The
5475
+ * split primitives never install a getter, so prefer them when this
5476
+ * side effect is unwanted.
5477
+ *
5478
+ * @param options - Optional configuration (timeout in milliseconds, default 300000 = 5 min)
5479
+ * @returns AuthResult with userToken and userInfo
5480
+ * @throws ConnectTimeoutError if authentication times out
5481
+ * @throws AlterSDKError if session creation or authentication fails
5482
+ */
5483
+ async authenticate(options) {
5484
+ this.#assertNotClosed();
5485
+ const timeoutMs = options?.timeout ?? 3e5;
5486
+ const session = await this.createAuthSession();
5487
+ await __VaultClient.#openBrowserOrPrint(
5488
+ session.authUrl,
5489
+ "Open this URL to authenticate"
5490
+ );
5491
+ const result = await this.pollAuthSession(session.sessionToken, { timeoutMs });
5492
+ if (this.#userTokenGetter !== null) {
5493
+ this.#logger.warn(
5494
+ "[alter-sdk] authenticate() replaced a previously-set userTokenGetter on this _VaultClient instance. Subsequent requests will use the authenticate() token, not the constructor getter."
5495
+ );
5310
5496
  }
5497
+ this.#userTokenGetter = () => result.userToken;
5498
+ return result;
5311
5499
  }
5312
5500
  /**
5313
5501
  * Poll the Connect session for completion status (INTERNAL).
@@ -6439,6 +6627,20 @@ var App = class _App {
6439
6627
  return this.#client.createConnectSessionForError(...args);
6440
6628
  }
6441
6629
  // ── Auth (operator-only) ───────────────────────────────────────────────
6630
+ /**
6631
+ * Mint a link-based end-user sign-in session (no browser, no instance
6632
+ * mutation). See {@link _VaultClient.createAuthSession}.
6633
+ */
6634
+ async createAuthSession() {
6635
+ return this.#client.createAuthSession();
6636
+ }
6637
+ /**
6638
+ * Poll a sign-in session to completion, returning the user's JWT.
6639
+ * See {@link _VaultClient.pollAuthSession}.
6640
+ */
6641
+ async pollAuthSession(...args) {
6642
+ return this.#client.pollAuthSession(...args);
6643
+ }
6442
6644
  async authenticate(options) {
6443
6645
  return this.#client.authenticate(options);
6444
6646
  }
@@ -6871,7 +7073,7 @@ var DEFAULT_BASE_URL = "https://backend.alterauth.com";
6871
7073
  var PAT_API_PREFIX = "/api/v1/dev-portal";
6872
7074
  var HTTP_ERROR_THRESHOLD = 400;
6873
7075
  var DEFAULT_TIMEOUT_MS = 3e4;
6874
- var CLI_VERSION = "0.3.2";
7076
+ var CLI_VERSION = "0.3.3";
6875
7077
  var USER_AGENT = buildUserAgent();
6876
7078
  function buildUserAgent() {
6877
7079
  let osTag = "";
@@ -7017,6 +7219,9 @@ var DashboardClient = class {
7017
7219
  policy;
7018
7220
  /** Audit log read surface (CLI `alter audit` subcommands). */
7019
7221
  audit;
7222
+ /** OAuth + managed-secret grant inspect/revoke (CLI `alter grants`
7223
+ * subcommands). Requires ``dashboard_grants:read`` / ``:admin``. */
7224
+ grants;
7020
7225
  #closed = false;
7021
7226
  constructor(options = {}) {
7022
7227
  const resolvedPat = options.pat ?? (typeof process !== "undefined" ? process.env.ALTER_PAT : void 0);
@@ -7066,6 +7271,7 @@ var DashboardClient = class {
7066
7271
  this.managedSecrets = new ManagedSecretsNamespace(this);
7067
7272
  this.policy = new PolicyNamespace(this);
7068
7273
  this.audit = new AuditNamespace(this);
7274
+ this.grants = new GrantsNamespace(this);
7069
7275
  }
7070
7276
  /**
7071
7277
  * Release internal state. The default `fetch` is a global; this is
@@ -8029,9 +8235,8 @@ var AuditNamespace = class {
8029
8235
  }
8030
8236
  /** List runtime audit-log rows. Requires `dashboard_audit:read`.
8031
8237
  *
8032
- * NOTE: returns ``{logs, total, limit, offset, is_truncated}`` — a
8033
- * non-standard envelope (the audit routes pre-date the canonical
8034
- * ``items`` shape). Consumers narrow ``logs`` themselves.
8238
+ * Returns the canonical envelope ``{items, total, limit, offset, has_more}``
8239
+ * plus the distinct ``is_truncated`` cap-signal (the 10k offset cap was hit).
8035
8240
  */
8036
8241
  async list(options = {}) {
8037
8242
  const query = optionsToQuery({
@@ -8044,8 +8249,8 @@ var AuditNamespace = class {
8044
8249
  }
8045
8250
  /** List dashboard / CLI admin actions. Requires `dashboard_audit:read`.
8046
8251
  *
8047
- * NOTE: returns ``{events, total, limit, offset}``; consumers narrow
8048
- * ``events`` themselves.
8252
+ * Returns the canonical envelope ``{items, total, limit, offset, has_more}``
8253
+ * plus the distinct ``is_truncated`` cap-signal.
8049
8254
  */
8050
8255
  async listPortalActions(options = {}) {
8051
8256
  const query = optionsToQuery({
@@ -8063,8 +8268,8 @@ var AuditNamespace = class {
8063
8268
  }
8064
8269
  /** List grant-lifecycle events. Requires `dashboard_audit:read`.
8065
8270
  *
8066
- * NOTE: returns ``{events, total, limit, offset}``; consumers narrow
8067
- * ``events`` themselves.
8271
+ * Returns the canonical envelope ``{items, total, limit, offset, has_more}``
8272
+ * plus the distinct ``is_truncated`` cap-signal.
8068
8273
  */
8069
8274
  async listGrantEvents(options = {}) {
8070
8275
  const query = optionsToQuery({
@@ -8082,8 +8287,8 @@ var AuditNamespace = class {
8082
8287
  }
8083
8288
  /** List trace summaries. Requires `dashboard_audit:read`.
8084
8289
  *
8085
- * NOTE: returns ``{traces, total, limit, offset}``; consumers narrow
8086
- * ``traces`` themselves.
8290
+ * Returns the canonical envelope ``{items, total, limit, offset, has_more}``
8291
+ * plus the distinct ``is_event_window_capped`` signal.
8087
8292
  */
8088
8293
  async listTraces(options = {}) {
8089
8294
  const query = optionsToQuery({
@@ -8110,6 +8315,59 @@ var AuditNamespace = class {
8110
8315
  return expectDict(body, "audit.get_trace", 200);
8111
8316
  }
8112
8317
  };
8318
+ var GrantsNamespace = class {
8319
+ #client;
8320
+ constructor(client) {
8321
+ this.#client = client;
8322
+ }
8323
+ /**
8324
+ * List grants on an app. Returns the polymorphic OAuth +
8325
+ * managed-secret list (each row carries a ``grant_type``
8326
+ * discriminator). Requires ``dashboard_grants:read``.
8327
+ *
8328
+ * Wire envelope: ``{ grants, total, limit, offset, has_more,
8329
+ * is_truncated }`` — non-canonical (pre-dates the ``items``
8330
+ * convention). Consumers narrow ``grants`` themselves.
8331
+ */
8332
+ async list(appId, options = {}) {
8333
+ const app = encodePathParam(appId, "appId");
8334
+ const query = optionsToQuery({
8335
+ // The backend's FastAPI Query param on this GET route is named
8336
+ // ``grant_status`` (no alias). The public-facing CLI flag is
8337
+ // ``--status`` for ergonomics; we map it to the wire name here.
8338
+ grant_status: options.status,
8339
+ provider_id: options.provider_id,
8340
+ app_user_id: options.app_user_id,
8341
+ search: options.search,
8342
+ limit: options.limit ?? 100,
8343
+ offset: options.offset ?? 0
8344
+ });
8345
+ const body = await this.#client._call(
8346
+ "GET",
8347
+ `/apps/${app}/grants`,
8348
+ "grants.list",
8349
+ { query }
8350
+ );
8351
+ return expectDict(body, "grants.list", 200);
8352
+ }
8353
+ /**
8354
+ * Revoke a grant on an app. Cascade-revokes any active agent
8355
+ * delegations under the grant and best-effort deletes the vault
8356
+ * token. Recoverable via re-consent through the OAuth flow.
8357
+ * Requires ``dashboard_grants:admin``.
8358
+ */
8359
+ async revoke(appId, grantId, options = {}) {
8360
+ const app = encodePathParam(appId, "appId");
8361
+ const grant = encodePathParam(grantId, "grantId");
8362
+ const body = await this.#client._call(
8363
+ "POST",
8364
+ `/apps/${app}/grants/${grant}/revoke`,
8365
+ "grants.revoke",
8366
+ { jsonBody: { reason: options.reason ?? null } }
8367
+ );
8368
+ return expectDict(body, "grants.revoke", 200);
8369
+ }
8370
+ };
8113
8371
 
8114
8372
  // src/portal-factory.ts
8115
8373
  async function createPortalClient() {
@@ -9299,10 +9557,6 @@ function parseRelativeFromNow(raw, direction) {
9299
9557
  }
9300
9558
 
9301
9559
  // src/commands/audit.ts
9302
- function normalizeAuditEnvelope(result, domainKey) {
9303
- const { [domainKey]: items, ...rest } = result;
9304
- return { ...rest, items };
9305
- }
9306
9560
  function resolveStartDate(start, since) {
9307
9561
  if (start !== void 0 && since !== void 0) {
9308
9562
  process.stderr.write(
@@ -9415,7 +9669,7 @@ function buildAuditCommand() {
9415
9669
  offset: options.offset
9416
9670
  });
9417
9671
  const logs = validateRows(
9418
- extractArrayField(result, "logs", "audit.list"),
9672
+ extractArrayField(result, "items", "audit.list"),
9419
9673
  {
9420
9674
  id: isString,
9421
9675
  timestamp: isString,
@@ -9434,7 +9688,7 @@ function buildAuditCommand() {
9434
9688
  if (format === "table" || format === "jsonl") {
9435
9689
  emit(format, logs, AUDIT_COLUMNS);
9436
9690
  } else {
9437
- emit(format, normalizeAuditEnvelope(result, "logs"));
9691
+ emit(format, result);
9438
9692
  }
9439
9693
  });
9440
9694
  }
@@ -9481,7 +9735,7 @@ function buildAuditCommand() {
9481
9735
  offset: options.offset
9482
9736
  });
9483
9737
  const events = validateRows(
9484
- extractArrayField(result, "events", "audit.list_portal_actions"),
9738
+ extractArrayField(result, "items", "audit.list_portal_actions"),
9485
9739
  {
9486
9740
  id: isString,
9487
9741
  timestamp: isString,
@@ -9496,7 +9750,7 @@ function buildAuditCommand() {
9496
9750
  if (format === "table" || format === "jsonl") {
9497
9751
  emit(format, events, PORTAL_ACTION_COLUMNS);
9498
9752
  } else {
9499
- emit(format, normalizeAuditEnvelope(result, "events"));
9753
+ emit(format, result);
9500
9754
  }
9501
9755
  });
9502
9756
  }
@@ -9532,7 +9786,7 @@ function buildAuditCommand() {
9532
9786
  offset: options.offset
9533
9787
  });
9534
9788
  const events = validateRows(
9535
- extractArrayField(result, "events", "audit.list_grant_events"),
9789
+ extractArrayField(result, "items", "audit.list_grant_events"),
9536
9790
  {
9537
9791
  id: isString,
9538
9792
  timestamp: isString,
@@ -9546,7 +9800,7 @@ function buildAuditCommand() {
9546
9800
  if (format === "table" || format === "jsonl") {
9547
9801
  emit(format, events, GRANT_EVENT_COLUMNS);
9548
9802
  } else {
9549
- emit(format, normalizeAuditEnvelope(result, "events"));
9803
+ emit(format, result);
9550
9804
  }
9551
9805
  });
9552
9806
  }
@@ -9573,7 +9827,7 @@ function buildAuditCommand() {
9573
9827
  offset: options.offset
9574
9828
  });
9575
9829
  const traces = validateRows(
9576
- extractArrayField(result, "traces", "audit.list_traces"),
9830
+ extractArrayField(result, "items", "audit.list_traces"),
9577
9831
  {
9578
9832
  trace_id: isString,
9579
9833
  first_timestamp: isString,
@@ -9588,7 +9842,7 @@ function buildAuditCommand() {
9588
9842
  if (format === "table" || format === "jsonl") {
9589
9843
  emit(format, traces, TRACE_COLUMNS);
9590
9844
  } else {
9591
- emit(format, normalizeAuditEnvelope(result, "traces"));
9845
+ emit(format, result);
9592
9846
  }
9593
9847
  });
9594
9848
  }
@@ -9630,7 +9884,13 @@ var DEFAULT_SCOPES = [
9630
9884
  // ``:delete`` and ``:admin`` are deliberate opt-ins via ``--scopes``
9631
9885
  // per the destructive-action policy in CLAUDE.md.
9632
9886
  "dashboard_secrets:read",
9633
- "dashboard_secrets:write"
9887
+ "dashboard_secrets:write",
9888
+ // ``dashboard_grants`` (added to backend catalog v4). Read is in
9889
+ // defaults so ``alter grants list`` works out of the box; ``:admin``
9890
+ // (revoke) requires explicit ``--scopes`` opt-in per the
9891
+ // destructive-action policy in CLAUDE.md — same posture as
9892
+ // ``dashboard_keys:admin``.
9893
+ "dashboard_grants:read"
9634
9894
  ];
9635
9895
  function deriveDashboardUrl(baseUrl) {
9636
9896
  return baseUrl.replace(/^https:\/\/backend\./, "https://portal.");
@@ -9920,7 +10180,17 @@ var DASHBOARD_RESOURCE_VERBS = {
9920
10180
  // grant + delegation + audit anchor and vault-deletes the stored
9921
10181
  // credential (irrecoverable). Backend catalog v3, see
9922
10182
  // ``apps/backend/app/core/scopes/catalog.py`` _DASHBOARD_RESOURCES_V3.
9923
- dashboard_secrets: ["read", "write", "delete"]
10183
+ dashboard_secrets: ["read", "write", "delete"],
10184
+ // ``dashboard_grants`` (catalog v4): inspect + revoke end-user
10185
+ // grants on an app. ``:read`` → GET /apps/{id}/grants (list, OAuth
10186
+ // + managed-secret polymorphic); ``:admin`` → POST /apps/{id}/grants/
10187
+ // {id}/revoke. Grant revocation is RECOVERABLE per CLAUDE.md's
10188
+ // destructive-action tiers (re-consent restores access), so the
10189
+ // revoke route stays on the standard ``:admin`` verb — no separate
10190
+ // ``:delete`` tier. ``:write`` omitted because grants are minted by
10191
+ // the OAuth flow, never via direct API edits. Backend source of
10192
+ // truth: ``_DASHBOARD_RESOURCES_V4``.
10193
+ dashboard_grants: ["read", "admin"]
9924
10194
  };
9925
10195
  function parseScopesFlag(raw) {
9926
10196
  if (typeof raw !== "string") {
@@ -10568,8 +10838,148 @@ function buildCompletionCommand() {
10568
10838
  return completion;
10569
10839
  }
10570
10840
 
10571
- // src/commands/keys.ts
10841
+ // src/commands/grants.ts
10572
10842
  import { Command as Command6 } from "commander";
10843
+ var GRANT_COLUMNS = [
10844
+ { label: "ID", get: (g) => g.id, maxWidth: 36 },
10845
+ { label: "TYPE", get: (g) => g.grant_type },
10846
+ { label: "PROVIDER", get: (g) => g.provider_name, maxWidth: 24 },
10847
+ { label: "STATUS", get: (g) => g.status },
10848
+ {
10849
+ label: "USER",
10850
+ get: (g) => g.user_identifier ?? g.account_display_name ?? "\u2014",
10851
+ maxWidth: 32
10852
+ },
10853
+ { label: "CREATED", get: (g) => g.created_at }
10854
+ ];
10855
+ function buildGrantsCommand() {
10856
+ const grants = new Command6("grants").description(
10857
+ "Inspect and revoke end-user grants (OAuth + managed-secret)"
10858
+ );
10859
+ grants.command("list").description(
10860
+ "List grants on an app (OAuth + managed-secret, polymorphic). Requires dashboard_grants:read scope."
10861
+ ).option(
10862
+ "--app <app-id>",
10863
+ "App ID. Falls back to ALTER_APP_ID env or .alter/config.yaml"
10864
+ ).option(
10865
+ "--status <status>",
10866
+ "Filter by status: active | expired | revoked. Case-sensitive \u2014 matches the backend's ``GrantStatus`` enum verbatim."
10867
+ ).option(
10868
+ "--provider <id>",
10869
+ "Filter by provider ID (e.g. 'google', 'github', or a managed-secret template slug)."
10870
+ ).option(
10871
+ "--app-user <uuid>",
10872
+ "Filter by end-user (app_user_id, internal UUID)",
10873
+ parseUuidArgument("--app-user")
10874
+ ).option(
10875
+ "--search <text>",
10876
+ "Substring search on user_identifier OR account_display_name (max 100 chars; backend-side filter)"
10877
+ ).option(
10878
+ "--limit <n>",
10879
+ "Page size (1\u20131000)",
10880
+ parseBoundedInt("--limit", 1, 1e3),
10881
+ 100
10882
+ ).option(
10883
+ "--offset <n>",
10884
+ "Pagination offset (oversize values clamp to 10,000 with is_truncated=true)",
10885
+ parseNonNegativeInt("--offset"),
10886
+ 0
10887
+ ).option(
10888
+ "--output <format>",
10889
+ "Output format: json|jsonl|table (default: table)",
10890
+ "table"
10891
+ ).action(
10892
+ async (options) => {
10893
+ const format = coerceOutputFormat(options.output);
10894
+ const status = validateChoice(
10895
+ "--status",
10896
+ options.status,
10897
+ ["active", "expired", "revoked"]
10898
+ );
10899
+ if (options.search !== void 0) {
10900
+ validateNoControlCharsOrExit("--search", options.search);
10901
+ }
10902
+ await withClient(async (client) => {
10903
+ const result = await client.grants.list(resolveAppIdOrExit(options.app), {
10904
+ status,
10905
+ provider_id: options.provider,
10906
+ app_user_id: options.appUser,
10907
+ search: options.search,
10908
+ limit: options.limit,
10909
+ offset: options.offset
10910
+ });
10911
+ const rows = validateRows(
10912
+ extractArrayField(result, "grants", "grants.list"),
10913
+ {
10914
+ id: isString,
10915
+ grant_type: isString,
10916
+ provider_id: isString,
10917
+ provider_name: isString,
10918
+ status: isString,
10919
+ user_identifier: isOptionalString,
10920
+ account_display_name: isOptionalString,
10921
+ created_at: isString,
10922
+ last_used_at: isOptionalString,
10923
+ expires_at: isOptionalString
10924
+ },
10925
+ "grants.list"
10926
+ );
10927
+ if (format === "table" || format === "jsonl") {
10928
+ emit(format, rows, GRANT_COLUMNS);
10929
+ } else {
10930
+ emit(format, result);
10931
+ }
10932
+ });
10933
+ }
10934
+ );
10935
+ grants.command("revoke").description(
10936
+ "Revoke a grant immediately (cascades to active agent delegations under the grant + best-effort vault token delete). Recoverable via re-consent through the OAuth flow. Requires dashboard_grants:admin scope."
10937
+ ).option(
10938
+ "--app <app-id>",
10939
+ "App ID. Falls back to ALTER_APP_ID env or .alter/config.yaml"
10940
+ ).requiredOption(
10941
+ "--grant <grant-id>",
10942
+ "Grant ID",
10943
+ parseUuidArgument("--grant")
10944
+ ).option(
10945
+ "--reason <text>",
10946
+ "Optional operator-supplied reason recorded on the audit row."
10947
+ ).option("--yes", "Skip the interactive y/N prompt").action(
10948
+ async (options) => {
10949
+ if (options.reason !== void 0) {
10950
+ validateNoControlCharsOrExit("--reason", options.reason);
10951
+ }
10952
+ await withClient(async (client) => {
10953
+ if (!options.yes) {
10954
+ const ok = await promptYesNo(
10955
+ `Revoke grant ${options.grant}? This cascades to any active agent delegations. [y/N]: `
10956
+ );
10957
+ if (!ok) {
10958
+ process.stderr.write("alter: revoke cancelled.\n");
10959
+ process.exit(EXIT_CANCELLED);
10960
+ }
10961
+ }
10962
+ const result = await client.grants.revoke(
10963
+ resolveAppIdOrExit(options.app),
10964
+ options.grant,
10965
+ { reason: options.reason }
10966
+ );
10967
+ const revokedAt = result.revoked_at;
10968
+ if (typeof revokedAt === "string") {
10969
+ process.stdout.write(`Revoked grant ${options.grant} at ${revokedAt}.
10970
+ `);
10971
+ } else {
10972
+ process.stdout.write(`Revoked grant ${options.grant}.
10973
+ `);
10974
+ }
10975
+ });
10976
+ }
10977
+ );
10978
+ return grants;
10979
+ }
10980
+
10981
+ // src/commands/keys.ts
10982
+ import { Command as Command7 } from "commander";
10573
10983
  var KEY_TYPE_MAP = {
10574
10984
  runtime: "rk",
10575
10985
  agent: "ak"
@@ -10583,7 +10993,7 @@ var KEY_COLUMNS2 = [
10583
10993
  { label: "STATUS", get: (k) => k.revoked_at ? "revoked" : "active" }
10584
10994
  ];
10585
10995
  function buildKeysCommand() {
10586
- const keys = new Command6("keys").description("Manage scoped API keys");
10996
+ const keys = new Command7("keys").description("Manage scoped API keys");
10587
10997
  keys.command("list").description("List keys for an app").option("--app <app-id>", "App ID. Falls back to ALTER_APP_ID env or .alter/config.yaml").option(
10588
10998
  "--output <format>",
10589
10999
  "Output format: json|jsonl|table (default: table)",
@@ -10805,9 +11215,9 @@ function buildKeysCommand() {
10805
11215
  // src/commands/link.ts
10806
11216
  import { existsSync as existsSync4 } from "fs";
10807
11217
  import { join as join5 } from "path";
10808
- import { Command as Command7 } from "commander";
11218
+ import { Command as Command8 } from "commander";
10809
11219
  function buildLinkCommand() {
10810
- const link = new Command7("link").description(
11220
+ const link = new Command8("link").description(
10811
11221
  "Pin the current directory tree to a default app_id so subsequent commands (``keys``, ``agents``, ``providers``, ``policy``) don't need --app."
10812
11222
  ).argument("[app-id]", "App ID to pin. Omitted when --status is passed.").option(
10813
11223
  "--force",
@@ -10835,7 +11245,7 @@ function buildLinkCommand() {
10835
11245
  return link;
10836
11246
  }
10837
11247
  function buildUnlinkCommand() {
10838
- return new Command7("unlink").description("Remove the current directory's .alter/config.yaml pin.").action(async () => {
11248
+ return new Command8("unlink").description("Remove the current directory's .alter/config.yaml pin.").action(async () => {
10839
11249
  const removed = removeWorkspaceConfig();
10840
11250
  if (removed) {
10841
11251
  process.stdout.write(
@@ -10923,7 +11333,7 @@ async function runStatus() {
10923
11333
 
10924
11334
  // src/commands/managed-secrets.ts
10925
11335
  import { readFileSync as readFileSync6 } from "fs";
10926
- import { Command as Command8, Option } from "commander";
11336
+ import { Command as Command9, Option } from "commander";
10927
11337
  var PRINCIPAL_TYPES = ["user", "group", "system", "agent"];
10928
11338
  var CREDENTIAL_TYPES = [
10929
11339
  "bearer_token",
@@ -11277,7 +11687,7 @@ var SECRET_COLUMNS = [
11277
11687
  { label: "GRANTS", get: (s) => String(s.grant_count) },
11278
11688
  { label: "CREATED", get: (s) => s.created_at }
11279
11689
  ];
11280
- var GRANT_COLUMNS = [
11690
+ var GRANT_COLUMNS2 = [
11281
11691
  { label: "GRANT_ID", get: (g) => g.grant_id, maxWidth: 36 },
11282
11692
  { label: "PRINCIPAL", get: (g) => g.principal_type },
11283
11693
  { label: "LABEL", get: (g) => g.label ?? "\u2014", maxWidth: 24 },
@@ -11309,7 +11719,7 @@ var ACCESS_COLUMNS = [
11309
11719
  { label: "AGENT", get: (a) => a.via_agent_name ?? "\u2014" }
11310
11720
  ];
11311
11721
  function buildGrantsSubcommand() {
11312
- const grants = new Command8("grants").description(
11722
+ const grants = new Command9("grants").description(
11313
11723
  "Manage grants on managed secrets (list / create / update / revoke)."
11314
11724
  );
11315
11725
  grants.command("list <secret-id>").description("List grants for a managed secret").option("--app <app-id>", "App ID. Falls back to ALTER_APP_ID env or .alter/config.yaml").option(
@@ -11347,7 +11757,7 @@ function buildGrantsSubcommand() {
11347
11757
  created_at: isString,
11348
11758
  expires_at: isOptionalString
11349
11759
  }, "managed-secrets.grants.list");
11350
- emit(format, rows, GRANT_COLUMNS);
11760
+ emit(format, rows, GRANT_COLUMNS2);
11351
11761
  });
11352
11762
  }
11353
11763
  );
@@ -11377,7 +11787,7 @@ function buildGrantsSubcommand() {
11377
11787
  created_at: isString,
11378
11788
  expires_at: isOptionalString
11379
11789
  }, "managed-secrets.grants.list-for-agent");
11380
- emit(format, rows, GRANT_COLUMNS);
11790
+ emit(format, rows, GRANT_COLUMNS2);
11381
11791
  });
11382
11792
  }
11383
11793
  );
@@ -11581,7 +11991,7 @@ function buildGrantsSubcommand() {
11581
11991
  return grants;
11582
11992
  }
11583
11993
  function buildGroupsSubcommand() {
11584
- const groups = new Command8("groups").description(
11994
+ const groups = new Command9("groups").description(
11585
11995
  "App-user-group autocomplete + group detail helpers."
11586
11996
  );
11587
11997
  groups.command("list").description("List app-user-groups (Group-tab picker source)").option("--app <app-id>", "App ID").option("--idp <uuid>", "Filter by identity provider id").option("--search <substring>", "Substring search on group name / external_group_id").option("--limit <n>", "Page size (default: 50)", parseBoundedInt("--limit", 1, 200)).option("--offset <n>", "Starting offset (default: 0)", parseNonNegativeInt("--offset")).option(
@@ -11653,7 +12063,7 @@ function buildGroupsSubcommand() {
11653
12063
  return groups;
11654
12064
  }
11655
12065
  function buildManagedSecretsCommand() {
11656
- const root = new Command8("managed-secrets").description(
12066
+ const root = new Command9("managed-secrets").description(
11657
12067
  "Manage managed-secret credentials, grants, and access (CRUD + autocomplete helpers)."
11658
12068
  );
11659
12069
  root.command("templates").description("List the managed-secret template catalog for an app").option("--app <app-id>", "App ID").option("--limit <n>", "Page size (default: 200)", parseBoundedInt("--limit", 1, 1e3)).option("--offset <n>", "Starting offset (default: 0)", parseNonNegativeInt("--offset")).option(
@@ -11724,7 +12134,7 @@ function buildManagedSecretsCommand() {
11724
12134
  root.command("create").description("Create a managed secret + base grant").option("--app <app-id>", "App ID").option(
11725
12135
  "--input <path>",
11726
12136
  "JSON wire body from file (@path) or stdin (-). Mirrors `gh api --input`. Replaces per-field flags; the body is sent to POST /apps/{id}/managed-secrets verbatim. Use when you have the body pre-computed (CI / templating) or want one stable shape across the CLI."
11727
- ).option("--slug <slug>", "Stable kebab-case identifier (e.g. 'openai-api'). Required unless --input is supplied.").option("--name <name>", "Human display name. Required unless --input is supplied.").option("--description <text>", "Free-form description").option("--template <id>", "Template id (e.g. 'openai'); omit for custom").addOption(
12137
+ ).option("--name <name>", "Human display name. Required unless --input is supplied.").option("--description <text>", "Free-form description").option("--template <id>", "Template id (e.g. 'openai'); omit for custom").addOption(
11728
12138
  // ``--credential-type`` is enum-validated at RUNTIME (see the
11729
12139
  // per-field branch below) rather than parse-time so a typo'd
11730
12140
  // value alongside ``--input @body.json`` (which discards the
@@ -11769,7 +12179,6 @@ function buildManagedSecretsCommand() {
11769
12179
  "--input"
11770
12180
  );
11771
12181
  const perField = [
11772
- "slug",
11773
12182
  "name",
11774
12183
  "description",
11775
12184
  "template",
@@ -11802,12 +12211,6 @@ function buildManagedSecretsCommand() {
11802
12211
  });
11803
12212
  return;
11804
12213
  }
11805
- if (options.slug === void 0) {
11806
- process.stderr.write(
11807
- "alter: managed-secrets create requires --slug (or use --input @file.json to supply the full body)\n"
11808
- );
11809
- process.exit(EXIT_USAGE);
11810
- }
11811
12214
  if (options.name === void 0) {
11812
12215
  process.stderr.write(
11813
12216
  "alter: managed-secrets create requires --name (or use --input @file.json to supply the full body)\n"
@@ -11820,7 +12223,6 @@ function buildManagedSecretsCommand() {
11820
12223
  CREDENTIAL_TYPES
11821
12224
  );
11822
12225
  validateNoControlCharsOrExit("--name", options.name);
11823
- validateNoControlCharsOrExit("--slug", options.slug);
11824
12226
  if (options.description !== void 0) {
11825
12227
  validateNoControlCharsOrExit("--description", options.description);
11826
12228
  }
@@ -11844,7 +12246,6 @@ function buildManagedSecretsCommand() {
11844
12246
  const additionalCredentials = loadedCredentials !== void 0 ? loadedCredentials.additionalCredentials : options.credentialField;
11845
12247
  const additionalInjections = options.injectionRule !== void 0 ? loadInjectionRules(options.injectionRule) : void 0;
11846
12248
  const body = {
11847
- slug: options.slug,
11848
12249
  name: options.name,
11849
12250
  credential_type: options.credentialType,
11850
12251
  credential_value: credentialValue
@@ -12103,7 +12504,7 @@ function buildManagedSecretsCommand() {
12103
12504
  }
12104
12505
 
12105
12506
  // src/commands/pats.ts
12106
- import { Command as Command9 } from "commander";
12507
+ import { Command as Command10 } from "commander";
12107
12508
  async function whoami(options) {
12108
12509
  const client = await createPortalClient();
12109
12510
  let exitCode = 0;
@@ -12125,7 +12526,7 @@ async function whoami(options) {
12125
12526
  }
12126
12527
  }
12127
12528
  function buildPatsCommand() {
12128
- const pats = new Command9("pats").description("Inspect Personal Access Tokens");
12529
+ const pats = new Command10("pats").description("Inspect Personal Access Tokens");
12129
12530
  pats.command("whoami").description("Show the currently-active PAT and its scopes").option("--output <format>", "Output format: json|table (default: json)", "json").action(async (options) => {
12130
12531
  const format = coerceOutputFormat(options.output);
12131
12532
  await whoami({ output: format });
@@ -12134,9 +12535,9 @@ function buildPatsCommand() {
12134
12535
  }
12135
12536
 
12136
12537
  // src/commands/policy.ts
12137
- import { Command as Command10 } from "commander";
12538
+ import { Command as Command11 } from "commander";
12138
12539
  function buildPolicyCommand() {
12139
- const policy = new Command10("policy").description(
12540
+ const policy = new Command11("policy").description(
12140
12541
  "Read app-level policy (read-only \u2014 edits via dashboard)"
12141
12542
  );
12142
12543
  policy.command("show-app").description(
@@ -12153,7 +12554,7 @@ function buildPolicyCommand() {
12153
12554
 
12154
12555
  // src/commands/providers.ts
12155
12556
  import { readFileSync as readFileSync7 } from "fs";
12156
- import { Command as Command11 } from "commander";
12557
+ import { Command as Command12 } from "commander";
12157
12558
  var CREDENTIAL_SOURCES = ["custom", "shared_dev"];
12158
12559
  var PROVIDER_STATUSES = ["active", "disabled"];
12159
12560
  var PROVIDER_COLUMNS = [
@@ -12232,7 +12633,7 @@ function surfaceProviderResponse(row) {
12232
12633
  }
12233
12634
  }
12234
12635
  function buildProvidersCommand() {
12235
- const providers = new Command11("providers").description(
12636
+ const providers = new Command12("providers").description(
12236
12637
  "Manage OAuth provider configs"
12237
12638
  );
12238
12639
  providers.command("list").description("List OAuth provider configs for an app").option("--app <app-id>", "App ID. Falls back to ALTER_APP_ID env or .alter/config.yaml").option(
@@ -12469,9 +12870,9 @@ function buildProvidersCommand() {
12469
12870
  }
12470
12871
 
12471
12872
  // src/commands/sdk-passthrough.ts
12472
- import { Command as Command12 } from "commander";
12873
+ import { Command as Command13 } from "commander";
12473
12874
  function buildSdkPassthroughCommand() {
12474
- const cmd = new Command12("sdk-passthrough").description(
12875
+ const cmd = new Command13("sdk-passthrough").description(
12475
12876
  "Escape hatch for ad-hoc runtime SDK calls (vault.request). Uses a runtime API key, NOT a PAT."
12476
12877
  );
12477
12878
  cmd.command("request <grant-id>").description(
@@ -12600,7 +13001,7 @@ function buildSdkPassthroughCommand() {
12600
13001
  // src/commands/self-update.ts
12601
13002
  import { spawnSync } from "child_process";
12602
13003
  import { platform as platform4 } from "os";
12603
- import { Command as Command13 } from "commander";
13004
+ import { Command as Command14 } from "commander";
12604
13005
  var CLI_PACKAGE = "@alter-ai/cli";
12605
13006
  function runNpmInstall(spec) {
12606
13007
  const result = spawnSync(
@@ -12655,7 +13056,7 @@ function probeNpmInstall() {
12655
13056
  };
12656
13057
  }
12657
13058
  function buildSelfUpdateCommand() {
12658
- return new Command13("self-update").description("Upgrade the alter CLI globally via npm").option("--to <version>", "Pin to a specific version instead of @latest").option(
13059
+ return new Command14("self-update").description("Upgrade the alter CLI globally via npm").option("--to <version>", "Pin to a specific version instead of @latest").option(
12659
13060
  "--dry-run",
12660
13061
  "Print the npm install command that would run, without executing it"
12661
13062
  ).action(async (options) => {
@@ -12710,7 +13111,7 @@ function buildSelfUpdateCommand() {
12710
13111
 
12711
13112
  // src/program.ts
12712
13113
  function buildProgram() {
12713
- const program2 = new Command14();
13114
+ const program2 = new Command15();
12714
13115
  program2.name("alter").description("Alter Vault command-line interface").version(package_default.version);
12715
13116
  program2.option(
12716
13117
  "--fields <list>",
@@ -12739,6 +13140,7 @@ function buildProgram() {
12739
13140
  program2.addCommand(buildManagedSecretsCommand());
12740
13141
  program2.addCommand(buildPolicyCommand());
12741
13142
  program2.addCommand(buildAuditCommand());
13143
+ program2.addCommand(buildGrantsCommand());
12742
13144
  program2.addCommand(buildPatsCommand());
12743
13145
  program2.addCommand(buildLinkCommand());
12744
13146
  program2.addCommand(buildUnlinkCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alter-ai/cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Command-line interface for the Alter Vault dev portal — scripted dashboard automation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,7 +43,7 @@
43
43
  "tsup": "^8.0.0",
44
44
  "tsx": "^4.7.0",
45
45
  "typescript": "^5.5.0",
46
- "vitest": "^3.0.0"
46
+ "vitest": "^4.1.8"
47
47
  },
48
48
  "engines": {
49
49
  "node": ">=20.10.0"