@jwilleke/ngdpbase-types 5.39.0 → 5.41.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 +1 -1
- package/lib/src/context/ApiContext.d.ts +3 -2
- package/lib/src/context/ApiContext.js +4 -2
- package/lib/src/ice/fileHandling.d.ts +6 -1
- package/lib/src/ice/givenName.d.ts +19 -0
- package/lib/src/ice/runExport.d.ts +41 -0
- package/lib/src/ice/runImport.d.ts +37 -8
- package/lib/src/ice/shims/csv.d.ts +24 -0
- package/lib/src/ice/shims/export.d.ts +23 -0
- package/lib/src/ice/shims/index.d.ts +3 -1
- package/lib/src/ice/shims/svg.d.ts +27 -0
- package/lib/src/ice/shims/takeout.d.ts +83 -0
- package/lib/src/ice/types.d.ts +301 -5
- package/lib/src/ice/vet.d.ts +19 -0
- package/lib/src/managers/AttachmentManager.d.ts +100 -36
- package/lib/src/managers/AuthManager.d.ts +152 -12
- package/lib/src/managers/AuthManager.js +3830 -0
- package/lib/src/managers/BackgroundJobManager.d.ts +25 -0
- package/lib/src/managers/BackupManager.d.ts +24 -0
- package/lib/src/managers/BackupManager.js +48 -1
- package/lib/src/managers/ExportManager.d.ts +16 -0
- package/lib/src/managers/ExportManager.js +25 -0
- package/lib/src/managers/FileManager.d.ts +182 -0
- package/lib/src/managers/ImportManager.d.ts +279 -16
- package/lib/src/managers/MediaManager.d.ts +2 -0
- package/lib/src/managers/PageManager.d.ts +53 -0
- package/lib/src/managers/SearchManager.d.ts +4 -0
- package/lib/src/managers/SharedFolderManager.d.ts +320 -0
- package/lib/src/managers/ValidationManager.d.ts +5 -2
- package/lib/src/middleware/csrf.js +99 -0
- package/lib/src/providers/AgentTokenAuthProvider.js +46 -0
- package/lib/src/providers/AuthentikBearerAuthProvider.js +176 -0
- package/lib/src/providers/BaseCredentialsProvider.js +21 -0
- package/lib/src/providers/BaseMediaProvider.d.ts +4 -0
- package/lib/src/providers/BasePageProvider.d.ts +8 -0
- package/lib/src/providers/CloudflareAccessAuthProvider.js +160 -0
- package/lib/src/providers/FileCredentialsProvider.js +188 -0
- package/lib/src/providers/FileSystemMediaProvider.d.ts +6 -0
- package/lib/src/providers/MagicLinkAuthProvider.js +333 -0
- package/lib/src/providers/OidcAuthProvider.js +350 -0
- package/lib/src/providers/OidcBearerAuthProvider.js +27 -0
- package/lib/src/providers/PasskeyAuthProvider.js +147 -0
- package/lib/src/providers/PasswordAuthProvider.js +44 -0
- package/lib/src/security/PolicyDecisionPoint.d.ts +3 -2
- package/lib/src/security/pageActions.js +43 -0
- package/lib/src/security/permissions.generated.d.ts +7 -3
- package/lib/src/types/File.d.ts +90 -0
- package/lib/src/types/Provider.d.ts +15 -0
- package/lib/src/types/express.d.ts +7 -1
- package/lib/src/types/guards.js +366 -0
- package/lib/src/types/linkedom.d.ts +2 -0
- package/lib/src/utils/atomicWrite.js +129 -0
- package/lib/src/utils/auditEventNames.generated.d.ts +12 -0
- package/lib/src/utils/auditEventNames.generated.js +12 -0
- package/lib/src/utils/auditEvents.d.ts +15 -0
- package/lib/src/utils/auditEvents.js +17 -1
- package/lib/src/utils/bip39English.js +2 -0
- package/lib/src/utils/browserDescription.js +28 -0
- package/lib/src/utils/delegation.js +79 -0
- package/lib/src/utils/deriveUsername.js +38 -0
- package/lib/src/utils/emailForLog.js +28 -0
- package/lib/src/utils/fileMigrationPlan.d.ts +81 -0
- package/lib/src/utils/instanceEnvBackup.d.ts +6 -0
- package/lib/src/utils/instanceEnvBackup.js +13 -0
- package/lib/src/utils/ipLocation.js +106 -0
- package/lib/src/utils/magicLinkDeviceState.js +97 -0
- package/lib/src/utils/pageUrl.d.ts +1 -0
- package/lib/src/utils/passwordHash.js +150 -0
- package/lib/src/utils/permissionCatalog.js +102 -0
- package/lib/src/utils/privateStoreAccess.d.ts +25 -0
- package/lib/src/utils/privateStoreAccess.js +22 -1
- package/lib/src/utils/privateStoreCatalogs.js +57 -0
- package/lib/src/utils/privateStoreCrypto.js +266 -0
- package/lib/src/utils/privateStoreDoor.d.ts +10 -0
- package/lib/src/utils/privateStoreDoor.js +298 -0
- package/lib/src/utils/privateStoreImport.d.ts +0 -10
- package/lib/src/utils/privateStoreMeta.js +123 -0
- package/lib/src/utils/privateStorePath.d.ts +37 -1
- package/lib/src/utils/privateStorePath.js +60 -3
- package/lib/src/utils/privateStoreTakeout.js +149 -0
- package/lib/src/utils/privateStoreUnlock.d.ts +3 -0
- package/lib/src/utils/privateStoreUnlock.js +499 -0
- package/lib/src/utils/recoveryWordsFile.d.ts +54 -0
- package/lib/src/utils/safeRedirect.js +55 -0
- package/lib/src/utils/sessionAccount.js +55 -0
- package/lib/src/utils/sessionEnd.d.ts +1 -0
- package/lib/src/utils/sessionEnd.js +42 -0
- package/lib/src/utils/sessionGeneration.js +29 -0
- package/lib/src/utils/sessionIdle.d.ts +4 -0
- package/lib/src/utils/sessionIdle.js +72 -0
- package/lib/src/utils/sessionLimits.js +72 -0
- package/lib/src/utils/sessionSecurity.js +122 -0
- package/lib/src/utils/shareLockbox.js +101 -0
- package/lib/src/utils/sharedFolderAccess.d.ts +57 -0
- package/lib/src/utils/sharedFolderAccess.js +80 -0
- package/lib/src/utils/userCreateError.js +82 -0
- package/lib/src/utils/username.d.ts +2 -0
- package/lib/src/utils/username.js +105 -0
- package/package.json +2 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class OidcBearerAuthProvider {
|
|
2
|
+
engine;
|
|
3
|
+
id = 'oidc-bearer';
|
|
4
|
+
displayName = 'OpenID Connect access token';
|
|
5
|
+
/** Marks this provider as usable by the bearer middleware. */
|
|
6
|
+
acceptsBearer = true;
|
|
7
|
+
constructor(engine) {
|
|
8
|
+
this.engine = engine;
|
|
9
|
+
}
|
|
10
|
+
async verify(credentials) {
|
|
11
|
+
const token = credentials?.token;
|
|
12
|
+
if (typeof token !== 'string' || token.length === 0)
|
|
13
|
+
return null;
|
|
14
|
+
const oidc = this.engine.getManager('OidcManager');
|
|
15
|
+
const verified = await oidc?.verifyAccessToken(token);
|
|
16
|
+
if (!verified)
|
|
17
|
+
return null;
|
|
18
|
+
const name = await oidc.clientName(verified.clientId);
|
|
19
|
+
return {
|
|
20
|
+
username: verified.username,
|
|
21
|
+
// The grant, not the token: a token rotates; the grant is what the person allowed.
|
|
22
|
+
viaToken: { id: `oidc:${verified.grantId}`, name, scopes: verified.scopes },
|
|
23
|
+
aal: verified.aal
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export default OidcBearerAuthProvider;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passkeys — WebAuthn (#448), on `@simplewebauthn/server` directly (decided
|
|
3
|
+
* 2026-10-03; not activescott's provider, which needs its JWT session layer).
|
|
4
|
+
*
|
|
5
|
+
* A passkey alone signs a person in (NIST SP 800-63B: a multi-factor
|
|
6
|
+
* cryptographic authenticator suffices for AAL2), and it is phishing-resistant:
|
|
7
|
+
* the browser signs only for the relying party it was created for.
|
|
8
|
+
*
|
|
9
|
+
* - The relying party is the host of `ngdpbase.application.base-url`, never the
|
|
10
|
+
* request, so a passkey works only on that hostname. AuthManager registers
|
|
11
|
+
* this provider only when base-url is set explicitly and is a secure context
|
|
12
|
+
* (https, or localhost).
|
|
13
|
+
* - Passkeys live in the credentials store (#1524), kind `passkey`: the
|
|
14
|
+
* subject is the credential id, the secret the public key and its counter.
|
|
15
|
+
* AuthManager hands this provider two narrow functions for that, so the
|
|
16
|
+
* store keeps one owner.
|
|
17
|
+
* - The challenge is single-use and kept server-side by the caller (the
|
|
18
|
+
* session); this provider only generates options and verifies responses.
|
|
19
|
+
*/
|
|
20
|
+
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from '@simplewebauthn/server';
|
|
21
|
+
import logger from '../utils/logger.js';
|
|
22
|
+
/** The base-url origin, its host, and whether WebAuthn may run there (https, or localhost). */
|
|
23
|
+
export function relyingPartyFrom(baseUrl, rpName) {
|
|
24
|
+
let url;
|
|
25
|
+
try {
|
|
26
|
+
url = new URL(baseUrl);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const local = url.hostname === 'localhost' || url.hostname.endsWith('.localhost');
|
|
32
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && local))
|
|
33
|
+
return null;
|
|
34
|
+
return { rpID: url.hostname, origin: url.origin, rpName };
|
|
35
|
+
}
|
|
36
|
+
function parseSecret(secret) {
|
|
37
|
+
try {
|
|
38
|
+
const s = JSON.parse(secret);
|
|
39
|
+
return typeof s.publicKey === 'string' && typeof s.counter === 'number' ? s : null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export class PasskeyAuthProvider {
|
|
46
|
+
rp;
|
|
47
|
+
store;
|
|
48
|
+
id = 'passkey';
|
|
49
|
+
displayName = 'Passkey';
|
|
50
|
+
/**
|
|
51
|
+
* Something you have (the key) unlocked by something you know or are (the
|
|
52
|
+
* PIN or biometric the authenticator requires): AAL2, phishing-resistant.
|
|
53
|
+
* `swk` because most passkeys sync; a device-bound key is stronger, never
|
|
54
|
+
* weaker, than what is claimed here.
|
|
55
|
+
*/
|
|
56
|
+
factor = { amr: ['swk', 'user'], aal: 2, acr: 'phr', primary: true };
|
|
57
|
+
constructor(rp, store) {
|
|
58
|
+
this.rp = rp;
|
|
59
|
+
this.store = store;
|
|
60
|
+
}
|
|
61
|
+
/** The relying party passkeys are tied to — for the admin page and the enrol screen. */
|
|
62
|
+
relyingParty() {
|
|
63
|
+
return { ...this.rp };
|
|
64
|
+
}
|
|
65
|
+
/** Options for enrolling a passkey; `existing` keeps one authenticator from enrolling twice. */
|
|
66
|
+
async registrationOptions(username, displayName, existing) {
|
|
67
|
+
return generateRegistrationOptions({
|
|
68
|
+
rpName: this.rp.rpName,
|
|
69
|
+
rpID: this.rp.rpID,
|
|
70
|
+
userName: username,
|
|
71
|
+
userDisplayName: displayName || username,
|
|
72
|
+
attestationType: 'none',
|
|
73
|
+
excludeCredentials: existing.map(c => ({ id: c.subject, transports: parseSecret(c.secret)?.transports })),
|
|
74
|
+
authenticatorSelection: { residentKey: 'required', userVerification: 'required' }
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Verify an enrolment; returns what the credentials store keeps, or null when it does not verify. */
|
|
78
|
+
async verifyRegistration(response, expectedChallenge) {
|
|
79
|
+
try {
|
|
80
|
+
const result = await verifyRegistrationResponse({
|
|
81
|
+
response,
|
|
82
|
+
expectedChallenge,
|
|
83
|
+
expectedOrigin: this.rp.origin,
|
|
84
|
+
expectedRPID: this.rp.rpID,
|
|
85
|
+
requireUserVerification: true
|
|
86
|
+
});
|
|
87
|
+
if (!result.verified)
|
|
88
|
+
return null;
|
|
89
|
+
const info = result.registrationInfo;
|
|
90
|
+
const secret = {
|
|
91
|
+
publicKey: Buffer.from(info.credential.publicKey).toString('base64url'),
|
|
92
|
+
counter: info.credential.counter,
|
|
93
|
+
transports: info.credential.transports,
|
|
94
|
+
deviceType: info.credentialDeviceType,
|
|
95
|
+
backedUp: info.credentialBackedUp
|
|
96
|
+
};
|
|
97
|
+
return { subject: info.credential.id, secret: JSON.stringify(secret) };
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
logger.warn(`[Passkey] enrolment did not verify: ${err.message}`);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Options for signing in: no username asked, any passkey for this host may answer. */
|
|
105
|
+
async authenticationOptions() {
|
|
106
|
+
return generateAuthenticationOptions({ rpID: this.rp.rpID, userVerification: 'required' });
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Verify a sign-in. The credential id names the passkey; its public key and
|
|
110
|
+
* counter come from the store. A counter that does not move forward (where
|
|
111
|
+
* the authenticator keeps one) is a cloned key and fails.
|
|
112
|
+
*/
|
|
113
|
+
async verify(credentials) {
|
|
114
|
+
const assertion = credentials.webauthn;
|
|
115
|
+
if (!assertion)
|
|
116
|
+
return null;
|
|
117
|
+
const response = assertion.response;
|
|
118
|
+
const row = typeof response?.id === 'string' ? this.store.find(response.id) : null;
|
|
119
|
+
const secret = row ? parseSecret(row.secret) : null;
|
|
120
|
+
if (!row || !secret)
|
|
121
|
+
return null;
|
|
122
|
+
try {
|
|
123
|
+
const result = await verifyAuthenticationResponse({
|
|
124
|
+
response,
|
|
125
|
+
expectedChallenge: assertion.expectedChallenge,
|
|
126
|
+
expectedOrigin: this.rp.origin,
|
|
127
|
+
expectedRPID: this.rp.rpID,
|
|
128
|
+
credential: {
|
|
129
|
+
id: row.subject,
|
|
130
|
+
publicKey: new Uint8Array(Buffer.from(secret.publicKey, 'base64url')),
|
|
131
|
+
counter: secret.counter,
|
|
132
|
+
transports: secret.transports
|
|
133
|
+
},
|
|
134
|
+
requireUserVerification: true
|
|
135
|
+
});
|
|
136
|
+
if (!result.verified)
|
|
137
|
+
return null;
|
|
138
|
+
const next = { ...secret, counter: result.authenticationInfo.newCounter, backedUp: result.authenticationInfo.credentialBackedUp };
|
|
139
|
+
await this.store.used(row.id, new Date().toISOString(), JSON.stringify(next));
|
|
140
|
+
return { username: row.username };
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
logger.warn(`[Passkey] sign-in did not verify for credential ${row.id}: ${err.message}`);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PasswordAuthProvider — username/password authentication.
|
|
3
|
+
*
|
|
4
|
+
* The password method's check (#1758): AuthManager is the one door for every
|
|
5
|
+
* check of a secret, and this provider is its connection to the password.
|
|
6
|
+
* The hash stays on the account (UserManager keeps it and hands it only to
|
|
7
|
+
* this check); the check, and the decision to re-hash at the current cost
|
|
8
|
+
* (#1042), are made here. Registered by AuthManager as the default provider.
|
|
9
|
+
*/
|
|
10
|
+
import { hashPassword, verifyPassword, needsRehash, isLegacyHash } from '../utils/passwordHash.js';
|
|
11
|
+
export class PasswordAuthProvider {
|
|
12
|
+
engine;
|
|
13
|
+
id = 'password';
|
|
14
|
+
displayName = 'Username & Password';
|
|
15
|
+
/** Something you know (#1523). */
|
|
16
|
+
factor = { amr: ['pwd'], aal: 1, primary: true };
|
|
17
|
+
constructor(engine) {
|
|
18
|
+
this.engine = engine;
|
|
19
|
+
}
|
|
20
|
+
async verify(credentials) {
|
|
21
|
+
const { username, password } = credentials;
|
|
22
|
+
if (!username || !password)
|
|
23
|
+
return null;
|
|
24
|
+
const userManager = this.engine.getManager('UserManager');
|
|
25
|
+
if (!userManager)
|
|
26
|
+
return null;
|
|
27
|
+
const stored = await userManager.storedPasswordHash(username);
|
|
28
|
+
if (!stored)
|
|
29
|
+
return null;
|
|
30
|
+
// The legacy salt keeps its historic value: legacy digests were made with it (#1042).
|
|
31
|
+
const legacySalt = this.engine.getManager('ConfigurationManager')
|
|
32
|
+
?.getProperty('ngdpbase.user.security.passwordsalt', 'amdwiki-salt');
|
|
33
|
+
if (!(await verifyPassword(password, stored, legacySalt || 'amdwiki-salt')))
|
|
34
|
+
return null;
|
|
35
|
+
// #1882: the account as stored, never the casing typed — lookups ignore case (#1436).
|
|
36
|
+
const account = await userManager.getUser(username);
|
|
37
|
+
if (!account?.username)
|
|
38
|
+
return null;
|
|
39
|
+
// #1042: upgrade the stored hash now, while the plaintext is known to be right.
|
|
40
|
+
if (needsRehash(stored))
|
|
41
|
+
await userManager.upgradePasswordHash(account.username, await hashPassword(password), isLegacyHash(stored));
|
|
42
|
+
return { username: account.username };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -131,9 +131,10 @@ export declare class PolicyDecisionPoint extends BaseManager {
|
|
|
131
131
|
* {@link permits}, which is "may it act now", and named so: an affordance
|
|
132
132
|
* (show the reveal button, show the sign-in methods section) asks this, so a
|
|
133
133
|
* person whose sign-in is not fresh still sees the way to the prompt that
|
|
134
|
-
* makes it fresh. A door never asks this.
|
|
134
|
+
* makes it fresh. A door never asks this. `on` asks it about a resource,
|
|
135
|
+
* as {@link decide} does (#2120: a vault page's export).
|
|
135
136
|
*/
|
|
136
|
-
holds(subject: DecisionInput, action: string): Promise<boolean>;
|
|
137
|
+
holds(subject: DecisionInput, action: string, on?: Omit<DecisionRequest, 'action'>): Promise<boolean>;
|
|
137
138
|
/**
|
|
138
139
|
* Step-up (#1525, #1635), decided here — in the one place every door asks —
|
|
139
140
|
* rather than by routes in front of the doors. A permission marked
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* One vocabulary for page actions (#1431, step 5).
|
|
4
|
+
*
|
|
5
|
+
* A door asks for a PERMISSION, and every permission name is declared in
|
|
6
|
+
* `ngdpbase.permissions.definitions`. Page-level checks grew their own words
|
|
7
|
+
* anyway — `canAccess('view')`, `canAccess('edit')` — which are declared
|
|
8
|
+
* nowhere, and `PolicyInformationPoint.checkDefaultPermission` invented a third set with
|
|
9
|
+
* colons (`page:read`), declared nowhere either, so that check could only ever
|
|
10
|
+
* deny ([#1174](https://github.com/jwilleke/ngdpbase/issues/1174)).
|
|
11
|
+
*
|
|
12
|
+
* This is the one translation: the short word a caller passes, mapped to the
|
|
13
|
+
* registry name policy is written in. It exists so the mapping is not
|
|
14
|
+
* copied — it was already in `checkPagePermissionWithContext` twice, and in
|
|
15
|
+
* `checkDefaultPermission` in a different and broken form.
|
|
16
|
+
*
|
|
17
|
+
* New code should pass the registry name directly. The short forms stay
|
|
18
|
+
* because they are what a page author writes in `[{ALLOW view …}]` ACL markup,
|
|
19
|
+
* and that is a content syntax, not an internal vocabulary.
|
|
20
|
+
*/
|
|
21
|
+
/** The short words accepted at a page door, and in ACL markup. */
|
|
22
|
+
export const PAGE_ACTION_ALIASES = {
|
|
23
|
+
view: 'page-read',
|
|
24
|
+
read: 'page-read',
|
|
25
|
+
edit: 'page-edit',
|
|
26
|
+
delete: 'page-delete',
|
|
27
|
+
create: 'page-create',
|
|
28
|
+
rename: 'page-rename',
|
|
29
|
+
export: 'page-export',
|
|
30
|
+
upload: 'asset-upload'
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The registry permission a page action means.
|
|
34
|
+
*
|
|
35
|
+
* An unknown word is returned unchanged rather than guessed at: it is either
|
|
36
|
+
* already a registry name — which is what new code passes — or it is a typo,
|
|
37
|
+
* and the permission-registry invariant is what catches a typo, not a silent
|
|
38
|
+
* rewrite into `page:<whatever>` (which is what the deleted code did).
|
|
39
|
+
*/
|
|
40
|
+
export function permissionForPageAction(action) {
|
|
41
|
+
const key = action.trim().toLowerCase();
|
|
42
|
+
return PAGE_ACTION_ALIASES[key] ?? key;
|
|
43
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Every permission core declares. Addons generate their own union. */
|
|
2
2
|
export type CorePermission =
|
|
3
|
-
/** Change what protects your own account — password, email, sign-in methods (passkeys), vault takeout and import, approving a device (#1525). Granted wherever profile-manage is; asks for a fresh sign-in
|
|
3
|
+
/** Change what protects your own account — password, email, sign-in methods (passkeys), vault takeout and import, approving a device (#1525). Granted wherever profile-manage is; asks for a fresh sign-in, because the entry carries "step-up": true (#1638) */
|
|
4
4
|
'account-security'
|
|
5
5
|
/** View administration screens (read-only, no changes) */
|
|
6
6
|
| 'admin-read'
|
|
@@ -20,7 +20,7 @@ export type CorePermission =
|
|
|
20
20
|
| 'audit-export'
|
|
21
21
|
/** Add a comment, and delete your own; deleting anyone's is admin-system (#1198) */
|
|
22
22
|
| 'comment-create'
|
|
23
|
-
/** Change any configuration setting, and download a backup (#1525). Admin only by default; asks for a fresh sign-in
|
|
23
|
+
/** Change any configuration setting, and download a backup (#1525). Admin only by default; asks for a fresh sign-in, because the entry carries "step-up": true (#1638) */
|
|
24
24
|
| 'config-manage'
|
|
25
25
|
/** Open the Executive Hub (/executive): the organization's summary cards and the requests waiting for the board (#2093). Held by the executive role; deciding a request still needs that request's own permission */
|
|
26
26
|
| 'executive-read'
|
|
@@ -54,7 +54,7 @@ export type CorePermission =
|
|
|
54
54
|
| 'search-page'
|
|
55
55
|
/** Search users */
|
|
56
56
|
| 'search-user'
|
|
57
|
-
/** Unmask a configuration secret (#1525). Admin only by default; asks for a fresh sign-in
|
|
57
|
+
/** Unmask a configuration secret (#1525). Admin only by default; asks for a fresh sign-in, because the entry carries "step-up": true (#1638). Hashed credentials (agent tokens) and shown-once secrets can never be revealed */
|
|
58
58
|
| 'secret-reveal'
|
|
59
59
|
/** End another person's signed-in session (#1638) */
|
|
60
60
|
| 'session-revoke'
|
|
@@ -64,6 +64,10 @@ export type CorePermission =
|
|
|
64
64
|
| 'share-extend'
|
|
65
65
|
/** Revoke your own share links; revoking anyone's also needs admin-system (#1638) */
|
|
66
66
|
| 'share-revoke'
|
|
67
|
+
/** Add, edit and delete pages and files in a shared folder (/shared/<name>, #2125). Scoped to one folder by the policy's resource (type shared-folder, pattern shared/<name>). The writes themselves are recorded by the page and file doors (page-create, page-edit, page-delete, asset-upload, asset-delete) */
|
|
68
|
+
| 'shared-folder-file'
|
|
69
|
+
/** Open a shared folder (/shared/<name>, #2125): its pages, their history and its files. Scoped to one folder by the policy's resource (type shared-folder, pattern shared/<name>); a folder lists no roles. Shipped granted on shared/default to every account, and on every folder (shared/*) to admin */
|
|
70
|
+
| 'shared-folder-read'
|
|
67
71
|
/** Walk through a private store's door and create your own copy of it — with a user key and recovery words when the store kind is encrypted (#1414). Every signed-in role; never anonymous. The door also needs a password sign-in, so a token or share cannot use it */
|
|
68
72
|
| 'store-create'
|
|
69
73
|
/** Extend your own live agent tokens instead of minting new ones, to at most max-ttl-hours from now; the token and its scopes stay the same (#1734). Admin only by default. A token can never carry this scope */
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The file record (#1852, epic #1851): one shape for every file the site
|
|
3
|
+
* keeps, read through `FileManager`, the one door.
|
|
4
|
+
*
|
|
5
|
+
* The decisions behind it are the epic's decision log
|
|
6
|
+
* (https://github.com/jwilleke/ngdpbase/issues/1851#issuecomment-6236003946);
|
|
7
|
+
* the practice is docs/guides/storage-developer-guide.md.
|
|
8
|
+
*
|
|
9
|
+
* It is not a new vocabulary. `AssetRecord` (src/types/Asset.ts) is already
|
|
10
|
+
* the internal, schema.org-named shape for a stored file (docs/schemas.md
|
|
11
|
+
* Decision 11), so the record takes its fields from there by name and adds
|
|
12
|
+
* only what an asset record does not say: where the file lives, who owns it,
|
|
13
|
+
* and its system-category. A field that changes in `AssetRecord` changes
|
|
14
|
+
* here too; there is no second copy to drift.
|
|
15
|
+
*/
|
|
16
|
+
import type { AssetRecord } from './Asset.js';
|
|
17
|
+
/**
|
|
18
|
+
* Where a file lives: the public area, one user's vault, or one shared
|
|
19
|
+
* folder. A file is unique by its place plus its name; the same name may
|
|
20
|
+
* exist in two places.
|
|
21
|
+
*
|
|
22
|
+
* Each place is named by the identifiers the site already uses: a vault by
|
|
23
|
+
* its owner's username and its vault id (`pages/vaults/{owner}/{vault}/`), a
|
|
24
|
+
* shared folder by its name (`/shared/{folder}`, SharedFolderManager). The
|
|
25
|
+
* shared folders' internal container (`SHARED_FOLDER_OWNER`) is never a
|
|
26
|
+
* vault owner here: a folder is always `{ kind: 'shared-folder' }` (#2148).
|
|
27
|
+
*/
|
|
28
|
+
export type FilePlace = {
|
|
29
|
+
kind: 'public';
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'vault';
|
|
32
|
+
owner: string;
|
|
33
|
+
vault: string;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'shared-folder';
|
|
36
|
+
folder: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* One file, as the door shows it.
|
|
40
|
+
*
|
|
41
|
+
* From `AssetRecord`, unchanged in meaning:
|
|
42
|
+
* - `id`: the stable identity, kept across renames and moves.
|
|
43
|
+
* - `filename`: the file's name, the one links use (`[Annual Report.pdf]`),
|
|
44
|
+
* unique within its place.
|
|
45
|
+
* - `name`: the human title, when one was given (schema.org `name`).
|
|
46
|
+
* - `description`, `keywords`: descriptive metadata.
|
|
47
|
+
* - `encodingFormat`: the media type (`application/pdf`, `image/png`…).
|
|
48
|
+
* - `contentSize`, `url`, `mentions` (the pages that show it).
|
|
49
|
+
* - `dateCreated` / `author`: when and by whom it was created, as recorded
|
|
50
|
+
* (the uploader; a legacy record may say `Unknown`).
|
|
51
|
+
* - `dateModified` / `editor`: when and by whom it last changed.
|
|
52
|
+
*
|
|
53
|
+
* Added here:
|
|
54
|
+
* - `place`: public, a vault, or a shared folder.
|
|
55
|
+
* - `owner`: the user it belongs to. A vault's files are the vault owner's.
|
|
56
|
+
* A public file's owner is its uploader when the record names a user, or
|
|
57
|
+
* `null` when it does not (`Unknown`, empty, an email used as a name):
|
|
58
|
+
* the migration gives those an owner from configuration.
|
|
59
|
+
* - `systemCategory`: the category whose rules apply. Per file, never by
|
|
60
|
+
* type. A public file today is the default category's; a vault file is
|
|
61
|
+
* the category that declares its vault, or `null` when none does.
|
|
62
|
+
*/
|
|
63
|
+
export type FileRecord = Pick<AssetRecord, 'id' | 'filename' | 'name' | 'description' | 'keywords' | 'encodingFormat' | 'contentSize' | 'url' | 'mentions' | 'dateCreated' | 'author' | 'dateModified' | 'editor'> & {
|
|
64
|
+
place: FilePlace;
|
|
65
|
+
owner: string | null;
|
|
66
|
+
systemCategory: string | null;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* What the operator inventory shows of one vault (#1859's dry run). Counts
|
|
70
|
+
* only: a vault's names, owners and metadata belong to its owner and never
|
|
71
|
+
* leave the door. A sealed vault can't be read without its owner's key, so
|
|
72
|
+
* only what is on disk is counted (bytes as stored, sealed) and its clashes
|
|
73
|
+
* are unknown (`null`).
|
|
74
|
+
*/
|
|
75
|
+
export interface VaultFileSummary {
|
|
76
|
+
owner: string;
|
|
77
|
+
vault: string;
|
|
78
|
+
systemCategory: string | null;
|
|
79
|
+
/** Whether its index could be read. False for a sealed vault (no key here) or one refused as not yet opened. */
|
|
80
|
+
readable: boolean;
|
|
81
|
+
files: number;
|
|
82
|
+
bytes: number;
|
|
83
|
+
/** Files that would get a `-###` suffix in this vault; `null` when it can't be read. */
|
|
84
|
+
clashes: number | null;
|
|
85
|
+
}
|
|
86
|
+
/** Everything the site keeps as files, for the operator (#1859): the public files in full, each vault as counts. */
|
|
87
|
+
export interface FileInventory {
|
|
88
|
+
publicFiles: FileRecord[];
|
|
89
|
+
vaults: VaultFileSummary[];
|
|
90
|
+
}
|
|
@@ -342,6 +342,15 @@ export interface PageProvider extends BaseProvider {
|
|
|
342
342
|
* @returns Canonical page title or null
|
|
343
343
|
*/
|
|
344
344
|
findPage(identifier: string): string | null;
|
|
345
|
+
/**
|
|
346
|
+
* The uuid of the page that holds `name` as its title, or as its slug (#2140).
|
|
347
|
+
* This is the store's uniqueness rule: see the provider's implementation.
|
|
348
|
+
* @param field - Which name: 'title' or 'slug'
|
|
349
|
+
* @param name - The title or slug
|
|
350
|
+
* @param ctx - Who is asking: a vault page's title is matched in its vault
|
|
351
|
+
* @returns The holder's uuid, or null when no page holds it
|
|
352
|
+
*/
|
|
353
|
+
uuidHoldingName(field: 'title' | 'slug', name: string, ctx: ActorContext): string | null;
|
|
345
354
|
/**
|
|
346
355
|
* Get a page by its UUID
|
|
347
356
|
* @param uuid - Page UUID
|
|
@@ -558,6 +567,12 @@ export interface AttachmentMetadata {
|
|
|
558
567
|
filePath: string;
|
|
559
568
|
/** Description */
|
|
560
569
|
description?: string;
|
|
570
|
+
/**
|
|
571
|
+
* True when the store already held these bytes and stored nothing new
|
|
572
|
+
* (#2130): the record returned is the existing one, under its own name.
|
|
573
|
+
* Set by `storeAttachment`; absent for a new file.
|
|
574
|
+
*/
|
|
575
|
+
alreadyHeld?: boolean;
|
|
561
576
|
/** Additional metadata */
|
|
562
577
|
[key: string]: unknown;
|
|
563
578
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { ShareGrant } from './Share.js';
|
|
19
19
|
import type { ProxyClaim } from '../managers/UserManager.js';
|
|
20
|
-
import type { SignInRecord, PendingConnection, OutsideChoice } from '../managers/AuthManager.js';
|
|
20
|
+
import type { SignInRecord, PendingConnection, OutsideChoice, PendingStepUp, StepUpPrompt } from '../managers/AuthManager.js';
|
|
21
21
|
declare module 'express-session' {
|
|
22
22
|
interface SessionData {
|
|
23
23
|
csrfToken?: string;
|
|
@@ -40,6 +40,12 @@ declare module 'express-session' {
|
|
|
40
40
|
purpose: 'register' | 'authenticate' | 'reauth';
|
|
41
41
|
expires: number;
|
|
42
42
|
};
|
|
43
|
+
/** The step-up asked for and not yet given (#2145): its permission, where it returns, whether returning redoes it. */
|
|
44
|
+
pendingStepUp?: PendingStepUp;
|
|
45
|
+
/** The path whose next page view says "Confirmed — you can do it now" after a step-up (#2145). */
|
|
46
|
+
stepUpConfirmed?: string;
|
|
47
|
+
/** A script's step-up prompts (#2193), the newest few: each id and whether it was confirmed or cancelled. */
|
|
48
|
+
stepUpPrompts?: StepUpPrompt[];
|
|
43
49
|
/** A sign-in waiting for its second factor: the handle, and where to go after (#1523). No identity yet. */
|
|
44
50
|
pendingSignIn?: string;
|
|
45
51
|
pendingRedirect?: string;
|