@alter-ai/cli 0.9.0 → 0.9.2

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.2",
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.25.0",
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",
@@ -4739,7 +4743,14 @@ var AmbiguousGrantError = class extends BackendError {
4739
4743
  // scoped to the caller's own accessible set. Retry with the chosen
4740
4744
  // `grantId`.
4741
4745
  candidates;
4742
- constructor(message, providerId, accountIdentifiers, accountWasProvided, appUserIds, details, grantIds, candidates) {
4746
+ // True when more grants matched than the error body can carry. It bounds
4747
+ // EVERY disambiguator on this error — `candidates`, `accountIdentifiers`,
4748
+ // `appUserIds` and `grantIds` all come from the same capped set — so a
4749
+ // grant, account or user missing from any of them is not proof it does not
4750
+ // exist. Narrow the request or call `listGrants()` for the authoritative
4751
+ // set.
4752
+ candidatesTruncated;
4753
+ constructor(message, providerId, accountIdentifiers, accountWasProvided, appUserIds, details, grantIds, candidates, candidatesTruncated) {
4743
4754
  super(message, details);
4744
4755
  this.name = "AmbiguousGrantError";
4745
4756
  this.providerId = providerId;
@@ -4748,6 +4759,7 @@ var AmbiguousGrantError = class extends BackendError {
4748
4759
  this.appUserIds = appUserIds ?? [];
4749
4760
  this.grantIds = grantIds ?? [];
4750
4761
  this.candidates = candidates ?? [];
4762
+ this.candidatesTruncated = candidatesTruncated ?? false;
4751
4763
  }
4752
4764
  };
4753
4765
  var NoDelegatedGrantError = class extends BackendError {
@@ -5720,6 +5732,14 @@ var ConnectSession = class {
5720
5732
  * Python SDK's scope_constraint_warnings coercion (parity contract).
5721
5733
  */
5722
5734
  scopeConstraintWarnings;
5735
+ /**
5736
+ * True when the session names exactly one provider: the hosted UI shows no
5737
+ * picker, confirmation screen or Alter-branded page, and the browser widget
5738
+ * can open the provider window straight from the application's click when
5739
+ * the value is passed through as `open({ direct })`. False on a malformed
5740
+ * wire value (informational, not load-bearing).
5741
+ */
5742
+ direct;
5723
5743
  constructor(data) {
5724
5744
  _assertString(data.session_token, "session_token", "ConnectSession");
5725
5745
  _assertString(data.connect_url, "connect_url", "ConnectSession");
@@ -5741,10 +5761,12 @@ var ConnectSession = class {
5741
5761
  (w) => typeof w === "string"
5742
5762
  ) : []
5743
5763
  );
5764
+ this.direct = data.direct === true;
5744
5765
  Object.freeze(this);
5745
5766
  }
5746
5767
  toJSON() {
5747
5768
  return {
5769
+ direct: this.direct,
5748
5770
  session_token: this.sessionToken,
5749
5771
  connect_url: this.connectUrl,
5750
5772
  expires_in: this.expiresIn,
@@ -6255,7 +6277,11 @@ var ConnectResult = class {
6255
6277
  providerId;
6256
6278
  accountIdentifier;
6257
6279
  scopes;
6258
- /** Ordinary Connect completion or in-place repair of a broken grant. */
6280
+ /**
6281
+ * "creation" when a new grant was minted; "reauth" when an existing
6282
+ * grant was returned with its id preserved (credential repair, or reuse
6283
+ * of an already-connected grant).
6284
+ */
6259
6285
  operation;
6260
6286
  grantPolicy;
6261
6287
  /**
@@ -8143,7 +8169,7 @@ async function _raiseForStatus(response) {
8143
8169
  if (code === AgentConcurrentUpdateError.code && response.status === 409) {
8144
8170
  throw new AgentConcurrentUpdateError({ message, details: body, hint });
8145
8171
  }
8146
- const ExcCtor = ERROR_CODE_STATUS_MAP[`${code}:${response.status}`] ?? ERROR_CODE_MAP[code];
8172
+ const ExcCtor = ERROR_CODE_STATUS_MAP[`${code}:${response.status}`] ?? (Object.hasOwn(ERROR_CODE_MAP, code) ? ERROR_CODE_MAP[code] : void 0);
8147
8173
  if (ExcCtor !== void 0) {
8148
8174
  throw new ExcCtor({ message, details: body, hint });
8149
8175
  }
@@ -10172,7 +10198,7 @@ function _extractAdditionalCredentials(token) {
10172
10198
  return _additionalCredsStore.get(token);
10173
10199
  }
10174
10200
  var _fetch;
10175
- var SDK_VERSION = "0.24.0";
10201
+ var SDK_VERSION = "0.25.0";
10176
10202
  var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
10177
10203
  function pyUnquote(s) {
10178
10204
  if (!s.includes("%")) return s;
@@ -10243,8 +10269,12 @@ function canonicalSigningQuery(rawQuery) {
10243
10269
  pairs.sort((p, q) => byCodePoint(p[0], q[0]) || byCodePoint(p[1], q[1]));
10244
10270
  return pairs.map(([k, v]) => `${pyQuotePlus(k)}=${pyQuotePlus(v)}`).join("&");
10245
10271
  }
10272
+ var DEFAULT_TIMEOUT_MS = 95e3;
10273
+ var _ApprovalDeadlineElapsed = class extends Error {
10274
+ };
10246
10275
  var AUTH_POLL_SERVER_WAIT_MS = 25e3;
10247
10276
  var AUTH_POLL_HTTP_BUFFER_MS = 15e3;
10277
+ var APPROVAL_POLL_SERVER_WAIT_MS = 25e3;
10248
10278
  var PERMANENT_POLL_STATUSES = /* @__PURE__ */ new Set([400, 401, 403, 404, 422]);
10249
10279
  var HTTP_FORBIDDEN = 403;
10250
10280
  var HTTP_NO_CONTENT2 = 204;
@@ -10520,6 +10550,23 @@ function validateAndSerializeContext(context) {
10520
10550
  return encoded;
10521
10551
  }
10522
10552
  var MAX_BODY_SIZE_BYTES = 1e4;
10553
+ var AUDIT_REFUSAL_BODY_MAX_CHARS = 65536;
10554
+ var REFUSAL_MESSAGE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
10555
+ var REFUSAL_MESSAGE_MAX_CHARS = 300;
10556
+ function sanitizeRefusalMessage(message) {
10557
+ const cleaned = message.replace(REFUSAL_MESSAGE_CONTROL_CHARS, " ");
10558
+ return cleaned.length > REFUSAL_MESSAGE_MAX_CHARS ? `${cleaned.slice(0, REFUSAL_MESSAGE_MAX_CHARS)}\u2026` : cleaned;
10559
+ }
10560
+ function _redactUrlForLog(url2) {
10561
+ try {
10562
+ const parsed = new URL(url2);
10563
+ if (parsed.protocol && parsed.hostname) {
10564
+ return `${parsed.protocol}//${parsed.hostname}`;
10565
+ }
10566
+ } catch {
10567
+ }
10568
+ return "<unparseable-url>";
10569
+ }
10523
10570
  var HTTP_CLIENT_ERROR_START = 400;
10524
10571
  var MAX_ACTOR_STRING_LENGTH = 255;
10525
10572
  var SAFE_HEADER_PATTERN = /^[\x20-\x7E]+$/;
@@ -10808,6 +10855,16 @@ var HttpClient = class {
10808
10855
  new DOMException("The operation timed out.", "TimeoutError")
10809
10856
  );
10810
10857
  }, effectiveTimeoutMs);
10858
+ const callerSignal = options?.signal;
10859
+ let onCallerAbort;
10860
+ if (callerSignal !== void 0) {
10861
+ if (callerSignal.aborted) {
10862
+ controller.abort(callerSignal.reason);
10863
+ } else {
10864
+ onCallerAbort = () => controller.abort(callerSignal.reason);
10865
+ callerSignal.addEventListener("abort", onCallerAbort);
10866
+ }
10867
+ }
10811
10868
  const init = {
10812
10869
  method,
10813
10870
  headers: mergedHeaders,
@@ -10846,6 +10903,9 @@ var HttpClient = class {
10846
10903
  return response;
10847
10904
  } finally {
10848
10905
  clearTimeout(timeoutId);
10906
+ if (callerSignal !== void 0 && onCallerAbort !== void 0) {
10907
+ callerSignal.removeEventListener("abort", onCallerAbort);
10908
+ }
10849
10909
  }
10850
10910
  }
10851
10911
  /**
@@ -10958,7 +11018,7 @@ var _VaultClient = class __VaultClient {
10958
11018
  unresolvedBaseUrl,
10959
11019
  options.logger ?? console
10960
11020
  );
10961
- const timeoutMs = options.timeout ?? 3e4;
11021
+ const timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;
10962
11022
  this.#caller = options.caller;
10963
11023
  const rawCallerType = options.callerType ?? "agent";
10964
11024
  if (rawCallerType !== "agent" && rawCallerType !== "service") {
@@ -11640,7 +11700,11 @@ ${label}:${value}`;
11640
11700
  appUserIds,
11641
11701
  errorData,
11642
11702
  grantIds,
11643
- candidates
11703
+ candidates,
11704
+ // Absent on the wire means "complete" — only a real cap emits the
11705
+ // key, so a strict `=== true` keeps a stray falsy value from
11706
+ // reading as truncation.
11707
+ errorData.candidates_truncated === true
11644
11708
  );
11645
11709
  }
11646
11710
  if (errorData.error === "sibling_label_conflict") {
@@ -12163,9 +12227,34 @@ ${label}:${value}`;
12163
12227
  });
12164
12228
  this.#cacheActorIdFromResponse(response);
12165
12229
  if (!response.ok) {
12230
+ let detail = "";
12231
+ try {
12232
+ const raw = await response.text();
12233
+ if (raw.length <= AUDIT_REFUSAL_BODY_MAX_CHARS) {
12234
+ const body = JSON.parse(raw);
12235
+ let message = body.message;
12236
+ if (typeof message !== "string" || message === "") {
12237
+ const inner = body.detail;
12238
+ if (typeof inner === "string") {
12239
+ message = inner;
12240
+ } else if (inner !== null && typeof inner === "object") {
12241
+ message = inner.message;
12242
+ }
12243
+ }
12244
+ if (typeof message === "string" && message !== "") {
12245
+ detail = `: ${sanitizeRefusalMessage(message)}`;
12246
+ }
12247
+ }
12248
+ } catch {
12249
+ }
12166
12250
  this.#logger.warn(
12167
- `Audit log failed with status ${response.status} (non-fatal)`
12251
+ `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
12252
  );
12253
+ } else {
12254
+ try {
12255
+ await response.arrayBuffer();
12256
+ } catch {
12257
+ }
12169
12258
  }
12170
12259
  } catch (error51) {
12171
12260
  this.#logger.warn(
@@ -12676,7 +12765,7 @@ ${label}:${value}`;
12676
12765
  headers: hmacHeaders,
12677
12766
  // Cap the best-effort background report at 2s (parity with the Python
12678
12767
  // SDK's asyncio.wait_for). Without an override it would inherit the
12679
- // instance timeout (default 30s), so a hung backend could keep the
12768
+ // instance timeout (default 95s), so a hung backend could keep the
12680
12769
  // fire-and-forget task — and close()'s drain of it — alive far longer
12681
12770
  // than the Python SDK does. Next provider 403 re-reports (self-healing).
12682
12771
  timeoutMs: 2e3
@@ -13053,8 +13142,11 @@ ${label}:${value}`;
13053
13142
  * @param options - Optional revocation options (reason for audit log).
13054
13143
  * @returns RevokeGrantResult confirming the revocation.
13055
13144
  * @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.
13145
+ * active, (when a user token is configured) does not belong to
13146
+ * the calling user, or — when this client holds an AGENT key — is
13147
+ * not a grant the agent is the principal of (its own delegation or
13148
+ * an agent-owned grant). An allowlist entry for the grant's provider
13149
+ * is never enough.
13058
13150
  * @throws {NetworkError} If connection to backend fails.
13059
13151
  * @throws {TimeoutError} If request to backend times out.
13060
13152
  */
@@ -13865,14 +13957,17 @@ ${label}:${value}`;
13865
13957
  * @returns One `ConnectResult` per provider the user completed
13866
13958
  * within the session (multi-provider Connect sessions yield
13867
13959
  * multiple results).
13868
- * @throws AlterValueError if `sessionToken` is blank, or `timeoutMs` /
13869
- * `pollIntervalMs` is non-finite or not greater than zero.
13960
+ * @throws AlterValueError if `sessionToken` is blank, `timeoutMs` /
13961
+ * `pollIntervalMs` is non-finite or not greater than zero, or
13962
+ * `onEvent` is provided but not a function.
13870
13963
  * @throws ConnectTimeoutError if the local `timeoutMs` elapses, or a
13871
13964
  * session observed as pending expires before Alter receives a
13872
13965
  * completion callback. `details.reason` distinguishes
13873
13966
  * `poll_deadline_elapsed`, `session_expired`, and `rate_limited` (the
13874
13967
  * whole budget was spent backing off a throttled poll —
13875
- * `details.retryAfter` carries the last hint the server gave).
13968
+ * `details.retryAfter` carries the last hint the server gave). When
13969
+ * denials were observed before the timeout,
13970
+ * `details.declined_providers` lists them on EVERY variant.
13876
13971
  * @throws ConnectFlowError / ConnectDeniedError / ConnectConfigError
13877
13972
  * for user denial, a first poll that finds an unavailable/expired
13878
13973
  * session, unrecognized status, or a completed session whose every grant
@@ -13892,8 +13987,18 @@ ${label}:${value}`;
13892
13987
  const timeoutMs = options?.timeoutMs ?? 3e5;
13893
13988
  const pollIntervalMs = options?.pollIntervalMs ?? 2e3;
13894
13989
  validatePollingInputs(sessionToken, timeoutMs, pollIntervalMs);
13990
+ if (options?.onEvent !== void 0 && typeof options.onEvent !== "function") {
13991
+ throw new AlterValueError("onEvent must be a function when provided");
13992
+ }
13895
13993
  const deadline = performance.now() + timeoutMs;
13896
13994
  let sawPending = false;
13995
+ const seenDeclinedProviders = /* @__PURE__ */ new Set();
13996
+ const withDeclined = (base) => {
13997
+ if (seenDeclinedProviders.size > 0) {
13998
+ base.declined_providers = [...seenDeclinedProviders].sort();
13999
+ }
14000
+ return base;
14001
+ };
13897
14002
  while (true) {
13898
14003
  this.#assertNotClosed();
13899
14004
  let pollResult;
@@ -13907,11 +14012,11 @@ ${label}:${value}`;
13907
14012
  if (remaining2 <= 0) {
13908
14013
  throw new ConnectTimeoutError(
13909
14014
  `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
- {
14015
+ withDeclined({
13911
14016
  timeoutMs,
13912
14017
  reason: "rate_limited",
13913
14018
  retryAfter: e.retryAfter
13914
- }
14019
+ })
13915
14020
  );
13916
14021
  }
13917
14022
  let backoffMs = pollIntervalMs;
@@ -14044,7 +14149,10 @@ ${label}:${value}`;
14044
14149
  ].includes(errorCode)) {
14045
14150
  throw new ConnectConfigError(errorMessage, errorDetails);
14046
14151
  }
14047
- const identitySyncStatus = IDENTITY_SYNC_POLL_STATUS[errorCode];
14152
+ const identitySyncStatus = Object.hasOwn(
14153
+ IDENTITY_SYNC_POLL_STATUS,
14154
+ errorCode
14155
+ ) ? IDENTITY_SYNC_POLL_STATUS[errorCode] : void 0;
14048
14156
  if (identitySyncStatus !== void 0) {
14049
14157
  throw new BackendError(
14050
14158
  errorMessage,
@@ -14058,7 +14166,7 @@ ${label}:${value}`;
14058
14166
  if (sawPending) {
14059
14167
  throw new ConnectTimeoutError(
14060
14168
  "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" }
14169
+ withDeclined({ timeoutMs, reason: "session_expired" })
14062
14170
  );
14063
14171
  }
14064
14172
  throw new ConnectFlowError(
@@ -14071,6 +14179,46 @@ ${label}:${value}`;
14071
14179
  { status: pollStatus }
14072
14180
  );
14073
14181
  }
14182
+ const rawDeclined = pollResult.declined_providers;
14183
+ if (Array.isArray(rawDeclined)) {
14184
+ for (const entry of rawDeclined) {
14185
+ if (entry === null || typeof entry !== "object") continue;
14186
+ const declinedProvider = entry.provider_id;
14187
+ if (typeof declinedProvider !== "string" || declinedProvider === "") {
14188
+ continue;
14189
+ }
14190
+ const rawCode = entry.error_code;
14191
+ if (typeof rawCode !== "string" || rawCode === "") continue;
14192
+ if (seenDeclinedProviders.has(declinedProvider)) continue;
14193
+ seenDeclinedProviders.add(declinedProvider);
14194
+ if (!options?.onEvent) continue;
14195
+ const rawDeclinedAt = entry.declined_at;
14196
+ const logCallbackFailure = (error51) => {
14197
+ this.#logger.warn(
14198
+ "pollConnectSession onEvent callback threw (non-fatal)",
14199
+ {
14200
+ provider_id: declinedProvider,
14201
+ error_type: error51 instanceof Error ? error51.constructor.name : typeof error51
14202
+ }
14203
+ );
14204
+ };
14205
+ try {
14206
+ const result = options.onEvent({
14207
+ type: "provider_declined",
14208
+ provider_id: declinedProvider,
14209
+ error_code: rawCode,
14210
+ declined_at: typeof rawDeclinedAt === "string" ? rawDeclinedAt : null
14211
+ });
14212
+ if (result !== null && (typeof result === "object" || typeof result === "function") && typeof result.then === "function") {
14213
+ void Promise.resolve(result).catch(
14214
+ logCallbackFailure
14215
+ );
14216
+ }
14217
+ } catch (error51) {
14218
+ logCallbackFailure(error51);
14219
+ }
14220
+ }
14221
+ }
14074
14222
  sawPending = true;
14075
14223
  const remaining = deadline - performance.now();
14076
14224
  if (remaining <= 0) break;
@@ -14080,7 +14228,7 @@ ${label}:${value}`;
14080
14228
  }
14081
14229
  throw new ConnectTimeoutError(
14082
14230
  `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" }
14231
+ withDeclined({ timeoutMs, reason: "poll_deadline_elapsed" })
14084
14232
  );
14085
14233
  }
14086
14234
  /**
@@ -14333,7 +14481,10 @@ ${label}:${value}`;
14333
14481
  const errorMessage = typeof pollData.error_message === "string" && pollData.error_message ? pollData.error_message : "unknown error";
14334
14482
  const message = `Authentication failed: ${errorMessage}`;
14335
14483
  const syncCode = typeof pollData.error_code === "string" ? pollData.error_code : "";
14336
- const identitySyncStatus = IDENTITY_SYNC_POLL_STATUS[syncCode];
14484
+ const identitySyncStatus = Object.hasOwn(
14485
+ IDENTITY_SYNC_POLL_STATUS,
14486
+ syncCode
14487
+ ) ? IDENTITY_SYNC_POLL_STATUS[syncCode] : void 0;
14337
14488
  if (identitySyncStatus !== void 0) {
14338
14489
  const details = { error_code: syncCode };
14339
14490
  const retryAfter = _coerceInt(pollData.retry_after_seconds);
@@ -14958,10 +15109,34 @@ ${label}:${value}`;
14958
15109
  error: errCode
14959
15110
  });
14960
15111
  }
14961
- /** Single-shot poll of an approval. */
14962
- async getApprovalStatus(approvalId) {
15112
+ /**
15113
+ * Single-shot poll of an approval.
15114
+ *
15115
+ * `wait` asks the server to hold the request open until the approval's
15116
+ * status changes, up to that many seconds (0 = return immediately, the
15117
+ * default and the historical behaviour). It is a pure latency optimisation:
15118
+ * the response shape is identical either way, and a backend that predates
15119
+ * the parameter simply ignores it and answers immediately. The server caps
15120
+ * `wait` at 25 seconds (`APPROVAL_POLL_SERVER_WAIT_MS`, mirroring the
15121
+ * backend's `MAX_LONG_POLL_SECONDS`) and REJECTS larger values with a
15122
+ * validation error (422) rather than clamping them.
15123
+ *
15124
+ * `signal` (internal — not exposed on App/Agent) lets `awaitApproval` abort
15125
+ * an in-flight held request its local deadline has abandoned.
15126
+ *
15127
+ * @throws AlterValueError if `wait` is not a finite number.
15128
+ */
15129
+ async getApprovalStatus(approvalId, options = {}) {
14963
15130
  this.#assertNotClosed();
14964
- const sdkPath = `/sdk/approvals/${approvalId}`;
15131
+ if (options !== null && options !== void 0 && typeof options !== "object") {
15132
+ throw new AlterValueError("options must be an object");
15133
+ }
15134
+ const waitOption = options?.wait ?? 0;
15135
+ if (typeof waitOption !== "number" || !Number.isFinite(waitOption)) {
15136
+ throw new AlterValueError("wait must be a finite number");
15137
+ }
15138
+ const wait = Math.max(0, Math.floor(waitOption));
15139
+ const sdkPath = wait > 0 ? `/sdk/approvals/${approvalId}?wait=${wait}` : `/sdk/approvals/${approvalId}`;
14965
15140
  const hmacHeaders = this.#computeHmacHeaders("GET", sdkPath, "");
14966
15141
  const traceparent = await ambientTraceparent();
14967
15142
  if (traceparent !== void 0) {
@@ -14970,7 +15145,18 @@ ${label}:${value}`;
14970
15145
  let response;
14971
15146
  try {
14972
15147
  response = await this.#alterClient.request("GET", sdkPath, {
14973
- headers: hmacHeaders
15148
+ headers: hmacHeaders,
15149
+ // A held request needs a wider per-request timeout than the client
15150
+ // default, or the client aborts its own long-poll (see
15151
+ // AUTH_POLL_HTTP_BUFFER_MS). Only applied when actually parking.
15152
+ // Capped at the server's own ceiling: `wait` is deliberately NOT
15153
+ // clamped on the wire (the backend owns that contract and answers an
15154
+ // over-cap value with its documented validation error), but an
15155
+ // unbounded `wait * 1000` overflows to `Infinity` for a huge finite
15156
+ // value, and Node coerces such a timer to fire almost immediately —
15157
+ // aborting the request before that very error can come back.
15158
+ timeoutMs: wait > 0 ? Math.min(wait * 1e3, APPROVAL_POLL_SERVER_WAIT_MS) + AUTH_POLL_HTTP_BUFFER_MS : void 0,
15159
+ signal: options?.signal
14974
15160
  });
14975
15161
  } catch (error51) {
14976
15162
  if (error51 instanceof Error && error51.name === "AbortError") {
@@ -15041,9 +15227,39 @@ ${label}:${value}`;
15041
15227
  throw this.#buildApprovalTimeout(approvalId, timeoutMs, lastTransient);
15042
15228
  }
15043
15229
  let status;
15230
+ const raceController = new AbortController();
15231
+ let deadlineTimer;
15044
15232
  try {
15045
- status = await this.getApprovalStatus(approvalId);
15233
+ const serverWaitSeconds = Math.max(
15234
+ 0,
15235
+ Math.min(
15236
+ Math.floor(APPROVAL_POLL_SERVER_WAIT_MS / 1e3),
15237
+ Math.floor((deadline - Date.now()) / 1e3)
15238
+ )
15239
+ );
15240
+ const remainingMs = Math.max(0, deadline - Date.now());
15241
+ status = await Promise.race([
15242
+ this.getApprovalStatus(approvalId, {
15243
+ wait: serverWaitSeconds,
15244
+ signal: raceController.signal
15245
+ }),
15246
+ new Promise((_resolve, reject) => {
15247
+ deadlineTimer = setTimeout(
15248
+ () => reject(new _ApprovalDeadlineElapsed()),
15249
+ remainingMs
15250
+ );
15251
+ deadlineTimer.unref?.();
15252
+ })
15253
+ ]);
15046
15254
  } catch (err2) {
15255
+ if (err2 instanceof _ApprovalDeadlineElapsed) {
15256
+ raceController.abort();
15257
+ throw this.#buildApprovalTimeout(
15258
+ approvalId,
15259
+ timeoutMs,
15260
+ lastTransient
15261
+ );
15262
+ }
15047
15263
  if (!__VaultClient.#isTransientPollError(err2)) {
15048
15264
  throw err2;
15049
15265
  }
@@ -15065,6 +15281,8 @@ ${label}:${value}`;
15065
15281
  );
15066
15282
  await new Promise((res) => setTimeout(res, sleep2));
15067
15283
  continue;
15284
+ } finally {
15285
+ if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
15068
15286
  }
15069
15287
  lastTransient = null;
15070
15288
  if (status.status === "executed" && !status.hasResult) {
@@ -15880,8 +16098,8 @@ var Agent = class _Agent {
15880
16098
  return this.#client.delegate(grantId, toAgentId, options);
15881
16099
  }
15882
16100
  // ── Approvals ──────────────────────────────────────────────────────────
15883
- async getApprovalStatus(approvalId) {
15884
- return this.#client.getApprovalStatus(approvalId);
16101
+ async getApprovalStatus(approvalId, options = {}) {
16102
+ return this.#client.getApprovalStatus(approvalId, options);
15885
16103
  }
15886
16104
  async awaitApproval(...args) {
15887
16105
  return this.#client.awaitApproval(...args);
@@ -16301,8 +16519,8 @@ var App = class _App {
16301
16519
  });
16302
16520
  }
16303
16521
  // ── Approvals ──────────────────────────────────────────────────────────
16304
- async getApprovalStatus(approvalId) {
16305
- return this.#client.getApprovalStatus(approvalId);
16522
+ async getApprovalStatus(approvalId, options = {}) {
16523
+ return this.#client.getApprovalStatus(approvalId, options);
16306
16524
  }
16307
16525
  async awaitApproval(...args) {
16308
16526
  return this.#client.awaitApproval(...args);
@@ -16558,8 +16776,8 @@ async function _listGrantsUnified(client, body) {
16558
16776
  var DEFAULT_BASE_URL = "https://backend.alterauth.com";
16559
16777
  var PAT_API_PREFIX = "/api/v1/dev-portal";
16560
16778
  var HTTP_ERROR_THRESHOLD = 400;
16561
- var DEFAULT_TIMEOUT_MS = 3e4;
16562
- var CLI_VERSION = "0.9.0";
16779
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
16780
+ var CLI_VERSION = "0.9.2";
16563
16781
  var USER_AGENT = buildUserAgent();
16564
16782
  function buildUserAgent() {
16565
16783
  let osTag = "";
@@ -16804,7 +17022,7 @@ var DashboardClient = class {
16804
17022
  );
16805
17023
  }
16806
17024
  }
16807
- this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
17025
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
16808
17026
  this.pats = new PATsNamespace(this);
16809
17027
  this.apps = new AppsNamespace(this);
16810
17028
  this.keys = new KeysNamespace2(this);
@@ -17544,6 +17762,13 @@ var AgentsNamespace2 = class {
17544
17762
  return expectDict(body, "agents.undeprecate_key", 200);
17545
17763
  }
17546
17764
  };
17765
+ function isRedirectUriAliasSpec(value) {
17766
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
17767
+ return false;
17768
+ }
17769
+ const spec = value;
17770
+ return typeof spec.label === "string" && typeof spec.description === "string";
17771
+ }
17547
17772
  function isSharedDevProviderEntry(value) {
17548
17773
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
17549
17774
  return false;
@@ -17565,7 +17790,7 @@ function isProviderCatalogEntry(value) {
17565
17790
  const v = value;
17566
17791
  return typeof v.id === "string" && typeof v.name === "string" && typeof v.display_name === "string" && (v.category === null || typeof v.category === "string") && (v.supports_refresh === null || typeof v.supports_refresh === "boolean") && (v.supports_pkce === null || typeof v.supports_pkce === "boolean") && (v.available_environments === void 0 || Array.isArray(v.available_environments) && v.available_environments.every(
17567
17792
  (environment) => environment === "production" || environment === "sandbox"
17568
- )) && (v.scopes_configured_on_provider === void 0 || typeof v.scopes_configured_on_provider === "boolean") && v.available_scopes !== null && typeof v.available_scopes === "object" && !Array.isArray(v.available_scopes) && Object.values(v.available_scopes).every(
17793
+ )) && (v.scopes_configured_on_provider === void 0 || typeof v.scopes_configured_on_provider === "boolean") && (v.redirect_uri_alias === void 0 || v.redirect_uri_alias === null || isRedirectUriAliasSpec(v.redirect_uri_alias)) && v.available_scopes !== null && typeof v.available_scopes === "object" && !Array.isArray(v.available_scopes) && Object.values(v.available_scopes).every(
17569
17794
  isProviderScopeCatalogEntry
17570
17795
  ) && (v.managed_credentials_available === void 0 || typeof v.managed_credentials_available === "boolean") && (v.managed_approved_scopes === void 0 || v.managed_approved_scopes === null || Array.isArray(v.managed_approved_scopes) && v.managed_approved_scopes.every(
17571
17796
  (scope) => typeof scope === "string"
@@ -17660,7 +17885,8 @@ var ProvidersNamespace = class {
17660
17885
  client_secret: options.client_secret,
17661
17886
  environment: options.environment,
17662
17887
  scopes: options.scopes,
17663
- redirect_uris: options.redirect_uris
17888
+ redirect_uris: options.redirect_uris,
17889
+ redirect_uri_alias: options.redirect_uri_alias
17664
17890
  });
17665
17891
  const body = await this.#client._call(
17666
17892
  "POST",
@@ -17697,6 +17923,7 @@ var ProvidersNamespace = class {
17697
17923
  environment: options.environment,
17698
17924
  scopes: options.scopes,
17699
17925
  redirect_uris: options.redirect_uris,
17926
+ redirect_uri_alias: options.redirect_uri_alias,
17700
17927
  status: options.status
17701
17928
  });
17702
17929
  const body = await this.#client._call(
@@ -19535,6 +19762,38 @@ function resolveAppId(flagValue, options = {}) {
19535
19762
  return null;
19536
19763
  }
19537
19764
 
19765
+ // ../shared-utils/src/dns-host.ts
19766
+ var LABEL_SEPARATOR = /[.。.。]/;
19767
+ var TRAILING_LABEL_SEPARATOR = /[.。.。]$/;
19768
+ var isInvalidLabel = (label) => label.length === 0 || label.includes("_") || label.startsWith("-") || label.endsWith("-") || label.length >= 4 && label[2] === "-" && label[3] === "-" && !label.startsWith("xn--");
19769
+ 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;
19770
+ var isOutsideIdna2008Repertoire = (label) => !IDNA2008_LABEL.test(label);
19771
+ var canonicalizeDnsHost = (host) => {
19772
+ let h = host;
19773
+ if (!/^[\x20-\x7e]*$/.test(h)) {
19774
+ if (/[/:@\s?#%\\]/.test(h)) return null;
19775
+ const uLabels = h.normalize("NFKC").replace(TRAILING_LABEL_SEPARATOR, "").split(LABEL_SEPARATOR);
19776
+ if (uLabels.some(isInvalidLabel)) return null;
19777
+ if (uLabels.some(isOutsideIdna2008Repertoire)) return null;
19778
+ let url2;
19779
+ try {
19780
+ url2 = new URL(`http://${h}`);
19781
+ } catch {
19782
+ return null;
19783
+ }
19784
+ if (url2.pathname !== "/" || url2.search !== "" || url2.hash !== "" || url2.port !== "" || url2.username !== "" || url2.password !== "") {
19785
+ return null;
19786
+ }
19787
+ h = url2.hostname;
19788
+ if (!/^[\x20-\x7e]*$/.test(h)) return null;
19789
+ h = h.replace(/\.$/, "");
19790
+ if (h.split(".").some(isInvalidLabel)) return null;
19791
+ }
19792
+ if (h.length > 253) return null;
19793
+ if (h.split(".").some((label) => label.length > 63)) return null;
19794
+ return h;
19795
+ };
19796
+
19538
19797
  // src/commands/_helpers.ts
19539
19798
  async function withClient(fn) {
19540
19799
  const client = await createPortalClient();
@@ -20087,9 +20346,9 @@ function parseAllowedHostsOrExit(flag, entries) {
20087
20346
  fail(`${JSON.stringify(entry)} must not contain control characters`);
20088
20347
  }
20089
20348
  }
20090
- if (/[/:@\s]/.test(e)) {
20349
+ if (/[/:@\s?#%\\]/.test(e)) {
20091
20350
  fail(
20092
- `${JSON.stringify(entry)} must be a bare host \u2014 no scheme, port, path, or "@"`
20351
+ `${JSON.stringify(entry)} must be a bare host \u2014 no scheme, port, path, "@", whitespace, or the URL delimiters ? # % \\`
20093
20352
  );
20094
20353
  }
20095
20354
  let normalized;
@@ -20100,12 +20359,23 @@ function parseAllowedHostsOrExit(flag, entries) {
20100
20359
  `${JSON.stringify(entry)} is a malformed wildcard \u2014 use "*.domain.tld"`
20101
20360
  );
20102
20361
  }
20103
- if (!rest.includes(".")) {
20362
+ const canonicalRest = canonicalizeDnsHost(rest);
20363
+ if (canonicalRest === null) {
20364
+ fail(
20365
+ `${JSON.stringify(entry)} is not a valid DNS name (IDNA-encodable, labels up to 63 characters)`
20366
+ );
20367
+ }
20368
+ if (!canonicalRest.includes(".")) {
20104
20369
  fail(
20105
20370
  `${JSON.stringify(entry)}: wildcard must cover a domain, not a bare TLD (use "*.example.com", not "*.com")`
20106
20371
  );
20107
20372
  }
20108
- normalized = `*.${rest}`;
20373
+ if (canonicalRest.length > 251) {
20374
+ fail(
20375
+ `${JSON.stringify(entry)} is too long for a wildcard \u2014 no host could ever match it`
20376
+ );
20377
+ }
20378
+ normalized = `*.${canonicalRest}`;
20109
20379
  } else if (e.includes("*")) {
20110
20380
  fail(
20111
20381
  `${JSON.stringify(entry)}: "*" is only allowed as a leading subdomain wildcard ("*.domain.tld")`
@@ -20114,9 +20384,16 @@ function parseAllowedHostsOrExit(flag, entries) {
20114
20384
  normalized = e.replace(/\.+$/, "");
20115
20385
  if (!normalized) {
20116
20386
  fail(
20117
- `${JSON.stringify(entry)} must be a bare host \u2014 no scheme, port, path, or "@"`
20387
+ `${JSON.stringify(entry)} must be a bare host \u2014 no scheme, port, path, "@", whitespace, or the URL delimiters ? # % \\`
20388
+ );
20389
+ }
20390
+ const canonical = canonicalizeDnsHost(normalized);
20391
+ if (canonical === null) {
20392
+ fail(
20393
+ `${JSON.stringify(entry)} is not a valid DNS name (IDNA-encodable, labels up to 63 characters)`
20118
20394
  );
20119
20395
  }
20396
+ normalized = canonical;
20120
20397
  }
20121
20398
  if (seen.has(normalized)) continue;
20122
20399
  seen.add(normalized);
@@ -20372,6 +20649,20 @@ function surfaceCatalogWarnings(row) {
20372
20649
  );
20373
20650
  }
20374
20651
  }
20652
+ function surfaceScopeWarnings(row) {
20653
+ if (typeof row !== "object" || row === null) return;
20654
+ const warnings = row.scope_warnings;
20655
+ if (!Array.isArray(warnings)) return;
20656
+ for (const warning of warnings) {
20657
+ if (typeof warning !== "string") continue;
20658
+ const sanitized = sanitizeStderrText(warning);
20659
+ if (sanitized.length === 0) continue;
20660
+ process.stderr.write(
20661
+ `alter: scope warning \u2014 ${sanitized} (save was allowed)
20662
+ `
20663
+ );
20664
+ }
20665
+ }
20375
20666
  function writeNextPageHint(page, noun) {
20376
20667
  if (typeof page !== "object" || page === null) return;
20377
20668
  const { has_more: hasMore, offset, limit } = page;
@@ -20534,7 +20825,9 @@ function buildAgentsCommand() {
20534
20825
  });
20535
20826
  }
20536
20827
  );
20537
- agents.command("create").description("Create a managed agent (returns one-shot API key)").option(APP_FLAG, APP_TARGET_DESC).option(
20828
+ agents.command("create").description(
20829
+ "Create a managed agent (returns its first API key ONCE, in the api_key field)"
20830
+ ).option(APP_FLAG, APP_TARGET_DESC).option(
20538
20831
  "--name <name>",
20539
20832
  "Agent name (stable identifier). Required unless --input is supplied."
20540
20833
  ).option("--display-name <name>", "Human-readable display name").option("--type <type>", "Type: agent|service (default: agent)", "agent").option(
@@ -20566,6 +20859,7 @@ function buildAgentsCommand() {
20566
20859
  "alter: the api_key field above is shown ONCE and cannot be retrieved later.\n"
20567
20860
  );
20568
20861
  surfaceApproverWarnings(result);
20862
+ surfaceScopeWarnings(result);
20569
20863
  });
20570
20864
  return;
20571
20865
  }
@@ -20621,6 +20915,7 @@ function buildAgentsCommand() {
20621
20915
  "alter: the api_key field above is shown ONCE and cannot be retrieved later.\n"
20622
20916
  );
20623
20917
  surfaceApproverWarnings(result);
20918
+ surfaceScopeWarnings(result);
20624
20919
  });
20625
20920
  }
20626
20921
  );
