@astram/guard 0.2.0 → 0.4.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 +245 -0
- package/dist/auth/account.controller.d.ts.map +1 -0
- package/dist/auth/account.controller.js +673 -0
- package/dist/auth/auth.controller.d.ts +219 -5
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +790 -12
- package/dist/auth/auth.module.d.ts +101 -3
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +300 -8
- package/dist/auth/dto.d.ts +16 -6
- package/dist/auth/dto.d.ts.map +1 -1
- package/dist/auth/ports/audit-sink.d.ts +47 -0
- package/dist/auth/ports/audit-sink.d.ts.map +1 -0
- package/dist/auth/ports/audit-sink.js +20 -0
- package/dist/auth/ports/credential-writer.d.ts +23 -0
- package/dist/auth/ports/credential-writer.d.ts.map +1 -0
- package/dist/auth/ports/credential-writer.js +2 -0
- package/dist/auth/ports/email-sender.d.ts +14 -0
- package/dist/auth/ports/email-sender.d.ts.map +1 -0
- package/dist/auth/ports/email-sender.js +2 -0
- package/dist/auth/ports/mfa-device-store.d.ts +45 -0
- package/dist/auth/ports/mfa-device-store.d.ts.map +1 -0
- package/dist/auth/ports/mfa-device-store.js +13 -0
- package/dist/auth/ports/profile-writer.d.ts +12 -0
- package/dist/auth/ports/profile-writer.d.ts.map +1 -0
- package/dist/auth/ports/profile-writer.js +2 -0
- package/dist/auth/ports/recovery-code-store.d.ts +10 -0
- package/dist/auth/ports/recovery-code-store.d.ts.map +1 -0
- package/dist/auth/ports/recovery-code-store.js +2 -0
- package/dist/auth/ports/reset-token-store.d.ts +12 -0
- package/dist/auth/ports/reset-token-store.d.ts.map +1 -0
- package/dist/auth/ports/reset-token-store.js +2 -0
- package/dist/auth/ports/session-facts-projector.d.ts +44 -0
- package/dist/auth/ports/session-facts-projector.d.ts.map +1 -0
- package/dist/auth/ports/session-facts-projector.js +30 -0
- package/dist/auth/ports/session-sweeper.d.ts +12 -0
- package/dist/auth/ports/session-sweeper.d.ts.map +1 -0
- package/dist/auth/ports/session-sweeper.js +2 -0
- package/dist/auth/profile-resolver.d.ts +73 -0
- package/dist/auth/profile-resolver.d.ts.map +1 -0
- package/dist/auth/profile-resolver.js +28 -0
- package/dist/auth/rate-limit/attempt-limiter.d.ts +91 -0
- package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -0
- package/dist/auth/rate-limit/attempt-limiter.js +133 -0
- package/dist/auth/tokens.d.ts +174 -0
- package/dist/auth/tokens.d.ts.map +1 -1
- package/dist/auth/tokens.js +190 -1
- package/dist/auth/totp-enrollment.d.ts +42 -0
- package/dist/auth/totp-enrollment.d.ts.map +1 -0
- package/dist/auth/totp-enrollment.js +96 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts +38 -24
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-resolver.js +42 -28
- package/dist/authz/cross-tenant-grant-writer.d.ts +259 -0
- package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -0
- package/dist/authz/cross-tenant-grant-writer.js +397 -0
- package/dist/authz/manifest-operation-authority-adapter.d.ts +65 -0
- package/dist/authz/manifest-operation-authority-adapter.d.ts.map +1 -0
- package/dist/authz/manifest-operation-authority-adapter.js +108 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts +90 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js +211 -0
- package/dist/authz/mariadb-record-rule-adapter.d.ts +60 -0
- package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -0
- package/dist/authz/mariadb-record-rule-adapter.js +154 -0
- package/dist/authz/mariadb-record-share-adapter.d.ts +75 -0
- package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -0
- package/dist/authz/mariadb-record-share-adapter.js +106 -0
- package/dist/authz/record-share-cascade.d.ts +52 -0
- package/dist/authz/record-share-cascade.d.ts.map +1 -0
- package/dist/authz/record-share-cascade.js +49 -0
- package/dist/authz/record-share-writer.d.ts +175 -0
- package/dist/authz/record-share-writer.d.ts.map +1 -0
- package/dist/authz/record-share-writer.js +225 -0
- package/dist/authz/wire-record-authorization.d.ts +66 -0
- package/dist/authz/wire-record-authorization.d.ts.map +1 -0
- package/dist/authz/wire-record-authorization.js +34 -0
- package/dist/completeness/catalog-reconciler.d.ts +21 -0
- package/dist/completeness/catalog-reconciler.d.ts.map +1 -0
- package/dist/completeness/catalog-reconciler.js +74 -0
- package/dist/completeness/operation-inventory-scanner.d.ts +17 -0
- package/dist/completeness/operation-inventory-scanner.d.ts.map +1 -0
- package/dist/completeness/operation-inventory-scanner.js +79 -0
- package/dist/completeness/ports/catalog-store.d.ts +33 -0
- package/dist/completeness/ports/catalog-store.d.ts.map +1 -0
- package/dist/completeness/ports/catalog-store.js +2 -0
- package/dist/config/app-config.d.ts +18 -16
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +68 -35
- package/dist/enforcement/classification.decorator.d.ts +33 -14
- package/dist/enforcement/classification.decorator.d.ts.map +1 -1
- package/dist/enforcement/classification.decorator.js +34 -14
- package/dist/enforcement/global-enforcement.guard.d.ts +29 -19
- package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
- package/dist/enforcement/global-enforcement.guard.js +108 -25
- package/dist/enforcement/reenroll-allowlist.d.ts +49 -0
- package/dist/enforcement/reenroll-allowlist.d.ts.map +1 -0
- package/dist/enforcement/reenroll-allowlist.js +77 -0
- package/dist/index.d.ts +35 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +159 -7
- package/dist/module/shield-tokens.d.ts +15 -0
- package/dist/module/shield-tokens.d.ts.map +1 -0
- package/dist/module/shield-tokens.js +17 -0
- package/dist/module/shield.module.d.ts +154 -10
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +181 -24
- package/dist/request-auth/request-principal.d.ts +42 -0
- package/dist/request-auth/request-principal.d.ts.map +1 -0
- package/dist/request-auth/request-principal.js +87 -0
- package/dist/request-auth/session-facts-als.d.ts +65 -0
- package/dist/request-auth/session-facts-als.d.ts.map +1 -0
- package/dist/request-auth/session-facts-als.js +57 -0
- package/dist/request-auth/session-token-binder.d.ts +130 -0
- package/dist/request-auth/session-token-binder.d.ts.map +1 -0
- package/dist/request-auth/session-token-binder.js +277 -0
- package/dist/request-auth/tokens.d.ts +14 -0
- package/dist/request-auth/tokens.d.ts.map +1 -0
- package/dist/request-auth/tokens.js +16 -0
- package/package.json +2 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `buildRecordAuthorization` — the ONE place the four framework-owned record-tier
|
|
3
|
+
* guard adapters are constructed and handed to core's `createWiredDecisionAuthority`
|
|
4
|
+
* (design §9, Phase 5 — "the framework pre-wires").
|
|
5
|
+
*
|
|
6
|
+
* ── WHY THIS FILE EXISTS (the record-tier ownership invariant, ADR-0003 / the
|
|
7
|
+
* "framework-owns-the-data-layer" initiative): before Phase 5, the CONSUMER app
|
|
8
|
+
* hand-constructed `MariaDbRecordPermissionRuleAdapter` / `MariaDbRecordShareAdapter`
|
|
9
|
+
* / `MariaDbCrossTenantGrantAdapter` / `ManifestOperationAuthorityAdapter` and called
|
|
10
|
+
* core's `createWiredDecisionAuthority` itself. That meant the app COULD swap in a
|
|
11
|
+
* fail-OPEN record adapter — the exact hole the initiative closes. Phase 5 moves that
|
|
12
|
+
* construction HERE, inside the framework: the app supplies ONLY the DB pool + the two
|
|
13
|
+
* app-owned ports (`permissionFacts`, `sessionFacts`) + the module registry + the
|
|
14
|
+
* operation-manifest DATA. The four record-tier read adapters are framework-built and
|
|
15
|
+
* UN-overridable — a planted fail-open adapter is now impossible.
|
|
16
|
+
*
|
|
17
|
+
* ── WHY IT IS NOT STATICALLY IMPORTED BY shield.module.ts (the enforcement-only
|
|
18
|
+
* invariant): this file value-imports `@astram/core` (`createWiredDecisionAuthority`)
|
|
19
|
+
* and the adapter classes (which themselves value-import core). `shield.module.ts` is
|
|
20
|
+
* deliberately kept core-free in its OWN compiled output (see its header) so the
|
|
21
|
+
* enforcement-only path is provably free of a top-level `require('@astram/core')`.
|
|
22
|
+
* So `shield.module.ts` reaches `buildRecordAuthorization` ONLY through a LAZY dynamic
|
|
23
|
+
* import inside the `SHIELD_DECISION_AUTHORITY` async factory — which runs ONLY when a
|
|
24
|
+
* consumer opts into `recordAuthorization`. This file (and thus core) loads only then.
|
|
25
|
+
*/
|
|
26
|
+
import type { DecisionAuthority, ModuleRegistry, PermissionFactsPort, SessionFactsReader } from '@astram/core';
|
|
27
|
+
import { type RuleQueryable } from './mariadb-record-rule-adapter.js';
|
|
28
|
+
import { type OperationManifestEntry } from './manifest-operation-authority-adapter.js';
|
|
29
|
+
/**
|
|
30
|
+
* The pieces a consumer supplies for the framework to build the real record-tier
|
|
31
|
+
* DecisionAuthority. The app owns ONLY these — the pool, the two app-owned ports, the
|
|
32
|
+
* module registry, and the operation-manifest DATA. The four record-tier read adapters
|
|
33
|
+
* are framework-constructed from `pool` and are DELIBERATELY NOT part of this shape
|
|
34
|
+
* (they cannot be overridden — the Phase-5 acceptance criterion).
|
|
35
|
+
*/
|
|
36
|
+
export interface RecordAuthorizationInputs {
|
|
37
|
+
/**
|
|
38
|
+
* The MariaDB pool the three DB-backed record-tier adapters read through. Typed
|
|
39
|
+
* structurally (`RuleQueryable`: `{ query(sql, params?) }`) — a `mariadb` `Pool`
|
|
40
|
+
* satisfies it — so the guard module signature does not force a hard `mariadb` type
|
|
41
|
+
* on consumers that inject a custom queryable. The framework builds the adapters over
|
|
42
|
+
* THIS pool; the app never constructs them.
|
|
43
|
+
*/
|
|
44
|
+
readonly pool: RuleQueryable;
|
|
45
|
+
/** APP-OWNED port: the principal's coarse grants for the object-type gate (gate 5). */
|
|
46
|
+
readonly permissionFacts: PermissionFactsPort;
|
|
47
|
+
/** APP-OWNED port: per-request cached session facts (null-slot until the Redis hot path). */
|
|
48
|
+
readonly sessionFacts: SessionFactsReader;
|
|
49
|
+
/** APP-OWNED: the module registry (object types + actions + tiers), bridged from the app's registrations. */
|
|
50
|
+
readonly registry: ModuleRegistry;
|
|
51
|
+
/**
|
|
52
|
+
* APP-SUPPLIED DATA (not an adapter): the declared trusted-operation manifest entries.
|
|
53
|
+
* The framework constructs the `ManifestOperationAuthorityAdapter` over them. Empty
|
|
54
|
+
* (default) ⇒ every delegation lookup is pass-through (undefined). The app supplies the
|
|
55
|
+
* ENTRIES; the framework owns the ADAPTER.
|
|
56
|
+
*/
|
|
57
|
+
readonly operationManifest?: readonly OperationManifestEntry[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Construct the four framework-owned record-tier adapters over the supplied pool +
|
|
61
|
+
* manifest, and return the fully-wired core {@link DecisionAuthority}. Behavior-identical
|
|
62
|
+
* to what the consumer's `buildDecisionAuthority` did before Phase 5 (same adapters, same
|
|
63
|
+
* pool, same registry, same ports) — this is a wiring MOVE, not a logic change.
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildRecordAuthorization(inputs: RecordAuthorizationInputs): DecisionAuthority;
|
|
66
|
+
//# sourceMappingURL=wire-record-authorization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire-record-authorization.d.ts","sourceRoot":"","sources":["../../src/authz/wire-record-authorization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,2CAA2C,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,uFAAuF;IACvF,QAAQ,CAAC,eAAe,EAAE,mBAAmB,CAAC;IAC9C,6FAA6F;IAC7F,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAAC;IAC1C,6GAA6G;IAC7G,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;CAChE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,yBAAyB,GAAG,iBAAiB,CAmB7F"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildRecordAuthorization = buildRecordAuthorization;
|
|
4
|
+
const core_1 = require("@astram/core");
|
|
5
|
+
const mariadb_record_rule_adapter_js_1 = require("./mariadb-record-rule-adapter.js");
|
|
6
|
+
const mariadb_record_share_adapter_js_1 = require("./mariadb-record-share-adapter.js");
|
|
7
|
+
const mariadb_cross_tenant_grant_adapter_js_1 = require("./mariadb-cross-tenant-grant-adapter.js");
|
|
8
|
+
const manifest_operation_authority_adapter_js_1 = require("./manifest-operation-authority-adapter.js");
|
|
9
|
+
/**
|
|
10
|
+
* Construct the four framework-owned record-tier adapters over the supplied pool +
|
|
11
|
+
* manifest, and return the fully-wired core {@link DecisionAuthority}. Behavior-identical
|
|
12
|
+
* to what the consumer's `buildDecisionAuthority` did before Phase 5 (same adapters, same
|
|
13
|
+
* pool, same registry, same ports) — this is a wiring MOVE, not a logic change.
|
|
14
|
+
*/
|
|
15
|
+
function buildRecordAuthorization(inputs) {
|
|
16
|
+
return (0, core_1.createWiredDecisionAuthority)({
|
|
17
|
+
registry: inputs.registry,
|
|
18
|
+
permissionFacts: inputs.permissionFacts,
|
|
19
|
+
sessionFacts: inputs.sessionFacts,
|
|
20
|
+
// ── The FOUR framework-owned record-tier adapters — constructed HERE, and ONLY here.
|
|
21
|
+
// Record-permit ADMIN rules over `shield_authz.record_permission_rule` (widen the
|
|
22
|
+
// core-owned owner baseline, deny-wins). Empty table ⇒ owner-only (NOT a lockout).
|
|
23
|
+
recordRule: new mariadb_record_rule_adapter_js_1.MariaDbRecordPermissionRuleAdapter(inputs.pool),
|
|
24
|
+
// Per-record SHARE source over `shield_authz.record_share` (widening permit, union
|
|
25
|
+
// with baseline under deny-wins). No share ⇒ false (baseline still governs).
|
|
26
|
+
recordShare: new mariadb_record_share_adapter_js_1.MariaDbRecordShareAdapter(inputs.pool),
|
|
27
|
+
// Cross-tenant consented grants over `shield_authz.cross_tenant_grant`. No active
|
|
28
|
+
// grant ⇒ null (deny-by-default for the cross-tenant reach).
|
|
29
|
+
crossTenantGrant: new mariadb_cross_tenant_grant_adapter_js_1.MariaDbCrossTenantGrantAdapter(inputs.pool),
|
|
30
|
+
// Delegation / trusted-operation authority over the DECLARED manifest (no DB).
|
|
31
|
+
// Empty ⇒ pass-through. The app supplies the entries; the framework owns the adapter.
|
|
32
|
+
operationAuthority: new manifest_operation_authority_adapter_js_1.ManifestOperationAuthorityAdapter(inputs.operationManifest ?? []),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { OperationInventory, ModuleRegistry } from '@astram/core';
|
|
2
|
+
import type { CatalogStore, ReconcileReport } from './ports/catalog-store.js';
|
|
3
|
+
/** A @Classified route could not be gated (stranded or key-not-registered). Boot must
|
|
4
|
+
* abort — an ungatable route may never serve traffic (ADR-0002 fail-closed). */
|
|
5
|
+
export declare class CatalogGatabilityError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class CatalogReconciler {
|
|
9
|
+
/**
|
|
10
|
+
* BOOT — read-only, fail-closed. Throws if any non-public route is stranded (no key)
|
|
11
|
+
* or unclassified (declares a key the registry doesn't know). No DB, no writes.
|
|
12
|
+
*/
|
|
13
|
+
verifyGatable(inventory: OperationInventory, registry: ModuleRegistry): void;
|
|
14
|
+
/**
|
|
15
|
+
* CRON — mutating. Auto-adds an ACTIVE catalog row per registered non-public action,
|
|
16
|
+
* marks orphaned any stored row with no registered action, reactivates revived ones.
|
|
17
|
+
* Idempotent. Returns a report for logging + /health/catalog.
|
|
18
|
+
*/
|
|
19
|
+
reconcile(registry: ModuleRegistry, store: CatalogStore, nowIso: string): Promise<ReconcileReport>;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=catalog-reconciler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-reconciler.d.ts","sourceRoot":"","sources":["../../src/completeness/catalog-reconciler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE9E;iFACiF;AACjF,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,iBAAiB;IAC5B;;;OAGG;IACH,aAAa,CAAC,SAAS,EAAE,kBAAkB,EAAE,QAAQ,EAAE,cAAc,GAAG,IAAI;IAgB5E;;;;OAIG;IACG,SAAS,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;CAoCzG"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CatalogReconciler = exports.CatalogGatabilityError = void 0;
|
|
4
|
+
// astram-framework/packages/guard/src/completeness/catalog-reconciler.ts
|
|
5
|
+
const core_1 = require("@astram/core");
|
|
6
|
+
/** A @Classified route could not be gated (stranded or key-not-registered). Boot must
|
|
7
|
+
* abort — an ungatable route may never serve traffic (ADR-0002 fail-closed). */
|
|
8
|
+
class CatalogGatabilityError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'CatalogGatabilityError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.CatalogGatabilityError = CatalogGatabilityError;
|
|
15
|
+
class CatalogReconciler {
|
|
16
|
+
/**
|
|
17
|
+
* BOOT — read-only, fail-closed. Throws if any non-public route is stranded (no key)
|
|
18
|
+
* or unclassified (declares a key the registry doesn't know). No DB, no writes.
|
|
19
|
+
*/
|
|
20
|
+
verifyGatable(inventory, registry) {
|
|
21
|
+
const result = (0, core_1.checkCompleteness)(inventory, registry);
|
|
22
|
+
if (result.strandedOperations.length > 0 || result.unclassifiedOperations.length > 0) {
|
|
23
|
+
const stranded = result.strandedOperations.map((o) => o.operationId);
|
|
24
|
+
const unclassified = result.unclassifiedOperations.map((o) => `${o.operationId}${o.declaredPermissionKey ? ` (${o.declaredPermissionKey})` : ''}`);
|
|
25
|
+
throw new CatalogGatabilityError(`Shield: ${stranded.length} stranded + ${unclassified.length} unclassified route(s) — cannot boot. ` +
|
|
26
|
+
`Stranded (no @Classified): ${stranded.join(', ') || 'none'}. ` +
|
|
27
|
+
`Unclassified (key not registered): ${unclassified.join(', ') || 'none'}. ` +
|
|
28
|
+
`Classify the route or register the action.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* CRON — mutating. Auto-adds an ACTIVE catalog row per registered non-public action,
|
|
33
|
+
* marks orphaned any stored row with no registered action, reactivates revived ones.
|
|
34
|
+
* Idempotent. Returns a report for logging + /health/catalog.
|
|
35
|
+
*/
|
|
36
|
+
async reconcile(registry, store, nowIso) {
|
|
37
|
+
const stored = await store.listPermissions();
|
|
38
|
+
const storedByKey = new Map(stored.map((r) => [r.permissionKey, r]));
|
|
39
|
+
const registered = registry.listActions().filter((a) => a.tier !== 'public');
|
|
40
|
+
const registeredKeys = new Set(registered.map((a) => a.permissionKey));
|
|
41
|
+
// code → catalog: add missing rows; revive orphaned-but-now-registered.
|
|
42
|
+
const added = [];
|
|
43
|
+
const reactivated = [];
|
|
44
|
+
for (const a of registered) {
|
|
45
|
+
const key = a.permissionKey;
|
|
46
|
+
const existing = storedByKey.get(key);
|
|
47
|
+
if (!existing) {
|
|
48
|
+
const inserted = await store.upsertPermission({
|
|
49
|
+
permissionKey: key,
|
|
50
|
+
moduleKey: a.moduleKey,
|
|
51
|
+
typeKey: a.action.typeKey,
|
|
52
|
+
action: a.action.actionKey,
|
|
53
|
+
label: `${a.action.actionKey} ${a.action.typeKey}`,
|
|
54
|
+
tier: a.tier,
|
|
55
|
+
});
|
|
56
|
+
if (inserted)
|
|
57
|
+
added.push(key);
|
|
58
|
+
}
|
|
59
|
+
else if (existing.status === 'orphaned') {
|
|
60
|
+
reactivated.push(key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (reactivated.length)
|
|
64
|
+
await store.markActive(reactivated);
|
|
65
|
+
// catalog → code: mark orphaned any active stored row with no registered action.
|
|
66
|
+
const orphaned = stored
|
|
67
|
+
.filter((r) => r.status === 'active' && !registeredKeys.has(r.permissionKey))
|
|
68
|
+
.map((r) => r.permissionKey);
|
|
69
|
+
if (orphaned.length)
|
|
70
|
+
await store.markOrphaned(orphaned);
|
|
71
|
+
return { added, orphaned, reactivated, at: nowIso };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.CatalogReconciler = CatalogReconciler;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DiscoveryService, MetadataScanner, Reflector } from '@nestjs/core';
|
|
2
|
+
import type { OperationInventory } from '@astram/core';
|
|
3
|
+
/**
|
|
4
|
+
* Reflects every controller handler at boot and emits an OperationInventory for core's
|
|
5
|
+
* checkCompleteness. operationId = `${ControllerClass.name}#${method}` (core's contract).
|
|
6
|
+
* A handler's permissionKey is `${moduleKey}:${typeKey}:${action}` from its @Classified;
|
|
7
|
+
* @Public handlers carry isPublic:true; an unclassified non-public handler carries no
|
|
8
|
+
* permissionKey (→ core reports it stranded).
|
|
9
|
+
*/
|
|
10
|
+
export declare class OperationInventoryScanner {
|
|
11
|
+
private readonly discovery;
|
|
12
|
+
private readonly scanner;
|
|
13
|
+
private readonly reflector;
|
|
14
|
+
constructor(discovery: DiscoveryService, scanner: MetadataScanner, reflector: Reflector);
|
|
15
|
+
scan(moduleKey: string): OperationInventory;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=operation-inventory-scanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation-inventory-scanner.d.ts","sourceRoot":"","sources":["../../src/completeness/operation-inventory-scanner.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE5E,OAAO,KAAK,EAAE,kBAAkB,EAAuB,MAAM,cAAc,CAAC;AAO5E;;;;;;GAMG;AACH,qBACa,yBAAyB;IAElC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAFT,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,SAAS;IAGvC,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB;CAyC5C"}
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.OperationInventoryScanner = void 0;
|
|
13
|
+
const common_1 = require("@nestjs/common");
|
|
14
|
+
const constants_1 = require("@nestjs/common/constants");
|
|
15
|
+
const core_1 = require("@nestjs/core");
|
|
16
|
+
const core_2 = require("@astram/core");
|
|
17
|
+
const classification_decorator_js_1 = require("../enforcement/classification.decorator.js");
|
|
18
|
+
/**
|
|
19
|
+
* Reflects every controller handler at boot and emits an OperationInventory for core's
|
|
20
|
+
* checkCompleteness. operationId = `${ControllerClass.name}#${method}` (core's contract).
|
|
21
|
+
* A handler's permissionKey is `${moduleKey}:${typeKey}:${action}` from its @Classified;
|
|
22
|
+
* @Public handlers carry isPublic:true; an unclassified non-public handler carries no
|
|
23
|
+
* permissionKey (→ core reports it stranded).
|
|
24
|
+
*/
|
|
25
|
+
let OperationInventoryScanner = class OperationInventoryScanner {
|
|
26
|
+
discovery;
|
|
27
|
+
scanner;
|
|
28
|
+
reflector;
|
|
29
|
+
constructor(discovery, scanner, reflector) {
|
|
30
|
+
this.discovery = discovery;
|
|
31
|
+
this.scanner = scanner;
|
|
32
|
+
this.reflector = reflector;
|
|
33
|
+
}
|
|
34
|
+
scan(moduleKey) {
|
|
35
|
+
const operations = [];
|
|
36
|
+
for (const wrapper of this.discovery.getControllers()) {
|
|
37
|
+
const { instance, metatype } = wrapper;
|
|
38
|
+
if (!instance || !metatype)
|
|
39
|
+
continue;
|
|
40
|
+
const controllerName = metatype.name;
|
|
41
|
+
const prototype = Object.getPrototypeOf(instance);
|
|
42
|
+
for (const method of this.scanner.getAllMethodNames(prototype)) {
|
|
43
|
+
const handler = prototype[method];
|
|
44
|
+
// Only real HTTP route handlers carry route metadata (set by @Get/@Post/etc.).
|
|
45
|
+
// Private/helper methods (e.g. buildQuery()) are emitted by getAllMethodNames but
|
|
46
|
+
// carry no PATH_METADATA — skipping them prevents core from stranding them and
|
|
47
|
+
// aborting boot. This matches what the guard actually enforces at runtime, which
|
|
48
|
+
// only ever runs on mapped handlers via context.getHandler().
|
|
49
|
+
if (Reflect.getMetadata(constants_1.PATH_METADATA, handler) === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
// Mirror the guard's precedence: method metadata overrides class-level.
|
|
52
|
+
const isPublic = this.reflector.getAllAndOverride(classification_decorator_js_1.CLASSIFICATION_METADATA.PUBLIC, [
|
|
53
|
+
handler,
|
|
54
|
+
metatype,
|
|
55
|
+
]) === true;
|
|
56
|
+
const raw = this.reflector.getAllAndOverride(classification_decorator_js_1.CLASSIFICATION_METADATA.CLASSIFIED, [handler, metatype]);
|
|
57
|
+
const classification = (0, classification_decorator_js_1.normalizeClassification)(raw);
|
|
58
|
+
// Single-source the key format via core's derivePermissionKey (returns the
|
|
59
|
+
// branded PermissionKey) rather than a hand-built template string.
|
|
60
|
+
const permissionKey = !isPublic && classification
|
|
61
|
+
? (0, core_2.derivePermissionKey)(moduleKey, classification.typeKey, classification.action)
|
|
62
|
+
: undefined;
|
|
63
|
+
operations.push({
|
|
64
|
+
operationId: `${controllerName}#${method}`,
|
|
65
|
+
isPublic,
|
|
66
|
+
...(permissionKey ? { permissionKey } : {}),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { operations };
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
exports.OperationInventoryScanner = OperationInventoryScanner;
|
|
74
|
+
exports.OperationInventoryScanner = OperationInventoryScanner = __decorate([
|
|
75
|
+
(0, common_1.Injectable)(),
|
|
76
|
+
__metadata("design:paramtypes", [core_1.DiscoveryService,
|
|
77
|
+
core_1.MetadataScanner,
|
|
78
|
+
core_1.Reflector])
|
|
79
|
+
], OperationInventoryScanner);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DB seam for the catalog reconciler. The framework defines the interface only;
|
|
3
|
+
* the consumer app supplies the implementation (ADR-0003 — guard does no DB). A row is
|
|
4
|
+
* keyed by its permission key (`moduleKey:typeKey:actionKey`); `status` is the soft
|
|
5
|
+
* active/orphaned marker (never hard-delete).
|
|
6
|
+
*/
|
|
7
|
+
export interface CatalogPermissionRow {
|
|
8
|
+
readonly permissionKey: string;
|
|
9
|
+
readonly moduleKey: string;
|
|
10
|
+
readonly typeKey: string;
|
|
11
|
+
readonly action: string;
|
|
12
|
+
readonly label: string;
|
|
13
|
+
readonly tier: string;
|
|
14
|
+
readonly status: 'active' | 'orphaned';
|
|
15
|
+
}
|
|
16
|
+
/** The result of one mutating reconcile pass — surfaced via /health/catalog + logged. */
|
|
17
|
+
export interface ReconcileReport {
|
|
18
|
+
readonly added: readonly string[];
|
|
19
|
+
readonly orphaned: readonly string[];
|
|
20
|
+
readonly reactivated: readonly string[];
|
|
21
|
+
readonly at: string;
|
|
22
|
+
}
|
|
23
|
+
export interface CatalogStore {
|
|
24
|
+
/** All catalog rows (both active + orphaned). */
|
|
25
|
+
listPermissions(): Promise<CatalogPermissionRow[]>;
|
|
26
|
+
/** Idempotent insert of an ACTIVE row (no-op if the key already exists as active). */
|
|
27
|
+
upsertPermission(row: Omit<CatalogPermissionRow, 'status'>): Promise<boolean>;
|
|
28
|
+
/** Mark keys orphaned (soft). No-op for keys already orphaned. */
|
|
29
|
+
markOrphaned(keys: readonly string[]): Promise<void>;
|
|
30
|
+
/** Flip keys orphaned → active (a route returned). */
|
|
31
|
+
markActive(keys: readonly string[]): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=catalog-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-store.d.ts","sourceRoot":"","sources":["../../../src/completeness/ports/catalog-store.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAC;CACxC;AAED,yFAAyF;AACzF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,eAAe,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAC;IACnD,sFAAsF;IACtF,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,oBAAoB,EAAE,QAAQ,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9E,kEAAkE;IAClE,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,sDAAsD;IACtD,UAAU,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD"}
|
|
@@ -42,7 +42,7 @@ export interface AppConfig {
|
|
|
42
42
|
*/
|
|
43
43
|
readonly redis: RedisConfig;
|
|
44
44
|
/**
|
|
45
|
-
* The keyed-HMAC secret for the refresh-token hasher (`
|
|
45
|
+
* The keyed-HMAC secret for the refresh-token hasher (`REFRESH_HMAC_KEY`).
|
|
46
46
|
* ⛔ REQUIRED FOR AUTH — this is a SECRET (a leaked DB would be offline-brute-
|
|
47
47
|
* forceable without it). {@link loadAppConfig} reads it but does NOT throw when
|
|
48
48
|
* blank, so the ENFORCEMENT-ONLY scaffold (example-backend / a freshly-scaffolded
|
|
@@ -146,12 +146,13 @@ export declare const SCHEMA_SVC_POOL_HARD_CAP = 2;
|
|
|
146
146
|
/**
|
|
147
147
|
* Build the {@link PoolBudgetConfig} from env with documented defaults.
|
|
148
148
|
* Field ↔ env name:
|
|
149
|
-
* workers ←
|
|
150
|
-
* maxConnections ←
|
|
151
|
-
* headroom ←
|
|
152
|
-
* perAccountMax.shield_core ←
|
|
153
|
-
* perAccountMax.shield_app_crud ←
|
|
154
|
-
* perAccountMax.shield_schema_svc ←
|
|
149
|
+
* workers ← POOL_WORKERS
|
|
150
|
+
* maxConnections ← DB_MAX_CONNECTIONS
|
|
151
|
+
* headroom ← POOL_HEADROOM
|
|
152
|
+
* perAccountMax.shield_core ← POOL_MAX_CORE
|
|
153
|
+
* perAccountMax.shield_app_crud ← POOL_MAX_APP_CRUD
|
|
154
|
+
* perAccountMax.shield_schema_svc ← POOL_MAX_SCHEMA_SVC
|
|
155
|
+
* (Legacy `SHIELD_`-prefixed names are still read as a fallback — see {@link readEnv}.)
|
|
155
156
|
*/
|
|
156
157
|
export declare function loadPoolBudget(env?: NodeJS.ProcessEnv): PoolBudgetConfig;
|
|
157
158
|
/** Documented defaults for the runtime MariaDB descriptor (local docker substrate). */
|
|
@@ -161,9 +162,9 @@ export declare const DEFAULT_REDIS_URL = "redis://localhost:6379";
|
|
|
161
162
|
/**
|
|
162
163
|
* Build the runtime {@link DbConfig} from env with documented defaults.
|
|
163
164
|
* Field ↔ env name:
|
|
164
|
-
* host ←
|
|
165
|
-
* user ←
|
|
166
|
-
* database ←
|
|
165
|
+
* host ← DB_HOST port ← DB_PORT
|
|
166
|
+
* user ← DB_USER password ← DB_PASSWORD (secret)
|
|
167
|
+
* database ← DB_NAME (legacy SHIELD_DB_* still read as a fallback)
|
|
167
168
|
* The password defaults to '' when absent — a live connection then fails at
|
|
168
169
|
* connect time (loud), which is correct for a runtime that needs real creds; the
|
|
169
170
|
* secret is deliberately NOT hard-required at config load (unlike the HMAC key,
|
|
@@ -171,9 +172,9 @@ export declare const DEFAULT_REDIS_URL = "redis://localhost:6379";
|
|
|
171
172
|
*/
|
|
172
173
|
export declare function loadDbConfig(env?: NodeJS.ProcessEnv): DbConfig;
|
|
173
174
|
/**
|
|
174
|
-
* Build the {@link RedisConfig} from env. Prefers `
|
|
175
|
-
* to composing `redis://host:port` from `
|
|
176
|
-
*
|
|
175
|
+
* Build the {@link RedisConfig} from env. Prefers `REDIS_URL`; falls back
|
|
176
|
+
* to composing `redis://host:port` from `REDIS_HOST`/`REDIS_PORT`, else the
|
|
177
|
+
* documented default. (Legacy `SHIELD_REDIS_*` names still read as a fallback.)
|
|
177
178
|
*/
|
|
178
179
|
export declare function loadRedisConfig(env?: NodeJS.ProcessEnv): RedisConfig;
|
|
179
180
|
/**
|
|
@@ -182,9 +183,10 @@ export declare function loadRedisConfig(env?: NodeJS.ProcessEnv): RedisConfig;
|
|
|
182
183
|
* core's `resolveLifetimes` job (throws LOUD at composition) — this loader only
|
|
183
184
|
* reads the numbers.
|
|
184
185
|
* Field ↔ env name:
|
|
185
|
-
* accessTtlMs ←
|
|
186
|
-
* refreshIdleMs ←
|
|
187
|
-
* refreshAbsoluteCapMs ←
|
|
186
|
+
* accessTtlMs ← ACCESS_TTL_MS
|
|
187
|
+
* refreshIdleMs ← REFRESH_IDLE_MS
|
|
188
|
+
* refreshAbsoluteCapMs ← REFRESH_ABSOLUTE_CAP_MS
|
|
189
|
+
* (Legacy `SHIELD_`-prefixed names still read as a fallback.)
|
|
188
190
|
*/
|
|
189
191
|
export declare function loadSessionLifetimes(env?: NodeJS.ProcessEnv): SessionLifetimeOverrides;
|
|
190
192
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,SAAS;IACxB,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,WAAW,EAAE,aAAa,GAAG,MAAM,GAAG,YAAY,CAAC;IAC5D,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAGhC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,wBAAwB,CAAC;CAC9C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,gEAAgE;IAChE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,yDAAyD;IACzD,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CACxC;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC;CAC7C;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8CAA8C;IAC9C,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,wEAAwE;IACxE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACpC;AAED,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,EAAE,gBASjC,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAwB1C
|
|
1
|
+
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,SAAS;IACxB,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,WAAW,EAAE,aAAa,GAAG,MAAM,GAAG,YAAY,CAAC;IAC5D,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAGhC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,wBAAwB,CAAC;CAC9C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,gEAAgE;IAChE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,yDAAyD;IACzD,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CACxC;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC;CAC7C;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8CAA8C;IAC9C,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,wEAAwE;IACxE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACpC;AAED,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,EAAE,gBASjC,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAwB1C;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,gBAAgB,CAuBrF;AAQD,uFAAuF;AACvF,eAAO,MAAM,iBAAiB,EAAE,IAAI,CAAC,QAAQ,EAAE,UAAU,CAKxD,CAAC;AAEF,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,2BAA2B,CAAC;AAoD1D;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,QAAQ,CAQ3E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,WAAW,CAajF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,wBAAwB,CAa1B;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAE/E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAUxD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,SAAS,CAU7E;AAMD,mFAAmF;AACnF,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,+EAA+E;IAC/E,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,0EAA0E;IAC1E,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,0EAA0E;IAC1E,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;CAC1C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,gBAAgB,GAAG,oBAAoB,CAgB9E"}
|
|
@@ -47,22 +47,23 @@ function parsePositiveInt(raw, fallback) {
|
|
|
47
47
|
/**
|
|
48
48
|
* Build the {@link PoolBudgetConfig} from env with documented defaults.
|
|
49
49
|
* Field ↔ env name:
|
|
50
|
-
* workers ←
|
|
51
|
-
* maxConnections ←
|
|
52
|
-
* headroom ←
|
|
53
|
-
* perAccountMax.shield_core ←
|
|
54
|
-
* perAccountMax.shield_app_crud ←
|
|
55
|
-
* perAccountMax.shield_schema_svc ←
|
|
50
|
+
* workers ← POOL_WORKERS
|
|
51
|
+
* maxConnections ← DB_MAX_CONNECTIONS
|
|
52
|
+
* headroom ← POOL_HEADROOM
|
|
53
|
+
* perAccountMax.shield_core ← POOL_MAX_CORE
|
|
54
|
+
* perAccountMax.shield_app_crud ← POOL_MAX_APP_CRUD
|
|
55
|
+
* perAccountMax.shield_schema_svc ← POOL_MAX_SCHEMA_SVC
|
|
56
|
+
* (Legacy `SHIELD_`-prefixed names are still read as a fallback — see {@link readEnv}.)
|
|
56
57
|
*/
|
|
57
58
|
function loadPoolBudget(env = process.env) {
|
|
58
59
|
return {
|
|
59
|
-
workers: parsePositiveInt(env
|
|
60
|
-
maxConnections: parsePositiveInt(env
|
|
61
|
-
headroom: parsePositiveInt(env
|
|
60
|
+
workers: parsePositiveInt(readEnv(env, 'POOL_WORKERS'), exports.DEFAULT_POOL_BUDGET.workers),
|
|
61
|
+
maxConnections: parsePositiveInt(readEnv(env, 'DB_MAX_CONNECTIONS'), exports.DEFAULT_POOL_BUDGET.maxConnections),
|
|
62
|
+
headroom: parsePositiveInt(readEnv(env, 'POOL_HEADROOM'), exports.DEFAULT_POOL_BUDGET.headroom),
|
|
62
63
|
perAccountMax: {
|
|
63
|
-
shield_core: parsePositiveInt(env
|
|
64
|
-
shield_app_crud: parsePositiveInt(env
|
|
65
|
-
shield_schema_svc: parsePositiveInt(env
|
|
64
|
+
shield_core: parsePositiveInt(readEnv(env, 'POOL_MAX_CORE'), exports.DEFAULT_POOL_BUDGET.perAccountMax.shield_core),
|
|
65
|
+
shield_app_crud: parsePositiveInt(readEnv(env, 'POOL_MAX_APP_CRUD'), exports.DEFAULT_POOL_BUDGET.perAccountMax.shield_app_crud),
|
|
66
|
+
shield_schema_svc: parsePositiveInt(readEnv(env, 'POOL_MAX_SCHEMA_SVC'), exports.DEFAULT_POOL_BUDGET.perAccountMax.shield_schema_svc),
|
|
66
67
|
},
|
|
67
68
|
};
|
|
68
69
|
}
|
|
@@ -80,6 +81,36 @@ exports.DEFAULT_DB_CONFIG = {
|
|
|
80
81
|
};
|
|
81
82
|
/** Documented default Redis url (local docker substrate). */
|
|
82
83
|
exports.DEFAULT_REDIS_URL = 'redis://localhost:6379';
|
|
84
|
+
/**
|
|
85
|
+
* Read a consumer-facing env var by its CURRENT (unprefixed) name, falling back to
|
|
86
|
+
* the LEGACY `SHIELD_`-prefixed name for one deprecation window. The framework's
|
|
87
|
+
* public config vars were debranded from `SHIELD_*` to bare names (e.g.
|
|
88
|
+
* `SHIELD_DB_HOST` → `DB_HOST`); reading both keeps existing `.env` files (which may
|
|
89
|
+
* still set `SHIELD_*`) working unchanged. The new name wins when both are set.
|
|
90
|
+
* A non-blank legacy value emits a one-time deprecation warning (never logs secrets —
|
|
91
|
+
* only the var NAME). Internal DI-token symbols (`Symbol('SHIELD_...')`) are unrelated
|
|
92
|
+
* and unchanged.
|
|
93
|
+
*/
|
|
94
|
+
const legacyEnvWarned = new Set();
|
|
95
|
+
function readEnv(env, name) {
|
|
96
|
+
const current = env[name];
|
|
97
|
+
if (current !== undefined && current.trim() !== '')
|
|
98
|
+
return current;
|
|
99
|
+
const legacyName = `SHIELD_${name}`;
|
|
100
|
+
const legacy = env[legacyName];
|
|
101
|
+
if (legacy !== undefined && legacy.trim() !== '') {
|
|
102
|
+
if (!legacyEnvWarned.has(legacyName)) {
|
|
103
|
+
legacyEnvWarned.add(legacyName);
|
|
104
|
+
// eslint-disable-next-line no-console
|
|
105
|
+
console.warn(`[astram] env var '${legacyName}' is deprecated — rename it to '${name}'. ` +
|
|
106
|
+
`The SHIELD_ prefix will be removed in a future release.`);
|
|
107
|
+
}
|
|
108
|
+
return legacy;
|
|
109
|
+
}
|
|
110
|
+
// Return whichever was present-but-blank (so parseString applies its own fallback),
|
|
111
|
+
// preferring the current name.
|
|
112
|
+
return current ?? legacy;
|
|
113
|
+
}
|
|
83
114
|
/** Read a string from env with a fallback (trims; blank ⇒ fallback). */
|
|
84
115
|
function parseString(raw, fallback) {
|
|
85
116
|
if (raw === undefined || raw.trim() === '')
|
|
@@ -101,9 +132,9 @@ function parseOptionalPositiveInt(raw) {
|
|
|
101
132
|
/**
|
|
102
133
|
* Build the runtime {@link DbConfig} from env with documented defaults.
|
|
103
134
|
* Field ↔ env name:
|
|
104
|
-
* host ←
|
|
105
|
-
* user ←
|
|
106
|
-
* database ←
|
|
135
|
+
* host ← DB_HOST port ← DB_PORT
|
|
136
|
+
* user ← DB_USER password ← DB_PASSWORD (secret)
|
|
137
|
+
* database ← DB_NAME (legacy SHIELD_DB_* still read as a fallback)
|
|
107
138
|
* The password defaults to '' when absent — a live connection then fails at
|
|
108
139
|
* connect time (loud), which is correct for a runtime that needs real creds; the
|
|
109
140
|
* secret is deliberately NOT hard-required at config load (unlike the HMAC key,
|
|
@@ -111,25 +142,25 @@ function parseOptionalPositiveInt(raw) {
|
|
|
111
142
|
*/
|
|
112
143
|
function loadDbConfig(env = process.env) {
|
|
113
144
|
return {
|
|
114
|
-
host: parseString(env
|
|
115
|
-
port: parsePort(env
|
|
116
|
-
user: parseString(env
|
|
117
|
-
password: parseString(env
|
|
118
|
-
database: parseString(env
|
|
145
|
+
host: parseString(readEnv(env, 'DB_HOST'), exports.DEFAULT_DB_CONFIG.host),
|
|
146
|
+
port: parsePort(readEnv(env, 'DB_PORT'), exports.DEFAULT_DB_CONFIG.port),
|
|
147
|
+
user: parseString(readEnv(env, 'DB_USER'), exports.DEFAULT_DB_CONFIG.user),
|
|
148
|
+
password: parseString(readEnv(env, 'DB_PASSWORD'), ''),
|
|
149
|
+
database: parseString(readEnv(env, 'DB_NAME'), exports.DEFAULT_DB_CONFIG.database),
|
|
119
150
|
};
|
|
120
151
|
}
|
|
121
152
|
/**
|
|
122
|
-
* Build the {@link RedisConfig} from env. Prefers `
|
|
123
|
-
* to composing `redis://host:port` from `
|
|
124
|
-
*
|
|
153
|
+
* Build the {@link RedisConfig} from env. Prefers `REDIS_URL`; falls back
|
|
154
|
+
* to composing `redis://host:port` from `REDIS_HOST`/`REDIS_PORT`, else the
|
|
155
|
+
* documented default. (Legacy `SHIELD_REDIS_*` names still read as a fallback.)
|
|
125
156
|
*/
|
|
126
157
|
function loadRedisConfig(env = process.env) {
|
|
127
|
-
const explicit = env
|
|
158
|
+
const explicit = readEnv(env, 'REDIS_URL');
|
|
128
159
|
if (explicit !== undefined && explicit.trim() !== '') {
|
|
129
160
|
return { url: explicit.trim() };
|
|
130
161
|
}
|
|
131
|
-
const host = env
|
|
132
|
-
const port = env
|
|
162
|
+
const host = readEnv(env, 'REDIS_HOST');
|
|
163
|
+
const port = readEnv(env, 'REDIS_PORT');
|
|
133
164
|
if ((host !== undefined && host.trim() !== '') || (port !== undefined && port.trim() !== '')) {
|
|
134
165
|
const h = parseString(host, 'localhost');
|
|
135
166
|
const p = parsePort(port, 6379);
|
|
@@ -143,15 +174,16 @@ function loadRedisConfig(env = process.env) {
|
|
|
143
174
|
* core's `resolveLifetimes` job (throws LOUD at composition) — this loader only
|
|
144
175
|
* reads the numbers.
|
|
145
176
|
* Field ↔ env name:
|
|
146
|
-
* accessTtlMs ←
|
|
147
|
-
* refreshIdleMs ←
|
|
148
|
-
* refreshAbsoluteCapMs ←
|
|
177
|
+
* accessTtlMs ← ACCESS_TTL_MS
|
|
178
|
+
* refreshIdleMs ← REFRESH_IDLE_MS
|
|
179
|
+
* refreshAbsoluteCapMs ← REFRESH_ABSOLUTE_CAP_MS
|
|
180
|
+
* (Legacy `SHIELD_`-prefixed names still read as a fallback.)
|
|
149
181
|
*/
|
|
150
182
|
function loadSessionLifetimes(env = process.env) {
|
|
151
183
|
const overrides = {};
|
|
152
|
-
const access = parseOptionalPositiveInt(env
|
|
153
|
-
const idle = parseOptionalPositiveInt(env
|
|
154
|
-
const cap = parseOptionalPositiveInt(env
|
|
184
|
+
const access = parseOptionalPositiveInt(readEnv(env, 'ACCESS_TTL_MS'));
|
|
185
|
+
const idle = parseOptionalPositiveInt(readEnv(env, 'REFRESH_IDLE_MS'));
|
|
186
|
+
const cap = parseOptionalPositiveInt(readEnv(env, 'REFRESH_ABSOLUTE_CAP_MS'));
|
|
155
187
|
if (access !== undefined)
|
|
156
188
|
overrides.accessTtlMs = access;
|
|
157
189
|
if (idle !== undefined)
|
|
@@ -167,7 +199,7 @@ function loadSessionLifetimes(env = process.env) {
|
|
|
167
199
|
* the key's value.
|
|
168
200
|
*/
|
|
169
201
|
function loadRefreshHmacKey(env = process.env) {
|
|
170
|
-
return parseString(env
|
|
202
|
+
return parseString(readEnv(env, 'REFRESH_HMAC_KEY'), '');
|
|
171
203
|
}
|
|
172
204
|
/**
|
|
173
205
|
* ⛔ FAIL-CLOSED GATE for the refresh-token HMAC key. Called by the auth
|
|
@@ -186,9 +218,10 @@ function loadRefreshHmacKey(env = process.env) {
|
|
|
186
218
|
*/
|
|
187
219
|
function assertRefreshHmacKey(key) {
|
|
188
220
|
if (typeof key !== 'string' || key.trim() === '') {
|
|
189
|
-
throw new Error('
|
|
221
|
+
throw new Error('Astram auth composition: REFRESH_HMAC_KEY is required and must be ' +
|
|
190
222
|
'non-empty (fail-closed — refusing to compose auth with a blank refresh-token ' +
|
|
191
|
-
'HMAC key). Set it from your secrets manager; never commit the value.'
|
|
223
|
+
'HMAC key). Set it from your secrets manager; never commit the value. ' +
|
|
224
|
+
'(The legacy env var name SHIELD_REFRESH_HMAC_KEY is still accepted.)');
|
|
192
225
|
}
|
|
193
226
|
return key;
|
|
194
227
|
}
|