@murumets-ee/media 0.64.0 → 0.65.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.
@@ -157,6 +157,53 @@ declare function memoiseWithTtl<T>(load: () => Promise<T>, ttlMs: number, now?:
157
157
  declare function createPublicMediaResolver(): (ids: readonly string[], opts?: {
158
158
  locale?: string | undefined;
159
159
  }) => Promise<Map<string, PublicMediaProjection>>;
160
+ /**
161
+ * One image the CANVAS can draw for a media id — the shape `@murumets-ee/blocks`'
162
+ * `CanvasImageResolver` asks for, declared structurally so this package does not
163
+ * depend on blocks.
164
+ */
165
+ interface CanvasImage {
166
+ readonly url: string;
167
+ readonly alt: string | null;
168
+ readonly width: number | null;
169
+ readonly height: number | null;
170
+ }
171
+ /**
172
+ * Media ids → images the block-editor CANVAS may draw: a BOUND `field.media()`
173
+ * column (frontend-delivery F112) and a collection card's picture.
174
+ *
175
+ * The ready-made `blocksPlugin({ resolveImages })` value, so an app wires one lazy
176
+ * line instead of copying a resolver (frontend-delivery D025 — no builder's
177
+ * manual):
178
+ *
179
+ * ```ts
180
+ * blocksPlugin({
181
+ * resolveImages: async (ids, opts) =>
182
+ * (await import('@murumets-ee/media/public')).resolveCanvasImages(ids, opts),
183
+ * })
184
+ * ```
185
+ *
186
+ * Two properties are the point, not conveniences:
187
+ *
188
+ * - **The SAME public resolver content-api uses**, so the canvas cannot show an
189
+ * image the published site would not (a private asset draws as empty on both).
190
+ * - **Images only.** A `field.media({ kind: 'image' })` constrains only the
191
+ * picker's list (editor F116), so a video or PDF can sit in the column; drawn
192
+ * through an `<img>` it would be a broken image. Anything that is not
193
+ * `image/*` is left out, and the canvas draws its empty placeholder instead.
194
+ *
195
+ * The resolver is built once per process — it memoises the image-style set, which
196
+ * a fresh one per call would re-read.
197
+ */
198
+ declare function resolveCanvasImages(ids: readonly string[], opts: {
199
+ locale: string;
200
+ }): Promise<Map<string, CanvasImage>>;
201
+ /**
202
+ * The pure half of {@link resolveCanvasImages}: keep images, drop everything else,
203
+ * project to what the canvas draws. Exported so the filter has a test that does
204
+ * not need a database.
205
+ */
206
+ declare function toCanvasImages(found: ReadonlyMap<string, PublicMediaProjection>): Map<string, CanvasImage>;
160
207
  //#endregion
161
- export { PublicMediaProjection, PublicMediaResolverDeps, PublicMediaRow, cacheOnceUnlessRejected, createPublicMediaResolver, memoiseWithTtl, resolvePublicMedia };
208
+ export { CanvasImage, PublicMediaProjection, PublicMediaResolverDeps, PublicMediaRow, cacheOnceUnlessRejected, createPublicMediaResolver, memoiseWithTtl, resolveCanvasImages, resolvePublicMedia, toCanvasImages };
162
209
  //# sourceMappingURL=public-resolver.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public-resolver.d.mts","names":[],"sources":["../src/async-cache.ts","../src/public-resolver.ts"],"mappings":";;;;;;AA2BA;;;;;;;;;;;;;;;;AAAmF;;;;AC0BnF;;;iBD1BgB,uBAAA,GAAA,CAA2B,IAAA,QAAY,OAAA,CAAQ,CAAA,UAAW,OAAA,CAAQ,CAAA;;;;;ACoDhE;AAIlB;;;;;;;;;;;;UA9BiB,qBAAA;EACf,EAAA;EACA,GAAA;EACA,QAAA;EACA,KAAA;EACA,MAAA;EACA,GAAA;EACA,KAAA;EAmD0C;;;;;;;;;;;;;;;;;;EAhC1C,QAAA,EAAU,MAAM;AAAA;;UAID,cAAA;EACf,EAAA;EACA,OAAA;EACA,QAAA;EACA,KAAA;EACA,MAAA;EACA,GAAA;EACA,KAAA;AAAA;AAAA,UAGe,uBAAA;EA0CP;;;;;;;;;EAhCR,SAAA,GAAY,GAAA,qBAAwB,MAAA,cAAoB,OAAA,CAAQ,cAAA;EAgCtB;AAAA;AA2F5C;;;EArHE,aAAA,GAAgB,IAAA,wBAA4B,OAAA,CAAQ,GAAA;EAsHxC;EApHZ,WAAA,QAAmB,OAAA,CAAQ,MAAA,SAAe,UAAA;EAuHnC;EArHP,UAAA,GAAa,OAAA,UAAiB,SAAA,UAAmB,MAAA;AAAA;;;;;;;;;;AAqHjC;AAelB;;;;;iBAlHsB,kBAAA,CACpB,GAAA,qBACA,IAAA,EAAM,uBAAA,EACN,IAAA;EAAQ,MAAA;AAAA,IACP,OAAA,CAAQ,GAAA,SAAY,qBAAA;;;;;;;;AAiHuB;;;;;;;;;;;;;;;;;;;;;;;iBAtB9B,cAAA,GAAA,CACd,IAAA,QAAY,OAAA,CAAQ,CAAA,GACpB,KAAA,UACA,GAAA,wBACO,OAAA,CAAQ,CAAA;AAAA,iBAeD,yBAAA,CAAA,IACd,GAAA,qBACA,IAAA;EAAS,MAAA;AAAA,MACN,OAAA,CAAQ,GAAA,SAAY,qBAAA"}
1
+ {"version":3,"file":"public-resolver.d.mts","names":[],"sources":["../src/async-cache.ts","../src/public-resolver.ts"],"mappings":";;;;;;AA2BA;;;;;;;;;;;;;;;;AAAmF;;;;AC4BnF;;;iBD5BgB,uBAAA,GAAA,CAA2B,IAAA,QAAY,OAAA,CAAQ,CAAA,UAAW,OAAA,CAAQ,CAAA;;;;;ACsDhE;AAIlB;;;;;;;;;;;;UA9BiB,qBAAA;EACf,EAAA;EACA,GAAA;EACA,QAAA;EACA,KAAA;EACA,MAAA;EACA,GAAA;EACA,KAAA;EAmD0C;;;;;;;;;;;;;;;;;;EAhC1C,QAAA,EAAU,MAAM;AAAA;;UAID,cAAA;EACf,EAAA;EACA,OAAA;EACA,QAAA;EACA,KAAA;EACA,MAAA;EACA,GAAA;EACA,KAAA;AAAA;AAAA,UAGe,uBAAA;EA0CP;;;;;;;;;EAhCR,SAAA,GAAY,GAAA,qBAAwB,MAAA,cAAoB,OAAA,CAAQ,cAAA;EAgCtB;AAAA;AA2F5C;;;EArHE,aAAA,GAAgB,IAAA,wBAA4B,OAAA,CAAQ,GAAA;EAsHxC;EApHZ,WAAA,QAAmB,OAAA,CAAQ,MAAA,SAAe,UAAA;EAuHnC;EArHP,UAAA,GAAa,OAAA,UAAiB,SAAA,UAAmB,MAAA;AAAA;;;;;;;;;;AAqHjC;AAelB;;;;;iBAlHsB,kBAAA,CACpB,GAAA,qBACA,IAAA,EAAM,uBAAA,EACN,IAAA;EAAQ,MAAA;AAAA,IACP,OAAA,CAAQ,GAAA,SAAY,qBAAA;;;;;;;;AAiHuB;AAuE9C;;;;;;;;;AAIiB;AAgCjB;;;;;;;;;;;;iBAjIgB,cAAA,GAAA,CACd,IAAA,QAAY,OAAA,CAAQ,CAAA,GACpB,KAAA,UACA,GAAA,wBACO,OAAA,CAAQ,CAAA;AAAA,iBAeD,yBAAA,CAAA,IACd,GAAA,qBACA,IAAA;EAAS,MAAA;AAAA,MACN,OAAA,CAAQ,GAAA,SAAY,qBAAA;AAwHzB;;;;;AAAA,UAjDiB,WAAA;EAAA,SACN,GAAA;EAAA,SACA,GAAA;EAAA,SACA,KAAA;EAAA,SACA,MAAA;AAAA;;;;;AA+Ce;;;;;;;;;;;;;;;;;;;;;;;iBAfJ,mBAAA,CACpB,GAAA,qBACA,IAAA;EAAQ,MAAA;AAAA,IACP,OAAA,CAAQ,GAAA,SAAY,WAAA;;;;;;iBAUP,cAAA,CACd,KAAA,EAAO,WAAA,SAAoB,qBAAA,IAC1B,GAAA,SAAY,WAAA"}
@@ -1,2 +1,2 @@
1
- import{t as e}from"./async-cache-C_Ycvs7b.mjs";async function t(e,t,n={}){let r=new Map,i=[...new Set(e)];if(i.length===0)return r;let a=await t.readMedia(i,n.locale);if(a.length===0)return r;let o=await t.imageStyles(),s=Object.entries(o),c=[],l=new Map;for(let e of a){if(typeof e.fileKey!=`string`||e.fileKey===``)continue;let n=s.map(([n,r])=>[n,t.variantKey(e.fileKey,n,r.format??`webp`)]);l.set(e.id,{fileKey:e.fileKey,variantKeys:n}),c.push(e.fileKey,...n.map(([,e])=>e))}let u=await t.getPublicUrls(c);for(let e of a){let t=l.get(e.id);if(!t)continue;let n=u.get(t.fileKey);if(n===void 0)continue;let i={};for(let[e,n]of t.variantKeys){let t=u.get(n);t!==void 0&&(i[e]=t)}r.set(e.id,{id:e.id,url:n,mimeType:e.mimeType??null,width:e.width??null,height:e.height??null,alt:e.alt??null,title:e.title??null,variants:i})}return r}function n(e,t,n=Date.now){let r=null;return async()=>{let i=n();if(r&&i-r.at<t)return r.value;let a=e();return r={value:a,at:i},a.catch(()=>{r?.value===a&&(r=null)}),a}}function r(){let e=n(async()=>{let{getApp:e}=await import(`@murumets-ee/core`),{resolveImageStyles:t}=await import(`./resolve-image-styles-iN9JbZYf.mjs`);return t(e())},3e4);return async(n,r={})=>{let[{createMediaQueryClient:i},{getSharedStorageClient:a},{deriveVariantKey:o}]=await Promise.all([import(`./query-client.mjs`),import(`./client.mjs`),import(`./variant-key-JBTJXPL1.mjs`).then(e=>e.n)]),[s,c]=await Promise.all([i(),a()]);return t(n,{readMedia:async(e,t)=>{let{schemaRegistry:n}=await import(`@murumets-ee/db`),{getTableColumns:r,inArray:i}=await import(`drizzle-orm`),a=n.get(`media`);if(!a)return[];let o=r(a).id;return o?await s.findMany({where:i(o,[...e]),limit:e.length,...t!==void 0&&{locale:t}}):[]},getPublicUrls:e=>c.getPublicUrls(e),imageStyles:e,variantKey:o},r)}}export{e as cacheOnceUnlessRejected,r as createPublicMediaResolver,n as memoiseWithTtl,t as resolvePublicMedia};
1
+ import{t as e}from"./async-cache-C_Ycvs7b.mjs";async function t(e,t,n={}){let r=new Map,i=[...new Set(e)];if(i.length===0)return r;let a=await t.readMedia(i,n.locale);if(a.length===0)return r;let o=await t.imageStyles(),s=Object.entries(o),c=[],l=new Map;for(let e of a){if(typeof e.fileKey!=`string`||e.fileKey===``)continue;let n=s.map(([n,r])=>[n,t.variantKey(e.fileKey,n,r.format??`webp`)]);l.set(e.id,{fileKey:e.fileKey,variantKeys:n}),c.push(e.fileKey,...n.map(([,e])=>e))}let u=await t.getPublicUrls(c);for(let e of a){let t=l.get(e.id);if(!t)continue;let n=u.get(t.fileKey);if(n===void 0)continue;let i={};for(let[e,n]of t.variantKeys){let t=u.get(n);t!==void 0&&(i[e]=t)}r.set(e.id,{id:e.id,url:n,mimeType:e.mimeType??null,width:e.width??null,height:e.height??null,alt:e.alt??null,title:e.title??null,variants:i})}return r}function n(e,t,n=Date.now){let r=null;return async()=>{let i=n();if(r&&i-r.at<t)return r.value;let a=e();return r={value:a,at:i},a.catch(()=>{r?.value===a&&(r=null)}),a}}function r(){let e=n(async()=>{let{getApp:e}=await import(`@murumets-ee/core`),{resolveImageStyles:t}=await import(`./resolve-image-styles-iN9JbZYf.mjs`);return t(e())},3e4);return async(n,r={})=>{let[{createMediaQueryClient:i},{getSharedStorageClient:a},{deriveVariantKey:o}]=await Promise.all([import(`./query-client.mjs`),import(`./client.mjs`),import(`./variant-key-JBTJXPL1.mjs`).then(e=>e.n)]),[s,c]=await Promise.all([i(),a()]);return t(n,{readMedia:async(e,t)=>{let{schemaRegistry:n}=await import(`@murumets-ee/db`),{getTableColumns:r,inArray:i}=await import(`drizzle-orm`),a=n.get(`media`);if(!a)return[];let o=r(a).id;return o?await s.findMany({where:i(o,[...e]),limit:e.length,...t!==void 0&&{locale:t}}):[]},getPublicUrls:e=>c.getPublicUrls(e),imageStyles:e,variantKey:o},r)}}let i;async function a(e,t){return i??=r(),o(await i(e,t))}function o(e){let t=new Map;for(let[n,r]of e)r.mimeType?.startsWith(`image/`)===!0&&t.set(n,{url:r.url,alt:r.alt,width:r.width,height:r.height});return t}export{e as cacheOnceUnlessRejected,r as createPublicMediaResolver,n as memoiseWithTtl,a as resolveCanvasImages,t as resolvePublicMedia,o as toCanvasImages};
2
2
  //# sourceMappingURL=public-resolver.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"public-resolver.mjs","names":[],"sources":["../src/public-resolver.ts"],"sourcesContent":["/**\n * The PUBLIC media resolver — media as data, for a consumer that builds its own\n * markup (plan/api phase 06, D021).\n *\n * **Why not `resolveMediaRefs`.** That helper parses `[media:type:id:variant]`\n * tags out of a text string and substitutes rendered HTML (`<img …>`, `<a\n * href=…>`) through per-type renderers. Right for a server-rendered page, wrong\n * for an HTTP API: a frontend needs `{url, width, height, alt}` so it can emit\n * its own `<picture>` with its own `srcset`, not a pre-baked tag it has to parse\n * back apart. Nothing here deprecates `resolveMediaRefs`; the API simply uses a\n * different, data-shaped path.\n *\n * **Why the entity cannot just be whitelisted.** `Media` is a real entity, but\n * it has no `url` field and cannot have one: what is stored is `fileKey`, and\n * the URL is DERIVED by the storage layer, which also decides whether it is a\n * direct public link or a time-limited signed one. Serving `media` as an\n * ordinary whitelisted entity would hand a consumer a storage key — useless for\n * rendering, and a gratuitous disclosure of storage layout.\n *\n * **This resolver can never emit a signed URL, structurally.** It asks storage\n * for PUBLIC urls (`getPublicUrls`) rather than asking for \"a url\" and\n * inspecting what came back. The second shape produces a signed URL on the\n * happy path and relies on a later check to suppress it — and a signed URL is a\n * bearer capability that outlives the permission check that minted it, on a\n * response the public role is deliberately allowed to cache. There is no branch\n * here to forget.\n *\n * D021 permits a permission-gated scoped URL for NON-public principals on\n * responses already forced to `no-store`. It is deliberately not built: no\n * consumer needs it, and an unexercised signed-URL branch is a check that\n * cannot fail. The shape is fixed so nobody re-derives it; the path waits for a\n * named consumer.\n */\n\nimport type { PgColumn } from 'drizzle-orm/pg-core'\nexport { cacheOnceUnlessRejected } from './async-cache.js'\nimport type { ImageStyle } from './types.js'\n\n/**\n * One media item as it reaches an HTTP consumer.\n *\n * A STRUCTURAL match for `@murumets-ee/content-api`'s `MediaProjection`, not an\n * import of it: `content-api` is a leaf that this package's consumers wire into\n * by hand, exactly like `ContentApiResolution` mirrors content's\n * `ResolveResult`. The app that wires the two is where TypeScript checks they\n * agree.\n *\n * `width`/`height` ride along because a frontend needs them to reserve layout\n * space before the image loads; omitting them forces either a layout shift or a\n * second request, and the second request is the 4+N failure D001 exists to\n * prevent. `alt`/`title` are translatable on the Media entity, so this\n * projection is locale-dependent.\n */\nexport interface PublicMediaProjection {\n id: string\n url: string\n mimeType: string | null\n width: number | null\n height: number | null\n alt: string | null\n title: string | null\n /**\n * Variant URLs by image-style name, INLINED as data rather than handed over\n * as a URL pattern to interpolate.\n *\n * Two reasons, and the second is a security property. D008's rule is that a\n * client derives nothing — a consumer that constructs URLs from a pattern\n * encodes an assumption the server never published, and nothing can detect\n * when it goes stale. And a URL pattern is necessarily public-SHAPED: the\n * moment a consumer can build its own URL, the server has lost the ability to\n * decide what that consumer may address, and the \"never a signed URL\"\n * guarantee becomes unenforceable because the consumer stopped asking.\n *\n * The payload multiplier is bounded by CONFIGURATION, not by content: the set\n * of image styles is operator-chosen and neither the caller nor the editor can\n * grow it. So this needs no `?populate=`-style opt-in — unlike reference\n * expansion, where the caller could otherwise drag arbitrarily large targets\n * in. A style whose variant file is missing or not public is simply absent.\n */\n variants: Record<string, string>\n}\n\n/** The Media rows this resolver needs. Structural, so any read path can supply them. */\nexport interface PublicMediaRow {\n id: string\n fileKey: string\n mimeType?: string | null\n width?: number | null\n height?: number | null\n alt?: string | null\n title?: string | null\n}\n\nexport interface PublicMediaResolverDeps {\n /**\n * Read media rows by id through a PUBLISHED-ONLY path.\n *\n * **The wiring must guarantee this** — pass a `MediaQueryClient`/`QueryClient`\n * read, never an `AdminClient` one. This module cannot enforce the choice (it\n * is injected), and an admin read would surface unpublished media on the\n * anonymous surface. Same posture, and same reason, as\n * `ContentApiResolution.resolvePath`.\n */\n readMedia: (ids: readonly string[], locale?: string) => Promise<PublicMediaRow[]>\n /**\n * Public URLs for a batch of storage keys. Anything missing or non-public is\n * ABSENT from the map — see `StorageClient.getPublicUrls`, which is the ask-\n * for-public shape this whole module rests on.\n */\n getPublicUrls: (keys: readonly string[]) => Promise<Map<string, string>>\n /** The configured image styles. Operator-owned; neither caller nor editor grows it. */\n imageStyles: () => Promise<Record<string, ImageStyle>>\n /** Derives a variant's storage key from the original's. */\n variantKey: (fileKey: string, styleName: string, format: string) => string\n}\n\n/**\n * Resolve media ids to public projections.\n *\n * Ids that do not exist, are unpublished for this caller, or whose file is not\n * public are ABSENT from the returned map — one absence, four causes, exactly\n * as phase 05's failure semantics require. The caller renders without them.\n *\n * Cost is THREE round trips at most, regardless of batch size: the image-style\n * lookup (usually served from the caller's cache — see\n * {@link createPublicMediaResolver}), one entity read, and one storage read\n * covering every original and every variant key. Emphatically NOT\n * N×(styles+1), which is what a per-item convenience method would have made it.\n * The storage read is internally chunked when the key count exceeds what one\n * query may carry, so \"one storage read\" is `ceil(keys / 500)` in the extreme.\n */\nexport async function resolvePublicMedia(\n ids: readonly string[],\n deps: PublicMediaResolverDeps,\n opts: { locale?: string | undefined } = {},\n): Promise<Map<string, PublicMediaProjection>> {\n const out = new Map<string, PublicMediaProjection>()\n const unique = [...new Set(ids)]\n if (unique.length === 0) return out\n\n const rows = await deps.readMedia(unique, opts.locale)\n if (rows.length === 0) return out\n\n const styles = await deps.imageStyles()\n const styleEntries = Object.entries(styles)\n\n // Every key we might need — originals AND variants — resolved in one storage\n // query. `getPublicUrls` returns nothing for a key that is missing or not\n // public, so a private original drops the whole item and a private variant\n // drops only that variant.\n const keysToLookUp: string[] = []\n /** Per row: its original key, plus (styleName → variant key). */\n const perRow = new Map<string, { fileKey: string; variantKeys: Array<[string, string]> }>()\n\n for (const row of rows) {\n if (typeof row.fileKey !== 'string' || row.fileKey === '') continue\n const variantKeys: Array<[string, string]> = styleEntries.map(([name, style]) => [\n name,\n deps.variantKey(row.fileKey, name, style.format ?? 'webp'),\n ])\n perRow.set(row.id, { fileKey: row.fileKey, variantKeys })\n keysToLookUp.push(row.fileKey, ...variantKeys.map(([, key]) => key))\n }\n\n const publicUrls = await deps.getPublicUrls(keysToLookUp)\n\n for (const row of rows) {\n const keys = perRow.get(row.id)\n if (!keys) continue\n const url = publicUrls.get(keys.fileKey)\n // No public URL for the ORIGINAL → the item does not resolve at all. Not\n // \"resolves with variants only\": the variants of a private original are an\n // accident of processing, not a sanctioned public view of it.\n if (url === undefined) continue\n\n const variants: Record<string, string> = {}\n for (const [styleName, variantKey] of keys.variantKeys) {\n const variantUrl = publicUrls.get(variantKey)\n if (variantUrl !== undefined) variants[styleName] = variantUrl\n }\n\n out.set(row.id, {\n id: row.id,\n url,\n mimeType: row.mimeType ?? null,\n width: row.width ?? null,\n height: row.height ?? null,\n alt: row.alt ?? null,\n title: row.title ?? null,\n variants,\n })\n }\n\n return out\n}\n\n/**\n * Wire {@link resolvePublicMedia} to the running app — the one line an app puts\n * into `createContentApiHandler({ resolveMedia })`.\n *\n * Every dependency is resolved through the PUBLIC read path:\n * `MediaQueryClient` (so the publish filter, the `view` gate and locale merging\n * all apply, and `alt`/`title` come back in the requested locale), and\n * `StorageClient.getPublicUrls` (which has no signed-URL branch at all).\n *\n * Imports are dynamic for the same reason `createMediaQueryClient`'s are: this\n * must be callable from a route module without dragging the storage/settings\n * graph into whatever bundle imports it.\n */\n/**\n * A single-value cache with a wall-clock TTL.\n *\n * Extracted rather than inlined so it can be tested with an injected clock:\n * the thing being asserted is time-dependent, and new stateful code on the\n * anonymous request path with no coverage is how a cache ends up serving the\n * wrong thing for a window nobody measured.\n *\n * Caches the PROMISE, not the settled value, so concurrent callers arriving\n * before the first load resolves share it. Caching only the value would let N\n * simultaneous requests each start their own read — the stampede this memo\n * exists to remove, on the one path where bursts are expected.\n *\n * A rejection is still NOT pinned for the TTL: the entry is dropped when the\n * promise rejects, so the next call retries rather than replaying a transient\n * outage for the rest of the window.\n */\nexport function memoiseWithTtl<T>(\n load: () => Promise<T>,\n ttlMs: number,\n now: () => number = Date.now,\n): () => Promise<T> {\n let cached: { value: Promise<T>; at: number } | null = null\n return async () => {\n const t = now()\n if (cached && t - cached.at < ttlMs) return cached.value\n const value = load()\n cached = { value, at: t }\n value.catch(() => {\n // Only clear OUR entry — a later call may already have replaced it.\n if (cached?.value === value) cached = null\n })\n return value\n }\n}\n\nexport function createPublicMediaResolver(): (\n ids: readonly string[],\n opts?: { locale?: string | undefined },\n) => Promise<Map<string, PublicMediaProjection>> {\n // The image-style set, memoised with a SHORT TTL.\n //\n // `resolveImageStyles` reads the settings DB and is documented as not cached,\n // so without this every media pass costs a settings query — and there is more\n // than one pass per response (the root rows, plus one per `?populate=`\n // expansion group), on an anonymous request path. A process-lifetime cache\n // would be wrong in the other direction: image styles are ADMIN-EDITABLE at\n // runtime, so it would serve a stale style set until redeploy.\n //\n // 30s mirrors the TTL `content-api` uses for its permission checker, and the\n // bound is the same shape: per process, so a multi-instance deployment sees a\n // new style within one TTL of each instance's own expiry. The cost of being\n // stale here is a variant URL for a style that was just added or removed —\n // the \"missing or not public\" path already handles it as an absent variant.\n const STYLES_TTL_MS = 30_000\n const imageStyles = memoiseWithTtl(async (): Promise<Record<string, ImageStyle>> => {\n const { getApp } = await import('@murumets-ee/core')\n const { resolveImageStyles } = await import('./resolve-image-styles.js')\n return resolveImageStyles(getApp())\n }, STYLES_TTL_MS)\n\n return async (ids, opts = {}) => {\n const [{ createMediaQueryClient }, { getSharedStorageClient }, { deriveVariantKey }] =\n await Promise.all([\n import('./query-client.js'),\n import('./client.js'),\n import('./variant-key.js'),\n ])\n\n const [mediaQuery, storage] = await Promise.all([\n createMediaQueryClient(),\n getSharedStorageClient(),\n ])\n\n return resolvePublicMedia(ids, {\n readMedia: async (mediaIds, locale) => {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { getTableColumns, inArray } = await import('drizzle-orm')\n const table = schemaRegistry.get('media')\n if (!table) return []\n // Column refs come from drizzle's own typed accessor, never from a bare\n // `table.id` — the registry hands back a dynamically-generated table\n // whose property access is untyped, so reaching through it would put an\n // `any` straight into the WHERE clause.\n const columns = getTableColumns(table) as Record<string, PgColumn>\n const idColumn = columns.id\n if (!idColumn) return []\n const rows = await mediaQuery.findMany({\n where: inArray(idColumn, [...mediaIds]),\n limit: mediaIds.length,\n ...(locale !== undefined && { locale }),\n })\n return rows as unknown as PublicMediaRow[]\n },\n getPublicUrls: (keys) => storage.getPublicUrls(keys),\n imageStyles,\n variantKey: deriveVariantKey,\n }, opts)\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"public-resolver.mjs","names":[],"sources":["../src/public-resolver.ts"],"sourcesContent":["/**\n * The PUBLIC media resolver — media as data, for a consumer that builds its own\n * markup (plan/api phase 06, D021).\n *\n * **Why not `resolveMediaRefs`.** That helper parses `[media:type:id:variant]`\n * tags out of a text string and substitutes rendered HTML (`<img …>`, `<a\n * href=…>`) through per-type renderers. Right for a server-rendered page, wrong\n * for an HTTP API: a frontend needs `{url, width, height, alt}` so it can emit\n * its own `<picture>` with its own `srcset`, not a pre-baked tag it has to parse\n * back apart. Nothing here deprecates `resolveMediaRefs`; the API simply uses a\n * different, data-shaped path.\n *\n * **Why the entity cannot just be whitelisted.** `Media` is a real entity, but\n * it has no `url` field and cannot have one: what is stored is `fileKey`, and\n * the URL is DERIVED by the storage layer, which also decides whether it is a\n * direct public link or a time-limited signed one. Serving `media` as an\n * ordinary whitelisted entity would hand a consumer a storage key — useless for\n * rendering, and a gratuitous disclosure of storage layout.\n *\n * **This resolver can never emit a signed URL, structurally.** It asks storage\n * for PUBLIC urls (`getPublicUrls`) rather than asking for \"a url\" and\n * inspecting what came back. The second shape produces a signed URL on the\n * happy path and relies on a later check to suppress it — and a signed URL is a\n * bearer capability that outlives the permission check that minted it, on a\n * response the public role is deliberately allowed to cache. There is no branch\n * here to forget.\n *\n * D021 permits a permission-gated scoped URL for NON-public principals on\n * responses already forced to `no-store`. It is deliberately not built: no\n * consumer needs it, and an unexercised signed-URL branch is a check that\n * cannot fail. The shape is fixed so nobody re-derives it; the path waits for a\n * named consumer.\n */\n\nimport type { PgColumn } from 'drizzle-orm/pg-core'\n\nexport { cacheOnceUnlessRejected } from './async-cache.js'\n\nimport type { ImageStyle } from './types.js'\n\n/**\n * One media item as it reaches an HTTP consumer.\n *\n * A STRUCTURAL match for `@murumets-ee/content-api`'s `MediaProjection`, not an\n * import of it: `content-api` is a leaf that this package's consumers wire into\n * by hand, exactly like `ContentApiResolution` mirrors content's\n * `ResolveResult`. The app that wires the two is where TypeScript checks they\n * agree.\n *\n * `width`/`height` ride along because a frontend needs them to reserve layout\n * space before the image loads; omitting them forces either a layout shift or a\n * second request, and the second request is the 4+N failure D001 exists to\n * prevent. `alt`/`title` are translatable on the Media entity, so this\n * projection is locale-dependent.\n */\nexport interface PublicMediaProjection {\n id: string\n url: string\n mimeType: string | null\n width: number | null\n height: number | null\n alt: string | null\n title: string | null\n /**\n * Variant URLs by image-style name, INLINED as data rather than handed over\n * as a URL pattern to interpolate.\n *\n * Two reasons, and the second is a security property. D008's rule is that a\n * client derives nothing — a consumer that constructs URLs from a pattern\n * encodes an assumption the server never published, and nothing can detect\n * when it goes stale. And a URL pattern is necessarily public-SHAPED: the\n * moment a consumer can build its own URL, the server has lost the ability to\n * decide what that consumer may address, and the \"never a signed URL\"\n * guarantee becomes unenforceable because the consumer stopped asking.\n *\n * The payload multiplier is bounded by CONFIGURATION, not by content: the set\n * of image styles is operator-chosen and neither the caller nor the editor can\n * grow it. So this needs no `?populate=`-style opt-in — unlike reference\n * expansion, where the caller could otherwise drag arbitrarily large targets\n * in. A style whose variant file is missing or not public is simply absent.\n */\n variants: Record<string, string>\n}\n\n/** The Media rows this resolver needs. Structural, so any read path can supply them. */\nexport interface PublicMediaRow {\n id: string\n fileKey: string\n mimeType?: string | null\n width?: number | null\n height?: number | null\n alt?: string | null\n title?: string | null\n}\n\nexport interface PublicMediaResolverDeps {\n /**\n * Read media rows by id through a PUBLISHED-ONLY path.\n *\n * **The wiring must guarantee this** — pass a `MediaQueryClient`/`QueryClient`\n * read, never an `AdminClient` one. This module cannot enforce the choice (it\n * is injected), and an admin read would surface unpublished media on the\n * anonymous surface. Same posture, and same reason, as\n * `ContentApiResolution.resolvePath`.\n */\n readMedia: (ids: readonly string[], locale?: string) => Promise<PublicMediaRow[]>\n /**\n * Public URLs for a batch of storage keys. Anything missing or non-public is\n * ABSENT from the map — see `StorageClient.getPublicUrls`, which is the ask-\n * for-public shape this whole module rests on.\n */\n getPublicUrls: (keys: readonly string[]) => Promise<Map<string, string>>\n /** The configured image styles. Operator-owned; neither caller nor editor grows it. */\n imageStyles: () => Promise<Record<string, ImageStyle>>\n /** Derives a variant's storage key from the original's. */\n variantKey: (fileKey: string, styleName: string, format: string) => string\n}\n\n/**\n * Resolve media ids to public projections.\n *\n * Ids that do not exist, are unpublished for this caller, or whose file is not\n * public are ABSENT from the returned map — one absence, four causes, exactly\n * as phase 05's failure semantics require. The caller renders without them.\n *\n * Cost is THREE round trips at most, regardless of batch size: the image-style\n * lookup (usually served from the caller's cache — see\n * {@link createPublicMediaResolver}), one entity read, and one storage read\n * covering every original and every variant key. Emphatically NOT\n * N×(styles+1), which is what a per-item convenience method would have made it.\n * The storage read is internally chunked when the key count exceeds what one\n * query may carry, so \"one storage read\" is `ceil(keys / 500)` in the extreme.\n */\nexport async function resolvePublicMedia(\n ids: readonly string[],\n deps: PublicMediaResolverDeps,\n opts: { locale?: string | undefined } = {},\n): Promise<Map<string, PublicMediaProjection>> {\n const out = new Map<string, PublicMediaProjection>()\n const unique = [...new Set(ids)]\n if (unique.length === 0) return out\n\n const rows = await deps.readMedia(unique, opts.locale)\n if (rows.length === 0) return out\n\n const styles = await deps.imageStyles()\n const styleEntries = Object.entries(styles)\n\n // Every key we might need — originals AND variants — resolved in one storage\n // query. `getPublicUrls` returns nothing for a key that is missing or not\n // public, so a private original drops the whole item and a private variant\n // drops only that variant.\n const keysToLookUp: string[] = []\n /** Per row: its original key, plus (styleName → variant key). */\n const perRow = new Map<string, { fileKey: string; variantKeys: Array<[string, string]> }>()\n\n for (const row of rows) {\n if (typeof row.fileKey !== 'string' || row.fileKey === '') continue\n const variantKeys: Array<[string, string]> = styleEntries.map(([name, style]) => [\n name,\n deps.variantKey(row.fileKey, name, style.format ?? 'webp'),\n ])\n perRow.set(row.id, { fileKey: row.fileKey, variantKeys })\n keysToLookUp.push(row.fileKey, ...variantKeys.map(([, key]) => key))\n }\n\n const publicUrls = await deps.getPublicUrls(keysToLookUp)\n\n for (const row of rows) {\n const keys = perRow.get(row.id)\n if (!keys) continue\n const url = publicUrls.get(keys.fileKey)\n // No public URL for the ORIGINAL → the item does not resolve at all. Not\n // \"resolves with variants only\": the variants of a private original are an\n // accident of processing, not a sanctioned public view of it.\n if (url === undefined) continue\n\n const variants: Record<string, string> = {}\n for (const [styleName, variantKey] of keys.variantKeys) {\n const variantUrl = publicUrls.get(variantKey)\n if (variantUrl !== undefined) variants[styleName] = variantUrl\n }\n\n out.set(row.id, {\n id: row.id,\n url,\n mimeType: row.mimeType ?? null,\n width: row.width ?? null,\n height: row.height ?? null,\n alt: row.alt ?? null,\n title: row.title ?? null,\n variants,\n })\n }\n\n return out\n}\n\n/**\n * Wire {@link resolvePublicMedia} to the running app — the one line an app puts\n * into `createContentApiHandler({ resolveMedia })`.\n *\n * Every dependency is resolved through the PUBLIC read path:\n * `MediaQueryClient` (so the publish filter, the `view` gate and locale merging\n * all apply, and `alt`/`title` come back in the requested locale), and\n * `StorageClient.getPublicUrls` (which has no signed-URL branch at all).\n *\n * Imports are dynamic for the same reason `createMediaQueryClient`'s are: this\n * must be callable from a route module without dragging the storage/settings\n * graph into whatever bundle imports it.\n */\n/**\n * A single-value cache with a wall-clock TTL.\n *\n * Extracted rather than inlined so it can be tested with an injected clock:\n * the thing being asserted is time-dependent, and new stateful code on the\n * anonymous request path with no coverage is how a cache ends up serving the\n * wrong thing for a window nobody measured.\n *\n * Caches the PROMISE, not the settled value, so concurrent callers arriving\n * before the first load resolves share it. Caching only the value would let N\n * simultaneous requests each start their own read — the stampede this memo\n * exists to remove, on the one path where bursts are expected.\n *\n * A rejection is still NOT pinned for the TTL: the entry is dropped when the\n * promise rejects, so the next call retries rather than replaying a transient\n * outage for the rest of the window.\n */\nexport function memoiseWithTtl<T>(\n load: () => Promise<T>,\n ttlMs: number,\n now: () => number = Date.now,\n): () => Promise<T> {\n let cached: { value: Promise<T>; at: number } | null = null\n return async () => {\n const t = now()\n if (cached && t - cached.at < ttlMs) return cached.value\n const value = load()\n cached = { value, at: t }\n value.catch(() => {\n // Only clear OUR entry — a later call may already have replaced it.\n if (cached?.value === value) cached = null\n })\n return value\n }\n}\n\nexport function createPublicMediaResolver(): (\n ids: readonly string[],\n opts?: { locale?: string | undefined },\n) => Promise<Map<string, PublicMediaProjection>> {\n // The image-style set, memoised with a SHORT TTL.\n //\n // `resolveImageStyles` reads the settings DB and is documented as not cached,\n // so without this every media pass costs a settings query — and there is more\n // than one pass per response (the root rows, plus one per `?populate=`\n // expansion group), on an anonymous request path. A process-lifetime cache\n // would be wrong in the other direction: image styles are ADMIN-EDITABLE at\n // runtime, so it would serve a stale style set until redeploy.\n //\n // 30s mirrors the TTL `content-api` uses for its permission checker, and the\n // bound is the same shape: per process, so a multi-instance deployment sees a\n // new style within one TTL of each instance's own expiry. The cost of being\n // stale here is a variant URL for a style that was just added or removed —\n // the \"missing or not public\" path already handles it as an absent variant.\n const STYLES_TTL_MS = 30_000\n const imageStyles = memoiseWithTtl(async (): Promise<Record<string, ImageStyle>> => {\n const { getApp } = await import('@murumets-ee/core')\n const { resolveImageStyles } = await import('./resolve-image-styles.js')\n return resolveImageStyles(getApp())\n }, STYLES_TTL_MS)\n\n return async (ids, opts = {}) => {\n const [{ createMediaQueryClient }, { getSharedStorageClient }, { deriveVariantKey }] =\n await Promise.all([\n import('./query-client.js'),\n import('./client.js'),\n import('./variant-key.js'),\n ])\n\n const [mediaQuery, storage] = await Promise.all([\n createMediaQueryClient(),\n getSharedStorageClient(),\n ])\n\n return resolvePublicMedia(\n ids,\n {\n readMedia: async (mediaIds, locale) => {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { getTableColumns, inArray } = await import('drizzle-orm')\n const table = schemaRegistry.get('media')\n if (!table) return []\n // Column refs come from drizzle's own typed accessor, never from a bare\n // `table.id` — the registry hands back a dynamically-generated table\n // whose property access is untyped, so reaching through it would put an\n // `any` straight into the WHERE clause.\n const columns = getTableColumns(table) as Record<string, PgColumn>\n const idColumn = columns.id\n if (!idColumn) return []\n const rows = await mediaQuery.findMany({\n where: inArray(idColumn, [...mediaIds]),\n limit: mediaIds.length,\n ...(locale !== undefined && { locale }),\n })\n return rows as unknown as PublicMediaRow[]\n },\n getPublicUrls: (keys) => storage.getPublicUrls(keys),\n imageStyles,\n variantKey: deriveVariantKey,\n },\n opts,\n )\n }\n}\n\n/**\n * One image the CANVAS can draw for a media id — the shape `@murumets-ee/blocks`'\n * `CanvasImageResolver` asks for, declared structurally so this package does not\n * depend on blocks.\n */\nexport interface CanvasImage {\n readonly url: string\n readonly alt: string | null\n readonly width: number | null\n readonly height: number | null\n}\n\nlet canvasResolver: ReturnType<typeof createPublicMediaResolver> | undefined\n\n/**\n * Media ids → images the block-editor CANVAS may draw: a BOUND `field.media()`\n * column (frontend-delivery F112) and a collection card's picture.\n *\n * The ready-made `blocksPlugin({ resolveImages })` value, so an app wires one lazy\n * line instead of copying a resolver (frontend-delivery D025 — no builder's\n * manual):\n *\n * ```ts\n * blocksPlugin({\n * resolveImages: async (ids, opts) =>\n * (await import('@murumets-ee/media/public')).resolveCanvasImages(ids, opts),\n * })\n * ```\n *\n * Two properties are the point, not conveniences:\n *\n * - **The SAME public resolver content-api uses**, so the canvas cannot show an\n * image the published site would not (a private asset draws as empty on both).\n * - **Images only.** A `field.media({ kind: 'image' })` constrains only the\n * picker's list (editor F116), so a video or PDF can sit in the column; drawn\n * through an `<img>` it would be a broken image. Anything that is not\n * `image/*` is left out, and the canvas draws its empty placeholder instead.\n *\n * The resolver is built once per process — it memoises the image-style set, which\n * a fresh one per call would re-read.\n */\nexport async function resolveCanvasImages(\n ids: readonly string[],\n opts: { locale: string },\n): Promise<Map<string, CanvasImage>> {\n canvasResolver ??= createPublicMediaResolver()\n return toCanvasImages(await canvasResolver(ids, opts))\n}\n\n/**\n * The pure half of {@link resolveCanvasImages}: keep images, drop everything else,\n * project to what the canvas draws. Exported so the filter has a test that does\n * not need a database.\n */\nexport function toCanvasImages(\n found: ReadonlyMap<string, PublicMediaProjection>,\n): Map<string, CanvasImage> {\n const images = new Map<string, CanvasImage>()\n for (const [id, media] of found) {\n if (media.mimeType?.startsWith('image/') !== true) continue\n images.set(id, { url: media.url, alt: media.alt, width: media.width, height: media.height })\n }\n return images\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@murumets-ee/media",
3
- "version": "0.64.0",
3
+ "version": "0.65.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -65,13 +65,13 @@
65
65
  "sharp": "^0.34.5",
66
66
  "tailwind-merge": "^2.6.0",
67
67
  "zod": "^3.24.1",
68
- "@murumets-ee/core": "0.64.0",
69
- "@murumets-ee/db": "0.64.0",
70
- "@murumets-ee/entity": "0.64.0",
71
- "@murumets-ee/logging": "0.64.0",
72
- "@murumets-ee/settings": "0.64.0",
73
- "@murumets-ee/storage": "0.64.0",
74
- "@murumets-ee/ui": "0.64.0"
68
+ "@murumets-ee/core": "0.65.0",
69
+ "@murumets-ee/db": "0.65.0",
70
+ "@murumets-ee/entity": "0.65.0",
71
+ "@murumets-ee/logging": "0.65.0",
72
+ "@murumets-ee/settings": "0.65.0",
73
+ "@murumets-ee/storage": "0.65.0",
74
+ "@murumets-ee/ui": "0.65.0"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "lucide-react": ">=0.400.0 <1",