@@ -20840,6 +21135,7 @@ function buildAgentsCommand() {
20840
21135
  }
20841
21136
  );
20842
21137
  emit2(format, row);
21138
+ surfaceScopeWarnings(row);
20843
21139
  } catch (error51) {
20844
21140
  if (error51 instanceof PortalBackendError && error51.statusCode === 409 && error51.code === "agent_concurrent_update") {
20845
21141
  throw new PortalBackendError(
@@ -20936,7 +21232,9 @@ function buildAgentsCommand() {
20936
21232
  `);
20937
21233
  });
20938
21234
  });
20939
- agents.command("mint-key").description("Mint a fresh API key for the agent (plaintext returned ONCE)").option(APP_FLAG, APP_TARGET_DESC).requiredOption(
21235
+ agents.command("mint-key").description(
21236
+ "Mint a fresh API key for the agent (plaintext returned ONCE, in the api_key field)"
21237
+ ).option(APP_FLAG, APP_TARGET_DESC).requiredOption(
20940
21238
  "--agent <agent-id>",
20941
21239
  "Agent ID",
20942
21240
  parseUuidArgument("--agent")
@@ -35908,7 +36206,7 @@ var RULE_TYPE_LABELS = /* @__PURE__ */ new Map([
35908
36206
  ["json_match", "Request condition"],
35909
36207
  ["ip_allowlist", "IP allowlist"],
35910
36208
  ["time_window", "Time window"],
35911
- ["require_approval", "Require approval"],
36209
+ ["require_approval", "Human in the loop (HITL)"],
35912
36210
  ["restriction", "Method and endpoint allowlist"],
35913
36211
  ["quota", "Request quota"],
35914
36212
  ["content_match", "Operation and parameter policy"]
@@ -36334,10 +36632,13 @@ function buildPolicyRulePresentation(rule) {
36334
36632
  });
36335
36633
  }
36336
36634
  if (rule.display?.inherited) {
36337
- displayItems.push({ label: "Relationship", value: "Inherited policy" });
36635
+ displayItems.push({
36636
+ label: "Relationship",
36637
+ value: "Inherited runtime policy"
36638
+ });
36338
36639
  }
36339
36640
  if (displayItems.length > 0) {
36340
- groups.unshift({ label: "Policy scope", items: displayItems });
36641
+ groups.unshift({ label: "Runtime policy scope", items: displayItems });
36341
36642
  }
36342
36643
  return {
36343
36644
  typeLabel: policyRuleTypeLabel(rule.rule_type),
@@ -36350,7 +36651,10 @@ function formatPolicyRuleAsText(rule) {
36350
36651
  const lines = [
36351
36652
  ...rule.name ? [`Name: ${rule.name}`] : [],
36352
36653
  ...rule.id ? [`Policy ID: ${rule.id}`] : [],
36353
- `Type: ${presentation.typeLabel}`,
36654
+ // The friendly label matches the dashboard; the wire identifier is what
36655
+ // `alter policy rules create --type <rule_type>` accepts, so the text
36656
+ // view prints both for operators who script from it.
36657
+ `Type: ${presentation.typeLabel} [${rule.rule_type}]`,
36354
36658
  `Effect: ${presentation.effectLabel}`,
36355
36659
  `Status: ${rule.enabled === false ? "Disabled" : "Enabled"}`,
36356
36660
  presentation.summary,
@@ -36865,7 +37169,7 @@ function buildAppsCommand() {
36865
37169
  "table"
36866
37170
  ).option(
36867
37171
  "--no-include-stats",
36868
- "Skip per-app statistics (grant / key / API-call counts)"
37172
+ "Skip per-app statistics (grant / connection / secret / provider / key / API-call counts)"
36869
37173
  ).option(
36870
37174
  "--include-archived",
36871
37175
  "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)."
@@ -38012,7 +38316,25 @@ import { hostname as hostname3, platform as platform2 } from "os";
38012
38316
  import { spawn } from "child_process";
38013
38317
  var MIN_EPHEMERAL_PORT = 49152;
38014
38318
  var MAX_EPHEMERAL_PORT = 65535;
38015
- var LISTENER_TIMEOUT_MS = 12e4;
38319
+ var LISTENER_TIMEOUT_MS = 6e5;
38320
+ function describeDeadline(ms) {
38321
+ if (ms >= 6e4 && ms % 6e4 === 0) {
38322
+ const minutes = ms / 6e4;
38323
+ return `${minutes} minute${minutes === 1 ? "" : "s"}`;
38324
+ }
38325
+ if (ms >= 1e3) {
38326
+ const seconds = Math.round(ms / 1e3);
38327
+ return `${seconds} second${seconds === 1 ? "" : "s"}`;
38328
+ }
38329
+ return `${ms} ms`;
38330
+ }
38331
+ var SELF_INSPECTION_SCOPE = "dashboard_pats:read";
38332
+ function withSelfInspectionScope(scopes) {
38333
+ if (scopes.includes(SELF_INSPECTION_SCOPE)) {
38334
+ return { scopes: [...scopes], added: false };
38335
+ }
38336
+ return { scopes: [...scopes, SELF_INSPECTION_SCOPE], added: true };
38337
+ }
38016
38338
  var CALLBACK_STATE_PATTERN = /^[0-9a-f]{64}$/;
38017
38339
  var DEFAULT_SCOPES = [
38018
38340
  "dashboard_apps:read",
@@ -38090,7 +38412,9 @@ function pickEphemeralPort() {
38090
38412
  }
38091
38413
  async function runBrowserDance(options) {
38092
38414
  const state = randomBytes2(32).toString("hex");
38093
- const scopes = options.scopes ?? DEFAULT_SCOPES;
38415
+ const { scopes, added: addedSelfInspectionScope } = withSelfInspectionScope(
38416
+ options.scopes ?? DEFAULT_SCOPES
38417
+ );
38094
38418
  const dashboardUrl = options.dashboardUrl ?? deriveDashboardUrl(options.baseUrl);
38095
38419
  const openBrowser = options.openBrowser ?? defaultOpenBrowser;
38096
38420
  const timeoutMs = options.timeoutMs ?? LISTENER_TIMEOUT_MS;
@@ -38145,7 +38469,7 @@ async function runBrowserDance(options) {
38145
38469
  cleanupListener();
38146
38470
  reject(
38147
38471
  new Error(
38148
- "browser-dance timed out \u2014 operator did not complete the consent flow within 2 minutes"
38472
+ `browser-dance timed out \u2014 operator did not complete the consent flow within ${describeDeadline(timeoutMs)}`
38149
38473
  )
38150
38474
  );
38151
38475
  }, timeoutMs);
@@ -38303,9 +38627,15 @@ async function runBrowserDance(options) {
38303
38627
  bindQuery = `&bind=cidr&cidr=${encodeURIComponent(options.cidrValue)}`;
38304
38628
  }
38305
38629
  const url2 = `${dashboardUrl}/cli-auth?port=${port}&state=${state}&scopes=${scopeQuery}&hostname=${encodeURIComponent(hostname3())}${bindQuery}`;
38630
+ if (addedSelfInspectionScope) {
38631
+ process.stderr.write(
38632
+ `alter: added ${SELF_INSPECTION_SCOPE} to the requested scopes \u2014 the CLI verifies the sign-in (and \`alter auth status\`) with it.
38633
+ `
38634
+ );
38635
+ }
38306
38636
  process.stdout.write(
38307
38637
  `alter: opening browser at ${dashboardUrl}/cli-auth
38308
- (waiting up to 2 minutes for you to approve)\u2026
38638
+ (waiting up to ${describeDeadline(timeoutMs)} for you to approve)\u2026
38309
38639
  `
38310
38640
  );
38311
38641
  openBrowser(url2);
@@ -38951,7 +39281,7 @@ function buildAuthCommand() {
38951
39281
  "Override the dashboard URL used by the browser-dance flow. Defaults to the backend URL with the leftmost `backend.` subdomain swapped for `portal.`. Use this when the dashboard isn't at the regex-derivable host (e.g. staging / self-hosted setups). Must use https://."
38952
39282
  ).option(
38953
39283
  "--scopes <list>",
38954
- "Comma-separated scope list to request on the browser-dance consent page (e.g. 'dashboard_apps:read,dashboard_keys:write'). Defaults to the full read+write set across the CLI surface \u2014 enough for every non-destructive day-to-day command. Opt in to admin / delete tiers explicitly: 'dashboard_keys:admin' for runtime-key admin ops, 'dashboard_apps:admin' to archive/unarchive an app, and 'dashboard_apps:delete' for cascade-delete an app (note the Destructive-Action Policy keeps :delete as its own verb, separate from :admin \u2014 :admin does not grant the cascade). Each entry is validated client-side against the dashboard scope catalog \u2014 typos fail fast. Mutually exclusive with --token / --token-file / --token-stdin \u2014 pre-minted PATs have their scope set baked at mint time on the dashboard's Settings page; combining the flags is a usage error."
39284
+ "Comma-separated scope list to request on the browser-dance consent page (e.g. 'dashboard_apps:read,dashboard_keys:write'). Defaults to the full read+write set across the CLI surface \u2014 enough for every non-destructive day-to-day command. Opt in to admin / delete tiers explicitly: 'dashboard_keys:admin' for runtime-key admin ops, 'dashboard_apps:admin' to archive/unarchive an app, and 'dashboard_apps:delete' for cascade-delete an app (note the Destructive-Action Policy keeps :delete as its own verb, separate from :admin \u2014 :admin does not grant the cascade). The list REPLACES the default set, except that dashboard_pats:read is always added: the CLI verifies every sign-in (and `alter auth status`) with it, so a PAT without it cannot complete the login. Each entry is validated client-side against the dashboard scope catalog \u2014 typos fail fast. Mutually exclusive with --token / --token-file / --token-stdin \u2014 pre-minted PATs have their scope set baked at mint time on the dashboard's Settings page; combining the flags is a usage error."
38955
39285
  ).option(
38956
39286
  "--no-bind-ip",
38957
39287
  "Explicit opt-out of IP binding. This is now the DEFAULT for the browser-dance flow, so the flag is a no-op alias retained for scripts that pass it explicitly. The resulting PAT works from any source IP. Browser-dance flow only."
@@ -40696,7 +41026,9 @@ function buildKeysCommand() {
40696
41026
  emit2(format, row);
40697
41027
  });
40698
41028
  });
40699
- keys.command("mint").description("Mint a new scoped key (plaintext returned ONCE)").option(APP_FLAG, APP_TARGET_DESC).option(
41029
+ keys.command("mint").description(
41030
+ "Mint a new scoped key (plaintext returned ONCE, in the plain_key field \u2014 api_key is the key's metadata; capture it with --output json --fields plain_key or jq -r .plain_key)"
41031
+ ).option(APP_FLAG, APP_TARGET_DESC).option(
40700
41032
  "--name <name>",
40701
41033
  "Display name. Required unless --input is supplied."
40702
41034
  ).option(
@@ -40758,7 +41090,7 @@ function buildKeysCommand() {
40758
41090
  }
40759
41091
  emit2(format, outcome2.result);
40760
41092
  process.stderr.write(
40761
- "alter: the plaintext key above is shown ONCE and cannot be retrieved later.\n"
41093
+ "alter: the plain_key field above is the secret (api_key is metadata); it is shown ONCE and cannot be retrieved later.\n"
40762
41094
  );
40763
41095
  return;
40764
41096
  }
@@ -40851,7 +41183,7 @@ function buildKeysCommand() {
40851
41183
  }
40852
41184
  emit2(format, outcome.result);
40853
41185
  process.stderr.write(
40854
- "alter: the plaintext key above is shown ONCE and cannot be retrieved later.\n"
41186
+ "alter: the plain_key field above is the secret (api_key is metadata); it is shown ONCE and cannot be retrieved later.\n"
40855
41187
  );
40856
41188
  }
40857
41189
  );
