@jwilleke/ngdpbase-types 5.41.1 → 5.42.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 (34) hide show
  1. package/lib/src/managers/AddonsManager.d.ts +8 -0
  2. package/lib/src/managers/AttachmentManager.d.ts +59 -0
  3. package/lib/src/managers/AuditManager.d.ts +477 -0
  4. package/lib/src/managers/AuthManager.d.ts +247 -15
  5. package/lib/src/managers/AuthManager.js +588 -45
  6. package/lib/src/managers/ConsentManager.d.ts +15 -0
  7. package/lib/src/managers/ImportManager.d.ts +15 -0
  8. package/lib/src/providers/BaseAttachmentProvider.d.ts +4 -0
  9. package/lib/src/providers/BaseAuditProvider.d.ts +492 -0
  10. package/lib/src/providers/BaseAuthProvider.d.ts +2 -0
  11. package/lib/src/providers/BaseCredentialsProvider.d.ts +2 -2
  12. package/lib/src/providers/BaseCredentialsProvider.js +1 -1
  13. package/lib/src/providers/BasicAttachmentProvider.d.ts +9 -0
  14. package/lib/src/providers/FileCredentialsProvider.js +1 -1
  15. package/lib/src/providers/NullAuditProvider.d.ts +82 -0
  16. package/lib/src/providers/TotpAuthProvider.d.ts +55 -0
  17. package/lib/src/providers/TotpAuthProvider.js +145 -0
  18. package/lib/src/types/Consent.d.ts +17 -4
  19. package/lib/src/types/Consent.js +11 -1
  20. package/lib/src/types/Provider.d.ts +6 -0
  21. package/lib/src/types/express.d.ts +22 -0
  22. package/lib/src/utils/auditChain.d.ts +83 -0
  23. package/lib/src/utils/auditHeadWitness.d.ts +97 -0
  24. package/lib/src/utils/auditLifecycle.d.ts +71 -0
  25. package/lib/src/utils/browserDescription.d.ts +18 -0
  26. package/lib/src/utils/browserDescription.js +12 -0
  27. package/lib/src/utils/postureRecord.d.ts +73 -0
  28. package/lib/src/utils/privateStorePath.d.ts +6 -0
  29. package/lib/src/utils/privateStorePath.js +8 -0
  30. package/lib/src/utils/securityPosture.d.ts +52 -0
  31. package/lib/src/utils/tlsConfig.d.ts +59 -0
  32. package/lib/src/utils/totpSeedCipher.d.ts +4 -0
  33. package/lib/src/utils/totpSeedCipher.js +45 -0
  34. package/package.json +5 -2
@@ -12,7 +12,8 @@
12
12
  * multi-factor flow, the per-role check and the boot refusal follow under #1523.
13
13
  *
14
14
  * Built-in providers, each gated on its own config key and all registered
