nestjs-slightly-better-auth 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -983,16 +983,16 @@ runConformance(
983
983
  );
984
984
  ```
985
985
 
986
- | Kit | Unit | Covers |
987
- | ---------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
988
- | `httpPlatformConformance` | `HttpPlatform` | Mounting (including a dynamic `allowedHosts` base URL), byte-exact bodies, limits with host CORS, URL and header facts, client IP (production-mode per-IP rate-limit buckets, Better Auth `trustedProxies`), HTTP/2, write-back, request accessors, error delivery, lifecycle (no adapter, including microservices, late adapter, reused `TestingModule`, double `forRoot`, hooks kept after `close()`), diagnostics (404 hints, `W_PROXY_UNTRUSTED`) and log hygiene |
989
- | `transportConformance` | `AuthTransport` | Selection, credentials, per-request memoization, per-invocation decisions, acceptance, cookie forwarding or refresh suppression, origin and form-CSRF rules including forwarding on safe operations and inherited forwarding fields, `@SkipOriginCheck()` and `disableCSRFCheck` opt-outs and direct caller-session calls, WebSocket handshake origins on every message and at connection time (untrusted, missing and forwarded origins, junk tokens, dynamic base URLs, function-valued `trustedOrigins`), subscription credentials and origins, error shapes and logging, the exact coverage report of the transport's claims, metadata canaries, `globalScope: false`, readings with and without the scope interceptor, public handlers in unclaimed contexts, inherited handlers, dynamic base URLs, stale GraphQL contexts, mixed-kind session reads, nested and federation readings, lazy scope construction and fail-closed extraction |
990
- | `principalSourceConformance` | `PrincipalSource` | Rejection without throwing, storage outages, delivery of every own-credential `Set-Cookie` exactly once, refresh suppression, clean absence, principal shape, declared credential headers, `sessionBacked` and `delegates` declarations, dynamic base URLs (direct and behind a trusted proxy), credential redaction in logs and raw causes; for API-key sources the verification results, outage and write-outage probes and organization-owned keys; for session-backed sources short-circuited session reads and the cookie bridge (third-party sign-up, foreign credentials); for sources of the `jwt` plugin the issuer and audience claims and key-set outages |
991
- | `policyConformance` | `AuthorizationPolicy` | Deny values, storage outages with the cookie cache off and warm, APIError mapping and re-classification, sessions lost before the policy's call, delegation scope, unnamed-kind gating, per-invocation decisions with shared I/O, request-scoped class policies, class policies' `requires` fields, plugin prerequisites, and the admin, organization and API-key rows below; requirements name policy objects (construct a DI policy with its dependencies) |
986
+ | Kit | Unit | Covers |
987
+ | ---------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
988
+ | `httpPlatformConformance` | `HttpPlatform` | Mounting (including a dynamic `allowedHosts` base URL), byte-exact bodies, limits with host CORS, URL and header facts, client IP (production-mode per-IP rate-limit buckets, Better Auth `trustedProxies`), HTTP/2, write-back, request accessors, error delivery, lifecycle (no adapter, including microservices, late adapter, reused `TestingModule`, double `forRoot`, hooks kept after `close()`), diagnostics (404 hints, `W_PROXY_UNTRUSTED`) and log hygiene |
989
+ | `transportConformance` | `AuthTransport` | Selection, credentials, per-request memoization, per-invocation decisions, acceptance, cookie forwarding or refresh suppression, origin and form-CSRF rules including forwarding on safe operations and inherited forwarding fields, `@SkipOriginCheck()` and `disableCSRFCheck` opt-outs, application-route checks under Better Auth's boolean origin skip, direct caller-session calls, WebSocket handshake origins on every message and at connection time (untrusted, missing and forwarded origins, junk tokens, dynamic base URLs, function-valued `trustedOrigins`), subscription credentials and origins, error shapes, logging (one ERROR entry per request and distinct reader configuration error across GraphQL list items), the exact coverage report of the transport's claims, metadata canaries, `globalScope: false` and its boot summary line, readings with and without the scope interceptor, public handlers in unclaimed contexts, inherited handlers, dynamic base URLs, stale GraphQL contexts, mixed-kind session reads, nested and federation readings, lazy scope construction and fail-closed extraction |
990
+ | `principalSourceConformance` | `PrincipalSource` | Rejection without throwing, storage outages (resolved directly and through a guarded HTTP route), delivery of every own-credential `Set-Cookie` exactly once (to the cookie sink and to an HTTP client, without other headers of those calls), refresh suppression, clean absence, principal shape, declared credential headers, `sessionBacked` and `delegates` declarations, dynamic base URLs (direct and behind a trusted proxy), credential redaction in logs and raw causes; for API-key sources the verification results, outage and write-outage probes and organization-owned keys; for session-backed sources short-circuited session reads and the cookie bridge (third-party sign-up, foreign credentials); for sources of the `jwt` plugin the issuer and audience claims and key-set outages |
991
+ | `policyConformance` | `AuthorizationPolicy` | Deny values, storage outages with the cookie cache off and warm, APIError mapping and re-classification, sessions lost before the policy's call, delegation scope, unnamed-kind gating, per-invocation decisions with shared I/O, request-scoped class policies, class policies' `requires` fields, plugin prerequisites, and the admin, organization and API-key rows below; requirements name policy objects (construct a DI policy with its dependencies) |
992
992
 
993
- The principal-source kit passes each credential factory the instance the case runs against: `credentials.valid(auth)` receives `auth`, or for `S-dynamic-base-url` a variant with a dynamic `baseURL` (allowed hosts `localhost:3000` and `public.example`, no fallback) that `variant(overrides)` builds. The default variant is `createConformanceAuth()` with the plugins of `auth`; a call that creates a credential on the variant passes a `host` header. `credentials.apiKey(auth, fields)` creates the keys of the `S-apikey-*` cases (default: `auth.api.createApiKey` for a new user), `credentials.organizationKey(auth)` enables `S-apikey-org-key`, and `http: { platform, createHttpAdapter }` enables the `S-bridge-*` routes. Each case boots its own module, so register a source that keeps state across requests, such as the API-key outage probe or a JWT key cache, as a `defineExtension()` factory.
993
+ The principal-source kit passes each credential factory the instance the case runs against: `credentials.valid(auth)` receives `auth`, or for `S-dynamic-base-url` a variant with a dynamic `baseURL` (allowed hosts `localhost:3000` and `public.example`, no fallback) that `variant(overrides)` builds. The default variant is `createConformanceAuth()` with the plugins of `auth`; a call that creates a credential on the variant passes a `host` header. `credentials.apiKey(auth, fields)` creates the keys of the `S-apikey-*` cases (default: `auth.api.createApiKey` for a new user), `credentials.organizationKey(auth)` enables `S-apikey-org-key`, and `http: { platform, createHttpAdapter }` enables the guarded route of the HTTP-route rows of `S-cookie-forwarded` and `S-infra-throws` and the `S-bridge-*` routes. Each case boots its own module, so register a source that keeps state across requests, such as the API-key outage probe or a JWT key cache, as a `defineExtension()` factory.
994
994
 
995
- Every case boots real Nest applications or modules with a real Better Auth instance that includes `conformanceProbePlugin()`. The probe counts and faults every operation of the instance's database adapter, so storage outages reach endpoints that would swallow them, and records the `Set-Cookie` lines each call produces. `createConformanceAuth({ plugins })` builds such an instance: memory adapter, `testUtils()`, `bearer()`, the probe plugin, `nestjs()` last, and `session.updateAge: 0` and origin checks on (`advanced.disableOriginCheck: false` and `advanced.disableCSRFCheck: false`), whatever `options` says. The principal-source and policy kits take that instance as `auth`; the principal-source kit rejects an instance without `session.updateAge: 0`. The transport kit creates its own instance, with the organization plugin and the organizations its `org` fixture names, and passes it to your `createApp` harness together with the kit's API-key source in `principals`, a `logger` to boot with, and the `globalScope`, `appEnhancers`, `forwardDirectCalls`, `fieldResolverEnhancers`, `fieldResolverCoverage`, `federation` and `graphqlContext` settings of each boot. Apply every fixture decorator, including the kit's `SetMetadata()` marker, which maps the transport's claims to fixtures, and the class decorators of `inherited.subclasses` and `graph.classDecorators`. On a connection-shaped browser leg, the kit's cookie credential carries the instance's base URL, `http://localhost:3000`, as `Origin`, as a browser page of that origin does; the `T-ws-origin-*` cases pass their own `Origin`, `Host` and forwarding headers, which the harness sends in the handshake.
995
+ Every case boots real Nest applications or modules with a real Better Auth instance that includes `conformanceProbePlugin()`. The probe counts and faults every operation of the instance's database adapter, so storage outages reach endpoints that would swallow them, and records the `Set-Cookie` lines each call produces. `createConformanceAuth({ plugins })` builds such an instance: memory adapter, `testUtils()`, `bearer()`, the probe plugin, `nestjs()` last, and `session.updateAge: 0` and origin checks on (`advanced.disableOriginCheck: false` and `advanced.disableCSRFCheck: false`), whatever `options` says. Its second argument, `ConformanceAuthSettings`, sets those two flags explicitly: `createConformanceAuth(options, { disableOriginCheck: true, disableCSRFCheck: false })` builds the instance of the `T-csrf-http-unsafe` row in which Better Auth skips origin checks on its own routes while application routes stay checked. The principal-source and policy kits take that instance as `auth`; the principal-source kit rejects an instance without `session.updateAge: 0`. The transport kit creates its own instance, with the organization plugin and the organizations its `org` fixture names, and passes it to your `createApp` harness together with the kit's API-key source in `principals`, a `logger` to boot with, and the `globalScope`, `appEnhancers`, `forwardDirectCalls`, `logSummary`, `fieldResolverEnhancers` (including `'filters'`), `fieldResolverCoverage`, `federation` and `graphqlContext` settings of each boot. GraphQL harnesses serve `graph.roots.items`, when given, as a list of `FixtureNode`, and the `organizationReader` and `serviceSession` fields as `String` fields. Apply every fixture decorator, including the kit's `SetMetadata()` marker, which maps the transport's claims to fixtures, and the class decorators of `inherited.subclasses` and `graph.classDecorators`. On a connection-shaped browser leg, the kit's cookie credential carries the instance's base URL, `http://localhost:3000`, as `Origin`, as a browser page of that origin does; the `T-ws-origin-*` cases pass their own `Origin`, `Host` and forwarding headers, which the harness sends in the handshake.
996
996
 
