@vectros-ai/cli 0.21.0 → 0.22.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.
package/dist/cli.mjs CHANGED
@@ -1619,7 +1619,7 @@ var require_proper_lockfile = __commonJS({
1619
1619
  // src/cli-bootstrap.ts
1620
1620
  import * as readline2 from "readline/promises";
1621
1621
 
1622
- // ../../node_modules/@vectros-ai/sdk/dist/index.mjs
1622
+ // node_modules/@vectros-ai/sdk/dist/index.mjs
1623
1623
  var __defProp2 = Object.defineProperty;
1624
1624
  var __export = (target, all) => {
1625
1625
  for (var name in all)
@@ -5739,7 +5739,7 @@ var AuthClient = class {
5739
5739
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
5740
5740
  }
5741
5741
  /**
5742
- * Updates the mutable fields of a registered issuer: `subClaim`, `emailClaim`, `status` (`active`/`suspended` — a suspended issuer's tokens are rejected identically to an unregistered issuer at exchange time), and `selfSignupPolicies`. Fields omitted from the body are left unchanged (partial update). `issuer`, `jwksUri`, `audience`, and `contextId` are trust-anchor / routing-pin fields and are immutable via this route — supplying a value that differs from the current registration is rejected with 400; supplying the current value back is a no-op. Rotating a trust anchor requires deleting and re-registering the issuer, which is itself refused while any user is bound through it. Requires a root API key or the bootstrap's provisioning capability, gated identically to every other operation on this surface. A credential confined to one app context may only update an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key may update any issuer.
5742
+ * Updates the mutable fields of a registered issuer: `subClaim`, `emailClaim`, `userinfoUri`, `capturedClaims`, `restrictedToDomain` (#1250 Tier 1a — a new non-blank value must already be a VERIFIED domain for your account; an empty string clears it back to domain-less; see `IssuerUpdateRequest.restrictedToDomain` for the full semantics), `status` (`active`/`suspended` — a suspended issuer's tokens are rejected identically to an unregistered issuer at exchange time), and `selfSignupPolicies`. Fields omitted from the body are left unchanged (partial update). `issuer`, `jwksUri`, `audience`, and `contextId` are trust-anchor / routing-pin fields and are immutable via this route — supplying a value that differs from the current registration is rejected with 400; supplying the current value back is a no-op. Rotating a trust anchor requires deleting and re-registering the issuer, which is itself refused while any user is bound through it. **`subClaim` is identity-determining, not merely cosmetic**: it names which verified JWT claim is read as the federated user's identifier, so changing it on an issuer that already has bound users would silently re-identify (or, under self-signup, orphan) every one of them — changing it is therefore refused once any user has bound through this issuer, the same guard `DELETE` already applies, and is only free before the first real login. Requires a root API key or the bootstrap's provisioning capability, gated identically to every other operation on this surface. A credential confined to one app context may only update an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key may update any issuer.
5743
5743
  *
5744
5744
  * @param {Vectros.IssuerUpdateRequest} request
5745
5745
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5806,7 +5806,7 @@ var AuthClient = class {
5806
5806
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/auth/issuers/{issuerId}");
5807
5807
  }
5808
5808
  /**
5809
- * Deregisters a trusted third-party IdP issuer. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context may only deregister an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key may deregister any issuer. Refused if any user account was ever created or matched via this issuer (by a prior self-signup or accepted invite, through `POST /v1/auth/token/exchange`) — that access cannot be silently orphaned. Deactivate the affected users first if you intend to cut off their access, or register a replacement issuer before removing this one. An issuer that has never been used for an exchange (no bound users yet) can always be deregistered.
5809
+ * Deregisters a trusted third-party IdP issuer. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context may only deregister an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key may deregister any issuer. Refused if any user account was ever created or matched via this issuer (by a prior self-signup or accepted invite, through `POST /v1/auth/token/exchange`) — that access cannot be silently orphaned. This is unconditional on the affected users' `status`: suspending them first does not lift the refusal. **A bound registration cannot be replaced, self-service, within its own app context**: registering a replacement issuer under a different issuerId in the SAME context also fails, because the context's one-active-issuer claim is released only when THIS registration is deleted. Two real options: suspend this issuer (stops new exchanges immediately) and register a replacement under a DIFFERENT app context — note that targets a different context, so this context's existing users/roles/access profiles are not carried over; or contact your platform operator, who can force-release this registration so a replacement may be registered in the SAME context under a new issuerId. **The operator path is not a lighter-weight alternative to the first — it PERMANENTLY RETIRES this issuerId and leaves every bound user unable to ever exchange through it again; it does not preserve continuity for them any better than registering under a different context does.** An issuer that has never been used for an exchange (no bound users yet) can always be deregistered.
5810
5810
  *
5811
5811
  * @param {Vectros.DeleteIssuerRequest} request
5812
5812
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5932,7 +5932,7 @@ var AuthClient = class {
5932
5932
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
5933
5933
  }
5934
5934
  /**
5935
- * Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. A credential authorized only via the provisioning capability may register only against the app context it is bound to; naming a different one returns 403. A root API key is unaffected and may register against any of its contexts. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant. If `issuerId` collides with a registration owned by a different app context than the one you're confined to, the request fails with 400 rather than returning that context's configuration. An app context may have at most one active issuer — deregister the existing one first if you need to replace it. One issuer MAY serve several contexts today, each via its own registration row with a distinct `audience`.
5935
+ * Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. A credential authorized only via the provisioning capability may register only against the app context it is bound to; naming a different one returns 403. A root API key is unaffected and may register against any of its contexts. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant. If `issuerId` collides with a registration owned by a different app context than the one you're confined to, the request fails with 400 rather than returning that context's configuration. An app context may have at most one active issuer — deregister the existing one first if you need to replace it. One issuer MAY serve several contexts today, each via its own registration row with a distinct `audience`. Optionally name `restrictedToDomain` (#1250 Tier 1a) to scope this registration's (issuer, audience) uniqueness to a specific VERIFIED domain rather than the bare pair — see that field's own description for the full contract, including why it does NOT protect a shared/consumer-IdP registration with no company-domain population.
5936
5936
  *
5937
5937
  * @param {Vectros.IssuerRequest} request
5938
5938
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7430,7 +7430,7 @@ var DocumentsClient = class {
7430
7430
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/documents/{id}/versions");
7431
7431
  }
7432
7432
  /**
7433
- * Starts a file-based document by returning a short-lived presigned S3 PUT URL. Upload the file bytes directly to `uploadUrl`; the document is then automatically queued for text extraction and asynchronous indexing. Supplying an `externalId` makes this idempotent — re-initiating an upload with the same `externalId` re-issues a fresh presigned URL to the SAME existing document/object (so a re-upload inherently replaces the file body) rather than creating a duplicate. The response's `created` field (and the HTTP status — 201 when a new document was minted, 200 when an existing one was matched) tells the two apart. With `?upsert=true`, the submitted `payload`/`title` are also applied to the matched document (file-body divergence cannot be diffed at upload-init — the bytes have not arrived yet — so the re-upload itself replaces the body). Creating a NEW document requires the `documents:c` scope. Re-uploading over an EXISTING document overwrites (and re-indexes) its body, so it is an update: it requires the `documents:u` scope (as does `?upsert=true` for the metadata).
7433
+ * Starts a file-based document by returning a short-lived presigned S3 PUT URL. Upload the file bytes directly to `uploadUrl`; the document is then automatically queued for text extraction and asynchronous indexing. Supplying an `externalId` makes this idempotent — re-initiating an upload with the same `externalId` re-issues a fresh presigned URL for the SAME existing document, targeting a newly-staged object rather than the document's current one — your PUT lands there first, and the document only adopts it once the upload is validated (so a re-upload inherently replaces the file body) rather than creating a duplicate. The response's `created` field (and the HTTP status — 201 when a new document was minted, 200 when an existing one was matched) tells the two apart. With `?upsert=true`, the submitted `payload`/`title` are also applied to the matched document (file-body divergence cannot be diffed at upload-init — the bytes have not arrived yet — so the re-upload itself replaces the body). Creating a NEW document requires the `documents:c` scope. Re-uploading over an EXISTING document overwrites (and re-indexes) its body, so it is an update: it requires the `documents:u` scope (as does `?upsert=true` for the metadata). **Your PUT to `uploadUrl` MUST include the header named in the response's `requiredHeaderName`, set to `requiredHeaderValue` exactly.** It is a signed part of the presigned request — the PUT never reaches this service either way, so neither failure uses this API's JSON error shape. Omitting it, or sending a different value, invalidates the request's signature and S3 rejects it with a 403 `SignatureDoesNotMatch`. Sending it correctly makes the URL single-use: `requiredHeaderName` is always `If-None-Match` and `requiredHeaderValue` is always `*` — a create, a `#559` heal recovery, and a re-upload ALL target a freshly-minted, never-written key, so the same S3 conditional-write precondition applies uniformly (S3's own documented conditional-write behavior): a replay after the object already exists (from your own completed PUT, or anyone else's) fails with `412 Precondition Failed`; a replay after the object was deleted and never re-created finds an absent key again and can succeed — single-use is a property of the object's existence, not a one-time token. **Either precondition can also fail with `409 Conflict` if your PUT genuinely races a concurrent delete on the same object.** Whichever of `412` or `409` you receive, the remedy is the same: call this endpoint again for a fresh URL rather than retrying the same PUT — a fresh call always targets a new key, so it is never subject to whatever raced the previous one.
7434
7434
  *
7435
7435
  * @param {Vectros.FileUploadRequest} request
7436
7436
  * @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9196,7 +9196,7 @@ var IdentityClient = class {
9196
9196
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/exists-by-email");
9197
9197
  }
9198
9198
  /**
9199
- * Returns the audit trail of changes to a user, most recent first. Identity history is always recorded and always available. Sensitive field values are redacted in every historical version. Returns a page in the `{data, nextCursor}` envelope. Requires the `users:r` scope.
9199
+ * Returns the audit trail of changes to a user, most recent first, including after the user has been deleted. Identity history is always recorded. Post-delete, this is readable by an account-level (root) API key only — a context-confined credential 404s after the delete, the same response it would get for a nonexistent id. Sensitive field values are redacted in every historical version. Returns a page in the `{data, nextCursor}` envelope. Requires the `users:r` scope.
9200
9200
  *
9201
9201
  * @param {Vectros.GetUserVersionsRequest} request
9202
9202
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -10673,7 +10673,7 @@ var SchemasClient = class {
10673
10673
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/schemas/{id}");
10674
10674
  }
10675
10675
  /**
10676
- * Permanently deletes a record schema. The request is refused with 409 if records of this type still exist — delete those records first, since every record must reference a live schema. A lineage base (a schema other schemas declare `basedOn`) also cannot be deleted while any such variant still exists — delete the variant schema(s) first. It is likewise refused while any trigger rule fires off this schema — delete those trigger rules first. Requires the `schemas:d` scope.
10676
+ * Permanently deletes a record schema. The request is refused with 409 if records of this type still exist — delete those records first, since every record must reference a live schema. The same applies to documents bound to this schema (for a schema declaring the `document` surface) — delete those documents first. A lineage base (a schema other schemas declare `basedOn`) also cannot be deleted while any such variant still exists — delete the variant schema(s) first. It is likewise refused while any trigger rule fires off this schema — delete those trigger rules first. Requires the `schemas:d` scope.
10677
10677
  *
10678
10678
  * @param {Vectros.DeleteSchemaRequest} request
10679
10679
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -10808,7 +10808,7 @@ var ScriptsClient = class {
10808
10808
  *
10809
10809
  * **Budget.** Seconds of useful work: the script has 15 s of wall clock (never more than 16) and a bounded number of statements and host calls; a runaway script is stopped early with `RESOURCE_LIMIT_EXCEEDED`. The transaction is bounded by the number of underlying storage rows it writes — 100 rows after merging — rather than by the number of entities: creating a folder costs 3 rows; creating a plain text document or a record costs 1 row plus one per range-indexed lookup field plus one per `reference` field. So "create a folder and file N documents" holds up to 97 plain documents, 48 with one range field or one reference, 32 with both; if a script exceeds the limit the error says so (`WRITE_BUFFER_CAP_EXCEEDED`, with `total` and `limit`) and nothing is committed. This request takes the write-path limits (burst, credit ceiling, principal quota) once, before the script runs — there is no free-read allowance here, so an account over its credit ceiling is refused even for a read-only script. Reads the script performs are metered as reads, writes as writes, and execution time beyond what those operations include is charged as script execution time (see `GET /v1/usage`).
10810
10810
  *
10811
- * **Reads inside the script.** A `get` sees the script's own staged writes. A list, query or lookup never shows a row the script has deleted, shows a row it re-saved with the updated content, and does not yet show a row it created — you hold that row's id from `create()`.
10811
+ * **Reads inside the script.** A `get` sees the script's own staged writes. A list, query or lookup never shows a row the script has deleted, shows a row it re-saved with the updated content, and does not show a row it created — you hold that row's id from `create()`.
10812
10812
  *
10813
10813
  * **Retries.** Send an optional `Idempotency-Key` header (1–128 characters of letters, digits, `.`, `_`, `:` or `-`) to make a retry safe: a request repeated under the same key within 24 hours receives the same response as the first attempt without executing again — when that first attempt SUCCEEDED (a duplicate still in flight is refused with `409 IDEMPOTENCY_IN_PROGRESS`; the same key on a different request is refused with `422 IDEMPOTENCY_KEY_REUSED`). A first attempt that FAILED with any other status is not remembered: the same key is free to try again, so a retry after a `400` or a `429` executes. If an execution's outcome cannot be determined — the script did not finish inside the budget while its writes may have committed — the response is `500 EXECUTION_OUTCOME_UNKNOWN` carrying `execution.id`, and a keyed retry receives that same answer: inspect what the script would have written, then use a NEW key for a new attempt. When a key is present the serialised `result` is capped at 16 KB instead of 256 KB. Without a key, a lost response cannot be distinguished from a failed one; re-read before retrying.
10814
10814
  *
@@ -10903,11 +10903,14 @@ var ScriptsClient = class {
10903
10903
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/scripts/execute");
10904
10904
  }
10905
10905
  /**
10906
- * Returns a paginated list of your scripts. Pass `name` to list every version of one script name (oldest first); omit it for a flat list across every name in your account (newest first). Results are returned as a `{data, nextCursor}` envelope — pass `nextCursor` as `startFrom` to fetch the next page. Requires the `scripts:r` scope.
10906
+ * Returns a paginated list of your scripts. Pass `name` to list every version of one script name (oldest first); omit it for a flat list across every name in your account (newest first). Results are returned as a `{data, nextCursor}` envelope — pass `nextCursor` as `startFrom` to fetch the next page. Requires the `scripts:r` scope. **Each row's `source` is omitted by default** (`sourceOmitted: true`) — scripts are immutable per version, so a name's version history can otherwise carry every version's complete source in one response. Pass `?includeSource=true` to get it back inline, or fetch one version's full source with a by-id GET. **`?name=<n>&latest=true`** answers "what is the current version of this name" in one bounded call — a single `ScriptResponse` object (not a page), instead of draining every page of the name's version history to find the newest yourself.
10907
10907
  *
10908
10908
  * @param {Vectros.ListScriptsRequest} request
10909
10909
  * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
10910
10910
  *
10911
+ * @throws {@link Vectros.BadRequestError}
10912
+ * @throws {@link Vectros.NotFoundError}
10913
+ *
10911
10914
  * @example
10912
10915
  * await client.scripts.listScripts({
10913
10916
  * name: "notify-assignee",
@@ -10918,11 +10921,13 @@ var ScriptsClient = class {
10918
10921
  return HttpResponsePromise.fromPromise(this.__listScripts(request, requestOptions));
10919
10922
  }
10920
10923
  async __listScripts(request = {}, requestOptions) {
10921
- const { name, startFrom, limit } = request;
10924
+ const { name, startFrom, limit, includeSource, latest } = request;
10922
10925
  const _queryParams = {
10923
10926
  name,
10924
10927
  startFrom,
10925
- limit
10928
+ limit,
10929
+ includeSource,
10930
+ latest
10926
10931
  };
10927
10932
  const _authRequest = await this._options.authProvider.getAuthRequest();
10928
10933
  const _headers = mergeHeaders(
@@ -10948,16 +10953,23 @@ var ScriptsClient = class {
10948
10953
  return { data: _response.body, rawResponse: _response.rawResponse };
10949
10954
  }
10950
10955
  if (_response.error.reason === "status-code") {
10951
- throw new VectrosError({
10952
- statusCode: _response.error.statusCode,
10953
- body: _response.error.body,
10954
- rawResponse: _response.rawResponse
10955
- });
10956
+ switch (_response.error.statusCode) {
10957
+ case 400:
10958
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
10959
+ case 404:
10960
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
10961
+ default:
10962
+ throw new VectrosError({
10963
+ statusCode: _response.error.statusCode,
10964
+ body: _response.error.body,
10965
+ rawResponse: _response.rawResponse
10966
+ });
10967
+ }
10956
10968
  }
10957
10969
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/scripts");
10958
10970
  }
10959
10971
  /**
10960
- * Stores a new version of a script object. Every POST creates a genuinely new version — this is NOT idempotent-by-name: pushing `name` again always creates a new row with `scriptVersion` auto-incremented (the first version pushed for a name is 1). `source` is stored as-is: nothing parses or validates it at push time. A version runs when a trigger rule references it (`POST /v1/triggers`) or when `POST /v1/scripts/execute` names it. Requires the `scripts:c` scope.
10972
+ * Stores a new version of a script object. Every POST creates a genuinely new version — this is NOT idempotent-by-name: pushing `name` again always creates a new row with `scriptVersion` auto-incremented (the first version pushed for a name is 1). `source` is stored as-is: nothing parses or validates it at push time. A version runs when a trigger rule references it (`POST /v1/triggers`) or when `POST /v1/scripts/execute` names it. `provisionedBy` MAY BE SET ON ABSENT, NEVER CHANGED, evaluated against the CURRENT LATEST version of this `name` at push time: if the latest version has no marker recorded, this push may set any value; if the latest version already has one, an omitted value inherits it forward and a DIFFERENT value is refused — a live comparison against whichever version is newest at push time, not a value fixed for the name's whole history. Pushing onto an EXISTING name with no `provisionedBy` recorded on its current latest version additionally requires that you were the caller who pushed that latest version — a name carrying a `provisionedBy` marker is exempt from this check (proving you can supply or inherit the matching marker is itself the ownership proof there). Requires the `scripts:c` scope.
10961
10973
  *
10962
10974
  * @param {Vectros.ScriptRequest} request
10963
10975
  * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -11144,12 +11156,13 @@ var ScriptsClient = class {
11144
11156
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/scripts/{id}");
11145
11157
  }
11146
11158
  /**
11147
- * Permanently deletes one script version by ID. Deleting one version does not affect any other version of the same name. Requires the `scripts:d` scope.
11159
+ * Permanently deletes one script version by ID. Deleting one version does not affect any other version of the same name. Requires the `scripts:d` scope. Refused with 409 if a live trigger rule still references this version — either directly (a pinned `scriptRef`), or via `"latest"` when this IS the newest version of its name. Delete or re-point those rules first.
11148
11160
  *
11149
11161
  * @param {Vectros.DeleteScriptRequest} request
11150
11162
  * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
11151
11163
  *
11152
11164
  * @throws {@link Vectros.NotFoundError}
11165
+ * @throws {@link Vectros.ConflictError}
11153
11166
  * @throws {@link Vectros.TooManyRequestsError}
11154
11167
  *
11155
11168
  * @example
@@ -11189,6 +11202,8 @@ var ScriptsClient = class {
11189
11202
  switch (_response.error.statusCode) {
11190
11203
  case 404:
11191
11204
  throw new NotFoundError(_response.error.body, _response.rawResponse);
11205
+ case 409:
11206
+ throw new ConflictError(_response.error.body, _response.rawResponse);
11192
11207
  case 429:
11193
11208
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11194
11209
  default:
@@ -11734,6 +11749,25 @@ var logging;
11734
11749
  import { z } from "zod";
11735
11750
  import { z as z2 } from "zod";
11736
11751
  var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
11752
+ var FORGERY_CHARS_RE = /[\x00-\x1f\x7f-\x9f\u00ad\u061c\u200b-\u200f\u2028\u2029\u202a-\u202e\u2060\u2066-\u2069\ufeff\u{e0000}-\u{e007f}]/u;
11753
+ function safeDisplayString(base) {
11754
+ return base.refine((v) => !FORGERY_CHARS_RE.test(v), {
11755
+ message: "must not contain control characters, invisible/zero-width characters, line/paragraph separators, or bidi marks/overrides \u2014 these can forge, hide, or reorder text in a rendered plan preview"
11756
+ });
11757
+ }
11758
+ function safeUrlString(base) {
11759
+ return base.refine(
11760
+ (v) => {
11761
+ if (!/^[\x20-\x7e]*$/.test(v)) return false;
11762
+ const authorityMatch = /^https:\/\/([^/?#]*)/i.exec(v);
11763
+ if (!authorityMatch) return false;
11764
+ return /^[A-Za-z0-9.-]+(:\d+)?$/.test(authorityMatch[1]);
11765
+ },
11766
+ {
11767
+ message: "must be a printable-ASCII https:// URL whose authority is only letters, digits, dot, dash, and an optional port \u2014 no userinfo ('@'), percent-encoding, or non-ASCII host (including a look-alike/homoglyph domain) is accepted; supply an internationalized domain in its punycode (xn--\u2026) form"
11768
+ }
11769
+ );
11770
+ }
11737
11771
  var MAX_SCOPE_DIMENSIONS = 2;
11738
11772
  var ValidationRulesSchema = z.object({
11739
11773
  required: z.boolean().optional(),
@@ -11763,7 +11797,7 @@ var RenderHintsSchema = z.object({
11763
11797
  displayField: z.boolean().optional()
11764
11798
  }).strict();
11765
11799
  var BlueprintFieldDefSchema = z.object({
11766
- fieldId: z.string().min(1),
11800
+ fieldId: safeDisplayString(z.string().min(1)),
11767
11801
  fieldType: z.string().min(1),
11768
11802
  required: z.boolean().optional(),
11769
11803
  searchable: z.boolean().optional(),
@@ -11854,13 +11888,13 @@ var BlueprintFieldDefSchema = z.object({
11854
11888
  }
11855
11889
  });
11856
11890
  var BlueprintLookupFieldSchema = z.union([
11857
- z.string().min(1),
11891
+ safeDisplayString(z.string().min(1)),
11858
11892
  z.object({
11859
11893
  // A lookup field declares either ONE field (fieldName) or 2-3 fields
11860
11894
  // matched together (fieldNames) — never both, never neither. `.strict()`
11861
11895
  // below only rejects unknown KEYS; it does not enforce this combination,
11862
11896
  // so the `.superRefine()` after it carries the actual rule.
11863
- fieldName: z.string().min(1).optional(),
11897
+ fieldName: safeDisplayString(z.string().min(1)).optional(),
11864
11898
  // Composite (conjunctive) lookup: match on all of these fields at once —
11865
11899
  // "every record where `status` is `open` AND `area` is `billing`", exact,
11866
11900
  // in a stable order. Order is significant and locked at create: a query
@@ -11869,7 +11903,7 @@ var BlueprintLookupFieldSchema = z.union([
11869
11903
  // lookup for that. Record-only: the schema's `allowedSurfaces` must be
11870
11904
  // exactly `['record']` (checked at the schema level below) — the
11871
11905
  // platform's composite index has no document/user/entity reader yet.
11872
- fieldNames: z.array(z.string().min(1)).min(2).max(3).optional(),
11906
+ fieldNames: z.array(safeDisplayString(z.string().min(1))).min(2).max(3).optional(),
11873
11907
  // Not available on a composite (`fieldNames`) lookup — checked below.
11874
11908
  unique: z.boolean().optional(),
11875
11909
  // Opt this field into ordered range + prefix lookups (from/to/prefix) on
@@ -11919,7 +11953,14 @@ var BlueprintCapabilitiesSchema = z.object({
11919
11953
  triggersEnabled: z.boolean().optional()
11920
11954
  }).strict();
11921
11955
  var BlueprintSchemaSchema = z.object({
11922
- typeName: z.string().min(1),
11956
+ // safeDisplayString: buildPlan's schema step renders this raw into a single summary
11957
+ // line (`schema '${s.typeName}' (...)`) — a gap found after the rest of this file's
11958
+ // forgery-character pass had already shipped, the same "unconstrained free-text field
11959
+ // reaches a rendered plan line" shape as issuer/allowedActions/etc. `displayName`
11960
+ // here is NOT wrapped: unlike
11961
+ // servicePrincipal's own displayName, this one is never interpolated into a rendered
11962
+ // summary anywhere in loader.ts today (only forwarded to the schema-upsert payload).
11963
+ typeName: safeDisplayString(z.string().min(1)),
11923
11964
  displayName: z.string().min(1),
11924
11965
  description: z.string().optional(),
11925
11966
  // Default search-index mode for instances of this type. HYBRID, SEMANTIC and
@@ -12056,7 +12097,7 @@ var BlueprintAccessProfileSchema = z.object({
12056
12097
  // `.superRefine` on this schema enforces the XOR, mirroring the platform's
12057
12098
  // own access-profile scopes/role-composition XOR check). Exactly one of
12058
12099
  // the two authors this profile's grant.
12059
- allowedActions: z.array(z.string().min(1)).min(1).optional(),
12100
+ allowedActions: z.array(safeDisplayString(z.string().min(1))).min(1).optional(),
12060
12101
  // Additive role composition: this profile's effective grant is
12061
12102
  // each named role's own clauses, concatenated in the order listed, instead of
12062
12103
  // an inline clause authored here. Every id must name a role this SAME
@@ -12085,7 +12126,14 @@ var BlueprintAccessProfileSchema = z.object({
12085
12126
  // NOT see tenant-level/seed records). Every KEY is validated below
12086
12127
  // (`lintDataScopeKeys`) — `userId` or a grammar-valid `scope:<ns>`, no
12087
12128
  // other shape.
12088
- dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
12129
+ // safeDisplayString on the value: not currently rendered by VALUE anywhere in buildPlan
12130
+ // (only `Object.keys(...)` of a profile's dataScope is shown today, unlike a role clause's
12131
+ // dataScope, which `renderPlanDataScope` prints values for — the reason that sibling field
12132
+ // WAS wrapped). Wrapped anyway for consistency with every other dataScope-shaped field in
12133
+ // this file, and so a future preview enhancement that starts showing these values (a very
12134
+ // plausible one, given the existing renderPlanDataScope precedent) doesn't silently reopen
12135
+ // this exact class on a field that looks, byte-for-byte, like every other one already closed.
12136
+ dataScope: z.record(z.array(z.union([safeDisplayString(z.string().min(1)), z.null()]))).optional(),
12089
12137
  // Optional identity overrides for the service principal's profile — scope
12090
12138
  // values its key STAMPS onto everything it writes (a credential can only
12091
12139
  // stamp ownership its identity carries). Keys are the namespaced `scope:<ns>`
@@ -12161,8 +12209,8 @@ var BlueprintAccessProfileSchema = z.object({
12161
12209
  }
12162
12210
  });
12163
12211
  var BlueprintRoleClauseSchema = z.object({
12164
- allowedActions: z.array(z.string().min(1)).min(1),
12165
- dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
12212
+ allowedActions: z.array(safeDisplayString(z.string().min(1))).min(1),
12213
+ dataScope: z.record(z.array(z.union([safeDisplayString(z.string().min(1)), z.null()]))).optional(),
12166
12214
  dataScopeRef: z.string().min(1).optional(),
12167
12215
  capabilities: GrantedCapabilitiesSchema,
12168
12216
  assignableRoles: AssignableRolesSchema
@@ -12176,10 +12224,10 @@ var BlueprintRoleClauseSchema = z.object({
12176
12224
  }
12177
12225
  });
12178
12226
  var BlueprintFragmentsSchema = z.record(
12179
- z.record(z.array(z.union([z.string().min(1), z.null()])))
12227
+ z.record(z.array(z.union([safeDisplayString(z.string().min(1)), z.null()])))
12180
12228
  );
12181
- var BlueprintRolesSchema = z.record(z.array(BlueprintRoleClauseSchema).min(1));
12182
- var BlueprintRoleAssumableSchema = z.record(z.array(z.string().min(1)).min(1));
12229
+ var BlueprintRolesSchema = z.record(safeDisplayString(z.string().min(1)), z.array(BlueprintRoleClauseSchema).min(1));
12230
+ var BlueprintRoleAssumableSchema = z.record(z.array(safeDisplayString(z.string().min(1))).min(1));
12183
12231
  var BlueprintRoleAssumableMapSchema = z.record(BlueprintRoleAssumableSchema);
12184
12232
  var BlueprintTriggerFiringSourceSchema = z.object({
12185
12233
  schemaName: z.string().min(1),
@@ -12187,7 +12235,12 @@ var BlueprintTriggerFiringSourceSchema = z.object({
12187
12235
  }).strict();
12188
12236
  var SCRIPT_VERSION_INTEGER_RE = /^[1-9][0-9]*$/;
12189
12237
  var BlueprintTriggerScriptRefSchema = z.object({
12190
- name: z.string().min(1),
12238
+ // safeDisplayString: rendered raw into buildPlan's trigger step (`${scriptRef.name}@...`).
12239
+ // When naming a script THIS blueprint ships, `name` is transitively safe already (it must
12240
+ // equal a `scripts` map key, itself grammar-pinned by SCRIPT_NAME_RE) — but a scriptRef
12241
+ // naming a script this blueprint does NOT ship is, by design, "entirely unrestricted" (see
12242
+ // this file's own doc on that case), so the wrap is load-bearing for exactly that path.
12243
+ name: safeDisplayString(z.string().min(1)),
12191
12244
  version: z.string().min(1)
12192
12245
  }).strict().superRefine((ref, ctx) => {
12193
12246
  if (ref.version !== "latest" && !SCRIPT_VERSION_INTEGER_RE.test(ref.version)) {
@@ -12207,9 +12260,9 @@ var BlueprintTriggerManifestSchema = z.array(
12207
12260
  var BlueprintTriggerEntrySchema = z.object({
12208
12261
  firingSource: BlueprintTriggerFiringSourceSchema,
12209
12262
  scriptRef: BlueprintTriggerScriptRefSchema,
12210
- allowedActions: z.array(z.string().min(1)).min(1).optional(),
12263
+ allowedActions: z.array(safeDisplayString(z.string().min(1))).min(1).optional(),
12211
12264
  roleIds: z.array(z.string().min(1)).min(1).optional(),
12212
- dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
12265
+ dataScope: z.record(z.array(z.union([safeDisplayString(z.string().min(1)), z.null()]))).optional(),
12213
12266
  capabilities: GrantedCapabilitiesSchema,
12214
12267
  manifest: BlueprintTriggerManifestSchema,
12215
12268
  /**
@@ -12233,7 +12286,7 @@ var BlueprintTriggerEntrySchema = z.object({
12233
12286
  * together are capped at 224 KB serialised — a firing whose projected input exceeds that is not
12234
12287
  * run, and is recorded as an `INPUT_TOO_LARGE` failure naming the size.
12235
12288
  */
12236
- fields: z.array(z.string().min(1))
12289
+ fields: z.array(safeDisplayString(z.string().min(1)))
12237
12290
  }).strict().superRefine((entry, ctx) => {
12238
12291
  const hasInline = entry.allowedActions !== void 0;
12239
12292
  const hasRoleIds = entry.roleIds !== void 0;
@@ -12262,7 +12315,7 @@ var BlueprintTriggerEntrySchema = z.object({
12262
12315
  }
12263
12316
  }
12264
12317
  });
12265
- var BlueprintTriggersSchema = z.record(BlueprintTriggerEntrySchema);
12318
+ var BlueprintTriggersSchema = z.record(safeDisplayString(z.string().min(1)), BlueprintTriggerEntrySchema);
12266
12319
  var SCRIPT_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
12267
12320
  var SCRIPT_SOURCE_MAX_BYTES = 3e5;
12268
12321
  var SCRIPT_INPUT_CONTRACT_MAX_BYTES = 5e4;
@@ -12308,8 +12361,15 @@ var IdentityKindSchema = z.string().min(1).superRefine((val, ctx) => {
12308
12361
  });
12309
12362
  var IdentityDeclSchema = z.object({
12310
12363
  kind: IdentityKindSchema,
12311
- externalId: z.string().min(1),
12312
- displayName: z.string().min(1).optional(),
12364
+ // safeDisplayString: `@vectros-ai/cli`'s `runBootstrapCli` and `blueprint plan` both print
12365
+ // this RAW to stdout ("~ <name> (<kind>) — external id '<externalId>'"), immediately before
12366
+ // the "Apply this plan? [y/N]" confirmation gate itself — the highest-severity gap found in
12367
+ // this pass, since it sits at the confirmation prompt's own disclosure text, not merely
12368
+ // somewhere upstream of it. `displayName` is wrapped too though not currently
12369
+ // rendered in any preview (defense-in-depth, matching this file's practice for its adjacent
12370
+ // sibling fields) — it does reach a platform API payload as an entity's `name`.
12371
+ externalId: safeDisplayString(z.string().min(1)),
12372
+ displayName: safeDisplayString(z.string().min(1)).optional(),
12313
12373
  metadata: z.record(z.unknown()).optional()
12314
12374
  }).strict();
12315
12375
  var IDENTITY_NAME_RE = /^[A-Za-z_]\w*$/;
@@ -12320,8 +12380,8 @@ var IdentitiesDeclSchema = z.record(
12320
12380
  IdentityDeclSchema
12321
12381
  );
12322
12382
  var BlueprintServicePrincipalSchema = z.object({
12323
- externalId: z.string().min(1),
12324
- displayName: z.string().min(1)
12383
+ externalId: safeDisplayString(z.string().min(1)),
12384
+ displayName: safeDisplayString(z.string().min(1))
12325
12385
  }).strict();
12326
12386
  var ISSUER_ID_RE = CONTEXT_ID_RE;
12327
12387
  var MAX_CAPTURED_CLAIMS = 20;
@@ -12347,10 +12407,17 @@ var BlueprintIssuerSchema = z.object({
12347
12407
  issuerId: z.string().regex(ISSUER_ID_RE, {
12348
12408
  message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
12349
12409
  }),
12350
- /** The IdP's `iss` claim value, exactly as it appears in tokens it issues. */
12351
- issuer: z.string().min(1),
12352
- /** The IdP's remote JWKS endpoint, used to verify presented tokens' signatures. */
12353
- jwksUri: z.string().min(1),
12410
+ /**
12411
+ * The IdP's `iss` claim value, exactly as it appears in tokens it issues. `safeUrlString`,
12412
+ * not `safeDisplayString`: this is the trust-anchor field the plan preview exists to
12413
+ * disclose, so it gets the stronger positive check (see that function's own doc) rather
12414
+ * than the blocklist every OTHER free-text field uses — a homoglyph host here is exactly
12415
+ * what an operator approving a plan must be able to trust reading. Virtually every real
12416
+ * OIDC issuer is URL-shaped in practice, even though the spec does not strictly require it.
12417
+ */
12418
+ issuer: safeUrlString(z.string().min(1)),
12419
+ /** The IdP's remote JWKS endpoint, used to verify presented tokens' signatures. `safeUrlString` — see `issuer`'s own comment just above. */
12420
+ jwksUri: safeUrlString(z.string().min(1)),
12354
12421
  /**
12355
12422
  * The `aud` claim value a presented subject_token must carry. Must be globally
12356
12423
  * unique in combination with `issuer` — use a distinct audience per
@@ -12378,7 +12445,9 @@ var BlueprintIssuerSchema = z.object({
12378
12445
  * back to calling this endpoint (with the presented token as the bearer credential) and reads
12379
12446
  * `emailClaim` from its JSON response instead. Omit to leave the fallback disabled.
12380
12447
  */
12381
- userinfoUri: z.string().min(1).optional(),
12448
+ // `safeUrlString` — see `issuer`'s own comment above for why these three trust-anchor
12449
+ // fields get the stronger positive check instead of `safeDisplayString`'s blocklist.
12450
+ userinfoUri: safeUrlString(z.string().min(1)).optional(),
12382
12451
  /**
12383
12452
  * Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
12384
12453
  * caller presents no invite token but names a signupType matching one of these (or omits it and
@@ -12418,9 +12487,18 @@ var BlueprintIssuerSchema = z.object({
12418
12487
  * separate opt-ins, and capturing a claim does not by itself expose it on a profile.
12419
12488
  */
12420
12489
  capturedClaims: z.array(
12421
- z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
12422
- message: `a capturedClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
12423
- }).refine((name) => name.trim().length > 0, {
12490
+ // `safeDisplayString` (a `.refine`) then the blank-name `.refine` — both ordered AFTER
12491
+ // `.max`, since `.refine` returns a ZodEffects, which has no `.max` (`.refine` itself
12492
+ // chains fine onto a ZodEffects — it's on the ZodType base, not ZodString-specific).
12493
+ // `.min(1)` would be a LENGTH check and would admit a whitespace-only name while the
12494
+ // message claims "non-blank"; the platform rejects on isBlank(). Note the value is
12495
+ // REJECTED, not trimmed — it reaches the wire exactly as authored, matching how the
12496
+ // platform stores it.
12497
+ safeDisplayString(
12498
+ z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
12499
+ message: `a capturedClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
12500
+ })
12501
+ ).refine((name) => name.trim().length > 0, {
12424
12502
  message: "a capturedClaims entry must be a non-blank OIDC claim name"
12425
12503
  })
12426
12504
  ).max(MAX_CAPTURED_CLAIMS, {
@@ -12591,14 +12669,24 @@ function lintNamespaces(value, ctx) {
12591
12669
  });
12592
12670
  }
