@jwilleke/ngdpbase-types 5.29.1 → 5.30.1

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 (44) hide show
  1. package/README.md +1 -1
  2. package/lib/packages/ngdpbase-types/index.d.ts +8 -3
  3. package/lib/src/context/JobContext.d.ts +20 -1
  4. package/lib/src/context/JobContext.js +18 -2
  5. package/lib/src/context/bootActions.js +5 -1
  6. package/lib/src/ice/fileHandling.d.ts +24 -2
  7. package/lib/src/ice/originalKeep.d.ts +40 -0
  8. package/lib/src/ice/types.d.ts +33 -1
  9. package/lib/src/managers/AttachmentManager.d.ts +12 -8
  10. package/lib/src/managers/AuthManager.d.ts +111 -0
  11. package/lib/src/managers/BackgroundJobManager.d.ts +8 -0
  12. package/lib/src/managers/BackupManager.d.ts +6 -0
  13. package/lib/src/managers/BackupManager.js +38 -11
  14. package/lib/src/managers/{PersonManager.d.ts → IdentityManager.d.ts} +20 -16
  15. package/lib/src/managers/ImportManager.d.ts +2 -5
  16. package/lib/src/managers/OrganizationManager.d.ts +2 -2
  17. package/lib/src/managers/PolicyEvaluator.d.ts +7 -0
  18. package/lib/src/managers/RoleManager.d.ts +7 -7
  19. package/lib/src/managers/UserManager.d.ts +17 -5
  20. package/lib/src/providers/BaseAttachmentProvider.d.ts +2 -8
  21. package/lib/src/providers/BaseCredentialsProvider.d.ts +7 -1
  22. package/lib/src/providers/BasicAttachmentProvider.d.ts +3 -8
  23. package/lib/src/security/PolicyDecisionPoint.d.ts +12 -0
  24. package/lib/src/security/PolicyInformationPoint.d.ts +1 -1
  25. package/lib/src/security/permissions.generated.d.ts +4 -4
  26. package/lib/src/types/Config.d.ts +1 -1
  27. package/lib/src/types/Person.d.ts +1 -1
  28. package/lib/src/types/PersonProvider.d.ts +2 -2
  29. package/lib/src/types/Policy.d.ts +2 -2
  30. package/lib/src/types/Provider.d.ts +21 -0
  31. package/lib/src/utils/addonConfigLayer.d.ts +16 -0
  32. package/lib/src/utils/addonConfigLayer.js +40 -3
  33. package/lib/src/utils/addonHost.d.ts +21 -3
  34. package/lib/src/utils/addonHost.js +10 -4
  35. package/lib/src/utils/auditEventNames.generated.d.ts +10 -2
  36. package/lib/src/utils/auditEventNames.generated.js +10 -2
  37. package/lib/src/utils/auditEvents.d.ts +16 -3
  38. package/lib/src/utils/auditEvents.js +10 -11
  39. package/lib/src/utils/availability.d.ts +120 -0
  40. package/lib/src/utils/availability.js +174 -0
  41. package/lib/src/utils/contactAccess.d.ts +9 -9
  42. package/lib/src/utils/keyedSerial.js +41 -0
  43. package/lib/src/utils/privateStoreUnlock.d.ts +1 -1
  44. package/package.json +1 -1
