@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,182 @@
1
+ /**
2
+ * FileManager — the one door for files (#1852, epic #1851).
3
+ *
4
+ * It reads today's attachments as file records (src/types/File.ts): the
5
+ * shared pool as the public place, and every vault the requester may read.
6
+ * It writes through one door, `put` (#2148): a file goes into the place the
7
+ * caller chose, after the permission for that place, with AttachmentManager
8
+ * as the storage underneath; `placesWritableBy` lists the places a user may
9
+ * write to. Rename, move, delete and per-place indexes are later steps
10
+ * (docs/guides/storage-developer-guide.md).
11
+ *
12
+ * Read access is the existing rules, asked at this door:
13
+ * - the public place needs `asset-read`, from the PDP;
14
+ * - a vault is read through AttachmentManager's own vault reads, so the
15
+ * container rule (owner or delegate, never a role, admin included) decides
16
+ * exactly as it does for a vault page's files.
17
+ *
18
+ * The operator inventory (the migration dry run, #1859) is not on the
19
+ * engine's door: only an instance opened by {@link FileManager.openForOperator},
20
+ * from the command line, answers it.
21
+ */
22
+ import BaseManager from './BaseManager.js';
23
+ import { type AttachmentMetadata, type PutFile, type PutOptions, type UploadOptions } from './AttachmentManager.js';
24
+ import { type ActorContext } from '../context/ActorContext.js';
25
+ import { type AttachmentDetail } from '../utils/auditEvents.js';
26
+ import { type AssetRecord } from '../types/Asset.js';
27
+ import type { FileInventory, FilePlace, FileRecord } from '../types/File.js';
28
+ import type { WikiEngine } from '../types/WikiEngine.js';
29
+ import { type FileMigrationReport } from '../utils/fileMigrationPlan.js';
30
+ /**
31
+ * What `put` hands back (#2149): the stored file's metadata, and, when
32
+ * vetting cleaned it, what was removed (one note per kind), so the uploader
33
+ * can be told.
34
+ */
35
+ export type PutResult = AttachmentMetadata & {
36
+ cleaned?: string[];
37
+ };
38
+ /** A place the Upload file dialog offers (#2149): its key in the form, the words the user reads, and the place. */
39
+ export interface UploadPlace {
40
+ key: string;
41
+ label: string;
42
+ place: FilePlace;
43
+ }
44
+ /** Where the Upload file dialog was opened (#2149): the page's path, and the page it shows, when there is one. */
45
+ export interface UploadWhere {
46
+ path?: string;
47
+ pageName?: string;
48
+ }
49
+ declare class FileManager extends BaseManager {
50
+ /** Set only by {@link openForOperator}: this instance answers the inventory. */
51
+ private operatorInstance;
52
+ constructor(engine: WikiEngine);
53
+ /**
54
+ * Open the door read-only for an operator command (#1859): the site's
55
+ * configuration and attachment index, with nothing else started and
56
+ * nothing written. The site may be running; this process changes no file.
57
+ */
58
+ static openForOperator(): Promise<FileManager>;
59
+ /** Every file `ctx` may read, newest first; `place` narrows it to one place. */
60
+ list(ctx: ActorContext, filter?: {
61
+ place?: FilePlace;
62
+ }): Promise<FileRecord[]>;
63
+ /** The file with this id, when `ctx` may read it. */
64
+ get(id: string, ctx: ActorContext): Promise<FileRecord | null>;
65
+ /**
66
+ * The files named `filename` that `ctx` may read: one per place at most,
67
+ * since a name is unique within a place (today's pool can hold more than
68
+ * one, which is what the migration's suffixes are for). `place` narrows it.
69
+ */
70
+ findByName(filename: string, ctx: ActorContext, place?: FilePlace): Promise<FileRecord[]>;
71
+ /**
72
+ * The username a public record's uploader names (#2097): the uploader is
73
+ * stored as an identity @id, resolved through IdentityManager; an orphan,
74
+ * an @id with no account, `Unknown` or an email names no owner.
75
+ */
76
+ private ownerOf;
77
+ private readable;
78
+ /** `asset-read` for the public place, from the PDP. No PDP, no answer: closed. */
79
+ private mayReadPublic;
80
+ /**
81
+ * The places `ctx` may put a file in (#2148, operator 2026-10-30): the
82
+ * public place when the PDP grants `asset-upload`; each of the user's own
83
+ * vaults the vault rule lets them upload to (the default vault, and every
84
+ * vault they already have); each shared folder where they hold
85
+ * `shared-folder-file`. A place they may not write to is never listed.
86
+ *
87
+ * Each place is asked exactly as {@link put} asks it, so what is offered is
88
+ * what the door allows. An affordance: nothing is recorded.
89
+ */
90
+ placesWritableBy(ctx: ActorContext): Promise<FilePlace[]>;
91
+ /**
92
+ * Put a file in `place` (#2148): the one write door. The permission for
93
+ * that place is asked first, and a refusal stores nothing. Then ICE vets
94
+ * the file (#2149): its type from its bytes, and its format's shim cleans
95
+ * it; the cleaned copy is the one kept. AttachmentManager stores it as the
96
+ * place requires (sealed in a sealed vault or shared folder), and this door
97
+ * records the upload with what vetting removed (a sealed place's file by
98
+ * id only, #2048). No page decides the place: `options.pageName` is only
99
+ * the page that shows it.
100
+ *
101
+ * @throws Error 'Permission denied: …' when `ctx` may not write there, or
102
+ * is nobody (#1926); a route answers it 401 or 403.
103
+ * @throws IceVetRefused when the file could not be checked; nothing is stored
104
+ */
105
+ put(ctx: ActorContext, place: FilePlace, file: PutFile, options?: PutOptions): Promise<PutResult>;
106
+ /**
107
+ * Record a file stored or deleted (#2149: moved here from AttachmentManager,
108
+ * which stores and records nothing itself). An upload is `continue`: a
109
+ * failed record is logged, never fatal. A delete is critical: it rethrows,
110
+ * tagged `AUDIT_WRITE_FAILED`, so the delete does not happen unrecorded and
111
+ * a caller can tell an unwritable record from a failed delete.
112
+ *
113
+ * A sealed store's file is recorded by id and store only (#2048), its
114
+ * owner by identity @id with the username beside it (#2041).
115
+ * AttachmentManager's delete door calls this until deleting is FileManager's.
116
+ */
117
+ recordFileEvent(op: 'upload' | 'delete', context: ActorContext, detail: AttachmentDetail, wikiContext?: {
118
+ request?: {
119
+ ip?: string;
120
+ } | null;
121
+ }): Promise<void>;
122
+ /**
123
+ * What the Upload file dialog offers `ctx` (#2149): every place
124
+ * {@link placesWritableBy} lists, and nothing else, each with the words the
125
+ * user reads; and the one selected at first: where they are working
126
+ * ({@link workingPlaceKey}), else the public place, else the first. Never
127
+ * inferred from whether a page exists. An affordance: nothing is recorded.
128
+ */
129
+ uploadPlaces(ctx: ActorContext, where?: UploadWhere): Promise<{
130
+ places: UploadPlace[];
131
+ defaultKey: string | null;
132
+ }>;
133
+ /** The place a dialog key names, when it is one `ctx` may write to (#2149); null otherwise. Never a place it was not offered. */
134
+ placeForKey(ctx: ActorContext, key: string): Promise<FilePlace | null>;
135
+ /** A place's key in the dialog: `public`, `vault:<id>` (always the user's own) or `folder:<name>`. */
136
+ static placeKey(place: FilePlace): string;
137
+ /** The words the user reads for a place. */
138
+ private placeLabel;
139
+ /**
140
+ * Where the user is working, as a dialog key (#2149): a shared folder's
141
+ * list (`/shared/<folder>`) or one of its pages is that folder; a page in
142
+ * one of their own vaults is that vault. Read from the location, never
143
+ * from whether a page by that name exists; anywhere else is no place.
144
+ */
145
+ private workingPlaceKey;
146
+ /**
147
+ * The place today's page-tied upload goes to (#1396, #1398, #2125), until
148
+ * #2149/#2150 move its callers to a place the user chose: an upload onto a
149
+ * shared folder's page goes into that folder, saved or not; onto a vault
150
+ * page into that page's vault, by someone who may edit the page; with the
151
+ * Private tick into the uploader's own vault (`store`, or their default);
152
+ * otherwise the public place.
153
+ */
154
+ placeForPageUpload(ctx: ActorContext, options: Pick<UploadOptions, 'pageName' | 'private' | 'store'>): Promise<FilePlace>;
155
+ /** May `ctx` put a file in `place`? The existing rule for each kind, asked of its owner. */
156
+ private mayPut;
157
+ /** The user's own vaults a file may go in: their default vault, and every vault they already have. */
158
+ private ownVaultIds;
159
+ private storeLayout;
160
+ /** A well-formed place: a vault is a user's (never the shared folders' container), with valid ids. */
161
+ private static isPlace;
162
+ /** A place in a log line. Ids only, never a file's name. */
163
+ private static describePlace;
164
+ /**
165
+ * Everything kept as files, for the migration dry run: the public files in
166
+ * full and each vault as counts (files, bytes, clash renames), never its
167
+ * names. Only on an instance opened by {@link openForOperator}, for an
168
+ * operator's context.
169
+ */
170
+ inventory(ctx: ActorContext): Promise<FileInventory>;
171
+ /** The dry run: the inventory, planned with the configured values. Writes nothing. */
172
+ migrationDryRun(ctx: ActorContext): Promise<FileMigrationReport>;
173
+ /** An asset record as a file record: the same fields, plus place, owner and system-category. */
174
+ static toFileRecord(asset: AssetRecord, place: FilePlace, owner: string | null, systemCategory: string | null): FileRecord;
175
+ private configManager;
176
+ /** The public place's category today: the default entry of `ngdpbase.system-category`. */
177
+ private defaultCategory;
178
+ /** The category that declares this vault, or null when none does. */
179
+ private vaultCategory;
180
+ private suffixDigits;
181
+ }
182
+ export default FileManager;
@@ -33,7 +33,7 @@ import { IContentConverter } from '../converters/IContentConverter.js';
33
33
  import { type ZipReadLimits } from '../utils/zipArchive.js';
