@astram/guard 0.13.0 → 0.15.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 (134) hide show
  1. package/dist/auth/auth.module.d.ts.map +1 -1
  2. package/dist/auth/auth.module.js +57 -30
  3. package/dist/auth/connections.d.ts +2 -2
  4. package/dist/auth/connections.d.ts.map +1 -1
  5. package/dist/auth/connections.js +4 -13
  6. package/dist/authentication/mariadb-api-key-store.d.ts +10 -1
  7. package/dist/authentication/mariadb-api-key-store.d.ts.map +1 -1
  8. package/dist/authentication/mariadb-api-key-store.js +20 -10
  9. package/dist/authentication/mariadb-credential-resolver.d.ts +8 -1
  10. package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
  11. package/dist/authentication/mariadb-credential-resolver.js +15 -5
  12. package/dist/authentication/mariadb-credential-writer.d.ts +6 -1
  13. package/dist/authentication/mariadb-credential-writer.d.ts.map +1 -1
  14. package/dist/authentication/mariadb-credential-writer.js +9 -3
  15. package/dist/authentication/mariadb-email-address-changer.d.ts +10 -1
  16. package/dist/authentication/mariadb-email-address-changer.d.ts.map +1 -1
  17. package/dist/authentication/mariadb-email-address-changer.js +22 -9
  18. package/dist/authentication/mariadb-email-change-token-store.d.ts +8 -1
  19. package/dist/authentication/mariadb-email-change-token-store.d.ts.map +1 -1
  20. package/dist/authentication/mariadb-email-change-token-store.js +18 -9
  21. package/dist/authentication/mariadb-mfa-device-store.d.ts +6 -1
  22. package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -1
  23. package/dist/authentication/mariadb-mfa-device-store.js +22 -13
  24. package/dist/authentication/mariadb-profile-resolver.d.ts +6 -1
  25. package/dist/authentication/mariadb-profile-resolver.d.ts.map +1 -1
  26. package/dist/authentication/mariadb-profile-resolver.js +12 -6
  27. package/dist/authentication/mariadb-profile-writer.d.ts +6 -1
  28. package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
  29. package/dist/authentication/mariadb-profile-writer.js +11 -4
  30. package/dist/authentication/mariadb-recovery-code-store.d.ts +6 -1
  31. package/dist/authentication/mariadb-recovery-code-store.d.ts.map +1 -1
  32. package/dist/authentication/mariadb-recovery-code-store.js +11 -5
  33. package/dist/authentication/mariadb-reset-token-store.d.ts +6 -1
  34. package/dist/authentication/mariadb-reset-token-store.d.ts.map +1 -1
  35. package/dist/authentication/mariadb-reset-token-store.js +11 -5
  36. package/dist/authentication/mariadb-session-sweeper.d.ts +6 -1
  37. package/dist/authentication/mariadb-session-sweeper.d.ts.map +1 -1
  38. package/dist/authentication/mariadb-session-sweeper.js +11 -5
  39. package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -1
  40. package/dist/authz/cross-tenant-grant-writer.js +4 -3
  41. package/dist/authz/list-scope-sql.d.ts +10 -2
  42. package/dist/authz/list-scope-sql.d.ts.map +1 -1
  43. package/dist/authz/list-scope-sql.js +4 -4
  44. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -1
  45. package/dist/authz/mariadb-cross-tenant-grant-adapter.js +4 -3
  46. package/dist/authz/mariadb-list-scope-adapter.d.ts.map +1 -1
  47. package/dist/authz/mariadb-list-scope-adapter.js +4 -3
  48. package/dist/authz/mariadb-policy-store.d.ts.map +1 -1
  49. package/dist/authz/mariadb-policy-store.js +4 -3
  50. package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -1
  51. package/dist/authz/mariadb-record-rule-adapter.js +4 -3
  52. package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -1
  53. package/dist/authz/mariadb-record-share-adapter.js +4 -3
  54. package/dist/authz/record-share-cascade.d.ts.map +1 -1
  55. package/dist/authz/record-share-cascade.js +4 -3
  56. package/dist/authz/record-share-writer.d.ts.map +1 -1
  57. package/dist/authz/record-share-writer.js +4 -3
  58. package/dist/authz/wire-record-authorization.d.ts +8 -0
  59. package/dist/authz/wire-record-authorization.d.ts.map +1 -1
  60. package/dist/authz/wire-record-authorization.js +14 -6
  61. package/dist/boot/boot-sequence.d.ts +4 -0
  62. package/dist/boot/boot-sequence.d.ts.map +1 -1
  63. package/dist/boot/boot-sequence.js +13 -1
  64. package/dist/boot/migration-ledger-check.d.ts +94 -0
  65. package/dist/boot/migration-ledger-check.d.ts.map +1 -0
  66. package/dist/boot/migration-ledger-check.js +280 -0
  67. package/dist/config/app-config.d.ts +72 -3
  68. package/dist/config/app-config.d.ts.map +1 -1
  69. package/dist/config/app-config.js +129 -7
  70. package/dist/config/config.module.d.ts +7 -0
  71. package/dist/config/config.module.d.ts.map +1 -1
  72. package/dist/config/config.module.js +13 -2
  73. package/dist/config/framework-schema-names.d.ts +29 -0
  74. package/dist/config/framework-schema-names.d.ts.map +1 -0
  75. package/dist/config/framework-schema-names.js +41 -0
  76. package/dist/config/loopback-host.d.ts +9 -0
  77. package/dist/config/loopback-host.d.ts.map +1 -0
  78. package/dist/config/loopback-host.js +19 -0
  79. package/dist/encryption/local-kek-crypto.d.ts +6 -0
  80. package/dist/encryption/local-kek-crypto.d.ts.map +1 -1
  81. package/dist/encryption/local-kek-crypto.js +8 -0
  82. package/dist/identity/identity-actions.d.ts +18 -7
  83. package/dist/identity/identity-actions.d.ts.map +1 -1
  84. package/dist/identity/identity-actions.js +16 -17
  85. package/dist/identity/module-registry.bridge.d.ts.map +1 -1
  86. package/dist/identity/module-registry.bridge.js +5 -3
  87. package/dist/identity/permission-facts.adapter.d.ts +7 -1
  88. package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
  89. package/dist/identity/permission-facts.adapter.js +15 -6
  90. package/dist/index.d.ts +14 -3
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +68 -22
  93. package/dist/module/shield-tokens.d.ts +11 -0
  94. package/dist/module/shield-tokens.d.ts.map +1 -1
  95. package/dist/module/shield-tokens.js +12 -1
  96. package/dist/module/shield.module.d.ts +10 -0
  97. package/dist/module/shield.module.d.ts.map +1 -1
  98. package/dist/module/shield.module.js +78 -12
  99. package/dist/secrets/boot-secrets.d.ts +33 -0
  100. package/dist/secrets/boot-secrets.d.ts.map +1 -0
  101. package/dist/secrets/boot-secrets.js +180 -0
  102. package/dist/session/mariadb-session-record-store.d.ts +8 -1
  103. package/dist/session/mariadb-session-record-store.d.ts.map +1 -1
  104. package/dist/session/mariadb-session-record-store.js +23 -13
  105. package/dist/session/session-restriction-store.d.ts +8 -1
  106. package/dist/session/session-restriction-store.d.ts.map +1 -1
  107. package/dist/session/session-restriction-store.js +15 -5
  108. package/dist/storage/file-intake.interceptor.d.ts +54 -0
  109. package/dist/storage/file-intake.interceptor.d.ts.map +1 -0
  110. package/dist/storage/file-intake.interceptor.js +223 -0
  111. package/dist/storage/node-sha256-digest.d.ts +6 -0
  112. package/dist/storage/node-sha256-digest.d.ts.map +1 -0
  113. package/dist/storage/node-sha256-digest.js +15 -0
  114. package/dist/storage/object-store-boot-check.d.ts +16 -0
  115. package/dist/storage/object-store-boot-check.d.ts.map +1 -0
  116. package/dist/storage/object-store-boot-check.js +52 -0
  117. package/dist/storage/optional-peer.d.ts +10 -0
  118. package/dist/storage/optional-peer.d.ts.map +1 -0
  119. package/dist/storage/optional-peer.js +26 -0
  120. package/dist/storage/s3-object-store.d.ts +35 -0
  121. package/dist/storage/s3-object-store.d.ts.map +1 -0
  122. package/dist/storage/s3-object-store.js +238 -0
  123. package/dist/storage/storage.providers.d.ts +10 -0
  124. package/dist/storage/storage.providers.d.ts.map +1 -0
  125. package/dist/storage/storage.providers.js +104 -0
  126. package/migrations/1710000000006-CreateAuthzSchema.cjs +15 -4
  127. package/migrations/1710000000007-ExtendPermissionAndRolePermission.cjs +11 -3
  128. package/migrations/1710000000008-CreateAccessBundleTables.cjs +14 -4
  129. package/migrations/1710000000009-CreateRecordPermissionRule.cjs +15 -4
  130. package/migrations/1710000000014-CreateRecordShare.cjs +15 -4
  131. package/migrations/1710000000015-CreateScopeClosure.cjs +14 -4
  132. package/migrations/1710000000016-AlterCompanyParentFkRestrict.cjs +20 -9
  133. package/migrations/1710000000017-CreateServiceAccountKey.cjs +14 -4
  134. package/package.json +11 -3
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MariaDbEmailAddressChanger = void 0;
4
+ const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
4
5
  /** Seconds to wait for the email-change named lock before giving up (caller → 503). */
