@jwilleke/ngdpbase-types 5.29.1 → 5.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/packages/ngdpbase-types/index.d.ts +8 -3
- package/lib/src/context/JobContext.d.ts +20 -1
- package/lib/src/context/JobContext.js +18 -2
- package/lib/src/context/bootActions.js +5 -1
- package/lib/src/ice/fileHandling.d.ts +24 -2
- package/lib/src/ice/originalKeep.d.ts +40 -0
- package/lib/src/ice/types.d.ts +33 -1
- package/lib/src/managers/AttachmentManager.d.ts +12 -8
- package/lib/src/managers/AuthManager.d.ts +111 -0
- package/lib/src/managers/BackgroundJobManager.d.ts +8 -0
- package/lib/src/managers/BackupManager.d.ts +6 -0
- package/lib/src/managers/BackupManager.js +38 -11
- package/lib/src/managers/{PersonManager.d.ts → IdentityManager.d.ts} +20 -16
- package/lib/src/managers/ImportManager.d.ts +2 -5
- package/lib/src/managers/OrganizationManager.d.ts +2 -2
- package/lib/src/managers/PolicyEvaluator.d.ts +7 -0
- package/lib/src/managers/RoleManager.d.ts +7 -7
- package/lib/src/managers/UserManager.d.ts +17 -5
- package/lib/src/providers/BaseAttachmentProvider.d.ts +2 -8
- package/lib/src/providers/BaseCredentialsProvider.d.ts +7 -1
- package/lib/src/providers/BasicAttachmentProvider.d.ts +3 -8
- package/lib/src/security/PolicyDecisionPoint.d.ts +12 -0
- package/lib/src/security/PolicyInformationPoint.d.ts +1 -1
- package/lib/src/security/permissions.generated.d.ts +4 -4
- package/lib/src/types/Config.d.ts +1 -1
- package/lib/src/types/Person.d.ts +1 -1
- package/lib/src/types/PersonProvider.d.ts +2 -2
- package/lib/src/types/Policy.d.ts +2 -2
- package/lib/src/types/Provider.d.ts +21 -0
- package/lib/src/utils/addonConfigLayer.d.ts +16 -0
- package/lib/src/utils/addonConfigLayer.js +40 -3
- package/lib/src/utils/addonHost.d.ts +21 -3
- package/lib/src/utils/addonHost.js +10 -4
- package/lib/src/utils/auditEventNames.generated.d.ts +10 -2
- package/lib/src/utils/auditEventNames.generated.js +10 -2
- package/lib/src/utils/auditEvents.d.ts +16 -3
- package/lib/src/utils/auditEvents.js +10 -11
- package/lib/src/utils/availability.d.ts +120 -0
- package/lib/src/utils/availability.js +174 -0
- package/lib/src/utils/contactAccess.d.ts +9 -9
- package/lib/src/utils/keyedSerial.js +41 -0
- package/lib/src/utils/privateStoreUnlock.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ import type { AddonModule, AddonHost, WikiEngine } from '@jwilleke/ngdpbase-type
|
|
|
11
11
|
import { buildAddonHost, openAddonTestDatabase, toCsv, formatters } from '@jwilleke/ngdpbase-types/testing';
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
- The main entry is types only. Importing it at run time fails, on purpose. An add-on takes core's values from `host` (`AddonHost`: the request and audit helpers, the logger, `guardedFetch`, the egress tools, `isAtLeast`) and a manager's job from `engine.getManager` (such as `PageManager.pageUrl` or `PolicyDecisionPoint.usersHolding`); it never imports core's values ([#1813](https://github.com/jwilleke/ngdpbase/issues/1813)).
|
|
14
|
+
- The main entry is types only. Importing it at run time fails, on purpose. An add-on takes core's values from `host` (`AddonHost`: the request and audit helpers, the logger, `guardedFetch`, the egress tools, `isAtLeast`, `zonedParts`) and a manager's job from `engine.getManager` (such as `PageManager.pageUrl` or `PolicyDecisionPoint.usersHolding`); it never imports core's values ([#1813](https://github.com/jwilleke/ngdpbase/issues/1813)).
|
|
15
15
|
- `/testing` is for tests. `buildAddonHost(slug, defaults, setEnabledForPerson?, engine?)` is the real host; `host.setEnabledForPerson` calls the setter you pass (it refuses without one), and `engine` names the system principal `host.systemContext(reason)` uses (core's fallback without one). `openAddonTestDatabase(owner, { migrations, ledgerTable })` is the handle `openAddonDatabase` returns, in memory. `toCsv` is `ExportManager.toCsv`. `formatters` is a plugin's `context.formatters`.
|
|
16
16
|
|
|
17
17
|
The add-on guide is [docs/guides/addons-developer-guide.md](https://github.com/jwilleke/ngdpbase/blob/master/docs/guides/addons-developer-guide.md).
|
|
@@ -23,8 +23,9 @@ export type { PermissionSubject } from '../../src/managers/UserManager.js';
|
|
|
23
23
|
export type { Decision } from '../../src/types/Policy.js';
|
|
24
24
|
export type { PluginContext, PluginParams, PluginFormatters } from '../../src/managers/PluginManager.js';
|
|
25
25
|
export type { SimplePlugin } from '../../src/plugins/types.js';
|
|
26
|
+
export type { KeyedSerial } from '../../src/utils/keyedSerial.js';
|
|
26
27
|
export type { AuditEvent } from '../../src/utils/auditEvents.js';
|
|
27
|
-
export type { JobRunContext } from '../../src/managers/BackgroundJobManager.js';
|
|
28
|
+
export type { default as BackgroundJobManager, JobDefinition, JobResult, JobRunContext } from '../../src/managers/BackgroundJobManager.js';
|
|
28
29
|
export type { default as DatabaseManager, AddonDatabaseOptions } from '../../src/managers/DatabaseManager.js';
|
|
29
30
|
export type { Migration } from '../../src/providers/sqliteMigrations.js';
|
|
30
31
|
export type { AddonDatabaseDeclaration, DatabaseEncrypt } from '../../src/utils/addonDatabaseRules.js';
|
|
@@ -36,10 +37,14 @@ export type { PageAction } from '../../src/utils/pageUrl.js';
|
|
|
36
37
|
export type { default as ConsentManager, ConsentScopeStatus } from '../../src/managers/ConsentManager.js';
|
|
37
38
|
export type { AddonConsentRegistration, ConsentDocument, ConsentRecord, ConsentRequiredError, ConsentScope, ConsentSeen, ConsentStatus } from '../../src/types/Consent.js';
|
|
38
39
|
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, IceOriginalChoices, IceUpload, IceSource, IceRunRequest, IceRunReport, IceFileReport, IceFileStatus } from '../../src/ice/types.js';
|
|
40
|
+
export type { IceShim, IceReceiver, IceItem, IceKind, IceWarning, ShimDirection, ShimAccepts, ShimTools, ShimReadResult, ReceiveInfo, ReceiveOutcome, IceCitation, KeptOriginal, OriginalChoice, IceOriginalChoices, OriginalKeep, IceUpload, IceSource, IceRunRequest, IceRunReport, IceFileReport, IceFileStatus } from '../../src/ice/types.js';
|
|
40
41
|
export type { default as UserManager, UserUpdateInput, UserUpdateFn } from '../../src/managers/UserManager.js';
|
|
41
42
|
export type { User, UserPreferences } from '../../src/types/User.js';
|
|
42
|
-
export type { default as
|
|
43
|
+
export type { default as AuthManager, InviteOutcome, InvitationView } from '../../src/managers/AuthManager.js';
|
|
44
|
+
export type { default as IdentityManager, LinkOfferOutcome } from '../../src/managers/IdentityManager.js';
|
|
45
|
+
export type {
|
|
46
|
+
/** @deprecated Renamed `IdentityManager` (#2038); this alias goes after one release. */
|
|
47
|
+
default as PersonManager } from '../../src/managers/IdentityManager.js';
|
|
43
48
|
export type { default as OrganizationManager } from '../../src/managers/OrganizationManager.js';
|
|
44
49
|
export type { Person, PersonAffiliation, PersonInput, PersonUpdate } from '../../src/types/Person.js';
|
|
45
50
|
export type { Organization, OrganizationInput, OrganizationUpdate, ContactPoint, ContactPointEntry, PostalAddress } from '../../src/types/Organization.js';
|
|
@@ -60,7 +60,10 @@ export interface JobContext {
|
|
|
60
60
|
viaToken?: AgentTokenGrant;
|
|
61
61
|
/** The share the requesting call presented, when it did (#1222). Carried for the same reason. */
|
|
62
62
|
viaShare?: ShareGrant;
|
|
63
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* The proxy grant the requesting call acted under, when it did (#1762). Carried for the same reason: a job never runs as the represented user unbounded.
|
|
65
|
+
* Never with its `keyHandle` (#2047): a job carries no key of any kind, so it reaches no key copy and none is written to a run record ({@link withoutKeyHandle}).
|
|
66
|
+
*/
|
|
64
67
|
viaProxy?: ProxyClaim;
|
|
65
68
|
/** The address the requesting call came from (#1179), when there was one. Provenance for the record. */
|
|
66
69
|
ipAddress?: string;
|
|
@@ -68,6 +71,15 @@ export interface JobContext {
|
|
|
68
71
|
requestedAt: string;
|
|
69
72
|
/** Why, for origins with no person to ask. Free text, for the audit record. */
|
|
70
73
|
reason?: string;
|
|
74
|
+
/**
|
|
75
|
+
* The scheduled job this run acts as (#1999): its id. Set by
|
|
76
|
+
* BackgroundJobManager on a scheduled slot's context, never on a run
|
|
77
|
+
* started by hand. When a policy names the job (`{ "type": "job", "value":
|
|
78
|
+
* "<id>" }`), its decisions are made as `job:<id>`, holding only what such
|
|
79
|
+
* policies grant; when none does, as the system principal, as before. Only
|
|
80
|
+
* the system principal's context can act as a job.
|
|
81
|
+
*/
|
|
82
|
+
job?: string;
|
|
71
83
|
}
|
|
72
84
|
/** The shape a request-bound context exposes; both WikiContext and ApiContext satisfy it. */
|
|
73
85
|
export interface RequestIdentity {
|
|
@@ -96,6 +108,13 @@ export declare const ANONYMOUS_USERNAME = "Anonymous";
|
|
|
96
108
|
* string, so `viaToken` comes along instead of being dropped.
|
|
97
109
|
*/
|
|
98
110
|
export declare function jobContextFromRequest(identity: RequestIdentity | null | undefined, now?: Date): JobContext;
|
|
111
|
+
/**
|
|
112
|
+
* A job context with no key handle in it (#2047). `jobContextFromRequest`
|
|
113
|
+
* already leaves the handle out; the job manager also passes every context it
|
|
114
|
+
* stores through this, so a context built by hand cannot carry a key copy's
|
|
115
|
+
* handle into a job or onto disk.
|
|
116
|
+
*/
|
|
117
|
+
export declare function withoutKeyHandle(ctx: JobContext): JobContext;
|
|
99
118
|
/**
|
|
100
119
|
* A request-origin context that also says why (#1179).
|
|
101
120
|
*
|
|
@@ -54,11 +54,25 @@ export function jobContextFromRequest(identity, now = new Date()) {
|
|
|
54
54
|
origin: 'request',
|
|
55
55
|
...(identity?.viaToken ? { viaToken: identity.viaToken } : {}),
|
|
56
56
|
...(identity?.viaShare ? { viaShare: identity.viaShare } : {}),
|
|
57
|
-
...(identity?.viaProxy ? { viaProxy: identity.viaProxy } : {}),
|
|
57
|
+
...(identity?.viaProxy ? { viaProxy: proxyClaimForJob(identity.viaProxy) } : {}),
|
|
58
58
|
...(identity?.ipAddress ? { ipAddress: identity.ipAddress } : {}),
|
|
59
59
|
requestedAt: now.toISOString()
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
/** The proxy claim without its key handle (#2047): the actor, grant and scopes stay for the ceiling and the record. */
|
|
63
|
+
function proxyClaimForJob(claim) {
|
|
64
|
+
const { keyHandle: _dropped, ...kept } = claim;
|
|
65
|
+
return kept;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A job context with no key handle in it (#2047). `jobContextFromRequest`
|
|
69
|
+
* already leaves the handle out; the job manager also passes every context it
|
|
70
|
+
* stores through this, so a context built by hand cannot carry a key copy's
|
|
71
|
+
* handle into a job or onto disk.
|
|
72
|
+
*/
|
|
73
|
+
export function withoutKeyHandle(ctx) {
|
|
74
|
+
return ctx.viaProxy?.keyHandle === undefined ? ctx : { ...ctx, viaProxy: proxyClaimForJob(ctx.viaProxy) };
|
|
75
|
+
}
|
|
62
76
|
/**
|
|
63
77
|
* A request-origin context that also says why (#1179).
|
|
64
78
|
*
|
|
@@ -133,7 +147,9 @@ export function toPermissionSubject(ctx) {
|
|
|
133
147
|
resolveRolesNow: true,
|
|
134
148
|
...(ctx.viaToken ? { viaToken: ctx.viaToken } : {}),
|
|
135
149
|
...(ctx.viaShare ? { viaShare: ctx.viaShare } : {}),
|
|
136
|
-
...(ctx.viaProxy ? { viaProxy: ctx.viaProxy } : {})
|
|
150
|
+
...(ctx.viaProxy ? { viaProxy: ctx.viaProxy } : {}),
|
|
151
|
+
// #1999: the job a scheduled run acts as, for the PIP to resolve.
|
|
152
|
+
...(ctx.job ? { job: ctx.job } : {})
|
|
137
153
|
};
|
|
138
154
|
}
|
|
139
155
|
/** One line for a log or an audit record: who, from where, and how. */
|
|
@@ -64,7 +64,11 @@ export function attributedTo(ctx) {
|
|
|
64
64
|
origin: ctx.origin,
|
|
65
65
|
reason: ctx.reason ?? null,
|
|
66
66
|
requestedAt: ctx.requestedAt,
|
|
67
|
-
|
|
67
|
+
// #1999: a scheduled run names the job it acts as on every row it records.
|
|
68
|
+
...(ctx.job ? { job: ctx.job } : {}),
|
|
69
|
+
// #2059: the delegation a request-origin job was queued under, by kind,
|
|
70
|
+
// with the same precedence as a request's own record (`actorOf`).
|
|
71
|
+
...(ctx.viaToken ? { delegated: 'token' } : ctx.viaShare ? { delegated: 'share' } : proxyMeta)
|
|
68
72
|
}
|
|
69
73
|
};
|
|
70
74
|
}
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { type ZipReadLimits } from '../utils/zipArchive.js';
|
|
2
2
|
import type { IceUpload } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A file's media type by its extension (#2061): how the engine labels a file
|
|
5
|
+
* it unpacks from a zip, and how the folder import labels a file it reads.
|
|
6
|
+
* A typed table, not configuration: it states what each format is, which is
|
|
7
|
+
* not a site's choice. What a site decides is which types it takes, and that
|
|
8
|
+
* is already its attachment allow-list (and a receiver's `originals.types`).
|
|
9
|
+
*/
|
|
10
|
+
export declare const MEDIA_TYPE_BY_EXTENSION: Readonly<Record<string, string>>;
|
|
11
|
+
/** A file's media type by its extension alone: {@link MEDIA_TYPE_BY_EXTENSION}, else `application/octet-stream`. */
|
|
12
|
+
export declare function mediaTypeByExtension(name: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* The media type of a file unpacked from a zip (#2061): its extension's,
|
|
15
|
+
* checked against its bytes where that is cheap. JSON must start with `{` or
|
|
16
|
+
* `[` (NDJSON with `{`), XML with `<`; one that does not is
|
|
17
|
+
* `application/octet-stream`. Then the upload door's own rule
|
|
18
|
+
* (`resolveUploadMime`): bytes that show a type decide (an XML file with an
|
|
19
|
+
* `<?xml` prolog is `text/xml`), and a claim they do not bear out is
|
|
20
|
+
* `application/octet-stream`. So the type a receiver is handed is the type
|
|
21
|
+
* the door stores.
|
|
22
|
+
*/
|
|
23
|
+
export declare function memberMediaType(name: string, bytes: Buffer): string;
|
|
3
24
|
/** A file's fingerprint: SHA-256 of its bytes, hex. */
|
|
4
25
|
export declare function fingerprint(bytes: Buffer): string;
|
|
5
26
|
/**
|
|
@@ -15,8 +36,9 @@ export declare function isNoise(filePath: string, patterns: readonly string[]):
|
|
|
15
36
|
export declare function isZip(upload: IceUpload): boolean;
|
|
16
37
|
/**
|
|
17
38
|
* Unpack the zips among the uploads into the files they hold, named
|
|
18
|
-
* `archive.zip/inner/path
|
|
19
|
-
*
|
|
39
|
+
* `archive.zip/inner/path`, each with its media type ({@link memberMediaType},
|
|
40
|
+
* #2061). A nested zip is unpacked too. Everything else is passed through,
|
|
41
|
+
* with the type its request gave it. Limits are per archive (`ZipReadLimits`), so a small
|
|
20
42
|
* archive that expands enormously stops at the budget. An archive that cannot
|
|
21
43
|
* be read is returned in `unreadable`, with why; the rest go on.
|
|
22
44
|
*/
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The allowance a keep at upload carries to the upload door (#2061).
|
|
3
|
+
*
|
|
4
|
+
* A receiver that keeps originals in its own encrypted vault (phr) declares
|
|
5
|
+
* the media types it keeps (`originals.types`). The site's attachment
|
|
6
|
+
* allow-list does not name them (FHIR JSON, C-CDA XML), so the door would
|
|
7
|
+
* refuse the keep. The exemption is narrow, and lives here, not in a flag:
|
|
8
|
+
*
|
|
9
|
+
* - the engine opens one {@link OriginalKeep} for one receiver, one user and
|
|
10
|
+
* one `keepOriginal` call, and closes it when the call returns;
|
|
11
|
+
* - the receiver passes it to `AttachmentManager.uploadAttachment` as its
|
|
12
|
+
* `keep` option, unchanged;
|
|
13
|
+
* - the door honours it only while it is open, and only for a file going
|
|
14
|
+
* into that user's own vault of a system-category the receiver's add-on
|
|
15
|
+
* declares ({@link keepAllowsInto}). Anywhere else it changes nothing.
|
|
16
|
+
*
|
|
17
|
+
* An object the engine did not open is never honoured, whatever it says.
|
|
18
|
+
*/
|
|
19
|
+
import type { ActorContext } from '../context/ActorContext.js';
|
|
20
|
+
import type { OriginalKeep, RegisteredReceiver } from './types.js';
|
|
21
|
+
/** Open a keep for one call of `receiver.keepOriginal` with `ctx`. Close it with {@link closeOriginalKeep}. */
|
|
22
|
+
export declare function openOriginalKeep(receiver: RegisteredReceiver, ctx: ActorContext): OriginalKeep;
|
|
23
|
+
/** The call is over: the keep is honoured no more. */
|
|
24
|
+
export declare function closeOriginalKeep(keep: OriginalKeep): void;
|
|
25
|
+
/** Where an upload is going, as the door resolved it. */
|
|
26
|
+
export interface KeepDestination {
|
|
27
|
+
/** Who is uploading: the request's username. */
|
|
28
|
+
uploader: string;
|
|
29
|
+
/** The vault's owner. */
|
|
30
|
+
owner: string;
|
|
31
|
+
/** The add-on whose system-category declares the vault (`StoreKind.owner`); null when no category does. */
|
|
32
|
+
vaultAddon: string | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Does this keep apply to this upload? Only an open keep, for a file going
|
|
36
|
+
* into its user's own vault of a system-category its receiver's add-on
|
|
37
|
+
* declares, uploaded by that user. A `false` leaves the upload exactly as
|
|
38
|
+
* any other: the site's allow-list decides.
|
|
39
|
+
*/
|
|
40
|
+
export declare function keepAllowsInto(keep: OriginalKeep | undefined, to: KeepDestination): boolean;
|
package/lib/src/ice/types.d.ts
CHANGED
|
@@ -130,6 +130,34 @@ export interface IceOriginalChoices {
|
|
|
130
130
|
allowed: OriginalChoice[];
|
|
131
131
|
/** One of `allowed`. Default: `ngdpbase.import.engine.default-original` when allowed, else the first allowed. */
|
|
132
132
|
default?: OriginalChoice;
|
|
133
|
+
/**
|
|
134
|
+
* The media types this receiver keeps (#2061), exactly, e.g.
|
|
135
|
+
* `['application/json', 'application/x-ndjson', 'text/xml']`: no wildcards.
|
|
136
|
+
* For a keep at upload into the receiver's own vault, the upload door
|
|
137
|
+
* allows these even when the site's attachment allow-list does not
|
|
138
|
+
* ({@link OriginalKeep}). Every other upload still uses the site's list.
|
|
139
|
+
*/
|
|
140
|
+
types?: string[];
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* What a keep at upload carries to the upload door (#2061): the engine opens
|
|
144
|
+
* one for one receiver, one user and one call of its `keepOriginal`, and
|
|
145
|
+
* closes it when the call returns. Pass it on unchanged as the upload's
|
|
146
|
+
* `keep` option. The door honours only one the engine has open, and only
|
|
147
|
+
* for a file going into this user's own vault of a system-category the
|
|
148
|
+
* receiver's add-on declares: there it allows `types` beyond the site's
|
|
149
|
+
* attachment allow-list, and the file is always served as a download.
|
|
150
|
+
* Anywhere else it changes nothing.
|
|
151
|
+
*/
|
|
152
|
+
export interface OriginalKeep {
|
|
153
|
+
/** The receiver keeping, `<slug>.<name>`. */
|
|
154
|
+
readonly receiver: string;
|
|
155
|
+
/** The add-on that registered it: only a vault its own system-category declares. */
|
|
156
|
+
readonly addon: string;
|
|
157
|
+
/** The user the upload is for: only their own vault. */
|
|
158
|
+
readonly user: string;
|
|
159
|
+
/** The receiver's `originals.types`. */
|
|
160
|
+
readonly types: readonly string[];
|
|
133
161
|
}
|
|
134
162
|
/** A citation to the origin (D11): the receiver renders it, as a footnote on a page. */
|
|
135
163
|
export interface IceCitation {
|
|
@@ -180,8 +208,12 @@ export interface IceReceiver {
|
|
|
180
208
|
/**
|
|
181
209
|
* Keep the original file itself (D11: phr keeps originals in its own
|
|
182
210
|
* store). Without this, `keep` hands the original to the files receiver.
|
|
211
|
+
* `original.mediaType` is the engine's (#2061): the request's type for a
|
|
212
|
+
* file uploaded on its own, the engine's for one unpacked from a zip.
|
|
213
|
+
* `keep` is given for a keep at upload (`keepsEncrypted`): pass it to the
|
|
214
|
+
* upload door unchanged, as the upload's `keep` option.
|
|
183
215
|
*/
|
|
184
|
-
keepOriginal?(original: IceItem, ctx: ActorContext): Promise<ReceiveOutcome>;
|
|
216
|
+
keepOriginal?(original: IceItem, ctx: ActorContext, keep?: OriginalKeep): Promise<ReceiveOutcome>;
|
|
185
217
|
/**
|
|
186
218
|
* `keepOriginal` writes into an encrypted store (phr's vault), which only a
|
|
187
219
|
* request holding the user's unlocked key can write (#2034). The engine then
|
|
@@ -10,7 +10,8 @@ import type { WikiEngine } from '../types/WikiEngine.js';
|
|
|
10
10
|
import type { CatalogSource, CatalogQuery, CatalogPage, CreativeWork, SchemaType, RebuildOpts } from '../types/Schema.js';
|
|
11
11
|
import type { User } from '../providers/BaseAttachmentProvider.js';
|
|
12
12
|
import { type AssetQuery, type AssetRecord } from '../types/Asset.js';
|
|
13
|
-
import type { StoreFileEntry, StoreFileLocation } from '../types/Provider.js';
|
|
13
|
+
import type { StoreFileEntry, StoreFileInput, StoreFileLocation } from '../types/Provider.js';
|
|
14
|
+
import type { OriginalKeep } from '../ice/types.js';
|
|
14
15
|
/**
|
|
15
16
|
* Base attachment provider interface
|
|
16
17
|
*/
|
|
@@ -44,13 +45,7 @@ interface BaseAttachmentProvider {
|
|
|
44
45
|
getProviderInfo(): {
|
|
45
46
|
features: string[];
|
|
46
47
|
};
|
|
47
|
-
storeFileInStore(location: StoreFileLocation, bytes: Buffer, file:
|
|
48
|
-
originalName: string;
|
|
49
|
-
mimeType: string;
|
|
50
|
-
description: string;
|
|
51
|
-
author?: string;
|
|
52
|
-
pageName?: string;
|
|
53
|
-
}): Promise<StoreFileEntry>;
|
|
48
|
+
storeFileInStore(location: StoreFileLocation, bytes: Buffer, file: StoreFileInput): Promise<StoreFileEntry>;
|
|
54
49
|
getFileInStore(location: StoreFileLocation, id: string): Promise<{
|
|
55
50
|
entry: StoreFileEntry;
|
|
56
51
|
bytes: Buffer;
|
|
@@ -104,6 +99,15 @@ export interface UploadOptions {
|
|
|
104
99
|
private?: boolean;
|
|
105
100
|
/** Store id when private with no private page; default from ConfigurationManager defaultstoreid */
|
|
106
101
|
store?: string;
|
|
102
|
+
/**
|
|
103
|
+
* An import's keep at upload (#2061): the `OriginalKeep` the engine handed
|
|
104
|
+
* the receiver's `keepOriginal`, passed on unchanged. Honoured only while
|
|
105
|
+
* the engine holds it open, for a file going into its user's own vault of a
|
|
106
|
+
* system-category the receiver's add-on declares: there the receiver's
|
|
107
|
+
* declared types are allowed beyond the site's allow-list, and the file is
|
|
108
|
+
* served as a download. Anywhere else it changes nothing.
|
|
109
|
+
*/
|
|
110
|
+
keep?: OriginalKeep;
|
|
107
111
|
}
|
|
108
112
|
/**
|
|
109
113
|
* Mention object (WebPage reference)
|
|
@@ -38,6 +38,7 @@ import type { Aal, AuthProvider, AuthInitiateContext, AuthVerifyCredentials, Phi
|
|
|
38
38
|
import { type OutsideProviderIcon } from '../providers/OidcAuthProvider.js';
|
|
39
39
|
import { type PasskeyRelyingParty } from '../providers/PasskeyAuthProvider.js';
|
|
40
40
|
import { type CredentialKind, type CredentialRecord } from '../providers/BaseCredentialsProvider.js';
|
|
41
|
+
import { type ActorContext } from '../context/ActorContext.js';
|
|
41
42
|
import { type AuditEventName } from '../utils/auditEventNames.js';
|
|
42
43
|
import { type ScopeDescription } from '../utils/permissionCatalog.js';
|
|
43
44
|
import type { Request } from 'express';
|
|
@@ -360,6 +361,32 @@ export type ProxyGrantSide = {
|
|
|
360
361
|
};
|
|
361
362
|
/** Where a signed-in person chooses whom to act for, and switches back (#1762 slice 4). */
|
|
362
363
|
export declare const ACTING_FOR_PATH = "/account/acting-for";
|
|
364
|
+
/** Where an invitation link opens (#2049): `GET` shows it, `POST` uses it. */
|
|
365
|
+
export declare const INVITATION_PATH = "/invitation";
|
|
366
|
+
/** Where an invited user sets up their sign-in after the link signs them in (#2049). */
|
|
367
|
+
export declare const ACCOUNT_SETUP_PATH = "/account/setup";
|
|
368
|
+
/** How many days an invitation link stays usable (#2049); read on every call. */
|
|
369
|
+
export declare const INVITATION_TTL_KEY = "ngdpbase.auth.invitation.ttl-days";
|
|
370
|
+
/** An invitation as an administrator sees it (#2049): never the link or its token. */
|
|
371
|
+
export interface InvitationView {
|
|
372
|
+
id: string;
|
|
373
|
+
/** The account invited. */
|
|
374
|
+
username: string;
|
|
375
|
+
/** Who sent it. */
|
|
376
|
+
createdBy: string;
|
|
377
|
+
/** RFC 3339. */
|
|
378
|
+
createdAt: string;
|
|
379
|
+
/** RFC 3339: after this the link does nothing. */
|
|
380
|
+
expiresAt: string;
|
|
381
|
+
}
|
|
382
|
+
/** What sending an invitation came to (#2049); `reason` is plain words for the administrator. */
|
|
383
|
+
export type InviteOutcome = {
|
|
384
|
+
outcome: 'sent';
|
|
385
|
+
invitation: InvitationView;
|
|
386
|
+
} | {
|
|
387
|
+
outcome: 'email-unavailable' | 'send-failed' | 'not-invitable';
|
|
388
|
+
reason: string;
|
|
389
|
+
};
|
|
363
390
|
/**
|
|
364
391
|
* Why a session started or stopped acting for someone (#1762 slice 4), as the
|
|
365
392
|
* `proxy-switch` record's `reason` says it: the person chose a grant, chose
|
|
@@ -1166,6 +1193,83 @@ declare class AuthManager extends BaseManager {
|
|
|
1166
1193
|
* the one that started the session. Null when the result is no sign-in.
|
|
1167
1194
|
*/
|
|
1168
1195
|
reauthenticated(signIn: SignInRecord | undefined, result: AuthenticateResult): SignInRecord | null;
|
|
1196
|
+
/** How long an invitation link stays usable: `ngdpbase.auth.invitation.ttl-days`, read on every call. */
|
|
1197
|
+
invitationTtlMs(): number;
|
|
1198
|
+
/**
|
|
1199
|
+
* Why no invitation can be sent on this site now, in plain words for the
|
|
1200
|
+
* administrator, or null when one can: email set up and working, an
|
|
1201
|
+
* explicit site address for the link, and the credentials store open.
|
|
1202
|
+
*/
|
|
1203
|
+
invitationsUnavailable(): string | null;
|
|
1204
|
+
/** Sending, resending and cancelling an invitation is creating an account's way in: `user-create` (#2049). */
|
|
1205
|
+
private mayInvite;
|
|
1206
|
+
/** The live and expired invitation rows of the store, or none while it is closed. */
|
|
1207
|
+
private invitationRows;
|
|
1208
|
+
/**
|
|
1209
|
+
* Why `username` cannot be invited, in plain words, or null when they can:
|
|
1210
|
+
* an active account of this site, with an email address, no password, and
|
|
1211
|
+
* never signed in. An account with a way in needs no invitation.
|
|
1212
|
+
*/
|
|
1213
|
+
private notInvitable;
|
|
1214
|
+
/**
|
|
1215
|
+
* Send `username` an invitation (#2049): an email with a one-time link to
|
|
1216
|
+
* set up their sign-in. Needs `user-create` (throws `Permission denied`).
|
|
1217
|
+
* A new invitation replaces any earlier one, whose link stops working
|
|
1218
|
+
* (resend). Recorded first, as `invitation-send` (refuse on failure), so no
|
|
1219
|
+
* link exists that the audit log does not know of. When email cannot be
|
|
1220
|
+
* sent the answer says so and nothing is kept; a send that fails removes
|
|
1221
|
+
* the link again, recorded as `invitation-cancel` with reason `not-sent`.
|
|
1222
|
+
* The message goes through EmailManager with the caller's context.
|
|
1223
|
+
*/
|
|
1224
|
+
inviteUser(ctx: ActorContext, username: string): Promise<InviteOutcome>;
|
|
1225
|
+
/**
|
|
1226
|
+
* Cancel `username`'s invitation (#2049): its link stops working. Needs
|
|
1227
|
+
* `user-create` (throws `Permission denied`). Recorded first, as
|
|
1228
|
+
* `invitation-cancel` (refuse on failure). False when none was waiting.
|
|
1229
|
+
*/
|
|
1230
|
+
cancelInvitation(ctx: ActorContext, username: string): Promise<boolean>;
|
|
1231
|
+
/**
|
|
1232
|
+
* The invitations waiting to be used, for the administrator's user list
|
|
1233
|
+
* (#2049). Reading is not a change: it asks whether the caller holds
|
|
1234
|
+
* `user-read`. Expired ones are removed first, each recorded as
|
|
1235
|
+
* `invitation-expire`.
|
|
1236
|
+
*/
|
|
1237
|
+
listInvitations(ctx: ActorContext): Promise<InvitationView[]>;
|
|
1238
|
+
/** The live invitation a link's token names, or null: unknown, used, or expired (an expired one is removed, recorded). */
|
|
1239
|
+
private liveInvitation;
|
|
1240
|
+
/**
|
|
1241
|
+
* Who an invitation link is for, to show before it is used (#2049), or
|
|
1242
|
+
* null. Opening the link changes nothing (mail scanners open links): the
|
|
1243
|
+
* page's button posts to `useInvitation`, behind the session's CSRF token.
|
|
1244
|
+
*/
|
|
1245
|
+
invitationFor(token: unknown): Promise<{
|
|
1246
|
+
username: string;
|
|
1247
|
+
displayName: string;
|
|
1248
|
+
expiresAt: string;
|
|
1249
|
+
} | null>;
|
|
1250
|
+
/**
|
|
1251
|
+
* Use an invitation link (#2049): spend it, then sign its user in once
|
|
1252
|
+
* through `completeSignIn` (a new session; the terms prompt first when
|
|
1253
|
+
* terms are waiting), at AAL1 as an email link, so a role above that
|
|
1254
|
+
* steps down as for any such sign-in. Returns where to go next
|
|
1255
|
+
* (`ACCOUNT_SETUP_PATH`, or the terms prompt on the way there), or null
|
|
1256
|
+
* when the link is unknown, used, expired, or its account is turned off.
|
|
1257
|
+
*
|
|
1258
|
+
* The row's removal is the single-use gate: the store deletes in memory
|
|
1259
|
+
* before it writes, so of two requests with one link exactly one sees
|
|
1260
|
+
* true. Recorded as `invitation-use`; the sign-in itself as
|
|
1261
|
+
* `authentication-success`, method `invitation`.
|
|
1262
|
+
*/
|
|
1263
|
+
useInvitation(req: Request, token: unknown): Promise<string | null>;
|
|
1264
|
+
/** Remove every invitation past its end date, each recorded as `invitation-expire`. */
|
|
1265
|
+
private sweepInvitations;
|
|
1266
|
+
private expireInvitation;
|
|
1267
|
+
/** End one invitation the caller may end: recorded first (`invitation-cancel`, refuse), then removed. */
|
|
1268
|
+
private endInvitation;
|
|
1269
|
+
/** An invitation record: `user` who acted (the sender, the canceller, the system), `resource` the account invited. Never the token. */
|
|
1270
|
+
private auditInvitation;
|
|
1271
|
+
/** `invitation-use`: the invited user, before any session exists, from the request's address and browser. */
|
|
1272
|
+
private recordInvitationUse;
|
|
1169
1273
|
private pendingSignIns;
|
|
1170
1274
|
private approvalTokens;
|
|
1171
1275
|
private enrolTokens;
|
|
@@ -1241,6 +1345,13 @@ declare class AuthManager extends BaseManager {
|
|
|
1241
1345
|
* scanner opens links too.
|
|
1242
1346
|
*/
|
|
1243
1347
|
approvalDetails(token: string, opener?: ApprovalVisitor): Promise<ApprovalDetails | null>;
|
|
1348
|
+
/**
|
|
1349
|
+
* `at` as `username` reads it: in their own time zone (their `timezone`
|
|
1350
|
+
* preference, else `ngdpbase.default.timezone`), locale and clock, with the
|
|
1351
|
+
* zone named (#1532). For a message sent to them, where no page script can
|
|
1352
|
+
* show it in local time; the approval request and an invitation (#2049).
|
|
1353
|
+
*/
|
|
1354
|
+
private localTimeFor;
|
|
1244
1355
|
/**
|
|
1245
1356
|
* What an approval request says about a waiting sign-in (#1532), one source
|
|
1246
1357
|
* for the message and the page: the site's name, when it was asked in the
|
|
@@ -282,6 +282,14 @@ declare class BackgroundJobManager extends BaseManager {
|
|
|
282
282
|
private compileScheduled;
|
|
283
283
|
/** A run's time limit: the job's own, else `fallback`, capped by `ngdpbase.jobs.max-timeout-ms` when set. */
|
|
284
284
|
private timeoutFor;
|
|
285
|
+
/**
|
|
286
|
+
* The time zone the scheduler runs a schedule in when the schedule names
|
|
287
|
+
* none (#1998): `ngdpbase.default.timezone`, UTC when it is not set. Read
|
|
288
|
+
* each call, as the schedules are. An add-on's job passes it, with its
|
|
289
|
+
* `ctx.slot`, to `host.zonedParts` to learn the slot's local month or day.
|
|
290
|
+
* A schedule that names its own `tz` runs in that zone instead.
|
|
291
|
+
*/
|
|
292
|
+
scheduleTimeZone(): string;
|
|
285
293
|
private compile;
|
|
286
294
|
/**
|
|
287
295
|
* `ngdpbase.jobs.<id>.schedule` in place of the registered schedule. One
|
|
@@ -124,6 +124,8 @@ export declare function isCompanionOf(filename: string, documentFilename: string
|
|
|
124
124
|
* and the Backup help page list the same.
|
|
125
125
|
*/
|
|
126
126
|
export declare const NEVER_IN_A_BACKUP: readonly string[];
|
|
127
|
+
/** The backup entry a registered manager's part is recorded under (#2038): its name, unless it was renamed. */
|
|
128
|
+
export declare function managerEntryName(registered: string): string;
|
|
127
129
|
/** The backup document entry, and the site restore part, an add-on's files are recorded under (#1985). */
|
|
128
130
|
export declare function addonEntryName(slug: string): string;
|
|
129
131
|
/**
|
|
@@ -575,6 +577,10 @@ declare class BackupManager extends BaseManager {
|
|
|
575
577
|
*/
|
|
576
578
|
restoreFromFile(ctx: ActorContext, backupPath: string, options?: RestoreOptions): Promise<RestoreResults>;
|
|
577
579
|
private restoreFromFileNow;
|
|
580
|
+
/** The registered managers, by the entry each is recorded under in a backup (#2038), in registration order. */
|
|
581
|
+
private registeredEntries;
|
|
582
|
+
/** The manager a backup entry belongs to (#2038), or undefined when this site has none. */
|
|
583
|
+
private managerFor;
|
|
578
584
|
/** Whether this site's manager of that name stages its restore (#1876), or its entry is declared folders (#1985). */
|
|
579
585
|
private restoresStaged;
|
|
580
586
|
/**
|
|
@@ -61,7 +61,8 @@ const RESTORE_LABELS = {
|
|
|
61
61
|
BackgroundJobManager: 'scheduled-job state',
|
|
62
62
|
AttachmentManager: 'attachment files',
|
|
63
63
|
RoleManager: 'role membership',
|
|
64
|
-
|
|
64
|
+
// IdentityManager's entry keeps its old name (#2038, MANAGER_ENTRY_NAMES).
|
|
65
|
+
PersonManager: 'identity records (of users and contacts)',
|
|
65
66
|
OrganizationManager: 'organizations',
|
|
66
67
|
FootnoteManager: 'footnotes on public pages',
|
|
67
68
|
CommentManager: 'comments on public pages',
|
|
@@ -83,6 +84,24 @@ const ADDON_ENTRY_NAMES = {
|
|
|
83
84
|
calendar: 'CalendarDataManager',
|
|
84
85
|
feeds: 'FeedManager'
|
|
85
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* A core manager renamed after its entry was first written keeps that entry
|
|
89
|
+
* (#2038): IdentityManager was PersonManager, and its part is still recorded
|
|
90
|
+
* as `PersonManager` in the backup document and the site restore record, so
|
|
91
|
+
* an old backup restores and an older site can restore a new one. Keyed by
|
|
92
|
+
* the registered name; an entry is a stored name, never renamed.
|
|
93
|
+
*/
|
|
94
|
+
const MANAGER_ENTRY_NAMES = {
|
|
95
|
+
IdentityManager: 'PersonManager'
|
|
96
|
+
};
|
|
97
|
+
/** The backup entry a registered manager's part is recorded under (#2038): its name, unless it was renamed. */
|
|
98
|
+
export function managerEntryName(registered) {
|
|
99
|
+
return Object.hasOwn(MANAGER_ENTRY_NAMES, registered) ? MANAGER_ENTRY_NAMES[registered] : registered;
|
|
100
|
+
}
|
|
101
|
+
/** The registered manager a backup entry belongs to (#2038): the inverse of {@link managerEntryName}. */
|
|
102
|
+
function registeredNameOf(entry) {
|
|
103
|
+
return Object.entries(MANAGER_ENTRY_NAMES).find(([, e]) => e === entry)?.[0] ?? entry;
|
|
104
|
+
}
|
|
86
105
|
/** The backup document entry, and the site restore part, an add-on's files are recorded under (#1985). */
|
|
87
106
|
export function addonEntryName(slug) {
|
|
88
107
|
return ADDON_ENTRY_NAMES[slug] ?? `addon:${slug}`;
|
|
@@ -99,7 +118,7 @@ export function applyStagedPartsAtStart(engine, manager, declared) {
|
|
|
99
118
|
const configManager = engine?.getManager?.('ConfigurationManager');
|
|
100
119
|
if (!configManager)
|
|
101
120
|
return null;
|
|
102
|
-
return applyDeclaredPartsAtStart(manager, declared, configManager);
|
|
121
|
+
return applyDeclaredPartsAtStart(managerEntryName(manager), declared, configManager);
|
|
103
122
|
}
|
|
104
123
|
/** The site restore manifest's file, as configured; a module function for the start, before BackupManager exists (#1986). */
|
|
105
124
|
function manifestFileOf(engine) {
|
|
@@ -526,7 +545,7 @@ class BackupManager extends BaseManager {
|
|
|
526
545
|
managers: {}
|
|
527
546
|
};
|
|
528
547
|
// Get all manager names from engine
|
|
529
|
-
const managerNames = this.
|
|
548
|
+
const managerNames = this.registeredEntries();
|
|
530
549
|
logger.info(`📋 Found ${managerNames.length} registered managers`);
|
|
531
550
|
// #1723: a manager may write its own files (the databases) beside the
|
|
532
551
|
// document; they share its stem and are pruned with it.
|
|
@@ -550,7 +569,7 @@ class BackupManager extends BaseManager {
|
|
|
550
569
|
continue;
|
|
551
570
|
}
|
|
552
571
|
try {
|
|
553
|
-
const manager = this.
|
|
572
|
+
const manager = this.managerFor(managerName);
|
|
554
573
|
const parts = declared.get(managerName) ?? [];
|
|
555
574
|
if (!manager && parts.length === 0) {
|
|
556
575
|
logger.warn(`⚠️ Manager not found: ${managerName}`);
|
|
@@ -1318,7 +1337,7 @@ class BackupManager extends BaseManager {
|
|
|
1318
1337
|
continue;
|
|
1319
1338
|
}
|
|
1320
1339
|
try {
|
|
1321
|
-
const manager = this.
|
|
1340
|
+
const manager = this.managerFor(managerName);
|
|
1322
1341
|
if (!manager) {
|
|
1323
1342
|
logger.warn(`⚠️ Manager not found: ${managerName}, skipping`);
|
|
1324
1343
|
results.skipped.push(managerName);
|
|
@@ -1361,11 +1380,19 @@ class BackupManager extends BaseManager {
|
|
|
1361
1380
|
throw error;
|
|
1362
1381
|
}
|
|
1363
1382
|
}
|
|
1383
|
+
/** The registered managers, by the entry each is recorded under in a backup (#2038), in registration order. */
|
|
1384
|
+
registeredEntries() {
|
|
1385
|
+
return this.engine.getRegisteredManagers().map(managerEntryName);
|
|
1386
|
+
}
|
|
1387
|
+
/** The manager a backup entry belongs to (#2038), or undefined when this site has none. */
|
|
1388
|
+
managerFor(entry) {
|
|
1389
|
+
return this.engine.getManager(registeredNameOf(entry));
|
|
1390
|
+
}
|
|
1364
1391
|
/** Whether this site's manager of that name stages its restore (#1876), or its entry is declared folders (#1985). */
|
|
1365
1392
|
restoresStaged(name) {
|
|
1366
1393
|
if (this.declaredFor(name).length > 0)
|
|
1367
1394
|
return true;
|
|
1368
|
-
const manager = this.
|
|
1395
|
+
const manager = this.managerFor(name);
|
|
1369
1396
|
return typeof manager?.restoreIsStaged === 'function' && manager.restoreIsStaged();
|
|
1370
1397
|
}
|
|
1371
1398
|
/**
|
|
@@ -1384,7 +1411,7 @@ class BackupManager extends BaseManager {
|
|
|
1384
1411
|
}
|
|
1385
1412
|
/** What a manager declares, and an add-on's folders recorded under the same entry (#1985). */
|
|
1386
1413
|
declaredFor(name, forBackup = false) {
|
|
1387
|
-
const manager = this.
|
|
1414
|
+
const manager = this.managerFor(name);
|
|
1388
1415
|
let own = [];
|
|
1389
1416
|
try {
|
|
1390
1417
|
own = typeof manager?.backupFolders === 'function' ? manager.backupFolders() : [];
|
|
@@ -1396,7 +1423,7 @@ class BackupManager extends BaseManager {
|
|
|
1396
1423
|
}
|
|
1397
1424
|
/** Every entry with declared folders or files, in registration order, add-ons' after (#1985). */
|
|
1398
1425
|
declaredParts(options = {}) {
|
|
1399
|
-
const names = [...this.
|
|
1426
|
+
const names = [...this.registeredEntries(), ...this.addonParts().map((a) => a.entry)];
|
|
1400
1427
|
const out = new Map();
|
|
1401
1428
|
for (const name of names) {
|
|
1402
1429
|
if (out.has(name) || name === 'BackupManager')
|
|
@@ -1492,7 +1519,7 @@ class BackupManager extends BaseManager {
|
|
|
1492
1519
|
await stageDeclaredParts(name, declared, backupData.managers[name], restoreOptions);
|
|
1493
1520
|
continue;
|
|
1494
1521
|
}
|
|
1495
|
-
await this.
|
|
1522
|
+
await this.managerFor(name).restore(backupData.managers[name], restoreOptions);
|
|
1496
1523
|
}
|
|
1497
1524
|
// #1876: the instance .env, BackupManager's own part; applied at the very start of the process.
|
|
1498
1525
|
const env = backupData.managers.BackupManager?.data?.env;
|
|
@@ -1729,7 +1756,7 @@ class BackupManager extends BaseManager {
|
|
|
1729
1756
|
}
|
|
1730
1757
|
/** The managers on this site whose restore is staged (#1876), in registration order: what a site restore stages. */
|
|
1731
1758
|
stagedManagerNames() {
|
|
1732
|
-
const names = this.
|
|
1759
|
+
const names = this.registeredEntries().filter((name) => this.restoresStaged(name));
|
|
1733
1760
|
// #1985: an add-on's files under an entry no manager of that name has (`addon:<slug>`).
|
|
1734
1761
|
for (const { entry } of this.addonParts())
|
|
1735
1762
|
if (!names.includes(entry))
|
|
@@ -1750,7 +1777,7 @@ class BackupManager extends BaseManager {
|
|
|
1750
1777
|
.filter((name) => name !== 'DatabaseManager' || databases?.isEnabled?.() !== false)
|
|
1751
1778
|
.map(restoreLabel);
|
|
1752
1779
|
const notRestored = Object.keys(RESTORE_LABELS)
|
|
1753
|
-
.filter((name) => name !== 'BackupManager' && this.
|
|
1780
|
+
.filter((name) => name !== 'BackupManager' && this.managerFor(name) && !this.restoresStaged(name))
|
|
1754
1781
|
.map(restoreLabel);
|
|
1755
1782
|
return { restored, notRestored: [...notRestored, ...NEVER_IN_A_BACKUP] };
|
|
1756
1783
|
}
|