@alter-ai/cli 0.3.1 → 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 (3) hide show
  1. package/README.md +3 -2
  2. package/dist/cli.js +606 -130
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -26,8 +26,9 @@ apps list | create | show | update | archive | unarchive | delete
26
26
  keys list | mint | show | rotate | revoke | rename
27
27
  agents list | create | show | update | revoke (+ mint-key, list-keys, revoke-key)
28
28
  providers list | list-catalog | create | show | update | delete
29
- managed-secrets list | show | create | rotate | delete | templates | access | users
30
- grants {list, list-for-agent, create, update, revoke}
29
+ managed-secrets list | show | create | rotate | delete | templates | access |
30
+ set-delegation-policy | users |
31
+ grants {list, list-for-agent, create, update, revoke} |
31
32
  groups {list, show}
32
33
  policy show-app
33
34
  audit list | show | portal-actions | grant-events | traces
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.1",
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");
5310
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
+ );
5496
+ }
5497
+ this.#userTokenGetter = () => result.userToken;
5498
+ return result;
5311
5499
  }
5312
5500
  /**
5313
5501
  * Poll the Connect session for completion status (INTERNAL).
@@ -6229,6 +6417,21 @@ var Agent = class _Agent {
6229
6417
  async connect(options) {
6230
6418
  return this.#client.connect(options);
6231
6419
  }
6420
+ /**
6421
+ * Poll a Connect session to completion. See
6422
+ * {@link _VaultClient.pollConnectSession} for the full signature.
6423
+ */
6424
+ async pollConnectSession(...args) {
6425
+ return this.#client.pollConnectSession(...args);
6426
+ }
6427
+ /**
6428
+ * Mint a recovery Connect session from a typed error. See
6429
+ * {@link _VaultClient.createConnectSessionForError} for the full
6430
+ * signature.
6431
+ */
6432
+ async createConnectSessionForError(...args) {
6433
+ return this.#client.createConnectSessionForError(...args);
6434
+ }
6232
6435
  // ── Delegation self-revoke ─────────────────────────────────────────────
6233
6436
  /**
6234
6437
  * Opt out of this agent's delegation on an OAuth grant.
@@ -6408,7 +6611,36 @@ var App = class _App {
6408
6611
  async connect(options) {
6409
6612
  return this.#client.connect(options);
6410
6613
  }
6614
+ /**
6615
+ * Poll a Connect session to completion. See
6616
+ * {@link _VaultClient.pollConnectSession} for the full signature.
6617
+ */
6618
+ async pollConnectSession(...args) {
6619
+ return this.#client.pollConnectSession(...args);
6620
+ }
6621
+ /**
6622
+ * Mint a recovery Connect session from a typed error. See
6623
+ * {@link _VaultClient.createConnectSessionForError} for the full
6624
+ * signature.
6625
+ */
6626
+ async createConnectSessionForError(...args) {
6627
+ return this.#client.createConnectSessionForError(...args);
6628
+ }
6411
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
+ }
6412
6644
  async authenticate(options) {
6413
6645
  return this.#client.authenticate(options);
6414
6646
  }
@@ -6841,7 +7073,7 @@ var DEFAULT_BASE_URL = "https://backend.alterauth.com";
6841
7073
  var PAT_API_PREFIX = "/api/v1/dev-portal";
6842
7074
  var HTTP_ERROR_THRESHOLD = 400;
6843
7075
  var DEFAULT_TIMEOUT_MS = 3e4;
6844
- var CLI_VERSION = "0.3.1";
7076
+ var CLI_VERSION = "0.3.3";
6845
7077
  var USER_AGENT = buildUserAgent();
