@jwilleke/ngdpbase-types 5.27.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.
- package/lib/packages/ngdpbase-types/index.d.ts +4 -1
- package/lib/src/context/ActorContext.d.ts +15 -2
- package/lib/src/context/ActorContext.js +20 -0
- package/lib/src/ice/types.d.ts +22 -0
- package/lib/src/managers/AddonsManager.d.ts +7 -3
- package/lib/src/managers/AssetManager.d.ts +7 -3
- package/lib/src/managers/ExportManager.js +5 -0
- package/lib/src/managers/ImportManager.d.ts +28 -7
- package/lib/src/managers/OrganizationManager.d.ts +5 -3
- package/lib/src/managers/PageManager.d.ts +32 -0
- package/lib/src/managers/PersonManager.d.ts +36 -11
- package/lib/src/managers/RoleManager.d.ts +76 -17
- package/lib/src/managers/UserManager.d.ts +55 -2
- package/lib/src/security/PolicyDecisionPoint.d.ts +18 -9
- package/lib/src/security/PolicyInformationPoint.d.ts +25 -10
- package/lib/src/types/User.d.ts +1 -1
- package/lib/src/utils/auditEventNames.generated.d.ts +10 -10
- package/lib/src/utils/auditEventNames.generated.js +10 -10
- package/lib/src/utils/contactAccess.d.ts +30 -0
- package/lib/src/utils/keyedSerial.d.ts +31 -0
- package/lib/src/utils/privateStoreAccess.js +1 -3
- package/lib/src/utils/roleChangeError.d.ts +25 -0
- package/lib/src/utils/subjectMayDo.d.ts +2 -2
- package/package.json +1 -1
|
@@ -36,9 +36,12 @@ export type { PageAction } from '../../src/utils/pageUrl.js';
|
|
|
36
36
|
export type { default as ConsentManager, ConsentScopeStatus } from '../../src/managers/ConsentManager.js';
|
|
37
37
|
export type { AddonConsentRegistration, ConsentDocument, ConsentRecord, ConsentRequiredError, ConsentScope, ConsentSeen, ConsentStatus } from '../../src/types/Consent.js';
|
|
38
38
|
export type { default as ImportManager } from '../../src/managers/ImportManager.js';
|
|
39
|
-
export type { IceShim, IceReceiver, IceItem, IceKind, IceWarning, ShimDirection, ShimAccepts, ShimTools, ShimReadResult, ReceiveInfo, ReceiveOutcome, IceCitation, KeptOriginal, OriginalChoice, IceUpload, IceSource, IceRunRequest, IceRunReport, IceFileReport, IceFileStatus } from '../../src/ice/types.js';
|
|
39
|
+
export type { IceShim, IceReceiver, IceItem, IceKind, IceWarning, ShimDirection, ShimAccepts, ShimTools, ShimReadResult, ReceiveInfo, ReceiveOutcome, IceCitation, KeptOriginal, OriginalChoice, IceOriginalChoices, IceUpload, IceSource, IceRunRequest, IceRunReport, IceFileReport, IceFileStatus } from '../../src/ice/types.js';
|
|
40
|
+
export type { default as UserManager, UserUpdateInput, UserUpdateFn } from '../../src/managers/UserManager.js';
|
|
41
|
+
export type { User, UserPreferences } from '../../src/types/User.js';
|
|
40
42
|
export type { default as PersonManager, LinkOfferOutcome } from '../../src/managers/PersonManager.js';
|
|
41
43
|
export type { default as OrganizationManager } from '../../src/managers/OrganizationManager.js';
|
|
42
44
|
export type { Person, PersonAffiliation, PersonInput, PersonUpdate } from '../../src/types/Person.js';
|
|
43
45
|
export type { Organization, OrganizationInput, OrganizationUpdate } from '../../src/types/Organization.js';
|
|
44
46
|
export type { ContactRefusedError } from '../../src/utils/contactAccess.js';
|
|
47
|
+
export type { default as RoleManager } from '../../src/managers/RoleManager.js';
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* the call arrived under, and the address it came from. What does not: roles.
|
|
14
14
|
* Authority is resolved at the moment of each decision, never carried.
|
|
15
15
|
*/
|
|
16
|
-
import type { PermissionSubject } from '../managers/UserManager.js';
|
|
17
|
-
import type
|
|
16
|
+
import type { PermissionSubject, JobSubject } from '../managers/UserManager.js';
|
|
17
|
+
import { type JobContext } from './JobContext.js';
|
|
18
18
|
export type ActorContext = PermissionSubject | JobContext;
|
|
19
19
|
/**
|
|
20
20
|
* The fields an audit record takes from a context.
|
|
@@ -35,6 +35,19 @@ export interface ActorAttribution {
|
|
|
35
35
|
metadata: Record<string, unknown>;
|
|
36
36
|
}
|
|
37
37
|
export declare function isJobContext(ctx: ActorContext): ctx is JobContext;
|
|
38
|
+
/**
|
|
39
|
+
* The one place a context becomes the subject a decision reads (#2022).
|
|
40
|
+
*
|
|
41
|
+
* Called by the deciders — `PolicyDecisionPoint` (`decide`, `permits`,
|
|
42
|
+
* `holds`) and `PolicyInformationPoint`'s page questions — and by nobody
|
|
43
|
+
* else: a caller hands them its context as given. A `JobContext` carries
|
|
44
|
+
* identity, never authority, so it becomes a `JobSubject` whose roles policy
|
|
45
|
+
* resolves now, keeping its token, share and proxy claims
|
|
46
|
+
* (`toPermissionSubject`). Anything else is already a subject and passes
|
|
47
|
+
* through. The object guard is #1173's: `'x' in 'jim'` throws. Not part of
|
|
48
|
+
* the add-on API; `npm run lint:context` holds callers to it.
|
|
49
|
+
*/
|
|
50
|
+
export declare function decisionSubject<S extends PermissionSubject | JobSubject>(input: ActorContext | S | null | undefined): PermissionSubject | JobSubject | S | null | undefined;
|
|
38
51
|
/**
|
|
39
52
|
* Who acted, for the record — read from the context, never guessed.
|
|
40
53
|
*
|
|
@@ -1,8 +1,28 @@
|
|
|
1
|
+
import { toPermissionSubject } from './JobContext.js';
|
|
1
2
|
import { attributedTo } from './bootActions.js';
|
|
2
3
|
import { proxyAttribution } from '../utils/proxyClaim.js';
|
|
3
4
|
export function isJobContext(ctx) {
|
|
4
5
|
return 'origin' in ctx && 'requestedAt' in ctx;
|
|
5
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* The one place a context becomes the subject a decision reads (#2022).
|
|
9
|
+
*
|
|
10
|
+
* Called by the deciders — `PolicyDecisionPoint` (`decide`, `permits`,
|
|
11
|
+
* `holds`) and `PolicyInformationPoint`'s page questions — and by nobody
|
|
12
|
+
* else: a caller hands them its context as given. A `JobContext` carries
|
|
13
|
+
* identity, never authority, so it becomes a `JobSubject` whose roles policy
|
|
14
|
+
* resolves now, keeping its token, share and proxy claims
|
|
15
|
+
* (`toPermissionSubject`). Anything else is already a subject and passes
|
|
16
|
+
* through. The object guard is #1173's: `'x' in 'jim'` throws. Not part of
|
|
17
|
+
* the add-on API; `npm run lint:context` holds callers to it.
|
|
18
|
+
*/
|
|
19
|
+
export function decisionSubject(input) {
|
|
20
|
+
if (typeof input !== 'object' || input === null)
|
|
21
|
+
return input;
|
|
22
|
+
// A JobSubject is not an ActorContext, and is not a job context either.
|
|
23
|
+
const ctx = input;
|
|
24
|
+
return isJobContext(ctx) ? toPermissionSubject(ctx) : input;
|
|
25
|
+
}
|
|
6
26
|
/**
|
|
7
27
|
* Who acted, for the record — read from the context, never guessed.
|
|
8
28
|
*
|
package/lib/src/ice/types.d.ts
CHANGED
|
@@ -121,6 +121,16 @@ export interface RegisteredShim extends IceShim {
|
|
|
121
121
|
}
|
|
122
122
|
/** D11: what the person importing chose to do with the original file. */
|
|
123
123
|
export type OriginalChoice = 'cite' | 'keep' | 'none';
|
|
124
|
+
/**
|
|
125
|
+
* D11: the original-file choices a receiver offers, and which one is chosen
|
|
126
|
+
* when the user picks none (#2021). The import screen offers only `allowed`,
|
|
127
|
+
* and the engine refuses a run asking for any other.
|
|
128
|
+
*/
|
|
129
|
+
export interface IceOriginalChoices {
|
|
130
|
+
allowed: OriginalChoice[];
|
|
131
|
+
/** One of `allowed`. Default: `ngdpbase.import.engine.default-original` when allowed, else the first allowed. */
|
|
132
|
+
default?: OriginalChoice;
|
|
133
|
+
}
|
|
124
134
|
/** A citation to the origin (D11): the receiver renders it, as a footnote on a page. */
|
|
125
135
|
export interface IceCitation {
|
|
126
136
|
name: string;
|
|
@@ -172,6 +182,8 @@ export interface IceReceiver {
|
|
|
172
182
|
* store). Without this, `keep` hands the original to the files receiver.
|
|
173
183
|
*/
|
|
174
184
|
keepOriginal?(original: IceItem, ctx: ActorContext): Promise<ReceiveOutcome>;
|
|
185
|
+
/** The original-file choices this receiver offers (D11). Without it: all three. */
|
|
186
|
+
originals?: IceOriginalChoices;
|
|
175
187
|
}
|
|
176
188
|
export interface RegisteredReceiver extends IceReceiver {
|
|
177
189
|
id: string;
|
|
@@ -238,6 +250,14 @@ export interface IceRunReport {
|
|
|
238
250
|
error?: string;
|
|
239
251
|
files: IceFileReport[];
|
|
240
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* A kept original as the ledger remembers it, with the door that kept it
|
|
255
|
+
* (a receiver's own `keepOriginal`, or `core.files`). A later run of the same
|
|
256
|
+
* fingerprint through the same door reuses it instead of keeping another copy.
|
|
257
|
+
*/
|
|
258
|
+
export interface LedgerOriginal extends KeptOriginal {
|
|
259
|
+
door: string;
|
|
260
|
+
}
|
|
241
261
|
/** What the engine remembers about one file in a scope. */
|
|
242
262
|
export interface LedgerFileEntry {
|
|
243
263
|
name: string;
|
|
@@ -247,6 +267,8 @@ export interface LedgerFileEntry {
|
|
|
247
267
|
message?: string;
|
|
248
268
|
at: string;
|
|
249
269
|
refs: string[];
|
|
270
|
+
/** The original kept for this fingerprint, whatever became of the conversion (D11). */
|
|
271
|
+
original?: LedgerOriginal;
|
|
250
272
|
}
|
|
251
273
|
/**
|
|
252
274
|
* Where the engine keeps fingerprints, per-file status and item keys, per
|
|
@@ -121,8 +121,12 @@ export interface AddonModule {
|
|
|
121
121
|
* "section was not rendered at all" (addon disabled mid-session, cached
|
|
122
122
|
* form). Without it, absent checkbox fields look identical to "user
|
|
123
123
|
* submitted no such field" and you may silently clobber stored values.
|
|
124
|
+
*
|
|
125
|
+
* `ctx` is the signed-in user's own context, forwarded from the request:
|
|
126
|
+
* pass it to your manager's door (and to the PDP) instead of rebuilding a
|
|
127
|
+
* subject from `user` (#2018).
|
|
124
128
|
*/
|
|
125
|
-
profileSection?(user: AddonProfileUser): Promise<AddonProfileSection | null> | AddonProfileSection | null;
|
|
129
|
+
profileSection?(user: AddonProfileUser, ctx: ActorContext): Promise<AddonProfileSection | null> | AddonProfileSection | null;
|
|
126
130
|
/**
|
|
127
131
|
* Optional save handler paired with `profileSection()` (#534).
|
|
128
132
|
*
|
|
@@ -673,7 +677,7 @@ declare class AddonsManager extends BaseManager {
|
|
|
673
677
|
* Result order follows registration order of the addons map so the
|
|
674
678
|
* displayed section order is stable across renders.
|
|
675
679
|
*/
|
|
676
|
-
getProfileSections(user: AddonProfileUser): Promise<AddonProfileSectionEntry[]>;
|
|
680
|
+
getProfileSections(user: AddonProfileUser, ctx: ActorContext): Promise<AddonProfileSectionEntry[]>;
|
|
677
681
|
/**
|
|
678
682
|
* #1234: say at load when an addon still implements the pre-#1234 hook.
|
|
679
683
|
*
|
|
@@ -689,7 +693,7 @@ declare class AddonsManager extends BaseManager {
|
|
|
689
693
|
* `saveProfileSection()` handler (#534).
|
|
690
694
|
*
|
|
691
695
|
* Runs SEQUENTIALLY — each addon's read-modify-write of user preferences
|
|
692
|
-
* (
|
|
696
|
+
* (`updateUser`'s function form, #2025) must see the
|
|
693
697
|
* previous addon's writes. Parallel execution would create a same-snapshot
|
|
694
698
|
* race where two addons read identical preferences, each merge their own
|
|
695
699
|
* keys, and the second writer clobbers the first writer's changes.
|
|
@@ -67,10 +67,11 @@ declare class AssetManager extends BaseManager {
|
|
|
67
67
|
/**
|
|
68
68
|
* Return all AssetRecords referenced by the given page slug.
|
|
69
69
|
*
|
|
70
|
-
* O(1) index lookup + one
|
|
70
|
+
* O(1) index lookup + one provider lookup per referenced asset.
|
|
71
71
|
* Stale entries (asset deleted or provider removed) are silently filtered out.
|
|
72
|
+
* Nothing for a caller who may not read assets (`asset-read`, #2018).
|
|
72
73
|
*/
|
|
73
|
-
getAssetsForPage(slug: string): Promise<AssetRecord[]>;
|
|
74
|
+
getAssetsForPage(ctx: ActorContext, slug: string): Promise<AssetRecord[]>;
|
|
74
75
|
/**
|
|
75
76
|
* Remove a page's entry from the reverse index.
|
|
76
77
|
* Call this when a page is deleted or renamed.
|
|
@@ -103,8 +104,11 @@ declare class AssetManager extends BaseManager {
|
|
|
103
104
|
/**
|
|
104
105
|
* Retrieve a single asset by id. If providerId is given only that provider
|
|
105
106
|
* is checked; otherwise all providers are tried in registration order.
|
|
107
|
+
* Null for a caller who may not read assets (`asset-read`, asked of the PDP
|
|
108
|
+
* with `ctx`, as FileManager asks for the public place; #2018).
|
|
106
109
|
*/
|
|
107
|
-
getById(id: string, providerId?: string): Promise<AssetRecord | null>;
|
|
110
|
+
getById(ctx: ActorContext, id: string, providerId?: string): Promise<AssetRecord | null>;
|
|
111
|
+
private lookup;
|
|
108
112
|
/**
|
|
109
113
|
* Generate or retrieve a cached thumbnail.
|
|
110
114
|
* Requires the provider to declare the 'thumbnail' capability.
|
|
@@ -539,6 +539,11 @@ class ExportManager extends BaseManager {
|
|
|
539
539
|
* @returns {Promise<void>}
|
|
540
540
|
*/
|
|
541
541
|
async deleteExport(filename) {
|
|
542
|
+
// Only a plain file name inside the export folder: never a path that
|
|
543
|
+
// reaches another folder.
|
|
544
|
+
if (!filename || filename !== path.basename(filename) || filename === '.' || filename === '..') {
|
|
545
|
+
throw new Error(`Not an export file name: ${filename}`);
|
|
546
|
+
}
|
|
542
547
|
const filePath = path.join(this.exportDirectory, filename);
|
|
543
548
|
await fs.unlink(filePath);
|
|
544
549
|
logger.info(`Deleted export: ${filename}`);
|
|
@@ -31,8 +31,8 @@ import BaseManager, { BackupData } from './BaseManager.js';
|
|
|
31
31
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
32
32
|
import { IContentConverter } from '../converters/IContentConverter.js';
|
|
33
33
|
import { type ZipReadLimits } from '../utils/zipArchive.js';
|
|
34
|
-
import {
|
|
35
|
-
import type { IceLedger, IceReceiver, IceRunReport, IceRunRequest, IceShim, RegisteredShim } from '../ice/types.js';
|
|
34
|
+
import type { JobContext } from '../context/JobContext.js';
|
|
35
|
+
import type { IceLedger, IceOriginalChoices, IceReceiver, IceRunReport, IceRunRequest, IceShim, RegisteredShim } from '../ice/types.js';
|
|
36
36
|
/**
|
|
37
37
|
* Options for import operations
|
|
38
38
|
*/
|
|
@@ -253,6 +253,12 @@ export type TakeoutImportReport = {
|
|
|
253
253
|
*/
|
|
254
254
|
reportPage?: string;
|
|
255
255
|
};
|
|
256
|
+
/** A receiver the import screen may offer this user (#2021): its id, label and original-file choices (D11). */
|
|
257
|
+
export interface UsableReceiver {
|
|
258
|
+
id: string;
|
|
259
|
+
label?: string;
|
|
260
|
+
originals: Required<IceOriginalChoices>;
|
|
261
|
+
}
|
|
256
262
|
/**
|
|
257
263
|
* Import Manager class
|
|
258
264
|
*
|
|
@@ -310,11 +316,24 @@ declare class ImportManager extends BaseManager {
|
|
|
310
316
|
listShims(): Array<Pick<RegisteredShim, 'id' | 'owner' | 'label' | 'direction' | 'produces' | 'receivers'> & {
|
|
311
317
|
enabled: boolean;
|
|
312
318
|
}>;
|
|
313
|
-
/**
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
319
|
+
/**
|
|
320
|
+
* The receivers this context may use (D5): each answers with its own
|
|
321
|
+
* existing permission. Each comes with the original-file choices it offers
|
|
322
|
+
* (D11), for the import screen (#2021).
|
|
323
|
+
*/
|
|
324
|
+
receiversFor(ctx: ActorContext): Promise<UsableReceiver[]>;
|
|
325
|
+
/**
|
|
326
|
+
* The original-file choices a receiver offers (D11, #2021): what it
|
|
327
|
+
* declares, else all three; its default when it names one it allows, else
|
|
328
|
+
* `ngdpbase.import.engine.default-original` when allowed, else the first.
|
|
329
|
+
*/
|
|
330
|
+
private originalChoices;
|
|
331
|
+
/**
|
|
332
|
+
* The request as the engine runs it: the receiver's default original when
|
|
333
|
+
* none was chosen, and a refusal for a choice the receiver does not offer
|
|
334
|
+
* (D11, #2021). An unknown receiver is left for the run to report.
|
|
335
|
+
*/
|
|
336
|
+
private withOriginal;
|
|
318
337
|
/**
|
|
319
338
|
* Run one import now and wait for it. For code already off the request
|
|
320
339
|
* path: an add-on's own background job, a test. A route uses
|
|
@@ -332,6 +351,8 @@ declare class ImportManager extends BaseManager {
|
|
|
332
351
|
enqueueImport(request: IceRunRequest, ctx: JobContext): Promise<string>;
|
|
333
352
|
/** A run's report, to the person it runs as; null to anyone else. */
|
|
334
353
|
getImportRun(runId: string, ctx: ActorContext): IceRunReport | null;
|
|
354
|
+
/** The runs this process holds for the user `ctx` names, newest first (#2021): their own, never anyone else's. */
|
|
355
|
+
importRunsFor(ctx: ActorContext): IceRunReport[];
|
|
335
356
|
/** Use this ledger instead of the file one: a test's, or a database table's. */
|
|
336
357
|
setImportLedger(ledger: IceLedger): void;
|
|
337
358
|
private newRunReport;
|
|
@@ -88,13 +88,15 @@ declare class OrganizationManager extends BaseManager {
|
|
|
88
88
|
* problem actually bites.
|
|
89
89
|
*/
|
|
90
90
|
getInstallOrg(): Promise<Organization | null>;
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
/** One organization by `@id`, for a caller who may read contacts (`contact-read`), or core's own work (#2018). */
|
|
92
|
+
getById(ctx: ActorContext, id: string): Promise<Organization | null>;
|
|
93
|
+
/** One organization by its file name, decided as {@link getById} (#2018). */
|
|
94
|
+
getByFile(ctx: ActorContext, filename: string): Promise<Organization | null>;
|
|
93
95
|
/** #1006: how many organizations this instance holds. Count only. */
|
|
94
96
|
getManagerStats(): Promise<ManagerStats>;
|
|
95
97
|
/** Every organization, for a caller who may read contacts (`contact-read`). */
|
|
96
98
|
list(ctx: ActorContext): Promise<Organization[]>;
|
|
97
|
-
/** One organization by `@id
|
|
99
|
+
/** One organization by `@id`: {@link getById}. */
|
|
98
100
|
get(ctx: ActorContext, id: string): Promise<Organization | null>;
|
|
99
101
|
/**
|
|
100
102
|
* Organizations whose `name`, `legalName`, `email` or a contact point's
|
|
@@ -1128,6 +1128,38 @@ declare class PageManager extends BaseManager implements CatalogSource {
|
|
|
1128
1128
|
* const deleted = await pageManager.deletePage('Old Page', req.userContext);
|
|
1129
1129
|
*/
|
|
1130
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
|
+
}>;
|
|
1131
1163
|
/**
|
|
1132
1164
|
* The name a page was reached by, as the shared indexes key it: a private
|
|
1133
1165
|
* page's path (#1456), else its title — the caller may have used a uuid or
|
|
@@ -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
|
-
*
|
|
27
|
-
*
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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.
|
|
118
|
-
*
|
|
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
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* role
|
|
129
|
-
|
|
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
|
|
132
|
-
|
|
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
|
-
|
|
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
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import BaseManager from '../managers/BaseManager.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type ActorContext } from '../context/ActorContext.js';
|
|
3
3
|
import { WikiEngine } from '../types/WikiEngine.js';
|
|
4
4
|
import type { AgentTokenGrant, ProxyClaim } from '../managers/UserManager.js';
|
|
5
5
|
import type { Request } from 'express';
|
|
@@ -46,6 +46,12 @@ interface UserContext {
|
|
|
46
46
|
viaProxy?: ProxyClaim;
|
|
47
47
|
[key: string]: unknown;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* What the page questions take (#2022): the caller's context as given — a
|
|
51
|
+
* request's subject or a job's `JobContext` — or a subject already in decision
|
|
52
|
+
* form. They convert it themselves, through {@link asSubject}.
|
|
53
|
+
*/
|
|
54
|
+
type PageQuestionCaller = ActorContext | UserContext | null | undefined;
|
|
49
55
|
/**
|
|
50
56
|
* Access decision log entry
|
|
51
57
|
*/
|
|
@@ -173,6 +179,15 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
173
179
|
* visitor holds, which is the safe answer for someone who no longer exists.
|
|
174
180
|
*/
|
|
175
181
|
resolveSubjectNow(username: string | undefined): Promise<UserContext>;
|
|
182
|
+
/**
|
|
183
|
+
* The subject as a page's own rules match it (#2022): for a job's subject
|
|
184
|
+
* (`resolveRolesNow`, #1212), its username with the roles its account
|
|
185
|
+
* holds NOW, read through {@link resolveSubjectNow} — the path the PDP
|
|
186
|
+
* takes for the same subject; anyone else as carried. A job carries no
|
|
187
|
+
* roles, so matching it without this matched none, and an audience naming
|
|
188
|
+
* a role its user holds left the job out.
|
|
189
|
+
*/
|
|
190
|
+
private matchableNow;
|
|
176
191
|
/**
|
|
177
192
|
* Who is making this request: the subject the session middleware resolved,
|
|
178
193
|
* else the session's account resolved now, else the anonymous visitor.
|
|
@@ -348,14 +363,14 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
348
363
|
* private page gets at Tier 0: the owner, or a delegate of the owner; never
|
|
349
364
|
* a role. A refusal is recorded like any page refusal (`authorization-deny`).
|
|
350
365
|
*
|
|
351
|
-
* @param
|
|
366
|
+
* @param caller - the caller's context, as given: a request's subject or a job's (#2022); null is anonymous
|
|
352
367
|
* @param owner - the container's owner (a private file's `creator`)
|
|
353
368
|
* @param resource - what is being reached, for the record (e.g. `attachment:<id>`)
|
|
354
369
|
* @param action - the action asked for (e.g. `view`)
|
|
355
370
|
* @param vault - the vault inside the container, when the question is about one:
|
|
356
371
|
* a share link the owner issued gets in only for a vault it names (#1388)
|
|
357
372
|
*/
|
|
358
|
-
canAccessPrivateContainer(
|
|
373
|
+
canAccessPrivateContainer(caller: PageQuestionCaller, owner: string, resource: string, action: string, vault?: string): boolean;
|
|
359
374
|
/**
|
|
360
375
|
* Check whether a user can access a given page — for cross-page checks where
|
|
361
376
|
* the current WikiContext describes a DIFFERENT page than the one being
|
|
@@ -381,14 +396,14 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
381
396
|
* case.
|
|
382
397
|
* - any tier returns deny.
|
|
383
398
|
*
|
|
384
|
-
* @param
|
|
399
|
+
* @param caller - The caller's context, as given: a request's subject or a job's (#2022); null / undefined is anonymous.
|
|
385
400
|
* @param pageName - Target page to check.
|
|
386
401
|
* @param action - Action verb (e.g., `'view'`, `'edit'`, `'delete'`).
|
|
387
402
|
* @param knownMetadata - The page's metadata when the caller has just
|
|
388
403
|
* resolved it: the page door (#1622), which reads the page by the same
|
|
389
404
|
* resolution it decides on. Omitted, it is loaded here.
|
|
390
405
|
*/
|
|
391
|
-
canUserAccessPage(
|
|
406
|
+
canUserAccessPage(caller: PageQuestionCaller, pageName: string, action: string, knownMetadata?: PageFrontmatter): Promise<boolean>;
|
|
392
407
|
/**
|
|
393
408
|
* {@link canUserAccessPage} as an affordance (#1668): would the door allow
|
|
394
409
|
* this action on that page — asked only to decide whether to show a control
|
|
@@ -399,7 +414,7 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
399
414
|
* @param knownMetadata - As for {@link canUserAccessPage}: the page's
|
|
400
415
|
* metadata when the caller has just resolved it (`PageManager.mayShowPage`).
|
|
401
416
|
*/
|
|
402
|
-
pageAffordanceFor(
|
|
417
|
+
pageAffordanceFor(caller: PageQuestionCaller, pageName: string, action: string, knownMetadata?: PageFrontmatter): Promise<boolean>;
|
|
403
418
|
/** The cross-page check, for the door (`record`) or an affordance (#1668). */
|
|
404
419
|
private crossPageDecision;
|
|
405
420
|
/**
|
|
@@ -425,7 +440,7 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
425
440
|
* A candidate without metadata is not listed (#714's convention). Order is
|
|
426
441
|
* preserved. Returns titles.
|
|
427
442
|
*/
|
|
428
|
-
filterAccessiblePages(
|
|
443
|
+
filterAccessiblePages(caller: PageQuestionCaller, action: string, candidates: ReadonlyArray<{
|
|
429
444
|
title: string;
|
|
430
445
|
metadata: PageFrontmatter | null | undefined;
|
|
431
446
|
}>): Promise<string[]>;
|
|
@@ -445,7 +460,7 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
445
460
|
* A refusal is audited as `authorization-deny` on the media resource, with
|
|
446
461
|
* the share attribution when there is one.
|
|
447
462
|
*/
|
|
448
|
-
canUserAccessMediaItem(
|
|
463
|
+
canUserAccessMediaItem(caller: PageQuestionCaller, item: MediaItem): Promise<boolean>;
|
|
449
464
|
/**
|
|
450
465
|
* Check front matter audience / access fields (Tier 1).
|
|
451
466
|
* Returns a decision object; decided=false means no front matter restriction — fall through.
|
|
@@ -462,10 +477,10 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
462
477
|
* write door, so a write cannot reach a page its writer is outside the
|
|
463
478
|
* audience of — whatever route it came by. Records nothing; the door does.
|
|
464
479
|
*
|
|
465
|
-
* @param
|
|
480
|
+
* @param caller - the caller's context, as given (a job's included, #2022)
|
|
466
481
|
* @param metadata - the page's current frontmatter
|
|
467
482
|
*/
|
|
468
|
-
audienceAdmits(
|
|
483
|
+
audienceAdmits(caller: PageQuestionCaller, metadata: PageFrontmatter | null | undefined): Promise<boolean>;
|
|
469
484
|
/**
|
|
470
485
|
* Record/audit an access decision.
|
|
471
486
|
* Accepts either a single object or positional args for backward compatibility.
|
package/lib/src/types/User.d.ts
CHANGED
|
@@ -73,7 +73,7 @@ export interface User {
|
|
|
73
73
|
/**
|
|
74
74
|
* @deprecated since #617 iteration 3b. Role membership is now stored
|
|
75
75
|
* canonically as OrganizationRole records owned by `RoleManager`; resolve
|
|
76
|
-
* a user's roles via `RoleManager.resolveUserRoles(username)`. The field
|
|
76
|
+
* a user's roles via `RoleManager.resolveUserRoles(ctx, username)`. The field
|
|
77
77
|
* is retained as optional for backwards compatibility while in-flight
|
|
78
78
|
* `users.json` files still carry it; the `strip-user-roles.ts` migration
|
|
79
79
|
* removes it. New code MUST NOT read or write this field directly.
|
|
@@ -50,9 +50,9 @@ export declare const AUDIT_EVENT: {
|
|
|
50
50
|
readonly CONFIG_CHANGE: "config-change";
|
|
51
51
|
/** Every custom configuration value discarded; recorded before the reset, which cannot proceed without it */
|
|
52
52
|
readonly CONFIG_RESET: "config-reset";
|
|
53
|
-
/** A
|
|
53
|
+
/** A user accepted a document they are asked to accept (#1603): which document (core's terms-of-service or privacy-policy, or an add-on's), its version and effective date, and how (sign-up, sign-in, turning an add-on on). Recorded before the acceptance is stored, which cannot proceed without it: consent must be provable */
|
|
54
54
|
readonly CONSENT_GRANT: "consent-grant";
|
|
55
|
-
/** A
|
|
55
|
+
/** A user's acceptances ended (#1603): today only when their account is deleted, which removes their consent records; names the documents and versions they had accepted. Recorded before the records are removed */
|
|
56
56
|
readonly CONSENT_WITHDRAW: "consent-withdraw";
|
|
57
57
|
/** Contact created, changed, deleted or linked to an account; metadata names the kind (person or organization), the operation and the fields changed, never their values */
|
|
58
58
|
readonly CONTACT_MANAGE: "contact-manage";
|
|
@@ -112,21 +112,21 @@ export declare const AUDIT_EVENT: {
|
|
|
112
112
|
readonly POLICY_EVALUATE: "policy-evaluate";
|
|
113
113
|
/** Security posture at startup, compared against the previous start */
|
|
114
114
|
readonly POSTURE_RECORDED: "posture-recorded";
|
|
115
|
-
/** Proxy grant made: someone may act for the
|
|
115
|
+
/** Proxy grant made: someone may act for the user named in user; actor is who made it (#1762) */
|
|
116
116
|
readonly PROXY_GRANT: "proxy-grant";
|
|
117
|
-
/** Proxy grant ended: revoked, or the account closed; user is the
|
|
117
|
+
/** Proxy grant ended: revoked, or the account closed; user is the user represented, actor who ended it (#1762) */
|
|
118
118
|
readonly PROXY_REVOKE: "proxy-revoke";
|
|
119
|
-
/** A signed-in session started or stopped acting for someone under a proxy grant, or opens their private store: chosen, switched-back, ended because the grant no longer allows it (grant-ended, proxy-access-off), or vault-opened; user is the
|
|
119
|
+
/** A signed-in session started or stopped acting for someone under a proxy grant, or opens their private store: chosen, switched-back, ended because the grant no longer allows it (grant-ended, proxy-access-off), or vault-opened; user is the user represented, actor the user signed in (#1762) */
|
|
120
120
|
readonly PROXY_SWITCH: "proxy-switch";
|
|
121
|
-
/** Step-up: a re-authentication failed (wrong password, another
|
|
121
|
+
/** Step-up: a re-authentication failed (wrong password, another user's passkey, throttled) */
|
|
122
122
|
readonly REAUTH_FAILURE: "reauth-failure";
|
|
123
123
|
/** Step-up: an action asked for a fresh sign-in (#1525) — or refused a delegated credential that cannot give one */
|
|
124
124
|
readonly REAUTH_PROMPT: "reauth-prompt";
|
|
125
|
-
/** Step-up: the
|
|
125
|
+
/** Step-up: the user re-authenticated, with the factor used */
|
|
126
126
|
readonly REAUTH_SUCCESS: "reauth-success";
|
|
127
127
|
/** Page search; not recorded, read volume */
|
|
128
128
|
readonly SEARCH_PAGE: "search-page";
|
|
129
|
-
/**
|
|
129
|
+
/** Users searched for; enumerating users is disclosive in a way searching pages is not. Off by default as read volume */
|
|
130
130
|
readonly SEARCH_USER: "search-user";
|
|
131
131
|
/** A masked configuration value shown to an administrator; the key is recorded, never the value */
|
|
132
132
|
readonly SECRET_REVEAL: "secret-reveal";
|
|
@@ -146,9 +146,9 @@ export declare const AUDIT_EVENT: {
|
|
|
146
146
|
readonly SHARE_REVOKE: "share-revoke";
|
|
147
147
|
/** Sign-in approval: the browser that was waiting gave up the sign-in (#1532) */
|
|
148
148
|
readonly SIGNINAPPROVAL_ABANDON: "signinapproval-abandon";
|
|
149
|
-
/** Sign-in approval: the
|
|
149
|
+
/** Sign-in approval: the user pressed Approve (#1532) */
|
|
150
150
|
readonly SIGNINAPPROVAL_APPROVE: "signinapproval-approve";
|
|
151
|
-
/** Sign-in approval: the
|
|
151
|
+
/** Sign-in approval: the user pressed Deny; that sign-in will not complete (#1532) */
|
|
152
152
|
readonly SIGNINAPPROVAL_DENY: "signinapproval-deny";
|
|
153
153
|
/** Sign-in approval: a waiting sign-in expired before it was completed (#1532) */
|
|
154
154
|
readonly SIGNINAPPROVAL_EXPIRE: "signinapproval-expire";
|
|
@@ -57,9 +57,9 @@ export const AUDIT_EVENT = {
|
|
|
57
57
|
CONFIG_CHANGE: 'config-change',
|
|
58
58
|
/** Every custom configuration value discarded; recorded before the reset, which cannot proceed without it */
|
|
59
59
|
CONFIG_RESET: 'config-reset',
|
|
60
|
-
/** A
|
|
60
|
+
/** A user accepted a document they are asked to accept (#1603): which document (core's terms-of-service or privacy-policy, or an add-on's), its version and effective date, and how (sign-up, sign-in, turning an add-on on). Recorded before the acceptance is stored, which cannot proceed without it: consent must be provable */
|
|
61
61
|
CONSENT_GRANT: 'consent-grant',
|
|
62
|
-
/** A
|
|
62
|
+
/** A user's acceptances ended (#1603): today only when their account is deleted, which removes their consent records; names the documents and versions they had accepted. Recorded before the records are removed */
|
|
63
63
|
CONSENT_WITHDRAW: 'consent-withdraw',
|
|
64
64
|
/** Contact created, changed, deleted or linked to an account; metadata names the kind (person or organization), the operation and the fields changed, never their values */
|
|
65
65
|
CONTACT_MANAGE: 'contact-manage',
|
|
@@ -119,21 +119,21 @@ export const AUDIT_EVENT = {
|
|
|
119
119
|
POLICY_EVALUATE: 'policy-evaluate',
|
|
120
120
|
/** Security posture at startup, compared against the previous start */
|
|
121
121
|
POSTURE_RECORDED: 'posture-recorded',
|
|
122
|
-
/** Proxy grant made: someone may act for the
|
|
122
|
+
/** Proxy grant made: someone may act for the user named in user; actor is who made it (#1762) */
|
|
123
123
|
PROXY_GRANT: 'proxy-grant',
|
|
124
|
-
/** Proxy grant ended: revoked, or the account closed; user is the
|
|
124
|
+
/** Proxy grant ended: revoked, or the account closed; user is the user represented, actor who ended it (#1762) */
|
|
125
125
|
PROXY_REVOKE: 'proxy-revoke',
|
|
126
|
-
/** A signed-in session started or stopped acting for someone under a proxy grant, or opens their private store: chosen, switched-back, ended because the grant no longer allows it (grant-ended, proxy-access-off), or vault-opened; user is the
|
|
126
|
+
/** A signed-in session started or stopped acting for someone under a proxy grant, or opens their private store: chosen, switched-back, ended because the grant no longer allows it (grant-ended, proxy-access-off), or vault-opened; user is the user represented, actor the user signed in (#1762) */
|
|
127
127
|
PROXY_SWITCH: 'proxy-switch',
|
|
128
|
-
/** Step-up: a re-authentication failed (wrong password, another
|
|
128
|
+
/** Step-up: a re-authentication failed (wrong password, another user's passkey, throttled) */
|
|
129
129
|
REAUTH_FAILURE: 'reauth-failure',
|
|
130
130
|
/** Step-up: an action asked for a fresh sign-in (#1525) — or refused a delegated credential that cannot give one */
|
|
131
131
|
REAUTH_PROMPT: 'reauth-prompt',
|
|
132
|
-
/** Step-up: the
|
|
132
|
+
/** Step-up: the user re-authenticated, with the factor used */
|
|
133
133
|
REAUTH_SUCCESS: 'reauth-success',
|
|
134
134
|
/** Page search; not recorded, read volume */
|
|
135
135
|
SEARCH_PAGE: 'search-page',
|
|
136
|
-
/**
|
|
136
|
+
/** Users searched for; enumerating users is disclosive in a way searching pages is not. Off by default as read volume */
|
|
137
137
|
SEARCH_USER: 'search-user',
|
|
138
138
|
/** A masked configuration value shown to an administrator; the key is recorded, never the value */
|
|
139
139
|
SECRET_REVEAL: 'secret-reveal',
|
|
@@ -153,9 +153,9 @@ export const AUDIT_EVENT = {
|
|
|
153
153
|
SHARE_REVOKE: 'share-revoke',
|
|
154
154
|
/** Sign-in approval: the browser that was waiting gave up the sign-in (#1532) */
|
|
155
155
|
SIGNINAPPROVAL_ABANDON: 'signinapproval-abandon',
|
|
156
|
-
/** Sign-in approval: the
|
|
156
|
+
/** Sign-in approval: the user pressed Approve (#1532) */
|
|
157
157
|
SIGNINAPPROVAL_APPROVE: 'signinapproval-approve',
|
|
158
|
-
/** Sign-in approval: the
|
|
158
|
+
/** Sign-in approval: the user pressed Deny; that sign-in will not complete (#1532) */
|
|
159
159
|
SIGNINAPPROVAL_DENY: 'signinapproval-deny',
|
|
160
160
|
/** Sign-in approval: a waiting sign-in expired before it was completed (#1532) */
|
|
161
161
|
SIGNINAPPROVAL_EXPIRE: 'signinapproval-expire',
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
* A route turns it into the answer with `ApiContext.enforceDeny(err.permission,
|
|
13
13
|
* err.decision)`, the one place a refusal is shaped (#1431); the door never
|
|
14
14
|
* builds an HTTP answer itself.
|
|
15
|
+
*
|
|
16
|
+
* Every lookup takes a context too (#2018, operator 2026-10-23), through
|
|
17
|
+
* {@link requireRecordAccess}: a user's own identity record under
|
|
18
|
+
* `profile-manage`, anyone else's under `contact-read` / `contact-manage`, and
|
|
19
|
+
* core's own reads under the system context without a policy evaluation, so
|
|
20
|
+
* resolving roles on every request stays cheap.
|
|
15
21
|
*/
|
|
16
22
|
import { type ActorContext } from '../context/ActorContext.js';
|
|
17
23
|
import type { CorePermission } from '../security/permissions.generated.js';
|
|
@@ -37,6 +43,30 @@ export declare class ContactRefusedError extends Error {
|
|
|
37
43
|
* refuses: closed, never open.
|
|
38
44
|
*/
|
|
39
45
|
export declare function requireContactPermission(engine: EngineLike, ctx: ActorContext, kind: keyof typeof CONTACT_PERMISSION, also?: readonly CorePermission[]): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Core's own work (#2018): a boot or schedule context naming the system
|
|
48
|
+
* principal, with no token, share or proxy claim. Recognised by its fields
|
|
49
|
+
* alone, with no policy evaluation, because RoleManager resolves roles
|
|
50
|
+
* through these lookups on every request. A request never carries this
|
|
51
|
+
* context: the system principal's name is reserved, so no account holds it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isSystemWork(engine: EngineLike, ctx: ActorContext | null | undefined): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* A lookup or change of one record at a contacts door (#2018). Throws
|
|
56
|
+
* {@link ContactRefusedError} unless `ctx` may:
|
|
57
|
+
*
|
|
58
|
+
* - read it as core's own work ({@link isSystemWork}): allowed, not evaluated;
|
|
59
|
+
* - read or change its own identity record (the context's username is the
|
|
60
|
+
* record's `identifier`): `profile-manage`, which every user holds;
|
|
61
|
+
* - read or change any other record: `contact-read` / `contact-manage`.
|
|
62
|
+
*
|
|
63
|
+
* `record` is the identity record (or what names it, `{ identifier }`); left
|
|
64
|
+
* out for a record that is never anyone's own (an organization, a role).
|
|
65
|
+
* "Own" is decided by comparing usernames, never by a role.
|
|
66
|
+
*/
|
|
67
|
+
export declare function requireRecordAccess(engine: EngineLike, ctx: ActorContext, kind: keyof typeof CONTACT_PERMISSION, record?: {
|
|
68
|
+
identifier?: unknown;
|
|
69
|
+
} | null): Promise<void>;
|
|
40
70
|
/** What a contacts record says was done. Names fields, never their values. */
|
|
41
71
|
export interface ContactChange {
|
|
42
72
|
kind: 'person' | 'organization';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KeyedSerial — one task at a time per key, within this process (#2015).
|
|
3
|
+
*
|
|
4
|
+
* A read-modify-write of one record (read the file, change it, write it back)
|
|
5
|
+
* is only safe while nothing else writes that record in between. Two role
|
|
6
|
+
* changes to one role used to interleave: each read the member list before the
|
|
7
|
+
* other wrote it, and the second write dropped the first one's member. Running
|
|
8
|
+
* every read-modify-write of a key through `run` makes them happen one after
|
|
9
|
+
* another, each reading what the last one wrote. Different keys still run side
|
|
10
|
+
* by side.
|
|
11
|
+
*
|
|
12
|
+
* What it does not do:
|
|
13
|
+
*
|
|
14
|
+
* - **Not across processes.** Two server processes sharing one data folder can
|
|
15
|
+
* still race; no supported deployment runs that way.
|
|
16
|
+
* - **Not re-entrant.** A task must not `run` the same key again and wait for
|
|
17
|
+
* it: the inner task queues behind the outer one, which never finishes.
|
|
18
|
+
*
|
|
19
|
+
* A failed task fails its own caller only; the next task on the key runs as
|
|
20
|
+
* usual. A key's entry is dropped once its queue is empty, so the map holds
|
|
21
|
+
* only keys with work in flight.
|
|
22
|
+
*/
|
|
23
|
+
export declare class KeyedSerial {
|
|
24
|
+
/** The settled tail of each key's queue: never rejects. */
|
|
25
|
+
private readonly tails;
|
|
26
|
+
/** Run `task` after every earlier task on `key` has settled; resolves or rejects as `task` does. */
|
|
27
|
+
run<T>(key: string, task: () => Promise<T>): Promise<T>;
|
|
28
|
+
/** Resolves once every task queued so far, on every key, has settled (shutdown). */
|
|
29
|
+
settled(): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export default KeyedSerial;
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
* slice 4).
|
|
26
26
|
*/
|
|
27
27
|
import { isJobContext } from '../context/ActorContext.js';
|
|
28
|
-
import { toPermissionSubject } from '../context/JobContext.js';
|
|
29
28
|
import { shareNamesVault } from '../types/Share.js';
|
|
30
29
|
import { mixesProxyWithDelegation, proxyCarries } from './proxyClaim.js';
|
|
31
30
|
export function mayActInPrivateContainer(ctx, owner, opts = {}) {
|
|
@@ -60,6 +59,5 @@ export function mayActInPrivateContainer(ctx, owner, opts = {}) {
|
|
|
60
59
|
export async function permitsInVault(pdp, ctx, action, where) {
|
|
61
60
|
if (!pdp || !mayActInPrivateContainer(ctx, where.owner, { vault: where.vault }))
|
|
62
61
|
return false;
|
|
63
|
-
|
|
64
|
-
return (await pdp.decide(subject, { action, resource: { type: 'page', id: where.resource }, attributes: { vault: where.vault } })).permit;
|
|
62
|
+
return (await pdp.decide(ctx, { action, resource: { type: 'page', id: where.resource }, attributes: { vault: where.vault } })).permit;
|
|
65
63
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A role change that did not happen (#2015).
|
|
3
|
+
*
|
|
4
|
+
* Operator, 2026-10-23: adding or removing a role completes or it does not
|
|
5
|
+
* happen, everywhere, and one that cannot be made is always an error. So a
|
|
6
|
+
* request that asks for several role changes either gets all of them or none:
|
|
7
|
+
* RoleManager undoes the ones that landed before the failure, then throws
|
|
8
|
+
* this, and the request that asked fails with it.
|
|
9
|
+
*
|
|
10
|
+
* The message is shown to whoever asked (an administrator's form). It names
|
|
11
|
+
* the account, the change that failed and, when it is something an
|
|
12
|
+
* administrator can fix, the reason (no Person record, no anchor
|
|
13
|
+
* Organization). Storage detail (paths, errno) stays in `cause`, for the log.
|
|
14
|
+
*/
|
|
15
|
+
export declare class RoleChangeError extends Error {
|
|
16
|
+
/** The change that could not be made, as `add <role>` / `remove <role>`. */
|
|
17
|
+
readonly failed: readonly string[];
|
|
18
|
+
/** Changes that landed, could not be undone, and so are still in effect. Empty when nothing was left changed. */
|
|
19
|
+
readonly notUndone: readonly string[];
|
|
20
|
+
constructor(subject: string, failed: readonly string[], options?: {
|
|
21
|
+
reason?: string;
|
|
22
|
+
cause?: unknown;
|
|
23
|
+
notUndone?: readonly string[];
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
* had somewhere to go that is not a second evaluator: a role name skips
|
|
13
13
|
* PolicyEvaluator, deny policies and the token ceiling; this does not.
|
|
14
14
|
*/
|
|
15
|
-
import type {
|
|
15
|
+
import type { ActorContext } from '../context/ActorContext.js';
|
|
16
16
|
import type { CorePermission } from '../security/permissions.generated.js';
|
|
17
17
|
type EngineLike = {
|
|
18
18
|
getManager: (name: string) => unknown;
|
|
19
19
|
} | null | undefined;
|
|
20
|
-
export declare function subjectMayDo(engine: EngineLike, subject:
|
|
20
|
+
export declare function subjectMayDo(engine: EngineLike, subject: ActorContext | null | undefined, action: CorePermission): Promise<boolean>;
|
|
21
21
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jwilleke/ngdpbase-types",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.28.0",
|
|
4
4
|
"description": "TypeScript types for ngdpbase add-ons, and a test kit of core's own values. A dev dependency, versioned with ngdpbase.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Jim Willeke",
|