@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
@@ -12,11 +12,12 @@
12
12
  * again because a `.d.ts` may not import a `.ts`. Four copies of one shape is
13
13
  * how a field gets added to one and missed in the others.
14
14
  */
15
- /** Who a policy grants to. The evaluator matches `role` and `job` (#1999), its `SUBJECT_MATCHERS`. */
15
+ /** Who a policy grants to. The evaluator matches `role`, `job` (#1999), `authenticated` and `anyone` (#2261), its `SUBJECT_MATCHERS`. */
16
16
  export interface PolicySubject {
17
- /** `role`, or `job` for a scheduled job acting as itself (#1999). */
17
+ /** `role`, `job` for a scheduled job acting as itself (#1999), or `authenticated` / `anyone` (#2261). */
18
18
  type: string;
19
- value: string;
19
+ /** The role or the job id. `authenticated` and `anyone` take none. */
20
+ value?: string;
20
21
  }
21
22
  /**
22
23
  * What a policy covers.
@@ -28,6 +29,45 @@ export interface PolicyResource {
28
29
  type: string;
29
30
  pattern: string;
30
31
  }
32
+ /**
33
+ * A comparison of one attribute with a value (#2255, #2261). A string starting
34
+ * `subject:`, `resource:` or `environment:` names an attribute; anything else
35
+ * is a literal. Exactly one operator key beside `attribute`:
36
+ * `equals`, `not-equals`, `in`, `greater-or-equal`, `less-or-equal`.
37
+ */
38
+ export type LeafCondition = {
39
+ attribute: string;
40
+ } & ({
41
+ equals: unknown;
42
+ } | {
43
+ 'not-equals': unknown;
44
+ } | {
45
+ in: unknown[] | string;
46
+ } | {
47
+ 'greater-or-equal': number | string;
48
+ } | {
49
+ 'less-or-equal': number | string;
50
+ });
51
+ /**
52
+ * A policy's condition: a {@link LeafCondition}, or `any-of` / `all-of` /
53
+ * `not` over other conditions, nested at most 4 deep.
54
+ *
55
+ * Each node is true, false or unknown. A leaf reading an attribute that is
56
+ * missing is unknown; in `any-of` a true branch decides, in `all-of` a false
57
+ * one does, and `not` of unknown is unknown. An allow applies only when the
58
+ * condition is true; a deny applies when it is true or unknown.
59
+ */
60
+ export type PolicyCondition = LeafCondition | {
61
+ 'any-of': PolicyCondition[];
62
+ } | {
63
+ 'all-of': PolicyCondition[];
64
+ } | {
65
+ not: PolicyCondition;
66
+ };
67
+ /** The prefixes that mark a condition value as an attribute rather than a literal (#2255). */
68
+ export declare const ATTRIBUTE_CATEGORIES: readonly ["subject", "resource", "environment"];
69
+ /** True when `value` names an attribute (`subject:id`, `resource:owner`), not a literal. */
70
+ export declare function isAttributeName(value: unknown): value is string;
31
71
  /** One entry of `ngdpbase.access.policies`. */
32
72
  export interface Policy {
33
73
  id: string;
@@ -44,6 +84,8 @@ export interface Policy {
44
84
  subjects?: PolicySubject[];
45
85
  resources?: PolicyResource[];
46
86
  actions?: string[];
87
+ /** Applies only when this holds (#2255); see {@link PolicyCondition}. */
88
+ condition?: PolicyCondition;
47
89
  [key: string]: unknown;
48
90
  }
49
91
  /**
@@ -88,11 +130,18 @@ export interface DecisionRequest {
88
130
  action: string;
89
131
  resource?: DecisionResource;
90
132
  /**
91
- * What the PIP knows about the resource beyond its reference, for the
92
- * policy resource types that match on it (#1504): a page's
93
- * `system-category`. The PDP passes it through; it never loads it.
133
+ * Where a page or file sits, for the resource types that match on it: the
134
+ * `vault` (#1539) or the `shared-folder` (#2125). A page's own attributes —
135
+ * its `system-category`, its `owner` — are never passed: the PDP asks the
136
+ * PIP for them from the page id (#2255).
94
137
  */
95
138
  attributes?: Record<string, unknown>;
139
+ /**
140
+ * The page's frontmatter when the PIP itself already holds it (the page
141
+ * door, the list filter), so the PDP's lookup reads no page twice. Only the
142
+ * PIP sets it; a route never does (#2255).
143
+ */
144
+ pageMetadata?: Record<string, unknown> | null;
96
145
  /**
97
146
  * Whether a share's `resources` cover the thing being asked about.
98
147
  *
@@ -128,6 +177,18 @@ export interface Decision {
128
177
  */
129
178
  applicable: boolean;
130
179
  reason: string;
180
+ /**
181
+ * The ids of the policies that decided (#2255): the denies that matched,
182
+ * or, when none did, the allows that matched. Empty when no policy matched
183
+ * or something other than policy decided (a ceiling, step-up).
184
+ */
185
+ policyIds: string[];
186
+ /**
187
+ * The ids of allow policies that matched subject, resource and action but
188
+ * whose condition was false or unknown (#2288), so a refusal can say which
189
+ * allow was not met. Ids only, never attribute values. Absent when none.
190
+ */
191
+ conditionNotMet?: string[];
131
192
  }
