@alter-ai/cli 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -33,7 +33,7 @@ import { join } from "path";
33
33
  // package.json
34
34
  var package_default = {
35
35
  name: "@alter-ai/cli",
36
- version: "0.9.0",
36
+ version: "0.9.1",
37
37
  description: "Command-line interface for the Alter Vault dev portal \u2014 scripted dashboard automation.",
38
38
  type: "module",
39
39
  bin: {
@@ -44,7 +44,8 @@ var package_default = {
44
44
  "dist",
45
45
  "README.md",
46
46
  "LICENSE",
47
- "THIRD_PARTY_NOTICES"
47
+ "THIRD_PARTY_NOTICES",
48
+ "npm-shrinkwrap.json"
48
49
  ],
49
50
  scripts: {
50
51
  build: "tsup",
@@ -69,14 +70,17 @@ var package_default = {
69
70
  author: "Alter Labs, Inc.",
70
71
  license: "MIT",
71
72
  dependencies: {
72
- commander: "^12.0.0",
73
- "posthog-node": "^4.18.0"
73
+ commander: "12.1.0",
74
+ "posthog-node": "4.18.0"
74
75
  },
75
76
  optionalDependencies: {
76
- keytar: "^7.9.0"
77
+ keytar: "7.9.0"
78
+ },
79
+ overrides: {
80
+ esbuild: "0.28.1"
77
81
  },
78
82
  devDependencies: {
79
- "@alter-ai/alter-sdk": "workspace:0.24.0",
83
+ "@alter-ai/alter-sdk": "workspace:0.24.2",
80
84
  "@alter-vault/shared-types": "workspace:0.0.1",
81
85
  "@alter-vault/shared-utils": "workspace:0.0.1",
82
86
  "@eslint/js": "9.39.4",
@@ -6255,7 +6259,11 @@ var ConnectResult = class {
6255
6259
  providerId;
6256
6260
  accountIdentifier;
6257
6261
  scopes;
6258
- /** Ordinary Connect completion or in-place repair of a broken grant. */
6262
+ /**
6263
+ * "creation" when a new grant was minted; "reauth" when an existing
6264
+ * grant was returned with its id preserved (credential repair, or reuse
6265
+ * of an already-connected grant).
6266
+ */
6259
6267
  operation;
6260
6268
  grantPolicy;
6261
6269
  /**
@@ -8143,7 +8151,7 @@ async function _raiseForStatus(response) {
8143
8151
  if (code === AgentConcurrentUpdateError.code && response.status === 409) {
8144
8152
  throw new AgentConcurrentUpdateError({ message, details: body, hint });
8145
8153
  }
8146
- const ExcCtor = ERROR_CODE_STATUS_MAP[`${code}:${response.status}`] ?? ERROR_CODE_MAP[code];
8154
+ const ExcCtor = ERROR_CODE_STATUS_MAP[`${code}:${response.status}`] ?? (Object.hasOwn(ERROR_CODE_MAP, code) ? ERROR_CODE_MAP[code] : void 0);
8147
8155
  if (ExcCtor !== void 0) {
8148
8156
  throw new ExcCtor({ message, details: body, hint });
8149
8157
  }
@@ -10172,7 +10180,7 @@ function _extractAdditionalCredentials(token) {
10172
10180
  return _additionalCredsStore.get(token);
10173
10181
  }
10174
10182
  var _fetch;
10175
- var SDK_VERSION = "0.24.0";
10183
+ var SDK_VERSION = "0.24.2";
10176
10184
  var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
10177
10185
  function pyUnquote(s) {
10178
10186
  if (!s.includes("%")) return s;
@@ -10243,8 +10251,12 @@ function canonicalSigningQuery(rawQuery) {
10243
10251
  pairs.sort((p, q) => byCodePoint(p[0], q[0]) || byCodePoint(p[1], q[1]));
10244
10252
  return pairs.map(([k, v]) => `${pyQuotePlus(k)}=${pyQuotePlus(v)}`).join("&");
10245
10253
  }
10254
+ var DEFAULT_TIMEOUT_MS = 95e3;
10255
+ var _ApprovalDeadlineElapsed = class extends Error {
10256
+ };
10246
10257
  var AUTH_POLL_SERVER_WAIT_MS = 25e3;
10247
10258
  var AUTH_POLL_HTTP_BUFFER_MS = 15e3;
10259
+ var APPROVAL_POLL_SERVER_WAIT_MS = 25e3;
10248
10260
  var PERMANENT_POLL_STATUSES = /* @__PURE__ */ new Set([400, 401, 403, 404, 422]);
10249
10261
  var HTTP_FORBIDDEN = 403;
10250
10262
  var HTTP_NO_CONTENT2 = 204;
@@ -10520,6 +10532,23 @@ function validateAndSerializeContext(context) {
10520
10532
  return encoded;
10521
10533
  }
10522
10534
  var MAX_BODY_SIZE_BYTES = 1e4;
10535
+ var AUDIT_REFUSAL_BODY_MAX_CHARS = 65536;
10536
+ var REFUSAL_MESSAGE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
10537
+ var REFUSAL_MESSAGE_MAX_CHARS = 300;
10538
+ function sanitizeRefusalMessage(message) {
10539
+ const cleaned = message.replace(REFUSAL_MESSAGE_CONTROL_CHARS, " ");
10540
+ return cleaned.length > REFUSAL_MESSAGE_MAX_CHARS ? `${cleaned.slice(0, REFUSAL_MESSAGE_MAX_CHARS)}\u2026` : cleaned;
10541
+ }
10542
+ function _redactUrlForLog(url2) {
10543
+ try {
10544
+ const parsed = new URL(url2);
10545
+ if (parsed.protocol && parsed.hostname) {
10546
+ return `${parsed.protocol}//${parsed.hostname}`;
10547
+ }
10548
+ } catch {
10549
+ }
10550
+ return "<unparseable-url>";
10551
+ }
10523
10552
  var HTTP_CLIENT_ERROR_START = 400;
10524
10553
  var MAX_ACTOR_STRING_LENGTH = 255;
10525
10554
  var SAFE_HEADER_PATTERN = /^[\x20-\x7E]+$/;
@@ -10808,6 +10837,16 @@ var HttpClient = class {
10808
10837
  new DOMException("The operation timed out.", "TimeoutError")
10809
10838
  );
10810
10839
  }, effectiveTimeoutMs);
10840
+ const callerSignal = options?.signal;
10841
+ let onCallerAbort;
10842
+ if (callerSignal !== void 0) {
10843
+ if (callerSignal.aborted) {
10844
+ controller.abort(callerSignal.reason);
10845
+ } else {
10846
+ onCallerAbort = () => controller.abort(callerSignal.reason);
10847
+ callerSignal.addEventListener("abort", onCallerAbort);
10848
+ }
10849
+ }
10811
10850
  const init = {
10812
10851
  method,
10813
10852
  headers: mergedHeaders,
@@ -10846,6 +10885,9 @@ var HttpClient = class {
10846
10885
  return response;
10847
10886
  } finally {
10848
10887
  clearTimeout(timeoutId);
10888
+ if (callerSignal !== void 0 && onCallerAbort !== void 0) {
10889
+ callerSignal.removeEventListener("abort", onCallerAbort);
10890
+ }
10849
10891
  }
10850
10892
  }
10851
10893
  /**
@@ -10958,7 +11000,7 @@ var _VaultClient = class __VaultClient {
10958
11000
  unresolvedBaseUrl,
10959
11001
  options.logger ?? console
10960
11002
  );
10961
- const timeoutMs = options.timeout ?? 3e4;
11003
+ const timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;
10962
11004
  this.#caller = options.caller;
10963
11005
  const rawCallerType = options.callerType ?? "agent";
10964
11006
  if (rawCallerType !== "agent" && rawCallerType !== "service") {
@@ -12163,9 +12205,34 @@ ${label}:${value}`;
12163
12205
  });
12164
12206
  this.#cacheActorIdFromResponse(response);
12165
12207
  if (!response.ok) {
12208
+ let detail = "";
12209
+ try {
12210
+ const raw = await response.text();
12211
+ if (raw.length <= AUDIT_REFUSAL_BODY_MAX_CHARS) {
12212
+ const body = JSON.parse(raw);
12213
+ let message = body.message;
12214
+ if (typeof message !== "string" || message === "") {
12215
+ const inner = body.detail;
12216
+ if (typeof inner === "string") {
12217
+ message = inner;
12218
+ } else if (inner !== null && typeof inner === "object") {
12219
+ message = inner.message;
12220
+ }
12221
+ }
12222
+ if (typeof message === "string" && message !== "") {
12223
+ detail = `: ${sanitizeRefusalMessage(message)}`;
12224
+ }
12225
+ }
12226
+ } catch {
12227
+ }
12166
12228
  this.#logger.warn(
12167
- `Audit log failed with status ${response.status} (non-fatal)`
12229
+ `Audit log failed with status ${response.status} (non-fatal)${detail} \u2014 this call's api-call audit row was not recorded (grant_id=${params.grantId}, method=${params.method}, url=${_redactUrlForLog(params.url)})`
12168
12230
  );
12231
+ } else {
12232
+ try {
12233
+ await response.arrayBuffer();
12234
+ } catch {
12235
+ }
12169
12236
  }
12170
12237
  } catch (error51) {
12171
12238
  this.#logger.warn(
@@ -12676,7 +12743,7 @@ ${label}:${value}`;
12676
12743
  headers: hmacHeaders,
12677
12744
  // Cap the best-effort background report at 2s (parity with the Python
12678
12745
  // SDK's asyncio.wait_for). Without an override it would inherit the
12679
- // instance timeout (default 30s), so a hung backend could keep the
12746
+ // instance timeout (default 95s), so a hung backend could keep the
12680
12747
  // fire-and-forget task — and close()'s drain of it — alive far longer
12681
12748
  // than the Python SDK does. Next provider 403 re-reports (self-healing).
12682
12749
  timeoutMs: 2e3
@@ -13053,8 +13120,11 @@ ${label}:${value}`;
13053
13120
  * @param options - Optional revocation options (reason for audit log).
13054
13121
  * @returns RevokeGrantResult confirming the revocation.
13055
13122
  * @throws {GrantNotFoundError} If the grant does not exist, is not
13056
- * active, or (when a user token is configured) does not belong to
13057
- * the calling user.
13123
+ * active, (when a user token is configured) does not belong to
13124
+ * the calling user, or — when this client holds an AGENT key — is
13125
+ * not a grant the agent is the principal of (its own delegation or
13126
+ * an agent-owned grant). An allowlist entry for the grant's provider
13127
+ * is never enough.
13058
13128
  * @throws {NetworkError} If connection to backend fails.
13059
13129
  * @throws {TimeoutError} If request to backend times out.
13060
13130
  */
@@ -13865,14 +13935,17 @@ ${label}:${value}`;
13865
13935
  * @returns One `ConnectResult` per provider the user completed
13866
13936
  * within the session (multi-provider Connect sessions yield
13867
13937
  * multiple results).
13868
- * @throws AlterValueError if `sessionToken` is blank, or `timeoutMs` /
13869
- * `pollIntervalMs` is non-finite or not greater than zero.
13938
+ * @throws AlterValueError if `sessionToken` is blank, `timeoutMs` /
13939
+ * `pollIntervalMs` is non-finite or not greater than zero, or
13940
+ * `onEvent` is provided but not a function.
13870
13941
  * @throws ConnectTimeoutError if the local `timeoutMs` elapses, or a
13871
13942
  * session observed as pending expires before Alter receives a
13872
13943
  * completion callback. `details.reason` distinguishes
13873
13944
  * `poll_deadline_elapsed`, `session_expired`, and `rate_limited` (the
13874
13945
  * whole budget was spent backing off a throttled poll —
13875
- * `details.retryAfter` carries the last hint the server gave).
13946
+ * `details.retryAfter` carries the last hint the server gave). When
13947
+ * denials were observed before the timeout,
13948
+ * `details.declined_providers` lists them on EVERY variant.
13876
13949
  * @throws ConnectFlowError / ConnectDeniedError / ConnectConfigError
13877
13950
  * for user denial, a first poll that finds an unavailable/expired
13878
13951
  * session, unrecognized status, or a completed session whose every grant
@@ -13892,8 +13965,18 @@ ${label}:${value}`;
13892
13965
  const timeoutMs = options?.timeoutMs ?? 3e5;
13893
13966
  const pollIntervalMs = options?.pollIntervalMs ?? 2e3;
13894
13967
  validatePollingInputs(sessionToken, timeoutMs, pollIntervalMs);
13968
+ if (options?.onEvent !== void 0 && typeof options.onEvent !== "function") {
13969
+ throw new AlterValueError("onEvent must be a function when provided");
13970
+ }
13895
13971
  const deadline = performance.now() + timeoutMs;
13896
13972
  let sawPending = false;
13973
+ const seenDeclinedProviders = /* @__PURE__ */ new Set();
13974
+ const withDeclined = (base) => {
13975
+ if (seenDeclinedProviders.size > 0) {
13976
+ base.declined_providers = [...seenDeclinedProviders].sort();
13977
+ }
13978
+ return base;
13979
+ };
13897
13980
  while (true) {
13898
13981
  this.#assertNotClosed();
13899
13982
  let pollResult;
@@ -13907,11 +13990,11 @@ ${label}:${value}`;
13907
13990
  if (remaining2 <= 0) {
13908
13991
  throw new ConnectTimeoutError(
13909
13992
  `OAuth flow did not complete within ${Math.round(timeoutMs / 1e3)} seconds: polling was rate-limited for the remainder of the budget. The authorization may still have succeeded \u2014 retry the poll with a fresh timeout, or raise the API key's per-minute rate limit if Connect flows run concurrently.`,
13910
- {
13993
+ withDeclined({
13911
13994
  timeoutMs,
13912
13995
  reason: "rate_limited",
13913
13996
  retryAfter: e.retryAfter
13914
- }
13997
+ })
13915
13998
  );
13916
13999
  }
13917
14000
  let backoffMs = pollIntervalMs;
@@ -14044,7 +14127,10 @@ ${label}:${value}`;
14044
14127
  ].includes(errorCode)) {
14045
14128
  throw new ConnectConfigError(errorMessage, errorDetails);
14046
14129
  }
14047
- const identitySyncStatus = IDENTITY_SYNC_POLL_STATUS[errorCode];
14130
+ const identitySyncStatus = Object.hasOwn(
14131
+ IDENTITY_SYNC_POLL_STATUS,
14132
+ errorCode
14133
+ ) ? IDENTITY_SYNC_POLL_STATUS[errorCode] : void 0;
14048
14134
  if (identitySyncStatus !== void 0) {
14049
14135
  throw new BackendError(
14050
14136
  errorMessage,
@@ -14058,7 +14144,7 @@ ${label}:${value}`;
14058
14144
  if (sawPending) {
14059
14145
  throw new ConnectTimeoutError(
14060
14146
  "Connect session expired before authorization returned to Alter. The provider may have shown an error without redirecting, the browser may have been closed, or the user may not have completed the flow.",
14061
- { timeoutMs, reason: "session_expired" }
14147
+ withDeclined({ timeoutMs, reason: "session_expired" })
14062
14148
  );
14063
14149
  }
14064
14150
  throw new ConnectFlowError(
@@ -14071,6 +14157,46 @@ ${label}:${value}`;
14071
14157
  { status: pollStatus }
14072
14158
  );
14073
14159
  }
14160
+ const rawDeclined = pollResult.declined_providers;
14161
+ if (Array.isArray(rawDeclined)) {
14162
+ for (const entry of rawDeclined) {
14163
+ if (entry === null || typeof entry !== "object") continue;
14164
+ const declinedProvider = entry.provider_id;
14165
+ if (typeof declinedProvider !== "string" || declinedProvider === "") {
14166
+ continue;
14167
+ }
14168
+ const rawCode = entry.error_code;
14169
+ if (typeof rawCode !== "string" || rawCode === "") continue;
14170
+ if (seenDeclinedProviders.has(declinedProvider)) continue;
14171
+ seenDeclinedProviders.add(declinedProvider);
14172
+ if (!options?.onEvent) continue;
14173
+ const rawDeclinedAt = entry.declined_at;
14174
+ const logCallbackFailure = (error51) => {
14175
+ this.#logger.warn(
14176
+ "pollConnectSession onEvent callback threw (non-fatal)",
14177
+ {
14178
+ provider_id: declinedProvider,
14179
+ error_type: error51 instanceof Error ? error51.constructor.name : typeof error51
14180
+ }
14181
+ );
14182
+ };
14183
+ try {
14184
+ const result = options.onEvent({
14185
+ type: "provider_declined",
14186
+ provider_id: declinedProvider,
14187
+ error_code: rawCode,
14188
+ declined_at: typeof rawDeclinedAt === "string" ? rawDeclinedAt : null
14189
+ });
14190
+ if (result !== null && (typeof result === "object" || typeof result === "function") && typeof result.then === "function") {
14191
+ void Promise.resolve(result).catch(
14192
+ logCallbackFailure
14193
+ );
14194
+ }
14195
+ } catch (error51) {
14196
+ logCallbackFailure(error51);
14197
+ }
14198
+ }
14199
+ }
14074
14200
  sawPending = true;
14075
14201
  const remaining = deadline - performance.now();
14076
14202
  if (remaining <= 0) break;
@@ -14080,7 +14206,7 @@ ${label}:${value}`;
14080
14206
  }
14081
14207
  throw new ConnectTimeoutError(
14082
14208
  `OAuth flow did not complete within ${Math.round(timeoutMs / 1e3)} seconds. Alter did not receive a completion callback; the provider may have shown an error without redirecting, the browser may have been closed, or the user may not have finished authorizing.`,
14083
- { timeoutMs, reason: "poll_deadline_elapsed" }
14209
+ withDeclined({ timeoutMs, reason: "poll_deadline_elapsed" })
14084
14210
  );
14085
14211
  }
14086
14212
  /**
@@ -14333,7 +14459,10 @@ ${label}:${value}`;
14333
14459
  const errorMessage = typeof pollData.error_message === "string" && pollData.error_message ? pollData.error_message : "unknown error";
14334
14460
  const message = `Authentication failed: ${errorMessage}`;
14335
14461
  const syncCode = typeof pollData.error_code === "string" ? pollData.error_code : "";
14336
- const identitySyncStatus = IDENTITY_SYNC_POLL_STATUS[syncCode];
14462
+ const identitySyncStatus = Object.hasOwn(
14463
+ IDENTITY_SYNC_POLL_STATUS,
14464
+ syncCode
14465
+ ) ? IDENTITY_SYNC_POLL_STATUS[syncCode] : void 0;
14337
14466
  if (identitySyncStatus !== void 0) {
14338
14467
  const details = { error_code: syncCode };
14339
14468
  const retryAfter = _coerceInt(pollData.retry_after_seconds);
@@ -14958,10 +15087,34 @@ ${label}:${value}`;
14958
15087
  error: errCode
14959
15088
  });
14960
15089
  }
14961
- /** Single-shot poll of an approval. */
14962
- async getApprovalStatus(approvalId) {
15090
+ /**
15091
+ * Single-shot poll of an approval.
15092
+ *
15093
+ * `wait` asks the server to hold the request open until the approval's
15094
+ * status changes, up to that many seconds (0 = return immediately, the
15095
+ * default and the historical behaviour). It is a pure latency optimisation:
15096
+ * the response shape is identical either way, and a backend that predates
15097
+ * the parameter simply ignores it and answers immediately. The server caps
15098
+ * `wait` at 25 seconds (`APPROVAL_POLL_SERVER_WAIT_MS`, mirroring the
15099
+ * backend's `MAX_LONG_POLL_SECONDS`) and REJECTS larger values with a
15100
+ * validation error (422) rather than clamping them.
15101
+ *
15102
+ * `signal` (internal — not exposed on App/Agent) lets `awaitApproval` abort
15103
+ * an in-flight held request its local deadline has abandoned.
15104
+ *
15105
+ * @throws AlterValueError if `wait` is not a finite number.
15106
+ */
15107
+ async getApprovalStatus(approvalId, options = {}) {
14963
15108
  this.#assertNotClosed();
14964
- const sdkPath = `/sdk/approvals/${approvalId}`;
15109
+ if (options !== null && options !== void 0 && typeof options !== "object") {
15110
+ throw new AlterValueError("options must be an object");
15111
+ }
15112
+ const waitOption = options?.wait ?? 0;
15113
+ if (typeof waitOption !== "number" || !Number.isFinite(waitOption)) {
15114
+ throw new AlterValueError("wait must be a finite number");
15115
+ }
15116
+ const wait = Math.max(0, Math.floor(waitOption));
15117
+ const sdkPath = wait > 0 ? `/sdk/approvals/${approvalId}?wait=${wait}` : `/sdk/approvals/${approvalId}`;
14965
15118
  const hmacHeaders = this.#computeHmacHeaders("GET", sdkPath, "");
14966
15119
  const traceparent = await ambientTraceparent();
14967
15120
  if (traceparent !== void 0) {
@@ -14970,7 +15123,18 @@ ${label}:${value}`;
14970
15123
  let response;
14971
15124
  try {
14972
15125
  response = await this.#alterClient.request("GET", sdkPath, {
14973
- headers: hmacHeaders
15126
+ headers: hmacHeaders,
15127
+ // A held request needs a wider per-request timeout than the client
15128
+ // default, or the client aborts its own long-poll (see
15129
+ // AUTH_POLL_HTTP_BUFFER_MS). Only applied when actually parking.
15130
+ // Capped at the server's own ceiling: `wait` is deliberately NOT
15131
+ // clamped on the wire (the backend owns that contract and answers an
15132
+ // over-cap value with its documented validation error), but an
15133
+ // unbounded `wait * 1000` overflows to `Infinity` for a huge finite
15134
+ // value, and Node coerces such a timer to fire almost immediately —
15135
+ // aborting the request before that very error can come back.
15136
+ timeoutMs: wait > 0 ? Math.min(wait * 1e3, APPROVAL_POLL_SERVER_WAIT_MS) + AUTH_POLL_HTTP_BUFFER_MS : void 0,
15137
+ signal: options?.signal
14974
15138
  });
14975
15139
  } catch (error51) {
14976
15140
  if (error51 instanceof Error && error51.name === "AbortError") {
@@ -15041,9 +15205,39 @@ ${label}:${value}`;
15041
15205
  throw this.#buildApprovalTimeout(approvalId, timeoutMs, lastTransient);
15042
15206
  }
15043
15207
  let status;
15208
+ const raceController = new AbortController();
15209
+ let deadlineTimer;
15044
15210
  try {
15045
- status = await this.getApprovalStatus(approvalId);
15211
+ const serverWaitSeconds = Math.max(
15212
+ 0,
15213
+ Math.min(
15214
+ Math.floor(APPROVAL_POLL_SERVER_WAIT_MS / 1e3),
15215
+ Math.floor((deadline - Date.now()) / 1e3)
15216
+ )
15217
+ );
15218
+ const remainingMs = Math.max(0, deadline - Date.now());
15219
+ status = await Promise.race([
15220
+ this.getApprovalStatus(approvalId, {
15221
+ wait: serverWaitSeconds,
15222
+ signal: raceController.signal
15223
+ }),
15224
+ new Promise((_resolve, reject) => {
15225
+ deadlineTimer = setTimeout(
15226
+ () => reject(new _ApprovalDeadlineElapsed()),
15227
+ remainingMs
15228
+ );
15229
+ deadlineTimer.unref?.();
15230
+ })
15231
+ ]);
15046
15232
  } catch (err2) {
15233
+ if (err2 instanceof _ApprovalDeadlineElapsed) {
15234
+ raceController.abort();
15235
+ throw this.#buildApprovalTimeout(
15236
+ approvalId,
15237
+ timeoutMs,
15238
+ lastTransient
15239
+ );
15240
+ }
15047
15241
  if (!__VaultClient.#isTransientPollError(err2)) {
15048
15242
  throw err2;
15049
15243
  }
@@ -15065,6 +15259,8 @@ ${label}:${value}`;
15065
15259
  );
15066
15260
  await new Promise((res) => setTimeout(res, sleep2));
15067
15261
  continue;
15262
+ } finally {
15263
+ if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
15068
15264
  }
15069
15265
  lastTransient = null;
15070
15266
  if (status.status === "executed" && !status.hasResult) {
@@ -15880,8 +16076,8 @@ var Agent = class _Agent {
15880
16076
  return this.#client.delegate(grantId, toAgentId, options);
15881
16077
  }
15882
16078
  // ── Approvals ──────────────────────────────────────────────────────────
15883
- async getApprovalStatus(approvalId) {
15884
- return this.#client.getApprovalStatus(approvalId);
16079
+ async getApprovalStatus(approvalId, options = {}) {
16080
+ return this.#client.getApprovalStatus(approvalId, options);
15885
16081
  }
15886
16082
  async awaitApproval(...args) {
15887
16083
  return this.#client.awaitApproval(...args);
@@ -16301,8 +16497,8 @@ var App = class _App {
16301
16497
  });
16302
16498
  }
16303
16499
  // ── Approvals ──────────────────────────────────────────────────────────
16304
- async getApprovalStatus(approvalId) {
16305
- return this.#client.getApprovalStatus(approvalId);
16500
+ async getApprovalStatus(approvalId, options = {}) {
16501
+ return this.#client.getApprovalStatus(approvalId, options);
16306
16502
  }
16307
16503
  async awaitApproval(...args) {
16308
16504
  return this.#client.awaitApproval(...args);
@@ -16558,8 +16754,8 @@ async function _listGrantsUnified(client, body) {
16558
16754
  var DEFAULT_BASE_URL = "https://backend.alterauth.com";
16559
16755
  var PAT_API_PREFIX = "/api/v1/dev-portal";
16560
16756
  var HTTP_ERROR_THRESHOLD = 400;
16561
- var DEFAULT_TIMEOUT_MS = 3e4;
16562
- var CLI_VERSION = "0.9.0";
16757
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
16758
+ var CLI_VERSION = "0.9.1";
16563
16759
  var USER_AGENT = buildUserAgent();
16564
16760
  function buildUserAgent() {
16565
16761
  let osTag = "";
@@ -16804,7 +17000,7 @@ var DashboardClient = class {
16804
17000
  );
16805
17001
  }
16806
17002
  }
16807
- this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
17003
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
16808
17004
  this.pats = new PATsNamespace(this);
16809
17005
  this.apps = new AppsNamespace(this);
16810
17006
  this.keys = new KeysNamespace2(this);
@@ -19535,6 +19731,38 @@ function resolveAppId(flagValue, options = {}) {
19535
19731
  return null;
19536
19732
  }
19537
19733
 
19734
+ // ../shared-utils/src/dns-host.ts
19735
+ var LABEL_SEPARATOR = /[.。.。]/;
19736
+ var TRAILING_LABEL_SEPARATOR = /[.。.。]$/;
19737
+ var isInvalidLabel = (label) => label.length === 0 || label.includes("_") || label.startsWith("-") || label.endsWith("-") || label.length >= 4 && label[2] === "-" && label[3] === "-" && !label.startsWith("xn--");
19738
+ var IDNA2008_LABEL = /^(?:(?!\u0640|\u07FA|\u302E|\u302F|[\u3031-\u3035]|\u303B)[\p{L}\p{Mn}\p{Mc}\p{Nd}\-\u00DF\u03C2\u06FD\u06FE\u0F0B\u3007\u00B7\u0375\u05F3\u05F4\u30FB]|\u200C|\u200D)*$/u;
19739
+ var isOutsideIdna2008Repertoire = (label) => !IDNA2008_LABEL.test(label);
19740
+ var canonicalizeDnsHost = (host) => {
19741
+ let h = host;
19742
+ if (!/^[\x20-\x7e]*$/.test(h)) {
19743
+ if (/[/:@\s?#%\\]/.test(h)) return null;
19744
+ const uLabels = h.normalize("NFKC").replace(TRAILING_LABEL_SEPARATOR, "").split(LABEL_SEPARATOR);
19745
+ if (uLabels.some(isInvalidLabel)) return null;
19746
+ if (uLabels.some(isOutsideIdna2008Repertoire)) return null;
19747
+ let url2;
19748
+ try {
19749
+ url2 = new URL(`http://${h}`);
19750
+ } catch {
19751
+ return null;
19752
+ }
19753
+ if (url2.pathname !== "/" || url2.search !== "" || url2.hash !== "" || url2.port !== "" || url2.username !== "" || url2.password !== "") {
19754
+ return null;
19755
+ }
19756
+ h = url2.hostname;
19757
+ if (!/^[\x20-\x7e]*$/.test(h)) return null;
19758
+ h = h.replace(/\.$/, "");
19759
+ if (h.split(".").some(isInvalidLabel)) return null;
19760
+ }
19761
+ if (h.length > 253) return null;
19762
+ if (h.split(".").some((label) => label.length > 63)) return null;
19763
+ return h;
19764
+ };
19765
+
19538
19766
  // src/commands/_helpers.ts
19539
19767
  async function withClient(fn) {
19540
19768
  const client = await createPortalClient();
@@ -20087,9 +20315,9 @@ function parseAllowedHostsOrExit(flag, entries) {
20087
20315
  fail(`${JSON.stringify(entry)} must not contain control characters`);
20088
20316
  }
20089
20317
  }
20090
- if (/[/:@\s]/.test(e)) {
20318
+ if (/[/:@\s?#%\\]/.test(e)) {
20091
20319
  fail(
20092
- `${JSON.stringify(entry)} must be a bare host \u2014 no scheme, port, path, or "@"`
20320
+ `${JSON.stringify(entry)} must be a bare host \u2014 no scheme, port, path, "@", whitespace, or the URL delimiters ? # % \\`
20093
20321
  );
20094
20322
  }
20095
20323
  let normalized;
@@ -20100,12 +20328,23 @@ function parseAllowedHostsOrExit(flag, entries) {
20100
20328
  `${JSON.stringify(entry)} is a malformed wildcard \u2014 use "*.domain.tld"`
20101
20329
  );
20102
20330
  }
20103
- if (!rest.includes(".")) {
20331
+ const canonicalRest = canonicalizeDnsHost(rest);
20332
+ if (canonicalRest === null) {
20333
+ fail(
20334
+ `${JSON.stringify(entry)} is not a valid DNS name (IDNA-encodable, labels up to 63 characters)`
20335
+ );
20336
+ }
20337
+ if (!canonicalRest.includes(".")) {
20104
20338
  fail(
20105
20339
  `${JSON.stringify(entry)}: wildcard must cover a domain, not a bare TLD (use "*.example.com", not "*.com")`
20106
20340
  );
20107
20341
  }
20108
- normalized = `*.${rest}`;
20342
+ if (canonicalRest.length > 251) {
20343
+ fail(
20344
+ `${JSON.stringify(entry)} is too long for a wildcard \u2014 no host could ever match it`
20345
+ );
20346
+ }
20347
+ normalized = `*.${canonicalRest}`;
20109
20348
  } else if (e.includes("*")) {
20110
20349
  fail(
20111
20350
  `${JSON.stringify(entry)}: "*" is only allowed as a leading subdomain wildcard ("*.domain.tld")`
@@ -20114,9 +20353,16 @@ function parseAllowedHostsOrExit(flag, entries) {
20114
20353
  normalized = e.replace(/\.+$/, "");
20115
20354
  if (!normalized) {
20116
20355
  fail(
20117
- `${JSON.stringify(entry)} must be a bare host \u2014 no scheme, port, path, or "@"`
20356
+ `${JSON.stringify(entry)} must be a bare host \u2014 no scheme, port, path, "@", whitespace, or the URL delimiters ? # % \\`
20357
+ );
20358
+ }
20359
+ const canonical = canonicalizeDnsHost(normalized);
20360
+ if (canonical === null) {
20361
+ fail(
20362
+ `${JSON.stringify(entry)} is not a valid DNS name (IDNA-encodable, labels up to 63 characters)`
20118
20363
  );
20119
20364
  }
20365
+ normalized = canonical;
20120
20366
  }
20121
20367
  if (seen.has(normalized)) continue;
20122
20368
  seen.add(normalized);
@@ -20372,6 +20618,20 @@ function surfaceCatalogWarnings(row) {
20372
20618
  );
20373
20619
  }
20374
20620
  }
20621
+ function surfaceScopeWarnings(row) {
20622
+ if (typeof row !== "object" || row === null) return;
20623
+ const warnings = row.scope_warnings;
20624
+ if (!Array.isArray(warnings)) return;
20625
+ for (const warning of warnings) {
20626
+ if (typeof warning !== "string") continue;
20627
+ const sanitized = sanitizeStderrText(warning);
20628
+ if (sanitized.length === 0) continue;
20629
+ process.stderr.write(
20630
+ `alter: scope warning \u2014 ${sanitized} (save was allowed)
20631
+ `
20632
+ );
20633
+ }
20634
+ }
20375
20635
  function writeNextPageHint(page, noun) {
20376
20636
  if (typeof page !== "object" || page === null) return;
20377
20637
  const { has_more: hasMore, offset, limit } = page;
@@ -20566,6 +20826,7 @@ function buildAgentsCommand() {
20566
20826
  "alter: the api_key field above is shown ONCE and cannot be retrieved later.\n"
20567
20827
  );
20568
20828
  surfaceApproverWarnings(result);
20829
+ surfaceScopeWarnings(result);
20569
20830
  });
20570
20831
  return;
20571
20832
  }
