@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
@@ -95,6 +95,8 @@ declare class ConsentManager extends BaseManager {
95
95
  * made on an earlier account of the same name is never theirs (#1937).
96
96
  */
97
97
  private recordsOf;
98
+ /** The acceptances still standing: each `accept` line with no later `withdraw` of the same document, version and subject. */
99
+ private standingOf;
98
100
  private accountOf;
99
101
  /**
100
102
  * The documents of `scope` that `username` has not accepted at their
@@ -134,6 +136,19 @@ declare class ConsentManager extends BaseManager {
134
136
  * @returns The records written; empty when there was nothing to accept
135
137
  */
136
138
  accept(ctx: ActorContext, scope: ConsentScope, seen: ConsentSeen[], how: string, subject?: string): Promise<ConsentRecord[]>;
139
+ /**
140
+ * Withdraw `username`'s acceptances in `scope` about `subject` (#1523 slice
141
+ * 1c, Q5: a remembered browser is withdrawn by removing it). Recorded first
142
+ * as `consent-withdraw` (on-failure refuse: nothing is withdrawn), then a
143
+ * `withdraw` line is appended for each acceptance, so the history keeps
144
+ * both. Called by the code that removed the thing the acceptance was
145
+ * about, which has already decided the caller may; nothing to withdraw is
146
+ * not an error.
147
+ *
148
+ * @param why Why, for the record: `removed`, `expired`, `password-changed`
149
+ * @returns The `withdraw` lines written
150
+ */
151
+ withdraw(username: string, scope: ConsentScope, subject: string, ctx: ActorContext, why: string): Promise<ConsentRecord[]>;
137
152
  /**
138
153
  * The account `username` was deleted (`AuthManager.closeAccess`): its
139
154
  * records go with it, after `consent-withdraw` names what it had accepted
@@ -312,6 +312,10 @@ declare class ImportManager extends BaseManager {
312
312
  private readonly iceRuns;
313
313
  /** ICE (#2171): a rechecking run's store, as fingerprint → id, read once per run and store; dropped when the run ends. */
314
314
  private readonly storeHoldings;
315
+ /** ICE (#2188): what a run gives files back their ids with, by run and store; dropped when the run ends. */
316
+ private readonly storeRestoring;
317
+ /** ICE (#2171): the fingerprints of the files a rechecking run put back into a store, by run and store; dropped when the run ends. */
318
+ private readonly storeRestored;
315
319
  /** ICE: work waiting for the background job, each with its own context: an import run or an export (#2120). */
316
320
  private readonly iceQueue;
317
321
  /** ICE: true while a background job is draining the queue. */
@@ -701,6 +705,17 @@ declare class ImportManager extends BaseManager {
701
705
  * save door. Nothing here logs a page's or a file's name (#1461, #2110).
702
706
  */
703
707
  private receiveTakeoutEntry;
708
+ /**
709
+ * Point every page of the user's store `store` that links a file by an id
710
+ * in `ids` at the file's id now (#2188), through the page door. Only an id
711
+ * no store of the user's holds is repointed: a link that opens their own
712
+ * file, say one that moved to another of their stores keeping its id, is
713
+ * left. An id held only by the shared pool or someone else's store is not
714
+ * the page's file, so it is repointed.
715
+ * A page that cannot be saved is passed over and named in `failed`; the
716
+ * rest are still done. Pages are named by uuid, never by title.
717
+ */
718
+ private relinkStorePages;
704
719
  /** A takeout's page into the store, as #1472 decided: see {@link privateStoreReceiver}. */
705
720
  private saveTakeoutPage;
706
721
  /**
@@ -160,6 +160,10 @@ declare abstract class BaseAttachmentProvider extends BaseProvider implements At
160
160
  filesInStoreForPage(_location: StoreFileLocation, _pageName: string): Promise<StoreFileEntry[]>;
161
161
  /** Delete a file from a store: its bytes and its index entry. Null when the store does not list it. */
162
162
  deleteFileInStore(_location: StoreFileLocation, _id: string): Promise<StoreFileEntry | null>;
163
+ /** The ids a takeout import gave a store's files that could not keep their own, as takeout id → id here (#2188). */
164
+ readStoreRenumbered(_location: StoreFileLocation): Promise<Map<string, string>>;
165
+ /** Adds takeout id → id pairs to {@link readStoreRenumbered}'s (#2188). */
166
+ noteStoreRenumbered(_location: StoreFileLocation, _pairs: ReadonlyMap<string, string>): Promise<void>;
163
167
  /**
164
168
  * Refresh internal cache/index
165
169
  * Re-scans storage and rebuilds indexes
@@ -0,0 +1,492 @@
1
+ import { AuditEvent } from '../types/index.js';
2
+ import type { ProviderInfo } from '../types/Provider.js';
3
+ import type { WikiEngine } from '../types/WikiEngine.js';
4
+ import BaseProvider, { type ProviderDurability } from './BaseProvider.js';
5
+ import { type ActorContext } from '../context/ActorContext.js';
6
+ /**
7
+ * Audit search filters
8
+ */
9
+ export interface AuditFilters {
10
+ /** Filter by username — the account acted on */
11
+ user?: string;
12
+ /** Filter by the person who actually acted, under a proxy grant (#1762) */
13
+ actor?: string;
14
+ /** Filter by event type */
15
+ eventType?: string;
16
+ /** Filter by result (allow, deny, error) */
17
+ result?: string;
18
+ /** Filter by severity level */
19
+ severity?: string;
20
+ /** Start date filter */
21
+ startDate?: Date;
22
+ /** End date filter */
23
+ endDate?: Date;
24
+ /** Filter by resource */
25
+ resource?: string;
26
+ /** Filter by action */
27
+ action?: string;
28
+ /** Maximum results to return */
29
+ limit?: number;
30
+ /** Results offset for pagination */
31
+ offset?: number;
32
+ /** Sort field */
33
+ sortBy?: string;
34
+ /** Sort order (asc/desc) */
35
+ sortOrder?: 'asc' | 'desc';
36
+ }
37
+ /**
38
+ * Audit search results
39
+ */
40
+ export interface AuditSearchResults {
41
+ /** Array of audit events */
42
+ results: AuditEvent[];
43
+ /** Total matching events */
44
+ total: number;
45
+ /** Result limit */
46
+ limit: number;
47
+ /** Result offset */
48
+ offset: number;
49
+ /** Whether more results are available */
50
+ hasMore: boolean;
51
+ }
52
+ /**
53
+ * Audit statistics
54
+ */
55
+ export interface AuditStats {
56
+ /** Total number of events */
57
+ totalEvents: number;
58
+ /** Events by type */
59
+ eventsByType?: Record<string, number>;
60
+ /** Events by result */
61
+ eventsByResult?: Record<string, number>;
62
+ /** Events by severity */
63
+ eventsBySeverity?: Record<string, number>;
64
+ /** Events by user */
65
+ eventsByUser?: Record<string, number>;
66
+ /** Recent activity entries */
67
+ recentActivity?: AuditEvent[];
68
+ /** Number of security incidents (high/critical severity) */
69
+ securityIncidents?: number;
70
+ /** Additional statistics */
71
+ [key: string]: unknown;
72
+ }
73
+ /**
74
+ * Paging for the entries about one person (#1537): the newest first.
75
+ */
76
+ export interface PersonEntriesOptions {
77
+ limit?: number;
78
+ offset?: number;
79
+ /**
80
+ * The user's identity `@id` (#2041). From that release on a row names the
81
+ * owner of its resource by `@id` in `metadata.owner`; rows written before
82
+ * name the owner's username. A row is about the user under either.
83
+ */
84
+ ownerId?: string;
85
+ }
86
+ /** Where the chain head is published (#1138), shared by every storing provider. */
87
+ export declare const WITNESS_DESTINATION_KEY = "ngdpbase.audit.chain-witness.destination";
88
+ export declare const WITNESS_INTERVAL_KEY = "ngdpbase.audit.chain-witness.interval-minutes";
89
+ /** #1122, #1537: how often retention runs after boot. Hourly is far finer than a day-scale window. */
90
+ export declare const AUDIT_RETENTION_INTERVAL_MS: number;
91
+ /**
92
+ * The shape every storing provider writes (#1119, #1537): one shape, so a
93
+ * record reads, exports and verifies the same whichever provider wrote it.
94
+ */
95
+ export interface StoredAuditRecord {
96
+ id: string;
97
+ timestamp: string;
98
+ level?: string;
99
+ eventType: string;
100
+ user?: string;
101
+ /** Who actually did it when that is not `user` — under a proxy grant (#1762). */
102
+ actor?: string;
103
+ userId?: string;
104
+ sessionId?: string;
105
+ ipAddress?: string;
106
+ userAgent?: string;
107
+ resource?: string;
108
+ resourceType?: string;
109
+ action?: string;
110
+ result?: string;
111
+ reason?: string;
112
+ policyId?: string;
113
+ policyName?: string;
114
+ context?: Record<string, unknown>;
115
+ metadata?: Record<string, unknown>;
116
+ duration?: number;
117
+ severity?: string;
118
+ }
119
+ /**
120
+ * Normalise an incoming event into the stored record shape (#1119). Runs
121
+ * BEFORE the base stamps the chain: the id and timestamp assigned here are
122
+ * part of what gets hashed, and assigning them afterwards would mean the hash
123
+ * covered a record different from the one written.
124
+ */
125
+ export declare function toStoredAuditRecord(auditEvent: Record<string, unknown>): StoredAuditRecord;
126
+ /**
127
+ * The owner of the resource a record is about (#1537, #1825): `metadata.owner`,
128
+ * where token, private-store and share records carry it today. Null when the
129
+ * record names none.
130
+ */
131
+ export declare function recordOwner(record: Record<string, unknown>): string | null;
132
+ /**
133
+ * The username of the owner of the resource a record is about (#2041):
134
+ * `metadata.ownerUser`, written beside `metadata.owner` (the identity `@id`)
135
+ * so the rows about an account still list after it is deleted. Null when the
136
+ * record names none (every row written before #2041).
137
+ */
138
+ export declare function recordOwnerUser(record: Record<string, unknown>): string | null;
139
+ /**
140
+ * Whether a record is about the user `person` (#1537): the user is its `user`
141
+ * (the account acted on, or the user represented under a proxy grant), its
142
+ * `actor` (who actually did it, under a proxy grant), or the owner of the
143
+ * resource it names ({@link recordOwner}). The owner is the user's identity
144
+ * `@id` (`ownerId`) on rows written from #2041 on, with their username
145
+ * beside it ({@link recordOwnerUser}), and their username on rows written
146
+ * before, which are never rewritten; any of them matches. The username
147
+ * beside the `@id` is what lists the rows of an account deleted since.
148
+ */
149
+ export declare function recordIsAbout(record: Record<string, unknown>, person: string, ownerId?: string): boolean;
150
+ /**
151
+ * Audit backup data
152
+ */
153
+ export interface AuditBackupData {
154
+ /** Provider name */
155
+ provider: string;
156
+ /** Initialization state */
157
+ initialized: boolean;
158
+ /** Backup timestamp */
159
+ timestamp: string;
160
+ /** Additional backup data */
161
+ [key: string]: unknown;
162
+ }
163
+ /**
164
+ * BaseAuditProvider - Abstract base class for audit providers
165
+ *
166
+ * Provides the interface that all audit providers must implement.
167
+ * Follows the provider pattern established in CacheManager, AttachmentManager, and PageManager.
168
+ *
169
+ * Audit providers implement different storage backends (file, database, cloud logging)
170
+ *
171
+ * @class BaseAuditProvider
172
+ * @abstract
173
+ *
174
+ * @property {WikiEngine} engine - Reference to the wiki engine
175
+ * @property {boolean} initialized - Whether provider has been initialized
176
+ *
177
+ * @see {@link FileAuditProvider} for file-based implementation
178
+ * @see {@link DatabaseAuditProvider} for database implementation
179
+ * @see {@link AuditManager} for usage
180
+ *
181
+ * @example
182
+ * class MyAuditProvider extends BaseAuditProvider {
183
+ * async initialize(): Promise<void> {
184
+ * const config = this.engine.getManager('ConfigurationManager');
185
+ * this.auditPath = config.getProperty('audit.path');
186
+ * this.initialized = true;
187
+ * }
188
+ *
189
+ * async logAuditEvent(event: AuditEvent): Promise<string> {
190
+ * // Implementation
191
+ * return event.id;
192
+ * }
193
+ * }
194
+ */
195
+ /** What a provider reports about itself. */
196
+ export interface AuditReport {
197
+ /** Alteration of a stored record is detectable. Deletion of the tail is not — see #1138. */
198
+ tamperEvident: boolean;
199
+ /** How records reach disk, or null when the provider has not stated it. */
200
+ durability: ProviderDurability | null;
201
+ queryable: boolean;
202
+ /**
203
+ * Where and when the chain head was last published, or null when nothing
204
+ * publishes it (#1138).
205
+ *
206
+ * NOT a boolean. The old `offBox: boolean` was hardcoded `false`, and making
207
+ * it `true` would have been a claim nothing on this machine can establish —
208
+ * no code here can verify that a path leaves the box. So this reports the
209
+ * facts and the reader judges, exactly as `durability` does after #1148.
210
+ *
211
+ * Without a witness, truncation of the tail is undetectable: removing records
212
+ * from the end breaks no link.
213
+ */
214
+ headWitness: {
215
+ destination: string;
216
+ intervalMinutes: number;
217
+ lastPublishedAt: string | null;
218
+ lastSeq: number | null;
219
+ } | null;
220
+ /**
221
+ * Whether the records are encrypted where they are stored (#1537). Stated
222
+ * only by a provider that knows: the database provider reports whether the
223
+ * application database is SQLCipher-encrypted. Absent means not stated.
224
+ */
225
+ encryptedAtRest?: boolean;
226
+ }
227
+ declare abstract class BaseAuditProvider extends BaseProvider {
228
+ /** Reference to the wiki engine */
229
+ protected engine: WikiEngine;
230
+ /** Whether provider has been initialized */
231
+ protected initialized: boolean;
232
+ /**
233
+ * Create a new audit provider
234
+ *
235
+ * @constructor
236
+ * @param {WikiEngine} engine - The WikiEngine instance
237
+ * @throws {Error} If engine is not provided
238
+ */
239
+ constructor(engine: WikiEngine);
240
+ /**
241
+ * Initialize the audit provider
242
+ *
243
+ * Implementations should load configuration from ConfigurationManager:
244
+ * const configManager = this.engine.getManager('ConfigurationManager');
245
+ * const value = configManager.getProperty('key', 'default');
246
+ *
247
+ * Do NOT read configuration files directly.
248
+ *
249
+ * @async
250
+ * @abstract
251
+ * @returns {Promise<void>}
252
+ * @throws {Error} Always throws - must be implemented by subclass
253
+ */
254
+ abstract initialize(): Promise<void>;
255
+ /**
256
+ * Get provider information
257
+ *
258
+ * @returns {ProviderInfo} Provider metadata
259
+ */
260
+ getProviderInfo(): ProviderInfo;
261
+ /**
262
+ * What this provider actually guarantees (#1119).
263
+ *
264
+ * Reportable rather than inferable. A security property that has to be
265
+ * deduced from configuration is not one an instance can be assessed on — so
266
+ * an operator or an assessor asks the system instead of reading the config
267
+ * and hoping.
268
+ *
269
+ * `tamperEvident` follows from the chain and is therefore true for every
270
+ * storing provider and false for the inert one, without a subclass having to
271
+ * remember to say so.
272
+ */
273
+ getGuarantees(): AuditReport;
274
+ /** Sequence of the last record this provider stamped (#1119). */
275
+ protected chainSeq: number;
276
+ /** Hash of the last record this provider stamped (#1119). */
277
+ protected chainPrevHash: string;
278
+ /**
279
+ * Whether this provider chains its records (#1119).
280
+ *
281
+ * True for anything that stores events. `NullAuditProvider` overrides it: it
282
+ * keeps nothing, so it has nothing to chain — a legitimate state rather than
283
+ * a hole, provided the instance cannot then CLAIM tamper evidence, which is
284
+ * what `getProviderInfo().guarantees` is for.
285
+ */
286
+ protected chainEnabled(): boolean;
287
+ /**
288
+ * Resume the chain from storage, or null to start a new one (#1119).
289
+ *
290
+ * Called once, lazily, before the first record is stamped. A provider that
291
+ * cannot resume returns null and starts a fresh chain — honest, and visible
292
+ * as a `seq` restarting at 1 rather than as a silent break.
293
+ *
294
+ * @protected
295
+ */
296
+ protected loadChainHead(): Promise<{
297
+ seq: number;
298
+ hash: string;
299
+ } | null>;
300
+ /** Whether the chain head has been resumed yet. */
301
+ private chainResumed;
302
+ /**
303
+ * Record an audit event.
304
+ *
305
+ * __Deliberately concrete and not overridable in spirit.__ Integrity is a
306
+ * property of the CONTRACT, not of one implementation: stamped in a subclass
307
+ * it would protect that subclass only, and whether an instance were
308
+ * tamper-evident would depend on which storage backend was configured. "It
309
+ * depends on your backend" is not an answer that survives an assessment.
310
+ *
311
+ * A subclass implements {@link writeEvent} — storage — and never sees the
312
+ * un-stamped record, so it cannot skip the integrity step. Adding a fifth
313
+ * provider gets tamper evidence without having to remember it.
314
+ *
315
+ * @param auditEvent - The event to record
316
+ * @returns The stored event's id
317
+ */
318
+ logAuditEvent(auditEvent: AuditEvent): Promise<string>;
319
+ /**
320
+ * Begin a new chain, recording that the old one was abandoned (#1124).
321
+ *
322
+ * __Never called automatically.__ A system that silently repairs its own
323
+ * audit chain is worse than one that stays visibly broken, so this exists
324
+ * only for an explicit operator action — `npm run audit:restart-chain`.
325
+ *
326
+ * The marker is written INTO the log as an ordinary record, which is what
327
+ * stops it being an escape hatch: an attacker who wants a clean chain has to
328
+ * leave a record saying they broke it. It does not repair the past — the
329
+ * abandoned segment stays in the file and stays unverifiable. It only says
330
+ * the discontinuity is known, by whom, and why.
331
+ *
332
+ * @param reason - Why the chain is being restarted. Recorded verbatim.
333
+ * @param ctx - Who authorised it (#1179): the operator's context, never a bare name.
334
+ * @returns The id of the marker record.
335
+ */
336
+ restartChain(reason: string, ctx: ActorContext): Promise<string>;
337
+ /** #1138: when the chain head was last published, and what was published. */
338
+ private witnessPublishedAtMs;
339
+ private witnessLast;
340
+ /**
341
+ * Publish the chain head to the configured destination (#1138), after the
342
+ * records it names are stored — never before, or the next verification
343
+ * would report truncation.
344
+ *
345
+ * The chain cannot detect truncation of its own tail — removing records from
346
+ * the end breaks no link — and it cannot be fixed locally, because an
347
+ * attacker who owns the machine owns anything the machine wrote locally. So
348
+ * the head goes to wherever the operator points it, and this code takes no
349
+ * view on whether that is genuinely off-box (D13): it reports the
350
+ * destination and never claims what the destination is (D21).
351
+ *
352
+ * Failure to publish is logged and swallowed. An instance that refuses to
353
+ * serve because it could not write a witness would be trading a working
354
+ * system for a detection property, which is the wrong way round — and the
355
+ * absence of a fresh witness is itself visible in getGuarantees().
356
+ */
357
+ protected publishChainHead(): Promise<void>;
358
+ /**
359
+ * Where the chain head is published, or null when nothing publishes it
360
+ * (#1138). Facts, not a claim — see the note on AuditReport.headWitness.
361
+ */
362
+ protected headWitnessReport(): AuditReport['headWitness'];
363
+ /**
364
+ * One export format for every provider: JSON as stored, or CSV through
365
+ * ExportManager's writer — quoting, and no cell a spreadsheet would run as a
366
+ * formula.
367
+ */
368
+ protected formatExport(results: readonly AuditEvent[], format: 'json' | 'csv'): string;
369
+ /**
370
+ * The entries about one person, newest first, a page at a time (#1537):
371
+ * those where the person is the `user`, the `actor`, or the resource's
372
+ * owner ({@link recordIsAbout}). A provider that cannot answer says so,
373
+ * rather than returning an empty page that reads as "nothing happened".
374
+ */
375
+ searchEntriesAbout(_person: string, _options?: PersonEntriesOptions): Promise<AuditSearchResults>;
376
+ /**
377
+ * The stored record with this id, or null (#1523: a remembered browser's
378
+ * row names the audit record of its sign-in, and is checked against it).
379
+ * Null too from a provider that cannot look a record up by id.
380
+ */
381
+ findRecordById(_id: string): Promise<Record<string, unknown> | null>;
382
+ /**
383
+ * Normalise an incoming event into the record that will be stored (#1119).
384
+ *
385
+ * Identity by default. A provider that assigns its own id, timestamp or
386
+ * field shape overrides this rather than doing it in {@link writeEvent},
387
+ * because the stamp must cover exactly what is written.
388
+ *
389
+ * @protected
390
+ */
391
+ protected prepareRecord(record: Record<string, unknown>): Record<string, unknown>;
392
+ /**
393
+ * Store an already-stamped record.
394
+ *
395
+ * @async
396
+ * @abstract
397
+ * @param record - The record to store, with seq/prevHash/hash already applied
398
+ * @returns {Promise<string>} Event ID
399
+ */
400
+ abstract writeEvent(record: Record<string, unknown>): Promise<string>;
401
+ /**
402
+ * Search audit logs
403
+ *
404
+ * @async
405
+ * @abstract
406
+ * @param {AuditFilters} filters - Search filters
407
+ * @param {Record<string, any>} options - Search options
408
+ * @returns {Promise<AuditSearchResults>} Search results
409
+ * @throws {Error} Always throws - must be implemented by subclass
410
+ */
411
+ abstract searchAuditLogs(filters?: AuditFilters, options?: Record<string, unknown>): Promise<AuditSearchResults>;
412
+ /**
413
+ * Get audit statistics
414
+ *
415
+ * @async
416
+ * @abstract
417
+ * @param {AuditFilters} filters - Optional filters
418
+ * @returns {Promise<AuditStats>} Audit statistics
419
+ * @throws {Error} Always throws - must be implemented by subclass
420
+ */
421
+ abstract getAuditStats(filters?: AuditFilters): Promise<AuditStats>;
422
+ /**
423
+ * Export audit logs
424
+ *
425
+ * @async
426
+ * @abstract
427
+ * @param {AuditFilters} filters - Export filters
428
+ * @param {string} format - Export format ('json', 'csv')
429
+ * @returns {Promise<string>} Exported data
430
+ * @throws {Error} Always throws - must be implemented by subclass
431
+ */
432
+ abstract exportAuditLogs(filters?: AuditFilters, format?: 'json' | 'csv'): Promise<string>;
433
+ /**
434
+ * Flush pending audit events to storage
435
+ *
436
+ * @async
437
+ * @abstract
438
+ * @returns {Promise<void>}
439
+ * @throws {Error} Always throws - must be implemented by subclass
440
+ */
441
+ abstract flush(): Promise<void>;
442
+ /**
443
+ * Clean up old audit logs based on retention policy
444
+ *
445
+ * @async
446
+ * @abstract
447
+ * @returns {Promise<void>}
448
+ * @throws {Error} Always throws - must be implemented by subclass
449
+ */
450
+ abstract cleanup(): Promise<void>;
451
+ /**
452
+ * Check if the audit provider is healthy
453
+ *
454
+ * @async
455
+ * @abstract
456
+ * @returns {Promise<boolean>} True if healthy
457
+ * @throws {Error} Always throws - must be implemented by subclass
458
+ */
459
+ abstract isHealthy(): Promise<boolean>;
460
+ /**
461
+ * Close/cleanup the audit provider
462
+ *
463
+ * @async
464
+ * @abstract
465
+ * @returns {Promise<void>}
466
+ * @throws {Error} Always throws - must be implemented by subclass
467
+ */
468
+ abstract close(): Promise<void>;
469
+ /**
470
+ * Backup audit configuration and state (optional)
471
+ *
472
+ * Default implementation provides basic backup data.
473
+ * Subclasses can override to include provider-specific data.
474
+ *
475
+ * @async
476
+ * @returns {Promise<AuditBackupData>} Backup data
477
+ */
478
+ backup(): Promise<AuditBackupData>;
479
+ /**
480
+ * Restore audit from backup (optional)
481
+ *
482
+ * Default implementation does nothing.
483
+ * Subclasses can override if they support restore functionality.
484
+ *
485
+ * @async
486
+ * @param {AuditBackupData} _backupData - Backup data
487
+ * @returns {Promise<void>}
488
+ */
489
+ restore(_backupData: AuditBackupData): Promise<void>;
490
+ }
491
+ export default BaseAuditProvider;
492
+ export { WikiEngine, ProviderInfo };
@@ -46,6 +46,8 @@ export interface AuthVerifyCredentials {
46
46
  password?: string;
47
47
  /** One-time token — used by MagicLinkAuthProvider */
48
48
  token?: string;
49
+ /** A six-digit authenticator-app code — used by TotpAuthProvider (#421). */
50
+ code?: string;
49
51
  /** The account chosen for a link to a shared address (#2073) — used by MagicLinkAuthProvider. */
50
52
  account?: string;
51
53
  /** OAuth `state` from the callback — used by OidcAuthProvider */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Passwords stay on the user record and are never copied here (decided
5
5
  * 2026-10-02). This store holds passkeys, verified email / phone addresses
6
- * and known devices (TOTP seeds once #421 is built) — one row each, many per
6
+ * known devices and TOTP seeds (#421, encrypted) — one row each, many per
7
7
  * account.
8
8
  *
9
9
  * Every row is signed with `NGDPBASE_CREDENTIALS_KEY` (the instance `.env`).
@@ -40,7 +40,7 @@ export interface CredentialRecord {
40
40
  kind: CredentialKind;
41
41
  /** What identifies it within its kind: a passkey's credential id, an address, a device token's hash, an outside identity's `iss#sub`. */
42
42
  subject: string;
43
- /** Kind-specific: a public key and counter, or empty (an encrypted TOTP seed once #421 is built). */
43
+ /** Kind-specific: a public key and counter, an encrypted TOTP seed (#421), or empty. */
44
44
  secret: string;
45
45
  /** What the person calls it — "Jim's iPhone". */
46
46
  label: string;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Passwords stay on the user record and are never copied here (decided
5
5
  * 2026-10-02). This store holds passkeys, verified email / phone addresses
6
- * and known devices (TOTP seeds once #421 is built) — one row each, many per
6
+ * known devices and TOTP seeds (#421, encrypted) — one row each, many per
7
7
  * account.
8
8
  *
9
9
  * Every row is signed with `NGDPBASE_CREDENTIALS_KEY` (the instance `.env`).
@@ -276,6 +276,15 @@ declare class BasicAttachmentProvider extends BaseAttachmentProvider implements
276
276
  private requirePagesDirectory;
277
277
  private readStoreIndex;
278
278
  private writeStoreIndex;
279
+ private storeRenumberedFile;
280
+ /**
281
+ * The ids a takeout import last gave the store's files that could not keep
282
+ * their own (#2188), as takeout id → id here. Kept apart from the index, so
283
+ * it outlives a delete of the file. Ids only.
284
+ */
285
+ readStoreRenumbered(location: StoreFileLocation): Promise<Map<string, string>>;
286
+ /** Adds `pairs` (takeout id → id here) to {@link readStoreRenumbered}'s; a later pair for an id replaces the earlier. */
287
+ noteStoreRenumbered(location: StoreFileLocation, pairs: ReadonlyMap<string, string>): Promise<void>;
279
288
  storeFileInStore(location: StoreFileLocation, bytes: Buffer, file: StoreFileInput): Promise<StoreFileEntry>;
280
289
  getFileInStore(location: StoreFileLocation, id: string): Promise<{
281
290
  entry: StoreFileEntry;
@@ -9,7 +9,7 @@
9
9
  * missing or wrong is reported and can never sign anyone in; it is kept in
10
10
  * quarantine and written back unchanged (#1633), so a lost key costs no
11
11
  * data. `scripts/retrust-credentials.ts` re-signs them on purpose.
12
- * - Nothing here is a password hash. Public keys, (when TOTP is built, #421, encrypted seeds) and token
12
+ * - Nothing here is a password hash. Public keys, encrypted TOTP seeds (#421) and token
13
13
  * hashes are low value to a reader; a writer is what signing stops.
14
14
  */
15
15
  import { createHmac, timingSafeEqual } from 'node:crypto';