@jwilleke/ngdpbase-types 5.26.0 → 5.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/lib/packages/ngdpbase-types/index.d.ts +8 -2
  2. package/lib/packages/ngdpbase-types/testing.d.ts +7 -3
  3. package/lib/packages/ngdpbase-types/testing.js +7 -3
  4. package/lib/src/context/ActorContext.d.ts +15 -2
  5. package/lib/src/context/ActorContext.js +20 -0
  6. package/lib/src/converters/DocxConverter.d.ts +30 -0
  7. package/lib/src/converters/MarkdownConverter.d.ts +31 -0
  8. package/lib/src/export/ChromeServicePdfProvider.d.ts +52 -0
  9. package/lib/src/export/ChromeServicePdfProvider.js +153 -0
  10. package/lib/src/export/LocalChromePdfProvider.d.ts +25 -0
  11. package/lib/src/export/LocalChromePdfProvider.js +33 -0
  12. package/lib/src/export/PdfProvider.d.ts +51 -0
  13. package/lib/src/export/PdfProvider.js +37 -0
  14. package/lib/src/export/pdfEngine.d.ts +113 -0
  15. package/lib/src/export/pdfEngine.js +195 -0
  16. package/lib/src/ice/converterShim.d.ts +5 -0
  17. package/lib/src/ice/fileHandling.d.ts +30 -0
  18. package/lib/src/ice/ledger.d.ts +37 -0
  19. package/lib/src/ice/runImport.d.ts +51 -0
  20. package/lib/src/ice/types.d.ts +284 -0
  21. package/lib/src/mail/MailProvider.d.ts +15 -0
  22. package/lib/src/mail/NodemailerMailProvider.d.ts +1 -1
  23. package/lib/src/managers/AddonsManager.d.ts +25 -5
  24. package/lib/src/managers/AssetManager.d.ts +7 -3
  25. package/lib/src/managers/EmailManager.d.ts +32 -5
  26. package/lib/src/managers/ExportManager.d.ts +62 -0
  27. package/lib/src/managers/ExportManager.js +152 -2
  28. package/lib/src/managers/ImportManager.d.ts +611 -0
  29. package/lib/src/managers/OrganizationManager.d.ts +5 -3
  30. package/lib/src/managers/PageManager.d.ts +77 -1
  31. package/lib/src/managers/PersonManager.d.ts +36 -11
  32. package/lib/src/managers/RoleManager.d.ts +76 -17
  33. package/lib/src/managers/UserManager.d.ts +55 -2
  34. package/lib/src/security/PolicyDecisionPoint.d.ts +18 -9
  35. package/lib/src/security/PolicyInformationPoint.d.ts +48 -10
  36. package/lib/src/types/Config.d.ts +2 -0
  37. package/lib/src/types/User.d.ts +1 -1
  38. package/lib/src/utils/addonHost.d.ts +23 -2
  39. package/lib/src/utils/addonHost.js +25 -2
  40. package/lib/src/utils/auditEventNames.generated.d.ts +10 -10
  41. package/lib/src/utils/auditEventNames.generated.js +10 -10
  42. package/lib/src/utils/contactAccess.d.ts +30 -0
  43. package/lib/src/utils/frontmatterAccess.d.ts +54 -11
  44. package/lib/src/utils/importRunSummary.d.ts +64 -0
  45. package/lib/src/utils/keyedSerial.d.ts +31 -0
  46. package/lib/src/utils/ncmNotify.d.ts +30 -0
  47. package/lib/src/utils/privateStoreAccess.js +1 -3
  48. package/lib/src/utils/privateStoreImport.d.ts +58 -0
  49. package/lib/src/utils/roleChangeError.d.ts +25 -0
  50. package/lib/src/utils/subjectMayDo.d.ts +2 -2
  51. package/lib/src/utils/zipArchive.d.ts +71 -0
  52. package/package.json +1 -1
