@vectros-ai/cli 0.21.0 → 0.23.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/AUTHORING.md CHANGED
@@ -5,9 +5,10 @@ A **blueprint** is a versioned, reviewed bundle for one use case: a schema set
5
5
  data, all with stable identifiers so applying it twice converges instead of
6
6
  duplicating. `vectros bootstrap --blueprint <file>` applies it to provision a
7
7
  ready-to-use data model + a narrow `ssk_*` scoped key — no root key, no manual
8
- admin steps.
8
+ admin steps. `vectros blueprint apply <file> --tenant test|live` applies the same
9
+ blueprint WITHOUT minting a key, for re-applying a change from CI.
9
10
 
10
- This guide is the authoring loop. Everything here except `bootstrap` is
11
+ This guide is the authoring loop. Everything here except `bootstrap` and `blueprint apply` is
11
12
  **creds-free and offline** — you can iterate on a blueprint with no account.
12
13
 
13
14
  > The examples use the bare `vectros` binary — install it once with
@@ -22,10 +23,11 @@ vectros blueprint init my-thing # 1. scaffold a starter
22
23
  vectros blueprint validate ./my-thing.blueprint.yaml # 2. structural + scope-gate + lint (offline)
23
24
  vectros blueprint plan ./my-thing.blueprint.yaml # 3. preview what it provisions (offline)
24
25
  vectros blueprint-test ./my-thing.blueprint.yaml --env staging # 4. prove it live (creds; auto-teardown)
25
- vectros bootstrap --blueprint ./my-thing.blueprint.yaml # 5. apply for real (needs a bridge token)
26
+ vectros bootstrap --blueprint ./my-thing.blueprint.yaml # 5. apply for real + mint a key (needs a bridge token)
27
+ vectros blueprint apply ./my-thing.blueprint.yaml --tenant test # 5'. apply for real, no key (CI: add --yes --confirm-existing-principal)
26
28
  ```
27
29
 
28
- Steps 1–3 are **creds-free and offline**. Step 4 (`blueprint-test`) and step 5 (`bootstrap`) hit a live
30
+ Steps 1–3 are **creds-free and offline**. Step 4 (`blueprint-test`) and step 5 (`bootstrap` / `blueprint apply`) hit a live
29
31
  environment — sign in once first with `vectros login` (`vectros whoami` shows the active session;
30
32
  `vectros logout` clears it), or pass `--token`.
31
33
 
@@ -93,7 +95,7 @@ A name must be 2–50 chars: a lowercase letter, then lowercase letters / digits
93
95
 
94
96
  ```
95
97
  Plan for blueprint 'my-thing' (v1.0.0) → context 'mcp':
96
- + schema 'thing' (2 fields, HYBRID) lookup: [externalId]
98
+ + schema 'thing' (1 field, HYBRID) lookup: [title]
97
99
  + context 'mcp' (MCP — my-thing)
98
100
  + principal 'my-thing-sp' (SERVICE)
99
101
  + profile scopes [records:r, records:c, records:u, search:r, schemas:r] (data-plane ✓)
@@ -107,7 +109,7 @@ sharing an action set can still carry different `dataScope`:
107
109
 
