Skip to content

refactor(auth): align rockets-server-auth with the Rockets patterns (audit follow-up) - #108

Draft
tnramalho wants to merge 29 commits into
mainfrom
refactor/auth-alignment
Draft

refactor(auth): align rockets-server-auth with the Rockets patterns (audit follow-up)#108
tnramalho wants to merge 29 commits into
mainfrom
refactor/auth-alignment

Conversation

@tnramalho

@tnramalho tnramalho commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Follow-up of the rockets-server-auth alignment audit (2026-08-27). Base is feat/schema-engine-104 (#105); rebase onto main once #105 lands. One commit per audit item, each with its own regression.

What changed

  1. Dropped the empty ConceptaRepositoryCompatModule — a no-op global module left from the pre-v8 bridge.

  2. Handlers forward the caller context as-is — the old helper silently replaced a non-AppContextHost context with a fresh one (hook-free, outside the caller's transaction). Now upstream AppContextHost.from(): same instance in, throws otherwise. GetActiveCredentialQuery takes ctx first and its handler requires the credentials repository (userCredentials is a mandatory entity).

  3. Auth no longer registers infrastructure core already owns (CqrsModule, RepositoryModule, CrudModule, a second global SwaggerUiModule). Core re-exports RepositoryModule (so TransactionScope stays injectable) and registers CrudModule.forRoot() unconditionally (auth mounts upstream CRUD controllers outside the plan). RocketsAuthOptionsInterface.swagger / .crud removed — they only fed the duplicates.

  4. Repository contract imported from @concepta/rockets-core everywhere (rule 2); core re-exports TransactionScope; a lint override on rockets-server and rockets-server-auth keeps it that way.

  5. Dead dependencies dropped (jsonwebtoken, passport*, @nestjs/jwt, accesscontrol); @types/passport-jwt / @types/passport-strategy stay because upstream's published .d.ts reference them (packed-consumer check proves it).

  6. GET /admin/users/:userId/roles answers through a named schema (RocketsAuthUserRoleDto), fail-closed; first package e2e for the admin user-role routes.

  7. Invitations work end to end — the first package e2e for the four invitation routes found the flow broken at every step on the alpha.9 line (details in the commit and in packages/rockets-server-auth/CHANGELOG.md): inviting a new address 500'd, no email handler was registered, the invitation was sent twice (second OTP killed the first), the acceptance listener ran twice per event (alias provider), the password was written v7-style onto the user row, the invitation entity lacked dateAccepted/dateRevoked, and already-accepted / revoked answered 500. Breaking: RocketsAuthInvitationResponseDto drops emailSent/emailError (the email is dispatched on commit, after the response); INVITATION_ACCEPTANCE_LISTENER_TOKEN removed; invitationEntity is typed against upstream's InvitationEntityInterface.

  8. Extension points proven in sample-server-auth — the package exports handler seams, controller extras and an exception base that no example exercised. The sample now overrides userCrud.handlers.signupHandler (blocked email domains, app-owned RocketsAuthException subclass) and decorates the generated OTP send route (otp.controller.routes.send, stricter throttle); test/auth-extension-points.e2e-spec.ts fails when either is removed. This is the answer to audit item 6 for now: prove what is public before cutting what is not (the cut of internal leakage — concrete handlers, internal modules, RAW_* tokens, build* factories — is a separate decision).

Trust model of invitations (unchanged semantics, now explicit)

Inviting an address that already has an account creates no user; accepting the invitation activates that account and sets the supplied password. The passcode only reaches the mailbox owner, so this is the same trust model as password recovery. An admin can therefore re-activate a deactivated user by inviting them — an admin-only action, by design.

Gates (all local, on the last commit)

build · api:report:check-built · typecheck:spec · unit 1116/1116 · package e2e 452/452 · sample-server 191 · sample-server-auth 49 · sample-code-review 8 · packed consumer · lint · lint:md.

Left open on purpose

  • Public surface (audit item 6): 151 of 209 exported names had no consumer anywhere; the legitimate extension points are now exercised by the sample (item 8). Cutting the internal leakage is still a maintainer decision.
  • On SQLite (single connection) upstream's post-commit listeners (InvitationRevokedListener → OTP clear, accepted notification) can interleave writes with the next request; the e2e waits for them. Postgres/MySQL are unaffected. Worth an upstream note.
  • POST /admin/users/:id/roles with an unknown roleId is a 500 (FK) — upstream AssignRoleCommand does not check the role exists.
  • The acceptance listener still swallows its own errors (event-listener contract): a failed onboarding answers 200 on acceptance. Design decision pending.

tnramalho and others added 29 commits August 25, 2026 22:41
Rockets used @nestjs/config only for registerAs + ConfigModule.forFeature to
hand default settings to upstream's createSettingsProvider. Those defaults
are now plain Nest providers registered by each module (core, swagger-ui,
server, auth); ConfigModule is no longer re-exported.

Why now: the upstream 8.0.0-alpha.9 graph pins @nestjs/config@12.0.0-next.0,
which is ESM-only — a CJS require() from the Rockets dist fails at runtime
while tsc stays green. The root resolutions pin is removed so upstream keeps
the version it declares. rockets-server-auth keeps it as a devDependency for
the e2e ConfigService stub.

RFC #104, stage 1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t shim

`@concepta/rockets-core/standard-schema` and `/standard-schema/swagger`
had no consumer in Rockets or its examples and duplicated what Nest 12
ships natively (`@Body({ schema })` + `StandardSchemaValidationPipe`,
`@SerializeOptions({ schema })`, `ApiResponse({ standardSchema })`).

Generated DTOs no longer stamp `@Allow()` on their keys: the stamp only
existed to survive a foreign `ValidationPipe({ whitelist: true })`, a
pipe that must not sit in front of a schema-validated route at all.

`isStandardSchema` / `getCarriedStandardSchema` move to
`common/utils/standard-schema.util.ts` (internal). Public API report
updated: the two entry points and their 24 declarations are gone.

RFC #104, stage 2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`zod-swagger-golden.e2e-spec.ts`, `zod-parity.e2e-spec.ts` and their
hand-written control resources compared a class-DTO `defineResource` twin
against the zod resource. Class-DTO authoring is being retired, so the
twin goes; `zod-library.e2e-spec.ts` keeps every document and runtime
assertion against the zod author/book pair alone. `contract.json` is
unchanged (the controls were never mounted in the app).

RFC #104, stage 3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…chemas

One schema engine. Every request body and response in Rockets is a named
zod schema (`withOpenApi(schema, id)`), validated per route by Nest's
native StandardSchemaValidationPipe with the Rockets exception factory
(`details[]` on every 400), serialized by the response schema (undeclared
columns never leave, Date -> ISO), and documented from the schema's own
JSON Schema bridge with a Rockets converter that `$ref`s named components.

Core: `defineResource` dto/input/output/paginated are schemas; zod
compiler emits `.zod.schemas`; `f.date()`, audit columns as `z.date()`;
response-exposed `z.iso.datetime()` rejected; optional fields admit the
`null` a nullable column reads back; `userMetadata` is
`{ entity, updateSchema, responseSchema }`; `SchemaValidatorConflictCheck`
refuses a global StandardSchemaValidationPipe; planner rejects two schema
instances under one component id; `createDocument` installs the converter
and lifts `definitions` out of upstream's inline CRUD request bodies;
exceptions filter vendors `mapHttpStatus` and handles alpha.9's
`RuntimeException extends HttpException` (hook 4xx no longer served as
500). Removed: createPaginatedDto, FreeFormJson, ROCKETS_TO_* options,
ZodBodyValidationInterceptor, whitelistedFromDto (-> validateWithSchema),
UserUpdateDto/UserResponseDto, BaseUser*Dto, PersistenceRow, ZodResourceDtos.

Server: `/me` rebuilt as `buildMeController(config)` (userMetadata null
before the first PATCH; hidden columns stay hidden). Auth: user/role/
invitation DTO classes are schemas composed from upstream schemas; admin,
signup, token, recovery and invitation routes on the engine; password
rotation runs consume + write in one transaction scope (the consume-only
scope left a finished transaction on the request context); new OTP login
e2e. `@concepta/nestjs-common` dropped. Examples and both contract.json
regenerated; docs updated.

Known gap (upstream): `CrudInitApiBody` inlines generated CRUD request
bodies, so `${Name}CreateDto`-style request components are inlined rather
than `$ref`'d (responses are).

RFC #104, stage 4.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…gine; legacy validators removed

Stages 5 and 6 of RFC #104, landed together: their agents ran in
parallel and share the regenerated contract.json / public API report.

Stage 5 — operationResource: compiled descriptors carry `inputSchema`,
`paramsSchema` and a schema `output` (no DTO classes). The generated
controller has a class-level StandardSchemaValidationPipe with the
Rockets factory; the body is `@Body({ schema })` behind a payload-shape
guard (missing body -> {}, array/scalar/Buffer -> 400 naming the whole
body), the query `@Query({ schema })` and the params `@Param({ schema })`
(raw params merged so extra path params still reach ctx.params). Output
is validated inline (null / mismatch -> 500). OpenAPI comes from the
schema bridge: named `<Base>Input` / `<Base>Output` components, query and
path params one per property. Defect fixed on the way: the generated
method exposed no `design:paramtypes`, so swagger documented no body,
query or params for any operation. Component-id uniqueness across CRUD
and operation resources is one planner check.

Stage 6 — hand-written routes: rockets-auth's OTP, change-password,
invitation revoke / acceptance-payload and admin role-assignment bodies,
sample-server's pet-share and sample-code-review's DTOs are named zod
schemas validated by each controller's own pipe (invitation-acceptance
`payload` is now validated). class-validator, class-transformer and
nestjs-zod are removed from every manifest; compileDtoClass / namedZodDto
are gone; the packed-consumer check installs no validation library beside
zod. Exception: rockets-auth keeps class-validator/class-transformer as
plain dependencies while nestjs-email/event (7.x) pull nestjs-common@7,
which requires them at import — the packed consumer proved it (locally
they resolved from a node_modules OUTSIDE the repo).

Also: sample-server scope hooks forward `ctx` to their repository
lookups (rule 16); docs, CHANGELOGs, api policy note.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review of the RFC #104 branch (adversarial pass) plus the first external
consumer run (realtystack) surfaced these; all fixed with a failing test
first.

- A create body that validates to `{}` is a valid create. Generated
  resources run on `RocketsCrudAdapter`, which drops the bare 400 the
  upstream adapter answered to an empty validated payload. An
  all-server-stamped sub-resource (`POST /parents/:id/stamps {}`) is the
  new e2e case.
- The fail-closed response check walks every wrapper (intersection,
  tuple, record/map/set value, readonly, catch, any single-child
  wrapper) and covers hand-supplied paginated envelopes.
- Hidden columns stay hidden at every depth of a computed field.
- One OpenAPI component describes one side: a schema used as both a
  request and a response is a document-build error.
- `f.date()` rejects null and booleans instead of storing 1970.
- Invitation acceptance validates `userMetadata` with the same default
  schema as signup and admin when the app configures none (a smuggled
  `userId` no longer reaches the row).
- A `@Body/@Query/@PARAM({ schema })` that no StandardSchemaValidationPipe
  reaches fails the boot (`requireSchemaPipe`); the route audit is
  always on, the route policy stays opt-in.
- Hand-written auth request bodies keep their component names
  (`LocalLoginDto`, `RefreshDto`, `Recovery*Dto`).
- Docs that still described the class-DTO era are rewritten.

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

The report records declarations reachable from public signatures; the
token/recovery controllers' body type aliases now point at the named
Rockets schemas (LocalLoginDto, RefreshDto, Recovery*Dto).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial review of the branch plus the realtystack run; every item
landed with a test that failed first.

- Generated CRUD request bodies are $ref'd to their named component in
  the OpenAPI document again (`restoreNamedRequestBodies` drops the
  inline stamp upstream's CrudInitApiBody leaves where the route's
  @Body({ schema }) is named — upstream #467). Both contracts regenerated:
  every *CreateDto / *UpdateDto / *ReplaceDto component is back.
- The one-component-one-side rule covers nested named schemas: one nested
  id emitted with two shapes is an error, not a last-wins merge.
- Hand-written @SerializeOptions({ schema }) gets the fail-closed check
  at boot (`requireClosedResponse`); `requireSchemaPipe` has its own
  exemption list (`allowUnvalidatedSchema`) instead of riding on `allow`.
- rockets-auth: admin PATCH /admin/users/:id and /admin/roles/:id bodies
  are validated (the schema moved from the controller to the operation —
  upstream stamps the pipe from the operation only), the user update runs
  in one outermost transaction scope (it answered 500 on the finished
  transaction upstream leaves on the context, #468), and the metadata
  repository pins userId from the caller on the update branch.
- Migration note for the renamed/removed OpenAPI components.

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

- The /me metadata handlers forward the request context to every
  repository call and pin userId from the caller on the update branch;
  UpsertUserMetadataCommand(ctx, userId, data) and
  GetUserMetadataQuery(ctx, userId) take the context first (breaking for
  handler overrides; migration note in the changelogs).
- A generated CRUD body with no schema fails the boot under
  requireSchemaPipe — the structural guard for the admin-update defect
  (a controller-level request.body documents and validates nothing).
  Two e2e fixtures that relied on unvalidated bodies now declare theirs.
- allowUnvalidatedSchema gets the same more-than-one-match guard as allow.
- The audit report lists responses documented with standardSchema but
  serialized through no @SerializeOptions (unserializedResponseSchemas).
- Migration notes: explicit @ApiBody beside a named @Body is dropped;
  allow / allowControllers no longer exempt the schema-pipe check.

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pipe in-side, Node 20.19 floor

- Computed-field projection rebuilds every composite wrapper (union,
  intersection, pipe, default, catch, readonly, nonoptional, lazy) so a
  `dto: { response: false }` column below one never reaches the wire;
  a hidden column below a wrapper it cannot rebuild (discriminated
  union, tuple, record, map, set) fails at definition time. Runtime
  regression for `f.compute(z.union([nested, fallback]))`.
- `assertFailClosedResponse` walks BOTH sides of a pipe: an ordinary
  `.transform()` keeps its (open) object on the IN side. Regression for
  `.passthrough().transform(v => v)`.
- Node floor is 20.19.0 in every published `engines`, the docs and the
  CI matrix (the CommonJS build loads the ESM Nest 12 line through
  require(esm); 20.18 fails with ERR_REQUIRE_ESM).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Loading vitest.config.mts straight into Node needs native TypeScript
stripping (Node 22+); the 20.19.0 leg exists to test the published
packages' floor (build, tests, packed consumer), not the repo tooling.

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

Second pass on the PR #105 review, after an adversarial check of the
first one:

- `.default()` / `.catch()` hand their payload over without running the
  inner schema, so a hidden column below them cannot be stripped — they
  are rejected at definition time (they were wrongly "rebuilt").
- A recursive `z.lazy` with a hidden column overflowed the stack: the
  rebuilt lazy is memoized per source instance so walker identity holds.
- The strip now runs on all three response paths — computed fields, JSON
  columns and exposed relations — not only under `f.compute()`.
- The fail-closed walker reads a pipe's IN side only when its OUT is a
  transform; `z.pipe(open, closed)` strips on the way out and passes.
- The Vitest runner preloads `@nestjs/core` (setup file), which removes
  the ERR_REQUIRE_CYCLE_MODULE Node 20.19 raised when `@nestjs/cqrs`
  required the still-evaluating ESM module. Both CI legs now run the full
  suite; `fail-fast: false`; the module cache is keyed by Node version.

Regressions for each case fail against the previous commit.

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

Third pass on the PR #105 review round, after a second adversarial seal:

- An `operationResource` output built from an entity schema strips its
  `dto: { response: false }` fields — the fourth response path, beside
  computed fields, JSON columns and exposed relations. E2E asserts the
  secret is absent from the HTTP body.
- A TOP-LEVEL `.default()` on a field with a hidden column is rejected
  at definition time (it was silently dropped, and the row then failed
  serialization at runtime); `.prefault()` is rebuilt — its payload does
  run through the inner schema.
- The fail-closed walker reads a pipe's IN side whenever its OUT passes
  values through (transform, any, unknown, custom), not only transforms.
- CI: one test-results artifact per matrix leg (the report reads the
  22.x one); release-readiness pins the exact 20.19.0 floor; the wording
  no longer claims the example apps run in the PR matrix.
- The cqrs/ESM runner gap is filed upstream (nestjs/nest#17583) and the
  preload file says when to remove it.

Regressions for each case fail against the previous commit.

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

Fourth pass on the PR #105 review round, after a third adversarial seal:

- A hand-written response schema (dto.response, operations.*.output,
  dto.paginated, userMetadata.responseSchema) is not projected and keeps
  the author's component id, so a `dto: { response: false }` field
  inside it is rejected at definition time with a pointer at `.omit()`
  (`assertNoHiddenFields`) instead of reaching the wire.
- The projected field is stripped WITH its wrappers (`stripHidden` on
  the field itself): a top-level `z.preprocess` is kept and rebuilt, and
  a top-level `.default()` / `.catch()` is rejected like a nested one —
  no more peel-then-partially-reapply.
- The fail-closed walker's pass-through test is recursive: `any`,
  `unknown`, `custom`, `transform` behind optional / nullable / readonly
  / lazy or as a union member also make a pipe's IN side walkable.
- Stale CI comment about the packed-consumer contract corrected.

Regressions for each case (six new) fail against the previous commit.

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

Fifth pass on the PR #105 review round, after a fourth adversarial seal:

- rockets-auth hands `userCrud.model` / `roleCrud.model` straight to
  upstream CRUD serialization (no defineResource projection), so only
  the component name was checked. The signup, admin-users and
  admin-roles modules now also require a fail-closed schema with no
  `dto: { response: false }` field (`assertNoHiddenFields`, exported from
  core). Regression per module, each failing against the previous commit.
- The fail-closed walker's pass-through test reaches composites: an OUT
  that is an array / object / record / intersection / nested pipe holding
  `any` / `unknown` / `custom` / a transform hands input through, so the
  pipe's IN side is walked. Five new spec rows.

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

Sixth pass on the PR #105 review round, after a fifth adversarial seal:

- `passesThrough` descended through `schemaChildren`, whose pipe branch
  called `passesThrough` again with a fresh cycle set — a recursive lazy
  that crosses a `z.preprocess` overflowed the stack in every walker
  (fail-closed check, hidden-field detection, projection). It is now
  memoized per schema instance, with the in-progress entry reading
  `false`, so a cycle terminates. Regressions on all three entry points
  fail against the previous commit with RangeError.
- The fail-closed guard on rockets-auth's `userCrud.model` /
  `roleCrud.model` had no test: an open (`.catchall()`) model is now
  rejected by a regression per module.
- CONFIGURATION.md documents strip-vs-reject (projected vs hand-written
  response schemas) and the deliberate over-flag on pass-through pipes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ields in serializer schemas

Seventh pass on the PR #105 review round, after a sixth adversarial seal:

- The pass-through memo stored an in-progress `false` as a final answer,
  which made the fail-closed verdict depend on visit order and fail
  OPEN (the same schema reported nothing when the branch that closed a
  cycle was visited first). Only `true` is memoized now (the predicate is
  monotone); termination comes from a per-walk in-progress set threaded
  through the shared child walker. Regressions pin both field orders and
  a warm-then-probe run over shared instances; both fail against the
  previous commit.
- The route audit rejects a hand-written `@SerializeOptions({ schema })`
  that declares a `dto: { response: false }` field
  (`RouteAuditEntry.hiddenResponseField`, `requireClosedResponse`) — the
  last response path with neither strip nor reject.
- CONFIGURATION: `op.sse()` declares no `output` at all (not `output:
  false`).

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

The previous commit said both memo regressions failed against c571386;
only the field-order one did — its warm-up case computed the cold
verdict first, which cached `true` on the shared node before the walk
that would have poisoned it. The warm-up now comes first, on the same
instances, with the transform inside the cycle; it fails on c571386.

Also: the two JSDoc blocks orphaned by the last insertions are back on
`assertNoHiddenFields` / `readOpenResponseSchema`, the open-response
reader reuses `readSerializerSchema`, and the CHANGELOG notes the
complexity trade of a walker with no `false` memo.

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A global module with no providers, self-described as a no-op left from the
pre-v8 repository bridge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
resolveConceptadevAppContext replaced any value that was not an
AppContextHost with a fresh host, so a mistyped context ran hook-free and
outside the caller's transaction without any signal. Upstream
AppContextHost.from() keeps the instance and throws otherwise.

GetActiveCredentialQuery takes ctx first and requires it, like every other
command/query; its handler requires the credentials repository —
userCredentials is a mandatory persistence entity, so a missing repository
is a wiring error, not "no credential".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RocketsAuthModule always boots inside RocketsCoreModule, which registers
CqrsModule, RepositoryModule, CrudModule and SwaggerUiModule once. The auth
module registered all four again; the Swagger one is global, so two
registrations competed for the same document, and the crud/swagger option
fields existed only to feed those duplicates.

Core now re-exports RepositoryModule (TransactionScope stays injectable from
auth and bundles) and registers CrudModule.forRoot unconditionally, because
auth mounts upstream CRUD controllers outside the plan. Public API report
regenerated (also picks up the GetActiveCredentialQuery change).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Feature packages consume RepositoryInterface / Where /
getDynamicRepositoryToken / RepositoryModuleInterface through core's
re-exports (AGENTS.md rule 2); TransactionScope joins that list so the last
seven direct @concepta/nestjs-repository imports go too. A lint override on
rockets-server and rockets-server-auth keeps it that way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jsonwebtoken, passport, passport-jwt, passport-strategy, @nestjs/jwt and
accesscontrol are owned by upstream nestjs-authentication /
nestjs-access-control; @concepta/nestjs-repository and accesscontrol move to
devDependencies (fixtures only). @types/passport-jwt and
@types/passport-strategy stay: upstream's published .d.ts reference those
modules while listing the types only as devDependencies, so the packed
consumer's tsc fails without them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /admin/users/:userId/roles returned the raw upstream RoleAssignment
aggregates with no response contract. It now maps them to plain rows,
serializes each through RocketsAuthUserRoleDto
(StandardSchemaSerializerInterceptor, fail-closed) and documents the array
in OpenAPI. First package e2e for the admin user-role routes.

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

The new invitation e2e (create, reattempt, accept, revoke, guards) found the
flow broken at every step on the alpha.9 line:

- POST /admin/invitations answered 500 for any address without an account
  (upstream v8 only resolves an existing user). RocketsInviteUserByEmailCommand
  creates the invited account inactive, in the same scope as the invitation.
- SendInvitationEmailHandler / SendAcceptedEmailHandler were the configured
  notification port but never registered as providers: no email was sent.
- The controller sent the invitation a second time after creating it;
  upstream create() already does, and the second OTP deactivated the one
  the invitee received. Acceptance could never succeed.
- The acceptance listener received every InvitationAcceptedEvent twice: it
  was also provided under an alias token, and Nest CQRS registers a handler
  once per provider wrapper. INVITATION_ACCEPTANCE_LISTENER_TOKEN removed.
- The listener wrote v7-style passwordHash columns onto the user row; v8
  login reads user credentials. The password now goes through the
  set-password port recovery uses.
- Invitation entities need dateAccepted / dateRevoked: upstream derives
  active / accepted from them. invitationEntity is typed against upstream's
  InvitationEntityInterface; sample entity and e2e fixture fixed.
- Already-accepted → 409 and revoked → 410 instead of 500 (upstream
  exceptions carry no HTTP status).
- RocketsAuthInvitationResponseDto drops emailSent / emailError: the email
  is dispatched on commit, after the response. Sample contract regenerated
  (also picks up RocketsAuthUserRoleDto from the previous commit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The package exports handler seams, controller extras and an exception base
that no example exercised — promises nobody tested. The sample now overrides
the signup handler (userCrud.handlers.signupHandler: blocked email domains,
app-owned RocketsAuthException subclass) and decorates the generated OTP
send route (otp.controller.routes.send: stricter throttle), with an e2e that
fails when either extension point is removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base automatically changed from feat/schema-engine-104 to main September 9, 2026 21:20
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