132
193
  /**
133
194
  * The reason a deny carries when the subject holds the permission but its
@@ -1,4 +1,10 @@
1
1
  'use strict';
2
+ /** The prefixes that mark a condition value as an attribute rather than a literal (#2255). */
3
+ export const ATTRIBUTE_CATEGORIES = ['subject', 'resource', 'environment'];
4
+ /** True when `value` names an attribute (`subject:id`, `resource:owner`), not a literal. */
5
+ export function isAttributeName(value) {
6
+ return typeof value === 'string' && ATTRIBUTE_CATEGORIES.some((c) => value.startsWith(`${c}:`));
7
+ }
2
8
  /**
3
9
  * The reason a deny carries when the subject holds the permission but its
4
10
  * sign-in is not fresh enough to act (#1635). Declared once: the PDP writes it
@@ -319,6 +319,18 @@ export interface PageProvider extends BaseProvider {
319
319
  reason: string;
320
320
  detail?: string;
321
321
  }>;
322
+ /**
323
+ * Where a trashed page would come back to, and its frontmatter, so the
324
+ * write door can decide the restore before it happens (#1815, security
325
+ * review L5): its title, or its vault name when it was deleted from a
326
+ * vault. Null when the uuid is not in the trash.
327
+ *
328
+ * @param uuid - Page UUID
329
+ */
330
+ deletedPageForRestore?(uuid: string): Promise<{
331
+ name: string;
332
+ metadata: PageFrontmatter;
333
+ } | null>;
322
334
  /**
323
335
  * Get all page titles
324
336
  * @returns Sorted array of page titles
@@ -630,6 +642,12 @@ export interface StoreFileInput {
630
642
  keptOriginal?: {
631
643
  types: readonly string[];
632
644
  };
645
+ /**
646
+ * The id to store the file under, set only by AttachmentManager for a file
647
+ * a takeout re-import brings back (#2188), after it checked that no other
648
+ * file anywhere holds it. A store that already lists the id refuses it.
649
+ */
650
+ id?: string;
633
651
  }
634
652
  /**
635
653
  * A page in a private store (#1456): its entry in the store's OWN page index,
@@ -56,6 +56,12 @@ export interface UserPreferences {
56
56
  /** Additional custom preferences */
57
57
  [key: string]: unknown;
58
58
  }
59
+ /**
60
+ * The account fields whose change is recorded on `user-edit` with the field name
61
+ * only, and that a policy condition may never read as `account:` or `profile:`
62
+ * (#2261): the password, the email, the roles and the like.
63
+ */
64
+ export declare const SENSITIVE_USER_FIELDS: readonly ["password", "roles", "isActive", "isExternal", "isTest", "email", "profileLocked", "username"];
59
65
  /**
60
66
  * User object
61
67
  *
@@ -40,6 +40,14 @@ declare module 'express-session' {
40
40
  purpose: 'register' | 'authenticate' | 'reauth';
41
41
  expires: number;
42
42
  };
43
+ /**
44
+ * #421: an authenticator-app seed offered on /profile/totp, until a code confirms it (10 minutes).
45
+ * Sealed for the account by `totpEnrolmentStart`; the plain seed never enters the session.
46
+ */
47
+ totpEnrolment?: {
48
+ pending: string;
49
+ expires: number;
50
+ };
43
51
  /** The step-up asked for and not yet given (#2145): its permission, where it returns, whether returning redoes it. */