15
- * through the public {@link AuthManager.registerProvider} (#1050):
15
+ * through registerCoreProvider(), which shares the checks of the public
16
+ * {@link AuthManager.registerProvider} (#1050):
16
17
  * - PasswordAuthProvider (unless ngdpbase.auth.password.enabled is false)
17
18
  * - MagicLinkAuthProvider (ngdpbase.auth.magic-link.enabled)
18
19
  * - OidcAuthProvider, one per entry (ngdpbase.auth.oidc.providers; the old
@@ -21,8 +22,9 @@
21
22
  * - AuthentikBearerAuthProvider (ngdpbase.auth.authentik-bearer.enabled)
22
23
  * - AgentTokenAuthProvider (ngdpbase.auth.agent-token.enabled)
23
24
  *
24
- * Addons register through the same method, so the contributed path is the one
25
- * exercised on every boot rather than a second, less-travelled one.
25
+ * Addons register through registerProvider(), so the contributed path is the one
26
+ * exercised on every boot rather than a second, less-travelled one. Core's ids
27
+ * and the `oidc:` prefix are reserved for core there (#2223).
26
28
  *
27
29
  * Future providers (see #421):
28
30
  * - TotpAuthProvider
@@ -36,6 +38,7 @@ import type { PermissionSubject } from './UserManager.js';
36
38
  import type { JobContext } from '../context/JobContext.js';
37
39
  import type { Aal, AuthProvider, AuthInitiateContext, AuthVerifyCredentials, PhishingResistance, ReportedAssurance, ViaToken } from '../providers/BaseAuthProvider.js';
38
40
  import { type OutsideProviderIcon } from '../providers/OidcAuthProvider.js';
41
+ import { type BrowserIdentity } from '../utils/browserDescription.js';
39
42
  import { type PasskeyRelyingParty } from '../providers/PasskeyAuthProvider.js';
40
43
  import { type CredentialKind, type CredentialRecord } from '../providers/BaseCredentialsProvider.js';
41
44
  import { type ActorContext } from '../context/ActorContext.js';
@@ -140,7 +143,44 @@ export interface PendingSignIn {
140
143
  denied?: boolean;
141
144
  /** When the last approval message went out, for the resend limit. */
142
145
  lastSentAt?: number;
146
+ /**
147
+ * #1523 slice 1c: the highest `required-aal` among the person's roles, not
148
+ * capped by what the account can reach: the bar a remembered browser's skip
149
+ * must clear.
150
+ */
151
+ rolesAal?: Aal;
152
+ /** #1523 slice 1c: "Remember this browser" was ticked, with the version of its page the person was shown. */
153
+ remember?: {
154
+ version: string;
155
+ };
156
+ /** #1523 slice 1c: the one-time challenge a remembered browser signs; taken by its first use. */
157
+ deviceChallenge?: string;
158
+ /** #1523 slice 1c: the remembered device that proved itself, so the prompt is skipped. Not a factor. */
159
+ knownDevice?: string;
160
+ }
161
+ /** #1523 slice 1c: a remembered browser's `device` row secret: its public key and what it was issued to. */
162
+ export interface DeviceSecret extends BrowserIdentity {
163
+ /** The ECDSA P-256 public key, SPKI DER, base64url. Never a private key. */
164
+ publicKey: string;
165
+ /** The `amr` of the sign-in that remembered it. */
166
+ amr: string[];
167
+ /** RFC 3339: after this it no longer skips the prompt. */
168
+ expiresAt: string;
169
+ /**
170
+ * The account's password-change generation when it was remembered (#1482):
171
+ * a later password change retires it.
172
+ */
173
+ generation?: number;
174
+ /**
175
+ * The id of the audit record of the sign-in that remembered it (#1523,
176
+ * operator 2026-10-31), and how that sign-in was made, as the record says.
177
+ * A browser remembered before this has neither.
178
+ */
179
+ auditId?: string;
180
+ signInMethod?: string;
143
181
  }
182
+ /** What a remembered browser signs: this, then the challenge. Separates the signature from any other use of the key. */
183
+ export declare const KNOWN_DEVICE_SIGNED_PREFIX = "ngdpbase known device v1\n";
144
184
  /**
145
185
  * What an approval request says about a waiting sign-in (#1532): one source
146
186
  * for the message and the page.
@@ -297,6 +337,8 @@ export type OutsideSignIn =
297
337
  /** What the one completion step leaves for the route (#1743): where to go instead, when a pending identity waits to be confirmed. */
298
338
  export interface SignInCompleted {
299
339
  next?: string;
340
+ /** The id of the sign-in's audit record, when it was recorded (#1523: a remembered browser's row names it). */
341
+ auditId?: string;
300
342
  }
301
343
  /**
302
344
  * How a delegated credential is renewed (#1764): `fixed` ends on its date and
@@ -447,6 +489,8 @@ export interface ActingForChoices {
447
489
  export interface SignInRecord extends Assurance {
448
490
  provider: string;
449
491
  factors: SatisfiedFactor[];
492
+ /** #1523 slice 1c: the second-factor prompt was skipped on a remembered browser. It raises nothing. */
493
+ knownDevice?: true;
450
494
  /** When the sign-in completed, RFC 3339. */
451
495
  at: string;
452
496
  }
@@ -556,6 +600,11 @@ export interface AuthenticateResult {
556
600
  aal?: Aal;
557
601
  /** #1633: what an outside provider reported about its own sign-in, for the audit record. */
558
602
  reported?: ReportedValues;
603
+ /**
604
+ * #1523 slice 1c: the second-factor prompt was skipped because this browser
605
+ * is remembered. Not a factor: `factors` holds only what was entered.
606
+ */
607
+ knownDevice?: boolean;
559
608
  }
560
609
  declare class AuthManager extends BaseManager {
561
610
  private providers;
@@ -564,6 +613,12 @@ declare class AuthManager extends BaseManager {
564
613
  private factorCache;
565
614
  /** The credentials store (#1524); null when it could not be opened. */
566
615
  private credentials;
616
+ /**
617
+ * #421: TOTP, built for enrolment only. Not registered as a sign-in
618
+ * provider until the second-factor step can ask for it (slice 2b): offering
619
+ * it earlier would raise admins to AAL2 before they could use it.
620
+ */
621
+ private totp;
567
622
  /** `ngdpbase.auth.credentials.file`, resolved at start: what a backup copies (#1876). */
568
623
  private credentialsFile;
569
624
  constructor(engine: WikiEngine);
@@ -742,6 +797,32 @@ declare class AuthManager extends BaseManager {
742
797
  * the request — so a passkey made here works only on that hostname.
743
798
  */
744
799
  private registerPasskeys;
800
+ /** TOTP enrolment (#421), available whenever the credentials store is open. */
801
+ private openTotp;
802
+ /** Whether an authenticator app can be enrolled here (#421). */
803
+ totpAvailable(): boolean;
804
+ /**
805
+ * A new seed for `username` to add to an authenticator app (#421): the
806
+ * manual key, the `otpauth://` URI and that URI as an SVG QR code. Nothing
807
+ * is stored. `pending` is the seed sealed for this account: the caller keeps
808
+ * that, never the seed, (in the session) until a code confirms it. Refused
809
+ * while the account already has one.
810
+ */
811
+ totpEnrolmentStart(ctx: PermissionSubject, username: string): Promise<{
812
+ seed: string;
813
+ pending: string;
814
+ uri: string;
815
+ qrSvg: string;
816
+ }>;
817
+ /**
818
+ * Confirm an enrolment with a code from the app and store the seed,
819
+ * encrypted (#421). Returns the credential id, or null when the code does
820
+ * not match, or when `pending` is not a seed `totpEnrolmentStart` sealed
821
+ * for this account. The confirming code is spent: it cannot also sign
822
+ * anyone in.
823
+ */
824
+ totpEnrolmentConfirm(ctx: PermissionSubject, username: string, pending: string, code: string, label: string): Promise<string | null>;
825
+ private totpEnrolGate;
745
826
  private passkeyProvider;
746
827
  /** The host passkeys are tied to, or null when passkeys are off (#448). */
747
828
  passkeyRelyingParty(): PasskeyRelyingParty | null;
@@ -1096,7 +1177,10 @@ declare class AuthManager extends BaseManager {
1096
1177
  * - AAL1 otherwise.
1097
1178
  */
1098
1179
  outsideLevel(reported: ReportedValues | undefined): Pick<Factor, 'amr' | 'aal' | 'acr'>;
1099
- /** The audit record of one sign-in method's check (#1758): written here, never by a route. */
1180
+ /**
1181
+ * The audit record of one sign-in method's check (#1758): written here, never
1182
+ * by a route. Returns the record's id when it was recorded (#1523).
1183
+ */
1100
1184
  private auditCheck;
1101
1185
  /** Each role's `required-aal`, and whether the operator set it, read through the catalogue's owner (#1523, #448). */
1102
1186
  private requiredLevels;
@@ -1158,6 +1242,26 @@ declare class AuthManager extends BaseManager {
1158
1242
  };
1159
1243
  /** Every role whose configured level the available factors cannot reach, described (#1523). */
1160
1244
  unreachableRequiredAal(): string[];
1245
+ /**
1246
+ * The start-up check of the `ngdpbase.auth.factors` ids, once add-ons have
1247
+ * registered their providers (#1523 Q1, operator 2026-10-31). An id no
1248
+ * provider could ever register, such as a typo, refuses the boot when its
1249
+ * entry is on, and only warns when it is `enabled: false`. Returns the
1250
+ * problems that must refuse the boot.
1251
+ */
1252
+ checkFactorIds(): string[];
1253
+ /**
1254
+ * The same check for a value the configuration screen is about to save as
1255
+ * `ngdpbase.auth.factors` (#1523), so a typo is refused there rather than
1256
+ * at the next restart, which would refuse to start. `refused` names each
1257
+ * enabled unknown id; `ignored` each one whose entry is `enabled: false`.
1258
+ */
1259
+ checkFactorEntries(value: unknown): {
1260
+ refused: string[];
1261
+ ignored: string[];
1262
+ };
1263
+ /** The entries whose id no provider could ever register, split by whether the entry is on. */
1264
+ private unknownFactorIds;
1161
1265
  /**
1162
1266
  * The start-up check, once add-ons have registered their providers (#1523,
1163
1267
  * #448). Returns the problems that must refuse the boot: a level the operator
@@ -1335,9 +1439,9 @@ declare class AuthManager extends BaseManager {
1335
1439
  private warnedLegacyStepUp;
1336
1440
  /**
1337
1441
  * Whether `permission` needs a fresher factor than `signIn` holds (#1525).
1338
- * Fresh: a factor satisfied within the step-up window that reaches the
1339
- * level the person's roles require (at least 1 — a known device, aal 0,
1340
- * never counts). A delegated credential has no sign-in of its own, so it
1442
+ * Fresh: the factors satisfied within the step-up window together reach
1443
+ * the level the person's roles require (at least 1 — a known device, aal 0,
1444
+ * never counts); a password and an authenticator code reach AAL2 (#421). A delegated credential has no sign-in of its own, so it
1341
1445
  * never satisfies step-up.
1342
1446
  */
1343
1447
  stepUpNeeded(permission: string, signIn: SignInRecord | undefined, roles: readonly string[], delegated: boolean, now?: number, cap?: number): boolean;
@@ -1463,9 +1567,9 @@ declare class AuthManager extends BaseManager {
1463
1567
  /**
1464
1568
  * The second factors `username` has enrolled and can use now (#1523), in
1465
1569
  * `secondFactorOrder` after the pending sign-in's first step; without one,
1466
- * by `preference`. Only what the second step can run is offered — today the
1467
- * email link — and one that is not truly available (no mail transport) is
1468
- * not. Empty means the sign-in is one step, as before.
1570
+ * by `preference`. Only what the second step can run is offered — the
1571
+ * email link and an authenticator app (#421) — and one that is not truly
1572
+ * available (no mail transport) is not. Empty means the sign-in is one step, as before.
1469
1573
  */
1470
1574
  secondFactorsFor(username: string, signIn?: Pick<PendingSignIn, 'first' | 'requiredAal'>): AvailableSecondFactor[];
1471
1575
  /**
@@ -1546,6 +1650,30 @@ declare class AuthManager extends BaseManager {
1546
1650
  private approvalContext;
1547
1651
  /** Approve the waiting sign-in from its link (the button press). Single use; returns the username, or null. */
1548
1652
  approveByToken(token: string, presser?: ApprovalVisitor): Promise<string | null>;
1653
+ /**
1654
+ * The authenticator app's code for the waiting sign-in (#421 slice 2b). Its
1655
+ * check goes through `authenticate`, which records a wrong code as a failed
1656
+ * sign-in; a right one approves the pending sign-in, which `completeTwoStep`
1657
+ * then finishes with both factors. `unavailable` when the sign-in is gone,
1658
+ * already decided, or the account has no authenticator app offered here.
1659
+ * The caller throttles wrong codes as it does wrong passwords.
1660
+ */
1661
+ approveByTotp(handle: string, binding: string, code: string, from?: ApprovalVisitor): Promise<'approved' | 'wrong' | 'unavailable'>;
1662
+ /**
1663
+ * Whether step-up offers `username` the authenticator app's code (#421
1664
+ * slice 2c): as at the second step, an app enrolled and its
1665
+ * `ngdpbase.auth.factors` entry enabled.
1666
+ */
1667
+ stepUpTotpOffered(username: string): boolean;
1668
+ /**
1669
+ * Step-up with the authenticator app (#421 slice 2c). The code is checked
1670
+ * through `authenticate`, as at the second step of sign-in, so a wrong one
1671
+ * is recorded as a failed re-check. With `password` (a role that needs
1672
+ * AAL2), the password is checked first, and the result carries both
1673
+ * factors, which together reach AAL2. `unavailable` when the app is not
1674
+ * offered here. The caller throttles wrong answers as it does wrong passwords.
1675
+ */
1676
+ reauthenticateByTotp(username: string, code: string, password: string | null, attempt?: AuthAttempt): Promise<AuthenticateResult | 'unavailable'>;
1549
1677
  /** Refuse the waiting sign-in from its link (Deny). Returns the username, or null. */
1550
1678
  denyByToken(token: string, presser?: ApprovalVisitor): Promise<string | null>;
1551
1679
  /**
@@ -1555,7 +1683,102 @@ declare class AuthManager extends BaseManager {
1555
1683
  completeTwoStep(handle: string, binding: string): {
1556
1684
  result: AuthenticateResult;
1557
1685
  privateStoreHandle?: string;
1686
+ remember?: {
1687
+ version: string;
1688
+ };
1558
1689
  } | null;
1690
+ /** How many days a remembered browser stays remembered: `ngdpbase.auth.known-device.remember-days`; 0 turns remembering off. */
1691
+ rememberDays(): number;
1692
+ /**
1693
+ * Whether a remembered browser may skip this sign-in's second step: the
1694
+ * factors entered already reach the highest `required-aal` among the
1695
+ * person's roles (operator, 2026-11-01). A person whose roles need AAL2 is
1696
+ * always asked.
1697
+ */
1698
+ knownDeviceMaySkip(p: Pick<PendingSignIn, 'first' | 'rolesAal'>): boolean;
1699
+ /** Whether "Remember this browser" is offered at this second step: only where it could ever skip one. */
1700
+ rememberOffered(p: Pick<PendingSignIn, 'first' | 'rolesAal'>): boolean;
1701
+ /** "Remember this browser" was ticked at the second step, having been shown `version` of its page. */
1702
+ noteRemember(handle: string, binding: string, version: string): void;
1703
+ /**
1704
+ * When a remembered browser stops skipping the prompt: its stored expiry,
1705
+ * or its creation plus the current `remember-days`, whichever is sooner,
1706
+ * so lowering the setting shortens browsers already remembered.
1707
+ */
1708
+ private deviceExpiry;
1709
+ /**
1710
+ * `username`'s remembered browsers that have not expired. With the
1711
+ * account's password-change `generation`, also only those remembered
1712
+ * since the last password change (#1482).
1713
+ */
1714
+ rememberedDevices(username: string, now?: number, generation?: number): Array<{
1715
+ row: CredentialRecord;
1716
+ device: DeviceSecret;
1717
+ }>;
1718
+ private accountGeneration;
1719
+ /**
1720
+ * Drop `username`'s remembered browsers that no longer count: expired,
1721
+ * unreadable, or remembered before the last password change. Each is
1722
+ * recorded as `credential-remove` and its consent withdrawn. Returns the
1723
+ * ones that still count.
1724
+ */
1725
+ pruneRememberedDevices(username: string, now?: number): Promise<Array<{
1726
+ row: CredentialRecord;
1727
+ device: DeviceSecret;
1728
+ }>>;
1729
+ /** Withdraw the "remember this browser" consent bound to `deviceId`; a failure is logged, the browser stays removed. */
1730
+ private withdrawRememberConsent;
1731
+ /**
1732
+ * A one-time challenge for a remembered browser to sign, or null when the
1733
+ * skip cannot apply (the roles need more, or no browser is remembered).
1734
+ * A new challenge replaces the last.
1735
+ */
1736
+ knownDeviceChallenge(handle: string, binding: string): string | null;
1737
+ /**
1738
+ * A remembered browser's answer to the challenge (#1523 slice 1c). It
1739
+ * counts only when the device is this person's, has not expired, the
1740
+ * signature over the challenge verifies with its public key, and the
1741
+ * browser and system still match what it was issued to, and the audit
1742
+ * record its row names says the same as the row (#1523, operator
1743
+ * 2026-10-31). Any failure is recorded and the prompt stays. A row whose
1744
+ * audit record is missing or disagrees is also a security event; a row
1745
+ * remembered before rows named their record is removed, so the browser is
1746
+ * asked once and may be remembered again. The challenge is spent either way.
1747
+ */
1748
+ proveKnownDevice(handle: string, binding: string, deviceId: string, signature: string, from: {
1749
+ ip?: string;
1750
+ userAgent?: string;
1751
+ platformHint?: string;
1752
+ }): Promise<boolean>;
1753
+ /**
1754
+ * How a remembered browser's row disagrees with the audit record it names
1755
+ * (#1523), or null when they agree: the record is the account's sign-in,
1756
+ * made the way the row says, with the same factors. `'no record named'` for
1757
+ * a row remembered before rows named one.
1758
+ */
1759
+ private deviceRecordDisagreement;
1760
+ /**
1761
+ * Remember this browser for `ctx`'s person (#1523 slice 1c, Q5): the
1762
+ * consent record first, bound to the device id, then the `device` row with
1763
+ * the public key, the browser and system, and when it expires. Refused
1764
+ * when the key is not an ECDSA P-256 public key, the browser cannot be
1765
+ * told, the page changed since it was shown, or consent cannot be recorded.
1766
+ * The row names the audit record of the sign-in (`auditId`, from
1767
+ * `completeSignIn`), which must be readable and be this person's sign-in
1768
+ * with these factors; otherwise nothing is remembered (#1523).
1769
+ */
1770
+ rememberBrowser(ctx: {
1771
+ username: string;
1772
+ } & Record<string, unknown>, input: {
1773
+ publicKey: string;
1774
+ version: string;
1775
+ userAgent?: string;
1776
+ platformHint?: string;
1777
+ amr: string[];
1778
+ auditId?: string;
1779
+ }): Promise<{
1780
+ deviceId: string;
1781
+ } | null>;
1559
1782
  /**
1560
1783
  * The waiting browser gave up the sign-in (Cancel, #1532): dropped, its
1561
1784
  * keys locked, and recorded as `signinapproval-abandon`.
@@ -1636,9 +1859,11 @@ declare class AuthManager extends BaseManager {
1636
1859
  /**
1637
1860
  * Register an authentication provider (#1050).
1638
1861
  *
1639
- * The built-ins call this during `initialize()`, so the path an addon uses is
1640
- * the path exercised on every boot rather than a second, less-travelled one.
1641
- * Config gating stays with the caller: this method decides nothing about
1862
+ * The built-ins register during `initialize()` through registerCoreProvider(),
1863
+ * which shares every check below except the reserved ids, so the path an addon
1864
+ * uses is the path exercised on every boot rather than a second, less-travelled
1865
+ * one. Core ids and the `oidc:` prefix are reserved: an addon registering one is
1866
+ * refused, whether or not core registered it on this site (#2223). Config gating stays with the caller: this method decides nothing about
1642
1867
  * whether a provider *should* be active, only that it is.
1643
1868
  *
1644
1869
  * ## Duplicate ids: first registration wins
@@ -1666,10 +1891,17 @@ declare class AuthManager extends BaseManager {
1666
1891
  * otherwise here would be worse than the gap.
1667
1892
  *
1668
1893
  * @param provider the provider to register
1669
- * @param source attribution for logs — an addon name, or 'built-in'
1670
- * @returns true if registered; false if rejected as malformed or duplicate
1894
+ * @param source attribution for logs: the add-on's name
1895
+ * @returns true if registered; false if rejected as reserved, malformed or duplicate
1671
1896
  */
1672
1897
  registerProvider(provider: AuthProvider, source?: string): boolean;
1898
+ /**
1899
+ * Core's own registrations (#2223): the one way a reserved id gets in. Called
1900
+ * only from initialize() and the helpers it runs; add-ons get the AuthManager
1901
+ * through the engine and call registerProvider(), which refuses these ids.
1902
+ */
1903
+ private registerCoreProvider;
1904
+ private addProvider;
1673
1905
  /**
1674
1906
  * Begin a redirect-based flow; returns the URL to send the browser to (#1049).
1675
1907
  *