@jwilleke/ngdpbase-types 5.41.0 → 5.42.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 (40) hide show
  1. package/lib/packages/media-date/index.d.ts +34 -0
  2. package/lib/src/managers/AddonsManager.d.ts +8 -0
  3. package/lib/src/managers/AttachmentManager.d.ts +59 -0
  4. package/lib/src/managers/AuditManager.d.ts +477 -0
  5. package/lib/src/managers/AuthManager.d.ts +247 -15
  6. package/lib/src/managers/AuthManager.js +588 -45
  7. package/lib/src/managers/ConfigurationManager.d.ts +6 -0
  8. package/lib/src/managers/ConsentManager.d.ts +15 -0
  9. package/lib/src/managers/ImportManager.d.ts +15 -0
  10. package/lib/src/managers/MediaManager.d.ts +9 -0
  11. package/lib/src/providers/BaseAttachmentProvider.d.ts +4 -0
  12. package/lib/src/providers/BaseAuditProvider.d.ts +492 -0
  13. package/lib/src/providers/BaseAuthProvider.d.ts +2 -0
  14. package/lib/src/providers/BaseCredentialsProvider.d.ts +2 -2
  15. package/lib/src/providers/BaseCredentialsProvider.js +1 -1
  16. package/lib/src/providers/BasicAttachmentProvider.d.ts +9 -0
  17. package/lib/src/providers/FileCredentialsProvider.js +1 -1
  18. package/lib/src/providers/FileSystemMediaProvider.d.ts +19 -0
  19. package/lib/src/providers/NullAuditProvider.d.ts +82 -0
  20. package/lib/src/providers/TotpAuthProvider.d.ts +55 -0
  21. package/lib/src/providers/TotpAuthProvider.js +145 -0
  22. package/lib/src/types/Consent.d.ts +17 -4
  23. package/lib/src/types/Consent.js +11 -1
  24. package/lib/src/types/Provider.d.ts +6 -0
  25. package/lib/src/types/express.d.ts +22 -0
  26. package/lib/src/utils/auditChain.d.ts +83 -0
  27. package/lib/src/utils/auditHeadWitness.d.ts +97 -0
  28. package/lib/src/utils/auditLifecycle.d.ts +71 -0
  29. package/lib/src/utils/browserDescription.d.ts +18 -0
  30. package/lib/src/utils/browserDescription.js +12 -0
  31. package/lib/src/utils/mediaFolderOverlap.d.ts +50 -0
  32. package/lib/src/utils/postureRecord.d.ts +73 -0
  33. package/lib/src/utils/privateStorePath.d.ts +6 -0
  34. package/lib/src/utils/privateStorePath.js +8 -0
  35. package/lib/src/utils/securityPosture.d.ts +52 -0
  36. package/lib/src/utils/tlsConfig.d.ts +59 -0
  37. package/lib/src/utils/totpSeedCipher.d.ts +4 -0
  38. package/lib/src/utils/totpSeedCipher.js +45 -0
  39. package/package.json +5 -2
  40. package/lib/src/providers/mediaFilenameDate.d.ts +0 -22
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Process lifecycle as audit events (#1149).
3
+ *
4
+ * Nothing recorded that an instance started or stopped, so an unclean exit was
5
+ * undetectable from the log. That matters because of #1148: on an unclean exit
6
+ * the buffered audit records are lost, the chain resumes from the last WRITTEN
7
+ * record at boot, verification passes, and nothing shows anything is absent.
8
+ * The log does not lie about the records it holds — it simply cannot say that
9
+ * records are missing.
10
+ *
11
+ * A `system-start` with no `system-shutdown` before it says exactly that. It
12
+ * cannot recover the lost records; it marks the window in which records may be
13
+ * absent, which is the difference between an audit trail with a known gap and
14
+ * one that quietly appears complete.
15
+ */
16
+ import type { AuditEvent } from './auditEvents.js';
17
+ /** What the previous run's ending can be established to be. */
18
+ export type PreviousRun =
19
+ /** A shutdown was recorded after the last start. */
20
+ 'clean'
21
+ /** A start was recorded with no shutdown after it. Records may be missing. */
22
+ | 'unclean'
23
+ /** No start on record — a first boot, or history beyond the log. */
24
+ | 'none'
25
+ /** The records present cannot establish an ordering. */
26
+ | 'unknown';
27
+ /** The minimum of a stored record this assessment needs. */
28
+ export interface LifecycleRecord {
29
+ timestamp?: string;
30
+ }
31
+ /**
32
+ * Decide how the previous run ended, from the latest record of each phase.
33
+ *
34
+ * Deliberately conservative in three places, because the whole value of this
35
+ * is that it does not overclaim:
36
+ *
37
+ * - No start on record is `none`, not `unclean`. A first boot must not accuse
38
+ * itself, or every new instance cries wolf and the signal stops being read.
39
+ * - A shutdown with no start is `unknown`, not `clean`. That is only reachable
40
+ * through a truncated or rotated log, and concluding "clean" from half the
41
+ * evidence is the failure this exists to prevent.
42
+ * - Equal timestamps are `unclean`. Same-millisecond records cannot establish
43
+ * ordering, so the shutdown did not *demonstrably* follow the start.
44
+ */
45
+ export declare function assessPreviousRun(lastStart: LifecycleRecord | null, lastShutdown: LifecycleRecord | null): PreviousRun;
46
+ export interface LifecycleEventInput {
47
+ /** The system principal (#631). Falls back to the literal only where no UserManager answers. */
48
+ principal?: string;
49
+ phase: 'start' | 'shutdown';
50
+ /** The running version, so a restart is distinguishable from an upgrade. */
51
+ version: string;
52
+ pid: number;
53
+ /** Required on a start, meaningless on a shutdown. */
54
+ previousRun?: PreviousRun;
55
+ /**
56
+ * The transport the instance actually bound (#1153).
57
+ *
58
+ * "This instance is serving HTTP" is a security fact, and the log had no
59
+ * record of it — only the version and pid.
60
+ */
61
+ scheme?: 'http' | 'https';
62
+ }
63
+ /**
64
+ * Build the audit event for a process starting or stopping.
65
+ *
66
+ * `previousRun` appears on a start only, following the rule `page-rename` and
67
+ * `token-mint` already use: a field present on every event is useless as a
68
+ * filter. A shutdown is a statement about itself and makes no claim about the
69
+ * past.
70
+ */
71
+ export declare function buildLifecycleAuditEvent(input: LifecycleEventInput): AuditEvent;
@@ -9,3 +9,21 @@
9
9
  * Null when the header names neither a browser nor a device we know.
10
10
  */
11
11
  export declare function describeBrowser(userAgent: string | undefined): string | null;
12
+ /**
13
+ * #1523 slice 1c — the browser and operating system a remembered browser's
14
+ * device key was issued to, from the User-Agent and the `Sec-CH-UA-Platform`
15
+ * client hint every Chromium browser sends. Families only ("Chrome",
16
+ * "Windows"), never versions, so an update keeps the browser remembered. No
17
+ * fingerprinting beyond what each request already carries (operator,
18
+ * 2026-10-04). Null when the browser or the system cannot be told: such a
19
+ * browser is never remembered.
20
+ */
21
+ export interface BrowserIdentity {
22
+ browser: string;
23
+ os: string;
24
+ /** The `Sec-CH-UA-Platform` hint, unquoted; empty when the browser sent none. */
25
+ platform: string;
26
+ }
27
+ export declare function browserIdentity(userAgent: string | undefined, platformHint: string | undefined): BrowserIdentity | null;
28
+ /** Whether two identities name the same browser on the same system, the hint included (both sent it, or neither). */
29
+ export declare function sameBrowser(a: BrowserIdentity, b: BrowserIdentity): boolean;
@@ -26,3 +26,15 @@ export function describeBrowser(userAgent) {
26
26
  return `${browser} on ${device}`;
27
27
  return browser ?? device;
28
28
  }
29
+ export function browserIdentity(userAgent, platformHint) {
30
+ const described = describeBrowser(userAgent);
31
+ const [browser, os] = described?.split(' on ') ?? [];
32
+ if (!browser || !os)
33
+ return null;
34
+ const platform = (platformHint ?? '').trim().replace(/^"|"$/g, '').slice(0, 40);
35
+ return { browser, os, platform };
36
+ }
37
+ /** Whether two identities name the same browser on the same system, the hint included (both sent it, or neither). */
38
+ export function sameBrowser(a, b) {
39
+ return a.browser === b.browser && a.os === b.os && a.platform === b.platform;
40
+ }
@@ -0,0 +1,50 @@
1
+ /** One place private files are kept, and what to call it in a message. */
2
+ export interface PrivateStorageRoot {
3
+ label: string;
4
+ path: string;
5
+ }
6
+ /** How a media folder overlaps a root: it is the root, holds it, or is inside it. */
7
+ export type OverlapRelation = 'equals' | 'contains' | 'is inside';
8
+ export interface MediaFolderOverlap {
9
+ folder: string;
10
+ /** 1-based position of the folder in `ngdpbase.media.folders`. */
11
+ position: number;
12
+ root: PrivateStorageRoot;
13
+ relation: OverlapRelation;
14
+ }
15
+ /** Minimal configuration surface, so tests need no live engine. */
16
+ export interface StorageRootConfig {
17
+ getProperty(key: string, defaultValue: unknown): unknown;
18
+ getResolvedDataPath(key: string, defaultValue: string): string;
19
+ }
20
+ /**
21
+ * Where private files live: the vaults (shared folders' pages and files are
22
+ * the `shared-folders` container's vault, so they are in here), the legacy
23
+ * `private/` root a site may not have moved yet, the shared folders' key
24
+ * storage, and two places in attachment storage: the old private attachments
25
+ * the migration leaves behind and reads as a fallback, and the thumbnail cache,
26
+ * which holds thumbnails of vault images. Attachment paths are the ones
27
+ * BasicAttachmentProvider resolves.
28
+ */
29
+ export declare function privateStorageRoots(config: StorageRootConfig): PrivateStorageRoot[];
30
+ /**
31
+ * The real path of `p`, or of its nearest existing ancestor with the rest
32
+ * appended. `p` is not normalized first: `link/..` is the parent of where
33
+ * `link` points, not the directory holding `link` (#2195).
34
+ */
35
+ export declare function realPathOf(p: string): string;
36
+ /** Whether `child` is `parent` or under it; both already real paths. */
37
+ export declare function isSameOrUnder(child: string, parent: string): boolean;
38
+ /** How `folder` overlaps `root`, or null for no overlap. */
39
+ export declare function overlapRelation(folder: string, root: string): OverlapRelation | null;
40
+ /** The first private storage root `folder` overlaps, or null. */
41
+ export declare function findMediaFolderOverlap(folder: string, roots: PrivateStorageRoot[], position?: number): MediaFolderOverlap | null;
42
+ /** Each of `folders` that overlaps a root, with its position in the list. */
43
+ export declare function findMediaFolderOverlaps(folders: string[], roots: PrivateStorageRoot[]): MediaFolderOverlap[];
44
+ /**
45
+ * The message for a refused folder. It names the folder by its position and
46
+ * the kind of storage. It never echoes the folder: one inside a vault would
47
+ * name the vault's owner and id, and the message reaches admin notices and
48
+ * their email.
49
+ */
50
+ export declare function describeMediaFolderOverlap(o: MediaFolderOverlap): string;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Record the security posture at boot and compare it against the previous boot
3
+ * (#1156, D19 of docs/security-posture.md).
4
+ *
5
+ * `config-change` (#1150) audits every change made through `setProperty()`,
6
+ * which leaves two holes:
7
+ *
8
+ * - An `app-custom-config.json` edited __directly on disk__ never passes
9
+ * through `setProperty()` and emits nothing.
10
+ * - The state an instance __started in__ is never stated, only its deltas.
11
+ *
12
+ * Recording the posture at every start closes the second. Comparing that record
13
+ * against the previous start's closes the first: a change made on disk, or
14
+ * while the process was stopped, shows up as a difference between two
15
+ * consecutive boots __even though nothing observed the edit itself__.
16
+ *
17
+ * This is not the self-scoring D20 rejects. The comparison is against this
18
+ * instance's own previous state, a fact it holds, rather than against a
19
+ * recommended value set nobody can define.
20
+ */
21
+ import type { PostureGroup } from './securityPosture.js';
22
+ /** What a secret ingredient records instead of its value. */
23
+ export declare const SECRET_PLACEHOLDER = "[secret]";
24
+ /** The posture as recorded: one flat map of ingredient to value. */
25
+ export type FlatPosture = Record<string, unknown>;
26
+ /**
27
+ * Flatten the grouped posture for recording.
28
+ *
29
+ * A secret records that it is __set__ and never what it is set to. An entry
30
+ * naming a key alongside its value would reintroduce the disclosure
31
+ * `ngdpbase.config.sensitive-values` (its `secret` entries) exists to prevent, by a different route and
32
+ * into a file with longer retention than the logs it already guards.
33
+ */
34
+ export declare function flattenPosture(groups: readonly PostureGroup[]): FlatPosture;
35
+ export interface PostureDiff {
36
+ /**
37
+ * False when there is no previous record to compare against.
38
+ *
39
+ * Kept separate from "nothing changed", because they are different facts and
40
+ * only one of them is reassuring.
41
+ */
42
+ comparable: boolean;
43
+ changed: Array<{
44
+ key: string;
45
+ from: unknown;
46
+ to: unknown;
47
+ }>;
48
+ added: string[];
49
+ removed: string[];
50
+ }
51
+ /**
52
+ * Compare the posture now against the posture at the previous boot.
53
+ *
54
+ * __No previous record is `comparable: false`, never "no change".__ Only the
55
+ * last 1000 log lines are loaded for search (`FileAuditProvider.ts:610`), so on
56
+ * a busy instance the previous boot's record can fall outside the window.
57
+ * Reporting that as "nothing changed" would be a false all-clear at exactly the
58
+ * moment an operator is relying on the check.
59
+ *
60
+ * An ingredient added to or removed from the __view__ is reported separately
61
+ * from a __value__ change: removing an ingredient changes nothing about what
62
+ * the instance does (D4, D16), and folding both into "posture changed" would
63
+ * lose the distinction that matters when reading the log back.
64
+ */
65
+ export declare function diffPostures(previous: FlatPosture | null, current: FlatPosture): PostureDiff;
66
+ /**
67
+ * The line an operator reads at startup.
68
+ *
69
+ * A difference found here says explicitly that nothing observed the edit —
70
+ * otherwise the obvious question is why it is being reported at boot rather
71
+ * than as a `config-change`, and the answer is the whole point.
72
+ */
73
+ export declare function describePostureDiff(diff: PostureDiff): string;
@@ -155,6 +155,12 @@ export interface PrivatePageName {
155
155
  * also its URL. No account can be called this (`RESERVED_USERNAMES`).
156
156
  */
157
157
  export declare const SHARED_FOLDER_OWNER = "shared-folders";
158
+ /**
159
+ * Whether `id` has the shape a store gives a file (`crypto.randomUUID()`), so
160
+ * a takeout's file can be stored under it again (#2188). Anything else, a
161
+ * path or a pool's content hash among them, is never reused as a file name.
162
+ */
163
+ export declare function isRestorableFileId(id: string): boolean;
158
164
  /** A shared folder page's name, and its URL without the leading slash: `shared/{folder}/{title}`. */
159
165
  export declare const SHARED_PAGE_NAME_PREFIX = "shared/";
160
166
  /** The first path segment of a shared folder's URL: `/shared/{folder}/{title}`. */
@@ -212,6 +212,14 @@ export const PRIVATE_SLUG_PREFIX = 'vaults--';
212
212
  * also its URL. No account can be called this (`RESERVED_USERNAMES`).
213
213
  */
214
214
  export const SHARED_FOLDER_OWNER = 'shared-folders';
215
+ /**
216
+ * Whether `id` has the shape a store gives a file (`crypto.randomUUID()`), so
217
+ * a takeout's file can be stored under it again (#2188). Anything else, a
218
+ * path or a pool's content hash among them, is never reused as a file name.
219
+ */
220
+ export function isRestorableFileId(id) {
221
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id);
222
+ }
215
223
  /** A shared folder page's name, and its URL without the leading slash: `shared/{folder}/{title}`. */
216
224
  export const SHARED_PAGE_NAME_PREFIX = 'shared/';
217
225
  /** The first path segment of a shared folder's URL: `/shared/{folder}/{title}`. */
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The security posture — a view over the settings that determine what an
3
+ * instance guarantees (#1145).
4
+ *
5
+ * An operator cannot otherwise see what their instance's security settings
6
+ * are: they sit in `config/app-default-config.json` among some five hundred
7
+ * other keys, with nothing presenting them as one subject. One of them is
8
+ * where a gap becomes visible as it ships — `ngdpbase.auth.factors` offers
9
+ * only single factors, which is where the absence of MFA becomes a visible
10
+ * fact rather than tribal knowledge.
11
+ *
12
+ * __This is a view, not a resolution layer (D3).__ Every item is an ordinary
13
+ * key with its own shipped default, read by live code. The posture decides
14
+ * which settings are presented together and shows what each is currently set
15
+ * to. It adds no resolution step and changes no value on its own — which is
16
+ * also why removing an ingredient is safe: the key keeps whatever it is set
17
+ * to and the code keeps reading it, it simply stops being displayed (D4).
18
+ *
19
+ * See docs/security-posture.md.
20
+ */
21
+ /** The shape of `ConfigurationManager.getProperty`. */
22
+ export type ConfigReader = (key: string, fallback?: unknown) => unknown;
23
+ export declare const POSTURE_KEY = "ngdpbase.security.posture";
24
+ export interface PostureItem {
25
+ key: string;
26
+ /** The current value. Absent when the key is a declared secret. */
27
+ value?: unknown;
28
+ /** Whether a change takes effect only after a restart (D6). */
29
+ restart: boolean;
30
+ /**
31
+ * True when the key is named in `ngdpbase.config.sensitive-values` (its `secret` entries).
32
+ *
33
+ * Reported as present but masked rather than dropped: an operator who added
34
+ * it deserves to know it is set, and silently omitting it would make the
35
+ * section quietly incomplete.
36
+ */
37
+ secret: boolean;
38
+ /** A plain-words caution when the value, read with another setting, does not do what it seems to. */
39
+ note?: string;
40
+ }
41
+ export interface PostureGroup {
42
+ group: string;
43
+ items: PostureItem[];
44
+ }
45
+ /**
46
+ * Read the posture and each ingredient's current value.
47
+ *
48
+ * A malformed or absent posture yields nothing rather than throwing. This
49
+ * feeds an admin screen, and an operator who mistyped the object should get an
50
+ * empty section and a chance to fix it, not a page that fails to render.
51
+ */
52
+ export declare function resolvePosture(read: ConfigReader): PostureGroup[];
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Serve HTTPS when a certificate and key are configured (#1153).
3
+ *
4
+ * There was no TLS path at all: `app.listen()` is plain HTTP and no cert or
5
+ * key key existed, so an operator holding a certificate had nowhere to put it.
6
+ * The concrete consequence was a security setting that could not be turned on
7
+ * safely — `ngdpbase.session.secure` ships `false` because a LAN instance on
8
+ * plain HTTP that set it `true` would issue cookies the browser refuses to
9
+ * send back and lock itself out of its own sessions.
10
+ *
11
+ * __A broken TLS configuration does not silently become HTTP.__ That is a
12
+ * transport downgrade: the operator configured TLS, believes traffic is
13
+ * encrypted, and it is in the clear. It takes #1152's survivable-failure path
14
+ * instead — the instance boots into maintenance mode, names the file that is
15
+ * wrong, and keeps `/admin` and `/login` reachable so it can be repaired
16
+ * without filesystem access.
17
+ *
18
+ * __Expiry is the deliberate exception.__ Node does not validate `notAfter`
19
+ * when building a server context; the client rejects the certificate instead.
20
+ * So an expired certificate still serves and is reported loudly. Falling back
21
+ * would downgrade the transport over a certificate that is merely stale, and
22
+ * blocking would take down an instance whose operator may be mid-renewal —
23
+ * while a stale certificate is already loudly visible to every client, which a
24
+ * silent downgrade is not.
25
+ */
26
+ /** The shape of `ConfigurationManager.getProperty`. */
27
+ export type ConfigReader = (key: string, fallback?: unknown) => unknown;
28
+ export declare const TLS_CERT_KEY = "ngdpbase.server.tls.cert-file";
29
+ export declare const TLS_KEY_KEY = "ngdpbase.server.tls.key-file";
30
+ export type TlsResolution =
31
+ /** No certificate configured. The ordinary case; not a problem. */
32
+ {
33
+ mode: 'http';
34
+ }
35
+ /** Usable. `expired` is true when the certificate's own validity has passed. */
36
+ | {
37
+ mode: 'https';
38
+ cert: Buffer;
39
+ key: Buffer;
40
+ expired: boolean;
41
+ expiresAt: string | null;
42
+ }
43
+ /** Configured and unusable. Takes the #1152 survivable-failure path. */
44
+ | {
45
+ mode: 'blocked';
46
+ reasons: string[];
47
+ };
48
+ /** Injectable so the classification is testable without real certificates. */
49
+ export interface TlsDeps {
50
+ readFile?: (path: string) => Buffer;
51
+ createSecureContext?: (options: {
52
+ cert: Buffer;
53
+ key: Buffer;
54
+ }) => unknown;
55
+ now?: () => Date;
56
+ /** ISO expiry of the certificate, or null when it cannot be read. */
57
+ certExpiry?: (cert: Buffer) => string | null;
58
+ }
59
+ export declare function resolveTlsConfig(read: ConfigReader, deps?: TlsDeps): TlsResolution;
@@ -0,0 +1,4 @@
1
+ /** Seal `plain` for `username`; returns `v1.<iv>.<ciphertext+tag>`, base64url. */
2
+ export declare function sealTotpSecret(credentialsKey: string, username: string, plain: string): string;
3
+ /** Open a sealed value for `username`; null when it is malformed, for someone else, or under another key. */
4
+ export declare function openTotpSecret(credentialsKey: string, username: string, sealed: string): string | null;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Encryption for TOTP seeds in the credentials store (#421).
3
+ *
4
+ * The store signs every row with `NGDPBASE_CREDENTIALS_KEY` but keeps it
5
+ * readable; a TOTP seed is a shared secret, so a reader of the file must not
6
+ * get it. The seed is AES-256-GCM encrypted under a subkey derived from the
7
+ * same key by HKDF (decided on #1523, 2026-10-31), so there is no new key to
8
+ * back up. The account name is the associated data: a sealed seed copied onto
9
+ * another person's row does not open.
10
+ */
11
+ import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from 'node:crypto';
12
+ const INFO = 'ngdpbase totp seed v1';
13
+ const PREFIX = 'v1';
14
+ function subkey(credentialsKey) {
15
+ if (!credentialsKey)
16
+ throw new Error('A TOTP seed needs NGDPBASE_CREDENTIALS_KEY (#421)');
17
+ return Buffer.from(hkdfSync('sha256', credentialsKey, Buffer.alloc(0), INFO, 32));
18
+ }
19
+ /** Seal `plain` for `username`; returns `v1.<iv>.<ciphertext+tag>`, base64url. */
20
+ export function sealTotpSecret(credentialsKey, username, plain) {
21
+ const iv = randomBytes(12);
22
+ const cipher = createCipheriv('aes-256-gcm', subkey(credentialsKey), iv);
23
+ cipher.setAAD(Buffer.from(username, 'utf8'));
24
+ const body = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final(), cipher.getAuthTag()]);
25
+ return `${PREFIX}.${iv.toString('base64url')}.${body.toString('base64url')}`;
26
+ }
27
+ /** Open a sealed value for `username`; null when it is malformed, for someone else, or under another key. */
28
+ export function openTotpSecret(credentialsKey, username, sealed) {
29
+ const parts = sealed.split('.');
30
+ if (parts.length !== 3 || parts[0] !== PREFIX)
31
+ return null;
32
+ try {
33
+ const iv = Buffer.from(parts[1], 'base64url');
34
+ const body = Buffer.from(parts[2], 'base64url');
35
+ if (iv.length !== 12 || body.length < 16)
36
+ return null;
37
+ const decipher = createDecipheriv('aes-256-gcm', subkey(credentialsKey), iv);
38
+ decipher.setAAD(Buffer.from(username, 'utf8'));
39
+ decipher.setAuthTag(body.subarray(body.length - 16));
40
+ return Buffer.concat([decipher.update(body.subarray(0, body.length - 16)), decipher.final()]).toString('utf8');
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwilleke/ngdpbase-types",
3
- "version": "5.41.0",
3
+ "version": "5.42.0",
4
4
  "description": "TypeScript types for ngdpbase add-ons, and a test kit of core's own values. A dev dependency, versioned with ngdpbase.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Jim Willeke",
@@ -40,6 +40,7 @@
40
40
  "@types/markdown-it": "^14.2.0",
41
41
  "@types/multer": "^2.3.0",
42
42
  "@types/node": "^24.19.2",
43
+ "@types/qrcode": "^1.5.6",
43
44
  "better-sqlite3-multiple-ciphers": "^13.0.3",
44
45
  "dotenv": "^18.0.9",
45
46
  "express": "^5.3.0",
@@ -47,12 +48,14 @@
47
48
  "fast-xml-parser": "^5.11.2",
48
49
  "fs-extra": "^11.4.1",
49
50
  "ipaddr.js": "^2.5.0",
50
- "jose": "^6.2.13",
51
+ "jose": "^6.2.14",
51
52
  "linkedom": "^0.18.14",
52
53
  "markdown-it": "^14.3.3",
53
54
  "minisearch": "7.2.0",
54
55
  "mmdb-lib": "^3.0.3",
55
56
  "multer": "^2.4.0",
57
+ "otpauth": "^9.5.2",
58
+ "qrcode": "^1.5.4",
56
59
  "rrule-temporal": "2.2.8",
57
60
  "tar": "^7.5.22",
58
61
  "temporal-polyfill": "1.0.6",
@@ -1,22 +0,0 @@
1
- /**
2
- * Conservative filename → capture-date parser (#809, slice C of #807).
3
- *
4
- * Last-resort fallback used by media providers when a file carries no usable
5
- * EXIF/QuickTime capture date. Many devices encode the capture time in the
6
- * filename (e.g. `PXL_20260113_123414708.jpg`, `2026-04-03-IMG_4654.jpg`,
7
- * `Feb_20_2026_19_25_04.jpg`); without this, those files index with
8
- * `dateTimeOriginal: null` and sort only by file `mtime`.
9
- *
10
- * Design constraints (from the issue):
11
- * - **Conservative** — only a small set of high-confidence, anchored patterns
12
- * are accepted. Anything ambiguous returns `null`, and the caller then
13
- * falls back to `mtime` and surfaces the file to admins.
14
- * - **Distinguishable provenance** — the caller records that the resulting
15
- * date came from the filename (`captureDateSource: 'filename'`), so a
16
- * parsed date never masquerades as authoritative EXIF metadata.
17
- *
18
- * Returns a `"YYYY-MM-DD HH:MM:SS"` string (the exact shape produced by the
19
- * EXIF extractor, so it flows through sort/filter identically), or `null`.
20
- * A date-only match yields a `00:00:00` time.
21
- */
22
- export declare function parseDateFromFilename(filename: string): string | null;