@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
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
2
18
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
19
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
20
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
21
|
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
22
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
23
|
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
8
41
|
var ShieldModule_1;
|
|
9
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
43
|
exports.ShieldModule = exports.SHIELD_DECISION_AUTHORITY = void 0;
|
|
11
44
|
const common_1 = require("@nestjs/common");
|
|
45
|
+
const node_url_1 = require("node:url");
|
|
12
46
|
const core_seam_js_1 = require("../enforcement/core-seam.js");
|
|
13
47
|
const enforcement_module_js_1 = require("../enforcement/enforcement.module.js");
|
|
14
48
|
const boot_module_js_1 = require("../boot/boot.module.js");
|
|
@@ -21,14 +55,76 @@ const app_config_js_1 = require("../config/app-config.js");
|
|
|
21
55
|
// the enforcement-only path never INSTANTIATES it (no live pool/Redis, no HMAC
|
|
22
56
|
// gate, no core load).
|
|
23
57
|
const auth_module_js_1 = require("../auth/auth.module.js");
|
|
58
|
+
// The `SHIELD_DECISION_AUTHORITY` token now lives in `./shield-tokens.js` so the
|
|
59
|
+
// enforcement guard can import it without a cycle (guard → token → ShieldModule →
|
|
60
|
+
// EnforcementModule → guard). Re-exported here to keep the public import path stable.
|
|
61
|
+
const shield_tokens_js_1 = require("./shield-tokens.js");
|
|
62
|
+
Object.defineProperty(exports, "SHIELD_DECISION_AUTHORITY", { enumerable: true, get: function () { return shield_tokens_js_1.SHIELD_DECISION_AUTHORITY; } });
|
|
63
|
+
/**
|
|
64
|
+
* A `Function`-wrapped dynamic `import()` that survives the CommonJS **build** — the same
|
|
65
|
+
* shim as auth.module.ts's `evalImport`. Under `module: commonjs`, `tsc` would rewrite a
|
|
66
|
+
* literal `import()` into `require(...)`; routing through the `Function` constructor keeps
|
|
67
|
+
* a genuine native `import()` in the emitted dist so this file's OWN compiled output stays
|
|
68
|
+
* free of a top-level `require('../authz/wire-record-authorization.js')` (→ core). We pass
|
|
69
|
+
* the file's absolute URL (resolved off `__dirname` in the CJS dist / `import.meta.url`
|
|
70
|
+
* would not exist here) so the eval'd import has a base to resolve against.
|
|
71
|
+
*/
|
|
72
|
+
const evalImport = new Function('specifier', 'return import(specifier);');
|
|
73
|
+
let cachedRecordAuthApi;
|
|
74
|
+
/**
|
|
75
|
+
* Lazily load the framework's record-authorization builder. Invoked ONLY inside the
|
|
76
|
+
* `SHIELD_DECISION_AUTHORITY` async factory when a consumer supplies
|
|
77
|
+
* `recordAuthorization` — NEVER at module top-level. The target file value-imports
|
|
78
|
+
* `@astram/core` + the four adapter classes, so deferring its load keeps a runtime
|
|
79
|
+
* `require('@astram/core')` OUT of THIS module's (the enforcement-only) load path.
|
|
80
|
+
*
|
|
81
|
+
* TWO strategies, mirroring auth.module.ts's `loadCore`, so it works in BOTH runtimes:
|
|
82
|
+
* 1. A plain dynamic `import()` — native under vitest/SWC (source path).
|
|
83
|
+
* 2. On failure (the tsc-built CJS dist downlevels #1 to a throwing `require` when the
|
|
84
|
+
* transitive `@astram/core` ESM cannot be `require`d in that context), the
|
|
85
|
+
* {@link evalImport} `Function`-wrapped native `import()` resolved off `__dirname`.
|
|
86
|
+
*/
|
|
87
|
+
async function loadBuildRecordAuthorization() {
|
|
88
|
+
if (cachedRecordAuthApi === undefined) {
|
|
89
|
+
try {
|
|
90
|
+
cachedRecordAuthApi = (await Promise.resolve().then(() => __importStar(require('../authz/wire-record-authorization.js'))));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Built-dist fallback (the CJS dist path — see evalImport). `__dirname` is defined
|
|
94
|
+
// in the emitted CJS module; resolve the compiled sibling relative to it and import
|
|
95
|
+
// it as a file:// URL so Node's CJS→ESM interop loads it (and its ESM-only core dep).
|
|
96
|
+
const base = (0, node_url_1.pathToFileURL)(`${__dirname}/../authz/wire-record-authorization.js`).href;
|
|
97
|
+
cachedRecordAuthApi = (await evalImport(base));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return cachedRecordAuthApi.buildRecordAuthorization;
|
|
101
|
+
}
|
|
24
102
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* framework
|
|
29
|
-
*
|
|
103
|
+
* Resolve the `SHIELD_DECISION_AUTHORITY` VALUE from the caller's options (the ONE seam,
|
|
104
|
+
* ADR-0023 — only how its value is PRODUCED changes here). Precedence + mutual exclusion:
|
|
105
|
+
*
|
|
106
|
+
* - `recordAuthorization` PRESENT → the framework builds the REAL wired authority itself
|
|
107
|
+
* (the four record-tier adapters constructed inside guard, un-overridable). This is the
|
|
108
|
+
* Phase-5 path: the app can no longer supply or weaken those adapters.
|
|
109
|
+
* - `decisionAuthority` PRESENT (legacy passthrough) → bind the caller's pre-built
|
|
110
|
+
* authority verbatim (kept for other consumers / back-compat).
|
|
111
|
+
* - BOTH present → MISCONFIG: throw a clear error (they are mutually exclusive).
|
|
112
|
+
* - NEITHER → the fail-closed {@link stubDecisionAuthority} (unchanged fallback).
|
|
113
|
+
*
|
|
114
|
+
* Async because the `recordAuthorization` path lazy-loads the (core-touching) builder.
|
|
30
115
|
*/
|
|
31
|
-
|
|
116
|
+
async function resolveDecisionAuthority(options) {
|
|
117
|
+
if (options.recordAuthorization && options.decisionAuthority) {
|
|
118
|
+
throw new Error('ShieldModule: `recordAuthorization` and `decisionAuthority` are mutually exclusive — ' +
|
|
119
|
+
'supply the record-authorization inputs (pool + app ports + registry) so the framework ' +
|
|
120
|
+
'builds the decision authority, OR a pre-built `decisionAuthority`, but not both.');
|
|
121
|
+
}
|
|
122
|
+
if (options.recordAuthorization) {
|
|
123
|
+
const build = await loadBuildRecordAuthorization();
|
|
124
|
+
return build(options.recordAuthorization);
|
|
125
|
+
}
|
|
126
|
+
return options.decisionAuthority ?? core_seam_js_1.stubDecisionAuthority;
|
|
127
|
+
}
|
|
32
128
|
/**
|
|
33
129
|
* `ShieldModule` — the SINGLE registration seam (ADR-0023 §4).
|
|
34
130
|
*
|
|
@@ -68,13 +164,34 @@ let ShieldModule = ShieldModule_1 = class ShieldModule {
|
|
|
68
164
|
// irrelevant (no AuthModule composes) — a documented no-op.
|
|
69
165
|
const authModule = options.auth
|
|
70
166
|
? auth_module_js_1.AuthModule.forRoot(
|
|
71
|
-
// Only pass
|
|
167
|
+
// Only pass a property when the consumer supplied one — under
|
|
72
168
|
// `exactOptionalPropertyTypes`, an explicit `undefined` is not assignable
|
|
73
|
-
// to
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
169
|
+
// to an optional field; a spread keeps it truly absent so AuthModule falls
|
|
170
|
+
// back to its defaults (MariaDb credential store; 501 /auth/me).
|
|
171
|
+
{
|
|
172
|
+
...(options.credentialResolver
|
|
173
|
+
? { credentialResolver: options.credentialResolver }
|
|
174
|
+
: {}),
|
|
175
|
+
...(options.profileResolver
|
|
176
|
+
? { profileResolver: options.profileResolver }
|
|
177
|
+
: {}),
|
|
178
|
+
// The account/credential-surface seams — same spread-when-present rule
|
|
179
|
+
// (an explicit `undefined` is not assignable under
|
|
180
|
+
// exactOptionalPropertyTypes; absent → AuthModule binds `null` → 501).
|
|
181
|
+
...(options.credentialWriter ? { credentialWriter: options.credentialWriter } : {}),
|
|
182
|
+
...(options.resetTokenStore ? { resetTokenStore: options.resetTokenStore } : {}),
|
|
183
|
+
...(options.emailSender ? { emailSender: options.emailSender } : {}),
|
|
184
|
+
...(options.recoveryCodeStore ? { recoveryCodeStore: options.recoveryCodeStore } : {}),
|
|
185
|
+
...(options.profileWriter ? { profileWriter: options.profileWriter } : {}),
|
|
186
|
+
...(options.sessionSweeper ? { sessionSweeper: options.sessionSweeper } : {}),
|
|
187
|
+
...(options.auditSink ? { auditSink: options.auditSink } : {}),
|
|
188
|
+
...(options.mfaDeviceStore ? { mfaDeviceStore: options.mfaDeviceStore } : {}),
|
|
189
|
+
...(options.totpVerifier ? { totpVerifier: options.totpVerifier } : {}),
|
|
190
|
+
...(options.rootIds ? { rootIds: options.rootIds } : {}),
|
|
191
|
+
...(options.sessionFactsProjector
|
|
192
|
+
? { sessionFactsProjector: options.sessionFactsProjector }
|
|
193
|
+
: {}),
|
|
194
|
+
})
|
|
78
195
|
: undefined;
|
|
79
196
|
return {
|
|
80
197
|
module: ShieldModule_1,
|
|
@@ -87,8 +204,19 @@ let ShieldModule = ShieldModule_1 = class ShieldModule {
|
|
|
87
204
|
providers: [
|
|
88
205
|
{ provide: config_module_js_1.APP_CONFIG, useValue: config },
|
|
89
206
|
{
|
|
90
|
-
|
|
91
|
-
|
|
207
|
+
// Async factory: the `recordAuthorization` path lazy-loads the (core-touching)
|
|
208
|
+
// builder — see resolveDecisionAuthority. The legacy `decisionAuthority` +
|
|
209
|
+
// stub paths resolve synchronously but flow through the same one seam. Both
|
|
210
|
+
// supplied → throws (mutual exclusion).
|
|
211
|
+
provide: shield_tokens_js_1.SHIELD_DECISION_AUTHORITY,
|
|
212
|
+
useFactory: () => resolveDecisionAuthority({
|
|
213
|
+
...(options.recordAuthorization
|
|
214
|
+
? { recordAuthorization: options.recordAuthorization }
|
|
215
|
+
: {}),
|
|
216
|
+
...(options.decisionAuthority
|
|
217
|
+
? { decisionAuthority: options.decisionAuthority }
|
|
218
|
+
: {}),
|
|
219
|
+
}),
|
|
92
220
|
},
|
|
93
221
|
],
|
|
94
222
|
// Re-export the composed AuthModule app-wide ONLY when auth is opted in — its
|
|
@@ -96,7 +224,7 @@ let ShieldModule = ShieldModule_1 = class ShieldModule {
|
|
|
96
224
|
// visible to a consumer's controllers/services via the @Global seam.
|
|
97
225
|
exports: [
|
|
98
226
|
config_module_js_1.APP_CONFIG,
|
|
99
|
-
|
|
227
|
+
shield_tokens_js_1.SHIELD_DECISION_AUTHORITY,
|
|
100
228
|
...(authModule ? [authModule] : []),
|
|
101
229
|
],
|
|
102
230
|
};
|
|
@@ -124,8 +252,16 @@ let ShieldModule = ShieldModule_1 = class ShieldModule {
|
|
|
124
252
|
inject: [optionsToken],
|
|
125
253
|
};
|
|
126
254
|
const authorityProvider = {
|
|
127
|
-
provide:
|
|
128
|
-
|
|
255
|
+
provide: shield_tokens_js_1.SHIELD_DECISION_AUTHORITY,
|
|
256
|
+
// Same one-seam resolution as forRoot, over the async-resolved options: the
|
|
257
|
+
// `recordAuthorization` path (the app hands pool + app ports + registry — computed
|
|
258
|
+
// in its useFactory from the injected registrations) has the framework build the
|
|
259
|
+
// real wired authority; `decisionAuthority` is the legacy passthrough; both →
|
|
260
|
+
// throws; neither → stub. Async because the record path lazy-loads the builder.
|
|
261
|
+
useFactory: (opts) => resolveDecisionAuthority({
|
|
262
|
+
...(opts.recordAuthorization ? { recordAuthorization: opts.recordAuthorization } : {}),
|
|
263
|
+
...(opts.decisionAuthority ? { decisionAuthority: opts.decisionAuthority } : {}),
|
|
264
|
+
}),
|
|
129
265
|
inject: [optionsToken],
|
|
130
266
|
};
|
|
131
267
|
// Same one-instance opt-in AuthModule as forRoot (imported + re-exported).
|
|
@@ -133,13 +269,34 @@ let ShieldModule = ShieldModule_1 = class ShieldModule {
|
|
|
133
269
|
// registration-time collaborator, threaded straight through here.
|
|
134
270
|
const authModule = options.auth
|
|
135
271
|
? auth_module_js_1.AuthModule.forRoot(
|
|
136
|
-
// Only pass
|
|
272
|
+
// Only pass a property when the consumer supplied one — under
|
|
137
273
|
// `exactOptionalPropertyTypes`, an explicit `undefined` is not assignable
|
|
138
|
-
// to
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
274
|
+
// to an optional field; a spread keeps it truly absent so AuthModule falls
|
|
275
|
+
// back to its defaults (MariaDb credential store; 501 /auth/me).
|
|
276
|
+
{
|
|
277
|
+
...(options.credentialResolver
|
|
278
|
+
? { credentialResolver: options.credentialResolver }
|
|
279
|
+
: {}),
|
|
280
|
+
...(options.profileResolver
|
|
281
|
+
? { profileResolver: options.profileResolver }
|
|
282
|
+
: {}),
|
|
283
|
+
// The account/credential-surface seams — same spread-when-present rule
|
|
284
|
+
// (an explicit `undefined` is not assignable under
|
|
285
|
+
// exactOptionalPropertyTypes; absent → AuthModule binds `null` → 501).
|
|
286
|
+
...(options.credentialWriter ? { credentialWriter: options.credentialWriter } : {}),
|
|
287
|
+
...(options.resetTokenStore ? { resetTokenStore: options.resetTokenStore } : {}),
|
|
288
|
+
...(options.emailSender ? { emailSender: options.emailSender } : {}),
|
|
289
|
+
...(options.recoveryCodeStore ? { recoveryCodeStore: options.recoveryCodeStore } : {}),
|
|
290
|
+
...(options.profileWriter ? { profileWriter: options.profileWriter } : {}),
|
|
291
|
+
...(options.sessionSweeper ? { sessionSweeper: options.sessionSweeper } : {}),
|
|
292
|
+
...(options.auditSink ? { auditSink: options.auditSink } : {}),
|
|
293
|
+
...(options.mfaDeviceStore ? { mfaDeviceStore: options.mfaDeviceStore } : {}),
|
|
294
|
+
...(options.totpVerifier ? { totpVerifier: options.totpVerifier } : {}),
|
|
295
|
+
...(options.rootIds ? { rootIds: options.rootIds } : {}),
|
|
296
|
+
...(options.sessionFactsProjector
|
|
297
|
+
? { sessionFactsProjector: options.sessionFactsProjector }
|
|
298
|
+
: {}),
|
|
299
|
+
})
|
|
143
300
|
: undefined;
|
|
144
301
|
return {
|
|
145
302
|
module: ShieldModule_1,
|
|
@@ -156,7 +313,7 @@ let ShieldModule = ShieldModule_1 = class ShieldModule {
|
|
|
156
313
|
providers: [asyncOptionsProvider, configProvider, authorityProvider],
|
|
157
314
|
exports: [
|
|
158
315
|
config_module_js_1.APP_CONFIG,
|
|
159
|
-
|
|
316
|
+
shield_tokens_js_1.SHIELD_DECISION_AUTHORITY,
|
|
160
317
|
...(authModule ? [authModule] : []),
|
|
161
318
|
],
|
|
162
319
|
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-principal resolution — extract the bearer access token from an HTTP
|
|
3
|
+
* request and resolve it (via the {@link SessionTokenBinder}) to a `Principal`,
|
|
4
|
+
* fail-closed. This is the request-auth read path the enforcement guard uses for
|
|
5
|
+
* `@Classified` routes before calling `decide()`.
|
|
6
|
+
*
|
|
7
|
+
* The `Principal` is rebuilt from the bound claims. We deliberately do NOT re-run
|
|
8
|
+
* the Core identity broker here (which would apply the root allowlist and could
|
|
9
|
+
* mint a root from an id) — instead we reconstruct a plain, non-root
|
|
10
|
+
* Principal from the exact claims captured at login. `isRoot` is set to
|
|
11
|
+
* `false` on the request path unless the binding explicitly recorded it (it is
|
|
12
|
+
* captured from the login-time Principal, so a genuinely-root login stays
|
|
13
|
+
* root; a normal login can never become one on a later request).
|
|
14
|
+
*/
|
|
15
|
+
import type { Principal } from '@astram/core';
|
|
16
|
+
import type { SessionTokenBinder, BoundPrincipalClaims } from './session-token-binder.js';
|
|
17
|
+
/** Extract a bearer token from an `Authorization: Bearer <token>` header. */
|
|
18
|
+
export declare function extractBearerToken(authorizationHeader: unknown): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Rebuild a `Principal` from bound claims (validated). `isRoot` is read
|
|
21
|
+
* from the binding (captured at login), NEVER inferred — a normal user cannot become
|
|
22
|
+
* a root on the request path.
|
|
23
|
+
*/
|
|
24
|
+
export declare function principalFromClaims(claims: BoundPrincipalClaims): Principal | null;
|
|
25
|
+
/**
|
|
26
|
+
* Resolve an HTTP request's bearer token to a `Principal`, or `null` (fail-closed)
|
|
27
|
+
* when there is no token, no binding, or the binding is malformed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveRequestPrincipal(authorizationHeader: unknown, binder: SessionTokenBinder): Promise<Principal | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve an HTTP request's bearer token to BOTH a `Principal` AND the raw
|
|
32
|
+
* {@link BoundPrincipalClaims}, or `null` (fail-closed) — the same read path as
|
|
33
|
+
* {@link resolveRequestPrincipal}, but it also surfaces the claims the `Principal`
|
|
34
|
+
* projection drops (notably `mustReenroll` and `sessionId`). The enforcement guard
|
|
35
|
+
* uses this so it can read `claims.mustReenroll` to apply the enforced-re-enroll gate
|
|
36
|
+
* while still attaching the resolved `Principal` for `decide()`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveRequestAuth(authorizationHeader: unknown, binder: SessionTokenBinder): Promise<{
|
|
39
|
+
principal: Principal;
|
|
40
|
+
claims: BoundPrincipalClaims;
|
|
41
|
+
} | null>;
|
|
42
|
+
//# sourceMappingURL=request-principal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-principal.d.ts","sourceRoot":"","sources":["../../src/request-auth/request-principal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAK1F,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,mBAAmB,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAK9E;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,oBAAoB,GAAG,SAAS,GAAG,IAAI,CAoBlF;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,mBAAmB,EAAE,OAAO,EAC5B,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAM3B;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,mBAAmB,EAAE,OAAO,EAC5B,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC;IAAE,SAAS,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,GAAG,IAAI,CAAC,CAQxE"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Request-principal resolution — extract the bearer access token from an HTTP
|
|
4
|
+
* request and resolve it (via the {@link SessionTokenBinder}) to a `Principal`,
|
|
5
|
+
* fail-closed. This is the request-auth read path the enforcement guard uses for
|
|
6
|
+
* `@Classified` routes before calling `decide()`.
|
|
7
|
+
*
|
|
8
|
+
* The `Principal` is rebuilt from the bound claims. We deliberately do NOT re-run
|
|
9
|
+
* the Core identity broker here (which would apply the root allowlist and could
|
|
10
|
+
* mint a root from an id) — instead we reconstruct a plain, non-root
|
|
11
|
+
* Principal from the exact claims captured at login. `isRoot` is set to
|
|
12
|
+
* `false` on the request path unless the binding explicitly recorded it (it is
|
|
13
|
+
* captured from the login-time Principal, so a genuinely-root login stays
|
|
14
|
+
* root; a normal login can never become one on a later request).
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.extractBearerToken = extractBearerToken;
|
|
18
|
+
exports.principalFromClaims = principalFromClaims;
|
|
19
|
+
exports.resolveRequestPrincipal = resolveRequestPrincipal;
|
|
20
|
+
exports.resolveRequestAuth = resolveRequestAuth;
|
|
21
|
+
/** Valid `Principal.kind` values (mirror of the Core union; validated at rebuild). */
|
|
22
|
+
const PRINCIPAL_KINDS = new Set(['user', 'role_group', 'service_account']);
|
|
23
|
+
/** Extract a bearer token from an `Authorization: Bearer <token>` header. */
|
|
24
|
+
function extractBearerToken(authorizationHeader) {
|
|
25
|
+
if (typeof authorizationHeader !== 'string')
|
|
26
|
+
return null;
|
|
27
|
+
const m = /^Bearer\s+(.+)$/i.exec(authorizationHeader.trim());
|
|
28
|
+
const token = m?.[1]?.trim();
|
|
29
|
+
return token && token.length > 0 ? token : null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Rebuild a `Principal` from bound claims (validated). `isRoot` is read
|
|
33
|
+
* from the binding (captured at login), NEVER inferred — a normal user cannot become
|
|
34
|
+
* a root on the request path.
|
|
35
|
+
*/
|
|
36
|
+
function principalFromClaims(claims) {
|
|
37
|
+
if (typeof claims.id !== 'string' ||
|
|
38
|
+
claims.id.length === 0 ||
|
|
39
|
+
typeof claims.tenantId !== 'string' ||
|
|
40
|
+
claims.tenantId.length === 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const kind = PRINCIPAL_KINDS.has(claims.kind) ? claims.kind : 'user';
|
|
44
|
+
const roleIds = Array.isArray(claims.roleIds)
|
|
45
|
+
? claims.roleIds.filter((r) => typeof r === 'string')
|
|
46
|
+
: [];
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
kind,
|
|
49
|
+
id: claims.id,
|
|
50
|
+
tenantId: claims.tenantId,
|
|
51
|
+
roleIds: Object.freeze([...roleIds]),
|
|
52
|
+
isRoot: claims.isRoot === true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve an HTTP request's bearer token to a `Principal`, or `null` (fail-closed)
|
|
57
|
+
* when there is no token, no binding, or the binding is malformed.
|
|
58
|
+
*/
|
|
59
|
+
async function resolveRequestPrincipal(authorizationHeader, binder) {
|
|
60
|
+
const token = extractBearerToken(authorizationHeader);
|
|
61
|
+
if (token === null)
|
|
62
|
+
return null;
|
|
63
|
+
const claims = await binder.resolve(token);
|
|
64
|
+
if (claims === null)
|
|
65
|
+
return null;
|
|
66
|
+
return principalFromClaims(claims);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve an HTTP request's bearer token to BOTH a `Principal` AND the raw
|
|
70
|
+
* {@link BoundPrincipalClaims}, or `null` (fail-closed) — the same read path as
|
|
71
|
+
* {@link resolveRequestPrincipal}, but it also surfaces the claims the `Principal`
|
|
72
|
+
* projection drops (notably `mustReenroll` and `sessionId`). The enforcement guard
|
|
73
|
+
* uses this so it can read `claims.mustReenroll` to apply the enforced-re-enroll gate
|
|
74
|
+
* while still attaching the resolved `Principal` for `decide()`.
|
|
75
|
+
*/
|
|
76
|
+
async function resolveRequestAuth(authorizationHeader, binder) {
|
|
77
|
+
const token = extractBearerToken(authorizationHeader);
|
|
78
|
+
if (token === null)
|
|
79
|
+
return null;
|
|
80
|
+
const claims = await binder.resolve(token);
|
|
81
|
+
if (claims === null)
|
|
82
|
+
return null;
|
|
83
|
+
const principal = principalFromClaims(claims);
|
|
84
|
+
if (principal === null)
|
|
85
|
+
return null;
|
|
86
|
+
return { principal, claims };
|
|
87
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-request **session-facts AsyncLocalStorage** (ADR-0011 hot path) — the
|
|
3
|
+
* SINGLE shared instance the enforcement guard ENTERS and the consumer's
|
|
4
|
+
* `SessionFactsReader` READS. This is the seam that lets gate 5 serve a principal's
|
|
5
|
+
* coarse grants from the request's cached facts instead of a DB reload on every
|
|
6
|
+
* request, WITHOUT touching the frozen 4-arg `decide()` signature or the core
|
|
7
|
+
* `SessionFactsReader` dep contract.
|
|
8
|
+
*
|
|
9
|
+
* ── WHY A FRAMEWORK-EXPORTED SINGLETON (not DI-injected) ─────────────────────────
|
|
10
|
+
* The guard (framework) must ENTER the scope; the reader (app-owned — astram3 swaps
|
|
11
|
+
* `nullSessionFacts` for an ALS-backed one) must READ the SAME scope. An
|
|
12
|
+
* `AsyncLocalStorage` only shares state across `run()`/`getStore()` calls on the
|
|
13
|
+
* SAME instance, so both sides MUST hold one identical object. Exporting ONE module
|
|
14
|
+
* singleton and importing it on both sides is the simplest way to guarantee that —
|
|
15
|
+
* no DI token round-trip, no risk of two divergent instances. It is a pure in-memory
|
|
16
|
+
* request-scoping primitive (no I/O, no secret), so a shared module-level singleton
|
|
17
|
+
* is safe and idiomatic (this is exactly how Nest's own `ClsService`-style helpers
|
|
18
|
+
* and request-context libraries share an ALS).
|
|
19
|
+
*
|
|
20
|
+
* ── CROSS-PRINCIPAL ISOLATION (the #1 danger) ────────────────────────────────────
|
|
21
|
+
* `AsyncLocalStorage.run(store, cb)` binds `store` to the async execution context of
|
|
22
|
+
* `cb` and everything it awaits — and ONLY that context. Two concurrent requests each
|
|
23
|
+
* call `run()` with THEIR OWN store, so `getStore()` inside each returns that request's
|
|
24
|
+
* facts and never the other's. The guard enters the scope with THIS request's
|
|
25
|
+
* `claims.cachedFacts` (same bearer token → same principal → same facts) wrapping the
|
|
26
|
+
* `decide()` call, so the facts a gate reads are always the current request's. The
|
|
27
|
+
* reader holds NO per-request state on `this` — only the ALS handle — so nothing leaks
|
|
28
|
+
* across requests through the reader object itself.
|
|
29
|
+
*
|
|
30
|
+
* ── FAIL-CLOSED ──────────────────────────────────────────────────────────────────
|
|
31
|
+
* Outside any `run()` scope (or when the guard entered with `cachedFacts: null` because
|
|
32
|
+
* the binding carried none / was malformed), `getStore()` is `undefined` / the store's
|
|
33
|
+
* `cachedFacts` is `null`. The reader maps BOTH to `{ cachedFacts: null,
|
|
34
|
+
* permissionsVersion: 0 }`, so gate 5 falls back to the authoritative DB reload
|
|
35
|
+
* (`PermissionFactsPort.loadCoarseGrants`). A cache miss NEVER fabricates a permit.
|
|
36
|
+
*/
|
|
37
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
38
|
+
import type { CoarseGrants } from '@astram/core';
|
|
39
|
+
/**
|
|
40
|
+
* The per-request session-facts store carried through the ALS. `cachedFacts` is the
|
|
41
|
+
* principal's coarse grants for THIS request (or `null` when none were projected →
|
|
42
|
+
* gate 5 DB-reloads); `permissionsVersion` is the version those facts were projected
|
|
43
|
+
* at (0 when absent). Mirrors the core `SessionFactsReader` shape 1:1.
|
|
44
|
+
*/
|
|
45
|
+
export interface SessionFactsStore {
|
|
46
|
+
/** THIS request's coarse grants (ADR-0011), or `null` → gate 5 reloads via the port. */
|
|
47
|
+
readonly cachedFacts: CoarseGrants | null;
|
|
48
|
+
/** The version `cachedFacts` was projected at (0 when absent). */
|
|
49
|
+
readonly permissionsVersion: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The ONE shared session-facts `AsyncLocalStorage` (see the file header). The
|
|
53
|
+
* enforcement guard imports this to `run()` the `decide()` call inside the request's
|
|
54
|
+
* scope; the consumer's ALS-backed `SessionFactsReader` imports the SAME instance to
|
|
55
|
+
* `getStore()`. Exported through the guard barrel so the app reads the identical
|
|
56
|
+
* singleton — never a second, divergent ALS.
|
|
57
|
+
*/
|
|
58
|
+
export declare const SHIELD_SESSION_FACTS_ALS: AsyncLocalStorage<SessionFactsStore>;
|
|
59
|
+
/**
|
|
60
|
+
* The fail-closed "no facts" store — what a reader returns when there is no active
|
|
61
|
+
* scope or the scope carried no facts. Kept here as the single source of the
|
|
62
|
+
* fail-closed shape so the guard and the reader agree byte-for-byte.
|
|
63
|
+
*/
|
|
64
|
+
export declare const NO_SESSION_FACTS: SessionFactsStore;
|
|
65
|
+
//# sourceMappingURL=session-facts-als.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-facts-als.d.ts","sourceRoot":"","sources":["../../src/request-auth/session-facts-als.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAIrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,wFAAwF;IACxF,QAAQ,CAAC,WAAW,EAAE,YAAY,GAAG,IAAI,CAAC;IAC1C,kEAAkE;IAClE,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACrC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,sCAA6C,CAAC;AAEnF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,iBAG7B,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The per-request **session-facts AsyncLocalStorage** (ADR-0011 hot path) — the
|
|
4
|
+
* SINGLE shared instance the enforcement guard ENTERS and the consumer's
|
|
5
|
+
* `SessionFactsReader` READS. This is the seam that lets gate 5 serve a principal's
|
|
6
|
+
* coarse grants from the request's cached facts instead of a DB reload on every
|
|
7
|
+
* request, WITHOUT touching the frozen 4-arg `decide()` signature or the core
|
|
8
|
+
* `SessionFactsReader` dep contract.
|
|
9
|
+
*
|
|
10
|
+
* ── WHY A FRAMEWORK-EXPORTED SINGLETON (not DI-injected) ─────────────────────────
|
|
11
|
+
* The guard (framework) must ENTER the scope; the reader (app-owned — astram3 swaps
|
|
12
|
+
* `nullSessionFacts` for an ALS-backed one) must READ the SAME scope. An
|
|
13
|
+
* `AsyncLocalStorage` only shares state across `run()`/`getStore()` calls on the
|
|
14
|
+
* SAME instance, so both sides MUST hold one identical object. Exporting ONE module
|
|
15
|
+
* singleton and importing it on both sides is the simplest way to guarantee that —
|
|
16
|
+
* no DI token round-trip, no risk of two divergent instances. It is a pure in-memory
|
|
17
|
+
* request-scoping primitive (no I/O, no secret), so a shared module-level singleton
|
|
18
|
+
* is safe and idiomatic (this is exactly how Nest's own `ClsService`-style helpers
|
|
19
|
+
* and request-context libraries share an ALS).
|
|
20
|
+
*
|
|
21
|
+
* ── CROSS-PRINCIPAL ISOLATION (the #1 danger) ────────────────────────────────────
|
|
22
|
+
* `AsyncLocalStorage.run(store, cb)` binds `store` to the async execution context of
|
|
23
|
+
* `cb` and everything it awaits — and ONLY that context. Two concurrent requests each
|
|
24
|
+
* call `run()` with THEIR OWN store, so `getStore()` inside each returns that request's
|
|
25
|
+
* facts and never the other's. The guard enters the scope with THIS request's
|
|
26
|
+
* `claims.cachedFacts` (same bearer token → same principal → same facts) wrapping the
|
|
27
|
+
* `decide()` call, so the facts a gate reads are always the current request's. The
|
|
28
|
+
* reader holds NO per-request state on `this` — only the ALS handle — so nothing leaks
|
|
29
|
+
* across requests through the reader object itself.
|
|
30
|
+
*
|
|
31
|
+
* ── FAIL-CLOSED ──────────────────────────────────────────────────────────────────
|
|
32
|
+
* Outside any `run()` scope (or when the guard entered with `cachedFacts: null` because
|
|
33
|
+
* the binding carried none / was malformed), `getStore()` is `undefined` / the store's
|
|
34
|
+
* `cachedFacts` is `null`. The reader maps BOTH to `{ cachedFacts: null,
|
|
35
|
+
* permissionsVersion: 0 }`, so gate 5 falls back to the authoritative DB reload
|
|
36
|
+
* (`PermissionFactsPort.loadCoarseGrants`). A cache miss NEVER fabricates a permit.
|
|
37
|
+
*/
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.NO_SESSION_FACTS = exports.SHIELD_SESSION_FACTS_ALS = void 0;
|
|
40
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
41
|
+
/**
|
|
42
|
+
* The ONE shared session-facts `AsyncLocalStorage` (see the file header). The
|
|
43
|
+
* enforcement guard imports this to `run()` the `decide()` call inside the request's
|
|
44
|
+
* scope; the consumer's ALS-backed `SessionFactsReader` imports the SAME instance to
|
|
45
|
+
* `getStore()`. Exported through the guard barrel so the app reads the identical
|
|
46
|
+
* singleton — never a second, divergent ALS.
|
|
47
|
+
*/
|
|
48
|
+
exports.SHIELD_SESSION_FACTS_ALS = new node_async_hooks_1.AsyncLocalStorage();
|
|
49
|
+
/**
|
|
50
|
+
* The fail-closed "no facts" store — what a reader returns when there is no active
|
|
51
|
+
* scope or the scope carried no facts. Kept here as the single source of the
|
|
52
|
+
* fail-closed shape so the guard and the reader agree byte-for-byte.
|
|
53
|
+
*/
|
|
54
|
+
exports.NO_SESSION_FACTS = Object.freeze({
|
|
55
|
+
cachedFacts: null,
|
|
56
|
+
permissionsVersion: 0,
|
|
57
|
+
});
|