@jwilleke/ngdpbase-types 5.41.1 → 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 (70) 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 +8 -28
  7. package/lib/src/managers/AgentTokenManager.d.ts +8 -0
  8. package/lib/src/managers/AttachmentManager.d.ts +59 -0
  9. package/lib/src/managers/AuditManager.d.ts +477 -0
  10. package/lib/src/managers/AuthManager.d.ts +247 -15
  11. package/lib/src/managers/AuthManager.js +588 -45
  12. package/lib/src/managers/ConfigurationManager.d.ts +15 -0
  13. package/lib/src/managers/ConsentManager.d.ts +15 -0
  14. package/lib/src/managers/ImportManager.d.ts +15 -0
  15. package/lib/src/managers/PageManager.d.ts +185 -23
  16. package/lib/src/managers/PolicyEvaluator.d.ts +99 -24
  17. package/lib/src/managers/ShareManager.d.ts +3 -2
  18. package/lib/src/managers/UserManager.d.ts +19 -5
  19. package/lib/src/managers/ValidationManager.d.ts +3 -1
  20. package/lib/src/providers/AgentTokenAuthProvider.js +1 -1
  21. package/lib/src/providers/BaseAttachmentProvider.d.ts +4 -0
  22. package/lib/src/providers/BaseAuditProvider.d.ts +492 -0
  23. package/lib/src/providers/BaseAuthProvider.d.ts +4 -0
  24. package/lib/src/providers/BaseCredentialsProvider.d.ts +2 -2
  25. package/lib/src/providers/BaseCredentialsProvider.js +1 -1
  26. package/lib/src/providers/BasicAttachmentProvider.d.ts +9 -0
  27. package/lib/src/providers/FileCredentialsProvider.js +1 -1
  28. package/lib/src/providers/NullAuditProvider.d.ts +82 -0
  29. package/lib/src/providers/TotpAuthProvider.d.ts +55 -0
  30. package/lib/src/providers/TotpAuthProvider.js +145 -0
  31. package/lib/src/security/PolicyDecisionPoint.d.ts +57 -1
  32. package/lib/src/security/PolicyInformationPoint.d.ts +86 -6
  33. package/lib/src/security/permissions.generated.d.ts +8 -0
  34. package/lib/src/types/Consent.d.ts +17 -4
  35. package/lib/src/types/Consent.js +11 -1
  36. package/lib/src/types/Page.d.ts +0 -7
  37. package/lib/src/types/Policy.d.ts +67 -6
  38. package/lib/src/types/Policy.js +6 -0
  39. package/lib/src/types/Provider.d.ts +18 -0
  40. package/lib/src/types/User.d.ts +6 -0
  41. package/lib/src/types/express.d.ts +22 -0
  42. package/lib/src/utils/addonPageSync.d.ts +0 -12
  43. package/lib/src/utils/auditChain.d.ts +83 -0
  44. package/lib/src/utils/auditEventNames.generated.d.ts +2 -0
  45. package/lib/src/utils/auditEventNames.generated.js +2 -0
  46. package/lib/src/utils/auditEvents.d.ts +2 -0
  47. package/lib/src/utils/auditEvents.js +3 -1
  48. package/lib/src/utils/auditHeadWitness.d.ts +97 -0
  49. package/lib/src/utils/auditLifecycle.d.ts +71 -0
  50. package/lib/src/utils/browserDescription.d.ts +18 -0
  51. package/lib/src/utils/browserDescription.js +12 -0
  52. package/lib/src/utils/frontmatterAccess.d.ts +15 -24
  53. package/lib/src/utils/legacyStoreKeys.d.ts +16 -0
  54. package/lib/src/utils/pageWritePolicies.d.ts +55 -0
  55. package/lib/src/utils/permissionCatalog.d.ts +2 -0
  56. package/lib/src/utils/permissionCatalog.js +19 -0
  57. package/lib/src/utils/policyAttributes.d.ts +141 -0
  58. package/lib/src/utils/policyAttributes.js +219 -0
  59. package/lib/src/utils/postureRecord.d.ts +73 -0
  60. package/lib/src/utils/privateStoreAccess.d.ts +24 -4
  61. package/lib/src/utils/privateStoreAccess.js +19 -4
  62. package/lib/src/utils/privateStorePath.d.ts +6 -0
  63. package/lib/src/utils/privateStorePath.js +8 -0
  64. package/lib/src/utils/securityPosture.d.ts +52 -0
  65. package/lib/src/utils/tlsConfig.d.ts +59 -0
  66. package/lib/src/utils/totpSeedCipher.d.ts +4 -0
  67. package/lib/src/utils/totpSeedCipher.js +45 -0
  68. package/lib/src/utils/username.d.ts +13 -2
  69. package/lib/src/utils/username.js +18 -3
  70. package/package.json +5 -2
