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
9 changes: 9 additions & 0 deletions .changeset/19441-plugin-security-ledger-rows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@objectstack/spec': minor
---

`ERROR_CODE_LEDGER['@objectstack/plugin-security']` now lists the three codes the package stamps as class fields and ships in `dist`: `INVALID_STATE` (`PermissionSetOverlayStateError`, 409), `NOT_FOUND` (`PermissionSetNotFoundError`, 404) and `NOT_OVERRIDABLE` (`PackagedPermissionSetLockedError` and `PackagedPermissionSetProvenanceUnknownError`, 403) (#19441).

Clause-②: yes

Provenance, not identity: each code was already registered under another package (`@objectstack/rest`, `@objectstack/metadata-protocol`), so the `ErrorCode` union, the wire, and every other package's rows are unchanged. What widens is the per-package face a consumer reads from `ERROR_CODE_LEDGER['@objectstack/plugin-security']`. The `NOT_FOUND` synonym waiver's `reason` text now names plugin-security among its emitters; its `code` and `shadows` are unchanged. Nothing to migrate.
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,12 @@ export function classifyPackagedPermissionSet(
*
* `NOT_OVERRIDABLE` / 403 is deliberately the SAME envelope the metadata
* protocol's ADR-0005 tier gate already answers with for this exact condition
* — one condition, one vocabulary (ADR-0112's closed set; the code is a
* StandardErrorCode, so no ledger entry is minted). What changes is the
* — one condition, one vocabulary. The code is NOT a `StandardErrorCode`
* member: it is an ADR-0112 registered extension code, and because this
* package stamps it, `ERROR_CODE_LEDGER['@objectstack/plugin-security']`
* lists it beside metadata-protocol's row (provenance, not identity — a code
* shipped in `dist` is registered under every package that stamps it; the
* same holds for the two overlay-discard codes). What changes is the
* MESSAGE: the producer's says the type has not opted into overlay writes,
* which tells an admin nothing they can act on. The ruling's whole point is
* that the refusal teaches the sanctioned path.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ import { readDeclared } from './bootstrap-declared-permissions.js';
import { PermissionDeniedError } from './errors.js';
import { isTenantAdmin } from './delegated-admin-gate.js';

/** Thrown when the referenced `sys_permission_set` row does not exist → HTTP 404. */
/**
* Thrown when the referenced `sys_permission_set` row does not exist → HTTP 404.
* `NOT_FOUND` is listed under `ERROR_CODE_LEDGER['@objectstack/plugin-security']`
* (provenance row — this package stamps it).
*/
export class PermissionSetNotFoundError extends Error {
readonly code = 'NOT_FOUND';
readonly statusCode = 404;
Expand All @@ -96,7 +100,11 @@ export class PermissionSetNotFoundError extends Error {
}
}

/** Thrown when there is no active overlay to discard → HTTP 409. */
/**
* Thrown when there is no active overlay to discard → HTTP 409.
* `INVALID_STATE` is listed under `ERROR_CODE_LEDGER['@objectstack/plugin-security']`
* (provenance row — this package stamps it).
*/
export class PermissionSetOverlayStateError extends Error {
readonly code = 'INVALID_STATE';
readonly statusCode = 409;
Expand Down
25 changes: 25 additions & 0 deletions packages/spec/src/api/error-code-ledger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,31 @@ describe('ErrorCode (standard ∪ registered)', () => {
expect(ERROR_CODE_LEDGER['@objectstack/objectql']).toContain('NAMESPACE_CONFLICT');
});

it('lists the plugin-security class-field stamps under their stamping package (#19441)', () => {
// Provenance, not identity: each of these codes was already registered by
// another package, and `@objectstack/plugin-security` stamps it too, as a
// class field (`readonly code = '…'`) — the spelling the provenance gate
// declares itself blind to, so this suite is what holds the rows.
const stamps: Record<string, keyof typeof ERROR_CODE_LEDGER> = {
INVALID_STATE: '@objectstack/rest', // PermissionSetOverlayStateError, 409
NOT_FOUND: '@objectstack/rest', // PermissionSetNotFoundError, 404
NOT_OVERRIDABLE: '@objectstack/metadata-protocol', // PackagedPermissionSet{Locked,ProvenanceUnknown}Error, 403
};
for (const [code, firstOwner] of Object.entries(stamps)) {
expect(ERROR_CODE_LEDGER['@objectstack/plugin-security'], `${code} listed under plugin-security`)
.toContain(code);
expect(ERROR_CODE_LEDGER[firstOwner], `${code} still listed under ${firstOwner}`).toContain(code);
expect(ErrorCode.parse(code)).toBe(code);
}
// The fact the exempting comment in `packaged-permission-set-lock.ts` had
// backwards: NOT_OVERRIDABLE is an extension code, not a standard member —
// so "no ledger entry is minted" never followed. Control leg: the same
// membership test answers true for a standard member.
const standard = new Set<string>(StandardErrorCode.options);
expect(standard.has('NOT_OVERRIDABLE')).toBe(false);
expect(standard.has('PERMISSION_DENIED')).toBe(true);
});

it('rejects unregistered, lowercase, and numeric codes', () => {
expect(() => ErrorCode.parse('TOTALLY_MADE_UP_CODE')).toThrow();
expect(() => ErrorCode.parse('validation_error')).toThrow(); // pre-ADR-0112 dialect
Expand Down
35 changes: 33 additions & 2 deletions packages/spec/src/api/error-code-ledger.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,36 @@ export const ERROR_CODE_LEDGER = {
// packages is listed once per emitting package — provenance, not identity
// (see above; #7504).
'INVALID_METADATA',
// [#19441] Provenance rows for the class-field stamps
// (`readonly code = '…'`) this package ships in `dist` — a spelling
// `check:error-code-provenance` declares itself blind to, which is how
// they went unlisted. Each code is already registered by another package;
// per this file's header a code emitted by several packages is listed
// once per emitting package — provenance, not identity — so the union,
// its casing and every other package's rows are unchanged.
//
// `PermissionSetOverlayStateError` (`permission-set-overlay-discard.ts`),
// 409: `discardPermissionSetOverlay` found no active overlay to discard.
// Served by `@objectstack/rest`'s `POST …/security/permission-sets/:id/
// discard-overlay` route, whose `handleError` reads the thrown `code`
// ahead of its `INTERNAL` default.
'INVALID_STATE',
// `PermissionSetNotFoundError` (`permission-set-overlay-discard.ts`),
// 404: the addressed `sys_permission_set` row does not exist. Same route
// and door as the row above. A waived synonym of `RESOURCE_NOT_FOUND`
// (`STANDARD_SYNONYM_WAIVERS` admits it per code, not per package — this
// row widens that waiver's emitter list, never the waiver table).
'NOT_FOUND',
// `PackagedPermissionSetLockedError` and
// `PackagedPermissionSetProvenanceUnknownError`
// (`packaged-permission-set-lock.ts`), 403: a data-path write targets a
// package-declared permission set, or one whose provenance cannot be
// determined (fail-closed). Deliberately the SAME envelope
// `@objectstack/metadata-protocol`'s ADR-0005 tier gate answers for this
// condition — one condition, one vocabulary. NOT a `StandardErrorCode`
// member: it is an extension code registered here, under that package and
// now this one.
'NOT_OVERRIDABLE',
'SUGGESTION_NOT_FOUND',
'SUGGESTION_STATE', // suggestion exists but is not in a confirmable/dismissable state
// [#19307] The data door's duplicate-name refusal on `sys_permission_set`
Expand Down Expand Up @@ -1507,8 +1537,9 @@ export const STANDARD_SYNONYM_WAIVERS: readonly StandardSynonymWaiver[] = [
{
code: 'NOT_FOUND',
shadows: 'RESOURCE_NOT_FOUND',
reason: 'Pre-gate synonym on the wire from @objectstack/rest and plugin-sharing. ' +
'Wire value kept; consolidation deferred per #8211.',
reason: 'Pre-gate synonym on the wire from @objectstack/rest and plugin-sharing; ' +
'#19441 added the plugin-security provenance row for the same pre-existing wire value ' +
'(its permission-set overlay-discard 404). Wire value kept; consolidation deferred per #8211.',
},
{
code: 'UNAUTHORIZED',
Expand Down
Loading