@jwilleke/ngdpbase-types 5.30.1 → 5.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/packages/ngdpbase-types/index.d.ts +4 -2
- package/lib/src/managers/AuthManager.d.ts +96 -1
- package/lib/src/managers/BackgroundJobManager.d.ts +9 -0
- package/lib/src/managers/IdentityManager.d.ts +109 -4
- package/lib/src/managers/OrganizationManager.d.ts +9 -5
- package/lib/src/managers/ReviewManager.d.ts +108 -0
- package/lib/src/managers/UserManager.d.ts +39 -15
- package/lib/src/providers/AuthentikBearerAuthProvider.d.ts +0 -1
- package/lib/src/providers/BaseAuthProvider.d.ts +7 -0
- package/lib/src/providers/CloudflareAccessAuthProvider.d.ts +0 -1
- package/lib/src/providers/MagicLinkAuthProvider.d.ts +12 -0
- package/lib/src/providers/databaseMigrations.js +30 -0
- package/lib/src/types/Config.d.ts +10 -0
- package/lib/src/types/Organization.d.ts +3 -0
- package/lib/src/types/Person.d.ts +84 -6
- package/lib/src/types/Review.d.ts +67 -0
- package/lib/src/types/express.d.ts +3 -1
- package/lib/src/utils/addonHost.d.ts +19 -0
- package/lib/src/utils/addonHost.js +31 -1
- package/lib/src/utils/auditEventNames.generated.d.ts +12 -0
- package/lib/src/utils/auditEventNames.generated.js +12 -0
- package/lib/src/utils/languages.d.ts +9 -0
- package/lib/src/utils/phoneNumbers.d.ts +38 -0
- package/lib/src/utils/roleGrants.d.ts +18 -3
- package/lib/src/utils/sessionSecurity.d.ts +116 -0
- package/lib/src/utils/userCreateError.d.ts +4 -2
- package/package.json +1 -1
|
@@ -24,6 +24,8 @@ 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
26
|
export type { KeyedSerial } from '../../src/utils/keyedSerial.js';
|
|
27
|
+
export type { AddonReviews, ReviewDecision, ReviewHandler, ReviewHandlerResult, ReviewRequest, ReviewRequestInput, ReviewStatus } from '../../src/types/Review.js';
|
|
28
|
+
export type { default as ReviewManager, ReviewRefusedError, ReviewsUnavailableError, ReviewNotOpenError, ReviewInputError, ReviewHandlerFailedError } from '../../src/managers/ReviewManager.js';
|
|
27
29
|
export type { AuditEvent } from '../../src/utils/auditEvents.js';
|
|
28
30
|
export type { default as BackgroundJobManager, JobDefinition, JobResult, JobRunContext } from '../../src/managers/BackgroundJobManager.js';
|
|
29
31
|
export type { default as DatabaseManager, AddonDatabaseOptions } from '../../src/managers/DatabaseManager.js';
|
|
@@ -41,12 +43,12 @@ export type { IceShim, IceReceiver, IceItem, IceKind, IceWarning, ShimDirection,
|
|
|
41
43
|
export type { default as UserManager, UserUpdateInput, UserUpdateFn } from '../../src/managers/UserManager.js';
|
|
42
44
|
export type { User, UserPreferences } from '../../src/types/User.js';
|
|
43
45
|
export type { default as AuthManager, InviteOutcome, InvitationView } from '../../src/managers/AuthManager.js';
|
|
44
|
-
export type { default as IdentityManager, LinkOfferOutcome } from '../../src/managers/IdentityManager.js';
|
|
46
|
+
export type { default as IdentityManager, LinkOfferOutcome, AffiliationQuery, IdentitySearch, IdentityMatch } from '../../src/managers/IdentityManager.js';
|
|
45
47
|
export type {
|
|
46
48
|
/** @deprecated Renamed `IdentityManager` (#2038); this alias goes after one release. */
|
|
47
49
|
default as PersonManager } from '../../src/managers/IdentityManager.js';
|
|
48
50
|
export type { default as OrganizationManager } from '../../src/managers/OrganizationManager.js';
|
|
49
|
-
export type { Person, PersonAffiliation, PersonInput, PersonUpdate } from '../../src/types/Person.js';
|
|
51
|
+
export type { Person, PersonAffiliation, LegacyPersonAffiliation, PersonChange, PersonInput, PersonUpdate } from '../../src/types/Person.js';
|
|
50
52
|
export type { Organization, OrganizationInput, OrganizationUpdate, ContactPoint, ContactPointEntry, PostalAddress } from '../../src/types/Organization.js';
|
|
51
53
|
export type { ContactRefusedError } from '../../src/utils/contactAccess.js';
|
|
52
54
|
export type { default as RoleManager } from '../../src/managers/RoleManager.js';
|
|
@@ -34,7 +34,7 @@ import type { BackupData, BackupFolder } from './BaseManager.js';
|
|
|
34
34
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
35
35
|
import type { PermissionSubject } from './UserManager.js';
|
|
36
36
|
import type { JobContext } from '../context/JobContext.js';
|
|
37
|
-
import type { Aal, AuthProvider, AuthInitiateContext, AuthVerifyCredentials, PhishingResistance, ViaToken } from '../providers/BaseAuthProvider.js';
|
|
37
|
+
import type { Aal, AuthProvider, AuthInitiateContext, AuthVerifyCredentials, PhishingResistance, ReportedAssurance, ViaToken } from '../providers/BaseAuthProvider.js';
|
|
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';
|
|
@@ -229,6 +229,26 @@ export interface PendingConnection {
|
|
|
229
229
|
/** Where the sign-in that follows was going; a declined connection goes there. */
|
|
230
230
|
returnTo?: string;
|
|
231
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* An outside sign-in whose verified email several accounts here share
|
|
234
|
+
* (#2073): held in the session while the person chooses which account, asked
|
|
235
|
+
* every time and never remembered (no identity is connected). Single use.
|
|
236
|
+
*/
|
|
237
|
+
export interface OutsideChoice {
|
|
238
|
+
providerId: string;
|
|
239
|
+
/** The provider's verified email. */
|
|
240
|
+
email: string;
|
|
241
|
+
/** The accounts on that email, by username, when the callback came back. */
|
|
242
|
+
choices: string[];
|
|
243
|
+
/** The provider's name, for the page. */
|
|
244
|
+
displayName: string;
|
|
245
|
+
/** What the provider reported about its own sign-in (#1633), kept for the level it counts for. */
|
|
246
|
+
reported: ReportedAssurance;
|
|
247
|
+
/** Where the sign-in was going. */
|
|
248
|
+
returnTo: string;
|
|
249
|
+
/** Epoch ms. */
|
|
250
|
+
expiresAt: number;
|
|
251
|
+
}
|
|
232
252
|
/** How an outside sign-in's callback ended (#1743), for the route to answer. */
|
|
233
253
|
export type OutsideSignIn =
|
|
234
254
|
/** Signed in (`created`: an account was made for it first); `next` is where to go. */
|
|
@@ -241,6 +261,10 @@ export type OutsideSignIn =
|
|
|
241
261
|
outcome: 'connect-first';
|
|
242
262
|
error?: string;
|
|
243
263
|
}
|
|
264
|
+
/** #2073: several accounts share the verified email; held for the person to choose one (`outsideChoice`). */
|
|
265
|
+
| {
|
|
266
|
+
outcome: 'choose-account';
|
|
267
|
+
}
|
|
244
268
|
/** Refused; nothing says whether an account exists. */
|
|
245
269
|
| {
|
|
246
270
|
outcome: 'refused';
|
|
@@ -601,6 +625,22 @@ declare class AuthManager extends BaseManager {
|
|
|
601
625
|
private createAccountFor;
|
|
602
626
|
/** Delete an account created for an outside sign-in that could not be finished (#1743), through UserManager, which closes its access. */
|
|
603
627
|
private removeCreatedAccount;
|
|
628
|
+
/**
|
|
629
|
+
* The accounts an outside sign-in to a shared email offers (#2073), for the
|
|
630
|
+
* page asking which; null when none is waiting, and an expired one is dropped.
|
|
631
|
+
*/
|
|
632
|
+
outsideChoice(req: Request): {
|
|
633
|
+
displayName: string;
|
|
634
|
+
choices: string[];
|
|
635
|
+
} | null;
|
|
636
|
+
/**
|
|
637
|
+
* Finish an outside sign-in to a shared email (#2073): `account` must be one
|
|
638
|
+
* the sign-in offered, still hold that email, be active, and be allowed to
|
|
639
|
+
* use the provider. The choice is used once, connects nothing and is not
|
|
640
|
+
* remembered; the next sign-in asks again. A sign-in at AAL1 takes the
|
|
641
|
+
* second step, as any outside sign-in does (#1532).
|
|
642
|
+
*/
|
|
643
|
+
finishOutsideChoice(req: Request, account: string): Promise<OutsideSignIn>;
|
|
604
644
|
/**
|
|
605
645
|
* The person's pending connection, for the sign-in page's notice and the
|
|
606
646
|
* confirmation page; an expired one is dropped (#1743).
|
|
@@ -1273,6 +1313,10 @@ declare class AuthManager extends BaseManager {
|
|
|
1273
1313
|
private pendingSignIns;
|
|
1274
1314
|
private approvalTokens;
|
|
1275
1315
|
private enrolTokens;
|
|
1316
|
+
/** #2073: links confirming a user's new email, by token; one waiting per account. In memory: a restart drops them. */
|
|
1317
|
+
private emailChangeTokens;
|
|
1318
|
+
/** #2073: when each account's email-change links were sent, for the cooldown and the cap. In memory. */
|
|
1319
|
+
private emailChangeSends;
|
|
1276
1320
|
/** Mail can carry an email-link factor: a mail transport and an explicit base-url for the link. */
|
|
1277
1321
|
private mailForLinks;
|
|
1278
1322
|
private linkBase;
|
|
@@ -1390,6 +1434,52 @@ declare class AuthManager extends BaseManager {
|
|
|
1390
1434
|
startEmailFactorEnrolment(ctx: PermissionSubject, username: string): Promise<'sent' | 'no-email' | 'unavailable' | 'already'>;
|
|
1391
1435
|
/** Finish enrolling from the emailed link, by the same signed-in person. Single use. */
|
|
1392
1436
|
confirmEmailFactorEnrolment(ctx: PermissionSubject, token: string): Promise<boolean>;
|
|
1437
|
+
/**
|
|
1438
|
+
* A user asks to change their own email (#2073, operator 2026-10-25): it
|
|
1439
|
+
* changes only when they click a link sent to the new address. Until then
|
|
1440
|
+
* the account keeps its old email. The link is single use, lasts
|
|
1441
|
+
* `ngdpbase.auth.email-change.ttl-minutes` (60), is bound to the account and
|
|
1442
|
+
* the new address, and replaces any link still waiting for that account. The
|
|
1443
|
+
* old address, when there is one, is told a change was asked, with no link.
|
|
1444
|
+
* Recorded as `emailchange-request`, naming the account, never an address.
|
|
1445
|
+
*
|
|
1446
|
+
* Only the account itself asks (its own context, signed in or just
|
|
1447
|
+
* registered). `unavailable`: this site cannot send email now, so nothing
|
|
1448
|
+
* is sent and an administrator can change it instead. `failed`: the send
|
|
1449
|
+
* failed and no link is waiting. `unchanged`: the address is the one the
|
|
1450
|
+
* account already has. `throttled`: the account asked too soon after its
|
|
1451
|
+
* last link (`ngdpbase.auth.email-change.cooldown-seconds`, 60) or too often
|
|
1452
|
+
* in the window (`ngdpbase.auth.email-change.max-per-window` links per
|
|
1453
|
+
* `ngdpbase.auth.email-change.window-minutes`, 5 per 60); no mail is sent.
|
|
1454
|
+
*/
|
|
1455
|
+
requestEmailChange(ctx: ActorContext, username: string, newEmail: string, via: 'profile' | 'registration'): Promise<'sent' | 'unchanged' | 'unavailable' | 'failed' | 'throttled'>;
|
|
1456
|
+
/** The new address waiting to be confirmed for `username` (#2073), for the profile to show; null when none. */
|
|
1457
|
+
pendingEmailChange(username: string): string | null;
|
|
1458
|
+
/** What a confirmation link would change (#2073), for its page; null for an unknown, used or expired link. */
|
|
1459
|
+
emailChangeOffer(token: string): {
|
|
1460
|
+
username: string;
|
|
1461
|
+
address: string;
|
|
1462
|
+
} | null;
|
|
1463
|
+
/**
|
|
1464
|
+
* Confirm a new email from its emailed link (#2073). Single use: the link is
|
|
1465
|
+
* spent first. The rules are checked again now: the account still exists,
|
|
1466
|
+
* is on, and is not a shared account whose email only an administrator
|
|
1467
|
+
* changes; and UserManager's own (an administrator's email is never
|
|
1468
|
+
* shared). Saved through UserManager under the system context naming the
|
|
1469
|
+
* account (the link is the authority), which keeps the identity record in
|
|
1470
|
+
* step. Nobody is signed in. Recorded as `emailchange-confirm`. A user who
|
|
1471
|
+
* had no email yet (just registered) is then offered their contact record,
|
|
1472
|
+
* as at sign-up (#1930).
|
|
1473
|
+
*/
|
|
1474
|
+
confirmEmailChange(token: string): Promise<{
|
|
1475
|
+
outcome: 'changed';
|
|
1476
|
+
username: string;
|
|
1477
|
+
} | {
|
|
1478
|
+
outcome: 'expired';
|
|
1479
|
+
} | {
|
|
1480
|
+
outcome: 'refused';
|
|
1481
|
+
message: string;
|
|
1482
|
+
}>;
|
|
1393
1483
|
/**
|
|
1394
1484
|
* Initiate a challenge-based auth flow (magic link email, OAuth redirect).
|
|
1395
1485
|
*/
|
|
@@ -1473,6 +1563,11 @@ declare class AuthManager extends BaseManager {
|
|
|
1473
1563
|
* here would make an unbindable flow look like an attack.
|
|
1474
1564
|
*/
|
|
1475
1565
|
getDeviceState(providerId: string, handle: string): string | null;
|
|
1566
|
+
/**
|
|
1567
|
+
* The accounts an emailed link to a shared address offers (#2073), by
|
|
1568
|
+
* username, or null when the link names one account. Read without consuming.
|
|
1569
|
+
*/
|
|
1570
|
+
getAccountChoices(providerId: string, handle: string): string[] | null;
|
|
1476
1571
|
/**
|
|
1477
1572
|
* Create the account behind a first-time credential (#1026, #1049).
|
|
1478
1573
|
*
|
|
@@ -327,6 +327,15 @@ declare class BackgroundJobManager extends BaseManager {
|
|
|
327
327
|
private startDrain;
|
|
328
328
|
/** Every scheduled job, its state and its recent history. */
|
|
329
329
|
listScheduledJobs(): Promise<ScheduledJobView[]>;
|
|
330
|
+
/**
|
|
331
|
+
* Every scheduled job's id and display name, as `listScheduledJobs` names
|
|
332
|
+
* them, without reading their state — for the admin roles page's Jobs part
|
|
333
|
+
* (#2067).
|
|
334
|
+
*/
|
|
335
|
+
scheduledJobNames(): Array<{
|
|
336
|
+
id: string;
|
|
337
|
+
displayName: string;
|
|
338
|
+
}>;
|
|
330
339
|
/**
|
|
331
340
|
* One scheduled job as `listScheduledJobs` shows it, without its run
|
|
332
341
|
* history, or null when no job of that id is scheduled. For a manager that
|
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import BaseManager, { type BackupFolder, type ManagerStats } from './BaseManager.js';
|
|
2
2
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
3
|
-
import type { Person,
|
|
3
|
+
import type { ContactDetailsInput, Person, PersonAffiliation, PersonChange, PersonInput } from '../types/Person.js';
|
|
4
4
|
import type { PersonProvider } from '../types/PersonProvider.js';
|
|
5
5
|
import { type ActorContext } from '../context/ActorContext.js';
|
|
6
|
+
/** What `search(ctx, { affiliation })` looks for (#2039). */
|
|
7
|
+
export interface AffiliationQuery {
|
|
8
|
+
/** The organization, by its `@id` (or `{ '@id' }`). */
|
|
9
|
+
memberOf: string | {
|
|
10
|
+
'@id': string;
|
|
11
|
+
};
|
|
12
|
+
/** The title, compared without case or surrounding spaces. Left out: any title, or none. */
|
|
13
|
+
title?: string;
|
|
14
|
+
/** True (the default): only affiliations held today, the first and last day included. False: any, past or future too. */
|
|
15
|
+
current?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** A search by affiliation (#2039). `limit` as for a text search; 0 is no limit. */
|
|
18
|
+
export interface IdentitySearch {
|
|
19
|
+
affiliation: AffiliationQuery;
|
|
20
|
+
limit?: number;
|
|
21
|
+
}
|
|
22
|
+
/** One identity a search by affiliation found: a user or a contact, and the affiliations that matched. */
|
|
23
|
+
export interface IdentityMatch {
|
|
24
|
+
identity: Person;
|
|
25
|
+
/** True for a user (an identity with credentials), false for a contact. */
|
|
26
|
+
hasCredentials: boolean;
|
|
27
|
+
affiliations: PersonAffiliation[];
|
|
28
|
+
}
|
|
6
29
|
/** What the confirmation page shows: the person the link would join. */
|
|
7
30
|
export interface LinkOfferDetails {
|
|
8
31
|
personName: string | null;
|
|
@@ -52,6 +75,7 @@ declare class IdentityManager extends BaseManager {
|
|
|
52
75
|
private storageDir;
|
|
53
76
|
private byIdentifierCache;
|
|
54
77
|
private byIdCache;
|
|
78
|
+
private allCache;
|
|
55
79
|
private metricsRef;
|
|
56
80
|
/** Sign-up link offers by token (#1930). In memory: a restart drops them, and an administrator links by hand. */
|
|
57
81
|
private linkOffers;
|
|
@@ -59,6 +83,18 @@ declare class IdentityManager extends BaseManager {
|
|
|
59
83
|
private readonly accountRecords;
|
|
60
84
|
constructor(engine: WikiEngine);
|
|
61
85
|
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Once, at start-up (#2039): every identity record still holding an
|
|
88
|
+
* affiliation in the old `{ organization, role? }` shape is rewritten as
|
|
89
|
+
* schema.org `OrganizationRole` (organization → `memberOf`, role →
|
|
90
|
+
* `roleName`), each rewrite recorded as `identity-manage` (`person-update`,
|
|
91
|
+
* field `affiliation`) under the system context. Nothing is dropped: any
|
|
92
|
+
* other field an old entry carries is kept on it. A record with no old entry
|
|
93
|
+
* is not touched, so a second start finds nothing to do. A record that
|
|
94
|
+
* cannot be rewritten is logged and left as it is; reads convert it anyway.
|
|
95
|
+
* Runs before any request, so nothing else writes these records meanwhile.
|
|
96
|
+
*/
|
|
97
|
+
private convertLegacyAffiliations;
|
|
62
98
|
/** `ngdpbase.application.persons.storagedir`, resolved: where the records are. */
|
|
63
99
|
private configuredStorageDir;
|
|
64
100
|
/**
|
|
@@ -79,6 +115,8 @@ declare class IdentityManager extends BaseManager {
|
|
|
79
115
|
*/
|
|
80
116
|
getByIdentifier(ctx: ActorContext, identifier: string): Promise<Person | null>;
|
|
81
117
|
private cachedById;
|
|
118
|
+
/** Every record, affiliations in the current shape, read from the provider once per write (#2039). */
|
|
119
|
+
private allRecords;
|
|
82
120
|
private cachedByIdentifier;
|
|
83
121
|
/** #1006: how many people this instance holds. Count only, never records. */
|
|
84
122
|
getManagerStats(): Promise<ManagerStats>;
|
|
@@ -93,11 +131,20 @@ declare class IdentityManager extends BaseManager {
|
|
|
93
131
|
* `query`, ignoring case; an empty query matches everyone. `accountless`
|
|
94
132
|
* narrows to persons with no account. At most `limit` results
|
|
95
133
|
* (`ngdpbase.application.persons.search.limit` by default).
|
|
134
|
+
*
|
|
135
|
+
* Or by affiliation (#2039): `search(ctx, { affiliation: { memberOf, title?,
|
|
136
|
+
* current? } })` finds every identity, users and contacts alike, with an
|
|
137
|
+
* affiliation to that organization: with that title when one is given, and
|
|
138
|
+
* held today (the first and last day included) unless `current` is false.
|
|
139
|
+
* Each match says whether it has credentials and which affiliations matched.
|
|
140
|
+
* Checked like any read of others' records: `identity-read`.
|
|
96
141
|
*/
|
|
97
142
|
search(ctx: ActorContext, query: string, options?: {
|
|
98
143
|
limit?: number;
|
|
99
144
|
accountless?: boolean;
|
|
100
145
|
}): Promise<Person[]>;
|
|
146
|
+
search(ctx: ActorContext, query: IdentitySearch): Promise<IdentityMatch[]>;
|
|
147
|
+
private searchByAffiliation;
|
|
101
148
|
/**
|
|
102
149
|
* Create a person. With no `identifier` the person has no account and gets
|
|
103
150
|
* `person:<uuid>`. An `identifier` must be an existing account that has no
|
|
@@ -108,8 +155,48 @@ declare class IdentityManager extends BaseManager {
|
|
|
108
155
|
* Change a person's fields, affiliations included. `@id` and `identifier`
|
|
109
156
|
* never change here. The caller's own identity record under
|
|
110
157
|
* `profile-manage`, anyone else's under `identity-manage` (#2018).
|
|
158
|
+
*
|
|
159
|
+
* Affiliations (#2039): one with the site's own organization is a fact, so
|
|
160
|
+
* adding, changing or removing one also asks `identity-manage`, even on the
|
|
161
|
+
* caller's own record; one with any other organization is a claim, which a
|
|
162
|
+
* user may set on their own record under `profile-manage`.
|
|
163
|
+
*/
|
|
164
|
+
update(ctx: ActorContext, id: string, patch: PersonChange): Promise<Person | null>;
|
|
165
|
+
/**
|
|
166
|
+
* The parts of an identity record a user keeps on `/profile` (#2073):
|
|
167
|
+
* given and family name, phones, the emergency contact, the home and
|
|
168
|
+
* mailing addresses, languages, a photo and links. A part left out of
|
|
169
|
+
* `input` is not touched; a text given empty is removed.
|
|
170
|
+
*
|
|
171
|
+
* - Phones are stored in E.164 with any extension apart (`extension`),
|
|
172
|
+
* read with the country chosen beside each one (the configured default,
|
|
173
|
+
* `ngdpbase.application.persons.default-country`, when none is given); a
|
|
174
|
+
* number typed with a leading `+` keeps its own country code.
|
|
175
|
+
* - A phone, the emergency contact and the mailing address each take the
|
|
176
|
+
* place of the record's first entry of their kind in `contactPoint`
|
|
177
|
+
* (a ContactPoint by `contactType`, the mailing address as the
|
|
178
|
+
* `mailing` PostalAddress), keeping that entry's other fields; every
|
|
179
|
+
* other entry, an add-on's included, is kept as it is.
|
|
180
|
+
* - Languages are stored as tags ("es"), given by name or tag.
|
|
181
|
+
*
|
|
182
|
+
* Everything is checked before anything is written: a bad part throws
|
|
183
|
+
* {@link ContactDetailsInvalidError}, naming the part and the rule, never
|
|
184
|
+
* the value. The access and the record are {@link update}'s: the caller's
|
|
185
|
+
* own record under `profile-manage`, anyone else's under `identity-manage`,
|
|
186
|
+
* recorded as an `identity-manage` update naming the fields that changed.
|
|
187
|
+
* Nothing changed, nothing is written or recorded.
|
|
188
|
+
*/
|
|
189
|
+
updateContactDetails(ctx: ActorContext, id: string, input: ContactDetailsInput): Promise<Person | null>;
|
|
190
|
+
/**
|
|
191
|
+
* Check `input` as {@link updateContactDetails} would, writing nothing
|
|
192
|
+
* (#2073): throws {@link ContactDetailsInvalidError} for a bad part. For a
|
|
193
|
+
* caller that writes something else first (the /profile card writes the
|
|
194
|
+
* account's name and email), so a bad detail refuses the whole save before
|
|
195
|
+
* anything is written.
|
|
111
196
|
*/
|
|
112
|
-
|
|
197
|
+
checkContactDetails(input: ContactDetailsInput): void;
|
|
198
|
+
/** The country a phone or address is read with when none is chosen (#2073): `ngdpbase.application.persons.default-country`, `US` when unset or unknown. */
|
|
199
|
+
defaultCountry(): string;
|
|
113
200
|
delete(ctx: ActorContext, id: string): Promise<boolean>;
|
|
114
201
|
/**
|
|
115
202
|
* An administrator joins a person who has no account to an account (#1930),
|
|
@@ -150,10 +237,18 @@ declare class IdentityManager extends BaseManager {
|
|
|
150
237
|
*/
|
|
151
238
|
private join;
|
|
152
239
|
/**
|
|
153
|
-
* Each affiliation
|
|
154
|
-
*
|
|
240
|
+
* Each affiliation is a schema.org `OrganizationRole` (#2039): `memberOf` an
|
|
241
|
+
* organization that exists, an optional `roleName` (the title, trimmed free
|
|
242
|
+
* text) and optional `startDate` / `endDate` (`YYYY-MM-DD`, the start not
|
|
243
|
+
* after the end; a null end is an ongoing one, stored without it). An entry
|
|
244
|
+
* in the old `{ organization, role? }` shape is converted (one release, for
|
|
245
|
+
* add-ons not yet switched). Throws on any other shape. The organization is
|
|
246
|
+
* looked up as core's own check, under the system context, so a user may
|
|
247
|
+
* name one on their own record without `identity-read`.
|
|
155
248
|
*/
|
|
156
249
|
private checkedAffiliations;
|
|
250
|
+
/** The site's own organization's `@id` (`OrganizationManager.getInstallOrg`): an affiliation with it is a fact (#2039). */
|
|
251
|
+
private siteOrganizationId;
|
|
157
252
|
private configNumber;
|
|
158
253
|
/**
|
|
159
254
|
* Clear all caches. Called on every write — the simplest correct
|
|
@@ -181,5 +276,15 @@ declare class IdentityManager extends BaseManager {
|
|
|
181
276
|
private getMetrics;
|
|
182
277
|
private normalizeProviderName;
|
|
183
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* A contact detail given to {@link IdentityManager.updateContactDetails} is not valid (#2073); nothing was written.
|
|
281
|
+
* `part` is which detail (a phone's `contactType`, `emergency`, `address`, `mailing`, `knowsLanguage`, `image`,
|
|
282
|
+
* `sameAs`, `name`), `field` which box of it. The message names the rule, never the value given: it reaches URLs and logs.
|
|
283
|
+
*/
|
|
284
|
+
export declare class ContactDetailsInvalidError extends Error {
|
|
285
|
+
readonly part: string;
|
|
286
|
+
readonly field: string;
|
|
287
|
+
constructor(part: string, field: string, message: string);
|
|
288
|
+
}
|
|
184
289
|
export { ContactRefusedError } from '../utils/contactAccess.js';
|
|
185
290
|
export default IdentityManager;
|
|
@@ -2,7 +2,7 @@ import BaseManager, { type BackupFolder, type ManagerStats } from './BaseManager
|
|
|
2
2
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
3
3
|
import type { Organization, OrganizationInput, OrganizationUpdate } from '../types/Organization.js';
|
|
4
4
|
import type { OrganizationProvider } from '../types/OrganizationProvider.js';
|
|
5
|
-
import type { Person } from '../types/Person.js';
|
|
5
|
+
import type { Person, PersonAffiliation } from '../types/Person.js';
|
|
6
6
|
import type { ActorContext } from '../context/ActorContext.js';
|
|
7
7
|
/**
|
|
8
8
|
* Subset of install-form data this manager needs to seed the anchor org.
|
|
@@ -107,13 +107,17 @@ declare class OrganizationManager extends BaseManager {
|
|
|
107
107
|
limit?: number;
|
|
108
108
|
}): Promise<Organization[]>;
|
|
109
109
|
/**
|
|
110
|
-
* The
|
|
111
|
-
* `affiliation` (#1930): the link is stored
|
|
112
|
-
*
|
|
110
|
+
* The identities affiliated with an organization, users and contacts alike,
|
|
111
|
+
* read from each identity record's `affiliation` (#1930): the link is stored
|
|
112
|
+
* once, on the identity record. One entry per affiliation, each with its
|
|
113
|
+
* schema.org `OrganizationRole` (title as `roleName`, dates) (#2039), past
|
|
114
|
+
* and future ones included; `IdentityManager.search(ctx, { affiliation })`
|
|
115
|
+
* answers who holds one today. Searching identities is IdentityManager's,
|
|
116
|
+
* so this asks it.
|
|
113
117
|
*/
|
|
114
118
|
people(ctx: ActorContext, organizationId: string): Promise<Array<{
|
|
115
119
|
person: Person;
|
|
116
|
-
|
|
120
|
+
affiliation: PersonAffiliation;
|
|
117
121
|
}>>;
|
|
118
122
|
/**
|
|
119
123
|
* Create an organization. `name` is required; `@id` defaults to the `url`,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import BaseManager from './BaseManager.js';
|
|
2
|
+
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
3
|
+
import type { ActorContext } from '../context/ActorContext.js';
|
|
4
|
+
import type { Decision } from '../types/Policy.js';
|
|
5
|
+
import type { ReviewDecision, ReviewHandler, ReviewHandlerResult, ReviewRequest, ReviewRequestInput, ReviewStatus } from '../types/Review.js';
|
|
6
|
+
export type { ReviewDecision, ReviewHandler, ReviewHandlerResult, ReviewRequest, ReviewRequestInput, ReviewStatus };
|
|
7
|
+
/** The door said no: the caller may not decide (or see) this request. `decision` is the PDP's answer. */
|
|
8
|
+
export declare class ReviewRefusedError extends Error {
|
|
9
|
+
readonly permission: string;
|
|
10
|
+
readonly decision: Decision;
|
|
11
|
+
readonly name = "ReviewRefusedError";
|
|
12
|
+
constructor(permission: string, decision: Decision);
|
|
13
|
+
}
|
|
14
|
+
/** Requests cannot be kept on this site right now; the message says why and how to fix it. */
|
|
15
|
+
export declare class ReviewsUnavailableError extends Error {
|
|
16
|
+
readonly name = "ReviewsUnavailableError";
|
|
17
|
+
}
|
|
18
|
+
/** A request that is not open any more (decided already, or by someone else just now). */
|
|
19
|
+
export declare class ReviewNotOpenError extends Error {
|
|
20
|
+
readonly name = "ReviewNotOpenError";
|
|
21
|
+
}
|
|
22
|
+
/** What was filed is not a valid request; the message says what is wrong. */
|
|
23
|
+
export declare class ReviewInputError extends Error {
|
|
24
|
+
readonly name = "ReviewInputError";
|
|
25
|
+
}
|
|
26
|
+
/** The handler did not carry out an approved request; it stays open with `reason`. */
|
|
27
|
+
export declare class ReviewHandlerFailedError extends Error {
|
|
28
|
+
readonly request: ReviewRequest;
|
|
29
|
+
readonly reason: string;
|
|
30
|
+
readonly name = "ReviewHandlerFailedError";
|
|
31
|
+
constructor(request: ReviewRequest, reason: string);
|
|
32
|
+
}
|
|
33
|
+
declare class ReviewManager extends BaseManager {
|
|
34
|
+
readonly description = "Requests for review: filed by add-ons, decided by the holders of a permission, carried out by the add-on (#2072)";
|
|
35
|
+
private db;
|
|
36
|
+
/** Why the door is closed, in plain words for an administrator; null while it is open. */
|
|
37
|
+
private unavailable;
|
|
38
|
+
/** Handler id → its add-on and the function. */
|
|
39
|
+
private readonly handlers;
|
|
40
|
+
/** One decision at a time per request, so two reviewers cannot both run its handler (#2015). */
|
|
41
|
+
private readonly serial;
|
|
42
|
+
constructor(engine: WikiEngine);
|
|
43
|
+
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
44
|
+
/** Why requests cannot be filed or decided on this site, or null when they can. */
|
|
45
|
+
getUnavailableReason(): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Register the handler that carries out an approved request (#2072). Core
|
|
48
|
+
* calls this for an add-on through `host.registerReviewHandler`, which binds
|
|
49
|
+
* `owner` to the add-on's slug: the id must be `<owner>.<name>`, so one
|
|
50
|
+
* add-on can never register, or replace, another's. Registering the same id
|
|
51
|
+
* again (a restart of the add-on) replaces it.
|
|
52
|
+
*/
|
|
53
|
+
registerHandler(owner: string, handlerId: string, run: ReviewHandler): void;
|
|
54
|
+
/** Forget an add-on's handlers (it was turned off): its open requests then fail to approve, saying so. */
|
|
55
|
+
unregisterHandlers(owner: string): void;
|
|
56
|
+
/**
|
|
57
|
+
* File a request. `ctx` is whoever the request is filed for (a public
|
|
58
|
+
* form's anonymous visitor included); filing asks no permission, because
|
|
59
|
+
* only add-on code reaches this, and an add-on files only for a handler it
|
|
60
|
+
* registered. Holders of `decidePermission` are told, without waiting.
|
|
61
|
+
*
|
|
62
|
+
* @throws ReviewInputError when the request is malformed: a kind or handler
|
|
63
|
+
* outside the handler's add-on, an unknown or ungrantable permission, no
|
|
64
|
+
* subject or summary, or details too large
|
|
65
|
+
* @throws ReviewsUnavailableError when requests cannot be kept here
|
|
66
|
+
*/
|
|
67
|
+
fileRequest(ctx: ActorContext, input: ReviewRequestInput): Promise<ReviewRequest>;
|
|
68
|
+
/**
|
|
69
|
+
* The open requests `ctx` may decide, oldest first: each asked of the PDP
|
|
70
|
+
* by its permission, with the caller's context, never a role. At most
|
|
71
|
+
* `ngdpbase.reviews.list-limit` of them.
|
|
72
|
+
*/
|
|
73
|
+
listOpen(ctx: ActorContext): Promise<ReviewRequest[]>;
|
|
74
|
+
/**
|
|
75
|
+
* One request, when `ctx` may decide it (open or decided); null when there
|
|
76
|
+
* is no such request.
|
|
77
|
+
*
|
|
78
|
+
* @throws ReviewRefusedError when the caller does not hold its permission
|
|
79
|
+
*/
|
|
80
|
+
get(ctx: ActorContext, id: string): Promise<ReviewRequest | null>;
|
|
81
|
+
/**
|
|
82
|
+
* Approve or reject an open request, as `ctx`.
|
|
83
|
+
*
|
|
84
|
+
* - Reject: the request is marked rejected with the note; no handler runs.
|
|
85
|
+
* - Approve: the add-on's handler runs with `ctx`. When it returns, the
|
|
86
|
+
* request is approved with the handler's summary. When it throws (or is
|
|
87
|
+
* not registered), the request stays open with the reason and the time,
|
|
88
|
+
* and {@link ReviewHandlerFailedError} is thrown so the reviewer sees it.
|
|
89
|
+
*
|
|
90
|
+
* @throws ReviewRefusedError when `ctx` may not decide it (hand to `enforceDeny`)
|
|
91
|
+
* @throws ReviewNotOpenError when it is decided already, or does not exist
|
|
92
|
+
*/
|
|
93
|
+
decide(ctx: ActorContext, id: string, decision: ReviewDecision, note?: string): Promise<ReviewRequest>;
|
|
94
|
+
private open;
|
|
95
|
+
private settings;
|
|
96
|
+
private decider;
|
|
97
|
+
/** A whole number of 0 or more from the configuration, or the default. */
|
|
98
|
+
private number;
|
|
99
|
+
/** Throws {@link ReviewRefusedError} unless `ctx` may decide a request with this permission. No context or no PDP refuses. */
|
|
100
|
+
private requireDecider;
|
|
101
|
+
/** Remove decided requests older than `ngdpbase.reviews.retention-days` (0 keeps them). One indexed delete. */
|
|
102
|
+
private removeExpired;
|
|
103
|
+
/** Tell the holders of the request's permission. Never throws: a notice that cannot be sent is logged. */
|
|
104
|
+
private notifyDeciders;
|
|
105
|
+
/** The audit record: who, which request, its kind, handler and permission — names, never the summary, details or note. */
|
|
106
|
+
private record;
|
|
107
|
+
}
|
|
108
|
+
export default ReviewManager;
|
|
@@ -222,6 +222,8 @@ declare class UserManager extends BaseManager {
|
|
|
222
222
|
private passwordSalt?;
|
|
223
223
|
/** One read-merge-write at a time per account, keyed by the normalized username (#2025). */
|
|
224
224
|
private readonly accountWrites;
|
|
225
|
+
/** Whether this process has already reported the withheld session cookie (#2071). */
|
|
226
|
+
private sessionCookieWithheldReported;
|
|
225
227
|
/**
|
|
226
228
|
* Creates a new UserManager instance
|
|
227
229
|
*
|
|
@@ -247,22 +249,44 @@ declare class UserManager extends BaseManager {
|
|
|
247
249
|
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
248
250
|
/** An email as accounts are compared on it (#1748): trimmed, any case. No provider rules (Gmail dots, plus addressing, aliases). */
|
|
249
251
|
private static emailKey;
|
|
250
|
-
/** The accounts holding this email, by username. Empty for no email. */
|
|
251
|
-
private accountsWithEmail;
|
|
252
252
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* and
|
|
263
|
-
*
|
|
264
|
-
|
|
265
|
-
|
|
253
|
+
* Every account holding this email (#2073): two or more accounts may share
|
|
254
|
+
* one (a couple, an older adult and a carer). Compared trimmed and in any
|
|
255
|
+
* case, with no provider rules (plus addressing is a different address).
|
|
256
|
+
* Empty for no email. Without the password hash.
|
|
257
|
+
*/
|
|
258
|
+
getUsersByEmail(email: unknown): Promise<Array<Omit<User, 'password'>>>;
|
|
259
|
+
/**
|
|
260
|
+
* An account holding the admin role never shares its email (#2073, operator
|
|
261
|
+
* 2026-10-25), either way round: refused when `username` is or will be an
|
|
262
|
+
* administrator and another account holds `email`, and when another account
|
|
263
|
+
* holding `email` is an administrator. `roles` are the account's roles
|
|
264
|
+
* after the change, when the change sets them; otherwise its current ones
|
|
265
|
+
* are read. Throws `UserCreateError('admin-email-shared')`; the message names
|
|
266
|
+
* the accounts, for the log and an administrator, never for a visitor.
|
|
267
|
+
*/
|
|
268
|
+
assertAdminEmailUnshared(username: string, email: unknown, ctx: ActorContext, roles?: readonly string[]): Promise<void>;
|
|
269
|
+
/**
|
|
270
|
+
* An account never sets its own email without the emailed link (#2073): a
|
|
271
|
+
* change of email made as the account itself is refused unless the caller
|
|
272
|
+
* holds `user-edit` (an administrator in Admin → Users is trusted). The
|
|
273
|
+
* link is AuthManager's (`requestEmailChange`, `confirmEmailChange`), which
|
|
274
|
+
* saves under the system context. Throws `UserCreateError('email-unconfirmed')`.
|
|
275
|
+
*/
|
|
276
|
+
private assertEmailChangeAllowed;
|
|
277
|
+
/** The usernames of every account whose email another account holds too (#2073): the admins' marker on the user list. */
|
|
278
|
+
usernamesSharingAnEmail(): Promise<Set<string>>;
|
|
279
|
+
/** Whether an account other than `ownUsername` holds this email too (#2073): for the note at save and the admins' marker. */
|
|
280
|
+
isEmailShared(email: unknown, ownUsername: string | null): Promise<boolean>;
|
|
281
|
+
/**
|
|
282
|
+
* The session cookie is `secure` and a request came over plain http, so
|
|
283
|
+
* express-session sent no cookie and nobody can sign in (#2071). Sessions
|
|
284
|
+
* are this manager's, so the report is too: an error in the log, the
|
|
285
|
+
* dashboard's "needs your attention", and a notification for admins, once
|
|
286
|
+
* per process. Every later call is one boolean check, because app.ts calls
|
|
287
|
+
* this on each such request.
|
|
288
|
+
*/
|
|
289
|
+
reportSessionCookieWithheld(): void;
|
|
266
290
|
/**
|
|
267
291
|
* Normalize provider name from configuration (lowercase) to class name (PascalCase)
|
|
268
292
|
* @param {string} providerName - Provider name from configuration (e.g., 'fileuserprovider')
|
|
@@ -46,6 +46,8 @@ export interface AuthVerifyCredentials {
|
|
|
46
46
|
password?: string;
|
|
47
47
|
/** One-time token — used by MagicLinkAuthProvider */
|
|
48
48
|
token?: string;
|
|
49
|
+
/** The account chosen for a link to a shared address (#2073) — used by MagicLinkAuthProvider. */
|
|
50
|
+
account?: string;
|
|
49
51
|
/** OAuth `state` from the callback — used by OidcAuthProvider */
|
|
50
52
|
state?: string;
|
|
51
53
|
/** The browser-binding value from the caller's cookie (#1022 magic link, #1630 OAuth) */
|
|
@@ -207,6 +209,11 @@ export interface AuthProvider {
|
|
|
207
209
|
* Must be read BEFORE `consumeToken()`, which deletes the entry.
|
|
208
210
|
*/
|
|
209
211
|
getDeviceState?(handle: string): string | null;
|
|
212
|
+
/**
|
|
213
|
+
* The accounts a flow to a shared address offers (#2073), by username, or
|
|
214
|
+
* null when it names one account. Read without consuming.
|
|
215
|
+
*/
|
|
216
|
+
accountChoices?(handle: string): string[] | null;
|
|
210
217
|
/**
|
|
211
218
|
* Create the account behind a first-time credential (#1049).
|
|
212
219
|
*
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Tokens are in-memory only — lost on restart, acceptable for the default 15-min TTL.
|
|
18
18
|
*
|
|
19
|
+
* A shared address (#2073): two or more accounts may hold one email. The link
|
|
20
|
+
* then names no account; the person opening it chooses one of the accounts on
|
|
21
|
+
* the address, and the link alone is enough for any of them (the mailbox is
|
|
22
|
+
* the shared trust, operator 2026-10-25).
|
|
23
|
+
*
|
|
19
24
|
* @see AuthManager
|
|
20
25
|
* @see BaseAuthProvider
|
|
21
26
|
*/
|
|
@@ -57,6 +62,13 @@ export declare class MagicLinkAuthProvider implements AuthProvider {
|
|
|
57
62
|
* Does NOT consume the token — call consumeToken() after the session is created.
|
|
58
63
|
*/
|
|
59
64
|
verify(credentials: AuthVerifyCredentials): Promise<AuthResult | null>;
|
|
65
|
+
/**
|
|
66
|
+
* The accounts a link to a shared address offers (#2073), by username, or
|
|
67
|
+
* null for a link to one account (or an unknown or expired one). Read
|
|
68
|
+
* without consuming, so the page asking which account can be shown.
|
|
69
|
+
*/
|
|
70
|
+
accountChoices(token: string): string[] | null;
|
|
71
|
+
private liveEntry;
|
|
60
72
|
/**
|
|
61
73
|
* Delete the token and report whether THIS caller was the one that did.
|
|
62
74
|
*
|