34
34
  import { type JobContext } from '../context/JobContext.js';
35
35
  import { type IceNameForm } from '../ice/givenName.js';
36
- import type { IceLedger, IceOriginalChoices, IceReceiver, IceRunReport, IceRunRequest, IceShim, RecentImport, RegisteredShim } from '../ice/types.js';
36
+ import type { IceExportFormat, IceExportReport, IceExportRequest, IceExportSource, IceLedger, IceOriginalChoices, IceReceiver, IceRunReport, IceRunRequest, IceShim, IceTarget, IceUpload, IceVetResult, RecentImport, RegisteredShim } from '../ice/types.js';
37
37
  /**
38
38
  * Options for import operations
39
39
  */
@@ -218,6 +218,31 @@ export declare class ImportUploadRefused extends Error {
218
218
  readonly reason: string;
219
219
  constructor(file: string, reason: string);
220
220
  }
221
+ /**
222
+ * An export that is not started (#2120): the thing is not there, the user may
223
+ * not export it, or the format is not one offered for it. A route answers it
224
+ * as "not found", so it never tells the user which.
225
+ */
226
+ export declare class IceExportRefused extends Error {
227
+ constructor(message?: string);
228
+ }
229
+ /**
230
+ * An export not started because too much is already being made (#2120): the
231
+ * user's own queued exports are at `max-queued-per-user`, or every held run is
232
+ * still being made. A route answers it as 429; nothing was read or queued.
233
+ */
234
+ export declare class IceExportBusy extends Error {
235
+ constructor(message: string);
236
+ }
237
+ /**
238
+ * An export refused only because the user's sign-in is not fresh enough
239
+ * (#2120): they hold `permission` on the thing, and step-up asks for it. A
240
+ * route answers it with the step-up prompt, not "not found".
241
+ */
242
+ export declare class IceExportNeedsFreshSignIn extends Error {
243
+ readonly permission: string;
244
+ constructor(permission: string);
245
+ }
221
246
  /** One page's outcome in a takeout import (#1472). */
