@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.js CHANGED
@@ -1614,7 +1614,7 @@ var require_proper_lockfile = __commonJS({
1614
1614
  // src/cli-bootstrap.ts
1615
1615
  var readline2 = __toESM(require("readline/promises"));
1616
1616
 
1617
- // ../../node_modules/@vectros-ai/sdk/dist/index.mjs
1617
+ // node_modules/@vectros-ai/sdk/dist/index.mjs
1618
1618
  var __defProp2 = Object.defineProperty;
1619
1619
  var __export = (target, all) => {
1620
1620
  for (var name in all)
@@ -5734,7 +5734,7 @@ var AuthClient = class {
5734
5734
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
5735
5735
  }
5736
5736
  /**
5737
- * 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.
5737
+ * 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.
5738
5738
  *
5739
5739
  * @param {Vectros.IssuerUpdateRequest} request
5740
5740
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5801,7 +5801,7 @@ var AuthClient = class {
5801
5801
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/auth/issuers/{issuerId}");
5802
5802
  }
5803
5803
  /**
5804
- * 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.
5804
+ * 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.
5805
5805
  *
5806
5806
  * @param {Vectros.DeleteIssuerRequest} request
5807
5807
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5927,7 +5927,7 @@ var AuthClient = class {
5927
5927
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
5928
5928
  }
5929
5929
  /**
5930
- * 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`.
5930
+ * 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.
5931
5931
  *
5932
5932
  * @param {Vectros.IssuerRequest} request
5933
5933
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7425,7 +7425,7 @@ var DocumentsClient = class {
7425
7425
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/documents/{id}/versions");
7426
7426
  }
7427
7427
  /**
7428
- * 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).
7428
+ * 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.
7429
7429
  *
7430
7430
  * @param {Vectros.FileUploadRequest} request
7431
7431
  * @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9191,7 +9191,7 @@ var IdentityClient = class {
9191
9191
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/exists-by-email");
9192
9192
  }
9193
9193
  /**
9194
- * 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.
9194
+ * 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.
9195
9195
  *
9196
9196
  * @param {Vectros.GetUserVersionsRequest} request
9197
9197
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -10668,7 +10668,7 @@ var SchemasClient = class {
10668
10668
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/schemas/{id}");
10669
10669
  }
10670
10670
  /**
10671
- * 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.
10671
+ * 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.
10672
10672
  *
10673
10673
  * @param {Vectros.DeleteSchemaRequest} request
10674
10674
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -10803,7 +10803,7 @@ var ScriptsClient = class {
10803
10803
  *
10804
10804
  * **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`).
10805
10805
  *
10806
- * **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()`.
10806
+ * **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()`.
10807
10807
  *
10808
10808
  * **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.
10809
10809
  *
@@ -10898,11 +10898,14 @@ var ScriptsClient = class {
10898
10898
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/scripts/execute");
10899
10899
  }
10900
10900
  /**
10901
- * 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.
10901
+ * 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.
10902
10902
  *
10903
10903
  * @param {Vectros.ListScriptsRequest} request
10904
10904
  * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
10905
10905
  *
10906
+ * @throws {@link Vectros.BadRequestError}
10907
+ * @throws {@link Vectros.NotFoundError}
10908
+ *
10906
10909
  * @example
10907
10910
  * await client.scripts.listScripts({
10908
10911
  * name: "notify-assignee",
@@ -10913,11 +10916,13 @@ var ScriptsClient = class {
10913
10916
  return HttpResponsePromise.fromPromise(this.__listScripts(request, requestOptions));
10914
10917
  }
10915
10918
  async __listScripts(request = {}, requestOptions) {
10916
- const { name, startFrom, limit } = request;
10919
+ const { name, startFrom, limit, includeSource, latest } = request;
10917
10920
  const _queryParams = {
10918
10921
  name,
10919
10922
  startFrom,
10920
- limit
10923
+ limit,
10924
+ includeSource,
10925
+ latest
10921
10926
  };
10922
10927
  const _authRequest = await this._options.authProvider.getAuthRequest();
10923
10928
  const _headers = mergeHeaders(
@@ -10943,16 +10948,23 @@ var ScriptsClient = class {
10943
10948
  return { data: _response.body, rawResponse: _response.rawResponse };
10944
10949
  }
10945
10950
  if (_response.error.reason === "status-code") {
10946
- throw new VectrosError({
10947
- statusCode: _response.error.statusCode,
10948
- body: _response.error.body,
10949
- rawResponse: _response.rawResponse
10950
- });
10951
+ switch (_response.error.statusCode) {
10952
+ case 400:
10953
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
10954
+ case 404:
10955
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
10956
+ default:
10957
+ throw new VectrosError({
10958
+ statusCode: _response.error.statusCode,
10959
+ body: _response.error.body,
10960
+ rawResponse: _response.rawResponse
10961
+ });
10962
+ }
10951
10963
  }
10952
10964
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/scripts");
10953
10965
  }
10954
10966
  /**
10955
- * 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.
10967
+ * 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.
10956
10968
  *
10957
10969
  * @param {Vectros.ScriptRequest} request
10958
10970
  * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -11139,12 +11151,13 @@ var ScriptsClient = class {
11139
11151
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/scripts/{id}");
11140
11152
  }
11141
11153
  /**
11142
- * 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.
11154
+ * 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.
11143
11155
  *
11144
11156
  * @param {Vectros.DeleteScriptRequest} request
11145
11157
  * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
11146
11158
  *
11147
11159
  * @throws {@link Vectros.NotFoundError}
11160
+ * @throws {@link Vectros.ConflictError}
11148
11161
  * @throws {@link Vectros.TooManyRequestsError}
11149
11162
  *
11150
11163
  * @example
@@ -11184,6 +11197,8 @@ var ScriptsClient = class {
11184
11197
  switch (_response.error.statusCode) {
11185
11198
  case 404:
11186
11199
  throw new NotFoundError(_response.error.body, _response.rawResponse);
11200
+ case 409:
11201
+ throw new ConflictError(_response.error.body, _response.rawResponse);
11187
11202
  case 429:
11188
11203
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11189
11204
  default:
@@ -11729,6 +11744,25 @@ var logging;
11729
11744
  var import_zod = require("zod");
11730
11745
  var import_zod2 = require("zod");
11731
11746
  var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
11747
+ 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;
11748
+ function safeDisplayString(base) {
11749
+ return base.refine((v) => !FORGERY_CHARS_RE.test(v), {
11750
+ 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"
11751
+ });
11752
+ }
11753
+ function safeUrlString(base) {
11754
+ return base.refine(
11755
+ (v) => {
11756
+ if (!/^[\x20-\x7e]*$/.test(v)) return false;
11757
+ const authorityMatch = /^https:\/\/([^/?#]*)/i.exec(v);
11758
+ if (!authorityMatch) return false;
11759
+ return /^[A-Za-z0-9.-]+(:\d+)?$/.test(authorityMatch[1]);
11760
+ },
11761
+ {
11762
+ 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"
11763
+ }
11764
+ );
11765
+ }
11732
11766
  var MAX_SCOPE_DIMENSIONS = 2;
11733
11767
  var ValidationRulesSchema = import_zod.z.object({
11734
11768
  required: import_zod.z.boolean().optional(),
@@ -11758,7 +11792,7 @@ var RenderHintsSchema = import_zod.z.object({
11758
11792
  displayField: import_zod.z.boolean().optional()
11759
11793
  }).strict();
11760
11794
  var BlueprintFieldDefSchema = import_zod.z.object({
11761
- fieldId: import_zod.z.string().min(1),
11795
+ fieldId: safeDisplayString(import_zod.z.string().min(1)),
11762
11796
  fieldType: import_zod.z.string().min(1),
11763
11797
  required: import_zod.z.boolean().optional(),
11764
11798
  searchable: import_zod.z.boolean().optional(),
@@ -11849,13 +11883,13 @@ var BlueprintFieldDefSchema = import_zod.z.object({
11849
11883
  }
11850
11884
  });
11851
11885
  var BlueprintLookupFieldSchema = import_zod.z.union([
11852
- import_zod.z.string().min(1),
11886
+ safeDisplayString(import_zod.z.string().min(1)),
11853
11887
  import_zod.z.object({
11854
11888
  // A lookup field declares either ONE field (fieldName) or 2-3 fields
11855
11889
  // matched together (fieldNames) — never both, never neither. `.strict()`
11856
11890
  // below only rejects unknown KEYS; it does not enforce this combination,
11857
11891
  // so the `.superRefine()` after it carries the actual rule.
11858
- fieldName: import_zod.z.string().min(1).optional(),
11892
+ fieldName: safeDisplayString(import_zod.z.string().min(1)).optional(),
11859
11893
  // Composite (conjunctive) lookup: match on all of these fields at once —
11860
11894
  // "every record where `status` is `open` AND `area` is `billing`", exact,
11861
11895
  // in a stable order. Order is significant and locked at create: a query
@@ -11864,7 +11898,7 @@ var BlueprintLookupFieldSchema = import_zod.z.union([
11864
11898
  // lookup for that. Record-only: the schema's `allowedSurfaces` must be
11865
11899
  // exactly `['record']` (checked at the schema level below) — the
11866
11900
  // platform's composite index has no document/user/entity reader yet.
11867
- fieldNames: import_zod.z.array(import_zod.z.string().min(1)).min(2).max(3).optional(),
11901
+ fieldNames: import_zod.z.array(safeDisplayString(import_zod.z.string().min(1))).min(2).max(3).optional(),
11868
11902
  // Not available on a composite (`fieldNames`) lookup — checked below.
11869
11903
  unique: import_zod.z.boolean().optional(),
11870
11904
  // Opt this field into ordered range + prefix lookups (from/to/prefix) on
@@ -11914,7 +11948,14 @@ var BlueprintCapabilitiesSchema = import_zod.z.object({
11914
11948
  triggersEnabled: import_zod.z.boolean().optional()
11915
11949
  }).strict();
11916
11950
  var BlueprintSchemaSchema = import_zod.z.object({
11917
- typeName: import_zod.z.string().min(1),
11951
+ // safeDisplayString: buildPlan's schema step renders this raw into a single summary
11952
+ // line (`schema '${s.typeName}' (...)`) — a gap found after the rest of this file's
11953
+ // forgery-character pass had already shipped, the same "unconstrained free-text field
11954
+ // reaches a rendered plan line" shape as issuer/allowedActions/etc. `displayName`
11955
+ // here is NOT wrapped: unlike
11956
+ // servicePrincipal's own displayName, this one is never interpolated into a rendered
11957
+ // summary anywhere in loader.ts today (only forwarded to the schema-upsert payload).
11958
+ typeName: safeDisplayString(import_zod.z.string().min(1)),
11918
11959
  displayName: import_zod.z.string().min(1),
11919
11960
  description: import_zod.z.string().optional(),
11920
11961
  // Default search-index mode for instances of this type. HYBRID, SEMANTIC and
@@ -12051,7 +12092,7 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
12051
12092
  // `.superRefine` on this schema enforces the XOR, mirroring the platform's
12052
12093
  // own access-profile scopes/role-composition XOR check). Exactly one of
12053
12094
  // the two authors this profile's grant.
12054
- allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
12095
+ allowedActions: import_zod.z.array(safeDisplayString(import_zod.z.string().min(1))).min(1).optional(),
12055
12096
  // Additive role composition: this profile's effective grant is
12056
12097
  // each named role's own clauses, concatenated in the order listed, instead of
12057
12098
  // an inline clause authored here. Every id must name a role this SAME
@@ -12080,7 +12121,14 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
12080
12121
  // NOT see tenant-level/seed records). Every KEY is validated below
12081
12122
  // (`lintDataScopeKeys`) — `userId` or a grammar-valid `scope:<ns>`, no
12082
12123
  // other shape.
12083
- dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
12124
+ // safeDisplayString on the value: not currently rendered by VALUE anywhere in buildPlan
12125
+ // (only `Object.keys(...)` of a profile's dataScope is shown today, unlike a role clause's
12126
+ // dataScope, which `renderPlanDataScope` prints values for — the reason that sibling field
12127
+ // WAS wrapped). Wrapped anyway for consistency with every other dataScope-shaped field in
12128
+ // this file, and so a future preview enhancement that starts showing these values (a very
12129
+ // plausible one, given the existing renderPlanDataScope precedent) doesn't silently reopen
12130
+ // this exact class on a field that looks, byte-for-byte, like every other one already closed.
12131
+ dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([safeDisplayString(import_zod.z.string().min(1)), import_zod.z.null()]))).optional(),
12084
12132
  // Optional identity overrides for the service principal's profile — scope
12085
12133
  // values its key STAMPS onto everything it writes (a credential can only
12086
12134
  // stamp ownership its identity carries). Keys are the namespaced `scope:<ns>`
@@ -12156,8 +12204,8 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
12156
12204
  }
12157
12205
  });
12158
12206
  var BlueprintRoleClauseSchema = import_zod.z.object({
12159
- allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
12160
- dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
12207
+ allowedActions: import_zod.z.array(safeDisplayString(import_zod.z.string().min(1))).min(1),
12208
+ dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([safeDisplayString(import_zod.z.string().min(1)), import_zod.z.null()]))).optional(),
12161
12209
  dataScopeRef: import_zod.z.string().min(1).optional(),
12162
12210
  capabilities: GrantedCapabilitiesSchema,
12163
12211
  assignableRoles: AssignableRolesSchema
@@ -12171,10 +12219,10 @@ var BlueprintRoleClauseSchema = import_zod.z.object({
12171
12219
  }
12172
12220
  });
12173
12221
  var BlueprintFragmentsSchema = import_zod.z.record(
12174
- import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()])))
12222
+ import_zod.z.record(import_zod.z.array(import_zod.z.union([safeDisplayString(import_zod.z.string().min(1)), import_zod.z.null()])))
12175
12223
  );
12176
- var BlueprintRolesSchema = import_zod.z.record(import_zod.z.array(BlueprintRoleClauseSchema).min(1));
12177
- var BlueprintRoleAssumableSchema = import_zod.z.record(import_zod.z.array(import_zod.z.string().min(1)).min(1));
12224
+ var BlueprintRolesSchema = import_zod.z.record(safeDisplayString(import_zod.z.string().min(1)), import_zod.z.array(BlueprintRoleClauseSchema).min(1));
12225
+ var BlueprintRoleAssumableSchema = import_zod.z.record(import_zod.z.array(safeDisplayString(import_zod.z.string().min(1))).min(1));
12178
12226
  var BlueprintRoleAssumableMapSchema = import_zod.z.record(BlueprintRoleAssumableSchema);
12179
12227
  var BlueprintTriggerFiringSourceSchema = import_zod.z.object({
12180
12228
  schemaName: import_zod.z.string().min(1),
@@ -12182,7 +12230,12 @@ var BlueprintTriggerFiringSourceSchema = import_zod.z.object({
12182
12230
  }).strict();
12183
12231
  var SCRIPT_VERSION_INTEGER_RE = /^[1-9][0-9]*$/;
12184
12232
  var BlueprintTriggerScriptRefSchema = import_zod.z.object({
12185
- name: import_zod.z.string().min(1),
12233
+ // safeDisplayString: rendered raw into buildPlan's trigger step (`${scriptRef.name}@...`).
12234
+ // When naming a script THIS blueprint ships, `name` is transitively safe already (it must
12235
+ // equal a `scripts` map key, itself grammar-pinned by SCRIPT_NAME_RE) — but a scriptRef
12236
+ // naming a script this blueprint does NOT ship is, by design, "entirely unrestricted" (see
12237
+ // this file's own doc on that case), so the wrap is load-bearing for exactly that path.
12238
+ name: safeDisplayString(import_zod.z.string().min(1)),
12186
12239
  version: import_zod.z.string().min(1)
12187
12240
  }).strict().superRefine((ref, ctx) => {
12188
12241
  if (ref.version !== "latest" && !SCRIPT_VERSION_INTEGER_RE.test(ref.version)) {
@@ -12202,9 +12255,9 @@ var BlueprintTriggerManifestSchema = import_zod.z.array(
12202
12255
  var BlueprintTriggerEntrySchema = import_zod.z.object({
12203
12256
  firingSource: BlueprintTriggerFiringSourceSchema,
12204
12257
  scriptRef: BlueprintTriggerScriptRefSchema,
12205
- allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
12258
+ allowedActions: import_zod.z.array(safeDisplayString(import_zod.z.string().min(1))).min(1).optional(),
12206
12259
  roleIds: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
12207
- dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
12260
+ dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([safeDisplayString(import_zod.z.string().min(1)), import_zod.z.null()]))).optional(),
12208
12261
  capabilities: GrantedCapabilitiesSchema,
12209
12262
  manifest: BlueprintTriggerManifestSchema,
12210
12263
  /**
@@ -12228,7 +12281,7 @@ var BlueprintTriggerEntrySchema = import_zod.z.object({
12228
12281
  * together are capped at 224 KB serialised — a firing whose projected input exceeds that is not
12229
12282
  * run, and is recorded as an `INPUT_TOO_LARGE` failure naming the size.
12230
12283
  */
12231
- fields: import_zod.z.array(import_zod.z.string().min(1))
12284
+ fields: import_zod.z.array(safeDisplayString(import_zod.z.string().min(1)))
12232
12285
  }).strict().superRefine((entry, ctx) => {
12233
12286
  const hasInline = entry.allowedActions !== void 0;
12234
12287
  const hasRoleIds = entry.roleIds !== void 0;
@@ -12257,7 +12310,7 @@ var BlueprintTriggerEntrySchema = import_zod.z.object({
12257
12310
  }
12258
12311
  }
12259
12312
  });
12260
- var BlueprintTriggersSchema = import_zod.z.record(BlueprintTriggerEntrySchema);
12313
+ var BlueprintTriggersSchema = import_zod.z.record(safeDisplayString(import_zod.z.string().min(1)), BlueprintTriggerEntrySchema);
12261
12314
  var SCRIPT_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
12262
12315
  var SCRIPT_SOURCE_MAX_BYTES = 3e5;
12263
12316
  var SCRIPT_INPUT_CONTRACT_MAX_BYTES = 5e4;
@@ -12303,8 +12356,15 @@ var IdentityKindSchema = import_zod.z.string().min(1).superRefine((val, ctx) =>
12303
12356
  });
