@jwilleke/ngdpbase-types 5.40.0 → 5.41.1

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 (47) hide show
  1. package/lib/packages/media-date/index.d.ts +34 -0
  2. package/lib/packages/ngdpbase-types/index.d.ts +1 -1
  3. package/lib/src/ice/fileHandling.d.ts +6 -1
  4. package/lib/src/ice/runExport.d.ts +41 -0
  5. package/lib/src/ice/runImport.d.ts +16 -7
  6. package/lib/src/ice/shims/csv.d.ts +24 -0
  7. package/lib/src/ice/shims/export.d.ts +23 -0
  8. package/lib/src/ice/shims/index.d.ts +3 -1
  9. package/lib/src/ice/shims/svg.d.ts +27 -0
  10. package/lib/src/ice/shims/takeout.d.ts +83 -0
  11. package/lib/src/ice/types.d.ts +251 -5
  12. package/lib/src/ice/vet.d.ts +19 -0
  13. package/lib/src/managers/AttachmentManager.d.ts +86 -36
  14. package/lib/src/managers/AuthManager.d.ts +117 -7
  15. package/lib/src/managers/AuthManager.js +341 -37
  16. package/lib/src/managers/ConfigurationManager.d.ts +6 -0
  17. package/lib/src/managers/ExportManager.d.ts +16 -0
  18. package/lib/src/managers/ExportManager.js +25 -0
  19. package/lib/src/managers/FileManager.d.ts +182 -0
  20. package/lib/src/managers/ImportManager.d.ts +264 -16
  21. package/lib/src/managers/MediaManager.d.ts +11 -0
  22. package/lib/src/managers/SharedFolderManager.d.ts +320 -0
  23. package/lib/src/providers/BaseMediaProvider.d.ts +4 -0
  24. package/lib/src/providers/FileSystemMediaProvider.d.ts +25 -0
  25. package/lib/src/security/PolicyDecisionPoint.d.ts +3 -2
  26. package/lib/src/security/permissions.generated.d.ts +3 -3
  27. package/lib/src/types/File.d.ts +90 -0
  28. package/lib/src/types/express.d.ts +3 -1
  29. package/lib/src/types/linkedom.d.ts +2 -0
  30. package/lib/src/utils/auditEventNames.generated.d.ts +2 -0
  31. package/lib/src/utils/auditEventNames.generated.js +2 -0
  32. package/lib/src/utils/auditEvents.d.ts +3 -0
  33. package/lib/src/utils/auditEvents.js +3 -0
  34. package/lib/src/utils/fileMigrationPlan.d.ts +81 -0
  35. package/lib/src/utils/mediaFolderOverlap.d.ts +50 -0
  36. package/lib/src/utils/privateStoreAccess.d.ts +21 -0
  37. package/lib/src/utils/privateStoreAccess.js +11 -0
  38. package/lib/src/utils/privateStoreImport.d.ts +0 -10
  39. package/lib/src/utils/privateStorePath.d.ts +12 -0
  40. package/lib/src/utils/privateStorePath.js +18 -0
  41. package/lib/src/utils/recoveryWordsFile.d.ts +54 -0
  42. package/lib/src/utils/sessionEnd.d.ts +1 -0
  43. package/lib/src/utils/sessionEnd.js +4 -0
  44. package/lib/src/utils/sessionIdle.d.ts +4 -0
  45. package/lib/src/utils/sessionIdle.js +6 -0
  46. package/package.json +1 -1
  47. package/lib/src/providers/mediaFilenameDate.d.ts +0 -22
