@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.
- package/lib/src/managers/AddonsManager.d.ts +8 -0
- package/lib/src/managers/AttachmentManager.d.ts +59 -0
- package/lib/src/managers/AuditManager.d.ts +477 -0
- package/lib/src/managers/AuthManager.d.ts +247 -15
- package/lib/src/managers/AuthManager.js +588 -45
- package/lib/src/managers/ConsentManager.d.ts +15 -0
- package/lib/src/managers/ImportManager.d.ts +15 -0
- package/lib/src/providers/BaseAttachmentProvider.d.ts +4 -0
- package/lib/src/providers/BaseAuditProvider.d.ts +492 -0
- package/lib/src/providers/BaseAuthProvider.d.ts +2 -0
- package/lib/src/providers/BaseCredentialsProvider.d.ts +2 -2
- package/lib/src/providers/BaseCredentialsProvider.js +1 -1
- package/lib/src/providers/BasicAttachmentProvider.d.ts +9 -0
- package/lib/src/providers/FileCredentialsProvider.js +1 -1
- package/lib/src/providers/NullAuditProvider.d.ts +82 -0
- package/lib/src/providers/TotpAuthProvider.d.ts +55 -0
- package/lib/src/providers/TotpAuthProvider.js +145 -0
- package/lib/src/types/Consent.d.ts +17 -4
- package/lib/src/types/Consent.js +11 -1
- package/lib/src/types/Provider.d.ts +6 -0
- package/lib/src/types/express.d.ts +22 -0
- package/lib/src/utils/auditChain.d.ts +83 -0
- package/lib/src/utils/auditHeadWitness.d.ts +97 -0
- package/lib/src/utils/auditLifecycle.d.ts +71 -0
- package/lib/src/utils/browserDescription.d.ts +18 -0
- package/lib/src/utils/browserDescription.js +12 -0
- package/lib/src/utils/postureRecord.d.ts +73 -0
- package/lib/src/utils/privateStorePath.d.ts +6 -0
- package/lib/src/utils/privateStorePath.js +8 -0
- package/lib/src/utils/securityPosture.d.ts +52 -0
- package/lib/src/utils/tlsConfig.d.ts +59 -0
- package/lib/src/utils/totpSeedCipher.d.ts +4 -0
- package/lib/src/utils/totpSeedCipher.js +45 -0
- 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
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
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,
|
|
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';
|