@@ -0,0 +1,611 @@
1
+ /**
2
+ * Import Manager
3
+ *
4
+ * Manages the import of content from external wiki formats into ngdpbase.
5
+ * Uses an extensible converter registry pattern to support multiple formats
6
+ * (JSPWiki, MediaWiki, Confluence, etc.).
7
+ *
8
+ * @module ImportManager
9
+ *
10
+ * @example
11
+ * const importManager = engine.getManager('ImportManager');
12
+ *
13
+ * // Register additional converters
14
+ * importManager.registerConverter(new MediaWikiConverter());
15
+ *
16
+ * // Preview import
17
+ * const preview = await importManager.previewImport({
18
+ * sourceDir: '/path/to/wiki',
19
+ * format: 'auto'
20
+ * });
21
+ *
22
+ * // Execute import
23
+ * const result = await importManager.importPages({
24
+ * sourceDir: '/path/to/wiki',
25
+ * format: 'jspwiki',
26
+ * dryRun: false
27
+ * });
28
+ */
29
+ import { type ActorContext } from '../context/ActorContext.js';
30
+ import BaseManager, { BackupData } from './BaseManager.js';
31
+ import type { WikiEngine } from '../types/WikiEngine.js';
32
+ import { IContentConverter } from '../converters/IContentConverter.js';
33
+ import { type ZipReadLimits } from '../utils/zipArchive.js';
34
+ import type { JobContext } from '../context/JobContext.js';
35
+ import type { IceLedger, IceOriginalChoices, IceReceiver, IceRunReport, IceRunRequest, IceShim, RegisteredShim } from '../ice/types.js';
36
+ /**
37
+ * Options for import operations
38
+ */
39
+ export interface ImportOptions {
40
+ /** Source directory containing files to import */
41
+ sourceDir: string;
42
+ /**
43
+ * Target directory for converted files (default: the live pages dir).
44
+ * When this resolves to the live pages directory, imports go through
45
+ * PageManager save semantics (#880 — indexed, versioned, in Recent
46
+ * Changes). Any other directory means convert-to-files/export mode:
47
+ * raw markdown files are written there instead.
48
+ */
49
+ targetDir?: string;
50
+ /** Format ID or 'auto' for auto-detection */
51
+ format?: string;
52
+ /** Keep original files after conversion (default: true) */
53
+ preserveOriginals?: boolean;
54
+ /** Preview only, don't write files (default: false) */
55
+ dryRun?: boolean;
56
+ /** Generate UUIDs for pages that don't have them (default: true) */
57
+ generateUUIDs?: boolean;
58
+ /** File extensions to process (default: determined by format) */
59
+ fileExtensions?: string[];
60
+ /** Maximum files to process (for large imports) */
61
+ limit?: number;
62
+ /** Skip first N files (for resuming imports) */
63
+ offset?: number;
64
+ /** Progress callback for streaming updates */
65
+ onProgress?: (event: ImportProgressEvent) => void;
66
+ /**
67
+ * Categorises this run for the per-run summary (#738). Examples:
68
+ * `'paste'`, `'url'`, `'file'`, `'jspwiki'`, `'ingest:<sourceId>'`.
69
+ * Defaults to `'import'` when caller doesn't specify.
70
+ */
71
+ importType?: string;
72
+ /**
73
+ * The identity that initiated this run, forwarded rather than flattened
74
+ * (#1164, #1179, #1236).
75
+ *
76
+ * Until #1236 this sat beside `actor?: string` and `actorIsSystem?`, two
77
+ * more carriers of the same fact that the run summary and the page
78
+ * frontmatter read instead. One identity now: the run's actor, the page's
79
+ * `editor` and its `author` fallback, and the summary's `isSystem` are all
80
+ * derived from this context. A username string cannot carry a
81
+ * delegation. `importPageAttachments` needed a context to authorise an
82
+ * upload, could not get one from a string, and __fabricated__ one:
83
+ *
84
+ * context: { username: author, name: author, isAuthenticated: true,
85
+ * roles: ['admin'] }
86
+ *
87
+ * where `author` came from the imported file's own `attachment.properties`.
88
+ * The PDP (then `UserManager.hasPermission`) trusts supplied roles verbatim — it does not
89
+ * re-resolve them — so that object authorised as an admin, attributed the
90
+ * write to a name taken from imported content, and carried no `viaToken`,
91
+ * so an admin's agent token doing an import escaped its scope ceiling.
92
+ *
93
+ * That is P1 twice over: a parameter that could not carry provenance, and a
94
+ * synthetic admin principal invented to fill the gap it left.
95
+ *
96
+ * Mandatory: the upload door records it, the summary names it, the page
97
+ * carries it. A non-request caller passes a JobContext with its reason.
98
+ */
99
+ actorContext: ActorContext;
100
+ /**
101
+ * What to do when a file's title matches an existing page (#874).
102
+ * `'skip'` (default) — leave the existing page untouched, report duplicate.
103
+ * `'overwrite'` — update the existing page in place through PageManager save
104
+ * semantics: UUID, author, created, and slug preserved; version bumped by
105
+ * the provider; `editor` = `actor`; search index and link graph updated
106
+ * in-band (same contract as an edit-form save / the ingest API upsert).
107
+ * Folder import only — URL import keeps hard-skip.
108
+ */
109
+ conflictPolicy?: 'skip' | 'overwrite';
110
+ /**
111
+ * Explicit private-store destination (#1389). Markdown still goes through
112
+ * PageManager. JSON/XML become files in the store via AttachmentManager.
113
+ * Never inferred from a page. Store id from ConfigurationManager when omitted.
114
+ */
115
+ private?: boolean;
116
+ store?: string;
117
+ }
118
+ /**
119
+ * Progress event for streaming import updates
120
+ */
121
+ export interface ImportProgressEvent {
122
+ /** Event type */
123
+ type: 'start' | 'progress' | 'complete' | 'error';
124
+ /** Source file path */
125
+ file?: string;
126
+ /** Current file index (0-based) */
127
+ index?: number;
128
+ /** Total files to process */
129
+ total?: number;
130
+ /** Status of the file import */
131
+ status?: 'success' | 'skipped' | 'failed';
132
+ /** Error message if failed */
133
+ error?: string;
134
+ /** Imported page title (success only) — lets the UI link to /view/<title> */
135
+ pageTitle?: string;
136
+ /** Final result (for complete event) */
137
+ result?: unknown;
138
+ }
139
+ /**
140
+ * Error information for failed imports
141
+ */
142
+ export interface ImportError {
143
+ /** Source file path */
144
+ file: string;
145
+ /** Error message */
146
+ message: string;
147
+ /** Error stack trace (optional) */
148
+ stack?: string;
149
+ }
150
+ /**
151
+ * Information about an imported file
152
+ */
153
+ export interface ImportedFile {
154
+ /** Original source file path */
155
+ sourcePath: string;
156
+ /** Target file path */
157
+ targetPath: string;
158
+ /** Format detected/used */
159
+ format: string;
160
+ /** File size in bytes */
161
+ size: number;
162
+ /** Extracted metadata */
163
+ metadata: Record<string, unknown>;
164
+ /** Conversion warnings */
165
+ warnings: string[];
166
+ /** Whether file was actually written (false for dry runs or duplicates) */
167
+ written: boolean;
168
+ /** Reason the file was skipped (e.g. 'duplicate') */
169
+ skippedReason?: string;
170
+ /** UUID of existing page if duplicate */
171
+ existingPageUuid?: string;
172
+ /** True when an existing page was (or on dry run: would be) updated in place (#874) */
173
+ overwritten?: boolean;
174
+ /** Attachment import stats (JSPWiki imports only) */
175
+ attachments?: {
176
+ imported: number;
177
+ skipped: number;
178
+ errors: string[];
179
+ };
180
+ }
181
+ /**
182
+ * Result of an import operation
183
+ */
184
+ export interface ImportResult {
185
+ /** Overall success status */
186
+ success: boolean;
187
+ /** Number of files converted */
188
+ converted: number;
189
+ /** Number of files skipped */
190
+ skipped: number;
191
+ /** Number of files with errors */
192
+ failed: number;
193
+ /** Total files processed */
194
+ total: number;
195
+ /** Error details */
196
+ errors: ImportError[];
197
+ /** Information about imported files */
198
+ files: ImportedFile[];
199
+ /** Import duration in milliseconds */
200
+ durationMs: number;
201
+ }
202
+ /**
203
+ * Why a takeout import was refused before anything was written (#1472).
204
+ * A refusal is never a partial import.
205
+ */
206
+ export declare class TakeoutImportRefused extends Error {
207
+ readonly reason: 'not-owner' | 'no-such-store' | 'locked' | 'unreadable';
208
+ constructor(reason: 'not-owner' | 'no-such-store' | 'locked' | 'unreadable', message: string);
209
+ }
210
+ /** One page's outcome in a takeout import (#1472). */
211
+ export type TakeoutPageOutcome = {
212
+ title: string;
213
+ outcome: 'imported';
214
+ importedAs: string;
215
+ } | {
216
+ title: string;
217
+ outcome: 'unchanged';
218
+ } | {
219
+ title: string;
220
+ outcome: 'changed-since-takeout';
221
+ }
222
+ /** `where` is named only when the requester may view that page. */
223
+ | {
224
+ title: string;
225
+ outcome: 'uuid-elsewhere';
226
+ where?: string;
227
+ } | {
228
+ title: string;
229
+ outcome: 'failed';
230
+ message: string;
231
+ };
232
+ export type TakeoutImportReport = {
233
+ store: string;
234
+ pages: TakeoutPageOutcome[];
235
+ /** Files stored, or matched to one the store already held. */
236
+ files: number;
237
+ /** Files that could not be stored, by name. */
238
+ fileErrors: Array<{
239
+ name: string;
240
+ message: string;
241
+ }>;
242
+ /**
243
+ * Files stored that no page now in the store links to — typically because
244
+ * the pages that used them were skipped (operator, 2026-09-25: they come
245
+ * in, and are reported). Named here, for the owner; counted in the logs.
246
+ */
247
+ unlinkedFiles: string[];
248
+ /** Archive members that are neither a page nor a file. */
249
+ ignored: string[];
250
+ /**
251
+ * The private page this import's report was saved as, in the target store
252
+ * (operator, 2026-09-25). Absent only when that save failed.
253
+ */
254
+ reportPage?: string;
255
+ };
256
+ /** A receiver the import screen may offer this user (#2021): its id, label and original-file choices (D11). */
257
+ export interface UsableReceiver {
258
+ id: string;
259
+ label?: string;
260
+ originals: Required<IceOriginalChoices>;
261
+ }
262
+ /**
263
+ * Import Manager class
264
+ *
265
+ * Manages content import with extensible format support via converter registry.
266
+ */
267
+ declare class ImportManager extends BaseManager {
268
+ /** Registry of format converters */
269
+ private converterRegistry;
270
+ /** ICE (#1979): shims by id (`<owner>.<name>`), in registration order. */
271
+ private readonly shimRegistry;
272
+ /** ICE: receivers by id. */
273
+ private readonly receiverRegistry;
274
+ /** ICE: runs this process knows about, oldest first. */
275
+ private readonly iceRuns;
276
+ /** ICE: runs waiting for the background job, each with its own context. */
277
+ private readonly iceQueue;
278
+ /** ICE: true while a background job is draining the queue. */
279
+ private iceDraining;
280
+ /** ICE: the last background run id the queue was handed to. */
281
+ private iceLastJobRunId;
282
+ private iceLedger;
283
+ constructor(engine: WikiEngine);
284
+ /**
285
+ * Initialize the manager
286
+ */
287
+ initialize(config?: Record<string, unknown>): Promise<void>;
288
+ /**
289
+ * Register a content converter. Since #1979 (D8) it is also the ICE shim
290
+ * `core.<formatId>`, producing NCM for the pages receiver, so the folder
291
+ * import and the engine run the same conversion.
292
+ *
293
+ * @param converter - Converter instance implementing IContentConverter
294
+ */
295
+ registerConverter(converter: IContentConverter): void;
296
+ /**
297
+ * Register a shim (D6, D7). Core registers its own as owner `core`; an
298
+ * add-on registers through `host.registerShim`, which names the add-on as
299
+ * the owner and refuses a shim its manifest does not declare.
300
+ *
301
+ * @returns The shim's id, `<owner>.<name>`
302
+ * @throws Error when the shim is malformed or its id is taken
303
+ */
304
+ registerShim(owner: string, shim: IceShim): string;
305
+ /**
306
+ * Register a receiver (D4): a door that already exists. Core's are
307
+ * `core.pages` and `core.files`; an add-on registers its own manager's door
308
+ * through `host.registerReceiver`.
309
+ *
310
+ * @returns The receiver's id, `<owner>.<name>`
311
+ */
312
+ registerReceiver(owner: string, receiver: IceReceiver): string;
313
+ /** Is this shim switched on (D6)? `ngdpbase.import.shims.<id>.enabled`, default on. */
314
+ isShimEnabled(id: string): boolean;
315
+ /** Every registered shim, and whether it is on. */
316
+ listShims(): Array<Pick<RegisteredShim, 'id' | 'owner' | 'label' | 'direction' | 'produces' | 'receivers'> & {
317
+ enabled: boolean;
318
+ }>;
319
+ /**
320
+ * The receivers this context may use (D5): each answers with its own
321
+ * existing permission. Each comes with the original-file choices it offers
322
+ * (D11), for the import screen (#2021).
323
+ */
324
+ receiversFor(ctx: ActorContext): Promise<UsableReceiver[]>;
325
+ /**
326
+ * The original-file choices a receiver offers (D11, #2021): what it
327
+ * declares, else all three; its default when it names one it allows, else
328
+ * `ngdpbase.import.engine.default-original` when allowed, else the first.
329
+ */
330
+ private originalChoices;
331
+ /**
332
+ * The request as the engine runs it: the receiver's default original when
333
+ * none was chosen, and a refusal for a choice the receiver does not offer
334
+ * (D11, #2021). An unknown receiver is left for the run to report.
335
+ */
336
+ private withOriginal;
337
+ /**
338
+ * Run one import now and wait for it. For code already off the request
339
+ * path: an add-on's own background job, a test. A route uses
340
+ * {@link enqueueImport}, so the work runs off the request path.
341
+ *
342
+ * @param ctx - Who it runs as: the person's context, or a job's (P1)
343
+ */
344
+ runImport(request: IceRunRequest, ctx: ActorContext): Promise<IceRunReport>;
345
+ /**
346
+ * Queue an import as background work and return its run id at once. The
347
+ * run carries `ctx` — the person who asked (`jobContextFromRequest`), or a
348
+ * job's context with its reason — and each receiver checks its own
349
+ * permission with it when the run reaches it (D5).
350
+ */
351
+ enqueueImport(request: IceRunRequest, ctx: JobContext): Promise<string>;
352
+ /** A run's report, to the person it runs as; null to anyone else. */
353
+ getImportRun(runId: string, ctx: ActorContext): IceRunReport | null;
354
+ /** The runs this process holds for the user `ctx` names, newest first (#2021): their own, never anyone else's. */
355
+ importRunsFor(ctx: ActorContext): IceRunReport[];
356
+ /** Use this ledger instead of the file one: a test's, or a database table's. */
357
+ setImportLedger(ledger: IceLedger): void;
358
+ private newRunReport;
359
+ private executeRun;
360
+ private enabledShims;
361
+ /**
362
+ * Hand the queue to the background job. One job run drains every queued
363
+ * import, each under its own context. A job run that has finished draining
364
+ * but is not yet marked done answers `enqueue` with its old run id; then
365
+ * try again shortly, so nothing queued is stranded.
366
+ */
367
+ private kickIceQueue;
368
+ private setting;
369
+ private numberSetting;
370
+ /** `ngdpbase.import.engine.*`: every limit the engine applies is configuration. */
371
+ private engineSettings;
372
+ /** The engine's ledger (fingerprints, per-file status, item keys), opened on first use. */
373
+ private ledger;
374
+ /** What a shim is handed: the engine's guarded fetch (design rule) and its own options (D6). */
375
+ private shimTools;
376
+ /** A receiver's existing permission, asked of the PDP with the run's context (D5, P2). */
377
+ private permits;
378
+ /**
379
+ * `core.pages` (D4): NCM into pages, through PageManager's save door, as the
380
+ * folder import saves. It checks `page-create` itself (D5). A title that
381
+ * already exists is a duplicate, as it is for the folder import by default.
382
+ */
383
+ private pagesReceiver;
384
+ /**
385
+ * `core.files` (D4): a file through AttachmentManager's upload door, which
386
+ * checks `asset-upload` itself. Also where a kept original goes (D11) when
387
+ * the chosen receiver does not keep its own. It becomes FileManager's write
388
+ * door once the files layer has one (#1851).
389
+ */
390
+ private filesReceiver;
391
+ /**
392
+ * Get available format IDs
393
+ *
394
+ * @returns Array of registered format identifiers
395
+ */
396
+ getAvailableFormats(): string[];
397
+ /**
398
+ * Get converter by format ID
399
+ *
400
+ * @param formatId - Format identifier
401
+ * @returns Converter instance or undefined
402
+ */
403
+ getConverter(formatId: string): IContentConverter | undefined;
404
+ /**
405
+ * Get all registered converters with their metadata
406
+ *
407
+ * @returns Array of converter info objects
408
+ */
409
+ getConverterInfo(): Array<{
410
+ formatId: string;
411
+ formatName: string;
412
+ fileExtensions: string[];
413
+ }>;
414
+ /**
415
+ * Auto-detect format from file content
416
+ *
417
+ * @param content - File content
418
+ * @param filename - Filename (for extension matching)
419
+ * @returns Format ID or null if no match
420
+ */
421
+ detectFormat(content: string, filename: string): string | null;
422
+ /**
423
+ * Preview import without writing files
424
+ *
425
+ * @param options - Import options (dryRun is forced to true)
426
+ * @returns Import result with preview data
427
+ */
428
+ previewImport(options: ImportOptions): Promise<ImportResult>;
429
+ /**
430
+ * Import pages with streaming progress updates
431
+ * Alias for importPages that makes the progress callback more explicit
432
+ *
433
+ * @param options - Import options with onProgress callback
434
+ * @returns Import result with statistics and file details
435
+ */
436
+ importPagesWithProgress(options: ImportOptions): Promise<ImportResult>;
437
+ /**
438
+ * Import pages from source directory
439
+ *
440
+ * @param options - Import options
441
+ * @returns Import result with statistics and file details
442
+ */
443
+ importPages(options: ImportOptions): Promise<ImportResult>;
444
+ /**
445
+ * Import a single page
446
+ *
447
+ * @param filePath - Source file path
448
+ * @param options - Import options
449
+ * @returns Imported file info or null if skipped
450
+ */
451
+ importSinglePage(filePath: string, options: ImportOptions): Promise<ImportedFile | null>;
452
+ /**
453
+ * Update an existing page in place from an import (#874 conflict policy
454
+ * 'overwrite'). Mirrors the edit-form / ingest-API save contract: UUID,
455
+ * author, created, and slug are preserved from the existing page; the
456
+ * provider bumps the version; `editor` is the import actor; the search
457
+ * index, link graph, and render cache are updated in-band so the result
458
+ * is immediately searchable and link-resolvable.
459
+ */
460
+ private overwriteExistingPage;
461
+ /**
462
+ * A title the save door had to rewrite (#1455) is named in this file's
463
+ * warnings, so the import report says what the source called the page and
464
+ * what it is called here. Returns the title the page actually has.
465
+ */
466
+ private reportNormalisedTitle;
467
+ /**
468
+ * Create a new page from an import through PageManager save semantics
469
+ * (#880): the provider assigns versioning and the page comes out indexed,
470
+ * link-resolvable, and visible in Recent Changes — instead of the raw file
471
+ * write that left pages invisible to search until a manual reindex.
472
+ * `author` and `editor` are the import actor (unless the source frontmatter
473
+ * carries an author).
474
+ */
475
+ private createPageThroughPipeline;
476
+ /**
477
+ * Import a page from a URL
478
+ *
479
+ * Fetches the URL and imports it like a file import of the same HTML
480
+ * (#1337): the NCM normalizer (links, tables, `ncmVersion`), the #1332 fix
481
+ * steps, then a save through PageManager — validated, audited as the
482
+ * importer, versioned and indexed. The fetched page's schema.org metadata
483
+ * and the source URL are kept in frontmatter.
484
+ *
485
+ * @param url - URL to fetch and import
486
+ * @param options - The importer's context, and optional title and dryRun
487
+ * @returns Imported file info
488
+ */
489
+ importFromUrl(url: string, options: {
490
+ actorContext: ActorContext;
491
+ title?: string;
492
+ dryRun?: boolean;
493
+ }): Promise<ImportedFile>;
494
+ /**
495
+ * Run every Markdown fix step over an imported body (#1332), through
496
+ * PageManager — the steps Convert to NCM runs. Each step that changed
497
+ * something is added to `warnings` as a `converter-note`, which the import
498
+ * preview and the run notification already show. Without a PageManager the
499
+ * body is returned unchanged.
500
+ *
501
+ * @param body - Page body, without frontmatter
502
+ * @param warnings - The conversion's warnings, appended to
503
+ * @returns The fixed body
504
+ */
505
+ private applyFixSteps;
506
+ /** Style classes for up-converted tables (NCM §2.1), operator-configurable. */
507
+ private ncmTableClasses;
508
+ /**
509
+ * JSON/XML into a private store are files, not pages (#1389).
510
+ */
511
+ private importPrivateStoreFile;
512
+ /**
513
+ * Import attachments from a JSPWiki `-att/` directory alongside a page file.
514
+ *
515
+ * @param sourceFilePath - Path to the source `.txt` page file
516
+ * @param pageName - Decoded page name (used to link attachments)
517
+ * @param options - Import options (dryRun support)
518
+ * @returns Stats about imported attachments
519
+ */
520
+ private importPageAttachments;
521
+ /**
522
+ * Get MIME type from file extension
523
+ */
524
+ private getMimeType;
525
+ /**
526
+ * Get file extensions to process based on options
527
+ */
528
+ private getFileExtensions;
529
+ /**
530
+ * Find all files with matching extensions in directory
531
+ */
532
+ private findFiles;
533
+ /**
534
+ * Build YAML frontmatter from conversion result
535
+ */
536
+ private buildImportMetadata;
537
+ private buildFrontmatter;
538
+ /**
539
+ * Register new user-keywords to the custom configuration
540
+ *
541
+ * When importing JSPWiki pages with %%category%% blocks, this method
542
+ * adds extracted categories to the config so they appear in the
543
+ * user-keywords picker when editing pages.
544
+ *
545
+ * @param keywords - Array of keyword names to register
546
+ * @returns Number of new keywords added
547
+ */
548
+ private registerUserKeywordsToConfig;
549
+ /**
550
+ * Import a takeout into one of the requester's OWN private stores (#1472).
551
+ *
552
+ * The mirror of `PageManager.buildOwnStoreTakeout`, and not `importPages`:
553
+ * that reads a directory on the server, so a decrypted takeout would have to
554
+ * be staged on disk, and it converts every page, which would rewrite bodies
555
+ * that are already this system's own markdown. Here the archive is read in
556
+ * memory and each page and file goes through the same doors an edit and an
557
+ * upload use, so a sealed store encrypts on write with no new crypto.
558
+ *
559
+ * Refused before anything is written — never a partial import — when the
560
+ * requester is not the owner, the store is not theirs, or it is encrypted
561
+ * and locked in this session.
562
+ *
563
+ * Idempotent (operator, 2026-09-25): importing the same takeout twice leaves
564
+ * the store as the first import did.
565
+ *
566
+ * - Files first, so page links can be pointed at them. The store's upload
567
+ * finds a file whose bytes it already holds and returns that one, so a
568
+ * second import stores nothing new.
569
+ * - A page whose uuid the target store already holds is skipped:
570
+ * `unchanged` when its body matches, `changed-since-takeout` when not —
571
+ * the live page wins.
572
+ * - A page whose uuid is used elsewhere on the site is skipped as
573
+ * `uuid-elsewhere`, naming the page only when the requester may view it.
574
+ * - A title held by a DIFFERENT page lands beside it as `Title (imported)`.
575
+ *
576
+ * This is not a restore: a takeout carries no history and no trash.
577
+ *
578
+ * @param ctx - Who is importing; only into their own container
579
+ */
580
+ importOwnStoreTakeout(ctx: ActorContext, options: {
581
+ store: string;
582
+ archive: Buffer;
583
+ limits: ZipReadLimits;
584
+ /** The uploaded file's name, for the report page. */
585
+ sourceName?: string;
586
+ }): Promise<TakeoutImportReport>;
587
+ /** The import report page's markdown. Private links are `[Title|store/Title]` (#1457). */
588
+ private takeoutReportMarkdown;
589
+ /**
590
+ * Where a uuid is already in use outside the target store, or null.
591
+ *
592
+ * The owner's other stores this session can read, then the public pages.
593
+ * `where` is set only for a page the requester may view: a uuid held by a
594
+ * page they cannot see is reported without naming it, so an import cannot
595
+ * be used to probe for pages.
596
+ */
597
+ private whereUuidLives;
598
+ /**
599
+ * Backup manager data (no persistent data to backup)
600
+ */
601
+ backup(): Promise<BackupData>;
602
+ /**
603
+ * Restore manager data (no persistent data to restore)
604
+ */
605
+ restore(backupData: BackupData): Promise<void>;
606
+ /**
607
+ * Shutdown the manager
608
+ */
609
+ shutdown(): Promise<void>;
610
+ }
611
+ export default ImportManager;
@@ -88,13 +88,15 @@ declare class OrganizationManager extends BaseManager {
88
88
  * problem actually bites.
89
89
  */
90
90
  getInstallOrg(): Promise<Organization | null>;
91
- getById(id: string): Promise<Organization | null>;
92
- getByFile(filename: string): Promise<Organization | null>;
91
+ /** One organization by `@id`, for a caller who may read contacts (`contact-read`), or core's own work (#2018). */
92
+ getById(ctx: ActorContext, id: string): Promise<Organization | null>;
93
+ /** One organization by its file name, decided as {@link getById} (#2018). */
94
+ getByFile(ctx: ActorContext, filename: string): Promise<Organization | null>;
93
95
  /** #1006: how many organizations this instance holds. Count only. */
94
96
  getManagerStats(): Promise<ManagerStats>;
95
97
  /** Every organization, for a caller who may read contacts (`contact-read`). */
96
98
  list(ctx: ActorContext): Promise<Organization[]>;
97
- /** One organization by `@id`, for a caller who may read contacts (`contact-read`). */
99
+ /** One organization by `@id`: {@link getById}. */
98
100
  get(ctx: ActorContext, id: string): Promise<Organization | null>;
99
101
  /**
100
102
  * Organizations whose `name`, `legalName`, `email` or a contact point's