@astram/guard 0.1.1 → 0.3.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/account.controller.js.map +1 -0
- package/dist/auth/auth.controller.d.ts +316 -0
- package/dist/auth/auth.controller.d.ts.map +1 -0
- package/dist/auth/auth.controller.js +1091 -0
- package/dist/auth/auth.controller.js.map +1 -0
- package/dist/auth/auth.module.d.ts +202 -0
- package/dist/auth/auth.module.d.ts.map +1 -0
- package/dist/auth/auth.module.js +610 -0
- package/dist/auth/auth.module.js.map +1 -0
- package/dist/auth/connections.d.ts +61 -0
- package/dist/auth/connections.d.ts.map +1 -0
- package/dist/auth/connections.js +116 -0
- package/dist/auth/connections.js.map +1 -0
- package/dist/auth/dto.d.ts +154 -0
- package/dist/auth/dto.d.ts.map +1 -0
- package/dist/auth/dto.js +29 -0
- package/dist/auth/dto.js.map +1 -0
- package/dist/auth/pending-mfa-store.d.ts +72 -0
- package/dist/auth/pending-mfa-store.d.ts.map +1 -0
- package/dist/auth/pending-mfa-store.js +106 -0
- package/dist/auth/pending-mfa-store.js.map +1 -0
- package/dist/auth/persistence.module.d.ts +42 -0
- package/dist/auth/persistence.module.d.ts.map +1 -0
- package/dist/auth/persistence.module.js +94 -0
- package/dist/auth/persistence.module.js.map +1 -0
- 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/audit-sink.js.map +1 -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/credential-writer.js.map +1 -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/email-sender.js.map +1 -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/mfa-device-store.js.map +1 -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/profile-writer.js.map +1 -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/recovery-code-store.js.map +1 -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/reset-token-store.js.map +1 -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-facts-projector.js.map +1 -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/ports/session-sweeper.js.map +1 -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/profile-resolver.js.map +1 -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/rate-limit/attempt-limiter.js.map +1 -0
- package/dist/auth/tokens.d.ts +236 -0
- package/dist/auth/tokens.d.ts.map +1 -0
- package/dist/auth/tokens.js +254 -0
- package/dist/auth/tokens.js.map +1 -0
- 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/auth/totp-enrollment.js.map +1 -0
- package/dist/authentication/argon2id-password-hasher.js.map +1 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts +110 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -0
- package/dist/authentication/mariadb-credential-resolver.js +170 -0
- package/dist/authentication/mariadb-credential-resolver.js.map +1 -0
- 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/cross-tenant-grant-writer.js.map +1 -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/manifest-operation-authority-adapter.js.map +1 -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-cross-tenant-grant-adapter.js.map +1 -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-rule-adapter.js.map +1 -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/mariadb-record-share-adapter.js.map +1 -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-cascade.js.map +1 -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/record-share-writer.js.map +1 -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/authz/wire-record-authorization.js.map +1 -0
- package/dist/boot/boot-sequence.js.map +1 -0
- package/dist/boot/boot.module.js.map +1 -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/catalog-reconciler.js.map +1 -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/operation-inventory-scanner.js.map +1 -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/completeness/ports/catalog-store.js.map +1 -0
- package/dist/config/app-config.d.ts +134 -1
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +144 -2
- package/dist/config/app-config.js.map +1 -0
- package/dist/config/config.module.js.map +1 -0
- 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/classification.decorator.js.map +1 -0
- package/dist/enforcement/core-seam.js.map +1 -0
- package/dist/enforcement/enforcement.module.js.map +1 -0
- 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/global-enforcement.guard.js.map +1 -0
- 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/enforcement/reenroll-allowlist.js.map +1 -0
- package/dist/index.d.ts +43 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +198 -1
- package/dist/index.js.map +1 -0
- 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-tokens.js.map +1 -0
- package/dist/module/shield.module.d.ts +203 -8
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +232 -14
- package/dist/module/shield.module.js.map +1 -0
- 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/request-principal.js.map +1 -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-facts-als.js.map +1 -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/session-token-binder.js.map +1 -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/dist/request-auth/tokens.js.map +1 -0
- package/dist/session/crypto-adapters.js.map +1 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/mariadb-session-record-store.js.map +1 -0
- package/dist/session/redis-hot-session-store.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `PersistenceModule` — the ONE place live infra connections are created (spec
|
|
4
|
+
* §4). Provides the live `mariadb` {@link Pool} ({@link SHIELD_DB_POOL}) and the
|
|
5
|
+
* live `ioredis` client ({@link SHIELD_REDIS}) from {@link AppConfig}, with
|
|
6
|
+
* graceful shutdown. Everything auth persists (sessions, credentials, hot state)
|
|
7
|
+
* flows through these two connections.
|
|
8
|
+
*
|
|
9
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
* Team B (connection + composition) OWNS this file. The two factories create the
|
|
11
|
+
* real driver objects; the {@link ConnectionLifecycle} provider (a class Nest
|
|
12
|
+
* invokes `onModuleDestroy` on) closes them on shutdown — a value provider is not
|
|
13
|
+
* itself lifecycle-aware, so the teardown lives in that holder (see
|
|
14
|
+
* `connections.ts`). Both connections are exported so AuthModule can inject the
|
|
15
|
+
* pool (credential resolver + session record store) and the redis (hot store).
|
|
16
|
+
*
|
|
17
|
+
* SHIELD_DB_POOL — {@link createDbPool}: `mariadb.createPool` for the
|
|
18
|
+
* `shield_core` account from `config.db`, `connectionLimit` from the ADR-0018
|
|
19
|
+
* budget (`config.pool.perAccountMax.shield_core`). NOT a duplicate pool knob.
|
|
20
|
+
* SHIELD_REDIS — {@link createRedisClient}: `new Redis(config.redis.url)`
|
|
21
|
+
* (ioredis). The hot tier (ADR-0013).
|
|
22
|
+
*
|
|
23
|
+
* Neither factory reads the HMAC key (that is the session/hasher composition in
|
|
24
|
+
* AuthModule). A blank `db.password` is allowed — it fails LOUD at connect time,
|
|
25
|
+
* which is correct for a runtime credential. Nothing here logs a credential.
|
|
26
|
+
*
|
|
27
|
+
* ⛔ OPT-IN: like AuthModule, this module opens LIVE connections, so it must only
|
|
28
|
+
* be imported when a deployment actually wants auth (AuthModule imports it). An
|
|
29
|
+
* enforcement-only app that never imports AuthModule never opens a pool/redis and
|
|
30
|
+
* keeps the no-`.env` boot. See auth.module.ts for the opt-in mechanism.
|
|
31
|
+
*
|
|
32
|
+
* ADR-0003: the real `mariadb`/`ioredis` live in guard (Core stays zero-dep).
|
|
33
|
+
*/
|
|
34
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
35
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
36
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
37
|
+
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;
|
|
38
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
39
|
+
};
|
|
40
|
+
var PersistenceModule_1;
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.PersistenceModule = void 0;
|
|
43
|
+
const common_1 = require("@nestjs/common");
|
|
44
|
+
const config_module_js_1 = require("../config/config.module.js");
|
|
45
|
+
const tokens_js_1 = require("./tokens.js");
|
|
46
|
+
const connections_js_1 = require("./connections.js");
|
|
47
|
+
/**
|
|
48
|
+
* The live MariaDB pool provider. Creates the ONE `shield_core` pool from config
|
|
49
|
+
* via {@link createDbPool}. `inject: [APP_CONFIG]` so the factory sees the resolved
|
|
50
|
+
* config. Teardown is handled by {@link ConnectionLifecycle}.
|
|
51
|
+
*/
|
|
52
|
+
const dbPoolProvider = {
|
|
53
|
+
provide: tokens_js_1.SHIELD_DB_POOL,
|
|
54
|
+
useFactory: (config) => (0, connections_js_1.createDbPool)(config),
|
|
55
|
+
inject: [config_module_js_1.APP_CONFIG],
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The live Redis client provider. Creates the ONE hot-tier client from
|
|
59
|
+
* `config.redis.url` via {@link createRedisClient}. Teardown via
|
|
60
|
+
* {@link ConnectionLifecycle}.
|
|
61
|
+
*/
|
|
62
|
+
const redisProvider = {
|
|
63
|
+
provide: tokens_js_1.SHIELD_REDIS,
|
|
64
|
+
useFactory: (config) => (0, connections_js_1.createRedisClient)(config),
|
|
65
|
+
inject: [config_module_js_1.APP_CONFIG],
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The lifecycle holder — injects the two connections and closes them on shutdown
|
|
69
|
+
* (`onModuleDestroy`). A value-provider connection gets no lifecycle callback, so
|
|
70
|
+
* this class is where graceful teardown happens.
|
|
71
|
+
*/
|
|
72
|
+
const lifecycleProvider = {
|
|
73
|
+
provide: connections_js_1.ConnectionLifecycle,
|
|
74
|
+
useFactory: (pool, redis) => new connections_js_1.ConnectionLifecycle(pool, redis),
|
|
75
|
+
inject: [tokens_js_1.SHIELD_DB_POOL, tokens_js_1.SHIELD_REDIS],
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The persistence module. `forRoot()` returns the two connection providers (both
|
|
79
|
+
* exported so AuthModule can inject them) plus the shutdown holder. Opt-in: only
|
|
80
|
+
* imported when auth is wired.
|
|
81
|
+
*/
|
|
82
|
+
let PersistenceModule = PersistenceModule_1 = class PersistenceModule {
|
|
83
|
+
static forRoot() {
|
|
84
|
+
return {
|
|
85
|
+
module: PersistenceModule_1,
|
|
86
|
+
providers: [dbPoolProvider, redisProvider, lifecycleProvider],
|
|
87
|
+
exports: [tokens_js_1.SHIELD_DB_POOL, tokens_js_1.SHIELD_REDIS],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
exports.PersistenceModule = PersistenceModule;
|
|
92
|
+
exports.PersistenceModule = PersistenceModule = PersistenceModule_1 = __decorate([
|
|
93
|
+
(0, common_1.Module)({})
|
|
94
|
+
], PersistenceModule);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence.module.js","sourceRoot":"","sources":["../../src/auth/persistence.module.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;;;;;;;;;;AAEH,2CAA2E;AAI3E,iEAAwD;AACxD,2CAA2D;AAC3D,qDAAwF;AAExF;;;;GAIG;AACH,MAAM,cAAc,GAAa;IAC/B,OAAO,EAAE,0BAAc;IACvB,UAAU,EAAE,CAAC,MAAiB,EAAQ,EAAE,CAAC,IAAA,6BAAY,EAAC,MAAM,CAAC;IAC7D,MAAM,EAAE,CAAC,6BAAU,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,MAAM,aAAa,GAAa;IAC9B,OAAO,EAAE,wBAAY;IACrB,UAAU,EAAE,CAAC,MAAiB,EAAS,EAAE,CAAC,IAAA,kCAAiB,EAAC,MAAM,CAAC;IACnE,MAAM,EAAE,CAAC,6BAAU,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,MAAM,iBAAiB,GAAa;IAClC,OAAO,EAAE,oCAAmB;IAC5B,UAAU,EAAE,CAAC,IAAU,EAAE,KAAY,EAAuB,EAAE,CAC5D,IAAI,oCAAmB,CAAC,IAAI,EAAE,KAAK,CAAC;IACtC,MAAM,EAAE,CAAC,0BAAc,EAAE,wBAAY,CAAC;CACvC,CAAC;AAEF;;;;GAIG;AAEI,IAAM,iBAAiB,yBAAvB,MAAM,iBAAiB;IAC5B,MAAM,CAAC,OAAO;QACZ,OAAO;YACL,MAAM,EAAE,mBAAiB;YACzB,SAAS,EAAE,CAAC,cAAc,EAAE,aAAa,EAAE,iBAAiB,CAAC;YAC7D,OAAO,EAAE,CAAC,0BAAc,EAAE,wBAAY,CAAC;SACxC,CAAC;IACJ,CAAC;CACF,CAAA;AARY,8CAAiB;4BAAjB,iBAAiB;IAD7B,IAAA,eAAM,EAAC,EAAE,CAAC;GACE,iBAAiB,CAQ7B"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuditSink — the auth-surface **audit seam** (spec §6.2). The auth surface is
|
|
3
|
+
* otherwise un-audited (only the @astram/engine pipe audits record CRUD); this port
|
|
4
|
+
* introduces the FIRST auth audit event. The framework builds the event; the consumer
|
|
5
|
+
* persists it (astram3 writes `shield_audit.audit_log`).
|
|
6
|
+
*
|
|
7
|
+
* OPTIONAL, consumer-plugged through `ShieldModule.forRoot({ auditSink })` and threaded
|
|
8
|
+
* to `AuthModule.forRoot` exactly like the other account/credential seams. Absent
|
|
9
|
+
* (`null`) → the route still works, just un-audited (the `@Optional()` injection in
|
|
10
|
+
* `AuthController` resolves to no seam → the best-effort call is a no-op).
|
|
11
|
+
*
|
|
12
|
+
* BEST-EFFORT by contract: `record` is invoked fire-and-forget on the recovery-login
|
|
13
|
+
* success path — a throwing or slow sink NEVER fails the (legitimate) login. The
|
|
14
|
+
* consumer's impl should not surface errors into the response path.
|
|
15
|
+
*
|
|
16
|
+
* NO SECRET LEAK: the event carries NO password, recovery code, TOTP secret, or token —
|
|
17
|
+
* only the actor id, tenant, action key, and non-secret attributes.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* A framework-built auth audit event. `type` is the audit category (`'authentication'`
|
|
21
|
+
* for the auth surface); `action` is the specific event key (e.g. `'mfa.recovery_used'`).
|
|
22
|
+
* `attributes` carries non-secret structured detail (never a code/token/secret).
|
|
23
|
+
*/
|
|
24
|
+
export interface AuthAuditEvent {
|
|
25
|
+
/** The audit category. Always `'authentication'` for auth-surface events. */
|
|
26
|
+
readonly type: 'authentication';
|
|
27
|
+
/** When the event occurred (epoch ms). */
|
|
28
|
+
readonly occurredAtMs: number;
|
|
29
|
+
/** The acting principal's id (the account the event is about). */
|
|
30
|
+
readonly actorId: string;
|
|
31
|
+
/** The active tenant/company the event is scoped to, if known. */
|
|
32
|
+
readonly tenantId?: string;
|
|
33
|
+
/** The specific event key, e.g. `'mfa.recovery_used'`. */
|
|
34
|
+
readonly action: string;
|
|
35
|
+
/** Non-secret structured detail. NEVER a code / token / secret. */
|
|
36
|
+
readonly attributes?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The port a consumer implements to persist auth audit events. Supplied to the
|
|
40
|
+
* framework via `ShieldModule.forRoot({ auditSink })`. Best-effort: `record` is called
|
|
41
|
+
* fire-and-forget, so an impl error must not propagate into the login response.
|
|
42
|
+
*/
|
|
43
|
+
export interface AuditSink {
|
|
44
|
+
/** Persist one auth audit event. Best-effort — never fails the caller's flow. */
|
|
45
|
+
record(event: AuthAuditEvent): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=audit-sink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-sink.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/audit-sink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,0CAA0C;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,mEAAmE;IACnE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/C;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,iFAAiF;IACjF,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9C"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AuditSink — the auth-surface **audit seam** (spec §6.2). The auth surface is
|
|
4
|
+
* otherwise un-audited (only the @astram/engine pipe audits record CRUD); this port
|
|
5
|
+
* introduces the FIRST auth audit event. The framework builds the event; the consumer
|
|
6
|
+
* persists it (astram3 writes `shield_audit.audit_log`).
|
|
7
|
+
*
|
|
8
|
+
* OPTIONAL, consumer-plugged through `ShieldModule.forRoot({ auditSink })` and threaded
|
|
9
|
+
* to `AuthModule.forRoot` exactly like the other account/credential seams. Absent
|
|
10
|
+
* (`null`) → the route still works, just un-audited (the `@Optional()` injection in
|
|
11
|
+
* `AuthController` resolves to no seam → the best-effort call is a no-op).
|
|
12
|
+
*
|
|
13
|
+
* BEST-EFFORT by contract: `record` is invoked fire-and-forget on the recovery-login
|
|
14
|
+
* success path — a throwing or slow sink NEVER fails the (legitimate) login. The
|
|
15
|
+
* consumer's impl should not surface errors into the response path.
|
|
16
|
+
*
|
|
17
|
+
* NO SECRET LEAK: the event carries NO password, recovery code, TOTP secret, or token —
|
|
18
|
+
* only the actor id, tenant, action key, and non-secret attributes.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-sink.js","sourceRoot":"","sources":["../../../src/auth/ports/audit-sink.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialWriter — the seam that WRITES a principal's password credential. The
|
|
3
|
+
* framework never owns the credential store; the consumer plugs this (like
|
|
4
|
+
* CredentialResolver, but the write side). Supplied via
|
|
5
|
+
* ShieldModule.forRoot({ credentialWriter }). Absent → the routes that need it
|
|
6
|
+
* return 501. Writes HASHES only — never plaintext.
|
|
7
|
+
*
|
|
8
|
+
* TOTP secrets are NOT written here — the multi-device MFA model stores
|
|
9
|
+
* every authenticator secret in the `MfaDeviceStore` (one storage location, one code
|
|
10
|
+
* path). So this seam is now password-only: `setPassword` + `getPasswordHash`.
|
|
11
|
+
*/
|
|
12
|
+
export interface CredentialWriter {
|
|
13
|
+
/** Set the principal's password to an already-hashed value. */
|
|
14
|
+
setPassword(principalId: string, passwordHash: string): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Read the stored password hash by principal id — needed by change-password, which
|
|
17
|
+
* holds the principal id (from the token), whereas CredentialResolver keys by
|
|
18
|
+
* identityHint. Null if the principal is unknown. (This is the ONE read on the
|
|
19
|
+
* writer seam; see Task 5 for why it lives here rather than a separate seam.)
|
|
20
|
+
*/
|
|
21
|
+
getPasswordHash(principalId: string): Promise<string | null>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=credential-writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-writer.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/credential-writer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+DAA+D;IAC/D,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE;;;;;OAKG;IACH,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC9D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-writer.js","sourceRoot":"","sources":["../../../src/auth/ports/credential-writer.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmailSender — the delivery seam. The framework builds the message; the consumer
|
|
3
|
+
* delivers it (SendGrid, SMTP, or a test capturer). Consumer-plugged.
|
|
4
|
+
*/
|
|
5
|
+
export interface EmailMessage {
|
|
6
|
+
readonly to: string;
|
|
7
|
+
readonly subject: string;
|
|
8
|
+
readonly html: string;
|
|
9
|
+
readonly text: string;
|
|
10
|
+
}
|
|
11
|
+
export interface EmailSender {
|
|
12
|
+
send(message: EmailMessage): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=email-sender.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-sender.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/email-sender.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AACD,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-sender.js","sourceRoot":"","sources":["../../../src/auth/ports/email-sender.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MfaDeviceStore — the seam that persists a principal's TOTP authenticator DEVICES
|
|
3
|
+
* (multi-device MFA). The framework never owns the store; the consumer
|
|
4
|
+
* plugs this (like {@link CredentialWriter}, but for the N-device MFA model).
|
|
5
|
+
* Each device has its OWN base32 secret; login matches ANY confirmed device (the
|
|
6
|
+
* OR-match lives in the consumer resolver + a custom TOTP verifier, NOT here — this
|
|
7
|
+
* port is pure persistence). Supplied via `ShieldModule.forRoot({ mfaDeviceStore })`.
|
|
8
|
+
* Absent (`null`) → the device routes return 501. A device's `secret` is NEVER
|
|
9
|
+
* returned to a client (only {@link listConfirmedSecrets} for the login bundle,
|
|
10
|
+
* server-side) — the display shape {@link MfaDevice} deliberately omits it.
|
|
11
|
+
*/
|
|
12
|
+
/** The DISPLAY shape of one MFA device (NO secret — never leaves the server). */
|
|
13
|
+
export interface MfaDevice {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly createdAtMs: number;
|
|
17
|
+
readonly lastUsedAtMs: number | null;
|
|
18
|
+
readonly confirmed: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface MfaDeviceStore {
|
|
21
|
+
/** All of the principal's devices incl. pending (internal/debug; routes filter to confirmed for display). */
|
|
22
|
+
listAll(principalId: string): Promise<MfaDevice[]>;
|
|
23
|
+
/** Confirmed devices' base32 secrets — for the login requiredFactor bundle + verify. */
|
|
24
|
+
listConfirmedSecrets(principalId: string): Promise<string[]>;
|
|
25
|
+
/**
|
|
26
|
+
* The base32 secret of a SINGLE device the principal owns (confirmed OR pending), or
|
|
27
|
+
* `null` if not found / not owned. Used ONLY server-side by `confirm` to check the
|
|
28
|
+
* presented code against the pending device's own secret — the secret is NEVER
|
|
29
|
+
* returned to a client (the display {@link MfaDevice} deliberately omits it).
|
|
30
|
+
*/
|
|
31
|
+
getSecret(principalId: string, deviceId: string): Promise<string | null>;
|
|
32
|
+
/** Create a PENDING device (own id); prunes the caller's stale pending rows first. Returns the deviceId. */
|
|
33
|
+
begin(principalId: string, name: string, secret: string): Promise<string>;
|
|
34
|
+
/** Activate a pending device (confirmed). False if not found / not owned / not pending. */
|
|
35
|
+
confirm(principalId: string, deviceId: string): Promise<boolean>;
|
|
36
|
+
/** Rename a device. Returns the updated device, or null if not found / not owned. */
|
|
37
|
+
rename(principalId: string, deviceId: string, name: string): Promise<MfaDevice | null>;
|
|
38
|
+
/** Delete a device. False if not found / not owned. */
|
|
39
|
+
remove(principalId: string, deviceId: string): Promise<boolean>;
|
|
40
|
+
/** Count of CONFIRMED devices (drives "first device" / "mfa enabled"). */
|
|
41
|
+
countConfirmed(principalId: string): Promise<number>;
|
|
42
|
+
/** Best-effort: stamp last-used for the device whose secret verified (match by unique secret). */
|
|
43
|
+
stampUsed(secret: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=mfa-device-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mfa-device-store.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/mfa-device-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,6GAA6G;IAC7G,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACnD,wFAAwF;IACxF,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7D;;;;;OAKG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzE,4GAA4G;IAC5G,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1E,2FAA2F;IAC3F,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,qFAAqF;IACrF,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACvF,uDAAuD;IACvD,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChE,0EAA0E;IAC1E,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,kGAAkG;IAClG,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MfaDeviceStore — the seam that persists a principal's TOTP authenticator DEVICES
|
|
4
|
+
* (multi-device MFA). The framework never owns the store; the consumer
|
|
5
|
+
* plugs this (like {@link CredentialWriter}, but for the N-device MFA model).
|
|
6
|
+
* Each device has its OWN base32 secret; login matches ANY confirmed device (the
|
|
7
|
+
* OR-match lives in the consumer resolver + a custom TOTP verifier, NOT here — this
|
|
8
|
+
* port is pure persistence). Supplied via `ShieldModule.forRoot({ mfaDeviceStore })`.
|
|
9
|
+
* Absent (`null`) → the device routes return 501. A device's `secret` is NEVER
|
|
10
|
+
* returned to a client (only {@link listConfirmedSecrets} for the login bundle,
|
|
11
|
+
* server-side) — the display shape {@link MfaDevice} deliberately omits it.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mfa-device-store.js","sourceRoot":"","sources":["../../../src/auth/ports/mfa-device-store.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CurrentUserProfile } from '../profile-resolver.js';
|
|
2
|
+
/**
|
|
3
|
+
* ProfileWriter — self-service profile edit (name/email). Returns the refreshed
|
|
4
|
+
* display profile (names, never UUIDs). Consumer-plugged.
|
|
5
|
+
*/
|
|
6
|
+
export interface ProfileWriter {
|
|
7
|
+
update(principalId: string, changes: {
|
|
8
|
+
name?: string;
|
|
9
|
+
email?: string;
|
|
10
|
+
}): Promise<CurrentUserProfile>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=profile-writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile-writer.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/profile-writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,CACJ,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GACzC,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAChC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile-writer.js","sourceRoot":"","sources":["../../../src/auth/ports/profile-writer.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecoveryCodeStore — persists Argon2-hashed one-time MFA recovery codes.
|
|
3
|
+
* `replaceAll` wipes prior codes on (re-)enroll. `consume` returns true iff a
|
|
4
|
+
* matching unconsumed code exists, then marks it consumed (single-use).
|
|
5
|
+
*/
|
|
6
|
+
export interface RecoveryCodeStore {
|
|
7
|
+
replaceAll(principalId: string, codeHashes: readonly string[]): Promise<void>;
|
|
8
|
+
consume(principalId: string, rawCode: string): Promise<boolean>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=recovery-code-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recovery-code-store.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/recovery-code-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACjE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recovery-code-store.js","sourceRoot":"","sources":["../../../src/auth/ports/recovery-code-store.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResetTokenStore — persists single-use, TTL'd password-reset tokens. The framework
|
|
3
|
+
* hashes the raw token (SHA-256) before it reaches this store; the store never sees
|
|
4
|
+
* the raw token. `consume` returns the principalId iff the token is unexpired AND
|
|
5
|
+
* unconsumed, then marks it consumed (single-use). Consumer-plugged.
|
|
6
|
+
*/
|
|
7
|
+
export interface ResetTokenStore {
|
|
8
|
+
issue(principalId: string, tokenHash: string, expiresAtMs: number): Promise<void>;
|
|
9
|
+
consume(tokenHash: string): Promise<string | null>;
|
|
10
|
+
invalidateAllForPrincipal(principalId: string): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=reset-token-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reset-token-store.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/reset-token-store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClF,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnD,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reset-token-store.js","sourceRoot":"","sources":["../../../src/auth/ports/reset-token-store.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `SessionFactsProjector` PORT (ADR-0011 eager projection) — the OPT-IN seam the
|
|
3
|
+
* auth surface uses to compute a principal's coarse grants at LOGIN / REFRESH time so
|
|
4
|
+
* they can be cached on the session binding, letting gate 5 serve them from the
|
|
5
|
+
* request's `cachedFacts` instead of a DB reload on every request.
|
|
6
|
+
*
|
|
7
|
+
* ── WHY A DEDICATED PORT (not the core `PermissionFactsPort` directly) ────────────
|
|
8
|
+
* The core `PermissionFactsPort` is an authorization-engine dep wired into the record
|
|
9
|
+
* decision authority (`recordAuthorization`), not something AuthModule injects. Rather
|
|
10
|
+
* than couple the auth controller to that engine wiring, the consumer supplies a thin
|
|
11
|
+
* `SessionFactsProjector` — which it typically implements by delegating to the SAME
|
|
12
|
+
* `DbPermissionFactsAdapter.loadCoarseGrants` it already hands the decision authority.
|
|
13
|
+
* One tiny interface, one clean seam, no engine coupling in AuthModule.
|
|
14
|
+
*
|
|
15
|
+
* ── OPT-IN / ADDITIVE ────────────────────────────────────────────────────────────
|
|
16
|
+
* ABSENT (the default): the auth controller omits `cachedFacts` when binding the
|
|
17
|
+
* session → gate 5 sees no cache → DB reload every request = today's exact behavior.
|
|
18
|
+
* PRESENT: the controller calls `project(...)` at login + refresh and stores the
|
|
19
|
+
* returned {@link CoarseGrants} on the binding, so subsequent requests hit the cache.
|
|
20
|
+
* Turning it on NEVER changes a decision — it only removes a DB round-trip when the
|
|
21
|
+
* cached version is fresh; a stale/absent cache still reloads (gate 5's version gate).
|
|
22
|
+
*
|
|
23
|
+
* ── FAIL-CLOSED ──────────────────────────────────────────────────────────────────
|
|
24
|
+
* `project(...)` returning `null` (or throwing — the caller swallows it best-effort)
|
|
25
|
+
* means "no facts cached" → the session binds WITHOUT `cachedFacts` → gate 5 DB-reloads.
|
|
26
|
+
* A projection failure therefore degrades safely to the correct (un-cached) behavior;
|
|
27
|
+
* it never fabricates or omits a grant that would change a decision.
|
|
28
|
+
*/
|
|
29
|
+
import type { CoarseGrants } from '@astram/core';
|
|
30
|
+
export interface SessionFactsProjector {
|
|
31
|
+
/**
|
|
32
|
+
* Project the coarse feature/object-type grants for `principalId` in `tenantId` at
|
|
33
|
+
* login/refresh time (ADR-0011). Returns the {@link CoarseGrants} to cache on the
|
|
34
|
+
* session binding, or `null` when nothing should be cached (→ gate 5 DB-reloads).
|
|
35
|
+
*
|
|
36
|
+
* A principal with NO grants SHOULD return a {@link CoarseGrants} with an empty
|
|
37
|
+
* `grantedPermissionKeys` (tagged with the current version) — that is a legitimate
|
|
38
|
+
* "genuinely no grants" cache, distinct from `null` ("don't cache"). Implementations
|
|
39
|
+
* typically delegate to the same `PermissionFactsPort.loadCoarseGrants` the decision
|
|
40
|
+
* authority uses.
|
|
41
|
+
*/
|
|
42
|
+
project(principalId: string, tenantId: string): Promise<CoarseGrants | null>;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=session-facts-projector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-facts-projector.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/session-facts-projector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;CAC9E"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The `SessionFactsProjector` PORT (ADR-0011 eager projection) — the OPT-IN seam the
|
|
4
|
+
* auth surface uses to compute a principal's coarse grants at LOGIN / REFRESH time so
|
|
5
|
+
* they can be cached on the session binding, letting gate 5 serve them from the
|
|
6
|
+
* request's `cachedFacts` instead of a DB reload on every request.
|
|
7
|
+
*
|
|
8
|
+
* ── WHY A DEDICATED PORT (not the core `PermissionFactsPort` directly) ────────────
|
|
9
|
+
* The core `PermissionFactsPort` is an authorization-engine dep wired into the record
|
|
10
|
+
* decision authority (`recordAuthorization`), not something AuthModule injects. Rather
|
|
11
|
+
* than couple the auth controller to that engine wiring, the consumer supplies a thin
|
|
12
|
+
* `SessionFactsProjector` — which it typically implements by delegating to the SAME
|
|
13
|
+
* `DbPermissionFactsAdapter.loadCoarseGrants` it already hands the decision authority.
|
|
14
|
+
* One tiny interface, one clean seam, no engine coupling in AuthModule.
|
|
15
|
+
*
|
|
16
|
+
* ── OPT-IN / ADDITIVE ────────────────────────────────────────────────────────────
|
|
17
|
+
* ABSENT (the default): the auth controller omits `cachedFacts` when binding the
|
|
18
|
+
* session → gate 5 sees no cache → DB reload every request = today's exact behavior.
|
|
19
|
+
* PRESENT: the controller calls `project(...)` at login + refresh and stores the
|
|
20
|
+
* returned {@link CoarseGrants} on the binding, so subsequent requests hit the cache.
|
|
21
|
+
* Turning it on NEVER changes a decision — it only removes a DB round-trip when the
|
|
22
|
+
* cached version is fresh; a stale/absent cache still reloads (gate 5's version gate).
|
|
23
|
+
*
|
|
24
|
+
* ── FAIL-CLOSED ──────────────────────────────────────────────────────────────────
|
|
25
|
+
* `project(...)` returning `null` (or throwing — the caller swallows it best-effort)
|
|
26
|
+
* means "no facts cached" → the session binds WITHOUT `cachedFacts` → gate 5 DB-reloads.
|
|
27
|
+
* A projection failure therefore degrades safely to the correct (un-cached) behavior;
|
|
28
|
+
* it never fabricates or omits a grant that would change a decision.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-facts-projector.js","sourceRoot":"","sources":["../../../src/auth/ports/session-facts-projector.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionSweeper — revokes a principal's sessions after a credential change. The
|
|
3
|
+
* core has no revoke-all-for-principal, so the consumer implements this over its
|
|
4
|
+
* session store (there is an s_principal_id column + index). Consumer-plugged.
|
|
5
|
+
*/
|
|
6
|
+
export interface SessionSweeper {
|
|
7
|
+
/** Revoke ALL of the principal's sessions (used after a password reset). */
|
|
8
|
+
revokeAllForPrincipal(principalId: string): Promise<void>;
|
|
9
|
+
/** Revoke all EXCEPT keepSessionId (used after an authenticated change-password). */
|
|
10
|
+
revokeOthersForPrincipal(principalId: string, keepSessionId: string): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=session-sweeper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-sweeper.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/session-sweeper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,qFAAqF;IACrF,wBAAwB,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-sweeper.js","sourceRoot":"","sources":["../../../src/auth/ports/session-sweeper.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProfileResolver — the seam that turns an authenticated `Principal` into the
|
|
3
|
+
* signed-in user's **display profile** for `GET /auth/me` (the current-user
|
|
4
|
+
* endpoint). This is the fill for the flagged gap: login/session return only IDs
|
|
5
|
+
* (`principalId` / `tenantId`), so a UI has no human-readable name/company/role to
|
|
6
|
+
* show without leaking a raw UUID.
|
|
7
|
+
*
|
|
8
|
+
* WHY THIS IS A SEAM (not baked into the framework): the framework owns the
|
|
9
|
+
* CREDENTIAL store (`shield_identity`) but is deliberately AGNOSTIC to the app's
|
|
10
|
+
* ORG model — how users, companies (tenants), and roles are named lives in the
|
|
11
|
+
* consumer's own schema (astram3's `shield_app` tables, another app's users/roles
|
|
12
|
+
* model, etc.). So the framework opens THIS port and the consumer implements it,
|
|
13
|
+
* exactly like {@link CredentialResolver}. Zero core changes; the framework never
|
|
14
|
+
* hard-codes a table or a display-column.
|
|
15
|
+
*
|
|
16
|
+
* SECURITY / CONTRACT:
|
|
17
|
+
* - Called ONLY after the request-auth layer has resolved a live bearer token to a
|
|
18
|
+
* `Principal` (so it always acts as an authenticated caller — never anonymous).
|
|
19
|
+
* - Returns display NAMES, never raw UUIDs (the never-show-UUIDs rule): the
|
|
20
|
+
* consumer resolves `principal.id` → the user's name/email, `principal.tenantId`
|
|
21
|
+
* → the company's display name, `principal.roleIds` → the roles' names.
|
|
22
|
+
* - `null` = the principal has no resolvable profile (e.g. the user row was
|
|
23
|
+
* deleted since login) — the endpoint returns 404-style "no profile", never the
|
|
24
|
+
* ids. A store OUTAGE should THROW (fail-closed) — the endpoint maps it to a
|
|
25
|
+
* generic error, not an empty profile.
|
|
26
|
+
*/
|
|
27
|
+
import type { Principal } from '@astram/core';
|
|
28
|
+
/** A role as shown to the user — the display name, plus its id for the UI's keys. */
|
|
29
|
+
export interface ProfileRole {
|
|
30
|
+
/** The role's stable id (safe in `key=`/`data-*`, never rendered as text). */
|
|
31
|
+
readonly id: string;
|
|
32
|
+
/** The role's human-readable display name (what the UI renders). */
|
|
33
|
+
readonly name: string;
|
|
34
|
+
}
|
|
35
|
+
/** The signed-in user's tenant (company) as shown to the user. */
|
|
36
|
+
export interface ProfileTenant {
|
|
37
|
+
/** The tenant/company id (safe in machine contexts, never rendered as text). */
|
|
38
|
+
readonly id: string;
|
|
39
|
+
/** The company's human-readable display name (what the UI renders). */
|
|
40
|
+
readonly name: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The current-user display profile returned by `GET /auth/me`. Everything here is
|
|
44
|
+
* SAFE to render — names, not UUIDs. The bare `id` fields are for the UI's internal
|
|
45
|
+
* keys/routing only (the never-show-UUIDs rule allows ids in machine contexts).
|
|
46
|
+
*/
|
|
47
|
+
export interface CurrentUserProfile {
|
|
48
|
+
/** The user's id (machine use only — routing/keys, never rendered as text). */
|
|
49
|
+
readonly id: string;
|
|
50
|
+
/** The user's display name (what the header/card renders). */
|
|
51
|
+
readonly name: string;
|
|
52
|
+
/** The user's email/username, if the app exposes one (else omit). */
|
|
53
|
+
readonly email?: string;
|
|
54
|
+
/** The active tenant/company (the boundary the session is scoped to). */
|
|
55
|
+
readonly company: ProfileTenant;
|
|
56
|
+
/** The roles the user holds in the active tenant (display names). */
|
|
57
|
+
readonly roles: readonly ProfileRole[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The port a consumer implements to back `GET /auth/me`. Supplied to the framework
|
|
61
|
+
* via `ShieldModule.forRoot({ profileResolver })`. Framework-agnostic: the framework
|
|
62
|
+
* never reads the app's org tables — it calls this seam.
|
|
63
|
+
*/
|
|
64
|
+
export interface ProfileResolver {
|
|
65
|
+
/**
|
|
66
|
+
* Resolve an authenticated `Principal` → the display profile, or `null` when no
|
|
67
|
+
* profile exists for this principal (deleted user, etc.). MUST return display
|
|
68
|
+
* NAMES (never raw UUIDs) for `name`/`company.name`/`roles[].name`. A store outage
|
|
69
|
+
* should THROW (fail-closed) rather than return a partial/empty profile.
|
|
70
|
+
*/
|
|
71
|
+
resolve(principal: Principal): Promise<CurrentUserProfile | null>;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=profile-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile-resolver.d.ts","sourceRoot":"","sources":["../../src/auth/profile-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,qFAAqF;AACrF,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;CACxC;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,OAAO,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;CACnE"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ProfileResolver — the seam that turns an authenticated `Principal` into the
|
|
4
|
+
* signed-in user's **display profile** for `GET /auth/me` (the current-user
|
|
5
|
+
* endpoint). This is the fill for the flagged gap: login/session return only IDs
|
|
6
|
+
* (`principalId` / `tenantId`), so a UI has no human-readable name/company/role to
|
|
7
|
+
* show without leaking a raw UUID.
|
|
8
|
+
*
|
|
9
|
+
* WHY THIS IS A SEAM (not baked into the framework): the framework owns the
|
|
10
|
+
* CREDENTIAL store (`shield_identity`) but is deliberately AGNOSTIC to the app's
|
|
11
|
+
* ORG model — how users, companies (tenants), and roles are named lives in the
|
|
12
|
+
* consumer's own schema (astram3's `shield_app` tables, another app's users/roles
|
|
13
|
+
* model, etc.). So the framework opens THIS port and the consumer implements it,
|
|
14
|
+
* exactly like {@link CredentialResolver}. Zero core changes; the framework never
|
|
15
|
+
* hard-codes a table or a display-column.
|
|
16
|
+
*
|
|
17
|
+
* SECURITY / CONTRACT:
|
|
18
|
+
* - Called ONLY after the request-auth layer has resolved a live bearer token to a
|
|
19
|
+
* `Principal` (so it always acts as an authenticated caller — never anonymous).
|
|
20
|
+
* - Returns display NAMES, never raw UUIDs (the never-show-UUIDs rule): the
|
|
21
|
+
* consumer resolves `principal.id` → the user's name/email, `principal.tenantId`
|
|
22
|
+
* → the company's display name, `principal.roleIds` → the roles' names.
|
|
23
|
+
* - `null` = the principal has no resolvable profile (e.g. the user row was
|
|
24
|
+
* deleted since login) — the endpoint returns 404-style "no profile", never the
|
|
25
|
+
* ids. A store OUTAGE should THROW (fail-closed) — the endpoint maps it to a
|
|
26
|
+
* generic error, not an empty profile.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile-resolver.js","sourceRoot":"","sources":["../../src/auth/profile-resolver.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG"}
|