@jwilleke/ngdpbase-types 5.30.1 → 5.31.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 +2 -2
- package/lib/src/managers/BackgroundJobManager.d.ts +9 -0
- package/lib/src/managers/IdentityManager.d.ts +64 -4
- package/lib/src/managers/OrganizationManager.d.ts +9 -5
- package/lib/src/managers/UserManager.d.ts +11 -0
- package/lib/src/types/Person.d.ts +39 -6
- package/lib/src/utils/roleGrants.d.ts +18 -3
- package/lib/src/utils/sessionSecurity.d.ts +116 -0
- package/package.json +1 -1
|
@@ -41,12 +41,12 @@ export type { IceShim, IceReceiver, IceItem, IceKind, IceWarning, ShimDirection,
|
|
|
41
41
|
export type { default as UserManager, UserUpdateInput, UserUpdateFn } from '../../src/managers/UserManager.js';
|
|
42
42
|
export type { User, UserPreferences } from '../../src/types/User.js';
|
|
43
43
|
export type { default as AuthManager, InviteOutcome, InvitationView } from '../../src/managers/AuthManager.js';
|
|
44
|
-
export type { default as IdentityManager, LinkOfferOutcome } from '../../src/managers/IdentityManager.js';
|
|
44
|
+
export type { default as IdentityManager, LinkOfferOutcome, AffiliationQuery, IdentitySearch, IdentityMatch } from '../../src/managers/IdentityManager.js';
|
|
45
45
|
export type {
|
|
46
46
|
/** @deprecated Renamed `IdentityManager` (#2038); this alias goes after one release. */
|
|
47
47
|
default as PersonManager } from '../../src/managers/IdentityManager.js';
|
|
48
48
|
export type { default as OrganizationManager } from '../../src/managers/OrganizationManager.js';
|
|
49
|
-
export type { Person, PersonAffiliation, PersonInput, PersonUpdate } from '../../src/types/Person.js';
|
|
49
|
+
export type { Person, PersonAffiliation, LegacyPersonAffiliation, PersonChange, PersonInput, PersonUpdate } from '../../src/types/Person.js';
|
|
50
50
|
export type { Organization, OrganizationInput, OrganizationUpdate, ContactPoint, ContactPointEntry, PostalAddress } from '../../src/types/Organization.js';
|
|
51
51
|
export type { ContactRefusedError } from '../../src/utils/contactAccess.js';
|
|
52
52
|
export type { default as RoleManager } from '../../src/managers/RoleManager.js';
|
|
@@ -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 { 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,13 @@ 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`.
|
|
111
163
|
*/
|
|
112
|
-
update(ctx: ActorContext, id: string, patch:
|
|
164
|
+
update(ctx: ActorContext, id: string, patch: PersonChange): Promise<Person | null>;
|
|
113
165
|
delete(ctx: ActorContext, id: string): Promise<boolean>;
|
|
114
166
|
/**
|
|
115
167
|
* An administrator joins a person who has no account to an account (#1930),
|
|
@@ -150,10 +202,18 @@ declare class IdentityManager extends BaseManager {
|
|
|
150
202
|
*/
|
|
151
203
|
private join;
|
|
152
204
|
/**
|
|
153
|
-
* Each affiliation
|
|
154
|
-
*
|
|
205
|
+
* Each affiliation is a schema.org `OrganizationRole` (#2039): `memberOf` an
|
|
206
|
+
* organization that exists, an optional `roleName` (the title, trimmed free
|
|
207
|
+
* text) and optional `startDate` / `endDate` (`YYYY-MM-DD`, the start not
|
|
208
|
+
* after the end; a null end is an ongoing one, stored without it). An entry
|
|
209
|
+
* in the old `{ organization, role? }` shape is converted (one release, for
|
|
210
|
+
* add-ons not yet switched). Throws on any other shape. The organization is
|
|
211
|
+
* looked up as core's own check, under the system context, so a user may
|
|
212
|
+
* name one on their own record without `identity-read`.
|
|
155
213
|
*/
|
|
156
214
|
private checkedAffiliations;
|
|
215
|
+
/** The site's own organization's `@id` (`OrganizationManager.getInstallOrg`): an affiliation with it is a fact (#2039). */
|
|
216
|
+
private siteOrganizationId;
|
|
157
217
|
private configNumber;
|
|
158
218
|
/**
|
|
159
219
|
* Clear all caches. Called on every write — the simplest correct
|
|
@@ -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`,
|
|
@@ -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
|
*
|
|
@@ -255,6 +257,15 @@ declare class UserManager extends BaseManager {
|
|
|
255
257
|
* its safe message and other routes their own.
|
|
256
258
|
*/
|
|
257
259
|
private assertEmailFree;
|
|
260
|
+
/**
|
|
261
|
+
* The session cookie is `secure` and a request came over plain http, so
|
|
262
|
+
* express-session sent no cookie and nobody can sign in (#2071). Sessions
|
|
263
|
+
* are this manager's, so the report is too: an error in the log, the
|
|
264
|
+
* dashboard's "needs your attention", and a notification for admins, once
|
|
265
|
+
* per process. Every later call is one boolean check, because app.ts calls
|
|
266
|
+
* this on each such request.
|
|
267
|
+
*/
|
|
268
|
+
reportSessionCookieWithheld(): void;
|
|
258
269
|
/**
|
|
259
270
|
* Accounts that share an email, saved before #1748 refused it. They are not
|
|
260
271
|
* changed — an admin decides — but they are never silent: the manager is
|
|
@@ -17,15 +17,40 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { PostalAddress, ContactPointEntry } from './Organization.js';
|
|
19
19
|
/**
|
|
20
|
-
* One
|
|
21
|
-
* `
|
|
20
|
+
* One affiliation of a digital identity (#2039): schema.org `affiliation`
|
|
21
|
+
* with an `OrganizationRole`, as the identity model says
|
|
22
|
+
* (`docs/planning/identity-model.md`, "Affiliation shape"). It puts the
|
|
23
|
+
* identity in a group, the organization plus the title, and never grants a
|
|
24
|
+
* permission.
|
|
25
|
+
*
|
|
22
26
|
* The organization is named by its `@id`; OrganizationManager lists an
|
|
23
27
|
* organization's people by reading these, so the link is stored once, here.
|
|
28
|
+
* With the site's own organization it is a fact, set only under
|
|
29
|
+
* `identity-manage`; with any other it is a claim, which a user may also set
|
|
30
|
+
* on their own identity record. IdentityManager's door decides.
|
|
24
31
|
*/
|
|
25
32
|
export interface PersonAffiliation {
|
|
26
|
-
|
|
33
|
+
'@type': 'OrganizationRole';
|
|
34
|
+
/** The organization, by its `@id`. */
|
|
35
|
+
memberOf: {
|
|
36
|
+
'@id': string;
|
|
37
|
+
};
|
|
38
|
+
/** The affiliation's title there (Owner, Treasurer, Vendor): free text, never a Role. Left out when none is known. */
|
|
39
|
+
roleName?: string;
|
|
40
|
+
/** ISO date (`YYYY-MM-DD`) the affiliation began; left out when not known. */
|
|
41
|
+
startDate?: string;
|
|
42
|
+
/** ISO date (`YYYY-MM-DD`) it ended or ends, the day included; left out while it is ongoing. */
|
|
43
|
+
endDate?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* An affiliation in its shape before #2039, `{ organization, role? }`.
|
|
47
|
+
* Converted (organization → `memberOf`, role → `roleName`) when read, once on
|
|
48
|
+
* disk at start-up, and when an add-on still writes it (for one release).
|
|
49
|
+
*
|
|
50
|
+
* @deprecated Write {@link PersonAffiliation}.
|
|
51
|
+
*/
|
|
52
|
+
export interface LegacyPersonAffiliation {
|
|
27
53
|
organization: string;
|
|
28
|
-
/** The person's role or title there (schema.org `roleName`), when known. */
|
|
29
54
|
role?: string;
|
|
30
55
|
}
|
|
31
56
|
/**
|
|
@@ -59,7 +84,7 @@ export interface Person {
|
|
|
59
84
|
image?: string;
|
|
60
85
|
/** Other URLs that identify this person (LinkedIn, GitHub, etc.) */
|
|
61
86
|
sameAs?: string[];
|
|
62
|
-
/**
|
|
87
|
+
/** Affiliations: each a schema.org OrganizationRole, an organization with a title and dates (#2039). */
|
|
63
88
|
affiliation?: PersonAffiliation[];
|
|
64
89
|
/** Allow extension fields (additionalProperty, etc.) */
|
|
65
90
|
[key: string]: unknown;
|
|
@@ -69,12 +94,20 @@ export interface Person {
|
|
|
69
94
|
* `@context`/`@type` are immutable; `@id` and `identifier` cannot be changed via update.
|
|
70
95
|
*/
|
|
71
96
|
export type PersonUpdate = Partial<Omit<Person, '@context' | '@type' | '@id' | 'identifier'>>;
|
|
97
|
+
/**
|
|
98
|
+
* What `IdentityManager.update` takes: a {@link PersonUpdate} whose
|
|
99
|
+
* `affiliation` (the whole list) may still hold entries in the old
|
|
100
|
+
* `{ organization, role? }` shape, converted at the door, for one release (#2039).
|
|
101
|
+
*/
|
|
102
|
+
export type PersonChange = Omit<PersonUpdate, 'affiliation'> & {
|
|
103
|
+
affiliation?: Array<PersonAffiliation | LegacyPersonAffiliation>;
|
|
104
|
+
};
|
|
72
105
|
/**
|
|
73
106
|
* What a caller gives `IdentityManager.create` (#1930): the person's fields.
|
|
74
107
|
* `identifier` is left out for a person with no account (one is generated);
|
|
75
108
|
* UserManager passes the account's username when it creates the person for a
|
|
76
109
|
* new account.
|
|
77
110
|
*/
|
|
78
|
-
export type PersonInput =
|
|
111
|
+
export type PersonInput = PersonChange & {
|
|
79
112
|
identifier?: string;
|
|
80
113
|
};
|
|
@@ -24,12 +24,27 @@ export interface RoleGrants {
|
|
|
24
24
|
/** Actions a deny policy names for this role. */
|
|
25
25
|
denies: string[];
|
|
26
26
|
}
|
|
27
|
+
/** A grant to one subject, with the policies that give it (#2067). */
|
|
28
|
+
export interface SubjectGrant extends RoleGrant {
|
|
29
|
+
/** The ids of the allow policies that give it, lowest priority first. */
|
|
30
|
+
by: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface SubjectGrants {
|
|
33
|
+
allows: SubjectGrant[];
|
|
34
|
+
/** Actions a deny policy names for this subject. */
|
|
35
|
+
denies: string[];
|
|
36
|
+
}
|
|
27
37
|
/**
|
|
28
38
|
* Every role a policy names, with what its policies allow and deny — the one
|
|
29
39
|
* reading of "what a role grants" (#1539): the user forms, the profile, the
|
|
30
40
|
* admin roles page and `PolicyDecisionPoint.rolePermissions` all come from
|
|
31
|
-
* here.
|
|
32
|
-
* a lower-priority allow gave and an allow above it gives it back — the order
|
|
33
|
-
* the evaluator's first-match-by-priority decides in.
|
|
41
|
+
* here.
|
|
34
42
|
*/
|
|
35
43
|
export declare function roleGrants(policies: readonly Policy[]): Record<string, RoleGrants>;
|
|
44
|
+
/**
|
|
45
|
+
* Every scheduled job a policy names (`{ "type": "job", "value": "<job id>" }`,
|
|
46
|
+
* #1999), with what its policies allow and deny and which policy gives each
|
|
47
|
+
* allow — the admin roles page's Jobs part (#2067). A job a policy names acts
|
|
48
|
+
* as itself and holds only these. Display only, like {@link roleGrants}.
|
|
49
|
+
*/
|
|
50
|
+
export declare function jobGrants(policies: readonly Policy[]): Record<string, SubjectGrants>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveSessionSecurity — keep `session.secure` and `trust proxy` consistent (#1046).
|
|
3
|
+
*
|
|
4
|
+
* #1043 made the session cookie `secure` actually turn on in production. On
|
|
5
|
+
* every HTTPS deployment that terminates TLS upstream — Cloudflare, an ingress,
|
|
6
|
+
* a tunnel — that silently broke login:
|
|
7
|
+
*
|
|
8
|
+
* 1. `secure` resolves true, so express-session will only emit `Set-Cookie`
|
|
9
|
+
* on a connection it believes is https.
|
|
10
|
+
* 2. `trust proxy` defaults to false, so Express reads the raw socket and
|
|
11
|
+
* believes the connection is plain http. It is not wrong — the hop from
|
|
12
|
+
* the proxy really is http; it simply has not been told to read
|
|
13
|
+
* X-Forwarded-Proto.
|
|
14
|
+
* 3. No session cookie ever reaches the browser. Every following request
|
|
15
|
+
* builds a fresh session, so `req.session.csrfToken` is missing and the
|
|
16
|
+
* CSRF middleware answers `403 Forbidden — invalid CSRF token` — on
|
|
17
|
+
* `POST /login` included. Nobody can sign in.
|
|
18
|
+
*
|
|
19
|
+
* The two settings are not independent, so they are resolved together here.
|
|
20
|
+
* Turning on `secure` without `trust proxy` is not a working configuration;
|
|
21
|
+
* `secure` is the operator's stated intent to be behind TLS, and behind
|
|
22
|
+
* terminated TLS `trust proxy` is what makes Express able to see it.
|
|
23
|
+
*
|
|
24
|
+
* The operator's explicit choice still wins for both keys. The shipped
|
|
25
|
+
* app-default-config.json pins `ngdpbase.session.secure` and
|
|
26
|
+
* `ngdpbase.server.trust-proxy` to false, so a plain getProperty() read cannot
|
|
27
|
+
* tell "operator asked for false" from "nobody has said anything" — hence
|
|
28
|
+
* reading getCustomProperties() rather than the merged view.
|
|
29
|
+
*
|
|
30
|
+
* ## #1160 — native TLS breaks the assumption
|
|
31
|
+
*
|
|
32
|
+
* All of the above assumed `secure: true` meant TLS terminated UPSTREAM, so a
|
|
33
|
+
* proxy existed whose `X-Forwarded-Proto` Express had to be told to read. #1153
|
|
34
|
+
* made the server able to terminate TLS itself, and then there is no proxy —
|
|
35
|
+
* and trusting a forwarded header that nothing sets is worse than not trusting
|
|
36
|
+
* it. With `trust proxy` on and nothing in front, a caller can assert their own
|
|
37
|
+
* address and scheme, which the login throttle counts by and the audit log
|
|
38
|
+
* records.
|
|
39
|
+
*
|
|
40
|
+
* So the derivation now asks what is actually true rather than inferring from
|
|
41
|
+
* `secure` alone. The same applies to the misconfiguration warning: `secure`
|
|
42
|
+
* with `trust-proxy` explicitly false is the CORRECT configuration under native
|
|
43
|
+
* TLS, and warning about it would train an operator to ignore a warning that is
|
|
44
|
+
* load-bearing in the upstream case.
|
|
45
|
+
*/
|
|
46
|
+
/** Express's accepted `trust proxy` values: off, hop count, or a subnet list. */
|
|
47
|
+
export type TrustProxyValue = boolean | number | string;
|
|
48
|
+
/** What the resolution needs to know about the transport (#1160). */
|
|
49
|
+
export interface SessionSecurityContext {
|
|
50
|
+
/**
|
|
51
|
+
* True when this server terminates TLS itself (#1153).
|
|
52
|
+
*
|
|
53
|
+
* Defaults to false so every existing caller keeps its pre-#1160 behaviour
|
|
54
|
+
* rather than silently changing what it resolves.
|
|
55
|
+
*/
|
|
56
|
+
nativeTls?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface SessionSecurity {
|
|
59
|
+
/** Value for the session cookie's `secure` flag. */
|
|
60
|
+
secure: boolean;
|
|
61
|
+
/** Value for `app.set('trust proxy', …)`. `false` means do not set it. */
|
|
62
|
+
trustProxy: TrustProxyValue;
|
|
63
|
+
/** True when `trustProxy` was derived from `secure` rather than configured. */
|
|
64
|
+
trustProxyDerived: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* True when the resolved pair cannot issue a session cookie behind
|
|
67
|
+
* terminated TLS — `secure` on with `trust proxy` explicitly off. Only
|
|
68
|
+
* reachable when the operator configured that combination by hand; the
|
|
69
|
+
* caller is expected to log it loudly rather than silently "fix" a stated
|
|
70
|
+
* choice.
|
|
71
|
+
*/
|
|
72
|
+
misconfigured: boolean;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the session cookie's `secure` flag and the matching `trust proxy`.
|
|
76
|
+
*
|
|
77
|
+
* `secure`: the operator's explicit boolean, else on when NODE_ENV is
|
|
78
|
+
* production. Inferring it from the request would get the terminated-TLS case
|
|
79
|
+
* exactly backwards, which is the case that matters.
|
|
80
|
+
*
|
|
81
|
+
* `trustProxy`: the operator's explicit value, else `true` whenever `secure`
|
|
82
|
+
* ends up on. `true` — rather than a hop count — because req.ip must come out
|
|
83
|
+
* as the end client. Under `1` behind Cloudflare, req.ip would resolve to the
|
|
84
|
+
* edge address shared by every visitor, and the login throttle's `ip:` bucket
|
|
85
|
+
* (#1044) would lock out all users at once on ten failures from anyone. An
|
|
86
|
+
* operator whose instance is also reachable directly should pin a hop count or
|
|
87
|
+
* a subnet list, since X-Forwarded-For is client-spoofable on that path.
|
|
88
|
+
*
|
|
89
|
+
* @param customProperties operator overrides only — NOT the merged config
|
|
90
|
+
* @param nodeEnv typically process.env.NODE_ENV
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveSessionSecurity(customProperties: Record<string, unknown> | null | undefined, nodeEnv: string | undefined, context?: SessionSecurityContext): SessionSecurity;
|
|
93
|
+
/**
|
|
94
|
+
* #2071 — what an administrator and a visitor are told when the session
|
|
95
|
+
* cookie cannot reach the browser, spelled once: the server log, the admin
|
|
96
|
+
* dashboard, the CSRF refusal and the login page all say this.
|
|
97
|
+
*/
|
|
98
|
+
export declare const SESSION_COOKIE_WITHHELD_MESSAGE: string;
|
|
99
|
+
/** The two request fields the check reads; express's Request has both. */
|
|
100
|
+
export interface SessionCookieRequest {
|
|
101
|
+
secure?: boolean;
|
|
102
|
+
session?: {
|
|
103
|
+
cookie?: {
|
|
104
|
+
secure?: unknown;
|
|
105
|
+
};
|
|
106
|
+
} | null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* True when express-session will NOT send the session cookie on this request
|
|
110
|
+
* (#2071): the cookie is `secure` and the request is not https as Express
|
|
111
|
+
* sees it, after `trust proxy`. express-session makes exactly this check and
|
|
112
|
+
* then silently sends nothing, so the browser has no session, every POST
|
|
113
|
+
* meets a fresh one, and the CSRF middleware refuses it. A boolean read of
|
|
114
|
+
* two fields, cheap enough for every request.
|
|
115
|
+
*/
|
|
116
|
+
export declare function sessionCookieWithheld(req: SessionCookieRequest): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jwilleke/ngdpbase-types",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.31.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",
|