@aventara/nest 0.1.0-pilot.1 → 0.1.0-pilot.2

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
@@ -126,6 +126,21 @@ own when it mounts them, through Nest's `Logger` (context `AventaraModule`; `log
126
126
  [Nest] LOG [AventaraModule] Aventara mounted 10 operations at /api (+ GET /api/_contract, POST /api/_transactions)
127
127
  ```
128
128
 
129
+ An internal failure (`A3000`–`A3004`, answered as a 500) is logged as one error line through Nest's `Logger`
130
+ (context `Aventara`): the code, the operation, the scope and the request id, and the thrown value's class name — never
131
+ its message, which can hold SQL or a connection string. The raw error still goes to your `diagnostics` sink, when you
132
+ configure one:
133
+
134
+ ```text
135
+ [Nest] ERROR [Aventara] A3001 on Post.create.one (client scope, request 3f1c…): PrismaClientValidationError. The raw error goes to the framework's diagnostics sink.
136
+ ```
137
+
138
+ For `A3004` — one of your pipes returned arguments the server contract refuses — the line also names the pipe, the
139
+ operation, the offending field and its V-code; the caller only ever sees the generic message.
140
+
141
+ The line is added when the module builds the framework from `config`; a framework you build yourself and pass as
142
+ `framework` keeps exactly the `diagnostics` sink you gave it.
143
+
129
144
  The framework's validation is outermost:
130
145
 
131
146
  | | Express | Fastify |
@@ -2,17 +2,16 @@ import { type AdapterModel, type AnyFrameworkConfig, type Framework, type Framew
2
2
  import { type DynamicModule, type FactoryProvider, type ModuleMetadata, type NestModule, type OnModuleInit } from "@nestjs/common";
3
3
  import { type PendingProtocolHost } from "./protocol-host.factory.js";
4
4
  /**
5
- * How the application's Framework enters Nest (Q1, ruled): either the
6
- * developer's own instance — they called `createFramework` — or the
7
- * configuration the module hands to `createFramework` itself. Exactly one arm;
8
- * the `?: never` halves make both a type error.
5
+ * How the application's Framework enters Nest: either the developer's own instance
6
+ * — they called `createFramework` — or the configuration the module hands to
7
+ * `createFramework` itself. Exactly one arm; the `?: never` halves make both a
8
+ * type error.
9
9
  *
10
- * The `config` arm is typed exactly as `createFramework`'s parameter
11
- * (`Cfg & FrameworkConfig<M, Cfg>`; U12, architect 2026-10-05), so a
10
+ * The `config` arm is typed exactly as `createFramework`'s parameter, so a
12
11
  * configuration annotated `FrameworkConfig<M>`, written inline, inferred or
13
- * checked with `satisfies` is accepted — and checked against its own Adapter
14
- * model — wherever `createFramework` would accept it. The defaults are the
15
- * untyped forms.
12
+ * checked with `satisfies` is accepted — and checked against its own Adapter model
13
+ * — wherever `createFramework` would accept it. The defaults are the untyped
14
+ * forms.
16
15
  */
17
16
  export type AventaraModuleOptions<M extends AdapterModel = AdapterModel, Cfg = AnyFrameworkConfig> = {
18
17
  readonly framework: Framework;
@@ -22,9 +21,9 @@ export type AventaraModuleOptions<M extends AdapterModel = AdapterModel, Cfg = A
22
21
  readonly framework?: never;
23
22
  };
24
23
  /**
25
- * `forRootAsync`'s options: the Nest idiom when what builds the Framework
26
- * (a Prisma client, a configuration service) is itself a provider. The
27
- * factory resolves to either arm of {@link AventaraModuleOptions} (Q1).
24
+ * `forRootAsync`'s options: the Nest idiom when what builds the Framework (a
25
+ * Prisma client, a configuration service) is itself a provider. The factory
26
+ * resolves to either arm of {@link AventaraModuleOptions}.
28
27
  */
29
28
  export type AventaraModuleAsyncOptions<M extends AdapterModel = AdapterModel, Cfg = AnyFrameworkConfig> = {
30
29
  readonly imports?: ModuleMetadata["imports"];
@@ -32,16 +31,16 @@ export type AventaraModuleAsyncOptions<M extends AdapterModel = AdapterModel, Cf
32
31
  readonly useFactory: (...deps: never[]) => AventaraModuleOptions<M, Cfg> | Promise<AventaraModuleOptions<M, Cfg>>;
33
32
  };
34
33
  /**
35
- * The global module that hosts one Aventara Framework (Q2, Q3), and mounts its
36
- * protocol on the application's platform from `configure()` (Q5): after the
37
- * developer's `app.use` / `enableCors`, before `MiddlewareConsumer` middleware
38
- * and every controller, so the reserved `_` namespace cannot be shadowed.
34
+ * The global module that hosts one Aventara Framework, and mounts its protocol on
35
+ * the application's platform from `configure()`: after the developer's `app.use` /
36
+ * `enableCors`, before `MiddlewareConsumer` middleware and every controller, so
37
+ * the reserved `_` namespace cannot be shadowed.
39
38
  */
40
39
  export declare class AventaraModule implements NestModule, OnModuleInit {
41
40
  private readonly host;
42
41
  constructor(host: PendingProtocolHost);
43
42
  configure(): void;
44
- /** An application context never receives a platform: refused here, at its init (N1). */
43
+ /** An application context never receives a platform: refused here, at its init. */
45
44
  onModuleInit(): void;
46
45
  static forRoot<M extends AdapterModel, const Cfg = AnyFrameworkConfig>(options: AventaraModuleOptions<M, Cfg>): DynamicModule;
47
46
  static forRootAsync<M extends AdapterModel, const Cfg = AnyFrameworkConfig>(options: AventaraModuleAsyncOptions<M, Cfg>): DynamicModule;
@@ -3,32 +3,21 @@ import { Inject, Module, } from "@nestjs/common";
3
3
  import { HttpAdapterHost } from "@nestjs/core";
4
4
  import { AVENTARA_FRAMEWORK } from "./framework.token.js";
5
5
  import { checkedOptions, recordMount } from "./host-bootstrap.validator.js";
6
+ import { withInternalFailureLog } from "./internal-failure.logger.js";
6
7
  import { createPendingProtocolHost, } from "./protocol-host.factory.js";
7
- /**
8
- * One value per `forRoot`/`forRootAsync` call, carried in the module's
9
- * metadata: under Nest's opt-in `deep-hash` module ids, two calls whose
10
- * metadata serialized alike would collapse into one module and the second
11
- * mount would vanish instead of being refused (Q18).
12
- */
13
8
  const MOUNT = Symbol("AVENTARA_MOUNT");
14
9
  let mounts = 0;
15
- /** The bound protocol and its mounter for this application's platform. */
16
10
  const PROTOCOL_HOST = Symbol("AVENTARA_PROTOCOL_HOST");
17
- /**
18
- * The Framework the options name: the developer's instance, or the one
19
- * `createFramework` builds from the configuration — whose
20
- * `FrameworkConstructionError` propagates unchanged (Q1).
21
- */
22
11
  async function frameworkOf(options) {
23
12
  const checked = checkedOptions(options);
24
- return checked.framework ?? createFramework(checked.config);
13
+ if (checked.framework !== undefined) {
14
+ return checked.framework;
15
+ }
16
+ return createFramework({
17
+ ...checked.config,
18
+ diagnostics: withInternalFailureLog(checked.config.diagnostics),
19
+ });
25
20
  }
26
- /**
27
- * The global module that hosts one Aventara Framework (Q2, Q3), and mounts its
28
- * protocol on the application's platform from `configure()` (Q5): after the
29
- * developer's `app.use` / `enableCors`, before `MiddlewareConsumer` middleware
30
- * and every controller, so the reserved `_` namespace cannot be shadowed.
31
- */
32
21
  export class AventaraModule {
33
22
  host;
34
23
  constructor(host) {
@@ -37,7 +26,6 @@ export class AventaraModule {
37
26
  configure() {
38
27
  this.host.mount();
39
28
  }
40
- /** An application context never receives a platform: refused here, at its init (N1). */
41
29
  onModuleInit() {
42
30
  this.host.assertReady();
43
31
  }
@@ -2,33 +2,31 @@ import type { AvProtocolBinding } from "@aventara/core/protocol";
2
2
  import { type NodeIncoming, type NodeOutgoing } from "./node-exchange.translator.js";
3
3
  import type { ProtocolHost } from "./protocol-host.factory.js";
4
4
  /**
5
- * The protocol on Nest's Express platform (Q4, Q5, Q8, Q9, Q11, Q12, P1–P3).
5
+ * The protocol on Nest's Express platform.
6
6
  *
7
- * - **The pre-parser** (Q8), installed while Nest instantiates providers —
8
- * before Nest's own body parsers: a request under `<entrypoint>/_` has its
9
- * body read raw, bounded and decoded, and kept for its route. Its stream is
10
- * then finished, so Nest's parsers skip it; every other request keeps
11
- * Nest's parsed body. It reads; it answers nothing.
12
- * - **The routes**, mounted from the module's `configure()` (Q5): one platform
13
- * route per `protocol.surface()` entry, each at its escaped literal path
14
- * under the entrypoint, then one fallback — `protocol.answerAbsent`, the
15
- * absence handler, not a catch-all: it executes nothing, and a path it does
16
- * not own (`undefined`) goes on to Nest.
7
+ * - **The pre-parser**, installed while Nest instantiates providers — before
8
+ * Nest's own body parsers: a request under `<entrypoint>/_` has its body read
9
+ * raw, bounded and decoded, and kept for its route. Its stream is then finished,
10
+ * so Nest's parsers skip it; every other request keeps Nest's parsed body. It
11
+ * reads; it answers nothing.
12
+ * - **The routes**, mounted from the module's `configure()`: one platform route
13
+ * per `protocol.surface()` entry, each at its escaped literal path under the
14
+ * entrypoint, then one fallback — `protocol.answerAbsent`, the absence handler,
15
+ * not a catch-all: it executes nothing, and a path it does not own (`undefined`)
16
+ * goes on to Nest.
17
17
  *
18
- * Its whole job is translation: the Express request into an
19
- * `AvProtocolRequest`, the `AvProtocolResponse` back onto the wire. Express's
20
- * types are the slices below (P4: no `@types/node`, no `@types/express`).
18
+ * Its whole job is translation: the Express request into an `AvProtocolRequest`,
19
+ * the `AvProtocolResponse` back onto the wire.
21
20
  */
22
21
  /** Express's request: Node's `IncomingMessage`, plus `originalUrl`. */
23
22
  type ExpressRequest = NodeIncoming & {
24
23
  readonly method: string;
25
24
  readonly originalUrl: string;
26
25
  };
27
- /** Express's response, written through Node's own `writeHead` (P1: never `send`/`json`). */
26
+ /** Express's response, written through Node's own `writeHead`. */
28
27
  type ExpressResponse = NodeOutgoing;
29
28
  /** Express passes `next`; Nest's handler type declares it as an optional `Function`. */
30
29
  type ExpressHandler = (request: ExpressRequest, response: ExpressResponse, next?: unknown) => unknown;
31
- /** The slice of Nest's `ExpressAdapter` this mounter registers on. */
32
30
  export type ExpressPlatform = {
33
31
  get(path: string, handler: ExpressHandler): unknown;
34
32
  post(path: string, handler: ExpressHandler): unknown;
@@ -1,14 +1,8 @@
1
1
  import { NO_BYTES, pathnameOf, protocolRequest, writeAnswer, } from "./node-exchange.translator.js";
2
2
  import { PROTOCOL_ROUTES } from "./protocol-route.table.js";
3
3
  import { readRequestBody, unreadBodyAnswer, } from "./request-body.reader.js";
4
- /** The platform method that registers a route of each surface method. */
5
4
  const REGISTER = { GET: "get", POST: "post" };
6
- /** A request the pre-parser did not read: zero bytes. */
7
5
  const NOTHING_READ = { kind: "read", content: NO_BYTES };
8
- /**
9
- * Installs the pre-parser on `platform` now, and returns the host whose
10
- * `mount` registers the routes and the absence fallback under `entrypoint`.
11
- */
12
6
  export function createExpressProtocolHost(platform, binding, entrypoint, maxRequestBytes) {
13
7
  const bodies = new WeakMap();
14
8
  const bodyOf = (request) => bodies.get(request) ?? NOTHING_READ;
@@ -45,21 +39,14 @@ export function createExpressProtocolHost(platform, binding, entrypoint, maxRequ
45
39
  },
46
40
  };
47
41
  }
48
- /** Hands the request on to whatever Express runs next — Nest's own routes. */
49
42
  function handOn(next) {
50
43
  if (typeof next === "function") {
51
44
  next();
52
45
  }
53
46
  }
54
- /**
55
- * The route path as a literal for Express's router (`path-to-regexp` 8):
56
- * every character it reads as syntax is escaped, so a Resource key never
57
- * mounts a parameter or a wildcard (H11, DA-13).
58
- */
59
47
  function literalPath(path) {
60
48
  return path.replace(/[()[\]{}*+?!:\\]/g, "\\$&");
61
49
  }
62
- /** The Express request as the protocol reads it. */
63
50
  function translated(request, entrypoint, content) {
64
51
  return protocolRequest(request.method, request.originalUrl, request, entrypoint, content);
65
52
  }
@@ -2,37 +2,33 @@ import type { AvProtocolBinding } from "@aventara/core/protocol";
2
2
  import { type NodeIncoming, type NodeOutgoing, type PlatformHeaders } from "./node-exchange.translator.js";
3
3
  import type { ProtocolHost } from "./protocol-host.factory.js";
4
4
  /**
5
- * The protocol on Nest's Fastify platform (Q10; Q8, Q9, Q11, Q12, P1–P3 as on
6
- * Express), mounted from the module's `configure()` (Q5). Measured in S4:
5
+ * The protocol on Nest's Fastify platform, mounted from the module's
6
+ * `configure()`.
7
7
  *
8
8
  * - **One Fastify route per `protocol.surface()` entry**, at its literal path
9
- * under the entrypoint, with Fastify's automatic `HEAD` route off (U9), so
10
- * `HEAD /_contract` reaches the protocol's `405` as on Express (P3).
9
+ * under the entrypoint, with Fastify's automatic `HEAD` route off, so `HEAD
10
+ * /_contract` reaches the protocol's `405` as on Express.
11
11
  * - **Each route is answered from its own `onRequest` hook** — Fastify's first
12
- * step, before its content-type check and body parsing: the body is read raw
13
- * off Node's request by the bounded reader, the bound member answers, and the
14
- * reply is hijacked. Fastify's parsers, `bodyLimit` and early `415` (on a
15
- * `Content-Type` it cannot parse) never run on a protocol route, so every
16
- * body failure is the protocol's own answer, as on Express (director,
17
- * 2026-10-05). The route handler is never reached.
18
- * - **The absence fallback** is a root `onRequest` hook acting only on a
19
- * request Fastify matched to no route (`request.is404`) under
20
- * `<entrypoint>/`: `protocol.answerAbsent`, and `undefined` hands the request
21
- * on to Nest's own not-found handler (U8, director, 2026-10-05). A not-found
22
- * handler of our own could not hand back: inside one, Fastify answers its
23
- * plain-text 404, not Nest's.
24
- * - **Literal paths** (architect, 2026-10-05): each route is registered
25
- * `decodeURI`-ed, as find-my-way decodes a request path before matching it
26
- * (`café`, `a[b]`). A path find-my-way cannot match as a literal — a `*`
27
- * (its wildcard) or a reserved escape (it keeps those encoded, and never
28
- * matches them against a static route) — is refused while Nest builds the
29
- * application, naming the Resource keys; Express mounts them all.
30
- * - **Writes** go through Node's response after `reply.hijack()` (P1: no
31
- * charset rewrite, no lower-cased names, no added `Content-Type`), with the
32
- * headers Fastify hooks already set (CORS) beneath the protocol's own —
33
- * what Node's `writeHead` does on Express (director, 2026-10-05).
34
- *
35
- * Fastify's types are the slices below (P4).
12
+ * step, before its content-type check and body parsing: the body is read raw off
13
+ * Node's request by the bounded reader, the bound member answers, and the reply
14
+ * is hijacked. Fastify's parsers, `bodyLimit` and early `415` (on a
15
+ * `Content-Type` it cannot parse) never run on a protocol route, so every body
16
+ * failure is the protocol's own answer, as on Express. The route handler is
17
+ * never reached.
18
+ * - **The absence fallback** is a root `onRequest` hook acting only on a request
19
+ * Fastify matched to no route (`request.is404`) under `<entrypoint>/`:
20
+ * `protocol.answerAbsent`, and `undefined` hands the request on to Nest's own
21
+ * not-found handler. A not-found handler of our own could not hand back: inside
22
+ * one, Fastify answers its plain-text 404, not Nest's.
23
+ * - **Literal paths**: each route is registered `decodeURI`-ed, as find-my-way
24
+ * decodes a request path before matching it (`café`, `a[b]`). A path find-my-way
25
+ * cannot match as a literal — a `*` (its wildcard) or a reserved escape (it
26
+ * keeps those encoded, and never matches them against a static route) — is
27
+ * refused while Nest builds the application, naming the Resource keys; Express
28
+ * mounts them all.
29
+ * - **Writes** go through Node's response after `reply.hijack()`, with the headers
30
+ * Fastify hooks already set (CORS) beneath the protocol's own — what Node's
31
+ * `writeHead` does on Express.
36
32
  */
37
33
  /** Fastify's request: Node's own under `raw`, and whether a route matched. */
38
34
  type FastifyRequest = {
@@ -49,7 +45,6 @@ type FastifyReply = {
49
45
  readonly raw: NodeOutgoing;
50
46
  };
51
47
  type OnRequestHook = (request: FastifyRequest, reply: FastifyReply) => Promise<FastifyReply | undefined>;
52
- /** The slice of the Fastify instance (`httpAdapter.getInstance()`) this mounter registers on. */
53
48
  export type FastifyPlatform = {
54
49
  route(options: {
55
50
  readonly method: string;
@@ -1,17 +1,7 @@
1
1
  import { NO_BYTES, pathnameOf, protocolRequest, writeAnswer, } from "./node-exchange.translator.js";
2
2
  import { PROTOCOL_ROUTES } from "./protocol-route.table.js";
3
3
  import { readRequestBody, unreadBodyAnswer } from "./request-body.reader.js";
4
- /**
5
- * What find-my-way cannot match in a static route path: its wildcard, and the
6
- * escapes of the characters `decodeURI` leaves encoded (: + # $ & , / ; = ? @).
7
- */
8
4
  const UNMATCHABLE = /\*|%(?:3A|2B|23|24|26|2C|2F|3B|3D|3F|40)/i;
9
- /**
10
- * Refuses an entrypoint find-my-way cannot match literally — a `*`, its
11
- * wildcard; every other character a canonical entrypoint may carry raw
12
- * mounts (measured S6; a `:` is doubled) — then, naming them, the Resource
13
- * keys whose routes it cannot match literally.
14
- */
15
5
  function checkLiteralRoutes(binding, entrypoint) {
16
6
  if (entrypoint.includes("*")) {
17
7
  throw new Error(`AventaraModule cannot mount the entrypoint \`${entrypoint}\` as a literal path on Nest's Fastify platform: it may not carry \`*\`. Change the entrypoint, or host on Express.`);
