From df71b8d75c349a0edc205bac73225aeb70852efd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 04:10:21 +0000 Subject: [PATCH 1/2] =?UTF-8?q?test(rest):=20pin=20the=20approvals=20FORBI?= =?UTF-8?q?DDEN=20=E2=86=92=20403=20row's=20live=20emission?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `handleApprovalError`'s `[/^FORBIDDEN/, 403, 'FORBIDDEN']` row is the one every authorisation refusal rides, and it had no live-emission pin: the service suites assert the `FORBIDDEN:` message prefix at the throw site, which is a different fact from what the route answers on the wire. Adds one `it()` to `rest-approvals-wire-codes.test.ts` driving the real recall route with a service that throws the real refusal, asserting status 403, `code === 'FORBIDDEN'`, and that the [#13095] anchored strip removed the prefix. Losing the row fails closed (500 `APPROVAL_RECALL_FAILED` with the raw message), so the third assertion catches the strip half of the regression as well as the status half. Test-only; no production behaviour changed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza --- .../src/rest-approvals-wire-codes.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/rest/src/rest-approvals-wire-codes.test.ts b/packages/rest/src/rest-approvals-wire-codes.test.ts index 4745193b7e..482e517e9c 100644 --- a/packages/rest/src/rest-approvals-wire-codes.test.ts +++ b/packages/rest/src/rest-approvals-wire-codes.test.ts @@ -28,6 +28,11 @@ * route. The derivation mirrors the production template exactly * (single-occurrence `.replace('-', '_')` included), so a route name the * template would mangle into an invalid code also fails here. + * [#14573] The file has since become the home for the approvals door's + * live-emission pins generally, not only the #8885 population: the + * `FORBIDDEN` → 403 case below pins a row that was already registered + * vocabulary but whose EMISSION nobody observed. See its own comment. + * * 3. The union stays CLOSED — the control case in * `rest-field-visibility-fault-envelope.test.ts` covers this file too (same * schema instance); membership green here is evidence, not vacuity. @@ -35,6 +40,7 @@ import { describe, it, expect, vi } from 'vitest'; import { ApiErrorSchema } from '@objectstack/spec/api'; +import { BUILTIN_OPERATION_MESSAGES } from '@objectstack/spec/system'; // `.js` on purpose — NodeNext resolution requires the extension (#7248). import { RestServer } from './rest-server.js'; @@ -115,6 +121,48 @@ describe('approvals wire codes are registered vocabulary (#8885)', () => { ).toBe(true); }); + // [#14573] The `FORBIDDEN` → 403 row is the one EVERY authorisation + // refusal rides: recall by a non-submitter, decide by a non-approver, + // reassign / remind / sendBack / resubmit by the wrong actor, and + // `resolveActor`'s impersonation refusals all reach this table through + // the same single row. Until this case it was the only mapping row whose + // 403 nobody observed on the wire — the service suites + // (`recall-refusal-user-copy.test.ts`, `approval-revise.test.ts`) assert + // the `FORBIDDEN:` MESSAGE PREFIX at the throw site, which is a different + // fact from what the route answers. + // + // Losing the row FAILS CLOSED, which is why this is a contract pin and not + // a security one: `handleApprovalError` returns false on no match, the + // caller rethrows, and the terminal catch answers 500 + // `APPROVAL_RECALL_FAILED`. The caller is still refused — at the wrong + // status, with the wrong code, and (because that arm forwards + // `String(error?.message ?? error)` verbatim) with the raw `FORBIDDEN: ` + // token the [#13095] anchored strip exists to remove. Two contract + // properties ride this one row, so the third assertion below is NOT + // redundant with the first two: it is what catches the degraded shape's + // unstripped message. + it('recall by a non-submitter answers 403 FORBIDDEN, prefix stripped — the row every authorisation refusal rides', async () => { + // The message the real service throws: `approval-service.ts`'s recall + // non-submitter branch is `FORBIDDEN: ${userFacingRefusal(...)}`. + // Read from the catalog rather than transcribed so a [#11993] copy + // edit cannot red this pin for a reason that is not the wire contract + // — the same construction `approval-revise.test.ts` uses. + const refusal = BUILTIN_OPERATION_MESSAGES.en.approval_recall_not_submitter; + const rest = boot({ + recall: vi.fn().mockRejectedValue(new Error(`FORBIDDEN: ${refusal}`)), + }); + const answer = await drive(rest, 'POST', `${REQ}/recall`); + expect(answer.status).toBe(403); + expect(answer.body?.code).toBe('FORBIDDEN'); + // [#13095] Exactly the `FORBIDDEN:` this row just answered comes off, + // and the user-facing sentence reaches the wire whole. + expect(answer.body?.error).toBe(refusal); + expect( + ApiErrorSchema.safeParse({ code: answer.body?.code, message: answer.body?.error }).success, + 'FORBIDDEN must be in StandardErrorCode ∪ ERROR_CODE_LEDGER', + ).toBe(true); + }); + // [#13182] `READ_BACK_FAILED` is a NAMED wire row (the RESUME_FAILED // precedent: a genuine server-side inconsistency, but named): the write is // recorded and NOT rolled back, the read-back is org-filtered, and the From 9a278f17199a517c30870e32e2b75aa896a49896 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 04:36:11 +0000 Subject: [PATCH 2/2] =?UTF-8?q?test(rest):=20record=20that=20=C2=A77=20alr?= =?UTF-8?q?eady=20pins=20this=20row=20=E2=80=94=20the=20card's=20premise?= =?UTF-8?q?=20is=20false?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured, not read: deleting `[/^FORBIDDEN/, 403, 'FORBIDDEN']` from `handleApprovalError` reds THREE cases, not one — this new pin and both cases of `rest-data-door-code-prefix.test.ts` §7, which already drive the real approve route and already assert 403, `code: 'FORBIDDEN'` and the anchored strip. #14573 was filed and triaged on the reading that the row had no live-emission pin anywhere. That is wrong. What is true is narrower: the file that OWNS the approvals wire-code contract did not pin it, so an audit of wire codes here saw a gap a strip-contract file was silently covering. Naming §7 from here is half the fix — the unlabelled duplicate is what got the card mis-filed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza --- .../src/rest-approvals-wire-codes.test.ts | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/rest/src/rest-approvals-wire-codes.test.ts b/packages/rest/src/rest-approvals-wire-codes.test.ts index 482e517e9c..145b3a42ae 100644 --- a/packages/rest/src/rest-approvals-wire-codes.test.ts +++ b/packages/rest/src/rest-approvals-wire-codes.test.ts @@ -30,8 +30,9 @@ * template would mangle into an invalid code also fails here. * [#14573] The file has since become the home for the approvals door's * live-emission pins generally, not only the #8885 population: the - * `FORBIDDEN` → 403 case below pins a row that was already registered - * vocabulary but whose EMISSION nobody observed. See its own comment. + * `FORBIDDEN` → 403 case below pins a row that is registered vocabulary and + * whose emission was — contrary to that card's premise — already observed + * elsewhere. See its own comment for where, and why it is pinned here too. * * 3. The union stays CLOSED — the control case in * `rest-field-visibility-fault-envelope.test.ts` covers this file too (same @@ -125,11 +126,26 @@ describe('approvals wire codes are registered vocabulary (#8885)', () => { // refusal rides: recall by a non-submitter, decide by a non-approver, // reassign / remind / sendBack / resubmit by the wrong actor, and // `resolveActor`'s impersonation refusals all reach this table through - // the same single row. Until this case it was the only mapping row whose - // 403 nobody observed on the wire — the service suites - // (`recall-refusal-user-copy.test.ts`, `approval-revise.test.ts`) assert - // the `FORBIDDEN:` MESSAGE PREFIX at the throw site, which is a different - // fact from what the route answers. + // the same single row. + // + // ⚠️ THIS IS A SECOND PIN, NOT THE FIRST — read this before adding a + // third. #14573 was filed and triaged on the reading that the row had NO + // live-emission pin, and that reading is WRONG: §7 of + // `rest-data-door-code-prefix.test.ts` ("[#13095] the approvals door + // strips the code it answers") already drives the REAL approve route with + // a `FORBIDDEN: …` throw and already asserts 403, `code: 'FORBIDDEN'` and + // the strip. Measured, not read: deleting the row from `rest-server.ts` + // reds THREE cases — this one and both of §7's. What was true is narrower + // than the card: the file that OWNS the approvals wire-code contract did + // not pin the row, so a reader auditing wire codes here saw a gap that a + // strip-contract file was silently covering. That is the gap this case + // closes, and naming §7 here is half the fix — an unlabelled duplicate is + // what got the card mis-filed in the first place. + // + // The service suites (`recall-refusal-user-copy.test.ts`, + // `approval-revise.test.ts`) are NOT pins on this row: they assert the + // `FORBIDDEN:` MESSAGE PREFIX at the throw site, a different fact from + // what the route answers. // // Losing the row FAILS CLOSED, which is why this is a contract pin and not // a security one: `handleApprovalError` returns false on no match, the @@ -140,7 +156,8 @@ describe('approvals wire codes are registered vocabulary (#8885)', () => { // token the [#13095] anchored strip exists to remove. Two contract // properties ride this one row, so the third assertion below is NOT // redundant with the first two: it is what catches the degraded shape's - // unstripped message. + // unstripped message. (Ablation: all three reds read + // `expected 500 to be 403`.) it('recall by a non-submitter answers 403 FORBIDDEN, prefix stripped — the row every authorisation refusal rides', async () => { // The message the real service throws: `approval-service.ts`'s recall // non-submitter branch is `FORBIDDEN: ${userFacingRefusal(...)}`.