Zod security hardening (alpha.8) + full Jest→Vitest migration - #31
Conversation
…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>
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 55 |
| Duplication | 3 |
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| path: '**/node_modules' | ||
| key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} | ||
| path: '/node_modules' | ||
| key: ${{ runner.os }}-modules-${{ hashFiles('/yarn.lock') }} |
There was a problem hiding this comment.
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') }}There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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)
- HIGH / blocker — computed fields with a hidden column vanish from responses; compute output is not runtime-enforced —
packages/rockets-core/src/zod/zod-projections.ts(withHiddenFieldsRemoved). Rebuilding the field loses therocketsFieldMetaregistration, socompileDtoClassnever 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 (toPlainisexposeAll), including keys never declared in the compute schema. Reproduced against the built dist replicating theCrudSerializeInterceptoroptions — repro output in the inline comment. The new unit test asserts schema keys only, so it cannot catch this. - MEDIUM — CI dependency cache is dead —
.github/workflows/ci-pr-test.ymlandci-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. - LOW —
Unhandled 5xxlog 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)
OwnerScopeHookis fail-open when no actor is on the context (documented as "public route" semantics). It relies entirely onAuthServerGuardrejecting unauthenticated requests first; any future route that is authenticated through a mechanism that doesn't populate the actor would silently return the full table.isThunkindefine-sub-resource.tsnow throws forfunction-expression entities. Consumers compiling with a target below ES2015 (transpiled classes have writable prototypes) would hit the ambiguity throw for a legitimateentity: 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.tsand 30*.e2e-spec.tsexist underpackages/— the include/exclude globs invitest.config.tsun-test nothing.tsconfig.spec.jsoncovers all spec/e2e/fixture locations; the only excluded specs are the Playwright files underapps/web/e2e(correctly out of Vitest scope).
Checked and clean
- Owner scoping defaults:
resolveOwnerColumns+applyOwnerHookswireOwnerScopeHookper owner column with per-side opt-outs; read coverage is complete (the repository abstraction's only read entry points arefindOne/findAndCount, both hooked).zodModuleResourcecontributes non-CRUD slices only, so it is not a scoping bypass. - Response exposure (non-compute paths): runtime-enforced, not docs-only —
compileDtoClassemits class-level@Exclude+ per-field@Expose, andtoInstanceruns withexcludeAll/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 sharesprojectSchema; create/update parity with the oldomit()behavior holds,userIdfrozen on update, and managed fields are enforced on top of the projection.- Sub-resource scoping:
materialise-sub-resource.tsassertsScopeGuard/ScopeHooksurvive provider merging againstcore.providers(the real artifact); guard now precedes consumer decorators.path-scope.guard.tscache-key fix (JSON tuple vs::join) is correct. - Exceptions filter rewrite: unwrap chains are cycle-safe,
unwrapToClientRuntimeExceptioncorrectly 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 theNODE_ENVstack-leak gate. - Firestore:
Date→Timestamp→Dateround 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;expectbalance is +95/−2, and both removals are org-rename substitutions with the assertion preserved.globals: false+ SWC decorator metadata config is correct;groupOrderis unique per project. - CI report pipeline:
download-artifact@v4+run-idfix is correct, artifact paths line up withtest:ci/test:e2e:covoutputs,java-junitparses Vitest's junit reporter output,$GITHUB_OUTPUTmigration 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→@conceptadevrename 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.
|
All three review findings are addressed as of
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. |
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>
What this PR contains
Two phases, stacked (6 commits):
Phase 1 — alpha baseline + security hardening (
b43f6b8..aed9e47)@conceptadev/*; upstream@concepta/nestjs-*moves to8.0.0-alpha.8(NestJS 12 alpha).ownerScope: falseopts out — the sample pet resource does, in favour of its owner-or-shared hook). Breaking for consumers relying on unscoped reads.tokenExpiresAtwhile missingapiKey). Secrets opt out explicitly withdto: { response: false }.defineZodUserMetadataprojects throughprojectSchema— closed a second projection path that ignoredresponse: false.core.providers(real artifact) + behavioural e2e; the write-onlymeta.guardsmirror is deleted.Loggerin the exceptions filter (noconsole, noNODE_ENVgate), local validation-error flattening (no private Nest API), descriptor-basedisThunk, WeakMap hook caches, Firestore nativeTimestampround-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).projectsmodel: rootvitest.config.tsdeclaresunit,e2e-packages, and one project per example;vitest.shared.tsholds the single SWC block (decorator metadata for Nest DI — esbuild can't emit it).yarn test:allruns the whole monorepo: 106 files / 971 tests.globals: falseeverywhere — every test file imports its API explicitly; proven by the runner itself.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, deepdist/imports, under-typed doubles) — all fixed or deleted, none suppressed. Two interfaces consumers need are now exported from the public index.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
yarn build/yarn typecheck:spec/yarn lint:all--project unit)--project e2e-packages)yarn test:ciartifactsKnown limitations (documented in CHANGELOG)
SafeCrudContextInterceptorstill string-matches an upstream error message (named contract test pins it; deleted when upstream5249672fships).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.@concepta 8.0.0-alpha.x.🤖 Generated with Claude Code