@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
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveBootSecrets = resolveBootSecrets;
4
+ exports.wasResolvedFromSecretRef = wasResolvedFromSecretRef;
5
+ exports.createSecretsProviderFromEnv = createSecretsProviderFromEnv;
6
+ exports.assertNoUnresolvedSecretRefs = assertNoUnresolvedSecretRefs;
7
+ /**
8
+ * Boot-time `secret-ref:` resolution (ADR-0032; FILE_STORAGE SPEC §5 L-N, §13).
9
+ *
10
+ * - {@link resolveBootSecrets}: called first in the app's `main.ts`, before `AppModule` is loaded,
11
+ * so every `forRoot` and direct env reader sees resolved values. Resolves every `secret-ref:` env
12
+ * value via core `resolveSecretRefs` and writes the results back into `process.env` for those
13
+ * keys only, and only once every reference has resolved (no partial write-back).
14
+ * - {@link createSecretsProviderFromEnv}: builds the configured {@link SecretsProvider} from the
15
+ * `SECRETS_*` variables (`none` | `encrypted-file`; `vault` reserved).
16
+ * - {@link assertNoUnresolvedSecretRefs}: synchronous backstop at every `loadAppConfig()` call site.
17
+ *
18
+ * SECURITY (Shield #1, #10): fail closed on every bad variable, file, key or reference. Errors name
19
+ * env variable names (and, for a failed resolution, the reference path); never a value, a key
20
+ * byte or file contents. Nothing is logged.
21
+ */
22
+ const node_crypto_1 = require("node:crypto");
23
+ const node_fs_1 = require("node:fs");
24
+ const core_1 = require("@astram/core");
25
+ const local_kek_crypto_js_1 = require("../encryption/local-kek-crypto.js");
26
+ const encrypted_file_secrets_js_1 = require("./encrypted-file-secrets.js");
27
+ /** AEAD additional data the secrets DEK is wrapped under. */
28
+ const SECRETS_DEK_AAD = 'shield:secrets';
29
+ const KEK_BYTES = 32;
30
+ /** Env keys whose value is still a `secret-ref:` reference. */
31
+ function refKeys(env) {
32
+ return Object.keys(env).filter((k) => (0, core_1.isSecretRef)(env[k]));
33
+ }
34
+ /**
35
+ * Resolve every `secret-ref:` value in `process.env` in place. No references and no provider
36
+ * configured: no-op. Idempotent (a second call finds no references).
37
+ */
38
+ async function resolveBootSecrets(options = {}) {
39
+ const env = process.env;
40
+ const provider = options.provider ?? createSecretsProviderFromEnv(env);
41
+ const keys = refKeys(env);
42
+ if (keys.length === 0)
43
+ return;
44
+ if (!provider) {
45
+ throw new Error(`secret-ref: values are set but no secrets provider is configured ` +
46
+ `(SECRETS_PROVIDER is unset or 'none'); env keys: ${keys.join(', ')}`);
47
+ }
48
+ // Core resolves into a NEW bag and throws on the first failure (key + path, never a value),
49
+ // so process.env is only touched once every reference has resolved.
50
+ const resolved = await (0, core_1.resolveSecretRefs)(env, provider);
51
+ for (const key of keys) {
52
+ const value = resolved[key];
53
+ env[key] = value;
54
+ if (value !== undefined)
55
+ resolvedFromRef.set(key, fingerprint(value));
56
+ }
57
+ }
58
+ /**
59
+ * Env key -> keyed fingerprint of the value {@link resolveBootSecrets} wrote for it. The value
60
+ * itself is never kept: an HMAC-SHA256 under a random per-process key (so the fingerprint cannot
61
+ * be checked against guesses outside this process).
62
+ */
63
+ const resolvedFromRef = new Map();
64
+ const FINGERPRINT_KEY = (0, node_crypto_1.randomBytes)(32);
65
+ function fingerprint(value) {
66
+ return (0, node_crypto_1.createHmac)('sha256', FINGERPRINT_KEY).update(value, 'utf8').digest('hex');
67
+ }
68
+ /**
69
+ * `true` when `key`'s CURRENT value in `env` (default `process.env`) is the value
70
+ * {@link resolveBootSecrets} resolved from a `secret-ref:` at boot (FILE_STORAGE SPEC §5 F:
71
+ * credentials outside development/test must be references, never literals). A value changed
72
+ * after resolution (e.g. a literal written over it) is `false`. No value is kept, only a keyed
73
+ * fingerprint.
74
+ */
75
+ function wasResolvedFromSecretRef(key, env = process.env) {
76
+ const recorded = resolvedFromRef.get(key);
77
+ const current = env[key];
78
+ if (recorded === undefined || typeof current !== 'string')
79
+ return false;
80
+ return (0, node_crypto_1.timingSafeEqual)(Buffer.from(recorded, 'hex'), Buffer.from(fingerprint(current), 'hex'));
81
+ }
82
+ /**
83
+ * Build the {@link SecretsProvider} named by `SECRETS_PROVIDER`. Returns `undefined` for `none`
84
+ * or unset. Throws (naming the variable only) on an unknown value, `vault`, an unset, missing or
85
+ * unreadable file, a corrupt store, a KEK that is not 32 bytes, or a DEK that does not unwrap
86
+ * under the KEK.
87
+ */
88
+ function createSecretsProviderFromEnv(env) {
89
+ const kind = (env.SECRETS_PROVIDER ?? '').trim();
90
+ if (kind === '' || kind === 'none')
91
+ return undefined;
92
+ if (kind === 'vault') {
93
+ throw new Error(`SECRETS_PROVIDER 'vault' is not supported yet (use 'none' or 'encrypted-file')`);
94
+ }
95
+ if (kind !== 'encrypted-file') {
96
+ throw new Error(`SECRETS_PROVIDER has an unknown value (expected 'none' or 'encrypted-file')`);
97
+ }
98
+ const storeFile = requirePath(env, 'SECRETS_FILE');
99
+ const kekFile = requirePath(env, 'SECRETS_KEK_FILE');
100
+ const dekFile = requirePath(env, 'SECRETS_DEK_FILE');
101
+ // SPEC §13: a missing, unreadable or corrupt store aborts boot naming SECRETS_FILE, even when
102
+ // no env value references it yet (a misconfigured provider is never silently empty).
103
+ checkStoreFile(storeFile);
104
+ const kek = readKeyFile(kekFile, 'SECRETS_KEK_FILE');
105
+ if (kek.length !== KEK_BYTES) {
106
+ throw new Error(`SECRETS_KEK_FILE must hold exactly ${KEK_BYTES} raw bytes`);
107
+ }
108
+ const crypto = new local_kek_crypto_js_1.LocalKekCrypto(new Uint8Array(kek));
109
+ kek.fill(0);
110
+ const wrapped = readKeyFile(dekFile, 'SECRETS_DEK_FILE');
111
+ let dek;
112
+ try {
113
+ dek = crypto.unwrapDekSync(new Uint8Array(wrapped), SECRETS_DEK_AAD);
114
+ }
115
+ catch {
116
+ throw new Error(`SECRETS_DEK_FILE could not be unwrapped with the key in SECRETS_KEK_FILE ` +
117
+ `(wrong KEK, wrong wrap context or a corrupt file)`);
118
+ }
119
+ return new encrypted_file_secrets_js_1.EncryptedFileSecretsProvider(storeFile, crypto, dek);
120
+ }
121
+ /**
122
+ * Synchronous backstop: throws if any env value still starts with `secret-ref:` (the app did not
123
+ * call {@link resolveBootSecrets} before building modules). Lists env key names only.
124
+ */
125
+ function assertNoUnresolvedSecretRefs(env) {
126
+ const keys = refKeys(env);
127
+ if (keys.length === 0)
128
+ return;
129
+ throw new Error(`unresolved secret references in env keys: ${keys.join(', ')}. ` +
130
+ `Call resolveBootSecrets() in main.ts before loading AppModule.`);
131
+ }
132
+ function requirePath(env, name) {
133
+ const value = env[name]?.trim();
134
+ if (!value)
135
+ throw new Error(`${name} must be set when SECRETS_PROVIDER is 'encrypted-file'`);
136
+ return value;
137
+ }
138
+ /**
139
+ * The store must exist, be readable and hold a JSON object of `{ version, ciphertext }` entries
140
+ * (an empty file is an empty store). Errors name `SECRETS_FILE` and an errno code only, never
141
+ * the path's contents.
142
+ */
143
+ function checkStoreFile(path) {
144
+ let raw;
145
+ try {
146
+ raw = (0, node_fs_1.readFileSync)(path, 'utf8');
147
+ }
148
+ catch (e) {
149
+ const code = e.code;
150
+ throw new Error(`SECRETS_FILE could not be read${code ? ` (${code})` : ''}`);
151
+ }
152
+ if (raw.trim() === '')
153
+ return;
154
+ let parsed;
155
+ try {
156
+ parsed = JSON.parse(raw);
157
+ }
158
+ catch {
159
+ parsed = undefined;
160
+ }
161
+ const isEntry = (v) => typeof v === 'object' &&
162
+ v !== null &&
163
+ Number.isInteger(v.version) &&
164
+ typeof v.ciphertext === 'string';
165
+ if (typeof parsed !== 'object' ||
166
+ parsed === null ||
167
+ Array.isArray(parsed) ||
168
+ !Object.values(parsed).every(isEntry)) {
169
+ throw new Error('SECRETS_FILE is not a valid secrets store (corrupt or wrong format)');
170
+ }
171
+ }
172
+ function readKeyFile(path, name) {
173
+ try {
174
+ return (0, node_fs_1.readFileSync)(path);
175
+ }
176
+ catch (e) {
177
+ const code = e.code;
178
+ throw new Error(`${name} could not be read${code ? ` (${code})` : ''}`);
179
+ }
180
+ }
@@ -19,11 +19,18 @@
19
19
  */