12304
12357
  var IdentityDeclSchema = import_zod.z.object({
12305
12358
  kind: IdentityKindSchema,
12306
- externalId: import_zod.z.string().min(1),
12307
- displayName: import_zod.z.string().min(1).optional(),
12359
+ // safeDisplayString: `@vectros-ai/cli`'s `runBootstrapCli` and `blueprint plan` both print
12360
+ // this RAW to stdout ("~ <name> (<kind>) — external id '<externalId>'"), immediately before
12361
+ // the "Apply this plan? [y/N]" confirmation gate itself — the highest-severity gap found in
12362
+ // this pass, since it sits at the confirmation prompt's own disclosure text, not merely
12363
+ // somewhere upstream of it. `displayName` is wrapped too though not currently
12364
+ // rendered in any preview (defense-in-depth, matching this file's practice for its adjacent
12365
+ // sibling fields) — it does reach a platform API payload as an entity's `name`.
12366
+ externalId: safeDisplayString(import_zod.z.string().min(1)),
12367
+ displayName: safeDisplayString(import_zod.z.string().min(1)).optional(),
12308
12368
  metadata: import_zod.z.record(import_zod.z.unknown()).optional()
12309
12369
  }).strict();
12310
12370
  var IDENTITY_NAME_RE = /^[A-Za-z_]\w*$/;
@@ -12315,8 +12375,8 @@ var IdentitiesDeclSchema = import_zod.z.record(
12315
12375
  IdentityDeclSchema
12316
12376
  );
12317
12377
  var BlueprintServicePrincipalSchema = import_zod.z.object({
12318
- externalId: import_zod.z.string().min(1),
12319
- displayName: import_zod.z.string().min(1)
12378
+ externalId: safeDisplayString(import_zod.z.string().min(1)),
12379
+ displayName: safeDisplayString(import_zod.z.string().min(1))
12320
12380
  }).strict();
12321
12381
  var ISSUER_ID_RE = CONTEXT_ID_RE;
12322
12382
  var MAX_CAPTURED_CLAIMS = 20;
@@ -12342,10 +12402,17 @@ var BlueprintIssuerSchema = import_zod.z.object({
12342
12402
  issuerId: import_zod.z.string().regex(ISSUER_ID_RE, {
12343
12403
  message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
12344
12404
  }),
12345
- /** The IdP's `iss` claim value, exactly as it appears in tokens it issues. */
12346
- issuer: import_zod.z.string().min(1),
12347
- /** The IdP's remote JWKS endpoint, used to verify presented tokens' signatures. */
12348
- jwksUri: import_zod.z.string().min(1),
12405
+ /**
12406
+ * The IdP's `iss` claim value, exactly as it appears in tokens it issues. `safeUrlString`,
12407
+ * not `safeDisplayString`: this is the trust-anchor field the plan preview exists to
12408
+ * disclose, so it gets the stronger positive check (see that function's own doc) rather
12409
+ * than the blocklist every OTHER free-text field uses — a homoglyph host here is exactly
12410
+ * what an operator approving a plan must be able to trust reading. Virtually every real
12411
+ * OIDC issuer is URL-shaped in practice, even though the spec does not strictly require it.
12412
+ */
12413
+ issuer: safeUrlString(import_zod.z.string().min(1)),
12414
+ /** The IdP's remote JWKS endpoint, used to verify presented tokens' signatures. `safeUrlString` — see `issuer`'s own comment just above. */
12415
+ jwksUri: safeUrlString(import_zod.z.string().min(1)),
12349
12416
  /**
12350
12417
  * The `aud` claim value a presented subject_token must carry. Must be globally
12351
12418
  * unique in combination with `issuer` — use a distinct audience per
@@ -12373,7 +12440,9 @@ var BlueprintIssuerSchema = import_zod.z.object({
12373
12440
  * back to calling this endpoint (with the presented token as the bearer credential) and reads
12374
12441
  * `emailClaim` from its JSON response instead. Omit to leave the fallback disabled.
12375
12442
  */
12376
- userinfoUri: import_zod.z.string().min(1).optional(),
12443
+ // `safeUrlString` — see `issuer`'s own comment above for why these three trust-anchor
12444
+ // fields get the stronger positive check instead of `safeDisplayString`'s blocklist.
12445
+ userinfoUri: safeUrlString(import_zod.z.string().min(1)).optional(),
12377
12446
  /**
12378
12447
  * Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
12379
12448
  * caller presents no invite token but names a signupType matching one of these (or omits it and
@@ -12413,9 +12482,18 @@ var BlueprintIssuerSchema = import_zod.z.object({
12413
12482
  * separate opt-ins, and capturing a claim does not by itself expose it on a profile.
12414
12483
  */
12415
12484
  capturedClaims: import_zod.z.array(
12416
- import_zod.z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
12417
- message: `a capturedClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
12418
- }).refine((name) => name.trim().length > 0, {
12485
+ // `safeDisplayString` (a `.refine`) then the blank-name `.refine` — both ordered AFTER
12486
+ // `.max`, since `.refine` returns a ZodEffects, which has no `.max` (`.refine` itself
12487
+ // chains fine onto a ZodEffects — it's on the ZodType base, not ZodString-specific).
12488
+ // `.min(1)` would be a LENGTH check and would admit a whitespace-only name while the
12489
+ // message claims "non-blank"; the platform rejects on isBlank(). Note the value is
12490
+ // REJECTED, not trimmed — it reaches the wire exactly as authored, matching how the
12491
+ // platform stores it.
12492
+ safeDisplayString(
12493
+ import_zod.z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
12494
+ message: `a capturedClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
12495
+ })
12496
+ ).refine((name) => name.trim().length > 0, {
12419
12497
  message: "a capturedClaims entry must be a non-blank OIDC claim name"
12420
12498
  })
12421
12499
  ).max(MAX_CAPTURED_CLAIMS, {
@@ -12586,14 +12664,24 @@ function lintNamespaces(value, ctx) {
12586
12664
  });
12587
12665
  }
12588
12666
  var SeedCommonShape = {
12589
- /** The schema typeName this seed instantiates (should match a declared schema). */
12590
- typeName: import_zod.z.string().min(1),
12667
+ /**
12668
+ * The schema typeName this seed instantiates (should match a declared schema).
12669
+ * `safeDisplayString`, matching `BlueprintSchemaSchema.typeName`: NOT currently
12670
+ * rendered into any plan-preview line (the seed step only counts records/documents),
12671
+ * but it is the same field concept and costs nothing to close now rather than leave
12672
+ * as a silent gap the next preview enhancement reopens.
12673
+ */
12674
+ typeName: safeDisplayString(import_zod.z.string().min(1)),
12591
12675
  /**
12592
12676
  * Stable, caller-supplied id — the loader's idempotency key AND the value other
12593
12677
  * seeds resolve a `reference` against (across surfaces: a record seed may
12594
12678
  * reference a document seed by its externalId, and vice versa).
12679
+ * `safeDisplayString`: not currently rendered in a plan-preview line (same as this schema's
12680
+ * `typeName` just above until this pass), but the same "costs nothing to close now" standard
12681
+ * applies — a short caller-supplied id, not free-form prose, so no YAML block-scalar
12682
+ * collision risk like `description`'s.
12595
12683
  */
12596
- externalId: import_zod.z.string().min(1),
12684
+ externalId: safeDisplayString(import_zod.z.string().min(1)),
12597
12685
  /**
12598
12686
  * Optional scope ownership for the seeded item, as `namespace:value` entries
12599
12687
  * (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
@@ -13252,15 +13340,31 @@ var BLUEPRINT_FIELD_PHASES = Object.freeze({
13252
13340
  });
13253
13341
  var BlueprintSchema = import_zod.z.object({
13254
13342
  /** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
13255
- name: import_zod.z.string().min(1),
13256
- version: import_zod.z.string().min(1),
13343
+ name: safeDisplayString(import_zod.z.string().min(1)),
13344
+ version: safeDisplayString(import_zod.z.string().min(1)),
13345
+ // NOT safeDisplayString — deliberately, after that wrap broke a real, multi-line-authored
13346
+ // blueprint's `description` in review. `description` is never interpolated into any
13347
+ // rendered plan-preview line (`buildPlan`'s header prints
13348
+ // `name`/`version`/`contextId`, never `description` — confirmed by grep, not assumed), so
13349
+ // wrapping it protected a sink that does not exist. It also actively broke real content:
13350
+ // this is exactly the field authors reach for a YAML folded (`>`) or literal (`|`) block
13351
+ // scalar to write, and clip chomping (the YAML default) always leaves exactly one trailing
13352
+ // `\n` — which `safeDisplayString` rejects as a C0 control, indistinguishable from a
13353
+ // forgery attempt. See `safeDisplayString`'s own doc for why this is a property of the
13354
+ // wrapper, not a one-off carve-out for this field alone.
13257
13355
  description: import_zod.z.string().min(1),
13258
13356
  /** The app-context the profile + scoped key bind to (e.g. "mcp"). */
13259
13357
  contextId: import_zod.z.string().regex(CONTEXT_ID_RE, {
13260
13358
  message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'mcp')"
13261
13359
  }),
