@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.
@@ -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: '', source: 'directory', enabled: true, defaults }], {});
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
- * The card's live stats are sourced from the add-on's existing status() method.
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
- /** URL of the add-on's admin page */
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. Shared by directory scans and npm
478
- * (#673) discovery; `sourceLabel` is for logging only. `source` selects the
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 all registered dashboard cards in registration order.
844
- * Called by the admin dashboard route to populate the addon cards row.
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
- getDashboardCards(): AddonDashboardCard[];
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` entry — either a directory (bundled/drop-in) or, per
145
- * #673/#924, an npm package matched by a `node_modules:<glob>` entry.
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 directory-vs-npm-pattern split, npm glob
150
- * matching, AND the canonical-identity resolution are all shared with
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: they had, and packaged addons could
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 — directories and `node_modules:` patterns — so bundled
22
- * and external addons are treated alike. Only an addon whose
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 — directories and `node_modules:` patterns — so bundled
22
- * and external addons are treated alike. Only an addon whose
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, findNodeModulesDir, installedAddonsDir, matchNpmPackageDirs, resolveAddonSlug, splitAddonsPath } from './addonsPathResolver.js';
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, npmPatterns } = splitAddonsPath(base[ADDONS_PATH_KEY] ?? './addons');
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), source: 'directory' });
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, source: 'directory' });
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, source } of discoverAddonDirs(base, cwd, dataFolder)) {
126
- const slug = resolveAddonSlug(dir, source);
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, source, enabled, defaults });
132
+ out.push({ slug, dir, enabled, defaults });
140
133
  }
141
134
  catch (err) {
142
- out.push({ slug, dir, source, enabled, defaults: {}, error: err instanceof Error ? err.message : String(err) });
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
- * #924: `AddonsManager` (real runtime discovery) and `ConfigurationManager`
6
- * (the #672 boot-time "does this enabled addon exist" safety check) used to
7
- * each carry their own copy of this logic. They drifted: `AddonsManager` was
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
- /** npm package name globs (the part after `node_modules:`), e.g. `@jwilleke/*-addon`. */
21
- npmPatterns: string[];
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 vs. `node_modules:<glob>` npm patterns.
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) so the four historical identities (folder name, module `name`,
67
- * config key, manifest slug) can no longer drift:
37
+ * validator):
68
38
  *
69
- * canonicalId = package.json `ngdpbase.slug` ?? deriveAddonSlugFromPackageDirName(folder)
39
+ * canonicalId = package.json `ngdpbase.slug` ?? the folder name verbatim
70
40
  *
71
- * Reads `package.json` statically — never imports the module — which is what
72
- * lets the boot validator compute the exact id the runtime will register
73
- * under, instead of guessing. The module's exported `name` is a display
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, source: 'directory' | 'npm'): 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
- * #924: `AddonsManager` (real runtime discovery) and `ConfigurationManager`
6
- * (the #672 boot-time "does this enabled addon exist" safety check) used to
7
- * each carry their own copy of this logic. They drifted: `AddonsManager` was
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
- import { minimatch } from 'minimatch';
18
- /** Prefix marking an addons-path entry as an npm-package glob rather than a directory. */
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 vs. `node_modules:<glob>` npm patterns.
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(NPM_ADDON_PREFIX)),
28
- npmPatterns: entries
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) so the four historical identities (folder name, module `name`,
102
- * config key, manifest slug) can no longer drift:
48
+ * validator):
103
49
  *
104
- * canonicalId = package.json `ngdpbase.slug` ?? deriveAddonSlugFromPackageDirName(folder)
50
+ * canonicalId = package.json `ngdpbase.slug` ?? the folder name verbatim
105
51
  *
106
- * Reads `package.json` statically — never imports the module — which is what
107
- * lets the boot validator compute the exact id the runtime will register
108
- * under, instead of guessing. The module's exported `name` is a display
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, source) {
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-derived id */
66
+ /* no/invalid package.json — fall back to the folder name */
122
67
  }
123
- // Fallback when no slug is declared. npm packages follow the
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.36.0",
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.8",
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.12",
50
- "linkedom": "^0.18.13",
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",