12593
12671
  var SeedCommonShape = {
12594
- /** The schema typeName this seed instantiates (should match a declared schema). */
12595
- typeName: z.string().min(1),
12672
+ /**
12673
+ * The schema typeName this seed instantiates (should match a declared schema).
12674
+ * `safeDisplayString`, matching `BlueprintSchemaSchema.typeName`: NOT currently
12675
+ * rendered into any plan-preview line (the seed step only counts records/documents),
12676
+ * but it is the same field concept and costs nothing to close now rather than leave
12677
+ * as a silent gap the next preview enhancement reopens.
12678
+ */
12679
+ typeName: safeDisplayString(z.string().min(1)),
12596
12680
  /**
12597
12681
  * Stable, caller-supplied id — the loader's idempotency key AND the value other
12598
12682
  * seeds resolve a `reference` against (across surfaces: a record seed may
12599
12683
  * reference a document seed by its externalId, and vice versa).
12684
+ * `safeDisplayString`: not currently rendered in a plan-preview line (same as this schema's
12685
+ * `typeName` just above until this pass), but the same "costs nothing to close now" standard
12686
+ * applies — a short caller-supplied id, not free-form prose, so no YAML block-scalar
12687
+ * collision risk like `description`'s.
12600
12688
  */
12601
- externalId: z.string().min(1),
12689
+ externalId: safeDisplayString(z.string().min(1)),
12602
12690
  /**
12603
12691
  * Optional scope ownership for the seeded item, as `namespace:value` entries
12604
12692
  * (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
@@ -13257,15 +13345,31 @@ var BLUEPRINT_FIELD_PHASES = Object.freeze({
13257
13345
  });
13258
13346
  var BlueprintSchema = z.object({
13259
13347
  /** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
13260
- name: z.string().min(1),
13261
- version: z.string().min(1),
13348
+ name: safeDisplayString(z.string().min(1)),
13349
+ version: safeDisplayString(z.string().min(1)),
13350
+ // NOT safeDisplayString — deliberately, after that wrap broke a real, multi-line-authored
13351
+ // blueprint's `description` in review. `description` is never interpolated into any
13352
+ // rendered plan-preview line (`buildPlan`'s header prints
13353
+ // `name`/`version`/`contextId`, never `description` — confirmed by grep, not assumed), so
13354
+ // wrapping it protected a sink that does not exist. It also actively broke real content:
13355
+ // this is exactly the field authors reach for a YAML folded (`>`) or literal (`|`) block
13356
+ // scalar to write, and clip chomping (the YAML default) always leaves exactly one trailing
13357
+ // `\n` — which `safeDisplayString` rejects as a C0 control, indistinguishable from a
13358
+ // forgery attempt. See `safeDisplayString`'s own doc for why this is a property of the
13359
+ // wrapper, not a one-off carve-out for this field alone.
13262
13360
  description: z.string().min(1),
13263
13361
  /** The app-context the profile + scoped key bind to (e.g. "mcp"). */
13264
13362
  contextId: z.string().regex(CONTEXT_ID_RE, {
13265
13363
  message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'mcp')"
13266
13364
  }),
