@jwilleke/ngdpbase-types 5.24.0 → 5.25.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/README.md CHANGED
@@ -12,6 +12,6 @@ import { buildAddonHost, openAddonTestDatabase, toCsv, formatters } from '@jwill
12
12
  ```
13
13
 
14
14
  - The main entry is types only. Importing it at run time fails, on purpose.
15
- - `/testing` is for tests. `buildAddonHost(slug, defaults)` is the real host. `openAddonTestDatabase(owner, { migrations, ledgerTable })` is the handle `openAddonDatabase` returns, in memory. `toCsv` is `ExportManager.toCsv`. `formatters` is a plugin's `context.formatters`.
15
+ - `/testing` is for tests. `buildAddonHost(slug, defaults, setEnabledForPerson?, engine?)` is the real host; `host.setEnabledForPerson` calls the setter you pass (it refuses without one), and `engine` names the system principal `host.systemContext(reason)` uses (core's fallback without one). `openAddonTestDatabase(owner, { migrations, ledgerTable })` is the handle `openAddonDatabase` returns, in memory. `toCsv` is `ExportManager.toCsv`. `formatters` is a plugin's `context.formatters`.
16
16
 
17
17
  The add-on guide is [docs/guides/addons-developer-guide.md](https://github.com/jwilleke/ngdpbase/blob/master/docs/guides/addons-developer-guide.md).
@@ -14,8 +14,8 @@
14
14
  * `@jwilleke/ngdpbase-types/testing`. A test holds this file to `export type`
15
15
  * only, so the published entry carries no JavaScript.
16
16
  */
17
- export type { AddonModule } from '../../src/managers/AddonsManager.js';
18
- export type { AddonHost } from '../../src/utils/addonHost.js';
17
+ export type { AddonModule, AddonManifest, AddonListing } from '../../src/managers/AddonsManager.js';
18
+ export type { AddonHost, PersonSwitchSetter } from '../../src/utils/addonHost.js';
19
19
  export type { WikiEngine } from '../../src/types/WikiEngine.js';
20
20
  export type { ApiContext } from '../../src/context/ApiContext.js';
21
21
  export type { ActorContext } from '../../src/context/ActorContext.js';
@@ -4,7 +4,7 @@
4
4
  import ExportManager from '../../src/managers/ExportManager.js';
5
5
  import { type AddonDatabaseOptions } from '../../src/managers/DatabaseManager.js';
6
6
  import { type SqliteHandle } from '../../src/providers/SqliteDatabaseProvider.js';
7
- import { type AddonHost } from '../../src/utils/addonHost.js';
7
+ import { buildAddonHost as buildHost, type AddonHost, type PersonSwitchSetter } from '../../src/utils/addonHost.js';
8
8
  export { formatters } from '../../src/managers/PluginManager.js';
9
9
  /**
10
10
  * Core's `buildAddonHost`, with the audit registry bound to this add-on's
@@ -15,8 +15,13 @@ export { formatters } from '../../src/managers/PluginManager.js';
15
15
  *
16
16
  * @param slug - The add-on's canonical slug
17
17
  * @param addonDefaults - The add-on's own `config/default-config.json`
18
+ * @param setEnabledForPerson - What `host.setEnabledForPerson` does in the
19
+ * test (#1963); a site binds core's setter. Without one it refuses.
20
+ * @param engine - Optional: an engine (or stand-in) whose PolicyInformationPoint
21
+ * names the system principal for `host.systemContext`; without one it is
22
+ * core's fallback principal
18
23
  */
19
- export declare function buildAddonHost(slug: string, addonDefaults: Record<string, unknown>): AddonHost;
24
+ export declare function buildAddonHost(slug: string, addonDefaults: Record<string, unknown>, setEnabledForPerson?: PersonSwitchSetter, engine?: Parameters<typeof buildHost>[3]): AddonHost;
20
25
  /** The add-on whose declaration a test database is checked against: its slug and its own `config/default-config.json`. */
21
26
  export interface AddonTestDatabaseDeclarer {
22
27
  slug: string;
@@ -14,10 +14,15 @@ export { formatters } from '../../src/managers/PluginManager.js';
14
14
  *
15
15
  * @param slug - The add-on's canonical slug
16
16
  * @param addonDefaults - The add-on's own `config/default-config.json`
17
+ * @param setEnabledForPerson - What `host.setEnabledForPerson` does in the
18
+ * test (#1963); a site binds core's setter. Without one it refuses.
19
+ * @param engine - Optional: an engine (or stand-in) whose PolicyInformationPoint
20
+ * names the system principal for `host.systemContext`; without one it is
21
+ * core's fallback principal
17
22
  */
18
- export function buildAddonHost(slug, addonDefaults) {
23
+ export function buildAddonHost(slug, addonDefaults, setEnabledForPerson, engine) {
19
24
  bindAuditEvents((key, fallback) => addonDefaults[key] ?? fallback);
20
- return buildHost(slug, addonDefaults);
25
+ return buildHost(slug, addonDefaults, setEnabledForPerson, engine);
21
26
  }
22
27
  /**
23
28
  * An add-on's database as `DatabaseManager.openAddonDatabase` returns it, in
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * @see {@link https://github.com/jwilleke/ngdpbase/issues/158}
12
12
  */
13
- import type { ActorContext } from '../context/ActorContext.js';
13
+ import { type ActorContext } from '../context/ActorContext.js';
14
14
  import BaseManager from './BaseManager.js';
15
15
  import type { BackupData } from './BaseManager.js';
16
16
  import type { WikiEngine } from '../types/WikiEngine.js';
@@ -208,7 +208,49 @@ export interface AddonManifest {
208
208
  * docs/guides/addons-developer-guide.md "Declare a Private Store".
209
209
  */
210
210
  stores?: unknown;
211
+ /**
212
+ * #1963: each person turns this add-on on or off for themselves. Their
213
+ * switch is the preference `<slug>.enabled` (`personSwitchKey`), read only
214
+ * by `AddonsManager.enabledAddonsFor` and written only through
215
+ * `host.setEnabledForPerson`. Absent or anything but `true`: not per-person.
216
+ */
217
+ perPerson?: boolean;
218
+ /**
219
+ * #1965: the add-on's About page, a page it ships in `pages/`, by uuid or
220
+ * slug. `[{ConfigAccessor type='addons'}]` links to it ("More about it").
221
+ * The site overrides it with `ngdpbase.addons.<slug>.about-page`.
222
+ */
223
+ aboutPage?: string;
224
+ /**
225
+ * #1965: the heading the add-on is listed under, shared by related add-ons
226
+ * (e.g. "Accounting" for accounting, accounting-donations and accounting-dues).
227
+ */
228
+ family?: string;
229
+ /** #1965: `false` keeps plumbing (e.g. elasticsearch) out of the add-on listings. */
230
+ listed?: boolean;
231
+ }
232
+ /**
233
+ * One add-on as the listings show it (#1965), from `AddonsManager.listedAddons()`.
234
+ * `version`, `loaded` and `error` are for administrators; the plugin that
235
+ * renders a listing decides who sees them.
236
+ */
237
+ export interface AddonListing {
238
+ slug: string;
239
+ description: string;
240
+ /** The heading it is grouped under, or null */
241
+ family: string | null;
242
+ /** The About page (uuid or slug), the site's override first; null when none */
243
+ aboutPage: string | null;
244
+ perPerson: boolean;
245
+ version: string;
246
+ loaded: boolean;
247
+ error: string | null;
211
248
  }
249
+ /**
250
+ * The preference that holds a person's switch for a per-person add-on (#1963):
251
+ * `<slug>.enabled`. The one place the key is spelled.
252
+ */
253
+ export declare function personSwitchKey(slug: string): string;
212
254
  /**
213
255
  * Details returned by add-on's status() method
214
256
  */
@@ -262,6 +304,7 @@ export interface AddonStatus {
262
304
  * AddonsManager - Manages optional add-on modules
263
305
  */
264
306
  declare class AddonsManager extends BaseManager {
307
+ #private;
265
308
  /** Map of discovered add-ons by name */
266
309
  private addons;
267
310
  /** Configured paths to addons directories (one or more) */
@@ -282,6 +325,8 @@ declare class AddonsManager extends BaseManager {
282
325
  private domainAddonName;
283
326
  /** #1883: the enabled add-ons that did not load this start, with why — each reported once. */
284
327
  private readonly loadFailures;
328
+ /** #1973: every enabled add-on has had its turn to load this boot (see {@link addonsLoaded}). */
329
+ private loadPassDone;
285
330
  constructor(engine: WikiEngine);
286
331
  /**
287
332
  * Initialize the AddonsManager
@@ -405,6 +450,20 @@ declare class AddonsManager extends BaseManager {
405
450
  ok: false;
406
451
  blockedBy: string[];
407
452
  };
453
+ /**
454
+ * The installed version of an add-on (#1973): the `version` in its
455
+ * package.json, or its module's `version` when the package.json has none.
456
+ * Undefined for an add-on that was not discovered.
457
+ *
458
+ * @param slug - The add-on's slug
459
+ */
460
+ getAddonVersion(slug: string): string | undefined;
461
+ /**
462
+ * Whether this boot's load pass is over (#1973): every enabled add-on has
463
+ * had its `register()` called, loaded or not. Until then, what an add-on has
464
+ * not registered yet says nothing about whether it still provides it.
465
+ */
466
+ addonsLoaded(): boolean;
408
467
  /**
409
468
  * Get add-on specific configuration
410
469
  */
@@ -614,6 +673,28 @@ declare class AddonsManager extends BaseManager {
614
673
  * other addons' saves or the core-preferences save.
615
674
  */
616
675
  saveProfileSections(ctx: ActorContext, body: Record<string, unknown>): Promise<void>;
676
+ /**
677
+ * The add-ons the site's listings show (#1965): enabled, and not
678
+ * `listed: false` in their manifest, in load order. Cheap: it calls no
679
+ * add-on's status(), so a page can render it.
680
+ */
681
+ listedAddons(): AddonListing[];
682
+ /** #1963: the loaded add-ons whose manifest says `perPerson: true`, in load order. */
683
+ private perPersonAddons;
684
+ /**
685
+ * The per-person add-ons this subject has turned on (#1963): each loaded
686
+ * add-on whose manifest says `perPerson: true` and whose switch, the
687
+ * person's preference `<slug>.enabled`, is `true`.
688
+ *
689
+ * It reads exactly those keys and no other preference, so no access check
690
+ * or listing built on it can come to depend on an arbitrary setting. An
691
+ * anonymous subject, an unknown or inactive account, and the system
692
+ * principal (which has no account) have none.
693
+ *
694
+ * @param ctx - Whose add-ons: the request's subject, or a job's context
695
+ * @returns The slugs, in load order
696
+ */
697
+ enabledAddonsFor(ctx: ActorContext): Promise<string[]>;
617
698
  /**
618
699
  * Get count of loaded add-ons
619
700
  */
@@ -1,4 +1,5 @@
1
1
  import BaseManager from '../managers/BaseManager.js';
2
+ import type { ActorContext } from '../context/ActorContext.js';
2
3
  import { WikiEngine } from '../types/WikiEngine.js';
3
4
  import type { AgentTokenGrant, ProxyClaim } from '../managers/UserManager.js';
4
5
  import type { Request } from 'express';
@@ -201,6 +202,13 @@ declare class PolicyInformationPoint extends BaseManager {
201
202
  * session at all, so the session path alone would still have said Anonymous.
202
203
  */
203
204
  currentSubject(req: Request): Promise<UserContext>;
205
+ /**
206
+ * The subject's per-person add-ons that are turned on (#1963): an attribute
207
+ * the PIP supplies to a decision (the page audience `addon:<slug>`, #1964)
208
+ * the way it supplies roles from RoleManager. AddonsManager owns the answer;
209
+ * none when it is not running.
210
+ */
211
+ subjectEnabledAddons(ctx: ActorContext): Promise<string[]>;
204
212
  private userManager;
205
213
  private roleManager;
206
214
  /**
@@ -10,6 +10,11 @@
10
10
  * - `recordAuditEvent` and `addonAuditEventName` accept only the audit events
11
11
  * this add-on declares in its own `config/default-config.json`, so one
12
12
  * add-on cannot write records that look like another's or like core's;
13
+ * - `systemContext(reason)` is bound to the running engine, so the system
14
+ * principal's name comes from core (`NGDPBASE_SYSTEM_USER`), never from
15
+ * the add-on (#1973);
16
+ * - `setEnabledForPerson` sets this add-on's own per-person switch (#1963),
17
+ * bound to its slug, so it can never set another add-on's;
13
18
  * - the object is `Object.freeze`d, so no add-on can replace a value.
14
19
  *
15
20
  * Additive only: nothing here is removed or renamed without a major release.
@@ -17,8 +22,9 @@
17
22
  */
18
23
  import type { Logger } from 'winston';
19
24
  import { ApiContext, ApiError, sendApiError } from '../context/ApiContext.js';
20
- import { actorOf } from '../context/ActorContext.js';
21
- import { jobContextFromRequest, jobContextFromSystem } from '../context/JobContext.js';
25
+ import { actorOf, type ActorContext } from '../context/ActorContext.js';
26
+ import { jobContextFromRequest, jobContextFromSystem, type JobContext } from '../context/JobContext.js';
27
+ import { systemContext } from '../context/bootActions.js';
22
28
  import { guardedFetch } from '../http/guardedFetch.js';
23
29
  import { recordAuditEvent } from './auditEvents.js';
24
30
  import { addonAuditEventName } from './auditEventNames.js';
@@ -32,9 +38,25 @@ export interface AddonHost {
32
38
  readonly actorOf: typeof actorOf;
33
39
  readonly jobContextFromRequest: typeof jobContextFromRequest;
34
40
  readonly jobContextFromSystem: typeof jobContextFromSystem;
41
+ /**
42
+ * The context the add-on's own boot work runs under (#1973): core's system
43
+ * principal, with `reason` saying what and why, for the record. Pass it
44
+ * where a manager door asks for a context at start, such as
45
+ * `FormsDataManager.registerShippedForm`.
46
+ */
47
+ readonly systemContext: (reason: string) => JobContext;
35
48
  readonly logger: Logger;
36
49
  readonly guardedFetch: typeof guardedFetch;
50
+ /**
51
+ * #1963: turn this per-person add-on on (`true`) or off (`false`) for the
52
+ * person `ctx` acts as: their preference `<slug>.enabled`. Pass the context
53
+ * you were given (a profile hook's `ctx`). Refused when the add-on's manifest
54
+ * does not say `perPerson: true`, or the person may not manage their profile.
55
+ */
56
+ readonly setEnabledForPerson: PersonSwitchSetter;
37
57
  }
58
+ /** Sets one add-on's per-person switch; core binds the slug when it builds the host (#1963). */
59
+ export type PersonSwitchSetter = (ctx: ActorContext, on: boolean) => Promise<void>;
38
60
  /**
39
61
  * The audit event names one add-on declares: the permission entries carrying
40
62
  * an `audit` block, and the `ngdpbase.audit.events` entries, in that add-on's
@@ -47,5 +69,9 @@ export declare function addonDeclaredAuditEvents(addonDefaults: Record<string, u
47
69
  * @param slug - The add-on's canonical slug
48
70
  * @param addonDefaults - That add-on's own default-config.json, as folded into
49
71
  * the configuration merge (`ConfigurationManager.getAddonDeclaredDefaults`)
72
+ * @param setEnabledForPerson - This add-on's switch setter, slug already bound
73
+ * (`AddonsManager` supplies it); without one, the host's setter refuses
74
+ * @param engine - The running engine, which names the system principal for
75
+ * `systemContext`; without one (a test) it is core's fallback principal
50
76
  */
51
- export declare function buildAddonHost(slug: string, addonDefaults: Record<string, unknown>): AddonHost;
77
+ export declare function buildAddonHost(slug: string, addonDefaults: Record<string, unknown>, setEnabledForPerson?: PersonSwitchSetter, engine?: Parameters<typeof systemContext>[0]): AddonHost;
@@ -10,6 +10,11 @@
10
10
  * - `recordAuditEvent` and `addonAuditEventName` accept only the audit events
11
11
  * this add-on declares in its own `config/default-config.json`, so one
12
12
  * add-on cannot write records that look like another's or like core's;
13
+ * - `systemContext(reason)` is bound to the running engine, so the system
14
+ * principal's name comes from core (`NGDPBASE_SYSTEM_USER`), never from
15
+ * the add-on (#1973);
16
+ * - `setEnabledForPerson` sets this add-on's own per-person switch (#1963),
17
+ * bound to its slug, so it can never set another add-on's;
13
18
  * - the object is `Object.freeze`d, so no add-on can replace a value.
14
19
  *
15
20
  * Additive only: nothing here is removed or renamed without a major release.
@@ -18,11 +23,14 @@
18
23
  import { ApiContext, ApiError, sendApiError } from '../context/ApiContext.js';
19
24
  import { actorOf } from '../context/ActorContext.js';
20
25
  import { jobContextFromRequest, jobContextFromSystem } from '../context/JobContext.js';
26
+ import { systemContext } from '../context/bootActions.js';
21
27
  import { guardedFetch } from '../http/guardedFetch.js';
22
28
  import { recordAuditEvent } from './auditEvents.js';
23
29
  import { addonAuditEventName } from './auditEventNames.js';
24
30
  import { auditDeclarationsFrom } from './auditRegistry.js';
25
31
  import logger from './logger.js';
32
+ /** A host built outside a site (the test kit, with no setter given) has no account store. */
33
+ const noPersonSwitch = () => Promise.reject(new Error('setEnabledForPerson is not available: this host was built without a site'));
26
34
  /**
27
35
  * The audit event names one add-on declares: the permission entries carrying
28
36
  * an `audit` block, and the `ngdpbase.audit.events` entries, in that add-on's
@@ -45,8 +53,12 @@ function refuseUndeclared(slug, name, declared) {
45
53
  * @param slug - The add-on's canonical slug
46
54
  * @param addonDefaults - That add-on's own default-config.json, as folded into
47
55
  * the configuration merge (`ConfigurationManager.getAddonDeclaredDefaults`)
56
+ * @param setEnabledForPerson - This add-on's switch setter, slug already bound
57
+ * (`AddonsManager` supplies it); without one, the host's setter refuses
58
+ * @param engine - The running engine, which names the system principal for
59
+ * `systemContext`; without one (a test) it is core's fallback principal
48
60
  */
49
- export function buildAddonHost(slug, addonDefaults) {
61
+ export function buildAddonHost(slug, addonDefaults, setEnabledForPerson = noPersonSwitch, engine) {
50
62
  const declared = addonDeclaredAuditEvents(addonDefaults);
51
63
  const hostAddonAuditEventName = (name) => {
52
64
  const checked = addonAuditEventName(name);
@@ -66,7 +78,9 @@ export function buildAddonHost(slug, addonDefaults) {
66
78
  actorOf,
67
79
  jobContextFromRequest,
68
80
  jobContextFromSystem,
81
+ systemContext: (reason) => systemContext(engine, reason),
69
82
  logger: logger.child({ addon: slug }),
70
- guardedFetch
83
+ guardedFetch,
84
+ setEnabledForPerson
71
85
  });
72
86
  }
@@ -34,8 +34,8 @@ export declare function evaluateSeededAddonPage(args: {
34
34
  /**
35
35
  * How a shipped item's sync status reads on an admin screen: the status words
36
36
  * and the edit protection of Admin → Required Pages, which every screen that
37
- * syncs shipped content shares (`views/_sync-status.ejs`, `views/_sync-button.ejs`;
38
- * shipped forms, #1918).
37
+ * syncs shipped content shares (`views/_sync-status.ejs`, `views/_sync-button.ejs`,
38
+ * #1918).
39
39
  *
40
40
  * - `status` — `modified` when the live copy differs from what ships,
41
41
  * `current` when it is the same.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwilleke/ngdpbase-types",
3
- "version": "5.24.0",
3
+ "version": "5.25.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",