108
110
  ```
109
111
  Plan for blueprint 'my-thing' (v1.0.0) → context 'mcp':
110
- + schema 'thing' (2 fields, HYBRID) lookup: [externalId]
112
+ + schema 'thing' (1 field, HYBRID) lookup: [title]
111
113
  + context 'mcp' (MCP — my-thing)
112
114
  + principal 'my-thing-sp' (SERVICE)
113
115
  + role 'case-handler' (1 clause, actions [records:cru])
@@ -132,6 +134,16 @@ config snippet to stdout instead of merging it into your client's config file. T
132
134
  touching your account, stop at `blueprint plan` (offline) — `bootstrap`, with or without `--no-merge`,
133
135
  always applies.
134
136
 
137
+ **`vectros blueprint apply`** is the same pipeline without the key. `--tenant test|live` is required (no default);
138
+ it prints the account, tenant and context it is pointed at before anything changes and asks (with no terminal,
139
+ `--yes` is required). It **deletes nothing unless `--prune` is passed**: with it, a trigger the file no longer
140
+ declares is removed when its stored `provisionedBy` marker equals this blueprint's `name` (read from the file) and
141
+ its principal's external id is the one this blueprint would derive. Those are stored markers, not verified
142
+ identities, so a blueprint that reuses another's `name` and omits some of its triggers will remove them. An
143
+ existing service principal needs `--confirm-existing-principal` (standing consent in a pipeline). It writes the
144
+ blueprint's sample `seed` records **only when you pass `--seed`**, in either tenant (the opposite of `bootstrap`,
145
+ which seeds unless `--no-seed`); `--no-seed` is refused on `apply`.
146
+
135
147
  **Provisioning a production context — `--no-seed`.** A blueprint's `seed`
136
148
  records are illustrative sample data: handy for a getting-started or demo
137
149
  context, but synthetic clutter in a context that will hold real data. Pass
@@ -184,9 +196,10 @@ schemas:
184
196
  filterable: false # available as a query filter
185
197
  validation: { minLength: 1, maxLength: 200 } # server-enforced (ValidationRules)
186
198
  renderHints: { label: Title, widget: text, order: 1, displayField: true } # UI hints (displayField = the headline column)
187
- - fieldId: externalId
188
- fieldType: string
189
- required: true
199
+ # No `externalId` field: it is a first-class identifier the platform reserves. A seed
200
+ # sets its externalId at the TOP LEVEL of the seed (see `seed:` below), never as a
201
+ # schema field and never inside `fields`; declaring it makes the first seeded
202
+ # provision fail with "Required field 'externalId' cannot be empty."
190
203
  lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10);
191
204
  # object form { fieldName, unique?, rangeEnabled?,
192
205
  # sortBy?, allowOverflow? } for constraints/range, or
@@ -250,9 +263,9 @@ seed:
250
263
  typeName: thing
251
264
  externalId: seed-welcome # stable → idempotent; re-apply converges
252
265
  fields: { title: "Hello" }
253
- # Seeds are illustrative sample data. They're applied by default; provision a
254
- # production context with `bootstrap --no-seed` to skip them (schemas + key
255
- # still apply).
266
+ # Seeds are illustrative sample data. `bootstrap` applies them by default (pass
267
+ # `--no-seed` for a production context: schemas + key still apply); `blueprint
268
+ # apply` writes them only when you pass `--seed`.
256
269
 
257
270
  issuers:
258
271
  - issuerId: auth0-prod # your slug; 3-31 chars, lowercase letter first
@@ -264,9 +277,17 @@ issuers:
264
277
  # in the BOOTSTRAP-token phase, alongside app-context/service-principal
265
278
  # creation — NOT the same phase as schemas/accessProfile/roles above (see
266
279
  # BLUEPRINT_FIELD_PHASES, exported by @vectros-ai/blueprints). Registration
267
- # is create-once: re-applying an already-registered issuerId leaves it
268
- # unchanged — there's no update, so an edited jwksUri/audience on a live
269
- # issuer won't take effect via re-apply.
280
+ # is idempotent by issuerId, and a re-apply reconciles the editable fields
281
+ # (subClaim, emailClaim, userinfoUri, restrictedToDomain, selfSignupPolicies,
282
+ # capturedClaims). The trust-anchor fields (issuer, jwksUri, audience, contextId)
283
+ # are immutable: editing one on a registered issuer fails the apply with a
284
+ # 400, and rotating one means deleting the issuer and registering it again.
285
+ #
286
+ # On platform 0.45.0 and later an issuer without restrictedToDomain starts
287
+ # pending_verification and accepts no token until you prove you control the
288
+ # identity provider with `vectros issuers verify`. bootstrap still exits 0 and
289
+ # ends its output with the challenge and that command (--require-verified-issuers
290
+ # turns a pending issuer into exit 4). A restrictedToDomain issuer is active at once.
270
291
  #
271
292
  # Each entry's contextId must equal the blueprint's own contextId. An issuer
272
293
  # is a trust anchor — whoever controls its jwksUri can mint identities your
@@ -625,14 +646,47 @@ version and compares the stored `source` and `declaredInputContract` against
625
646
  what the blueprint declares, and pushes only on a real difference. So a re-apply
626
647
  of an unedited blueprint writes nothing, and editing `source` stores exactly one
627
648
  new version, which every `version: latest` rule then picks up. The apply summary
