@vectros-ai/cli 0.16.0 → 0.20.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 +73 -17
- package/CHANGELOG.md +332 -0
- package/README.md +136 -13
- package/THIRD-PARTY-NOTICES.md +4 -4
- package/dist/cli.js +1664 -223
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1664 -223
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -9,11 +9,15 @@ The Vectros provisioning CLI — the `vectros` binary:
|
|
|
9
9
|
- **`vectros blueprint`** — author + inspect blueprints (`init` / `validate` / `plan` / `list`), creds-free.
|
|
10
10
|
- **`vectros blueprint-test`** — apply a blueprint to a live env, assert it provisioned a working credential, then tear it down.
|
|
11
11
|
- **`vectros context`** — create, list, and destroy app contexts.
|
|
12
|
+
- **`vectros issuers`** — register, update, delete, and inspect third-party IdP issuers (token-exchange trust anchors) for a context.
|
|
13
|
+
- **`vectros namespace`** — register a new scope namespace, and list/get existing ones.
|
|
12
14
|
- **`vectros identity`** — manage tenant-wide principals (users, orgs, clients, and namespaces you register).
|
|
13
15
|
- **`vectros role`** — define reusable, context-scoped access rules.
|
|
16
|
+
- **`vectros assume`** — switch identity namespace values on your own token, per a role's `assumable` grant.
|
|
14
17
|
- **`vectros access`** — bind principals to contexts, and inspect what a binding grants.
|
|
15
18
|
- **`vectros key`** — issue, list, revoke, and rotate scoped API keys (`ssk_*`).
|
|
16
19
|
- **`vectros keyring`** — hold several identities on one machine, pick which is active, and let other tools resolve keys from one place.
|
|
20
|
+
- **`vectros join`** — enroll a principal in a context and keep the resulting credential in the keyring, in one step.
|
|
17
21
|
|
|
18
22
|
```bash
|
|
19
23
|
npx -y @vectros-ai/cli bootstrap # provision a credential
|
|
@@ -144,6 +148,63 @@ vectros context destroy my-app # irreversible — asks you to r
|
|
|
144
148
|
All four accept `--json` for machine-readable output, plus the common auth
|
|
145
149
|
flags (`--base-url`, `--token`).
|
|
146
150
|
|
|
151
|
+
## `vectros issuers` — registered third-party IdP issuers
|
|
152
|
+
|
|
153
|
+
Manage the third-party identity-provider registrations a context trusts for
|
|
154
|
+
[RFC 8693 token exchange](https://www.rfc-editor.org/rfc/rfc8693) — the
|
|
155
|
+
`issuer`/`jwksUri`/`audience` a blueprint's `issuers[]` entries register, plus
|
|
156
|
+
any self-signup policy attached to one. Everything a blueprint's `issuers:`
|
|
157
|
+
block can do is available standalone here too — adding a second IdP, or
|
|
158
|
+
editing one issuer's `selfSignupPolicies` after initial setup, no longer
|
|
159
|
+
requires hand-authoring a blueprint and re-running a full apply.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
vectros issuers register --context my-app --issuer-id auth0-prod \
|
|
163
|
+
--issuer https://your-tenant.us.auth0.com/ \
|
|
164
|
+
--jwks-uri https://your-tenant.us.auth0.com/.well-known/jwks.json \
|
|
165
|
+
--audience https://api.your-app.example.com
|
|
166
|
+
vectros issuers list --context my-app
|
|
167
|
+
vectros issuers get primary --context my-app
|
|
168
|
+
vectros issuers update primary --context my-app --status suspended
|
|
169
|
+
vectros issuers delete primary --context my-app # re-types the id to confirm
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
| Subcommand | Flags | Notes |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| `register` | `--context`, `--issuer-id`, `--issuer`, `--jwks-uri`, `--audience` (all required); `--sub-claim`, `--email-claim`, `--userinfo-uri`, `--self-signup-policy <type:roleId>` (repeatable), `--tenant test\|live` | Idempotent by `--issuer-id` within the tenant — a repeat returns the existing registration unchanged, not an error. |
|
|
175
|
+
| `update <issuerId>` | `--context <contextId>` (required); `--sub-claim`, `--email-claim`, `--userinfo-uri`, `--status active\|suspended`, `--self-signup-policy <type:roleId>` (repeatable), `--clear-self-signup-policies`, `--tenant test\|live` | Only ever sends the SAFE, mutable fields — `issuer`/`jwks-uri`/`audience`/`contextId` are trust-anchor fields, immutable via this route on the platform side; rotating one is `delete` + `register`. |
|
|
176
|
+
| `delete <issuerId>` | `--context <contextId>` (required), `--force`, `--tenant test\|live` | Re-types the issuer id to confirm (mirrors `context destroy`); `--force` skips the prompt and is required when stdin isn't a TTY. Refused server-side while any user was ever created or matched through this issuer. |
|
|
177
|
+
| `list` | `--context <contextId>` (required), `--tenant test\|live` | Every issuer registered in that context. `--context` has no default — an issuer read is confined to the context you name; there's no tenant-wide "every issuer in every context" view. |
|
|
178
|
+
| `get <issuerId>` | `--context <contextId>` (required), `--tenant test\|live` | One issuer's full registration, including its self-signup policies. |
|
|
179
|
+
|
|
180
|
+
All five accept `--json`, plus the common auth flags (`--base-url`, `--token`).
|
|
181
|
+
|
|
182
|
+
## `vectros namespace` — scope-namespace registrations
|
|
183
|
+
|
|
184
|
+
Register and inspect the namespaces a `scope:<namespace>` value can belong
|
|
185
|
+
to — whether values in that namespace must resolve to a real identity entity
|
|
186
|
+
(`entityBacked`, the same field `identity create --type <namespace>`
|
|
187
|
+
relies on) or are free-form strings. Registering a brand-new namespace (e.g.
|
|
188
|
+
a `practice`/`team` entity dimension) was blueprint-apply-only before this
|
|
189
|
+
command existed.
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
vectros namespace register practice --context my-app --specificity-rank 1500 --entity-backed
|
|
193
|
+
vectros namespace list # tenant-wide registrations
|
|
194
|
+
vectros namespace list --context my-app # just my-app's own
|
|
195
|
+
vectros namespace get practice --context my-app
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
| Subcommand | Flags | Notes |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| `register <name>` | `--context`, `--specificity-rank <n>` (both required); `--entity-backed`, `--default-schema-id <id>` | NOT idempotent — a repeat name 400s (unlike `issuers register`). `--context` has no tenant-wide option here — a confined credential may only register against its own bound context. |
|
|
201
|
+
| `list` | `--context <contextId>` (optional) | Omit for every tenant-wide registration; supply for one context's own. |
|
|
202
|
+
| `get <name>` | `--context <contextId>` (optional) | Same two-mode read as `list`. |
|
|
203
|
+
|
|
204
|
+
No `update`/`delete` — both routes require a genuine root API key on the
|
|
205
|
+
platform side, which this CLI never holds, so neither could ever succeed
|
|
206
|
+
here. All three subcommands accept `--json`.
|
|
207
|
+
|
|
147
208
|
## `vectros identity` — tenant-wide principals
|
|
148
209
|
|
|
149
210
|
Manage the principals a context's access profiles and roles reference: users
|
|
@@ -158,6 +219,8 @@ vectros identity create --type org --external-id acme-inc --name "Acme Inc"
|
|
|
158
219
|
vectros identity list --type user
|
|
159
220
|
vectros identity get --type org --id <vectrosId>
|
|
160
221
|
vectros identity delete --type org --id <vectrosId>
|
|
222
|
+
vectros identity invite --context my-app --role editor --email bob@example.com \
|
|
223
|
+
--accept-url https://my-app.example.com/accept
|
|
161
224
|
```
|
|
162
225
|
|
|
163
226
|
| Subcommand | Flags | Notes |
|
|
@@ -166,11 +229,13 @@ vectros identity delete --type org --id <vectrosId>
|
|
|
166
229
|
| | `--type user` only: `--email <email>`, `--service` (mint a SERVICE principal; default HUMAN) | `--name`/`--scope` are rejected for `--type user`. |
|
|
167
230
|
| | entity namespaces only: `--name <name>`, `--scope <ns:value>` (repeatable, up to 2 parent ownership edges) | `--email`/`--service` are rejected for an entity namespace. |
|
|
168
231
|
| | both: `--metadata <json>` | Free-form JSON object. |
|
|
169
|
-
| `list` | `--type <user\|namespace>` (required), `--external-id <id>` (filter), `--limit <n>` | Omit `--limit` and the CLI drains every page for the complete set; pass `--limit` (1–100) and it returns exactly one page of that size instead. |
|
|
232
|
+
| `list` | `--type <user\|namespace>` (required), `--external-id <id>` (filter), `--limit <n>` | Scoped to your own context — see `list-all` below for a user created but never granted access anywhere. Omit `--limit` and the CLI drains every page for the complete set; pass `--limit` (1–100) and it returns exactly one page of that size instead. |
|
|
233
|
+
| `list-all` | `--external-id <id>` (filter), `--tenant test\|live`, `--limit <n>` | **Users only, and requires an account owner login.** Lists every user in the tenant, regardless of app-context access — for recovering a user `identity list` can never show you: one created but not (yet) granted access anywhere, so no context-scoped view can ever reach it, and a `create` retry with the same `--external-id` just collides with the same invisible row. Find it here by `--external-id`, then grant it access or `identity delete` it with an ordinary credential. |
|
|
170
234
|
| `get` | `--type <user\|namespace>` (required), `--id <vectrosId>` (required) | `--id` is the Vectros-assigned id from `create`/`list`, not the `--external-id`. |
|
|
171
235
|
| `delete` | `--type <user\|namespace>` (required), `--id <vectrosId>` (required) | |
|
|
236
|
+
| `invite` | `--context <c>`, `--role <roleId>`, `--email <e>` (all required); `--first-name <n>`, `--accept-url <url>`, `--no-send-email` | Onboards a person into a context by email (`POST /v1/users/invite`) — no root key required, your `vectros login` session already carries enough scope. `--role` is an existing Role's own id (no separate lookup). `--accept-url` is required unless `--no-send-email` is given, in which case the response carries the raw `inviteToken`/`acceptLink` for you to deliver yourself. |
|
|
172
237
|
|
|
173
|
-
`create`/`list`/`get` accept `--json`; `delete` does not.
|
|
238
|
+
`create`/`list`/`list-all`/`get`/`invite` accept `--json`; `delete` does not.
|
|
174
239
|
|
|
175
240
|
## `vectros role` — reusable, context-scoped access rules
|
|
176
241
|
|
|
@@ -185,6 +250,8 @@ vectros role create --context my-app --role-id editor --name Editor \
|
|
|
185
250
|
--actions records:cru,search:r
|
|
186
251
|
vectros role create --context my-app --role-id own-org --name "Own org" \
|
|
187
252
|
--actions records:r --data-scope '{"scope:org":["<orgId>", null]}'
|
|
253
|
+
vectros role create --context my-app --role-id hr-admin --name "HR Admin" \
|
|
254
|
+
--actions records:cru --assumable '{"scope:org":["org_engineering", "org_sales"]}'
|
|
188
255
|
vectros role list --context my-app
|
|
189
256
|
vectros role get --context my-app --role-id editor
|
|
190
257
|
vectros role delete --context my-app --role-id editor
|
|
@@ -192,13 +259,35 @@ vectros role delete --context my-app --role-id editor
|
|
|
192
259
|
|
|
193
260
|
| Subcommand | Flags | Notes |
|
|
194
261
|
|---|---|---|
|
|
195
|
-
| `create` | `--context <c>`, `--role-id <id>`, `--name <n>`, `--actions <csv>` (all required); `--data-scope <json>`, `--description <text>` | Idempotent by `roleId`. `--data-scope` confines the clause to owned data — a JSON map of ownership dimension → allowed values, ANDed across keys (e.g. `{"userId":["${{ self.userId }}"]}`, or `{"scope:org":["<orgId>", null]}` — the `null` entry additionally admits scope-less/tenant-shared records). Keys must be `userId` or `scope:<namespace>` (the legacy flat `orgId`/`clientId` keys are rejected server-side). |
|
|
262
|
+
| `create` | `--context <c>`, `--role-id <id>`, `--name <n>`, `--actions <csv>` (all required); `--data-scope <json>`, `--assumable <json>`, `--description <text>` | Idempotent by `roleId`. `--data-scope` confines the clause to owned data — a JSON map of ownership dimension → allowed values, ANDed across keys (e.g. `{"userId":["${{ self.userId }}"]}`, or `{"scope:org":["<orgId>", null]}` — the `null` entry additionally admits scope-less/tenant-shared records). Keys must be `userId` or `scope:<namespace>` (the legacy flat `orgId`/`clientId` keys are rejected server-side). `--assumable` is a SEPARATE, role-level grant — a JSON map of `scope:<namespace>` → allowed values naming which values a holder of this role may switch into via `vectros assume`; holding broad `--data-scope` reach in a namespace does not by itself grant assuming any value in it. |
|
|
196
263
|
| `list` | `--context <c>` (required) | |
|
|
197
|
-
| `get` | `--context <c>`, `--role-id <id>` (both required) | |
|
|
264
|
+
| `get` | `--context <c>`, `--role-id <id>` (both required) | Also shows the role's `assumable` grant, if any is set. |
|
|
198
265
|
| `delete` | `--context <c>`, `--role-id <id>` (both required) | No cascade — any access profile still referencing the role fails to mint (not silently downgraded) until reassigned. |
|
|
199
266
|
|
|
200
267
|
`create`/`list`/`get` accept `--json`; `delete` does not.
|
|
201
268
|
|
|
269
|
+
## `vectros assume` — switch identity namespace values on your own token
|
|
270
|
+
|
|
271
|
+
Re-mints your own verb token with one or more `identity.<namespace>` values
|
|
272
|
+
switched — the "org switcher" flow, named (like the platform endpoint it
|
|
273
|
+
calls) after AWS STS `AssumeRole`. Only works if a role you currently hold
|
|
274
|
+
explicitly grants assuming the requested value(s) via that role's own
|
|
275
|
+
`--assumable` grant (checked live against your CURRENT roles, never a value
|
|
276
|
+
frozen into your token at mint time).
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
vectros assume --context my-app --scope org:org_sales
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
| Flags | Notes |
|
|
283
|
+
|---|---|
|
|
284
|
+
| `--context <c>` (required), `--scope <ns:value>` (repeatable, at least one required) | Each `--scope` becomes one `scope:<namespace>` entry in the request. No artificial cap on how many namespaces you name — the platform requires a SINGLE role to jointly grant the whole combination and rejects an unsatisfiable one itself. |
|
|
285
|
+
|
|
286
|
+
Prints the re-minted `access_token`/`expires_in` (accepts `--json`) — there
|
|
287
|
+
is no `--to-keyring` here, since the result is an ephemeral bearer token
|
|
288
|
+
with the same remaining lifetime as the verb token this command itself
|
|
289
|
+
mints to make the call, not a durable secret the keyring is built to hold.
|
|
290
|
+
|
|
202
291
|
## `vectros access` — bind principals to contexts
|
|
203
292
|
|
|
204
293
|
The binding layer between an identity and a context: `access grant` joins a
|
|
@@ -224,14 +313,42 @@ shares this grammar.
|
|
|
224
313
|
|
|
225
314
|
| Subcommand | Flags | Notes |
|
|
226
315
|
|---|---|---|
|
|
227
|
-
| `grant` | `--principal <p>`, `--context <c>` (required); exactly one of `--role <roleId>` / `--actions <csv>`; `--scope <ns:value>` (repeatable), `--identity-overrides <json>` | Idempotent **upsert** — re-granting an existing binding updates it to the newly-declared shape (role/actions/overrides), it does not no-op. `--scope`/`--identity-overrides` extend the principal's stamped identity (e.g. `--scope org:<id>` → `scope:org`) — a credential can only stamp ownership its identity carries, so granting write access to org-scoped data requires that scope on the profile. When both are given, `--identity-overrides` wins on key conflicts. |
|
|
316
|
+
| `grant` | `--principal <p>`, `--context <c>` (required); exactly one of `--role <roleId>` (repeatable — 2+ compose additively) / `--actions <csv>`; `--scope <ns:value>` (repeatable), `--identity-overrides <json>` | Idempotent **upsert** — re-granting an existing binding updates it to the newly-declared shape (role(s)/actions/overrides), it does not no-op. Repeating `--role` composes: `--role case-handler --role hr-admin` grants each role's own clauses, concatenated in the order given — never merged, so each clause keeps meaning exactly what its own author wrote. `--scope`/`--identity-overrides` extend the principal's stamped identity (e.g. `--scope org:<id>` → `scope:org`) — a credential can only stamp ownership its identity carries, so granting write access to org-scoped data requires that scope on the profile. When both are given, `--identity-overrides` wins on key conflicts. |
|
|
228
317
|
| `revoke` | `--principal <p>`, `--context <c>` (required) | |
|
|
229
318
|
| `list` | Exactly one of `--context <c>` / `--principal <p>` | `--context` lists the context's members; `--principal` lists every context that principal is bound to. |
|
|
230
319
|
| `get` | `--principal <p>`, `--context <c>` (required) | |
|
|
231
|
-
| `explain` | `--principal <p>`, `--context <c>` (required) | Resolves the binding's clauses (following
|
|
320
|
+
| `explain` | `--principal <p>`, `--context <c>` (required) | Resolves the binding's clauses (following every referenced role and concatenating their clauses, if bound via one or more) and renders a concrete **access matrix** — a canned set of record-ownership archetypes (yours alone, another user's, org-shared, scope-less, …) evaluated against the clauses. A client-side preview for authoring-time sanity, not the platform's own evaluator: it models READ reachability only — a `c`/`u`/`d` action shown as granted is advisory, not a guarantee a specific write will succeed, since the platform's write rule (a clause must explicitly name a dimension to place a value into it) diverges from its read rule (an unmentioned dimension never narrows reads). |
|
|
232
321
|
|
|
233
322
|
`grant`/`list`/`get`/`explain` accept `--json`; `revoke` does not.
|
|
234
323
|
|
|
324
|
+
## `vectros join` — enroll and keep the credential, in one step
|
|
325
|
+
|
|
326
|
+
`join` is a **composition verb**: everything it does already exists as a
|
|
327
|
+
discrete command (`access grant`, `key issue`, `keyring add`) — `join` runs
|
|
328
|
+
them in order against one context and records the outcome as a keyring
|
|
329
|
+
entry, so `vectros switch <alias>` (and the MCP server / recall hook) can act
|
|
330
|
+
as that principal from then on.
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
vectros join my-app --role editor # enroll "me" via a role
|
|
334
|
+
vectros join my-app --role case-handler --role hr-admin # compose 2+ roles additively
|
|
335
|
+
vectros join my-app --actions records:cru,search:r --scope org:<orgId>
|
|
336
|
+
vectros join my-app --as usr_<id> --role editor --alias teammate
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
| Flag | Notes |
|
|
340
|
+
|---|---|
|
|
341
|
+
| `--role <roleId>` | Repeatable — 2+ compose additively, same semantics as `access grant --role`. XOR with `--actions`. |
|
|
342
|
+
| `--actions <csv>` | Inline single-clause grant. XOR with `--role`. |
|
|
343
|
+
| `--as me\|usr_<id>` (default `me`) | Which principal to enroll — `me` resolves to your own signed-in user. |
|
|
344
|
+
| `--scope <ns:value>` | Repeatable. Places the member under a scope for this context (an identity override on the profile) so their key can WRITE that scope's data. |
|
|
345
|
+
| `--alias <name>` | Keyring entry name (defaults to the context id). |
|
|
346
|
+
| `--activate` / `--no-activate` | Force (or skip) making the freshly-stored key this machine's active identity. |
|
|
347
|
+
|
|
348
|
+
Re-joining an alias whose secret is still held locally is idempotent: it
|
|
349
|
+
re-grants (a no-op upsert) and re-activates without churning the key. Only
|
|
350
|
+
when the local secret is missing does it rotate to obtain a fresh one.
|
|
351
|
+
|
|
235
352
|
## `vectros key` — the scoped API key (`ssk_*`) lifecycle
|
|
236
353
|
|
|
237
354
|
Mint, inspect, revoke, and rotate the scoped keys (`ssk_*`) that authenticate
|
|
@@ -368,9 +485,15 @@ already holds — those verbs can only disclose or redistribute access, never
|
|
|
368
485
|
manufacture new reach. `profiles:c` (creating a profile for an arbitrary,
|
|
369
486
|
caller-chosen principal name) still hard-rejects — **except** `profiles:c:self`,
|
|
370
487
|
which the platform resolves from the credential's own identity at request
|
|
371
|
-
time, never from anything the blueprint declares
|
|
372
|
-
|
|
373
|
-
|
|
488
|
+
time, never from anything the blueprint declares, and **except a bare
|
|
489
|
+
`profiles:c` on a clause that also declares the `member-lifecycle` capability**
|
|
490
|
+
(see Capabilities, below) — that combination is admitted because
|
|
491
|
+
`member-lifecycle` already grants unbounded principal-creation reach, so
|
|
492
|
+
gating bare `profiles:c` behind it adds no further technical bound but keeps
|
|
493
|
+
the same explicit, fail-closed opt-in signal every other admission here
|
|
494
|
+
requires. A literal-qualified `profiles:c:usr_<id>` naming another principal
|
|
495
|
+
still hard-rejects regardless of `member-lifecycle`, same as every other
|
|
496
|
+
control-plane scope.
|
|
374
497
|
|
|
375
498
|
### Capabilities (`granted_capabilities`) — a separate, equally-gated dimension
|
|
376
499
|
|
|
@@ -378,10 +501,10 @@ A blueprint's `accessProfile` and role clauses may also declare
|
|
|
378
501
|
`capabilities: [...]` — named platform effects (like creating or removing an
|
|
379
502
|
identity in your tenant) that reach across a partition boundary in a way no
|
|
380
503
|
`resource:ops` verb can express. This rides a field entirely separate from
|
|
381
|
-
`allowedActions`, and it has its own allowlist:
|
|
382
|
-
admitted today.** Every other capability name —
|
|
383
|
-
own registry recognizes — is refused, the same
|
|
384
|
-
posture as the scope gate above. `vectros blueprint validate`/`plan` and
|
|
504
|
+
`allowedActions`, and it has its own allowlist: **`member-lifecycle` and
|
|
505
|
+
`delegate-principal-stamp` are admitted today.** Every other capability name —
|
|
506
|
+
including ones the platform's own registry recognizes — is refused, the same
|
|
507
|
+
fail-closed, deliberately-widen posture as the scope gate above. `vectros blueprint validate`/`plan` and
|
|
385
508
|
`bootstrap` all report a rejected capability the same way they report a
|
|
386
509
|
rejected scope: nothing mints, and the CLI exits non-zero.
|
|
387
510
|
|
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Third-party notices
|
|
2
2
|
|
|
3
3
|
`@vectros-ai/cli` compiles the following third-party packages directly into its
|
|
4
|
-
published
|
|
5
|
-
|
|
4
|
+
published bundle. Their licenses require that these notices travel with the code,
|
|
5
|
+
so they are reproduced in full below.
|
|
6
6
|
|
|
7
|
-
This file is GENERATED by `pipelines/third_party_notices.mjs` from
|
|
8
|
-
|
|
7
|
+
This file is GENERATED by `pipelines/third_party_notices.mjs` from `packages/cli/tsup.config.ts`'s
|
|
8
|
+
`noExternal` list. Do not edit it by hand — run
|
|
9
9
|
`node pipelines/third_party_notices.mjs --write`. CI fails if it is stale, because a
|
|
10
10
|
notices file that silently drifts is worse than none: it asserts a compliance that
|
|
11
11
|
nobody checked.
|