@astram/guard 0.1.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/dist/authentication/argon2id-password-hasher.d.ts +61 -0
  4. package/dist/authentication/argon2id-password-hasher.d.ts.map +1 -0
  5. package/dist/authentication/argon2id-password-hasher.js +108 -0
  6. package/dist/boot/boot-sequence.d.ts +43 -0
  7. package/dist/boot/boot-sequence.d.ts.map +1 -0
  8. package/dist/boot/boot-sequence.js +90 -0
  9. package/dist/boot/boot.module.d.ts +8 -0
  10. package/dist/boot/boot.module.d.ts.map +1 -0
  11. package/dist/boot/boot.module.js +24 -0
  12. package/dist/config/app-config.d.ts +112 -0
  13. package/dist/config/app-config.d.ts.map +1 -0
  14. package/dist/config/app-config.js +94 -0
  15. package/dist/config/config.module.d.ts +12 -0
  16. package/dist/config/config.module.d.ts.map +1 -0
  17. package/dist/config/config.module.js +35 -0
  18. package/dist/enforcement/classification.decorator.d.ts +41 -0
  19. package/dist/enforcement/classification.decorator.d.ts.map +1 -0
  20. package/dist/enforcement/classification.decorator.js +46 -0
  21. package/dist/enforcement/core-seam.d.ts +40 -0
  22. package/dist/enforcement/core-seam.d.ts.map +1 -0
  23. package/dist/enforcement/core-seam.js +38 -0
  24. package/dist/enforcement/enforcement.module.d.ts +18 -0
  25. package/dist/enforcement/enforcement.module.d.ts.map +1 -0
  26. package/dist/enforcement/enforcement.module.js +40 -0
  27. package/dist/enforcement/global-enforcement.guard.d.ts +31 -0
  28. package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -0
  29. package/dist/enforcement/global-enforcement.guard.js +76 -0
  30. package/dist/index.d.ts +26 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +88 -0
  33. package/dist/module/shield.module.d.ts +79 -0
  34. package/dist/module/shield.module.d.ts.map +1 -0
  35. package/dist/module/shield.module.js +108 -0
  36. package/dist/session/crypto-adapters.d.ts +60 -0
  37. package/dist/session/crypto-adapters.d.ts.map +1 -0
  38. package/dist/session/crypto-adapters.js +80 -0
  39. package/dist/session/index.d.ts +17 -0
  40. package/dist/session/index.d.ts.map +1 -0
  41. package/dist/session/index.js +32 -0
  42. package/dist/session/mariadb-session-record-store.d.ts +50 -0
  43. package/dist/session/mariadb-session-record-store.d.ts.map +1 -0
  44. package/dist/session/mariadb-session-record-store.js +162 -0
  45. package/dist/session/redis-hot-session-store.d.ts +30 -0
  46. package/dist/session/redis-hot-session-store.d.ts.map +1 -0
  47. package/dist/session/redis-hot-session-store.js +102 -0
  48. package/package.json +52 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Astram
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @astram/guard — the enforcement gate + NestJS adapters
2
+
3
+ The **enforcement gate + thin NestJS integration** for the Astram framework — the
4
+ shippable glue a consumer app installs (it runs on / integrates with NestJS). Per
5
+ [ADR-0023](../../docs/Shield/ADR/0023-framework-model-publishable-core.md)
6
+ this is **framework code, owned + locked**, not consumer-editable: it lives in the
7
+ consumer's `node_modules`, and upgrades flow through `npm update`. It depends on
8
+ [`@astram/core`](../core) (the framework-agnostic kernel) and PEER-depends on the
9
+ host's NestJS runtime.
10
+
11
+ ## What's in here
12
+
13
+ | Piece | File | Purpose |
14
+ | ------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
15
+ | **`ShieldModule`** | `src/module/shield.module.ts` | The **one registration seam** — `forRoot()` / `forRootAsync()`. Consumers import it once; the framework owns composition. |
16
+ | Global enforcement guard | `src/enforcement/global-enforcement.guard.ts` | The deny-by-default `APP_GUARD` (ADR-0002) every route traverses. |
17
+ | Classification decorators | `src/enforcement/classification.decorator.ts` | `@Public()` / `@Classified()` — the consumer authors these on controllers. |
18
+ | Core seam | `src/enforcement/core-seam.ts` | Type-only import of `@astram/core`'s `DecisionAuthority` (ADR-0003) + the fail-closed stub. |
19
+ | Boot-pool assertion | `src/boot/boot-sequence.ts` | ADR-0018 connection-pool budget check at boot (fail-closed). |
20
+ | App config | `src/config/app-config.ts` | Port / environment / ADR-0018 pool budget (env loader, pure). |
21
+
22
+ ## The registration seam (ADR-0023 §4)
23
+
24
+ A consumer app wires Astram with a **single import** in its root module — it never
25
+ edits the framework:
26
+
27
+ ```ts
28
+ import { Module } from '@nestjs/common';
29
+ import { ShieldModule } from '@astram/guard';
30
+
31
+ @Module({
32
+ imports: [
33
+ ShieldModule.forRoot(), // fail-closed stub authority; env-loaded config
34
+ // ShieldModule.forRoot({ decisionAuthority, config }) to supply the real Core authority
35
+ ],
36
+ controllers: [/* the consumer's feature controllers */],
37
+ })
38
+ export class AppModule {}
39
+ ```
40
+
41
+ `forRoot()` binds:
42
+
43
+ - **`GlobalEnforcementGuard` as `APP_GUARD`** → the global, structurally-unavoidable,
44
+ deny-by-default enforcement point (ADR-0002). `@Public()` is the only opt-out;
45
+ an unclassified route is **denied (403)**.
46
+ - The resolved **`AppConfig`** (under `APP_CONFIG`) + the **`BootSequence`** hook,
47
+ which runs the **ADR-0018** pool-budget assertion at boot (aborts on violation).
48
+ - The **Core decision authority** (under `SHIELD_DECISION_AUTHORITY`) — the real
49
+ `@astram/core` authority when supplied via `forRoot({ decisionAuthority })`, else
50
+ the fail-closed **stub** (deny-by-default), so a mis-wiring cannot open a door.
51
+
52
+ `forRootAsync({ useFactory, inject, imports })` is the DI-computed variant (a stub
53
+ in this layer): the factory resolves the same options at startup; the guard/boot
54
+ wiring is identical.
55
+
56
+ ## Peer dependencies (ADR-0023 §2)
57
+
58
+ `@astram/guard` **peer-depends** on the host runtime so the consumer owns exactly
59
+ one copy (a single `reflect-metadata` singleton / DI injector):
60
+
61
+ ```
62
+ @nestjs/common ^11 · @nestjs/core ^11 · reflect-metadata ^0.2 · rxjs ^7
63
+ ```
64
+
65
+ These are also `devDependencies` here so the package compiles + tests locally.
66
+ `@astram/core` is a real (workspace) **dependency**.
67
+
68
+ ## Commands
69
+
70
+ ```bash
71
+ pnpm --filter @astram/guard build # tsc -p tsconfig.build.json (emits dist + .d.ts)
72
+ pnpm --filter @astram/guard lint # eslint
73
+ pnpm --filter @astram/guard test # vitest run (guard unit + boot-pool + forRoot e2e)
74
+ ```
75
+
76
+ ## Notes
77
+
78
+ - **CommonJS** locally (`"type": "commonjs"`) for the NestJS decorator +
79
+ `reflect-metadata` DI model. The repo root and `@astram/core` stay ESM/NodeNext;
80
+ the override is scoped to this package (see `tsconfig.json`).
81
+ - The core seam imports the REAL `@astram/core` **barrel** (type-only) — Core's
82
+ `exports` map exposes only `"."`, so subpath imports fail Node resolution. This
83
+ package's tsconfig clears the base `paths` so `@astram/core` resolves through the
84
+ node_modules workspace symlink to Core's built `dist`.
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `Argon2idPasswordHasher` — the concrete {@link PasswordHasher} for
3
+ * `@astram/guard`, backed by the `argon2` package (Argon2id).
4
+ *
5
+ * ADR-0003 (why this lives HERE, not in Core): `@astram/core` is ZERO runtime
6
+ * dependencies — the load-bearing invariant. Argon2id needs the `argon2` npm
7
+ * package + its native binding, so the concrete hasher ships in this integration
8
+ * package and is INJECTED into `@astram/core`'s `createAuthenticator({ hasher })`.
9
+ * Core defines only the `PasswordHasher` PORT; nothing about argon2 crosses the
10
+ * ADR-0003 boundary.
11
+ *
12
+ * SECURITY — never logs the plaintext or the hash (they are transient inputs). A
13
+ * malformed stored hash resolves `false` (not a throw) so the engine's
14
+ * verification path stays fail-closed rather than surfacing a raw error.
15
+ */
16
+ import type { PasswordHasher } from '@astram/core';
17
+ /**
18
+ * Tuned Argon2id parameters (sane server defaults; OWASP-aligned baseline). A
19
+ * deployment can override via the constructor if its hardware profile differs;
20
+ * changing them does not change the ENCODED hash's self-describing header, so
21
+ * older hashes still verify.
22
+ *
23
+ * - `memoryCost` 19456 KiB (19 MiB) — the OWASP "m=19456, t=2, p=1" baseline.
24
+ * - `timeCost` 2 iterations.
25
+ * - `parallelism` 1 lane.
26
+ */
27
+ export interface Argon2idParams {
28
+ readonly memoryCost?: number;
29
+ readonly timeCost?: number;
30
+ readonly parallelism?: number;
31
+ }
32
+ /** The DI token consumers/ShieldModule bind the concrete hasher under. */
33
+ export declare const SHIELD_PASSWORD_HASHER: unique symbol;
34
+ /**
35
+ * Argon2id implementation of the Core {@link PasswordHasher} PORT. Injectable —
36
+ * bind it under {@link SHIELD_PASSWORD_HASHER} and hand it to
37
+ * `createAuthenticator({ hasher })`.
38
+ */
39
+ export declare class Argon2idPasswordHasher implements PasswordHasher {
40
+ private readonly options;
41
+ constructor(params?: Argon2idParams);
42
+ /** Hash a plaintext into an encoded, self-describing Argon2id hash string. */
43
+ hash(plaintext: string): Promise<string>;
44
+ /**
45
+ * Verify a plaintext against an encoded Argon2id hash. Runs in constant time
46
+ * w.r.t. the digest (argon2's own comparison). A malformed/unparseable hash
47
+ * resolves `false` (fail-closed) instead of throwing.
48
+ */
49
+ verify(hash: string, plaintext: string): Promise<boolean>;
50
+ }
51
+ /**
52
+ * Provider factory for wiring the Argon2id hasher into a Nest DI container under
53
+ * {@link SHIELD_PASSWORD_HASHER}. Kept minimal (ADR-0023 §7): a plain provider,
54
+ * not a whole module — the consumer includes it where it composes the
55
+ * authenticator. Exported so `ShieldModule` (or a consumer) can bind it.
56
+ */
57
+ export declare function argon2idHasherProvider(params?: Argon2idParams): {
58
+ provide: symbol;
59
+ useValue: Argon2idPasswordHasher;
60
+ };
61
+ //# sourceMappingURL=argon2id-password-hasher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"argon2id-password-hasher.d.ts","sourceRoot":"","sources":["../../src/authentication/argon2id-password-hasher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;GASG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAQD,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,eAAmC,CAAC;AAEvE;;;;GAIG;AACH,qBAAa,sBAAuB,YAAW,cAAc;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuC;gBAEnD,MAAM,GAAE,cAAmB;IASvC,8EAA8E;IACxE,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK9C;;;;OAIG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAQhE;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,GAAE,cAAmB,GAAG;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,sBAAsB,CAAC;CAClC,CAKA"}
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ /**
3
+ * `Argon2idPasswordHasher` — the concrete {@link PasswordHasher} for
4
+ * `@astram/guard`, backed by the `argon2` package (Argon2id).
5
+ *
6
+ * ADR-0003 (why this lives HERE, not in Core): `@astram/core` is ZERO runtime
7
+ * dependencies — the load-bearing invariant. Argon2id needs the `argon2` npm
8
+ * package + its native binding, so the concrete hasher ships in this integration
9
+ * package and is INJECTED into `@astram/core`'s `createAuthenticator({ hasher })`.
10
+ * Core defines only the `PasswordHasher` PORT; nothing about argon2 crosses the
11
+ * ADR-0003 boundary.
12
+ *
13
+ * SECURITY — never logs the plaintext or the hash (they are transient inputs). A
14
+ * malformed stored hash resolves `false` (not a throw) so the engine's
15
+ * verification path stays fail-closed rather than surfacing a raw error.
16
+ */
17
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
+ desc = { enumerable: true, get: function() { return m[k]; } };
22
+ }
23
+ Object.defineProperty(o, k2, desc);
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || (function () {
34
+ var ownKeys = function(o) {
35
+ ownKeys = Object.getOwnPropertyNames || function (o) {
36
+ var ar = [];
37
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
38
+ return ar;
39
+ };
40
+ return ownKeys(o);
41
+ };
42
+ return function (mod) {
43
+ if (mod && mod.__esModule) return mod;
44
+ var result = {};
45
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
46
+ __setModuleDefault(result, mod);
47
+ return result;
48
+ };
49
+ })();
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.Argon2idPasswordHasher = exports.SHIELD_PASSWORD_HASHER = void 0;
52
+ exports.argon2idHasherProvider = argon2idHasherProvider;
53
+ const argon2 = __importStar(require("argon2"));
54
+ const DEFAULT_PARAMS = {
55
+ memoryCost: 19_456,
56
+ timeCost: 2,
57
+ parallelism: 1,
58
+ };
59
+ /** The DI token consumers/ShieldModule bind the concrete hasher under. */
60
+ exports.SHIELD_PASSWORD_HASHER = Symbol('SHIELD_PASSWORD_HASHER');
61
+ /**
62
+ * Argon2id implementation of the Core {@link PasswordHasher} PORT. Injectable —
63
+ * bind it under {@link SHIELD_PASSWORD_HASHER} and hand it to
64
+ * `createAuthenticator({ hasher })`.
65
+ */
66
+ class Argon2idPasswordHasher {
67
+ options;
68
+ constructor(params = {}) {
69
+ this.options = {
70
+ type: argon2.argon2id,
71
+ memoryCost: params.memoryCost ?? DEFAULT_PARAMS.memoryCost,
72
+ timeCost: params.timeCost ?? DEFAULT_PARAMS.timeCost,
73
+ parallelism: params.parallelism ?? DEFAULT_PARAMS.parallelism,
74
+ };
75
+ }
76
+ /** Hash a plaintext into an encoded, self-describing Argon2id hash string. */
77
+ async hash(plaintext) {
78
+ // argon2 generates a fresh random salt per call and embeds it in the output.
79
+ return argon2.hash(plaintext, this.options);
80
+ }
81
+ /**
82
+ * Verify a plaintext against an encoded Argon2id hash. Runs in constant time
83
+ * w.r.t. the digest (argon2's own comparison). A malformed/unparseable hash
84
+ * resolves `false` (fail-closed) instead of throwing.
85
+ */
86
+ async verify(hash, plaintext) {
87
+ try {
88
+ return await argon2.verify(hash, plaintext);
89
+ }
90
+ catch {
91
+ // An unparseable hash (or a decode error) is NOT a match — never a leak.
92
+ return false;
93
+ }
94
+ }
95
+ }
96
+ exports.Argon2idPasswordHasher = Argon2idPasswordHasher;
97
+ /**
98
+ * Provider factory for wiring the Argon2id hasher into a Nest DI container under
99
+ * {@link SHIELD_PASSWORD_HASHER}. Kept minimal (ADR-0023 §7): a plain provider,
100
+ * not a whole module — the consumer includes it where it composes the
101
+ * authenticator. Exported so `ShieldModule` (or a consumer) can bind it.
102
+ */
103
+ function argon2idHasherProvider(params = {}) {
104
+ return {
105
+ provide: exports.SHIELD_PASSWORD_HASHER,
106
+ useValue: new Argon2idPasswordHasher(params),
107
+ };
108
+ }
@@ -0,0 +1,43 @@
1
+ import { type OnApplicationBootstrap } from '@nestjs/common';
2
+ import { type AppConfig, type PoolBudgetConfig } from '../config/app-config.js';
3
+ /**
4
+ * Boot sequence extension point (Infrastructure layer — ADR-0018).
5
+ *
6
+ * Nest calls {@link onApplicationBootstrap} once, after all modules are
7
+ * initialized and before the HTTP server starts accepting traffic. This is the
8
+ * seam where boot-time assertions attach.
9
+ *
10
+ * ───────────────────────────────────────────────────────────────────────────
11
+ * ADR-0018 connection-pool assertion (FILLED).
12
+ * {@link assertConnectionPoolReady} computes the connection budget
13
+ * workers × Σ(per-account runtime pool max) + headroom ≤ maxConnections
14
+ * and THROWS (fail-closed, abort boot) on violation, so a pool config that
15
+ * would exceed MariaDB's `max_connections` is caught at boot, never under load.
16
+ * ───────────────────────────────────────────────────────────────────────────
17
+ */
18
+ export declare class BootSequence implements OnApplicationBootstrap {
19
+ private readonly config;
20
+ constructor(config: AppConfig);
21
+ onApplicationBootstrap(): Promise<void>;
22
+ /**
23
+ * ADR-0018: assert the configured connection-pool budget fits under the DB
24
+ * `max_connections` ceiling BEFORE the app serves traffic. THROWS on
25
+ * violation so boot aborts (fail-closed on config, ADR-0018 §3).
26
+ *
27
+ * Pure computation over {@link AppConfig.pool}; no DB I/O — the ceiling is a
28
+ * declared invariant validated against the DB's deliberately-set value, not
29
+ * probed live. (Live pool health-probing can attach here later without
30
+ * changing this contract.)
31
+ */
32
+ private assertConnectionPoolReady;
33
+ }
34
+ /**
35
+ * Validate a {@link PoolBudgetConfig} against the ADR-0018 budget. Exported so
36
+ * unit tests can assert the throw/no-throw contract directly without booting
37
+ * the whole Nest app.
38
+ *
39
+ * @throws Error (fail-closed) when the required connections exceed
40
+ * `maxConnections`, or when the schema-service pool exceeds its hard cap.
41
+ */
42
+ export declare function assertPoolBudget(cfg: PoolBudgetConfig): void;
43
+ //# sourceMappingURL=boot-sequence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot-sequence.d.ts","sourceRoot":"","sources":["../../src/boot/boot-sequence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAEjF,OAAO,EAAsB,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEpG;;;;;;;;;;;;;;GAcG;AACH,qBACa,YAAa,YAAW,sBAAsB;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,SAAS;IAE5D,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAK7C;;;;;;;;;OASG;YACW,yBAAyB;CAIxC;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAuB5D"}
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.BootSequence = void 0;
16
+ exports.assertPoolBudget = assertPoolBudget;
17
+ const common_1 = require("@nestjs/common");
18
+ const config_module_js_1 = require("../config/config.module.js");
19
+ const app_config_js_1 = require("../config/app-config.js");
20
+ /**
21
+ * Boot sequence extension point (Infrastructure layer — ADR-0018).
22
+ *
23
+ * Nest calls {@link onApplicationBootstrap} once, after all modules are
24
+ * initialized and before the HTTP server starts accepting traffic. This is the
25
+ * seam where boot-time assertions attach.
26
+ *
27
+ * ───────────────────────────────────────────────────────────────────────────
28
+ * ADR-0018 connection-pool assertion (FILLED).
29
+ * {@link assertConnectionPoolReady} computes the connection budget
30
+ * workers × Σ(per-account runtime pool max) + headroom ≤ maxConnections
31
+ * and THROWS (fail-closed, abort boot) on violation, so a pool config that
32
+ * would exceed MariaDB's `max_connections` is caught at boot, never under load.
33
+ * ───────────────────────────────────────────────────────────────────────────
34
+ */
35
+ let BootSequence = class BootSequence {
36
+ config;
37
+ constructor(config) {
38
+ this.config = config;
39
+ }
40
+ async onApplicationBootstrap() {
41
+ await this.assertConnectionPoolReady();
42
+ // Future boot-time assertions attach here (each fail-closed).
43
+ }
44
+ /**
45
+ * ADR-0018: assert the configured connection-pool budget fits under the DB
46
+ * `max_connections` ceiling BEFORE the app serves traffic. THROWS on
47
+ * violation so boot aborts (fail-closed on config, ADR-0018 §3).
48
+ *
49
+ * Pure computation over {@link AppConfig.pool}; no DB I/O — the ceiling is a
50
+ * declared invariant validated against the DB's deliberately-set value, not
51
+ * probed live. (Live pool health-probing can attach here later without
52
+ * changing this contract.)
53
+ */
54
+ async assertConnectionPoolReady() {
55
+ assertPoolBudget(this.config.pool);
56
+ return Promise.resolve();
57
+ }
58
+ };
59
+ exports.BootSequence = BootSequence;
60
+ exports.BootSequence = BootSequence = __decorate([
61
+ (0, common_1.Injectable)(),
62
+ __param(0, (0, common_1.Inject)(config_module_js_1.APP_CONFIG)),
63
+ __metadata("design:paramtypes", [Object])
64
+ ], BootSequence);
65
+ /**
66
+ * Validate a {@link PoolBudgetConfig} against the ADR-0018 budget. Exported so
67
+ * unit tests can assert the throw/no-throw contract directly without booting
68
+ * the whole Nest app.
69
+ *
70
+ * @throws Error (fail-closed) when the required connections exceed
71
+ * `maxConnections`, or when the schema-service pool exceeds its hard cap.
72
+ */
73
+ function assertPoolBudget(cfg) {
74
+ const e = (0, app_config_js_1.evaluatePoolBudget)(cfg);
75
+ if (!e.schemaSvcWithinHardCap) {
76
+ throw new Error('[ADR-0018] connection-pool assertion FAILED (fail-closed): ' +
77
+ `shield_schema_svc pool max is ${cfg.perAccountMax.shield_schema_svc}, ` +
78
+ 'which exceeds the hard cap of 2. DDL is rare + serialized per tenant ' +
79
+ '(ADR-0014 §7) — keep this pool tiny (1–2).');
80
+ }
81
+ if (!e.withinBudget) {
82
+ throw new Error('[ADR-0018] connection-pool assertion FAILED (fail-closed): required ' +
83
+ `connections ${e.requiredConnections} exceed max_connections ` +
84
+ `${cfg.maxConnections}. Budget = workers(${cfg.workers}) × ` +
85
+ `Σ(per-account pool max)(${e.perWorkerConnections}) + headroom(` +
86
+ `${cfg.headroom}) = ${e.requiredConnections} > ${cfg.maxConnections}. ` +
87
+ 'Reduce a per-account pool cap, the worker count, or raise ' +
88
+ 'max_connections (and MariaDB my.cnf) to restore the budget.');
89
+ }
90
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Registers the {@link BootSequence} lifecycle hook (Scaffold).
3
+ * Holds the named extension point where the ADR-0018 connection-pool assertion
4
+ * attaches in a later layer.
5
+ */
6
+ export declare class BootModule {
7
+ }
8
+ //# sourceMappingURL=boot.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot.module.d.ts","sourceRoot":"","sources":["../../src/boot/boot.module.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,qBAGa,UAAU;CAAG"}
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.BootModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const boot_sequence_js_1 = require("./boot-sequence.js");
12
+ /**
13
+ * Registers the {@link BootSequence} lifecycle hook (Scaffold).
14
+ * Holds the named extension point where the ADR-0018 connection-pool assertion
15
+ * attaches in a later layer.
16
+ */
17
+ let BootModule = class BootModule {
18
+ };
19
+ exports.BootModule = BootModule;
20
+ exports.BootModule = BootModule = __decorate([
21
+ (0, common_1.Module)({
22
+ providers: [boot_sequence_js_1.BootSequence],
23
+ })
24
+ ], BootModule);
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Application configuration for the NestJS integration layer.
3
+ *
4
+ * The Scaffold shipped a deliberately-tiny loader (port + environment). The
5
+ * Infrastructure layer (this file) extends it with the ADR-0018 connection-pool
6
+ * budget: per-account pool caps, the app worker count, headroom, and the DB
7
+ * `maxConnections` ceiling. All are read from the environment with documented
8
+ * defaults and performs NO I/O — pure + side-effect-free so it stays trivially
9
+ * unit-testable, and so the boot-time budget assertion can validate it before
10
+ * the app serves traffic (fail-closed on config, ADR-0018 §3).
11
+ *
12
+ * ─────────────────────────────────────────────────────────────────────────────
13
+ * ADR-0018 budget (the invariant this config feeds):
14
+ * workers × Σ(per-account RUNTIME pool max) + headroom ≤ maxConnections
15
+ *
16
+ * "Runtime" pools are the pools each PM2 cluster worker holds open: shield_core,
17
+ * shield_app_crud, shield_schema_svc. The deploy/ops accounts (migrate, backup,
18
+ * monitor) are short-lived, NOT per-worker, and are covered by `headroom`.
19
+ * shield_schema_svc stays TINY (DDL is rare + serialized per tenant, ADR-0014 §7).
20
+ *
21
+ * Defaults below agree byte-for-byte with infra/mariadb/conf.d/my.cnf
22
+ * (max_connections=250) and the local docker-compose substrate:
23
+ * 8 × (12 + 8 + 1) + 40 = 208 ≤ 250 ✓
24
+ * ─────────────────────────────────────────────────────────────────────────────
25
+ */
26
+ export interface AppConfig {
27
+ /** HTTP port the Nest app listens on. */
28
+ readonly port: number;
29
+ /** Runtime environment label (for logging / conditional wiring). */
30
+ readonly environment: 'development' | 'test' | 'production';
31
+ /** ADR-0018 connection-pool budget (validated at boot, fail-closed). */
32
+ readonly pool: PoolBudgetConfig;
33
+ }
34
+ /**
35
+ * The ADR-0018 connection-pool budget surface. Every field is configurable per
36
+ * deployment/edition via env; the defaults are the MVP single-node values that
37
+ * match the local substrate.
38
+ */
39
+ export interface PoolBudgetConfig {
40
+ /**
41
+ * PM2 cluster worker count (≈ 1 per vCPU on the app host). Each worker holds
42
+ * its OWN independent set of runtime pools — pools are not shared across
43
+ * workers (ADR-0018 context).
44
+ */
45
+ readonly workers: number;
46
+ /**
47
+ * MariaDB `max_connections` ceiling this app must fit under. Set deliberately
48
+ * (NOT the 151 default) and must equal the DB server's configured value.
49
+ */
50
+ readonly maxConnections: number;
51
+ /**
52
+ * Reserved connections NOT consumed by per-worker runtime pools: admin
53
+ * sessions + the short-lived deploy/ops accounts (migrate/backup/monitor).
54
+ */
55
+ readonly headroom: number;
56
+ /** Per-account RUNTIME pool caps (multiplied by `workers` in the budget). */
57
+ readonly perAccountMax: RuntimeAccountPools;
58
+ }
59
+ /**
60
+ * Per-account maximum pool sizes for the RUNTIME accounts (the ones each worker
61
+ * opens). Keyed by the exact MariaDB account name so the budget math and the
62
+ * grant matrix line up 1:1.
63
+ */
64
+ export interface RuntimeAccountPools {
65
+ /** shield_core — DML across all schemas, hot path: the main pool. */
66
+ readonly shield_core: number;
67
+ /** shield_app_crud — application DML load. */
68
+ readonly shield_app_crud: number;
69
+ /** shield_schema_svc — DDL only, rare + serialized: keep TINY (1–2). */
70
+ readonly shield_schema_svc: number;
71
+ }
72
+ /** Documented MVP single-node defaults (see the class doc + my.cnf). */
73
+ export declare const DEFAULT_POOL_BUDGET: PoolBudgetConfig;
74
+ /** Upper bound the schema-service pool must never exceed (ADR-0018 §2: 1–2). */
75
+ export declare const SCHEMA_SVC_POOL_HARD_CAP = 2;
76
+ /**
77
+ * Build the {@link PoolBudgetConfig} from env with documented defaults.
78
+ * Field ↔ env name:
79
+ * workers ← SHIELD_POOL_WORKERS
80
+ * maxConnections ← SHIELD_DB_MAX_CONNECTIONS
81
+ * headroom ← SHIELD_POOL_HEADROOM
82
+ * perAccountMax.shield_core ← SHIELD_POOL_MAX_CORE
83
+ * perAccountMax.shield_app_crud ← SHIELD_POOL_MAX_APP_CRUD
84
+ * perAccountMax.shield_schema_svc ← SHIELD_POOL_MAX_SCHEMA_SVC
85
+ */
86
+ export declare function loadPoolBudget(env?: NodeJS.ProcessEnv): PoolBudgetConfig;
87
+ /**
88
+ * Build the {@link AppConfig} from `process.env` (or an injected env bag, for
89
+ * testability). Pure + side-effect-free so it is trivially unit-testable.
90
+ */
91
+ export declare function loadAppConfig(env?: NodeJS.ProcessEnv): AppConfig;
92
+ /** Result of evaluating a {@link PoolBudgetConfig} against the ADR-0018 budget. */
93
+ export interface PoolBudgetEvaluation {
94
+ /** Σ(per-account runtime pool max) for a single worker. */
95
+ readonly perWorkerConnections: number;
96
+ /** workers × perWorkerConnections — total runtime connections at full load. */
97
+ readonly runtimeConnections: number;
98
+ /** runtimeConnections + headroom — what must fit under maxConnections. */
99
+ readonly requiredConnections: number;
100
+ /** maxConnections − requiredConnections (negative ⇒ over budget). */
101
+ readonly slack: number;
102
+ /** True iff requiredConnections ≤ maxConnections. */
103
+ readonly withinBudget: boolean;
104
+ /** True iff shield_schema_svc pool ≤ {@link SCHEMA_SVC_POOL_HARD_CAP}. */
105
+ readonly schemaSvcWithinHardCap: boolean;
106
+ }
107
+ /**
108
+ * Evaluate a pool budget. Pure: no throwing, no I/O — the caller decides how to
109
+ * react (the boot assertion throws; tests inspect the fields).
110
+ */
111
+ export declare function evaluatePoolBudget(cfg: PoolBudgetConfig): PoolBudgetEvaluation;
112
+ //# sourceMappingURL=app-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,SAAS;IACxB,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,WAAW,EAAE,aAAa,GAAG,MAAM,GAAG,YAAY,CAAC;IAC5D,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;CACjC;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC;CAC7C;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8CAA8C;IAC9C,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,wEAAwE;IACxE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACpC;AAED,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,EAAE,gBASjC,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAwB1C;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,gBAAgB,CAuBrF;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,SAAS,CAM7E;AAMD,mFAAmF;AACnF,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,+EAA+E;IAC/E,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,0EAA0E;IAC1E,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,0EAA0E;IAC1E,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;CAC1C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,gBAAgB,GAAG,oBAAoB,CAgB9E"}