628
- reports which of the two happened per script.
629
-
630
- Two consequences worth planning for. A concurrent apply of the same blueprint
631
- can collide on one script name; the loader re-reads and retries once, so two
632
- runners applying identical content converge rather than storing it twice, but a
633
- sustained collision fails the apply and asks you to re-run. And **removing a
634
- `scripts:` entry does not delete anything** — every stored version stays, because
635
- a script carries no record of which blueprint pushed it. Remove one deliberately
649
+ reports which of `created` (no prior version) / `superseded` (this blueprint's
650
+ own prior version, replaced) / `adopted` (see below) / `unchanged` happened per
651
+ script.
652
+
653
+ **A THIRD qualification: pushing onto a name that already has a version is
654
+ gated on OWNING it.** A script name is a flat, per-context namespace shared
655
+ with every other blueprint and every hand-pushed script, so before superseding
656
+ one, the loader checks who pushed the current version:
657
+ - **owned by a different blueprint** — refused outright, no override. Ship
658
+ under a different name instead, or coordinate with whoever owns it.
659
+ - **no owner recorded** (hand-authored via `vectros scripts push`, or pushed
660
+ before this check existed) — refused unless the OPERATOR re-runs
661
+ `bootstrap` (or `blueprint apply`) with `--allow-script-adoption`. This is deliberately a
662
+ human-supplied CLI flag, never something a blueprint's own YAML/JSON can
663
+ set — the blueprint is untrusted input, and "I already own this name" is not
664
+ something it gets to assert for itself. A successful adopting push reports
665
+ `adopted`, distinct from an ordinary `superseded`.
666
+
667
+ **Know before you rely on this: adoption is safe for an account owner,
668
+ and refused for a Dev Portal sub-user adopting someone else's hand-push.**
669
+ The platform's own authorization still applies underneath this flag, and it
670
+ resolves caller identity differently for the two shapes. An account owner
671
+ always authenticates as the same identity regardless of which login session
672
+ minted the token, so adopting a script you (the owner) hand-pushed earlier —
673
+ or one nobody's identity is recorded against at all — succeeds. A distinct
674
+ team member (a sub-user) resolves to their own, different identity, and the
675
+ platform refuses to supersede an unmarked name pushed by anyone else — so
676
+ one sub-user cannot adopt a script a DIFFERENT sub-user hand-pushed, even
677
+ with this flag. If you hit that, either have the original pusher run the
678
+ adopting apply, or push the script under the account owner's own login
679
+ first.
680
+ - **owned by this SAME blueprint, or the name is brand new** — no flag needed;
681
+ this is the ordinary re-apply / first-push path.
682
+
683
+ Two more consequences worth planning for. A concurrent apply of the same
684
+ blueprint can collide on one script name; the loader re-reads and retries once,
685
+ so two runners applying identical content converge rather than storing it
686
+ twice, but a sustained collision fails the apply and asks you to re-run. And
687
+ **removing a `scripts:` entry does not delete anything** — every stored version
688
+ stays, because there is no second, principal-shaped factor to safely gate a
689
+ DELETE the way a trigger row's provenance marker does. Remove one deliberately
636
690
  with `vectros scripts delete`.
637
691
 
638
692
  ## Distribution
package/CHANGELOG.md CHANGED
@@ -3,6 +3,315 @@
3
3
  All notable changes to `@vectros-ai/cli` are documented here.
