@jwilleke/ngdpbase-types 5.26.0 → 5.28.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 (52) hide show
  1. package/lib/packages/ngdpbase-types/index.d.ts +8 -2
  2. package/lib/packages/ngdpbase-types/testing.d.ts +7 -3
  3. package/lib/packages/ngdpbase-types/testing.js +7 -3
  4. package/lib/src/context/ActorContext.d.ts +15 -2
  5. package/lib/src/context/ActorContext.js +20 -0
  6. package/lib/src/converters/DocxConverter.d.ts +30 -0
  7. package/lib/src/converters/MarkdownConverter.d.ts +31 -0
  8. package/lib/src/export/ChromeServicePdfProvider.d.ts +52 -0
  9. package/lib/src/export/ChromeServicePdfProvider.js +153 -0
  10. package/lib/src/export/LocalChromePdfProvider.d.ts +25 -0
  11. package/lib/src/export/LocalChromePdfProvider.js +33 -0
  12. package/lib/src/export/PdfProvider.d.ts +51 -0
  13. package/lib/src/export/PdfProvider.js +37 -0
  14. package/lib/src/export/pdfEngine.d.ts +113 -0
  15. package/lib/src/export/pdfEngine.js +195 -0
  16. package/lib/src/ice/converterShim.d.ts +5 -0
  17. package/lib/src/ice/fileHandling.d.ts +30 -0
  18. package/lib/src/ice/ledger.d.ts +37 -0
  19. package/lib/src/ice/runImport.d.ts +51 -0
  20. package/lib/src/ice/types.d.ts +284 -0
  21. package/lib/src/mail/MailProvider.d.ts +15 -0
  22. package/lib/src/mail/NodemailerMailProvider.d.ts +1 -1
  23. package/lib/src/managers/AddonsManager.d.ts +25 -5
  24. package/lib/src/managers/AssetManager.d.ts +7 -3
  25. package/lib/src/managers/EmailManager.d.ts +32 -5
  26. package/lib/src/managers/ExportManager.d.ts +62 -0
  27. package/lib/src/managers/ExportManager.js +152 -2
  28. package/lib/src/managers/ImportManager.d.ts +611 -0
  29. package/lib/src/managers/OrganizationManager.d.ts +5 -3
  30. package/lib/src/managers/PageManager.d.ts +77 -1
  31. package/lib/src/managers/PersonManager.d.ts +36 -11
  32. package/lib/src/managers/RoleManager.d.ts +76 -17
  33. package/lib/src/managers/UserManager.d.ts +55 -2
  34. package/lib/src/security/PolicyDecisionPoint.d.ts +18 -9
  35. package/lib/src/security/PolicyInformationPoint.d.ts +48 -10
  36. package/lib/src/types/Config.d.ts +2 -0
  37. package/lib/src/types/User.d.ts +1 -1
  38. package/lib/src/utils/addonHost.d.ts +23 -2
  39. package/lib/src/utils/addonHost.js +25 -2
  40. package/lib/src/utils/auditEventNames.generated.d.ts +10 -10
  41. package/lib/src/utils/auditEventNames.generated.js +10 -10
  42. package/lib/src/utils/contactAccess.d.ts +30 -0
  43. package/lib/src/utils/frontmatterAccess.d.ts +54 -11
  44. package/lib/src/utils/importRunSummary.d.ts +64 -0
  45. package/lib/src/utils/keyedSerial.d.ts +31 -0
  46. package/lib/src/utils/ncmNotify.d.ts +30 -0
  47. package/lib/src/utils/privateStoreAccess.js +1 -3
  48. package/lib/src/utils/privateStoreImport.d.ts +58 -0
  49. package/lib/src/utils/roleChangeError.d.ts +25 -0
  50. package/lib/src/utils/subjectMayDo.d.ts +2 -2
  51. package/lib/src/utils/zipArchive.d.ts +71 -0
  52. package/package.json +1 -1
