@jwilleke/ngdpbase-types 5.37.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.
Files changed (32) hide show
  1. package/lib/packages/ngdpbase-types/index.d.ts +2 -1
  2. package/lib/src/ice/ledger.d.ts +2 -0
  3. package/lib/src/ice/runImport.d.ts +6 -0
  4. package/lib/src/ice/shims/csv.d.ts +59 -0
  5. package/lib/src/ice/shims/dotpath.d.ts +7 -0
  6. package/lib/src/ice/shims/geojson.d.ts +19 -0
  7. package/lib/src/ice/shims/index.d.ts +16 -0
  8. package/lib/src/ice/shims/record.d.ts +68 -0
  9. package/lib/src/ice/shims/restjson.d.ts +14 -0
  10. package/lib/src/ice/shims/rss-atom.d.ts +22 -0
  11. package/lib/src/ice/shims/xml-index.d.ts +32 -0
  12. package/lib/src/ice/shims/xml.d.ts +30 -0
  13. package/lib/src/ice/types.d.ts +109 -7
  14. package/lib/src/managers/AddonsManager.d.ts +50 -6
  15. package/lib/src/managers/BackupManager.d.ts +20 -2
  16. package/lib/src/managers/BackupManager.js +39 -6
  17. package/lib/src/managers/ConsentManager.d.ts +19 -3
  18. package/lib/src/managers/ImportManager.d.ts +36 -1
  19. package/lib/src/managers/PageManager.d.ts +53 -3
  20. package/lib/src/managers/ShareManager.d.ts +27 -0
  21. package/lib/src/managers/ValidationManager.d.ts +27 -16
  22. package/lib/src/security/permissions.generated.d.ts +2 -0
  23. package/lib/src/types/Consent.d.ts +43 -1
  24. package/lib/src/types/Consent.js +34 -0
  25. package/lib/src/utils/auditEventNames.generated.d.ts +8 -0
  26. package/lib/src/utils/auditEventNames.generated.js +8 -0
  27. package/lib/src/utils/privateStoreCrypto.d.ts +10 -2
  28. package/lib/src/utils/privateStoreUnlock.d.ts +2 -0
  29. package/lib/src/utils/shareLockbox.d.ts +1 -1
  30. package/lib/src/utils/sharedFoldersKey.d.ts +17 -0
  31. package/lib/src/utils/sharedFoldersKey.js +33 -0
  32. package/package.json +2 -1
@@ -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. Refused when the names changed since the page
487
- * was loaded.
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
- const reminder = this.backupKeyInfo().saved ? envReminder : backupKeyNotSaved(backupKeyFingerprint(key));
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' && (status.reason?.startsWith(KEY_NOT_SAVED_PREFIX) || status.reason?.startsWith(ENV_KEYS_CHANGED_PREFIX));
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. Refused when the names changed since the page
934
- * was loaded.
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
- * @param how How they were asked: `sign-up`, `sign-in` or `add-on`
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 whose source is `repo` are excluded.
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
- * A category entry's source. An entry without a valid one is a site category.
50
- * The one rule every reader of a category's source goes through.
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 categorySource(cfg: {
53
- source?: string;
54
- } | null | undefined): CategorySource;
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 come from (#1503)
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
- getCategorySource(category: string): CategorySource;
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
@@ -22,6 +22,8 @@ export type CorePermission =
22
22
  | 'comment-create'
23
23
  /** Change any configuration setting, and download a backup (#1525). Admin only by default; asks for a fresh sign-in when listed in ngdpbase.auth.step-up */
24
24
  | 'config-manage'
25
+ /** Open the Executive Hub (/executive): the organization's summary cards and the requests waiting for the board (#2093). Held by the executive role; deciding a request still needs that request's own permission */
26
+ | 'executive-read'
25
27
  /** Create, edit, delete and link identity records, of users and contacts alike, and organizations, and an identity's affiliations, through IdentityManager and OrganizationManager (#1930). Reading them is identity-read. Was contact-manage until #2038; a policy naming that is still read as this for one release. Admins by default; an add-on grants it to its own roles in its own policy */
26
28
  | 'identity-manage'
27
29
  /** List, search and view identity records, of users and contacts alike, and organizations, and their affiliations, through IdentityManager and OrganizationManager, and the /admin/contacts and /admin/organizations views (#1930). Was contact-read until #2038; a policy naming that is still read as this for one release. Admins and demo admins by default; an add-on grants it to its own roles in its own policy */
@@ -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
- /** Who and how: the account, where from, and the way they were asked (`sign-up`, `sign-in`, `add-on`). */
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;
@@ -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.
@@ -58,6 +58,8 @@ export declare const AUDIT_EVENT: {
58
58
  readonly EMAILCHANGE_CONFIRM: "emailchange-confirm";
59
59
  /** A user asked to change their own email: a confirmation link was sent to the new address (#2073). Names only, never an address */
60
60
  readonly EMAILCHANGE_REQUEST: "emailchange-request";
61
+ /** Executive Hub read; not recorded, read volume */
62
+ readonly EXECUTIVE_READ: "executive-read";
61
63
  /** A page's footnote list changed — action add, import, transfer, update or delete; page content written on someone's behalf (#1233) */
62
64
  readonly FOOTNOTE_EDIT: "footnote-edit";
63
65
  /** Identity record or organization created, changed, deleted or linked to an account (recorded as contact-manage before #2038); metadata names the kind (person or organization), the operation and the fields changed, never their values */
@@ -164,6 +166,12 @@ export declare const AUDIT_EVENT: {
164
166
  readonly SHARE_EXTEND: "share-extend";
165
167
  /** Share link revoked; pairs with token-revoke */
166
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";
167
175
  /** Sign-in approval: the browser that was waiting gave up the sign-in (#1532) */
168
176
  readonly SIGNINAPPROVAL_ABANDON: "signinapproval-abandon";
169
177
  /** Sign-in approval: the user pressed Approve (#1532) */
@@ -65,6 +65,8 @@ export const AUDIT_EVENT = {
65
65
  EMAILCHANGE_CONFIRM: 'emailchange-confirm',
66
66
  /** A user asked to change their own email: a confirmation link was sent to the new address (#2073). Names only, never an address */
67
67
  EMAILCHANGE_REQUEST: 'emailchange-request',
68
+ /** Executive Hub read; not recorded, read volume */
69
+ EXECUTIVE_READ: 'executive-read',
68
70
  /** A page's footnote list changed — action add, import, transfer, update or delete; page content written on someone's behalf (#1233) */
69
71
  FOOTNOTE_EDIT: 'footnote-edit',
70
72
  /** Identity record or organization created, changed, deleted or linked to an account (recorded as contact-manage before #2038); metadata names the kind (person or organization), the operation and the fields changed, never their values */
@@ -171,6 +173,12 @@ export const AUDIT_EVENT = {
171
173
  SHARE_EXTEND: 'share-extend',
172
174
  /** Share link revoked; pairs with token-revoke */
173
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',
174
182
  /** Sign-in approval: the browser that was waiting gave up the sign-in (#1532) */
175
183
  SIGNINAPPROVAL_ABANDON: 'signinapproval-abandon',
176
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?: {