@@ -0,0 +1,320 @@
1
+ import BaseManager, { type BackupFolder } from './BaseManager.js';
2
+ import type { WikiEngine } from '../types/WikiEngine.js';
3
+ import { type ActorContext } from '../context/ActorContext.js';
4
+ import { type RecoveryWordsExport } from '../utils/recoveryWordsFile.js';
5
+ /** The folder every signed-in user may open (decision; the shipped `shared-folder-default` policy). */
6
+ export declare const DEFAULT_SHARED_FOLDER = "default";
7
+ /** The Executive shared folder (#2095); who opens it is the shipped policies' (`shared-folder-executive-read`, `shared-folder-executive-file`, `shared-folder-admin`). */
8
+ export declare const EXECUTIVE_SHARED_FOLDER = "executive";
9
+ /** The folders every site has, made once the recovery words are confirmed (#2125, #2095). */
10
+ export declare const SHIPPED_SHARED_FOLDERS: readonly string[];
11
+ /** Who may read and who may file in a folder, as the Shared folders screen sets it (#2126). */
12
+ export interface SharedFolderAccess {
13
+ /** Roles granted `shared-folder-read` on `shared/<name>` by the screen's policy. */
14
+ read: string[];
15
+ /** Roles granted `shared-folder-file` on `shared/<name>` by the screen's policy. */
16
+ file: string[];
17
+ }
18
+ /** A policy in force that reaches a folder and that the screen did not write: shipped, an add-on's, or the site's own. Shown, never changed here. */
19
+ export interface SharedFolderOtherPolicy {
20
+ id: string;
21
+ name: string;
22
+ effect: string;
23
+ /** Its subjects as written (`role:editor`, `job:nightly`); empty: everyone, anonymous visitors included. */
24
+ subjects: string[];
25
+ /** Which of `shared-folder-read` and `shared-folder-file` it decides, as the PDP matches actions (an empty list or `*` is both). */
26
+ actions: string[];
27
+ /** It has an id the screen writes for this folder: saving the folder is refused until it is renamed or removed. */
28
+ reserved: boolean;
29
+ }
30
+ /** The words waiting for this admin, as the confirm step needs them: never the words. */
31
+ export interface WaitingWords {
32
+ token: string;
33
+ /** Whether they replace a set already saved (a new set, #2144). */
34
+ replacing: boolean;
35
+ }
36
+ /** What Admin → Backup shows about the organization's recovery words: never the words. */
37
+ export interface SharedFoldersWordsInfo {
38
+ /** Which set (`recoveryWordsId`): what the admin's confirmation is bound to. */
39
+ wordsId: string;
40
+ madeAt: string;
41
+ shownTo: string;
42
+ /** Whether any folder key is sealed for this set: such a set can never be discarded. */
43
+ protectsFolders: boolean;
44
+ }
45
+ /** The door said no: not allowed, or the request does not fit the state. Nothing was changed. */
46
+ export declare class SharedFolderRefusedError extends Error {
47
+ readonly name = "SharedFolderRefusedError";
48
+ }
49
+ /** The words typed back are not the ones shown (#2144). Nothing was saved; the words still wait, to be typed again. */
50
+ export declare class SharedFolderWordsMismatchError extends SharedFolderRefusedError {
51
+ constructor();
52
+ }
53
+ /** The keys cannot be used right now (no server key, or not the one they were made under); the message says why. */
54
+ export declare class SharedFoldersUnavailableError extends Error {
55
+ readonly name = "SharedFoldersUnavailableError";
56
+ }
57
+ declare class SharedFolderManager extends BaseManager {
58
+ readonly description = "The organization's shared folders: each folder's key, the organization key and the recovery words (#1775, #2124)";
59
+ private storageDir;
60
+ /** The 32-byte wrap key derived from `NGDPBASE_SHARED_FOLDERS_KEY`, read once at start; null without one. */
61
+ private wrapKey;
62
+ private keyFingerprint;
63
+ /** Why the keys cannot be used, or null when they can. */
64
+ private unavailable;
65
+ private file;
66
+ private readonly pending;
67
+ private readonly serial;
68
+ constructor(engine: WikiEngine);
69
+ initialize(config?: Record<string, unknown>): Promise<void>;
70
+ shutdown(): Promise<void>;
71
+ private configuredStorageDir;
72
+ /** The wrapped keys are in the backup; a backup from before #2124 has none, so it is optional. */
73
+ backupFolders(): BackupFolder[];
74
+ /** The organization's recovery words as made (never the words), or null: none made yet. */
75
+ recoveryWordsInfo(): SharedFoldersWordsInfo | null;
76
+ /**
77
+ * Whether the admin shown the current words confirmed saving them, at the
78
+ * wording's current version: their consent in `shared-folders-recovery-words`
79
+ * for this set exists (ConsentManager's fact, asked each time). False with
80
+ * no words, no ConsentManager, or no wording page (then nobody could have
81
+ * confirmed what it says).
82
+ */
83
+ recoveryWordsConfirmed(): Promise<boolean>;
84
+ /** Why the keys cannot be used now, or null when they can. */
85
+ unavailableReason(): string | null;
86
+ /**
87
+ * Make the organization's 12 recovery words, to show to the admin `ctx`
88
+ * once (decision 4). Nothing is saved: the words wait, in memory only, for
89
+ * that admin's `saveRecoveryWords` with the returned token, for
90
+ * `ngdpbase.shared-folders.words-pending-minutes`. A second call replaces
91
+ * the first set unsaved. Refused while a set is saved: an unconfirmed set is
92
+ * discarded first (`discardUnconfirmedWords`), and a saved set is replaced
93
+ * with `prepareNewRecoveryWords`.
94
+ */
95
+ prepareRecoveryWords(ctx: ActorContext): Promise<{
96
+ token: string;
97
+ words: string[];
98
+ }>;
99
+ /**
100
+ * Make a new set of recovery words to replace the saved set (#2144): the
101
+ * admin `ctx`, with a fresh sign-in (`admin-system` and the step-up
102
+ * permission). Shown and confirmed exactly as the first set; nothing changes
103
+ * until `saveRecoveryWords` with all 12 typed back, which seals every folder
104
+ * key for the new words and retires the old ones. Abandoned or discarded
105
+ * (`discardRecoveryWords`), the old set stays.
106
+ */
107
+ prepareNewRecoveryWords(ctx: ActorContext): Promise<{
108
+ token: string;
109
+ words: string[];
110
+ }>;
111
+ /** Make 12 words and hold them, for `ctx` only, until saved, discarded or expired. Replaces any set waiting for them. */
112
+ private holdNewWords;
113
+ /** The words waiting for `ctx` to type them back, or null: none, or they expired. Never the words. */
114
+ waitingWords(ctx: ActorContext): WaitingWords | null;
115
+ /**
116
+ * The words waiting for `ctx`, to download as a file or print (#2144),
117
+ * while they still wait: once saved they are gone and cannot be made again.
118
+ * Recorded first as `recovery-words-export` (who, which set, how; never the
119
+ * words; on-failure refuse). Refused for a token that is not the one shown
120
+ * to them, or expired; the words are not discarded by a refused export.
121
+ */
122
+ pendingRecoveryWords(ctx: ActorContext, token: string, how: RecoveryWordsExport): Promise<{
123
+ words: string[];
124
+ madeAt: Date;
125
+ wordsId: string;
126
+ }>;
127
+ /** Forget the words shown to `ctx` and not saved: they are never shown again. A set they were to replace stays. */
128
+ discardRecoveryWords(ctx: ActorContext): void;
129
+ /** The words waiting for `ctx`, or undefined; expired words are forgotten. */
130
+ private livePending;
131
+ /**
132
+ * Discard the saved recovery words while the admin shown them has not
133
+ * confirmed saving them (operator, 2026-10-29): any holder of
134
+ * `admin-system` may, so a reminder that nobody can clear (that admin's
135
+ * account deleted, say) can always be cleared by making a new set and
136
+ * confirming it themselves. Refused once the set is confirmed, and while
137
+ * any folder key is sealed for it (`createFolderKey` needs a confirmed set,
138
+ * so such a set was confirmed once). Recorded as
139
+ * `shared-folder-words-discard` before the words are removed (on-failure
140
+ * refuse).
141
+ */
142
+ discardUnconfirmedWords(ctx: ActorContext): Promise<void>;
143
+ /**
144
+ * Save the recovery words shown to `ctx` (`prepareRecoveryWords` or
145
+ * `prepareNewRecoveryWords`) once they proved they kept them: `typed` is
146
+ * all 12 typed back (#2144). The caller asks for the tick first, and records
147
+ * the confirmation with ConsentManager after (`wordsId`).
148
+ *
149
+ * - A token that is not the one shown to them, or expired: refused, and the
150
+ * words are discarded.
151
+ * - Words that do not match: `SharedFolderWordsMismatchError`; nothing is
152
+ * saved and the words still wait, to be typed again.
153
+ * - The first set: recorded as `shared-folder-words-create`, then saved.
154
+ * Refused when a set was saved meanwhile.
155
+ * - A new set (#2144): asks the fresh sign-in again, seals every folder key
156
+ * again for the new words from its server-key wrap, records
157
+ * `shared-folder-words-replace`, then writes the new set and the sealed
158
+ * keys as one file renamed into place: the old words stop opening the
159
+ * folders, and on any failure before that the old set stays as it was.
160
+ * Refused when the saved set changed meanwhile.
161
+ *
162
+ * Each record is on-failure refuse, written before the save.
163
+ *
164
+ * @returns Which set of words was saved (`recoveryWordsId`)
165
+ */
166
+ saveRecoveryWords(ctx: ActorContext, token: string, typed: string): Promise<string>;
167
+ /**
168
+ * Every folder key in `folders`, its recovery copy sealed again for the
169
+ * words of `recovery` (#2144), opened with the server key. Nothing is
170
+ * written; a key that cannot be opened throws, and the caller keeps the
171
+ * old set.
172
+ */
173
+ private sealedForWords;
174
+ /**
175
+ * Make folder `folderId`'s own key (decision 2): a random key, wrapped
176
+ * under the server key and sealed for the recovery words, so it needs the
177
+ * words to exist and to be confirmed saved by the admin shown them
178
+ * (`recoveryWordsConfirmed`; operator, 2026-10-29): no folder key is ever
179
+ * protected only by words nobody saved. Recorded as `shared-folder-key-create` before it is
180
+ * saved (on-failure refuse). The key itself is not returned; open it with
181
+ * `openFolderKey`.
182
+ */
183
+ createFolderKey(ctx: ActorContext, folderId: string): Promise<void>;
184
+ /**
185
+ * Folder `folderId`'s key, unwrapped with the server key, for a caller the
186
+ * PDP permits `shared-folder-read` on the folder (#2125). The caller holds
187
+ * it for the moment it needs it and zeroes it. The page and file doors do
188
+ * not call this: they reach the key through the store I/O
189
+ * (`sharedFolderKeyFor`), only for a context permitted on the folder.
190
+ */
191
+ openFolderKey(ctx: ActorContext, folderId: string): Promise<Buffer>;
192
+ /** Folder `folderId`'s key with the server key, or undefined: no such folder, or the keys cannot be used. Never asks who; its callers have. */
193
+ private folderKey;
194
+ /** The folders there are, by name: each has its key. */
195
+ folderNames(): string[];
196
+ /**
197
+ * Make shared folder `name` (#2125): its key (`createFolderKey`, which
198
+ * needs the recovery words confirmed) and its place for pages, sealed with
199
+ * that key. Admins only (decision 8) until the Shared folders screen
200
+ * (slice 3). A new folder is opened by admins (`shared-folder-admin`) and
201
+ * read by executives (`shared-folder-executive-read`), and by nobody else
202
+ * until a policy names `shared/<name>`; `default` is shipped open to every
203
+ * signed-in user.
204
+ * Recorded as `shared-folder-create` before anything is made (on-failure
205
+ * refuse). Refused when it exists.
206
+ */
207
+ createFolder(ctx: ActorContext, name: string): Promise<void>;
208
+ /**
209
+ * Make the shipped folders (`SHIPPED_SHARED_FOLDERS`: `/shared/default` and
210
+ * `/shared/executive`) that are not there yet, once they can be made: the
211
+ * recovery words are confirmed. Finishing the words does this, and so does
212
+ * every start (`ensureShippedFoldersAtBoot`), so a site that confirmed its
213
+ * words before a shipped folder existed gets it on its next start.
214
+ * Idempotent. Returns the folders made now.
215
+ */
216
+ ensureShippedFolders(ctx: ActorContext): Promise<string[]>;
217
+ /**
218
+ * At start, make the shipped folders a site with confirmed words does not
219
+ * have yet (#2095), under the system context. Best-effort: a failure is
220
+ * logged and never blocks start-up; nothing is tried while the keys cannot
221
+ * be used.
222
+ */
223
+ ensureShippedFoldersAtBoot(): Promise<void>;
224
+ /**
225
+ * May `ctx` do `action` (a page or file action, or `shared-folder-read` /
226
+ * `shared-folder-file`) in folder `name`? The PDP's answer
227
+ * (`permitsInSharedFolder`); false for a folder that does not exist.
228
+ */
229
+ mayInFolder(ctx: ActorContext, action: string, name: string): Promise<boolean>;
230
+ /**
231
+ * The folders `ctx` may open (`shared-folder-read`), by name. Asking opens
232
+ * each one's key to this context (`sharedFolderKeyFor`), so the page and
233
+ * file doors can read what this reader may. The session middleware asks it
234
+ * on every request, so a grant removed ends access on the next request
235
+ * (decision 5).
236
+ */
237
+ foldersReadableBy(ctx: ActorContext): Promise<string[]>;
238
+ /**
239
+ * Record that `ctx` opened `what` (a page, its history, or a file) in
240
+ * folder `name` (`shared-folder-read`, on by default: every read of a
241
+ * shared folder is recorded, #1775). Called by what serves it, after the
242
+ * door allowed it. The writes are recorded by the page and file doors.
243
+ */
244
+ recordRead(ctx: ActorContext, name: string, what: {
245
+ page?: string;
246
+ attachmentId?: string;
247
+ action?: string;
248
+ }): Promise<void>;
249
+ /**
250
+ * Who may read and who may file in folder `name`, as the Shared folders
251
+ * screen set it: the roles its own policies (`shared-folder-access-<name>-read`
252
+ * / `-file`, in the site's custom configuration, in the shape the screen
253
+ * writes) name, and every other policy in force that reaches the folder,
254
+ * shown as it is, matched as PolicyEvaluator matches it. A policy with one of
255
+ * the screen's ids that the screen did not write is shown, marked
256
+ * `reserved`. `admin-system`, as the screen is. Display only: the PDP
257
+ * decides every access.
258
+ */
259
+ folderAccess(ctx: ActorContext, name: string): Promise<SharedFolderAccess & {
260
+ others: SharedFolderOtherPolicy[];
261
+ }>;
262
+ /** The roles a folder's access may name: every role there is but `anonymous`, by name. */
263
+ grantableRoles(): string[];
264
+ /**
265
+ * Set who may read and who may file in folder `name` (#2126, decision 8):
266
+ * writes the site's policies `shared-folder-access-<name>-read` (granting
267
+ * `shared-folder-read` on `shared/<name>`) and `-file` (`shared-folder-file`)
268
+ * to the roles given, through ConfigurationManager, and removes one whose
269
+ * list is empty. `admin-system`, with a fresh sign-in (the step-up
270
+ * permission, as every other configuration write asks). Refused for a folder
271
+ * that does not exist, a role that does not exist, `anonymous` (a folder is
272
+ * never public), and a folder one of whose ids holds a policy the screen did
273
+ * not write (it is never overwritten). Recorded as
274
+ * `shared-folder-access-change` (who, the folder, the roles before and
275
+ * after) once the write has succeeded; a failed write is recorded as a
276
+ * failure, and a record that cannot be written puts the policies back
277
+ * (on-failure refuse). ConfigurationManager records the configuration
278
+ * change as well. Nothing is written or recorded when nothing changes.
279
+ * Returns whether it changed.
280
+ *
281
+ * Only the screen's own policies change: a shipped or add-on policy, or one
282
+ * the site wrote elsewhere, still applies (`folderAccess` shows them).
283
+ */
284
+ setFolderAccess(ctx: ActorContext, name: string, access: SharedFolderAccess): Promise<boolean>;
285
+ /**
286
+ * Folder `name`'s access as the screen sees it: the screen's own policies
287
+ * (the site's entry under each id, in the shape `accessPolicy` writes), the
288
+ * ids holding a policy the screen did not write (the site's in another
289
+ * shape, a deny say, or one in force from a shipped or add-on layer), and
290
+ * the policies in force.
291
+ */
292
+ private accessState;
293
+ /** The site's own entries of the access policies (the custom configuration layer), as they are: never the shipped or an add-on's. */
294
+ private siteAccessPolicies;
295
+ /** The pages directory and the configured vault layout, where folders are kept. */
296
+ private pagesPlace;
297
+ /**
298
+ * Folder `folderId`'s key, opened with the recovery words instead of the
299
+ * server key: what recovery after a lost server key will use (a later
300
+ * slice: a new server key, every folder key rewrapped). Throws when the
301
+ * words are wrong. Works with no server key at all.
302
+ */
303
+ openFolderKeyWithWords(ctx: ActorContext, folderId: string, words: string): Promise<Buffer>;
304
+ private requireAdmin;
305
+ /**
306
+ * The fresh sign-in a new set of words (#2144) and an access change (#2126)
307
+ * need: the PDP applies step-up to `FRESH_SIGN_IN_PERMISSION`, and never
308
+ * grants it to a delegated credential (an agent token).
309
+ */
310
+ private requireFreshSignIn;
311
+ /** The wrap key, or throws why there is none to use. */
312
+ private requireUsable;
313
+ private pendingMinutes;
314
+ private fileOrNew;
315
+ private keysPath;
316
+ private readKeysFile;
317
+ /** Owner-only, written whole and renamed into place, so a crash never leaves half a file. */
318
+ private writeKeysFile;
319
+ }
320
+ export default SharedFolderManager;
@@ -68,8 +68,12 @@ export interface ScanResult {
68
68
  skippedTruncated?: boolean;
69
69
  /** Number of indexed items with no usable capture date (EXIF/QuickTime) — they sort by file mtime (#807) */
70
70
  noCaptureDate?: number;
71
+ /** Paths of the first items counted in `noCaptureDate`, capped; the count stays exact (#2191) */
72
+ noCaptureDateFiles?: string[];
71
73
  /** Number of indexed items with a partial (year-only / year+month) EXIF date, defaulted to Jan 1 (#808) */
72
74
  partialCaptureDate?: number;
75
+ /** Paths of the first items counted in `partialCaptureDate`, capped; the count stays exact (#2191) */
76
+ partialCaptureDateFiles?: string[];
73
77
  /** Number of stale index entries pruned because their file vanished from a scanned folder (#867) */
74
78
  removed?: number;
75
79
  /** Total elapsed time in milliseconds */
@@ -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
  */
@@ -221,6 +240,12 @@ declare class FileSystemMediaProvider extends BaseMediaProvider {
221
240
  * Patterns ending with `/` are directory-only; all others match both files and dirs.
222
241
  */
223
242
  private matchesIgnorePattern;
243
+ /**
244
+ * Add a file to a capture-date list until CAPTURE_DATE_LIST_CAP (#2191).
245
+ * The path is relative to the media folder it was found under, so the
246
+ * notice and its email do not disclose the server's storage layout.
247
+ */
248
+ private listCaptureDateFile;
224
249
  /**
225
250
  * Process a single media file: stat, EXIF read, index update.
226
251
  */
@@ -131,9 +131,10 @@ export declare class PolicyDecisionPoint extends BaseManager {
131
131
  * {@link permits}, which is "may it act now", and named so: an affordance
132
132
  * (show the reveal button, show the sign-in methods section) asks this, so a
133
133
  * person whose sign-in is not fresh still sees the way to the prompt that
134
- * makes it fresh. A door never asks this.
134
+ * makes it fresh. A door never asks this. `on` asks it about a resource,
135
+ * as {@link decide} does (#2120: a vault page's export).
135
136
  */
136
- holds(subject: DecisionInput, action: string): Promise<boolean>;
137
+ holds(subject: DecisionInput, action: string, on?: Omit<DecisionRequest, 'action'>): Promise<boolean>;
137
138
  /**
138
139
  * Step-up (#1525, #1635), decided here — in the one place every door asks —
139
140
  * rather than by routes in front of the doors. A permission marked
@@ -1,6 +1,6 @@
1
1
  /** Every permission core declares. Addons generate their own union. */
2
2
  export type CorePermission =
3
- /** Change what protects your own account — password, email, sign-in methods (passkeys), vault takeout and import, approving a device (#1525). Granted wherever profile-manage is; asks for a fresh sign-in when listed in ngdpbase.auth.step-up */
3
+ /** Change what protects your own account — password, email, sign-in methods (passkeys), vault takeout and import, approving a device (#1525). Granted wherever profile-manage is; asks for a fresh sign-in, because the entry carries "step-up": true (#1638) */
4
4
  'account-security'
5
5
  /** View administration screens (read-only, no changes) */
6
6
  | 'admin-read'
@@ -20,7 +20,7 @@ export type CorePermission =
20
20
  | 'audit-export'
21
21
  /** Add a comment, and delete your own; deleting anyone's is admin-system (#1198) */
22
22
  | 'comment-create'
23
- /** Change any configuration setting, and download a backup (#1525). Admin only by default; asks for a fresh sign-in when listed in ngdpbase.auth.step-up */
23
+ /** Change any configuration setting, and download a backup (#1525). Admin only by default; asks for a fresh sign-in, because the entry carries "step-up": true (#1638) */
24
24
  | 'config-manage'
25
25
  /** Open the Executive Hub (/executive): the organization's summary cards and the requests waiting for the board (#2093). Held by the executive role; deciding a request still needs that request's own permission */
26
26
  | 'executive-read'
@@ -54,7 +54,7 @@ export type CorePermission =
54
54
  | 'search-page'
55
55
  /** Search users */
56
56
  | 'search-user'
57
- /** Unmask a configuration secret (#1525). Admin only by default; asks for a fresh sign-in when listed in ngdpbase.auth.step-up. Hashed credentials (agent tokens) and shown-once secrets can never be revealed */
57
+ /** Unmask a configuration secret (#1525). Admin only by default; asks for a fresh sign-in, because the entry carries "step-up": true (#1638). Hashed credentials (agent tokens) and shown-once secrets can never be revealed */
58
58
  | 'secret-reveal'
59
59
  /** End another person's signed-in session (#1638) */
60
60
  | 'session-revoke'
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The file record (#1852, epic #1851): one shape for every file the site
3
+ * keeps, read through `FileManager`, the one door.
4
+ *
5
+ * The decisions behind it are the epic's decision log
6
+ * (https://github.com/jwilleke/ngdpbase/issues/1851#issuecomment-6236003946);
7
+ * the practice is docs/guides/storage-developer-guide.md.
8
+ *
9
+ * It is not a new vocabulary. `AssetRecord` (src/types/Asset.ts) is already
10
+ * the internal, schema.org-named shape for a stored file (docs/schemas.md
11
+ * Decision 11), so the record takes its fields from there by name and adds
12
+ * only what an asset record does not say: where the file lives, who owns it,
13
+ * and its system-category. A field that changes in `AssetRecord` changes
14
+ * here too; there is no second copy to drift.
15
+ */
16
+ import type { AssetRecord } from './Asset.js';
17
+ /**
18
+ * Where a file lives: the public area, one user's vault, or one shared
19
+ * folder. A file is unique by its place plus its name; the same name may
20
+ * exist in two places.
21
+ *
22
+ * Each place is named by the identifiers the site already uses: a vault by
23
+ * its owner's username and its vault id (`pages/vaults/{owner}/{vault}/`), a
24
+ * shared folder by its name (`/shared/{folder}`, SharedFolderManager). The
25
+ * shared folders' internal container (`SHARED_FOLDER_OWNER`) is never a
26
+ * vault owner here: a folder is always `{ kind: 'shared-folder' }` (#2148).
27
+ */
28
+ export type FilePlace = {
29
+ kind: 'public';
30
+ } | {
31
+ kind: 'vault';
32
+ owner: string;
33
+ vault: string;
34
+ } | {
35
+ kind: 'shared-folder';
36
+ folder: string;
37
+ };
38
+ /**
39
+ * One file, as the door shows it.
40
+ *
41
+ * From `AssetRecord`, unchanged in meaning:
42
+ * - `id`: the stable identity, kept across renames and moves.
43
+ * - `filename`: the file's name, the one links use (`[Annual Report.pdf]`),
44
+ * unique within its place.
45
+ * - `name`: the human title, when one was given (schema.org `name`).
46
+ * - `description`, `keywords`: descriptive metadata.
47
+ * - `encodingFormat`: the media type (`application/pdf`, `image/png`…).
48
+ * - `contentSize`, `url`, `mentions` (the pages that show it).
49
+ * - `dateCreated` / `author`: when and by whom it was created, as recorded
50
+ * (the uploader; a legacy record may say `Unknown`).
51
+ * - `dateModified` / `editor`: when and by whom it last changed.
52
+ *
53
+ * Added here:
54
+ * - `place`: public, a vault, or a shared folder.
55
+ * - `owner`: the user it belongs to. A vault's files are the vault owner's.
56
+ * A public file's owner is its uploader when the record names a user, or
57
+ * `null` when it does not (`Unknown`, empty, an email used as a name):
58
+ * the migration gives those an owner from configuration.
59
+ * - `systemCategory`: the category whose rules apply. Per file, never by
60
+ * type. A public file today is the default category's; a vault file is
61
+ * the category that declares its vault, or `null` when none does.
62
+ */
63
+ export type FileRecord = Pick<AssetRecord, 'id' | 'filename' | 'name' | 'description' | 'keywords' | 'encodingFormat' | 'contentSize' | 'url' | 'mentions' | 'dateCreated' | 'author' | 'dateModified' | 'editor'> & {
64
+ place: FilePlace;
65
+ owner: string | null;
66
+ systemCategory: string | null;
67
+ };
68
+ /**
69
+ * What the operator inventory shows of one vault (#1859's dry run). Counts
70
+ * only: a vault's names, owners and metadata belong to its owner and never
71
+ * leave the door. A sealed vault can't be read without its owner's key, so
72
+ * only what is on disk is counted (bytes as stored, sealed) and its clashes
73
+ * are unknown (`null`).
74
+ */
75
+ export interface VaultFileSummary {
76
+ owner: string;
77
+ vault: string;
78
+ systemCategory: string | null;
79
+ /** Whether its index could be read. False for a sealed vault (no key here) or one refused as not yet opened. */
80
+ readable: boolean;
81
+ files: number;
82
+ bytes: number;
83
+ /** Files that would get a `-###` suffix in this vault; `null` when it can't be read. */
84
+ clashes: number | null;
85
+ }
86
+ /** Everything the site keeps as files, for the operator (#1859): the public files in full, each vault as counts. */
87
+ export interface FileInventory {
88
+ publicFiles: FileRecord[];
89
+ vaults: VaultFileSummary[];
90
+ }
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import type { ShareGrant } from './Share.js';
19
19
  import type { ProxyClaim } from '../managers/UserManager.js';
20
- import type { SignInRecord, PendingConnection, OutsideChoice, PendingStepUp } from '../managers/AuthManager.js';
20
+ import type { SignInRecord, PendingConnection, OutsideChoice, PendingStepUp, StepUpPrompt } from '../managers/AuthManager.js';
21
21
  declare module 'express-session' {
22
22
  interface SessionData {
23
23
  csrfToken?: string;
@@ -44,6 +44,8 @@ declare module 'express-session' {
44
44
  pendingStepUp?: PendingStepUp;
45
45
  /** The path whose next page view says "Confirmed — you can do it now" after a step-up (#2145). */
46
46
  stepUpConfirmed?: string;
47
+ /** A script's step-up prompts (#2193), the newest few: each id and whether it was confirmed or cancelled. */
48
+ stepUpPrompts?: StepUpPrompt[];
47
49
  /** A sign-in waiting for its second factor: the handle, and where to go after (#1523). No identity yet. */
48
50
  pendingSignIn?: string;
49
51
  pendingRedirect?: string;
@@ -60,6 +60,8 @@ declare module 'linkedom' {
60
60
  getElementById(id: string): LinkedomElement | null;
61
61
  querySelector(selector: string): LinkedomElement | null;
62
62
  querySelectorAll(selector: string): LinkedomElement[];
63
+ /** The whole document as HTML, its doctype included. */
64
+ toString(): string;
63
65
  }
64
66
 
65
67
  function parseHTML(html: string): { document: LinkedomDocument };
@@ -170,6 +170,8 @@ export declare const AUDIT_EVENT: {
170
170
  readonly SHARE_EXTEND: "share-extend";
171
171
  /** Share link revoked; pairs with token-revoke */
172
172
  readonly SHARE_REVOKE: "share-revoke";
173
+ /** An administrator changed who may read and who may file in a shared folder on the Shared folders screen (#2126): which folder, who, and the roles before and after for each. Recorded once the folder's policies are written; a failed write is recorded as a failure, with the error, and a record that cannot be written puts the policies back */
174
+ readonly SHARED_FOLDER_ACCESS_CHANGE: "shared-folder-access-change";
173
175
  /** An administrator created a shared folder (#2125): which folder and who. Recorded before the folder is made, which cannot proceed without it */
174
176
  readonly SHARED_FOLDER_CREATE: "shared-folder-create";
175
177
  /** A shared folder's own key was made (#2124): which folder and who, never the key. Recorded before the key is saved, which cannot proceed without it */
@@ -177,6 +177,8 @@ export const AUDIT_EVENT = {
177
177
  SHARE_EXTEND: 'share-extend',
178
178
  /** Share link revoked; pairs with token-revoke */
179
179
  SHARE_REVOKE: 'share-revoke',
180
+ /** An administrator changed who may read and who may file in a shared folder on the Shared folders screen (#2126): which folder, who, and the roles before and after for each. Recorded once the folder's policies are written; a failed write is recorded as a failure, with the error, and a record that cannot be written puts the policies back */
181
+ SHARED_FOLDER_ACCESS_CHANGE: 'shared-folder-access-change',
180
182
  /** An administrator created a shared folder (#2125): which folder and who. Recorded before the folder is made, which cannot proceed without it */
181
183
  SHARED_FOLDER_CREATE: 'shared-folder-create',
182
184
  /** A shared folder's own key was made (#2124): which folder and who, never the key. Recorded before the key is saved, which cannot proceed without it */
@@ -163,6 +163,8 @@ export type AttachmentDetail = {
163
163
  stored?: false;
164
164
  heldAs?: string;
165
165
  offeredAs?: string;
166
+ /** What vetting removed before the file was kept (#2149): one note per kind, counts only, never content. */
167
+ cleaned?: string[];
166
168
  }
167
169
  /** A sealed store's file: `owner` is the store owner's identity @id in the record, `ownerUser` their username beside it (#2041). */
168
170
  | {
@@ -172,6 +174,7 @@ export type AttachmentDetail = {
172
174
  store: string;
173
175
  ownerUser?: string;
174
176
  };
177
+ cleaned?: string[];
175
178
  };
176
179
  export type AttachmentInput = CommonInput & {
177
180
  op: AttachmentOp;
@@ -169,6 +169,9 @@ export function buildAttachmentAuditEvent(input) {
169
169
  metadata.offeredAs = input.offeredAs;
170
170
  }
171
171
  }
172
+ // #2149: what vetting removed, in either form: kinds and counts, nothing of the file.
173
+ if (input.cleaned && input.cleaned.length > 0)
174
+ metadata.cleaned = [...input.cleaned];
172
175
  const severity = op === 'delete'
173
176
  ? (viaToken ? 'high' : 'medium')
174
177
  : (viaToken ? 'medium' : 'low');