Skip to content

Zod security hardening (alpha.8) + full Jest→Vitest migration - #31

Merged
tnramalho merged 21 commits into
mainfrom
feat/vitest-migration
Aug 6, 2026
Merged

Zod security hardening (alpha.8) + full Jest→Vitest migration#31
tnramalho merged 21 commits into
mainfrom
feat/vitest-migration

Conversation

@tnramalho

@tnramalho tnramalho commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

What this PR contains

Two phases, stacked (6 commits):

Phase 1 — alpha baseline + security hardening (b43f6b8..aed9e47)

  • Packages publish as @conceptadev/*; upstream @concepta/nestjs-* moves to 8.0.0-alpha.8 (NestJS 12 alpha).
  • Security (CWE-863 / CWE-200 / CWE-284), covered by a dedicated e2e suite:
    • Owner scoping on by default for owner-annotated zod resources (ownerScope: false opts out — the sample pet resource does, in favour of its owner-or-shared hook). Breaking for consumers relying on unscoped reads.
    • Response DTO exposure is opt-in; the credential-name regex is gone (it hid tokenExpiresAt while missing apiKey). Secrets opt out explicitly with dto: { response: false }.
    • defineZodUserMetadata projects through projectSchema — closed a second projection path that ignored response: false.
    • Path-scope invariants assert on core.providers (real artifact) + behavioural e2e; the write-only meta.guards mirror is deleted.
  • Library hygiene: Nest Logger in the exceptions filter (no console, no NODE_ENV gate), local validation-error flattening (no private Nest API), descriptor-based isThunk, WeakMap hook caches, Firestore native Timestamp round-trip, per-instance in-memory backend.

Phase 2 — Jest→Vitest 4 (93a2569..857cbd2)

  • scripts/ no longer exists: the isolated-runner bridge, both Babel ESM-compat plugins, all 14 Jest configs, and every Jest dependency across 10 workspaces are deleted (net −1,400 lines).
  • Official projects model: root vitest.config.ts declares unit, e2e-packages, and one project per example; vitest.shared.ts holds the single SWC block (decorator metadata for Nest DI — esbuild can't emit it). yarn test:all runs the whole monorepo: 106 files / 971 tests.
  • globals: false everywhere — every test file imports its API explicitly; proven by the runner itself.
  • Test files are now type-checked (yarn typecheck:spec, wired into CI). First run surfaced 42 latent errors across 19 files (orphaned fixtures importing upstream members deleted in alpha.8, entity-contract drift, deep dist/ imports, under-typed doubles) — all fixed or deleted, none suppressed. Two interfaces consumers need are now exported from the public index.
  • Dynamic-import audit: 41 cargo-cult await import()s replaced by static imports after checking the real graph (madge); the one justified case is kept and documented with file:line evidence.

Test evidence

Gate Result
yarn build / yarn typecheck:spec / yarn lint:all green
unit (--project unit) 62 files / 572 tests — identical to Jest baseline
package e2e (--project e2e-packages) 30 / 156
samples 8/194 · 2/40 · 4 files/9 tests
yarn test:ci artifacts junit.xml + coverage/ + coverage-e2e/ at historical paths

Known limitations (documented in CHANGELOG)

  • Intermittent e2e failures: diagnosed as host memory pressure, not a code defect. Rotating victim suite with inconsistent symptoms; the instrumented hunt showed a failing run takes 216s vs 10.8s, two independent suites stall in the same instant, the real failures are 30s timeouts with no HTTP response, and the host was at ~16 MB free RAM with continuous pageouts (16 GB machine, 26 GB swap in use). Not a CI risk — a full run peaks at ~650 MB across 4 workers vs 4 GB on a standard runner. Full evidence in CHANGELOG "Intermittent e2e failures".
  • SafeCrudContextInterceptor still string-matches an upstream error message (named contract test pins it; deleted when upstream 5249672f ships).
  • Firestore now stores dates as native Timestamp; rows written by earlier builds hold ISO strings and are no longer converted on read. Verified safe: the package has never been published to npm (registry 404) and the only in-repo consumer is a test stub, so there is no persisted data to migrate.
  • Base remains pre-release @concepta 8.0.0-alpha.x.

🤖 Generated with Claude Code

tnramalho and others added 6 commits August 5, 2026 14:07
…dening

Baseline: packages publish as @conceptadev/*, the upstream
@concepta/nestjs-* stack moves to 8.0.0-alpha.8 (NestJS 12 alpha), CI
drops the dead Code Climate step, and the firebase adapter grows
guard/exception/util coverage.

Security (CWE-863 / CWE-200 / CWE-284), covered by the new
rockets-core-zod-security e2e suite plus unit specs:

- Owner scoping on by default: owner-annotated zod resources wire
  OwnerScopeHook on reads next to OwnerStampHook on writes; opt out per
  resource with ownerScope: false (the sample pet resource does, in
  favour of its owner-or-shared hook).
- Response DTO exposure is opt-in: f.* helpers register
  dto.response: true, raw zod fields stay off the wire, base-entity
  columns remain exposed. The credential-name regex is gone — it hid
  harmless fields (tokenExpiresAt, mfaEnabled) while missing real
  secrets (apiKey, salt); secrets opt out explicitly with
  dto: { response: false }.
- defineZodUserMetadata projects through projectSchema, closing the
  second projection path that ignored dto.response: false; f.compute
  schemas strip response: false columns.
- Sub-resource materialisation asserts PathScopeGuard/PathScopeHook
  survived provider merging in core.providers; the write-only
  meta.guards mirror is removed in favour of behavioural e2e proof.

Library hygiene:

- Exceptions filter logs via Nest Logger on every 5xx (no console, no
  NODE_ENV gate) and flattens validation errors locally instead of
  calling Nest's private ValidationPipe method.
- isThunk discriminates entity class vs thunk by the prototype
  descriptor; the ambiguous function-expression form throws.
- Hook subclass caches use WeakMap; PathScopeGuard cache key is
  collision-free; InMemoryFirestoreBackend stores per instance.
- Firestore round-trips Date as native Timestamp instead of
  stringifying on write and guessing types back from field names.
- sample-server-auth entities updated to alpha.8 interface contracts.

Testing:

- Package e2e runs one Jest process per spec file
  (scripts/run-isolated-e2e.cjs) with merged coverage; shared-worker
  runs flaked ~25% from cumulative state across ~30 app boots. The
  forceExit flag, the dead barrel-last sequencer, and the e2e-only
  monkey-patch of DependenciesScanner/CrudModule.forFeature are gone.
- A named e2e test pins the upstream 'No entity defined' message that
  SafeCrudContextInterceptor matches on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
At cpus-1 the parallel instrumented app boots contend for CPU hard
enough to produce load-induced timing failures inside suites that are
10/10 green alone. 4 keeps the run well under shared-worker times
(34s / 78s with coverage) without hitting contention. Override with
E2E_CONCURRENCY.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… refresh

Root CHANGELOG documents the security defaults (ownerScope on, opt-in
response exposure), the Firestore Timestamp format change, and the
conscious debts: SafeCrud message match, shape/compute projection
asymmetry, the alpha.8 upstream base, and the e2e isolation runner as a
bridge until the Vitest migration deletes it and the Babel ESM-compat
plugins. Rules/skills drop the dead barrel-last sequencer and describe
per-file e2e isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Net -1,415 lines. What this deletes, permanently:

- scripts/run-isolated-e2e.cjs — pool: 'forks' gives every spec file a
  fresh process natively (verified: distinct pid per file even at one
  worker). The repo-root scripts/ directory no longer exists.
- Both Babel plugins that adapted the ESM-only @nestjs v12 dist to
  Jest's CJS runtime — Vitest runs ESM natively.
- All 14 Jest config files, tsconfig.jest.json, the orphaned
  jest.setup.ts, and every jest / ts-jest / babel-jest / jest-junit /
  jest-extended / jest-mock-extended / @types/jest dependency across 10
  workspaces.

How it works now:

- Root vitest.config.ts (units) and vitest.e2e.config.ts (package e2e);
  each example workspace carries its own vitest.e2e.config.ts mirroring
  the root shape. unplugin-swc supplies the decorator metadata Nest DI
  requires (esbuild cannot emit it).
- globals is false everywhere: every test file imports its API
  explicitly (import { describe, it, expect, vi } from 'vitest') — the
  runner itself proves no ambient-typing magic remains.
- Per-package test scripts route through the root configs via Vitest
  path filters; CI emits the same junit.xml + coverage/ + coverage-e2e/
  artifacts at the same paths (dorny reporter switched to java-junit).

Test counts match the Jest baselines exactly: units 62 files/572 tests,
package e2e 30/156, samples 8/194 + 2/40 + 4 files/9 tests.

Latent defects fixed on the way:

- sample-code-review's ConfigModule guard keyed on JEST_WORKER_ID, which
  Vitest never sets — e2e runs would have read a developer's real .env;
  now keyed on VITEST.
- The same app's Jest config had lost its setupFiles wiring, so its
  FIREBASE_PROJECT_ID default never applied; ported properly.
- rockets-auth-handler-overrides.spec.ts replaced its lazy-require
  cycle workaround (3 eslint-disables) with typed beforeAll dynamic
  imports.
- The core↔server contract test keeps its compile-time tripwire via
  vi.importActual<typeof import(...)>.

The pre-existing rotating-404 full-run flake (~1 in 4 runs, one suite,
always an unexpected 404, never reproducible solo) is documented with
its evidence trail in CHANGELOG "Known flaky failure" — it predates this
migration and is the next investigation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two things Jest never gave us, now enforced:

1. Test files are type-checked (`yarn typecheck:spec`, wired into both
   CI workflows). ts-jest ran with `isolatedModules`, i.e. transpile
   only — spec files were never seen by the compiler, and SWC kept that
   true. The gate's first run surfaced 42 latent errors across 19 files:
   an orphaned fixture chain importing upstream members deleted in
   alpha.8 (5 files removed), alpha.8 entity-contract drift in the e2e
   factory fixtures, deep `dist/` imports blocked by upstream exports
   maps, under-typed test doubles, and one `as unknown as` cast. All
   fixed or deleted, none suppressed. `EmailSendOptionsInterface` and
   `RocketsAuthUserMetadataModelUpdatableInterface` are now exported
   from @conceptadev/rockets-auth's public index — consumers needed
   them and could previously only reach them through dist paths.

2. The config follows Vitest 4's official monorepo guidance — the
   `projects` model. Root `vitest.config.ts` declares every project
   (`unit`, `e2e-packages`, one per example workspace);
   `vitest.shared.ts` carries the shared SWC/settings block (one copy
   instead of five); example configs are `defineProject` +
   `mergeConfig(shared, …)`; `vitest.e2e.config.ts` is deleted.
   Coverage/reporters live at root (root-only by design); thresholds
   moved to the unit-gate scripts because the e2e coverage run was
   historically threshold-free and root thresholds would now apply to
   it. `sequence.groupOrder` sequences unit → package e2e → examples,
   which makes `yarn test:all` a single command running the entire
   monorepo: 106 files / 971 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audited every `await import()` inside test bodies/hooks that claimed to
dodge dependency cycles, against the real import graph (madge):

- rockets-auth-handler-overrides.spec.ts: the claimed cycle with
  rockets-auth-ports.module does not exist — no cycle in
  rockets-server-auth involves it. Static imports; 22/22 green.
- define-resource.spec.ts (35 sites) + aggregate-resources.spec.ts (6):
  the only real cycle is define-resource <-> materialise-sub-resource
  (mutual recursion by design), which the specs already enter through
  their top-level defineResource import. The dynamically imported
  modules participate in no cycle. Hoisted; 127/127 green.
- auth-and-me-endpoints.e2e-spec.ts: `typeorm` and a leaf entity —
  no cycle possible. Static.
- sample-code-review.e2e-spec.ts keeps its ONE justified dynamic
  import, now documented with evidence: src/app.module.ts reads
  FIREBASE_USE_FAKE at module scope, so a static import would evaluate
  it before the spec's process.env assignments (ESM imports hoist).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codacy-production

codacy-production Bot commented Aug 5, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 55 complexity · 3 duplication

Metric Results
Complexity 55
Duplication 3

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

* Returns the field untouched when nothing is hidden, so the common case
* keeps its exact original wrappers.
*/
function withHiddenFieldsRemoved(field: z.ZodType, path: string): z.ZodType {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH — withHiddenFieldsRemoved silently drops the entire computed field from runtime responses; the leak it targets is only closed by accident

Rebuilding the field (z.object(shape) / z.array(strippedElement) + re-wrapping) loses the rocketsFieldMeta registration — the registry is keyed by schema instance, and the rebuilt schema was never registered. Downstream, compileDtoClass reads meta.compute off the projected field to install the @Transform that populates the value. With the meta gone, no transform is installed, so the computed key is absent from every HTTP response — while the response DTO / OpenAPI still documents it (minus the hidden column).

Verified against the built dist, replicating the upstream CrudSerializeInterceptor pipeline (default toInstanceOptions + ZOD_TO_PLAIN_OPTIONS from zod-operations.ts), with secret: f.string({ dto: { response: false } }) inside the compute element schema:

B (hidden col in compute schema): meta.compute preserved? false
   wire: {"id":"row-1"}                              // computed field GONE
A (clean compute schema):        meta.compute preserved? true
   wire: {"id":"row-1","items":[{"id":"i1","label":"ok",
          "secret":"SECRET-VALUE","extraNotInSchema":"ALSO-OUT?"}]}

Case A shows the deeper problem: the compute schema is never enforced at runtime. toPlain runs with strategy: 'exposeAll' and the transform output bypasses all whitelisting, so whatever the callback returns goes out verbatim — including keys never declared in the compute schema. The stripping added here only edits the documented schema; the CWE-200 scenario in this docstring is prevented solely because the whole field vanishes, and rows carrying sensitive columns that are hidden on some other schema (or not annotated at all) still leak through a compute in the clean case.

The new unit test (strips dto.response=false out of a computed field schema) asserts schema keys only, which is why this wasn't caught.

Suggested fix: enforce the projection at serialization time, not (only) schema time — in compileDtoClass, wrap the compute transform to pick only the projected element keys (or parse through the stripped schema), and preserve the field-meta registration when this function rebuilds the field. Add an e2e asserting the computed field still appears on the wire minus the hidden column.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5bb83319 + f1b50966. The rebuilt field re-registers its rocketsFieldMeta (compute @Transform installs again — unit test pins the registration), and the transform now strips the compute output to the declared shape at runtime (key-level, not schema.parse, since computed values legitimately differ from wire types). The e2e you asked for is in rockets-core-zod-security.e2e-spec.ts — 'computed field reaches the wire minus hidden and undeclared keys' — through the real serializer pipeline: field present, internalNote and an undeclared key absent. It fails against both shapes of the original bug.

Comment thread .github/workflows/ci-pr-test.yml Outdated
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
path: '/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('/yarn.lock') }}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM — dependency cache is dead after this change: both paths are absolute

path: '/node_modules' points at the filesystem root — that directory never exists on the runner, so the post-job cache save fails silently. And hashFiles() patterns are relative to GITHUB_WORKSPACE; '/yarn.lock' matches nothing, so the expression returns the empty string and the key degenerates to the constant Linux-modules-. Net effect: every CI run cold-installs, and nothing useful is ever saved or restored. The same edit landed in ci-merge.yml.

If the intent was "cache only the root node_modules" instead of the old **/node_modules glob:

path: node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5bb83319: relative path: node_modules + hashFiles('yarn.lock'), with a workflow comment recording why the absolute paths were dead. Applied to both ci-pr-test.yml and ci-merge.yml.

};
// eslint-disable-next-line no-console
console.error('[RocketsCoreExceptionsFilter] 5xx:', e.stack ?? exception);
this.logger.error('Unhandled 5xx', e.stack ?? String(exception));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOW — String(exception) logs [object Object] for stack-less exceptions

