@jwilleke/ngdpbase-types 5.26.0 → 5.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/packages/ngdpbase-types/index.d.ts +5 -2
- package/lib/packages/ngdpbase-types/testing.d.ts +7 -3
- package/lib/packages/ngdpbase-types/testing.js +7 -3
- package/lib/src/converters/DocxConverter.d.ts +30 -0
- package/lib/src/converters/MarkdownConverter.d.ts +31 -0
- package/lib/src/export/ChromeServicePdfProvider.d.ts +52 -0
- package/lib/src/export/ChromeServicePdfProvider.js +153 -0
- package/lib/src/export/LocalChromePdfProvider.d.ts +25 -0
- package/lib/src/export/LocalChromePdfProvider.js +33 -0
- package/lib/src/export/PdfProvider.d.ts +51 -0
- package/lib/src/export/PdfProvider.js +37 -0
- package/lib/src/export/pdfEngine.d.ts +113 -0
- package/lib/src/export/pdfEngine.js +195 -0
- package/lib/src/ice/converterShim.d.ts +5 -0
- package/lib/src/ice/fileHandling.d.ts +30 -0
- package/lib/src/ice/ledger.d.ts +37 -0
- package/lib/src/ice/runImport.d.ts +51 -0
- package/lib/src/ice/types.d.ts +262 -0
- package/lib/src/mail/MailProvider.d.ts +15 -0
- package/lib/src/mail/NodemailerMailProvider.d.ts +1 -1
- package/lib/src/managers/AddonsManager.d.ts +18 -2
- package/lib/src/managers/EmailManager.d.ts +32 -5
- package/lib/src/managers/ExportManager.d.ts +62 -0
- package/lib/src/managers/ExportManager.js +147 -2
- package/lib/src/managers/ImportManager.d.ts +590 -0
- package/lib/src/managers/PageManager.d.ts +45 -1
- package/lib/src/security/PolicyInformationPoint.d.ts +25 -2
- package/lib/src/types/Config.d.ts +2 -0
- package/lib/src/utils/addonHost.d.ts +23 -2
- package/lib/src/utils/addonHost.js +25 -2
- package/lib/src/utils/frontmatterAccess.d.ts +54 -11
- package/lib/src/utils/importRunSummary.d.ts +64 -0
- package/lib/src/utils/ncmNotify.d.ts +30 -0
- package/lib/src/utils/privateStoreImport.d.ts +58 -0
- package/lib/src/utils/zipArchive.d.ts +71 -0
- package/package.json +1 -1
|
@@ -209,6 +209,12 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
209
209
|
* none when it is not running.
|
|
210
210
|
*/
|
|
211
211
|
subjectEnabledAddons(ctx: ActorContext): Promise<string[]>;
|
|
212
|
+
/**
|
|
213
|
+
* {@link subjectEnabledAddons} for a page decision (#1964): none for no
|
|
214
|
+
* subject, and none when the lookup fails — an `addon:<slug>` entry then
|
|
215
|
+
* matches nobody, never everyone.
|
|
216
|
+
*/
|
|
217
|
+
private enabledAddonsOf;
|
|
212
218
|
private userManager;
|
|
213
219
|
private roleManager;
|
|
214
220
|
/**
|
|
@@ -326,7 +332,9 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
326
332
|
* - __Tier 0.5, author-lock__, `edit` only. It only denies: an author, or a
|
|
327
333
|
* subject holding `admin-system` (#1431 7b), falls through to Tier 1+.
|
|
328
334
|
* - __Tier 1, frontmatter__ `access[action]` / `audience`. Page-level, and
|
|
329
|
-
* overrides global policy when it states a rule (#1054).
|
|
335
|
+
* overrides global policy when it states a rule (#1054). An `audience`
|
|
336
|
+
* covers every action, and a subject holding `admin-system` is its one
|
|
337
|
+
* exception; `access` has none (#1964).
|
|
330
338
|
* - __Tier 2.__ For a share visitor the share IS the policy (#1222); for
|
|
331
339
|
* anyone else, global policy.
|
|
332
340
|
* - __Default deny.__ Nothing below Tier 2 grants — JSPWiki's page-body ACL
|
|
@@ -439,10 +447,25 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
439
447
|
*/
|
|
440
448
|
canUserAccessMediaItem(userContext: UserContext | null | undefined, item: MediaItem): Promise<boolean>;
|
|
441
449
|
/**
|
|
442
|
-
* Check front matter audience / access fields (Tier 1
|
|
450
|
+
* Check front matter audience / access fields (Tier 1).
|
|
443
451
|
* Returns a decision object; decided=false means no front matter restriction — fall through.
|
|
452
|
+
*
|
|
453
|
+
* An audience that leaves the subject out refuses every action, unless the
|
|
454
|
+
* subject holds `admin-system` (#1964, operator 2026-10-23): asked only
|
|
455
|
+
* then. `access[action]` has no such exception.
|
|
444
456
|
*/
|
|
445
457
|
private checkFrontmatterAccess;
|
|
458
|
+
/**
|
|
459
|
+
* Does the page's `audience` admit this subject (#1964)? True when the page
|
|
460
|
+
* names no audience, the subject matches it, or the subject holds
|
|
461
|
+
* `admin-system`. The same rule Tier 1 applies, asked by PageManager's
|
|
462
|
+
* write door, so a write cannot reach a page its writer is outside the
|
|
463
|
+
* audience of — whatever route it came by. Records nothing; the door does.
|
|
464
|
+
*
|
|
465
|
+
* @param userContext - the subject, as given
|
|
466
|
+
* @param metadata - the page's current frontmatter
|
|
467
|
+
*/
|
|
468
|
+
audienceAdmits(userContext: UserContext | null | undefined, metadata: PageFrontmatter | null | undefined): Promise<boolean>;
|
|
446
469
|
/**
|
|
447
470
|
* Record/audit an access decision.
|
|
448
471
|
* Accepts either a single object or positional args for backward compatibility.
|
|
@@ -121,6 +121,8 @@ export interface WikiConfig {
|
|
|
121
121
|
* counting every failure inside it (#1916). Default: 60.
|
|
122
122
|
*/
|
|
123
123
|
'ngdpbase.mail.failure-notice.window-minutes': number;
|
|
124
|
+
/** The most a message's attachments may total, in bytes (#1974). Default: 10485760. */
|
|
125
|
+
'ngdpbase.mail.attachments.max-bytes': number;
|
|
124
126
|
/**
|
|
125
127
|
* Anti-spam: max submissions per IP per `window-minutes` window before
|
|
126
128
|
* the 429 trips (#670 Phase E). Default: 5.
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* the add-on (#1973);
|
|
16
16
|
* - `setEnabledForPerson` sets this add-on's own per-person switch (#1963),
|
|
17
17
|
* bound to its slug, so it can never set another add-on's;
|
|
18
|
+
* - `registerShim` and `registerReceiver` (#1979, ICE) register into the import
|
|
19
|
+
* engine under this add-on's slug, and only the names its manifest declares;
|
|
18
20
|
* - the egress tools (`resolveEgressPolicy`, `validateUrl`,
|
|
19
21
|
* `createGuardedElasticsearchClient`, `refusedNodeMessage`) and `isAtLeast`
|
|
20
22
|
* are core's own pure functions, handed on unchanged, so an add-on never
|
|
@@ -36,6 +38,8 @@ import { createGuardedElasticsearchClient, refusedNodeMessage } from '../http/gu
|
|
|
36
38
|
import { isAtLeast } from './version.js';
|
|
37
39
|
import { recordAuditEvent } from './auditEvents.js';
|
|
38
40
|
import { addonAuditEventName } from './auditEventNames.js';
|
|
41
|
+
import type { IceReceiver, IceShim } from '../ice/types.js';
|
|
42
|
+
import type { AddonManifest } from '../managers/AddonsManager.js';
|
|
39
43
|
/** The values core offers an add-on, under the names they were imported as. */
|
|
40
44
|
export interface AddonHost {
|
|
41
45
|
readonly ApiContext: typeof ApiContext;
|
|
@@ -76,7 +80,21 @@ export interface AddonHost {
|
|
|
76
80
|
readonly refusedNodeMessage: typeof refusedNodeMessage;
|
|
77
81
|
/** Whether an `x.y.z` version is at least another; throws on a version that is not `x.y.z`. */
|
|
78
82
|
readonly isAtLeast: typeof isAtLeast;
|
|
83
|
+
/**
|
|
84
|
+
* #1979 (ICE, D6): register an import shim, as `<slug>.<shim.name>`. Call it
|
|
85
|
+
* in `register`. Refused when the manifest's `shims` does not name it.
|
|
86
|
+
* Returns the shim's id; site configuration switches it on and off.
|
|
87
|
+
*/
|
|
88
|
+
readonly registerShim: (shim: IceShim) => string;
|
|
89
|
+
/**
|
|
90
|
+
* #1979 (ICE, D4): register a receiver — your manager's own door for what
|
|
91
|
+
* shims produce — as `<slug>.<receiver.name>`. Refused when the manifest's
|
|
92
|
+
* `receivers` does not name it. Returns the receiver's id.
|
|
93
|
+
*/
|
|
94
|
+
readonly registerReceiver: (receiver: IceReceiver) => string;
|
|
79
95
|
}
|
|
96
|
+
/** What the host may register for an add-on: the shim and receiver names its manifest declares (#1979). */
|
|
97
|
+
export type AddonIceDeclaration = Pick<AddonManifest, 'shims' | 'receivers'>;
|
|
80
98
|
/** Sets one add-on's per-person switch; core binds the slug when it builds the host (#1963). */
|
|
81
99
|
export type PersonSwitchSetter = (ctx: ActorContext, on: boolean) => Promise<void>;
|
|
82
100
|
/**
|
|
@@ -94,6 +112,9 @@ export declare function addonDeclaredAuditEvents(addonDefaults: Record<string, u
|
|
|
94
112
|
* @param setEnabledForPerson - This add-on's switch setter, slug already bound
|
|
95
113
|
* (`AddonsManager` supplies it); without one, the host's setter refuses
|
|
96
114
|
* @param engine - The running engine, which names the system principal for
|
|
97
|
-
* `systemContext
|
|
115
|
+
* `systemContext` and holds the import engine `registerShim` reaches;
|
|
116
|
+
* without one (a test) it is core's fallback principal, and no registry
|
|
117
|
+
* @param manifest - The add-on's manifest (`ngdpbase` in its package.json):
|
|
118
|
+
* the shims and receivers it may register (#1979)
|
|
98
119
|
*/
|
|
99
|
-
export declare function buildAddonHost(slug: string, addonDefaults: Record<string, unknown>, setEnabledForPerson?: PersonSwitchSetter, engine?: Parameters<typeof systemContext>[0]): AddonHost;
|
|
120
|
+
export declare function buildAddonHost(slug: string, addonDefaults: Record<string, unknown>, setEnabledForPerson?: PersonSwitchSetter, engine?: Parameters<typeof systemContext>[0], manifest?: AddonIceDeclaration | null): AddonHost;
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* the add-on (#1973);
|
|
16
16
|
* - `setEnabledForPerson` sets this add-on's own per-person switch (#1963),
|
|
17
17
|
* bound to its slug, so it can never set another add-on's;
|
|
18
|
+
* - `registerShim` and `registerReceiver` (#1979, ICE) register into the import
|
|
19
|
+
* engine under this add-on's slug, and only the names its manifest declares;
|
|
18
20
|
* - the egress tools (`resolveEgressPolicy`, `validateUrl`,
|
|
19
21
|
* `createGuardedElasticsearchClient`, `refusedNodeMessage`) and `isAtLeast`
|
|
20
22
|
* are core's own pure functions, handed on unchanged, so an add-on never
|
|
@@ -64,10 +66,29 @@ function refuseUndeclared(slug, name, declared) {
|
|
|
64
66
|
* @param setEnabledForPerson - This add-on's switch setter, slug already bound
|
|
65
67
|
* (`AddonsManager` supplies it); without one, the host's setter refuses
|
|
66
68
|
* @param engine - The running engine, which names the system principal for
|
|
67
|
-
* `systemContext
|
|
69
|
+
* `systemContext` and holds the import engine `registerShim` reaches;
|
|
70
|
+
* without one (a test) it is core's fallback principal, and no registry
|
|
71
|
+
* @param manifest - The add-on's manifest (`ngdpbase` in its package.json):
|
|
72
|
+
* the shims and receivers it may register (#1979)
|
|
68
73
|
*/
|
|
69
|
-
export function buildAddonHost(slug, addonDefaults, setEnabledForPerson = noPersonSwitch, engine) {
|
|
74
|
+
export function buildAddonHost(slug, addonDefaults, setEnabledForPerson = noPersonSwitch, engine, manifest) {
|
|
70
75
|
const declared = addonDeclaredAuditEvents(addonDefaults);
|
|
76
|
+
const names = (list) => new Set(Array.isArray(list) ? list.filter((n) => typeof n === 'string') : []);
|
|
77
|
+
const iceDeclared = { shim: names(manifest?.shims), receiver: names(manifest?.receivers) };
|
|
78
|
+
const iceRegistry = (what, name) => {
|
|
79
|
+
const allowed = iceDeclared[what];
|
|
80
|
+
if (typeof name !== 'string' || !allowed.has(name)) {
|
|
81
|
+
const message = `Add-on '${slug}' may register only the ${what}s its manifest declares ("ngdpbase": { "${what}s": [...] } in package.json); ` +
|
|
82
|
+
`'${String(name)}' is not one of them (declared: ${allowed.size > 0 ? [...allowed].sort().join(', ') : 'none'}).`;
|
|
83
|
+
logger.error(`[addons] ${message}`);
|
|
84
|
+
throw new Error(message);
|
|
85
|
+
}
|
|
86
|
+
const registry = engine?.getManager('ImportManager');
|
|
87
|
+
if (typeof registry?.registerShim !== 'function' || typeof registry.registerReceiver !== 'function') {
|
|
88
|
+
throw new Error(`register${what === 'shim' ? 'Shim' : 'Receiver'} is not available: this host was built without a site's ImportManager`);
|
|
89
|
+
}
|
|
90
|
+
return registry;
|
|
91
|
+
};
|
|
71
92
|
const hostAddonAuditEventName = (name) => {
|
|
72
93
|
const checked = addonAuditEventName(name);
|
|
73
94
|
refuseUndeclared(slug, name, declared);
|
|
@@ -90,6 +111,8 @@ export function buildAddonHost(slug, addonDefaults, setEnabledForPerson = noPers
|
|
|
90
111
|
logger: logger.child({ addon: slug }),
|
|
91
112
|
guardedFetch,
|
|
92
113
|
setEnabledForPerson,
|
|
114
|
+
registerShim: (shim) => iceRegistry('shim', shim?.name).registerShim(slug, shim),
|
|
115
|
+
registerReceiver: (receiver) => iceRegistry('receiver', receiver?.name).registerReceiver(slug, receiver),
|
|
93
116
|
resolveEgressPolicy,
|
|
94
117
|
validateUrl,
|
|
95
118
|
createGuardedElasticsearchClient,
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* frontmatterAccess — the single implementation of ACL Tier 1 (#1054).
|
|
3
3
|
*
|
|
4
4
|
* A page's frontmatter can restrict who may act on it, via `access[<action>]`
|
|
5
|
-
* or
|
|
6
|
-
* its own version of it, which is how they came to disagree:
|
|
5
|
+
* or `audience`, which covers every action (#1964). Two subsystems needed
|
|
6
|
+
* that answer and each had its own version of it, which is how they came to disagree:
|
|
7
7
|
*
|
|
8
8
|
* - `PolicyInformationPoint.checkFrontmatterAccess` decided a request, correctly.
|
|
9
9
|
* - `VersioningFileProvider.getRecentChanges` decided a listing, and only
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
export interface AccessMetadataLike {
|
|
23
23
|
/** `{ view: ['role'], edit: [...] }` — per-action principal lists. */
|
|
24
24
|
access?: unknown;
|
|
25
|
-
/**
|
|
25
|
+
/** Who may act on the page at all; administrators excepted (#1964). */
|
|
26
26
|
audience?: unknown;
|
|
27
27
|
}
|
|
28
28
|
export interface FrontmatterDecision {
|
|
@@ -34,21 +34,64 @@ export interface FrontmatterDecision {
|
|
|
34
34
|
matched?: string;
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
* The page's `audience`, or null when it names none (#1964, operator
|
|
38
|
+
* 2026-10-23). An audience restricts EVERY action: someone outside it may
|
|
39
|
+
* neither read nor edit the page.
|
|
40
|
+
*/
|
|
41
|
+
export declare function audiencePrincipals(metadata: AccessMetadataLike | null | undefined): string[] | null;
|
|
42
|
+
/**
|
|
43
|
+
* The page's `access[action]` list, or null when it states none. It applies
|
|
44
|
+
* to that action only, and to everyone — administrators included.
|
|
45
|
+
*/
|
|
46
|
+
export declare function accessPrincipals(metadata: AccessMetadataLike | null | undefined, action: string): string[] | null;
|
|
47
|
+
/**
|
|
48
|
+
* The entry `addon:<slug>` (#1964): "people who turned on the per-person
|
|
49
|
+
* add-on <slug>". It is matched only against the viewer's enabled add-ons,
|
|
50
|
+
* never against a role or a username.
|
|
51
|
+
*/
|
|
52
|
+
export declare const ADDON_PRINCIPAL_PREFIX = "addon:";
|
|
53
|
+
/**
|
|
54
|
+
* Whether deciding `action` on this page needs the viewer's enabled add-ons —
|
|
55
|
+
* so a caller asks for them only when the page names one (#1964).
|
|
43
56
|
*/
|
|
44
|
-
export declare function
|
|
57
|
+
export declare function namesAddonPrincipal(metadata: AccessMetadataLike | null | undefined, action: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Every add-on slug the page names in `audience` or in any `access` list, once
|
|
60
|
+
* each, in the order first named — for the warning about a slug that can match
|
|
61
|
+
* nobody (#1964).
|
|
62
|
+
*/
|
|
63
|
+
export declare function addonAudienceSlugs(metadata: AccessMetadataLike | null | undefined): string[];
|
|
64
|
+
/**
|
|
65
|
+
* Whether the page's audience leaves this viewer out — true only when it
|
|
66
|
+
* names one and nothing in it matches. The caller then asks whether the
|
|
67
|
+
* viewer is an administrator, the one exception (#1964), and passes the
|
|
68
|
+
* answer to {@link decideFrontmatterAccess}; it asks only when this is true.
|
|
69
|
+
*/
|
|
70
|
+
export declare function audienceExcludes(metadata: AccessMetadataLike | null | undefined, viewerPrincipals: readonly string[], enabledAddons?: readonly string[]): boolean;
|
|
71
|
+
/** The `matched` an administrator outside the audience is admitted by. */
|
|
72
|
+
export declare const AUDIENCE_ADMIN_MATCH = "admin-system";
|
|
45
73
|
/**
|
|
46
74
|
* Decide Tier 1 for a viewer described as a flat principal list — their roles
|
|
47
75
|
* plus their username. Callers holding a `UserContext` should pass
|
|
48
76
|
* `[...roles, username]`; the provider layer already works in these terms.
|
|
49
77
|
*
|
|
78
|
+
* Two rules, in order (#1964, operator 2026-10-23):
|
|
79
|
+
*
|
|
80
|
+
* - __`audience`__ applies to every action. Someone it leaves out may neither
|
|
81
|
+
* read nor edit the page: every action is refused. The one exception is an
|
|
82
|
+
* administrator — a viewer holding `admin-system`, which the caller decides
|
|
83
|
+
* and passes as `isAdmin` (this module never reads a role for it). For
|
|
84
|
+
* `view`, a viewer the audience admits is allowed; for any other action it
|
|
85
|
+
* only lets them through to `access` and then policy.
|
|
86
|
+
* - __`access[action]`__ applies to that action, to everyone, administrators
|
|
87
|
+
* included. When the page states it, it decides.
|
|
88
|
+
*
|
|
89
|
+
* An `addon:<slug>` entry matches only when `<slug>` is in `enabledAddons`,
|
|
90
|
+
* the viewer's enabled per-person add-ons (#1964); a caller that does not
|
|
91
|
+
* supply them matches no such entry, so it can under-list but never leak.
|
|
92
|
+
*
|
|
50
93
|
* Denies when the page states a rule and nothing matches. That default is the
|
|
51
94
|
* point: a listing that treats "no match" as "show it" is the bug this exists
|
|
52
95
|
* to prevent.
|
|
53
96
|
*/
|
|
54
|
-
export declare function decideFrontmatterAccess(metadata: AccessMetadataLike | null | undefined, viewerPrincipals: readonly string[], action: string): FrontmatterDecision;
|
|
97
|
+
export declare function decideFrontmatterAccess(metadata: AccessMetadataLike | null | undefined, viewerPrincipals: readonly string[], action: string, enabledAddons?: readonly string[], isAdmin?: boolean): FrontmatterDecision;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-run summary persistence for NCM / import conversion outcomes (#738).
|
|
3
|
+
*
|
|
4
|
+
* Writes one JSON file per import run under FAST_STORAGE (configurable via
|
|
5
|
+
* `ngdpbase.import.runs-dir`, default `./data/import-runs`). Each file's
|
|
6
|
+
* name is the ISO timestamp of `startedAt`, sanitised for filesystem use.
|
|
7
|
+
*
|
|
8
|
+
* The trend view (admin-import.ejs) reads the last N summaries via
|
|
9
|
+
* listRecentRuns() to surface dominant failure causes across runs.
|
|
10
|
+
*
|
|
11
|
+
* Append-only by design. Retention is operator-managed for now — the
|
|
12
|
+
* directory can grow indefinitely; a future cleanup job (or admin button)
|
|
13
|
+
* can prune. Listed as a follow-up in the #738 acceptance criteria.
|
|
14
|
+
*/
|
|
15
|
+
/** Compact per-run record persisted to disk. */
|
|
16
|
+
export interface ImportRunSummary {
|
|
17
|
+
/** Stable id; same as the filename without `.json`. */
|
|
18
|
+
runId: string;
|
|
19
|
+
/** ISO-8601 timestamp of when the run started. */
|
|
20
|
+
startedAt: string;
|
|
21
|
+
/** ISO-8601 timestamp of when the run finished (may equal startedAt for trivial runs). */
|
|
22
|
+
finishedAt: string;
|
|
23
|
+
/** What kind of import surfaced this run (`paste`, `url`, `file`, `ingest:<sourceId>`). */
|
|
24
|
+
importType: string;
|
|
25
|
+
/**
|
|
26
|
+
* Who initiated the run — `ImportOptions.actorContext.username` (#1236): the
|
|
27
|
+
* request's subject for a manual import, the system principal for a job.
|
|
28
|
+
*/
|
|
29
|
+
actor: string;
|
|
30
|
+
/**
|
|
31
|
+
* True when the run was a job with no person behind it (a JobContext whose
|
|
32
|
+
* origin is boot, schedule or operator) — lets the trend view distinguish
|
|
33
|
+
* "alice triggered an import" from "scheduled feed ingested overnight".
|
|
34
|
+
* False for a request, or a request-origin job.
|
|
35
|
+
*/
|
|
36
|
+
isSystem?: boolean;
|
|
37
|
+
/** Where the run came from: `request`, `boot`, `schedule` or `operator` (#1236). Absent on records written before it. */
|
|
38
|
+
origin?: string;
|
|
39
|
+
/** Total items the run attempted. */
|
|
40
|
+
total: number;
|
|
41
|
+
/** Items successfully converted to pages. */
|
|
42
|
+
converted: number;
|
|
43
|
+
/** Items intentionally skipped (e.g. duplicates). */
|
|
44
|
+
skipped: number;
|
|
45
|
+
/** Items that failed to convert. */
|
|
46
|
+
failed: number;
|
|
47
|
+
/**
|
|
48
|
+
* Counts of ConversionWarning.kind values aggregated across all items in the run.
|
|
49
|
+
* Keys are flat (no colon-subkinds today) per the #738 design decision.
|
|
50
|
+
*/
|
|
51
|
+
kindCounts: Record<string, number>;
|
|
52
|
+
}
|
|
53
|
+
/** Aggregate a flat list of ConversionWarning-like objects into kindCounts. */
|
|
54
|
+
export declare function tallyWarnings(warnings: Array<{
|
|
55
|
+
kind: string;
|
|
56
|
+
}> | undefined): Record<string, number>;
|
|
57
|
+
/** Write a single per-run summary. Idempotent for same runId. Best-effort — errors logged, never thrown. */
|
|
58
|
+
export declare function writeRunSummary(runsDir: string, summary: ImportRunSummary): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Return the most-recent N run summaries, newest first. Filenames are ISO
|
|
61
|
+
* timestamps so lexicographic sort is chronological. Missing/empty dir
|
|
62
|
+
* returns []. Malformed individual files are skipped, not fatal.
|
|
63
|
+
*/
|
|
64
|
+
export declare function listRecentRuns(runsDir: string, limit?: number): Promise<ImportRunSummary[]>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NCM conversion → /admin/notifications (#728 Phase 2 Slice 5d)
|
|
3
|
+
*
|
|
4
|
+
* Operator decision 2026-05-17: on **non-preview** NCM conversion paths
|
|
5
|
+
* (bulk import, #685 ingestion, MCP) — where no human sees the preview — a
|
|
6
|
+
* summary of the conversion warnings is pushed to the existing
|
|
7
|
+
* `/admin/notifications` centre via `NotificationManager` (the same
|
|
8
|
+
* subsystem `MediaManager` already feeds), so admins get an actionable
|
|
9
|
+
* per-event alert. Distinct from #738 (aggregate metrics/trend).
|
|
10
|
+
*
|
|
11
|
+
* Engine-coupled glue lives here (not in `src/converters/ncm/`, which is
|
|
12
|
+
* kept pure). Best-effort and non-blocking — notification failure must never
|
|
13
|
+
* fail a conversion.
|
|
14
|
+
*
|
|
15
|
+
* @module utils/ncmNotify
|
|
16
|
+
*/
|
|
17
|
+
interface EngineLike {
|
|
18
|
+
getManager(name: string): unknown;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Push an admin notification summarising NCM conversion warnings.
|
|
22
|
+
* No-op when there are no warnings or NotificationManager is unavailable.
|
|
23
|
+
*
|
|
24
|
+
* @param engine Wiki engine (structural — just needs getManager)
|
|
25
|
+
* @param source Human label for the path, e.g. "Import jspwiki", "MCP create_page"
|
|
26
|
+
* @param target The page/file that was converted
|
|
27
|
+
* @param warnings Flattened `"kind: detail"` strings
|
|
28
|
+
*/
|
|
29
|
+
export declare function notifyNcmConversion(engine: EngineLike, source: string, target: string, warnings: string[]): void;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a takeout handed back for import (#1472) — the mirror of
|
|
3
|
+
* `privateStoreExport.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Pure: entries in, a description of what they hold out. No I/O, no access
|
|
6
|
+
* decision, no write. The door that acts on it is
|
|
7
|
+
* `ImportManager.importOwnStoreTakeout`.
|
|
8
|
+
*
|
|
9
|
+
* What a takeout is, as #1387 writes it: one `{store}/` folder holding each
|
|
10
|
+
* page as `{title}.md` with full frontmatter, files under `attachments/`, and
|
|
11
|
+
* `files-index.json` mapping each file's id to its path in the archive. The
|
|
12
|
+
* reader is lenient about the one thing people change — the folder. Someone
|
|
13
|
+
* who extracts a takeout and zips it again may add a level, or zip the
|
|
14
|
+
* contents with no folder at all, so the takeout's root is found rather than
|
|
15
|
+
* assumed.
|
|
16
|
+
*/
|
|
17
|
+
import type { ZipEntry } from './zipArchive.js';
|
|
18
|
+
/** One page of a takeout. */
|
|
19
|
+
export type TakeoutPage = {
|
|
20
|
+
/** Where it was in the archive — how the report names a page that failed. */
|
|
21
|
+
archivePath: string;
|
|
22
|
+
/** From the frontmatter, else the file's name. */
|
|
23
|
+
title: string;
|
|
24
|
+
/** From the frontmatter. Absent for a page written by hand. */
|
|
25
|
+
uuid?: string;
|
|
26
|
+
/** The frontmatter, as written. */
|
|
27
|
+
metadata: Record<string, unknown>;
|
|
28
|
+
body: string;
|
|
29
|
+
};
|
|
30
|
+
/** One file of a takeout. */
|
|
31
|
+
export type TakeoutFile = {
|
|
32
|
+
archivePath: string;
|
|
33
|
+
/** Its id where it came from, when the file index names it. */
|
|
34
|
+
oldId?: string;
|
|
35
|
+
/** The name it was uploaded with. */
|
|
36
|
+
name: string;
|
|
37
|
+
encodingFormat?: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
bytes: Buffer;
|
|
40
|
+
};
|
|
41
|
+
export type ParsedTakeout = {
|
|
42
|
+
pages: TakeoutPage[];
|
|
43
|
+
files: TakeoutFile[];
|
|
44
|
+
/** Archive members that are neither a page nor a file, left alone. */
|
|
45
|
+
ignored: string[];
|
|
46
|
+
};
|
|
47
|
+
/** What a takeout's entries hold. */
|
|
48
|
+
export declare function readTakeout(entries: readonly ZipEntry[]): ParsedTakeout;
|
|
49
|
+
/** `body` with every `/attachments/{old}` link pointed at its new id. */
|
|
50
|
+
export declare function rewriteAttachmentLinks(body: string, ids: ReadonlyMap<string, string>): string;
|
|
51
|
+
/** Whether `body` links to the file `id`. */
|
|
52
|
+
export declare function linksToAttachment(body: string, id: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The first free title of `title`, `title (imported)`, `title (imported 2)` …
|
|
55
|
+
* (operator, 2026-09-25): a DIFFERENT page already holding the title keeps it,
|
|
56
|
+
* and the import lands beside it.
|
|
57
|
+
*/
|
|
58
|
+
export declare function freeImportTitle(title: string, isTaken: (candidate: string) => Promise<boolean>): Promise<string>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing a zip archive from bytes already in hand (#1387).
|
|
3
|
+
*
|
|
4
|
+
* A takeout lands on the owner's laptop or phone, and that decides the format:
|
|
5
|
+
* iOS, iPadOS, Android, Windows and macOS all open a `.zip` by tapping it,
|
|
6
|
+
* while `.tar.gz` needs a third-party app on exactly the devices people carry.
|
|
7
|
+
*
|
|
8
|
+
* This is the only archive the system writes, and `readZip` below is the only
|
|
9
|
+
* one it reads: a takeout handed back for import (#1472). The instance backup does not use
|
|
10
|
+
* one: it carries private files as base64 inside the backup document (#1387),
|
|
11
|
+
* so there is nothing to unpack on restore.
|
|
12
|
+
*
|
|
13
|
+
* Bytes in, one Buffer out: a takeout is decrypted, and nothing decrypted may
|
|
14
|
+
* be staged on the server's disk, so there is no file for a packer to read.
|
|
15
|
+
*
|
|
16
|
+
* Written here rather than taken from a library because the zip container is
|
|
17
|
+
* small and completely specified, while every candidate library wants to read
|
|
18
|
+
* from or write to the filesystem. The tests extract each archive with an
|
|
19
|
+
* independent implementation and compare the bytes, so this is checked against
|
|
20
|
+
* a real reader rather than against its own assumptions.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately NOT implemented: zip64. An archive over 4 GiB, a member over
|
|
23
|
+
* 4 GiB, or more than 65,535 members is refused with a clear error rather than
|
|
24
|
+
* written as something an extractor would mis-read. A store that large wants
|
|
25
|
+
* streaming and zip64 together, which is its own piece of work.
|
|
26
|
+
*/
|
|
27
|
+
/** One file to put in the archive. */
|
|
28
|
+
export type ZipEntry = {
|
|
29
|
+
/** Path inside the archive, `/`-separated, no leading slash. */
|
|
30
|
+
path: string;
|
|
31
|
+
bytes: Buffer;
|
|
32
|
+
/** Defaults to now. Zip stores 2-second resolution, and nothing before 1980. */
|
|
33
|
+
mtime?: Date;
|
|
34
|
+
};
|
|
35
|
+
export declare function crc32(buf: Buffer): number;
|
|
36
|
+
/**
|
|
37
|
+
* A zip archive of the given entries, as one Buffer.
|
|
38
|
+
*
|
|
39
|
+
* Entries are deflated when that makes them smaller — markdown compresses well,
|
|
40
|
+
* and a phone on mobile data is the expected destination — and stored as-is
|
|
41
|
+
* when it does not, which is the usual outcome for photos and other media.
|
|
42
|
+
*/
|
|
43
|
+
export declare function packZip(entries: readonly ZipEntry[]): Promise<Buffer>;
|
|
44
|
+
/** Limits on what `readZip` will unpack, so an upload cannot exhaust memory. */
|
|
45
|
+
export type ZipReadLimits = {
|
|
46
|
+
/** Most members accepted. */
|
|
47
|
+
maxEntries: number;
|
|
48
|
+
/** Most bytes accepted once everything is inflated. */
|
|
49
|
+
maxTotalBytes: number;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The files in a zip archive, as bytes in memory (#1472).
|
|
53
|
+
*
|
|
54
|
+
* Read from the central directory, which is the archive's own table of
|
|
55
|
+
* contents, so a member written with a data descriptor — what a streaming
|
|
56
|
+
* writer, or an OS re-zipping a folder, produces — is read the same as any
|
|
57
|
+
* other. Directory entries are dropped, and so is what macOS adds when it
|
|
58
|
+
* zips a folder (`__MACOSX/`, `.DS_Store`): they are not anyone's data.
|
|
59
|
+
*
|
|
60
|
+
* Refused, with an error naming why, rather than read as something else:
|
|
61
|
+
*
|
|
62
|
+
* - a path that would climb out of wherever it is extracted — absolute, or
|
|
63
|
+
* holding a `..` segment, with `\` read as the separator it is on Windows;
|
|
64
|
+
* - encryption, zip64, split archives, and any method but stored and deflate;
|
|
65
|
+
* - a member whose inflated bytes do not match the size or CRC the archive
|
|
66
|
+
* declares for it;
|
|
67
|
+
* - more members, or more inflated bytes, than `limits` allow. Inflating is
|
|
68
|
+
* capped per member at what is left of the budget, so a small archive
|
|
69
|
+
* that expands enormously stops at the budget, not at the machine's memory.
|
|
70
|
+
*/
|
|
71
|
+
export declare function readZip(archive: Buffer, limits: ZipReadLimits): ZipEntry[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jwilleke/ngdpbase-types",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.27.0",
|
|
4
4
|
"description": "TypeScript types for ngdpbase add-ons, and a test kit of core's own values. A dev dependency, versioned with ngdpbase.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Jim Willeke",
|