Skip to content

Commit 5091da3

Browse files
os-litantclaude
andauthored
feat(cli): build the cross-producer sys_secret reference union (#12663) (#12754)
* wip: secret reference union * wip: fix default-parameter trap in the undeclared-datasources pin * wip: mask positive control + changeset * wip: registerObject packageId for the built declaration * wip: find double holds the caller's bound; no-limit note on the union reads --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 86df0c9 commit 5091da3

3 files changed

Lines changed: 1147 additions & 0 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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

Comments
 (0)