@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.
Files changed (99) hide show
  1. package/lib/packages/ngdpbase-types/index.d.ts +1 -1
  2. package/lib/src/context/ApiContext.d.ts +3 -2
  3. package/lib/src/context/ApiContext.js +4 -2
  4. package/lib/src/ice/fileHandling.d.ts +6 -1
  5. package/lib/src/ice/givenName.d.ts +19 -0
  6. package/lib/src/ice/runExport.d.ts +41 -0
  7. package/lib/src/ice/runImport.d.ts +37 -8
  8. package/lib/src/ice/shims/csv.d.ts +24 -0
  9. package/lib/src/ice/shims/export.d.ts +23 -0
  10. package/lib/src/ice/shims/index.d.ts +3 -1
  11. package/lib/src/ice/shims/svg.d.ts +27 -0
  12. package/lib/src/ice/shims/takeout.d.ts +83 -0
  13. package/lib/src/ice/types.d.ts +301 -5
  14. package/lib/src/ice/vet.d.ts +19 -0
  15. package/lib/src/managers/AttachmentManager.d.ts +100 -36
  16. package/lib/src/managers/AuthManager.d.ts +152 -12
  17. package/lib/src/managers/AuthManager.js +3830 -0
  18. package/lib/src/managers/BackgroundJobManager.d.ts +25 -0
  19. package/lib/src/managers/BackupManager.d.ts +24 -0
  20. package/lib/src/managers/BackupManager.js +48 -1
  21. package/lib/src/managers/ExportManager.d.ts +16 -0
  22. package/lib/src/managers/ExportManager.js +25 -0
  23. package/lib/src/managers/FileManager.d.ts +182 -0
  24. package/lib/src/managers/ImportManager.d.ts +279 -16
  25. package/lib/src/managers/MediaManager.d.ts +2 -0
  26. package/lib/src/managers/PageManager.d.ts +53 -0
  27. package/lib/src/managers/SearchManager.d.ts +4 -0
  28. package/lib/src/managers/SharedFolderManager.d.ts +320 -0
  29. package/lib/src/managers/ValidationManager.d.ts +5 -2
  30. package/lib/src/middleware/csrf.js +99 -0
  31. package/lib/src/providers/AgentTokenAuthProvider.js +46 -0
  32. package/lib/src/providers/AuthentikBearerAuthProvider.js +176 -0
  33. package/lib/src/providers/BaseCredentialsProvider.js +21 -0
  34. package/lib/src/providers/BaseMediaProvider.d.ts +4 -0
  35. package/lib/src/providers/BasePageProvider.d.ts +8 -0
  36. package/lib/src/providers/CloudflareAccessAuthProvider.js +160 -0
  37. package/lib/src/providers/FileCredentialsProvider.js +188 -0
  38. package/lib/src/providers/FileSystemMediaProvider.d.ts +6 -0
  39. package/lib/src/providers/MagicLinkAuthProvider.js +333 -0
  40. package/lib/src/providers/OidcAuthProvider.js +350 -0
  41. package/lib/src/providers/OidcBearerAuthProvider.js +27 -0
  42. package/lib/src/providers/PasskeyAuthProvider.js +147 -0
  43. package/lib/src/providers/PasswordAuthProvider.js +44 -0
  44. package/lib/src/security/PolicyDecisionPoint.d.ts +3 -2
  45. package/lib/src/security/pageActions.js +43 -0
  46. package/lib/src/security/permissions.generated.d.ts +7 -3
  47. package/lib/src/types/File.d.ts +90 -0
  48. package/lib/src/types/Provider.d.ts +15 -0
  49. package/lib/src/types/express.d.ts +7 -1
  50. package/lib/src/types/guards.js +366 -0
  51. package/lib/src/types/linkedom.d.ts +2 -0
  52. package/lib/src/utils/atomicWrite.js +129 -0
  53. package/lib/src/utils/auditEventNames.generated.d.ts +12 -0
  54. package/lib/src/utils/auditEventNames.generated.js +12 -0
  55. package/lib/src/utils/auditEvents.d.ts +15 -0
  56. package/lib/src/utils/auditEvents.js +17 -1
  57. package/lib/src/utils/bip39English.js +2 -0
  58. package/lib/src/utils/browserDescription.js +28 -0
  59. package/lib/src/utils/delegation.js +79 -0
  60. package/lib/src/utils/deriveUsername.js +38 -0
  61. package/lib/src/utils/emailForLog.js +28 -0
  62. package/lib/src/utils/fileMigrationPlan.d.ts +81 -0
  63. package/lib/src/utils/instanceEnvBackup.d.ts +6 -0
  64. package/lib/src/utils/instanceEnvBackup.js +13 -0
  65. package/lib/src/utils/ipLocation.js +106 -0
  66. package/lib/src/utils/magicLinkDeviceState.js +97 -0
  67. package/lib/src/utils/pageUrl.d.ts +1 -0
  68. package/lib/src/utils/passwordHash.js +150 -0
  69. package/lib/src/utils/permissionCatalog.js +102 -0
  70. package/lib/src/utils/privateStoreAccess.d.ts +25 -0
  71. package/lib/src/utils/privateStoreAccess.js +22 -1
  72. package/lib/src/utils/privateStoreCatalogs.js +57 -0
  73. package/lib/src/utils/privateStoreCrypto.js +266 -0
  74. package/lib/src/utils/privateStoreDoor.d.ts +10 -0
  75. package/lib/src/utils/privateStoreDoor.js +298 -0
  76. package/lib/src/utils/privateStoreImport.d.ts +0 -10
  77. package/lib/src/utils/privateStoreMeta.js +123 -0
  78. package/lib/src/utils/privateStorePath.d.ts +37 -1
  79. package/lib/src/utils/privateStorePath.js +60 -3
  80. package/lib/src/utils/privateStoreTakeout.js +149 -0
  81. package/lib/src/utils/privateStoreUnlock.d.ts +3 -0
  82. package/lib/src/utils/privateStoreUnlock.js +499 -0
  83. package/lib/src/utils/recoveryWordsFile.d.ts +54 -0
  84. package/lib/src/utils/safeRedirect.js +55 -0
  85. package/lib/src/utils/sessionAccount.js +55 -0
  86. package/lib/src/utils/sessionEnd.d.ts +1 -0
  87. package/lib/src/utils/sessionEnd.js +42 -0
  88. package/lib/src/utils/sessionGeneration.js +29 -0
  89. package/lib/src/utils/sessionIdle.d.ts +4 -0
  90. package/lib/src/utils/sessionIdle.js +72 -0
  91. package/lib/src/utils/sessionLimits.js +72 -0
  92. package/lib/src/utils/sessionSecurity.js +122 -0
  93. package/lib/src/utils/shareLockbox.js +101 -0
  94. package/lib/src/utils/sharedFolderAccess.d.ts +57 -0
  95. package/lib/src/utils/sharedFolderAccess.js +80 -0
  96. package/lib/src/utils/userCreateError.js +82 -0
  97. package/lib/src/utils/username.d.ts +2 -0
  98. package/lib/src/utils/username.js +105 -0
  99. 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');