@jwilleke/ngdpbase-types 5.29.0 → 5.30.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 +6 -2
- package/lib/src/context/JobContext.d.ts +11 -1
- package/lib/src/context/JobContext.js +15 -1
- package/lib/src/context/bootActions.js +3 -1
- package/lib/src/managers/AuthManager.d.ts +111 -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/OrganizationManager.d.ts +2 -2
- package/lib/src/managers/RoleManager.d.ts +7 -7
- package/lib/src/managers/UserManager.d.ts +20 -5
- package/lib/src/providers/BaseCredentialsProvider.d.ts +7 -1
- package/lib/src/security/PolicyDecisionPoint.d.ts +4 -0
- package/lib/src/security/permissions.generated.d.ts +4 -4
- package/lib/src/types/Config.d.ts +1 -1
- package/lib/src/types/Organization.d.ts +16 -4
- package/lib/src/types/Person.d.ts +4 -4
- package/lib/src/types/PersonProvider.d.ts +2 -2
- 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/contactAccess.d.ts +16 -9
- package/lib/src/utils/privateStoreUnlock.d.ts +1 -1
- package/package.json +1 -1
|
@@ -39,9 +39,13 @@ export type { default as ImportManager } from '../../src/managers/ImportManager.
|
|
|
39
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
40
|
export type { default as UserManager, UserUpdateInput, UserUpdateFn } from '../../src/managers/UserManager.js';
|
|
41
41
|
export type { User, UserPreferences } from '../../src/types/User.js';
|
|
42
|
-
export type { default as
|
|
42
|
+
export type { default as AuthManager, InviteOutcome, InvitationView } from '../../src/managers/AuthManager.js';
|
|
43
|
+
export type { default as IdentityManager, LinkOfferOutcome } from '../../src/managers/IdentityManager.js';
|
|
44
|
+
export type {
|
|
45
|
+
/** @deprecated Renamed `IdentityManager` (#2038); this alias goes after one release. */
|
|
46
|
+
default as PersonManager } from '../../src/managers/IdentityManager.js';
|
|
43
47
|
export type { default as OrganizationManager } from '../../src/managers/OrganizationManager.js';
|
|
44
48
|
export type { Person, PersonAffiliation, PersonInput, PersonUpdate } from '../../src/types/Person.js';
|
|
45
|
-
export type { Organization, OrganizationInput, OrganizationUpdate } from '../../src/types/Organization.js';
|
|
49
|
+
export type { Organization, OrganizationInput, OrganizationUpdate, ContactPoint, ContactPointEntry, PostalAddress } from '../../src/types/Organization.js';
|
|
46
50
|
export type { ContactRefusedError } from '../../src/utils/contactAccess.js';
|
|
47
51
|
export type { default as RoleManager } from '../../src/managers/RoleManager.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;
|
|
@@ -96,6 +99,13 @@ export declare const ANONYMOUS_USERNAME = "Anonymous";
|
|
|
96
99
|
* string, so `viaToken` comes along instead of being dropped.
|
|
97
100
|
*/
|
|
98
101
|
export declare function jobContextFromRequest(identity: RequestIdentity | null | undefined, now?: Date): JobContext;
|
|
102
|
+
/**
|
|
103
|
+
* A job context with no key handle in it (#2047). `jobContextFromRequest`
|
|
104
|
+
* already leaves the handle out; the job manager also passes every context it
|
|
105
|
+
* stores through this, so a context built by hand cannot carry a key copy's
|
|
106
|
+
* handle into a job or onto disk.
|
|
107
|
+
*/
|
|
108
|
+
export declare function withoutKeyHandle(ctx: JobContext): JobContext;
|
|
99
109
|
/**
|
|
100
110
|
* A request-origin context that also says why (#1179).
|
|
101
111
|
*
|
|
@@ -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
|
*
|
|
@@ -64,7 +64,9 @@ export function attributedTo(ctx) {
|
|
|
64
64
|
origin: ctx.origin,
|
|
65
65
|
reason: ctx.reason ?? null,
|
|
66
66
|
requestedAt: ctx.requestedAt,
|
|
67
|
-
|
|
67
|
+
// #2059: the delegation a request-origin job was queued under, by kind,
|
|
68
|
+
// with the same precedence as a request's own record (`actorOf`).
|
|
69
|
+
...(ctx.viaToken ? { delegated: 'token' } : ctx.viaShare ? { delegated: 'share' } : proxyMeta)
|
|
68
70
|
}
|
|
69
71
|
};
|
|
70
72
|
}
|
|
@@ -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
|
|
@@ -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
|
}
|
|
@@ -11,8 +11,12 @@ export interface LinkOfferDetails {
|
|
|
11
11
|
/** The outcome of offering a sign-up link (#1930). */
|
|
12
12
|
export type LinkOfferOutcome = 'sent' | 'no-match' | 'several' | 'unavailable' | 'failed';
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* IdentityManager — the identity record of every digital identity, user or
|
|
15
|
+
* contact (#617), and the door for them (#1930). Named PersonManager until
|
|
16
|
+
* #2038; `getManager('PersonManager')` still answers with it for one release
|
|
17
|
+
* (`MANAGER_ALIASES` in `src/core/Engine.ts`). What is stored keeps its
|
|
18
|
+
* "person" names: `data/persons/`, `ngdpbase.application.persons.*`, the
|
|
19
|
+
* `Person` types, `person:<uuid>`, the `person-*` audit actions.
|
|
16
20
|
*
|
|
17
21
|
* A person may have an account or not. Each account has exactly one person
|
|
18
22
|
* (`identifier` = the username); a person with no account has the identifier
|
|
@@ -20,14 +24,14 @@ export type LinkOfferOutcome = 'sent' | 'no-match' | 'several' | 'unavailable' |
|
|
|
20
24
|
* the account lifecycle under a system context.
|
|
21
25
|
*
|
|
22
26
|
* The door: `create`, `update`, `delete`, `get`, `list`, `search` and
|
|
23
|
-
* `linkAccount` take the caller's context first and ask the PDP: `
|
|
24
|
-
* to list, search or get; `
|
|
25
|
-
* the `
|
|
27
|
+
* `linkAccount` take the caller's context first and ask the PDP: `identity-read`
|
|
28
|
+
* to list, search or get; `identity-manage` to change (`linkAccount` also `user-edit`). Each change is recorded as
|
|
29
|
+
* the `identity-manage` audit event. A refusal throws `ContactRefusedError`.
|
|
26
30
|
*
|
|
27
31
|
* Every lookup takes the caller's context too (#2018): `getById`,
|
|
28
32
|
* `getByIdentifier` and `getByUserIdentifier`. A user reads their own identity
|
|
29
33
|
* record, and `update` changes it, under `profile-manage`; anyone else's needs
|
|
30
|
-
* `
|
|
34
|
+
* `identity-read` / `identity-manage`. Core's own reads (RoleManager resolving
|
|
31
35
|
* roles on every request, UserManager's account lifecycle) pass the system
|
|
32
36
|
* context, allowed without a policy evaluation (`requireRecordAccess`).
|
|
33
37
|
*
|
|
@@ -40,8 +44,8 @@ export type LinkOfferOutcome = 'sent' | 'no-match' | 'several' | 'unavailable' |
|
|
|
40
44
|
* `getByUserIdentifier(ctx, username)` returns a user's identity record and
|
|
41
45
|
* makes it when the user has none (#2037): every user has exactly one.
|
|
42
46
|
*/
|
|
43
|
-
declare class
|
|
44
|
-
readonly description = "
|
|
47
|
+
declare class IdentityManager extends BaseManager {
|
|
48
|
+
readonly description = "Identity records of users and contacts (#617)";
|
|
45
49
|
private provider;
|
|
46
50
|
private providerClass?;
|
|
47
51
|
/** `ngdpbase.application.persons.storagedir`, resolved at start: what a backup copies (#1876). */
|
|
@@ -65,7 +69,7 @@ declare class PersonManager extends BaseManager {
|
|
|
65
69
|
getProvider(): PersonProvider | null;
|
|
66
70
|
/**
|
|
67
71
|
* One identity record by `@id` (#2018): the caller's own under
|
|
68
|
-
* `profile-manage`, anyone else's under `
|
|
72
|
+
* `profile-manage`, anyone else's under `identity-read`, core's own work
|
|
69
73
|
* under the system context. A refusal throws `ContactRefusedError`.
|
|
70
74
|
*/
|
|
71
75
|
getById(ctx: ActorContext, id: string): Promise<Person | null>;
|
|
@@ -80,9 +84,9 @@ declare class PersonManager extends BaseManager {
|
|
|
80
84
|
getManagerStats(): Promise<ManagerStats>;
|
|
81
85
|
/** True when the person has no account: its identifier is `person:<uuid>` (#1930). */
|
|
82
86
|
static isAccountless(person: Pick<Person, 'identifier'>): boolean;
|
|
83
|
-
/** Every person, for a caller who may read contacts (`
|
|
87
|
+
/** Every person, for a caller who may read contacts (`identity-read`). */
|
|
84
88
|
list(ctx: ActorContext): Promise<Person[]>;
|
|
85
|
-
/** One person by `@id`: {@link getById}, the caller's own under `profile-manage`, anyone else's under `
|
|
89
|
+
/** One person by `@id`: {@link getById}, the caller's own under `profile-manage`, anyone else's under `identity-read`. */
|
|
86
90
|
get(ctx: ActorContext, id: string): Promise<Person | null>;
|
|
87
91
|
/**
|
|
88
92
|
* Persons whose name (`name`, `givenName`, `familyName`) or `email` contains
|
|
@@ -103,13 +107,13 @@ declare class PersonManager extends BaseManager {
|
|
|
103
107
|
/**
|
|
104
108
|
* Change a person's fields, affiliations included. `@id` and `identifier`
|
|
105
109
|
* never change here. The caller's own identity record under
|
|
106
|
-
* `profile-manage`, anyone else's under `
|
|
110
|
+
* `profile-manage`, anyone else's under `identity-manage` (#2018).
|
|
107
111
|
*/
|
|
108
112
|
update(ctx: ActorContext, id: string, patch: PersonUpdate): Promise<Person | null>;
|
|
109
113
|
delete(ctx: ActorContext, id: string): Promise<boolean>;
|
|
110
114
|
/**
|
|
111
115
|
* An administrator joins a person who has no account to an account (#1930),
|
|
112
|
-
* for when the email differs or none was given. Asks `
|
|
116
|
+
* for when the email differs or none was given. Asks `identity-manage` and
|
|
113
117
|
* `user-edit`. The person keeps its `@id`; the account's own person, if it
|
|
114
118
|
* has one, is folded in and removed.
|
|
115
119
|
*/
|
|
@@ -163,11 +167,11 @@ declare class PersonManager extends BaseManager {
|
|
|
163
167
|
* store) gets it here, from the account's display name and email.
|
|
164
168
|
*
|
|
165
169
|
* The read is decided as {@link getByIdentifier} (#2018): a signed-in user's
|
|
166
|
-
* own under `profile-manage`, anyone else's under `
|
|
170
|
+
* own under `profile-manage`, anyone else's under `identity-read`, core's own
|
|
167
171
|
* work under the system context. Making the missing record is core's
|
|
168
172
|
* bookkeeping, not the caller's contact management, so it goes through
|
|
169
173
|
* {@link create} under the system context, naming the caller, and is
|
|
170
|
-
* recorded as `
|
|
174
|
+
* recorded as `identity-manage`. One at a time per username, so two lookups
|
|
171
175
|
* never make two records. Null when there is no account by that name.
|
|
172
176
|
* Never a second record: a contact with the same email is left alone
|
|
173
177
|
* (an administrator links it, {@link linkAccount}).
|
|
@@ -178,4 +182,4 @@ declare class PersonManager extends BaseManager {
|
|
|
178
182
|
private normalizeProviderName;
|
|
179
183
|
}
|
|
180
184
|
export { ContactRefusedError } from '../utils/contactAccess.js';
|
|
181
|
-
export default
|
|
185
|
+
export default IdentityManager;
|
|
@@ -88,13 +88,13 @@ declare class OrganizationManager extends BaseManager {
|
|
|
88
88
|
* problem actually bites.
|
|
89
89
|
*/
|
|
90
90
|
getInstallOrg(): Promise<Organization | null>;
|
|
91
|
-
/** One organization by `@id`, for a caller who may read contacts (`
|
|
91
|
+
/** One organization by `@id`, for a caller who may read contacts (`identity-read`), or core's own work (#2018). */
|
|
92
92
|
getById(ctx: ActorContext, id: string): Promise<Organization | null>;
|
|
93
93
|
/** One organization by its file name, decided as {@link getById} (#2018). */
|
|
94
94
|
getByFile(ctx: ActorContext, filename: string): Promise<Organization | null>;
|
|
95
95
|
/** #1006: how many organizations this instance holds. Count only. */
|
|
96
96
|
getManagerStats(): Promise<ManagerStats>;
|
|
97
|
-
/** Every organization, for a caller who may read contacts (`
|
|
97
|
+
/** Every organization, for a caller who may read contacts (`identity-read`). */
|
|
98
98
|
list(ctx: ActorContext): Promise<Organization[]>;
|
|
99
99
|
/** One organization by `@id`: {@link getById}. */
|
|
100
100
|
get(ctx: ActorContext, id: string): Promise<Organization | null>;
|
|
@@ -45,7 +45,7 @@ declare class RoleManager extends BaseManager {
|
|
|
45
45
|
backupFolders(): BackupFolder[];
|
|
46
46
|
/** Provider accessor, mainly for tests. */
|
|
47
47
|
getProvider(): RoleProvider | null;
|
|
48
|
-
/** One role record by `@id`: its members are identity records, so `
|
|
48
|
+
/** One role record by `@id`: its members are identity records, so `identity-read`, or core's own work (#2018). */
|
|
49
49
|
getById(ctx: ActorContext, id: string): Promise<Role | null>;
|
|
50
50
|
getByOrgAndPosition(organizationId: string, namedPosition: string): Promise<Role | null>;
|
|
51
51
|
listByMember(personId: string): Promise<Role[]>;
|
|
@@ -67,13 +67,13 @@ declare class RoleManager extends BaseManager {
|
|
|
67
67
|
* The pseudo-roles `'Authenticated'` and `'All'` are NOT added here — the
|
|
68
68
|
* caller adds those when constructing `userContext.roles`.
|
|
69
69
|
*
|
|
70
|
-
* Returns `[]` when
|
|
70
|
+
* Returns `[]` when IdentityManager is unavailable, there is no account by
|
|
71
71
|
* that username, no record lists them, or the lookup throws. An account
|
|
72
72
|
* missing its identity record gets it here (#2037).
|
|
73
73
|
*
|
|
74
|
-
* `ctx` is who is asking (#2018), decided by
|
|
74
|
+
* `ctx` is who is asking (#2018), decided by IdentityManager's door: a user
|
|
75
75
|
* about themselves under `profile-manage`, about anyone else under
|
|
76
|
-
* `
|
|
76
|
+
* `identity-read`; core's own work passes the system context. A refusal is
|
|
77
77
|
* thrown as `ContactRefusedError`, never answered as "no roles".
|
|
78
78
|
*/
|
|
79
79
|
resolveUserRoles(ctx: ActorContext, username: string): Promise<string[]>;
|
|
@@ -114,7 +114,7 @@ declare class RoleManager extends BaseManager {
|
|
|
114
114
|
* All or nothing (#2015): when any change cannot be made, the ones already
|
|
115
115
|
* made are undone and a `RoleChangeError` is thrown, after it is logged as
|
|
116
116
|
* an error and admins are notified. That includes a change that cannot even
|
|
117
|
-
* be attempted: no
|
|
117
|
+
* be attempted: no IdentityManager, no Person record for the username, or no
|
|
118
118
|
* anchor Organization.
|
|
119
119
|
*/
|
|
120
120
|
applyRoleDiff(username: string, oldRoles: string[], newRoles: string[]): Promise<void>;
|
|
@@ -139,9 +139,9 @@ declare class RoleManager extends BaseManager {
|
|
|
139
139
|
grantToEveryAccountOnce(roleName: string, usernames: readonly string[], ctx: JobContext): Promise<number>;
|
|
140
140
|
/**
|
|
141
141
|
* Move every membership held by one Person `@id` to another (#1930): when
|
|
142
|
-
*
|
|
142
|
+
* IdentityManager joins an account to an existing person, the roles the
|
|
143
143
|
* account's person held go with the account. Returns how many records
|
|
144
|
-
* changed.
|
|
144
|
+
* changed. IdentityManager's join records the change. All or nothing (#2015).
|
|
145
145
|
*/
|
|
146
146
|
moveMemberships(fromPersonId: string, toPersonId: string): Promise<number>;
|
|
147
147
|
/**
|
|
@@ -343,9 +343,9 @@ declare class UserManager extends BaseManager {
|
|
|
343
343
|
/**
|
|
344
344
|
* Every account has exactly one identity record, made by `createUser`
|
|
345
345
|
* (#2037). One that has none (written straight into the user store, or
|
|
346
|
-
* whose record was lost) gets it at boot, once, through
|
|
346
|
+
* whose record was lost) gets it at boot, once, through IdentityManager's
|
|
347
347
|
* door under the system context, each creation recorded as
|
|
348
|
-
* `
|
|
348
|
+
* `identity-manage`. The identity records are listed once and only the
|
|
349
349
|
* missing ones are made, so a large user store costs one read. A failure
|
|
350
350
|
* is logged and the boot goes on: the role paths make the record when the
|
|
351
351
|
* account is next used (`getByUserIdentifier`).
|
|
@@ -457,6 +457,16 @@ declare class UserManager extends BaseManager {
|
|
|
457
457
|
* which accounts exist.
|
|
458
458
|
*/
|
|
459
459
|
resetPasswordWithRecoveryWords(username: string, words: string, newPassword: string, ctx: ActorContext): Promise<boolean>;
|
|
460
|
+
/**
|
|
461
|
+
* A user's first password, chosen by themselves (#2049: after an
|
|
462
|
+
* invitation signed them in). Only for their own account, signed in as
|
|
463
|
+
* themselves (no token, share link or proxy), under `account-security`,
|
|
464
|
+
* and only while the account has no password: a password already set is
|
|
465
|
+
* changed on the profile, which asks for the current one. `false`,
|
|
466
|
+
* changing nothing, otherwise. The length rule is the route's, as for
|
|
467
|
+
* every password form. Recorded like any password change (`user-edit`).
|
|
468
|
+
*/
|
|
469
|
+
setFirstPassword(username: string, newPassword: string, ctx: PermissionSubject): Promise<boolean>;
|
|
460
470
|
/**
|
|
461
471
|
* Change an account (#2025: one change at a time per account).
|
|
462
472
|
*
|
|
@@ -517,6 +527,11 @@ declare class UserManager extends BaseManager {
|
|
|
517
527
|
/**
|
|
518
528
|
* Search users by username, displayName, or email (case-insensitive substring).
|
|
519
529
|
* Optionally filter by role and active status.
|
|
530
|
+
*
|
|
531
|
+
* Decides `search-user` itself, with `ctx` (#2018): every user holds it as
|
|
532
|
+
* shipped, anonymous does not. A refusal throws `ContactRefusedError`. Core's
|
|
533
|
+
* own lookups (a notice's recipients, a role's holders) pass the system
|
|
534
|
+
* context with a reason, allowed without a policy evaluation.
|
|
520
535
|
*/
|
|
521
536
|
searchUsers(query: string, options: {
|
|
522
537
|
role?: string;
|
|
@@ -524,10 +539,10 @@ declare class UserManager extends BaseManager {
|
|
|
524
539
|
activeOnly?: boolean;
|
|
525
540
|
}, ctx: ActorContext): Promise<Omit<User, 'password'>[]>;
|
|
526
541
|
/**
|
|
527
|
-
* The account lifecycle reaches
|
|
542
|
+
* The account lifecycle reaches IdentityManager's door under a system context
|
|
528
543
|
* (#1930): keeping an account's person in step with the account is core's
|
|
529
544
|
* own bookkeeping, not the caller's contact management, so it never depends
|
|
530
|
-
* on the caller holding `
|
|
545
|
+
* on the caller holding `identity-manage`. The reason names the account and
|
|
531
546
|
* who changed it, so the record says why.
|
|
532
547
|
*/
|
|
533
548
|
private personSyncContext;
|
|
@@ -542,7 +557,7 @@ declare class UserManager extends BaseManager {
|
|
|
542
557
|
private syncPersonOnCreate;
|
|
543
558
|
/**
|
|
544
559
|
* At sign-up, offer the new account a person with no account who has the
|
|
545
|
-
* same email (#1930):
|
|
560
|
+
* same email (#1930): IdentityManager mails an "Is this you?" link, and joins
|
|
546
561
|
* them only when the account confirms. Skipped when email is off. Never
|
|
547
562
|
* fails the account write.
|
|
548
563
|
*/
|
|
@@ -24,8 +24,14 @@ export declare const CREDENTIALS_KEY_ENV = "NGDPBASE_CREDENTIALS_KEY";
|
|
|
24
24
|
* represented, `subject` the grant id, `secret` the grant's terms as JSON
|
|
25
25
|
* (whom it is for, the permissions, who made it, when it ends). Never a way
|
|
26
26
|
* in; AuthManager reads and writes it only through its proxy-grant methods.
|
|
27
|
+
*
|
|
28
|
+
* `invitation` (#2049): a one-time link that lets a new user set up their
|
|
29
|
+
* sign-in — `username` the account invited, `subject` the SHA-256 of the
|
|
30
|
+
* link's token (the token itself is never stored), `secret` the terms as JSON
|
|
31
|
+
* (who sent it, when it ends). Not a sign-in method; AuthManager reads and
|
|
32
|
+
* writes it only through its invitation methods.
|
|
27
33
|
*/
|
|
28
|
-
export type CredentialKind = 'passkey' | 'totp' | 'email' | 'sms' | 'device' | 'identity' | 'proxy';
|
|
34
|
+
export type CredentialKind = 'passkey' | 'totp' | 'email' | 'sms' | 'device' | 'identity' | 'proxy' | 'invitation';
|
|
29
35
|
export declare const CREDENTIAL_KINDS: readonly CredentialKind[];
|
|
30
36
|
/** One credential, as stored. */
|
|
31
37
|
export interface CredentialRecord {
|
|
@@ -43,6 +43,10 @@ export declare class PolicyDecisionPoint extends BaseManager {
|
|
|
43
43
|
* derived, never stored.
|
|
44
44
|
*/
|
|
45
45
|
constructor(engine: WikiEngine);
|
|
46
|
+
/** Each `<policy id> <retired name>` already warned about (#2038): said once per process. */
|
|
47
|
+
private readonly warnedRetired;
|
|
48
|
+
/** Warn, once per policy and name, that a policy names a retired permission and what it is read as (#2038). */
|
|
49
|
+
private warnRetiredOnce;
|
|
46
50
|
/**
|
|
47
51
|
* The policies in force right now, highest priority first.
|
|
48
52
|
*
|
|
@@ -22,10 +22,10 @@ export type CorePermission =
|
|
|
22
22
|
| 'comment-create'
|
|
23
23
|
/** Change any configuration setting, and download a backup (#1525). Admin only by default; asks for a fresh sign-in when listed in ngdpbase.auth.step-up */
|
|
24
24
|
| 'config-manage'
|
|
25
|
-
/** Create, edit, delete and link
|
|
26
|
-
| '
|
|
27
|
-
/** List, search and view
|
|
28
|
-
| '
|
|
25
|
+
/** Create, edit, delete and link identity records, of users and contacts alike, and organizations, and an identity's affiliations, through IdentityManager and OrganizationManager (#1930). Reading them is identity-read. Was contact-manage until #2038; a policy naming that is still read as this for one release. Admins by default; an add-on grants it to its own roles in its own policy */
|
|
26
|
+
| 'identity-manage'
|
|
27
|
+
/** List, search and view identity records, of users and contacts alike, and organizations, and their affiliations, through IdentityManager and OrganizationManager, and the /admin/contacts and /admin/organizations views (#1930). Was contact-read until #2038; a policy naming that is still read as this for one release. Admins and demo admins by default; an add-on grants it to its own roles in its own policy */
|
|
28
|
+
| 'identity-read'
|
|
29
29
|
/** Browse and view the media library (not delegated to anonymous visitors) */
|
|
30
30
|
| 'media-read'
|
|
31
31
|
/** Create new pages */
|
|
@@ -312,7 +312,7 @@ export interface ApplicationOrganizationConfig {
|
|
|
312
312
|
'ngdpbase.application.persons.provider.default': string;
|
|
313
313
|
/** Active Person provider class (lowercased; falls back to .default) */
|
|
314
314
|
'ngdpbase.application.persons.provider': string;
|
|
315
|
-
/** #1930: the most results `
|
|
315
|
+
/** #1930: the most results `IdentityManager.search` returns when the caller gives no limit */
|
|
316
316
|
'ngdpbase.application.persons.search.limit': number;
|
|
317
317
|
/** #1930: minutes the emailed "Is this you?" sign-up link stays usable */
|
|
318
318
|
'ngdpbase.application.persons.link-offer.ttl-minutes': number;
|
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
* @see https://schema.org/Organization
|
|
13
13
|
*/
|
|
14
14
|
/**
|
|
15
|
-
* Schema.org PostalAddress (subset used by Organization)
|
|
15
|
+
* Schema.org PostalAddress (subset used by Organization/Person).
|
|
16
|
+
*
|
|
17
|
+
* In schema.org a PostalAddress is a ContactPoint, so it carries the
|
|
18
|
+
* ContactPoint fields too: a `contactPoint[]` entry may be one (a mailing or
|
|
19
|
+
* business address, with its `contactType`, `name` and `telephone`).
|
|
16
20
|
*
|
|
17
21
|
* @see https://schema.org/PostalAddress
|
|
18
22
|
*/
|
|
19
|
-
export interface PostalAddress {
|
|
23
|
+
export interface PostalAddress extends Omit<ContactPoint, '@type'> {
|
|
20
24
|
'@type': 'PostalAddress';
|
|
21
25
|
streetAddress?: string | null;
|
|
22
26
|
addressLocality?: string | null;
|
|
@@ -32,11 +36,19 @@ export interface PostalAddress {
|
|
|
32
36
|
export interface ContactPoint {
|
|
33
37
|
'@type': 'ContactPoint';
|
|
34
38
|
contactType?: string;
|
|
39
|
+
/** Who or what this contact point reaches, e.g. an agent's or an emergency contact's name (schema.org Thing.name). */
|
|
40
|
+
name?: string;
|
|
35
41
|
email?: string;
|
|
36
42
|
telephone?: string;
|
|
37
43
|
url?: string;
|
|
38
44
|
availableLanguage?: string[];
|
|
39
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* One `contactPoint[]` entry: a ContactPoint, or a PostalAddress (schema.org's
|
|
48
|
+
* ContactPoint subtype). Typed by `contactType` (`mailing`, `business`,
|
|
49
|
+
* `agent`, `emergency`, …); the home address stays in `address`.
|
|
50
|
+
*/
|
|
51
|
+
export type ContactPointEntry = ContactPoint | PostalAddress;
|
|
40
52
|
/**
|
|
41
53
|
* Canonical Organization record.
|
|
42
54
|
*
|
|
@@ -62,8 +74,8 @@ export interface Organization {
|
|
|
62
74
|
foundingDate?: string;
|
|
63
75
|
/** Postal address */
|
|
64
76
|
address?: PostalAddress;
|
|
65
|
-
/** Contact points (support, sales, etc.) */
|
|
66
|
-
contactPoint?:
|
|
77
|
+
/** Contact points (support, sales, a mailing address, etc.) */
|
|
78
|
+
contactPoint?: ContactPointEntry[];
|
|
67
79
|
/** Other URLs that identify this org (LinkedIn, Twitter, etc.) */
|
|
68
80
|
sameAs?: string[];
|
|
69
81
|
/** Optional legacy slug or external identifier */
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* @see https://schema.org/Person
|
|
17
17
|
*/
|
|
18
|
-
import type { PostalAddress,
|
|
18
|
+
import type { PostalAddress, ContactPointEntry } from './Organization.js';
|
|
19
19
|
/**
|
|
20
20
|
* One organization a person is affiliated with (#1930): schema.org
|
|
21
21
|
* `affiliation`, with the person's role or title there when one is known.
|
|
@@ -51,8 +51,8 @@ export interface Person {
|
|
|
51
51
|
email?: string;
|
|
52
52
|
/** Postal address */
|
|
53
53
|
address?: PostalAddress;
|
|
54
|
-
/**
|
|
55
|
-
contactPoint?:
|
|
54
|
+
/** Everything but the home address: phones, a mailing or business address (a PostalAddress), an agent, an emergency contact; each typed by `contactType`. */
|
|
55
|
+
contactPoint?: ContactPointEntry[];
|
|
56
56
|
/** ISO language tags this person knows */
|
|
57
57
|
knowsLanguage?: string[];
|
|
58
58
|
/** Profile/avatar image URL */
|
|
@@ -70,7 +70,7 @@ export interface Person {
|
|
|
70
70
|
*/
|
|
71
71
|
export type PersonUpdate = Partial<Omit<Person, '@context' | '@type' | '@id' | 'identifier'>>;
|
|
72
72
|
/**
|
|
73
|
-
* What a caller gives `
|
|
73
|
+
* What a caller gives `IdentityManager.create` (#1930): the person's fields.
|
|
74
74
|
* `identifier` is left out for a person with no account (one is generated);
|
|
75
75
|
* UserManager passes the account's username when it creates the person for a
|
|
76
76
|
* new account.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Storage seam for Person records (#617)
|
|
3
3
|
*
|
|
4
4
|
* File system is the v1 default. Future providers (LDAP, external directory)
|
|
5
|
-
* implement the same interface and register without touching
|
|
5
|
+
* implement the same interface and register without touching IdentityManager.
|
|
6
6
|
*/
|
|
7
7
|
import type { BaseProvider } from './Provider.js';
|
|
8
8
|
import type { Person, PersonUpdate } from './Person.js';
|
|
@@ -20,7 +20,7 @@ export interface PersonProvider extends BaseProvider {
|
|
|
20
20
|
create(person: Person): Promise<Person>;
|
|
21
21
|
/**
|
|
22
22
|
* Write the whole record in place of the one with the same `@id` (#1930).
|
|
23
|
-
* The only write that may change `identifier`:
|
|
23
|
+
* The only write that may change `identifier`: IdentityManager uses it to join
|
|
24
24
|
* a person to an account.
|
|
25
25
|
*/
|
|
26
26
|
replace(person: Person): Promise<Person>;
|
|
@@ -54,10 +54,18 @@ export declare const AUDIT_EVENT: {
|
|
|
54
54
|
readonly CONSENT_GRANT: "consent-grant";
|
|
55
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
|
-
/** 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
|
-
readonly CONTACT_MANAGE: "contact-manage";
|
|
59
57
|
/** A page's footnote list changed — action add, import, transfer, update or delete; page content written on someone's behalf (#1233) */
|
|
60
58
|
readonly FOOTNOTE_EDIT: "footnote-edit";
|
|
59
|
+
/** Identity record or organization created, changed, deleted or linked to an account (recorded as contact-manage before #2038); metadata names the kind (person or organization), the operation and the fields changed, never their values */
|
|
60
|
+
readonly IDENTITY_MANAGE: "identity-manage";
|
|
61
|
+
/** Invitation cancelled (#2049): an administrator cancelled it or sent a new one, the email could not be sent, or the account was deleted; the link stops working */
|
|
62
|
+
readonly INVITATION_CANCEL: "invitation-cancel";
|
|
63
|
+
/** Invitation expired (#2049): the link was not used in time and was removed */
|
|
64
|
+
readonly INVITATION_EXPIRE: "invitation-expire";
|
|
65
|
+
/** Invitation sent (#2049): a one-time link emailed to a new user to set up their sign-in; user is who sent it, resource the account invited. A credential nobody knows exists is the worst case, so it is recorded before the link exists */
|
|
66
|
+
readonly INVITATION_SEND: "invitation-send";
|
|
67
|
+
/** Invitation used (#2049): the invited user opened their link and was signed in once; the link is spent */
|
|
68
|
+
readonly INVITATION_USE: "invitation-use";
|
|
61
69
|
/** A background job finished successfully */
|
|
62
70
|
readonly JOB_COMPLETED: "job-completed";
|
|
63
71
|
/** A background job failed */
|
|
@@ -61,10 +61,18 @@ export const AUDIT_EVENT = {
|
|
|
61
61
|
CONSENT_GRANT: 'consent-grant',
|
|
62
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
|
-
/** 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
|
-
CONTACT_MANAGE: 'contact-manage',
|
|
66
64
|
/** A page's footnote list changed — action add, import, transfer, update or delete; page content written on someone's behalf (#1233) */
|
|
67
65
|
FOOTNOTE_EDIT: 'footnote-edit',
|
|
66
|
+
/** Identity record or organization created, changed, deleted or linked to an account (recorded as contact-manage before #2038); metadata names the kind (person or organization), the operation and the fields changed, never their values */
|
|
67
|
+
IDENTITY_MANAGE: 'identity-manage',
|
|
68
|
+
/** Invitation cancelled (#2049): an administrator cancelled it or sent a new one, the email could not be sent, or the account was deleted; the link stops working */
|
|
69
|
+
INVITATION_CANCEL: 'invitation-cancel',
|
|
70
|
+
/** Invitation expired (#2049): the link was not used in time and was removed */
|
|
71
|
+
INVITATION_EXPIRE: 'invitation-expire',
|
|
72
|
+
/** Invitation sent (#2049): a one-time link emailed to a new user to set up their sign-in; user is who sent it, resource the account invited. A credential nobody knows exists is the worst case, so it is recorded before the link exists */
|
|
73
|
+
INVITATION_SEND: 'invitation-send',
|
|
74
|
+
/** Invitation used (#2049): the invited user opened their link and was signed in once; the link is spent */
|
|
75
|
+
INVITATION_USE: 'invitation-use',
|
|
68
76
|
/** A background job finished successfully */
|
|
69
77
|
JOB_COMPLETED: 'job-completed',
|
|
70
78
|
/** A background job failed */
|
|
@@ -139,13 +139,26 @@ export interface TokenInput extends CommonInput {
|
|
|
139
139
|
/** Only meaningful for `revoke`; ignored otherwise. */
|
|
140
140
|
revokedBy?: string;
|
|
141
141
|
}
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
/**
|
|
143
|
+
* What an attachment record says about the file. A file in an ENCRYPTED store
|
|
144
|
+
* is recorded by its id and the store only (#2048): its name, its page and its
|
|
145
|
+
* size are the store's to keep sealed, so the record carries none of them.
|
|
146
|
+
*/
|
|
147
|
+
export type AttachmentDetail = {
|
|
144
148
|
attachmentId: string;
|
|
145
149
|
filename: string;
|
|
146
150
|
pageName?: string | null;
|
|
147
151
|
sizeBytes?: number | null;
|
|
148
|
-
}
|
|
152
|
+
} | {
|
|
153
|
+
attachmentId: string;
|
|
154
|
+
sealedStore: {
|
|
155
|
+
owner: string;
|
|
156
|
+
store: string;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
export type AttachmentInput = CommonInput & {
|
|
160
|
+
op: AttachmentOp;
|
|
161
|
+
} & AttachmentDetail;
|
|
149
162
|
/**
|
|
150
163
|
* Build the audit event for a page create, edit, rename, or link rewrite.
|
|
151
164
|
*
|
|
@@ -145,18 +145,17 @@ export function buildPageViewAuditEvent(input) {
|
|
|
145
145
|
* unattended destruction is the case someone reviewing the log is looking for.
|
|
146
146
|
*/
|
|
147
147
|
export function buildAttachmentAuditEvent(input) {
|
|
148
|
-
const { op, attachmentId,
|
|
148
|
+
const { op, attachmentId, viaToken } = input;
|
|
149
149
|
const who = whoOf(input);
|
|
150
|
-
const metadata =
|
|
151
|
-
attachmentId,
|
|
152
|
-
filename,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
metadata.sizeBytes = sizeBytes;
|
|
150
|
+
const metadata = 'sealedStore' in input
|
|
151
|
+
? { attachmentId, owner: input.sealedStore.owner, store: input.sealedStore.store, sealed: true, ...tokenMetadata(viaToken), ...who.metadata }
|
|
152
|
+
: { attachmentId, filename: input.filename, ...tokenMetadata(viaToken), ...who.metadata };
|
|
153
|
+
if (!('sealedStore' in input)) {
|
|
154
|
+
if (input.pageName)
|
|
155
|
+
metadata.pageName = input.pageName;
|
|
156
|
+
if (typeof input.sizeBytes === 'number')
|
|
157
|
+
metadata.sizeBytes = input.sizeBytes;
|
|
158
|
+
}
|
|
160
159
|
const severity = op === 'delete'
|
|
161
160
|
? (viaToken ? 'high' : 'medium')
|
|
162
161
|
: (viaToken ? 'medium' : 'low');
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The contacts door's one decision and its one record (#1930).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* IdentityManager and OrganizationManager are the enforcement points for
|
|
5
5
|
* contacts. Both ask the PDP the same two questions with the caller's
|
|
6
|
-
* context: `
|
|
6
|
+
* context: `identity-read` to list, search or view, `identity-manage` to
|
|
7
7
|
* create, change, delete or link; and both record each change as the
|
|
8
|
-
* `
|
|
8
|
+
* `identity-manage` audit event. That rule lives here once, so the two doors
|
|
9
9
|
* cannot drift (operator, 2026-10-20).
|
|
10
10
|
*
|
|
11
11
|
* A refusal is thrown as {@link ContactRefusedError}, carrying the decision.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Every lookup takes a context too (#2018, operator 2026-10-23), through
|
|
17
17
|
* {@link requireRecordAccess}: a user's own identity record under
|
|
18
|
-
* `profile-manage`, anyone else's under `
|
|
18
|
+
* `profile-manage`, anyone else's under `identity-read` / `identity-manage`, and
|
|
19
19
|
* core's own reads under the system context without a policy evaluation, so
|
|
20
20
|
* resolving roles on every request stays cheap.
|
|
21
21
|
*/
|
|
@@ -27,8 +27,8 @@ type EngineLike = {
|
|
|
27
27
|
};
|
|
28
28
|
/** What a contacts door asks: `read` to list, search or view; `manage` to change. */
|
|
29
29
|
export declare const CONTACT_PERMISSION: {
|
|
30
|
-
readonly read: "
|
|
31
|
-
readonly manage: "
|
|
30
|
+
readonly read: "identity-read";
|
|
31
|
+
readonly manage: "identity-manage";
|
|
32
32
|
};
|
|
33
33
|
/** A contacts door said no. `permission` is what was refused; `decision` is the PDP's answer. */
|
|
34
34
|
export declare class ContactRefusedError extends Error {
|
|
@@ -38,7 +38,7 @@ export declare class ContactRefusedError extends Error {
|
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* Throws {@link ContactRefusedError} unless `ctx` may do `kind` (`read` asks
|
|
41
|
-
* `
|
|
41
|
+
* `identity-read`, `manage` asks `identity-manage`), and each of `also` (a link
|
|
42
42
|
* to an account also asks `user-edit`). A missing context or decision point
|
|
43
43
|
* refuses: closed, never open.
|
|
44
44
|
*/
|
|
@@ -58,7 +58,7 @@ export declare function isSystemWork(engine: EngineLike, ctx: ActorContext | nul
|
|
|
58
58
|
* - read it as core's own work ({@link isSystemWork}): allowed, not evaluated;
|
|
59
59
|
* - read or change its own identity record (the context's username is the
|
|
60
60
|
* record's `identifier`): `profile-manage`, which every user holds;
|
|
61
|
-
* - read or change any other record: `
|
|
61
|
+
* - read or change any other record: `identity-read` / `identity-manage`.
|
|
62
62
|
*
|
|
63
63
|
* `record` is the identity record (or what names it, `{ identifier }`); left
|
|
64
64
|
* out for a record that is never anyone's own (an organization, a role).
|
|
@@ -67,6 +67,13 @@ export declare function isSystemWork(engine: EngineLike, ctx: ActorContext | nul
|
|
|
67
67
|
export declare function requireRecordAccess(engine: EngineLike, ctx: ActorContext, kind: keyof typeof CONTACT_PERMISSION, record?: {
|
|
68
68
|
identifier?: unknown;
|
|
69
69
|
} | null): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* A lookup of users (#2018): `UserManager.searchUsers` asks `permission`
|
|
72
|
+
* (`search-user`) with the caller's context, and throws
|
|
73
|
+
* {@link ContactRefusedError} on a refusal. Core's own work
|
|
74
|
+
* ({@link isSystemWork}) is allowed without a policy evaluation.
|
|
75
|
+
*/
|
|
76
|
+
export declare function requireLookupPermission(engine: EngineLike, ctx: ActorContext, permission: CorePermission): Promise<void>;
|
|
70
77
|
/** What a contacts record says was done. Names fields, never their values. */
|
|
71
78
|
export interface ContactChange {
|
|
72
79
|
kind: 'person' | 'organization';
|
|
@@ -79,7 +86,7 @@ export interface ContactChange {
|
|
|
79
86
|
via?: 'admin' | 'email';
|
|
80
87
|
}
|
|
81
88
|
/**
|
|
82
|
-
* Record a contacts change as `
|
|
89
|
+
* Record a contacts change as `identity-manage`, under the caller's context.
|
|
83
90
|
* `continue`: a lost record is counted, the change stands. A system context
|
|
84
91
|
* goes through `recordSystemAction`, so the bootstrap admin's person, made
|
|
85
92
|
* before the audit sink is up, is recorded once it is.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { type PrivateStoreLayout } from './privateStorePath.js';
|
|
16
16
|
import { type UserCatalog } from './privateStoreCatalogs.js';
|
|
17
|
-
import type
|
|
17
|
+
import { type ActorContext } from '../context/ActorContext.js';
|
|
18
18
|
import type { LinkPublicKey, Lockbox } from './shareLockbox.js';
|
|
19
19
|
export declare function unlockPrivateStores(sessionId: string, username: string, kek: Buffer): void;
|
|
20
20
|
export declare function getSessionUserIndex(sessionId: string): UserCatalog | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jwilleke/ngdpbase-types",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.30.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",
|