@alter-ai/cli 0.8.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.8.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.23.3",
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",
@@ -4618,7 +4622,7 @@ var AlterSDKError = class extends Error {
4618
4622
  constructor(message, details) {
4619
4623
  super(message);
4620
4624
  this.name = "AlterSDKError";
4621
- this.details = details ?? {};
4625
+ this.details = details !== null && typeof details === "object" && !Array.isArray(details) ? details : {};
4622
4626
  Object.setPrototypeOf(this, new.target.prototype);
4623
4627
  }
4624
4628
  toString() {
@@ -4836,9 +4840,9 @@ var InsufficientScopeError = class _InsufficientScopeError extends BackendError
4836
4840
  constructor(message = "Insufficient scope", options = {}) {
4837
4841
  super(message, options.details);
4838
4842
  this.name = "InsufficientScopeError";
4839
- this.required = options.required ?? [];
4840
- this.granted = options.granted ?? [];
4841
- this.missing = options.missing ?? [];
4843
+ this.required = options.required ? [...options.required] : [];
4844
+ this.granted = options.granted ? [...options.granted] : [];
4845
+ this.missing = options.missing ? [...options.missing] : [];
4842
4846
  this.scopeVersion = options.scopeVersion ?? null;
4843
4847
  this.currentScopeVersion = options.currentScopeVersion ?? null;
4844
4848
  this.scopeVersionMismatch = options.scopeVersionMismatch ?? false;
@@ -4871,7 +4875,9 @@ var InsufficientScopeError = class _InsufficientScopeError extends BackendError
4871
4875
  */
4872
4876
  static fromErrorBody(errorData) {
4873
4877
  return new _InsufficientScopeError(
4874
- typeof errorData.message === "string" ? errorData.message : "Insufficient scope",
4878
+ // Non-empty string only (parity with `_messageOr` / Python
4879
+ // `from_error_body`): an empty wire message is as uninformative as none.
4880
+ typeof errorData.message === "string" && errorData.message ? errorData.message : "Insufficient scope",
4875
4881
  {
4876
4882
  // Element-level filtering: a non-array coerces to [], and non-string
4877
4883
  // elements inside an array are dropped (a bare cast would smuggle
@@ -5056,7 +5062,7 @@ var AgentError = class extends BackendError {
5056
5062
  hint;
5057
5063
  constructor(opts = {}) {
5058
5064
  const subclassCode = new.target.code;
5059
- super(opts.message ?? subclassCode, opts.details);
5065
+ super(opts.message || subclassCode, opts.details);
5060
5066
  this.name = "AgentError";
5061
5067
  this.code = subclassCode;
5062
5068
  this.hint = opts.hint;
@@ -6253,7 +6259,11 @@ var ConnectResult = class {
6253
6259
  providerId;
6254
6260
  accountIdentifier;
6255
6261
  scopes;
6256
- /** 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
+ */
6257
6267
  operation;
6258
6268
  grantPolicy;
6259
6269
  /**
@@ -6796,12 +6806,13 @@ var ApprovalResult = class {
6796
6806
  headers: this.headers,
6797
6807
  body_b64: this.bodyB64,
6798
6808
  body_truncated: this.bodyTruncated,
6799
- // Included, unlike the deliberately-omitted ``durationMs``: that one is
6800
- // OBSERVATIONAL (how long this particular call took), while this is part
6801
- // of the result's meaningthe reason the provider rejected the
6802
- // credential. A caller serializing a failure for a log or a bug report
6803
- // needs the explanation to survive; the timing of that one attempt does
6804
- // not.
6809
+ // Include duration_ms so JSON round-trips carry the timing metadata.
6810
+ // The Python twin's ApprovalResult is a plain Pydantic model whose
6811
+ // model_dump() includes EVERY public field duration_ms and
6812
+ // credential_hint alike so omitting either here would make a TS
6813
+ // JSON round-trip lossy where the Python one is not (wire-shape
6814
+ // parity: toJSON() must serialize the same field set as model_dump()).
6815
+ duration_ms: this.durationMs,
6805
6816
  credential_hint: this.credentialHint
6806
6817
  };
6807
6818
  }
@@ -8112,7 +8123,7 @@ async function _raiseForStatus(response) {
8112
8123
  const rawCode = body.error;
8113
8124
  const code = typeof rawCode === "string" ? rawCode : "";
8114
8125
  const rawMessage = body.message;
8115
- const message = typeof rawMessage === "string" ? rawMessage : `Backend error ${response.status}`;
8126
+ const message = typeof rawMessage === "string" && rawMessage ? rawMessage : `Backend error ${response.status}`;
8116
8127
  const rawHint = body.hint;
8117
8128
  const hint = typeof rawHint === "string" ? rawHint : void 0;
8118
8129
  if (code === "insufficient_scope") {
@@ -8120,7 +8131,7 @@ async function _raiseForStatus(response) {
8120
8131
  }
8121
8132
  if (code === "rate_limit_exceeded") {
8122
8133
  throw new RateLimitError(
8123
- typeof body.message === "string" ? body.message : "Rate limit exceeded \u2014 retry after the window resets.",
8134
+ typeof body.message === "string" && body.message ? body.message : "Rate limit exceeded \u2014 retry after the window resets.",
8124
8135
  _parseRetryAfter(response.headers.get("Retry-After"), body.retry_after),
8125
8136
  body,
8126
8137
  typeof body.scope === "string" ? body.scope : void 0,
@@ -8140,7 +8151,7 @@ async function _raiseForStatus(response) {
8140
8151
  if (code === AgentConcurrentUpdateError.code && response.status === 409) {
8141
8152
  throw new AgentConcurrentUpdateError({ message, details: body, hint });
8142
8153
  }
8143
- 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);
8144
8155
  if (ExcCtor !== void 0) {
8145
8156
  throw new ExcCtor({ message, details: body, hint });
8146
8157
  }
@@ -8318,9 +8329,15 @@ var AgentsNamespace = class {
8318
8329
  const limit = options.limit ?? 100;
8319
8330
  const offset = options.offset ?? 0;
8320
8331
  const includeRevoked = options.includeRevoked ?? false;
8332
+ if (!Number.isInteger(limit)) {
8333
+ throw new AlterValueError("limit must be an integer");
8334
+ }
8321
8335
  if (limit < 1 || limit > LIST_LIMIT_MAX) {
8322
8336
  throw new AlterValueError("limit must be between 1 and 1000");
8323
8337
  }
8338
+ if (!Number.isInteger(offset)) {
8339
+ throw new AlterValueError("offset must be an integer");
8340
+ }
8324
8341
  if (offset < 0) {
8325
8342
  throw new AlterValueError("offset must be >= 0");
8326
8343
  }
@@ -8766,7 +8783,7 @@ function _optionalStringArray(raw, key, context) {
8766
8783
  }
8767
8784
  function _normalizeKeyId(value, operation) {
8768
8785
  if (typeof value === "string") {
8769
- if (value.length === 0) {
8786
+ if (value.trim().length === 0) {
8770
8787
  throw new AlterValueError(`${operation}() requires a non-empty keyId`);
8771
8788
  }
8772
8789
  return value;
@@ -9468,6 +9485,26 @@ var SpansNamespace = class {
9468
9485
  `spans[${index}] must be a UserSpan with traceId, name, and startTime`
9469
9486
  );
9470
9487
  }
9488
+ if (span.traceId.length < 1 || span.traceId.length > 255) {
9489
+ throw new AlterValueError(
9490
+ `spans[${index}].traceId must be 1\u2013255 characters`
9491
+ );
9492
+ }
9493
+ if (span.name.length < 1 || span.name.length > 255) {
9494
+ throw new AlterValueError(
9495
+ `spans[${index}].name must be 1\u2013255 characters`
9496
+ );
9497
+ }
9498
+ if (span.spanId !== void 0 && (typeof span.spanId !== "string" || span.spanId.length < 1 || span.spanId.length > 32)) {
9499
+ throw new AlterValueError(
9500
+ `spans[${index}].spanId must be 1\u201332 characters when provided`
9501
+ );
9502
+ }
9503
+ if (span.parentSpanId !== void 0 && (typeof span.parentSpanId !== "string" || span.parentSpanId.length < 1 || span.parentSpanId.length > 32)) {
9504
+ throw new AlterValueError(
9505
+ `spans[${index}].parentSpanId must be 1\u201332 characters when provided`
9506
+ );
9507
+ }
9471
9508
  });
9472
9509
  const body = {
9473
9510
  spans: spans.map((span) => {
@@ -9794,8 +9831,14 @@ var FORBIDDEN_INJECTION_HEADERS2 = /* @__PURE__ */ new Set([
9794
9831
  function _coerceInt(value) {
9795
9832
  return Number.isInteger(value) && value >= 0 ? value : void 0;
9796
9833
  }
9834
+ var IDENTITY_SYNC_POLL_STATUS = {
9835
+ service_unavailable: 503,
9836
+ identity_provider_inactive: 409,
9837
+ identity_group_name_ambiguous: 409
9838
+ };
9797
9839
  function _messageOr(errorData, fallback) {
9798
- return typeof errorData.message === "string" ? errorData.message : fallback;
9840
+ const raw = errorData.message;
9841
+ return typeof raw === "string" && raw ? raw : fallback;
9799
9842
  }
9800
9843
  function _errorPayload(errorData) {
9801
9844
  const nested = errorData.details;
@@ -9918,7 +9961,8 @@ function validateTokenInjectionMetadata(tokenData) {
9918
9961
  let additionalCredentials = null;
9919
9962
  if (additionalCredentialsRaw != null) {
9920
9963
  if (typeof additionalCredentialsRaw !== "object" || Array.isArray(additionalCredentialsRaw) || Object.keys(additionalCredentialsRaw).length > MAX_ADDITIONAL_CREDENTIAL_KEYS || !Object.entries(additionalCredentialsRaw).every(
9921
- ([key, value]) => key.length <= MAX_ADDITIONAL_CREDENTIAL_KEY_LENGTH && typeof value === "string" && value.trim().length > 0 && value.length <= MAX_ADDITIONAL_CREDENTIAL_VALUE_LENGTH && // eslint-disable-next-line no-control-regex
9964
+ ([key, value]) => key.length <= MAX_ADDITIONAL_CREDENTIAL_KEY_LENGTH && typeof value === "string" && // pythonStrip: match the backend's Python str.strip() (see token_type).
9965
+ pythonStrip(value).length > 0 && value.length <= MAX_ADDITIONAL_CREDENTIAL_VALUE_LENGTH && // eslint-disable-next-line no-control-regex
9922
9966
  !/[\r\n\x00]/.test(value)
9923
9967
  )) {
9924
9968
  throw new BackendError("Backend returned invalid additional_credentials");
@@ -10016,7 +10060,23 @@ function isValidOffsetDateTime(value) {
10016
10060
  const hour = Number(hourText);
10017
10061
  const minute = Number(minuteText);
10018
10062
  const second = Number(secondText);
10019
- if (month < 1 || month > 12 || day < 1 || day > new Date(Date.UTC(year, month, 0)).getUTCDate() || hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59) {
10063
+ const isLeapYear = year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
10064
+ const daysInMonth = [
10065
+ 31,
10066
+ isLeapYear ? 29 : 28,
10067
+ 31,
10068
+ 30,
10069
+ 31,
10070
+ 30,
10071
+ 31,
10072
+ 31,
10073
+ 30,
10074
+ 31,
10075
+ 30,
10076
+ 31
10077
+ ];
10078
+ const maxDay = daysInMonth[month - 1] ?? 0;
10079
+ if (year < 1 || month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59) {
10020
10080
  return false;
10021
10081
  }
10022
10082
  return !Number.isNaN(Date.parse(value));
@@ -10036,7 +10096,10 @@ function validateTokenResponseContract(tokenData) {
10036
10096
  throw new BackendError("Backend returned an invalid access_token");
10037
10097
  }
10038
10098
  const tokenType = tokenData.token_type === void 0 ? "Bearer" : tokenData.token_type;
10039
- if (typeof tokenType !== "string" || tokenType.trim().length === 0 || tokenType !== tokenType.trim() || tokenType.length > 255 || // eslint-disable-next-line no-control-regex
10099
+ if (typeof tokenType !== "string" || // pythonStrip (not .trim()): the backend normalizes with Python str.strip(),
10100
+ // whose whitespace set differs from JS trim() at the edges (NEL/BOM/C0
10101
+ // separators). Match the backend so the two SDKs accept/reject identically.
10102
+ pythonStrip(tokenType).length === 0 || tokenType !== pythonStrip(tokenType) || tokenType.length > 255 || // eslint-disable-next-line no-control-regex
10040
10103
  /[\x00-\x1f\x7f]/.test(tokenType)) {
10041
10104
  throw new BackendError("Backend returned an invalid token_type");
10042
10105
  }
@@ -10050,7 +10113,8 @@ function validateTokenResponseContract(tokenData) {
10050
10113
  }
10051
10114
  const scopes = tokenData.scopes === void 0 ? [] : tokenData.scopes;
10052
10115
  if (!Array.isArray(scopes) || scopes.some(
10053
- (scope) => typeof scope !== "string" || scope.trim().length === 0 || scope !== scope.trim() || // eslint-disable-next-line no-control-regex
10116
+ (scope) => typeof scope !== "string" || // pythonStrip: match the backend's Python str.strip() (see token_type).
10117
+ pythonStrip(scope).length === 0 || scope !== pythonStrip(scope) || // eslint-disable-next-line no-control-regex
10054
10118
  /[\x00-\x1f\x7f]/.test(scope)
10055
10119
  )) {
10056
10120
  throw new BackendError("Backend returned invalid scopes");
@@ -10060,7 +10124,8 @@ function validateTokenResponseContract(tokenData) {
10060
10124
  throw new BackendError("Backend returned an invalid grant_id");
10061
10125
  }
10062
10126
  const providerId = tokenData.provider_id === void 0 ? "" : tokenData.provider_id;
10063
- if (typeof providerId !== "string" || providerId !== providerId.trim() || providerId.length > 255 || // eslint-disable-next-line no-control-regex
10127
+ if (typeof providerId !== "string" || // pythonStrip: match the backend's Python str.strip() (see token_type).
10128
+ providerId !== pythonStrip(providerId) || providerId.length > 255 || // eslint-disable-next-line no-control-regex
10064
10129
  /[\x00-\x1f\x7f]/.test(providerId)) {
10065
10130
  throw new BackendError("Backend returned an invalid provider_id");
10066
10131
  }
@@ -10089,7 +10154,10 @@ function isRetryErrorInfoPayload(x) {
10089
10154
  return false;
10090
10155
  }
10091
10156
  const e = x;
10092
- return typeof e.attempt === "number" && Number.isFinite(e.attempt) && e.attempt >= 1 && typeof e.error === "string" && e.error.length <= 500 && typeof e.error_type === "string" && typeof e.delay_s === "number" && Number.isFinite(e.delay_s) && e.delay_s >= 0 && (e.permanent === void 0 || typeof e.permanent === "boolean");
10157
+ return typeof e.attempt === "number" && // Integer-only (not merely finite) mirrors the backend Pydantic `int`
10158
+ // schema and the Python SDK's strict `RetryErrorInfo.attempt`, so a
10159
+ // fractional attempt (e.g. 1.5) is dropped identically in both SDKs.
10160
+ Number.isInteger(e.attempt) && e.attempt >= 1 && typeof e.error === "string" && e.error.length <= 500 && typeof e.error_type === "string" && typeof e.delay_s === "number" && Number.isFinite(e.delay_s) && e.delay_s >= 0 && (e.permanent === void 0 || typeof e.permanent === "boolean");
10093
10161
  }
10094
10162
  var _tokenStore = /* @__PURE__ */ new WeakMap();
10095
10163
  var _additionalCredsStore = /* @__PURE__ */ new WeakMap();
@@ -10112,7 +10180,7 @@ function _extractAdditionalCredentials(token) {
10112
10180
  return _additionalCredsStore.get(token);
10113
10181
  }
10114
10182
  var _fetch;
10115
- var SDK_VERSION = "0.23.3";
10183
+ var SDK_VERSION = "0.24.2";
10116
10184
  var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
10117
10185
  function pyUnquote(s) {
10118
10186
  if (!s.includes("%")) return s;
@@ -10183,8 +10251,12 @@ function canonicalSigningQuery(rawQuery) {
10183
10251
  pairs.sort((p, q) => byCodePoint(p[0], q[0]) || byCodePoint(p[1], q[1]));
10184
10252
  return pairs.map(([k, v]) => `${pyQuotePlus(k)}=${pyQuotePlus(v)}`).join("&");
10185
10253
  }
10254
+ var DEFAULT_TIMEOUT_MS = 95e3;
10255
+ var _ApprovalDeadlineElapsed = class extends Error {
10256
+ };
10186
10257
  var AUTH_POLL_SERVER_WAIT_MS = 25e3;
10187
10258
  var AUTH_POLL_HTTP_BUFFER_MS = 15e3;
10259
+ var APPROVAL_POLL_SERVER_WAIT_MS = 25e3;
10188
10260
  var PERMANENT_POLL_STATUSES = /* @__PURE__ */ new Set([400, 401, 403, 404, 422]);
10189
10261
  var HTTP_FORBIDDEN = 403;
10190
10262
  var HTTP_NO_CONTENT2 = 204;
@@ -10381,7 +10453,14 @@ var PROXY_FORBIDDEN_HEADER_NAMES = /* @__PURE__ */ new Set([
10381
10453
  "x-goog-authorization",
10382
10454
  "x-goog-iam-authorization-token",
10383
10455
  "x-stripe-account",
10384
- "x-shopify-access-token"
10456
+ "x-shopify-access-token",
10457
+ // Method tunnelling: OData V2 (SAP S/4HANA) and several enterprise APIs
10458
+ // honour these as an override of the real HTTP verb. The policy gate
10459
+ // classifies on the wire method, so a tunnelled verb would split the
10460
+ // operation the gate names from the one the provider executes.
10461
+ "x-http-method",
10462
+ "x-http-method-override",
10463
+ "x-method-override"
10385
10464
  ]);
10386
10465
  var MAX_CONTEXT_LENGTH = 4096;
10387
10466
  var MAX_CONTEXT_KEYS = 20;
@@ -10418,9 +10497,10 @@ function validateAndSerializeContext(context) {
10418
10497
  `context keys must be strings, got ${typeof k}`
10419
10498
  );
10420
10499
  }
10421
- if (k.length > MAX_CONTEXT_KEY_LENGTH) {
10500
+ const keyLength = [...k].length;
10501
+ if (keyLength > MAX_CONTEXT_KEY_LENGTH) {
10422
10502
  throw new AlterValueError(
10423
- `context key "${k.slice(0, 32)}" exceeds max length (${k.length} > ${MAX_CONTEXT_KEY_LENGTH})`
10503
+ `context key "${k.slice(0, 32)}" exceeds max length (${keyLength} > ${MAX_CONTEXT_KEY_LENGTH})`
10424
10504
  );
10425
10505
  }
10426
10506
  const v = context[k];
@@ -10429,9 +10509,10 @@ function validateAndSerializeContext(context) {
10429
10509
  `context value for key "${k}" must be a string, got ${typeof v}`
10430
10510
  );
10431
10511
  }
10432
- if (v.length > MAX_CONTEXT_VALUE_LENGTH) {
10512
+ const valueLength = [...v].length;
10513
+ if (valueLength > MAX_CONTEXT_VALUE_LENGTH) {
10433
10514
  throw new AlterValueError(
10434
- `context value for key "${k}" exceeds max length (${v.length} > ${MAX_CONTEXT_VALUE_LENGTH})`
10515
+ `context value for key "${k}" exceeds max length (${valueLength} > ${MAX_CONTEXT_VALUE_LENGTH})`
10435
10516
  );
10436
10517
  }
10437
10518
  }
@@ -10451,6 +10532,23 @@ function validateAndSerializeContext(context) {
10451
10532
  return encoded;
10452
10533
  }
10453
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
+ }
10454
10552
  var HTTP_CLIENT_ERROR_START = 400;
10455
10553
  var MAX_ACTOR_STRING_LENGTH = 255;
10456
10554
  var SAFE_HEADER_PATTERN = /^[\x20-\x7E]+$/;
@@ -10531,11 +10629,28 @@ function connectMetadataToWire(metadata) {
10531
10629
  }
10532
10630
  return wire;
10533
10631
  }
10534
- var GRANT_POLICY_INPUT_KEYS = /* @__PURE__ */ new Set([
10535
- "expiresAt",
10536
- "maxTtlSeconds",
10537
- "defaultTtlSeconds"
10538
- ]);
10632
+ var GRANT_POLICY_CAMEL_TO_WIRE = [
10633
+ ["expiresAt", "expires_at"],
10634
+ ["maxTtlSeconds", "max_ttl_seconds"],
10635
+ ["defaultTtlSeconds", "default_ttl_seconds"]
10636
+ ];
10637
+ var GRANT_POLICY_INPUT_KEYS = new Set(
10638
+ GRANT_POLICY_CAMEL_TO_WIRE.map(([camelKey]) => camelKey)
10639
+ );
10640
+ function assertParseableExpiresAt(value) {
10641
+ if (typeof value !== "string" || value.length === 0 || Number.isNaN(Date.parse(value))) {
10642
+ throw new AlterValueError(
10643
+ "grantPolicy.expiresAt must be a non-empty ISO 8601 string when provided"
10644
+ );
10645
+ }
10646
+ }
10647
+ function assertPositiveIntegerSeconds(camelKey, value) {
10648
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
10649
+ throw new AlterValueError(
10650
+ `grantPolicy.${camelKey} must be a positive integer (seconds) when provided`
10651
+ );
10652
+ }
10653
+ }
10539
10654
  function grantPolicyInputToWire(policy) {
10540
10655
  if (policy === null || typeof policy !== "object" || Array.isArray(policy)) {
10541
10656
  throw new AlterValueError(
@@ -10545,32 +10660,57 @@ function grantPolicyInputToWire(policy) {
10545
10660
  for (const key of Object.keys(policy)) {
10546
10661
  if (!GRANT_POLICY_INPUT_KEYS.has(key)) {
10547
10662
  throw new AlterValueError(
10548
- `grantPolicy contains unknown key "${key}" (allowed: expiresAt, maxTtlSeconds, defaultTtlSeconds). Note: keys are camelCase in the TypeScript SDK \u2014 snake_case keys like max_ttl_seconds are the Python SDK surface.`
10663
+ `grantPolicy contains unknown key "${key}" (allowed: ${[...GRANT_POLICY_INPUT_KEYS].join(", ")}). Note: keys are camelCase in the TypeScript SDK \u2014 snake_case keys like max_ttl_seconds are the Python SDK surface.`
10549
10664
  );
10550
10665
  }
10551
10666
  }
10552
10667
  const wire = {};
10553
- if (policy.expiresAt !== void 0 && policy.expiresAt !== null) {
10554
- if (typeof policy.expiresAt !== "string" || policy.expiresAt.length === 0) {
10668
+ for (const [camelKey, wireKey] of GRANT_POLICY_CAMEL_TO_WIRE) {
10669
+ const value = policy[camelKey];
10670
+ if (value === void 0 || value === null) {
10671
+ continue;
10672
+ }
10673
+ if (camelKey === "expiresAt") {
10674
+ assertParseableExpiresAt(value);
10675
+ } else {
10676
+ assertPositiveIntegerSeconds(camelKey, value);
10677
+ }
10678
+ wire[wireKey] = value;
10679
+ }
10680
+ return wire;
10681
+ }
10682
+ function _mintGrantPolicyToWire(policy) {
10683
+ for (const spelling of ["defaultTtlSeconds", "default_ttl_seconds"]) {
10684
+ if (Object.prototype.hasOwnProperty.call(policy, spelling)) {
10555
10685
  throw new AlterValueError(
10556
- "grantPolicy.expiresAt must be a non-empty ISO 8601 string when provided"
10686
+ `grantPolicy.${spelling} is not supported on mint \u2014 a default TTL is accepted only by Connect sessions (createConnectSession / connect) and createManagedSecretGrant`
10557
10687
  );
10558
10688
  }
10559
- wire.expires_at = policy.expiresAt;
10560
10689
  }
10561
- for (const [camelKey, wireKey] of [
10562
- ["maxTtlSeconds", "max_ttl_seconds"],
10563
- ["defaultTtlSeconds", "default_ttl_seconds"]
10564
- ]) {
10690
+ const wire = {};
10691
+ for (const [key, value] of Object.entries(policy)) {
10692
+ if (!GRANT_POLICY_INPUT_KEYS.has(key)) {
10693
+ wire[key] = value;
10694
+ }
10695
+ }
10696
+ for (const [camelKey, wireKey] of GRANT_POLICY_CAMEL_TO_WIRE) {
10565
10697
  const value = policy[camelKey];
10566
- if (value === void 0 || value === null) {
10698
+ if (value === void 0) {
10567
10699
  continue;
10568
10700
  }
10569
- if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
10701
+ if (Object.prototype.hasOwnProperty.call(policy, wireKey)) {
10570
10702
  throw new AlterValueError(
10571
- `grantPolicy.${camelKey} must be a positive integer (seconds) when provided`
10703
+ `grantPolicy sets both "${camelKey}" and "${wireKey}" \u2014 supply exactly one spelling of the field`
10572
10704
  );
10573
10705
  }
10706
+ if (value === null) {
10707
+ continue;
10708
+ }
10709
+ if (camelKey === "expiresAt") {
10710
+ assertParseableExpiresAt(value);
10711
+ } else {
10712
+ assertPositiveIntegerSeconds(camelKey, value);
10713
+ }
10574
10714
  wire[wireKey] = value;
10575
10715
  }
10576
10716
  return wire;
@@ -10697,6 +10837,16 @@ var HttpClient = class {
10697
10837
  new DOMException("The operation timed out.", "TimeoutError")
10698
10838
  );
10699
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
+ }
10700
10850
  const init = {
10701
10851
  method,
10702
10852
  headers: mergedHeaders,
@@ -10735,6 +10885,9 @@ var HttpClient = class {
10735
10885
  return response;
10736
10886
  } finally {
10737
10887
  clearTimeout(timeoutId);
10888
+ if (callerSignal !== void 0 && onCallerAbort !== void 0) {
10889
+ callerSignal.removeEventListener("abort", onCallerAbort);
10890
+ }
10738
10891
  }
10739
10892
  }
10740
10893
  /**
@@ -10847,7 +11000,7 @@ var _VaultClient = class __VaultClient {
10847
11000
  unresolvedBaseUrl,
10848
11001
  options.logger ?? console
10849
11002
  );
10850
- const timeoutMs = options.timeout ?? 3e4;
11003
+ const timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;
10851
11004
  this.#caller = options.caller;
10852
11005
  const rawCallerType = options.callerType ?? "agent";
10853
11006
  if (rawCallerType !== "agent" && rawCallerType !== "service") {
@@ -11428,8 +11581,13 @@ ${label}:${value}`;
11428
11581
  };
11429
11582
  throw new ScopeReauthRequiredError(
11430
11583
  _messageOr(errorData, "Grant is missing required scopes"),
11431
- details.grant_id ?? void 0,
11432
- details.provider_id ?? void 0,
11584
+ // `?? undefined` alone only replaces null — a NON-string grant_id /
11585
+ // provider_id (number, object, bool) would be cast `as string` and
11586
+ // flow through as the wrong runtime value. typeof-guard instead, to
11587
+ // match every sibling branch in this method AND the Python SDK's
11588
+ // `_handle_forbidden` scope_mismatch branch (isinstance → None).
11589
+ typeof details.grant_id === "string" ? details.grant_id : void 0,
11590
+ typeof details.provider_id === "string" ? details.provider_id : void 0,
11433
11591
  response.status,
11434
11592
  // No provider response body on this path — the backend refused at
11435
11593
  // /sdk/token before any provider call, so responseBody is left unset
@@ -11471,6 +11629,15 @@ ${label}:${value}`;
11471
11629
  }
11472
11630
  if (response.status === HTTP_CONFLICT) {
11473
11631
  const errorData = await __VaultClient.#safeParseJson(response);
11632
+ if (errorData.error === "managed_oauth_scope_approval_required") {
11633
+ throw new ConnectConfigError(
11634
+ _messageOr(
11635
+ errorData,
11636
+ "The requested permissions require managed OAuth approval."
11637
+ ),
11638
+ errorData
11639
+ );
11640
+ }
11474
11641
  if (errorData.error === "token_refresh_in_progress") {
11475
11642
  throw new TokenRefreshInProgressError(
11476
11643
  _messageOr(
@@ -12038,9 +12205,34 @@ ${label}:${value}`;
12038
12205
  });
12039
12206
  this.#cacheActorIdFromResponse(response);
12040
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
+ }
12041
12228
  this.#logger.warn(
12042
- `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)})`
12043
12230
  );
12231
+ } else {
12232
+ try {
12233
+ await response.arrayBuffer();
12234
+ } catch {
12235
+ }
12044
12236
  }
12045
12237
  } catch (error51) {
12046
12238
  this.#logger.warn(
@@ -12551,7 +12743,7 @@ ${label}:${value}`;
12551
12743
  headers: hmacHeaders,
12552
12744
  // Cap the best-effort background report at 2s (parity with the Python
12553
12745
  // SDK's asyncio.wait_for). Without an override it would inherit the
12554
- // instance timeout (default 30s), so a hung backend could keep the
12746
+ // instance timeout (default 95s), so a hung backend could keep the
12555
12747
  // fire-and-forget task — and close()'s drain of it — alive far longer
12556
12748
  // than the Python SDK does. Next provider 403 re-reports (self-healing).
12557
12749
  timeoutMs: 2e3
@@ -12928,8 +13120,11 @@ ${label}:${value}`;
12928
13120
  * @param options - Optional revocation options (reason for audit log).
12929
13121
  * @returns RevokeGrantResult confirming the revocation.
12930
13122
  * @throws {GrantNotFoundError} If the grant does not exist, is not
12931
- * active, or (when a user token is configured) does not belong to
12932
- * 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.
12933
13128
  * @throws {NetworkError} If connection to backend fails.
12934
13129
  * @throws {TimeoutError} If request to backend times out.
12935
13130
  */
@@ -13350,6 +13545,16 @@ ${label}:${value}`;
13350
13545
  * entry. Thrown before any wire traffic (parity with the Python SDK).
13351
13546
  * @throws AlterSDKError if the SDK instance is closed
13352
13547
  * @throws NetworkError if the connection to the backend fails
13548
+ * @throws ConnectConfigError if the session's requested scopes exceed what
13549
+ * the Alter-managed OAuth client is approved for (HTTP 409
13550
+ * `managed_oauth_scope_approval_required`). NOT a `BackendError` — a
13551
+ * caller that branches only on `BackendError` leaves this uncaught.
13552
+ * `details` carries the whole 409 body: `details.details.provider_id` and
13553
+ * `details.details.unapproved_scopes` name what needs approving,
13554
+ * `details.remediation.action` / `.description` carry the typed remedy,
13555
+ * and `details.retryable` is `false`. Retrying is futile: contact Alter to
13556
+ * approve the scopes, or narrow the app's configured scopes to the
13557
+ * approved set, and only then create a new Connect session.
13353
13558
  * @throws BackendError if the backend rejects the request (e.g.
13354
13559
  * `scope_not_allowed` when a requested scope is not in the app's Dev
13355
13560
  * Portal allowlist)
@@ -13682,6 +13887,10 @@ ${label}:${value}`;
13682
13887
  * authorized grant is revoked because the user's usage limits could not be
13683
13888
  * applied. In the last case, `details.failed_grants` contains the typed
13684
13889
  * failure data in snake_case wire form.
13890
+ * @throws BackendError if identity synchronization could not be completed —
13891
+ * `statusCode` 503 (transient, with `details.retry_after_seconds`) or 409
13892
+ * (an operator must repair the provider; no retry hint). Not a
13893
+ * `ConnectFlowError`: the flow itself was fine.
13685
13894
  * @throws AlterSDKError if SDK is closed or session creation fails
13686
13895
  */
13687
13896
  async connect(options) {
@@ -13726,12 +13935,17 @@ ${label}:${value}`;
13726
13935
  * @returns One `ConnectResult` per provider the user completed
13727
13936
  * within the session (multi-provider Connect sessions yield
13728
13937
  * multiple results).
13729
- * @throws AlterValueError if `sessionToken` is blank, or `timeoutMs` /
13730
- * `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.
13731
13941
  * @throws ConnectTimeoutError if the local `timeoutMs` elapses, or a
13732
13942
  * session observed as pending expires before Alter receives a
13733
13943
  * completion callback. `details.reason` distinguishes
13734
- * `poll_deadline_elapsed` from `session_expired`.
13944
+ * `poll_deadline_elapsed`, `session_expired`, and `rate_limited` (the
13945
+ * whole budget was spent backing off a throttled poll —
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.
13735
13949
  * @throws ConnectFlowError / ConnectDeniedError / ConnectConfigError
13736
13950
  * for user denial, a first poll that finds an unavailable/expired
13737
13951
  * session, unrecognized status, or a completed session whose every grant
@@ -13739,6 +13953,11 @@ ${label}:${value}`;
13739
13953
  * total-failure error carries
13740
13954
  * `details.failed_grants`; partial failures are surfaced on every
13741
13955
  * returned result as `failedGrants`.
13956
+ * @throws BackendError if identity synchronization could not be completed.
13957
+ * `statusCode` says whether to come back: 503 is transient contention
13958
+ * and `details.retry_after_seconds` carries the backoff hint, while 409
13959
+ * is an operator-repairable provider fault (disabled provider, duplicate
13960
+ * group name) that no retry will clear and which carries no hint.
13742
13961
  * @throws AlterSDKError if the SDK instance has been closed.
13743
13962
  */
13744
13963
  async pollConnectSession(sessionToken, options) {
@@ -13746,11 +13965,48 @@ ${label}:${value}`;
13746
13965
  const timeoutMs = options?.timeoutMs ?? 3e5;
13747
13966
  const pollIntervalMs = options?.pollIntervalMs ?? 2e3;
13748
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
+ }
13749
13971
  const deadline = performance.now() + timeoutMs;
13750
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
+ };
13751
13980
  while (true) {
13752
13981
  this.#assertNotClosed();
13753
- const pollResult = await this.#pollSession(sessionToken);
13982
+ let pollResult;
13983
+ try {
13984
+ pollResult = await this.#pollSession(sessionToken);
13985
+ } catch (e) {
13986
+ if (!(e instanceof RateLimitError)) {
13987
+ throw e;
13988
+ }
13989
+ const remaining2 = deadline - performance.now();
13990
+ if (remaining2 <= 0) {
13991
+ throw new ConnectTimeoutError(
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.`,
13993
+ withDeclined({
13994
+ timeoutMs,
13995
+ reason: "rate_limited",
13996
+ retryAfter: e.retryAfter
13997
+ })
13998
+ );
13999
+ }
14000
+ let backoffMs = pollIntervalMs;
14001
+ if (typeof e.retryAfter === "number" && e.retryAfter > 0) {
14002
+ backoffMs = Math.max(e.retryAfter * 1e3, pollIntervalMs);
14003
+ }
14004
+ await new Promise(
14005
+ (resolve5) => setTimeout(resolve5, Math.min(backoffMs, remaining2))
14006
+ );
14007
+ this.#assertNotClosed();
14008
+ continue;
14009
+ }
13754
14010
  const pollStatus = pollResult.status;
13755
14011
  if (pollStatus === "completed") {
13756
14012
  const rawGrants = pollResult.grants;
@@ -13848,7 +14104,15 @@ ${label}:${value}`;
13848
14104
  }
13849
14105
  );
13850
14106
  }
13851
- const errorDetails = { error_code: errorCode };
14107
+ const errorDetails = {
14108
+ error_code: errorCode
14109
+ };
14110
+ const retryAfter = _coerceInt(
14111
+ err2.retry_after_seconds
14112
+ );
14113
+ if (retryAfter !== void 0) {
14114
+ errorDetails.retry_after_seconds = retryAfter;
14115
+ }
13852
14116
  if (errorCode === "connect_denied") {
13853
14117
  throw new ConnectDeniedError(errorMessage, errorDetails);
13854
14118
  }
@@ -13858,17 +14122,29 @@ ${label}:${value}`;
13858
14122
  "invalid_client",
13859
14123
  "unauthorized_client",
13860
14124
  "provider_configuration_unavailable",
13861
- "shared_dev_credential_unavailable"
14125
+ "shared_dev_credential_unavailable",
14126
+ "managed_oauth_scope_approval_required"
13862
14127
  ].includes(errorCode)) {
13863
14128
  throw new ConnectConfigError(errorMessage, errorDetails);
13864
14129
  }
14130
+ const identitySyncStatus = Object.hasOwn(
14131
+ IDENTITY_SYNC_POLL_STATUS,
14132
+ errorCode
14133
+ ) ? IDENTITY_SYNC_POLL_STATUS[errorCode] : void 0;
14134
+ if (identitySyncStatus !== void 0) {
14135
+ throw new BackendError(
14136
+ errorMessage,
14137
+ errorDetails,
14138
+ identitySyncStatus
14139
+ );
14140
+ }
13865
14141
  throw new ConnectFlowError(errorMessage, errorDetails);
13866
14142
  }
13867
14143
  if (pollStatus === "expired") {
13868
14144
  if (sawPending) {
13869
14145
  throw new ConnectTimeoutError(
13870
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.",
13871
- { timeoutMs, reason: "session_expired" }
14147
+ withDeclined({ timeoutMs, reason: "session_expired" })
13872
14148
  );
13873
14149
  }
13874
14150
  throw new ConnectFlowError(
@@ -13881,6 +14157,46 @@ ${label}:${value}`;
13881
14157
  { status: pollStatus }
13882
14158
  );
13883
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
+ }
13884
14200
  sawPending = true;
13885
14201
  const remaining = deadline - performance.now();
13886
14202
  if (remaining <= 0) break;
@@ -13890,7 +14206,7 @@ ${label}:${value}`;
13890
14206
  }
13891
14207
  throw new ConnectTimeoutError(
13892
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.`,
13893
- { timeoutMs, reason: "poll_deadline_elapsed" }
14209
+ withDeclined({ timeoutMs, reason: "poll_deadline_elapsed" })
13894
14210
  );
13895
14211
  }
13896
14212
  /**
@@ -13955,6 +14271,14 @@ ${label}:${value}`;
13955
14271
  * shape (popup, mobile redirect, headless). The convenience
13956
14272
  * method doesn't infer these from the error.
13957
14273
  * @throws AlterValueError if `error.providerId` is `undefined`.
14274
+ * @throws ConnectConfigError if the recovery session could not be minted
14275
+ * because the Alter-managed OAuth client is not approved for the scopes
14276
+ * this app requests (HTTP 409 `managed_oauth_scope_approval_required`).
14277
+ * This helper forwards to `createConnectSession`, so it throws
14278
+ * everything that method throws — see its `@throws` list for the
14279
+ * `details` payload. Retrying is futile: contact Alter to approve the
14280
+ * scopes, or narrow the app's configured scopes to the approved set, and
14281
+ * only then mint a recovery session.
13958
14282
  */
13959
14283
  async createConnectSessionForError(error51, options) {
13960
14284
  const providerId = error51.providerId;
@@ -14096,6 +14420,11 @@ ${label}:${value}`;
14096
14420
  * `pollIntervalMs` is non-finite or not greater than zero.
14097
14421
  * @throws AlterSDKError if the SDK is closed or the IDP returned a terminal
14098
14422
  * error / the session expired.
14423
+ * @throws BackendError if identity synchronization could not be completed.
14424
+ * Surfaced separately from the terminal `AlterSDKError` above precisely
14425
+ * because it is NOT a rejected login: `statusCode` 503 is transient
14426
+ * contention carrying `details.retry_after_seconds`, and 409 is an
14427
+ * operator-repairable provider fault that no retry will clear.
14099
14428
  * @throws ConnectTimeoutError if the user did not complete login in time
14100
14429
  * (`details.reason` is `"poll_deadline_elapsed"`).
14101
14430
  */
@@ -14127,8 +14456,22 @@ ${label}:${value}`;
14127
14456
  });
14128
14457
  }
14129
14458
  if (pollData.status === "error") {
14130
- const errorMessage = pollData.error_message || "unknown error";
14131
- throw new AlterSDKError(`Authentication failed: ${errorMessage}`);
14459
+ const errorMessage = typeof pollData.error_message === "string" && pollData.error_message ? pollData.error_message : "unknown error";
14460
+ const message = `Authentication failed: ${errorMessage}`;
14461
+ const syncCode = typeof pollData.error_code === "string" ? pollData.error_code : "";
14462
+ const identitySyncStatus = Object.hasOwn(
14463
+ IDENTITY_SYNC_POLL_STATUS,
14464
+ syncCode
14465
+ ) ? IDENTITY_SYNC_POLL_STATUS[syncCode] : void 0;
14466
+ if (identitySyncStatus !== void 0) {
14467
+ const details = { error_code: syncCode };
14468
+ const retryAfter = _coerceInt(pollData.retry_after_seconds);
14469
+ if (retryAfter !== void 0) {
14470
+ details.retry_after_seconds = retryAfter;
14471
+ }
14472
+ throw new BackendError(message, details, identitySyncStatus);
14473
+ }
14474
+ throw new AlterSDKError(message);
14132
14475
  }
14133
14476
  if (pollData.status === "expired") {
14134
14477
  throw new AlterSDKError("Authentication session expired");
@@ -14145,10 +14488,13 @@ ${label}:${value}`;
14145
14488
  (resolve5) => setTimeout(resolve5, Math.min(pollIntervalMs, remaining))
14146
14489
  );
14147
14490
  }
14148
- throw new ConnectTimeoutError("Authentication timed out", {
14149
- timeoutMs,
14150
- reason: "poll_deadline_elapsed"
14151
- });
14491
+ throw new ConnectTimeoutError(
14492
+ `Authentication timed out after ${timeoutMs}ms. The user may not have completed login in the browser.`,
14493
+ {
14494
+ timeoutMs,
14495
+ reason: "poll_deadline_elapsed"
14496
+ }
14497
+ );
14152
14498
  }
14153
14499
  /**
14154
14500
  * Single sign-in poll attempt (INTERNAL).
@@ -14712,12 +15058,13 @@ ${label}:${value}`;
14712
15058
  async #mapProxyErrorResponse(response) {
14713
15059
  let detail = {};
14714
15060
  try {
14715
- const errBody = await response.clone().json();
14716
- detail = errBody.detail || errBody || {};
15061
+ const parsed = await response.clone().json();
15062
+ const payloadDict = isPlainRecord(parsed) ? parsed : {};
15063
+ detail = isPlainRecord(payloadDict.detail) ? payloadDict.detail : payloadDict;
14717
15064
  } catch {
14718
15065
  }
14719
15066
  const errCode = detail.error || "";
14720
- const msg = detail.message || `/sdk/proxy returned ${response.status}`;
15067
+ const msg = typeof detail.message === "string" && detail.message ? detail.message : `/sdk/proxy returned ${response.status}`;
14721
15068
  if (errCode === "approval_denied")
14722
15069
  throw new ApprovalDeniedError({ message: msg });
14723
15070
  if (errCode === "approval_expired")
@@ -14740,10 +15087,34 @@ ${label}:${value}`;
14740
15087
  error: errCode
14741
15088
  });
14742
15089
  }
14743
- /** Single-shot poll of an approval. */
14744
- 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 = {}) {
14745
15108
  this.#assertNotClosed();
14746
- 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}`;
14747
15118
  const hmacHeaders = this.#computeHmacHeaders("GET", sdkPath, "");
14748
15119
  const traceparent = await ambientTraceparent();
14749
15120
  if (traceparent !== void 0) {
@@ -14752,7 +15123,18 @@ ${label}:${value}`;
14752
15123
  let response;
14753
15124
  try {
14754
15125
  response = await this.#alterClient.request("GET", sdkPath, {
14755
- 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
14756
15138
  });
14757
15139
  } catch (error51) {
14758
15140
  if (error51 instanceof Error && error51.name === "AbortError") {
@@ -14801,11 +15183,21 @@ ${label}:${value}`;
14801
15183
  * @throws ApprovalExpiredError on `expired`.
14802
15184
  * @throws ApprovalExecutionFailedError on `failed`.
14803
15185
  * @throws ApprovalTimeoutError if the local wait elapses before any decision.
15186
+ * @throws AlterValueError if `timeoutMs` is not a finite number, or
15187
+ * `pollIntervalMs` is non-finite or not greater than zero.
14804
15188
  */
14805
15189
  async awaitApproval(approvalId, options = {}) {
14806
15190
  this.#assertNotClosed();
14807
15191
  const timeoutMs = options.timeoutMs ?? 3e5;
14808
15192
  const pollIntervalMs = options.pollIntervalMs ?? 2e3;
15193
+ if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
15194
+ throw new AlterValueError("timeoutMs must be a finite number");
15195
+ }
15196
+ if (typeof pollIntervalMs !== "number" || !Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) {
15197
+ throw new AlterValueError(
15198
+ "pollIntervalMs must be a finite number greater than 0"
15199
+ );
15200
+ }
14809
15201
  const deadline = Date.now() + timeoutMs;
14810
15202
  let lastTransient = null;
14811
15203
  while (true) {
@@ -14813,9 +15205,39 @@ ${label}:${value}`;
14813
15205
  throw this.#buildApprovalTimeout(approvalId, timeoutMs, lastTransient);
14814
15206
  }
14815
15207
  let status;
15208
+ const raceController = new AbortController();
15209
+ let deadlineTimer;
14816
15210
  try {
14817
- 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
+ ]);
14818
15232
  } catch (err2) {
15233
+ if (err2 instanceof _ApprovalDeadlineElapsed) {
15234
+ raceController.abort();
15235
+ throw this.#buildApprovalTimeout(
15236
+ approvalId,
15237
+ timeoutMs,
15238
+ lastTransient
15239
+ );
15240
+ }
14819
15241
  if (!__VaultClient.#isTransientPollError(err2)) {
14820
15242
  throw err2;
14821
15243
  }
@@ -14831,16 +15253,18 @@ ${label}:${value}`;
14831
15253
  lastTransient
14832
15254
  );
14833
15255
  }
14834
- const sleep2 = Math.max(
14835
- 100,
14836
- Math.min(pollIntervalMs, deadline - Date.now())
15256
+ const sleep2 = Math.min(
15257
+ pollIntervalMs,
15258
+ Math.max(0, deadline - Date.now())
14837
15259
  );
14838
15260
  await new Promise((res) => setTimeout(res, sleep2));
14839
15261
  continue;
15262
+ } finally {
15263
+ if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
14840
15264
  }
14841
15265
  lastTransient = null;
14842
15266
  if (status.status === "executed" && !status.hasResult) {
14843
- const next = Math.max(100, deadline - Date.now());
15267
+ const next = Math.max(0, deadline - Date.now());
14844
15268
  await new Promise((r) => setTimeout(r, Math.min(pollIntervalMs, next)));
14845
15269
  continue;
14846
15270
  }
@@ -14867,9 +15291,9 @@ ${label}:${value}`;
14867
15291
  lastTransient
14868
15292
  );
14869
15293
  }
14870
- const sleep2 = Math.max(
14871
- 100,
14872
- Math.min(pollIntervalMs, deadline - Date.now())
15294
+ const sleep2 = Math.min(
15295
+ pollIntervalMs,
15296
+ Math.max(0, deadline - Date.now())
14873
15297
  );
14874
15298
  await new Promise((res) => setTimeout(res, sleep2));
14875
15299
  continue;
@@ -14879,7 +15303,7 @@ ${label}:${value}`;
14879
15303
  if (now >= deadline) {
14880
15304
  throw this.#buildApprovalTimeout(approvalId, timeoutMs, lastTransient);
14881
15305
  }
14882
- const sleep = Math.max(100, Math.min(pollIntervalMs, deadline - now));
15306
+ const sleep = Math.min(pollIntervalMs, Math.max(0, deadline - now));
14883
15307
  await new Promise((res) => setTimeout(res, sleep));
14884
15308
  }
14885
15309
  }
@@ -15652,8 +16076,8 @@ var Agent = class _Agent {
15652
16076
  return this.#client.delegate(grantId, toAgentId, options);
15653
16077
  }
15654
16078
  // ── Approvals ──────────────────────────────────────────────────────────
15655
- async getApprovalStatus(approvalId) {
15656
- return this.#client.getApprovalStatus(approvalId);
16079
+ async getApprovalStatus(approvalId, options = {}) {
16080
+ return this.#client.getApprovalStatus(approvalId, options);
15657
16081
  }
15658
16082
  async awaitApproval(...args) {
15659
16083
  return this.#client.awaitApproval(...args);
@@ -15841,11 +16265,18 @@ var App = class _App {
15841
16265
  * @param options.label - Required sibling address (resolution key:
15842
16266
  * provider + label). Must be unique among the credential's active
15843
16267
  * sibling grants.
15844
- * @param options.grantPolicy - Optional per-grant policy
15845
- * (snake_case wire keys e.g. `expires_at`, `max_ttl_seconds`,
15846
- * `restrictions`, `requires_approval`). Validated server-side; an
15847
- * invalid policy throws `BackendError` from the 422
15848
- * `invalid_grant_policy` response.
16268
+ * @param options.grantPolicy - Optional per-grant policy. `expiresAt`
16269
+ * and `maxTtlSeconds` are accepted in this SDK's camelCase
16270
+ * (validated locally and mapped to the snake_case wire keys,
16271
+ * exactly like `createConnectSession`); the wider policy grammar
16272
+ * (`restrictions`, `requires_approval`, …) uses the backend's
16273
+ * snake_case keys and is validated server-side — an invalid policy
16274
+ * throws `BackendError` from the 422 `invalid_grant_policy`
16275
+ * response. A default TTL is not part of the mint grammar:
16276
+ * `defaultTtlSeconds` / `default_ttl_seconds` throws
16277
+ * `AlterValueError` (it is accepted only by Connect sessions and
16278
+ * `createManagedSecretGrant`). Supplying both spellings of one
16279
+ * trio field also throws `AlterValueError`.
15849
16280
  * @param options.grantTags - Optional list of tag strings stored on
15850
16281
  * the sibling grant.
15851
16282
  * @returns {@link GrantInfo} for the newly minted sibling grant
@@ -15881,7 +16312,7 @@ var App = class _App {
15881
16312
  }
15882
16313
  const wireBody = { label: options.label };
15883
16314
  if (options.grantPolicy !== void 0) {
15884
- wireBody.grant_policy = options.grantPolicy;
16315
+ wireBody.grant_policy = _mintGrantPolicyToWire(options.grantPolicy);
15885
16316
  }
15886
16317
  if (options.grantTags !== void 0) {
15887
16318
  wireBody.grant_tags = options.grantTags;
@@ -16066,8 +16497,8 @@ var App = class _App {
16066
16497
  });
16067
16498
  }
16068
16499
  // ── Approvals ──────────────────────────────────────────────────────────
16069
- async getApprovalStatus(approvalId) {
16070
- return this.#client.getApprovalStatus(approvalId);
16500
+ async getApprovalStatus(approvalId, options = {}) {
16501
+ return this.#client.getApprovalStatus(approvalId, options);
16071
16502
  }
16072
16503
  async awaitApproval(...args) {
16073
16504
  return this.#client.awaitApproval(...args);
@@ -16323,8 +16754,8 @@ async function _listGrantsUnified(client, body) {
16323
16754
  var DEFAULT_BASE_URL = "https://backend.alterauth.com";
16324
16755
  var PAT_API_PREFIX = "/api/v1/dev-portal";
16325
16756
  var HTTP_ERROR_THRESHOLD = 400;
16326
- var DEFAULT_TIMEOUT_MS = 3e4;
16327
- var CLI_VERSION = "0.8.0";
16757
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
16758
+ var CLI_VERSION = "0.9.1";
16328
16759
  var USER_AGENT = buildUserAgent();
16329
16760
  function buildUserAgent() {
16330
16761
  let osTag = "";
@@ -16569,7 +17000,7 @@ var DashboardClient = class {
16569
17000
  );
16570
17001
  }
16571
17002
  }
16572
- this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
17003
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
16573
17004
  this.pats = new PATsNamespace(this);
16574
17005
  this.apps = new AppsNamespace(this);
16575
17006
  this.keys = new KeysNamespace2(this);
@@ -17330,7 +17761,7 @@ function isProviderCatalogEntry(value) {
17330
17761
  const v = value;
17331
17762
  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(
17332
17763
  (environment) => environment === "production" || environment === "sandbox"
17333
- )) && v.available_scopes !== null && typeof v.available_scopes === "object" && !Array.isArray(v.available_scopes) && Object.values(v.available_scopes).every(
17764
+ )) && (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(
17334
17765
  isProviderScopeCatalogEntry
17335
17766
  ) && (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(
17336
17767
  (scope) => typeof scope === "string"
@@ -18485,6 +18916,25 @@ var AuditNamespace = class {
18485
18916
  });
18486
18917
  return expectDict(body, "audit.list", 200);
18487
18918
  }
18919
+ /** Provider facet values present in one app's audit events. Requires
18920
+ * `dashboard_audit:read`.
18921
+ *
18922
+ * Returns ``{providers}`` — the alphabetized distinct provider ids
18923
+ * (OAuth provider ids, managed-secret template ids — a custom secret's
18924
+ * name-derived slug — and IDP types) actually
18925
+ * present on the app's audit rows; the accepted vocabulary for the
18926
+ * ``--provider`` filters. Data-derived, never a hardcoded shortlist.
18927
+ */
18928
+ async providers(appId) {
18929
+ const query = optionsToQuery({ app_id: appId });
18930
+ const body = await this.#client._call(
18931
+ "GET",
18932
+ "/audit-logs/unified/providers",
18933
+ "audit.providers",
18934
+ { query }
18935
+ );
18936
+ return expectDict(body, "audit.providers", 200);
18937
+ }
18488
18938
  /** List dashboard / CLI admin actions. Requires `dashboard_audit:read`.
18489
18939
  *
18490
18940
  * Returns the canonical envelope ``{items, total, limit, offset, has_more}``
@@ -19281,6 +19731,38 @@ function resolveAppId(flagValue, options = {}) {
19281
19731
  return null;
19282
19732
  }
19283
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
+
19284
19766
  // src/commands/_helpers.ts
19285
19767
  async function withClient(fn) {
19286
19768
  const client = await createPortalClient();
@@ -19833,9 +20315,9 @@ function parseAllowedHostsOrExit(flag, entries) {
19833
20315
  fail(`${JSON.stringify(entry)} must not contain control characters`);
19834
20316
  }
19835
20317
  }
19836
- if (/[/:@\s]/.test(e)) {
20318
+ if (/[/:@\s?#%\\]/.test(e)) {
19837
20319
  fail(
19838
- `${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 ? # % \\`
19839
20321
  );
19840
20322
  }
19841
20323
  let normalized;
@@ -19846,12 +20328,23 @@ function parseAllowedHostsOrExit(flag, entries) {
19846
20328
  `${JSON.stringify(entry)} is a malformed wildcard \u2014 use "*.domain.tld"`
19847
20329
  );
19848
20330
  }
19849
- 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(".")) {
19850
20338
  fail(
19851
20339
  `${JSON.stringify(entry)}: wildcard must cover a domain, not a bare TLD (use "*.example.com", not "*.com")`
19852
20340
  );
19853
20341
  }
19854
- 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}`;
19855
20348
  } else if (e.includes("*")) {
19856
20349
  fail(
19857
20350
  `${JSON.stringify(entry)}: "*" is only allowed as a leading subdomain wildcard ("*.domain.tld")`
@@ -19860,9 +20353,16 @@ function parseAllowedHostsOrExit(flag, entries) {
19860
20353
  normalized = e.replace(/\.+$/, "");
19861
20354
  if (!normalized) {
19862
20355
  fail(
19863
- `${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 ? # % \\`
19864
20357
  );
19865
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)`
20363
+ );
20364
+ }
20365
+ normalized = canonical;
19866
20366
  }
19867
20367
  if (seen.has(normalized)) continue;
19868
20368
  seen.add(normalized);
@@ -20118,6 +20618,20 @@ function surfaceCatalogWarnings(row) {
20118
20618
  );
20119
20619
  }
20120
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
+ }
20121
20635
  function writeNextPageHint(page, noun) {
20122
20636
  if (typeof page !== "object" || page === null) return;
20123
20637
  const { has_more: hasMore, offset, limit } = page;
@@ -20312,6 +20826,7 @@ function buildAgentsCommand() {
20312
20826
  "alter: the api_key field above is shown ONCE and cannot be retrieved later.\n"
20313
20827
  );
20314
20828
  surfaceApproverWarnings(result);
20829
+ surfaceScopeWarnings(result);
20315
20830
  });
20316
20831
  return;
20317
20832
  }
@@ -20367,6 +20882,7 @@ function buildAgentsCommand() {
20367
20882
  "alter: the api_key field above is shown ONCE and cannot be retrieved later.\n"
20368
20883
  );
20369
20884
  surfaceApproverWarnings(result);
20885
+ surfaceScopeWarnings(result);
20370
20886
  });
20371
20887
  }
20372
20888
  );
@@ -20586,6 +21102,7 @@ function buildAgentsCommand() {
20586
21102
  }
20587
21103
  );
20588
21104
  emit2(format, row);
21105
+ surfaceScopeWarnings(row);
20589
21106
  } catch (error51) {
20590
21107
  if (error51 instanceof PortalBackendError && error51.statusCode === 409 && error51.code === "agent_concurrent_update") {
20591
21108
  throw new PortalBackendError(
@@ -35607,6 +36124,9 @@ var CONTENT_MATCH_LIMITS = {
35607
36124
  };
35608
36125
  var MAX_RULE_NAME_LEN = 120;
35609
36126
  var MAX_RULE_DESCRIPTION_LEN = 2e3;
36127
+ var MAX_RULES_PER_TARGET = 100;
36128
+ var MAX_DERIVED_RULES_PER_TARGET = 2;
36129
+ var MAX_POLICY_RULE_PAGE_SIZE = MAX_RULES_PER_TARGET + MAX_DERIVED_RULES_PER_TARGET;
35610
36130
  var HITL_NOTIFICATION_CHANNELS = ["email"];
35611
36131
  var HITL_LIMITS = {
35612
36132
  maxApprovers: 10,
@@ -35651,7 +36171,7 @@ var RULE_TYPE_LABELS = /* @__PURE__ */ new Map([
35651
36171
  ["json_match", "Request condition"],
35652
36172
  ["ip_allowlist", "IP allowlist"],
35653
36173
  ["time_window", "Time window"],
35654
- ["require_approval", "Require approval"],
36174
+ ["require_approval", "Human in the loop (HITL)"],
35655
36175
  ["restriction", "Method and endpoint allowlist"],
35656
36176
  ["quota", "Request quota"],
35657
36177
  ["content_match", "Operation and parameter policy"]
@@ -36077,10 +36597,13 @@ function buildPolicyRulePresentation(rule) {
36077
36597
  });
36078
36598
  }
36079
36599
  if (rule.display?.inherited) {
36080
- displayItems.push({ label: "Relationship", value: "Inherited policy" });
36600
+ displayItems.push({
36601
+ label: "Relationship",
36602
+ value: "Inherited runtime policy"
36603
+ });
36081
36604
  }
36082
36605
  if (displayItems.length > 0) {
36083
- groups.unshift({ label: "Policy scope", items: displayItems });
36606
+ groups.unshift({ label: "Runtime policy scope", items: displayItems });
36084
36607
  }
36085
36608
  return {
36086
36609
  typeLabel: policyRuleTypeLabel(rule.rule_type),
@@ -36093,7 +36616,10 @@ function formatPolicyRuleAsText(rule) {
36093
36616
  const lines = [
36094
36617
  ...rule.name ? [`Name: ${rule.name}`] : [],
36095
36618
  ...rule.id ? [`Policy ID: ${rule.id}`] : [],
36096
- `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}]`,
36097
36623
  `Effect: ${presentation.effectLabel}`,
36098
36624
  `Status: ${rule.enabled === false ? "Disabled" : "Enabled"}`,
36099
36625
  presentation.summary,
@@ -36608,7 +37134,7 @@ function buildAppsCommand() {
36608
37134
  "table"
36609
37135
  ).option(
36610
37136
  "--no-include-stats",
36611
- "Skip per-app statistics (grant / key / API-call counts)"
37137
+ "Skip per-app statistics (grant / connection / secret / provider / key / API-call counts)"
36612
37138
  ).option(
36613
37139
  "--include-archived",
36614
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)."
@@ -37302,6 +37828,33 @@ function buildAuditCommand() {
37302
37828
  });
37303
37829
  }
37304
37830
  );
37831
+ audit.command("providers").description(
37832
+ "List the provider IDs present in an app's audit events \u2014 the values `--provider` accepts (OAuth provider ids, managed-secret template ids \u2014 a custom secret's name-derived slug \u2014 and IDP types). Derived from the audit data, so providers whose config was since deleted still appear."
37833
+ ).option(
37834
+ APP_FLAG,
37835
+ "App whose audit trail to facet (ID or name; falls back to ALTER_APP_ID / the workspace pin)"
37836
+ ).option(
37837
+ "--output <format>",
37838
+ "Output format: json|jsonl|table (default: table)",
37839
+ "table"
37840
+ ).action(async (options) => {
37841
+ const format = coerceOutputFormat(options.output);
37842
+ const appId = await resolveAppOrExit(options.app);
37843
+ await withClient(async (client) => {
37844
+ const result = await client.audit.providers(appId);
37845
+ const providers = result["providers"];
37846
+ if (!Array.isArray(providers) || !providers.every((p) => typeof p === "string")) {
37847
+ err(
37848
+ 'audit.providers: response is missing the "providers" string array. This is a wire-format regression; report it to the backend team.'
37849
+ );
37850
+ process.exit(EXIT_ERROR);
37851
+ }
37852
+ const rows = providers.map((provider_id) => ({ provider_id }));
37853
+ emit2(format, rows, [
37854
+ { label: "PROVIDER", get: (r) => r.provider_id }
37855
+ ]);
37856
+ });
37857
+ });
37305
37858
  audit.command("show <trace-id>").description("Show every event for one trace").option(
37306
37859
  "--output <format>",
37307
37860
  "Output format: json|jsonl|table (default: json)",
@@ -40782,6 +41335,7 @@ async function runStatus() {
40782
41335
  // src/commands/managed-secrets.ts
40783
41336
  import { readFileSync as readFileSync9 } from "fs";
40784
41337
  import { Command as Command16, Option } from "commander";
41338
+ var MAX_SEARCH_LENGTH = 255;
40785
41339
  var PRINCIPAL_TYPES = ["user", "group", "system", "agent"];
40786
41340
  var CREDENTIAL_TYPES = [
40787
41341
  "bearer_token",
@@ -41186,6 +41740,13 @@ function surfaceManagedSecretResponse(row) {
41186
41740
  }
41187
41741
  surfaceApproverWarnings(row);
41188
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
+ }
41189
41750
  function groupPrincipalGateMessage(e) {
41190
41751
  if (!(e instanceof PortalBackendError) || e.statusCode !== 422 || e.code !== "group_principal_unsupported_idp") {
41191
41752
  return null;
@@ -41522,7 +42083,11 @@ var TEMPLATE_COLUMNS = [
41522
42083
  get: (t) => t.form_schema?.fields.map(
41523
42084
  (field) => `${credentialFieldFlag(field)}${field.required ? "" : " (optional)"}`
41524
42085
  ).join(" ") || "\u2014"
41525
- }
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" }
41526
42091
  ];
41527
42092
  var SECRET_COLUMNS = [
41528
42093
  { label: "ID", get: (s) => s.id, maxWidth: 36 },
@@ -41991,7 +42556,7 @@ function buildGroupsSubcommand() {
41991
42556
  );
41992
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(
41993
42558
  "--search <substring>",
41994
- "Substring search on group name / external_group_id"
42559
+ "Substring search on group name / external_group_id (max 255 chars)"
41995
42560
  ).option(
41996
42561
  "--limit <n>",
41997
42562
  "Page size (default: 50)",
@@ -42008,6 +42573,12 @@ function buildGroupsSubcommand() {
42008
42573
  async (options) => {
42009
42574
  const format = coerceOutputFormat(options.output);
42010
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
+ );
42011
42582
  const resolvedAppId = await resolveAppOrExit(options.app);
42012
42583
  await withClient(async (client) => {
42013
42584
  const response = await client.managedSecrets.listGroups(
@@ -42120,7 +42691,8 @@ function buildManagedSecretsCommand() {
42120
42691
  credential_type: isString,
42121
42692
  category: isOptionalString,
42122
42693
  popular: (v) => typeof v === "boolean",
42123
- form_schema: isTemplateFormSchema
42694
+ form_schema: isTemplateFormSchema,
42695
+ allowed_hosts_from_field: isAbsentOrOptionalString
42124
42696
  },
42125
42697
  "managed-secrets.templates"
42126
42698
  );
@@ -42504,11 +43076,25 @@ function buildManagedSecretsCommand() {
42504
43076
  }
42505
43077
  const hostList = options.clear ? null : parseAllowedHostsOrExit("--host", options.host);
42506
43078
  await withClient(async (client) => {
42507
- const row = await client.managedSecrets.setAllowedHosts(
42508
- appId,
42509
- secretId,
42510
- hostList
42511
- );
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
+ }
42512
43098
  emit2(format, row);
42513
43099
  if (options.clear) {
42514
43100
  process.stderr.write(
@@ -42794,7 +43380,7 @@ function buildManagedSecretsCommand() {
42794
43380
  );
42795
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(
42796
43382
  "--search <substring>",
42797
- "Substring search on email / display_name / external_subject_id"
43383
+ "Substring search on email / display_name / external_subject_id (max 255 chars)"
42798
43384
  ).option(
42799
43385
  "--limit <n>",
42800
43386
  "Page size (default: 50)",
@@ -42811,6 +43397,12 @@ function buildManagedSecretsCommand() {
42811
43397
  async (options) => {
42812
43398
  const format = coerceOutputFormat(options.output);
42813
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
+ );
42814
43406
  const resolvedAppId = await resolveAppOrExit(options.app);
42815
43407
  await withClient(async (client) => {
42816
43408
  const response = await client.managedSecrets.listUsers(
@@ -44326,8 +44918,8 @@ function buildRulesSubcommand() {
44326
44918
  )
44327
44919
  ).option(
44328
44920
  "--limit <n>",
44329
- "Page size (1-100; when omitted the backend default of 100 applies)",
44330
- parseBoundedInt("--limit", 1, 100)
44921
+ `Page size (1-${MAX_POLICY_RULE_PAGE_SIZE}; when omitted the backend default of ${MAX_POLICY_RULE_PAGE_SIZE} applies)`,
44922
+ parseBoundedInt("--limit", 1, MAX_POLICY_RULE_PAGE_SIZE)
44331
44923
  ).option(
44332
44924
  "--offset <n>",
44333
44925
  "Page offset (default 0)",
@@ -44761,7 +45353,8 @@ async function providerCatalogError(client, providerId, environment, credentialS
44761
45353
  const availableScopes = provider.available_scopes ?? {};
44762
45354
  if (Object.keys(availableScopes).length === 0) {
44763
45355
  if (scopes !== void 0 && scopes.length > 0) {
44764
- return `alter: provider '${providerId}' publishes no selectable scopes \u2014 omit --scopes (Alter does not send a scope parameter to it)
45356
+ const where = provider.scopes_configured_on_provider === true ? `; its permissions are configured on the ${provider.display_name} app itself` : "";
45357
+ return `alter: provider '${providerId}' publishes no selectable scopes \u2014 omit --scopes (Alter does not send a scope parameter to it${where})
44765
45358
  `;
44766
45359
  }
44767
45360
  return null;