dsh-coding-subscription-oauth 0.5.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 (178) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/CONTRIBUTING.md +109 -0
  3. package/INSTALL.md +220 -0
  4. package/LICENSE +19 -0
  5. package/NOTICE +11 -0
  6. package/README.de.md +254 -0
  7. package/README.es.md +254 -0
  8. package/README.fr.md +254 -0
  9. package/README.ja.md +254 -0
  10. package/README.ko.md +254 -0
  11. package/README.md +286 -0
  12. package/README.pt-BR.md +254 -0
  13. package/README.ru.md +254 -0
  14. package/README.zh-CN.md +271 -0
  15. package/cordis.patch.yml +13 -0
  16. package/docs/00-project-rules.md +195 -0
  17. package/docs/02-architecture.md +132 -0
  18. package/docs/02-architecture.zh-CN.md +128 -0
  19. package/lib/adapter.d.ts +24 -0
  20. package/lib/adapter.d.ts.map +1 -0
  21. package/lib/alias-adapter.d.ts +43 -0
  22. package/lib/alias-adapter.d.ts.map +1 -0
  23. package/lib/auth-routes.d.ts +155 -0
  24. package/lib/auth-routes.d.ts.map +1 -0
  25. package/lib/auth.d.ts +29 -0
  26. package/lib/auth.d.ts.map +1 -0
  27. package/lib/bin.d.ts +6 -0
  28. package/lib/bin.d.ts.map +1 -0
  29. package/lib/bin.js +27591 -0
  30. package/lib/bin.js.map +7 -0
  31. package/lib/capability-routes.d.ts +43 -0
  32. package/lib/capability-routes.d.ts.map +1 -0
  33. package/lib/capability-runtime.d.ts +56 -0
  34. package/lib/capability-runtime.d.ts.map +1 -0
  35. package/lib/capability-settings.d.ts +263 -0
  36. package/lib/capability-settings.d.ts.map +1 -0
  37. package/lib/capability-tools.d.ts +50 -0
  38. package/lib/capability-tools.d.ts.map +1 -0
  39. package/lib/catalog.d.ts +53 -0
  40. package/lib/catalog.d.ts.map +1 -0
  41. package/lib/client.js +3 -0
  42. package/lib/client.js.map +7 -0
  43. package/lib/codex-http.d.ts +77 -0
  44. package/lib/codex-http.d.ts.map +1 -0
  45. package/lib/codex-images.d.ts +109 -0
  46. package/lib/codex-images.d.ts.map +1 -0
  47. package/lib/codex-model-capabilities.d.ts +112 -0
  48. package/lib/codex-model-capabilities.d.ts.map +1 -0
  49. package/lib/codex-search.d.ts +96 -0
  50. package/lib/codex-search.d.ts.map +1 -0
  51. package/lib/codex-usage.d.ts +79 -0
  52. package/lib/codex-usage.d.ts.map +1 -0
  53. package/lib/gateway-anthropic-messages.d.ts +8 -0
  54. package/lib/gateway-anthropic-messages.d.ts.map +1 -0
  55. package/lib/gateway-auth.d.ts +22 -0
  56. package/lib/gateway-auth.d.ts.map +1 -0
  57. package/lib/gateway-backend.d.ts +62 -0
  58. package/lib/gateway-backend.d.ts.map +1 -0
  59. package/lib/gateway-body.d.ts +9 -0
  60. package/lib/gateway-body.d.ts.map +1 -0
  61. package/lib/gateway-config.d.ts +24 -0
  62. package/lib/gateway-config.d.ts.map +1 -0
  63. package/lib/gateway-http.d.ts +16 -0
  64. package/lib/gateway-http.d.ts.map +1 -0
  65. package/lib/gateway-openai-chat.d.ts +8 -0
  66. package/lib/gateway-openai-chat.d.ts.map +1 -0
  67. package/lib/gateway-openai-responses.d.ts +8 -0
  68. package/lib/gateway-openai-responses.d.ts.map +1 -0
  69. package/lib/gateway-parse.d.ts +10 -0
  70. package/lib/gateway-parse.d.ts.map +1 -0
  71. package/lib/gateway-protocol.d.ts +47 -0
  72. package/lib/gateway-protocol.d.ts.map +1 -0
  73. package/lib/gateway-routes.d.ts +21 -0
  74. package/lib/gateway-routes.d.ts.map +1 -0
  75. package/lib/gateway.d.ts +48 -0
  76. package/lib/gateway.d.ts.map +1 -0
  77. package/lib/grok-imagine.d.ts +271 -0
  78. package/lib/grok-imagine.d.ts.map +1 -0
  79. package/lib/grok-import.d.ts +21 -0
  80. package/lib/grok-import.d.ts.map +1 -0
  81. package/lib/http-json.d.ts +10 -0
  82. package/lib/http-json.d.ts.map +1 -0
  83. package/lib/ids.d.ts +33 -0
  84. package/lib/ids.d.ts.map +1 -0
  85. package/lib/imagine-routes.d.ts +59 -0
  86. package/lib/imagine-routes.d.ts.map +1 -0
  87. package/lib/index.d.ts +69 -0
  88. package/lib/index.d.ts.map +1 -0
  89. package/lib/index.js +35355 -0
  90. package/lib/index.js.map +7 -0
  91. package/lib/invariant.d.ts +9 -0
  92. package/lib/invariant.d.ts.map +1 -0
  93. package/lib/invariant.js +14 -0
  94. package/lib/invariant.js.map +7 -0
  95. package/lib/kimi-errors.d.ts +13 -0
  96. package/lib/kimi-errors.d.ts.map +1 -0
  97. package/lib/media-store.d.ts +130 -0
  98. package/lib/media-store.d.ts.map +1 -0
  99. package/lib/oauth-import-routes.d.ts +52 -0
  100. package/lib/oauth-import-routes.d.ts.map +1 -0
  101. package/lib/oauth-providers.d.ts +26 -0
  102. package/lib/oauth-providers.d.ts.map +1 -0
  103. package/lib/oauth-session.d.ts +40 -0
  104. package/lib/oauth-session.d.ts.map +1 -0
  105. package/lib/oauth-sources.d.ts +205 -0
  106. package/lib/oauth-sources.d.ts.map +1 -0
  107. package/lib/oauth.d.ts +79 -0
  108. package/lib/oauth.d.ts.map +1 -0
  109. package/lib/provider.d.ts +38 -0
  110. package/lib/provider.d.ts.map +1 -0
  111. package/lib/proxy.d.ts +17 -0
  112. package/lib/proxy.d.ts.map +1 -0
  113. package/lib/redact.d.ts +5 -0
  114. package/lib/redact.d.ts.map +1 -0
  115. package/lib/session.d.ts +40 -0
  116. package/lib/session.d.ts.map +1 -0
  117. package/lib/store.d.ts +46 -0
  118. package/lib/store.d.ts.map +1 -0
  119. package/lib/web-origin.d.ts +10 -0
  120. package/lib/web-origin.d.ts.map +1 -0
  121. package/lib/web-routes.d.ts +20 -0
  122. package/lib/web-routes.d.ts.map +1 -0
  123. package/package.json +185 -0
  124. package/patches/dsh-agy@0.1.2.patch +25 -0
  125. package/scripts/release.mjs +166 -0
  126. package/scripts/smoke-deployed-routes.mjs +146 -0
  127. package/scripts/verify-deployed-catalog.mjs +87 -0
  128. package/src/adapter.ts +282 -0
  129. package/src/alias-adapter.ts +152 -0
  130. package/src/auth-routes.ts +871 -0
  131. package/src/auth.ts +67 -0
  132. package/src/bin.ts +350 -0
  133. package/src/capability-routes.ts +275 -0
  134. package/src/capability-runtime.ts +313 -0
  135. package/src/capability-settings.ts +657 -0
  136. package/src/capability-tools.ts +666 -0
  137. package/src/catalog.ts +271 -0
  138. package/src/client/GrokBuildSettings.tsx +2221 -0
  139. package/src/client/index.tsx +37 -0
  140. package/src/client/locales.ts +421 -0
  141. package/src/codex-http.ts +447 -0
  142. package/src/codex-images.ts +485 -0
  143. package/src/codex-model-capabilities.ts +320 -0
  144. package/src/codex-search.ts +245 -0
  145. package/src/codex-usage.ts +263 -0
  146. package/src/gateway-anthropic-messages.ts +84 -0
  147. package/src/gateway-auth.ts +100 -0
  148. package/src/gateway-backend.ts +274 -0
  149. package/src/gateway-body.ts +49 -0
  150. package/src/gateway-config.ts +76 -0
  151. package/src/gateway-http.ts +104 -0
  152. package/src/gateway-openai-chat.ts +124 -0
  153. package/src/gateway-openai-responses.ts +53 -0
  154. package/src/gateway-parse.ts +224 -0
  155. package/src/gateway-protocol.ts +52 -0
  156. package/src/gateway-routes.ts +152 -0
  157. package/src/gateway.ts +242 -0
  158. package/src/grok-imagine.ts +1627 -0
  159. package/src/grok-import.ts +151 -0
  160. package/src/http-json.ts +82 -0
  161. package/src/ids.ts +45 -0
  162. package/src/imagine-routes.ts +461 -0
  163. package/src/index.ts +598 -0
  164. package/src/invariant.ts +17 -0
  165. package/src/kimi-errors.ts +26 -0
  166. package/src/media-store.ts +927 -0
  167. package/src/oauth-import-routes.ts +314 -0
  168. package/src/oauth-providers.ts +152 -0
  169. package/src/oauth-session.ts +183 -0
  170. package/src/oauth-sources.ts +1104 -0
  171. package/src/oauth.ts +620 -0
  172. package/src/provider.ts +128 -0
  173. package/src/proxy.ts +99 -0
  174. package/src/redact.ts +72 -0
  175. package/src/session.ts +218 -0
  176. package/src/store.ts +217 -0
  177. package/src/web-origin.ts +60 -0
  178. package/src/web-routes.ts +75 -0