@@ -0,0 +1,82 @@
1
+ import BaseAuditProvider, { WikiEngine, AuditFilters, AuditSearchResults, AuditStats } from './BaseAuditProvider.js';
2
+ import type { AuditReport } from './BaseAuditProvider.js';
3
+ /**
4
+ * NullAuditProvider - No-op audit provider
5
+ *
6
+ * Used when auditing is disabled or for testing.
7
+ * All audit operations are no-ops that return immediately.
8
+ */
9
+ declare class NullAuditProvider extends BaseAuditProvider {
10
+ /**
11
+ * Not chained (#1119): this provider stores nothing, so there is nothing to
12
+ * chain. A legitimate configuration rather than a hole — provided the
13
+ * instance cannot then claim tamper evidence, which is what
14
+ * getProviderInfo().guarantees reports.
15
+ */
16
+ protected chainEnabled(): boolean;
17
+ /** Nothing is stored, so nothing is guaranteed (#1119). */
18
+ getGuarantees(): AuditReport;
19
+ constructor(engine: WikiEngine);
20
+ /**
21
+ * Initialize the null audit provider (no-op)
22
+ * @returns {Promise<void>}
23
+ */
24
+ initialize(): Promise<void>;
25
+ /**
26
+ * Get provider information
27
+ * @returns {Object} Provider metadata
28
+ */
29
+ getProviderInfo(): {
30
+ name: string;
31
+ version: string;
32
+ description: string;
33
+ features: string[];
34
+ };
35
+ /**
36
+ * Log an audit event (no-op)
37
+ * @param {AuditEvent} _auditEvent - Audit event data
38
+ * @returns {Promise<string>} Dummy event ID
39
+ */
40
+ writeEvent(_record: Record<string, unknown>): Promise<string>;
41
+ /**
42
+ * Search audit logs (returns empty results)
43
+ * @param {AuditFilters} _filters - Search filters
44
+ * @param {Record<string, number>} options - Search options
45
+ * @returns {Promise<AuditSearchResults>} Empty search results
46
+ */
47
+ searchAuditLogs(_filters?: AuditFilters, options?: Record<string, number>): Promise<AuditSearchResults>;
48
+ /**
49
+ * Get audit statistics (returns zeros)
50
+ * @param {AuditFilters} _filters - Optional filters
51
+ * @returns {Promise<AuditStats>} Empty statistics
52
+ */
53
+ getAuditStats(_filters?: AuditFilters): Promise<AuditStats>;
54
+ /**
55
+ * Export audit logs (returns empty data)
56
+ * @param {AuditFilters} _filters - Export filters
57
+ * @param {string} format - Export format ('json', 'csv')
58
+ * @returns {Promise<string>} Empty export data
59
+ */
60
+ exportAuditLogs(_filters?: AuditFilters, format?: 'json' | 'csv'): Promise<string>;
61
+ /**
62
+ * Flush pending audit events (no-op)
63
+ * @returns {Promise<void>}
64
+ */
65
+ flush(): Promise<void>;
66
+ /**
67
+ * Clean up old audit logs (no-op)
68
+ * @returns {Promise<void>}
69
+ */
70
+ cleanup(): Promise<void>;
71
+ /**
72
+ * Check if the audit provider is healthy (always true)
73
+ * @returns {Promise<boolean>} Always true
74
+ */
75
+ isHealthy(): Promise<boolean>;
76
+ /**
77
+ * Close/cleanup the audit provider (no-op)
78
+ * @returns {Promise<void>}
79
+ */
80
+ close(): Promise<void>;
81
+ }
82
+ export default NullAuditProvider;
@@ -0,0 +1,55 @@
1
+ import type { AuthProvider, AuthResult, AuthVerifyCredentials, FactorDescription } from './BaseAuthProvider.js';
2
+ import type { CredentialRecord } from './BaseCredentialsProvider.js';
3
+ /** Steps either side of now that are accepted. */
4
+ export declare const TOTP_DRIFT_STEPS = 1;
5
+ /** The store access AuthManager grants: an account's TOTP rows, and recording a use. */
6
+ export interface TotpStoreAccess {
7
+ rows(username: string): CredentialRecord[];
8
+ used(id: string, at: string, secret: string): Promise<void>;
9
+ }
10
+ /** A new seed, offered while enrolling and kept by the caller until it is confirmed. */
11
+ export interface TotpEnrolment {
12
+ /** Base32, for typing into the app by hand. */
13
+ seed: string;
14
+ /** The `otpauth://` URI the QR code carries. */
15
+ uri: string;
16
+ }
17
+ /**
18
+ * The time step `code` belongs to within the drift window, or null. Pure; the
19
+ * caller decides about replay.
20
+ */
21
+ export declare function matchingStep(seed: string, code: string, now: number): number | null;
22
+ export declare class TotpAuthProvider implements AuthProvider {
23
+ private readonly credentialsKey;
24
+ private readonly issuer;
25
+ private readonly store;
26
+ private readonly now;
27
+ readonly id = "totp";
28
+ readonly displayName = "Authenticator app";
29
+ readonly factor: FactorDescription;
30
+ private readonly serial;
31
+ /**
32
+ * @param credentialsKey `NGDPBASE_CREDENTIALS_KEY`, from which the seed key is derived
33
+ * @param issuer shown in the authenticator app beside the account
34
+ */
35
+ constructor(credentialsKey: string, issuer: string, store: TotpStoreAccess, now?: () => number);
36
+ /** A fresh 160-bit seed and its URI for `username`. Nothing is stored. */
37
+ newEnrolment(username: string): TotpEnrolment;
38
+ /**
39
+ * Seal a seed that is offered but not yet confirmed, so it can wait in the
40
+ * session without being readable there or in a session log (#421). Its own
41
+ * shape, so it never opens as a stored secret.
42
+ */
43
+ sealPending(username: string, seed: string): string;
44
+ /** Open what `sealPending` made for `username`; null when it is not that. */
45
+ openPending(username: string, sealed: unknown): string | null;
46
+ /**
47
+ * Check the code that confirms an enrolment and seal the seed for the store.
48
+ * The confirming code's step is recorded, so it cannot then sign anyone in.
49
+ * Null when the seed is malformed or the code does not match.
50
+ */
51
+ confirmEnrolment(username: string, seed: string, code: string): string | null;
52
+ /** Verify `credentials.code` for `credentials.username`. A failure gives no reason. */
53
+ verify(credentials: AuthVerifyCredentials): Promise<AuthResult | null>;
54
+ private open;
55
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * TOTP — an authenticator app's six-digit code (#421), on `otpauth` (decided
3
+ * on #1523, 2026-10-31).
4
+ *
5
+ * - A second factor only (`primary: false`): something you have, `amr`
6
+ * `otp`, AAL1 on its own. After a password the sign-in covers two factor
7
+ * types and AuthManager's `assess()` lifts it to AAL2. Not phishing-resistant.
8
+ * - The seed lives in the credentials store (#1524), kind `totp`, AES-GCM
9
+ * encrypted under a subkey of `NGDPBASE_CREDENTIALS_KEY`
10
+ * (`utils/totpSeedCipher`). The row's sealed secret also holds the last time
11
+ * step used, so a code cannot be used twice in its window.
12
+ * - A code is accepted for the current 30-second step and one step either
13
+ * side (RFC 6238 §5.2 drift), never for a step at or before the last one used.
14
+ * - Verification for one account runs one at a time, so two requests carrying
15
+ * the same code cannot both pass before the step is recorded.
16
+ */
17
+ import { Secret, TOTP, URI } from 'otpauth';
18
+ import { openTotpSecret, sealTotpSecret } from '../utils/totpSeedCipher.js';
19
+ import { KeyedSerial } from '../utils/keyedSerial.js';
20
+ /** RFC 6238 defaults, which every authenticator app supports. */
21
+ const PERIOD = 30;
22
+ const DIGITS = 6;
23
+ /** Steps either side of now that are accepted. */
24
+ export const TOTP_DRIFT_STEPS = 1;
25
+ function totpFor(seed, issuer = '', label = '') {
26
+ return new TOTP({ issuer, label, algorithm: 'SHA1', digits: DIGITS, period: PERIOD, secret: Secret.fromBase32(seed) });
27
+ }
28
+ function isCode(code) {
29
+ return typeof code === 'string' && /^\d{6}$/.test(code);
30
+ }
31
+ function currentStep(now) {
32
+ return Math.floor(now / 1000 / PERIOD);
33
+ }
34
+ /**
35
+ * The time step `code` belongs to within the drift window, or null. Pure; the
36
+ * caller decides about replay.
37
+ */
38
+ export function matchingStep(seed, code, now) {
39
+ if (!isCode(code))
40
+ return null;
41
+ const delta = totpFor(seed).validate({ token: code, timestamp: now, window: TOTP_DRIFT_STEPS });
42
+ return delta === null ? null : currentStep(now) + delta;
43
+ }
44
+ export class TotpAuthProvider {
45
+ credentialsKey;
46
+ issuer;
47
+ store;
48
+ now;
49
+ id = 'totp';
50
+ displayName = 'Authenticator app';
51
+ factor = { amr: ['otp'], aal: 1, primary: false };
52
+ serial = new KeyedSerial();
53
+ /**
54
+ * @param credentialsKey `NGDPBASE_CREDENTIALS_KEY`, from which the seed key is derived
55
+ * @param issuer shown in the authenticator app beside the account
56
+ */
57
+ constructor(credentialsKey, issuer, store, now = Date.now) {
58
+ this.credentialsKey = credentialsKey;
59
+ this.issuer = issuer;
60
+ this.store = store;
61
+ this.now = now;
62
+ if (!credentialsKey)
63
+ throw new Error('TOTP needs NGDPBASE_CREDENTIALS_KEY (#421)');
64
+ }
65
+ /** A fresh 160-bit seed and its URI for `username`. Nothing is stored. */
66
+ newEnrolment(username) {
67
+ const seed = new Secret({ size: 20 }).base32;
68
+ return { seed, uri: URI.stringify(totpFor(seed, this.issuer, username)) };
69
+ }
70
+ /**
71
+ * Seal a seed that is offered but not yet confirmed, so it can wait in the
72
+ * session without being readable there or in a session log (#421). Its own
73
+ * shape, so it never opens as a stored secret.
74
+ */
75
+ sealPending(username, seed) {
76
+ return sealTotpSecret(this.credentialsKey, username, JSON.stringify({ pendingSeed: seed }));
77
+ }
78
+ /** Open what `sealPending` made for `username`; null when it is not that. */
79
+ openPending(username, sealed) {
80
+ if (typeof sealed !== 'string')
81
+ return null;
82
+ const plain = openTotpSecret(this.credentialsKey, username, sealed);
83
+ if (!plain)
84
+ return null;
85
+ try {
86
+ const p = JSON.parse(plain);
87
+ return typeof p.pendingSeed === 'string' ? p.pendingSeed : null;
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ /**
94
+ * Check the code that confirms an enrolment and seal the seed for the store.
95
+ * The confirming code's step is recorded, so it cannot then sign anyone in.
96
+ * Null when the seed is malformed or the code does not match.
97
+ */
98
+ confirmEnrolment(username, seed, code) {
99
+ let step;
100
+ try {
101
+ step = matchingStep(seed, code, this.now());
102
+ }
103
+ catch {
104
+ return null; // not base32
105
+ }
106
+ if (step === null)
107
+ return null;
108
+ return sealTotpSecret(this.credentialsKey, username, JSON.stringify({ seed, lastStep: step }));
109
+ }
110
+ /** Verify `credentials.code` for `credentials.username`. A failure gives no reason. */
111
+ async verify(credentials) {
112
+ const { username, code } = credentials;
113
+ if (typeof username !== 'string' || !username || !isCode(code))
114
+ return null;
115
+ return this.serial.run(username, async () => {
116
+ const now = this.now();
117
+ for (const row of this.store.rows(username)) {
118
+ const secret = this.open(row);
119
+ if (!secret)
120
+ continue;
121
+ const step = matchingStep(secret.seed, code, now);
122
+ if (step === null || step <= secret.lastStep)
123
+ continue;
124
+ const next = sealTotpSecret(this.credentialsKey, username, JSON.stringify({ seed: secret.seed, lastStep: step }));
125
+ await this.store.used(row.id, new Date(now).toISOString(), next);
126
+ return { username };
127
+ }
128
+ return null;
129
+ });
130
+ }
131
+ open(row) {
132
+ if (row.kind !== 'totp')
133
+ return null;
134
+ const plain = openTotpSecret(this.credentialsKey, row.username, row.secret);
135
+ if (!plain)
136
+ return null;
137
+ try {
138
+ const s = JSON.parse(plain);
139
+ return typeof s.seed === 'string' && typeof s.lastStep === 'number' ? s : null;
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ }
145
+ }
@@ -30,6 +30,8 @@ import { type RoleGrant } from '../utils/roleGrants.js';
30
30
  import { type ActorContext } from '../context/ActorContext.js';
31
31
  export declare const POLICIES_KEY = "ngdpbase.access.policies";
32
32
  export declare const POLICIES_ENABLED_KEY = "ngdpbase.access.policies.enabled";
33
+ /** Whether an administrator may enter another person's unencrypted vault (#2255). */
34
+ export declare const VAULTS_ADMIN_ACCESS_KEY = "ngdpbase.vaults.admin-access";
33
35
  /**
34
36
  * What `decide`, `permits` and `holds` take (#2022): the caller's own context
35
37
  * — a request's `PermissionSubject` or a job's `JobContext` — or a subject
@@ -256,7 +258,20 @@ export declare class PolicyDecisionPoint extends BaseManager {
256
258
  * own sign-in, once policy has said yes.
257
259
  */
258
260
  private proxyCeiling;
259
- /** Does the named actor HOLD `action` (policy alone, live)? Step-up is the proxy subject's question, not theirs. */
261
+ /**
262
+ * Does the named actor HOLD the action (policy alone, live)? Step-up is the
263
+ * proxy subject's question, not theirs. Asked about the same page when the
264
+ * request names one (#2255), so an actor who may edit only some categories
265
+ * is asked about this page's.
266
+ *
267
+ * Inside a vault or a shared folder the actor is asked the bare capability,
268
+ * as before #2255 (#2267): the `vault` and `shared-folder` matchers assume
269
+ * the container rule proved the subject may be there, and for the actor it
270
+ * did not — the person represented passed it. Asked with the container, a
271
+ * reader actor would hold everything `vault-owner` holds in someone else's
272
+ * vault. The door (`permitsInVault`), the page door and the list filter all
273
+ * come through here, so they agree.
274
+ */
260
275
  private actorHolds;
261
276
  /**
262
277
  * Decide. The order is load-bearing:
@@ -277,5 +292,46 @@ export declare class PolicyDecisionPoint extends BaseManager {
277
292
  decide(subject: DecisionInput, request: DecisionRequest): Promise<Decision>;
278
293
  /** `decide`, with the step-up rule applied or not — {@link holds} is the only caller that leaves it out. */
279
294
  private evaluate;
295
+ /**
296
+ * The first declared system category on which the policies permit a page
297
+ * write (#2255, decision 10), or null. Asked once per category, so a deny
298
+ * on one still wins there. Owner conditions are unknown here (no page), so
299
+ * a grant that needs the author does not count. Only enabled categories
300
+ * that are not bound to a location count (#2268): `shared` is a shared
301
+ * folder's, decided with the folder, and every account holds `vault-owner`,
302
+ * so counting it would give a reader every page write as a capability.
303
+ */
304
+ private onSomeCategory;
305
+ /**
306
+ * Every system category the catalogue declares, core and add-ons, enabled
307
+ * or not, lowercased; with `inUse`, only those enabled and not bound to a
308
+ * shared folder (`storageLocation.sharedfolder`).
309
+ */
310
+ private declaredCategories;
311
+ /**
312
+ * May an administrator enter `owner`'s vault (#2255,
313
+ * `ngdpbase.vaults.admin-access`)? Asked only after the container rule has
314
+ * refused someone who is not the owner. Yes when all hold:
315
+ *
316
+ * - the key is true, read now, on every request (shipped true);
317
+ * - the caller is a signed-in session or a job, not a share link, a proxy
318
+ * or an agent token;
319
+ * - the caller holds `admin-system`;
320
+ * - the vault is not encrypted: an encrypted vault always needs its key,
321
+ * whatever the key says, so it is not entered at all.
322
+ *
323
+ * `record` true (a door) writes `vault-admin-access` with the administrator
324
+ * as the actor and the owner, vault and page named; an affordance passes
325
+ * false and records nothing. Lists and search never ask this, so an
326
+ * administrator does not see other people's vault pages listed.
327
+ */
328
+ adminMayEnterVault(ctx: ActorContext | null | undefined, owner: string, vault: string, resource: string, record?: boolean): Promise<boolean>;
329
+ /**
330
+ * The declared system categories on which an allow policy grants `action`
331
+ * through the `system-category` resource type (#2255). The sitemap asks it
332
+ * with `pushed-pages-edit` to leave pushed pages out. A summary of the
333
+ * policies, never an answer to "may this subject".
334
+ */
335
+ categoriesGranting(action: string): string[];
280
336
  }
281
337
  export default PolicyDecisionPoint;
@@ -1,4 +1,5 @@
1
1
  import BaseManager from '../managers/BaseManager.js';
2
+ import { type AalFacts } from '../utils/policyAttributes.js';
2
3
  import { type ActorContext } from '../context/ActorContext.js';
3
4
  import { WikiEngine } from '../types/WikiEngine.js';
4
5
  import type { AgentTokenGrant, ProxyClaim } from '../managers/UserManager.js';
@@ -52,6 +53,10 @@ interface UserContext {
52
53
  * form. They convert it themselves, through {@link asSubject}.
53
54
  */
54
55
  type PageQuestionCaller = ActorContext | UserContext | null | undefined;
56
+ /** What a subject attribute is read from (#2261): who, and what bears on `subject:aal`. */
57
+ export type SubjectFacts = AalFacts & {
58
+ username?: string;
59
+ };
55
60
  /**
56
61
  * Access decision log entry
57
62
  */
@@ -61,8 +66,24 @@ interface AccessDecisionLog {
61
66
  action?: string;
62
67
  allowed?: boolean;
63
68
  reason?: string;
69
+ /** The policies that decided (#2255), recorded on a denial. */
70
+ policyIds?: string[];
71
+ /** Allows that matched but whose condition was not met (#2288), recorded on a denial. */
72
+ conditionNotMet?: string[];
64
73
  context?: Record<string, unknown>;
65
74
  }
75
+ /**
76
+ * What the policies may read about a page (#2255), resolved by the PIP from
77
+ * the page itself. `attributes` holds what a resource type matches on
78
+ * (`system-category`), read eagerly; `read` answers a condition's
79
+ * `resource:<name>` for an attribute declared on the `page` resource type,
80
+ * lazily, so a lookup (the owner's account) is made only when a policy needs
81
+ * it. `undefined` is a missing attribute.
82
+ */
83
+ export interface PageAttributes {
84
+ attributes: Record<string, unknown>;
85
+ read(name: string): unknown;
86
+ }
66
87
  /**
67
88
  * PolicyInformationPoint — what a page decision needs to know about the page
68
89
  * (#1431; was ACLManager until step 8).
@@ -353,10 +374,12 @@ declare class PolicyInformationPoint extends BaseManager {
353
374
  * Tier 1 is skipped for a vault page: `audience` grants nothing there.
354
375
  * - __Tier 0.5, author-lock__, `edit` only. It only denies: an author, or a
355
376
  * subject holding `admin-system` (#1431 7b), falls through to Tier 1+.
356
- * - __Tier 1, frontmatter__ `access[action]` / `audience`. Page-level, and
357
- * overrides global policy when it states a rule (#1054). An `audience`
358
- * covers every action, and a subject holding `admin-system` is its one
359
- * exception; `access` has none (#1964).
377
+ * - __Tier 1, frontmatter__ `audience`. Page-level, and decides a view
378
+ * when it states a rule (#1054). An `audience` covers every action, and a
379
+ * subject holding `admin-system` is its one exception (#1964). `access`
380
+ * is no longer read (#2255).
381
+ * - A page in a shared folder is decided by the folder's permission, and a
382
+ * change also by the `shared` category's policy and author-lock (#2255).
360
383
  * - __Tier 2.__ For a share visitor the share IS the policy (#1222); for
361
384
  * anyone else, global policy.
362
385
  * - __Default deny.__ Nothing below Tier 2 grants — JSPWiki's page-body ACL
@@ -477,14 +500,65 @@ declare class PolicyInformationPoint extends BaseManager {
477
500
  */
478
501
  canUserAccessMediaItem(caller: PageQuestionCaller, item: MediaItem): Promise<boolean>;
479
502
  /**
480
- * Check front matter audience / access fields (Tier 1).
503
+ * Check the front matter audience (Tier 1).
481
504
  * Returns a decision object; decided=false means no front matter restriction — fall through.
482
505
  *
483
506
  * An audience that leaves the subject out refuses every action, unless the
484
507
  * subject holds `admin-system` (#1964, operator 2026-10-23): asked only
485
- * then. `access[action]` has no such exception.
508
+ * then. `access` is no longer read (#2255).
486
509
  */
487
510
  private checkFrontmatterAccess;
511
+ /**
512
+ * A page's attributes for the policies (#2255, XACML's PIP): the PDP asks
513
+ * this from the page id, so no route passes them. `knownMetadata` is the
514
+ * frontmatter when this PIP already holds it (the page door); otherwise the
515
+ * page is read with the caller's own context, so a sealed page stays closed
516
+ * to anyone without its key (#1422). A page that cannot be read has no
517
+ * attributes: no category policy matches it and every condition on it is
518
+ * unknown.
519
+ */
520
+ pageAttributes(caller: PageQuestionCaller, pageName: string, knownMetadata?: Record<string, unknown> | null): Promise<PageAttributes>;
521
+ /**
522
+ * {@link pageAttributes} from frontmatter already in hand (the list
523
+ * filter). Attributes are those declared on the `page` resource type in
524
+ * `ngdpbase.access.resource-types`, each read from its `from` source; the
525
+ * sources are code (`frontmatter:<field>` today).
526
+ *
527
+ * - `system-category` is lowercased; a page with none is in the default
528
+ * category (the entry with `default: true`); a page in a shared folder is
529
+ * `shared`, whatever its frontmatter says (#2255).
530
+ * - An `identity` attribute (`owner`, from `author`) is compared by the
531
+ * account it names: an @id or a username not yet converted both read as
532
+ * that account's username, an orphan as missing (#2097).
533
+ */
534
+ pageAttributesFrom(pageName: string, metadata: PageFrontmatter | null | undefined): PageAttributes;
535
+ /**
536
+ * A subject attribute a condition reads (#2255, #2261): `subject:<name>`,
537
+ * declared in `ngdpbase.access.subject-attributes` (`id` always is) and read
538
+ * from its `from` source here, the only place a subject attribute is read. A
539
+ * request cannot send one. Missing when it is not declared, cannot be read, or
540
+ * the subject has none (`subject:aal` of a visitor who is not signed in, of a
541
+ * token made before it was stored); the evaluator then fails closed. Returns
542
+ * the value, or a Promise of it for a field read from the account (the
543
+ * evaluator awaits every read).
544
+ */
545
+ subjectAttribute(subject: SubjectFacts | null | undefined, name: string): unknown;
546
+ /** One read of a declared subject attribute from its source. A value of the wrong type, or a source that throws, is missing. */
547
+ private readSubjectSource;
548
+ /** The page's category for the policies (#2255): lowercased; the default when it has none; `shared` in a shared folder. */
549
+ private pageCategory;
550
+ /** The attributes declared on the `page` resource type (#2255). */
551
+ private declaredPageAttributes;
552
+ /** An identity reference as the policies compare it: the username of the account it names, or missing (#2097). */
553
+ private identityValue;
554
+ /** Pages already warned about for an `access:` that is not the legacy stamp (#2255): once per page per process. */
555
+ private readonly warnedAccess;
556
+ /**
557
+ * `access:` frontmatter no longer has any effect (#2255). The stamp install
558
+ * wrote (`{ edit: [admin] }`) is left alone silently; any other value is
559
+ * someone's rule that is now ignored, so it is said once per page.
560
+ */
561
+ private warnLegacyAccess;
488
562
  /**
489
563
  * Does the page's `audience` admit this subject (#1964)? True when the page
490
564
  * names no audience, the subject matches it, or the subject holds
@@ -501,6 +575,12 @@ declare class PolicyInformationPoint extends BaseManager {
501
575
  * Accepts either a single object or positional args for backward compatibility.
502
576
  */
503
577
  logAccessDecision(userOrObj: UserContext | AccessDecisionLog, pageName?: string, action?: string, allowed?: boolean, reason?: string, _context?: Record<string, unknown>): void;
578
+ /**
579
+ * Record a page write a door refused on a capability alone (#2272): the
580
+ * route's `page-edit` / `page-create` check asks the PDP, which records
581
+ * nothing. Same record as every other page denial, `policyIds` included.
582
+ */
583
+ recordPageRefusal(subject: UserContext | null | undefined, pageName: string, action: string, reason: string, policyIds?: readonly string[], conditionNotMet?: readonly string[]): Promise<void>;
504
584
  /** The name a decision is recorded under: the subject's, or `anonymous`. */
505
585
  private static recordedUser;
506
586
  /**
@@ -30,6 +30,8 @@ export type CorePermission =
30
30
  | 'identity-read'
31
31
  /** Browse and view the media library (not delegated to anonymous visitors) */
32
32
  | 'media-read'
33
+ /** Set or change a page's system-category (#2255). A page created in a shared folder gets the shared category without it */
34
+ | 'page-categorize'
33
35
  /** Create new pages */
34
36
  | 'page-create'
35
37
  /** Delete pages */
@@ -50,6 +52,12 @@ export type CorePermission =
50
52
  | 'profile-manage'
51
53
  /** Let another person act for you, within permissions you hold, and revoke it (#1762). Every signed-in role except demo-admin; an administrator making or revoking a grant for someone else also needs user-edit, and that grant never carries a copy of the person's vault key. A grant that includes proxy-grant lets the proxy give others access too. Asks for a fresh sign-in */
52
54
  | 'proxy-grant'
55
+ /** Delete pushed pages (#2255) */
56
+ | 'pushed-pages-delete'
57
+ /** Edit pushed pages: pages shipped by the system or an add-on (the categories a policy grants it on; system, documentation and addon as shipped) (#2255) */
58
+ | 'pushed-pages-edit'
59
+ /** Rename pushed pages (#2255) */
60
+ | 'pushed-pages-rename'
53
61
  /** Search pages */
54
62
  | 'search-page'
55
63
  /** Search users */
@@ -33,6 +33,15 @@ export declare const RECOVERY_WORDS_DOCUMENT = "recovery-words";
33
33
  export declare const SHARED_FOLDERS_WORDS_CONSENT_SCOPE = "shared-folders-recovery-words";
34
34
  /** The page whose `version:` is the version of the shared-folders recovery-words wording (#2124). */
35
35
  export declare const SHARED_FOLDERS_WORDS_DOCUMENT = "shared-folders-recovery-words";
36
+ /**
37
+ * The scope of "Remember this browser" (#1523 slice 1c, Q5): a person's
38
+ * opt-in, at the second-factor step, to a device key on this browser that
39
+ * skips the second-factor prompt next time. Bound to a subject: the remembered
40
+ * device's id, so each browser is its own consent, withdrawn by removing it.
41
+ */
42
+ export declare const REMEMBER_BROWSER_CONSENT_SCOPE = "remember-browser";
43
+ /** The page whose `version:` is the version of the "Remember this browser" wording (#1523 slice 1c). */
44
+ export declare const REMEMBER_BROWSER_DOCUMENT = "remember-browser";
36
45
  /** Core's scopes bound to a subject, each with its one document (#1928, #2124). */
37
46
  export declare const SUBJECT_CONSENT_DOCUMENTS: Readonly<Record<ConsentScope, string>>;
38
47
  /** Scopes whose acceptance is about one thing (a subject), not the account as a whole (#1928). */
@@ -61,17 +70,21 @@ export interface ConsentSeen {
61
70
  version: string;
62
71
  }
63
72
  /**
64
- * One acceptance, one line of the person's file. Append-only: a later
65
- * version is a later line, nothing is rewritten.
73
+ * One acceptance, or its withdrawal, one line of the person's file.
74
+ * Append-only: a later version is a later line, and a withdrawal is a later
75
+ * `withdraw` line naming the same document, version and subject; nothing is
76
+ * rewritten.
66
77
  */
67
78
  export interface ConsentRecord {
68
- kind: 'accept';
79
+ kind: 'accept' | 'withdraw';
69
80
  document: string;
70
81
  scope: ConsentScope;
71
82
  version: string;
72
83
  effective: string | null;
73
- /** ISO 8601 time the person accepted. */
84
+ /** ISO 8601 time the person accepted (on a `withdraw` line, of the acceptance withdrawn). */
74
85
  acceptedAt: string;
86
+ /** ISO 8601 time it was withdrawn; on a `withdraw` line only. */
87
+ withdrawnAt?: string;
75
88
  /**
76
89
  * The account this was accepted on, by its creation time (#1937): a later
77
90
  * account that takes the same username never inherits it.
@@ -31,10 +31,20 @@ export const RECOVERY_WORDS_DOCUMENT = 'recovery-words';
31
31
  export const SHARED_FOLDERS_WORDS_CONSENT_SCOPE = 'shared-folders-recovery-words';
32
32
  /** The page whose `version:` is the version of the shared-folders recovery-words wording (#2124). */
33
33
  export const SHARED_FOLDERS_WORDS_DOCUMENT = 'shared-folders-recovery-words';
34
+ /**
35
+ * The scope of "Remember this browser" (#1523 slice 1c, Q5): a person's
36
+ * opt-in, at the second-factor step, to a device key on this browser that
37
+ * skips the second-factor prompt next time. Bound to a subject: the remembered
38
+ * device's id, so each browser is its own consent, withdrawn by removing it.
39
+ */
40
+ export const REMEMBER_BROWSER_CONSENT_SCOPE = 'remember-browser';
41
+ /** The page whose `version:` is the version of the "Remember this browser" wording (#1523 slice 1c). */
42
+ export const REMEMBER_BROWSER_DOCUMENT = 'remember-browser';
34
43
  /** Core's scopes bound to a subject, each with its one document (#1928, #2124). */
35
44
  export const SUBJECT_CONSENT_DOCUMENTS = {
36
45
  [RECOVERY_WORDS_CONSENT_SCOPE]: RECOVERY_WORDS_DOCUMENT,
37
- [SHARED_FOLDERS_WORDS_CONSENT_SCOPE]: SHARED_FOLDERS_WORDS_DOCUMENT
46
+ [SHARED_FOLDERS_WORDS_CONSENT_SCOPE]: SHARED_FOLDERS_WORDS_DOCUMENT,
47
+ [REMEMBER_BROWSER_CONSENT_SCOPE]: REMEMBER_BROWSER_DOCUMENT
38
48
  };
39
49
  /** Scopes whose acceptance is about one thing (a subject), not the account as a whole (#1928). */
40
50
  export const SUBJECT_CONSENT_SCOPES = Object.keys(SUBJECT_CONSENT_DOCUMENTS);
@@ -95,13 +95,6 @@ export interface PageFrontmatter {
95
95
  * in Slice E (v3.7.0). All datasets migrated; this field is the only source of truth.
96
96
  */
97
97
  private?: boolean;
98
- /** Per-action principal lists — overrides audience for the named action */
99
- access?: {
100
- view?: string[];
101
- edit?: string[];
102
- delete?: string[];
103
- [key: string]: string[] | undefined;
104
- };
105
98
  /** Additional custom metadata */
106
99
  [key: string]: unknown;
107
100
  }