refactor(auth): align rockets-server-auth with the Rockets patterns (audit follow-up) - #108
Draft
tnramalho wants to merge 29 commits into
Draft
refactor(auth): align rockets-server-auth with the Rockets patterns (audit follow-up)#108tnramalho wants to merge 29 commits into
tnramalho wants to merge 29 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up of the rockets-server-auth alignment audit (2026-08-27). Base is
feat/schema-engine-104(#105); rebase ontomainonce #105 lands. One commit per audit item, each with its own regression.What changed
Dropped the empty
ConceptaRepositoryCompatModule— a no-op global module left from the pre-v8 bridge.Handlers forward the caller context as-is — the old helper silently replaced a non-
AppContextHostcontext with a fresh one (hook-free, outside the caller's transaction). Now upstreamAppContextHost.from(): same instance in, throws otherwise.GetActiveCredentialQuerytakesctxfirst and its handler requires the credentials repository (userCredentialsis a mandatory entity).Auth no longer registers infrastructure core already owns (
CqrsModule,RepositoryModule,CrudModule, a second globalSwaggerUiModule). Core re-exportsRepositoryModule(soTransactionScopestays injectable) and registersCrudModule.forRoot()unconditionally (auth mounts upstream CRUD controllers outside the plan).RocketsAuthOptionsInterface.swagger/.crudremoved — they only fed the duplicates.Repository contract imported from
@concepta/rockets-coreeverywhere (rule 2); core re-exportsTransactionScope; a lint override onrockets-serverandrockets-server-authkeeps it that way.Dead dependencies dropped (
jsonwebtoken,passport*,@nestjs/jwt,accesscontrol);@types/passport-jwt/@types/passport-strategystay because upstream's published.d.tsreference them (packed-consumer check proves it).GET /admin/users/:userId/rolesanswers through a named schema (RocketsAuthUserRoleDto), fail-closed; first package e2e for the admin user-role routes.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 lackeddateAccepted/dateRevoked, and already-accepted / revoked answered 500. Breaking:RocketsAuthInvitationResponseDtodropsemailSent/emailError(the email is dispatched on commit, after the response);INVITATION_ACCEPTANCE_LISTENER_TOKENremoved;invitationEntityis typed against upstream'sInvitationEntityInterface.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 overridesuserCrud.handlers.signupHandler(blocked email domains, app-ownedRocketsAuthExceptionsubclass) and decorates the generated OTP send route (otp.controller.routes.send, stricter throttle);test/auth-extension-points.e2e-spec.tsfails 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
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/roleswith an unknownroleIdis a 500 (FK) — upstreamAssignRoleCommanddoes not check the role exists.