@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,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guards for runtime type checking
|
|
3
|
+
*
|
|
4
|
+
* This module provides type guard functions that can be used to validate
|
|
5
|
+
* data structures at runtime and provide TypeScript type narrowing.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Check if value is a valid PageFrontmatter object
|
|
9
|
+
*/
|
|
10
|
+
export function isPageFrontmatter(value) {
|
|
11
|
+
if (!value || typeof value !== 'object') {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const obj = value;
|
|
15
|
+
// Required fields
|
|
16
|
+
if (typeof obj.title !== 'string' || !obj.title) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (typeof obj.uuid !== 'string' || !obj.uuid) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (typeof obj.lastModified !== 'string' || !obj.lastModified) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
// Optional fields with type checks
|
|
26
|
+
if (obj['system-category'] !== undefined && typeof obj['system-category'] !== 'string') {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (obj.category !== undefined && typeof obj.category !== 'string') {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (obj['user-keywords'] !== undefined && !Array.isArray(obj['user-keywords'])) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Check if value is a valid WikiPage object
|
|
39
|
+
*/
|
|
40
|
+
export function isWikiPage(value) {
|
|
41
|
+
if (!value || typeof value !== 'object') {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const obj = value;
|
|
45
|
+
if (typeof obj.title !== 'string' || !obj.title) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (typeof obj.uuid !== 'string' || !obj.uuid) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (typeof obj.content !== 'string') {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (!isPageFrontmatter(obj.metadata)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (typeof obj.filePath !== 'string' || !obj.filePath) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if value is a valid PageInfo object
|
|
64
|
+
*/
|
|
65
|
+
export function isPageInfo(value) {
|
|
66
|
+
if (!value || typeof value !== 'object') {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const obj = value;
|
|
70
|
+
if (typeof obj.title !== 'string' || !obj.title) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (typeof obj.uuid !== 'string' || !obj.uuid) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (typeof obj.filePath !== 'string' || !obj.filePath) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (!isPageFrontmatter(obj.metadata)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Check if value is a valid VersionMetadata object
|
|
86
|
+
*/
|
|
87
|
+
export function isVersionMetadata(value) {
|
|
88
|
+
if (!value || typeof value !== 'object') {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const obj = value;
|
|
92
|
+
if (typeof obj.version !== 'number' || obj.version < 1) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (typeof obj.author !== 'string' || !obj.author) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (typeof obj.timestamp !== 'string' || !obj.timestamp) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const validChangeTypes = ['create', 'update', 'minor', 'major'];
|
|
102
|
+
if (typeof obj.changeType !== 'string' || !validChangeTypes.includes(obj.changeType)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (typeof obj.contentHash !== 'string' || !obj.contentHash) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (typeof obj.contentSize !== 'number' || obj.contentSize < 0) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (typeof obj.compressed !== 'boolean') {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if (typeof obj.isDelta !== 'boolean') {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Check if value is a valid VersionManifest object
|
|
121
|
+
*/
|
|
122
|
+
export function isVersionManifest(value) {
|
|
123
|
+
if (!value || typeof value !== 'object') {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const obj = value;
|
|
127
|
+
if (typeof obj.pageUuid !== 'string' || !obj.pageUuid) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (typeof obj.pageTitle !== 'string' || !obj.pageTitle) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (typeof obj.totalVersions !== 'number' || obj.totalVersions < 0) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (typeof obj.currentVersion !== 'number' || obj.currentVersion < 1) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
if (!Array.isArray(obj.versions)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
// Validate each version metadata
|
|
143
|
+
for (const version of obj.versions) {
|
|
144
|
+
if (!isVersionMetadata(version)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (typeof obj.createdAt !== 'string' || !obj.createdAt) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
if (typeof obj.updatedAt !== 'string' || !obj.updatedAt) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check if value is a valid User object
|
|
158
|
+
*/
|
|
159
|
+
export function isUser(value) {
|
|
160
|
+
if (!value || typeof value !== 'object') {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
const obj = value;
|
|
164
|
+
if (typeof obj.username !== 'string' || !obj.username) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (typeof obj.email !== 'string' || !obj.email) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
if (typeof obj.displayName !== 'string' || !obj.displayName) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if (typeof obj.password !== 'string' || !obj.password) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
// `roles` is deprecated since #617 iteration 3b — RoleManager owns
|
|
177
|
+
// OrganizationRole records canonically. Accept users without the field;
|
|
178
|
+
// when present, accept any array shape (legacy files).
|
|
179
|
+
if (obj.roles !== undefined && !Array.isArray(obj.roles)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (typeof obj.isActive !== 'boolean') {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
if (typeof obj.isSystem !== 'boolean') {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (typeof obj.isExternal !== 'boolean') {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
if (typeof obj.createdAt !== 'string' || !obj.createdAt) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (typeof obj.loginCount !== 'number' || obj.loginCount < 0) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
if (!obj.preferences || typeof obj.preferences !== 'object') {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Check if value is a valid UserSession object
|
|
204
|
+
*/
|
|
205
|
+
export function isUserSession(value) {
|
|
206
|
+
if (!value || typeof value !== 'object') {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const obj = value;
|
|
210
|
+
if (typeof obj.sessionId !== 'string' || !obj.sessionId) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
if (typeof obj.username !== 'string' || !obj.username) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (typeof obj.userId !== 'string' || !obj.userId) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
if (typeof obj.createdAt !== 'string' || !obj.createdAt) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
if (typeof obj.expiresAt !== 'string' || !obj.expiresAt) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (typeof obj.lastActivity !== 'string' || !obj.lastActivity) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Check if value is a valid AttachmentMetadata object
|
|
232
|
+
*/
|
|
233
|
+
export function isAttachmentMetadata(value) {
|
|
234
|
+
if (!value || typeof value !== 'object') {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
const obj = value;
|
|
238
|
+
if (typeof obj.id !== 'string' || !obj.id) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if (typeof obj.filename !== 'string' || !obj.filename) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
if (typeof obj.pageUuid !== 'string' || !obj.pageUuid) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
if (typeof obj.mimeType !== 'string' || !obj.mimeType) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
if (typeof obj.size !== 'number' || obj.size < 0) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
if (typeof obj.uploadedAt !== 'string' || !obj.uploadedAt) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
if (typeof obj.uploadedBy !== 'string' || !obj.uploadedBy) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
if (typeof obj.filePath !== 'string' || !obj.filePath) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Check if value is a valid AuditEvent object
|
|
266
|
+
*/
|
|
267
|
+
export function isAuditEvent(value) {
|
|
268
|
+
if (!value || typeof value !== 'object') {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
const obj = value;
|
|
272
|
+
if (typeof obj.id !== 'string' || !obj.id) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
if (typeof obj.type !== 'string' || !obj.type) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (typeof obj.actor !== 'string' || !obj.actor) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
if (typeof obj.target !== 'string' || !obj.target) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
if (typeof obj.action !== 'string' || !obj.action) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
if (typeof obj.timestamp !== 'string' || !obj.timestamp) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
const validResults = ['success', 'failure'];
|
|
291
|
+
if (typeof obj.result !== 'string' || !validResults.includes(obj.result)) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Check if value is a valid UUID (v4 format)
|
|
298
|
+
*/
|
|
299
|
+
export function isUuid(value) {
|
|
300
|
+
if (typeof value !== 'string') {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
304
|
+
return uuidRegex.test(value);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Check if value is a valid ISO 8601 timestamp
|
|
308
|
+
*/
|
|
309
|
+
export function isIsoTimestamp(value) {
|
|
310
|
+
if (typeof value !== 'string') {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const date = new Date(value);
|
|
315
|
+
return date.toISOString() === value;
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Check if value is a valid email address
|
|
323
|
+
*/
|
|
324
|
+
export function isEmail(value) {
|
|
325
|
+
if (typeof value !== 'string') {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
329
|
+
return emailRegex.test(value);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Validate and assert PageFrontmatter
|
|
333
|
+
* @throws {TypeError} if validation fails
|
|
334
|
+
*/
|
|
335
|
+
export function assertPageFrontmatter(value) {
|
|
336
|
+
if (!isPageFrontmatter(value)) {
|
|
337
|
+
throw new TypeError('Invalid PageFrontmatter object');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Validate and assert WikiPage
|
|
342
|
+
* @throws {TypeError} if validation fails
|
|
343
|
+
*/
|
|
344
|
+
export function assertWikiPage(value) {
|
|
345
|
+
if (!isWikiPage(value)) {
|
|
346
|
+
throw new TypeError('Invalid WikiPage object');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Validate and assert VersionMetadata
|
|
351
|
+
* @throws {TypeError} if validation fails
|
|
352
|
+
*/
|
|
353
|
+
export function assertVersionMetadata(value) {
|
|
354
|
+
if (!isVersionMetadata(value)) {
|
|
355
|
+
throw new TypeError('Invalid VersionMetadata object');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Validate and assert User
|
|
360
|
+
* @throws {TypeError} if validation fails
|
|
361
|
+
*/
|
|
362
|
+
export function assertUser(value) {
|
|
363
|
+
if (!isUser(value)) {
|
|
364
|
+
throw new TypeError('Invalid User object');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -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 };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* atomicWrite — never leave a half-written file behind (#1062).
|
|
3
|
+
*
|
|
4
|
+
* `fs.writeFile` over a live path truncates it first, so a crash, container
|
|
5
|
+
* kill, OOM, disk-full, or power loss part-way through leaves the file
|
|
6
|
+
* truncated: not the old content, not the new content. The page index and the
|
|
7
|
+
* version manifest already guarded against this with temp-then-rename; page
|
|
8
|
+
* content and version snapshots did not.
|
|
9
|
+
*
|
|
10
|
+
* That gap mattered more than it looks. #1061's argument for refusing a stale
|
|
11
|
+
* save is that the losing edit survives in version history — which is only true
|
|
12
|
+
* while the history files themselves are intact. A truncated snapshot corrupts
|
|
13
|
+
* the recovery path that the rest of the durability story leans on.
|
|
14
|
+
*
|
|
15
|
+
* ## What this guarantees, and what it does not
|
|
16
|
+
*
|
|
17
|
+
* `rename(2)` within a filesystem is atomic: a reader sees either the whole old
|
|
18
|
+
* file or the whole new one, never a mixture. Writing to a sibling temp file
|
|
19
|
+
* first means the live path is never in a partial state.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately stated rather than implied:
|
|
22
|
+
*
|
|
23
|
+
* - **Same directory, always.** A rename across filesystems is not atomic — it
|
|
24
|
+
* degrades to copy-then-unlink. Putting the temp file beside the target keeps
|
|
25
|
+
* it on the same device by construction, including when the pages directory
|
|
26
|
+
* is a mount of its own.
|
|
27
|
+
* - **fsync is OFF by default, deliberately.** Measured on this repo's storage:
|
|
28
|
+
* 0.13 ms/write plain, 8.62 ms/write with fsync — 65x, or about +150 seconds
|
|
29
|
+
* across a full 18,000-page index rebuild.
|
|
30
|
+
*
|
|
31
|
+
* What that cost buys is narrow. The failure this issue is about is a
|
|
32
|
+
* *process* death — deploy, OOM, node eviction — and there the kernel still
|
|
33
|
+
* flushes the page cache, so temp-then-rename alone leaves the file whole.
|
|
34
|
+
* fsync only adds protection against power loss, kernel panic, or a host
|
|
35
|
+
* crash, which the storage layer under a PV normally handles.
|
|
36
|
+
*
|
|
37
|
+
* Callers that genuinely need it pass `{ fsync: true }`. Defaulting it on
|
|
38
|
+
* would have made every bulk operation dramatically slower to buy a
|
|
39
|
+
* guarantee against a failure mode this deployment does not face.
|
|
40
|
+
* - **This is not a lock.** Two writers still race; the loser's content is
|
|
41
|
+
* simply overwritten whole rather than interleaved. Concurrency is #1061's
|
|
42
|
+
* problem, not this one.
|
|
43
|
+
*/
|
|
44
|
+
import fs from 'fs-extra';
|
|
45
|
+
import { open } from 'fs/promises';
|
|
46
|
+
import path from 'path';
|
|
47
|
+
import logger from './logger.js';
|
|
48
|
+
/** Counter making concurrent temp names unique within a process. */
|
|
49
|
+
let sequence = 0;
|
|
50
|
+
/**
|
|
51
|
+
* Write `data` to `filePath` so that the path is never observed partially
|
|
52
|
+
* written.
|
|
53
|
+
*
|
|
54
|
+
* The temp file carries the pid and a per-process counter so two writers — or
|
|
55
|
+
* two processes sharing a volume — cannot collide on it and corrupt each
|
|
56
|
+
* other's staging file.
|
|
57
|
+
*
|
|
58
|
+
* @param filePath - final destination
|
|
59
|
+
* @param data - contents to write
|
|
60
|
+
* @param encoding - defaults to utf8
|
|
61
|
+
* @param options.fsync - also flush to the device. Off by default; see the file
|
|
62
|
+
* header for why, and for the measured cost.
|
|
63
|
+
* @param options.mode - permission bits for the file, e.g. `0o600` for a file
|
|
64
|
+
* holding secrets (#1524, #1560). Set on the temp file before any data is
|
|
65
|
+
* written, so the secret is never on disk with wider permissions; the rename
|
|
66
|
+
* keeps them. Absent: the process default, as before.
|
|
67
|
+
* @param options.exclusive - create only: refuse (EEXIST) when `filePath`
|
|
68
|
+
* already exists, instead of replacing it (#1625). The finished temp file is
|
|
69
|
+
* published with a hard link, which fails atomically if the name is taken,
|
|
70
|
+
* so a crash mid-write never leaves a truncated file under the real name.
|
|
71
|
+
*/
|
|
72
|
+
export async function writeFileAtomic(filePath, data, encoding = 'utf8', options = {}) {
|
|
73
|
+
const dir = path.dirname(filePath);
|
|
74
|
+
const tempPath = path.join(dir, `.${path.basename(filePath)}.tmp.${process.pid}.${++sequence}`);
|
|
75
|
+
let handle;
|
|
76
|
+
try {
|
|
77
|
+
await fs.ensureDir(dir);
|
|
78
|
+
handle = await open(tempPath, 'w', options.mode);
|
|
79
|
+
// open()'s mode is filtered by the umask; chmod sets it exactly.
|
|
80
|
+
if (options.mode !== undefined)
|
|
81
|
+
await handle.chmod(options.mode);
|
|
82
|
+
await handle.write(typeof data === 'string' ? Buffer.from(data, encoding) : data);
|
|
83
|
+
if (options.fsync) {
|
|
84
|
+
// Only when the caller asks: see the cost note in the file header.
|
|
85
|
+
await handle.sync();
|
|
86
|
+
}
|
|
87
|
+
await handle.close();
|
|
88
|
+
handle = undefined;
|
|
89
|
+
if (options.exclusive) {
|
|
90
|
+
await fs.link(tempPath, filePath);
|
|
91
|
+
await fs.remove(tempPath);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
await fs.rename(tempPath, filePath);
|
|
95
|
+
}
|
|
96
|
+
if (options.fsync)
|
|
97
|
+
await fsyncDirectory(dir);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (handle) {
|
|
101
|
+
await handle.close().catch(() => { });
|
|
102
|
+
}
|
|
103
|
+
// Leaving the temp file behind would accumulate junk beside real pages and,
|
|
104
|
+
// being dot-prefixed, would be invisible in most listings.
|
|
105
|
+
await fs.remove(tempPath).catch(() => { });
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Flush the directory entry so the rename survives a power loss.
|
|
111
|
+
*
|
|
112
|
+
* Best-effort on purpose: not every platform or filesystem permits opening a
|
|
113
|
+
* directory for fsync, and a page save must not fail because of it. A failure
|
|
114
|
+
* here narrows the guarantee to "no partial file" without weakening it.
|
|
115
|
+
*/
|
|
116
|
+
async function fsyncDirectory(dir) {
|
|
117
|
+
let dirHandle;
|
|
118
|
+
try {
|
|
119
|
+
dirHandle = await open(dir, 'r');
|
|
120
|
+
await dirHandle.sync();
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
logger.debug(`[atomicWrite] Directory fsync skipped for ${dir}: ${String(err)}`);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await dirHandle?.close().catch(() => { });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export default writeFileAtomic;
|
|
@@ -34,6 +34,8 @@ export declare const AUDIT_EVENT: {
|
|
|
34
34
|
readonly AUTHORIZATION_DENY: "authorization-deny";
|
|
35
35
|
/** A full backup written; where it went and who asked */
|
|
36
36
|
readonly BACKUP_CREATE: "backup-create";
|
|
37
|
+
/** An administrator downloaded the backup key file from the backup-key page (#2134): who, and the key's fingerprint, never the key. Recorded before the file is made, which cannot proceed without it */
|
|
38
|
+
readonly BACKUP_KEY_DOWNLOAD: "backup-key-download";
|
|
37
39
|
/** A restore of a backup's databases started from Admin → Backup: which backup and who asked; recorded before the fresh backup and the staging, which cannot proceed without it (#1723) */
|
|
38
40
|
readonly BACKUP_RESTORE: "backup-restore";
|
|
39
41
|
/** A staged restore (site or databases-only) put in place at start: which backup, when it was taken, who started the restore and which parts were applied; written into the restored audit log, so the log itself says a restore happened (#1876) */
|
|
@@ -138,6 +140,8 @@ export declare const AUDIT_EVENT: {
|
|
|
138
140
|
readonly REAUTH_PROMPT: "reauth-prompt";
|
|
139
141
|
/** Step-up: the user re-authenticated, with the factor used */
|
|
140
142
|
readonly REAUTH_SUCCESS: "reauth-success";
|
|
143
|
+
/** Recovery words still waiting to be confirmed were downloaded as a file or opened to print (#2144): who, which words (the shared folders' or a user's vault key), the set's id and how, never the words. Recorded before the words are sent, which cannot proceed without it */
|
|
144
|
+
readonly RECOVERY_WORDS_EXPORT: "recovery-words-export";
|
|
141
145
|
/** A request was approved and the add-on's handler carried it out (#2072): who decided, its id, kind and handler; never the note, summary or details */
|
|
142
146
|
readonly REVIEW_APPROVED: "review-approved";
|
|
143
147
|
/** A request was approved but the add-on's handler did not carry it out, so it stays open (#2072): who approved it, its id, kind and handler */
|
|
@@ -166,12 +170,20 @@ export declare const AUDIT_EVENT: {
|
|
|
166
170
|
readonly SHARE_EXTEND: "share-extend";
|
|
167
171
|
/** Share link revoked; pairs with token-revoke */
|
|
168
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";
|
|
175
|
+
/** An administrator created a shared folder (#2125): which folder and who. Recorded before the folder is made, which cannot proceed without it */
|
|
176
|
+
readonly SHARED_FOLDER_CREATE: "shared-folder-create";
|
|
169
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 */
|
|
170
178
|
readonly SHARED_FOLDER_KEY_CREATE: "shared-folder-key-create";
|
|
179
|
+
/** A shared folder's page or file was opened (#2125): who, the folder and what. On by default: every read of a shared folder is recorded (#1775) */
|
|
180
|
+
readonly SHARED_FOLDER_READ: "shared-folder-read";
|
|
171
181
|
/** The organization's 12 recovery words for shared folders were made and saved (#2124): the admin who was shown them, and which set (an id, never the words). Recorded before the words are saved, which cannot proceed without it */
|
|
172
182
|
readonly SHARED_FOLDER_WORDS_CREATE: "shared-folder-words-create";
|
|
173
183
|
/** An administrator discarded the organization's shared-folders recovery words before the admin shown them confirmed saving them (#2124): who, which set (an id) and who it was shown to. Recorded before the words are removed, which cannot proceed without it */
|
|
174
184
|
readonly SHARED_FOLDER_WORDS_DISCARD: "shared-folder-words-discard";
|
|
185
|
+
/** An administrator, with a fresh sign-in, replaced the organization's shared-folders recovery words with a new set they typed back (#2144): who, the new set and the set it replaced (ids, never the words) and how many folder keys were sealed for the new set. The old words stop opening the folders. Recorded before the new set is saved, which cannot proceed without it */
|
|
186
|
+
readonly SHARED_FOLDER_WORDS_REPLACE: "shared-folder-words-replace";
|
|
175
187
|
/** Sign-in approval: the browser that was waiting gave up the sign-in (#1532) */
|
|
176
188
|
readonly SIGNINAPPROVAL_ABANDON: "signinapproval-abandon";
|
|
177
189
|
/** Sign-in approval: the user pressed Approve (#1532) */
|
|
@@ -41,6 +41,8 @@ export const AUDIT_EVENT = {
|
|
|
41
41
|
AUTHORIZATION_DENY: 'authorization-deny',
|
|
42
42
|
/** A full backup written; where it went and who asked */
|
|
43
43
|
BACKUP_CREATE: 'backup-create',
|
|
44
|
+
/** An administrator downloaded the backup key file from the backup-key page (#2134): who, and the key's fingerprint, never the key. Recorded before the file is made, which cannot proceed without it */
|
|
45
|
+
BACKUP_KEY_DOWNLOAD: 'backup-key-download',
|
|
44
46
|
/** A restore of a backup's databases started from Admin → Backup: which backup and who asked; recorded before the fresh backup and the staging, which cannot proceed without it (#1723) */
|
|
45
47
|
BACKUP_RESTORE: 'backup-restore',
|
|
46
48
|
/** A staged restore (site or databases-only) put in place at start: which backup, when it was taken, who started the restore and which parts were applied; written into the restored audit log, so the log itself says a restore happened (#1876) */
|
|
@@ -145,6 +147,8 @@ export const AUDIT_EVENT = {
|
|
|
145
147
|
REAUTH_PROMPT: 'reauth-prompt',
|
|
146
148
|
/** Step-up: the user re-authenticated, with the factor used */
|
|
147
149
|
REAUTH_SUCCESS: 'reauth-success',
|
|
150
|
+
/** Recovery words still waiting to be confirmed were downloaded as a file or opened to print (#2144): who, which words (the shared folders' or a user's vault key), the set's id and how, never the words. Recorded before the words are sent, which cannot proceed without it */
|
|
151
|
+
RECOVERY_WORDS_EXPORT: 'recovery-words-export',
|
|
148
152
|
/** A request was approved and the add-on's handler carried it out (#2072): who decided, its id, kind and handler; never the note, summary or details */
|
|
149
153
|
REVIEW_APPROVED: 'review-approved',
|
|
150
154
|
/** A request was approved but the add-on's handler did not carry it out, so it stays open (#2072): who approved it, its id, kind and handler */
|
|
@@ -173,12 +177,20 @@ export const AUDIT_EVENT = {
|
|
|
173
177
|
SHARE_EXTEND: 'share-extend',
|
|
174
178
|
/** Share link revoked; pairs with token-revoke */
|
|
175
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',
|
|
182
|
+
/** An administrator created a shared folder (#2125): which folder and who. Recorded before the folder is made, which cannot proceed without it */
|
|
183
|
+
SHARED_FOLDER_CREATE: 'shared-folder-create',
|
|
176
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 */
|
|
177
185
|
SHARED_FOLDER_KEY_CREATE: 'shared-folder-key-create',
|
|
186
|
+
/** A shared folder's page or file was opened (#2125): who, the folder and what. On by default: every read of a shared folder is recorded (#1775) */
|
|
187
|
+
SHARED_FOLDER_READ: 'shared-folder-read',
|
|
178
188
|
/** The organization's 12 recovery words for shared folders were made and saved (#2124): the admin who was shown them, and which set (an id, never the words). Recorded before the words are saved, which cannot proceed without it */
|
|
179
189
|
SHARED_FOLDER_WORDS_CREATE: 'shared-folder-words-create',
|
|
180
190
|
/** An administrator discarded the organization's shared-folders recovery words before the admin shown them confirmed saving them (#2124): who, which set (an id) and who it was shown to. Recorded before the words are removed, which cannot proceed without it */
|
|
181
191
|
SHARED_FOLDER_WORDS_DISCARD: 'shared-folder-words-discard',
|
|
192
|
+
/** An administrator, with a fresh sign-in, replaced the organization's shared-folders recovery words with a new set they typed back (#2144): who, the new set and the set it replaced (ids, never the words) and how many folder keys were sealed for the new set. The old words stop opening the folders. Recorded before the new set is saved, which cannot proceed without it */
|
|
193
|
+
SHARED_FOLDER_WORDS_REPLACE: 'shared-folder-words-replace',
|
|
182
194
|
/** Sign-in approval: the browser that was waiting gave up the sign-in (#1532) */
|
|
183
195
|
SIGNINAPPROVAL_ABANDON: 'signinapproval-abandon',
|
|
184
196
|
/** Sign-in approval: the user pressed Approve (#1532) */
|
|
@@ -121,6 +121,8 @@ export interface PageMutationInput extends CommonInput {
|
|
|
121
121
|
from: string;
|
|
122
122
|
to: string;
|
|
123
123
|
} | null;
|
|
124
|
+
/** A `create` from an import whose user gave the page another title (#2130): the file's own name. */
|
|
125
|
+
importedFrom?: string | null;
|
|
124
126
|
}
|
|
125
127
|
export interface TokenInput extends CommonInput {
|
|
126
128
|
op: TokenOp;
|
|
@@ -151,6 +153,18 @@ export type AttachmentDetail = {
|
|
|
151
153
|
filename: string;
|
|
152
154
|
pageName?: string | null;
|
|
153
155
|
sizeBytes?: number | null;
|
|
156
|
+
/** An import's own file name, when the user gave the file another (#2130). */
|
|
157
|
+
importedFrom?: string | null;
|
|
158
|
+
/**
|
|
159
|
+
* `false` when the pool already held these bytes and nothing new was
|
|
160
|
+
* stored (#2130): `filename` and `heldAs` are then the name it is held
|
|
161
|
+
* under, and `offeredAs` the name the upload offered. Absent: stored.
|
|
162
|
+
*/
|
|
163
|
+
stored?: false;
|
|
164
|
+
heldAs?: string;
|
|
165
|
+
offeredAs?: string;
|
|
166
|
+
/** What vetting removed before the file was kept (#2149): one note per kind, counts only, never content. */
|
|
167
|
+
cleaned?: string[];
|
|
154
168
|
}
|
|
155
169
|
/** A sealed store's file: `owner` is the store owner's identity @id in the record, `ownerUser` their username beside it (#2041). */
|
|
156
170
|
| {
|
|
@@ -160,6 +174,7 @@ export type AttachmentDetail = {
|
|
|
160
174
|
store: string;
|
|
161
175
|
ownerUser?: string;
|
|
162
176
|
};
|
|
177
|
+
cleaned?: string[];
|
|
163
178
|
};
|
|
164
179
|
export type AttachmentInput = CommonInput & {
|
|
165
180
|
op: AttachmentOp;
|
|
@@ -79,7 +79,7 @@ function tokenMetadata(viaToken) {
|
|
|
79
79
|
* nothing the file does not already say.
|
|
80
80
|
*/
|
|
81
81
|
export function buildPageMutationAuditEvent(input) {
|
|
82
|
-
const { op, pageName, uuid, fromPageName, rewriteOf, viaToken } = input;
|
|
82
|
+
const { op, pageName, uuid, fromPageName, rewriteOf, importedFrom, viaToken } = input;
|
|
83
83
|
const who = whoOf(input);
|
|
84
84
|
const metadata = {
|
|
85
85
|
pageName,
|
|
@@ -98,6 +98,10 @@ export function buildPageMutationAuditEvent(input) {
|
|
|
98
98
|
metadata.rewriteFrom = rewriteOf.from;
|
|
99
99
|
metadata.rewriteTo = rewriteOf.to;
|
|
100
100
|
}
|
|
101
|
+
// #2130: the file an imported page came from, when the user named the page otherwise.
|
|
102
|
+
if (op === 'create' && importedFrom) {
|
|
103
|
+
metadata.importedFrom = importedFrom;
|
|
104
|
+
}
|
|
101
105
|
return {
|
|
102
106
|
eventType: PAGE_EVENT[op],
|
|
103
107
|
...who.fields,
|
|
@@ -155,7 +159,19 @@ export function buildAttachmentAuditEvent(input) {
|
|
|
155
159
|
metadata.pageName = input.pageName;
|
|
156
160
|
if (typeof input.sizeBytes === 'number')
|
|
157
161
|
metadata.sizeBytes = input.sizeBytes;
|
|
162
|
+
if (input.importedFrom)
|
|
163
|
+
metadata.importedFrom = input.importedFrom;
|
|
164
|
+
if (input.stored === false) {
|
|
165
|
+
metadata.stored = false;
|
|
166
|
+
if (input.heldAs)
|
|
167
|
+
metadata.heldAs = input.heldAs;
|
|
168
|
+
if (input.offeredAs)
|
|
169
|
+
metadata.offeredAs = input.offeredAs;
|
|
170
|
+
}
|
|
158
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];
|
|
159
175
|
const severity = op === 'delete'
|
|
160
176
|
? (viaToken ? 'high' : 'medium')
|
|
161
177
|
: (viaToken ? 'medium' : 'low');
|