@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.
Files changed (144) hide show
  1. package/dist/audit/audit-chain-sealer.d.ts +90 -0
  2. package/dist/audit/audit-chain-sealer.d.ts.map +1 -0
  3. package/dist/audit/audit-chain-sealer.js +520 -0
  4. package/dist/audit/audit-seal-config.d.ts +22 -0
  5. package/dist/audit/audit-seal-config.d.ts.map +1 -0
  6. package/dist/audit/audit-seal-config.js +35 -0
  7. package/dist/audit/audit-sealer.service.d.ts +62 -0
  8. package/dist/audit/audit-sealer.service.d.ts.map +1 -0
  9. package/dist/audit/audit-sealer.service.js +197 -0
  10. package/dist/audit/node-sha256-hash.d.ts +6 -0
  11. package/dist/audit/node-sha256-hash.d.ts.map +1 -0
  12. package/dist/audit/node-sha256-hash.js +16 -0
  13. package/dist/auth/account.controller.d.ts +3 -1
  14. package/dist/auth/account.controller.d.ts.map +1 -1
  15. package/dist/auth/account.controller.js +79 -6
  16. package/dist/auth/admin-signin-controls.d.ts +80 -0
  17. package/dist/auth/admin-signin-controls.d.ts.map +1 -0
  18. package/dist/auth/admin-signin-controls.js +91 -0
  19. package/dist/auth/auth-audit.d.ts +59 -0
  20. package/dist/auth/auth-audit.d.ts.map +1 -0
  21. package/dist/auth/auth-audit.js +93 -0
  22. package/dist/auth/auth.controller.d.ts +53 -6
  23. package/dist/auth/auth.controller.d.ts.map +1 -1
  24. package/dist/auth/auth.controller.js +330 -44
  25. package/dist/auth/auth.module.d.ts +49 -0
  26. package/dist/auth/auth.module.d.ts.map +1 -1
  27. package/dist/auth/auth.module.js +149 -4
  28. package/dist/auth/captcha-gate.d.ts +103 -0
  29. package/dist/auth/captcha-gate.d.ts.map +1 -0
  30. package/dist/auth/captcha-gate.js +245 -0
  31. package/dist/auth/dto.d.ts +22 -1
  32. package/dist/auth/dto.d.ts.map +1 -1
  33. package/dist/auth/login-name-count-boot.d.ts +24 -0
  34. package/dist/auth/login-name-count-boot.d.ts.map +1 -0
  35. package/dist/auth/login-name-count-boot.js +47 -0
  36. package/dist/auth/logout-proof.d.ts +55 -0
  37. package/dist/auth/logout-proof.d.ts.map +1 -0
  38. package/dist/auth/logout-proof.js +127 -0
  39. package/dist/auth/pending-mfa-store.d.ts +5 -0
  40. package/dist/auth/pending-mfa-store.d.ts.map +1 -1
  41. package/dist/auth/pending-mfa-store.js +2 -0
  42. package/dist/auth/persistence.module.d.ts +2 -2
  43. package/dist/auth/persistence.module.d.ts.map +1 -1
  44. package/dist/auth/persistence.module.js +19 -3
  45. package/dist/auth/ports/audit-sink.d.ts +5 -2
  46. package/dist/auth/ports/audit-sink.d.ts.map +1 -1
  47. package/dist/auth/ports/mfa-device-store.d.ts +7 -0
  48. package/dist/auth/ports/mfa-device-store.d.ts.map +1 -1
  49. package/dist/auth/profile-resolver.d.ts +6 -0
  50. package/dist/auth/profile-resolver.d.ts.map +1 -1
  51. package/dist/auth/rate-limit/attempt-limiter.d.ts +6 -0
  52. package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -1
  53. package/dist/auth/rate-limit/attempt-limiter.js +14 -0
  54. package/dist/auth/tokens.d.ts +59 -0
  55. package/dist/auth/tokens.d.ts.map +1 -1
  56. package/dist/auth/tokens.js +61 -1
  57. package/dist/authentication/credential-provisioner.d.ts +7 -0
  58. package/dist/authentication/credential-provisioner.d.ts.map +1 -1
  59. package/dist/authentication/mariadb-credential-provisioner.d.ts +1 -0
  60. package/dist/authentication/mariadb-credential-provisioner.d.ts.map +1 -1
  61. package/dist/authentication/mariadb-credential-provisioner.js +7 -0
  62. package/dist/authentication/mariadb-credential-resolver.d.ts +23 -2
  63. package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
  64. package/dist/authentication/mariadb-credential-resolver.js +45 -1
  65. package/dist/authentication/mariadb-mfa-device-store.d.ts +7 -0
  66. package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -1
  67. package/dist/authentication/mariadb-mfa-device-store.js +32 -0
  68. package/dist/authentication/mariadb-mfa-requirement-source.d.ts +55 -0
  69. package/dist/authentication/mariadb-mfa-requirement-source.d.ts.map +1 -0
  70. package/dist/authentication/mariadb-mfa-requirement-source.js +164 -0
  71. package/dist/authentication/mariadb-mfa-resetter.d.ts +42 -0
  72. package/dist/authentication/mariadb-mfa-resetter.d.ts.map +1 -0
  73. package/dist/authentication/mariadb-mfa-resetter.js +59 -0
  74. package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
  75. package/dist/authentication/mariadb-profile-writer.js +5 -0
  76. package/dist/authentication/sign-in-with.d.ts +14 -0
  77. package/dist/authentication/sign-in-with.d.ts.map +1 -0
  78. package/dist/authentication/sign-in-with.js +36 -0
  79. package/dist/authz/core-access.d.ts +11 -0
  80. package/dist/authz/core-access.d.ts.map +1 -0
  81. package/dist/authz/core-access.js +14 -0
  82. package/dist/authz/list-scope-sql.d.ts.map +1 -1
  83. package/dist/authz/list-scope-sql.js +15 -5
  84. package/dist/authz/wire-record-authorization.d.ts.map +1 -1
  85. package/dist/authz/wire-record-authorization.js +21 -3
  86. package/dist/captcha/captcha.providers.d.ts +18 -0
  87. package/dist/captcha/captcha.providers.d.ts.map +1 -0
  88. package/dist/captcha/captcha.providers.js +78 -0
  89. package/dist/captcha/recaptcha-verifier.d.ts +20 -0
  90. package/dist/captcha/recaptcha-verifier.d.ts.map +1 -0
  91. package/dist/captcha/recaptcha-verifier.js +48 -0
  92. package/dist/captcha/siteverify.d.ts +47 -0
  93. package/dist/captcha/siteverify.d.ts.map +1 -0
  94. package/dist/captcha/siteverify.js +98 -0
  95. package/dist/captcha/turnstile-verifier.d.ts +18 -0
  96. package/dist/captcha/turnstile-verifier.d.ts.map +1 -0
  97. package/dist/captcha/turnstile-verifier.js +41 -0
  98. package/dist/config/captcha-config.d.ts +47 -0
  99. package/dist/config/captcha-config.d.ts.map +1 -0
  100. package/dist/config/captcha-config.js +163 -0
  101. package/dist/config/reserved-principal-ids.d.ts +15 -0
  102. package/dist/config/reserved-principal-ids.d.ts.map +1 -1
  103. package/dist/config/reserved-principal-ids.js +20 -3
  104. package/dist/enforcement/global-enforcement.guard.d.ts +9 -1
  105. package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
  106. package/dist/enforcement/global-enforcement.guard.js +52 -10
  107. package/dist/enforcement/reenroll-allowlist.d.ts +5 -0
  108. package/dist/enforcement/reenroll-allowlist.d.ts.map +1 -1
  109. package/dist/enforcement/reenroll-allowlist.js +5 -0
  110. package/dist/identity/auth-facts.controller.d.ts +31 -4
  111. package/dist/identity/auth-facts.controller.d.ts.map +1 -1
  112. package/dist/identity/auth-facts.controller.js +46 -11
  113. package/dist/identity/identity-actions.d.ts +2 -2
  114. package/dist/identity/identity-actions.d.ts.map +1 -1
  115. package/dist/identity/module-registry.bridge.d.ts.map +1 -1
  116. package/dist/identity/module-registry.bridge.js +3 -6
  117. package/dist/identity/permission-facts.adapter.d.ts +22 -0
  118. package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
  119. package/dist/identity/permission-facts.adapter.js +26 -1
  120. package/dist/index.d.ts +11 -2
  121. package/dist/index.d.ts.map +1 -1
  122. package/dist/index.js +43 -11
  123. package/dist/internal.d.ts +27 -0
  124. package/dist/internal.d.ts.map +1 -1
  125. package/dist/internal.js +36 -1
  126. package/dist/module/shield.module.d.ts +27 -0
  127. package/dist/module/shield.module.d.ts.map +1 -1
  128. package/dist/module/shield.module.js +10 -0
  129. package/dist/request-auth/request-principal.d.ts +8 -1
  130. package/dist/request-auth/request-principal.d.ts.map +1 -1
  131. package/dist/request-auth/request-principal.js +11 -4
  132. package/dist/request-auth/session-token-binder.d.ts +82 -0
  133. package/dist/request-auth/session-token-binder.d.ts.map +1 -1
  134. package/dist/request-auth/session-token-binder.js +203 -1
  135. package/dist/session/mfa-requirement-service.d.ts +168 -0
  136. package/dist/session/mfa-requirement-service.d.ts.map +1 -0
  137. package/dist/session/mfa-requirement-service.js +461 -0
  138. package/dist/session/session-restriction-store.d.ts +36 -0
  139. package/dist/session/session-restriction-store.d.ts.map +1 -1
  140. package/dist/session/session-restriction-store.js +57 -6
  141. package/dist/session/session-restriction.d.ts +22 -0
  142. package/dist/session/session-restriction.d.ts.map +1 -0
  143. package/dist/session/session-restriction.js +23 -0
  144. 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,6 @@
1
+ import type { HashFunction } from '@astram/core';
2
+ export declare class NodeSha256Hash implements HashFunction {
3
+ /** Lower-case hex SHA-256 of the UTF-8 bytes of `input`. */
4
+ hash(input: string): string;
5
+ }
6
+ //# sourceMappingURL=node-sha256-hash.d.ts.map
@@ -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
- 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);
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":"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"}
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
- constructor(binder, hasher, writer, sweeper, recoveryCodes, totpVerifier, profileResolver, profileWriter, mfaDevices, sessionRestrictions, loginTotpVerifier, mfaLimiter, loginLimiter, emailChangeTokens, emailAddresses, emailSender, emailChangeLinkBaseUrl, emailChangeLimiter, auditSink, messageDispatcher) {
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
- if (claims.mustReenroll === true && !allowedDuringReenroll) {
311
- throw new common_1.HttpException({ error: 'reenrollment_required' }, common_1.HttpStatus.FORBIDDEN);
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
- const removed = await store.remove(claims.id, deviceId);
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
- if ((await store.countConfirmed(claims.id)) === 0) {
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
- __metadata("design:paramtypes", [Function, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object])
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"}