@jwilleke/ngdpbase-types 5.41.0 → 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/packages/media-date/index.d.ts +34 -0
- 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/ConfigurationManager.d.ts +6 -0
- package/lib/src/managers/ConsentManager.d.ts +15 -0
- package/lib/src/managers/ImportManager.d.ts +15 -0
- package/lib/src/managers/MediaManager.d.ts +9 -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/FileSystemMediaProvider.d.ts +19 -0
- 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/mediaFolderOverlap.d.ts +50 -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
- package/lib/src/providers/mediaFilenameDate.d.ts +0 -22
|
@@ -37,6 +37,12 @@ export interface FileSystemMediaProviderConfig {
|
|
|
37
37
|
readonly: boolean;
|
|
38
38
|
/** Set of file extensions (lowercase, no dot) to index */
|
|
39
39
|
extensions: Set<string>;
|
|
40
|
+
/**
|
|
41
|
+
* Private storage the scanner never walks under or keeps in the index (#2195):
|
|
42
|
+
* the roots `privateStorageRoots` lists. MediaManager already refuses a folder
|
|
43
|
+
* that overlaps them; this holds even if one gets past it.
|
|
44
|
+
*/
|
|
45
|
+
excludedRoots?: string[];
|
|
40
46
|
}
|
|
41
47
|
/** Default supported media file extensions — used when no config override is provided */
|
|
42
48
|
export declare const DEFAULT_MEDIA_EXTENSIONS: string[];
|
|
@@ -54,6 +60,8 @@ declare class FileSystemMediaProvider extends BaseMediaProvider {
|
|
|
54
60
|
private readonly config;
|
|
55
61
|
/** In-memory index: id → MediaIndexEntry */
|
|
56
62
|
private index;
|
|
63
|
+
/** `config.excludedRoots` as real paths, resolved once (#2195). */
|
|
64
|
+
private readonly excludedReal;
|
|
57
65
|
/** Single ExifTool worker process reused across all reads */
|
|
58
66
|
private readonly exiftoolInstance;
|
|
59
67
|
constructor(config: FileSystemMediaProviderConfig);
|
|
@@ -62,6 +70,17 @@ declare class FileSystemMediaProvider extends BaseMediaProvider {
|
|
|
62
70
|
* (before the first background scan).
|
|
63
71
|
*/
|
|
64
72
|
initialize(): Promise<void>;
|
|
73
|
+
/** The private storage root `p` is under, or undefined (#2195). */
|
|
74
|
+
private excludedRootOf;
|
|
75
|
+
/** Whether `p` is under private storage the scanner must not touch (#2195). */
|
|
76
|
+
private isExcluded;
|
|
77
|
+
/**
|
|
78
|
+
* Drop index entries under private storage (#2195): a site that had a media
|
|
79
|
+
* folder overlapping it before the check existed has those files indexed.
|
|
80
|
+
*/
|
|
81
|
+
private dropExcludedEntries;
|
|
82
|
+
/** Delete every cached thumbnail (`{id}-*.webp`) of `ids`: they are copies of the files. */
|
|
83
|
+
private removeThumbnails;
|
|
65
84
|
/**
|
|
66
85
|
* Release the ExifTool worker process.
|
|
67
86
|
*/
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import BaseAuditProvider, { WikiEngine, AuditFilters, AuditSearchResults, AuditStats } from './BaseAuditProvider.js';
|
|
2
|
+
import type { AuditReport } from './BaseAuditProvider.js';
|
|
3
|
+
/**
|
|
4
|
+
* NullAuditProvider - No-op audit provider
|
|
5
|
+
*
|
|
6
|
+
* Used when auditing is disabled or for testing.
|
|
7
|
+
* All audit operations are no-ops that return immediately.
|
|
8
|
+
*/
|
|
9
|
+
declare class NullAuditProvider extends BaseAuditProvider {
|
|
10
|
+
/**
|
|
11
|
+
* Not chained (#1119): this provider stores nothing, so there is nothing to
|
|
12
|
+
* chain. A legitimate configuration rather than a hole — provided the
|
|
13
|
+
* instance cannot then claim tamper evidence, which is what
|
|
14
|
+
* getProviderInfo().guarantees reports.
|
|
15
|
+
*/
|
|
16
|
+
protected chainEnabled(): boolean;
|
|
17
|
+
/** Nothing is stored, so nothing is guaranteed (#1119). */
|
|
18
|
+
getGuarantees(): AuditReport;
|
|
19
|
+
constructor(engine: WikiEngine);
|
|
20
|
+
/**
|
|
21
|
+
* Initialize the null audit provider (no-op)
|
|
22
|
+
* @returns {Promise<void>}
|
|
23
|
+
*/
|
|
24
|
+
initialize(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Get provider information
|
|
27
|
+
* @returns {Object} Provider metadata
|
|
28
|
+
*/
|
|
29
|
+
getProviderInfo(): {
|
|
30
|
+
name: string;
|
|
31
|
+
version: string;
|
|
32
|
+
description: string;
|
|
33
|
+
features: string[];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Log an audit event (no-op)
|
|
37
|
+
* @param {AuditEvent} _auditEvent - Audit event data
|
|
38
|
+
* @returns {Promise<string>} Dummy event ID
|
|
39
|
+
*/
|
|
40
|
+
writeEvent(_record: Record<string, unknown>): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Search audit logs (returns empty results)
|
|
43
|
+
* @param {AuditFilters} _filters - Search filters
|
|
44
|
+
* @param {Record<string, number>} options - Search options
|
|
45
|
+
* @returns {Promise<AuditSearchResults>} Empty search results
|
|
46
|
+
*/
|
|
47
|
+
searchAuditLogs(_filters?: AuditFilters, options?: Record<string, number>): Promise<AuditSearchResults>;
|
|
48
|
+
/**
|
|
49
|
+
* Get audit statistics (returns zeros)
|
|
50
|
+
* @param {AuditFilters} _filters - Optional filters
|
|
51
|
+
* @returns {Promise<AuditStats>} Empty statistics
|
|
52
|
+
*/
|
|
53
|
+
getAuditStats(_filters?: AuditFilters): Promise<AuditStats>;
|
|
54
|
+
/**
|
|
55
|
+
* Export audit logs (returns empty data)
|
|
56
|
+
* @param {AuditFilters} _filters - Export filters
|
|
57
|
+
* @param {string} format - Export format ('json', 'csv')
|
|
58
|
+
* @returns {Promise<string>} Empty export data
|
|
59
|
+
*/
|
|
60
|
+
exportAuditLogs(_filters?: AuditFilters, format?: 'json' | 'csv'): Promise<string>;
|
|
61
|
+
/**
|
|
62
|
+
* Flush pending audit events (no-op)
|
|
63
|
+
* @returns {Promise<void>}
|
|
64
|
+
*/
|
|
65
|
+
flush(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Clean up old audit logs (no-op)
|
|
68
|
+
* @returns {Promise<void>}
|
|
69
|
+
*/
|
|
70
|
+
cleanup(): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Check if the audit provider is healthy (always true)
|
|
73
|
+
* @returns {Promise<boolean>} Always true
|
|
74
|
+
*/
|
|
75
|
+
isHealthy(): Promise<boolean>;
|
|
76
|
+
/**
|
|
77
|
+
* Close/cleanup the audit provider (no-op)
|
|
78
|
+
* @returns {Promise<void>}
|
|
79
|
+
*/
|
|
80
|
+
close(): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
export default NullAuditProvider;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AuthProvider, AuthResult, AuthVerifyCredentials, FactorDescription } from './BaseAuthProvider.js';
|
|
2
|
+
import type { CredentialRecord } from './BaseCredentialsProvider.js';
|
|
3
|
+
/** Steps either side of now that are accepted. */
|
|
4
|
+
export declare const TOTP_DRIFT_STEPS = 1;
|
|
5
|
+
/** The store access AuthManager grants: an account's TOTP rows, and recording a use. */
|
|
6
|
+
export interface TotpStoreAccess {
|
|
7
|
+
rows(username: string): CredentialRecord[];
|
|
8
|
+
used(id: string, at: string, secret: string): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/** A new seed, offered while enrolling and kept by the caller until it is confirmed. */
|
|
11
|
+
export interface TotpEnrolment {
|
|
12
|
+
/** Base32, for typing into the app by hand. */
|
|
13
|
+
seed: string;
|
|
14
|
+
/** The `otpauth://` URI the QR code carries. */
|
|
15
|
+
uri: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The time step `code` belongs to within the drift window, or null. Pure; the
|
|
19
|
+
* caller decides about replay.
|
|
20
|
+
*/
|
|
21
|
+
export declare function matchingStep(seed: string, code: string, now: number): number | null;
|
|
22
|
+
export declare class TotpAuthProvider implements AuthProvider {
|
|
23
|
+
private readonly credentialsKey;
|
|
24
|
+
private readonly issuer;
|
|
25
|
+
private readonly store;
|
|
26
|
+
private readonly now;
|
|
27
|
+
readonly id = "totp";
|
|
28
|
+
readonly displayName = "Authenticator app";
|
|
29
|
+
readonly factor: FactorDescription;
|
|
30
|
+
private readonly serial;
|
|
31
|
+
/**
|
|
32
|
+
* @param credentialsKey `NGDPBASE_CREDENTIALS_KEY`, from which the seed key is derived
|
|
33
|
+
* @param issuer shown in the authenticator app beside the account
|
|
34
|
+
*/
|
|
35
|
+
constructor(credentialsKey: string, issuer: string, store: TotpStoreAccess, now?: () => number);
|
|
36
|
+
/** A fresh 160-bit seed and its URI for `username`. Nothing is stored. */
|
|
37
|
+
newEnrolment(username: string): TotpEnrolment;
|
|
38
|
+
/**
|
|
39
|
+
* Seal a seed that is offered but not yet confirmed, so it can wait in the
|
|
40
|
+
* session without being readable there or in a session log (#421). Its own
|
|
41
|
+
* shape, so it never opens as a stored secret.
|
|
42
|
+
*/
|
|
43
|
+
sealPending(username: string, seed: string): string;
|
|
44
|
+
/** Open what `sealPending` made for `username`; null when it is not that. */
|
|
45
|
+
openPending(username: string, sealed: unknown): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Check the code that confirms an enrolment and seal the seed for the store.
|
|
48
|
+
* The confirming code's step is recorded, so it cannot then sign anyone in.
|
|
49
|
+
* Null when the seed is malformed or the code does not match.
|
|
50
|
+
*/
|
|
51
|
+
confirmEnrolment(username: string, seed: string, code: string): string | null;
|
|
52
|
+
/** Verify `credentials.code` for `credentials.username`. A failure gives no reason. */
|
|
53
|
+
verify(credentials: AuthVerifyCredentials): Promise<AuthResult | null>;
|
|
54
|
+
private open;
|
|
55
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOTP — an authenticator app's six-digit code (#421), on `otpauth` (decided
|
|
3
|
+
* on #1523, 2026-10-31).
|
|
4
|
+
*
|
|
5
|
+
* - A second factor only (`primary: false`): something you have, `amr`
|
|
6
|
+
* `otp`, AAL1 on its own. After a password the sign-in covers two factor
|
|
7
|
+
* types and AuthManager's `assess()` lifts it to AAL2. Not phishing-resistant.
|
|
8
|
+
* - The seed lives in the credentials store (#1524), kind `totp`, AES-GCM
|
|
9
|
+
* encrypted under a subkey of `NGDPBASE_CREDENTIALS_KEY`
|
|
10
|
+
* (`utils/totpSeedCipher`). The row's sealed secret also holds the last time
|
|
11
|
+
* step used, so a code cannot be used twice in its window.
|
|
12
|
+
* - A code is accepted for the current 30-second step and one step either
|
|
13
|
+
* side (RFC 6238 §5.2 drift), never for a step at or before the last one used.
|
|
14
|
+
* - Verification for one account runs one at a time, so two requests carrying
|
|
15
|
+
* the same code cannot both pass before the step is recorded.
|
|
16
|
+
*/
|
|
17
|
+
import { Secret, TOTP, URI } from 'otpauth';
|
|
18
|
+
import { openTotpSecret, sealTotpSecret } from '../utils/totpSeedCipher.js';
|
|
19
|
+
import { KeyedSerial } from '../utils/keyedSerial.js';
|
|
20
|
+
/** RFC 6238 defaults, which every authenticator app supports. */
|
|
21
|
+
const PERIOD = 30;
|
|
22
|
+
const DIGITS = 6;
|
|
23
|
+
/** Steps either side of now that are accepted. */
|
|
24
|
+
export const TOTP_DRIFT_STEPS = 1;
|
|
25
|
+
function totpFor(seed, issuer = '', label = '') {
|
|
26
|
+
return new TOTP({ issuer, label, algorithm: 'SHA1', digits: DIGITS, period: PERIOD, secret: Secret.fromBase32(seed) });
|
|
27
|
+
}
|
|
28
|
+
function isCode(code) {
|
|
29
|
+
return typeof code === 'string' && /^\d{6}$/.test(code);
|
|
30
|
+
}
|
|
31
|
+
function currentStep(now) {
|
|
32
|
+
return Math.floor(now / 1000 / PERIOD);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The time step `code` belongs to within the drift window, or null. Pure; the
|
|
36
|
+
* caller decides about replay.
|
|
37
|
+
*/
|
|
38
|
+
export function matchingStep(seed, code, now) {
|
|
39
|
+
if (!isCode(code))
|
|
40
|
+
return null;
|
|
41
|
+
const delta = totpFor(seed).validate({ token: code, timestamp: now, window: TOTP_DRIFT_STEPS });
|
|
42
|
+
return delta === null ? null : currentStep(now) + delta;
|
|
43
|
+
}
|
|
44
|
+
export class TotpAuthProvider {
|
|
45
|
+
credentialsKey;
|
|
46
|
+
issuer;
|
|
47
|
+
store;
|
|
48
|
+
now;
|
|
49
|
+
id = 'totp';
|
|
50
|
+
displayName = 'Authenticator app';
|
|
51
|
+
factor = { amr: ['otp'], aal: 1, primary: false };
|
|
52
|
+
serial = new KeyedSerial();
|
|
53
|
+
/**
|
|
54
|
+
* @param credentialsKey `NGDPBASE_CREDENTIALS_KEY`, from which the seed key is derived
|
|
55
|
+
* @param issuer shown in the authenticator app beside the account
|
|
56
|
+
*/
|
|
57
|
+
constructor(credentialsKey, issuer, store, now = Date.now) {
|
|
58
|
+
this.credentialsKey = credentialsKey;
|
|
59
|
+
this.issuer = issuer;
|
|
60
|
+
this.store = store;
|
|
61
|
+
this.now = now;
|
|
62
|
+
if (!credentialsKey)
|
|
63
|
+
throw new Error('TOTP needs NGDPBASE_CREDENTIALS_KEY (#421)');
|
|
64
|
+
}
|
|
65
|
+
/** A fresh 160-bit seed and its URI for `username`. Nothing is stored. */
|
|
66
|
+
newEnrolment(username) {
|
|
67
|
+
const seed = new Secret({ size: 20 }).base32;
|
|
68
|
+
return { seed, uri: URI.stringify(totpFor(seed, this.issuer, username)) };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Seal a seed that is offered but not yet confirmed, so it can wait in the
|
|
72
|
+
* session without being readable there or in a session log (#421). Its own
|
|
73
|
+
* shape, so it never opens as a stored secret.
|
|
74
|
+
*/
|
|
75
|
+
sealPending(username, seed) {
|
|
76
|
+
return sealTotpSecret(this.credentialsKey, username, JSON.stringify({ pendingSeed: seed }));
|
|
77
|
+
}
|
|
78
|
+
/** Open what `sealPending` made for `username`; null when it is not that. */
|
|
79
|
+
openPending(username, sealed) {
|
|
80
|
+
if (typeof sealed !== 'string')
|
|
81
|
+
return null;
|
|
82
|
+
const plain = openTotpSecret(this.credentialsKey, username, sealed);
|
|
83
|
+
if (!plain)
|
|
84
|
+
return null;
|
|
85
|
+
try {
|
|
86
|
+
const p = JSON.parse(plain);
|
|
87
|
+
return typeof p.pendingSeed === 'string' ? p.pendingSeed : null;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check the code that confirms an enrolment and seal the seed for the store.
|
|
95
|
+
* The confirming code's step is recorded, so it cannot then sign anyone in.
|
|
96
|
+
* Null when the seed is malformed or the code does not match.
|
|
97
|
+
*/
|
|
98
|
+
confirmEnrolment(username, seed, code) {
|
|
99
|
+
let step;
|
|
100
|
+
try {
|
|
101
|
+
step = matchingStep(seed, code, this.now());
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null; // not base32
|
|
105
|
+
}
|
|
106
|
+
if (step === null)
|
|
107
|
+
return null;
|
|
108
|
+
return sealTotpSecret(this.credentialsKey, username, JSON.stringify({ seed, lastStep: step }));
|
|
109
|
+
}
|
|
110
|
+
/** Verify `credentials.code` for `credentials.username`. A failure gives no reason. */
|
|
111
|
+
async verify(credentials) {
|
|
112
|
+
const { username, code } = credentials;
|
|
113
|
+
if (typeof username !== 'string' || !username || !isCode(code))
|
|
114
|
+
return null;
|
|
115
|
+
return this.serial.run(username, async () => {
|
|
116
|
+
const now = this.now();
|
|
117
|
+
for (const row of this.store.rows(username)) {
|
|
118
|
+
const secret = this.open(row);
|
|
119
|
+
if (!secret)
|
|
120
|
+
continue;
|
|
121
|
+
const step = matchingStep(secret.seed, code, now);
|
|
122
|
+
if (step === null || step <= secret.lastStep)
|
|
123
|
+
continue;
|
|
124
|
+
const next = sealTotpSecret(this.credentialsKey, username, JSON.stringify({ seed: secret.seed, lastStep: step }));
|
|
125
|
+
await this.store.used(row.id, new Date(now).toISOString(), next);
|
|
126
|
+
return { username };
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
open(row) {
|
|
132
|
+
if (row.kind !== 'totp')
|
|
133
|
+
return null;
|
|
134
|
+
const plain = openTotpSecret(this.credentialsKey, row.username, row.secret);
|
|
135
|
+
if (!plain)
|
|
136
|
+
return null;
|
|
137
|
+
try {
|
|
138
|
+
const s = JSON.parse(plain);
|
|
139
|
+
return typeof s.seed === 'string' && typeof s.lastStep === 'number' ? s : null;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -33,6 +33,15 @@ export declare const RECOVERY_WORDS_DOCUMENT = "recovery-words";
|
|
|
33
33
|
export declare const SHARED_FOLDERS_WORDS_CONSENT_SCOPE = "shared-folders-recovery-words";
|
|
34
34
|
/** The page whose `version:` is the version of the shared-folders recovery-words wording (#2124). */
|
|
35
35
|
export declare const SHARED_FOLDERS_WORDS_DOCUMENT = "shared-folders-recovery-words";
|
|
36
|
+
/**
|
|
37
|
+
* The scope of "Remember this browser" (#1523 slice 1c, Q5): a person's
|
|
38
|
+
* opt-in, at the second-factor step, to a device key on this browser that
|
|
39
|
+
* skips the second-factor prompt next time. Bound to a subject: the remembered
|
|
40
|
+
* device's id, so each browser is its own consent, withdrawn by removing it.
|
|
41
|
+
*/
|
|
42
|
+
export declare const REMEMBER_BROWSER_CONSENT_SCOPE = "remember-browser";
|
|
43
|
+
/** The page whose `version:` is the version of the "Remember this browser" wording (#1523 slice 1c). */
|
|
44
|
+
export declare const REMEMBER_BROWSER_DOCUMENT = "remember-browser";
|
|
36
45
|
/** Core's scopes bound to a subject, each with its one document (#1928, #2124). */
|
|
37
46
|
export declare const SUBJECT_CONSENT_DOCUMENTS: Readonly<Record<ConsentScope, string>>;
|
|
38
47
|
/** Scopes whose acceptance is about one thing (a subject), not the account as a whole (#1928). */
|
|
@@ -61,17 +70,21 @@ export interface ConsentSeen {
|
|
|
61
70
|
version: string;
|
|
62
71
|
}
|
|
63
72
|
/**
|
|
64
|
-
* One acceptance, one line of the person's file.
|
|
65
|
-
* version is a later line,
|
|
73
|
+
* One acceptance, or its withdrawal, one line of the person's file.
|
|
74
|
+
* Append-only: a later version is a later line, and a withdrawal is a later
|
|
75
|
+
* `withdraw` line naming the same document, version and subject; nothing is
|
|
76
|
+
* rewritten.
|
|
66
77
|
*/
|
|
67
78
|
export interface ConsentRecord {
|
|
68
|
-
kind: 'accept';
|
|
79
|
+
kind: 'accept' | 'withdraw';
|
|
69
80
|
document: string;
|
|
70
81
|
scope: ConsentScope;
|
|
71
82
|
version: string;
|
|
72
83
|
effective: string | null;
|
|
73
|
-
/** ISO 8601 time the person accepted. */
|
|
84
|
+
/** ISO 8601 time the person accepted (on a `withdraw` line, of the acceptance withdrawn). */
|
|
74
85
|
acceptedAt: string;
|
|
86
|
+
/** ISO 8601 time it was withdrawn; on a `withdraw` line only. */
|
|
87
|
+
withdrawnAt?: string;
|
|
75
88
|
/**
|
|
76
89
|
* The account this was accepted on, by its creation time (#1937): a later
|
|
77
90
|
* account that takes the same username never inherits it.
|
package/lib/src/types/Consent.js
CHANGED
|
@@ -31,10 +31,20 @@ export const RECOVERY_WORDS_DOCUMENT = 'recovery-words';
|
|
|
31
31
|
export const SHARED_FOLDERS_WORDS_CONSENT_SCOPE = 'shared-folders-recovery-words';
|
|
32
32
|
/** The page whose `version:` is the version of the shared-folders recovery-words wording (#2124). */
|
|
33
33
|
export const SHARED_FOLDERS_WORDS_DOCUMENT = 'shared-folders-recovery-words';
|
|
34
|
+
/**
|
|
35
|
+
* The scope of "Remember this browser" (#1523 slice 1c, Q5): a person's
|
|
36
|
+
* opt-in, at the second-factor step, to a device key on this browser that
|
|
37
|
+
* skips the second-factor prompt next time. Bound to a subject: the remembered
|
|
38
|
+
* device's id, so each browser is its own consent, withdrawn by removing it.
|
|
39
|
+
*/
|
|
40
|
+
export const REMEMBER_BROWSER_CONSENT_SCOPE = 'remember-browser';
|
|
41
|
+
/** The page whose `version:` is the version of the "Remember this browser" wording (#1523 slice 1c). */
|
|
42
|
+
export const REMEMBER_BROWSER_DOCUMENT = 'remember-browser';
|
|
34
43
|
/** Core's scopes bound to a subject, each with its one document (#1928, #2124). */
|
|
35
44
|
export const SUBJECT_CONSENT_DOCUMENTS = {
|
|
36
45
|
[RECOVERY_WORDS_CONSENT_SCOPE]: RECOVERY_WORDS_DOCUMENT,
|
|
37
|
-
[SHARED_FOLDERS_WORDS_CONSENT_SCOPE]: SHARED_FOLDERS_WORDS_DOCUMENT
|
|
46
|
+
[SHARED_FOLDERS_WORDS_CONSENT_SCOPE]: SHARED_FOLDERS_WORDS_DOCUMENT,
|
|
47
|
+
[REMEMBER_BROWSER_CONSENT_SCOPE]: REMEMBER_BROWSER_DOCUMENT
|
|
38
48
|
};
|
|
39
49
|
/** Scopes whose acceptance is about one thing (a subject), not the account as a whole (#1928). */
|
|
40
50
|
export const SUBJECT_CONSENT_SCOPES = Object.keys(SUBJECT_CONSENT_DOCUMENTS);
|
|
@@ -630,6 +630,12 @@ export interface StoreFileInput {
|
|
|
630
630
|
keptOriginal?: {
|
|
631
631
|
types: readonly string[];
|
|
632
632
|
};
|
|
633
|
+
/**
|
|
634
|
+
* The id to store the file under, set only by AttachmentManager for a file
|
|
635
|
+
* a takeout re-import brings back (#2188), after it checked that no other
|
|
636
|
+
* file anywhere holds it. A store that already lists the id refuses it.
|
|
637
|
+
*/
|
|
638
|
+
id?: string;
|
|
633
639
|
}
|
|
634
640
|
/**
|
|
635
641
|
* A page in a private store (#1456): its entry in the store's OWN page index,
|
|
@@ -40,6 +40,14 @@ declare module 'express-session' {
|
|
|
40
40
|
purpose: 'register' | 'authenticate' | 'reauth';
|
|
41
41
|
expires: number;
|
|
42
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* #421: an authenticator-app seed offered on /profile/totp, until a code confirms it (10 minutes).
|
|
45
|
+
* Sealed for the account by `totpEnrolmentStart`; the plain seed never enters the session.
|
|
46
|
+
*/
|
|
47
|
+
totpEnrolment?: {
|
|
48
|
+
pending: string;
|
|
49
|
+
expires: number;
|
|
50
|
+
};
|
|
43
51
|
/** The step-up asked for and not yet given (#2145): its permission, where it returns, whether returning redoes it. */
|
|
44
52
|
pendingStepUp?: PendingStepUp;
|
|
45
53
|
/** The path whose next page view says "Confirmed — you can do it now" after a step-up (#2145). */
|
|
@@ -49,6 +57,20 @@ declare module 'express-session' {
|
|
|
49
57
|
/** A sign-in waiting for its second factor: the handle, and where to go after (#1523). No identity yet. */
|
|
50
58
|
pendingSignIn?: string;
|
|
51
59
|
pendingRedirect?: string;
|
|
60
|
+
/**
|
|
61
|
+
* #1523 slice 1c: "Remember this browser" was ticked and a second factor
|
|
62
|
+
* completed the sign-in; the next page has the browser make its key. The
|
|
63
|
+
* version of the page the person was shown, the sign-in's amr, where to go
|
|
64
|
+
* after, until when (epoch ms), and the id of the sign-in's audit record,
|
|
65
|
+
* which the remembered row names. Single use.
|
|
66
|
+
*/
|
|
67
|
+
rememberBrowser?: {
|
|
68
|
+
version: string;
|
|
69
|
+
amr: string[];
|
|
70
|
+
next: string;
|
|
71
|
+
until: number;
|
|
72
|
+
auditId?: string;
|
|
73
|
+
};
|
|
52
74
|
/** A verified outside identity no account has connected, waiting for the person to sign in another way (#1743). */
|
|
53
75
|
pendingConnection?: PendingConnection;
|
|
54
76
|
/** #2073: an outside sign-in whose email several accounts share, waiting for the person to choose one. */
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** `prevHash` of the first record in a chain. */
|
|
2
|
+
export declare const GENESIS_HASH = "genesis";
|
|
3
|
+
/**
|
|
4
|
+
* Hash a record in its position in the chain.
|
|
5
|
+
*
|
|
6
|
+
* `seq` and `prevHash` are part of the input, not just the payload — otherwise
|
|
7
|
+
* records could be reordered, or a record moved to a different predecessor,
|
|
8
|
+
* without breaking anything.
|
|
9
|
+
*/
|
|
10
|
+
export declare function hashRecord(record: Record<string, unknown>, seq: number, prevHash: string): string;
|
|
11
|
+
/** Add `seq`, `prevHash` and `hash` to a record. Returns a new object. */
|
|
12
|
+
export declare function stampRecord(record: Record<string, unknown>, seq: number, prevHash: string): Record<string, unknown>;
|
|
13
|
+
/** Outcome of verifying a chain. `brokenAt` is the 1-based position, not the seq. */
|
|
14
|
+
export interface ChainVerdict {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
checked: number;
|
|
17
|
+
brokenAt?: number;
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Where a chain that does not begin at genesis begins (#1537): the sequence
|
|
22
|
+
* and hash of the last record before the first one present. A retention
|
|
23
|
+
* checkpoint holds exactly this for the records it removed, so what remains
|
|
24
|
+
* verifies from it rather than from genesis.
|
|
25
|
+
*/
|
|
26
|
+
export interface ChainStart {
|
|
27
|
+
seq: number;
|
|
28
|
+
hash: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Walk a chain in order and report the first break.
|
|
32
|
+
*
|
|
33
|
+
* @param records - Audit records, oldest first
|
|
34
|
+
* @param options.expectedHead - Hash the last record must have. Supplying it is
|
|
35
|
+
* the only way to detect truncation: removing records from the end breaks no
|
|
36
|
+
* link, so the chain alone cannot see it.
|
|
37
|
+
* @param options.start - The checkpoint the chain continues from (#1537). The
|
|
38
|
+
* first record must then carry `start.seq + 1` and link to `start.hash`;
|
|
39
|
+
* without it the first record must link to genesis.
|
|
40
|
+
*/
|
|
41
|
+
export declare function verifyChain(records: readonly Record<string, unknown>[], options?: {
|
|
42
|
+
expectedHead?: string;
|
|
43
|
+
start?: ChainStart;
|
|
44
|
+
}): ChainVerdict;
|
|
45
|
+
/** Event type of a record that declares a deliberate chain discontinuity (#1124). */
|
|
46
|
+
export declare const CHAIN_RESTART_EVENT: "audit-chain-restart";
|
|
47
|
+
/** One contiguous chain within a log. */
|
|
48
|
+
export interface ChainSegment {
|
|
49
|
+
/** Index of the segment's first record in the input. */
|
|
50
|
+
startIndex: number;
|
|
51
|
+
verdict: ChainVerdict;
|
|
52
|
+
/** The restart marker that begins this segment, if it was started by one. */
|
|
53
|
+
restart?: Record<string, unknown>;
|
|
54
|
+
/** True when a break at the END of this segment is followed by a restart marker. */
|
|
55
|
+
explained: boolean;
|
|
56
|
+
/** True when this segment's marker names a previous head that does not match. */
|
|
57
|
+
mismatchedPrevious: boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface LogVerdict {
|
|
60
|
+
ok: boolean;
|
|
61
|
+
segments: ChainSegment[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Verify a log that may contain deliberate discontinuities (#1124).
|
|
65
|
+
*
|
|
66
|
+
* `verifyChain` answers "is this one chain intact", which is the right question
|
|
67
|
+
* for a chain and the wrong one for a log that has legitimately restarted — a
|
|
68
|
+
* migration, a restore, or a bug that wrote unverifiable records. Without this,
|
|
69
|
+
* one bad record leaves a log unverifiable forever and the only ways forward
|
|
70
|
+
* are dishonest: edit the log, delete it, or ignore the verifier.
|
|
71
|
+
*
|
|
72
|
+
* A break is __explained__ only when the very next record is a restart marker.
|
|
73
|
+
* That is what keeps the marker from being an escape hatch: an attacker who
|
|
74
|
+
* wants a clean chain must write a record saying they broke it, which is a
|
|
75
|
+
* confession rather than a cover-up.
|
|
76
|
+
*
|
|
77
|
+
* The marker's claimed `previousHash` is checked against the head it actually
|
|
78
|
+
* follows. A marker asserting a history that did not happen is itself a
|
|
79
|
+
* finding — otherwise a restart could claim anything.
|
|
80
|
+
*/
|
|
81
|
+
export declare function verifyLog(records: readonly Record<string, unknown>[], options?: {
|
|
82
|
+
start?: ChainStart;
|
|
83
|
+
}): LogVerdict;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Off-box witness for the audit chain head (#1138).
|
|
3
|
+
*
|
|
4
|
+
* The chain detects alteration and deletion, and __cannot detect truncation of
|
|
5
|
+
* the tail__: removing records from the end breaks no link. That is also the
|
|
6
|
+
* attack an intruder actually wants — remove the record of what they just did.
|
|
7
|
+
* Everything before their session verifies perfectly, and the log ends looking
|
|
8
|
+
* like a clean shutdown.
|
|
9
|
+
*
|
|
10
|
+
* It cannot be fixed locally, for the reason #1119 made the chain functions
|
|
11
|
+
* pure: an attacker who owns the machine owns anything the machine wrote
|
|
12
|
+
* locally, including a head stored beside the log. __The head has to leave the
|
|
13
|
+
* box, or it is not evidence.__
|
|
14
|
+
*
|
|
15
|
+
* Three decisions from docs/security-posture.md shape what this does and does
|
|
16
|
+
* not do:
|
|
17
|
+
*
|
|
18
|
+
* - __D13__ — deployment methodology does not influence the design. This
|
|
19
|
+
* publishes a head to a configured destination and takes no view on what that
|
|
20
|
+
* destination is. Whether it is genuinely off-box is the operator's decision
|
|
21
|
+
* and the operator's to state.
|
|
22
|
+
* - __D21__ — report facts, not a claim. There is deliberately no `offBox:
|
|
23
|
+
* true` produced here. Nothing on this machine can verify that a path leaves
|
|
24
|
+
* it, and asserting so would be the `durable` defect (#1148) repeated: a
|
|
25
|
+
* boolean derived from something that does not establish it.
|
|
26
|
+
* - __D20__ — no scoring. A verdict says what is observably true of the records
|
|
27
|
+
* and the witness, and stops there.
|
|
28
|
+
*/
|
|
29
|
+
/** What is published: one sequence number and one hash. Nothing else. */
|
|
30
|
+
export interface ChainWitness {
|
|
31
|
+
seq: number;
|
|
32
|
+
hash: string;
|
|
33
|
+
/** Which instance the head belongs to, so one store can hold several. */
|
|
34
|
+
instance: string;
|
|
35
|
+
publishedAt: string;
|
|
36
|
+
}
|
|
37
|
+
/** The minimum of a stored record this assessment needs. */
|
|
38
|
+
export interface ChainRecord {
|
|
39
|
+
seq?: number;
|
|
40
|
+
hash?: string;
|
|
41
|
+
}
|
|
42
|
+
export type WitnessVerdict =
|
|
43
|
+
/** The log is consistent with the witness. */
|
|
44
|
+
'intact'
|
|
45
|
+
/** The witness is ahead of the log: records were removed from the end. */
|
|
46
|
+
| 'truncated'
|
|
47
|
+
/** The witnessed sequence is present with a different hash, or absent. */
|
|
48
|
+
| 'altered'
|
|
49
|
+
/** No witness. NOT the same statement as "intact". */
|
|
50
|
+
| 'unknown';
|
|
51
|
+
export interface WitnessAssessment {
|
|
52
|
+
verdict: WitnessVerdict;
|
|
53
|
+
/** How many records are missing from the end. Only meaningful when truncated. */
|
|
54
|
+
missing?: number;
|
|
55
|
+
reason?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface BuildWitnessInput {
|
|
58
|
+
seq: number;
|
|
59
|
+
hash: string;
|
|
60
|
+
instance: string;
|
|
61
|
+
at: Date;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build the record to publish.
|
|
65
|
+
*
|
|
66
|
+
* The head is one hash and one sequence number. Publishing more would put
|
|
67
|
+
* audit CONTENT wherever the witness lives, which is a different and much
|
|
68
|
+
* larger trust decision than publishing a fingerprint of it.
|
|
69
|
+
*/
|
|
70
|
+
export declare function buildWitness(input: BuildWitnessInput): ChainWitness;
|
|
71
|
+
/**
|
|
72
|
+
* Is it time to publish again?
|
|
73
|
+
*
|
|
74
|
+
* The gap between publications is the window an attacker can truncate within,
|
|
75
|
+
* which makes the interval the actual security parameter — worth setting
|
|
76
|
+
* explicitly rather than inheriting.
|
|
77
|
+
*
|
|
78
|
+
* A zero or negative interval publishes every time rather than never: a
|
|
79
|
+
* misconfigured interval must fail toward more evidence, not toward silently
|
|
80
|
+
* disabling the witness.
|
|
81
|
+
*/
|
|
82
|
+
export declare function shouldPublish(lastPublishedAtMs: number | null, nowMs: number, intervalMs: number): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Compare a published head against the records now on disk.
|
|
85
|
+
*
|
|
86
|
+
* The verdicts are kept distinct because an assessor needs the difference:
|
|
87
|
+
* truncation says records were removed, alteration says one was rewritten, and
|
|
88
|
+
* reporting both as "broken" loses what they would act on.
|
|
89
|
+
*
|
|
90
|
+
* `unknown` is never reported as `intact`. "Nothing is witnessing this log" and
|
|
91
|
+
* "this log is intact" are different statements, and deriving the second from
|
|
92
|
+
* the first is exactly the claim #1148 removed elsewhere.
|
|
93
|
+
*/
|
|
94
|
+
export declare function assessWitness(witness: ChainWitness | {
|
|
95
|
+
seq: number;
|
|
96
|
+
hash: string;
|
|
97
|
+
} | null, records: readonly ChainRecord[]): WitnessAssessment;
|