@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.
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/authentication/argon2id-password-hasher.d.ts +61 -0
- package/dist/authentication/argon2id-password-hasher.d.ts.map +1 -0
- package/dist/authentication/argon2id-password-hasher.js +108 -0
- package/dist/boot/boot-sequence.d.ts +43 -0
- package/dist/boot/boot-sequence.d.ts.map +1 -0
- package/dist/boot/boot-sequence.js +90 -0
- package/dist/boot/boot.module.d.ts +8 -0
- package/dist/boot/boot.module.d.ts.map +1 -0
- package/dist/boot/boot.module.js +24 -0
- package/dist/config/app-config.d.ts +112 -0
- package/dist/config/app-config.d.ts.map +1 -0
- package/dist/config/app-config.js +94 -0
- package/dist/config/config.module.d.ts +12 -0
- package/dist/config/config.module.d.ts.map +1 -0
- package/dist/config/config.module.js +35 -0
- package/dist/enforcement/classification.decorator.d.ts +41 -0
- package/dist/enforcement/classification.decorator.d.ts.map +1 -0
- package/dist/enforcement/classification.decorator.js +46 -0
- package/dist/enforcement/core-seam.d.ts +40 -0
- package/dist/enforcement/core-seam.d.ts.map +1 -0
- package/dist/enforcement/core-seam.js +38 -0
- package/dist/enforcement/enforcement.module.d.ts +18 -0
- package/dist/enforcement/enforcement.module.d.ts.map +1 -0
- package/dist/enforcement/enforcement.module.js +40 -0
- package/dist/enforcement/global-enforcement.guard.d.ts +31 -0
- package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -0
- package/dist/enforcement/global-enforcement.guard.js +76 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +88 -0
- package/dist/module/shield.module.d.ts +79 -0
- package/dist/module/shield.module.d.ts.map +1 -0
- package/dist/module/shield.module.js +108 -0
- package/dist/session/crypto-adapters.d.ts +60 -0
- package/dist/session/crypto-adapters.d.ts.map +1 -0
- package/dist/session/crypto-adapters.js +80 -0
- package/dist/session/index.d.ts +17 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +32 -0
- package/dist/session/mariadb-session-record-store.d.ts +50 -0
- package/dist/session/mariadb-session-record-store.d.ts.map +1 -0
- package/dist/session/mariadb-session-record-store.js +162 -0
- package/dist/session/redis-hot-session-store.d.ts +30 -0
- package/dist/session/redis-hot-session-store.d.ts.map +1 -0
- package/dist/session/redis-hot-session-store.js +102 -0
- 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"}
|