|
| 1 | +--- |
| 2 | +"@objectstack/cli": patch |
| 3 | +--- |
| 4 | + |
| 5 | +feat(cli): build the cross-producer `sys_secret` reference union — the primitive a safe orphan sweep needs (#12663) |
| 6 | + |
| 7 | +`sys_secret` has three privileged producers, each holding its handle in a |
| 8 | +column of its own: `SettingsService` (a bare `sec_…` in |
| 9 | +`sys_setting.value_enc`), the engine's `secret`-typed field channel |
| 10 | +(`secret:<id>` on an arbitrary business row) and the datasource credential |
| 11 | +binder (`sys_secret:<id>` at a datasource artefact's |
| 12 | +`external.credentialsRef`). Nothing enumerated all three, so the only sound |
| 13 | +deletion predicate — "attributable AND unreferenced by the COMPLETE union" — |
| 14 | +had no union to stand on. `packages/cli/src/utils/secret-reference-union.ts` |
| 15 | +is that union, read-only across all three surfaces. |
| 16 | + |
| 17 | +Why the shipped report-only classifier is not enough, reproduced against real |
| 18 | +code in the new test file: `classifySysSecretRows` attributes a row by |
| 19 | +`(namespace, key)` membership in the settings manifests' encrypted specifiers, |
| 20 | +and that is a **name match, not ownership** — `sys_secret` carries no producer |
| 21 | +column and the three producers write those two columns with three different |
| 22 | +meanings. A live, engine-owned credential on an object named like a settings |
| 23 | +namespace, with a field named like a specifier key, classifies `orphaned` |
| 24 | +today. There is no recovery from acting on that: the audit trail records |
| 25 | +digests, not handles, so nothing can name the destroyed handle afterwards. |
| 26 | + |
| 27 | +Completeness is therefore the whole contract, and it is structural rather than |
| 28 | +asserted: |
| 29 | + |
| 30 | +- the family set is closed and the assembler takes a `Record` over it, so |
| 31 | + omitting a producer is a type error, not a smaller union; |
| 32 | +- a read that did not happen is a declared **gap**, never an empty answer — a |
| 33 | + missing driver, a throwing read, an unparseable artefact, or a host that did |
| 34 | + not declare its code-defined datasources all make the union |
| 35 | + `complete: false`, and `assertSecretReferenceUnionComplete` refuses it with |
| 36 | + the ADR-0112 pair `PRECONDITION_REQUIRED` / 428; |
| 37 | +- family 2 is enumerated from the metadata registry on every call, because its |
| 38 | + holders are every `secret`-typed field on every registered object, |
| 39 | + tenant-authored ones included — a newly registered secret field is in the |
| 40 | + union with no code change. |
| 41 | + |
| 42 | +Reads go to the driver through the engine's public `getDriverForObject()`: the |
| 43 | +`secret:` ref only exists at that level (the read path masks it |
| 44 | +unconditionally), and any scoped read would silently under-report — the |
| 45 | +direction that deletes live credentials. |
| 46 | + |
| 47 | +`patch` rather than `minor`: this adds no surface to the package's entry |
| 48 | +barrel and no command or flag. It is an internal primitive whose named reader |
| 49 | +is the deletion half of #8103, in this same package; publishing it as an |
| 50 | +external API is a separate decision with its own changeset. Read-only by |
| 51 | +construction — nothing here writes, deletes or decrypts, and it contains no |
| 52 | +deletion command, dry-run or sweep. |
0 commit comments