@astram/guard 0.4.0 → 0.5.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 (64) hide show
  1. package/dist/auth/auth.module.d.ts +6 -0
  2. package/dist/auth/auth.module.d.ts.map +1 -1
  3. package/dist/auth/auth.module.js +106 -15
  4. package/dist/auth/tokens.d.ts +9 -0
  5. package/dist/auth/tokens.d.ts.map +1 -1
  6. package/dist/auth/tokens.js +10 -1
  7. package/dist/authentication/log-email-sender.d.ts +13 -0
  8. package/dist/authentication/log-email-sender.d.ts.map +1 -0
  9. package/dist/authentication/log-email-sender.js +13 -0
  10. package/dist/authentication/mariadb-credential-writer.d.ts +16 -0
  11. package/dist/authentication/mariadb-credential-writer.d.ts.map +1 -0
  12. package/dist/authentication/mariadb-credential-writer.js +19 -0
  13. package/dist/authentication/mariadb-mfa-device-store.d.ts +16 -0
  14. package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -0
  15. package/dist/authentication/mariadb-mfa-device-store.js +99 -0
  16. package/dist/authentication/mariadb-or-totp-verifier.d.ts +31 -0
  17. package/dist/authentication/mariadb-or-totp-verifier.d.ts.map +1 -0
  18. package/dist/authentication/mariadb-or-totp-verifier.js +48 -0
  19. package/dist/authentication/mariadb-profile-resolver.d.ts +24 -0
  20. package/dist/authentication/mariadb-profile-resolver.d.ts.map +1 -0
  21. package/dist/authentication/mariadb-profile-resolver.js +54 -0
  22. package/dist/authentication/mariadb-profile-writer.d.ts +18 -0
  23. package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -0
  24. package/dist/authentication/mariadb-profile-writer.js +36 -0
  25. package/dist/authentication/mariadb-recovery-code-store.d.ts +47 -0
  26. package/dist/authentication/mariadb-recovery-code-store.d.ts.map +1 -0
  27. package/dist/authentication/mariadb-recovery-code-store.js +53 -0
  28. package/dist/authentication/mariadb-reset-token-store.d.ts +26 -0
  29. package/dist/authentication/mariadb-reset-token-store.d.ts.map +1 -0
  30. package/dist/authentication/mariadb-reset-token-store.js +43 -0
  31. package/dist/authentication/mariadb-session-sweeper.d.ts +33 -0
  32. package/dist/authentication/mariadb-session-sweeper.d.ts.map +1 -0
  33. package/dist/authentication/mariadb-session-sweeper.js +60 -0
  34. package/dist/config/app-config.d.ts +5 -7
  35. package/dist/config/app-config.d.ts.map +1 -1
  36. package/dist/config/app-config.js +24 -57
  37. package/dist/identity/auth-facts.controller.d.ts +24 -0
  38. package/dist/identity/auth-facts.controller.d.ts.map +1 -0
  39. package/dist/identity/auth-facts.controller.js +106 -0
  40. package/dist/identity/identity-actions.d.ts +22 -0
  41. package/dist/identity/identity-actions.d.ts.map +1 -0
  42. package/dist/identity/identity-actions.js +32 -0
  43. package/dist/identity/module-registry.bridge.d.ts +4 -0
  44. package/dist/identity/module-registry.bridge.d.ts.map +1 -0
  45. package/dist/identity/module-registry.bridge.js +43 -0
  46. package/dist/identity/permission-facts.adapter.d.ts +47 -0
  47. package/dist/identity/permission-facts.adapter.d.ts.map +1 -0
  48. package/dist/identity/permission-facts.adapter.js +108 -0
  49. package/dist/index.d.ts +3 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +20 -2
  52. package/dist/migrations-dir.d.ts +2 -0
  53. package/dist/migrations-dir.d.ts.map +1 -0
  54. package/dist/migrations-dir.js +13 -0
  55. package/dist/module/shield-tokens.d.ts +6 -1
  56. package/dist/module/shield-tokens.d.ts.map +1 -1
  57. package/dist/module/shield-tokens.js +7 -1
  58. package/dist/module/shield.module.d.ts +17 -0
  59. package/dist/module/shield.module.d.ts.map +1 -1
  60. package/dist/module/shield.module.js +12 -0
  61. package/migrations/1710000000006-CreateAuthzSchema.cjs +53 -0
  62. package/migrations/1710000000007-ExtendPermissionAndRolePermission.cjs +66 -0
  63. package/migrations/1710000000008-CreateAccessBundleTables.cjs +49 -0
  64. package/package.json +5 -3