@@ -26,10 +16,6 @@ function checkLiteralRoutes(binding, entrypoint) {
26
16
  throw new Error(`AventaraModule cannot mount Resource ${refused.map((key) => `\`${key}\``).join(", ")} as literal routes on Nest's Fastify platform: a route path may not carry \`*\` or a reserved character (: + # $ & , / ; = ? @). Rename the Resource, or host on Express.`);
27
17
  }
28
18
  }
29
- /**
30
- * The host whose `mount` registers the routes and the absence hook under
31
- * `entrypoint` — after refusing, now, a surface it cannot mount literally.
32
- */
33
19
  export function createFastifyProtocolHost(platform, binding, entrypoint, maxRequestBytes) {
34
20
  checkLiteralRoutes(binding, entrypoint);
35
21
  const translated = (request, content) => protocolRequest(request.raw.method, request.raw.url, request.raw, entrypoint, content);
@@ -68,15 +54,9 @@ export function createFastifyProtocolHost(platform, binding, entrypoint, maxRequ
68
54
  },
69
55
  };
70
56
  }
71
- /**
72
- * The route path as find-my-way matches it: `decodeURI`-ed, as it decodes a
73
- * request path before the lookup, and every `:` (raw only in the entrypoint —
74
- * the surface encodes it) doubled, find-my-way's escape for a literal colon.
75
- */
76
57
  function literalPath(path) {
77
58
  return decodeURI(path).replaceAll(":", "::");
78
59
  }