@@ -40868,7 +41200,7 @@ function buildKeysCommand() {
40868
41200
  });
40869
41201
  });
40870
41202
  keys.command("rotate").description(
40871
- "Rotate a key (new plaintext returned ONCE; old key enters grace until revoke). The app-key successor receives the organization's current finite default TTL. Requires dashboard_keys:write scope."
41203
+ "Rotate a key (new plaintext returned ONCE, in the new_plain_key field; old key enters grace until revoke). The app-key successor receives the organization's current finite default TTL. Requires dashboard_keys:write scope."
40872
41204
  ).option(APP_FLAG, APP_TARGET_DESC).requiredOption("--key <key-id>", "Key ID", parseUuidArgument("--key")).option(
40873
41205
  "--scopes <list>",
40874
41206
  "Optional new scope set (defaults to the key's current scopes)"
@@ -40893,7 +41225,7 @@ function buildKeysCommand() {
40893
41225
  });
40894
41226
  emit2(format, result);
40895
41227
  process.stderr.write(
40896
- "alter: the new plaintext key above is shown ONCE and cannot be retrieved later.\n"
41228
+ "alter: the new_plain_key field above is the new secret (new_api_key is metadata); it is shown ONCE and cannot be retrieved later.\n"
40897
41229
  );
40898
41230
  });