44
52
  pendingStepUp?: PendingStepUp;
45
53
  /** The path whose next page view says "Confirmed — you can do it now" after a step-up (#2145). */
@@ -49,6 +57,20 @@ declare module 'express-session' {
49
57
  /** A sign-in waiting for its second factor: the handle, and where to go after (#1523). No identity yet. */
50
58
  pendingSignIn?: string;
51
59
  pendingRedirect?: string;
60
+ /**
61
+ * #1523 slice 1c: "Remember this browser" was ticked and a second factor
62
+ * completed the sign-in; the next page has the browser make its key. The
63
+ * version of the page the person was shown, the sign-in's amr, where to go
64
+ * after, until when (epoch ms), and the id of the sign-in's audit record,
65
+ * which the remembered row names. Single use.
66
+ */
67
+ rememberBrowser?: {
68
+ version: string;
69
+ amr: string[];
70
+ next: string;
71
+ until: number;
72
+ auditId?: string;
73
+ };
52
74
  /** A verified outside identity no account has connected, waiting for the person to sign in another way (#1743). */
53
75
  pendingConnection?: PendingConnection;
54
76
  /** #2073: an outside sign-in whose email several accounts share, waiting for the person to choose one. */
@@ -55,15 +55,3 @@ export interface SyncRowState {
55
55
  * @param userModifiedFlag - The live copy is explicitly marked edited
56
56
  */
57
57
  export declare function syncRowState(seedStatus: SeededAddonPageStatus, userModifiedFlag?: boolean): SyncRowState;
58
- /**
59
- * Default edit access for a page ngdpbase ships (#971, #1411): a `general`
60
- * page is instance-owned and gets no stamp; any other category — system,
61
- * documentation, addon, unclassified — is administrator-edit only. A source
62
- * that declares its own `access` overrides this.
63
- *
64
- * @param category - The page's resolved `system-category`
65
- * @returns The access object to stamp, or undefined to leave the page unprotected
66
- */
67
- export declare function defaultShippedPageAccess(category: unknown): {
68
- edit: string[];
69
- } | undefined;
@@ -0,0 +1,83 @@
1
+ /** `prevHash` of the first record in a chain. */
2
+ export declare const GENESIS_HASH = "genesis";
3
+ /**
4
+ * Hash a record in its position in the chain.
5
+ *
6
+ * `seq` and `prevHash` are part of the input, not just the payload — otherwise
7
+ * records could be reordered, or a record moved to a different predecessor,
8
+ * without breaking anything.
9
+ */
10
+ export declare function hashRecord(record: Record<string, unknown>, seq: number, prevHash: string): string;
11
+ /** Add `seq`, `prevHash` and `hash` to a record. Returns a new object. */
12
+ export declare function stampRecord(record: Record<string, unknown>, seq: number, prevHash: string): Record<string, unknown>;
13
+ /** Outcome of verifying a chain. `brokenAt` is the 1-based position, not the seq. */
14
+ export interface ChainVerdict {
15
+ ok: boolean;
16
+ checked: number;
17
+ brokenAt?: number;
18
+ reason?: string;
19
+ }
20
+ /**
21
+ * Where a chain that does not begin at genesis begins (#1537): the sequence
22
+ * and hash of the last record before the first one present. A retention
23
+ * checkpoint holds exactly this for the records it removed, so what remains
24
+ * verifies from it rather than from genesis.
25
+ */
26
+ export interface ChainStart {
27
+ seq: number;
28
+ hash: string;
29
+ }
30
+ /**
31
+ * Walk a chain in order and report the first break.
32
+ *
33
+ * @param records - Audit records, oldest first
34
+ * @param options.expectedHead - Hash the last record must have. Supplying it is
35
+ * the only way to detect truncation: removing records from the end breaks no
36
+ * link, so the chain alone cannot see it.
37
+ * @param options.start - The checkpoint the chain continues from (#1537). The
38
+ * first record must then carry `start.seq + 1` and link to `start.hash`;
39
+ * without it the first record must link to genesis.
40
+ */
41
+ export declare function verifyChain(records: readonly Record<string, unknown>[], options?: {
42
+ expectedHead?: string;
43
+ start?: ChainStart;
44
+ }): ChainVerdict;
45
+ /** Event type of a record that declares a deliberate chain discontinuity (#1124). */
46
+ export declare const CHAIN_RESTART_EVENT: "audit-chain-restart";
47
+ /** One contiguous chain within a log. */
48
+ export interface ChainSegment {
49
+ /** Index of the segment's first record in the input. */
50
+ startIndex: number;
51
+ verdict: ChainVerdict;
52
+ /** The restart marker that begins this segment, if it was started by one. */
53
+ restart?: Record<string, unknown>;
54
+ /** True when a break at the END of this segment is followed by a restart marker. */
55
+ explained: boolean;
56
+ /** True when this segment's marker names a previous head that does not match. */
57
+ mismatchedPrevious: boolean;
58
+ }
59
+ export interface LogVerdict {
60
+ ok: boolean;
61
+ segments: ChainSegment[];
62
+ }
63
+ /**
64
+ * Verify a log that may contain deliberate discontinuities (#1124).
65
+ *
66
+ * `verifyChain` answers "is this one chain intact", which is the right question
67
+ * for a chain and the wrong one for a log that has legitimately restarted — a
68
+ * migration, a restore, or a bug that wrote unverifiable records. Without this,
69
+ * one bad record leaves a log unverifiable forever and the only ways forward
70
+ * are dishonest: edit the log, delete it, or ignore the verifier.
71
+ *
72
+ * A break is __explained__ only when the very next record is a restart marker.
73
+ * That is what keeps the marker from being an escape hatch: an attacker who
74
+ * wants a clean chain must write a record saying they broke it, which is a
75
+ * confession rather than a cover-up.
76
+ *
77
+ * The marker's claimed `previousHash` is checked against the head it actually
78
+ * follows. A marker asserting a history that did not happen is itself a
79
+ * finding — otherwise a restart could claim anything.
80
+ */
81
+ export declare function verifyLog(records: readonly Record<string, unknown>[], options?: {
82
+ start?: ChainStart;
83
+ }): LogVerdict;
@@ -224,6 +224,8 @@ export declare const AUDIT_EVENT: {
224
224
  readonly USER_EDIT: "user-edit";
225
225
  /** User profile read; not recorded, read volume */
226
226
  readonly USER_READ: "user-read";
227
+ /** An administrator read or changed a page inside another person's vault that is not encrypted (ngdpbase.vaults.admin-access, #2255): the administrator is the actor, and the vault's owner, the vault and the page are named */
228
+ readonly VAULT_ADMIN_ACCESS: "vault-admin-access";
227
229
  /** Not an action: read from a proxy grant at the vault door; the grant itself is recorded as proxy-grant */
228
230
  readonly VAULT_PROXY: "vault-proxy";
229
231
  /** A vault page's leftovers moved into its vault (#1842): its shared-pool files (moved, or copied where other public pages use them) and its footnotes and comments from the public folders. Names the owner, vault and page uuid and the counts, never a title or file name. Run by the vault's owner, or for a vault that isn't sealed by someone holding admin-system */
@@ -231,6 +231,8 @@ export const AUDIT_EVENT = {
231
231
  USER_EDIT: 'user-edit',
232
232
  /** User profile read; not recorded, read volume */
233
233
  USER_READ: 'user-read',
234
+ /** An administrator read or changed a page inside another person's vault that is not encrypted (ngdpbase.vaults.admin-access, #2255): the administrator is the actor, and the vault's owner, the vault and the page are named */
235
+ VAULT_ADMIN_ACCESS: 'vault-admin-access',
234
236
  /** Not an action: read from a proxy grant at the vault door; the grant itself is recorded as proxy-grant */
235
237
  VAULT_PROXY: 'vault-proxy',
236
238
  /** A vault page's leftovers moved into its vault (#1842): its shared-pool files (moved, or copied where other public pages use them) and its footnotes and comments from the public folders. Names the owner, vault and page uuid and the counts, never a title or file name. Run by the vault's owner, or for a vault that isn't sealed by someone holding admin-system */
@@ -142,6 +142,8 @@ export interface TokenInput extends CommonInput {
142
142
  previousExpiresAt?: string;
143
143
  /** Only meaningful for `revoke`; ignored otherwise. */
144
144
  revokedBy?: string;
145
+ /** Only meaningful for `mint` (#2287): the sign-in level stored with the token; absent when it stored none. */
146
+ aal?: 1 | 2 | 3;
145
147
  }
146
148
  /**
147
149
  * What an attachment record says about the file. A file in an ENCRYPTED store
@@ -289,7 +289,7 @@ export async function recordAuditEvent(sink, event, onError) {
289
289
  * every event is useless as a filter.
290
290
  */
291
291
  export function buildTokenAuditEvent(input) {
292
- const { op, id, owner, ownerUser, name, scopes, expiresAt, previousExpiresAt, revokedBy, viaToken } = input;
292
+ const { op, id, owner, ownerUser, name, scopes, expiresAt, previousExpiresAt, revokedBy, viaToken, aal } = input;
293
293
  const who = whoOf(input);
294
294
  const metadata = {
295
295
  id,
@@ -302,6 +302,8 @@ export function buildTokenAuditEvent(input) {
302
302
  if (op === 'mint') {
303
303
  metadata.scopes = [...(scopes ?? [])];
304
304
  metadata.expiresAt = expiresAt;
305
+ // #2287: the level the token holds, or null when it stored none; the record that answers "was this minted after a second factor?"
306
+ metadata.aal = aal ?? null;
305
307
  }
306
308
  if (op === 'extend') {
307
309
  metadata.previousExpiresAt = previousExpiresAt;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Off-box witness for the audit chain head (#1138).
3
+ *
4
+ * The chain detects alteration and deletion, and __cannot detect truncation of
5
+ * the tail__: removing records from the end breaks no link. That is also the
6
+ * attack an intruder actually wants — remove the record of what they just did.
7
+ * Everything before their session verifies perfectly, and the log ends looking
8
+ * like a clean shutdown.
9
+ *
10
+ * It cannot be fixed locally, for the reason #1119 made the chain functions
11
+ * pure: an attacker who owns the machine owns anything the machine wrote
12
+ * locally, including a head stored beside the log. __The head has to leave the
13
+ * box, or it is not evidence.__
14
+ *
15
+ * Three decisions from docs/security-posture.md shape what this does and does
16
+ * not do:
17
+ *
18
+ * - __D13__ — deployment methodology does not influence the design. This
19
+ * publishes a head to a configured destination and takes no view on what that
20
+ * destination is. Whether it is genuinely off-box is the operator's decision
21
+ * and the operator's to state.
22
+ * - __D21__ — report facts, not a claim. There is deliberately no `offBox:
23
+ * true` produced here. Nothing on this machine can verify that a path leaves
24
+ * it, and asserting so would be the `durable` defect (#1148) repeated: a
25
+ * boolean derived from something that does not establish it.
26
+ * - __D20__ — no scoring. A verdict says what is observably true of the records
27
+ * and the witness, and stops there.
28
+ */
29
+ /** What is published: one sequence number and one hash. Nothing else. */
30
+ export interface ChainWitness {
31
+ seq: number;
32
+ hash: string;
33
+ /** Which instance the head belongs to, so one store can hold several. */
34
+ instance: string;
35
+ publishedAt: string;
36
+ }
37
+ /** The minimum of a stored record this assessment needs. */
38
+ export interface ChainRecord {
39
+ seq?: number;
40
+ hash?: string;
41
+ }
42
+ export type WitnessVerdict =
43
+ /** The log is consistent with the witness. */
44
+ 'intact'
45
+ /** The witness is ahead of the log: records were removed from the end. */
46
+ | 'truncated'
47
+ /** The witnessed sequence is present with a different hash, or absent. */
48
+ | 'altered'
49
+ /** No witness. NOT the same statement as "intact". */
50
+ | 'unknown';
51
+ export interface WitnessAssessment {
52
+ verdict: WitnessVerdict;
53
+ /** How many records are missing from the end. Only meaningful when truncated. */
54
+ missing?: number;
55
+ reason?: string;
56
+ }
57
+ export interface BuildWitnessInput {
58
+ seq: number;
59
+ hash: string;
60
+ instance: string;
61
+ at: Date;
62
+ }
63
+ /**
64
+ * Build the record to publish.
65
+ *
66
+ * The head is one hash and one sequence number. Publishing more would put
67
+ * audit CONTENT wherever the witness lives, which is a different and much
68
+ * larger trust decision than publishing a fingerprint of it.
69
+ */
70
+ export declare function buildWitness(input: BuildWitnessInput): ChainWitness;
71
+ /**
72
+ * Is it time to publish again?
73
+ *
74
+ * The gap between publications is the window an attacker can truncate within,
75
+ * which makes the interval the actual security parameter — worth setting
76
+ * explicitly rather than inheriting.
77
+ *
78
+ * A zero or negative interval publishes every time rather than never: a
79
+ * misconfigured interval must fail toward more evidence, not toward silently
80
+ * disabling the witness.
81
+ */
82
+ export declare function shouldPublish(lastPublishedAtMs: number | null, nowMs: number, intervalMs: number): boolean;
83
+ /**
84
+ * Compare a published head against the records now on disk.
85
+ *
86
+ * The verdicts are kept distinct because an assessor needs the difference:
87
+ * truncation says records were removed, alteration says one was rewritten, and
88
+ * reporting both as "broken" loses what they would act on.
89
+ *
90
+ * `unknown` is never reported as `intact`. "Nothing is witnessing this log" and
91
+ * "this log is intact" are different statements, and deriving the second from
92
+ * the first is exactly the claim #1148 removed elsewhere.
93
+ */
94
+ export declare function assessWitness(witness: ChainWitness | {
95
+ seq: number;
96
+ hash: string;
97
+ } | null, records: readonly ChainRecord[]): WitnessAssessment;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Process lifecycle as audit events (#1149).
3
+ *
4
+ * Nothing recorded that an instance started or stopped, so an unclean exit was
5
+ * undetectable from the log. That matters because of #1148: on an unclean exit
6
+ * the buffered audit records are lost, the chain resumes from the last WRITTEN
7
+ * record at boot, verification passes, and nothing shows anything is absent.
8
+ * The log does not lie about the records it holds — it simply cannot say that
9
+ * records are missing.
10
+ *
11
+ * A `system-start` with no `system-shutdown` before it says exactly that. It
12
+ * cannot recover the lost records; it marks the window in which records may be
13
+ * absent, which is the difference between an audit trail with a known gap and
14
+ * one that quietly appears complete.
15
+ */
16
+ import type { AuditEvent } from './auditEvents.js';
17
+ /** What the previous run's ending can be established to be. */
18
+ export type PreviousRun =
19
+ /** A shutdown was recorded after the last start. */
20
+ 'clean'
21
+ /** A start was recorded with no shutdown after it. Records may be missing. */
22
+ | 'unclean'
23
+ /** No start on record — a first boot, or history beyond the log. */
24
+ | 'none'
25
+ /** The records present cannot establish an ordering. */
26
+ | 'unknown';
27
+ /** The minimum of a stored record this assessment needs. */
28
+ export interface LifecycleRecord {
29
+ timestamp?: string;
30
+ }
31
+ /**
32
+ * Decide how the previous run ended, from the latest record of each phase.
33
+ *
34
+ * Deliberately conservative in three places, because the whole value of this
35
+ * is that it does not overclaim:
36
+ *
37
+ * - No start on record is `none`, not `unclean`. A first boot must not accuse
38
+ * itself, or every new instance cries wolf and the signal stops being read.
39
+ * - A shutdown with no start is `unknown`, not `clean`. That is only reachable
40
+ * through a truncated or rotated log, and concluding "clean" from half the
41
+ * evidence is the failure this exists to prevent.
42
+ * - Equal timestamps are `unclean`. Same-millisecond records cannot establish
43
+ * ordering, so the shutdown did not *demonstrably* follow the start.
44
+ */
45
+ export declare function assessPreviousRun(lastStart: LifecycleRecord | null, lastShutdown: LifecycleRecord | null): PreviousRun;
46
+ export interface LifecycleEventInput {
47
+ /** The system principal (#631). Falls back to the literal only where no UserManager answers. */
48
+ principal?: string;
49
+ phase: 'start' | 'shutdown';
50
+ /** The running version, so a restart is distinguishable from an upgrade. */
51
+ version: string;
52
+ pid: number;
53
+ /** Required on a start, meaningless on a shutdown. */
54
+ previousRun?: PreviousRun;
55
+ /**
56
+ * The transport the instance actually bound (#1153).
57
+ *
58
+ * "This instance is serving HTTP" is a security fact, and the log had no
59
+ * record of it — only the version and pid.
60
+ */
61
+ scheme?: 'http' | 'https';
62
+ }
63
+ /**
64
+ * Build the audit event for a process starting or stopping.
65
+ *
66
+ * `previousRun` appears on a start only, following the rule `page-rename` and
67
+ * `token-mint` already use: a field present on every event is useless as a
68
+ * filter. A shutdown is a statement about itself and makes no claim about the
69
+ * past.
70
+ */
71
+ export declare function buildLifecycleAuditEvent(input: LifecycleEventInput): AuditEvent;
@@ -9,3 +9,21 @@
9
9
  * Null when the header names neither a browser nor a device we know.
10
10
  */
11
11
  export declare function describeBrowser(userAgent: string | undefined): string | null;
12
+ /**
13
+ * #1523 slice 1c — the browser and operating system a remembered browser's
14
+ * device key was issued to, from the User-Agent and the `Sec-CH-UA-Platform`
15
+ * client hint every Chromium browser sends. Families only ("Chrome",
16
+ * "Windows"), never versions, so an update keeps the browser remembered. No
17
+ * fingerprinting beyond what each request already carries (operator,
18
+ * 2026-10-04). Null when the browser or the system cannot be told: such a
19
+ * browser is never remembered.
20
+ */
21
+ export interface BrowserIdentity {
22
+ browser: string;
23
+ os: string;
24
+ /** The `Sec-CH-UA-Platform` hint, unquoted; empty when the browser sent none. */
25
+ platform: string;
26
+ }
27
+ export declare function browserIdentity(userAgent: string | undefined, platformHint: string | undefined): BrowserIdentity | null;
28
+ /** Whether two identities name the same browser on the same system, the hint included (both sent it, or neither). */
29
+ export declare function sameBrowser(a: BrowserIdentity, b: BrowserIdentity): boolean;
@@ -26,3 +26,15 @@ export function describeBrowser(userAgent) {
26
26
  return `${browser} on ${device}`;
27
27
  return browser ?? device;
28
28
  }
29
+ export function browserIdentity(userAgent, platformHint) {
30
+ const described = describeBrowser(userAgent);
31
+ const [browser, os] = described?.split(' on ') ?? [];
32
+ if (!browser || !os)
33
+ return null;
34
+ const platform = (platformHint ?? '').trim().replace(/^"|"$/g, '').slice(0, 40);
35
+ return { browser, os, platform };
36
+ }
37
+ /** Whether two identities name the same browser on the same system, the hint included (both sent it, or neither). */
38
+ export function sameBrowser(a, b) {
39
+ return a.browser === b.browser && a.os === b.os && a.platform === b.platform;
40
+ }
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * frontmatterAccess — the single implementation of ACL Tier 1 (#1054).
3
3
  *
4
- * A page's frontmatter can restrict who may act on it, via `access[<action>]`
5
- * or `audience`, which covers every action (#1964). Two subsystems needed
6
- * that answer and each had its own version of it, which is how they came to disagree:
4
+ * A page's frontmatter can restrict who may act on it with `audience`, which
5
+ * covers every action (#1964). The `access[<action>]` lists it once honoured
6
+ * were removed in #2255: what a user may do on a page is decided by its
7
+ * system-category and policy. Two subsystems needed the audience answer and
8
+ * each had its own version of it, which is how they came to disagree:
7
9
  *
8
10
  * - `PolicyInformationPoint.checkFrontmatterAccess` decided a request, correctly.
9
11
  * - `VersioningFileProvider.getRecentChanges` decided a listing, and only
@@ -20,8 +22,6 @@
20
22
  */
21
23
  /** The frontmatter fields Tier 1 reads. */
22
24
  export interface AccessMetadataLike {
23
- /** `{ view: ['role'], edit: [...] }` — per-action principal lists. */
24
- access?: unknown;
25
25
  /** Who may act on the page at all; administrators excepted (#1964). */
26
26
  audience?: unknown;
27
27
  }
@@ -39,11 +39,6 @@ export interface FrontmatterDecision {
39
39
  * neither read nor edit the page.
40
40
  */
41
41
  export declare function audiencePrincipals(metadata: AccessMetadataLike | null | undefined): string[] | null;
42
- /**
43
- * The page's `access[action]` list, or null when it states none. It applies
44
- * to that action only, and to everyone — administrators included.
45
- */
46
- export declare function accessPrincipals(metadata: AccessMetadataLike | null | undefined, action: string): string[] | null;
47
42
  /**
48
43
  * The entry `addon:<slug>` (#1964): "people who turned on the per-person
49
44
  * add-on <slug>". It is matched only against the viewer's enabled add-ons,
@@ -54,11 +49,10 @@ export declare const ADDON_PRINCIPAL_PREFIX = "addon:";
54
49
  * Whether deciding `action` on this page needs the viewer's enabled add-ons —
55
50
  * so a caller asks for them only when the page names one (#1964).
56
51
  */
57
- export declare function namesAddonPrincipal(metadata: AccessMetadataLike | null | undefined, action: string): boolean;
52
+ export declare function namesAddonPrincipal(metadata: AccessMetadataLike | null | undefined, _action: string): boolean;
58
53
  /**
59
- * Every add-on slug the page names in `audience` or in any `access` list, once
60
- * each, in the order first named — for the warning about a slug that can match
61
- * nobody (#1964).
54
+ * Every add-on slug the page names in `audience`, once each, in the order
55
+ * first named — for the warning about a slug that can match nobody (#1964).
62
56
  */
63
57
  export declare function addonAudienceSlugs(metadata: AccessMetadataLike | null | undefined): string[];
64
58
  /**
@@ -75,16 +69,13 @@ export declare const AUDIENCE_ADMIN_MATCH = "admin-system";
75
69
  * plus their username. Callers holding a `UserContext` should pass
76
70
  * `[...roles, username]`; the provider layer already works in these terms.
77
71
  *
78
- * Two rules, in order (#1964, operator 2026-10-23):
79
- *
80
- * - __`audience`__ applies to every action. Someone it leaves out may neither
81
- * read nor edit the page: every action is refused. The one exception is an
82
- * administrator — a viewer holding `admin-system`, which the caller decides
83
- * and passes as `isAdmin` (this module never reads a role for it). For
84
- * `view`, a viewer the audience admits is allowed; for any other action it
85
- * only lets them through to `access` and then policy.
86
- * - __`access[action]`__ applies to that action, to everyone, administrators
87
- * included. When the page states it, it decides.
72
+ * `audience` applies to every action (#1964, operator 2026-10-23). Someone it
73
+ * leaves out may neither read nor edit the page: every action is refused. The
74
+ * one exception is an administrator — a viewer holding `admin-system`, which
75
+ * the caller decides and passes as `isAdmin` (this module never reads a role
76
+ * for it). For `view`, a viewer the audience admits is allowed; for any other
77
+ * action it only lets them through to policy. A page's `access` field is not
78
+ * read (#2255).
88
79
  *
89
80
  * An `addon:<slug>` entry matches only when `<slug>` is in `enabledAddons`,
90
81
  * the viewer's enabled per-person add-ons (#1964); a caller that does not
@@ -0,0 +1,16 @@
1
+ /** One leftover key and what to tell the operator about it. */
2
+ export interface LegacyStoreKey {
3
+ key: string;
4
+ kind: string;
5
+ setting: 'owner' | 'encrypt';
6
+ /** One sentence: the key, that it has no effect, and what replaces it. */
7
+ message: string;
8
+ }
9
+ /**
10
+ * Every legacy per-kind key in the operator's custom config, in file order.
11
+ * `categories` is the merged `ngdpbase.system-category` map, used to name the
12
+ * entry that declares the kind's vault (the `default` vault is `general`'s).
13
+ */
14
+ export declare function legacyStoreKeys(customConfig: Record<string, unknown> | null | undefined, categories: unknown): LegacyStoreKey[];
15
+ /** The needs-attention reason for a non-empty list: every key, in one sentence. */
16
+ export declare function legacyStoreKeysReason(found: LegacyStoreKey[]): string;