@takuhon/api 1.3.0 → 1.4.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.
- package/NOTICE +1 -1
- package/dist/index.d.ts +209 -75
- package/dist/index.js +549 -124
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/error-envelope.ts","../src/public-app.ts","../src/html/build-html.ts","../src/html/html-helpers.ts","../src/html/brand-icons.ts","../src/locale-resolution.ts","../src/locale-prefix.ts","../src/index.ts","../src/admin/admin-api-app.ts","../src/admin/bearer.ts","../src/admin/origin.ts","../src/admin/admin-ui-app.ts","../src/admin/admin-html.ts","../src/admin/admin-asset-headers.ts","../src/admin/audit-logger.ts","../src/admin/cache-purger.ts","../src/html/site.ts","../src/html/cv-html.ts"],"sourcesContent":["import type { Context } from 'hono';\nimport type { ContentfulStatusCode } from 'hono/utils/http-status';\n\n/**\n * RFC 7807 problem type slugs used by takuhon. The 11 below are the\n * Spec-defined values (api.md §5.1); `methodNotAllowed` is added locally\n * for the 405 path that the Spec leaves unnamed.\n */\nexport const ERROR_SLUGS = {\n badRequest: 'bad-request',\n unauthorized: 'unauthorized',\n forbidden: 'forbidden',\n notFound: 'not-found',\n methodNotAllowed: 'method-not-allowed',\n conflict: 'conflict',\n payloadTooLarge: 'payload-too-large',\n unsupportedMediaType: 'unsupported-media-type',\n validationFailed: 'validation-failed',\n tooManyRequests: 'too-many-requests',\n internal: 'internal',\n serviceUnavailable: 'service-unavailable',\n} as const;\n\nexport type ErrorSlug = (typeof ERROR_SLUGS)[keyof typeof ERROR_SLUGS];\n\nconst TYPE_BASE = 'https://takuhon.org/errors';\n\nexport interface ProblemFieldError {\n path: string;\n message: string;\n}\n\nexport interface ProblemDetails {\n type: string;\n title: string;\n status: number;\n detail: string;\n instance: string;\n errors?: ProblemFieldError[];\n currentVersion?: string;\n}\n\nexport interface BuildProblemInput {\n slug: ErrorSlug;\n status: number;\n title: string;\n detail: string;\n instance: string;\n errors?: ProblemFieldError[];\n currentVersion?: string;\n}\n\nexport function buildProblem(input: BuildProblemInput): ProblemDetails {\n const out: ProblemDetails = {\n type: `${TYPE_BASE}/${input.slug}`,\n title: input.title,\n status: input.status,\n detail: input.detail,\n instance: input.instance,\n };\n if (input.errors !== undefined) out.errors = input.errors;\n if (input.currentVersion !== undefined) out.currentVersion = input.currentVersion;\n return out;\n}\n\nexport interface ProblemResponseInput {\n slug: ErrorSlug;\n status: ContentfulStatusCode;\n title: string;\n detail: string;\n errors?: ProblemFieldError[];\n currentVersion?: string;\n}\n\nexport function problemResponse(c: Context, input: ProblemResponseInput): Response {\n const body = buildProblem({\n slug: input.slug,\n status: input.status,\n title: input.title,\n detail: input.detail,\n instance: new URL(c.req.url).pathname,\n errors: input.errors,\n currentVersion: input.currentVersion,\n });\n return c.body(JSON.stringify(body), input.status, {\n 'content-type': 'application/problem+json; charset=utf-8',\n });\n}\n","import {\n NotFoundError,\n SCHEMA_VERSION,\n applyPublicPrivacyFilter,\n generateJsonLd,\n normalize,\n resolveLocale,\n schema,\n type ActivityStorage,\n type Takuhon,\n type TakuhonStorage,\n} from '@takuhon/core';\nimport { Hono } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse } from './error-envelope.js';\nimport { renderProfileHtml } from './html/build-html.js';\nimport { localePrefixGetPath, pathLocaleFromUrl } from './locale-prefix.js';\nimport { resolveRequestLocales } from './locale-resolution.js';\n\ndeclare module 'hono' {\n interface ContextVariableMap {\n /**\n * Set by the `/` handler when it embeds the contact widget, so the\n * security-headers middleware serves the Turnstile-allowing CSP variant for\n * that one response and the strict default everywhere else.\n */\n contactEnabled?: boolean;\n }\n}\n\nexport interface PublicAppDeps {\n storage: TakuhonStorage;\n /**\n * Returned when storage reports NotFoundError. Adapters that ship a\n * bundled example fixture (e.g. @takuhon/cloudflare) pass a thunk that\n * returns the validated document so initial-onboarding requests still\n * succeed before the first admin write.\n */\n fallback?: () => Takuhon;\n /**\n * Source of the synced developer-activity snapshot, exposed at\n * `GET /api/activity`. Optional, like the admin app's `assetStorage`:\n * deployments that don't sync activity leave it unset and the route\n * answers 404. The route also answers 404 while `settings.activity` is\n * not enabled in the profile, so disabling the feature stops serving a\n * previously synced snapshot immediately.\n */\n activityStorage?: ActivityStorage;\n /**\n * Path of the read-only MCP endpoint, advertised in\n * `/.well-known/takuhon.json` as `mcp`. Only set it on adapters that actually\n * serve MCP (e.g. @takuhon/cloudflare's `/mcp`); left unset, the discovery\n * document omits `mcp` so static / Vercel deployments don't point at an\n * endpoint they don't host.\n */\n mcpPath?: string;\n}\n\nconst FALLBACK_VERSION = 'bundled-fixture';\n\n// Cloudflare Turnstile (the contact widget's challenge) loads its script from,\n// renders its iframe on, and reports back to this single origin.\nconst TURNSTILE_ORIGIN = 'https://challenges.cloudflare.com';\n\n/**\n * Build the public Content-Security-Policy. With `contact` the `@takuhon/contact`\n * widget is embedded, so the Turnstile origin is added to `script-src` (its\n * api.js), `frame-src` (its challenge iframe), and `connect-src` (its\n * verification XHR). The widget's config travels as `data-*` attributes on the\n * external script, so `script-src` still needs no `'unsafe-inline'`. This\n * relaxation is applied ONLY to the HTML page that actually embeds the widget\n * (gated per-request in the `/` handler); every other route keeps the strict,\n * `'self'`-only policy below.\n */\nfunction buildPublicCsp(contact: boolean): string {\n return [\n \"default-src 'self'\",\n // `https:` lets the server-rendered profile page load remote avatar images\n // (the schema permits any https avatar URL, and `safeUrl` in the renderer\n // already blocks non-http(s) schemes); `data:` covers inline placeholders.\n \"img-src 'self' https: data:\",\n \"style-src 'self' 'unsafe-inline'\",\n contact ? `script-src 'self' ${TURNSTILE_ORIGIN}` : \"script-src 'self'\",\n \"font-src 'self'\",\n contact ? `connect-src 'self' ${TURNSTILE_ORIGIN}` : \"connect-src 'self'\",\n ...(contact ? [`frame-src ${TURNSTILE_ORIGIN}`] : []),\n \"frame-ancestors 'none'\",\n \"base-uri 'self'\",\n \"form-action 'self'\",\n 'upgrade-insecure-requests',\n ].join('; ');\n}\n\nconst PUBLIC_CSP = buildPublicCsp(false);\nconst PUBLIC_CSP_WITH_CONTACT = buildPublicCsp(true);\n\nasync function loadProfile(deps: PublicAppDeps): Promise<{ data: Takuhon; version: string }> {\n try {\n return await deps.storage.getProfile();\n } catch (e) {\n if (e instanceof NotFoundError && deps.fallback) {\n return { data: deps.fallback(), version: FALLBACK_VERSION };\n }\n throw e;\n }\n}\n\nexport function createPublicApp(deps: PublicAppDeps): Hono {\n // `getPath` strips a leading `/{locale}` prefix (e.g. `/ja/api/profile`\n // → `/api/profile`) so the flat routes below match locale-prefixed\n // URLs. The same function is applied on the adapter's top-level router,\n // because Hono's `route()` flattens this app's routes into the parent\n // and dispatches with the parent's `getPath` only — setting it here\n // alone would be honored for direct `app.fetch()` (tests) but not in\n // production. Handlers recover the locale token from the original URL\n // (`c.req.url`), which `getPath` does not mutate.\n const app = new Hono({ getPath: localePrefixGetPath });\n\n app.use('*', async (c, next) => {\n await next();\n const h = c.res.headers;\n h.set('strict-transport-security', 'max-age=63072000; includeSubDomains; preload');\n h.set('x-content-type-options', 'nosniff');\n h.set('x-frame-options', 'DENY');\n h.set('referrer-policy', 'strict-origin-when-cross-origin');\n h.set('permissions-policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');\n // The `/` handler sets `contactEnabled` only when it embeds the contact\n // widget, so the relaxed (Turnstile-allowing) policy is scoped to that one\n // page; every other route falls through to the strict default.\n h.set(\n 'content-security-policy',\n c.get('contactEnabled') ? PUBLIC_CSP_WITH_CONTACT : PUBLIC_CSP,\n );\n // Every route on this app is unauthenticated, read-only, and already\n // privacy-filtered, so the responses are safe to expose to any origin. This\n // is what lets browsers and AI tools fetch the profile / JSON-LD / discovery\n // document cross-origin — the public API's \"read anywhere\" goal. No cookies\n // or credentials are involved, so `*` is correct and `Vary: Origin` is not\n // needed; preflights are answered by the OPTIONS handler below. The admin\n // app (createAdminApiApp) is a separate Hono instance and is unaffected.\n h.set('access-control-allow-origin', '*');\n h.set('access-control-expose-headers', 'ETag');\n });\n\n app.onError((err, c) => {\n // This 500 body is public — and cross-origin readable once CORS is enabled —\n // so it must not echo internal exception text (storage/render errors can\n // carry implementation detail). Log the real error server-side and return a\n // generic, non-revealing detail.\n console.error('Public app request failed:', err);\n return problemResponse(c, {\n slug: ERROR_SLUGS.internal,\n status: 500,\n title: 'Internal Error',\n detail: 'An unexpected error occurred while handling the request.',\n });\n });\n\n app.notFound((c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: `No route matches ${new URL(c.req.url).pathname}.`,\n }),\n );\n\n // The public profile page. `getPath` has already stripped any `/{locale}`\n // prefix, so this single route serves `/` (default locale) and `/<locale>/`\n // alike; the locale token is recovered from the original URL. The same\n // load → normalize → resolveLocale → privacy-filter pipeline that backs\n // `/api/profile` feeds the pure `renderProfileHtml`, so the page a visitor —\n // and any crawler reading the embedded JSON-LD — sees matches the API and the\n // static `takuhon build` output exactly. Canonical / hreflang are derived\n // from this request's own origin, so they are correct without configuration.\n app.get('/', async (c) => {\n const { data, version } = await loadProfile(deps);\n const profile = normalize(data);\n const { locale, fallbackLocale } = resolveRequestLocales(\n c,\n profile.settings.availableLocales,\n pathLocaleFromUrl(c.req.url),\n );\n const localized = applyPublicPrivacyFilter(resolveLocale(profile, locale, fallbackLocale));\n\n const defaultLocale = profile.settings.defaultLocale;\n const locales = [...new Set([defaultLocale, ...profile.settings.availableLocales])];\n const current = localized.resolvedLocale;\n const origin = new URL(c.req.url).origin;\n const localePath = (l: string): string => (l === defaultLocale ? '/' : `/${l}/`);\n\n const snapshot =\n profile.settings.activity?.enabled === true && deps.activityStorage\n ? await deps.activityStorage.getActivitySnapshot()\n : null;\n\n // Embed the contact widget only when the owner has enabled it AND provided\n // the public Turnstile site key (without the key the widget cannot mount).\n // The secret / recipient / From live in adapter env and gate the POST\n // endpoint separately; the page only needs the public key. Setting the\n // context flag relaxes this response's CSP for the Turnstile origin.\n const contactSettings = profile.settings.contact;\n const contactSiteKey = contactSettings?.turnstileSiteKey?.trim();\n const contact =\n contactSettings?.enabled === true && contactSiteKey\n ? {\n siteKey: contactSiteKey,\n ...(contactSettings.endpoint ? { endpoint: contactSettings.endpoint } : {}),\n }\n : undefined;\n if (contact) c.set('contactEnabled', true);\n\n const html = renderProfileHtml({\n localized,\n canonicalUrl: `${origin}${localePath(current)}`,\n alternates: [\n ...locales.map((l) => ({ hreflang: l, href: `${origin}${localePath(l)}` })),\n { hreflang: 'x-default', href: `${origin}${localePath(defaultLocale)}` },\n ],\n localeNav: locales.map((l) => ({ locale: l, href: localePath(l), current: l === current })),\n jsonLd: profile.settings.enableJsonLd !== false,\n activitySnapshot: snapshot ?? undefined,\n contact,\n });\n\n c.header('etag', `\"${version}\"`);\n c.header('cache-control', 'public, max-age=300');\n c.header('vary', 'Accept-Language, Cookie');\n return c.html(html);\n });\n\n // Liveness probe. Intentionally storage-independent: it reports that the\n // worker itself is serving requests, not that the profile store is\n // reachable. A readiness probe that also checks storage can be added\n // later under a separate path if deployment platforms need it.\n app.get('/health', (c) => {\n c.header('cache-control', 'no-store');\n return c.json({ status: 'ok', schemaVersion: SCHEMA_VERSION });\n });\n\n app.get('/api/profile', async (c) => {\n const { data, version } = await loadProfile(deps);\n const { locale, fallbackLocale } = resolveRequestLocales(\n c,\n data.settings.availableLocales,\n pathLocaleFromUrl(c.req.url),\n );\n const localized = applyPublicPrivacyFilter(\n resolveLocale(normalize(data), locale, fallbackLocale),\n );\n const body = {\n data: localized,\n meta: {\n schemaVersion: localized.schemaVersion,\n locale: localized.resolvedLocale,\n updatedAt: localized.meta.updatedAt,\n },\n };\n c.header('etag', `\"${version}\"`);\n c.header('cache-control', 'private, max-age=300');\n c.header('vary', 'Accept-Language, Cookie');\n return c.json(body);\n });\n\n app.get('/api/schema', (c) => c.json(schema));\n\n // Public read of the synced developer-activity snapshot (design decision\n // §9-5: public, like /api/profile). The snapshot is already owner-derived\n // public metrics — no privacy filter applies — but the owner's opt-in is\n // re-checked on every read so disabling `settings.activity` takes effect\n // immediately, even while a stale snapshot is still stored. All three\n // unavailable states answer the same 404 problem.\n app.get('/api/activity', async (c) => {\n const unavailable = (): Response =>\n problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: 'No activity snapshot is available.',\n });\n\n if (!deps.activityStorage) return unavailable();\n const { data } = await loadProfile(deps);\n if (data.settings.activity?.enabled !== true) return unavailable();\n const snapshot = await deps.activityStorage.getActivitySnapshot();\n if (snapshot === null) return unavailable();\n\n c.header('cache-control', 'public, max-age=300');\n return c.json(snapshot);\n });\n\n app.get('/api/jsonld', async (c) => {\n const { data, version } = await loadProfile(deps);\n const { locale, fallbackLocale } = resolveRequestLocales(\n c,\n data.settings.availableLocales,\n pathLocaleFromUrl(c.req.url),\n );\n const localized = applyPublicPrivacyFilter(\n resolveLocale(normalize(data), locale, fallbackLocale),\n );\n const ld = generateJsonLd(localized);\n c.header('etag', `\"${version}\"`);\n c.header('cache-control', 'private, max-age=300');\n c.header('vary', 'Accept-Language, Cookie');\n c.header('content-type', 'application/ld+json; charset=utf-8');\n return c.body(JSON.stringify(ld));\n });\n\n app.get('/takuhon.json', async (c) => {\n const { data, version } = await loadProfile(deps);\n const filtered = applyPublicPrivacyFilter(data);\n c.header('etag', `\"${version}\"`);\n c.header('cache-control', 'public, max-age=300');\n return c.json(filtered);\n });\n\n app.get('/.well-known/takuhon.json', (c) => {\n c.header('cache-control', 'public, max-age=3600');\n return c.json({\n schemaVersion: SCHEMA_VERSION,\n schemaUrl: '/api/schema',\n profile: '/api/profile',\n jsonld: '/api/jsonld',\n export: '/api/admin/export',\n canonical: '/takuhon.json',\n // Only advertised when the adapter serves MCP (see PublicAppDeps.mcpPath).\n ...(deps.mcpPath !== undefined ? { mcp: deps.mcpPath } : {}),\n });\n });\n\n // CORS preflight for cross-origin reads. The actual GET responses carry\n // `Access-Control-Allow-Origin` via the middleware above; this answers the\n // preflight a browser sends before a non-simple cross-origin request (a\n // simple GET needs no preflight). Without this, OPTIONS would fall through to\n // the 404 handler and the preflight would fail.\n app.options('*', (c) => {\n c.header('access-control-allow-methods', 'GET, HEAD, OPTIONS');\n c.header('access-control-allow-headers', c.req.header('access-control-request-headers') ?? '*');\n c.header('access-control-max-age', '86400');\n return c.body(null, 204);\n });\n\n app.on(['POST', 'PUT', 'PATCH', 'DELETE'], '*', (c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.methodNotAllowed,\n status: 405,\n title: 'Method Not Allowed',\n detail: `${c.req.method} ${new URL(c.req.url).pathname} is not supported on the public app.`,\n }),\n );\n\n return app;\n}\n","/**\n * Pure HTML rendering for `takuhon build`.\n *\n * {@link renderProfileHtml} turns one locale-resolved {@link LocalizedTakuhon}\n * into a complete, self-contained static HTML document: semantic markup for\n * every profile section, an inline stylesheet, optional Schema.org JSON-LD,\n * and the `<head>` metadata (`<title>`, description, canonical, hreflang\n * alternates) the caller supplies.\n *\n * This is a deliberately separate, simpler surface from the React\n * `@takuhon/ui` (which is delivered as CSS-Modules components needing a\n * bundler). It reuses only `@takuhon/core` and has no DOM/browser dependency,\n * so it renders in plain Node and is unit-testable as a pure string function.\n *\n * Security: every piece of profile-derived text is escaped before it reaches\n * the markup ({@link escapeHtml}), and the JSON-LD payload is `<`/`>`/`&`\n * unicode-escaped so it cannot break out of its `<script>` element.\n */\n\nimport { generateJsonLd, renderActivitySvg } from '@takuhon/core';\nimport type {\n ActivitySnapshot,\n AppearanceColors,\n AppearanceSettings,\n LocalizedTakuhon,\n} from '@takuhon/core';\n\nimport { brandIconSvg } from './brand-icons.js';\nimport { dateRange, escapeHtml, nonEmpty, safeUrl } from './html-helpers.js';\n\n// Re-exported for existing importers (e.g. dev-command, tests) that pull\n// `escapeHtml` from this module; the implementation now lives in html-helpers.\nexport { escapeHtml } from './html-helpers.js';\n\ntype LocalizedProfile = LocalizedTakuhon['profile'];\n\n/** One entry in the human-facing locale switcher. */\nexport interface LocaleLink {\n locale: string;\n href: string;\n current: boolean;\n}\n\n/** One `<link rel=\"alternate\" hreflang>` entry. */\nexport interface Alternate {\n hreflang: string;\n href: string;\n}\n\nexport interface RenderInput {\n /** The locale-resolved document to render. */\n localized: LocalizedTakuhon;\n /** Absolute canonical URL for this page (only when `--base-url` was given). */\n canonicalUrl?: string;\n /** hreflang alternates (empty when no base URL is available). */\n alternates: readonly Alternate[];\n /** Human locale switcher links (rendered only when more than one locale). */\n localeNav: readonly LocaleLink[];\n /** Whether to emit Schema.org JSON-LD (mirrors `settings.enableJsonLd`). */\n jsonLd: boolean;\n /**\n * Synced developer-activity snapshot, rendered as a self-owned inline SVG\n * section. The caller gates it on `settings.activity.enabled`; an absent (or\n * metric-less) snapshot omits the section entirely.\n */\n activitySnapshot?: ActivitySnapshot;\n /**\n * When set, embed the `@takuhon/contact` widget: a `<link>` to its stylesheet\n * in `<head>` and a deferred `<script>` whose config travels as `data-*`\n * attributes (no inline script, so the page CSP needs no `'unsafe-inline'`).\n * The caller gates it on `settings.contact.enabled` and a present site key;\n * the adapter is responsible for serving `/contact-widget.{js,css}` and for\n * relaxing its CSP to allow the Turnstile origin.\n */\n contact?: { siteKey: string; endpoint?: string };\n}\n\n/** Unicode-escape `<`, `>`, `&` so a JSON-LD payload cannot break out of `<script>`. */\nfunction escapeJsonLd(json: string): string {\n return json.replace(/</g, '\\\\u003c').replace(/>/g, '\\\\u003e').replace(/&/g, '\\\\u0026');\n}\n\n/**\n * Overridable design tokens (light). Every color and the font the renderer uses\n * is a named `--takuhon-*` custom property with a default here; owners re-skin\n * the page by overriding a subset via `settings.appearance` ({@link\n * buildTokenCss}). The static rules reference only these variables (and the\n * internal tokens below), never hard-coded colors or fonts, so an override\n * propagates everywhere.\n */\nconst DEFAULT_TOKENS: Record<string, string> = {\n '--takuhon-color-bg': '#ffffff',\n '--takuhon-color-surface': '#f6f7f9',\n '--takuhon-color-text': '#1f2933',\n '--takuhon-color-text-muted': '#52606d',\n '--takuhon-color-border': '#d8dee7',\n '--takuhon-color-primary': '#2563eb',\n '--takuhon-color-primary-contrast': '#ffffff',\n '--takuhon-color-accent': '#4f46e5',\n '--takuhon-font-family':\n \"system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif\",\n};\n\n/**\n * Default dark palette, applied under `prefers-color-scheme: dark`. Only colors\n * flip in dark mode (the internal scale tokens are shared). Owner `colorsDark`\n * overrides merge over these, exactly as `colors` merges over the light set.\n */\nconst DEFAULT_TOKENS_DARK: Record<string, string> = {\n '--takuhon-color-bg': '#0f172a',\n '--takuhon-color-surface': '#1e293b',\n '--takuhon-color-text': '#e2e8f0',\n '--takuhon-color-text-muted': '#94a3b8',\n '--takuhon-color-border': '#334155',\n '--takuhon-color-primary': '#60a5fa',\n '--takuhon-color-primary-contrast': '#0f172a',\n '--takuhon-color-accent': '#818cf8',\n};\n\n/**\n * Internal design-scale tokens (spacing, radius, type scale, layout). These are\n * NOT part of the `settings.appearance` override contract — they are the\n * renderer's own layout vocabulary — so they are emitted in `:root` but not\n * exposed as overridable schema keys. Kept in sync between light and dark.\n */\nconst INTERNAL_TOKENS: Record<string, string> = {\n '--takuhon-space-1': '4px',\n '--takuhon-space-2': '8px',\n '--takuhon-space-3': '12px',\n '--takuhon-space-4': '16px',\n '--takuhon-space-5': '24px',\n '--takuhon-space-6': '32px',\n '--takuhon-radius-sm': '6px',\n '--takuhon-radius-md': '12px',\n '--takuhon-radius-full': '9999px',\n '--takuhon-tap-target': '44px',\n '--takuhon-font-size-sm': '14px',\n '--takuhon-font-size-base': '16px',\n '--takuhon-font-size-lg': '18px',\n '--takuhon-font-size-xl': '22px',\n '--takuhon-font-size-2xl': '28px',\n '--takuhon-line-height': '1.7',\n '--takuhon-max-content-width': '720px',\n};\n\n/** Map each `AppearanceColors` key to the CSS custom property it overrides. */\nconst COLOR_TOKEN_VARS: Record<keyof AppearanceColors, string> = {\n bg: '--takuhon-color-bg',\n surface: '--takuhon-color-surface',\n text: '--takuhon-color-text',\n textMuted: '--takuhon-color-text-muted',\n border: '--takuhon-color-border',\n accent: '--takuhon-color-accent',\n primary: '--takuhon-color-primary',\n primaryContrast: '--takuhon-color-primary-contrast',\n};\n\n/**\n * Defense in depth. The schema pattern-constrains these values, but data can\n * reach the renderer unvalidated (pre-1.2.0 documents, adapters that skip\n * `validate()`), so the renderer re-sanitizes every token with an allowlist\n * that mirrors the schema. An unsafe, non-string, or empty value is dropped and\n * the built-in default stands.\n *\n * Colors accept only a hex value, a bare keyword (named colors / currentColor /\n * transparent), or a known color function — never `url()`, `image-set()`,\n * `var()`, or anything else that could trigger an external request or escape\n * the inline `<style>`. A permissive \"any char but `;{}<>`\" filter is NOT\n * enough: `url(//evil/x.png)` carries none of those characters yet would make\n * the page fetch a third-party resource. These patterns mirror `CssColor` and\n * the font-family pattern in `@takuhon/core`'s takuhon.schema.json — keep them\n * in sync.\n */\nconst SAFE_COLOR =\n /^(?:#[0-9A-Fa-f]{3,8}|[A-Za-z]+|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([A-Za-z0-9.,%/\\s-]*\\))$/;\nconst SAFE_FONT = /^[A-Za-z0-9\\s,'\"._-]+$/;\n\nfunction safeValue(value: unknown, pattern: RegExp, maxLength: number): string | undefined {\n if (typeof value !== 'string') return undefined;\n const v = value.trim();\n if (v === '' || v.length > maxLength || !pattern.test(v)) return undefined;\n return v;\n}\n\nconst safeColor = (value: unknown): string | undefined => safeValue(value, SAFE_COLOR, 64);\nconst safeFont = (value: unknown): string | undefined => safeValue(value, SAFE_FONT, 256);\n\n/** Sanitized `[cssVar, value]` pairs for the color overrides that are present. */\nfunction colorOverrides(colors: AppearanceColors | undefined): [string, string][] {\n if (!colors) return [];\n const out: [string, string][] = [];\n for (const key of Object.keys(COLOR_TOKEN_VARS) as (keyof AppearanceColors)[]) {\n const safe = safeColor(colors[key]);\n if (safe !== undefined) out.push([COLOR_TOKEN_VARS[key], safe]);\n }\n return out;\n}\n\n/** Serialize `[var, value]` pairs into a `:root{…}` declaration block. */\nfunction rootBlock(pairs: Iterable<[string, string]>): string {\n return `:root{${[...pairs].map(([k, v]) => `${k}:${v}`).join(';')}}`;\n}\n\n/**\n * Build the token stylesheet:\n * - `:root` = internal scale tokens + the light color/font defaults, with any\n * owner `colors`/`fontFamily` overrides merged on top.\n * - a `prefers-color-scheme: dark` block = the default dark palette with any\n * owner `colorsDark` overrides merged on top.\n *\n * Only the fixed set of named tokens is ever emitted — never arbitrary CSS —\n * and every overridable value is sanitized ({@link safeColor} / {@link\n * safeFont}), so a value can neither escape the inline `<style>` nor trigger an\n * external request.\n */\nfunction buildTokenCss(appearance: AppearanceSettings | undefined): string {\n const light = new Map<string, string>([\n ...Object.entries(INTERNAL_TOKENS),\n ...Object.entries(DEFAULT_TOKENS),\n ]);\n if (appearance) {\n const font = safeFont(appearance.fontFamily);\n if (font !== undefined) light.set('--takuhon-font-family', font);\n for (const [cssVar, value] of colorOverrides(appearance.colors)) {\n light.set(cssVar, value);\n }\n }\n\n const dark = new Map<string, string>(Object.entries(DEFAULT_TOKENS_DARK));\n for (const [cssVar, value] of colorOverrides(appearance?.colorsDark)) {\n dark.set(cssVar, value);\n }\n\n return `${rootBlock(light)}\\n@media (prefers-color-scheme:dark){${rootBlock(dark)}}`;\n}\n\nconst CSS = `*{box-sizing:border-box}\nhtml{font-size:100%}\nbody{margin:0;color:var(--takuhon-color-text);background:var(--takuhon-color-bg);font-family:var(--takuhon-font-family);font-size:var(--takuhon-font-size-base);line-height:var(--takuhon-line-height);-webkit-text-size-adjust:100%}\nmain{max-width:var(--takuhon-max-content-width);margin:0 auto;padding:var(--takuhon-space-6) var(--takuhon-space-4)}\na{color:var(--takuhon-color-primary)}\na:focus-visible{outline:2px solid var(--takuhon-color-accent);outline-offset:2px;border-radius:var(--takuhon-radius-sm)}\nh1{font-size:var(--takuhon-font-size-2xl);font-weight:700;line-height:1.2;margin:0 0 var(--takuhon-space-2)}\nh2{font-size:var(--takuhon-font-size-xl);margin:0 0 var(--takuhon-space-3);padding-bottom:var(--takuhon-space-2);border-bottom:1px solid var(--takuhon-color-border)}\nh3{font-size:var(--takuhon-font-size-lg);font-weight:600;margin:0}\nheader{margin-bottom:var(--takuhon-space-6);display:flow-root}\nheader .avatar{width:96px;height:96px;border-radius:var(--takuhon-radius-full);object-fit:cover;float:left;margin:0 var(--takuhon-space-3) var(--takuhon-space-3) 0;shape-outside:circle();border:1px solid var(--takuhon-color-border)}\n.tagline{font-size:var(--takuhon-font-size-lg);color:var(--takuhon-color-text-muted);margin:0 0 var(--takuhon-space-2)}\n.location{font-size:var(--takuhon-font-size-sm);color:var(--takuhon-color-text-muted);margin:0}\n.bio{margin:var(--takuhon-space-3) 0 0}\nsection{margin:0 0 var(--takuhon-space-6)}\nul{padding:0;margin:0;list-style:none}\n.entries>li{margin:0 0 var(--takuhon-space-4)}\n.entries--timeline>li{position:relative;display:flex;flex-direction:column;margin:0 0 0 var(--takuhon-space-2);padding:0 0 var(--takuhon-space-5) var(--takuhon-space-4);border-left:2px solid var(--takuhon-color-border)}\n.entries--timeline>li:last-child{padding-bottom:0}\n.entries--timeline>li::before{content:\"\";position:absolute;left:-7px;top:6px;width:12px;height:12px;border-radius:var(--takuhon-radius-full);background:var(--takuhon-color-primary)}\n.entries--timeline>li.is-current::before{background:var(--takuhon-color-accent)}\n.entries--timeline .meta{order:-1;margin:0 0 var(--takuhon-space-1)}\n.entries--cards{display:grid;gap:var(--takuhon-space-3)}\n.entries--cards>li{margin:0;padding:var(--takuhon-space-4);border:1px solid var(--takuhon-color-border);border-radius:var(--takuhon-radius-md);background:var(--takuhon-color-surface)}\n.entries--cards>li.is-highlighted{border-color:var(--takuhon-color-accent)}\n.sub{margin:var(--takuhon-space-1) 0 0;font-weight:600}\n.meta{margin:var(--takuhon-space-1) 0 0;color:var(--takuhon-color-text-muted);font-size:var(--takuhon-font-size-sm)}\n.featured-links,.other-links{margin:0 0 var(--takuhon-space-6)}\n.featured-links>ul,.other-links>ul{list-style:none;padding:0;margin:0;display:grid;gap:var(--takuhon-space-2)}\n.featured-links>ul{grid-template-columns:repeat(auto-fill,minmax(220px,1fr))}\n.featured-links a,.other-links a{display:flex;align-items:center;gap:var(--takuhon-space-2);min-height:var(--takuhon-tap-target);padding:var(--takuhon-space-2) var(--takuhon-space-3);background:var(--takuhon-color-surface);color:var(--takuhon-color-text);text-decoration:none;border:1px solid var(--takuhon-color-border);border-radius:var(--takuhon-radius-md)}\n.featured-links a:hover,.other-links a:hover{border-color:var(--takuhon-color-accent)}\n.link-main{display:inline-flex;align-items:center;gap:var(--takuhon-space-2);min-width:0}\n.brand-icon{width:1.15em;height:1.15em;flex:none;opacity:.85}\n.skills,.tags{display:flex;flex-wrap:wrap;gap:var(--takuhon-space-2)}\n.skills>li,.tags>li{background:var(--takuhon-color-surface);border:1px solid var(--takuhon-color-border);border-radius:var(--takuhon-radius-full);padding:var(--takuhon-space-1) var(--takuhon-space-3);font-size:var(--takuhon-font-size-sm)}\n.skills-groups{display:grid;gap:var(--takuhon-space-4)}\n.skill-group h3{font-size:var(--takuhon-font-size-base);margin:0 0 var(--takuhon-space-2);color:var(--takuhon-color-text-muted);text-transform:uppercase;letter-spacing:.04em}\n.rec{margin:0 0 var(--takuhon-space-4)}\n.rec blockquote{margin:0;padding-left:var(--takuhon-space-3);border-left:3px solid var(--takuhon-color-border)}\n.rec figcaption{color:var(--takuhon-color-text-muted);font-size:var(--takuhon-font-size-sm);margin-top:var(--takuhon-space-2)}\nnav.locales{display:flex;gap:var(--takuhon-space-3);margin-bottom:var(--takuhon-space-4);font-size:var(--takuhon-font-size-sm)}\n.activity svg{max-width:100%;height:auto}\nfooter.powered{max-width:var(--takuhon-max-content-width);margin:0 auto;padding:var(--takuhon-space-5) var(--takuhon-space-4);color:var(--takuhon-color-text-muted);font-size:var(--takuhon-font-size-sm)}`;\n\ninterface EntryView {\n heading: string;\n sub?: string;\n dates?: string;\n body?: string;\n url?: string;\n tags?: readonly string[];\n /**\n * Timeline-variant \"ongoing\" marker (from `career.isCurrent` etc.) → an\n * `is-current` class on the `<li>` so the timeline dot uses the accent color.\n */\n current?: boolean;\n /**\n * Card-variant highlight (from `project.highlighted`) → an `is-highlighted`\n * class on the `<li>` so the card gets an accent border.\n */\n highlighted?: boolean;\n}\n\n/**\n * Layout variant for a section's entry list. Undefined = the default flat list;\n * `timeline` decorates each `<li>` as a dotted left-border timeline row (dates\n * float to the top via CSS order); `cards` lays the entries out as bordered\n * surface cards (intentionally single-column within the reading-width column —\n * long descriptions stay readable — not a responsive multi-column grid). Only\n * the container/`<li>` decoration differs — the inner {@link renderEntry}\n * markup is identical across all three.\n */\ntype EntriesVariant = 'timeline' | 'cards';\n\nfunction renderEntry(entry: EntryView): string {\n const href = entry.url ? safeUrl(entry.url) : undefined;\n const heading = href\n ? `<a href=\"${escapeHtml(href)}\">${escapeHtml(entry.heading)}</a>`\n : escapeHtml(entry.heading);\n const parts = [`<h3>${heading}</h3>`];\n if (entry.sub) parts.push(`<p class=\"sub\">${escapeHtml(entry.sub)}</p>`);\n // `entry.dates` is already an escaped HTML fragment from `dateRange` (localized\n // <time> elements), so it is inserted raw rather than re-escaped.\n // NOTE: the `timeline` variant hoists this `.meta` above the heading via CSS\n // `order:-1`, which only works while `.meta` stays a *direct child* of the\n // entry `<li>`. Keep these entry parts as flat siblings — do not wrap them in\n // a container div, or the date-first timeline ordering silently breaks.\n if (entry.dates) parts.push(`<p class=\"meta\">${entry.dates}</p>`);\n if (entry.body) parts.push(`<p>${escapeHtml(entry.body)}</p>`);\n if (entry.tags && entry.tags.length > 0) {\n parts.push(\n `<ul class=\"tags\">${entry.tags.map((t) => `<li>${escapeHtml(t)}</li>`).join('')}</ul>`,\n );\n }\n // Both markers come from real schema booleans; only the relevant one is ever\n // set per section (`current` for careers, `highlighted` for projects).\n const cls = [entry.current ? 'is-current' : '', entry.highlighted ? 'is-highlighted' : '']\n .filter(Boolean)\n .join(' ');\n return `<li${cls ? ` class=\"${cls}\"` : ''}>${parts.join('')}</li>`;\n}\n\n/** Render a `<section>` of entries, or `''` when there are none. */\nfunction entryList(title: string, entries: readonly EntryView[], variant?: EntriesVariant): string {\n if (entries.length === 0) return '';\n const cls = variant ? `entries entries--${variant}` : 'entries';\n return `<section><h2>${escapeHtml(title)}</h2><ul class=\"${cls}\">${entries\n .map(renderEntry)\n .join('')}</ul></section>`;\n}\n\nfunction renderHeader(p: LocalizedProfile): string {\n const parts: string[] = [];\n const avatarSrc = p.avatar?.url ? safeUrl(p.avatar.url) : undefined;\n if (avatarSrc) {\n parts.push(\n `<img class=\"avatar\" src=\"${escapeHtml(avatarSrc)}\" alt=\"${escapeHtml(p.avatar?.alt ?? '')}\">`,\n );\n }\n parts.push(`<h1>${escapeHtml(p.displayName)}</h1>`);\n if (p.tagline) parts.push(`<p class=\"tagline\">${escapeHtml(p.tagline)}</p>`);\n if (p.location?.display) parts.push(`<p class=\"location\">${escapeHtml(p.location.display)}</p>`);\n if (p.bio) parts.push(`<p class=\"bio\">${escapeHtml(p.bio)}</p>`);\n return `<header>${parts.join('')}</header>`;\n}\n\ntype LocalizedLink = LocalizedTakuhon['links'][number];\n\n/** Render one link as a pill: brand glyph (when the type has one) + label. */\nfunction renderLinkItem(link: LocalizedLink): string {\n const label = escapeHtml(link.label ?? link.url);\n const main = `<span class=\"link-main\">${brandIconSvg(link.type)}<span>${label}</span></span>`;\n const href = safeUrl(link.url);\n // rel=\"me\" declares these as the owner's own profiles (IndieWeb / Mastodon\n // verification); noopener hardens the external navigation.\n return href\n ? `<li><a href=\"${escapeHtml(href)}\" rel=\"me noopener\">${main}</a></li>`\n : `<li>${main}</li>`;\n}\n\n/** Ascending by `order` (absent sorts as 0), preserving input order on ties. */\nfunction byOrder(a: LocalizedLink, b: LocalizedLink): number {\n return (a.order ?? 0) - (b.order ?? 0);\n}\n\n/**\n * Render the links as two groups — featured first, then the rest — each an\n * ordered pill list with an inline brand glyph for recognized types. Splitting\n * on `featured` and sorting on `order` uses only existing schema fields; an\n * empty group is omitted.\n */\nfunction renderLinks(links: LocalizedTakuhon['links']): string {\n if (links.length === 0) return '';\n const featured = links.filter((l) => l.featured === true).sort(byOrder);\n const others = links.filter((l) => l.featured !== true).sort(byOrder);\n const group = (cls: string, ariaLabel: string, items: LocalizedLink[]): string =>\n items.length === 0\n ? ''\n : `<nav class=\"${cls}\" aria-label=\"${ariaLabel}\"><ul>${items.map(renderLinkItem).join('')}</ul></nav>`;\n return [\n group('featured-links', 'Featured links', featured),\n group('other-links', 'Links', others),\n ]\n .filter(Boolean)\n .join('\\n');\n}\n\ntype LocalizedSkill = LocalizedTakuhon['skills'][number];\n\n/**\n * Render the Skills section. With no `settings.skillCategories` it stays a flat\n * chip list (the default). When categories are configured, skills are grouped\n * by their `category` under the configured localized headings in declared\n * order; any category present on a skill but not configured renders after, with\n * its raw key as the heading; and uncategorized skills fall into a final,\n * heading-less group — so no skill is ever dropped.\n */\nfunction renderSkills(\n skills: LocalizedTakuhon['skills'],\n categories: LocalizedTakuhon['settings']['skillCategories'],\n): string {\n if (skills.length === 0) return '';\n const chips = (list: readonly LocalizedSkill[]): string =>\n `<ul class=\"skills\">${list.map((s) => `<li>${escapeHtml(s.label)}</li>`).join('')}</ul>`;\n\n if (!categories || categories.length === 0) {\n return `<section><h2>Skills</h2>${chips(skills)}</section>`;\n }\n\n // Bucket by category, preserving input order within each bucket. `category`\n // has minLength 1 in the schema, so '' is a safe marker for \"uncategorized\".\n const UNCAT = '';\n const buckets = new Map<string, LocalizedSkill[]>();\n for (const s of skills) {\n const key = s.category ?? UNCAT;\n const arr = buckets.get(key) ?? [];\n arr.push(s);\n buckets.set(key, arr);\n }\n\n const group = (heading: string | undefined, list: readonly LocalizedSkill[]): string =>\n `<div class=\"skill-group\">${heading !== undefined ? `<h3>${escapeHtml(heading)}</h3>` : ''}${chips(list)}</div>`;\n\n const seen = new Set<string>();\n const groups: string[] = [];\n for (const cat of categories) {\n const list = buckets.get(cat.id);\n if (!list || list.length === 0) continue;\n seen.add(cat.id);\n groups.push(group(cat.label, list));\n }\n for (const [key, list] of buckets) {\n if (key === UNCAT || seen.has(key)) continue;\n groups.push(group(key, list));\n }\n const uncategorized = buckets.get(UNCAT);\n if (uncategorized && uncategorized.length > 0) groups.push(group(undefined, uncategorized));\n\n return `<section><h2>Skills</h2><div class=\"skills-groups\">${groups.join('')}</div></section>`;\n}\n\nfunction renderLanguages(languages: LocalizedTakuhon['languages']): string {\n if (languages.length === 0) return '';\n const items = languages\n .map((l) => `<li>${escapeHtml(`${l.displayName ?? l.language} — ${l.proficiency}`)}</li>`)\n .join('');\n return `<section><h2>Languages</h2><ul class=\"entries\">${items}</ul></section>`;\n}\n\nfunction renderRecommendations(recs: LocalizedTakuhon['recommendations']): string {\n if (recs.length === 0) return '';\n const items = recs\n .map((r) => {\n const authorHref = r.author.url ? safeUrl(r.author.url) : undefined;\n const name = authorHref\n ? `<a href=\"${escapeHtml(authorHref)}\">${escapeHtml(r.author.name)}</a>`\n : escapeHtml(r.author.name);\n const caption = [name, r.author.headline ? escapeHtml(r.author.headline) : '']\n .filter(Boolean)\n .join(', ');\n const rel = r.relationship ? ` (${escapeHtml(r.relationship)})` : '';\n return `<figure class=\"rec\"><blockquote>${escapeHtml(r.body)}</blockquote><figcaption>— ${caption}${rel}</figcaption></figure>`;\n })\n .join('');\n return `<section><h2>Recommendations</h2>${items}</section>`;\n}\n\nfunction renderContact(contact: LocalizedTakuhon['contact']): string {\n const items: string[] = [];\n if (contact.email) {\n items.push(\n `<li><a href=\"mailto:${escapeHtml(contact.email)}\">${escapeHtml(contact.email)}</a></li>`,\n );\n }\n const formHref = contact.formUrl ? safeUrl(contact.formUrl) : undefined;\n if (formHref) {\n items.push(`<li><a href=\"${escapeHtml(formHref)}\">Contact form</a></li>`);\n }\n if (items.length === 0) return '';\n return `<section><h2>Contact</h2><ul class=\"entries\">${items.join('')}</ul></section>`;\n}\n\n/**\n * Render the developer-activity section from the synced snapshot, or `''`\n * when there is none (or it carries no metric data). The SVG is generated by\n * `@takuhon/core` from stored numbers only — no external badge image — so the\n * page works under an `img-src 'self'` CSP.\n */\nfunction renderActivity(snapshot: ActivitySnapshot | undefined): string {\n if (!snapshot) return '';\n const svg = renderActivitySvg(snapshot);\n if (svg === '') return '';\n return `<section class=\"activity\"><h2>Activity</h2>${svg}</section>`;\n}\n\nfunction renderJsonLdScript(data: LocalizedTakuhon): string {\n const payload = JSON.stringify(generateJsonLd(data));\n return `<script type=\"application/ld+json\">${escapeJsonLd(payload)}</script>`;\n}\n\nfunction renderLocaleNav(localeNav: readonly LocaleLink[]): string {\n const items = localeNav\n .map((l) =>\n l.current\n ? `<span aria-current=\"true\">${escapeHtml(l.locale)}</span>`\n : `<a href=\"${escapeHtml(l.href)}\">${escapeHtml(l.locale)}</a>`,\n )\n .join('');\n return `<nav class=\"locales\" aria-label=\"Language\">${items}</nav>`;\n}\n\n/** Render a complete static HTML document for one locale-resolved profile. */\nexport function renderProfileHtml(input: RenderInput): string {\n const d = input.localized;\n const p = d.profile;\n const description = p.tagline ?? p.bio ?? '';\n\n const head = [\n '<meta charset=\"utf-8\">',\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">',\n `<title>${escapeHtml(p.displayName)}</title>`,\n description\n ? `<meta name=\"description\" content=\"${escapeHtml(description.slice(0, 300))}\">`\n : '',\n input.canonicalUrl ? `<link rel=\"canonical\" href=\"${escapeHtml(input.canonicalUrl)}\">` : '',\n ...input.alternates.map(\n (a) =>\n `<link rel=\"alternate\" hreflang=\"${escapeHtml(a.hreflang)}\" href=\"${escapeHtml(a.href)}\">`,\n ),\n input.jsonLd ? renderJsonLdScript(d) : '',\n input.contact ? '<link rel=\"stylesheet\" href=\"/contact-widget.css\">' : '',\n `<style>${buildTokenCss(d.settings.appearance)}\\n${CSS}</style>`,\n ]\n .filter(Boolean)\n .join('\\n ');\n\n const body = [\n input.localeNav.length > 1 ? renderLocaleNav(input.localeNav) : '',\n renderHeader(p),\n renderLinks(d.links),\n entryList(\n 'Experience',\n d.careers.map((c) => ({\n heading: c.role,\n sub: c.organization,\n dates: dateRange(c.startDate, {\n end: c.endDate,\n isCurrent: c.isCurrent,\n locale: d.resolvedLocale,\n }),\n body: c.description,\n url: c.url,\n current: c.isCurrent,\n })),\n 'timeline',\n ),\n entryList(\n 'Projects',\n d.projects.map((x) => ({\n heading: x.title,\n dates: dateRange(x.startDate, { end: x.endDate, locale: d.resolvedLocale }),\n body: x.description,\n url: x.url,\n tags: x.tags,\n highlighted: x.highlighted,\n })),\n 'cards',\n ),\n renderSkills(d.skills, d.settings.skillCategories),\n renderActivity(input.activitySnapshot),\n entryList(\n 'Education',\n d.education.map((e) => {\n const degree = nonEmpty([e.degree, e.fieldOfStudy], ', ');\n return {\n heading: degree ?? e.institution,\n sub: degree ? e.institution : undefined,\n dates: dateRange(e.startDate, {\n end: e.endDate,\n isCurrent: e.isCurrent,\n locale: d.resolvedLocale,\n }),\n body: e.description,\n url: e.url,\n };\n }),\n ),\n entryList(\n 'Certifications',\n d.certifications.map((c) => ({\n heading: c.title,\n sub: c.issuingOrganization,\n dates: dateRange(c.issueDate, { end: c.expirationDate, locale: d.resolvedLocale }),\n url: c.url,\n })),\n ),\n entryList(\n 'Publications',\n d.publications.map((x) => ({\n heading: x.title,\n sub: nonEmpty([x.publisher, x.coAuthors?.join(', ')], ' · '),\n dates: dateRange(x.date, { locale: d.resolvedLocale }),\n body: x.description,\n url: x.url ?? (x.doi ? `https://doi.org/${x.doi}` : undefined),\n })),\n ),\n entryList(\n 'Honors & awards',\n d.honors.map((x) => ({\n heading: x.title,\n sub: x.issuer,\n dates: dateRange(x.date, { locale: d.resolvedLocale }),\n body: x.description,\n url: x.url,\n })),\n ),\n entryList(\n 'Memberships',\n d.memberships.map((x) => ({\n heading: x.role ?? x.organization,\n sub: x.role ? x.organization : undefined,\n dates: dateRange(x.startDate, {\n end: x.endDate,\n isCurrent: x.isCurrent,\n locale: d.resolvedLocale,\n }),\n body: x.description,\n url: x.url,\n })),\n ),\n entryList(\n 'Volunteering',\n d.volunteering.map((x) => ({\n heading: x.role,\n sub: nonEmpty([x.organization, x.cause], ' · '),\n dates: dateRange(x.startDate, {\n end: x.endDate,\n isCurrent: x.isCurrent,\n locale: d.resolvedLocale,\n }),\n body: x.description,\n url: x.url,\n })),\n ),\n entryList(\n 'Courses',\n d.courses.map((x) => ({\n heading: x.title,\n sub: x.provider,\n dates: dateRange(x.completionDate, { locale: d.resolvedLocale }),\n body: x.description,\n url: x.certificateUrl,\n })),\n ),\n entryList(\n 'Patents',\n d.patents.map((x) => ({\n heading: x.title,\n sub: nonEmpty([x.patentNumber, x.office, x.status, x.coInventors?.join(', ')], ' · '),\n dates: dateRange(x.filingDate ?? x.grantDate, { locale: d.resolvedLocale }),\n body: x.description,\n url: x.url,\n })),\n ),\n entryList(\n 'Test scores',\n d.testScores.map((x) => ({\n heading: `${x.title}: ${x.score}`,\n dates: dateRange(x.date, { locale: d.resolvedLocale }),\n body: x.description,\n url: x.url,\n })),\n ),\n renderLanguages(d.languages),\n renderRecommendations(d.recommendations),\n renderContact(d.contact),\n ]\n .filter(Boolean)\n .join('\\n');\n\n const footer =\n d.settings.showPoweredBy === true ? '<footer class=\"powered\">Powered by takuhon</footer>' : '';\n\n // The widget reads its config from these data-* attributes (see\n // @takuhon/contact's browser entry); `defer` lets it mount after parsing\n // without an inline bootstrap script.\n const contactScript = input.contact\n ? `<script src=\"/contact-widget.js\" data-site-key=\"${escapeHtml(input.contact.siteKey)}\"` +\n (input.contact.endpoint ? ` data-endpoint=\"${escapeHtml(input.contact.endpoint)}\"` : '') +\n ' defer></script>\\n'\n : '';\n\n return (\n `<!DOCTYPE html>\\n<html lang=\"${escapeHtml(d.resolvedLocale)}\">\\n<head>\\n ${head}\\n</head>\\n` +\n `<body>\\n<main>\\n${body}\\n</main>\\n${footer ? `${footer}\\n` : ''}${contactScript}</body>\\n</html>\\n`\n );\n}\n","/**\n * Pure HTML helpers shared by the static-site renderer ({@link\n * import('./build-html.js')}) and the CV renderer ({@link\n * import('./cv-html.js')}). All are string-in / string-out with no I/O, so they\n * stay unit-testable and keep both renderers' escaping behavior identical.\n */\n\nimport { formatDate, getPresentLabel, type LocaleTag } from '@takuhon/core';\n\n/** Escape text for use in HTML element content or double/single-quoted attributes. */\nexport function escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Return `url` only when its scheme is safe to place in an `href`/`src`, else\n * `undefined`. Relative, protocol-relative, fragment, and query URLs (no\n * scheme) are allowed; among absolute URLs only `http:`, `https:`, and\n * `mailto:` are. This blocks `javascript:`, `data:`, `vbscript:`, etc. — the\n * schema validates only a generic URI, so a hostile document could otherwise\n * smuggle an executable scheme into the generated page.\n */\nexport function safeUrl(url: string): string | undefined {\n const trimmed = url.trim();\n const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed)?.[1]?.toLowerCase();\n if (scheme === undefined) return trimmed; // relative / protocol-relative / fragment\n return scheme === 'http' || scheme === 'https' || scheme === 'mailto' ? trimmed : undefined;\n}\n\n/**\n * Wrap a single ISO date in a `<time>` element: the machine-readable ISO value\n * stays verbatim in the `datetime` attribute while the visible text is the\n * locale-formatted form from `@takuhon/core`'s {@link formatDate}. Both parts\n * are escaped, so the returned fragment is already safe to insert raw — callers\n * must NOT pass it back through {@link escapeHtml}.\n */\nexport function timeTag(iso: string, locale: LocaleTag): string {\n return `<time datetime=\"${escapeHtml(iso)}\">${escapeHtml(formatDate(iso, locale))}</time>`;\n}\n\n/** The localized ongoing-role marker (en `Present` / ja `現在`), escaped. */\nexport function presentLabel(locale: LocaleTag): string {\n return escapeHtml(getPresentLabel(locale));\n}\n\n/**\n * Format a date range as an escaped HTML fragment: each bound becomes a\n * localized `<time>` element ({@link timeTag}), and a `null` end or `isCurrent`\n * renders as the localized \"Present\" marker ({@link presentLabel}). `locale` is\n * required, so a call that forgets it is a compile error rather than a silently\n * mis-formatted output. The result is fully escaped — callers insert it raw,\n * without {@link escapeHtml}; the only non-escaped literals are the static\n * en-dash separator and the `<time>` tags themselves.\n */\nexport function dateRange(\n start: string | undefined,\n opts: { end?: string | null; isCurrent?: boolean; locale: LocaleTag },\n): string {\n const { end, isCurrent, locale } = opts;\n const left = start ? timeTag(start, locale) : '';\n const right =\n isCurrent === true || end === null ? presentLabel(locale) : end ? timeTag(end, locale) : '';\n if (left && right) return `${left} – ${right}`;\n return left || right;\n}\n\n/** Join the non-empty values with `separator`, or `undefined` when none remain. */\nexport function nonEmpty(\n values: readonly (string | undefined)[],\n separator: string,\n): string | undefined {\n const joined = values\n .filter((v): v is string => typeof v === 'string' && v.length > 0)\n .join(separator);\n return joined.length > 0 ? joined : undefined;\n}\n","/**\n * Inline brand-logo SVG glyphs for social link types.\n *\n * Each entry is a single monochrome path drawn with `fill=\"currentColor\"`, so a\n * link inherits its icon color from the surrounding text and the page never\n * needs an `img-src` beyond `'self'` (the glyphs are inlined into the HTML, not\n * fetched). Only link {@link LinkType}s that have an unambiguous mark are\n * covered; `website`, `email`, and `custom` render without an icon rather than\n * inventing a generic glyph. `blog` reuses the RSS glyph — the conventional\n * feed/blog symbol — via {@link BRAND_ICON_ALIASES}.\n *\n * Sources & licensing (see this package's NOTICE):\n * - `linkedin` — Bootstrap Icons (MIT). simple-icons dropped the LinkedIn mark\n * over trademark policy, so the Bootstrap Icons glyph (viewBox 0 0 16 16) is\n * used instead.\n * - all others — Simple Icons (CC0-1.0, public domain), viewBox 0 0 24 24.\n *\n * Brand logos are trademarks of their respective owners; inclusion here does not\n * imply endorsement.\n */\n\nimport type { LinkType } from '@takuhon/core';\n\nimport { escapeHtml } from './html-helpers.js';\n\ninterface BrandIcon {\n viewBox: string;\n path: string;\n}\n\nconst BRAND_ICONS: Partial<Record<LinkType, BrandIcon>> = {\n github: {\n viewBox: '0 0 24 24',\n path: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',\n },\n gitlab: {\n viewBox: '0 0 24 24',\n path: 'm23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z',\n },\n linkedin: {\n viewBox: '0 0 16 16',\n path: 'M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z',\n },\n x: {\n viewBox: '0 0 24 24',\n path: 'M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z',\n },\n mastodon: {\n viewBox: '0 0 24 24',\n path: 'M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z',\n },\n bluesky: {\n viewBox: '0 0 24 24',\n path: 'M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026',\n },\n instagram: {\n viewBox: '0 0 24 24',\n path: 'M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077',\n },\n youtube: {\n viewBox: '0 0 24 24',\n path: 'M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z',\n },\n threads: {\n viewBox: '0 0 24 24',\n path: 'M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65Zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z',\n },\n facebook: {\n viewBox: '0 0 24 24',\n path: 'M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z',\n },\n rss: {\n viewBox: '0 0 24 24',\n path: 'M19.199 24C19.199 13.467 10.533 4.8 0 4.8V0c13.165 0 24 10.835 24 24h-4.801zM3.291 17.415c1.814 0 3.293 1.479 3.293 3.295 0 1.813-1.485 3.29-3.301 3.29C1.47 24 0 22.526 0 20.71s1.475-3.294 3.291-3.295zM15.909 24h-4.665c0-6.169-5.075-11.245-11.244-11.245V8.09c8.727 0 15.909 7.184 15.909 15.91z',\n },\n};\n\n/**\n * Link types that borrow another type's glyph rather than carry their own. A\n * blog is not a brand, but the RSS mark is the conventional feed/blog symbol, so\n * `blog` reuses the (already bundled, CC0) `rss` glyph instead of inventing a\n * generic icon.\n */\nconst BRAND_ICON_ALIASES: Partial<Record<LinkType, LinkType>> = {\n blog: 'rss',\n};\n\n/**\n * Inline SVG markup for a link type's glyph, or `''` when the type has none\n * (`website`, `email`, `custom`, or an unknown value). The glyph is\n * decorative — the adjacent text label names the link — so it is `aria-hidden`.\n * The static `class=\"brand-icon\"` and fixed viewBox are safe literals; only the\n * path data (a vetted constant, never user input) is interpolated, and it is\n * escaped for defense in depth.\n */\nexport function brandIconSvg(type: LinkType): string {\n const icon = BRAND_ICONS[BRAND_ICON_ALIASES[type] ?? type];\n if (!icon) return '';\n return (\n `<svg class=\"brand-icon\" viewBox=\"${icon.viewBox}\" width=\"18\" height=\"18\" ` +\n `fill=\"currentColor\" aria-hidden=\"true\" focusable=\"false\">` +\n `<path d=\"${escapeHtml(icon.path)}\"/></svg>`\n );\n}\n","/**\n * HTTP-layer locale resolution for the public app.\n *\n * Reads request-side locale candidates in this priority order:\n *\n * 1. `?lang=` query parameter\n * 2. URL path prefix (e.g. `/ja/`), passed in as `pathLocale`\n * 3. `takuhon_locale` cookie\n * 4. `Accept-Language` request header (q-value ordered)\n *\n * The URL-path candidate is extracted structurally by\n * `locale-prefix.ts` (`stripLocalePrefix` / `pathLocaleFromUrl`) and\n * handed to {@link resolveRequestLocales} as `pathLocale`; this module\n * does not parse the path itself. Settings-tier fallbacks\n * (`settings.defaultLocale`, `settings.fallbackLocale`,\n * `settings.availableLocales[0]`) are resolved inside `@takuhon/core`'s\n * `resolveLocale` and do not appear here.\n *\n * `resolveLocale` only exposes two caller slots (`locale`,\n * `fallbackLocale`). To avoid wasting them on tags the document can't\n * serve, candidates are filtered against `availableLocales` (case-\n * insensitive on the full tag or its primary subtag) and the matched\n * available locale token is substituted before forwarding, so a\n * primary-subtag match like `en` → `en-US` does not silently fall\n * through to the settings tier. Filtered candidates beyond the second\n * fall through to `resolveLocale`'s own settings-tier candidates, not\n * in request order — an acceptable loss given the contract.\n */\nimport type { Context } from 'hono';\nimport { getCookie } from 'hono/cookie';\n\nconst BCP47 = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/;\n\n// DoS guards. The header parser is exposed to untrusted client input,\n// so the byte budget and entry count are bounded before any per-token\n// work. Numbers are conservative defaults, not spec-derived.\nconst ACCEPT_LANG_MAX = 2048;\nconst ACCEPT_LANG_MAX_ENTRIES = 16;\nconst COOKIE_VALUE_MAX = 64;\n\nexport function isValidBcp47(tag: string): boolean {\n return BCP47.test(tag);\n}\n\ninterface AcceptLangEntry {\n readonly tag: string;\n readonly q: number;\n}\n\n/**\n * Parse an `Accept-Language` header into BCP-47 tags ordered by q\n * descending. Invalid or zero-quality entries and `*` wildcards are\n * dropped. Input larger than {@link ACCEPT_LANG_MAX} bytes or with more\n * than {@link ACCEPT_LANG_MAX_ENTRIES} comma-separated parts is\n * truncated before parsing.\n */\nexport function parseAcceptLanguage(header: string | null | undefined): string[] {\n if (!header) return [];\n const trimmed = header.length > ACCEPT_LANG_MAX ? header.slice(0, ACCEPT_LANG_MAX) : header;\n const parts = trimmed.split(',').slice(0, ACCEPT_LANG_MAX_ENTRIES);\n\n const entries: AcceptLangEntry[] = [];\n for (const rawPart of parts) {\n const segments = rawPart.split(';');\n const tagSegment = segments[0];\n if (tagSegment === undefined) continue;\n const tag = tagSegment.trim();\n if (tag === '' || tag === '*') continue;\n if (!isValidBcp47(tag)) continue;\n\n let q = 1;\n for (let i = 1; i < segments.length; i++) {\n const segment = segments[i];\n if (segment === undefined) continue;\n const match = /^\\s*q\\s*=\\s*([0-9.]+)\\s*$/i.exec(segment);\n if (!match) continue;\n const parsed = Number.parseFloat(match[1] ?? '');\n if (Number.isNaN(parsed)) {\n q = 1;\n } else if (parsed < 0 || parsed > 1) {\n // RFC 7231 §5.3.1 says values MUST be in [0, 1]; treat\n // out-of-range as missing (q=1) rather than dropping.\n q = 1;\n } else {\n q = parsed;\n }\n break;\n }\n if (q === 0) continue;\n\n entries.push({ tag, q });\n }\n\n // Stable sort: Array.prototype.sort is stable in ES2019+.\n entries.sort((a, b) => b.q - a.q);\n return entries.map((e) => e.tag);\n}\n\nfunction primarySubtag(tag: string): string {\n const dash = tag.indexOf('-');\n return (dash === -1 ? tag : tag.slice(0, dash)).toLowerCase();\n}\n\nfunction matchAvailable(tag: string, available: readonly string[]): string | undefined {\n const tagLower = tag.toLowerCase();\n const tagPrimary = primarySubtag(tag);\n // Prefer exact (case-insensitive) match over primary-subtag match so a\n // request that names a region explicitly wins over a region-stripped\n // alternative.\n for (const a of available) {\n if (a.toLowerCase() === tagLower) return a;\n }\n for (const a of available) {\n if (primarySubtag(a) === tagPrimary) return a;\n }\n return undefined;\n}\n\n/**\n * Resolve HTTP-layer locale candidates from the request — query, URL\n * path prefix, cookie, and `Accept-Language` in that priority order.\n * Returns the top two candidates that survive validation and the\n * `availableLocales` filter, after substituting the matched available\n * token so primary-subtag matches resolve correctly downstream.\n *\n * @param pathLocale The locale token extracted from a `/{locale}` path\n * prefix by `pathLocaleFromUrl`, or `undefined` when the request has\n * no path prefix. Inserted at priority #2 (after query, before\n * cookie).\n */\nexport function resolveRequestLocales(\n c: Context,\n available: readonly string[],\n pathLocale?: string,\n): { locale?: string; fallbackLocale?: string } {\n const raw: string[] = [];\n\n const query = c.req.query('lang');\n if (query !== undefined && isValidBcp47(query)) {\n raw.push(query);\n }\n\n if (pathLocale !== undefined && isValidBcp47(pathLocale)) {\n raw.push(pathLocale);\n }\n\n const cookie = getCookie(c, 'takuhon_locale');\n if (cookie !== undefined && cookie.length <= COOKIE_VALUE_MAX && isValidBcp47(cookie)) {\n raw.push(cookie);\n }\n\n const accept = c.req.header('accept-language');\n if (accept !== undefined) {\n raw.push(...parseAcceptLanguage(accept));\n }\n\n const seen = new Set<string>();\n const filtered: string[] = [];\n for (const tag of raw) {\n const matched = matchAvailable(tag, available);\n if (matched === undefined) continue;\n const key = matched.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n filtered.push(matched);\n if (filtered.length === 2) break;\n }\n\n const out: { locale?: string; fallbackLocale?: string } = {};\n if (filtered[0] !== undefined) out.locale = filtered[0];\n if (filtered[1] !== undefined) out.fallbackLocale = filtered[1];\n return out;\n}\n","/**\n * URL-path locale prefix handling for the public app.\n *\n * Implements locale resolution priority #2: a leading `/{locale}` path\n * segment, e.g. `/ja/api/profile`, ranked after the `?lang=` query (#1)\n * and before the `takuhon_locale` cookie (#3). This module is responsible\n * only for the *structural* concern — detecting and stripping the prefix\n * so the existing flat routes match — while the locale *value* it\n * extracts is fed into {@link resolveRequestLocales} at slot #2 by the\n * route handlers.\n *\n * The prefix is honored via Hono's `getPath` option rather than parametric\n * routes: {@link localePrefixGetPath} rewrites the match path so a request\n * to `/ja/api/profile` is routed to the `/api/profile` handler. Hono's\n * `route()` flattens a sub-app's routes into the parent and dispatches with\n * the *parent* router's `getPath` only, so the same function is applied on\n * both the standalone public app (for direct tests) and the adapter's\n * top-level router (production). The original request URL is untouched, so\n * handlers recover the locale token from `c.req.url`.\n */\nimport { isValidBcp47 } from './locale-resolution.js';\n\n/**\n * Remainder paths that may legitimately follow a `/{locale}` segment.\n *\n * This allowlist — NOT the BCP-47 shape check — is the load-bearing safety\n * mechanism. It keeps locale-agnostic paths (`/health`, `/api/schema`,\n * `/.well-known/*`, `/takuhon.json`) and admin paths (`/api/admin/*`,\n * `/admin/*`) from being misread as a locale prefix. Note that `api`\n * itself satisfies the BCP-47 primary-subtag shape (`[a-z]{2,3}`), so a\n * shape check alone would treat `/api/schema` as locale `api` + `/schema`;\n * the remainder allowlist is what prevents that.\n *\n * Keep this in sync with the locale-aware routes in `public-app.ts`.\n */\nexport const LOCALE_AWARE_REMAINDERS = ['/', '/api/profile', '/api/jsonld'] as const;\n\n/**\n * First-path segments that are reserved namespaces and must never be read\n * as a locale, even though they satisfy the BCP-47 shape. Without this,\n * a bare `/api` (segment `api` is a valid 2–3 letter primary subtag,\n * remainder defaults to the landing `/`) would alias the landing page\n * instead of 404ing. Other reserved roots (`admin`, `health`,\n * `takuhon.json`, `.well-known`) fail the BCP-47 shape check and need no\n * entry here; `api` is the only collision.\n */\nconst RESERVED_FIRST_SEGMENTS = new Set(['api']);\n\nfunction getPathname(url: string): string {\n return new URL(url).pathname;\n}\n\n/**\n * Split a leading `/{locale}` segment from `pathname` when — and only\n * when — the segment is BCP-47-shaped and the remainder is a locale-aware\n * route ({@link LOCALE_AWARE_REMAINDERS}).\n *\n * - `/ja/api/profile` → `{ locale: 'ja', path: '/api/profile' }`\n * - `/api/profile` → `{ path: '/api/profile' }` (remainder `/profile` not locale-aware)\n * - `/api/schema` → `{ path: '/api/schema' }` (the `api`-collision guard)\n * - `/ja/api/admin` → `{ path: '/ja/api/admin' }` (remainder `/api/admin` not locale-aware → 404, admin isolated)\n * - `/ja` and `/ja/` → `{ locale: 'ja', path: '/' }` (trailing slash normalized to landing)\n *\n * The returned `locale` is the raw path token; it is not matched against\n * `availableLocales` here (that happens downstream in\n * {@link resolveRequestLocales}), so an unknown-but-shaped prefix like\n * `/fr/` on an en/ja document strips structurally and then falls through\n * to the next resolution tier, mirroring `?lang=fr` semantics.\n */\nexport function stripLocalePrefix(pathname: string): { locale?: string; path: string } {\n // Match a leading single segment: `/seg` or `/seg/rest...`.\n const match = /^\\/([^/]+)(\\/.*)?$/.exec(pathname);\n if (match === null) return { path: pathname };\n\n const seg = match[1];\n if (seg === undefined || !isValidBcp47(seg)) return { path: pathname };\n\n // Reserved namespace segments (e.g. `api`) pass the BCP-47 shape but are\n // not locales; leave them for the route table (so `/api` 404s as before).\n if (RESERVED_FIRST_SEGMENTS.has(seg.toLowerCase())) return { path: pathname };\n\n // Normalize a bare `/ja` (no trailing content) to the landing remainder.\n const remainder = match[2] ?? '/';\n if (!(LOCALE_AWARE_REMAINDERS as readonly string[]).includes(remainder)) {\n return { path: pathname };\n }\n\n return { locale: seg, path: remainder };\n}\n\n/**\n * Hono `getPath` implementation: returns the locale-stripped path used for\n * route matching. Apply on every Hono router that dispatches the public\n * routes (the standalone public app and the adapter's top-level router).\n */\nexport function localePrefixGetPath(req: Request): string {\n return stripLocalePrefix(getPathname(req.url)).path;\n}\n\n/**\n * Extract the locale token from a request URL's path prefix, or `undefined`\n * when there is none. Used by route handlers to feed priority #2 into\n * {@link resolveRequestLocales}. Reads the original URL, which `getPath`\n * does not mutate.\n */\nexport function pathLocaleFromUrl(url: string): string | undefined {\n return stripLocalePrefix(getPathname(url)).locale;\n}\n","/**\n * @takuhon/api — Hono-based HTTP handlers and response builders for takuhon.\n *\n * Phase 3.3 introduced the public-app factory and the RFC 7807 envelope\n * helpers. Phase 3.4 adds the admin app factories (PUT/DELETE profile and\n * the inline `/admin` HTML editor) plus the `CachePurger` / `AuditLogger`\n * dependency-injection interfaces that adapters bind to a runtime.\n */\n\nexport {\n ERROR_SLUGS,\n buildProblem,\n problemResponse,\n type ErrorSlug,\n type ProblemDetails,\n type ProblemFieldError,\n type BuildProblemInput,\n type ProblemResponseInput,\n} from './error-envelope.js';\nexport { createPublicApp, type PublicAppDeps } from './public-app.js';\n// Re-exported from @takuhon/core (it is a pure transform over core types and\n// now lives there); kept here for backwards compatibility.\nexport { applyPublicPrivacyFilter } from '@takuhon/core';\nexport {\n LOCALE_AWARE_REMAINDERS,\n localePrefixGetPath,\n pathLocaleFromUrl,\n stripLocalePrefix,\n} from './locale-prefix.js';\n\nexport { createAdminApiApp, type AdminApiAppDeps } from './admin/admin-api-app.js';\nexport { createAdminUiApp } from './admin/admin-ui-app.js';\nexport { adminAssetSecurityHeaders } from './admin/admin-asset-headers.js';\nexport {\n noopAuditLogger,\n type AuditEvent,\n type AuditEventType,\n type AuditLogger,\n} from './admin/audit-logger.js';\nexport { noopCachePurger, type CachePurger } from './admin/cache-purger.js';\n\n// Pure, core-only static HTML rendering. `renderProfileHtml` powers both the\n// server-rendered public profile page (the `GET /` route below) and the static\n// pages emitted by `@takuhon/cli`'s `build`/`dev`; keeping it here lets every\n// adapter serve the same markup the CLI writes to disk.\nexport {\n renderProfileHtml,\n escapeHtml,\n type RenderInput,\n type Alternate,\n type LocaleLink,\n} from './html/build-html.js';\nexport { generateSite, type SitePage, type GenerateOptions } from './html/site.js';\nexport { renderCvHtml } from './html/cv-html.js';\n","import {\n ConflictError,\n detectImageMime,\n exportTakuhon,\n MAX_IMAGE_BYTES,\n MAX_IMAGE_DIMENSION,\n MAX_IMAGE_FRAMES,\n NotFoundError,\n normalize,\n readImageInfo,\n stripImageMetadata,\n validate,\n type Takuhon,\n type TakuhonAssetStorage,\n type TakuhonStorage,\n type ValidationError,\n} from '@takuhon/core';\nimport { Hono } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse, type ProblemFieldError } from '../error-envelope.js';\n\nimport type { AuditLogger } from './audit-logger.js';\nimport { bearerMiddleware, getActorTokenHash } from './bearer.js';\nimport type { CachePurger } from './cache-purger.js';\nimport { originMiddleware } from './origin.js';\n\n/**\n * Defense-in-depth CSP for JSON-only responses. Nothing renders here, so\n * `'none'` everywhere is the strongest stance the spec leaves room for.\n */\nconst ADMIN_API_CSP = [\"default-src 'none'\", \"frame-ancestors 'none'\", \"base-uri 'none'\"].join(\n '; ',\n);\n\nexport interface AdminApiAppDeps {\n storage: TakuhonStorage;\n /**\n * Binary-asset store for uploaded images. Optional: when omitted (e.g. a\n * static export or a deployment without R2), `POST /assets` is not registered\n * and resolves to 404, so avatars stay URL-only.\n */\n assetStorage?: TakuhonAssetStorage;\n /** Returns the configured admin token, or undefined if no secret is set. */\n getAdminToken: () => string | undefined;\n /** Allowlist of origins permitted for browser-originating admin requests. */\n getAdminOrigins: () => string[];\n cachePurger: CachePurger;\n auditLogger: AuditLogger;\n}\n\n/**\n * Map a core `ValidationError` to the RFC 7807 field-error shape. The leading\n * `#` produces a JSON Schema-style fragment reference (`#/profile/...`) that\n * matches the example in `api.md §5`.\n */\nfunction toFieldError(e: ValidationError): ProblemFieldError {\n return { path: `#${e.pointer}`, message: e.message };\n}\n\n/**\n * Normalize an `If-Match` header value to the bare version token.\n *\n * Strips the optional `W/` weak-validator prefix before the RFC 7232\n * double-quote delimiters. Compressing CDNs (e.g. Cloudflare serving gzip/br)\n * downgrade the strong ETag we emit to a weak one (`W/\"<version>\"`), and a\n * browser echoes that weakened value straight back as `If-Match`. Without\n * stripping the prefix the comparison against the stored version never matches,\n * so every optimistic-locking save behind such a CDN fails with 409. The\n * weak/strong distinction is meaningless for our opaque version tokens, so\n * collapsing both forms to the raw value is the correct comparison.\n */\nfunction stripETag(raw: string): string {\n let trimmed = raw.trim();\n if (trimmed.startsWith('W/')) {\n trimmed = trimmed.slice(2).trim();\n }\n if (trimmed.startsWith('\"') && trimmed.endsWith('\"') && trimmed.length >= 2) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/**\n * Hono factory for `/api/admin/profile`. Mounted by adapters at `/api/admin`\n * (so the sub-app sees `/profile` as the route path).\n */\nexport function createAdminApiApp(deps: AdminApiAppDeps): Hono {\n const app = new Hono();\n\n app.use('*', async (c, next) => {\n await next();\n const h = c.res.headers;\n h.set('strict-transport-security', 'max-age=63072000; includeSubDomains; preload');\n h.set('x-content-type-options', 'nosniff');\n h.set('x-frame-options', 'DENY');\n h.set('referrer-policy', 'strict-origin-when-cross-origin');\n h.set('permissions-policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');\n h.set('content-security-policy', ADMIN_API_CSP);\n h.set('cache-control', 'private, no-store');\n });\n\n app.use('*', originMiddleware({ getAdminOrigins: deps.getAdminOrigins }));\n app.use(\n '*',\n bearerMiddleware({ getAdminToken: deps.getAdminToken, auditLogger: deps.auditLogger }),\n );\n\n app.onError((err, c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.internal,\n status: 500,\n title: 'Internal Error',\n detail: err instanceof Error ? err.message : 'Unknown failure',\n }),\n );\n\n app.notFound((c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: `No admin route matches ${new URL(c.req.url).pathname}.`,\n }),\n );\n\n app.put('/profile', async (c) => {\n const contentType = c.req.header('content-type') ?? '';\n if (!contentType.toLowerCase().startsWith('application/json')) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.unsupportedMediaType,\n status: 415,\n title: 'Unsupported Media Type',\n detail: `Content-Type must be application/json (got \"${contentType}\").`,\n });\n }\n\n let parsed: unknown;\n try {\n parsed = await c.req.json();\n } catch {\n return problemResponse(c, {\n slug: ERROR_SLUGS.badRequest,\n status: 400,\n title: 'Bad Request',\n detail: 'Request body is not valid JSON.',\n });\n }\n\n const result = validate(parsed);\n if (!result.ok) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.validationFailed,\n status: 422,\n title: 'Validation Failed',\n detail: `Schema validation failed (${String(result.errors.length)} error(s)).`,\n errors: result.errors.map(toFieldError),\n });\n }\n\n const data: Takuhon = result.data;\n const ifMatchRaw = c.req.header('if-match');\n const ifMatch = ifMatchRaw !== undefined ? stripETag(ifMatchRaw) : undefined;\n\n let saved: { version: string };\n try {\n saved = await deps.storage.saveProfile(data, ifMatch);\n } catch (e) {\n if (e instanceof ConflictError) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.conflict,\n status: 409,\n title: 'Conflict',\n detail: 'Stored profile version does not match If-Match.',\n currentVersion: e.currentVersion,\n });\n }\n throw e;\n }\n\n await deps.cachePurger.profileUpdated();\n\n const updatedAt = new Date().toISOString();\n const tokenHash = await getActorTokenHash(c);\n deps.auditLogger({\n type: 'admin.profile.update',\n timestamp: updatedAt,\n actor: { tokenHash },\n request: {\n method: 'PUT',\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n },\n result: { status: 200, version: saved.version },\n });\n\n return c.json({\n data: normalize(data),\n meta: {\n schemaVersion: data.schemaVersion,\n version: saved.version,\n updatedAt,\n },\n });\n });\n\n app.delete('/profile', async (c) => {\n await deps.storage.deleteProfile();\n await deps.cachePurger.profileDeleted();\n\n const tokenHash = await getActorTokenHash(c);\n deps.auditLogger({\n type: 'admin.profile.delete',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: {\n method: 'DELETE',\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n },\n result: { status: 204 },\n });\n\n return c.body(null, 204);\n });\n\n app.get('/export', async (c) => {\n let stored: { data: Takuhon; version: string };\n try {\n stored = await deps.storage.getProfile();\n } catch (e) {\n if (e instanceof NotFoundError) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: 'No profile has been saved yet; there is nothing to export.',\n });\n }\n throw e;\n }\n\n // Token holders receive the full document: the public privacy filter is\n // intentionally bypassed here (Spec §6.21). `exportTakuhon` with\n // `updateTimestamp: false` returns the stored document verbatim (raw\n // transport form, no envelope), so the body round-trips with\n // `importTakuhon` and preserves the real `meta.updatedAt`.\n const exported = exportTakuhon(stored.data, { updateTimestamp: false });\n\n const tokenHash = await getActorTokenHash(c);\n deps.auditLogger({\n type: 'admin.profile.export',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: {\n method: 'GET',\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n },\n result: { status: 200 },\n });\n\n // Surface the stored version so a form-based editor can load the full\n // document and its current version in a single request, then send it back\n // as `If-Match` on the next `PUT` for optimistic locking. Quoted per\n // RFC 7232, matching the public read endpoints and `stripETag`.\n c.header('etag', `\"${stored.version}\"`);\n return c.json(exported);\n });\n\n // Image upload (security.md §4). Registered only when an asset store is\n // configured; otherwise `POST /assets` falls through to the 404 handler.\n if (deps.assetStorage) {\n const assetStorage = deps.assetStorage;\n app.post('/assets', async (c) => {\n // Coarse pre-parse guard: reject before buffering the body when the\n // declared length already exceeds the limit plus a small multipart\n // overhead. The exact check on the decoded file size happens below.\n const declared = Number(c.req.header('content-length') ?? '');\n if (Number.isFinite(declared) && declared > MAX_IMAGE_BYTES + 8192) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.payloadTooLarge,\n status: 413,\n title: 'Payload Too Large',\n detail: `Image exceeds the ${String(MAX_IMAGE_BYTES)}-byte limit.`,\n });\n }\n\n let form: FormData;\n try {\n form = await c.req.formData();\n } catch {\n return problemResponse(c, {\n slug: ERROR_SLUGS.badRequest,\n status: 400,\n title: 'Bad Request',\n detail: 'Request body must be multipart/form-data with a \"file\" field.',\n });\n }\n\n const file = form.get('file');\n if (!(file instanceof File)) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.badRequest,\n status: 400,\n title: 'Bad Request',\n detail: 'Expected an uploaded file in the \"file\" field.',\n });\n }\n\n const bytes = new Uint8Array(await file.arrayBuffer());\n if (bytes.length > MAX_IMAGE_BYTES) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.payloadTooLarge,\n status: 413,\n title: 'Payload Too Large',\n detail: `Image is ${String(bytes.length)} bytes; the limit is ${String(MAX_IMAGE_BYTES)}.`,\n });\n }\n\n // Authenticate the type from the bytes, not the declared Content-Type.\n const mime = detectImageMime(bytes);\n if (mime === null) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.unsupportedMediaType,\n status: 415,\n title: 'Unsupported Media Type',\n detail: 'File is not an accepted image (JPEG, PNG, WebP, or GIF).',\n });\n }\n\n const info = readImageInfo(bytes, mime);\n if (info === null) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.validationFailed,\n status: 422,\n title: 'Validation Failed',\n detail: 'Image header could not be parsed.',\n });\n }\n if (info.width > MAX_IMAGE_DIMENSION || info.height > MAX_IMAGE_DIMENSION) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.validationFailed,\n status: 422,\n title: 'Validation Failed',\n detail: `Image is ${String(info.width)}×${String(info.height)}px; the limit is ${String(MAX_IMAGE_DIMENSION)}×${String(MAX_IMAGE_DIMENSION)}px.`,\n });\n }\n if (info.frames > MAX_IMAGE_FRAMES) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.validationFailed,\n status: 422,\n title: 'Validation Failed',\n detail: `Image has ${String(info.frames)} frames; the limit is ${String(MAX_IMAGE_FRAMES)}.`,\n });\n }\n\n // Strip metadata (EXIF/IPTC/XMP/color profile) before handing the bytes\n // to the adapter, which persists them verbatim.\n const stripped = stripImageMetadata(bytes, mime);\n // `Uint8Array.from` yields an ArrayBuffer-backed array (not the\n // SharedArrayBuffer-possible `ArrayBufferLike`), so it is a valid BlobPart.\n const record = await assetStorage.putAsset(\n new Blob([Uint8Array.from(stripped)], { type: mime }),\n {\n filename: file.name,\n contentType: mime,\n },\n );\n\n const tokenHash = await getActorTokenHash(c);\n deps.auditLogger({\n type: 'admin.asset.upload',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: {\n method: 'POST',\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n },\n result: { status: 201 },\n asset: { key: record.id, mimeType: mime, size: stripped.length },\n });\n\n return c.json(record, 201);\n });\n }\n\n app.on(['POST', 'PATCH'], '/profile', (c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.methodNotAllowed,\n status: 405,\n title: 'Method Not Allowed',\n detail: `${c.req.method} ${new URL(c.req.url).pathname} is not supported on the admin app.`,\n }),\n );\n\n return app;\n}\n","import type { Context, Next } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse } from '../error-envelope.js';\n\nimport type { AuditLogger } from './audit-logger.js';\n\n/**\n * Constant-time byte comparison.\n *\n * Iterates over `max(len(a), len(b))` bytes so wall-clock cost is independent\n * of *where* a mismatch occurs and of length differences (within the same\n * length class). Length-mismatch is folded into the accumulator so the\n * boolean result still discriminates correctly.\n *\n * This is intentionally a from-scratch implementation rather than\n * `crypto.subtle.timingSafeEqual`, which Cloudflare Workers exposes but\n * Node lacks — `@takuhon/api` is adapter-neutral.\n */\nexport function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {\n const len = a.length > b.length ? a.length : b.length;\n let diff = a.length === b.length ? 0 : 1;\n for (let i = 0; i < len; i++) {\n const ai = a[i] ?? 0;\n const bi = b[i] ?? 0;\n diff |= ai ^ bi;\n }\n return diff === 0;\n}\n\n/** SHA-256 hex digest (lowercase) over a UTF-8 string. */\nexport async function sha256Hex(input: string): Promise<string> {\n const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));\n const bytes = new Uint8Array(buf);\n let out = '';\n for (const b of bytes) {\n out += b.toString(16).padStart(2, '0');\n }\n return out;\n}\n\n/**\n * Extracts the Bearer token from the request and returns a stable\n * `sha256:<hex>` digest used as the actor identity in audit logs. Returns\n * `sha256:absent` when no token is present.\n */\nexport async function getActorTokenHash(c: Context): Promise<string> {\n const header = c.req.header('authorization') ?? '';\n const m = /^Bearer\\s+(.+)$/i.exec(header);\n const token = m?.[1] ?? '';\n if (token === '') return 'sha256:absent';\n return `sha256:${await sha256Hex(token)}`;\n}\n\nexport interface BearerMiddlewareOptions {\n /**\n * Source of the expected admin token. Returns `undefined` when the deploy\n * has not provisioned a secret — in that case every request is rejected,\n * mirroring \"no admin access\" semantics.\n */\n getAdminToken: () => string | undefined;\n auditLogger: AuditLogger;\n}\n\n/**\n * Hono middleware that gates downstream handlers on a constant-time Bearer\n * token check. Emits an audit event for both success and failure.\n */\nexport function bearerMiddleware(opts: BearerMiddlewareOptions) {\n return async (c: Context, next: Next): Promise<Response | void> => {\n const expected = opts.getAdminToken();\n const header = c.req.header('authorization') ?? '';\n const match = /^Bearer\\s+(.+)$/i.exec(header);\n const presented = match?.[1];\n\n const isOk =\n expected !== undefined &&\n presented !== undefined &&\n constantTimeEqual(new TextEncoder().encode(presented), new TextEncoder().encode(expected));\n\n const tokenHash = await getActorTokenHash(c);\n const baseRequest = {\n method: c.req.method,\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n };\n\n if (!isOk) {\n opts.auditLogger({\n type: 'admin.auth.failure',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: baseRequest,\n result: { status: 401 },\n });\n return problemResponse(c, {\n slug: ERROR_SLUGS.unauthorized,\n status: 401,\n title: 'Unauthorized',\n detail: 'Bearer token missing or invalid.',\n });\n }\n\n opts.auditLogger({\n type: 'admin.auth.success',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: baseRequest,\n result: { status: 200 },\n });\n\n await next();\n };\n}\n","import type { Context, Next } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse } from '../error-envelope.js';\n\nexport interface OriginMiddlewareOptions {\n /**\n * Returns the allowlist of admin Origins (e.g.\n * `['https://admin.example.com']`). Empty list disables the check —\n * deploys are expected to populate the env before going to production,\n * with the trade-off documented in the adapter README.\n */\n getAdminOrigins: () => string[];\n}\n\n/**\n * Hono middleware that enforces a same-origin / allowlisted-origin policy\n * when configured. Requests without an `Origin` header (curl, server-to-\n * server, native apps) are allowed through; the Bearer token is the primary\n * auth boundary and the absence of `Origin` is itself an indicator that the\n * request did not originate from a browser CSRF context.\n */\nexport function originMiddleware(opts: OriginMiddlewareOptions) {\n return async (c: Context, next: Next): Promise<Response | void> => {\n const allow = opts.getAdminOrigins();\n if (allow.length === 0) {\n await next();\n return;\n }\n const origin = c.req.header('origin');\n if (origin !== undefined && !allow.includes(origin)) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.forbidden,\n status: 403,\n title: 'Forbidden',\n detail: `Origin ${origin} is not in the admin allowlist.`,\n });\n }\n await next();\n };\n}\n","import { Hono } from 'hono';\n\nimport { renderAdminHtml } from './admin-html.js';\n\n/**\n * Per-request CSP (security.md §1.3). Differs from the public CSP:\n * - `img-src` drops `data:` and adds `blob:` for client-side previews.\n * - `style-src` and `script-src` drop `unsafe-inline` and pin a nonce.\n * - `require-trusted-types-for 'script'` blocks DOM-XSS sinks.\n */\nfunction adminCsp(nonce: string): string {\n return [\n \"default-src 'self'\",\n \"img-src 'self' blob:\",\n `style-src 'self' 'nonce-${nonce}'`,\n `script-src 'self' 'nonce-${nonce}'`,\n \"font-src 'self'\",\n \"connect-src 'self'\",\n \"frame-ancestors 'none'\",\n \"base-uri 'self'\",\n \"form-action 'self'\",\n \"require-trusted-types-for 'script'\",\n 'upgrade-insecure-requests',\n ].join('; ');\n}\n\nfunction generateNonce(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n let bin = '';\n for (const b of bytes) {\n bin += String.fromCharCode(b);\n }\n return btoa(bin);\n}\n\n/**\n * Hono factory for the `/admin` HTML editor. Mounted by adapters at\n * `/admin` (so the sub-app sees `/` as its root path). Each request gets a\n * freshly-generated nonce shared between the CSP header and the inline\n * `<style>` / `<script>` tags.\n */\nexport function createAdminUiApp(): Hono {\n const app = new Hono();\n\n app.get('/', (c) => {\n const nonce = generateNonce();\n c.header('strict-transport-security', 'max-age=63072000; includeSubDomains; preload');\n c.header('x-content-type-options', 'nosniff');\n c.header('x-frame-options', 'DENY');\n c.header('referrer-policy', 'strict-origin-when-cross-origin');\n c.header('permissions-policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');\n c.header('content-security-policy', adminCsp(nonce));\n c.header('cache-control', 'private, no-store');\n return c.html(renderAdminHtml(nonce));\n });\n\n return app;\n}\n","/**\n * Inline HTML for the minimal admin editor served at `GET /admin`.\n *\n * Single-page, no build step: a token input, a JSON textarea, and\n * Load / Save (PUT) / Delete (DELETE) buttons. Nothing is loaded until the\n * owner enters the admin token and presses Load, which fetches the *full*\n * document from the authenticated `GET /api/admin/export` (the privacy filter\n * is bypassed there, so fields and links the public profile omits are editable\n * here and survive the next Save). The public `/takuhon.json` is deliberately\n * not used as the editor source: it is privacy-filtered, so loading from it\n * would silently drop non-public data the moment the owner saved.\n *\n * The page operates under a strict CSP (`script-src 'self' 'nonce-<n>'`,\n * `style-src 'self' 'nonce-<n>'`, `require-trusted-types-for 'script'`), so\n * both the inline `<script>` and `<style>` blocks carry the request-scoped\n * nonce. We avoid `innerHTML`/`eval` so Trusted Types is non-disruptive.\n */\nexport function renderAdminHtml(nonce: string): string {\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>takuhon admin</title>\n<style nonce=\"${nonce}\">\nbody { font-family: system-ui, -apple-system, sans-serif; max-width: 960px; margin: 2rem auto; padding: 0 1rem; color: #222; }\nh1 { font-size: 1.5rem; }\np.note { color: #555; }\nlabel { display: block; margin: 1rem 0 0.25rem; font-weight: 600; }\ninput[type=password], textarea { width: 100%; box-sizing: border-box; padding: 0.5rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9rem; border: 1px solid #999; border-radius: 4px; }\ntextarea { min-height: 24rem; }\n.row { display: flex; gap: 0.5rem; margin-top: 1rem; }\nbutton { padding: 0.5rem 1rem; font-size: 0.95rem; border: 1px solid #444; background: #fafafa; border-radius: 4px; cursor: pointer; }\nbutton.danger { border-color: #b03; color: #b03; background: #fff5f5; }\n#status { margin-top: 1rem; padding: 0.75rem; border-radius: 4px; white-space: pre-wrap; word-break: break-word; }\n#status.ok { background: #e6f4ea; color: #1b5e20; border: 1px solid #82c891; }\n#status.err { background: #fdecea; color: #b71c1c; border: 1px solid #ef9a9a; }\nsmall.version { color: #555; }\n</style>\n</head>\n<body>\n<h1>takuhon admin</h1>\n<p class=\"note\">Enter your admin token, then <strong>Load</strong> to fetch the full <code>takuhon.json</code> document — including fields the public profile omits — for editing. <strong>Save</strong> writes it back; optimistic locking via <code>If-Match</code> guards concurrent edits. Nothing is loaded until you provide the token, and the token is never sent over the URL.</p>\n<label for=\"token\">Admin token</label>\n<input id=\"token\" type=\"password\" autocomplete=\"off\" spellcheck=\"false\">\n<label for=\"payload\">takuhon.json <small class=\"version\" id=\"versionLabel\"></small></label>\n<textarea id=\"payload\" spellcheck=\"false\" autocapitalize=\"off\" autocomplete=\"off\"></textarea>\n<div class=\"row\">\n <button id=\"reload\" type=\"button\">Load current</button>\n <button id=\"save\" type=\"button\">Save</button>\n <button id=\"delete\" type=\"button\" class=\"danger\">Delete profile</button>\n</div>\n<div id=\"status\" hidden></div>\n<script nonce=\"${nonce}\">\n(function () {\n var tokenEl = document.getElementById('token');\n var payloadEl = document.getElementById('payload');\n var versionEl = document.getElementById('versionLabel');\n var statusEl = document.getElementById('status');\n var ifMatch = '';\n\n function setStatus(message, ok) {\n statusEl.textContent = message;\n statusEl.className = ok ? 'ok' : 'err';\n statusEl.hidden = false;\n }\n function setVersion(etag) {\n ifMatch = etag || '';\n versionEl.textContent = ifMatch ? '(current version: ' + ifMatch + ')' : '(no stored version)';\n }\n function getToken() {\n var t = tokenEl.value.trim();\n if (!t) { setStatus('Admin token is required.', false); return null; }\n return t;\n }\n async function loadCurrent() {\n var token = getToken();\n if (!token) return;\n try {\n // The authenticated full export, NOT the public /takuhon.json: the latter\n // is privacy-filtered, so editing it would drop non-public data on Save.\n var res = await fetch('/api/admin/export', {\n cache: 'no-store',\n headers: { 'Authorization': 'Bearer ' + token }\n });\n if (res.status === 404) {\n // No profile stored yet. Start from an empty editor; Save creates it.\n setVersion('');\n payloadEl.value = '';\n setStatus('No profile stored yet. Paste a takuhon.json document and Save to create it.', true);\n return;\n }\n if (!res.ok) {\n var err = await res.json().catch(function () { return null; });\n setStatus('Failed to load profile (' + res.status + '): ' + (err ? JSON.stringify(err, null, 2) : 'check the admin token'), false);\n return;\n }\n setVersion(res.headers.get('etag'));\n var json = await res.json();\n payloadEl.value = JSON.stringify(json, null, 2);\n setStatus('Loaded the full profile for editing.', true);\n } catch (e) {\n setStatus('Network error loading profile: ' + (e && e.message ? e.message : String(e)), false);\n }\n }\n async function save() {\n var token = getToken();\n if (!token) return;\n var body;\n try {\n body = JSON.parse(payloadEl.value);\n } catch (e) {\n setStatus('JSON parse error: ' + (e && e.message ? e.message : String(e)), false);\n return;\n }\n var headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token };\n if (ifMatch) headers['If-Match'] = ifMatch;\n try {\n var res = await fetch('/api/admin/profile', { method: 'PUT', headers: headers, body: JSON.stringify(body) });\n var json = await res.json().catch(function () { return null; });\n if (res.ok && json && json.meta && json.meta.version) {\n setVersion('\"' + json.meta.version + '\"');\n setStatus('Saved. New version: ' + json.meta.version, true);\n } else {\n setStatus('Save failed (' + res.status + '): ' + (json ? JSON.stringify(json, null, 2) : 'no body'), false);\n }\n } catch (e) {\n setStatus('Network error during save: ' + (e && e.message ? e.message : String(e)), false);\n }\n }\n async function deleteProfile() {\n var token = getToken();\n if (!token) return;\n if (!confirm('Delete the profile? The bundled onboarding fixture will be shown until you save a new document.')) return;\n var headers = { 'Authorization': 'Bearer ' + token };\n try {\n var res = await fetch('/api/admin/profile', { method: 'DELETE', headers: headers });\n if (res.ok) {\n payloadEl.value = '';\n setVersion('');\n setStatus('Deleted.', true);\n } else {\n var json = await res.json().catch(function () { return null; });\n setStatus('Delete failed (' + res.status + '): ' + (json ? JSON.stringify(json, null, 2) : 'no body'), false);\n }\n } catch (e) {\n setStatus('Network error during delete: ' + (e && e.message ? e.message : String(e)), false);\n }\n }\n\n document.getElementById('save').addEventListener('click', save);\n document.getElementById('delete').addEventListener('click', deleteProfile);\n document.getElementById('reload').addEventListener('click', loadCurrent);\n // No auto-load: the editor stays empty until the owner enters the admin token\n // and presses Load. Nothing about the profile is fetched without the token.\n})();\n</script>\n</body>\n</html>\n`;\n}\n","/**\n * Strict admin Content-Security-Policy + security headers for a *bundled*\n * admin SPA served from static assets (security.md §1.2 admin pages).\n *\n * Unlike {@link createAdminUiApp}'s per-request policy, this variant carries no\n * nonce: the SPA's scripts and styles are external, same-origin files, so\n * `script-src 'self'` / `style-src 'self'` cover them without `'unsafe-inline'`.\n * `require-trusted-types-for 'script'` is retained to block DOM-XSS sinks.\n */\nconst ADMIN_ASSET_CSP = [\n \"default-src 'self'\",\n \"img-src 'self' blob:\",\n \"style-src 'self'\",\n \"script-src 'self'\",\n \"font-src 'self'\",\n \"connect-src 'self'\",\n \"frame-ancestors 'none'\",\n \"base-uri 'self'\",\n \"form-action 'self'\",\n \"require-trusted-types-for 'script'\",\n 'upgrade-insecure-requests',\n].join('; ');\n\n/**\n * Response headers to attach to admin SPA asset responses. Adapters serving the\n * bundle (e.g. Cloudflare Workers Assets) clone the asset response and apply\n * these so the admin origin keeps the strict CSP and is never cached.\n */\nexport function adminAssetSecurityHeaders(): Record<string, string> {\n return {\n 'strict-transport-security': 'max-age=63072000; includeSubDomains; preload',\n 'x-content-type-options': 'nosniff',\n 'x-frame-options': 'DENY',\n 'referrer-policy': 'strict-origin-when-cross-origin',\n 'permissions-policy': 'camera=(), microphone=(), geolocation=(), interest-cohort=()',\n 'content-security-policy': ADMIN_ASSET_CSP,\n 'cache-control': 'private, no-store',\n };\n}\n","/**\n * Structured audit-log emitter for admin actions (per security.md §5).\n *\n * Covers the auth, profile-write, and asset events. Adapters bind a concrete\n * sink (Cloudflare uses `console.log`, which Workers Tail / Logpush captures);\n * tests inject a `vi.fn()` recorder.\n */\nexport type AuditEventType =\n | 'admin.auth.success'\n | 'admin.auth.failure'\n | 'admin.profile.update'\n | 'admin.profile.delete'\n | 'admin.profile.export'\n | 'admin.asset.upload'\n | 'admin.asset.delete'\n | 'admin.cache.purge';\n\nexport interface AuditEvent {\n type: AuditEventType;\n /** ISO-8601 UTC timestamp generated at the call site. */\n timestamp: string;\n /**\n * Actor identity. `tokenHash` is `sha256:<hex>` over the presented Bearer\n * token, or `sha256:absent` when no token was supplied. The raw token is\n * never logged.\n */\n actor?: { tokenHash: string };\n request: {\n method: string;\n path: string;\n /** Originating client IP from `cf-connecting-ip`; undefined off-Cloudflare. */\n ip?: string;\n };\n result: {\n status: number;\n /** Opaque storage version emitted by `TakuhonStorage.saveProfile`. */\n version?: string;\n };\n /**\n * Asset details for `admin.asset.upload` / `admin.asset.delete` events\n * (security.md §5: object key, MIME, and size). Absent for other events.\n */\n asset?: {\n key: string;\n mimeType?: string;\n size?: number;\n };\n}\n\nexport type AuditLogger = (event: AuditEvent) => void;\n\n/** Default sink that discards events; useful for tests and bare runtimes. */\nexport const noopAuditLogger: AuditLogger = () => {\n /* no-op */\n};\n","/**\n * Cache invalidation contract for admin write paths.\n *\n * `@takuhon/api` stays adapter-neutral, so the actual edge-cache calls live\n * in each adapter (Cloudflare's `caches.default.delete`, future runtimes'\n * equivalents). Tests inject a recording implementation to assert that the\n * admin handlers fire the right purge after a successful write.\n */\nexport interface CachePurger {\n /** Called after a successful `PUT /api/admin/profile`. */\n profileUpdated(): Promise<void>;\n /** Called after a successful `DELETE /api/admin/profile`. */\n profileDeleted(): Promise<void>;\n}\n\n/**\n * No-op default: callers that don't run on an edge cache (Node dev server,\n * unit tests that don't care about purge calls) can pass this in.\n */\nexport const noopCachePurger: CachePurger = {\n profileUpdated: () => Promise.resolve(),\n profileDeleted: () => Promise.resolve(),\n};\n","/**\n * Shared static-site generation for `takuhon build` and `takuhon dev`.\n *\n * {@link generateSite} turns one validated, normalized, privacy-filtered profile\n * into the full set of pages the static surface exposes: one per available\n * locale. `build` writes each page's {@link SitePage.file} to disk; `dev` serves\n * each page's {@link SitePage.route} from memory. Keeping the per-locale loop,\n * the locale switcher links, and the canonical/hreflang logic here means both\n * commands share a single source of truth for the site's structure — the only\n * difference between them is disk write vs. in-memory serve.\n *\n * This reuses `@takuhon/core` only (validate/normalize/resolve happen upstream;\n * this module just resolves each locale and renders), so it stays bundler-free\n * and unit-testable as a pure function.\n */\n\nimport type { ActivitySnapshot, NormalizedTakuhon } from '@takuhon/core';\nimport { deriveCv, resolveLocale } from '@takuhon/core';\n\nimport { renderProfileHtml, type Alternate, type LocaleLink } from './build-html.js';\nimport { renderCvHtml } from './cv-html.js';\n\n/** One generated page: a serve route, a relative output file, and its HTML. */\nexport interface SitePage {\n /** URL path used by `dev` (default locale at `/`, others at `/<locale>/`). */\n readonly route: string;\n /** Output path used by `build`, relative to the output dir. */\n readonly file: string;\n /** Rendered HTML document. */\n readonly html: string;\n}\n\nexport interface GenerateOptions {\n /**\n * Site origin (e.g. `https://me.example`). When set, pages carry absolute\n * canonical + hreflang links; when absent those are omitted (the human locale\n * switcher is always relative either way).\n */\n readonly baseUrl?: string;\n /**\n * Synced developer-activity snapshot (the `activity.json` beside the\n * profile). Rendered as an inline-SVG section only while\n * `settings.activity.enabled` is true — the same opt-in gate the public\n * `GET /api/activity` applies — so disabling the feature drops the section\n * even if a stale snapshot remains on disk. The snapshot is locale-agnostic\n * and shared by every page.\n */\n readonly activitySnapshot?: ActivitySnapshot | null;\n /**\n * Also emit a print-ready CV/résumé page per locale (the default locale at\n * `cv.html` / route `/cv/`, others at `<locale>/cv.html` / `/<locale>/cv/`).\n * `takuhon build` gates this behind `--cv`; `takuhon dev` always enables it so\n * the page is previewable. Off by default, so a plain build is unchanged.\n */\n readonly cv?: boolean;\n}\n\n/**\n * Generate every page for a profile: the default locale first, then the rest,\n * de-duplicated. Schema.org JSON-LD is emitted unless `settings.enableJsonLd`\n * is explicitly `false`.\n */\nexport function generateSite(\n profile: NormalizedTakuhon,\n options: GenerateOptions = {},\n): SitePage[] {\n const { baseUrl } = options;\n const defaultLocale = profile.settings.defaultLocale;\n // Default locale first, then the rest, de-duplicated.\n const locales = [...new Set([defaultLocale, ...profile.settings.availableLocales])];\n const jsonLd = profile.settings.enableJsonLd !== false;\n const activitySnapshot =\n profile.settings.activity?.enabled === true\n ? (options.activitySnapshot ?? undefined)\n : undefined;\n\n const pages: SitePage[] = [];\n for (const locale of locales) {\n const localized = resolveLocale(profile, locale);\n const isDefault = locale === defaultLocale;\n\n const localeNav: LocaleLink[] = locales.map((to) => ({\n locale: to,\n href: localeHref(locale, to, defaultLocale),\n current: to === locale,\n }));\n const canonicalUrl = baseUrl ? absoluteUrl(baseUrl, locale, defaultLocale) : undefined;\n const alternates: Alternate[] = baseUrl ? buildAlternates(baseUrl, locales, defaultLocale) : [];\n\n const html = renderProfileHtml({\n localized,\n canonicalUrl,\n alternates,\n localeNav,\n jsonLd,\n activitySnapshot,\n });\n pages.push({\n route: isDefault ? '/' : `/${locale}/`,\n file: isDefault ? 'index.html' : `${locale}/index.html`,\n html,\n });\n\n if (options.cv === true) {\n pages.push({\n route: isDefault ? '/cv/' : `/${locale}/cv/`,\n file: isDefault ? 'cv.html' : `${locale}/cv.html`,\n html: renderCvHtml(deriveCv(localized)),\n });\n }\n }\n return pages;\n}\n\n/** Absolute URL for a locale's page (default locale lives at the site root). */\nfunction absoluteUrl(baseUrl: string, locale: string, defaultLocale: string): string {\n return locale === defaultLocale ? `${baseUrl}/` : `${baseUrl}/${locale}/`;\n}\n\n/** hreflang alternates for every locale plus an `x-default` pointing at the default. */\nfunction buildAlternates(\n baseUrl: string,\n locales: readonly string[],\n defaultLocale: string,\n): Alternate[] {\n const alternates: Alternate[] = locales.map((locale) => ({\n hreflang: locale,\n href: absoluteUrl(baseUrl, locale, defaultLocale),\n }));\n alternates.push({\n hreflang: 'x-default',\n href: absoluteUrl(baseUrl, defaultLocale, defaultLocale),\n });\n return alternates;\n}\n\n/**\n * Depth-correct relative link from the page for `from` to the page for `to`,\n * for the human locale switcher. Always relative (independent of `baseUrl`)\n * so the switcher works regardless of where the site is hosted: the default\n * locale lives at the root, every other locale one directory deep.\n */\nfunction localeHref(from: string, to: string, defaultLocale: string): string {\n const fromRoot = from === defaultLocale;\n const toRoot = to === defaultLocale;\n if (fromRoot) return toRoot ? './' : `${to}/`;\n return toRoot ? '../' : `../${to}/`;\n}\n","/**\n * Pure HTML rendering of a {@link CvDocument} for `takuhon build --cv`.\n *\n * {@link renderCvHtml} turns one locale-resolved CV (from `@takuhon/core`'s\n * {@link deriveCv}) into a complete, self-contained static HTML résumé: an\n * inline stylesheet sized for A4 with an `@media print` block, so the page\n * reads well on screen and the browser's \"Save as PDF\" produces a clean,\n * single-column résumé. No external resources are referenced, so the page works\n * under a strict `img-src 'self'` / no-network policy (the same self-owned\n * stance the profile page and the activity card take).\n *\n * Security: every CV-derived string is escaped before it reaches the markup\n * ({@link escapeHtml}), and any URL is scheme-checked ({@link safeUrl}) so a\n * hostile document cannot smuggle a `javascript:` href into the résumé.\n */\n\nimport type { CvDocument, CvSection, LocaleTag, LocalizedLanguage } from '@takuhon/core';\n\nimport { dateRange, escapeHtml, nonEmpty, safeUrl } from './html-helpers.js';\n\n/** Heading shown for each section, by `kind`. Plain English (CV chrome). */\nconst SECTION_TITLES: Record<CvSection['kind'], string> = {\n experience: 'Experience',\n education: 'Education',\n skills: 'Skills',\n certifications: 'Certifications',\n publications: 'Publications',\n honors: 'Honors & Awards',\n courses: 'Courses',\n patents: 'Patents',\n languages: 'Languages',\n volunteering: 'Volunteering',\n memberships: 'Memberships',\n};\n\n// A4-sized, single-column résumé. The screen view is centered on a neutral\n// backdrop; the print view drops the backdrop/margins so the page fills the\n// sheet, and `break-inside: avoid` keeps entries from splitting across pages.\nconst CSS = `:root{--fg:#1a1a1a;--muted:#555;--accent:#0b5fff;--line:#d9d9d9}\n*{box-sizing:border-box}\nbody{margin:0;background:#f3f4f6;color:var(--fg);font:13px/1.5 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif}\nmain{background:#fff;width:210mm;min-height:297mm;margin:1.5rem auto;padding:18mm 16mm;box-shadow:0 1px 6px rgba(0,0,0,.15)}\nh1{font-size:1.7rem;margin:0}\n.tagline{font-size:1.05rem;color:var(--muted);margin:.15rem 0 0}\n.contact{color:var(--muted);font-size:.85rem;margin:.4rem 0 0;display:flex;flex-wrap:wrap;gap:.25rem 1rem}\n.contact a{color:var(--accent)}\n.bio{margin:.75rem 0 0}\nheader{border-bottom:2px solid var(--fg);padding-bottom:.6rem;margin-bottom:.4rem}\nsection{margin-top:1.1rem}\nh2{font-size:.95rem;text-transform:uppercase;letter-spacing:.05em;color:var(--accent);border-bottom:1px solid var(--line);margin:0 0 .5rem;padding-bottom:.15rem}\nul{padding:0;margin:0;list-style:none}\n.entry{margin:0 0 .7rem;break-inside:avoid}\n.entry .row{display:flex;justify-content:space-between;gap:1rem;align-items:baseline}\n.entry h3{font-size:.95rem;margin:0}\n.entry .dates{color:var(--muted);font-size:.8rem;white-space:nowrap}\n.entry .sub{color:var(--muted);margin:.05rem 0}\n.entry p{margin:.2rem 0 0}\n.entry a{color:var(--accent)}\n.chips{display:flex;flex-wrap:wrap;gap:.3rem}\n.chips li{border:1px solid var(--line);border-radius:1rem;padding:.05rem .55rem;font-size:.8rem}\n@media print{\n body{background:#fff}\n main{width:auto;min-height:0;margin:0;padding:0;box-shadow:none}\n a{color:var(--fg)}\n}\n@page{size:A4;margin:14mm}`;\n\n/** One résumé entry: a heading row (title + dates), an optional sub line, body. */\ninterface CvEntryView {\n heading: string;\n url?: string;\n dates?: string;\n sub?: string;\n body?: string;\n}\n\nfunction renderEntry(entry: CvEntryView): string {\n const href = entry.url ? safeUrl(entry.url) : undefined;\n const title = href\n ? `<a href=\"${escapeHtml(href)}\">${escapeHtml(entry.heading)}</a>`\n : escapeHtml(entry.heading);\n // `entry.dates` is already an escaped HTML fragment from `dateRange` (localized\n // <time> elements), so it is inserted raw rather than re-escaped.\n const dates = entry.dates ? `<span class=\"dates\">${entry.dates}</span>` : '';\n const parts = [`<div class=\"row\"><h3>${title}</h3>${dates}</div>`];\n if (entry.sub) parts.push(`<p class=\"sub\">${escapeHtml(entry.sub)}</p>`);\n if (entry.body) parts.push(`<p>${escapeHtml(entry.body)}</p>`);\n return `<li class=\"entry\">${parts.join('')}</li>`;\n}\n\nfunction entryListSection(title: string, entries: readonly CvEntryView[]): string {\n return `<section><h2>${escapeHtml(title)}</h2><ul>${entries.map(renderEntry).join('')}</ul></section>`;\n}\n\n/** Skills / languages render as compact chip rows rather than dated entries. */\nfunction chipSection(title: string, labels: readonly string[]): string {\n const chips = labels.map((l) => `<li>${escapeHtml(l)}</li>`).join('');\n return `<section><h2>${escapeHtml(title)}</h2><ul class=\"chips\">${chips}</ul></section>`;\n}\n\nfunction languageLabel(l: LocalizedLanguage): string {\n return `${l.displayName ?? l.language} — ${l.proficiency}`;\n}\n\n/**\n * Render one CV section to its `<section>` markup, dispatching on `kind`. The\n * `locale` formats every date in the section (via {@link dateRange}); the\n * per-entry helpers below receive their dates already formatted, so only this\n * dispatcher needs the locale.\n */\nfunction renderSection(section: CvSection, locale: LocaleTag): string {\n const title = SECTION_TITLES[section.kind];\n switch (section.kind) {\n case 'experience':\n return entryListSection(\n title,\n section.entries.map((c) => ({\n heading: c.role,\n sub: c.organization,\n dates: dateRange(c.startDate, { end: c.endDate, isCurrent: c.isCurrent, locale }),\n body: c.description,\n url: c.url,\n })),\n );\n case 'education':\n return entryListSection(\n title,\n section.entries.map((e) => {\n const degree = nonEmpty([e.degree, e.fieldOfStudy], ', ');\n return {\n heading: degree ?? e.institution,\n sub: degree ? e.institution : undefined,\n dates: dateRange(e.startDate, { end: e.endDate, isCurrent: e.isCurrent, locale }),\n body: e.description,\n url: e.url,\n };\n }),\n );\n case 'skills':\n return chipSection(\n title,\n section.entries.map((s) => s.label),\n );\n case 'languages':\n return chipSection(title, section.entries.map(languageLabel));\n case 'certifications':\n return entryListSection(\n title,\n section.entries.map((c) => ({\n heading: c.title,\n sub: c.issuingOrganization,\n dates: dateRange(c.issueDate, { end: c.expirationDate, locale }),\n url: c.url,\n })),\n );\n case 'publications':\n return entryListSection(\n title,\n section.entries.map((x) => ({\n heading: x.title,\n sub: nonEmpty([x.publisher, x.coAuthors?.join(', ')], ' · '),\n dates: dateRange(x.date, { locale }),\n body: x.description,\n url: x.url ?? (x.doi ? `https://doi.org/${x.doi}` : undefined),\n })),\n );\n case 'honors':\n return entryListSection(\n title,\n section.entries.map((x) => ({\n heading: x.title,\n sub: x.issuer,\n dates: dateRange(x.date, { locale }),\n body: x.description,\n url: x.url,\n })),\n );\n case 'courses':\n return entryListSection(\n title,\n section.entries.map((x) => ({\n heading: x.title,\n sub: x.provider,\n dates: dateRange(x.completionDate, { locale }),\n body: x.description,\n url: x.certificateUrl,\n })),\n );\n case 'patents':\n return entryListSection(\n title,\n section.entries.map((x) => ({\n heading: x.title,\n sub: nonEmpty([x.patentNumber, x.office, x.status], ' · '),\n dates: dateRange(x.filingDate ?? x.grantDate, { locale }),\n body: x.description,\n url: x.url,\n })),\n );\n case 'volunteering':\n return entryListSection(\n title,\n section.entries.map((x) => ({\n heading: x.role,\n sub: nonEmpty([x.organization, x.cause], ' · '),\n dates: dateRange(x.startDate, { end: x.endDate, isCurrent: x.isCurrent, locale }),\n body: x.description,\n url: x.url,\n })),\n );\n case 'memberships':\n return entryListSection(\n title,\n section.entries.map((x) => ({\n heading: x.role ?? x.organization,\n sub: x.role ? x.organization : undefined,\n dates: dateRange(x.startDate, { end: x.endDate, isCurrent: x.isCurrent, locale }),\n body: x.description,\n url: x.url,\n })),\n );\n }\n}\n\nfunction renderHeader(cv: CvDocument): string {\n const h = cv.header;\n const parts = [`<h1>${escapeHtml(h.displayName)}</h1>`];\n if (h.tagline) parts.push(`<p class=\"tagline\">${escapeHtml(h.tagline)}</p>`);\n\n const contact: string[] = [];\n if (h.location) contact.push(`<span>${escapeHtml(h.location)}</span>`);\n if (h.email) {\n contact.push(`<a href=\"mailto:${escapeHtml(h.email)}\">${escapeHtml(h.email)}</a>`);\n }\n const formHref = h.formUrl ? safeUrl(h.formUrl) : undefined;\n if (formHref) contact.push(`<a href=\"${escapeHtml(formHref)}\">Contact</a>`);\n if (contact.length > 0) parts.push(`<div class=\"contact\">${contact.join('')}</div>`);\n\n if (h.bio) parts.push(`<p class=\"bio\">${escapeHtml(h.bio)}</p>`);\n return `<header>${parts.join('')}</header>`;\n}\n\n/** Render a complete static HTML résumé document for one locale-resolved CV. */\nexport function renderCvHtml(cv: CvDocument): string {\n const title = `${cv.header.displayName} — CV`;\n const body = [\n renderHeader(cv),\n ...cv.sections.map((s) => renderSection(s, cv.resolvedLocale)),\n ].join('\\n');\n return (\n `<!DOCTYPE html>\\n<html lang=\"${escapeHtml(cv.resolvedLocale)}\">\\n<head>\\n` +\n ` <meta charset=\"utf-8\">\\n` +\n ` <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\\n` +\n ` <title>${escapeHtml(title)}</title>\\n` +\n ` <style>${CSS}</style>\\n</head>\\n` +\n `<body>\\n<main>\\n${body}\\n</main>\\n</body>\\n</html>\\n`\n );\n}\n"],"mappings":"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","names":["generateJsonLd","generateJsonLd","applyPublicPrivacyFilter","NotFoundError","normalize","Hono","Hono","normalize","NotFoundError","Hono","Hono","resolveLocale","CSS","renderEntry","renderHeader","resolveLocale"]}
|
|
1
|
+
{"version":3,"sources":["../src/error-envelope.ts","../src/public-app.ts","../src/html/build-html.ts","../src/html/html-helpers.ts","../src/html/brand-icons.ts","../src/locale-resolution.ts","../src/locale-prefix.ts","../src/index.ts","../src/admin/admin-api-app.ts","../src/admin/bearer.ts","../src/admin/origin.ts","../src/admin/admin-ui-app.ts","../src/admin/admin-html.ts","../src/admin/admin-asset-headers.ts","../src/admin/audit-logger.ts","../src/admin/cache-purger.ts","../src/html/site.ts","../src/html/cv-html.ts"],"sourcesContent":["import type { Context } from 'hono';\nimport type { ContentfulStatusCode } from 'hono/utils/http-status';\n\n/**\n * RFC 7807 problem type slugs used by takuhon. The 11 below are the\n * Spec-defined values (api.md §5.1); `methodNotAllowed` is added locally\n * for the 405 path that the Spec leaves unnamed.\n */\nexport const ERROR_SLUGS = {\n badRequest: 'bad-request',\n unauthorized: 'unauthorized',\n forbidden: 'forbidden',\n notFound: 'not-found',\n methodNotAllowed: 'method-not-allowed',\n conflict: 'conflict',\n payloadTooLarge: 'payload-too-large',\n unsupportedMediaType: 'unsupported-media-type',\n validationFailed: 'validation-failed',\n tooManyRequests: 'too-many-requests',\n internal: 'internal',\n serviceUnavailable: 'service-unavailable',\n} as const;\n\nexport type ErrorSlug = (typeof ERROR_SLUGS)[keyof typeof ERROR_SLUGS];\n\nconst TYPE_BASE = 'https://takuhon.org/errors';\n\nexport interface ProblemFieldError {\n path: string;\n message: string;\n}\n\nexport interface ProblemDetails {\n type: string;\n title: string;\n status: number;\n detail: string;\n instance: string;\n errors?: ProblemFieldError[];\n currentVersion?: string;\n}\n\nexport interface BuildProblemInput {\n slug: ErrorSlug;\n status: number;\n title: string;\n detail: string;\n instance: string;\n errors?: ProblemFieldError[];\n currentVersion?: string;\n}\n\nexport function buildProblem(input: BuildProblemInput): ProblemDetails {\n const out: ProblemDetails = {\n type: `${TYPE_BASE}/${input.slug}`,\n title: input.title,\n status: input.status,\n detail: input.detail,\n instance: input.instance,\n };\n if (input.errors !== undefined) out.errors = input.errors;\n if (input.currentVersion !== undefined) out.currentVersion = input.currentVersion;\n return out;\n}\n\nexport interface ProblemResponseInput {\n slug: ErrorSlug;\n status: ContentfulStatusCode;\n title: string;\n detail: string;\n errors?: ProblemFieldError[];\n currentVersion?: string;\n}\n\nexport function problemResponse(c: Context, input: ProblemResponseInput): Response {\n const body = buildProblem({\n slug: input.slug,\n status: input.status,\n title: input.title,\n detail: input.detail,\n instance: new URL(c.req.url).pathname,\n errors: input.errors,\n currentVersion: input.currentVersion,\n });\n return c.body(JSON.stringify(body), input.status, {\n 'content-type': 'application/problem+json; charset=utf-8',\n });\n}\n","import {\n NotFoundError,\n SCHEMA_VERSION,\n applyPublicPrivacyFilter,\n generateJsonLd,\n normalize,\n resolveLocale,\n schema,\n type ActivityStorage,\n type Takuhon,\n type TakuhonStorage,\n} from '@takuhon/core';\nimport { Hono } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse } from './error-envelope.js';\nimport { renderProfileHtml, type RenderInput } from './html/build-html.js';\nimport { localePrefixGetPath, pathLocaleFromUrl } from './locale-prefix.js';\nimport { resolveRequestLocales } from './locale-resolution.js';\n\ndeclare module 'hono' {\n interface ContextVariableMap {\n /**\n * Set by the `/` handler when it embeds the contact widget, so the\n * security-headers middleware serves the Turnstile-allowing CSP variant for\n * that one response and the strict default everywhere else.\n */\n contactEnabled?: boolean;\n /**\n * Set by the `/` handler so the security-headers middleware serves the\n * host-extended CSP (`deps.render.csp`) on the profile page only; every\n * other route keeps the strict `'self'` policy.\n */\n profilePage?: boolean;\n }\n}\n\nexport interface PublicAppDeps {\n storage: TakuhonStorage;\n /**\n * Returned when storage reports NotFoundError. Adapters that ship a\n * bundled example fixture (e.g. @takuhon/cloudflare) pass a thunk that\n * returns the validated document so initial-onboarding requests still\n * succeed before the first admin write.\n */\n fallback?: () => Takuhon;\n /**\n * Source of the synced developer-activity snapshot, exposed at\n * `GET /api/activity`. Optional, like the admin app's `assetStorage`:\n * deployments that don't sync activity leave it unset and the route\n * answers 404. The route also answers 404 while `settings.activity` is\n * not enabled in the profile, so disabling the feature stops serving a\n * previously synced snapshot immediately.\n */\n activityStorage?: ActivityStorage;\n /**\n * Path of the read-only MCP endpoint, advertised in\n * `/.well-known/takuhon.json` as `mcp`. Only set it on adapters that actually\n * serve MCP (e.g. @takuhon/cloudflare's `/mcp`); left unset, the discovery\n * document omits `mcp` so static / Vercel deployments don't point at an\n * endpoint they don't host.\n */\n mcpPath?: string;\n /**\n * First-party host composition applied to the server-rendered profile page\n * (`GET /`): the renderer's `slots` / `labels` / `omitSections` (see\n * {@link RenderInput}) plus an optional {@link PublicRenderCsp} that widens the\n * page's Content-Security-Policy so injected slots can load their scripts.\n * Absent (the turnkey default) leaves the page and its strict CSP untouched.\n */\n render?: PublicRenderOptions;\n}\n\n/**\n * Declarative, additive extensions to the public profile page's\n * Content-Security-Policy (see {@link PublicRenderOptions.csp}). Each list is\n * appended to the corresponding base directive; `scriptHashes` (e.g.\n * `'sha256-…'`) are appended to `script-src` so a specific inline script — such\n * as a service-worker registration injected via a slot — is allowed WITHOUT a\n * blanket `'unsafe-inline'`. Only the profile page's CSP is widened; every other\n * public route keeps the strict `'self'` policy.\n *\n * Values are host-supplied deploy configuration (not user data), but each token\n * is validated at construction and invalid entries are dropped (with a\n * `console.warn` naming them):\n * - Origin lists must be single CSP source expressions of printable-ASCII\n * characters with no `;` or `,`, so a malformed value can neither inject a\n * directive, split the header, nor crash header construction (a control\n * character or non-Latin-1 codepoint would throw in `Headers.set`). The two\n * most common blanket relaxations, `'unsafe-inline'` / `'unsafe-eval'`, are\n * also rejected — they are the usual copy-paste footgun. This is deliberately\n * narrow, NOT a comprehensive guardrail: `render.csp` is trusted host deploy\n * config, so a host can still broaden its own `script-src` with e.g. `*` or a\n * scheme source, exactly as it could by hand-writing the policy.\n * - `scriptHashes` must be a CSP hash expression (`'sha256-…'` / `'sha384-…'` /\n * `'sha512-…'`); anything else (including `'unsafe-inline'`) is dropped.\n */\nexport interface PublicRenderCsp {\n /** Extra `script-src` origins (e.g. an analytics beacon's script host). */\n scriptSrc?: readonly string[];\n /** Extra `connect-src` origins (e.g. where a beacon reports back). */\n connectSrc?: readonly string[];\n /** `worker-src` origins (e.g. `'self'` for a PWA service worker). Adds the directive when set. */\n workerSrc?: readonly string[];\n /** `'sha256-…'` / `'sha384-…'` / `'sha512-…'` hashes for specific inline scripts, appended to `script-src`. */\n scriptHashes?: readonly string[];\n}\n\n/**\n * Host composition for the server-rendered profile page: the renderer's\n * first-party {@link RenderInput} seams (`slots` / `labels` / `omitSections`)\n * plus an optional {@link PublicRenderCsp}. The CSP lives here, beside the slots\n * whose scripts it authorizes.\n */\nexport type PublicRenderOptions = Pick<RenderInput, 'slots' | 'labels' | 'omitSections'> & {\n csp?: PublicRenderCsp;\n};\n\nconst FALLBACK_VERSION = 'bundled-fixture';\n\n// Cloudflare Turnstile (the contact widget's challenge) loads its script from,\n// renders its iframe on, and reports back to this single origin.\nconst TURNSTILE_ORIGIN = 'https://challenges.cloudflare.com';\n\n/**\n * A single CSP source expression: one or more printable-ASCII characters\n * (`!`–`~`), which the {@link sanitizeCsp} predicate further narrows by rejecting\n * `;` and `,`. This is an ALLOWLIST, not a `\\s`-based denylist: a denylist\n * silently passes control characters (NUL, DEL), C1 controls, and non-Latin-1\n * codepoints (e.g. a copy-pasted zero-width space) — none of which are `\\s` —\n * and those then throw inside `Headers.set` (invalid header value / non-ByteString\n * codepoint), 500-ing the profile page on every request. Restricting to printable\n * ASCII means a malformed value can neither split the header, inject a directive,\n * nor crash header construction; it is simply dropped.\n */\nconst CSP_SOURCE = /^[!-~]+$/;\n/**\n * A CSP hash source — `'sha256-…'` / `'sha384-…'` / `'sha512-…'` (base64,\n * optionally `=`-padded). Anything else offered as a `scriptHashes` entry\n * (including `'unsafe-inline'`) is rejected.\n */\nconst CSP_HASH = /^'sha(256|384|512)-[A-Za-z0-9+/]+={0,2}'$/;\n/**\n * The two most notorious blanket relaxations, refused from ANY directive so a\n * host can't reintroduce them via `render.csp` — they are the usual copy-paste\n * footgun (an analytics/embed snippet that says \"add 'unsafe-inline'\"). This is\n * deliberately narrow, not a comprehensive guardrail: `render.csp` is trusted\n * host deploy config, so broadening sources like `*` or a bare scheme remain the\n * host's own choice (as in any hand-written policy). Compared case-insensitively\n * because browsers match these keywords without regard to case.\n */\nconst CSP_BLANKET: ReadonlySet<string> = new Set([\"'unsafe-inline'\", \"'unsafe-eval'\"]);\n\n/**\n * A host-supplied {@link PublicRenderCsp} after validation: every token is a\n * guaranteed-safe CSP source (or hash), so {@link buildPublicCsp} appends it\n * verbatim. Produced by {@link sanitizeCsp}.\n */\ninterface CleanCsp {\n scriptSrc: readonly string[];\n connectSrc: readonly string[];\n workerSrc: readonly string[];\n scriptHashes: readonly string[];\n}\n\n/**\n * Validate a host-supplied {@link PublicRenderCsp} into a {@link CleanCsp}.\n * Origin lists keep only well-formed source expressions that are not blanket\n * relaxations (`'unsafe-inline'` / `'unsafe-eval'`); `scriptHashes` keeps only\n * CSP hash expressions. Every rejected token is collected in `dropped` so the\n * caller can warn — silently dropping a config typo would turn it into a\n * production CSP-violation hunt.\n */\nfunction sanitizeCsp(ext: PublicRenderCsp): { clean: CleanCsp; dropped: string[] } {\n const dropped: string[] = [];\n const keep = (tokens: readonly string[] | undefined, ok: (t: string) => boolean): string[] =>\n (tokens ?? []).filter((t) => {\n if (ok(t)) return true;\n dropped.push(t);\n return false;\n });\n const source = (t: string): boolean =>\n CSP_SOURCE.test(t) && !t.includes(';') && !t.includes(',') && !CSP_BLANKET.has(t.toLowerCase());\n return {\n clean: {\n scriptSrc: keep(ext.scriptSrc, source),\n connectSrc: keep(ext.connectSrc, source),\n workerSrc: keep(ext.workerSrc, source),\n scriptHashes: keep(ext.scriptHashes, (t) => CSP_HASH.test(t)),\n },\n dropped,\n };\n}\n\n/**\n * Build the public Content-Security-Policy. With `contact` the `@takuhon/contact`\n * widget is embedded, so the Turnstile origin is added to `script-src` (its\n * api.js), `frame-src` (its challenge iframe), and `connect-src` (its\n * verification XHR). The widget's config travels as `data-*` attributes on the\n * external script, so `script-src` still needs no `'unsafe-inline'`. This\n * relaxation is applied ONLY to the HTML page that actually embeds the widget\n * (gated per-request in the `/` handler); every other route keeps the strict,\n * `'self'`-only policy below.\n *\n * `ext` (a validated {@link CleanCsp}) appends origins/hashes to the profile\n * page's directives. When `ext` is undefined the output is byte-identical to the\n * pre-extension policy, so the turnkey default is unchanged.\n */\nfunction buildPublicCsp(contact: boolean, ext?: CleanCsp): string {\n const turnstile = contact ? [TURNSTILE_ORIGIN] : [];\n const scriptSrc = [\"'self'\", ...turnstile, ...(ext?.scriptSrc ?? []), ...(ext?.scriptHashes ?? [])]; // prettier-ignore\n const connectSrc = [\"'self'\", ...turnstile, ...(ext?.connectSrc ?? [])];\n const frameSrc = [...turnstile];\n const workerSrc = ext?.workerSrc ?? [];\n return [\n \"default-src 'self'\",\n // `https:` lets the server-rendered profile page load remote avatar images\n // (the schema permits any https avatar URL, and `safeUrl` in the renderer\n // already blocks non-http(s) schemes); `data:` covers inline placeholders.\n \"img-src 'self' https: data:\",\n \"style-src 'self' 'unsafe-inline'\",\n `script-src ${scriptSrc.join(' ')}`,\n \"font-src 'self'\",\n `connect-src ${connectSrc.join(' ')}`,\n ...(frameSrc.length > 0 ? [`frame-src ${frameSrc.join(' ')}`] : []),\n ...(workerSrc.length > 0 ? [`worker-src ${workerSrc.join(' ')}`] : []),\n \"frame-ancestors 'none'\",\n \"base-uri 'self'\",\n \"form-action 'self'\",\n 'upgrade-insecure-requests',\n ].join('; ');\n}\n\nconst PUBLIC_CSP = buildPublicCsp(false);\nconst PUBLIC_CSP_WITH_CONTACT = buildPublicCsp(true);\n\nasync function loadProfile(deps: PublicAppDeps): Promise<{ data: Takuhon; version: string }> {\n try {\n return await deps.storage.getProfile();\n } catch (e) {\n if (e instanceof NotFoundError && deps.fallback) {\n return { data: deps.fallback(), version: FALLBACK_VERSION };\n }\n throw e;\n }\n}\n\nexport function createPublicApp(deps: PublicAppDeps): Hono {\n // `getPath` strips a leading `/{locale}` prefix (e.g. `/ja/api/profile`\n // → `/api/profile`) so the flat routes below match locale-prefixed\n // URLs. The same function is applied on the adapter's top-level router,\n // because Hono's `route()` flattens this app's routes into the parent\n // and dispatches with the parent's `getPath` only — setting it here\n // alone would be honored for direct `app.fetch()` (tests) but not in\n // production. Handlers recover the locale token from the original URL\n // (`c.req.url`), which `getPath` does not mutate.\n const app = new Hono({ getPath: localePrefixGetPath });\n\n // The profile page's CSP, widened by the host's `render.csp` (if any). Host\n // tokens are validated (`sanitizeCsp`) and any rejected token is warned about\n // once at construction, so a config typo surfaces in the logs rather than\n // silently failing at runtime. With no extension these equal the base\n // policies, so the profile page is byte-identical to every other route and no\n // behavior changes.\n const cspExt = deps.render?.csp;\n let pageCsp = PUBLIC_CSP;\n let pageCspWithContact = PUBLIC_CSP_WITH_CONTACT;\n if (cspExt) {\n const { clean, dropped } = sanitizeCsp(cspExt);\n if (dropped.length > 0) {\n console.warn(\n `[takuhon] Ignored ${dropped.length} invalid render.csp token(s): ${dropped.join(', ')}`,\n );\n }\n pageCsp = buildPublicCsp(false, clean);\n pageCspWithContact = buildPublicCsp(true, clean);\n }\n\n app.use('*', async (c, next) => {\n await next();\n const h = c.res.headers;\n h.set('strict-transport-security', 'max-age=63072000; includeSubDomains; preload');\n h.set('x-content-type-options', 'nosniff');\n h.set('x-frame-options', 'DENY');\n h.set('referrer-policy', 'strict-origin-when-cross-origin');\n h.set('permissions-policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');\n // The `/` handler sets `contactEnabled` (contact widget embedded) and\n // `profilePage` (host `render.csp` extension applies), so both relaxations\n // are scoped to that one page; every other route falls through to the strict\n // default.\n const contact = c.get('contactEnabled') === true;\n const page = c.get('profilePage') === true;\n h.set(\n 'content-security-policy',\n page\n ? contact\n ? pageCspWithContact\n : pageCsp\n : contact\n ? PUBLIC_CSP_WITH_CONTACT\n : PUBLIC_CSP,\n );\n // Every route on this app is unauthenticated, read-only, and already\n // privacy-filtered, so the responses are safe to expose to any origin. This\n // is what lets browsers and AI tools fetch the profile / JSON-LD / discovery\n // document cross-origin — the public API's \"read anywhere\" goal. No cookies\n // or credentials are involved, so `*` is correct and `Vary: Origin` is not\n // needed; preflights are answered by the OPTIONS handler below. The admin\n // app (createAdminApiApp) is a separate Hono instance and is unaffected.\n h.set('access-control-allow-origin', '*');\n h.set('access-control-expose-headers', 'ETag');\n });\n\n app.onError((err, c) => {\n // This 500 body is public — and cross-origin readable once CORS is enabled —\n // so it must not echo internal exception text (storage/render errors can\n // carry implementation detail). Log the real error server-side and return a\n // generic, non-revealing detail.\n console.error('Public app request failed:', err);\n return problemResponse(c, {\n slug: ERROR_SLUGS.internal,\n status: 500,\n title: 'Internal Error',\n detail: 'An unexpected error occurred while handling the request.',\n });\n });\n\n app.notFound((c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: `No route matches ${new URL(c.req.url).pathname}.`,\n }),\n );\n\n // The public profile page. `getPath` has already stripped any `/{locale}`\n // prefix, so this single route serves `/` (default locale) and `/<locale>/`\n // alike; the locale token is recovered from the original URL. The same\n // load → normalize → resolveLocale → privacy-filter pipeline that backs\n // `/api/profile` feeds the pure `renderProfileHtml`, so the page a visitor —\n // and any crawler reading the embedded JSON-LD — sees matches the API and the\n // static `takuhon build` output exactly. Canonical / hreflang are derived\n // from this request's own origin, so they are correct without configuration.\n app.get('/', async (c) => {\n const { data, version } = await loadProfile(deps);\n const profile = normalize(data);\n const { locale, fallbackLocale } = resolveRequestLocales(\n c,\n profile.settings.availableLocales,\n pathLocaleFromUrl(c.req.url),\n );\n const localized = applyPublicPrivacyFilter(resolveLocale(profile, locale, fallbackLocale));\n\n const defaultLocale = profile.settings.defaultLocale;\n const locales = [...new Set([defaultLocale, ...profile.settings.availableLocales])];\n const current = localized.resolvedLocale;\n const origin = new URL(c.req.url).origin;\n const localePath = (l: string): string => (l === defaultLocale ? '/' : `/${l}/`);\n\n const snapshot =\n profile.settings.activity?.enabled === true && deps.activityStorage\n ? await deps.activityStorage.getActivitySnapshot()\n : null;\n\n // Embed the contact widget only when the owner has enabled it AND provided\n // the public Turnstile site key (without the key the widget cannot mount).\n // The secret / recipient / From live in adapter env and gate the POST\n // endpoint separately; the page only needs the public key. Setting the\n // context flag relaxes this response's CSP for the Turnstile origin.\n const contactSettings = profile.settings.contact;\n const contactSiteKey = contactSettings?.turnstileSiteKey?.trim();\n const contact =\n contactSettings?.enabled === true && contactSiteKey\n ? {\n siteKey: contactSiteKey,\n ...(contactSettings.endpoint ? { endpoint: contactSettings.endpoint } : {}),\n }\n : undefined;\n if (contact) c.set('contactEnabled', true);\n\n const html = renderProfileHtml({\n // Host composition seams (slots / labels / omitSections). `csp` is a\n // response-header concern handled above, not a renderer input, so it is\n // intentionally excluded here.\n ...(deps.render\n ? {\n slots: deps.render.slots,\n labels: deps.render.labels,\n omitSections: deps.render.omitSections,\n }\n : {}),\n localized,\n canonicalUrl: `${origin}${localePath(current)}`,\n alternates: [\n ...locales.map((l) => ({ hreflang: l, href: `${origin}${localePath(l)}` })),\n { hreflang: 'x-default', href: `${origin}${localePath(defaultLocale)}` },\n ],\n localeNav: locales.map((l) => ({ locale: l, href: localePath(l), current: l === current })),\n jsonLd: profile.settings.enableJsonLd !== false,\n activitySnapshot: snapshot ?? undefined,\n contact,\n year: new Date().getFullYear(),\n });\n\n c.header('etag', `\"${version}\"`);\n c.header('cache-control', 'public, max-age=300');\n c.header('vary', 'Accept-Language, Cookie');\n // Scope the host CSP extension (if any) to this page — set only now that the\n // profile page rendered successfully, so a failure above never widens the\n // CSP on the error response (mirrors `contactEnabled`).\n c.set('profilePage', true);\n return c.html(html);\n });\n\n // Liveness probe. Intentionally storage-independent: it reports that the\n // worker itself is serving requests, not that the profile store is\n // reachable. A readiness probe that also checks storage can be added\n // later under a separate path if deployment platforms need it.\n app.get('/health', (c) => {\n c.header('cache-control', 'no-store');\n return c.json({ status: 'ok', schemaVersion: SCHEMA_VERSION });\n });\n\n app.get('/api/profile', async (c) => {\n const { data, version } = await loadProfile(deps);\n const { locale, fallbackLocale } = resolveRequestLocales(\n c,\n data.settings.availableLocales,\n pathLocaleFromUrl(c.req.url),\n );\n const localized = applyPublicPrivacyFilter(\n resolveLocale(normalize(data), locale, fallbackLocale),\n );\n const body = {\n data: localized,\n meta: {\n schemaVersion: localized.schemaVersion,\n locale: localized.resolvedLocale,\n updatedAt: localized.meta.updatedAt,\n },\n };\n c.header('etag', `\"${version}\"`);\n c.header('cache-control', 'private, max-age=300');\n c.header('vary', 'Accept-Language, Cookie');\n return c.json(body);\n });\n\n app.get('/api/schema', (c) => c.json(schema));\n\n // Public read of the synced developer-activity snapshot (design decision\n // §9-5: public, like /api/profile). The snapshot is already owner-derived\n // public metrics — no privacy filter applies — but the owner's opt-in is\n // re-checked on every read so disabling `settings.activity` takes effect\n // immediately, even while a stale snapshot is still stored. All three\n // unavailable states answer the same 404 problem.\n app.get('/api/activity', async (c) => {\n const unavailable = (): Response =>\n problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: 'No activity snapshot is available.',\n });\n\n if (!deps.activityStorage) return unavailable();\n const { data } = await loadProfile(deps);\n if (data.settings.activity?.enabled !== true) return unavailable();\n const snapshot = await deps.activityStorage.getActivitySnapshot();\n if (snapshot === null) return unavailable();\n\n c.header('cache-control', 'public, max-age=300');\n return c.json(snapshot);\n });\n\n app.get('/api/jsonld', async (c) => {\n const { data, version } = await loadProfile(deps);\n const { locale, fallbackLocale } = resolveRequestLocales(\n c,\n data.settings.availableLocales,\n pathLocaleFromUrl(c.req.url),\n );\n const localized = applyPublicPrivacyFilter(\n resolveLocale(normalize(data), locale, fallbackLocale),\n );\n const ld = generateJsonLd(localized);\n c.header('etag', `\"${version}\"`);\n c.header('cache-control', 'private, max-age=300');\n c.header('vary', 'Accept-Language, Cookie');\n c.header('content-type', 'application/ld+json; charset=utf-8');\n return c.body(JSON.stringify(ld));\n });\n\n app.get('/takuhon.json', async (c) => {\n const { data, version } = await loadProfile(deps);\n const filtered = applyPublicPrivacyFilter(data);\n c.header('etag', `\"${version}\"`);\n c.header('cache-control', 'public, max-age=300');\n return c.json(filtered);\n });\n\n app.get('/.well-known/takuhon.json', (c) => {\n c.header('cache-control', 'public, max-age=3600');\n return c.json({\n schemaVersion: SCHEMA_VERSION,\n schemaUrl: '/api/schema',\n profile: '/api/profile',\n jsonld: '/api/jsonld',\n export: '/api/admin/export',\n canonical: '/takuhon.json',\n // Only advertised when the adapter serves MCP (see PublicAppDeps.mcpPath).\n ...(deps.mcpPath !== undefined ? { mcp: deps.mcpPath } : {}),\n });\n });\n\n // CORS preflight for cross-origin reads. The actual GET responses carry\n // `Access-Control-Allow-Origin` via the middleware above; this answers the\n // preflight a browser sends before a non-simple cross-origin request (a\n // simple GET needs no preflight). Without this, OPTIONS would fall through to\n // the 404 handler and the preflight would fail.\n app.options('*', (c) => {\n c.header('access-control-allow-methods', 'GET, HEAD, OPTIONS');\n c.header('access-control-allow-headers', c.req.header('access-control-request-headers') ?? '*');\n c.header('access-control-max-age', '86400');\n return c.body(null, 204);\n });\n\n app.on(['POST', 'PUT', 'PATCH', 'DELETE'], '*', (c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.methodNotAllowed,\n status: 405,\n title: 'Method Not Allowed',\n detail: `${c.req.method} ${new URL(c.req.url).pathname} is not supported on the public app.`,\n }),\n );\n\n return app;\n}\n","/**\n * Pure HTML rendering for `takuhon build`.\n *\n * {@link renderProfileHtml} turns one locale-resolved {@link LocalizedTakuhon}\n * into a complete, self-contained static HTML document: semantic markup for\n * every profile section, an inline stylesheet, optional Schema.org JSON-LD,\n * and the `<head>` metadata (`<title>`, description, canonical, hreflang\n * alternates) the caller supplies.\n *\n * This is a deliberately separate, simpler surface from the React\n * `@takuhon/ui` (which is delivered as CSS-Modules components needing a\n * bundler). It reuses only `@takuhon/core` and has no DOM/browser dependency,\n * so it renders in plain Node and is unit-testable as a pure string function.\n *\n * Security: every piece of profile-derived text is escaped before it reaches\n * the markup ({@link escapeHtml}), and the JSON-LD payload is `<`/`>`/`&`\n * unicode-escaped so it cannot break out of its `<script>` element.\n */\n\nimport { generateJsonLd, renderActivitySvg, SECTION_KEYS } from '@takuhon/core';\nimport type {\n ActivitySnapshot,\n AppearanceColors,\n AppearanceSettings,\n LabelKey,\n LocalizedTakuhon,\n SectionKey,\n} from '@takuhon/core';\n\nimport { brandIconForHost, brandIconForLink, brandIconForPlatform } from './brand-icons.js';\nimport { dateRange, escapeHtml, nonEmpty, safeUrl, timeTag } from './html-helpers.js';\n\n// Re-exported for existing importers (e.g. dev-command, tests) that pull\n// `escapeHtml` from this module; the implementation now lives in html-helpers.\nexport { escapeHtml } from './html-helpers.js';\n\ntype LocalizedProfile = LocalizedTakuhon['profile'];\n\n/** One entry in the human-facing locale switcher. */\nexport interface LocaleLink {\n locale: string;\n href: string;\n current: boolean;\n}\n\n/** One `<link rel=\"alternate\" hreflang>` entry. */\nexport interface Alternate {\n hreflang: string;\n href: string;\n}\n\nexport interface RenderInput {\n /** The locale-resolved document to render. */\n localized: LocalizedTakuhon;\n /** Absolute canonical URL for this page (only when `--base-url` was given). */\n canonicalUrl?: string;\n /** hreflang alternates (empty when no base URL is available). */\n alternates: readonly Alternate[];\n /** Human locale switcher links (rendered only when more than one locale). */\n localeNav: readonly LocaleLink[];\n /** Whether to emit Schema.org JSON-LD (mirrors `settings.enableJsonLd`). */\n jsonLd: boolean;\n /**\n * Synced developer-activity snapshot, rendered as a self-owned inline SVG\n * section. The caller gates it on `settings.activity.enabled`; an absent (or\n * metric-less) snapshot omits the section entirely.\n */\n activitySnapshot?: ActivitySnapshot;\n /**\n * When set, embed the `@takuhon/contact` widget: a `<link>` to its stylesheet\n * in `<head>` and a deferred `<script>` whose config travels as `data-*`\n * attributes (no inline script, so the page CSP needs no `'unsafe-inline'`).\n * The caller gates it on `settings.contact.enabled` and a present site key;\n * the adapter is responsible for serving `/contact-widget.{js,css}` and for\n * relaxing its CSP to allow the Turnstile origin.\n */\n contact?: { siteKey: string; endpoint?: string };\n /**\n * Copyright year for the footer license line (`© {year} {name} — {spdx}`).\n * The renderer is pure, so the clock lives with the caller: a server/CLI\n * passes `new Date().getFullYear()`; a test passes a fixed value. When absent,\n * the license line omits the year (`© {name} — {spdx}`) so output stays\n * deterministic by default.\n */\n year?: number;\n /**\n * First-party composition slots: raw HTML injected verbatim at three fixed\n * points so a host can extend the page without forking the renderer (a page\n * `<head>`, a trailing `<main>` section, a pre-`</body>` block). Unlike every\n * other input, slot content is NOT escaped — it is the caller's own trusted\n * markup (e.g. an og:image tag, a PWA manifest link, an analytics beacon, a\n * bespoke section), never profile/user data. Absent slots emit nothing, so\n * passing no slots is byte-identical to passing empty slots. See {@link\n * RenderSlots}.\n */\n slots?: RenderSlots;\n /**\n * Localized overrides for the section headings and chrome labels, merged over\n * the built-in pack chosen for the resolved locale ({@link pickLabelPack}: a\n * Japanese base-language locale gets the Japanese pack, everything else\n * English). A host supplies overrides only for the strings it wants to change\n * (e.g. a bespoke heading); an absent field takes the pack default.\n */\n labels?: Partial<SectionLabels>;\n /**\n * Sections whose DEFAULT rendering to suppress in the visible body — so a host\n * can replace one with its own markup via {@link RenderSlots.mainEnd} without\n * it appearing twice. This affects the visible body ONLY: the embedded JSON-LD\n * is still generated from the complete document, so structured data never\n * loses a suppressed section. Absent / empty = every section renders.\n */\n omitSections?: readonly SectionKey[];\n}\n\n/**\n * First-party raw-HTML injection points. Content is inserted verbatim (not\n * escaped): it is the host's own trusted markup, not profile/user data. Keep it\n * self-contained — the renderer makes no guarantees about it.\n *\n * CSP: the renderer does not set any Content-Security-Policy — the adapter does.\n * The turnkey public app serves `script-src 'self'` (no `'unsafe-inline'`), so\n * an inline `<script>` injected via `bodyEnd`/`head` is blocked there; prefer an\n * external `<script src>` or ensure the host's own CSP permits the slot content.\n *\n * Reserved id: the renderer owns `<main id=\"main\">` (the skip-link target). A\n * slot MUST NOT inject another element with `id=\"main\"`, or the skip anchor\n * becomes ambiguous.\n */\nexport interface RenderSlots {\n /**\n * Injected in `<head>`, after the `<style>` block (e.g. OG image tags, a PWA\n * manifest link, theme-color). NOTE: the renderer already owns the data-derived\n * Open Graph tags (`og:type`/`og:title`/`og:description`/`og:url`); a host slot\n * should supply only the asset tags it owns (`og:image`, `twitter:image`, …)\n * and MUST NOT re-inject the renderer-owned ones, or they appear twice.\n */\n head?: string;\n /** Injected at the end of `<main>`, after all rendered sections (e.g. a bespoke section). */\n mainEnd?: string;\n /**\n * Injected before `</body>`, after the footer and contact script (e.g. an\n * analytics beacon). Inline scripts here require a CSP that allows them (see\n * the interface note) — the turnkey `script-src 'self'` would block them.\n */\n bodyEnd?: string;\n}\n\n/**\n * Section headings (one per {@link SectionKey}) plus chrome labels (skip link,\n * nav aria-labels, the other-links heading, the footer credit lead-in) the\n * caller can localize. The full key set is the core {@link LabelKey} taxonomy,\n * so this type, `settings.sectionLabels`, and {@link RenderInput.labels} stay in\n * lock-step. The renderer ships Japanese and English packs and picks one by the\n * resolved locale ({@link pickLabelPack}); `settings.sectionLabels` then\n * `labels` override individual strings on top.\n */\nexport type SectionLabels = Record<LabelKey, string>;\n\n/** Built-in English label pack. */\nconst LABELS_EN: SectionLabels = {\n about: 'About',\n careers: 'Experience',\n projects: 'Projects',\n volunteering: 'Volunteering',\n skills: 'Skills',\n activity: 'Developer activity',\n education: 'Education',\n certifications: 'Certifications',\n publications: 'Publications',\n honors: 'Honors & awards',\n memberships: 'Memberships',\n courses: 'Courses',\n patents: 'Patents',\n testScores: 'Test scores',\n languages: 'Languages',\n recommendations: 'Recommendations',\n highlights: 'Selected posts',\n contact: 'Contact',\n skipLink: 'Skip to main content',\n localeNav: 'Language',\n featuredLinks: 'Featured links',\n otherLinks: 'Links',\n poweredBy: 'Powered by',\n};\n\n/** Built-in Japanese label pack. */\nconst LABELS_JA: SectionLabels = {\n about: '自己紹介',\n careers: '経歴',\n projects: 'プロジェクト',\n volunteering: 'ボランティア',\n skills: 'スキル',\n activity: '開発アクティビティ',\n education: '学歴',\n certifications: '資格・認定',\n publications: '出版物',\n honors: '受賞・栄誉',\n memberships: '所属',\n courses: '講座',\n patents: '特許',\n testScores: 'テストスコア',\n languages: '言語',\n recommendations: '推薦',\n highlights: 'ピックアップ投稿',\n contact: '連絡先',\n skipLink: 'メインコンテンツへスキップ',\n localeNav: '言語',\n featuredLinks: '主要リンク',\n otherLinks: 'その他のリンク',\n poweredBy: 'Powered by',\n};\n\n/**\n * Choose a built-in label pack by the resolved locale's base language: a\n * Japanese locale (`ja`, `ja-JP`, …) gets the Japanese pack, everything else the\n * English pack. The renderer used to ship English only and leave localization to\n * the caller; shipping a Japanese pack and auto-selecting is deliberate (a\n * Japanese turnkey site should not show English headings). A caller can still\n * override any string via {@link RenderInput.labels}.\n */\n/**\n * Base-language test: true for `ja`, `ja-JP`, … The primary language subtag is\n * compared exactly (so a different language that merely starts with \"ja\" does\n * not match). Drives both the label pack and the date-range separator.\n */\nfunction isJapaneseLocale(resolvedLocale: string): boolean {\n return resolvedLocale.toLowerCase().split(/[-_]/)[0] === 'ja';\n}\n\nfunction pickLabelPack(resolvedLocale: string): SectionLabels {\n return isJapaneseLocale(resolvedLocale) ? LABELS_JA : LABELS_EN;\n}\n\n/** Unicode-escape `<`, `>`, `&` so a JSON-LD payload cannot break out of `<script>`. */\nfunction escapeJsonLd(json: string): string {\n return json.replace(/</g, '\\\\u003c').replace(/>/g, '\\\\u003e').replace(/&/g, '\\\\u0026');\n}\n\n/**\n * Overridable design tokens (light). Every color and the font the renderer uses\n * is a named `--takuhon-*` custom property with a default here; owners re-skin\n * the page by overriding a subset via `settings.appearance` ({@link\n * buildTokenCss}). The static rules reference only these variables (and the\n * internal tokens below), never hard-coded colors or fonts, so an override\n * propagates everywhere.\n */\nconst DEFAULT_TOKENS: Record<string, string> = {\n '--takuhon-color-bg': '#ffffff',\n '--takuhon-color-surface': '#f6f7f9',\n '--takuhon-color-text': '#1f2933',\n '--takuhon-color-text-muted': '#52606d',\n '--takuhon-color-border': '#d8dee7',\n '--takuhon-color-primary': '#2563eb',\n '--takuhon-color-primary-contrast': '#ffffff',\n '--takuhon-color-accent': '#4f46e5',\n '--takuhon-color-heading': '#1e1888',\n '--takuhon-font-family':\n \"system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif\",\n};\n\n/**\n * Default dark palette, applied under `prefers-color-scheme: dark`. Only colors\n * flip in dark mode (the internal scale tokens are shared). Owner `colorsDark`\n * overrides merge over these, exactly as `colors` merges over the light set.\n */\nconst DEFAULT_TOKENS_DARK: Record<string, string> = {\n '--takuhon-color-bg': '#0f172a',\n '--takuhon-color-surface': '#1e293b',\n '--takuhon-color-text': '#e2e8f0',\n '--takuhon-color-text-muted': '#94a3b8',\n '--takuhon-color-border': '#334155',\n '--takuhon-color-primary': '#60a5fa',\n '--takuhon-color-primary-contrast': '#0f172a',\n '--takuhon-color-accent': '#818cf8',\n '--takuhon-color-heading': '#c7d2fe',\n};\n\n/**\n * Internal design-scale tokens (spacing, radius, type scale, layout). These are\n * NOT part of the `settings.appearance` override contract — they are the\n * renderer's own layout vocabulary — so they are emitted in `:root` but not\n * exposed as overridable schema keys. Kept in sync between light and dark.\n */\nconst INTERNAL_TOKENS: Record<string, string> = {\n '--takuhon-space-1': '4px',\n '--takuhon-space-2': '8px',\n '--takuhon-space-3': '12px',\n '--takuhon-space-4': '16px',\n '--takuhon-space-5': '24px',\n '--takuhon-space-6': '32px',\n '--takuhon-radius-sm': '6px',\n '--takuhon-radius-md': '12px',\n '--takuhon-radius-full': '9999px',\n '--takuhon-tap-target': '44px',\n '--takuhon-font-size-sm': '14px',\n '--takuhon-font-size-base': '16px',\n '--takuhon-font-size-lg': '18px',\n '--takuhon-font-size-xl': '22px',\n '--takuhon-font-size-2xl': '28px',\n '--takuhon-line-height': '1.7',\n '--takuhon-max-content-width': '720px',\n};\n\n/** Map each `AppearanceColors` key to the CSS custom property it overrides. */\nconst COLOR_TOKEN_VARS: Record<keyof AppearanceColors, string> = {\n bg: '--takuhon-color-bg',\n surface: '--takuhon-color-surface',\n text: '--takuhon-color-text',\n textMuted: '--takuhon-color-text-muted',\n border: '--takuhon-color-border',\n accent: '--takuhon-color-accent',\n primary: '--takuhon-color-primary',\n primaryContrast: '--takuhon-color-primary-contrast',\n heading: '--takuhon-color-heading',\n};\n\n/**\n * Defense in depth. The schema pattern-constrains these values, but data can\n * reach the renderer unvalidated (pre-1.2.0 documents, adapters that skip\n * `validate()`), so the renderer re-sanitizes every token with an allowlist\n * that mirrors the schema. An unsafe, non-string, or empty value is dropped and\n * the built-in default stands.\n *\n * Colors accept only a hex value, a bare keyword (named colors / currentColor /\n * transparent), or a known color function — never `url()`, `image-set()`,\n * `var()`, or anything else that could trigger an external request or escape\n * the inline `<style>`. A permissive \"any char but `;{}<>`\" filter is NOT\n * enough: `url(//evil/x.png)` carries none of those characters yet would make\n * the page fetch a third-party resource. These patterns mirror `CssColor` and\n * the font-family pattern in `@takuhon/core`'s takuhon.schema.json — keep them\n * in sync.\n */\nconst SAFE_COLOR =\n /^(?:#[0-9A-Fa-f]{3,8}|[A-Za-z]+|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([A-Za-z0-9.,%/\\s-]*\\))$/;\nconst SAFE_FONT = /^[A-Za-z0-9\\s,'\"._-]+$/;\n\nfunction safeValue(value: unknown, pattern: RegExp, maxLength: number): string | undefined {\n if (typeof value !== 'string') return undefined;\n const v = value.trim();\n if (v === '' || v.length > maxLength || !pattern.test(v)) return undefined;\n return v;\n}\n\nconst safeColor = (value: unknown): string | undefined => safeValue(value, SAFE_COLOR, 64);\nconst safeFont = (value: unknown): string | undefined => safeValue(value, SAFE_FONT, 256);\n\n/** Sanitized `[cssVar, value]` pairs for the color overrides that are present. */\nfunction colorOverrides(colors: AppearanceColors | undefined): [string, string][] {\n if (!colors) return [];\n const out: [string, string][] = [];\n for (const key of Object.keys(COLOR_TOKEN_VARS) as (keyof AppearanceColors)[]) {\n const safe = safeColor(colors[key]);\n if (safe !== undefined) out.push([COLOR_TOKEN_VARS[key], safe]);\n }\n return out;\n}\n\n/** Serialize `[var, value]` pairs into a `:root{…}` declaration block. */\nfunction rootBlock(pairs: Iterable<[string, string]>): string {\n return `:root{${[...pairs].map(([k, v]) => `${k}:${v}`).join(';')}}`;\n}\n\n/**\n * Build the token stylesheet:\n * - `:root` = internal scale tokens + the light color/font defaults, with any\n * owner `colors`/`fontFamily` overrides merged on top.\n * - a `prefers-color-scheme: dark` block = the default dark palette with any\n * owner `colorsDark` overrides merged on top.\n *\n * Only the fixed set of named tokens is ever emitted — never arbitrary CSS —\n * and every overridable value is sanitized ({@link safeColor} / {@link\n * safeFont}), so a value can neither escape the inline `<style>` nor trigger an\n * external request.\n */\nfunction buildTokenCss(appearance: AppearanceSettings | undefined): string {\n const light = new Map<string, string>([\n ...Object.entries(INTERNAL_TOKENS),\n ...Object.entries(DEFAULT_TOKENS),\n ]);\n if (appearance) {\n const font = safeFont(appearance.fontFamily);\n if (font !== undefined) light.set('--takuhon-font-family', font);\n for (const [cssVar, value] of colorOverrides(appearance.colors)) {\n light.set(cssVar, value);\n }\n }\n\n const dark = new Map<string, string>(Object.entries(DEFAULT_TOKENS_DARK));\n for (const [cssVar, value] of colorOverrides(appearance?.colorsDark)) {\n dark.set(cssVar, value);\n }\n\n return `${rootBlock(light)}\\n@media (prefers-color-scheme:dark){${rootBlock(dark)}}`;\n}\n\nconst CSS = `*{box-sizing:border-box}\nhtml{font-size:100%}\nbody{margin:0;color:var(--takuhon-color-text);background:var(--takuhon-color-bg);font-family:var(--takuhon-font-family);font-size:var(--takuhon-font-size-base);line-height:var(--takuhon-line-height);-webkit-text-size-adjust:100%}\nmain{max-width:var(--takuhon-max-content-width);margin:0 auto;padding:var(--takuhon-space-6) var(--takuhon-space-4)}\na{color:var(--takuhon-color-primary)}\na:focus-visible{outline:2px solid var(--takuhon-color-accent);outline-offset:2px;border-radius:var(--takuhon-radius-sm)}\n.skip-link{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}\n.skip-link:focus{position:fixed;left:var(--takuhon-space-2);top:var(--takuhon-space-2);width:auto;height:auto;padding:var(--takuhon-space-2) var(--takuhon-space-4);background:var(--takuhon-color-primary);color:var(--takuhon-color-primary-contrast);border-radius:var(--takuhon-radius-sm);z-index:100}\nh1{font-size:var(--takuhon-font-size-2xl);font-weight:700;line-height:1.2;margin:0 0 var(--takuhon-space-2)}\nh2{font-size:var(--takuhon-font-size-xl);margin:0 0 var(--takuhon-space-3);padding-bottom:var(--takuhon-space-2);border-bottom:1px solid var(--takuhon-color-border)}\nh3{font-size:var(--takuhon-font-size-lg);font-weight:600;margin:0}\nheader{margin-bottom:var(--takuhon-space-6);display:flow-root}\nheader .avatar{width:96px;height:96px;border-radius:var(--takuhon-radius-full);object-fit:cover;float:left;margin:0 var(--takuhon-space-3) var(--takuhon-space-3) 0;shape-outside:circle();border:1px solid var(--takuhon-color-border)}\n.tagline{font-size:var(--takuhon-font-size-lg);color:var(--takuhon-color-text-muted);margin:0 0 var(--takuhon-space-2)}\n.location{font-size:var(--takuhon-font-size-sm);color:var(--takuhon-color-text-muted);margin:0}\nsection{margin:0 0 var(--takuhon-space-6)}\n.bio-body h3{font-size:var(--takuhon-font-size-lg);font-weight:600;color:var(--takuhon-color-heading);margin:var(--takuhon-space-5) 0 var(--takuhon-space-2)}\n.bio-body h4{font-size:var(--takuhon-font-size-base);font-weight:600;color:var(--takuhon-color-heading);margin:var(--takuhon-space-4) 0 var(--takuhon-space-2)}\n.bio-body p{margin:0 0 var(--takuhon-space-3)}\n.bio-body ul{list-style:disc;margin:0 0 var(--takuhon-space-3);padding-left:var(--takuhon-space-5)}\n.bio-body li{margin:0 0 var(--takuhon-space-1)}\n.bio-body hr{border:0;border-top:1px solid var(--takuhon-color-border);margin:var(--takuhon-space-5) 0}\nul{padding:0;margin:0;list-style:none}\n.entries>li{margin:0 0 var(--takuhon-space-4)}\n.entries--timeline>li{position:relative;display:flex;flex-direction:column;margin:0 0 0 var(--takuhon-space-2);padding:0 0 var(--takuhon-space-5) var(--takuhon-space-4);border-left:2px solid var(--takuhon-color-border)}\n.entries--timeline>li:last-child{padding-bottom:0}\n.entries--timeline>li::before{content:\"\";position:absolute;left:-7px;top:6px;width:12px;height:12px;border-radius:var(--takuhon-radius-full);background:var(--takuhon-color-text-muted)}\n.entries--timeline>li.is-current::before{background:var(--takuhon-color-accent)}\n.entries--timeline .meta{order:-1;margin:0 0 var(--takuhon-space-1)}\n.entries--timeline>li>h3{font-size:var(--takuhon-font-size-base);font-weight:600;margin:0}\n.entries--cards{display:grid;gap:var(--takuhon-space-3)}\n.entries--cards>li{margin:0;padding:var(--takuhon-space-4);border:1px solid var(--takuhon-color-border);border-radius:var(--takuhon-radius-md);background:var(--takuhon-color-surface)}\n.entries--cards>li.is-highlighted{border-color:var(--takuhon-color-accent)}\n.entries h3 a{color:inherit;text-decoration:none}\n.entries h3 a:hover{color:var(--takuhon-color-primary);text-decoration:underline}\n.sub{margin:var(--takuhon-space-1) 0 0;color:var(--takuhon-color-text-muted)}\n.sub a{color:inherit;text-decoration:none}\n.sub a:hover{color:var(--takuhon-color-primary);text-decoration:underline}\n.role-badge{margin:var(--takuhon-space-1) 0 0;font-size:var(--takuhon-font-size-sm);font-weight:600;letter-spacing:.02em;color:var(--takuhon-color-accent)}\n.desc{margin:var(--takuhon-space-2) 0 0}\n.entries--timeline .desc{white-space:pre-wrap}\n.external-icon{width:.72em;height:.72em;margin-left:.35em;vertical-align:-.05em;opacity:.55}\n.meta{margin:var(--takuhon-space-1) 0 0;color:var(--takuhon-color-text-muted);font-size:var(--takuhon-font-size-sm)}\n.featured-links,.other-links{margin:0 0 var(--takuhon-space-6)}\n.featured-links>ul,.other-links>ul{list-style:none;padding:0;margin:0;display:grid;gap:var(--takuhon-space-2)}\n.featured-links>ul{grid-template-columns:repeat(auto-fill,minmax(220px,1fr))}\n.featured-links a,.other-links a{display:flex;align-items:center;justify-content:space-between;gap:var(--takuhon-space-2);min-height:var(--takuhon-tap-target);padding:var(--takuhon-space-2) var(--takuhon-space-3);background:var(--takuhon-color-surface);color:var(--takuhon-color-text);text-decoration:none;border:1px solid var(--takuhon-color-border);border-radius:var(--takuhon-radius-md)}\n.featured-links a:hover,.other-links a:hover{border-color:var(--takuhon-color-accent)}\n.link-main{display:inline-flex;align-items:center;gap:var(--takuhon-space-2);min-width:0}\n.link-type{font-size:var(--takuhon-font-size-sm);color:var(--takuhon-color-text-muted)}\n.brand-icon{width:1.15em;height:1.15em;flex:none;opacity:.85}\n.skills,.tags{display:flex;flex-wrap:wrap;gap:var(--takuhon-space-2)}\n.skills>li,.tags>li{background:var(--takuhon-color-surface);border:1px solid var(--takuhon-color-border);border-radius:var(--takuhon-radius-full);padding:var(--takuhon-space-1) var(--takuhon-space-3);font-size:var(--takuhon-font-size-sm)}\n.entries--cards .tags{margin-top:var(--takuhon-space-2)}\n.tags>li{background:var(--takuhon-color-bg);color:var(--takuhon-color-text-muted);padding:2px var(--takuhon-space-2)}\n.skills-groups{display:grid;gap:var(--takuhon-space-4)}\n.skill-group h3{font-size:var(--takuhon-font-size-base);margin:0 0 var(--takuhon-space-2);color:var(--takuhon-color-text-muted);text-transform:uppercase;letter-spacing:.04em}\n.vol-list{display:grid;gap:var(--takuhon-space-2)}\n.vol{padding:var(--takuhon-space-3) 0;border-bottom:1px solid var(--takuhon-color-border)}\n.vol:last-child{border-bottom:0}\n.vol-org{margin:0;font-size:var(--takuhon-font-size-lg)}\n.vol-org a{color:inherit;text-decoration:none}\n.vol-org a:hover{color:var(--takuhon-color-primary);text-decoration:underline}\n.vol-role{margin:var(--takuhon-space-1) 0 0;color:var(--takuhon-color-text-muted);font-size:var(--takuhon-font-size-sm)}\n.vol-desc{margin:var(--takuhon-space-1) 0 0;font-size:var(--takuhon-font-size-sm)}\n.vol-secondary{display:inline-flex;align-items:center;gap:var(--takuhon-space-1);margin-top:var(--takuhon-space-2);padding:2px var(--takuhon-space-2);font-size:var(--takuhon-font-size-sm);background:var(--takuhon-color-surface);border:1px solid var(--takuhon-color-border);border-radius:var(--takuhon-radius-full);color:var(--takuhon-color-text-muted);text-decoration:none}\n.vol-secondary:hover{color:var(--takuhon-color-text);border-color:var(--takuhon-color-accent)}\n.vol-secondary .brand-icon{width:1em;height:1em;opacity:.85}\n.rec{margin:0 0 var(--takuhon-space-4)}\n.rec blockquote{margin:0;padding-left:var(--takuhon-space-3);border-left:3px solid var(--takuhon-color-border)}\n.rec figcaption{color:var(--takuhon-color-text-muted);font-size:var(--takuhon-font-size-sm);margin-top:var(--takuhon-space-2)}\nnav.locales{display:flex;justify-content:flex-end;gap:var(--takuhon-space-3);margin-bottom:var(--takuhon-space-4);font-size:var(--takuhon-font-size-sm)}\nnav.locales a,nav.locales [aria-current]{padding:var(--takuhon-space-1) var(--takuhon-space-2);border-radius:var(--takuhon-radius-sm);text-transform:uppercase}\nnav.locales a{color:var(--takuhon-color-primary);text-decoration:none}\nnav.locales a:hover{text-decoration:underline}\nnav.locales [aria-current]{color:var(--takuhon-color-text);font-weight:700;background:var(--takuhon-color-surface)}\n.activity svg{width:100%;height:auto}\nfooter.powered{max-width:var(--takuhon-max-content-width);margin:var(--takuhon-space-6) auto 0;padding:var(--takuhon-space-4) var(--takuhon-space-4) var(--takuhon-space-6);border-top:1px solid var(--takuhon-color-border);text-align:center;color:var(--takuhon-color-text-muted);font-size:var(--takuhon-font-size-sm)}\nfooter.powered p{margin:0 0 var(--takuhon-space-2)}\nfooter.powered p:last-child{margin-bottom:0}\nfooter.powered .powered-by a{color:var(--takuhon-color-primary);text-decoration:none}\nfooter.powered .powered-by a:hover{text-decoration:underline}\n.highlights .highlights-intro{color:var(--takuhon-color-text-muted);margin:0 0 var(--takuhon-space-3)}\n.highlights-track{display:flex;gap:var(--takuhon-space-3);list-style:none;margin:0;padding:0 0 var(--takuhon-space-2);overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-left:var(--takuhon-space-1);-webkit-overflow-scrolling:touch;overscroll-behavior-x:contain}\n.highlight-card{position:relative;flex:0 0 85%;scroll-snap-align:start;display:flex;flex-direction:column;background:var(--takuhon-color-surface);border:1px solid var(--takuhon-color-border);border-radius:var(--takuhon-radius-md);overflow:hidden}\n@media (min-width:640px){.highlight-card{flex-basis:calc((100% - var(--takuhon-space-3)) / 2)}}\n@media (min-width:960px){.highlight-card{flex-basis:calc((100% - 2 * var(--takuhon-space-3)) / 3)}}\n.highlight-card:hover{border-color:var(--takuhon-color-accent)}\n.highlight-card:focus-within{outline:2px solid var(--takuhon-color-accent);outline-offset:2px}\n.highlight-thumb{aspect-ratio:1 / 1;background:var(--takuhon-color-bg)}\n.highlight-thumb img{display:block;width:100%;height:100%;object-fit:cover}\n.highlight-body{padding:var(--takuhon-space-3);display:flex;flex-direction:column;gap:var(--takuhon-space-2)}\n.highlight-body>*{margin:0}\n.highlight-badge{display:inline-flex;align-items:center;gap:var(--takuhon-space-1);font-size:var(--takuhon-font-size-sm);color:var(--takuhon-color-text-muted)}\n.highlight-badge .brand-icon{width:1em;height:1em;opacity:.85}\n.highlight-title{font-size:var(--takuhon-font-size-base)}\n.highlight-title a{color:var(--takuhon-color-text);text-decoration:none}\n.highlight-title a:hover{color:var(--takuhon-color-primary)}\n.highlight-title a::after{content:\"\";position:absolute;inset:0}\n.highlight-title a:focus-visible{outline:none}\n.highlight-date{font-size:var(--takuhon-font-size-sm);color:var(--takuhon-color-text-muted)}\n.highlight-desc{font-size:var(--takuhon-font-size-sm)}\n.highlight-cta{font-size:var(--takuhon-font-size-sm);font-weight:600;color:var(--takuhon-color-primary)}`;\n\ninterface EntryView {\n heading: string;\n /**\n * An accent \"role\" badge shown directly under the heading (a project's role).\n * Distinct from {@link sub}, which is a muted secondary line.\n */\n role?: string;\n sub?: string;\n /**\n * When set, {@link sub} renders as an external link (e.g. a career's\n * organization site) with the external-link affordance icon.\n */\n subUrl?: string;\n dates?: string;\n body?: string;\n /**\n * When set, the heading renders as an external link (e.g. a project's site)\n * with the external-link affordance icon.\n */\n url?: string;\n tags?: readonly string[];\n /**\n * Timeline-variant \"ongoing\" marker (from `career.isCurrent` etc.) → an\n * `is-current` class on the `<li>` so the timeline dot uses the accent color.\n */\n current?: boolean;\n /**\n * Card-variant highlight (from `project.highlighted`) → an `is-highlighted`\n * class on the `<li>` so the card gets an accent border.\n */\n highlighted?: boolean;\n}\n\n/**\n * External-link affordance icon appended to a linked heading or sub, so on touch\n * devices (no hover) it is clear the link opens another site. A vetted static\n * literal (Feather \"external-link\"), never user input.\n */\nconst EXTERNAL_ICON =\n '<svg class=\"external-icon\" viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/><polyline points=\"15 3 21 3 21 9\"/><line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/></svg>';\n\n/** An external link wrapping already-escaped inner HTML, with the affordance icon. */\nfunction externalLink(url: string, innerEscaped: string): string {\n return `<a href=\"${escapeHtml(url)}\" rel=\"noopener\">${innerEscaped}${EXTERNAL_ICON}</a>`;\n}\n\n/**\n * Layout variant for a section's entry list. Undefined = the default flat list;\n * `timeline` decorates each `<li>` as a dotted left-border timeline row (dates\n * float to the top via CSS order); `cards` lays the entries out as bordered\n * surface cards (intentionally single-column within the reading-width column —\n * long descriptions stay readable — not a responsive multi-column grid). Only\n * the container/`<li>` decoration differs — the inner {@link renderEntry}\n * markup is identical across all three.\n */\ntype EntriesVariant = 'timeline' | 'cards';\n\nfunction renderEntry(entry: EntryView): string {\n const headHref = entry.url ? safeUrl(entry.url) : undefined;\n const heading = headHref\n ? externalLink(headHref, escapeHtml(entry.heading))\n : escapeHtml(entry.heading);\n const parts = [`<h3>${heading}</h3>`];\n if (entry.role) parts.push(`<p class=\"role-badge\">${escapeHtml(entry.role)}</p>`);\n if (entry.sub) {\n const subHref = entry.subUrl ? safeUrl(entry.subUrl) : undefined;\n const sub = subHref ? externalLink(subHref, escapeHtml(entry.sub)) : escapeHtml(entry.sub);\n parts.push(`<p class=\"sub\">${sub}</p>`);\n }\n // `entry.dates` is already an escaped HTML fragment from `dateRange` (localized\n // <time> elements), so it is inserted raw rather than re-escaped.\n // NOTE: the `timeline` variant hoists this `.meta` above the heading via CSS\n // `order:-1`, which only works while `.meta` stays a *direct child* of the\n // entry `<li>`. Keep these entry parts as flat siblings — do not wrap them in\n // a container div, or the date-first timeline ordering silently breaks.\n if (entry.dates) parts.push(`<p class=\"meta\">${entry.dates}</p>`);\n if (entry.body) parts.push(`<p class=\"desc\">${escapeHtml(entry.body)}</p>`);\n if (entry.tags && entry.tags.length > 0) {\n parts.push(\n `<ul class=\"tags\">${entry.tags.map((t) => `<li>${escapeHtml(t)}</li>`).join('')}</ul>`,\n );\n }\n // Both markers come from real schema booleans; only the relevant one is ever\n // set per section (`current` for careers, `highlighted` for projects).\n const cls = [entry.current ? 'is-current' : '', entry.highlighted ? 'is-highlighted' : '']\n .filter(Boolean)\n .join(' ');\n return `<li${cls ? ` class=\"${cls}\"` : ''}>${parts.join('')}</li>`;\n}\n\n/** Render a `<section>` of entries, or `''` when there are none. */\nfunction entryList(title: string, entries: readonly EntryView[], variant?: EntriesVariant): string {\n if (entries.length === 0) return '';\n const cls = variant ? `entries entries--${variant}` : 'entries';\n return `<section><h2>${escapeHtml(title)}</h2><ul class=\"${cls}\">${entries\n .map(renderEntry)\n .join('')}</ul></section>`;\n}\n\n/**\n * Render a `**bold**` inline run. Text is HTML-escaped FIRST, then the `**`\n * markers (plain ASCII, untouched by escaping) are turned into `<strong>` — so\n * escaped user content can never inject a tag. Non-greedy, non-nesting.\n */\nfunction renderInline(text: string): string {\n return escapeHtml(text).replace(/\\*\\*([^*]+?)\\*\\*/g, '<strong>$1</strong>');\n}\n\n/**\n * Minimal, dependency-free Markdown subset for `profile.bio`: `## ` → `<h3>`,\n * `### ` → `<h4>`, `---` → `<hr>`, `- ` → `<ul><li>`, `**bold**`, and\n * blank-line-separated paragraphs. Every text node is HTML-escaped before inline\n * markers are applied ({@link renderInline}), so content cannot inject markup.\n * Anything not matching a block marker becomes a paragraph — so a plain one-line\n * bio degrades to a single `<p>`. No inline links/images (kept deliberately out\n * of the subset so the section needs no URL-safety gate).\n */\nfunction renderMarkdown(input: string): string {\n const lines = input.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n const trimmed = (lines[i] ?? '').trim();\n if (trimmed === '') {\n i++;\n continue;\n }\n if (trimmed === '---') {\n out.push('<hr>');\n i++;\n continue;\n }\n if (trimmed.startsWith('### ')) {\n out.push(`<h4>${renderInline(trimmed.slice(4))}</h4>`);\n i++;\n continue;\n }\n if (trimmed.startsWith('## ')) {\n out.push(`<h3>${renderInline(trimmed.slice(3))}</h3>`);\n i++;\n continue;\n }\n if (trimmed.startsWith('- ')) {\n const items: string[] = [];\n while (i < lines.length) {\n const cur = (lines[i] ?? '').trim();\n if (!cur.startsWith('- ')) break;\n items.push(`<li>${renderInline(cur.slice(2))}</li>`);\n i++;\n }\n out.push(`<ul>${items.join('')}</ul>`);\n continue;\n }\n const para: string[] = [];\n while (i < lines.length) {\n const cur = (lines[i] ?? '').trim();\n if (\n cur === '' ||\n cur === '---' ||\n cur.startsWith('## ') ||\n cur.startsWith('### ') ||\n cur.startsWith('- ')\n ) {\n break;\n }\n para.push(cur);\n i++;\n }\n if (para.length > 0) out.push(`<p>${renderInline(para.join(' '))}</p>`);\n }\n return out.join('\\n');\n}\n\n/**\n * Render the \"About\" section from `profile.bio` as a Markdown-subset block\n * ({@link renderMarkdown}). Returns `''` when there is no bio. The section\n * carries a heading so it reads consistently with the other sections; a plain\n * prose bio still renders (as a single paragraph).\n */\nfunction renderBio(bio: string | undefined, heading: string): string {\n if (!bio) return '';\n return `<section class=\"bio\"><h2>${escapeHtml(heading)}</h2><div class=\"bio-body\">${renderMarkdown(bio)}</div></section>`;\n}\n\nfunction renderHeader(p: LocalizedProfile, localeNavHtml: string): string {\n const parts: string[] = [];\n // The locale switcher sits at the very top of the header (right-aligned via\n // CSS) rather than as a separate pre-header nav.\n if (localeNavHtml) parts.push(localeNavHtml);\n const avatarSrc = p.avatar?.url ? safeUrl(p.avatar.url) : undefined;\n if (avatarSrc) {\n parts.push(\n `<img class=\"avatar\" src=\"${escapeHtml(avatarSrc)}\" alt=\"${escapeHtml(p.avatar?.alt ?? '')}\">`,\n );\n }\n parts.push(`<h1>${escapeHtml(p.displayName)}</h1>`);\n if (p.tagline) parts.push(`<p class=\"tagline\">${escapeHtml(p.tagline)}</p>`);\n if (p.location?.display) parts.push(`<p class=\"location\">${escapeHtml(p.location.display)}</p>`);\n // `p.bio` is no longer rendered here: it is now its own Markdown \"About\"\n // section ({@link renderBio}) placed after the links. It still feeds the\n // `<meta name=\"description\">` fallback and the JSON-LD, both built from the\n // document rather than this markup.\n return `<header>${parts.join('')}</header>`;\n}\n\ntype LocalizedLink = LocalizedTakuhon['links'][number];\n\n/** Human display names for the built-in link types, used for the type pill. */\nconst LINK_TYPE_DISPLAY: Partial<Record<LocalizedLink['type'], string>> = {\n website: 'Website',\n blog: 'Blog',\n github: 'GitHub',\n gitlab: 'GitLab',\n linkedin: 'LinkedIn',\n x: 'X (Twitter)',\n mastodon: 'Mastodon',\n bluesky: 'Bluesky',\n instagram: 'Instagram',\n youtube: 'YouTube',\n threads: 'Threads',\n facebook: 'Facebook',\n email: 'Email',\n rss: 'RSS',\n};\n\n/**\n * A small muted type pill (e.g. \"GitHub\") shown at the trailing edge of a link\n * that has NO owner-supplied label, so a link whose visible text falls back to\n * the raw URL still names its platform. When the owner set an explicit `label`,\n * that label is authoritative and the brand glyph already signals the platform,\n * so no pill is added (this also avoids a redundant English pill next to a\n * localized label, e.g. \"ブログ\" + \"Blog\"). Omitted for `custom` links (no\n * canonical type name) and when the type name would just repeat the URL text.\n */\nfunction linkTypePill(link: LocalizedLink, label: string): string {\n const display = LINK_TYPE_DISPLAY[link.type];\n if (!display || display === label) return '';\n return `<span class=\"link-type\">${escapeHtml(display)}</span>`;\n}\n\n/** Render one link as a pill: brand glyph (when resolvable) + label + type pill. */\nfunction renderLinkItem(link: LocalizedLink): string {\n const label = link.label ?? link.url;\n const main = `<span class=\"link-main\">${brandIconForLink(link)}<span>${escapeHtml(label)}</span></span>`;\n // The type pill only names the platform when there is no owner label (the main\n // text is then the URL); an explicit label is authoritative, so no pill.\n const pill = link.label == null ? linkTypePill(link, label) : '';\n const href = safeUrl(link.url);\n // rel=\"me\" declares these as the owner's own profiles (IndieWeb / Mastodon\n // verification); noopener hardens the external navigation.\n return href\n ? `<li><a href=\"${escapeHtml(href)}\" rel=\"me noopener\">${main}${pill}</a></li>`\n : `<li>${main}${pill}</li>`;\n}\n\n/** Ascending by `order` (absent sorts as 0), preserving input order on ties. */\nfunction byOrder(a: LocalizedLink, b: LocalizedLink): number {\n return (a.order ?? 0) - (b.order ?? 0);\n}\n\n/**\n * The featured links — a top-of-page pill grid (a `<nav>`, no visible heading).\n * Sorted by `order`; `''` when there are no featured links.\n */\nfunction renderFeaturedLinks(links: LocalizedTakuhon['links'], ariaLabel: string): string {\n const featured = links.filter((l) => l.featured === true).sort(byOrder);\n if (featured.length === 0) return '';\n return `<nav class=\"featured-links\" aria-label=\"${escapeHtml(ariaLabel)}\"><ul>${featured\n .map(renderLinkItem)\n .join('')}</ul></nav>`;\n}\n\n/**\n * The remaining links — a bottom-of-page `<section>` with a visible heading.\n * Sorted by `order`; `''` when every link is featured. This section is pinned\n * to the bottom of the page (below the reorderable content sections).\n */\nfunction renderOtherLinks(links: LocalizedTakuhon['links'], heading: string): string {\n const others = links.filter((l) => l.featured !== true).sort(byOrder);\n if (others.length === 0) return '';\n return `<section class=\"other-links\"><h2>${escapeHtml(heading)}</h2><ul>${others\n .map(renderLinkItem)\n .join('')}</ul></section>`;\n}\n\ntype LocalizedSkill = LocalizedTakuhon['skills'][number];\n\n/**\n * Render the Skills section. With no `settings.skillCategories` it stays a flat\n * chip list (the default). When categories are configured, skills are grouped\n * by their `category` under the configured localized headings in declared\n * order; any category present on a skill but not configured renders after, with\n * its raw key as the heading; and uncategorized skills fall into a final,\n * heading-less group — so no skill is ever dropped.\n */\nfunction renderSkills(\n skills: LocalizedTakuhon['skills'],\n categories: LocalizedTakuhon['settings']['skillCategories'],\n heading: string,\n): string {\n if (skills.length === 0) return '';\n const h = escapeHtml(heading);\n const chips = (list: readonly LocalizedSkill[]): string =>\n `<ul class=\"skills\">${list.map((s) => `<li>${escapeHtml(s.label)}</li>`).join('')}</ul>`;\n\n if (!categories || categories.length === 0) {\n return `<section><h2>${h}</h2>${chips(skills)}</section>`;\n }\n\n // Bucket by category, preserving input order within each bucket. `category`\n // has minLength 1 in the schema, so '' is a safe marker for \"uncategorized\".\n const UNCAT = '';\n const buckets = new Map<string, LocalizedSkill[]>();\n for (const s of skills) {\n const key = s.category ?? UNCAT;\n const arr = buckets.get(key) ?? [];\n arr.push(s);\n buckets.set(key, arr);\n }\n\n const group = (heading: string | undefined, list: readonly LocalizedSkill[]): string =>\n `<div class=\"skill-group\">${heading !== undefined ? `<h3>${escapeHtml(heading)}</h3>` : ''}${chips(list)}</div>`;\n\n const seen = new Set<string>();\n const groups: string[] = [];\n for (const cat of categories) {\n const list = buckets.get(cat.id);\n if (!list || list.length === 0) continue;\n seen.add(cat.id);\n groups.push(group(cat.label, list));\n }\n for (const [key, list] of buckets) {\n if (key === UNCAT || seen.has(key)) continue;\n groups.push(group(key, list));\n }\n const uncategorized = buckets.get(UNCAT);\n if (uncategorized && uncategorized.length > 0) groups.push(group(undefined, uncategorized));\n\n return `<section><h2>${h}</h2><div class=\"skills-groups\">${groups.join('')}</div></section>`;\n}\n\ntype LocalizedVolunteering = LocalizedTakuhon['volunteering'][number];\ntype LocalizedSecondaryLink = NonNullable<LocalizedVolunteering['secondaryLink']>;\n\n/**\n * A human-ish label from a URL host (leading `www.` dropped). Falls back to the\n * raw URL when the URL is unparseable OR carries no host (e.g. `mailto:` / `tel:`,\n * whose `hostname` is `''`) — so a label-less secondary link never renders as an\n * empty pill.\n */\nfunction hostLabel(url: string): string {\n try {\n const host = new URL(url).hostname.replace(/^www\\./, '');\n return host || url;\n } catch {\n return url;\n }\n}\n\n/**\n * A small trailing pill for a volunteering entry's secondary link (e.g. its\n * GitHub org page). The brand glyph is resolved from the URL host; the label\n * falls back to the host when absent. Rendered as a plain (non-link) span when\n * the URL is unsafe, so the label is never dropped but no unsafe href is emitted.\n */\nfunction secondaryLinkPill(link: LocalizedSecondaryLink): string {\n const label = escapeHtml(link.label ?? hostLabel(link.url));\n const href = safeUrl(link.url);\n const glyph = href ? brandIconForHost(link.url) : '';\n const inner = `${glyph}<span>${label}</span>`;\n return href\n ? `<a class=\"vol-secondary\" href=\"${escapeHtml(href)}\" rel=\"noopener\">${inner}</a>`\n : `<span class=\"vol-secondary\">${inner}</span>`;\n}\n\n/**\n * Render the Volunteering section as a compact list. Each entry stacks the\n * organization (linked, when a URL is present, with the external-link icon) and\n * the role as block lines, then an optional description and an optional\n * secondary-link pill. Dates are intentionally not shown — this reads as an\n * \"involved with\" list rather than a dated timeline. Returns `''` when there\n * are none.\n */\nfunction renderVolunteering(items: readonly LocalizedVolunteering[], heading: string): string {\n if (items.length === 0) return '';\n const li = (v: LocalizedVolunteering): string => {\n const orgHref = v.url ? safeUrl(v.url) : undefined;\n const org = orgHref\n ? externalLink(orgHref, escapeHtml(v.organization))\n : escapeHtml(v.organization);\n const role = v.role ? `<p class=\"vol-role\">${escapeHtml(v.role)}</p>` : '';\n const desc = v.description ? `<p class=\"vol-desc\">${escapeHtml(v.description)}</p>` : '';\n const secondary = v.secondaryLink ? secondaryLinkPill(v.secondaryLink) : '';\n // `.vol-head` groups the org + role as the entry's head, kept apart from the\n // description and secondary-link body. It carries no layout CSS now that the\n // two lines stack as blocks — it is retained as a stable grouping hook, not\n // dead markup, so do not remove it when tidying the stylesheet.\n return `<li class=\"vol\"><div class=\"vol-head\"><p class=\"vol-org\">${org}</p>${role}</div>${desc}${secondary}</li>`;\n };\n return `<section><h2>${escapeHtml(heading)}</h2><ul class=\"vol-list\">${items\n .map(li)\n .join('')}</ul></section>`;\n}\n\nfunction renderLanguages(languages: LocalizedTakuhon['languages'], heading: string): string {\n if (languages.length === 0) return '';\n const items = languages\n .map((l) => `<li>${escapeHtml(`${l.displayName ?? l.language} — ${l.proficiency}`)}</li>`)\n .join('');\n return `<section><h2>${escapeHtml(heading)}</h2><ul class=\"entries\">${items}</ul></section>`;\n}\n\nfunction renderRecommendations(recs: LocalizedTakuhon['recommendations'], heading: string): string {\n if (recs.length === 0) return '';\n const items = recs\n .map((r) => {\n const authorHref = r.author.url ? safeUrl(r.author.url) : undefined;\n const name = authorHref\n ? `<a href=\"${escapeHtml(authorHref)}\">${escapeHtml(r.author.name)}</a>`\n : escapeHtml(r.author.name);\n const caption = [name, r.author.headline ? escapeHtml(r.author.headline) : '']\n .filter(Boolean)\n .join(', ');\n const rel = r.relationship ? ` (${escapeHtml(r.relationship)})` : '';\n return `<figure class=\"rec\"><blockquote>${escapeHtml(r.body)}</blockquote><figcaption>— ${caption}${rel}</figcaption></figure>`;\n })\n .join('');\n return `<section><h2>${escapeHtml(heading)}</h2>${items}</section>`;\n}\n\nfunction renderContact(contact: LocalizedTakuhon['contact'], heading: string): string {\n const items: string[] = [];\n if (contact.email) {\n items.push(\n `<li><a href=\"mailto:${escapeHtml(contact.email)}\">${escapeHtml(contact.email)}</a></li>`,\n );\n }\n const formHref = contact.formUrl ? safeUrl(contact.formUrl) : undefined;\n if (formHref) {\n items.push(`<li><a href=\"${escapeHtml(formHref)}\">Contact form</a></li>`);\n }\n if (items.length === 0) return '';\n return `<section><h2>${escapeHtml(heading)}</h2><ul class=\"entries\">${items.join('')}</ul></section>`;\n}\n\ntype LocalizedHighlight = LocalizedTakuhon['highlights'][number];\n\n/**\n * Human display names for common highlight platforms. `highlights[].platform` is\n * a free-form string (not an enum), so an unknown platform shows its raw value.\n */\nconst PLATFORM_DISPLAY: Record<string, string> = {\n instagram: 'Instagram',\n x: 'X',\n linkedin: 'LinkedIn',\n github: 'GitHub',\n gitlab: 'GitLab',\n youtube: 'YouTube',\n threads: 'Threads',\n facebook: 'Facebook',\n mastodon: 'Mastodon',\n bluesky: 'Bluesky',\n zenn: 'Zenn',\n qiita: 'Qiita',\n note: 'note',\n blog: 'Blog',\n event: 'Event',\n artwork: 'Artwork',\n project: 'Project',\n};\n\n/** A platform's display label: a curated name for known keys, else the raw value. */\nfunction platformDisplay(platform: string): string {\n // `platform` is free-form, so a `typeof` guard on the looked-up value both\n // narrows away `undefined` (noUncheckedIndexedAccess) and rejects an inherited\n // Object.prototype member (e.g. \"constructor\" / \"toString\" resolve to\n // functions, not strings) — falling back to the raw value in either case.\n const label = PLATFORM_DISPLAY[platform.trim().toLowerCase()];\n return typeof label === 'string' ? label : platform;\n}\n\n/** The platform badge: brand glyph (when resolvable) + platform name. */\nfunction highlightBadge(h: LocalizedHighlight): string {\n return `<span class=\"highlight-badge\">${brandIconForPlatform(h.platform, h.url)}<span>${escapeHtml(platformDisplay(h.platform))}</span></span>`;\n}\n\n/**\n * Render one highlight (\"selected post\") card: a square self-hosted thumbnail,\n * a platform badge, the title (a stretched link over the whole card when a URL\n * is present), an optional date/description/tags, and a decorative CTA. The\n * whole-card link uses the CSS stretched-link pattern (`.highlight-title\n * a::after`), so there is exactly one focusable link per card and the heading\n * semantics are preserved; the CTA is `aria-hidden` since the link's aria-label\n * already carries it.\n */\nfunction renderHighlightCard(h: LocalizedHighlight, locale: string): string {\n const imgSrc = h.image ? safeUrl(h.image) : undefined;\n const img = imgSrc\n ? `<div class=\"highlight-thumb\"><img src=\"${escapeHtml(imgSrc)}\" alt=\"${escapeHtml(h.alt)}\" loading=\"lazy\" decoding=\"async\"></div>`\n : '';\n const href = h.url ? safeUrl(h.url) : undefined;\n const name = platformDisplay(h.platform);\n const cta = isJapaneseLocale(locale) ? `${name}で見る` : `View on ${name}`;\n const title = escapeHtml(h.title);\n const titleEl = href\n ? `<h3 class=\"highlight-title\"><a href=\"${escapeHtml(href)}\" rel=\"noopener\" aria-label=\"${escapeHtml(`${h.title} — ${cta}`)}\">${title}</a></h3>`\n : `<h3 class=\"highlight-title\">${title}</h3>`;\n // `timeTag` returns an already-escaped <time> fragment, so it is inserted raw.\n const date = h.postedAt ? `<p class=\"highlight-date\">${timeTag(h.postedAt, locale)}</p>` : '';\n const desc = h.description ? `<p class=\"highlight-desc\">${escapeHtml(h.description)}</p>` : '';\n const tags =\n h.tags && h.tags.length > 0\n ? `<ul class=\"tags\">${h.tags.map((t) => `<li>${escapeHtml(t)}</li>`).join('')}</ul>`\n : '';\n const ctaEl = href\n ? `<span class=\"highlight-cta\" aria-hidden=\"true\">${escapeHtml(cta)} →</span>`\n : '';\n return `<li class=\"highlight-card\">${img}<div class=\"highlight-body\">${highlightBadge(h)}${titleEl}${date}${desc}${tags}${ctaEl}</div></li>`;\n}\n\n/**\n * Render the curated highlights (\"selected posts\") carousel: a CSS scroll-snap\n * track of cards (no JS, no autoplay), with an optional intro line under the\n * heading. Returns `''` when there are no highlights, so the section is omitted.\n */\nfunction renderHighlights(\n items: readonly LocalizedHighlight[],\n heading: string,\n intro: string | undefined,\n locale: string,\n): string {\n if (items.length === 0) return '';\n const introEl = intro ? `<p class=\"highlights-intro\">${escapeHtml(intro)}</p>` : '';\n const cards = items.map((h) => renderHighlightCard(h, locale)).join('');\n return `<section class=\"highlights\"><h2>${escapeHtml(heading)}</h2>${introEl}<ul class=\"highlights-track\" role=\"list\">${cards}</ul></section>`;\n}\n\n/**\n * Render the developer-activity section from the synced snapshot, or `''`\n * when there is none (or it carries no metric data). The SVG is generated by\n * `@takuhon/core` from stored numbers only — no external badge image — so the\n * page works under an `img-src 'self'` CSP.\n */\nfunction renderActivity(snapshot: ActivitySnapshot | undefined, heading: string): string {\n if (!snapshot) return '';\n const svg = renderActivitySvg(snapshot);\n if (svg === '') return '';\n return `<section class=\"activity\"><h2>${escapeHtml(heading)}</h2>${svg}</section>`;\n}\n\nfunction renderJsonLdScript(data: LocalizedTakuhon): string {\n const payload = JSON.stringify(generateJsonLd(data));\n return `<script type=\"application/ld+json\">${escapeJsonLd(payload)}</script>`;\n}\n\nfunction renderLocaleNav(localeNav: readonly LocaleLink[], ariaLabel: string): string {\n const items = localeNav\n .map((l) =>\n l.current\n ? `<span aria-current=\"true\">${escapeHtml(l.locale)}</span>`\n : `<a href=\"${escapeHtml(l.href)}\">${escapeHtml(l.locale)}</a>`,\n )\n .join('');\n return `<nav class=\"locales\" aria-label=\"${escapeHtml(ariaLabel)}\">${items}</nav>`;\n}\n\n/**\n * Date-range separators, chosen by locale: a wave dash for Japanese\n * (`2020 〜 2024`, the convention on the canonical site) and a spaced en-dash\n * for everything else (`2020 – 2024`, the international convention and the CV /\n * `dateRange` default). Static literals, never user data.\n */\nconst DATE_SEPARATOR_JA = ' 〜 ';\nconst DATE_SEPARATOR_DEFAULT = ' – ';\n\n/** Render a complete static HTML document for one locale-resolved profile. */\nexport function renderProfileHtml(input: RenderInput): string {\n const d = input.localized;\n const p = d.profile;\n const locale = d.resolvedLocale;\n const description = p.tagline ?? p.bio ?? '';\n\n // Section headings + chrome labels, merged in precedence order: the built-in\n // pack for the resolved locale, then the owner's `settings.sectionLabels`\n // (data-driven overrides), then the caller's per-request `labels` (code seam).\n // `omit` suppresses a section's visible rendering (never its JSON-LD, generated\n // from the full document below).\n const L: SectionLabels = {\n ...pickLabelPack(locale),\n ...(d.settings.sectionLabels ?? {}),\n ...input.labels,\n };\n const omit = new Set<SectionKey>(input.omitSections ?? []);\n const keep = (key: SectionKey, html: string): string => (omit.has(key) ? '' : html);\n\n // Every date range on the page uses the locale-appropriate separator (wave\n // dash for Japanese, en-dash otherwise) and the resolved locale; this closure\n // keeps the per-section maps below terse.\n const dateSeparator = isJapaneseLocale(locale) ? DATE_SEPARATOR_JA : DATE_SEPARATOR_DEFAULT;\n const dates = (\n start: string | undefined,\n opts: { end?: string | null; isCurrent?: boolean },\n ): string => dateRange(start, { ...opts, locale, separator: dateSeparator });\n\n const head = [\n '<meta charset=\"utf-8\">',\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">',\n `<title>${escapeHtml(p.displayName)}</title>`,\n description\n ? `<meta name=\"description\" content=\"${escapeHtml(description.slice(0, 300))}\">`\n : '',\n // Data-derived Open Graph tags (the renderer owns these; asset tags like\n // og:image and PWA/theme-color are the host's, via the `head` slot).\n '<meta property=\"og:type\" content=\"profile\">',\n `<meta property=\"og:title\" content=\"${escapeHtml(p.displayName)}\">`,\n description\n ? `<meta property=\"og:description\" content=\"${escapeHtml(description.slice(0, 300))}\">`\n : '',\n input.canonicalUrl\n ? `<meta property=\"og:url\" content=\"${escapeHtml(input.canonicalUrl)}\">`\n : '',\n input.canonicalUrl ? `<link rel=\"canonical\" href=\"${escapeHtml(input.canonicalUrl)}\">` : '',\n ...input.alternates.map(\n (a) =>\n `<link rel=\"alternate\" hreflang=\"${escapeHtml(a.hreflang)}\" href=\"${escapeHtml(a.href)}\">`,\n ),\n input.jsonLd ? renderJsonLdScript(d) : '',\n input.contact ? '<link rel=\"stylesheet\" href=\"/contact-widget.css\">' : '',\n `<style>${buildTokenCss(d.settings.appearance)}\\n${CSS}</style>`,\n // First-party <head> slot (raw, unescaped): OG image tags, manifest link, etc.\n input.slots?.head ?? '',\n ]\n .filter(Boolean)\n .join('\\n ');\n\n // Rendered HTML for every content section, keyed by its canonical\n // {@link SectionKey}. Assembled in the default order below.\n const sections: Record<SectionKey, string> = {\n about: renderBio(p.bio, L.about),\n careers: entryList(\n L.careers,\n d.careers.map((c) => ({\n heading: c.role,\n sub: c.organization,\n subUrl: c.url,\n dates: dates(c.startDate, { end: c.endDate, isCurrent: c.isCurrent }),\n body: c.description,\n current: c.isCurrent,\n })),\n 'timeline',\n ),\n projects: entryList(\n L.projects,\n d.projects.map((x) => ({\n heading: x.title,\n url: x.url,\n role: x.role,\n dates: dates(x.startDate, { end: x.endDate }),\n body: x.description,\n tags: x.tags,\n highlighted: x.highlighted,\n })),\n 'cards',\n ),\n volunteering: renderVolunteering(d.volunteering, L.volunteering),\n skills: renderSkills(d.skills, d.settings.skillCategories, L.skills),\n activity: renderActivity(input.activitySnapshot, L.activity),\n education: entryList(\n L.education,\n d.education.map((e) => {\n const degree = nonEmpty([e.degree, e.fieldOfStudy], ', ');\n return {\n heading: degree ?? e.institution,\n sub: degree ? e.institution : undefined,\n dates: dates(e.startDate, { end: e.endDate, isCurrent: e.isCurrent }),\n body: e.description,\n url: e.url,\n };\n }),\n ),\n certifications: entryList(\n L.certifications,\n d.certifications.map((c) => ({\n heading: c.title,\n sub: c.issuingOrganization,\n dates: dates(c.issueDate, { end: c.expirationDate }),\n url: c.url,\n })),\n ),\n publications: entryList(\n L.publications,\n d.publications.map((x) => ({\n heading: x.title,\n sub: nonEmpty([x.publisher, x.coAuthors?.join(', ')], ' · '),\n dates: dates(x.date, {}),\n body: x.description,\n url: x.url ?? (x.doi ? `https://doi.org/${x.doi}` : undefined),\n })),\n ),\n honors: entryList(\n L.honors,\n d.honors.map((x) => ({\n heading: x.title,\n sub: x.issuer,\n dates: dates(x.date, {}),\n body: x.description,\n url: x.url,\n })),\n ),\n memberships: entryList(\n L.memberships,\n d.memberships.map((x) => ({\n heading: x.role ?? x.organization,\n sub: x.role ? x.organization : undefined,\n dates: dates(x.startDate, { end: x.endDate, isCurrent: x.isCurrent }),\n body: x.description,\n url: x.url,\n })),\n ),\n courses: entryList(\n L.courses,\n d.courses.map((x) => ({\n heading: x.title,\n sub: x.provider,\n dates: dates(x.completionDate, {}),\n body: x.description,\n url: x.certificateUrl,\n })),\n ),\n patents: entryList(\n L.patents,\n d.patents.map((x) => ({\n heading: x.title,\n sub: nonEmpty([x.patentNumber, x.office, x.status, x.coInventors?.join(', ')], ' · '),\n dates: dates(x.filingDate ?? x.grantDate, {}),\n body: x.description,\n url: x.url,\n })),\n ),\n testScores: entryList(\n L.testScores,\n d.testScores.map((x) => ({\n heading: `${x.title}: ${x.score}`,\n dates: dates(x.date, {}),\n body: x.description,\n url: x.url,\n })),\n ),\n languages: renderLanguages(d.languages, L.languages),\n recommendations: renderRecommendations(d.recommendations, L.recommendations),\n highlights: renderHighlights(d.highlights, L.highlights, d.settings.highlightsIntro, locale),\n contact: renderContact(d.contact, L.contact),\n };\n\n const localeNavHtml =\n input.localeNav.length > 1 ? renderLocaleNav(input.localeNav, L.localeNav) : '';\n\n // Resolve the section order: the owner's `settings.sectionOrder` first (valid,\n // de-duplicated keys only), then every remaining section in the default order,\n // so a partial list reorders what it names and leaves the rest untouched.\n const seenKeys = new Set<SectionKey>();\n const orderedKeys: SectionKey[] = [];\n for (const key of d.settings.sectionOrder ?? []) {\n if (SECTION_KEYS.includes(key) && !seenKeys.has(key)) {\n seenKeys.add(key);\n orderedKeys.push(key);\n }\n }\n for (const key of SECTION_KEYS) {\n if (!seenKeys.has(key)) orderedKeys.push(key);\n }\n\n const body = [\n renderHeader(p, localeNavHtml),\n // Featured links at the top; the remaining links are a fixed bottom section.\n renderFeaturedLinks(d.links, L.featuredLinks),\n ...orderedKeys.map((key) => keep(key, sections[key])),\n renderOtherLinks(d.links, L.otherLinks),\n input.slots?.mainEnd ?? '',\n ]\n .filter(Boolean)\n .join('\\n');\n\n // Footer (default on): a copyright/license line plus a \"Powered by Takuhon\"\n // credit unless the owner opted out (`showPoweredBy: false`).\n const spdx = d.meta?.contentLicense?.spdxId;\n // Defense in depth: `year` is typed `number`, but an untyped/JS caller could\n // pass anything; only a finite number reaches the raw-interpolated line.\n const year =\n typeof input.year === 'number' && Number.isFinite(input.year) ? input.year : undefined;\n const licenseText =\n year !== undefined\n ? `© ${year} ${escapeHtml(p.displayName)} — ${escapeHtml(spdx ?? '')}`\n : `© ${escapeHtml(p.displayName)} — ${escapeHtml(spdx ?? '')}`;\n const licenseLine = spdx ? `<p class=\"license\">${licenseText}</p>` : '';\n const poweredByLine =\n d.settings.showPoweredBy !== false\n ? `<p class=\"powered-by\">${escapeHtml(L.poweredBy)} <a href=\"https://takuhon.org/\" rel=\"noopener\">Takuhon</a></p>`\n : '';\n const footerInner = `${licenseLine}${poweredByLine}`;\n const footer = footerInner ? `<footer class=\"powered\">${footerInner}</footer>` : '';\n\n // The widget reads its config from these data-* attributes (see\n // @takuhon/contact's browser entry); `defer` lets it mount after parsing\n // without an inline bootstrap script.\n const contactScript = input.contact\n ? `<script src=\"/contact-widget.js\" data-site-key=\"${escapeHtml(input.contact.siteKey)}\"` +\n (input.contact.endpoint ? ` data-endpoint=\"${escapeHtml(input.contact.endpoint)}\"` : '') +\n ' defer></script>\\n'\n : '';\n\n // Skip link (always on): a visually-hidden a11y affordance that becomes\n // visible on focus and jumps past the chrome to `<main id=\"main\">`.\n const skipLink = `<a class=\"skip-link\" href=\"#main\">${escapeHtml(L.skipLink)}</a>\\n`;\n // First-party pre-`</body>` slot (raw, unescaped): analytics beacon, SW\n // registration, etc.\n const bodyEnd = input.slots?.bodyEnd ?? '';\n\n return (\n `<!DOCTYPE html>\\n<html lang=\"${escapeHtml(d.resolvedLocale)}\">\\n<head>\\n ${head}\\n</head>\\n` +\n `<body>\\n${skipLink}<main id=\"main\">\\n${body}\\n</main>\\n${footer ? `${footer}\\n` : ''}${contactScript}${bodyEnd}</body>\\n</html>\\n`\n );\n}\n","/**\n * Pure HTML helpers shared by the static-site renderer ({@link\n * import('./build-html.js')}) and the CV renderer ({@link\n * import('./cv-html.js')}). All are string-in / string-out with no I/O, so they\n * stay unit-testable and keep both renderers' escaping behavior identical.\n */\n\nimport { formatDate, getPresentLabel, type LocaleTag } from '@takuhon/core';\n\n/** Escape text for use in HTML element content or double/single-quoted attributes. */\nexport function escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Return `url` only when its scheme is safe to place in an `href`/`src`, else\n * `undefined`. Relative, protocol-relative, fragment, and query URLs (no\n * scheme) are allowed; among absolute URLs only `http:`, `https:`, and\n * `mailto:` are. This blocks `javascript:`, `data:`, `vbscript:`, etc. — the\n * schema validates only a generic URI, so a hostile document could otherwise\n * smuggle an executable scheme into the generated page.\n */\nexport function safeUrl(url: string): string | undefined {\n const trimmed = url.trim();\n const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed)?.[1]?.toLowerCase();\n if (scheme === undefined) return trimmed; // relative / protocol-relative / fragment\n return scheme === 'http' || scheme === 'https' || scheme === 'mailto' ? trimmed : undefined;\n}\n\n/**\n * Wrap a single ISO date in a `<time>` element: the machine-readable ISO value\n * stays verbatim in the `datetime` attribute while the visible text is the\n * locale-formatted form from `@takuhon/core`'s {@link formatDate}. Both parts\n * are escaped, so the returned fragment is already safe to insert raw — callers\n * must NOT pass it back through {@link escapeHtml}.\n */\nexport function timeTag(iso: string, locale: LocaleTag): string {\n return `<time datetime=\"${escapeHtml(iso)}\">${escapeHtml(formatDate(iso, locale))}</time>`;\n}\n\n/** The localized ongoing-role marker (en `Present` / ja `現在`), escaped. */\nexport function presentLabel(locale: LocaleTag): string {\n return escapeHtml(getPresentLabel(locale));\n}\n\n/**\n * Format a date range as an escaped HTML fragment: each bound becomes a\n * localized `<time>` element ({@link timeTag}), and a `null` end or `isCurrent`\n * renders as the localized \"Present\" marker ({@link presentLabel}). `locale` is\n * required, so a call that forgets it is a compile error rather than a silently\n * mis-formatted output. The result is fully escaped — callers insert it raw,\n * without {@link escapeHtml}; the only non-escaped literals are the static\n * separator and the `<time>` tags themselves. `separator` defaults to a\n * spaced en-dash; a caller can pass another safe, static separator (the profile\n * renderer uses a wave dash) — never user data, since it is inserted raw.\n */\nexport function dateRange(\n start: string | undefined,\n opts: { end?: string | null; isCurrent?: boolean; locale: LocaleTag; separator?: string },\n): string {\n const { end, isCurrent, locale, separator = ' – ' } = opts;\n const left = start ? timeTag(start, locale) : '';\n const right =\n isCurrent === true || end === null ? presentLabel(locale) : end ? timeTag(end, locale) : '';\n if (left && right) return `${left}${separator}${right}`;\n return left || right;\n}\n\n/** Join the non-empty values with `separator`, or `undefined` when none remain. */\nexport function nonEmpty(\n values: readonly (string | undefined)[],\n separator: string,\n): string | undefined {\n const joined = values\n .filter((v): v is string => typeof v === 'string' && v.length > 0)\n .join(separator);\n return joined.length > 0 ? joined : undefined;\n}\n","/**\n * Inline brand-logo SVG glyphs for profile links.\n *\n * Each entry is a single monochrome path drawn with `fill=\"currentColor\"`, so a\n * link inherits its icon color from the surrounding text and the page never\n * needs an `img-src` beyond `'self'` (the glyphs are inlined into the HTML, not\n * fetched).\n *\n * A glyph is resolved for a link in two steps ({@link brandIconForLink}):\n * 1. by its built-in {@link LinkType} (`github`, `linkedin`, …; `blog` reuses\n * the RSS mark). `website` and `email` have no unambiguous mark and render\n * without an icon rather than inventing a generic glyph.\n * 2. for a user-defined `custom` link, by the host of its URL — so a link to\n * `npmjs.com`, `linktr.ee`, `wakatime.com`, `wordpress.org`, or a covered\n * social host still shows its mark without hard-coding any bio-specific id.\n *\n * Sources & licensing (see this package's NOTICE):\n * - `linkedin` — Bootstrap Icons (MIT). simple-icons dropped the LinkedIn mark\n * over trademark policy, so the Bootstrap Icons glyph (viewBox 0 0 16 16) is\n * used instead.\n * - all others — Simple Icons (CC0-1.0, public domain), viewBox 0 0 24 24.\n *\n * Brand logos are trademarks of their respective owners; inclusion here does not\n * imply endorsement.\n */\n\nimport type { LinkType } from '@takuhon/core';\n\nimport { escapeHtml } from './html-helpers.js';\n\ninterface BrandIcon {\n viewBox: string;\n path: string;\n}\n\n/** Monochrome glyphs keyed by an internal glyph name (not a `LinkType`). */\nconst GLYPHS: Record<string, BrandIcon> = {\n github: {\n viewBox: '0 0 24 24',\n path: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',\n },\n gitlab: {\n viewBox: '0 0 24 24',\n path: 'm23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z',\n },\n linkedin: {\n viewBox: '0 0 16 16',\n path: 'M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z',\n },\n x: {\n viewBox: '0 0 24 24',\n path: 'M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z',\n },\n mastodon: {\n viewBox: '0 0 24 24',\n path: 'M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z',\n },\n bluesky: {\n viewBox: '0 0 24 24',\n path: 'M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026',\n },\n instagram: {\n viewBox: '0 0 24 24',\n path: 'M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077',\n },\n youtube: {\n viewBox: '0 0 24 24',\n path: 'M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z',\n },\n threads: {\n viewBox: '0 0 24 24',\n path: 'M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65Zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z',\n },\n facebook: {\n viewBox: '0 0 24 24',\n path: 'M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z',\n },\n rss: {\n viewBox: '0 0 24 24',\n path: 'M19.199 24C19.199 13.467 10.533 4.8 0 4.8V0c13.165 0 24 10.835 24 24h-4.801zM3.291 17.415c1.814 0 3.293 1.479 3.293 3.295 0 1.813-1.485 3.29-3.301 3.29C1.47 24 0 22.526 0 20.71s1.475-3.294 3.291-3.295zM15.909 24h-4.665c0-6.169-5.075-11.245-11.244-11.245V8.09c8.727 0 15.909 7.184 15.909 15.91z',\n },\n npm: {\n viewBox: '0 0 24 24',\n path: 'M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z',\n },\n linktree: {\n viewBox: '0 0 24 24',\n path: 'm13.73635 5.85251 4.00467-4.11665 2.3248 2.3808-4.20064 4.00466h5.9085v3.30473h-5.9365l4.22865 4.10766-2.3248 2.3338L12.0005 12.099l-5.74052 5.76852-2.3248-2.3248 4.22864-4.10766h-5.9375V8.12132h5.9085L3.93417 4.11666l2.3248-2.3808 4.00468 4.11665V0h3.4727zm-3.4727 10.30614h3.4727V24h-3.4727z',\n },\n wakatime: {\n viewBox: '0 0 24 24',\n path: 'M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 2.824a9.176 9.176 0 1 1 0 18.352 9.176 9.176 0 0 1 0-18.352zm5.097 5.058c-.327 0-.61.19-.764.45-1.025 1.463-2.21 3.162-3.288 4.706l-.387-.636a.897.897 0 0 0-.759-.439.901.901 0 0 0-.788.492l-.357.581-1.992-2.943a.897.897 0 0 0-.761-.446c-.514 0-.903.452-.903.96a1 1 0 0 0 .207.61l2.719 3.96c.152.272.44.47.776.47a.91.91 0 0 0 .787-.483c.046-.071.23-.368.314-.504l.324.52c-.035-.047.076.113.087.13.024.031.054.059.078.085.019.019.04.036.058.052.036.033.08.056.115.08.025.016.052.028.076.04.029.015.06.024.088.035.058.025.122.027.18.04.031.004.064.003.092.005.29 0 .546-.149.707-.36 1.4-2 2.842-4.055 4.099-5.849A.995.995 0 0 0 18 8.842c0-.508-.389-.96-.903-.96',\n },\n wordpress: {\n viewBox: '0 0 24 24',\n path: 'M21.469 6.825c.84 1.537 1.318 3.3 1.318 5.175 0 3.979-2.156 7.456-5.363 9.325l3.295-9.527c.615-1.54.82-2.771.82-3.864 0-.405-.026-.78-.07-1.11m-7.981.105c.647-.03 1.232-.105 1.232-.105.582-.075.514-.93-.067-.899 0 0-1.755.135-2.88.135-1.064 0-2.85-.15-2.85-.15-.585-.03-.661.855-.075.885 0 0 .54.061 1.125.09l1.68 4.605-2.37 7.08L5.354 6.9c.649-.03 1.234-.1 1.234-.1.585-.075.516-.93-.065-.896 0 0-1.746.138-2.874.138-.2 0-.438-.008-.69-.015C4.911 3.15 8.235 1.215 12 1.215c2.809 0 5.365 1.072 7.286 2.833-.046-.003-.091-.009-.141-.009-1.06 0-1.812.923-1.812 1.914 0 .89.513 1.643 1.06 2.531.411.72.89 1.643.89 2.977 0 .915-.354 1.994-.821 3.479l-1.075 3.585-3.9-11.61.001.014zM12 22.784c-1.059 0-2.081-.153-3.048-.437l3.237-9.406 3.315 9.087c.024.053.05.101.078.149-1.12.393-2.325.609-3.582.609M1.211 12c0-1.564.336-3.05.935-4.39L7.29 21.709C3.694 19.96 1.212 16.271 1.211 12M12 0C5.385 0 0 5.385 0 12s5.385 12 12 12 12-5.385 12-12S18.615 0 12 0',\n },\n};\n\n/**\n * Built-in link types that carry a glyph. `website`, `email`, and `custom` are\n * absent: the first two have no unambiguous mark, and a `custom` link resolves\n * by URL host instead ({@link HOST_GLYPHS}). `blog` borrows the RSS mark — the\n * conventional feed/blog symbol.\n */\nconst TYPE_GLYPHS: Partial<Record<LinkType, string>> = {\n github: 'github',\n gitlab: 'gitlab',\n linkedin: 'linkedin',\n x: 'x',\n mastodon: 'mastodon',\n bluesky: 'bluesky',\n instagram: 'instagram',\n youtube: 'youtube',\n threads: 'threads',\n facebook: 'facebook',\n rss: 'rss',\n blog: 'rss',\n};\n\n/**\n * Registrable hosts whose links carry a known glyph, for resolving `custom`\n * links by their URL (e.g. an npm profile, a Linktree, a WakaTime badge, a\n * WordPress.org profile). Matched against the URL's hostname (exact or as a\n * subdomain), so `www.npmjs.com` and `profiles.wordpress.org` both resolve.\n * Federated platforms (Mastodon) have no single host and are intentionally\n * omitted — they still resolve via their `type`.\n */\nconst HOST_GLYPHS: Record<string, string> = {\n 'github.com': 'github',\n 'gitlab.com': 'gitlab',\n 'linkedin.com': 'linkedin',\n 'x.com': 'x',\n 'twitter.com': 'x',\n 'instagram.com': 'instagram',\n 'youtube.com': 'youtube',\n 'threads.net': 'threads',\n 'facebook.com': 'facebook',\n 'bsky.app': 'bluesky',\n 'npmjs.com': 'npm',\n 'linktr.ee': 'linktree',\n 'wakatime.com': 'wakatime',\n 'wordpress.org': 'wordpress',\n};\n\n/** Resolve a glyph name from a URL's host, or `null` when none is known. */\nexport function hostGlyph(url: string): string | null {\n let host: string;\n try {\n // A dummy base lets protocol-relative (`//host/…`) and relative URLs parse —\n // matching the set the renderer accepts as an href (see `safeUrl`). The\n // reserved `.invalid` base host never matches a known domain, so a relative\n // path simply resolves to \"no glyph\" rather than a spurious match.\n host = new URL(url, 'https://takuhon.invalid').hostname.toLowerCase();\n } catch {\n return null;\n }\n for (const [domain, glyph] of Object.entries(HOST_GLYPHS)) {\n if (host === domain || host.endsWith(`.${domain}`)) return glyph;\n }\n return null;\n}\n\n/** Inline SVG markup for a glyph name, or `''` when the name is unknown. */\nfunction renderGlyph(name: string): string {\n const icon = GLYPHS[name];\n if (!icon) return '';\n return (\n `<svg class=\"brand-icon\" viewBox=\"${icon.viewBox}\" width=\"18\" height=\"18\" ` +\n `fill=\"currentColor\" aria-hidden=\"true\" focusable=\"false\">` +\n `<path d=\"${escapeHtml(icon.path)}\"/></svg>`\n );\n}\n\n/**\n * Inline SVG glyph for a link, or `''` when none is known. Resolved by the\n * built-in {@link LinkType} first; a `custom` link falls back to its URL host\n * ({@link hostGlyph}). The glyph is decorative — the adjacent text label names\n * the link — so it is `aria-hidden`. The static class and viewBox are safe\n * literals; only vetted constant path data is interpolated (escaped for defense\n * in depth), never user input.\n */\nexport function brandIconForLink(link: { type: LinkType; url: string }): string {\n const glyph = link.type === 'custom' ? hostGlyph(link.url) : TYPE_GLYPHS[link.type];\n return glyph ? renderGlyph(glyph) : '';\n}\n\n/**\n * Inline SVG glyph for a highlight's free-form `platform`, or `''` when none is\n * known. Resolved by the platform name first — a direct glyph match, with\n * `blog` reusing the RSS mark like link types do — then by the post URL's host\n * ({@link hostGlyph}). So `instagram` / `x` / `github` / … get a badge glyph\n * while an unknown platform (`zenn`, `event`, …) resolves to none and the\n * renderer falls back to a text-only badge. Decorative (`aria-hidden`); the\n * adjacent platform name carries the meaning.\n */\nexport function brandIconForPlatform(platform: string, url: string): string {\n const key = platform.trim().toLowerCase();\n // `Object.hasOwn`, not `key in GLYPHS`: `platform` is a free-form string, so a\n // value like \"constructor\" / \"toString\" would otherwise match an inherited\n // Object.prototype member and feed a non-icon into renderGlyph.\n const named = key === 'blog' ? 'rss' : Object.hasOwn(GLYPHS, key) ? key : null;\n const glyph = named ?? hostGlyph(url);\n return glyph ? renderGlyph(glyph) : '';\n}\n\n/**\n * Inline SVG glyph resolved purely from a URL's host ({@link hostGlyph}), or\n * `''` when the host carries no known brand. Used for secondary links whose only\n * signal is the URL (e.g. a volunteering entry's GitHub org page). Decorative\n * (`aria-hidden`); the adjacent label carries the meaning.\n */\nexport function brandIconForHost(url: string): string {\n const glyph = hostGlyph(url);\n return glyph ? renderGlyph(glyph) : '';\n}\n","/**\n * HTTP-layer locale resolution for the public app.\n *\n * Reads request-side locale candidates in this priority order:\n *\n * 1. `?lang=` query parameter\n * 2. URL path prefix (e.g. `/ja/`), passed in as `pathLocale`\n * 3. `takuhon_locale` cookie\n * 4. `Accept-Language` request header (q-value ordered)\n *\n * The URL-path candidate is extracted structurally by\n * `locale-prefix.ts` (`stripLocalePrefix` / `pathLocaleFromUrl`) and\n * handed to {@link resolveRequestLocales} as `pathLocale`; this module\n * does not parse the path itself. Settings-tier fallbacks\n * (`settings.defaultLocale`, `settings.fallbackLocale`,\n * `settings.availableLocales[0]`) are resolved inside `@takuhon/core`'s\n * `resolveLocale` and do not appear here.\n *\n * `resolveLocale` only exposes two caller slots (`locale`,\n * `fallbackLocale`). To avoid wasting them on tags the document can't\n * serve, candidates are filtered against `availableLocales` (case-\n * insensitive on the full tag or its primary subtag) and the matched\n * available locale token is substituted before forwarding, so a\n * primary-subtag match like `en` → `en-US` does not silently fall\n * through to the settings tier. Filtered candidates beyond the second\n * fall through to `resolveLocale`'s own settings-tier candidates, not\n * in request order — an acceptable loss given the contract.\n */\nimport type { Context } from 'hono';\nimport { getCookie } from 'hono/cookie';\n\nconst BCP47 = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/;\n\n// DoS guards. The header parser is exposed to untrusted client input,\n// so the byte budget and entry count are bounded before any per-token\n// work. Numbers are conservative defaults, not spec-derived.\nconst ACCEPT_LANG_MAX = 2048;\nconst ACCEPT_LANG_MAX_ENTRIES = 16;\nconst COOKIE_VALUE_MAX = 64;\n\nexport function isValidBcp47(tag: string): boolean {\n return BCP47.test(tag);\n}\n\ninterface AcceptLangEntry {\n readonly tag: string;\n readonly q: number;\n}\n\n/**\n * Parse an `Accept-Language` header into BCP-47 tags ordered by q\n * descending. Invalid or zero-quality entries and `*` wildcards are\n * dropped. Input larger than {@link ACCEPT_LANG_MAX} bytes or with more\n * than {@link ACCEPT_LANG_MAX_ENTRIES} comma-separated parts is\n * truncated before parsing.\n */\nexport function parseAcceptLanguage(header: string | null | undefined): string[] {\n if (!header) return [];\n const trimmed = header.length > ACCEPT_LANG_MAX ? header.slice(0, ACCEPT_LANG_MAX) : header;\n const parts = trimmed.split(',').slice(0, ACCEPT_LANG_MAX_ENTRIES);\n\n const entries: AcceptLangEntry[] = [];\n for (const rawPart of parts) {\n const segments = rawPart.split(';');\n const tagSegment = segments[0];\n if (tagSegment === undefined) continue;\n const tag = tagSegment.trim();\n if (tag === '' || tag === '*') continue;\n if (!isValidBcp47(tag)) continue;\n\n let q = 1;\n for (let i = 1; i < segments.length; i++) {\n const segment = segments[i];\n if (segment === undefined) continue;\n const match = /^\\s*q\\s*=\\s*([0-9.]+)\\s*$/i.exec(segment);\n if (!match) continue;\n const parsed = Number.parseFloat(match[1] ?? '');\n if (Number.isNaN(parsed)) {\n q = 1;\n } else if (parsed < 0 || parsed > 1) {\n // RFC 7231 §5.3.1 says values MUST be in [0, 1]; treat\n // out-of-range as missing (q=1) rather than dropping.\n q = 1;\n } else {\n q = parsed;\n }\n break;\n }\n if (q === 0) continue;\n\n entries.push({ tag, q });\n }\n\n // Stable sort: Array.prototype.sort is stable in ES2019+.\n entries.sort((a, b) => b.q - a.q);\n return entries.map((e) => e.tag);\n}\n\nfunction primarySubtag(tag: string): string {\n const dash = tag.indexOf('-');\n return (dash === -1 ? tag : tag.slice(0, dash)).toLowerCase();\n}\n\nfunction matchAvailable(tag: string, available: readonly string[]): string | undefined {\n const tagLower = tag.toLowerCase();\n const tagPrimary = primarySubtag(tag);\n // Prefer exact (case-insensitive) match over primary-subtag match so a\n // request that names a region explicitly wins over a region-stripped\n // alternative.\n for (const a of available) {\n if (a.toLowerCase() === tagLower) return a;\n }\n for (const a of available) {\n if (primarySubtag(a) === tagPrimary) return a;\n }\n return undefined;\n}\n\n/**\n * Resolve HTTP-layer locale candidates from the request — query, URL\n * path prefix, cookie, and `Accept-Language` in that priority order.\n * Returns the top two candidates that survive validation and the\n * `availableLocales` filter, after substituting the matched available\n * token so primary-subtag matches resolve correctly downstream.\n *\n * @param pathLocale The locale token extracted from a `/{locale}` path\n * prefix by `pathLocaleFromUrl`, or `undefined` when the request has\n * no path prefix. Inserted at priority #2 (after query, before\n * cookie).\n */\nexport function resolveRequestLocales(\n c: Context,\n available: readonly string[],\n pathLocale?: string,\n): { locale?: string; fallbackLocale?: string } {\n const raw: string[] = [];\n\n const query = c.req.query('lang');\n if (query !== undefined && isValidBcp47(query)) {\n raw.push(query);\n }\n\n if (pathLocale !== undefined && isValidBcp47(pathLocale)) {\n raw.push(pathLocale);\n }\n\n const cookie = getCookie(c, 'takuhon_locale');\n if (cookie !== undefined && cookie.length <= COOKIE_VALUE_MAX && isValidBcp47(cookie)) {\n raw.push(cookie);\n }\n\n const accept = c.req.header('accept-language');\n if (accept !== undefined) {\n raw.push(...parseAcceptLanguage(accept));\n }\n\n const seen = new Set<string>();\n const filtered: string[] = [];\n for (const tag of raw) {\n const matched = matchAvailable(tag, available);\n if (matched === undefined) continue;\n const key = matched.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n filtered.push(matched);\n if (filtered.length === 2) break;\n }\n\n const out: { locale?: string; fallbackLocale?: string } = {};\n if (filtered[0] !== undefined) out.locale = filtered[0];\n if (filtered[1] !== undefined) out.fallbackLocale = filtered[1];\n return out;\n}\n","/**\n * URL-path locale prefix handling for the public app.\n *\n * Implements locale resolution priority #2: a leading `/{locale}` path\n * segment, e.g. `/ja/api/profile`, ranked after the `?lang=` query (#1)\n * and before the `takuhon_locale` cookie (#3). This module is responsible\n * only for the *structural* concern — detecting and stripping the prefix\n * so the existing flat routes match — while the locale *value* it\n * extracts is fed into {@link resolveRequestLocales} at slot #2 by the\n * route handlers.\n *\n * The prefix is honored via Hono's `getPath` option rather than parametric\n * routes: {@link localePrefixGetPath} rewrites the match path so a request\n * to `/ja/api/profile` is routed to the `/api/profile` handler. Hono's\n * `route()` flattens a sub-app's routes into the parent and dispatches with\n * the *parent* router's `getPath` only, so the same function is applied on\n * both the standalone public app (for direct tests) and the adapter's\n * top-level router (production). The original request URL is untouched, so\n * handlers recover the locale token from `c.req.url`.\n */\nimport { isValidBcp47 } from './locale-resolution.js';\n\n/**\n * Remainder paths that may legitimately follow a `/{locale}` segment.\n *\n * This allowlist — NOT the BCP-47 shape check — is the load-bearing safety\n * mechanism. It keeps locale-agnostic paths (`/health`, `/api/schema`,\n * `/.well-known/*`, `/takuhon.json`) and admin paths (`/api/admin/*`,\n * `/admin/*`) from being misread as a locale prefix. Note that `api`\n * itself satisfies the BCP-47 primary-subtag shape (`[a-z]{2,3}`), so a\n * shape check alone would treat `/api/schema` as locale `api` + `/schema`;\n * the remainder allowlist is what prevents that.\n *\n * Keep this in sync with the locale-aware routes in `public-app.ts`.\n */\nexport const LOCALE_AWARE_REMAINDERS = ['/', '/api/profile', '/api/jsonld'] as const;\n\n/**\n * First-path segments that are reserved namespaces and must never be read\n * as a locale, even though they satisfy the BCP-47 shape. Without this,\n * a bare `/api` (segment `api` is a valid 2–3 letter primary subtag,\n * remainder defaults to the landing `/`) would alias the landing page\n * instead of 404ing. Other reserved roots (`admin`, `health`,\n * `takuhon.json`, `.well-known`) fail the BCP-47 shape check and need no\n * entry here; `api` is the only collision.\n */\nconst RESERVED_FIRST_SEGMENTS = new Set(['api']);\n\nfunction getPathname(url: string): string {\n return new URL(url).pathname;\n}\n\n/**\n * Split a leading `/{locale}` segment from `pathname` when — and only\n * when — the segment is BCP-47-shaped and the remainder is a locale-aware\n * route ({@link LOCALE_AWARE_REMAINDERS}).\n *\n * - `/ja/api/profile` → `{ locale: 'ja', path: '/api/profile' }`\n * - `/api/profile` → `{ path: '/api/profile' }` (remainder `/profile` not locale-aware)\n * - `/api/schema` → `{ path: '/api/schema' }` (the `api`-collision guard)\n * - `/ja/api/admin` → `{ path: '/ja/api/admin' }` (remainder `/api/admin` not locale-aware → 404, admin isolated)\n * - `/ja` and `/ja/` → `{ locale: 'ja', path: '/' }` (trailing slash normalized to landing)\n *\n * The returned `locale` is the raw path token; it is not matched against\n * `availableLocales` here (that happens downstream in\n * {@link resolveRequestLocales}), so an unknown-but-shaped prefix like\n * `/fr/` on an en/ja document strips structurally and then falls through\n * to the next resolution tier, mirroring `?lang=fr` semantics.\n */\nexport function stripLocalePrefix(pathname: string): { locale?: string; path: string } {\n // Match a leading single segment: `/seg` or `/seg/rest...`.\n const match = /^\\/([^/]+)(\\/.*)?$/.exec(pathname);\n if (match === null) return { path: pathname };\n\n const seg = match[1];\n if (seg === undefined || !isValidBcp47(seg)) return { path: pathname };\n\n // Reserved namespace segments (e.g. `api`) pass the BCP-47 shape but are\n // not locales; leave them for the route table (so `/api` 404s as before).\n if (RESERVED_FIRST_SEGMENTS.has(seg.toLowerCase())) return { path: pathname };\n\n // Normalize a bare `/ja` (no trailing content) to the landing remainder.\n const remainder = match[2] ?? '/';\n if (!(LOCALE_AWARE_REMAINDERS as readonly string[]).includes(remainder)) {\n return { path: pathname };\n }\n\n return { locale: seg, path: remainder };\n}\n\n/**\n * Hono `getPath` implementation: returns the locale-stripped path used for\n * route matching. Apply on every Hono router that dispatches the public\n * routes (the standalone public app and the adapter's top-level router).\n */\nexport function localePrefixGetPath(req: Request): string {\n return stripLocalePrefix(getPathname(req.url)).path;\n}\n\n/**\n * Extract the locale token from a request URL's path prefix, or `undefined`\n * when there is none. Used by route handlers to feed priority #2 into\n * {@link resolveRequestLocales}. Reads the original URL, which `getPath`\n * does not mutate.\n */\nexport function pathLocaleFromUrl(url: string): string | undefined {\n return stripLocalePrefix(getPathname(url)).locale;\n}\n","/**\n * @takuhon/api — Hono-based HTTP handlers and response builders for takuhon.\n *\n * Phase 3.3 introduced the public-app factory and the RFC 7807 envelope\n * helpers. Phase 3.4 adds the admin app factories (PUT/DELETE profile and\n * the inline `/admin` HTML editor) plus the `CachePurger` / `AuditLogger`\n * dependency-injection interfaces that adapters bind to a runtime.\n */\n\nexport {\n ERROR_SLUGS,\n buildProblem,\n problemResponse,\n type ErrorSlug,\n type ProblemDetails,\n type ProblemFieldError,\n type BuildProblemInput,\n type ProblemResponseInput,\n} from './error-envelope.js';\nexport {\n createPublicApp,\n type PublicAppDeps,\n type PublicRenderOptions,\n type PublicRenderCsp,\n} from './public-app.js';\n// Re-exported from @takuhon/core (it is a pure transform over core types and\n// now lives there); kept here for backwards compatibility.\nexport { applyPublicPrivacyFilter } from '@takuhon/core';\nexport {\n LOCALE_AWARE_REMAINDERS,\n localePrefixGetPath,\n pathLocaleFromUrl,\n stripLocalePrefix,\n} from './locale-prefix.js';\n\nexport { createAdminApiApp, type AdminApiAppDeps } from './admin/admin-api-app.js';\nexport { createAdminUiApp } from './admin/admin-ui-app.js';\nexport { adminAssetSecurityHeaders } from './admin/admin-asset-headers.js';\nexport {\n noopAuditLogger,\n type AuditEvent,\n type AuditEventType,\n type AuditLogger,\n} from './admin/audit-logger.js';\nexport { noopCachePurger, type CachePurger } from './admin/cache-purger.js';\n\n// Pure, core-only static HTML rendering. `renderProfileHtml` powers both the\n// server-rendered public profile page (the `GET /` route below) and the static\n// pages emitted by `@takuhon/cli`'s `build`/`dev`; keeping it here lets every\n// adapter serve the same markup the CLI writes to disk.\nexport {\n renderProfileHtml,\n escapeHtml,\n type RenderInput,\n type Alternate,\n type LocaleLink,\n} from './html/build-html.js';\nexport { generateSite, type SitePage, type GenerateOptions } from './html/site.js';\nexport { renderCvHtml } from './html/cv-html.js';\n","import {\n ConflictError,\n detectImageMime,\n exportTakuhon,\n MAX_IMAGE_BYTES,\n MAX_IMAGE_DIMENSION,\n MAX_IMAGE_FRAMES,\n NotFoundError,\n normalize,\n readImageInfo,\n stripImageMetadata,\n validate,\n type Takuhon,\n type TakuhonAssetStorage,\n type TakuhonStorage,\n type ValidationError,\n} from '@takuhon/core';\nimport { Hono } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse, type ProblemFieldError } from '../error-envelope.js';\n\nimport type { AuditLogger } from './audit-logger.js';\nimport { bearerMiddleware, getActorTokenHash } from './bearer.js';\nimport type { CachePurger } from './cache-purger.js';\nimport { originMiddleware } from './origin.js';\n\n/**\n * Defense-in-depth CSP for JSON-only responses. Nothing renders here, so\n * `'none'` everywhere is the strongest stance the spec leaves room for.\n */\nconst ADMIN_API_CSP = [\"default-src 'none'\", \"frame-ancestors 'none'\", \"base-uri 'none'\"].join(\n '; ',\n);\n\nexport interface AdminApiAppDeps {\n storage: TakuhonStorage;\n /**\n * Binary-asset store for uploaded images. Optional: when omitted (e.g. a\n * static export or a deployment without R2), `POST /assets` is not registered\n * and resolves to 404, so avatars stay URL-only.\n */\n assetStorage?: TakuhonAssetStorage;\n /** Returns the configured admin token, or undefined if no secret is set. */\n getAdminToken: () => string | undefined;\n /** Allowlist of origins permitted for browser-originating admin requests. */\n getAdminOrigins: () => string[];\n cachePurger: CachePurger;\n auditLogger: AuditLogger;\n}\n\n/**\n * Map a core `ValidationError` to the RFC 7807 field-error shape. The leading\n * `#` produces a JSON Schema-style fragment reference (`#/profile/...`) that\n * matches the example in `api.md §5`.\n */\nfunction toFieldError(e: ValidationError): ProblemFieldError {\n return { path: `#${e.pointer}`, message: e.message };\n}\n\n/**\n * Normalize an `If-Match` header value to the bare version token.\n *\n * Strips the optional `W/` weak-validator prefix before the RFC 7232\n * double-quote delimiters. Compressing CDNs (e.g. Cloudflare serving gzip/br)\n * downgrade the strong ETag we emit to a weak one (`W/\"<version>\"`), and a\n * browser echoes that weakened value straight back as `If-Match`. Without\n * stripping the prefix the comparison against the stored version never matches,\n * so every optimistic-locking save behind such a CDN fails with 409. The\n * weak/strong distinction is meaningless for our opaque version tokens, so\n * collapsing both forms to the raw value is the correct comparison.\n */\nfunction stripETag(raw: string): string {\n let trimmed = raw.trim();\n if (trimmed.startsWith('W/')) {\n trimmed = trimmed.slice(2).trim();\n }\n if (trimmed.startsWith('\"') && trimmed.endsWith('\"') && trimmed.length >= 2) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/**\n * Hono factory for `/api/admin/profile`. Mounted by adapters at `/api/admin`\n * (so the sub-app sees `/profile` as the route path).\n */\nexport function createAdminApiApp(deps: AdminApiAppDeps): Hono {\n const app = new Hono();\n\n app.use('*', async (c, next) => {\n await next();\n const h = c.res.headers;\n h.set('strict-transport-security', 'max-age=63072000; includeSubDomains; preload');\n h.set('x-content-type-options', 'nosniff');\n h.set('x-frame-options', 'DENY');\n h.set('referrer-policy', 'strict-origin-when-cross-origin');\n h.set('permissions-policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');\n h.set('content-security-policy', ADMIN_API_CSP);\n h.set('cache-control', 'private, no-store');\n });\n\n app.use('*', originMiddleware({ getAdminOrigins: deps.getAdminOrigins }));\n app.use(\n '*',\n bearerMiddleware({ getAdminToken: deps.getAdminToken, auditLogger: deps.auditLogger }),\n );\n\n app.onError((err, c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.internal,\n status: 500,\n title: 'Internal Error',\n detail: err instanceof Error ? err.message : 'Unknown failure',\n }),\n );\n\n app.notFound((c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: `No admin route matches ${new URL(c.req.url).pathname}.`,\n }),\n );\n\n app.put('/profile', async (c) => {\n const contentType = c.req.header('content-type') ?? '';\n if (!contentType.toLowerCase().startsWith('application/json')) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.unsupportedMediaType,\n status: 415,\n title: 'Unsupported Media Type',\n detail: `Content-Type must be application/json (got \"${contentType}\").`,\n });\n }\n\n let parsed: unknown;\n try {\n parsed = await c.req.json();\n } catch {\n return problemResponse(c, {\n slug: ERROR_SLUGS.badRequest,\n status: 400,\n title: 'Bad Request',\n detail: 'Request body is not valid JSON.',\n });\n }\n\n const result = validate(parsed);\n if (!result.ok) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.validationFailed,\n status: 422,\n title: 'Validation Failed',\n detail: `Schema validation failed (${String(result.errors.length)} error(s)).`,\n errors: result.errors.map(toFieldError),\n });\n }\n\n const data: Takuhon = result.data;\n const ifMatchRaw = c.req.header('if-match');\n const ifMatch = ifMatchRaw !== undefined ? stripETag(ifMatchRaw) : undefined;\n\n let saved: { version: string };\n try {\n saved = await deps.storage.saveProfile(data, ifMatch);\n } catch (e) {\n if (e instanceof ConflictError) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.conflict,\n status: 409,\n title: 'Conflict',\n detail: 'Stored profile version does not match If-Match.',\n currentVersion: e.currentVersion,\n });\n }\n throw e;\n }\n\n await deps.cachePurger.profileUpdated();\n\n const updatedAt = new Date().toISOString();\n const tokenHash = await getActorTokenHash(c);\n deps.auditLogger({\n type: 'admin.profile.update',\n timestamp: updatedAt,\n actor: { tokenHash },\n request: {\n method: 'PUT',\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n },\n result: { status: 200, version: saved.version },\n });\n\n return c.json({\n data: normalize(data),\n meta: {\n schemaVersion: data.schemaVersion,\n version: saved.version,\n updatedAt,\n },\n });\n });\n\n app.delete('/profile', async (c) => {\n await deps.storage.deleteProfile();\n await deps.cachePurger.profileDeleted();\n\n const tokenHash = await getActorTokenHash(c);\n deps.auditLogger({\n type: 'admin.profile.delete',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: {\n method: 'DELETE',\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n },\n result: { status: 204 },\n });\n\n return c.body(null, 204);\n });\n\n app.get('/export', async (c) => {\n let stored: { data: Takuhon; version: string };\n try {\n stored = await deps.storage.getProfile();\n } catch (e) {\n if (e instanceof NotFoundError) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: 'No profile has been saved yet; there is nothing to export.',\n });\n }\n throw e;\n }\n\n // Token holders receive the full document: the public privacy filter is\n // intentionally bypassed here (Spec §6.21). `exportTakuhon` with\n // `updateTimestamp: false` returns the stored document verbatim (raw\n // transport form, no envelope), so the body round-trips with\n // `importTakuhon` and preserves the real `meta.updatedAt`.\n const exported = exportTakuhon(stored.data, { updateTimestamp: false });\n\n const tokenHash = await getActorTokenHash(c);\n deps.auditLogger({\n type: 'admin.profile.export',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: {\n method: 'GET',\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n },\n result: { status: 200 },\n });\n\n // Surface the stored version so a form-based editor can load the full\n // document and its current version in a single request, then send it back\n // as `If-Match` on the next `PUT` for optimistic locking. Quoted per\n // RFC 7232, matching the public read endpoints and `stripETag`.\n c.header('etag', `\"${stored.version}\"`);\n return c.json(exported);\n });\n\n // Image upload (security.md §4). Registered only when an asset store is\n // configured; otherwise `POST /assets` falls through to the 404 handler.\n if (deps.assetStorage) {\n const assetStorage = deps.assetStorage;\n app.post('/assets', async (c) => {\n // Coarse pre-parse guard: reject before buffering the body when the\n // declared length already exceeds the limit plus a small multipart\n // overhead. The exact check on the decoded file size happens below.\n const declared = Number(c.req.header('content-length') ?? '');\n if (Number.isFinite(declared) && declared > MAX_IMAGE_BYTES + 8192) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.payloadTooLarge,\n status: 413,\n title: 'Payload Too Large',\n detail: `Image exceeds the ${String(MAX_IMAGE_BYTES)}-byte limit.`,\n });\n }\n\n let form: FormData;\n try {\n form = await c.req.formData();\n } catch {\n return problemResponse(c, {\n slug: ERROR_SLUGS.badRequest,\n status: 400,\n title: 'Bad Request',\n detail: 'Request body must be multipart/form-data with a \"file\" field.',\n });\n }\n\n const file = form.get('file');\n if (!(file instanceof File)) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.badRequest,\n status: 400,\n title: 'Bad Request',\n detail: 'Expected an uploaded file in the \"file\" field.',\n });\n }\n\n const bytes = new Uint8Array(await file.arrayBuffer());\n if (bytes.length > MAX_IMAGE_BYTES) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.payloadTooLarge,\n status: 413,\n title: 'Payload Too Large',\n detail: `Image is ${String(bytes.length)} bytes; the limit is ${String(MAX_IMAGE_BYTES)}.`,\n });\n }\n\n // Authenticate the type from the bytes, not the declared Content-Type.\n const mime = detectImageMime(bytes);\n if (mime === null) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.unsupportedMediaType,\n status: 415,\n title: 'Unsupported Media Type',\n detail: 'File is not an accepted image (JPEG, PNG, WebP, or GIF).',\n });\n }\n\n const info = readImageInfo(bytes, mime);\n if (info === null) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.validationFailed,\n status: 422,\n title: 'Validation Failed',\n detail: 'Image header could not be parsed.',\n });\n }\n if (info.width > MAX_IMAGE_DIMENSION || info.height > MAX_IMAGE_DIMENSION) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.validationFailed,\n status: 422,\n title: 'Validation Failed',\n detail: `Image is ${String(info.width)}×${String(info.height)}px; the limit is ${String(MAX_IMAGE_DIMENSION)}×${String(MAX_IMAGE_DIMENSION)}px.`,\n });\n }\n if (info.frames > MAX_IMAGE_FRAMES) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.validationFailed,\n status: 422,\n title: 'Validation Failed',\n detail: `Image has ${String(info.frames)} frames; the limit is ${String(MAX_IMAGE_FRAMES)}.`,\n });\n }\n\n // Strip metadata (EXIF/IPTC/XMP/color profile) before handing the bytes\n // to the adapter, which persists them verbatim.\n const stripped = stripImageMetadata(bytes, mime);\n // `Uint8Array.from` yields an ArrayBuffer-backed array (not the\n // SharedArrayBuffer-possible `ArrayBufferLike`), so it is a valid BlobPart.\n const record = await assetStorage.putAsset(\n new Blob([Uint8Array.from(stripped)], { type: mime }),\n {\n filename: file.name,\n contentType: mime,\n },\n );\n\n const tokenHash = await getActorTokenHash(c);\n deps.auditLogger({\n type: 'admin.asset.upload',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: {\n method: 'POST',\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n },\n result: { status: 201 },\n asset: { key: record.id, mimeType: mime, size: stripped.length },\n });\n\n return c.json(record, 201);\n });\n }\n\n app.on(['POST', 'PATCH'], '/profile', (c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.methodNotAllowed,\n status: 405,\n title: 'Method Not Allowed',\n detail: `${c.req.method} ${new URL(c.req.url).pathname} is not supported on the admin app.`,\n }),\n );\n\n return app;\n}\n","import type { Context, Next } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse } from '../error-envelope.js';\n\nimport type { AuditLogger } from './audit-logger.js';\n\n/**\n * Constant-time byte comparison.\n *\n * Iterates over `max(len(a), len(b))` bytes so wall-clock cost is independent\n * of *where* a mismatch occurs and of length differences (within the same\n * length class). Length-mismatch is folded into the accumulator so the\n * boolean result still discriminates correctly.\n *\n * This is intentionally a from-scratch implementation rather than\n * `crypto.subtle.timingSafeEqual`, which Cloudflare Workers exposes but\n * Node lacks — `@takuhon/api` is adapter-neutral.\n */\nexport function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {\n const len = a.length > b.length ? a.length : b.length;\n let diff = a.length === b.length ? 0 : 1;\n for (let i = 0; i < len; i++) {\n const ai = a[i] ?? 0;\n const bi = b[i] ?? 0;\n diff |= ai ^ bi;\n }\n return diff === 0;\n}\n\n/** SHA-256 hex digest (lowercase) over a UTF-8 string. */\nexport async function sha256Hex(input: string): Promise<string> {\n const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));\n const bytes = new Uint8Array(buf);\n let out = '';\n for (const b of bytes) {\n out += b.toString(16).padStart(2, '0');\n }\n return out;\n}\n\n/**\n * Extracts the Bearer token from the request and returns a stable\n * `sha256:<hex>` digest used as the actor identity in audit logs. Returns\n * `sha256:absent` when no token is present.\n */\nexport async function getActorTokenHash(c: Context): Promise<string> {\n const header = c.req.header('authorization') ?? '';\n const m = /^Bearer\\s+(.+)$/i.exec(header);\n const token = m?.[1] ?? '';\n if (token === '') return 'sha256:absent';\n return `sha256:${await sha256Hex(token)}`;\n}\n\nexport interface BearerMiddlewareOptions {\n /**\n * Source of the expected admin token. Returns `undefined` when the deploy\n * has not provisioned a secret — in that case every request is rejected,\n * mirroring \"no admin access\" semantics.\n */\n getAdminToken: () => string | undefined;\n auditLogger: AuditLogger;\n}\n\n/**\n * Hono middleware that gates downstream handlers on a constant-time Bearer\n * token check. Emits an audit event for both success and failure.\n */\nexport function bearerMiddleware(opts: BearerMiddlewareOptions) {\n return async (c: Context, next: Next): Promise<Response | void> => {\n const expected = opts.getAdminToken();\n const header = c.req.header('authorization') ?? '';\n const match = /^Bearer\\s+(.+)$/i.exec(header);\n const presented = match?.[1];\n\n const isOk =\n expected !== undefined &&\n presented !== undefined &&\n constantTimeEqual(new TextEncoder().encode(presented), new TextEncoder().encode(expected));\n\n const tokenHash = await getActorTokenHash(c);\n const baseRequest = {\n method: c.req.method,\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n };\n\n if (!isOk) {\n opts.auditLogger({\n type: 'admin.auth.failure',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: baseRequest,\n result: { status: 401 },\n });\n return problemResponse(c, {\n slug: ERROR_SLUGS.unauthorized,\n status: 401,\n title: 'Unauthorized',\n detail: 'Bearer token missing or invalid.',\n });\n }\n\n opts.auditLogger({\n type: 'admin.auth.success',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: baseRequest,\n result: { status: 200 },\n });\n\n await next();\n };\n}\n","import type { Context, Next } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse } from '../error-envelope.js';\n\nexport interface OriginMiddlewareOptions {\n /**\n * Returns the allowlist of admin Origins (e.g.\n * `['https://admin.example.com']`). Empty list disables the check —\n * deploys are expected to populate the env before going to production,\n * with the trade-off documented in the adapter README.\n */\n getAdminOrigins: () => string[];\n}\n\n/**\n * Hono middleware that enforces a same-origin / allowlisted-origin policy\n * when configured. Requests without an `Origin` header (curl, server-to-\n * server, native apps) are allowed through; the Bearer token is the primary\n * auth boundary and the absence of `Origin` is itself an indicator that the\n * request did not originate from a browser CSRF context.\n */\nexport function originMiddleware(opts: OriginMiddlewareOptions) {\n return async (c: Context, next: Next): Promise<Response | void> => {\n const allow = opts.getAdminOrigins();\n if (allow.length === 0) {\n await next();\n return;\n }\n const origin = c.req.header('origin');\n if (origin !== undefined && !allow.includes(origin)) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.forbidden,\n status: 403,\n title: 'Forbidden',\n detail: `Origin ${origin} is not in the admin allowlist.`,\n });\n }\n await next();\n };\n}\n","import { Hono } from 'hono';\n\nimport { renderAdminHtml } from './admin-html.js';\n\n/**\n * Per-request CSP (security.md §1.3). Differs from the public CSP:\n * - `img-src` drops `data:` and adds `blob:` for client-side previews.\n * - `style-src` and `script-src` drop `unsafe-inline` and pin a nonce.\n * - `require-trusted-types-for 'script'` blocks DOM-XSS sinks.\n */\nfunction adminCsp(nonce: string): string {\n return [\n \"default-src 'self'\",\n \"img-src 'self' blob:\",\n `style-src 'self' 'nonce-${nonce}'`,\n `script-src 'self' 'nonce-${nonce}'`,\n \"font-src 'self'\",\n \"connect-src 'self'\",\n \"frame-ancestors 'none'\",\n \"base-uri 'self'\",\n \"form-action 'self'\",\n \"require-trusted-types-for 'script'\",\n 'upgrade-insecure-requests',\n ].join('; ');\n}\n\nfunction generateNonce(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n let bin = '';\n for (const b of bytes) {\n bin += String.fromCharCode(b);\n }\n return btoa(bin);\n}\n\n/**\n * Hono factory for the `/admin` HTML editor. Mounted by adapters at\n * `/admin` (so the sub-app sees `/` as its root path). Each request gets a\n * freshly-generated nonce shared between the CSP header and the inline\n * `<style>` / `<script>` tags.\n */\nexport function createAdminUiApp(): Hono {\n const app = new Hono();\n\n app.get('/', (c) => {\n const nonce = generateNonce();\n c.header('strict-transport-security', 'max-age=63072000; includeSubDomains; preload');\n c.header('x-content-type-options', 'nosniff');\n c.header('x-frame-options', 'DENY');\n c.header('referrer-policy', 'strict-origin-when-cross-origin');\n c.header('permissions-policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');\n c.header('content-security-policy', adminCsp(nonce));\n c.header('cache-control', 'private, no-store');\n return c.html(renderAdminHtml(nonce));\n });\n\n return app;\n}\n","/**\n * Inline HTML for the minimal admin editor served at `GET /admin`.\n *\n * Single-page, no build step: a token input, a JSON textarea, and\n * Load / Save (PUT) / Delete (DELETE) buttons. Nothing is loaded until the\n * owner enters the admin token and presses Load, which fetches the *full*\n * document from the authenticated `GET /api/admin/export` (the privacy filter\n * is bypassed there, so fields and links the public profile omits are editable\n * here and survive the next Save). The public `/takuhon.json` is deliberately\n * not used as the editor source: it is privacy-filtered, so loading from it\n * would silently drop non-public data the moment the owner saved.\n *\n * The page operates under a strict CSP (`script-src 'self' 'nonce-<n>'`,\n * `style-src 'self' 'nonce-<n>'`, `require-trusted-types-for 'script'`), so\n * both the inline `<script>` and `<style>` blocks carry the request-scoped\n * nonce. We avoid `innerHTML`/`eval` so Trusted Types is non-disruptive.\n */\nexport function renderAdminHtml(nonce: string): string {\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>takuhon admin</title>\n<style nonce=\"${nonce}\">\nbody { font-family: system-ui, -apple-system, sans-serif; max-width: 960px; margin: 2rem auto; padding: 0 1rem; color: #222; }\nh1 { font-size: 1.5rem; }\np.note { color: #555; }\nlabel { display: block; margin: 1rem 0 0.25rem; font-weight: 600; }\ninput[type=password], textarea { width: 100%; box-sizing: border-box; padding: 0.5rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9rem; border: 1px solid #999; border-radius: 4px; }\ntextarea { min-height: 24rem; }\n.row { display: flex; gap: 0.5rem; margin-top: 1rem; }\nbutton { padding: 0.5rem 1rem; font-size: 0.95rem; border: 1px solid #444; background: #fafafa; border-radius: 4px; cursor: pointer; }\nbutton.danger { border-color: #b03; color: #b03; background: #fff5f5; }\n#status { margin-top: 1rem; padding: 0.75rem; border-radius: 4px; white-space: pre-wrap; word-break: break-word; }\n#status.ok { background: #e6f4ea; color: #1b5e20; border: 1px solid #82c891; }\n#status.err { background: #fdecea; color: #b71c1c; border: 1px solid #ef9a9a; }\nsmall.version { color: #555; }\n</style>\n</head>\n<body>\n<h1>takuhon admin</h1>\n<p class=\"note\">Enter your admin token, then <strong>Load</strong> to fetch the full <code>takuhon.json</code> document — including fields the public profile omits — for editing. <strong>Save</strong> writes it back; optimistic locking via <code>If-Match</code> guards concurrent edits. Nothing is loaded until you provide the token, and the token is never sent over the URL.</p>\n<label for=\"token\">Admin token</label>\n<input id=\"token\" type=\"password\" autocomplete=\"off\" spellcheck=\"false\">\n<label for=\"payload\">takuhon.json <small class=\"version\" id=\"versionLabel\"></small></label>\n<textarea id=\"payload\" spellcheck=\"false\" autocapitalize=\"off\" autocomplete=\"off\"></textarea>\n<div class=\"row\">\n <button id=\"reload\" type=\"button\">Load current</button>\n <button id=\"save\" type=\"button\">Save</button>\n <button id=\"delete\" type=\"button\" class=\"danger\">Delete profile</button>\n</div>\n<div id=\"status\" hidden></div>\n<script nonce=\"${nonce}\">\n(function () {\n var tokenEl = document.getElementById('token');\n var payloadEl = document.getElementById('payload');\n var versionEl = document.getElementById('versionLabel');\n var statusEl = document.getElementById('status');\n var ifMatch = '';\n\n function setStatus(message, ok) {\n statusEl.textContent = message;\n statusEl.className = ok ? 'ok' : 'err';\n statusEl.hidden = false;\n }\n function setVersion(etag) {\n ifMatch = etag || '';\n versionEl.textContent = ifMatch ? '(current version: ' + ifMatch + ')' : '(no stored version)';\n }\n function getToken() {\n var t = tokenEl.value.trim();\n if (!t) { setStatus('Admin token is required.', false); return null; }\n return t;\n }\n async function loadCurrent() {\n var token = getToken();\n if (!token) return;\n try {\n // The authenticated full export, NOT the public /takuhon.json: the latter\n // is privacy-filtered, so editing it would drop non-public data on Save.\n var res = await fetch('/api/admin/export', {\n cache: 'no-store',\n headers: { 'Authorization': 'Bearer ' + token }\n });\n if (res.status === 404) {\n // No profile stored yet. Start from an empty editor; Save creates it.\n setVersion('');\n payloadEl.value = '';\n setStatus('No profile stored yet. Paste a takuhon.json document and Save to create it.', true);\n return;\n }\n if (!res.ok) {\n var err = await res.json().catch(function () { return null; });\n setStatus('Failed to load profile (' + res.status + '): ' + (err ? JSON.stringify(err, null, 2) : 'check the admin token'), false);\n return;\n }\n setVersion(res.headers.get('etag'));\n var json = await res.json();\n payloadEl.value = JSON.stringify(json, null, 2);\n setStatus('Loaded the full profile for editing.', true);\n } catch (e) {\n setStatus('Network error loading profile: ' + (e && e.message ? e.message : String(e)), false);\n }\n }\n async function save() {\n var token = getToken();\n if (!token) return;\n var body;\n try {\n body = JSON.parse(payloadEl.value);\n } catch (e) {\n setStatus('JSON parse error: ' + (e && e.message ? e.message : String(e)), false);\n return;\n }\n var headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token };\n if (ifMatch) headers['If-Match'] = ifMatch;\n try {\n var res = await fetch('/api/admin/profile', { method: 'PUT', headers: headers, body: JSON.stringify(body) });\n var json = await res.json().catch(function () { return null; });\n if (res.ok && json && json.meta && json.meta.version) {\n setVersion('\"' + json.meta.version + '\"');\n setStatus('Saved. New version: ' + json.meta.version, true);\n } else {\n setStatus('Save failed (' + res.status + '): ' + (json ? JSON.stringify(json, null, 2) : 'no body'), false);\n }\n } catch (e) {\n setStatus('Network error during save: ' + (e && e.message ? e.message : String(e)), false);\n }\n }\n async function deleteProfile() {\n var token = getToken();\n if (!token) return;\n if (!confirm('Delete the profile? The bundled onboarding fixture will be shown until you save a new document.')) return;\n var headers = { 'Authorization': 'Bearer ' + token };\n try {\n var res = await fetch('/api/admin/profile', { method: 'DELETE', headers: headers });\n if (res.ok) {\n payloadEl.value = '';\n setVersion('');\n setStatus('Deleted.', true);\n } else {\n var json = await res.json().catch(function () { return null; });\n setStatus('Delete failed (' + res.status + '): ' + (json ? JSON.stringify(json, null, 2) : 'no body'), false);\n }\n } catch (e) {\n setStatus('Network error during delete: ' + (e && e.message ? e.message : String(e)), false);\n }\n }\n\n document.getElementById('save').addEventListener('click', save);\n document.getElementById('delete').addEventListener('click', deleteProfile);\n document.getElementById('reload').addEventListener('click', loadCurrent);\n // No auto-load: the editor stays empty until the owner enters the admin token\n // and presses Load. Nothing about the profile is fetched without the token.\n})();\n</script>\n</body>\n</html>\n`;\n}\n","/**\n * Strict admin Content-Security-Policy + security headers for a *bundled*\n * admin SPA served from static assets (security.md §1.2 admin pages).\n *\n * Unlike {@link createAdminUiApp}'s per-request policy, this variant carries no\n * nonce: the SPA's scripts and styles are external, same-origin files, so\n * `script-src 'self'` / `style-src 'self'` cover them without `'unsafe-inline'`.\n * `require-trusted-types-for 'script'` is retained to block DOM-XSS sinks.\n */\nconst ADMIN_ASSET_CSP = [\n \"default-src 'self'\",\n \"img-src 'self' blob:\",\n \"style-src 'self'\",\n \"script-src 'self'\",\n \"font-src 'self'\",\n \"connect-src 'self'\",\n \"frame-ancestors 'none'\",\n \"base-uri 'self'\",\n \"form-action 'self'\",\n \"require-trusted-types-for 'script'\",\n 'upgrade-insecure-requests',\n].join('; ');\n\n/**\n * Response headers to attach to admin SPA asset responses. Adapters serving the\n * bundle (e.g. Cloudflare Workers Assets) clone the asset response and apply\n * these so the admin origin keeps the strict CSP and is never cached.\n */\nexport function adminAssetSecurityHeaders(): Record<string, string> {\n return {\n 'strict-transport-security': 'max-age=63072000; includeSubDomains; preload',\n 'x-content-type-options': 'nosniff',\n 'x-frame-options': 'DENY',\n 'referrer-policy': 'strict-origin-when-cross-origin',\n 'permissions-policy': 'camera=(), microphone=(), geolocation=(), interest-cohort=()',\n 'content-security-policy': ADMIN_ASSET_CSP,\n 'cache-control': 'private, no-store',\n };\n}\n","/**\n * Structured audit-log emitter for admin actions (per security.md §5).\n *\n * Covers the auth, profile-write, and asset events. Adapters bind a concrete\n * sink (Cloudflare uses `console.log`, which Workers Tail / Logpush captures);\n * tests inject a `vi.fn()` recorder.\n */\nexport type AuditEventType =\n | 'admin.auth.success'\n | 'admin.auth.failure'\n | 'admin.profile.update'\n | 'admin.profile.delete'\n | 'admin.profile.export'\n | 'admin.asset.upload'\n | 'admin.asset.delete'\n | 'admin.cache.purge';\n\nexport interface AuditEvent {\n type: AuditEventType;\n /** ISO-8601 UTC timestamp generated at the call site. */\n timestamp: string;\n /**\n * Actor identity. `tokenHash` is `sha256:<hex>` over the presented Bearer\n * token, or `sha256:absent` when no token was supplied. The raw token is\n * never logged.\n */\n actor?: { tokenHash: string };\n request: {\n method: string;\n path: string;\n /** Originating client IP from `cf-connecting-ip`; undefined off-Cloudflare. */\n ip?: string;\n };\n result: {\n status: number;\n /** Opaque storage version emitted by `TakuhonStorage.saveProfile`. */\n version?: string;\n };\n /**\n * Asset details for `admin.asset.upload` / `admin.asset.delete` events\n * (security.md §5: object key, MIME, and size). Absent for other events.\n */\n asset?: {\n key: string;\n mimeType?: string;\n size?: number;\n };\n}\n\nexport type AuditLogger = (event: AuditEvent) => void;\n\n/** Default sink that discards events; useful for tests and bare runtimes. */\nexport const noopAuditLogger: AuditLogger = () => {\n /* no-op */\n};\n","/**\n * Cache invalidation contract for admin write paths.\n *\n * `@takuhon/api` stays adapter-neutral, so the actual edge-cache calls live\n * in each adapter (Cloudflare's `caches.default.delete`, future runtimes'\n * equivalents). Tests inject a recording implementation to assert that the\n * admin handlers fire the right purge after a successful write.\n */\nexport interface CachePurger {\n /** Called after a successful `PUT /api/admin/profile`. */\n profileUpdated(): Promise<void>;\n /** Called after a successful `DELETE /api/admin/profile`. */\n profileDeleted(): Promise<void>;\n}\n\n/**\n * No-op default: callers that don't run on an edge cache (Node dev server,\n * unit tests that don't care about purge calls) can pass this in.\n */\nexport const noopCachePurger: CachePurger = {\n profileUpdated: () => Promise.resolve(),\n profileDeleted: () => Promise.resolve(),\n};\n","/**\n * Shared static-site generation for `takuhon build` and `takuhon dev`.\n *\n * {@link generateSite} turns one validated, normalized, privacy-filtered profile\n * into the full set of pages the static surface exposes: one per available\n * locale. `build` writes each page's {@link SitePage.file} to disk; `dev` serves\n * each page's {@link SitePage.route} from memory. Keeping the per-locale loop,\n * the locale switcher links, and the canonical/hreflang logic here means both\n * commands share a single source of truth for the site's structure — the only\n * difference between them is disk write vs. in-memory serve.\n *\n * This reuses `@takuhon/core` only (validate/normalize/resolve happen upstream;\n * this module just resolves each locale and renders), so it stays bundler-free\n * and unit-testable as a pure function.\n */\n\nimport type { ActivitySnapshot, NormalizedTakuhon } from '@takuhon/core';\nimport { deriveCv, resolveLocale } from '@takuhon/core';\n\nimport { renderProfileHtml, type Alternate, type LocaleLink } from './build-html.js';\nimport { renderCvHtml } from './cv-html.js';\n\n/** One generated page: a serve route, a relative output file, and its HTML. */\nexport interface SitePage {\n /** URL path used by `dev` (default locale at `/`, others at `/<locale>/`). */\n readonly route: string;\n /** Output path used by `build`, relative to the output dir. */\n readonly file: string;\n /** Rendered HTML document. */\n readonly html: string;\n}\n\nexport interface GenerateOptions {\n /**\n * Site origin (e.g. `https://me.example`). When set, pages carry absolute\n * canonical + hreflang links; when absent those are omitted (the human locale\n * switcher is always relative either way).\n */\n readonly baseUrl?: string;\n /**\n * Synced developer-activity snapshot (the `activity.json` beside the\n * profile). Rendered as an inline-SVG section only while\n * `settings.activity.enabled` is true — the same opt-in gate the public\n * `GET /api/activity` applies — so disabling the feature drops the section\n * even if a stale snapshot remains on disk. The snapshot is locale-agnostic\n * and shared by every page.\n */\n readonly activitySnapshot?: ActivitySnapshot | null;\n /**\n * Also emit a print-ready CV/résumé page per locale (the default locale at\n * `cv.html` / route `/cv/`, others at `<locale>/cv.html` / `/<locale>/cv/`).\n * `takuhon build` gates this behind `--cv`; `takuhon dev` always enables it so\n * the page is previewable. Off by default, so a plain build is unchanged.\n */\n readonly cv?: boolean;\n}\n\n/**\n * Generate every page for a profile: the default locale first, then the rest,\n * de-duplicated. Schema.org JSON-LD is emitted unless `settings.enableJsonLd`\n * is explicitly `false`.\n */\nexport function generateSite(\n profile: NormalizedTakuhon,\n options: GenerateOptions = {},\n): SitePage[] {\n const { baseUrl } = options;\n const defaultLocale = profile.settings.defaultLocale;\n // Default locale first, then the rest, de-duplicated.\n const locales = [...new Set([defaultLocale, ...profile.settings.availableLocales])];\n const jsonLd = profile.settings.enableJsonLd !== false;\n const activitySnapshot =\n profile.settings.activity?.enabled === true\n ? (options.activitySnapshot ?? undefined)\n : undefined;\n\n const pages: SitePage[] = [];\n for (const locale of locales) {\n const localized = resolveLocale(profile, locale);\n const isDefault = locale === defaultLocale;\n\n const localeNav: LocaleLink[] = locales.map((to) => ({\n locale: to,\n href: localeHref(locale, to, defaultLocale),\n current: to === locale,\n }));\n const canonicalUrl = baseUrl ? absoluteUrl(baseUrl, locale, defaultLocale) : undefined;\n const alternates: Alternate[] = baseUrl ? buildAlternates(baseUrl, locales, defaultLocale) : [];\n\n const html = renderProfileHtml({\n localized,\n canonicalUrl,\n alternates,\n localeNav,\n jsonLd,\n activitySnapshot,\n year: new Date().getFullYear(),\n });\n pages.push({\n route: isDefault ? '/' : `/${locale}/`,\n file: isDefault ? 'index.html' : `${locale}/index.html`,\n html,\n });\n\n if (options.cv === true) {\n pages.push({\n route: isDefault ? '/cv/' : `/${locale}/cv/`,\n file: isDefault ? 'cv.html' : `${locale}/cv.html`,\n html: renderCvHtml(deriveCv(localized)),\n });\n }\n }\n return pages;\n}\n\n/** Absolute URL for a locale's page (default locale lives at the site root). */\nfunction absoluteUrl(baseUrl: string, locale: string, defaultLocale: string): string {\n return locale === defaultLocale ? `${baseUrl}/` : `${baseUrl}/${locale}/`;\n}\n\n/** hreflang alternates for every locale plus an `x-default` pointing at the default. */\nfunction buildAlternates(\n baseUrl: string,\n locales: readonly string[],\n defaultLocale: string,\n): Alternate[] {\n const alternates: Alternate[] = locales.map((locale) => ({\n hreflang: locale,\n href: absoluteUrl(baseUrl, locale, defaultLocale),\n }));\n alternates.push({\n hreflang: 'x-default',\n href: absoluteUrl(baseUrl, defaultLocale, defaultLocale),\n });\n return alternates;\n}\n\n/**\n * Depth-correct relative link from the page for `from` to the page for `to`,\n * for the human locale switcher. Always relative (independent of `baseUrl`)\n * so the switcher works regardless of where the site is hosted: the default\n * locale lives at the root, every other locale one directory deep.\n */\nfunction localeHref(from: string, to: string, defaultLocale: string): string {\n const fromRoot = from === defaultLocale;\n const toRoot = to === defaultLocale;\n if (fromRoot) return toRoot ? './' : `${to}/`;\n return toRoot ? '../' : `../${to}/`;\n}\n","/**\n * Pure HTML rendering of a {@link CvDocument} for `takuhon build --cv`.\n *\n * {@link renderCvHtml} turns one locale-resolved CV (from `@takuhon/core`'s\n * {@link deriveCv}) into a complete, self-contained static HTML résumé: an\n * inline stylesheet sized for A4 with an `@media print` block, so the page\n * reads well on screen and the browser's \"Save as PDF\" produces a clean,\n * single-column résumé. No external resources are referenced, so the page works\n * under a strict `img-src 'self'` / no-network policy (the same self-owned\n * stance the profile page and the activity card take).\n *\n * Security: every CV-derived string is escaped before it reaches the markup\n * ({@link escapeHtml}), and any URL is scheme-checked ({@link safeUrl}) so a\n * hostile document cannot smuggle a `javascript:` href into the résumé.\n */\n\nimport type { CvDocument, CvSection, LocaleTag, LocalizedLanguage } from '@takuhon/core';\n\nimport { dateRange, escapeHtml, nonEmpty, safeUrl } from './html-helpers.js';\n\n/** Heading shown for each section, by `kind`. Plain English (CV chrome). */\nconst SECTION_TITLES: Record<CvSection['kind'], string> = {\n experience: 'Experience',\n education: 'Education',\n skills: 'Skills',\n certifications: 'Certifications',\n publications: 'Publications',\n honors: 'Honors & Awards',\n courses: 'Courses',\n patents: 'Patents',\n languages: 'Languages',\n volunteering: 'Volunteering',\n memberships: 'Memberships',\n};\n\n// A4-sized, single-column résumé. The screen view is centered on a neutral\n// backdrop; the print view drops the backdrop/margins so the page fills the\n// sheet, and `break-inside: avoid` keeps entries from splitting across pages.\nconst CSS = `:root{--fg:#1a1a1a;--muted:#555;--accent:#0b5fff;--line:#d9d9d9}\n*{box-sizing:border-box}\nbody{margin:0;background:#f3f4f6;color:var(--fg);font:13px/1.5 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif}\nmain{background:#fff;width:210mm;min-height:297mm;margin:1.5rem auto;padding:18mm 16mm;box-shadow:0 1px 6px rgba(0,0,0,.15)}\nh1{font-size:1.7rem;margin:0}\n.tagline{font-size:1.05rem;color:var(--muted);margin:.15rem 0 0}\n.contact{color:var(--muted);font-size:.85rem;margin:.4rem 0 0;display:flex;flex-wrap:wrap;gap:.25rem 1rem}\n.contact a{color:var(--accent)}\n.bio{margin:.75rem 0 0}\nheader{border-bottom:2px solid var(--fg);padding-bottom:.6rem;margin-bottom:.4rem}\nsection{margin-top:1.1rem}\nh2{font-size:.95rem;text-transform:uppercase;letter-spacing:.05em;color:var(--accent);border-bottom:1px solid var(--line);margin:0 0 .5rem;padding-bottom:.15rem}\nul{padding:0;margin:0;list-style:none}\n.entry{margin:0 0 .7rem;break-inside:avoid}\n.entry .row{display:flex;justify-content:space-between;gap:1rem;align-items:baseline}\n.entry h3{font-size:.95rem;margin:0}\n.entry .dates{color:var(--muted);font-size:.8rem;white-space:nowrap}\n.entry .sub{color:var(--muted);margin:.05rem 0}\n.entry p{margin:.2rem 0 0}\n.entry a{color:var(--accent)}\n.chips{display:flex;flex-wrap:wrap;gap:.3rem}\n.chips li{border:1px solid var(--line);border-radius:1rem;padding:.05rem .55rem;font-size:.8rem}\n@media print{\n body{background:#fff}\n main{width:auto;min-height:0;margin:0;padding:0;box-shadow:none}\n a{color:var(--fg)}\n}\n@page{size:A4;margin:14mm}`;\n\n/** One résumé entry: a heading row (title + dates), an optional sub line, body. */\ninterface CvEntryView {\n heading: string;\n url?: string;\n dates?: string;\n sub?: string;\n body?: string;\n}\n\nfunction renderEntry(entry: CvEntryView): string {\n const href = entry.url ? safeUrl(entry.url) : undefined;\n const title = href\n ? `<a href=\"${escapeHtml(href)}\">${escapeHtml(entry.heading)}</a>`\n : escapeHtml(entry.heading);\n // `entry.dates` is already an escaped HTML fragment from `dateRange` (localized\n // <time> elements), so it is inserted raw rather than re-escaped.\n const dates = entry.dates ? `<span class=\"dates\">${entry.dates}</span>` : '';\n const parts = [`<div class=\"row\"><h3>${title}</h3>${dates}</div>`];\n if (entry.sub) parts.push(`<p class=\"sub\">${escapeHtml(entry.sub)}</p>`);\n if (entry.body) parts.push(`<p>${escapeHtml(entry.body)}</p>`);\n return `<li class=\"entry\">${parts.join('')}</li>`;\n}\n\nfunction entryListSection(title: string, entries: readonly CvEntryView[]): string {\n return `<section><h2>${escapeHtml(title)}</h2><ul>${entries.map(renderEntry).join('')}</ul></section>`;\n}\n\n/** Skills / languages render as compact chip rows rather than dated entries. */\nfunction chipSection(title: string, labels: readonly string[]): string {\n const chips = labels.map((l) => `<li>${escapeHtml(l)}</li>`).join('');\n return `<section><h2>${escapeHtml(title)}</h2><ul class=\"chips\">${chips}</ul></section>`;\n}\n\nfunction languageLabel(l: LocalizedLanguage): string {\n return `${l.displayName ?? l.language} — ${l.proficiency}`;\n}\n\n/**\n * Render one CV section to its `<section>` markup, dispatching on `kind`. The\n * `locale` formats every date in the section (via {@link dateRange}); the\n * per-entry helpers below receive their dates already formatted, so only this\n * dispatcher needs the locale.\n */\nfunction renderSection(section: CvSection, locale: LocaleTag): string {\n const title = SECTION_TITLES[section.kind];\n switch (section.kind) {\n case 'experience':\n return entryListSection(\n title,\n section.entries.map((c) => ({\n heading: c.role,\n sub: c.organization,\n dates: dateRange(c.startDate, { end: c.endDate, isCurrent: c.isCurrent, locale }),\n body: c.description,\n url: c.url,\n })),\n );\n case 'education':\n return entryListSection(\n title,\n section.entries.map((e) => {\n const degree = nonEmpty([e.degree, e.fieldOfStudy], ', ');\n return {\n heading: degree ?? e.institution,\n sub: degree ? e.institution : undefined,\n dates: dateRange(e.startDate, { end: e.endDate, isCurrent: e.isCurrent, locale }),\n body: e.description,\n url: e.url,\n };\n }),\n );\n case 'skills':\n return chipSection(\n title,\n section.entries.map((s) => s.label),\n );\n case 'languages':\n return chipSection(title, section.entries.map(languageLabel));\n case 'certifications':\n return entryListSection(\n title,\n section.entries.map((c) => ({\n heading: c.title,\n sub: c.issuingOrganization,\n dates: dateRange(c.issueDate, { end: c.expirationDate, locale }),\n url: c.url,\n })),\n );\n case 'publications':\n return entryListSection(\n title,\n section.entries.map((x) => ({\n heading: x.title,\n sub: nonEmpty([x.publisher, x.coAuthors?.join(', ')], ' · '),\n dates: dateRange(x.date, { locale }),\n body: x.description,\n url: x.url ?? (x.doi ? `https://doi.org/${x.doi}` : undefined),\n })),\n );\n case 'honors':\n return entryListSection(\n title,\n section.entries.map((x) => ({\n heading: x.title,\n sub: x.issuer,\n dates: dateRange(x.date, { locale }),\n body: x.description,\n url: x.url,\n })),\n );\n case 'courses':\n return entryListSection(\n title,\n section.entries.map((x) => ({\n heading: x.title,\n sub: x.provider,\n dates: dateRange(x.completionDate, { locale }),\n body: x.description,\n url: x.certificateUrl,\n })),\n );\n case 'patents':\n return entryListSection(\n title,\n section.entries.map((x) => ({\n heading: x.title,\n sub: nonEmpty([x.patentNumber, x.office, x.status], ' · '),\n dates: dateRange(x.filingDate ?? x.grantDate, { locale }),\n body: x.description,\n url: x.url,\n })),\n );\n case 'volunteering':\n return entryListSection(\n title,\n section.entries.map((x) => ({\n heading: x.role,\n sub: nonEmpty([x.organization, x.cause], ' · '),\n dates: dateRange(x.startDate, { end: x.endDate, isCurrent: x.isCurrent, locale }),\n body: x.description,\n url: x.url,\n })),\n );\n case 'memberships':\n return entryListSection(\n title,\n section.entries.map((x) => ({\n heading: x.role ?? x.organization,\n sub: x.role ? x.organization : undefined,\n dates: dateRange(x.startDate, { end: x.endDate, isCurrent: x.isCurrent, locale }),\n body: x.description,\n url: x.url,\n })),\n );\n }\n}\n\nfunction renderHeader(cv: CvDocument): string {\n const h = cv.header;\n const parts = [`<h1>${escapeHtml(h.displayName)}</h1>`];\n if (h.tagline) parts.push(`<p class=\"tagline\">${escapeHtml(h.tagline)}</p>`);\n\n const contact: string[] = [];\n if (h.location) contact.push(`<span>${escapeHtml(h.location)}</span>`);\n if (h.email) {\n contact.push(`<a href=\"mailto:${escapeHtml(h.email)}\">${escapeHtml(h.email)}</a>`);\n }\n const formHref = h.formUrl ? safeUrl(h.formUrl) : undefined;\n if (formHref) contact.push(`<a href=\"${escapeHtml(formHref)}\">Contact</a>`);\n if (contact.length > 0) parts.push(`<div class=\"contact\">${contact.join('')}</div>`);\n\n if (h.bio) parts.push(`<p class=\"bio\">${escapeHtml(h.bio)}</p>`);\n return `<header>${parts.join('')}</header>`;\n}\n\n/** Render a complete static HTML résumé document for one locale-resolved CV. */\nexport function renderCvHtml(cv: CvDocument): string {\n const title = `${cv.header.displayName} — CV`;\n const body = [\n renderHeader(cv),\n ...cv.sections.map((s) => renderSection(s, cv.resolvedLocale)),\n ].join('\\n');\n return (\n `<!DOCTYPE html>\\n<html lang=\"${escapeHtml(cv.resolvedLocale)}\">\\n<head>\\n` +\n ` <meta charset=\"utf-8\">\\n` +\n ` <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\\n` +\n ` <title>${escapeHtml(title)}</title>\\n` +\n ` <style>${CSS}</style>\\n</head>\\n` +\n `<body>\\n<main>\\n${body}\\n</main>\\n</body>\\n</html>\\n`\n );\n}\n"],"mappings":"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","names":["generateJsonLd","heading","generateJsonLd","applyPublicPrivacyFilter","NotFoundError","normalize","Hono","Hono","normalize","NotFoundError","Hono","Hono","resolveLocale","CSS","renderEntry","renderHeader","resolveLocale"]}
|