@jwilleke/ngdpbase-types 5.38.0 → 5.39.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 -1
- package/lib/src/ice/ledger.d.ts +2 -0
- package/lib/src/ice/runImport.d.ts +6 -0
- package/lib/src/ice/shims/csv.d.ts +59 -0
- package/lib/src/ice/shims/dotpath.d.ts +7 -0
- package/lib/src/ice/shims/geojson.d.ts +19 -0
- package/lib/src/ice/shims/index.d.ts +16 -0
- package/lib/src/ice/shims/record.d.ts +68 -0
- package/lib/src/ice/shims/restjson.d.ts +14 -0
- package/lib/src/ice/shims/rss-atom.d.ts +22 -0
- package/lib/src/ice/shims/xml-index.d.ts +32 -0
- package/lib/src/ice/shims/xml.d.ts +30 -0
- package/lib/src/ice/types.d.ts +109 -7
- package/lib/src/managers/BackupManager.d.ts +20 -2
- package/lib/src/managers/BackupManager.js +39 -6
- package/lib/src/managers/ConsentManager.d.ts +19 -3
- package/lib/src/managers/ImportManager.d.ts +36 -1
- package/lib/src/managers/PageManager.d.ts +53 -3
- package/lib/src/managers/ShareManager.d.ts +27 -0
- package/lib/src/managers/ValidationManager.d.ts +27 -16
- package/lib/src/types/Consent.d.ts +43 -1
- package/lib/src/types/Consent.js +34 -0
- package/lib/src/utils/auditEventNames.generated.d.ts +6 -0
- package/lib/src/utils/auditEventNames.generated.js +6 -0
- package/lib/src/utils/privateStoreCrypto.d.ts +10 -2
- package/lib/src/utils/privateStoreUnlock.d.ts +2 -0
- package/lib/src/utils/shareLockbox.d.ts +1 -1
- package/lib/src/utils/sharedFoldersKey.d.ts +17 -0
- package/lib/src/utils/sharedFoldersKey.js +33 -0
- package/package.json +2 -1
|
@@ -40,7 +40,7 @@ export type { PageAction } from '../../src/utils/pageUrl.js';
|
|
|
40
40
|
export type { default as ConsentManager, ConsentScopeStatus } from '../../src/managers/ConsentManager.js';
|
|
41
41
|
export type { AddonConsentRegistration, ConsentDocument, ConsentRecord, ConsentRequiredError, ConsentScope, ConsentSeen, ConsentStatus } from '../../src/types/Consent.js';
|
|
42
42
|
export type { default as ImportManager } from '../../src/managers/ImportManager.js';
|
|
43
|
-
export type { IceShim, IceReceiver, IceItem, IceKind, IceWarning, ShimDirection, ShimAccepts, ShimTools, ShimReadResult, ReceiveInfo, ReceiveOutcome, IceCitation, KeptOriginal, OriginalChoice, IceOriginalChoices, OriginalKeep, IceUpload, IceSource, IceRunRequest, IceRunReport, IceFileReport, IceFileStatus } from '../../src/ice/types.js';
|
|
43
|
+
export type { IceShim, IceReceiver, IceItem, IceKind, IceWarning, ShimDirection, ShimAccepts, ShimTools, ShimReadResult, ReceiveInfo, ReceiveOutcome, IceCitation, KeptOriginal, OriginalChoice, IceOriginalChoices, OriginalKeep, IceUpload, IceSource, IceUrlSource, IceRunRequest, IceRecordData, FileDoneInfo, IceRunReport, IceFileReport, IceFileStatus, RecentImport } from '../../src/ice/types.js';
|
|
44
44
|
export type { default as UserManager, UserUpdateInput, UserUpdateFn } from '../../src/managers/UserManager.js';
|
|
45
45
|
export type { User, UserPreferences } from '../../src/types/User.js';
|
|
46
46
|
export type { default as AuthManager, InviteOutcome, InvitationView } from '../../src/managers/AuthManager.js';
|
package/lib/src/ice/ledger.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ declare abstract class ScopedLedger implements IceLedger {
|
|
|
25
25
|
recordFile(scope: string, entry: LedgerFileEntry): Promise<void>;
|
|
26
26
|
hasItem(scope: string, itemKey: string): Promise<boolean>;
|
|
27
27
|
recordItem(scope: string, itemKey: string): Promise<void>;
|
|
28
|
+
seenItems(scope: string, itemKeys: string[]): Promise<Set<string>>;
|
|
29
|
+
recordItems(scope: string, itemKeys: string[]): Promise<void>;
|
|
28
30
|
files(scope: string): Promise<LedgerFileEntry[]>;
|
|
29
31
|
}
|
|
30
32
|
/** The ledger in memory: for tests, and for a site that keeps no import history. */
|
|
@@ -9,6 +9,8 @@ export interface IceEngineSettings {
|
|
|
9
9
|
maxFileBytes: number;
|
|
10
10
|
maxChainDepth: number;
|
|
11
11
|
defaultOriginal: OriginalChoice;
|
|
12
|
+
/** The largest response a URL source may return (#2109). */
|
|
13
|
+
fetchMaxBytes: number;
|
|
12
14
|
}
|
|
13
15
|
export interface IceRunDeps {
|
|
14
16
|
/** Enabled shims, in registration order. */
|
|
@@ -17,6 +19,8 @@ export interface IceRunDeps {
|
|
|
17
19
|
ledger: IceLedger;
|
|
18
20
|
settings: IceEngineSettings;
|
|
19
21
|
tools(shim: RegisteredShim): ShimTools;
|
|
22
|
+
/** The engine's guarded fetch, for a URL source (#2109): the same one `tools` hands shims. */
|
|
23
|
+
fetch?: ShimTools['fetch'];
|
|
20
24
|
now?: () => Date;
|
|
21
25
|
}
|
|
22
26
|
/** The id of the receiver that keeps an original when the chosen receiver does not keep its own (D11). */
|
|
@@ -39,6 +43,8 @@ export declare const SCOPE_FORM = "ice2";
|
|
|
39
43
|
* `sub` (phr's source id) narrows it within that user's scope, never outside.
|
|
40
44
|
*/
|
|
41
45
|
export declare function boundScope(receiverId: string, owner: string, sub?: string): string;
|
|
46
|
+
/** Does this shim list the receiver, by id or as any receiver (`'*'`, #2109)? */
|
|
47
|
+
export declare function handsTo(shim: Pick<RegisteredShim, 'receivers'>, receiverId: string): boolean;
|
|
42
48
|
export interface ChainLeaf {
|
|
43
49
|
item: IceItem;
|
|
44
50
|
chain: string[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* csv shim (#685 slice 4+, #911; a core shim since #2109) — zero dependency (a
|
|
3
|
+
* small RFC 4180 parser). Needed for sources that only publish CSV, e.g. NASA
|
|
4
|
+
* FIRMS thermal hotspots (geohazardwatch#4), whose `/api/area/csv/...`
|
|
5
|
+
* endpoint has no JSON/GeoJSON/XML variant.
|
|
6
|
+
*
|
|
7
|
+
* Nothing in it is about feeds, so any receiver of records can use it (a
|
|
8
|
+
* bank statement as well as a feed). The first row is the header; each
|
|
9
|
+
* subsequent row becomes one `record` item keyed by the header column names
|
|
10
|
+
* (all values are strings — `map`/dot-path and the downstream schema
|
|
11
|
+
* mappers cast as needed, exactly as for the other shims).
|
|
12
|
+
*
|
|
13
|
+
* Rows rarely carry an `id` column, so when neither the `recordIdField`
|
|
14
|
+
* option nor an `id` field resolves, a stable content hash of the row is
|
|
15
|
+
* synthesized as the id — order-independent (keys sorted) so a column
|
|
16
|
+
* reorder upstream doesn't churn ids, and content-addressed so
|
|
17
|
+
* change-detection still works.
|
|
18
|
+
*
|
|
19
|
+
* Delimiter defaults to `,`; override with the `delimiter` option (e.g. `\t`).
|
|
20
|
+
*
|
|
21
|
+
* The `skipLines` option skips N leading lines before the header (#1102).
|
|
22
|
+
* Some sources — government data exports especially — emit a human-readable
|
|
23
|
+
* caption above the real header. Without skipping it, the caption becomes
|
|
24
|
+
* the sole column name, the real header is consumed as the first record,
|
|
25
|
+
* and every column but the first is discarded. That failure is entirely
|
|
26
|
+
* silent: HTTP 200, a successful parse, records stored, and a table of empty
|
|
27
|
+
* values. Hence {@link preambleSuspicion}, which makes it visible (a warning
|
|
28
|
+
* on the read) without blocking it.
|
|
29
|
+
*/
|
|
30
|
+
import { type IceRecordData, type IceShim } from '../types.js';
|
|
31
|
+
import { type RecordShimOptions, type RawRecord } from './record.js';
|
|
32
|
+
/**
|
|
33
|
+
* Parse CSV text into header-keyed row objects. RFC 4180 subset: quoted fields,
|
|
34
|
+
* escaped quotes (`""`), embedded commas/newlines inside quotes, CRLF/CR/LF line
|
|
35
|
+
* endings, and a leading UTF-8 BOM. Values are returned verbatim (strings);
|
|
36
|
+
* header cells are trimmed. Fully-empty lines are skipped.
|
|
37
|
+
*
|
|
38
|
+
* `skipLines` drops that many leading lines before the header is taken, for
|
|
39
|
+
* feeds with a caption preamble (#1102). Default 0 — byte-identical to the
|
|
40
|
+
* previous behaviour.
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseCsv(text: string, delimiter?: string, skipLines?: number): Record<string, string>[];
|
|
43
|
+
/**
|
|
44
|
+
* Return the suspect header when a parse looks like it consumed an unskipped
|
|
45
|
+
* preamble, or `null` when it looks fine (#1102).
|
|
46
|
+
*
|
|
47
|
+
* The signature of the failure is a single column whose name reads like a
|
|
48
|
+
* sentence: a caption line has no delimiter in it, so the whole line becomes one
|
|
49
|
+
* field. A legitimate single-column CSV has a short, word-like header, so the
|
|
50
|
+
* two are distinguishable in practice.
|
|
51
|
+
*
|
|
52
|
+
* Deliberately advisory. A one-column CSV is legal, just unusual, and a caller
|
|
53
|
+
* that guessed and re-parsed would only trade a visible wrong answer for an
|
|
54
|
+
* invisible one.
|
|
55
|
+
*/
|
|
56
|
+
export declare function preambleSuspicion(columns: readonly string[]): string | null;
|
|
57
|
+
/** One row as a record: its own id, or one synthesized from its content. */
|
|
58
|
+
export declare function csvRecord(raw: RawRecord, opts: RecordShimOptions): IceRecordData | null;
|
|
59
|
+
export declare const csvShim: IceShim;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trivial dot-path lookup (#685) — the only "mapping engine" the framework has.
|
|
3
|
+
* NO JSONPath dependency (design §7): `getByPath(obj, 'properties.mag')`,
|
|
4
|
+
* `getByPath(obj, 'geometry.coordinates.2')` (numeric segment = array index).
|
|
5
|
+
* Returns undefined for any missing segment.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getByPath(obj: unknown, path: string): unknown;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GeoJSON shim (#685 slice 4; a core shim since #2109) — zero dependency (JSON.parse).
|
|
3
|
+
*
|
|
4
|
+
* Handles a GeoJSON FeatureCollection (`{ features: [...] }`), a bare array of
|
|
5
|
+
* features, or a single Feature. Per feature, the id comes from the
|
|
6
|
+
* `recordIdField` option (dot-path) or the feature's own `id`; properties come
|
|
7
|
+
* from the `map` option (dot-path per field) or, with no map, the feature's
|
|
8
|
+
* `properties` object.
|
|
9
|
+
*
|
|
10
|
+
* Reference feed: the USGS earthquakes GeoJSON
|
|
11
|
+
* (https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_week.geojson).
|
|
12
|
+
*/
|
|
13
|
+
import { type IceRecordData, type IceShim } from '../types.js';
|
|
14
|
+
import { type RecordShimOptions, type RawRecord } from './record.js';
|
|
15
|
+
/** The features of a GeoJSON document. */
|
|
16
|
+
export declare function geojsonFeatures(data: unknown): RawRecord[];
|
|
17
|
+
/** One feature as a record. With no map, the feature's own `properties` object is lifted. */
|
|
18
|
+
export declare function geojsonRecord(raw: RawRecord, opts: RecordShimOptions): IceRecordData | null;
|
|
19
|
+
export declare const geojsonShim: IceShim;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core's record shims (#2109; moved into core by the operator's decision of
|
|
3
|
+
* 2026-10-29, "generic work goes to core"): one per structured format,
|
|
4
|
+
* registered by ImportManager as `core.<name>`, so any receiver that takes
|
|
5
|
+
* records can use them, with no add-on enabled. The feeds add-on was their
|
|
6
|
+
* first user and names one per source as its `adapter`; accounting's bank
|
|
7
|
+
* statements (#2121) are the next.
|
|
8
|
+
*
|
|
9
|
+
* Shipped: geojson + rest-json (zero-dep), xml (fast-xml-parser), csv
|
|
10
|
+
* (zero-dep, #911), rss-atom (#913, xml-based) and xml-index (#912, two-phase).
|
|
11
|
+
* Each produces `record` items with per-item duplicates and hands to any
|
|
12
|
+
* receiver that takes records (`receivers: ['*']`).
|
|
13
|
+
*/
|
|
14
|
+
import type { IceShim } from '../types.js';
|
|
15
|
+
/** Every record shim, in registration order. */
|
|
16
|
+
export declare const RECORD_SHIMS: readonly IceShim[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What core's record shims share (#685 slice 8; core shims since #2109): the
|
|
3
|
+
* options a run hands its shim, turning one raw item into a `record` item,
|
|
4
|
+
* and the item's key.
|
|
5
|
+
*
|
|
6
|
+
* Nothing here is about any one receiver. A record shim reads one format
|
|
7
|
+
* into `record` items (`data` is `{ id, properties }`, `IceRecordData`); which
|
|
8
|
+
* receiver takes them, and what it does with them, is the receiver's
|
|
9
|
+
* business. So the same CSV shim serves a feed (the feeds add-on's
|
|
10
|
+
* `feeds.items`) and a bank-statement import alike, with neither add-on's
|
|
11
|
+
* code (operator, 2026-10-29: generic work goes to core).
|
|
12
|
+
*
|
|
13
|
+
* id ← the `recordIdField` option (dot-path) or the item's own `id`.
|
|
14
|
+
* properties ← the `map` option (dot-path per field), or — with no map — the
|
|
15
|
+
* supplied `defaultProps` object (geojson lifts `feature.properties`;
|
|
16
|
+
* rest-json lifts the item itself). No id resolves → no record (skipped).
|
|
17
|
+
*/
|
|
18
|
+
import type { IceItem, IceRecordData, IceWarning } from '../types.js';
|
|
19
|
+
/** One raw item straight off the wire (a GeoJSON feature, an RSS item, a CSV row, …). */
|
|
20
|
+
export type RawRecord = Record<string, unknown>;
|
|
21
|
+
/** The kind every record shim produces: core's generic record (#2109). */
|
|
22
|
+
export declare const RECORD_KIND = "record";
|
|
23
|
+
/**
|
|
24
|
+
* A record shim's options (#2109): the site's `ngdpbase.import.shims.<id>.options`,
|
|
25
|
+
* with the run's own over them (a feed source's settings, an import's).
|
|
26
|
+
* Each is optional and generic: where the items are, which field is the id,
|
|
27
|
+
* a field map, and the few format settings (CSV's delimiter and preamble, an
|
|
28
|
+
* index's links).
|
|
29
|
+
*/
|
|
30
|
+
export interface RecordShimOptions {
|
|
31
|
+
/** Dot-path to each item's id. */
|
|
32
|
+
recordIdField?: string;
|
|
33
|
+
/** Dot-path map: property → source path. */
|
|
34
|
+
map?: Record<string, string>;
|
|
35
|
+
/** Dot-path to the items array (rest-json, xml, rss-atom, xml-index). */
|
|
36
|
+
itemsPath?: string;
|
|
37
|
+
/** csv: the field delimiter. Default ','. */
|
|
38
|
+
delimiter?: string;
|
|
39
|
+
/** csv: lines skipped before the header (#1102). Default 0. */
|
|
40
|
+
skipLines?: number;
|
|
41
|
+
/** xml-index: a regex matched against the index's hrefs. */
|
|
42
|
+
linkPattern?: string;
|
|
43
|
+
/** xml-index: the most item documents fetched in one run. */
|
|
44
|
+
maxItems?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The options as a shim reads them: each of the right type, or absent. The
|
|
48
|
+
* caller (a feed's configuration) validated its own; this only refuses a
|
|
49
|
+
* value of the wrong type from anywhere.
|
|
50
|
+
*/
|
|
51
|
+
export declare function recordOptions(raw: Record<string, unknown>): RecordShimOptions;
|
|
52
|
+
export declare function buildRecord(raw: RawRecord, opts: RecordShimOptions, defaultProps: Record<string, unknown> | undefined): IceRecordData | null;
|
|
53
|
+
/**
|
|
54
|
+
* The key the engine knows a record by (per-item duplicates, #2109): its own
|
|
55
|
+
* id with a hash of its content. The id alone would skip a record that
|
|
56
|
+
* changed upstream under the same id (a revised advisory), so the hash is
|
|
57
|
+
* the version. A receiver's own duplicate check is still its own (the feeds
|
|
58
|
+
* add-on compares the hash its record store keeps).
|
|
59
|
+
*/
|
|
60
|
+
export declare function recordItemKey(rec: IceRecordData): string;
|
|
61
|
+
/** The `record` items of one read, from its records in order; null records (no id) are dropped. */
|
|
62
|
+
export declare function recordItems(input: IceItem, records: Array<IceRecordData | null>, warnings?: IceWarning[]): IceItem[];
|
|
63
|
+
/**
|
|
64
|
+
* Locate the items array in a JSON response when no explicit `itemsPath` is
|
|
65
|
+
* configured: the body itself if it's an array, else the first array found
|
|
66
|
+
* under common envelope keys, else any array-valued property, else [].
|
|
67
|
+
*/
|
|
68
|
+
export declare function pickItemsArray(data: unknown): unknown[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rest-json shim (#685 slice 8; a core shim since #2109) — zero dependency (JSON.parse).
|
|
3
|
+
*
|
|
4
|
+
* Generic REST GET → JSON. Locates the items array via the `itemsPath` option
|
|
5
|
+
* (dot-path, e.g. 'results' or 'data.events'); with none, falls back to the
|
|
6
|
+
* body itself if it's an array, or the first array under a common envelope
|
|
7
|
+
* key (results/data/items/records). Each item IS the record (its own fields
|
|
8
|
+
* are lifted as properties when no `map` is set).
|
|
9
|
+
*/
|
|
10
|
+
import { type IceShim } from '../types.js';
|
|
11
|
+
import { type RawRecord } from './record.js';
|
|
12
|
+
/** The items of a JSON document: at `itemsPath`, or the first likely array. */
|
|
13
|
+
export declare function jsonItems(data: unknown, itemsPath: string | undefined): RawRecord[];
|
|
14
|
+
export declare const restJsonShim: IceShim;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rss-atom shim (#913; a core shim since #2109) — dependency: fast-xml-parser
|
|
3
|
+
* (shared with the xml shim). RSS 2.0 and Atom 1.0 are both XML, so this is a
|
|
4
|
+
* thin, sensible-default wrapper over the shared XML machinery rather than a
|
|
5
|
+
* separate parser.
|
|
6
|
+
*
|
|
7
|
+
* Item location (when the `itemsPath` option is not set):
|
|
8
|
+
* - RSS 2.0 → `rss.channel.item`
|
|
9
|
+
* - Atom 1.0 → `feed.entry`
|
|
10
|
+
* - else the generic envelope detection (pickItemsArray)
|
|
11
|
+
*
|
|
12
|
+
* Id resolution (when `recordIdField` is not set): RSS `<guid>` → `<link>`;
|
|
13
|
+
* Atom `<id>` → `<link href>`. guid/link may parse as objects (guid with
|
|
14
|
+
* `@isPermaLink`, Atom link as an attribute-only element) — both are unwrapped.
|
|
15
|
+
*/
|
|
16
|
+
import { type IceRecordData, type IceShim } from '../types.js';
|
|
17
|
+
import { type RecordShimOptions, type RawRecord } from './record.js';
|
|
18
|
+
/** The items of an RSS or Atom document. */
|
|
19
|
+
export declare function rssAtomItems(text: string, itemsPath: string | undefined): RawRecord[];
|
|
20
|
+
/** One RSS item or Atom entry as a record. */
|
|
21
|
+
export declare function rssAtomRecord(raw: RawRecord, opts: RecordShimOptions): IceRecordData | null;
|
|
22
|
+
export declare const rssAtomShim: IceShim;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xml-index shim (#912; a core shim since #2109) — dependency: fast-xml-parser
|
|
3
|
+
* (shared with the xml shim). For "index page + N item documents, no
|
|
4
|
+
* combined feed" sources common to bulletin-style publishers (e.g. Washington
|
|
5
|
+
* VAAC ash advisories, geohazardwatch#5): an HTML/text index links to many
|
|
6
|
+
* separately-fetchable per-record XML files, with no single URL returning
|
|
7
|
+
* them all.
|
|
8
|
+
*
|
|
9
|
+
* Two phases:
|
|
10
|
+
* 1. The index is the file the engine hands it (a URL source, fetched by the
|
|
11
|
+
* engine). Item URLs are the hrefs matching the `linkPattern` option (a
|
|
12
|
+
* regex string), resolved against the index's URL (`metadata.sourceUrl`,
|
|
13
|
+
* else the file's name). Deduped, capped at the `maxItems` option
|
|
14
|
+
* (default 100; a site sets its own with
|
|
15
|
+
* `ngdpbase.import.shims.core.xml-index.options.maxItems`).
|
|
16
|
+
* 2. Each item document is fetched by asking the engine (`tools.fetch`), so
|
|
17
|
+
* every request takes the one guarded path with the source's headers
|
|
18
|
+
* and timeout (design rule), then XML-parsed. Each yields one record (its
|
|
19
|
+
* unwrapped root), or — with `itemsPath` — the items located in it. The
|
|
20
|
+
* item's URL is kept as `__itemUrl` so it serves as the id when the
|
|
21
|
+
* document has none.
|
|
22
|
+
*
|
|
23
|
+
* A single failed item fetch or parse is skipped, never failing the whole
|
|
24
|
+
* read: the read is then `partial`, saying which.
|
|
25
|
+
*/
|
|
26
|
+
import { type IceShim } from '../types.js';
|
|
27
|
+
import { buildRecord, type RecordShimOptions, type RawRecord } from './record.js';
|
|
28
|
+
/** Extract, resolve, dedupe and cap item URLs from index HTML/text. */
|
|
29
|
+
export declare function extractItemUrls(html: string, baseUrl: string, linkPattern: string, maxItems: number): string[];
|
|
30
|
+
/** One item document's record: `__itemUrl` is plumbing, kept out of the properties but usable as the id. */
|
|
31
|
+
export declare function xmlIndexRecord(raw: RawRecord, opts: RecordShimOptions): ReturnType<typeof buildRecord>;
|
|
32
|
+
export declare const xmlIndexShim: IceShim;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xml shim (#685 slice 8; a core shim since #2109) — dependency: fast-xml-parser
|
|
3
|
+
* (itself zero-dep).
|
|
4
|
+
*
|
|
5
|
+
* Generic XML → records, for sources with no JSON surface (e.g. VAAC ash
|
|
6
|
+
* advisories, geohazardwatch#5). The document is parsed to plain objects so
|
|
7
|
+
* the existing dot-path machinery applies unchanged: locate the items array
|
|
8
|
+
* via the `itemsPath` option (e.g. 'rss.channel.item' or
|
|
9
|
+
* 'advisories.advisory'), map fields via `map` / `recordIdField`.
|
|
10
|
+
*
|
|
11
|
+
* XML-specific shape rules:
|
|
12
|
+
* - Attributes are kept, prefixed '@' — dot-path them as e.g. 'link.@href'.
|
|
13
|
+
* - Element text alongside attributes lands under '#text'.
|
|
14
|
+
* - A single repeated element parses as an object, not a 1-element array —
|
|
15
|
+
* the shim coerces the located items to an array, so `itemsPath` behaves
|
|
16
|
+
* identically for 1 and N items.
|
|
17
|
+
*/
|
|
18
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
19
|
+
import { type IceShim } from '../types.js';
|
|
20
|
+
import { type RawRecord } from './record.js';
|
|
21
|
+
/**
|
|
22
|
+
* Shared fast-xml-parser instance — reused by the rss-atom (#913) and xml-index
|
|
23
|
+
* (#912) shims so XML shape rules stay identical across all XML-based sources.
|
|
24
|
+
*/
|
|
25
|
+
export declare const xmlParser: XMLParser;
|
|
26
|
+
/** XML parses a lone repeated element as an object — normalize to an array. */
|
|
27
|
+
export declare function coerceItems(located: unknown): RawRecord[];
|
|
28
|
+
/** The items of an XML document: at `itemsPath`, else under its (single) root by envelope detection. */
|
|
29
|
+
export declare function xmlItems(text: string, itemsPath: string | undefined): RawRecord[];
|
|
30
|
+
export declare const xmlShim: IceShim;
|
package/lib/src/ice/types.d.ts
CHANGED
|
@@ -20,12 +20,16 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import type { ActorContext } from '../context/ActorContext.js';
|
|
22
22
|
import type { GuardedFetchOptions, GuardedResponse } from '../http/guardedFetch.js';
|
|
23
|
+
/** In a shim's `receivers`: any receiver that takes the kind the shim produces (#2109). */
|
|
24
|
+
export declare const ANY_RECEIVER = "*";
|
|
23
25
|
/** D3: which way a shim moves data. Most are `in`; PDF (#1974) would be `both`. */
|
|
24
26
|
export type ShimDirection = 'in' | 'out' | 'both';
|
|
25
27
|
/**
|
|
26
28
|
* The kind of a piece of content moving through the engine. `file` is raw
|
|
27
|
-
* input bytes; `html` and `ncm` are core's;
|
|
28
|
-
*
|
|
29
|
+
* input bytes; `html` and `ncm` are core's; `record` is one structured entry
|
|
30
|
+
* of a table or feed (`data` is `{ id, properties }`, {@link IceRecordData}),
|
|
31
|
+
* so a format shim such as CSV serves any receiver that takes records (#2109);
|
|
32
|
+
* an add-on's own kinds are named `<slug>.<kind>`, e.g. `phr.health-record`.
|
|
29
33
|
*/
|
|
30
34
|
export type IceKind = string;
|
|
31
35
|
/** A structured note from a shim, the same shape as a converter's warning. */
|
|
@@ -57,6 +61,15 @@ export interface IceItem {
|
|
|
57
61
|
*/
|
|
58
62
|
itemKey?: string;
|
|
59
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The `data` of a `record` item (#2109): one row of a table or one entry of a
|
|
66
|
+
* feed. `id` is the entry's own id (a configured id field, the format's own
|
|
67
|
+
* id, or a hash of the row when it has none); `properties` its fields.
|
|
68
|
+
*/
|
|
69
|
+
export interface IceRecordData {
|
|
70
|
+
id: string;
|
|
71
|
+
properties: Record<string, unknown>;
|
|
72
|
+
}
|
|
60
73
|
/** What a shim takes. A file matches by extension or media type; a chained input by kind. */
|
|
61
74
|
export interface ShimAccepts {
|
|
62
75
|
/** Lower-case, with the dot: `['.xml']`. */
|
|
@@ -78,7 +91,11 @@ export interface ShimTools {
|
|
|
78
91
|
* path, with the site's egress policy, User-Agent and timeout.
|
|
79
92
|
*/
|
|
80
93
|
fetch(url: string, options?: Omit<GuardedFetchOptions, 'policy' | 'transport' | 'resolver'>): Promise<GuardedResponse>;
|
|
81
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* This shim's options from site configuration (D6):
|
|
96
|
+
* `ngdpbase.import.shims.<id>.options`, with the run's own `options` over
|
|
97
|
+
* them (a feed source's parse settings, #2109).
|
|
98
|
+
*/
|
|
82
99
|
options: Record<string, unknown>;
|
|
83
100
|
}
|
|
84
101
|
/** What a shim's read returns. */
|
|
@@ -104,7 +121,10 @@ export interface IceShim {
|
|
|
104
121
|
produces: IceKind;
|
|
105
122
|
/**
|
|
106
123
|
* Receivers it may hand to directly (ids, `core.pages` or `<slug>.<name>`).
|
|
107
|
-
* Empty for a shim whose output only feeds another shim.
|
|
124
|
+
* Empty for a shim whose output only feeds another shim. {@link ANY_RECEIVER}
|
|
125
|
+
* (`'*'`): any receiver that takes the kind it produces — a generic format
|
|
126
|
+
* shim such as core's CSV, whose `record` items any add-on's receiver may
|
|
127
|
+
* take (#2109). Each receiver still decides with its own permission (D5).
|
|
108
128
|
*/
|
|
109
129
|
receivers: string[];
|
|
110
130
|
/** Two duplicate kinds (design rule): a whole file by fingerprint, or each item by `itemKey`. Default `file`. */
|
|
@@ -185,6 +205,13 @@ export interface ReceiveInfo {
|
|
|
185
205
|
citation?: IceCitation;
|
|
186
206
|
original?: KeptOriginal;
|
|
187
207
|
}
|
|
208
|
+
/** What a receiver's `fileDone` is told (#2109). */
|
|
209
|
+
export interface FileDoneInfo extends ReceiveInfo {
|
|
210
|
+
/** What the source said about the file: a URL source's `metadata`. */
|
|
211
|
+
metadata: Record<string, unknown>;
|
|
212
|
+
/** Every item the file held, in order: those handed to `receive`, and those skipped as already seen. */
|
|
213
|
+
items: IceItem[];
|
|
214
|
+
}
|
|
188
215
|
/** A receiver's answer for one item. `refused` is its existing permission saying no (D5). */
|
|
189
216
|
export interface ReceiveOutcome {
|
|
190
217
|
status: 'stored' | 'duplicate' | 'refused';
|
|
@@ -211,6 +238,29 @@ export interface IceReceiver {
|
|
|
211
238
|
mayUse(ctx: ActorContext): Promise<boolean>;
|
|
212
239
|
/** Hand one item to the receiver's own door. */
|
|
213
240
|
receive(item: IceItem, ctx: ActorContext, info: ReceiveInfo): Promise<ReceiveOutcome>;
|
|
241
|
+
/**
|
|
242
|
+
* Called once per file whose shims read it through (#2109), after each of
|
|
243
|
+
* its items was handed to `receive` or skipped as already seen, with every
|
|
244
|
+
* item the file held, in order. A receiver that holds a snapshot of a
|
|
245
|
+
* source (a feed's records) stores it here and drops what the source no
|
|
246
|
+
* longer holds: an item skipped as already seen is still in the source.
|
|
247
|
+
* Not called for a file that failed before its items were known. Returns a
|
|
248
|
+
* note for the file's status; a throw fails the file.
|
|
249
|
+
*/
|
|
250
|
+
fileDone?(ctx: ActorContext, info: FileDoneInfo): Promise<string | void>;
|
|
251
|
+
/**
|
|
252
|
+
* `false`: this receiver is fed only by its owner's own sources (a feed's
|
|
253
|
+
* schedule, #2109). The import screen never offers it and an upload into
|
|
254
|
+
* it is refused. Default: it takes uploads.
|
|
255
|
+
*/
|
|
256
|
+
uploads?: boolean;
|
|
257
|
+
/**
|
|
258
|
+
* Where this context can open what one of its references names (#2110),
|
|
259
|
+
* for "your recent imports" on the import screen: a URL, or null when
|
|
260
|
+
* the thing is gone or this context may not open it. The receiver decides
|
|
261
|
+
* with its own existing read rule (D5). Without it, nothing is linked.
|
|
262
|
+
*/
|
|
263
|
+
linkFor?(ref: string, ctx: ActorContext): Promise<string | null>;
|
|
214
264
|
/**
|
|
215
265
|
* Keep the original file itself (D11: phr keeps originals in its own
|
|
216
266
|
* store). Without this, `keep` hands the original to the files receiver.
|
|
@@ -241,13 +291,36 @@ export interface IceUpload {
|
|
|
241
291
|
mediaType?: string;
|
|
242
292
|
}
|
|
243
293
|
/**
|
|
244
|
-
* A
|
|
245
|
-
*
|
|
294
|
+
* A URL the engine fetches as the run's one file (D1, D10, #2109): through
|
|
295
|
+
* the one guarded path (`guardedFetch`, #1139) with the site's egress policy
|
|
296
|
+
* and default User-Agent, and the source's own headers and timeout (#1959),
|
|
297
|
+
* which also apply to every follow-up fetch a shim asks of the engine during
|
|
298
|
+
* the run. A feed's schedule is its owner's job; the engine fetches when run.
|
|
299
|
+
*/
|
|
300
|
+
export interface IceUrlSource {
|
|
301
|
+
type: 'url';
|
|
302
|
+
url: string;
|
|
303
|
+
/** The name the fetched file gets in reports and the ledger. Default: the URL. */
|
|
304
|
+
name?: string;
|
|
305
|
+
/** Request headers for every fetch of the run (a `User-Agent` replaces the site default). */
|
|
306
|
+
headers?: Record<string, string>;
|
|
307
|
+
/** Each fetch's time limit, ms. Default `ngdpbase.fetch-timeout-ms`. */
|
|
308
|
+
timeoutMs?: number;
|
|
309
|
+
/**
|
|
310
|
+
* Facts the source states about the file, carried on it and on every item
|
|
311
|
+
* read from it (a feed source's id). The engine adds `sourceUrl`, the URL
|
|
312
|
+
* fetched, so a shim can resolve the file's relative links.
|
|
313
|
+
*/
|
|
314
|
+
metadata?: Record<string, unknown>;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* A source (D1): uploaded files, or a URL fetched when the run starts
|
|
318
|
+
* (#2109). A server folder joins this union later.
|
|
246
319
|
*/
|
|
247
320
|
export type IceSource = {
|
|
248
321
|
type: 'upload';
|
|
249
322
|
files: IceUpload[];
|
|
250
|
-
};
|
|
323
|
+
} | IceUrlSource;
|
|
251
324
|
/** One import, as a caller asks for it. */
|
|
252
325
|
export interface IceRunRequest {
|
|
253
326
|
source: IceSource;
|
|
@@ -267,6 +340,12 @@ export interface IceRunRequest {
|
|
|
267
340
|
* value a client sent.
|
|
268
341
|
*/
|
|
269
342
|
scope?: string;
|
|
343
|
+
/**
|
|
344
|
+
* Options for this run's shims, over the site's (`ShimTools.options`): a
|
|
345
|
+
* feed source's parse settings (#2109). Server-built; never a value a
|
|
346
|
+
* client sent.
|
|
347
|
+
*/
|
|
348
|
+
options?: Record<string, unknown>;
|
|
270
349
|
}
|
|
271
350
|
/** Per-file status (first slice). */
|
|
272
351
|
export type IceFileStatus = 'imported' | 'partial' | 'unchanged' | 'duplicate' | 'noise' | 'unreadable' | 'refused' | 'failed';
|
|
@@ -318,6 +397,25 @@ export interface LedgerFileEntry {
|
|
|
318
397
|
/** The original kept for this fingerprint, whatever became of the conversion (D11). */
|
|
319
398
|
original?: LedgerOriginal;
|
|
320
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* One file in a user's recent imports (#2110), read from the ledger: what
|
|
402
|
+
* the import screen lists, so the list survives a restart. Only the user's
|
|
403
|
+
* own entries, from the receivers they may use.
|
|
404
|
+
*/
|
|
405
|
+
export interface RecentImport {
|
|
406
|
+
name: string;
|
|
407
|
+
status: IceFileStatus;
|
|
408
|
+
message?: string;
|
|
409
|
+
/** When the ledger last recorded this file, ISO. */
|
|
410
|
+
at: string;
|
|
411
|
+
/** The receiver it went into. */
|
|
412
|
+
receiver: string;
|
|
413
|
+
receiverLabel?: string;
|
|
414
|
+
/** How many references the receiver gave (pages, records). */
|
|
415
|
+
refs: number;
|
|
416
|
+
/** Where the user can open the first of them (`linkFor`), when they may. */
|
|
417
|
+
link?: string;
|
|
418
|
+
}
|
|
321
419
|
/**
|
|
322
420
|
* Where the engine keeps fingerprints, per-file status and item keys, per
|
|
323
421
|
* scope. One interface so a database table can replace the file store when
|
|
@@ -328,5 +426,9 @@ export interface IceLedger {
|
|
|
328
426
|
recordFile(scope: string, entry: LedgerFileEntry): Promise<void>;
|
|
329
427
|
hasItem(scope: string, itemKey: string): Promise<boolean>;
|
|
330
428
|
recordItem(scope: string, itemKey: string): Promise<void>;
|
|
429
|
+
/** Which of these item keys the scope has seen: one read for a whole file's items (#2109). */
|
|
430
|
+
seenItems(scope: string, itemKeys: string[]): Promise<Set<string>>;
|
|
431
|
+
/** Record several item keys in one write (#2109). */
|
|
432
|
+
recordItems(scope: string, itemKeys: string[]): Promise<void>;
|
|
331
433
|
files(scope: string): Promise<LedgerFileEntry[]>;
|
|
332
434
|
}
|
|
@@ -221,6 +221,15 @@ export declare function backupKeyNotSaved(fingerprint: string): string;
|
|
|
221
221
|
export declare const ENV_KEYS_SAVED_KEY = "ngdpbase.backup.env-keys-saved";
|
|
222
222
|
/** The title of the one notification per change of the `.env` key set (#1976). */
|
|
223
223
|
export declare const ENV_KEYS_CHANGED_TITLE = "Save a copy of .env";
|
|
224
|
+
/**
|
|
225
|
+
* The config key that records an admin's confirmation that the shared-folders
|
|
226
|
+
* key is saved off the server (#2124, epic #1775 decision 1): its fingerprint,
|
|
227
|
+
* recorded when they confirm a downloaded `.env` that holds it. A different
|
|
228
|
+
* key asks again.
|
|
229
|
+
*/
|
|
230
|
+
export declare const SHARED_FOLDERS_KEY_SAVED_KEY = "ngdpbase.backup.shared-folders-key-saved-fingerprint";
|
|
231
|
+
/** The banner while no admin has confirmed a saved `.env` holding the current shared-folders key (#2124). */
|
|
232
|
+
export declare function sharedFoldersKeyNotSaved(fingerprint: string): string;
|
|
224
233
|
/** The banner while the instance `.env` sets other names than the ones an admin last confirmed saved (#1976): names only, never a value. */
|
|
225
234
|
export declare function envKeysChanged(change: {
|
|
226
235
|
added: readonly string[];
|
|
@@ -240,6 +249,8 @@ export interface EnvKeysInfo {
|
|
|
240
249
|
removed: string[];
|
|
241
250
|
/** The file holds this site's current backup key, so a saved copy is a saved key too. */
|
|
242
251
|
holdsBackupKey: boolean;
|
|
252
|
+
/** The file holds this site's current shared-folders key (#2124), so a saved copy is a saved key too. */
|
|
253
|
+
holdsSharedFoldersKey: boolean;
|
|
243
254
|
}
|
|
244
255
|
/** What Admin → Backup shows about the backup key: never the key itself. */
|
|
245
256
|
export interface BackupKeyInfo {
|
|
@@ -458,6 +469,12 @@ declare class BackupManager extends BaseManager {
|
|
|
458
469
|
private checkInstanceEnvKeys;
|
|
459
470
|
/** The instance `.env` key set as Admin → Backup shows it (#1976), or null when the site has no instance data folder or the file can't be read. Never a value. */
|
|
460
471
|
envKeysInfo(): EnvKeysInfo | null;
|
|
472
|
+
/**
|
|
473
|
+
* The shared-folders key as Admin → Backup shows it (#2124): set or not,
|
|
474
|
+
* its fingerprint, and whether an admin confirmed a saved `.env` holding
|
|
475
|
+
* it. Never the key. Read at use, like the backup key.
|
|
476
|
+
*/
|
|
477
|
+
sharedFoldersKeyInfo(): BackupKeyInfo;
|
|
461
478
|
/** The backup key as Admin → Backup shows it: set or not, its fingerprint, and whether an admin confirmed it is saved. Never the key. */
|
|
462
479
|
backupKeyInfo(): BackupKeyInfo;
|
|
463
480
|
/**
|
|
@@ -483,8 +500,9 @@ declare class BackupManager extends BaseManager {
|
|
|
483
500
|
* With `envFingerprint` (#1976) the admin confirms the whole `.env` is
|
|
484
501
|
* saved, the one confirmation for both reminders: the names it sets are
|
|
485
502
|
* recorded (`ngdpbase.backup.env-keys-saved`), and so is the backup key
|
|
486
|
-
* when the file holds it
|
|
487
|
-
*
|
|
503
|
+
* when the file holds it, and the shared-folders key when it holds that
|
|
504
|
+
* (#2124, `ngdpbase.backup.shared-folders-key-saved-fingerprint`). Refused
|
|
505
|
+
* when the names changed since the page was loaded.
|
|
488
506
|
*/
|
|
489
507
|
confirmBackupKeySaved(ctx: ActorContext, fingerprint: string, envFingerprint?: string): Promise<void>;
|
|
490
508
|
private configManager;
|
|
@@ -12,6 +12,7 @@ import { applyPartAtStart, beginSiteRestore, commitSiteRestore, discardSiteResto
|
|
|
12
12
|
import { encryptBuffer, ENCRYPTED_SUFFIX } from '../utils/backupCrypto.js';
|
|
13
13
|
import { readBackupDocument, verifyBackupOffThread } from '../utils/backupDocument.js';
|
|
14
14
|
import { BACKUP_KEY_ENV, backupKeyFingerprint, currentBackupKey, isLeftOutForNoKey, LEFT_OUT_FOR_NO_KEY } from '../utils/backupKey.js';
|
|
15
|
+
import { currentSharedFoldersKey, SHARED_FOLDERS_KEY_ENV, sharedFoldersKeyFingerprint } from '../utils/sharedFoldersKey.js';
|
|
15
16
|
import { applyDeclaredPartsAtStart, archiveReader, archiveTarget, archiveWriter, backupDeclaredParts, placePartRecords, stageDeclaredParts, stageFilePart } from '../utils/directoryBackup.js';
|
|
16
17
|
import { writeTreeArchive } from '../utils/treeArchive.js';
|
|
17
18
|
import { envForBackup, envKeyNames, envKeyNamesFingerprint, INSTANCE_ENV_PART, instanceEnvPath } from '../utils/instanceEnvBackup.js';
|
|
@@ -195,8 +196,20 @@ export function backupKeyNotSaved(fingerprint) {
|
|
|
195
196
|
export const ENV_KEYS_SAVED_KEY = 'ngdpbase.backup.env-keys-saved';
|
|
196
197
|
/** The title of the one notification per change of the `.env` key set (#1976). */
|
|
197
198
|
export const ENV_KEYS_CHANGED_TITLE = 'Save a copy of .env';
|
|
199
|
+
/**
|
|
200
|
+
* The config key that records an admin's confirmation that the shared-folders
|
|
201
|
+
* key is saved off the server (#2124, epic #1775 decision 1): its fingerprint,
|
|
202
|
+
* recorded when they confirm a downloaded `.env` that holds it. A different
|
|
203
|
+
* key asks again.
|
|
204
|
+
*/
|
|
205
|
+
export const SHARED_FOLDERS_KEY_SAVED_KEY = 'ngdpbase.backup.shared-folders-key-saved-fingerprint';
|
|
198
206
|
const ENV_KEYS_CHANGED_PREFIX = 'The keys in .env changed since an admin last saved it';
|
|
199
207
|
const KEY_NOT_SAVED_PREFIX = 'Save the backup key off this server';
|
|
208
|
+
const SHARED_FOLDERS_KEY_NOT_SAVED_PREFIX = 'Save .env off this server: it holds the shared-folders key';
|
|
209
|
+
/** The banner while no admin has confirmed a saved `.env` holding the current shared-folders key (#2124). */
|
|
210
|
+
export function sharedFoldersKeyNotSaved(fingerprint) {
|
|
211
|
+
return `${SHARED_FOLDERS_KEY_NOT_SAVED_PREFIX} (${SHARED_FOLDERS_KEY_ENV}, fingerprint ${fingerprint}), which opens every shared folder. Download .env on Admin → Backup and keep the copy off this server. This stays until an admin confirms it is saved`;
|
|
212
|
+
}
|
|
200
213
|
/** The banner while the instance `.env` sets other names than the ones an admin last confirmed saved (#1976): names only, never a value. */
|
|
201
214
|
export function envKeysChanged(change) {
|
|
202
215
|
const parts = [
|
|
@@ -779,15 +792,18 @@ class BackupManager extends BaseManager {
|
|
|
779
792
|
// #1876: the banner asking an admin to save the key off the server, until one confirms;
|
|
780
793
|
// then #1976's, while the .env key set differs from the one last confirmed. One at a time,
|
|
781
794
|
// and one confirmation ("I have saved .env", which holds the key) can clear both.
|
|
782
|
-
|
|
795
|
+
// #2124: then the shared-folders key's, while no saved .env holding the current one is confirmed.
|
|
796
|
+
const shared = this.sharedFoldersKeyInfo();
|
|
797
|
+
const sharedReminder = shared.set && !shared.saved && shared.fingerprint ? sharedFoldersKeyNotSaved(shared.fingerprint) : null;
|
|
798
|
+
const reminder = this.backupKeyInfo().saved ? (envReminder ?? sharedReminder) : backupKeyNotSaved(backupKeyFingerprint(key));
|
|
783
799
|
const status = this.getManagerStatus();
|
|
784
|
-
const ours = status.state === 'degraded' &&
|
|
800
|
+
const ours = status.state === 'degraded' && [KEY_NOT_SAVED_PREFIX, ENV_KEYS_CHANGED_PREFIX, SHARED_FOLDERS_KEY_NOT_SAVED_PREFIX].some((p) => status.reason?.startsWith(p));
|
|
785
801
|
if (!reminder) {
|
|
786
802
|
if (ours)
|
|
787
803
|
this.markReady();
|
|
788
804
|
}
|
|
789
805
|
else if (status.state !== 'degraded' || ours) {
|
|
790
|
-
this.markNeedsAttention(reminder, reminder === envReminder ? ENV_KEYS_SAVED_KEY : BACKUP_KEY_SAVED_KEY);
|
|
806
|
+
this.markNeedsAttention(reminder, reminder === envReminder ? ENV_KEYS_SAVED_KEY : reminder === sharedReminder ? SHARED_FOLDERS_KEY_SAVED_KEY : BACKUP_KEY_SAVED_KEY);
|
|
791
807
|
}
|
|
792
808
|
return;
|
|
793
809
|
}
|
|
@@ -876,9 +892,22 @@ class BackupManager extends BaseManager {
|
|
|
876
892
|
recorded: recorded !== null,
|
|
877
893
|
added: recorded ? names.filter((n) => !recorded.includes(n)) : [],
|
|
878
894
|
removed: recorded ? recorded.filter((n) => !names.includes(n)) : [],
|
|
879
|
-
holdsBackupKey: Boolean(key) && readEnvLine(content, BACKUP_KEY_ENV) === key
|
|
895
|
+
holdsBackupKey: Boolean(key) && readEnvLine(content, BACKUP_KEY_ENV) === key,
|
|
896
|
+
holdsSharedFoldersKey: Boolean(currentSharedFoldersKey()) && readEnvLine(content, SHARED_FOLDERS_KEY_ENV) === currentSharedFoldersKey()
|
|
880
897
|
};
|
|
881
898
|
}
|
|
899
|
+
/**
|
|
900
|
+
* The shared-folders key as Admin → Backup shows it (#2124): set or not,
|
|
901
|
+
* its fingerprint, and whether an admin confirmed a saved `.env` holding
|
|
902
|
+
* it. Never the key. Read at use, like the backup key.
|
|
903
|
+
*/
|
|
904
|
+
sharedFoldersKeyInfo() {
|
|
905
|
+
const key = currentSharedFoldersKey();
|
|
906
|
+
if (!key)
|
|
907
|
+
return { set: false, fingerprint: null, saved: false };
|
|
908
|
+
const fingerprint = sharedFoldersKeyFingerprint(key);
|
|
909
|
+
return { set: true, fingerprint, saved: this.setting(SHARED_FOLDERS_KEY_SAVED_KEY, '') === fingerprint };
|
|
910
|
+
}
|
|
882
911
|
/** The backup key as Admin → Backup shows it: set or not, its fingerprint, and whether an admin confirmed it is saved. Never the key. */
|
|
883
912
|
backupKeyInfo() {
|
|
884
913
|
const key = currentBackupKey();
|
|
@@ -930,8 +959,9 @@ class BackupManager extends BaseManager {
|
|
|
930
959
|
* With `envFingerprint` (#1976) the admin confirms the whole `.env` is
|
|
931
960
|
* saved, the one confirmation for both reminders: the names it sets are
|
|
932
961
|
* recorded (`ngdpbase.backup.env-keys-saved`), and so is the backup key
|
|
933
|
-
* when the file holds it
|
|
934
|
-
*
|
|
962
|
+
* when the file holds it, and the shared-folders key when it holds that
|
|
963
|
+
* (#2124, `ngdpbase.backup.shared-folders-key-saved-fingerprint`). Refused
|
|
964
|
+
* when the names changed since the page was loaded.
|
|
935
965
|
*/
|
|
936
966
|
async confirmBackupKeySaved(ctx, fingerprint, envFingerprint) {
|
|
937
967
|
const STALE_KEY = 'That is not the fingerprint of the current backup key; reload Admin → Backup and check the key you saved';
|
|
@@ -950,6 +980,9 @@ class BackupManager extends BaseManager {
|
|
|
950
980
|
await configManager.setProperty(BACKUP_KEY_SAVED_KEY, fingerprint, ctx);
|
|
951
981
|
if (env)
|
|
952
982
|
await configManager.setProperty(ENV_KEYS_SAVED_KEY, env.names, ctx);
|
|
983
|
+
const shared = this.sharedFoldersKeyInfo();
|
|
984
|
+
if (env?.holdsSharedFoldersKey && shared.fingerprint && !shared.saved)
|
|
985
|
+
await configManager.setProperty(SHARED_FOLDERS_KEY_SAVED_KEY, shared.fingerprint, ctx);
|
|
953
986
|
await this.checkBackupKey();
|
|
954
987
|
}
|
|
955
988
|
// ---------------------------------------------------------------------------
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
* (`ngdpbase.consent.documents`, asked at sign-up and at an interactive
|
|
11
11
|
* sign-in) and each add-on's (asked before its per-person switch turns on,
|
|
12
12
|
* and again on a version change when the person next uses it).
|
|
13
|
+
* - __The recovery words (#1928).__ A user's confirmation that they saved
|
|
14
|
+
* their recovery words (`RECOVERY_WORDS_CONSENT_SCOPE`), bound to the set
|
|
15
|
+
* of words it is about (`subject`): asked where the words are shown, and
|
|
16
|
+
* reminded by a banner — it never holds a session. An admin's confirmation
|
|
17
|
+
* that they saved the organization's shared-folders recovery words
|
|
18
|
+
* (`SHARED_FOLDERS_WORDS_CONSENT_SCOPE`, #2124) is the same kind of consent,
|
|
19
|
+
* bound to the organization's set of words.
|
|
13
20
|
*
|
|
14
21
|
* The documents themselves are pages, read when asked (`version:` and
|
|
15
22
|
* `effective:` front matter), never copied. Whether a person has accepted
|
|
@@ -93,8 +100,12 @@ declare class ConsentManager extends BaseManager {
|
|
|
93
100
|
* The documents of `scope` that `username` has not accepted at their
|
|
94
101
|
* current version — what they are to be asked. Empty when there is
|
|
95
102
|
* nothing to ask (no documents, or no consent folder).
|
|
103
|
+
*
|
|
104
|
+
* In a scope bound to a subject (#1928, the recovery words) only an
|
|
105
|
+
* acceptance about `subject` counts, and with no subject there is nothing
|
|
106
|
+
* to ask: a user with no recovery words has none to confirm.
|
|
96
107
|
*/
|
|
97
|
-
pendingFor(username: string, scope?: ConsentScope): Promise<ConsentDocument[]>;
|
|
108
|
+
pendingFor(username: string, scope?: ConsentScope, subject?: string): Promise<ConsentDocument[]>;
|
|
98
109
|
/**
|
|
99
110
|
* "Your consents" for the person `ctx` acts as: each document of core's and
|
|
100
111
|
* of each registered add-on, with their acceptance of the current version
|
|
@@ -114,10 +125,15 @@ declare class ConsentManager extends BaseManager {
|
|
|
114
125
|
* - Each acceptance is recorded as `consent-grant` (on-failure refuse)
|
|
115
126
|
* before it is stored: a failed audit record refuses the acceptance.
|
|
116
127
|
*
|
|
117
|
-
*
|
|
128
|
+
* - In a scope bound to a subject (#1928) the acceptance is about `subject`
|
|
129
|
+
* (which set of recovery words); it is required there.
|
|
130
|
+
*
|
|
131
|
+
* @param how How they were asked: `sign-up`, `sign-in` or `add-on`; for the
|
|
132
|
+
* recovery words `store-door`, `reminder` or `new-words`
|
|
133
|
+
* @param subject What the acceptance is about, in a scope bound to one
|
|
118
134
|
* @returns The records written; empty when there was nothing to accept
|
|
119
135
|
*/
|
|
120
|
-
accept(ctx: ActorContext, scope: ConsentScope, seen: ConsentSeen[], how: string): Promise<ConsentRecord[]>;
|
|
136
|
+
accept(ctx: ActorContext, scope: ConsentScope, seen: ConsentSeen[], how: string, subject?: string): Promise<ConsentRecord[]>;
|
|
121
137
|
/**
|
|
122
138
|
* The account `username` was deleted (`AuthManager.closeAccess`): its
|
|
123
139
|
* records go with it, after `consent-withdraw` names what it had accepted
|
|
@@ -32,7 +32,7 @@ import type { WikiEngine } from '../types/WikiEngine.js';
|
|
|
32
32
|
import { IContentConverter } from '../converters/IContentConverter.js';
|
|
33
33
|
import { type ZipReadLimits } from '../utils/zipArchive.js';
|
|
34
34
|
import { type JobContext } from '../context/JobContext.js';
|
|
35
|
-
import type { IceLedger, IceOriginalChoices, IceReceiver, IceRunReport, IceRunRequest, IceShim, RegisteredShim } from '../ice/types.js';
|
|
35
|
+
import type { IceLedger, IceOriginalChoices, IceReceiver, IceRunReport, IceRunRequest, IceShim, RecentImport, RegisteredShim } from '../ice/types.js';
|
|
36
36
|
/**
|
|
37
37
|
* Options for import operations
|
|
38
38
|
*/
|
|
@@ -380,6 +380,28 @@ declare class ImportManager extends BaseManager {
|
|
|
380
380
|
getImportRun(runId: string, ctx: ActorContext): IceRunReport | null;
|
|
381
381
|
/** The runs this process holds for the user `ctx` names, newest first (#2021): their own, never anyone else's. */
|
|
382
382
|
importRunsFor(ctx: ActorContext): IceRunReport[];
|
|
383
|
+
/**
|
|
384
|
+
* The user's own recent imports, file by file, read from the ledger
|
|
385
|
+
* (#2110), so the list survives a restart: newest first, across the
|
|
386
|
+
* receivers `ctx` may use, at most `ngdpbase.import.engine.recent-imports`.
|
|
387
|
+
* Only the scopes bound to this user's identity (#2013), never anyone
|
|
388
|
+
* else's; a caller's narrower scope (phr's source id) is not read. Each
|
|
389
|
+
* entry links to what it created when the receiver says this user may
|
|
390
|
+
* open it (`linkFor`). Nothing is stored for this.
|
|
391
|
+
*
|
|
392
|
+
* @throws IdentityStoreUnavailableError when the identity store cannot
|
|
393
|
+
* answer: whose entries they are cannot be told, so none is shown
|
|
394
|
+
*/
|
|
395
|
+
recentImportsFor(ctx: ActorContext): Promise<RecentImport[]>;
|
|
396
|
+
/**
|
|
397
|
+
* Is an import into this receiver confidential (#2110)? Only when its
|
|
398
|
+
* destination is encrypted: a receiver that keeps into a sealed store
|
|
399
|
+
* (`keepsEncrypted`). No receiver can declare itself so otherwise. A
|
|
400
|
+
* confidential import's file names are shown to the uploader and kept in
|
|
401
|
+
* the audit log, and never written to application logs, notices or admin
|
|
402
|
+
* screens; every other import logs them as before.
|
|
403
|
+
*/
|
|
404
|
+
isConfidential(receiverId: string): boolean;
|
|
383
405
|
/** Use this ledger instead of the file one: a test's, or a database table's. */
|
|
384
406
|
setImportLedger(ledger: IceLedger): void;
|
|
385
407
|
/**
|
|
@@ -394,6 +416,13 @@ declare class ImportManager extends BaseManager {
|
|
|
394
416
|
* land in a scope the user's other runs do not use
|
|
395
417
|
*/
|
|
396
418
|
private importScope;
|
|
419
|
+
/**
|
|
420
|
+
* Whose a scope is (#2013): the identity `@id` of the user `ctx` names, or
|
|
421
|
+
* their username when the identity store answers that they have none.
|
|
422
|
+
*
|
|
423
|
+
* @throws IdentityStoreUnavailableError when the store cannot answer
|
|
424
|
+
*/
|
|
425
|
+
private scopeOwner;
|
|
397
426
|
private newRunReport;
|
|
398
427
|
private executeRun;
|
|
399
428
|
/** What a run, or keeping at upload, works with. */
|
|
@@ -414,6 +443,12 @@ declare class ImportManager extends BaseManager {
|
|
|
414
443
|
private ledger;
|
|
415
444
|
/** What a shim is handed: the engine's guarded fetch (design rule) and its own options (D6). */
|
|
416
445
|
private shimTools;
|
|
446
|
+
/**
|
|
447
|
+
* The engine's one way out (design rule, #1139): `guardedFetch` with the
|
|
448
|
+
* site's egress policy, read per call, and its default timeout. A URL
|
|
449
|
+
* source and every shim's follow-up fetch go through here (#2109).
|
|
450
|
+
*/
|
|
451
|
+
private engineFetch;
|
|
417
452
|
/** A receiver's existing permission, asked of the PDP with the run's context (D5, P2). */
|
|
418
453
|
private permits;
|
|
419
454
|
/**
|
|
@@ -253,6 +253,17 @@ export type VaultRepairResult = ({
|
|
|
253
253
|
message: string;
|
|
254
254
|
};
|
|
255
255
|
/** The move of existing captures into the capture vault (#1505), recorded per vault it moved them out of. */
|
|
256
|
+
/** The refusal when someone without `admin-system` names a system-category (#1503, operator 2026-10-29). */
|
|
257
|
+
export declare const SYSTEM_CATEGORY_ADMIN_ONLY = "Only administrators can set a page's system category";
|
|
258
|
+
/** What a write may give a page as its system-category ({@link PageManager.systemCategoryForWrite}). */
|
|
259
|
+
export type SystemCategoryForWrite = {
|
|
260
|
+
ok: true;
|
|
261
|
+
category: string | undefined;
|
|
262
|
+
admin: boolean;
|
|
263
|
+
} | {
|
|
264
|
+
ok: false;
|
|
265
|
+
reason: string;
|
|
266
|
+
};
|
|
256
267
|
export declare const CAPTURE_VAULT_MIGRATION = "capture-vault";
|
|
257
268
|
/** The move of private pages from the default vault into their system-category's own vault (#1507). */
|
|
258
269
|
export declare const CATEGORY_VAULT_MIGRATION = "category-vaults";
|
|
@@ -429,6 +440,8 @@ declare class PageManager extends BaseManager implements CatalogSource {
|
|
|
429
440
|
*/
|
|
430
441
|
private formerTitleIndex;
|
|
431
442
|
private providerClass?;
|
|
443
|
+
/** The system-categories each shipped source ships, read once ({@link shippedSystemCategories}, #1503). */
|
|
444
|
+
private shippedCategoriesRead;
|
|
432
445
|
/**
|
|
433
446
|
* Creates a new PageManager instance
|
|
434
447
|
*
|
|
@@ -583,9 +596,48 @@ declare class PageManager extends BaseManager implements CatalogSource {
|
|
|
583
596
|
private loadSeededRecord;
|
|
584
597
|
/**
|
|
585
598
|
* The source for the required pages this release ships (#1405, #1406).
|
|
586
|
-
* Pages in a category
|
|
599
|
+
* Pages in a system-category with no `storageLocation` (never stored on the
|
|
600
|
+
* site, #1503) are excluded.
|
|
587
601
|
*/
|
|
588
602
|
requiredPagesSource(): ShippedPageSource;
|
|
603
|
+
/**
|
|
604
|
+
* The system-categories that ship pages (#1503), lower-cased: the
|
|
605
|
+
* `system-category:` values in the frontmatter of required-pages/ (read
|
|
606
|
+
* once) and of each enabled add-on's pages (read once per add-on; a page
|
|
607
|
+
* without one is `addon`, as the seed stamps it). No setting names them
|
|
608
|
+
* (operator, 2026-09-28). A file the seed would exclude is left out.
|
|
609
|
+
*
|
|
610
|
+
* Only for category questions (moving a page into one, the #1377 check):
|
|
611
|
+
* whether a page is shipped is {@link isShippedPageCopy}, per page.
|
|
612
|
+
*/
|
|
613
|
+
shippedSystemCategories(): Promise<Set<string>>;
|
|
614
|
+
/** One shipped source's system-categories, read once; a failed read is not kept, so the next call reads again. */
|
|
615
|
+
private categoriesShippedBy;
|
|
616
|
+
private readShippedSystemCategories;
|
|
617
|
+
/**
|
|
618
|
+
* Which system-category a write by `ctx` gives a page (#1503, operator
|
|
619
|
+
* 2026-10-29): only an administrator (`admin-system`, asked of the PDP) sets
|
|
620
|
+
* or changes one. Anyone else's new page takes `fallback` (else the default
|
|
621
|
+
* system-category) and an existing page keeps its own (`current`); a request
|
|
622
|
+
* naming any other is refused, never rewritten. An administrator's request
|
|
623
|
+
* comes back as given (`undefined` when it names none), for the caller's own
|
|
624
|
+
* validation. Every write path that takes a system-category from a request
|
|
625
|
+
* asks here: create, save, ingest, takeout import.
|
|
626
|
+
*
|
|
627
|
+
* @param ctx - Who is writing
|
|
628
|
+
* @param requested - The system-category the request names, if any
|
|
629
|
+
* @param current - The page's system-category now; absent for a new page
|
|
630
|
+
* @param fallback - A new page's system-category when not the default (a vault's own)
|
|
631
|
+
*/
|
|
632
|
+
systemCategoryForWrite(ctx: ActorContext, requested: unknown, current?: string | null, fallback?: string): Promise<SystemCategoryForWrite>;
|
|
633
|
+
/**
|
|
634
|
+
* Whether a page on the site is a shipped page (#1503): a copy of a shipped
|
|
635
|
+
* original, its uuid in the site's seeded-pages record, from required-pages/
|
|
636
|
+
* or an add-on's pages alike (operator, 2026-10-29). Decided per page, so a
|
|
637
|
+
* page someone creates on the site in a shipped system-category is not
|
|
638
|
+
* taken for a shipped one. Shipped pages are admin-only and never private.
|
|
639
|
+
*/
|
|
640
|
+
isShippedPageCopy(uuid: string): Promise<boolean>;
|
|
589
641
|
/**
|
|
590
642
|
* One-time access backfill for a live required page (#1411): a page with no
|
|
591
643
|
* `access` gets the default for its category, as a metadata-only save that
|
|
@@ -1654,8 +1706,6 @@ declare class PageManager extends BaseManager implements CatalogSource {
|
|
|
1654
1706
|
private roleNames;
|
|
1655
1707
|
/** Whether an index entry still holds an author or editor the conversion would change (#2097). No lookup is written. */
|
|
1656
1708
|
private authorshipNeedsConversion;
|
|
1657
|
-
/** The system-categories whose pages are shipped with the code (source `shipped`), lower-cased (#2097). */
|
|
1658
|
-
private shippedCategories;
|
|
1659
1709
|
/** `ngdpbase.page.authorship-conversion.batch-size`: pages converted between yields (#2097). */
|
|
1660
1710
|
private authorshipBatchSize;
|
|
1661
1711
|
/** Counts only, never a name (#2097): the log, and the dashboard for orphans and refused pages. */
|
|
@@ -57,6 +57,33 @@ export default class ShareManager extends BaseManager {
|
|
|
57
57
|
constructor(engine: WikiEngine);
|
|
58
58
|
initialize(): Promise<void>;
|
|
59
59
|
isEnabled(): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* The origin every share link is minted and shown with, no trailing slash.
|
|
62
|
+
*
|
|
63
|
+
* `ngdpbase.share.public-base-url` when it is set and valid: an instance
|
|
64
|
+
* whose own address is private (a LAN name) can hand out links that open
|
|
65
|
+
* from outside, through a tunnel or proxy that carries only `/share/…`.
|
|
66
|
+
* Otherwise the configured `ngdpbase.application.base-url`, else the
|
|
67
|
+
* request's own origin. Read on every call, so a change made on the admin
|
|
68
|
+
* screen applies to the next link shown.
|
|
69
|
+
*/
|
|
70
|
+
linkBaseUrl(requestOrigin: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* The one startup warning for a public origin on an instance that terminates
|
|
73
|
+
* TLS itself (#2117), or null. A plain-http connection to such an instance
|
|
74
|
+
* gets the #1163 redirect to `ngdpbase.application.base-url`, so a tunnel or
|
|
75
|
+
* proxy in front that connects over http sends every link's visitor to the
|
|
76
|
+
* site's own (possibly private) address. Whether a proxy exists, and how it
|
|
77
|
+
* connects, cannot be seen from here; the warning says what it must do.
|
|
78
|
+
*/
|
|
79
|
+
publicOriginTlsWarning(nativeTls: boolean): string | null;
|
|
80
|
+
/**
|
|
81
|
+
* `ngdpbase.share.public-base-url`, validated: an absolute http(s) URL with
|
|
82
|
+
* no path beyond `/`, no query, fragment or credentials. Unset is
|
|
83
|
+
* `{ url: null, problem: null }`; an invalid value is ignored and named in
|
|
84
|
+
* `problem`.
|
|
85
|
+
*/
|
|
86
|
+
private readPublicBaseUrl;
|
|
60
87
|
/**
|
|
61
88
|
* Issue a new share: a delegation by `issuer` of `actions` over `resources`
|
|
62
89
|
* (#1221, epic #1225).
|
|
@@ -36,22 +36,28 @@ export interface PageValidationResult extends MetadataValidationResult {
|
|
|
36
36
|
export interface ContentValidationResult {
|
|
37
37
|
warnings: string[];
|
|
38
38
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Where a category's pages come from (#1503): made on this site, shipped with
|
|
41
|
-
* the release in required-pages/, or kept in the repository's docs/ and never
|
|
42
|
-
* stored on the site. Replaces the old storageLocation words one for one
|
|
43
|
-
* (regular → site, required → shipped, github → repo).
|
|
44
|
-
*/
|
|
45
39
|
/** A new share link's maximum lifetime when a vault's system-category sets none (operator, 2026-09-30, #1388). */
|
|
46
40
|
export declare const DEFAULT_SHARE_MAX_DAYS = 15;
|
|
47
|
-
export type CategorySource = 'site' | 'shipped' | 'repo';
|
|
48
41
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
42
|
+
* Where a system-category's pages are stored on the site (#1503,
|
|
43
|
+
* docs/system-category.md "Where a page's master copy lives"):
|
|
44
|
+
*
|
|
45
|
+
* - `none`: the entry has no `storageLocation`; its pages are never stored on
|
|
46
|
+
* the site (they live in the repository's docs/), so saving one is refused
|
|
47
|
+
* and none is seeded.
|
|
48
|
+
* - `public`: a `storageLocation` without a vault; its pages can never be
|
|
49
|
+
* private.
|
|
50
|
+
* - `vault`: a `storageLocation.privatestore`; its pages may be private.
|
|
51
|
+
*/
|
|
52
|
+
export type CategoryStored = 'none' | 'public' | 'vault';
|
|
53
|
+
/**
|
|
54
|
+
* Where an entry's pages are stored. The one rule every reader of "is this
|
|
55
|
+
* system-category stored here, and can it be private" goes through. A
|
|
56
|
+
* system-category's former `source` field is never read (#1503).
|
|
51
57
|
*/
|
|
52
|
-
export declare function
|
|
53
|
-
|
|
54
|
-
} | null | undefined):
|
|
58
|
+
export declare function categoryStored(cfg: {
|
|
59
|
+
storageLocation?: unknown;
|
|
60
|
+
} | null | undefined): CategoryStored;
|
|
55
61
|
/**
|
|
56
62
|
* What is wrong with a `ngdpbase.system-category` map, one line per problem
|
|
57
63
|
* (#1504). Empty when it is sound. Startup refuses a map with problems
|
|
@@ -73,9 +79,13 @@ export interface CategoryConfig {
|
|
|
73
79
|
label: string;
|
|
74
80
|
description?: string;
|
|
75
81
|
default?: boolean;
|
|
76
|
-
source?: CategorySource;
|
|
77
82
|
enabled?: boolean;
|
|
78
83
|
key?: string;
|
|
84
|
+
/** Where its pages are stored; see {@link categoryStored}. */
|
|
85
|
+
storageLocation?: {
|
|
86
|
+
defaultstore?: string;
|
|
87
|
+
privatestore?: string;
|
|
88
|
+
};
|
|
79
89
|
/** `false`: its pages show no Comments tab and take no comment, unless a page says otherwise (#1951). */
|
|
80
90
|
comments?: boolean;
|
|
81
91
|
/** `false`: its pages show no Footnotes tab and take no footnote change, unless a page says otherwise (#1951). */
|
|
@@ -243,11 +253,12 @@ declare class ValidationManager extends BaseManager {
|
|
|
243
253
|
*/
|
|
244
254
|
getCategoryConfig(label: string): CategoryConfig | null;
|
|
245
255
|
/**
|
|
246
|
-
* Where a category's pages
|
|
256
|
+
* Where a system-category's pages are stored on the site (#1503): see
|
|
257
|
+
* {@link categoryStored}. Null for a system-category the site does not
|
|
258
|
+
* know; no rule refuses a page for that.
|
|
247
259
|
* @param {string} category - Category label
|
|
248
|
-
* @returns {CategorySource} 'site', 'shipped' or 'repo'; an unknown category is 'site'
|
|
249
260
|
*/
|
|
250
|
-
|
|
261
|
+
getCategoryStored(category: string): CategoryStored | null;
|
|
251
262
|
/**
|
|
252
263
|
* How a new page's Private box starts for this system-category and this
|
|
253
264
|
* person (#1504): the entry's `defaultPrivate` — `true`, `false`, or
|
|
@@ -8,6 +8,37 @@ export type ConsentScope = string;
|
|
|
8
8
|
export declare const CONSENT_PATH = "/consent";
|
|
9
9
|
/** The scope of core's own documents, `ngdpbase.consent.documents`. */
|
|
10
10
|
export declare const CORE_CONSENT_SCOPE = "core";
|
|
11
|
+
/**
|
|
12
|
+
* The scope of a user's confirmation that they saved their own recovery words
|
|
13
|
+
* (#1928). Not core's terms: it never holds a session. It is asked where the
|
|
14
|
+
* words are shown (the store door, and a new set of words, #1925), and a user
|
|
15
|
+
* with encrypted vaults who has not confirmed the current version for their
|
|
16
|
+
* current words is reminded by a banner.
|
|
17
|
+
*
|
|
18
|
+
* Bound to a subject: the set of words (`recoveryWordsId`), so a new set asks
|
|
19
|
+
* again.
|
|
20
|
+
*/
|
|
21
|
+
export declare const RECOVERY_WORDS_CONSENT_SCOPE = "recovery-words";
|
|
22
|
+
/** The page whose `version:` is the version of the recovery-words wording (#1928). */
|
|
23
|
+
export declare const RECOVERY_WORDS_DOCUMENT = "recovery-words";
|
|
24
|
+
/**
|
|
25
|
+
* The scope of an admin's confirmation that they saved the organization's
|
|
26
|
+
* shared-folders recovery words (#2124, epic #1775 decision 4). Like
|
|
27
|
+
* `recovery-words` it never holds a session: it is asked where the words are
|
|
28
|
+
* shown (Admin → Backup), and until the admin who was shown them confirms,
|
|
29
|
+
* holders of `admin-system` see a banner. Bound to a subject: the
|
|
30
|
+
* organization's set of words (`SharedFolderManager.recoveryWordsInfo().wordsId`),
|
|
31
|
+
* so a new set asks again.
|
|
32
|
+
*/
|
|
33
|
+
export declare const SHARED_FOLDERS_WORDS_CONSENT_SCOPE = "shared-folders-recovery-words";
|
|
34
|
+
/** The page whose `version:` is the version of the shared-folders recovery-words wording (#2124). */
|
|
35
|
+
export declare const SHARED_FOLDERS_WORDS_DOCUMENT = "shared-folders-recovery-words";
|
|
36
|
+
/** Core's scopes bound to a subject, each with its one document (#1928, #2124). */
|
|
37
|
+
export declare const SUBJECT_CONSENT_DOCUMENTS: Readonly<Record<ConsentScope, string>>;
|
|
38
|
+
/** Scopes whose acceptance is about one thing (a subject), not the account as a whole (#1928). */
|
|
39
|
+
export declare const SUBJECT_CONSENT_SCOPES: readonly ConsentScope[];
|
|
40
|
+
/** Scopes core reserves: an add-on can never register one as its slug. */
|
|
41
|
+
export declare const RESERVED_CONSENT_SCOPES: readonly ConsentScope[];
|
|
11
42
|
/**
|
|
12
43
|
* A document a person is asked to accept, as it stands now: a page whose
|
|
13
44
|
* front matter carries `version:` and `effective:`. Read from the page each
|
|
@@ -46,7 +77,18 @@ export interface ConsentRecord {
|
|
|
46
77
|
* account that takes the same username never inherits it.
|
|
47
78
|
*/
|
|
48
79
|
account?: string;
|
|
49
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* What the acceptance is about, in a scope bound to a subject (#1928): for
|
|
82
|
+
* the recovery words, which set of words (`recoveryWordsId`). Absent
|
|
83
|
+
* elsewhere. A new subject (a new set of words) asks again.
|
|
84
|
+
*/
|
|
85
|
+
subject?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Who and how: the account, where from, and the way they were asked
|
|
88
|
+
* (`sign-up`, `sign-in`, `add-on`; for the recovery words `store-door`,
|
|
89
|
+
* `reminder`, and `new-words` once #1925 makes a new set; for the
|
|
90
|
+
* shared-folders words `setup` and `reminder`).
|
|
91
|
+
*/
|
|
50
92
|
context: {
|
|
51
93
|
by: string;
|
|
52
94
|
how: string;
|
package/lib/src/types/Consent.js
CHANGED
|
@@ -6,6 +6,40 @@
|
|
|
6
6
|
export const CONSENT_PATH = '/consent';
|
|
7
7
|
/** The scope of core's own documents, `ngdpbase.consent.documents`. */
|
|
8
8
|
export const CORE_CONSENT_SCOPE = 'core';
|
|
9
|
+
/**
|
|
10
|
+
* The scope of a user's confirmation that they saved their own recovery words
|
|
11
|
+
* (#1928). Not core's terms: it never holds a session. It is asked where the
|
|
12
|
+
* words are shown (the store door, and a new set of words, #1925), and a user
|
|
13
|
+
* with encrypted vaults who has not confirmed the current version for their
|
|
14
|
+
* current words is reminded by a banner.
|
|
15
|
+
*
|
|
16
|
+
* Bound to a subject: the set of words (`recoveryWordsId`), so a new set asks
|
|
17
|
+
* again.
|
|
18
|
+
*/
|
|
19
|
+
export const RECOVERY_WORDS_CONSENT_SCOPE = 'recovery-words';
|
|
20
|
+
/** The page whose `version:` is the version of the recovery-words wording (#1928). */
|
|
21
|
+
export const RECOVERY_WORDS_DOCUMENT = 'recovery-words';
|
|
22
|
+
/**
|
|
23
|
+
* The scope of an admin's confirmation that they saved the organization's
|
|
24
|
+
* shared-folders recovery words (#2124, epic #1775 decision 4). Like
|
|
25
|
+
* `recovery-words` it never holds a session: it is asked where the words are
|
|
26
|
+
* shown (Admin → Backup), and until the admin who was shown them confirms,
|
|
27
|
+
* holders of `admin-system` see a banner. Bound to a subject: the
|
|
28
|
+
* organization's set of words (`SharedFolderManager.recoveryWordsInfo().wordsId`),
|
|
29
|
+
* so a new set asks again.
|
|
30
|
+
*/
|
|
31
|
+
export const SHARED_FOLDERS_WORDS_CONSENT_SCOPE = 'shared-folders-recovery-words';
|
|
32
|
+
/** The page whose `version:` is the version of the shared-folders recovery-words wording (#2124). */
|
|
33
|
+
export const SHARED_FOLDERS_WORDS_DOCUMENT = 'shared-folders-recovery-words';
|
|
34
|
+
/** Core's scopes bound to a subject, each with its one document (#1928, #2124). */
|
|
35
|
+
export const SUBJECT_CONSENT_DOCUMENTS = {
|
|
36
|
+
[RECOVERY_WORDS_CONSENT_SCOPE]: RECOVERY_WORDS_DOCUMENT,
|
|
37
|
+
[SHARED_FOLDERS_WORDS_CONSENT_SCOPE]: SHARED_FOLDERS_WORDS_DOCUMENT
|
|
38
|
+
};
|
|
39
|
+
/** Scopes whose acceptance is about one thing (a subject), not the account as a whole (#1928). */
|
|
40
|
+
export const SUBJECT_CONSENT_SCOPES = Object.keys(SUBJECT_CONSENT_DOCUMENTS);
|
|
41
|
+
/** Scopes core reserves: an add-on can never register one as its slug. */
|
|
42
|
+
export const RESERVED_CONSENT_SCOPES = [CORE_CONSENT_SCOPE, ...SUBJECT_CONSENT_SCOPES];
|
|
9
43
|
/**
|
|
10
44
|
* Thrown when a per-person add-on is turned on before its documents are
|
|
11
45
|
* accepted (#1603). `documents` names what is still to accept.
|
|
@@ -166,6 +166,12 @@ export declare const AUDIT_EVENT: {
|
|
|
166
166
|
readonly SHARE_EXTEND: "share-extend";
|
|
167
167
|
/** Share link revoked; pairs with token-revoke */
|
|
168
168
|
readonly SHARE_REVOKE: "share-revoke";
|
|
169
|
+
/** A shared folder's own key was made (#2124): which folder and who, never the key. Recorded before the key is saved, which cannot proceed without it */
|
|
170
|
+
readonly SHARED_FOLDER_KEY_CREATE: "shared-folder-key-create";
|
|
171
|
+
/** The organization's 12 recovery words for shared folders were made and saved (#2124): the admin who was shown them, and which set (an id, never the words). Recorded before the words are saved, which cannot proceed without it */
|
|
172
|
+
readonly SHARED_FOLDER_WORDS_CREATE: "shared-folder-words-create";
|
|
173
|
+
/** An administrator discarded the organization's shared-folders recovery words before the admin shown them confirmed saving them (#2124): who, which set (an id) and who it was shown to. Recorded before the words are removed, which cannot proceed without it */
|
|
174
|
+
readonly SHARED_FOLDER_WORDS_DISCARD: "shared-folder-words-discard";
|
|
169
175
|
/** Sign-in approval: the browser that was waiting gave up the sign-in (#1532) */
|
|
170
176
|
readonly SIGNINAPPROVAL_ABANDON: "signinapproval-abandon";
|
|
171
177
|
/** Sign-in approval: the user pressed Approve (#1532) */
|
|
@@ -173,6 +173,12 @@ export const AUDIT_EVENT = {
|
|
|
173
173
|
SHARE_EXTEND: 'share-extend',
|
|
174
174
|
/** Share link revoked; pairs with token-revoke */
|
|
175
175
|
SHARE_REVOKE: 'share-revoke',
|
|
176
|
+
/** A shared folder's own key was made (#2124): which folder and who, never the key. Recorded before the key is saved, which cannot proceed without it */
|
|
177
|
+
SHARED_FOLDER_KEY_CREATE: 'shared-folder-key-create',
|
|
178
|
+
/** The organization's 12 recovery words for shared folders were made and saved (#2124): the admin who was shown them, and which set (an id, never the words). Recorded before the words are saved, which cannot proceed without it */
|
|
179
|
+
SHARED_FOLDER_WORDS_CREATE: 'shared-folder-words-create',
|
|
180
|
+
/** An administrator discarded the organization's shared-folders recovery words before the admin shown them confirmed saving them (#2124): who, which set (an id) and who it was shown to. Recorded before the words are removed, which cannot proceed without it */
|
|
181
|
+
SHARED_FOLDER_WORDS_DISCARD: 'shared-folder-words-discard',
|
|
176
182
|
/** Sign-in approval: the browser that was waiting gave up the sign-in (#1532) */
|
|
177
183
|
SIGNINAPPROVAL_ABANDON: 'signinapproval-abandon',
|
|
178
184
|
/** Sign-in approval: the user pressed Approve (#1532) */
|
|
@@ -78,9 +78,9 @@ export declare function sealKekCopy(recipient: LinkPublicKey, kek: Buffer): Prom
|
|
|
78
78
|
* their private key from `envelope`, which opens `copy`. Throws when any step
|
|
79
79
|
* fails; the private key never leaves this function.
|
|
80
80
|
*/
|
|
81
|
-
export declare function openKekCopy(envelope: UserKeyEnvelope, ownKek: Buffer, copy: Lockbox): Promise<Buffer>;
|
|
81
|
+
export declare function openKekCopy(envelope: Pick<UserKeyEnvelope, 'personKey'>, ownKek: Buffer, copy: Lockbox): Promise<Buffer>;
|
|
82
82
|
export declare function unwrapKekWithPassword(envelope: UserKeyEnvelope, password: string): Promise<Buffer>;
|
|
83
|
-
export declare function unwrapKekWithMnemonic(envelope: UserKeyEnvelope, mnemonic: string): Buffer;
|
|
83
|
+
export declare function unwrapKekWithMnemonic(envelope: Pick<UserKeyEnvelope, 'recoveryWrap'>, mnemonic: string): Buffer;
|
|
84
84
|
/**
|
|
85
85
|
* A fresh set of 12 recovery words for a KEK that has not been committed yet
|
|
86
86
|
* (#1414): the store door's words screen, where a failed confirmation discards
|
|
@@ -91,6 +91,14 @@ export declare function newRecoveryWords(kek: Buffer): {
|
|
|
91
91
|
mnemonic: string;
|
|
92
92
|
recoveryWrap: WrappedBlob;
|
|
93
93
|
};
|
|
94
|
+
/**
|
|
95
|
+
* Which set of recovery words an envelope holds (#1928), as a short id: a
|
|
96
|
+
* digest of the recovery wrap, which a new set of words replaces and nothing
|
|
97
|
+
* else changes (a password change or a new person key keep it). Reveals
|
|
98
|
+
* nothing about the words or the key; it is what a recorded "I saved my
|
|
99
|
+
* recovery words" is bound to, so a new set asks again.
|
|
100
|
+
*/
|
|
101
|
+
export declare function recoveryWordsId(envelope: Pick<UserKeyEnvelope, 'recoveryWrap'>): string;
|
|
94
102
|
/** True when two phrases are the same 12 words, compared as the recovery wrap reads them. */
|
|
95
103
|
export declare function sameMnemonic(entered: string, expected: string): boolean;
|
|
96
104
|
export declare function rewrapPassword(envelope: UserKeyEnvelope, oldPassword: string, newPassword: string, opts?: {
|
|
@@ -52,6 +52,8 @@ export declare function unlockPrivateStoresWithMnemonic(args: {
|
|
|
52
52
|
}): Promise<boolean>;
|
|
53
53
|
/** Whether `username` has keys at all, i.e. a store that can be encrypted. */
|
|
54
54
|
export declare function hasUserKeys(pagesDirectory: string, username: string): Promise<boolean>;
|
|
55
|
+
/** Which set of recovery words `username` has (#1928, `recoveryWordsId`), or null when they have no keys. */
|
|
56
|
+
export declare function recoveryWordsIdOf(pagesDirectory: string, username: string): Promise<string | null>;
|
|
55
57
|
/**
|
|
56
58
|
* The public half of `username`'s key pair (#1762 slice 5), or null when they
|
|
57
59
|
* have no keys or no pair yet. Public: anyone may seal to it.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* recipient's browser opens each lockbox with the private key from the link.
|
|
12
12
|
*
|
|
13
13
|
* The algorithm is WebCrypto's own, so the server (Node's `crypto.webcrypto`)
|
|
14
|
-
* and the browser (`window.crypto.subtle`, public/
|
|
14
|
+
* and the browser (`window.crypto.subtle`, public/share/_assets/share-viewer.js) run the
|
|
15
15
|
* same steps: ECDH on P-256 between a fresh ephemeral key and the link's key,
|
|
16
16
|
* HKDF-SHA-256 over the shared secret, AES-256-GCM over the content.
|
|
17
17
|
*/
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The environment variable holding the shared-folders key. Never a config key: config is readable from the admin screens. */
|
|
2
|
+
export declare const SHARED_FOLDERS_KEY_ENV = "NGDPBASE_SHARED_FOLDERS_KEY";
|
|
3
|
+
/** The shared-folders key as set now, trimmed; '' when there is none. Never logged. */
|
|
4
|
+
export declare function currentSharedFoldersKey(): string;
|
|
5
|
+
/**
|
|
6
|
+
* A short fingerprint of the shared-folders key: the first 16 hex digits of an
|
|
7
|
+
* HMAC of a fixed label under the key, as for the backup key. Shown on Admin →
|
|
8
|
+
* Backup and recorded with the wrapped folder keys, so a different key says so
|
|
9
|
+
* plainly; it never reveals the key.
|
|
10
|
+
*/
|
|
11
|
+
export declare function sharedFoldersKeyFingerprint(key: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* The 32-byte key the folder keys are wrapped under, derived from the `.env`
|
|
14
|
+
* value (HKDF-SHA256, one HMAC round: cheap, never a stall). Derived, not
|
|
15
|
+
* decoded, so a value an operator set themselves in any form still works.
|
|
16
|
+
*/
|
|
17
|
+
export declare function sharedFoldersWrapKey(key: string): Buffer;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared-folders key (#2124, epic #1775 decision 1): the organization's
|
|
3
|
+
* server-held key, `NGDPBASE_SHARED_FOLDERS_KEY`, in the instance `.env`
|
|
4
|
+
* beside the backup, database and credentials keys. Generated on first start
|
|
5
|
+
* (`bootstrap-env.ts`), read at startup by SharedFolderManager, which wraps
|
|
6
|
+
* every shared folder's own key under it. One declaration here, with no
|
|
7
|
+
* dependencies, so the boot can name it without loading the manager, and
|
|
8
|
+
* BackupManager can fingerprint it for the "saved off the server" check.
|
|
9
|
+
*/
|
|
10
|
+
import { createHmac, hkdfSync } from 'node:crypto';
|
|
11
|
+
/** The environment variable holding the shared-folders key. Never a config key: config is readable from the admin screens. */
|
|
12
|
+
export const SHARED_FOLDERS_KEY_ENV = 'NGDPBASE_SHARED_FOLDERS_KEY';
|
|
13
|
+
/** The shared-folders key as set now, trimmed; '' when there is none. Never logged. */
|
|
14
|
+
export function currentSharedFoldersKey() {
|
|
15
|
+
return (process.env[SHARED_FOLDERS_KEY_ENV] ?? '').trim();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A short fingerprint of the shared-folders key: the first 16 hex digits of an
|
|
19
|
+
* HMAC of a fixed label under the key, as for the backup key. Shown on Admin →
|
|
20
|
+
* Backup and recorded with the wrapped folder keys, so a different key says so
|
|
21
|
+
* plainly; it never reveals the key.
|
|
22
|
+
*/
|
|
23
|
+
export function sharedFoldersKeyFingerprint(key) {
|
|
24
|
+
return createHmac('sha256', key).update('ngdpbase shared folders key fingerprint v1').digest('hex').slice(0, 16);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The 32-byte key the folder keys are wrapped under, derived from the `.env`
|
|
28
|
+
* value (HKDF-SHA256, one HMAC round: cheap, never a stall). Derived, not
|
|
29
|
+
* decoded, so a value an operator set themselves in any form still works.
|
|
30
|
+
*/
|
|
31
|
+
export function sharedFoldersWrapKey(key) {
|
|
32
|
+
return Buffer.from(hkdfSync('sha256', key, Buffer.alloc(0), 'ngdpbase shared folders wrap key v1', 32));
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jwilleke/ngdpbase-types",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.39.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",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"dotenv": "^18.0.9",
|
|
45
45
|
"express": "^5.3.0",
|
|
46
46
|
"express-session": "^1.19.0",
|
|
47
|
+
"fast-xml-parser": "^5.11.2",
|
|
47
48
|
"fs-extra": "^11.4.1",
|
|
48
49
|
"ipaddr.js": "^2.5.0",
|
|
49
50
|
"jose": "^6.2.13",
|