@@ -11,8 +11,12 @@ export interface LinkOfferDetails {
11
11
  /** The outcome of offering a sign-up link (#1930). */
12
12
  export type LinkOfferOutcome = 'sent' | 'no-match' | 'several' | 'unavailable' | 'failed';
13
13
  /**
14
- * PersonManager — canonical core record for persons (#617), and the door for
15
- * contacts (#1930).
14
+ * IdentityManager — the identity record of every digital identity, user or
15
+ * contact (#617), and the door for them (#1930). Named PersonManager until
16
+ * #2038; `getManager('PersonManager')` still answers with it for one release
17
+ * (`MANAGER_ALIASES` in `src/core/Engine.ts`). What is stored keeps its
18
+ * "person" names: `data/persons/`, `ngdpbase.application.persons.*`, the
19
+ * `Person` types, `person:<uuid>`, the `person-*` audit actions.
16
20
  *
17
21
  * A person may have an account or not. Each account has exactly one person
18
22
  * (`identifier` = the username); a person with no account has the identifier
@@ -20,14 +24,14 @@ export type LinkOfferOutcome = 'sent' | 'no-match' | 'several' | 'unavailable' |
20
24
  * the account lifecycle under a system context.
21
25
  *
22
26
  * The door: `create`, `update`, `delete`, `get`, `list`, `search` and
23
- * `linkAccount` take the caller's context first and ask the PDP: `contact-read`
24
- * to list, search or get; `contact-manage` to change (`linkAccount` also `user-edit`). Each change is recorded as
25
- * the `contact-manage` audit event. A refusal throws `ContactRefusedError`.
27
+ * `linkAccount` take the caller's context first and ask the PDP: `identity-read`
28
+ * to list, search or get; `identity-manage` to change (`linkAccount` also `user-edit`). Each change is recorded as
29
+ * the `identity-manage` audit event. A refusal throws `ContactRefusedError`.
26
30
  *
27
31
  * Every lookup takes the caller's context too (#2018): `getById`,
28
32
  * `getByIdentifier` and `getByUserIdentifier`. A user reads their own identity
29
33
  * record, and `update` changes it, under `profile-manage`; anyone else's needs
30
- * `contact-read` / `contact-manage`. Core's own reads (RoleManager resolving
34
+ * `identity-read` / `identity-manage`. Core's own reads (RoleManager resolving
31
35
  * roles on every request, UserManager's account lifecycle) pass the system
32
36
  * context, allowed without a policy evaluation (`requireRecordAccess`).
33
37
  *
@@ -40,8 +44,8 @@ export type LinkOfferOutcome = 'sent' | 'no-match' | 'several' | 'unavailable' |
40
44
  * `getByUserIdentifier(ctx, username)` returns a user's identity record and
41
45
  * makes it when the user has none (#2037): every user has exactly one.
42
46
  */
43
- declare class PersonManager extends BaseManager {
44
- readonly description = "Canonical Person records (#617)";
47
+ declare class IdentityManager extends BaseManager {
48
+ readonly description = "Identity records of users and contacts (#617)";
45
49
  private provider;
46
50
  private providerClass?;
47
51
  /** `ngdpbase.application.persons.storagedir`, resolved at start: what a backup copies (#1876). */
@@ -65,7 +69,7 @@ declare class PersonManager extends BaseManager {
65
69
  getProvider(): PersonProvider | null;
66
70
  /**
67
71
  * One identity record by `@id` (#2018): the caller's own under
68
- * `profile-manage`, anyone else's under `contact-read`, core's own work
72
+ * `profile-manage`, anyone else's under `identity-read`, core's own work
69
73
  * under the system context. A refusal throws `ContactRefusedError`.
70
74
  */
71
75
  getById(ctx: ActorContext, id: string): Promise<Person | null>;
@@ -80,9 +84,9 @@ declare class PersonManager extends BaseManager {
80
84
  getManagerStats(): Promise<ManagerStats>;
81
85
  /** True when the person has no account: its identifier is `person:<uuid>` (#1930). */
82
86
  static isAccountless(person: Pick<Person, 'identifier'>): boolean;
83
- /** Every person, for a caller who may read contacts (`contact-read`). */
87
+ /** Every person, for a caller who may read contacts (`identity-read`). */
84
88
  list(ctx: ActorContext): Promise<Person[]>;
85
- /** One person by `@id`: {@link getById}, the caller's own under `profile-manage`, anyone else's under `contact-read`. */
89
+ /** One person by `@id`: {@link getById}, the caller's own under `profile-manage`, anyone else's under `identity-read`. */
86
90
  get(ctx: ActorContext, id: string): Promise<Person | null>;
87
91
  /**
88
92
  * Persons whose name (`name`, `givenName`, `familyName`) or `email` contains
@@ -103,13 +107,13 @@ declare class PersonManager extends BaseManager {
103
107
  /**
104
108
  * Change a person's fields, affiliations included. `@id` and `identifier`
105
109
  * never change here. The caller's own identity record under
106
- * `profile-manage`, anyone else's under `contact-manage` (#2018).
110
+ * `profile-manage`, anyone else's under `identity-manage` (#2018).
107
111
  */
108
112
  update(ctx: ActorContext, id: string, patch: PersonUpdate): Promise<Person | null>;
109
113
  delete(ctx: ActorContext, id: string): Promise<boolean>;
110
114
  /**
111
115
  * An administrator joins a person who has no account to an account (#1930),
112
- * for when the email differs or none was given. Asks `contact-manage` and
116
+ * for when the email differs or none was given. Asks `identity-manage` and
113
117
  * `user-edit`. The person keeps its `@id`; the account's own person, if it
114
118
  * has one, is folded in and removed.
115
119
  */
@@ -163,11 +167,11 @@ declare class PersonManager extends BaseManager {
163
167
  * store) gets it here, from the account's display name and email.
164
168
  *
165
169
  * The read is decided as {@link getByIdentifier} (#2018): a signed-in user's
166
- * own under `profile-manage`, anyone else's under `contact-read`, core's own
170
+ * own under `profile-manage`, anyone else's under `identity-read`, core's own
167
171
  * work under the system context. Making the missing record is core's
168
172
  * bookkeeping, not the caller's contact management, so it goes through
169
173
  * {@link create} under the system context, naming the caller, and is
170
- * recorded as `contact-manage`. One at a time per username, so two lookups
174
+ * recorded as `identity-manage`. One at a time per username, so two lookups
171
175
  * never make two records. Null when there is no account by that name.
172
176
  * Never a second record: a contact with the same email is left alone
173
177
  * (an administrator links it, {@link linkAccount}).
@@ -178,4 +182,4 @@ declare class PersonManager extends BaseManager {
178
182
  private normalizeProviderName;
179
183
  }
180
184
  export { ContactRefusedError } from '../utils/contactAccess.js';
181
- export default PersonManager;
185
+ export default IdentityManager;
@@ -267,7 +267,8 @@ export type TakeoutImportReport = {
267
267
  export interface UsableReceiver {
268
268
  id: string;
269
269
  label?: string;
270
- originals: Required<IceOriginalChoices>;
270
+ /** What the screen offers; the kept types (`types`, #2061) are the upload door's business, not the screen's. */
271
+ originals: Required<Omit<IceOriginalChoices, 'types'>>;
271
272
  }
272
273
  /**
273
274
  * Import Manager class
@@ -546,10 +547,6 @@ declare class ImportManager extends BaseManager {
546
547
  * @returns Stats about imported attachments
547
548
  */
548
549
  private importPageAttachments;
549
- /**
550
- * Get MIME type from file extension
551
- */
552
- private getMimeType;
553
550
  /**
554
551
  * Get file extensions to process based on options
555
552
  */
@@ -88,13 +88,13 @@ declare class OrganizationManager extends BaseManager {
88
88
  * problem actually bites.
89
89
  */
90
90
  getInstallOrg(): Promise<Organization | null>;
91
- /** One organization by `@id`, for a caller who may read contacts (`contact-read`), or core's own work (#2018). */
91
+ /** One organization by `@id`, for a caller who may read contacts (`identity-read`), or core's own work (#2018). */
92
92
  getById(ctx: ActorContext, id: string): Promise<Organization | null>;
93
93
  /** One organization by its file name, decided as {@link getById} (#2018). */
94
94
  getByFile(ctx: ActorContext, filename: string): Promise<Organization | null>;
95
95
  /** #1006: how many organizations this instance holds. Count only. */
96
96
  getManagerStats(): Promise<ManagerStats>;
97
- /** Every organization, for a caller who may read contacts (`contact-read`). */
97
+ /** Every organization, for a caller who may read contacts (`identity-read`). */
98
98
  list(ctx: ActorContext): Promise<Organization[]>;
99
99
  /** One organization by `@id`: {@link getById}. */
100
100
  get(ctx: ActorContext, id: string): Promise<Organization | null>;
@@ -34,6 +34,13 @@ interface AccessContext {
34
34
  * policy naming it decides nothing (docs/access-policies.md).
35
35
  */
36
36
  export declare const RESOURCE_MATCHERS: Record<string, (pattern: string, pageName: string, attributes?: ResourceAttributes) => boolean>;
37
+ /**
38
+ * How each policy subject type matches (#1999), one matcher per type. These
39
+ * are the only subject types a decision matches, and the only ones
40
+ * PolicyValidator accepts: a type with no matcher here would be a policy that
41
+ * grants nothing.
42
+ */
43
+ export declare const SUBJECT_MATCHERS: Record<string, (value: string, userContext: UserContext | undefined) => boolean>;
37
44
  /**
38
45
  * The resource types configuration declares (#1504), for the evaluator and the
39
46
  * validator alike. When the key is not set at all — a configuration older than
@@ -45,7 +45,7 @@ declare class RoleManager extends BaseManager {
45
45
  backupFolders(): BackupFolder[];
46
46
  /** Provider accessor, mainly for tests. */
47
47
  getProvider(): RoleProvider | null;
48
- /** One role record by `@id`: its members are identity records, so `contact-read`, or core's own work (#2018). */
48
+ /** One role record by `@id`: its members are identity records, so `identity-read`, or core's own work (#2018). */
49
49
  getById(ctx: ActorContext, id: string): Promise<Role | null>;
50
50
  getByOrgAndPosition(organizationId: string, namedPosition: string): Promise<Role | null>;
51
51
  listByMember(personId: string): Promise<Role[]>;
@@ -67,13 +67,13 @@ declare class RoleManager extends BaseManager {
67
67
  * The pseudo-roles `'Authenticated'` and `'All'` are NOT added here — the
68
68
  * caller adds those when constructing `userContext.roles`.
69
69
  *
70
- * Returns `[]` when PersonManager is unavailable, there is no account by
70
+ * Returns `[]` when IdentityManager is unavailable, there is no account by
71
71
  * that username, no record lists them, or the lookup throws. An account
72
72
  * missing its identity record gets it here (#2037).
73
73
  *
74
- * `ctx` is who is asking (#2018), decided by PersonManager's door: a user
74
+ * `ctx` is who is asking (#2018), decided by IdentityManager's door: a user
75
75
  * about themselves under `profile-manage`, about anyone else under
76
- * `contact-read`; core's own work passes the system context. A refusal is
76
+ * `identity-read`; core's own work passes the system context. A refusal is
77
77
  * thrown as `ContactRefusedError`, never answered as "no roles".
78
78
  */
79
79
  resolveUserRoles(ctx: ActorContext, username: string): Promise<string[]>;
@@ -114,7 +114,7 @@ declare class RoleManager extends BaseManager {
114
114
  * All or nothing (#2015): when any change cannot be made, the ones already
115
115
  * made are undone and a `RoleChangeError` is thrown, after it is logged as
116
116
  * an error and admins are notified. That includes a change that cannot even
117
- * be attempted: no PersonManager, no Person record for the username, or no
117
+ * be attempted: no IdentityManager, no Person record for the username, or no
118
118
  * anchor Organization.
119
119
  */
120
120
  applyRoleDiff(username: string, oldRoles: string[], newRoles: string[]): Promise<void>;
@@ -139,9 +139,9 @@ declare class RoleManager extends BaseManager {
139
139
  grantToEveryAccountOnce(roleName: string, usernames: readonly string[], ctx: JobContext): Promise<number>;
140
140
  /**
141
141
  * Move every membership held by one Person `@id` to another (#1930): when
142
- * PersonManager joins an account to an existing person, the roles the
142
+ * IdentityManager joins an account to an existing person, the roles the
143
143
  * account's person held go with the account. Returns how many records
144
- * changed. PersonManager's join records the change. All or nothing (#2015).
144
+ * changed. IdentityManager's join records the change. All or nothing (#2015).
145
145
  */
146
146
  moveMemberships(fromPersonId: string, toPersonId: string): Promise<number>;
147
147
  /**
@@ -132,6 +132,8 @@ export interface JobSubject {
132
132
  viaToken?: AgentTokenGrant;
133
133
  viaShare?: ShareGrant;
134
134
  viaProxy?: ProxyClaim;
135
+ /** #1999: the scheduled job a run acts as (`JobContext.job`). */
136
+ job?: string;
135
137
  }
136
138
  /**
137
139
  * User creation input data
@@ -343,9 +345,9 @@ declare class UserManager extends BaseManager {
343
345
  /**
344
346
  * Every account has exactly one identity record, made by `createUser`
345
347
  * (#2037). One that has none (written straight into the user store, or
346
- * whose record was lost) gets it at boot, once, through PersonManager's
348
+ * whose record was lost) gets it at boot, once, through IdentityManager's
347
349
  * door under the system context, each creation recorded as
348
- * `contact-manage`. The identity records are listed once and only the
350
+ * `identity-manage`. The identity records are listed once and only the
349
351
  * missing ones are made, so a large user store costs one read. A failure
350
352
  * is logged and the boot goes on: the role paths make the record when the
351
353
  * account is next used (`getByUserIdentifier`).
@@ -457,6 +459,16 @@ declare class UserManager extends BaseManager {
457
459
  * which accounts exist.
458
460
  */
459
461
  resetPasswordWithRecoveryWords(username: string, words: string, newPassword: string, ctx: ActorContext): Promise<boolean>;
462
+ /**
463
+ * A user's first password, chosen by themselves (#2049: after an
464
+ * invitation signed them in). Only for their own account, signed in as
465
+ * themselves (no token, share link or proxy), under `account-security`,
466
+ * and only while the account has no password: a password already set is
467
+ * changed on the profile, which asks for the current one. `false`,
468
+ * changing nothing, otherwise. The length rule is the route's, as for
469
+ * every password form. Recorded like any password change (`user-edit`).
470
+ */
471
+ setFirstPassword(username: string, newPassword: string, ctx: PermissionSubject): Promise<boolean>;
460
472
  /**
461
473
  * Change an account (#2025: one change at a time per account).
462
474
  *
@@ -529,10 +541,10 @@ declare class UserManager extends BaseManager {
529
541
  activeOnly?: boolean;
530
542
  }, ctx: ActorContext): Promise<Omit<User, 'password'>[]>;
531
543
  /**
532
- * The account lifecycle reaches PersonManager's door under a system context
544
+ * The account lifecycle reaches IdentityManager's door under a system context
533
545
  * (#1930): keeping an account's person in step with the account is core's
534
546
  * own bookkeeping, not the caller's contact management, so it never depends
535
- * on the caller holding `contact-manage`. The reason names the account and
547
+ * on the caller holding `identity-manage`. The reason names the account and
536
548
  * who changed it, so the record says why.
537
549
  */
538
550
  private personSyncContext;
@@ -547,7 +559,7 @@ declare class UserManager extends BaseManager {
547
559
  private syncPersonOnCreate;
548
560
  /**
549
561
  * At sign-up, offer the new account a person with no account who has the
550
- * same email (#1930): PersonManager mails an "Is this you?" link, and joins
562
+ * same email (#1930): IdentityManager mails an "Is this you?" link, and joins
551
563
  * them only when the account confirms. Skipped when email is off. Never
552
564
  * fails the account write.
553
565
  */
@@ -1,5 +1,5 @@
1
1
  import { AttachmentMetadata, AttachmentProvider } from '../types/index.js';
2
- import type { StoreFileEntry, StoreFileLocation } from '../types/Provider.js';
2
+ import type { StoreFileEntry, StoreFileInput, StoreFileLocation } from '../types/Provider.js';
3
3
  import type { WikiEngine } from '../types/WikiEngine.js';
4
4
  import { ProviderInfo } from './BasePageProvider.js';
5
5
  import BaseProvider from './BaseProvider.js';
@@ -150,13 +150,7 @@ declare abstract class BaseAttachmentProvider extends BaseProvider implements At
150
150
  */
151
151
  getAttachmentByFilename(_filename: string): Promise<AttachmentMetadata | null>;
152
152
  /** Store a file in a store. The same bytes already in THAT store return the existing entry. */
153
- storeFileInStore(_location: StoreFileLocation, _bytes: Buffer, _file: {
154
- originalName: string;
155
- mimeType: string;
156
- description: string;
157
- author?: string;
158
- pageName?: string;
159
- }): Promise<StoreFileEntry>;
153
+ storeFileInStore(_location: StoreFileLocation, _bytes: Buffer, _file: StoreFileInput): Promise<StoreFileEntry>;
160
154
  /** One file of a store, with its bytes; null when the store does not list it. */
161
155
  getFileInStore(_location: StoreFileLocation, _id: string): Promise<{
162
156
  entry: StoreFileEntry;
@@ -24,8 +24,14 @@ export declare const CREDENTIALS_KEY_ENV = "NGDPBASE_CREDENTIALS_KEY";
24
24
  * represented, `subject` the grant id, `secret` the grant's terms as JSON
25
25
  * (whom it is for, the permissions, who made it, when it ends). Never a way
26
26
  * in; AuthManager reads and writes it only through its proxy-grant methods.
27
+ *
28
+ * `invitation` (#2049): a one-time link that lets a new user set up their
29
+ * sign-in — `username` the account invited, `subject` the SHA-256 of the
30
+ * link's token (the token itself is never stored), `secret` the terms as JSON
31
+ * (who sent it, when it ends). Not a sign-in method; AuthManager reads and
32
+ * writes it only through its invitation methods.
27
33
  */
28
- export type CredentialKind = 'passkey' | 'totp' | 'email' | 'sms' | 'device' | 'identity' | 'proxy';
34
+ export type CredentialKind = 'passkey' | 'totp' | 'email' | 'sms' | 'device' | 'identity' | 'proxy' | 'invitation';
29
35
  export declare const CREDENTIAL_KINDS: readonly CredentialKind[];
30
36
  /** One credential, as stored. */
31
37
  export interface CredentialRecord {
@@ -1,5 +1,5 @@
1
1
  import BaseAttachmentProvider, { FileInfo, User, AttachmentResult } from './BaseAttachmentProvider.js';
2
- import type { ProviderInfo, StoreFileEntry, StoreFileLocation } from '../types/Provider.js';
2
+ import type { ProviderInfo, StoreFileEntry, StoreFileInput, StoreFileLocation } from '../types/Provider.js';
3
3
  import { AttachmentMetadata } from '../types/index.js';
4
4
  import type { AssetProvider, AssetRecord, AssetQuery, AssetPage, AssetInput, AssetMetadata, AssetMetadataPatch } from '../types/Asset.js';
5
5
  import type { CreativeWork } from '../types/Schema.js';
@@ -213,6 +213,7 @@ declare class BasicAttachmentProvider extends BaseAttachmentProvider implements
213
213
  /**
214
214
  * Validate file against constraints
215
215
  * @param fileInfo - File information
216
+ * @param alsoAllowed - Types allowed beyond the site's list: only an import's kept original into its receiver's own vault (#2061)
216
217
  * @private
217
218
  */
218
219
  private validateFile;
@@ -261,13 +262,7 @@ declare class BasicAttachmentProvider extends BaseAttachmentProvider implements
261
262
  private requirePagesDirectory;
262
263
  private readStoreIndex;
263
264
  private writeStoreIndex;
264
- storeFileInStore(location: StoreFileLocation, bytes: Buffer, file: {
265
- originalName: string;
266
- mimeType: string;
267
- description: string;
268
- author?: string;
269
- pageName?: string;
270
- }): Promise<StoreFileEntry>;
265
+ storeFileInStore(location: StoreFileLocation, bytes: Buffer, file: StoreFileInput): Promise<StoreFileEntry>;
271
266
  getFileInStore(location: StoreFileLocation, id: string): Promise<{
272
267
  entry: StoreFileEntry;
273
268
  bytes: Buffer;
@@ -43,6 +43,10 @@ export declare class PolicyDecisionPoint extends BaseManager {
43
43
  * derived, never stored.
44
44
  */
45
45
  constructor(engine: WikiEngine);
46
+ /** Each `<policy id> <retired name>` already warned about (#2038): said once per process. */
47
+ private readonly warnedRetired;
48
+ /** Warn, once per policy and name, that a policy names a retired permission and what it is read as (#2038). */
49
+ private warnRetiredOnce;
46
50
  /**
47
51
  * The policies in force right now, highest priority first.
48
52
  *
@@ -55,6 +59,14 @@ export declare class PolicyDecisionPoint extends BaseManager {
55
59
  * not something to resolve silently here.
56
60
  */
57
61
  policies(): Policy[];
62
+ /**
63
+ * Whether a policy in force names the scheduled job `jobId` as a subject
64
+ * (`{ "type": "job", "value": jobId }`, #1999), allow or deny. When one
65
+ * does, the job's runs act as the job itself; when none does, as the system
66
+ * principal. Read live, so a site that removes the policy gives the job the
67
+ * system roles back at its next decision.
68
+ */
69
+ namesJob(jobId: string): boolean;
58
70
  /**
59
71
  * What each role permits: role name → the actions its holders are granted.
60
72
  *
@@ -178,7 +178,7 @@ declare class PolicyInformationPoint extends BaseManager {
178
178
  * resolves to the anonymous subject: the job then holds exactly what a
179
179
  * visitor holds, which is the safe answer for someone who no longer exists.
180
180
  */
181
- resolveSubjectNow(username: string | undefined): Promise<UserContext>;
181
+ resolveSubjectNow(username: string | undefined, job?: string): Promise<UserContext>;
182
182
  /**
183
183
  * The subject as a page's own rules match it (#2022): for a job's subject
184
184
  * (`resolveRolesNow`, #1212), its username with the roles its account
@@ -22,10 +22,10 @@ export type CorePermission =
22
22
  | 'comment-create'
23
23
  /** Change any configuration setting, and download a backup (#1525). Admin only by default; asks for a fresh sign-in when listed in ngdpbase.auth.step-up */
24
24
  | 'config-manage'
25
- /** Create, edit, delete and link contacts: persons (with or without an account) and organizations, and a person's affiliations, through PersonManager and OrganizationManager (#1930). Reading them is contact-read. Admins by default; an add-on grants it to its own roles in its own policy */
26
- | 'contact-manage'
27
- /** List, search and view contacts: persons (with or without an account) and organizations, and their affiliations, through PersonManager and OrganizationManager, and the /admin/contacts and /admin/organizations views (#1930). Admins and demo admins by default; an add-on grants it to its own roles in its own policy */
28
- | 'contact-read'
25
+ /** Create, edit, delete and link identity records, of users and contacts alike, and organizations, and an identity's affiliations, through IdentityManager and OrganizationManager (#1930). Reading them is identity-read. Was contact-manage until #2038; a policy naming that is still read as this for one release. Admins by default; an add-on grants it to its own roles in its own policy */
26
+ | 'identity-manage'
27
+ /** List, search and view identity records, of users and contacts alike, and organizations, and their affiliations, through IdentityManager and OrganizationManager, and the /admin/contacts and /admin/organizations views (#1930). Was contact-read until #2038; a policy naming that is still read as this for one release. Admins and demo admins by default; an add-on grants it to its own roles in its own policy */
28
+ | 'identity-read'
29
29
  /** Browse and view the media library (not delegated to anonymous visitors) */
30
30
  | 'media-read'
31
31
  /** Create new pages */
@@ -312,7 +312,7 @@ export interface ApplicationOrganizationConfig {
312
312
  'ngdpbase.application.persons.provider.default': string;
313
313
  /** Active Person provider class (lowercased; falls back to .default) */
314
314
  'ngdpbase.application.persons.provider': string;
315
- /** #1930: the most results `PersonManager.search` returns when the caller gives no limit */
315
+ /** #1930: the most results `IdentityManager.search` returns when the caller gives no limit */
316
316
  'ngdpbase.application.persons.search.limit': number;
317
317
  /** #1930: minutes the emailed "Is this you?" sign-up link stays usable */
318
318
  'ngdpbase.application.persons.link-offer.ttl-minutes': number;
@@ -70,7 +70,7 @@ export interface Person {
70
70
  */
71
71
  export type PersonUpdate = Partial<Omit<Person, '@context' | '@type' | '@id' | 'identifier'>>;
72
72
  /**
73
- * What a caller gives `PersonManager.create` (#1930): the person's fields.
73
+ * What a caller gives `IdentityManager.create` (#1930): the person's fields.
74
74
  * `identifier` is left out for a person with no account (one is generated);
75
75
  * UserManager passes the account's username when it creates the person for a
76
76
  * new account.
@@ -2,7 +2,7 @@
2
2
  * Storage seam for Person records (#617)
3
3
  *
4
4
  * File system is the v1 default. Future providers (LDAP, external directory)
5
- * implement the same interface and register without touching PersonManager.
5
+ * implement the same interface and register without touching IdentityManager.
6
6
  */
7
7
  import type { BaseProvider } from './Provider.js';
8
8
  import type { Person, PersonUpdate } from './Person.js';
@@ -20,7 +20,7 @@ export interface PersonProvider extends BaseProvider {
20
20
  create(person: Person): Promise<Person>;
21
21
  /**
22
22
  * Write the whole record in place of the one with the same `@id` (#1930).
23
- * The only write that may change `identifier`: PersonManager uses it to join
23
+ * The only write that may change `identifier`: IdentityManager uses it to join
24
24
  * a person to an account.
25
25
  */
26
26
  replace(person: Person): Promise<Person>;
@@ -12,9 +12,9 @@
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. `role` is the only type the evaluator matches today. */
15
+ /** Who a policy grants to. The evaluator matches `role` and `job` (#1999), its `SUBJECT_MATCHERS`. */
16
16
  export interface PolicySubject {
17
- /** `role`, and in future `user` or `group`. */
17
+ /** `role`, or `job` for a scheduled job acting as itself (#1999). */
18
18
  type: string;
19
19
  value: string;
20
20
  }
@@ -582,6 +582,27 @@ export interface StoreFileEntry {
582
582
  dateModified: string;
583
583
  /** Pages the file was uploaded onto. */
584
584
  mentions: string[];
585
+ /**
586
+ * An import's kept original (#2061): always served as a download
587
+ * (`Content-Disposition: attachment`, `nosniff`), never shown inline.
588
+ */
589
+ download?: true;
590
+ }
591
+ /** What `storeFileInStore` is told about a file it stores in a private store. */
592
+ export interface StoreFileInput {
593
+ originalName: string;
594
+ mimeType: string;
595
+ description: string;
596
+ author?: string;
597
+ pageName?: string;
598
+ /**
599
+ * Set only by AttachmentManager for an import's keep into the receiver's
600
+ * own vault (#2061): `types` are allowed beyond the site's allow-list, and
601
+ * the file is marked {@link StoreFileEntry.download}.
602
+ */
603
+ keptOriginal?: {
604
+ types: readonly string[];
605
+ };
585
606
  }
586
607
  /**
587
608
  * A page in a private store (#1456): its entry in the store's OWN page index,
@@ -51,6 +51,11 @@
51
51
  * classify only its own keys, `ngdpbase.addons.<slug>` and what is under it
52
52
  * (paths inside its values included). An entry for any other key refuses the
53
53
  * addon, since it could unhide core's or another addon's secret.
54
+ *
55
+ * A fifth, `ngdpbase.access.policies` job subjects (#1999): an enabled addon's
56
+ * policy may name only its own scheduled jobs, `{ "type": "job", "value":
57
+ * "<slug>.<job>" }`. A policy naming core's job or another addon's refuses
58
+ * the addon, since it would grant that job permissions its owner never chose.
54
59
  */
55
60
  import { type AddonDatabaseDeclaration } from './addonDatabaseRules.js';
56
61
  export declare const ADDONS_PATH_KEY = "ngdpbase.managers.addons-manager.addons-path";
@@ -119,6 +124,17 @@ export interface AddonSensitiveValuesLayer {
119
124
  * enabled addon's entries reach the merge, so only an enabled addon is checked.
120
125
  */
121
126
  export declare function addonSensitiveValuesLayer(addons: readonly AddonDefaults[]): AddonSensitiveValuesLayer;
127
+ export interface AddonJobPolicyLayer {
128
+ /** Addons whose policies name a job that is not their own, by slug, with why: never loaded. */
129
+ refused: Map<string, string>;
130
+ problems: string[];
131
+ }
132
+ /**
133
+ * Which jobs an addon's policies may name (#1999): its own, ids starting
134
+ * `<slug>.`, the prefix its job ids carry. Only an enabled addon's policies
135
+ * reach the merge, so only an enabled addon is checked.
136
+ */
137
+ export declare function addonJobPolicyLayer(addons: readonly AddonDefaults[]): AddonJobPolicyLayer;
122
138
  /** The configuration key that makes an addon site-critical (#1883). */
123
139
  export declare function addonSiteCriticalKey(slug: string): string;
124
140
  export interface AddonSiteCriticalLayer {
@@ -51,6 +51,11 @@
51
51
  * classify only its own keys, `ngdpbase.addons.<slug>` and what is under it
52
52
  * (paths inside its values included). An entry for any other key refuses the
53
53
  * addon, since it could unhide core's or another addon's secret.
54
+ *
55
+ * A fifth, `ngdpbase.access.policies` job subjects (#1999): an enabled addon's
56
+ * policy may name only its own scheduled jobs, `{ "type": "job", "value":
57
+ * "<slug>.<job>" }`. A policy naming core's job or another addon's refuses
58
+ * the addon, since it would grant that job permissions its owner never chose.
54
59
  */
55
60
  import fs from 'fs';
56
61
  import path from 'path';
@@ -313,6 +318,37 @@ export function addonSensitiveValuesLayer(addons) {
313
318
  }
314
319
  return layer;
315
320
  }
321
+ /**
322
+ * Which jobs an addon's policies may name (#1999): its own, ids starting
323
+ * `<slug>.`, the prefix its job ids carry. Only an enabled addon's policies
324
+ * reach the merge, so only an enabled addon is checked.
325
+ */
326
+ export function addonJobPolicyLayer(addons) {
327
+ const layer = { refused: new Map(), problems: [] };
328
+ for (const addon of addons) {
329
+ if (!addon.enabled)
330
+ continue;
331
+ const policies = addon.defaults['ngdpbase.access.policies'];
332
+ if (!Array.isArray(policies))
333
+ continue;
334
+ const foreign = [];
335
+ for (const policy of policies) {
336
+ const subjects = isPlainObject(policy) && Array.isArray(policy.subjects) ? policy.subjects : [];
337
+ for (const subject of subjects) {
338
+ if (!isPlainObject(subject) || subject.type !== 'job')
339
+ continue;
340
+ if (typeof subject.value !== 'string' || !subject.value.startsWith(`${addon.slug}.`))
341
+ foreign.push(`'${String(subject.value)}' (policy '${String(policy.id)}')`);
342
+ }
343
+ }
344
+ if (foreign.length === 0)
345
+ continue;
346
+ const reason = `its policies name the job(s) ${foreign.join(', ')}, which are not its own; an add-on's policy names only its own jobs, ${addon.slug}.<job>`;
347
+ layer.refused.set(addon.slug, reason);
348
+ layer.problems.push(`${addonProblemSubject(addon.slug, addon.enabled)}: ${reason}; the add-on is not loaded`);
349
+ }
350
+ return layer;
351
+ }
316
352
  /** The configuration key that makes an addon site-critical (#1883). */
317
353
  export function addonSiteCriticalKey(slug) {
318
354
  return `ngdpbase.addons.${slug}.site-critical`;
@@ -402,9 +438,10 @@ export function mergeWithAddonLayer(shipped, addons, custom) {
402
438
  const databaseLayer = addonDatabaseLayer(addons, custom);
403
439
  const criticalLayer = addonSiteCriticalLayer(addons, custom);
404
440
  const sensitiveLayer = addonSensitiveValuesLayer(addons);
441
+ const jobPolicyLayer = addonJobPolicyLayer(addons);
405
442
  const siteCritical = criticalLayer.siteCritical;
406
443
  const refused = new Map(layer.refused);
407
- for (const [slug, reason] of [...databaseLayer.refused, ...criticalLayer.refused, ...sensitiveLayer.refused])
444
+ for (const [slug, reason] of [...databaseLayer.refused, ...criticalLayer.refused, ...sensitiveLayer.refused, ...jobPolicyLayer.refused])
408
445
  refused.set(slug, refused.has(slug) ? `${refused.get(slug)}; ${reason}` : reason);
409
446
  const databases = databaseLayer.declarations.map((d) => ({ ...d, enabled: d.enabled && !refused.has(d.declaredBy) }));
410
447
  const withAddons = addons
@@ -418,14 +455,14 @@ export function mergeWithAddonLayer(shipped, addons, custom) {
418
455
  return deepMergeObjects(acc, rest);
419
456
  }, shipped);
420
457
  if (Object.keys(layer.entries).length === 0) {
421
- return { merged: deepMergeConfigs(withAddons, custom), problems: [...layer.problems, ...databaseLayer.problems, ...criticalLayer.problems, ...sensitiveLayer.problems], refused, databases, siteCritical };
458
+ return { merged: deepMergeConfigs(withAddons, custom), problems: [...layer.problems, ...databaseLayer.problems, ...criticalLayer.problems, ...sensitiveLayer.problems, ...jobPolicyLayer.problems], refused, databases, siteCritical };
422
459
  }
423
460
  const shippedCategories = isPlainObject(withAddons[SYSTEM_CATEGORY_KEY]) ? withAddons[SYSTEM_CATEGORY_KEY] : {};
424
461
  const layered = { ...withAddons, [SYSTEM_CATEGORY_KEY]: deepMergeObjects(shippedCategories, layer.entries) };
425
462
  const merged = deepMergeObjects(layered, custom);
426
463
  const stood = ownerDeclarationsStand(merged[SYSTEM_CATEGORY_KEY], layer, customCategories);
427
464
  merged[SYSTEM_CATEGORY_KEY] = stood.categories;
428
- return { merged: merged, problems: [...layer.problems, ...stood.problems, ...databaseLayer.problems, ...criticalLayer.problems, ...sensitiveLayer.problems], refused, databases, siteCritical };
465
+ return { merged: merged, problems: [...layer.problems, ...stood.problems, ...databaseLayer.problems, ...criticalLayer.problems, ...sensitiveLayer.problems, ...jobPolicyLayer.problems], refused, databases, siteCritical };
429
466
  }
430
467
  /**
431
468
  * The full merge for a caller with the two files already read (the manager,
@@ -18,9 +18,11 @@
18
18
  * - `registerShim` and `registerReceiver` (#1979, ICE) register into the import
19
19
  * engine under this add-on's slug, and only the names its manifest declares;
20
20
  * - the egress tools (`resolveEgressPolicy`, `validateUrl`,
21
- * `createGuardedElasticsearchClient`, `refusedNodeMessage`) and `isAtLeast`
22
- * are core's own pure functions, handed on unchanged, so an add-on never
23
- * imports them from `dist/` (#1813);
21
+ * `createGuardedElasticsearchClient`, `refusedNodeMessage`), `isAtLeast`
22
+ * and `zonedParts` (#1998) are core's own pure functions, handed on
23
+ * unchanged, so an add-on never imports them from `dist/` (#1813);
24
+ * - `keyedSerial()` returns a new `KeyedSerial`, core's own per-key queue, so
25
+ * an add-on's manager changes each of its records one at a time (#2065);
24
26
  * - the object is `Object.freeze`d, so no add-on can replace a value.
25
27
  *
26
28
  * Additive only: nothing here is removed or renamed without a major release.
@@ -36,6 +38,8 @@ import { resolveEgressPolicy } from '../http/egressPolicy.js';
36
38
  import { validateUrl } from '../http/ssrf.js';
37
39
  import { createGuardedElasticsearchClient, refusedNodeMessage } from '../http/guardedElasticsearch.js';
38
40
  import { isAtLeast } from './version.js';
41
+ import { zonedParts } from './availability.js';
42
+ import { KeyedSerial } from './keyedSerial.js';
39
43
  import { recordAuditEvent } from './auditEvents.js';
40
44
  import { addonAuditEventName } from './auditEventNames.js';
41
45
  import type { IceReceiver, IceShim } from '../ice/types.js';
@@ -80,6 +84,20 @@ export interface AddonHost {
80
84
  readonly refusedNodeMessage: typeof refusedNodeMessage;
81
85
  /** Whether an `x.y.z` version is at least another; throws on a version that is not `x.y.z`. */
82
86
  readonly isAtLeast: typeof isAtLeast;
87
+ /**
88
+ * #1998: an instant in a named time zone, as its local parts: `year`,
89
+ * `month` (1–12), `date` (`YYYY-MM-DD`), `day` (weekday name) and `minutes`
90
+ * since midnight. A scheduled job passes its `ctx.slot` and
91
+ * `BackgroundJobManager.scheduleTimeZone()` to learn which local month or
92
+ * day the slot falls in. An unknown zone answers in UTC.
93
+ */
94
+ readonly zonedParts: typeof zonedParts;
95
+ /**
96
+ * #2065: a new `KeyedSerial` (core's own) at each call; take one per manager.
97
+ * Run each read-modify-write of one of your records inside `run(recordId, fn)`,
98
+ * the read included. Not re-entrant: `fn` must not `run` the same key again.
99
+ */
100
+ readonly keyedSerial: () => KeyedSerial;
83
101
  /**
84
102
  * #1979 (ICE, D6): register an import shim, as `<slug>.<shim.name>`. Call it
85
103
  * in `register`. Refused when the manifest's `shims` does not name it.