13262
- /** Human-readable app-context name; defaults to `MCP — <name>` (see {@link contextNameOf}) when absent. */
13263
- contextName: import_zod.z.string().min(1).optional(),
13360
+ /**
13361
+ * Human-readable app-context name; defaults to `MCP — <name>` (see {@link contextNameOf})
13362
+ * when absent. `safeDisplayString`: `contextNameOf` renders this raw into the FIRST line of
13363
+ * every `buildPlan` summary (the `context '...' (...)` step) — a gap found after the rest
13364
+ * of this file's forgery-character pass had already shipped; same shape as
13365
+ * `name`/`version`/`description` right above it.
13366
+ */
13367
+ contextName: safeDisplayString(import_zod.z.string().min(1)).optional(),
13264
13368
  /**
13265
13369
  * The deploying organization's own display name — distinct from {@link contextName}.
13266
13370
  * `contextName` is the blueprint author's fixed identity for the app (the same for every
@@ -13268,8 +13372,13 @@ var BlueprintSchema = import_zod.z.object({
13268
13372
  * deployer-supplied `${{ inputs.x }}` value rather than hardcoded. Used for branding on
13269
13373
  * platform-sent correspondence (e.g. invite emails) alongside `contextName` — not a
13270
13374
  * replacement for it. See {@link companyNameOf}.
13375
+ *
13376
+ * `safeDisplayString`: not interpolated into any CLI plan-preview line today (confirmed —
13377
+ * `loader.ts` never reads `companyName`), but it IS forwarded to the platform for the
13378
+ * invite-email correspondence described above, a second class of human-read surface this
13379
+ * value reaches. Wrapped for the same reason as every sibling scalar identity field here.
13271
13380
  */
13272
- companyName: import_zod.z.string().min(1).optional(),
13381
+ companyName: safeDisplayString(import_zod.z.string().min(1)).optional(),
13273
13382
  schemas: import_zod.z.array(BlueprintSchemaSchema).default([]),
13274
13383
  accessProfile: BlueprintAccessProfileSchema,
13275
13384
  servicePrincipal: BlueprintServicePrincipalSchema,
