@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.
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 +9 -9
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ /**
3
+ * Phase 0b Auth-surface **endpoint DTOs / view types** — the request/response
4
+ * SHAPES the four `@Public()` `/auth/*` endpoints speak (spec §2). ⛔ FROZEN by
5
+ * this prelude; **Team C builds controllers to these**. Types only — no NestJS /
6
+ * class-validator decorators here (the prelude froze the wire shape; Team C adds
7
+ * the validation decorators on its own DTO classes if it wants runtime
8
+ * validation, keeping this file framework-agnostic and importable anywhere).
9
+ *
10
+ * ─────────────────────────────────────────────────────────────────────────────
11
+ * SECURITY RULES these shapes ENCODE (spec §2, ACs 2/3/6; enforce in controllers):
12
+ *
13
+ * • ANTI-ENUMERATION — `/auth/login` and `/auth/mfa` failure responses are a
14
+ * SINGLE generic 401 for {unknown user, wrong password, wrong/expired factor}.
15
+ * The response body MUST NOT distinguish which failed (the engine's decoy-verify
16
+ * already equalizes timing). NEVER add a `reason`/`field`/`code` that leaks
17
+ * "no such user" vs "bad password".
18
+ * • NO SECRET LEAK — NO response/log ever carries a password, a password hash, a
19
+ * TOTP secret, or a RAW refresh/access token OTHER than the two token fields a
20
+ * successful lifecycle event legitimately returns ({@link SessionTokensView}).
21
+ * The {@link SessionView} below deliberately OMITS every secret — it is the
22
+ * ONLY session shape a response body may embed.
23
+ * • FAIL-CLOSED — a store/transient failure on refresh is the ADR-0016 retryable
24
+ * signal (503-style), NOT a terminal deny/logout (see {@link RefreshResponse}).
25
+ * ─────────────────────────────────────────────────────────────────────────────
26
+ *
27
+ * ADR-0003: framework-agnostic — no framework / HTTP / DB imports.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Phase 0b Auth-surface — the **pending-MFA challenge store** backing
3
+ * `/auth/login` (mfa branch) → `/auth/mfa`.
4
+ *
5
+ * ── WHY THIS EXISTS ──────────────────────────────────────────────────────────
6
+ * The core `Authenticator` has NO separate "verify one more factor" entry point:
7
+ * `authenticate(hint, credentials[])` is the ONLY seam (see
8
+ * `../../core/authentication/authenticator.ts`). To COMPLETE an MFA login the
9
+ * controller must RE-CALL `authenticate(hint, [password, totp])` with BOTH the
10
+ * original password AND the freshly-presented TOTP code. So between the login that
11
+ * returned `mfa_required` and the `/auth/mfa` that finishes it, the server must
12
+ * hold the ORIGINAL `identityHint` + `password` (the "pending authentication").
13
+ *
14
+ * ── THE NON-FORGEABILITY DESIGN (the security decision) ──────────────────────
15
+ * The `challengeToken` handed to the client is a **cryptographically-random opaque
16
+ * handle** (`node:crypto.randomBytes`), NOT a signed/encrypted blob carrying the
17
+ * credentials. The pending `{ identityHint, password }` state lives ONLY
18
+ * server-side, keyed by that random token. Consequences:
19
+ * • FORGERY-PROOF BY CONSTRUCTION — a hand-crafted token maps to no entry, so it
20
+ * is rejected. There is no signature to strip, no payload to tamper with, and
21
+ * the client is never given the credentials (even encrypted) to hold.
22
+ * • SHORT-LIVED — each entry has an expiry (default 5 min). An expired handle is
23
+ * treated as unknown (same generic 401 as a forged one — anti-enumeration).
24
+ * • SINGLE-USE — `consume()` DELETES the entry as it reads it, so a
25
+ * challengeToken cannot be replayed to brute-force TOTP codes indefinitely on
26
+ * one handle. (A caller that fails still burns the handle → must re-login. This
27
+ * is the built-in anti-brute-force cooldown for THIS layer; the resolver/engine
28
+ * add no separate one.)
29
+ * • NEVER LOGGED — the stored password + the token are never logged.
30
+ *
31
+ * TRADE-OFF (documented): server-side state is process-local (an in-memory `Map`).
32
+ * For the single-instance framework scope of Phase 0b this is correct and the
33
+ * safest option (the password never reaches the client). A multi-instance
34
+ * deployment would swap this `Map` for a shared TTL store (Redis) behind the SAME
35
+ * interface — the controller depends only on `store/consume`, not on the backing.
36
+ *
37
+ * ADR-0003 note: this file DOES use `node:crypto` (guard owns node builtins; core
38
+ * does not). It holds NO framework/HTTP coupling — a plain injectable class.
39
+ */
40
+ /**
41
+ * A minimal server-side, single-use, TTL'd store of pending MFA challenges.
42
+ * Injected into the auth controller. Deliberately depends on nothing framework-y.
43
+ */
44
+ export declare class PendingMfaStore {
45
+ private readonly ttlMs;
46
+ /** Clock (UTC epoch ms) — injectable for deterministic tests. */
47
+ private readonly nowMs;
48
+ private readonly pending;
49
+ constructor(ttlMs?: number,
50
+ /** Clock (UTC epoch ms) — injectable for deterministic tests. */
51
+ nowMs?: () => number);
52
+ /**
53
+ * Store a pending authentication and return the opaque, unguessable
54
+ * `challengeToken` the client re-presents to `/auth/mfa`. The token carries NO
55
+ * information about the user — it is a random handle only.
56
+ */
57
+ store(identityHint: string, password: string): string;
58
+ /**
59
+ * Look up + SINGLE-USE consume a pending challenge. Returns the held credentials
60
+ * for a live, known token; `null` for an unknown, forged, or expired token (the
61
+ * controller maps `null` to the SAME generic 401 as a wrong code — no leak of
62
+ * WHY it failed). The entry is DELETED whether or not it was live, so a handle
63
+ * is never replayable.
64
+ */
65
+ consume(token: string): {
66
+ identityHint: string;
67
+ password: string;
68
+ } | null;
69
+ /** Drop expired entries so the map cannot grow unbounded from abandoned logins. */
70
+ private sweep;
71
+ }
72
+ //# sourceMappingURL=pending-mfa-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-mfa-store.d.ts","sourceRoot":"","sources":["../../src/auth/pending-mfa-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAmBH;;;GAGG;AACH,qBAAa,eAAe;IAIxB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK;IALxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;gBAGxC,KAAK,GAAE,MAAuB;IAC/C,iEAAiE;IAChD,KAAK,GAAE,MAAM,MAAiB;IAGjD;;;;OAIG;IACH,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAWrD;;;;;;OAMG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAWzE,mFAAmF;IACnF,OAAO,CAAC,KAAK;CAMd"}
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ /**
3
+ * Phase 0b Auth-surface — the **pending-MFA challenge store** backing
4
+ * `/auth/login` (mfa branch) → `/auth/mfa`.
5
+ *
6
+ * ── WHY THIS EXISTS ──────────────────────────────────────────────────────────
7
+ * The core `Authenticator` has NO separate "verify one more factor" entry point:
8
+ * `authenticate(hint, credentials[])` is the ONLY seam (see
9
+ * `../../core/authentication/authenticator.ts`). To COMPLETE an MFA login the
10
+ * controller must RE-CALL `authenticate(hint, [password, totp])` with BOTH the
11
+ * original password AND the freshly-presented TOTP code. So between the login that
12
+ * returned `mfa_required` and the `/auth/mfa` that finishes it, the server must
13
+ * hold the ORIGINAL `identityHint` + `password` (the "pending authentication").
14
+ *
15
+ * ── THE NON-FORGEABILITY DESIGN (the security decision) ──────────────────────
16
+ * The `challengeToken` handed to the client is a **cryptographically-random opaque
17
+ * handle** (`node:crypto.randomBytes`), NOT a signed/encrypted blob carrying the
18
+ * credentials. The pending `{ identityHint, password }` state lives ONLY
19
+ * server-side, keyed by that random token. Consequences:
20
+ * • FORGERY-PROOF BY CONSTRUCTION — a hand-crafted token maps to no entry, so it
21
+ * is rejected. There is no signature to strip, no payload to tamper with, and
22
+ * the client is never given the credentials (even encrypted) to hold.
23
+ * • SHORT-LIVED — each entry has an expiry (default 5 min). An expired handle is
24
+ * treated as unknown (same generic 401 as a forged one — anti-enumeration).
25
+ * • SINGLE-USE — `consume()` DELETES the entry as it reads it, so a
26
+ * challengeToken cannot be replayed to brute-force TOTP codes indefinitely on
27
+ * one handle. (A caller that fails still burns the handle → must re-login. This
28
+ * is the built-in anti-brute-force cooldown for THIS layer; the resolver/engine
29
+ * add no separate one.)
30
+ * • NEVER LOGGED — the stored password + the token are never logged.
31
+ *
32
+ * TRADE-OFF (documented): server-side state is process-local (an in-memory `Map`).
33
+ * For the single-instance framework scope of Phase 0b this is correct and the
34
+ * safest option (the password never reaches the client). A multi-instance
35
+ * deployment would swap this `Map` for a shared TTL store (Redis) behind the SAME
36
+ * interface — the controller depends only on `store/consume`, not on the backing.
37
+ *
38
+ * ADR-0003 note: this file DOES use `node:crypto` (guard owns node builtins; core
39
+ * does not). It holds NO framework/HTTP coupling — a plain injectable class.
40
+ */
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.PendingMfaStore = void 0;
43
+ const node_crypto_1 = require("node:crypto");
44
+ /** Default challenge lifetime — short, per spec §2 ("a short TTL"). */
45
+ const DEFAULT_TTL_MS = 5 * 60 * 1000;
46
+ /** How many random bytes back a challenge handle (256-bit, url-safe hex → 64 chars). */
47
+ const TOKEN_BYTES = 32;
48
+ /**
49
+ * A minimal server-side, single-use, TTL'd store of pending MFA challenges.
50
+ * Injected into the auth controller. Deliberately depends on nothing framework-y.
51
+ */
52
+ class PendingMfaStore {
53
+ ttlMs;
54
+ nowMs;
55
+ pending = new Map();
56
+ constructor(ttlMs = DEFAULT_TTL_MS,
57
+ /** Clock (UTC epoch ms) — injectable for deterministic tests. */
58
+ nowMs = Date.now) {
59
+ this.ttlMs = ttlMs;
60
+ this.nowMs = nowMs;
61
+ }
62
+ /**
63
+ * Store a pending authentication and return the opaque, unguessable
64
+ * `challengeToken` the client re-presents to `/auth/mfa`. The token carries NO
65
+ * information about the user — it is a random handle only.
66
+ */
67
+ store(identityHint, password) {
68
+ this.sweep();
69
+ const token = (0, node_crypto_1.randomBytes)(TOKEN_BYTES).toString('hex');
70
+ this.pending.set(token, {
71
+ identityHint,
72
+ password,
73
+ expiresAtMs: this.nowMs() + this.ttlMs,
74
+ });
75
+ return token;
76
+ }
77
+ /**
78
+ * Look up + SINGLE-USE consume a pending challenge. Returns the held credentials
79
+ * for a live, known token; `null` for an unknown, forged, or expired token (the
80
+ * controller maps `null` to the SAME generic 401 as a wrong code — no leak of
81
+ * WHY it failed). The entry is DELETED whether or not it was live, so a handle
82
+ * is never replayable.
83
+ */
84
+ consume(token) {
85
+ if (typeof token !== 'string' || token.length === 0)
86
+ return null;
87
+ const entry = this.pending.get(token);
88
+ // Single-use: remove on read regardless of validity (a spent/expired handle
89
+ // can never be retried → forces a fresh login, the anti-brute-force cooldown).
90
+ this.pending.delete(token);
91
+ if (entry === undefined)
92
+ return null;
93
+ if (this.nowMs() >= entry.expiresAtMs)
94
+ return null;
95
+ return { identityHint: entry.identityHint, password: entry.password };
96
+ }
97
+ /** Drop expired entries so the map cannot grow unbounded from abandoned logins. */
98
+ sweep() {
99
+ const now = this.nowMs();
100
+ for (const [token, entry] of this.pending) {
101
+ if (now >= entry.expiresAtMs)
102
+ this.pending.delete(token);
103
+ }
104
+ }
105
+ }
106
+ exports.PendingMfaStore = PendingMfaStore;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `PersistenceModule` — the ONE place live infra connections are created (spec
3
+ * §4). Provides the live `mariadb` {@link Pool} ({@link SHIELD_DB_POOL}) and the
4
+ * live `ioredis` client ({@link SHIELD_REDIS}) from {@link AppConfig}, with
5
+ * graceful shutdown. Everything auth persists (sessions, credentials, hot state)
6
+ * flows through these two connections.
7
+ *
8
+ * ─────────────────────────────────────────────────────────────────────────────
9
+ * Team B (connection + composition) OWNS this file. The two factories create the
10
+ * real driver objects; the {@link ConnectionLifecycle} provider (a class Nest
11
+ * invokes `onModuleDestroy` on) closes them on shutdown — a value provider is not
12
+ * itself lifecycle-aware, so the teardown lives in that holder (see
13
+ * `connections.ts`). Both connections are exported so AuthModule can inject the
14
+ * pool (credential resolver + session record store) and the redis (hot store).
15
+ *
16
+ * SHIELD_DB_POOL — {@link createDbPool}: `mariadb.createPool` for the
17
+ * `shield_core` account from `config.db`, `connectionLimit` from the ADR-0018
18
+ * budget (`config.pool.perAccountMax.shield_core`). NOT a duplicate pool knob.
19
+ * SHIELD_REDIS — {@link createRedisClient}: `new Redis(config.redis.url)`
20
+ * (ioredis). The hot tier (ADR-0013).
21
+ *
22
+ * Neither factory reads the HMAC key (that is the session/hasher composition in
23
+ * AuthModule). A blank `db.password` is allowed — it fails LOUD at connect time,
24
+ * which is correct for a runtime credential. Nothing here logs a credential.
25
+ *
26
+ * ⛔ OPT-IN: like AuthModule, this module opens LIVE connections, so it must only
27
+ * be imported when a deployment actually wants auth (AuthModule imports it). An
28
+ * enforcement-only app that never imports AuthModule never opens a pool/redis and
29
+ * keeps the no-`.env` boot. See auth.module.ts for the opt-in mechanism.
30
+ *
31
+ * ADR-0003: the real `mariadb`/`ioredis` live in guard (Core stays zero-dep).
32
+ */
33
+ import { type DynamicModule } from '@nestjs/common';
34
+ /**
35
+ * The persistence module. `forRoot()` returns the two connection providers (both
36
+ * exported so AuthModule can inject them) plus the shutdown holder. Opt-in: only
37
+ * imported when auth is wired.
38
+ */
39
+ export declare class PersistenceModule {
40
+ static forRoot(): DynamicModule;
41
+ }
42
+ //# sourceMappingURL=persistence.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence.module.d.ts","sourceRoot":"","sources":["../../src/auth/persistence.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAU,KAAK,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AA0C3E;;;;GAIG;AACH,qBACa,iBAAiB;IAC5B,MAAM,CAAC,OAAO,IAAI,aAAa;CAOhC"}
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ /**
3
+ * `PersistenceModule` — the ONE place live infra connections are created (spec
4
+ * §4). Provides the live `mariadb` {@link Pool} ({@link SHIELD_DB_POOL}) and the
5
+ * live `ioredis` client ({@link SHIELD_REDIS}) from {@link AppConfig}, with
6
+ * graceful shutdown. Everything auth persists (sessions, credentials, hot state)
7
+ * flows through these two connections.
8
+ *
9
+ * ─────────────────────────────────────────────────────────────────────────────
10
+ * Team B (connection + composition) OWNS this file. The two factories create the
11
+ * real driver objects; the {@link ConnectionLifecycle} provider (a class Nest
12
+ * invokes `onModuleDestroy` on) closes them on shutdown — a value provider is not
13
+ * itself lifecycle-aware, so the teardown lives in that holder (see
14
+ * `connections.ts`). Both connections are exported so AuthModule can inject the
15
+ * pool (credential resolver + session record store) and the redis (hot store).
16
+ *
17
+ * SHIELD_DB_POOL — {@link createDbPool}: `mariadb.createPool` for the
18
+ * `shield_core` account from `config.db`, `connectionLimit` from the ADR-0018
19
+ * budget (`config.pool.perAccountMax.shield_core`). NOT a duplicate pool knob.
20
+ * SHIELD_REDIS — {@link createRedisClient}: `new Redis(config.redis.url)`
21
+ * (ioredis). The hot tier (ADR-0013).
22
+ *
23
+ * Neither factory reads the HMAC key (that is the session/hasher composition in
24
+ * AuthModule). A blank `db.password` is allowed — it fails LOUD at connect time,
25
+ * which is correct for a runtime credential. Nothing here logs a credential.
26
+ *
27
+ * ⛔ OPT-IN: like AuthModule, this module opens LIVE connections, so it must only
28
+ * be imported when a deployment actually wants auth (AuthModule imports it). An
29
+ * enforcement-only app that never imports AuthModule never opens a pool/redis and
30
+ * keeps the no-`.env` boot. See auth.module.ts for the opt-in mechanism.
31
+ *
32
+ * ADR-0003: the real `mariadb`/`ioredis` live in guard (Core stays zero-dep).
33
+ */
34
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
35
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
36
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
37
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
38
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
39
+ };
40
+ var PersistenceModule_1;
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.PersistenceModule = void 0;
43
+ const common_1 = require("@nestjs/common");
44
+ const config_module_js_1 = require("../config/config.module.js");
45
+ const tokens_js_1 = require("./tokens.js");
46
+ const connections_js_1 = require("./connections.js");
47
+ /**
48
+ * The live MariaDB pool provider. Creates the ONE `shield_core` pool from config
49
+ * via {@link createDbPool}. `inject: [APP_CONFIG]` so the factory sees the resolved
50
+ * config. Teardown is handled by {@link ConnectionLifecycle}.
51
+ */
52
+ const dbPoolProvider = {
53
+ provide: tokens_js_1.SHIELD_DB_POOL,
54
+ useFactory: (config) => (0, connections_js_1.createDbPool)(config),
55
+ inject: [config_module_js_1.APP_CONFIG],
56
+ };
57
+ /**
58
+ * The live Redis client provider. Creates the ONE hot-tier client from
59
+ * `config.redis.url` via {@link createRedisClient}. Teardown via
60
+ * {@link ConnectionLifecycle}.
61
+ */
62
+ const redisProvider = {
63
+ provide: tokens_js_1.SHIELD_REDIS,
64
+ useFactory: (config) => (0, connections_js_1.createRedisClient)(config),
65
+ inject: [config_module_js_1.APP_CONFIG],
66
+ };
67
+ /**
68
+ * The lifecycle holder — injects the two connections and closes them on shutdown
69
+ * (`onModuleDestroy`). A value-provider connection gets no lifecycle callback, so
70
+ * this class is where graceful teardown happens.
71
+ */
72
+ const lifecycleProvider = {
73
+ provide: connections_js_1.ConnectionLifecycle,
74
+ useFactory: (pool, redis) => new connections_js_1.ConnectionLifecycle(pool, redis),
75
+ inject: [tokens_js_1.SHIELD_DB_POOL, tokens_js_1.SHIELD_REDIS],
76
+ };
77
+ /**
78
+ * The persistence module. `forRoot()` returns the two connection providers (both
79
+ * exported so AuthModule can inject them) plus the shutdown holder. Opt-in: only
80
+ * imported when auth is wired.
81
+ */
82
+ let PersistenceModule = PersistenceModule_1 = class PersistenceModule {
83
+ static forRoot() {
84
+ return {
85
+ module: PersistenceModule_1,
86
+ providers: [dbPoolProvider, redisProvider, lifecycleProvider],
87
+ exports: [tokens_js_1.SHIELD_DB_POOL, tokens_js_1.SHIELD_REDIS],
88
+ };
89
+ }
90
+ };
91
+ exports.PersistenceModule = PersistenceModule;
92
+ exports.PersistenceModule = PersistenceModule = PersistenceModule_1 = __decorate([
93
+ (0, common_1.Module)({})
94
+ ], PersistenceModule);
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Phase 0b Auth-surface **DI provider tokens** — the shared injection seams the
3
+ * three parallel teams bind/consume so they never collide (ADR-0023: composition
4
+ * is the framework's job; these are the framework's internal wiring points).
5
+ *
6
+ * ⛔ FROZEN by this prelude. Team B (composition) PROVIDES these; Team A
7
+ * (credential store) provides the resolver behind {@link SHIELD_CREDENTIAL_RESOLVER};
8
+ * Team C (`/auth` controllers) CONSUMES {@link SHIELD_AUTHENTICATOR} +
9
+ * {@link SHIELD_SESSION_AUTHORITY}. Do not rename these Symbols — a rename is a
10
+ * breaking change to every team's wiring.
11
+ *
12
+ * NOTE — some session/hasher adapters already ship their OWN tokens in
13
+ * `@astram/guard` (`SHIELD_PASSWORD_HASHER`, `SHIELD_SESSION_RECORD_STORE`,
14
+ * `SHIELD_HOT_SESSION_STORE`, `SHIELD_TOKEN_GENERATOR`, `SHIELD_REFRESH_HASHER`,
15
+ * `SHIELD_CLOCK`). Those stay the canonical tokens for those adapters. This file
16
+ * adds ONLY the NEW seams Phase 0b introduces: the live connections
17
+ * ({@link SHIELD_DB_POOL} / {@link SHIELD_REDIS}), the credential resolver, and the
18
+ * two COMPOSED engines ({@link SHIELD_AUTHENTICATOR} / {@link SHIELD_SESSION_AUTHORITY}).
19
+ * {@link SHIELD_PASSWORD_HASHER} is re-exported from the argon2 adapter for
20
+ * convenience so a team can import every auth-relevant token from one module.
21
+ *
22
+ * ADR-0003: no framework/DB/crypto imports here — just Symbols (+ one re-export).
23
+ */
24
+ export { SHIELD_PASSWORD_HASHER } from '../authentication/argon2id-password-hasher.js';
25
+ /**
26
+ * The ONE live `mariadb` `Pool` (the `shield_core` account — DML on
27
+ * `shield_identity`, ADR-0013/0014). **Provided by Team B** (the persistence
28
+ * factory — the single place a live DB connection is created). **Consumed by**
29
+ * the {@link SHIELD_CREDENTIAL_RESOLVER} factory and the
30
+ * {@link SHIELD_SESSION_RECORD_STORE} factory. Disposed on app shutdown.
31
+ */
32
+ export declare const SHIELD_DB_POOL: unique symbol;
33
+ /**
34
+ * The ONE live `ioredis` client (the hot session tier, ADR-0013). **Provided by
35
+ * Team B** (the persistence factory). **Consumed by** the
36
+ * {@link SHIELD_HOT_SESSION_STORE} factory. Disposed on app shutdown.
37
+ */
38
+ export declare const SHIELD_REDIS: unique symbol;
39
+ /**
40
+ * The `CredentialResolver` (core port) that resolves an identity hint →
41
+ * `ResolvedCredential | null`. **Provided by Team A** (the
42
+ * `MariaDbCredentialResolver`, injected with {@link SHIELD_DB_POOL}). **Consumed
43
+ * by** the {@link SHIELD_AUTHENTICATOR} factory (Team B) as
44
+ * `createAuthenticator({ resolver })`.
45
+ */
46
+ export declare const SHIELD_CREDENTIAL_RESOLVER: unique symbol;
47
+ /**
48
+ * The composed `Authenticator` (core engine) — `createAuthenticator({ hasher,
49
+ * resolver, identityBroker, factorVerifiers:[totp] })`. **Provided by Team B**
50
+ * (the AuthModule factory). **Consumed by** Team C's `/auth/login` +
51
+ * `/auth/mfa` controllers.
52
+ */
53
+ export declare const SHIELD_AUTHENTICATOR: unique symbol;
54
+ /**
55
+ * The composed `SessionAuthority` (core engine) — `createSessionAuthority({
56
+ * recordStore, hotStore, tokenGenerator, refreshHasher, clock, lifetimes })`.
57
+ * **Provided by Team B** (the AuthModule factory). **Consumed by** Team C's
58
+ * `/auth/login` (establish), `/auth/refresh` (rotate), `/auth/logout` (revoke),
59
+ * and `/auth/mfa` (establish after factor).
60
+ */
61
+ export declare const SHIELD_SESSION_AUTHORITY: unique symbol;
62
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+CAA+C,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,eAA2B,CAAC;AAEvD;;;;GAIG;AACH,eAAO,MAAM,YAAY,eAAyB,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC"}
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ /**
3
+ * Phase 0b Auth-surface **DI provider tokens** — the shared injection seams the
4
+ * three parallel teams bind/consume so they never collide (ADR-0023: composition
5
+ * is the framework's job; these are the framework's internal wiring points).
6
+ *
7
+ * ⛔ FROZEN by this prelude. Team B (composition) PROVIDES these; Team A
8
+ * (credential store) provides the resolver behind {@link SHIELD_CREDENTIAL_RESOLVER};
9
+ * Team C (`/auth` controllers) CONSUMES {@link SHIELD_AUTHENTICATOR} +
10
+ * {@link SHIELD_SESSION_AUTHORITY}. Do not rename these Symbols — a rename is a
11
+ * breaking change to every team's wiring.
12
+ *
13
+ * NOTE — some session/hasher adapters already ship their OWN tokens in
14
+ * `@astram/guard` (`SHIELD_PASSWORD_HASHER`, `SHIELD_SESSION_RECORD_STORE`,
15
+ * `SHIELD_HOT_SESSION_STORE`, `SHIELD_TOKEN_GENERATOR`, `SHIELD_REFRESH_HASHER`,
16
+ * `SHIELD_CLOCK`). Those stay the canonical tokens for those adapters. This file
17
+ * adds ONLY the NEW seams Phase 0b introduces: the live connections
18
+ * ({@link SHIELD_DB_POOL} / {@link SHIELD_REDIS}), the credential resolver, and the
19
+ * two COMPOSED engines ({@link SHIELD_AUTHENTICATOR} / {@link SHIELD_SESSION_AUTHORITY}).
20
+ * {@link SHIELD_PASSWORD_HASHER} is re-exported from the argon2 adapter for
21
+ * convenience so a team can import every auth-relevant token from one module.
22
+ *
23
+ * ADR-0003: no framework/DB/crypto imports here — just Symbols (+ one re-export).
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.SHIELD_SESSION_AUTHORITY = exports.SHIELD_AUTHENTICATOR = exports.SHIELD_CREDENTIAL_RESOLVER = exports.SHIELD_REDIS = exports.SHIELD_DB_POOL = exports.SHIELD_PASSWORD_HASHER = void 0;
27
+ var argon2id_password_hasher_js_1 = require("../authentication/argon2id-password-hasher.js");
28
+ Object.defineProperty(exports, "SHIELD_PASSWORD_HASHER", { enumerable: true, get: function () { return argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER; } });
29
+ /**
30
+ * The ONE live `mariadb` `Pool` (the `shield_core` account — DML on
31
+ * `shield_identity`, ADR-0013/0014). **Provided by Team B** (the persistence
32
+ * factory — the single place a live DB connection is created). **Consumed by**
33
+ * the {@link SHIELD_CREDENTIAL_RESOLVER} factory and the
34
+ * {@link SHIELD_SESSION_RECORD_STORE} factory. Disposed on app shutdown.
35
+ */
36
+ exports.SHIELD_DB_POOL = Symbol('SHIELD_DB_POOL');
37
+ /**
38
+ * The ONE live `ioredis` client (the hot session tier, ADR-0013). **Provided by
39
+ * Team B** (the persistence factory). **Consumed by** the
40
+ * {@link SHIELD_HOT_SESSION_STORE} factory. Disposed on app shutdown.
41
+ */
42
+ exports.SHIELD_REDIS = Symbol('SHIELD_REDIS');
43
+ /**
44
+ * The `CredentialResolver` (core port) that resolves an identity hint →
45
+ * `ResolvedCredential | null`. **Provided by Team A** (the
46
+ * `MariaDbCredentialResolver`, injected with {@link SHIELD_DB_POOL}). **Consumed
47
+ * by** the {@link SHIELD_AUTHENTICATOR} factory (Team B) as
48
+ * `createAuthenticator({ resolver })`.
49
+ */
50
+ exports.SHIELD_CREDENTIAL_RESOLVER = Symbol('SHIELD_CREDENTIAL_RESOLVER');
51
+ /**
52
+ * The composed `Authenticator` (core engine) — `createAuthenticator({ hasher,
53
+ * resolver, identityBroker, factorVerifiers:[totp] })`. **Provided by Team B**
54
+ * (the AuthModule factory). **Consumed by** Team C's `/auth/login` +
55
+ * `/auth/mfa` controllers.
56
+ */
57
+ exports.SHIELD_AUTHENTICATOR = Symbol('SHIELD_AUTHENTICATOR');
58
+ /**
59
+ * The composed `SessionAuthority` (core engine) — `createSessionAuthority({
60
+ * recordStore, hotStore, tokenGenerator, refreshHasher, clock, lifetimes })`.
61
+ * **Provided by Team B** (the AuthModule factory). **Consumed by** Team C's
62
+ * `/auth/login` (establish), `/auth/refresh` (rotate), `/auth/logout` (revoke),
63
+ * and `/auth/mfa` (establish after factor).
64
+ */
65
+ exports.SHIELD_SESSION_AUTHORITY = Symbol('SHIELD_SESSION_AUTHORITY');
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `MariaDbCredentialResolver` — the concrete {@link CredentialResolver} (core
3
+ * port) backed by the `shield_identity.identity_credential` table. Resolves an
4
+ * `identityHint` (username / email) → a {@link ResolvedCredential} the
5
+ * Authentication engine verifies, or `null` for an unknown hint.
6
+ *
7
+ * ⛔⛔ PHASE 0b PRELUDE SKELETON — the CONTRACT is frozen here; **Team A fills the
8
+ * body**. `resolve()` currently THROWS "not implemented" so a missing Team-A impl
9
+ * fails CLOSED (a broken resolver can NEVER silently authenticate). Do not change
10
+ * the constructor shape (it takes the `SHIELD_DB_POOL`) or the return type — the
11
+ * composition (Team B) and the engine depend on them.
12
+ *
13
+ * ─────────────────────────────────────────────────────────────────────────────
14
+ * THE TABLE CONTRACT Team A must create (infra migration/seed
15
+ * `infra/mariadb/init/30-identity-credential.sql`, in `shield_identity`; the
16
+ * runtime `shield_core` account already has DML there — no new grant). Columns
17
+ * per spec §3 (prefix `ic_` to match the repo's session-table convention):
18
+ *
19
+ * identity_credential
20
+ * ┌────────────────────────┬──────────────┬────────────────────────────────────┐
21
+ * │ column │ type │ notes │
22
+ * ├────────────────────────┼──────────────┼────────────────────────────────────┤
23
+ * │ ic_identity_hint │ VARCHAR(255) │ UNIQUE — the login key (user/email)│
24
+ * │ ic_password_hash │ VARCHAR(255) │ Argon2id encoded hash (verify in) │
25
+ * │ ic_principal_id │ VARCHAR(255) │ → ResolvedCredential.claims.id │
26
+ * │ ic_tenant_id │ VARCHAR(255) │ → claims.tenantId │
27
+ * │ ic_roles │ JSON / TEXT │ role-id array → claims.roleIds │
28
+ * │ ic_totp_secret │ VARCHAR(255) │ NULLABLE — base32 TOTP secret; │
29
+ * │ │ │ present ⇒ requiredFactors:[totp] │
30
+ * │ ic_created_at_ms │ BIGINT │ UTC epoch ms (platform sets UTC) │
31
+ * │ ic_updated_at_ms │ BIGINT │ UTC epoch ms │
32
+ * └────────────────────────┴──────────────┴────────────────────────────────────┘
33
+ * PRIMARY KEY / UNIQUE on ic_identity_hint (single-row resolve by hint).
34
+ *
35
+ * The resolver's SELECT must:
36
+ * 1. `SELECT ic_password_hash, ic_principal_id, ic_tenant_id, ic_roles,
37
+ * ic_totp_secret FROM shield_identity.identity_credential
38
+ * WHERE ic_identity_hint = ? LIMIT 1` (parameterized — NEVER string-built).
39
+ * 2. Return `null` when NO row matches — the ONLY "not found" signal
40
+ * (anti-enumeration; MUST NOT throw for an unknown hint; the engine burns a
41
+ * decoy verify on `null` so timing is uniform).
42
+ * 3. Map a found row → `ResolvedCredential`:
43
+ * passwordHash = ic_password_hash
44
+ * claims = { id: ic_principal_id, tenantId: ic_tenant_id,
45
+ * roleIds: <parsed ic_roles> } // canonical Internal claims
46
+ * requiredFactors = ic_totp_secret ? [{ factor:'totp', secret: ic_totp_secret }] : []
47
+ * 4. NEVER log the hash, the TOTP secret, or the raw row.
48
+ * 5. A store outage (connection/query error) MAY throw — the engine treats a
49
+ * resolver throw as fail-closed `authentication_unavailable` (never a leak).
50
+ * Team A does NOT need to swallow it, but MUST NOT convert it into a `null`
51
+ * (that would make an outage look like "unknown user").
52
+ *
53
+ * (`claims.kind` is optional — the Internal adapter defaults an absent kind; add
54
+ * an `ic_kind` column later if a deployment needs non-`user` principals. Do NOT
55
+ * put any superuser flag in the row — superuser is the broker's server-side
56
+ * allowlist, never a claim, per core/identity.)
57
+ * ─────────────────────────────────────────────────────────────────────────────
58
+ *
59
+ * ADR-0003: the concrete DB lives in guard (Core stays zero-dep). ADR-0013/0014:
60
+ * reads `shield_identity` via the hot-path `shield_core` account.
61
+ */
62
+ import type { Pool } from 'mariadb';
63
+ import type { CredentialResolver, ResolvedCredential } from '@astram/core';
64
+ /** DI token for the resolver — re-exported from the shared auth tokens module. */
65
+ export { SHIELD_CREDENTIAL_RESOLVER } from '../auth/tokens.js';
66
+ /**
67
+ * The concrete MariaDB-backed {@link CredentialResolver}. Constructed with the
68
+ * live `shield_core` {@link Pool} (via {@link SHIELD_CREDENTIAL_RESOLVER}'s
69
+ * factory, which injects {@link SHIELD_DB_POOL}). Stateless per call apart from
70
+ * the injected pool.
71
+ *
72
+ * ⛔ SKELETON: `resolve()` throws until Team A implements the SELECT above.
73
+ */
74
+ export declare class MariaDbCredentialResolver implements CredentialResolver {
75
+ private readonly pool;
76
+ constructor(pool: Pool);
77
+ /**
78
+ * Resolve `identityHint` → {@link ResolvedCredential}, or `null` when no such
79
+ * principal exists.
80
+ *
81
+ * - Parameterized SELECT (`WHERE ic_identity_hint = ? LIMIT 1`) — never
82
+ * string-built, so the hint can never inject SQL.
83
+ * - **No row ⇒ `null`** — the ONLY "not found" signal (anti-enumeration; the
84
+ * engine burns a decoy password verify on `null` so an unknown user is
85
+ * indistinguishable from a wrong password by content or timing). We do NOT
86
+ * throw for an unknown hint.
87
+ * - A **query/connection ERROR propagates** (rejects) — an outage is NOT an
88
+ * unknown user; the engine treats a resolver throw as fail-closed
89
+ * `authentication_unavailable`. We never convert an error into `null` (that
90
+ * would mask an outage AND make it look like "no such user").
91
+ * - The password hash and TOTP secret are NEVER logged; the raw row is never
92
+ * logged. On success they are returned to the engine as verify inputs only.
93
+ */
94
+ resolve(identityHint: string): Promise<ResolvedCredential | null>;
95
+ }
96
+ //# sourceMappingURL=mariadb-credential-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mariadb-credential-resolver.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-credential-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EACV,kBAAkB,EAElB,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAEtB,kFAAkF;AAClF,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAwD/D;;;;;;;GAOG;AACH,qBAAa,yBAA0B,YAAW,kBAAkB;IACtD,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEvC;;;;;;;;;;;;;;;;OAgBG;IACG,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;CAgCxE"}