@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.
- package/lib/packages/ngdpbase-types/index.d.ts +8 -2
- package/lib/packages/ngdpbase-types/testing.d.ts +7 -3
- package/lib/packages/ngdpbase-types/testing.js +7 -3
- package/lib/src/context/ActorContext.d.ts +15 -2
- package/lib/src/context/ActorContext.js +20 -0
- 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 +284 -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 +25 -5
- package/lib/src/managers/AssetManager.d.ts +7 -3
- package/lib/src/managers/EmailManager.d.ts +32 -5
- package/lib/src/managers/ExportManager.d.ts +62 -0
- package/lib/src/managers/ExportManager.js +152 -2
- package/lib/src/managers/ImportManager.d.ts +611 -0
- package/lib/src/managers/OrganizationManager.d.ts +5 -3
- package/lib/src/managers/PageManager.d.ts +77 -1
- package/lib/src/managers/PersonManager.d.ts +36 -11
- package/lib/src/managers/RoleManager.d.ts +76 -17
- package/lib/src/managers/UserManager.d.ts +55 -2
- package/lib/src/security/PolicyDecisionPoint.d.ts +18 -9
- package/lib/src/security/PolicyInformationPoint.d.ts +48 -10
- package/lib/src/types/Config.d.ts +2 -0
- package/lib/src/types/User.d.ts +1 -1
- package/lib/src/utils/addonHost.d.ts +23 -2
- package/lib/src/utils/addonHost.js +25 -2
- package/lib/src/utils/auditEventNames.generated.d.ts +10 -10
- package/lib/src/utils/auditEventNames.generated.js +10 -10
- package/lib/src/utils/contactAccess.d.ts +30 -0
- package/lib/src/utils/frontmatterAccess.d.ts +54 -11
- package/lib/src/utils/importRunSummary.d.ts +64 -0
- package/lib/src/utils/keyedSerial.d.ts +31 -0
- package/lib/src/utils/ncmNotify.d.ts +30 -0
- package/lib/src/utils/privateStoreAccess.js +1 -3
- package/lib/src/utils/privateStoreImport.d.ts +58 -0
- package/lib/src/utils/roleChangeError.d.ts +25 -0
- package/lib/src/utils/subjectMayDo.d.ts +2 -2
- package/lib/src/utils/zipArchive.d.ts +71 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import BaseManager from '../managers/BaseManager.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type ActorContext } from '../context/ActorContext.js';
|
|
3
3
|
import { WikiEngine } from '../types/WikiEngine.js';
|
|
4
4
|
import type { AgentTokenGrant, ProxyClaim } from '../managers/UserManager.js';
|
|
5
5
|
import type { Request } from 'express';
|
|
@@ -46,6 +46,12 @@ interface UserContext {
|
|
|
46
46
|
viaProxy?: ProxyClaim;
|
|
47
47
|
[key: string]: unknown;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* What the page questions take (#2022): the caller's context as given — a
|
|
51
|
+
* request's subject or a job's `JobContext` — or a subject already in decision
|
|
52
|
+
* form. They convert it themselves, through {@link asSubject}.
|
|
53
|
+
*/
|
|
54
|
+
type PageQuestionCaller = ActorContext | UserContext | null | undefined;
|
|
49
55
|
/**
|
|
50
56
|
* Access decision log entry
|
|
51
57
|
*/
|
|
@@ -173,6 +179,15 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
173
179
|
* visitor holds, which is the safe answer for someone who no longer exists.
|
|
174
180
|
*/
|
|
175
181
|
resolveSubjectNow(username: string | undefined): Promise<UserContext>;
|
|
182
|
+
/**
|
|
183
|
+
* The subject as a page's own rules match it (#2022): for a job's subject
|
|
184
|
+
* (`resolveRolesNow`, #1212), its username with the roles its account
|
|
185
|
+
* holds NOW, read through {@link resolveSubjectNow} — the path the PDP
|
|
186
|
+
* takes for the same subject; anyone else as carried. A job carries no
|
|
187
|
+
* roles, so matching it without this matched none, and an audience naming
|
|
188
|
+
* a role its user holds left the job out.
|
|
189
|
+
*/
|
|
190
|
+
private matchableNow;
|
|
176
191
|
/**
|
|
177
192
|
* Who is making this request: the subject the session middleware resolved,
|
|
178
193
|
* else the session's account resolved now, else the anonymous visitor.
|
|
@@ -209,6 +224,12 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
209
224
|
* none when it is not running.
|
|
210
225
|
*/
|
|
211
226
|
subjectEnabledAddons(ctx: ActorContext): Promise<string[]>;
|
|
227
|
+
/**
|
|
228
|
+
* {@link subjectEnabledAddons} for a page decision (#1964): none for no
|
|
229
|
+
* subject, and none when the lookup fails — an `addon:<slug>` entry then
|
|
230
|
+
* matches nobody, never everyone.
|
|
231
|
+
*/
|
|
232
|
+
private enabledAddonsOf;
|
|
212
233
|
private userManager;
|
|
213
234
|
private roleManager;
|
|
214
235
|
/**
|
|
@@ -326,7 +347,9 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
326
347
|
* - __Tier 0.5, author-lock__, `edit` only. It only denies: an author, or a
|
|
327
348
|
* subject holding `admin-system` (#1431 7b), falls through to Tier 1+.
|
|
328
349
|
* - __Tier 1, frontmatter__ `access[action]` / `audience`. Page-level, and
|
|
329
|
-
* overrides global policy when it states a rule (#1054).
|
|
350
|
+
* overrides global policy when it states a rule (#1054). An `audience`
|
|
351
|
+
* covers every action, and a subject holding `admin-system` is its one
|
|
352
|
+
* exception; `access` has none (#1964).
|
|
330
353
|
* - __Tier 2.__ For a share visitor the share IS the policy (#1222); for
|
|
331
354
|
* anyone else, global policy.
|
|
332
355
|
* - __Default deny.__ Nothing below Tier 2 grants — JSPWiki's page-body ACL
|
|
@@ -340,14 +363,14 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
340
363
|
* private page gets at Tier 0: the owner, or a delegate of the owner; never
|
|
341
364
|
* a role. A refusal is recorded like any page refusal (`authorization-deny`).
|
|
342
365
|
*
|
|
343
|
-
* @param
|
|
366
|
+
* @param caller - the caller's context, as given: a request's subject or a job's (#2022); null is anonymous
|
|
344
367
|
* @param owner - the container's owner (a private file's `creator`)
|
|
345
368
|
* @param resource - what is being reached, for the record (e.g. `attachment:<id>`)
|
|
346
369
|
* @param action - the action asked for (e.g. `view`)
|
|
347
370
|
* @param vault - the vault inside the container, when the question is about one:
|
|
348
371
|
* a share link the owner issued gets in only for a vault it names (#1388)
|
|
349
372
|
*/
|
|
350
|
-
canAccessPrivateContainer(
|
|
373
|
+
canAccessPrivateContainer(caller: PageQuestionCaller, owner: string, resource: string, action: string, vault?: string): boolean;
|
|
351
374
|
/**
|
|
352
375
|
* Check whether a user can access a given page — for cross-page checks where
|
|
353
376
|
* the current WikiContext describes a DIFFERENT page than the one being
|
|
@@ -373,14 +396,14 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
373
396
|
* case.
|
|
374
397
|
* - any tier returns deny.
|
|
375
398
|
*
|
|
376
|
-
* @param
|
|
399
|
+
* @param caller - The caller's context, as given: a request's subject or a job's (#2022); null / undefined is anonymous.
|
|
377
400
|
* @param pageName - Target page to check.
|
|
378
401
|
* @param action - Action verb (e.g., `'view'`, `'edit'`, `'delete'`).
|
|
379
402
|
* @param knownMetadata - The page's metadata when the caller has just
|
|
380
403
|
* resolved it: the page door (#1622), which reads the page by the same
|
|
381
404
|
* resolution it decides on. Omitted, it is loaded here.
|
|
382
405
|
*/
|
|
383
|
-
canUserAccessPage(
|
|
406
|
+
canUserAccessPage(caller: PageQuestionCaller, pageName: string, action: string, knownMetadata?: PageFrontmatter): Promise<boolean>;
|
|
384
407
|
/**
|
|
385
408
|
* {@link canUserAccessPage} as an affordance (#1668): would the door allow
|
|
386
409
|
* this action on that page — asked only to decide whether to show a control
|
|
@@ -391,7 +414,7 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
391
414
|
* @param knownMetadata - As for {@link canUserAccessPage}: the page's
|
|
392
415
|
* metadata when the caller has just resolved it (`PageManager.mayShowPage`).
|
|
393
416
|
*/
|
|
394
|
-
pageAffordanceFor(
|
|
417
|
+
pageAffordanceFor(caller: PageQuestionCaller, pageName: string, action: string, knownMetadata?: PageFrontmatter): Promise<boolean>;
|
|
395
418
|
/** The cross-page check, for the door (`record`) or an affordance (#1668). */
|
|
396
419
|
private crossPageDecision;
|
|
397
420
|
/**
|
|
@@ -417,7 +440,7 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
417
440
|
* A candidate without metadata is not listed (#714's convention). Order is
|
|
418
441
|
* preserved. Returns titles.
|
|
419
442
|
*/
|
|
420
|
-
filterAccessiblePages(
|
|
443
|
+
filterAccessiblePages(caller: PageQuestionCaller, action: string, candidates: ReadonlyArray<{
|
|
421
444
|
title: string;
|
|
422
445
|
metadata: PageFrontmatter | null | undefined;
|
|
423
446
|
}>): Promise<string[]>;
|
|
@@ -437,12 +460,27 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
437
460
|
* A refusal is audited as `authorization-deny` on the media resource, with
|
|
438
461
|
* the share attribution when there is one.
|
|
439
462
|
*/
|
|
440
|
-
canUserAccessMediaItem(
|
|
463
|
+
canUserAccessMediaItem(caller: PageQuestionCaller, item: MediaItem): Promise<boolean>;
|
|
441
464
|
/**
|
|
442
|
-
* Check front matter audience / access fields (Tier 1
|
|
465
|
+
* Check front matter audience / access fields (Tier 1).
|
|
443
466
|
* Returns a decision object; decided=false means no front matter restriction — fall through.
|
|
467
|
+
*
|
|
468
|
+
* An audience that leaves the subject out refuses every action, unless the
|
|
469
|
+
* subject holds `admin-system` (#1964, operator 2026-10-23): asked only
|
|
470
|
+
* then. `access[action]` has no such exception.
|
|
444
471
|
*/
|
|
445
472
|
private checkFrontmatterAccess;
|
|
473
|
+
/**
|
|
474
|
+
* Does the page's `audience` admit this subject (#1964)? True when the page
|
|
475
|
+
* names no audience, the subject matches it, or the subject holds
|
|
476
|
+
* `admin-system`. The same rule Tier 1 applies, asked by PageManager's
|
|
477
|
+
* write door, so a write cannot reach a page its writer is outside the
|
|
478
|
+
* audience of — whatever route it came by. Records nothing; the door does.
|
|
479
|
+
*
|
|
480
|
+
* @param caller - the caller's context, as given (a job's included, #2022)
|
|
481
|
+
* @param metadata - the page's current frontmatter
|
|
482
|
+
*/
|
|
483
|
+
audienceAdmits(caller: PageQuestionCaller, metadata: PageFrontmatter | null | undefined): Promise<boolean>;
|
|
446
484
|
/**
|
|
447
485
|
* Record/audit an access decision.
|
|
448
486
|
* 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.
|
package/lib/src/types/User.d.ts
CHANGED
|
@@ -73,7 +73,7 @@ export interface User {
|
|
|
73
73
|
/**
|
|
74
74
|
* @deprecated since #617 iteration 3b. Role membership is now stored
|
|
75
75
|
* canonically as OrganizationRole records owned by `RoleManager`; resolve
|
|
76
|
-
* a user's roles via `RoleManager.resolveUserRoles(username)`. The field
|
|
76
|
+
* a user's roles via `RoleManager.resolveUserRoles(ctx, username)`. The field
|
|
77
77
|
* is retained as optional for backwards compatibility while in-flight
|
|
78
78
|
* `users.json` files still carry it; the `strip-user-roles.ts` migration
|
|
79
79
|
* removes it. New code MUST NOT read or write this field directly.
|
|
@@ -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,
|
|
@@ -50,9 +50,9 @@ export declare const AUDIT_EVENT: {
|
|
|
50
50
|
readonly CONFIG_CHANGE: "config-change";
|
|
51
51
|
/** Every custom configuration value discarded; recorded before the reset, which cannot proceed without it */
|
|
52
52
|
readonly CONFIG_RESET: "config-reset";
|
|
53
|
-
/** A
|
|
53
|
+
/** A user accepted a document they are asked to accept (#1603): which document (core's terms-of-service or privacy-policy, or an add-on's), its version and effective date, and how (sign-up, sign-in, turning an add-on on). Recorded before the acceptance is stored, which cannot proceed without it: consent must be provable */
|
|
54
54
|
readonly CONSENT_GRANT: "consent-grant";
|
|
55
|
-
/** A
|
|
55
|
+
/** A user's acceptances ended (#1603): today only when their account is deleted, which removes their consent records; names the documents and versions they had accepted. Recorded before the records are removed */
|
|
56
56
|
readonly CONSENT_WITHDRAW: "consent-withdraw";
|
|
57
57
|
/** Contact created, changed, deleted or linked to an account; metadata names the kind (person or organization), the operation and the fields changed, never their values */
|
|
58
58
|
readonly CONTACT_MANAGE: "contact-manage";
|
|
@@ -112,21 +112,21 @@ export declare const AUDIT_EVENT: {
|
|
|
112
112
|
readonly POLICY_EVALUATE: "policy-evaluate";
|
|
113
113
|
/** Security posture at startup, compared against the previous start */
|
|
114
114
|
readonly POSTURE_RECORDED: "posture-recorded";
|
|
115
|
-
/** Proxy grant made: someone may act for the
|
|
115
|
+
/** Proxy grant made: someone may act for the user named in user; actor is who made it (#1762) */
|
|
116
116
|
readonly PROXY_GRANT: "proxy-grant";
|
|
117
|
-
/** Proxy grant ended: revoked, or the account closed; user is the
|
|
117
|
+
/** Proxy grant ended: revoked, or the account closed; user is the user represented, actor who ended it (#1762) */
|
|
118
118
|
readonly PROXY_REVOKE: "proxy-revoke";
|
|
119
|
-
/** A signed-in session started or stopped acting for someone under a proxy grant, or opens their private store: chosen, switched-back, ended because the grant no longer allows it (grant-ended, proxy-access-off), or vault-opened; user is the
|
|
119
|
+
/** A signed-in session started or stopped acting for someone under a proxy grant, or opens their private store: chosen, switched-back, ended because the grant no longer allows it (grant-ended, proxy-access-off), or vault-opened; user is the user represented, actor the user signed in (#1762) */
|
|
120
120
|
readonly PROXY_SWITCH: "proxy-switch";
|
|
121
|
-
/** Step-up: a re-authentication failed (wrong password, another
|
|
121
|
+
/** Step-up: a re-authentication failed (wrong password, another user's passkey, throttled) */
|
|
122
122
|
readonly REAUTH_FAILURE: "reauth-failure";
|
|
123
123
|
/** Step-up: an action asked for a fresh sign-in (#1525) — or refused a delegated credential that cannot give one */
|
|
124
124
|
readonly REAUTH_PROMPT: "reauth-prompt";
|
|
125
|
-
/** Step-up: the
|
|
125
|
+
/** Step-up: the user re-authenticated, with the factor used */
|
|
126
126
|
readonly REAUTH_SUCCESS: "reauth-success";
|
|
127
127
|
/** Page search; not recorded, read volume */
|
|
128
128
|
readonly SEARCH_PAGE: "search-page";
|
|
129
|
-
/**
|
|
129
|
+
/** Users searched for; enumerating users is disclosive in a way searching pages is not. Off by default as read volume */
|
|
130
130
|
readonly SEARCH_USER: "search-user";
|
|
131
131
|
/** A masked configuration value shown to an administrator; the key is recorded, never the value */
|
|
132
132
|
readonly SECRET_REVEAL: "secret-reveal";
|
|
@@ -146,9 +146,9 @@ export declare const AUDIT_EVENT: {
|
|
|
146
146
|
readonly SHARE_REVOKE: "share-revoke";
|
|
147
147
|
/** Sign-in approval: the browser that was waiting gave up the sign-in (#1532) */
|
|
148
148
|
readonly SIGNINAPPROVAL_ABANDON: "signinapproval-abandon";
|
|
149
|
-
/** Sign-in approval: the
|
|
149
|
+
/** Sign-in approval: the user pressed Approve (#1532) */
|
|
150
150
|
readonly SIGNINAPPROVAL_APPROVE: "signinapproval-approve";
|
|
151
|
-
/** Sign-in approval: the
|
|
151
|
+
/** Sign-in approval: the user pressed Deny; that sign-in will not complete (#1532) */
|
|
152
152
|
readonly SIGNINAPPROVAL_DENY: "signinapproval-deny";
|
|
153
153
|
/** Sign-in approval: a waiting sign-in expired before it was completed (#1532) */
|
|
154
154
|
readonly SIGNINAPPROVAL_EXPIRE: "signinapproval-expire";
|
|
@@ -57,9 +57,9 @@ export const AUDIT_EVENT = {
|
|
|
57
57
|
CONFIG_CHANGE: 'config-change',
|
|
58
58
|
/** Every custom configuration value discarded; recorded before the reset, which cannot proceed without it */
|
|
59
59
|
CONFIG_RESET: 'config-reset',
|
|
60
|
-
/** A
|
|
60
|
+
/** A user accepted a document they are asked to accept (#1603): which document (core's terms-of-service or privacy-policy, or an add-on's), its version and effective date, and how (sign-up, sign-in, turning an add-on on). Recorded before the acceptance is stored, which cannot proceed without it: consent must be provable */
|
|
61
61
|
CONSENT_GRANT: 'consent-grant',
|
|
62
|
-
/** A
|
|
62
|
+
/** A user's acceptances ended (#1603): today only when their account is deleted, which removes their consent records; names the documents and versions they had accepted. Recorded before the records are removed */
|
|
63
63
|
CONSENT_WITHDRAW: 'consent-withdraw',
|
|
64
64
|
/** Contact created, changed, deleted or linked to an account; metadata names the kind (person or organization), the operation and the fields changed, never their values */
|
|
65
65
|
CONTACT_MANAGE: 'contact-manage',
|
|
@@ -119,21 +119,21 @@ export const AUDIT_EVENT = {
|
|
|
119
119
|
POLICY_EVALUATE: 'policy-evaluate',
|
|
120
120
|
/** Security posture at startup, compared against the previous start */
|
|
121
121
|
POSTURE_RECORDED: 'posture-recorded',
|
|
122
|
-
/** Proxy grant made: someone may act for the
|
|
122
|
+
/** Proxy grant made: someone may act for the user named in user; actor is who made it (#1762) */
|
|
123
123
|
PROXY_GRANT: 'proxy-grant',
|
|
124
|
-
/** Proxy grant ended: revoked, or the account closed; user is the
|
|
124
|
+
/** Proxy grant ended: revoked, or the account closed; user is the user represented, actor who ended it (#1762) */
|
|
125
125
|
PROXY_REVOKE: 'proxy-revoke',
|
|
126
|
-
/** A signed-in session started or stopped acting for someone under a proxy grant, or opens their private store: chosen, switched-back, ended because the grant no longer allows it (grant-ended, proxy-access-off), or vault-opened; user is the
|
|
126
|
+
/** A signed-in session started or stopped acting for someone under a proxy grant, or opens their private store: chosen, switched-back, ended because the grant no longer allows it (grant-ended, proxy-access-off), or vault-opened; user is the user represented, actor the user signed in (#1762) */
|
|
127
127
|
PROXY_SWITCH: 'proxy-switch',
|
|
128
|
-
/** Step-up: a re-authentication failed (wrong password, another
|
|
128
|
+
/** Step-up: a re-authentication failed (wrong password, another user's passkey, throttled) */
|
|
129
129
|
REAUTH_FAILURE: 'reauth-failure',
|
|
130
130
|
/** Step-up: an action asked for a fresh sign-in (#1525) — or refused a delegated credential that cannot give one */
|
|
131
131
|
REAUTH_PROMPT: 'reauth-prompt',
|
|
132
|
-
/** Step-up: the
|
|
132
|
+
/** Step-up: the user re-authenticated, with the factor used */
|
|
133
133
|
REAUTH_SUCCESS: 'reauth-success',
|
|
134
134
|
/** Page search; not recorded, read volume */
|
|
135
135
|
SEARCH_PAGE: 'search-page',
|
|
136
|
-
/**
|
|
136
|
+
/** Users searched for; enumerating users is disclosive in a way searching pages is not. Off by default as read volume */
|
|
137
137
|
SEARCH_USER: 'search-user',
|
|
138
138
|
/** A masked configuration value shown to an administrator; the key is recorded, never the value */
|
|
139
139
|
SECRET_REVEAL: 'secret-reveal',
|
|
@@ -153,9 +153,9 @@ export const AUDIT_EVENT = {
|
|
|
153
153
|
SHARE_REVOKE: 'share-revoke',
|
|
154
154
|
/** Sign-in approval: the browser that was waiting gave up the sign-in (#1532) */
|
|
155
155
|
SIGNINAPPROVAL_ABANDON: 'signinapproval-abandon',
|
|
156
|
-
/** Sign-in approval: the
|
|
156
|
+
/** Sign-in approval: the user pressed Approve (#1532) */
|
|
157
157
|
SIGNINAPPROVAL_APPROVE: 'signinapproval-approve',
|
|
158
|
-
/** Sign-in approval: the
|
|
158
|
+
/** Sign-in approval: the user pressed Deny; that sign-in will not complete (#1532) */
|
|
159
159
|
SIGNINAPPROVAL_DENY: 'signinapproval-deny',
|
|
160
160
|
/** Sign-in approval: a waiting sign-in expired before it was completed (#1532) */
|
|
161
161
|
SIGNINAPPROVAL_EXPIRE: 'signinapproval-expire',
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
* A route turns it into the answer with `ApiContext.enforceDeny(err.permission,
|
|
13
13
|
* err.decision)`, the one place a refusal is shaped (#1431); the door never
|
|
14
14
|
* builds an HTTP answer itself.
|
|
15
|
+
*
|
|
16
|
+
* Every lookup takes a context too (#2018, operator 2026-10-23), through
|
|
17
|
+
* {@link requireRecordAccess}: a user's own identity record under
|
|
18
|
+
* `profile-manage`, anyone else's under `contact-read` / `contact-manage`, and
|
|
19
|
+
* core's own reads under the system context without a policy evaluation, so
|
|
20
|
+
* resolving roles on every request stays cheap.
|
|
15
21
|
*/
|
|
16
22
|
import { type ActorContext } from '../context/ActorContext.js';
|
|
17
23
|
import type { CorePermission } from '../security/permissions.generated.js';
|
|
@@ -37,6 +43,30 @@ export declare class ContactRefusedError extends Error {
|
|
|
37
43
|
* refuses: closed, never open.
|
|
38
44
|
*/
|
|
39
45
|
export declare function requireContactPermission(engine: EngineLike, ctx: ActorContext, kind: keyof typeof CONTACT_PERMISSION, also?: readonly CorePermission[]): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Core's own work (#2018): a boot or schedule context naming the system
|
|
48
|
+
* principal, with no token, share or proxy claim. Recognised by its fields
|
|
49
|
+
* alone, with no policy evaluation, because RoleManager resolves roles
|
|
50
|
+
* through these lookups on every request. A request never carries this
|
|
51
|
+
* context: the system principal's name is reserved, so no account holds it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isSystemWork(engine: EngineLike, ctx: ActorContext | null | undefined): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* A lookup or change of one record at a contacts door (#2018). Throws
|
|
56
|
+
* {@link ContactRefusedError} unless `ctx` may:
|
|
57
|
+
*
|
|
58
|
+
* - read it as core's own work ({@link isSystemWork}): allowed, not evaluated;
|
|
59
|
+
* - read or change its own identity record (the context's username is the
|
|
60
|
+
* record's `identifier`): `profile-manage`, which every user holds;
|
|
61
|
+
* - read or change any other record: `contact-read` / `contact-manage`.
|
|
62
|
+
*
|
|
63
|
+
* `record` is the identity record (or what names it, `{ identifier }`); left
|
|
64
|
+
* out for a record that is never anyone's own (an organization, a role).
|
|
65
|
+
* "Own" is decided by comparing usernames, never by a role.
|
|
66
|
+
*/
|
|
67
|
+
export declare function requireRecordAccess(engine: EngineLike, ctx: ActorContext, kind: keyof typeof CONTACT_PERMISSION, record?: {
|
|
68
|
+
identifier?: unknown;
|
|
69
|
+
} | null): Promise<void>;
|
|
40
70
|
/** What a contacts record says was done. Names fields, never their values. */
|
|
41
71
|
export interface ContactChange {
|
|
42
72
|
kind: 'person' | 'organization';
|
|
@@ -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,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KeyedSerial — one task at a time per key, within this process (#2015).
|
|
3
|
+
*
|
|
4
|
+
* A read-modify-write of one record (read the file, change it, write it back)
|
|
5
|
+
* is only safe while nothing else writes that record in between. Two role
|
|
6
|
+
* changes to one role used to interleave: each read the member list before the
|
|
7
|
+
* other wrote it, and the second write dropped the first one's member. Running
|
|
8
|
+
* every read-modify-write of a key through `run` makes them happen one after
|
|
9
|
+
* another, each reading what the last one wrote. Different keys still run side
|
|
10
|
+
* by side.
|
|
11
|
+
*
|
|
12
|
+
* What it does not do:
|
|
13
|
+
*
|
|
14
|
+
* - **Not across processes.** Two server processes sharing one data folder can
|
|
15
|
+
* still race; no supported deployment runs that way.
|
|
16
|
+
* - **Not re-entrant.** A task must not `run` the same key again and wait for
|
|
17
|
+
* it: the inner task queues behind the outer one, which never finishes.
|
|
18
|
+
*
|
|
19
|
+
* A failed task fails its own caller only; the next task on the key runs as
|
|
20
|
+
* usual. A key's entry is dropped once its queue is empty, so the map holds
|
|
21
|
+
* only keys with work in flight.
|
|
22
|
+
*/
|
|
23
|
+
export declare class KeyedSerial {
|
|
24
|
+
/** The settled tail of each key's queue: never rejects. */
|
|
25
|
+
private readonly tails;
|
|
26
|
+
/** Run `task` after every earlier task on `key` has settled; resolves or rejects as `task` does. */
|
|
27
|
+
run<T>(key: string, task: () => Promise<T>): Promise<T>;
|
|
28
|
+
/** Resolves once every task queued so far, on every key, has settled (shutdown). */
|
|
29
|
+
settled(): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export default KeyedSerial;
|
|
@@ -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 {};
|