5
6
  const LOCK_TIMEOUT_S = 10;
6
7
  /** MariaDB `ER_DUP_ENTRY`: a unique index (here `uq_users_email` or the hint PK) refused a write. */
@@ -15,13 +16,25 @@ function isDuplicateKey(err) {
15
16
  class MariaDbEmailAddressChanger {
16
17
  pool;
17
18
  now;
18
- constructor(pool, now = Date.now) {
19
+ /** `<app>`.`users`. */
20
+ usersTable;
21
+ /** `<identity>`.`identity_credential`. */
22
+ credentialTable;
23
+ /** `<identity>`.`email_change_token`. */
24
+ tokenTable;
25
+ constructor(pool, now = Date.now,
26
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
27
+ schemas) {
19
28
  this.pool = pool;
20
29
  this.now = now;
30
+ const s = (0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas);
31
+ this.usersTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.app, 'users');
32
+ this.credentialTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.identity, 'identity_credential');
33
+ this.tokenTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.identity, 'email_change_token');
21
34
  }
22
35
  async readCurrent(principalId, candidate) {
23
36
  const rows = (await this.pool.query(`SELECT u_email, u_status, (u_email = ?) AS same_as_candidate
24
- FROM \`shield_app\`.\`users\` WHERE u_uuid = ? LIMIT 1`, [candidate, principalId]));
37
+ FROM ${this.usersTable} WHERE u_uuid = ? LIMIT 1`, [candidate, principalId]));
25
38
  const row = rows[0];
26
39
  if (row === undefined)
27
40
  return null;
@@ -66,7 +79,7 @@ class MariaDbEmailAddressChanger {
66
79
  }
67
80
  async changeLocked(conn, request) {
68
81
  const { principalId, newEmail } = request;
69
- const self = (await conn.query('SELECT u_email, u_status FROM `shield_app`.`users` WHERE u_uuid = ? FOR UPDATE', [principalId]));
82
+ const self = (await conn.query(`SELECT u_email, u_status FROM ${this.usersTable} WHERE u_uuid = ? FOR UPDATE`, [principalId]));
70
83
  const row = self[0];
71
84
  if (row === undefined)
72
85
  return { status: 'not_found' };
@@ -76,24 +89,24 @@ class MariaDbEmailAddressChanger {
76
89
  if (request.expectedOldEmail !== undefined && oldEmail !== request.expectedOldEmail) {
77
90
  return { status: 'stale' };
78
91
  }
79
- const otherUser = (await conn.query('SELECT u_uuid FROM `shield_app`.`users` WHERE u_email = ? AND u_uuid <> ? LIMIT 1 FOR UPDATE', [newEmail, principalId]));
92
+ const otherUser = (await conn.query(`SELECT u_uuid FROM ${this.usersTable} WHERE u_email = ? AND u_uuid <> ? LIMIT 1 FOR UPDATE`, [newEmail, principalId]));
80
93
  if (otherUser.length > 0)
81
94
  return { status: 'unavailable' };
82
- const hintOwners = (await conn.query('SELECT ic_principal_id FROM `shield_identity`.`identity_credential` WHERE ic_identity_hint = ? FOR UPDATE', [newEmail]));
95
+ const hintOwners = (await conn.query(`SELECT ic_principal_id FROM ${this.credentialTable} WHERE ic_identity_hint = ? FOR UPDATE`, [newEmail]));
83
96
  if (hintOwners.some((h) => h.ic_principal_id !== principalId))
84
97
  return { status: 'unavailable' };
85
98
  const selfAlreadyHasNewHint = hintOwners.length > 0;
86
- await conn.query('SELECT ic_identity_hint FROM `shield_identity`.`identity_credential` WHERE ic_principal_id = ? FOR UPDATE', [principalId]);
99
+ await conn.query(`SELECT ic_identity_hint FROM ${this.credentialTable} WHERE ic_principal_id = ? FOR UPDATE`, [principalId]);
87
100
  // Steps 3–4: a duplicate key here is a row the locked reads could not see (written by a path
88
101
  // without the named lock); the index refuses it → `unavailable`, and `change` rolls back.
89
102
  let hintMoved = false;
90
103
  try {
91
- await conn.query('UPDATE `shield_app`.`users` SET u_email = ? WHERE u_uuid = ?', [
104
+ await conn.query(`UPDATE ${this.usersTable} SET u_email = ? WHERE u_uuid = ?`, [
92
105
  newEmail,
93
106
  principalId,
94
107
  ]);
95
108
  if (oldEmail !== null && oldEmail !== '' && !selfAlreadyHasNewHint) {
96
- const moved = (await conn.query(`UPDATE \`shield_identity\`.\`identity_credential\`
109
+ const moved = (await conn.query(`UPDATE ${this.credentialTable}
97
110
  SET ic_identity_hint = ?, ic_updated_at_ms = ?
98
111
  WHERE ic_principal_id = ? AND ic_identity_hint = ?`, [newEmail, this.now(), principalId, oldEmail]));
99
112
  hintMoved = moved.affectedRows > 0;
@@ -104,7 +117,7 @@ class MariaDbEmailAddressChanger {
104
117
  return { status: 'unavailable' }; // the driver message is dropped
105
118
  throw err;
106
119
  }
107
- await conn.query(`UPDATE \`shield_identity\`.\`email_change_token\` SET ect_consumed_at_ms = ?
120
+ await conn.query(`UPDATE ${this.tokenTable} SET ect_consumed_at_ms = ?
108
121
  WHERE ect_principal_id = ? AND ect_consumed_at_ms IS NULL`, [this.now(), principalId]);
109
122
  if (request.withinTransaction !== undefined) {
110
123
  await request.withinTransaction((sql, params) => conn.query(sql, params ?? []), { oldEmail, hintMoved });
@@ -14,10 +14,17 @@
14
14
  */
15
15
  import type { Pool } from 'mariadb';
16
16
  import type { EmailChangeTokenBinding, EmailChangeTokenIssue, EmailChangeTokenStore } from '../auth/ports/email-change-token-store.js';
17
+ import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
17
18
  export declare class MariaDbEmailChangeTokenStore implements EmailChangeTokenStore {
18
19
  private readonly pool;
19
20
  private readonly now;
20
- constructor(pool: Pool, now?: () => number);
21
+ /** `<identity>`.`email_change_token`. */
22
+ private readonly table;
23
+ /** `<app>`.`users`. */
24
+ private readonly usersTable;
25
+ constructor(pool: Pool, now?: () => number,
26
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
27
+ schemas?: FrameworkSchemaNames);
21
28
  issue(token: EmailChangeTokenIssue): Promise<void>;
22
29
  consume(tokenHash: string): Promise<EmailChangeTokenBinding | null>;
23
30
  invalidateAllForPrincipal(principalId: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"mariadb-email-change-token-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-email-change-token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,IAAI,EAAkB,MAAM,SAAS,CAAC;AACpD,OAAO,KAAK,EACV,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,2CAA2C,CAAC;AAYnD,qBAAa,4BAA6B,YAAW,qBAAqB;IAEtE,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBADH,IAAI,EAAE,IAAI,EACV,GAAG,GAAE,MAAM,MAAiB;IAGzC,KAAK,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IA2ClD,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC;IAwBnE,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAOpE"}
1
+ {"version":3,"file":"mariadb-email-change-token-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-email-change-token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,IAAI,EAAkB,MAAM,SAAS,CAAC;AACpD,OAAO,KAAK,EACV,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAU7C,qBAAa,4BAA6B,YAAW,qBAAqB;IAOtE,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,yCAAyC;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,uBAAuB;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAGjB,IAAI,EAAE,IAAI,EACV,GAAG,GAAE,MAAM,MAAiB;IAC7C,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAO1B,KAAK,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IA2ClD,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC;IAwBnE,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAOpE"}
@@ -1,13 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MariaDbEmailChangeTokenStore = void 0;
4
- const TABLE = '`shield_identity`.`email_change_token`';
4
+ const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
5
5
  class MariaDbEmailChangeTokenStore {
6
6
  pool;
7
7
  now;
8
- constructor(pool, now = Date.now) {
8
+ /** `<identity>`.`email_change_token`. */
9
+ table;
10
+ /** `<app>`.`users`. */
11
+ usersTable;
12
+ constructor(pool, now = Date.now,
13
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
14
+ schemas) {
9
15
  this.pool = pool;
10
16
  this.now = now;
17
+ const s = (0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas);
18
+ this.table = (0, framework_schema_names_js_1.qualifiedTableName)(s.identity, 'email_change_token');
19
+ this.usersTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.app, 'users');
11
20
  }
12
21
  async issue(token) {
13
22
  const now = this.now();
@@ -15,13 +24,13 @@ class MariaDbEmailChangeTokenStore {
15
24
  try {
16
25
  await conn.beginTransaction();
17
26
  try {
18
- const user = (await conn.query('SELECT u_uuid FROM `shield_app`.`users` WHERE u_uuid = ? FOR UPDATE', [token.principalId]));
27
+ const user = (await conn.query(`SELECT u_uuid FROM ${this.usersTable} WHERE u_uuid = ? FOR UPDATE`, [token.principalId]));
19
28
  if (user.length === 0)
20
29
  throw new Error('email change: unknown user');
21
- await conn.query(`DELETE FROM ${TABLE} WHERE ect_expires_at_ms <= ?`, [now]);
22
- await conn.query(`UPDATE ${TABLE} SET ect_consumed_at_ms = ?
30
+ await conn.query(`DELETE FROM ${this.table} WHERE ect_expires_at_ms <= ?`, [now]);
31
+ await conn.query(`UPDATE ${this.table} SET ect_consumed_at_ms = ?
23
32
  WHERE ect_principal_id = ? AND ect_consumed_at_ms IS NULL`, [now, token.principalId]);
24
- await conn.query(`INSERT INTO ${TABLE}
33
+ await conn.query(`INSERT INTO ${this.table}
25
34
  (ect_token_hash, ect_principal_id, ect_new_email, ect_old_email, ect_session_id,
26
35
  ect_tenant_id, ect_expires_at_ms, ect_consumed_at_ms, ect_created_at_ms)
27
36
  VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?)`, [
@@ -47,12 +56,12 @@ class MariaDbEmailChangeTokenStore {
47
56
  }
48
57
  async consume(tokenHash) {
49
58
  const now = this.now();
50
- const res = (await this.pool.query(`UPDATE ${TABLE} SET ect_consumed_at_ms = ?
59
+ const res = (await this.pool.query(`UPDATE ${this.table} SET ect_consumed_at_ms = ?
51
60
  WHERE ect_token_hash = ? AND ect_consumed_at_ms IS NULL AND ect_expires_at_ms > ?`, [now, tokenHash, now]));
52
61
  if (res.affectedRows !== 1)
53
62
  return null;
54
63
  const rows = (await this.pool.query(`SELECT ect_principal_id, ect_new_email, ect_old_email, ect_session_id, ect_tenant_id
55
- FROM ${TABLE} WHERE ect_token_hash = ? LIMIT 1`, [tokenHash]));
64
+ FROM ${this.table} WHERE ect_token_hash = ? LIMIT 1`, [tokenHash]));
56
65
  const row = rows[0];
57
66
  if (row === undefined)
58
67
  return null; // the user was deleted in between (FK cascade)
@@ -65,7 +74,7 @@ class MariaDbEmailChangeTokenStore {
65
74
  };
66
75
  }
67
76
  async invalidateAllForPrincipal(principalId) {
68
- await this.pool.query(`UPDATE ${TABLE} SET ect_consumed_at_ms = ?
77
+ await this.pool.query(`UPDATE ${this.table} SET ect_consumed_at_ms = ?
69
78
  WHERE ect_principal_id = ? AND ect_consumed_at_ms IS NULL`, [this.now(), principalId]);
70
79
  }
