Repository navigation
Commit 642dd4f
fix(metadata-protocol)!: a datasource saved through the metadata door may name only its own stored credentials reference (#27817)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D47r79fQtBBe4WAGvjKrwVi)_
Fixes #27762
domain:server#3 · os-elon-musk · session_016WV4cx2wNeyC4kD8R4P7Ns (card
thread)
Clause-②: no (narrowing) — the metadata save door refuses a `datasource`
body whose `external.credentialsRef` is not the reference its own stored
row already holds, where it stored the value as given before. Nothing is
widened.
Metadata-protocol: narrowed
Skills-docs: none — `skills/**` and `content/docs/**` teach
`external.credentialsRef` only on datasources declared in code
(`defineDatasource`, `*.datasource.ts`), which the metadata save door
does not serve (a code-defined name is refused there by the package
door); none teaches naming a reference on a runtime metadata save, and
the refusal itself names the door that binds one.
## 维护者速读
- 改了什么:通过元数据保存门(`PUT /api/v1/meta/datasource/:name`、运行时 `/meta` 门、MCP
与进程内保存,同一个保存入口)保存数据源时,`external.credentialsRef`
只能是这个数据源自己已存记录里的那个引用;新建时带引用、编辑时换成别的引用、或记录本没有引用却带了一个,一律 400
拒绝,且什么都不写,无论引用指向什么答复都相同,消息里不出现引用值。
- 为什么改:卡上要求按 #27582
的规则审第三个凭据引用写入口:「非系统写入只能封存新值,或保留本记录自己已存的引用」。实测这个门会原样保存请求带来的引用,不符合规则。
- 风险与回滚:拒绝发生在任何写入之前;读出再原样存回(GET → 编辑 →
PUT)会带回自己的引用,照常通过;不带这个键的保存不受影响;系统改写(已存元数据迁移)照常放行;已存记录不会被重新判定。包复制与非托管导入也走这个保存入口,复制出的新数据源若带着源数据源的引用,该项会被拒(复制结果里列为失败项)。控制台的数据源页面走数据源管理入口,不受影响。回滚只需
revert 本 PR。
-
先看:`packages/metadata-protocol/src/protocol-datasource-credentials-ref.ts`(规则本身),`packages/metadata-protocol/src/protocol.ts`
中 `saveMetaItem` 的那一处调用,两个新测试文件。
- 两分钟验证:`pnpm --filter @objectstack/metadata-protocol exec vitest run
src/protocol.datasource-credentials-ref-own-reference.test.ts`,预期 14/14
通过。
- 需要你做的:无,席位落地;本 PR 是收窄(narrowing),落地前需一次契约复审,由席位安排。
## What changes
The rule, as #27582 set it for `secret` fields (PR #27695) and #27722
applied it to the datasource admin door's create (PR #27761): **a
non-system write may only seal a new value or keep its own record's
stored reference.** The metadata save door seals nothing, so the one
reference a save through it may name is the one its own stored row
already holds.
- **The judge.** `datasourceCredentialsRefSaveRefusal`, a new module
beside `protocol.ts` (`protocol-datasource-credentials-ref.ts`). For a
`datasource` body that names `external.credentialsRef`, it reads the row
the save writes, the way the lock-family judge reads it: the
repository's write address for the save's state, package and bundle
member, and for a draft with no draft row yet, the active row it
overlays. The save is accepted only when that row holds exactly the same
reference. It is handed the repository's `headAt` read alone, so it can
read and cannot write.
- **The refusal.** `VALIDATION_ERROR` / 400, the standard-catalog pair
this door's own name refusal and the #27582 refusal already use. It is
thrown before anything is written. The message names the key and the
datasource, never the value, and nothing about the reference is looked
up, so the answer is the same whatever the reference names. It names the
remedy: send back the reference a read serves, omit the key, or bind a
secret through the datasource admin door.
- **Where it is asked.** `saveMetaItem`, beside the other
datasource-family save refusals: after the schema gate (the body is a
valid datasource, so `credentialsRef` is a string) and before the
authoring gates and every write. `protocol.ts` gains its import and one
call site; nothing in it moves.
- **Who passes.** The server-stated rewrites of a row the store already
holds (`source` set: the stored-metadata migration) keep the
pass-through, as the system principal does under the #27582 rule. Every
other save is judged: the REST `PUT` doors (`meta-envelope`), the
runtime dispatcher's `/meta` door (`meta-dispatch`), MCP and in-process
authoring, and the package copy (`package-duplicate`) and unmanaged
import (`package-unmanaged-import`), which write new rows that hold no
reference of their own.
- **Unchanged.** The round trip that sends back its own reference keeps
it. A body naming no reference saves as before. Rows already stored are
not re-judged.
## Why
The card's measurement, on `origin/main` at 968cacf: a non-system save
through this door stored a datasource body's `external.credentialsRef`
as given — on a create, on an edit naming another datasource's
reference, and on a draft. The pin below was committed first (d816620)
and was red on that tree in all ten refusal cases.
## Tests
-
`packages/metadata-protocol/src/protocol.datasource-credentials-ref-own-reference.test.ts`
(new, 14 cases): the real `saveMetaItem` over a stub engine.
- Refused, with `code` and `status` asserted, the store byte-identical
afterwards and no write issued: a create naming a reference another
datasource holds, and one naming a reference no row holds (identical
answers); an edit naming a reference other than its own (identical
answers for a held and an unheld one); an edit naming one where its row
holds none; a draft naming one the active row does not hold; the plural
type spelling; and each face — `meta-envelope`, `meta-dispatch`,
`package-duplicate`, `package-unmanaged-import`, an MCP save. No refusal
message contains any reference.
- Accepted: the GET → edit → PUT round trip keeps the row's own
reference; a first draft sending back the active row's reference keeps
it; a body with no reference; the `source: 'migrate-stored'`
pass-through.
-
`packages/qa/dogfood/test/datasource-credentials-ref-own-reference.dogfood.test.ts`
(new, 3 cases): the same rule at `PUT /api/v1/meta/datasource/:name` of
a booted stack, as a signed-in non-system caller — a create refused with
one answer and nothing stored, an edit refused with the stored reference
kept, and the round trip accepted with the reference kept.
- The pin's stub engine is held to the engine-double gates: its `find`
applies the caller's `limit` after the filter, and
`scripts/engine-double-contract.pinned.json` records its `findOne` and
`update` seams (regenerated with `--write`; 2 rows added, none lost).
Readings: the unit pin on `origin/main` 968cacf, before the fix, **10
failed | 4 passed (14)** (the four that passed are the accepted cases).
With the fix: **14 passed (14)**; the dogfood pin **3 passed (3)**.
## Acceptance notes
- **Keeping the stored reference** is implemented as: a save that sends
back its own row's reference keeps it. A save that omits the key stores
the body without one, exactly as before this change; carrying a
reference forward on omission would change what an omitted key means, so
it is not done here.
- **The package copy and the unmanaged import are judged too.** Both
write a new row, which holds no reference of its own, so a copied or
imported datasource body that names a reference is refused. In a package
copy that item lands in the copy response's `failed[]` with the refusal
text and the rest of the package copies on; in an import the item is
refused the same way.
- **Not changed here:** rows already stored are not re-judged, and the
restore doors (rollback, revert) restore an item's own stored history as
before. Both are outside this card's door; a follow-up is the seat's
call.
- **`protocol.ts`** gains one import and one call-site block; nothing in
it moves or reflows (serial queue #25467).
- **Declarations:** `Metadata-protocol: narrowed` (the save door refuses
a body it accepted), hence a `minor` bump on
`@objectstack/metadata-protocol` and the `!` title. ADR-0087:
`not-required (no-migration-prescription)` — the refusal prescribes a
remedy for the request, not a migration of stored data.
## Verification
All at the branch head unless stated. Heavy runs under
`scripts/pm/os-verify-lock.sh`.
- `pnpm --filter @objectstack/metadata-protocol typecheck` exit 0;
`vitest run` over the whole package at 7e9b721 (main merged in): **281
passed | 4 skipped (285) files, 21816 passed | 26 skipped tests**, exit
0.
- `pnpm exec turbo run build --concurrency=2
--filter=!@objectstack/docs` at 7e9b721: **114/114** tasks, exit 0.
- `node scripts/pm/dispatch-gates.mjs --ran`: **73 derived, 73 run, 0
NOT-MEASURED, 0 UNRUN**, exit 0. 63 families ran at 7e9b721;
`check:engine-double-contract` and `check:objectql-double-limit` were
red there on the new pin's stub engine, were fixed in 7256944 and
re-ran green with the 8 families that commit's `scripts/` file adds.
`pnpm lint` exit 0.
- **Unit ablations** of the judge, each through
`scripts/ablation-replace.mjs` (mutate, run the pin, restore; the
restore proven blob-equal to HEAD):
- every caller takes the pass-through → 10 failed | 4 passed;
- any stored reference counts, not only the same one → 2 failed | 12
passed;
- a first draft no longer reads the active row it overlays → 1 failed |
13 passed;
- the refusal message carries the value → 10 failed | 4 passed;
- the server-stated rewrite loses its pass-through → 1 failed | 13
passed. (The first attempt at this leg was refused by `ablation-replace`
before any test ran — its replacement was a substring of its anchor, so
the count could not rise — and applied no mutation; it was redone with a
distinct anchor.)
- **Dist ablation** at the REST door: the call site disabled in
`protocol.ts`, `@objectstack/metadata-protocol` rebuilt,
`scripts/ablation-dist-preflight.mjs` proved the marker in `dist/` (exit
0), the dogfood pin **3 failed (3)**; restored (`git diff` empty),
rebuilt, preflight `--absent` exit 0, the pin **3 passed (3)**;
`protocol.ts` blob equal to HEAD.
- **Dogfood battery** (`pnpm --filter @objectstack/dogfood test`, the
whole battery, at 7e9b721): **354 passed | 2 failed | 3 skipped (359)
files, 2867 passed | 3 failed | 20 skipped (2890) tests**. The three
failures are not this change's: `error-sentences-live.dogfood.test.ts`
(2) and `object-hook-lifecycle-scratch.dogfood.test.ts` (1) assert that
an undeclared hook abort on the `/data` door answers with no `code`, and
#27667 (21c4bdd, on `main`) now answers `HOOK_ABORTED` there; neither
test is changed on `origin/main` at c5f4a06. The commit after 7e9b721
touches only a unit-test double and a gate ledger, nothing the battery
loads.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(not touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: no.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the maintainer brief
excepted).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016WV4cx2wNeyC4kD8R4P7Ns)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4cx2wNeyC4kD8R4P7Ns
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 755e285 commit 642dd4f
6 files changed
Lines changed: 549 additions & 0 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- qa/dogfood/test
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
0 commit comments