@@ -0,0 +1,927 @@
1
+ /**
2
+ * Owner-private content-addressed store for Grok Imagine video artifacts.
3
+ * Public identifiers are opaque; callers never receive a filesystem path,
4
+ * content hash, or a signed upstream URL.
5
+ * @module dsh-coding-subscription-oauth/media-store
6
+ */
7
+
8
+ import { createHash, randomBytes } from "node:crypto";
9
+ import { constants as fsConstants } from "node:fs";
10
+ import { chmod, link, lstat, mkdir, open, readdir, unlink } from "node:fs/promises";
11
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
12
+ import { writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
13
+
14
+ /** Hard ceiling for one artifact and for aggregate stored object bytes. */
15
+ export const MEDIA_STORE_MAX_BYTES = 256 * 1024 * 1024;
16
+
17
+ /** Hard ceiling for artifact retention. Callers cannot raise this. */
18
+ export const MEDIA_STORE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
19
+
20
+ export const MEDIA_STORE_DIR_MODE = 0o700;
21
+ export const MEDIA_STORE_FILE_MODE = 0o600;
22
+ export const MEDIA_STORE_INDEX_VERSION = 1;
23
+ export const MEDIA_STORE_INDEX_MAX_BYTES = 16 * 1024;
24
+
25
+ /** Opaque public id: 32 lowercase hex characters. */
26
+ export const MEDIA_ARTIFACT_ID_PATTERN = /^[a-f0-9]{32}$/;
27
+
28
+ export const IMAGINE_MEDIA_ROUTE_PREFIX = "/plugins/dsh-grok-build/imagine/media/";
29
+
30
+ const NOFOLLOW = process.platform === "win32" ? 0 : (fsConstants.O_NOFOLLOW ?? 0);
31
+ const TRUSTED_IMAGINE = Symbol.for("dsh-coding-subscription-oauth.trusted-imagine");
32
+
33
+ export type MediaStoreVideoType = "video/mp4" | "video/webm";
34
+
35
+ export type MediaStoreErrorCode =
36
+ | "INVALID_ID"
37
+ | "INVALID_INPUT"
38
+ | "TOO_LARGE"
39
+ | "NOT_FOUND"
40
+ | "EXPIRED"
41
+ | "CORRUPT"
42
+ | "UNSUPPORTED_TYPE"
43
+ | "FORBIDDEN";
44
+
45
+ export class MediaStoreError extends Error {
46
+ readonly code: MediaStoreErrorCode;
47
+
48
+ constructor(code: MediaStoreErrorCode, message: string, options?: { cause?: unknown }) {
49
+ super(message, options);
50
+ this.name = "MediaStoreError";
51
+ this.code = code;
52
+ }
53
+ }
54
+
55
+ export interface MediaArtifactMeta {
56
+ /** Opaque public identifier. Never a path, hash, or bearer URL. */
57
+ artifactId: string;
58
+ mediaType: MediaStoreVideoType;
59
+ bytes: number;
60
+ createdAt: number;
61
+ expiresAt: number;
62
+ name?: string;
63
+ }
64
+
65
+ export interface SaveMediaInput {
66
+ data: Uint8Array;
67
+ mediaType: MediaStoreVideoType;
68
+ name?: string;
69
+ }
70
+
71
+ export interface StoredMedia {
72
+ meta: MediaArtifactMeta;
73
+ data: Uint8Array;
74
+ }
75
+
76
+ export interface MediaCleanupReport {
77
+ expiredArtifacts: number;
78
+ removedObjects: number;
79
+ }
80
+
81
+ export interface MediaStoreOptions {
82
+ now?: () => number;
83
+ /** Per-artifact ceiling, capped at {@link MEDIA_STORE_MAX_BYTES}. */
84
+ maxBytes?: number;
85
+ /** Aggregate unique-object ceiling, capped at {@link MEDIA_STORE_MAX_BYTES}. */
86
+ maxTotalBytes?: number;
87
+ retentionMs?: number;
88
+ randomId?: () => string;
89
+ }
90
+
91
+ export interface MediaDownloadHeaders {
92
+ "Content-Type": string;
93
+ "Content-Length": string;
94
+ "Content-Disposition": string;
95
+ "Cache-Control": string;
96
+ "X-Content-Type-Options": "nosniff";
97
+ }
98
+
99
+ export interface MediaDownloadView {
100
+ meta: MediaArtifactMeta;
101
+ body: Uint8Array;
102
+ headers: MediaDownloadHeaders;
103
+ }
104
+
105
+ /** Runtime token proving a route already authorized this download. */
106
+ export interface TrustedImagineAuthz {
107
+ readonly [TRUSTED_IMAGINE]: true;
108
+ }
109
+
110
+ export interface CreateTrustedImagineAuthzInput {
111
+ readonly remoteAddress?: string;
112
+ readonly authorized?: boolean;
113
+ }
114
+
115
+ interface IndexDocument {
116
+ version: typeof MEDIA_STORE_INDEX_VERSION;
117
+ artifactId: string;
118
+ sha256: string;
119
+ mediaType: MediaStoreVideoType;
120
+ bytes: number;
121
+ createdAt: number;
122
+ expiresAt: number;
123
+ name?: string;
124
+ }
125
+
126
+ function isENOENT(error: unknown): boolean {
127
+ return (error as NodeJS.ErrnoException | null)?.code === "ENOENT";
128
+ }
129
+
130
+ function isEEXIST(error: unknown): boolean {
131
+ return (error as NodeJS.ErrnoException | null)?.code === "EEXIST";
132
+ }
133
+
134
+ function sha256Hex(data: Uint8Array): string {
135
+ return createHash("sha256").update(data).digest("hex");
136
+ }
137
+
138
+ function generateArtifactId(): string {
139
+ return randomBytes(16).toString("hex");
140
+ }
141
+
142
+ function clampPositive(value: number | undefined, ceiling: number, label: string): number {
143
+ const raw = value ?? ceiling;
144
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) {
145
+ throw new MediaStoreError("INVALID_INPUT", `${label} must be a positive finite number`);
146
+ }
147
+ return Math.min(Math.floor(raw), ceiling);
148
+ }
149
+
150
+ /** Reject anything that is not a 32-char lowercase hex artifact id. */
151
+ export function parseMediaArtifactId(value: string): string | undefined {
152
+ return MEDIA_ARTIFACT_ID_PATTERN.test(value) ? value : undefined;
153
+ }
154
+
155
+ export function isSafeMediaArtifactId(value: string): boolean {
156
+ return parseMediaArtifactId(value) !== undefined;
157
+ }
158
+
159
+ function sanitizeDisplayName(value: string | undefined): string | undefined {
160
+ if (value === undefined) return undefined;
161
+ const basename = value.slice(Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\")) + 1);
162
+ const clean = [...basename]
163
+ .filter((character) => {
164
+ const codePoint = character.codePointAt(0) ?? 0;
165
+ return codePoint >= 0x20 && codePoint !== 0x7f;
166
+ })
167
+ .join("")
168
+ .trim()
169
+ .slice(0, 255);
170
+ return clean === "" ? undefined : clean;
171
+ }
172
+
173
+ /**
174
+ * Characters disallowed inside an HTTP `filename=` token. Quoting, semicolons,
175
+ * directory separators, and CR/LF could break out of the header parameter or
176
+ * inject an additional parameter. The legacy header stays ASCII; any
177
+ * disallowed or non-ASCII characters are moved into the optional
178
+ * `filename*=UTF-8''…` parameter per RFC 5987.
179
+ */
180
+ const ASCII_BANNED_CODE_POINTS = new Set<number>([
181
+ 0x22, // "
182
+ 0x3b, // ;
183
+ 0x5c, // backslash
184
+ 0x2f, // forward slash
185
+ ]);
186
+
187
+ function sanitizeFilenameToken(value: string): string {
188
+ const safe: string[] = [];
189
+ for (const character of value) {
190
+ const codePoint = character.codePointAt(0) ?? 0;
191
+ if (codePoint < 0x20 || codePoint > 0x7e) continue;
192
+ if (ASCII_BANNED_CODE_POINTS.has(codePoint)) continue;
193
+ safe.push(character);
194
+ }
195
+ return safe.join("").trim().slice(0, 120);
196
+ }
197
+
198
+ /**
199
+ * RFC 5987 percent-encoding for the UTF-8 `filename*` parameter.
200
+ * `encodeURIComponent` covers the bulk; we additionally re-encode the
201
+ * punctuation marks RFC 5987 mandates inside an encoded-word.
202
+ */
203
+ function encodeRfc5987(value: string): string {
204
+ return encodeURIComponent(value).replace(
205
+ /[!'()*]/gu,
206
+ (character) => `%${character.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`,
207
+ );
208
+ }
209
+
210
+ /**
211
+ * Returns either a `filename=` parameter (when the value is already safe ASCII)
212
+ * or both `filename=` and `filename*=UTF-8''…` (when the original had to be
213
+ * downgraded for the legacy parameter). The caller wraps the result with
214
+ * `attachment; `.
215
+ */
216
+ function contentDispositionFilename(value: string, fallback: string): string {
217
+ const safe = sanitizeFilenameToken(value) || fallback;
218
+ if (safe === value) return `filename="${value}"`;
219
+ const encoded = encodeRfc5987(value);
220
+ return `filename="${safe}"; filename*=UTF-8''${encoded}`;
221
+ }
222
+
223
+ function extensionFor(mediaType: MediaStoreVideoType): "mp4" | "webm" {
224
+ return mediaType === "video/webm" ? "webm" : "mp4";
225
+ }
226
+
227
+ function assertInsideRoot(root: string, candidate: string): string {
228
+ const base = resolve(root);
229
+ const resolved = resolve(candidate);
230
+ const rel = relative(base, resolved);
231
+ if (rel === "" || rel.startsWith(`..${sep}`) || rel === ".." || isAbsolute(rel)) {
232
+ throw new MediaStoreError("INVALID_ID", "media path escaped the store root");
233
+ }
234
+ return resolved;
235
+ }
236
+
237
+ async function lstatOrUndefined(path: string): Promise<import("node:fs").Stats | undefined> {
238
+ try {
239
+ return await lstat(path);
240
+ } catch (error) {
241
+ if (isENOENT(error)) return undefined;
242
+ throw error;
243
+ }
244
+ }
245
+
246
+ async function assertRealDirectory(path: string, kind: "root" | "parent" | "leaf"): Promise<void> {
247
+ const stat = await lstatOrUndefined(path);
248
+ if (stat === undefined) {
249
+ throw new MediaStoreError(kind === "root" ? "INVALID_INPUT" : "CORRUPT", `media ${kind} directory is missing`);
250
+ }
251
+ if (stat.isSymbolicLink()) {
252
+ throw new MediaStoreError(
253
+ kind === "root" ? "INVALID_INPUT" : "CORRUPT",
254
+ `media ${kind} directory must not be a symlink`,
255
+ );
256
+ }
257
+ if (!stat.isDirectory()) {
258
+ throw new MediaStoreError(kind === "root" ? "INVALID_INPUT" : "CORRUPT", `media ${kind} path is not a directory`);
259
+ }
260
+ }
261
+
262
+ async function assertSafeRoot(root: string): Promise<void> {
263
+ const stat = await lstatOrUndefined(root);
264
+ if (stat === undefined) return;
265
+ if (stat.isSymbolicLink()) {
266
+ throw new MediaStoreError("INVALID_INPUT", "media store root must not be a symlink");
267
+ }
268
+ if (!stat.isDirectory()) {
269
+ throw new MediaStoreError("INVALID_INPUT", "media store root must be a directory");
270
+ }
271
+ }
272
+
273
+ async function ensurePrivateDir(root: string, path: string): Promise<void> {
274
+ const resolved = path === root ? resolve(root) : assertInsideRoot(root, path);
275
+ if (resolved === resolve(root)) {
276
+ const existing = await lstatOrUndefined(resolved);
277
+ if (existing === undefined) {
278
+ await mkdir(resolved, { recursive: true, mode: MEDIA_STORE_DIR_MODE });
279
+ }
280
+ await assertRealDirectory(resolved, "root");
281
+ if (process.platform !== "win32") await chmod(resolved, MEDIA_STORE_DIR_MODE);
282
+ return;
283
+ }
284
+ await assertRealDirectory(root, "root");
285
+ const relativePath = relative(resolve(root), resolved);
286
+ let current = resolve(root);
287
+ for (const part of relativePath.split(sep)) {
288
+ if (part === "" || part === ".") continue;
289
+ if (part === "..") throw new MediaStoreError("INVALID_ID", "media path escaped the store root");
290
+ current = join(current, part);
291
+ const existing = await lstatOrUndefined(current);
292
+ if (existing === undefined) {
293
+ try {
294
+ await mkdir(current, { mode: MEDIA_STORE_DIR_MODE });
295
+ } catch (error) {
296
+ if (!isEEXIST(error)) throw error;
297
+ }
298
+ }
299
+ await assertRealDirectory(current, "parent");
300
+ if (process.platform !== "win32") await chmod(current, MEDIA_STORE_DIR_MODE);
301
+ }
302
+ }
303
+
304
+ async function openNoFollow(
305
+ path: string,
306
+ flags: number,
307
+ mode?: number,
308
+ ): Promise<import("node:fs/promises").FileHandle> {
309
+ try {
310
+ return mode === undefined ? await open(path, flags | NOFOLLOW) : await open(path, flags | NOFOLLOW, mode);
311
+ } catch (error) {
312
+ const code = (error as NodeJS.ErrnoException | null)?.code;
313
+ if (code === "ELOOP" || code === "EMLINK") {
314
+ throw new MediaStoreError("CORRUPT", "media path is a symlink", { cause: error });
315
+ }
316
+ throw error;
317
+ }
318
+ }
319
+
320
+ async function readBoundedFile(path: string, maxBytes: number): Promise<Uint8Array> {
321
+ const existing = await lstatOrUndefined(path);
322
+ if (existing === undefined) {
323
+ const missing = new Error("ENOENT");
324
+ (missing as NodeJS.ErrnoException).code = "ENOENT";
325
+ throw missing;
326
+ }
327
+ if (existing.isSymbolicLink()) {
328
+ throw new MediaStoreError("CORRUPT", "media leaf must not be a symlink");
329
+ }
330
+ if (!existing.isFile()) {
331
+ throw new MediaStoreError("CORRUPT", "media leaf is not a regular file");
332
+ }
333
+ if (existing.size > maxBytes) {
334
+ throw new MediaStoreError("TOO_LARGE", "stored media exceeds the bounded read ceiling");
335
+ }
336
+ const handle = await openNoFollow(path, fsConstants.O_RDONLY);
337
+ try {
338
+ const stat = await handle.stat();
339
+ if (!stat.isFile() || stat.size > maxBytes) {
340
+ throw new MediaStoreError("CORRUPT", "media leaf failed a no-follow stat");
341
+ }
342
+ const buffer = Buffer.alloc(stat.size);
343
+ const { bytesRead } = await handle.read(buffer, 0, stat.size, 0);
344
+ if (bytesRead !== stat.size) {
345
+ throw new MediaStoreError("CORRUPT", "media leaf could not be read completely");
346
+ }
347
+ return new Uint8Array(buffer.subarray(0, bytesRead));
348
+ } finally {
349
+ await handle.close();
350
+ }
351
+ }
352
+
353
+ async function writeExclusiveFile(root: string, path: string, data: Uint8Array | string): Promise<void> {
354
+ const target = assertInsideRoot(root, path);
355
+ await ensurePrivateDir(root, dirname(target));
356
+ const existing = await lstatOrUndefined(target);
357
+ if (existing !== undefined) {
358
+ if (existing.isSymbolicLink()) {
359
+ throw new MediaStoreError("CORRUPT", "media leaf must not be a symlink");
360
+ }
361
+ const collision = new Error("EEXIST");
362
+ (collision as NodeJS.ErrnoException).code = "EEXIST";
363
+ throw collision;
364
+ }
365
+ const payload = typeof data === "string" ? Buffer.from(data) : data;
366
+ const temporary = join(dirname(target), `.tmp-${randomBytes(8).toString("hex")}`);
367
+ const handle = await openNoFollow(
368
+ temporary,
369
+ fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL,
370
+ MEDIA_STORE_FILE_MODE,
371
+ );
372
+ try {
373
+ await handle.writeFile(payload);
374
+ if (process.platform !== "win32") await handle.chmod(MEDIA_STORE_FILE_MODE);
375
+ } catch (error) {
376
+ await handle.close().catch(() => undefined);
377
+ await unlink(temporary).catch(() => undefined);
378
+ throw error;
379
+ }
380
+ await handle.close();
381
+ try {
382
+ if (process.platform === "win32") {
383
+ const created = await openNoFollow(
384
+ target,
385
+ fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL,
386
+ MEDIA_STORE_FILE_MODE,
387
+ );
388
+ try {
389
+ await created.writeFile(payload);
390
+ } finally {
391
+ await created.close();
392
+ }
393
+ } else {
394
+ await link(temporary, target);
395
+ await chmod(target, MEDIA_STORE_FILE_MODE);
396
+ }
397
+ } catch (error) {
398
+ await unlink(temporary).catch(() => undefined);
399
+ if (isEEXIST(error)) throw error;
400
+ await unlink(target).catch(() => undefined);
401
+ throw error;
402
+ }
403
+ await unlink(temporary).catch(() => undefined);
404
+ }
405
+
406
+ function objectPath(root: string, sha256: string): string {
407
+ if (!/^[a-f0-9]{64}$/u.test(sha256)) {
408
+ throw new MediaStoreError("CORRUPT", "stored digest is not a sha256 hex string");
409
+ }
410
+ return assertInsideRoot(root, join(root, "objects", sha256.slice(0, 2), sha256));
411
+ }
412
+
413
+ function indexPath(root: string, artifactId: string): string {
414
+ const id = parseMediaArtifactId(artifactId);
415
+ if (id === undefined) throw new MediaStoreError("INVALID_ID", "artifact id is not a safe opaque identifier");
416
+ return assertInsideRoot(root, join(root, "index", id.slice(0, 2), `${id}.json`));
417
+ }
418
+
419
+ function parseIndexDocument(text: string): IndexDocument {
420
+ let value: unknown;
421
+ try {
422
+ value = JSON.parse(text);
423
+ } catch {
424
+ throw new MediaStoreError("CORRUPT", "media index is not valid JSON");
425
+ }
426
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
427
+ throw new MediaStoreError("CORRUPT", "media index must contain an object");
428
+ }
429
+ const document = value as Record<string, unknown>;
430
+ if (document["version"] !== MEDIA_STORE_INDEX_VERSION) {
431
+ throw new MediaStoreError("CORRUPT", "media index has an unsupported version");
432
+ }
433
+ const artifactId = document["artifactId"];
434
+ const sha256 = document["sha256"];
435
+ const mediaType = document["mediaType"];
436
+ const bytes = document["bytes"];
437
+ const createdAt = document["createdAt"];
438
+ const expiresAt = document["expiresAt"];
439
+ if (typeof artifactId !== "string" || parseMediaArtifactId(artifactId) === undefined) {
440
+ throw new MediaStoreError("CORRUPT", "media index artifact id is invalid");
441
+ }
442
+ if (typeof sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(sha256)) {
443
+ throw new MediaStoreError("CORRUPT", "media index digest is invalid");
444
+ }
445
+ if (mediaType !== "video/mp4" && mediaType !== "video/webm") {
446
+ throw new MediaStoreError("CORRUPT", "media index type is unsupported");
447
+ }
448
+ if (typeof bytes !== "number" || !Number.isSafeInteger(bytes) || bytes <= 0) {
449
+ throw new MediaStoreError("CORRUPT", "media index byte length is invalid");
450
+ }
451
+ if (typeof createdAt !== "number" || !Number.isFinite(createdAt) || createdAt <= 0) {
452
+ throw new MediaStoreError("CORRUPT", "media index createdAt is invalid");
453
+ }
454
+ if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt) || expiresAt <= createdAt) {
455
+ throw new MediaStoreError("CORRUPT", "media index expiresAt is invalid");
456
+ }
457
+ const name = typeof document["name"] === "string" ? sanitizeDisplayName(document["name"]) : undefined;
458
+ const parsed: IndexDocument = {
459
+ version: MEDIA_STORE_INDEX_VERSION,
460
+ artifactId,
461
+ sha256,
462
+ mediaType,
463
+ bytes,
464
+ createdAt,
465
+ expiresAt,
466
+ };
467
+ if (name !== undefined) parsed.name = name;
468
+ return parsed;
469
+ }
470
+
471
+ function toMeta(document: IndexDocument, expiresAt = document.expiresAt): MediaArtifactMeta {
472
+ const meta: MediaArtifactMeta = {
473
+ artifactId: document.artifactId,
474
+ mediaType: document.mediaType,
475
+ bytes: document.bytes,
476
+ createdAt: document.createdAt,
477
+ expiresAt,
478
+ };
479
+ if (document.name !== undefined) meta.name = document.name;
480
+ return meta;
481
+ }
482
+
483
+ async function storedObjectBytes(root: string): Promise<number> {
484
+ let total = 0;
485
+ for (const file of await listRegularFiles(root, "objects")) {
486
+ const stat = await lstatOrUndefined(file);
487
+ if (stat === undefined || stat.isSymbolicLink() || !stat.isFile()) continue;
488
+ total += stat.size;
489
+ if (!Number.isSafeInteger(total)) throw new MediaStoreError("CORRUPT", "media store byte accounting overflowed");
490
+ }
491
+ return total;
492
+ }
493
+
494
+ async function listRegularFiles(root: string, kind: "index" | "objects"): Promise<string[]> {
495
+ const base = join(root, kind);
496
+ const rootStat = await lstatOrUndefined(base);
497
+ if (rootStat === undefined) return [];
498
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
499
+ throw new MediaStoreError("CORRUPT", `${kind} directory must not be a symlink`);
500
+ }
501
+ let buckets: string[];
502
+ try {
503
+ buckets = await readdir(base);
504
+ } catch (error) {
505
+ if (isENOENT(error)) return [];
506
+ throw error;
507
+ }
508
+ const files: string[] = [];
509
+ for (const bucket of buckets) {
510
+ if (bucket.startsWith(".tmp-")) {
511
+ const leftover = join(base, bucket);
512
+ const leftoverStat = await lstatOrUndefined(leftover);
513
+ if (leftoverStat?.isSymbolicLink() === true || leftoverStat?.isFile() === true) {
514
+ await unlink(leftover).catch(() => undefined);
515
+ }
516
+ continue;
517
+ }
518
+ if (!/^[a-f0-9]{2}$/u.test(bucket)) continue;
519
+ const directory = join(base, bucket);
520
+ const directoryStat = await lstatOrUndefined(directory);
521
+ if (directoryStat === undefined) continue;
522
+ if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
523
+ throw new MediaStoreError("CORRUPT", `${kind} bucket must not be a symlink`);
524
+ }
525
+ let entries: string[];
526
+ try {
527
+ entries = await readdir(directory);
528
+ } catch (error) {
529
+ if (isENOENT(error)) continue;
530
+ throw error;
531
+ }
532
+ for (const entry of entries) {
533
+ const full = join(directory, entry);
534
+ const stat = await lstatOrUndefined(full);
535
+ if (stat === undefined) continue;
536
+ if (entry.startsWith(".tmp-")) {
537
+ if (stat.isSymbolicLink() || stat.isFile()) await unlink(full).catch(() => undefined);
538
+ continue;
539
+ }
540
+ if (stat.isSymbolicLink()) {
541
+ await unlink(full).catch(() => undefined);
542
+ continue;
543
+ }
544
+ if (!stat.isFile()) continue;
545
+ if (kind === "index" && !entry.endsWith(".json")) continue;
546
+ if (kind === "objects" && !/^[a-f0-9]{64}$/u.test(entry)) continue;
547
+ files.push(full);
548
+ }
549
+ }
550
+ return files;
551
+ }
552
+
553
+ /** Trusted same-origin path for one stored video artifact. */
554
+ export function imagineMediaPath(artifactId: string): string {
555
+ const id = parseMediaArtifactId(artifactId);
556
+ if (id === undefined) throw new MediaStoreError("INVALID_ID", "artifact id is not a safe opaque identifier");
557
+ return `${IMAGINE_MEDIA_ROUTE_PREFIX}${id}`;
558
+ }
559
+
560
+ /** Extract a safe artifact id from a same-origin media route. */
561
+ export function parseImagineMediaPath(pathname: string): string | undefined {
562
+ if (!pathname.startsWith(IMAGINE_MEDIA_ROUTE_PREFIX)) return undefined;
563
+ const rest = pathname.slice(IMAGINE_MEDIA_ROUTE_PREFIX.length);
564
+ if (rest.includes("/") || rest.includes("\\") || rest.includes("..")) return undefined;
565
+ return parseMediaArtifactId(rest);
566
+ }
567
+
568
+ export function mediaDownloadHeaders(meta: MediaArtifactMeta): MediaDownloadHeaders {
569
+ const fallback = `imagine-${meta.artifactId}.${extensionFor(meta.mediaType)}`;
570
+ const filename = meta.name ?? fallback;
571
+ return {
572
+ "Content-Type": meta.mediaType,
573
+ "Content-Length": String(meta.bytes),
574
+ "Content-Disposition": `attachment; ${contentDispositionFilename(filename, fallback)}`,
575
+ "Cache-Control": "private, max-age=0, no-store",
576
+ "X-Content-Type-Options": "nosniff",
577
+ };
578
+ }
579
+
580
+ /** Loopback-only peer check for the parent route gate. */
581
+ export function isTrustedImaginePeer(remoteAddress: string | undefined): boolean {
582
+ return remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
583
+ }
584
+
585
+ /** Build a runtime authz token. Raw objects are not accepted by openTrusted helpers. */
586
+ export function createTrustedImagineAuthz(input: CreateTrustedImagineAuthzInput): TrustedImagineAuthz {
587
+ if (input.authorized === true || isTrustedImaginePeer(input.remoteAddress)) {
588
+ return { [TRUSTED_IMAGINE]: true };
589
+ }
590
+ throw new MediaStoreError("FORBIDDEN", "imagine download is not authorized");
591
+ }
592
+
593
+ export function assertTrustedImagineAuthz(authz: TrustedImagineAuthz): void {
594
+ if (authz?.[TRUSTED_IMAGINE] !== true) {
595
+ throw new MediaStoreError("FORBIDDEN", "imagine download requires an explicit trusted context");
596
+ }
597
+ }
598
+
599
+ /**
600
+ * Content-addressed video store. Directories are 0700; objects and index
601
+ * documents are 0600. Public lookup uses an opaque id, never the digest.
602
+ */
603
+ export class MediaStore {
604
+ readonly root: string;
605
+ readonly maxBytes: number;
606
+ readonly maxTotalBytes: number;
607
+ retentionMs: number;
608
+ private readonly now: () => number;
609
+ private readonly randomId: () => string;
610
+ private tail: Promise<void> = Promise.resolve();
611
+ private retentionReconciled = false;
612
+
613
+ constructor(root: string, options: MediaStoreOptions = {}) {
614
+ if (typeof root !== "string" || root.trim() === "") {
615
+ throw new MediaStoreError("INVALID_INPUT", "media store root must be a non-empty path");
616
+ }
617
+ this.root = resolve(root);
618
+ this.now = options.now ?? Date.now;
619
+ this.maxBytes = clampPositive(options.maxBytes, MEDIA_STORE_MAX_BYTES, "maxBytes");
620
+ this.maxTotalBytes = clampPositive(options.maxTotalBytes, MEDIA_STORE_MAX_BYTES, "maxTotalBytes");
621
+ this.retentionMs = clampPositive(options.retentionMs, MEDIA_STORE_RETENTION_MS, "retentionMs");
622
+ this.randomId = options.randomId ?? generateArtifactId;
623
+ }
624
+
625
+ /**
626
+ * Apply a live retention ceiling under the store lock. Lowering it rewrites
627
+ * existing index expiries and deletes newly expired objects before resolving;
628
+ * raising it affects only artifacts saved after this call.
629
+ */
630
+ async applyRetentionMs(retentionMs: number): Promise<MediaCleanupReport> {
631
+ const next = clampPositive(retentionMs, MEDIA_STORE_RETENTION_MS, "retentionMs");
632
+ return this.runExclusive(async () => {
633
+ const previous = this.retentionMs;
634
+ if (next > previous && !this.retentionReconciled) {
635
+ const report = await this.cleanupUnlocked();
636
+ this.retentionReconciled = true;
637
+ this.retentionMs = next;
638
+ return report;
639
+ }
640
+ const lowered = next < previous;
641
+ this.retentionMs = next;
642
+ if (!lowered && this.retentionReconciled) return { expiredArtifacts: 0, removedObjects: 0 };
643
+ try {
644
+ const report = await this.cleanupUnlocked();
645
+ this.retentionReconciled = true;
646
+ return report;
647
+ } catch (error) {
648
+ this.retentionReconciled = false;
649
+ throw error;
650
+ }
651
+ });
652
+ }
653
+
654
+ async save(input: SaveMediaInput): Promise<MediaArtifactMeta> {
655
+ return this.runExclusive(() => this.saveUnlocked(input));
656
+ }
657
+
658
+ async lookup(artifactId: string): Promise<MediaArtifactMeta | undefined> {
659
+ if (parseMediaArtifactId(artifactId) === undefined) return undefined;
660
+ const document = await this.readIndex(artifactId);
661
+ if (document === undefined) return undefined;
662
+ if (this.effectiveExpiresAt(document) <= this.now()) return undefined;
663
+ return this.effectiveMeta(document);
664
+ }
665
+
666
+ async read(artifactId: string): Promise<StoredMedia> {
667
+ const id = parseMediaArtifactId(artifactId);
668
+ if (id === undefined) throw new MediaStoreError("INVALID_ID", "artifact id is not a safe opaque identifier");
669
+ const document = await this.readIndex(id);
670
+ if (document === undefined) throw new MediaStoreError("NOT_FOUND", "media artifact was not found");
671
+ if (this.effectiveExpiresAt(document) <= this.now()) {
672
+ throw new MediaStoreError("EXPIRED", "media artifact has expired");
673
+ }
674
+ if (document.bytes > this.maxBytes) {
675
+ throw new MediaStoreError("CORRUPT", "media index exceeds the store byte ceiling");
676
+ }
677
+ let data: Uint8Array;
678
+ try {
679
+ data = await readBoundedFile(objectPath(this.root, document.sha256), this.maxBytes);
680
+ } catch (error) {
681
+ if (error instanceof MediaStoreError) throw error;
682
+ if (isENOENT(error)) throw new MediaStoreError("NOT_FOUND", "media object is missing", { cause: error });
683
+ throw error;
684
+ }
685
+ if (sha256Hex(data) !== document.sha256 || data.byteLength !== document.bytes) {
686
+ throw new MediaStoreError("CORRUPT", "stored media failed integrity verification");
687
+ }
688
+ return { meta: this.effectiveMeta(document), data };
689
+ }
690
+
691
+ async delete(artifactId: string): Promise<boolean> {
692
+ return this.runExclusive(() => this.deleteUnlocked(artifactId));
693
+ }
694
+
695
+ async cleanup(): Promise<MediaCleanupReport> {
696
+ return this.runExclusive(async () => {
697
+ try {
698
+ const report = await this.cleanupUnlocked();
699
+ this.retentionReconciled = true;
700
+ return report;
701
+ } catch (error) {
702
+ this.retentionReconciled = false;
703
+ throw error;
704
+ }
705
+ });
706
+ }
707
+
708
+ /** Trusted same-origin download primitive. Never returns an upstream URL. */
709
+ async openDownload(artifactId: string, authz: TrustedImagineAuthz): Promise<MediaDownloadView> {
710
+ assertTrustedImagineAuthz(authz);
711
+ const stored = await this.read(artifactId);
712
+ return {
713
+ meta: stored.meta,
714
+ body: stored.data,
715
+ headers: mediaDownloadHeaders(stored.meta),
716
+ };
717
+ }
718
+
719
+ private effectiveExpiresAt(document: IndexDocument): number {
720
+ return Math.min(document.expiresAt, document.createdAt + this.retentionMs);
721
+ }
722
+
723
+ private effectiveMeta(document: IndexDocument): MediaArtifactMeta {
724
+ return toMeta(document, this.effectiveExpiresAt(document));
725
+ }
726
+
727
+ private runExclusive<T>(operation: () => Promise<T>): Promise<T> {
728
+ const run = this.tail.then(operation, operation);
729
+ this.tail = run.then(
730
+ () => undefined,
731
+ () => undefined,
732
+ );
733
+ return run;
734
+ }
735
+
736
+ private async saveUnlocked(input: SaveMediaInput): Promise<MediaArtifactMeta> {
737
+ if (input.mediaType !== "video/mp4" && input.mediaType !== "video/webm") {
738
+ throw new MediaStoreError("UNSUPPORTED_TYPE", "media store only accepts video/mp4 or video/webm");
739
+ }
740
+ if (input.data.byteLength === 0) {
741
+ throw new MediaStoreError("INVALID_INPUT", "media artifact is empty");
742
+ }
743
+ if (input.data.byteLength > this.maxBytes) {
744
+ throw new MediaStoreError("TOO_LARGE", `media artifact exceeds the ${this.maxBytes} byte ceiling`);
745
+ }
746
+ await assertSafeRoot(this.root);
747
+ const createdAt = this.now();
748
+ const digest = sha256Hex(input.data);
749
+ const object = objectPath(this.root, digest);
750
+ await ensurePrivateDir(this.root, this.root);
751
+ await ensurePrivateDir(this.root, join(this.root, "objects"));
752
+ await ensurePrivateDir(this.root, join(this.root, "index"));
753
+ await ensurePrivateDir(this.root, dirname(object));
754
+ await this.cleanupUnlocked();
755
+ const existingObject = await lstatOrUndefined(object);
756
+ if (existingObject !== undefined) {
757
+ if (existingObject.isSymbolicLink() || !existingObject.isFile()) {
758
+ throw new MediaStoreError("CORRUPT", "object path is not a regular file");
759
+ }
760
+ const current = await readBoundedFile(object, this.maxBytes);
761
+ if (sha256Hex(current) !== digest) {
762
+ throw new MediaStoreError("CORRUPT", "stored object failed integrity verification");
763
+ }
764
+ } else {
765
+ const currentBytes = await storedObjectBytes(this.root);
766
+ if (currentBytes + input.data.byteLength > this.maxTotalBytes) {
767
+ throw new MediaStoreError("TOO_LARGE", `media store exceeds the ${this.maxTotalBytes} byte total ceiling`);
768
+ }
769
+ try {
770
+ await writeExclusiveFile(this.root, object, input.data);
771
+ } catch (error) {
772
+ if (!isEEXIST(error)) throw error;
773
+ const current = await readBoundedFile(object, this.maxBytes);
774
+ if (sha256Hex(current) !== digest) {
775
+ throw new MediaStoreError("CORRUPT", "stored object failed integrity verification");
776
+ }
777
+ }
778
+ }
779
+ let artifactId = this.randomId();
780
+ for (let attempt = 0; attempt < 8; attempt += 1) {
781
+ if (parseMediaArtifactId(artifactId) === undefined) {
782
+ throw new MediaStoreError("INVALID_ID", "generated artifact id is not opaque hex");
783
+ }
784
+ const target = indexPath(this.root, artifactId);
785
+ const existingIndex = await lstatOrUndefined(target);
786
+ if (existingIndex !== undefined) {
787
+ if (existingIndex.isSymbolicLink()) {
788
+ throw new MediaStoreError("CORRUPT", "media index leaf must not be a symlink");
789
+ }
790
+ artifactId = this.randomId();
791
+ continue;
792
+ }
793
+ const document: IndexDocument = {
794
+ version: MEDIA_STORE_INDEX_VERSION,
795
+ artifactId,
796
+ sha256: digest,
797
+ mediaType: input.mediaType,
798
+ bytes: input.data.byteLength,
799
+ createdAt,
800
+ expiresAt: createdAt + this.retentionMs,
801
+ };
802
+ const name = sanitizeDisplayName(input.name);
803
+ if (name !== undefined) document.name = name;
804
+ try {
805
+ await writeExclusiveFile(this.root, target, `${JSON.stringify(document)}\n`);
806
+ } catch (error) {
807
+ if (!isEEXIST(error)) throw error;
808
+ artifactId = this.randomId();
809
+ continue;
810
+ }
811
+ return toMeta(document);
812
+ }
813
+ throw new MediaStoreError("INVALID_ID", "unable to allocate an unused artifact id");
814
+ }
815
+
816
+ private async deleteUnlocked(artifactId: string): Promise<boolean> {
817
+ if (parseMediaArtifactId(artifactId) === undefined) return false;
818
+ await assertSafeRoot(this.root);
819
+ const document = await this.readIndex(artifactId);
820
+ if (document === undefined) return false;
821
+ const file = indexPath(this.root, document.artifactId);
822
+ const stat = await lstatOrUndefined(file);
823
+ if (stat?.isSymbolicLink() === true) {
824
+ await unlink(file).catch(() => undefined);
825
+ return true;
826
+ }
827
+ await unlink(file).catch((error: unknown) => {
828
+ if (!isENOENT(error)) throw error;
829
+ });
830
+ await this.removeObjectIfUnreferenced(document.sha256);
831
+ return true;
832
+ }
833
+
834
+ private async cleanupUnlocked(): Promise<MediaCleanupReport> {
835
+ await assertSafeRoot(this.root);
836
+ const now = this.now();
837
+ const files = await listRegularFiles(this.root, "index");
838
+ let expiredArtifacts = 0;
839
+ const liveDigests = new Set<string>();
840
+ for (const file of files) {
841
+ let document: IndexDocument;
842
+ try {
843
+ const raw = await readBoundedFile(file, MEDIA_STORE_INDEX_MAX_BYTES);
844
+ document = parseIndexDocument(new TextDecoder().decode(raw));
845
+ } catch {
846
+ await unlink(file).catch(() => undefined);
847
+ expiredArtifacts += 1;
848
+ continue;
849
+ }
850
+ const effectiveExpiresAt = this.effectiveExpiresAt(document);
851
+ if (effectiveExpiresAt <= now) {
852
+ await unlink(file).catch(() => undefined);
853
+ expiredArtifacts += 1;
854
+ continue;
855
+ }
856
+ if (effectiveExpiresAt < document.expiresAt) {
857
+ document.expiresAt = effectiveExpiresAt;
858
+ await writeFileAtomic(assertInsideRoot(this.root, file), `${JSON.stringify(document)}\n`, {
859
+ mode: MEDIA_STORE_FILE_MODE,
860
+ dirMode: MEDIA_STORE_DIR_MODE,
861
+ });
862
+ }
863
+ liveDigests.add(document.sha256);
864
+ }
865
+ let removedObjects = 0;
866
+ for (const file of await listRegularFiles(this.root, "objects")) {
867
+ const digest = file.slice(file.lastIndexOf(sep) + 1);
868
+ if (liveDigests.has(digest)) continue;
869
+ const stat = await lstatOrUndefined(file);
870
+ if (stat === undefined) continue;
871
+ if (stat.isSymbolicLink() || stat.isFile()) {
872
+ await unlink(file).catch(() => undefined);
873
+ removedObjects += 1;
874
+ }
875
+ }
876
+ return { expiredArtifacts, removedObjects };
877
+ }
878
+
879
+ private async readIndex(artifactId: string): Promise<IndexDocument | undefined> {
880
+ const file = indexPath(this.root, artifactId);
881
+ let bytes: Uint8Array;
882
+ try {
883
+ const stat = await lstatOrUndefined(file);
884
+ if (stat === undefined) return undefined;
885
+ if (stat.isSymbolicLink() || !stat.isFile()) return undefined;
886
+ bytes = await readBoundedFile(file, MEDIA_STORE_INDEX_MAX_BYTES);
887
+ } catch (error) {
888
+ if (isENOENT(error)) return undefined;
889
+ throw error;
890
+ }
891
+ const document = parseIndexDocument(new TextDecoder().decode(bytes));
892
+ if (document.artifactId !== artifactId) {
893
+ throw new MediaStoreError("CORRUPT", "media index id does not match its filename");
894
+ }
895
+ return document;
896
+ }
897
+
898
+ private async removeObjectIfUnreferenced(sha256: string): Promise<void> {
899
+ for (const file of await listRegularFiles(this.root, "index")) {
900
+ try {
901
+ const document = parseIndexDocument(
902
+ new TextDecoder().decode(await readBoundedFile(file, MEDIA_STORE_INDEX_MAX_BYTES)),
903
+ );
904
+ if (document.sha256 === sha256 && this.effectiveExpiresAt(document) > this.now()) return;
905
+ } catch {
906
+ // A corrupt sibling does not keep the object alive.
907
+ }
908
+ }
909
+ const object = objectPath(this.root, sha256);
910
+ const stat = await lstatOrUndefined(object);
911
+ if (stat === undefined) return;
912
+ if (stat.isSymbolicLink() || stat.isFile()) await unlink(object).catch(() => undefined);
913
+ }
914
+ }
915
+
916
+ /** Open a trusted same-origin download. Requires an explicit authz token. */
917
+ export async function openTrustedMediaDownload(
918
+ store: MediaStore,
919
+ artifactId: string,
920
+ authz: TrustedImagineAuthz,
921
+ ): Promise<MediaDownloadView> {
922
+ assertTrustedImagineAuthz(authz);
923
+ if (!isSafeMediaArtifactId(artifactId)) {
924
+ throw new MediaStoreError("INVALID_ID", "artifact id is not a safe opaque identifier");
925
+ }
926
+ return store.openDownload(artifactId, authz);
927
+ }