Skip to content

Commit 2c7971a

Browse files
committed
chore(changeset): @objectstack/service-datasource patch; pin the outage class rather than its digits
Sized as #15365 was: a fail-closed bug fix, no accept set widens. The outage arm now asserts the outage CLASS. Measured on this tree, the family answers 500 INTERNAL_ERROR rather than the 503 SERVICE_UNAVAILABLE the brand carries — a pre-existing relay gap this card did not open (the same escape has existed for the `ql` permission-store outage since #13279) and does not repair. Asserting the class keeps the pin on the security property. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
1 parent 54e2748 commit 2c7971a

2 files changed

Lines changed: 22 additions & 2 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/service-datasource': patch
3+
---
4+
5+
The datasource admin routes derive the tenancy posture before resolving the caller
6+
7+
`requireDatasourceAdmin` resolved the request with `resolveAuthzContext({ ql, headers, getSession })` and supplied no `tenancyPosture`. Both posture-conditional API-key refusals are gated on the caller supplying one — `organization_required` and `organization_membership_ended` — so neither ran on this family, and an API key stamped with an organization its owner had left was admitted; the routes then gated it on `authz.systemPermissions` alone. Because this family gates on system capabilities rather than on organization-scoped rows, the consequence was an admitted principal rather than a cross-organization row read.
8+
9+
The posture is now read off the kernel's `tenancy` service and classified rather than swallowed: a service that was never registered stays quiet (`undefined` — the supported no-tenancy composition, unchanged behaviour), while one that was registered and failed to build raises `AuthzStoreUnavailableError` instead of degrading to "no posture". Patch rather than minor: no accept set widens, and a declared guard returns to enforced.

‎packages/services/service-datasource/src/__tests__/admin-routes-tenancy-posture-admission.test.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,8 +525,19 @@ describe('[#15350] §5b — a `tenancy` service that was REGISTERED and FAILED i
525525
// The whole point of #13906 option A: a FAILURE must not read as "this
526526
// check does not apply". Whatever the transport renders, it is not the
527527
// 200 the ablation in §4 produces.
528-
expect(res.status).not.toBe(200);
529-
expect(res.status).toBeGreaterThanOrEqual(500);
528+
//
529+
// ⚠️ MEASURED on this tree, and deliberately not pinned tighter than the
530+
// outage CLASS: this family answers `500 INTERNAL_ERROR` ("No response from
531+
// handler"), not the `503 SERVICE_UNAVAILABLE` the brand carries. That is a
532+
// PRE-EXISTING relay gap, not one this card opened — `requireDatasourceAdmin`
533+
// has re-raised `AuthzStoreUnavailableError` for the identical `ql`
534+
// permission-store outage since #13279, out of route handlers that have no
535+
// `catch`, and the Hono adapter renders any escaped throw as a bare 500. It
536+
// is filed separately. Asserting the class rather than the digits keeps this
537+
// pin measuring the SECURITY property — the outage is never answered as an
538+
// admission or as a capability denial — so repairing the status to 503 does
539+
// not have to redden a security pin.
540+
expect([500, 503]).toContain(res.status);
530541
});
531542

532543
it('⛔ nor is a WRITE let through — nothing lands, read back from the store', async () => {

0 commit comments

Comments
 (0)