4
4
  This project adheres to [Semantic Versioning](https://semver.org).
5
5
 
6
+ ## 0.23.0 — 2026-09-22
7
+
8
+ ### Added
9
+
10
+ - **`vectros blueprint apply <file|name> --tenant test|live`** provisions a blueprint into a tenant WITHOUT minting a
11
+ key: the same credential, gates and loader as `vectros bootstrap`, with no scoped key created, no keyring opened
12
+ and no MCP client config touched, so a CI runner can re-apply a changed blueprint (a key is named per machine, so
13
+ a fresh runner would otherwise mint a new one every run). `--tenant` is required (no default), the account, tenant,
14
+ context and API are printed before anything changes, and with no terminal `--yes` is required (exit `2`).
15
+ **Deletion is opt-in:** nothing is deleted unless `--prune` is passed, and then only triggers whose stored
16
+ `provisionedBy` marker equals the blueprint's `name` and whose principal's external id is the one that blueprint
17
+ would derive, and which the file no longer declares. Those are stored markers, not verified identities, and the
18
+ name is read from the blueprint file after any input substitution, so `--set` and `--values` can change it. A
19
+ matched trigger that cannot be removed makes `apply` exit `1`. An existing service principal needs
20
+ `--confirm-existing-principal` with `--yes` or with no terminal (at a terminal it asks; standing consent in a
21
+ pipeline); every run that proceeds prints which principal it reused and why. **Sample seed records are opt-in:**
22
+ unlike `bootstrap`, `apply` writes none unless `--seed` is passed, in either tenant, and never asks; the plan and the
23
+ summary say whether they will be or were written, and `--no-seed` is refused with exit `2`
24
+ (`apply does not seed by default: pass --seed to write the sample records`). Exit codes `0` to `3`,
25
+ and `4` under `--require-verified-issuers`. The flags shared with `bootstrap` come from one table with a parity
26
+ test; key- and client-related flags are refused with a pointer to `bootstrap`. `bootstrap` itself is unchanged
27
+ except in what it prints: a trigger row with no blueprint marker, which it never deleted, is now reported as
28
+ "left in place" instead of "could NOT be removed … still live".
29
+
30
+ - **`vectros issuers verify <issuerId> --context <ctx>`** answers a `pending_verification` issuer's
31
+ challenge (platform 0.45.0 and later). A registration made without `restrictedToDomain` accepts no
32
+ token until you prove you control the identity provider: add an admin-controlled rule there that
33
+ puts the registration's one-time value in the claim it names, sign in once, and send the resulting
34
+ JWT. The JWT goes in `--idp-token-file <path>` (or `-`, or piped on stdin) or `--idp-token <jwt>`,
35
+ never `--token`, which stays your Vectros credential; a value on the command line is visible in your
36
+ shell history and the process list, so the flag warns. The token is checked once and never printed or
37
+ stored. Exit `0` verified, `1` the request did not report success (the platform's message, and the `issuers get`
38
+ command to check where the registration stands before retrying: a timeout can leave it verified), `2` a bad
39
+ input, reported before any request is made. A value given to `--idp-token-file` that looks like a token is
40
+ refused without being repeated, input over 64 KB is refused, `--idp-token-file -` is refused on a terminal,
41
+ and a mistyped flag or command carrying a token is redacted in the parser's error. Every command it prints for you to run carries the same `--tenant`,
42
+ `--env` and `--base-url` you gave it.
43
+ - **`issuers register` and `issuers get` show a pending registration's challenge**: the claim, the
44
+ one-time value and its expiry, how to add the rule at Auth0, Okta, Microsoft Entra ID or Keycloak, and
45
+ the exact `verify` command. `register` exits `0`, since a pending issuer is its expected result. An
46
+ issuer whose challenge already expired while pending is reported as unverifiable, with the delete
47
+ command, instead of instructions that cannot work. Nothing prints on a platform that predates the
48
+ challenge.
49
+ - **`vectros bootstrap` handles an issuer that needs verification.** A finished apply exits `0`; the
50
+ summary says `pending verification` beside the issuer, and the apply's last output, on stderr, is the
51
+ challenge and the `verify` command. `--require-verified-issuers` exits `4` instead, after the apply
52
+ completed (a one-line notice follows the challenge), for a pipeline that must not go green over an issuer that cannot yet sign anyone in. The
53
+ exit codes are now documented in `bootstrap --help`. The plan preview says up front that an issuer
54
+ without `restrictedToDomain` will start pending, and `vectros blueprint-test` treats a pending issuer
55
+ as a pass and says so.
56
+ - **`vectros bootstrap` applies a blueprint issuer's `restrictedToDomain`** (new in
57
+ `@vectros-ai/blueprints` 0.20.0, which this release requires). The plan lists the domain, it is
58
+ sent when the issuer is registered and again on a re-apply, and a blueprint that leaves it out
59
+ never clears a scope set another way. An unverified domain fails the apply with the platform's
60
+ `400`. The apply also fails, before any schema, profile, role or key is written, when the
61
+ platform's response does not carry a declared domain: a platform that predates the field ignores
62
+ it, and carrying on would report success for an issuer that accepts more tokens than the blueprint
63
+ says. `vectros blueprint-test` checks the same on its readback.
64
+ - **`issuers register` and `issuers update` accept `--restricted-to-domain <domain>`.** Scopes the
65
+ registration to a domain already verified for your account (verified once, in the developer
66
+ portal): a token then matches only when its `hd` claim equals that domain, so the registration is
67
+ never blocked by an unrelated registration of the same issuer and audience. Google Workspace emits
68
+ `hd` natively; other identity providers need a claims-mapping rule that emits a claim named
69
+ exactly `hd`, otherwise none of their tokens will match. An unverified domain is refused by the
70
+ platform with `400`. `issuers register` never changes an existing registration, so when its
71
+ response does not carry the requested scope (an existing `--issuer-id` with a different one, or a
72
+ platform that predates the field) it now fails with exit `1` and names `issuers update`, which
73
+ applies it; a repeat with the same scope is still an idempotent success. `issuers get`,
74
+ `register` and `update` now show `restrictedToDomain`, and `issuers list` gains a
75
+ `restricted to domain` column; `--json` output already carried the field.
76
+ - **`issuers update --clear-restricted-to-domain`** removes the domain scope. It is a separate flag
77
+ because an empty domain is the way the platform spells "remove the scope": a blank
78
+ `--restricted-to-domain ''` (an unset shell variable, say) is refused with exit `2` instead of
79
+ being read as a clear, as is any value that is not a DNS name, and the two flags are mutually
80
+ exclusive. Omitting both leaves the stored scope unchanged. The platform can refuse to clear a
81
+ scope, for example when another registration already holds the unrestricted `(issuer, audience)`
82
+ pair; its message says why.
83
+
84
+ ### Changed
85
+
86
+ - `vectros blueprint plan` ends with both ways to apply (`blueprint apply`, no key; `bootstrap`, which also mints one),
87
+ and, for a blueprint that declares sample records, says that `bootstrap` writes them unless `--no-seed` while
88
+ `blueprint apply` writes them only with `--seed`.
89
+ Every plan now states how a trigger is judged to belong to a blueprint (a stored marker, not a verified identity),
90
+ and a plan for `apply` says nothing is deleted unless `--prune` is passed. The existing-principal refusal says what
91
+ is about to happen when no key is minted (the principal's grants are rewritten).
92
+ - Built against the 0.45.0 SDK (`@vectros-ai/sdk`), which adds `verifyIssuer` and the
93
+ `pending_verification` status with its challenge fields.
94
+
95
+ ### Security
96
+
97
+ - **`vectros login`'s local callback listener now completes sign-in only on a `GET /callback` carrying that
98
+ sign-in's own one-time `state`; every other request is ignored and the CLI keeps waiting for the real
99
+ redirect.** An identity-provider error redirect that arrives without a `state` is also ignored — Amazon
100
+ Cognito's own error redirects often omit one — and in that case the browser tab says to return to the
101
+ terminal, the CLI prints one notice, and waits until its timeout: press Ctrl+C and run `vectros login`
102
+ again. An error the sign-in page reports is shown with control and invisible formatting characters removed
103
+ and its length capped, and an error code that is not a plain OAuth token is not printed. This does not
104
+ protect against a program on your machine that can read the `state` out of the process list, where the
105
+ browser launcher receives the sign-in URL.
106
+ - **The text of a failed sign-in, or of a failed session refresh, is stripped of control characters and capped.**
107
+ When the token endpoint answers with an error, its response text is now stripped and capped at 500
108
+ characters wherever the message is built, so it applies to every command that refreshes your session (every
109
+ authenticated command), not only `vectros login`. Every `login failed:` line is also stripped (line breaks
110
+ removed) and capped at 1000 characters, whatever the source (a network error, a file error).
111
+ - **Not covered here:** error text that comes from the Vectros API, and the values other commands print (names
112
+ of keys, roles, contexts), is printed as received. This entry changes only the sign-in flow and the token
113
+ endpoint's failure text. The plan preview that `vectros blueprint apply` and `bootstrap` print now uses the
114
+ same, stronger stripping for the issuer fields it shows.
115
+
116
+ ### Fixed
117
+
118
+ - The re-apply note in the plan preview listed only some of the issuer fields that reconcile. It now
119
+ names all of them, including `restrictedToDomain` and `capturedClaims`.
120
+ - `AUTHORING.md` said issuer registration is create-once with no update. A re-apply reconciles the
121
+ editable issuer fields and rejects an edited trust-anchor field (`issuer`, `jwksUri`, `audience`,
122
+ `contextId`) with the platform's `400`; the text now says so.
123
+ - **`vectros blueprint init`'s scaffold no longer fails on its first seeded provision.** In CLI 0.22.0 and earlier the
124
+ generated blueprint declared `externalId` as a required schema field, which the platform reserves, so applying it
125
+ with its seed (`vectros bootstrap`, or `vectros blueprint apply --seed`) stopped with `Required field 'externalId'
126
+ cannot be empty.` The scaffold and the authoring guide's example now leave the field out; a seed sets its
127
+ `externalId` at the top level only. A blueprint you scaffolded earlier still needs the field deleted by hand.
128
+
129
+ ## 0.22.0 — 2026-09-17
130
+
131
+ ### Changed
132
+
133
+ - **⚠️ BREAKING: `vectros bootstrap --blueprint` now refuses to push a script version onto a name it
134
+ cannot show it owns.** Previously, a blueprint declaring `scripts:` could push a new version onto
135
+ ANY existing script name in the target context — including one it never declared, hand-authored, or
136
+ belonging to a different blueprint — with no error. If a pre-existing trigger rule referenced that
137
+ name via `version: 'latest'` (the only form a blueprint-shipped script may use), the newly pushed
138
+ code would run under that rule's own, unrelated grant from its very next firing. An apply that used
139
+ to succeed against such a name now refuses before minting anything: a name already owned by a
140
+ DIFFERENT blueprint refuses unconditionally (push under a different name instead); a name with no
141
+ ownership marker at all (hand-authored, or pushed before this marker existed) refuses unless the new
142
+ `--allow-script-adoption` flag is passed, reporting the outcome as `adopted` rather than
143
+ `superseded`. **This is a MINOR bump, not a patch, for the same reason as the entry above:** an
144
+ operator or CI pipeline that did nothing wrong and passed no new flag can now see an apply that
145
+ previously succeeded fail instead. A blueprint re-applying onto a name it already owns, or shipping
146
+ a brand-new name, is unaffected either way. See `AUTHORING.md`'s *idempotency contract* section for
147
+ the full behavior.
148
+ - **⚠️ BREAKING: `--no-merge` no longer implies an unattended run.** `--no-merge`'s documented
149
+ purpose is only to change where the resulting config goes (printed vs. merged into your
150
+ client's config file) — it still mints/reconciles the same real state either way — but both
151
+ the "Apply this plan?" confirmation and the seed-data opt-out prompt used to treat it as an
152
+ implicit `--yes`, so an operator (or a script) that had been relying on `--no-merge` alone to
153
+ run without either prompt will now be asked, if the session is interactive. **This is a MINOR
154
+ bump, not a patch, for the same reason as `@vectros-ai/blueprints`' own bump below: narrowing
155
+ an UNATTENDED-RUN contract is a breaking change for a script or wrapper that passes no new
156
+ flag, arguably a sharper break than narrowing an input contract, since the caller did nothing
157
+ wrong and gets no error — it just now blocks on a prompt it never used to see.** Pass `--yes`
158
+ explicitly if you need `--no-merge` to keep running unattended; a genuinely non-interactive
159
+ session (no TTY — the common case for CI) is unaffected either way, since neither prompt ever
160
+ fires there. The help text is corrected to say so.
161
+
162
+ ### Security
163
+
164
+ - **`vectros login`'s persisted Cognito refresh token could be POSTed to an attacker-controlled
165
+ host.** `VECTROS_COGNITO_DOMAIN` (and `VECTROS_COGNITO_REGION`, interpolated into the default
166
+ domain) were read verbatim from the environment and never validated — unlike the partner-API base
167
+ URL, which already goes through the same credential-exfil guard (`validateBaseUrl`). A developer
168
+ with an env var influenced by a repo-scoped config they never read as executable (a `.envrc`, a
169
+ devcontainer, a CI variable, an `.mcp.json` snippet) could have their long-lived Cognito refresh
170
+ token — and, on the interactive flow, the authorization code + PKCE verifier — sent to that host
171
+ on the very next creds-bearing command, with no prompt and no warning.
172
+
173
+ The Cognito domain now goes through `validateBaseUrl` too, extended with an allow-list entry for
174
+ the two exact Cognito hosted-UI hosts this CLI actually talks to (the Cognito domain is never a
175
+ `vectros.ai` host, so it needed its own entry rather than the partner-API allow-list unchanged —
176
+ and a broad `*.amazoncognito.com` suffix was deliberately rejected as too wide: that apex is
177
+ AWS's shared namespace for every Cognito user pool of every AWS customer, not a Vectros-owned
178
+ one); `VECTROS_COGNITO_REGION` is checked against an AWS-region shape before it is ever interpolated
179
+ into a URL. Only affects anyone who has run `vectros login` and does not use
180
+ `--token`/`VECTROS_BOOTSTRAP_TOKEN`.
181
+
182
+ **This narrows the vector; it does not close it against an attacker with unconstrained
183
+ environment control.** `VECTROS_ALLOW_INSECURE_BASE_URL=1` still opts out of the new allow-list
184
+ (loudly — a warning is logged), same as the pre-existing partner-API base-url guard it mirrors —
185
+ so an attacker able to set `VECTROS_COGNITO_DOMAIN` can, in principle, also set that opt-out
186
+ variable in the same reach. The fix protects the common case (an env var landing without the
187
+ reach to also flip a second, differently-named one) but is not a guarantee against an attacker
188
+ who already controls the whole environment.
189
+ - **A blueprint-authored free-text field with no character-set constraint could forge a fake
190
+ extra line, hide/reorder text, or otherwise misrepresent what `vectros bootstrap --blueprint`'s
191
+ pre-apply plan preview AND its identity-disclosure/confirmation prompt show an operator before
192
+ they approve a real mint** — a raw newline/CR, an ANSI escape sequence, a Unicode line/paragraph
193
+ separator, a bidi-override/isolate/mark character, or an invisible/zero-width character embedded
194
+ in a value like a role's `allowedActions`/`dataScope`, a declared identity's `externalId`
195
+ (printed immediately before the "Apply this plan?" gate), or a blueprint's own `name`/
196
+ `contextName` could make the rendered plan (or the confirmation prompt itself) show something
197
+ other than what would actually be provisioned. **A separate, stronger check now also closes the
198
+ HOMOGLYPH class for an issuer's `issuer`/`jwksUri`/`userinfoUri`** — a Unicode look-alike domain
199
+ (e.g. a Cyrillic character standing in for a Latin one), invisible to any character blocklist —
200
+ by requiring those three fields to be printable-ASCII `https://` URLs whose AUTHORITY contains
201
+ only letters, digits, dot, dash, and an optional port (no userinfo, no percent-encoding, no
202
+ non-ASCII host); an internationalized domain must be supplied in its already-punycode (`xn--…`)
203
+ form. `http://` is explicitly rejected — the platform hard-refuses a non-`https` issuer at
204
+ registration, so accepting it here would repeat this ecosystem's own "lints clean, then 400s at
205
+ apply" anti-pattern for the exact field this check exists to catch problems in beforehand.
206
+
207
+ Requires **`@vectros-ai/blueprints@0.19.0`** (a MINOR bump, not patch — this narrows what the
208
+ library accepts, which is a breaking change for a validation library's published contract, even
209
+ though this repo's own blueprints are unaffected; see that package's own CHANGELOG entry for the
210
+ full field list, the named residuals of the URL check — a trailing-dot host and a bare IPv4
211
+ literal still pass, a bracketed IPv6 literal is rejected as an accepted side effect — and,
212
+ importantly, what is deliberately NOT covered — length is still unbounded, and the homoglyph
213
+ class stays open for non-URL fields like `name`/`contextName`/`fieldId`, since no enumerable
214
+ blocklist can close it there the way a positive URL-authority grammar can for a URL). The schema
215
+ closes both classes at parse time, once, for every current and future rendered surface, rather
216
+ than requiring each renderer to sanitize its own inputs. This preview's own per-field
217
+ sanitization (`jwksUri`/`userinfoUri`/`issuer`/`capturedClaims`, added in the previous entry
218
+ below) stays in place as a second line of defense for a caller that reaches `buildPlan` without
219
+ going through the parser — which is how every bundled/first-party blueprint (`--blueprint
220
+ task-management`, etc.) is loaded today.
221
+ - **The plan preview for `vectros bootstrap --blueprint` omitted an issuer's trust-anchor
222
+ fields.** `jwksUri` (whoever controls it can mint identities the platform accepts) and, when
223
+ set, `userinfoUri` (fetched with the presented token as bearer) were never rendered — an
224
+ operator approving the plan for a pack declaring a malicious issuer saw its context and a
225
+ self-signup policy count, never the field that actually decides trust. Both are now always
226
+ disclosed, and a self-signup policy is now listed by its actual `signupType→roleId` pairing
227
+ instead of a bare count, matching the standard the preview already applies to captured
228
+ identity-provider claims.
229
+
230
+ ### Fixed
231
+
232
+ - **A degraded credential store (file-backed, not your OS keychain) now warns — at write time,
233
+ and now at read time too — for BOTH ways this can be a fixable install accident, not just
234
+ one.** Previously: the optional `@napi-rs/keyring` package loading but being BROKEN (a
235
+ version/ABI mismatch, a corrupt native binary, an unrecognised module shape) already warned;
236
+ but the package being entirely ABSENT was always treated as an ordinary, nothing-to-fix
237
+ platform limitation — even on a platform/arch this CLI ships a prebuilt native binary for,
238
+ where the only way to end up without it is an npm install that silently skipped the optional
239
+ dependency. Both throw the identical `MODULE_NOT_FOUND` error, so telling them apart needed a
240
+ new check: whether a prebuilt is actually known to exist for the current `process.platform`/
241
+ `process.arch`, read from `@napi-rs/keyring`'s own published platform matrix — consulted
242
+ regardless of what the underlying failure looks like, not only for a recognised
243
+ `MODULE_NOT_FOUND`-shaped error (a genuinely-unsupported platform's own failure can carry no
244
+ error code at all, and every other error/code was previously treated as an install accident
245
+ unconditionally, which produced permanent, un-fixable "try reinstalling" noise on exactly the
246
+ platforms this feature must stay silent on). Now, a write about to land in the weaker store,
247
+ or a read of an entry already stamped for the OS keychain, prints one warning naming the
248
+ actual cause and the remedy on EITHER fixable path — install failure or silent absence —
249
+ while a genuinely-unsupported platform/arch stays quiet, whatever shape its failure takes.
250
+ **One accepted, disclosed residual:** this can't distinguish glibc from musl on Linux — `arm`
251
+ and `riscv64` each have a real GNU-libc prebuilt (so a glibc machine there is fully covered),
252
+ but neither has a MUSL prebuilt, and this check cannot tell the two libcs apart. A musl
253
+ machine on `arm`/`riscv64` will still get an unnecessary warning naming a remedy that won't
254
+ help — a false positive, the direction this control is meant to err in, not a silent miss.
255
+ - **`bootstrap --rotate` could no longer recover once the local key record it needs was itself
256
+ the thing missing.** `--rotate` revoked a prior key only by a `keyId` read from this machine's
257
+ own local record — so when that record was lost (the exact situation `--rotate` exists to
258
+ recover from), nothing was revoked, the subsequent mint hit the same already-exists response,
259
+ and the CLI re-threw the identical error telling the caller to run `--rotate` — which they just
260
+ had. `--rotate` now falls back to resolving the key to revoke server-side (listing this
261
+ principal's active keys in the context and matching by name) when no local record exists. The
262
+ error message for the case that still can't be resolved now points at `vectros key list` /
263
+ `vectros key revoke` instead of repeating the same `--rotate` advice a second time.
264
+ - **Any verb (not just credential minting) could abort the whole process with a native Windows
265
+ crash instead of the CLI's own exit code.** Every verb ended with `process.exit(await
266
+ runVerb(...))`; on Windows, a `fetch` still settling when `process.exit()` runs races libuv and
267
+ aborts the process, printing an `Assertion failed` line and replacing the real exit code with
268
+ `127` — which reads to a script, CI job, or wrapping agent as "command not found" rather than as
269
+ the CLI's own, already-printed, correctly-diagnosed failure (a bad `--context`, a suspended
270
+ member, an expired/invalid bearer, and so on). Every verb now sets `process.exitCode` and lets
271
+ the event loop drain naturally instead of hard-exiting, the same idiom `role`/`access` already
272
+ used for their own top-level exit.
273
+
274
+ **A related hazard was introduced by this conversion itself, and closed in the same pass —
275
+ not a pre-existing bug.** A shared helper, `parseTenantFlag()`, validates `--tenant` for 24 call
276
+ sites (`role`, `access`, `scripts`, `issuers`, `key`, `identity list-all`) and used to call
277
+ `process.exit(2)` directly: an unconditional stop from any call-stack depth, so no released
278
+ version could ever fall through past it. Converting the helper to the same `process.exitCode`
279
+ idiom broke that guarantee, because `exitCode` only unwinds the helper's own stack frame — a
280
+ caller that didn't check it after calling the helper would silently continue into the real
281
+ command with an invalid `--tenant` (defaulting to the **live** tenant, since `undefined` is also
282
+ this helper's valid "no `--tenant` given" result). All 24 call sites were audited and given an
283
+ explicit check immediately after the call, so no release ever shipped the fall-through — but it
284
+ is why this entry calls it out as a hazard of the refactor, not a latent defect the refactor
285
+ happened to discover.
286
+
287
+ - **Eight source comments named internal backend classes/methods, or an internal gotcha filename,
288
+ by name** (`src/commands/issuers.ts`, `src/loader.ts` x3, `src/scope-gate.ts`, `src/scope-grammar.ts`,
289
+ `src/keyring.ts`, `src/os-keychain.ts`). This package has no GitHub source mirror, but `dist/`'s
290
+ sourcemaps embed the original source comments verbatim, so all eight shipped in every npm
291
+ tarball. Reworded to describe the mechanism without the identifier; no behavioral change.
292
+
293
+ - **Six more shipped sources named a sibling package by monorepo-relative path or an internal
294
+ process stage** (`src/base-url.ts`'s doc comment, `src/scope-gate.ts` x3 — a backend method
295
+ name and two sibling-package references, one of them also a monorepo path —
296
+ `THIRD-PARTY-NOTICES.md`'s generation note naming an internal build-script path, and
297
+ `src/harness.ts`'s comment naming an internal review process) — found by a post-merge
298
+ audit. All reworded to describe the mechanism/sibling generically, keeping the substantive
299
+ rationale; no behavioral change.
300
+
301
+ - **`scripts delete`'s pre-delete warning no longer claims the platform doesn't check whether a
302
+ live trigger rule still references the version being deleted.** The platform now refuses that
303
+ delete (409, naming the offending rule) instead of silently allowing it; the warning described
304
+ the older, unguarded behavior. No behavior changed — the delete itself already surfaced whatever
305
+ the platform returned either way; only the pre-emptive warning text was stale.
306
+
307
+ ### Docs
308
+
309
+ - **`vectros issuers register`/`update`'s flag tables now document three refusal conditions
310
+ those commands can hit.** `--jwks-uri`/`--userinfo-uri` must be `https://` (an `http://` value
311
+ is refused with 400); a tenant is capped at 50 registered issuers; and a `--self-signup-policy`
312
+ naming a role that authors, updates, or deletes scripts is refused. No behavior changed — the
313
+ CLI already surfaced whatever the platform returned; the tables just didn't mention these yet.
314
+
6
315
  ## 0.21.0 — 2026-09-07
7
316
 
8
317
  ### Added
@@ -326,7 +635,7 @@ This project adheres to [Semantic Versioning](https://semver.org).
326
635
  install path) used to print only on `--no-merge` / a declined / a failed
327
636
  merge — a successful auto-merge silently hid it; it's now shown there too.
328
637
  - Scrubbed internal issue/MR references, review-process citations, and
329
- backend class names out of `packages/cli`'s comments — visible in the
638
+ backend class names out of this package's comments — visible in the
330
639
  published npm package's sourcemaps to anyone who reads mapped source.
331
640
  Comment-only; no behavior change.
332
641
  - **Keyring secret-store hardening.** A 0-byte salt file removed by