997
997
  The policy kit judges principals through the real evaluator, and cases that need route planning, acceptance or principal resolution send in-process requests through the real `BetterAuthGuard`. Pass the sources of the requirement's other principal kinds in `sources` (for example `apiKeyPrincipal()`). Unit-specific cases run when the requirement contains the built-in admin policy (`Z-admin-*`), an organization policy (`Z-org-ref-types` and the organization row of `Z-apikey-quota-per-request`) or the API-key permission policy (its `Z-apikey-quota-per-request` row), and skip with the reason otherwise. They use the instance's configuration where it matters: `session.cookieCache.enabled` for the warm-cache rows and `apiKey({ enableSessionForAPIKeys: true })` for the API-key session and quota rows. The kit builds the instances that need another configuration (`customSession` role shapes, `adminUserIds`, a dynamic `baseURL`, an instance without `apiKey()`) with `variant(overrides)`, which defaults to `createConformanceAuth(overrides)`. A storage job passes a variant that builds the same instance on its own database: add what `createConformanceAuth()` adds (`testUtils()`, `bearer()`, `conformanceProbePlugin()`, `nestjs()` last, `session.updateAge: 0` and `advanced.disableOriginCheck: false`), and migrate the schema before returning it.
998
998
 
@@ -1010,7 +1010,7 @@ The kit sends the probe plugin's trusted `Origin` with every delivered request.
1010
1010
  - `Z-apikey-quota-per-request`, the API-key session setup
