@jwilleke/ngdpbase-types 5.42.0 → 5.43.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 (40) hide show
  1. package/lib/src/context/ApiContext.js +1 -1
  2. package/lib/src/context/BaseContext.d.ts +20 -20
  3. package/lib/src/context/BaseContext.js +41 -24
  4. package/lib/src/context/JobContext.d.ts +31 -0
  5. package/lib/src/context/JobContext.js +34 -1
  6. package/lib/src/managers/AddonsManager.d.ts +0 -28
  7. package/lib/src/managers/AgentTokenManager.d.ts +8 -0
  8. package/lib/src/managers/ConfigurationManager.d.ts +15 -0
  9. package/lib/src/managers/PageManager.d.ts +185 -23
  10. package/lib/src/managers/PolicyEvaluator.d.ts +99 -24
  11. package/lib/src/managers/ShareManager.d.ts +3 -2
  12. package/lib/src/managers/UserManager.d.ts +19 -5
  13. package/lib/src/managers/ValidationManager.d.ts +3 -1
  14. package/lib/src/providers/AgentTokenAuthProvider.js +1 -1
  15. package/lib/src/providers/BaseAuthProvider.d.ts +2 -0
  16. package/lib/src/security/PolicyDecisionPoint.d.ts +57 -1
  17. package/lib/src/security/PolicyInformationPoint.d.ts +86 -6
  18. package/lib/src/security/permissions.generated.d.ts +8 -0
  19. package/lib/src/types/Page.d.ts +0 -7
  20. package/lib/src/types/Policy.d.ts +67 -6
  21. package/lib/src/types/Policy.js +6 -0
  22. package/lib/src/types/Provider.d.ts +12 -0
  23. package/lib/src/types/User.d.ts +6 -0
  24. package/lib/src/utils/addonPageSync.d.ts +0 -12
  25. package/lib/src/utils/auditEventNames.generated.d.ts +2 -0
  26. package/lib/src/utils/auditEventNames.generated.js +2 -0
  27. package/lib/src/utils/auditEvents.d.ts +2 -0
  28. package/lib/src/utils/auditEvents.js +3 -1
  29. package/lib/src/utils/frontmatterAccess.d.ts +15 -24
  30. package/lib/src/utils/legacyStoreKeys.d.ts +16 -0
  31. package/lib/src/utils/pageWritePolicies.d.ts +55 -0
  32. package/lib/src/utils/permissionCatalog.d.ts +2 -0
  33. package/lib/src/utils/permissionCatalog.js +19 -0
  34. package/lib/src/utils/policyAttributes.d.ts +141 -0
  35. package/lib/src/utils/policyAttributes.js +219 -0
  36. package/lib/src/utils/privateStoreAccess.d.ts +24 -4
  37. package/lib/src/utils/privateStoreAccess.js +19 -4
  38. package/lib/src/utils/username.d.ts +13 -2
  39. package/lib/src/utils/username.js +18 -3
  40. package/package.json +1 -1