13267
- /** Human-readable app-context name; defaults to `MCP — <name>` (see {@link contextNameOf}) when absent. */
13268
- contextName: z.string().min(1).optional(),
13365
+ /**
13366
+ * Human-readable app-context name; defaults to `MCP — <name>` (see {@link contextNameOf})
13367
+ * when absent. `safeDisplayString`: `contextNameOf` renders this raw into the FIRST line of
13368
+ * every `buildPlan` summary (the `context '...' (...)` step) — a gap found after the rest
13369
+ * of this file's forgery-character pass had already shipped; same shape as
13370
+ * `name`/`version`/`description` right above it.
13371
+ */
13372
+ contextName: safeDisplayString(z.string().min(1)).optional(),
13269
13373
  /**
13270
13374
  * The deploying organization's own display name — distinct from {@link contextName}.
13271
13375
  * `contextName` is the blueprint author's fixed identity for the app (the same for every
@@ -13273,8 +13377,13 @@ var BlueprintSchema = z.object({
13273
13377
  * deployer-supplied `${{ inputs.x }}` value rather than hardcoded. Used for branding on
13274
13378
  * platform-sent correspondence (e.g. invite emails) alongside `contextName` — not a
13275
13379
  * replacement for it. See {@link companyNameOf}.
13380
+ *
13381
+ * `safeDisplayString`: not interpolated into any CLI plan-preview line today (confirmed —
13382
+ * `loader.ts` never reads `companyName`), but it IS forwarded to the platform for the
13383
+ * invite-email correspondence described above, a second class of human-read surface this
13384
+ * value reaches. Wrapped for the same reason as every sibling scalar identity field here.
13276
13385
  */
13277
- companyName: z.string().min(1).optional(),
13386
+ companyName: safeDisplayString(z.string().min(1)).optional(),
13278
13387
  schemas: z.array(BlueprintSchemaSchema).default([]),
13279
13388
  accessProfile: BlueprintAccessProfileSchema,
13280
13389
  servicePrincipal: BlueprintServicePrincipalSchema,