1011
1011
  - `Z-admin-banned` on one connection. It runs when `connect` and a positive `connectionPrincipalTtlMs` are present, and first checks that a default-freshness handler reuses the connection's principal after sign-out.
1012
1012
 
1013
- This repository runs the platform kit against Express and Fastify, including the Fastify HTTP/2 adapter for the HTTP/2 rows; the transport kit against the built-in HTTP transport, an in-process reference transport, Socket.IO and `ws` (each with and without a principal TTL), the RPC transport over TCP (standalone and hybrid), gRPC, NATS, RabbitMQ, MQTT, Redis and Kafka, and the Apollo (Express and Fastify) and Mercurius transports over HTTP and over graphql-ws (with and without a subscription principal TTL; Mercurius also without `mercuriusSubscriptionContext()`), with federation 2 subgraphs and a federation 1 run of `T-reference-resolver`; the principal-source kit against the session source (its `S-bridge-*` rows on Express and Fastify), the API-key source with user-owned and organization-owned keys, and the design's JWT source sketch with derived and explicit claims; and the policy kit against the admin, organization (also with `customSession`), API-key and fresh-session policies. The policy kit's delivery rows run over Socket.IO and `ws` with a 300 s principal TTL, over RPC over TCP (standalone and hybrid), and over the Apollo (Express and Fastify) and Mercurius transports over HTTP and over graphql-ws with a 300 s subscription principal TTL. `Z-admin-null-role` also runs on PGlite with Better Auth's Kysely adapter and migrations. gRPC skips `T-error-shape`, because it delivers only a status and a message. The kit reads each call's browser leg while its invocation runs, because the GraphQL transports reject a completed request. Mutation tests confirm that each of these defects fails the intended case: a platform merging `Set-Cookie`, handing Better Auth no client IP, ignoring its proxy trust, dropping the request host or stripping forwarding headers, a kernel that unbinds hooks on `close()`, reports taking over a closed binding or keeps a microservice's binding open, a transport sharing invocation state across aliases, a transport without a browser leg on safe operations, a `defineExtension()` transport with a lineage or nesting and no helper, a WebSocket transport that enforces its handshake like a safe read, exempts handshakes that carry a token, builds its leg URL from the bare handshake path or from forwarded headers, or answers internal errors with their cause, a GraphQL transport whose aliases share one invocation, whose lineage gives every field of an operation one position, whose field resolvers do not inherit their root's access, or that serves reference resolvers without field guards, a GraphQL harness that omits `invokeGraph`, `invokeEntities` or `invokeConnection`, a transport that claims every context type, runs no metadata canary, claims classes without handlers or drops the `Host` header without declaring hostless calls, a planner that caches plans by handler function, ignores `@SkipOriginCheck()` or skips the origin check of safe operations, an HTTP transport whose invocation is not the handler's request (Fastify), a WebSocket transport whose messages on one socket share their invocation, a GraphQL transport that treats a completed request as live, ignores `connectionParams` credentials, treats socket operations as safe reads, exempts `connectionParams` tokens, serves inherited forwarding fields without field guards, omits `W_FIELD_RESOLVER_INHERITS` or `I_CLASS_METADATA_OPERATIONS_ONLY`, a planner that applies class-level acceptance to field resolvers or records a reading on a form-mode nested public field, a source throwing its denial, a source dropping its refresh cookie, a source turning a storage outage into a 401, API-key sources that report every failure as an invalid key, verify once per call, run without the outage probe or latency signal, probe on every invalid key, touch key rows, or map organization keys to users, a session source that trusts hook answers on authoritative routes, a source that wraps errors without the request's credentials, JWT sources that expect the basePath URL as issuer or deny a key-set outage, sources that drop the host or forwarded headers, a route planner that forwards every handler's direct calls, a `forwardForeignCookies()` without a declaration check or without forwarding, a policy widening delegated principals, a policy turning a lost session or a storage outage into a denial, an organization policy coercing non-string inputs (also over `ws`) or repeating its Better Auth call, admin policies that admit API keys by default, lack fresh identity (also over RPC) or hostless calls, ignore a key owner's ban, judge the session's role or trust the stored user row (also on PGlite), a request scope that reuses a connection's principal for authoritative handlers (Socket.IO, graphql-ws), a WebSocket transport that declares a principal TTL it does not apply, a graphql-ws delivery that opens a new socket for each operation, and boot validation or route planning that ignore request-scoped dependencies or a class policy's `requires` field.
1013
+ This repository runs the platform kit against Express and Fastify, including the Fastify HTTP/2 adapter for the HTTP/2 rows; the transport kit against the built-in HTTP transport, an in-process reference transport, Socket.IO and `ws` (each with and without a principal TTL), the RPC transport over TCP (standalone and hybrid), gRPC, NATS, RabbitMQ, MQTT, Redis and Kafka, and the Apollo (Express and Fastify) and Mercurius transports over HTTP and over graphql-ws (with and without a subscription principal TTL; Mercurius also without `mercuriusSubscriptionContext()`), with federation 2 subgraphs and a federation 1 run of `T-reference-resolver`; the principal-source kit against the session source (its HTTP-route and `S-bridge-*` rows on Express and Fastify), the API-key source with user-owned and organization-owned keys, and the design's JWT source sketch with derived and explicit claims; and the policy kit against the admin, organization (also with `customSession`), API-key and fresh-session policies. The policy kit's delivery rows run over Socket.IO and `ws` with a 300 s principal TTL, over RPC over TCP (standalone and hybrid), and over the Apollo (Express and Fastify) and Mercurius transports over HTTP and over graphql-ws with a 300 s subscription principal TTL. `Z-admin-null-role` also runs on PGlite with Better Auth's Kysely adapter and migrations. gRPC skips `T-error-shape`, because it delivers only a status and a message. The kit reads each call's browser leg while its invocation runs, because the GraphQL transports reject a completed request. Mutation tests confirm that each of these defects fails the intended case: a platform merging `Set-Cookie`, handing Better Auth no client IP, ignoring its proxy trust, dropping the request host or stripping forwarding headers, a kernel that unbinds hooks on `close()`, reports taking over a closed binding or keeps a microservice's binding open, a transport sharing invocation state across aliases, a transport without a browser leg on safe operations, a `defineExtension()` transport with a lineage or nesting and no helper, a WebSocket transport that enforces its handshake like a safe read, exempts handshakes that carry a token, builds its leg URL from the bare handshake path or from forwarded headers, or answers internal errors with their cause, a GraphQL transport whose aliases share one invocation, whose lineage gives every field of an operation one position, whose field resolvers do not inherit their root's access, or that serves reference resolvers without field guards, a GraphQL harness that omits `invokeGraph`, `invokeEntities` or `invokeConnection`, a transport that claims every context type, runs no metadata canary, claims classes without handlers or drops the `Host` header without declaring hostless calls, a planner that caches plans by handler function, ignores `@SkipOriginCheck()` or skips the origin check of safe operations, a kernel that treats Better Auth's boolean origin skip as its own, a harness that boots without the summary `T-coverage-claims` asks for, an HTTP transport whose invocation is not the handler's request (Fastify), a WebSocket transport whose messages on one socket share their invocation, a GraphQL transport that treats a completed request as live, ignores `connectionParams` credentials, treats socket operations as safe reads, exempts `connectionParams` tokens, serves inherited forwarding fields without field guards, omits `W_FIELD_RESOLVER_INHERITS` or `I_CLASS_METADATA_OPERATIONS_ONLY`, a planner that applies class-level acceptance to field resolvers or records a reading on a form-mode nested public field, a source throwing its denial, a source dropping its refresh cookie, a source turning a storage outage into a 401, API-key sources that report every failure as an invalid key, verify once per call, run without the outage probe or latency signal, probe on every invalid key, touch key rows, or map organization keys to users, a session source that trusts hook answers on authoritative routes, a source that wraps errors without the request's credentials, JWT sources that expect the basePath URL as issuer or deny a key-set outage, sources that drop the host or forwarded headers, a route planner that forwards every handler's direct calls, a `forwardForeignCookies()` without a declaration check or without forwarding, a policy widening delegated principals, a policy turning a lost session or a storage outage into a denial, an organization policy coercing non-string inputs (also over `ws`) or repeating its Better Auth call, admin policies that admit API keys by default, lack fresh identity (also over RPC) or hostless calls, ignore a key owner's ban, judge the session's role or trust the stored user row (also on PGlite), a request scope that reuses a connection's principal for authoritative handlers (Socket.IO, graphql-ws), a WebSocket transport that declares a principal TTL it does not apply, a graphql-ws delivery that opens a new socket for each operation, and boot validation or route planning that ignore request-scoped dependencies or a class policy's `requires` field.
1014
1014
 