6846
7078
  function buildUserAgent() {
6847
7079
  let osTag = "";
@@ -6987,6 +7219,9 @@ var DashboardClient = class {
6987
7219
  policy;
6988
7220
  /** Audit log read surface (CLI `alter audit` subcommands). */
6989
7221
  audit;
7222
+ /** OAuth + managed-secret grant inspect/revoke (CLI `alter grants`
7223
+ * subcommands). Requires ``dashboard_grants:read`` / ``:admin``. */
7224
+ grants;
6990
7225
  #closed = false;
6991
7226
  constructor(options = {}) {
6992
7227
  const resolvedPat = options.pat ?? (typeof process !== "undefined" ? process.env.ALTER_PAT : void 0);
@@ -7036,6 +7271,7 @@ var DashboardClient = class {
7036
7271
  this.managedSecrets = new ManagedSecretsNamespace(this);
7037
7272
  this.policy = new PolicyNamespace(this);
7038
7273
  this.audit = new AuditNamespace(this);
7274
+ this.grants = new GrantsNamespace(this);
7039
7275
  }
7040
7276
  /**
7041
7277
  * Release internal state. The default `fetch` is a global; this is
@@ -7999,9 +8235,8 @@ var AuditNamespace = class {
7999
8235
  }
8000
8236
  /** List runtime audit-log rows. Requires `dashboard_audit:read`.
8001
8237
  *
8002
- * NOTE: returns ``{logs, total, limit, offset, is_truncated}`` — a
8003
- * non-standard envelope (the audit routes pre-date the canonical
8004
- * ``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).
8005
8240
  */
8006
8241
  async list(options = {}) {
8007
8242
  const query = optionsToQuery({
@@ -8014,8 +8249,8 @@ var AuditNamespace = class {
8014
8249
  }
8015
8250
  /** List dashboard / CLI admin actions. Requires `dashboard_audit:read`.
8016
8251
  *
8017
- * NOTE: returns ``{events, total, limit, offset}``; consumers narrow
8018
- * ``events`` themselves.
8252
+ * Returns the canonical envelope ``{items, total, limit, offset, has_more}``
8253
+ * plus the distinct ``is_truncated`` cap-signal.
8019
8254
  */
8020
8255
  async listPortalActions(options = {}) {
8021
8256
  const query = optionsToQuery({
@@ -8033,8 +8268,8 @@ var AuditNamespace = class {
8033
8268
  }
8034
8269
  /** List grant-lifecycle events. Requires `dashboard_audit:read`.
8035
8270
  *
8036
- * NOTE: returns ``{events, total, limit, offset}``; consumers narrow
8037
- * ``events`` themselves.
8271
+ * Returns the canonical envelope ``{items, total, limit, offset, has_more}``
8272
+ * plus the distinct ``is_truncated`` cap-signal.
8038
8273
  */
8039
8274
  async listGrantEvents(options = {}) {
8040
8275
  const query = optionsToQuery({
@@ -8052,8 +8287,8 @@ var AuditNamespace = class {
8052
8287
  }
8053
8288
  /** List trace summaries. Requires `dashboard_audit:read`.
8054
8289
  *
8055
- * NOTE: returns ``{traces, total, limit, offset}``; consumers narrow
8056
- * ``traces`` themselves.
8290
+ * Returns the canonical envelope ``{items, total, limit, offset, has_more}``
8291
+ * plus the distinct ``is_event_window_capped`` signal.
8057
8292
  */
8058
8293
  async listTraces(options = {}) {
8059
8294
  const query = optionsToQuery({
@@ -8080,6 +8315,59 @@ var AuditNamespace = class {
8080
8315
  return expectDict(body, "audit.get_trace", 200);
8081
8316
  }
8082
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
+ };
8083
8371
 
8084
8372
  // src/portal-factory.ts
8085
8373
  async function createPortalClient() {
@@ -9269,10 +9557,6 @@ function parseRelativeFromNow(raw, direction) {
9269
9557
  }
9270
9558
 
9271
9559
  // src/commands/audit.ts
9272
- function normalizeAuditEnvelope(result, domainKey) {
9273
- const { [domainKey]: items, ...rest } = result;
9274
- return { ...rest, items };
9275
- }
9276
9560
  function resolveStartDate(start, since) {
9277
9561
  if (start !== void 0 && since !== void 0) {
9278
9562
  process.stderr.write(
@@ -9385,7 +9669,7 @@ function buildAuditCommand() {
9385
9669
  offset: options.offset
9386
9670
  });
9387
9671
  const logs = validateRows(
9388
- extractArrayField(result, "logs", "audit.list"),
9672
+ extractArrayField(result, "items", "audit.list"),
9389
9673
  {
9390
9674
  id: isString,
9391
9675
  timestamp: isString,
@@ -9404,7 +9688,7 @@ function buildAuditCommand() {
9404
9688
  if (format === "table" || format === "jsonl") {
9405
9689
  emit(format, logs, AUDIT_COLUMNS);
9406
9690
  } else {
9407
- emit(format, normalizeAuditEnvelope(result, "logs"));
9691
+ emit(format, result);
9408
9692
  }
9409
9693
  });
9410
9694
  }
@@ -9451,7 +9735,7 @@ function buildAuditCommand() {
9451
9735
  offset: options.offset
9452
9736
  });
9453
9737
  const events = validateRows(
9454
- extractArrayField(result, "events", "audit.list_portal_actions"),
9738
+ extractArrayField(result, "items", "audit.list_portal_actions"),
9455
9739
  {
9456
9740
  id: isString,
9457
9741
  timestamp: isString,
@@ -9466,7 +9750,7 @@ function buildAuditCommand() {
9466
9750
  if (format === "table" || format === "jsonl") {
9467
9751
  emit(format, events, PORTAL_ACTION_COLUMNS);
9468
9752
  } else {
9469
- emit(format, normalizeAuditEnvelope(result, "events"));
9753
+ emit(format, result);
9470
9754
  }
9471
9755
  });
9472
9756
  }
@@ -9502,7 +9786,7 @@ function buildAuditCommand() {
9502
9786
  offset: options.offset
9503
9787
  });
9504
9788
  const events = validateRows(
9505
- extractArrayField(result, "events", "audit.list_grant_events"),
9789
+ extractArrayField(result, "items", "audit.list_grant_events"),
9506
9790
  {
9507
9791
  id: isString,
9508
9792
  timestamp: isString,
@@ -9516,7 +9800,7 @@ function buildAuditCommand() {
9516
9800
  if (format === "table" || format === "jsonl") {
9517
9801
  emit(format, events, GRANT_EVENT_COLUMNS);
9518
9802
  } else {
9519
- emit(format, normalizeAuditEnvelope(result, "events"));
9803
+ emit(format, result);
9520
9804
  }
9521
9805
  });
9522
9806
  }
@@ -9543,7 +9827,7 @@ function buildAuditCommand() {
9543
9827
  offset: options.offset
9544
9828
  });
9545
9829
  const traces = validateRows(
9546
- extractArrayField(result, "traces", "audit.list_traces"),
9830
+ extractArrayField(result, "items", "audit.list_traces"),
9547
9831
  {
9548
9832
  trace_id: isString,
9549
9833
  first_timestamp: isString,
@@ -9558,7 +9842,7 @@ function buildAuditCommand() {
9558
9842
  if (format === "table" || format === "jsonl") {
9559
9843
  emit(format, traces, TRACE_COLUMNS);
9560
9844
  } else {
9561
- emit(format, normalizeAuditEnvelope(result, "traces"));
9845
+ emit(format, result);
9562
9846
  }
9563
9847
  });
9564
9848
  }
@@ -9600,7 +9884,13 @@ var DEFAULT_SCOPES = [
9600
9884
  // ``:delete`` and ``:admin`` are deliberate opt-ins via ``--scopes``
9601
9885
  // per the destructive-action policy in CLAUDE.md.
9602
9886
  "dashboard_secrets:read",
9603
- "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"
9604
9894
  ];
9605
9895
  function deriveDashboardUrl(baseUrl) {
9606
9896
  return baseUrl.replace(/^https:\/\/backend\./, "https://portal.");
@@ -9890,7 +10180,17 @@ var DASHBOARD_RESOURCE_VERBS = {
9890
10180
  // grant + delegation + audit anchor and vault-deletes the stored
9891
10181
  // credential (irrecoverable). Backend catalog v3, see
9892
10182
  // ``apps/backend/app/core/scopes/catalog.py`` _DASHBOARD_RESOURCES_V3.
9893
- 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"]
9894
10194
  };
9895
10195
  function parseScopesFlag(raw) {
9896
10196
  if (typeof raw !== "string") {
@@ -10538,8 +10838,148 @@ function buildCompletionCommand() {
10538
10838
  return completion;
10539
10839
  }
10540
10840
 
10541
- // src/commands/keys.ts
10841
+ // src/commands/grants.ts
10542
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";
10543
10983
  var KEY_TYPE_MAP = {
10544
10984
  runtime: "rk",
10545
10985
  agent: "ak"
@@ -10553,7 +10993,7 @@ var KEY_COLUMNS2 = [
10553
10993
  { label: "STATUS", get: (k) => k.revoked_at ? "revoked" : "active" }
10554
10994
  ];
10555
10995
  function buildKeysCommand() {
10556
- const keys = new Command6("keys").description("Manage scoped API keys");
10996
+ const keys = new Command7("keys").description("Manage scoped API keys");
10557
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(
10558
10998
  "--output <format>",
10559
10999
  "Output format: json|jsonl|table (default: table)",
@@ -10775,9 +11215,9 @@ function buildKeysCommand() {
10775
11215
  // src/commands/link.ts
10776
11216
  import { existsSync as existsSync4 } from "fs";
10777
11217
  import { join as join5 } from "path";
10778
- import { Command as Command7 } from "commander";
11218
+ import { Command as Command8 } from "commander";
10779
11219
  function buildLinkCommand() {
10780
- const link = new Command7("link").description(
11220
+ const link = new Command8("link").description(
10781
11221
  "Pin the current directory tree to a default app_id so subsequent commands (``keys``, ``agents``, ``providers``, ``policy``) don't need --app."
10782
11222
  ).argument("[app-id]", "App ID to pin. Omitted when --status is passed.").option(
10783
11223
  "--force",
@@ -10805,7 +11245,7 @@ function buildLinkCommand() {
10805
11245
  return link;
10806
11246
  }
10807
11247
  function buildUnlinkCommand() {
10808
- 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 () => {
10809
11249
  const removed = removeWorkspaceConfig();
10810
11250
  if (removed) {
10811
11251
  process.stdout.write(
@@ -10893,7 +11333,7 @@ async function runStatus() {
10893
11333
 
10894
11334
  // src/commands/managed-secrets.ts
10895
11335
  import { readFileSync as readFileSync6 } from "fs";
10896
- import { Command as Command8, Option } from "commander";
11336
+ import { Command as Command9, Option } from "commander";
10897
11337
  var PRINCIPAL_TYPES = ["user", "group", "system", "agent"];
10898
11338
  var CREDENTIAL_TYPES = [
10899
11339
  "bearer_token",
@@ -11247,7 +11687,7 @@ var SECRET_COLUMNS = [
11247
11687
  { label: "GRANTS", get: (s) => String(s.grant_count) },
11248
11688
  { label: "CREATED", get: (s) => s.created_at }
11249
11689
  ];
11250
- var GRANT_COLUMNS = [
11690
+ var GRANT_COLUMNS2 = [
11251
11691
  { label: "GRANT_ID", get: (g) => g.grant_id, maxWidth: 36 },
11252
11692
  { label: "PRINCIPAL", get: (g) => g.principal_type },
11253
11693
  { label: "LABEL", get: (g) => g.label ?? "\u2014", maxWidth: 24 },
@@ -11279,7 +11719,7 @@ var ACCESS_COLUMNS = [
11279
11719
  { label: "AGENT", get: (a) => a.via_agent_name ?? "\u2014" }
11280
11720
  ];
11281
11721
  function buildGrantsSubcommand() {
11282
- const grants = new Command8("grants").description(
11722
+ const grants = new Command9("grants").description(
11283
11723
  "Manage grants on managed secrets (list / create / update / revoke)."
11284
11724
  );
11285
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(
@@ -11317,7 +11757,7 @@ function buildGrantsSubcommand() {
11317
11757
  created_at: isString,
11318
11758
  expires_at: isOptionalString
11319
11759
  }, "managed-secrets.grants.list");
11320
- emit(format, rows, GRANT_COLUMNS);
11760
+ emit(format, rows, GRANT_COLUMNS2);
11321
11761
  });
11322
11762
  }
11323
11763
  );
@@ -11347,7 +11787,7 @@ function buildGrantsSubcommand() {
11347
11787
  created_at: isString,
11348
11788
  expires_at: isOptionalString
11349
11789
  }, "managed-secrets.grants.list-for-agent");
11350
- emit(format, rows, GRANT_COLUMNS);
11790
+ emit(format, rows, GRANT_COLUMNS2);
11351
11791
  });
11352
11792
  }
11353
11793
  );
@@ -11551,7 +11991,7 @@ function buildGrantsSubcommand() {
11551
11991
  return grants;
11552
11992
  }
11553
11993
  function buildGroupsSubcommand() {
11554
- const groups = new Command8("groups").description(
11994
+ const groups = new Command9("groups").description(
11555
11995
  "App-user-group autocomplete + group detail helpers."
11556
11996
  );
11557
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(
@@ -11623,7 +12063,7 @@ function buildGroupsSubcommand() {
11623
12063
  return groups;
11624
12064
  }
11625
12065
  function buildManagedSecretsCommand() {
11626
- const root = new Command8("managed-secrets").description(
12066
+ const root = new Command9("managed-secrets").description(
11627
12067
  "Manage managed-secret credentials, grants, and access (CRUD + autocomplete helpers)."
11628
12068
  );
11629
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(
@@ -11694,7 +12134,7 @@ function buildManagedSecretsCommand() {
11694
12134
  root.command("create").description("Create a managed secret + base grant").option("--app <app-id>", "App ID").option(
11695
12135
  "--input <path>",
11696
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."
11697
- ).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(
11698
12138
  // ``--credential-type`` is enum-validated at RUNTIME (see the
11699
12139
  // per-field branch below) rather than parse-time so a typo'd
11700
12140
  // value alongside ``--input @body.json`` (which discards the
@@ -11739,7 +12179,6 @@ function buildManagedSecretsCommand() {
11739
12179
  "--input"
11740
12180
  );
11741
12181
  const perField = [
11742
- "slug",
11743
12182
  "name",
11744
12183
  "description",
11745
12184
  "template",
@@ -11772,12 +12211,6 @@ function buildManagedSecretsCommand() {
11772
12211
  });
11773
12212
  return;
11774
12213
  }
11775
- if (options.slug === void 0) {
11776
- process.stderr.write(
11777
- "alter: managed-secrets create requires --slug (or use --input @file.json to supply the full body)\n"
11778
- );
11779
- process.exit(EXIT_USAGE);
11780
- }
11781
12214
  if (options.name === void 0) {
11782
12215
  process.stderr.write(
11783
12216
  "alter: managed-secrets create requires --name (or use --input @file.json to supply the full body)\n"
@@ -11790,7 +12223,6 @@ function buildManagedSecretsCommand() {
11790
12223
  CREDENTIAL_TYPES
11791
12224
  );
11792
12225
  validateNoControlCharsOrExit("--name", options.name);
11793
- validateNoControlCharsOrExit("--slug", options.slug);
11794
12226
  if (options.description !== void 0) {
11795
12227
  validateNoControlCharsOrExit("--description", options.description);
11796
12228
  }
@@ -11814,7 +12246,6 @@ function buildManagedSecretsCommand() {
11814
12246
  const additionalCredentials = loadedCredentials !== void 0 ? loadedCredentials.additionalCredentials : options.credentialField;
11815
12247
  const additionalInjections = options.injectionRule !== void 0 ? loadInjectionRules(options.injectionRule) : void 0;
11816
12248
  const body = {
11817
- slug: options.slug,
11818
12249
  name: options.name,
11819
12250
  credential_type: options.credentialType,
11820
12251
  credential_value: credentialValue
@@ -12073,7 +12504,7 @@ function buildManagedSecretsCommand() {
12073
12504
  }
12074
12505
 
12075
12506
  // src/commands/pats.ts
12076
- import { Command as Command9 } from "commander";
12507
+ import { Command as Command10 } from "commander";
12077
12508
  async function whoami(options) {
12078
12509
  const client = await createPortalClient();
12079
12510
  let exitCode = 0;
@@ -12095,7 +12526,7 @@ async function whoami(options) {
12095
12526
  }
12096
12527
  }
12097
12528
  function buildPatsCommand() {
12098
- const pats = new Command9("pats").description("Inspect Personal Access Tokens");
12529
+ const pats = new Command10("pats").description("Inspect Personal Access Tokens");
12099
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) => {
12100
12531
  const format = coerceOutputFormat(options.output);
12101
12532
  await whoami({ output: format });
@@ -12104,9 +12535,9 @@ function buildPatsCommand() {
12104
12535
  }
12105
12536
 
12106
12537
  // src/commands/policy.ts
12107
- import { Command as Command10 } from "commander";
12538
+ import { Command as Command11 } from "commander";
12108
12539
  function buildPolicyCommand() {
12109
- const policy = new Command10("policy").description(
12540
+ const policy = new Command11("policy").description(
12110
12541
  "Read app-level policy (read-only \u2014 edits via dashboard)"
12111
12542
  );
12112
12543
  policy.command("show-app").description(
@@ -12123,7 +12554,7 @@ function buildPolicyCommand() {
12123
12554
 
12124
12555
  // src/commands/providers.ts
12125
12556
  import { readFileSync as readFileSync7 } from "fs";
12126
- import { Command as Command11 } from "commander";
12557
+ import { Command as Command12 } from "commander";
12127
12558
  var CREDENTIAL_SOURCES = ["custom", "shared_dev"];
12128
12559
  var PROVIDER_STATUSES = ["active", "disabled"];
12129
12560
  var PROVIDER_COLUMNS = [
@@ -12161,8 +12592,48 @@ function collectRedirectUris(value, previous = []) {
12161
12592
  const next = value.split(",").map((s) => s.trim()).filter(Boolean);
12162
12593
  return [...previous, ...next];
12163
12594
  }
12595
+ function sanitizeStderrText(value) {
12596
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ").trim();
12597
+ }
12598
+ function surfaceProviderResponse(row) {
12599
+ if (typeof row !== "object" || row === null) return;
12600
+ const r = row;
12601
+ const preflight = r.preflight;
12602
+ if (preflight && typeof preflight === "object") {
12603
+ const status = preflight.status;
12604
+ const msg = typeof preflight.message === "string" ? sanitizeStderrText(preflight.message) : "";
12605
+ if (status === "passed") {
12606
+ process.stderr.write("alter: preflight: passed\n");
12607
+ } else if (status === "failed") {
12608
+ process.stderr.write(
12609
+ `alter: preflight: failed${msg ? ` \u2014 ${msg}` : ""}
12610
+ `
12611
+ );
12612
+ } else if (status === "inconclusive") {
12613
+ process.stderr.write(
12614
+ `alter: preflight: inconclusive${msg ? ` \u2014 ${msg}` : ""} (save was allowed)
12615
+ `
12616
+ );
12617
+ }
12618
+ }
12619
+ const bc = r.breaking_changes;
12620
+ if (Array.isArray(bc) && bc.length > 0) {
12621
+ process.stderr.write(
12622
+ `alter: warning: ${bc.length} breaking change(s) \u2014 ${sanitizeStderrText(JSON.stringify(bc))}
12623
+ `
12624
+ );
12625
+ }
12626
+ const warning = r.warning;
12627
+ if (typeof warning === "string") {
12628
+ const sanitized = sanitizeStderrText(warning);
12629
+ if (sanitized.length > 0) {
12630
+ process.stderr.write(`alter: warning: ${sanitized}
12631
+ `);
12632
+ }
12633
+ }
12634
+ }
12164
12635
  function buildProvidersCommand() {
12165
- const providers = new Command11("providers").description(
12636
+ const providers = new Command12("providers").description(
12166
12637
  "Manage OAuth provider configs"
12167
12638
  );
12168
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(
@@ -12251,6 +12722,7 @@ function buildProvidersCommand() {
12251
12722
  appId,
12252
12723
  body
12253
12724
  );
12725
+ surfaceProviderResponse(row);
12254
12726
  emit(format, row);
12255
12727
  });
12256
12728
  return;
@@ -12290,6 +12762,7 @@ function buildProvidersCommand() {
12290
12762
  scopes,
12291
12763
  redirect_uris: redirects
12292
12764
  });
12765
+ surfaceProviderResponse(row);
12293
12766
  emit(format, row);
12294
12767
  });
12295
12768
  }
@@ -12336,6 +12809,7 @@ function buildProvidersCommand() {
12336
12809
  options.provider,
12337
12810
  body
12338
12811
  );
12812
+ surfaceProviderResponse(row);
12339
12813
  emit(format, row);
12340
12814
  });
12341
12815
  return;
@@ -12362,6 +12836,7 @@ function buildProvidersCommand() {
12362
12836
  redirect_uris: redirects,
12363
12837
  status: providerStatus
12364
12838
  });
12839
+ surfaceProviderResponse(row);
12365
12840
  emit(format, row);
12366
12841
  });
12367
12842
  }
@@ -12395,9 +12870,9 @@ function buildProvidersCommand() {
12395
12870
  }
12396
12871
 
12397
12872
  // src/commands/sdk-passthrough.ts
12398
- import { Command as Command12 } from "commander";
12873
+ import { Command as Command13 } from "commander";
12399
12874
  function buildSdkPassthroughCommand() {
12400
- const cmd = new Command12("sdk-passthrough").description(
12875
+ const cmd = new Command13("sdk-passthrough").description(
12401
12876
  "Escape hatch for ad-hoc runtime SDK calls (vault.request). Uses a runtime API key, NOT a PAT."
12402
12877
  );
12403
12878
  cmd.command("request <grant-id>").description(
@@ -12526,7 +13001,7 @@ function buildSdkPassthroughCommand() {
12526
13001
  // src/commands/self-update.ts
12527
13002
  import { spawnSync } from "child_process";
12528
13003
  import { platform as platform4 } from "os";
12529
- import { Command as Command13 } from "commander";
13004
+ import { Command as Command14 } from "commander";
12530
13005
  var CLI_PACKAGE = "@alter-ai/cli";
12531
13006
  function runNpmInstall(spec) {
12532
13007
  const result = spawnSync(
@@ -12581,7 +13056,7 @@ function probeNpmInstall() {
12581
13056
  };
12582
13057
  }
12583
13058
  function buildSelfUpdateCommand() {
12584
- 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(
12585
13060
  "--dry-run",
12586
13061
  "Print the npm install command that would run, without executing it"
12587
13062
  ).action(async (options) => {
@@ -12636,7 +13111,7 @@ function buildSelfUpdateCommand() {
12636
13111
 
12637
13112
  // src/program.ts
12638
13113
  function buildProgram() {
12639
- const program2 = new Command14();
13114
+ const program2 = new Command15();
12640
13115
  program2.name("alter").description("Alter Vault command-line interface").version(package_default.version);
12641
13116
  program2.option(
12642
13117
  "--fields <list>",
@@ -12665,6 +13140,7 @@ function buildProgram() {
12665
13140
  program2.addCommand(buildManagedSecretsCommand());
12666
13141
  program2.addCommand(buildPolicyCommand());
12667
13142
  program2.addCommand(buildAuditCommand());
13143
+ program2.addCommand(buildGrantsCommand());
12668
13144
  program2.addCommand(buildPatsCommand());
12669
13145
  program2.addCommand(buildLinkCommand());
12670
13146
  program2.addCommand(buildUnlinkCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alter-ai/cli",
3
- "version": "0.3.1",
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"