40899
41231
  }
@@ -40902,7 +41234,7 @@ function buildKeysCommand() {
40902
41234
  "Revoke a key immediately (cascades to derived keys). Requires dashboard_keys:admin scope."
40903
41235
  ).option(APP_FLAG, APP_TARGET_DESC).requiredOption("--key <key-id>", "Key ID", parseUuidArgument("--key")).option(
40904
41236
  "--force",
40905
- "Override the last-active-key guard (revoking an agent's only active key bricks it until a fresh key is minted)"
41237
+ "Override the last-active-key guard (revoking the last active key on an app or an agent leaves whatever authenticates with it broken until a fresh key is minted)"
40906
41238
  ).option("--yes", "Skip the interactive y/N prompt").action(
40907
41239
  async (options) => {
40908
41240
  const resolvedAppId = await resolveAppOrExit(options.app);
@@ -41066,6 +41398,7 @@ async function runStatus() {
41066
41398
  // src/commands/managed-secrets.ts
41067
41399
  import { readFileSync as readFileSync9 } from "fs";
41068
41400
  import { Command as Command16, Option } from "commander";
41401
+ var MAX_SEARCH_LENGTH = 255;
41069
41402
  var PRINCIPAL_TYPES = ["user", "group", "system", "agent"];
41070
41403
  var CREDENTIAL_TYPES = [
41071
41404
  "bearer_token",
@@ -41470,6 +41803,13 @@ function surfaceManagedSecretResponse(row) {
41470
41803
  }
41471
41804
  surfaceApproverWarnings(row);
41472
41805
  }
41806
+ function derivedAllowlistMessage(e) {
41807
+ if (!(e instanceof PortalBackendError) || e.statusCode !== 409 || e.code !== "managed_secret_allowed_hosts_derived") {
41808
+ return null;
41809
+ }
41810
+ const detail = e.body?.detail;
41811
+ return typeof detail?.message === "string" ? sanitizeStderrText(detail.message) : "This secret's allowed hosts are derived from its provider binding and cannot be edited directly.";
41812
+ }
41473
41813
  function groupPrincipalGateMessage(e) {
41474
41814
  if (!(e instanceof PortalBackendError) || e.statusCode !== 422 || e.code !== "group_principal_unsupported_idp") {
41475
41815
  return null;
@@ -41806,7 +42146,11 @@ var TEMPLATE_COLUMNS = [
41806
42146
  get: (t) => t.form_schema?.fields.map(
41807
42147
  (field) => `${credentialFieldFlag(field)}${field.required ? "" : " (optional)"}`
41808
42148
  ).join(" ") || "\u2014"
41809
- }
42149
+ },
42150
+ // The field a bound template derives its allowlist from — the operator
42151
+ // needs it to know which --credential-field sets (and later retargets,
42152
+ // via rotate) the secret's allowed hosts.
42153
+ { label: "BINDING", get: (t) => t.allowed_hosts_from_field ?? "\u2014" }
41810
42154
  ];
41811
42155
  var SECRET_COLUMNS = [
41812
42156
  { label: "ID", get: (s) => s.id, maxWidth: 36 },
@@ -41818,10 +42162,20 @@ var SECRET_COLUMNS = [
41818
42162
  { label: "CREATED", get: (s) => s.created_at },
41819
42163
  { label: "UPDATED", get: (s) => s.updated_at ?? "\u2014" }
41820
42164
  ];
42165
+ var DEPTH_COLUMN = {
42166
+ label: "DEPTH",
42167
+ get: (g) => g.depth == null ? "?" : String(g.depth)
42168
+ };
42169
+ var PARENT_COLUMN = {
42170
+ label: "PARENT",
42171
+ get: (g) => g.parent_grant_id === void 0 ? "?" : g.parent_grant_id ?? "\u2014",
42172
+ maxWidth: 36
42173
+ };
41821
42174
  var GRANT_COLUMNS2 = [
41822
42175
  { label: "GRANT_ID", get: (g) => g.grant_id, maxWidth: 36 },
41823
42176
  { label: "PRINCIPAL", get: (g) => g.principal_type },
41824
42177
  { label: "LABEL", get: (g) => g.label ?? "\u2014", maxWidth: 24 },
42178
+ DEPTH_COLUMN,
41825
42179
  { label: "STATUS", get: (g) => g.status },
41826
42180
  { label: "CREATED", get: (g) => g.created_at },
41827
42181
  {
@@ -41833,6 +42187,9 @@ var GRANT_COLUMNS2 = [
41833
42187
  get: (g) => g.grant_expires_at ?? "\u2014"
41834
42188
  }
41835
42189
  ];
42190
+ var AGENT_GRANT_COLUMNS = GRANT_COLUMNS2.flatMap(
42191
+ (col) => col === DEPTH_COLUMN ? [col, PARENT_COLUMN] : [col]
42192
+ );
41836
42193
  var USER_COLUMNS = [
41837
42194
  { label: "ID", get: (u) => u.id, maxWidth: 36 },
41838
42195
  { label: "EMAIL", get: (u) => u.email ?? "\u2014" },
@@ -41909,7 +42266,9 @@ function buildGrantsSubcommand() {
41909
42266
  label: isOptionalString,
41910
42267
  created_at: isString,
41911
42268
  expires_at: isOptionalString,
41912
- grant_expires_at: isAbsentOrOptionalString
42269
+ grant_expires_at: isAbsentOrOptionalString,
42270
+ parent_grant_id: isAbsentOrOptionalString,
42271
+ depth: isAbsentOrOptionalNumber
41913
42272
  },
41914
42273
  "managed-secrets.grants.list"
41915
42274
  );
@@ -41957,11 +42316,14 @@ function buildGrantsSubcommand() {
41957
42316
  label: isOptionalString,
41958
42317
  created_at: isString,
41959
42318
  expires_at: isOptionalString,
41960
- grant_expires_at: isAbsentOrOptionalString
42319
+ grant_expires_at: isAbsentOrOptionalString,
42320
+ // Rendered through ``AGENT_GRANT_COLUMNS`` (adds PARENT).
42321
+ parent_grant_id: isAbsentOrOptionalString,
42322
+ depth: isAbsentOrOptionalNumber
41961
42323
  },
41962
42324
  "managed-secrets.grants.list-for-agent"
41963
42325
  );
41964
- emit2(format, rows, GRANT_COLUMNS2);
42326
+ emit2(format, rows, AGENT_GRANT_COLUMNS);
41965
42327
  });
41966
42328
  }
41967
42329
  );