71
80
  }
@@ -1,8 +1,13 @@
1
1
  import type { Pool } from 'mariadb';
2
2
  import type { MfaDevice, MfaDeviceStore } from '../auth/ports/mfa-device-store.js';
3
+ import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
3
4
  export declare class MariaDbMfaDeviceStore implements MfaDeviceStore {
4
5
  private readonly pool;
5
- constructor(pool: Pool);
6
+ /** `<identity>`.`mfa_device`, resolved at construction. */
7
+ private readonly table;
8
+ constructor(pool: Pool,
9
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
10
+ schemas?: FrameworkSchemaNames);
6
11
  listAll(principalId: string): Promise<MfaDevice[]>;
7
12
  listConfirmedSecrets(principalId: string): Promise<string[]>;
8
13
  getSecret(principalId: string, deviceId: string): Promise<string | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"mariadb-mfa-device-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-mfa-device-store.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAuBnF,qBAAa,qBAAsB,YAAW,cAAc;IAC9C,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEjC,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IASlD,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAS5D,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IASxE,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBzE,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAkBhE,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAetF,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQ/D,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IASpD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAU/C"}
1
+ {"version":3,"file":"mariadb-mfa-device-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-mfa-device-store.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACnF,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAuB7C,qBAAa,qBAAsB,YAAW,cAAc;IAKxD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJvB,2DAA2D;IAC3D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAGZ,IAAI,EAAE,IAAI;IAC3B,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAK1B,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IASlD,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAS5D,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IASxE,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBzE,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAkBhE,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAetF,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQ/D,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IASpD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAU/C"}
@@ -24,6 +24,7 @@ exports.MariaDbMfaDeviceStore = void 0;
24
24
  * other method's genuine store outage propagates (fails loud), never swallowed.