222
247
  export type TakeoutPageOutcome = {
223
248
  title: string;
@@ -272,13 +297,11 @@ export interface UsableReceiver {
272
297
  originals: Required<Omit<IceOriginalChoices, 'types'>>;
273
298
  /** What a name the user gives becomes (#2130), when the receiver takes one: the screen offers the field only then. */
274
299
  names?: IceNameForm;
300
+ /** Where within it this user may import (#2118), for a receiver with places: the screen offers a choice of them. */
301
+ targets?: IceTarget[];
275
302
  }
276
- /**
277
- * Import Manager class
278
- *
279
- * Manages content import with extensible format support via converter registry.
280
- */
281
303
  declare class ImportManager extends BaseManager {
304
+ #private;
282
305
  /** Registry of format converters */
283
306
  private converterRegistry;
284
307
  /** ICE (#1979): shims by id (`<owner>.<name>`), in registration order. */
@@ -287,13 +310,19 @@ declare class ImportManager extends BaseManager {
287
310
  private readonly receiverRegistry;
288
311
  /** ICE: runs this process knows about, oldest first. */
289
312
  private readonly iceRuns;
290
- /** ICE: runs waiting for the background job, each with its own context. */
313
+ /** ICE (#2171): a rechecking run's store, as fingerprint → id, read once per run and store; dropped when the run ends. */
314
+ private readonly storeHoldings;
315
+ /** ICE: work waiting for the background job, each with its own context: an import run or an export (#2120). */
291
316
  private readonly iceQueue;
292
317
  /** ICE: true while a background job is draining the queue. */
293
318
  private iceDraining;
294
319
  /** ICE: the last background run id the queue was handed to. */
295
320
  private iceLastJobRunId;
296
321
  private iceLedger;
322
+ /** ICE export (#2120): the owners' doors for reading what is exported, by id. */
323
+ private readonly exportSourceRegistry;
324
+ /** ICE export: runs this process holds, oldest first, each with its file once made. Never written to disk. */
325
+ private readonly exportRuns;
297
326
  constructor(engine: WikiEngine);
298
327
  /**
299
328
  * Initialize the manager
@@ -319,7 +348,9 @@ declare class ImportManager extends BaseManager {
319
348
  /**
320
349
  * Register a receiver (D4): a door that already exists. Core's are
321
350
  * `core.pages` and `core.files`; an add-on registers its own manager's door
322
- * through `host.registerReceiver`.
351
+ * through `host.registerReceiver`. Refuses `writesEncrypted` from every
352
+ * caller, whatever owner it names: core registers its own receivers
353
+ * through the private `#registerReceiver` (#2179).
323
354
  *
324
355
  * @returns The receiver's id, `<owner>.<name>`
325
356
  */
@@ -350,12 +381,35 @@ declare class ImportManager extends BaseManager {
350
381
  * `ngdpbase.import.engine.default-original` when allowed, else the first.
351
382
  */
352
383
  private originalChoices;
384
+ /**
385
+ * The request's place within the receiver, checked (#2118): one the
386
+ * receiver's `targets` gives this context, for a receiver with places,
387
+ * and none for any other. An unknown receiver is left for the run to report.
388
+ *
389
+ * @throws Error when the place is missing, not this user's, or not the receiver's
390
+ */
391
+ private checkTarget;
353
392
  /**
354
393
  * The request as the engine runs it: the receiver's default original when
355
394
  * none was chosen, and a refusal for a choice the receiver does not offer
356
395
  * (D11, #2021). An unknown receiver is left for the run to report.
357
396
  */
358
397
  private withOriginal;
398
+ /**
399
+ * Vet one file before it is kept as a file (#2149): its type from its
400
+ * bytes (#1624), then its format's shim, when that shim is switched on,
401
+ * cleans it. FileManager asks this for every file it stores, and keeps the
402
+ * bytes returned, never the ones it sent; `removed` says what was taken
403
+ * out, for the uploader and the upload's record.
404
+ *
405
+ * Runs in the caller's request: cleaning is one linear pass over a file no
406
+ * larger than the site's upload limit. It decides nothing about permission:
407
+ * the caller has asked that first.
408
+ *
409
+ * @param ctx - Who is keeping the file (P1); only named in the log line when something was cleaned
410
+ * @throws IceVetRefused when the cleaning shim fails: the file could not be checked, so it is not kept
411
+ */
412
+ vetFile(upload: IceUpload, ctx: ActorContext): Promise<IceVetResult>;
359
413
  /**
360
414
  * Run one import now and wait for it. For code already off the request
361
415
  * path: an add-on's own background job, a test. A route uses
@@ -383,6 +437,11 @@ declare class ImportManager extends BaseManager {
383
437
  * the store's sealed index entry written. Each is recorded in the ledger
384
438
  * as the keep-once record (#2011), so the run reuses it and needs no key.
385
439
  *
440
+ * A receiver whose `receive` itself may write into an encrypted store
441
+ * (`writesEncrypted`, #2118) has the whole run now, in this request, with
442
+ * `ctx` and its key, and the run's id is returned when it has ended: no
443
+ * key is ever carried into a job (#2034).
444
+ *
386
445
  * @throws ImportUploadRefused when an original could not be kept; nothing is queued
387
446
  */
388
447
  enqueueUpload(request: IceRunRequest, ctx: ActorContext): Promise<string>;
@@ -407,14 +466,105 @@ declare class ImportManager extends BaseManager {
407
466
  /**
408
467
  * Is an import into this receiver confidential (#2110)? Only when its
409
468
  * destination is encrypted: a receiver that keeps into a sealed store
410
- * (`keepsEncrypted`). No receiver can declare itself so otherwise. A
469
+ * (`keepsEncrypted`), or whose own door writes into one
470
+ * (`writesEncrypted`, #2118). No receiver can declare itself so otherwise. A
411
471
  * confidential import's file names are shown to the uploader and kept in
412
- * the audit log, and never written to application logs, notices or admin
413
- * screens; every other import logs them as before.
472
+ * the audit log, and never written to application logs, notices, admin
473
+ * screens or the import ledger (#2161); every other import logs them as before.
414
474
  */
415
475
  isConfidential(receiverId: string): boolean;
416
476
  /** Use this ledger instead of the file one: a test's, or a database table's. */
417
477
  setImportLedger(ledger: IceLedger): void;
478
+ /**
479
+ * Register an export source (#2120): the owner's door for reading what is
480
+ * exported, checking its existing read permission (D5). Core's is
481
+ * `core.pages`.
482
+ *
483
+ * @returns The source's id, `<owner>.<name>`
484
+ * @throws Error when the source is malformed or its id is taken
485
+ */
486
+ registerExportSource(owner: string, source: IceExportSource): string;
487
+ /**
488
+ * The formats the Export action offers this context for one thing (#2120):
489
+ * none when the source's owner would not let it export the thing (asked
490
+ * as an affordance, recording nothing), else each `out` shim, switched on,
491
+ * that can make a file from what the source yields.
492
+ */
493
+ exportFormatsFor(sourceId: string, ref: string, ctx: ActorContext): Promise<IceExportFormat[]>;
494
+ /**
495
+ * Start an export (#2120), from the request that asked: the source reads
496
+ * the thing now, with the request's own context, so its owner's door
497
+ * decides with everything the request carries (step-up, a vault's key)
498
+ * and records the export. The shims run off the request path, in the ICE
499
+ * background job, as the user's own job context. Returns the run id at once.
500
+ *
501
+ * Before anything is read, the user may have at most
502
+ * `max-queued-per-user` exports queued or running, and a held run must be
503
+ * free to make room under `max-kept-runs`. Once read, the items count
504
+ * toward `max-held-bytes` until their file is made.
505
+ *
506
+ * @throws IceExportRefused when the thing is not there, the user may not
507
+ * export it, or the format is not one offered for it
508
+ * @throws IceExportNeedsFreshSignIn when the user holds the permission but
509
+ * step-up asks for a fresh sign-in
510
+ * @throws IceExportBusy when the user, or the site, is making as many
511
+ * exports as it may
512
+ * @throws IceExportFailed when the thing is larger than this site exports
513
+ */
514
+ startExport(request: IceExportRequest, ctx: ActorContext): Promise<string>;
515
+ /** An export's status, to the user it runs as; null to anyone else, and once it is let go. */
516
+ getExportRun(runId: string, ctx: ActorContext): IceExportReport | null;
517
+ /** An export's file, to the user it runs as, while it is kept; null otherwise. */
518
+ exportFile(runId: string, ctx: ActorContext): {
519
+ name: string;
520
+ mediaType: string;
521
+ bytes: Buffer;
522
+ } | null;
523
+ /** The source's formats now: the enabled out shims' chains from what it yields. */
524
+ private exportChainsFor;
525
+ private executeExport;
526
+ /** The source's own record of how an export ended, if it keeps one; a record that fails is logged, never thrown. */
527
+ private recordExportOutcome;
528
+ /**
529
+ * Before a new export is read (#2120): refuse when the user already has
530
+ * `max-queued-per-user` exports queued or running, and let finished runs go
531
+ * until there is room under `max-kept-runs`. A queued or running run is
532
+ * never let go.
533
+ *
534
+ * @throws IceExportBusy when there is no room
535
+ */
536
+ private makeRoomForExport;
537
+ /**
538
+ * Hold a new run with the bytes its items take until its file is made,
539
+ * which count toward `max-held-bytes` (#2120). Finished files are let go
540
+ * to make room, the user's own first ({@link finishedExportRuns}).
541
+ *
542
+ * @throws IceExportFailed when the items alone are more than it may hold
543
+ * @throws IceExportBusy when queued and running exports alone hold too much
544
+ */
545
+ private holdExportRun;
546
+ /**
547
+ * Keep a made file in memory until `keep-minutes` have passed, never on
548
+ * disk: an export of a sealed vault's page must not rest unencrypted. Its
549
+ * items' bytes are released for the file's. Other finished files are let
550
+ * go when all would hold more than `ngdpbase.export.engine.max-held-bytes`,
551
+ * the user's own first.
552
+ */
553
+ private keepExportFile;
554
+ /** Let finished files go, `username`'s own first, until all held bytes are at most `limit`. */
555
+ private letFilesGo;
556
+ /** Every byte exports hold: made files, and the items of runs not yet made. */
557
+ private heldExportBytes;
558
+ /**
559
+ * The runs that may be let go, in the order they are: done or failed only,
560
+ * `username`'s own oldest first, then everyone else's oldest first, so one
561
+ * user's exports push out their own before anyone else's (#2120).
562
+ */
563
+ private finishedExportRuns;
564
+ private dropExportRun;
565
+ /** `ngdpbase.export.engine.*`: every limit an export applies is configuration (the chain's length is import's). */
566
+ private exportSettings;
567
+ private exportManager;
418
568
  /**
419
569
  * The ledger scope a run uses (#2013): bound to the receiver and to the user
420
570
  * `ctx` names, by their identity `@id`; their username only when the
@@ -441,7 +591,7 @@ declare class ImportManager extends BaseManager {
441
591
  private enabledShims;
442
592
  /**
443
593
  * Hand the queue to the background job. One job run drains every queued
444
- * import, each under its own context. A job run that has finished draining
594
+ * import and export, each under its own context. A job run that has finished draining
445
595
  * but is not yet marked done answers `enqueue` with its old run id; then
446
596
  * try again shortly, so nothing queued is stranded.
447
597
  */
@@ -471,14 +621,96 @@ declare class ImportManager extends BaseManager {
471
621
  */
472
622
  private pagesReceiver;
473
623
  /**
474
- * `core.files` (D4): a file through AttachmentManager's upload door, which
475
- * checks `asset-upload` itself. Also where a kept original goes (D11) when
476
- * the chosen receiver does not keep its own. It becomes FileManager's write
477
- * door once the files layer has one (#1851). A name the user gave (#2130)
624
+ * `page-export` for this page (#2120). For a page in a vault, asked about
625
+ * the page as the route door asks it (`hasPermissionOn`, #1539): the
626
+ * container rule, then the capability with the page's vault, so a
627
+ * site-wide role does not reach in. For any other page, the capability.
628
+ * An affordance asks whether it is held (no step-up); the door whether it
629
+ * is permitted, and a refusal to someone who holds it is step-up's.
630
+ */
631
+ private mayExportPage;
632
+ /**
633
+ * `core.pages` as an export source (#2120, D5: pages export checks
634
+ * `page-export`). A page is offered for export when its reader may see it
635
+ * and holds `page-export` (affordances, recording nothing). Reading it
636
+ * for export asks `page-export` ({@link mayExportPage}), then reads through the page
637
+ * door (`readPage`), which decides `page-read` on the page and records a
638
+ * refusal. The export is recorded as `page-export`, with the format and
639
+ * the run, once the run has ended: a success only when the file was made.
640
+ */
641
+ private pagesExportSource;
642
+ /**
643
+ * `core.files` (D4): a file into the public place through FileManager's
644
+ * write door, `put` (#2149), which asks `asset-upload` itself and vets the
645
+ * file. Also where a kept original goes (D11) when the chosen receiver does
646
+ * not keep its own. A name the user gave (#2130)
478
647
  * is the stored file's name; when the shared pool has a file of that name
479
648
  * already, the item is refused, never stored beside or over it.
480
649
  */
481
650
  private filesReceiver;
651
+ /**
652
+ * `core.private-store` (#2118): a takeout (#1387) handed back into one of
653
+ * its owner's own private stores, read by the shim `core.takeout`. It
654
+ * replaces the takeout import's own door (#1472). Each page goes through
655
+ * PageManager's save door and each file through FileManager's write door
656
+ * (`put`), the doors an edit and an upload use, so a sealed store
657
+ * encrypts on write with no new crypto.
658
+ *
659
+ * - No new permission (D5): the rule the takeout import always had, the
660
+ * `account-security` permission and the user's own private container.
661
+ * - Its places (`targets`) are the user's own stores this context can
662
+ * write now: those on disk and the default one, an encrypted one only
663
+ * while it is unlocked in this session.
664
+ * - Its writes may be sealed (`writesEncrypted`): the engine runs an
665
+ * upload into it in the request, with the request's key, and an import
666
+ * into it is confidential (#2110): no file or page name is logged.
667
+ * - Importing the same takeout again leaves the store as the first import
668
+ * did (operator, 2026-09-25), except that a page or a file deleted since
669
+ * comes back (operator, 2026-10-30; #2171). The engine skips a file it
670
+ * already took (fingerprints), but a page with a uuid and a file are read
671
+ * again (`rechecks`): the uuid check below decides for a page, and for a
672
+ * file whether the store still holds its bytes (`ownStoreFingerprints`, read once a run),
673
+ * so one still held is `duplicate` and not put again (no second upload
674
+ * record). On a first import the file door returns a file the store
675
+ * already holds. A page whose uuid the store holds is `duplicate`: unchanged, or
676
+ * changed since the download, and then the page here is kept. A uuid
677
+ * used elsewhere on the site is `duplicate`, the page named only when
678
+ * the user may view it. A title held by a DIFFERENT page lands beside
679
+ * it as `Title (imported)`.
680
+ * - The original is the file itself: none is kept or cited (D11), as for `core.files`.
681
+ * - The run is recorded as one act, `store-import`, with counts only, and
682
+ * kept as a report page in the store, sealed when the store is (operator,
683
+ * 2026-09-25 and 2026-10-30): see {@link keepRunReport}.
684
+ */
685
+ private privateStoreReceiver;
686
+ /** The takeout import's rule, unchanged (#1472): `account-security`, and only into the user's own container. */
687
+ private mayImportIntoOwnStore;
688
+ /** Where the private-store receiver writes, from configuration. */
689
+ private privateStoreSetup;
690
+ /**
691
+ * The user's own stores this context can write now (#2118): each store on
692
+ * disk and the default one, which a user on a new site has before they
693
+ * wrote anything; an encrypted one only while unlocked in this session.
694
+ * Making a store is its own door (#1414): an encrypted one needs keys.
695
+ */
696
+ private ownStoresWritable;
697
+ /**
698
+ * One page or file of a takeout into the store `info.target` (#2118, as
699
+ * #1472 did). A file through FileManager's `put`; a page after the files
700
+ * it links to, its links pointed at their ids here, through PageManager's
701
+ * save door. Nothing here logs a page's or a file's name (#1461, #2110).
702
+ */
703
+ private receiveTakeoutEntry;
704
+ /** A takeout's page into the store, as #1472 decided: see {@link privateStoreReceiver}. */
705
+ private saveTakeoutPage;
706
+ /**
707
+ * The run as one act (#1472's `store-import`): who, which store, and how
708
+ * each file went, as counts. Never a page's or a file's name (#1461); each
709
+ * file's own record is its door's (`asset-upload`, a sealed store's by id).
710
+ * The counts use the old door's keys, so whatever reads the event reads
711
+ * one shape.
712
+ */
713
+ private recordStoreImport;
482
714
  /**
483
715
  * Get available format IDs
484
716
  *
@@ -671,6 +903,22 @@ declare class ImportManager extends BaseManager {
671
903
  /** The uploaded file's name, for the report page. */
672
904
  sourceName?: string;
673
905
  }): Promise<TakeoutImportReport>;
906
+ /**
907
+ * An engine run into `core.private-store` as the takeout report the old
908
+ * door keeps (code review of #2160, finding 3; operator, 2026-10-30), from
909
+ * the run's per-file status. Only a run that read a takeout keeps one.
910
+ * A file the run skipped as already imported is in neither count.
911
+ *
912
+ * @returns How many files no page links to
913
+ */
914
+ private keepRunReport;
915
+ /**
916
+ * A takeout import's report, kept (operator, 2026-09-25): which files no
917
+ * page now in the store links to, counted in the log, and a page in the
918
+ * store it is about, sealed exactly when the store is, so it can name what
919
+ * a log may not. A failure to save it costs the record, never the import.
920
+ */
921
+ private keepTakeoutReport;
674
922
  /** The import report page's markdown. Private links are `[Title|store/Title]` (#1457). */
675
923
  private takeoutReportMarkdown;
676
924
  /**
@@ -37,6 +37,8 @@ declare class MediaManager extends BaseManager implements CatalogSource {
37
37
  readonly currentSchemaVersion = 1;
38
38
  /** Active media provider (null when not yet initialized) */
39
39
  private _provider;
40
+ /** Why configured folders were refused for overlapping private storage (#2195). */
41
+ private overlapReasons;
40
42
  /** The configured media folders, for the degraded-state reason (#1167). */
41
43
  private mediaFolders;
42
44
  /** Return the active media provider for use by AssetManager. */
@@ -105,6 +107,15 @@ declare class MediaManager extends BaseManager implements CatalogSource {
105
107
  * what degraded means (#1155) — configured, wanted, not working.
106
108
  */
107
109
  private recordFolderState;
110
+ /** Log and notify admins of each folder refused for overlapping private storage (#2195). */
111
+ private surfaceOverlaps;
112
+ /**
113
+ * Why each of `folders` may not be a media folder, empty when all may (#2195).
114
+ * The configuration screen asks before saving `ngdpbase.media.folders`.
115
+ */
116
+ checkFolders(folders: string[]): string[];
117
+ /** One path per line, then "and N more" when the provider capped the list (#2191). */
118
+ private formatFileList;
108
119
  /**
109
120
  * Surface scan/rebuild anomalies to admins via /admin/notifications and the
110
121
  * server log (#807). Missing folders, capture-date gaps, and processing