@jwilleke/ngdpbase-types 5.24.0 → 5.26.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 +2 -2
- package/lib/packages/ngdpbase-types/index.d.ts +6 -2
- package/lib/packages/ngdpbase-types/testing.d.ts +7 -2
- package/lib/packages/ngdpbase-types/testing.js +7 -2
- package/lib/src/http/egressPolicy.js +61 -0
- package/lib/src/http/guardedElasticsearch.d.ts +27 -0
- package/lib/src/http/guardedElasticsearch.js +99 -0
- package/lib/src/managers/AddonsManager.d.ts +119 -2
- package/lib/src/managers/AttachmentManager.d.ts +4 -5
- package/lib/src/managers/AuthManager.d.ts +150 -32
- package/lib/src/managers/BackgroundJobManager.d.ts +22 -12
- package/lib/src/managers/BackupManager.d.ts +779 -0
- package/lib/src/managers/BackupManager.js +2042 -0
- package/lib/src/managers/BaseManager.d.ts +156 -6
- package/lib/src/managers/BaseManager.js +64 -2
- package/lib/src/managers/CommentManager.d.ts +6 -6
- package/lib/src/managers/ConsentManager.d.ts +130 -0
- package/lib/src/managers/DatabaseManager.d.ts +6 -0
- package/lib/src/managers/DatabaseManager.js +25 -15
- package/lib/src/managers/FootnoteManager.d.ts +6 -6
- package/lib/src/managers/OrganizationManager.d.ts +8 -6
- package/lib/src/managers/PageManager.d.ts +16 -0
- package/lib/src/managers/PersonManager.d.ts +8 -6
- package/lib/src/managers/RoleManager.d.ts +6 -8
- package/lib/src/providers/BaseAttachmentProvider.d.ts +4 -4
- package/lib/src/providers/BaseBackupProvider.d.ts +77 -0
- package/lib/src/providers/BaseBackupProvider.js +52 -0
- package/lib/src/providers/BasicAttachmentProvider.d.ts +4 -4
- package/lib/src/providers/FileBackupProvider.d.ts +34 -0
- package/lib/src/providers/FileBackupProvider.js +116 -0
- package/lib/src/providers/FileConsentProvider.d.ts +18 -0
- package/lib/src/providers/SqliteDatabaseProvider.d.ts +2 -2
- package/lib/src/providers/SqliteDatabaseProvider.js +12 -11
- package/lib/src/providers/attachmentFilesBackup.d.ts +5 -4
- package/lib/src/providers/databaseMigrations.js +45 -0
- package/lib/src/security/PolicyDecisionPoint.d.ts +14 -0
- package/lib/src/security/PolicyInformationPoint.d.ts +8 -0
- package/lib/src/types/Consent.d.ts +90 -0
- package/lib/src/types/express.d.ts +6 -0
- package/lib/src/utils/addonHost.d.ts +51 -3
- package/lib/src/utils/addonHost.js +29 -2
- package/lib/src/utils/addonPageSync.d.ts +2 -2
- package/lib/src/utils/auditEventNames.generated.d.ts +20 -0
- package/lib/src/utils/auditEventNames.generated.js +20 -0
- package/lib/src/utils/backupCrypto.js +275 -0
- package/lib/src/utils/backupDocument.d.ts +36 -0
- package/lib/src/utils/backupDocument.js +99 -0
- package/lib/src/utils/browserDescription.d.ts +11 -0
- package/lib/src/utils/directoryBackup.d.ts +48 -48
- package/lib/src/utils/directoryBackup.js +291 -0
- package/lib/src/utils/instanceEnvBackup.d.ts +46 -0
- package/lib/src/utils/instanceEnvBackup.js +120 -0
- package/lib/src/utils/instanceEnvSecret.js +79 -0
- package/lib/src/utils/ipLocation.d.ts +11 -0
- package/lib/src/utils/schedule.js +171 -0
- package/lib/src/utils/secretFileMode.js +78 -0
- package/lib/src/utils/siteRestore.d.ts +13 -16
- package/lib/src/utils/siteRestore.js +33 -6
- package/lib/src/utils/treeArchive.d.ts +2 -0
- package/lib/src/utils/treeArchive.js +242 -0
- package/lib/src/utils/version.js +408 -0
- package/package.json +6 -1
- package/lib/src/utils/administrators.d.ts +0 -21
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ import type { AddonModule, AddonHost, WikiEngine } from '@jwilleke/ngdpbase-type
|
|
|
11
11
|
import { buildAddonHost, openAddonTestDatabase, toCsv, formatters } from '@jwilleke/ngdpbase-types/testing';
|
|
12
12
|
```
|
|
13
13
|
|
|
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`.
|
|
14
|
+
- The main entry is types only. Importing it at run time fails, on purpose. An add-on takes core's values from `host` (`AddonHost`: the request and audit helpers, the logger, `guardedFetch`, the egress tools, `isAtLeast`) and a manager's job from `engine.getManager` (such as `PageManager.pageUrl` or `PolicyDecisionPoint.usersHolding`); it never imports core's values ([#1813](https://github.com/jwilleke/ngdpbase/issues/1813)).
|
|
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';
|
|
@@ -30,6 +30,10 @@ export type { Migration } from '../../src/providers/sqliteMigrations.js';
|
|
|
30
30
|
export type { AddonDatabaseDeclaration, DatabaseEncrypt } from '../../src/utils/addonDatabaseRules.js';
|
|
31
31
|
export type { default as ExportManager } from '../../src/managers/ExportManager.js';
|
|
32
32
|
export type { default as PolicyDecisionPoint } from '../../src/security/PolicyDecisionPoint.js';
|
|
33
|
+
export type { default as PageManager } from '../../src/managers/PageManager.js';
|
|
34
|
+
export type { PageAction } from '../../src/utils/pageUrl.js';
|
|
35
|
+
export type { default as ConsentManager, ConsentScopeStatus } from '../../src/managers/ConsentManager.js';
|
|
36
|
+
export type { AddonConsentRegistration, ConsentDocument, ConsentRecord, ConsentRequiredError, ConsentScope, ConsentSeen, ConsentStatus } from '../../src/types/Consent.js';
|
|
33
37
|
export type { default as PersonManager, LinkOfferOutcome } from '../../src/managers/PersonManager.js';
|
|
34
38
|
export type { default as OrganizationManager } from '../../src/managers/OrganizationManager.js';
|
|
35
39
|
export type { Person, PersonAffiliation, PersonInput, PersonUpdate } from '../../src/types/Person.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
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration to policy (#1133).
|
|
3
|
+
*
|
|
4
|
+
* The operator sets two CIDR lists; this turns them into the `EgressPolicy`
|
|
5
|
+
* the guard enforces, and reconciles them at BOOT so a contradiction is a
|
|
6
|
+
* startup message rather than one dropped image months later — which would
|
|
7
|
+
* surface as a `fetch` warning indistinguishable from the remote host being
|
|
8
|
+
* down.
|
|
9
|
+
*
|
|
10
|
+
* Contradictions resolve by FIREWALL CONVENTION and none of them stops the
|
|
11
|
+
* boot (#1144, D8). #1133 chose longest-prefix match — the routing rule,
|
|
12
|
+
* because the values are routes — so a general overlap is not a conflict at
|
|
13
|
+
* all. The cases longest prefix cannot decide are settled here: an allow entry
|
|
14
|
+
* reaching the mechanism is unsatisfiable and dropped, and a range in both
|
|
15
|
+
* lists is a prefix-length tie the DENY wins.
|
|
16
|
+
*
|
|
17
|
+
* `ngdpbase.security.profile` used to decide whether a contradiction stopped
|
|
18
|
+
* the boot. That was the profile looking for a job: `iptables` rejects a bad
|
|
19
|
+
* rule and keeps the chain, and the Kubernetes API server rejects an invalid
|
|
20
|
+
* NetworkPolicy while the others keep applying. Neither takes the workload
|
|
21
|
+
* down. The one case with no safe silent resolution — a malformed range, where
|
|
22
|
+
* dropping a DENY fails open — is reported separately for #1152.
|
|
23
|
+
*/
|
|
24
|
+
import { reconcilePolicy } from './ssrf.js';
|
|
25
|
+
export const DENIED_RANGES_KEY = 'ngdpbase.security.egress.denied-ranges';
|
|
26
|
+
export const ALLOWED_RANGES_KEY = 'ngdpbase.security.egress.allowed-ranges';
|
|
27
|
+
/** Only strings can be CIDRs; anything else in the list is discarded, not coerced. */
|
|
28
|
+
function stringList(value) {
|
|
29
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read the configured ranges, reconcile them, and report what could not be
|
|
33
|
+
* resolved.
|
|
34
|
+
*
|
|
35
|
+
* A contradiction never widens access. Where the two lists disagree the
|
|
36
|
+
* stricter reading wins, which is the only direction a security control should
|
|
37
|
+
* fail when nobody can be asked.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveEgressPolicy(read) {
|
|
40
|
+
const deniedRanges = stringList(read(DENIED_RANGES_KEY, []));
|
|
41
|
+
const allowedRanges = stringList(read(ALLOWED_RANGES_KEY, []));
|
|
42
|
+
const { conflicts, malformed, duplicates } = reconcilePolicy(deniedRanges, allowedRanges);
|
|
43
|
+
const unusable = new Set(malformed);
|
|
44
|
+
// An allow entry is dropped when it cannot be parsed, when it reaches the
|
|
45
|
+
// mechanism, or when the same range is also denied. The last is D8's tie
|
|
46
|
+
// break: equal prefix lengths give longest-prefix nothing to work with, so
|
|
47
|
+
// the deny wins.
|
|
48
|
+
const safeAllowed = allowedRanges.filter((range) => !unusable.has(range)
|
|
49
|
+
&& !duplicates.includes(range)
|
|
50
|
+
&& !conflicts.some((c) => c.startsWith('allowed-ranges:') && c.includes(`'${range}'`)));
|
|
51
|
+
// A denied range is dropped ONLY when it cannot be parsed. Previously a
|
|
52
|
+
// range appearing in both lists was removed from here too, so the operator's
|
|
53
|
+
// stricter statement was discarded along with the looser one and the range
|
|
54
|
+
// fell back to the built-in defaults.
|
|
55
|
+
const safeDenied = deniedRanges.filter((range) => !unusable.has(range));
|
|
56
|
+
return {
|
|
57
|
+
policy: { deniedRanges: safeDenied, allowedRanges: safeAllowed },
|
|
58
|
+
conflicts,
|
|
59
|
+
malformed
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Client, type ClientOptions } from '@elastic/elasticsearch';
|
|
2
|
+
import { type Resolver } from './guardedLookup.js';
|
|
3
|
+
import { type ConfigReader } from './egressPolicy.js';
|
|
4
|
+
import { type EgressPolicy } from './ssrf.js';
|
|
5
|
+
export interface GuardedElasticsearchOptions {
|
|
6
|
+
/** Per-request timeout in ms; the SDK's default when omitted. */
|
|
7
|
+
requestTimeout?: number;
|
|
8
|
+
/**
|
|
9
|
+
* The resolution step, for tests only — the same single seam `guardedLookup`
|
|
10
|
+
* exposes. Production leaves it unset and `node:dns` answers.
|
|
11
|
+
*/
|
|
12
|
+
resolver?: Resolver;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The options that make a client guarded. Exported so a test can assert the
|
|
16
|
+
* wiring — that the connection class is the `http` one and that its agents
|
|
17
|
+
* carry the guarded lookup and the guarded connect — without opening a socket.
|
|
18
|
+
*/
|
|
19
|
+
export declare function guardedClientOptions(node: string, policy: EgressPolicy, options?: GuardedElasticsearchOptions): ClientOptions;
|
|
20
|
+
/** An Elasticsearch client whose every connection is subject to the instance's egress policy. */
|
|
21
|
+
export declare function createGuardedElasticsearchClient(node: string, read: ConfigReader, options?: GuardedElasticsearchOptions): Client;
|
|
22
|
+
/**
|
|
23
|
+
* The boot-time line both call sites log when the configured node would be
|
|
24
|
+
* refused — one sentence, the same shape as sist2's (#1186), so the operator
|
|
25
|
+
* is sent to the egress configuration and not to the Elasticsearch host.
|
|
26
|
+
*/
|
|
27
|
+
export declare function refusedNodeMessage(who: string, node: string, reason: string): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Elasticsearch client, built inside the boundary (#1188).
|
|
3
|
+
*
|
|
4
|
+
* `@elastic/elasticsearch` opens sockets the same way `fetch` does, and until
|
|
5
|
+
* #1188 it did so from two places outside `src/http/` — the search provider
|
|
6
|
+
* and the elasticsearch addon — with `new Client({ node })` and no policy at
|
|
7
|
+
* all. The boundary check (#1139) could not see it because the SDK was not in
|
|
8
|
+
* its list of client modules, so the guard read green while the process
|
|
9
|
+
* connected to whatever `es-url` said, loopback and RFC1918 included.
|
|
10
|
+
*
|
|
11
|
+
* This is the only place the SDK is constructed. It hands the client Node's
|
|
12
|
+
* `http` connection class with agents whose `lookup` is the guarded one, so
|
|
13
|
+
* every socket the client opens — the first request, a retry, a sniffed node —
|
|
14
|
+
* is judged against the instance's egress policy at connect time, exactly as
|
|
15
|
+
* `guardedFetch` does. There is no ungated constructor to reach for: the
|
|
16
|
+
* scanner now names the SDK, so an `import { Client }` outside this directory
|
|
17
|
+
* fails `lint:http`.
|
|
18
|
+
*
|
|
19
|
+
* What this means for an operator: an Elasticsearch node on `localhost` is
|
|
20
|
+
* refused — loopback is mechanism, not policy (#1186), and no allow entry
|
|
21
|
+
* opens it. A node on the LAN needs its prefix in
|
|
22
|
+
* `ngdpbase.security.egress.allowed-ranges`, the same as sist2. Both call
|
|
23
|
+
* sites say so at boot when the configured address would be refused.
|
|
24
|
+
*/
|
|
25
|
+
import { Agent as HttpAgent } from 'node:http';
|
|
26
|
+
import { Agent as HttpsAgent } from 'node:https';
|
|
27
|
+
import { isIP } from 'node:net';
|
|
28
|
+
import { Client, HttpConnection } from '@elastic/elasticsearch';
|
|
29
|
+
import { ESSRF_BLOCKED, guardedLookup } from './guardedLookup.js';
|
|
30
|
+
import { resolveEgressPolicy } from './egressPolicy.js';
|
|
31
|
+
import { isAddressAllowed, REFUSAL } from './ssrf.js';
|
|
32
|
+
/**
|
|
33
|
+
* The connect step, guarding the case the lookup never sees.
|
|
34
|
+
*
|
|
35
|
+
* Node calls an agent's `lookup` only for a NAME. A URL whose host is an IP
|
|
36
|
+
* literal — `http://192.168.0.10:9200`, `http://127.0.0.1:9200` — goes
|
|
37
|
+
* straight to the socket, so a guard that lives only in `lookup` waves every
|
|
38
|
+
* literal address through. Found by pointing the first version of this at
|
|
39
|
+
* jimstest's LAN node under an empty policy and watching `ping` return true.
|
|
40
|
+
* `guardedFetch` covers the same hole with `validateUrl` before each hop;
|
|
41
|
+
* here the judgement sits in `createConnection`, which every socket passes
|
|
42
|
+
* through, literal or resolved.
|
|
43
|
+
*/
|
|
44
|
+
function guardedCreateConnection(policy, connect) {
|
|
45
|
+
return function guarded(options, callback) {
|
|
46
|
+
const host = String(options.host ?? options.hostname ?? 'localhost');
|
|
47
|
+
if (isIP(host) && !isAddressAllowed(host, policy)) {
|
|
48
|
+
const err = Object.assign(new Error(`${REFUSAL}: ${host}`), { code: ESSRF_BLOCKED });
|
|
49
|
+
if (callback) {
|
|
50
|
+
process.nextTick(callback, err);
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
// Node's own connect for this agent — `net.createConnection` for http,
|
|
56
|
+
// the TLS one for https. The agent has merged its options in, so `lookup`
|
|
57
|
+
// (the guarded one) rides along for a host that is a name.
|
|
58
|
+
return connect.call(this, options, callback);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The options that make a client guarded. Exported so a test can assert the
|
|
63
|
+
* wiring — that the connection class is the `http` one and that its agents
|
|
64
|
+
* carry the guarded lookup and the guarded connect — without opening a socket.
|
|
65
|
+
*/
|
|
66
|
+
export function guardedClientOptions(node, policy, options = {}) {
|
|
67
|
+
// keepAlive stays on, as the SDK's own default agent has it: a kept socket
|
|
68
|
+
// was judged when it was opened, and both guards run again for every new one.
|
|
69
|
+
const lookup = guardedLookup(policy, options.resolver);
|
|
70
|
+
const agents = { http: new HttpAgent({ lookup, keepAlive: true }), https: new HttpsAgent({ lookup, keepAlive: true }) };
|
|
71
|
+
// `createConnection` is a method of the agent, not an option it accepts:
|
|
72
|
+
// Node reads `this.createConnection` when it opens a socket. Replaced on the
|
|
73
|
+
// instance, wrapping the prototype's own so TLS setup stays Node's.
|
|
74
|
+
const method = (agent) => Object.getPrototypeOf(agent).createConnection;
|
|
75
|
+
agents.http.createConnection =
|
|
76
|
+
guardedCreateConnection(policy, method(agents.http));
|
|
77
|
+
agents.https.createConnection =
|
|
78
|
+
guardedCreateConnection(policy, method(agents.https));
|
|
79
|
+
return {
|
|
80
|
+
node,
|
|
81
|
+
...(options.requestTimeout !== undefined ? { requestTimeout: options.requestTimeout } : {}),
|
|
82
|
+
Connection: HttpConnection,
|
|
83
|
+
agent: (connection) => (connection.url.protocol === 'https:' ? agents.https : agents.http)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** An Elasticsearch client whose every connection is subject to the instance's egress policy. */
|
|
87
|
+
export function createGuardedElasticsearchClient(node, read, options = {}) {
|
|
88
|
+
return new Client(guardedClientOptions(node, resolveEgressPolicy(read).policy, options));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The boot-time line both call sites log when the configured node would be
|
|
92
|
+
* refused — one sentence, the same shape as sist2's (#1186), so the operator
|
|
93
|
+
* is sent to the egress configuration and not to the Elasticsearch host.
|
|
94
|
+
*/
|
|
95
|
+
export function refusedNodeMessage(who, node, reason) {
|
|
96
|
+
return (`[${who}] Elasticsearch node ${node} is refused by the egress policy (${reason}). ` +
|
|
97
|
+
'Loopback and link-local can never be opened; a LAN address needs its prefix in ' +
|
|
98
|
+
'ngdpbase.security.egress.allowed-ranges. Every request to it will fail. (#1188)');
|
|
99
|
+
}
|
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @see {@link https://github.com/jwilleke/ngdpbase/issues/158}
|
|
12
12
|
*/
|
|
13
|
-
import type
|
|
13
|
+
import { type ActorContext } from '../context/ActorContext.js';
|
|
14
|
+
import type { BackupFolder } from './BaseManager.js';
|
|
14
15
|
import BaseManager from './BaseManager.js';
|
|
15
16
|
import type { BackupData } from './BaseManager.js';
|
|
16
17
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
17
18
|
import type { User } from '../types/User.js';
|
|
18
19
|
import { type StoreOwnerState } from '../utils/privateStoreDoor.js';
|
|
19
20
|
import { type AddonHost } from '../utils/addonHost.js';
|
|
21
|
+
import { ConsentRequiredError } from '../types/Consent.js';
|
|
20
22
|
/**
|
|
21
23
|
* Type passed to `AddonModule.profileSection()` (#534). Matches what
|
|
22
24
|
* `UserManager.getUser()` returns at runtime — `User` minus the password
|
|
@@ -76,6 +78,14 @@ export interface AddonModule {
|
|
|
76
78
|
* frozen. An add-on that takes two arguments keeps working until it moves.
|
|
77
79
|
*/
|
|
78
80
|
register(engine: WikiEngine, config: Record<string, unknown>, host?: AddonHost): Promise<void> | void;
|
|
81
|
+
/**
|
|
82
|
+
* Folders it keeps data in besides its `dataPath` (#1985), given its
|
|
83
|
+
* configuration. Its `dataPath` is backed up and restored without being
|
|
84
|
+
* declared; an add-on declares only further folders, and writes no backup
|
|
85
|
+
* code. BackupManager archives each one, stages it in a site restore, and
|
|
86
|
+
* puts it in place before `register()` runs at the next start.
|
|
87
|
+
*/
|
|
88
|
+
backupFolders?(config: Record<string, unknown>): BackupFolder[];
|
|
79
89
|
/**
|
|
80
90
|
* Optional health check returning add-on status details.
|
|
81
91
|
*/
|
|
@@ -208,7 +218,49 @@ export interface AddonManifest {
|
|
|
208
218
|
* docs/guides/addons-developer-guide.md "Declare a Private Store".
|
|
209
219
|
*/
|
|
210
220
|
stores?: unknown;
|
|
221
|
+
/**
|
|
222
|
+
* #1963: each person turns this add-on on or off for themselves. Their
|
|
223
|
+
* switch is the preference `<slug>.enabled` (`personSwitchKey`), read only
|
|
224
|
+
* by `AddonsManager.enabledAddonsFor` and written only through
|
|
225
|
+
* `host.setEnabledForPerson`. Absent or anything but `true`: not per-person.
|
|
226
|
+
*/
|
|
227
|
+
perPerson?: boolean;
|
|
228
|
+
/**
|
|
229
|
+
* #1965: the add-on's About page, a page it ships in `pages/`, by uuid or
|
|
230
|
+
* slug. `[{ConfigAccessor type='addons'}]` links to it ("More about it").
|
|
231
|
+
* The site overrides it with `ngdpbase.addons.<slug>.about-page`.
|
|
232
|
+
*/
|
|
233
|
+
aboutPage?: string;
|
|
234
|
+
/**
|
|
235
|
+
* #1965: the heading the add-on is listed under, shared by related add-ons
|
|
236
|
+
* (e.g. "Accounting" for accounting, accounting-donations and accounting-dues).
|
|
237
|
+
*/
|
|
238
|
+
family?: string;
|
|
239
|
+
/** #1965: `false` keeps plumbing (e.g. elasticsearch) out of the add-on listings. */
|
|
240
|
+
listed?: boolean;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* One add-on as the listings show it (#1965), from `AddonsManager.listedAddons()`.
|
|
244
|
+
* `version`, `loaded` and `error` are for administrators; the plugin that
|
|
245
|
+
* renders a listing decides who sees them.
|
|
246
|
+
*/
|
|
247
|
+
export interface AddonListing {
|
|
248
|
+
slug: string;
|
|
249
|
+
description: string;
|
|
250
|
+
/** The heading it is grouped under, or null */
|
|
251
|
+
family: string | null;
|
|
252
|
+
/** The About page (uuid or slug), the site's override first; null when none */
|
|
253
|
+
aboutPage: string | null;
|
|
254
|
+
perPerson: boolean;
|
|
255
|
+
version: string;
|
|
256
|
+
loaded: boolean;
|
|
257
|
+
error: string | null;
|
|
211
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* The preference that holds a person's switch for a per-person add-on (#1963):
|
|
261
|
+
* `<slug>.enabled`. The one place the key is spelled.
|
|
262
|
+
*/
|
|
263
|
+
export declare function personSwitchKey(slug: string): string;
|
|
212
264
|
/**
|
|
213
265
|
* Details returned by add-on's status() method
|
|
214
266
|
*/
|
|
@@ -262,6 +314,7 @@ export interface AddonStatus {
|
|
|
262
314
|
* AddonsManager - Manages optional add-on modules
|
|
263
315
|
*/
|
|
264
316
|
declare class AddonsManager extends BaseManager {
|
|
317
|
+
#private;
|
|
265
318
|
/** Map of discovered add-ons by name */
|
|
266
319
|
private addons;
|
|
267
320
|
/** Configured paths to addons directories (one or more) */
|
|
@@ -282,6 +335,8 @@ declare class AddonsManager extends BaseManager {
|
|
|
282
335
|
private domainAddonName;
|
|
283
336
|
/** #1883: the enabled add-ons that did not load this start, with why — each reported once. */
|
|
284
337
|
private readonly loadFailures;
|
|
338
|
+
/** #1973: every enabled add-on has had its turn to load this boot (see {@link addonsLoaded}). */
|
|
339
|
+
private loadPassDone;
|
|
285
340
|
constructor(engine: WikiEngine);
|
|
286
341
|
/**
|
|
287
342
|
* Initialize the AddonsManager
|
|
@@ -405,6 +460,34 @@ declare class AddonsManager extends BaseManager {
|
|
|
405
460
|
ok: false;
|
|
406
461
|
blockedBy: string[];
|
|
407
462
|
};
|
|
463
|
+
/**
|
|
464
|
+
* The installed version of an add-on (#1973): the `version` in its
|
|
465
|
+
* package.json, or its module's `version` when the package.json has none.
|
|
466
|
+
* Undefined for an add-on that was not discovered.
|
|
467
|
+
*
|
|
468
|
+
* @param slug - The add-on's slug
|
|
469
|
+
*/
|
|
470
|
+
getAddonVersion(slug: string): string | undefined;
|
|
471
|
+
/**
|
|
472
|
+
* Whether this boot's load pass is over (#1973): every enabled add-on has
|
|
473
|
+
* had its `register()` called, loaded or not. Until then, what an add-on has
|
|
474
|
+
* not registered yet says nothing about whether it still provides it.
|
|
475
|
+
*/
|
|
476
|
+
addonsLoaded(): boolean;
|
|
477
|
+
/**
|
|
478
|
+
* An add-on's folders (#1985): its `dataPath` — `ngdpbase.addons.<slug>.dataPath`,
|
|
479
|
+
* or `<FAST_STORAGE>/<slug>` when that is not set, as the bundled add-ons
|
|
480
|
+
* read it — under the part name of its slug, then any further folders its
|
|
481
|
+
* module declares. A `dataPath` that is the instance data folder, or holds
|
|
482
|
+
* it, is never one: a restore would swap the whole site's data.
|
|
483
|
+
*/
|
|
484
|
+
private addonFolders;
|
|
485
|
+
/** Each loaded add-on's `dataPath` and declared folders (#1985), in load order: what BackupManager backs up for it. */
|
|
486
|
+
addonBackupFolders(): Array<{
|
|
487
|
+
slug: string;
|
|
488
|
+
dataPath: string | null;
|
|
489
|
+
folders: BackupFolder[];
|
|
490
|
+
}>;
|
|
408
491
|
/**
|
|
409
492
|
* Get add-on specific configuration
|
|
410
493
|
*/
|
|
@@ -612,8 +695,42 @@ declare class AddonsManager extends BaseManager {
|
|
|
612
695
|
*
|
|
613
696
|
* Errors are caught per-addon and logged so one bad addon cannot block
|
|
614
697
|
* other addons' saves or the core-preferences save.
|
|
698
|
+
*
|
|
699
|
+
* #1603: an add-on whose switch was refused because its documents are not
|
|
700
|
+
* accepted yet (`ConsentRequiredError`) is returned, so the route can tell
|
|
701
|
+
* the person what to accept instead of the save failing silently.
|
|
702
|
+
*/
|
|
703
|
+
saveProfileSections(ctx: ActorContext, body: Record<string, unknown>): Promise<{
|
|
704
|
+
consentRequired: ConsentRequiredError[];
|
|
705
|
+
}>;
|
|
706
|
+
/**
|
|
707
|
+
* The add-ons the site's listings show (#1965): enabled, and not
|
|
708
|
+
* `listed: false` in their manifest, in load order. Cheap: it calls no
|
|
709
|
+
* add-on's status(), so a page can render it.
|
|
710
|
+
*/
|
|
711
|
+
listedAddons(): AddonListing[];
|
|
712
|
+
/**
|
|
713
|
+
* Whether `slug` is an add-on whose manifest says `perPerson: true` (#1963),
|
|
714
|
+
* loaded or still loading. ConsentManager asks it when an add-on registers
|
|
715
|
+
* documents: they guard the per-person switch, so only such an add-on has any.
|
|
716
|
+
*/
|
|
717
|
+
isPerPerson(slug: string): boolean;
|
|
718
|
+
/** #1963: the loaded add-ons whose manifest says `perPerson: true`, in load order. */
|
|
719
|
+
private perPersonAddons;
|
|
720
|
+
/**
|
|
721
|
+
* The per-person add-ons this subject has turned on (#1963): each loaded
|
|
722
|
+
* add-on whose manifest says `perPerson: true` and whose switch, the
|
|
723
|
+
* person's preference `<slug>.enabled`, is `true`.
|
|
724
|
+
*
|
|
725
|
+
* It reads exactly those keys and no other preference, so no access check
|
|
726
|
+
* or listing built on it can come to depend on an arbitrary setting. An
|
|
727
|
+
* anonymous subject, an unknown or inactive account, and the system
|
|
728
|
+
* principal (which has no account) have none.
|
|
729
|
+
*
|
|
730
|
+
* @param ctx - Whose add-ons: the request's subject, or a job's context
|
|
731
|
+
* @returns The slugs, in load order
|
|
615
732
|
*/
|
|
616
|
-
|
|
733
|
+
enabledAddonsFor(ctx: ActorContext): Promise<string[]>;
|
|
617
734
|
/**
|
|
618
735
|
* Get count of loaded add-ons
|
|
619
736
|
*/
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import BaseManager, { BackupData, type ManagerBackupOptions, type ManagerRestoreOptions, type ManagerStats } from './BaseManager.js';
|
|
1
|
+
import BaseManager, { BackupData, type BackupArchiveTarget, type ManagerBackupOptions, type ManagerRestoreOptions, type ManagerStats, type SiteRestoreStaging, type StagedRestoreAtStart } from './BaseManager.js';
|
|
2
2
|
import type { AttachmentFilesBackupData, AttachmentFilesBackupOutcome } from '../providers/attachmentFilesBackup.js';
|
|
3
|
-
import type { PartOutcome } from '../utils/siteRestore.js';
|
|
4
3
|
import { type ActorContext } from '../context/ActorContext.js';
|
|
5
4
|
/**
|
|
6
5
|
* Marks an error as "the audit record could not be written", not "the action
|
|
@@ -36,11 +35,11 @@ interface BaseAttachmentProvider {
|
|
|
36
35
|
backup(): Promise<unknown>;
|
|
37
36
|
restore(backupData: unknown): Promise<void>;
|
|
38
37
|
/** #1876: the files themselves beside the backup document, and their staged restore. */
|
|
39
|
-
backupFiles?(
|
|
40
|
-
stageFilesRestore?(providerBackup: Record<string, unknown>, files: AttachmentFilesBackupData,
|
|
38
|
+
backupFiles?(archive: (part: string) => BackupArchiveTarget): Promise<AttachmentFilesBackupOutcome>;
|
|
39
|
+
stageFilesRestore?(providerBackup: Record<string, unknown>, files: AttachmentFilesBackupData, readArchive: NonNullable<ManagerRestoreOptions['readArchive']>, siteRestore: SiteRestoreStaging): Promise<{
|
|
41
40
|
skippedVaults: string[];
|
|
42
41
|
}>;
|
|
43
|
-
takeRestoreAtStart?():
|
|
42
|
+
takeRestoreAtStart?(): StagedRestoreAtStart | null;
|
|
44
43
|
shutdown(): Promise<void>;
|
|
45
44
|
getProviderInfo(): {
|
|
46
45
|
features: string[];
|