25
25
  */
26
26
  const node_crypto_1 = require("node:crypto");
27
+ const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
27
28
  /** Pending-device TTL — a `begin` never confirmed within this window is self-pruned. */
28
29
  const PENDING_TTL_MS = 15 * 60 * 1000;
29
30
  /** Map a DB row → the display shape (NO secret). BIGINT columns arrive as bigint → Number. */
@@ -36,67 +37,75 @@ const toDevice = (r) => ({
36
37
  });
37
38
  class MariaDbMfaDeviceStore {
38
39
  pool;
39
- constructor(pool) {
40
+ /** `<identity>`.`mfa_device`, resolved at construction. */
41
+ table;
42
+ constructor(pool,
43
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
44
+ schemas) {
40
45
  this.pool = pool;
46
+ this.table = (0, framework_schema_names_js_1.qualifiedTableName)((0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas).identity, 'mfa_device');
41
47
  }
42
48
  async listAll(principalId) {
43
49
  const rows = (await this.pool.query(`SELECT md_id, md_name, md_secret, md_created_at_ms, md_last_used_at_ms, md_confirmed_at_ms
44
- FROM \`shield_identity\`.\`mfa_device\` WHERE md_principal_id = ? ORDER BY md_created_at_ms ASC`, [principalId]));
50
+ FROM ${this.table} WHERE md_principal_id = ? ORDER BY md_created_at_ms ASC`, [principalId]));
45
51
  return rows.map(toDevice);
46
52
  }
47
53
  async listConfirmedSecrets(principalId) {
48
- const rows = (await this.pool.query(`SELECT md_secret FROM \`shield_identity\`.\`mfa_device\`
54
+ const rows = (await this.pool.query(`SELECT md_secret FROM ${this.table}
49
55
  WHERE md_principal_id = ? AND md_confirmed_at_ms IS NOT NULL`, [principalId]));
50
56
  return rows.map((r) => r.md_secret);
51
57
  }
52
58
  async getSecret(principalId, deviceId) {
53
- const rows = (await this.pool.query(`SELECT md_secret FROM \`shield_identity\`.\`mfa_device\`
59
+ const rows = (await this.pool.query(`SELECT md_secret FROM ${this.table}
54
60
  WHERE md_id = ? AND md_principal_id = ? LIMIT 1`, [deviceId, principalId]));
55
61
  return rows[0]?.md_secret ?? null;
56
62
  }
57
63
  async begin(principalId, name, secret) {
58
64
  // Prune the caller's stale pending rows first (self-pruning; no cron).
59
- await this.pool.query(`DELETE FROM \`shield_identity\`.\`mfa_device\`
65
+ await this.pool.query(`DELETE FROM ${this.table}
60
66
  WHERE md_principal_id = ? AND md_confirmed_at_ms IS NULL AND md_created_at_ms < ?`, [principalId, Date.now() - PENDING_TTL_MS]);
61
67
  const id = (0, node_crypto_1.randomUUID)();
62
- await this.pool.query(`INSERT INTO \`shield_identity\`.\`mfa_device\`
68
+ await this.pool.query(`INSERT INTO ${this.table}
63
69
  (md_id, md_principal_id, md_name, md_secret, md_created_at_ms, md_last_used_at_ms, md_confirmed_at_ms)
64
70
  VALUES (?, ?, ?, ?, ?, NULL, NULL)`, [id, principalId, name, secret, Date.now()]);
65
71
  return id;
66
72
  }
67
73
  async confirm(principalId, deviceId) {
68
- const res = (await this.pool.query(`UPDATE \`shield_identity\`.\`mfa_device\` SET md_confirmed_at_ms = ?
74
+ const res = (await this.pool.query(`UPDATE ${this.table} SET md_confirmed_at_ms = ?
69
75
  WHERE md_id = ? AND md_principal_id = ? AND md_confirmed_at_ms IS NULL`, [Date.now(), deviceId, principalId]));
70
76
  if (res.affectedRows !== 1)
71
77
  return false;
72
78
  // KI #14 re-review: once a device is confirmed MFA is on, so every later begin needs a code.
73
79
  // Drop the principal's OTHER pending devices: one begun by a stolen session while MFA was
74
80
  // still off (between the user's own begin and confirm) must not stay confirmable.
75
- await this.pool.query(`DELETE FROM \`shield_identity\`.\`mfa_device\`
81
+ await this.pool.query(`DELETE FROM ${this.table}
76
82
  WHERE md_principal_id = ? AND md_confirmed_at_ms IS NULL AND md_id <> ?`, [principalId, deviceId]);
77
83
  return true;
78
84
  }
79
85
  async rename(principalId, deviceId, name) {
80
- const res = (await this.pool.query(`UPDATE \`shield_identity\`.\`mfa_device\` SET md_name = ?
86
+ const res = (await this.pool.query(`UPDATE ${this.table} SET md_name = ?
81
87
  WHERE md_id = ? AND md_principal_id = ?`, [name, deviceId, principalId]));
82
88
  if (res.affectedRows !== 1)
83
89
  return null;
84
90
  const rows = (await this.pool.query(`SELECT md_id, md_name, md_secret, md_created_at_ms, md_last_used_at_ms, md_confirmed_at_ms
85
- FROM \`shield_identity\`.\`mfa_device\` WHERE md_id = ? AND md_principal_id = ? LIMIT 1`, [deviceId, principalId]));
91
+ FROM ${this.table} WHERE md_id = ? AND md_principal_id = ? LIMIT 1`, [deviceId, principalId]));
86
92
  return rows[0] ? toDevice(rows[0]) : null;
87
93
  }
88
94
  async remove(principalId, deviceId) {
89
- const res = (await this.pool.query(`DELETE FROM \`shield_identity\`.\`mfa_device\` WHERE md_id = ? AND md_principal_id = ?`, [deviceId, principalId]));
95
+ const res = (await this.pool.query(`DELETE FROM ${this.table} WHERE md_id = ? AND md_principal_id = ?`, [deviceId, principalId]));
90
96
  return res.affectedRows === 1;