79
- /** The protocol's answer on the hijacked reply, the hooks' headers beneath it. */
80
60
  function write(reply, answer) {
81
61
  reply.hijack();
82
62
  writeAnswer(reply.raw, answer, reply.getHeaders());
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * The DI token under which `AventaraModule` provides the application's
3
- * `Framework` (Q2). A symbol, so injection is exact: an abstract-class token
4
- * would type the injected value as an empty class, not the application's own
5
- * `Framework<M, Cfg>` (Q3 — the application types the injection with its
6
- * own alias).
3
+ * `Framework`. A symbol, so injection is exact: an abstract-class token would type
4
+ * the injected value as an empty class, not the application's own `Framework<M,
5
+ * Cfg>`.
7
6
  */
8
7
  export declare const AVENTARA_FRAMEWORK: unique symbol;
9
- /** `@InjectFramework()` — `@Inject(AVENTARA_FRAMEWORK)` in one call (Q2). */
8
+ /** `@InjectFramework()` — `@Inject(AVENTARA_FRAMEWORK)` in one call. */
10
9
  export declare function InjectFramework(): ParameterDecorator;
@@ -1,13 +1,5 @@
1
1
  import { Inject } from "@nestjs/common";
2
- /**
3
- * The DI token under which `AventaraModule` provides the application's
4
- * `Framework` (Q2). A symbol, so injection is exact: an abstract-class token
5
- * would type the injected value as an empty class, not the application's own
6
- * `Framework<M, Cfg>` (Q3 — the application types the injection with its
7
- * own alias).
8
- */
9
2
  export const AVENTARA_FRAMEWORK = Symbol("AVENTARA_FRAMEWORK");
10
- /** `@InjectFramework()` — `@Inject(AVENTARA_FRAMEWORK)` in one call (Q2). */
11
3
  export function InjectFramework() {
12
4
  return Inject(AVENTARA_FRAMEWORK);
13
5
  }
@@ -3,7 +3,7 @@ import type { AventaraModuleOptions } from "./aventara.module.js";
3
3
  export declare function checkedOptions(options: unknown): AventaraModuleOptions;
4
4
  /** Records one mount in `application`; a second is refused. */
5
5
  export declare function recordMount(application: object): void;
6
- /** The Nest HTTP platforms the module mounts the protocol on (Q10). */
6
+ /** The Nest HTTP platforms the module mounts the protocol on. */
7
7
  export type HostPlatform = "express" | "fastify";
8
8
  /** The application's platform, as `httpAdapter.getType()` names it — or a refusal. */
9
9
  export declare function checkedPlatform(type: string | undefined): HostPlatform;
@@ -1,14 +1,7 @@
1
- /**
2
- * Q18's bootstrap checks, all at module setup, so `NestFactory.create`
3
- * rejects before the application listens: plain `Error`s with fixed messages
4
- * (no new public name — Q18 a). A `FrameworkConstructionError` is core's and
5
- * passes through untouched; nothing here wraps it.
6
- */
7
1
  const NEITHER_ARM = "AventaraModule: the options carry neither `framework` nor `config`; pass exactly one.";
8
2
  const BOTH_ARMS = "AventaraModule: the options carry both `framework` and `config`; pass exactly one.";
9
3
  const NOT_A_FRAMEWORK = "AventaraModule: `framework` is not an Aventara Framework (it has no string `entrypoint` and no `contracts.client`); pass what `createFramework` resolved to.";
10
4
  const MOUNTED_TWICE = "AventaraModule is mounted twice in one application; import AventaraModule.forRoot or forRootAsync once.";
11
- /** The options as given — `forRootAsync`'s factory may resolve anything at runtime. */
12
5
  export function checkedOptions(options) {
13
6
  const record = typeof options === "object" && options !== null
14
7
  ? options
@@ -26,7 +19,6 @@ export function checkedOptions(options) {
26
19
  }
27
20
  return record;
28
21
  }
29
- /** What the host reads of a Framework: its entrypoint, and its compiled ClientContract. */
30
22
  function isFramework(value) {
31
23
  if (typeof value !== "object" || value === null) {
32
24
  return false;
@@ -36,13 +28,7 @@ function isFramework(value) {
36
28
  typeof candidate.contracts?.client === "object" &&
37
29
  candidate.contracts.client !== null);
38
30
  }
39
- /**
40
- * Applications that already mount the module, keyed by an object Nest creates
41
- * once per application (its `HttpAdapterHost`). Weak, so a closed
42
- * application's entry goes with it.
43
- */
44
31
  const mounted = new WeakSet();
45
- /** Records one mount in `application`; a second is refused. */
46
32
  export function recordMount(application) {
47
33
  if (mounted.has(application)) {
48
34
  throw new Error(MOUNTED_TWICE);
@@ -50,7 +36,6 @@ export function recordMount(application) {
50
36
  mounted.add(application);
51
37
  }
52
38
  const HOST_PLATFORMS = ["express", "fastify"];
53
- /** The application's platform, as `httpAdapter.getType()` names it — or a refusal. */
54
39
  export function checkedPlatform(type) {
55
40
  const platform = HOST_PLATFORMS.find((supported) => supported === type);
56
41
  if (platform === undefined) {
@@ -0,0 +1,8 @@
1
+ import type { FrameworkDiagnostic, FrameworkDiagnosticsSink } from "@aventara/core";
2
+ /** Wraps the application's sink (if any): log an internal failure, then forward. */
3
+ export declare function withInternalFailureLog(sink: FrameworkDiagnosticsSink | undefined): FrameworkDiagnosticsSink;
4
+ /**
5
+ * e.g. `A3001 on Post.create.one (client scope, request 7f3c…): Error. The raw
6
+ * error goes to the framework's diagnostics sink.`
7
+ */
8
+ export declare function internalFailureLine(diagnostic: FrameworkDiagnostic): string;
@@ -0,0 +1,46 @@
1
+ import { Logger } from "@nestjs/common";
2
+ const INTERNAL_FAILURE_LOGGER = new Logger("Aventara");
3
+ export function withInternalFailureLog(sink) {
4
+ return (diagnostic) => {
5
+ if (diagnostic.code.startsWith("A3")) {
6
+ try {
7
+ INTERNAL_FAILURE_LOGGER.error(internalFailureLine(diagnostic));
8
+ }
9
+ catch {
10
+ }
11
+ }
12
+ return sink?.(diagnostic);
13
+ };
14
+ }
15
+ export function internalFailureLine(diagnostic) {
16
+ const subject = diagnostic.kind === "operation"
17
+ ? `${diagnostic.resource}.${diagnostic.family}.${diagnostic.variant}`
18
+ : "a transaction";
19
+ const step = diagnostic.operation === undefined
20
+ ? ""
21
+ : `, plan operation ${diagnostic.operation}`;
22
+ return `${diagnostic.code} on ${subject} (${diagnostic.scope} scope, request ${diagnostic.requestId}${step}): ${thrownKind(diagnostic.error)}${frameworkDetail(diagnostic.error)}. The raw error goes to the framework's diagnostics sink.`;
23
+ }
24
+ function frameworkDetail(error) {
25
+ try {
26
+ if (error instanceof Error &&
27
+ error.name === "PipeOutputError" &&
28
+ typeof error.message === "string") {
29
+ return ` — ${error.message}`;
30
+ }
31
+ }
32
+ catch {
33
+ }
34
+ return "";
35
+ }
36
+ function thrownKind(error) {
37
+ try {
38
+ const name = error instanceof Error ? error.constructor.name : undefined;
39
+ return typeof name === "string" && /^[A-Za-z_$][\w$]{0,63}$/.test(name)
40
+ ? name
41
+ : "a non-Error value";
42
+ }
43
+ catch {
44
+ return "an unreadable value";
45
+ }
46
+ }
@@ -1,14 +1,7 @@
1
1
  import type { AvProtocolRequest, AvProtocolResponse } from "@aventara/core/protocol";
2
- /**
3
- * The translation both platforms share, because both hand the host Node's own
4
- * request and response (Q10: shared only as measured — S4 ran the corpus on
5
- * Express and Fastify through it). Node's types are the slices below (P4).
6
- */
7
- /** The slice of Node's `IncomingMessage` the host reads. */
8
2
  export type NodeIncoming = AsyncIterable<Uint8Array> & {
9
3
  readonly headersDistinct: Readonly<Record<string, readonly string[]>>;
10
4
  };
11
- /** The slice of Node's `ServerResponse` the host writes (P1: no platform reply path). */
12
5
  export type NodeOutgoing = {
13
6
  writeHead(status: number, headers: Readonly<Record<string, string | number | readonly string[]>>): unknown;
14
7
  end(body: string): unknown;
@@ -17,14 +10,16 @@ export type NodeOutgoing = {
17
10
  export type PlatformHeaders = Readonly<Record<string, string | number | readonly string[] | undefined>>;
18
11
  /** Zero bytes: the body of a request whose body the protocol does not read. */
19
12
  export declare const NO_BYTES: Uint8Array<ArrayBuffer>;
20
- /** A request URL's raw pathname — query dropped (Q12), not URL-decoded (P2). */
13
+ /** A request URL's raw pathname — query dropped, not URL-decoded. */
21
14
  export declare function pathnameOf(url: string): string;
22
- /** The request as the protocol reads it: path relative to the entrypoint, `headersDistinct` (Q11), the raw arm (Q8). */
15
+ /**
16
+ * The request as the protocol reads it: path relative to the entrypoint,
17
+ * `headersDistinct`, the raw arm.
18
+ */
23
19
  export declare function protocolRequest(method: string, url: string, incoming: NodeIncoming, entrypoint: string, content: Uint8Array): AvProtocolRequest;
24
20
  /**
25
- * The protocol's answer, written as it is (P1). Headers the platform's hooks
26
- * already set (CORS, say) go beneath it; a protocol header of the same name,
27
- * in any case, wins — what Node's `writeHead` does with `setHeader`'s on
28
- * Express.
21
+ * The protocol's answer, written as it is. Headers the platform's hooks already
22
+ * set (CORS, say) go beneath it; a protocol header of the same name, in any case,
23
+ * wins — what Node's `writeHead` does with `setHeader`'s on Express.
29
24
  */
30
25
  export declare function writeAnswer(response: NodeOutgoing, answer: AvProtocolResponse, beneath?: PlatformHeaders): void;
@@ -1,11 +1,8 @@
1
- /** Zero bytes: the body of a request whose body the protocol does not read. */
2
1
  export const NO_BYTES = new Uint8Array();
3
- /** A request URL's raw pathname — query dropped (Q12), not URL-decoded (P2). */
4
2
  export function pathnameOf(url) {
5
3
  const query = url.indexOf("?");
6
4
  return query === -1 ? url : url.slice(0, query);
7
5
  }
8
- /** The request as the protocol reads it: path relative to the entrypoint, `headersDistinct` (Q11), the raw arm (Q8). */
9
6
  export function protocolRequest(method, url, incoming, entrypoint, content) {
10
7
  return {
11
8
  method,
@@ -14,12 +11,6 @@ export function protocolRequest(method, url, incoming, entrypoint, content) {
14
11
  body: { kind: "raw", content },
15
12
  };
16
13
  }
17
- /**
18
- * The protocol's answer, written as it is (P1). Headers the platform's hooks
19
- * already set (CORS, say) go beneath it; a protocol header of the same name,
20
- * in any case, wins — what Node's `writeHead` does with `setHeader`'s on
21
- * Express.
22
- */
23
14
  export function writeAnswer(response, answer, beneath = {}) {
24
15
  const own = new Set(Object.keys(answer.headers).map((name) => name.toLowerCase()));
25
16
  const headers = {};
@@ -7,9 +7,9 @@ export type ProtocolHost = {
7
7
  };
8
8
  /**
9
9
  * Binds `framework`'s protocol for the application's platform, refusing an
10
- * unsupported platform while providers are instantiated — so
11
- * `NestFactory.create` rejects before the application listens (Q18). The
12
- * routes are mounted later, from the module's `configure()` (Q5).
10
+ * unsupported platform while providers are instantiated — so `NestFactory.create`
11
+ * rejects before the application listens. The routes are mounted later, from the
12
+ * module's `configure()`.
13
13
  */
14
14
  export declare function createProtocolHost(adapter: AbstractHttpAdapter | undefined, framework: Framework): ProtocolHost;
15
15
  /**
@@ -32,17 +32,14 @@ export type AdapterHostView = {
32
32
  };
33
33
  };
34
34
  /**
35
- * N1 (plan B23) — a host for an application whose HTTP adapter may arrive after
36
- * the providers are instantiated.
37
- *
38
- * `NestFactory.create` sets the adapter first: the host is created at once, and
39
- * an unsupported platform rejects then, as before (Q18). Nest's testing module
40
- * compiles first and receives its adapter from `createNestApplication()`, which
41
- * sets it on the `HttpAdapterHost` — `init$` fires — before `app.init()`
42
- * registers Nest's body parsers: the host is created at that moment, so a
43
- * platform's body reader is still installed ahead of them (Q8). A refusal there
44
- * is kept and raised by `mount()` or `assertReady()`, from `app.init()` — still
45
- * before the application listens. With no adapter at all
46
- * (`createApplicationContext`), `assertReady()` refuses from `onModuleInit`.
35
+ * `NestFactory.create` sets the adapter first: the host is created at once, and an
36
+ * unsupported platform rejects then, as before. Nest's testing module compiles
37
+ * first and receives its adapter from `createNestApplication()`, which sets it on
38
+ * the `HttpAdapterHost` — `init$` fires — before `app.init()` registers Nest's
39
+ * body parsers: the host is created at that moment, so a platform's body reader is
40
+ * still installed ahead of them. A refusal there is kept and raised by `mount()`
41
+ * or `assertReady()`, from `app.init()` — still before the application listens.
42
+ * With no adapter at all (`createApplicationContext`), `assertReady()` refuses
43
+ * from `onModuleInit`.
47
44
  */
48
45
  export declare function createPendingProtocolHost(application: AdapterHostView, framework: Framework): PendingProtocolHost;
@@ -11,12 +11,6 @@ const PLATFORM_MOUNTERS = {
11
11
  }, binding, entrypoint, maxRequestBytes),
12
12
  fastify: (adapter, binding, entrypoint, maxRequestBytes) => createFastifyProtocolHost(adapter.getInstance(), binding, entrypoint, maxRequestBytes),
13
13
  };
14
- /**
15
- * Binds `framework`'s protocol for the application's platform, refusing an
16
- * unsupported platform while providers are instantiated — so
17
- * `NestFactory.create` rejects before the application listens (Q18). The
18
- * routes are mounted later, from the module's `configure()` (Q5).
19
- */
20
14
  export function createProtocolHost(adapter, framework) {
21
15
  const platform = checkedPlatform(adapter?.getType());
22
16
  const binding = AvProtocol.bind(framework);
@@ -28,18 +22,7 @@ export function createProtocolHost(adapter, framework) {
28
22
  },
29
23
  };
30
24
  }
31
- /**
32
- * pilot.1 — the protocol's routes are platform routes, which Nest's
33
- * `RouterExplorer` never logs; this is the one line that says they exist.
34
- * Through Nest's `Logger`, so the application's logger settings govern it.
35
- */
36
25
  const MOUNT_LOGGER = new Logger("AventaraModule");
37
- /**
38
- * What one mount registered, read off `protocol.surface()`: the operation
39
- * count under the entrypoint, then each framework route by method and path —
40
- * e.g. `Aventara mounted 5 operations at /api (+ GET /api/_contract, POST
41
- * /api/_transactions)`.
42
- */
43
26
  export function mountedLine(routes, entrypoint) {
44
27
  const operations = routes.filter((route) => route.kind === "operation");
45
28
  const framework = routes
@@ -47,20 +30,6 @@ export function mountedLine(routes, entrypoint) {
47
30
  .map((route) => `${route.method} ${entrypoint}${route.path}`);
48
31
  return `Aventara mounted ${operations.length} ${operations.length === 1 ? "operation" : "operations"} at ${entrypoint === "" ? "/" : entrypoint} (+ ${framework.join(", ")})`;
49
32
  }
50
- /**
51
- * N1 (plan B23) — a host for an application whose HTTP adapter may arrive after
52
- * the providers are instantiated.
53
- *
54
- * `NestFactory.create` sets the adapter first: the host is created at once, and
55
- * an unsupported platform rejects then, as before (Q18). Nest's testing module
56
- * compiles first and receives its adapter from `createNestApplication()`, which
57
- * sets it on the `HttpAdapterHost` — `init$` fires — before `app.init()`
58
- * registers Nest's body parsers: the host is created at that moment, so a
59
- * platform's body reader is still installed ahead of them (Q8). A refusal there
60
- * is kept and raised by `mount()` or `assertReady()`, from `app.init()` — still
61
- * before the application listens. With no adapter at all
62
- * (`createApplicationContext`), `assertReady()` refuses from `onModuleInit`.
63
- */
64
33
  export function createPendingProtocolHost(application, framework) {
65
34
  if (application.httpAdapter !== undefined) {
66
35
  const host = createProtocolHost(application.httpAdapter, framework);
@@ -80,7 +49,6 @@ export function createPendingProtocolHost(application, framework) {
80
49
  if (refusal !== undefined) {
81
50
  throw refusal;
82
51
  }
83
- // No adapter ever arrived: refused with the same sentence as at creation.
84
52
  return host ?? createProtocolHost(undefined, framework);
85
53
  };
86
54
  return {
@@ -1,20 +1,18 @@
1
1
  import type { AvProtocolBinding, AvProtocolResponse } from "@aventara/core/protocol";
2
2
  /**
3
- * The raw arm's body reader (Q8, Q9): a request body read off a Node stream,
4
- * decoded per its `Content-Encoding`, and stored up to `maxRequestBytes + 1`
5
- * DECODED bytes — one more than the limit is all the decoder needs to answer
6
- * `A2010`, and counting decoded bytes is also the decompression-bomb bound
7
- * (§12.3, §20.1). Past the bound the stream is still read to its end and
8
- * discarded, so the connection stays usable (U1); nothing more is inflated.
3
+ * The raw arm's body reader: a request body read off a Node stream, decoded per
4
+ * its `Content-Encoding`, and stored up to `maxRequestBytes + 1` DECODED bytes —
5
+ * one more than the limit is all the decoder needs to answer `A2010`, and counting
6
+ * decoded bytes is also the decompression-bomb bound. Past the bound the stream is
7
+ * still read to its end and discarded, so the connection stays usable; nothing
8
+ * more is inflated.
9
9
  *
10
- * Platform-free: it reads an async iterable of bytes, as Node's
11
- * `IncomingMessage` is. `node:zlib` is loaded by a specifier the compiler does
12
- * not resolve, typed by the slice below (P4: no `@types/node`).
10
+ * Platform-free: it reads an async iterable of bytes, as Node's `IncomingMessage`
11
+ * is.
13
12
  */
14
13
  /**
15
- * What the reader hands on: the stored bytes; an unsupported coding (Q9,
16
- * answered `A2011`); or a body that does not decode in the coding it names —
17
- * a malformed request (director, 2026-10-05: answered `A2000`).
14
+ * What the reader hands on: the stored bytes; an unsupported coding; or a body
15
+ * that does not decode in the coding it names — a malformed request.
18
16
  */
19
17
  export type RequestBodyRead = {
20
18
  readonly kind: "read";
@@ -25,12 +23,11 @@ export type RequestBodyRead = {
25
23
  readonly kind: "undecodable";
26
24
  };
27
25
  /**
28
- * The answer to a body the reader could not hand on, through the bound
29
- * `failure` member — the one place the host names an A-code (Q9): an
30
- * unsupported coding is `415 A2011`; a body that does not decode in its
31
- * coding is malformed, `400 A2000` (director, 2026-10-05). The request id is
32
- * minted by core: it exposes no adoption of the client's id to a host
33
- * (register row, S8).
26
+ * The answer to a body the reader could not hand on, through the bound `failure`
27
+ * member — the one place the host names an A-code: an unsupported coding is `415
28
+ * A2011`; a body that does not decode in its coding is malformed, `400 A2000`. The
29
+ * request id is minted by core: it exposes no adoption of the client's id to a
30
+ * host.
34
31
  */
35
32
  export declare function unreadBodyAnswer(binding: AvProtocolBinding, read: Exclude<RequestBodyRead, {
36
33
  readonly kind: "read";
@@ -1,11 +1,3 @@
1
- /**
2
- * The answer to a body the reader could not hand on, through the bound
3
- * `failure` member — the one place the host names an A-code (Q9): an
4
- * unsupported coding is `415 A2011`; a body that does not decode in its
5
- * coding is malformed, `400 A2000` (director, 2026-10-05). The request id is
6
- * minted by core: it exposes no adoption of the client's id to a host
7
- * (register row, S8).
8
- */
9
1
  export function unreadBodyAnswer(binding, read) {
10
2
  return read.kind === "unsupported-coding"
11
3
  ? binding.failure("A2011")
@@ -22,19 +14,9 @@ const zlib = () => {
22
14
  zlibModule ??= import(specifier);
23
15
  return zlibModule;
24
16
  };
25
- /**
26
- * Reads `source` to its end. `contentEncoding` is the header's values as
27
- * received; absent, empty or `identity` is no coding; one of `gzip`,
28
- * `deflate`, `br` (any case) is inflated; anything else — a stacked coding
29
- * included — is unsupported. A body that does not decode in the coding it
30
- * names is undecodable. Either way the stream is still read to its end.
31
- */
32
17
  export async function readRequestBody(source, contentEncoding, cap) {
33
18
  const coding = codingOf(contentEncoding);
34
19
  if (coding === undefined) {
35
- // Read to the end and discarded all the same: the stream finished is
36
- // what keeps the platform's own parsers off it (Q8), and the
37
- // connection usable.
38
20
  for await (const _ of source) {
39
21
  }
40
22
  return { kind: "unsupported-coding" };
@@ -55,11 +37,6 @@ export async function readRequestBody(source, contentEncoding, cap) {
55
37
  }
56
38
  }
57
39
  })();
58
- // Settles when the inflater stops — at its end, at the bound (destroyed),
59
- // or at a decoding error. Node calls no write callback once an inflater
60
- // has failed, so each write also waits on this, never on the callback
61
- // alone. Marking it handled here keeps an early decoding error from being
62
- // reported as unhandled before it is awaited below.
63
40
  const stopped = inflated.then(() => undefined, () => undefined);
64
41
  for await (const chunk of source) {
65
42
  if (!inflater.destroyed) {
@@ -82,7 +59,6 @@ export async function readRequestBody(source, contentEncoding, cap) {
82
59
  }
83
60
  return { kind: "read", content: stored.content() };
84
61
  }
85
- /** The coding the header names, `identity` for none, `undefined` for an unsupported one. */
86
62
  function codingOf(contentEncoding) {
87
63
  const named = (contentEncoding ?? [])
88
64
  .join(",")
@@ -103,7 +79,6 @@ function codingOf(contentEncoding) {
103
79
  ? only
104
80
  : undefined;
105
81
  }
106
- /** Stores bytes up to `limit`; `add` answers whether there is room for more. */
107
82
  function boundedStore(limit) {
108
83
  const chunks = [];
109
84
  let size = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/nest",
3
- "version": "0.1.0-pilot.1",
3
+ "version": "0.1.0-pilot.2",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "NestJS host integration for Aventara.",
6
6
  "type": "module",
@@ -21,7 +21,7 @@
21
21
  "LICENSE-ADDITIONAL-PERMISSION.md"
22
22
  ],
23
23
  "dependencies": {
24
- "@aventara/core": "0.1.0-pilot.1"
24
+ "@aventara/core": "0.1.0-pilot.2"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "@nestjs/common": "^12.0.0",
@@ -31,8 +31,8 @@
31
31
  "access": "public"
32
32
  },
33
33
  "devDependencies": {
34
- "@aventara/prisma7-adapter": "0.1.0-pilot.1",
35
- "@aventara/testing": "0.1.0-pilot.1",
34
+ "@aventara/prisma7-adapter": "0.1.0-pilot.2",
35
+ "@aventara/testing": "0.1.0-pilot.2",
36
36
  "@nestjs/common": "^12.1.2",
37
37
  "@nestjs/core": "^12.1.2",
38
38
  "@nestjs/platform-express": "^12.1.2",
@@ -43,7 +43,7 @@
43
43
  "rxjs": "^7.8.2"
44
44
  },
45
45
  "scripts": {
46
- "build": "tsc -p tsconfig.json",
46
+ "build": "tsc -p tsconfig.json --removeComments --declaration false && tsc -p tsconfig.json --emitDeclarationOnly && node ../../scripts/build/declaration-comments.sanitizer.ts dist",
47
47
  "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
48
48
  "test": "vitest run --typecheck --config ../../vitest.config.mts --root .",
49
49
  "lint": "biome lint src"