@vectros-ai/cli 0.20.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/AUTHORING.md +166 -3
- package/CHANGELOG.md +385 -1
- package/README.md +82 -17
- package/THIRD-PARTY-NOTICES.md +2 -3
- package/dist/cli.js +3928 -1069
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +3928 -1069
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
package/AUTHORING.md
CHANGED
|
@@ -54,7 +54,11 @@ A name must be 2–50 chars: a lowercase letter, then lowercase letters / digits
|
|
|
54
54
|
`namespaces` and the `*` wildcard are **hard-rejected, no override**.
|
|
55
55
|
`profiles` is a narrower exception — `r`/`u`/`d` are admitted, and `c` only
|
|
56
56
|
when qualified `profiles:c:self` (create a profile for yourself; a bare
|
|
57
|
-
`profiles:c`, or one naming another principal, still hard-rejects).
|
|
57
|
+
`profiles:c`, or one naming another principal, still hard-rejects).
|
|
58
|
+
`scripts` is the other — `scripts:x:<script-name>` (execute one named
|
|
59
|
+
script) is admitted, and nothing else on that resource is: a bare
|
|
60
|
+
`scripts:x`, a wildcard `scripts:x:*`, and `scripts:c` (authoring) all
|
|
61
|
+
hard-reject. Any
|
|
58
62
|
requested `capabilities` are checked the same way, against their own,
|
|
59
63
|
separate allowlist — see "Capabilities" below. A forked or hand-written
|
|
60
64
|
blueprint gets the *identical* gates a bundled one does — it can never
|
|
@@ -298,7 +302,7 @@ namespaces:
|
|
|
298
302
|
# Registered in the SAME bootstrap-token phase as issuers, before schemas.
|
|
299
303
|
# Owned by this blueprint's own contextId (above) by default. `tenantWide: true` opts
|
|
300
304
|
# out of that, but declaring it is not enough on its own: the applying credential must
|
|
301
|
-
# separately hold OWNER-only authority, and `bootstrap
|
|
305
|
+
# separately hold OWNER-only authority, and `vectros bootstrap` refuses to even request
|
|
302
306
|
# it without the explicit `--allow-tenant-wide-namespaces` flag — a blueprint cannot make
|
|
303
307
|
# this happen by itself. Registration is create-once, NOT idempotent server-side: a
|
|
304
308
|
# re-apply matching every declared field converges silently, but one that disagrees
|
|
@@ -449,6 +453,90 @@ so if you seed a referencing record, seed its target first. To query "which X
|
|
|
449
453
|
reference Y", add the reference field to `lookupFields` as an equality lookup
|
|
450
454
|
(there is no reverse-reference index on this surface).
|
|
451
455
|
|
|
456
|
+
### Scripts and triggers — code a record write runs
|
|
457
|
+
|
|
458
|
+
A blueprint can ship the code an event runs, and the rule that runs it. Two
|
|
459
|
+
top-level blocks, applied in-context and in this order (`scripts` first —
|
|
460
|
+
a rule's `scriptRef` is checked when the rule is *declared*, not when it fires,
|
|
461
|
+
so a rule naming a script with no versions is refused outright):
|
|
462
|
+
|
|
463
|
+
```yaml
|
|
464
|
+
schemas:
|
|
465
|
+
- typeName: intake
|
|
466
|
+
displayName: Intake
|
|
467
|
+
# A schema fires triggers only when it opts in. Default is false.
|
|
468
|
+
capabilities: { triggersEnabled: true }
|
|
469
|
+
fields:
|
|
470
|
+
- { fieldId: status, fieldType: string, inline: true } # `inline` = projectable
|
|
471
|
+
|
|
472
|
+
scripts:
|
|
473
|
+
notify-assignee:
|
|
474
|
+
source: |
|
|
475
|
+
export default function (input) {
|
|
476
|
+
return { notified: input.recordId };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
triggers:
|
|
480
|
+
on-intake-update:
|
|
481
|
+
firingSource: { schemaName: intake, event: UPDATE }
|
|
482
|
+
# A script this blueprint ships is always `latest` — a pin is rejected at authoring time.
|
|
483
|
+
scriptRef: { name: notify-assignee, version: latest }
|
|
484
|
+
fields: [status]
|
|
485
|
+
allowedActions: [records:r:intake, records:u:intake]
|
|
486
|
+
dataScope: { "scope:org": ["${{ input.scope.org }}"] }
|
|
487
|
+
manifest: [records.get, records.update]
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Four things to get right, because each is a refusal rather than a warning:
|
|
491
|
+
|
|
492
|
+
- **`fields` is required.** It names the record fields projected into the
|
|
493
|
+
script's `input.record` (and, on an `UPDATE`, `input.previous`). `fields: []`
|
|
494
|
+
is the explicit "project nothing" declaration — required rather than defaulted
|
|
495
|
+
so that projecting nothing is something you state. An entry may name only a
|
|
496
|
+
field the schema keeps **inline** (`inline: true`, `filterable: true`, or a
|
|
497
|
+
lookup field) and never one marked `sensitive`. `validate` checks this against
|
|
498
|
+
the schema in the same blueprint, so you see it before an apply starts.
|
|
499
|
+
- **A rule that declares any `fields` must be able to read what it fires on** —
|
|
500
|
+
its grant needs a `records:r` clause covering the firing schema's type, and
|
|
501
|
+
that clause may not carry a *literal* `dataScope` constraint. A
|
|
502
|
+
`${{ input.* }}` placeholder is fine, because it resolves to the firing
|
|
503
|
+
record's own value.
|
|
504
|
+
- **The schema must opt in** with `capabilities.triggersEnabled: true`, or the
|
|
505
|
+
rule is refused at `plan` time rather than created and silently never fired.
|
|
506
|
+
- **The grant is live authority.** A trigger's `allowedActions`/`dataScope`, or
|
|
507
|
+
the roles its `roleIds` composes, are what the script actually executes under
|
|
508
|
+
— author them as narrowly as any other credential's. `${{ self.* }}`,
|
|
509
|
+
`${{ under.self.* }}` and `${{ member.* }}` are rejected inside a trigger's
|
|
510
|
+
grant: they resolve against the trigger principal's identity, not yours.
|
|
511
|
+
|
|
512
|
+
**Shipping a script does not make it runnable.** A `scripts:` entry provisions
|
|
513
|
+
the source; the two consumers reach it differently. A **trigger** names it in
|
|
514
|
+
`scriptRef` and runs it under the trigger's own grant — nothing further needed.
|
|
515
|
+
**Your app calling `POST /v1/scripts/execute` needs an explicit grant**, and the
|
|
516
|
+
role that calls it must carry `scripts:x:<script-name>`, qualified to that one
|
|
517
|
+
name:
|
|
518
|
+
|
|
519
|
+
```yaml
|
|
520
|
+
roles:
|
|
521
|
+
case-handler:
|
|
522
|
+
- allowedActions: [scripts:x:create-case]
|
|
523
|
+
dataScope:
|
|
524
|
+
scope:org: ['${{ member.scope.org }}']
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Omit it and the call is a 403 that cannot tell you the grant is what's missing.
|
|
528
|
+
See *The scope gate* for why the bare and wildcard forms are refused.
|
|
529
|
+
|
|
530
|
+
**Triggers are the one block an apply reconciles by absence.** Remove an entry
|
|
531
|
+
and re-apply, and its rule and access profile are deleted, so a trigger you
|
|
532
|
+
removed cannot keep acting. Its service principal is kept, so re-adding the same
|
|
533
|
+
trigger reattaches to one identity. Removing a `scripts:` entry deletes nothing
|
|
534
|
+
— see *The idempotency contract*, below, for why.
|
|
535
|
+
|
|
536
|
+
Full field grammar, including the byte caps on `source` and
|
|
537
|
+
`declaredInputContract` and the `manifest` verb vocabulary:
|
|
538
|
+
[`@vectros-ai/blueprints`](https://www.npmjs.com/package/@vectros-ai/blueprints).
|
|
539
|
+
|
|
452
540
|
### The scope gate — what you can and can't request
|
|
453
541
|
|
|
454
542
|
The gate allows only **data-plane** resources:
|
|
@@ -457,11 +545,35 @@ The gate allows only **data-plane** resources:
|
|
|
457
545
|
registered — ownership-gated the same way as `records`; it grants no
|
|
458
546
|
authority over the namespace *registry* itself, which stays root-key-only).
|
|
459
547
|
Operations are `c|r|u|d`, combinable (`records:cru`), with a per-resource
|
|
460
|
-
wildcard (`records:*`). Anything else — control-plane resources
|
|
548
|
+
wildcard (`records:*`). Anything else — control-plane resources and the bare
|
|
461
549
|
`*` — is rejected, and there is **intentionally no override flag**. This is
|
|
462
550
|
*why* it's safe to apply a blueprint you didn't write: the binary is the trust
|
|
463
551
|
boundary, not the file.
|
|
464
552
|
|
|
553
|
+
**One exception on `scripts`, because a blueprint that ships a script has to be
|
|
554
|
+
able to grant permission to run it.** `scripts:x:<script-name>` is admitted.
|
|
555
|
+
Everything else on that resource is not:
|
|
556
|
+
|
|
557
|
+
| Grant | Verdict |
|
|
558
|
+
|---|---|
|
|
559
|
+
| `scripts:x:create-case` | **admitted** — execute that one script |
|
|
560
|
+
| `scripts:x` (bare) | rejected — it would mean "run every script in this context, including ones added later that your reviewer never saw" |
|
|
561
|
+
| `scripts:x:*` | rejected — **there is no wildcard form.** The qualifier is compared to the script name exactly, so a `*` matches nothing: the grant would store cleanly and then refuse every execution, with no error able to say why |
|
|
562
|
+
| `scripts:c` (authoring) | rejected — creating a script is a design-time admin surface, and a blueprint ships its source rather than granting the ability to write one |
|
|
563
|
+
| `triggers:*` | rejected — unchanged |
|
|
564
|
+
|
|
565
|
+
Execution consults no data scope, so this widens **what can be composed**, never
|
|
566
|
+
**what data is reachable**: every write the script makes is still checked, one
|
|
567
|
+
by one, against the caller's own permissions. A credential holding only
|
|
568
|
+
`scripts:x:<name>` runs the script and is refused on its first write.
|
|
569
|
+
|
|
570
|
+
If your blueprint declares a `scripts:` block, `validate` also checks each
|
|
571
|
+
`scripts:x:<name>` grant against the names you actually ship, and refuses a
|
|
572
|
+
grant that names something else. That check exists here because it cannot exist
|
|
573
|
+
anywhere else: the platform stores a misspelled grant without complaint, and
|
|
574
|
+
the resulting 403 at execution time deliberately cannot tell you the name was
|
|
575
|
+
wrong.
|
|
576
|
+
|
|
465
577
|
### Capabilities (`granted_capabilities`) — a separate, equally-gated dimension
|
|
466
578
|
|
|
467
579
|
An `accessProfile` or role clause may also declare `capabilities: [...]` —
|
|
@@ -505,6 +617,57 @@ from a blueprint's `externalId` colliding with an unrelated identity, so it asks
|
|
|
505
617
|
rather than guessing either way. `--rotate` is exempt only when the local
|
|
506
618
|
record already names the same principal being rotated.
|
|
507
619
|
|
|
620
|
+
**A second qualification, and a different KIND of one: a `scripts:` entry is not
|
|
621
|
+
idempotent server-side.** Every push of a script name stores a genuinely new,
|
|
622
|
+
immutable version — there is no update, and no create-or-return. Convergence
|
|
623
|
+
here is the loader's doing, not the platform's: it reads the name's current
|
|
624
|
+
version and compares the stored `source` and `declaredInputContract` against
|
|
625
|
+
what the blueprint declares, and pushes only on a real difference. So a re-apply
|
|
626
|
+
of an unedited blueprint writes nothing, and editing `source` stores exactly one
|
|
627
|
+
new version, which every `version: latest` rule then picks up. The apply summary
|
|
628
|
+
reports which of `created` (no prior version) / `superseded` (this blueprint's
|
|
629
|
+
own prior version, replaced) / `adopted` (see below) / `unchanged` happened per
|
|
630
|
+
script.
|
|
631
|
+
|
|
632
|
+
**A THIRD qualification: pushing onto a name that already has a version is
|
|
633
|
+
gated on OWNING it.** A script name is a flat, per-context namespace shared
|
|
634
|
+
with every other blueprint and every hand-pushed script, so before superseding
|
|
635
|
+
one, the loader checks who pushed the current version:
|
|
636
|
+
- **owned by a different blueprint** — refused outright, no override. Ship
|
|
637
|
+
under a different name instead, or coordinate with whoever owns it.
|
|
638
|
+
- **no owner recorded** (hand-authored via `vectros scripts push`, or pushed
|
|
639
|
+
before this check existed) — refused unless the OPERATOR re-runs
|
|
640
|
+
`bootstrap` with `--allow-script-adoption`. This is deliberately a
|
|
641
|
+
human-supplied CLI flag, never something a blueprint's own YAML/JSON can
|
|
642
|
+
set — the blueprint is untrusted input, and "I already own this name" is not
|
|
643
|
+
something it gets to assert for itself. A successful adopting push reports
|
|
644
|
+
`adopted`, distinct from an ordinary `superseded`.
|
|
645
|
+
|
|
646
|
+
**Know before you rely on this: adoption is safe for an account owner,
|
|
647
|
+
and refused for a Dev Portal sub-user adopting someone else's hand-push.**
|
|
648
|
+
The platform's own authorization still applies underneath this flag, and it
|
|
649
|
+
resolves caller identity differently for the two shapes. An account owner
|
|
650
|
+
always authenticates as the same identity regardless of which login session
|
|
651
|
+
minted the token, so adopting a script you (the owner) hand-pushed earlier —
|
|
652
|
+
or one nobody's identity is recorded against at all — succeeds. A distinct
|
|
653
|
+
team member (a sub-user) resolves to their own, different identity, and the
|
|
654
|
+
platform refuses to supersede an unmarked name pushed by anyone else — so
|
|
655
|
+
one sub-user cannot adopt a script a DIFFERENT sub-user hand-pushed, even
|
|
656
|
+
with this flag. If you hit that, either have the original pusher run the
|
|
657
|
+
adopting apply, or push the script under the account owner's own login
|
|
658
|
+
first.
|
|
659
|
+
- **owned by this SAME blueprint, or the name is brand new** — no flag needed;
|
|
660
|
+
this is the ordinary re-apply / first-push path.
|
|
661
|
+
|
|
662
|
+
Two more consequences worth planning for. A concurrent apply of the same
|
|
663
|
+
blueprint can collide on one script name; the loader re-reads and retries once,
|
|
664
|
+
so two runners applying identical content converge rather than storing it
|
|
665
|
+
twice, but a sustained collision fails the apply and asks you to re-run. And
|
|
666
|
+
**removing a `scripts:` entry does not delete anything** — every stored version
|
|
667
|
+
stays, because there is no second, principal-shaped factor to safely gate a
|
|
668
|
+
DELETE the way a trigger row's provenance marker does. Remove one deliberately
|
|
669
|
+
with `vectros scripts delete`.
|
|
670
|
+
|
|
508
671
|
## Distribution
|
|
509
672
|
|
|
510
673
|
- **Bundled** — reviewed, versioned blueprints shipped in the library
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,390 @@
|
|
|
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.22.0 — 2026-09-17
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- **⚠️ BREAKING: `vectros bootstrap --blueprint` now refuses to push a script version onto a name it
|
|
11
|
+
cannot show it owns.** Previously, a blueprint declaring `scripts:` could push a new version onto
|
|
12
|
+
ANY existing script name in the target context — including one it never declared, hand-authored, or
|
|
13
|
+
belonging to a different blueprint — with no error. If a pre-existing trigger rule referenced that
|
|
14
|
+
name via `version: 'latest'` (the only form a blueprint-shipped script may use), the newly pushed
|
|
15
|
+
code would run under that rule's own, unrelated grant from its very next firing. An apply that used
|
|
16
|
+
to succeed against such a name now refuses before minting anything: a name already owned by a
|
|
17
|
+
DIFFERENT blueprint refuses unconditionally (push under a different name instead); a name with no
|
|
18
|
+
ownership marker at all (hand-authored, or pushed before this marker existed) refuses unless the new
|
|
19
|
+
`--allow-script-adoption` flag is passed, reporting the outcome as `adopted` rather than
|
|
20
|
+
`superseded`. **This is a MINOR bump, not a patch, for the same reason as the entry above:** an
|
|
21
|
+
operator or CI pipeline that did nothing wrong and passed no new flag can now see an apply that
|
|
22
|
+
previously succeeded fail instead. A blueprint re-applying onto a name it already owns, or shipping
|
|
23
|
+
a brand-new name, is unaffected either way. See `AUTHORING.md`'s *idempotency contract* section for
|
|
24
|
+
the full behavior.
|
|
25
|
+
- **⚠️ BREAKING: `--no-merge` no longer implies an unattended run.** `--no-merge`'s documented
|
|
26
|
+
purpose is only to change where the resulting config goes (printed vs. merged into your
|
|
27
|
+
client's config file) — it still mints/reconciles the same real state either way — but both
|
|
28
|
+
the "Apply this plan?" confirmation and the seed-data opt-out prompt used to treat it as an
|
|
29
|
+
implicit `--yes`, so an operator (or a script) that had been relying on `--no-merge` alone to
|
|
30
|
+
run without either prompt will now be asked, if the session is interactive. **This is a MINOR
|
|
31
|
+
bump, not a patch, for the same reason as `@vectros-ai/blueprints`' own bump below: narrowing
|
|
32
|
+
an UNATTENDED-RUN contract is a breaking change for a script or wrapper that passes no new
|
|
33
|
+
flag, arguably a sharper break than narrowing an input contract, since the caller did nothing
|
|
34
|
+
wrong and gets no error — it just now blocks on a prompt it never used to see.** Pass `--yes`
|
|
35
|
+
explicitly if you need `--no-merge` to keep running unattended; a genuinely non-interactive
|
|
36
|
+
session (no TTY — the common case for CI) is unaffected either way, since neither prompt ever
|
|
37
|
+
fires there. The help text is corrected to say so.
|
|
38
|
+
|
|
39
|
+
### Security
|
|
40
|
+
|
|
41
|
+
- **`vectros login`'s persisted Cognito refresh token could be POSTed to an attacker-controlled
|
|
42
|
+
host.** `VECTROS_COGNITO_DOMAIN` (and `VECTROS_COGNITO_REGION`, interpolated into the default
|
|
43
|
+
domain) were read verbatim from the environment and never validated — unlike the partner-API base
|
|
44
|
+
URL, which already goes through the same credential-exfil guard (`validateBaseUrl`). A developer
|
|
45
|
+
with an env var influenced by a repo-scoped config they never read as executable (a `.envrc`, a
|
|
46
|
+
devcontainer, a CI variable, an `.mcp.json` snippet) could have their long-lived Cognito refresh
|
|
47
|
+
token — and, on the interactive flow, the authorization code + PKCE verifier — sent to that host
|
|
48
|
+
on the very next creds-bearing command, with no prompt and no warning.
|
|
49
|
+
|
|
50
|
+
The Cognito domain now goes through `validateBaseUrl` too, extended with an allow-list entry for
|
|
51
|
+
the two exact Cognito hosted-UI hosts this CLI actually talks to (the Cognito domain is never a
|
|
52
|
+
`vectros.ai` host, so it needed its own entry rather than the partner-API allow-list unchanged —
|
|
53
|
+
and a broad `*.amazoncognito.com` suffix was deliberately rejected as too wide: that apex is
|
|
54
|
+
AWS's shared namespace for every Cognito user pool of every AWS customer, not a Vectros-owned
|
|
55
|
+
one); `VECTROS_COGNITO_REGION` is checked against an AWS-region shape before it is ever interpolated
|
|
56
|
+
into a URL. Only affects anyone who has run `vectros login` and does not use
|
|
57
|
+
`--token`/`VECTROS_BOOTSTRAP_TOKEN`.
|
|
58
|
+
|
|
59
|
+
**This narrows the vector; it does not close it against an attacker with unconstrained
|
|
60
|
+
environment control.** `VECTROS_ALLOW_INSECURE_BASE_URL=1` still opts out of the new allow-list
|
|
61
|
+
(loudly — a warning is logged), same as the pre-existing partner-API base-url guard it mirrors —
|
|
62
|
+
so an attacker able to set `VECTROS_COGNITO_DOMAIN` can, in principle, also set that opt-out
|
|
63
|
+
variable in the same reach. The fix protects the common case (an env var landing without the
|
|
64
|
+
reach to also flip a second, differently-named one) but is not a guarantee against an attacker
|
|
65
|
+
who already controls the whole environment.
|
|
66
|
+
- **A blueprint-authored free-text field with no character-set constraint could forge a fake
|
|
67
|
+
extra line, hide/reorder text, or otherwise misrepresent what `vectros bootstrap --blueprint`'s
|
|
68
|
+
pre-apply plan preview AND its identity-disclosure/confirmation prompt show an operator before
|
|
69
|
+
they approve a real mint** — a raw newline/CR, an ANSI escape sequence, a Unicode line/paragraph
|
|
70
|
+
separator, a bidi-override/isolate/mark character, or an invisible/zero-width character embedded
|
|
71
|
+
in a value like a role's `allowedActions`/`dataScope`, a declared identity's `externalId`
|
|
72
|
+
(printed immediately before the "Apply this plan?" gate), or a blueprint's own `name`/
|
|
73
|
+
`contextName` could make the rendered plan (or the confirmation prompt itself) show something
|
|
74
|
+
other than what would actually be provisioned. **A separate, stronger check now also closes the
|
|
75
|
+
HOMOGLYPH class for an issuer's `issuer`/`jwksUri`/`userinfoUri`** — a Unicode look-alike domain
|
|
76
|
+
(e.g. a Cyrillic character standing in for a Latin one), invisible to any character blocklist —
|
|
77
|
+
by requiring those three fields to be printable-ASCII `https://` URLs whose AUTHORITY contains
|
|
78
|
+
only letters, digits, dot, dash, and an optional port (no userinfo, no percent-encoding, no
|
|
79
|
+
non-ASCII host); an internationalized domain must be supplied in its already-punycode (`xn--…`)
|
|
80
|
+
form. `http://` is explicitly rejected — the platform hard-refuses a non-`https` issuer at
|
|
81
|
+
registration, so accepting it here would repeat this ecosystem's own "lints clean, then 400s at
|
|
82
|
+
apply" anti-pattern for the exact field this check exists to catch problems in beforehand.
|
|
83
|
+
|
|
84
|
+
Requires **`@vectros-ai/blueprints@0.19.0`** (a MINOR bump, not patch — this narrows what the
|
|
85
|
+
library accepts, which is a breaking change for a validation library's published contract, even
|
|
86
|
+
though this repo's own blueprints are unaffected; see that package's own CHANGELOG entry for the
|
|
87
|
+
full field list, the named residuals of the URL check — a trailing-dot host and a bare IPv4
|
|
88
|
+
literal still pass, a bracketed IPv6 literal is rejected as an accepted side effect — and,
|
|
89
|
+
importantly, what is deliberately NOT covered — length is still unbounded, and the homoglyph
|
|
90
|
+
class stays open for non-URL fields like `name`/`contextName`/`fieldId`, since no enumerable
|
|
91
|
+
blocklist can close it there the way a positive URL-authority grammar can for a URL). The schema
|
|
92
|
+
closes both classes at parse time, once, for every current and future rendered surface, rather
|
|
93
|
+
than requiring each renderer to sanitize its own inputs. This preview's own per-field
|
|
94
|
+
sanitization (`jwksUri`/`userinfoUri`/`issuer`/`capturedClaims`, added in the previous entry
|
|
95
|
+
below) stays in place as a second line of defense for a caller that reaches `buildPlan` without
|
|
96
|
+
going through the parser — which is how every bundled/first-party blueprint (`--blueprint
|
|
97
|
+
task-management`, etc.) is loaded today.
|
|
98
|
+
- **The plan preview for `vectros bootstrap --blueprint` omitted an issuer's trust-anchor
|
|
99
|
+
fields.** `jwksUri` (whoever controls it can mint identities the platform accepts) and, when
|
|
100
|
+
set, `userinfoUri` (fetched with the presented token as bearer) were never rendered — an
|
|
101
|
+
operator approving the plan for a pack declaring a malicious issuer saw its context and a
|
|
102
|
+
self-signup policy count, never the field that actually decides trust. Both are now always
|
|
103
|
+
disclosed, and a self-signup policy is now listed by its actual `signupType→roleId` pairing
|
|
104
|
+
instead of a bare count, matching the standard the preview already applies to captured
|
|
105
|
+
identity-provider claims.
|
|
106
|
+
|
|
107
|
+
### Fixed
|
|
108
|
+
|
|
109
|
+
- **A degraded credential store (file-backed, not your OS keychain) now warns — at write time,
|
|
110
|
+
and now at read time too — for BOTH ways this can be a fixable install accident, not just
|
|
111
|
+
one.** Previously: the optional `@napi-rs/keyring` package loading but being BROKEN (a
|
|
112
|
+
version/ABI mismatch, a corrupt native binary, an unrecognised module shape) already warned;
|
|
113
|
+
but the package being entirely ABSENT was always treated as an ordinary, nothing-to-fix
|
|
114
|
+
platform limitation — even on a platform/arch this CLI ships a prebuilt native binary for,
|
|
115
|
+
where the only way to end up without it is an npm install that silently skipped the optional
|
|
116
|
+
dependency. Both throw the identical `MODULE_NOT_FOUND` error, so telling them apart needed a
|
|
117
|
+
new check: whether a prebuilt is actually known to exist for the current `process.platform`/
|
|
118
|
+
`process.arch`, read from `@napi-rs/keyring`'s own published platform matrix — consulted
|
|
119
|
+
regardless of what the underlying failure looks like, not only for a recognised
|
|
120
|
+
`MODULE_NOT_FOUND`-shaped error (a genuinely-unsupported platform's own failure can carry no
|
|
121
|
+
error code at all, and every other error/code was previously treated as an install accident
|
|
122
|
+
unconditionally, which produced permanent, un-fixable "try reinstalling" noise on exactly the
|
|
123
|
+
platforms this feature must stay silent on). Now, a write about to land in the weaker store,
|
|
124
|
+
or a read of an entry already stamped for the OS keychain, prints one warning naming the
|
|
125
|
+
actual cause and the remedy on EITHER fixable path — install failure or silent absence —
|
|
126
|
+
while a genuinely-unsupported platform/arch stays quiet, whatever shape its failure takes.
|
|
127
|
+
**One accepted, disclosed residual:** this can't distinguish glibc from musl on Linux — `arm`
|
|
128
|
+
and `riscv64` each have a real GNU-libc prebuilt (so a glibc machine there is fully covered),
|
|
129
|
+
but neither has a MUSL prebuilt, and this check cannot tell the two libcs apart. A musl
|
|
130
|
+
machine on `arm`/`riscv64` will still get an unnecessary warning naming a remedy that won't
|
|
131
|
+
help — a false positive, the direction this control is meant to err in, not a silent miss.
|
|
132
|
+
- **`bootstrap --rotate` could no longer recover once the local key record it needs was itself
|
|
133
|
+
the thing missing.** `--rotate` revoked a prior key only by a `keyId` read from this machine's
|
|
134
|
+
own local record — so when that record was lost (the exact situation `--rotate` exists to
|
|
135
|
+
recover from), nothing was revoked, the subsequent mint hit the same already-exists response,
|
|
136
|
+
and the CLI re-threw the identical error telling the caller to run `--rotate` — which they just
|
|
137
|
+
had. `--rotate` now falls back to resolving the key to revoke server-side (listing this
|
|
138
|
+
principal's active keys in the context and matching by name) when no local record exists. The
|
|
139
|
+
error message for the case that still can't be resolved now points at `vectros key list` /
|
|
140
|
+
`vectros key revoke` instead of repeating the same `--rotate` advice a second time.
|
|
141
|
+
- **Any verb (not just credential minting) could abort the whole process with a native Windows
|
|
142
|
+
crash instead of the CLI's own exit code.** Every verb ended with `process.exit(await
|
|
143
|
+
runVerb(...))`; on Windows, a `fetch` still settling when `process.exit()` runs races libuv and
|
|
144
|
+
aborts the process, printing an `Assertion failed` line and replacing the real exit code with
|
|
145
|
+
`127` — which reads to a script, CI job, or wrapping agent as "command not found" rather than as
|
|
146
|
+
the CLI's own, already-printed, correctly-diagnosed failure (a bad `--context`, a suspended
|
|
147
|
+
member, an expired/invalid bearer, and so on). Every verb now sets `process.exitCode` and lets
|
|
148
|
+
the event loop drain naturally instead of hard-exiting, the same idiom `role`/`access` already
|
|
149
|
+
used for their own top-level exit.
|
|
150
|
+
|
|
151
|
+
**A related hazard was introduced by this conversion itself, and closed in the same pass —
|
|
152
|
+
not a pre-existing bug.** A shared helper, `parseTenantFlag()`, validates `--tenant` for 24 call
|
|
153
|
+
sites (`role`, `access`, `scripts`, `issuers`, `key`, `identity list-all`) and used to call
|
|
154
|
+
`process.exit(2)` directly: an unconditional stop from any call-stack depth, so no released
|
|
155
|
+
version could ever fall through past it. Converting the helper to the same `process.exitCode`
|
|
156
|
+
idiom broke that guarantee, because `exitCode` only unwinds the helper's own stack frame — a
|
|
157
|
+
caller that didn't check it after calling the helper would silently continue into the real
|
|
158
|
+
command with an invalid `--tenant` (defaulting to the **live** tenant, since `undefined` is also
|
|
159
|
+
this helper's valid "no `--tenant` given" result). All 24 call sites were audited and given an
|
|
160
|
+
explicit check immediately after the call, so no release ever shipped the fall-through — but it
|
|
161
|
+
is why this entry calls it out as a hazard of the refactor, not a latent defect the refactor
|
|
162
|
+
happened to discover.
|
|
163
|
+
|
|
164
|
+
- **Eight source comments named internal backend classes/methods, or an internal gotcha filename,
|
|
165
|
+
by name** (`src/commands/issuers.ts`, `src/loader.ts` x3, `src/scope-gate.ts`, `src/scope-grammar.ts`,
|
|
166
|
+
`src/keyring.ts`, `src/os-keychain.ts`). This package has no GitHub source mirror, but `dist/`'s
|
|
167
|
+
sourcemaps embed the original source comments verbatim, so all eight shipped in every npm
|
|
168
|
+
tarball. Reworded to describe the mechanism without the identifier; no behavioral change.
|
|
169
|
+
|
|
170
|
+
- **Six more shipped sources named a sibling package by monorepo-relative path or an internal
|
|
171
|
+
process stage** (`src/base-url.ts`'s doc comment, `src/scope-gate.ts` x3 — a backend method
|
|
172
|
+
name and two sibling-package references, one of them also a monorepo path —
|
|
173
|
+
`THIRD-PARTY-NOTICES.md`'s generation note naming an internal build-script path, and
|
|
174
|
+
`src/harness.ts`'s "the Tier-2 panel caught three times" wording) — found by a post-merge
|
|
175
|
+
audit. All reworded to describe the mechanism/sibling generically, keeping the substantive
|
|
176
|
+
rationale; no behavioral change.
|
|
177
|
+
|
|
178
|
+
- **`scripts delete`'s pre-delete warning no longer claims the platform doesn't check whether a
|
|
179
|
+
live trigger rule still references the version being deleted.** The platform now refuses that
|
|
180
|
+
delete (409, naming the offending rule) instead of silently allowing it; the warning described
|
|
181
|
+
the older, unguarded behavior. No behavior changed — the delete itself already surfaced whatever
|
|
182
|
+
the platform returned either way; only the pre-emptive warning text was stale.
|
|
183
|
+
|
|
184
|
+
### Docs
|
|
185
|
+
|
|
186
|
+
- **`vectros issuers register`/`update`'s flag tables now document three refusal conditions
|
|
187
|
+
those commands can hit.** `--jwks-uri`/`--userinfo-uri` must be `https://` (an `http://` value
|
|
188
|
+
is refused with 400); a tenant is capped at 50 registered issuers; and a `--self-signup-policy`
|
|
189
|
+
naming a role that authors, updates, or deletes scripts is refused. No behavior changed — the
|
|
190
|
+
CLI already surfaced whatever the platform returned; the tables just didn't mention these yet.
|
|
191
|
+
|
|
192
|
+
## 0.21.0 — 2026-09-07
|
|
193
|
+
|
|
194
|
+
### Added
|
|
195
|
+
|
|
196
|
+
- **`vectros bootstrap --blueprint` provisions a blueprint's `scripts` block**, before the triggers
|
|
197
|
+
that reference them. A trigger's `scriptRef` is checked when the rule is DECLARED, not when it
|
|
198
|
+
fires, so the ordering is what makes a blueprint shipping both work at all — the other way round,
|
|
199
|
+
the trigger is refused for naming a script that does not exist yet, after the apply has already
|
|
200
|
+
minted that trigger's service principal and access profile.
|
|
201
|
+
|
|
202
|
+
**A re-apply pushes nothing when the source has not changed.** Pushing a script is not
|
|
203
|
+
idempotent — every push stores a new, server-numbered version — so an apply reads the name's
|
|
204
|
+
current version first and stores a new one only when `source` or `declaredInputContract` actually
|
|
205
|
+
differs. Without that, every apply of an unedited blueprint would mint a version and silently
|
|
206
|
+
re-point every `version: latest` rule at code identical to what it was already running. Editing
|
|
207
|
+
the source pushes exactly one new version, which `latest` rules then pick up. A concurrent push
|
|
208
|
+
from another apply is re-read rather than blindly retried, so two runners applying the same
|
|
209
|
+
blueprint converge instead of storing the same source twice.
|
|
210
|
+
|
|
211
|
+
**Removing a `scripts` entry does NOT delete the script** — the deliberate asymmetry with
|
|
212
|
+
triggers, which an apply does reconcile by absence. A script carries no record of which blueprint
|
|
213
|
+
pushed it, so an apply cannot tell one it created from one pushed by hand; the delete would be
|
|
214
|
+
unrecoverable, since the stored source is the only copy, and would break any trigger still
|
|
215
|
+
referencing it. An unreferenced script version is inert — it fires nothing and grants nothing — so
|
|
216
|
+
it is left in place. `blueprint plan` states this every time, including when the blueprint declares
|
|
217
|
+
no scripts at all, which is exactly the plan you see after removing the last one.
|
|
218
|
+
|
|
219
|
+
- **`vectros scripts <push|list|get|delete>`** — manage script objects outside a full apply, which
|
|
220
|
+
previously had no route at all short of raw HTTP.
|
|
221
|
+
|
|
222
|
+
- `scripts push <name> --source-file <path> --context <ctx>` stores a new version and reports its
|
|
223
|
+
number. It always pushes, matching the platform: `--if-changed` opts into the convergent
|
|
224
|
+
behaviour instead, which is what a CI job re-running the same push wants. Name grammar and both
|
|
225
|
+
byte caps are checked locally, so a bad push exits 2 naming the field instead of sending 300 KB
|
|
226
|
+
to earn a rejection. `--input-contract` / `--input-contract-file` supply the optional contract.
|
|
227
|
+
- `scripts list --context <ctx> [--name <name>]` — every version of one name, or all scripts. The
|
|
228
|
+
human view shows a byte count rather than the source, so listing a much-edited script does not
|
|
229
|
+
dump hundreds of kilobytes into a terminal; `--json` carries the source for a machine consumer.
|
|
230
|
+
- `scripts get <id> --context <ctx>` shows one version including its full source.
|
|
231
|
+
- `scripts delete <id> --context <ctx>` removes ONE version, leaving other versions of the name
|
|
232
|
+
alone — and warns that nothing checked whether a trigger still referenced it. That warning is
|
|
233
|
+
the point: a rule pinned to the deleted version fails on every firing, while a rule on `latest`
|
|
234
|
+
silently falls back to the previous version and keeps running older code.
|
|
235
|
+
|
|
236
|
+
There is no `scripts update`: a stored version is immutable and the update route always rejects.
|
|
237
|
+
`--context` is required on all four, since a trigger resolves its `scriptRef` inside its own
|
|
238
|
+
context and a defaulted one would push where nobody asked.
|
|
239
|
+
|
|
240
|
+
- **`blueprint-test` tears down the script versions a run pushed**, by id, after the trigger teardown
|
|
241
|
+
so a rule never briefly outlives the script it names. A version the run did not push — the source
|
|
242
|
+
already matched what the blueprint declares — is reported as skipped and left alone.
|
|
243
|
+
|
|
244
|
+
- **`blueprint-test` tears down trigger rules it created.** The harness deletes what a run
|
|
245
|
+
provisions, and triggers were the one resource it could not reach — so testing a blueprint with a
|
|
246
|
+
`triggers:` block left the rule behind, and because a schema will not delete while a rule fires
|
|
247
|
+
off it, the leak kept the schema alive and the next run failed against it. Deletion is
|
|
248
|
+
created-only and requires TWO independent factors, mirroring what `bootstrap` already does when it
|
|
249
|
+
reconciles a trigger away: the rule must be absent from the pre-run snapshot AND carry this
|
|
250
|
+
blueprint's own `provisionedBy` marker. A rule that pre-existed, one another blueprint provisioned,
|
|
251
|
+
one created directly through the API, or any rule at all when the snapshot could not be read, is
|
|
252
|
+
reported and left in place. Each rule's access profile goes with it — that is where its declared
|
|
253
|
+
authority lives — while its service principal is kept, so re-adding the same trigger reattaches to
|
|
254
|
+
one identity.
|
|
255
|
+
|
|
256
|
+
- **Blueprint trigger `fields` and schema field `inline` are provisioned.** A trigger rule declares
|
|
257
|
+
which record fields it projects into its script's `input.record` (and, on an `UPDATE` firing,
|
|
258
|
+
`input.previous`); `vectros bootstrap --blueprint` forwards that declaration, and forwards a
|
|
259
|
+
schema field's `inline` flag, which is what makes a field projectable at all. `blueprint plan`
|
|
260
|
+
now names the projected fields per trigger — this is the record data that leaves storage on every
|
|
261
|
+
firing, so an operator approving a plan should see it — and spells out a trigger that projects
|
|
262
|
+
nothing rather than omitting the line. Requires `@vectros-ai/blueprints` ≥ 0.18.0.
|
|
263
|
+
|
|
264
|
+
- **`assume` shows the identity you actually got.** `POST /v1/auth/token/assume` now returns a
|
|
265
|
+
`resolvedScope` alongside the re-minted token — the same plaintext data baked into the token's own
|
|
266
|
+
compressed `scope` claim, resolved server-side — so the human view reports the resolved `identity`
|
|
267
|
+
and `allowedActions` instead of leaving you to decode the token. Switching `identity` is the whole
|
|
268
|
+
point of assuming, so this is the direct answer to the question the command was run to ask. The
|
|
269
|
+
rows are omitted entirely against a backend that returns no `resolvedScope`, rather than rendered
|
|
270
|
+
as empty: on this command an em dash would read as "you assumed nothing". `--json` already carried
|
|
271
|
+
the field verbatim.
|
|
272
|
+
|
|
273
|
+
- **`role create --assignable-role <roleId>`** (repeatable) — restrict which named roles the new
|
|
274
|
+
role's clause may compose into a delegated AccessProfile. Omit for no restriction; the platform
|
|
275
|
+
rejects an empty list, so the flag has no "compose nothing" spelling and omitting it leaves the
|
|
276
|
+
clause byte-identical to one authored before this release. Validated locally against the platform's
|
|
277
|
+
roleId grammar, 20-entry cap and no-duplicates rule, so a bad list exits 2 naming the offending
|
|
278
|
+
entry instead of round-tripping to a `400`.
|
|
279
|
+
|
|
280
|
+
- **`access explain` reports a clause's `assignableRoles`**, in both the human and `--json` views,
|
|
281
|
+
for clauses that carry one. Reported alongside the access matrix rather than folded into it: the
|
|
282
|
+
restriction bounds role composition, not data reach, so narrowing the matrix on it would be wrong.
|
|
283
|
+
A blueprint can declare the same restriction via `assignableRoles` on a role or `accessProfile`
|
|
284
|
+
clause (`@vectros-ai/blueprints` ≥ 0.18.0), which `vectros bootstrap --blueprint` forwards.
|
|
285
|
+
|
|
286
|
+
- **`issuers register` / `issuers update` accept `--captured-claim <name>`** (repeatable) — additional
|
|
287
|
+
OIDC claim names, beyond the email claim, to capture from that issuer's tokens on every successful
|
|
288
|
+
token exchange and store as your tenant's golden identity-provider-asserted copy for the signed-in
|
|
289
|
+
user. `issuers update` also gains `--clear-captured-claims`, which sends an explicit empty list
|
|
290
|
+
(capture nothing beyond email) — distinct from omitting the flag, which leaves the stored value
|
|
291
|
+
unchanged. Mutually exclusive with `--captured-claim`. The list is bounds-checked locally (20
|
|
292
|
+
entries, 64 characters each, no duplicates), so a bad list exits 2 naming the offending entry
|
|
293
|
+
rather than round-tripping to a `400`. `issuers get` renders the captured names, and `issuers
|
|
294
|
+
list` gains a `captured claims` column.
|
|
295
|
+
A blueprint can declare the same thing via `issuers[].capturedClaims` (`@vectros-ai/blueprints`
|
|
296
|
+
≥ 0.18.0), which `vectros bootstrap --blueprint` now forwards and reconciles when re-applied.
|
|
297
|
+
|
|
298
|
+
- **Blueprint `identityProjectionClaims` is applied.** A blueprint declaring the new top-level block
|
|
299
|
+
(`@vectros-ai/blueprints` ≥ 0.18.0) has it set on the app context during the bootstrap phase,
|
|
300
|
+
alongside the context create — that is the only phase whose credential carries the platform
|
|
301
|
+
provisioning capability the field requires. Declaring it selects which captured identity-provider
|
|
302
|
+
claim names are projected, read-only, onto access profiles in that context. A blueprint that does
|
|
303
|
+
not declare it sends nothing, leaving any existing declaration untouched. **It applies only when this
|
|
304
|
+
blueprint CREATES the context**: the platform sets the declaration on a true create, so a context
|
|
305
|
+
created separately beforehand never receives it, and re-applying against an existing context does
|
|
306
|
+
not change it in either direction — edit an existing context's
|
|
307
|
+
declaration through the platform API.
|
|
308
|
+
|
|
309
|
+
- **Blueprint `triggers:` provisioning.** `vectros bootstrap --blueprint` now provisions each declared trigger:
|
|
310
|
+
a dedicated service principal, a context-scoped AccessProfile carrying the trigger's declared
|
|
311
|
+
grant, and the trigger rule itself. A trigger's real declared grant is attached only after the
|
|
312
|
+
trigger rule is confirmed created — a failed apply never leaves a principal holding meaningful
|
|
313
|
+
authority with nothing referencing it. `blueprint plan` renders each trigger's firing schema/
|
|
314
|
+
event, script reference, and manifest (or `(none)` when absent) alongside the principal/profile
|
|
315
|
+
steps it provisions. Requires `@vectros-ai/blueprints` ≥ 0.18.0. A schema's
|
|
316
|
+
`capabilities.triggersEnabled` is now applied along with `auditHistory` — it is the opt-in a schema
|
|
317
|
+
must declare before a trigger may fire on its records, and a blueprint declaring a trigger against
|
|
318
|
+
a schema without it is refused at `plan` time rather than creating a rule that never fires.
|
|
319
|
+
- **Removing a trigger from a blueprint now de-provisions it.** Triggers are the one resource
|
|
320
|
+
`vectros bootstrap --blueprint` reconciles by *absence* as well as presence: a trigger this blueprint previously
|
|
321
|
+
provisioned and no longer declares has its rule and its access profile deleted, so a trigger you
|
|
322
|
+
removed cannot keep acting under a grant that outlived it. Its service principal is kept, so
|
|
323
|
+
re-adding the same trigger later reattaches to the same identity. Triggers created outside this
|
|
324
|
+
blueprint — by hand, or by a different blueprint sharing the app context — are never touched.
|
|
325
|
+
Ownership is established from **two** independent pieces of evidence, both of which must agree: the
|
|
326
|
+
trigger rule's own `provisionedBy` marker (which `vectros bootstrap --blueprint` stamps, and which the platform refuses to
|
|
327
|
+
re-point once set) and the service principal the rule names. Neither alone is sufficient, because
|
|
328
|
+
each is writable with a different scope; requiring both means no single credential can make one
|
|
329
|
+
blueprint's apply delete another principal's grant. A trigger whose two factors disagree, or whose
|
|
330
|
+
principal cannot be read, is left in place and reported rather than deleted. Each de-provisioned
|
|
331
|
+
trigger is named individually in the apply output, and `blueprint plan` states the behaviour in
|
|
332
|
+
its legend. No other resource is ever deleted by an apply.
|
|
333
|
+
|
|
334
|
+
### Changed
|
|
335
|
+
|
|
336
|
+
- **A refusal caused by suspension is reported as one.** `POST
|
|
337
|
+
/v1/admin/keys/scoped` now answers with a `409` when the access profile or the user it names is
|
|
338
|
+
suspended, and the CLI renders that distinctly: no credential — root included — mints against a
|
|
339
|
+
suspended profile, so the remedy the `403` text points at (re-issue a broader credential) is
|
|
340
|
+
actively wrong here. Reactivate the profile, then retry. The developer scoped-token route answers
|
|
341
|
+
every refusal with one uniform `404`, and a suspended member is one of them; since every
|
|
342
|
+
credential-bearing verb mints through that route, the CLI names suspension among the causes rather
|
|
343
|
+
than reporting only that the context was not found.
|
|
344
|
+
|
|
345
|
+
- **The scope gate names `scripts` and `triggers` as control-plane.** Both were already refused for
|
|
346
|
+
a blueprint-issued role — they are not data-plane resources — and the refusal now says so, rather
|
|
347
|
+
than reading as an unrecognized resource that the allowlist might be widened for. Script authoring
|
|
348
|
+
and trigger declaration are deliberately admin-shaped surfaces. No scope that passed the gate
|
|
349
|
+
before is refused.
|
|
350
|
+
|
|
351
|
+
- **Two blueprints may not declare a trigger of the same name in one app context.** The second apply
|
|
352
|
+
is rejected with a `400`. A trigger fires real code under a real grant, so one blueprint quietly
|
|
353
|
+
re-pointing another's rule is not something an apply does. Rename one of the triggers, or move it
|
|
354
|
+
to its own app context.
|
|
355
|
+
|
|
356
|
+
### Fixed
|
|
357
|
+
|
|
358
|
+
- **`role` and `access` accept `--tenant`, so a blueprint applied to the test tenant can be verified.**
|
|
359
|
+
Every sibling verb (`context`, `issuers`, `scripts`, `key`, `identity`) already took the flag; these
|
|
360
|
+
two did not, and they are the ones that read back roles and access bindings. Omitting it fell back
|
|
361
|
+
to the live tenant and failed with *"Context not found"* — a message naming two possible causes,
|
|
362
|
+
neither of which was the real one. A blueprint apply's most security-relevant output was the one
|
|
363
|
+
part that could not be read back from the tenant it was written to.
|
|
364
|
+
- **A failed token mint no longer aborts the process.** `role` and `access` now set the exit code and
|
|
365
|
+
return rather than exiting mid-flight, which on Windows could trip a native assertion in Node's HTTP
|
|
366
|
+
stack and replace the command's real exit code with `127` — the code a shell uses for "command not
|
|
367
|
+
found". Anything branching on exit status read a refused request as a missing binary. If you script
|
|
368
|
+
against another verb and see `127` from a request that clearly reached the server, that is this bug
|
|
369
|
+
and not a missing binary; re-run and read the printed error, which is accurate.
|
|
370
|
+
|
|
371
|
+
- **A blueprint can now grant `scripts:x:<script-name>`, so a blueprint that ships a script can also
|
|
372
|
+
grant permission to run it.** The scope gate classified `scripts` as control-plane by resource, so
|
|
373
|
+
the grant was refused outright and a blueprint could ship a script's source with no way to
|
|
374
|
+
authorize its execution. That reasoning is correct for `scripts:c` — authoring is a design-time,
|
|
375
|
+
admin-shaped surface, and it is unchanged — but it does not carry to execution, which consults no
|
|
376
|
+
data scope at all and whose writes are still checked one by one against the caller's own
|
|
377
|
+
permissions. The grant widens what can be composed, never what data is reachable.
|
|
378
|
+
|
|
379
|
+
Qualified to a script name only. A bare `scripts:x` is still rejected: execution gates on the verb
|
|
380
|
+
and the name, so an unqualified grant would mean "may run every script in this context, including
|
|
381
|
+
ones added later". `scripts:*` and `scripts:x:*` are rejected too — **there is no wildcard form.**
|
|
382
|
+
The qualifier is compared to the script name exactly, so a `*` matches nothing: such a grant would
|
|
383
|
+
store cleanly and then refuse every execution, with no error able to explain why.
|
|
384
|
+
- **`blueprint validate`/`plan` now catch a misspelled script grant.** If your blueprint declares a
|
|
385
|
+
`scripts:` block, each `scripts:x:<name>` grant is checked against the names you actually ship, and
|
|
386
|
+
the refusal names the role and lists what is shipped. Nothing downstream can catch this: the
|
|
387
|
+
platform stores a wrong name without complaint and the execution-time 403 deliberately cannot say
|
|
388
|
+
the name is the problem.
|
|
389
|
+
|
|
6
390
|
## 0.20.0 — 2026-08-27
|
|
7
391
|
|
|
8
392
|
### Added
|
|
@@ -128,7 +512,7 @@ This project adheres to [Semantic Versioning](https://semver.org).
|
|
|
128
512
|
install path) used to print only on `--no-merge` / a declined / a failed
|
|
129
513
|
merge — a successful auto-merge silently hid it; it's now shown there too.
|
|
130
514
|
- Scrubbed internal issue/MR references, review-process citations, and
|
|
131
|
-
backend class names out of
|
|
515
|
+
backend class names out of this package's comments — visible in the
|
|
132
516
|
published npm package's sourcemaps to anyone who reads mapped source.
|
|
133
517
|
Comment-only; no behavior change.
|
|
134
518
|
- **Keyring secret-store hardening.** A 0-byte salt file removed by
|