91
97
  }
92
98
  async countConfirmed(principalId) {
93
- const rows = (await this.pool.query(`SELECT COUNT(*) AS c FROM \`shield_identity\`.\`mfa_device\`
99
+ const rows = (await this.pool.query(`SELECT COUNT(*) AS c FROM ${this.table}
94
100
  WHERE md_principal_id = ? AND md_confirmed_at_ms IS NOT NULL`, [principalId]));
95
101
  return Number(rows[0]?.c ?? 0);
96
102
  }
97
103
  async stampUsed(secret) {
98
104
  try {
99
- await this.pool.query(`UPDATE \`shield_identity\`.\`mfa_device\` SET md_last_used_at_ms = ? WHERE md_secret = ?`, [Date.now(), secret]);
105
+ await this.pool.query(`UPDATE ${this.table} SET md_last_used_at_ms = ? WHERE md_secret = ?`, [
106
+ Date.now(),
107
+ secret,
108
+ ]);
100
109
  }
101
110
  catch {
102
111
  // best-effort; a stamp failure must never fail a login.
@@ -14,10 +14,15 @@
14
14
  import type { Pool } from 'mariadb';
15
15
  import type { Principal } from '@astram/core';
16
16
  import type { CurrentUserProfile, ProfileResolver } from '../auth/profile-resolver.js';
17
+ import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
17
18
  export declare class MariaDbProfileResolver implements ProfileResolver {
18
19
  private readonly pool;
19
20
  private readonly now;
20
- constructor(pool: Pool, now?: () => number);
21
+ /** The org database (`<app>`), resolved at construction. */
22
+ private readonly app;
23
+ constructor(pool: Pool, now?: () => number,
24
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
25
+ schemas?: FrameworkSchemaNames);
21
26
  resolve(principal: Principal): Promise<CurrentUserProfile | null>;
22
27
  private resolveRoleNames;
23
28
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mariadb-profile-resolver.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-profile-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAe,MAAM,6BAA6B,CAAC;AAcpG,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,OAAO,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;YA4BzD,gBAAgB;CAc/B"}
1
+ {"version":3,"file":"mariadb-profile-resolver.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-profile-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAe,MAAM,6BAA6B,CAAC;AACpG,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAc7C,qBAAa,sBAAuB,YAAW,eAAe;IAK1D,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IALtB,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAGV,IAAI,EAAE,IAAI,EACV,GAAG,GAAE,MAAM,MAAiB;IAC7C,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAK1B,OAAO,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;YA4BzD,gBAAgB;CAc/B"}
@@ -14,21 +14,27 @@
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.MariaDbProfileResolver = void 0;
17
+ const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
17
18
  class MariaDbProfileResolver {
18
19
  pool;
19
20
  now;
20
- constructor(pool, now = Date.now) {
21
+ /** The org database (`<app>`), resolved at construction. */
22
+ app;
23
+ constructor(pool, now = Date.now,
24
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
25
+ schemas) {
21
26
  this.pool = pool;
22
27
  this.now = now;
28
+ this.app = (0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas).app;
23
29
  }
24
30
  async resolve(principal) {
25
31
  const userUuid = principal.id;
26
32
  const companyUuid = principal.tenantId;
27
- const users = (await this.pool.query('SELECT u_username, u_email FROM `shield_app`.`users` WHERE u_uuid = ? LIMIT 1', [userUuid]));
33
+ const users = (await this.pool.query(`SELECT u_username, u_email FROM \`${this.app}\`.\`users\` WHERE u_uuid = ? LIMIT 1`, [userUuid]));
28
34
  const user = users[0];
29
35
  if (!user)
30
36
  return null; // deleted user → no profile (never id-leak; endpoint 404s)
31
- const companies = (await this.pool.query('SELECT c_name FROM `shield_app`.`companies` WHERE c_uuid = ? LIMIT 1', [companyUuid]));
37
+ const companies = (await this.pool.query(`SELECT c_name FROM \`${this.app}\`.\`companies\` WHERE c_uuid = ? LIMIT 1`, [companyUuid]));
32
38
  const companyName = companies[0]?.c_name ?? '(unknown company)';
33
39
  const roles = await this.resolveRoleNames(userUuid, companyUuid);
34
40
  return {
@@ -42,9 +48,9 @@ class MariaDbProfileResolver {
42
48
  async resolveRoleNames(userUuid, companyUuid) {
43
49
  const nowMs = this.now();
44
50
  const rows = (await this.pool.query(`SELECT DISTINCT r.r_uuid, r.r_name
45
- FROM \`shield_app\`.\`membership_posting\` mp
46
- JOIN \`shield_app\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid
47
- JOIN \`shield_app\`.\`roles\` r ON r.r_uuid = mp.mp_role_uuid
51
+ FROM \`${this.app}\`.\`membership_posting\` mp
52
+ JOIN \`${this.app}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid
53
+ JOIN \`${this.app}\`.\`roles\` r ON r.r_uuid = mp.mp_role_uuid
48
54
  WHERE cm.cm_user_uuid = ? AND cm.cm_company_uuid = ?
49
55
  AND (mp.mp_effective_from_ms IS NULL OR mp.mp_effective_from_ms <= ?)
50
56
  AND (mp.mp_effective_until_ms IS NULL OR mp.mp_effective_until_ms > ?)`, [userUuid, companyUuid, nowMs, nowMs]));
@@ -7,10 +7,15 @@
7
7
  import type { Pool } from 'mariadb';
8
8
  import { type ProfileWriter } from '../auth/ports/profile-writer.js';
9
9
  import type { CurrentUserProfile } from '../auth/profile-resolver.js';
10
+ import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
10
11
  export declare class MariaDbProfileWriter implements ProfileWriter {
11
12
  private readonly pool;
12
13
  private readonly resolver;
13
- constructor(pool: Pool);
14
+ /** The org database (`<app>`), resolved at construction; the re-read resolver uses the same. */
15
+ private readonly app;
16
+ constructor(pool: Pool,
17
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
18
+ schemas?: FrameworkSchemaNames);
14
19
  update(principalId: string, changes: {
15
20
  name?: string;
16
21
  email?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"mariadb-profile-writer.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-profile-writer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAC5F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAgBtE,qBAAa,oBAAqB,YAAW,aAAa;IAE5C,OAAO,CAAC,QAAQ,CAAC,IAAI;IADjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;gBACrB,IAAI,EAAE,IAAI;IAIjC,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GACzC,OAAO,CAAC,kBAAkB,CAAC;CAgD/B"}
1
+ {"version":3,"file":"mariadb-profile-writer.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-profile-writer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAC5F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEtE,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAe7C,qBAAa,oBAAqB,YAAW,aAAa;IAKtD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAClD,gGAAgG;IAChG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEV,IAAI,EAAE,IAAI;IAC3B,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAO1B,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GACzC,OAAO,CAAC,kBAAkB,CAAC;CAgD/B"}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MariaDbProfileWriter = void 0;
4
4
  const profile_writer_js_1 = require("../auth/ports/profile-writer.js");
5
5
  const mariadb_profile_resolver_js_1 = require("./mariadb-profile-resolver.js");
6
+ const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
6
7
  /** MariaDB `ER_DUP_ENTRY`: here `IDX_users_username` refused the name (KI #97). */
7
8
  const ER_DUP_ENTRY = 1062;
8
9
  function isDuplicateKey(err) {
@@ -14,9 +15,15 @@ function isDuplicateKey(err) {
14
15
  class MariaDbProfileWriter {
15
16
  pool;
16
17
  resolver;
17
- constructor(pool) {
18
+ /** The org database (`<app>`), resolved at construction; the re-read resolver uses the same. */
19
+ app;
20
+ constructor(pool,
21
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
22
+ schemas) {
18
23
  this.pool = pool;
19
- this.resolver = new mariadb_profile_resolver_js_1.MariaDbProfileResolver(pool);
24
+ const s = (0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas);
25
+ this.app = s.app;
26
+ this.resolver = new mariadb_profile_resolver_js_1.MariaDbProfileResolver(pool, undefined, s);
20
27
  }
21
28
  async update(principalId, changes) {
22
29
  // Phase 12 / KI #87: the email is written ONLY by the verified email-change flow
@@ -34,7 +41,7 @@ class MariaDbProfileWriter {
34
41
  if (sets.length > 0) {
35
42
  params.push(principalId);
36
43
  try {
37
- await this.pool.query(`UPDATE \`shield_app\`.\`users\` SET ${sets.join(', ')} WHERE u_uuid = ?`, params);
44
+ await this.pool.query(`UPDATE \`${this.app}\`.\`users\` SET ${sets.join(', ')} WHERE u_uuid = ?`, params);
38
45
  }
39
46
  catch (err) {
40
47
  // KI #97: the driver message quotes the value; drop it and answer a typed conflict.
@@ -44,7 +51,7 @@ class MariaDbProfileWriter {
44
51
  }
45
52
  }
46
53
  // Resolve the caller's active company so the re-read profile is tenant-correct.
47
- 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]));
54
+ const active = (await this.pool.query(`SELECT cm_company_uuid FROM \`${this.app}\`.\`company_membership\` WHERE cm_user_uuid = ? AND cm_is_active = 1 AND cm_status = 'active' LIMIT 1`, [principalId]));
48
55
  const tenantId = active[0]?.cm_company_uuid ?? '';
49
56
  const profile = await this.resolver.resolve({
50
57
  kind: 'user',
@@ -21,14 +21,19 @@
21
21
  import type { Pool } from 'mariadb';
22
22
  import type { PasswordHasher } from '@astram/core';
23
23
  import type { RecoveryCodeStore } from '../auth/ports/recovery-code-store.js';
24
+ import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
24
25
  export declare class MariaDbRecoveryCodeStore implements RecoveryCodeStore {
25
26
  private readonly pool;
26
27
  /** The SAME Argon2id hasher the framework uses to hash the codes on enroll. */
27
28
  private readonly hasher;
28
29
  private readonly now;
30
+ /** `<identity>`.`mfa_recovery_code`, resolved at construction. */
31
+ private readonly table;
29
32
  constructor(pool: Pool,
30
33
  /** The SAME Argon2id hasher the framework uses to hash the codes on enroll. */
31
- hasher: PasswordHasher, now?: () => number);
34
+ hasher: PasswordHasher, now?: () => number,
35
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
36
+ schemas?: FrameworkSchemaNames);
32
37
  /**
33
38
  * Replace the principal's entire recovery-code set with `codeHashes` (already
34
39
  * Argon2-hashed). Wipes any prior codes first, then inserts the new batch. Passing
@@ -1 +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"}
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;AAC9E,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAM7C,qBAAa,wBAAyB,YAAW,iBAAiB;IAK9D,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,kEAAkE;IAClE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAGZ,IAAI,EAAE,IAAI;IAC3B,+EAA+E;IAC9D,MAAM,EAAE,cAAc,EACtB,GAAG,GAAE,MAAM,MAAiB;IAC7C,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAQhC;;;;OAIG;IACG,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAanF;;;;;;OAMG;IACG,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAqBtE"}
@@ -1,16 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MariaDbRecoveryCodeStore = void 0;
4
+ const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
4
5
  class MariaDbRecoveryCodeStore {
5
6
  pool;
6
7
  hasher;
7
8
  now;
9
+ /** `<identity>`.`mfa_recovery_code`, resolved at construction. */
10
+ table;
8
11
  constructor(pool,
9
12
  /** The SAME Argon2id hasher the framework uses to hash the codes on enroll. */
10
- hasher, now = Date.now) {
13
+ hasher, now = Date.now,
14
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
15
+ schemas) {
11
16
  this.pool = pool;
12
17
  this.hasher = hasher;
13
18
  this.now = now;
19
+ this.table = (0, framework_schema_names_js_1.qualifiedTableName)((0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas).identity, 'mfa_recovery_code');
14
20
  }
15
21
  /**
16
22
  * Replace the principal's entire recovery-code set with `codeHashes` (already
@@ -18,10 +24,10 @@ class MariaDbRecoveryCodeStore {
18
24
  * an empty array simply wipes (used when MFA is disabled).
19
25
  */
20
26
  async replaceAll(principalId, codeHashes) {
21
- await this.pool.query(`DELETE FROM \`shield_identity\`.\`mfa_recovery_code\` WHERE mrc_principal_id = ?`, [principalId]);
27
+ await this.pool.query(`DELETE FROM ${this.table} WHERE mrc_principal_id = ?`, [principalId]);
22
28
  const createdAt = this.now();
23
29
  for (const hash of codeHashes) {
24
- await this.pool.query(`INSERT INTO \`shield_identity\`.\`mfa_recovery_code\`
30
+ await this.pool.query(`INSERT INTO ${this.table}
25
31
  (mrc_principal_id, mrc_code_hash, mrc_consumed_at_ms, mrc_created_at_ms)
26
32
  VALUES (?, ?, NULL, ?)`, [principalId, hash, createdAt]);
27
33
  }
@@ -36,11 +42,11 @@ class MariaDbRecoveryCodeStore {
36
42
  async consume(principalId, rawCode) {
37
43
  if (typeof rawCode !== 'string' || rawCode.length === 0)
38
44
  return false;
39
- const rows = (await this.pool.query(`SELECT mrc_code_hash FROM \`shield_identity\`.\`mfa_recovery_code\`
45
+ const rows = (await this.pool.query(`SELECT mrc_code_hash FROM ${this.table}
40
46
  WHERE mrc_principal_id = ? AND mrc_consumed_at_ms IS NULL`, [principalId]));
41
47
  for (const row of rows) {
42
48
  if (await this.hasher.verify(row.mrc_code_hash, rawCode)) {
43
- const res = (await this.pool.query(`UPDATE \`shield_identity\`.\`mfa_recovery_code\`
49
+ const res = (await this.pool.query(`UPDATE ${this.table}
44
50
  SET mrc_consumed_at_ms = ?
45
51
  WHERE mrc_principal_id = ? AND mrc_code_hash = ? AND mrc_consumed_at_ms IS NULL`, [this.now(), principalId, row.mrc_code_hash]));
46
52
  // Single-use guard: only the UPDATE that flips NULL→consumed wins.
@@ -15,10 +15,15 @@
15
15
  */
16
16
  import type { Pool } from 'mariadb';
17
17
  import type { ResetTokenStore } from '../auth/ports/reset-token-store.js';
18
+ import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
18
19
  export declare class MariaDbResetTokenStore implements ResetTokenStore {
19
20
  private readonly pool;
20
21
  private readonly now;
21
- constructor(pool: Pool, now?: () => number);
22
+ /** `<identity>`.`password_reset_token`, resolved at construction. */
23
+ private readonly table;
24
+ constructor(pool: Pool, now?: () => number,
25
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
26
+ schemas?: FrameworkSchemaNames);
22
27
  issue(principalId: string, tokenHash: string, expiresAtMs: number): Promise<void>;
23
28
  consume(tokenHash: string): Promise<string | null>;
24
29
  invalidateAllForPrincipal(principalId: string): Promise<void>;
@@ -1 +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"}
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;AAC1E,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAM7C,qBAAa,sBAAuB,YAAW,eAAe;IAK1D,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IALtB,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAGZ,IAAI,EAAE,IAAI,EACV,GAAG,GAAE,MAAM,MAAiB;IAC7C,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAQ1B,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"}
@@ -1,15 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MariaDbResetTokenStore = void 0;
4
+ const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
4
5
  class MariaDbResetTokenStore {
5
6
  pool;
6
7
  now;
7
- constructor(pool, now = Date.now) {
8
+ /** `<identity>`.`password_reset_token`, resolved at construction. */
9
+ table;
10
+ constructor(pool, now = Date.now,
11
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
12
+ schemas) {
8
13
  this.pool = pool;
9
14
  this.now = now;
15
+ this.table = (0, framework_schema_names_js_1.qualifiedTableName)((0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas).identity, 'password_reset_token');
10
16
  }
11
17
  async issue(principalId, tokenHash, expiresAtMs) {
12
- await this.pool.query(`INSERT INTO \`shield_identity\`.\`password_reset_token\`
18
+ await this.pool.query(`INSERT INTO ${this.table}
13
19
  (prt_token_hash, prt_principal_id, prt_expires_at_ms, prt_consumed_at_ms, prt_created_at_ms)
14
20
  VALUES (?, ?, ?, NULL, ?)`, [tokenHash, principalId, expiresAtMs, this.now()]);
15
21
  }
@@ -17,7 +23,7 @@ class MariaDbResetTokenStore {
17
23
  const now = this.now();
18
24
  // 1) Find an unexpired, unconsumed token for this hash.
19
25
  const rows = (await this.pool.query(`SELECT prt_principal_id
20
- FROM \`shield_identity\`.\`password_reset_token\`
26
+ FROM ${this.table}
21
27
  WHERE prt_token_hash = ? AND prt_consumed_at_ms IS NULL AND prt_expires_at_ms > ?
22
28
  LIMIT 1`, [tokenHash, now]));
23
29
  const principalId = rows[0]?.prt_principal_id;
@@ -25,7 +31,7 @@ class MariaDbResetTokenStore {
25
31
  return null;
26
32
  // 2) Atomically claim it (single-use). The `IS NULL` guard + affectedRows check means a
27
33
  // 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\`
34
+ const res = (await this.pool.query(`UPDATE ${this.table}
29
35
  SET prt_consumed_at_ms = ?
30
36
  WHERE prt_token_hash = ? AND prt_consumed_at_ms IS NULL`, [now, tokenHash]));
31
37
  if (res.affectedRows !== 1)
@@ -35,7 +41,7 @@ class MariaDbResetTokenStore {
35
41
  async invalidateAllForPrincipal(principalId) {
36
42
  // Burn every outstanding (unconsumed) token for the principal after a successful reset, so a
37
43
  // second still-valid link cannot re-reset.
38
- await this.pool.query(`UPDATE \`shield_identity\`.\`password_reset_token\`
44
+ await this.pool.query(`UPDATE ${this.table}
39
45
  SET prt_consumed_at_ms = ?
40
46
  WHERE prt_principal_id = ? AND prt_consumed_at_ms IS NULL`, [this.now(), principalId]);
41
47
  }
@@ -23,6 +23,7 @@
23
23
  import type { Pool } from 'mariadb';
24
24
  import type { SessionSweeper } from '../auth/ports/session-sweeper.js';
25
25
  import type { SessionTokenBinder } from '../request-auth/session-token-binder.js';
26
+ import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
26
27
  /** The binder surface the sweeper needs; `unbindSessionStrict` (throws on a Redis error) is preferred. */
27
28
  type SweepBinder = Pick<SessionTokenBinder, 'unbindSession'> & Partial<Pick<SessionTokenBinder, 'unbindSessionStrict'>>;
28
29
  export declare class MariaDbSessionSweeper implements SessionSweeper {
@@ -30,9 +31,13 @@ export declare class MariaDbSessionSweeper implements SessionSweeper {
30
31
  private readonly binder;
31
32
  /** Clock for the "not yet expired" unbind window (epoch-ms). */
32
33
  private readonly now;
34
+ /** `<identity>`.`session`, resolved at construction (configurable-framework-db). */
35
+ private readonly sessionTable;
33
36
  constructor(pool: Pool, binder: SweepBinder,
34
37
  /** Clock for the "not yet expired" unbind window (epoch-ms). */
35
- now?: () => number);
38
+ now?: () => number,
39
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
40
+ schemas?: FrameworkSchemaNames);
36
41
  /**
37
42
  * Unbind the given sessions in Redis. Every session is attempted; if any unbind failed, the
38
43
  * FIRST error is thrown afterwards (Phase 3 review P1), so an administrative revoke reports it