Observed in this branch's own e2e output while verifying the run:

ERROR [RocketsCoreExceptionsFilter] Unhandled 5xx
[object Object]

For a thrown plain object (or an exception whose stack is missing), the fallback carries zero information — which defeats the purpose of always logging 5xx. Consider something like e.stack ?? (exception instanceof Error ? exception.message : JSON.stringify(exception)) so the log line is always actionable.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5bb83319: util.inspect fallback for both the exception and originalError — a stack-less plain object now logs its structure instead of '[object Object]'.

@tnramalho tnramalho left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — changes requested (posted as comment: GitHub blocks request-changes on own PR)

Independent review of both phases (security hardening + Jest→Vitest). One verified blocker, one CI bug, one logging nit. The known limitations declared in the PR body (rotating-404 flake, SafeCrud string-match, Firestore legacy ISO data, alpha base) were treated as out of scope.

Findings (ranked)

  1. HIGH / blocker — computed fields with a hidden column vanish from responses; compute output is not runtime-enforcedpackages/rockets-core/src/zod/zod-projections.ts (withHiddenFieldsRemoved). Rebuilding the field loses the rocketsFieldMeta registration, so compileDtoClass never installs the compute @Transform: the field is absent from every HTTP response while OpenAPI still documents it. The CWE-200 fix this docstring claims holds only because the field disappears entirely; in the clean-schema case, compute output still goes out verbatim (toPlain is exposeAll), including keys never declared in the compute schema. Reproduced against the built dist replicating the CrudSerializeInterceptor options — repro output in the inline comment. The new unit test asserts schema keys only, so it cannot catch this.
  2. MEDIUM — CI dependency cache is dead.github/workflows/ci-pr-test.yml and ci-merge.yml: path: '/node_modules' (filesystem root, never exists) + hashFiles('/yarn.lock') (workspace-relative patterns → empty hash → constant key). Every run cold-installs; the save step fails silently.
  3. LOW — Unhandled 5xx log prints [object Object]packages/rockets-core/src/infrastructure/filters/exceptions.filter.ts:145; String(exception) fallback carries no information (observed in this branch's own e2e output).

Observations (no action required, flagging deliberate trade-offs)

  • OwnerScopeHook is fail-open when no actor is on the context (documented as "public route" semantics). It relies entirely on AuthServerGuard rejecting unauthenticated requests first; any future route that is authenticated through a mechanism that doesn't populate the actor would silently return the full table.
  • isThunk in define-sub-resource.ts now throws for function-expression entities. Consumers compiling with a target below ES2015 (transpiled classes have writable prototypes) would hit the ambiguity throw for a legitimate entity: MyEntity. Modern targets are unaffected.

Verified green (commands run locally on the PR head, 857cbd2)

  • corepack yarn vitest run --project unit → 62 files / 572 tests, all pass (matches the PR's claim).
  • corepack yarn vitest run --project e2e-packages → 30 files / 156 tests, all pass, no flake on this run.
  • File-count audit: exactly 62 *.spec.ts and 30 *.e2e-spec.ts exist under packages/ — the include/exclude globs in vitest.config.ts un-test nothing. tsconfig.spec.json covers all spec/e2e/fixture locations; the only excluded specs are the Playwright files under apps/web/e2e (correctly out of Vitest scope).

Checked and clean

  • Owner scoping defaults: resolveOwnerColumns + applyOwnerHooks wire OwnerScopeHook per owner column with per-side opt-outs; read coverage is complete (the repository abstraction's only read entry points are findOne/findAndCount, both hooked). zodModuleResource contributes non-CRUD slices only, so it is not a scoping bypass.
  • Response exposure (non-compute paths): runtime-enforced, not docs-only — compileDtoClass emits class-level @Exclude + per-field @Expose, and toInstance runs with excludeAll/excludeExtraneousValues, so unprojected fields never reach the instance. Nested relation exposure (expose: true) recurses through per-field opt-in and dedicated nested DTO classes via @Type — hidden nested columns are stripped on the way in. Owner columns are excluded from create/update projections (no owner spoofing).
  • defineZodUserMetadata: now shares projectSchema; create/update parity with the old omit() behavior holds, userId frozen on update, and managed fields are enforced on top of the projection.
  • Sub-resource scoping: materialise-sub-resource.ts asserts ScopeGuard/ScopeHook survive provider merging against core.providers (the real artifact); guard now precedes consumer decorators. path-scope.guard.ts cache-key fix (JSON tuple vs :: join) is correct.
  • Exceptions filter rewrite: unwrap chains are cycle-safe, unwrapToClientRuntimeException correctly picks the innermost 4xx, local validation flattening matches the private-API behavior it replaces (parent-property prefixing included), and the always-log-5xx change removes the NODE_ENV stack-leak gate.
  • Firestore: DateTimestampDate round trip is now symmetric at the top level for both backends; the name-based (startsWith('date')/endsWith('At')) type-guessing is gone; in-memory backend state is per-instance.
  • Converted tests: no .only/.skip/any/ts-suppressions in added lines; expect balance is +95/−2, and both removals are org-rename substitutions with the assertion preserved. globals: false + SWC decorator metadata config is correct; groupOrder is unique per project.
  • CI report pipeline: download-artifact@v4 + run-id fix is correct, artifact paths line up with test:ci/test:e2e:cov outputs, java-junit parses Vitest's junit reporter output, $GITHUB_OUTPUT migration correct.

Not covered

  • Sample-app e2e suites (194/40/9 tests) were not re-run locally; I relied on the PR's evidence there.
  • yarn.lock (3,774 lines) reviewed only for shape, not audited dependency-by-dependency.
  • The ~250 files of mechanical @bitwild@conceptadev rename churn were spot-checked, not exhaustively read.
  • CHANGELOG/docs prose accuracy beyond the claims tested above.

Verdict: the migration itself is solid and the security defaults are real improvements — but finding 1 sits at the center of the PR's CWE-200 claim and silently breaks a documented feature, so it should be fixed (with a wire-level test) before merge.

@tnramalho

Copy link
Copy Markdown
Collaborator Author

All three review findings are addressed as of 5bb83319:

  • HIGH — compute projection was docs-only + rebuilt field lost its meta registration: withHiddenFieldsRemoved now re-registers the original rocketsFieldMeta on the rebuilt node (the compute @Transform installs again), and the transform strips the compute output to the declared shape at runtime — key-level rather than schema.parse, since computed values legitimately differ from wire types (Date vs ISO string). Two new unit tests pin both layers: meta survives the rebuild, and hidden + undeclared keys of embedded rows are absent from the serialized instance (zod-projections.spec.ts).
  • MEDIUM — dead CI cache: /node_modules + hashFiles('/yarn.lock') → relative paths; comment in the workflow records why.
  • LOW — [object Object] logs: util.inspect fallback in the exceptions filter.

Gates re-run on the fix: build, lint:all, typecheck:spec, unit 574/574 (2 new tests), package e2e 156/156 (one hit of the documented rotating-404 flake, green on rerun), sample-server 194/194.

tnramalho and others added 13 commits August 5, 2026 16:57
Closes the remaining ask from the PR #31 HIGH review comment: assert
through the real serializer pipeline (not plainToInstance in isolation)
that a computed field embedding rows with a hidden column still reaches
the wire, minus both the dto.response=false key and a key never declared
in the compute schema. Would have caught both shapes of the original
bug: the silently-vanished field (lost meta registration) and the
verbatim leak (unenforced compute output).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… rule

Human review of PR #31, decision 1 (approved): the opt-in model mirrors
the classic class-level @exclude() + per-field @expose() idiom, so the
rule ships documented — RocketsDtoFieldMeta's JSDoc still claimed
"response — always true" (stale), and the README zod section gains the
exposure table plus the mandatory dto: { response: false } rule for
secrets and the one-line audit grep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Human review of PR #31, decision 2 (approved): the ownerScope default
stays, admin access stays a separate guarded surface, and richer
scoping (dealer/group/tenant) stays consumer-written and opt-in — but
the capability must be documented. The README zod section now carries
the three-step recipe (actor metadata -> custom scope hook with
ownerScope: false -> separate admin surface), pointing at the two
shipped sample hooks as working references and at discussion #32 for
the declarative scope-policy RFC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Human review of PR #31, decision 3: the exposed-by-default owner id is
correct — it matches how mainstream APIs return an owner reference
(GitHub owner.id, Stripe customer), the value is an opaque
non-sequential uuid, and the UI needs a stable key to group rows by
author and answer "is this mine?" without a round-trip. Default
unchanged.

What was missing is the escape hatch: f.owner() took no options, so a
resource that genuinely should not publish the owner id had to bypass
the helper with a manual .register(). It now accepts
`f.owner({ dto: { response: false } })`. Only `response` is
configurable — create/update are decided by the owner-column projection
rule (server stamps, client value rejected), so accepting overrides
there would silently ignore them.

Two unit tests pin both directions; README documents when to opt out
(ownerScope: false resources whose rows are visible to non-owners).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Human review of PR #31, decision 4. The runtime strip stays key-level
(no schema.parse): computed fields routinely re-emit ORM rows where
f.createdAt() documents an ISO string and TypeORM hands back a Date —
parsing would reject the legitimate case, proved with a probe against
the real tagSchema.

But the review question was the right one: a HAND-BUILT projection with
a wrong type, or reading a source property that does not exist, used to
ship silently (42 where a string was promised, undefined where a value
was). Those are compiler problems, not runtime ones, and the callback
was typed `=> unknown`.

`f.compute` now types its return as ComputeResult<z.output<T>> — the
declared wire shape, relaxed so any key documented as string may also
arrive as a Date. Division of labour: the runtime strip decides which
KEYS may ship (undeclared/hidden keys cannot leak); TypeScript decides
value TYPES. This matches the ecosystem norm (class-transformer, tRPC,
Prisma select all trust the compiler for output types).

compute-result.typetest.ts pins all three cases; removing its two
@ts-expect-error directives produces exactly two errors. Type tests are
now part of `yarn typecheck:spec`, so CI runs them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Human review of PR #31, decision 5. The instrumented hunt ran and the
answer is not in this repo: a failing run takes 216s against 10.8s for a
passing one, two independent suites stall in the same instant, the
captured failures are 30s timeouts with no HTTP response (the earlier
"unexpected 404" reading was a request dying mid-flight, not a real
response), and sampling the host showed ~16 MB free RAM with continuous
pageouts on a 16 GB machine already carrying 26 GB of swap.

That explains every property which defeated the earlier hypotheses —
leaked apps, libuv threadpool exhaustion, poisoned file pairs,
cross-process state, all tested and ruled out: a host-level cause cannot
be prevented by process isolation, is never triggered by a single suite,
and predates the Vitest migration.

Not a CI risk: a full run peaks at ~650 MB across 4 workers against 4 GB
on a standard runner. CHANGELOG, the build/test rules and the e2e-fixer
skill now carry the diagnosis and the tell (a failing run is ~20x
slower), so nobody re-opens this as a code bug or papers over it with a
retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Human review of PR #31, decisions 6 and 7 — both verified by fact
rather than judgement:

- Public surface: the three new exports are `export type` only (erased
  at runtime, no bundle impact), and `meta.guards` has zero remaining
  references anywhere in the repo. A stale spec comment still described
  the removed field as a pending requirement; corrected to describe what
  the test actually asserts (core.providers).
- Firestore storage format: the Date -> native Timestamp change is a
  breaking storage change, but `@conceptadev/rockets-repository-firestore`
  has never been published (registry 404) and the only in-repo consumer
  is the sample app's test stub — there is no persisted data to migrate.
  Doing this after the first release would have required a backfill, so
  now is the right time. CHANGELOG records the finding and the added
  benefit (range queries and ordering by date work; ISO strings only
  sorted by lexicographic luck).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`yarn npm audit` reported two moderate TypeORM advisories against the
locked 0.3.28: SQL injection in UpdateQueryBuilder/SoftDeleteQueryBuilder
orderBy on MySQL/MariaDB (GHSA-9ggv-8w38-r7pm, fixed in 0.3.29) and
migration:generate template-literal code injection (GHSA-2rp8-mm9q-fp49,
fixed in 0.3.31).

Raising the declared ranges was not enough: a root `resolutions` entry
pinned typeorm to exactly 0.3.28 and kept overriding them. Both the
ranges and the pin now point at 0.3.31.

Verified on the new version: build, typecheck:spec, lint:all, 576 unit,
157 package e2e (5 consecutive full runs), and all three sample apps
(194 / 40 / 8+1 skipped).

Remaining audit output is two deprecation notices (eslint 8, rimraf 3) —
dev-only tooling that never reaches consumers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audited the repo against standard OSS/npm expectations and closed the
gaps. The one real defect found:

- **Every package pointed npm at a repository that does not exist.**
  `repository`, `homepage`, `bugs` and the funding link all referenced
  `github.com/conceptadev/rockets`, while the remote is `btwld/rockets`
  (the conceptadev path redirects to an unrelated repo). Every
  "Repository" and "Issues" link on npm would have been wrong.

Also fixed:

- `rockets-core` and `rockets-repository-typeorm` shipped **without a
  LICENSE file**, which BSD-3-Clause requires to accompany the
  distribution. Both now carry it and list it in `files`; verified via
  `yarn pack --dry-run`.
- Four packages were missing `homepage`, `bugs` and `keywords` (npm
  search relevance and the sidebar links). All six publishable packages
  now expose the same metadata set.

Added the community files GitHub and OpenSSF look for:

- `CONTRIBUTING.md` — setup, the exact checks CI runs, code standards,
  test tier policy, commit conventions, project layout.
- `SECURITY.md` — private reporting via GitHub Security Advisories,
  response times, and an explicit scope section stating which
  row-visibility and response-exposure guarantees are in scope (and that
  the example apps and documented opt-in hardening are not).
- `CODE_OF_CONDUCT.md` — Contributor Covenant 2.1.

Documentation:

- `lint:md` only covered `README.md` at the root, so every other root
  document was unlinted. Widened to `*.md`; the 15 pre-existing
  violations it exposed are fixed.
- READMEs updated for this branch: `typecheck:spec` added to the
  documented gate, the reference to the deleted `scripts/` directory
  removed, and the contributing section now points at CONTRIBUTING /
  SECURITY / CODE_OF_CONDUCT instead of restating a subset of the rules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- `funding` in rockets-server and rockets-server-auth pointed at
  github.com/sponsors/btwld; the org has no sponsors listing
  (`hasSponsorsListing: false`), so npm would render a link to a page
  that does not exist. Removed rather than left broken.
- CONTRIBUTING records that `btwld/rockets` is the correct URL today and
  lists exactly what must change when the organization moves to
  `conceptadev` — the three metadata fields per publishable package plus
  the README links, in one change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-version

Publishing was a single `publish:conceptadev` script that hardcoded
`--tag dev` while every document promised `alpha`, and the repo carried
two release tools that nothing used and that no longer work here:
lerna 3.22.1 (current is v8; that version predates Yarn 4 and the
`workspace:` protocol) and standard-version (deprecated since 2022).
Both removed, along with `lerna.json`.

Replaced by thin wrappers over Yarn 4's own versioning and publishing —
no custom release script to maintain:

- `version:alpha|patch|minor|major` — bumps every publishable workspace
  in one command via `yarn version --deferred` + `version apply --all`.
- `release:check` — the full gate (build, typecheck:spec, lint:all,
  test, test:e2e).
- `release:audit` — fails on high-severity advisories.
- `release:dry` — packs all six packages so the tarball contents can be
  inspected before anything is published.
- `publish:alpha|beta|latest` — re-runs the gate, clean-builds, then
  publishes in topological order to the matching dist-tag.

Verified: a packed `package.json` carries real version ranges
(`^0.0.1-dev.0`), never the `workspace:` protocol, and all six tarballs
contain dist/README/LICENSE/CHANGELOG with no `src/`, `*.spec.*` or
`docs/` leaking in.

These commands require Yarn 4; run under the global Yarn 1 they used to
fail with an opaque error, so they now guard on the user agent and print
`Run them via: corepack yarn <script>`.

CONTRIBUTING documents the full release flow, including that moving from
alpha to beta is an explicit one-time version bump (prerelease continues
whichever identifier the current version carries).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ch transitive CVEs

**Scope rename.** Packages publish as `@concepta/*` — the same scope as
the upstream `@concepta/nestjs-*` stack they compose, distinguished by
name rather than by a separate scope. Verified before renaming: the
`@concepta` scope exists on npm with access, all six target names are
free, and nothing had been published under `@conceptadev`, so no
consumer is affected. 228 files updated; the upstream
`@concepta/nestjs-*` imports are untouched (the replaced string is
`@conceptadev/`, which cannot match them).

**GitHub org.** `repository`, `homepage` and `bugs` now point at
`github.com/conceptadev/rockets` (the org exists; the repo will be moved
there). This replaces the interim `btwld` value and the note in
CONTRIBUTING that tracked the pending move.

**Security — transitive dependencies.** `yarn up -R` on the two packages
flagged by the scanner:

- `brace-expansion` across all three major lines (1.1.12 → 1.1.18,
  2.0.2 → 2.1.4, 5.0.2 → 5.0.9), clearing CVE-2026-13149 and
  CVE-2026-14257 (ReDoS / exponential-time complexity).
- `body-parser` 2.2.2 → 2.3.0, clearing CVE-2026-12590 (DoS via an
  invalid `limit` option).

Both resolved recursively rather than pinned through `resolutions`, so
the fix survives future installs. `yarn npm audit --severity high` is
clean.

**Found while renaming:** the ESLint rule that forbids `rockets-core`
from importing the auth package targeted `@concepta/rockets-server-auth`,
a package name that has never existed — the real name is
`@concepta/rockets-auth`. The rule could never fire. Corrected and
verified to trigger on a probe import.

Verified: build, typecheck:spec, lint:all, 576 unit, 157 package e2e,
and all three sample apps (194 / 40 / 8+1 skipped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tnramalho tnramalho self-assigned this Aug 6, 2026
@tnramalho
tnramalho merged commit 3ba72ff into main Aug 6, 2026
1 of 2 checks passed
@tnramalho
tnramalho deleted the feat/vitest-migration branch August 6, 2026 16:41
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