@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.
- package/dist/auth/auth.module.d.ts +8 -1
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +76 -31
- package/dist/authentication/mariadb-api-key-store.d.ts +10 -1
- package/dist/authentication/mariadb-api-key-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-api-key-store.js +20 -10
- package/dist/authentication/mariadb-credential-resolver.d.ts +8 -1
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-resolver.js +15 -5
- package/dist/authentication/mariadb-credential-writer.d.ts +6 -1
- package/dist/authentication/mariadb-credential-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-writer.js +9 -3
- package/dist/authentication/mariadb-email-address-changer.d.ts +10 -1
- package/dist/authentication/mariadb-email-address-changer.d.ts.map +1 -1
- package/dist/authentication/mariadb-email-address-changer.js +22 -9
- package/dist/authentication/mariadb-email-change-token-store.d.ts +8 -1
- package/dist/authentication/mariadb-email-change-token-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-email-change-token-store.js +18 -9
- package/dist/authentication/mariadb-mfa-device-store.d.ts +6 -1
- package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-mfa-device-store.js +22 -13
- package/dist/authentication/mariadb-profile-resolver.d.ts +6 -1
- package/dist/authentication/mariadb-profile-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-profile-resolver.js +12 -6
- package/dist/authentication/mariadb-profile-writer.d.ts +6 -1
- package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-profile-writer.js +11 -4
- package/dist/authentication/mariadb-recovery-code-store.d.ts +6 -1
- package/dist/authentication/mariadb-recovery-code-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-recovery-code-store.js +11 -5
- package/dist/authentication/mariadb-reset-token-store.d.ts +6 -1
- package/dist/authentication/mariadb-reset-token-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-reset-token-store.js +11 -5
- package/dist/authentication/mariadb-session-sweeper.d.ts +6 -1
- package/dist/authentication/mariadb-session-sweeper.d.ts.map +1 -1
- package/dist/authentication/mariadb-session-sweeper.js +11 -5
- package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -1
- package/dist/authz/cross-tenant-grant-writer.js +4 -3
- package/dist/authz/list-scope-sql.d.ts +10 -2
- package/dist/authz/list-scope-sql.d.ts.map +1 -1
- package/dist/authz/list-scope-sql.js +4 -4
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js +4 -3
- package/dist/authz/mariadb-list-scope-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-list-scope-adapter.js +4 -3
- package/dist/authz/mariadb-policy-store.d.ts.map +1 -1
- package/dist/authz/mariadb-policy-store.js +4 -3
- package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-record-rule-adapter.js +4 -3
- package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-record-share-adapter.js +4 -3
- package/dist/authz/record-share-cascade.d.ts.map +1 -1
- package/dist/authz/record-share-cascade.js +4 -3
- package/dist/authz/record-share-writer.d.ts.map +1 -1
- package/dist/authz/record-share-writer.js +4 -3
- package/dist/authz/wire-record-authorization.d.ts +8 -0
- package/dist/authz/wire-record-authorization.d.ts.map +1 -1
- package/dist/authz/wire-record-authorization.js +14 -6
- package/dist/boot/boot-sequence.d.ts +10 -0
- package/dist/boot/boot-sequence.d.ts.map +1 -1
- package/dist/boot/boot-sequence.js +47 -3
- package/dist/boot/migration-ledger-check.d.ts +94 -0
- package/dist/boot/migration-ledger-check.d.ts.map +1 -0
- package/dist/boot/migration-ledger-check.js +280 -0
- package/dist/boot/single-layout-probe.d.ts +66 -0
- package/dist/boot/single-layout-probe.d.ts.map +1 -0
- package/dist/boot/single-layout-probe.js +334 -0
- package/dist/config/app-config.d.ts +46 -3
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +90 -11
- package/dist/config/config.module.d.ts +7 -0
- package/dist/config/config.module.d.ts.map +1 -1
- package/dist/config/config.module.js +8 -1
- package/dist/config/framework-schema-names.d.ts +29 -0
- package/dist/config/framework-schema-names.d.ts.map +1 -0
- package/dist/config/framework-schema-names.js +41 -0
- package/dist/identity/permission-facts.adapter.d.ts +7 -1
- package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
- package/dist/identity/permission-facts.adapter.js +15 -6
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +43 -7
- package/dist/session/mariadb-session-record-store.d.ts +8 -1
- package/dist/session/mariadb-session-record-store.d.ts.map +1 -1
- package/dist/session/mariadb-session-record-store.js +23 -13
- package/dist/session/session-restriction-store.d.ts +8 -1
- package/dist/session/session-restriction-store.d.ts.map +1 -1
- package/dist/session/session-restriction-store.js +15 -5
- package/migrations/1710000000006-CreateAuthzSchema.cjs +15 -4
- package/migrations/1710000000007-ExtendPermissionAndRolePermission.cjs +11 -3
- package/migrations/1710000000008-CreateAccessBundleTables.cjs +14 -4
- package/migrations/1710000000009-CreateRecordPermissionRule.cjs +15 -4
- package/migrations/1710000000014-CreateRecordShare.cjs +15 -4
- package/migrations/1710000000015-CreateScopeClosure.cjs +14 -4
- package/migrations/1710000000016-AlterCompanyParentFkRestrict.cjs +20 -9
- package/migrations/1710000000017-CreateServiceAccountKey.cjs +14 -4
- 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
|
-
/**
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;
|
|
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
|
-
/**
|
|
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: '
|
|
110
|
-
database:
|
|
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:
|
|
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
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
cfg.perAccountMax.
|
|
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
|
*
|