@jwilleke/ngdpbase-types 5.40.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/ice/fileHandling.d.ts +6 -1
- package/lib/src/ice/runExport.d.ts +41 -0
- package/lib/src/ice/runImport.d.ts +16 -7
- 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 +251 -5
- package/lib/src/ice/vet.d.ts +19 -0
- package/lib/src/managers/AttachmentManager.d.ts +86 -36
- package/lib/src/managers/AuthManager.d.ts +117 -7
- package/lib/src/managers/AuthManager.js +341 -37
- 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 +264 -16
- package/lib/src/managers/MediaManager.d.ts +2 -0
- package/lib/src/managers/SharedFolderManager.d.ts +320 -0
- package/lib/src/providers/BaseMediaProvider.d.ts +4 -0
- package/lib/src/providers/FileSystemMediaProvider.d.ts +6 -0
- package/lib/src/security/PolicyDecisionPoint.d.ts +3 -2
- package/lib/src/security/permissions.generated.d.ts +3 -3
- package/lib/src/types/File.d.ts +90 -0
- package/lib/src/types/express.d.ts +3 -1
- package/lib/src/types/linkedom.d.ts +2 -0
- package/lib/src/utils/auditEventNames.generated.d.ts +2 -0
- package/lib/src/utils/auditEventNames.generated.js +2 -0
- package/lib/src/utils/auditEvents.d.ts +3 -0
- package/lib/src/utils/auditEvents.js +3 -0
- package/lib/src/utils/fileMigrationPlan.d.ts +81 -0
- package/lib/src/utils/privateStoreAccess.d.ts +21 -0
- package/lib/src/utils/privateStoreAccess.js +11 -0
- package/lib/src/utils/privateStoreImport.d.ts +0 -10
- package/lib/src/utils/privateStorePath.d.ts +12 -0
- package/lib/src/utils/privateStorePath.js +18 -0
- package/lib/src/utils/recoveryWordsFile.d.ts +54 -0
- package/lib/src/utils/sessionEnd.d.ts +1 -0
- package/lib/src/utils/sessionEnd.js +4 -0
- package/lib/src/utils/sessionIdle.d.ts +4 -0
- package/lib/src/utils/sessionIdle.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import BaseManager, { type BackupFolder } from './BaseManager.js';
|
|
2
|
+
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
3
|
+
import { type ActorContext } from '../context/ActorContext.js';
|
|
4
|
+
import { type RecoveryWordsExport } from '../utils/recoveryWordsFile.js';
|
|
5
|
+
/** The folder every signed-in user may open (decision; the shipped `shared-folder-default` policy). */
|
|
6
|
+
export declare const DEFAULT_SHARED_FOLDER = "default";
|
|
7
|
+
/** The Executive shared folder (#2095); who opens it is the shipped policies' (`shared-folder-executive-read`, `shared-folder-executive-file`, `shared-folder-admin`). */
|
|
8
|
+
export declare const EXECUTIVE_SHARED_FOLDER = "executive";
|
|
9
|
+
/** The folders every site has, made once the recovery words are confirmed (#2125, #2095). */
|
|
10
|
+
export declare const SHIPPED_SHARED_FOLDERS: readonly string[];
|
|
11
|
+
/** Who may read and who may file in a folder, as the Shared folders screen sets it (#2126). */
|
|
12
|
+
export interface SharedFolderAccess {
|
|
13
|
+
/** Roles granted `shared-folder-read` on `shared/<name>` by the screen's policy. */
|
|
14
|
+
read: string[];
|
|
15
|
+
/** Roles granted `shared-folder-file` on `shared/<name>` by the screen's policy. */
|
|
16
|
+
file: string[];
|
|
17
|
+
}
|
|
18
|
+
/** A policy in force that reaches a folder and that the screen did not write: shipped, an add-on's, or the site's own. Shown, never changed here. */
|
|
19
|
+
export interface SharedFolderOtherPolicy {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
effect: string;
|
|
23
|
+
/** Its subjects as written (`role:editor`, `job:nightly`); empty: everyone, anonymous visitors included. */
|
|
24
|
+
subjects: string[];
|
|
25
|
+
/** Which of `shared-folder-read` and `shared-folder-file` it decides, as the PDP matches actions (an empty list or `*` is both). */
|
|
26
|
+
actions: string[];
|
|
27
|
+
/** It has an id the screen writes for this folder: saving the folder is refused until it is renamed or removed. */
|
|
28
|
+
reserved: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** The words waiting for this admin, as the confirm step needs them: never the words. */
|
|
31
|
+
export interface WaitingWords {
|
|
32
|
+
token: string;
|
|
33
|
+
/** Whether they replace a set already saved (a new set, #2144). */
|
|
34
|
+
replacing: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** What Admin → Backup shows about the organization's recovery words: never the words. */
|
|
37
|
+
export interface SharedFoldersWordsInfo {
|
|
38
|
+
/** Which set (`recoveryWordsId`): what the admin's confirmation is bound to. */
|
|
39
|
+
wordsId: string;
|
|
40
|
+
madeAt: string;
|
|
41
|
+
shownTo: string;
|
|
42
|
+
/** Whether any folder key is sealed for this set: such a set can never be discarded. */
|
|
43
|
+
protectsFolders: boolean;
|
|
44
|
+
}
|
|
45
|
+
/** The door said no: not allowed, or the request does not fit the state. Nothing was changed. */
|
|
46
|
+
export declare class SharedFolderRefusedError extends Error {
|
|
47
|
+
readonly name = "SharedFolderRefusedError";
|
|
48
|
+
}
|
|
49
|
+
/** The words typed back are not the ones shown (#2144). Nothing was saved; the words still wait, to be typed again. */
|
|
50
|
+
export declare class SharedFolderWordsMismatchError extends SharedFolderRefusedError {
|
|
51
|
+
constructor();
|
|
52
|
+
}
|
|
53
|
+
/** The keys cannot be used right now (no server key, or not the one they were made under); the message says why. */
|
|
54
|
+
export declare class SharedFoldersUnavailableError extends Error {
|
|
55
|
+
readonly name = "SharedFoldersUnavailableError";
|
|
56
|
+
}
|
|
57
|
+
declare class SharedFolderManager extends BaseManager {
|
|
58
|
+
readonly description = "The organization's shared folders: each folder's key, the organization key and the recovery words (#1775, #2124)";
|
|
59
|
+
private storageDir;
|
|
60
|
+
/** The 32-byte wrap key derived from `NGDPBASE_SHARED_FOLDERS_KEY`, read once at start; null without one. */
|
|
61
|
+
private wrapKey;
|
|
62
|
+
private keyFingerprint;
|
|
63
|
+
/** Why the keys cannot be used, or null when they can. */
|
|
64
|
+
private unavailable;
|
|
65
|
+
private file;
|
|
66
|
+
private readonly pending;
|
|
67
|
+
private readonly serial;
|
|
68
|
+
constructor(engine: WikiEngine);
|
|
69
|
+
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
70
|
+
shutdown(): Promise<void>;
|
|
71
|
+
private configuredStorageDir;
|
|
72
|
+
/** The wrapped keys are in the backup; a backup from before #2124 has none, so it is optional. */
|
|
73
|
+
backupFolders(): BackupFolder[];
|
|
74
|
+
/** The organization's recovery words as made (never the words), or null: none made yet. */
|
|
75
|
+
recoveryWordsInfo(): SharedFoldersWordsInfo | null;
|
|
76
|
+
/**
|
|
77
|
+
* Whether the admin shown the current words confirmed saving them, at the
|
|
78
|
+
* wording's current version: their consent in `shared-folders-recovery-words`
|
|
79
|
+
* for this set exists (ConsentManager's fact, asked each time). False with
|
|
80
|
+
* no words, no ConsentManager, or no wording page (then nobody could have
|
|
81
|
+
* confirmed what it says).
|
|
82
|
+
*/
|
|
83
|
+
recoveryWordsConfirmed(): Promise<boolean>;
|
|
84
|
+
/** Why the keys cannot be used now, or null when they can. */
|
|
85
|
+
unavailableReason(): string | null;
|
|
86
|
+
/**
|
|
87
|
+
* Make the organization's 12 recovery words, to show to the admin `ctx`
|
|
88
|
+
* once (decision 4). Nothing is saved: the words wait, in memory only, for
|
|
89
|
+
* that admin's `saveRecoveryWords` with the returned token, for
|
|
90
|
+
* `ngdpbase.shared-folders.words-pending-minutes`. A second call replaces
|
|
91
|
+
* the first set unsaved. Refused while a set is saved: an unconfirmed set is
|
|
92
|
+
* discarded first (`discardUnconfirmedWords`), and a saved set is replaced
|
|
93
|
+
* with `prepareNewRecoveryWords`.
|
|
94
|
+
*/
|
|
95
|
+
prepareRecoveryWords(ctx: ActorContext): Promise<{
|
|
96
|
+
token: string;
|
|
97
|
+
words: string[];
|
|
98
|
+
}>;
|
|
99
|
+
/**
|
|
100
|
+
* Make a new set of recovery words to replace the saved set (#2144): the
|
|
101
|
+
* admin `ctx`, with a fresh sign-in (`admin-system` and the step-up
|
|
102
|
+
* permission). Shown and confirmed exactly as the first set; nothing changes
|
|
103
|
+
* until `saveRecoveryWords` with all 12 typed back, which seals every folder
|
|
104
|
+
* key for the new words and retires the old ones. Abandoned or discarded
|
|
105
|
+
* (`discardRecoveryWords`), the old set stays.
|
|
106
|
+
*/
|
|
107
|
+
prepareNewRecoveryWords(ctx: ActorContext): Promise<{
|
|
108
|
+
token: string;
|
|
109
|
+
words: string[];
|
|
110
|
+
}>;
|
|
111
|
+
/** Make 12 words and hold them, for `ctx` only, until saved, discarded or expired. Replaces any set waiting for them. */
|
|
112
|
+
private holdNewWords;
|
|
113
|
+
/** The words waiting for `ctx` to type them back, or null: none, or they expired. Never the words. */
|
|
114
|
+
waitingWords(ctx: ActorContext): WaitingWords | null;
|
|
115
|
+
/**
|
|
116
|
+
* The words waiting for `ctx`, to download as a file or print (#2144),
|
|
117
|
+
* while they still wait: once saved they are gone and cannot be made again.
|
|
118
|
+
* Recorded first as `recovery-words-export` (who, which set, how; never the
|
|
119
|
+
* words; on-failure refuse). Refused for a token that is not the one shown
|
|
120
|
+
* to them, or expired; the words are not discarded by a refused export.
|
|
121
|
+
*/
|
|
122
|
+
pendingRecoveryWords(ctx: ActorContext, token: string, how: RecoveryWordsExport): Promise<{
|
|
123
|
+
words: string[];
|
|
124
|
+
madeAt: Date;
|
|
125
|
+
wordsId: string;
|
|
126
|
+
}>;
|
|
127
|
+
/** Forget the words shown to `ctx` and not saved: they are never shown again. A set they were to replace stays. */
|
|
128
|
+
discardRecoveryWords(ctx: ActorContext): void;
|
|
129
|
+
/** The words waiting for `ctx`, or undefined; expired words are forgotten. */
|
|
130
|
+
private livePending;
|
|
131
|
+
/**
|
|
132
|
+
* Discard the saved recovery words while the admin shown them has not
|
|
133
|
+
* confirmed saving them (operator, 2026-10-29): any holder of
|
|
134
|
+
* `admin-system` may, so a reminder that nobody can clear (that admin's
|
|
135
|
+
* account deleted, say) can always be cleared by making a new set and
|
|
136
|
+
* confirming it themselves. Refused once the set is confirmed, and while
|
|
137
|
+
* any folder key is sealed for it (`createFolderKey` needs a confirmed set,
|
|
138
|
+
* so such a set was confirmed once). Recorded as
|
|
139
|
+
* `shared-folder-words-discard` before the words are removed (on-failure
|
|
140
|
+
* refuse).
|
|
141
|
+
*/
|
|
142
|
+
discardUnconfirmedWords(ctx: ActorContext): Promise<void>;
|
|
143
|
+
/**
|
|
144
|
+
* Save the recovery words shown to `ctx` (`prepareRecoveryWords` or
|
|
145
|
+
* `prepareNewRecoveryWords`) once they proved they kept them: `typed` is
|
|
146
|
+
* all 12 typed back (#2144). The caller asks for the tick first, and records
|
|
147
|
+
* the confirmation with ConsentManager after (`wordsId`).
|
|
148
|
+
*
|
|
149
|
+
* - A token that is not the one shown to them, or expired: refused, and the
|
|
150
|
+
* words are discarded.
|
|
151
|
+
* - Words that do not match: `SharedFolderWordsMismatchError`; nothing is
|
|
152
|
+
* saved and the words still wait, to be typed again.
|
|
153
|
+
* - The first set: recorded as `shared-folder-words-create`, then saved.
|
|
154
|
+
* Refused when a set was saved meanwhile.
|
|
155
|
+
* - A new set (#2144): asks the fresh sign-in again, seals every folder key
|
|
156
|
+
* again for the new words from its server-key wrap, records
|
|
157
|
+
* `shared-folder-words-replace`, then writes the new set and the sealed
|
|
158
|
+
* keys as one file renamed into place: the old words stop opening the
|
|
159
|
+
* folders, and on any failure before that the old set stays as it was.
|
|
160
|
+
* Refused when the saved set changed meanwhile.
|
|
161
|
+
*
|
|
162
|
+
* Each record is on-failure refuse, written before the save.
|
|
163
|
+
*
|
|
164
|
+
* @returns Which set of words was saved (`recoveryWordsId`)
|
|
165
|
+
*/
|
|
166
|
+
saveRecoveryWords(ctx: ActorContext, token: string, typed: string): Promise<string>;
|
|
167
|
+
/**
|
|
168
|
+
* Every folder key in `folders`, its recovery copy sealed again for the
|
|
169
|
+
* words of `recovery` (#2144), opened with the server key. Nothing is
|
|
170
|
+
* written; a key that cannot be opened throws, and the caller keeps the
|
|
171
|
+
* old set.
|
|
172
|
+
*/
|
|
173
|
+
private sealedForWords;
|
|
174
|
+
/**
|
|
175
|
+
* Make folder `folderId`'s own key (decision 2): a random key, wrapped
|
|
176
|
+
* under the server key and sealed for the recovery words, so it needs the
|
|
177
|
+
* words to exist and to be confirmed saved by the admin shown them
|
|
178
|
+
* (`recoveryWordsConfirmed`; operator, 2026-10-29): no folder key is ever
|
|
179
|
+
* protected only by words nobody saved. Recorded as `shared-folder-key-create` before it is
|
|
180
|
+
* saved (on-failure refuse). The key itself is not returned; open it with
|
|
181
|
+
* `openFolderKey`.
|
|
182
|
+
*/
|
|
183
|
+
createFolderKey(ctx: ActorContext, folderId: string): Promise<void>;
|
|
184
|
+
/**
|
|
185
|
+
* Folder `folderId`'s key, unwrapped with the server key, for a caller the
|
|
186
|
+
* PDP permits `shared-folder-read` on the folder (#2125). The caller holds
|
|
187
|
+
* it for the moment it needs it and zeroes it. The page and file doors do
|
|
188
|
+
* not call this: they reach the key through the store I/O
|
|
189
|
+
* (`sharedFolderKeyFor`), only for a context permitted on the folder.
|
|
190
|
+
*/
|
|
191
|
+
openFolderKey(ctx: ActorContext, folderId: string): Promise<Buffer>;
|
|
192
|
+
/** Folder `folderId`'s key with the server key, or undefined: no such folder, or the keys cannot be used. Never asks who; its callers have. */
|
|
193
|
+
private folderKey;
|
|
194
|
+
/** The folders there are, by name: each has its key. */
|
|
195
|
+
folderNames(): string[];
|
|
196
|
+
/**
|
|
197
|
+
* Make shared folder `name` (#2125): its key (`createFolderKey`, which
|
|
198
|
+
* needs the recovery words confirmed) and its place for pages, sealed with
|
|
199
|
+
* that key. Admins only (decision 8) until the Shared folders screen
|
|
200
|
+
* (slice 3). A new folder is opened by admins (`shared-folder-admin`) and
|
|
201
|
+
* read by executives (`shared-folder-executive-read`), and by nobody else
|
|
202
|
+
* until a policy names `shared/<name>`; `default` is shipped open to every
|
|
203
|
+
* signed-in user.
|
|
204
|
+
* Recorded as `shared-folder-create` before anything is made (on-failure
|
|
205
|
+
* refuse). Refused when it exists.
|
|
206
|
+
*/
|
|
207
|
+
createFolder(ctx: ActorContext, name: string): Promise<void>;
|
|
208
|
+
/**
|
|
209
|
+
* Make the shipped folders (`SHIPPED_SHARED_FOLDERS`: `/shared/default` and
|
|
210
|
+
* `/shared/executive`) that are not there yet, once they can be made: the
|
|
211
|
+
* recovery words are confirmed. Finishing the words does this, and so does
|
|
212
|
+
* every start (`ensureShippedFoldersAtBoot`), so a site that confirmed its
|
|
213
|
+
* words before a shipped folder existed gets it on its next start.
|
|
214
|
+
* Idempotent. Returns the folders made now.
|
|
215
|
+
*/
|
|
216
|
+
ensureShippedFolders(ctx: ActorContext): Promise<string[]>;
|
|
217
|
+
/**
|
|
218
|
+
* At start, make the shipped folders a site with confirmed words does not
|
|
219
|
+
* have yet (#2095), under the system context. Best-effort: a failure is
|
|
220
|
+
* logged and never blocks start-up; nothing is tried while the keys cannot
|
|
221
|
+
* be used.
|
|
222
|
+
*/
|
|
223
|
+
ensureShippedFoldersAtBoot(): Promise<void>;
|
|
224
|
+
/**
|
|
225
|
+
* May `ctx` do `action` (a page or file action, or `shared-folder-read` /
|
|
226
|
+
* `shared-folder-file`) in folder `name`? The PDP's answer
|
|
227
|
+
* (`permitsInSharedFolder`); false for a folder that does not exist.
|
|
228
|
+
*/
|
|
229
|
+
mayInFolder(ctx: ActorContext, action: string, name: string): Promise<boolean>;
|
|
230
|
+
/**
|
|
231
|
+
* The folders `ctx` may open (`shared-folder-read`), by name. Asking opens
|
|
232
|
+
* each one's key to this context (`sharedFolderKeyFor`), so the page and
|
|
233
|
+
* file doors can read what this reader may. The session middleware asks it
|
|
234
|
+
* on every request, so a grant removed ends access on the next request
|
|
235
|
+
* (decision 5).
|
|
236
|
+
*/
|
|
237
|
+
foldersReadableBy(ctx: ActorContext): Promise<string[]>;
|
|
238
|
+
/**
|
|
239
|
+
* Record that `ctx` opened `what` (a page, its history, or a file) in
|
|
240
|
+
* folder `name` (`shared-folder-read`, on by default: every read of a
|
|
241
|
+
* shared folder is recorded, #1775). Called by what serves it, after the
|
|
242
|
+
* door allowed it. The writes are recorded by the page and file doors.
|
|
243
|
+
*/
|
|
244
|
+
recordRead(ctx: ActorContext, name: string, what: {
|
|
245
|
+
page?: string;
|
|
246
|
+
attachmentId?: string;
|
|
247
|
+
action?: string;
|
|
248
|
+
}): Promise<void>;
|
|
249
|
+
/**
|
|
250
|
+
* Who may read and who may file in folder `name`, as the Shared folders
|
|
251
|
+
* screen set it: the roles its own policies (`shared-folder-access-<name>-read`
|
|
252
|
+
* / `-file`, in the site's custom configuration, in the shape the screen
|
|
253
|
+
* writes) name, and every other policy in force that reaches the folder,
|
|
254
|
+
* shown as it is, matched as PolicyEvaluator matches it. A policy with one of
|
|
255
|
+
* the screen's ids that the screen did not write is shown, marked
|
|
256
|
+
* `reserved`. `admin-system`, as the screen is. Display only: the PDP
|
|
257
|
+
* decides every access.
|
|
258
|
+
*/
|
|
259
|
+
folderAccess(ctx: ActorContext, name: string): Promise<SharedFolderAccess & {
|
|
260
|
+
others: SharedFolderOtherPolicy[];
|
|
261
|
+
}>;
|
|
262
|
+
/** The roles a folder's access may name: every role there is but `anonymous`, by name. */
|
|
263
|
+
grantableRoles(): string[];
|
|
264
|
+
/**
|
|
265
|
+
* Set who may read and who may file in folder `name` (#2126, decision 8):
|
|
266
|
+
* writes the site's policies `shared-folder-access-<name>-read` (granting
|
|
267
|
+
* `shared-folder-read` on `shared/<name>`) and `-file` (`shared-folder-file`)
|
|
268
|
+
* to the roles given, through ConfigurationManager, and removes one whose
|
|
269
|
+
* list is empty. `admin-system`, with a fresh sign-in (the step-up
|
|
270
|
+
* permission, as every other configuration write asks). Refused for a folder
|
|
271
|
+
* that does not exist, a role that does not exist, `anonymous` (a folder is
|
|
272
|
+
* never public), and a folder one of whose ids holds a policy the screen did
|
|
273
|
+
* not write (it is never overwritten). Recorded as
|
|
274
|
+
* `shared-folder-access-change` (who, the folder, the roles before and
|
|
275
|
+
* after) once the write has succeeded; a failed write is recorded as a
|
|
276
|
+
* failure, and a record that cannot be written puts the policies back
|
|
277
|
+
* (on-failure refuse). ConfigurationManager records the configuration
|
|
278
|
+
* change as well. Nothing is written or recorded when nothing changes.
|
|
279
|
+
* Returns whether it changed.
|
|
280
|
+
*
|
|
281
|
+
* Only the screen's own policies change: a shipped or add-on policy, or one
|
|
282
|
+
* the site wrote elsewhere, still applies (`folderAccess` shows them).
|
|
283
|
+
*/
|
|
284
|
+
setFolderAccess(ctx: ActorContext, name: string, access: SharedFolderAccess): Promise<boolean>;
|
|
285
|
+
/**
|
|
286
|
+
* Folder `name`'s access as the screen sees it: the screen's own policies
|
|
287
|
+
* (the site's entry under each id, in the shape `accessPolicy` writes), the
|
|
288
|
+
* ids holding a policy the screen did not write (the site's in another
|
|
289
|
+
* shape, a deny say, or one in force from a shipped or add-on layer), and
|
|
290
|
+
* the policies in force.
|
|
291
|
+
*/
|
|
292
|
+
private accessState;
|
|
293
|
+
/** The site's own entries of the access policies (the custom configuration layer), as they are: never the shipped or an add-on's. */
|
|
294
|
+
private siteAccessPolicies;
|
|
295
|
+
/** The pages directory and the configured vault layout, where folders are kept. */
|
|
296
|
+
private pagesPlace;
|
|
297
|
+
/**
|
|
298
|
+
* Folder `folderId`'s key, opened with the recovery words instead of the
|
|
299
|
+
* server key: what recovery after a lost server key will use (a later
|
|
300
|
+
* slice: a new server key, every folder key rewrapped). Throws when the
|
|
301
|
+
* words are wrong. Works with no server key at all.
|
|
302
|
+
*/
|
|
303
|
+
openFolderKeyWithWords(ctx: ActorContext, folderId: string, words: string): Promise<Buffer>;
|
|
304
|
+
private requireAdmin;
|
|
305
|
+
/**
|
|
306
|
+
* The fresh sign-in a new set of words (#2144) and an access change (#2126)
|
|
307
|
+
* need: the PDP applies step-up to `FRESH_SIGN_IN_PERMISSION`, and never
|
|
308
|
+
* grants it to a delegated credential (an agent token).
|
|
309
|
+
*/
|
|
310
|
+
private requireFreshSignIn;
|
|
311
|
+
/** The wrap key, or throws why there is none to use. */
|
|
312
|
+
private requireUsable;
|
|
313
|
+
private pendingMinutes;
|
|
314
|
+
private fileOrNew;
|
|
315
|
+
private keysPath;
|
|
316
|
+
private readKeysFile;
|
|
317
|
+
/** Owner-only, written whole and renamed into place, so a crash never leaves half a file. */
|
|
318
|
+
private writeKeysFile;
|
|
319
|
+
}
|
|
320
|
+
export default SharedFolderManager;
|
|
@@ -68,8 +68,12 @@ export interface ScanResult {
|
|
|
68
68
|
skippedTruncated?: boolean;
|
|
69
69
|
/** Number of indexed items with no usable capture date (EXIF/QuickTime) — they sort by file mtime (#807) */
|
|
70
70
|
noCaptureDate?: number;
|
|
71
|
+
/** Paths of the first items counted in `noCaptureDate`, capped; the count stays exact (#2191) */
|
|
72
|
+
noCaptureDateFiles?: string[];
|
|
71
73
|
/** Number of indexed items with a partial (year-only / year+month) EXIF date, defaulted to Jan 1 (#808) */
|
|
72
74
|
partialCaptureDate?: number;
|
|
75
|
+
/** Paths of the first items counted in `partialCaptureDate`, capped; the count stays exact (#2191) */
|
|
76
|
+
partialCaptureDateFiles?: string[];
|
|
73
77
|
/** Number of stale index entries pruned because their file vanished from a scanned folder (#867) */
|
|
74
78
|
removed?: number;
|
|
75
79
|
/** Total elapsed time in milliseconds */
|
|
@@ -221,6 +221,12 @@ declare class FileSystemMediaProvider extends BaseMediaProvider {
|
|
|
221
221
|
* Patterns ending with `/` are directory-only; all others match both files and dirs.
|
|
222
222
|
*/
|
|
223
223
|
private matchesIgnorePattern;
|
|
224
|
+
/**
|
|
225
|
+
* Add a file to a capture-date list until CAPTURE_DATE_LIST_CAP (#2191).
|
|
226
|
+
* The path is relative to the media folder it was found under, so the
|
|
227
|
+
* notice and its email do not disclose the server's storage layout.
|
|
228
|
+
*/
|
|
229
|
+
private listCaptureDateFile;
|
|
224
230
|
/**
|
|
225
231
|
* Process a single media file: stat, EXIF read, index update.
|
|
226
232
|
*/
|
|
@@ -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
|
|
@@ -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'
|
|
@@ -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
|
+
}
|
|
@@ -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, PendingStepUp } 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;
|
|
@@ -44,6 +44,8 @@ declare module 'express-session' {
|
|
|
44
44
|
pendingStepUp?: PendingStepUp;
|
|
45
45
|
/** The path whose next page view says "Confirmed — you can do it now" after a step-up (#2145). */
|
|
46
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[];
|
|
47
49
|
/** A sign-in waiting for its second factor: the handle, and where to go after (#1523). No identity yet. */
|
|
48
50
|
pendingSignIn?: string;
|
|
49
51
|
pendingRedirect?: string;
|
|
@@ -60,6 +60,8 @@ declare module 'linkedom' {
|
|
|
60
60
|
getElementById(id: string): LinkedomElement | null;
|
|
61
61
|
querySelector(selector: string): LinkedomElement | null;
|
|
62
62
|
querySelectorAll(selector: string): LinkedomElement[];
|
|
63
|
+
/** The whole document as HTML, its doctype included. */
|
|
64
|
+
toString(): string;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
function parseHTML(html: string): { document: LinkedomDocument };
|
|
@@ -170,6 +170,8 @@ export declare const AUDIT_EVENT: {
|
|
|
170
170
|
readonly SHARE_EXTEND: "share-extend";
|
|
171
171
|
/** Share link revoked; pairs with token-revoke */
|
|
172
172
|
readonly SHARE_REVOKE: "share-revoke";
|
|
173
|
+
/** An administrator changed who may read and who may file in a shared folder on the Shared folders screen (#2126): which folder, who, and the roles before and after for each. Recorded once the folder's policies are written; a failed write is recorded as a failure, with the error, and a record that cannot be written puts the policies back */
|
|
174
|
+
readonly SHARED_FOLDER_ACCESS_CHANGE: "shared-folder-access-change";
|
|
173
175
|
/** An administrator created a shared folder (#2125): which folder and who. Recorded before the folder is made, which cannot proceed without it */
|
|
174
176
|
readonly SHARED_FOLDER_CREATE: "shared-folder-create";
|
|
175
177
|
/** A shared folder's own key was made (#2124): which folder and who, never the key. Recorded before the key is saved, which cannot proceed without it */
|
|
@@ -177,6 +177,8 @@ export const AUDIT_EVENT = {
|
|
|
177
177
|
SHARE_EXTEND: 'share-extend',
|
|
178
178
|
/** Share link revoked; pairs with token-revoke */
|
|
179
179
|
SHARE_REVOKE: 'share-revoke',
|
|
180
|
+
/** An administrator changed who may read and who may file in a shared folder on the Shared folders screen (#2126): which folder, who, and the roles before and after for each. Recorded once the folder's policies are written; a failed write is recorded as a failure, with the error, and a record that cannot be written puts the policies back */
|
|
181
|
+
SHARED_FOLDER_ACCESS_CHANGE: 'shared-folder-access-change',
|
|
180
182
|
/** An administrator created a shared folder (#2125): which folder and who. Recorded before the folder is made, which cannot proceed without it */
|
|
181
183
|
SHARED_FOLDER_CREATE: 'shared-folder-create',
|
|
182
184
|
/** A shared folder's own key was made (#2124): which folder and who, never the key. Recorded before the key is saved, which cannot proceed without it */
|
|
@@ -163,6 +163,8 @@ export type AttachmentDetail = {
|
|
|
163
163
|
stored?: false;
|
|
164
164
|
heldAs?: string;
|
|
165
165
|
offeredAs?: string;
|
|
166
|
+
/** What vetting removed before the file was kept (#2149): one note per kind, counts only, never content. */
|
|
167
|
+
cleaned?: string[];
|
|
166
168
|
}
|
|
167
169
|
/** A sealed store's file: `owner` is the store owner's identity @id in the record, `ownerUser` their username beside it (#2041). */
|
|
168
170
|
| {
|
|
@@ -172,6 +174,7 @@ export type AttachmentDetail = {
|
|
|
172
174
|
store: string;
|
|
173
175
|
ownerUser?: string;
|
|
174
176
|
};
|
|
177
|
+
cleaned?: string[];
|
|
175
178
|
};
|
|
176
179
|
export type AttachmentInput = CommonInput & {
|
|
177
180
|
op: AttachmentOp;
|
|
@@ -169,6 +169,9 @@ export function buildAttachmentAuditEvent(input) {
|
|
|
169
169
|
metadata.offeredAs = input.offeredAs;
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
+
// #2149: what vetting removed, in either form: kinds and counts, nothing of the file.
|
|
173
|
+
if (input.cleaned && input.cleaned.length > 0)
|
|
174
|
+
metadata.cleaned = [...input.cleaned];
|
|
172
175
|
const severity = op === 'delete'
|
|
173
176
|
? (viaToken ? 'high' : 'medium')
|
|
174
177
|
: (viaToken ? 'medium' : 'low');
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files migration dry run (#1859, epic #1851): what copying today's
|
|
3
|
+
* attachments into files WOULD do, worked out from the door's inventory
|
|
4
|
+
* (`FileManager.inventory`). Pure: it reads nothing and writes nothing.
|
|
5
|
+
*
|
|
6
|
+
* The rules are the decision log's
|
|
7
|
+
* (https://github.com/jwilleke/ngdpbase/issues/1851#issuecomment-6236003946):
|
|
8
|
+
* - two different files with the same name in one place get a numeric
|
|
9
|
+
* suffix, `report.pdf` and `report-001.pdf`;
|
|
10
|
+
* - each page that shows a renamed file is rewritten to its new name, so it
|
|
11
|
+
* keeps showing its own file;
|
|
12
|
+
* - an unknown or invalid uploader gets an owner from configuration.
|
|
13
|
+
*/
|
|
14
|
+
import type { FileInventory, FileRecord, VaultFileSummary } from '../types/File.js';
|
|
15
|
+
/** The configured values the plan applies (`ngdpbase.files.migration.*`). */
|
|
16
|
+
export interface FileMigrationOptions {
|
|
17
|
+
/** Recorded uploader → username, applied first (`owner-map`). */
|
|
18
|
+
ownerMap: Record<string, string>;
|
|
19
|
+
/** Owner for a file whose record names no user (`default-owner`). */
|
|
20
|
+
defaultOwner: string;
|
|
21
|
+
/** Width of the clash suffix: 3 gives `-001` (`clash-suffix-digits`). */
|
|
22
|
+
suffixDigits: number;
|
|
23
|
+
}
|
|
24
|
+
/** A file the migration would give a new name, and the pages it would rewrite to it. */
|
|
25
|
+
export interface PlannedRename {
|
|
26
|
+
id: string;
|
|
27
|
+
from: string;
|
|
28
|
+
to: string;
|
|
29
|
+
pages: string[];
|
|
30
|
+
}
|
|
31
|
+
/** Who a group of public files would belong to after the migration. */
|
|
32
|
+
export interface PlannedOwner {
|
|
33
|
+
/** The uploader the record names, or `null` when it names none. */
|
|
34
|
+
recorded: string | null;
|
|
35
|
+
owner: string;
|
|
36
|
+
/** `kept`: the record names a user; `mapped`: `owner-map`; `default`: `default-owner`. */
|
|
37
|
+
how: 'kept' | 'mapped' | 'default';
|
|
38
|
+
files: number;
|
|
39
|
+
}
|
|
40
|
+
export interface FileMigrationReport {
|
|
41
|
+
publicFiles: {
|
|
42
|
+
count: number;
|
|
43
|
+
bytes: number;
|
|
44
|
+
byType: Array<{
|
|
45
|
+
type: string;
|
|
46
|
+
files: number;
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
owners: PlannedOwner[];
|
|
50
|
+
/** Public files whose record names no user (before the owner mapping). */
|
|
51
|
+
noOwner: number;
|
|
52
|
+
/** Each clashing name, with the renames it would get. */
|
|
53
|
+
clashes: Array<{
|
|
54
|
+
name: string;
|
|
55
|
+
files: number;
|
|
56
|
+
renames: PlannedRename[];
|
|
57
|
+
}>;
|
|
58
|
+
/** Each page that would be rewritten, and to which names. */
|
|
59
|
+
pagesToRewrite: Array<{
|
|
60
|
+
page: string;
|
|
61
|
+
changes: Array<{
|
|
62
|
+
from: string;
|
|
63
|
+
to: string;
|
|
64
|
+
}>;
|
|
65
|
+
}>;
|
|
66
|
+
vaults: VaultFileSummary[];
|
|
67
|
+
}
|
|
68
|
+
/** The `-###` name a clash gets: before the extension, `route.png` → `route-001.png`. */
|
|
69
|
+
export declare function suffixedName(name: string, n: number, digits: number): string;
|
|
70
|
+
/**
|
|
71
|
+
* The new name of every file in one place that shares its name with another
|
|
72
|
+
* (id → new name). The oldest keeps the name (then the lowest id, so the
|
|
73
|
+
* answer is the same on every run); the rest are numbered from 1 in that
|
|
74
|
+
* order, skipping any name already used in the place. Names match exactly,
|
|
75
|
+
* as a link finds a file today.
|
|
76
|
+
*/
|
|
77
|
+
export declare function planClashRenames(files: ReadonlyArray<Pick<FileRecord, 'id' | 'filename' | 'dateCreated'>>, digits: number): Map<string, string>;
|
|
78
|
+
/** What the migration would do with this inventory. */
|
|
79
|
+
export declare function planFileMigration(inventory: FileInventory, opts: FileMigrationOptions): FileMigrationReport;
|
|
80
|
+
/** The report as the operator reads it. */
|
|
81
|
+
export declare function formatFileMigrationReport(r: FileMigrationReport, header?: string[]): string;
|