@@ -184,6 +184,15 @@ export type PageRead<T> = {
184
184
  export interface PageConvertResult extends NcmResult {
185
185
  fixes: FixChange[];
186
186
  }
187
+ /**
188
+ * A write refused because the page's `audience` leaves the writer out (#1964).
189
+ * Someone outside a page's audience may neither read nor edit it; an
190
+ * administrator (`admin-system`) is the exception. Routes answer it with 403.
191
+ */
192
+ export declare class PageAudienceRefusedError extends Error {
193
+ readonly pageName: string;
194
+ constructor(pageName: string);
195
+ }
187
196
  export declare class PageContentValidationError extends Error {
188
197
  readonly validationErrors: FilterValidationError[];
189
198
  constructor(pageName: string, validationErrors: FilterValidationError[]);
@@ -797,10 +806,22 @@ declare class PageManager extends BaseManager implements CatalogSource {
797
806
  * the normal getPage() path sanitises or normalises away. Returns null
798
807
  * when the page is unknown or the provider doesn't support raw reads.
799
808
  */
800
- getRawPageContent(identifier: string): Promise<{
809
+ getRawPageContent(identifier: string, ctx: ActorContext): Promise<{
801
810
  filePath: string;
802
811
  content: string;
803
812
  } | null>;
813
+ /**
814
+ * The write door's audience check (#1964, operator 2026-10-23): someone a
815
+ * page's `audience` leaves out may neither read nor edit it, by any route —
816
+ * the editor, the raw editor, the API, a rename, a delete. The rule is the
817
+ * PIP's (`audienceAdmits`, the same one Tier 1 applies), asked with the
818
+ * caller's context; an administrator (`admin-system`) is admitted. A page
819
+ * that states no audience is not checked here — the routes ask the full
820
+ * page decision before they call the door.
821
+ *
822
+ * @throws PageAudienceRefusedError when the audience leaves `ctx` out
823
+ */
824
+ private assertAudienceAdmits;
804
825
  /**
805
826
  * Admin-override save (#689): the admin "Edit raw" textarea, split into
806
827
  * frontmatter and body and written through the door (#1462).
@@ -1107,6 +1128,38 @@ declare class PageManager extends BaseManager implements CatalogSource {
1107
1128
  * const deleted = await pageManager.deletePage('Old Page', req.userContext);
1108
1129
  */
1109
1130
  deletePage(identifier: string, ctx: ActorContext): Promise<boolean>;
1131
+ /**
1132
+ * Delete the pages a test made (#2008): every shared page carrying the
1133
+ * `test-artifact` system keyword (#1355), and never a page without it.
1134
+ * `only` narrows the set further — a deleted test account's profile is
1135
+ * `{ author, systemCategory: 'user-profile' }`.
1136
+ *
1137
+ * Each page is recorded as `page-delete` before it is deleted (on-failure
1138
+ * refuse: a page whose record cannot be written is not deleted), then goes
1139
+ * through {@link deletePage}, so the delete is soft and the shared indexes
1140
+ * follow. One page's failure never stops the rest; it is reported.
1141
+ *
1142
+ * The caller decides who may ask: the sweep route asks `admin-system`, and
1143
+ * an account delete has already been decided as `user-delete`. Private
1144
+ * vault pages are not in the shared index, so they are not reached here.
1145
+ *
1146
+ * @param ctx - Who is acting; named in every record
1147
+ * @param only - Narrow to one author and/or system-category
1148
+ */
1149
+ deleteTestArtifacts(ctx: ActorContext, only?: {
1150
+ author?: string;
1151
+ systemCategory?: string;
1152
+ }): Promise<{
1153
+ deleted: Array<{
1154
+ title: string;
1155
+ uuid: string;
1156
+ }>;
1157
+ failed: Array<{
1158
+ title: string;
1159
+ uuid: string;
1160
+ error: string;
1161
+ }>;
1162
+ }>;
1110
1163
  /**
1111
1164
  * The name a page was reached by, as the shared indexes key it: a private
1112
1165
  * page's path (#1456), else its title — the caller may have used a uuid or
@@ -1290,6 +1343,29 @@ declare class PageManager extends BaseManager implements CatalogSource {
1290
1343
  * @param action - Legacy verb (`view`, `edit`) or policy action; default `view`
1291
1344
  */
1292
1345
  filterPagesFor(subject: ActorContext | PermissionSubject | null | undefined, names: readonly string[], action?: string): Promise<string[]>;
1346
+ /**
1347
+ * The `addon:<slug>` entries on a page that can match nobody (#1964): the
1348
+ * slugs it names in `audience` or `access` that are not a loaded per-person
1349
+ * add-on. Such an entry still decides — it refuses everyone it would have
1350
+ * let in — so the author is told.
1351
+ *
1352
+ * @param metadata - The page's frontmatter
1353
+ * @returns The slugs, in the order the page names them
1354
+ */
1355
+ unmatchableAddonAudience(metadata: Partial<PageFrontmatter> | null | undefined): string[];
1356
+ /** Warn, naming the page and each slug, about {@link unmatchableAddonAudience}. */
1357
+ private warnUnmatchableAddonAudience;
1358
+ /**
1359
+ * At startup, once add-ons have loaded (#1964): warn about every page whose
1360
+ * `audience` or `access` names an add-on that can match nobody — unknown,
1361
+ * not per-person, or not loaded.
1362
+ *
1363
+ * @returns Each page and slug warned about
1364
+ */
1365
+ reportUnmatchableAddonAudiences(): Promise<Array<{
1366
+ page: string;
1367
+ slug: string;
1368
+ }>>;
1293
1369
  /**
1294
1370
  * Get all page titles (explicit alias for getAllPages)
1295
1371
  * Prefer this for new code that only needs page names.
@@ -23,8 +23,13 @@ export type LinkOfferOutcome = 'sent' | 'no-match' | 'several' | 'unavailable' |
23
23
  * `linkAccount` take the caller's context first and ask the PDP: `contact-read`
24
24
  * to list, search or get; `contact-manage` to change (`linkAccount` also `user-edit`). Each change is recorded as
25
25
  * the `contact-manage` audit event. A refusal throws `ContactRefusedError`.
26
- * `getById` / `getByIdentifier` stay as context-free lookups for core's own
27
- * hot paths (RoleManager resolves roles through them on every request).
26
+ *
27
+ * Every lookup takes the caller's context too (#2018): `getById`,
28
+ * `getByIdentifier` and `getByUserIdentifier`. A user reads their own identity
29
+ * record, and `update` changes it, under `profile-manage`; anyone else's needs
30
+ * `contact-read` / `contact-manage`. Core's own reads (RoleManager resolving
31
+ * roles on every request, UserManager's account lifecycle) pass the system
32
+ * context, allowed without a policy evaluation (`requireRecordAccess`).
28
33
  *
29
34
  * A later sign-up joins a person with no account in one of two ways: the new
30
35
  * account confirms an emailed link (`offerSignUpLink` → `confirmSignUpLink`),
@@ -32,8 +37,8 @@ export type LinkOfferOutcome = 'sent' | 'no-match' | 'several' | 'unavailable' |
32
37
  * person keeps its `@id` and takes the account; the person made for the new
33
38
  * account is folded into it and removed, so there is never a second person.
34
39
  *
35
- * `getByUserIdentifier(username)` remains as a lazy-migration entry point
36
- * for legacy users that pre-date the sync wiring.
40
+ * `getByUserIdentifier(ctx, username)` remains as a lazy-migration entry
41
+ * point for legacy users that pre-date the sync wiring.
37
42
  */
38
43
  declare class PersonManager extends BaseManager {
39
44
  readonly description = "Canonical Person records (#617)";
@@ -56,15 +61,26 @@ declare class PersonManager extends BaseManager {
56
61
  */
57
62
  backupFolders(): BackupFolder[];
58
63
  getProvider(): PersonProvider | null;
59
- getById(id: string): Promise<Person | null>;
60
- getByIdentifier(identifier: string): Promise<Person | null>;
64
+ /**
65
+ * One identity record by `@id` (#2018): the caller's own under
66
+ * `profile-manage`, anyone else's under `contact-read`, core's own work
67
+ * under the system context. A refusal throws `ContactRefusedError`.
68
+ */
69
+ getById(ctx: ActorContext, id: string): Promise<Person | null>;
70
+ /**
71
+ * The identity record whose `identifier` is `identifier` (a username, or a
72
+ * contact's `person:<uuid>`), decided as {@link getById} before it is read.
73
+ */
74
+ getByIdentifier(ctx: ActorContext, identifier: string): Promise<Person | null>;
75
+ private cachedById;
76
+ private cachedByIdentifier;
61
77
  /** #1006: how many people this instance holds. Count only, never records. */
62
78
  getManagerStats(): Promise<ManagerStats>;
63
79
  /** True when the person has no account: its identifier is `person:<uuid>` (#1930). */
64
80
  static isAccountless(person: Pick<Person, 'identifier'>): boolean;
65
81
  /** Every person, for a caller who may read contacts (`contact-read`). */
66
82
  list(ctx: ActorContext): Promise<Person[]>;
67
- /** One person by `@id`, for a caller who may read contacts (`contact-read`). */
83
+ /** One person by `@id`: {@link getById}, the caller's own under `profile-manage`, anyone else's under `contact-read`. */
68
84
  get(ctx: ActorContext, id: string): Promise<Person | null>;
69
85
  /**
70
86
  * Persons whose name (`name`, `givenName`, `familyName`) or `email` contains
@@ -82,7 +98,11 @@ declare class PersonManager extends BaseManager {
82
98
  * person yet: that is how UserManager makes the person for a new account.
83
99
  */
84
100
  create(ctx: ActorContext, input: PersonInput): Promise<Person>;
85
- /** Change a person's fields, affiliations included. `@id` and `identifier` never change here. */
101
+ /**
102
+ * Change a person's fields, affiliations included. `@id` and `identifier`
103
+ * never change here. The caller's own identity record under
104
+ * `profile-manage`, anyone else's under `contact-manage` (#2018).
105
+ */
86
106
  update(ctx: ActorContext, id: string, patch: PersonUpdate): Promise<Person | null>;
87
107
  delete(ctx: ActorContext, id: string): Promise<boolean>;
88
108
  /**
@@ -123,7 +143,10 @@ declare class PersonManager extends BaseManager {
123
143
  * to it, and that person is removed. Never a second person.
124
144
  */
125
145
  private join;
126
- /** Each affiliation names an organization that exists; `role` is trimmed text. Throws on any other shape. */
146
+ /**
147
+ * Each affiliation names an organization that exists, looked up with the
148
+ * caller's context; `role` is trimmed text. Throws on any other shape.
149
+ */
127
150
  private checkedAffiliations;
128
151
  private configNumber;
129
152
  /**
@@ -136,9 +159,11 @@ declare class PersonManager extends BaseManager {
136
159
  *
137
160
  * Returns the Person paired to `username`. If none exists and a matching
138
161
  * User does, a Person is created from the User's email/displayName and
139
- * persisted. Subsequent calls return the persisted record.
162
+ * persisted. Subsequent calls return the persisted record. Decided as
163
+ * {@link getByIdentifier} (#2018): a signed-in user's own under
164
+ * `profile-manage` ("your receipts"), anyone else's under `contact-read`.
140
165
  */
141
- getByUserIdentifier(username: string): Promise<Person | null>;
166
+ getByUserIdentifier(ctx: ActorContext, username: string): Promise<Person | null>;
142
167
  private requireProvider;
143
168
  private getMetrics;
144
169
  private normalizeProviderName;
@@ -12,6 +12,9 @@ declare class RoleManager extends BaseManager {
12
12
  private storageDir;
13
13
  private memberCache;
14
14
  private byOrgPositionCache;
15
+ private cacheGeneration;
16
+ /** #2015: one membership change at a time per role record, read included. */
17
+ private readonly membership;
15
18
  private metricsRef;
16
19
  constructor(engine: WikiEngine);
17
20
  initialize(config?: Record<string, unknown>): Promise<void>;
@@ -42,7 +45,8 @@ declare class RoleManager extends BaseManager {
42
45
  backupFolders(): BackupFolder[];
43
46
  /** Provider accessor, mainly for tests. */
44
47
  getProvider(): RoleProvider | null;
45
- getById(id: string): Promise<Role | null>;
48
+ /** One role record by `@id`: its members are identity records, so `contact-read`, or core's own work (#2018). */
49
+ getById(ctx: ActorContext, id: string): Promise<Role | null>;
46
50
  getByOrgAndPosition(organizationId: string, namedPosition: string): Promise<Role | null>;
47
51
  listByMember(personId: string): Promise<Role[]>;
48
52
  list(): Promise<Role[]>;
@@ -65,8 +69,13 @@ declare class RoleManager extends BaseManager {
65
69
  *
66
70
  * Returns `[]` when PersonManager is unavailable, the user has no Person
67
71
  * record, no record lists them, or the lookup throws.
72
+ *
73
+ * `ctx` is who is asking (#2018), decided by PersonManager's door: a user
74
+ * about themselves under `profile-manage`, about anyone else under
75
+ * `contact-read`; core's own work passes the system context. A refusal is
76
+ * thrown as `ContactRefusedError`, never answered as "no roles".
68
77
  */
69
- resolveUserRoles(username: string): Promise<string[]>;
78
+ resolveUserRoles(ctx: ActorContext, username: string): Promise<string[]>;
70
79
  /**
71
80
  * The `required-aal` levels the OPERATOR set in their own config (#448,
72
81
  * #1523), as opposed to the shipped defaults. An operator's level that no
@@ -86,15 +95,26 @@ declare class RoleManager extends BaseManager {
86
95
  * `anonymous`) requires nothing beyond being signed in.
87
96
  */
88
97
  roleRequiredAal(): Record<string, 1 | 2 | 3>;
89
- hasRole(username: string, roleName: string): Promise<boolean>;
90
- /** Assign one role to an existing account, and record it (#1204). */
98
+ /** Whether `username` holds `roleName` now, asked by `ctx`; decided as {@link resolveUserRoles} (#2018). */
99
+ hasRole(ctx: ActorContext, username: string, roleName: string): Promise<boolean>;
100
+ /**
101
+ * Assign one role to an existing account, and record it (#1204). A role
102
+ * that cannot be given is refused (#2015): the error is thrown, admins are
103
+ * notified, and the attempt is recorded as a failed user-edit.
104
+ */
91
105
  assignRole(username: string, roleName: string, ctx: ActorContext): Promise<boolean>;
92
- /** Remove one role from an existing account, and record it (#1204). */
106
+ /** Remove one role from an existing account, and record it (#1204); refused like `assignRole` when it cannot be done. */
93
107
  removeRole(username: string, roleName: string, ctx: ActorContext): Promise<boolean>;
94
108
  /**
95
109
  * Make the user's memberships go from `oldRoles` to `newRoles`. Used by
96
110
  * UserManager when an account is created or edited; the account write
97
111
  * records its own audit event, so this records none.
112
+ *
113
+ * All or nothing (#2015): when any change cannot be made, the ones already
114
+ * made are undone and a `RoleChangeError` is thrown, after it is logged as
115
+ * an error and admins are notified. That includes a change that cannot even
116
+ * be attempted: no PersonManager, no Person record for the username, or no
117
+ * anchor Organization.
98
118
  */
99
119
  applyRoleDiff(username: string, oldRoles: string[], newRoles: string[]): Promise<void>;
100
120
  /**
@@ -108,28 +128,67 @@ declare class RoleManager extends BaseManager {
108
128
  * a role change and is recorded as one, under the system context `ctx`,
109
129
  * deferred until the audit sink is up when this runs at boot.
110
130
  *
131
+ * #2015: each account gets the role or not. One that does not is recorded as
132
+ * a failed user-edit, and every miss is an error in the log and one error
133
+ * notification for admins naming the accounts to fix by hand. The boot goes
134
+ * on: nobody asked, so there is nobody to refuse.
135
+ *
111
136
  * @returns how many accounts were given the role (0 once it has been given)
112
137
  */
113
138
  grantToEveryAccountOnce(roleName: string, usernames: readonly string[], ctx: JobContext): Promise<number>;
114
139
  /**
115
140
  * Move every membership held by one Person `@id` to another (#1930): when
116
141
  * PersonManager joins an account to an existing person, the roles the
117
- * account's person held go with the account. One write per role record;
118
- * returns how many records changed. PersonManager's join records the change.
142
+ * account's person held go with the account. Returns how many records
143
+ * changed. PersonManager's join records the change. All or nothing (#2015).
119
144
  */
120
145
  moveMemberships(fromPersonId: string, toPersonId: string): Promise<number>;
121
- /** Remove a deleted account's Person from every role it held. */
122
- removeAllMemberships(username: string): Promise<void>;
123
146
  /**
124
- * Add the user's Person `@id` to the role record for (installOrg, roleName).
125
- * Idempotent: a no-op when the Person is already a member.
126
- *
127
- * Failures are logged, not thrown — account writes must succeed even when
128
- * role storage is degraded. #1027: every abandon path says why, because a
129
- * silent one is indistinguishable from success.
147
+ * Remove a deleted account's Person from every role it held. All or nothing
148
+ * (#2015): when any removal fails the others are put back and it throws, so
149
+ * the account delete is refused. Returns the undo, for a delete that fails
150
+ * after this: it gives every role back. An account with no Person holds no
151
+ * role, so there is nothing to remove.
152
+ */
153
+ removeAllMemberships(username: string): Promise<() => Promise<void>>;
154
+ /** The roles `username` gains and loses, all or nothing; `notify` false lets a batch send one notice. */
155
+ private changeRoles;
156
+ /**
157
+ * The Person `@id` and anchor Organization a username's role changes need
158
+ * (#1027). Either missing refuses the change (#2015): an account must not
159
+ * be left without the role it was given, nor keep one it was meant to lose.
160
+ */
161
+ private membershipTarget;
162
+ /** One member change and its inverse. */
163
+ private memberChange;
164
+ /**
165
+ * Make each change in turn (#2015). When one fails, undo the ones made, in
166
+ * reverse, and throw. Returns the changes made, for a later undo.
167
+ */
168
+ private allOrNothing;
169
+ /** Undo `done`, newest first; returns the labels that could not be undone. */
170
+ private undoAll;
171
+ /**
172
+ * A role change that did not happen is always an error (operator,
173
+ * 2026-10-23): logged at error level, and an error notification for admins
174
+ * unless the caller sends one for a whole batch. Returns the error to throw.
175
+ */
176
+ private roleFailure;
177
+ /** An error notification for admins; a notification that cannot be sent is logged, never thrown. */
178
+ private reportToAdmins;
179
+ /** #2015: the membership queue's key for one role record. */
180
+ private membershipKey;
181
+ /**
182
+ * Make `personId` a member of the role record, or not, one change at a time
183
+ * per record (#2015). The record is read inside the queue, so a change never
184
+ * works from a member list another change has since replaced. A missing
185
+ * record is created in `createIn` for an add; with no `createIn` an add to a
186
+ * missing record throws, and a removal from one changes nothing. False,
187
+ * writing nothing, when the person already is (or is not) a member.
130
188
  */
131
- private addMember;
132
- private removeMember;
189
+ private setMember;
190
+ /** assignRole / removeRole: the change, recorded as a user-edit whether it happened or not (#1204, #2015). */
191
+ private recordedRoleChange;
133
192
  /**
134
193
  * The role record for (installOrg, namedPosition), created when missing.
135
194
  *
@@ -153,7 +153,7 @@ interface UserCreateInput {
153
153
  /**
154
154
  * User update input data
155
155
  */
156
- interface UserUpdateInput {
156
+ export interface UserUpdateInput {
157
157
  email?: string;
158
158
  displayName?: string;
159
159
  password?: string;
@@ -166,6 +166,16 @@ interface UserUpdateInput {
166
166
  profilePage?: string;
167
167
  [key: string]: unknown;
168
168
  }
169
+ /**
170
+ * A change worked out from the account as it is now (#2025), the function form
171
+ * of `updateUser`. It runs inside the account's key, is handed a copy of the
172
+ * account without the password hash (an add-on's own fields included, as
173
+ * `UserUpdateInput` allows them), and returns the fields to merge, or
174
+ * `undefined` to change nothing.
175
+ */
176
+ export type UserUpdateFn = (current: Omit<User, 'password'> & {
177
+ [key: string]: unknown;
178
+ }) => UserUpdateInput | undefined | Promise<UserUpdateInput | undefined>;
169
179
  /**
170
180
  * UserManager - Handles user authentication, authorization, and roles
171
181
  *
@@ -208,6 +218,8 @@ declare class UserManager extends BaseManager {
208
218
  private provider;
209
219
  private providerClass?;
210
220
  private passwordSalt?;
221
+ /** One read-merge-write at a time per account, keyed by the normalized username (#2025). */
222
+ private readonly accountWrites;
211
223
  /**
212
224
  * Creates a new UserManager instance
213
225
  *
@@ -358,6 +370,15 @@ declare class UserManager extends BaseManager {
358
370
  upgradePasswordHash(username: string, newHash: string, legacy: boolean): Promise<void>;
359
371
  /** Count a completed sign-in on the account: last sign-in and sign-in count (#1758; AuthManager calls it for a sign-in, never a re-check). */
360
372
  recordSignIn(username: string): Promise<void>;
373
+ /** Run `task` inside the account's key: one read-merge-write at a time per account (#2025). Not re-entrant. */
374
+ private inAccountKey;
375
+ /**
376
+ * Read an account, change it and write it back, inside its key (#2025): for
377
+ * core's own bookkeeping (sign-in counters, the hash upgrade, the profile
378
+ * page), which is not an edit and is not recorded. False when there is no
379
+ * such account.
380
+ */
381
+ private patchAccount;
361
382
  /**
362
383
  * Check if a display name conflicts with existing page names or other users
363
384
  * @param {string} displayName - Display name to check
@@ -400,6 +421,12 @@ declare class UserManager extends BaseManager {
400
421
  * @returns {Promise<Omit<User, 'password'>>} Created user (without password)
401
422
  */
402
423
  createUser(userData: UserCreateInput, ctx: ActorContext): Promise<Omit<User, 'password'>>;
424
+ /**
425
+ * Take back an account whose roles could not be given (#2015): its Person,
426
+ * then the account. A failure here is an error in the log; the role failure
427
+ * that caused it is what the caller is told.
428
+ */
429
+ private undoAccountCreate;
403
430
  /**
404
431
  * Update user
405
432
  */
@@ -419,7 +446,31 @@ declare class UserManager extends BaseManager {
419
446
  * which accounts exist.
420
447
  */
421
448
  resetPasswordWithRecoveryWords(username: string, words: string, newPassword: string, ctx: ActorContext): Promise<boolean>;
422
- updateUser(username: string, updates: UserUpdateInput, ctx: ActorContext): Promise<User>;
449
+ /**
450
+ * Change an account (#2025: one change at a time per account).
451
+ *
452
+ * `updates` is the fields to merge, or a function that works them out from
453
+ * the account as it is now: `(current) => updates`. The function runs inside
454
+ * the account's `KeyedSerial` key, between the read and the write, so two
455
+ * changes to one account never read the same old record. It gets a copy of
456
+ * the account without the password hash, and returns the fields to merge, or
457
+ * `undefined` to change nothing (nothing is written or recorded). It must not
458
+ * call `updateUser` for the same account: the key is not re-entrant.
459
+ * Either form is checked, applied and recorded the same way.
460
+ *
461
+ * Returns the account as written (as read, when nothing changed), without
462
+ * the password hash: the hash never leaves UserManager except to the
463
+ * password check (`storedPasswordHash`).
464
+ */
465
+ updateUser(username: string, updates: UserUpdateInput | UserUpdateFn, ctx: ActorContext): Promise<Omit<User, 'password'>>;
466
+ /**
467
+ * The steps of an account change that come after the write, outside the
468
+ * account's key: they reach other managers, which may change the account
469
+ * themselves.
470
+ */
471
+ private finishEdit;
472
+ /** Read, check and write one account change. Runs inside the account's key (#2025). */
473
+ private editAccount;
423
474
  /**
424
475
  * Delete user
425
476
  */
@@ -469,6 +520,8 @@ declare class UserManager extends BaseManager {
469
520
  * who changed it, so the record says why.
470
521
  */
471
522
  private personSyncContext;
523
+ /** Core's bookkeeping for an account write, the read of its roles included (#2018): the system context, saying what and who changed the account. */
524
+ private lifecycleContext;
472
525
  /**
473
526
  * Persist a Person record paired with a newly-created User. The install's
474
527
  * anchor org (when configured) is referenced via `memberOf`; without it
@@ -27,8 +27,15 @@ import type { PermissionSubject, JobSubject } from '../managers/UserManager.js';
27
27
  import type { WikiEngine } from '../types/WikiEngine.js';
28
28
  import type { Decision, DecisionRequest, Policy } from '../types/Policy.js';
29
29
  import { type RoleGrant } from '../utils/roleGrants.js';
30
+ import { type ActorContext } from '../context/ActorContext.js';
30
31
  export declare const POLICIES_KEY = "ngdpbase.access.policies";
31
32
  export declare const POLICIES_ENABLED_KEY = "ngdpbase.access.policies.enabled";
33
+ /**
34
+ * What `decide`, `permits` and `holds` take (#2022): the caller's own context
35
+ * — a request's `PermissionSubject` or a job's `JobContext` — or a subject
36
+ * already in decision form (`JobSubject`). A caller passes what it was given.
37
+ */
38
+ type DecisionInput = ActorContext | JobSubject | null | undefined;
32
39
  export declare class PolicyDecisionPoint extends BaseManager {
33
40
  /**
34
41
  * A manager so every PEP can reach it through `engine.getManager`, the same
@@ -84,10 +91,11 @@ export declare class PolicyDecisionPoint extends BaseManager {
84
91
  * of {@link decide} for a capability. It was `UserManager.hasPermission`
85
92
  * until #1431 step 14; the decision was already here, only the door moved.
86
93
  *
87
- * Takes a `PermissionSubject` — the request's own identity, forwarded from
88
- * `req.userContext` (WikiContext, ApiContext, ParseContext) or a
89
- * `JobContext` for work with no request (#631). Roles arrive already
90
- * resolved; the session middleware did that once per request.
94
+ * Takes the caller's context as it was given (#2022): a `PermissionSubject`
95
+ * — the request's own identity, forwarded from `req.userContext`
96
+ * (WikiContext, ApiContext, ParseContext), roles already resolved by the
97
+ * session middleware — or a `JobContext` for work with no request (#631),
98
+ * which the PDP turns into a subject itself. Never convert one first.
91
99
  *
92
100
  * #1173 Part B: the username-string overload this method once accepted is
93
101
  * gone. A string cannot carry `viaToken`, so the agent-token ceiling below
@@ -98,12 +106,13 @@ export declare class PolicyDecisionPoint extends BaseManager {
98
106
  * separate question {@link userHoldsPermission} — "does the named user hold
99
107
  * this?" — which is a lookup about somebody else, not an authorisation.
100
108
  *
101
- * @param subject - The identity being authorised, with `roles` resolved and
102
- * `viaToken` present when a bearer token authenticated it.
109
+ * @param subject - The caller's context: the request's subject, with
110
+ * `viaToken` present when a bearer token authenticated it,
111
+ * or a job's context.
103
112
  * @param action - Action/permission to check (e.g., 'page-create', 'user-read')
104
113
  * @returns True if the subject may perform the action under current policy
105
114
  */
106
- permits(subject: PermissionSubject | JobSubject | null | undefined, action: string): Promise<boolean>;
115
+ permits(subject: DecisionInput, action: string): Promise<boolean>;
107
116
  /**
108
117
  * Does the subject HOLD this permission (#1635)? Policy alone — the
109
118
  * step-up freshness rule is not applied. A different question from
@@ -112,7 +121,7 @@ export declare class PolicyDecisionPoint extends BaseManager {
112
121
  * person whose sign-in is not fresh still sees the way to the prompt that
113
122
  * makes it fresh. A door never asks this.
114
123
  */
115
- holds(subject: PermissionSubject | JobSubject | null | undefined, action: string): Promise<boolean>;
124
+ holds(subject: DecisionInput, action: string): Promise<boolean>;
116
125
  /**
117
126
  * Step-up (#1525, #1635), decided here — in the one place every door asks —
118
127
  * rather than by routes in front of the doors. A permission marked
@@ -252,7 +261,7 @@ export declare class PolicyDecisionPoint extends BaseManager {
252
261
  * time, not at enqueue time (#631, #1212).
253
262
  * 4. __The policies__, through `PolicyEvaluator`.
254
263
  */
255
- decide(subject: PermissionSubject | JobSubject | null | undefined, request: DecisionRequest): Promise<Decision>;
264
+ decide(subject: DecisionInput, request: DecisionRequest): Promise<Decision>;
256
265
  /** `decide`, with the step-up rule applied or not — {@link holds} is the only caller that leaves it out. */
257
266
  private evaluate;
258
267
  }