@jwilleke/ngdpbase-types 5.36.0 → 5.38.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 +1 -0
- package/lib/packages/ngdpbase-types/testing.js +1 -1
- package/lib/src/managers/AddonsManager.d.ts +52 -25
- package/lib/src/managers/ConfigurationManager.d.ts +5 -6
- package/lib/src/security/permissions.generated.d.ts +2 -0
- package/lib/src/utils/addonConfigLayer.d.ts +2 -4
- package/lib/src/utils/addonConfigLayer.js +10 -17
- package/lib/src/utils/addonsPathResolver.d.ts +17 -48
- package/lib/src/utils/addonsPathResolver.js +23 -83
- package/lib/src/utils/auditEventNames.generated.d.ts +2 -0
- package/lib/src/utils/auditEventNames.generated.js +2 -0
- package/package.json +4 -5
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* only, so the published entry carries no JavaScript.
|
|
16
16
|
*/
|
|
17
17
|
export type { AddonModule, AddonManifest, AddonListing } from '../../src/managers/AddonsManager.js';
|
|
18
|
+
export type { AddonDashboardCard, AddonCardSurface, AddonCardSummary, AddonCardFigure } from '../../src/managers/AddonsManager.js';
|
|
18
19
|
export type { AddonHost, AddonIceDeclaration, PersonSwitchSetter } from '../../src/utils/addonHost.js';
|
|
19
20
|
export type { WikiEngine } from '../../src/types/WikiEngine.js';
|
|
20
21
|
export type { ApiContext } from '../../src/context/ApiContext.js';
|
|
@@ -48,7 +48,7 @@ export function openAddonTestDatabase(owner, options, declaredBy) {
|
|
|
48
48
|
checkAddonDatabaseRequest(owner, options);
|
|
49
49
|
if (declaredBy) {
|
|
50
50
|
const { slug, defaults } = declaredBy;
|
|
51
|
-
const layer = addonDatabaseLayer([{ slug, dir: '',
|
|
51
|
+
const layer = addonDatabaseLayer([{ slug, dir: '', enabled: true, defaults }], {});
|
|
52
52
|
const refused = layer.refused.get(slug);
|
|
53
53
|
if (refused)
|
|
54
54
|
throw new Error(`Add-on '${slug}' is not loaded: ${refused}`);
|
|
@@ -289,9 +289,27 @@ export interface AddonStatusDetails {
|
|
|
289
289
|
message?: string;
|
|
290
290
|
[key: string]: unknown;
|
|
291
291
|
}
|
|
292
|
+
/** The pages an add-on's card can be shown on: `/admin` (the default) and the Executive Hub, `/executive` (#2093). */
|
|
293
|
+
export type AddonCardSurface = 'admin' | 'executive';
|
|
294
|
+
/** One labelled figure on an Executive Hub card (`Funds`, `$12,400.00`). The add-on formats the value. */
|
|
295
|
+
export interface AddonCardFigure {
|
|
296
|
+
label: string;
|
|
297
|
+
value: string | number;
|
|
298
|
+
}
|
|
299
|
+
/** What an Executive Hub card shows a viewer: its figures, and an optional line under them. */
|
|
300
|
+
export interface AddonCardSummary {
|
|
301
|
+
figures?: AddonCardFigure[];
|
|
302
|
+
message?: string;
|
|
303
|
+
}
|
|
292
304
|
/**
|
|
293
|
-
* Card registered by an add-on for display on the admin dashboard
|
|
294
|
-
*
|
|
305
|
+
* Card registered by an add-on for display on the admin dashboard, or, with
|
|
306
|
+
* `surface: 'executive'`, on the Executive Hub (#2093). An add-on registers
|
|
307
|
+
* at most one card per surface.
|
|
308
|
+
*
|
|
309
|
+
* An admin card's live stats come from the add-on's existing status() method.
|
|
310
|
+
* An executive card names the `permission` a viewer must hold to see it, and
|
|
311
|
+
* its figures come from `summary(ctx)`, called with the viewer's context, so
|
|
312
|
+
* the add-on's own doors decide what it reads.
|
|
295
313
|
*/
|
|
296
314
|
export interface AddonDashboardCard {
|
|
297
315
|
/** Must match the add-on's registered name */
|
|
@@ -300,8 +318,25 @@ export interface AddonDashboardCard {
|
|
|
300
318
|
title: string;
|
|
301
319
|
/** Font Awesome class, e.g. 'fas fa-wpforms' */
|
|
302
320
|
icon: string;
|
|
303
|
-
/**
|
|
321
|
+
/** The page the card links to: the add-on's admin page, or for an executive card the page with the detail */
|
|
304
322
|
adminUrl: string;
|
|
323
|
+
/** Where the card is shown; omitted means `/admin` */
|
|
324
|
+
surface?: AddonCardSurface;
|
|
325
|
+
/** Executive cards: the permission a viewer must hold to see the card (the add-on's own read permission). Required there */
|
|
326
|
+
permission?: string;
|
|
327
|
+
/** Executive cards: the card's figures for this viewer, read with their context. Throw to say they could not be read */
|
|
328
|
+
summary?: (ctx: ActorContext) => Promise<AddonCardSummary>;
|
|
329
|
+
}
|
|
330
|
+
/** An executive card as the hub shows it to one viewer: the figures read for them, or that they could not be. */
|
|
331
|
+
export interface ExecutiveCardView {
|
|
332
|
+
addonName: string;
|
|
333
|
+
title: string;
|
|
334
|
+
icon: string;
|
|
335
|
+
adminUrl: string;
|
|
336
|
+
figures: AddonCardFigure[];
|
|
337
|
+
message: string;
|
|
338
|
+
/** True when the add-on's summary threw; the card says its figures could not be read. */
|
|
339
|
+
failed: boolean;
|
|
305
340
|
}
|
|
306
341
|
/** #2100: the PDP refused an administrator's add-on Update or Check. */
|
|
307
342
|
export declare class AddonUpdateForbiddenError extends Error {
|
|
@@ -342,8 +377,6 @@ declare class AddonsManager extends BaseManager {
|
|
|
342
377
|
private addonsPaths;
|
|
343
378
|
/** Resolved absolute paths to addons directories */
|
|
344
379
|
private resolvedAddonsPaths;
|
|
345
|
-
/** #673: npm package globs (from `node_modules:` addons-path entries) to discover from node_modules. */
|
|
346
|
-
private npmAddonPatterns;
|
|
347
380
|
/** #1830: `<data>/addons`, where installed add-ons live; null when there is no data folder. */
|
|
348
381
|
private installedDir;
|
|
349
382
|
/** #1830: the installed add-ons verified this boot — the only ones loaded from `installedDir`. */
|
|
@@ -461,25 +494,10 @@ declare class AddonsManager extends BaseManager {
|
|
|
461
494
|
* Scan a single addons directory and register any add-ons found.
|
|
462
495
|
*/
|
|
463
496
|
private scanAddonsDirectory;
|
|
464
|
-
/**
|
|
465
|
-
* #673 (packaged model): discover addons published as npm packages, matching a
|
|
466
|
-
* `node_modules:<glob>` addons-path entry (e.g. `node_modules:@jwilleke/*-addon`).
|
|
467
|
-
* Each matching package is loaded through the same slug/module/register()
|
|
468
|
-
* contract as bundled and drop-in addons — only discovery differs.
|
|
469
|
-
*/
|
|
470
|
-
private scanNpmAddons;
|
|
471
|
-
/** Locate the node_modules directory (app root / cwd). */
|
|
472
|
-
private findNodeModules;
|
|
473
|
-
/** Expand a `@scope/glob` (or bare `glob`) pattern to package directories. */
|
|
474
|
-
private resolveNpmAddonDirs;
|
|
475
497
|
/**
|
|
476
498
|
* Load a single addon from its directory (index.js/ts + optional package.json
|
|
477
|
-
* `ngdpbase` manifest) and register it.
|
|
478
|
-
*
|
|
479
|
-
* no-slug identity fallback (#927): a directory addon's folder name is its
|
|
480
|
-
* identity verbatim, while an npm package strips the conventional `-addon`
|
|
481
|
-
* suffix. Duplicate ids are skipped — since npm discovery runs last, a
|
|
482
|
-
* bundled/drop-in addon wins.
|
|
499
|
+
* `ngdpbase` manifest) and register it. `sourceLabel` is for logging only.
|
|
500
|
+
* Duplicate ids are skipped, so the first directory scanned wins.
|
|
483
501
|
*/
|
|
484
502
|
private registerAddonFromDir;
|
|
485
503
|
/**
|
|
@@ -840,10 +858,19 @@ declare class AddonsManager extends BaseManager {
|
|
|
840
858
|
*/
|
|
841
859
|
registerDashboardCard(card: AddonDashboardCard): void;
|
|
842
860
|
/**
|
|
843
|
-
* Return
|
|
844
|
-
*
|
|
861
|
+
* Return the registered cards for one surface, in registration order.
|
|
862
|
+
* The admin dashboard route calls it with no argument for the `/admin` cards.
|
|
863
|
+
*/
|
|
864
|
+
getDashboardCards(surface?: AddonCardSurface): AddonDashboardCard[];
|
|
865
|
+
/**
|
|
866
|
+
* The Executive Hub's cards for one viewer (#2093): each card whose
|
|
867
|
+
* permission the PDP permits `ctx`, asked with that context, with the
|
|
868
|
+
* figures its add-on's `summary(ctx)` reads for them. A card the viewer may
|
|
869
|
+
* not see is left out; a summary that throws shows the card with its
|
|
870
|
+
* figures marked as not read, and is logged by add-on name only. No
|
|
871
|
+
* context, or no PDP, shows no cards.
|
|
845
872
|
*/
|
|
846
|
-
|
|
873
|
+
executiveCardsFor(ctx: ActorContext): Promise<ExecutiveCardView[]>;
|
|
847
874
|
/**
|
|
848
875
|
* Get list of all discovered add-on names
|
|
849
876
|
*/
|
|
@@ -141,16 +141,15 @@ declare class ConfigurationManager extends BaseManager {
|
|
|
141
141
|
/**
|
|
142
142
|
* #672: refuse to start if any `ngdpbase.addons.<id>.enabled = true` key
|
|
143
143
|
* references an `<id>` that has no matching addon in any configured
|
|
144
|
-
* `addons-path`
|
|
145
|
-
*
|
|
144
|
+
* `addons-path` directory (bundled/drop-in) and is not named in
|
|
145
|
+
* `ngdpbase.addons.install`. A retired `node_modules:` entry is ignored (#2102).
|
|
146
146
|
* The discovery logic mirrors what `AddonsManager` does at runtime —
|
|
147
147
|
* directory/package present with `index.js` or `index.ts` — but without
|
|
148
148
|
* importing the modules (boot-time speed; `ConfigurationManager.initialize()`
|
|
149
|
-
* runs before any managers). The
|
|
150
|
-
*
|
|
149
|
+
* runs before any managers). The addons-path parsing AND the
|
|
150
|
+
* canonical-identity resolution are shared with
|
|
151
151
|
* `AddonsManager` via `utils/addonsPathResolver.ts` so the two can no
|
|
152
|
-
* longer drift out of sync (#924
|
|
153
|
-
* never boot as a result).
|
|
152
|
+
* longer drift out of sync (#924).
|
|
154
153
|
*
|
|
155
154
|
* Catches the failure mode that caused the 2026-05-10 geohazardwatch.com
|
|
156
155
|
* outage: the deploy configmap had `ngdpbase.addons.ve-geology.enabled =
|
|
@@ -22,6 +22,8 @@ export type CorePermission =
|
|
|
22
22
|
| 'comment-create'
|
|
23
23
|
/** Change any configuration setting, and download a backup (#1525). Admin only by default; asks for a fresh sign-in when listed in ngdpbase.auth.step-up */
|
|
24
24
|
| 'config-manage'
|
|
25
|
+
/** Open the Executive Hub (/executive): the organization's summary cards and the requests waiting for the board (#2093). Held by the executive role; deciding a request still needs that request's own permission */
|
|
26
|
+
| 'executive-read'
|
|
25
27
|
/** Create, edit, delete and link identity records, of users and contacts alike, and organizations, and an identity's affiliations, through IdentityManager and OrganizationManager (#1930). Reading them is identity-read. Was contact-manage until #2038; a policy naming that is still read as this for one release. Admins by default; an add-on grants it to its own roles in its own policy */
|
|
26
28
|
| 'identity-manage'
|
|
27
29
|
/** List, search and view identity records, of users and contacts alike, and organizations, and their affiliations, through IdentityManager and OrganizationManager, and the /admin/contacts and /admin/organizations views (#1930). Was contact-read until #2038; a policy naming that is still read as this for one release. Admins and demo admins by default; an add-on grants it to its own roles in its own policy */
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
* shipped entry. The operator's file still wins over everything.
|
|
19
19
|
*
|
|
20
20
|
* Discovery follows `ngdpbase.managers.addons-manager.addons-path` exactly as
|
|
21
|
-
* AddonsManager does —
|
|
22
|
-
* and
|
|
21
|
+
* AddonsManager does — directory entries — so bundled
|
|
22
|
+
* and drop-in addons are treated alike. Only an addon whose
|
|
23
23
|
* `ngdpbase.addons.<slug>.enabled` is true in the shipped+custom view
|
|
24
24
|
* contributes; a disabled addon's opinions do not reach the catalog.
|
|
25
25
|
*
|
|
@@ -63,7 +63,6 @@ export declare const ADDONS_MANAGER_ENABLED_KEY = "ngdpbase.managers.addons-mana
|
|
|
63
63
|
export interface AddonDefaults {
|
|
64
64
|
slug: string;
|
|
65
65
|
dir: string;
|
|
66
|
-
source: 'directory' | 'npm';
|
|
67
66
|
/**
|
|
68
67
|
* Whether the addon is switched on. A turned-off addon's file is read for its
|
|
69
68
|
* system-categories alone (#1559); none of its other keys reach the merge.
|
|
@@ -83,7 +82,6 @@ export declare function isAddonEnabled(base: Record<string, unknown>, slug: stri
|
|
|
83
82
|
*/
|
|
84
83
|
export declare function discoverAddonDirs(base: Record<string, unknown>, cwd?: string, dataFolder?: string): Array<{
|
|
85
84
|
dir: string;
|
|
86
|
-
source: 'directory' | 'npm';
|
|
87
85
|
}>;
|
|
88
86
|
/**
|
|
89
87
|
* The defaults of every installed addon, enabled or not, in discovery order,
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
* shipped entry. The operator's file still wins over everything.
|
|
19
19
|
*
|
|
20
20
|
* Discovery follows `ngdpbase.managers.addons-manager.addons-path` exactly as
|
|
21
|
-
* AddonsManager does —
|
|
22
|
-
* and
|
|
21
|
+
* AddonsManager does — directory entries — so bundled
|
|
22
|
+
* and drop-in addons are treated alike. Only an addon whose
|
|
23
23
|
* `ngdpbase.addons.<slug>.enabled` is true in the shipped+custom view
|
|
24
24
|
* contributes; a disabled addon's opinions do not reach the catalog.
|
|
25
25
|
*
|
|
@@ -61,7 +61,7 @@ import fs from 'fs';
|
|
|
61
61
|
import path from 'path';
|
|
62
62
|
import { isDeepStrictEqual } from 'util';
|
|
63
63
|
import { configFilePaths, deepMergeConfigs, deepMergeObjects, instanceDataFolder, isPlainObject, readConfigFilesSync } from './configFiles.js';
|
|
64
|
-
import { configuredInstallSlugs,
|
|
64
|
+
import { configuredInstallSlugs, installedAddonsDir, resolveAddonSlug, splitAddonsPath } from './addonsPathResolver.js';
|
|
65
65
|
import { lockedAddonDirsSync } from './addonLock.js';
|
|
66
66
|
import { LEGACY_SECRET_KEYS_KEY, SENSITIVE_VALUES_KEY } from './sensitiveValues.js';
|
|
67
67
|
import { parsePrivatestore } from './privateStorePath.js';
|
|
@@ -81,7 +81,7 @@ export function isAddonEnabled(base, slug) {
|
|
|
81
81
|
export function discoverAddonDirs(base, cwd = process.cwd(), dataFolder = instanceDataFolder()) {
|
|
82
82
|
if (base[ADDONS_MANAGER_ENABLED_KEY] === false)
|
|
83
83
|
return [];
|
|
84
|
-
const { directories
|
|
84
|
+
const { directories } = splitAddonsPath(base[ADDONS_PATH_KEY] ?? './addons');
|
|
85
85
|
const found = [];
|
|
86
86
|
for (const raw of directories) {
|
|
87
87
|
const root = path.resolve(cwd, raw);
|
|
@@ -95,20 +95,13 @@ export function discoverAddonDirs(base, cwd = process.cwd(), dataFolder = instan
|
|
|
95
95
|
for (const entry of entries) {
|
|
96
96
|
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'shared')
|
|
97
97
|
continue;
|
|
98
|
-
found.push({ dir: path.join(root, entry.name)
|
|
98
|
+
found.push({ dir: path.join(root, entry.name) });
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
const installed = installedAddonsDir(dataFolder, cwd);
|
|
102
102
|
if (!found.some((f) => path.dirname(f.dir) === installed)) {
|
|
103
103
|
for (const dir of lockedAddonDirsSync(installed, configuredInstallSlugs(base)))
|
|
104
|
-
found.push({ dir
|
|
105
|
-
}
|
|
106
|
-
const nmRoot = npmPatterns.length > 0 ? findNodeModulesDir(cwd) : null;
|
|
107
|
-
if (nmRoot) {
|
|
108
|
-
for (const pattern of npmPatterns) {
|
|
109
|
-
for (const dir of matchNpmPackageDirs(nmRoot, pattern))
|
|
110
|
-
found.push({ dir, source: 'npm' });
|
|
111
|
-
}
|
|
104
|
+
found.push({ dir });
|
|
112
105
|
}
|
|
113
106
|
return found;
|
|
114
107
|
}
|
|
@@ -122,8 +115,8 @@ export function discoverAddonDirs(base, cwd = process.cwd(), dataFolder = instan
|
|
|
122
115
|
*/
|
|
123
116
|
export function discoverAddonDefaults(base, cwd = process.cwd(), dataFolder = instanceDataFolder()) {
|
|
124
117
|
const out = [];
|
|
125
|
-
for (const { dir
|
|
126
|
-
const slug = resolveAddonSlug(dir
|
|
118
|
+
for (const { dir } of discoverAddonDirs(base, cwd, dataFolder)) {
|
|
119
|
+
const slug = resolveAddonSlug(dir);
|
|
127
120
|
const enabled = isAddonEnabled(base, slug);
|
|
128
121
|
const file = path.join(dir, 'config', 'default-config.json');
|
|
129
122
|
if (!fs.existsSync(file))
|
|
@@ -136,10 +129,10 @@ export function discoverAddonDefaults(base, cwd = process.cwd(), dataFolder = in
|
|
|
136
129
|
for (const [key, value] of Object.entries(raw))
|
|
137
130
|
if (!key.startsWith('_'))
|
|
138
131
|
defaults[key] = value;
|
|
139
|
-
out.push({ slug, dir,
|
|
132
|
+
out.push({ slug, dir, enabled, defaults });
|
|
140
133
|
}
|
|
141
134
|
catch (err) {
|
|
142
|
-
out.push({ slug, dir,
|
|
135
|
+
out.push({ slug, dir, enabled, defaults: {}, error: err instanceof Error ? err.message : String(err) });
|
|
143
136
|
}
|
|
144
137
|
}
|
|
145
138
|
return out;
|
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared parsing/discovery for the `ngdpbase.managers.addons-manager.addons-path`
|
|
3
|
-
* config value
|
|
3
|
+
* config value, used by `AddonsManager` (runtime discovery), `ConfigurationManager`
|
|
4
|
+
* (the #672 boot-time "does this enabled addon exist" check) and the add-on
|
|
5
|
+
* config layer, so the three cannot drift (#924).
|
|
4
6
|
*
|
|
5
|
-
* #
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* updated for #673's `node_modules:<glob>` packaged-addon syntax, but the
|
|
9
|
-
* validator's copy was not, so it resolved a `node_modules:` entry as a
|
|
10
|
-
* literal (non-existent) directory and treated every packaged addon as
|
|
11
|
-
* "unknown" — crashing boot the moment one was enabled. Consolidating the
|
|
12
|
-
* parsing/matching into one module removes the class of bug, not just this
|
|
13
|
-
* instance of it.
|
|
7
|
+
* #2102: the packaged model (`node_modules:<glob>` entries) is gone. A site
|
|
8
|
+
* config may still hold such an entry; it is ignored, never treated as a
|
|
9
|
+
* directory, and never stops the boot.
|
|
14
10
|
*/
|
|
15
|
-
/** Prefix marking an addons-path entry as an npm-package glob rather than a directory. */
|
|
16
|
-
export declare const NPM_ADDON_PREFIX = "node_modules:";
|
|
17
11
|
export interface SplitAddonsPath {
|
|
18
12
|
/** Plain directory entries (bundled/drop-in), not yet resolved to absolute. */
|
|
19
13
|
directories: string[];
|
|
20
|
-
/**
|
|
21
|
-
|
|
14
|
+
/** Retired `node_modules:` entries (#2102): ignored; AddonsManager warns about them once at start. */
|
|
15
|
+
retired: string[];
|
|
22
16
|
}
|
|
23
17
|
/**
|
|
24
18
|
* Coerce the raw config value (string or string[]) and split it into
|
|
25
|
-
* filesystem directory entries
|
|
19
|
+
* filesystem directory entries and retired `node_modules:` entries.
|
|
26
20
|
*/
|
|
27
21
|
export declare function splitAddonsPath(raw: unknown): SplitAddonsPath;
|
|
22
|
+
/** The warning for one retired `node_modules:` entry (#2102). */
|
|
23
|
+
export declare function retiredAddonsPathWarning(entry: string): string;
|
|
28
24
|
/** The config key naming the add-ons to install (#1830). */
|
|
29
25
|
export declare const ADDONS_INSTALL_KEY = "ngdpbase.addons.install";
|
|
30
26
|
/**
|
|
@@ -35,42 +31,15 @@ export declare const ADDONS_INSTALL_KEY = "ngdpbase.addons.install";
|
|
|
35
31
|
export declare function installedAddonsDir(dataFolder: string, cwd?: string): string;
|
|
36
32
|
/** The slugs `ngdpbase.addons.install` names, from a merged or shipped+custom view. */
|
|
37
33
|
export declare function configuredInstallSlugs(config: Record<string, unknown>): string[];
|
|
38
|
-
/** Locate the `node_modules` directory to search for packaged addons (resolved from cwd). */
|
|
39
|
-
export declare function findNodeModulesDir(cwd?: string): string | null;
|
|
40
|
-
/**
|
|
41
|
-
* Expand a `@scope/glob` (or bare `glob`) npm pattern to matching package
|
|
42
|
-
* directories under `nmRoot`.
|
|
43
|
-
*/
|
|
44
|
-
export declare function matchNpmPackageDirs(nmRoot: string, pattern: string): string[];
|
|
45
|
-
/**
|
|
46
|
-
* Best-effort addon identity for a packaged (npm) addon directory, derived
|
|
47
|
-
* from its package-directory name alone — no module import, matching the
|
|
48
|
-
* same boot-time-speed, no-module-loading design already accepted for the
|
|
49
|
-
* directory-scan case (see `ConfigurationManager.assertConfiguredAddonsExist`).
|
|
50
|
-
*
|
|
51
|
-
* Follows the documented packaged-addon naming convention
|
|
52
|
-
* (`@scope/<slug>-addon`, see docs/platform/deployment/addon-packaged.md):
|
|
53
|
-
* strips a trailing `-addon` suffix if present, else returns the folder name
|
|
54
|
-
* as-is.
|
|
55
|
-
*
|
|
56
|
-
* Caveat (same class as the existing directory-name heuristic): the addon's
|
|
57
|
-
* *real* identity is whatever its module exports as `name` at runtime. A
|
|
58
|
-
* package that doesn't follow the `-addon` naming convention, or whose
|
|
59
|
-
* module exports a different `name`, won't be matched precisely here — this
|
|
60
|
-
* is a validation-time approximation, not the source of truth.
|
|
61
|
-
*/
|
|
62
|
-
export declare function deriveAddonSlugFromPackageDirName(dirName: string): string;
|
|
63
34
|
/**
|
|
64
35
|
* Canonical identity for an addon directory (#927), resolved the SAME
|
|
65
36
|
* import-free way in every layer (discovery, `isEnabled`, the boot-time
|
|
66
|
-
* validator)
|
|
67
|
-
* config key, manifest slug) can no longer drift:
|
|
37
|
+
* validator):
|
|
68
38
|
*
|
|
69
|
-
* canonicalId = package.json `ngdpbase.slug` ??
|
|
39
|
+
* canonicalId = package.json `ngdpbase.slug` ?? the folder name verbatim
|
|
70
40
|
*
|
|
71
|
-
* Reads `package.json` statically — never imports the module
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* label validated against this at load time, not a source of identity.
|
|
41
|
+
* Reads `package.json` statically — never imports the module. The module's
|
|
42
|
+
* exported `name` is a display label validated against this at load time,
|
|
43
|
+
* not a source of identity.
|
|
75
44
|
*/
|
|
76
|
-
export declare function resolveAddonSlug(addonDir: string
|
|
45
|
+
export declare function resolveAddonSlug(addonDir: string): string;
|
|
@@ -1,36 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared parsing/discovery for the `ngdpbase.managers.addons-manager.addons-path`
|
|
3
|
-
* config value
|
|
3
|
+
* config value, used by `AddonsManager` (runtime discovery), `ConfigurationManager`
|
|
4
|
+
* (the #672 boot-time "does this enabled addon exist" check) and the add-on
|
|
5
|
+
* config layer, so the three cannot drift (#924).
|
|
4
6
|
*
|
|
5
|
-
* #
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* updated for #673's `node_modules:<glob>` packaged-addon syntax, but the
|
|
9
|
-
* validator's copy was not, so it resolved a `node_modules:` entry as a
|
|
10
|
-
* literal (non-existent) directory and treated every packaged addon as
|
|
11
|
-
* "unknown" — crashing boot the moment one was enabled. Consolidating the
|
|
12
|
-
* parsing/matching into one module removes the class of bug, not just this
|
|
13
|
-
* instance of it.
|
|
7
|
+
* #2102: the packaged model (`node_modules:<glob>` entries) is gone. A site
|
|
8
|
+
* config may still hold such an entry; it is ignored, never treated as a
|
|
9
|
+
* directory, and never stops the boot.
|
|
14
10
|
*/
|
|
15
11
|
import * as fs from 'fs';
|
|
16
12
|
import * as path from 'path';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export const NPM_ADDON_PREFIX = 'node_modules:';
|
|
13
|
+
/** The retired packaged-model prefix, recognised only so the entry can be ignored with a warning. */
|
|
14
|
+
const RETIRED_PACKAGED_PREFIX = 'node_modules:';
|
|
20
15
|
/**
|
|
21
16
|
* Coerce the raw config value (string or string[]) and split it into
|
|
22
|
-
* filesystem directory entries
|
|
17
|
+
* filesystem directory entries and retired `node_modules:` entries.
|
|
23
18
|
*/
|
|
24
19
|
export function splitAddonsPath(raw) {
|
|
25
20
|
const entries = Array.isArray(raw) ? raw.map(String) : [String(raw)];
|
|
26
21
|
return {
|
|
27
|
-
directories: entries.filter(p => !p.startsWith(
|
|
28
|
-
|
|
29
|
-
.filter(p => p.startsWith(NPM_ADDON_PREFIX))
|
|
30
|
-
.map(p => p.slice(NPM_ADDON_PREFIX.length).trim())
|
|
31
|
-
.filter(Boolean)
|
|
22
|
+
directories: entries.filter(p => !p.trim().startsWith(RETIRED_PACKAGED_PREFIX)),
|
|
23
|
+
retired: entries.filter(p => p.trim().startsWith(RETIRED_PACKAGED_PREFIX))
|
|
32
24
|
};
|
|
33
25
|
}
|
|
26
|
+
/** The warning for one retired `node_modules:` entry (#2102). */
|
|
27
|
+
export function retiredAddonsPathWarning(entry) {
|
|
28
|
+
return `addons-path entry '${entry}' is no longer supported and is ignored; install add-ons with ngdpbase.addons.install`;
|
|
29
|
+
}
|
|
34
30
|
/** The config key naming the add-ons to install (#1830). */
|
|
35
31
|
export const ADDONS_INSTALL_KEY = 'ngdpbase.addons.install';
|
|
36
32
|
/**
|
|
@@ -46,69 +42,18 @@ export function configuredInstallSlugs(config) {
|
|
|
46
42
|
const map = config[ADDONS_INSTALL_KEY];
|
|
47
43
|
return map && typeof map === 'object' && !Array.isArray(map) ? Object.keys(map) : [];
|
|
48
44
|
}
|
|
49
|
-
/** Locate the `node_modules` directory to search for packaged addons (resolved from cwd). */
|
|
50
|
-
export function findNodeModulesDir(cwd = process.cwd()) {
|
|
51
|
-
const nm = path.resolve(cwd, 'node_modules');
|
|
52
|
-
return fs.existsSync(nm) && fs.statSync(nm).isDirectory() ? nm : null;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Expand a `@scope/glob` (or bare `glob`) npm pattern to matching package
|
|
56
|
-
* directories under `nmRoot`.
|
|
57
|
-
*/
|
|
58
|
-
export function matchNpmPackageDirs(nmRoot, pattern) {
|
|
59
|
-
let baseDir = nmRoot;
|
|
60
|
-
let nameGlob = pattern;
|
|
61
|
-
if (pattern.startsWith('@')) {
|
|
62
|
-
const slash = pattern.indexOf('/');
|
|
63
|
-
const scope = slash > 0 ? pattern.slice(0, slash) : pattern;
|
|
64
|
-
nameGlob = slash > 0 ? pattern.slice(slash + 1) : '*';
|
|
65
|
-
baseDir = path.join(nmRoot, scope);
|
|
66
|
-
}
|
|
67
|
-
if (!fs.existsSync(baseDir))
|
|
68
|
-
return [];
|
|
69
|
-
const out = [];
|
|
70
|
-
for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
|
|
71
|
-
if (entry.isDirectory() && !entry.name.startsWith('.') && minimatch(entry.name, nameGlob)) {
|
|
72
|
-
out.push(path.join(baseDir, entry.name));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return out;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Best-effort addon identity for a packaged (npm) addon directory, derived
|
|
79
|
-
* from its package-directory name alone — no module import, matching the
|
|
80
|
-
* same boot-time-speed, no-module-loading design already accepted for the
|
|
81
|
-
* directory-scan case (see `ConfigurationManager.assertConfiguredAddonsExist`).
|
|
82
|
-
*
|
|
83
|
-
* Follows the documented packaged-addon naming convention
|
|
84
|
-
* (`@scope/<slug>-addon`, see docs/platform/deployment/addon-packaged.md):
|
|
85
|
-
* strips a trailing `-addon` suffix if present, else returns the folder name
|
|
86
|
-
* as-is.
|
|
87
|
-
*
|
|
88
|
-
* Caveat (same class as the existing directory-name heuristic): the addon's
|
|
89
|
-
* *real* identity is whatever its module exports as `name` at runtime. A
|
|
90
|
-
* package that doesn't follow the `-addon` naming convention, or whose
|
|
91
|
-
* module exports a different `name`, won't be matched precisely here — this
|
|
92
|
-
* is a validation-time approximation, not the source of truth.
|
|
93
|
-
*/
|
|
94
|
-
export function deriveAddonSlugFromPackageDirName(dirName) {
|
|
95
|
-
const suffix = '-addon';
|
|
96
|
-
return dirName.endsWith(suffix) ? dirName.slice(0, -suffix.length) : dirName;
|
|
97
|
-
}
|
|
98
45
|
/**
|
|
99
46
|
* Canonical identity for an addon directory (#927), resolved the SAME
|
|
100
47
|
* import-free way in every layer (discovery, `isEnabled`, the boot-time
|
|
101
|
-
* validator)
|
|
102
|
-
* config key, manifest slug) can no longer drift:
|
|
48
|
+
* validator):
|
|
103
49
|
*
|
|
104
|
-
* canonicalId = package.json `ngdpbase.slug` ??
|
|
50
|
+
* canonicalId = package.json `ngdpbase.slug` ?? the folder name verbatim
|
|
105
51
|
*
|
|
106
|
-
* Reads `package.json` statically — never imports the module
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* label validated against this at load time, not a source of identity.
|
|
52
|
+
* Reads `package.json` statically — never imports the module. The module's
|
|
53
|
+
* exported `name` is a display label validated against this at load time,
|
|
54
|
+
* not a source of identity.
|
|
110
55
|
*/
|
|
111
|
-
export function resolveAddonSlug(addonDir
|
|
56
|
+
export function resolveAddonSlug(addonDir) {
|
|
112
57
|
const dirName = path.basename(addonDir);
|
|
113
58
|
const pkgPath = path.join(addonDir, 'package.json');
|
|
114
59
|
try {
|
|
@@ -118,12 +63,7 @@ export function resolveAddonSlug(addonDir, source) {
|
|
|
118
63
|
return slug.trim();
|
|
119
64
|
}
|
|
120
65
|
catch {
|
|
121
|
-
/* no/invalid package.json — fall back to the folder
|
|
66
|
+
/* no/invalid package.json — fall back to the folder name */
|
|
122
67
|
}
|
|
123
|
-
|
|
124
|
-
// `@scope/<slug>-addon` publishing convention, so strip the conventional
|
|
125
|
-
// trailing `-addon`. A plain directory (bundled/drop-in) addon's folder
|
|
126
|
-
// name IS its identity verbatim — an addon may legitimately be named
|
|
127
|
-
// `something-addon` on disk — so never strip there.
|
|
128
|
-
return source === 'npm' ? deriveAddonSlugFromPackageDirName(dirName) : dirName;
|
|
68
|
+
return dirName;
|
|
129
69
|
}
|
|
@@ -58,6 +58,8 @@ export declare const AUDIT_EVENT: {
|
|
|
58
58
|
readonly EMAILCHANGE_CONFIRM: "emailchange-confirm";
|
|
59
59
|
/** A user asked to change their own email: a confirmation link was sent to the new address (#2073). Names only, never an address */
|
|
60
60
|
readonly EMAILCHANGE_REQUEST: "emailchange-request";
|
|
61
|
+
/** Executive Hub read; not recorded, read volume */
|
|
62
|
+
readonly EXECUTIVE_READ: "executive-read";
|
|
61
63
|
/** A page's footnote list changed — action add, import, transfer, update or delete; page content written on someone's behalf (#1233) */
|
|
62
64
|
readonly FOOTNOTE_EDIT: "footnote-edit";
|
|
63
65
|
/** Identity record or organization created, changed, deleted or linked to an account (recorded as contact-manage before #2038); metadata names the kind (person or organization), the operation and the fields changed, never their values */
|
|
@@ -65,6 +65,8 @@ export const AUDIT_EVENT = {
|
|
|
65
65
|
EMAILCHANGE_CONFIRM: 'emailchange-confirm',
|
|
66
66
|
/** A user asked to change their own email: a confirmation link was sent to the new address (#2073). Names only, never an address */
|
|
67
67
|
EMAILCHANGE_REQUEST: 'emailchange-request',
|
|
68
|
+
/** Executive Hub read; not recorded, read volume */
|
|
69
|
+
EXECUTIVE_READ: 'executive-read',
|
|
68
70
|
/** A page's footnote list changed — action add, import, transfer, update or delete; page content written on someone's behalf (#1233) */
|
|
69
71
|
FOOTNOTE_EDIT: 'footnote-edit',
|
|
70
72
|
/** Identity record or organization created, changed, deleted or linked to an account (recorded as contact-manage before #2038); metadata names the kind (person or organization), the operation and the fields changed, never their values */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jwilleke/ngdpbase-types",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.38.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",
|
|
@@ -41,15 +41,14 @@
|
|
|
41
41
|
"@types/multer": "^2.3.0",
|
|
42
42
|
"@types/node": "^24.19.2",
|
|
43
43
|
"better-sqlite3-multiple-ciphers": "^13.0.3",
|
|
44
|
-
"dotenv": "^18.0.
|
|
44
|
+
"dotenv": "^18.0.9",
|
|
45
45
|
"express": "^5.3.0",
|
|
46
46
|
"express-session": "^1.19.0",
|
|
47
47
|
"fs-extra": "^11.4.1",
|
|
48
48
|
"ipaddr.js": "^2.5.0",
|
|
49
|
-
"jose": "^6.2.
|
|
50
|
-
"linkedom": "^0.18.
|
|
49
|
+
"jose": "^6.2.13",
|
|
50
|
+
"linkedom": "^0.18.14",
|
|
51
51
|
"markdown-it": "^14.3.3",
|
|
52
|
-
"minimatch": "^10.2.6",
|
|
53
52
|
"minisearch": "7.2.0",
|
|
54
53
|
"multer": "^2.4.0",
|
|
55
54
|
"rrule-temporal": "2.2.8",
|