@astram/guard 0.1.0 → 0.2.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/dist/auth/auth.controller.d.ts +102 -0
- package/dist/auth/auth.controller.d.ts.map +1 -0
- package/dist/auth/auth.controller.js +313 -0
- package/dist/auth/auth.module.d.ts +104 -0
- package/dist/auth/auth.module.d.ts.map +1 -0
- package/dist/auth/auth.module.js +318 -0
- package/dist/auth/connections.d.ts +61 -0
- package/dist/auth/connections.d.ts.map +1 -0
- package/dist/auth/connections.js +116 -0
- package/dist/auth/dto.d.ts +144 -0
- package/dist/auth/dto.d.ts.map +1 -0
- package/dist/auth/dto.js +29 -0
- package/dist/auth/pending-mfa-store.d.ts +72 -0
- package/dist/auth/pending-mfa-store.d.ts.map +1 -0
- package/dist/auth/pending-mfa-store.js +106 -0
- package/dist/auth/persistence.module.d.ts +42 -0
- package/dist/auth/persistence.module.d.ts.map +1 -0
- package/dist/auth/persistence.module.js +94 -0
- package/dist/auth/tokens.d.ts +62 -0
- package/dist/auth/tokens.d.ts.map +1 -0
- package/dist/auth/tokens.js +65 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts +96 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -0
- package/dist/authentication/mariadb-credential-resolver.js +156 -0
- package/dist/config/app-config.d.ts +134 -1
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +144 -2
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -1
- package/dist/module/shield.module.d.ts +51 -0
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +65 -4
- package/package.json +9 -9
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `MariaDbCredentialResolver` — the concrete {@link CredentialResolver} (core
|
|
4
|
+
* port) backed by the `shield_identity.identity_credential` table. Resolves an
|
|
5
|
+
* `identityHint` (username / email) → a {@link ResolvedCredential} the
|
|
6
|
+
* Authentication engine verifies, or `null` for an unknown hint.
|
|
7
|
+
*
|
|
8
|
+
* ⛔⛔ PHASE 0b PRELUDE SKELETON — the CONTRACT is frozen here; **Team A fills the
|
|
9
|
+
* body**. `resolve()` currently THROWS "not implemented" so a missing Team-A impl
|
|
10
|
+
* fails CLOSED (a broken resolver can NEVER silently authenticate). Do not change
|
|
11
|
+
* the constructor shape (it takes the `SHIELD_DB_POOL`) or the return type — the
|
|
12
|
+
* composition (Team B) and the engine depend on them.
|
|
13
|
+
*
|
|
14
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
* THE TABLE CONTRACT Team A must create (infra migration/seed
|
|
16
|
+
* `infra/mariadb/init/30-identity-credential.sql`, in `shield_identity`; the
|
|
17
|
+
* runtime `shield_core` account already has DML there — no new grant). Columns
|
|
18
|
+
* per spec §3 (prefix `ic_` to match the repo's session-table convention):
|
|
19
|
+
*
|
|
20
|
+
* identity_credential
|
|
21
|
+
* ┌────────────────────────┬──────────────┬────────────────────────────────────┐
|
|
22
|
+
* │ column │ type │ notes │
|
|
23
|
+
* ├────────────────────────┼──────────────┼────────────────────────────────────┤
|
|
24
|
+
* │ ic_identity_hint │ VARCHAR(255) │ UNIQUE — the login key (user/email)│
|
|
25
|
+
* │ ic_password_hash │ VARCHAR(255) │ Argon2id encoded hash (verify in) │
|
|
26
|
+
* │ ic_principal_id │ VARCHAR(255) │ → ResolvedCredential.claims.id │
|
|
27
|
+
* │ ic_tenant_id │ VARCHAR(255) │ → claims.tenantId │
|
|
28
|
+
* │ ic_roles │ JSON / TEXT │ role-id array → claims.roleIds │
|
|
29
|
+
* │ ic_totp_secret │ VARCHAR(255) │ NULLABLE — base32 TOTP secret; │
|
|
30
|
+
* │ │ │ present ⇒ requiredFactors:[totp] │
|
|
31
|
+
* │ ic_created_at_ms │ BIGINT │ UTC epoch ms (platform sets UTC) │
|
|
32
|
+
* │ ic_updated_at_ms │ BIGINT │ UTC epoch ms │
|
|
33
|
+
* └────────────────────────┴──────────────┴────────────────────────────────────┘
|
|
34
|
+
* PRIMARY KEY / UNIQUE on ic_identity_hint (single-row resolve by hint).
|
|
35
|
+
*
|
|
36
|
+
* The resolver's SELECT must:
|
|
37
|
+
* 1. `SELECT ic_password_hash, ic_principal_id, ic_tenant_id, ic_roles,
|
|
38
|
+
* ic_totp_secret FROM shield_identity.identity_credential
|
|
39
|
+
* WHERE ic_identity_hint = ? LIMIT 1` (parameterized — NEVER string-built).
|
|
40
|
+
* 2. Return `null` when NO row matches — the ONLY "not found" signal
|
|
41
|
+
* (anti-enumeration; MUST NOT throw for an unknown hint; the engine burns a
|
|
42
|
+
* decoy verify on `null` so timing is uniform).
|
|
43
|
+
* 3. Map a found row → `ResolvedCredential`:
|
|
44
|
+
* passwordHash = ic_password_hash
|
|
45
|
+
* claims = { id: ic_principal_id, tenantId: ic_tenant_id,
|
|
46
|
+
* roleIds: <parsed ic_roles> } // canonical Internal claims
|
|
47
|
+
* requiredFactors = ic_totp_secret ? [{ factor:'totp', secret: ic_totp_secret }] : []
|
|
48
|
+
* 4. NEVER log the hash, the TOTP secret, or the raw row.
|
|
49
|
+
* 5. A store outage (connection/query error) MAY throw — the engine treats a
|
|
50
|
+
* resolver throw as fail-closed `authentication_unavailable` (never a leak).
|
|
51
|
+
* Team A does NOT need to swallow it, but MUST NOT convert it into a `null`
|
|
52
|
+
* (that would make an outage look like "unknown user").
|
|
53
|
+
*
|
|
54
|
+
* (`claims.kind` is optional — the Internal adapter defaults an absent kind; add
|
|
55
|
+
* an `ic_kind` column later if a deployment needs non-`user` principals. Do NOT
|
|
56
|
+
* put any superuser flag in the row — superuser is the broker's server-side
|
|
57
|
+
* allowlist, never a claim, per core/identity.)
|
|
58
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
*
|
|
60
|
+
* ADR-0003: the concrete DB lives in guard (Core stays zero-dep). ADR-0013/0014:
|
|
61
|
+
* reads `shield_identity` via the hot-path `shield_core` account.
|
|
62
|
+
*/
|
|
63
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
64
|
+
exports.MariaDbCredentialResolver = exports.SHIELD_CREDENTIAL_RESOLVER = void 0;
|
|
65
|
+
/** DI token for the resolver — re-exported from the shared auth tokens module. */
|
|
66
|
+
var tokens_js_1 = require("../auth/tokens.js");
|
|
67
|
+
Object.defineProperty(exports, "SHIELD_CREDENTIAL_RESOLVER", { enumerable: true, get: function () { return tokens_js_1.SHIELD_CREDENTIAL_RESOLVER; } });
|
|
68
|
+
/**
|
|
69
|
+
* Normalize the stored `ic_roles` into a role-id string array. Accepts BOTH:
|
|
70
|
+
* - a native `string[]` (the mariadb `JSON`-column driver already parsed it);
|
|
71
|
+
* - a JSON string (defensive — a `TEXT` column / `autoJsonMap` disabled), which
|
|
72
|
+
* is `JSON.parse`d here.
|
|
73
|
+
*
|
|
74
|
+
* A malformed roles value is a DATA-INTEGRITY error, not an unknown user — so this
|
|
75
|
+
* THROWS (the engine treats a resolver throw as fail-closed
|
|
76
|
+
* `authentication_unavailable`) rather than silently granting an empty role set (a
|
|
77
|
+
* silent privilege change) OR crashing later. Anything that is not, after
|
|
78
|
+
* normalization, an array of strings is rejected the same way.
|
|
79
|
+
*/
|
|
80
|
+
function parseRoleIds(raw, hint) {
|
|
81
|
+
let value = raw;
|
|
82
|
+
if (typeof value === 'string') {
|
|
83
|
+
try {
|
|
84
|
+
value = JSON.parse(value);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Never include the raw blob in the message (defensive; it isn't a secret,
|
|
88
|
+
// but keep the no-detail-leak discipline uniform).
|
|
89
|
+
throw new Error(`MariaDbCredentialResolver: malformed ic_roles JSON for identity "${hint}" ` +
|
|
90
|
+
`(data-integrity error — failing closed rather than granting empty roles).`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!Array.isArray(value) || !value.every((r) => typeof r === 'string')) {
|
|
94
|
+
throw new Error(`MariaDbCredentialResolver: ic_roles for identity "${hint}" is not an array ` +
|
|
95
|
+
`of strings (data-integrity error — failing closed).`);
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The concrete MariaDB-backed {@link CredentialResolver}. Constructed with the
|
|
101
|
+
* live `shield_core` {@link Pool} (via {@link SHIELD_CREDENTIAL_RESOLVER}'s
|
|
102
|
+
* factory, which injects {@link SHIELD_DB_POOL}). Stateless per call apart from
|
|
103
|
+
* the injected pool.
|
|
104
|
+
*
|
|
105
|
+
* ⛔ SKELETON: `resolve()` throws until Team A implements the SELECT above.
|
|
106
|
+
*/
|
|
107
|
+
class MariaDbCredentialResolver {
|
|
108
|
+
pool;
|
|
109
|
+
constructor(pool) {
|
|
110
|
+
this.pool = pool;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Resolve `identityHint` → {@link ResolvedCredential}, or `null` when no such
|
|
114
|
+
* principal exists.
|
|
115
|
+
*
|
|
116
|
+
* - Parameterized SELECT (`WHERE ic_identity_hint = ? LIMIT 1`) — never
|
|
117
|
+
* string-built, so the hint can never inject SQL.
|
|
118
|
+
* - **No row ⇒ `null`** — the ONLY "not found" signal (anti-enumeration; the
|
|
119
|
+
* engine burns a decoy password verify on `null` so an unknown user is
|
|
120
|
+
* indistinguishable from a wrong password by content or timing). We do NOT
|
|
121
|
+
* throw for an unknown hint.
|
|
122
|
+
* - A **query/connection ERROR propagates** (rejects) — an outage is NOT an
|
|
123
|
+
* unknown user; the engine treats a resolver throw as fail-closed
|
|
124
|
+
* `authentication_unavailable`. We never convert an error into `null` (that
|
|
125
|
+
* would mask an outage AND make it look like "no such user").
|
|
126
|
+
* - The password hash and TOTP secret are NEVER logged; the raw row is never
|
|
127
|
+
* logged. On success they are returned to the engine as verify inputs only.
|
|
128
|
+
*/
|
|
129
|
+
async resolve(identityHint) {
|
|
130
|
+
// A failed query rejects out of here unchanged (fail-closed): we deliberately
|
|
131
|
+
// do NOT wrap this in a try/catch that returns null — only a genuine "no row"
|
|
132
|
+
// maps to null (below), never a store outage.
|
|
133
|
+
const rows = (await this.pool.query(`SELECT ic_password_hash, ic_principal_id, ic_tenant_id, ic_roles, ic_totp_secret
|
|
134
|
+
FROM shield_identity.identity_credential
|
|
135
|
+
WHERE ic_identity_hint = ?
|
|
136
|
+
LIMIT 1`, [identityHint]));
|
|
137
|
+
const row = rows[0];
|
|
138
|
+
// Unknown hint → null (anti-enumeration). MUST NOT throw here.
|
|
139
|
+
if (!row) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const requiredFactors = row.ic_totp_secret
|
|
143
|
+
? [{ factor: 'totp', secret: row.ic_totp_secret }]
|
|
144
|
+
: [];
|
|
145
|
+
return {
|
|
146
|
+
passwordHash: row.ic_password_hash,
|
|
147
|
+
claims: {
|
|
148
|
+
id: row.ic_principal_id,
|
|
149
|
+
tenantId: row.ic_tenant_id,
|
|
150
|
+
roleIds: parseRoleIds(row.ic_roles, identityHint),
|
|
151
|
+
},
|
|
152
|
+
requiredFactors,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
exports.MariaDbCredentialResolver = MariaDbCredentialResolver;
|
|
@@ -30,6 +30,76 @@ export interface AppConfig {
|
|
|
30
30
|
readonly environment: 'development' | 'test' | 'production';
|
|
31
31
|
/** ADR-0018 connection-pool budget (validated at boot, fail-closed). */
|
|
32
32
|
readonly pool: PoolBudgetConfig;
|
|
33
|
+
/**
|
|
34
|
+
* The runtime MariaDB connection descriptor (the `shield_core` account — DML on
|
|
35
|
+
* `shield_identity` where the session + credential tables live). Consumed by
|
|
36
|
+
* the Phase 0b persistence provider that creates the ONE live `mariadb` Pool.
|
|
37
|
+
*/
|
|
38
|
+
readonly db: DbConfig;
|
|
39
|
+
/**
|
|
40
|
+
* The Redis (hot session tier, ADR-0013) connection descriptor. Consumed by the
|
|
41
|
+
* Phase 0b persistence provider that creates the ONE live `ioredis` client.
|
|
42
|
+
*/
|
|
43
|
+
readonly redis: RedisConfig;
|
|
44
|
+
/**
|
|
45
|
+
* The keyed-HMAC secret for the refresh-token hasher (`SHIELD_REFRESH_HMAC_KEY`).
|
|
46
|
+
* ⛔ REQUIRED FOR AUTH — this is a SECRET (a leaked DB would be offline-brute-
|
|
47
|
+
* forceable without it). {@link loadAppConfig} reads it but does NOT throw when
|
|
48
|
+
* blank, so the ENFORCEMENT-ONLY scaffold (example-backend / a freshly-scaffolded
|
|
49
|
+
* app with no auth wired) still boots with no `.env`. The FAIL-CLOSED gate is
|
|
50
|
+
* {@link assertRefreshHmacKey}, invoked by the auth composition (AuthModule /
|
|
51
|
+
* persistence factory) at the moment auth is actually wired — so a blank key can
|
|
52
|
+
* never produce a working-looking authenticator, while a non-auth app is
|
|
53
|
+
* unaffected. Never logged, never returned to a client.
|
|
54
|
+
*/
|
|
55
|
+
readonly refreshHmacKey: string;
|
|
56
|
+
/**
|
|
57
|
+
* Optional session/token lifetime overrides (ADR-0015). Any omitted field
|
|
58
|
+
* defaults in `@astram/core`'s `resolveLifetimes` to 15 min access / 14 d idle /
|
|
59
|
+
* 60 d absolute cap. `Partial` so the composition can hand it straight to
|
|
60
|
+
* `createSessionAuthority({ lifetimes })`.
|
|
61
|
+
*/
|
|
62
|
+
readonly lifetimes: SessionLifetimeOverrides;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The runtime MariaDB connection descriptor. Non-secret host/port/user/database
|
|
66
|
+
* plus the account password (a secret — supplied via env/secrets-manager, NEVER
|
|
67
|
+
* committed). Maps 1:1 onto `mariadb`'s `createPool({ host, port, user, password,
|
|
68
|
+
* database, connectionLimit })`. The pool cap comes from the ADR-0018 budget
|
|
69
|
+
* (`pool.perAccountMax.shield_core`), NOT duplicated here.
|
|
70
|
+
*/
|
|
71
|
+
export interface DbConfig {
|
|
72
|
+
readonly host: string;
|
|
73
|
+
readonly port: number;
|
|
74
|
+
readonly user: string;
|
|
75
|
+
/** ⛔ Secret — resolved from env/secrets-manager; never logged. */
|
|
76
|
+
readonly password: string;
|
|
77
|
+
readonly database: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The Redis connection descriptor. A single `url` (e.g. `redis://host:6379`) is
|
|
81
|
+
* the canonical form `ioredis` accepts directly; host/port are provided as a
|
|
82
|
+
* convenience fallback the persistence provider composes into a url when `url` is
|
|
83
|
+
* absent.
|
|
84
|
+
*/
|
|
85
|
+
export interface RedisConfig {
|
|
86
|
+
/** Full connection url (`redis://[:password@]host:port[/db]`). Preferred. */
|
|
87
|
+
readonly url: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Optional per-deployment overrides for the three ADR-0015 session lifetimes.
|
|
91
|
+
* Shape mirrors `@astram/core`'s `Partial<SessionLifetimeConfig>` so it is passed
|
|
92
|
+
* straight into `createSessionAuthority({ lifetimes })` — omitted fields take the
|
|
93
|
+
* core defaults. Kept as a local shape (not a core import) so this config file
|
|
94
|
+
* stays a plain env-reader; the composition owns the core wiring.
|
|
95
|
+
*/
|
|
96
|
+
export interface SessionLifetimeOverrides {
|
|
97
|
+
/** Access-token TTL in ms (core default 15 min). Omitted ⇒ core default. */
|
|
98
|
+
readonly accessTtlMs?: number;
|
|
99
|
+
/** Sliding refresh idle window in ms (core default 14 days). */
|
|
100
|
+
readonly refreshIdleMs?: number;
|
|
101
|
+
/** Absolute session cap in ms (core default 60 days). */
|
|
102
|
+
readonly refreshAbsoluteCapMs?: number;
|
|
33
103
|
}
|
|
34
104
|
/**
|
|
35
105
|
* The ADR-0018 connection-pool budget surface. Every field is configurable per
|
|
@@ -84,9 +154,72 @@ export declare const SCHEMA_SVC_POOL_HARD_CAP = 2;
|
|
|
84
154
|
* perAccountMax.shield_schema_svc ← SHIELD_POOL_MAX_SCHEMA_SVC
|
|
85
155
|
*/
|
|
86
156
|
export declare function loadPoolBudget(env?: NodeJS.ProcessEnv): PoolBudgetConfig;
|
|
157
|
+
/** Documented defaults for the runtime MariaDB descriptor (local docker substrate). */
|
|
158
|
+
export declare const DEFAULT_DB_CONFIG: Omit<DbConfig, 'password'>;
|
|
159
|
+
/** Documented default Redis url (local docker substrate). */
|
|
160
|
+
export declare const DEFAULT_REDIS_URL = "redis://localhost:6379";
|
|
161
|
+
/**
|
|
162
|
+
* Build the runtime {@link DbConfig} from env with documented defaults.
|
|
163
|
+
* Field ↔ env name:
|
|
164
|
+
* host ← SHIELD_DB_HOST port ← SHIELD_DB_PORT
|
|
165
|
+
* user ← SHIELD_DB_USER password ← SHIELD_DB_PASSWORD (secret)
|
|
166
|
+
* database ← SHIELD_DB_NAME
|
|
167
|
+
* The password defaults to '' when absent — a live connection then fails at
|
|
168
|
+
* connect time (loud), which is correct for a runtime that needs real creds; the
|
|
169
|
+
* secret is deliberately NOT hard-required at config load (unlike the HMAC key,
|
|
170
|
+
* which is a fail-closed security invariant even for unit tests).
|
|
171
|
+
*/
|
|
172
|
+
export declare function loadDbConfig(env?: NodeJS.ProcessEnv): DbConfig;
|
|
173
|
+
/**
|
|
174
|
+
* Build the {@link RedisConfig} from env. Prefers `SHIELD_REDIS_URL`; falls back
|
|
175
|
+
* to composing `redis://host:port` from `SHIELD_REDIS_HOST`/`SHIELD_REDIS_PORT`,
|
|
176
|
+
* else the documented default.
|
|
177
|
+
*/
|
|
178
|
+
export declare function loadRedisConfig(env?: NodeJS.ProcessEnv): RedisConfig;
|
|
179
|
+
/**
|
|
180
|
+
* Build the optional {@link SessionLifetimeOverrides} from env. Every field is
|
|
181
|
+
* optional (omitted ⇒ core ADR-0015 default). The bounds validation is the
|
|
182
|
+
* core's `resolveLifetimes` job (throws LOUD at composition) — this loader only
|
|
183
|
+
* reads the numbers.
|
|
184
|
+
* Field ↔ env name:
|
|
185
|
+
* accessTtlMs ← SHIELD_ACCESS_TTL_MS
|
|
186
|
+
* refreshIdleMs ← SHIELD_REFRESH_IDLE_MS
|
|
187
|
+
* refreshAbsoluteCapMs ← SHIELD_REFRESH_ABSOLUTE_CAP_MS
|
|
188
|
+
*/
|
|
189
|
+
export declare function loadSessionLifetimes(env?: NodeJS.ProcessEnv): SessionLifetimeOverrides;
|
|
190
|
+
/**
|
|
191
|
+
* Read the refresh-token HMAC key from env (raw; `''` when absent/blank). Total —
|
|
192
|
+
* does NOT throw, so {@link loadAppConfig} stays boot-safe for a non-auth scaffold.
|
|
193
|
+
* The FAIL-CLOSED enforcement lives in {@link assertRefreshHmacKey}. Never logs
|
|
194
|
+
* the key's value.
|
|
195
|
+
*/
|
|
196
|
+
export declare function loadRefreshHmacKey(env?: NodeJS.ProcessEnv): string;
|
|
197
|
+
/**
|
|
198
|
+
* ⛔ FAIL-CLOSED GATE for the refresh-token HMAC key. Called by the auth
|
|
199
|
+
* composition (AuthModule / persistence factory) at the moment auth is wired:
|
|
200
|
+
* throws LOUD when the key is blank so the app can NEVER compose a working-looking
|
|
201
|
+
* authenticator/session-authority on an empty HMAC key (a leaked DB would then be
|
|
202
|
+
* offline-brute-forceable, and every refresh-token hash would be unkeyed). Mirrors
|
|
203
|
+
* `Sha256RefreshTokenHasher`'s constructor guard but surfaces a clear composition
|
|
204
|
+
* error. Returns the validated non-empty key. NEVER logs the key's value.
|
|
205
|
+
*
|
|
206
|
+
* WHY here and not in `loadAppConfig`: an enforcement-only app (example-backend, a
|
|
207
|
+
* freshly-scaffolded app with no auth wired) must still boot with no `.env`
|
|
208
|
+
* (`create-astram-app/template/_env.example` promises exactly this). Gating at the
|
|
209
|
+
* auth boundary keeps that promise while making auth fail closed the instant it is
|
|
210
|
+
* actually composed.
|
|
211
|
+
*/
|
|
212
|
+
export declare function assertRefreshHmacKey(key: string): string;
|
|
87
213
|
/**
|
|
88
214
|
* Build the {@link AppConfig} from `process.env` (or an injected env bag, for
|
|
89
|
-
* testability). Pure
|
|
215
|
+
* testability). Pure, total, and side-effect-free — trivially unit-testable and
|
|
216
|
+
* boot-safe with NO `.env`.
|
|
217
|
+
*
|
|
218
|
+
* Every Phase 0b field has a documented default (DB/Redis descriptors, lifetimes)
|
|
219
|
+
* and `refreshHmacKey` is read-but-not-required HERE, so existing callers
|
|
220
|
+
* (example-backend / template `main.ts`, which read only `.port`) are unaffected.
|
|
221
|
+
* The refresh-key fail-closed check is deferred to {@link assertRefreshHmacKey} at
|
|
222
|
+
* auth-composition time (see its doc).
|
|
90
223
|
*/
|
|
91
224
|
export declare function loadAppConfig(env?: NodeJS.ProcessEnv): AppConfig;
|
|
92
225
|
/** Result of evaluating a {@link PoolBudgetConfig} against the ADR-0018 budget. */
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SCHEMA_SVC_POOL_HARD_CAP = exports.DEFAULT_POOL_BUDGET = void 0;
|
|
3
|
+
exports.DEFAULT_REDIS_URL = exports.DEFAULT_DB_CONFIG = exports.SCHEMA_SVC_POOL_HARD_CAP = exports.DEFAULT_POOL_BUDGET = void 0;
|
|
4
4
|
exports.loadPoolBudget = loadPoolBudget;
|
|
5
|
+
exports.loadDbConfig = loadDbConfig;
|
|
6
|
+
exports.loadRedisConfig = loadRedisConfig;
|
|
7
|
+
exports.loadSessionLifetimes = loadSessionLifetimes;
|
|
8
|
+
exports.loadRefreshHmacKey = loadRefreshHmacKey;
|
|
9
|
+
exports.assertRefreshHmacKey = assertRefreshHmacKey;
|
|
5
10
|
exports.loadAppConfig = loadAppConfig;
|
|
6
11
|
exports.evaluatePoolBudget = evaluatePoolBudget;
|
|
7
12
|
/** Documented MVP single-node defaults (see the class doc + my.cnf). */
|
|
@@ -61,15 +66,152 @@ function loadPoolBudget(env = process.env) {
|
|
|
61
66
|
},
|
|
62
67
|
};
|
|
63
68
|
}
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Phase 0b Auth-surface config loaders (additive) — DB / Redis / secret /
|
|
71
|
+
// lifetimes. All pure env-readers with documented defaults; the ONLY hard-
|
|
72
|
+
// required value is the refresh HMAC key (fail-closed).
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
/** Documented defaults for the runtime MariaDB descriptor (local docker substrate). */
|
|
75
|
+
exports.DEFAULT_DB_CONFIG = {
|
|
76
|
+
host: 'localhost',
|
|
77
|
+
port: 3306,
|
|
78
|
+
user: 'shield_core',
|
|
79
|
+
database: 'shield_identity',
|
|
80
|
+
};
|
|
81
|
+
/** Documented default Redis url (local docker substrate). */
|
|
82
|
+
exports.DEFAULT_REDIS_URL = 'redis://localhost:6379';
|
|
83
|
+
/** Read a string from env with a fallback (trims; blank ⇒ fallback). */
|
|
84
|
+
function parseString(raw, fallback) {
|
|
85
|
+
if (raw === undefined || raw.trim() === '')
|
|
86
|
+
return fallback;
|
|
87
|
+
return raw.trim();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Read an OPTIONAL positive-int env override, or `undefined` when absent/blank/
|
|
91
|
+
* invalid — so an omitted lifetime falls through to the core default rather than
|
|
92
|
+
* being coerced to a bogus number. (Distinct from {@link parsePositiveInt}, which
|
|
93
|
+
* always returns a concrete fallback for the pool budget.)
|
|
94
|
+
*/
|
|
95
|
+
function parseOptionalPositiveInt(raw) {
|
|
96
|
+
if (raw === undefined || raw.trim() === '')
|
|
97
|
+
return undefined;
|
|
98
|
+
const n = Number.parseInt(raw, 10);
|
|
99
|
+
return Number.isInteger(n) && n > 0 ? n : undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Build the runtime {@link DbConfig} from env with documented defaults.
|
|
103
|
+
* Field ↔ env name:
|
|
104
|
+
* host ← SHIELD_DB_HOST port ← SHIELD_DB_PORT
|
|
105
|
+
* user ← SHIELD_DB_USER password ← SHIELD_DB_PASSWORD (secret)
|
|
106
|
+
* database ← SHIELD_DB_NAME
|
|
107
|
+
* The password defaults to '' when absent — a live connection then fails at
|
|
108
|
+
* connect time (loud), which is correct for a runtime that needs real creds; the
|
|
109
|
+
* secret is deliberately NOT hard-required at config load (unlike the HMAC key,
|
|
110
|
+
* which is a fail-closed security invariant even for unit tests).
|
|
111
|
+
*/
|
|
112
|
+
function loadDbConfig(env = process.env) {
|
|
113
|
+
return {
|
|
114
|
+
host: parseString(env.SHIELD_DB_HOST, exports.DEFAULT_DB_CONFIG.host),
|
|
115
|
+
port: parsePort(env.SHIELD_DB_PORT, exports.DEFAULT_DB_CONFIG.port),
|
|
116
|
+
user: parseString(env.SHIELD_DB_USER, exports.DEFAULT_DB_CONFIG.user),
|
|
117
|
+
password: parseString(env.SHIELD_DB_PASSWORD, ''),
|
|
118
|
+
database: parseString(env.SHIELD_DB_NAME, exports.DEFAULT_DB_CONFIG.database),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build the {@link RedisConfig} from env. Prefers `SHIELD_REDIS_URL`; falls back
|
|
123
|
+
* to composing `redis://host:port` from `SHIELD_REDIS_HOST`/`SHIELD_REDIS_PORT`,
|
|
124
|
+
* else the documented default.
|
|
125
|
+
*/
|
|
126
|
+
function loadRedisConfig(env = process.env) {
|
|
127
|
+
const explicit = env.SHIELD_REDIS_URL;
|
|
128
|
+
if (explicit !== undefined && explicit.trim() !== '') {
|
|
129
|
+
return { url: explicit.trim() };
|
|
130
|
+
}
|
|
131
|
+
const host = env.SHIELD_REDIS_HOST;
|
|
132
|
+
const port = env.SHIELD_REDIS_PORT;
|
|
133
|
+
if ((host !== undefined && host.trim() !== '') || (port !== undefined && port.trim() !== '')) {
|
|
134
|
+
const h = parseString(host, 'localhost');
|
|
135
|
+
const p = parsePort(port, 6379);
|
|
136
|
+
return { url: `redis://${h}:${p}` };
|
|
137
|
+
}
|
|
138
|
+
return { url: exports.DEFAULT_REDIS_URL };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Build the optional {@link SessionLifetimeOverrides} from env. Every field is
|
|
142
|
+
* optional (omitted ⇒ core ADR-0015 default). The bounds validation is the
|
|
143
|
+
* core's `resolveLifetimes` job (throws LOUD at composition) — this loader only
|
|
144
|
+
* reads the numbers.
|
|
145
|
+
* Field ↔ env name:
|
|
146
|
+
* accessTtlMs ← SHIELD_ACCESS_TTL_MS
|
|
147
|
+
* refreshIdleMs ← SHIELD_REFRESH_IDLE_MS
|
|
148
|
+
* refreshAbsoluteCapMs ← SHIELD_REFRESH_ABSOLUTE_CAP_MS
|
|
149
|
+
*/
|
|
150
|
+
function loadSessionLifetimes(env = process.env) {
|
|
151
|
+
const overrides = {};
|
|
152
|
+
const access = parseOptionalPositiveInt(env.SHIELD_ACCESS_TTL_MS);
|
|
153
|
+
const idle = parseOptionalPositiveInt(env.SHIELD_REFRESH_IDLE_MS);
|
|
154
|
+
const cap = parseOptionalPositiveInt(env.SHIELD_REFRESH_ABSOLUTE_CAP_MS);
|
|
155
|
+
if (access !== undefined)
|
|
156
|
+
overrides.accessTtlMs = access;
|
|
157
|
+
if (idle !== undefined)
|
|
158
|
+
overrides.refreshIdleMs = idle;
|
|
159
|
+
if (cap !== undefined)
|
|
160
|
+
overrides.refreshAbsoluteCapMs = cap;
|
|
161
|
+
return overrides;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read the refresh-token HMAC key from env (raw; `''` when absent/blank). Total —
|
|
165
|
+
* does NOT throw, so {@link loadAppConfig} stays boot-safe for a non-auth scaffold.
|
|
166
|
+
* The FAIL-CLOSED enforcement lives in {@link assertRefreshHmacKey}. Never logs
|
|
167
|
+
* the key's value.
|
|
168
|
+
*/
|
|
169
|
+
function loadRefreshHmacKey(env = process.env) {
|
|
170
|
+
return parseString(env.SHIELD_REFRESH_HMAC_KEY, '');
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* ⛔ FAIL-CLOSED GATE for the refresh-token HMAC key. Called by the auth
|
|
174
|
+
* composition (AuthModule / persistence factory) at the moment auth is wired:
|
|
175
|
+
* throws LOUD when the key is blank so the app can NEVER compose a working-looking
|
|
176
|
+
* authenticator/session-authority on an empty HMAC key (a leaked DB would then be
|
|
177
|
+
* offline-brute-forceable, and every refresh-token hash would be unkeyed). Mirrors
|
|
178
|
+
* `Sha256RefreshTokenHasher`'s constructor guard but surfaces a clear composition
|
|
179
|
+
* error. Returns the validated non-empty key. NEVER logs the key's value.
|
|
180
|
+
*
|
|
181
|
+
* WHY here and not in `loadAppConfig`: an enforcement-only app (example-backend, a
|
|
182
|
+
* freshly-scaffolded app with no auth wired) must still boot with no `.env`
|
|
183
|
+
* (`create-astram-app/template/_env.example` promises exactly this). Gating at the
|
|
184
|
+
* auth boundary keeps that promise while making auth fail closed the instant it is
|
|
185
|
+
* actually composed.
|
|
186
|
+
*/
|
|
187
|
+
function assertRefreshHmacKey(key) {
|
|
188
|
+
if (typeof key !== 'string' || key.trim() === '') {
|
|
189
|
+
throw new Error('Shield auth composition: SHIELD_REFRESH_HMAC_KEY is required and must be ' +
|
|
190
|
+
'non-empty (fail-closed — refusing to compose auth with a blank refresh-token ' +
|
|
191
|
+
'HMAC key). Set it from your secrets manager; never commit the value.');
|
|
192
|
+
}
|
|
193
|
+
return key;
|
|
194
|
+
}
|
|
64
195
|
/**
|
|
65
196
|
* Build the {@link AppConfig} from `process.env` (or an injected env bag, for
|
|
66
|
-
* testability). Pure
|
|
197
|
+
* testability). Pure, total, and side-effect-free — trivially unit-testable and
|
|
198
|
+
* boot-safe with NO `.env`.
|
|
199
|
+
*
|
|
200
|
+
* Every Phase 0b field has a documented default (DB/Redis descriptors, lifetimes)
|
|
201
|
+
* and `refreshHmacKey` is read-but-not-required HERE, so existing callers
|
|
202
|
+
* (example-backend / template `main.ts`, which read only `.port`) are unaffected.
|
|
203
|
+
* The refresh-key fail-closed check is deferred to {@link assertRefreshHmacKey} at
|
|
204
|
+
* auth-composition time (see its doc).
|
|
67
205
|
*/
|
|
68
206
|
function loadAppConfig(env = process.env) {
|
|
69
207
|
return {
|
|
70
208
|
port: parsePort(env.PORT, 3000),
|
|
71
209
|
environment: parseEnvironment(env.NODE_ENV),
|
|
72
210
|
pool: loadPoolBudget(env),
|
|
211
|
+
db: loadDbConfig(env),
|
|
212
|
+
redis: loadRedisConfig(env),
|
|
213
|
+
refreshHmacKey: loadRefreshHmacKey(env),
|
|
214
|
+
lifetimes: loadSessionLifetimes(env),
|
|
73
215
|
};
|
|
74
216
|
}
|
|
75
217
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -22,5 +22,13 @@ export { Argon2idPasswordHasher, argon2idHasherProvider, SHIELD_PASSWORD_HASHER,
|
|
|
22
22
|
export { MariaDbSessionRecordStore, SHIELD_SESSION_RECORD_STORE, RedisHotSessionStore, SHIELD_HOT_SESSION_STORE, CryptoTokenGenerator, Sha256RefreshTokenHasher, SystemClock, cryptoTokenGeneratorProvider, refreshHasherProvider, systemClockProvider, SHIELD_TOKEN_GENERATOR, SHIELD_REFRESH_HASHER, SHIELD_CLOCK, } from './session/index.js';
|
|
23
23
|
export { BootSequence, assertPoolBudget } from './boot/boot-sequence.js';
|
|
24
24
|
export { APP_CONFIG } from './config/config.module.js';
|
|
25
|
-
export { loadAppConfig, loadPoolBudget, evaluatePoolBudget, DEFAULT_POOL_BUDGET, SCHEMA_SVC_POOL_HARD_CAP, type AppConfig, type PoolBudgetConfig, type PoolBudgetEvaluation, type RuntimeAccountPools, } from './config/app-config.js';
|
|
25
|
+
export { loadAppConfig, loadPoolBudget, evaluatePoolBudget, DEFAULT_POOL_BUDGET, SCHEMA_SVC_POOL_HARD_CAP, type AppConfig, type PoolBudgetConfig, type PoolBudgetEvaluation, type RuntimeAccountPools, loadDbConfig, loadRedisConfig, loadSessionLifetimes, loadRefreshHmacKey, assertRefreshHmacKey, DEFAULT_DB_CONFIG, DEFAULT_REDIS_URL, type DbConfig, type RedisConfig, type SessionLifetimeOverrides, } from './config/app-config.js';
|
|
26
|
+
export { SHIELD_DB_POOL, SHIELD_REDIS, SHIELD_CREDENTIAL_RESOLVER, SHIELD_AUTHENTICATOR, SHIELD_SESSION_AUTHORITY, } from './auth/tokens.js';
|
|
27
|
+
export { MariaDbCredentialResolver } from './authentication/mariadb-credential-resolver.js';
|
|
28
|
+
export type { SessionView, SessionTokensView, SessionSuccess, LoginRequest, MfaChallengeResponse, LoginResponse, MfaRequest, MfaResponse, RefreshRequest, RefreshResponse, LogoutRequest, LogoutAck, } from './auth/dto.js';
|
|
29
|
+
export { PersistenceModule } from './auth/persistence.module.js';
|
|
30
|
+
export { AuthModule, type AuthModuleOptions } from './auth/auth.module.js';
|
|
31
|
+
export type { CredentialResolver, ResolvedCredential, RequiredFactor, } from '@astram/core';
|
|
32
|
+
export { AuthController } from './auth/auth.controller.js';
|
|
33
|
+
export { PendingMfaStore } from './auth/pending-mfa-store.js';
|
|
26
34
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EACL,YAAY,EACZ,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,GAC9B,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EACL,MAAM,EACN,UAAU,EACV,uBAAuB,GACxB,MAAM,2CAA2C,CAAC;AAUnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,2CAA2C,CAAC;AAKnF,OAAO,EACL,qBAAqB,EACrB,KAAK,MAAM,EACX,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,KAAK,SAAS,GACf,MAAM,4BAA4B,CAAC;AAMpC,OAAO,EACL,sBAAsB,EACtB,sBAAsB,EACtB,sBAAsB,EACtB,KAAK,cAAc,GACpB,MAAM,8CAA8C,CAAC;AAOtD,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,oBAAoB,EACpB,wBAAwB,EACxB,oBAAoB,EACpB,wBAAwB,EACxB,WAAW,EACX,4BAA4B,EAC5B,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAOzE,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EACL,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EACL,YAAY,EACZ,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,GAC9B,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EACL,MAAM,EACN,UAAU,EACV,uBAAuB,GACxB,MAAM,2CAA2C,CAAC;AAUnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,2CAA2C,CAAC;AAKnF,OAAO,EACL,qBAAqB,EACrB,KAAK,MAAM,EACX,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,KAAK,SAAS,GACf,MAAM,4BAA4B,CAAC;AAMpC,OAAO,EACL,sBAAsB,EACtB,sBAAsB,EACtB,sBAAsB,EACtB,KAAK,cAAc,GACpB,MAAM,8CAA8C,CAAC;AAOtD,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,oBAAoB,EACpB,wBAAwB,EACxB,oBAAoB,EACpB,wBAAwB,EACxB,WAAW,EACX,4BAA4B,EAC5B,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAOzE,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EACL,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EAGxB,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,wBAAwB,GAC9B,MAAM,wBAAwB,CAAC;AAchC,OAAO,EACL,cAAc,EACd,YAAY,EACZ,0BAA0B,EAC1B,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,yBAAyB,EAAE,MAAM,iDAAiD,CAAC;AAI5F,YAAY,EACV,WAAW,EACX,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,aAAa,EACb,UAAU,EACV,WAAW,EACX,cAAc,EACd,eAAe,EACf,aAAa,EACb,SAAS,GACV,MAAM,eAAe,CAAC;AAKvB,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAS3E,YAAY,EACV,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,GACf,MAAM,cAAc,CAAC;AAOtB,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* `@nestjs/core`, `reflect-metadata`, `rxjs` so the host owns one copy of each.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.SCHEMA_SVC_POOL_HARD_CAP = exports.DEFAULT_POOL_BUDGET = exports.evaluatePoolBudget = exports.loadPoolBudget = exports.loadAppConfig = exports.APP_CONFIG = exports.assertPoolBudget = exports.BootSequence = exports.SHIELD_CLOCK = exports.SHIELD_REFRESH_HASHER = exports.SHIELD_TOKEN_GENERATOR = exports.systemClockProvider = exports.refreshHasherProvider = exports.cryptoTokenGeneratorProvider = exports.SystemClock = exports.Sha256RefreshTokenHasher = exports.CryptoTokenGenerator = exports.SHIELD_HOT_SESSION_STORE = exports.RedisHotSessionStore = exports.SHIELD_SESSION_RECORD_STORE = exports.MariaDbSessionRecordStore = exports.SHIELD_PASSWORD_HASHER = exports.argon2idHasherProvider = exports.Argon2idPasswordHasher = exports.stubDecisionAuthority = exports.GlobalEnforcementGuard = exports.CLASSIFICATION_METADATA = exports.Classified = exports.Public = exports.SHIELD_DECISION_AUTHORITY = exports.ShieldModule = void 0;
|
|
19
|
+
exports.PendingMfaStore = exports.AuthController = exports.AuthModule = exports.PersistenceModule = exports.MariaDbCredentialResolver = exports.SHIELD_SESSION_AUTHORITY = exports.SHIELD_AUTHENTICATOR = exports.SHIELD_CREDENTIAL_RESOLVER = exports.SHIELD_REDIS = exports.SHIELD_DB_POOL = exports.DEFAULT_REDIS_URL = exports.DEFAULT_DB_CONFIG = exports.assertRefreshHmacKey = exports.loadRefreshHmacKey = exports.loadSessionLifetimes = exports.loadRedisConfig = exports.loadDbConfig = exports.SCHEMA_SVC_POOL_HARD_CAP = exports.DEFAULT_POOL_BUDGET = exports.evaluatePoolBudget = exports.loadPoolBudget = exports.loadAppConfig = exports.APP_CONFIG = exports.assertPoolBudget = exports.BootSequence = exports.SHIELD_CLOCK = exports.SHIELD_REFRESH_HASHER = exports.SHIELD_TOKEN_GENERATOR = exports.systemClockProvider = exports.refreshHasherProvider = exports.cryptoTokenGeneratorProvider = exports.SystemClock = exports.Sha256RefreshTokenHasher = exports.CryptoTokenGenerator = exports.SHIELD_HOT_SESSION_STORE = exports.RedisHotSessionStore = exports.SHIELD_SESSION_RECORD_STORE = exports.MariaDbSessionRecordStore = exports.SHIELD_PASSWORD_HASHER = exports.argon2idHasherProvider = exports.Argon2idPasswordHasher = exports.stubDecisionAuthority = exports.GlobalEnforcementGuard = exports.CLASSIFICATION_METADATA = exports.Classified = exports.Public = exports.SHIELD_DECISION_AUTHORITY = exports.ShieldModule = void 0;
|
|
20
20
|
// The single registration seam.
|
|
21
21
|
var shield_module_js_1 = require("./module/shield.module.js");
|
|
22
22
|
Object.defineProperty(exports, "ShieldModule", { enumerable: true, get: function () { return shield_module_js_1.ShieldModule; } });
|
|
@@ -86,3 +86,48 @@ Object.defineProperty(exports, "loadPoolBudget", { enumerable: true, get: functi
|
|
|
86
86
|
Object.defineProperty(exports, "evaluatePoolBudget", { enumerable: true, get: function () { return app_config_js_1.evaluatePoolBudget; } });
|
|
87
87
|
Object.defineProperty(exports, "DEFAULT_POOL_BUDGET", { enumerable: true, get: function () { return app_config_js_1.DEFAULT_POOL_BUDGET; } });
|
|
88
88
|
Object.defineProperty(exports, "SCHEMA_SVC_POOL_HARD_CAP", { enumerable: true, get: function () { return app_config_js_1.SCHEMA_SVC_POOL_HARD_CAP; } });
|
|
89
|
+
// Phase 0b Auth-surface config (additive): the DB/Redis/lifetime loaders + the
|
|
90
|
+
// required-secret resolver/gate + the new config shapes.
|
|
91
|
+
Object.defineProperty(exports, "loadDbConfig", { enumerable: true, get: function () { return app_config_js_1.loadDbConfig; } });
|
|
92
|
+
Object.defineProperty(exports, "loadRedisConfig", { enumerable: true, get: function () { return app_config_js_1.loadRedisConfig; } });
|
|
93
|
+
Object.defineProperty(exports, "loadSessionLifetimes", { enumerable: true, get: function () { return app_config_js_1.loadSessionLifetimes; } });
|
|
94
|
+
Object.defineProperty(exports, "loadRefreshHmacKey", { enumerable: true, get: function () { return app_config_js_1.loadRefreshHmacKey; } });
|
|
95
|
+
Object.defineProperty(exports, "assertRefreshHmacKey", { enumerable: true, get: function () { return app_config_js_1.assertRefreshHmacKey; } });
|
|
96
|
+
Object.defineProperty(exports, "DEFAULT_DB_CONFIG", { enumerable: true, get: function () { return app_config_js_1.DEFAULT_DB_CONFIG; } });
|
|
97
|
+
Object.defineProperty(exports, "DEFAULT_REDIS_URL", { enumerable: true, get: function () { return app_config_js_1.DEFAULT_REDIS_URL; } });
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Phase 0b Auth surface (framework-owned login → session, spec §2/§3/§4).
|
|
100
|
+
//
|
|
101
|
+
// PRELUDE CONTRACT (this commit): the frozen DI tokens, endpoint DTO shapes, the
|
|
102
|
+
// credential-resolver skeleton (Team A fills), and the composition module
|
|
103
|
+
// skeletons (Team B fills). Team C's `/auth/*` controllers consume
|
|
104
|
+
// SHIELD_AUTHENTICATOR / SHIELD_SESSION_AUTHORITY. All ADDITIVE — the enforcement
|
|
105
|
+
// path is untouched; ShieldModule wiring is deferred to the integration owner.
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Shared DI provider tokens (the seams the teams bind/consume — see tokens.ts for
|
|
108
|
+
// who provides/consumes each).
|
|
109
|
+
var tokens_js_1 = require("./auth/tokens.js");
|
|
110
|
+
Object.defineProperty(exports, "SHIELD_DB_POOL", { enumerable: true, get: function () { return tokens_js_1.SHIELD_DB_POOL; } });
|
|
111
|
+
Object.defineProperty(exports, "SHIELD_REDIS", { enumerable: true, get: function () { return tokens_js_1.SHIELD_REDIS; } });
|
|
112
|
+
Object.defineProperty(exports, "SHIELD_CREDENTIAL_RESOLVER", { enumerable: true, get: function () { return tokens_js_1.SHIELD_CREDENTIAL_RESOLVER; } });
|
|
113
|
+
Object.defineProperty(exports, "SHIELD_AUTHENTICATOR", { enumerable: true, get: function () { return tokens_js_1.SHIELD_AUTHENTICATOR; } });
|
|
114
|
+
Object.defineProperty(exports, "SHIELD_SESSION_AUTHORITY", { enumerable: true, get: function () { return tokens_js_1.SHIELD_SESSION_AUTHORITY; } });
|
|
115
|
+
// The credential-store contract (skeleton — Team A implements the SELECT).
|
|
116
|
+
var mariadb_credential_resolver_js_1 = require("./authentication/mariadb-credential-resolver.js");
|
|
117
|
+
Object.defineProperty(exports, "MariaDbCredentialResolver", { enumerable: true, get: function () { return mariadb_credential_resolver_js_1.MariaDbCredentialResolver; } });
|
|
118
|
+
// The composition module skeletons (Team B fills the factory bodies; the
|
|
119
|
+
// integration owner imports AuthModule into ShieldModule — see auth.module.ts /
|
|
120
|
+
// DONE.md).
|
|
121
|
+
var persistence_module_js_1 = require("./auth/persistence.module.js");
|
|
122
|
+
Object.defineProperty(exports, "PersistenceModule", { enumerable: true, get: function () { return persistence_module_js_1.PersistenceModule; } });
|
|
123
|
+
var auth_module_js_1 = require("./auth/auth.module.js");
|
|
124
|
+
Object.defineProperty(exports, "AuthModule", { enumerable: true, get: function () { return auth_module_js_1.AuthModule; } });
|
|
125
|
+
// The framework-owned `/auth/*` HTTP controller (Team C) + its pending-MFA store.
|
|
126
|
+
// The controller CONSUMES SHIELD_AUTHENTICATOR / SHIELD_SESSION_AUTHORITY and the
|
|
127
|
+
// PendingMfaStore. It is EXPORTED so AuthModule (Team B) / the integration owner
|
|
128
|
+
// registers it in the module's `controllers` and provides `PendingMfaStore` — this
|
|
129
|
+
// file (Team C) does NOT edit auth.module.ts. See auth.controller.ts header.
|
|
130
|
+
var auth_controller_js_1 = require("./auth/auth.controller.js");
|
|
131
|
+
Object.defineProperty(exports, "AuthController", { enumerable: true, get: function () { return auth_controller_js_1.AuthController; } });
|
|
132
|
+
var pending_mfa_store_js_1 = require("./auth/pending-mfa-store.js");
|
|
133
|
+
Object.defineProperty(exports, "PendingMfaStore", { enumerable: true, get: function () { return pending_mfa_store_js_1.PendingMfaStore; } });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type DynamicModule } from '@nestjs/common';
|
|
2
|
+
import type { CredentialResolver } from '@astram/core';
|
|
2
3
|
import type { CoreDecisionAuthority } from '../enforcement/core-seam.js';
|
|
3
4
|
import { type AppConfig } from '../config/app-config.js';
|
|
4
5
|
/**
|
|
@@ -27,6 +28,37 @@ export interface ShieldModuleOptions {
|
|
|
27
28
|
* inject computed config through the one seam rather than editing the framework.
|
|
28
29
|
*/
|
|
29
30
|
readonly config?: AppConfig;
|
|
31
|
+
/**
|
|
32
|
+
* ⛔ OPT-IN: the framework-owned login → session auth surface (Phase 0b — the
|
|
33
|
+
* `/auth/*` endpoints + the composed `Authenticator` / `SessionAuthority`).
|
|
34
|
+
*
|
|
35
|
+
* DEFAULT `false` (ABSENT): an enforcement-only app does NOT import
|
|
36
|
+
* {@link AuthModule} — so it never opens a MariaDB pool / Redis client, never
|
|
37
|
+
* fires the fail-closed `assertRefreshHmacKey` gate, and keeps the no-`.env`
|
|
38
|
+
* boot promised by `create-astram-app`. `ShieldModule.forRoot()` composes with
|
|
39
|
+
* a blank refresh-HMAC key and no live connection (enforcement-only).
|
|
40
|
+
*
|
|
41
|
+
* `true`: imports {@link AuthModule.forRoot} (which pulls the live connections +
|
|
42
|
+
* composes the authenticator + session authority + registers the four `/auth/*`
|
|
43
|
+
* routes) and re-exports {@link SHIELD_AUTHENTICATOR} / {@link SHIELD_SESSION_AUTHORITY}
|
|
44
|
+
* app-wide. An auth-enabled app fails LOUD at boot if `SHIELD_REFRESH_HMAC_KEY`
|
|
45
|
+
* is blank — auth can never compose unkeyed.
|
|
46
|
+
*/
|
|
47
|
+
readonly auth?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* OPTIONAL custom credential resolver (the store login checks against). Omit →
|
|
50
|
+
* the framework's default `MariaDbCredentialResolver` (reads
|
|
51
|
+
* `shield_identity.identity_credential`). Supply a consumer implementation of the
|
|
52
|
+
* core `CredentialResolver` port to authenticate against your OWN user/identity
|
|
53
|
+
* schema (e.g. a multi-tenant users+roles model) WITHOUT editing the framework.
|
|
54
|
+
*
|
|
55
|
+
* Only consulted when `auth: true` (a no-op without it — no AuthModule composes,
|
|
56
|
+
* so no resolver is wired). The seam swaps the STORE only: a supplied resolver
|
|
57
|
+
* still flows through the SAME composed authenticator (anti-enumeration decoy
|
|
58
|
+
* verify, MFA, no-secret-leak). A resolver returning `null` = unknown principal
|
|
59
|
+
* (generic 401); throwing = fail-closed transient — exactly the default contract.
|
|
60
|
+
*/
|
|
61
|
+
readonly credentialResolver?: CredentialResolver;
|
|
30
62
|
}
|
|
31
63
|
/** Async variant of {@link ShieldModuleOptions} (DI-computed config). */
|
|
32
64
|
export interface ShieldModuleAsyncOptions {
|
|
@@ -36,6 +68,25 @@ export interface ShieldModuleAsyncOptions {
|
|
|
36
68
|
readonly useFactory: (...args: readonly unknown[]) => Promise<ShieldModuleOptions> | ShieldModuleOptions;
|
|
37
69
|
/** Modules exporting the injected providers. */
|
|
38
70
|
readonly imports?: readonly unknown[];
|
|
71
|
+
/**
|
|
72
|
+
* ⛔ OPT-IN auth (see {@link ShieldModuleOptions.auth}). STATIC here (not part of
|
|
73
|
+
* the async factory result): whether an app wires the auth surface is a
|
|
74
|
+
* deployment-topology decision known at REGISTRATION time, and a NestJS
|
|
75
|
+
* `DynamicModule.imports` list is composed statically — it cannot depend on the
|
|
76
|
+
* async-resolved `ShieldModuleOptions`. So the flag lives on the async-options
|
|
77
|
+
* object directly, gating the {@link AuthModule} import + engine re-exports
|
|
78
|
+
* exactly like {@link forRoot}. DEFAULT `false` (no auth → no live connection →
|
|
79
|
+
* no-`.env` boot preserved).
|
|
80
|
+
*/
|
|
81
|
+
readonly auth?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* OPTIONAL custom credential resolver — see {@link ShieldModuleOptions.credentialResolver}.
|
|
84
|
+
* STATIC here (like {@link auth}, NOT part of the async factory result): the
|
|
85
|
+
* resolver instance is a registration-time collaborator threaded into the
|
|
86
|
+
* statically-composed {@link AuthModule} import, so it cannot depend on the
|
|
87
|
+
* async-resolved options. Only consulted when `auth: true`.
|
|
88
|
+
*/
|
|
89
|
+
readonly credentialResolver?: CredentialResolver;
|
|
39
90
|
}
|
|
40
91
|
/**
|
|
41
92
|
* `ShieldModule` — the SINGLE registration seam (ADR-0023 §4).
|