@jwilleke/ngdpbase-types 5.27.0 → 5.29.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/context/ApiContext.d.ts +17 -4
- package/lib/src/context/ApiContext.js +22 -6
- package/lib/src/ice/runImport.d.ts +26 -0
- package/lib/src/ice/types.d.ts +29 -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 +57 -8
- 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 +48 -14
- package/lib/src/managers/RoleManager.d.ts +79 -19
- package/lib/src/managers/UserManager.d.ts +66 -2
- package/lib/src/security/PolicyDecisionPoint.d.ts +18 -9
- package/lib/src/security/PolicyInformationPoint.d.ts +25 -10
- package/lib/src/types/Consent.js +22 -0
- 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
|
*
|
|
@@ -40,21 +40,34 @@ export declare class ApiError extends Error {
|
|
|
40
40
|
* offer the way forward, as core routes do (`{ error, reauth }`).
|
|
41
41
|
*/
|
|
42
42
|
readonly reauth?: string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* For a refusal because the user has documents to accept (#2027): this
|
|
45
|
+
* site's consent prompt, returning to the page they were on. Sent with
|
|
46
|
+
* the 403 as `consent`; a page is redirected there.
|
|
47
|
+
*/
|
|
48
|
+
readonly consent?: string | undefined;
|
|
43
49
|
constructor(status: number, message: string,
|
|
44
50
|
/**
|
|
45
51
|
* For a step-up refusal (#1745): where the person re-authenticates,
|
|
46
52
|
* returning to the page they were on. Send it with the 403 so the page can
|
|
47
53
|
* offer the way forward, as core routes do (`{ error, reauth }`).
|
|
48
54
|
*/
|
|
49
|
-
reauth?: string | undefined
|
|
55
|
+
reauth?: string | undefined,
|
|
56
|
+
/**
|
|
57
|
+
* For a refusal because the user has documents to accept (#2027): this
|
|
58
|
+
* site's consent prompt, returning to the page they were on. Sent with
|
|
59
|
+
* the 403 as `consent`; a page is redirected there.
|
|
60
|
+
*/
|
|
61
|
+
consent?: string | undefined);
|
|
50
62
|
}
|
|
51
63
|
/**
|
|
52
64
|
* Send a refusal — the one way it reaches the client, from core routes and
|
|
53
65
|
* every add-on route alike (#1749). JSON is `{ success: false, error, reauth? }`;
|
|
54
66
|
* text is the message, then where to sign in again for a step-up refusal.
|
|
55
|
-
* `page` is for a link the
|
|
56
|
-
* redirects to the re-authenticate page, a
|
|
57
|
-
*
|
|
67
|
+
* `page` is for a link the user followed or a form they sent in the browser:
|
|
68
|
+
* a step-up refusal redirects to the re-authenticate page, a consent refusal
|
|
69
|
+
* to the consent prompt (#2027), a 401 to sign-in (returning here), anything
|
|
70
|
+
* else is the message as text. JSON carries the consent link as `consent`. A route never writes its own refusal:
|
|
58
71
|
* the step-up `reauth` is easy to drop.
|
|
59
72
|
*/
|
|
60
73
|
export declare function sendApiError(res: Response, err: ApiError, mode?: ReauthMode): void;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import '../types/express.js';
|
|
5
5
|
import { BaseContext } from './BaseContext.js';
|
|
6
6
|
import { STEP_UP_REASON } from '../types/Policy.js';
|
|
7
|
+
import { CONSENT_PATH } from '../types/Consent.js';
|
|
7
8
|
// ── ApiError ────────────────────────────────────────────────────────────────
|
|
8
9
|
/**
|
|
9
10
|
* Thrown by ApiContext guard methods (requirePermission, actingUsername).
|
|
@@ -12,16 +13,24 @@ import { STEP_UP_REASON } from '../types/Policy.js';
|
|
|
12
13
|
export class ApiError extends Error {
|
|
13
14
|
status;
|
|
14
15
|
reauth;
|
|
16
|
+
consent;
|
|
15
17
|
constructor(status, message,
|
|
16
18
|
/**
|
|
17
19
|
* For a step-up refusal (#1745): where the person re-authenticates,
|
|
18
20
|
* returning to the page they were on. Send it with the 403 so the page can
|
|
19
21
|
* offer the way forward, as core routes do (`{ error, reauth }`).
|
|
20
22
|
*/
|
|
21
|
-
reauth
|
|
23
|
+
reauth,
|
|
24
|
+
/**
|
|
25
|
+
* For a refusal because the user has documents to accept (#2027): this
|
|
26
|
+
* site's consent prompt, returning to the page they were on. Sent with
|
|
27
|
+
* the 403 as `consent`; a page is redirected there.
|
|
28
|
+
*/
|
|
29
|
+
consent) {
|
|
22
30
|
super(message);
|
|
23
31
|
this.status = status;
|
|
24
32
|
this.reauth = reauth;
|
|
33
|
+
this.consent = consent;
|
|
25
34
|
this.name = 'ApiError';
|
|
26
35
|
}
|
|
27
36
|
}
|
|
@@ -29,19 +38,26 @@ export class ApiError extends Error {
|
|
|
29
38
|
* Send a refusal — the one way it reaches the client, from core routes and
|
|
30
39
|
* every add-on route alike (#1749). JSON is `{ success: false, error, reauth? }`;
|
|
31
40
|
* text is the message, then where to sign in again for a step-up refusal.
|
|
32
|
-
* `page` is for a link the
|
|
33
|
-
* redirects to the re-authenticate page, a
|
|
34
|
-
*
|
|
41
|
+
* `page` is for a link the user followed or a form they sent in the browser:
|
|
42
|
+
* a step-up refusal redirects to the re-authenticate page, a consent refusal
|
|
43
|
+
* to the consent prompt (#2027), a 401 to sign-in (returning here), anything
|
|
44
|
+
* else is the message as text. JSON carries the consent link as `consent`. A route never writes its own refusal:
|
|
35
45
|
* the step-up `reauth` is easy to drop.
|
|
36
46
|
*/
|
|
37
47
|
export function sendApiError(res, err, mode = 'json') {
|
|
38
48
|
// Only this site's re-authenticate page: an add-on's ApiError cannot send the person elsewhere.
|
|
39
49
|
const reauth = typeof err.reauth === 'string' && err.reauth.startsWith('/auth/reauth?') ? err.reauth : undefined;
|
|
50
|
+
// Likewise only this site's consent prompt.
|
|
51
|
+
const consent = typeof err.consent === 'string' && (err.consent === CONSENT_PATH || err.consent.startsWith(`${CONSENT_PATH}?`)) ? err.consent : undefined;
|
|
40
52
|
if (mode === 'page') {
|
|
41
53
|
if (reauth) {
|
|
42
54
|
res.redirect(reauth);
|
|
43
55
|
return;
|
|
44
56
|
}
|
|
57
|
+
if (consent) {
|
|
58
|
+
res.redirect(consent);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
45
61
|
if (err.status === 401) {
|
|
46
62
|
res.redirect('/login?redirect=' + encodeURIComponent(res.req?.originalUrl || '/'));
|
|
47
63
|
return;
|
|
@@ -50,10 +66,10 @@ export function sendApiError(res, err, mode = 'json') {
|
|
|
50
66
|
return;
|
|
51
67
|
}
|
|
52
68
|
if (mode === 'text') {
|
|
53
|
-
res.status(err.status).send(reauth ? `${err.message}: ${reauth}` : err.message);
|
|
69
|
+
res.status(err.status).send(reauth ? `${err.message}: ${reauth}` : consent ? `${err.message} Read and accept them at ${consent}` : err.message);
|
|
54
70
|
return;
|
|
55
71
|
}
|
|
56
|
-
res.status(err.status).json({ success: false, error: err.message, ...(reauth ? { reauth } : {}) });
|
|
72
|
+
res.status(err.status).json({ success: false, error: err.message, ...(reauth ? { reauth } : {}), ...(consent ? { consent } : {}) });
|
|
57
73
|
}
|
|
58
74
|
/**
|
|
59
75
|
* The subject a request carries, refused when there is none (#1399).
|
|
@@ -43,6 +43,32 @@ export interface ChainSpec {
|
|
|
43
43
|
* warnings first. Partial notes are collected into `partial`.
|
|
44
44
|
*/
|
|
45
45
|
export declare function convertChain(shim: RegisteredShim, input: IceItem, chain: string[], spec: ChainSpec, partial: string[]): Promise<ChainLeaf[]>;
|
|
46
|
+
/** Does this run keep its originals during the upload (#2034)? */
|
|
47
|
+
export declare function keepsAtUpload(receiver: RegisteredReceiver | undefined, original: OriginalChoice): boolean;
|
|
48
|
+
/** What keeping at upload did: how many it kept, or the first file it could not keep. */
|
|
49
|
+
export interface UploadKeepResult {
|
|
50
|
+
kept: number;
|
|
51
|
+
refused?: {
|
|
52
|
+
name: string;
|
|
53
|
+
message: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Keep the originals of a run whose receiver keeps into an encrypted store,
|
|
58
|
+
* now, during the upload (#2034). `ctx` is the REQUEST's context: it holds
|
|
59
|
+
* the user's unlocked key, so the receiver's `keepOriginal` seals the bytes
|
|
60
|
+
* and the store's sealed index entry while the key is live. Each kept
|
|
61
|
+
* original is recorded in the ledger by fingerprint and door (#2011), so the
|
|
62
|
+
* queued run reuses it and never keeps again.
|
|
63
|
+
*
|
|
64
|
+
* The files are decided exactly as the run decides them ({@link planFile}):
|
|
65
|
+
* noise, a file too large, one nothing reads and one already imported are
|
|
66
|
+
* not kept. An original already kept through this door is not kept again.
|
|
67
|
+
* Stops at the first refusal (or failure) and says which file: the caller
|
|
68
|
+
* then queues nothing. Originals kept before it stay kept, and recorded, so
|
|
69
|
+
* the next try reuses them.
|
|
70
|
+
*/
|
|
71
|
+
export declare function keepOriginalsAtUpload(deps: IceRunDeps, request: IceRunRequest, ctx: ActorContext, scope: string): Promise<UploadKeepResult>;
|
|
46
72
|
/**
|
|
47
73
|
* Run one import. Fills and returns `report`, which the caller may be
|
|
48
74
|
* showing while it runs. Never throws for one file's failure; that file's
|
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,15 @@ 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
|
+
/**
|
|
186
|
+
* `keepOriginal` writes into an encrypted store (phr's vault), which only a
|
|
187
|
+
* request holding the user's unlocked key can write (#2034). The engine then
|
|
188
|
+
* keeps each original during the upload, with the request's own context,
|
|
189
|
+
* before the run is queued; the run reuses what was kept and needs no key.
|
|
190
|
+
*/
|
|
191
|
+
keepsEncrypted?: boolean;
|
|
192
|
+
/** The original-file choices this receiver offers (D11). Without it: all three. */
|
|
193
|
+
originals?: IceOriginalChoices;
|
|
175
194
|
}
|
|
176
195
|
export interface RegisteredReceiver extends IceReceiver {
|
|
177
196
|
id: string;
|
|
@@ -238,6 +257,14 @@ export interface IceRunReport {
|
|
|
238
257
|
error?: string;
|
|
239
258
|
files: IceFileReport[];
|
|
240
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* A kept original as the ledger remembers it, with the door that kept it
|
|
262
|
+
* (a receiver's own `keepOriginal`, or `core.files`). A later run of the same
|
|
263
|
+
* fingerprint through the same door reuses it instead of keeping another copy.
|
|
264
|
+
*/
|
|
265
|
+
export interface LedgerOriginal extends KeptOriginal {
|
|
266
|
+
door: string;
|
|
267
|
+
}
|
|
241
268
|
/** What the engine remembers about one file in a scope. */
|
|
242
269
|
export interface LedgerFileEntry {
|
|
243
270
|
name: string;
|
|
@@ -247,6 +274,8 @@ export interface LedgerFileEntry {
|
|
|
247
274
|
message?: string;
|
|
248
275
|
at: string;
|
|
249
276
|
refs: string[];
|
|
277
|
+
/** The original kept for this fingerprint, whatever became of the conversion (D11). */
|
|
278
|
+
original?: LedgerOriginal;
|
|
250
279
|
}
|
|
251
280
|
/**
|
|
252
281
|
* 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}`);
|
|
@@ -32,7 +32,7 @@ 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
34
|
import { type JobContext } from '../context/JobContext.js';
|
|
35
|
-
import type { IceLedger, IceReceiver, IceRunReport, IceRunRequest, IceShim, RegisteredShim } from '../ice/types.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
|
*/
|
|
@@ -207,6 +207,16 @@ export declare class TakeoutImportRefused extends Error {
|
|
|
207
207
|
readonly reason: 'not-owner' | 'no-such-store' | 'locked' | 'unreadable';
|
|
208
208
|
constructor(reason: 'not-owner' | 'no-such-store' | 'locked' | 'unreadable', message: string);
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* An import refused at upload, before anything was queued (#2034): keeping
|
|
212
|
+
* the original of `file` in the receiver's encrypted store was refused or
|
|
213
|
+
* failed. `message` is written for the user who uploaded.
|
|
214
|
+
*/
|
|
215
|
+
export declare class ImportUploadRefused extends Error {
|
|
216
|
+
readonly file: string;
|
|
217
|
+
readonly reason: string;
|
|
218
|
+
constructor(file: string, reason: string);
|
|
219
|
+
}
|
|
210
220
|
/** One page's outcome in a takeout import (#1472). */
|
|
211
221
|
export type TakeoutPageOutcome = {
|
|
212
222
|
title: string;
|
|
@@ -253,6 +263,12 @@ export type TakeoutImportReport = {
|
|
|
253
263
|
*/
|
|
254
264
|
reportPage?: string;
|
|
255
265
|
};
|
|
266
|
+
/** A receiver the import screen may offer this user (#2021): its id, label and original-file choices (D11). */
|
|
267
|
+
export interface UsableReceiver {
|
|
268
|
+
id: string;
|
|
269
|
+
label?: string;
|
|
270
|
+
originals: Required<IceOriginalChoices>;
|
|
271
|
+
}
|
|
256
272
|
/**
|
|
257
273
|
* Import Manager class
|
|
258
274
|
*
|
|
@@ -310,11 +326,24 @@ declare class ImportManager extends BaseManager {
|
|
|
310
326
|
listShims(): Array<Pick<RegisteredShim, 'id' | 'owner' | 'label' | 'direction' | 'produces' | 'receivers'> & {
|
|
311
327
|
enabled: boolean;
|
|
312
328
|
}>;
|
|
313
|
-
/**
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
329
|
+
/**
|
|
330
|
+
* The receivers this context may use (D5): each answers with its own
|
|
331
|
+
* existing permission. Each comes with the original-file choices it offers
|
|
332
|
+
* (D11), for the import screen (#2021).
|
|
333
|
+
*/
|
|
334
|
+
receiversFor(ctx: ActorContext): Promise<UsableReceiver[]>;
|
|
335
|
+
/**
|
|
336
|
+
* The original-file choices a receiver offers (D11, #2021): what it
|
|
337
|
+
* declares, else all three; its default when it names one it allows, else
|
|
338
|
+
* `ngdpbase.import.engine.default-original` when allowed, else the first.
|
|
339
|
+
*/
|
|
340
|
+
private originalChoices;
|
|
341
|
+
/**
|
|
342
|
+
* The request as the engine runs it: the receiver's default original when
|
|
343
|
+
* none was chosen, and a refusal for a choice the receiver does not offer
|
|
344
|
+
* (D11, #2021). An unknown receiver is left for the run to report.
|
|
345
|
+
*/
|
|
346
|
+
private withOriginal;
|
|
318
347
|
/**
|
|
319
348
|
* Run one import now and wait for it. For code already off the request
|
|
320
349
|
* path: an add-on's own background job, a test. A route uses
|
|
@@ -325,17 +354,37 @@ declare class ImportManager extends BaseManager {
|
|
|
325
354
|
runImport(request: IceRunRequest, ctx: ActorContext): Promise<IceRunReport>;
|
|
326
355
|
/**
|
|
327
356
|
* Queue an import as background work and return its run id at once. The
|
|
328
|
-
* run carries `ctx` — the
|
|
357
|
+
* run carries `ctx` — the user who asked (`jobContextFromRequest`), or a
|
|
329
358
|
* job's context with its reason — and each receiver checks its own
|
|
330
|
-
* permission with it when the run reaches it (D5).
|
|
359
|
+
* permission with it when the run reaches it (D5). A user's upload, from
|
|
360
|
+
* a request, goes through {@link enqueueUpload} instead.
|
|
331
361
|
*/
|
|
332
362
|
enqueueImport(request: IceRunRequest, ctx: JobContext): Promise<string>;
|
|
363
|
+
/**
|
|
364
|
+
* Queue a user's upload, from the request it arrived in (#2034). `ctx` is
|
|
365
|
+
* the REQUEST's context; the run carries the job context derived from it
|
|
366
|
+
* (`jobContextFromRequest`), as {@link enqueueImport} does.
|
|
367
|
+
*
|
|
368
|
+
* When the run keeps originals through a receiver that keeps into an
|
|
369
|
+
* encrypted store (`keepsEncrypted`), they are kept first, here, with
|
|
370
|
+
* `ctx`, while the user's key is in the request: the bytes are sealed and
|
|
371
|
+
* the store's sealed index entry written. Each is recorded in the ledger
|
|
372
|
+
* as the keep-once record (#2011), so the run reuses it and needs no key.
|
|
373
|
+
*
|
|
374
|
+
* @throws ImportUploadRefused when an original could not be kept; nothing is queued
|
|
375
|
+
*/
|
|
376
|
+
enqueueUpload(request: IceRunRequest, ctx: ActorContext): Promise<string>;
|
|
377
|
+
private enqueueResolved;
|
|
333
378
|
/** A run's report, to the person it runs as; null to anyone else. */
|
|
334
379
|
getImportRun(runId: string, ctx: ActorContext): IceRunReport | null;
|
|
380
|
+
/** The runs this process holds for the user `ctx` names, newest first (#2021): their own, never anyone else's. */
|
|
381
|
+
importRunsFor(ctx: ActorContext): IceRunReport[];
|
|
335
382
|
/** Use this ledger instead of the file one: a test's, or a database table's. */
|
|
336
383
|
setImportLedger(ledger: IceLedger): void;
|
|
337
384
|
private newRunReport;
|
|
338
385
|
private executeRun;
|
|
386
|
+
/** What a run, or keeping at upload, works with. */
|
|
387
|
+
private iceDeps;
|
|
339
388
|
private enabledShims;
|
|
340
389
|
/**
|
|
341
390
|
* Hand the queue to the background job. One job run drains every queued
|
|
@@ -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)`
|
|
36
|
-
*
|
|
40
|
+
* `getByUserIdentifier(ctx, username)` returns a user's identity record and
|
|
41
|
+
* makes it when the user has none (#2037): every user has exactly one.
|
|
37
42
|
*/
|
|
38
43
|
declare class PersonManager extends BaseManager {
|
|
39
44
|
readonly description = "Canonical Person records (#617)";
|
|
@@ -46,6 +51,8 @@ declare class PersonManager extends BaseManager {
|
|
|
46
51
|
private metricsRef;
|
|
47
52
|
/** Sign-up link offers by token (#1930). In memory: a restart drops them, and an administrator links by hand. */
|
|
48
53
|
private linkOffers;
|
|
54
|
+
/** #2037: making a user's missing identity record, one at a time per username. */
|
|
55
|
+
private readonly accountRecords;
|
|
49
56
|
constructor(engine: WikiEngine);
|
|
50
57
|
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
51
58
|
/** `ngdpbase.application.persons.storagedir`, resolved: where the records are. */
|
|
@@ -56,15 +63,26 @@ declare class PersonManager extends BaseManager {
|
|
|
56
63
|
*/
|
|
57
64
|
backupFolders(): BackupFolder[];
|
|
58
65
|
getProvider(): PersonProvider | null;
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
/**
|
|
67
|
+
* One identity record by `@id` (#2018): the caller's own under
|
|
68
|
+
* `profile-manage`, anyone else's under `contact-read`, core's own work
|
|
69
|
+
* under the system context. A refusal throws `ContactRefusedError`.
|
|
70
|
+
*/
|
|
71
|
+
getById(ctx: ActorContext, id: string): Promise<Person | null>;
|
|
72
|
+
/**
|
|
73
|
+
* The identity record whose `identifier` is `identifier` (a username, or a
|
|
74
|
+
* contact's `person:<uuid>`), decided as {@link getById} before it is read.
|
|
75
|
+
*/
|
|
76
|
+
getByIdentifier(ctx: ActorContext, identifier: string): Promise<Person | null>;
|
|
77
|
+
private cachedById;
|
|
78
|
+
private cachedByIdentifier;
|
|
61
79
|
/** #1006: how many people this instance holds. Count only, never records. */
|
|
62
80
|
getManagerStats(): Promise<ManagerStats>;
|
|
63
81
|
/** True when the person has no account: its identifier is `person:<uuid>` (#1930). */
|
|
64
82
|
static isAccountless(person: Pick<Person, 'identifier'>): boolean;
|
|
65
83
|
/** Every person, for a caller who may read contacts (`contact-read`). */
|
|
66
84
|
list(ctx: ActorContext): Promise<Person[]>;
|
|
67
|
-
/** One person by `@id
|
|
85
|
+
/** One person by `@id`: {@link getById}, the caller's own under `profile-manage`, anyone else's under `contact-read`. */
|
|
68
86
|
get(ctx: ActorContext, id: string): Promise<Person | null>;
|
|
69
87
|
/**
|
|
70
88
|
* Persons whose name (`name`, `givenName`, `familyName`) or `email` contains
|
|
@@ -82,7 +100,11 @@ declare class PersonManager extends BaseManager {
|
|
|
82
100
|
* person yet: that is how UserManager makes the person for a new account.
|
|
83
101
|
*/
|
|
84
102
|
create(ctx: ActorContext, input: PersonInput): Promise<Person>;
|
|
85
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Change a person's fields, affiliations included. `@id` and `identifier`
|
|
105
|
+
* never change here. The caller's own identity record under
|
|
106
|
+
* `profile-manage`, anyone else's under `contact-manage` (#2018).
|
|
107
|
+
*/
|
|
86
108
|
update(ctx: ActorContext, id: string, patch: PersonUpdate): Promise<Person | null>;
|
|
87
109
|
delete(ctx: ActorContext, id: string): Promise<boolean>;
|
|
88
110
|
/**
|
|
@@ -123,7 +145,10 @@ declare class PersonManager extends BaseManager {
|
|
|
123
145
|
* to it, and that person is removed. Never a second person.
|
|
124
146
|
*/
|
|
125
147
|
private join;
|
|
126
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* Each affiliation names an organization that exists, looked up with the
|
|
150
|
+
* caller's context; `role` is trimmed text. Throws on any other shape.
|
|
151
|
+
*/
|
|
127
152
|
private checkedAffiliations;
|
|
128
153
|
private configNumber;
|
|
129
154
|
/**
|
|
@@ -132,13 +157,22 @@ declare class PersonManager extends BaseManager {
|
|
|
132
157
|
*/
|
|
133
158
|
invalidateCache(): void;
|
|
134
159
|
/**
|
|
135
|
-
*
|
|
160
|
+
* The identity record of the user `username`, made when the user has none
|
|
161
|
+
* (#2037). Every user has exactly one identity record; one made outside
|
|
162
|
+
* `UserManager.createUser` (an account written straight into the user
|
|
163
|
+
* store) gets it here, from the account's display name and email.
|
|
136
164
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
165
|
+
* 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
|
|
167
|
+
* work under the system context. Making the missing record is core's
|
|
168
|
+
* bookkeeping, not the caller's contact management, so it goes through
|
|
169
|
+
* {@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
|
|
171
|
+
* never make two records. Null when there is no account by that name.
|
|
172
|
+
* Never a second record: a contact with the same email is left alone
|
|
173
|
+
* (an administrator links it, {@link linkAccount}).
|
|
140
174
|
*/
|
|
141
|
-
getByUserIdentifier(username: string): Promise<Person | null>;
|
|
175
|
+
getByUserIdentifier(ctx: ActorContext, username: string): Promise<Person | null>;
|
|
142
176
|
private requireProvider;
|
|
143
177
|
private getMetrics;
|
|
144
178
|
private normalizeProviderName;
|