@@ -13346,9 +13455,16 @@ var BlueprintSchema = import_zod.z.object({
13346
13455
  * context's name and needs a broader scope — see `@vectros-ai/cli`'s own notes.)
13347
13456
  */
13348
13457
  identityProjectionClaims: import_zod.z.array(
13349
- import_zod.z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
13350
- message: `an identityProjectionClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
13351
- }).refine((name) => name.trim().length > 0, {
13458
+ // safeDisplayString: rendered raw into buildPlan's context step (`identity projection:
13459
+ // [...]`) — same rendered-plan-preview shape as `capturedClaims` (its structural pair,
13460
+ // and fixed alongside it originally), missed here in the first pass; same ordering rule
13461
+ // applies (safeDisplayString's own `.refine` before the blank-name `.refine` below,
13462
+ // both after `.max` since `.refine` returns a ZodEffects with no `.max`).
13463
+ safeDisplayString(
13464
+ import_zod.z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
13465
+ message: `an identityProjectionClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
13466
+ })
13467
+ ).refine((name) => name.trim().length > 0, {
13352
13468
  message: "an identityProjectionClaims entry must be a non-blank claim name"
13353
13469
  })
13354
13470
  ).max(MAX_CAPTURED_CLAIMS, {
@@ -15563,6 +15679,9 @@ function validateBaseUrl(rawUrl, opts = {}) {
15563
15679
  if (host === ALLOWED_HOST_APEX || host.endsWith(ALLOWED_HOST_SUFFIX)) {
15564
15680
  return rawUrl;
15565
15681
  }
15682
+ if ((opts.additionalAllowedHosts ?? []).some((h) => host === h.toLowerCase())) {
15683
+ return rawUrl;
15684
+ }
15566
15685
  throw new InvalidBaseUrlError(
15567
15686
  `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.`
15568
15687
  );
@@ -15750,6 +15869,9 @@ var PER_ENV = {
15750
15869
  staging: { domainPrefix: "vectros-developer-staging", region: "us-east-1" },
15751
15870
  production: { domainPrefix: "vectros-developer", region: "us-east-1" }
15752
15871
  };
15872
+ var COGNITO_ALLOWED_HOSTS = Object.values(PER_ENV).map(
15873
+ (p) => `${p.domainPrefix}.auth.${p.region}.amazoncognito.com`
15874
+ );
15753
15875
  var BAKED_CLIENT_IDS = {
15754
15876
  staging: "61hjvm25kkjb1eei8t2d3jkcl8",
15755
15877
  // validated against deployed staging hosted UI (2026-06-05)
@@ -15764,13 +15886,30 @@ var MissingCognitoClientIdError = class extends Error {
15764
15886
  this.name = "MissingCognitoClientIdError";
15765
15887
  }
15766
15888
  };
15889
+ var REGION_SHAPE = /^[a-z]{2}(?:-[a-z]+)+-\d+$/;
15890
+ var InvalidCognitoRegionError = class extends Error {
15891
+ constructor(region) {
15892
+ super(
15893
+ `Invalid VECTROS_COGNITO_REGION ${JSON.stringify(region)} \u2014 expected an AWS region shape (e.g. "us-east-1").`
15894
+ );
15895
+ this.name = "InvalidCognitoRegionError";
15896
+ }
15897
+ };
15767
15898
  function resolveCognitoConfig(env) {
15768
15899
  const per = PER_ENV[env];
15769
- const region = process.env.VECTROS_COGNITO_REGION ?? per.region;
15770
- const domain = process.env.VECTROS_COGNITO_DOMAIN ?? `https://${per.domainPrefix}.auth.${region}.amazoncognito.com`;
15900
+ const domainOverride = process.env.VECTROS_COGNITO_DOMAIN;
15901
+ const regionOverride = process.env.VECTROS_COGNITO_REGION;
15902
+ if (domainOverride === void 0 && regionOverride !== void 0 && !REGION_SHAPE.test(regionOverride)) {
15903
+ throw new InvalidCognitoRegionError(regionOverride);
15904
+ }
15905
+ const region = regionOverride ?? per.region;
15906
+ const rawDomain = domainOverride ?? `https://${per.domainPrefix}.auth.${region}.amazoncognito.com`;
15907
+ const domain = validateBaseUrl(rawDomain, {
15908
+ additionalAllowedHosts: COGNITO_ALLOWED_HOSTS
15909
+ }).replace(/\/$/, "");
15771
15910
  const clientId = process.env.VECTROS_COGNITO_CLIENT_ID ?? BAKED_CLIENT_IDS[env];
15772
15911
  if (!clientId) throw new MissingCognitoClientIdError(env);
15773
- return { domain: domain.replace(/\/$/, ""), clientId, region };
15912
+ return { domain, clientId, region };
15774
15913
  }
15775
15914
  var LOOPBACK_PORTS = [8976, 8977, 8978];
15776
15915
  var OAUTH_SCOPES = ["openid", "email", "profile"];
@@ -16568,7 +16707,7 @@ async function withAuth(opts, deps, body) {
16568
16707
  log
16569
16708
  });
16570
16709
  } catch (err) {
16571
- if (err instanceof MissingCredentialError) {
16710
+ if (err instanceof MissingCredentialError || err instanceof InvalidBaseUrlError || err instanceof InvalidCognitoRegionError) {
16572
16711
  process.stderr.write(` \u2716 ${err.message}
16573
16712
  `);
16574
16713
  return 2;
@@ -16655,15 +16794,42 @@ var SCRIPT_CONFLICT_RETRY_DELAY_MS = 750;
16655
16794
  function isVersionConflict(err) {
16656
16795
  return typeof err === "object" && err !== null && err.statusCode === 409;
16657
16796
  }
16658
- async function readLatestScriptVersion(ctxClient, name) {
16659
- const versions = await drainPages(
16660
- (startFrom) => ctxClient.scripts.listScripts({ name, startFrom, limit: PAGE_LIMIT })
16797
+ function isForbidden(err) {
16798
+ return typeof err === "object" && err !== null && err.statusCode === 403;
16799
+ }
16800
+ function withAdoptionContext(err, name) {
16801
+ if (!(err instanceof Error) || !isForbidden(err)) return err;
16802
+ return new Error(
16803
+ `${err.message}
16804
+
16805
+ (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'.)`
16661
16806
  );
16662
- return versions.reduce((best, row) => {
16663
- if (row.scriptVersion === void 0) return best;
16664
- if (best === void 0 || best.scriptVersion === void 0) return row;
16665
- return row.scriptVersion > best.scriptVersion ? row : best;
16666
- }, void 0);
16807
+ }
16808
+ function isScriptNameNotFound(err) {
16809
+ return typeof err === "object" && err !== null && err.statusCode === 404;
16810
+ }
16811
+ function isScriptPageEnvelope(result) {
16812
+ return "data" in result;
16813
+ }
16814
+ async function readLatestScriptVersion(ctxClient, name) {
16815
+ let result;
16816
+ try {
16817
+ result = await ctxClient.scripts.listScripts({ name, latest: true, includeSource: true });
16818
+ } catch (err) {
16819
+ if (isScriptNameNotFound(err)) return void 0;
16820
+ throw err;
16821
+ }
16822
+ if (result == null) {
16823
+ throw new Error(
16824
+ `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.`
16825
+ );
16826
+ }
16827
+ if (isScriptPageEnvelope(result)) {
16828
+ throw new Error(
16829
+ `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.`
16830
+ );
16831
+ }
16832
+ return result;
16667
16833
  }
16668
16834
  function requireScriptId(row, name) {
16669
16835
  if (!row.id) {
@@ -16674,8 +16840,34 @@ function requireScriptId(row, name) {
16674
16840
  function scriptMatches(stored, declared) {
16675
16841
  return stored.source === declared.source && (stored.declaredInputContract ?? void 0) === declared.declaredInputContract;
16676
16842
  }
16843
+ var ScriptOwnershipConflictError = class extends Error {
16844
+ constructor(name, owner, blueprintName) {
16845
+ super(
16846
+ `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.`
16847
+ );
16848
+ this.name = "ScriptOwnershipConflictError";
16849
+ }
16850
+ };
16851
+ var ScriptAdoptionNotAllowedError = class extends Error {
16852
+ constructor(name) {
16853
+ super(
16854
+ `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.`
16855
+ );
16856
+ this.name = "ScriptAdoptionNotAllowedError";
16857
+ }
16858
+ };
16859
+ function assertScriptPushOwnable(name, latest, blueprintName, allowScriptAdoption) {
16860
+ if (latest === void 0) return { adopting: false };
16861
+ const owner = latest.provisionedBy?.trim() || void 0;
16862
+ if (owner === void 0) {
16863
+ if (!allowScriptAdoption) throw new ScriptAdoptionNotAllowedError(name);
16864
+ return { adopting: true };
16865
+ }
16866
+ if (owner !== blueprintName) throw new ScriptOwnershipConflictError(name, owner, blueprintName);
16867
+ return { adopting: false };
16868
+ }
16677
16869
  async function ensureScripts(args) {
16678
- const { ctxClient, scripts, log } = args;
16870
+ const { ctxClient, scripts, blueprintName, allowScriptAdoption, log } = args;
16679
16871
  const actions = [];
16680
16872
  for (const [name, entry] of Object.entries(scripts)) {
16681
16873
  let latest = await readLatestScriptVersion(ctxClient, name);
@@ -16686,18 +16878,32 @@ async function ensureScripts(args) {
16686
16878
  id: requireScriptId(latest, name),
16687
16879
  scriptVersion: latest.scriptVersion ?? 0
16688
16880
  });
16689
- log.debug(
16690
- { component: "loader", scriptName: name, scriptVersion: latest.scriptVersion },
16691
- "script already matches the declared source \u2014 no version pushed"
16692
- );
16881
+ const foreignOwner = latest.provisionedBy?.trim() || void 0;
16882
+ if (foreignOwner !== void 0 && foreignOwner !== blueprintName) {
16883
+ log.warn(
16884
+ { component: "loader", scriptName: name, provisionedBy: foreignOwner },
16885
+ `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`
16886
+ );
16887
+ } else {
16888
+ log.debug(
16889
+ { component: "loader", scriptName: name, scriptVersion: latest.scriptVersion },
16890
+ "script already matches the declared source \u2014 no version pushed"
16891
+ );
16892
+ }
16693
16893
  continue;
16694
16894
  }
16695
- const body = { name, source: entry.source, declaredInputContract: entry.declaredInputContract };
16895
+ let { adopting } = assertScriptPushOwnable(name, latest, blueprintName, allowScriptAdoption);
16896
+ const body = {
16897
+ name,
16898
+ source: entry.source,
16899
+ declaredInputContract: entry.declaredInputContract,
16900
+ provisionedBy: blueprintName
16901
+ };
16696
16902
  let pushed;
16697
16903
  try {
16698
16904
  pushed = await ctxClient.scripts.createScript(body);
16699
16905
  } catch (err) {
16700
- if (!isVersionConflict(err)) throw err;
16906
+ if (!isVersionConflict(err)) throw adopting ? withAdoptionContext(err, name) : err;
16701
16907
  log.debug(
16702
16908
  { component: "loader", scriptName: name },
16703
16909
  "concurrent push detected (409) \u2014 pausing, then re-reading the current version"
@@ -16723,10 +16929,11 @@ async function ensureScripts(args) {
16723
16929
  );
16724
16930
  continue;
16725
16931
  }
16932
+ ({ adopting } = assertScriptPushOwnable(name, latest, blueprintName, allowScriptAdoption));
16726
16933
  try {
16727
16934
  pushed = await ctxClient.scripts.createScript(body);
16728
16935
  } catch (retryErr) {
16729
- if (!isVersionConflict(retryErr)) throw retryErr;
16936
+ if (!isVersionConflict(retryErr)) throw adopting ? withAdoptionContext(retryErr, name) : retryErr;
16730
16937
  throw new Error(
16731
16938
  `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.`
16732
16939
  );
@@ -16738,13 +16945,13 @@ async function ensureScripts(args) {
16738
16945
  const firstVersion = latest === void 0;
16739
16946
  actions.push({
16740
16947
  name,
16741
- action: firstVersion ? "created" : "superseded",
16948
+ action: firstVersion ? "created" : adopting ? "adopted" : "superseded",
16742
16949
  id: pushed.id,
16743
16950
  scriptVersion: pushed.scriptVersion ?? 0
16744
16951
  });
16745
16952
  log.debug(
16746
16953
  { component: "loader", scriptName: name, scriptVersion: pushed.scriptVersion },
16747
- firstVersion ? "script pushed \u2014 first version for this name in this context" : "script source changed \u2014 new version pushed over an existing name"
16954
+ 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"
16748
16955
  );
16749
16956
  }
16750
16957
  return actions;
@@ -16915,9 +17122,10 @@ function describeNamespaceMismatch(decl, existing) {
16915
17122
  return mismatches.length > 0 ? mismatches.join("; ") : null;
16916
17123
  }
16917
17124
  var LostKeyError = class extends Error {
16918
- constructor(keyName) {
17125
+ constructor(keyName, rotateAttempted = false, revokedKeyId) {
17126
+ const revokedDisplay = revokedKeyId?.map((id) => `'${id}'`).join(", ");
16919
17127
  super(
16920
- `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.`
17128
+ `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.`)
16921
17129
  );
16922
17130
  this.keyName = keyName;
16923
17131
  this.name = "LostKeyError";
@@ -17125,6 +17333,7 @@ async function runLoader(client, input) {
17125
17333
  provisioned,
17126
17334
  readExistingKey,
17127
17335
  persistMintedKey,
17336
+ allowScriptAdoption = false,
17128
17337
  log
17129
17338
  } = input;
17130
17339
  assertNoUnresolvedIdentities(blueprint);
@@ -17275,7 +17484,13 @@ async function runLoader(client, input) {
17275
17484
  body: profileBody
17276
17485
  });
17277
17486
  log.debug({ component: "loader", principalId: `usr_${userId}`, accessProfileId: profile.id, profileAction }, "access profile ensured + reconciled to declared scopes");
17278
- const scriptActions = blueprint.scripts ? await ensureScripts({ ctxClient, scripts: blueprint.scripts, log }) : [];
17487
+ const scriptActions = blueprint.scripts ? await ensureScripts({
17488
+ ctxClient,
17489
+ scripts: blueprint.scripts,
17490
+ blueprintName: blueprint.name,
17491
+ allowScriptAdoption,
17492
+ log
17493
+ }) : [];
17279
17494
  const triggerActions = [];
17280
17495
  for (const [triggerName, entry] of Object.entries(blueprint.triggers ?? {})) {
17281
17496
  const schemaId = schemaIds[entry.firingSource.schemaName];
@@ -17386,9 +17601,45 @@ async function runLoader(client, input) {
17386
17601
  if (existing?.keyId) {
17387
17602
  await ctxClient.auth.revokeScopedKey({ keyId: existing.keyId });
17388
17603
  log.debug({ component: "loader", keyId: existing.keyId }, "revoked prior key for rotate");
17389
- revokedKeyId = existing.keyId;
17604
+ revokedKeyId = [existing.keyId];
17605
+ } else {
17606
+ let activeKeys;
17607
+ try {
17608
+ activeKeys = await drainPages(() => ctxClient.auth.listScopedKeys());
17609
+ } catch (err) {
17610
+ throw new Error(
17611
+ `--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.`,
17612
+ { cause: err }
17613
+ );
17614
+ }
17615
+ const matches = activeKeys.filter(
17616
+ (k) => k.contextId === blueprint.contextId && k.userId === userId && k.keyName === keyName && k.status === "active" && k.keyId
17617
+ );
17618
+ if (matches.length > 1) {
17619
+ log.warn(
17620
+ { component: "loader", keyName, contextId: blueprint.contextId, count: matches.length },
17621
+ "rotate: more than one active key matched this context/user/name tuple server-side \u2014 revoking all of them"
17622
+ );
17623
+ }
17624
+ if (matches.length > 0) {
17625
+ const revokedIds = [];
17626
+ for (const match of matches) {
17627
+ await ctxClient.auth.revokeScopedKey({ keyId: match.keyId });
17628
+ revokedIds.push(match.keyId);
17629
+ }
17630
+ log.debug(
17631
+ { component: "loader", keyIds: revokedIds },
17632
+ "revoked prior key(s) for rotate \u2014 resolved server-side, no local record existed"
17633
+ );
17634
+ revokedKeyId = revokedIds;
17635
+ } else {
17636
+ log.warn(
17637
+ { component: "loader", keyName, contextId: blueprint.contextId },
17638
+ "rotate: no local record and no matching active key found server-side \u2014 nothing to revoke"
17639
+ );
17640
+ }
17390
17641
  }
17391
- keyAction = "rotated";
17642
+ if (revokedKeyId) keyAction = "rotated";
17392
17643
  }
17393
17644
  const keyResp = await ctxClient.auth.createScopedKey({ keyName, tenantId, contextId: blueprint.contextId, userId });
17394
17645
  let rawKey;
@@ -17403,7 +17654,7 @@ async function runLoader(client, input) {
17403
17654
  } else {
17404
17655
  const existing = await readExistingKey(blueprint.contextId);
17405
17656
  if (!existing) {
17406
- throw new LostKeyError(keyName);
17657
+ throw new LostKeyError(keyName, rotate, revokedKeyId);
17407
17658
  }
17408
17659
  rawKey = existing.rawKey;
17409
17660
  keyId = existing.keyId;
@@ -17516,6 +17767,10 @@ async function runLoader(client, input) {
17516
17767
  seedIndexFailures
17517
17768
  };
17518
17769
  }
17770
+ function sanitizeForPlanDisplay(value, maxLen = 200) {
17771
+ const stripped = value.replace(/[\x00-\x1f\x7f]/g, "");
17772
+ return stripped.length > maxLen ? `${stripped.slice(0, maxLen)}\u2026` : stripped;
17773
+ }
17519
17774
  function renderPlanDataScope(ds) {
17520
17775
  const entries = Object.entries(ds ?? {});
17521
17776
  if (entries.length === 0) return "(none \u2014 unconstrained)";
@@ -17542,11 +17797,14 @@ function buildPlan(blueprint, opts = {}) {
17542
17797
  });
17543
17798
  }
17544
17799
  for (const iss of blueprint.issuers ?? []) {
17545
- const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
17546
- const captured = iss.capturedClaims?.length ? `, captured claims: [${iss.capturedClaims.join(", ")}]` : "";
17800
+ const issuerUrl = sanitizeForPlanDisplay(iss.issuer);
17801
+ const jwksNote = `, jwksUri: ${sanitizeForPlanDisplay(iss.jwksUri)}`;
17802
+ const userinfoNote = iss.userinfoUri ? `, userinfoUri: ${sanitizeForPlanDisplay(iss.userinfoUri)}` : "";
17803
+ const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: [${iss.selfSignupPolicies.map((p) => `${p.signupType}\u2192${p.roleId}`).join(", ")}]` : "";
17804
+ const captured = iss.capturedClaims?.length ? `, captured claims: [${iss.capturedClaims.map((c) => sanitizeForPlanDisplay(c)).join(", ")}]` : "";
17547
17805
  steps.push({
17548
17806
  kind: "issuer",
17549
- summary: `issuer '${iss.issuerId}' (${iss.issuer} \u2192 context '${iss.contextId}'${selfSignup}${captured}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
17807
+ summary: `issuer '${iss.issuerId}' (${issuerUrl} \u2192 context '${iss.contextId}'${jwksNote}${userinfoNote}${selfSignup}${captured}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
17550
17808
  });
17551
17809
  }
17552
17810
  for (const s of blueprint.schemas) {
@@ -17667,9 +17925,11 @@ function renderPlan(blueprint, steps) {
17667
17925
  ` immutable version and every earlier one persists, which is also how a 'latest' rule`,
17668
17926
  ` picks the change up. And scripts are NOT reconciled by absence: a script this blueprint`,
17669
17927
  ` previously shipped and no longer declares is LEFT IN PLACE, including every stored`,
17670
- ` version. A script row carries no provisioning marker, so an apply cannot tell one it`,
17671
- ` created from one you pushed by hand \u2014 and a wrong delete is unrecoverable and would`,
17672
- ` break any trigger still referencing it. Remove one deliberately with`,
17928
+ ` version. Each version does carry this blueprint's own provenance marker, and a push`,
17929
+ ` ONTO an already-marked-or-existing name is gated on it (see 'script' steps above) \u2014 but`,
17930
+ ` that gate has no bearing on DELETION: there is no second, principal-shaped factor to`,
17931
+ ` pair the marker with the way trigger rows have one, so a wrong delete stays unrecoverable`,
17932
+ ` and would break any trigger still referencing it. Remove one deliberately with`,
17673
17933
  ` 'vectros scripts delete', after 'vectros scripts list' shows you what is there.`
17674
17934
  );
17675
17935
  if (steps.some((s) => s.kind === "issuer")) {
@@ -17796,19 +18056,37 @@ var import_proper_lockfile = __toESM(require_proper_lockfile());
17796
18056
  // src/os-keychain.ts
17797
18057
  var KEYCHAIN_SERVICE = "vectros-cli";
17798
18058
  var PROBE_ACCOUNT = "__vectros_probe__";
17799
- async function loadKeychainBinding() {
18059
+ async function loadKeychainBinding(tryLoad = () => import("@napi-rs/keyring")) {
17800
18060
  try {
17801
- const mod = await import("@napi-rs/keyring");
18061
+ const mod = await tryLoad();
17802
18062
  if (typeof mod?.Entry === "function") return { binding: mod };
17803
- return { reason: "the @napi-rs/keyring package loaded but exposes no Entry \u2014 expected on no platform" };
18063
+ return {
18064
+ reason: "the @napi-rs/keyring package loaded but exposes no Entry \u2014 expected on no platform",
18065
+ installAccident: true
18066
+ };
17804
18067
  } catch (err) {
17805
- return { reason: loadFailureReason(err) };
18068
+ return { reason: loadFailureReason(err), installAccident: isInstallAccident(err) };
17806
18069
  }
17807
18070
  }
17808
- function loadFailureReason(err) {
18071
+ var PREBUILT_PLATFORMS = {
18072
+ darwin: ["arm64", "x64"],
18073
+ freebsd: ["x64"],
18074
+ linux: ["arm", "arm64", "riscv64", "x64"],
18075
+ win32: ["arm64", "ia32", "x64"]
18076
+ };
18077
+ function prebuiltShouldExist(platform = process.platform, arch = process.arch) {
18078
+ return PREBUILT_PLATFORMS[platform]?.includes(arch) ?? false;
18079
+ }
18080
+ function isInstallAccident(_err, platform = process.platform, arch = process.arch) {
18081
+ return prebuiltShouldExist(platform, arch);
18082
+ }
18083
+ function loadFailureReason(err, platform = process.platform, arch = process.arch) {
18084
+ if (!prebuiltShouldExist(platform, arch)) {
18085
+ return "the optional @napi-rs/keyring package is not installed for this platform";
18086
+ }
17809
18087
  const code = err.code;
17810
18088
  if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
17811
- return "the optional @napi-rs/keyring package is not installed for this platform";
18089
+ 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`;
17812
18090
  }
17813
18091
  return `the optional @napi-rs/keyring package is installed but would not load (${describeErr(err)})`;
17814
18092
  }
@@ -17932,8 +18210,8 @@ var OsKeychainSecretStore = class {
17932
18210
  * deleteCredential() -> false
17933
18211
  * => deleteSecret() -> false. The gate holds; `remove` refuses.
17934
18212
  *
17935
- * Reproduce with the recipe in GOTCHAS `gotcha-locked-credential-store-is-present
17936
- * -but-inert`. CI cannot pin this half — it unlocks the collection so the rest of
18213
+ * Reproduce by locking the credential store's collection while the CLI still holds a
18214
+ * cached handle to it. CI cannot pin this half — it unlocks the collection so the rest of
17937
18215
  * the suite can use it — so the evidence lives here rather than in a test.
17938
18216
  *
17939
18217
  * The WINDOWS half of the same contract was assumed for seven review rounds and
@@ -18375,7 +18653,11 @@ var KEYRING_BACKENDS = ["auto", "file"];
18375
18653
  async function resolveSecretStore(deps = {}) {
18376
18654
  const platform = deps.platform ?? process.platform;
18377
18655
  const env = deps.env ?? process.env;
18378
- const file = (reason2) => ({ kind: "crypto-file", reason: reason2 });
18656
+ const file = (reason2, installAccident2) => ({
18657
+ kind: "crypto-file",
18658
+ reason: reason2,
18659
+ ...installAccident2 ? { installAccident: installAccident2 } : {}
18660
+ });
18379
18661
  const requested = env[KEYRING_BACKEND_ENV]?.trim().toLowerCase();
18380
18662
  if (requested !== void 0 && requested !== "") {
18381
18663
  if (!KEYRING_BACKENDS.includes(requested)) {
@@ -18392,9 +18674,9 @@ async function resolveSecretStore(deps = {}) {
18392
18674
  return file("the macOS Keychain can prompt on unattended reads, which would break MCP startup");
18393
18675
  }
18394
18676
  const loadBinding0 = deps.loadBinding ?? loadKeychainBinding;
18395
- const { binding, reason } = await loadBinding0();
18677
+ const { binding, reason, installAccident } = await loadBinding0();
18396
18678
  if (!binding) {
18397
- return file(reason ?? "the optional @napi-rs/keyring package is not usable on this platform");
18679
+ return file(reason ?? "the optional @napi-rs/keyring package is not usable on this platform", installAccident);
18398
18680
  }
18399
18681
  const probe = deps.probe ?? probeKeychain;
18400
18682
  if (!probe(binding)) {
@@ -18423,6 +18705,9 @@ var Keyring = class _Keyring {
18423
18705
  stores;
18424
18706
  /** Where NEW secrets go, and why — surfaced by `keyring doctor`. */
18425
18707
  backend;
18708
+ warn;
18709
+ /** Fires at most once per instance — see {@link storeSecretSomewhere}. */
18710
+ installAccidentWarned = false;
18426
18711
  /**
18427
18712
  * Construct against EXPLICIT stores. The file store defaults to a
18428
18713
  * {@link CryptoFileSecretStore} over `dir` — no native binding, nothing outside
@@ -18437,6 +18722,8 @@ var Keyring = class _Keyring {
18437
18722
  "os-keychain": deps.keychainStore
18438
18723
  };
18439
18724
  this.backend = deps.selection ?? { kind: "crypto-file" };
18725
+ this.warn = deps.warn ?? ((msg) => process.stderr.write(`${msg}
18726
+ `));
18440
18727
  }
18441
18728
  /**
18442
18729
  * The keyring a command should use: the stores resolved for THIS machine per
@@ -18447,7 +18734,7 @@ var Keyring = class _Keyring {
18447
18734
  static async open(deps = {}) {
18448
18735
  if (deps.fileStore ?? deps.keychainStore ?? deps.selection) return new _Keyring(deps);
18449
18736
  const { keychain, ...selection } = await resolveSecretStore(deps);
18450
- return new _Keyring({ dir: deps.dir, keychainStore: keychain, selection });
18737
+ return new _Keyring({ dir: deps.dir, keychainStore: keychain, selection, warn: deps.warn });
18451
18738
  }
18452
18739
  /**
18453
18740
  * The store holding this entry's secret, or undefined when the stamp does not say
@@ -18564,6 +18851,28 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
18564
18851
  }
18565
18852
  return { landedIn, stranded: swept ? void 0 : prior, vetoedBy };
18566
18853
  }
18854
+ /**
18855
+ * Warn, at most once per instance, when this machine is about to write a secret to
18856
+ * the file store because of an INSTALL ACCIDENT — not because of an ordinary,
18857
+ * nothing-to-fix absence (unsupported platform, `VECTROS_KEYRING_BACKEND=file`,
18858
+ * macOS's own design choice, or "loaded fine but no OS credential store answers").
18859
+ *
18860
+ * Before this, the CLI degraded to the file store with ZERO signal at write
18861
+ * time, indistinguishable from every OTHER reason a machine ends up there — a
18862
+ * real `@napi-rs/keyring` install failure looked identical to a platform that
18863
+ * was never going to have one, unless you separately ran `keyring doctor`.
18864
+ * This is the signal that was missing, at the moment it matters (a real
18865
+ * secret is about to land somewhere weaker than intended), naming the one
18866
+ * remedy that actually applies to THIS case and no other.
18867
+ */
18868
+ warnOnceIfInstallAccident() {
18869
+ if (this.installAccidentWarned) return;
18870
+ if (this.backend.kind !== "crypto-file" || !this.backend.installAccident) return;
18871
+ this.installAccidentWarned = true;
18872
+ this.warn(
18873
+ `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.`
18874
+ );
18875
+ }
18567
18876
  /**
18568
18877
  * Persist a secret, preferring this machine's chosen store but NEVER losing the
18569
18878
  * secret if that store refuses. Returns where it actually landed.
@@ -18576,8 +18885,15 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
18576
18885
  * credential and leave it orphaned server-side with no local record of its
18577
18886
  * `keyId`. Degrading to the encrypted file is strictly better than that, and the
18578
18887
  * entry's stamp keeps the result honest rather than silently wrong.
18888
+ *
18889
+ * `warnIfDegraded` defaults to true and exists ONLY for {@link probeSecretStore}
18890
+ * to suppress with `false`: the once-per-instance install-accident warning is a
18891
+ * scarce resource (fires at most once per `Keyring`), and the probe's marker
18892
+ * value is never the credential the warning describes. Spending it on the probe
18893
+ * left the REAL write — the very next call, same instance — silent.
18579
18894
  */
18580
- async storeSecretSomewhere(ref, secret) {
18895
+ async storeSecretSomewhere(ref, secret, { warnIfDegraded = true } = {}) {
18896
+ if (warnIfDegraded) this.warnOnceIfInstallAccident();
18581
18897
  const preferred = this.preferred;
18582
18898
  if (preferred) {
18583
18899
  try {
@@ -18629,7 +18945,9 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
18629
18945
  async probeSecretStore() {
18630
18946
  const fileStore = this.stores["crypto-file"];
18631
18947
  const dirWasPresent = fileStore instanceof CryptoFileSecretStore ? await fileStore.secretsDirPresent() : true;
18632
- const landedIn = await this.storeSecretSomewhere(PROBE_SECRET_REF, PROBE_SECRET_VALUE);
18948
+ const landedIn = await this.storeSecretSomewhere(PROBE_SECRET_REF, PROBE_SECRET_VALUE, {
18949
+ warnIfDegraded: false
18950
+ });
18633
18951
  await this.stores[landedIn]?.deleteSecret(PROBE_SECRET_REF).catch(() => {
18634
18952
  });
18635
18953
  if (!dirWasPresent && fileStore instanceof CryptoFileSecretStore) {
@@ -18930,7 +19248,12 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
18930
19248
  async readSecret(entry) {
18931
19249
  const from = storeKindOf(entry);
18932
19250
  if (from === void 0) return void 0;
18933
- const secret = await this.stores[from]?.getSecret(entry.secretRef);
19251
+ const store = this.stores[from];
19252
+ if (!store) {
19253
+ this.warnOnceIfInstallAccident();
19254
+ return void 0;
19255
+ }
19256
+ const secret = await store.getSecret(entry.secretRef);
18934
19257
  if (secret === void 0) return void 0;
18935
19258
  if (from !== this.backend.kind) await this.migrateSecret(entry, from, secret);
18936
19259
  return secret;
@@ -19651,7 +19974,7 @@ function nowIso() {
19651
19974
  }
19652
19975
  function warnReplacingAlias(existing, alias, revokedKeyId) {
19653
19976
  if (!existing) return;
19654
- const stillLive = existing.keyId !== void 0 && existing.keyId !== revokedKeyId;
19977
+ const stillLive = existing.keyId !== void 0 && !(revokedKeyId ?? []).includes(existing.keyId);
19655
19978
  process.stdout.write(
19656
19979
  `
19657
19980
  \u26A0 Replacing keyring entry '${alias}' (was ${existing.principalId || "<unknown principal>"} in '${existing.context}' @ ${existing.env}, key ${existing.keyId}). Pass --alias to keep both.
@@ -19662,13 +19985,14 @@ function warnReplacingAlias(existing, alias, revokedKeyId) {
19662
19985
  }
19663
19986
  async function warnOtherAliasesNaming(keyring, revokedKeyId, exceptAlias) {
19664
19987
  const affected = (await keyring.list()).filter(
19665
- (e) => e.alias !== exceptAlias && e.keyId === revokedKeyId
19988
+ (e) => e.alias !== exceptAlias && e.keyId !== void 0 && revokedKeyId.includes(e.keyId)
19666
19989
  );
19667
19990
  if (affected.length === 0) return;
19668
19991
  const names = affected.map((e) => `'${e.alias}'`).join(", ");
19992
+ const revokedDisplay = revokedKeyId.length === 1 ? `Key ${revokedKeyId[0]}` : `Keys ${revokedKeyId.join(", ")}`;
19669
19993
  process.stderr.write(
19670
19994
  `
19671
- \u26A0 Key ${revokedKeyId} was just revoked, but ${affected.length === 1 ? "entry" : "entries"} ${names} still
19995
+ \u26A0 ${revokedDisplay} ${revokedKeyId.length === 1 ? "was" : "were"} just revoked, but ${affected.length === 1 ? "entry" : "entries"} ${names} still
19672
19996
  ${affected.length === 1 ? "names" : "name"} it. That secret is now dead \u2014 it keeps reading fine locally and
19673
19997
  will simply stop authenticating. Re-point ${affected.length === 1 ? "it" : "them"} with
19674
19998
  \`vectros key rotate --to-keyring --alias <name>\`, or drop with \`vectros keyring remove <name>\`.
@@ -19698,7 +20022,7 @@ async function storeMintedKey(keyring, binding, secret, opts = {}) {
19698
20022
  was kept too` : `the ${noun} did not
19699
20023
  confirm the delete`;
19700
20024
  const tidyUp = vetoNoun !== void 0 ? `Clear the copies by hand from your ${vetoNoun} and your ${noun}` : `Clear it by hand from your ${noun}`;
19701
- if (opts.revokedKeyId === stranded.keyId) {
20025
+ if (opts.revokedKeyId?.includes(stranded.keyId)) {
19702
20026
  process.stderr.write(
19703
20027
  `
19704
20028
  \u26A0 The previous key for '${entry.alias}' (${stranded.keyId}) was revoked and replaced, but
@@ -19769,6 +20093,7 @@ function parseBootstrapArgs(argv) {
19769
20093
  blankSlate: false,
19770
20094
  noSeed: false,
19771
20095
  allowTenantWideNamespaces: false,
20096
+ allowScriptAdoption: false,
19772
20097
  client: "desktop",
19773
20098
  tenant: "live",
19774
20099
  help: false
@@ -19833,6 +20158,9 @@ function parseBootstrapArgs(argv) {
19833
20158
  case "--allow-tenant-wide-namespaces":
19834
20159
  args.allowTenantWideNamespaces = true;
19835
20160
  break;
20161
+ case "--allow-script-adoption":
20162
+ args.allowScriptAdoption = true;
20163
+ break;
19836
20164
  case "--tenant": {
19837
20165
  const v = argv[++i];
19838
20166
  if (v !== "test" && v !== "live") {
@@ -19861,7 +20189,7 @@ function parseBootstrapArgs(argv) {
19861
20189
  return args;
19862
20190
  }
19863
20191
  function shouldOfferSeedOptOut(args, blueprint, isTty) {
19864
- return !args.noSeed && (blueprint.seed?.length ?? 0) > 0 && !args.yes && !args.noMerge && isTty;
20192
+ return !args.noSeed && (blueprint.seed?.length ?? 0) > 0 && !args.yes && isTty;
19865
20193
  }
19866
20194
  function seedDeclined(answer) {
19867
20195
  const a = answer.trim().toLowerCase();
@@ -19927,12 +20255,16 @@ function scriptTally(items) {
19927
20255
  if (items.length === 0) return null;
19928
20256
  const created = items.filter((i) => i.action === "created");
19929
20257
  const superseded = items.filter((i) => i.action === "superseded");
19930
- const unchanged = items.length - created.length - superseded.length;
20258
+ const adopted = items.filter((i) => i.action === "adopted");
20259
+ const unchanged = items.length - created.length - superseded.length - adopted.length;
19931
20260
  const seg = [];
19932
20261
  if (created.length) seg.push(`${created.length} first version${created.length === 1 ? "" : "s"} pushed`);
19933
20262
  if (superseded.length) {
19934
20263
  seg.push(`${superseded.map((i) => `'${i.name}'`).join(", ")} superseded (a new version now current)`);
19935
20264
  }
20265
+ if (adopted.length) {
20266
+ seg.push(`${adopted.map((i) => `'${i.name}'`).join(", ")} adopted (previously unmarked, now managed by this blueprint)`);
20267
+ }
19936
20268
  if (unchanged) seg.push(`${unchanged} already current \u2014 nothing written`);
19937
20269
  return `${items.length} script${items.length === 1 ? "" : "s"} (${seg.join("; ")})`;
19938
20270
  }
@@ -20004,6 +20336,14 @@ Options:
20004
20336
  credential can actually complete the registration either way \u2014
20005
20337
  this flag doesn't change who's allowed, only whether this run
20006
20338
  tries. Never needed for a blueprint with no tenantWide namespace.
20339
+ --allow-script-adoption
20340
+ Required to push a script version onto a name that already exists in
20341
+ this context but carries no blueprint provenance marker (hand-authored,
20342
+ or pushed before this marker existed) \u2014 without it, bootstrap refuses
20343
+ before minting anything for that name, naming it. Never needed for a
20344
+ brand-new script name, or for re-applying a blueprint that already owns
20345
+ the name it's pushing to. Does NOT help if the name is already owned by
20346
+ a DIFFERENT blueprint \u2014 that is refused unconditionally, no flag past it.
20007
20347
  --tenant test|live Which tenant the minted key scopes to (default: live).
20008
20348
  'test' provisions into your isolated test tenant \u2014 a
20009
20349
  dry-run before pointing real data at the same blueprint.
@@ -20026,7 +20366,8 @@ Options:
20026
20366
  --yes, -y Skip confirmations (agent / CI path).
20027
20367
  --no-merge Still mints/reconciles \u2014 this only changes where the result goes:
20028
20368
  print the config snippet instead of merging it into your client's
20029
- config file. Not a dry run; see 'blueprint plan' for that.
20369
+ config file. Not a dry run; see 'blueprint plan' for that. Does NOT
20370
+ skip the 'Apply this plan?' confirmation \u2014 pass --yes for that.
20030
20371
  --rotate Revoke + re-mint THIS machine's key.
20031
20372
  --confirm-existing-principal
20032
20373
  Required (with --yes, or with no TTY) to mint a NEW key bound to a
@@ -20226,7 +20567,7 @@ ${renderPlan(blueprint, planSteps)}
20226
20567
  `);
20227
20568
  }
20228
20569
  }
20229
- if (!args.yes && !args.noMerge && isTty) {
20570
+ if (!args.yes && isTty) {
20230
20571
  const ans = (await promptCtx.prompt("\n Apply this plan? [y/N]: ")).trim().toLowerCase();
20231
20572
  if (ans !== "y" && ans !== "yes") {
20232
20573
  process.stdout.write(" Aborted \u2014 nothing minted.\n");
@@ -20360,6 +20701,7 @@ ${renderPlan(blueprint, planSteps)}
20360
20701
  machineId,
20361
20702
  rotate: args.rotate,
20362
20703
  skipSeed: noSeed,
20704
+ allowScriptAdoption: args.allowScriptAdoption,
20363
20705
  readExistingKey,
20364
20706
  persistMintedKey,
20365
20707
  provisioned,
@@ -21573,7 +21915,7 @@ ${list}
21573
21915
  return storeAndReport(dest, key, auth, {
21574
21916
  ...opts,
21575
21917
  principalId: `usr_${userId}`,
21576
- revokedKeyId: existing?.keyId
21918
+ revokedKeyId: existing?.keyId ? [existing.keyId] : void 0
21577
21919
  });
21578
21920
  emitMintedKey(key, opts.format);
21579
21921
  return 0;
@@ -23245,6 +23587,7 @@ async function runScriptsList(opts, deps = {}) {
23245
23587
  const rows = await drainPages(
23246
23588
  (startFrom) => client.scripts.listScripts({
23247
23589
  limit: PAGE_LIMIT,
23590
+ includeSource: true,
23248
23591
  ...opts.name ? { name: opts.name } : {},
23249
23592
  ...startFrom ? { startFrom } : {}
23250
23593
  })
@@ -23305,10 +23648,9 @@ async function runScriptsDelete(opts, deps = {}) {
23305
23648
  `
23306
23649
  \u26A0 Deleting script version '${opts.id}' is permanent \u2014 the stored source is the only copy.
23307
23650
  Other versions of the same name are unaffected.
23308
- Nothing checks whether a trigger rule still references this version. A rule pinned to it
23309
- will fail on every firing; a rule on 'latest' will silently fall back to the previous
23310
- version and keep running older code. This CLI cannot list trigger rules, so confirm
23311
- against the API or the portal before removing a version a rule may still use.
23651
+ The platform refuses this delete if a trigger rule still references this version \u2014
23652
+ directly, or via 'latest' if this is the name's current newest version \u2014 and names the
23653
+ offending rule instead of deleting. If no rule references it, the delete proceeds.
23312
23654
  And if this is the name's LATEST version, its identity is REUSED: the next push of that
23313
23655
  name recomputes the same version number and the same id, so anything holding a pinned
23314
23656
  '${opts.id}' or a name@version reference will silently resolve to different source.
@@ -23477,7 +23819,7 @@ async function runLogin(opts, deps = {}) {
23477
23819
  try {
23478
23820
  config = resolveCognitoConfig(opts.env);
23479
23821
  } catch (err) {
23480
- if (err instanceof MissingCognitoClientIdError) {
23822
+ if (err instanceof MissingCognitoClientIdError || err instanceof InvalidBaseUrlError || err instanceof InvalidCognitoRegionError) {
23481
23823
  process.stderr.write(` \u2716 ${err.message}
23482
23824
  `);
23483
23825
  return 2;
@@ -24070,6 +24412,11 @@ async function runBlueprintHarness(client, input, deps) {
24070
24412
  // recording that it happened and nothing able to restore it. Opt out rather than grow the
24071
24413
  // snapshot a leg teardown could not honour anyway.
24072
24414
  pruneTriggers: false
24415
+ // `allowScriptAdoption` is left at its default `false` — the SAME created-only contract above
24416
+ // applies here: this harness may run against a shared tenant, and adopting a pre-existing,
24417
+ // unmarked script name would start this run's teardown treating (and, worse, later deleting)
24418
+ // versions on a name it does not own. A test blueprint that needs `scripts:` should ship a
24419
+ // fresh, collision-free name, exactly as the harness already expects for schemas/roles/issuers.
24073
24420
  });
24074
24421
  log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
24075
24422
  assertions = await runAssertions(
@@ -24283,7 +24630,7 @@ async function issueOrRotateKey(client, tenantId, context, userId, keyName) {
24283
24630
  "The key already exists and its secret could not be re-minted (the prior key may not have cleared yet). Retry shortly."
24284
24631
  );
24285
24632
  }
24286
- return { key: rotated, revokedKeyId: existing?.keyId };
24633
+ return { key: rotated, revokedKeyId: existing?.keyId ? [existing.keyId] : void 0 };
24287
24634
  }
24288
24635
  async function runJoin(opts, deps = {}) {
24289
24636
  const hasRole = Boolean(opts.role?.length);
@@ -24430,9 +24777,9 @@ function emitJoined(opts) {
24430
24777
 
24431
24778
  // src/build-info.ts
24432
24779
  var BUILD_INFO = {
24433
- cli: true ? "0.21.0" : "dev",
24434
- sdk: true ? "0.43.0" : "dev",
24435
- blueprints: true ? "0.18.0" : "dev"
24780
+ cli: true ? "0.22.0" : "dev",
24781
+ sdk: true ? "0.44.0" : "dev",
24782
+ blueprints: true ? "0.19.0" : "dev"
24436
24783
  };
24437
24784
  function formatBuildInfo() {
24438
24785
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -24459,11 +24806,13 @@ function addCommonAuthOptions(cmd) {
24459
24806
  "emit machine-readable JSON instead of the human-readable view"
24460
24807
  );
24461
24808
  }
24809
+ var TENANT_FLAG_INVALID = /* @__PURE__ */ Symbol("parseTenantFlag:invalid");
24462
24810
  function parseTenantFlag(raw) {
24463
24811
  if (raw !== void 0 && raw !== "test" && raw !== "live") {
24464
24812
  process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${raw}')
24465
24813
  `);
24466
- process.exit(2);
24814
+ process.exitCode = 2;
24815
+ return TENANT_FLAG_INVALID;
24467
24816
  }
24468
24817
  return raw;
24469
24818
  }
@@ -24484,14 +24833,14 @@ function buildProgram(deps = {}) {
24484
24833
  );
24485
24834
  const login = program.command("login").description("Sign in via your browser (Cognito PKCE) and store a session.").addOption(new import_commander.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)");
24486
24835
  login.action(async (flags) => {
24487
- process.exit(await runLogin({ env: flags.env, json: false, noBrowser: flags.browser === false }));
24836
+ process.exitCode = await runLogin({ env: flags.env, json: false, noBrowser: flags.browser === false });
24488
24837
  });
24489
24838
  program.command("logout").description("Clear the stored login session.").action(async () => {
24490
- process.exit(await runLogout({ env: "production", json: false }));
24839
+ process.exitCode = await runLogout({ env: "production", json: false });
24491
24840
  });
24492
24841
  const whoami = program.command("whoami").description("Show the authenticated session (tenant, env, principal).");
24493
24842
  addCommonAuthOptions(whoami).action(async (flags) => {
24494
- process.exit(await runWhoami(toCommonOptions(flags), deps));
24843
+ process.exitCode = await runWhoami(toCommonOptions(flags), deps);
24495
24844
  });
24496
24845
  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)");
24497
24846
  addCommonAuthOptions(blueprintTest).action(
@@ -24499,14 +24848,13 @@ function buildProgram(deps = {}) {
24499
24848
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
24500
24849
  process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
24501
24850
  `);
24502
- process.exit(2);
24503
- }
24504
- process.exit(
24505
- await runBlueprintTest(
24506
- file,
24507
- { ...toCommonOptions(flags), keep: Boolean(flags.keep), tenant: flags.tenant },
24508
- deps
24509
- )
24851
+ process.exitCode = 2;
24852
+ return;
24853
+ }
24854
+ process.exitCode = await runBlueprintTest(
24855
+ file,
24856
+ { ...toCommonOptions(flags), keep: Boolean(flags.keep), tenant: flags.tenant },
24857
+ deps
24510
24858
  );
24511
24859
  }
24512
24860
  );
@@ -24527,9 +24875,7 @@ function registerAssumeCommand(program, deps) {
24527
24875
  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, []);
24528
24876
  addCommonAuthOptions(assume).action(
24529
24877
  async (flags) => {
24530
- process.exit(
24531
- await runAssume({ ...toCommonOptions(flags), context: flags.context, scope: flags.scope ?? [] }, deps)
24532
- );
24878
+ process.exitCode = await runAssume({ ...toCommonOptions(flags), context: flags.context, scope: flags.scope ?? [] }, deps);
24533
24879
  }
24534
24880
  );
24535
24881
  }
@@ -24543,23 +24889,22 @@ function registerJoinCommand(program, deps) {
24543
24889
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
24544
24890
  process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
24545
24891
  `);
24546
- process.exit(2);
24892
+ process.exitCode = 2;
24893
+ return;
24547
24894
  }
24548
- process.exit(
24549
- await runJoin(
24550
- {
24551
- ...toCommonOptions(flags),
24552
- contextArg,
24553
- role: flags.role,
24554
- actions: flags.actions,
24555
- as: flags.as,
24556
- scope: flags.scope,
24557
- alias: flags.alias,
24558
- tenant: flags.tenant,
24559
- activate: flags.activate
24560
- },
24561
- deps
24562
- )
24895
+ process.exitCode = await runJoin(
24896
+ {
24897
+ ...toCommonOptions(flags),
24898
+ contextArg,
24899
+ role: flags.role,
24900
+ actions: flags.actions,
24901
+ as: flags.as,
24902
+ scope: flags.scope,
24903
+ alias: flags.alias,
24904
+ tenant: flags.tenant,
24905
+ activate: flags.activate
24906
+ },
24907
+ deps
24563
24908
  );
24564
24909
  }
24565
24910
  );
@@ -24568,29 +24913,27 @@ function registerKeyringCommands(program) {
24568
24913
  const KEYRING_SHOW_FORMATS = ["human", "env", "raw"];
24569
24914
  const keyring = program.command("keyring").description("Inspect and manage the local multi-principal keyring.");
24570
24915
  keyring.command("list").description("List keyring entries and which one is active.").option("--json", "emit machine-readable JSON").action(async (flags) => {
24571
- process.exit(await runKeyringList({ json: Boolean(flags.json) }));
24916
+ process.exitCode = await runKeyringList({ json: Boolean(flags.json) });
24572
24917
  });
24573
24918
  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) => {
24574
- process.exit(await runKeyringUse(alias, { json: Boolean(flags.json) }));
24919
+ process.exitCode = await runKeyringUse(alias, { json: Boolean(flags.json) });
24575
24920
  });
24576
24921
  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 import_commander.Option("--format <fmt>", "how to emit the key").choices(KEYRING_SHOW_FORMATS).default("human")).action(async (flags) => {
24577
- process.exit(await runKeyringShow({ alias: flags.alias, format: flags.format }));
24922
+ process.exitCode = await runKeyringShow({ alias: flags.alias, format: flags.format });
24578
24923
  });
24579
24924
  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) => {
24580
- process.exit(await runKeyringDoctor({ json: Boolean(flags.json) }));
24925
+ process.exitCode = await runKeyringDoctor({ json: Boolean(flags.json) });
24581
24926
  });
24582
24927
  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) => {
24583
- process.exit(
24584
- await runKeyringPrune({ dryRun: Boolean(flags.dryRun), force: Boolean(flags.force), json: Boolean(flags.json) })
24585
- );
24928
+ process.exitCode = await runKeyringPrune({ dryRun: Boolean(flags.dryRun), force: Boolean(flags.force), json: Boolean(flags.json) });
24586
24929
  });
24587
24930
  keyring.command("remove").description(
24588
24931
  "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."
24589
24932
  ).argument("<alias>", "the keyring entry to remove").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
24590
- process.exit(await runKeyringRemove(alias, { json: Boolean(flags.json) }));
24933
+ process.exitCode = await runKeyringRemove(alias, { json: Boolean(flags.json) });
24591
24934
  });
24592
24935
  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) => {
24593
- process.exit(await runKeyringUse(alias, { json: Boolean(flags.json) }));
24936
+ process.exitCode = await runKeyringUse(alias, { json: Boolean(flags.json) });
24594
24937
  });
24595
24938
  }
24596
24939
  function registerContextCommands(program, deps) {
@@ -24601,18 +24944,17 @@ function registerContextCommands(program, deps) {
24601
24944
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
24602
24945
  process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
24603
24946
  `);
24604
- process.exit(2);
24947
+ process.exitCode = 2;
24948
+ return;
24605
24949
  }
24606
- process.exit(
24607
- await runContextCreate(
24608
- {
24609
- ...toCommonOptions(flags),
24610
- contextId,
24611
- name: flags.name,
24612
- tenant: flags.tenant
24613
- },
24614
- deps
24615
- )
24950
+ process.exitCode = await runContextCreate(
24951
+ {
24952
+ ...toCommonOptions(flags),
24953
+ contextId,
24954
+ name: flags.name,
24955
+ tenant: flags.tenant
24956
+ },
24957
+ deps
24616
24958
  );
24617
24959
  }
24618
24960
  );
@@ -24621,13 +24963,12 @@ function registerContextCommands(program, deps) {
24621
24963
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
24622
24964
  process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
24623
24965
  `);
24624
- process.exit(2);
24966
+ process.exitCode = 2;
24967
+ return;
24625
24968
  }
24626
- process.exit(
24627
- await runContextList(
24628
- { ...toCommonOptions(flags), mine: Boolean(flags.mine), tenant: flags.tenant },
24629
- deps
24630
- )
24969
+ process.exitCode = await runContextList(
24970
+ { ...toCommonOptions(flags), mine: Boolean(flags.mine), tenant: flags.tenant },
24971
+ deps
24631
24972
  );
24632
24973
  });
24633
24974
  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)");
@@ -24635,13 +24976,12 @@ function registerContextCommands(program, deps) {
24635
24976
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
24636
24977
  process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
24637
24978
  `);
24638
- process.exit(2);
24979
+ process.exitCode = 2;
24980
+ return;
24639
24981
  }
24640
- process.exit(
24641
- await runContextGet(
24642
- { ...toCommonOptions(flags), contextId, tenant: flags.tenant },
24643
- deps
24644
- )
24982
+ process.exitCode = await runContextGet(
24983
+ { ...toCommonOptions(flags), contextId, tenant: flags.tenant },
24984
+ deps
24645
24985
  );
24646
24986
  });
24647
24987
  const destroy = context.command("destroy").description(
@@ -24652,18 +24992,17 @@ function registerContextCommands(program, deps) {
24652
24992
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
24653
24993
  process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
24654
24994
  `);
24655
- process.exit(2);
24995
+ process.exitCode = 2;
24996
+ return;
24656
24997
  }
24657
- process.exit(
24658
- await runContextDestroy(
24659
- {
24660
- ...toCommonOptions(flags),
24661
- contextId,
24662
- force: Boolean(flags.force),
24663
- tenant: flags.tenant
24664
- },
24665
- deps
24666
- )
24998
+ process.exitCode = await runContextDestroy(
24999
+ {
25000
+ ...toCommonOptions(flags),
25001
+ contextId,
25002
+ force: Boolean(flags.force),
25003
+ tenant: flags.tenant
25004
+ },
25005
+ deps
24667
25006
  );
24668
25007
  }
24669
25008
  );
@@ -24674,18 +25013,16 @@ function registerIssuersCommands(program, deps) {
24674
25013
  addCommonAuthOptions(list).action(
24675
25014
  async (flags) => {
24676
25015
  const tenant = parseTenantFlag(flags.tenant);
24677
- process.exit(
24678
- await runIssuersList({ ...toCommonOptions(flags), context: flags.context, tenant }, deps)
24679
- );
25016
+ if (tenant === TENANT_FLAG_INVALID) return;
25017
+ process.exitCode = await runIssuersList({ ...toCommonOptions(flags), context: flags.context, tenant }, deps);
24680
25018
  }
24681
25019
  );
24682
25020
  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)");
24683
25021
  addCommonAuthOptions(get).action(
24684
25022
  async (issuerId, flags) => {
24685
25023
  const tenant = parseTenantFlag(flags.tenant);
24686
- process.exit(
24687
- await runIssuersGet({ ...toCommonOptions(flags), context: flags.context, issuerId, tenant }, deps)
24688
- );
25024
+ if (tenant === TENANT_FLAG_INVALID) return;
25025
+ process.exitCode = await runIssuersGet({ ...toCommonOptions(flags), context: flags.context, issuerId, tenant }, deps);
24689
25026
  }
24690
25027
  );
24691
25028
  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(
@@ -24701,24 +25038,23 @@ function registerIssuersCommands(program, deps) {
24701
25038
  addCommonAuthOptions(register).action(
24702
25039
  async (flags) => {
24703
25040
  const tenant = parseTenantFlag(flags.tenant);
24704
- process.exit(
24705
- await runIssuersRegister(
24706
- {
24707
- ...toCommonOptions(flags),
24708
- context: flags.context,
24709
- issuerId: flags.issuerId,
24710
- issuer: flags.issuer,
24711
- jwksUri: flags.jwksUri,
24712
- audience: flags.audience,
24713
- subClaim: flags.subClaim,
24714
- emailClaim: flags.emailClaim,
24715
- userinfoUri: flags.userinfoUri,
24716
- selfSignupPolicy: flags.selfSignupPolicy,
24717
- capturedClaim: flags.capturedClaim,
24718
- tenant
24719
- },
24720
- deps
24721
- )
25041
+ if (tenant === TENANT_FLAG_INVALID) return;
25042
+ process.exitCode = await runIssuersRegister(
25043
+ {
25044
+ ...toCommonOptions(flags),
25045
+ context: flags.context,
25046
+ issuerId: flags.issuerId,
25047
+ issuer: flags.issuer,
25048
+ jwksUri: flags.jwksUri,
25049
+ audience: flags.audience,
25050
+ subClaim: flags.subClaim,
25051
+ emailClaim: flags.emailClaim,
25052
+ userinfoUri: flags.userinfoUri,
25053
+ selfSignupPolicy: flags.selfSignupPolicy,
25054
+ capturedClaim: flags.capturedClaim,
25055
+ tenant
25056
+ },
25057
+ deps
24722
25058
  );
24723
25059
  }
24724
25060
  );
@@ -24735,24 +25071,23 @@ function registerIssuersCommands(program, deps) {
24735
25071
  addCommonAuthOptions(update).action(
24736
25072
  async (issuerId, flags) => {
24737
25073
  const tenant = parseTenantFlag(flags.tenant);
24738
- process.exit(
24739
- await runIssuersUpdate(
24740
- {
24741
- ...toCommonOptions(flags),
24742
- context: flags.context,
24743
- issuerId,
24744
- subClaim: flags.subClaim,
24745
- emailClaim: flags.emailClaim,
24746
- userinfoUri: flags.userinfoUri,
24747
- status: flags.status,
24748
- selfSignupPolicy: flags.selfSignupPolicy,
24749
- clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
24750
- capturedClaim: flags.capturedClaim,
24751
- clearCapturedClaims: Boolean(flags.clearCapturedClaims),
24752
- tenant
24753
- },
24754
- deps
24755
- )
25074
+ if (tenant === TENANT_FLAG_INVALID) return;
25075
+ process.exitCode = await runIssuersUpdate(
25076
+ {
25077
+ ...toCommonOptions(flags),
25078
+ context: flags.context,
25079
+ issuerId,
25080
+ subClaim: flags.subClaim,
25081
+ emailClaim: flags.emailClaim,
25082
+ userinfoUri: flags.userinfoUri,
25083
+ status: flags.status,
25084
+ selfSignupPolicy: flags.selfSignupPolicy,
25085
+ clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
25086
+ capturedClaim: flags.capturedClaim,
25087
+ clearCapturedClaims: Boolean(flags.clearCapturedClaims),
25088
+ tenant
25089
+ },
25090
+ deps
24756
25091
  );
24757
25092
  }
24758
25093
  );
@@ -24760,11 +25095,10 @@ function registerIssuersCommands(program, deps) {
24760
25095
  addCommonAuthOptions(del).action(
24761
25096
  async (issuerId, flags) => {
24762
25097
  const tenant = parseTenantFlag(flags.tenant);
24763
- process.exit(
24764
- await runIssuersDelete(
24765
- { ...toCommonOptions(flags), context: flags.context, issuerId, force: Boolean(flags.force), tenant },
24766
- deps
24767
- )
25098
+ if (tenant === TENANT_FLAG_INVALID) return;
25099
+ process.exitCode = await runIssuersDelete(
25100
+ { ...toCommonOptions(flags), context: flags.context, issuerId, force: Boolean(flags.force), tenant },
25101
+ deps
24768
25102
  );
24769
25103
  }
24770
25104
  );
@@ -24780,33 +25114,30 @@ function registerNamespaceCommands(program, deps) {
24780
25114
  ` \u2716 --specificity-rank must be an integer between 0 and 1000000 (got '${flags.specificityRank}').
24781
25115
  `
24782
25116
  );
24783
- process.exit(2);
25117
+ process.exitCode = 2;
25118
+ return;
24784
25119
  }
24785
- process.exit(
24786
- await runNamespaceRegister(
24787
- {
24788
- ...toCommonOptions(flags),
24789
- namespace: name,
24790
- context: flags.context,
24791
- specificityRank,
24792
- entityBacked: Boolean(flags.entityBacked),
24793
- defaultSchemaId: flags.defaultSchemaId
24794
- },
24795
- deps
24796
- )
25120
+ process.exitCode = await runNamespaceRegister(
25121
+ {
25122
+ ...toCommonOptions(flags),
25123
+ namespace: name,
25124
+ context: flags.context,
25125
+ specificityRank,
25126
+ entityBacked: Boolean(flags.entityBacked),
25127
+ defaultSchemaId: flags.defaultSchemaId
25128
+ },
25129
+ deps
24797
25130
  );
24798
25131
  }
24799
25132
  );
24800
25133
  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");
24801
25134
  addCommonAuthOptions(list).action(async (flags) => {
24802
- process.exit(await runNamespaceList({ ...toCommonOptions(flags), context: flags.context }, deps));
25135
+ process.exitCode = await runNamespaceList({ ...toCommonOptions(flags), context: flags.context }, deps);
24803
25136
  });
24804
25137
  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");
24805
25138
  addCommonAuthOptions(get).action(
24806
25139
  async (name, flags) => {
24807
- process.exit(
24808
- await runNamespaceGet({ ...toCommonOptions(flags), namespace: name, context: flags.context }, deps)
24809
- );
25140
+ process.exitCode = await runNamespaceGet({ ...toCommonOptions(flags), namespace: name, context: flags.context }, deps);
24810
25141
  }
24811
25142
  );
24812
25143
  }
@@ -24815,59 +25146,59 @@ function registerScriptsCommands(program, deps) {
24815
25146
  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)");
24816
25147
  addCommonAuthOptions(push).action(
24817
25148
  async (name, flags) => {
24818
- process.exit(
24819
- await runScriptsPush(
24820
- {
24821
- ...toCommonOptions(flags),
24822
- name,
24823
- context: flags.context,
24824
- sourceFile: flags.sourceFile,
24825
- inputContract: flags.inputContract,
24826
- inputContractFile: flags.inputContractFile,
24827
- ifChanged: Boolean(flags.ifChanged),
24828
- tenant: parseTenantFlag(flags.tenant)
24829
- },
24830
- deps
24831
- )
25149
+ const tenant = parseTenantFlag(flags.tenant);
25150
+ if (tenant === TENANT_FLAG_INVALID) return;
25151
+ process.exitCode = await runScriptsPush(
25152
+ {
25153
+ ...toCommonOptions(flags),
25154
+ name,
25155
+ context: flags.context,
25156
+ sourceFile: flags.sourceFile,
25157
+ inputContract: flags.inputContract,
25158
+ inputContractFile: flags.inputContractFile,
25159
+ ifChanged: Boolean(flags.ifChanged),
25160
+ tenant
25161
+ },
25162
+ deps
24832
25163
  );
24833
25164
  }
24834
25165
  );
24835
25166
  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)");
24836
25167
  addCommonAuthOptions(list).action(
24837
25168
  async (flags) => {
24838
- process.exit(
24839
- await runScriptsList(
24840
- { ...toCommonOptions(flags), context: flags.context, name: flags.name, tenant: parseTenantFlag(flags.tenant) },
24841
- deps
24842
- )
25169
+ const tenant = parseTenantFlag(flags.tenant);
25170
+ if (tenant === TENANT_FLAG_INVALID) return;
25171
+ process.exitCode = await runScriptsList(
25172
+ { ...toCommonOptions(flags), context: flags.context, name: flags.name, tenant },
25173
+ deps
24843
25174
  );
24844
25175
  }
24845
25176
  );
24846
25177
  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)");
24847
25178
  addCommonAuthOptions(get).action(
24848
25179
  async (id, flags) => {
24849
- process.exit(
24850
- await runScriptsGet(
24851
- { ...toCommonOptions(flags), id, context: flags.context, tenant: parseTenantFlag(flags.tenant) },
24852
- deps
24853
- )
25180
+ const tenant = parseTenantFlag(flags.tenant);
25181
+ if (tenant === TENANT_FLAG_INVALID) return;
25182
+ process.exitCode = await runScriptsGet(
25183
+ { ...toCommonOptions(flags), id, context: flags.context, tenant },
25184
+ deps
24854
25185
  );
24855
25186
  }
24856
25187
  );
24857
25188
  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)");
24858
25189
  addCommonAuthOptions(del).action(
24859
25190
  async (id, flags) => {
24860
- process.exit(
24861
- await runScriptsDelete(
24862
- {
24863
- ...toCommonOptions(flags),
24864
- id,
24865
- context: flags.context,
24866
- tenant: parseTenantFlag(flags.tenant),
24867
- force: Boolean(flags.force)
24868
- },
24869
- deps
24870
- )
25191
+ const tenant = parseTenantFlag(flags.tenant);
25192
+ if (tenant === TENANT_FLAG_INVALID) return;
25193
+ process.exitCode = await runScriptsDelete(
25194
+ {
25195
+ ...toCommonOptions(flags),
25196
+ id,
25197
+ context: flags.context,
25198
+ tenant,
25199
+ force: Boolean(flags.force)
25200
+ },
25201
+ deps
24871
25202
  );
24872
25203
  }
24873
25204
  );
@@ -24887,9 +25218,11 @@ function registerRoleCommands(program, deps) {
24887
25218
  ).option("--description <text>", "free-text description").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24888
25219
  addCommonAuthOptions(create).action(
24889
25220
  async (flags) => {
25221
+ const tenant = parseTenantFlag(flags.tenant);
25222
+ if (tenant === TENANT_FLAG_INVALID) return;
24890
25223
  process.exitCode = await runRoleCreate({
24891
25224
  ...toCommonOptions(flags),
24892
- tenant: parseTenantFlag(flags.tenant),
25225
+ tenant,
24893
25226
  context: flags.context,
24894
25227
  roleId: flags.roleId,
24895
25228
  name: flags.name,
@@ -24903,16 +25236,20 @@ function registerRoleCommands(program, deps) {
24903
25236
  );
24904
25237
  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)");
24905
25238
  addCommonAuthOptions(list).action(async (flags) => {
25239
+ const tenant = parseTenantFlag(flags.tenant);
25240
+ if (tenant === TENANT_FLAG_INVALID) return;
24906
25241
  process.exitCode = await runRoleList(
24907
- { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context },
25242
+ { ...toCommonOptions(flags), tenant, context: flags.context },
24908
25243
  deps
24909
25244
  );
24910
25245
  });
24911
25246
  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)");
24912
25247
  addCommonAuthOptions(get).action(
24913
25248
  async (flags) => {
25249
+ const tenant = parseTenantFlag(flags.tenant);
25250
+ if (tenant === TENANT_FLAG_INVALID) return;
24914
25251
  process.exitCode = await runRoleGet(
24915
- { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context, roleId: flags.roleId },
25252
+ { ...toCommonOptions(flags), tenant, context: flags.context, roleId: flags.roleId },
24916
25253
  deps
24917
25254
  );
24918
25255
  }
@@ -24920,8 +25257,10 @@ function registerRoleCommands(program, deps) {
24920
25257
  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)");
24921
25258
  addAuthOptions(del).action(
24922
25259
  async (flags) => {
25260
+ const tenant = parseTenantFlag(flags.tenant);
25261
+ if (tenant === TENANT_FLAG_INVALID) return;
24923
25262
  process.exitCode = await runRoleDelete(
24924
- { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context, roleId: flags.roleId },
25263
+ { ...toCommonOptions(flags), tenant, context: flags.context, roleId: flags.roleId },
24925
25264
  deps
24926
25265
  );
24927
25266
  }
@@ -24932,9 +25271,11 @@ function registerAccessCommands(program, deps) {
24932
25271
  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)");
24933
25272
  addCommonAuthOptions(grant).action(
24934
25273
  async (flags) => {
25274
+ const tenant = parseTenantFlag(flags.tenant);
25275
+ if (tenant === TENANT_FLAG_INVALID) return;
24935
25276
  process.exitCode = await runAccessGrant({
24936
25277
  ...toCommonOptions(flags),
24937
- tenant: parseTenantFlag(flags.tenant),
25278
+ tenant,
24938
25279
  principal: flags.principal,
24939
25280
  context: flags.context,
24940
25281
  role: flags.role,
@@ -24947,8 +25288,10 @@ function registerAccessCommands(program, deps) {
24947
25288
  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)");
24948
25289
  addAuthOptions(revoke).action(
24949
25290
  async (flags) => {
25291
+ const tenant = parseTenantFlag(flags.tenant);
25292
+ if (tenant === TENANT_FLAG_INVALID) return;
24950
25293
  process.exitCode = await runAccessRevoke(
24951
- { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), principal: flags.principal, context: flags.context },
25294
+ { ...toCommonOptions(flags), tenant, principal: flags.principal, context: flags.context },
24952
25295
  deps
24953
25296
  );
24954
25297
  }
@@ -24956,8 +25299,10 @@ function registerAccessCommands(program, deps) {
24956
25299
  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)");
24957
25300
  addCommonAuthOptions(list).action(
24958
25301
  async (flags) => {
25302
+ const tenant = parseTenantFlag(flags.tenant);
25303
+ if (tenant === TENANT_FLAG_INVALID) return;
24959
25304
  process.exitCode = await runAccessList(
24960
- { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context, principal: flags.principal },
25305
+ { ...toCommonOptions(flags), tenant, context: flags.context, principal: flags.principal },
24961
25306
  deps
24962
25307
  );
24963
25308
  }
@@ -24965,8 +25310,10 @@ function registerAccessCommands(program, deps) {
24965
25310
  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)");
24966
25311
  addCommonAuthOptions(get).action(
24967
25312
  async (flags) => {
25313
+ const tenant = parseTenantFlag(flags.tenant);
25314
+ if (tenant === TENANT_FLAG_INVALID) return;
24968
25315
  process.exitCode = await runAccessGet(
24969
- { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), principal: flags.principal, context: flags.context },
25316
+ { ...toCommonOptions(flags), tenant, principal: flags.principal, context: flags.context },
24970
25317
  deps
24971
25318
  );
24972
25319
  }
@@ -24974,8 +25321,10 @@ function registerAccessCommands(program, deps) {
24974
25321
  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)");
24975
25322
  addCommonAuthOptions(explain).action(
24976
25323
  async (flags) => {
25324
+ const tenant = parseTenantFlag(flags.tenant);
25325
+ if (tenant === TENANT_FLAG_INVALID) return;
24977
25326
  process.exitCode = await runAccessExplain(
24978
- { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), principal: flags.principal, context: flags.context },
25327
+ { ...toCommonOptions(flags), tenant, principal: flags.principal, context: flags.context },
24979
25328
  deps
24980
25329
  );
24981
25330
  }
@@ -24992,31 +25341,27 @@ function registerIdentityCommands(program, deps) {
24992
25341
  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");
24993
25342
  addCommonAuthOptions(create).action(
24994
25343
  async (flags) => {
24995
- process.exit(
24996
- await runIdentityCreate({
24997
- ...toCommonOptions(flags),
24998
- type: flags.type,
24999
- externalId: flags.externalId,
25000
- name: flags.name,
25001
- email: flags.email,
25002
- service: flags.service,
25003
- scope: flags.scope,
25004
- metadata: flags.metadata
25005
- }, deps)
25006
- );
25344
+ process.exitCode = await runIdentityCreate({
25345
+ ...toCommonOptions(flags),
25346
+ type: flags.type,
25347
+ externalId: flags.externalId,
25348
+ name: flags.name,
25349
+ email: flags.email,
25350
+ service: flags.service,
25351
+ scope: flags.scope,
25352
+ metadata: flags.metadata
25353
+ }, deps);
25007
25354
  }
25008
25355
  );
25009
25356
  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));
25010
25357
  addCommonAuthOptions(list).action(
25011
25358
  async (flags) => {
25012
- process.exit(
25013
- await runIdentityList({
25014
- ...toCommonOptions(flags),
25015
- type: flags.type,
25016
- externalId: flags.externalId,
25017
- limit: flags.limit
25018
- }, deps)
25019
- );
25359
+ process.exitCode = await runIdentityList({
25360
+ ...toCommonOptions(flags),
25361
+ type: flags.type,
25362
+ externalId: flags.externalId,
25363
+ limit: flags.limit
25364
+ }, deps);
25020
25365
  }
25021
25366
  );
25022
25367
  const listAll = identity.command("list-all").description(
@@ -25025,38 +25370,35 @@ function registerIdentityCommands(program, deps) {
25025
25370
  addCommonAuthOptions(listAll).action(
25026
25371
  async (flags) => {
25027
25372
  const tenant = parseTenantFlag(flags.tenant);
25028
- process.exit(
25029
- await runIdentityListAll(
25030
- { ...toCommonOptions(flags), externalId: flags.externalId, limit: flags.limit, tenant },
25031
- deps
25032
- )
25373
+ if (tenant === TENANT_FLAG_INVALID) return;
25374
+ process.exitCode = await runIdentityListAll(
25375
+ { ...toCommonOptions(flags), externalId: flags.externalId, limit: flags.limit, tenant },
25376
+ deps
25033
25377
  );
25034
25378
  }
25035
25379
  );
25036
25380
  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)");
25037
25381
  addCommonAuthOptions(get).action(async (flags) => {
25038
- process.exit(await runIdentityGet({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));
25382
+ process.exitCode = await runIdentityGet({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps);
25039
25383
  });
25040
25384
  const del = identity.command("delete").description("Delete a principal by its Vectros id.").addOption(dimensionOption()).requiredOption("--id <vectrosId>", "the Vectros-assigned id");
25041
25385
  addAuthOptions(del).action(async (flags) => {
25042
- process.exit(await runIdentityDelete({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));
25386
+ process.exitCode = await runIdentityDelete({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps);
25043
25387
  });
25044
25388
  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");
25045
25389
  addCommonAuthOptions(invite).action(
25046
25390
  async (flags) => {
25047
- process.exit(
25048
- await runIdentityInvite(
25049
- {
25050
- ...toCommonOptions(flags),
25051
- context: flags.context,
25052
- role: flags.role,
25053
- email: flags.email,
25054
- firstName: flags.firstName,
25055
- acceptUrl: flags.acceptUrl,
25056
- noSendEmail: flags.sendEmail === false
25057
- },
25058
- deps
25059
- )
25391
+ process.exitCode = await runIdentityInvite(
25392
+ {
25393
+ ...toCommonOptions(flags),
25394
+ context: flags.context,
25395
+ role: flags.role,
25396
+ email: flags.email,
25397
+ firstName: flags.firstName,
25398
+ acceptUrl: flags.acceptUrl,
25399
+ noSendEmail: flags.sendEmail === false
25400
+ },
25401
+ deps
25060
25402
  );
25061
25403
  }
25062
25404
  );
@@ -25082,17 +25424,16 @@ function registerKeyCommands(program, deps) {
25082
25424
  );
25083
25425
  addAuthOptions(issue).action(async (flags) => {
25084
25426
  const tenant = parseTenantFlag(flags.tenant);
25085
- process.exit(
25086
- await runKeyIssue({
25087
- ...toCommonOptions(flags),
25088
- principal: flags.principal,
25089
- context: flags.context,
25090
- name: flags.name,
25091
- label: flags.label,
25092
- tenant,
25093
- ...toKeyDestination(flags)
25094
- }, deps)
25095
- );
25427
+ if (tenant === TENANT_FLAG_INVALID) return;
25428
+ process.exitCode = await runKeyIssue({
25429
+ ...toCommonOptions(flags),
25430
+ principal: flags.principal,
25431
+ context: flags.context,
25432
+ name: flags.name,
25433
+ label: flags.label,
25434
+ tenant,
25435
+ ...toKeyDestination(flags)
25436
+ }, deps);
25096
25437
  });
25097
25438
  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(
25098
25439
  "--all-contexts",
@@ -25102,20 +25443,20 @@ function registerKeyCommands(program, deps) {
25102
25443
  async (flags) => {
25103
25444
  if (flags.allContexts && flags.tenant !== void 0) {
25104
25445
  process.stderr.write(" \u2716 --tenant is not combinable with --all-contexts (which already spans both tenants).\n");
25105
- process.exit(2);
25446
+ process.exitCode = 2;
25447
+ return;
25106
25448
  }
25107
25449
  const tenant = parseTenantFlag(flags.tenant);
25108
- process.exit(
25109
- await runKeyList(
25110
- {
25111
- ...toCommonOptions(flags),
25112
- principal: flags.principal,
25113
- context: flags.context,
25114
- allContexts: Boolean(flags.allContexts),
25115
- tenant
25116
- },
25117
- deps
25118
- )
25450
+ if (tenant === TENANT_FLAG_INVALID) return;
25451
+ process.exitCode = await runKeyList(
25452
+ {
25453
+ ...toCommonOptions(flags),
25454
+ principal: flags.principal,
25455
+ context: flags.context,
25456
+ allContexts: Boolean(flags.allContexts),
25457
+ tenant
25458
+ },
25459
+ deps
25119
25460
  );
25120
25461
  }
25121
25462
  );
@@ -25123,14 +25464,16 @@ function registerKeyCommands(program, deps) {
25123
25464
  addCommonAuthOptions(get).action(
25124
25465
  async (keyId, flags) => {
25125
25466
  const tenant = parseTenantFlag(flags.tenant);
25126
- process.exit(await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
25467
+ if (tenant === TENANT_FLAG_INVALID) return;
25468
+ process.exitCode = await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps);
25127
25469
  }
25128
25470
  );
25129
25471
  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)");
25130
25472
  addAuthOptions(revoke).action(
25131
25473
  async (keyId, flags) => {
25132
25474
  const tenant = parseTenantFlag(flags.tenant);
25133
- process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
25475
+ if (tenant === TENANT_FLAG_INVALID) return;
25476
+ process.exitCode = await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps);
25134
25477
  }
25135
25478
  );
25136
25479
  const rotate = addKeyDestinationOptions(
@@ -25141,17 +25484,16 @@ function registerKeyCommands(program, deps) {
25141
25484
  );
25142
25485
  addAuthOptions(rotate).action(async (flags) => {
25143
25486
  const tenant = parseTenantFlag(flags.tenant);
25144
- process.exit(
25145
- await runKeyRotate({
25146
- ...toCommonOptions(flags),
25147
- principal: flags.principal,
25148
- context: flags.context,
25149
- name: flags.name,
25150
- forceNew: flags.forceNew,
25151
- tenant,
25152
- ...toKeyDestination(flags)
25153
- }, deps)
25154
- );
25487
+ if (tenant === TENANT_FLAG_INVALID) return;
25488
+ process.exitCode = await runKeyRotate({
25489
+ ...toCommonOptions(flags),
25490
+ principal: flags.principal,
25491
+ context: flags.context,
25492
+ name: flags.name,
25493
+ forceNew: flags.forceNew,
25494
+ tenant,
25495
+ ...toKeyDestination(flags)
25496
+ }, deps);
25155
25497
  });
25156
25498
  }
25157
25499
 
@@ -25164,7 +25506,8 @@ async function main() {
25164
25506
  const sub = process.argv[2];
25165
25507
  const passthrough = sub ? PASSTHROUGH_COMMANDS[sub] : void 0;
25166
25508
  if (passthrough) {
25167
- process.exit(await passthrough(process.argv.slice(3)));
25509
+ process.exitCode = await passthrough(process.argv.slice(3));
25510
+ return;
25168
25511
  }
25169
25512
  await buildProgram().parseAsync(process.argv);
25170
25513
  }
@@ -25175,6 +25518,6 @@ main().catch((err) => {
25175
25518
  process.stderr.write(`${err.stack}
25176
25519
  `);
25177
25520
  }
25178
- process.exit(2);
25521
+ process.exitCode = 2;
25179
25522
  });
25180
25523
  //# sourceMappingURL=cli.js.map