@astram/guard 0.14.0 → 0.16.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 (99) hide show
  1. package/dist/auth/auth.module.d.ts +8 -1
  2. package/dist/auth/auth.module.d.ts.map +1 -1
  3. package/dist/auth/auth.module.js +76 -31
  4. package/dist/authentication/mariadb-api-key-store.d.ts +10 -1
  5. package/dist/authentication/mariadb-api-key-store.d.ts.map +1 -1
  6. package/dist/authentication/mariadb-api-key-store.js +20 -10
  7. package/dist/authentication/mariadb-credential-resolver.d.ts +8 -1
  8. package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
  9. package/dist/authentication/mariadb-credential-resolver.js +15 -5
  10. package/dist/authentication/mariadb-credential-writer.d.ts +6 -1
  11. package/dist/authentication/mariadb-credential-writer.d.ts.map +1 -1
  12. package/dist/authentication/mariadb-credential-writer.js +9 -3
  13. package/dist/authentication/mariadb-email-address-changer.d.ts +10 -1
  14. package/dist/authentication/mariadb-email-address-changer.d.ts.map +1 -1
  15. package/dist/authentication/mariadb-email-address-changer.js +22 -9
  16. package/dist/authentication/mariadb-email-change-token-store.d.ts +8 -1
  17. package/dist/authentication/mariadb-email-change-token-store.d.ts.map +1 -1
  18. package/dist/authentication/mariadb-email-change-token-store.js +18 -9
  19. package/dist/authentication/mariadb-mfa-device-store.d.ts +6 -1
  20. package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -1
  21. package/dist/authentication/mariadb-mfa-device-store.js +22 -13
  22. package/dist/authentication/mariadb-profile-resolver.d.ts +6 -1
  23. package/dist/authentication/mariadb-profile-resolver.d.ts.map +1 -1
  24. package/dist/authentication/mariadb-profile-resolver.js +12 -6
  25. package/dist/authentication/mariadb-profile-writer.d.ts +6 -1
  26. package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
  27. package/dist/authentication/mariadb-profile-writer.js +11 -4
  28. package/dist/authentication/mariadb-recovery-code-store.d.ts +6 -1
  29. package/dist/authentication/mariadb-recovery-code-store.d.ts.map +1 -1
  30. package/dist/authentication/mariadb-recovery-code-store.js +11 -5
  31. package/dist/authentication/mariadb-reset-token-store.d.ts +6 -1
  32. package/dist/authentication/mariadb-reset-token-store.d.ts.map +1 -1
  33. package/dist/authentication/mariadb-reset-token-store.js +11 -5
  34. package/dist/authentication/mariadb-session-sweeper.d.ts +6 -1
  35. package/dist/authentication/mariadb-session-sweeper.d.ts.map +1 -1
  36. package/dist/authentication/mariadb-session-sweeper.js +11 -5
  37. package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -1
  38. package/dist/authz/cross-tenant-grant-writer.js +4 -3
  39. package/dist/authz/list-scope-sql.d.ts +10 -2
  40. package/dist/authz/list-scope-sql.d.ts.map +1 -1
  41. package/dist/authz/list-scope-sql.js +4 -4
  42. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -1
  43. package/dist/authz/mariadb-cross-tenant-grant-adapter.js +4 -3
  44. package/dist/authz/mariadb-list-scope-adapter.d.ts.map +1 -1
  45. package/dist/authz/mariadb-list-scope-adapter.js +4 -3
  46. package/dist/authz/mariadb-policy-store.d.ts.map +1 -1
  47. package/dist/authz/mariadb-policy-store.js +4 -3
  48. package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -1
  49. package/dist/authz/mariadb-record-rule-adapter.js +4 -3
  50. package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -1
  51. package/dist/authz/mariadb-record-share-adapter.js +4 -3
  52. package/dist/authz/record-share-cascade.d.ts.map +1 -1
  53. package/dist/authz/record-share-cascade.js +4 -3
  54. package/dist/authz/record-share-writer.d.ts.map +1 -1
  55. package/dist/authz/record-share-writer.js +4 -3
  56. package/dist/authz/wire-record-authorization.d.ts +8 -0
  57. package/dist/authz/wire-record-authorization.d.ts.map +1 -1
  58. package/dist/authz/wire-record-authorization.js +14 -6
  59. package/dist/boot/boot-sequence.d.ts +10 -0
  60. package/dist/boot/boot-sequence.d.ts.map +1 -1
  61. package/dist/boot/boot-sequence.js +47 -3
  62. package/dist/boot/migration-ledger-check.d.ts +94 -0
  63. package/dist/boot/migration-ledger-check.d.ts.map +1 -0
  64. package/dist/boot/migration-ledger-check.js +280 -0
  65. package/dist/boot/single-layout-probe.d.ts +66 -0
  66. package/dist/boot/single-layout-probe.d.ts.map +1 -0
  67. package/dist/boot/single-layout-probe.js +334 -0
  68. package/dist/config/app-config.d.ts +46 -3
  69. package/dist/config/app-config.d.ts.map +1 -1
  70. package/dist/config/app-config.js +90 -11
  71. package/dist/config/config.module.d.ts +7 -0
  72. package/dist/config/config.module.d.ts.map +1 -1
  73. package/dist/config/config.module.js +8 -1
  74. package/dist/config/framework-schema-names.d.ts +29 -0
  75. package/dist/config/framework-schema-names.d.ts.map +1 -0
  76. package/dist/config/framework-schema-names.js +41 -0
  77. package/dist/identity/permission-facts.adapter.d.ts +7 -1
  78. package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
  79. package/dist/identity/permission-facts.adapter.js +15 -6
  80. package/dist/index.d.ts +5 -2
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +18 -2
  83. package/dist/module/shield.module.d.ts.map +1 -1
  84. package/dist/module/shield.module.js +43 -7
  85. package/dist/session/mariadb-session-record-store.d.ts +8 -1
  86. package/dist/session/mariadb-session-record-store.d.ts.map +1 -1
  87. package/dist/session/mariadb-session-record-store.js +23 -13
  88. package/dist/session/session-restriction-store.d.ts +8 -1
  89. package/dist/session/session-restriction-store.d.ts.map +1 -1
  90. package/dist/session/session-restriction-store.js +15 -5
  91. package/migrations/1710000000006-CreateAuthzSchema.cjs +15 -4
  92. package/migrations/1710000000007-ExtendPermissionAndRolePermission.cjs +11 -3
  93. package/migrations/1710000000008-CreateAccessBundleTables.cjs +14 -4
  94. package/migrations/1710000000009-CreateRecordPermissionRule.cjs +15 -4
  95. package/migrations/1710000000014-CreateRecordShare.cjs +15 -4
  96. package/migrations/1710000000015-CreateScopeClosure.cjs +14 -4
  97. package/migrations/1710000000016-AlterCompanyParentFkRestrict.cjs +20 -9
  98. package/migrations/1710000000017-CreateServiceAccountKey.cjs +14 -4
  99. package/package.json +3 -3