@@ -42275,7 +42637,7 @@ function buildGroupsSubcommand() {
42275
42637
  );
42276
42638
  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
42639
  "--search <substring>",
42278
- "Substring search on group name / external_group_id"
42640
+ "Substring search on group name / external_group_id (max 255 chars)"
42279
42641
  ).option(
42280
42642
  "--limit <n>",
42281
42643
  "Page size (default: 50)",
@@ -42292,6 +42654,12 @@ function buildGroupsSubcommand() {
42292
42654
  async (options) => {
42293
42655
  const format = coerceOutputFormat(options.output);
42294
42656
  if (options.idp !== void 0) validateUuidOrExit("--idp", options.idp);
42657
+ if (options.search !== void 0)
42658
+ validateMaxLengthOrExit(
42659
+ "--search",
42660
+ options.search,
42661
+ MAX_SEARCH_LENGTH
42662
+ );
42295
42663
  const resolvedAppId = await resolveAppOrExit(options.app);
42296
42664
  await withClient(async (client) => {
42297
42665
  const response = await client.managedSecrets.listGroups(
@@ -42404,7 +42772,8 @@ function buildManagedSecretsCommand() {
42404
42772
  credential_type: isString,
42405
42773
  category: isOptionalString,
42406
42774
  popular: (v) => typeof v === "boolean",
42407
- form_schema: isTemplateFormSchema
42775
+ form_schema: isTemplateFormSchema,
42776
+ allowed_hosts_from_field: isAbsentOrOptionalString
42408
42777
  },
42409
42778
  "managed-secrets.templates"
42410
42779
  );
@@ -42788,11 +43157,25 @@ function buildManagedSecretsCommand() {
42788
43157
  }
42789
43158
  const hostList = options.clear ? null : parseAllowedHostsOrExit("--host", options.host);
42790
43159
  await withClient(async (client) => {
42791
- const row = await client.managedSecrets.setAllowedHosts(
42792
- appId,
42793
- secretId,
42794
- hostList
42795
- );
43160
+ let row;
43161
+ try {
43162
+ row = await client.managedSecrets.setAllowedHosts(
43163
+ appId,
43164
+ secretId,
43165
+ hostList
43166
+ );
43167
+ } catch (e) {
43168
+ const derived = derivedAllowlistMessage(e);
43169
+ if (derived !== null) {
43170
+ process.stderr.write(`alter: ${derived}
43171
+ `);
43172
+ process.stderr.write(
43173
+ "alter: rotate the credential (`alter managed-secrets rotate`) to change its binding\n"
43174
+ );
43175
+ process.exit(EXIT_CONFLICT);
43176
+ }
43177
+ throw e;
43178
+ }
42796
43179
  emit2(format, row);
42797
43180
  if (options.clear) {
42798
43181
  process.stderr.write(
@@ -43078,7 +43461,7 @@ function buildManagedSecretsCommand() {
43078
43461
  );
43079
43462
  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
43463
  "--search <substring>",
43081
- "Substring search on email / display_name / external_subject_id"
43464
+ "Substring search on email / display_name / external_subject_id (max 255 chars)"
43082
43465
  ).option(
43083
43466
  "--limit <n>",
43084
43467
  "Page size (default: 50)",
@@ -43095,6 +43478,12 @@ function buildManagedSecretsCommand() {
43095
43478
  async (options) => {
43096
43479
  const format = coerceOutputFormat(options.output);
43097
43480
  if (options.idp !== void 0) validateUuidOrExit("--idp", options.idp);
43481
+ if (options.search !== void 0)
43482
+ validateMaxLengthOrExit(
43483
+ "--search",
43484
+ options.search,
43485
+ MAX_SEARCH_LENGTH
43486
+ );
43098
43487
  const resolvedAppId = await resolveAppOrExit(options.app);
43099
43488
  await withClient(async (client) => {
43100
43489
  const response = await client.managedSecrets.listUsers(
@@ -43418,7 +43807,9 @@ function buildIdentityProvidersCommand() {
43418
43807
  const webhook = idp.command("webhook").description(
43419
43808
  "Manage an identity provider's webhook integration (requires dashboard_identity_providers:webhooks)"
43420
43809
  );
43421
- webhook.command("enable").description("Enable webhook integration (returns the signing secret ONCE)").option(APP_FLAG, APP_TARGET_DESC).requiredOption(
43810
+ webhook.command("enable").description(
43811
+ "Enable webhook integration (returns the signing secret ONCE, in the webhook_secret field)"
43812
+ ).option(APP_FLAG, APP_TARGET_DESC).requiredOption(
43422
43813
  "--provider <provider-id>",
43423
43814
  "Identity provider ID",
43424
43815
  parseUuidArgument("--provider")
@@ -43450,7 +43841,7 @@ function buildIdentityProvidersCommand() {
43450
43841
  emit2(format, row);
43451
43842
  if (typeof row["webhook_secret"] === "string") {
43452
43843
  process.stderr.write(
43453
- "alter: the webhook_secret above is shown ONCE and cannot be retrieved later.\n"
43844
+ "alter: the webhook_secret field above is shown ONCE and cannot be retrieved later.\n"
43454
43845
  );
43455
43846
  }
43456
43847
  return null;
@@ -43570,7 +43961,7 @@ alter: re-run with --force --confirm <issuer-host> to revoke those grants and di
43570
43961
  }
43571
43962
  );
43572
43963
  webhook.command("rotate").description(
43573
- "Rotate the webhook signing secret (invalidates the previous; returns the new one ONCE)"
43964
+ "Rotate the webhook signing secret (invalidates the previous; returns the new one ONCE, in the webhook_secret field)"
43574
43965
  ).option(APP_FLAG, APP_TARGET_DESC).requiredOption(
43575
43966
  "--provider <provider-id>",
43576
43967
  "Identity provider ID",
@@ -43618,7 +44009,7 @@ alter: re-run with --force --confirm <issuer-host> to revoke those grants and di
43618
44009
  emit2(format, row);
43619
44010
  if (typeof row["webhook_secret"] === "string") {
43620
44011
  process.stderr.write(
43621
- "alter: the webhook_secret above is shown ONCE and cannot be retrieved later.\n"
44012
+ "alter: the webhook_secret field above is shown ONCE and cannot be retrieved later.\n"
43622
44013
  );
43623
44014
  }
43624
44015
  return null;
@@ -45019,8 +45410,12 @@ function buildPolicyCommand() {
45019
45410
  import { Command as Command20 } from "commander";
45020
45411
  var CREDENTIAL_SOURCES = ["custom", "shared_dev"];
45021
45412
  var PROVIDER_ENVIRONMENTS = ["production", "sandbox"];
45022
- async function providerCatalogError(client, providerId, environment, credentialSource, scopes, scopesRequired = false) {
45023
- if (environment === void 0 && scopes === void 0 && !scopesRequired) {
45413
+ async function providerCatalogError(client, providerId, environment, credentialSource, scopes, options = {}) {
45414
+ const create = options.create === true;
45415
+ const redirectUriAlias = options.redirectUriAlias;
45416
+ const readStoredProvider = options.readStoredProvider;
45417
+ const switchesToCustom = !create && credentialSource === "custom" && readStoredProvider !== void 0;
45418
+ if (environment === void 0 && scopes === void 0 && redirectUriAlias === void 0 && !create && !switchesToCustom) {
45024
45419
  return null;
45025
45420
  }
45026
45421
  if (!providerId) {
@@ -45042,6 +45437,23 @@ async function providerCatalogError(client, providerId, environment, credentialS
45042
45437
  if (environment !== void 0 && credentialSource === "shared_dev" && environment !== "production") {
45043
45438
  return "alter: --credential-source shared_dev only supports --environment production; use custom sandbox credentials\n";
45044
45439
  }
45440
+ const aliasSpec = provider.redirect_uri_alias ?? null;
45441
+ const aliasError = redirectUriAliasError(
45442
+ providerId,
45443
+ aliasSpec,
45444
+ credentialSource,
45445
+ redirectUriAlias,
45446
+ create
45447
+ );
45448
+ if (aliasError !== null) return aliasError;
45449
+ if (switchesToCustom && readStoredProvider !== void 0 && aliasSpec !== null && redirectUriAlias === void 0) {
45450
+ const storedAliasError = await redirectUriAliasStoredStateError(
45451
+ readStoredProvider,
45452
+ providerId,
45453
+ aliasSpec
45454
+ );
45455
+ if (storedAliasError !== null) return storedAliasError;
45456
+ }
45045
45457
  const availableScopes = provider.available_scopes ?? {};
45046
45458
  if (Object.keys(availableScopes).length === 0) {
45047
45459
  if (scopes !== void 0 && scopes.length > 0) {
@@ -45051,7 +45463,7 @@ async function providerCatalogError(client, providerId, environment, credentialS
45051
45463
  }
45052
45464
  return null;
45053
45465
  }
45054
- if (scopesRequired && scopes === void 0) {
45466
+ if (create && scopes === void 0) {
45055
45467
  return `alter: provider '${providerId}' requires at least one scope (pass --scopes, or "scopes" in --input)
45056
45468
  `;
45057
45469
  }
@@ -45082,6 +45494,41 @@ async function providerCatalogError(client, providerId, environment, credentialS
45082
45494
  }
45083
45495
  return null;
45084
45496
  }
45497
+ function redirectUriAliasError(providerId, spec, credentialSource, redirectUriAlias, create) {
45498
+ if (spec === null) {
45499
+ return redirectUriAlias === void 0 ? null : `alter: --redirect-uri-alias is not used by ${providerId}
45500
+ `;
45501
+ }
45502
+ if (redirectUriAlias !== void 0 && credentialSource === "shared_dev") {
45503
+ return `alter: --redirect-uri-alias is only accepted with --credential-source custom (Alter-managed credentials carry their own ${spec.label})
45504
+ `;
45505
+ }
45506
+ const custom3 = credentialSource === void 0 || credentialSource === "custom";
45507
+ if (create && custom3 && redirectUriAlias === void 0) {
45508
+ return redirectUriAliasRequiredError(providerId, spec);
45509
+ }
45510
+ return null;
45511
+ }
45512
+ function redirectUriAliasRequiredError(providerId, spec) {
45513
+ return `alter: ${providerId} requires --redirect-uri-alias (${spec.label}): ${spec.description} (pass --redirect-uri-alias, or "redirect_uri_alias" in --input)
45514
+ `;
45515
+ }
45516
+ async function redirectUriAliasStoredStateError(readStoredProvider, providerId, spec) {
45517
+ let current;
45518
+ try {
45519
+ const row = await readStoredProvider();
45520
+ if (row === null || typeof row !== "object") return null;
45521
+ current = row;
45522
+ } catch {
45523
+ return null;
45524
+ }
45525
+ const storedSource = typeof current.credential_source === "string" ? current.credential_source : "custom";
45526
+ const storedAlias = typeof current.redirect_uri_alias === "string" ? current.redirect_uri_alias.trim() : "";
45527
+ if (storedSource === "shared_dev" || storedAlias.length === 0) {
45528
+ return redirectUriAliasRequiredError(providerId, spec);
45529
+ }
45530
+ return null;
45531
+ }
45085
45532
  function storedProviderReader(client, appId, providerId) {
45086
45533
  let pending2 = null;
45087
45534
  return () => pending2 ??= client.providers.get(appId, providerId);
@@ -45167,6 +45614,16 @@ var PROVIDER_COLUMNS = [
45167
45614
  // between test money and real money.
45168
45615
  { label: "ENV", get: (p) => p.environment ?? "production" },
45169
45616
  { label: "SCOPES", get: (p) => p.scopes.join(",") || "\u2014", maxWidth: 40 },
45617
+ // The provider-issued redirect name (eBay's RuName). Its provider-side
45618
+ // label is on ``list-catalog``'s REDIRECT ALIAS column. Wide enough for a
45619
+ // real RuName (``<user>-<app>-<title>-<token>``, ~40–60 chars) to print
45620
+ // untruncated — an operator copies this column back into a provider
45621
+ // console, so a clipped value is worse than none.
45622
+ {
45623
+ label: "REDIRECT ALIAS",
45624
+ get: (p) => p.redirect_uri_alias ?? "\u2014",
45625
+ maxWidth: 60
45626
+ },
45170
45627
  { label: "GRANTS", get: (p) => String(p.grants_count) },
45171
45628
  { label: "STATUS", get: (p) => p.status },
45172
45629
  {
@@ -45250,6 +45707,22 @@ function collectRedirectUris(value, previous = []) {
45250
45707
  const next = value.split(",").map((s) => s.trim()).filter(Boolean);
45251
45708
  return [...previous, ...next];
45252
45709
  }
45710
+ function validateRedirectUriAliasOrExit(raw, source = "--redirect-uri-alias") {
45711
+ if (raw === void 0) return void 0;
45712
+ const value = raw.trim();
45713
+ if (value.length === 0) {
45714
+ process.stderr.write(`alter: ${source} requires a non-empty value
45715
+ `);
45716
+ process.exit(EXIT_USAGE);
45717
+ }
45718
+ if (value.length > 255) {
45719
+ process.stderr.write(`alter: ${source} must be at most 255 characters
45720
+ `);
45721
+ process.exit(EXIT_USAGE);
45722
+ }
45723
+ return value;
45724
+ }
45725
+ var INPUT_ALIAS_SOURCE = '"redirect_uri_alias" in --input';
45253
45726
  function surfaceProviderResponse(row) {
45254
45727
  if (typeof row !== "object" || row === null) return;
45255
45728
  const r = row;
@@ -45319,6 +45792,7 @@ function buildProvidersCommand() {
45319
45792
  // "production" — defeating the tolerance this line exists for.
45320
45793
  environment: isAbsentOrOptionalString,
45321
45794
  scopes: (v) => Array.isArray(v),
45795
+ redirect_uri_alias: isAbsentOrOptionalString,
45322
45796
  status: isString,
45323
45797
  grants_count: (v) => typeof v === "number"
45324
45798
  },
@@ -45367,6 +45841,13 @@ function buildProvidersCommand() {
45367
45841
  get: (p) => p.managed_approved_scopes === void 0 ? "?" : p.managed_approved_scopes === null ? "\u2014" : p.managed_approved_scopes.join(",") || "(none)",
45368
45842
  maxWidth: 36
45369
45843
  },
45844
+ // Providers that take a provider-issued NAME as redirect_uri
45845
+ // instead of the callback URL show its label (eBay: RuName) —
45846
+ // ``create`` then requires ``--redirect-uri-alias``.
45847
+ {
45848
+ label: "REDIRECT ALIAS",
45849
+ get: (p) => p.redirect_uri_alias?.label ?? "\u2014"
45850
+ },
45370
45851
  { label: "STATUS", get: (p) => p.status }
45371
45852
  ]);
45372
45853
  });
@@ -45395,9 +45876,12 @@ function buildProvidersCommand() {
45395
45876
  "--redirect-uri <uri>",
45396
45877
  "Add a redirect URI. Repeat the flag (``--redirect-uri A --redirect-uri B``) or pass a comma-separated list (``--redirect-uri A,B``); both forms accumulate.",
45397
45878
  collectRedirectUris
45879
+ ).option(
45880
+ "--redirect-uri-alias <name>",
45881
+ "The provider-issued redirect name sent as redirect_uri in place of the callback URL \u2014 required for providers that use one (eBay's RuName); see `alter providers list-catalog`."
45398
45882
  ).option(
45399
45883
  "--input <path>",
45400
- "JSON wire body from file (@/path/to/body.json) or stdin (-). Mirrors ``gh api --input``. Replaces the per-field flags (``--provider``, ``--credential-source``, ``--client-id``, ``--client-secret``, ``--environment``, ``--scopes``, ``--redirect-uri``); the body is sent to ``POST /apps/<app>/providers`` verbatim. If both --input and per-field flags are supplied, --input wins and the per-field flags are ignored (with a warning)."
45884
+ "JSON wire body from file (@/path/to/body.json) or stdin (-). Mirrors ``gh api --input``. Replaces the per-field flags (``--provider``, ``--credential-source``, ``--client-id``, ``--client-secret``, ``--environment``, ``--scopes``, ``--redirect-uri``, ``--redirect-uri-alias``); the body is sent to ``POST /apps/<app>/providers`` verbatim. If both --input and per-field flags are supplied, --input wins and the per-field flags are ignored (with a warning)."
45401
45885
  ).option(
45402
45886
  "--output <format>",
45403
45887
  "Output format: json|jsonl|table (default: json)",
@@ -45415,7 +45899,8 @@ function buildProvidersCommand() {
45415
45899
  "clientSecret",
45416
45900
  "environment",
45417
45901
  "scopes",
45418
- "redirectUri"
45902
+ "redirectUri",
45903
+ "redirectUriAlias"
45419
45904
  ].filter((k) => {
45420
45905
  if (k === "credentialSource")
45421
45906
  return options.credentialSource !== "custom";
@@ -45424,6 +45909,10 @@ function buildProvidersCommand() {
45424
45909
  return options[k] !== void 0;
45425
45910
  });
45426
45911
  warnInputOverridesPerFieldFlags(perField.map(optionKeyToFlag));
45912
+ const bodyRedirectUriAlias = validateRedirectUriAliasOrExit(
45913
+ inputBodyString(body, "redirect_uri_alias"),
45914
+ INPUT_ALIAS_SOURCE
45915
+ );
45427
45916
  const inputPreflightError = await withClient(
45428
45917
  async (client) => {
45429
45918
  const bodyEnvironment = inputBodyString(body, "environment");
@@ -45433,7 +45922,7 @@ function buildProvidersCommand() {
45433
45922
  bodyEnvironment,
45434
45923
  inputBodyString(body, "credential_source"),
45435
45924
  inputBodyStringArray(body, "scopes"),
45436
- true
45925
+ { create: true, redirectUriAlias: bodyRedirectUriAlias }
45437
45926
  );
45438
45927
  if (catalogError !== null) return catalogError;
45439
45928
  const availabilityError = await sharedDevAvailabilityError(
@@ -45502,6 +45991,9 @@ function buildProvidersCommand() {
45502
45991
  validateUrlOrExit("--redirect-uri", uri);
45503
45992
  }
45504
45993
  const redirects = options.redirectUri && options.redirectUri.length > 0 ? options.redirectUri : void 0;
45994
+ const redirectUriAlias = validateRedirectUriAliasOrExit(
45995
+ options.redirectUriAlias
45996
+ );
45505
45997
  const environmentPreflightError = await withClient(
45506
45998
  async (client) => {
45507
45999
  const catalogError = await providerCatalogError(
@@ -45510,7 +46002,7 @@ function buildProvidersCommand() {
45510
46002
  environment,
45511
46003
  credentialSource,
45512
46004
  scopes,
45513
- true
46005
+ { create: true, redirectUriAlias }
45514
46006
  );
45515
46007
  if (catalogError !== null) return catalogError;
45516
46008
  const availabilityError = await sharedDevAvailabilityError(
@@ -45529,7 +46021,8 @@ function buildProvidersCommand() {
45529
46021
  // Scopeless provider with --scopes omitted: the dashboard's
45530
46022
  // exact wire body for these providers is ``scopes: []``.
45531
46023
  scopes: scopes ?? [],
45532
- redirect_uris: redirects
46024
+ redirect_uris: redirects,
46025
+ redirect_uri_alias: redirectUriAlias
45533
46026
  });
45534
46027
  surfaceProviderResponse(row);
45535
46028
  emit2(format, row);
@@ -45563,6 +46056,7 @@ function buildProvidersCommand() {
45563
46056
  credential_source: isOptionalString,
45564
46057
  environment: isAbsentOrOptionalString,
45565
46058
  scopes: (v) => Array.isArray(v),
46059
+ redirect_uri_alias: isAbsentOrOptionalString,
45566
46060
  status: isString,
45567
46061
  grants_count: (v) => typeof v === "number"
45568
46062
  },
@@ -45590,6 +46084,9 @@ function buildProvidersCommand() {
45590
46084
  "--redirect-uri <uri>",
45591
46085
  "Replace redirect URIs. Repeat the flag or pass a comma-separated list; both forms accumulate.",
45592
46086
  collectRedirectUris
46087
+ ).option(
46088
+ "--redirect-uri-alias <name>",
46089
+ "Replace the provider-issued redirect name sent as redirect_uri in place of the callback URL (eBay's RuName). Accepted only for providers that use one; see `alter providers list-catalog`."
45593
46090
  ).option(
45594
46091
  "--status <status>",
45595
46092
  "New status: active | disabled (rejected at the CLI if not exactly one of the two values)"
@@ -45619,24 +46116,33 @@ function buildProvidersCommand() {
45619
46116
  "environment",
45620
46117
  "scopes",
45621
46118
  "redirectUri",
46119
+ "redirectUriAlias",
45622
46120
  "status"
45623
46121
  ].filter((k) => options[k] !== void 0);
45624
46122
  warnInputOverridesPerFieldFlags(perField.map(optionKeyToFlag));
46123
+ const bodyRedirectUriAlias = validateRedirectUriAliasOrExit(
46124
+ inputBodyString(body, "redirect_uri_alias"),
46125
+ INPUT_ALIAS_SOURCE
46126
+ );
45625
46127
  const inputPreflightError = await withClient(
45626
46128
  async (client) => {
46129
+ const readStoredProvider = storedProviderReader(
46130
+ client,
46131
+ appId,
46132
+ options.provider
46133
+ );
45627
46134
  const catalogError = await providerCatalogError(
45628
46135
  client,
45629
46136
  options.provider,
45630
46137
  inputBodyString(body, "environment"),
45631
46138
  inputBodyString(body, "credential_source"),
45632
- inputBodyStringArray(body, "scopes")
46139
+ inputBodyStringArray(body, "scopes"),
46140
+ {
46141
+ redirectUriAlias: bodyRedirectUriAlias,
46142
+ readStoredProvider
46143
+ }
45633
46144
  );
45634
46145
  if (catalogError !== null) return catalogError;
45635
- const readStoredProvider = storedProviderReader(
45636
- client,
45637
- appId,
45638
- options.provider
45639
- );
45640
46146
  const storedStateError = await sharedDevStoredStateError(
45641
46147
  readStoredProvider,
45642
46148
  options.provider,
@@ -45698,21 +46204,25 @@ function buildProvidersCommand() {
45698
46204
  validateUrlOrExit("--redirect-uri", uri);
45699
46205
  }
45700
46206
  const redirects = options.redirectUri && options.redirectUri.length > 0 ? options.redirectUri : void 0;
46207
+ const redirectUriAlias = validateRedirectUriAliasOrExit(
46208
+ options.redirectUriAlias
46209
+ );
45701
46210
  const updatePreflightError = await withClient(
45702
46211
  async (client) => {
46212
+ const readStoredProvider = storedProviderReader(
46213
+ client,
46214
+ appId,
46215
+ options.provider
46216
+ );
45703
46217
  const catalogError = await providerCatalogError(
45704
46218
  client,
45705
46219
  options.provider,
45706
46220
  environment,
45707
46221
  credentialSource,
45708
- scopes
46222
+ scopes,
46223
+ { redirectUriAlias, readStoredProvider }
45709
46224
  );
45710
46225
  if (catalogError !== null) return catalogError;
45711
- const readStoredProvider = storedProviderReader(
45712
- client,
45713
- appId,
45714
- options.provider
45715
- );
45716
46226
  const storedStateError = await sharedDevStoredStateError(
45717
46227
  readStoredProvider,
45718
46228
  options.provider,
@@ -45736,6 +46246,7 @@ function buildProvidersCommand() {
45736
46246
  ...options.skipPreflight ? { skip_preflight: true } : {},
45737
46247
  scopes,
45738
46248
  redirect_uris: redirects,
46249
+ redirect_uri_alias: redirectUriAlias,
45739
46250
  status: providerStatus
45740
46251
  });
45741
46252
  surfaceProviderResponse(row);