@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,90 @@
1
+ import { type LoggerService } from '@nestjs/common';
2
+ import type { Pool } from 'mariadb';
3
+ import { type Clock, type FrameworkSchemas, type HashFunction } from '@astram/core';
4
+ /** Lookback for the incremental scan (SPEC §10). */
5
+ export declare const AUDIT_SEAL_LOOKBACK_MS: number;
6
+ /** Full sweep and table re-probe period (SPEC §10). */
7
+ export declare const AUDIT_SEAL_SWEEP_MS: number;
8
+ /** Candidate rows per keyset page (SPEC §10). */
9
+ export declare const AUDIT_SEAL_PAGE = 1000;
10
+ /** Keyset pages per tick (SPEC §10). */
11
+ export declare const AUDIT_SEAL_PAGES = 10;
12
+ /** Tenants sealed per tick (SPEC §10). */
13
+ export declare const AUDIT_SEAL_TENANTS = 200;
14
+ /** Parked (stuck) tenants held at most (SPEC §10). */
15
+ export declare const AUDIT_SEAL_STUCK = 200;
16
+ /** The logger surface the sealer uses (Nest `Logger` shape). */
17
+ export type AuditSealerLogger = Pick<LoggerService, 'log' | 'warn' | 'error'>;
18
+ export interface AuditChainSealerOptions {
19
+ /** `SHIELD_DB_POOL`. Only `getConnection()` is used: every statement runs on the lock's session. */
20
+ readonly pool: Pool;
21
+ /** Resolved framework database names (`FRAMEWORK_SCHEMAS`). */
22
+ readonly schemas: FrameworkSchemas;
23
+ /** Chain hash (`NodeSha256Hash` in production). */
24
+ readonly hash: HashFunction;
25
+ /** Rows per tenant per tick (`AUDIT_SEAL_BATCH`). */
26
+ readonly batch: number;
27
+ /** Sealer clock: `ac_sealed_at_ms` and the sweep period. Default `Date.now`. */
28
+ readonly clock?: Clock;
29
+ /** Default `new Logger('Shield')`. */
30
+ readonly logger?: AuditSealerLogger;
31
+ }
32
+ /**
33
+ * A log-safe class for an error: a typed core reason, a driver error code / errno, or
34
+ * `unexpected`. Never the message (it can echo row content, e.g. a duplicate-key value).
35
+ */
36
+ export declare function auditSealErrorClass(err: unknown): string;
37
+ /** The leader lock name: `<ns>:audit_sealer` (single layout: `<db>:audit_sealer`), ≤ 64 chars. */
38
+ export declare function auditSealerLockName(schemas: Pick<FrameworkSchemas, 'namespace'>): string;
39
+ export declare class AuditChainSealer {
40
+ private readonly pool;
41
+ private readonly hash;
42
+ private readonly batch;
43
+ private readonly clock;
44
+ private readonly logger;
45
+ private readonly lockName;
46
+ private readonly logTable;
47
+ private readonly chainTable;
48
+ private readonly checkpointTable;
49
+ /** Sealer clock at the start of the last sweep; `undefined` ⇒ the next leader tick sweeps. */
50
+ private lastSweepAtMs;
51
+ /** Tenants parked until the next sweep (exact strings). */
52
+ private readonly stuck;
53
+ /** `(reason, tenant)` keys already logged this sweep. */
54
+ private readonly logged;
55
+ /** The stuck-set overflow was logged this sweep. */
56
+ private overflowLogged;
57
+ /**
58
+ * Backlog cursor (SPEC §5.G step 2): the oldest position the last tick left unsealed because
59
+ * of the batch / page / tenant caps (stuck tenants excluded). Leader memory only.
60
+ */
61
+ private cursor;
62
+ constructor(options: AuditChainSealerOptions);
63
+ /**
64
+ * One tick. Always releases the lock and returns (or destroys) the connection before it
65
+ * settles. A tenant batch error never rejects; an unexpected error (e.g. the candidate read
66
+ * failing) rejects after the cleanup.
67
+ */
68
+ tick(): Promise<void>;
69
+ private forgetLeadership;
70
+ private sealOnce;
71
+ /** The oldest position the caps left behind, skipping stuck tenants; `undefined` ⇒ none. */
72
+ private oldestLeftBehind;
73
+ /** `MIN(MAX(ac_occurred_at_ms), db now) − lookback`, or `undefined` for an empty chain. */
74
+ private readWindowStart;
75
+ /**
76
+ * Keyset-paged candidate read, grouped by exact tenant string, ≤ `batch` rows per tenant and
77
+ * ≤ 200 tenants (first appearance in keyset order; the rest wait for the next tick). The
78
+ * first page starts after `windowStart`, or at the backlog cursor `start` when given. Also
79
+ * returns what the caps left behind, for the backlog cursor.
80
+ */
81
+ private readCandidates;
82
+ /** Each tenant's head: the `MAX(ac_sequence)` chain row, else its latest checkpoint. */
83
+ private readHeads;
84
+ /** Link and insert one tenant's batch in one transaction; errors stay with this tenant. */
85
+ private sealTenant;
86
+ /** Log a failed batch (once per tenant and reason per sweep) and park it if it repeats. */
87
+ private recordFailure;
88
+ private logOnce;
89
+ }
90
+ //# sourceMappingURL=audit-chain-sealer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-chain-sealer.d.ts","sourceRoot":"","sources":["../../src/audit/audit-chain-sealer.ts"],"names":[],"mappings":"AAmCA,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,KAAK,EAAE,IAAI,EAAkB,MAAM,SAAS,CAAC;AACpD,OAAO,EASL,KAAK,KAAK,EACV,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB,oDAAoD;AACpD,eAAO,MAAM,sBAAsB,QAAc,CAAC;AAClD,uDAAuD;AACvD,eAAO,MAAM,mBAAmB,QAAc,CAAC;AAC/C,iDAAiD;AACjD,eAAO,MAAM,eAAe,OAAO,CAAC;AACpC,wCAAwC;AACxC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,0CAA0C;AAC1C,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,sDAAsD;AACtD,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAOpC,gEAAgE;AAChE,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,aAAa,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;AAE9E,MAAM,WAAW,uBAAuB;IACtC,oGAAoG;IACpG,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;IACnC,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,sCAAsC;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,iBAAiB,CAAC;CACrC;AAyDD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CASxD;AAUD,kGAAkG;AAClG,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,WAAW,CAAC,GAAG,MAAM,CAOxF;AAOD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;IAC5B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IAEzC,8FAA8F;IAC9F,OAAO,CAAC,aAAa,CAAqB;IAC1C,2DAA2D;IAC3D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,yDAAyD;IACzD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,oDAAoD;IACpD,OAAO,CAAC,cAAc,CAAS;IAC/B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAA6B;gBAE/B,OAAO,EAAE,uBAAuB;IAa5C;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAgD3B,OAAO,CAAC,gBAAgB;YAQV,QAAQ;IAwCtB,4FAA4F;IAC5F,OAAO,CAAC,gBAAgB;IAWxB,2FAA2F;YAC7E,eAAe;IAa7B;;;;;OAKG;YACW,cAAc;IA0H5B,wFAAwF;YAC1E,SAAS;IAyDvB,2FAA2F;YAC7E,UAAU;IAmExB,2FAA2F;IAC3F,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,OAAO;CAMhB"}
@@ -0,0 +1,520 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuditChainSealer = exports.AUDIT_SEAL_STUCK = exports.AUDIT_SEAL_TENANTS = exports.AUDIT_SEAL_PAGES = exports.AUDIT_SEAL_PAGE = exports.AUDIT_SEAL_SWEEP_MS = exports.AUDIT_SEAL_LOOKBACK_MS = void 0;
4
+ exports.auditSealErrorClass = auditSealErrorClass;
5
+ exports.auditSealerLockName = auditSealerLockName;
6
+ /**
7
+ * `AuditChainSealer` — one tick of the deferred audit sealer (audit-chain-store SPEC §5.G,
8
+ * §10, §11, §13).
9
+ *
10
+ * Writers keep appending plain rows to `<audit>.audit_log`; one leader per namespace (named
11
+ * lock) chains every committed row per tenant into the append-only `<audit>.audit_chain`.
12
+ * A tick:
13
+ * 1. checks out ONE pool connection and takes `GET_LOCK('<ns>:audit_sealer', 0)`; not
14
+ * acquired ⇒ return the connection, no work;
15
+ * 2. window start `W = MIN(MAX(ac_occurred_at_ms), db now) − 15 min` from the chain alone
16
+ * (`idx_ac_occurred`); empty chain ⇒ no bound. The first tick of a leader and then one
17
+ * tick per hour is a sweep: no bound, the stuck set and the logged-once memory clear.
18
+ * Backlog cursor: when the batch / page / tenant caps leave rows behind, the oldest such
19
+ * `(al_occurred_at_ms, al_uuid)` (stuck tenants excluded) is kept in memory and the next
20
+ * tick starts its keyset there instead of at `W` when it is older; cleared when a tick
21
+ * leaves nothing behind, on lock loss and at each sweep (which may set it again);
22
+ * 3./4. reads candidates in keyset pages on `idx_audit_time` (1000 rows, ≤ 10 pages, stops at
23
+ * 200 tenants), anti-joined into `uq_ac_al_uuid`, excluding parked tenants and tenants
24
+ * that already hold `batch` rows this tick; groups them by exact tenant string in Node;
25
+ * reads each tenant's head (chain PK, else latest checkpoint, else genesis);
26
+ * 5. links each tenant's rows with core `auditEventFromRow` + `linkEntry` and inserts them
27
+ * in ONE transaction per tenant on the same connection. A failed batch rolls back alone;
28
+ * a repeating error (duplicate key 1062, `AuditRowError`, `AuditValidationError`) parks
29
+ * the tenant until the next sweep (≤ 200 parked), anything else retries next tick;
30
+ * 6. `finally`: `RELEASE_LOCK`, then return the connection; if the release fails (or the
31
+ * lock state is unknown) destroy the connection instead, so a pooled connection never
32
+ * carries the lock.
33
+ *
34
+ * Every `audit_log` ⇄ chain comparison and every tenant filter on `audit_log` carries
35
+ * `COLLATE utf8mb4_nopad_bin` on the `audit_log` side (exact bytes, NO PAD; `audit_log` is
36
+ * `utf8mb4_unicode_ci`, a bare comparison raises error 1267). Database names come from
37
+ * `FrameworkSchemas`, never a literal. Logs carry fixed text, tenant and sequence only:
38
+ * never `err.message` and never row content (SPEC §11).
39
+ */
40
+ const node_crypto_1 = require("node:crypto");
41
+ const common_1 = require("@nestjs/common");
42
+ const core_1 = require("@astram/core");
43
+ /** Lookback for the incremental scan (SPEC §10). */
44
+ exports.AUDIT_SEAL_LOOKBACK_MS = 15 * 60_000;
45
+ /** Full sweep and table re-probe period (SPEC §10). */
46
+ exports.AUDIT_SEAL_SWEEP_MS = 60 * 60_000;
47
+ /** Candidate rows per keyset page (SPEC §10). */
48
+ exports.AUDIT_SEAL_PAGE = 1000;
49
+ /** Keyset pages per tick (SPEC §10). */
50
+ exports.AUDIT_SEAL_PAGES = 10;
51
+ /** Tenants sealed per tick (SPEC §10). */
52
+ exports.AUDIT_SEAL_TENANTS = 200;
53
+ /** Parked (stuck) tenants held at most (SPEC §10). */
54
+ exports.AUDIT_SEAL_STUCK = 200;
55
+ /** MariaDB's limit on a `GET_LOCK` name. */
56
+ const MAX_LOCK_NAME = 64;
57
+ /** MariaDB duplicate-key error number. */
58
+ const ER_DUP_ENTRY = 1062;
59
+ /** A BIGINT column value as a `bigint` for ordering; `undefined` when it is not an integer. */
60
+ function ordinalOf(value) {
61
+ try {
62
+ if (typeof value === 'bigint')
63
+ return value;
64
+ if (typeof value === 'number' && Number.isInteger(value))
65
+ return BigInt(value);
66
+ if (typeof value === 'string' && /^-?\d+$/.test(value.trim()))
67
+ return BigInt(value.trim());
68
+ }
69
+ catch {
70
+ // fall through
71
+ }
72
+ return undefined;
73
+ }
74
+ /** Thrown inside a tick when the connection can no longer be trusted (rollback failed). */
75
+ class ConnectionUnusable extends Error {
76
+ constructor() {
77
+ super('audit_sealer_connection_unusable');
78
+ this.name = 'ConnectionUnusable';
79
+ }
80
+ }
81
+ const DRIVER_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
82
+ /**
83
+ * A log-safe class for an error: a typed core reason, a driver error code / errno, or
84
+ * `unexpected`. Never the message (it can echo row content, e.g. a duplicate-key value).
85
+ */
86
+ function auditSealErrorClass(err) {
87
+ if (err instanceof core_1.AuditRowError)
88
+ return `row_rejected:${err.reason}`;
89
+ if (err instanceof core_1.AuditValidationError)
90
+ return `event_rejected:${err.reason}`;
91
+ if (typeof err === 'object' && err !== null) {
92
+ const { code, errno } = err;
93
+ if (typeof code === 'string' && DRIVER_CODE.test(code))
94
+ return `db_error:${code}`;
95
+ if (typeof errno === 'number' && Number.isSafeInteger(errno))
96
+ return `db_error:${errno}`;
97
+ }
98
+ return 'unexpected';
99
+ }
100
+ /** Errors that repeat on every retry of the same rows, so the tenant is parked (SPEC §5.G.3). */
101
+ function isRepeating(err) {
102
+ if (err instanceof core_1.AuditRowError || err instanceof core_1.AuditValidationError)
103
+ return true;
104
+ return (typeof err === 'object' && err !== null && err.errno === ER_DUP_ENTRY);
105
+ }
106
+ /** The leader lock name: `<ns>:audit_sealer` (single layout: `<db>:audit_sealer`), ≤ 64 chars. */
107
+ function auditSealerLockName(schemas) {
108
+ const name = `${schemas.namespace}:audit_sealer`;
109
+ if (name.length <= MAX_LOCK_NAME)
110
+ return name;
111
+ // A 55-char namespace or 64-char single-layout database exceeds MariaDB's limit: keep the
112
+ // name unique per namespace with a digest (same approach as the migrate ledger lock).
113
+ const digest = (0, node_crypto_1.createHash)('sha256').update(schemas.namespace, 'utf8').digest('hex');
114
+ return `audit_sealer:${digest.slice(0, MAX_LOCK_NAME - 'audit_sealer:'.length)}`;
115
+ }
116
+ const quoteId = (name) => `\`${name.replace(/`/g, '``')}\``;
117
+ /** `?, ?, …` for `n` parameters. */
118
+ const placeholders = (n) => Array.from({ length: n }, () => '?').join(', ');
119
+ class AuditChainSealer {
120
+ pool;
121
+ hash;
122
+ batch;
123
+ clock;
124
+ logger;
125
+ lockName;
126
+ logTable;
127
+ chainTable;
128
+ checkpointTable;
129
+ /** Sealer clock at the start of the last sweep; `undefined` ⇒ the next leader tick sweeps. */
130
+ lastSweepAtMs;
131
+ /** Tenants parked until the next sweep (exact strings). */
132
+ stuck = new Set();
133
+ /** `(reason, tenant)` keys already logged this sweep. */
134
+ logged = new Set();
135
+ /** The stuck-set overflow was logged this sweep. */
136
+ overflowLogged = false;
137
+ /**
138
+ * Backlog cursor (SPEC §5.G step 2): the oldest position the last tick left unsealed because
139
+ * of the batch / page / tenant caps (stuck tenants excluded). Leader memory only.
140
+ */
141
+ cursor;
142
+ constructor(options) {
143
+ this.pool = options.pool;
144
+ this.hash = options.hash;
145
+ this.batch = options.batch;
146
+ this.clock = options.clock ?? { nowMs: () => Date.now() };
147
+ this.logger = options.logger ?? new common_1.Logger('Shield');
148
+ this.lockName = auditSealerLockName(options.schemas);
149
+ const db = quoteId(options.schemas.audit);
150
+ this.logTable = `${db}.${quoteId('audit_log')}`;
151
+ this.chainTable = `${db}.${quoteId('audit_chain')}`;
152
+ this.checkpointTable = `${db}.${quoteId('audit_chain_checkpoint')}`;
153
+ }
154
+ /**
155
+ * One tick. Always releases the lock and returns (or destroys) the connection before it
156
+ * settles. A tenant batch error never rejects; an unexpected error (e.g. the candidate read
157
+ * failing) rejects after the cleanup.
158
+ */
159
+ async tick() {
160
+ const conn = await this.pool.getConnection();
161
+ /** GET_LOCK's answer; stays `undefined` (outcome unknown) when the statement fails. */
162
+ let locked;
163
+ let destroy = false;
164
+ try {
165
+ const rows = (await conn.query('SELECT GET_LOCK(?, 0) AS acquired', [
166
+ this.lockName,
167
+ ]));
168
+ const acquired = rows[0]?.acquired;
169
+ locked = acquired !== null && acquired !== undefined && Number(acquired) === 1;
170
+ if (!locked) {
171
+ // Another instance leads. If this one leads again later it is a new leader: it sweeps
172
+ // first and starts with an empty stuck set.
173
+ this.forgetLeadership();
174
+ return;
175
+ }
176
+ await this.sealOnce(conn);
177
+ }
178
+ catch (err) {
179
+ if (!(err instanceof ConnectionUnusable))
180
+ throw err;
181
+ destroy = true;
182
+ }
183
+ finally {
184
+ if (locked === undefined) {
185
+ // GET_LOCK failed: whether the session holds the lock is unknown, so never pool it.
186
+ destroy = true;
187
+ }
188
+ else if (locked) {
189
+ try {
190
+ await conn.query('SELECT RELEASE_LOCK(?) AS released', [this.lockName]);
191
+ }
192
+ catch {
193
+ destroy = true;
194
+ }
195
+ }
196
+ if (destroy) {
197
+ try {
198
+ conn.destroy();
199
+ }
200
+ catch {
201
+ // The socket is closed either way; the server drops the session's lock with it.
202
+ }
203
+ }
204
+ else {
205
+ try {
206
+ await conn.release();
207
+ }
208
+ catch {
209
+ // Returning a connection to a closing pool is non-fatal.
210
+ }
211
+ }
212
+ }
213
+ }
214
+ forgetLeadership() {
215
+ this.lastSweepAtMs = undefined;
216
+ this.stuck.clear();
217
+ this.logged.clear();
218
+ this.overflowLogged = false;
219
+ this.cursor = undefined;
220
+ }
221
+ async sealOnce(conn) {
222
+ const now = this.clock.nowMs();
223
+ const sweep = this.lastSweepAtMs === undefined || now - this.lastSweepAtMs >= exports.AUDIT_SEAL_SWEEP_MS;
224
+ if (sweep) {
225
+ this.lastSweepAtMs = now;
226
+ this.stuck.clear();
227
+ this.logged.clear();
228
+ this.overflowLogged = false;
229
+ // The sweep scans from the start; it sets the cursor again if it leaves rows behind.
230
+ this.cursor = undefined;
231
+ }
232
+ const windowStart = sweep ? undefined : await this.readWindowStart(conn);
233
+ // Start from the backlog cursor instead of W when it is older (SPEC §5.G step 2).
234
+ const start = windowStart !== undefined &&
235
+ this.cursor !== undefined &&
236
+ this.cursor.ordinal <= BigInt(windowStart)
237
+ ? this.cursor
238
+ : undefined;
239
+ const read = await this.readCandidates(conn, start === undefined ? windowStart : undefined, start);
240
+ // Rows left behind by the caps, before sealing: if the connection dies mid-tick, the next
241
+ // tick still resumes from here.
242
+ this.cursor = this.oldestLeftBehind(read);
243
+ const { groups } = read;
244
+ if (groups.size === 0)
245
+ return;
246
+ const heads = await this.readHeads(conn, [...groups.keys()]);
247
+ for (const [tenant, rows] of groups) {
248
+ await this.sealTenant(conn, tenant, rows, heads.get(tenant));
249
+ }
250
+ // Tenants parked during this tick wait for the sweep: their rows do not hold the cursor.
251
+ this.cursor = this.oldestLeftBehind(read);
252
+ }
253
+ /** The oldest position the caps left behind, skipping stuck tenants; `undefined` ⇒ none. */
254
+ oldestLeftBehind(read) {
255
+ let oldest = read.tail;
256
+ for (const [tenant, position] of read.leftBehind) {
257
+ if (this.stuck.has(tenant))
258
+ continue;
259
+ if (oldest === undefined || read.order.get(position) < read.order.get(oldest)) {
260
+ oldest = position;
261
+ }
262
+ }
263
+ return oldest;
264
+ }
265
+ /** `MIN(MAX(ac_occurred_at_ms), db now) − lookback`, or `undefined` for an empty chain. */
266
+ async readWindowStart(conn) {
267
+ const rows = (await conn.query(`SELECT MAX(ac_occurred_at_ms) AS max_ms,
268
+ CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS SIGNED) AS now_ms
269
+ FROM ${this.chainTable}`));
270
+ const row = rows[0];
271
+ if (row?.max_ms === null || row?.max_ms === undefined)
272
+ return undefined;
273
+ const max = (0, core_1.toSafeInteger)(row.max_ms);
274
+ const dbNow = row.now_ms === null || row.now_ms === undefined ? max : (0, core_1.toSafeInteger)(row.now_ms);
275
+ return Math.min(max, dbNow) - exports.AUDIT_SEAL_LOOKBACK_MS;
276
+ }
277
+ /**
278
+ * Keyset-paged candidate read, grouped by exact tenant string, ≤ `batch` rows per tenant and
279
+ * ≤ 200 tenants (first appearance in keyset order; the rest wait for the next tick). The
280
+ * first page starts after `windowStart`, or at the backlog cursor `start` when given. Also
281
+ * returns what the caps left behind, for the backlog cursor.
282
+ */
283
+ async readCandidates(conn, windowStart, start) {
284
+ const groups = new Map();
285
+ const full = new Set();
286
+ const leftBehind = new Map();
287
+ const order = new Map();
288
+ /** Per full tenant, the read-order index of its last taken row. */
289
+ const lastTaken = new Map();
290
+ let after = start;
291
+ let index = 0;
292
+ let lastRow;
293
+ let exhausted = false;
294
+ const positionOf = (row, inclusive) => {
295
+ const ordinal = ordinalOf(row.al_occurred_at_ms);
296
+ if (ordinal === undefined)
297
+ return undefined;
298
+ return { occurred: row.al_occurred_at_ms, ordinal, uuid: row.al_uuid, inclusive };
299
+ };
300
+ for (let page = 0; page < exports.AUDIT_SEAL_PAGES; page++) {
301
+ const where = [];
302
+ const params = [];
303
+ if (windowStart !== undefined) {
304
+ where.push('al_occurred_at_ms > ?');
305
+ params.push(windowStart);
306
+ }
307
+ if (after !== undefined) {
308
+ // The cursor row itself is still unsealed: an inclusive start keeps it (`>=`).
309
+ where.push(`(al_occurred_at_ms > ? OR (al_occurred_at_ms = ? AND al_uuid ${after.inclusive ? '>=' : '>'} ?))`);
310
+ params.push(after.occurred, after.occurred, after.uuid);
311
+ }
312
+ const excluded = [...this.stuck, ...full];
313
+ if (excluded.length > 0) {
314
+ where.push(`al_tenant_id COLLATE utf8mb4_nopad_bin NOT IN (${placeholders(excluded.length)})`);
315
+ params.push(...excluded);
316
+ }
317
+ where.push(`NOT EXISTS (SELECT 1 FROM ${this.chainTable}
318
+ WHERE ac_al_uuid = al_uuid COLLATE utf8mb4_nopad_bin)`);
319
+ params.push(exports.AUDIT_SEAL_PAGE);
320
+ const rows = (await conn.query(`SELECT al_uuid, al_type, al_occurred_at_ms, al_actor_id, al_tenant_id, al_action_key,
321
+ al_resource_json, al_decision_json, al_attributes_json
322
+ FROM ${this.logTable}
323
+ WHERE ${where.join('\n AND ')}
324
+ ORDER BY al_occurred_at_ms, al_uuid
325
+ LIMIT ?`, params));
326
+ for (const row of rows) {
327
+ const i = index++;
328
+ const tenant = row.al_tenant_id;
329
+ if (this.stuck.has(tenant))
330
+ continue;
331
+ let group = groups.get(tenant);
332
+ let skip = false;
333
+ if (group === undefined) {
334
+ if (groups.size >= exports.AUDIT_SEAL_TENANTS) {
335
+ skip = true; // tenant cap
336
+ }
337
+ else {
338
+ group = [];
339
+ groups.set(tenant, group);
340
+ }
341
+ }
342
+ else if (group.length >= this.batch) {
343
+ skip = true; // batch cap
344
+ }
345
+ if (skip || group === undefined) {
346
+ if (!leftBehind.has(tenant)) {
347
+ const position = positionOf(row, true);
348
+ if (position !== undefined) {
349
+ leftBehind.set(tenant, position);
350
+ order.set(position, i);
351
+ }
352
+ }
353
+ continue;
354
+ }
355
+ group.push(row);
356
+ if (group.length >= this.batch) {
357
+ full.add(tenant);
358
+ lastTaken.set(tenant, { index: i, row });
359
+ }
360
+ }
361
+ lastRow = rows[rows.length - 1] ?? lastRow;
362
+ if (rows.length < exports.AUDIT_SEAL_PAGE) {
363
+ exhausted = true;
364
+ break;
365
+ }
366
+ if (groups.size >= exports.AUDIT_SEAL_TENANTS)
367
+ break;
368
+ const last = rows[rows.length - 1];
369
+ after = { occurred: last.al_occurred_at_ms, uuid: last.al_uuid, inclusive: false };
370
+ }
371
+ // A full tenant's later rows were excluded from the following pages (NOT IN), so it may
372
+ // have rows left even when none was seen: resume strictly after its last taken row.
373
+ for (const [tenant, taken] of lastTaken) {
374
+ if (leftBehind.has(tenant))
375
+ continue;
376
+ const position = positionOf(taken.row, false);
377
+ if (position === undefined)
378
+ continue;
379
+ leftBehind.set(tenant, position);
380
+ // Strictly after row `index`: before any row read later, after the row itself.
381
+ order.set(position, taken.index + 0.5);
382
+ }
383
+ // Paging stopped on `pages` or `tenants` with a full last page: the rest was never read.
384
+ let tail;
385
+ if (!exhausted && lastRow !== undefined) {
386
+ tail = positionOf(lastRow, false);
387
+ if (tail !== undefined)
388
+ order.set(tail, index - 1 + 0.5);
389
+ }
390
+ return { groups, leftBehind, tail, order };
391
+ }
392
+ /** Each tenant's head: the `MAX(ac_sequence)` chain row, else its latest checkpoint. */
393
+ async readHeads(conn, tenants) {
394
+ const heads = new Map();
395
+ const chainRows = (await conn.query(`SELECT ac.ac_tenant_id, ac.ac_sequence, ac.ac_entry_hash,
396
+ CASE WHEN al.al_uuid IS NULL THEN 1 ELSE 0 END AS al_missing
397
+ FROM (SELECT ac_tenant_id AS head_tenant, MAX(ac_sequence) AS head_sequence
398
+ FROM ${this.chainTable}
399
+ WHERE ac_tenant_id IN (${placeholders(tenants.length)})
400
+ GROUP BY ac_tenant_id) h
401
+ JOIN ${this.chainTable} ac
402
+ ON ac.ac_tenant_id = h.head_tenant AND ac.ac_sequence = h.head_sequence
403
+ LEFT JOIN ${this.logTable} al
404
+ ON al.al_uuid = ac.ac_al_uuid COLLATE utf8mb4_unicode_ci
405
+ AND al.al_uuid COLLATE utf8mb4_nopad_bin = ac.ac_al_uuid`, tenants));
406
+ for (const r of chainRows) {
407
+ heads.set(r.ac_tenant_id, {
408
+ anchor: {
409
+ tenantId: r.ac_tenant_id,
410
+ sequenceNumber: (0, core_1.toSafeInteger)(r.ac_sequence),
411
+ entryHash: r.ac_entry_hash,
412
+ },
413
+ missingRow: Number(r.al_missing) === 1,
414
+ });
415
+ }
416
+ const rest = tenants.filter((t) => !heads.has(t));
417
+ if (rest.length === 0)
418
+ return heads;
419
+ const checkpoints = (await conn.query(`SELECT c.acc_tenant_id, c.acc_sequence, c.acc_entry_hash
420
+ FROM (SELECT acc_tenant_id AS cp_tenant, MAX(acc_sequence) AS cp_sequence
421
+ FROM ${this.checkpointTable}
422
+ WHERE acc_tenant_id IN (${placeholders(rest.length)})
423
+ GROUP BY acc_tenant_id) m
424
+ JOIN ${this.checkpointTable} c
425
+ ON c.acc_tenant_id = m.cp_tenant AND c.acc_sequence = m.cp_sequence`, rest));
426
+ for (const r of checkpoints) {
427
+ heads.set(r.acc_tenant_id, {
428
+ anchor: {
429
+ tenantId: r.acc_tenant_id,
430
+ sequenceNumber: (0, core_1.toSafeInteger)(r.acc_sequence),
431
+ entryHash: r.acc_entry_hash,
432
+ },
433
+ missingRow: false,
434
+ });
435
+ }
436
+ return heads;
437
+ }
438
+ /** Link and insert one tenant's batch in one transaction; errors stay with this tenant. */
439
+ async sealTenant(conn, tenant, rows, head) {
440
+ if (head?.missingRow) {
441
+ // Keep sealing from the head's hash; audit:verify keeps reporting missing_row there.
442
+ this.logOnce('head_missing_row', tenant, `audit sealer: tenant ${JSON.stringify(tenant)} head at sequence ` +
443
+ `${head.anchor.sequenceNumber} has no audit_log row; sealing continues from its hash`);
444
+ }
445
+ let prev = head?.anchor;
446
+ /** The sequence being linked or inserted; a failure is reported at it. */
447
+ let sequence = prev !== undefined ? prev.sequenceNumber + 1 : 0;
448
+ let inTransaction = false;
449
+ try {
450
+ const links = rows.map((row) => {
451
+ sequence = prev !== undefined ? prev.sequenceNumber + 1 : 0;
452
+ const event = (0, core_1.auditEventFromRow)(row);
453
+ const link = (0, core_1.linkEntry)(event, prev, this.hash);
454
+ prev = { tenantId: tenant, sequenceNumber: link.sequenceNumber, entryHash: link.entryHash };
455
+ return { row, link, occurredAtMs: event.occurredAtMs };
456
+ });
457
+ const sealedAtMs = this.clock.nowMs();
458
+ await conn.beginTransaction();
459
+ inTransaction = true;
460
+ for (const { row, link, occurredAtMs } of links) {
461
+ sequence = link.sequenceNumber;
462
+ await conn.query(`INSERT INTO ${this.chainTable}
463
+ (ac_tenant_id, ac_sequence, ac_al_uuid, ac_content_hash, ac_prev_hash,
464
+ ac_entry_hash, ac_occurred_at_ms, ac_sealed_at_ms)
465
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
466
+ tenant,
467
+ link.sequenceNumber,
468
+ row.al_uuid,
469
+ link.contentHash,
470
+ link.prevHash,
471
+ link.entryHash,
472
+ occurredAtMs,
473
+ sealedAtMs,
474
+ ]);
475
+ }
476
+ await conn.commit();
477
+ inTransaction = false;
478
+ }
479
+ catch (err) {
480
+ let rollbackFailed = false;
481
+ if (inTransaction) {
482
+ try {
483
+ await conn.rollback();
484
+ }
485
+ catch {
486
+ rollbackFailed = true;
487
+ }
488
+ }
489
+ this.recordFailure(tenant, sequence, err);
490
+ if (rollbackFailed)
491
+ throw new ConnectionUnusable();
492
+ }
493
+ }
494
+ /** Log a failed batch (once per tenant and reason per sweep) and park it if it repeats. */
495
+ recordFailure(tenant, sequence, err) {
496
+ const reason = auditSealErrorClass(err);
497
+ const repeating = isRepeating(err);
498
+ if (repeating && this.stuck.size >= exports.AUDIT_SEAL_STUCK && !this.stuck.has(tenant)) {
499
+ // Systemic failure: one count-only line per sweep, no per-tenant log, retried each tick.
500
+ if (!this.overflowLogged) {
501
+ this.overflowLogged = true;
502
+ this.logger.error(`audit sealer: ${exports.AUDIT_SEAL_STUCK} tenants are parked (the maximum); further ` +
503
+ 'tenants failing with a repeating error are retried every tick until the next sweep');
504
+ }
505
+ return;
506
+ }
507
+ if (repeating)
508
+ this.stuck.add(tenant);
509
+ this.logOnce(reason, tenant, `audit sealer: tenant ${JSON.stringify(tenant)} batch rolled back at sequence ${sequence} ` +
510
+ `(${reason}); ${repeating ? 'parked until the next sweep' : 'retried next tick'}`);
511
+ }
512
+ logOnce(reason, tenant, message) {
513
+ const key = `${reason}\u0000${tenant}`;
514
+ if (this.logged.has(key))
515
+ return;
516
+ this.logged.add(key);
517
+ this.logger.error(message);
518
+ }
519
+ }
520
+ exports.AuditChainSealer = AuditChainSealer;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `AUDIT_SEAL_*` env loader for the deferred audit sealer (audit-chain-store SPEC §5.I, §10,
3
+ * §13). Read once at boot by the persistence wiring; an invalid value fails the boot with the
4
+ * variable name and its bounds, never the value (it might be a misplaced secret). Unset, empty
5
+ * or whitespace-only ⇒ the default (as `loadMessageConfig`).
6
+ */
7
+ /** The two tunables of the sealer (the sealer itself cannot be switched off, SPEC §11). */
8
+ export interface AuditSealConfig {
9
+ /** Milliseconds between the end of one tick and the start of the next. */
10
+ readonly intervalMs: number;
11
+ /** Rows sealed per tenant per tick. */
12
+ readonly batch: number;
13
+ }
14
+ export declare const DEFAULT_AUDIT_SEAL_INTERVAL_MS = 2000;
15
+ export declare const MIN_AUDIT_SEAL_INTERVAL_MS = 500;
16
+ export declare const MAX_AUDIT_SEAL_INTERVAL_MS = 60000;
17
+ export declare const DEFAULT_AUDIT_SEAL_BATCH = 500;
18
+ export declare const MIN_AUDIT_SEAL_BATCH = 1;
19
+ export declare const MAX_AUDIT_SEAL_BATCH = 5000;
20
+ /** Load and validate `AUDIT_SEAL_INTERVAL_MS` and `AUDIT_SEAL_BATCH`. Throws on an invalid value. */
21
+ export declare function loadAuditSealConfig(env: NodeJS.ProcessEnv): AuditSealConfig;
22
+ //# sourceMappingURL=audit-seal-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-seal-config.d.ts","sourceRoot":"","sources":["../../src/audit/audit-seal-config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,2FAA2F;AAC3F,MAAM,WAAW,eAAe;IAC9B,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,uCAAuC;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,8BAA8B,OAAO,CAAC;AACnD,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAC9C,eAAO,MAAM,0BAA0B,QAAS,CAAC;AACjD,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAC5C,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAqBzC,qGAAqG;AACrG,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,eAAe,CAiB3E"}
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ /**
3
+ * `AUDIT_SEAL_*` env loader for the deferred audit sealer (audit-chain-store SPEC §5.I, §10,
4
+ * §13). Read once at boot by the persistence wiring; an invalid value fails the boot with the
5
+ * variable name and its bounds, never the value (it might be a misplaced secret). Unset, empty
6
+ * or whitespace-only ⇒ the default (as `loadMessageConfig`).
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.MAX_AUDIT_SEAL_BATCH = exports.MIN_AUDIT_SEAL_BATCH = exports.DEFAULT_AUDIT_SEAL_BATCH = exports.MAX_AUDIT_SEAL_INTERVAL_MS = exports.MIN_AUDIT_SEAL_INTERVAL_MS = exports.DEFAULT_AUDIT_SEAL_INTERVAL_MS = void 0;
10
+ exports.loadAuditSealConfig = loadAuditSealConfig;
11
+ exports.DEFAULT_AUDIT_SEAL_INTERVAL_MS = 2000;
12
+ exports.MIN_AUDIT_SEAL_INTERVAL_MS = 500;
13
+ exports.MAX_AUDIT_SEAL_INTERVAL_MS = 60_000;
14
+ exports.DEFAULT_AUDIT_SEAL_BATCH = 500;
15
+ exports.MIN_AUDIT_SEAL_BATCH = 1;
16
+ exports.MAX_AUDIT_SEAL_BATCH = 5000;
17
+ /** Plain decimal digits only (no sign, exponent, radix prefix or fraction). */
18
+ const PLAIN_DIGITS = /^[0-9]{1,10}$/;
19
+ function boundedInteger(env, name, fallback, min, max) {
20
+ const value = env[name]?.trim();
21
+ if (!value)
22
+ return fallback;
23
+ const n = PLAIN_DIGITS.test(value) ? Number(value) : NaN;
24
+ if (!Number.isSafeInteger(n) || n < min || n > max) {
25
+ throw new Error(`${name} must be a whole number between ${min} and ${max}`);
26
+ }
27
+ return n;
28
+ }
29
+ /** Load and validate `AUDIT_SEAL_INTERVAL_MS` and `AUDIT_SEAL_BATCH`. Throws on an invalid value. */
30
+ function loadAuditSealConfig(env) {
31
+ return {
32
+ intervalMs: boundedInteger(env, 'AUDIT_SEAL_INTERVAL_MS', exports.DEFAULT_AUDIT_SEAL_INTERVAL_MS, exports.MIN_AUDIT_SEAL_INTERVAL_MS, exports.MAX_AUDIT_SEAL_INTERVAL_MS),
33
+ batch: boundedInteger(env, 'AUDIT_SEAL_BATCH', exports.DEFAULT_AUDIT_SEAL_BATCH, exports.MIN_AUDIT_SEAL_BATCH, exports.MAX_AUDIT_SEAL_BATCH),
34
+ };
35
+ }