@@ -0,0 +1,334 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SingleLayoutProbeBoot = void 0;
4
+ exports.probeSingleLayoutPrivileges = probeSingleLayoutPrivileges;
5
+ exports.singleLayoutWarningLines = singleLayoutWarningLines;
6
+ /**
7
+ * Single-database-layout boot privilege probe (single-database-layout spec §5.F, Req 7, AC-8,
8
+ * ADR-0041).
9
+ *
10
+ * With `SHIELD_DB_LAYOUT=single` the four framework modules share one database, so the isolation
11
+ * the four-database grant matrix gave is partly gone. Which protections are really lost depends on
12
+ * what the runtime account may do, so this reads (never writes) `information_schema`
13
+ * USER_PRIVILEGES / SCHEMA_PRIVILEGES / TABLE_PRIVILEGES / COLUMN_PRIVILEGES / APPLICABLE_ROLES for `CURRENT_USER()`, once at boot, and
14
+ * derives the five protections lost (spec §5.F). It does not look for `prune_audit`: MariaDB hides
15
+ * a routine from an account without EXECUTE on it, which the runtime account must not have (spec
16
+ * v3); `audit:prune` reports a missing routine itself.
17
+ *
18
+ * `GRANT ... TO PUBLIC` (MariaDB 10.11+) applies to every account but information_schema shows
19
+ * those rows only to privileged accounts (verified on 10.11 and 11.4: a plain account sees none).
20
+ * `SHOW GRANTS` for the current user needs no privilege and does list them, so its `TO PUBLIC`
21
+ * lines are parsed and merged with the information_schema rows. Its routine-level lines
22
+ * (`ON PROCEDURE|FUNCTION`) are the only view of routine privileges the account has: any such grant
23
+ * on the database loses the retention floor. An unparsable line of the account's own is ignored, an
24
+ * unparsable PUBLIC or routine line fails closed; a failing `SHOW GRANTS`
25
+ * fails closed like any other failed read.
26
+ *
27
+ * The probe never blocks boot (the operator's ack already accepted the risk) and is never on the
28
+ * request path (Shield #12). Warning lines carry no credential and no host (Shield #10): a failed
29
+ * query's driver message is dropped.
30
+ */
31
+ const common_1 = require("@nestjs/common");
32
+ const PROTECTIONS = {
33
+ 'append-only-audit': {
34
+ id: 'append-only-audit',
35
+ invariant: 'Shield #6',
36
+ text: 'Append-only audit: the runtime account can UPDATE or DELETE audit_log.',
37
+ },
38
+ 'no-runtime-ddl': {
39
+ id: 'no-runtime-ddl',
40
+ invariant: 'Shield #11',
41
+ text: 'No runtime DDL: the runtime account holds DDL privileges on the database.',
42
+ },
43
+ 'schema-separation': {
44
+ id: 'schema-separation',
45
+ invariant: 'Shield #11 (ADR-0014)',
46
+ text: 'Schema separation: identity, authz and audit tables share one database with app tables ' +
47
+ '(always lost in the single layout).',
48
+ },
49
+ 'retention-floor': {
50
+ id: 'retention-floor',
51
+ invariant: 'Shield #6',
52
+ text: 'DB-enforced retention floor: audit rows can be deleted directly, around prune_audit, or ' +
53
+ 'the runtime account holds a grant on prune_audit.',
54
+ },
55
+ 'separate-ddl-account': {
56
+ id: 'separate-ddl-account',
57
+ invariant: 'Shield #11',
58
+ text: 'Separate deploy-time DDL account: the runtime account can run migrations, so locking the ' +
59
+ 'migrate account between deploys is not possible.',
60
+ },
61
+ };
62
+ const ALL_IDS = Object.keys(PROTECTIONS);
63
+ /** Aligned with `scripts/db-grants/lib.mjs` DDL_PRIVS (GRANT OPTION is judged via IS_GRANTABLE too). */
64
+ const DDL_PRIVILEGES = new Set([
65
+ 'CREATE',
66
+ 'ALTER',
67
+ 'DROP',
68
+ 'INDEX',
69
+ 'CREATE VIEW',
70
+ 'SHOW VIEW',
71
+ 'TRIGGER',
72
+ 'CREATE ROUTINE',
73
+ 'ALTER ROUTINE',
74
+ 'EVENT',
75
+ 'REFERENCES',
76
+ 'CREATE TEMPORARY TABLES',
77
+ 'LOCK TABLES',
78
+ 'DELETE HISTORY',
79
+ 'GRANT OPTION',
80
+ ]);
81
+ const AUDIT_TABLE = 'audit_log';
82
+ /** `'user'@'host'` for CURRENT_USER(), built in SQL (never from env). */
83
+ const GRANTEE_SQL = `CONCAT("'", SUBSTRING_INDEX(CURRENT_USER(),'@',1), "'@'", ` +
84
+ `SUBSTRING_INDEX(CURRENT_USER(),'@',-1), "'")`;
85
+ /** Does a GRANT schema pattern (`\_` escaped, `_`/`%` wildcards) cover `database`? */
86
+ function schemaCovers(pattern, database) {
87
+ let re = '';
88
+ for (let i = 0; i < pattern.length; i++) {
89
+ const c = pattern[i];
90
+ if (c === '\\' && i + 1 < pattern.length) {
91
+ re += pattern[++i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
+ }
93
+ else if (c === '%')
94
+ re += '.*';
95
+ else if (c === '_')
96
+ re += '.';
97
+ else
98
+ re += c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
99
+ }
100
+ return new RegExp(`^${re}$`, 'i').test(database);
101
+ }
102
+ /** Splits `a, b (c, d), e` at top-level commas, respecting backticks and parentheses. */
103
+ function splitTopLevel(text) {
104
+ const out = [];
105
+ let cur = '';
106
+ let depth = 0;
107
+ let tick = false;
108
+ for (const c of text) {
109
+ if (c === '`')
110
+ tick = !tick;
111
+ else if (!tick && c === '(')
112
+ depth++;
113
+ else if (!tick && c === ')')
114
+ depth = Math.max(0, depth - 1);
115
+ if (c === ',' && depth === 0 && !tick) {
116
+ out.push(cur);
117
+ cur = '';
118
+ }
119
+ else
120
+ cur += c;
121
+ }
122
+ out.push(cur);
123
+ return out.map((x) => x.trim()).filter((x) => x.length > 0);
124
+ }
125
+ // `*`, backtick-quoted, ANSI_QUOTES double-quoted, or bare (sql_quote_show_create=0).
126
+ const IDENT = '(\\*|`(?:[^`]|``)*`|"(?:[^"]|"")*"|[^\\s.`"]+)';
127
+ const GRANT_LINE = /^GRANT\s+(.+?)\s+ON\s+(.+?)\s+TO\s+(.+?)(\s+WITH\s+GRANT\s+OPTION)?\s*$/is;
128
+ const TARGET = new RegExp(`^(?:(PROCEDURE|FUNCTION|PACKAGE|PACKAGE BODY)\\s+)?${IDENT}\\.${IDENT}$`, 'i');
129
+ const unquote = (id) => id.startsWith('`')
130
+ ? id.slice(1, -1).replace(/``/g, '`')
131
+ : id.startsWith('"')
132
+ ? id.slice(1, -1).replace(/""/g, '"')
133
+ : id;
134
+ const ROUTINE_TARGET = /^(PROCEDURE|FUNCTION|PACKAGE)\b/i;
135
+ /**
136
+ * Reads `TO PUBLIC` privileges and routine-level grants out of `SHOW GRANTS` lines. An
137
+ * unparsable `TO PUBLIC` or routine line sets `unreadable` (the probe then fails closed); any
138
+ * other unparsable line is the account's own and is skipped. Never throws.
139
+ */
140
+ function parseShowGrants(lines) {
141
+ const out = {
142
+ user: [],
143
+ schema: [],
144
+ table: [],
145
+ column: [],
146
+ routineSchemas: [],
147
+ unreadable: false,
148
+ };
149
+ for (const line of lines) {
150
+ const m = GRANT_LINE.exec(line);
151
+ if (!m)
152
+ continue;
153
+ const rawTarget = m[2].trim();
154
+ const target = TARGET.exec(rawTarget);
155
+ if (!target) {
156
+ if (/^PUBLIC$/i.test(m[3].trim()) || ROUTINE_TARGET.test(rawTarget)) {
157
+ out.unreadable = true;
158
+ }
159
+ continue;
160
+ }
161
+ const kind = target[1];
162
+ const schema = unquote(target[2]);
163
+ const name = unquote(target[3]);
164
+ if (kind) {
165
+ out.routineSchemas.push(schema);
166
+ continue;
167
+ }
168
+ if (!/^PUBLIC$/i.test(m[3].trim()))
169
+ continue;
170
+ const is = m[4] ? 'YES' : 'NO';
171
+ for (const item of splitTopLevel(m[1])) {
172
+ const paren = item.indexOf('(');
173
+ const type = (paren < 0 ? item : item.slice(0, paren)).trim().toUpperCase();
174
+ if (!type)
175
+ continue;
176
+ if (paren >= 0) {
177
+ // Column-level: one row per column (the name only matters for the table).
178
+ out.column.push({
179
+ TABLE_SCHEMA: schema,
180
+ TABLE_NAME: name,
181
+ PRIVILEGE_TYPE: type,
182
+ IS_GRANTABLE: is,
183
+ });
184
+ }
185
+ else if (schema === '*') {
186
+ out.user.push({ PRIVILEGE_TYPE: type, IS_GRANTABLE: is });
187
+ }
188
+ else if (name === '*') {
189
+ out.schema.push({ TABLE_SCHEMA: schema, PRIVILEGE_TYPE: type, IS_GRANTABLE: is });
190
+ }
191
+ else {
192
+ out.table.push({
193
+ TABLE_SCHEMA: schema,
194
+ TABLE_NAME: name,
195
+ PRIVILEGE_TYPE: type,
196
+ IS_GRANTABLE: is,
197
+ });
198
+ }
199
+ }
200
+ }
201
+ return out;
202
+ }
203
+ const priv = (r) => String(r.PRIVILEGE_TYPE ?? '').toUpperCase();
204
+ const grantable = (r) => String(r.IS_GRANTABLE ?? '').toUpperCase() === 'YES';
205
+ const failed = () => ({
206
+ lost: ALL_IDS.map((id) => PROTECTIONS[id]),
207
+ readFailed: true,
208
+ });
209
+ /**
210
+ * Derive the protections the runtime account has lost. Never throws, and never reports the audit
211
+ * wall intact when it cannot see the grants: a failed query, a runtime account whose own
212
+ * SELECT+INSERT on the database is invisible, or an account with roles (not inspected) reports all
213
+ * five with `readFailed`.
214
+ */
215
+ async function probeSingleLayoutPrivileges(query, database) {
216
+ try {
217
+ const userInfo = (await query(`SELECT PRIVILEGE_TYPE, IS_GRANTABLE FROM information_schema.USER_PRIVILEGES ` +
218
+ `WHERE GRANTEE = ${GRANTEE_SQL}`));
219
+ const schemaInfo = (await query(`SELECT TABLE_SCHEMA, PRIVILEGE_TYPE, IS_GRANTABLE FROM information_schema.SCHEMA_PRIVILEGES ` +
220
+ `WHERE GRANTEE = ${GRANTEE_SQL}`));
221
+ const tableInfo = (await query(`SELECT TABLE_SCHEMA, TABLE_NAME, PRIVILEGE_TYPE, IS_GRANTABLE ` +
222
+ `FROM information_schema.TABLE_PRIVILEGES WHERE GRANTEE = ${GRANTEE_SQL}`));
223
+ const columnInfo = (await query(`SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, PRIVILEGE_TYPE, IS_GRANTABLE ` +
224
+ `FROM information_schema.COLUMN_PRIVILEGES WHERE GRANTEE = ${GRANTEE_SQL}`));
225
+ const roles = (await query(`SELECT ROLE_NAME FROM information_schema.APPLICABLE_ROLES WHERE GRANTEE = ${GRANTEE_SQL}`));
226
+ // Privileges granted through roles are not inspected: do not claim a wall we cannot see.
227
+ if (roles.length > 0)
228
+ return failed();
229
+ // PUBLIC grants and routine grants: invisible in information_schema to this account.
230
+ const shown = (await query('SHOW GRANTS'));
231
+ const pub = parseShowGrants(shown.map((row) => String(Object.values(row ?? {})[0] ?? '')).filter((l) => l.length > 0));
232
+ if (pub.unreadable)
233
+ return failed();
234
+ const user = [...userInfo, ...pub.user];
235
+ const schema = [...schemaInfo, ...pub.schema];
236
+ const table = [...tableInfo, ...pub.table];
237
+ const column = [...columnInfo, ...pub.column];
238
+ const inDb = (r) => schemaCovers(String(r.TABLE_SCHEMA ?? ''), database);
239
+ const schemaRows = schema.filter(inDb);
240
+ const tables = table.filter(inDb);
241
+ const columns = column.filter(inDb);
242
+ // Any routine-level grant on the database (EXECUTE, ALTER ROUTINE, ...): prune_audit is reachable.
243
+ const routineGrant = pub.routineSchemas.some((sch) => schemaCovers(sch, database));
244
+ // Privileges that reach the whole database: global plus matching schema grants.
245
+ const db = new Set([...user.map(priv), ...schemaRows.map(priv)]);
246
+ const all = db.has('ALL PRIVILEGES') || db.has('ALL');
247
+ // The account booted, so it holds SELECT and INSERT on the database. Not seeing them means
248
+ // the grants are invisible (restricted view, odd grantee form): fail closed.
249
+ if (!all && !(db.has('SELECT') && db.has('INSERT')))
250
+ return failed();
251
+ // WITH GRANT OPTION anywhere on the database: the account can give itself anything.
252
+ const selfGrant = [...user, ...schemaRows, ...tables, ...columns].some(grantable);
253
+ const auditWrite = selfGrant ||
254
+ all ||
255
+ db.has('UPDATE') ||
256
+ db.has('DELETE') ||
257
+ // DROP allows TRUNCATE and dropping audit_log; ALTER rewrites it.
258
+ db.has('DROP') ||
259
+ db.has('ALTER') ||
260
+ tables.some((r) => String(r.TABLE_NAME ?? '').toLowerCase() === AUDIT_TABLE &&
261
+ ['UPDATE', 'DELETE', 'DROP', 'ALTER', 'ALL PRIVILEGES'].includes(priv(r))) ||
262
+ // Column-level UPDATE rewrites audit rows (there is no column-level DELETE).
263
+ columns.some((r) => String(r.TABLE_NAME ?? '').toLowerCase() === AUDIT_TABLE && priv(r) === 'UPDATE');
264
+ const ddl = selfGrant ||
265
+ all ||
266
+ [...db].some((p) => DDL_PRIVILEGES.has(p)) ||
267
+ tables.some((r) => DDL_PRIVILEGES.has(priv(r)));
268
+ // Database-level or global EXECUTE reaches prune_audit.
269
+ const execute = db.has('EXECUTE');
270
+ const lost = new Set(['schema-separation']);
271
+ if (auditWrite || routineGrant || execute) {
272
+ // The floor lives in prune_audit; a direct write on audit_log, or any grant on a routine
273
+ // of the database (EXECUTE / ALTER ROUTINE on prune_audit), gets around or weakens it.
274
+ lost.add('retention-floor');
275
+ }
276
+ if (auditWrite)
277
+ lost.add('append-only-audit');
278
+ if (ddl) {
279
+ lost.add('no-runtime-ddl');
280
+ lost.add('separate-ddl-account');
281
+ }
282
+ return {
283
+ lost: ALL_IDS.filter((id) => lost.has(id)).map((id) => PROTECTIONS[id]),
284
+ readFailed: false,
285
+ };
286
+ }
287
+ catch {
288
+ return failed();
289
+ }
290
+ }
291
+ /**
292
+ * The boot warning: a header naming the database and ADR-0041, then one line per lost protection
293
+ * citing its invariant. No credential, host or driver message.
294
+ */
295
+ function singleLayoutWarningLines(probe, database) {
296
+ const lines = [
297
+ `Shield single database layout on "${database}" (ADR-0041): reduced isolation accepted; ` +
298
+ `protections lost:`,
299
+ ];
300
+ if (probe.readFailed) {
301
+ lines.push('Runtime privileges could not be read; assuming all protections are lost.');
302
+ }
303
+ for (const p of probe.lost)
304
+ lines.push(`- ${p.text} (${p.invariant})`);
305
+ const ids = new Set(probe.lost.map((p) => p.id));
306
+ if (!probe.readFailed &&
307
+ !ids.has('append-only-audit') &&
308
+ !ids.has('no-runtime-ddl') &&
309
+ !ids.has('retention-floor')) {
310
+ lines.push('Two-account setup: audit wall intact (no direct UPDATE/DELETE/DDL/grant rights seen for ' +
311
+ 'this account).');
312
+ }
313
+ return lines;
314
+ }
315
+ /**
316
+ * Runs the probe once at application bootstrap on the runtime pool and logs the warning. Never
317
+ * throws, so boot is never blocked (spec §5.F).
318
+ */
319
+ class SingleLayoutProbeBoot {
320
+ query;
321
+ database;
322
+ logger;
323
+ constructor(query, database, logger = new common_1.Logger('Shield')) {
324
+ this.query = query;
325
+ this.database = database;
326
+ this.logger = logger;
327
+ }
328
+ async onApplicationBootstrap() {
329
+ const probe = await probeSingleLayoutPrivileges(this.query, this.database);
330
+ for (const line of singleLayoutWarningLines(probe, this.database))
331
+ this.logger.warn(line);
332
+ }
333
+ }
334
+ exports.SingleLayoutProbeBoot = SingleLayoutProbeBoot;
@@ -23,7 +23,7 @@
23
23
  * 8 × (12 + 8 + 1) + 40 = 208 ≤ 250 ✓
24
24
  * ─────────────────────────────────────────────────────────────────────────────
25
25
  */
26
- import type { SecretsProvider } from '@astram/core';
26
+ import type { FrameworkSchemas, SecretsProvider } from '@astram/core';
27
27
  export interface AppConfig {
28
28
  /** HTTP port the Nest app listens on. */
29
29
  readonly port: number;
@@ -74,7 +74,20 @@ export interface AppConfig {
74
74
  * composition (boot fails loud).
75
75
  */
76
76
  readonly refreshReplayWindowMs?: number;
77
+ /**
78
+ * The four framework database names (configurable-framework-db, ADR-0037), resolved once from
79
+ * `SHIELD_DB_NAMESPACE` by core `resolveFrameworkSchemas` (unset ⇒ the `shield_*` defaults).
80
+ * Optional so hand-built configs keep compiling; absent ⇒ {@link frameworkSchemasOf} resolves
81
+ * from `process.env` once at boot. Provided app-wide under `FRAMEWORK_SCHEMAS`.
82
+ */
83
+ readonly schemas?: FrameworkSchemas;
77
84
  }
85
+ /**
86
+ * The resolved framework database names for a config: `config.schemas`, else resolved from
87
+ * `process.env` (a hand-built config without `schemas`). Call at boot only (Shield #12).
88
+ * @throws FrameworkSchemaConfigError on an invalid `SHIELD_DB_NAMESPACE` (fail closed).
89
+ */
90
+ export declare function frameworkSchemasOf(config: Pick<AppConfig, 'schemas'>): FrameworkSchemas;
78
91
  /** The per-client-IP login failure limit (failures within `windowMs` that lock the IP). */
79
92
  export interface LoginIpLimitConfig {
80
93
  readonly maxAttempts: number;
@@ -169,6 +182,18 @@ export interface PoolBudgetConfig {
169
182
  readonly headroom: number;
170
183
  /** Per-account RUNTIME pool caps (multiplied by `workers` in the budget). */
171
184
  readonly perAccountMax: RuntimeAccountPools;
185
+ /**
186
+ * `'single'` when `SHIELD_DB_LAYOUT=single` (single-database-layout spec §5.C, AC-12, ADR-0041):
187
+ * one account opens the core, engine and authz pools, so the budget sums those instead.
188
+ * Absent in the four layout (today's math, unchanged).
189
+ */
190
+ readonly layout?: 'single';
191
+ /** Single layout: the engine pool cap (`POOL_MAX_ENGINE ?? APP_DB_MAX_CONNECTIONS ?? 10`). */
192
+ readonly engineMax?: number;
193
+ /** Single layout: the identity authz pool cap (`APP_AUTHZ_POOL_MAX ?? 4`). */
194
+ readonly authzMax?: number;
195
+ /** Single layout: the account's `MAX_USER_CONNECTIONS` (`APP_DB_MAX_USER_CONNECTIONS`), if set. */
196
+ readonly maxUserConnections?: number;
172
197
  }
173
198
  /**
174
199
  * Per-account maximum pool sizes for the RUNTIME accounts (the ones each worker
@@ -205,7 +230,11 @@ export declare function isDevOrTestNodeEnv(raw: string | undefined): boolean;
205
230
  * perAccountMax.shield_schema_svc ← POOL_MAX_SCHEMA_SVC
206
231
  */
207
232
  export declare function loadPoolBudget(env?: NodeJS.ProcessEnv): PoolBudgetConfig;
208
- /** Documented defaults for the runtime MariaDB descriptor (local docker substrate). */
233
+ /**
234
+ * Documented defaults for the runtime MariaDB descriptor (local docker substrate), for the
235
+ * default namespace. Under `SHIELD_DB_NAMESPACE` the user and database follow the namespace
236
+ * (`<ns>_core`, `<ns>_identity`; see {@link loadDbConfig}).
237
+ */
209
238
  export declare const DEFAULT_DB_CONFIG: Omit<DbConfig, 'password'>;
210
239
  /** Documented default Redis url (local docker substrate). */
211
240
  export declare const DEFAULT_REDIS_URL = "redis://localhost:6379";
@@ -219,8 +248,17 @@ export declare const DEFAULT_REDIS_URL = "redis://localhost:6379";
219
248
  * connect time (loud), which is correct for a runtime that needs real creds; the
220
249
  * secret is deliberately NOT hard-required at config load (unlike the HMAC key,
221
250
  * which is a fail-closed security invariant even for unit tests).
251
+ *
252
+ * Namespace (configurable-framework-db, spec 5.C): `APP_DB_USER` unset ⇒ `<ns>_core`;
253
+ * `APP_DB_NAME` unset ⇒ `<ns>_identity`. A set `APP_DB_NAME` must be one of the four resolved
254
+ * framework names, else this throws naming the variable (fail closed: an unqualified query must
255
+ * not reach another environment's database). An invalid `SHIELD_DB_NAMESPACE` throws too.
256
+ *
257
+ * `schemas` (optional): names the caller already resolved (e.g. `IdentityFeatureModule.forRoot
258
+ * ({ schemas })`). When given they replace the env namespace for the defaults and the
259
+ * `APP_DB_NAME` check, so the runtime database cannot drift from them (G-007 QA P2-5).
222
260
  */
223
- export declare function loadDbConfig(env?: NodeJS.ProcessEnv): DbConfig;
261
+ export declare function loadDbConfig(env?: NodeJS.ProcessEnv, schemas?: FrameworkSchemas): DbConfig;
224
262
  /**
225
263
  * `APP_DB_TLS` = `off` (default) | `on`; `APP_DB_TLS_CA` = PEM path (only with `on`). Anything
226
264
  * else throws at boot: a typo must not silently fall back to plaintext.
@@ -367,6 +405,11 @@ export interface PoolBudgetEvaluation {
367
405
  readonly withinBudget: boolean;
368
406
  /** True iff shield_schema_svc pool ≤ {@link SCHEMA_SVC_POOL_HARD_CAP}. */
369
407
  readonly schemaSvcWithinHardCap: boolean;
408
+ /**
409
+ * True iff requiredConnections ≤ `maxUserConnections` (always true when unset or in the four
410
+ * layout). Single layout only (spec AC-12).
411
+ */
412
+ readonly withinUserBudget: boolean;
370
413
  }
371
414
  /**
372
415
  * Evaluate a pool budget. Pure: no throwing, no I/O — the caller decides how to
@@ -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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"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DEFAULT_OBJECT_STORAGE_MAX_BYTES = exports.DEFAULT_REDIS_URL = exports.DEFAULT_DB_CONFIG = exports.SCHEMA_SVC_POOL_HARD_CAP = exports.DEFAULT_POOL_BUDGET = exports.DEFAULT_REDIS_COMMAND_TIMEOUT_MS = exports.DEFAULT_LOGIN_IP_LIMIT = void 0;
4
+ exports.frameworkSchemasOf = frameworkSchemasOf;
4
5
  exports.isDevOrTestNodeEnv = isDevOrTestNodeEnv;
5
6
  exports.loadPoolBudget = loadPoolBudget;
6
7
  exports.loadDbConfig = loadDbConfig;
@@ -17,6 +18,14 @@ exports.loadObjectStorageConfig = loadObjectStorageConfig;
17
18
  exports.evaluatePoolBudget = evaluatePoolBudget;
18
19
  const core_1 = require("@astram/core");
19
20
  const loopback_host_js_1 = require("./loopback-host.js");
21
+ /**
22
+ * The resolved framework database names for a config: `config.schemas`, else resolved from
23
+ * `process.env` (a hand-built config without `schemas`). Call at boot only (Shield #12).
24
+ * @throws FrameworkSchemaConfigError on an invalid `SHIELD_DB_NAMESPACE` (fail closed).
25
+ */
26
+ function frameworkSchemasOf(config) {
27
+ return config.schemas ?? (0, core_1.resolveFrameworkSchemas)(process.env);
28
+ }
20
29
  /** Plan D3 default: 50 failed logins per client IP per 15 minutes. */
21
30
  exports.DEFAULT_LOGIN_IP_LIMIT = {
22
31
  maxAttempts: 50,
@@ -75,6 +84,21 @@ function parsePositiveInt(raw, fallback) {
75
84
  const n = Number.parseInt(raw, 10);
76
85
  return Number.isInteger(n) && n > 0 ? n : fallback;
77
86
  }
87
+ /**
88
+ * A pool-size variable: unset or blank gives `undefined`; otherwise a positive integer in plain
89
+ * digits (surrounding whitespace allowed) or it throws naming the variable (never the value; fail
90
+ * closed). Identity's engine and authz pools apply the same rule, so the budget counts exactly the
91
+ * pools that open.
92
+ */
93
+ function strictOptionalPoolInt(raw, name) {
94
+ if (raw === undefined || raw.trim() === '')
95
+ return undefined;
96
+ const t = raw.trim();
97
+ const n = /^[0-9]+$/.test(t) ? Number(t) : Number.NaN;
98
+ if (!Number.isSafeInteger(n) || n < 1)
99
+ throw new Error(`${name} must be a positive integer`);
100
+ return n;
101
+ }
78
102
  /**
79
103
  * Build the {@link PoolBudgetConfig} from env with documented defaults.
80
104
  * Field ↔ env name:
@@ -86,7 +110,27 @@ function parsePositiveInt(raw, fallback) {
86
110
  * perAccountMax.shield_schema_svc ← POOL_MAX_SCHEMA_SVC
87
111
  */
88
112
  function loadPoolBudget(env = process.env) {
113
+ const single = env.SHIELD_DB_LAYOUT === 'single';
114
+ const maxUser = single
115
+ ? strictOptionalPoolInt(env.APP_DB_MAX_USER_CONNECTIONS, 'APP_DB_MAX_USER_CONNECTIONS')
116
+ : undefined;
89
117
  return {
118
+ ...(single
119
+ ? {
120
+ layout: 'single',
121
+ // Same rule as the engine pool identity opens (`engineDbFromEnv`): POOL_MAX_ENGINE, else
122
+ // today's APP_DB_MAX_CONNECTIONS fallback, else 10. The budget must count the pool that
123
+ // really opens, so an unset POOL_MAX_ENGINE with a large APP_DB_MAX_CONNECTIONS fails the
124
+ // budget instead of under-counting it (spec AC-12).
125
+ // The numbers are parsed exactly as identity's pools parse them (strict: a set but
126
+ // invalid value throws naming the variable, never a lenient fallback).
127
+ engineMax: strictOptionalPoolInt(env.POOL_MAX_ENGINE, 'POOL_MAX_ENGINE') ??
128
+ strictOptionalPoolInt(env.APP_DB_MAX_CONNECTIONS, 'APP_DB_MAX_CONNECTIONS') ??
129
+ 10,
130
+ authzMax: strictOptionalPoolInt(env.APP_AUTHZ_POOL_MAX, 'APP_AUTHZ_POOL_MAX') ?? 4,
131
+ ...(maxUser !== undefined ? { maxUserConnections: maxUser } : {}),
132
+ }
133
+ : {}),
90
134
  workers: parsePositiveInt(env.POOL_WORKERS, exports.DEFAULT_POOL_BUDGET.workers),
91
135
  maxConnections: parsePositiveInt(env.APP_DB_MAX_CONNECTIONS, exports.DEFAULT_POOL_BUDGET.maxConnections),
92
136
  headroom: parsePositiveInt(env.POOL_HEADROOM, exports.DEFAULT_POOL_BUDGET.headroom),
@@ -102,12 +146,18 @@ function loadPoolBudget(env = process.env) {
102
146
  // lifetimes. All pure env-readers with documented defaults; the ONLY hard-
103
147
  // required value is the refresh HMAC key (fail-closed).
104
148
  // ---------------------------------------------------------------------------
105
- /** Documented defaults for the runtime MariaDB descriptor (local docker substrate). */
149
+ /** The default-namespace framework names (`SHIELD_DB_NAMESPACE` unset). */
150
+ const DEFAULT_FRAMEWORK_SCHEMAS = (0, core_1.resolveFrameworkSchemas)({});
151
+ /**
152
+ * Documented defaults for the runtime MariaDB descriptor (local docker substrate), for the
153
+ * default namespace. Under `SHIELD_DB_NAMESPACE` the user and database follow the namespace
154
+ * (`<ns>_core`, `<ns>_identity`; see {@link loadDbConfig}).
155
+ */
106
156
  exports.DEFAULT_DB_CONFIG = {
107
157
  host: 'localhost',
108
158
  port: 3306,
109
- user: 'shield_core',
110
- database: 'shield_identity',
159
+ user: (0, core_1.frameworkAccountName)(DEFAULT_FRAMEWORK_SCHEMAS.namespace, 'core'),
160
+ database: DEFAULT_FRAMEWORK_SCHEMAS.identity,
111
161
  };
112
162
  /** Documented default Redis url (local docker substrate). */
113
163
  exports.DEFAULT_REDIS_URL = 'redis://localhost:6379';
@@ -139,14 +189,35 @@ function parseOptionalPositiveInt(raw) {
139
189
  * connect time (loud), which is correct for a runtime that needs real creds; the
140
190
  * secret is deliberately NOT hard-required at config load (unlike the HMAC key,
141
191
  * which is a fail-closed security invariant even for unit tests).
192
+ *
193
+ * Namespace (configurable-framework-db, spec 5.C): `APP_DB_USER` unset ⇒ `<ns>_core`;
194
+ * `APP_DB_NAME` unset ⇒ `<ns>_identity`. A set `APP_DB_NAME` must be one of the four resolved
195
+ * framework names, else this throws naming the variable (fail closed: an unqualified query must
196
+ * not reach another environment's database). An invalid `SHIELD_DB_NAMESPACE` throws too.
197
+ *
198
+ * `schemas` (optional): names the caller already resolved (e.g. `IdentityFeatureModule.forRoot
199
+ * ({ schemas })`). When given they replace the env namespace for the defaults and the
200
+ * `APP_DB_NAME` check, so the runtime database cannot drift from them (G-007 QA P2-5).
142
201
  */
143
- function loadDbConfig(env = process.env) {
202
+ function loadDbConfig(env = process.env, schemas = (0, core_1.resolveFrameworkSchemas)(env)) {
203
+ const database = parseString(env.APP_DB_NAME, schemas.identity);
204
+ // Single layout (spec §5.C): no `<ns>_core` account exists to default to; fail closed.
205
+ if (schemas.layout === 'single' && parseString(env.APP_DB_USER, '') === '') {
206
+ throw new core_1.FrameworkSchemaConfigError('APP_DB_USER is required when SHIELD_DB_LAYOUT=single (there is no default runtime account).');
207
+ }
208
+ const allowed = [schemas.app, schemas.identity, schemas.authz, schemas.audit];
209
+ if (!allowed.includes(database)) {
210
+ throw new core_1.FrameworkSchemaConfigError(`APP_DB_NAME must be one of the framework databases for namespace ` +
211
+ `"${schemas.namespace}" (${allowed.join(', ')}); unset it for ${schemas.identity}.`);
212
+ }
144
213
  return {
145
214
  host: parseString(env.APP_DB_HOST, exports.DEFAULT_DB_CONFIG.host),
146
215
  port: parsePort(env.APP_DB_PORT, exports.DEFAULT_DB_CONFIG.port),
147
- user: parseString(env.APP_DB_USER, exports.DEFAULT_DB_CONFIG.user),
216
+ user: schemas.layout === 'single'
217
+ ? parseString(env.APP_DB_USER, '')
218
+ : parseString(env.APP_DB_USER, (0, core_1.frameworkAccountName)(schemas.namespace, 'core')),
148
219
  password: parseString(env.APP_DB_PASSWORD, ''),
149
- database: parseString(env.APP_DB_NAME, exports.DEFAULT_DB_CONFIG.database),
220
+ database,
150
221
  tls: loadDbTlsConfig(env),
151
222
  };
152
223
  }
@@ -281,6 +352,7 @@ function assertRefreshHmacKey(key) {
281
352
  function loadAppConfig(env = process.env) {
282
353
  const refreshReplayWindowMs = loadRefreshReplayWindowMs(env);
283
354
  return {
355
+ schemas: (0, core_1.resolveFrameworkSchemas)(env),
284
356
  port: parsePort(env.PORT, 3000),
285
357
  environment: parseEnvironment(env.NODE_ENV),
286
358
  pool: loadPoolBudget(env),
@@ -406,18 +478,25 @@ function parseStorageMaxBytes(raw) {
406
478
  * react (the boot assertion throws; tests inspect the fields).
407
479
  */
408
480
  function evaluatePoolBudget(cfg) {
409
- const perWorkerConnections = cfg.perAccountMax.shield_core +
410
- cfg.perAccountMax.shield_app_crud +
411
- cfg.perAccountMax.shield_schema_svc;
481
+ // Single layout (AC-12): one account opens core + engine + authz; app_crud/schema_svc are never opened.
482
+ const perWorkerConnections = cfg.layout === 'single'
483
+ ? cfg.perAccountMax.shield_core + (cfg.engineMax ?? 10) + (cfg.authzMax ?? 4)
484
+ : cfg.perAccountMax.shield_core +
485
+ cfg.perAccountMax.shield_app_crud +
486
+ cfg.perAccountMax.shield_schema_svc;
412
487
  const runtimeConnections = cfg.workers * perWorkerConnections;
413
488
  const requiredConnections = runtimeConnections + cfg.headroom;
414
489
  const slack = cfg.maxConnections - requiredConnections;
490
+ const withinUserBudget = cfg.layout !== 'single' ||
491
+ cfg.maxUserConnections === undefined ||
492
+ requiredConnections <= cfg.maxUserConnections;
415
493
  return {
416
494
  perWorkerConnections,
417
495
  runtimeConnections,
418
496
  requiredConnections,
419
497
  slack,
420
- withinBudget: requiredConnections <= cfg.maxConnections,
421
- schemaSvcWithinHardCap: cfg.perAccountMax.shield_schema_svc <= exports.SCHEMA_SVC_POOL_HARD_CAP,
498
+ withinBudget: requiredConnections <= cfg.maxConnections && withinUserBudget,
499
+ schemaSvcWithinHardCap: cfg.layout === 'single' || cfg.perAccountMax.shield_schema_svc <= exports.SCHEMA_SVC_POOL_HARD_CAP,
500
+ withinUserBudget,
422
501
  };
423
502
  }
@@ -1,5 +1,12 @@
1
1
  /** DI token for the resolved {@link AppConfig}. */
2
2
  export declare const APP_CONFIG: unique symbol;
3
+ /**
4
+ * DI token for the resolved framework database names (core `FrameworkSchemas`:
5
+ * `{ namespace, app, identity, authz, audit, isDefault }`, frozen; configurable-framework-db,
6
+ * ADR-0037). Provided and exported app-wide by `ShieldModule` from `APP_CONFIG`, resolved once at
7
+ * boot; identity and app modules inject it instead of hard-coding `shield_*` names.
8
+ */
9
+ export declare const FRAMEWORK_SCHEMAS: unique symbol;
3
10
  /**
4
11
  * Minimal config module (Scaffold).
5
12
  *
@@ -1 +1 @@
1
- {"version":3,"file":"config.module.d.ts","sourceRoot":"","sources":["../../src/config/config.module.ts"],"names":[],"mappings":"AAIA,mDAAmD;AACnD,eAAO,MAAM,UAAU,eAAuB,CAAC;AAE/C;;;;;;GAMG;AACH,qBAaa,YAAY;CAAG"}
1
+ {"version":3,"file":"config.module.d.ts","sourceRoot":"","sources":["../../src/config/config.module.ts"],"names":[],"mappings":"AAIA,mDAAmD;AACnD,eAAO,MAAM,UAAU,eAAuB,CAAC;AAE/C;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,eAA8B,CAAC;AAE7D;;;;;;GAMG;AACH,qBAaa,YAAY;CAAG"}
@@ -6,12 +6,19 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.ConfigModule = exports.APP_CONFIG = void 0;
9
+ exports.ConfigModule = exports.FRAMEWORK_SCHEMAS = exports.APP_CONFIG = void 0;
10
10
  const common_1 = require("@nestjs/common");
11
11
  const app_config_js_1 = require("./app-config.js");
12
12
  const boot_secrets_js_1 = require("../secrets/boot-secrets.js");
13
13
  /** DI token for the resolved {@link AppConfig}. */
14
14
  exports.APP_CONFIG = Symbol('APP_CONFIG');
15
+ /**
16
+ * DI token for the resolved framework database names (core `FrameworkSchemas`:
17
+ * `{ namespace, app, identity, authz, audit, isDefault }`, frozen; configurable-framework-db,
18
+ * ADR-0037). Provided and exported app-wide by `ShieldModule` from `APP_CONFIG`, resolved once at
19
+ * boot; identity and app modules inject it instead of hard-coding `shield_*` names.
20
+ */
21
+ exports.FRAMEWORK_SCHEMAS = Symbol('FRAMEWORK_SCHEMAS');
15
22
  /**
16
23
  * Minimal config module (Scaffold).
17
24
  *