@astram/guard 0.18.0 → 0.20.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/audit/audit-chain-sealer.d.ts +90 -0
- package/dist/audit/audit-chain-sealer.d.ts.map +1 -0
- package/dist/audit/audit-chain-sealer.js +520 -0
- package/dist/audit/audit-seal-config.d.ts +22 -0
- package/dist/audit/audit-seal-config.d.ts.map +1 -0
- package/dist/audit/audit-seal-config.js +35 -0
- package/dist/audit/audit-sealer.service.d.ts +62 -0
- package/dist/audit/audit-sealer.service.d.ts.map +1 -0
- package/dist/audit/audit-sealer.service.js +197 -0
- package/dist/audit/node-sha256-hash.d.ts +6 -0
- package/dist/audit/node-sha256-hash.d.ts.map +1 -0
- package/dist/audit/node-sha256-hash.js +16 -0
- package/dist/auth/account.controller.d.ts +3 -1
- package/dist/auth/account.controller.d.ts.map +1 -1
- package/dist/auth/account.controller.js +79 -6
- package/dist/auth/admin-signin-controls.d.ts +80 -0
- package/dist/auth/admin-signin-controls.d.ts.map +1 -0
- package/dist/auth/admin-signin-controls.js +91 -0
- package/dist/auth/auth-audit.d.ts +59 -0
- package/dist/auth/auth-audit.d.ts.map +1 -0
- package/dist/auth/auth-audit.js +93 -0
- package/dist/auth/auth.controller.d.ts +53 -6
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +330 -44
- package/dist/auth/auth.module.d.ts +49 -0
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +149 -4
- package/dist/auth/captcha-gate.d.ts +103 -0
- package/dist/auth/captcha-gate.d.ts.map +1 -0
- package/dist/auth/captcha-gate.js +245 -0
- package/dist/auth/dto.d.ts +22 -1
- package/dist/auth/dto.d.ts.map +1 -1
- package/dist/auth/login-name-count-boot.d.ts +24 -0
- package/dist/auth/login-name-count-boot.d.ts.map +1 -0
- package/dist/auth/login-name-count-boot.js +47 -0
- package/dist/auth/logout-proof.d.ts +55 -0
- package/dist/auth/logout-proof.d.ts.map +1 -0
- package/dist/auth/logout-proof.js +127 -0
- package/dist/auth/pending-mfa-store.d.ts +5 -0
- package/dist/auth/pending-mfa-store.d.ts.map +1 -1
- package/dist/auth/pending-mfa-store.js +2 -0
- package/dist/auth/persistence.module.d.ts +2 -2
- package/dist/auth/persistence.module.d.ts.map +1 -1
- package/dist/auth/persistence.module.js +19 -3
- package/dist/auth/ports/audit-sink.d.ts +5 -2
- package/dist/auth/ports/audit-sink.d.ts.map +1 -1
- package/dist/auth/ports/mfa-device-store.d.ts +7 -0
- package/dist/auth/ports/mfa-device-store.d.ts.map +1 -1
- package/dist/auth/profile-resolver.d.ts +6 -0
- package/dist/auth/profile-resolver.d.ts.map +1 -1
- package/dist/auth/rate-limit/attempt-limiter.d.ts +6 -0
- package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -1
- package/dist/auth/rate-limit/attempt-limiter.js +14 -0
- package/dist/auth/tokens.d.ts +59 -0
- package/dist/auth/tokens.d.ts.map +1 -1
- package/dist/auth/tokens.js +61 -1
- package/dist/authentication/credential-provisioner.d.ts +7 -0
- package/dist/authentication/credential-provisioner.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-provisioner.d.ts +1 -0
- package/dist/authentication/mariadb-credential-provisioner.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-provisioner.js +7 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts +23 -2
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-resolver.js +45 -1
- package/dist/authentication/mariadb-mfa-device-store.d.ts +7 -0
- package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-mfa-device-store.js +32 -0
- package/dist/authentication/mariadb-mfa-requirement-source.d.ts +55 -0
- package/dist/authentication/mariadb-mfa-requirement-source.d.ts.map +1 -0
- package/dist/authentication/mariadb-mfa-requirement-source.js +164 -0
- package/dist/authentication/mariadb-mfa-resetter.d.ts +42 -0
- package/dist/authentication/mariadb-mfa-resetter.d.ts.map +1 -0
- package/dist/authentication/mariadb-mfa-resetter.js +59 -0
- package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-profile-writer.js +5 -0
- package/dist/authentication/sign-in-with.d.ts +14 -0
- package/dist/authentication/sign-in-with.d.ts.map +1 -0
- package/dist/authentication/sign-in-with.js +36 -0
- package/dist/authz/core-access.d.ts +11 -0
- package/dist/authz/core-access.d.ts.map +1 -0
- package/dist/authz/core-access.js +14 -0
- package/dist/authz/list-scope-sql.d.ts.map +1 -1
- package/dist/authz/list-scope-sql.js +15 -5
- package/dist/authz/wire-record-authorization.d.ts.map +1 -1
- package/dist/authz/wire-record-authorization.js +21 -3
- package/dist/captcha/captcha.providers.d.ts +18 -0
- package/dist/captcha/captcha.providers.d.ts.map +1 -0
- package/dist/captcha/captcha.providers.js +78 -0
- package/dist/captcha/recaptcha-verifier.d.ts +20 -0
- package/dist/captcha/recaptcha-verifier.d.ts.map +1 -0
- package/dist/captcha/recaptcha-verifier.js +48 -0
- package/dist/captcha/siteverify.d.ts +47 -0
- package/dist/captcha/siteverify.d.ts.map +1 -0
- package/dist/captcha/siteverify.js +98 -0
- package/dist/captcha/turnstile-verifier.d.ts +18 -0
- package/dist/captcha/turnstile-verifier.d.ts.map +1 -0
- package/dist/captcha/turnstile-verifier.js +41 -0
- package/dist/config/captcha-config.d.ts +47 -0
- package/dist/config/captcha-config.d.ts.map +1 -0
- package/dist/config/captcha-config.js +163 -0
- package/dist/config/reserved-principal-ids.d.ts +15 -0
- package/dist/config/reserved-principal-ids.d.ts.map +1 -1
- package/dist/config/reserved-principal-ids.js +20 -3
- package/dist/enforcement/global-enforcement.guard.d.ts +9 -1
- package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
- package/dist/enforcement/global-enforcement.guard.js +52 -10
- package/dist/enforcement/reenroll-allowlist.d.ts +5 -0
- package/dist/enforcement/reenroll-allowlist.d.ts.map +1 -1
- package/dist/enforcement/reenroll-allowlist.js +5 -0
- package/dist/identity/auth-facts.controller.d.ts +31 -4
- package/dist/identity/auth-facts.controller.d.ts.map +1 -1
- package/dist/identity/auth-facts.controller.js +46 -11
- package/dist/identity/identity-actions.d.ts +2 -2
- package/dist/identity/identity-actions.d.ts.map +1 -1
- package/dist/identity/module-registry.bridge.d.ts.map +1 -1
- package/dist/identity/module-registry.bridge.js +3 -6
- package/dist/identity/permission-facts.adapter.d.ts +22 -0
- package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
- package/dist/identity/permission-facts.adapter.js +26 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -11
- package/dist/internal.d.ts +27 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +36 -1
- package/dist/module/shield.module.d.ts +27 -0
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +10 -0
- package/dist/request-auth/request-principal.d.ts +8 -1
- package/dist/request-auth/request-principal.d.ts.map +1 -1
- package/dist/request-auth/request-principal.js +11 -4
- package/dist/request-auth/session-token-binder.d.ts +82 -0
- package/dist/request-auth/session-token-binder.d.ts.map +1 -1
- package/dist/request-auth/session-token-binder.js +203 -1
- package/dist/session/mfa-requirement-service.d.ts +168 -0
- package/dist/session/mfa-requirement-service.d.ts.map +1 -0
- package/dist/session/mfa-requirement-service.js +461 -0
- package/dist/session/session-restriction-store.d.ts +36 -0
- package/dist/session/session-restriction-store.d.ts.map +1 -1
- package/dist/session/session-restriction-store.js +57 -6
- package/dist/session/session-restriction.d.ts +22 -0
- package/dist/session/session-restriction.d.ts.map +1 -0
- package/dist/session/session-restriction.js +23 -0
- package/package.json +10 -5
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `AuditSealerService` — runs the deferred audit sealer in the app (audit-chain-store SPEC
|
|
3
|
+
* §5.H, §11, §12).
|
|
4
|
+
*
|
|
5
|
+
* Registered by `PersistenceModule.forRoot()`, so it exists exactly when `SHIELD_DB_POOL`
|
|
6
|
+
* does (`ShieldModule.forRoot({ auth: true })`); an enforcement-only app has neither. There
|
|
7
|
+
* is no switch to turn it off: only `AUDIT_SEAL_INTERVAL_MS` / `AUDIT_SEAL_BATCH` tune it.
|
|
8
|
+
*
|
|
9
|
+
* - Boot (`onApplicationBootstrap`): one bounded table probe on `information_schema.TABLES`;
|
|
10
|
+
* both `audit_log` and `audit_chain` present ⇒ tick every interval. `audit_log` absent
|
|
11
|
+
* (auth without identity), or the probe failing (DB down, a test's stub pool) ⇒ one warning,
|
|
12
|
+
* idle; `audit_chain` absent (migration 102 not applied) ⇒ one error per probe, idle. The
|
|
13
|
+
* probe repeats every sweep period (1 h) and starts or stops the ticks. Never throws.
|
|
14
|
+
* - Ticks never overlap: the next one is scheduled only after the previous settled. A
|
|
15
|
+
* rejected tick is logged by error class only and the loop continues.
|
|
16
|
+
* - Shutdown (`onModuleDestroy`, which runs before the pool is ended, and
|
|
17
|
+
* `onApplicationShutdown`): stops the timers and waits for an in-flight tick. Idempotent.
|
|
18
|
+
* - Every timer is `unref()`'d: the sealer never keeps a process alive. Nothing here touches
|
|
19
|
+
* the request path (Shield #12).
|
|
20
|
+
*/
|
|
21
|
+
import { type OnApplicationBootstrap, type OnApplicationShutdown, type OnModuleDestroy } from '@nestjs/common';
|
|
22
|
+
import type { Pool } from 'mariadb';
|
|
23
|
+
import type { FrameworkSchemas } from '@astram/core';
|
|
24
|
+
import { type AuditSealConfig } from './audit-seal-config.js';
|
|
25
|
+
import { type AuditSealerLogger } from './audit-chain-sealer.js';
|
|
26
|
+
export interface AuditSealerServiceOptions {
|
|
27
|
+
/** Default `loadAuditSealConfig(process.env)`. */
|
|
28
|
+
readonly config?: AuditSealConfig;
|
|
29
|
+
/** Default `new Logger('Shield')`. */
|
|
30
|
+
readonly logger?: AuditSealerLogger;
|
|
31
|
+
/** Test seam; default an {@link AuditChainSealer} over the pool with {@link NodeSha256Hash}. */
|
|
32
|
+
readonly sealer?: {
|
|
33
|
+
tick(): Promise<unknown>;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export declare class AuditSealerService implements OnApplicationBootstrap, OnModuleDestroy, OnApplicationShutdown {
|
|
37
|
+
private readonly pool;
|
|
38
|
+
private readonly schemas;
|
|
39
|
+
private readonly config;
|
|
40
|
+
private readonly logger;
|
|
41
|
+
private readonly sealer;
|
|
42
|
+
private stopped;
|
|
43
|
+
private active;
|
|
44
|
+
private lastOutcome;
|
|
45
|
+
private tickTimer;
|
|
46
|
+
private probeTimer;
|
|
47
|
+
private inFlightTick;
|
|
48
|
+
private inFlightProbe;
|
|
49
|
+
constructor(pool: Pool, schemas: FrameworkSchemas, options?: AuditSealerServiceOptions);
|
|
50
|
+
onApplicationBootstrap(): Promise<void>;
|
|
51
|
+
onModuleDestroy(): Promise<void>;
|
|
52
|
+
onApplicationShutdown(): Promise<void>;
|
|
53
|
+
private stop;
|
|
54
|
+
/** Probe once (never overlapping, never throwing) and start or stop the ticks. */
|
|
55
|
+
private runProbe;
|
|
56
|
+
private probe;
|
|
57
|
+
/** Warnings once per change of state; the missing-chain error on every probe (SPEC §12). */
|
|
58
|
+
private report;
|
|
59
|
+
private schedule;
|
|
60
|
+
private runTick;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=audit-sealer.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-sealer.service.d.ts","sourceRoot":"","sources":["../../src/audit/audit-sealer.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAGL,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACnF,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,yBAAyB,CAAC;AAMjC,MAAM,WAAW,yBAAyB;IACxC,kDAAkD;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,CAAC;IAClC,sCAAsC;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,iBAAiB,CAAC;IACpC,gGAAgG;IAChG,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;CAChD;AAID,qBACa,kBACX,YAAW,sBAAsB,EAAE,eAAe,EAAE,qBAAqB;IAevE,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAd1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IAEtD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAA2B;IAC9C,OAAO,CAAC,SAAS,CAA6B;IAC9C,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,YAAY,CAA4B;IAChD,OAAO,CAAC,aAAa,CAA4B;gBAG9B,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,gBAAgB,EAC1C,OAAO,GAAE,yBAA8B;IAenC,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAQvC,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAIhC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;YAI9B,IAAI;IAWlB,kFAAkF;IAClF,OAAO,CAAC,QAAQ;YAwBF,KAAK;IA4BnB,4FAA4F;IAC5F,OAAO,CAAC,MAAM;IAsBd,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,OAAO;CAehB"}
|
|
@@ -0,0 +1,197 @@
|
|
|
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.AuditSealerService = void 0;
|
|
13
|
+
/**
|
|
14
|
+
* `AuditSealerService` — runs the deferred audit sealer in the app (audit-chain-store SPEC
|
|
15
|
+
* §5.H, §11, §12).
|
|
16
|
+
*
|
|
17
|
+
* Registered by `PersistenceModule.forRoot()`, so it exists exactly when `SHIELD_DB_POOL`
|
|
18
|
+
* does (`ShieldModule.forRoot({ auth: true })`); an enforcement-only app has neither. There
|
|
19
|
+
* is no switch to turn it off: only `AUDIT_SEAL_INTERVAL_MS` / `AUDIT_SEAL_BATCH` tune it.
|
|
20
|
+
*
|
|
21
|
+
* - Boot (`onApplicationBootstrap`): one bounded table probe on `information_schema.TABLES`;
|
|
22
|
+
* both `audit_log` and `audit_chain` present ⇒ tick every interval. `audit_log` absent
|
|
23
|
+
* (auth without identity), or the probe failing (DB down, a test's stub pool) ⇒ one warning,
|
|
24
|
+
* idle; `audit_chain` absent (migration 102 not applied) ⇒ one error per probe, idle. The
|
|
25
|
+
* probe repeats every sweep period (1 h) and starts or stops the ticks. Never throws.
|
|
26
|
+
* - Ticks never overlap: the next one is scheduled only after the previous settled. A
|
|
27
|
+
* rejected tick is logged by error class only and the loop continues.
|
|
28
|
+
* - Shutdown (`onModuleDestroy`, which runs before the pool is ended, and
|
|
29
|
+
* `onApplicationShutdown`): stops the timers and waits for an in-flight tick. Idempotent.
|
|
30
|
+
* - Every timer is `unref()`'d: the sealer never keeps a process alive. Nothing here touches
|
|
31
|
+
* the request path (Shield #12).
|
|
32
|
+
*/
|
|
33
|
+
const common_1 = require("@nestjs/common");
|
|
34
|
+
const audit_seal_config_js_1 = require("./audit-seal-config.js");
|
|
35
|
+
const audit_chain_sealer_js_1 = require("./audit-chain-sealer.js");
|
|
36
|
+
const node_sha256_hash_js_1 = require("./node-sha256-hash.js");
|
|
37
|
+
/** Upper bound on the boot/re-probe query, so a hung pool cannot stall the boot hook. */
|
|
38
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
39
|
+
let AuditSealerService = class AuditSealerService {
|
|
40
|
+
pool;
|
|
41
|
+
schemas;
|
|
42
|
+
config;
|
|
43
|
+
logger;
|
|
44
|
+
sealer;
|
|
45
|
+
stopped = false;
|
|
46
|
+
active = false;
|
|
47
|
+
lastOutcome;
|
|
48
|
+
tickTimer;
|
|
49
|
+
probeTimer;
|
|
50
|
+
inFlightTick;
|
|
51
|
+
inFlightProbe;
|
|
52
|
+
constructor(pool, schemas, options = {}) {
|
|
53
|
+
this.pool = pool;
|
|
54
|
+
this.schemas = schemas;
|
|
55
|
+
this.config = options.config ?? (0, audit_seal_config_js_1.loadAuditSealConfig)(process.env);
|
|
56
|
+
this.logger = options.logger ?? new common_1.Logger('Shield');
|
|
57
|
+
this.sealer =
|
|
58
|
+
options.sealer ??
|
|
59
|
+
new audit_chain_sealer_js_1.AuditChainSealer({
|
|
60
|
+
pool,
|
|
61
|
+
schemas,
|
|
62
|
+
hash: new node_sha256_hash_js_1.NodeSha256Hash(),
|
|
63
|
+
batch: this.config.batch,
|
|
64
|
+
logger: this.logger,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async onApplicationBootstrap() {
|
|
68
|
+
if (this.stopped)
|
|
69
|
+
return;
|
|
70
|
+
await this.runProbe();
|
|
71
|
+
if (this.stopped || this.probeTimer !== undefined)
|
|
72
|
+
return;
|
|
73
|
+
this.probeTimer = setInterval(() => void this.runProbe(), audit_chain_sealer_js_1.AUDIT_SEAL_SWEEP_MS);
|
|
74
|
+
this.probeTimer.unref();
|
|
75
|
+
}
|
|
76
|
+
async onModuleDestroy() {
|
|
77
|
+
await this.stop();
|
|
78
|
+
}
|
|
79
|
+
async onApplicationShutdown() {
|
|
80
|
+
await this.stop();
|
|
81
|
+
}
|
|
82
|
+
async stop() {
|
|
83
|
+
this.stopped = true;
|
|
84
|
+
this.active = false;
|
|
85
|
+
if (this.tickTimer !== undefined)
|
|
86
|
+
clearTimeout(this.tickTimer);
|
|
87
|
+
if (this.probeTimer !== undefined)
|
|
88
|
+
clearInterval(this.probeTimer);
|
|
89
|
+
this.tickTimer = undefined;
|
|
90
|
+
this.probeTimer = undefined;
|
|
91
|
+
await this.inFlightProbe;
|
|
92
|
+
await this.inFlightTick;
|
|
93
|
+
}
|
|
94
|
+
/** Probe once (never overlapping, never throwing) and start or stop the ticks. */
|
|
95
|
+
runProbe() {
|
|
96
|
+
if (this.inFlightProbe !== undefined)
|
|
97
|
+
return this.inFlightProbe;
|
|
98
|
+
const run = (async () => {
|
|
99
|
+
const outcome = await this.probe();
|
|
100
|
+
if (this.stopped)
|
|
101
|
+
return;
|
|
102
|
+
this.report(outcome);
|
|
103
|
+
this.lastOutcome = outcome;
|
|
104
|
+
if (outcome === 'ready') {
|
|
105
|
+
if (!this.active) {
|
|
106
|
+
this.active = true;
|
|
107
|
+
this.schedule();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
this.active = false;
|
|
112
|
+
if (this.tickTimer !== undefined)
|
|
113
|
+
clearTimeout(this.tickTimer);
|
|
114
|
+
this.tickTimer = undefined;
|
|
115
|
+
}
|
|
116
|
+
})().finally(() => {
|
|
117
|
+
this.inFlightProbe = undefined;
|
|
118
|
+
});
|
|
119
|
+
this.inFlightProbe = run;
|
|
120
|
+
return run;
|
|
121
|
+
}
|
|
122
|
+
async probe() {
|
|
123
|
+
let timer;
|
|
124
|
+
try {
|
|
125
|
+
const timeout = new Promise((_, reject) => {
|
|
126
|
+
timer = setTimeout(() => reject(new Error('probe_timeout')), PROBE_TIMEOUT_MS);
|
|
127
|
+
timer.unref();
|
|
128
|
+
});
|
|
129
|
+
const query = Promise.resolve().then(() => this.pool.query(`SELECT TABLE_NAME FROM information_schema.TABLES
|
|
130
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME IN ('audit_log', 'audit_chain')`, [this.schemas.audit]));
|
|
131
|
+
query.catch(() => undefined); // a late rejection after the timeout is not unhandled
|
|
132
|
+
const rows = await Promise.race([query, timeout]);
|
|
133
|
+
if (!Array.isArray(rows))
|
|
134
|
+
return 'probe_failed';
|
|
135
|
+
const names = new Set(rows.map((r) => r.TABLE_NAME));
|
|
136
|
+
if (!names.has('audit_log'))
|
|
137
|
+
return 'no_audit_log';
|
|
138
|
+
if (!names.has('audit_chain'))
|
|
139
|
+
return 'no_audit_chain';
|
|
140
|
+
return 'ready';
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return 'probe_failed';
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
if (timer !== undefined)
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Warnings once per change of state; the missing-chain error on every probe (SPEC §12). */
|
|
151
|
+
report(outcome) {
|
|
152
|
+
const db = this.schemas.audit;
|
|
153
|
+
if (outcome === 'no_audit_chain') {
|
|
154
|
+
this.logger.error(`audit sealer idle: ${db}.audit_chain does not exist (apply framework migration ` +
|
|
155
|
+
'1710000000102); audit_log rows stay unsealed until it does');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (outcome === this.lastOutcome)
|
|
159
|
+
return;
|
|
160
|
+
if (outcome === 'no_audit_log') {
|
|
161
|
+
this.logger.warn(`audit sealer idle: ${db}.audit_log does not exist (no identity audit store); ` +
|
|
162
|
+
're-checked every hour');
|
|
163
|
+
}
|
|
164
|
+
else if (outcome === 'probe_failed') {
|
|
165
|
+
this.logger.warn('audit sealer idle: the audit table check could not run; re-checked every hour');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
schedule() {
|
|
169
|
+
if (!this.active || this.stopped || this.tickTimer !== undefined)
|
|
170
|
+
return;
|
|
171
|
+
this.tickTimer = setTimeout(() => {
|
|
172
|
+
this.tickTimer = undefined;
|
|
173
|
+
this.runTick();
|
|
174
|
+
}, this.config.intervalMs);
|
|
175
|
+
this.tickTimer.unref();
|
|
176
|
+
}
|
|
177
|
+
runTick() {
|
|
178
|
+
if (!this.active || this.stopped || this.inFlightTick !== undefined)
|
|
179
|
+
return;
|
|
180
|
+
this.inFlightTick = (async () => {
|
|
181
|
+
try {
|
|
182
|
+
await this.sealer.tick();
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
this.logger.error(`audit sealer: tick failed (${(0, audit_chain_sealer_js_1.auditSealErrorClass)(err)}); retried next interval`);
|
|
186
|
+
}
|
|
187
|
+
})().finally(() => {
|
|
188
|
+
this.inFlightTick = undefined;
|
|
189
|
+
this.schedule();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
exports.AuditSealerService = AuditSealerService;
|
|
194
|
+
exports.AuditSealerService = AuditSealerService = __decorate([
|
|
195
|
+
(0, common_1.Injectable)(),
|
|
196
|
+
__metadata("design:paramtypes", [Object, Object, Object])
|
|
197
|
+
], AuditSealerService);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-sha256-hash.d.ts","sourceRoot":"","sources":["../../src/audit/node-sha256-hash.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,qBAAa,cAAe,YAAW,YAAY;IACjD,4DAA4D;IAC5D,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;CAG5B"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NodeSha256Hash = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `HashFunction` adapter over `node:crypto` SHA-256 for the durable audit chain
|
|
6
|
+
* (audit-chain-store SPEC §5.F). Core is zero-dependency, so the hash is injected into
|
|
7
|
+
* `linkEntry` / `verifyChain`. Lower-case hex of the UTF-8 bytes; never logs its input.
|
|
8
|
+
*/
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
class NodeSha256Hash {
|
|
11
|
+
/** Lower-case hex SHA-256 of the UTF-8 bytes of `input`. */
|
|
12
|
+
hash(input) {
|
|
13
|
+
return (0, node_crypto_1.createHash)('sha256').update(input, 'utf8').digest('hex');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.NodeSha256Hash = NodeSha256Hash;
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
*/
|
|
45
45
|
import type { FactorVerifier, PasswordHasher } from '@astram/core';
|
|
46
46
|
import type { SessionTokenBinder } from '../request-auth/session-token-binder.js';
|
|
47
|
+
import type { MfaRequirementService } from '../session/mfa-requirement-service.js';
|
|
47
48
|
import type { AuditSink } from './ports/audit-sink.js';
|
|
48
49
|
import type { EmailSender } from './ports/email-sender.js';
|
|
49
50
|
import type { MessageDispatcher } from '../messaging/message-dispatcher.js';
|
|
@@ -87,7 +88,8 @@ export declare class AccountController {
|
|
|
87
88
|
private readonly emailChangeLimiter?;
|
|
88
89
|
private readonly auditSink?;
|
|
89
90
|
private readonly messageDispatcher?;
|
|
90
|
-
|
|
91
|
+
private readonly mfaRequirement?;
|
|
92
|
+
constructor(binder?: SessionTokenBinder | undefined, hasher?: PasswordHasher | undefined, writer?: (CredentialWriter | null) | undefined, sweeper?: (SessionSweeper | null) | undefined, recoveryCodes?: (RecoveryCodeStore | null) | undefined, totpVerifier?: (FactorVerifier | null) | undefined, profileResolver?: (ProfileResolver | null) | undefined, profileWriter?: (ProfileWriter | null) | undefined, mfaDevices?: (MfaDeviceStore | null) | undefined, sessionRestrictions?: (SessionRestrictionStore | null) | undefined, loginTotpVerifier?: (FactorVerifier | null) | undefined, mfaLimiter?: (AttemptLimiter | null) | undefined, loginLimiter?: (AttemptLimiter | null) | undefined, emailChangeTokens?: (EmailChangeTokenStore | null) | undefined, emailAddresses?: (EmailAddressChanger | null) | undefined, emailSender?: (EmailSender | null) | undefined, emailChangeLinkBaseUrl?: string | null | undefined, emailChangeLimiter?: (AttemptLimiter | null) | undefined, auditSink?: (AuditSink | null) | undefined, messageDispatcher?: (MessageDispatcher | null) | undefined, mfaRequirement?: (MfaRequirementService | null) | undefined);
|
|
91
93
|
/**
|
|
92
94
|
* Resolve a human-readable otpauth account LABEL for the caller (email, else display
|
|
93
95
|
* name) via the profile resolver. Best-effort: no resolver / null profile / a thrown
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account.controller.d.ts","sourceRoot":"","sources":["../../src/auth/account.controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAmBH,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAa,MAAM,cAAc,CAAC;AAK9E,OAAO,KAAK,EAEV,kBAAkB,EACnB,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"account.controller.d.ts","sourceRoot":"","sources":["../../src/auth/account.controller.ts"],"names":[],"mappings":"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"}
|
|
@@ -64,6 +64,8 @@ const classification_decorator_js_1 = require("../enforcement/classification.dec
|
|
|
64
64
|
const request_principal_js_1 = require("../request-auth/request-principal.js");
|
|
65
65
|
const tokens_js_1 = require("../request-auth/tokens.js");
|
|
66
66
|
const tokens_js_2 = require("./tokens.js");
|
|
67
|
+
const session_restriction_js_1 = require("../session/session-restriction.js");
|
|
68
|
+
const auth_audit_js_1 = require("./auth-audit.js");
|
|
67
69
|
const deliver_email_js_1 = require("../messaging/deliver-email.js");
|
|
68
70
|
const email_address_js_1 = require("./email-address.js");
|
|
69
71
|
const email_change_messages_js_1 = require("./email-change-messages.js");
|
|
@@ -169,6 +171,10 @@ function unprocessable(error) {
|
|
|
169
171
|
* `code`. Missing → this 400 (the client then asks for the code); a wrong code is the generic
|
|
170
172
|
* {@link badRequest}.
|
|
171
173
|
*/
|
|
174
|
+
/** R2.7: the 409 for removing the last device while a mandatory-2FA policy applies. */
|
|
175
|
+
function mfaRequiredByPolicy() {
|
|
176
|
+
throw new common_1.HttpException({ error: 'mfa_required_by_policy' }, common_1.HttpStatus.CONFLICT);
|
|
177
|
+
}
|
|
172
178
|
function nameTaken() {
|
|
173
179
|
throw new common_1.HttpException({ error: 'name_taken' }, common_1.HttpStatus.CONFLICT);
|
|
174
180
|
}
|
|
@@ -214,7 +220,8 @@ let AccountController = class AccountController {
|
|
|
214
220
|
emailChangeLimiter;
|
|
215
221
|
auditSink;
|
|
216
222
|
messageDispatcher;
|
|
217
|
-
|
|
223
|
+
mfaRequirement;
|
|
224
|
+
constructor(binder, hasher, writer, sweeper, recoveryCodes, totpVerifier, profileResolver, profileWriter, mfaDevices, sessionRestrictions, loginTotpVerifier, mfaLimiter, loginLimiter, emailChangeTokens, emailAddresses, emailSender, emailChangeLinkBaseUrl, emailChangeLimiter, auditSink, messageDispatcher, mfaRequirement) {
|
|
218
225
|
this.binder = binder;
|
|
219
226
|
this.hasher = hasher;
|
|
220
227
|
this.writer = writer;
|
|
@@ -235,6 +242,7 @@ let AccountController = class AccountController {
|
|
|
235
242
|
this.emailChangeLimiter = emailChangeLimiter;
|
|
236
243
|
this.auditSink = auditSink;
|
|
237
244
|
this.messageDispatcher = messageDispatcher;
|
|
245
|
+
this.mfaRequirement = mfaRequirement;
|
|
238
246
|
}
|
|
239
247
|
/**
|
|
240
248
|
* Resolve a human-readable otpauth account LABEL for the caller (email, else display
|
|
@@ -281,6 +289,11 @@ let AccountController = class AccountController {
|
|
|
281
289
|
const claims = await this.binder.resolve(token);
|
|
282
290
|
if (claims === null)
|
|
283
291
|
unauthorized();
|
|
292
|
+
// Auth capabilities R2.5: re-evaluate the mandatory-2FA requirement when the permission version
|
|
293
|
+
// moved (no DB query otherwise; a failure is the 503 `authentication_unavailable`).
|
|
294
|
+
if (this.mfaRequirement !== undefined && this.mfaRequirement !== null) {
|
|
295
|
+
return this.mfaRequirement.refresh(token, claims);
|
|
296
|
+
}
|
|
284
297
|
return claims;
|
|
285
298
|
}
|
|
286
299
|
/**
|
|
@@ -307,8 +320,11 @@ let AccountController = class AccountController {
|
|
|
307
320
|
* A normal (`mustReenroll:false`) session is unaffected — this is a no-op for it.
|
|
308
321
|
*/
|
|
309
322
|
enforceReenroll(claims, allowedDuringReenroll) {
|
|
310
|
-
|
|
311
|
-
|
|
323
|
+
// D-2: the same per-handler policy serves the mandatory-2FA restriction (`mfa_enrollment`,
|
|
324
|
+
// including a passed grace deadline), each reason with its own 403 code.
|
|
325
|
+
const reason = (0, session_restriction_js_1.restrictionOf)(claims, Date.now());
|
|
326
|
+
if (reason !== null && !allowedDuringReenroll) {
|
|
327
|
+
throw new common_1.HttpException({ error: (0, session_restriction_js_1.restrictionErrorCode)(reason) }, common_1.HttpStatus.FORBIDDEN);
|
|
312
328
|
}
|
|
313
329
|
}
|
|
314
330
|
/**
|
|
@@ -812,6 +828,24 @@ let AccountController = class AccountController {
|
|
|
812
828
|
if (claims.mustReenroll === true && this.sessionRestrictions) {
|
|
813
829
|
await this.sessionRestrictions.clearMustReenroll(claims.sessionId).catch(() => undefined);
|
|
814
830
|
}
|
|
831
|
+
// MANDATORY 2FA (R2.3, AC-7): a confirmed factor lifts the `mfa_enrollment` restriction and any
|
|
832
|
+
// grace deadline on THIS session at once (Redis claims + durable column; the very next request
|
|
833
|
+
// succeeds) and deletes the grace clock (R2.4). Best-effort like the re-enrol clear: a failure
|
|
834
|
+
// leaves the session restricted (the safe direction).
|
|
835
|
+
if (this.mfaRequirement !== undefined && this.mfaRequirement !== null) {
|
|
836
|
+
await this.mfaRequirement.onFactorConfirmed(claims);
|
|
837
|
+
}
|
|
838
|
+
else if ((claims.restriction === 'mfa_enrollment' || claims.mfaEnrollmentDeadlineMs !== undefined) &&
|
|
839
|
+
this.binder !== undefined &&
|
|
840
|
+
this.binder !== null) {
|
|
841
|
+
// The feature was switched off since the session was restricted: still lift it.
|
|
842
|
+
await this.binder.clearRestriction(claims.sessionId);
|
|
843
|
+
if (claims.restriction === 'mfa_enrollment') {
|
|
844
|
+
await this.sessionRestrictions?.clearMfaRestriction?.(claims.sessionId).catch(() => { });
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
// R12.2: the session's own actor and tenant; no device id, name, secret or code.
|
|
848
|
+
(0, auth_audit_js_1.emitAuthAudit)(this.auditSink, (0, auth_audit_js_1.authEvent)(auth_audit_js_1.AUTH_MFA_ENROLLED, claims.id, claims.tenantId, { firstDevice: wasFirst }));
|
|
815
849
|
if (wasFirst) {
|
|
816
850
|
const recoveryCodes = await this.replaceRecoveryCodes(claims.id, recoveryStore, hasher);
|
|
817
851
|
return { recoveryCodes };
|
|
@@ -869,20 +903,57 @@ let AccountController = class AccountController {
|
|
|
869
903
|
// Phase 11 #1: the canonical device id or 400 (after the session, before any proof is spent).
|
|
870
904
|
const deviceId = (0, engine_1.canonicalUuid)(id) ?? badRequest();
|
|
871
905
|
const code = typeof body?.code === 'string' ? body.code : '';
|
|
906
|
+
// R2.7 (AC-11): the LAST confirmed device cannot be removed while a mandatory-2FA policy
|
|
907
|
+
// applies to the caller. The requirement is evaluated first (DB reads are fine here; a failure
|
|
908
|
+
// is 503), then a cheap pre-check refuses before any proof is spent (no code is burnt), and the
|
|
909
|
+
// delete itself re-checks under a per-principal row lock (`removeKeepingOne`), so two
|
|
910
|
+
// concurrent removals can never leave zero devices.
|
|
911
|
+
const mfa = this.mfaRequirement;
|
|
912
|
+
let policyApplies = false;
|
|
913
|
+
if (mfa !== undefined && mfa !== null) {
|
|
914
|
+
try {
|
|
915
|
+
policyApplies = await mfa.requiresFactor(claims);
|
|
916
|
+
}
|
|
917
|
+
catch {
|
|
918
|
+
retryableUnavailable();
|
|
919
|
+
}
|
|
920
|
+
if (policyApplies) {
|
|
921
|
+
const confirmed = (await store.listAll(claims.id)).filter((d) => d.confirmed);
|
|
922
|
+
if (confirmed.length === 1 && confirmed[0].id === deviceId)
|
|
923
|
+
mfaRequiredByPolicy();
|
|
924
|
+
}
|
|
925
|
+
}
|
|
872
926
|
// Proof BEFORE the delete: a TOTP from any confirmed device (incl. the one being
|
|
873
927
|
// removed — legit for the last-device disable) OR a valid recovery code. Rate-limited
|
|
874
928
|
// and replay-guarded like `/auth/mfa` (Phase 8 #1).
|
|
875
929
|
await this.requireMfaProof(store, claims.id, code);
|
|
876
|
-
|
|
930
|
+
let removed;
|
|
931
|
+
if (policyApplies && typeof store.removeKeepingOne === 'function') {
|
|
932
|
+
const outcome = await store.removeKeepingOne(claims.id, deviceId);
|
|
933
|
+
if (outcome === 'last_device')
|
|
934
|
+
mfaRequiredByPolicy();
|
|
935
|
+
removed = outcome === 'removed';
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
removed = await store.remove(claims.id, deviceId);
|
|
939
|
+
}
|
|
877
940
|
if (!removed) {
|
|
878
941
|
throw new common_1.HttpException({ error: 'not_found' }, common_1.HttpStatus.NOT_FOUND);
|
|
879
942
|
}
|
|
880
943
|
// Removing the last confirmed device disables MFA → wipe the account recovery codes.
|
|
881
|
-
|
|
944
|
+
const mfaOff = (await store.countConfirmed(claims.id)) === 0;
|
|
945
|
+
// R2.7 defence in depth: a store without the atomic removal (or any other path) still left no
|
|
946
|
+
// factor while a policy applies → restrict THIS session now, not at the next sign-in.
|
|
947
|
+
if (mfaOff && policyApplies && mfa !== undefined && mfa !== null) {
|
|
948
|
+
await mfa.restrictNow(claims);
|
|
949
|
+
}
|
|
950
|
+
if (mfaOff) {
|
|
882
951
|
if (this.recoveryCodes !== undefined && this.recoveryCodes !== null) {
|
|
883
952
|
await this.recoveryCodes.replaceAll(claims.id, []);
|
|
884
953
|
}
|
|
885
954
|
}
|
|
955
|
+
// R12.2: the session's own actor and tenant; no device id, secret or code.
|
|
956
|
+
(0, auth_audit_js_1.emitAuthAudit)(this.auditSink, (0, auth_audit_js_1.authEvent)(auth_audit_js_1.AUTH_MFA_REMOVED, claims.id, claims.tenantId, { mfaOff }));
|
|
886
957
|
}
|
|
887
958
|
/**
|
|
888
959
|
* `POST /account/mfa/recovery-codes/regenerate` — mint a fresh recovery-code batch
|
|
@@ -1045,5 +1116,7 @@ exports.AccountController = AccountController = __decorate([
|
|
|
1045
1116
|
__param(18, (0, common_1.Inject)(tokens_js_2.SHIELD_AUDIT_SINK)),
|
|
1046
1117
|
__param(19, (0, common_1.Optional)()),
|
|
1047
1118
|
__param(19, (0, common_1.Inject)(tokens_js_2.SHIELD_MESSAGE_DISPATCHER)),
|
|
1048
|
-
|
|
1119
|
+
__param(20, (0, common_1.Optional)()),
|
|
1120
|
+
__param(20, (0, common_1.Inject)(tokens_js_2.SHIELD_MFA_REQUIREMENT)),
|
|
1121
|
+
__metadata("design:paramtypes", [Function, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object])
|
|
1049
1122
|
], AccountController);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth capabilities R11 (task 18, B-5): the guard pieces behind `@astram/identity`'s admin sign-in
|
|
3
|
+
* controls (`POST /identity/users/:uuid/{reset-mfa,unlock,end-sessions}`). Guard never imports
|
|
4
|
+
* identity (ADR-0003): identity injects these through the tokens re-exported from
|
|
5
|
+
* `@astram/guard/internal` ({@link SHIELD_ADMIN_STEP_UP}, {@link SHIELD_ACCOUNT_UNLOCKER}).
|
|
6
|
+
*
|
|
7
|
+
* ── STEP-UP (R11.2) ─────────────────────────────────────────────────────────
|
|
8
|
+
* The admin's OWN current TOTP, checked the way the code-gated `/account` routes check it
|
|
9
|
+
* (`AccountController.requireMfaProof`): the attempt is counted on the admin's `mfa:pid:<id>`
|
|
10
|
+
* budget BEFORE verifying (atomic increment-first), the code is verified against every confirmed
|
|
11
|
+
* device through the replay-guarded login verifier (an accepted code is spent on every path), a
|
|
12
|
+
* success resets the budget. TOTP only: recovery codes are not accepted here (R11.2). The login
|
|
13
|
+
* step-up (`mfa-step-up.ts`) needs a pending password challenge, so it is not reused directly.
|
|
14
|
+
* No confirmed device ⇒ `no_factor` before anything is counted.
|
|
15
|
+
*
|
|
16
|
+
* ── UNLOCK (R11.1) ──────────────────────────────────────────────────────────
|
|
17
|
+
* Clears `login:pid:<id>`, `mfa:pid:<id>`, `recovery:<id>` (each on its own limiter) and the
|
|
18
|
+
* account CAPTCHA counter of every CAPTCHA endpoint (`shield:captcha:<ep>:pid:<id>`). The
|
|
19
|
+
* typed-text CAPTCHA keys hash the text a user typed and cannot be derived from the account; they
|
|
20
|
+
* expire with their window. Any Redis error rejects ({@link AdminControlUnavailableError}).
|
|
21
|
+
*/
|
|
22
|
+
import type { FactorVerifier } from '@astram/core';
|
|
23
|
+
import type { MfaDeviceStore } from './ports/mfa-device-store.js';
|
|
24
|
+
import type { AttemptLimiter } from './rate-limit/attempt-limiter.js';
|
|
25
|
+
/** A store the admin control needs could not answer; nothing is claimed (caller → 503). */
|
|
26
|
+
export declare class AdminControlUnavailableError extends Error {
|
|
27
|
+
constructor();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The step-up verdict:
|
|
31
|
+
* - `verified`: the code is right (budget reset);
|
|
32
|
+
* - `no_factor`: the admin has no confirmed device (caller → 409 `mfa_required`), nothing counted;
|
|
33
|
+
* - `missing`: blank code, nothing counted;
|
|
34
|
+
* - `rejected`: wrong code (stays counted);
|
|
35
|
+
* - `locked`: the admin's `mfa:pid` budget is spent;
|
|
36
|
+
* - `unavailable`: a store could not answer (fail closed).
|
|
37
|
+
*/
|
|
38
|
+
export type AdminStepUpOutcome = 'verified' | 'no_factor' | 'missing' | 'rejected' | 'locked' | 'unavailable';
|
|
39
|
+
/** The seam identity injects under {@link SHIELD_ADMIN_STEP_UP}. */
|
|
40
|
+
export interface AdminStepUp {
|
|
41
|
+
verify(principalId: string, code: string): Promise<AdminStepUpOutcome>;
|
|
42
|
+
}
|
|
43
|
+
export interface TotpAdminStepUpDeps {
|
|
44
|
+
readonly devices: Pick<MfaDeviceStore, 'listConfirmedSecrets'> | null;
|
|
45
|
+
/** The replay-guarded login TOTP verifier (`SHIELD_LOGIN_TOTP_VERIFIER`). */
|
|
46
|
+
readonly verifier: FactorVerifier | null;
|
|
47
|
+
/** The MFA step limiter (`SHIELD_MFA_LIMITER`, `mfa:pid:<id>`). */
|
|
48
|
+
readonly limiter: Pick<AttemptLimiter, 'consume' | 'refund' | 'reset'> | null;
|
|
49
|
+
}
|
|
50
|
+
export declare class TotpAdminStepUp implements AdminStepUp {
|
|
51
|
+
private readonly deps;
|
|
52
|
+
constructor(deps: TotpAdminStepUpDeps);
|
|
53
|
+
verify(principalId: string, code: string): Promise<AdminStepUpOutcome>;
|
|
54
|
+
}
|
|
55
|
+
/** The keys one unlock clears (limiter keys without the `shield:attempts:` namespace). */
|
|
56
|
+
export declare function accountUnlockKeys(principalId: string): {
|
|
57
|
+
login: string;
|
|
58
|
+
mfa: string;
|
|
59
|
+
recovery: string;
|
|
60
|
+
captcha: string[];
|
|
61
|
+
};
|
|
62
|
+
/** The seam identity injects under {@link SHIELD_ACCOUNT_UNLOCKER}. */
|
|
63
|
+
export interface AccountUnlocker {
|
|
64
|
+
/** Clear every sign-in counter of the principal; rejects {@link AdminControlUnavailableError}. */
|
|
65
|
+
unlock(principalId: string): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
export interface RedisAccountUnlockerDeps {
|
|
68
|
+
readonly login: Pick<AttemptLimiter, 'clear'>;
|
|
69
|
+
readonly mfa: Pick<AttemptLimiter, 'clear'>;
|
|
70
|
+
readonly recovery: Pick<AttemptLimiter, 'clear'>;
|
|
71
|
+
readonly captcha: {
|
|
72
|
+
clearStrict(keys: readonly string[]): Promise<void>;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export declare class RedisAccountUnlocker implements AccountUnlocker {
|
|
76
|
+
private readonly deps;
|
|
77
|
+
constructor(deps: RedisAccountUnlockerDeps);
|
|
78
|
+
unlock(principalId: string): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=admin-signin-controls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-signin-controls.d.ts","sourceRoot":"","sources":["../../src/auth/admin-signin-controls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE,2FAA2F;AAC3F,qBAAa,4BAA6B,SAAQ,KAAK;;CAKtD;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAC5B,UAAU,GAAG,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,aAAa,CAAC;AAE/E,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACxE;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,sBAAsB,CAAC,GAAG,IAAI,CAAC;IACtE,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;IACzC,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC;CAC/E;AAED,qBAAa,eAAgB,YAAW,WAAW;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,mBAAmB;IAEhD,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAmC7E;AAED,0FAA0F;AAC1F,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAOA;AAED,uEAAuE;AACvE,MAAM,WAAW,eAAe;IAC9B,kGAAkG;IAClG,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;IAC9C,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;IACjD,QAAQ,CAAC,OAAO,EAAE;QAAE,WAAW,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;CAC3E;AAED,qBAAa,oBAAqB,YAAW,eAAe;IAC9C,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,wBAAwB;IAErD,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAcjD"}
|