1015
1015
  ## Develop in this monorepo
1016
1016
 
@@ -357,6 +357,7 @@ function createProbeState() {
357
357
  storageDelay: void 0,
358
358
  respond: void 0,
359
359
  delay: void 0,
360
+ responseHeader: void 0,
360
361
  streamCancelled: false
361
362
  };
362
363
  }
@@ -543,6 +544,8 @@ function probePlugin(options = {}) {
543
544
  path: ctx.path,
544
545
  skip: await (0, better_auth_api.getShouldSkipSessionRefresh)() === true
545
546
  });
547
+ const header = state.responseHeader?.(ctx.path);
548
+ if (header) ctx.setHeader(header[0], header[1]);
546
549
  const setCookies = ctx.context.responseHeaders?.getSetCookie() ?? [];
547
550
  if (setCookies.length) state.produced.push({
548
551
  path: ctx.path,
@@ -593,16 +596,9 @@ async function probeOf(auth) {
593
596
  * (advanced.disableOriginCheck: false, which vitest's TEST=true would otherwise switch off, and
594
597
  * advanced.disableCSRFCheck: false, which would otherwise switch every origin check off), session.updateAge 0 so
595
598
  * every session read refreshes, testUtils(), bearer(), conformanceProbePlugin() and nestjs() last. The kit settings
596
- * win over the same options in `options`.
599
+ * win over the same options in `options`; `settings` switches the origin checks off explicitly.
597
600
  */
598
- function createConformanceAuth(options = {}) {
599
- return conformanceAuth(options);
600
- }
601
- /**
602
- * Internal: createConformanceAuth() with the kits' own opt-out rows. Only a kit switches Better Auth's
603
- * advanced.disableCSRFCheck on, for the case that proves the kernel honors it; a caller's option never does.
604
- */
605
- function conformanceAuth(options, kit = {}) {
601
+ function createConformanceAuth(options = {}, settings = {}) {
606
602
  const tables = {};
607
603
  const resolved = {
608
604
  secret: globalThis.crypto.randomUUID() + globalThis.crypto.randomUUID(),
@@ -616,8 +612,8 @@ function conformanceAuth(options, kit = {}) {
616
612
  },
617
613
  advanced: {
618
614
  ...options.advanced,
619
- disableOriginCheck: false,
620
- disableCSRFCheck: kit.disableCSRFCheck === true
615
+ disableOriginCheck: settings.disableOriginCheck === true,
616
+ disableCSRFCheck: settings.disableCSRFCheck === true
621
617
  },
622
618
  plugins: [
623
619
  (0, better_auth_plugins.testUtils)(),
@@ -3793,6 +3789,9 @@ function bridgeController(kinds) {
3793
3789
  constructor(service) {
3794
3790
  this.service = service;
3795
3791
  }
3792
+ principal() {
3793
+ return { ok: true };
3794
+ }
3796
3795
  signUp() {
3797
3796
  return this.signUpAnother();
3798
3797
  }
@@ -3830,6 +3829,12 @@ function bridgeController(kinds) {
3830
3829
  return this.service.api.getSession({ headers: new Headers({ authorization: `Bearer ${token}` }) });
3831
3830
  }
3832
3831
  };
3832
+ require_decorate.__decorate([
3833
+ (0, _nestjs_common.Post)("principal"),
3834
+ require_decorate.__decorateMetadata("design:type", Function),
3835
+ require_decorate.__decorateMetadata("design:paramtypes", []),
3836
+ require_decorate.__decorateMetadata("design:returntype", void 0)
3837
+ ], PrincipalBridgeController.prototype, "principal", null);
3833
3838
  require_decorate.__decorate([
3834
3839
  (0, _nestjs_common.Post)("sign-up"),
3835
3840
  require_decorate.__decorateMetadata("design:type", Function),
@@ -3975,12 +3980,11 @@ function principalSourceConformance(options) {
3975
3980
  add("S-infra-throws", "a storage outage makes resolve throw instead of denying", async (harness) => {
3976
3981
  const { resolve, probe } = harness;
3977
3982
  const headers = await valid(harness);
3978
- const calls = probe.calls.length;
3979
3983
  const storage = probe.storage.length;
3980
3984
  probe.storageFault = () => /* @__PURE__ */ new Error("conformance storage outage");
3981
3985
  const result = await settle(() => resolve(headers, new RecordingSink()));
3982
3986
  probe.storageFault = void 0;
3983
- if (probe.calls.length === calls && probe.storage.length === storage) return conformanceSkip("the source read no storage and called no Better Auth endpoint for a valid credential");
3987
+ if (probe.storage.length === storage) return conformanceSkip("the source ran no storage operation for a valid credential");
3984
3988
  node_assert_strict.default.equal(result.ok, false, `a storage outage resolved ${JSON.stringify(result.ok && result.value)}`);
3985
3989
  node_assert_strict.default.ok(!require_auth_errors.isAuthFailure(!result.ok && result.error), `a storage outage was thrown as a denial: ${String(!result.ok && result.error)}`);
3986
3990
  }),
@@ -4099,6 +4103,7 @@ function principalSourceConformance(options) {
4099
4103
  if (decided?.outcome === "rejected") node_assert_strict.default.equal(decided.failure.status, 401);
4100
4104
  }),
4101
4105
  ...bridgeCases(options),
4106
+ ...httpRouteCases(options),
4102
4107
  ...logRedactionCases(options),
4103
4108
  ...jwtCases(options)
4104
4109
  ];
@@ -4367,6 +4372,47 @@ function bridgeCases(options) {
4367
4372
  node_assert_strict.default.deepEqual(linesWithToken(undeclared, foreign.token), []);
4368
4373
  }), skip)];
4369
4374
  }
4375
+ /** A response header the probe sets on the source's own calls; it must never reach the client (S-cookie-forwarded). */
4376
+ const CALL_HEADER = "x-conformance-call-header";
4377
+ /** S-cookie-forwarded and S-infra-throws through a guarded route of the http harness. */
4378
+ function httpRouteCases(options) {
4379
+ const skip = options.http ? void 0 : "the options give no http harness";
4380
+ const withRoute = async (fn) => {
4381
+ const inspected = await harness(options);
4382
+ const { source, probe } = inspected;
4383
+ await inspected.close();
4384
+ const boot = await bootBridge(options, options.http, source.kinds);
4385
+ try {
4386
+ return await fn(boot, source, probe);
4387
+ } finally {
4388
+ probe.storageFault = void 0;
4389
+ probe.responseHeader = void 0;
4390
+ await boot.close();
4391
+ }
4392
+ };
4393
+ return [conformanceCase("S-infra-throws", "through an HTTP route: a storage outage answers 5xx, never 401 or 403", () => withRoute(async (boot, _source, probe) => {
4394
+ const headers = await options.credentials.valid(options.auth);
4395
+ const storage = probe.storage.length;
4396
+ probe.storageFault = () => /* @__PURE__ */ new Error("conformance storage outage");
4397
+ const response = await boot.post("principal", headers);
4398
+ probe.storageFault = void 0;
4399
+ if (probe.storage.length === storage) return conformanceSkip("the source ran no storage operation for a valid credential");
4400
+ node_assert_strict.default.ok(response.status >= 500 && response.status < 600, `a guarded route answered ${response.status} for a storage outage instead of 5xx: ${response.body.toString("utf8")}`);
4401
+ }), skip), conformanceCase("S-cookie-forwarded", "through an HTTP route: each Set-Cookie line of the source's own-credential calls reaches the client exactly once, and no other header of those calls does", () => withRoute(async (boot, source, probe) => {
4402
+ const headers = await options.credentials.valid(options.auth);
4403
+ const from = probe.produced.length;
4404
+ const calls = probe.calls.length;
4405
+ probe.responseHeader = () => [CALL_HEADER, "leaked"];
4406
+ const response = await boot.post("principal", headers);
4407
+ probe.responseHeader = void 0;
4408
+ node_assert_strict.default.equal(response.status, 201, `the guarded route answered ${response.status}: ${response.body.toString("utf8")}`);
4409
+ const produced = ownCookies(probe, from, headers, source.credentialHeaders ?? []);
4410
+ const delivered = setCookieLines(response.headers);
4411
+ for (const line of new Set(produced)) node_assert_strict.default.equal(occurrences(delivered, line), 1, `the source's Better Auth calls produced ${names([line])[0]}, the client received it ${occurrences(delivered, line)} times (received: ${names(delivered).join(", ") || "nothing"})`);
4412
+ for (const line of delivered) node_assert_strict.default.ok(produced.includes(line), `the client received ${names([line])[0]}, which no call with the request's own credential produced`);
4413
+ if (probe.calls.length > calls) node_assert_strict.default.equal(response.headers[CALL_HEADER], void 0, "a header other than Set-Cookie that the source's Better Auth calls set reached the client");
4414
+ }), skip)];
4415
+ }
4370
4416
  function jwtCases(options) {
4371
4417
  const NOT_JWT = "the source does not require the jwt plugin";
4372
4418
  const jwtCase = (title, run) => conformanceCase("S-jwt-claims", title, () => withHarness(options, async (harness) => {
@@ -4495,6 +4541,8 @@ function principalHandler(decorators = [], operation = "read") {
4495
4541
  }
4496
4542
  /** Marks each fixture handler with its fixture name, so the kit can map claims and boot reports to fixtures. */
4497
4543
  const FIXTURE_METADATA = "nestjs-slightly-better-auth:conformance-fixture";
4544
+ /** How many FixtureNode items the `items` root returns. */
4545
+ const ITEM_COUNT = 20;
4498
4546
  /** The fixture marker of a graph handler, for claims and plans of GraphQL boots. */
4499
4547
  const graphMarker = (name) => `graph.${name}`;
4500
4548
  /**
@@ -4579,7 +4627,8 @@ function kitFixtures(extra = {}, everyHandler = [], graph = {}) {
4579
4627
  mixed: marked("mixed", handler([require_auth_decorators.AcceptPrincipals(SESSION_KIND, API_KEY_KIND)], [require_auth_decorators.CurrentPrincipal()], "read", ([p]) => ({
4580
4628
  id: "mixed",
4581
4629
  principal: view(p)
4582
- })))
4630
+ }))),
4631
+ ...graph.readerErrors ? { items: marked("items", handler([require_auth_decorators.AcceptPrincipals(SESSION_KIND, API_KEY_KIND)], [], "read", () => Array.from({ length: ITEM_COUNT }, (_, index) => ({ id: `item-${index}` })))) } : {}
4583
4632
  },
4584
4633
  fields: {
4585
4634
  plain: marked("plain", handler([], [], "read", () => "plain")),
@@ -4593,6 +4642,10 @@ function kitFixtures(extra = {}, everyHandler = [], graph = {}) {
4593
4642
  nestedReader: marked("nestedReader", reader())
4594
4643
  } : {},
4595
4644
  sessionReader: marked("sessionReader", handler([], [require_auth_module.CurrentSession()], "read", ([session]) => session?.user?.id ?? null)),
4645
+ ...graph.readerErrors ? {
4646
+ organizationReader: marked("organizationReader", handler([], [require_organization.ActiveOrganizationId()], "read", ([organization]) => typeof organization === "string" ? organization : null)),
4647
+ serviceSession: marked("serviceSession", handler([], [], "read", async (_params, _input, { service }) => (await service.getSession())?.user?.id ?? null))
4648
+ } : {},
4596
4649
  ...forwarding ? { forwarding: marked("forwarding", handler([require_auth_decorators.ForwardAuthCookies()], [], "read", async (_params, _input, { service }) => {
4597
4650
  await signIn(service, forwarding);
4598
4651
  return "forwarded";
@@ -4697,12 +4750,12 @@ async function seedOrganizations(auth, userId) {
4697
4750
  });
4698
4751
  }
4699
4752
  }
4700
- async function environment(authOptions = {}, connectionLeg = false) {
4753
+ async function environment(authOptions = {}, connectionLeg = false, settings = {}) {
4701
4754
  const plugins = authOptions.plugins ?? [];
4702
- const auth = conformanceAuth({
4755
+ const auth = createConformanceAuth({
4703
4756
  ...authOptions,
4704
4757
  plugins: plugins.some((plugin) => plugin.id === "organization") ? plugins : [...plugins, (0, better_auth_plugins.organization)()]
4705
- }, { disableCSRFCheck: authOptions.advanced?.disableCSRFCheck === true });
4758
+ }, settings);
4706
4759
  const probe = await probeOf(auth);
4707
4760
  const identity = await kitIdentity(auth);
4708
4761
  await seedOrganizations(auth, identity.userId);
@@ -4748,7 +4801,8 @@ async function withApp(options, env, fn, boot = {}, logger = new CapturingLogger
4748
4801
  ...boot.appEnhancers ? { appEnhancers: true } : {},
4749
4802
  ...boot.forwardDirectCalls ? { forwardDirectCalls: true } : {},
4750
4803
  ...boot.fieldResolverCoverage ? { fieldResolverCoverage: boot.fieldResolverCoverage } : {},
4751
- ...boot.graphqlContext ? { graphqlContext: boot.graphqlContext } : {}
4804
+ ...boot.graphqlContext ? { graphqlContext: boot.graphqlContext } : {},
4805
+ ...boot.logSummary ? { logSummary: true } : {}
4752
4806
  });
4753
4807
  app.useLogger(logger);
4754
4808
  try {
@@ -4772,6 +4826,12 @@ async function expectBootFailure(options, env, boot, code) {
4772
4826
  const codes = bootIssueCodes(error);
4773
4827
  node_assert_strict.default.ok(expected.some((value) => codes.includes(value) || String(error).includes(value)), `expected ${expected.join(" or ")}, got ${codes.join(", ")}: ${String(error)}`);
4774
4828
  }
4829
+ /** The default instance's boot summary line (BetterAuthModule's logSummary). */
4830
+ function summaryLine(logger) {
4831
+ const line = logger.entries.find((entry) => entry.level === "log" && entry.text.startsWith("'default': ") && entry.text.includes("; scope: "))?.text;
4832
+ node_assert_strict.default.ok(line, `boot logged no summary (pass the logSummary and logger options to the app): ${logger.text().slice(0, 500)}`);
4833
+ return line;
4834
+ }
4775
4835
  /** Stable codes of the WARN entries a logger captured (`CODE: message`). */
4776
4836
  function warningCodes(logger) {
4777
4837
  return new Set(logger.entries.filter((entry) => entry.level === "warn").map((entry) => /^([A-Z][A-Z0-9_]+):/.exec(entry.text)?.[1]).filter((code) => code !== void 0));
@@ -4933,7 +4993,7 @@ function transportConformance(options) {
4933
4993
  legShape ??= connectionShaped(options);
4934
4994
  return legShape;
4935
4995
  };
4936
- const kitEnvironment = async (authOptions) => environment(authOptions, await connectionLeg());
4996
+ const kitEnvironment = async (authOptions, settings) => environment(authOptions, await connectionLeg(), settings);
4937
4997
  const addRun = (id, title, run, skip) => cases.push(conformanceCase(id, title, run, skip));
4938
4998
  const add = (id, title, run, skip) => addRun(id, title, async () => run(await kitEnvironment()), skip);
4939
4999
  const noBrowser = options.expectBrowserLeg ? void 0 : "the transport declares no browser leg";
@@ -5088,7 +5148,7 @@ function transportConformance(options) {
5088
5148
  node_assert_strict.default.ok(!env.probe.writes.slice(writes).includes("session.update"), "the session row was refreshed");
5089
5149
  node_assert_strict.default.ok(env.probe.refresh.filter((entry) => entry.path === "/get-session").slice(-1).every((entry) => entry.skip), "getShouldSkipSessionRefresh() was not true inside the scope");
5090
5150
  }), options.expectCookieCapable ? "the transport delivers Set-Cookie (see T-cookie-forwarded)" : void 0);
5091
- add("T-csrf-http-unsafe", "an unsafe cookie operation needs a trusted origin, checked before any session read, unless an explicit opt-out applies", async (env) => {
5151
+ add("T-csrf-http-unsafe", "an unsafe cookie operation needs a trusted origin, checked before any session read, unless an explicit opt-out applies; Better Auth's boolean origin skip is not one", async (env) => {
5092
5152
  const skip = await operationLeg();
5093
5153
  if (skip) return skip;
5094
5154
  await withApp(options, env, async ({ app }) => {
@@ -5123,8 +5183,26 @@ function transportConformance(options) {
5123
5183
  node_assert_strict.default.equal(principalOf(result)?.userId, kit.identity.userId, label);
5124
5184
  };
5125
5185
  await withApp(options, env, ({ app }) => untrusted(app, env, "@SkipOriginCheck()"), { fixtures: kitFixtures({ unsafe: [require_auth_decorators.SkipOriginCheck()] }) });
5126
- const disabled = await kitEnvironment({ advanced: { disableCSRFCheck: true } });
5186
+ const disabled = await kitEnvironment({}, { disableCSRFCheck: true });
5127
5187
  await withApp(options, disabled, ({ app }) => untrusted(app, disabled, "advanced.disableCSRFCheck"));
5188
+ const sdkSkip = await kitEnvironment({}, {
5189
+ disableOriginCheck: true,
5190
+ disableCSRFCheck: false
5191
+ });
5192
+ const skipLogger = new CapturingLogger();
5193
+ await withApp(options, sdkSkip, async ({ app }) => {
5194
+ const from = sdkSkip.probe.calls.length;
5195
+ denied(await options.invoke(app, "unsafe", {
5196
+ ...cookie(sdkSkip),
5197
+ origin: UNTRUSTED_ORIGIN
5198
+ }), 403, "INVALID_ORIGIN");
5199
+ node_assert_strict.default.equal(sessionReads(sdkSkip.probe, from), 0, "disableOriginCheck: true with disableCSRFCheck: false: the session was read");
5200
+ succeeded(await options.invoke(app, "unsafe", {
5201
+ ...cookie(sdkSkip),
5202
+ origin: KIT_BASE_URL
5203
+ }), "disableOriginCheck: true with disableCSRFCheck: false: the trusted base URL");
5204
+ }, {}, skipLogger);
5205
+ node_assert_strict.default.ok(skipLogger.entries.some((entry) => entry.level === "warn" && entry.text.startsWith("W_ORIGIN_CHECK:") && entry.text.includes("app-route checks remain on")), `boot did not warn W_ORIGIN_CHECK that app-route checks remain on (pass the logger option to the app): ${skipLogger.text().slice(0, 500)}`);
5128
5206
  }, noBrowser);
5129
5207
  add("T-csrf-http-cookie-plus-token", "a cookie plus a bearer token from an untrusted origin is still denied", async (env) => await operationLeg() ?? withApp(options, env, async ({ app }) => {
5130
5208
  for (const token of ["a.b", env.identity.token]) denied(await options.invoke(app, "unsafe", {
@@ -5540,6 +5618,63 @@ function transportConformance(options) {
5540
5618
  node_assert_strict.default.equal(logger.errors().length - errors, 1, `${shape}: ${logger.errors().length - errors} ERROR entries for one request`);
5541
5619
  }
5542
5620
  }), twiceSkip && "the transport has no lineage, so each error is logged once by construction");
5621
+ add("T-internal-error-logged-once", "reader configuration errors under 20 list items log one ERROR entry per request and distinct error, with field interceptors and with lineage alone", async (env) => {
5622
+ const skip = await graphReady(env);
5623
+ if (skip) return skip;
5624
+ const apiKey = { "x-api-key": env.keys.valid };
5625
+ /** One `items` operation: asserts 20 failed `field` values and returns the ERROR entries it logged. */
5626
+ const items = async (app, logger, fields, headers, label) => {
5627
+ const from = logger.errors().length;
5628
+ const result = await options.invokeGraph(app, [{
5629
+ root: "items",
5630
+ fields
5631
+ }], headers);
5632
+ const list = result.data?.items;
5633
+ node_assert_strict.default.equal(list?.length, ITEM_COUNT, `${label}: the items root did not return ${ITEM_COUNT} items: ${JSON.stringify(result).slice(0, 500)}`);
5634
+ for (const field of fields) {
5635
+ const failed = result.errors.filter((error) => error.path.at(-1) === field);
5636
+ node_assert_strict.default.equal(failed.length, ITEM_COUNT, `${label}: ${failed.length} ${field} errors instead of one per item: ${JSON.stringify(result.errors).slice(0, 500)}`);
5637
+ node_assert_strict.default.ok(list.every((item) => item[field] === null), `${label}: a ${field} value resolved`);
5638
+ for (const error of failed) node_assert_strict.default.ok(!/[A-Z]+_[A-Z_]+/.test(error.message ?? ""), `${label}: the client saw the configuration detail ${error.message}`);
5639
+ }
5640
+ node_assert_strict.default.equal(result.errors.length, ITEM_COUNT * fields.length, `${label}: ${JSON.stringify(result.errors).slice(0, 500)}`);
5641
+ return logger.errors().slice(from).map((entry) => entry.text);
5642
+ };
5643
+ const once = (entries, code, label) => node_assert_strict.default.ok(entries.length === 1 && entries[0].includes(code), `${label}: expected one ERROR entry with ${code}, got ${entries.length}: ${entries.join(" | ").slice(0, 500)}`);
5644
+ for (const enhancers of [[
5645
+ "guards",
5646
+ "interceptors",
5647
+ "filters"
5648
+ ], ["guards", "filters"]]) {
5649
+ const scoped = enhancers.includes("interceptors");
5650
+ const boot = `[${enhancers.join(", ")}]`;
5651
+ await withApp(options, env, async ({ app, logger }) => {
5652
+ for (const round of [1, 2]) {
5653
+ const label = `${boot}, request ${round}`;
5654
+ once(await items(app, logger, ["sessionReader"], apiKey, `${label}: an API key's session readers`), "SESSION_REQUIRED", `${label}: an API key's session readers`);
5655
+ once(await items(app, logger, ["organizationReader"], cookie(env), `${label}: organization readers without a providing requirement`), "NO_INVOCATION_VALUE", `${label}: organization readers without a providing requirement`);
5656
+ const service = await items(app, logger, ["serviceSession"], apiKey, `${label}: service getSession() for an API key`);
5657
+ const code = scoped ? "SESSION_REQUIRED" : "NO_AUTH_SCOPE";
5658
+ node_assert_strict.default.equal(service.length, scoped ? 1 : ITEM_COUNT, `${label}: service getSession() logged ${service.length} ERROR entries`);
5659
+ node_assert_strict.default.ok(service.every((entry) => entry.includes(code)), `${label}: service getSession() did not log ${code}: ${service.join(" | ").slice(0, 500)}`);
5660
+ }
5661
+ const distinct = await items(app, logger, ["sessionReader", "organizationReader"], apiKey, `${boot}: session and organization readers together`);
5662
+ node_assert_strict.default.equal(distinct.length, 2, `${boot}: two reasons logged ${distinct.length} ERROR entries: ${distinct.join(" | ").slice(0, 500)}`);
5663
+ for (const code of ["SESSION_REQUIRED", "NO_INVOCATION_VALUE"]) node_assert_strict.default.ok(distinct.some((entry) => entry.includes(code)), `${boot}: ${code} was suppressed by another reason`);
5664
+ }, {
5665
+ fieldResolverEnhancers: enhancers,
5666
+ fixtures: kitFixtures({}, [], { readerErrors: true })
5667
+ });
5668
+ if (!scoped) continue;
5669
+ await withApp(options, env, async ({ app, logger }) => {
5670
+ once(await items(app, logger, ["reader"], cookie(env), `${boot}: readers without an authentication result`), "NO_AUTH_RESULT", `${boot}: readers without an authentication result`);
5671
+ }, {
5672
+ fieldResolverEnhancers: enhancers,
5673
+ fixtures: kitFixtures({}, [], { readerErrors: true }),
5674
+ override: (builder) => require_testing.overrideAuthGuard(builder, { canActivate: () => true })
5675
+ });
5676
+ }
5677
+ }, graphSkip);
5543
5678
  add("T-public-no-lookup", "public handlers never read the session", (env) => withApp(options, env, async ({ app }) => {
5544
5679
  const from = env.probe.calls.length;
5545
5680
  succeeded(await options.invoke(app, "public", cookie(env)));
@@ -5603,8 +5738,9 @@ function transportConformance(options) {
5603
5738
  const plan = planner.plan(claim.target, claim.method);
5604
5739
  if (!((plan.access === "public" || plan.access === "inherit") && plan.originCheck !== "form") && (claim.options.everyHandler || plan.declares)) expected.set(claim.fixture, claim.options.code);
5605
5740
  }
5606
- }, {}, defaults);
5741
+ }, { logSummary: true }, defaults);
5607
5742
  node_assert_strict.default.ok(claims.some((claim) => claim.fixture), "the transport claimed none of the kit's fixture handlers");
5743
+ node_assert_strict.default.match(summaryLine(defaults), /; scope: global;/, "the default boot's summary does not report the global scope interceptor");
5608
5744
  const bare = await settle(() => withApp(options, env, async () => void 0, { globalGuard: false }));
5609
5745
  node_assert_strict.default.equal(bare.ok, expected.size === 0, bare.ok ? `the bare fixtures booted, but the claims call for reports of ${JSON.stringify([...expected])}` : `no claim calls for a coverage report, but boot failed: ${String(bare.error)}`);
5610
5746
  const issues = !bare.ok && bare.error?.issues || [];
@@ -5641,9 +5777,13 @@ function transportConformance(options) {
5641
5777
  await withApp(options, env, async () => void 0, { override: (builder) => builder.overrideProvider(require_auth_guard.BetterAuthGuard).useClass(StandInGuard) });
5642
5778
  if (global) {
5643
5779
  const unscoped = new CapturingLogger();
5644
- await withApp(options, env, async () => void 0, { globalScope: false }, unscoped);
5780
+ await withApp(options, env, async () => void 0, {
5781
+ globalScope: false,
5782
+ logSummary: true
5783
+ }, unscoped);
5645
5784
  const added = [...warningCodes(unscoped)].filter((code) => !warningCodes(defaults).has(code));
5646
5785
  node_assert_strict.default.deepEqual(added, ["W_NO_GLOBAL_SCOPE"], `globalScope: false must boot global claims with W_NO_GLOBAL_SCOPE only (pass the logger option to the app): ${unscoped.text().slice(0, 500)}`);
5786
+ node_assert_strict.default.match(summaryLine(unscoped), /; scope: off — see W_NO_GLOBAL_SCOPE;/, "globalScope: false: the boot summary does not report scope: off");
5647
5787
  }
5648
5788
  });
5649
5789
  add("T-metadata-canary", "validate() fails NEST_METADATA_KEY_CHANGED when Nest writes its metadata under other keys, and claims no class without handlers", (env) => withApp(options, env, async ({ app }) => {