@astram/guard 0.6.0 → 0.8.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/account.controller.d.ts +3 -1
- package/dist/auth/account.controller.d.ts.map +1 -1
- package/dist/auth/account.controller.js +12 -5
- package/dist/auth/auth.controller.d.ts +130 -21
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +537 -143
- package/dist/auth/auth.module.d.ts +15 -1
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +241 -30
- package/dist/auth/connections.d.ts +1 -1
- package/dist/auth/connections.d.ts.map +1 -1
- package/dist/auth/connections.js +14 -3
- package/dist/auth/default-session-facts-projector.d.ts +26 -0
- package/dist/auth/default-session-facts-projector.d.ts.map +1 -0
- package/dist/auth/default-session-facts-projector.js +21 -0
- package/dist/auth/mfa-step-up.d.ts +32 -0
- package/dist/auth/mfa-step-up.d.ts.map +1 -0
- package/dist/auth/mfa-step-up.js +109 -0
- package/dist/auth/pending-mfa-store.d.ts +50 -52
- package/dist/auth/pending-mfa-store.d.ts.map +1 -1
- package/dist/auth/pending-mfa-store.js +103 -61
- package/dist/auth/ports/session-facts-projector.d.ts +5 -0
- package/dist/auth/ports/session-facts-projector.d.ts.map +1 -1
- package/dist/auth/rate-limit/canonical-hint.d.ts +24 -0
- package/dist/auth/rate-limit/canonical-hint.d.ts.map +1 -0
- package/dist/auth/rate-limit/canonical-hint.js +63 -0
- package/dist/auth/rate-limit/trust-proxy-check.d.ts +46 -0
- package/dist/auth/rate-limit/trust-proxy-check.d.ts.map +1 -0
- package/dist/auth/rate-limit/trust-proxy-check.js +94 -0
- package/dist/auth/tokens.d.ts +70 -11
- package/dist/auth/tokens.d.ts.map +1 -1
- package/dist/auth/tokens.js +71 -12
- package/dist/authentication/log-email-sender.d.ts +9 -4
- package/dist/authentication/log-email-sender.d.ts.map +1 -1
- package/dist/authentication/mariadb-api-key-store.d.ts +46 -0
- package/dist/authentication/mariadb-api-key-store.d.ts.map +1 -0
- package/dist/authentication/mariadb-api-key-store.js +111 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts +79 -65
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-resolver.js +116 -73
- package/dist/authentication/mariadb-credential-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-or-totp-verifier.d.ts +21 -3
- package/dist/authentication/mariadb-or-totp-verifier.d.ts.map +1 -1
- package/dist/authentication/mariadb-or-totp-verifier.js +37 -6
- package/dist/authentication/mariadb-profile-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-session-sweeper.d.ts +14 -1
- package/dist/authentication/mariadb-session-sweeper.d.ts.map +1 -1
- package/dist/authentication/mariadb-session-sweeper.js +29 -11
- package/dist/authentication/noop-email-sender.d.ts +12 -0
- package/dist/authentication/noop-email-sender.d.ts.map +1 -0
- package/dist/authentication/noop-email-sender.js +9 -0
- package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -1
- package/dist/authz/manifest-operation-authority-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js +3 -1
- package/dist/authz/mariadb-policy-store.js +1 -1
- package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-record-rule-adapter.js +1 -4
- package/dist/authz/permission-version-cache.d.ts +38 -0
- package/dist/authz/permission-version-cache.d.ts.map +1 -0
- package/dist/authz/permission-version-cache.js +81 -0
- package/dist/authz/wire-record-authorization.d.ts +25 -1
- package/dist/authz/wire-record-authorization.d.ts.map +1 -1
- package/dist/authz/wire-record-authorization.js +55 -2
- package/dist/completeness/catalog-reconciler.d.ts.map +1 -1
- package/dist/config/app-config.d.ts +52 -0
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +66 -4
- package/dist/encryption/local-kek-crypto.d.ts +15 -0
- package/dist/encryption/local-kek-crypto.d.ts.map +1 -0
- package/dist/encryption/local-kek-crypto.js +79 -0
- package/dist/enforcement/enforcement.module.d.ts.map +1 -1
- package/dist/enforcement/enforcement.module.js +9 -0
- package/dist/enforcement/engine-error.filter.d.ts +57 -0
- package/dist/enforcement/engine-error.filter.d.ts.map +1 -0
- package/dist/enforcement/engine-error.filter.js +118 -0
- package/dist/enforcement/global-enforcement.guard.d.ts +31 -2
- package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
- package/dist/enforcement/global-enforcement.guard.js +148 -6
- package/dist/identity/auth-facts.controller.d.ts.map +1 -1
- package/dist/identity/identity-actions.d.ts +9 -0
- package/dist/identity/identity-actions.d.ts.map +1 -1
- package/dist/identity/identity-actions.js +16 -1
- package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
- package/dist/identity/permission-facts.adapter.js +2 -3
- package/dist/index.d.ts +17 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +82 -25
- package/dist/module/shield.module.d.ts +9 -2
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +47 -38
- package/dist/request-auth/request-principal.d.ts +11 -3
- package/dist/request-auth/request-principal.d.ts.map +1 -1
- package/dist/request-auth/request-principal.js +58 -2
- package/dist/request-auth/session-facts-als.d.ts +16 -1
- package/dist/request-auth/session-facts-als.d.ts.map +1 -1
- package/dist/request-auth/session-facts-als.js +23 -0
- package/dist/request-auth/session-token-binder.d.ts +166 -10
- package/dist/request-auth/session-token-binder.d.ts.map +1 -1
- package/dist/request-auth/session-token-binder.js +408 -28
- package/dist/request-auth/tokens.d.ts +10 -0
- package/dist/request-auth/tokens.d.ts.map +1 -1
- package/dist/request-auth/tokens.js +11 -1
- package/dist/secrets/encrypted-file-secrets.d.ts +16 -0
- package/dist/secrets/encrypted-file-secrets.d.ts.map +1 -0
- package/dist/secrets/encrypted-file-secrets.js +98 -0
- package/dist/session/hot-revoking-session-record-store.d.ts +46 -0
- package/dist/session/hot-revoking-session-record-store.d.ts.map +1 -0
- package/dist/session/hot-revoking-session-record-store.js +58 -0
- package/dist/session/index.d.ts +3 -1
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +8 -1
- package/dist/session/mariadb-session-record-store.d.ts +6 -0
- package/dist/session/mariadb-session-record-store.d.ts.map +1 -1
- package/dist/session/mariadb-session-record-store.js +16 -1
- package/dist/session/session-lifetimes.d.ts +30 -0
- package/dist/session/session-lifetimes.d.ts.map +1 -0
- package/dist/session/session-lifetimes.js +21 -0
- package/dist/session/session-restriction-store.d.ts +43 -0
- package/dist/session/session-restriction-store.d.ts.map +1 -0
- package/dist/session/session-restriction-store.js +35 -0
- package/migrations/1710000000017-CreateServiceAccountKey.cjs +41 -0
- package/package.json +3 -3
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_REDIS_URL = exports.DEFAULT_DB_CONFIG = exports.SCHEMA_SVC_POOL_HARD_CAP = exports.DEFAULT_POOL_BUDGET = void 0;
|
|
3
|
+
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.isDevOrTestNodeEnv = isDevOrTestNodeEnv;
|
|
4
5
|
exports.loadPoolBudget = loadPoolBudget;
|
|
5
6
|
exports.loadDbConfig = loadDbConfig;
|
|
6
7
|
exports.loadRedisConfig = loadRedisConfig;
|
|
7
8
|
exports.loadSessionLifetimes = loadSessionLifetimes;
|
|
9
|
+
exports.loadLoginIpLimit = loadLoginIpLimit;
|
|
8
10
|
exports.loadRefreshHmacKey = loadRefreshHmacKey;
|
|
9
11
|
exports.assertRefreshHmacKey = assertRefreshHmacKey;
|
|
10
12
|
exports.loadAppConfig = loadAppConfig;
|
|
13
|
+
exports.loadAppConfigAsync = loadAppConfigAsync;
|
|
11
14
|
exports.evaluatePoolBudget = evaluatePoolBudget;
|
|
15
|
+
const core_1 = require("@astram/core");
|
|
16
|
+
/** Plan D3 default: 50 failed logins per client IP per 15 minutes. */
|
|
17
|
+
exports.DEFAULT_LOGIN_IP_LIMIT = {
|
|
18
|
+
maxAttempts: 50,
|
|
19
|
+
windowMs: 15 * 60 * 1000,
|
|
20
|
+
};
|
|
21
|
+
/** Default Redis per-command timeout (GAPS #7): the request path fails closed within ~2 s. */
|
|
22
|
+
exports.DEFAULT_REDIS_COMMAND_TIMEOUT_MS = 2000;
|
|
12
23
|
/** Documented MVP single-node defaults (see the class doc + my.cnf). */
|
|
13
24
|
exports.DEFAULT_POOL_BUDGET = {
|
|
14
25
|
workers: 8,
|
|
@@ -28,11 +39,27 @@ function parsePort(raw, fallback) {
|
|
|
28
39
|
const n = Number.parseInt(raw, 10);
|
|
29
40
|
return Number.isInteger(n) && n > 0 && n < 65_536 ? n : fallback;
|
|
30
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The `environment` LABEL (logging / display). NOTE: it maps an UNSET `NODE_ENV` to
|
|
44
|
+
* `'development'`, so it must NEVER drive a security default — an unset env is not
|
|
45
|
+
* proof of a dev box. Security-relevant choices (e.g. the dev `LogEmailSender`, which
|
|
46
|
+
* prints live reset links) read the RAW `NODE_ENV` instead: see
|
|
47
|
+
* {@link isDevOrTestNodeEnv}.
|
|
48
|
+
*/
|
|
31
49
|
function parseEnvironment(raw) {
|
|
32
50
|
if (raw === 'production' || raw === 'test')
|
|
33
51
|
return raw;
|
|
34
52
|
return 'development';
|
|
35
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* `true` ONLY when the RAW `NODE_ENV` is explicitly `'development'` or `'test'`.
|
|
56
|
+
* Unset, blank, `'production'`, `'staging'` or anything else ⇒ `false`. Used to gate
|
|
57
|
+
* dev-only defaults that would leak secrets in a real deployment (fail closed: an
|
|
58
|
+
* unset NODE_ENV is treated as "not dev").
|
|
59
|
+
*/
|
|
60
|
+
function isDevOrTestNodeEnv(raw) {
|
|
61
|
+
return raw === 'development' || raw === 'test';
|
|
62
|
+
}
|
|
36
63
|
/**
|
|
37
64
|
* Parse a positive integer from env with a fallback. Returns the fallback for
|
|
38
65
|
* missing/blank/non-integer/≤0 values — the boot assertion is the authority on
|
|
@@ -124,18 +151,19 @@ function loadDbConfig(env = process.env) {
|
|
|
124
151
|
* documented default.
|
|
125
152
|
*/
|
|
126
153
|
function loadRedisConfig(env = process.env) {
|
|
154
|
+
const commandTimeoutMs = parsePositiveInt(env.REDIS_COMMAND_TIMEOUT_MS, exports.DEFAULT_REDIS_COMMAND_TIMEOUT_MS);
|
|
127
155
|
const explicit = env.REDIS_URL;
|
|
128
156
|
if (explicit !== undefined && explicit.trim() !== '') {
|
|
129
|
-
return { url: explicit.trim() };
|
|
157
|
+
return { url: explicit.trim(), commandTimeoutMs };
|
|
130
158
|
}
|
|
131
159
|
const host = env.REDIS_HOST;
|
|
132
160
|
const port = env.REDIS_PORT;
|
|
133
161
|
if ((host !== undefined && host.trim() !== '') || (port !== undefined && port.trim() !== '')) {
|
|
134
162
|
const h = parseString(host, 'localhost');
|
|
135
163
|
const p = parsePort(port, 6379);
|
|
136
|
-
return { url: `redis://${h}:${p}
|
|
164
|
+
return { url: `redis://${h}:${p}`, commandTimeoutMs };
|
|
137
165
|
}
|
|
138
|
-
return { url: exports.DEFAULT_REDIS_URL };
|
|
166
|
+
return { url: exports.DEFAULT_REDIS_URL, commandTimeoutMs };
|
|
139
167
|
}
|
|
140
168
|
/**
|
|
141
169
|
* Build the optional {@link SessionLifetimeOverrides} from env. Every field is
|
|
@@ -160,6 +188,19 @@ function loadSessionLifetimes(env = process.env) {
|
|
|
160
188
|
overrides.refreshAbsoluteCapMs = cap;
|
|
161
189
|
return overrides;
|
|
162
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Build the per-client-IP login failure limit from env (plan D3). Each field falls back
|
|
193
|
+
* to {@link DEFAULT_LOGIN_IP_LIMIT} when absent/blank/invalid.
|
|
194
|
+
* Field ↔ env name:
|
|
195
|
+
* maxAttempts ← LOGIN_IP_MAX_FAILURES (default 50)
|
|
196
|
+
* windowMs ← LOGIN_IP_WINDOW_MS (default 900000 = 15 min)
|
|
197
|
+
*/
|
|
198
|
+
function loadLoginIpLimit(env = process.env) {
|
|
199
|
+
return {
|
|
200
|
+
maxAttempts: parsePositiveInt(env.LOGIN_IP_MAX_FAILURES, exports.DEFAULT_LOGIN_IP_LIMIT.maxAttempts),
|
|
201
|
+
windowMs: parsePositiveInt(env.LOGIN_IP_WINDOW_MS, exports.DEFAULT_LOGIN_IP_LIMIT.windowMs),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
163
204
|
/**
|
|
164
205
|
* Read the refresh-token HMAC key from env (raw; `''` when absent/blank). Total —
|
|
165
206
|
* does NOT throw, so {@link loadAppConfig} stays boot-safe for a non-auth scaffold.
|
|
@@ -212,8 +253,29 @@ function loadAppConfig(env = process.env) {
|
|
|
212
253
|
redis: loadRedisConfig(env),
|
|
213
254
|
refreshHmacKey: loadRefreshHmacKey(env),
|
|
214
255
|
lifetimes: loadSessionLifetimes(env),
|
|
256
|
+
loginIpLimit: loadLoginIpLimit(env),
|
|
215
257
|
};
|
|
216
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* ADR-0032 secrets integration — build the {@link AppConfig} AFTER resolving any `secret-ref:<path>`
|
|
261
|
+
* env references via the injected `SecretsProvider`, so an app can hold only REFERENCES in its env
|
|
262
|
+
* (safe to commit/log) and the real credentials are fetched at boot.
|
|
263
|
+
*
|
|
264
|
+
* - With a `provider`: every `secret-ref:` value in `env` is resolved (fail-closed — a missing
|
|
265
|
+
* reference aborts boot; a value is never logged), producing a resolved env the pure
|
|
266
|
+
* {@link loadAppConfig} consumes unchanged.
|
|
267
|
+
* - Without a `provider`: identical to `loadAppConfig(env)` — literal env, unchanged behaviour
|
|
268
|
+
* (backward compatible; references would pass through as literals, which fail loudly at connect
|
|
269
|
+
* time rather than silently — the operator is expected to wire a provider when using references).
|
|
270
|
+
*
|
|
271
|
+
* The core resolver (`resolveSecretRefs`) does the work; this is the guard-side boot seam.
|
|
272
|
+
*/
|
|
273
|
+
async function loadAppConfigAsync(provider, env = process.env) {
|
|
274
|
+
if (provider === undefined)
|
|
275
|
+
return loadAppConfig(env);
|
|
276
|
+
const resolved = await (0, core_1.resolveSecretRefs)(env, provider);
|
|
277
|
+
return loadAppConfig(resolved);
|
|
278
|
+
}
|
|
217
279
|
/**
|
|
218
280
|
* Evaluate a pool budget. Pure: no throwing, no I/O — the caller decides how to
|
|
219
281
|
* react (the boot assertion throws; tests inspect the fields).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CryptoPort, Dek, WrappedDek, Ciphertext } from '@astram/core';
|
|
2
|
+
export declare class LocalKekCrypto implements CryptoPort {
|
|
3
|
+
#private;
|
|
4
|
+
/**
|
|
5
|
+
* @param kek the 32-byte Key Encryption Key (from a KMS/secrets-manager reference). MUST be
|
|
6
|
+
* exactly 32 bytes. NEVER hard-code it; NEVER log it.
|
|
7
|
+
*/
|
|
8
|
+
constructor(kek: Uint8Array);
|
|
9
|
+
generateDek(): Promise<Dek>;
|
|
10
|
+
wrapDek(dek: Dek, tenantId: string): Promise<WrappedDek>;
|
|
11
|
+
unwrapDek(wrapped: WrappedDek, tenantId: string): Promise<Dek>;
|
|
12
|
+
encrypt(plaintext: Uint8Array, dek: Dek, tenantId: string): Promise<Ciphertext>;
|
|
13
|
+
decrypt(ciphertext: Ciphertext, dek: Dek, tenantId: string): Promise<Uint8Array>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=local-kek-crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-kek-crypto.d.ts","sourceRoot":"","sources":["../../src/encryption/local-kek-crypto.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAO5E,qBAAa,cAAe,YAAW,UAAU;;IAG/C;;;OAGG;gBACS,GAAG,EAAE,UAAU;IAOrB,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC;IAI3B,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAIxD,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAI9D,OAAO,CAAC,SAAS,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAI/E,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;CA2BvF"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalKekCrypto = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `LocalKekCrypto` — a reference {@link CryptoPort} adapter (ADR-0031). Real AES-256-GCM AEAD for
|
|
6
|
+
* data + DEK-wrapping, with the KEK held as a local secret (from a secrets-manager reference /
|
|
7
|
+
* env). This is the dev + air-gapped / single-box adapter (what astram-infra's box can use); the
|
|
8
|
+
* AWS-KMS / GCP-KMS / Vault-Transit adapters implement the SAME port and swap in on demand
|
|
9
|
+
* (Rule of Three).
|
|
10
|
+
*
|
|
11
|
+
* SECURITY:
|
|
12
|
+
* - AES-256-GCM (authenticated): a 96-bit random nonce per operation; the 128-bit tag is verified
|
|
13
|
+
* on decrypt; the `tenantId` is bound as ADDITIONAL AUTHENTICATED DATA (AAD) so a ciphertext for
|
|
14
|
+
* tenant A cannot be decrypted as tenant B (the core's cross-tenant guard, enforced in crypto).
|
|
15
|
+
* - DEK wrapping uses the SAME AEAD under the KEK, with the tenantId as AAD — a wrapped DEK is
|
|
16
|
+
* bound to its tenant.
|
|
17
|
+
* - The KEK is 32 bytes, provided at construction (never hard-coded, never logged). Losing it makes
|
|
18
|
+
* all wrapped DEKs (hence all data) unrecoverable — it belongs in a KMS/secrets-manager.
|
|
19
|
+
* - NEVER logs a plaintext, a DEK, or the KEK.
|
|
20
|
+
*
|
|
21
|
+
* Self-describing output layout (both encrypt + wrap): [1B version][12B nonce][16B tag][ciphertext].
|
|
22
|
+
*/
|
|
23
|
+
const node_crypto_1 = require("node:crypto");
|
|
24
|
+
const VERSION = 1;
|
|
25
|
+
const NONCE_LEN = 12;
|
|
26
|
+
const TAG_LEN = 16;
|
|
27
|
+
const DEK_LEN = 32; // AES-256
|
|
28
|
+
class LocalKekCrypto {
|
|
29
|
+
#kek;
|
|
30
|
+
/**
|
|
31
|
+
* @param kek the 32-byte Key Encryption Key (from a KMS/secrets-manager reference). MUST be
|
|
32
|
+
* exactly 32 bytes. NEVER hard-code it; NEVER log it.
|
|
33
|
+
*/
|
|
34
|
+
constructor(kek) {
|
|
35
|
+
if (kek.length !== DEK_LEN) {
|
|
36
|
+
throw new Error(`KEK must be ${DEK_LEN} bytes (got ${kek.length}).`);
|
|
37
|
+
}
|
|
38
|
+
this.#kek = Buffer.from(kek);
|
|
39
|
+
}
|
|
40
|
+
async generateDek() {
|
|
41
|
+
return new Uint8Array((0, node_crypto_1.randomBytes)(DEK_LEN));
|
|
42
|
+
}
|
|
43
|
+
async wrapDek(dek, tenantId) {
|
|
44
|
+
return this.#seal(this.#kek, Buffer.from(dek), tenantId);
|
|
45
|
+
}
|
|
46
|
+
async unwrapDek(wrapped, tenantId) {
|
|
47
|
+
return new Uint8Array(this.#open(this.#kek, Buffer.from(wrapped), tenantId));
|
|
48
|
+
}
|
|
49
|
+
async encrypt(plaintext, dek, tenantId) {
|
|
50
|
+
return this.#seal(Buffer.from(dek), Buffer.from(plaintext), tenantId);
|
|
51
|
+
}
|
|
52
|
+
async decrypt(ciphertext, dek, tenantId) {
|
|
53
|
+
return new Uint8Array(this.#open(Buffer.from(dek), Buffer.from(ciphertext), tenantId));
|
|
54
|
+
}
|
|
55
|
+
// ── AES-256-GCM seal/open, tenantId bound as AAD ──────────────────────────
|
|
56
|
+
#seal(key, plaintext, tenantId) {
|
|
57
|
+
const nonce = (0, node_crypto_1.randomBytes)(NONCE_LEN);
|
|
58
|
+
const cipher = (0, node_crypto_1.createCipheriv)('aes-256-gcm', key, nonce);
|
|
59
|
+
cipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
60
|
+
const body = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
61
|
+
const tag = cipher.getAuthTag();
|
|
62
|
+
return new Uint8Array(Buffer.concat([Buffer.from([VERSION]), nonce, tag, body]));
|
|
63
|
+
}
|
|
64
|
+
#open(key, sealed, tenantId) {
|
|
65
|
+
if (sealed.length < 1 + NONCE_LEN + TAG_LEN)
|
|
66
|
+
throw new Error('ciphertext too short / malformed');
|
|
67
|
+
if (sealed[0] !== VERSION)
|
|
68
|
+
throw new Error(`unsupported ciphertext version ${sealed[0]}`);
|
|
69
|
+
const nonce = sealed.subarray(1, 1 + NONCE_LEN);
|
|
70
|
+
const tag = sealed.subarray(1 + NONCE_LEN, 1 + NONCE_LEN + TAG_LEN);
|
|
71
|
+
const body = sealed.subarray(1 + NONCE_LEN + TAG_LEN);
|
|
72
|
+
const decipher = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', key, nonce);
|
|
73
|
+
decipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
74
|
+
decipher.setAuthTag(tag);
|
|
75
|
+
// .final() THROWS on a tag/AAD mismatch — the fail-closed guarantee the core relies on.
|
|
76
|
+
return Buffer.concat([decipher.update(body), decipher.final()]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.LocalKekCrypto = LocalKekCrypto;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enforcement.module.d.ts","sourceRoot":"","sources":["../../src/enforcement/enforcement.module.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"enforcement.module.d.ts","sourceRoot":"","sources":["../../src/enforcement/enforcement.module.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;GAcG;AACH,qBAgBa,iBAAiB;CAAG"}
|
|
@@ -10,6 +10,7 @@ exports.EnforcementModule = void 0;
|
|
|
10
10
|
const common_1 = require("@nestjs/common");
|
|
11
11
|
const core_1 = require("@nestjs/core");
|
|
12
12
|
const global_enforcement_guard_js_1 = require("./global-enforcement.guard.js");
|
|
13
|
+
const engine_error_filter_js_1 = require("./engine-error.filter.js");
|
|
13
14
|
/**
|
|
14
15
|
* The SOLE definition of the global `APP_GUARD` binding (ADR-0002).
|
|
15
16
|
*
|
|
@@ -35,6 +36,14 @@ exports.EnforcementModule = EnforcementModule = __decorate([
|
|
|
35
36
|
provide: core_1.APP_GUARD,
|
|
36
37
|
useClass: global_enforcement_guard_js_1.GlobalEnforcementGuard,
|
|
37
38
|
},
|
|
39
|
+
// The framework-wide engine error → HTTP mapping (security hardening Phase 1C): an engine
|
|
40
|
+
// deny / validation / unavailable error from any consumer controller answers 403 / 400 /
|
|
41
|
+
// 503 instead of 500. Bound here, beside the one APP_GUARD, so forRoot and forRootAsync
|
|
42
|
+
// (which both compose this module) register it exactly once.
|
|
43
|
+
{
|
|
44
|
+
provide: core_1.APP_FILTER,
|
|
45
|
+
useClass: engine_error_filter_js_1.EngineErrorFilter,
|
|
46
|
+
},
|
|
38
47
|
],
|
|
39
48
|
})
|
|
40
49
|
], EnforcementModule);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EngineErrorFilter — the framework-wide mapping of `@astram/engine` errors to HTTP
|
|
3
|
+
* (security hardening Phase 1C). Registered ONCE as a global `APP_FILTER` by
|
|
4
|
+
* `ShieldModule.forRoot` / `forRootAsync`.
|
|
5
|
+
*
|
|
6
|
+
* A consumer feature controller that writes through `RecordsService` sees the engine's typed,
|
|
7
|
+
* fail-closed errors. Unmapped, Nest answered every one of them with a 500. This filter answers:
|
|
8
|
+
*
|
|
9
|
+
* AuthorizationDeniedError (incl. ADR-0034 `create_outside_write_scope`) → 403
|
|
10
|
+
* ValidationError → 400
|
|
11
|
+
* EngineUnavailableError → 503
|
|
12
|
+
*
|
|
13
|
+
* with FIXED, id-free messages (ADR-0012): the engine's own message carries reason codes, record
|
|
14
|
+
* ids and detail meant for logs, never for clients. There is deliberately no 404 mapping: the
|
|
15
|
+
* engine has no not-found error, because a missing or other-tenant row is folded into a deny so
|
|
16
|
+
* that it cannot be probed (`fetch-record.ts`).
|
|
17
|
+
*
|
|
18
|
+
* ── WHAT IT LEAVES ALONE ───────────────────────────────────────────────────────────────────
|
|
19
|
+
* `@Catch` lists only {@link EngineHttpMappableError}, so Nest hands this filter nothing else:
|
|
20
|
+
* `HttpException`s, `CapabilityNotSupportedError` / `RegistrationError` (framework gaps and
|
|
21
|
+
* misconfiguration — a 500 is right), and every non-engine error keep Nest's default handling and
|
|
22
|
+
* any consumer filter. Controller/method filters and interceptors run first, so identity's
|
|
23
|
+
* `IdentityErrorInterceptor` maps its routes before this filter is consulted.
|
|
24
|
+
*
|
|
25
|
+
* ── WHY STRUCTURAL MATCHING (not `instanceof` the engine classes) ──────────────────────────────
|
|
26
|
+
* Guard depends on `@astram/engine`, but this file is statically imported by `shield.module.ts`,
|
|
27
|
+
* whose own compiled output is kept free of runtime engine/core requires (see its header). A
|
|
28
|
+
* structural match also keeps working when a consumer's tree holds two copies of
|
|
29
|
+
* `@astram/engine` (version skew), where `instanceof` silently fails and the 500 returns. An
|
|
30
|
+
* error matches only when ALL hold: it is an `Error`, its prototype chain contains a class named
|
|
31
|
+
* `EngineError` (the engine's base), its `name` is one of the three classes above (the engine
|
|
32
|
+
* sets `name = new.target.name`), and it carries that class's field (`reason` / `field`). A
|
|
33
|
+
* third-party error that merely shares a name is not mapped.
|
|
34
|
+
*/
|
|
35
|
+
import { type ArgumentsHost } from '@nestjs/common';
|
|
36
|
+
import { BaseExceptionFilter } from '@nestjs/core';
|
|
37
|
+
type EngineErrorKind = 'denied' | 'invalid' | 'unavailable';
|
|
38
|
+
/** Classify an engine error this filter maps, or `null` for anything else. */
|
|
39
|
+
export declare function classifyEngineError(err: unknown): EngineErrorKind | null;
|
|
40
|
+
/**
|
|
41
|
+
* The `@Catch` target: Nest selects a filter with `exception instanceof <target>`, and
|
|
42
|
+
* `Symbol.hasInstance` makes that the structural {@link classifyEngineError} test.
|
|
43
|
+
*/
|
|
44
|
+
export declare class EngineHttpMappableError {
|
|
45
|
+
static [Symbol.hasInstance](value: unknown): boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Map an engine error to the HTTP exception the framework answers with, or return it unchanged
|
|
49
|
+
* when it is not a mapped engine error. Exported for unit tests and for callers mapping by hand.
|
|
50
|
+
*/
|
|
51
|
+
export declare function toEngineHttpError(err: unknown): unknown;
|
|
52
|
+
/** The global filter: answers a mapped engine error through Nest's own HttpException reply. */
|
|
53
|
+
export declare class EngineErrorFilter extends BaseExceptionFilter {
|
|
54
|
+
catch(exception: unknown, host: ArgumentsHost): void;
|
|
55
|
+
}
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=engine-error.filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-error.filter.d.ts","sourceRoot":"","sources":["../../src/enforcement/engine-error.filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,EAML,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,KAAK,eAAe,GAAG,QAAQ,GAAG,SAAS,GAAG,aAAa,CAAC;AAa5D,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAaxE;AAED;;;GAGG;AACH,qBAAa,uBAAuB;IAClC,MAAM,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;CAGrD;AAKD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAsBvD;AAED,+FAA+F;AAC/F,qBACa,iBAAkB,SAAQ,mBAAmB;IAC/C,KAAK,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI;CAG9D"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.EngineErrorFilter = exports.EngineHttpMappableError = void 0;
|
|
10
|
+
exports.classifyEngineError = classifyEngineError;
|
|
11
|
+
exports.toEngineHttpError = toEngineHttpError;
|
|
12
|
+
/**
|
|
13
|
+
* EngineErrorFilter — the framework-wide mapping of `@astram/engine` errors to HTTP
|
|
14
|
+
* (security hardening Phase 1C). Registered ONCE as a global `APP_FILTER` by
|
|
15
|
+
* `ShieldModule.forRoot` / `forRootAsync`.
|
|
16
|
+
*
|
|
17
|
+
* A consumer feature controller that writes through `RecordsService` sees the engine's typed,
|
|
18
|
+
* fail-closed errors. Unmapped, Nest answered every one of them with a 500. This filter answers:
|
|
19
|
+
*
|
|
20
|
+
* AuthorizationDeniedError (incl. ADR-0034 `create_outside_write_scope`) → 403
|
|
21
|
+
* ValidationError → 400
|
|
22
|
+
* EngineUnavailableError → 503
|
|
23
|
+
*
|
|
24
|
+
* with FIXED, id-free messages (ADR-0012): the engine's own message carries reason codes, record
|
|
25
|
+
* ids and detail meant for logs, never for clients. There is deliberately no 404 mapping: the
|
|
26
|
+
* engine has no not-found error, because a missing or other-tenant row is folded into a deny so
|
|
27
|
+
* that it cannot be probed (`fetch-record.ts`).
|
|
28
|
+
*
|
|
29
|
+
* ── WHAT IT LEAVES ALONE ───────────────────────────────────────────────────────────────────
|
|
30
|
+
* `@Catch` lists only {@link EngineHttpMappableError}, so Nest hands this filter nothing else:
|
|
31
|
+
* `HttpException`s, `CapabilityNotSupportedError` / `RegistrationError` (framework gaps and
|
|
32
|
+
* misconfiguration — a 500 is right), and every non-engine error keep Nest's default handling and
|
|
33
|
+
* any consumer filter. Controller/method filters and interceptors run first, so identity's
|
|
34
|
+
* `IdentityErrorInterceptor` maps its routes before this filter is consulted.
|
|
35
|
+
*
|
|
36
|
+
* ── WHY STRUCTURAL MATCHING (not `instanceof` the engine classes) ──────────────────────────────
|
|
37
|
+
* Guard depends on `@astram/engine`, but this file is statically imported by `shield.module.ts`,
|
|
38
|
+
* whose own compiled output is kept free of runtime engine/core requires (see its header). A
|
|
39
|
+
* structural match also keeps working when a consumer's tree holds two copies of
|
|
40
|
+
* `@astram/engine` (version skew), where `instanceof` silently fails and the 500 returns. An
|
|
41
|
+
* error matches only when ALL hold: it is an `Error`, its prototype chain contains a class named
|
|
42
|
+
* `EngineError` (the engine's base), its `name` is one of the three classes above (the engine
|
|
43
|
+
* sets `name = new.target.name`), and it carries that class's field (`reason` / `field`). A
|
|
44
|
+
* third-party error that merely shares a name is not mapped.
|
|
45
|
+
*/
|
|
46
|
+
const common_1 = require("@nestjs/common");
|
|
47
|
+
const core_1 = require("@nestjs/core");
|
|
48
|
+
/** True when `err`'s prototype chain contains the engine's `EngineError` base class. */
|
|
49
|
+
function isEngineFamily(err) {
|
|
50
|
+
let proto = Object.getPrototypeOf(err);
|
|
51
|
+
while (proto !== null && proto !== Error.prototype && proto !== Object.prototype) {
|
|
52
|
+
const ctor = proto.constructor;
|
|
53
|
+
if (ctor?.name === 'EngineError')
|
|
54
|
+
return true;
|
|
55
|
+
proto = Object.getPrototypeOf(proto);
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
/** Classify an engine error this filter maps, or `null` for anything else. */
|
|
60
|
+
function classifyEngineError(err) {
|
|
61
|
+
if (!(err instanceof Error) || !isEngineFamily(err))
|
|
62
|
+
return null;
|
|
63
|
+
const fields = err;
|
|
64
|
+
switch (err.name) {
|
|
65
|
+
case 'AuthorizationDeniedError':
|
|
66
|
+
return typeof fields.reason === 'string' ? 'denied' : null;
|
|
67
|
+
case 'ValidationError':
|
|
68
|
+
return typeof fields.field === 'string' ? 'invalid' : null;
|
|
69
|
+
case 'EngineUnavailableError':
|
|
70
|
+
return 'unavailable';
|
|
71
|
+
default:
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The `@Catch` target: Nest selects a filter with `exception instanceof <target>`, and
|
|
77
|
+
* `Symbol.hasInstance` makes that the structural {@link classifyEngineError} test.
|
|
78
|
+
*/
|
|
79
|
+
class EngineHttpMappableError {
|
|
80
|
+
static [Symbol.hasInstance](value) {
|
|
81
|
+
return classifyEngineError(value) !== null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.EngineHttpMappableError = EngineHttpMappableError;
|
|
85
|
+
/** A field name safe to echo (a declared column-style identifier); anything else is not echoed. */
|
|
86
|
+
const ECHOABLE_FIELD = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
87
|
+
/**
|
|
88
|
+
* Map an engine error to the HTTP exception the framework answers with, or return it unchanged
|
|
89
|
+
* when it is not a mapped engine error. Exported for unit tests and for callers mapping by hand.
|
|
90
|
+
*/
|
|
91
|
+
function toEngineHttpError(err) {
|
|
92
|
+
if (err instanceof common_1.HttpException)
|
|
93
|
+
return err;
|
|
94
|
+
switch (classifyEngineError(err)) {
|
|
95
|
+
case 'denied':
|
|
96
|
+
return err.reason === 'create_outside_write_scope'
|
|
97
|
+
? new common_1.ForbiddenException('You cannot create this record: it is outside the organisations you manage.')
|
|
98
|
+
: new common_1.ForbiddenException('You do not have access to perform this action.');
|
|
99
|
+
case 'invalid': {
|
|
100
|
+
const field = err.field;
|
|
101
|
+
return new common_1.BadRequestException(ECHOABLE_FIELD.test(field) ? `Invalid value for '${field}'.` : 'Invalid input.');
|
|
102
|
+
}
|
|
103
|
+
case 'unavailable':
|
|
104
|
+
return new common_1.ServiceUnavailableException('This operation is temporarily unavailable. Try again.');
|
|
105
|
+
default:
|
|
106
|
+
return err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** The global filter: answers a mapped engine error through Nest's own HttpException reply. */
|
|
110
|
+
let EngineErrorFilter = class EngineErrorFilter extends core_1.BaseExceptionFilter {
|
|
111
|
+
catch(exception, host) {
|
|
112
|
+
super.catch(toEngineHttpError(exception), host);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
exports.EngineErrorFilter = EngineErrorFilter;
|
|
116
|
+
exports.EngineErrorFilter = EngineErrorFilter = __decorate([
|
|
117
|
+
(0, common_1.Catch)(EngineHttpMappableError)
|
|
118
|
+
], EngineErrorFilter);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
|
2
2
|
import { Reflector } from '@nestjs/core';
|
|
3
3
|
import type { CoreDecisionAuthority } from './core-seam.js';
|
|
4
|
-
import type
|
|
4
|
+
import { type SessionTokenBinder } from '../request-auth/session-token-binder.js';
|
|
5
|
+
import type { ServiceAuthenticator } from '@astram/core';
|
|
6
|
+
import type { ScopeSubtreeResolver } from '../auth/ports/scope-subtree-resolver.js';
|
|
5
7
|
/**
|
|
6
8
|
* Global, fail-closed enforcement guard (ADR-0002) — now with REAL request
|
|
7
9
|
* authorization for classified routes.
|
|
@@ -33,9 +35,36 @@ export declare class GlobalEnforcementGuard implements CanActivate {
|
|
|
33
35
|
private readonly reflector;
|
|
34
36
|
private readonly decisionAuthority?;
|
|
35
37
|
private readonly tokenBinder?;
|
|
36
|
-
|
|
38
|
+
private readonly serviceAuth?;
|
|
39
|
+
private readonly scopeSubtreeResolver?;
|
|
40
|
+
constructor(reflector: Reflector, decisionAuthority?: CoreDecisionAuthority | undefined, tokenBinder?: SessionTokenBinder | undefined, serviceAuth?: ServiceAuthenticator | undefined, scopeSubtreeResolver?: (ScopeSubtreeResolver | null) | undefined);
|
|
37
41
|
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
38
42
|
/** The real authorization path for a classified route (fail-closed throughout). */
|
|
39
43
|
private authorizeClassified;
|
|
44
|
+
/**
|
|
45
|
+
* Write gate 5's reloaded grants and the live roles onto the session binding (ADR-0035). Session
|
|
46
|
+
* bindings only: a service-account key has no binding (its roles and scope are read live per
|
|
47
|
+
* request by the key store). Best-effort and conditional (same session, same tenant, fresher
|
|
48
|
+
* facts, `XX`): a miss or a Redis error only means the next request reloads again.
|
|
49
|
+
*/
|
|
50
|
+
private rebindReloadedGrants;
|
|
51
|
+
/**
|
|
52
|
+
* Re-derive the session's scope subtree after gate 5 reloaded facts at `reloadedVersion`
|
|
53
|
+
* (security hardening Phase 1C). Identity bumps a tenant's permission version when a company is
|
|
54
|
+
* created, moved or deleted under it, so a reload at a version newer than the bound subtree's
|
|
55
|
+
* stamp means the subtree may be stale: resolve it again with the SAME resolver login uses,
|
|
56
|
+
* rebind it (token + session entries) and return the principal the handler should see.
|
|
57
|
+
*
|
|
58
|
+
* Off the hot path: runs only for a session binding that carries a subtree, with a resolver
|
|
59
|
+
* wired, and a reload NEWER than its stamp (or no stamp yet — once, then stamped). That is the
|
|
60
|
+
* only DB read added (one closure query), and only on a version change.
|
|
61
|
+
*
|
|
62
|
+
* FAIL-CLOSED, NON-WIDENING: a resolver error, a non-array or an EMPTY result →
|
|
63
|
+
* {@link SUBTREE_REDERIVE_FAILED}: the bound subtree stays (never replaced by a guess, never
|
|
64
|
+
* widened), the stamp is not advanced, the reloaded facts are not cached (ADR-0035), and the next
|
|
65
|
+
* request reloads and retries. `null` = no re-derive was due. A failed Redis rebind still returns the fresh principal for this request
|
|
66
|
+
* (the DB answer is authoritative); the next reload retries the rebind.
|
|
67
|
+
*/
|
|
68
|
+
private rederiveSubtreeOnVersionMove;
|
|
40
69
|
}
|
|
41
70
|
//# sourceMappingURL=global-enforcement.guard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"global-enforcement.guard.d.ts","sourceRoot":"","sources":["../../src/enforcement/global-enforcement.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,gBAAgB,EAMjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAOzC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"global-enforcement.guard.d.ts","sourceRoot":"","sources":["../../src/enforcement/global-enforcement.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,gBAAgB,EAMjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAOzC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAK5D,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAA2B,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAOlF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;AAoBpF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBACa,sBAAuB,YAAW,WAAW;IAEtD,OAAO,CAAC,QAAQ,CAAC,SAAS;IAG1B,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC;IAGnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAM7B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAM7B,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC;gBAlBrB,SAAS,EAAE,SAAS,EAGpB,iBAAiB,CAAC,EAAE,qBAAqB,YAAA,EAGzC,WAAW,CAAC,EAAE,kBAAkB,YAAA,EAMhC,WAAW,CAAC,EAAE,oBAAoB,YAAA,EAMlC,oBAAoB,CAAC,GAAE,oBAAoB,GAAG,IAAI,aAAA;IAG/D,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;IAuC9D,mFAAmF;YACrE,mBAAmB;IAyIjC;;;;;OAKG;YACW,oBAAoB;IAuBlC;;;;;;;;;;;;;;;;OAgBG;YACW,4BAA4B;CA8C3C"}
|