@@ -13351,9 +13460,16 @@ var BlueprintSchema = z.object({
13351
13460
  * context's name and needs a broader scope — see `@vectros-ai/cli`'s own notes.)
13352
13461
  */
13353
13462
  identityProjectionClaims: z.array(
13354
- z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
13355
- message: `an identityProjectionClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
13356
- }).refine((name) => name.trim().length > 0, {
13463
+ // safeDisplayString: rendered raw into buildPlan's context step (`identity projection:
13464
+ // [...]`) — same rendered-plan-preview shape as `capturedClaims` (its structural pair,
13465
+ // and fixed alongside it originally), missed here in the first pass; same ordering rule
13466
+ // applies (safeDisplayString's own `.refine` before the blank-name `.refine` below,
13467
+ // both after `.max` since `.refine` returns a ZodEffects with no `.max`).
13468
+ safeDisplayString(
13469
+ z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
13470
+ message: `an identityProjectionClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
13471
+ })
13472
+ ).refine((name) => name.trim().length > 0, {
13357
13473
  message: "an identityProjectionClaims entry must be a non-blank claim name"
13358
13474
  })
13359
13475
  ).max(MAX_CAPTURED_CLAIMS, {
@@ -15568,6 +15684,9 @@ function validateBaseUrl(rawUrl, opts = {}) {
15568
15684
  if (host === ALLOWED_HOST_APEX || host.endsWith(ALLOWED_HOST_SUFFIX)) {
15569
15685
  return rawUrl;
15570
15686
  }
15687
+ if ((opts.additionalAllowedHosts ?? []).some((h) => host === h.toLowerCase())) {
15688
+ return rawUrl;
15689
+ }
15571
15690
  throw new InvalidBaseUrlError(
15572
15691
  `Refusing base URL host "${url.host}" \u2014 not an official Vectros host (expected vectros.ai or a *.vectros.ai subdomain). Set ${INSECURE_BASE_URL_ENV}=1 to override (e.g. for a trusted local proxy); never point the CLI at an untrusted host while authenticated.`
15573
15692
  );
@@ -15755,6 +15874,9 @@ var PER_ENV = {
15755
15874
  staging: { domainPrefix: "vectros-developer-staging", region: "us-east-1" },
15756
15875
  production: { domainPrefix: "vectros-developer", region: "us-east-1" }
15757
15876
  };
15877
+ var COGNITO_ALLOWED_HOSTS = Object.values(PER_ENV).map(
15878
+ (p) => `${p.domainPrefix}.auth.${p.region}.amazoncognito.com`
15879
+ );
15758
15880
  var BAKED_CLIENT_IDS = {
15759
15881
  staging: "61hjvm25kkjb1eei8t2d3jkcl8",
15760
15882
  // validated against deployed staging hosted UI (2026-06-05)
@@ -15769,13 +15891,30 @@ var MissingCognitoClientIdError = class extends Error {
15769
15891
  this.name = "MissingCognitoClientIdError";
15770
15892
  }
15771
15893
  };
15894
+ var REGION_SHAPE = /^[a-z]{2}(?:-[a-z]+)+-\d+$/;
15895
+ var InvalidCognitoRegionError = class extends Error {
15896
+ constructor(region) {
15897
+ super(
15898
+ `Invalid VECTROS_COGNITO_REGION ${JSON.stringify(region)} \u2014 expected an AWS region shape (e.g. "us-east-1").`
15899
+ );
15900
+ this.name = "InvalidCognitoRegionError";
15901
+ }
15902
+ };
15772
15903
  function resolveCognitoConfig(env) {
15773
15904
  const per = PER_ENV[env];
15774
- const region = process.env.VECTROS_COGNITO_REGION ?? per.region;
15775
- const domain = process.env.VECTROS_COGNITO_DOMAIN ?? `https://${per.domainPrefix}.auth.${region}.amazoncognito.com`;
15905
+ const domainOverride = process.env.VECTROS_COGNITO_DOMAIN;
15906
+ const regionOverride = process.env.VECTROS_COGNITO_REGION;
15907
+ if (domainOverride === void 0 && regionOverride !== void 0 && !REGION_SHAPE.test(regionOverride)) {
15908
+ throw new InvalidCognitoRegionError(regionOverride);
15909
+ }
15910
+ const region = regionOverride ?? per.region;
15911
+ const rawDomain = domainOverride ?? `https://${per.domainPrefix}.auth.${region}.amazoncognito.com`;
15912
+ const domain = validateBaseUrl(rawDomain, {
15913
+ additionalAllowedHosts: COGNITO_ALLOWED_HOSTS
15914
+ }).replace(/\/$/, "");
15776
15915
  const clientId = process.env.VECTROS_COGNITO_CLIENT_ID ?? BAKED_CLIENT_IDS[env];
15777
15916
  if (!clientId) throw new MissingCognitoClientIdError(env);
15778
- return { domain: domain.replace(/\/$/, ""), clientId, region };
15917
+ return { domain, clientId, region };
15779
15918
  }
15780
15919
  var LOOPBACK_PORTS = [8976, 8977, 8978];
15781
15920
  var OAUTH_SCOPES = ["openid", "email", "profile"];
@@ -16573,7 +16712,7 @@ async function withAuth(opts, deps, body) {
16573
16712
  log
16574
16713
  });
16575
16714
  } catch (err) {
16576
- if (err instanceof MissingCredentialError) {
16715
+ if (err instanceof MissingCredentialError || err instanceof InvalidBaseUrlError || err instanceof InvalidCognitoRegionError) {
16577
16716
  process.stderr.write(` \u2716 ${err.message}
16578
16717
  `);
16579
16718
  return 2;
@@ -16660,15 +16799,42 @@ var SCRIPT_CONFLICT_RETRY_DELAY_MS = 750;
16660
16799
  function isVersionConflict(err) {
16661
16800
  return typeof err === "object" && err !== null && err.statusCode === 409;
16662
16801
  }
16663
- async function readLatestScriptVersion(ctxClient, name) {
16664
- const versions = await drainPages(
16665
- (startFrom) => ctxClient.scripts.listScripts({ name, startFrom, limit: PAGE_LIMIT })
16802
+ function isForbidden(err) {
16803
+ return typeof err === "object" && err !== null && err.statusCode === 403;
16804
+ }
16805
+ function withAdoptionContext(err, name) {
16806
+ if (!(err instanceof Error) || !isForbidden(err)) return err;
16807
+ return new Error(
16808
+ `${err.message}
16809
+
16810
+ (This push was adopting '${name}' into this blueprint's management, via --allow-script-adoption \u2014 this CLI's own ownership check cleared it, but the platform refused the push itself. The platform requires EITHER the credential pushing this scoped-token bootstrap to hold 'scripts:c', OR \u2014 if '${name}' already has an unmarked version \u2014 that this same apply is running as the SAME caller identity that originally pushed it. An account OWNER always resolves to the same identity and can adopt any unmarked name; a Dev Portal SUB-USER can only adopt a name they themselves hand-pushed. If neither applies here, ask the original pusher to run this apply, push the account owner's own login first, or push it directly via 'vectros scripts push'.)`
16666
16811
  );
16667
- return versions.reduce((best, row) => {
16668
- if (row.scriptVersion === void 0) return best;
16669
- if (best === void 0 || best.scriptVersion === void 0) return row;
16670
- return row.scriptVersion > best.scriptVersion ? row : best;
16671
- }, void 0);
16812
+ }
16813
+ function isScriptNameNotFound(err) {
16814
+ return typeof err === "object" && err !== null && err.statusCode === 404;
16815
+ }
16816
+ function isScriptPageEnvelope(result) {
16817
+ return "data" in result;
16818
+ }
16819
+ async function readLatestScriptVersion(ctxClient, name) {
16820
+ let result;
16821
+ try {
16822
+ result = await ctxClient.scripts.listScripts({ name, latest: true, includeSource: true });
16823
+ } catch (err) {
16824
+ if (isScriptNameNotFound(err)) return void 0;
16825
+ throw err;
16826
+ }
16827
+ if (result == null) {
16828
+ throw new Error(
16829
+ `Script '${name}': a bounded '?latest=true' read returned an empty response body instead of a version object. This is a platform/SDK contract change this code does not understand \u2014 refusing to guess whether the name is unclaimed or a version was silently omitted.`
16830
+ );
16831
+ }
16832
+ if (isScriptPageEnvelope(result)) {
16833
+ throw new Error(
16834
+ `Script '${name}': a bounded '?latest=true' read unexpectedly returned a page envelope (a \`data\` array) instead of a single version. This is a platform/SDK contract change this code does not understand \u2014 refusing to guess which row is actually current.`
16835
+ );
16836
+ }
16837
+ return result;
16672
16838
  }
16673
16839
  function requireScriptId(row, name) {
16674
16840
  if (!row.id) {
@@ -16679,8 +16845,34 @@ function requireScriptId(row, name) {
16679
16845
  function scriptMatches(stored, declared) {
16680
16846
  return stored.source === declared.source && (stored.declaredInputContract ?? void 0) === declared.declaredInputContract;
16681
16847
  }
16848
+ var ScriptOwnershipConflictError = class extends Error {
16849
+ constructor(name, owner, blueprintName) {
16850
+ super(
16851
+ `Script '${name}' is already provisioned by a different blueprint ('${owner}', this blueprint is '${blueprintName}') \u2014 refusing to push over it. No script version is pushed (anything already provisioned earlier in this same apply is unaffected). If you intended to replace that blueprint's script, do it under ITS name; if two blueprints legitimately need distinct code, give this one a different script name.`
16852
+ );
16853
+ this.name = "ScriptOwnershipConflictError";
16854
+ }
16855
+ };
16856
+ var ScriptAdoptionNotAllowedError = class extends Error {
16857
+ constructor(name) {
16858
+ super(
16859
+ `Script '${name}' already exists in this context and carries no blueprint provenance marker \u2014 it is either hand-authored or was pushed before this blueprint managed it. Refusing to push a new version onto it without an explicit opt-in: if a pre-existing trigger rule references '${name}' via 'version: latest', that rule's own operator-authored grant would run this blueprint's code from its very next firing \u2014 the blueprint supplying the code, someone else's rule supplying the authority. No script version is pushed (anything already provisioned earlier in this same apply is unaffected). Re-run with --allow-script-adoption once you've confirmed this blueprint should take over '${name}' going forward.`
16860
+ );
16861
+ this.name = "ScriptAdoptionNotAllowedError";
16862
+ }
16863
+ };
16864
+ function assertScriptPushOwnable(name, latest, blueprintName, allowScriptAdoption) {
16865
+ if (latest === void 0) return { adopting: false };
16866
+ const owner = latest.provisionedBy?.trim() || void 0;
16867
+ if (owner === void 0) {
16868
+ if (!allowScriptAdoption) throw new ScriptAdoptionNotAllowedError(name);
16869
+ return { adopting: true };
16870
+ }
16871
+ if (owner !== blueprintName) throw new ScriptOwnershipConflictError(name, owner, blueprintName);
16872
+ return { adopting: false };
16873
+ }
16682
16874
  async function ensureScripts(args) {
16683
- const { ctxClient, scripts, log } = args;
16875
+ const { ctxClient, scripts, blueprintName, allowScriptAdoption, log } = args;
16684
16876
  const actions = [];
16685
16877
  for (const [name, entry] of Object.entries(scripts)) {
16686
16878
  let latest = await readLatestScriptVersion(ctxClient, name);
@@ -16691,18 +16883,32 @@ async function ensureScripts(args) {
16691
16883
  id: requireScriptId(latest, name),
16692
16884
  scriptVersion: latest.scriptVersion ?? 0
16693
16885
  });
16694
- log.debug(
16695
- { component: "loader", scriptName: name, scriptVersion: latest.scriptVersion },
16696
- "script already matches the declared source \u2014 no version pushed"
16697
- );
16886
+ const foreignOwner = latest.provisionedBy?.trim() || void 0;
16887
+ if (foreignOwner !== void 0 && foreignOwner !== blueprintName) {
16888
+ log.warn(
16889
+ { component: "loader", scriptName: name, provisionedBy: foreignOwner },
16890
+ `script '${name}' already matches the declared source, but is provisioned by a DIFFERENT blueprint ('${foreignOwner}') \u2014 if this blueprint also declares a 'version: latest' trigger on this name, a future push by that other blueprint will change what that trigger runs`
16891
+ );
16892
+ } else {
16893
+ log.debug(
16894
+ { component: "loader", scriptName: name, scriptVersion: latest.scriptVersion },
16895
+ "script already matches the declared source \u2014 no version pushed"
16896
+ );
16897
+ }
16698
16898
  continue;
16699
16899
  }
16700
- const body = { name, source: entry.source, declaredInputContract: entry.declaredInputContract };
16900
+ let { adopting } = assertScriptPushOwnable(name, latest, blueprintName, allowScriptAdoption);
16901
+ const body = {
16902
+ name,
16903
+ source: entry.source,
16904
+ declaredInputContract: entry.declaredInputContract,
16905
+ provisionedBy: blueprintName
16906
+ };
16701
16907
  let pushed;
16702
16908
  try {
16703
16909
  pushed = await ctxClient.scripts.createScript(body);
16704
16910
  } catch (err) {
16705
- if (!isVersionConflict(err)) throw err;
16911
+ if (!isVersionConflict(err)) throw adopting ? withAdoptionContext(err, name) : err;
16706
16912
  log.debug(
16707
16913
  { component: "loader", scriptName: name },
16708
16914
  "concurrent push detected (409) \u2014 pausing, then re-reading the current version"
@@ -16728,10 +16934,11 @@ async function ensureScripts(args) {
16728
16934
  );
16729
16935
  continue;
16730
16936
  }
16937
+ ({ adopting } = assertScriptPushOwnable(name, latest, blueprintName, allowScriptAdoption));
16731
16938
  try {
16732
16939
  pushed = await ctxClient.scripts.createScript(body);
16733
16940
  } catch (retryErr) {
16734
- if (!isVersionConflict(retryErr)) throw retryErr;
16941
+ if (!isVersionConflict(retryErr)) throw adopting ? withAdoptionContext(retryErr, name) : retryErr;
16735
16942
  throw new Error(
16736
16943
  `Script '${name}': still conflicting (409) after a re-read and retry. Another apply is pushing this script name concurrently \u2014 re-run once it has finished.`
16737
16944
  );
@@ -16743,13 +16950,13 @@ async function ensureScripts(args) {
16743
16950
  const firstVersion = latest === void 0;
16744
16951
  actions.push({
16745
16952
  name,
16746
- action: firstVersion ? "created" : "superseded",
16953
+ action: firstVersion ? "created" : adopting ? "adopted" : "superseded",
16747
16954
  id: pushed.id,
16748
16955
  scriptVersion: pushed.scriptVersion ?? 0
16749
16956
  });
16750
16957
  log.debug(
16751
16958
  { component: "loader", scriptName: name, scriptVersion: pushed.scriptVersion },
16752
- firstVersion ? "script pushed \u2014 first version for this name in this context" : "script source changed \u2014 new version pushed over an existing name"
16959
+ firstVersion ? "script pushed \u2014 first version for this name in this context" : adopting ? "script pushed \u2014 adopted a previously-unmarked name into this blueprint" : "script source changed \u2014 new version pushed over an existing name"
16753
16960
  );
16754
16961
  }
16755
16962
  return actions;
@@ -16920,9 +17127,10 @@ function describeNamespaceMismatch(decl, existing) {
16920
17127
  return mismatches.length > 0 ? mismatches.join("; ") : null;
16921
17128
  }
16922
17129
  var LostKeyError = class extends Error {
16923
- constructor(keyName) {
17130
+ constructor(keyName, rotateAttempted = false, revokedKeyId) {
17131
+ const revokedDisplay = revokedKeyId?.map((id) => `'${id}'`).join(", ");
16924
17132
  super(
16925
- `Scoped key '${keyName}' already exists for this machine but its local copy (~/.vectros) is missing. The raw key is shown only once and cannot be re-read. Re-run with --rotate to revoke + re-mint this machine's key.`
17133
+ `Scoped key '${keyName}' already exists for this machine but its local copy (~/.vectros) is missing. The raw key is shown only once and cannot be re-read. ` + (revokedKeyId && revokedKeyId.length > 0 ? `--rotate just revoked ${revokedKeyId.length === 1 ? "key" : "keys"} ${revokedDisplay}, but this call still sees the scoped key as already existing \u2014 most likely a brief propagation delay right after the revoke, not a failed rotate. Wait a few seconds and re-run, or run \`vectros key list --context <contextId>\` to confirm ${revokedKeyId.length === 1 ? "it is" : "they are"} actually gone before retrying.` : rotateAttempted ? `--rotate ran and found no matching active key to revoke for this context/principal \u2014 either it was already revoked elsewhere or something else holds that name. Run \`vectros key list --context <contextId>\` to find the key actually blocking this, then \`vectros key revoke <id>\` it directly before re-running.` : `Re-run with --rotate to revoke + re-mint this machine's key.`)
16926
17134
  );
16927
17135
  this.keyName = keyName;
16928
17136
  this.name = "LostKeyError";
@@ -17130,6 +17338,7 @@ async function runLoader(client, input) {
17130
17338
  provisioned,
17131
17339
  readExistingKey,
17132
17340
  persistMintedKey,
17341
+ allowScriptAdoption = false,
17133
17342
  log
17134
17343
  } = input;
17135
17344
  assertNoUnresolvedIdentities(blueprint);
@@ -17280,7 +17489,13 @@ async function runLoader(client, input) {
17280
17489
  body: profileBody
17281
17490
  });
17282
17491
  log.debug({ component: "loader", principalId: `usr_${userId}`, accessProfileId: profile.id, profileAction }, "access profile ensured + reconciled to declared scopes");
17283
- const scriptActions = blueprint.scripts ? await ensureScripts({ ctxClient, scripts: blueprint.scripts, log }) : [];
17492
+ const scriptActions = blueprint.scripts ? await ensureScripts({
17493
+ ctxClient,
17494
+ scripts: blueprint.scripts,
17495
+ blueprintName: blueprint.name,
17496
+ allowScriptAdoption,
17497
+ log
17498
+ }) : [];
17284
17499
  const triggerActions = [];
17285
17500
  for (const [triggerName, entry] of Object.entries(blueprint.triggers ?? {})) {
17286
17501
  const schemaId = schemaIds[entry.firingSource.schemaName];
@@ -17391,9 +17606,45 @@ async function runLoader(client, input) {
17391
17606
  if (existing?.keyId) {
17392
17607
  await ctxClient.auth.revokeScopedKey({ keyId: existing.keyId });
17393
17608
  log.debug({ component: "loader", keyId: existing.keyId }, "revoked prior key for rotate");
17394
- revokedKeyId = existing.keyId;
17609
+ revokedKeyId = [existing.keyId];
17610
+ } else {
17611
+ let activeKeys;
17612
+ try {
17613
+ activeKeys = await drainPages(() => ctxClient.auth.listScopedKeys());
17614
+ } catch (err) {
17615
+ throw new Error(
17616
+ `--rotate could not check for a server-side key to revoke, and no local record of one exists on this machine: ${err instanceof Error ? err.message : String(err)}. Run \`vectros key list --context ${blueprint.contextId}\` and \`vectros key revoke <id>\` manually to find and remove the key blocking this, then re-run without --rotate.`,
17617
+ { cause: err }
17618
+ );
17619
+ }
17620
+ const matches = activeKeys.filter(
17621
+ (k) => k.contextId === blueprint.contextId && k.userId === userId && k.keyName === keyName && k.status === "active" && k.keyId
17622
+ );
17623
+ if (matches.length > 1) {
17624
+ log.warn(
17625
+ { component: "loader", keyName, contextId: blueprint.contextId, count: matches.length },
17626
+ "rotate: more than one active key matched this context/user/name tuple server-side \u2014 revoking all of them"
17627
+ );
17628
+ }
17629
+ if (matches.length > 0) {
17630
+ const revokedIds = [];
17631
+ for (const match of matches) {
17632
+ await ctxClient.auth.revokeScopedKey({ keyId: match.keyId });
17633
+ revokedIds.push(match.keyId);
17634
+ }
17635
+ log.debug(
17636
+ { component: "loader", keyIds: revokedIds },
17637
+ "revoked prior key(s) for rotate \u2014 resolved server-side, no local record existed"
17638
+ );
17639
+ revokedKeyId = revokedIds;
17640
+ } else {
17641
+ log.warn(
17642
+ { component: "loader", keyName, contextId: blueprint.contextId },
17643
+ "rotate: no local record and no matching active key found server-side \u2014 nothing to revoke"
17644
+ );
17645
+ }
17395
17646
  }
17396
- keyAction = "rotated";
17647
+ if (revokedKeyId) keyAction = "rotated";
17397
17648
  }
17398
17649
  const keyResp = await ctxClient.auth.createScopedKey({ keyName, tenantId, contextId: blueprint.contextId, userId });
17399
17650
  let rawKey;
@@ -17408,7 +17659,7 @@ async function runLoader(client, input) {
17408
17659
  } else {
17409
17660
  const existing = await readExistingKey(blueprint.contextId);
17410
17661
  if (!existing) {
17411
- throw new LostKeyError(keyName);
17662
+ throw new LostKeyError(keyName, rotate, revokedKeyId);
17412
17663
  }
17413
17664
  rawKey = existing.rawKey;
17414
17665
  keyId = existing.keyId;
@@ -17521,6 +17772,10 @@ async function runLoader(client, input) {
17521
17772
  seedIndexFailures
17522
17773
  };
17523
17774
  }
17775
+ function sanitizeForPlanDisplay(value, maxLen = 200) {
17776
+ const stripped = value.replace(/[\x00-\x1f\x7f]/g, "");
17777
+ return stripped.length > maxLen ? `${stripped.slice(0, maxLen)}\u2026` : stripped;
17778
+ }
17524
17779
  function renderPlanDataScope(ds) {
17525
17780
  const entries = Object.entries(ds ?? {});
17526
17781
  if (entries.length === 0) return "(none \u2014 unconstrained)";
@@ -17547,11 +17802,14 @@ function buildPlan(blueprint, opts = {}) {
17547
17802
  });
17548
17803
  }
17549
17804
  for (const iss of blueprint.issuers ?? []) {
17550
- const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
17551
- const captured = iss.capturedClaims?.length ? `, captured claims: [${iss.capturedClaims.join(", ")}]` : "";
17805
+ const issuerUrl = sanitizeForPlanDisplay(iss.issuer);
17806
+ const jwksNote = `, jwksUri: ${sanitizeForPlanDisplay(iss.jwksUri)}`;
17807
+ const userinfoNote = iss.userinfoUri ? `, userinfoUri: ${sanitizeForPlanDisplay(iss.userinfoUri)}` : "";
17808
+ const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: [${iss.selfSignupPolicies.map((p) => `${p.signupType}\u2192${p.roleId}`).join(", ")}]` : "";
17809
+ const captured = iss.capturedClaims?.length ? `, captured claims: [${iss.capturedClaims.map((c) => sanitizeForPlanDisplay(c)).join(", ")}]` : "";
17552
17810
  steps.push({
17553
17811
  kind: "issuer",
17554
- summary: `issuer '${iss.issuerId}' (${iss.issuer} \u2192 context '${iss.contextId}'${selfSignup}${captured}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
17812
+ summary: `issuer '${iss.issuerId}' (${issuerUrl} \u2192 context '${iss.contextId}'${jwksNote}${userinfoNote}${selfSignup}${captured}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
17555
17813
  });
17556
17814
  }
17557
17815
  for (const s of blueprint.schemas) {
@@ -17672,9 +17930,11 @@ function renderPlan(blueprint, steps) {
17672
17930
  ` immutable version and every earlier one persists, which is also how a 'latest' rule`,
17673
17931
  ` picks the change up. And scripts are NOT reconciled by absence: a script this blueprint`,
17674
17932
  ` previously shipped and no longer declares is LEFT IN PLACE, including every stored`,
17675
- ` version. A script row carries no provisioning marker, so an apply cannot tell one it`,
17676
- ` created from one you pushed by hand \u2014 and a wrong delete is unrecoverable and would`,
17677
- ` break any trigger still referencing it. Remove one deliberately with`,
17933
+ ` version. Each version does carry this blueprint's own provenance marker, and a push`,
17934
+ ` ONTO an already-marked-or-existing name is gated on it (see 'script' steps above) \u2014 but`,
17935
+ ` that gate has no bearing on DELETION: there is no second, principal-shaped factor to`,
17936
+ ` pair the marker with the way trigger rows have one, so a wrong delete stays unrecoverable`,
17937
+ ` and would break any trigger still referencing it. Remove one deliberately with`,
17678
17938
  ` 'vectros scripts delete', after 'vectros scripts list' shows you what is there.`
17679
17939
  );
17680
17940
  if (steps.some((s) => s.kind === "issuer")) {
@@ -17801,19 +18061,37 @@ import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, scryptSy
17801
18061
  // src/os-keychain.ts
17802
18062
  var KEYCHAIN_SERVICE = "vectros-cli";
17803
18063
  var PROBE_ACCOUNT = "__vectros_probe__";
17804
- async function loadKeychainBinding() {
18064
+ async function loadKeychainBinding(tryLoad = () => import("@napi-rs/keyring")) {
17805
18065
  try {
17806
- const mod = await import("@napi-rs/keyring");
18066
+ const mod = await tryLoad();
17807
18067
  if (typeof mod?.Entry === "function") return { binding: mod };
17808
- return { reason: "the @napi-rs/keyring package loaded but exposes no Entry \u2014 expected on no platform" };
18068
+ return {
18069
+ reason: "the @napi-rs/keyring package loaded but exposes no Entry \u2014 expected on no platform",
18070
+ installAccident: true
18071
+ };
17809
18072
  } catch (err) {
17810
- return { reason: loadFailureReason(err) };
18073
+ return { reason: loadFailureReason(err), installAccident: isInstallAccident(err) };
17811
18074
  }
17812
18075
  }
17813
- function loadFailureReason(err) {
18076
+ var PREBUILT_PLATFORMS = {
18077
+ darwin: ["arm64", "x64"],
18078
+ freebsd: ["x64"],
18079
+ linux: ["arm", "arm64", "riscv64", "x64"],
18080
+ win32: ["arm64", "ia32", "x64"]
18081
+ };
18082
+ function prebuiltShouldExist(platform = process.platform, arch = process.arch) {
18083
+ return PREBUILT_PLATFORMS[platform]?.includes(arch) ?? false;
18084
+ }
18085
+ function isInstallAccident(_err, platform = process.platform, arch = process.arch) {
18086
+ return prebuiltShouldExist(platform, arch);
18087
+ }
18088
+ function loadFailureReason(err, platform = process.platform, arch = process.arch) {
18089
+ if (!prebuiltShouldExist(platform, arch)) {
18090
+ return "the optional @napi-rs/keyring package is not installed for this platform";
18091
+ }
17814
18092
  const code = err.code;
17815
18093
  if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
17816
- return "the optional @napi-rs/keyring package is not installed for this platform";
18094
+ return `the optional @napi-rs/keyring package is missing even though a prebuilt exists for this platform (${platform}/${arch}) \u2014 most likely skipped during install`;
17817
18095
  }
17818
18096
  return `the optional @napi-rs/keyring package is installed but would not load (${describeErr(err)})`;
17819
18097
  }
@@ -17937,8 +18215,8 @@ var OsKeychainSecretStore = class {
17937
18215
  * deleteCredential() -> false
17938
18216
  * => deleteSecret() -> false. The gate holds; `remove` refuses.
17939
18217
  *
17940
- * Reproduce with the recipe in GOTCHAS `gotcha-locked-credential-store-is-present
17941
- * -but-inert`. CI cannot pin this half — it unlocks the collection so the rest of
18218
+ * Reproduce by locking the credential store's collection while the CLI still holds a
18219
+ * cached handle to it. CI cannot pin this half — it unlocks the collection so the rest of
17942
18220
  * the suite can use it — so the evidence lives here rather than in a test.
17943
18221
  *
17944
18222
  * The WINDOWS half of the same contract was assumed for seven review rounds and
@@ -18380,7 +18658,11 @@ var KEYRING_BACKENDS = ["auto", "file"];
18380
18658
  async function resolveSecretStore(deps = {}) {
18381
18659
  const platform = deps.platform ?? process.platform;
18382
18660
  const env = deps.env ?? process.env;
18383
- const file = (reason2) => ({ kind: "crypto-file", reason: reason2 });
18661
+ const file = (reason2, installAccident2) => ({
18662
+ kind: "crypto-file",
18663
+ reason: reason2,
18664
+ ...installAccident2 ? { installAccident: installAccident2 } : {}
18665
+ });
18384
18666
  const requested = env[KEYRING_BACKEND_ENV]?.trim().toLowerCase();
18385
18667
  if (requested !== void 0 && requested !== "") {
18386
18668
  if (!KEYRING_BACKENDS.includes(requested)) {
@@ -18397,9 +18679,9 @@ async function resolveSecretStore(deps = {}) {
18397
18679
  return file("the macOS Keychain can prompt on unattended reads, which would break MCP startup");
18398
18680
  }
18399
18681
  const loadBinding0 = deps.loadBinding ?? loadKeychainBinding;
18400
- const { binding, reason } = await loadBinding0();
18682
+ const { binding, reason, installAccident } = await loadBinding0();
18401
18683
  if (!binding) {
18402
- return file(reason ?? "the optional @napi-rs/keyring package is not usable on this platform");
18684
+ return file(reason ?? "the optional @napi-rs/keyring package is not usable on this platform", installAccident);
18403
18685
  }
18404
18686
  const probe = deps.probe ?? probeKeychain;
18405
18687
  if (!probe(binding)) {
@@ -18428,6 +18710,9 @@ var Keyring = class _Keyring {
18428
18710
  stores;
18429
18711
  /** Where NEW secrets go, and why — surfaced by `keyring doctor`. */
18430
18712
  backend;
18713
+ warn;
18714
+ /** Fires at most once per instance — see {@link storeSecretSomewhere}. */
18715
+ installAccidentWarned = false;
18431
18716
  /**
18432
18717
  * Construct against EXPLICIT stores. The file store defaults to a
18433
18718
  * {@link CryptoFileSecretStore} over `dir` — no native binding, nothing outside
@@ -18442,6 +18727,8 @@ var Keyring = class _Keyring {
18442
18727
  "os-keychain": deps.keychainStore
18443
18728
  };
18444
18729
  this.backend = deps.selection ?? { kind: "crypto-file" };
18730
+ this.warn = deps.warn ?? ((msg) => process.stderr.write(`${msg}
18731
+ `));
18445
18732
  }
18446
18733
  /**
18447
18734
  * The keyring a command should use: the stores resolved for THIS machine per
@@ -18452,7 +18739,7 @@ var Keyring = class _Keyring {
18452
18739
  static async open(deps = {}) {
18453
18740
  if (deps.fileStore ?? deps.keychainStore ?? deps.selection) return new _Keyring(deps);
18454
18741
  const { keychain, ...selection } = await resolveSecretStore(deps);
18455
- return new _Keyring({ dir: deps.dir, keychainStore: keychain, selection });
18742
+ return new _Keyring({ dir: deps.dir, keychainStore: keychain, selection, warn: deps.warn });
18456
18743
  }
18457
18744
  /**
18458
18745
  * The store holding this entry's secret, or undefined when the stamp does not say
@@ -18569,6 +18856,28 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
18569
18856
  }
18570
18857
  return { landedIn, stranded: swept ? void 0 : prior, vetoedBy };
18571
18858
  }
18859
+ /**
18860
+ * Warn, at most once per instance, when this machine is about to write a secret to
18861
+ * the file store because of an INSTALL ACCIDENT — not because of an ordinary,
18862
+ * nothing-to-fix absence (unsupported platform, `VECTROS_KEYRING_BACKEND=file`,
18863
+ * macOS's own design choice, or "loaded fine but no OS credential store answers").
18864
+ *
18865
+ * Before this, the CLI degraded to the file store with ZERO signal at write
18866
+ * time, indistinguishable from every OTHER reason a machine ends up there — a
18867
+ * real `@napi-rs/keyring` install failure looked identical to a platform that
18868
+ * was never going to have one, unless you separately ran `keyring doctor`.
18869
+ * This is the signal that was missing, at the moment it matters (a real
18870
+ * secret is about to land somewhere weaker than intended), naming the one
18871
+ * remedy that actually applies to THIS case and no other.
18872
+ */
18873
+ warnOnceIfInstallAccident() {
18874
+ if (this.installAccidentWarned) return;
18875
+ if (this.backend.kind !== "crypto-file" || !this.backend.installAccident) return;
18876
+ this.installAccidentWarned = true;
18877
+ this.warn(
18878
+ `WARNING: storing this credential in the encrypted file store, not your OS credential store \u2014 ${this.backend.reason ?? "the optional @napi-rs/keyring package failed to load"}. This may be an install accident rather than an unsupported platform: try reinstalling (npm install -g @vectros-ai/cli, or add @napi-rs/keyring as a direct dependency alongside it) to restore OS-credential-store protection. Run \`vectros keyring doctor\` for the full picture.`
18879
+ );
18880
+ }
18572
18881
  /**
18573
18882
  * Persist a secret, preferring this machine's chosen store but NEVER losing the
18574
18883
  * secret if that store refuses. Returns where it actually landed.
@@ -18581,8 +18890,15 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
18581
18890
  * credential and leave it orphaned server-side with no local record of its
18582
18891
  * `keyId`. Degrading to the encrypted file is strictly better than that, and the
18583
18892
  * entry's stamp keeps the result honest rather than silently wrong.
18893
+ *
18894
+ * `warnIfDegraded` defaults to true and exists ONLY for {@link probeSecretStore}
18895
+ * to suppress with `false`: the once-per-instance install-accident warning is a
18896
+ * scarce resource (fires at most once per `Keyring`), and the probe's marker
18897
+ * value is never the credential the warning describes. Spending it on the probe
18898
+ * left the REAL write — the very next call, same instance — silent.
18584
18899
  */
18585
- async storeSecretSomewhere(ref, secret) {
18900
+ async storeSecretSomewhere(ref, secret, { warnIfDegraded = true } = {}) {
18901
+ if (warnIfDegraded) this.warnOnceIfInstallAccident();
18586
18902
  const preferred = this.preferred;
18587
18903
  if (preferred) {
18588
18904
  try {
@@ -18634,7 +18950,9 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
18634
18950
  async probeSecretStore() {
18635
18951
  const fileStore = this.stores["crypto-file"];
18636
18952
  const dirWasPresent = fileStore instanceof CryptoFileSecretStore ? await fileStore.secretsDirPresent() : true;
18637
- const landedIn = await this.storeSecretSomewhere(PROBE_SECRET_REF, PROBE_SECRET_VALUE);
18953
+ const landedIn = await this.storeSecretSomewhere(PROBE_SECRET_REF, PROBE_SECRET_VALUE, {
18954
+ warnIfDegraded: false
18955
+ });
18638
18956
  await this.stores[landedIn]?.deleteSecret(PROBE_SECRET_REF).catch(() => {
18639
18957
  });
18640
18958
  if (!dirWasPresent && fileStore instanceof CryptoFileSecretStore) {
@@ -18935,7 +19253,12 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
18935
19253
  async readSecret(entry) {
18936
19254
  const from = storeKindOf(entry);
18937
19255
  if (from === void 0) return void 0;
18938
- const secret = await this.stores[from]?.getSecret(entry.secretRef);
19256
+ const store = this.stores[from];
19257
+ if (!store) {
19258
+ this.warnOnceIfInstallAccident();
19259
+ return void 0;
19260
+ }
19261
+ const secret = await store.getSecret(entry.secretRef);
18939
19262
  if (secret === void 0) return void 0;
18940
19263
  if (from !== this.backend.kind) await this.migrateSecret(entry, from, secret);
18941
19264
  return secret;
@@ -19656,7 +19979,7 @@ function nowIso() {
19656
19979
  }
19657
19980
  function warnReplacingAlias(existing, alias, revokedKeyId) {
19658
19981
  if (!existing) return;
19659
- const stillLive = existing.keyId !== void 0 && existing.keyId !== revokedKeyId;
19982
+ const stillLive = existing.keyId !== void 0 && !(revokedKeyId ?? []).includes(existing.keyId);
19660
19983
  process.stdout.write(
19661
19984
  `
19662
19985
  \u26A0 Replacing keyring entry '${alias}' (was ${existing.principalId || "<unknown principal>"} in '${existing.context}' @ ${existing.env}, key ${existing.keyId}). Pass --alias to keep both.
@@ -19667,13 +19990,14 @@ function warnReplacingAlias(existing, alias, revokedKeyId) {
19667
19990
  }
19668
19991
  async function warnOtherAliasesNaming(keyring, revokedKeyId, exceptAlias) {
19669
19992
  const affected = (await keyring.list()).filter(
19670
- (e) => e.alias !== exceptAlias && e.keyId === revokedKeyId
19993
+ (e) => e.alias !== exceptAlias && e.keyId !== void 0 && revokedKeyId.includes(e.keyId)
19671
19994
  );
19672
19995
  if (affected.length === 0) return;
19673
19996
  const names = affected.map((e) => `'${e.alias}'`).join(", ");
19997
+ const revokedDisplay = revokedKeyId.length === 1 ? `Key ${revokedKeyId[0]}` : `Keys ${revokedKeyId.join(", ")}`;
19674
19998
  process.stderr.write(
19675
19999
  `
19676
- \u26A0 Key ${revokedKeyId} was just revoked, but ${affected.length === 1 ? "entry" : "entries"} ${names} still
20000
+ \u26A0 ${revokedDisplay} ${revokedKeyId.length === 1 ? "was" : "were"} just revoked, but ${affected.length === 1 ? "entry" : "entries"} ${names} still
19677
20001
  ${affected.length === 1 ? "names" : "name"} it. That secret is now dead \u2014 it keeps reading fine locally and
19678
20002
  will simply stop authenticating. Re-point ${affected.length === 1 ? "it" : "them"} with
19679
20003
  \`vectros key rotate --to-keyring --alias <name>\`, or drop with \`vectros keyring remove <name>\`.
@@ -19703,7 +20027,7 @@ async function storeMintedKey(keyring, binding, secret, opts = {}) {
19703
20027
  was kept too` : `the ${noun} did not
19704
20028
  confirm the delete`;
19705
20029
  const tidyUp = vetoNoun !== void 0 ? `Clear the copies by hand from your ${vetoNoun} and your ${noun}` : `Clear it by hand from your ${noun}`;
19706
- if (opts.revokedKeyId === stranded.keyId) {
20030
+ if (opts.revokedKeyId?.includes(stranded.keyId)) {
19707
20031
  process.stderr.write(
19708
20032
  `
19709
20033
  \u26A0 The previous key for '${entry.alias}' (${stranded.keyId}) was revoked and replaced, but
@@ -19774,6 +20098,7 @@ function parseBootstrapArgs(argv) {
19774
20098
  blankSlate: false,
19775
20099
  noSeed: false,
19776
20100
  allowTenantWideNamespaces: false,
20101
+ allowScriptAdoption: false,
19777
20102
  client: "desktop",
19778
20103
  tenant: "live",
19779
20104
  help: false
@@ -19838,6 +20163,9 @@ function parseBootstrapArgs(argv) {
19838
20163
  case "--allow-tenant-wide-namespaces":
19839
20164
  args.allowTenantWideNamespaces = true;
19840
20165
  break;
20166
+ case "--allow-script-adoption":
20167
+ args.allowScriptAdoption = true;
20168
+ break;
19841
20169
  case "--tenant": {
19842
20170
  const v = argv[++i];
19843
20171
  if (v !== "test" && v !== "live") {
@@ -19866,7 +20194,7 @@ function parseBootstrapArgs(argv) {
19866
20194
  return args;
19867
20195
  }
19868
20196
  function shouldOfferSeedOptOut(args, blueprint, isTty) {
19869
- return !args.noSeed && (blueprint.seed?.length ?? 0) > 0 && !args.yes && !args.noMerge && isTty;
20197
+ return !args.noSeed && (blueprint.seed?.length ?? 0) > 0 && !args.yes && isTty;
19870
20198
  }
19871
20199
  function seedDeclined(answer) {
19872
20200
  const a = answer.trim().toLowerCase();
@@ -19932,12 +20260,16 @@ function scriptTally(items) {
19932
20260
  if (items.length === 0) return null;
19933
20261
  const created = items.filter((i) => i.action === "created");
19934
20262
  const superseded = items.filter((i) => i.action === "superseded");
19935
- const unchanged = items.length - created.length - superseded.length;
20263
+ const adopted = items.filter((i) => i.action === "adopted");
20264
+ const unchanged = items.length - created.length - superseded.length - adopted.length;
19936
20265
  const seg = [];
19937
20266
  if (created.length) seg.push(`${created.length} first version${created.length === 1 ? "" : "s"} pushed`);
19938
20267
  if (superseded.length) {
19939
20268
  seg.push(`${superseded.map((i) => `'${i.name}'`).join(", ")} superseded (a new version now current)`);
19940
20269
  }
20270
+ if (adopted.length) {
20271
+ seg.push(`${adopted.map((i) => `'${i.name}'`).join(", ")} adopted (previously unmarked, now managed by this blueprint)`);
20272
+ }
19941
20273
  if (unchanged) seg.push(`${unchanged} already current \u2014 nothing written`);
19942
20274
  return `${items.length} script${items.length === 1 ? "" : "s"} (${seg.join("; ")})`;
19943
20275
  }
@@ -20009,6 +20341,14 @@ Options:
20009
20341
  credential can actually complete the registration either way \u2014
20010
20342
  this flag doesn't change who's allowed, only whether this run
20011
20343
  tries. Never needed for a blueprint with no tenantWide namespace.
20344
+ --allow-script-adoption
20345
+ Required to push a script version onto a name that already exists in
20346
+ this context but carries no blueprint provenance marker (hand-authored,
20347
+ or pushed before this marker existed) \u2014 without it, bootstrap refuses
20348
+ before minting anything for that name, naming it. Never needed for a
20349
+ brand-new script name, or for re-applying a blueprint that already owns
20350
+ the name it's pushing to. Does NOT help if the name is already owned by
20351
+ a DIFFERENT blueprint \u2014 that is refused unconditionally, no flag past it.
20012
20352
  --tenant test|live Which tenant the minted key scopes to (default: live).
20013
20353
  'test' provisions into your isolated test tenant \u2014 a
20014
20354
  dry-run before pointing real data at the same blueprint.
@@ -20031,7 +20371,8 @@ Options:
20031
20371
  --yes, -y Skip confirmations (agent / CI path).
20032
20372
  --no-merge Still mints/reconciles \u2014 this only changes where the result goes:
20033
20373
  print the config snippet instead of merging it into your client's
20034
- config file. Not a dry run; see 'blueprint plan' for that.
20374
+ config file. Not a dry run; see 'blueprint plan' for that. Does NOT
20375
+ skip the 'Apply this plan?' confirmation \u2014 pass --yes for that.
20035
20376
  --rotate Revoke + re-mint THIS machine's key.
20036
20377
  --confirm-existing-principal
20037
20378
  Required (with --yes, or with no TTY) to mint a NEW key bound to a
@@ -20231,7 +20572,7 @@ ${renderPlan(blueprint, planSteps)}
20231
20572
  `);
20232
20573
  }
20233
20574
  }
20234
- if (!args.yes && !args.noMerge && isTty) {
20575
+ if (!args.yes && isTty) {
20235
20576
  const ans = (await promptCtx.prompt("\n Apply this plan? [y/N]: ")).trim().toLowerCase();
20236
20577
  if (ans !== "y" && ans !== "yes") {
20237
20578
  process.stdout.write(" Aborted \u2014 nothing minted.\n");
@@ -20365,6 +20706,7 @@ ${renderPlan(blueprint, planSteps)}
20365
20706
  machineId,
20366
20707
  rotate: args.rotate,
20367
20708
  skipSeed: noSeed,
20709
+ allowScriptAdoption: args.allowScriptAdoption,
20368
20710
  readExistingKey,
20369
20711
  persistMintedKey,
20370
20712
  provisioned,
@@ -21578,7 +21920,7 @@ ${list}
21578
21920
  return storeAndReport(dest, key, auth, {
21579
21921
  ...opts,
21580
21922
  principalId: `usr_${userId}`,
21581
- revokedKeyId: existing?.keyId
21923
+ revokedKeyId: existing?.keyId ? [existing.keyId] : void 0
21582
21924
  });
21583
21925
  emitMintedKey(key, opts.format);
21584
21926
  return 0;
@@ -23250,6 +23592,7 @@ async function runScriptsList(opts, deps = {}) {
23250
23592
  const rows = await drainPages(
23251
23593
  (startFrom) => client.scripts.listScripts({
23252
23594
  limit: PAGE_LIMIT,
23595
+ includeSource: true,
23253
23596
  ...opts.name ? { name: opts.name } : {},
23254
23597
  ...startFrom ? { startFrom } : {}
23255
23598
  })
@@ -23310,10 +23653,9 @@ async function runScriptsDelete(opts, deps = {}) {
23310
23653
  `
23311
23654
  \u26A0 Deleting script version '${opts.id}' is permanent \u2014 the stored source is the only copy.
23312
23655
  Other versions of the same name are unaffected.
23313
- Nothing checks whether a trigger rule still references this version. A rule pinned to it
23314
- will fail on every firing; a rule on 'latest' will silently fall back to the previous
23315
- version and keep running older code. This CLI cannot list trigger rules, so confirm
23316
- against the API or the portal before removing a version a rule may still use.
23656
+ The platform refuses this delete if a trigger rule still references this version \u2014
23657
+ directly, or via 'latest' if this is the name's current newest version \u2014 and names the
23658
+ offending rule instead of deleting. If no rule references it, the delete proceeds.
23317
23659
  And if this is the name's LATEST version, its identity is REUSED: the next push of that
23318
23660
  name recomputes the same version number and the same id, so anything holding a pinned
23319
23661
  '${opts.id}' or a name@version reference will silently resolve to different source.
@@ -23482,7 +23824,7 @@ async function runLogin(opts, deps = {}) {
23482
23824
  try {
23483
23825
  config = resolveCognitoConfig(opts.env);
23484
23826
  } catch (err) {
23485
- if (err instanceof MissingCognitoClientIdError) {
23827
+ if (err instanceof MissingCognitoClientIdError || err instanceof InvalidBaseUrlError || err instanceof InvalidCognitoRegionError) {
23486
23828
  process.stderr.write(` \u2716 ${err.message}
23487
23829
  `);
23488
23830
  return 2;
@@ -24075,6 +24417,11 @@ async function runBlueprintHarness(client, input, deps) {
24075
24417
  // recording that it happened and nothing able to restore it. Opt out rather than grow the
24076
24418
  // snapshot a leg teardown could not honour anyway.
24077
24419
  pruneTriggers: false
24420
+ // `allowScriptAdoption` is left at its default `false` — the SAME created-only contract above
24421
+ // applies here: this harness may run against a shared tenant, and adopting a pre-existing,
24422
+ // unmarked script name would start this run's teardown treating (and, worse, later deleting)
24423
+ // versions on a name it does not own. A test blueprint that needs `scripts:` should ship a
24424
+ // fresh, collision-free name, exactly as the harness already expects for schemas/roles/issuers.
24078
24425
  });
24079
24426
  log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
24080
24427
  assertions = await runAssertions(
@@ -24288,7 +24635,7 @@ async function issueOrRotateKey(client, tenantId, context, userId, keyName) {
24288
24635
  "The key already exists and its secret could not be re-minted (the prior key may not have cleared yet). Retry shortly."
24289
24636
  );
24290
24637
  }
24291
- return { key: rotated, revokedKeyId: existing?.keyId };
24638
+ return { key: rotated, revokedKeyId: existing?.keyId ? [existing.keyId] : void 0 };
24292
24639
  }
24293
24640
  async function runJoin(opts, deps = {}) {
24294
24641
  const hasRole = Boolean(opts.role?.length);
@@ -24435,9 +24782,9 @@ function emitJoined(opts) {
24435
24782
 
24436
24783
  // src/build-info.ts
24437
24784
  var BUILD_INFO = {
24438
- cli: true ? "0.21.0" : "dev",
24439
- sdk: true ? "0.43.0" : "dev",
24440
- blueprints: true ? "0.18.0" : "dev"
24785
+ cli: true ? "0.22.0" : "dev",
24786
+ sdk: true ? "0.44.0" : "dev",
24787
+ blueprints: true ? "0.19.0" : "dev"
24441
24788
  };
24442
24789
  function formatBuildInfo() {
24443
24790
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -24464,11 +24811,13 @@ function addCommonAuthOptions(cmd) {
24464
24811
  "emit machine-readable JSON instead of the human-readable view"
24465
24812
  );
24466
24813
  }
24814
+ var TENANT_FLAG_INVALID = /* @__PURE__ */ Symbol("parseTenantFlag:invalid");
24467
24815
  function parseTenantFlag(raw) {
24468
24816
  if (raw !== void 0 && raw !== "test" && raw !== "live") {
24469
24817
  process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${raw}')
24470
24818
  `);
24471
- process.exit(2);
24819
+ process.exitCode = 2;
24820
+ return TENANT_FLAG_INVALID;
24472
24821
  }
24473
24822
  return raw;
24474
24823
  }
@@ -24489,14 +24838,14 @@ function buildProgram(deps = {}) {
24489
24838
  );
24490
24839
  const login = program.command("login").description("Sign in via your browser (Cognito PKCE) and store a session.").addOption(new Option("--env <env>", "target environment").choices(["staging", "production"]).default("production").hideHelp()).option("--no-browser", "print the sign-in URL instead of opening a browser (headless/CI/WSL)");
24491
24840
  login.action(async (flags) => {
24492
- process.exit(await runLogin({ env: flags.env, json: false, noBrowser: flags.browser === false }));
24841
+ process.exitCode = await runLogin({ env: flags.env, json: false, noBrowser: flags.browser === false });
24493
24842
  });
24494
24843
  program.command("logout").description("Clear the stored login session.").action(async () => {
24495
- process.exit(await runLogout({ env: "production", json: false }));
24844
+ process.exitCode = await runLogout({ env: "production", json: false });
24496
24845
  });
24497
24846
  const whoami = program.command("whoami").description("Show the authenticated session (tenant, env, principal).");
24498
24847
  addCommonAuthOptions(whoami).action(async (flags) => {
24499
- process.exit(await runWhoami(toCommonOptions(flags), deps));
24848
+ process.exitCode = await runWhoami(toCommonOptions(flags), deps);
24500
24849
  });
24501
24850
  const blueprintTest = program.command("blueprint-test").description("Apply a blueprint to a live env, assert it provisioned a working credential, then tear it down.").argument("<file>", "blueprint file path (./my.yaml) or a bundled blueprint name").option("--keep", "leave the provisioned graph in place (skip teardown) for inspection").option("--tenant <tenant>", "target tenant for the run: 'test' or 'live' (default live)");
24502
24851
  addCommonAuthOptions(blueprintTest).action(
@@ -24504,14 +24853,13 @@ function buildProgram(deps = {}) {
24504
24853
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
24505
24854
  process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
24506
24855
  `);
24507
- process.exit(2);
24508
- }
24509
- process.exit(
24510
- await runBlueprintTest(
24511
- file,
24512
- { ...toCommonOptions(flags), keep: Boolean(flags.keep), tenant: flags.tenant },
24513
- deps
24514
- )
24856
+ process.exitCode = 2;
24857
+ return;
24858
+ }
24859
+ process.exitCode = await runBlueprintTest(
24860
+ file,
24861
+ { ...toCommonOptions(flags), keep: Boolean(flags.keep), tenant: flags.tenant },
24862
+ deps
24515
24863
  );
24516
24864
  }
24517
24865
  );
@@ -24532,9 +24880,7 @@ function registerAssumeCommand(program, deps) {
24532
24880
  const assume = program.command("assume").description("Re-mint your own verb token with one or more identity.<namespace> values switched (needs a role's assumable grant).").requiredOption("--context <c>", "app context to operate in").option("--scope <ns:value>", "namespace value to assume, repeatable (e.g. org:orgB)", collect, []);
24533
24881
  addCommonAuthOptions(assume).action(
24534
24882
  async (flags) => {
24535
- process.exit(
24536
- await runAssume({ ...toCommonOptions(flags), context: flags.context, scope: flags.scope ?? [] }, deps)
24537
- );
24883
+ process.exitCode = await runAssume({ ...toCommonOptions(flags), context: flags.context, scope: flags.scope ?? [] }, deps);
24538
24884
  }
24539
24885
  );
24540
24886
  }
@@ -24548,23 +24894,22 @@ function registerJoinCommand(program, deps) {
24548
24894
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
24549
24895
  process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
24550
24896
  `);
24551
- process.exit(2);
24897
+ process.exitCode = 2;
24898
+ return;
24552
24899
  }
24553
- process.exit(
24554
- await runJoin(
24555
- {
24556
- ...toCommonOptions(flags),
24557
- contextArg,
24558
- role: flags.role,
24559
- actions: flags.actions,
24560
- as: flags.as,
24561
- scope: flags.scope,
24562
- alias: flags.alias,
24563
- tenant: flags.tenant,
24564
- activate: flags.activate
24565
- },
24566
- deps
24567
- )
24900
+ process.exitCode = await runJoin(
24901
+ {
24902
+ ...toCommonOptions(flags),
24903
+ contextArg,
24904
+ role: flags.role,
24905
+ actions: flags.actions,
24906
+ as: flags.as,
24907
+ scope: flags.scope,
24908
+ alias: flags.alias,
24909
+ tenant: flags.tenant,
24910
+ activate: flags.activate
24911
+ },
24912
+ deps
24568
24913
  );
24569
24914
  }
24570
24915
  );
@@ -24573,29 +24918,27 @@ function registerKeyringCommands(program) {
24573
24918
  const KEYRING_SHOW_FORMATS = ["human", "env", "raw"];
24574
24919
  const keyring = program.command("keyring").description("Inspect and manage the local multi-principal keyring.");
24575
24920
  keyring.command("list").description("List keyring entries and which one is active.").option("--json", "emit machine-readable JSON").action(async (flags) => {
24576
- process.exit(await runKeyringList({ json: Boolean(flags.json) }));
24921
+ process.exitCode = await runKeyringList({ json: Boolean(flags.json) });
24577
24922
  });
24578
24923
  keyring.command("use").description("Make an entry the active identity.").argument("<alias>", "the keyring entry to activate").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
24579
- process.exit(await runKeyringUse(alias, { json: Boolean(flags.json) }));
24924
+ process.exitCode = await runKeyringUse(alias, { json: Boolean(flags.json) });
24580
24925
  });
24581
24926
  keyring.command("show").description("Reveal the active (or a named) entry\u2019s key. `--format raw` prints just the secret \u2014 the form scripts and the MCP server resolve a key through.").option("--alias <name>", "show this entry instead of the active one").addOption(new Option("--format <fmt>", "how to emit the key").choices(KEYRING_SHOW_FORMATS).default("human")).action(async (flags) => {
24582
- process.exit(await runKeyringShow({ alias: flags.alias, format: flags.format }));
24927
+ process.exitCode = await runKeyringShow({ alias: flags.alias, format: flags.format });
24583
24928
  });
24584
24929
  keyring.command("doctor").description("Health-check the keyring: which entry is active, whether each secret is readable, which entries hold live keys (the ones that act on real data), and which store is holding your secrets.").option("--json", "emit machine-readable JSON").action(async (flags) => {
24585
- process.exit(await runKeyringDoctor({ json: Boolean(flags.json) }));
24930
+ process.exitCode = await runKeyringDoctor({ json: Boolean(flags.json) });
24586
24931
  });
24587
24932
  keyring.command("prune").description("Drop orphaned entries that have no stored secret. Entries whose secret will not decrypt, or is held in the OS credential store (which cannot report absence), are reported and never removed.").option("--dry-run", "report what would be removed without removing anything").option("--force", "skip the interactive confirmation (required when stdin is not a TTY)").option("--json", "emit machine-readable JSON").action(async (flags) => {
24588
- process.exit(
24589
- await runKeyringPrune({ dryRun: Boolean(flags.dryRun), force: Boolean(flags.force), json: Boolean(flags.json) })
24590
- );
24933
+ process.exitCode = await runKeyringPrune({ dryRun: Boolean(flags.dryRun), force: Boolean(flags.force), json: Boolean(flags.json) });
24591
24934
  });
24592
24935
  keyring.command("remove").description(
24593
24936
  "Drop an entry and its stored secret. Refuses if the secret cannot be confirmed deleted \u2014 removing the entry would strand a live key by discarding the only record of its id."
24594
24937
  ).argument("<alias>", "the keyring entry to remove").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
24595
- process.exit(await runKeyringRemove(alias, { json: Boolean(flags.json) }));
24938
+ process.exitCode = await runKeyringRemove(alias, { json: Boolean(flags.json) });
24596
24939
  });
24597
24940
  program.command("switch").description("Shortcut for `keyring use` \u2014 flip the active identity.").argument("<alias>", "the keyring entry to activate").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
24598
- process.exit(await runKeyringUse(alias, { json: Boolean(flags.json) }));
24941
+ process.exitCode = await runKeyringUse(alias, { json: Boolean(flags.json) });
24599
24942
  });
24600
24943
  }
24601
24944
  function registerContextCommands(program, deps) {
@@ -24606,18 +24949,17 @@ function registerContextCommands(program, deps) {
24606
24949
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
24607
24950
  process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
24608
24951
  `);
24609
- process.exit(2);
24952
+ process.exitCode = 2;
24953
+ return;
24610
24954
  }
24611
- process.exit(
24612
- await runContextCreate(
24613
- {
24614
- ...toCommonOptions(flags),
24615
- contextId,
24616
- name: flags.name,
24617
- tenant: flags.tenant
24618
- },
24619
- deps
24620
- )
24955
+ process.exitCode = await runContextCreate(
24956
+ {
24957
+ ...toCommonOptions(flags),
24958
+ contextId,
24959
+ name: flags.name,
24960
+ tenant: flags.tenant
24961
+ },
24962
+ deps
24621
24963
  );
24622
24964
  }
24623
24965
  );
@@ -24626,13 +24968,12 @@ function registerContextCommands(program, deps) {
24626
24968
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
24627
24969
  process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
24628
24970
  `);
24629
- process.exit(2);
24971
+ process.exitCode = 2;
24972
+ return;
24630
24973
  }
24631
- process.exit(
24632
- await runContextList(
24633
- { ...toCommonOptions(flags), mine: Boolean(flags.mine), tenant: flags.tenant },
24634
- deps
24635
- )
24974
+ process.exitCode = await runContextList(
24975
+ { ...toCommonOptions(flags), mine: Boolean(flags.mine), tenant: flags.tenant },
24976
+ deps
24636
24977
  );
24637
24978
  });
24638
24979
  const get = context.command("get").description("Show one app context.").argument("<contextId>").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
@@ -24640,13 +24981,12 @@ function registerContextCommands(program, deps) {
24640
24981
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
24641
24982
  process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
24642
24983
  `);
24643
- process.exit(2);
24984
+ process.exitCode = 2;
24985
+ return;
24644
24986
  }
24645
- process.exit(
24646
- await runContextGet(
24647
- { ...toCommonOptions(flags), contextId, tenant: flags.tenant },
24648
- deps
24649
- )
24987
+ process.exitCode = await runContextGet(
24988
+ { ...toCommonOptions(flags), contextId, tenant: flags.tenant },
24989
+ deps
24650
24990
  );
24651
24991
  });
24652
24992
  const destroy = context.command("destroy").description(
@@ -24657,18 +24997,17 @@ function registerContextCommands(program, deps) {
24657
24997
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
24658
24998
  process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
24659
24999
  `);
24660
- process.exit(2);
25000
+ process.exitCode = 2;
25001
+ return;
24661
25002
  }
24662
- process.exit(
24663
- await runContextDestroy(
24664
- {
24665
- ...toCommonOptions(flags),
24666
- contextId,
24667
- force: Boolean(flags.force),
24668
- tenant: flags.tenant
24669
- },
24670
- deps
24671
- )
25003
+ process.exitCode = await runContextDestroy(
25004
+ {
25005
+ ...toCommonOptions(flags),
25006
+ contextId,
25007
+ force: Boolean(flags.force),
25008
+ tenant: flags.tenant
25009
+ },
25010
+ deps
24672
25011
  );
24673
25012
  }
24674
25013
  );
@@ -24679,18 +25018,16 @@ function registerIssuersCommands(program, deps) {
24679
25018
  addCommonAuthOptions(list).action(
24680
25019
  async (flags) => {
24681
25020
  const tenant = parseTenantFlag(flags.tenant);
24682
- process.exit(
24683
- await runIssuersList({ ...toCommonOptions(flags), context: flags.context, tenant }, deps)
24684
- );
25021
+ if (tenant === TENANT_FLAG_INVALID) return;
25022
+ process.exitCode = await runIssuersList({ ...toCommonOptions(flags), context: flags.context, tenant }, deps);
24685
25023
  }
24686
25024
  );
24687
25025
  const get = issuers.command("get").description("Show one registered issuer.").argument("<issuerId>").requiredOption("--context <c>", "app context the issuer is registered under").option("--tenant <tenant>", "tenant to read: 'test' or 'live' (default live)");
24688
25026
  addCommonAuthOptions(get).action(
24689
25027
  async (issuerId, flags) => {
24690
25028
  const tenant = parseTenantFlag(flags.tenant);
24691
- process.exit(
24692
- await runIssuersGet({ ...toCommonOptions(flags), context: flags.context, issuerId, tenant }, deps)
24693
- );
25029
+ if (tenant === TENANT_FLAG_INVALID) return;
25030
+ process.exitCode = await runIssuersGet({ ...toCommonOptions(flags), context: flags.context, issuerId, tenant }, deps);
24694
25031
  }
24695
25032
  );
24696
25033
  const register = issuers.command("register").description("Register (idempotent by issuerId) a trusted third-party IdP issuer.").requiredOption("--context <c>", "app context an exchanged token targets").requiredOption("--issuer-id <id>", "short slug for this issuer (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--issuer <url>", "the IdP's iss claim value").requiredOption("--jwks-uri <url>", "the IdP's remote JWKS endpoint").requiredOption("--audience <aud>", "the required aud claim value (globally unique with --issuer)").option("--sub-claim <claim>", "claim carrying the subject identifier (default 'sub')").option("--email-claim <claim>", "claim carrying the subject's email (default 'email')").option("--userinfo-uri <url>", "OIDC userinfo endpoint, used as an email-resolution fallback").option(
@@ -24706,24 +25043,23 @@ function registerIssuersCommands(program, deps) {
24706
25043
  addCommonAuthOptions(register).action(
24707
25044
  async (flags) => {
24708
25045
  const tenant = parseTenantFlag(flags.tenant);
24709
- process.exit(
24710
- await runIssuersRegister(
24711
- {
24712
- ...toCommonOptions(flags),
24713
- context: flags.context,
24714
- issuerId: flags.issuerId,
24715
- issuer: flags.issuer,
24716
- jwksUri: flags.jwksUri,
24717
- audience: flags.audience,
24718
- subClaim: flags.subClaim,
24719
- emailClaim: flags.emailClaim,
24720
- userinfoUri: flags.userinfoUri,
24721
- selfSignupPolicy: flags.selfSignupPolicy,
24722
- capturedClaim: flags.capturedClaim,
24723
- tenant
24724
- },
24725
- deps
24726
- )
25046
+ if (tenant === TENANT_FLAG_INVALID) return;
25047
+ process.exitCode = await runIssuersRegister(
25048
+ {
25049
+ ...toCommonOptions(flags),
25050
+ context: flags.context,
25051
+ issuerId: flags.issuerId,
25052
+ issuer: flags.issuer,
25053
+ jwksUri: flags.jwksUri,
25054
+ audience: flags.audience,
25055
+ subClaim: flags.subClaim,
25056
+ emailClaim: flags.emailClaim,
25057
+ userinfoUri: flags.userinfoUri,
25058
+ selfSignupPolicy: flags.selfSignupPolicy,
25059
+ capturedClaim: flags.capturedClaim,
25060
+ tenant
25061
+ },
25062
+ deps
24727
25063
  );
24728
25064
  }
24729
25065
  );
@@ -24740,24 +25076,23 @@ function registerIssuersCommands(program, deps) {
24740
25076
  addCommonAuthOptions(update).action(
24741
25077
  async (issuerId, flags) => {
24742
25078
  const tenant = parseTenantFlag(flags.tenant);
24743
- process.exit(
24744
- await runIssuersUpdate(
24745
- {
24746
- ...toCommonOptions(flags),
24747
- context: flags.context,
24748
- issuerId,
24749
- subClaim: flags.subClaim,
24750
- emailClaim: flags.emailClaim,
24751
- userinfoUri: flags.userinfoUri,
24752
- status: flags.status,
24753
- selfSignupPolicy: flags.selfSignupPolicy,
24754
- clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
24755
- capturedClaim: flags.capturedClaim,
24756
- clearCapturedClaims: Boolean(flags.clearCapturedClaims),
24757
- tenant
24758
- },
24759
- deps
24760
- )
25079
+ if (tenant === TENANT_FLAG_INVALID) return;
25080
+ process.exitCode = await runIssuersUpdate(
25081
+ {
25082
+ ...toCommonOptions(flags),
25083
+ context: flags.context,
25084
+ issuerId,
25085
+ subClaim: flags.subClaim,
25086
+ emailClaim: flags.emailClaim,
25087
+ userinfoUri: flags.userinfoUri,
25088
+ status: flags.status,
25089
+ selfSignupPolicy: flags.selfSignupPolicy,
25090
+ clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
25091
+ capturedClaim: flags.capturedClaim,
25092
+ clearCapturedClaims: Boolean(flags.clearCapturedClaims),
25093
+ tenant
25094
+ },
25095
+ deps
24761
25096
  );
24762
25097
  }
24763
25098
  );
@@ -24765,11 +25100,10 @@ function registerIssuersCommands(program, deps) {
24765
25100
  addCommonAuthOptions(del).action(
24766
25101
  async (issuerId, flags) => {
24767
25102
  const tenant = parseTenantFlag(flags.tenant);
24768
- process.exit(
24769
- await runIssuersDelete(
24770
- { ...toCommonOptions(flags), context: flags.context, issuerId, force: Boolean(flags.force), tenant },
24771
- deps
24772
- )
25103
+ if (tenant === TENANT_FLAG_INVALID) return;
25104
+ process.exitCode = await runIssuersDelete(
25105
+ { ...toCommonOptions(flags), context: flags.context, issuerId, force: Boolean(flags.force), tenant },
25106
+ deps
24773
25107
  );
24774
25108
  }
24775
25109
  );
@@ -24785,33 +25119,30 @@ function registerNamespaceCommands(program, deps) {
24785
25119
  ` \u2716 --specificity-rank must be an integer between 0 and 1000000 (got '${flags.specificityRank}').
24786
25120
  `
24787
25121
  );
24788
- process.exit(2);
25122
+ process.exitCode = 2;
25123
+ return;
24789
25124
  }
24790
- process.exit(
24791
- await runNamespaceRegister(
24792
- {
24793
- ...toCommonOptions(flags),
24794
- namespace: name,
24795
- context: flags.context,
24796
- specificityRank,
24797
- entityBacked: Boolean(flags.entityBacked),
24798
- defaultSchemaId: flags.defaultSchemaId
24799
- },
24800
- deps
24801
- )
25125
+ process.exitCode = await runNamespaceRegister(
25126
+ {
25127
+ ...toCommonOptions(flags),
25128
+ namespace: name,
25129
+ context: flags.context,
25130
+ specificityRank,
25131
+ entityBacked: Boolean(flags.entityBacked),
25132
+ defaultSchemaId: flags.defaultSchemaId
25133
+ },
25134
+ deps
24802
25135
  );
24803
25136
  }
24804
25137
  );
24805
25138
  const list = namespace.command("list").description("List scope-namespace registrations \u2014 tenant-wide, or one context\u2019s own.").option("--context <c>", "list one app context\u2019s own registrations instead of the tenant-wide set");
24806
25139
  addCommonAuthOptions(list).action(async (flags) => {
24807
- process.exit(await runNamespaceList({ ...toCommonOptions(flags), context: flags.context }, deps));
25140
+ process.exitCode = await runNamespaceList({ ...toCommonOptions(flags), context: flags.context }, deps);
24808
25141
  });
24809
25142
  const get = namespace.command("get").description("Show one namespace registration \u2014 tenant-wide, or one context\u2019s own.").argument("<name>").option("--context <c>", "resolve for one app context\u2019s own registration instead of the tenant-wide one");
24810
25143
  addCommonAuthOptions(get).action(
24811
25144
  async (name, flags) => {
24812
- process.exit(
24813
- await runNamespaceGet({ ...toCommonOptions(flags), namespace: name, context: flags.context }, deps)
24814
- );
25145
+ process.exitCode = await runNamespaceGet({ ...toCommonOptions(flags), namespace: name, context: flags.context }, deps);
24815
25146
  }
24816
25147
  );
24817
25148
  }
@@ -24820,59 +25151,59 @@ function registerScriptsCommands(program, deps) {
24820
25151
  const push = scripts.command("push").description("Store a new version of a script name (always creates a version unless --if-changed).").argument("<name>", "script name: 1-64 chars of letters, digits, '_' and '-'").requiredOption("--context <c>", "app context to push into").requiredOption("--source-file <path>", "file holding the script source (max 300,000 UTF-8 bytes)").option("--input-contract <text>", "free-text description of the expected input shape (descriptive only)").option("--input-contract-file <path>", "read the input contract from a file instead").option("--if-changed", "push nothing when the current version already carries this exact content").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)");
24821
25152
  addCommonAuthOptions(push).action(
24822
25153
  async (name, flags) => {
24823
- process.exit(
24824
- await runScriptsPush(
24825
- {
24826
- ...toCommonOptions(flags),
24827
- name,
24828
- context: flags.context,
24829
- sourceFile: flags.sourceFile,
24830
- inputContract: flags.inputContract,
24831
- inputContractFile: flags.inputContractFile,
24832
- ifChanged: Boolean(flags.ifChanged),
24833
- tenant: parseTenantFlag(flags.tenant)
24834
- },
24835
- deps
24836
- )
25154
+ const tenant = parseTenantFlag(flags.tenant);
25155
+ if (tenant === TENANT_FLAG_INVALID) return;
25156
+ process.exitCode = await runScriptsPush(
25157
+ {
25158
+ ...toCommonOptions(flags),
25159
+ name,
25160
+ context: flags.context,
25161
+ sourceFile: flags.sourceFile,
25162
+ inputContract: flags.inputContract,
25163
+ inputContractFile: flags.inputContractFile,
25164
+ ifChanged: Boolean(flags.ifChanged),
25165
+ tenant
25166
+ },
25167
+ deps
24837
25168
  );
24838
25169
  }
24839
25170
  );
24840
25171
  const list = scripts.command("list").description("List script versions \u2014 every version of one --name (oldest first), or all scripts (newest first).").requiredOption("--context <c>", "app context to read").option("--name <name>", "list every version of this script name instead of a flat cross-name list").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)");
24841
25172
  addCommonAuthOptions(list).action(
24842
25173
  async (flags) => {
24843
- process.exit(
24844
- await runScriptsList(
24845
- { ...toCommonOptions(flags), context: flags.context, name: flags.name, tenant: parseTenantFlag(flags.tenant) },
24846
- deps
24847
- )
25174
+ const tenant = parseTenantFlag(flags.tenant);
25175
+ if (tenant === TENANT_FLAG_INVALID) return;
25176
+ process.exitCode = await runScriptsList(
25177
+ { ...toCommonOptions(flags), context: flags.context, name: flags.name, tenant },
25178
+ deps
24848
25179
  );
24849
25180
  }
24850
25181
  );
24851
25182
  const get = scripts.command("get").description("Show one script version, including its full source.").argument("<id>", "the script VERSION id (each version has its own)").requiredOption("--context <c>", "app context to read").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)");
24852
25183
  addCommonAuthOptions(get).action(
24853
25184
  async (id, flags) => {
24854
- process.exit(
24855
- await runScriptsGet(
24856
- { ...toCommonOptions(flags), id, context: flags.context, tenant: parseTenantFlag(flags.tenant) },
24857
- deps
24858
- )
25185
+ const tenant = parseTenantFlag(flags.tenant);
25186
+ if (tenant === TENANT_FLAG_INVALID) return;
25187
+ process.exitCode = await runScriptsGet(
25188
+ { ...toCommonOptions(flags), id, context: flags.context, tenant },
25189
+ deps
24859
25190
  );
24860
25191
  }
24861
25192
  );
24862
25193
  const del = scripts.command("delete").description("Permanently delete ONE script version (other versions of the name are unaffected).").argument("<id>", "the script VERSION id").requiredOption("--context <c>", "app context to delete from").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)").option("--force", "skip the interactive confirmation (required in a non-interactive shell)");
24863
25194
  addCommonAuthOptions(del).action(
24864
25195
  async (id, flags) => {
24865
- process.exit(
24866
- await runScriptsDelete(
24867
- {
24868
- ...toCommonOptions(flags),
24869
- id,
24870
- context: flags.context,
24871
- tenant: parseTenantFlag(flags.tenant),
24872
- force: Boolean(flags.force)
24873
- },
24874
- deps
24875
- )
25196
+ const tenant = parseTenantFlag(flags.tenant);
25197
+ if (tenant === TENANT_FLAG_INVALID) return;
25198
+ process.exitCode = await runScriptsDelete(
25199
+ {
25200
+ ...toCommonOptions(flags),
25201
+ id,
25202
+ context: flags.context,
25203
+ tenant,
25204
+ force: Boolean(flags.force)
25205
+ },
25206
+ deps
24876
25207
  );
24877
25208
  }
24878
25209
  );
@@ -24892,9 +25223,11 @@ function registerRoleCommands(program, deps) {
24892
25223
  ).option("--description <text>", "free-text description").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24893
25224
  addCommonAuthOptions(create).action(
24894
25225
  async (flags) => {
25226
+ const tenant = parseTenantFlag(flags.tenant);
25227
+ if (tenant === TENANT_FLAG_INVALID) return;
24895
25228
  process.exitCode = await runRoleCreate({
24896
25229
  ...toCommonOptions(flags),
24897
- tenant: parseTenantFlag(flags.tenant),
25230
+ tenant,
24898
25231
  context: flags.context,
24899
25232
  roleId: flags.roleId,
24900
25233
  name: flags.name,
@@ -24908,16 +25241,20 @@ function registerRoleCommands(program, deps) {
24908
25241
  );
24909
25242
  const list = role.command("list").description("List the roles defined in a context.").requiredOption("--context <c>", "app context").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24910
25243
  addCommonAuthOptions(list).action(async (flags) => {
25244
+ const tenant = parseTenantFlag(flags.tenant);
25245
+ if (tenant === TENANT_FLAG_INVALID) return;
24911
25246
  process.exitCode = await runRoleList(
24912
- { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context },
25247
+ { ...toCommonOptions(flags), tenant, context: flags.context },
24913
25248
  deps
24914
25249
  );
24915
25250
  });
24916
25251
  const get = role.command("get").description("Show one role by id.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24917
25252
  addCommonAuthOptions(get).action(
24918
25253
  async (flags) => {
25254
+ const tenant = parseTenantFlag(flags.tenant);
25255
+ if (tenant === TENANT_FLAG_INVALID) return;
24919
25256
  process.exitCode = await runRoleGet(
24920
- { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context, roleId: flags.roleId },
25257
+ { ...toCommonOptions(flags), tenant, context: flags.context, roleId: flags.roleId },
24921
25258
  deps
24922
25259
  );
24923
25260
  }
@@ -24925,8 +25262,10 @@ function registerRoleCommands(program, deps) {
24925
25262
  const del = role.command("delete").description("Delete a role (referencing profiles must be reassigned).").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24926
25263
  addAuthOptions(del).action(
24927
25264
  async (flags) => {
25265
+ const tenant = parseTenantFlag(flags.tenant);
25266
+ if (tenant === TENANT_FLAG_INVALID) return;
24928
25267
  process.exitCode = await runRoleDelete(
24929
- { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context, roleId: flags.roleId },
25268
+ { ...toCommonOptions(flags), tenant, context: flags.context, roleId: flags.roleId },
24930
25269
  deps
24931
25270
  );
24932
25271
  }
@@ -24937,9 +25276,11 @@ function registerAccessCommands(program, deps) {
24937
25276
  const grant = access2.command("grant").description("Bind a principal to a context via a role or inline actions.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--role <roleId>", "reference a Role (repeatable \u2014 2+ compose additively; XOR --actions)", collect, []).option("--actions <csv>", "inline single-clause actions, comma-separated (XOR --role)").option("--scope <ns:value>", "identity override: a scope the principal acts under (repeatable, e.g. org:<id>)", collect, []).option("--identity-overrides <json>", "general identity overrides as a JSON object (merged over --scope)").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24938
25277
  addCommonAuthOptions(grant).action(
24939
25278
  async (flags) => {
25279
+ const tenant = parseTenantFlag(flags.tenant);
25280
+ if (tenant === TENANT_FLAG_INVALID) return;
24940
25281
  process.exitCode = await runAccessGrant({
24941
25282
  ...toCommonOptions(flags),
24942
- tenant: parseTenantFlag(flags.tenant),
25283
+ tenant,
24943
25284
  principal: flags.principal,
24944
25285
  context: flags.context,
24945
25286
  role: flags.role,
@@ -24952,8 +25293,10 @@ function registerAccessCommands(program, deps) {
24952
25293
  const revoke = access2.command("revoke").description("Remove a principal\u2019s binding in a context.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24953
25294
  addAuthOptions(revoke).action(
24954
25295
  async (flags) => {
25296
+ const tenant = parseTenantFlag(flags.tenant);
25297
+ if (tenant === TENANT_FLAG_INVALID) return;
24955
25298
  process.exitCode = await runAccessRevoke(
24956
- { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), principal: flags.principal, context: flags.context },
25299
+ { ...toCommonOptions(flags), tenant, principal: flags.principal, context: flags.context },
24957
25300
  deps
24958
25301
  );
24959
25302
  }
@@ -24961,8 +25304,10 @@ function registerAccessCommands(program, deps) {
24961
25304
  const list = access2.command("list").description("List a context\u2019s members (--context) or a principal\u2019s contexts (--principal).").option("--context <c>", "list members of this context").option("--principal <p>", "list contexts this principal is bound to ('me' = your own user)").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24962
25305
  addCommonAuthOptions(list).action(
24963
25306
  async (flags) => {
25307
+ const tenant = parseTenantFlag(flags.tenant);
25308
+ if (tenant === TENANT_FLAG_INVALID) return;
24964
25309
  process.exitCode = await runAccessList(
24965
- { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context, principal: flags.principal },
25310
+ { ...toCommonOptions(flags), tenant, context: flags.context, principal: flags.principal },
24966
25311
  deps
24967
25312
  );
24968
25313
  }
@@ -24970,8 +25315,10 @@ function registerAccessCommands(program, deps) {
24970
25315
  const get = access2.command("get").description("Show one principal\u2019s binding in a context.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24971
25316
  addCommonAuthOptions(get).action(
24972
25317
  async (flags) => {
25318
+ const tenant = parseTenantFlag(flags.tenant);
25319
+ if (tenant === TENANT_FLAG_INVALID) return;
24973
25320
  process.exitCode = await runAccessGet(
24974
- { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), principal: flags.principal, context: flags.context },
25321
+ { ...toCommonOptions(flags), tenant, principal: flags.principal, context: flags.context },
24975
25322
  deps
24976
25323
  );
24977
25324
  }
@@ -24979,8 +25326,10 @@ function registerAccessCommands(program, deps) {
24979
25326
  const explain = access2.command("explain").description("Show what a principal\u2019s binding grants: its clauses + a concrete access matrix.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24980
25327
  addCommonAuthOptions(explain).action(
24981
25328
  async (flags) => {
25329
+ const tenant = parseTenantFlag(flags.tenant);
25330
+ if (tenant === TENANT_FLAG_INVALID) return;
24982
25331
  process.exitCode = await runAccessExplain(
24983
- { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), principal: flags.principal, context: flags.context },
25332
+ { ...toCommonOptions(flags), tenant, principal: flags.principal, context: flags.context },
24984
25333
  deps
24985
25334
  );
24986
25335
  }
@@ -24997,31 +25346,27 @@ function registerIdentityCommands(program, deps) {
24997
25346
  const create = identity.command("create").description("Create (idempotent by externalId) a principal.").addOption(dimensionOption()).requiredOption("--external-id <id>", "your stable id for the principal (idempotency key)").option("--name <name>", "display name (entity namespaces only, not --type user)").option("--email <email>", "email (user only)").option("--service", "user only: mint a SERVICE principal (default HUMAN)").option("--scope <ns:value>", "entity only: a parent ownership edge (repeatable, \u22642, e.g. org:<id>)", collect, []).option("--metadata <json>", "JSON object of metadata");
24998
25347
  addCommonAuthOptions(create).action(
24999
25348
  async (flags) => {
25000
- process.exit(
25001
- await runIdentityCreate({
25002
- ...toCommonOptions(flags),
25003
- type: flags.type,
25004
- externalId: flags.externalId,
25005
- name: flags.name,
25006
- email: flags.email,
25007
- service: flags.service,
25008
- scope: flags.scope,
25009
- metadata: flags.metadata
25010
- }, deps)
25011
- );
25349
+ process.exitCode = await runIdentityCreate({
25350
+ ...toCommonOptions(flags),
25351
+ type: flags.type,
25352
+ externalId: flags.externalId,
25353
+ name: flags.name,
25354
+ email: flags.email,
25355
+ service: flags.service,
25356
+ scope: flags.scope,
25357
+ metadata: flags.metadata
25358
+ }, deps);
25012
25359
  }
25013
25360
  );
25014
25361
  const list = identity.command("list").description("List principals of a dimension (optionally filtered).").addOption(dimensionOption()).option("--external-id <id>", "filter to a stable id").option("--limit <n>", "page size (1\u2013100)", (v) => Number.parseInt(v, 10));
25015
25362
  addCommonAuthOptions(list).action(
25016
25363
  async (flags) => {
25017
- process.exit(
25018
- await runIdentityList({
25019
- ...toCommonOptions(flags),
25020
- type: flags.type,
25021
- externalId: flags.externalId,
25022
- limit: flags.limit
25023
- }, deps)
25024
- );
25364
+ process.exitCode = await runIdentityList({
25365
+ ...toCommonOptions(flags),
25366
+ type: flags.type,
25367
+ externalId: flags.externalId,
25368
+ limit: flags.limit
25369
+ }, deps);
25025
25370
  }
25026
25371
  );
25027
25372
  const listAll = identity.command("list-all").description(
@@ -25030,38 +25375,35 @@ function registerIdentityCommands(program, deps) {
25030
25375
  addCommonAuthOptions(listAll).action(
25031
25376
  async (flags) => {
25032
25377
  const tenant = parseTenantFlag(flags.tenant);
25033
- process.exit(
25034
- await runIdentityListAll(
25035
- { ...toCommonOptions(flags), externalId: flags.externalId, limit: flags.limit, tenant },
25036
- deps
25037
- )
25378
+ if (tenant === TENANT_FLAG_INVALID) return;
25379
+ process.exitCode = await runIdentityListAll(
25380
+ { ...toCommonOptions(flags), externalId: flags.externalId, limit: flags.limit, tenant },
25381
+ deps
25038
25382
  );
25039
25383
  }
25040
25384
  );
25041
25385
  const get = identity.command("get").description("Show one principal by its Vectros id.").addOption(dimensionOption()).requiredOption("--id <vectrosId>", "the Vectros-assigned id (from create/list)");
25042
25386
  addCommonAuthOptions(get).action(async (flags) => {
25043
- process.exit(await runIdentityGet({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));
25387
+ process.exitCode = await runIdentityGet({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps);
25044
25388
  });
25045
25389
  const del = identity.command("delete").description("Delete a principal by its Vectros id.").addOption(dimensionOption()).requiredOption("--id <vectrosId>", "the Vectros-assigned id");
25046
25390
  addAuthOptions(del).action(async (flags) => {
25047
- process.exit(await runIdentityDelete({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));
25391
+ process.exitCode = await runIdentityDelete({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps);
25048
25392
  });
25049
25393
  const invite = identity.command("invite").description("Onboard a person into a context by email (POST /v1/users/invite) \u2014 no root key required.").requiredOption("--context <contextId>", "the app context the invitee will operate in").requiredOption("--role <roleId>", "an existing Role in that context; the invitee inherits its scopes").requiredOption("--email <email>", "the invitee's email address").option("--first-name <name>", "personalizes the invitation email greeting").option("--accept-url <url>", "your accept-invite landing page (required unless --no-send-email)").option("--no-send-email", "skip platform delivery; the response carries the raw inviteToken/acceptLink instead");
25050
25394
  addCommonAuthOptions(invite).action(
25051
25395
  async (flags) => {
25052
- process.exit(
25053
- await runIdentityInvite(
25054
- {
25055
- ...toCommonOptions(flags),
25056
- context: flags.context,
25057
- role: flags.role,
25058
- email: flags.email,
25059
- firstName: flags.firstName,
25060
- acceptUrl: flags.acceptUrl,
25061
- noSendEmail: flags.sendEmail === false
25062
- },
25063
- deps
25064
- )
25396
+ process.exitCode = await runIdentityInvite(
25397
+ {
25398
+ ...toCommonOptions(flags),
25399
+ context: flags.context,
25400
+ role: flags.role,
25401
+ email: flags.email,
25402
+ firstName: flags.firstName,
25403
+ acceptUrl: flags.acceptUrl,
25404
+ noSendEmail: flags.sendEmail === false
25405
+ },
25406
+ deps
25065
25407
  );
25066
25408
  }
25067
25409
  );
@@ -25087,17 +25429,16 @@ function registerKeyCommands(program, deps) {
25087
25429
  );
25088
25430
  addAuthOptions(issue).action(async (flags) => {
25089
25431
  const tenant = parseTenantFlag(flags.tenant);
25090
- process.exit(
25091
- await runKeyIssue({
25092
- ...toCommonOptions(flags),
25093
- principal: flags.principal,
25094
- context: flags.context,
25095
- name: flags.name,
25096
- label: flags.label,
25097
- tenant,
25098
- ...toKeyDestination(flags)
25099
- }, deps)
25100
- );
25432
+ if (tenant === TENANT_FLAG_INVALID) return;
25433
+ process.exitCode = await runKeyIssue({
25434
+ ...toCommonOptions(flags),
25435
+ principal: flags.principal,
25436
+ context: flags.context,
25437
+ name: flags.name,
25438
+ label: flags.label,
25439
+ tenant,
25440
+ ...toKeyDestination(flags)
25441
+ }, deps);
25101
25442
  });
25102
25443
  const list = key.command("list").description("List active scoped keys in one context (default, or --context <id>); --all-contexts for every context.").option("--principal <p>", "only keys for this principal (usr_<id> or <id>)").option("--context <c>", "only keys in this context (default: the default context)").option(
25103
25444
  "--all-contexts",
@@ -25107,20 +25448,20 @@ function registerKeyCommands(program, deps) {
25107
25448
  async (flags) => {
25108
25449
  if (flags.allContexts && flags.tenant !== void 0) {
25109
25450
  process.stderr.write(" \u2716 --tenant is not combinable with --all-contexts (which already spans both tenants).\n");
25110
- process.exit(2);
25451
+ process.exitCode = 2;
25452
+ return;
25111
25453
  }
25112
25454
  const tenant = parseTenantFlag(flags.tenant);
25113
- process.exit(
25114
- await runKeyList(
25115
- {
25116
- ...toCommonOptions(flags),
25117
- principal: flags.principal,
25118
- context: flags.context,
25119
- allContexts: Boolean(flags.allContexts),
25120
- tenant
25121
- },
25122
- deps
25123
- )
25455
+ if (tenant === TENANT_FLAG_INVALID) return;
25456
+ process.exitCode = await runKeyList(
25457
+ {
25458
+ ...toCommonOptions(flags),
25459
+ principal: flags.principal,
25460
+ context: flags.context,
25461
+ allContexts: Boolean(flags.allContexts),
25462
+ tenant
25463
+ },
25464
+ deps
25124
25465
  );
25125
25466
  }
25126
25467
  );
@@ -25128,14 +25469,16 @@ function registerKeyCommands(program, deps) {
25128
25469
  addCommonAuthOptions(get).action(
25129
25470
  async (keyId, flags) => {
25130
25471
  const tenant = parseTenantFlag(flags.tenant);
25131
- process.exit(await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
25472
+ if (tenant === TENANT_FLAG_INVALID) return;
25473
+ process.exitCode = await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps);
25132
25474
  }
25133
25475
  );
25134
25476
  const revoke = key.command("revoke").description("Revoke a key (soft-delete; ~5 min to take effect).").argument("<keyId>").option("--context <c>", "context the key is bound to (default: the default context)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)");
25135
25477
  addAuthOptions(revoke).action(
25136
25478
  async (keyId, flags) => {
25137
25479
  const tenant = parseTenantFlag(flags.tenant);
25138
- process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
25480
+ if (tenant === TENANT_FLAG_INVALID) return;
25481
+ process.exitCode = await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps);
25139
25482
  }
25140
25483
  );
25141
25484
  const rotate = addKeyDestinationOptions(
@@ -25146,17 +25489,16 @@ function registerKeyCommands(program, deps) {
25146
25489
  );
25147
25490
  addAuthOptions(rotate).action(async (flags) => {
25148
25491
  const tenant = parseTenantFlag(flags.tenant);
25149
- process.exit(
25150
- await runKeyRotate({
25151
- ...toCommonOptions(flags),
25152
- principal: flags.principal,
25153
- context: flags.context,
25154
- name: flags.name,
25155
- forceNew: flags.forceNew,
25156
- tenant,
25157
- ...toKeyDestination(flags)
25158
- }, deps)
25159
- );
25492
+ if (tenant === TENANT_FLAG_INVALID) return;
25493
+ process.exitCode = await runKeyRotate({
25494
+ ...toCommonOptions(flags),
25495
+ principal: flags.principal,
25496
+ context: flags.context,
25497
+ name: flags.name,
25498
+ forceNew: flags.forceNew,
25499
+ tenant,
25500
+ ...toKeyDestination(flags)
25501
+ }, deps);
25160
25502
  });
25161
25503
  }
25162
25504
 
@@ -25169,7 +25511,8 @@ async function main() {
25169
25511
  const sub = process.argv[2];
25170
25512
  const passthrough = sub ? PASSTHROUGH_COMMANDS[sub] : void 0;
25171
25513
  if (passthrough) {
25172
- process.exit(await passthrough(process.argv.slice(3)));
25514
+ process.exitCode = await passthrough(process.argv.slice(3));
25515
+ return;
25173
25516
  }
25174
25517
  await buildProgram().parseAsync(process.argv);
25175
25518
  }
@@ -25180,6 +25523,6 @@ main().catch((err) => {
25180
25523
  process.stderr.write(`${err.stack}
25181
25524
  `);
25182
25525
  }
25183
- process.exit(2);
25526
+ process.exitCode = 2;
25184
25527
  });
25185
25528
  //# sourceMappingURL=cli.mjs.map