@@ -0,0 +1,18 @@
1
+ /**
2
+ * MariaDbProfileWriter — the framework-DEFAULT ProfileWriter (backs PATCH /account/profile). Updates
3
+ * ONLY the supplied columns on shield_app.users, then re-reads via MariaDbProfileResolver so the returned
4
+ * display profile has ONE source of truth (names, never UUIDs). Fail-closed: throws if the re-read is null.
5
+ */
6
+ import type { Pool } from 'mariadb';
7
+ import type { ProfileWriter } from '../auth/ports/profile-writer.js';
8
+ import type { CurrentUserProfile } from '../auth/profile-resolver.js';
9
+ export declare class MariaDbProfileWriter implements ProfileWriter {
10
+ private readonly pool;
11
+ private readonly resolver;
12
+ constructor(pool: Pool);
13
+ update(principalId: string, changes: {
14
+ name?: string;
15
+ email?: string;
16
+ }): Promise<CurrentUserProfile>;
17
+ }
18
+ //# sourceMappingURL=mariadb-profile-writer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mariadb-profile-writer.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-profile-writer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAOtE,qBAAa,oBAAqB,YAAW,aAAa;IAE5C,OAAO,CAAC,QAAQ,CAAC,IAAI;IADjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;gBACrB,IAAI,EAAE,IAAI;IAIjC,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC;CA0B3G"}
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MariaDbProfileWriter = void 0;
4
+ const mariadb_profile_resolver_js_1 = require("./mariadb-profile-resolver.js");
5
+ class MariaDbProfileWriter {
6
+ pool;
7
+ resolver;
8
+ constructor(pool) {
9
+ this.pool = pool;
10
+ this.resolver = new mariadb_profile_resolver_js_1.MariaDbProfileResolver(pool);
11
+ }
12
+ async update(principalId, changes) {
13
+ const sets = [];
14
+ const params = [];
15
+ if (changes.name !== undefined) {
16
+ sets.push('u_username = ?');
17
+ params.push(changes.name);
18
+ }
19
+ if (changes.email !== undefined) {
20
+ sets.push('u_email = ?');
21
+ params.push(changes.email);
22
+ }
23
+ if (sets.length > 0) {
24
+ params.push(principalId);
25
+ await this.pool.query(`UPDATE \`shield_app\`.\`users\` SET ${sets.join(', ')} WHERE u_uuid = ?`, params);
26
+ }
27
+ // Resolve the caller's active company so the re-read profile is tenant-correct.
28
+ const active = (await this.pool.query("SELECT cm_company_uuid FROM `shield_app`.`company_membership` WHERE cm_user_uuid = ? AND cm_is_active = 1 AND cm_status = 'active' LIMIT 1", [principalId]));
29
+ const tenantId = active[0]?.cm_company_uuid ?? '';
30
+ const profile = await this.resolver.resolve({ kind: 'user', id: principalId, tenantId, roleIds: [], isRoot: false });
31
+ if (profile === null)
32
+ throw new Error('profile_unavailable'); // fail-closed: never a partial write result
33
+ return profile;
34
+ }
35
+ }
36
+ exports.MariaDbProfileWriter = MariaDbProfileWriter;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * MariaDbRecoveryCodeStore — the framework-DEFAULT RecoveryCodeStore (backs
3
+ * POST /account/mfa/recovery-codes/regenerate + recovery-code login), persisted in
4
+ * `shield_identity`.`mfa_recovery_code` (one row per Argon2-hashed one-time recovery
5
+ * code, PK `(mrc_principal_id, mrc_code_hash)`).
6
+ *
7
+ * The framework hands this store ALREADY-HASHED codes (Argon2), so `replaceAll` never
8
+ * sees the raw codes — it only wipes-then-inserts hashes. `consume` receives the RAW
9
+ * code the user presents; because Argon2 hashes are salted and NOT queryable by
10
+ * plaintext, it loads the principal's UNCONSUMED rows and verifies the raw code against
11
+ * each hash with the injected hasher until one matches, then atomically marks THAT row
12
+ * consumed (single-use). A conditional `UPDATE … WHERE mrc_consumed_at_ms IS NULL` with
13
+ * an `affectedRows === 1` race guard makes the mark single-winner: two concurrent
14
+ * consumes of the same code both verify, but only one wins the UPDATE; the loser treats
15
+ * it as already-consumed → `false`. Every query is parameterized (never string-built).
16
+ *
17
+ * SECURITY — never logs the raw code or a hash; a hash mismatch resolves to "not this
18
+ * code" (fail-closed), never a thrown error. A genuine store outage (the `query` call
19
+ * rejecting) propagates rather than being swallowed to `false`.
20
+ */
21
+ import type { Pool } from 'mariadb';
22
+ import type { PasswordHasher } from '@astram/core';
23
+ import type { RecoveryCodeStore } from '../auth/ports/recovery-code-store.js';
24
+ export declare class MariaDbRecoveryCodeStore implements RecoveryCodeStore {
25
+ private readonly pool;
26
+ /** The SAME Argon2id hasher the framework uses to hash the codes on enroll. */
27
+ private readonly hasher;
28
+ private readonly now;
29
+ constructor(pool: Pool,
30
+ /** The SAME Argon2id hasher the framework uses to hash the codes on enroll. */
31
+ hasher: PasswordHasher, now?: () => number);
32
+ /**
33
+ * Replace the principal's entire recovery-code set with `codeHashes` (already
34
+ * Argon2-hashed). Wipes any prior codes first, then inserts the new batch. Passing
35
+ * an empty array simply wipes (used when MFA is disabled).
36
+ */
37
+ replaceAll(principalId: string, codeHashes: readonly string[]): Promise<void>;
38
+ /**
39
+ * Consume one of the principal's recovery codes by RAW value. Loads the unconsumed
40
+ * hashes, verifies the raw code against each, and on the first match atomically marks
41
+ * that row consumed (single-use, `affectedRows === 1` race guard). Returns `true` iff
42
+ * a code was matched AND won the consume; `false` for empty input, no match, an
43
+ * already-consumed code, or a lost race.
44
+ */
45
+ consume(principalId: string, rawCode: string): Promise<boolean>;
46
+ }
47
+ //# sourceMappingURL=mariadb-recovery-code-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mariadb-recovery-code-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-recovery-code-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAM9E,qBAAa,wBAAyB,YAAW,iBAAiB;IAE9D,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAHH,IAAI,EAAE,IAAI;IAC3B,+EAA+E;IAC9D,MAAM,EAAE,cAAc,EACtB,GAAG,GAAE,MAAM,MAAiB;IAG/C;;;;OAIG;IACG,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBnF;;;;;;OAMG;IACG,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAqBtE"}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MariaDbRecoveryCodeStore = void 0;
4
+ class MariaDbRecoveryCodeStore {
5
+ pool;
6
+ hasher;
7
+ now;
8
+ constructor(pool,
9
+ /** The SAME Argon2id hasher the framework uses to hash the codes on enroll. */
10
+ hasher, now = Date.now) {
11
+ this.pool = pool;
12
+ this.hasher = hasher;
13
+ this.now = now;
14
+ }
15
+ /**
16
+ * Replace the principal's entire recovery-code set with `codeHashes` (already
17
+ * Argon2-hashed). Wipes any prior codes first, then inserts the new batch. Passing
18
+ * an empty array simply wipes (used when MFA is disabled).
19
+ */
20
+ async replaceAll(principalId, codeHashes) {
21
+ await this.pool.query(`DELETE FROM \`shield_identity\`.\`mfa_recovery_code\` WHERE mrc_principal_id = ?`, [principalId]);
22
+ const createdAt = this.now();
23
+ for (const hash of codeHashes) {
24
+ await this.pool.query(`INSERT INTO \`shield_identity\`.\`mfa_recovery_code\`
25
+ (mrc_principal_id, mrc_code_hash, mrc_consumed_at_ms, mrc_created_at_ms)
26
+ VALUES (?, ?, NULL, ?)`, [principalId, hash, createdAt]);
27
+ }
28
+ }
29
+ /**
30
+ * Consume one of the principal's recovery codes by RAW value. Loads the unconsumed
31
+ * hashes, verifies the raw code against each, and on the first match atomically marks
32
+ * that row consumed (single-use, `affectedRows === 1` race guard). Returns `true` iff
33
+ * a code was matched AND won the consume; `false` for empty input, no match, an
34
+ * already-consumed code, or a lost race.
35
+ */
36
+ async consume(principalId, rawCode) {
37
+ if (typeof rawCode !== 'string' || rawCode.length === 0)
38
+ return false;
39
+ const rows = (await this.pool.query(`SELECT mrc_code_hash FROM \`shield_identity\`.\`mfa_recovery_code\`
40
+ WHERE mrc_principal_id = ? AND mrc_consumed_at_ms IS NULL`, [principalId]));
41
+ for (const row of rows) {
42
+ if (await this.hasher.verify(row.mrc_code_hash, rawCode)) {
43
+ const res = (await this.pool.query(`UPDATE \`shield_identity\`.\`mfa_recovery_code\`
44
+ SET mrc_consumed_at_ms = ?
45
+ WHERE mrc_principal_id = ? AND mrc_code_hash = ? AND mrc_consumed_at_ms IS NULL`, [this.now(), principalId, row.mrc_code_hash]));
46
+ // Single-use guard: only the UPDATE that flips NULL→consumed wins.
47
+ return res.affectedRows === 1;
48
+ }
49
+ }
50
+ return false;
51
+ }
52
+ }
53
+ exports.MariaDbRecoveryCodeStore = MariaDbRecoveryCodeStore;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * MariaDbResetTokenStore — the framework-DEFAULT ResetTokenStore (backs POST /auth/forgot-password
3
+ * + /auth/reset-password), persisted in `shield_identity`.`password_reset_token`. The framework
4
+ * hashes the raw reset token (SHA-256) BEFORE it reaches this store, so the store never sees the
5
+ * raw token — only its hash (the primary key).
6
+ *
7
+ * SINGLE-USE + TTL: `consume` returns the principal id IFF the token is both unexpired AND
8
+ * unconsumed, then atomically marks it consumed. It is a SELECT-then-conditional-UPDATE with an
9
+ * `affectedRows` race guard: two concurrent consumes of the same token both pass the SELECT, but
10
+ * only ONE wins the `UPDATE … WHERE prt_consumed_at_ms IS NULL` (affectedRows === 1); the loser
11
+ * (affectedRows === 0) is treated as already-consumed → `null`. Unknown/expired/already-consumed
12
+ * tokens all return `null` too — never a distinguishing throw (anti-enumeration). A genuine store
13
+ * outage (the `query` call rejecting) propagates rather than being swallowed to `null`. Every
14
+ * query is parameterized (never string-built); the token hash is never logged.
15
+ */
16
+ import type { Pool } from 'mariadb';
17
+ import type { ResetTokenStore } from '../auth/ports/reset-token-store.js';
18
+ export declare class MariaDbResetTokenStore implements ResetTokenStore {
19
+ private readonly pool;
20
+ private readonly now;
21
+ constructor(pool: Pool, now?: () => number);
22
+ issue(principalId: string, tokenHash: string, expiresAtMs: number): Promise<void>;
23
+ consume(tokenHash: string): Promise<string | null>;
24
+ invalidateAllForPrincipal(principalId: string): Promise<void>;
25
+ }
26
+ //# sourceMappingURL=mariadb-reset-token-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mariadb-reset-token-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-reset-token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAM1E,qBAAa,sBAAuB,YAAW,eAAe;IAE1D,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBADH,IAAI,EAAE,IAAI,EACV,GAAG,GAAE,MAAM,MAAiB;IAGzC,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASjF,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA0BlD,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAUpE"}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MariaDbResetTokenStore = void 0;
4
+ class MariaDbResetTokenStore {
5
+ pool;
6
+ now;
7
+ constructor(pool, now = Date.now) {
8
+ this.pool = pool;
9
+ this.now = now;
10
+ }
11
+ async issue(principalId, tokenHash, expiresAtMs) {
12
+ await this.pool.query(`INSERT INTO \`shield_identity\`.\`password_reset_token\`
13
+ (prt_token_hash, prt_principal_id, prt_expires_at_ms, prt_consumed_at_ms, prt_created_at_ms)
14
+ VALUES (?, ?, ?, NULL, ?)`, [tokenHash, principalId, expiresAtMs, this.now()]);
15
+ }
16
+ async consume(tokenHash) {
17
+ const now = this.now();
18
+ // 1) Find an unexpired, unconsumed token for this hash.
19
+ const rows = (await this.pool.query(`SELECT prt_principal_id
20
+ FROM \`shield_identity\`.\`password_reset_token\`
21
+ WHERE prt_token_hash = ? AND prt_consumed_at_ms IS NULL AND prt_expires_at_ms > ?
22
+ LIMIT 1`, [tokenHash, now]));
23
+ const principalId = rows[0]?.prt_principal_id;
24
+ if (principalId === undefined)
25
+ return null;
26
+ // 2) Atomically claim it (single-use). The `IS NULL` guard + affectedRows check means a
27
+ // concurrent consume can never both succeed — the loser sees 0 rows and returns null.
28
+ const res = (await this.pool.query(`UPDATE \`shield_identity\`.\`password_reset_token\`
29
+ SET prt_consumed_at_ms = ?
30
+ WHERE prt_token_hash = ? AND prt_consumed_at_ms IS NULL`, [now, tokenHash]));
31
+ if (res.affectedRows !== 1)
32
+ return null; // lost the race → treat as consumed
33
+ return principalId;
34
+ }
35
+ async invalidateAllForPrincipal(principalId) {
36
+ // Burn every outstanding (unconsumed) token for the principal after a successful reset, so a
37
+ // second still-valid link cannot re-reset.
38
+ await this.pool.query(`UPDATE \`shield_identity\`.\`password_reset_token\`
39
+ SET prt_consumed_at_ms = ?
40
+ WHERE prt_principal_id = ? AND prt_consumed_at_ms IS NULL`, [this.now(), principalId]);
41
+ }
42
+ }
43
+ exports.MariaDbResetTokenStore = MariaDbResetTokenStore;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * MariaDbSessionSweeper — the framework-DEFAULT SessionSweeper (backs the best-effort
3
+ * calls from POST /account/change-password + /auth/reset-password), revoking a
4
+ * principal's durable sessions in `shield_identity`.`session` after a credential
5
+ * change.
6
+ *
7
+ * - revokeAllForPrincipal → after a password RESET (kill every session).
8
+ * - revokeOthersForPrincipal → after an authenticated CHANGE-password (kill every
9
+ * session except the caller's current one, so the actor stays signed in).
10
+ *
11
+ * WHY WE ALSO DROP THE REDIS LIVENESS KEY: flipping the durable `session` row to
12
+ * 'revoked' makes the session non-rotatable (refresh is rejected), but an
13
+ * outstanding ACCESS token's per-request liveness is gated on the Redis key the
14
+ * `SessionTokenBinder` owns (see its SESSION-LIVENESS GATE in `resolve()`). The
15
+ * durable row alone does not reach that key, so a still-valid access token would
16
+ * survive until its TTL. To make revocation IMMEDIATE, we also drop the liveness
17
+ * key for each revoked session via `binder.unbindSession` — the SAME mechanism
18
+ * logout uses.
19
+ *
20
+ * The binder is passed in at construction time rather than DI-resolved here (this
21
+ * class is a plain adapter; wiring lives with the consumer).
22
+ */
23
+ import type { Pool } from 'mariadb';
24
+ import type { SessionSweeper } from '../auth/ports/session-sweeper.js';
25
+ import type { SessionTokenBinder } from '../request-auth/session-token-binder.js';
26
+ export declare class MariaDbSessionSweeper implements SessionSweeper {
27
+ private readonly pool;
28
+ private readonly binder;
29
+ constructor(pool: Pool, binder: SessionTokenBinder);
30
+ revokeAllForPrincipal(principalId: string): Promise<void>;
31
+ revokeOthersForPrincipal(principalId: string, keepSessionId: string): Promise<void>;
32
+ }
33
+ //# sourceMappingURL=mariadb-session-sweeper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mariadb-session-sweeper.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-session-sweeper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAMlF,qBAAa,qBAAsB,YAAW,cAAc;IAExD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,kBAAkB;IAGvC,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBzD,wBAAwB,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAmB1F"}
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * MariaDbSessionSweeper — the framework-DEFAULT SessionSweeper (backs the best-effort
4
+ * calls from POST /account/change-password + /auth/reset-password), revoking a
5
+ * principal's durable sessions in `shield_identity`.`session` after a credential
6
+ * change.
7
+ *
8
+ * - revokeAllForPrincipal → after a password RESET (kill every session).
9
+ * - revokeOthersForPrincipal → after an authenticated CHANGE-password (kill every
10
+ * session except the caller's current one, so the actor stays signed in).
11
+ *
12
+ * WHY WE ALSO DROP THE REDIS LIVENESS KEY: flipping the durable `session` row to
13
+ * 'revoked' makes the session non-rotatable (refresh is rejected), but an
14
+ * outstanding ACCESS token's per-request liveness is gated on the Redis key the
15
+ * `SessionTokenBinder` owns (see its SESSION-LIVENESS GATE in `resolve()`). The
16
+ * durable row alone does not reach that key, so a still-valid access token would
17
+ * survive until its TTL. To make revocation IMMEDIATE, we also drop the liveness
18
+ * key for each revoked session via `binder.unbindSession` — the SAME mechanism
19
+ * logout uses.
20
+ *
21
+ * The binder is passed in at construction time rather than DI-resolved here (this
22
+ * class is a plain adapter; wiring lives with the consumer).
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.MariaDbSessionSweeper = void 0;
26
+ class MariaDbSessionSweeper {
27
+ pool;
28
+ binder;
29
+ constructor(pool, binder) {
30
+ this.pool = pool;
31
+ this.binder = binder;
32
+ }
33
+ async revokeAllForPrincipal(principalId) {
34
+ const rows = (await this.pool.query(`SELECT s_session_id
35
+ FROM \`shield_identity\`.\`session\`
36
+ WHERE s_principal_id = ? AND s_status = 'active'`, [principalId]));
37
+ await this.pool.query(`UPDATE \`shield_identity\`.\`session\`
38
+ SET s_status = 'revoked'
39
+ WHERE s_principal_id = ? AND s_status = 'active'`, [principalId]);
40
+ // Drop each revoked session's Redis liveness key so outstanding access tokens die
41
+ // immediately, not at their TTL.
42
+ for (const { s_session_id } of rows) {
43
+ await this.binder.unbindSession(s_session_id);
44
+ }
45
+ }
46
+ async revokeOthersForPrincipal(principalId, keepSessionId) {
47
+ const rows = (await this.pool.query(`SELECT s_session_id
48
+ FROM \`shield_identity\`.\`session\`
49
+ WHERE s_principal_id = ? AND s_status = 'active' AND s_session_id <> ?`, [principalId, keepSessionId]));
50
+ await this.pool.query(`UPDATE \`shield_identity\`.\`session\`
51
+ SET s_status = 'revoked'
52
+ WHERE s_principal_id = ? AND s_status = 'active' AND s_session_id <> ?`, [principalId, keepSessionId]);
53
+ // Only the OTHER sessions are unbound — the kept (current) session's liveness key
54
+ // is never touched, so the actor stays signed in after a change-password.
55
+ for (const { s_session_id } of rows) {
56
+ await this.binder.unbindSession(s_session_id);
57
+ }
58
+ }
59
+ }
60
+ exports.MariaDbSessionSweeper = MariaDbSessionSweeper;
@@ -147,12 +147,11 @@ export declare const SCHEMA_SVC_POOL_HARD_CAP = 2;
147
147
  * Build the {@link PoolBudgetConfig} from env with documented defaults.
148
148
  * Field ↔ env name:
149
149
  * workers ← POOL_WORKERS
150
- * maxConnections ← DB_MAX_CONNECTIONS
150
+ * maxConnections ← APP_DB_MAX_CONNECTIONS
151
151
  * headroom ← POOL_HEADROOM
152
152
  * perAccountMax.shield_core ← POOL_MAX_CORE
153
153
  * perAccountMax.shield_app_crud ← POOL_MAX_APP_CRUD
154
154
  * perAccountMax.shield_schema_svc ← POOL_MAX_SCHEMA_SVC
155
- * (Legacy `SHIELD_`-prefixed names are still read as a fallback — see {@link readEnv}.)
156
155
  */
157
156
  export declare function loadPoolBudget(env?: NodeJS.ProcessEnv): PoolBudgetConfig;
158
157
  /** Documented defaults for the runtime MariaDB descriptor (local docker substrate). */
@@ -162,9 +161,9 @@ export declare const DEFAULT_REDIS_URL = "redis://localhost:6379";
162
161
  /**
163
162
  * Build the runtime {@link DbConfig} from env with documented defaults.
164
163
  * Field ↔ env name:
165
- * host ← DB_HOST port ← DB_PORT
166
- * user ← DB_USER password ← DB_PASSWORD (secret)
167
- * database ← DB_NAME (legacy SHIELD_DB_* still read as a fallback)
164
+ * host ← APP_DB_HOST port ← APP_DB_PORT
165
+ * user ← APP_DB_USER password ← APP_DB_PASSWORD (secret)
166
+ * database ← APP_DB_NAME
168
167
  * The password defaults to '' when absent — a live connection then fails at
169
168
  * connect time (loud), which is correct for a runtime that needs real creds; the
170
169
  * secret is deliberately NOT hard-required at config load (unlike the HMAC key,
@@ -174,7 +173,7 @@ export declare function loadDbConfig(env?: NodeJS.ProcessEnv): DbConfig;
174
173
  /**
175
174
  * Build the {@link RedisConfig} from env. Prefers `REDIS_URL`; falls back
176
175
  * to composing `redis://host:port` from `REDIS_HOST`/`REDIS_PORT`, else the
177
- * documented default. (Legacy `SHIELD_REDIS_*` names still read as a fallback.)
176
+ * documented default.
178
177
  */
179
178
  export declare function loadRedisConfig(env?: NodeJS.ProcessEnv): RedisConfig;
180
179
  /**
@@ -186,7 +185,6 @@ export declare function loadRedisConfig(env?: NodeJS.ProcessEnv): RedisConfig;
186
185
  * accessTtlMs ← ACCESS_TTL_MS
187
186
  * refreshIdleMs ← REFRESH_IDLE_MS
188
187
  * refreshAbsoluteCapMs ← REFRESH_ABSOLUTE_CAP_MS
189
- * (Legacy `SHIELD_`-prefixed names still read as a fallback.)
190
188
  */
191
189
  export declare function loadSessionLifetimes(env?: NodeJS.ProcessEnv): SessionLifetimeOverrides;
192
190
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"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"}
@@ -48,22 +48,21 @@ function parsePositiveInt(raw, fallback) {
48
48
  * Build the {@link PoolBudgetConfig} from env with documented defaults.
49
49
  * Field ↔ env name:
50
50
  * workers ← POOL_WORKERS
51
- * maxConnections ← DB_MAX_CONNECTIONS
51
+ * maxConnections ← APP_DB_MAX_CONNECTIONS
52
52
  * headroom ← POOL_HEADROOM
53
53
  * perAccountMax.shield_core ← POOL_MAX_CORE
54
54
  * perAccountMax.shield_app_crud ← POOL_MAX_APP_CRUD
55
55
  * perAccountMax.shield_schema_svc ← POOL_MAX_SCHEMA_SVC
56
- * (Legacy `SHIELD_`-prefixed names are still read as a fallback — see {@link readEnv}.)
57
56
  */
58
57
  function loadPoolBudget(env = process.env) {
59
58
  return {
60
- workers: parsePositiveInt(readEnv(env, 'POOL_WORKERS'), exports.DEFAULT_POOL_BUDGET.workers),
61
- maxConnections: parsePositiveInt(readEnv(env, 'DB_MAX_CONNECTIONS'), exports.DEFAULT_POOL_BUDGET.maxConnections),
62
- headroom: parsePositiveInt(readEnv(env, 'POOL_HEADROOM'), exports.DEFAULT_POOL_BUDGET.headroom),
59
+ workers: parsePositiveInt(env.POOL_WORKERS, exports.DEFAULT_POOL_BUDGET.workers),
60
+ maxConnections: parsePositiveInt(env.APP_DB_MAX_CONNECTIONS, exports.DEFAULT_POOL_BUDGET.maxConnections),
61
+ headroom: parsePositiveInt(env.POOL_HEADROOM, exports.DEFAULT_POOL_BUDGET.headroom),
63
62
  perAccountMax: {
64
- shield_core: parsePositiveInt(readEnv(env, 'POOL_MAX_CORE'), exports.DEFAULT_POOL_BUDGET.perAccountMax.shield_core),
65
- shield_app_crud: parsePositiveInt(readEnv(env, 'POOL_MAX_APP_CRUD'), exports.DEFAULT_POOL_BUDGET.perAccountMax.shield_app_crud),
66
- shield_schema_svc: parsePositiveInt(readEnv(env, 'POOL_MAX_SCHEMA_SVC'), exports.DEFAULT_POOL_BUDGET.perAccountMax.shield_schema_svc),
63
+ shield_core: parsePositiveInt(env.POOL_MAX_CORE, exports.DEFAULT_POOL_BUDGET.perAccountMax.shield_core),
64
+ shield_app_crud: parsePositiveInt(env.POOL_MAX_APP_CRUD, exports.DEFAULT_POOL_BUDGET.perAccountMax.shield_app_crud),
65
+ shield_schema_svc: parsePositiveInt(env.POOL_MAX_SCHEMA_SVC, exports.DEFAULT_POOL_BUDGET.perAccountMax.shield_schema_svc),
67
66
  },
68
67
  };
69
68
  }
@@ -81,36 +80,6 @@ exports.DEFAULT_DB_CONFIG = {
81
80
  };
82
81
  /** Documented default Redis url (local docker substrate). */
83
82
  exports.DEFAULT_REDIS_URL = 'redis://localhost:6379';
84
- /**
85
- * Read a consumer-facing env var by its CURRENT (unprefixed) name, falling back to
86
- * the LEGACY `SHIELD_`-prefixed name for one deprecation window. The framework's
87
- * public config vars were debranded from `SHIELD_*` to bare names (e.g.
88
- * `SHIELD_DB_HOST` → `DB_HOST`); reading both keeps existing `.env` files (which may
89
- * still set `SHIELD_*`) working unchanged. The new name wins when both are set.
90
- * A non-blank legacy value emits a one-time deprecation warning (never logs secrets —
91
- * only the var NAME). Internal DI-token symbols (`Symbol('SHIELD_...')`) are unrelated
92
- * and unchanged.
93
- */
94
- const legacyEnvWarned = new Set();
95
- function readEnv(env, name) {
96
- const current = env[name];
97
- if (current !== undefined && current.trim() !== '')
98
- return current;
99
- const legacyName = `SHIELD_${name}`;
100
- const legacy = env[legacyName];
101
- if (legacy !== undefined && legacy.trim() !== '') {
102
- if (!legacyEnvWarned.has(legacyName)) {
103
- legacyEnvWarned.add(legacyName);
104
- // eslint-disable-next-line no-console
105
- console.warn(`[astram] env var '${legacyName}' is deprecated — rename it to '${name}'. ` +
106
- `The SHIELD_ prefix will be removed in a future release.`);
107
- }
108
- return legacy;
109
- }
110
- // Return whichever was present-but-blank (so parseString applies its own fallback),
111
- // preferring the current name.
112
- return current ?? legacy;
113
- }
114
83
  /** Read a string from env with a fallback (trims; blank ⇒ fallback). */
115
84
  function parseString(raw, fallback) {
116
85
  if (raw === undefined || raw.trim() === '')
@@ -132,9 +101,9 @@ function parseOptionalPositiveInt(raw) {
132
101
  /**
133
102
  * Build the runtime {@link DbConfig} from env with documented defaults.
134
103
  * Field ↔ env name:
135
- * host ← DB_HOST port ← DB_PORT
136
- * user ← DB_USER password ← DB_PASSWORD (secret)
137
- * database ← DB_NAME (legacy SHIELD_DB_* still read as a fallback)
104
+ * host ← APP_DB_HOST port ← APP_DB_PORT
105
+ * user ← APP_DB_USER password ← APP_DB_PASSWORD (secret)
106
+ * database ← APP_DB_NAME
138
107
  * The password defaults to '' when absent — a live connection then fails at
139
108
  * connect time (loud), which is correct for a runtime that needs real creds; the
140
109
  * secret is deliberately NOT hard-required at config load (unlike the HMAC key,
@@ -142,25 +111,25 @@ function parseOptionalPositiveInt(raw) {
142
111
  */
143
112
  function loadDbConfig(env = process.env) {
144
113
  return {
145
- host: parseString(readEnv(env, 'DB_HOST'), exports.DEFAULT_DB_CONFIG.host),
146
- port: parsePort(readEnv(env, 'DB_PORT'), exports.DEFAULT_DB_CONFIG.port),
147
- user: parseString(readEnv(env, 'DB_USER'), exports.DEFAULT_DB_CONFIG.user),
148
- password: parseString(readEnv(env, 'DB_PASSWORD'), ''),
149
- database: parseString(readEnv(env, 'DB_NAME'), exports.DEFAULT_DB_CONFIG.database),
114
+ host: parseString(env.APP_DB_HOST, exports.DEFAULT_DB_CONFIG.host),
115
+ port: parsePort(env.APP_DB_PORT, exports.DEFAULT_DB_CONFIG.port),
116
+ user: parseString(env.APP_DB_USER, exports.DEFAULT_DB_CONFIG.user),
117
+ password: parseString(env.APP_DB_PASSWORD, ''),
118
+ database: parseString(env.APP_DB_NAME, exports.DEFAULT_DB_CONFIG.database),
150
119
  };
151
120
  }
152
121
  /**
153
122
  * Build the {@link RedisConfig} from env. Prefers `REDIS_URL`; falls back
154
123
  * to composing `redis://host:port` from `REDIS_HOST`/`REDIS_PORT`, else the
155
- * documented default. (Legacy `SHIELD_REDIS_*` names still read as a fallback.)
124
+ * documented default.
156
125
  */
157
126
  function loadRedisConfig(env = process.env) {
158
- const explicit = readEnv(env, 'REDIS_URL');
127
+ const explicit = env.REDIS_URL;
159
128
  if (explicit !== undefined && explicit.trim() !== '') {
160
129
  return { url: explicit.trim() };
161
130
  }
162
- const host = readEnv(env, 'REDIS_HOST');
163
- const port = readEnv(env, 'REDIS_PORT');
131
+ const host = env.REDIS_HOST;
132
+ const port = env.REDIS_PORT;
164
133
  if ((host !== undefined && host.trim() !== '') || (port !== undefined && port.trim() !== '')) {
165
134
  const h = parseString(host, 'localhost');
166
135
  const p = parsePort(port, 6379);
@@ -177,13 +146,12 @@ function loadRedisConfig(env = process.env) {
177
146
  * accessTtlMs ← ACCESS_TTL_MS
178
147
  * refreshIdleMs ← REFRESH_IDLE_MS
179
148
  * refreshAbsoluteCapMs ← REFRESH_ABSOLUTE_CAP_MS
180
- * (Legacy `SHIELD_`-prefixed names still read as a fallback.)
181
149
  */
182
150
  function loadSessionLifetimes(env = process.env) {
183
151
  const overrides = {};
184
- const access = parseOptionalPositiveInt(readEnv(env, 'ACCESS_TTL_MS'));
185
- const idle = parseOptionalPositiveInt(readEnv(env, 'REFRESH_IDLE_MS'));
186
- const cap = parseOptionalPositiveInt(readEnv(env, 'REFRESH_ABSOLUTE_CAP_MS'));
152
+ const access = parseOptionalPositiveInt(env.ACCESS_TTL_MS);
153
+ const idle = parseOptionalPositiveInt(env.REFRESH_IDLE_MS);
154
+ const cap = parseOptionalPositiveInt(env.REFRESH_ABSOLUTE_CAP_MS);
187
155
  if (access !== undefined)
188
156
  overrides.accessTtlMs = access;
189
157
  if (idle !== undefined)
@@ -199,7 +167,7 @@ function loadSessionLifetimes(env = process.env) {
199
167
  * the key's value.
200
168
  */
201
169
  function loadRefreshHmacKey(env = process.env) {
202
- return parseString(readEnv(env, 'REFRESH_HMAC_KEY'), '');
170
+ return parseString(env.REFRESH_HMAC_KEY, '');
203
171
  }
204
172
  /**
205
173
  * ⛔ FAIL-CLOSED GATE for the refresh-token HMAC key. Called by the auth
@@ -220,8 +188,7 @@ function assertRefreshHmacKey(key) {
220
188
  if (typeof key !== 'string' || key.trim() === '') {
221
189
  throw new Error('Astram auth composition: REFRESH_HMAC_KEY is required and must be ' +
222
190
  'non-empty (fail-closed — refusing to compose auth with a blank refresh-token ' +
223
- 'HMAC key). Set it from your secrets manager; never commit the value. ' +
224
- '(The legacy env var name SHIELD_REFRESH_HMAC_KEY is still accepted.)');
191
+ 'HMAC key). Set it from your secrets manager; never commit the value.');
225
192
  }
226
193
  return key;
227
194
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * AuthFactsController — the framework-DEFAULT `/identity/auth-facts` + `/identity/permissions-version`
3
+ * endpoints backing the frontend's permission-gated nav (the SDK's fact projection). Both are
4
+ * `@Public()` at the route level but SELF-SCOPED: principalId + tenantId come ONLY from the resolved
5
+ * bearer-token Principal (never a query/body param). Missing/invalid token → 401; an authorization
6
+ * store outage → 503 (never swallowed into an empty projection / fabricated version — that would
7
+ * fail-OPEN by letting a stale/empty view read as authoritative).
8
+ *
9
+ * FRAMEWORK-OWNED: mounted by the auth surface when `auth: true`; the consumer implements nothing.
10
+ */
11
+ import type { SessionTokenBinder } from '../request-auth/session-token-binder.js';
12
+ import { type AuthFactProjection, type MariaDbPermissionFactsAdapter } from './permission-facts.adapter.js';
13
+ export declare class AuthFactsController {
14
+ private readonly tokenBinder;
15
+ private readonly facts;
16
+ constructor(tokenBinder: SessionTokenBinder | undefined, facts: MariaDbPermissionFactsAdapter | undefined);
17
+ getAuthFacts(authorization?: string): Promise<AuthFactProjection>;
18
+ getPermissionsVersion(authorization?: string): Promise<{
19
+ version: number;
20
+ }>;
21
+ /** Resolve the bearer token → the caller's own Principal, or throw a generic 401. */
22
+ private authenticate;
23
+ }
24
+ //# sourceMappingURL=auth-facts.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-facts.controller.d.ts","sourceRoot":"","sources":["../../src/identity/auth-facts.controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAGlF,OAAO,EAAiC,KAAK,kBAAkB,EAAE,KAAK,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAE3I,qBACa,mBAAmB;IAEqB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADD,WAAW,EAAE,kBAAkB,GAAG,SAAS,EAC/C,KAAK,EAAE,6BAA6B,GAAG,SAAS;IAK1G,YAAY,CAA2B,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAiB3F,qBAAqB,CAA2B,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAgB3G,qFAAqF;YACvE,YAAY;CAU3B"}