Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .changeset/runtime-seed-apply-driver-text.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
---
"@objectstack/runtime": patch
"@objectstack/metadata-protocol": patch
---

fix(runtime): the package-publish door no longer discloses driver text on `seedApplied` (#8443)

`POST /api/v1/packages/:id/publish-drafts` answered, on a **200**:

```json
{ "success": true, "data": { "seedApplied": {
"success": false, "error": "SQLITE_ERROR: no such table: sys_metadata" } } }
```

The door keeps a route-level seed apply for protocols that do not apply seeds
inside `publishPackageDrafts` themselves. That fallback is a second copy of
`metadata-protocol`'s `applySeedBodies`, and it kept the ADR-0112 defect the
original was fixed for: a caught error's sentence interpolated onto a
client-facing payload. `seedApplied` rides on a success body as **data**, so no
HTTP boundary's 5xx message withhold can reach it — the disclosure had to be
closed at the producer.

Driven for real before being changed, which found **two** carriers on that one
field rather than the one reported:

- the door's `catch` — a driver failure under the seed loader's
dependency-graph read, which is unguarded;
- the per-read `errors[]` entries — a driver failure reading the just-published
seed body back. This is the carrier a `sys_metadata` outage reaches first, so
a fix confined to the `catch` would have left the commonest outage shape
disclosing exactly as before.

Both now follow the rule already in force next door: a caught sentence is
quoted only when the error **declared** itself a client-facing refusal (4xx
`status`); anything else gets a stable line and the original goes to the server
log.

**Authoring feedback is preserved, not blanked.** A malformed seed body used to
arrive in the same `catch` as a raw `ZodError` — undeclared, so the withhold
would have replaced a real authoring error with `seed apply failed`. The seed
request is now parsed with `safeParse` and its rejection minted as a declared
`INVALID_METADATA` / 422, so the author receives a curated summary naming the
seed and the key (strictly better than the multi-line dump of zod internals the
field used to carry). Self-correcting refusals such as `[item_locked]` continue
to reach the caller verbatim.

`@objectstack/metadata-protocol` exports `clientFacingFailureText` and
`seedRequestValidationError` so the runtime door applies the producer's own
decision instead of restating it — **an enabling export only; no behaviour in
that package changes.**
10 changes: 10 additions & 0 deletions packages/metadata-protocol/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,16 @@ export { ObjectStackProtocolImplementation, ConcurrentUpdateError, normalizeView
// ObjectQL FALLBACK in `@objectstack/runtime`'s `callData` builds the SAME one
// instead of minting a second not-found shape. See `recordNotFoundError`.
export { recordNotFoundError } from './protocol.js';
// [#8443] The ADR-0112 disclosure rule (#8086 / #8136 / #8333), exported for
// the SECOND seed-apply producer: `@objectstack/runtime`'s package-publish door
// keeps a fallback apply for protocols that do not self-apply, and it reports
// failure as data on the same `seedApplied` field. Both halves travel together
// because both are needed to apply the rule without losing authoring feedback:
// `clientFacingFailureText` withholds what was never declared, and
// `seedRequestValidationError` is what DECLARES the one population that must
// still be quoted (a malformed seed body). Exporting is enabling-only — no
// behaviour in this package changes.
export { clientFacingFailureText, seedRequestValidationError } from './protocol.js';
// [#7823] The write-response half of the `internal: true` guarantee — THE
// single helper every generic write ingress routes its response records
// through (A-prime ruling, 2026-08-13). Tripwire-enforced; see the module
Expand Down
23 changes: 21 additions & 2 deletions packages/metadata-protocol/src/protocol.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1641,8 +1641,19 @@ function overlayDeleteFailureMessage(err: unknown, type: string, name: string):
* existing no-message fallback at every call site (`'delete failed'`,
* `'cleanup failed'`), so the withheld case reuses the sentence the caller
* could already receive rather than inventing a second vocabulary.
*
* [#8443] EXPORTED (via `index.ts`) for the same reason `recordNotFoundError`
* is: `@objectstack/runtime`'s package-publish door carries a seed-apply
* fallback for protocols that do not self-apply, and it reports failure as
* data on the same `seedApplied` field this package's `applySeedBodies` does.
* A second private restatement of the rule over there is exactly how the rule
* drifts out of sync — the withhold and the quote must be ONE decision, in one
* place, for both producers. `declaresClientRefusal` stays private on purpose:
* nothing outside this file needs the raw predicate, and an exported surface
* with no consumer is the "declared but nobody pulls it" shape AGENTS.md
* treats as debt.
*/
function clientFacingFailureText(err: unknown, fallback: string): string {
export function clientFacingFailureText(err: unknown, fallback: string): string {
if (declaresClientRefusal(err)) {
const declared = (err as { message?: unknown } | null | undefined)?.message;
if (typeof declared === 'string' && declared.length > 0) return declared;
Expand DownExpand Up@@ -1681,8 +1692,16 @@ function clientFacingFailureText(err: unknown, fallback: string): string {
* `ZodError`, so `seedApplied.error` was a multi-line JSON dump of raw zod
* internals. This is the curated summary {@link zodIssuesToMetadataIssues}
* already produces for every other authoring surface.
*
* [#8443] EXPORTED alongside {@link clientFacingFailureText}: the runtime
* package-publish door parses the SAME `SeedLoaderRequestSchema` in its own
* seed-apply fallback and hits the identical two-population catch, so it needs
* the identical declaration — same sentence, same `INVALID_METADATA`/422, same
* curated `issues`. Minting a second 422 over there would give one authoring
* mistake two different envelopes depending on which protocol served the
* publish.
*/
function seedRequestValidationError(zodIssues: unknown): Error {
export function seedRequestValidationError(zodIssues: unknown): Error {
const issues = zodIssuesToMetadataIssues(zodIssues);
const summary = issues.slice(0, 3)
.map((i: { path: string; message: string }) => `${i.path || '<root>'}: ${i.message}`)
Expand Down
Loading
Loading