20
20
  import type { Pool } from 'mariadb';
21
21
  import type { CreateSessionInput, InsertRefreshInput, RefreshTokenRecord, SessionRecord, SessionRecordStore } from '@astram/core';
22
+ import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
22
23
  /** The DI token the store is bound under. */
23
24
  export declare const SHIELD_SESSION_RECORD_STORE: unique symbol;
24
25
  export declare class MariaDbSessionRecordStore implements SessionRecordStore {
25
26
  private readonly pool;
26
- constructor(pool: Pool);
27
+ /** `<identity>`.`session`, resolved at construction (lifecycle writes only, ADR-0013). */
28
+ private readonly sessionTable;
29
+ /** `<identity>`.`refresh_token`. */
30
+ private readonly refreshTable;
31
+ constructor(pool: Pool,
32
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
33
+ schemas?: FrameworkSchemaNames);
27
34
  createSession(input: CreateSessionInput): Promise<SessionRecord>;
28
35
  insertRefreshRecord(input: InsertRefreshInput): Promise<RefreshTokenRecord>;
29
36
  findRefreshByHash(tokenHash: string): Promise<RefreshTokenRecord | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"mariadb-session-record-store.d.ts","sourceRoot":"","sources":["../../src/session/mariadb-session-record-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAkB,MAAM,SAAS,CAAC;AACpD,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAGtB,6CAA6C;AAC7C,eAAO,MAAM,2BAA2B,eAAwC,CAAC;AAmDjF,qBAAa,yBAA0B,YAAW,kBAAkB;IACtD,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEjC,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC;IAuBhE,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA4B3E,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IASxE,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IASvE;;;;;;;OAOG;IACG,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASxD,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQnD;;;OAGG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBrD;;;;OAIG;IACG,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAQ1D,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAarD,sEAAsE;YACxD,MAAM;CAiBrB"}
1
+ {"version":3,"file":"mariadb-session-record-store.d.ts","sourceRoot":"","sources":["../../src/session/mariadb-session-record-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAkB,MAAM,SAAS,CAAC;AACpD,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAE7C,6CAA6C;AAC7C,eAAO,MAAM,2BAA2B,eAAwC,CAAC;AAmDjF,qBAAa,yBAA0B,YAAW,kBAAkB;IAOhE,OAAO,CAAC,QAAQ,CAAC,IAAI;IANvB,0FAA0F;IAC1F,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,oCAAoC;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAGnB,IAAI,EAAE,IAAI;IAC3B,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAO1B,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC;IAuBhE,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA4B3E,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IASxE,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IASvE;;;;;;;OAOG;IACG,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASxD,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQnD;;;OAGG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBrD;;;;OAIG;IACG,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAQ1D,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAarD,sEAAsE;YACxD,MAAM;CAiBrB"}
@@ -20,6 +20,7 @@
20
20
  */
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
22
  exports.MariaDbSessionRecordStore = exports.SHIELD_SESSION_RECORD_STORE = void 0;
23
+ const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
23
24
  /** The DI token the store is bound under. */
24
25
  exports.SHIELD_SESSION_RECORD_STORE = Symbol('SHIELD_SESSION_RECORD_STORE');
25
26
  /** BIGINT columns can arrive as string; normalize to number safely. */
@@ -50,11 +51,20 @@ function mapRefresh(r) {
50
51
  }
51
52
  class MariaDbSessionRecordStore {
52
53
  pool;
53
- constructor(pool) {
54
+ /** `<identity>`.`session`, resolved at construction (lifecycle writes only, ADR-0013). */
55
+ sessionTable;
56
+ /** `<identity>`.`refresh_token`. */
57
+ refreshTable;
58
+ constructor(pool,
59
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
60
+ schemas) {
54
61
  this.pool = pool;
62
+ const identity = (0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas).identity;
63
+ this.sessionTable = (0, framework_schema_names_js_1.qualifiedTableName)(identity, 'session');
64
+ this.refreshTable = (0, framework_schema_names_js_1.qualifiedTableName)(identity, 'refresh_token');
55
65
  }
56
66
  async createSession(input) {
57
- await this.pool.query(`INSERT INTO shield_identity.session
67
+ await this.pool.query(`INSERT INTO ${this.sessionTable}
58
68
  (s_session_id, s_principal_id, s_tenant_id, s_status, s_issued_at_ms, s_absolute_expires_at_ms)
59
69
  VALUES (?, ?, ?, 'active', ?, ?)`, [
60
70
  input.sessionId,
@@ -73,7 +83,7 @@ class MariaDbSessionRecordStore {
73
83
  };
74
84
  }
75
85
  async insertRefreshRecord(input) {
76
- await this.pool.query(`INSERT INTO shield_identity.refresh_token
86
+ await this.pool.query(`INSERT INTO ${this.refreshTable}
77
87
  (rt_refresh_id, rt_session_id, rt_lineage_id, rt_prev_refresh_id,
78
88
  rt_token_hash, rt_status, rt_issued_at_ms, rt_idle_expires_at_ms)
79
89
  VALUES (?, ?, ?, ?, ?, 'active', ?, ?)`, [
@@ -97,12 +107,12 @@ class MariaDbSessionRecordStore {
97
107
  };
98
108
  }
99
109
  async findRefreshByHash(tokenHash) {
100
- const rows = (await this.pool.query(`SELECT * FROM shield_identity.refresh_token WHERE rt_token_hash = ? LIMIT 1`, [tokenHash]));
110
+ const rows = (await this.pool.query(`SELECT * FROM ${this.refreshTable} WHERE rt_token_hash = ? LIMIT 1`, [tokenHash]));
101
111
  const row = rows[0];
102
112
  return row ? mapRefresh(row) : null;
103
113
  }
104
114
  async findSessionById(sessionId) {
105
- const rows = (await this.pool.query(`SELECT * FROM shield_identity.session WHERE s_session_id = ? LIMIT 1`, [sessionId]));
115
+ const rows = (await this.pool.query(`SELECT * FROM ${this.sessionTable} WHERE s_session_id = ? LIMIT 1`, [sessionId]));
106
116
  const row = rows[0];
107
117
  return row ? mapSession(row) : null;
108
118
  }
@@ -115,12 +125,12 @@ class MariaDbSessionRecordStore {
115
125
  * gets `0` (row already 'rotated' → loser). This makes rotation single-winner.
116
126
  */
117
127
  async markRotatedIfActive(refreshId) {
118
- const res = (await this.pool.query(`UPDATE shield_identity.refresh_token SET rt_status = 'rotated'
128
+ const res = (await this.pool.query(`UPDATE ${this.refreshTable} SET rt_status = 'rotated'
119
129
  WHERE rt_refresh_id = ? AND rt_status = 'active'`, [refreshId]));
120
130
  return res?.affectedRows === 1;
121
131
  }
122
132
  async markRevoked(refreshId) {
123
- await this.pool.query(`UPDATE shield_identity.refresh_token SET rt_status = 'revoked'
133
+ await this.pool.query(`UPDATE ${this.refreshTable} SET rt_status = 'revoked'
124
134
  WHERE rt_refresh_id = ?`, [refreshId]);
125
135
  }
126
136
  /**
@@ -129,11 +139,11 @@ class MariaDbSessionRecordStore {
129
139
  */
130
140
  async revokeLineage(lineageId) {
131
141
  await this.withTx(async (conn) => {
132
- await conn.query(`UPDATE shield_identity.refresh_token SET rt_status = 'revoked'
142
+ await conn.query(`UPDATE ${this.refreshTable} SET rt_status = 'revoked'
133
143
  WHERE rt_lineage_id = ?`, [lineageId]);
134
- await conn.query(`UPDATE shield_identity.session SET s_status = 'revoked'
144
+ await conn.query(`UPDATE ${this.sessionTable} SET s_status = 'revoked'
135
145
  WHERE s_session_id IN (
136
- SELECT DISTINCT rt_session_id FROM shield_identity.refresh_token WHERE rt_lineage_id = ?
146
+ SELECT DISTINCT rt_session_id FROM ${this.refreshTable} WHERE rt_lineage_id = ?
137
147
  )`, [lineageId]);
138
148
  });
139
149
  }
@@ -143,13 +153,13 @@ class MariaDbSessionRecordStore {
143
153
  * those sessions in Redis — never on the per-request path.
144
154
  */
145
155
  async sessionIdsForLineage(lineageId) {
146
- const rows = (await this.pool.query(`SELECT DISTINCT rt_session_id FROM shield_identity.refresh_token WHERE rt_lineage_id = ?`, [lineageId]));
156
+ const rows = (await this.pool.query(`SELECT DISTINCT rt_session_id FROM ${this.refreshTable} WHERE rt_lineage_id = ?`, [lineageId]));
147
157
  return rows.map((r) => r.rt_session_id);
148
158
  }
149
159
  async revokeSession(sessionId) {
150
160
  await this.withTx(async (conn) => {
151
- await conn.query(`UPDATE shield_identity.session SET s_status = 'revoked' WHERE s_session_id = ?`, [sessionId]);
152
- await conn.query(`UPDATE shield_identity.refresh_token SET rt_status = 'revoked' WHERE rt_session_id = ?`, [sessionId]);
161
+ await conn.query(`UPDATE ${this.sessionTable} SET s_status = 'revoked' WHERE s_session_id = ?`, [sessionId]);
162
+ await conn.query(`UPDATE ${this.refreshTable} SET rt_status = 'revoked' WHERE rt_session_id = ?`, [sessionId]);
153
163
  });
154
164
  }
155
165
  /** Run `fn` in a transaction, releasing the connection either way. */
@@ -15,6 +15,7 @@
15
15
  * Every read error propagates: the caller fails closed.
16
16
  */
17
17
  import type { Pool } from 'mariadb';
18
+ import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
18
19
  /** What the refresh rebuild needs to know about a session and its principal. */
19
20
  export interface SessionRebuildFacts {
20
21
  /** The session was opened by a recovery-code login and is not yet re-secured. */
@@ -35,7 +36,13 @@ export interface SessionRestrictionStore {
35
36
  }
36
37
  export declare class MariaDbSessionRestrictionStore implements SessionRestrictionStore {
37
38
  private readonly pool;
38
- constructor(pool: Pool);
39
+ /** `<identity>`.`session`. */
40
+ private readonly sessionTable;
41
+ /** `<app>`.`users`. */
42
+ private readonly usersTable;
43
+ constructor(pool: Pool,
44
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
45
+ schemas?: FrameworkSchemaNames);
39
46
  markMustReenroll(sessionId: string): Promise<void>;
40
47
  clearMustReenroll(sessionId: string): Promise<void>;
41
48
  rebuildFacts(sessionId: string, principalId: string): Promise<SessionRebuildFacts>;
@@ -1 +1 @@
1
- {"version":3,"file":"session-restriction-store.d.ts","sourceRoot":"","sources":["../../src/session/session-restriction-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,uBAAuB;IACtC,uGAAuG;IACvG,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,+DAA+D;IAC/D,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,2FAA2F;IAC3F,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACpF;AAKD,qBAAa,8BAA+B,YAAW,uBAAuB;IAChE,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEjC,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOlD,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOnD,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;CAqBzF"}
1
+ {"version":3,"file":"session-restriction-store.d.ts","sourceRoot":"","sources":["../../src/session/session-restriction-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAE7C,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,uBAAuB;IACtC,uGAAuG;IACvG,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,+DAA+D;IAC/D,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,2FAA2F;IAC3F,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACpF;AAKD,qBAAa,8BAA+B,YAAW,uBAAuB;IAO1E,OAAO,CAAC,QAAQ,CAAC,IAAI;IANvB,8BAA8B;IAC9B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,uBAAuB;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAGjB,IAAI,EAAE,IAAI;IAC3B,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAO1B,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOlD,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOnD,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;CAqBzF"}
@@ -1,26 +1,36 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MariaDbSessionRestrictionStore = void 0;
4
+ const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
4
5
  /** MariaDB `ER_NO_SUCH_TABLE`. */
5
6
  const ER_NO_SUCH_TABLE = 1146;
6
7
  class MariaDbSessionRestrictionStore {
7
8
  pool;
8
- constructor(pool) {
9
+ /** `<identity>`.`session`. */
10
+ sessionTable;
11
+ /** `<app>`.`users`. */
12
+ usersTable;
13
+ constructor(pool,
14
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
15
+ schemas) {
9
16
  this.pool = pool;
17
+ const s = (0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas);
18
+ this.sessionTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.identity, 'session');
19
+ this.usersTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.app, 'users');
10
20
  }
11
21
  async markMustReenroll(sessionId) {
12
- await this.pool.query('UPDATE `shield_identity`.`session` SET s_must_reenroll = 1 WHERE s_session_id = ?', [sessionId]);
22
+ await this.pool.query(`UPDATE ${this.sessionTable} SET s_must_reenroll = 1 WHERE s_session_id = ?`, [sessionId]);
13
23
  }
14
24
  async clearMustReenroll(sessionId) {
15
- await this.pool.query('UPDATE `shield_identity`.`session` SET s_must_reenroll = 0 WHERE s_session_id = ?', [sessionId]);
25
+ await this.pool.query(`UPDATE ${this.sessionTable} SET s_must_reenroll = 0 WHERE s_session_id = ?`, [sessionId]);
16
26
  }
17
27
  async rebuildFacts(sessionId, principalId) {
18
- const rows = (await this.pool.query('SELECT s_must_reenroll FROM `shield_identity`.`session` WHERE s_session_id = ? LIMIT 1', [sessionId]));
28
+ const rows = (await this.pool.query(`SELECT s_must_reenroll FROM ${this.sessionTable} WHERE s_session_id = ? LIMIT 1`, [sessionId]));
19
29
  // No row → treat as restricted (the caller also re-reads the session and refuses a missing one).
20
30
  const mustReenroll = rows[0] === undefined || Number(rows[0].s_must_reenroll) !== 0;
21
31
  let accountActive = true;
22
32
  try {
23
- const users = (await this.pool.query('SELECT u_status FROM `shield_app`.`users` WHERE u_uuid = ? LIMIT 1', [principalId]));
33
+ const users = (await this.pool.query(`SELECT u_status FROM ${this.usersTable} WHERE u_uuid = ? LIMIT 1`, [principalId]));
24
34
  if (users[0] !== undefined)
25
35
  accountActive = users[0].u_status === 'active';
26
36
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `FileIntake(options)` (FILE_STORAGE SPEC §5 G, §13): one multipart file, checked, then handed
3
+ * to the route handler.
4
+ *
5
+ * @Classified({ typeKey: 'stored_file', action: 'create' })
6
+ * @UseInterceptors(FileIntake({ field: 'file', profile: 'xlsx' }))
7
+ *
8
+ * - Interceptors run after guards, so the global guard has decided (and a denied caller is
9
+ * refused) before any multipart byte is read (Shield #1, #3). A request with no principal is
10
+ * refused here too, before parsing.
11
+ * - Parsing wraps `@nestjs/platform-express` `FileInterceptor` with multer's memory storage
12
+ * (no module-level `MulterModule` options apply: no disk storage, no consumer file filter) and
13
+ * limits `{ files: 1, fileSize: maxBytes + 1, fields: 0, parts: 2 }` and `defParamCharset:
14
+ * 'utf8'`. `maxBytes` defaults to the bound profile's configured cap (`OBJECT_STORAGE_MAX_BYTES`
15
+ * under `storage: 'env'`); an explicit smaller `maxBytes` wins; never above 50 MiB.
16
+ * - Then `SHIELD_FILE_INGEST` checks the bytes (size, extension, declared type, magic) with the
17
+ * tenant from the session (`req.principal.tenantId`, never the request body).
18
+ * - Errors are mapped here, at the source: 400 / 413 / 415 `HttpException`s carrying a fixed
19
+ * reason code. The file name, field name and bytes are never echoed (Shield #10).
20
+ * - On success the request carries `req[FILE_INTAKE_REQUEST_KEY] = { file, bytes }` for
21
+ * `FilesService.store`; multer's raw `req.file` (unsanitised name) is removed.
22
+ */
23
+ import { type NestInterceptor, type Type } from '@nestjs/common';
24
+ import { type FileRejectionReason, type IngestedFile } from '@astram/core';
25
+ /** The HTTP reason codes `FileIntake` answers with: the core reasons plus `bad_request`. */
26
+ export type FileIntakeRejectionReason = FileRejectionReason | 'bad_request';
27
+ /** Options for {@link FileIntake}. */
28
+ export interface FileIntakeOptions {
29
+ /** The multipart field that carries the file. */
30
+ readonly field: string;
31
+ /** A profile name known to `SHIELD_FILE_INGEST` (e.g. `'xlsx'`). */
32
+ readonly profile: string;
33
+ /**
34
+ * Lower multipart byte cap for this route (at most 50 MiB). Default: the profile's configured
35
+ * cap on `SHIELD_FILE_INGEST` (`OBJECT_STORAGE_MAX_BYTES` under `storage: 'env'`, else 10 MiB);
36
+ * a larger value than the profile cap is lowered to it. The parser stops at `maxBytes + 1`;
37
+ * the profile's own cap still applies in the ingest check.
38
+ */
39
+ readonly maxBytes?: number;
40
+ }
41
+ /** What {@link FileIntake} attaches to the request for `FilesService.store`. */
42
+ export interface FileIntakeResult {
43
+ /** Server-built metadata (storage key, sha256, canonical type, size, sanitised name). */
44
+ readonly file: IngestedFile;
45
+ /** The exact bytes that were checked. */
46
+ readonly bytes: Uint8Array;
47
+ }
48
+ /** The request property {@link FileIntake} sets to a {@link FileIntakeResult}. */
49
+ export declare const FILE_INTAKE_REQUEST_KEY = "shieldFileIntake";
50
+ /** Fallback multipart cap (10 MiB, SPEC §10) when the bound ingest cannot report a profile cap. */
51
+ export declare const FILE_INTAKE_DEFAULT_MAX_BYTES: number;
52
+ /** Build the intake interceptor for one route (see the file header). Throws at boot on bad options. */
53
+ export declare function FileIntake(options: FileIntakeOptions): Type<NestInterceptor>;
54
+ //# sourceMappingURL=file-intake.interceptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-intake.interceptor.d.ts","sourceRoot":"","sources":["../../src/storage/file-intake.interceptor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAWL,KAAK,eAAe,EACpB,KAAK,IAAI,EACV,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAIL,KAAK,mBAAmB,EACxB,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAMtB,4FAA4F;AAC5F,MAAM,MAAM,yBAAyB,GAAG,mBAAmB,GAAG,aAAa,CAAC;AAE5E,sCAAsC;AACtC,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAC/B,yFAAyF;IACzF,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,qBAAqB,CAAC;AAE1D,mGAAmG;AACnG,eAAO,MAAM,6BAA6B,QAAmB,CAAC;AAuH9D,uGAAuG;AACvG,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAC,eAAe,CAAC,CAkF5E"}