@astram/guard 0.1.1 → 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.
Files changed (34) hide show
  1. package/dist/auth/auth.controller.d.ts +102 -0
  2. package/dist/auth/auth.controller.d.ts.map +1 -0
  3. package/dist/auth/auth.controller.js +313 -0
  4. package/dist/auth/auth.module.d.ts +104 -0
  5. package/dist/auth/auth.module.d.ts.map +1 -0
  6. package/dist/auth/auth.module.js +318 -0
  7. package/dist/auth/connections.d.ts +61 -0
  8. package/dist/auth/connections.d.ts.map +1 -0
  9. package/dist/auth/connections.js +116 -0
  10. package/dist/auth/dto.d.ts +144 -0
  11. package/dist/auth/dto.d.ts.map +1 -0
  12. package/dist/auth/dto.js +29 -0
  13. package/dist/auth/pending-mfa-store.d.ts +72 -0
  14. package/dist/auth/pending-mfa-store.d.ts.map +1 -0
  15. package/dist/auth/pending-mfa-store.js +106 -0
  16. package/dist/auth/persistence.module.d.ts +42 -0
  17. package/dist/auth/persistence.module.d.ts.map +1 -0
  18. package/dist/auth/persistence.module.js +94 -0
  19. package/dist/auth/tokens.d.ts +62 -0
  20. package/dist/auth/tokens.d.ts.map +1 -0
  21. package/dist/auth/tokens.js +65 -0
  22. package/dist/authentication/mariadb-credential-resolver.d.ts +96 -0
  23. package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -0
  24. package/dist/authentication/mariadb-credential-resolver.js +156 -0
  25. package/dist/config/app-config.d.ts +134 -1
  26. package/dist/config/app-config.d.ts.map +1 -1
  27. package/dist/config/app-config.js +144 -2
  28. package/dist/index.d.ts +9 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +46 -1
  31. package/dist/module/shield.module.d.ts +51 -0
  32. package/dist/module/shield.module.d.ts.map +1 -1
  33. package/dist/module/shield.module.js +65 -4
  34. package/package.json +1 -1
@@ -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 + side-effect-free so it is trivially unit-testable.
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;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"}
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 + side-effect-free so it is trivially unit-testable.
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
@@ -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,GACzB,MAAM,wBAAwB,CAAC"}
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).