@alter-ai/cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +366 -81
  2. package/package.json +2 -2
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.0",
37
37
  description: "Command-line interface for the Alter Vault dev portal \u2014 scripted dashboard automation.",
38
38
  type: "module",
39
39
  bin: {
@@ -76,7 +76,7 @@ var package_default = {
76
76
  keytar: "^7.9.0"
77
77
  },
78
78
  devDependencies: {
79
- "@alter-ai/alter-sdk": "workspace:0.23.3",
79
+ "@alter-ai/alter-sdk": "workspace:0.24.0",
80
80
  "@alter-vault/shared-types": "workspace:0.0.1",
81
81
  "@alter-vault/shared-utils": "workspace:0.0.1",
82
82
  "@eslint/js": "9.39.4",
@@ -4618,7 +4618,7 @@ var AlterSDKError = class extends Error {
4618
4618
  constructor(message, details) {
4619
4619
  super(message);
4620
4620
  this.name = "AlterSDKError";
4621
- this.details = details ?? {};
4621
+ this.details = details !== null && typeof details === "object" && !Array.isArray(details) ? details : {};
4622
4622
  Object.setPrototypeOf(this, new.target.prototype);
4623
4623
  }
4624
4624
  toString() {
@@ -4836,9 +4836,9 @@ var InsufficientScopeError = class _InsufficientScopeError extends BackendError
4836
4836
  constructor(message = "Insufficient scope", options = {}) {
4837
4837
  super(message, options.details);
4838
4838
  this.name = "InsufficientScopeError";
4839
- this.required = options.required ?? [];
4840
- this.granted = options.granted ?? [];
4841
- this.missing = options.missing ?? [];
4839
+ this.required = options.required ? [...options.required] : [];
4840
+ this.granted = options.granted ? [...options.granted] : [];
4841
+ this.missing = options.missing ? [...options.missing] : [];
4842
4842
  this.scopeVersion = options.scopeVersion ?? null;
4843
4843
  this.currentScopeVersion = options.currentScopeVersion ?? null;
4844
4844
  this.scopeVersionMismatch = options.scopeVersionMismatch ?? false;
@@ -4871,7 +4871,9 @@ var InsufficientScopeError = class _InsufficientScopeError extends BackendError
4871
4871
  */
4872
4872
  static fromErrorBody(errorData) {
4873
4873
  return new _InsufficientScopeError(
4874
- typeof errorData.message === "string" ? errorData.message : "Insufficient scope",
4874
+ // Non-empty string only (parity with `_messageOr` / Python
4875
+ // `from_error_body`): an empty wire message is as uninformative as none.
4876
+ typeof errorData.message === "string" && errorData.message ? errorData.message : "Insufficient scope",
4875
4877
  {
4876
4878
  // Element-level filtering: a non-array coerces to [], and non-string
4877
4879
  // elements inside an array are dropped (a bare cast would smuggle
@@ -5056,7 +5058,7 @@ var AgentError = class extends BackendError {
5056
5058
  hint;
5057
5059
  constructor(opts = {}) {
5058
5060
  const subclassCode = new.target.code;
5059
- super(opts.message ?? subclassCode, opts.details);
5061
+ super(opts.message || subclassCode, opts.details);
5060
5062
  this.name = "AgentError";
5061
5063
  this.code = subclassCode;
5062
5064
  this.hint = opts.hint;
@@ -6796,12 +6798,13 @@ var ApprovalResult = class {
6796
6798
  headers: this.headers,
6797
6799
  body_b64: this.bodyB64,
6798
6800
  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.
6801
+ // Include duration_ms so JSON round-trips carry the timing metadata.
6802
+ // The Python twin's ApprovalResult is a plain Pydantic model whose
6803
+ // model_dump() includes EVERY public field duration_ms and
6804
+ // credential_hint alike so omitting either here would make a TS
6805
+ // JSON round-trip lossy where the Python one is not (wire-shape
6806
+ // parity: toJSON() must serialize the same field set as model_dump()).
6807
+ duration_ms: this.durationMs,
6805
6808
  credential_hint: this.credentialHint
6806
6809
  };
6807
6810
  }
@@ -8112,7 +8115,7 @@ async function _raiseForStatus(response) {
8112
8115
  const rawCode = body.error;
8113
8116
  const code = typeof rawCode === "string" ? rawCode : "";
8114
8117
  const rawMessage = body.message;
8115
- const message = typeof rawMessage === "string" ? rawMessage : `Backend error ${response.status}`;
8118
+ const message = typeof rawMessage === "string" && rawMessage ? rawMessage : `Backend error ${response.status}`;
8116
8119
  const rawHint = body.hint;
8117
8120
  const hint = typeof rawHint === "string" ? rawHint : void 0;
8118
8121
  if (code === "insufficient_scope") {
@@ -8120,7 +8123,7 @@ async function _raiseForStatus(response) {
8120
8123
  }
8121
8124
  if (code === "rate_limit_exceeded") {
8122
8125
  throw new RateLimitError(
8123
- typeof body.message === "string" ? body.message : "Rate limit exceeded \u2014 retry after the window resets.",
8126
+ typeof body.message === "string" && body.message ? body.message : "Rate limit exceeded \u2014 retry after the window resets.",
8124
8127
  _parseRetryAfter(response.headers.get("Retry-After"), body.retry_after),
8125
8128
  body,
8126
8129
  typeof body.scope === "string" ? body.scope : void 0,
@@ -8318,9 +8321,15 @@ var AgentsNamespace = class {
8318
8321
  const limit = options.limit ?? 100;
8319
8322
  const offset = options.offset ?? 0;
8320
8323
  const includeRevoked = options.includeRevoked ?? false;
8324
+ if (!Number.isInteger(limit)) {
8325
+ throw new AlterValueError("limit must be an integer");
8326
+ }
8321
8327
  if (limit < 1 || limit > LIST_LIMIT_MAX) {
8322
8328
  throw new AlterValueError("limit must be between 1 and 1000");
8323
8329
  }
8330
+ if (!Number.isInteger(offset)) {
8331
+ throw new AlterValueError("offset must be an integer");
8332
+ }
8324
8333
  if (offset < 0) {
8325
8334
  throw new AlterValueError("offset must be >= 0");
8326
8335
  }
@@ -8766,7 +8775,7 @@ function _optionalStringArray(raw, key, context) {
8766
8775
  }
8767
8776
  function _normalizeKeyId(value, operation) {
8768
8777
  if (typeof value === "string") {
8769
- if (value.length === 0) {
8778
+ if (value.trim().length === 0) {
8770
8779
  throw new AlterValueError(`${operation}() requires a non-empty keyId`);
8771
8780
  }
8772
8781
  return value;
@@ -9468,6 +9477,26 @@ var SpansNamespace = class {
9468
9477
  `spans[${index}] must be a UserSpan with traceId, name, and startTime`
9469
9478
  );
9470
9479
  }
9480
+ if (span.traceId.length < 1 || span.traceId.length > 255) {
9481
+ throw new AlterValueError(
9482
+ `spans[${index}].traceId must be 1\u2013255 characters`
9483
+ );
9484
+ }
9485
+ if (span.name.length < 1 || span.name.length > 255) {
9486
+ throw new AlterValueError(
9487
+ `spans[${index}].name must be 1\u2013255 characters`
9488
+ );
9489
+ }
9490
+ if (span.spanId !== void 0 && (typeof span.spanId !== "string" || span.spanId.length < 1 || span.spanId.length > 32)) {
9491
+ throw new AlterValueError(
9492
+ `spans[${index}].spanId must be 1\u201332 characters when provided`
9493
+ );
9494
+ }
9495
+ if (span.parentSpanId !== void 0 && (typeof span.parentSpanId !== "string" || span.parentSpanId.length < 1 || span.parentSpanId.length > 32)) {
9496
+ throw new AlterValueError(
9497
+ `spans[${index}].parentSpanId must be 1\u201332 characters when provided`
9498
+ );
9499
+ }
9471
9500
  });
9472
9501
  const body = {
9473
9502
  spans: spans.map((span) => {
@@ -9794,8 +9823,14 @@ var FORBIDDEN_INJECTION_HEADERS2 = /* @__PURE__ */ new Set([
9794
9823
  function _coerceInt(value) {
9795
9824
  return Number.isInteger(value) && value >= 0 ? value : void 0;
9796
9825
  }
9826
+ var IDENTITY_SYNC_POLL_STATUS = {
9827
+ service_unavailable: 503,
9828
+ identity_provider_inactive: 409,
9829
+ identity_group_name_ambiguous: 409
9830
+ };
9797
9831
  function _messageOr(errorData, fallback) {
9798
- return typeof errorData.message === "string" ? errorData.message : fallback;
9832
+ const raw = errorData.message;
9833
+ return typeof raw === "string" && raw ? raw : fallback;
9799
9834
  }
9800
9835
  function _errorPayload(errorData) {
9801
9836
  const nested = errorData.details;
@@ -9918,7 +9953,8 @@ function validateTokenInjectionMetadata(tokenData) {
9918
9953
  let additionalCredentials = null;
9919
9954
  if (additionalCredentialsRaw != null) {
9920
9955
  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
9956
+ ([key, value]) => key.length <= MAX_ADDITIONAL_CREDENTIAL_KEY_LENGTH && typeof value === "string" && // pythonStrip: match the backend's Python str.strip() (see token_type).
9957
+ pythonStrip(value).length > 0 && value.length <= MAX_ADDITIONAL_CREDENTIAL_VALUE_LENGTH && // eslint-disable-next-line no-control-regex
9922
9958
  !/[\r\n\x00]/.test(value)
9923
9959
  )) {
9924
9960
  throw new BackendError("Backend returned invalid additional_credentials");
@@ -10016,7 +10052,23 @@ function isValidOffsetDateTime(value) {
10016
10052
  const hour = Number(hourText);
10017
10053
  const minute = Number(minuteText);
10018
10054
  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) {
10055
+ const isLeapYear = year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
10056
+ const daysInMonth = [
10057
+ 31,
10058
+ isLeapYear ? 29 : 28,
10059
+ 31,
10060
+ 30,
10061
+ 31,
10062
+ 30,
10063
+ 31,
10064
+ 31,
10065
+ 30,
10066
+ 31,
10067
+ 30,
10068
+ 31
10069
+ ];
10070
+ const maxDay = daysInMonth[month - 1] ?? 0;
10071
+ if (year < 1 || month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59) {
10020
10072
  return false;
10021
10073
  }
10022
10074
  return !Number.isNaN(Date.parse(value));
@@ -10036,7 +10088,10 @@ function validateTokenResponseContract(tokenData) {
10036
10088
  throw new BackendError("Backend returned an invalid access_token");
10037
10089
  }
10038
10090
  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
10091
+ if (typeof tokenType !== "string" || // pythonStrip (not .trim()): the backend normalizes with Python str.strip(),
10092
+ // whose whitespace set differs from JS trim() at the edges (NEL/BOM/C0
10093
+ // separators). Match the backend so the two SDKs accept/reject identically.
10094
+ pythonStrip(tokenType).length === 0 || tokenType !== pythonStrip(tokenType) || tokenType.length > 255 || // eslint-disable-next-line no-control-regex
10040
10095
  /[\x00-\x1f\x7f]/.test(tokenType)) {
10041
10096
  throw new BackendError("Backend returned an invalid token_type");
10042
10097
  }
@@ -10050,7 +10105,8 @@ function validateTokenResponseContract(tokenData) {
10050
10105
  }
10051
10106
  const scopes = tokenData.scopes === void 0 ? [] : tokenData.scopes;
10052
10107
  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
10108
+ (scope) => typeof scope !== "string" || // pythonStrip: match the backend's Python str.strip() (see token_type).
10109
+ pythonStrip(scope).length === 0 || scope !== pythonStrip(scope) || // eslint-disable-next-line no-control-regex
10054
10110
  /[\x00-\x1f\x7f]/.test(scope)
10055
10111
  )) {
10056
10112
  throw new BackendError("Backend returned invalid scopes");
@@ -10060,7 +10116,8 @@ function validateTokenResponseContract(tokenData) {
10060
10116
  throw new BackendError("Backend returned an invalid grant_id");
10061
10117
  }
10062
10118
  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
10119
+ if (typeof providerId !== "string" || // pythonStrip: match the backend's Python str.strip() (see token_type).
10120
+ providerId !== pythonStrip(providerId) || providerId.length > 255 || // eslint-disable-next-line no-control-regex
10064
10121
  /[\x00-\x1f\x7f]/.test(providerId)) {
10065
10122
  throw new BackendError("Backend returned an invalid provider_id");
10066
10123
  }
@@ -10089,7 +10146,10 @@ function isRetryErrorInfoPayload(x) {
10089
10146
  return false;
10090
10147
  }
10091
10148
  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");
10149
+ return typeof e.attempt === "number" && // Integer-only (not merely finite) mirrors the backend Pydantic `int`
10150
+ // schema and the Python SDK's strict `RetryErrorInfo.attempt`, so a
10151
+ // fractional attempt (e.g. 1.5) is dropped identically in both SDKs.
10152
+ 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
10153
  }
10094
10154
  var _tokenStore = /* @__PURE__ */ new WeakMap();
10095
10155
  var _additionalCredsStore = /* @__PURE__ */ new WeakMap();
@@ -10112,7 +10172,7 @@ function _extractAdditionalCredentials(token) {
10112
10172
  return _additionalCredsStore.get(token);
10113
10173
  }
10114
10174
  var _fetch;
10115
- var SDK_VERSION = "0.23.3";
10175
+ var SDK_VERSION = "0.24.0";
10116
10176
  var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
10117
10177
  function pyUnquote(s) {
10118
10178
  if (!s.includes("%")) return s;
@@ -10381,7 +10441,14 @@ var PROXY_FORBIDDEN_HEADER_NAMES = /* @__PURE__ */ new Set([
10381
10441
  "x-goog-authorization",
10382
10442
  "x-goog-iam-authorization-token",
10383
10443
  "x-stripe-account",
10384
- "x-shopify-access-token"
10444
+ "x-shopify-access-token",
10445
+ // Method tunnelling: OData V2 (SAP S/4HANA) and several enterprise APIs
10446
+ // honour these as an override of the real HTTP verb. The policy gate
10447
+ // classifies on the wire method, so a tunnelled verb would split the
10448
+ // operation the gate names from the one the provider executes.
10449
+ "x-http-method",
10450
+ "x-http-method-override",
10451
+ "x-method-override"
10385
10452
  ]);
10386
10453
  var MAX_CONTEXT_LENGTH = 4096;
10387
10454
  var MAX_CONTEXT_KEYS = 20;
@@ -10418,9 +10485,10 @@ function validateAndSerializeContext(context) {
10418
10485
  `context keys must be strings, got ${typeof k}`
10419
10486
  );
10420
10487
  }
10421
- if (k.length > MAX_CONTEXT_KEY_LENGTH) {
10488
+ const keyLength = [...k].length;
10489
+ if (keyLength > MAX_CONTEXT_KEY_LENGTH) {
10422
10490
  throw new AlterValueError(
10423
- `context key "${k.slice(0, 32)}" exceeds max length (${k.length} > ${MAX_CONTEXT_KEY_LENGTH})`
10491
+ `context key "${k.slice(0, 32)}" exceeds max length (${keyLength} > ${MAX_CONTEXT_KEY_LENGTH})`
10424
10492
  );
10425
10493
  }
10426
10494
  const v = context[k];
@@ -10429,9 +10497,10 @@ function validateAndSerializeContext(context) {
10429
10497
  `context value for key "${k}" must be a string, got ${typeof v}`
10430
10498
  );
10431
10499
  }
10432
- if (v.length > MAX_CONTEXT_VALUE_LENGTH) {
10500
+ const valueLength = [...v].length;
10501
+ if (valueLength > MAX_CONTEXT_VALUE_LENGTH) {
10433
10502
  throw new AlterValueError(
10434
- `context value for key "${k}" exceeds max length (${v.length} > ${MAX_CONTEXT_VALUE_LENGTH})`
10503
+ `context value for key "${k}" exceeds max length (${valueLength} > ${MAX_CONTEXT_VALUE_LENGTH})`
10435
10504
  );
10436
10505
  }
10437
10506
  }
@@ -10531,11 +10600,28 @@ function connectMetadataToWire(metadata) {
10531
10600
  }
10532
10601
  return wire;
10533
10602
  }
10534
- var GRANT_POLICY_INPUT_KEYS = /* @__PURE__ */ new Set([
10535
- "expiresAt",
10536
- "maxTtlSeconds",
10537
- "defaultTtlSeconds"
10538
- ]);
10603
+ var GRANT_POLICY_CAMEL_TO_WIRE = [
10604
+ ["expiresAt", "expires_at"],
10605
+ ["maxTtlSeconds", "max_ttl_seconds"],
10606
+ ["defaultTtlSeconds", "default_ttl_seconds"]
10607
+ ];
10608
+ var GRANT_POLICY_INPUT_KEYS = new Set(
10609
+ GRANT_POLICY_CAMEL_TO_WIRE.map(([camelKey]) => camelKey)
10610
+ );
10611
+ function assertParseableExpiresAt(value) {
10612
+ if (typeof value !== "string" || value.length === 0 || Number.isNaN(Date.parse(value))) {
10613
+ throw new AlterValueError(
10614
+ "grantPolicy.expiresAt must be a non-empty ISO 8601 string when provided"
10615
+ );
10616
+ }
10617
+ }
10618
+ function assertPositiveIntegerSeconds(camelKey, value) {
10619
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
10620
+ throw new AlterValueError(
10621
+ `grantPolicy.${camelKey} must be a positive integer (seconds) when provided`
10622
+ );
10623
+ }
10624
+ }
10539
10625
  function grantPolicyInputToWire(policy) {
10540
10626
  if (policy === null || typeof policy !== "object" || Array.isArray(policy)) {
10541
10627
  throw new AlterValueError(
@@ -10545,32 +10631,57 @@ function grantPolicyInputToWire(policy) {
10545
10631
  for (const key of Object.keys(policy)) {
10546
10632
  if (!GRANT_POLICY_INPUT_KEYS.has(key)) {
10547
10633
  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.`
10634
+ `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
10635
  );
10550
10636
  }
10551
10637
  }
10552
10638
  const wire = {};
10553
- if (policy.expiresAt !== void 0 && policy.expiresAt !== null) {
10554
- if (typeof policy.expiresAt !== "string" || policy.expiresAt.length === 0) {
10639
+ for (const [camelKey, wireKey] of GRANT_POLICY_CAMEL_TO_WIRE) {
10640
+ const value = policy[camelKey];
10641
+ if (value === void 0 || value === null) {
10642
+ continue;
10643
+ }
10644
+ if (camelKey === "expiresAt") {
10645
+ assertParseableExpiresAt(value);
10646
+ } else {
10647
+ assertPositiveIntegerSeconds(camelKey, value);
10648
+ }
10649
+ wire[wireKey] = value;
10650
+ }
10651
+ return wire;
10652
+ }
10653
+ function _mintGrantPolicyToWire(policy) {
10654
+ for (const spelling of ["defaultTtlSeconds", "default_ttl_seconds"]) {
10655
+ if (Object.prototype.hasOwnProperty.call(policy, spelling)) {
10555
10656
  throw new AlterValueError(
10556
- "grantPolicy.expiresAt must be a non-empty ISO 8601 string when provided"
10657
+ `grantPolicy.${spelling} is not supported on mint \u2014 a default TTL is accepted only by Connect sessions (createConnectSession / connect) and createManagedSecretGrant`
10557
10658
  );
10558
10659
  }
10559
- wire.expires_at = policy.expiresAt;
10560
10660
  }
10561
- for (const [camelKey, wireKey] of [
10562
- ["maxTtlSeconds", "max_ttl_seconds"],
10563
- ["defaultTtlSeconds", "default_ttl_seconds"]
10564
- ]) {
10661
+ const wire = {};
10662
+ for (const [key, value] of Object.entries(policy)) {
10663
+ if (!GRANT_POLICY_INPUT_KEYS.has(key)) {
10664
+ wire[key] = value;
10665
+ }
10666
+ }
10667
+ for (const [camelKey, wireKey] of GRANT_POLICY_CAMEL_TO_WIRE) {
10565
10668
  const value = policy[camelKey];
10566
- if (value === void 0 || value === null) {
10669
+ if (value === void 0) {
10567
10670
  continue;
10568
10671
  }
10569
- if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
10672
+ if (Object.prototype.hasOwnProperty.call(policy, wireKey)) {
10570
10673
  throw new AlterValueError(
10571
- `grantPolicy.${camelKey} must be a positive integer (seconds) when provided`
10674
+ `grantPolicy sets both "${camelKey}" and "${wireKey}" \u2014 supply exactly one spelling of the field`
10572
10675
  );
10573
10676
  }
10677
+ if (value === null) {
10678
+ continue;
10679
+ }
10680
+ if (camelKey === "expiresAt") {
10681
+ assertParseableExpiresAt(value);
10682
+ } else {
10683
+ assertPositiveIntegerSeconds(camelKey, value);
10684
+ }
10574
10685
  wire[wireKey] = value;
10575
10686
  }
10576
10687
  return wire;
@@ -11428,8 +11539,13 @@ ${label}:${value}`;
11428
11539
  };
11429
11540
  throw new ScopeReauthRequiredError(
11430
11541
  _messageOr(errorData, "Grant is missing required scopes"),
11431
- details.grant_id ?? void 0,
11432
- details.provider_id ?? void 0,
11542
+ // `?? undefined` alone only replaces null — a NON-string grant_id /
11543
+ // provider_id (number, object, bool) would be cast `as string` and
11544
+ // flow through as the wrong runtime value. typeof-guard instead, to
11545
+ // match every sibling branch in this method AND the Python SDK's
11546
+ // `_handle_forbidden` scope_mismatch branch (isinstance → None).
11547
+ typeof details.grant_id === "string" ? details.grant_id : void 0,
11548
+ typeof details.provider_id === "string" ? details.provider_id : void 0,
11433
11549
  response.status,
11434
11550
  // No provider response body on this path — the backend refused at
11435
11551
  // /sdk/token before any provider call, so responseBody is left unset
@@ -11471,6 +11587,15 @@ ${label}:${value}`;
11471
11587
  }
11472
11588
  if (response.status === HTTP_CONFLICT) {
11473
11589
  const errorData = await __VaultClient.#safeParseJson(response);
11590
+ if (errorData.error === "managed_oauth_scope_approval_required") {
11591
+ throw new ConnectConfigError(
11592
+ _messageOr(
11593
+ errorData,
11594
+ "The requested permissions require managed OAuth approval."
11595
+ ),
11596
+ errorData
11597
+ );
11598
+ }
11474
11599
  if (errorData.error === "token_refresh_in_progress") {
11475
11600
  throw new TokenRefreshInProgressError(
11476
11601
  _messageOr(
@@ -13350,6 +13475,16 @@ ${label}:${value}`;
13350
13475
  * entry. Thrown before any wire traffic (parity with the Python SDK).
13351
13476
  * @throws AlterSDKError if the SDK instance is closed
13352
13477
  * @throws NetworkError if the connection to the backend fails
13478
+ * @throws ConnectConfigError if the session's requested scopes exceed what
13479
+ * the Alter-managed OAuth client is approved for (HTTP 409
13480
+ * `managed_oauth_scope_approval_required`). NOT a `BackendError` — a
13481
+ * caller that branches only on `BackendError` leaves this uncaught.
13482
+ * `details` carries the whole 409 body: `details.details.provider_id` and
13483
+ * `details.details.unapproved_scopes` name what needs approving,
13484
+ * `details.remediation.action` / `.description` carry the typed remedy,
13485
+ * and `details.retryable` is `false`. Retrying is futile: contact Alter to
13486
+ * approve the scopes, or narrow the app's configured scopes to the
13487
+ * approved set, and only then create a new Connect session.
13353
13488
  * @throws BackendError if the backend rejects the request (e.g.
13354
13489
  * `scope_not_allowed` when a requested scope is not in the app's Dev
13355
13490
  * Portal allowlist)
@@ -13682,6 +13817,10 @@ ${label}:${value}`;
13682
13817
  * authorized grant is revoked because the user's usage limits could not be
13683
13818
  * applied. In the last case, `details.failed_grants` contains the typed
13684
13819
  * failure data in snake_case wire form.
13820
+ * @throws BackendError if identity synchronization could not be completed —
13821
+ * `statusCode` 503 (transient, with `details.retry_after_seconds`) or 409
13822
+ * (an operator must repair the provider; no retry hint). Not a
13823
+ * `ConnectFlowError`: the flow itself was fine.
13685
13824
  * @throws AlterSDKError if SDK is closed or session creation fails
13686
13825
  */
13687
13826
  async connect(options) {
@@ -13731,7 +13870,9 @@ ${label}:${value}`;
13731
13870
  * @throws ConnectTimeoutError if the local `timeoutMs` elapses, or a
13732
13871
  * session observed as pending expires before Alter receives a
13733
13872
  * completion callback. `details.reason` distinguishes
13734
- * `poll_deadline_elapsed` from `session_expired`.
13873
+ * `poll_deadline_elapsed`, `session_expired`, and `rate_limited` (the
13874
+ * whole budget was spent backing off a throttled poll —
13875
+ * `details.retryAfter` carries the last hint the server gave).
13735
13876
  * @throws ConnectFlowError / ConnectDeniedError / ConnectConfigError
13736
13877
  * for user denial, a first poll that finds an unavailable/expired
13737
13878
  * session, unrecognized status, or a completed session whose every grant
@@ -13739,6 +13880,11 @@ ${label}:${value}`;
13739
13880
  * total-failure error carries
13740
13881
  * `details.failed_grants`; partial failures are surfaced on every
13741
13882
  * returned result as `failedGrants`.
13883
+ * @throws BackendError if identity synchronization could not be completed.
13884
+ * `statusCode` says whether to come back: 503 is transient contention
13885
+ * and `details.retry_after_seconds` carries the backoff hint, while 409
13886
+ * is an operator-repairable provider fault (disabled provider, duplicate
13887
+ * group name) that no retry will clear and which carries no hint.
13742
13888
  * @throws AlterSDKError if the SDK instance has been closed.
13743
13889
  */
13744
13890
  async pollConnectSession(sessionToken, options) {
@@ -13750,7 +13896,34 @@ ${label}:${value}`;
13750
13896
  let sawPending = false;
13751
13897
  while (true) {
13752
13898
  this.#assertNotClosed();
13753
- const pollResult = await this.#pollSession(sessionToken);
13899
+ let pollResult;
13900
+ try {
13901
+ pollResult = await this.#pollSession(sessionToken);
13902
+ } catch (e) {
13903
+ if (!(e instanceof RateLimitError)) {
13904
+ throw e;
13905
+ }
13906
+ const remaining2 = deadline - performance.now();
13907
+ if (remaining2 <= 0) {
13908
+ throw new ConnectTimeoutError(
13909
+ `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
+ {
13911
+ timeoutMs,
13912
+ reason: "rate_limited",
13913
+ retryAfter: e.retryAfter
13914
+ }
13915
+ );
13916
+ }
13917
+ let backoffMs = pollIntervalMs;
13918
+ if (typeof e.retryAfter === "number" && e.retryAfter > 0) {
13919
+ backoffMs = Math.max(e.retryAfter * 1e3, pollIntervalMs);
13920
+ }
13921
+ await new Promise(
13922
+ (resolve5) => setTimeout(resolve5, Math.min(backoffMs, remaining2))
13923
+ );
13924
+ this.#assertNotClosed();
13925
+ continue;
13926
+ }
13754
13927
  const pollStatus = pollResult.status;
13755
13928
  if (pollStatus === "completed") {
13756
13929
  const rawGrants = pollResult.grants;
@@ -13848,7 +14021,15 @@ ${label}:${value}`;
13848
14021
  }
13849
14022
  );
13850
14023
  }
13851
- const errorDetails = { error_code: errorCode };
14024
+ const errorDetails = {
14025
+ error_code: errorCode
14026
+ };
14027
+ const retryAfter = _coerceInt(
14028
+ err2.retry_after_seconds
14029
+ );
14030
+ if (retryAfter !== void 0) {
14031
+ errorDetails.retry_after_seconds = retryAfter;
14032
+ }
13852
14033
  if (errorCode === "connect_denied") {
13853
14034
  throw new ConnectDeniedError(errorMessage, errorDetails);
13854
14035
  }
@@ -13858,10 +14039,19 @@ ${label}:${value}`;
13858
14039
  "invalid_client",
13859
14040
  "unauthorized_client",
13860
14041
  "provider_configuration_unavailable",
13861
- "shared_dev_credential_unavailable"
14042
+ "shared_dev_credential_unavailable",
14043
+ "managed_oauth_scope_approval_required"
13862
14044
  ].includes(errorCode)) {
13863
14045
  throw new ConnectConfigError(errorMessage, errorDetails);
13864
14046
  }
14047
+ const identitySyncStatus = IDENTITY_SYNC_POLL_STATUS[errorCode];
14048
+ if (identitySyncStatus !== void 0) {
14049
+ throw new BackendError(
14050
+ errorMessage,
14051
+ errorDetails,
14052
+ identitySyncStatus
14053
+ );
14054
+ }
13865
14055
  throw new ConnectFlowError(errorMessage, errorDetails);
13866
14056
  }
13867
14057
  if (pollStatus === "expired") {
@@ -13955,6 +14145,14 @@ ${label}:${value}`;
13955
14145
  * shape (popup, mobile redirect, headless). The convenience
13956
14146
  * method doesn't infer these from the error.
13957
14147
  * @throws AlterValueError if `error.providerId` is `undefined`.
14148
+ * @throws ConnectConfigError if the recovery session could not be minted
14149
+ * because the Alter-managed OAuth client is not approved for the scopes
14150
+ * this app requests (HTTP 409 `managed_oauth_scope_approval_required`).
14151
+ * This helper forwards to `createConnectSession`, so it throws
14152
+ * everything that method throws — see its `@throws` list for the
14153
+ * `details` payload. Retrying is futile: contact Alter to approve the
14154
+ * scopes, or narrow the app's configured scopes to the approved set, and
14155
+ * only then mint a recovery session.
13958
14156
  */
13959
14157
  async createConnectSessionForError(error51, options) {
13960
14158
  const providerId = error51.providerId;
@@ -14096,6 +14294,11 @@ ${label}:${value}`;
14096
14294
  * `pollIntervalMs` is non-finite or not greater than zero.
14097
14295
  * @throws AlterSDKError if the SDK is closed or the IDP returned a terminal
14098
14296
  * error / the session expired.
14297
+ * @throws BackendError if identity synchronization could not be completed.
14298
+ * Surfaced separately from the terminal `AlterSDKError` above precisely
14299
+ * because it is NOT a rejected login: `statusCode` 503 is transient
14300
+ * contention carrying `details.retry_after_seconds`, and 409 is an
14301
+ * operator-repairable provider fault that no retry will clear.
14099
14302
  * @throws ConnectTimeoutError if the user did not complete login in time
14100
14303
  * (`details.reason` is `"poll_deadline_elapsed"`).
14101
14304
  */
@@ -14127,8 +14330,19 @@ ${label}:${value}`;
14127
14330
  });
14128
14331
  }
14129
14332
  if (pollData.status === "error") {
14130
- const errorMessage = pollData.error_message || "unknown error";
14131
- throw new AlterSDKError(`Authentication failed: ${errorMessage}`);
14333
+ const errorMessage = typeof pollData.error_message === "string" && pollData.error_message ? pollData.error_message : "unknown error";
14334
+ const message = `Authentication failed: ${errorMessage}`;
14335
+ const syncCode = typeof pollData.error_code === "string" ? pollData.error_code : "";
14336
+ const identitySyncStatus = IDENTITY_SYNC_POLL_STATUS[syncCode];
14337
+ if (identitySyncStatus !== void 0) {
14338
+ const details = { error_code: syncCode };
14339
+ const retryAfter = _coerceInt(pollData.retry_after_seconds);
14340
+ if (retryAfter !== void 0) {
14341
+ details.retry_after_seconds = retryAfter;
14342
+ }
14343
+ throw new BackendError(message, details, identitySyncStatus);
14344
+ }
14345
+ throw new AlterSDKError(message);
14132
14346
  }
14133
14347
  if (pollData.status === "expired") {
14134
14348
  throw new AlterSDKError("Authentication session expired");
@@ -14145,10 +14359,13 @@ ${label}:${value}`;
14145
14359
  (resolve5) => setTimeout(resolve5, Math.min(pollIntervalMs, remaining))
14146
14360
  );
14147
14361
  }
14148
- throw new ConnectTimeoutError("Authentication timed out", {
14149
- timeoutMs,
14150
- reason: "poll_deadline_elapsed"
14151
- });
14362
+ throw new ConnectTimeoutError(
14363
+ `Authentication timed out after ${timeoutMs}ms. The user may not have completed login in the browser.`,
14364
+ {
14365
+ timeoutMs,
14366
+ reason: "poll_deadline_elapsed"
14367
+ }
14368
+ );
14152
14369
  }
14153
14370
  /**
14154
14371
  * Single sign-in poll attempt (INTERNAL).
@@ -14712,12 +14929,13 @@ ${label}:${value}`;
14712
14929
  async #mapProxyErrorResponse(response) {
14713
14930
  let detail = {};
14714
14931
  try {
14715
- const errBody = await response.clone().json();
14716
- detail = errBody.detail || errBody || {};
14932
+ const parsed = await response.clone().json();
14933
+ const payloadDict = isPlainRecord(parsed) ? parsed : {};
14934
+ detail = isPlainRecord(payloadDict.detail) ? payloadDict.detail : payloadDict;
14717
14935
  } catch {
14718
14936
  }
14719
14937
  const errCode = detail.error || "";
14720
- const msg = detail.message || `/sdk/proxy returned ${response.status}`;
14938
+ const msg = typeof detail.message === "string" && detail.message ? detail.message : `/sdk/proxy returned ${response.status}`;
14721
14939
  if (errCode === "approval_denied")
14722
14940
  throw new ApprovalDeniedError({ message: msg });
14723
14941
  if (errCode === "approval_expired")
@@ -14801,11 +15019,21 @@ ${label}:${value}`;
14801
15019
  * @throws ApprovalExpiredError on `expired`.
14802
15020
  * @throws ApprovalExecutionFailedError on `failed`.
14803
15021
  * @throws ApprovalTimeoutError if the local wait elapses before any decision.
15022
+ * @throws AlterValueError if `timeoutMs` is not a finite number, or
15023
+ * `pollIntervalMs` is non-finite or not greater than zero.
14804
15024
  */
14805
15025
  async awaitApproval(approvalId, options = {}) {
14806
15026
  this.#assertNotClosed();
14807
15027
  const timeoutMs = options.timeoutMs ?? 3e5;
14808
15028
  const pollIntervalMs = options.pollIntervalMs ?? 2e3;
15029
+ if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
15030
+ throw new AlterValueError("timeoutMs must be a finite number");
15031
+ }
15032
+ if (typeof pollIntervalMs !== "number" || !Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) {
15033
+ throw new AlterValueError(
15034
+ "pollIntervalMs must be a finite number greater than 0"
15035
+ );
15036
+ }
14809
15037
  const deadline = Date.now() + timeoutMs;
14810
15038
  let lastTransient = null;
14811
15039
  while (true) {
@@ -14831,16 +15059,16 @@ ${label}:${value}`;
14831
15059
  lastTransient
14832
15060
  );
14833
15061
  }
14834
- const sleep2 = Math.max(
14835
- 100,
14836
- Math.min(pollIntervalMs, deadline - Date.now())
15062
+ const sleep2 = Math.min(
15063
+ pollIntervalMs,
15064
+ Math.max(0, deadline - Date.now())
14837
15065
  );
14838
15066
  await new Promise((res) => setTimeout(res, sleep2));
14839
15067
  continue;
14840
15068
  }
14841
15069
  lastTransient = null;
14842
15070
  if (status.status === "executed" && !status.hasResult) {
14843
- const next = Math.max(100, deadline - Date.now());
15071
+ const next = Math.max(0, deadline - Date.now());
14844
15072
  await new Promise((r) => setTimeout(r, Math.min(pollIntervalMs, next)));
14845
15073
  continue;
14846
15074
  }
@@ -14867,9 +15095,9 @@ ${label}:${value}`;
14867
15095
  lastTransient
14868
15096
  );
14869
15097
  }
14870
- const sleep2 = Math.max(
14871
- 100,
14872
- Math.min(pollIntervalMs, deadline - Date.now())
15098
+ const sleep2 = Math.min(
15099
+ pollIntervalMs,
15100
+ Math.max(0, deadline - Date.now())
14873
15101
  );
14874
15102
  await new Promise((res) => setTimeout(res, sleep2));
14875
15103
  continue;
@@ -14879,7 +15107,7 @@ ${label}:${value}`;
14879
15107
  if (now >= deadline) {
14880
15108
  throw this.#buildApprovalTimeout(approvalId, timeoutMs, lastTransient);
14881
15109
  }
14882
- const sleep = Math.max(100, Math.min(pollIntervalMs, deadline - now));
15110
+ const sleep = Math.min(pollIntervalMs, Math.max(0, deadline - now));
14883
15111
  await new Promise((res) => setTimeout(res, sleep));
14884
15112
  }
14885
15113
  }
@@ -15841,11 +16069,18 @@ var App = class _App {
15841
16069
  * @param options.label - Required sibling address (resolution key:
15842
16070
  * provider + label). Must be unique among the credential's active
15843
16071
  * 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.
16072
+ * @param options.grantPolicy - Optional per-grant policy. `expiresAt`
16073
+ * and `maxTtlSeconds` are accepted in this SDK's camelCase
16074
+ * (validated locally and mapped to the snake_case wire keys,
16075
+ * exactly like `createConnectSession`); the wider policy grammar
16076
+ * (`restrictions`, `requires_approval`, …) uses the backend's
16077
+ * snake_case keys and is validated server-side — an invalid policy
16078
+ * throws `BackendError` from the 422 `invalid_grant_policy`
16079
+ * response. A default TTL is not part of the mint grammar:
16080
+ * `defaultTtlSeconds` / `default_ttl_seconds` throws
16081
+ * `AlterValueError` (it is accepted only by Connect sessions and
16082
+ * `createManagedSecretGrant`). Supplying both spellings of one
16083
+ * trio field also throws `AlterValueError`.
15849
16084
  * @param options.grantTags - Optional list of tag strings stored on
15850
16085
  * the sibling grant.
15851
16086
  * @returns {@link GrantInfo} for the newly minted sibling grant
@@ -15881,7 +16116,7 @@ var App = class _App {
15881
16116
  }
15882
16117
  const wireBody = { label: options.label };
15883
16118
  if (options.grantPolicy !== void 0) {
15884
- wireBody.grant_policy = options.grantPolicy;
16119
+ wireBody.grant_policy = _mintGrantPolicyToWire(options.grantPolicy);
15885
16120
  }
15886
16121
  if (options.grantTags !== void 0) {
15887
16122
  wireBody.grant_tags = options.grantTags;
@@ -16324,7 +16559,7 @@ var DEFAULT_BASE_URL = "https://backend.alterauth.com";
16324
16559
  var PAT_API_PREFIX = "/api/v1/dev-portal";
16325
16560
  var HTTP_ERROR_THRESHOLD = 400;
16326
16561
  var DEFAULT_TIMEOUT_MS = 3e4;
16327
- var CLI_VERSION = "0.8.0";
16562
+ var CLI_VERSION = "0.9.0";
16328
16563
  var USER_AGENT = buildUserAgent();
16329
16564
  function buildUserAgent() {
16330
16565
  let osTag = "";
@@ -17330,7 +17565,7 @@ function isProviderCatalogEntry(value) {
17330
17565
  const v = value;
17331
17566
  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
17567
  (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(
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(
17334
17569
  isProviderScopeCatalogEntry
17335
17570
  ) && (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
17571
  (scope) => typeof scope === "string"
@@ -18485,6 +18720,25 @@ var AuditNamespace = class {
18485
18720
  });
18486
18721
  return expectDict(body, "audit.list", 200);
18487
18722
  }
18723
+ /** Provider facet values present in one app's audit events. Requires
18724
+ * `dashboard_audit:read`.
18725
+ *
18726
+ * Returns ``{providers}`` — the alphabetized distinct provider ids
18727
+ * (OAuth provider ids, managed-secret template ids — a custom secret's
18728
+ * name-derived slug — and IDP types) actually
18729
+ * present on the app's audit rows; the accepted vocabulary for the
18730
+ * ``--provider`` filters. Data-derived, never a hardcoded shortlist.
18731
+ */
18732
+ async providers(appId) {
18733
+ const query = optionsToQuery({ app_id: appId });
18734
+ const body = await this.#client._call(
18735
+ "GET",
18736
+ "/audit-logs/unified/providers",
18737
+ "audit.providers",
18738
+ { query }
18739
+ );
18740
+ return expectDict(body, "audit.providers", 200);
18741
+ }
18488
18742
  /** List dashboard / CLI admin actions. Requires `dashboard_audit:read`.
18489
18743
  *
18490
18744
  * Returns the canonical envelope ``{items, total, limit, offset, has_more}``
@@ -35607,6 +35861,9 @@ var CONTENT_MATCH_LIMITS = {
35607
35861
  };
35608
35862
  var MAX_RULE_NAME_LEN = 120;
35609
35863
  var MAX_RULE_DESCRIPTION_LEN = 2e3;
35864
+ var MAX_RULES_PER_TARGET = 100;
35865
+ var MAX_DERIVED_RULES_PER_TARGET = 2;
35866
+ var MAX_POLICY_RULE_PAGE_SIZE = MAX_RULES_PER_TARGET + MAX_DERIVED_RULES_PER_TARGET;
35610
35867
  var HITL_NOTIFICATION_CHANNELS = ["email"];
35611
35868
  var HITL_LIMITS = {
35612
35869
  maxApprovers: 10,
@@ -37302,6 +37559,33 @@ function buildAuditCommand() {
37302
37559
  });
37303
37560
  }
37304
37561
  );
37562
+ audit.command("providers").description(
37563
+ "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."
37564
+ ).option(
37565
+ APP_FLAG,
37566
+ "App whose audit trail to facet (ID or name; falls back to ALTER_APP_ID / the workspace pin)"
37567
+ ).option(
37568
+ "--output <format>",
37569
+ "Output format: json|jsonl|table (default: table)",
37570
+ "table"
37571
+ ).action(async (options) => {
37572
+ const format = coerceOutputFormat(options.output);
37573
+ const appId = await resolveAppOrExit(options.app);
37574
+ await withClient(async (client) => {
37575
+ const result = await client.audit.providers(appId);
37576
+ const providers = result["providers"];
37577
+ if (!Array.isArray(providers) || !providers.every((p) => typeof p === "string")) {
37578
+ err(
37579
+ 'audit.providers: response is missing the "providers" string array. This is a wire-format regression; report it to the backend team.'
37580
+ );
37581
+ process.exit(EXIT_ERROR);
37582
+ }
37583
+ const rows = providers.map((provider_id) => ({ provider_id }));
37584
+ emit2(format, rows, [
37585
+ { label: "PROVIDER", get: (r) => r.provider_id }
37586
+ ]);
37587
+ });
37588
+ });
37305
37589
  audit.command("show <trace-id>").description("Show every event for one trace").option(
37306
37590
  "--output <format>",
37307
37591
  "Output format: json|jsonl|table (default: json)",
@@ -44326,8 +44610,8 @@ function buildRulesSubcommand() {
44326
44610
  )
44327
44611
  ).option(
44328
44612
  "--limit <n>",
44329
- "Page size (1-100; when omitted the backend default of 100 applies)",
44330
- parseBoundedInt("--limit", 1, 100)
44613
+ `Page size (1-${MAX_POLICY_RULE_PAGE_SIZE}; when omitted the backend default of ${MAX_POLICY_RULE_PAGE_SIZE} applies)`,
44614
+ parseBoundedInt("--limit", 1, MAX_POLICY_RULE_PAGE_SIZE)
44331
44615
  ).option(
44332
44616
  "--offset <n>",
44333
44617
  "Page offset (default 0)",
@@ -44761,7 +45045,8 @@ async function providerCatalogError(client, providerId, environment, credentialS
44761
45045
  const availableScopes = provider.available_scopes ?? {};
44762
45046
  if (Object.keys(availableScopes).length === 0) {
44763
45047
  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)
45048
+ const where = provider.scopes_configured_on_provider === true ? `; its permissions are configured on the ${provider.display_name} app itself` : "";
45049
+ return `alter: provider '${providerId}' publishes no selectable scopes \u2014 omit --scopes (Alter does not send a scope parameter to it${where})
44765
45050
  `;
44766
45051
  }
44767
45052
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alter-ai/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Command-line interface for the Alter Vault dev portal — scripted dashboard automation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,7 +43,7 @@
43
43
  "keytar": "^7.9.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@alter-ai/alter-sdk": "workspace:0.23.3",
46
+ "@alter-ai/alter-sdk": "workspace:0.24.0",
47
47
  "@alter-vault/shared-types": "workspace:0.0.1",
48
48
  "@alter-vault/shared-utils": "workspace:0.0.1",
49
49
  "@eslint/js": "9.39.4",