You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(metadata-protocol,rest): gate the data path on object existence — the 404 the exposure gate assumed (#3770) (#3866)
`enforceApiAccess` passes through any object it cannot find in metadata, and
justified that with `// unknown object → let the data path 404`. That fallback
did not exist: `findData` (and every data entry point except `cloneData`) had no
existence check; the engine does not reject unregistered names (`resolveObjectName`
falls back to `resolveTableName({ name })`, so the object name IS the table name);
and the 404 was only ever a side effect of the driver erroring on a missing table,
recognised by string-matching that error in the REST layer.
So the 404 held only while the table happened not to exist. When one did — out-of-band
DDL, a registration that failed after `syncObjectSchema` had run, a registration race
— the exposure gate was silently skipped and the rows were served.
Adds `ObjectStackProtocolImplementation.assertObjectRegistered`, run before storage
is touched by findData, getData, createData, cloneData, updateData, deleteData,
batchData, createManyData, insertManyData, updateManyData, deleteManyData and
analyticsQuery. An object absent from the schema registry is rejected with
`OBJECT_NOT_FOUND` / 404 — an authoritative answer from the registry, raised before
the name becomes a table name. `cloneData`'s open-coded check becomes that shared
gate; its envelope is unchanged.
The gate sits at the protocol ingress, the same boundary `apiEnabled` guards:
internal callers (hooks, flows, migrations, raw ObjectQL) reach the engine directly
and are unaffected. When the engine exposes no schema registry there is nothing to
consult, so it stands down and warns once per process — the tiering #3545 recorded
in `api-exposure.ts` for a whole-registry outage.
`mapDataError` maps the protocol's `OBJECT_NOT_FOUND` to the canonical
`object_not_found` ApiErrorCode, byte-identical to the envelope the driver-string
branch already produced, so one condition has one wire code regardless of which
layer noticed. That branch stays as the safety net for the other failure it actually
covers: an object that IS registered but whose physical table is missing.
`sendError` defers to it so both funnels agree.
The misleading comment is replaced with what actually closes the hole — this gate
for existence, plugin-security's `unresolved` posture (#3545) for authorization —
and a note not to widen the exposure gate on the assumption that some other layer
404s.
Follow-up filed as #3867: the same shape in a different subsystem (analytics
`/query` on deployments running AnalyticsServicePlugin), plus a raw-SQL leak on
that route's error path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0 commit comments