Skip to content

feat(authz): Layer C — fail-closed global route guard + route-inventory fitness test (cccvu/wiki-v2#13) - #3

Merged
PMQ9 merged 1 commit into
wiki-v2from
feat/issue-13-layer-c
Aug 31, 2026
Merged

PMQ9 merged 1 commit into
wiki-v2from
feat/issue-13-layer-c

Conversation

@PMQ9

@PMQ9 PMQ9 commented Aug 31, 2026

Copy link
Copy Markdown

Fork half of cccvu/wiki-v2#13. Companion to the super-repo PR (which bumps the submodule to this commit).

What

Adds the Layer C the fork lacked (the platform already had it):

  • authz/route-guard/route-inventory.spec.ts — a static TS-AST route-inventory fitness test: every controller route must be @Public/@PlatformPublic, guarded by JwtAuthGuard/CollabServiceSecretGuard, @PlatformAuthz, or in the reasoned intentional-unguarded ledger (13 entries) — else the build fails. AST scan (not import-reflection) so it's immune to workspace-build / .tsx resolution fragility.
  • PlatformAuthorizationGuard registered as a global APP_GUARD (seam fix(authz): harden PLATFORM_AUTHZ_TIMEOUT_MS parse (empty/typo → deny-all self-DoS) #4 app.module.ts) — 403s any offender at runtime, pass-through for decided routes (the controller's own guard still enforces). Shares the exact classify() the test uses.
  • Malformed-PDP-response hardening in platform-authz.client.ts — fail-closed Array.isArray/strict-boolean guards so a Byzantine 200 can't flow downstream as an allow.

Boundary

One upstream-file edit (app.module.ts, documented seam #4); everything else is fork-owned under authz/route-guard/. check-upstream-boundary.sh passes. The fitness test proves authentication, not object-level authorization (that residual stays with Layer A — documented in-code).

Notes

  • collab/stats ledgered as a known-gap (separate CollabAppModule process, env-gated off) → cccvu/wiki-v2#80.
  • Verified: nest build clean, jest src/authz 179/179; new-unguarded-route mutation → RED, reverted.

🤖 Generated with Claude Code

…ry fitness test (#13)

Add the fork-side Layer C the platform already had. The fork had no global guard
and no route-inventory test, so a new unguarded single-object route (Top Risk #3's
unauthenticated half) could merge silently — a real leak (collab/stats) already
existed.

- route-guard/: a static route-inventory FITNESS TEST (TS-AST scan of the source,
  no boot/DB — robust to workspace-build/.tsx resolution), a shared pure classify()
  both the test and the guard funnel through, the intentional-unguarded LEDGER (13
  reasoned entries), fork @PlatformAuthz/@PlatformPublic decorators, and the
  fail-closed PlatformAuthorizationGuard (offender -> 403; pass-through for decided
  routes so the controller's own JwtAuthGuard still enforces).
- app.module.ts (documented seam #4): register the guard as APP_GUARD.
- platform-authz.client.ts: fail-closed response-shape guards (Array.isArray /
  strict-boolean) so a malformed/Byzantine PDP 200 can't flow downstream as an allow.
- collab/stats ledgered as a known-gap (separate CollabAppModule process, env-gated
  off) -> tracking issue cccvu/wiki-v2#80.

The build-time fitness test keeps the ledger complete, so the runtime guard can never
fail-close a legitimate route in a green build. The test proves AUTHENTICATION, not
object-level authorization (that residual stays with Layer A) — documented in-code.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@PMQ9
PMQ9 merged commit 48dc57e into wiki-v2 Aug 31, 2026
@PMQ9
PMQ9 deleted the feat/issue-13-layer-c branch August 31, 2026 18:08
PMQ9 added a commit that referenced this pull request Sep 8, 2026
…ge, DTO validation, keyset hardening

All four round-justifying (non-blocking Level-3) findings from the 7-lens review, plus overlapping Level-2
robustness folded in while in the same files:

- [Testing L3 / DevOps L3] listByPage: give it opt-in {limit,before} keyset paging (consistent with the
  members/ACL reads), and add service-attachment.pg.spec.ts — resolvePage + listByPage now execute on real
  Postgres (a raw-sql column/table typo no longer ships green), and the paging keyset walks a shared-ms tie
  with no skip / no duplicate.
- [Testing L3] Add dto-validation.spec.ts — the new class-validator DTOs (ContentSearchDto, ContentSortDto,
  ContentCursorDto, ContentListDto incl. nested sort) now run through validate(), so dropping a constraint
  (@Max/@IsUUID/@IsIn/@IsNotEmpty/@IsISO8601, …) reds a test.
- [Correctness L3] Add a real-Postgres walk for the generalized TIMESTAMP keyset (only title was walked) and
  for value='' (null-title) as a mid-walk cursor bound.
- [Security F1 / Correctness #1 / Testing P2] A malformed keyset/cursor timestamp now 400s instead of
  500-ing at the ::timestamptz cast: a shared isIsoInstant() guard (rejects Date.parse-lenient-but-PG-invalid
  values like '2026') used by parseSubCollectionQuery and the content keyset builder.
- [Correctness #2] A text sort (title/name) now REQUIRES its cursor `value` (400) instead of silently
  falling back to a timestamp `updatedAt` and paginating on the wrong boundary.
- [DevOps #3] Bump service-bridge.openapi.json version 1.1.1 → 1.2.0 (additive expansion).

Non-blocking Level ≤2 polish is deferred to a follow-up issue. Verified: nest build, 19 service-bridge suites
(179 tests), 3 real-Postgres pg specs (30 tests), eslint, import-boundary — all green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
PMQ9 added a commit that referenced this pull request Sep 17, 2026
… content write when Redis off (docmost#344)

Two pre-existing content-write defects on the collaboration path, both found while
tracing docmost#282 and both able to lose or silently drop a page's content.

docmost#342 — a content `replace` emptied the Yjs fragment BEFORE building the new document
from the request body. If `TiptapTransformer.toYdoc` threw (a body that passed
`jsonToNode` but the Yjs transformer rejects), the connection's closing store persisted
the emptied fragment: the page was WIPED while the request also failed. Build the new
state first, then delete+apply — a conversion failure now aborts before any mutation and
the original content survives. Shared by the ordinary `updatePageContent` handler and the
docmost#282 conditional write.

docmost#344 — custom collab events route only through the RedisSync extension, so with
COLLAB_DISABLE_REDIS (single-node standalone) `handleYjsEvent('updatePageContent')`
returned undefined and `PageService.update` ignored it: a REST/`/v1` content write
returned 200 while persisting nothing. Fail that one un-routable write loudly instead.
Narrowest boundary: the supported standalone mode still boots, interactive editing (direct
Hocuspocus path) is untouched, and the docmost#282 seams keep their fail-closed `undefined`
contract.

Tests: build-before-delete leaves the fragment intact on a throwing toYdoc; the gateway
guard throws for updatePageContent with Redis off, stays quiet for best-effort events, and
routes normally with Redis on. UPSTREAM_MODIFICATIONS.md seams #2/#3 updated.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant