@glw907/cairn-cms 0.60.0 → 0.62.1

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 (281) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/components/AdminLayout.svelte +152 -229
  3. package/dist/components/CairnAdmin.svelte +13 -42
  4. package/dist/components/CairnLogo.svelte +1 -6
  5. package/dist/components/CairnMediaLibrary.svelte +821 -1210
  6. package/dist/components/CairnTidySettings.svelte +194 -261
  7. package/dist/components/CairnTidySettings.svelte.d.ts +1 -1
  8. package/dist/components/ComponentForm.svelte +110 -185
  9. package/dist/components/ComponentInsertDialog.svelte +163 -283
  10. package/dist/components/ConceptList.svelte +111 -191
  11. package/dist/components/ConfirmPage.svelte +5 -12
  12. package/dist/components/CsrfField.svelte +5 -11
  13. package/dist/components/DeleteDialog.svelte +15 -42
  14. package/dist/components/EditPage.svelte +781 -1205
  15. package/dist/components/EditorToolbar.svelte +108 -170
  16. package/dist/components/HelpHome.svelte +824 -0
  17. package/dist/components/HelpHome.svelte.d.ts +22 -0
  18. package/dist/components/IconPicker.svelte +23 -53
  19. package/dist/components/LinkPicker.svelte +34 -58
  20. package/dist/components/LoginPage.svelte +14 -27
  21. package/dist/components/ManageEditors.svelte +3 -15
  22. package/dist/components/MarkdownEditor.svelte +689 -957
  23. package/dist/components/MarkdownHelpDialog.svelte +12 -27
  24. package/dist/components/MediaCaptureCard.svelte +18 -57
  25. package/dist/components/MediaFigureControl.svelte +32 -71
  26. package/dist/components/MediaHeroField.svelte +210 -329
  27. package/dist/components/MediaInsertPopover.svelte +156 -283
  28. package/dist/components/MediaPicker.svelte +67 -131
  29. package/dist/components/NavTree.svelte +46 -78
  30. package/dist/components/RenameDialog.svelte +16 -43
  31. package/dist/components/ShortcutsDialog.svelte +9 -13
  32. package/dist/components/ShortcutsGrid.svelte +1 -2
  33. package/dist/components/TidyReview.svelte +140 -248
  34. package/dist/components/WebLinkDialog.svelte +19 -40
  35. package/dist/components/cairn-admin.css +4 -0
  36. package/dist/components/client-ingest.d.ts +16 -8
  37. package/dist/components/client-ingest.js +12 -6
  38. package/dist/components/editor-media.js +16 -8
  39. package/dist/components/editor-placeholder.d.ts +4 -2
  40. package/dist/components/editor-tidy.d.ts +24 -12
  41. package/dist/components/editor-tidy.js +8 -4
  42. package/dist/components/index.d.ts +1 -0
  43. package/dist/components/index.js +1 -0
  44. package/dist/components/link-completion.d.ts +12 -6
  45. package/dist/components/link-completion.js +12 -6
  46. package/dist/components/markdown-directives.d.ts +9 -6
  47. package/dist/components/markdown-directives.js +9 -6
  48. package/dist/components/markdown-format.d.ts +7 -2
  49. package/dist/components/markdown-format.js +59 -28
  50. package/dist/components/markdown-reference.d.ts +8 -0
  51. package/dist/components/markdown-reference.js +22 -0
  52. package/dist/components/media-upload-outcome.d.ts +12 -6
  53. package/dist/components/objective-errors.d.ts +8 -4
  54. package/dist/components/objective-errors.js +8 -4
  55. package/dist/components/preview-doc.d.ts +4 -2
  56. package/dist/components/preview-doc.js +4 -2
  57. package/dist/components/spellcheck.d.ts +57 -29
  58. package/dist/components/spellcheck.js +50 -20
  59. package/dist/components/tidy-categorize.d.ts +20 -10
  60. package/dist/components/tidy-categorize.js +16 -8
  61. package/dist/components/tidy-validate.d.ts +12 -6
  62. package/dist/components/tidy-validate.js +20 -10
  63. package/dist/components/topbar-context.d.ts +4 -2
  64. package/dist/content/advisories.d.ts +51 -0
  65. package/dist/content/advisories.js +79 -0
  66. package/dist/content/compose.d.ts +4 -2
  67. package/dist/content/compose.js +1 -0
  68. package/dist/content/excerpt.js +4 -2
  69. package/dist/content/getting-started.d.ts +18 -0
  70. package/dist/content/getting-started.js +12 -0
  71. package/dist/content/links.d.ts +16 -8
  72. package/dist/content/links.js +12 -6
  73. package/dist/content/manifest.d.ts +36 -18
  74. package/dist/content/manifest.js +32 -16
  75. package/dist/content/media-refs.d.ts +4 -2
  76. package/dist/content/media-refs.js +4 -2
  77. package/dist/content/media-rewrite.d.ts +8 -4
  78. package/dist/content/media-rewrite.js +76 -38
  79. package/dist/content/schema.d.ts +20 -10
  80. package/dist/content/site-dictionary.d.ts +4 -2
  81. package/dist/content/site-dictionary.js +8 -4
  82. package/dist/content/types.d.ts +97 -42
  83. package/dist/delivery/CairnHead.svelte +8 -11
  84. package/dist/delivery/content-index.d.ts +16 -8
  85. package/dist/delivery/feeds.js +4 -2
  86. package/dist/delivery/json-ld.d.ts +3 -0
  87. package/dist/delivery/json-ld.js +3 -0
  88. package/dist/delivery/manifest.d.ts +4 -2
  89. package/dist/delivery/manifest.js +4 -2
  90. package/dist/delivery/public-routes.d.ts +12 -6
  91. package/dist/delivery/public-routes.js +4 -2
  92. package/dist/delivery/seo-fields.d.ts +12 -6
  93. package/dist/delivery/seo-fields.js +8 -4
  94. package/dist/delivery/site-indexes.d.ts +4 -2
  95. package/dist/delivery/site-resolver.d.ts +4 -2
  96. package/dist/delivery/site-resolver.js +4 -2
  97. package/dist/doctor/cloudflare-api.d.ts +6 -0
  98. package/dist/doctor/cloudflare-api.js +6 -0
  99. package/dist/doctor/index.d.ts +12 -6
  100. package/dist/doctor/report.d.ts +3 -0
  101. package/dist/doctor/report.js +3 -0
  102. package/dist/doctor/run.d.ts +3 -0
  103. package/dist/doctor/run.js +3 -0
  104. package/dist/doctor/types.d.ts +10 -2
  105. package/dist/doctor/types.js +6 -0
  106. package/dist/doctor/wrangler-config.d.ts +7 -2
  107. package/dist/doctor/wrangler-config.js +3 -0
  108. package/dist/email.d.ts +4 -2
  109. package/dist/env.d.ts +0 -3
  110. package/dist/env.js +0 -3
  111. package/dist/github/branches.d.ts +4 -2
  112. package/dist/github/branches.js +4 -2
  113. package/dist/github/signing.d.ts +1 -1
  114. package/dist/github/signing.js +2 -2
  115. package/dist/log/events.d.ts +1 -1
  116. package/dist/media/bulk-delete-plan.d.ts +8 -4
  117. package/dist/media/config.d.ts +12 -6
  118. package/dist/media/config.js +16 -8
  119. package/dist/media/delivery-bucket.d.ts +4 -2
  120. package/dist/media/library-entry.d.ts +4 -2
  121. package/dist/media/library-entry.js +4 -2
  122. package/dist/media/manifest.d.ts +29 -15
  123. package/dist/media/manifest.js +29 -16
  124. package/dist/media/naming.d.ts +12 -6
  125. package/dist/media/naming.js +24 -12
  126. package/dist/media/orphan-scan.d.ts +4 -2
  127. package/dist/media/reconcile.d.ts +21 -11
  128. package/dist/media/reconcile.js +12 -6
  129. package/dist/media/reference.d.ts +8 -4
  130. package/dist/media/reference.js +12 -6
  131. package/dist/media/rewrite-plan.d.ts +12 -6
  132. package/dist/media/sniff.d.ts +4 -2
  133. package/dist/media/sniff.js +28 -14
  134. package/dist/media/store.d.ts +16 -8
  135. package/dist/media/store.js +4 -2
  136. package/dist/media/transform-url.d.ts +12 -6
  137. package/dist/media/transform-url.js +8 -4
  138. package/dist/media/usage.d.ts +8 -4
  139. package/dist/nav/site-config.d.ts +16 -8
  140. package/dist/render/component-grammar.d.ts +23 -10
  141. package/dist/render/component-grammar.js +19 -8
  142. package/dist/render/component-insert.d.ts +8 -4
  143. package/dist/render/component-insert.js +4 -2
  144. package/dist/render/component-reference.d.ts +4 -2
  145. package/dist/render/component-reference.js +4 -2
  146. package/dist/render/component-validate.d.ts +3 -0
  147. package/dist/render/component-validate.js +3 -0
  148. package/dist/render/glyph.d.ts +4 -2
  149. package/dist/render/glyph.js +4 -2
  150. package/dist/render/pipeline.d.ts +20 -10
  151. package/dist/render/pipeline.js +4 -2
  152. package/dist/render/registry.d.ts +40 -20
  153. package/dist/render/registry.js +16 -8
  154. package/dist/render/rehype-dispatch.d.ts +22 -8
  155. package/dist/render/rehype-dispatch.js +22 -8
  156. package/dist/render/remark-directives.d.ts +3 -0
  157. package/dist/render/remark-directives.js +3 -0
  158. package/dist/render/remark-figure.d.ts +4 -2
  159. package/dist/render/remark-figure.js +4 -2
  160. package/dist/render/resolve-links.d.ts +4 -2
  161. package/dist/render/resolve-links.js +4 -2
  162. package/dist/render/resolve-media.d.ts +16 -8
  163. package/dist/render/resolve-media.js +12 -6
  164. package/dist/sveltekit/admin-dispatch.d.ts +2 -0
  165. package/dist/sveltekit/admin-dispatch.js +9 -3
  166. package/dist/sveltekit/auth-routes.d.ts +3 -0
  167. package/dist/sveltekit/auth-routes.js +3 -0
  168. package/dist/sveltekit/cairn-admin.d.ts +16 -5
  169. package/dist/sveltekit/cairn-admin.js +26 -10
  170. package/dist/sveltekit/content-routes.d.ts +191 -86
  171. package/dist/sveltekit/content-routes.js +295 -107
  172. package/dist/sveltekit/editors-routes.d.ts +3 -0
  173. package/dist/sveltekit/editors-routes.js +3 -0
  174. package/dist/sveltekit/guard.d.ts +4 -2
  175. package/dist/sveltekit/guard.js +4 -2
  176. package/dist/sveltekit/https-required-page.d.ts +1 -1
  177. package/dist/sveltekit/https-required-page.js +1 -1
  178. package/dist/sveltekit/index.d.ts +1 -1
  179. package/dist/sveltekit/media-route.d.ts +1 -2
  180. package/dist/sveltekit/media-route.js +13 -8
  181. package/dist/sveltekit/nav-routes.d.ts +7 -2
  182. package/dist/sveltekit/nav-routes.js +3 -0
  183. package/dist/sveltekit/types.d.ts +4 -2
  184. package/dist/vite/index.d.ts +32 -16
  185. package/dist/vite/index.js +52 -26
  186. package/dist/vite/resolve-root.d.ts +8 -4
  187. package/dist/vite/resolve-root.js +4 -2
  188. package/package.json +8 -2
  189. package/src/lib/components/AdminLayout.svelte +22 -0
  190. package/src/lib/components/CairnAdmin.svelte +3 -0
  191. package/src/lib/components/CairnTidySettings.svelte +2 -2
  192. package/src/lib/components/ComponentForm.svelte +0 -1
  193. package/src/lib/components/EditPage.svelte +133 -41
  194. package/src/lib/components/HelpHome.svelte +850 -0
  195. package/src/lib/components/MarkdownHelpDialog.svelte +4 -15
  196. package/src/lib/components/client-ingest.ts +20 -10
  197. package/src/lib/components/editor-media.ts +20 -10
  198. package/src/lib/components/editor-placeholder.ts +12 -6
  199. package/src/lib/components/editor-tidy.ts +28 -14
  200. package/src/lib/components/index.ts +1 -0
  201. package/src/lib/components/link-completion.ts +12 -6
  202. package/src/lib/components/markdown-directives.ts +13 -8
  203. package/src/lib/components/markdown-format.ts +63 -30
  204. package/src/lib/components/markdown-reference.ts +30 -0
  205. package/src/lib/components/media-upload-outcome.ts +12 -6
  206. package/src/lib/components/objective-errors.ts +16 -8
  207. package/src/lib/components/preview-doc.ts +4 -2
  208. package/src/lib/components/spellcheck.ts +92 -40
  209. package/src/lib/components/tidy-categorize.ts +28 -14
  210. package/src/lib/components/tidy-validate.ts +28 -14
  211. package/src/lib/components/topbar-context.ts +4 -2
  212. package/src/lib/content/advisories.ts +141 -0
  213. package/src/lib/content/compose.ts +5 -2
  214. package/src/lib/content/excerpt.ts +4 -2
  215. package/src/lib/content/getting-started.ts +31 -0
  216. package/src/lib/content/links.ts +16 -8
  217. package/src/lib/content/manifest.ts +36 -18
  218. package/src/lib/content/media-refs.ts +4 -2
  219. package/src/lib/content/media-rewrite.ts +100 -50
  220. package/src/lib/content/schema.ts +20 -10
  221. package/src/lib/content/site-dictionary.ts +8 -4
  222. package/src/lib/content/types.ts +97 -42
  223. package/src/lib/delivery/content-index.ts +16 -8
  224. package/src/lib/delivery/feeds.ts +4 -2
  225. package/src/lib/delivery/json-ld.ts +3 -0
  226. package/src/lib/delivery/manifest.ts +4 -2
  227. package/src/lib/delivery/public-routes.ts +16 -8
  228. package/src/lib/delivery/seo-fields.ts +12 -6
  229. package/src/lib/delivery/site-indexes.ts +4 -2
  230. package/src/lib/delivery/site-resolver.ts +4 -2
  231. package/src/lib/doctor/cloudflare-api.ts +6 -0
  232. package/src/lib/doctor/index.ts +12 -6
  233. package/src/lib/doctor/report.ts +3 -0
  234. package/src/lib/doctor/run.ts +3 -0
  235. package/src/lib/doctor/types.ts +10 -2
  236. package/src/lib/doctor/wrangler-config.ts +7 -2
  237. package/src/lib/email.ts +4 -2
  238. package/src/lib/env.ts +0 -3
  239. package/src/lib/github/branches.ts +4 -2
  240. package/src/lib/github/signing.ts +2 -2
  241. package/src/lib/log/events.ts +1 -0
  242. package/src/lib/media/bulk-delete-plan.ts +8 -4
  243. package/src/lib/media/config.ts +24 -12
  244. package/src/lib/media/delivery-bucket.ts +4 -2
  245. package/src/lib/media/library-entry.ts +4 -2
  246. package/src/lib/media/manifest.ts +33 -18
  247. package/src/lib/media/naming.ts +24 -12
  248. package/src/lib/media/orphan-scan.ts +4 -2
  249. package/src/lib/media/reconcile.ts +21 -11
  250. package/src/lib/media/reference.ts +12 -6
  251. package/src/lib/media/rewrite-plan.ts +12 -6
  252. package/src/lib/media/sniff.ts +28 -14
  253. package/src/lib/media/store.ts +16 -8
  254. package/src/lib/media/transform-url.ts +12 -6
  255. package/src/lib/media/usage.ts +8 -4
  256. package/src/lib/nav/site-config.ts +16 -8
  257. package/src/lib/render/component-grammar.ts +23 -10
  258. package/src/lib/render/component-insert.ts +8 -4
  259. package/src/lib/render/component-reference.ts +4 -2
  260. package/src/lib/render/component-validate.ts +3 -0
  261. package/src/lib/render/glyph.ts +4 -2
  262. package/src/lib/render/pipeline.ts +20 -10
  263. package/src/lib/render/registry.ts +44 -22
  264. package/src/lib/render/rehype-dispatch.ts +22 -8
  265. package/src/lib/render/remark-directives.ts +3 -0
  266. package/src/lib/render/remark-figure.ts +4 -2
  267. package/src/lib/render/resolve-links.ts +4 -2
  268. package/src/lib/render/resolve-media.ts +16 -8
  269. package/src/lib/sveltekit/admin-dispatch.ts +10 -4
  270. package/src/lib/sveltekit/auth-routes.ts +3 -0
  271. package/src/lib/sveltekit/cairn-admin.ts +37 -15
  272. package/src/lib/sveltekit/content-routes.ts +492 -197
  273. package/src/lib/sveltekit/editors-routes.ts +3 -0
  274. package/src/lib/sveltekit/guard.ts +4 -2
  275. package/src/lib/sveltekit/https-required-page.ts +1 -1
  276. package/src/lib/sveltekit/index.ts +3 -0
  277. package/src/lib/sveltekit/media-route.ts +13 -8
  278. package/src/lib/sveltekit/nav-routes.ts +7 -2
  279. package/src/lib/sveltekit/types.ts +4 -2
  280. package/src/lib/vite/index.ts +60 -30
  281. package/src/lib/vite/resolve-root.ts +8 -4
package/src/lib/env.ts CHANGED
@@ -7,7 +7,6 @@ import type { DeliveryBucket } from './media/delivery-bucket.js';
7
7
  *
8
8
  * The origin is always config-derived, never read from a request header, so a
9
9
  * forged Host header cannot redirect a magic link (spec 7.1, risk H3).
10
- *
11
10
  * @throws CairnError (`config.public-origin-invalid`) when `PUBLIC_ORIGIN` is unset or
12
11
  * empty, fails to parse as a URL, or uses http on a non-local host.
13
12
  */
@@ -41,7 +40,6 @@ export function requireOrigin(env: { PUBLIC_ORIGIN?: string }): string {
41
40
  *
42
41
  * The handlers read D1 off `event.platform.env`; without this a misconfigured binding
43
42
  * surfaces as a raw `TypeError` deep in a store call. This gives the failure a name.
44
- *
45
43
  * @throws CairnError (`config.bindings-missing`) when `AUTH_DB` is missing.
46
44
  */
47
45
  export function requireDb(env: { AUTH_DB?: D1Database }): D1Database {
@@ -65,7 +63,6 @@ export function requireDb(env: { AUTH_DB?: D1Database }): D1Database {
65
63
  * which is truthy but carries no callable `get`. Without that check the cast would succeed and the
66
64
  * first `bucket.get(...)` would throw an uncaught 500 rather than the drained 503 a missing binding
67
65
  * earns.
68
- *
69
66
  * @throws CairnError (`config.bindings-missing`) when the named binding is absent or not an R2 bucket.
70
67
  */
71
68
  export function requireBucket(env: Record<string, unknown>, bindingName: string): DeliveryBucket {
@@ -66,9 +66,11 @@ function nextPageUrl(link: string | null): string | null {
66
66
  return null;
67
67
  }
68
68
 
69
- /** Branch names under `prefix`, sorted. The matching-refs API paginates at 30 by default, so a
69
+ /**
70
+ * Branch names under `prefix`, sorted. The matching-refs API paginates at 30 by default, so a
70
71
  * site with 31+ pending entries would silently truncate; request the 100-per-page maximum and
71
- * follow the Link rel="next" chain until exhausted. */
72
+ * follow the Link rel="next" chain until exhausted.
73
+ */
72
74
  export async function listBranches(repo: RepoRef, prefix: string, token: string): Promise<string[]> {
73
75
  const names: string[] = [];
74
76
  let url: string | null = `${gitUrl(repo, `matching-refs/heads/${prefix}`)}?per_page=100`;
@@ -20,7 +20,7 @@ function buf(bytes: Uint8Array): ArrayBuffer {
20
20
  return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
21
21
  }
22
22
 
23
- /** DER length octets for a value of `n` bytes (short form < 128, else long form). */
23
+ /** DER length octets for a value of `n` bytes (short form `< 128`, else long form). */
24
24
  function derLength(n: number): number[] {
25
25
  if (n < 0x80) return [n];
26
26
  const out: number[] = [];
@@ -87,7 +87,7 @@ interface CachedToken {
87
87
  * Build an installation-token cache. A module-global instance memoizes the minted token per
88
88
  * installation for most of its one-hour life, so a warm Worker isolate reuses it across requests
89
89
  * instead of re-signing and re-calling GitHub on every list and commit. A cold isolate re-mints,
90
- * which is always safe. This mirrors the default of @octokit/auth-app, which caches installation
90
+ * which is always safe. This mirrors the default of `@octokit/auth-app`, which caches installation
91
91
  * tokens in memory and returns them until expiry. The TTL stays under GitHub's documented one-hour
92
92
  * lifetime, so a fixed margin avoids parsing the API expiry. The cache holds the in-flight
93
93
  * promise, not the resolved token, so a cold isolate's parallel loads coalesce into one mint;
@@ -14,6 +14,7 @@ export type CairnLogEvent =
14
14
  | 'entry.published'
15
15
  | 'entry.discarded'
16
16
  | 'publish.failed'
17
+ | 'publish.address_collision'
17
18
  | 'github.unreachable'
18
19
  | 'guard.rejected'
19
20
  | 'media.uploaded'
@@ -8,16 +8,20 @@
8
8
  import type { UsageEntry, UsageIndex } from './usage.js';
9
9
  import type { MediaManifest } from './manifest.js';
10
10
 
11
- /** One selected hash that is not deleted, with why and (for the where-used) its usage rows. The rows
12
- * are present only for 'still-referenced'; an 'uncommitted' skip carries an empty list. */
11
+ /**
12
+ * One selected hash that is not deleted, with why and (for the where-used) its usage rows. The rows
13
+ * are present only for 'still-referenced'; an 'uncommitted' skip carries an empty list.
14
+ */
13
15
  export interface BulkDeleteSkip {
14
16
  hash: string;
15
17
  reason: 'still-referenced' | 'uncommitted';
16
18
  usage: UsageEntry[];
17
19
  }
18
20
 
19
- /** The partitioned selection: the hashes safe to purge and the hashes held back. Both arrays keep the
20
- * input order of `selected` so the screen reports them in the order the user picked. */
21
+ /**
22
+ * The partitioned selection: the hashes safe to purge and the hashes held back. Both arrays keep the
23
+ * input order of `selected` so the screen reports them in the order the user picked.
24
+ */
21
25
  export interface BulkDeletePlan {
22
26
  deletable: string[];
23
27
  skipped: BulkDeleteSkip[];
@@ -7,9 +7,11 @@
7
7
  import type { AssetConfig } from '../content/types.js';
8
8
  import type { VariantSpec } from './transform-url.js';
9
9
 
10
- /** The resolved media config the engine serves from. When a site declares no assets block, media is
10
+ /**
11
+ * The resolved media config the engine serves from. When a site declares no assets block, media is
11
12
  * off and the value is `{ enabled: false }`; otherwise every field is filled from the AssetConfig
12
- * or its default. */
13
+ * or its default.
14
+ */
13
15
  export type ResolvedAssetConfig =
14
16
  | { enabled: false }
15
17
  | {
@@ -20,8 +22,10 @@ export type ResolvedAssetConfig =
20
22
  maxUploadBytes: number;
21
23
  allowedTypes: string[];
22
24
  variants: Record<string, VariantSpec>;
23
- /** Whether Cloudflare Image Transformations are enabled for the zone. With it false, the media
24
- * resolver serves the bare full-size delivery path and ignores any preset. */
25
+ /**
26
+ * Whether Cloudflare Image Transformations are enabled for the zone. With it false, the media
27
+ * resolver serves the bare full-size delivery path and ignores any preset.
28
+ */
25
29
  transformations: boolean;
26
30
  };
27
31
 
@@ -32,8 +36,10 @@ const DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
32
36
  /** The default accepted upload MIME types: the common web image formats. */
33
37
  const DEFAULT_ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/avif'];
34
38
 
35
- /** The built-in named transform presets. A site's `variants` merge over these, so a caller preset of
36
- * the same name overrides the built-in. */
39
+ /**
40
+ * The built-in named transform presets. A site's `variants` merge over these, so a caller preset of
41
+ * the same name overrides the built-in.
42
+ */
37
43
  const BUILT_IN_PRESETS: Record<string, VariantSpec> = {
38
44
  thumb: { width: 320, height: 320, fit: 'cover' },
39
45
  inline: { width: 800 },
@@ -43,8 +49,10 @@ const BUILT_IN_PRESETS: Record<string, VariantSpec> = {
43
49
 
44
50
  /** The fit values Cloudflare Images accepts. A variant whose fit is set to anything else is rejected. */
45
51
  const FIT_VALUES: ReadonlySet<string> = new Set(['scale-down', 'contain', 'cover', 'crop', 'pad']);
46
- /** The named gravity keywords Cloudflare Images accepts. A gravity is also valid as a coordinate
47
- * string; everything else is rejected. */
52
+ /**
53
+ * The named gravity keywords Cloudflare Images accepts. A gravity is also valid as a coordinate
54
+ * string; everything else is rejected.
55
+ */
48
56
  const GRAVITY_KEYWORDS: ReadonlySet<string> = new Set([
49
57
  'auto',
50
58
  'face',
@@ -57,9 +65,11 @@ const GRAVITY_KEYWORDS: ReadonlySet<string> = new Set([
57
65
  /** A gravity coordinate string, e.g. "0.5x0.5". */
58
66
  const GRAVITY_COORD_RE = /^\d+(\.\d+)?x\d+(\.\d+)?$/;
59
67
 
60
- /** Validate one variant's fit and gravity, throwing a cairn:-prefixed error naming the offending
68
+ /**
69
+ * Validate one variant's fit and gravity, throwing a cairn:-prefixed error naming the offending
61
70
  * preset and value. The type system collapses VariantSpec.gravity to string, so the gravity check
62
- * is the only guard against a bogus value reaching the transform URL. */
71
+ * is the only guard against a bogus value reaching the transform URL.
72
+ */
63
73
  function validateVariant(name: string, spec: VariantSpec): void {
64
74
  if (spec.fit !== undefined && !FIT_VALUES.has(spec.fit)) {
65
75
  throw new Error(`cairn: media variant "${name}" has an unknown fit "${spec.fit}"`);
@@ -73,10 +83,12 @@ function validateVariant(name: string, spec: VariantSpec): void {
73
83
  }
74
84
  }
75
85
 
76
- /** Validate a site's AssetConfig and resolve it into a ResolvedAssetConfig. An undefined block leaves
86
+ /**
87
+ * Validate a site's AssetConfig and resolve it into a ResolvedAssetConfig. An undefined block leaves
77
88
  * media off and returns `{ enabled: false }` rather than throwing. A declared block must name its R2
78
89
  * bucket and carry a known urlForm and valid variant fit and gravity values; each failure throws a
79
- * cairn:-prefixed error. The named variants merge over the built-in presets. */
90
+ * cairn:-prefixed error. The named variants merge over the built-in presets.
91
+ */
80
92
  export function normalizeAssets(assets: AssetConfig | undefined): ResolvedAssetConfig {
81
93
  if (assets === undefined) return { enabled: false };
82
94
 
@@ -16,9 +16,11 @@ export interface DeliveryObject {
16
16
  httpEtag: string;
17
17
  /** The full object size in bytes, the denominator of a `Content-Range`. */
18
18
  size: number;
19
- /** Present only on a ranged read: the served window, used to build the `Content-Range`. R2 fills
19
+ /**
20
+ * Present only on a ranged read: the served window, used to build the `Content-Range`. R2 fills
20
21
  * both fields for a `bytes=start-end` request; each is typed optional so the route derives the
21
- * range bounds defensively against `size`. */
22
+ * range bounds defensively against `size`.
23
+ */
22
24
  range?: { offset?: number; length?: number };
23
25
  }
24
26
 
@@ -39,9 +39,11 @@ export interface MediaLibraryEntry {
39
39
  /** The projected library keyed by the 16-hex content hash, exactly EditData's `mediaLibrary`. */
40
40
  export type MediaLibrary = Record<string, MediaLibraryEntry>;
41
41
 
42
- /** Project a stored MediaEntry to the picker's MediaLibraryEntry, copying every display field and
42
+ /**
43
+ * Project a stored MediaEntry to the picker's MediaLibraryEntry, copying every display field and
43
44
  * dropping the source-only sha256 and original filename. The single projection editLoad and
44
- * mediaLibraryLoad both call, so the popover and the Library never diverge on the shared shape. */
45
+ * mediaLibraryLoad both call, so the popover and the Library never diverge on the shared shape.
46
+ */
45
47
  export function mediaLibraryEntry(entry: MediaEntry): MediaLibraryEntry {
46
48
  return {
47
49
  hash: entry.hash,
@@ -4,10 +4,12 @@
4
4
  // are never stored twice. It mirrors the content manifest in ../content/manifest.ts, keyed by the
5
5
  // 16-hex content-hash prefix rather than concept and id.
6
6
 
7
- /** One stored asset's row: its content hash, its human layer, and its byte and pixel facts. The
7
+ /**
8
+ * One stored asset's row: its content hash, its human layer, and its byte and pixel facts. The
8
9
  * `contentType` is the stored MIME type, so the delivery route serves it verbatim rather than
9
10
  * guessing from the extension. `width` and `height` are null when no dimensions are known (the
10
- * client is the only dimension source and a Worker cannot re-derive them). */
11
+ * client is the only dimension source and a Worker cannot re-derive them).
12
+ */
11
13
  export interface MediaEntry {
12
14
  hash: string;
13
15
  sha256: string;
@@ -26,19 +28,22 @@ export interface MediaEntry {
26
28
  /** The whole stored-asset record, keyed by the 16-hex content-hash prefix. */
27
29
  export type MediaManifest = Record<string, MediaEntry>;
28
30
 
29
- /** Parse a committed media manifest. Tolerant: an empty, missing, null, or non-object input yields
30
- * an empty manifest, so a first ingest into a site with no manifest file reads a clean {}. A valid
31
- * object is returned as the manifest. */
31
+ /**
32
+ * Parse a committed media manifest. Tolerant: an empty, missing, null, or non-object input yields
33
+ * an empty manifest, so a first ingest into a site with no manifest file reads a clean `{}`. A valid
34
+ * object is returned as the manifest.
35
+ */
32
36
  export function parseMediaManifest(json: unknown): MediaManifest {
33
37
  if (!json || typeof json !== 'object' || Array.isArray(json)) return {};
34
38
  return json as MediaManifest;
35
39
  }
36
40
 
37
- /** Validate one posted value as a MediaEntry, returning it narrowed or undefined. The trust boundary
41
+ /**
42
+ * Validate one posted value as a MediaEntry, returning it narrowed or undefined. The trust boundary
38
43
  * for an optimistic record the client re-posts: the upload action server-owned each field at
39
44
  * creation, but a re-post is untrusted, so every field is re-checked. A `hash` must be the 16-hex
40
- * content-hash prefix; the string fields must be strings; `bytes` must be finite; `width`/`height`
41
- * must each be a number or null; `createdAt` must be a string. */
45
+ * content-hash prefix.
46
+ */
42
47
  function validateMediaEntry(value: unknown): MediaEntry | undefined {
43
48
  if (!value || typeof value !== 'object') return undefined;
44
49
  const e = value as Record<string, unknown>;
@@ -67,12 +72,14 @@ function validateMediaEntry(value: unknown): MediaEntry | undefined {
67
72
  };
68
73
  }
69
74
 
70
- /** Parse the posted `media` field into a validated list of MediaEntry rows. The field arrives as a
75
+ /**
76
+ * Parse the posted `media` field into a validated list of MediaEntry rows. The field arrives as a
71
77
  * JSON string (the usual form-post shape), an already-parsed array, or junk. A string is JSON-parsed
72
78
  * inside a try/catch that yields `[]` on a parse failure; a non-string array is taken directly;
73
79
  * anything else yields `[]`. Each element is validated and a failing element is dropped, so a partly
74
80
  * malformed post still lands its good rows. This is the trust boundary for the client's optimistic
75
- * records. */
81
+ * records.
82
+ */
76
83
  export function parseMediaEntries(value: unknown): MediaEntry[] {
77
84
  let raw: unknown = value;
78
85
  if (typeof value === 'string') {
@@ -91,28 +98,36 @@ export function parseMediaEntries(value: unknown): MediaEntry[] {
91
98
  return entries;
92
99
  }
93
100
 
94
- /** The dedup lookup: the entry stored under the content-hash prefix, or undefined when no bytes with
95
- * that hash are stored yet. */
101
+ /**
102
+ * The dedup lookup: the entry stored under the content-hash prefix, or undefined when no bytes with
103
+ * that hash are stored yet.
104
+ */
96
105
  export function findByHash(manifest: MediaManifest, hash: string): MediaEntry | undefined {
97
106
  return manifest[hash];
98
107
  }
99
108
 
100
- /** Set the entry under its own hash, replacing any same-hash row. Returns a new manifest and leaves
101
- * the input untouched, so a caller's prior manifest reference stays valid. The ingest path's patch. */
109
+ /**
110
+ * Set the entry under its own hash, replacing any same-hash row. Returns a new manifest and leaves
111
+ * the input untouched, so a caller's prior manifest reference stays valid. The ingest path's patch.
112
+ */
102
113
  export function upsertMediaEntry(manifest: MediaManifest, entry: MediaEntry): MediaManifest {
103
114
  return { ...manifest, [entry.hash]: entry };
104
115
  }
105
116
 
106
- /** Drop the entry under the given hash, returning a new manifest and leaving the input untouched.
117
+ /**
118
+ * Drop the entry under the given hash, returning a new manifest and leaving the input untouched.
107
119
  * Removing an absent hash is a no-op that still returns an equivalent new manifest. The safe-delete
108
- * path's patch. */
120
+ * path's patch.
121
+ */
109
122
  export function removeMediaEntry(manifest: MediaManifest, hash: string): MediaManifest {
110
123
  const { [hash]: _removed, ...rest } = manifest;
111
124
  return rest;
112
125
  }
113
126
 
114
- /** Serialize canonically: the top-level hash keys sorted ascending, two-space pretty, and a trailing
115
- * newline, so the committed file diffs cleanly in a PR and a re-serialization is byte-identical. */
127
+ /**
128
+ * Serialize canonically: the top-level hash keys sorted ascending, two-space pretty, and a trailing
129
+ * newline, so the committed file diffs cleanly in a PR and a re-serialization is byte-identical.
130
+ */
116
131
  export function serializeMediaManifest(manifest: MediaManifest): string {
117
132
  const sorted: MediaManifest = {};
118
133
  for (const hash of Object.keys(manifest).sort()) {
@@ -7,13 +7,17 @@
7
7
  // slugifyFilename output always satisfies parseMediaToken's grammar (lowercase alphanumerics joined
8
8
  // by single internal hyphens, no leading or trailing hyphen), or is the literal `file`.
9
9
 
10
- /** Combining marks (Unicode block U+0300 to U+036F), left over after an NFD decompose, stripped to
10
+ /**
11
+ * Combining marks (Unicode block U+0300 to U+036F), left over after an NFD decompose, stripped to
11
12
  * fold an accented letter down to its ASCII base. Written as escapes because the literal marks are
12
- * invisible in source. */
13
+ * invisible in source.
14
+ */
13
15
  const COMBINING_MARKS = /[\u0300-\u036f]/g;
14
16
 
15
- /** Windows reserved device names. A bare match (case-insensitive) cannot survive as the slug, since
16
- * it names a device rather than a file on that platform. */
17
+ /**
18
+ * Windows reserved device names. A bare match (case-insensitive) cannot survive as the slug, since
19
+ * it names a device rather than a file on that platform.
20
+ */
17
21
  const RESERVED = new Set([
18
22
  'con',
19
23
  'prn',
@@ -42,8 +46,10 @@ const RESERVED = new Set([
42
46
  /** The maximum slug length, applied before the reserved-name and empty fallbacks. */
43
47
  const MAX_SLUG = 80;
44
48
 
45
- /** A 16-character lowercase hex content-hash prefix, the bare-hash reference form. A slug that
46
- * matches this shape would collide with `media:<hash>`, so slugifyFilename screens it. */
49
+ /**
50
+ * A 16-character lowercase hex content-hash prefix, the bare-hash reference form. A slug that
51
+ * matches this shape would collide with `media:<hash>`, so slugifyFilename screens it.
52
+ */
47
53
  const HASH_RE = /^[0-9a-f]{16}$/;
48
54
 
49
55
  /** A short alphanumeric extension (no dot), the only shape r2Key accepts, for example `webp`. */
@@ -69,10 +75,12 @@ export function shortHash(full: string): string {
69
75
  return full.slice(0, 16);
70
76
  }
71
77
 
72
- /** The strict ingest transform from a raw filename to a slug that satisfies the media: slug grammar,
78
+ /**
79
+ * The strict ingest transform from a raw filename to a slug that satisfies the media: slug grammar,
73
80
  * or the literal `file`. Drops the extension, lowercases, transliterates accents, collapses non-alphanumeric runs
74
81
  * to a single hyphen, trims, caps at 80 chars, screens Windows reserved names, and falls back to
75
- * `file` when nothing usable is left. */
82
+ * `file` when nothing usable is left.
83
+ */
76
84
  export function slugifyFilename(name: string): string {
77
85
  const dot = name.lastIndexOf('.');
78
86
  const stem = dot === -1 ? name : name.slice(0, dot);
@@ -97,9 +105,11 @@ export function slugifyFilename(name: string): string {
97
105
  return slug;
98
106
  }
99
107
 
100
- /** The content-addressed R2 object key `media/<aa>/<shortHash>.<ext>`, fanned out on the first two
108
+ /**
109
+ * The content-addressed R2 object key `media/<aa>/<shortHash>.<ext>`, fanned out on the first two
101
110
  * hex chars of the short hash. No leading slash: this is an object key, not a URL. `ext` is bare
102
- * (no dot), for example `webp`. */
111
+ * (no dot), for example `webp`.
112
+ */
103
113
  export function r2Key(shortHash: string, ext: string): string {
104
114
  if (!HASH_RE.test(shortHash)) {
105
115
  throw new Error(`r2Key: hash must be 16 lowercase hex chars, got "${shortHash}"`);
@@ -110,10 +120,12 @@ export function r2Key(shortHash: string, ext: string): string {
110
120
  return `media/${shortHash.slice(0, 2)}/${shortHash}.${ext}`;
111
121
  }
112
122
 
113
- /** The public delivery URL path, with a leading slash, under the delivery base (`publicBase`,
123
+ /**
124
+ * The public delivery URL path, with a leading slash, under the delivery base (`publicBase`,
114
125
  * default `/media`). The `slug` form is human-readable (`<base>/<slug>.<shortHash>.<ext>`, or
115
126
  * `<base>/<shortHash>.<ext>` when the slug is null); the `opaque` form mirrors the R2 fan-out
116
- * (`<base>/<aa>/<shortHash>.<ext>`) and ignores the slug. */
127
+ * (`<base>/<aa>/<shortHash>.<ext>`) and ignores the slug.
128
+ */
117
129
  export function publicPath(
118
130
  slug: string | null,
119
131
  shortHash: string,
@@ -23,8 +23,10 @@ export interface OrphanByteRow {
23
23
  hash: string;
24
24
  }
25
25
 
26
- /** A broken reference: a manifest row whose bytes are gone. Read-only, since purging it would drop a
27
- * still-referenced asset's record; the screen shows where it is used so an operator can re-ingest. */
26
+ /**
27
+ * A broken reference: a manifest row whose bytes are gone. Read-only, since purging it would drop a
28
+ * still-referenced asset's record; the screen shows where it is used so an operator can re-ingest.
29
+ */
28
30
  export interface BrokenRefRow {
29
31
  /** The 16-hex content hash of the manifest row whose bytes are missing. */
30
32
  hash: string;
@@ -8,12 +8,16 @@
8
8
  import type { MediaManifest } from './manifest.js';
9
9
  import { log } from '../log/index.js';
10
10
 
11
- /** A stored media object key parses to its short hash via `media/<aa>/<shortHash>.<ext>`. Exported so
12
- * the orphan-scan projection derives the same hash from an orphaned key without a second grammar. */
11
+ /**
12
+ * A stored media object key parses to its short hash via `media/<aa>/<shortHash>.<ext>`. Exported so
13
+ * the orphan-scan projection derives the same hash from an orphaned key without a second grammar.
14
+ */
13
15
  export const MEDIA_KEY_RE = /^media\/[0-9a-f]{2}\/([0-9a-f]{16})\.[a-z0-9]{1,5}$/;
14
16
 
15
- /** What a reconcile read found in either direction. `orphanedObjects` are stored R2 keys whose hash
16
- * has no manifest row; `missingObjects` are manifest hashes with no stored object. */
17
+ /**
18
+ * What a reconcile read found in either direction. `orphanedObjects` are stored R2 keys whose hash
19
+ * has no manifest row; `missingObjects` are manifest hashes with no stored object.
20
+ */
17
21
  export interface ReconcileResult {
18
22
  /** Stored keys (full R2 keys) whose content hash is absent from the manifest. */
19
23
  orphanedObjects: string[];
@@ -21,9 +25,11 @@ export interface ReconcileResult {
21
25
  missingObjects: string[];
22
26
  }
23
27
 
24
- /** The pure core: compare the stored R2 keys against the manifest's content-hash keys and report
28
+ /**
29
+ * The pure core: compare the stored R2 keys against the manifest's content-hash keys and report
25
30
  * both orphan directions. A stored key that does not match the media-key grammar is ignored, since
26
- * it is not a content-addressed media object this reconcile owns. */
31
+ * it is not a content-addressed media object this reconcile owns.
32
+ */
27
33
  export function reconcileMedia(storedKeys: string[], manifest: MediaManifest): ReconcileResult {
28
34
  const manifestHashes = new Set(Object.keys(manifest));
29
35
  const storedHashes = new Set<string>();
@@ -48,17 +54,21 @@ interface ReconcileListPage {
48
54
  cursor?: string;
49
55
  }
50
56
 
51
- /** The R2 bucket surface the reconcile read needs: a single prefixed, paginated list. A local
52
- * structural interface so no @cloudflare/workers-types name is imported (the module is internal and
53
- * on no public subpath, but the narrow seam keeps the build self-contained either way). */
57
+ /**
58
+ * The R2 bucket surface the reconcile read needs: a single prefixed, paginated list. A local
59
+ * structural interface so no `@cloudflare/workers-types` name is imported (the module is internal and
60
+ * on no public subpath, but the narrow seam keeps the build self-contained either way).
61
+ */
54
62
  export interface ReconcileBucket {
55
63
  list(opts?: { prefix?: string; cursor?: string }): Promise<ReconcileListPage>;
56
64
  }
57
65
 
58
- /** The glue runner: list every stored key under the media/ prefix (paginating through R2's
66
+ /**
67
+ * The glue runner: list every stored key under the media/ prefix (paginating through R2's
59
68
  * cursor/truncated), reconcile against the manifest, log the count summary, and return the result.
60
69
  * The log record carries counts only, never bytes or a key list; the keys are content hashes and so
61
- * carry no PII, but the count summary is all an operator needs to size the orphan state. */
70
+ * carry no PII, but the count summary is all an operator needs to size the orphan state.
71
+ */
62
72
  export async function runReconcile(
63
73
  bucket: ReconcileBucket,
64
74
  manifest: MediaManifest,
@@ -14,14 +14,18 @@ export interface MediaRef {
14
14
  /** A 16-character lowercase hex content-hash prefix. */
15
15
  const HASH_RE = /^[0-9a-f]{16}$/;
16
16
 
17
- /** The slug grammar from the Task 2 slugify transform: lowercase alphanumerics joined by single
17
+ /**
18
+ * The slug grammar from the Task 2 slugify transform: lowercase alphanumerics joined by single
18
19
  * internal hyphens, with no leading or trailing hyphen and no dot (the dot is the slug/hash
19
- * separator). */
20
+ * separator).
21
+ */
20
22
  const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
21
23
 
22
- /** Parse a `media:<slug>.<hash>` href (or the bare `media:<hash>` form), or null for any other
24
+ /**
25
+ * Parse a `media:<slug>.<hash>` href (or the bare `media:<hash>` form), or null for any other
23
26
  * href or a malformed token. Splits on the last dot, so a slug that illegally contains a dot fails
24
- * the slug grammar and returns null. */
27
+ * the slug grammar and returns null.
28
+ */
25
29
  export function parseMediaToken(href: string): MediaRef | null {
26
30
  if (!href.startsWith('media:')) return null;
27
31
  const rest = href.slice('media:'.length);
@@ -33,8 +37,10 @@ export function parseMediaToken(href: string): MediaRef | null {
33
37
  return { slug, hash };
34
38
  }
35
39
 
36
- /** Write the canonical media: token for a ref. The inverse of parseMediaToken, so a parse then
37
- * write round trip is stable: `media:<slug>.<hash>` when the slug is present, else `media:<hash>`. */
40
+ /**
41
+ * Write the canonical media: token for a ref. The inverse of parseMediaToken, so a parse then
42
+ * write round trip is stable: `media:<slug>.<hash>` when the slug is present, else `media:<hash>`.
43
+ */
38
44
  export function mediaToken(ref: MediaRef): string {
39
45
  return ref.slug === null ? `media:${ref.hash}` : `media:${ref.slug}.${ref.hash}`;
40
46
  }
@@ -23,9 +23,11 @@ import { filenameFromId } from '../content/ids.js';
23
23
  import { readRaw } from '../github/repo.js';
24
24
  import { buildUsageIndex } from './usage.js';
25
25
 
26
- /** One main entry the rewrite will touch: its identity, its file path, the transform's per-placement
26
+ /**
27
+ * One main entry the rewrite will touch: its identity, its file path, the transform's per-placement
27
28
  * diff, and the rewritten markdown a later apply commits. `P` is the transform's placement type
28
- * (a RepointPlacement for replace, an AltPlacement for fill-alt). */
29
+ * (a RepointPlacement for replace, an AltPlacement for fill-alt).
30
+ */
29
31
  export interface PlannedEntry<P = unknown> {
30
32
  /** The concept id, e.g. "posts". */
31
33
  concept: string;
@@ -39,9 +41,11 @@ export interface PlannedEntry<P = unknown> {
39
41
  newMarkdown: string;
40
42
  }
41
43
 
42
- /** One open edit branch that also references the asset, with the entries on it. Report-only: an apply
44
+ /**
45
+ * One open edit branch that also references the asset, with the entries on it. Report-only: an apply
43
46
  * rewrites main, never a branch, so the screen surfaces these as a delta the editor handles by
44
- * republishing the draft. */
47
+ * republishing the draft.
48
+ */
45
49
  export interface BranchRef {
46
50
  /** The cairn/* branch name. */
47
51
  branch: string;
@@ -49,8 +53,10 @@ export interface BranchRef {
49
53
  entries: { concept: string; id: string }[];
50
54
  }
51
55
 
52
- /** The preview plan: the main entries to rewrite, the report-only branch delta, and the distinct
53
- * count of affected main entries (the entries the transform actually changed). */
56
+ /**
57
+ * The preview plan: the main entries to rewrite, the report-only branch delta, and the distinct
58
+ * count of affected main entries (the entries the transform actually changed).
59
+ */
54
60
  export interface RewritePlan<P = unknown> {
55
61
  entries: PlannedEntry<P>[];
56
62
  branchDelta: BranchRef[];
@@ -8,16 +8,22 @@
8
8
  // nosniff, Content-Disposition: inline, a restrictive Content-Security-Policy) are the real XSS control
9
9
  // for the served bytes; sniffing here is the ingest gate, not the served-bytes defense.
10
10
 
11
- /** The leading ASCII whitespace bytes skipped before the deny-list's first-byte-is-`<` check:
12
- * tab (0x09), newline (0x0A), carriage return (0x0D), and space (0x20). */
11
+ /**
12
+ * The leading ASCII whitespace bytes skipped before the deny-list's first-byte-is-`<` check:
13
+ * tab (0x09), newline (0x0A), carriage return (0x0D), and space (0x20).
14
+ */
13
15
  const WHITESPACE = new Set([0x09, 0x0a, 0x0d, 0x20]);
14
16
 
15
- /** The single byte `<` (0x3C). A payload whose first non-whitespace byte is `<` is markup (SVG, HTML,
16
- * XML) and is denied regardless of its declared type or any site `allowedTypes`. */
17
+ /**
18
+ * The single byte `<` (0x3C). A payload whose first non-whitespace byte is `<` is markup (SVG, HTML,
19
+ * XML) and is denied regardless of its declared type or any site `allowedTypes`.
20
+ */
17
21
  const LT = 0x3c;
18
22
 
19
- /** Declared content types denied at the engine level, independent of any site `allowedTypes`. SVG and
20
- * the markup types carry active content (script, foreignObject), so they never ingest as media. */
23
+ /**
24
+ * Declared content types denied at the engine level, independent of any site `allowedTypes`. SVG and
25
+ * the markup types carry active content (script, foreignObject), so they never ingest as media.
26
+ */
21
27
  const DENIED_TYPES = new Set(['image/svg+xml', 'image/svg', 'text/html', 'application/xml']);
22
28
 
23
29
  /** The ISO-BMFF major-brand codes (at bytes 8..11 of an `ftyp` box) that mean an AVIF image. */
@@ -26,8 +32,10 @@ const AVIF_BRANDS = new Set(['avif', 'avis']);
26
32
  /** The ISO-BMFF major-brand codes that mean a HEIF/HEIC image. */
27
33
  const HEIC_BRANDS = new Set(['heic', 'heix', 'heif', 'hevc', 'hevx', 'mif1', 'msf1']);
28
34
 
29
- /** True when every byte of `magic` matches `bytes` starting at `offset`. False if `bytes` is too
30
- * short to hold the whole magic. */
35
+ /**
36
+ * True when every byte of `magic` matches `bytes` starting at `offset`. False if `bytes` is too
37
+ * short to hold the whole magic.
38
+ */
31
39
  function matches(bytes: Uint8Array, offset: number, magic: number[]): boolean {
32
40
  if (bytes.length < offset + magic.length) return false;
33
41
  for (let i = 0; i < magic.length; i++) {
@@ -36,8 +44,10 @@ function matches(bytes: Uint8Array, offset: number, magic: number[]): boolean {
36
44
  return true;
37
45
  }
38
46
 
39
- /** The four ASCII characters at bytes `offset..offset+3`, or null when the input is too short. Used to
40
- * read an ISO-BMFF brand code as a string. */
47
+ /**
48
+ * The four ASCII characters at bytes `offset..offset+3`, or null when the input is too short. Used to
49
+ * read an ISO-BMFF brand code as a string.
50
+ */
41
51
  function ascii4(bytes: Uint8Array, offset: number): string | null {
42
52
  if (bytes.length < offset + 4) return null;
43
53
  return String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
@@ -77,9 +87,11 @@ export function sniffMediaType(bytes: Uint8Array): string | null {
77
87
  return null;
78
88
  }
79
89
 
80
- /** The bare file extension (no dot) for each sniffed media type the upload path stores. The ext is
90
+ /**
91
+ * The bare file extension (no dot) for each sniffed media type the upload path stores. The ext is
81
92
  * derived from the server-sniffed type, never the client filename, so the stored key and the
82
- * delivery extension allow-list always agree. An unmappable type returns null (the upload 415s). */
93
+ * delivery extension allow-list always agree. An unmappable type returns null (the upload 415s).
94
+ */
83
95
  const EXT_BY_TYPE: Record<string, string> = {
84
96
  'image/jpeg': 'jpg',
85
97
  'image/png': 'png',
@@ -88,8 +100,10 @@ const EXT_BY_TYPE: Record<string, string> = {
88
100
  'image/avif': 'avif',
89
101
  };
90
102
 
91
- /** The storage extension for a sniffed media type, or null for a type the upload path does not store
92
- * (HEIC, an unknown type). Driven by the sniffed type, so the key's ext is server-owned. */
103
+ /**
104
+ * The storage extension for a sniffed media type, or null for a type the upload path does not store
105
+ * (HEIC, an unknown type). Driven by the sniffed type, so the key's ext is server-owned.
106
+ */
93
107
  export function extForMediaType(type: string): string | null {
94
108
  return EXT_BY_TYPE[type] ?? null;
95
109
  }