@@ -97,6 +97,26 @@ export interface PageSaveOptions {
97
97
  * doc says so.
98
98
  */
99
99
  preserveEditor?: boolean;
100
+ /**
101
+ * The user a NEW page is made for, by a system job (#1815): they become its
102
+ * author and editor, where a create otherwise takes both from the acting
103
+ * context. For the pages the system makes on a user's behalf — the profile
104
+ * page at sign-up, the new profile page of a rename — so the job's
105
+ * `page-create`, granted in policy, decides the write, and the page is
106
+ * still the user's (author-lock leaves it to them). Refused from any context
107
+ * but the system principal's job, and refused for a page that already
108
+ * exists (`PageWriteRefusedError`, `page-create`): the job never edits
109
+ * that page instead.
110
+ */
111
+ createdFor?: string;
112
+ /**
113
+ * The author of a page an add-on pushes (#2255): `addon:<slug>`. A new copy
114
+ * is authored by the add-on, and a copy seeded before #2255, authored by
115
+ * the system principal, is re-stamped with it. Taken only from the system
116
+ * principal's job and only in the `addon:<slug>` form; ignored otherwise.
117
+ * Any other author is kept, as on every edit.
118
+ */
119
+ shippedAuthor?: string;
100
120
  /**
101
121
  * Request-level detail the manager cannot see, for the audit record (#1121).
102
122
  *
@@ -199,11 +219,25 @@ export declare class PageAudienceRefusedError extends Error {
199
219
  readonly pageName: string;
200
220
  constructor(pageName: string);
201
221
  }
222
+ /** The permission a page write is decided on (#1815). */
223
+ export type PageWriteAction = 'page-create' | 'page-edit' | 'page-delete' | 'page-rename';
224
+ /**
225
+ * A write refused by the write door (#1815): the context may not create,
226
+ * edit or delete this page, decided by policy, the page's own rules and the
227
+ * vault container rule. Nothing was written. Routes answer it with 403. The
228
+ * message names no page, so a private title never reaches a log (#2178).
229
+ */
230
+ export declare class PageWriteRefusedError extends Error {
231
+ readonly pageName: string;
232
+ readonly action: PageWriteAction;
233
+ constructor(pageName: string, action: PageWriteAction);
234
+ }
202
235
  export declare class PageContentValidationError extends Error {
203
236
  readonly validationErrors: FilterValidationError[];
204
237
  constructor(pageName: string, validationErrors: FilterValidationError[]);
205
238
  }
206
239
  import { type PageAction } from '../utils/pageUrl.js';
240
+ import { type VaultSealState } from '../utils/privateStoreMeta.js';
207
241
  /** One vault page with data outside its vault (#1842), as the repair actions offer it. */
208
242
  export interface VaultRepairItem {
209
243
  owner: string;
@@ -261,6 +295,9 @@ export type VaultRepairResult = ({
261
295
  /** The move of existing captures into the capture vault (#1505), recorded per vault it moved them out of. */
262
296
  /** The refusal when someone without `admin-system` names a system-category (#1503, operator 2026-10-29). */
263
297
  export declare const SYSTEM_CATEGORY_ADMIN_ONLY = "Only administrators can set a page's system category";
298
+ /** The system-category of a page in a shared folder (#2255): given when it is created there, by nothing else. */
299
+ export declare const SHARED_CATEGORY = "shared";
300
+ export declare const SHARED_CATEGORY_REFUSAL = "A page is in the shared system category only when it is created in a shared folder";
264
301
  /** What a write may give a page as its system-category ({@link PageManager.systemCategoryForWrite}). */
265
302
  export type SystemCategoryForWrite = {
266
303
  ok: true;
@@ -327,6 +364,11 @@ export interface ShippedPageSource {
327
364
  exclude?: (data: Record<string, unknown>) => string | undefined;
328
365
  /** Metadata the source adds to every page it writes (e.g. `addon`, a default category) */
329
366
  extraMetadata?: (data: Record<string, unknown>) => Record<string, unknown>;
367
+ /**
368
+ * The author of the pages it writes, for an add-on's (#2255): `addon:<slug>`
369
+ * (`SavePageOptions.shippedAuthor`). Absent, they are the system principal's.
370
+ */
371
+ author?: string;
330
372
  /**
331
373
  * Called by the boot seed for a source page this site already holds, with the
332
374
  * live page and the parsed source — the addon seed's metadata tidy-ups and
@@ -622,8 +664,10 @@ declare class PageManager extends BaseManager implements CatalogSource {
622
664
  private readShippedSystemCategories;
623
665
  /**
624
666
  * Which system-category a write by `ctx` gives a page (#1503, operator
625
- * 2026-10-29): only an administrator (`admin-system`, asked of the PDP) sets
626
- * or changes one. Anyone else's new page takes `fallback` (else the default
667
+ * 2026-10-29): only a holder of `page-categorize` (#2255; shipped to admin)
668
+ * sets or changes one. `shared` is set by nobody: a page created in a shared
669
+ * folder gets it at the write door (`savePage`), and a request naming it for
670
+ * any other page is refused, `page-categorize` or not. Anyone else's new page takes `fallback` (else the default
627
671
  * system-category) and an existing page keeps its own (`current`); a request
628
672
  * naming any other is refused, never rewritten. An administrator's request
629
673
  * comes back as given (`undefined` when it names none), for the caller's own
@@ -644,16 +688,10 @@ declare class PageManager extends BaseManager implements CatalogSource {
644
688
  * taken for a shipped one. Shipped pages are admin-only and never private.
645
689
  */
646
690
  isShippedPageCopy(uuid: string): Promise<boolean>;
647
- /**
648
- * One-time access backfill for a live required page (#1411): a page with no
649
- * `access` gets the default for its category, as a metadata-only save that
650
- * keeps `lastModified`. A page that has any `access` — an operator's choice —
651
- * is left alone.
652
- */
653
- private backfillShippedPageAccess;
654
691
  /**
655
692
  * The source for the pages an addon ships (#1406). Pages it writes carry
656
- * `addon` and, when the source names none, the `addon` category (#931).
693
+ * `addon`, the add-on as author (`addon:<slug>`, #2255) and, when the source
694
+ * names none, the `addon` category (#931).
657
695
  *
658
696
  * @param addonName - The addon's name
659
697
  * @param pagesDir - The addon's `pages/` folder
@@ -926,6 +964,82 @@ declare class PageManager extends BaseManager implements CatalogSource {
926
964
  * @throws when `ctx` may not file into the page's folder
927
965
  */
928
966
  private assertMayFileInSharedFolder;
967
+ /**
968
+ * May `ctx` edit or delete this existing page (#1815)? The page decision
969
+ * every route asks (`canUserAccessPage`), with the page's metadata, so the
970
+ * delegation ceilings, the vault container rule, author-lock, the page's own
971
+ * rules and policy all count, in that one evaluator. `record` false asks it
972
+ * as an affordance, which records nothing (#1668). Without a PIP nothing is
973
+ * writable.
974
+ */
975
+ private mayChangeExisting;
976
+ /**
977
+ * May `ctx` create a page at `destination` (#1815)? In a vault, the
978
+ * container rule and then `page-create` with the vault (`permitsInVault`;
979
+ * a shared folder by its own permission). A public page, `page-create`
980
+ * asked of the PDP about the page (#2255: the PDP reads its attributes from
981
+ * the PIP). The same rule as `hasPermissionOn`.
982
+ *
983
+ * @returns `permit`, and the `policyIds` of the `page-create` decision when
984
+ * one was made (#2272); a vault page is decided by the container rule and
985
+ * `permitsInVault`, which name none.
986
+ */
987
+ private decideCreateAt;
988
+ /**
989
+ * Where a save puts the page (#1815, decision 7): the private name the
990
+ * provider will store it under, or its public title. Read from the page's
991
+ * own `private` / `store` frontmatter, the rule the provider's placement
992
+ * follows: a private name stays private unless the save says
993
+ * `private: false`; a public title goes into a vault when it says
994
+ * `private: true`, the owner's and the store it names, else the vault the
995
+ * page is in now, else the default vault.
996
+ *
997
+ * @param owner - The username whose vault a public title would go into: the
998
+ * page's author (for a new page, whoever it is made by or for)
999
+ */
1000
+ private writeDestination;
1001
+ /** The container a page name is in: its vault, or the public space. */
1002
+ private static containerOf;
1003
+ /**
1004
+ * The write door's decision for a save (#1815, operator 2026-11-04). An
1005
+ * existing page is edited only by a context the page decision lets edit it
1006
+ * ({@link mayChangeExisting}); a page arriving in a container it is not in
1007
+ * now — a new page, or a move into or out of a vault — is also created
1008
+ * there ({@link decideCreateAt}). Whatever route the write came by: the route's
1009
+ * own check is an early refusal, this is the one that counts.
1010
+ *
1011
+ * @throws PageWriteRefusedError when either is refused; nothing is written
1012
+ */
1013
+ private assertMaySave;
1014
+ /**
1015
+ * Record a refused `page-create` as `authorization-deny` (#1815, security
1016
+ * review L1). The edit and delete refusals are recorded by the page
1017
+ * decision (`canUserAccessPage`); a create is decided by the PDP, which
1018
+ * records nothing, so the door records it here: core records every refusal.
1019
+ * Standard tier, `continue` on failure: the refusal stands either way.
1020
+ *
1021
+ * @param policyIds - The policies of the `page-create` decision that refused
1022
+ * (#2272). Only a refusal that decision made passes any; one no policy
1023
+ * decided (`createdFor` misuse) leaves the field off the record.
1024
+ * @param conditionNotMet - Allows of that decision that matched but whose condition was not met (#2288).
1025
+ */
1026
+ private recordCreateRefusal;
1027
+ /** `shippedAuthor` when the system principal's job gives it in the `addon:<slug>` form (#2255); otherwise nothing. */
1028
+ private shippedAuthorFor;
1029
+ /** Is `ctx` a job of the system principal (#631) — the only context that may make a page for someone else (#1815)? */
1030
+ private isSystemJob;
1031
+ /** The username a page's stored author names (#2097), or null. */
1032
+ private authorUsername;
1033
+ /**
1034
+ * Would `ctx` be allowed to edit this page — asked only to decide what to
1035
+ * offer or report, never before writing it (#1815). The same decision as
1036
+ * the write door, as an affordance: it records nothing (#1668). Saving still
1037
+ * asks the door.
1038
+ *
1039
+ * @param identifier - Page UUID, title, slug or vault name
1040
+ * @param ctx - Whose question this is — never defaulted
1041
+ */
1042
+ mayEditPage(identifier: string, ctx: ActorContext): Promise<boolean>;
929
1043
  /**
930
1044
  * Admin-override save (#689): the admin "Edit raw" textarea, split into
931
1045
  * frontmatter and body and written through the door (#1462).
@@ -975,7 +1089,7 @@ declare class PageManager extends BaseManager implements CatalogSource {
975
1089
  * What a page stores for the user `name` (#2097): `IdentityManager.storedReferenceOf`.
976
1090
  */
977
1091
  private referenceTo;
978
- /** `audience` and each `access[action]` of a save, user entries as {@link referenceTo} stores them (#2097). */
1092
+ /** The `audience` of a save, user entries as {@link referenceTo} stores them (#2097). */
979
1093
  private convertAccessListsForSave;
980
1094
  /**
981
1095
  * The frontmatter a save writes, from the frontmatter it was given (#1462).
@@ -994,6 +1108,7 @@ declare class PageManager extends BaseManager implements CatalogSource {
994
1108
  * @param actingUser - Who is writing this revision, from the context (#1164)
995
1109
  * @param viaToken - The agent token this write came through (#946), if any
996
1110
  * @param keepEditor - Leave the stored `editor` alone (`preserveEditor`, #1457)
1111
+ * @param shippedAuthor - An add-on page's author (`shippedAuthor`, #2255), already checked
997
1112
  * @returns The frontmatter to hand the provider
998
1113
  */
999
1114
  private normalizeSaveMetadata;
@@ -1009,7 +1124,9 @@ declare class PageManager extends BaseManager implements CatalogSource {
1009
1124
  *
1010
1125
  * `ctx` is mandatory and positional: the save acts as the caller's subject
1011
1126
  * (#1179), a private-store write reaches that session's keys through it
1012
- * (#1382), and the audit record names it. A caller forwards the context it
1127
+ * (#1382), and the audit record names it. The write is decided here, for
1128
+ * that context, before anything is written (#1815): `page-edit` on the page
1129
+ * as it is, `page-create` where it is going when that is somewhere new. A caller forwards the context it
1013
1130
  * was given — a request's subject, or the job's for boot and scheduled
1014
1131
  * work — and never invents one.
1015
1132
  *
@@ -1020,6 +1137,7 @@ declare class PageManager extends BaseManager implements CatalogSource {
1020
1137
  * @param options - Validation and conflict-check opt-outs, `rawFrontmatter` (#689),
1021
1138
  * `preserveLastModified`, `preserveEditor` (#1457), audit enrichment
1022
1139
  * @returns Where the page landed, and what it was called before
1140
+ * @throws PageWriteRefusedError when `ctx` may not make this write
1023
1141
  *
1024
1142
  * @example
1025
1143
  * await pageManager.savePage('New Page', '# Hello World', { 'user-keywords': ['tutorial'] }, req.userContext);
@@ -1110,6 +1228,26 @@ declare class PageManager extends BaseManager implements CatalogSource {
1110
1228
  }>>;
1111
1229
  /** The store ids the requester has of their own (#1387). */
1112
1230
  listOwnStoreIds(ctx: ActorContext): Promise<string[]>;
1231
+ /**
1232
+ * The requester's own vaults, each with its pages and whether it is
1233
+ * encrypted (#2245), for `/my/vaults`.
1234
+ *
1235
+ * `state` is the vault's {@link VaultSealState} (#1834): `sealed` is
1236
+ * encrypted, `plain` is not, and `unopened` or `mismatch` is a vault whose
1237
+ * kind says it must be encrypted while its copy is not. `readable` is false
1238
+ * for a vault this context cannot open, such as a sealed vault still
1239
+ * locked: its pages are then unknown, not none.
1240
+ */
1241
+ listOwnVaults(ctx: ActorContext): Promise<Array<{
1242
+ id: string;
1243
+ state: VaultSealState;
1244
+ readable: boolean;
1245
+ pages: Array<{
1246
+ name: string;
1247
+ title: string;
1248
+ uuid: string;
1249
+ }>;
1250
+ }>>;
1113
1251
  listOwnDeletedPrivatePages(ctx: ActorContext): Promise<StoreDeletedEntry[]>;
1114
1252
  /**
1115
1253
  * Bring one of the requester's private pages back from its store's trash
@@ -1233,14 +1371,25 @@ declare class PageManager extends BaseManager implements CatalogSource {
1233
1371
  * and the context names who put it there. Afterwards the page is taken out
1234
1372
  * of every shared index, here and nowhere else (#1462).
1235
1373
  *
1374
+ * The write door decides `page-delete` on the page first (#1815), and the
1375
+ * delete is recorded as `page-delete` before it happens.
1376
+ *
1236
1377
  * @param identifier - Page UUID, title, or slug
1237
1378
  * @param ctx - Who is acting (#1179)
1379
+ * @param options - `audit.skip` when the caller already recorded this
1380
+ * delete; `audit.ipAddress`, the request's address, for the record
1238
1381
  * @returns True if deleted, false if not found
1382
+ * @throws PageWriteRefusedError when `ctx` may not delete the page
1239
1383
  *
1240
1384
  * @example
1241
1385
  * const deleted = await pageManager.deletePage('Old Page', req.userContext);
1242
1386
  */
1243
- deletePage(identifier: string, ctx: ActorContext): Promise<boolean>;
1387
+ deletePage(identifier: string, ctx: ActorContext, options?: {
1388
+ audit?: {
1389
+ skip?: boolean;
1390
+ ipAddress?: string;
1391
+ };
1392
+ }): Promise<boolean>;
1244
1393
  /**
1245
1394
  * Delete the pages a test made (#2008): every shared page carrying the
1246
1395
  * `test-artifact` system keyword (#1355), and never a page without it.
@@ -1253,7 +1402,8 @@ declare class PageManager extends BaseManager implements CatalogSource {
1253
1402
  * follow. One page's failure never stops the rest; it is reported.
1254
1403
  *
1255
1404
  * The caller decides who may ask: the sweep route asks `admin-system`, and
1256
- * an account delete has already been decided as `user-delete`. Private
1405
+ * an account delete has already been decided as `user-delete` and sends a
1406
+ * system job (#1815). Each delete is still decided by `deletePage`. Private
1257
1407
  * vault pages are not in the shared index, so they are not reached here.
1258
1408
  *
1259
1409
  * @param ctx - Who is acting; named in every record
@@ -1502,7 +1652,7 @@ declare class PageManager extends BaseManager implements CatalogSource {
1502
1652
  private warnUnmatchableAddonAudience;
1503
1653
  /**
1504
1654
  * At startup, once add-ons have loaded (#1964): warn about every page whose
1505
- * `audience` or `access` names an add-on that can match nobody — unknown,
1655
+ * `audience` names an add-on that can match nobody — unknown,
1506
1656
  * not per-person, or not loaded.
1507
1657
  *
1508
1658
  * @returns Each page and slug warned about
@@ -1617,8 +1767,10 @@ declare class PageManager extends BaseManager implements CatalogSource {
1617
1767
  *
1618
1768
  * `pages/vaults/{user}/` and every store below it is owned by that user.
1619
1769
  * Nobody else acts in it unless the owner delegated (a share the owner
1620
- * issued, once the store's Share switch exists — #1388). No role reaches in,
1621
- * admin included (security-posture P2: no `hasRole` as an allow).
1770
+ * issued, once the store's Share switch exists — #1388). No role reaches in
1771
+ * (security-posture P2: no `hasRole` as an allow); at a door, a holder of
1772
+ * `admin-system` may enter an unencrypted vault when
1773
+ * `ngdpbase.vaults.admin-access` is on (#2255), recorded each time.
1622
1774
  *
1623
1775
  * The owner is the one in the page's name (#1456), not frontmatter
1624
1776
  * `author`, so reassigning `author` cannot move ownership (#711).
@@ -1629,7 +1781,7 @@ declare class PageManager extends BaseManager implements CatalogSource {
1629
1781
  * - `false` — a private name, and the caller is neither — whether or not
1630
1782
  * the page exists
1631
1783
  */
1632
- checkPrivatePageAccess(wikiContext: WikiContext, pageNameOrUuid: string): Promise<boolean | null>;
1784
+ checkPrivatePageAccess(wikiContext: WikiContext, pageNameOrUuid: string, adminEntry?: 'record' | 'quiet' | 'none'): Promise<boolean | null>;
1633
1785
  /**
1634
1786
  * Owner and store of a private page, or `null` when the page is not private
1635
1787
  * or does not exist (#1398). The owner owns the page and every attachment
@@ -1686,6 +1838,15 @@ declare class PageManager extends BaseManager implements CatalogSource {
1686
1838
  * Best-effort: a failure is logged and never blocks start-up. An encrypted
1687
1839
  * store is not readable here and is not touched — it migrates at its
1688
1840
  * owner's unlock, through {@link migratePrivateLinks}.
1841
+ *
1842
+ * #1815 (operator, 2026-11-04, decision 2): the public pages are the system
1843
+ * principal's work; each user's vaults are a job on behalf of that user
1844
+ * ({@link jobContextForOwner}), through the write door like any write. The
1845
+ * vault container rule admits a job carrying its owner, and the owner's
1846
+ * rights in their own vaults come from policy, resolved when each write is
1847
+ * decided. The system principal is let into no vault. A shared folder is
1848
+ * not a user's and opens to no boot job (its key never reaches one), so it
1849
+ * is not visited.
1689
1850
  */
1690
1851
  migratePrivateLinksAtBoot(): Promise<void>;
1691
1852
  /**
@@ -1799,8 +1960,9 @@ declare class PageManager extends BaseManager implements CatalogSource {
1799
1960
  * keeps its `lastModified` and `editor`, since nobody edited it. The audit
1800
1961
  * record is a rename, from the old path to the new.
1801
1962
  *
1802
- * Access is not decided here, as for every save: the caller has already
1803
- * been let into the page. A page is never moved out of an encrypted vault
1963
+ * Access is decided by the save it makes, as for every write (#1815): an
1964
+ * edit of the page where it is and a create in the vault it goes to. A
1965
+ * page is never moved out of an encrypted vault
1804
1966
  * into one that is not, since that would write it in the clear.
1805
1967
  *
1806
1968
  * @param pageName - The page's private name, `vaults/{owner}/{vault}/{title}`
@@ -1823,7 +1985,7 @@ declare class PageManager extends BaseManager implements CatalogSource {
1823
1985
  *
1824
1986
  * @returns How many pages moved
1825
1987
  */
1826
- moveCapturesToCaptureVault(ctx: ActorContext): Promise<number>;
1988
+ moveCapturesToCaptureVault(ctx: ActorContext, owner?: string): Promise<number>;
1827
1989
  /**
1828
1990
  * Move private pages made before #1505 into their system-category's own
1829
1991
  * vault (#1507). Until then every private page went to its owner's default
@@ -1838,7 +2000,7 @@ declare class PageManager extends BaseManager implements CatalogSource {
1838
2000
  *
1839
2001
  * @returns How many pages moved
1840
2002
  */
1841
- movePagesToCategoryVaults(ctx: ActorContext): Promise<number>;
2003
+ movePagesToCategoryVaults(ctx: ActorContext, owner?: string): Promise<number>;
1842
2004
  /**
1843
2005
  * Every private slug in the `vaults--…` form (#1507), once per vault. A
1844
2006
  * private page's slug is made fresh on every save, so each page whose slug
@@ -1848,7 +2010,7 @@ declare class PageManager extends BaseManager implements CatalogSource {
1848
2010
  *
1849
2011
  * @returns How many pages were re-saved
1850
2012
  */
1851
- migratePrivateSlugs(ctx: ActorContext): Promise<number>;
2013
+ migratePrivateSlugs(ctx: ActorContext, owner?: string): Promise<number>;
1852
2014
  /**
1853
2015
  * One pass of a vault move: every page in the vaults `ctx` can open is
1854
2016
  * offered to `pick`, which names the vault it goes to (and any frontmatter
@@ -1,5 +1,6 @@
1
1
  import BaseManager from './BaseManager.js';
2
- import type { Policy, PolicySubject, PolicyResource } from '../types/Policy.js';
2
+ import type { Policy, PolicySubject, PolicyResource, PolicyCondition } from '../types/Policy.js';
3
+ import { type DeclaredSubjectAttribute } from '../utils/policyAttributes.js';
3
4
  import { WikiEngine } from '../types/WikiEngine.js';
4
5
  /**
5
6
  * User context for policy evaluation
@@ -17,15 +18,55 @@ interface UserContext {
17
18
  * PIP (#1504): today a page's `system-category`.
18
19
  */
19
20
  export type ResourceAttributes = Record<string, unknown>;
21
+ /**
22
+ * Reads one attribute a condition names, `subject:id` or `resource:owner`
23
+ * (#2255). Supplied by the PDP from the PIP; `undefined` or `null` is a
24
+ * missing attribute. Asked only when a matching policy has a condition. A
25
+ * reader may be async (an identity is resolved through IdentityManager), and
26
+ * the evaluator awaits every read.
27
+ */
28
+ export type AttributeReader = (name: string) => unknown | Promise<unknown>;
20
29
  /**
21
30
  * Access evaluation context
22
31
  */
23
32
  interface AccessContext {
24
33
  pageName: string;
34
+ /**
35
+ * The action asked. `actions` widens it: a policy granting any of them
36
+ * matches (#2255: a page write is `page-edit` or `pushed-pages-edit`).
37
+ */
25
38
  action: string;
39
+ actions?: readonly string[];
26
40
  userContext?: UserContext;
27
41
  attributes?: ResourceAttributes;
42
+ /** For a policy's `condition`; without one every condition is unknown. */
43
+ readAttribute?: AttributeReader;
28
44
  }
45
+ /**
46
+ * What is wrong with a policy's condition, one sentence each (#2255, #2261);
47
+ * empty when it is fine. Each sentence says what is wrong, then what to
48
+ * change. A node is a comparison (`attribute` and one operator) or `any-of`,
49
+ * `all-of` or `not` over other conditions, nested at most
50
+ * {@link MAX_CONDITION_DEPTH} deep, at most {@link MAX_CONDITION_NODES} nodes
51
+ * in all, an `in` list of at most {@link MAX_IN_ITEMS} and an `any-of` or
52
+ * `all-of` of at most {@link MAX_GROUP_CHILDREN}. Each attribute a comparison names must be
53
+ * declared: a `subject:<name>` in `ngdpbase.access.subject-attributes`
54
+ * (`id` always, `aal` as shipped), or a `resource:<name>` declared on the `page`
55
+ * resource type. The types of the two sides must agree. A condition with a
56
+ * problem never holds: an allow with it does not apply and a deny with it
57
+ * does, as for a missing attribute.
58
+ */
59
+ export declare function conditionProblems(condition: unknown, pageAttributes: Record<string, {
60
+ type?: string;
61
+ }>, subjectAttributes?: Record<string, DeclaredSubjectAttribute>): string[];
62
+ /**
63
+ * Is the condition true, false or unknown (#2255, #2261)? `true` or `false`,
64
+ * or `undefined` when it cannot be told because an attribute it reads is
65
+ * missing; the caller fails closed on that. Each node is judged on its own:
66
+ * a missing attribute makes its leaf unknown, a true branch decides `any-of`,
67
+ * a false branch decides `all-of`, and `not` of unknown is unknown.
68
+ */
69
+ export declare function conditionHolds(condition: PolicyCondition | undefined, read: AttributeReader | undefined, depth?: number): Promise<boolean | undefined>;
29
70
  /**
30
71
  * How each policy resource type matches (#1504). The TYPES are declared in
31
72
  * configuration, `ngdpbase.access.resource-types`, and read through
@@ -40,7 +81,12 @@ export declare const RESOURCE_MATCHERS: Record<string, (pattern: string, pageNam
40
81
  * PolicyValidator accepts: a type with no matcher here would be a policy that
41
82
  * grants nothing.
42
83
  */
43
- export declare const SUBJECT_MATCHERS: Record<string, (value: string, userContext: UserContext | undefined) => boolean>;
84
+ export declare const SUBJECT_MATCHERS: Record<string, (value: string | undefined, userContext: UserContext | undefined) => boolean>;
85
+ /**
86
+ * The subject types that take no `value` (#2261), for the validator. A value
87
+ * on one is refused rather than ignored.
88
+ */
89
+ export declare const VALUELESS_SUBJECT_TYPES: readonly string[];
44
90
  /**
45
91
  * The resource types configuration declares (#1504), for the evaluator and the
46
92
  * validator alike. When the key is not set at all — a configuration older than
@@ -56,14 +102,19 @@ interface EvaluationResult {
56
102
  allowed: boolean;
57
103
  reason: string;
58
104
  policyName: string | null;
105
+ /** The deciding policies' ids (#2255): the denies that matched, else the allows. Empty when none matched. */
106
+ policyIds: string[];
107
+ /** Allows that matched subject, resource and action but whose condition was false or unknown (#2288). Absent when none. */
108
+ conditionNotMet?: string[];
59
109
  }
60
110
  /**
61
111
  * PolicyEvaluator - Evaluates access policies against a given context.
62
112
  *
63
113
  * PolicyEvaluator mimics how JSPWiki uses Java's built-in security framework
64
114
  * (java.security) to load and evaluate security policies from a policy file.
65
- * It evaluates policies in priority order and returns the first matching policy's
66
- * decision.
115
+ * Among the policies that match, a deny overrides any allow whatever its
116
+ * priority, and no match is a deny (#2255). Each decision names the policies
117
+ * that decided it.
67
118
  *
68
119
  * @class PolicyEvaluator
69
120
  * @extends BaseManager
@@ -112,24 +163,35 @@ declare class PolicyEvaluator extends BaseManager {
112
163
  */
113
164
  initialize(): Promise<void>;
114
165
  /**
115
- * Evaluates all relevant policies to make an access decision.
116
- *
117
- * Policies are evaluated in priority order (highest first). The first matching
118
- * policy determines the access decision. If no policies match, access is denied.
119
- *
120
- * @async
121
- * @param {AccessContext} context - The context of the access request
122
- * @returns {Promise<EvaluationResult>} Evaluation result with decision and reason
166
+ * Say at boot what an administrator should look at in the policies after
167
+ * #2255, one warning each, and one needs-attention entry for them all:
168
+ * stored policies the upgrade rewrote, policies it left that now decide
169
+ * differently (a deny) or reach pushed pages, conditions that never hold,
170
+ * and stored categories no policy lets anyone change (their pages are
171
+ * changed by nobody, admin included).
172
+ */
173
+ private reportPageWritePolicies;
174
+ /**
175
+ * Evaluates all relevant policies to make an access decision (#2255).
123
176
  *
124
- * @example
125
- * const result = await evaluator.evaluateAccess({
126
- * pageName: 'AdminPanel',
127
- * action: 'page:edit',
128
- * userContext: { username: 'user', roles: ['editor'] }
129
- * });
130
- * console.log('Allowed:', result.allowed, 'Reason:', result.reason);
177
+ * Deny overrides allow: if any matching deny applies, the answer is deny,
178
+ * whatever the priorities; otherwise any matching allow permits; otherwise
179
+ * no policy decided. A policy with a `condition` applies only when it holds;
180
+ * an unknown condition (a missing attribute) keeps an allow from applying
181
+ * and lets a deny apply, so a missing attribute fails closed.
131
182
  */
132
183
  evaluateAccess(context: AccessContext): Promise<EvaluationResult>;
184
+ /**
185
+ * Checks and compiles each policy's condition (#2261). `compile()` does it
186
+ * once for a whole listing; a single decision does it for its few matches.
187
+ */
188
+ private prepare;
189
+ /**
190
+ * Deny overrides allow, over policies that already match subject, resource
191
+ * and action (#2255). Every matching deny whose condition is true or
192
+ * unknown decides; failing that, every allow whose condition is true.
193
+ */
194
+ private combine;
133
195
  /**
134
196
  * Checks if a single policy matches the given context.
135
197
  *
@@ -189,12 +251,25 @@ declare class PolicyEvaluator extends BaseManager {
189
251
  * `filter` shape, compiled once and applied to many resources.
190
252
  *
191
253
  * Policies that cannot match this subject or this action are dropped here,
192
- * once; the predicate matches resources only, in the same order as
193
- * `evaluateAccess` so first-match-wins gives the same answer. No log line
194
- * per call: a listing asks this ~18k times, and the per-policy INFO line
195
- * `evaluateAccess` writes is right for one decision and wrong for a filter.
254
+ * once; the predicate matches resources only and combines them exactly as
255
+ * `evaluateAccess` does (deny overrides allow, #2255), so a list and a
256
+ * single decision agree. No log line per call: a listing asks this ~18k
257
+ * times, and the INFO line `evaluateAccess` writes is right for one
258
+ * decision and wrong for a filter.
196
259
  */
197
- compile(userContext: UserContext | undefined, action: string): (pageName: string, attributes?: ResourceAttributes) => EvaluationResult;
260
+ compile(userContext: UserContext | undefined, action: string, actions?: readonly string[]): (pageName: string, attributes?: ResourceAttributes, readAttribute?: AttributeReader) => Promise<EvaluationResult>;
261
+ /** The attributes declared on the `page` resource type (#2255), for checking a condition. */
262
+ declaredPageAttributes(): Record<string, {
263
+ type?: string;
264
+ }>;
265
+ /** The subject attributes a condition may read (#2261): `id` always, and those declared in `ngdpbase.access.subject-attributes`. */
266
+ declaredSubjectAttributes(): Record<string, DeclaredSubjectAttribute>;
267
+ /** Each policy in force whose condition has a problem, with the problems (#2255): for the boot warning. */
268
+ conditionProblemsInForce(): Array<{
269
+ id: string;
270
+ effect: Policy['effect'];
271
+ problems: string[];
272
+ }>;
198
273
  matchesResource(resources: PolicyResource[] | undefined, pageName: string, attributes?: ResourceAttributes): boolean;
199
274
  /**
200
275
  * Checks if the action matches the policy's actions.
@@ -219,8 +219,9 @@ export default class ShareManager extends BaseManager {
219
219
  * True when a page must never appear in any share:
220
220
  * - `private: true` (decision 1)
221
221
  * - `owner-only` in user-keywords (decision 1)
222
- * - `audience` or per-action `access` frontmatter (decision 3 — a share
223
- * must not silently widen an author's chosen audience)
222
+ * - `audience` frontmatter (decision 3 — a share must not silently widen
223
+ * an author's chosen audience). A page's `access` field is not read
224
+ * since #2255.
224
225
  */
225
226
  private isPageExcluded;
226
227
  /** Prepare and lock everything a link to an encrypted vault covers. `ownerCtx` must hold the vault's key. */
@@ -19,6 +19,12 @@ export interface AgentTokenGrant {
19
19
  id: string;
20
20
  name: string;
21
21
  scopes: string[];
22
+ /**
23
+ * The assurance level stored with the token when it was made (#2261): the
24
+ * level of the minting session at that moment, never higher. Absent on a
25
+ * token made before it was stored, which then has no `subject:aal`.
26
+ */
27
+ aal?: 1 | 2 | 3;
22
28
  }
23
29
  /**
24
30
  * A person acting for another under a proxy grant (#1762 slice 2), as a
@@ -112,6 +118,12 @@ export interface PermissionSubject {
112
118
  * account's cap, since it is the actor who signs in (#1762).
113
119
  */
114
120
  aalCap?: import('../providers/BaseAuthProvider.js').Aal;
121
+ /**
122
+ * Set by the bearer middleware on a subject authenticated by a JWT that
123
+ * records no sign-in level (Authentik, #2287): its `subject:aal` is missing,
124
+ * not AAL 1. Set from the verified result, never from a request field.
125
+ */
126
+ bearerWithoutLevel?: true;
115
127
  }
116
128
  /**
117
129
  * The subject of work with no request behind it (#631, #1212).
@@ -134,6 +146,8 @@ export interface JobSubject {
134
146
  viaProxy?: ProxyClaim;
135
147
  /** #1999: the scheduled job a run acts as (`JobContext.job`). */
136
148
  job?: string;
149
+ /** #2261: the level stored with the job's request (`JobContext.aal`); absent when it stored none. */
150
+ grantedAal?: 1 | 2 | 3;
137
151
  }
138
152
  /**
139
153
  * User creation input data
@@ -426,11 +440,11 @@ declare class UserManager extends BaseManager {
426
440
  /**
427
441
  * Create a user page for a new user.
428
442
  *
429
- * The page is written AS the new user (#1462): it is their page, and the
430
- * door takes a new page's author from who is acting. Written as the admin
431
- * who created the account — or as whoever ran self-registration — the
432
- * profile would be authored by them, and `author-lock` would leave the
433
- * account's own owner unable to edit it.
443
+ * The page is the new user's (#1462): they are its author. Authored by the
444
+ * admin who created the account — or by whoever ran self-registration —
445
+ * `author-lock` would leave the account's own owner unable to edit it.
446
+ * It is written by a system job through the write door (#1815), with the
447
+ * new user named as the one it is made for.
434
448
  *
435
449
  * @param {User} user - User object
436
450
  * @param ctx - Who created the account, for the log line only
@@ -48,8 +48,10 @@ export declare const DEFAULT_SHARE_MAX_DAYS = 15;
48
48
  * - `public`: a `storageLocation` without a vault; its pages can never be
49
49
  * private.
50
50
  * - `vault`: a `storageLocation.privatestore`; its pages may be private.
51
+ * - `shared-folder`: `storageLocation.sharedfolder: true` (#2255); its pages
52
+ * live in shared folders (the `shared` category).
51
53
  */
52
- export type CategoryStored = 'none' | 'public' | 'vault';
54
+ export type CategoryStored = 'none' | 'public' | 'vault' | 'shared-folder';
53
55
  /**
54
56
  * Where an entry's pages are stored. The one rule every reader of "is this
55
57
  * system-category stored here, and can it be private" goes through. A
@@ -39,7 +39,7 @@ export class AgentTokenAuthProvider {
39
39
  return null;
40
40
  return {
41
41
  username: record.owner,
42
- viaToken: { id: record.id, name: record.name, scopes: record.scopes }
42
+ viaToken: { id: record.id, name: record.name, scopes: record.scopes, ...(record.aal ? { aal: record.aal } : {}) }
43
43
  };
44
44
  }
45
45
  }
@@ -74,6 +74,8 @@ export interface ViaToken {
74
74
  name: string;
75
75
  /** Scope ceiling this token may act within */
76
76
  scopes: string[];
77
+ /** The assurance level stored with the token when it was made (#2261); absent on an older token. */
78
+ aal?: 1 | 2 | 3;
77
79
  }
78
80
  /**
79
81
  * Returned by verify() on success.