@c-rex/services 0.3.0-build.59 → 0.3.0-build.67

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.
@@ -87,6 +87,9 @@ var slugToPath = (slug, version = IDS_API_VERSION) => {
87
87
  return `${area}/${version}/${resource}`;
88
88
  };
89
89
 
90
+ // src/html-enrichment/loader.ts
91
+ var import_constants = require("@c-rex/constants");
92
+
90
93
  // src/base-client-request.ts
91
94
  var baseClientRequest = async (endpoint, queryParams, options, extra) => {
92
95
  const res = await fetch(`/api/rpc`, {
@@ -150,7 +153,7 @@ var loadEnrichmentCollection = async (descriptor) => {
150
153
  if (params.Links === void 0) params.Links = true;
151
154
  }
152
155
  try {
153
- const isPublicGmpStartTiles = descriptor.renderer === "GMPTilesRenderer" && basePath === "iirds/v1/Fragments";
156
+ const isPublicGmpStartTiles = descriptor.renderer === import_constants.GMP_TILES_RENDERER && basePath === "iirds/v1/Fragments";
154
157
  const data = isPublicGmpStartTiles ? await baseClientRequest(basePath, params, void 0, {
155
158
  authMode: "anonymous"
156
159
  }) : await baseClientRequest(basePath, params);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/html-enrichment/index.ts","../../src/html-enrichment/descriptor.ts","../../src/html-enrichment/endpoint-path.ts","../../src/base-client-request.ts","../../src/html-enrichment/loader.ts"],"sourcesContent":["export type {\r\n HtmlEnrichmentDescriptor,\r\n HtmlEnrichmentLoadMode,\r\n HtmlEnrichmentState,\r\n HtmlEnrichmentEndpointResult,\r\n} from \"./types\";\r\nexport { parseEnrichmentDescriptor } from \"./descriptor\";\r\nexport { slugToPath, IDS_API_VERSION } from \"./endpoint-path\";\r\nexport {\r\n loadEnrichment,\r\n loadEnrichmentCollection,\r\n loadEnrichmentHtml,\r\n loadEnrichmentResource,\r\n} from \"./loader\";\r\nexport type {\r\n EnrichmentResourceModel,\r\n EnrichmentResourceResult,\r\n} from \"./loader\";\r\n","import type {\r\n HtmlEnrichmentDescriptor,\r\n HtmlEnrichmentLoadMode,\r\n HtmlEnrichmentState,\r\n} from \"./types\";\r\n\r\nconst VALID_LOAD_MODES = new Set<string>([\"onload\", \"onappear\", \"onclick\"]);\r\nconst VALID_STATES = new Set<string>([\r\n \"unloaded\",\r\n \"loading\",\r\n \"loaded\",\r\n \"denied\",\r\n \"error\",\r\n]);\r\n\r\nconst toQueryParamName = (rawName: string): string => {\r\n const normalized = rawName.toLowerCase();\r\n if (normalized === \"restriction\") return \"Restrict\";\r\n\r\n return rawName\r\n .split(\"-\")\r\n .filter(Boolean)\r\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\r\n .join(\"\");\r\n};\r\n\r\nconst parseParamValue = (name: string, value: string): string | string[] => {\r\n // Restrict selectors use semicolons internally, so the complete selector is\r\n // one API value. Other marker params use semicolons as list separators.\r\n if (name === \"Restrict\") return value.trim();\r\n return value\r\n .split(\";\")\r\n .map((item) => item.trim())\r\n .filter(Boolean);\r\n};\r\n\r\nconst parseParams = (\r\n el: HTMLElement,\r\n): Record<string, string | string[]> | undefined => {\r\n const params: Record<string, string | string[]> = {};\r\n\r\n for (const attributeName of el.getAttributeNames()) {\r\n const prefix = \"data-crex-param-\";\r\n if (!attributeName.startsWith(prefix)) continue;\r\n\r\n const rawValue = el.getAttribute(attributeName);\r\n if (!rawValue) continue;\r\n const name = toQueryParamName(attributeName.slice(prefix.length));\r\n params[name] = parseParamValue(name, rawValue);\r\n }\r\n\r\n return Object.keys(params).length > 0 ? params : undefined;\r\n};\r\n\r\n/**\r\n * Convert a `data-crex-*` enriched DOM node into a typed descriptor.\r\n * Unknown `load`/`state` values are dropped (undefined) rather than trusted.\r\n */\r\nexport const parseEnrichmentDescriptor = (\r\n el: HTMLElement,\r\n): HtmlEnrichmentDescriptor => {\r\n const loadModeRaw = el.getAttribute(\"data-crex-load\") ?? undefined;\r\n const stateRaw = el.getAttribute(\"data-crex-state\") ?? undefined;\r\n\r\n return {\r\n elementId: el.id || undefined,\r\n loadMode:\r\n loadModeRaw && VALID_LOAD_MODES.has(loadModeRaw)\r\n ? (loadModeRaw as HtmlEnrichmentLoadMode)\r\n : undefined,\r\n endpoint: el.getAttribute(\"data-crex-endpoint\") ?? undefined,\r\n state:\r\n stateRaw && VALID_STATES.has(stateRaw)\r\n ? (stateRaw as HtmlEnrichmentState)\r\n : undefined,\r\n clientFilter: el.getAttribute(\"data-crex-client-filter\") ?? undefined,\r\n renderer: el.getAttribute(\"data-crex-renderer\") ?? undefined,\r\n resourceId: el.getAttribute(\"data-crex-id\") ?? undefined,\r\n params: parseParams(el),\r\n };\r\n};\r\n","/**\r\n * API version segment injected into resolved enrichment paths.\r\n * Kept as a code constant for now; promote to an env var when versioning\r\n * needs to vary per deployment (note: client-side use requires NEXT_PUBLIC_*).\r\n */\r\nexport const IDS_API_VERSION = \"v1\";\r\n\r\nconst capitalize = (s: string): string =>\r\n s ? s.charAt(0).toUpperCase() + s.slice(1) : s;\r\n\r\n/**\r\n * Convert a logical endpoint slug into a versioned API base path.\r\n *\r\n * Slug shape: `<area>-<resource>` (resource may be multi-word, dash-separated).\r\n * `ids-resources` -> `ids/v1/Resources`\r\n * `ids-info-units` -> `ids/v1/InfoUnits`\r\n *\r\n * Returns null when the slug does not match the expected shape.\r\n * The full path is later combined with the encoded resource id by the loader\r\n * and resolved against the API root by `CrexApi.buildUrl`.\r\n */\r\nexport const slugToPath = (\r\n slug: string,\r\n version: string = IDS_API_VERSION,\r\n): string | null => {\r\n const parts = slug.split(\"-\").filter(Boolean);\r\n if (parts.length < 2) return null;\r\n\r\n const [area, ...resourceParts] = parts;\r\n const resource = resourceParts.map(capitalize).join(\"\");\r\n if (!area || !resource) return null;\r\n\r\n return `${area}/${version}/${resource}`;\r\n};\r\n","export const baseClientRequest = async <T>(\r\n endpoint: string,\r\n queryParams?: Record<\r\n string,\r\n string | number | boolean | (string | number | boolean)[] | undefined\r\n >,\r\n options?: { signal?: AbortSignal },\r\n extra?: {\r\n responseType?: \"json\" | \"text\";\r\n authMode?: \"configured\" | \"anonymous\";\r\n },\r\n): Promise<T> => {\r\n const res = await fetch(`/api/rpc`, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify({\r\n method: \"CrexApi.execute\",\r\n params: {\r\n url: endpoint,\r\n method: \"GET\",\r\n params: queryParams,\r\n responseType: extra?.responseType,\r\n authMode: extra?.authMode,\r\n },\r\n }),\r\n credentials: \"include\",\r\n signal: options?.signal,\r\n });\r\n\r\n const json = await res.json();\r\n\r\n if (!res.ok) throw new Error(json.error || \"Unknown error\");\r\n\r\n return json.data;\r\n};\r\n","import type { LiteralModel, RenditionModel } from \"@c-rex/interfaces\";\r\nimport { baseClientRequest } from \"../base-client-request\";\r\nimport { slugToPath } from \"./endpoint-path\";\r\nimport type {\r\n HtmlEnrichmentDescriptor,\r\n HtmlEnrichmentEndpointResult,\r\n} from \"./types\";\r\n\r\n/**\r\n * Access-denied heuristic. This is NOT an HTTP auth failure — metadata may\r\n * resolve while the content rendition is licence-restricted. HTTP 403 is the\r\n * current signal until the IDS API confirms a canonical denied contract\r\n * (see HTML_RUNTIME_ENRICHMENT_SPEC.md → Open Questions).\r\n */\r\nconst isDeniedError = (err: unknown): boolean =>\r\n err instanceof Error && err.message.includes(\"403\");\r\n\r\n/**\r\n * Generic enrichment loader: resolves `data-crex-endpoint` (slug) + the\r\n * `data-crex-id` resource IRI into a single request and returns the raw HTML.\r\n *\r\n * Request shape: `{<area>/<version>/<Resource>}/view/{encodeURIComponent(id)}`\r\n * fetched as text. The `/view/` segment returns the rendered HTML rendition.\r\n * Works for any `<area>-<resource>` slug — no per-endpoint registration needed.\r\n */\r\nexport const loadEnrichmentHtml = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n): Promise<HtmlEnrichmentEndpointResult> => {\r\n if (!descriptor.endpoint || !descriptor.resourceId) {\r\n return { html: null, isDenied: false };\r\n }\r\n\r\n const basePath = slugToPath(descriptor.endpoint);\r\n if (!basePath) {\r\n return { html: null, isDenied: false };\r\n }\r\n\r\n const url = `${basePath}/view/${encodeURIComponent(descriptor.resourceId)}`;\r\n\r\n try {\r\n const html = await baseClientRequest<string>(\r\n url,\r\n descriptor.params,\r\n undefined,\r\n {\r\n responseType: \"text\",\r\n },\r\n );\r\n return { html, isDenied: false };\r\n } catch (err: unknown) {\r\n if (isDeniedError(err)) {\r\n return { html: null, isDenied: true };\r\n }\r\n throw err;\r\n }\r\n};\r\n\r\n/**\r\n * Loads a collection-style enrichment marker such as the GMP Startseite tile\r\n * container. Collection markers do not have a data-crex-id; their API query is\r\n * carried by data-crex-param-* attributes instead.\r\n */\r\nexport const loadEnrichmentCollection = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n): Promise<HtmlEnrichmentEndpointResult> => {\r\n if (!descriptor.endpoint) return { html: null, isDenied: false };\r\n\r\n const basePath = slugToPath(descriptor.endpoint);\r\n if (!basePath) return { html: null, isDenied: false };\r\n\r\n const params = { ...(descriptor.params ?? {}) };\r\n const fields = params.Fields;\r\n const hasRenditions = Array.isArray(fields)\r\n ? fields.some((field) => String(field).toLowerCase() === \"renditions\")\r\n : String(fields ?? \"\")\r\n .toLowerCase()\r\n .includes(\"renditions\");\r\n\r\n // Tile renderers need the rendition objects and their view links in the\r\n // collection response. Keep the marker's explicit params authoritative,\r\n // while filling in the two optional API flags when renditions were asked for.\r\n if (hasRenditions) {\r\n if (params.Embed === undefined) params.Embed = [\"renditions\"];\r\n if (params.Links === undefined) params.Links = true;\r\n }\r\n\r\n try {\r\n const isPublicGmpStartTiles =\r\n descriptor.renderer === \"GMPTilesRenderer\" &&\r\n basePath === \"iirds/v1/Fragments\";\r\n const data = isPublicGmpStartTiles\r\n ? await baseClientRequest<unknown>(basePath, params, undefined, {\r\n authMode: \"anonymous\",\r\n })\r\n : await baseClientRequest<unknown>(basePath, params);\r\n return { html: null, data, isDenied: false };\r\n } catch (err: unknown) {\r\n if (isDeniedError(err)) {\r\n return { html: null, isDenied: true };\r\n }\r\n throw err;\r\n }\r\n};\r\n\r\n/** Selects the individual-resource or collection loader from the marker. */\r\nexport const loadEnrichment = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n): Promise<HtmlEnrichmentEndpointResult> =>\r\n descriptor.resourceId\r\n ? loadEnrichmentHtml(descriptor)\r\n : loadEnrichmentCollection(descriptor);\r\n\r\n/**\r\n * Minimal shape shared by the iiRDS model endpoints used for onclick enrichment\r\n * (Fragments, InformationUnits, …). Kept loose so any model-with-renditions\r\n * endpoint works without per-endpoint typing.\r\n */\r\nexport type EnrichmentResourceModel = {\r\n /**\r\n * Root short id of the resource. Used to build in-app navigation targets\r\n * (e.g. swapping the topic id segment) instead of rendering the payload.\r\n */\r\n shortId?: string | null;\r\n renditions?: RenditionModel[] | null;\r\n titles?: LiteralModel[] | null;\r\n labels?: LiteralModel[] | null;\r\n /**\r\n * Content access state. The request itself can succeed (HTTP 200, metadata\r\n * present) while the content is licence-restricted — IDS signals that here\r\n * rather than via an HTTP error.\r\n */\r\n authorization?: { state?: string | null } | null;\r\n};\r\n\r\nexport type EnrichmentResourceResult = {\r\n data: EnrichmentResourceModel | null;\r\n isDenied: boolean;\r\n};\r\n\r\n/**\r\n * Generic onclick enrichment loader: resolves `data-crex-endpoint` (slug) + the\r\n * `data-crex-id` resource IRI and fetches the JSON model (e.g. a Fragment or\r\n * InformationUnit) for rendering inside a popover/modal. Endpoint resolution is\r\n * fully dynamic via `slugToPath` — no per-endpoint hardcoding.\r\n */\r\nexport const loadEnrichmentResource = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n queryParams?: Record<\r\n string,\r\n string | number | boolean | (string | number | boolean)[] | undefined\r\n >,\r\n): Promise<EnrichmentResourceResult> => {\r\n if (!descriptor.endpoint || !descriptor.resourceId) {\r\n return { data: null, isDenied: false };\r\n }\r\n\r\n const basePath = slugToPath(descriptor.endpoint);\r\n if (!basePath) {\r\n return { data: null, isDenied: false };\r\n }\r\n\r\n const url = `${basePath}/${encodeURIComponent(descriptor.resourceId)}`;\r\n\r\n try {\r\n const data = await baseClientRequest<EnrichmentResourceModel>(\r\n url,\r\n queryParams,\r\n );\r\n // Denied is carried in the (successful) payload, not as an HTTP error.\r\n const isDenied = data?.authorization?.state === \"denied\";\r\n return { data, isDenied };\r\n } catch (err: unknown) {\r\n if (isDeniedError(err)) {\r\n return { data: null, isDenied: true };\r\n }\r\n throw err;\r\n }\r\n};\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/html-enrichment/index.ts","../../src/html-enrichment/descriptor.ts","../../src/html-enrichment/endpoint-path.ts","../../src/html-enrichment/loader.ts","../../src/base-client-request.ts"],"sourcesContent":["export type {\r\n HtmlEnrichmentDescriptor,\r\n HtmlEnrichmentLoadMode,\r\n HtmlEnrichmentState,\r\n HtmlEnrichmentEndpointResult,\r\n} from \"./types\";\r\nexport { parseEnrichmentDescriptor } from \"./descriptor\";\r\nexport { slugToPath, IDS_API_VERSION } from \"./endpoint-path\";\r\nexport {\r\n loadEnrichment,\r\n loadEnrichmentCollection,\r\n loadEnrichmentHtml,\r\n loadEnrichmentResource,\r\n} from \"./loader\";\r\nexport type {\r\n EnrichmentResourceModel,\r\n EnrichmentResourceResult,\r\n} from \"./loader\";\r\n","import type {\r\n HtmlEnrichmentDescriptor,\r\n HtmlEnrichmentLoadMode,\r\n HtmlEnrichmentState,\r\n} from \"./types\";\r\n\r\nconst VALID_LOAD_MODES = new Set<string>([\"onload\", \"onappear\", \"onclick\"]);\r\nconst VALID_STATES = new Set<string>([\r\n \"unloaded\",\r\n \"loading\",\r\n \"loaded\",\r\n \"denied\",\r\n \"error\",\r\n]);\r\n\r\nconst toQueryParamName = (rawName: string): string => {\r\n const normalized = rawName.toLowerCase();\r\n if (normalized === \"restriction\") return \"Restrict\";\r\n\r\n return rawName\r\n .split(\"-\")\r\n .filter(Boolean)\r\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\r\n .join(\"\");\r\n};\r\n\r\nconst parseParamValue = (name: string, value: string): string | string[] => {\r\n // Restrict selectors use semicolons internally, so the complete selector is\r\n // one API value. Other marker params use semicolons as list separators.\r\n if (name === \"Restrict\") return value.trim();\r\n return value\r\n .split(\";\")\r\n .map((item) => item.trim())\r\n .filter(Boolean);\r\n};\r\n\r\nconst parseParams = (\r\n el: HTMLElement,\r\n): Record<string, string | string[]> | undefined => {\r\n const params: Record<string, string | string[]> = {};\r\n\r\n for (const attributeName of el.getAttributeNames()) {\r\n const prefix = \"data-crex-param-\";\r\n if (!attributeName.startsWith(prefix)) continue;\r\n\r\n const rawValue = el.getAttribute(attributeName);\r\n if (!rawValue) continue;\r\n const name = toQueryParamName(attributeName.slice(prefix.length));\r\n params[name] = parseParamValue(name, rawValue);\r\n }\r\n\r\n return Object.keys(params).length > 0 ? params : undefined;\r\n};\r\n\r\n/**\r\n * Convert a `data-crex-*` enriched DOM node into a typed descriptor.\r\n * Unknown `load`/`state` values are dropped (undefined) rather than trusted.\r\n */\r\nexport const parseEnrichmentDescriptor = (\r\n el: HTMLElement,\r\n): HtmlEnrichmentDescriptor => {\r\n const loadModeRaw = el.getAttribute(\"data-crex-load\") ?? undefined;\r\n const stateRaw = el.getAttribute(\"data-crex-state\") ?? undefined;\r\n\r\n return {\r\n elementId: el.id || undefined,\r\n loadMode:\r\n loadModeRaw && VALID_LOAD_MODES.has(loadModeRaw)\r\n ? (loadModeRaw as HtmlEnrichmentLoadMode)\r\n : undefined,\r\n endpoint: el.getAttribute(\"data-crex-endpoint\") ?? undefined,\r\n state:\r\n stateRaw && VALID_STATES.has(stateRaw)\r\n ? (stateRaw as HtmlEnrichmentState)\r\n : undefined,\r\n clientFilter: el.getAttribute(\"data-crex-client-filter\") ?? undefined,\r\n renderer: el.getAttribute(\"data-crex-renderer\") ?? undefined,\r\n resourceId: el.getAttribute(\"data-crex-id\") ?? undefined,\r\n params: parseParams(el),\r\n };\r\n};\r\n","/**\r\n * API version segment injected into resolved enrichment paths.\r\n * Kept as a code constant for now; promote to an env var when versioning\r\n * needs to vary per deployment (note: client-side use requires NEXT_PUBLIC_*).\r\n */\r\nexport const IDS_API_VERSION = \"v1\";\r\n\r\nconst capitalize = (s: string): string =>\r\n s ? s.charAt(0).toUpperCase() + s.slice(1) : s;\r\n\r\n/**\r\n * Convert a logical endpoint slug into a versioned API base path.\r\n *\r\n * Slug shape: `<area>-<resource>` (resource may be multi-word, dash-separated).\r\n * `ids-resources` -> `ids/v1/Resources`\r\n * `ids-info-units` -> `ids/v1/InfoUnits`\r\n *\r\n * Returns null when the slug does not match the expected shape.\r\n * The full path is later combined with the encoded resource id by the loader\r\n * and resolved against the API root by `CrexApi.buildUrl`.\r\n */\r\nexport const slugToPath = (\r\n slug: string,\r\n version: string = IDS_API_VERSION,\r\n): string | null => {\r\n const parts = slug.split(\"-\").filter(Boolean);\r\n if (parts.length < 2) return null;\r\n\r\n const [area, ...resourceParts] = parts;\r\n const resource = resourceParts.map(capitalize).join(\"\");\r\n if (!area || !resource) return null;\r\n\r\n return `${area}/${version}/${resource}`;\r\n};\r\n","import type { LiteralModel, RenditionModel } from \"@c-rex/interfaces\";\r\nimport { GMP_TILES_RENDERER } from \"@c-rex/constants\";\r\nimport { baseClientRequest } from \"../base-client-request\";\r\nimport { slugToPath } from \"./endpoint-path\";\r\nimport type {\r\n HtmlEnrichmentDescriptor,\r\n HtmlEnrichmentEndpointResult,\r\n} from \"./types\";\r\n\r\n/**\r\n * Access-denied heuristic. This is NOT an HTTP auth failure — metadata may\r\n * resolve while the content rendition is licence-restricted. HTTP 403 is the\r\n * current signal until the IDS API confirms a canonical denied contract\r\n * (see HTML_RUNTIME_ENRICHMENT_SPEC.md → Open Questions).\r\n */\r\nconst isDeniedError = (err: unknown): boolean =>\r\n err instanceof Error && err.message.includes(\"403\");\r\n\r\n/**\r\n * Generic enrichment loader: resolves `data-crex-endpoint` (slug) + the\r\n * `data-crex-id` resource IRI into a single request and returns the raw HTML.\r\n *\r\n * Request shape: `{<area>/<version>/<Resource>}/view/{encodeURIComponent(id)}`\r\n * fetched as text. The `/view/` segment returns the rendered HTML rendition.\r\n * Works for any `<area>-<resource>` slug — no per-endpoint registration needed.\r\n */\r\nexport const loadEnrichmentHtml = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n): Promise<HtmlEnrichmentEndpointResult> => {\r\n if (!descriptor.endpoint || !descriptor.resourceId) {\r\n return { html: null, isDenied: false };\r\n }\r\n\r\n const basePath = slugToPath(descriptor.endpoint);\r\n if (!basePath) {\r\n return { html: null, isDenied: false };\r\n }\r\n\r\n const url = `${basePath}/view/${encodeURIComponent(descriptor.resourceId)}`;\r\n\r\n try {\r\n const html = await baseClientRequest<string>(\r\n url,\r\n descriptor.params,\r\n undefined,\r\n {\r\n responseType: \"text\",\r\n },\r\n );\r\n return { html, isDenied: false };\r\n } catch (err: unknown) {\r\n if (isDeniedError(err)) {\r\n return { html: null, isDenied: true };\r\n }\r\n throw err;\r\n }\r\n};\r\n\r\n/**\r\n * Loads a collection-style enrichment marker such as the GMP Startseite tile\r\n * container. Collection markers do not have a data-crex-id; their API query is\r\n * carried by data-crex-param-* attributes instead.\r\n */\r\nexport const loadEnrichmentCollection = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n): Promise<HtmlEnrichmentEndpointResult> => {\r\n if (!descriptor.endpoint) return { html: null, isDenied: false };\r\n\r\n const basePath = slugToPath(descriptor.endpoint);\r\n if (!basePath) return { html: null, isDenied: false };\r\n\r\n const params = { ...(descriptor.params ?? {}) };\r\n const fields = params.Fields;\r\n const hasRenditions = Array.isArray(fields)\r\n ? fields.some((field) => String(field).toLowerCase() === \"renditions\")\r\n : String(fields ?? \"\")\r\n .toLowerCase()\r\n .includes(\"renditions\");\r\n\r\n // Tile renderers need the rendition objects and their view links in the\r\n // collection response. Keep the marker's explicit params authoritative,\r\n // while filling in the two optional API flags when renditions were asked for.\r\n if (hasRenditions) {\r\n if (params.Embed === undefined) params.Embed = [\"renditions\"];\r\n if (params.Links === undefined) params.Links = true;\r\n }\r\n\r\n try {\r\n const isPublicGmpStartTiles =\r\n descriptor.renderer === GMP_TILES_RENDERER &&\r\n basePath === \"iirds/v1/Fragments\";\r\n const data = isPublicGmpStartTiles\r\n ? await baseClientRequest<unknown>(basePath, params, undefined, {\r\n authMode: \"anonymous\",\r\n })\r\n : await baseClientRequest<unknown>(basePath, params);\r\n return { html: null, data, isDenied: false };\r\n } catch (err: unknown) {\r\n if (isDeniedError(err)) {\r\n return { html: null, isDenied: true };\r\n }\r\n throw err;\r\n }\r\n};\r\n\r\n/** Selects the individual-resource or collection loader from the marker. */\r\nexport const loadEnrichment = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n): Promise<HtmlEnrichmentEndpointResult> =>\r\n descriptor.resourceId\r\n ? loadEnrichmentHtml(descriptor)\r\n : loadEnrichmentCollection(descriptor);\r\n\r\n/**\r\n * Minimal shape shared by the iiRDS model endpoints used for onclick enrichment\r\n * (Fragments, InformationUnits, …). Kept loose so any model-with-renditions\r\n * endpoint works without per-endpoint typing.\r\n */\r\nexport type EnrichmentResourceModel = {\r\n /**\r\n * Root short id of the resource. Used to build in-app navigation targets\r\n * (e.g. swapping the topic id segment) instead of rendering the payload.\r\n */\r\n shortId?: string | null;\r\n renditions?: RenditionModel[] | null;\r\n titles?: LiteralModel[] | null;\r\n labels?: LiteralModel[] | null;\r\n /**\r\n * Content access state. The request itself can succeed (HTTP 200, metadata\r\n * present) while the content is licence-restricted — IDS signals that here\r\n * rather than via an HTTP error.\r\n */\r\n authorization?: { state?: string | null } | null;\r\n};\r\n\r\nexport type EnrichmentResourceResult = {\r\n data: EnrichmentResourceModel | null;\r\n isDenied: boolean;\r\n};\r\n\r\n/**\r\n * Generic onclick enrichment loader: resolves `data-crex-endpoint` (slug) + the\r\n * `data-crex-id` resource IRI and fetches the JSON model (e.g. a Fragment or\r\n * InformationUnit) for rendering inside a popover/modal. Endpoint resolution is\r\n * fully dynamic via `slugToPath` — no per-endpoint hardcoding.\r\n */\r\nexport const loadEnrichmentResource = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n queryParams?: Record<\r\n string,\r\n string | number | boolean | (string | number | boolean)[] | undefined\r\n >,\r\n): Promise<EnrichmentResourceResult> => {\r\n if (!descriptor.endpoint || !descriptor.resourceId) {\r\n return { data: null, isDenied: false };\r\n }\r\n\r\n const basePath = slugToPath(descriptor.endpoint);\r\n if (!basePath) {\r\n return { data: null, isDenied: false };\r\n }\r\n\r\n const url = `${basePath}/${encodeURIComponent(descriptor.resourceId)}`;\r\n\r\n try {\r\n const data = await baseClientRequest<EnrichmentResourceModel>(\r\n url,\r\n queryParams,\r\n );\r\n // Denied is carried in the (successful) payload, not as an HTTP error.\r\n const isDenied = data?.authorization?.state === \"denied\";\r\n return { data, isDenied };\r\n } catch (err: unknown) {\r\n if (isDeniedError(err)) {\r\n return { data: null, isDenied: true };\r\n }\r\n throw err;\r\n }\r\n};\r\n","export const baseClientRequest = async <T>(\r\n endpoint: string,\r\n queryParams?: Record<\r\n string,\r\n string | number | boolean | (string | number | boolean)[] | undefined\r\n >,\r\n options?: { signal?: AbortSignal },\r\n extra?: {\r\n responseType?: \"json\" | \"text\";\r\n authMode?: \"configured\" | \"anonymous\";\r\n },\r\n): Promise<T> => {\r\n const res = await fetch(`/api/rpc`, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify({\r\n method: \"CrexApi.execute\",\r\n params: {\r\n url: endpoint,\r\n method: \"GET\",\r\n params: queryParams,\r\n responseType: extra?.responseType,\r\n authMode: extra?.authMode,\r\n },\r\n }),\r\n credentials: \"include\",\r\n signal: options?.signal,\r\n });\r\n\r\n const json = await res.json();\r\n\r\n if (!res.ok) throw new Error(json.error || \"Unknown error\");\r\n\r\n return json.data;\r\n};\r\n"],"mappings":"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","names":[]}
@@ -55,6 +55,9 @@ var slugToPath = (slug, version = IDS_API_VERSION) => {
55
55
  return `${area}/${version}/${resource}`;
56
56
  };
57
57
 
58
+ // src/html-enrichment/loader.ts
59
+ import { GMP_TILES_RENDERER } from "@c-rex/constants";
60
+
58
61
  // src/base-client-request.ts
59
62
  var baseClientRequest = async (endpoint, queryParams, options, extra) => {
60
63
  const res = await fetch(`/api/rpc`, {
@@ -118,7 +121,7 @@ var loadEnrichmentCollection = async (descriptor) => {
118
121
  if (params.Links === void 0) params.Links = true;
119
122
  }
120
123
  try {
121
- const isPublicGmpStartTiles = descriptor.renderer === "GMPTilesRenderer" && basePath === "iirds/v1/Fragments";
124
+ const isPublicGmpStartTiles = descriptor.renderer === GMP_TILES_RENDERER && basePath === "iirds/v1/Fragments";
122
125
  const data = isPublicGmpStartTiles ? await baseClientRequest(basePath, params, void 0, {
123
126
  authMode: "anonymous"
124
127
  }) : await baseClientRequest(basePath, params);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/html-enrichment/descriptor.ts","../../src/html-enrichment/endpoint-path.ts","../../src/base-client-request.ts","../../src/html-enrichment/loader.ts"],"sourcesContent":["import type {\r\n HtmlEnrichmentDescriptor,\r\n HtmlEnrichmentLoadMode,\r\n HtmlEnrichmentState,\r\n} from \"./types\";\r\n\r\nconst VALID_LOAD_MODES = new Set<string>([\"onload\", \"onappear\", \"onclick\"]);\r\nconst VALID_STATES = new Set<string>([\r\n \"unloaded\",\r\n \"loading\",\r\n \"loaded\",\r\n \"denied\",\r\n \"error\",\r\n]);\r\n\r\nconst toQueryParamName = (rawName: string): string => {\r\n const normalized = rawName.toLowerCase();\r\n if (normalized === \"restriction\") return \"Restrict\";\r\n\r\n return rawName\r\n .split(\"-\")\r\n .filter(Boolean)\r\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\r\n .join(\"\");\r\n};\r\n\r\nconst parseParamValue = (name: string, value: string): string | string[] => {\r\n // Restrict selectors use semicolons internally, so the complete selector is\r\n // one API value. Other marker params use semicolons as list separators.\r\n if (name === \"Restrict\") return value.trim();\r\n return value\r\n .split(\";\")\r\n .map((item) => item.trim())\r\n .filter(Boolean);\r\n};\r\n\r\nconst parseParams = (\r\n el: HTMLElement,\r\n): Record<string, string | string[]> | undefined => {\r\n const params: Record<string, string | string[]> = {};\r\n\r\n for (const attributeName of el.getAttributeNames()) {\r\n const prefix = \"data-crex-param-\";\r\n if (!attributeName.startsWith(prefix)) continue;\r\n\r\n const rawValue = el.getAttribute(attributeName);\r\n if (!rawValue) continue;\r\n const name = toQueryParamName(attributeName.slice(prefix.length));\r\n params[name] = parseParamValue(name, rawValue);\r\n }\r\n\r\n return Object.keys(params).length > 0 ? params : undefined;\r\n};\r\n\r\n/**\r\n * Convert a `data-crex-*` enriched DOM node into a typed descriptor.\r\n * Unknown `load`/`state` values are dropped (undefined) rather than trusted.\r\n */\r\nexport const parseEnrichmentDescriptor = (\r\n el: HTMLElement,\r\n): HtmlEnrichmentDescriptor => {\r\n const loadModeRaw = el.getAttribute(\"data-crex-load\") ?? undefined;\r\n const stateRaw = el.getAttribute(\"data-crex-state\") ?? undefined;\r\n\r\n return {\r\n elementId: el.id || undefined,\r\n loadMode:\r\n loadModeRaw && VALID_LOAD_MODES.has(loadModeRaw)\r\n ? (loadModeRaw as HtmlEnrichmentLoadMode)\r\n : undefined,\r\n endpoint: el.getAttribute(\"data-crex-endpoint\") ?? undefined,\r\n state:\r\n stateRaw && VALID_STATES.has(stateRaw)\r\n ? (stateRaw as HtmlEnrichmentState)\r\n : undefined,\r\n clientFilter: el.getAttribute(\"data-crex-client-filter\") ?? undefined,\r\n renderer: el.getAttribute(\"data-crex-renderer\") ?? undefined,\r\n resourceId: el.getAttribute(\"data-crex-id\") ?? undefined,\r\n params: parseParams(el),\r\n };\r\n};\r\n","/**\r\n * API version segment injected into resolved enrichment paths.\r\n * Kept as a code constant for now; promote to an env var when versioning\r\n * needs to vary per deployment (note: client-side use requires NEXT_PUBLIC_*).\r\n */\r\nexport const IDS_API_VERSION = \"v1\";\r\n\r\nconst capitalize = (s: string): string =>\r\n s ? s.charAt(0).toUpperCase() + s.slice(1) : s;\r\n\r\n/**\r\n * Convert a logical endpoint slug into a versioned API base path.\r\n *\r\n * Slug shape: `<area>-<resource>` (resource may be multi-word, dash-separated).\r\n * `ids-resources` -> `ids/v1/Resources`\r\n * `ids-info-units` -> `ids/v1/InfoUnits`\r\n *\r\n * Returns null when the slug does not match the expected shape.\r\n * The full path is later combined with the encoded resource id by the loader\r\n * and resolved against the API root by `CrexApi.buildUrl`.\r\n */\r\nexport const slugToPath = (\r\n slug: string,\r\n version: string = IDS_API_VERSION,\r\n): string | null => {\r\n const parts = slug.split(\"-\").filter(Boolean);\r\n if (parts.length < 2) return null;\r\n\r\n const [area, ...resourceParts] = parts;\r\n const resource = resourceParts.map(capitalize).join(\"\");\r\n if (!area || !resource) return null;\r\n\r\n return `${area}/${version}/${resource}`;\r\n};\r\n","export const baseClientRequest = async <T>(\r\n endpoint: string,\r\n queryParams?: Record<\r\n string,\r\n string | number | boolean | (string | number | boolean)[] | undefined\r\n >,\r\n options?: { signal?: AbortSignal },\r\n extra?: {\r\n responseType?: \"json\" | \"text\";\r\n authMode?: \"configured\" | \"anonymous\";\r\n },\r\n): Promise<T> => {\r\n const res = await fetch(`/api/rpc`, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify({\r\n method: \"CrexApi.execute\",\r\n params: {\r\n url: endpoint,\r\n method: \"GET\",\r\n params: queryParams,\r\n responseType: extra?.responseType,\r\n authMode: extra?.authMode,\r\n },\r\n }),\r\n credentials: \"include\",\r\n signal: options?.signal,\r\n });\r\n\r\n const json = await res.json();\r\n\r\n if (!res.ok) throw new Error(json.error || \"Unknown error\");\r\n\r\n return json.data;\r\n};\r\n","import type { LiteralModel, RenditionModel } from \"@c-rex/interfaces\";\r\nimport { baseClientRequest } from \"../base-client-request\";\r\nimport { slugToPath } from \"./endpoint-path\";\r\nimport type {\r\n HtmlEnrichmentDescriptor,\r\n HtmlEnrichmentEndpointResult,\r\n} from \"./types\";\r\n\r\n/**\r\n * Access-denied heuristic. This is NOT an HTTP auth failure — metadata may\r\n * resolve while the content rendition is licence-restricted. HTTP 403 is the\r\n * current signal until the IDS API confirms a canonical denied contract\r\n * (see HTML_RUNTIME_ENRICHMENT_SPEC.md → Open Questions).\r\n */\r\nconst isDeniedError = (err: unknown): boolean =>\r\n err instanceof Error && err.message.includes(\"403\");\r\n\r\n/**\r\n * Generic enrichment loader: resolves `data-crex-endpoint` (slug) + the\r\n * `data-crex-id` resource IRI into a single request and returns the raw HTML.\r\n *\r\n * Request shape: `{<area>/<version>/<Resource>}/view/{encodeURIComponent(id)}`\r\n * fetched as text. The `/view/` segment returns the rendered HTML rendition.\r\n * Works for any `<area>-<resource>` slug — no per-endpoint registration needed.\r\n */\r\nexport const loadEnrichmentHtml = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n): Promise<HtmlEnrichmentEndpointResult> => {\r\n if (!descriptor.endpoint || !descriptor.resourceId) {\r\n return { html: null, isDenied: false };\r\n }\r\n\r\n const basePath = slugToPath(descriptor.endpoint);\r\n if (!basePath) {\r\n return { html: null, isDenied: false };\r\n }\r\n\r\n const url = `${basePath}/view/${encodeURIComponent(descriptor.resourceId)}`;\r\n\r\n try {\r\n const html = await baseClientRequest<string>(\r\n url,\r\n descriptor.params,\r\n undefined,\r\n {\r\n responseType: \"text\",\r\n },\r\n );\r\n return { html, isDenied: false };\r\n } catch (err: unknown) {\r\n if (isDeniedError(err)) {\r\n return { html: null, isDenied: true };\r\n }\r\n throw err;\r\n }\r\n};\r\n\r\n/**\r\n * Loads a collection-style enrichment marker such as the GMP Startseite tile\r\n * container. Collection markers do not have a data-crex-id; their API query is\r\n * carried by data-crex-param-* attributes instead.\r\n */\r\nexport const loadEnrichmentCollection = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n): Promise<HtmlEnrichmentEndpointResult> => {\r\n if (!descriptor.endpoint) return { html: null, isDenied: false };\r\n\r\n const basePath = slugToPath(descriptor.endpoint);\r\n if (!basePath) return { html: null, isDenied: false };\r\n\r\n const params = { ...(descriptor.params ?? {}) };\r\n const fields = params.Fields;\r\n const hasRenditions = Array.isArray(fields)\r\n ? fields.some((field) => String(field).toLowerCase() === \"renditions\")\r\n : String(fields ?? \"\")\r\n .toLowerCase()\r\n .includes(\"renditions\");\r\n\r\n // Tile renderers need the rendition objects and their view links in the\r\n // collection response. Keep the marker's explicit params authoritative,\r\n // while filling in the two optional API flags when renditions were asked for.\r\n if (hasRenditions) {\r\n if (params.Embed === undefined) params.Embed = [\"renditions\"];\r\n if (params.Links === undefined) params.Links = true;\r\n }\r\n\r\n try {\r\n const isPublicGmpStartTiles =\r\n descriptor.renderer === \"GMPTilesRenderer\" &&\r\n basePath === \"iirds/v1/Fragments\";\r\n const data = isPublicGmpStartTiles\r\n ? await baseClientRequest<unknown>(basePath, params, undefined, {\r\n authMode: \"anonymous\",\r\n })\r\n : await baseClientRequest<unknown>(basePath, params);\r\n return { html: null, data, isDenied: false };\r\n } catch (err: unknown) {\r\n if (isDeniedError(err)) {\r\n return { html: null, isDenied: true };\r\n }\r\n throw err;\r\n }\r\n};\r\n\r\n/** Selects the individual-resource or collection loader from the marker. */\r\nexport const loadEnrichment = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n): Promise<HtmlEnrichmentEndpointResult> =>\r\n descriptor.resourceId\r\n ? loadEnrichmentHtml(descriptor)\r\n : loadEnrichmentCollection(descriptor);\r\n\r\n/**\r\n * Minimal shape shared by the iiRDS model endpoints used for onclick enrichment\r\n * (Fragments, InformationUnits, …). Kept loose so any model-with-renditions\r\n * endpoint works without per-endpoint typing.\r\n */\r\nexport type EnrichmentResourceModel = {\r\n /**\r\n * Root short id of the resource. Used to build in-app navigation targets\r\n * (e.g. swapping the topic id segment) instead of rendering the payload.\r\n */\r\n shortId?: string | null;\r\n renditions?: RenditionModel[] | null;\r\n titles?: LiteralModel[] | null;\r\n labels?: LiteralModel[] | null;\r\n /**\r\n * Content access state. The request itself can succeed (HTTP 200, metadata\r\n * present) while the content is licence-restricted — IDS signals that here\r\n * rather than via an HTTP error.\r\n */\r\n authorization?: { state?: string | null } | null;\r\n};\r\n\r\nexport type EnrichmentResourceResult = {\r\n data: EnrichmentResourceModel | null;\r\n isDenied: boolean;\r\n};\r\n\r\n/**\r\n * Generic onclick enrichment loader: resolves `data-crex-endpoint` (slug) + the\r\n * `data-crex-id` resource IRI and fetches the JSON model (e.g. a Fragment or\r\n * InformationUnit) for rendering inside a popover/modal. Endpoint resolution is\r\n * fully dynamic via `slugToPath` — no per-endpoint hardcoding.\r\n */\r\nexport const loadEnrichmentResource = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n queryParams?: Record<\r\n string,\r\n string | number | boolean | (string | number | boolean)[] | undefined\r\n >,\r\n): Promise<EnrichmentResourceResult> => {\r\n if (!descriptor.endpoint || !descriptor.resourceId) {\r\n return { data: null, isDenied: false };\r\n }\r\n\r\n const basePath = slugToPath(descriptor.endpoint);\r\n if (!basePath) {\r\n return { data: null, isDenied: false };\r\n }\r\n\r\n const url = `${basePath}/${encodeURIComponent(descriptor.resourceId)}`;\r\n\r\n try {\r\n const data = await baseClientRequest<EnrichmentResourceModel>(\r\n url,\r\n queryParams,\r\n );\r\n // Denied is carried in the (successful) payload, not as an HTTP error.\r\n const isDenied = data?.authorization?.state === \"denied\";\r\n return { data, isDenied };\r\n } catch (err: unknown) {\r\n if (isDeniedError(err)) {\r\n return { data: null, isDenied: true };\r\n }\r\n throw err;\r\n }\r\n};\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/html-enrichment/descriptor.ts","../../src/html-enrichment/endpoint-path.ts","../../src/html-enrichment/loader.ts","../../src/base-client-request.ts"],"sourcesContent":["import type {\r\n HtmlEnrichmentDescriptor,\r\n HtmlEnrichmentLoadMode,\r\n HtmlEnrichmentState,\r\n} from \"./types\";\r\n\r\nconst VALID_LOAD_MODES = new Set<string>([\"onload\", \"onappear\", \"onclick\"]);\r\nconst VALID_STATES = new Set<string>([\r\n \"unloaded\",\r\n \"loading\",\r\n \"loaded\",\r\n \"denied\",\r\n \"error\",\r\n]);\r\n\r\nconst toQueryParamName = (rawName: string): string => {\r\n const normalized = rawName.toLowerCase();\r\n if (normalized === \"restriction\") return \"Restrict\";\r\n\r\n return rawName\r\n .split(\"-\")\r\n .filter(Boolean)\r\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\r\n .join(\"\");\r\n};\r\n\r\nconst parseParamValue = (name: string, value: string): string | string[] => {\r\n // Restrict selectors use semicolons internally, so the complete selector is\r\n // one API value. Other marker params use semicolons as list separators.\r\n if (name === \"Restrict\") return value.trim();\r\n return value\r\n .split(\";\")\r\n .map((item) => item.trim())\r\n .filter(Boolean);\r\n};\r\n\r\nconst parseParams = (\r\n el: HTMLElement,\r\n): Record<string, string | string[]> | undefined => {\r\n const params: Record<string, string | string[]> = {};\r\n\r\n for (const attributeName of el.getAttributeNames()) {\r\n const prefix = \"data-crex-param-\";\r\n if (!attributeName.startsWith(prefix)) continue;\r\n\r\n const rawValue = el.getAttribute(attributeName);\r\n if (!rawValue) continue;\r\n const name = toQueryParamName(attributeName.slice(prefix.length));\r\n params[name] = parseParamValue(name, rawValue);\r\n }\r\n\r\n return Object.keys(params).length > 0 ? params : undefined;\r\n};\r\n\r\n/**\r\n * Convert a `data-crex-*` enriched DOM node into a typed descriptor.\r\n * Unknown `load`/`state` values are dropped (undefined) rather than trusted.\r\n */\r\nexport const parseEnrichmentDescriptor = (\r\n el: HTMLElement,\r\n): HtmlEnrichmentDescriptor => {\r\n const loadModeRaw = el.getAttribute(\"data-crex-load\") ?? undefined;\r\n const stateRaw = el.getAttribute(\"data-crex-state\") ?? undefined;\r\n\r\n return {\r\n elementId: el.id || undefined,\r\n loadMode:\r\n loadModeRaw && VALID_LOAD_MODES.has(loadModeRaw)\r\n ? (loadModeRaw as HtmlEnrichmentLoadMode)\r\n : undefined,\r\n endpoint: el.getAttribute(\"data-crex-endpoint\") ?? undefined,\r\n state:\r\n stateRaw && VALID_STATES.has(stateRaw)\r\n ? (stateRaw as HtmlEnrichmentState)\r\n : undefined,\r\n clientFilter: el.getAttribute(\"data-crex-client-filter\") ?? undefined,\r\n renderer: el.getAttribute(\"data-crex-renderer\") ?? undefined,\r\n resourceId: el.getAttribute(\"data-crex-id\") ?? undefined,\r\n params: parseParams(el),\r\n };\r\n};\r\n","/**\r\n * API version segment injected into resolved enrichment paths.\r\n * Kept as a code constant for now; promote to an env var when versioning\r\n * needs to vary per deployment (note: client-side use requires NEXT_PUBLIC_*).\r\n */\r\nexport const IDS_API_VERSION = \"v1\";\r\n\r\nconst capitalize = (s: string): string =>\r\n s ? s.charAt(0).toUpperCase() + s.slice(1) : s;\r\n\r\n/**\r\n * Convert a logical endpoint slug into a versioned API base path.\r\n *\r\n * Slug shape: `<area>-<resource>` (resource may be multi-word, dash-separated).\r\n * `ids-resources` -> `ids/v1/Resources`\r\n * `ids-info-units` -> `ids/v1/InfoUnits`\r\n *\r\n * Returns null when the slug does not match the expected shape.\r\n * The full path is later combined with the encoded resource id by the loader\r\n * and resolved against the API root by `CrexApi.buildUrl`.\r\n */\r\nexport const slugToPath = (\r\n slug: string,\r\n version: string = IDS_API_VERSION,\r\n): string | null => {\r\n const parts = slug.split(\"-\").filter(Boolean);\r\n if (parts.length < 2) return null;\r\n\r\n const [area, ...resourceParts] = parts;\r\n const resource = resourceParts.map(capitalize).join(\"\");\r\n if (!area || !resource) return null;\r\n\r\n return `${area}/${version}/${resource}`;\r\n};\r\n","import type { LiteralModel, RenditionModel } from \"@c-rex/interfaces\";\r\nimport { GMP_TILES_RENDERER } from \"@c-rex/constants\";\r\nimport { baseClientRequest } from \"../base-client-request\";\r\nimport { slugToPath } from \"./endpoint-path\";\r\nimport type {\r\n HtmlEnrichmentDescriptor,\r\n HtmlEnrichmentEndpointResult,\r\n} from \"./types\";\r\n\r\n/**\r\n * Access-denied heuristic. This is NOT an HTTP auth failure — metadata may\r\n * resolve while the content rendition is licence-restricted. HTTP 403 is the\r\n * current signal until the IDS API confirms a canonical denied contract\r\n * (see HTML_RUNTIME_ENRICHMENT_SPEC.md → Open Questions).\r\n */\r\nconst isDeniedError = (err: unknown): boolean =>\r\n err instanceof Error && err.message.includes(\"403\");\r\n\r\n/**\r\n * Generic enrichment loader: resolves `data-crex-endpoint` (slug) + the\r\n * `data-crex-id` resource IRI into a single request and returns the raw HTML.\r\n *\r\n * Request shape: `{<area>/<version>/<Resource>}/view/{encodeURIComponent(id)}`\r\n * fetched as text. The `/view/` segment returns the rendered HTML rendition.\r\n * Works for any `<area>-<resource>` slug — no per-endpoint registration needed.\r\n */\r\nexport const loadEnrichmentHtml = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n): Promise<HtmlEnrichmentEndpointResult> => {\r\n if (!descriptor.endpoint || !descriptor.resourceId) {\r\n return { html: null, isDenied: false };\r\n }\r\n\r\n const basePath = slugToPath(descriptor.endpoint);\r\n if (!basePath) {\r\n return { html: null, isDenied: false };\r\n }\r\n\r\n const url = `${basePath}/view/${encodeURIComponent(descriptor.resourceId)}`;\r\n\r\n try {\r\n const html = await baseClientRequest<string>(\r\n url,\r\n descriptor.params,\r\n undefined,\r\n {\r\n responseType: \"text\",\r\n },\r\n );\r\n return { html, isDenied: false };\r\n } catch (err: unknown) {\r\n if (isDeniedError(err)) {\r\n return { html: null, isDenied: true };\r\n }\r\n throw err;\r\n }\r\n};\r\n\r\n/**\r\n * Loads a collection-style enrichment marker such as the GMP Startseite tile\r\n * container. Collection markers do not have a data-crex-id; their API query is\r\n * carried by data-crex-param-* attributes instead.\r\n */\r\nexport const loadEnrichmentCollection = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n): Promise<HtmlEnrichmentEndpointResult> => {\r\n if (!descriptor.endpoint) return { html: null, isDenied: false };\r\n\r\n const basePath = slugToPath(descriptor.endpoint);\r\n if (!basePath) return { html: null, isDenied: false };\r\n\r\n const params = { ...(descriptor.params ?? {}) };\r\n const fields = params.Fields;\r\n const hasRenditions = Array.isArray(fields)\r\n ? fields.some((field) => String(field).toLowerCase() === \"renditions\")\r\n : String(fields ?? \"\")\r\n .toLowerCase()\r\n .includes(\"renditions\");\r\n\r\n // Tile renderers need the rendition objects and their view links in the\r\n // collection response. Keep the marker's explicit params authoritative,\r\n // while filling in the two optional API flags when renditions were asked for.\r\n if (hasRenditions) {\r\n if (params.Embed === undefined) params.Embed = [\"renditions\"];\r\n if (params.Links === undefined) params.Links = true;\r\n }\r\n\r\n try {\r\n const isPublicGmpStartTiles =\r\n descriptor.renderer === GMP_TILES_RENDERER &&\r\n basePath === \"iirds/v1/Fragments\";\r\n const data = isPublicGmpStartTiles\r\n ? await baseClientRequest<unknown>(basePath, params, undefined, {\r\n authMode: \"anonymous\",\r\n })\r\n : await baseClientRequest<unknown>(basePath, params);\r\n return { html: null, data, isDenied: false };\r\n } catch (err: unknown) {\r\n if (isDeniedError(err)) {\r\n return { html: null, isDenied: true };\r\n }\r\n throw err;\r\n }\r\n};\r\n\r\n/** Selects the individual-resource or collection loader from the marker. */\r\nexport const loadEnrichment = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n): Promise<HtmlEnrichmentEndpointResult> =>\r\n descriptor.resourceId\r\n ? loadEnrichmentHtml(descriptor)\r\n : loadEnrichmentCollection(descriptor);\r\n\r\n/**\r\n * Minimal shape shared by the iiRDS model endpoints used for onclick enrichment\r\n * (Fragments, InformationUnits, …). Kept loose so any model-with-renditions\r\n * endpoint works without per-endpoint typing.\r\n */\r\nexport type EnrichmentResourceModel = {\r\n /**\r\n * Root short id of the resource. Used to build in-app navigation targets\r\n * (e.g. swapping the topic id segment) instead of rendering the payload.\r\n */\r\n shortId?: string | null;\r\n renditions?: RenditionModel[] | null;\r\n titles?: LiteralModel[] | null;\r\n labels?: LiteralModel[] | null;\r\n /**\r\n * Content access state. The request itself can succeed (HTTP 200, metadata\r\n * present) while the content is licence-restricted — IDS signals that here\r\n * rather than via an HTTP error.\r\n */\r\n authorization?: { state?: string | null } | null;\r\n};\r\n\r\nexport type EnrichmentResourceResult = {\r\n data: EnrichmentResourceModel | null;\r\n isDenied: boolean;\r\n};\r\n\r\n/**\r\n * Generic onclick enrichment loader: resolves `data-crex-endpoint` (slug) + the\r\n * `data-crex-id` resource IRI and fetches the JSON model (e.g. a Fragment or\r\n * InformationUnit) for rendering inside a popover/modal. Endpoint resolution is\r\n * fully dynamic via `slugToPath` — no per-endpoint hardcoding.\r\n */\r\nexport const loadEnrichmentResource = async (\r\n descriptor: HtmlEnrichmentDescriptor,\r\n queryParams?: Record<\r\n string,\r\n string | number | boolean | (string | number | boolean)[] | undefined\r\n >,\r\n): Promise<EnrichmentResourceResult> => {\r\n if (!descriptor.endpoint || !descriptor.resourceId) {\r\n return { data: null, isDenied: false };\r\n }\r\n\r\n const basePath = slugToPath(descriptor.endpoint);\r\n if (!basePath) {\r\n return { data: null, isDenied: false };\r\n }\r\n\r\n const url = `${basePath}/${encodeURIComponent(descriptor.resourceId)}`;\r\n\r\n try {\r\n const data = await baseClientRequest<EnrichmentResourceModel>(\r\n url,\r\n queryParams,\r\n );\r\n // Denied is carried in the (successful) payload, not as an HTTP error.\r\n const isDenied = data?.authorization?.state === \"denied\";\r\n return { data, isDenied };\r\n } catch (err: unknown) {\r\n if (isDeniedError(err)) {\r\n return { data: null, isDenied: true };\r\n }\r\n throw err;\r\n }\r\n};\r\n","export const baseClientRequest = async <T>(\r\n endpoint: string,\r\n queryParams?: Record<\r\n string,\r\n string | number | boolean | (string | number | boolean)[] | undefined\r\n >,\r\n options?: { signal?: AbortSignal },\r\n extra?: {\r\n responseType?: \"json\" | \"text\";\r\n authMode?: \"configured\" | \"anonymous\";\r\n },\r\n): Promise<T> => {\r\n const res = await fetch(`/api/rpc`, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify({\r\n method: \"CrexApi.execute\",\r\n params: {\r\n url: endpoint,\r\n method: \"GET\",\r\n params: queryParams,\r\n responseType: extra?.responseType,\r\n authMode: extra?.authMode,\r\n },\r\n }),\r\n credentials: \"include\",\r\n signal: options?.signal,\r\n });\r\n\r\n const json = await res.json();\r\n\r\n if (!res.ok) throw new Error(json.error || \"Unknown error\");\r\n\r\n return json.data;\r\n};\r\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c-rex/services",
3
- "version": "0.3.0-build.59",
3
+ "version": "0.3.0-build.67",
4
4
  "files": [
5
5
  "dist"
6
6
  ],