@@ -20621,6 +20882,7 @@ function buildAgentsCommand() {
20621
20882
  "alter: the api_key field above is shown ONCE and cannot be retrieved later.\n"
20622
20883
  );
20623
20884
  surfaceApproverWarnings(result);
20885
+ surfaceScopeWarnings(result);
20624
20886
  });
20625
20887
  }
20626
20888
  );
@@ -20840,6 +21102,7 @@ function buildAgentsCommand() {
20840
21102
  }
20841
21103
  );
20842
21104
  emit2(format, row);
21105
+ surfaceScopeWarnings(row);
20843
21106
  } catch (error51) {
20844
21107
  if (error51 instanceof PortalBackendError && error51.statusCode === 409 && error51.code === "agent_concurrent_update") {
20845
21108
  throw new PortalBackendError(
@@ -35908,7 +36171,7 @@ var RULE_TYPE_LABELS = /* @__PURE__ */ new Map([
35908
36171
  ["json_match", "Request condition"],
35909
36172
  ["ip_allowlist", "IP allowlist"],
35910
36173
  ["time_window", "Time window"],
35911
- ["require_approval", "Require approval"],
36174
+ ["require_approval", "Human in the loop (HITL)"],
35912
36175
  ["restriction", "Method and endpoint allowlist"],
35913
36176
  ["quota", "Request quota"],
35914
36177
  ["content_match", "Operation and parameter policy"]
@@ -36334,10 +36597,13 @@ function buildPolicyRulePresentation(rule) {
36334
36597
  });
36335
36598
  }
36336
36599
  if (rule.display?.inherited) {
36337
- displayItems.push({ label: "Relationship", value: "Inherited policy" });
36600
+ displayItems.push({
36601
+ label: "Relationship",
36602
+ value: "Inherited runtime policy"
36603
+ });
36338
36604
  }
36339
36605
  if (displayItems.length > 0) {
36340
- groups.unshift({ label: "Policy scope", items: displayItems });
36606
+ groups.unshift({ label: "Runtime policy scope", items: displayItems });
36341
36607
  }
36342
36608
  return {
36343
36609
  typeLabel: policyRuleTypeLabel(rule.rule_type),
@@ -36350,7 +36616,10 @@ function formatPolicyRuleAsText(rule) {
36350
36616
  const lines = [
36351
36617
  ...rule.name ? [`Name: ${rule.name}`] : [],
36352
36618
  ...rule.id ? [`Policy ID: ${rule.id}`] : [],
36353
- `Type: ${presentation.typeLabel}`,
36619
+ // The friendly label matches the dashboard; the wire identifier is what
36620
+ // `alter policy rules create --type <rule_type>` accepts, so the text
36621
+ // view prints both for operators who script from it.
36622
+ `Type: ${presentation.typeLabel} [${rule.rule_type}]`,
36354
36623
  `Effect: ${presentation.effectLabel}`,
36355
36624
  `Status: ${rule.enabled === false ? "Disabled" : "Enabled"}`,
36356
36625
  presentation.summary,
@@ -36865,7 +37134,7 @@ function buildAppsCommand() {
36865
37134
  "table"
36866
37135
  ).option(
36867
37136
  "--no-include-stats",
36868
- "Skip per-app statistics (grant / key / API-call counts)"
37137
+ "Skip per-app statistics (grant / connection / secret / provider / key / API-call counts)"
36869
37138
  ).option(
36870
37139
  "--include-archived",
36871
37140
  "Include archived (soft-deleted) apps. Default is active-only \u2014 archive's whole point is to hide clutter from the default view, so opt in only when you want the full set (e.g. before unarchiving something)."
@@ -41066,6 +41335,7 @@ async function runStatus() {
41066
41335
  // src/commands/managed-secrets.ts
41067
41336
  import { readFileSync as readFileSync9 } from "fs";
41068
41337
  import { Command as Command16, Option } from "commander";
41338
+ var MAX_SEARCH_LENGTH = 255;
41069
41339
  var PRINCIPAL_TYPES = ["user", "group", "system", "agent"];
41070
41340
  var CREDENTIAL_TYPES = [
41071
41341
  "bearer_token",
@@ -41470,6 +41740,13 @@ function surfaceManagedSecretResponse(row) {
41470
41740
  }
41471
41741
  surfaceApproverWarnings(row);
41472
41742
  }
41743
+ function derivedAllowlistMessage(e) {
41744
+ if (!(e instanceof PortalBackendError) || e.statusCode !== 409 || e.code !== "managed_secret_allowed_hosts_derived") {
41745
+ return null;
41746
+ }
41747
+ const detail = e.body?.detail;
41748
+ return typeof detail?.message === "string" ? sanitizeStderrText(detail.message) : "This secret's allowed hosts are derived from its provider binding and cannot be edited directly.";
41749
+ }
41473
41750
  function groupPrincipalGateMessage(e) {
41474
41751
  if (!(e instanceof PortalBackendError) || e.statusCode !== 422 || e.code !== "group_principal_unsupported_idp") {
41475
41752
  return null;
@@ -41806,7 +42083,11 @@ var TEMPLATE_COLUMNS = [
41806
42083
  get: (t) => t.form_schema?.fields.map(
41807
42084
  (field) => `${credentialFieldFlag(field)}${field.required ? "" : " (optional)"}`
41808
42085
  ).join(" ") || "\u2014"
41809
- }
42086
+ },
42087
+ // The field a bound template derives its allowlist from — the operator
42088
+ // needs it to know which --credential-field sets (and later retargets,
42089
+ // via rotate) the secret's allowed hosts.
42090
+ { label: "BINDING", get: (t) => t.allowed_hosts_from_field ?? "\u2014" }
41810
42091
  ];
41811
42092
  var SECRET_COLUMNS = [
41812
42093
  { label: "ID", get: (s) => s.id, maxWidth: 36 },
@@ -42275,7 +42556,7 @@ function buildGroupsSubcommand() {
42275
42556
  );
42276
42557
  groups.command("list").description("List app-user-groups (Group-tab picker source)").option(APP_FLAG, APP_TARGET_DESC).option("--idp <uuid>", "Filter by identity provider id").option(
42277
42558
  "--search <substring>",
42278
- "Substring search on group name / external_group_id"
42559
+ "Substring search on group name / external_group_id (max 255 chars)"
42279
42560
  ).option(
42280
42561
  "--limit <n>",
42281
42562
  "Page size (default: 50)",
@@ -42292,6 +42573,12 @@ function buildGroupsSubcommand() {
42292
42573
  async (options) => {
42293
42574
  const format = coerceOutputFormat(options.output);
42294
42575
  if (options.idp !== void 0) validateUuidOrExit("--idp", options.idp);
42576
+ if (options.search !== void 0)
42577
+ validateMaxLengthOrExit(
42578
+ "--search",
42579
+ options.search,
42580
+ MAX_SEARCH_LENGTH
42581
+ );
42295
42582
  const resolvedAppId = await resolveAppOrExit(options.app);
42296
42583
  await withClient(async (client) => {
42297
42584
  const response = await client.managedSecrets.listGroups(
@@ -42404,7 +42691,8 @@ function buildManagedSecretsCommand() {
42404
42691
  credential_type: isString,
42405
42692
  category: isOptionalString,
42406
42693
  popular: (v) => typeof v === "boolean",
42407
- form_schema: isTemplateFormSchema
42694
+ form_schema: isTemplateFormSchema,
42695
+ allowed_hosts_from_field: isAbsentOrOptionalString
42408
42696
  },
42409
42697
  "managed-secrets.templates"
42410
42698
  );
@@ -42788,11 +43076,25 @@ function buildManagedSecretsCommand() {
42788
43076
  }
42789
43077
  const hostList = options.clear ? null : parseAllowedHostsOrExit("--host", options.host);
42790
43078
  await withClient(async (client) => {
42791
- const row = await client.managedSecrets.setAllowedHosts(
42792
- appId,
42793
- secretId,
42794
- hostList
42795
- );
43079
+ let row;
43080
+ try {
43081
+ row = await client.managedSecrets.setAllowedHosts(
43082
+ appId,
43083
+ secretId,
43084
+ hostList
43085
+ );
43086
+ } catch (e) {
43087
+ const derived = derivedAllowlistMessage(e);
43088
+ if (derived !== null) {
43089
+ process.stderr.write(`alter: ${derived}
43090
+ `);
43091
+ process.stderr.write(
43092
+ "alter: rotate the credential (`alter managed-secrets rotate`) to change its binding\n"
43093
+ );
43094
+ process.exit(EXIT_CONFLICT);
43095
+ }
43096
+ throw e;
43097
+ }
42796
43098
  emit2(format, row);
42797
43099
  if (options.clear) {
42798
43100
  process.stderr.write(
@@ -43078,7 +43380,7 @@ function buildManagedSecretsCommand() {
43078
43380
  );
43079
43381
  root.command("users").description("List app-users (User-tab picker source for grants create)").option(APP_FLAG, APP_TARGET_DESC).option("--idp <uuid>", "Filter by identity provider id").option(
43080
43382
  "--search <substring>",
43081
- "Substring search on email / display_name / external_subject_id"
43383
+ "Substring search on email / display_name / external_subject_id (max 255 chars)"
43082
43384
  ).option(
43083
43385
  "--limit <n>",
43084
43386
  "Page size (default: 50)",
@@ -43095,6 +43397,12 @@ function buildManagedSecretsCommand() {
43095
43397
  async (options) => {
43096
43398
  const format = coerceOutputFormat(options.output);
43097
43399
  if (options.idp !== void 0) validateUuidOrExit("--idp", options.idp);
43400
+ if (options.search !== void 0)
43401
+ validateMaxLengthOrExit(
43402
+ "--search",
43403
+ options.search,
43404
+ MAX_SEARCH_LENGTH
43405
+ );
43098
43406
  const resolvedAppId = await resolveAppOrExit(options.app);
43099
43407
  await withClient(async (client) => {
43100
43408
  const response = await client.managedSecrets.listUsers(