@takuhon/cloudflare 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference types="@cloudflare/workers-types" />
2
+ import { PublicRenderOptions } from '@takuhon/api';
2
3
  import { Takuhon } from '@takuhon/core';
3
4
  import { Inquiry, EmailTransport, ChallengeVerifier } from '@takuhon/contact';
4
5
 
@@ -155,6 +156,15 @@ interface CreateTakuhonWorkerOptions {
155
156
  * the resulting value.
156
157
  */
157
158
  readonly fallback: () => Takuhon;
159
+ /**
160
+ * First-party host composition for the server-rendered profile page: renderer
161
+ * `slots` / `labels` / `omitSections` plus an optional CSP extension (see
162
+ * {@link PublicRenderOptions}). Lets a host worker inject its own C-group
163
+ * (PWA registration, analytics beacon, extra `<head>` tags) and widen the
164
+ * page CSP to allow those scripts. Omitted (the turnkey default) leaves the
165
+ * page and its strict CSP untouched.
166
+ */
167
+ readonly render?: PublicRenderOptions;
158
168
  }
159
169
  /**
160
170
  * Build a Cloudflare Worker handler for the takuhon adapter. Wires
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import { Hono } from "hono";
15
15
 
16
16
  // ../../examples/personal-profile/takuhon.json
17
17
  var takuhon_default = {
18
- schemaVersion: "1.3.0",
18
+ schemaVersion: "1.4.0",
19
19
  profile: {
20
20
  displayName: {
21
21
  en: "Pat Rivera",
@@ -140,6 +140,10 @@ var takuhon_default = {
140
140
  en: "axe-helpers",
141
141
  ja: "axe-helpers"
142
142
  },
143
+ role: {
144
+ en: "Author & maintainer",
145
+ ja: "\u4F5C\u8005\u30FB\u30E1\u30F3\u30C6\u30CA"
146
+ },
143
147
  description: {
144
148
  en: "A tiny set of utilities that wraps axe-core for use in component-level integration tests.",
145
149
  ja: "\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8\u5358\u4F4D\u306E\u7D71\u5408\u30C6\u30B9\u30C8\u5411\u3051\u306B axe-core \u3092\u30E9\u30C3\u30D7\u3059\u308B\u5C0F\u898F\u6A21\u30E6\u30FC\u30C6\u30A3\u30EA\u30C6\u30A3\u96C6\u3002"
@@ -183,7 +187,12 @@ var takuhon_default = {
183
187
  { id: "aria", label: "ARIA", category: "design", order: 3 },
184
188
  { id: "storybook", label: "Storybook", category: "programming", order: 4 },
185
189
  { id: "playwright", label: "Playwright", category: "programming", order: 5 },
186
- { id: "design-tokens", label: "Design tokens", category: "design", order: 6 },
190
+ {
191
+ id: "design-tokens",
192
+ label: { en: "Design tokens", ja: "\u30C7\u30B6\u30A4\u30F3\u30C8\u30FC\u30AF\u30F3" },
193
+ category: "design",
194
+ order: 6
195
+ },
187
196
  { id: "portuguese", label: "Portuguese (B2)", category: "language", order: 7 }
188
197
  ],
189
198
  certifications: [
@@ -265,6 +274,13 @@ var takuhon_default = {
265
274
  endDate: "2024-06",
266
275
  isCurrent: false,
267
276
  url: "https://example.org/code-org",
277
+ secondaryLink: {
278
+ url: "https://github.com/code-dot-org",
279
+ label: {
280
+ en: "GitHub organization",
281
+ ja: "GitHub Organization"
282
+ }
283
+ },
268
284
  order: 0
269
285
  }
270
286
  ],
@@ -492,6 +508,45 @@ var takuhon_default = {
492
508
  order: 1
493
509
  }
494
510
  ],
511
+ highlights: [
512
+ {
513
+ id: "post-axe-helpers-launch",
514
+ platform: "github",
515
+ url: "https://example.com/pat/axe-helpers",
516
+ image: "/assets/highlights/axe-helpers.webp",
517
+ alt: {
518
+ en: "Terminal screenshot of axe-helpers running in a test suite.",
519
+ ja: "\u30C6\u30B9\u30C8\u30B9\u30A4\u30FC\u30C8\u3067 axe-helpers \u304C\u52D5\u4F5C\u3059\u308B\u30BF\u30FC\u30DF\u30CA\u30EB\u306E\u30B9\u30AF\u30EA\u30FC\u30F3\u30B7\u30E7\u30C3\u30C8\u3002"
520
+ },
521
+ title: {
522
+ en: "axe-helpers hits 1.0",
523
+ ja: "axe-helpers \u304C 1.0 \u306B\u5230\u9054"
524
+ },
525
+ description: {
526
+ en: "The accessibility test utilities are now stable, with docs and recipes.",
527
+ ja: "\u30A2\u30AF\u30BB\u30B7\u30D3\u30EA\u30C6\u30A3\u30C6\u30B9\u30C8\u30E6\u30FC\u30C6\u30A3\u30EA\u30C6\u30A3\u304C\u5B89\u5B9A\u7248\u306B\u3002\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8\u3068\u30EC\u30B7\u30D4\u3082\u6574\u5099\u3002"
528
+ },
529
+ postedAt: "2024-05-18",
530
+ tags: ["accessibility", "release"],
531
+ order: 0
532
+ },
533
+ {
534
+ id: "post-a11y-meetup-talk",
535
+ platform: "event",
536
+ url: "https://example.org/meetups/a11y-lisbon-2024",
537
+ image: "/assets/highlights/a11y-meetup.webp",
538
+ alt: {
539
+ en: "Pat presenting on stage at a local accessibility meetup.",
540
+ ja: "\u5730\u57DF\u306E\u30A2\u30AF\u30BB\u30B7\u30D3\u30EA\u30C6\u30A3\u52C9\u5F37\u4F1A\u3067\u767B\u58C7\u3059\u308B Pat\u3002"
541
+ },
542
+ title: {
543
+ en: "Talk: Testing for non-visual journeys",
544
+ ja: "\u767B\u58C7: \u975E\u8996\u899A\u7684\u306A\u5C0E\u7DDA\u3092\u30C6\u30B9\u30C8\u3059\u308B"
545
+ },
546
+ postedAt: "2024-03-09",
547
+ order: 1
548
+ }
549
+ ],
495
550
  contact: {
496
551
  email: "pat@example.com",
497
552
  showEmail: false,
@@ -513,7 +568,16 @@ var takuhon_default = {
513
568
  label: { en: "Design & accessibility", ja: "\u30C7\u30B6\u30A4\u30F3\u30FB\u30A2\u30AF\u30BB\u30B7\u30D3\u30EA\u30C6\u30A3" }
514
569
  },
515
570
  { id: "language", label: { en: "Languages", ja: "\u8A00\u8A9E" } }
516
- ]
571
+ ],
572
+ sectionOrder: ["about", "projects", "careers", "skills"],
573
+ sectionLabels: {
574
+ projects: { en: "Selected work", ja: "\u4E3B\u306A\u5236\u4F5C" },
575
+ volunteering: { en: "Open-source contributions", ja: "OSS \u30B3\u30F3\u30C8\u30EA\u30D3\u30E5\u30FC\u30B7\u30E7\u30F3" }
576
+ },
577
+ highlightsIntro: {
578
+ en: "A few recent posts and talks.",
579
+ ja: "\u6700\u8FD1\u306E\u6295\u7A3F\u3068\u767B\u58C7\u3092\u3044\u304F\u3064\u304B\u3002"
580
+ }
517
581
  },
518
582
  meta: {
519
583
  createdAt: "2026-01-15T09:00:00Z",
@@ -1090,7 +1154,10 @@ function createTakuhonWorker(opts) {
1090
1154
  // 404s while no snapshot is stored or activity is not enabled.
1091
1155
  activityStorage: new KvActivityStorage(env.TAKUHON_KV),
1092
1156
  // Advertise the read-only MCP endpoint in `/.well-known/takuhon.json`.
1093
- mcpPath: "/mcp"
1157
+ mcpPath: "/mcp",
1158
+ // Host composition (slots / labels / omitSections / CSP), passed
1159
+ // straight through to the profile page. Undefined by default.
1160
+ render: opts.render
1094
1161
  })
1095
1162
  );
1096
1163
  return router.fetch(request, env);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../../../examples/personal-profile/takuhon.json","../src/activity-svg.ts","../src/kv-activity-storage.ts","../src/kv-storage.ts","../src/activity-sync.ts","../src/admin/cloudflare-cache-purger.ts","../src/admin/console-audit-logger.ts","../src/contact.ts","../src/mcp.ts","../src/r2-storage.ts"],"sourcesContent":["import {\n ERROR_SLUGS,\n adminAssetSecurityHeaders,\n createAdminApiApp,\n createAdminUiApp,\n createPublicApp,\n localePrefixGetPath,\n problemResponse,\n type AuditLogger,\n type CachePurger,\n} from '@takuhon/api';\nimport { handleContact } from '@takuhon/contact';\nimport { contactWidgetCss, contactWidgetJs } from '@takuhon/contact/assets';\nimport { validate, type Takuhon } from '@takuhon/core';\nimport { Hono } from 'hono';\n\nimport exampleJson from '../../../examples/personal-profile/takuhon.json' with { type: 'json' };\n\nimport { serveActivitySvg } from './activity-svg.js';\nimport { syncActivity } from './activity-sync.js';\nimport { CloudflareCachePurger } from './admin/cloudflare-cache-purger.js';\nimport { consoleAuditLogger } from './admin/console-audit-logger.js';\nimport {\n createSendEmailTransport,\n createTurnstileVerifier,\n type SendEmailBinding,\n} from './contact.js';\nimport { KvActivityStorage } from './kv-activity-storage.js';\nimport { KvTakuhonStorage } from './kv-storage.js';\nimport { serveMcp } from './mcp.js';\nimport { ASSET_CACHE_CONTROL, R2TakuhonAssetStorage } from './r2-storage.js';\n\nexport { buildInquiryEmail, createSendEmailTransport, createTurnstileVerifier } from './contact.js';\nexport type {\n InquiryEmail,\n SendEmailBinding,\n SendEmailTransportConfig,\n TurnstileVerifierOptions,\n} from './contact.js';\n\nexport interface Env {\n TAKUHON_KV: KVNamespace;\n /**\n * Admin bearer token. Provision via `wrangler secret put TAKUHON_ADMIN_TOKEN`.\n * Leave unset to disable admin writes entirely (every PUT/DELETE returns 401).\n */\n TAKUHON_ADMIN_TOKEN?: string;\n /**\n * Comma-separated Origin allowlist for browser-originating admin requests.\n * Empty / unset disables the check (deploy without a configured allowlist is\n * acceptable when the admin UI is same-origin; documented in the README).\n */\n TAKUHON_ADMIN_ORIGIN?: string;\n /**\n * Workers Assets binding holding the bundled admin SPA (`apps/admin`). When\n * present, `/admin/*` is served from it under a strict CSP; when absent, the\n * Worker falls back to the inline `createAdminUiApp` editor, so deployments\n * without Workers Assets configured still have a working admin.\n */\n ASSETS?: Fetcher;\n /**\n * R2 bucket holding uploaded image assets. Optional, mirroring {@link ASSETS}:\n * when bound, `POST /api/admin/assets` stores images here and `GET /assets/*`\n * serves them; when absent, the upload endpoint stays unregistered (404) and\n * avatars remain URL-only.\n */\n TAKUHON_R2?: R2Bucket;\n /**\n * GitHub token for the scheduled activity sync. Optional: languages are\n * fetched unauthenticated without it; the contribution calendar (GraphQL,\n * token-only) is simply skipped. Provision via\n * `wrangler secret put TAKUHON_GITHUB_TOKEN` — never in `wrangler.toml`.\n */\n TAKUHON_GITHUB_TOKEN?: string;\n /**\n * WakaTime API key for the scheduled activity sync; required to read coding\n * time (WakaTime has no unauthenticated mode). Provision via\n * `wrangler secret put TAKUHON_WAKATIME_KEY` — never in `wrangler.toml`.\n * Only flows through the sync step; never persisted.\n */\n TAKUHON_WAKATIME_KEY?: string;\n /**\n * Cloudflare `send_email` binding that delivers contact-form submissions.\n * Optional, like {@link ASSETS} / {@link TAKUHON_R2}: when bound, `POST\n * /api/contact` is mounted (and additionally requires `settings.contact`\n * `.enabled`); when absent the endpoint stays unmounted, so a stray POST\n * 405s and the contact form is effectively off regardless of the profile.\n * Provision with a `[[send_email]]` block in `wrangler.toml`.\n */\n TAKUHON_CONTACT_EMAIL?: SendEmailBinding;\n /**\n * Verified destination inbox for contact submissions (must be a\n * Cloudflare Email Routing-verified address). A missing value degrades to a\n * failed delivery (502), never a crash.\n */\n TAKUHON_CONTACT_TO?: string;\n /**\n * From address for contact emails; must be on a domain you control\n * (e.g. `noreply@example.com`). A missing value degrades to a 502.\n */\n TAKUHON_CONTACT_FROM?: string;\n /**\n * Turnstile secret key for server-side `siteverify`. Provision via\n * `wrangler secret put TAKUHON_TURNSTILE_SECRET` — never in `wrangler.toml`.\n * A missing secret degrades to a rejected challenge (422), never a crash.\n * The public site key is not here — it lives in `settings.contact`\n * `.turnstileSiteKey` because it is embedded in the page.\n */\n TAKUHON_TURNSTILE_SECRET?: string;\n}\n\n/** Options accepted by {@link createTakuhonWorker}. */\nexport interface CreateTakuhonWorkerOptions {\n /**\n * Lazy producer for the fallback Takuhon document served when KV has no\n * stored profile yet. Called at most once per Worker invocation, on the\n * cold path where the storage layer returns no entry. Implementations\n * typically import a bundled `takuhon.json`, validate it once, and return\n * the resulting value.\n */\n readonly fallback: () => Takuhon;\n}\n\nfunction parseOrigins(raw: string | undefined): string[] {\n if (raw === undefined || raw === '') return [];\n return raw\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s !== '');\n}\n\n/** Admin UI (not the `/api/admin` API) request paths served from the SPA bundle. */\nfunction isAdminUiPath(pathname: string): boolean {\n return pathname === '/admin' || pathname.startsWith('/admin/');\n}\n\n/**\n * Public asset-delivery paths served from R2. Matches only the literal\n * `/assets/...` prefix, so a locale-prefixed `/{locale}/assets/...` is left to\n * the router (which 404s it) — asset delivery is intentionally locale-agnostic,\n * like `/health` and the admin surface.\n */\nfunction isAssetPath(pathname: string): boolean {\n return pathname.startsWith('/assets/');\n}\n\n/**\n * The read-only MCP endpoint. Matches only the literal `/mcp`, so a\n * locale-prefixed `/{locale}/mcp` is left to the router (which 404s it) — the\n * endpoint is intentionally locale-agnostic, like `/health` and `/assets/*`.\n */\nfunction isMcpPath(pathname: string): boolean {\n return pathname === '/mcp';\n}\n\n/**\n * The read-only activity-badge endpoint. Matches only the literal\n * `/activity.svg`, so a locale-prefixed `/{locale}/activity.svg` is left to the\n * router (which 404s it) — the endpoint is intentionally locale-agnostic, like\n * `/health`, `/assets/*`, and `/mcp`.\n */\nfunction isActivitySvgPath(pathname: string): boolean {\n return pathname === '/activity.svg';\n}\n\n/**\n * The bundled contact-widget assets. Matches only the literal\n * `/contact-widget.{js,css}` (locale-agnostic, like `/mcp` and `/activity.svg`);\n * these are the URLs the server-rendered page references when the profile\n * enables the contact form.\n */\nfunction isContactWidgetPath(pathname: string): boolean {\n return pathname === '/contact-widget.js' || pathname === '/contact-widget.css';\n}\n\n/**\n * Serve a bundled contact-widget asset (`GET`/`HEAD /contact-widget.{js,css}`).\n * The content is inlined into the Worker from `@takuhon/contact/assets`, so no\n * asset-hosting binding is needed; `nosniff` keeps the served type honest and a\n * moderate cache lets it refresh on the next deploy.\n */\nfunction serveContactWidget(request: Request, url: URL): Response {\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n return new Response('Method Not Allowed', { status: 405 });\n }\n const isJs = url.pathname === '/contact-widget.js';\n const headers = new Headers();\n headers.set('content-type', isJs ? 'text/javascript; charset=utf-8' : 'text/css; charset=utf-8');\n headers.set('x-content-type-options', 'nosniff');\n headers.set('cache-control', 'public, max-age=3600');\n const body = request.method === 'HEAD' ? null : isJs ? contactWidgetJs : contactWidgetCss;\n return new Response(body, { status: 200, headers });\n}\n\n/**\n * Handle a contact-form submission (`POST /api/contact`). The caller has\n * already confirmed the `send_email` binding is bound; this additionally\n * requires `settings.contact.enabled` (so disabling the form turns off the\n * endpoint too) and then runs the portable `handleContact` pipeline with the\n * Cloudflare Turnstile + `send_email` seams. Stateless and never throws: a\n * missing secret / recipient degrades to a 422 / 502 inside the pipeline.\n */\nasync function serveContact(\n request: Request,\n env: Env,\n fallback: () => Takuhon,\n url: URL,\n): Promise<Response> {\n const storage = new KvTakuhonStorage(env.TAKUHON_KV);\n let profile: Takuhon;\n try {\n profile = (await storage.getProfile()).data;\n } catch {\n profile = fallback();\n }\n\n const contact = profile.settings.contact;\n if (contact?.enabled !== true) {\n // Form disabled in the profile: behave as if the endpoint were not mounted.\n return new Response(JSON.stringify({ ok: false, error: 'not_found' }), {\n status: 404,\n headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' },\n });\n }\n\n // The binding presence is the caller's gate; assert it for the type.\n const binding = env.TAKUHON_CONTACT_EMAIL!;\n return handleContact(request, {\n verifier: createTurnstileVerifier(env.TAKUHON_TURNSTILE_SECRET ?? ''),\n transport: createSendEmailTransport(binding, {\n to: env.TAKUHON_CONTACT_TO ?? '',\n from: { email: env.TAKUHON_CONTACT_FROM ?? '' },\n ...(contact.subjectPrefix ? { subjectPrefix: contact.subjectPrefix } : {}),\n }),\n // Same-origin only: the widget POSTs from the page it is embedded on.\n config: { allowedOrigins: [url.origin] },\n readMeta: (req) => {\n const cf = (req as { cf?: { country?: unknown } }).cf;\n const country = typeof cf?.country === 'string' ? cf.country : undefined;\n const ip = req.headers.get('cf-connecting-ip') ?? undefined;\n return { ...(country ? { country } : {}), ...(ip ? { ip } : {}) };\n },\n });\n}\n\n/**\n * Serve an uploaded asset from R2 (`GET`/`HEAD /assets/*`). This is the public,\n * unauthenticated delivery proxy: the bucket stays private and the Worker\n * mediates every read so it can force `X-Content-Type-Options: nosniff`\n * (`security.md` §4.7) and an immutable cache policy. A missing object is 404.\n */\nasync function serveAsset(request: Request, bucket: R2Bucket, url: URL): Promise<Response> {\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n return new Response('Method Not Allowed', { status: 405 });\n }\n // The R2 object key is the pathname without its leading slash, e.g.\n // `/assets/1715432100-a8f3.webp` → `assets/1715432100-a8f3.webp`.\n const key = url.pathname.slice(1);\n const object = await bucket.get(key);\n if (object === null) {\n return new Response('Not Found', { status: 404 });\n }\n const headers = new Headers();\n const contentType = object.httpMetadata?.contentType;\n if (contentType !== undefined) headers.set('content-type', contentType);\n // SECURITY-CRITICAL (security.md §4.7): force nosniff so a stored object can\n // never be reinterpreted as an active type (e.g. HTML) by the browser.\n headers.set('x-content-type-options', 'nosniff');\n headers.set('cache-control', ASSET_CACHE_CONTROL);\n headers.set('etag', object.httpEtag);\n const body = request.method === 'HEAD' ? null : object.body;\n return new Response(body, { status: 200, headers });\n}\n\n/**\n * Serve the admin SPA from the Workers Assets binding. The bundle's files live\n * at the assets root, so the `/admin` prefix is stripped before lookup; `/admin`\n * and `/admin/` map to `index.html`. The strict admin CSP / security headers are\n * applied to every response (the binding's own headers would otherwise cache\n * the operator-only UI and omit the policy).\n */\nasync function serveAdminSpa(request: Request, assets: Fetcher, url: URL): Promise<Response> {\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n return new Response('Method Not Allowed', { status: 405 });\n }\n // Strip the `/admin` mount prefix; `/admin` and `/admin/` map to the bundle\n // root `/` (which Workers Assets serves as index.html). Requesting\n // `/index.html` directly would 307-redirect under `auto-trailing-slash`.\n const rest = url.pathname.slice('/admin'.length);\n const assetPath = rest === '' ? '/' : rest;\n const assetResponse = await assets.fetch(new Request(new URL(assetPath, url.origin), request));\n const headers = new Headers(assetResponse.headers);\n for (const [name, value] of Object.entries(adminAssetSecurityHeaders())) {\n headers.set(name, value);\n }\n // The admin UI is `private, no-store`, so a conditional-request ETag carried\n // over from the binding would be misleading — drop it.\n headers.delete('etag');\n return new Response(assetResponse.body, {\n status: assetResponse.status,\n statusText: assetResponse.statusText,\n headers,\n });\n}\n\n/**\n * Build a Cloudflare Worker handler for the takuhon adapter. Wires\n * `@takuhon/api`'s public/admin app factories to the KV-backed storage,\n * Cloudflare edge cache purger, and console audit logger that ship with\n * this package.\n *\n * This is the entry point used by projects scaffolded with\n * `create-takuhon`: their `src/index.ts` imports `createTakuhonWorker`,\n * passes a `fallback` that loads the project's own `takuhon.json`, and\n * `export default`s the returned handler. The default export of this\n * module is a convenience that calls the same factory with the monorepo's\n * bundled `personal-profile` fixture.\n */\nexport function createTakuhonWorker(opts: CreateTakuhonWorkerOptions): {\n fetch: (request: Request, env: Env) => Response | Promise<Response>;\n scheduled: (controller: ScheduledController, env: Env, ctx: ExecutionContext) => Promise<void>;\n} {\n return {\n fetch(request: Request, env: Env): Response | Promise<Response> {\n const url = new URL(request.url);\n\n // Serve the bundled admin SPA when a Workers Assets binding is present.\n // Without it, the request falls through to the inline editor mounted on\n // the router below, so admin stays available either way.\n if (env.ASSETS && isAdminUiPath(url.pathname)) {\n return serveAdminSpa(request, env.ASSETS, url);\n }\n\n // Serve uploaded assets from R2 when the bucket is bound. Without it,\n // `/assets/*` falls through to the router and 404s, matching the\n // unregistered upload endpoint.\n if (env.TAKUHON_R2 && isAssetPath(url.pathname)) {\n return serveAsset(request, env.TAKUHON_R2, url);\n }\n\n // Read-only MCP endpoint. Stateless (no Durable Object / session); reads\n // the profile from the same KV the public API uses.\n if (isMcpPath(url.pathname)) {\n return serveMcp(request, env.TAKUHON_KV, opts.fallback);\n }\n\n // Read-only activity badge. Renders the synced snapshot as a\n // self-contained SVG image (?theme=dark for the dark variant), re-checking\n // the settings.activity opt-in on every request — parity with\n // GET /api/activity.\n if (isActivitySvgPath(url.pathname)) {\n return serveActivitySvg(request, env.TAKUHON_KV, opts.fallback);\n }\n\n // Bundled contact-widget assets, referenced by the server-rendered page\n // when the contact form is enabled. Served from the Worker bundle, so no\n // asset binding is required (parity with /mcp and /activity.svg).\n if (isContactWidgetPath(url.pathname)) {\n return serveContactWidget(request, url);\n }\n\n // Contact-form submission. Mounted only when the send_email binding is\n // bound; serveContact additionally requires settings.contact.enabled.\n // Without the binding the POST falls through to the public app's 405.\n if (\n url.pathname === '/api/contact' &&\n request.method === 'POST' &&\n env.TAKUHON_CONTACT_EMAIL\n ) {\n return serveContact(request, env, opts.fallback, url);\n }\n\n const storage = new KvTakuhonStorage(env.TAKUHON_KV);\n // Enable image uploads only when an R2 bucket is bound; otherwise the\n // admin API leaves `POST /assets` unregistered, so uploads are disabled\n // and avatars stay URL-only. The public URL is built on this request's\n // origin since the Worker proxies delivery from its own `/assets/*` route.\n const assetStorage = env.TAKUHON_R2\n ? new R2TakuhonAssetStorage(env.TAKUHON_R2, { publicBaseUrl: url.origin })\n : undefined;\n const cachePurger: CachePurger = new CloudflareCachePurger(() => caches.default, {\n origin: url.origin,\n });\n const auditLogger: AuditLogger = consoleAuditLogger;\n\n // `getPath` strips a leading `/{locale}` prefix before route\n // matching. This is the production-critical placement: Hono's\n // `route()` flattens each mounted sub-app's routes into this router\n // and dispatches with this router's `getPath` only, so a `getPath`\n // set on the public sub-app alone would not run here. The shared\n // function's allowlist guard never strips admin remainders, so\n // `/api/admin/*` and `/admin/*` mounts stay locale-agnostic.\n const router = new Hono({ getPath: localePrefixGetPath });\n router.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 router.route(\n '/api/admin',\n createAdminApiApp({\n storage,\n assetStorage,\n getAdminToken: () => env.TAKUHON_ADMIN_TOKEN,\n getAdminOrigins: () => parseOrigins(env.TAKUHON_ADMIN_ORIGIN),\n cachePurger,\n auditLogger,\n }),\n );\n router.route('/admin', createAdminUiApp());\n router.route(\n '/',\n createPublicApp({\n storage,\n fallback: opts.fallback,\n // Serves `GET /api/activity` from the synced snapshot; the route\n // 404s while no snapshot is stored or activity is not enabled.\n activityStorage: new KvActivityStorage(env.TAKUHON_KV),\n // Advertise the read-only MCP endpoint in `/.well-known/takuhon.json`.\n mcpPath: '/mcp',\n }),\n );\n\n return router.fetch(request, env);\n },\n\n /**\n * Cron-driven activity sync (enable with a `[triggers] crons` entry in\n * `wrangler.toml`; daily is the recommended cadence). MUST NOT throw: a\n * failed sync keeps the last-known snapshot and surfaces through the\n * structured log line below (captured by Workers Tail / Logpush), never by\n * failing the cron run.\n */\n async scheduled(_controller: ScheduledController, env: Env, _ctx: ExecutionContext) {\n const timestamp = new Date().toISOString();\n try {\n const result = await syncActivity({\n profileStorage: new KvTakuhonStorage(env.TAKUHON_KV),\n activityStorage: new KvActivityStorage(env.TAKUHON_KV),\n secrets: {\n githubToken: env.TAKUHON_GITHUB_TOKEN,\n wakatimeKey: env.TAKUHON_WAKATIME_KEY,\n },\n fallback: opts.fallback,\n });\n const type =\n result.status === 'synced'\n ? 'activity.sync.success'\n : result.status === 'empty'\n ? 'activity.sync.failure'\n : 'activity.sync.skipped';\n console.log(\n JSON.stringify({ type, timestamp, reason: result.reason, failures: result.failures }),\n );\n } catch (err) {\n console.error(\n JSON.stringify({\n type: 'activity.sync.failure',\n timestamp,\n reason: err instanceof Error ? err.message : String(err),\n failures: [],\n }),\n );\n }\n },\n };\n}\n\nfunction bundledFallback(): Takuhon {\n const r = validate(exampleJson);\n if (!r.ok) throw new Error('Bundled fixture failed validation.');\n return r.data;\n}\n\nexport default createTakuhonWorker({ fallback: bundledFallback });\n","{\n \"schemaVersion\": \"1.3.0\",\n \"profile\": {\n \"displayName\": {\n \"en\": \"Pat Rivera\",\n \"ja\": \"パット・リベラ\"\n },\n \"tagline\": {\n \"en\": \"Open-source maintainer and accessibility advocate\",\n \"ja\": \"オープンソースメンテナ / アクセシビリティ実践者\"\n },\n \"bio\": {\n \"en\": \"Pat Rivera is a fictional persona used to exercise every field of the takuhon profile schema. They maintain a handful of open-source libraries focused on accessibility tooling and frequently speak at local meetups.\",\n \"ja\": \"Pat Rivera は takuhon プロフィール schema の全フィールドを示すための架空の人物です。アクセシビリティ系のオープンソースライブラリを保守し、地域コミュニティで登壇しています。\"\n },\n \"avatar\": {\n \"url\": \"/assets/avatar.webp\",\n \"alt\": {\n \"en\": \"Pat Rivera smiling, wearing round glasses, in front of a soft gradient.\",\n \"ja\": \"ソフトなグラデーションを背景に微笑む Pat Rivera。丸眼鏡を着用。\"\n }\n },\n \"location\": {\n \"country\": \"PT\",\n \"region\": \"Lisbon\",\n \"locality\": {\n \"en\": \"Lisbon\",\n \"ja\": \"リスボン\"\n },\n \"display\": {\n \"en\": \"Lisbon, Portugal\",\n \"ja\": \"ポルトガル・リスボン\"\n }\n }\n },\n \"links\": [\n {\n \"id\": \"website\",\n \"type\": \"website\",\n \"label\": { \"en\": \"Personal site\" },\n \"url\": \"https://example.com/pat\",\n \"featured\": true,\n \"order\": 0\n },\n {\n \"id\": \"github\",\n \"type\": \"github\",\n \"url\": \"https://example.com/pat/github\",\n \"featured\": true,\n \"order\": 1\n },\n {\n \"id\": \"mastodon\",\n \"type\": \"mastodon\",\n \"url\": \"https://example.social/@pat\",\n \"order\": 2\n },\n {\n \"id\": \"blog\",\n \"type\": \"blog\",\n \"label\": {\n \"en\": \"Notes on accessible UI\",\n \"ja\": \"アクセシブル UI の覚え書き\"\n },\n \"url\": \"https://example.com/pat/blog\",\n \"order\": 3\n },\n {\n \"id\": \"newsletter\",\n \"type\": \"custom\",\n \"label\": {\n \"en\": \"Weekly newsletter\",\n \"ja\": \"週次ニュースレター\"\n },\n \"url\": \"https://example.com/pat/newsletter\",\n \"iconUrl\": \"https://example.com/assets/icons/newsletter.svg\",\n \"order\": 4\n }\n ],\n \"careers\": [\n {\n \"id\": \"stellar-ux\",\n \"organization\": {\n \"en\": \"Stellar UX Studio\",\n \"ja\": \"ステラ UX スタジオ\"\n },\n \"role\": {\n \"en\": \"Principal Accessibility Engineer\",\n \"ja\": \"プリンシパル・アクセシビリティエンジニア\"\n },\n \"description\": {\n \"en\": \"Lead the accessibility engineering practice across product surfaces, drive WCAG 2.2 conformance reviews, and mentor a team of five engineers.\",\n \"ja\": \"プロダクト全体のアクセシビリティ設計を統括。WCAG 2.2 適合レビューを主導し、5 名のエンジニアをメンタリング。\"\n },\n \"startDate\": \"2023-04\",\n \"endDate\": null,\n \"isCurrent\": true,\n \"url\": \"https://example.com/stellar\",\n \"order\": 0\n },\n {\n \"id\": \"harbor-labs\",\n \"organization\": {\n \"en\": \"Harbor Labs\",\n \"ja\": \"ハーバーラボ\"\n },\n \"role\": {\n \"en\": \"Senior Frontend Engineer\",\n \"ja\": \"シニアフロントエンドエンジニア\"\n },\n \"description\": {\n \"en\": \"Built the design system foundation and an accessible component library used across nine internal products.\",\n \"ja\": \"デザインシステム基盤と、社内 9 プロダクトで利用されるアクセシブルなコンポーネントライブラリを設計。\"\n },\n \"startDate\": \"2019-06\",\n \"endDate\": \"2023-03\",\n \"order\": 1\n }\n ],\n \"projects\": [\n {\n \"id\": \"axe-helpers\",\n \"title\": {\n \"en\": \"axe-helpers\",\n \"ja\": \"axe-helpers\"\n },\n \"description\": {\n \"en\": \"A tiny set of utilities that wraps axe-core for use in component-level integration tests.\",\n \"ja\": \"コンポーネント単位の統合テスト向けに axe-core をラップする小規模ユーティリティ集。\"\n },\n \"url\": \"https://example.com/axe-helpers\",\n \"tags\": [\"accessibility\", \"testing\", \"typescript\"],\n \"relatedCareerId\": \"stellar-ux\",\n \"startDate\": \"2023-09\",\n \"highlighted\": true,\n \"order\": 0\n },\n {\n \"id\": \"color-contrast-cli\",\n \"title\": {\n \"en\": \"color-contrast-cli\",\n \"ja\": \"color-contrast-cli\"\n },\n \"description\": {\n \"en\": \"Command-line tool that audits design tokens for WCAG contrast ratios.\",\n \"ja\": \"デザイントークンの WCAG コントラスト比を監査するコマンドラインツール。\"\n },\n \"url\": \"https://example.com/color-contrast-cli\",\n \"tags\": [\"accessibility\", \"cli\", \"design-tokens\"],\n \"startDate\": \"2021-02\",\n \"endDate\": \"2022-08\",\n \"order\": 1\n },\n {\n \"id\": \"meetup-talks\",\n \"title\": {\n \"en\": \"Local meetup talks\",\n \"ja\": \"地域コミュニティ登壇\"\n },\n \"order\": 2\n }\n ],\n \"skills\": [\n { \"id\": \"typescript\", \"label\": \"TypeScript\", \"category\": \"programming\", \"order\": 0 },\n { \"id\": \"react\", \"label\": \"React\", \"category\": \"programming\", \"order\": 1 },\n { \"id\": \"wcag-2-2\", \"label\": \"WCAG 2.2\", \"category\": \"design\", \"order\": 2 },\n { \"id\": \"aria\", \"label\": \"ARIA\", \"category\": \"design\", \"order\": 3 },\n { \"id\": \"storybook\", \"label\": \"Storybook\", \"category\": \"programming\", \"order\": 4 },\n { \"id\": \"playwright\", \"label\": \"Playwright\", \"category\": \"programming\", \"order\": 5 },\n { \"id\": \"design-tokens\", \"label\": \"Design tokens\", \"category\": \"design\", \"order\": 6 },\n { \"id\": \"portuguese\", \"label\": \"Portuguese (B2)\", \"category\": \"language\", \"order\": 7 }\n ],\n \"certifications\": [\n {\n \"id\": \"iaap-cpacc-2022\",\n \"title\": {\n \"en\": \"Certified Professional in Accessibility Core Competencies (CPACC)\",\n \"ja\": \"アクセシビリティ専門家認定 (CPACC)\"\n },\n \"issuingOrganization\": {\n \"en\": \"International Association of Accessibility Professionals\",\n \"ja\": \"国際アクセシビリティ専門家協会\"\n },\n \"issueDate\": \"2022-06\",\n \"expirationDate\": null,\n \"credentialId\": \"CPACC-2022-PR-09231\",\n \"url\": \"https://example.org/iaap/credentials/cpacc\",\n \"order\": 0\n },\n {\n \"id\": \"iaap-was-2023\",\n \"title\": {\n \"en\": \"Web Accessibility Specialist (WAS)\",\n \"ja\": \"Web アクセシビリティスペシャリスト (WAS)\"\n },\n \"issuingOrganization\": {\n \"en\": \"International Association of Accessibility Professionals\",\n \"ja\": \"国際アクセシビリティ専門家協会\"\n },\n \"issueDate\": \"2023-03\",\n \"expirationDate\": \"2026-03\",\n \"credentialId\": \"WAS-2023-PR-04522\",\n \"url\": \"https://example.org/iaap/credentials/was\",\n \"order\": 1\n }\n ],\n \"memberships\": [\n {\n \"id\": \"iaap-senior\",\n \"organization\": {\n \"en\": \"International Association of Accessibility Professionals\",\n \"ja\": \"国際アクセシビリティ専門家協会\"\n },\n \"role\": {\n \"en\": \"Senior Member\",\n \"ja\": \"シニアメンバー\"\n },\n \"description\": {\n \"en\": \"Active participant in the Web Accessibility special interest group, contributing to monthly working sessions on WCAG 2.2 interpretation.\",\n \"ja\": \"Web Accessibility 専門部会に参加し、WCAG 2.2 解釈の月例セッションに貢献。\"\n },\n \"startDate\": \"2022-09\",\n \"endDate\": null,\n \"isCurrent\": true,\n \"url\": \"https://example.org/iaap\",\n \"order\": 0\n }\n ],\n \"volunteering\": [\n {\n \"id\": \"code-org-instructor\",\n \"organization\": {\n \"en\": \"Code.org\",\n \"ja\": \"Code.org\"\n },\n \"role\": {\n \"en\": \"Volunteer Instructor\",\n \"ja\": \"ボランティア講師\"\n },\n \"cause\": {\n \"en\": \"Education\",\n \"ja\": \"教育\"\n },\n \"description\": {\n \"en\": \"Taught introductory programming concepts to middle-school students in weekend workshops, with a focus on inclusive curriculum design.\",\n \"ja\": \"週末ワークショップで中学生にプログラミング入門を指導。インクルーシブなカリキュラム設計を重視。\"\n },\n \"startDate\": \"2021-09\",\n \"endDate\": \"2024-06\",\n \"isCurrent\": false,\n \"url\": \"https://example.org/code-org\",\n \"order\": 0\n }\n ],\n \"honors\": [\n {\n \"id\": \"wai-recognized-contributor-2024\",\n \"title\": {\n \"en\": \"Recognized Contributor, Web Accessibility Initiative\",\n \"ja\": \"Web Accessibility Initiative 貢献者表彰\"\n },\n \"issuer\": {\n \"en\": \"W3C Web Accessibility Initiative\",\n \"ja\": \"W3C Web Accessibility Initiative\"\n },\n \"description\": {\n \"en\": \"Recognized for sustained reviews of the WCAG 2.2 Authoring Practices documentation and for contributions to non-visual focus management patterns.\",\n \"ja\": \"WCAG 2.2 Authoring Practices ドキュメントの継続レビュー、および非視覚的フォーカス管理パターンへの貢献を評価。\"\n },\n \"date\": \"2024-04\",\n \"url\": \"https://example.org/wai/recognitions/2024\",\n \"order\": 0\n }\n ],\n \"education\": [\n {\n \"id\": \"westbrook-bsc-2018\",\n \"institution\": {\n \"en\": \"Westbrook University\",\n \"ja\": \"ウェストブルック大学\"\n },\n \"degree\": {\n \"en\": \"BSc in Cognitive Science\",\n \"ja\": \"認知科学学士\"\n },\n \"fieldOfStudy\": {\n \"en\": \"Human-Computer Interaction\",\n \"ja\": \"ヒューマン・コンピュータ・インタラクション\"\n },\n \"description\": {\n \"en\": \"Senior thesis on screen-reader interaction patterns for non-visual users navigating dynamic web interfaces.\",\n \"ja\": \"卒業研究は動的 Web インターフェースを操作する非視覚ユーザー向けスクリーンリーダー対話パターンの分析。\"\n },\n \"grade\": \"magna cum laude\",\n \"startDate\": \"2014-09\",\n \"endDate\": \"2018-06\",\n \"isCurrent\": false,\n \"url\": \"https://example.org/westbrook\",\n \"order\": 0\n }\n ],\n \"publications\": [\n {\n \"id\": \"design-tokens-wcag-2024\",\n \"title\": {\n \"en\": \"Auditing Design Tokens for WCAG 2.2 Conformance\",\n \"ja\": \"デザイントークンの WCAG 2.2 適合性監査\"\n },\n \"publisher\": {\n \"en\": \"ACM SIGACCESS\",\n \"ja\": \"ACM SIGACCESS\"\n },\n \"description\": {\n \"en\": \"A method for statically analyzing design-token files to surface color-contrast violations before they reach implementation.\",\n \"ja\": \"実装前にデザイントークンファイルを静的解析し、色コントラスト違反を発見する手法を提案。\"\n },\n \"date\": \"2024-03\",\n \"url\": \"https://example.org/papers/design-tokens-wcag\",\n \"doi\": \"10.1145/3678901.3678910\",\n \"coAuthors\": [\"Jamie Chen\", \"Sofia Almeida\"],\n \"order\": 0\n }\n ],\n \"languages\": [\n {\n \"id\": \"lang-en\",\n \"language\": \"en\",\n \"displayName\": {\n \"en\": \"English\",\n \"ja\": \"英語\"\n },\n \"proficiency\": \"native\",\n \"order\": 0\n },\n {\n \"id\": \"lang-pt\",\n \"language\": \"pt\",\n \"displayName\": {\n \"en\": \"Portuguese\",\n \"ja\": \"ポルトガル語\"\n },\n \"proficiency\": \"intermediate\",\n \"order\": 1\n }\n ],\n \"courses\": [\n {\n \"id\": \"coursera-advanced-a11y-2023\",\n \"title\": {\n \"en\": \"Advanced Web Accessibility Patterns\",\n \"ja\": \"Web アクセシビリティ応用パターン\"\n },\n \"provider\": {\n \"en\": \"Coursera (University of Michigan)\",\n \"ja\": \"Coursera (ミシガン大学)\"\n },\n \"courseNumber\": \"UMICH-A11Y-301\",\n \"description\": {\n \"en\": \"Covered ARIA authoring practices, accessible component patterns, and assistive technology testing workflows.\",\n \"ja\": \"ARIA オーサリングプラクティス、アクセシブルコンポーネントパターン、支援技術テストワークフローを扱う。\"\n },\n \"completionDate\": \"2023-11\",\n \"certificateUrl\": \"https://example.org/certificates/coursera/UMICH-A11Y-301\",\n \"order\": 0\n }\n ],\n \"patents\": [\n {\n \"id\": \"us-2024-a11y-test\",\n \"title\": {\n \"en\": \"Method and System for Automated Screen-Reader Output Verification\",\n \"ja\": \"スクリーンリーダー出力の自動検証手法およびシステム\"\n },\n \"patentNumber\": \"US 99,999,999 B2\",\n \"office\": \"USPTO\",\n \"status\": \"issued\",\n \"description\": {\n \"en\": \"An automated harness that captures screen-reader output and compares it against an authoring-time accessibility annotation.\",\n \"ja\": \"スクリーンリーダーの出力を自動的にキャプチャし、オーサリング時のアクセシビリティ注釈と照合する自動化ハーネス。\"\n },\n \"filingDate\": \"2022-04\",\n \"grantDate\": \"2024-03\",\n \"url\": \"https://patents.example.org/us-11987654\",\n \"coInventors\": [\"Jamie Chen\"],\n \"order\": 0\n },\n {\n \"id\": \"us-pending-focus\",\n \"title\": {\n \"en\": \"Predictive Focus Management for Single-Page Applications\",\n \"ja\": \"シングルページアプリケーション向け予測的フォーカス管理\"\n },\n \"patentNumber\": \"US 99/999,999\",\n \"office\": \"USPTO\",\n \"status\": \"pending\",\n \"description\": {\n \"en\": \"An algorithm that predicts the next likely focus target based on user navigation patterns to reduce focus loss in dynamic content.\",\n \"ja\": \"ユーザーのナビゲーションパターンから次のフォーカス候補を予測し、動的コンテンツにおけるフォーカス喪失を低減するアルゴリズム。\"\n },\n \"filingDate\": \"2024-08\",\n \"url\": \"https://patents.example.org/us-pending-focus\",\n \"order\": 1\n }\n ],\n \"testScores\": [\n {\n \"id\": \"celpe-bras-2023\",\n \"title\": {\n \"en\": \"CELPE-Bras (Brazilian Portuguese Proficiency)\",\n \"ja\": \"CELPE-Bras (ブラジルポルトガル語能力試験)\"\n },\n \"score\": \"Upper Intermediate\",\n \"date\": \"2023-05\",\n \"description\": {\n \"en\": \"Brazilian government certificate of proficiency in Portuguese as a foreign language.\",\n \"ja\": \"ブラジル政府認定の外国語としてのポルトガル語能力証明書。\"\n },\n \"url\": \"https://example.org/scores/celpe-bras-2023\",\n \"order\": 0\n },\n {\n \"id\": \"gre-general-2013\",\n \"title\": {\n \"en\": \"GRE General Test\",\n \"ja\": \"GRE 一般試験\"\n },\n \"score\": \"332 / 340\",\n \"date\": \"2013-10\",\n \"relatedEducationId\": \"westbrook-bsc-2018\",\n \"description\": {\n \"en\": \"Combined verbal and quantitative reasoning score submitted with the Westbrook University application.\",\n \"ja\": \"ウェストブルック大学出願時に提出した言語・数的推論の合計スコア。\"\n },\n \"url\": \"https://example.org/scores/gre-general-2013\",\n \"order\": 1\n }\n ],\n \"recommendations\": [\n {\n \"id\": \"rec-jordan-stellar\",\n \"body\": {\n \"en\": \"Pat is one of the most rigorous accessibility engineers I have worked with. They turned our design-token audit into a repeatable process and mentored the whole team on assistive-technology testing.\",\n \"ja\": \"Pat は私が共に働いた中で最も厳格なアクセシビリティエンジニアの一人です。デザイントークン監査を再現可能なプロセスに変え、支援技術テストについてチーム全体を指導してくれました。\"\n },\n \"author\": {\n \"name\": \"Jordan Avery\",\n \"headline\": {\n \"en\": \"Engineering Manager at Stellar UX\",\n \"ja\": \"Stellar UX エンジニアリングマネージャー\"\n },\n \"url\": \"https://example.org/in/jordan-avery\"\n },\n \"relationship\": {\n \"en\": \"Managed Pat directly at Stellar UX\",\n \"ja\": \"Stellar UX で Pat を直属で管理\"\n },\n \"date\": \"2023-09\",\n \"relatedCareerId\": \"stellar-ux\",\n \"order\": 0\n },\n {\n \"id\": \"rec-sofia-harbor\",\n \"body\": {\n \"en\": \"I collaborated with Pat on a screen-reader testing harness at Harbor Labs. Their attention to non-visual user journeys raised the bar for the entire engineering org.\",\n \"ja\": \"Harbor Labs でスクリーンリーダーのテストハーネスを Pat と共同開発しました。非視覚ユーザーの導線への配慮は、エンジニアリング組織全体の基準を引き上げました。\"\n },\n \"author\": {\n \"name\": \"Sofia Almeida\",\n \"headline\": {\n \"en\": \"Staff Engineer at Harbor Labs\",\n \"ja\": \"Harbor Labs スタッフエンジニア\"\n }\n },\n \"date\": \"2021-06\",\n \"relatedCareerId\": \"harbor-labs\",\n \"order\": 1\n }\n ],\n \"contact\": {\n \"email\": \"pat@example.com\",\n \"showEmail\": false,\n \"formUrl\": \"https://example.com/pat/contact\"\n },\n \"settings\": {\n \"defaultLocale\": \"en\",\n \"fallbackLocale\": \"en\",\n \"availableLocales\": [\"en\", \"ja\"],\n \"theme\": \"default\",\n \"showPoweredBy\": true,\n \"enableJsonLd\": true,\n \"enableApi\": true,\n \"enableAnalytics\": false,\n \"skillCategories\": [\n { \"id\": \"programming\", \"label\": { \"en\": \"Programming\", \"ja\": \"プログラミング\" } },\n {\n \"id\": \"design\",\n \"label\": { \"en\": \"Design & accessibility\", \"ja\": \"デザイン・アクセシビリティ\" }\n },\n { \"id\": \"language\", \"label\": { \"en\": \"Languages\", \"ja\": \"言語\" } }\n ]\n },\n \"meta\": {\n \"createdAt\": \"2026-01-15T09:00:00Z\",\n \"updatedAt\": \"2026-05-12T08:30:00Z\",\n \"generator\": \"Takuhon\",\n \"contentLicense\": {\n \"spdxId\": \"CC-BY-4.0\",\n \"url\": \"https://creativecommons.org/licenses/by/4.0/\",\n \"attribution\": {\n \"name\": \"Pat Rivera\",\n \"url\": \"https://example.com/pat\"\n }\n },\n \"privacy\": {\n \"hideCredentialIds\": true,\n \"hideEducationGrades\": true\n }\n }\n}\n","/**\n * Read-only activity-badge endpoint (`GET`/`HEAD /activity.svg`) for the\n * Cloudflare adapter.\n *\n * Renders the synced developer-activity snapshot as a self-contained SVG card,\n * suitable for embedding as an image in a GitHub profile README (served through\n * the Camo image proxy) or anywhere an `<img>` can point at a public URL. The\n * markup is the same `renderActivitySvg` output the profile page embeds, so the\n * badge never drifts from the in-page dashboard.\n *\n * Public and unauthenticated, at parity with `GET /api/activity`: the owner's\n * opt-in (`settings.activity.enabled`) is re-checked on every request, so\n * disabling the feature 404s the badge immediately even while a stale snapshot\n * is still stored. `?theme=dark` selects the dark palette; anything else\n * (including no query) renders the default light card.\n *\n * The card carries its own opaque background, so it stays legible on a dark\n * README. The response is `nosniff` `image/svg+xml` with a ~4h public cache\n * policy — the badge is fronted by GitHub's Camo cache, so the origin is hit\n * rarely.\n */\n\nimport {\n DARK_PALETTE,\n LIGHT_PALETTE,\n NotFoundError,\n renderActivitySvg,\n type Takuhon,\n} from '@takuhon/core';\n\nimport { KvActivityStorage } from './kv-activity-storage.js';\nimport { KvTakuhonStorage } from './kv-storage.js';\n\n/**\n * ~4 hours, matching github-readme-stats' default. README badges are cached at\n * Camo, so a longer TTL keeps origin traffic minimal at the cost of a bounded\n * delay before a fresh sync shows up.\n */\nexport const ACTIVITY_SVG_CACHE_CONTROL = 'public, max-age=14400';\n\n/**\n * Render `GET`/`HEAD /activity.svg` against the KV-backed profile and snapshot.\n * Builds its own storage wrappers from `kv` (no shared state) and falls back to\n * the bundled profile when KV is empty, exactly as the public API does.\n */\nexport async function serveActivitySvg(\n request: Request,\n kv: KVNamespace,\n fallback: () => Takuhon,\n): Promise<Response> {\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n return new Response('Method Not Allowed', { status: 405 });\n }\n\n const notFound = (): Response => new Response('Not Found', { status: 404 });\n\n // Re-check the owner's opt-in on every request (parity with GET /api/activity):\n // disabling settings.activity must 404 the badge immediately, even while a\n // stale snapshot is still stored.\n let profile: Takuhon;\n try {\n profile = (await new KvTakuhonStorage(kv).getProfile()).data;\n } catch (e) {\n // KV is empty before the first admin write; serve the bundled profile.\n if (e instanceof NotFoundError) profile = fallback();\n else throw e;\n }\n if (profile.settings.activity?.enabled !== true) return notFound();\n\n const snapshot = await new KvActivityStorage(kv).getActivitySnapshot();\n if (snapshot === null) return notFound();\n\n const palette =\n new URL(request.url).searchParams.get('theme') === 'dark' ? DARK_PALETTE : LIGHT_PALETTE;\n const svg = renderActivitySvg(snapshot, { palette });\n // An opted-in but metric-less snapshot renders to ''. There is no image to\n // serve, so treat it like a missing snapshot rather than emit an empty badge.\n if (svg === '') return notFound();\n\n return new Response(request.method === 'HEAD' ? null : svg, {\n status: 200,\n headers: {\n 'content-type': 'image/svg+xml; charset=utf-8',\n 'cache-control': ACTIVITY_SVG_CACHE_CONTROL,\n // Force nosniff so the SVG can't be reinterpreted as another type\n // (security.md §4.7), matching the /assets/* delivery proxy.\n 'x-content-type-options': 'nosniff',\n },\n });\n}\n","import { isActivitySnapshot, type ActivitySnapshot, type ActivityStorage } from '@takuhon/core';\n\n/**\n * KV key holding the synced developer-activity snapshot. A second key in the\n * same namespace as the profile (`TAKUHON_DATA`): the snapshot is a sibling\n * document, machine-written by the scheduled sync and deliberately kept out of\n * the canonical profile document.\n */\nexport const ACTIVITY_KV_KEY = 'TAKUHON_ACTIVITY';\n\n/**\n * Cloudflare KV implementation of the `ActivityStorage` contract.\n *\n * Reads are forgiving: an absent, unparseable, or malformed value resolves to\n * `null` (never throws), because a missing snapshot is the normal pre-sync /\n * opt-out state and the renderer simply omits the activity section. Writes are\n * last-writer-wins with no version metadata — the snapshot carries its own\n * `lastSyncedAt`, and the only writer is the scheduled sync.\n */\nexport class KvActivityStorage implements ActivityStorage {\n constructor(private readonly kv: KVNamespace) {}\n\n async getActivitySnapshot(): Promise<ActivitySnapshot | null> {\n const raw = await this.kv.get(ACTIVITY_KV_KEY, 'text');\n if (raw === null) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // A corrupt snapshot is treated as absent, not fatal: the next sync\n // rewrites it whole, and the renderer omits the section meanwhile.\n return null;\n }\n return isActivitySnapshot(parsed) ? parsed : null;\n }\n\n async saveActivitySnapshot(snapshot: ActivitySnapshot): Promise<void> {\n await this.kv.put(ACTIVITY_KV_KEY, JSON.stringify(snapshot));\n }\n}\n","import { ConflictError, NotFoundError, type Takuhon, type TakuhonStorage } from '@takuhon/core';\n\nexport const KV_KEY = 'TAKUHON_DATA';\n\nexport interface KvMetadata {\n version: string;\n updatedAt: string;\n}\n\n/**\n * Cloudflare KV implementation of the `TakuhonStorage` contract. Stores the\n * profile document as JSON under a single key (`TAKUHON_DATA`) and tracks the\n * optimistic-locking token inside KV value metadata.\n *\n * `version` is a fresh UUIDv4 on every successful write. Callers compare it\n * verbatim against the `If-Match` precondition; mismatches raise\n * `ConflictError` with `currentVersion` so the API layer can build the RFC\n * 7807 envelope without an extra round trip.\n */\nexport class KvTakuhonStorage implements TakuhonStorage {\n constructor(private readonly kv: KVNamespace) {}\n\n async getProfile(): Promise<{ data: Takuhon; version: string }> {\n const result = await this.kv.getWithMetadata<Takuhon, KvMetadata>(KV_KEY, 'json');\n if (result.value === null || !result.metadata?.version) {\n throw new NotFoundError(`No profile is stored at KV key \"${KV_KEY}\".`);\n }\n return { data: result.value, version: result.metadata.version };\n }\n\n async saveProfile(data: Takuhon, ifMatch?: string): Promise<{ version: string }> {\n if (ifMatch !== undefined) {\n const current = await this.kv.getWithMetadata<Takuhon, KvMetadata>(KV_KEY, 'json');\n const currentVersion = current.metadata?.version;\n if (currentVersion !== ifMatch) {\n throw new ConflictError(\n `If-Match preconditioned on version \"${ifMatch}\" but current is \"${currentVersion ?? 'absent'}\".`,\n { currentVersion },\n );\n }\n }\n const version = crypto.randomUUID();\n const updatedAt = new Date().toISOString();\n await this.kv.put(KV_KEY, JSON.stringify(data), {\n metadata: { version, updatedAt } satisfies KvMetadata,\n });\n return { version };\n }\n\n async deleteProfile(): Promise<void> {\n await this.kv.delete(KV_KEY);\n }\n}\n","/**\n * Scheduled developer-activity sync for the Cloudflare worker.\n *\n * The cron-driven counterpart of the CLI's `takuhon activity sync`: it reads\n * `settings.activity` from the stored profile, fetches the configured GitHub /\n * WakaTime metrics through `@takuhon/activity`, and persists the snapshot via\n * an `ActivityStorage` — so public rendering stays a static read.\n *\n * The function reports its outcome instead of throwing wherever it can, and a\n * sync that gathers no data never overwrites a good snapshot: the last-known\n * one is kept and the result says so, letting the `scheduled` handler log the\n * failure without failing the cron run.\n */\n\nimport { fetchActivitySnapshot, isEmptySnapshot, type ActivitySecrets } from '@takuhon/activity';\nimport {\n NotFoundError,\n type ActivityStorage,\n type Takuhon,\n type TakuhonStorage,\n} from '@takuhon/core';\n\nexport interface ActivitySyncOptions {\n /** Source of the profile whose `settings.activity` configures the sync. */\n profileStorage: TakuhonStorage;\n /** Destination for the synced snapshot. */\n activityStorage: ActivityStorage;\n /** Secrets from the Worker env; never persisted. */\n secrets: ActivitySecrets;\n /** Fallback profile used when storage has none (same as the public app). */\n fallback?: () => Takuhon;\n /** HTTP client override for tests. Defaults to the global `fetch`. */\n fetch?: typeof fetch;\n /** Clock for `lastSyncedAt`; injectable for deterministic tests. */\n now?: () => Date;\n}\n\n/** A per-source fetch failure, with any secret value already redacted. */\nexport interface ActivitySourceFailure {\n source: string;\n message: string;\n}\n\nexport interface ActivitySyncResult {\n /**\n * `synced` — a snapshot was gathered and stored. `skipped` — activity is not\n * configured / enabled, so nothing was attempted. `empty` — every configured\n * source came back empty or failing; the last-known snapshot was kept.\n */\n status: 'synced' | 'skipped' | 'empty';\n /** Why a `skipped` / `empty` run did not store anything. */\n reason?: string;\n /** Per-source failures reported by the fetcher (may be non-empty on `synced`). */\n failures: ActivitySourceFailure[];\n}\n\n/** Remove any literal occurrence of the secrets from a string before it is logged. */\nfunction redactSecrets(text: string, secrets: ActivitySecrets): string {\n let out = text;\n for (const secret of [secrets.githubToken, secrets.wakatimeKey]) {\n if (secret !== undefined && secret !== '') out = out.split(secret).join('***');\n }\n return out;\n}\n\n/**\n * Run one activity sync. Resolves with a result for every configuration state;\n * it only rejects if the storage layer itself fails (the `scheduled` handler\n * catches that, so a broken sync never fails the cron run).\n */\nexport async function syncActivity(opts: ActivitySyncOptions): Promise<ActivitySyncResult> {\n let profile: Takuhon;\n try {\n profile = (await opts.profileStorage.getProfile()).data;\n } catch (err) {\n if (err instanceof NotFoundError && opts.fallback) {\n profile = opts.fallback();\n } else if (err instanceof NotFoundError) {\n return { status: 'skipped', reason: 'no stored profile', failures: [] };\n } else {\n throw err;\n }\n }\n\n const config = profile.settings.activity;\n if (config?.enabled !== true) {\n return {\n status: 'skipped',\n reason: 'activity is not enabled in settings.activity',\n failures: [],\n };\n }\n const hasGithub = config.github?.username !== undefined && config.github.username !== '';\n const hasWakatime = config.wakatime?.username !== undefined && config.wakatime.username !== '';\n if (!hasGithub && !hasWakatime) {\n return { status: 'skipped', reason: 'no github or wakatime username configured', failures: [] };\n }\n\n const failures: ActivitySourceFailure[] = [];\n const snapshot = await fetchActivitySnapshot(config, opts.secrets, {\n fetch: opts.fetch,\n now: opts.now,\n onError: (source, err) => {\n const detail = err instanceof Error ? err.message : String(err);\n failures.push({ source, message: redactSecrets(detail, opts.secrets) });\n },\n });\n\n if (isEmptySnapshot(snapshot)) {\n // Never replace a good snapshot with an empty one (design §6): keep the\n // last-known document and report the dry run.\n return {\n status: 'empty',\n reason: 'the sync gathered no activity data; kept the last-known snapshot',\n failures,\n };\n }\n\n await opts.activityStorage.saveActivitySnapshot(snapshot);\n return { status: 'synced', failures };\n}\n","import type { CachePurger } from '@takuhon/api';\n\nexport interface CloudflareCachePurgerOptions {\n /**\n * Absolute origin (e.g. `https://example.com`) used to build the URLs\n * passed to `Cache.delete`. The Worker derives this from the incoming\n * request's URL so the same code works under any production hostname.\n */\n origin: string;\n /**\n * Locale codes to include when purging language-keyed cache entries.\n * Cloudflare caches `?lang=` query variants as distinct keys; we purge\n * a representative set on every write. Adapters can extend the list to\n * cover other locales the deploy serves.\n */\n langs?: string[];\n}\n\n/**\n * `CachePurger` backed by Cloudflare's colo-local `caches.default`.\n *\n * Limitations (documented in the adapter README):\n * - Cloudflare's `Cache.delete` clears the current colo only, not the\n * entire edge. Other colos honour the response's `Cache-Control`\n * `s-maxage` (5 minutes today) before refreshing.\n * - Truly global invalidation requires the REST `/purge_cache` API,\n * which needs a zone-scoped token; that's deferred to a later phase.\n */\nexport class CloudflareCachePurger implements CachePurger {\n private readonly origin: string;\n private readonly langs: string[];\n\n /**\n * `getCache` is a thunk so the Workers-only `caches` global is touched\n * lazily — public-only requests on this Worker never run admin handlers\n * and must not pay (or fail under Node tests) for the lookup.\n */\n constructor(\n private readonly getCache: () => Cache,\n opts: CloudflareCachePurgerOptions,\n ) {\n this.origin = opts.origin.replace(/\\/$/, '');\n this.langs = opts.langs ?? ['en', 'ja'];\n }\n\n async profileUpdated(): Promise<void> {\n await this.purge();\n }\n\n async profileDeleted(): Promise<void> {\n await this.purge();\n }\n\n private async purge(): Promise<void> {\n const cache = this.getCache();\n const targets = ['/', '/api/profile', '/api/jsonld', '/takuhon.json'];\n for (const lang of this.langs) {\n const q = `?lang=${encodeURIComponent(lang)}`;\n targets.push(`/api/profile${q}`, `/api/jsonld${q}`);\n }\n for (const path of targets) {\n await cache.delete(new Request(this.origin + path), { ignoreMethod: true });\n }\n }\n}\n","import type { AuditEvent, AuditLogger } from '@takuhon/api';\n\n/**\n * `AuditLogger` that writes a single line of JSON per event to `console.log`.\n *\n * Cloudflare captures these via Workers Tail / Logpush, where they can be\n * routed to R2, S3, or any downstream SIEM. Token bodies never reach this\n * sink — the upstream middleware only emits `sha256:<hex>` digests in\n * `actor.tokenHash`.\n */\nexport const consoleAuditLogger: AuditLogger = (event: AuditEvent): void => {\n console.log(JSON.stringify(event));\n};\n","/**\n * Cloudflare implementations of the `@takuhon/contact` adapter seams.\n *\n * Two pieces — a Turnstile {@link ChallengeVerifier} and a `send_email`-binding\n * {@link EmailTransport} — are the only Cloudflare-specific parts of the contact\n * feature. Everything else (validation, the request pipeline) lives in the\n * portable `@takuhon/contact` core, so the feature can be re-homed on another\n * host by swapping just these.\n */\n\nimport type { ChallengeVerifier, EmailTransport, Inquiry } from '@takuhon/contact';\n\nconst TURNSTILE_SITEVERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';\n\n/** Options for {@link createTurnstileVerifier}. */\nexport interface TurnstileVerifierOptions {\n /** Override the siteverify endpoint (for tests). */\n endpoint?: string;\n /** Injected `fetch` (for tests). Defaults to the global `fetch`. */\n fetch?: typeof fetch;\n}\n\n/**\n * Create a {@link ChallengeVerifier} backed by Cloudflare Turnstile's\n * server-side `siteverify` endpoint. The secret is used server-side only and is\n * never exposed to the client.\n */\nexport function createTurnstileVerifier(\n secret: string,\n options?: TurnstileVerifierOptions,\n): ChallengeVerifier {\n const endpoint = options?.endpoint ?? TURNSTILE_SITEVERIFY_URL;\n // Always invoke fetch as a free function (never as a method) so the runtime\n // never sees a rebound `this` — cf. the @takuhon/activity illegal-invocation fix.\n const doFetch: typeof fetch = options?.fetch ?? ((input, init) => fetch(input, init));\n\n return {\n async verify(token, context) {\n if (!token) return false;\n const form = new FormData();\n form.append('secret', secret);\n form.append('response', token);\n if (context?.remoteIp) form.append('remoteip', context.remoteIp);\n\n let response: Response;\n try {\n response = await doFetch(endpoint, { method: 'POST', body: form });\n } catch {\n return false;\n }\n if (!response.ok) return false;\n\n const outcome = (await response.json().catch(() => null)) as { success?: boolean } | null;\n return outcome?.success === true;\n },\n };\n}\n\n/**\n * Minimal structural shape of the Cloudflare `send_email` binding we depend on\n * (the object-form `send`). Declared here rather than imported so the adapter\n * does not pin a specific `@cloudflare/workers-types` shape; the real binding\n * satisfies it structurally.\n */\nexport interface SendEmailBinding {\n send(message: {\n to: string | string[];\n from: { email: string; name?: string };\n replyTo?: string;\n subject: string;\n text?: string;\n html?: string;\n }): Promise<unknown>;\n}\n\n/** Configuration for {@link createSendEmailTransport}. */\nexport interface SendEmailTransportConfig {\n /** Verified destination address — the site owner's inbox. */\n to: string;\n /** From address; must be on a domain you control (e.g. `noreply@tak3.jp`). */\n from: { email: string; name?: string };\n /** Optional subject prefix, e.g. `\"[me.tak3.jp contact]\"`. */\n subjectPrefix?: string;\n}\n\n/** A built notification email. */\nexport interface InquiryEmail {\n subject: string;\n text: string;\n html: string;\n}\n\nconst HTML_ESCAPES: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n};\n\nfunction escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => HTML_ESCAPES[char] ?? char);\n}\n\n/** Strip CR/LF so a value is safe to use in an email header (the subject). */\nfunction sanitizeHeader(value: string): string {\n return value.replace(/[\\r\\n]+/g, ' ').trim();\n}\n\n/**\n * Build the owner-facing notification email for an inquiry: a plain-text body\n * plus an HTML body with every untrusted field escaped. The visitor's message\n * is rendered as text and never interpolated into HTML unescaped.\n */\nexport function buildInquiryEmail(\n inquiry: Inquiry,\n config: Pick<SendEmailTransportConfig, 'subjectPrefix'> = {},\n): InquiryEmail {\n const prefix = config.subjectPrefix?.trim();\n const subject = sanitizeHeader(\n prefix ? `${prefix} ${inquiry.email}` : `Contact from ${inquiry.email}`,\n );\n\n const fields: [string, string | undefined][] = [\n ['From', inquiry.email],\n ['Locale', inquiry.locale],\n ['Page', inquiry.pageUrl],\n ['User-Agent', inquiry.userAgent],\n ['Country', inquiry.meta?.country],\n ['IP', inquiry.meta?.ip],\n ['Received', inquiry.meta?.receivedAt],\n ];\n const present = fields.filter((entry): entry is [string, string] => entry[1] !== undefined);\n\n const text = [\n inquiry.message,\n '',\n '—',\n ...present.map(([key, value]) => `${key}: ${value}`),\n ].join('\\n');\n\n const html = [\n `<p style=\"white-space:pre-wrap;margin:0 0 1em\">${escapeHtml(inquiry.message)}</p>`,\n '<hr>',\n '<p style=\"color:#555;font-size:13px;line-height:1.6\">',\n present.map(([key, value]) => `${escapeHtml(key)}: ${escapeHtml(value)}`).join('<br>'),\n '</p>',\n ].join('');\n\n return { subject, text, html };\n}\n\n/**\n * Create an {@link EmailTransport} that delivers an inquiry to the owner's inbox\n * via the Cloudflare `send_email` binding. `Reply-To` is set to the visitor's\n * address so the owner can reply straight from their normal mail app.\n */\nexport function createSendEmailTransport(\n binding: SendEmailBinding,\n config: SendEmailTransportConfig,\n): EmailTransport {\n return {\n async send(inquiry) {\n const mail = buildInquiryEmail(inquiry, config);\n await binding.send({\n to: config.to,\n from: config.from,\n replyTo: inquiry.email,\n subject: mail.subject,\n text: mail.text,\n html: mail.html,\n });\n },\n };\n}\n","/**\n * Read-only MCP endpoint (`GET`/`POST /mcp`) for the Cloudflare adapter.\n *\n * The deployed profile is exposed over the Model Context Protocol so any MCP\n * client can read it — the remote counterpart of the CLI's `takuhon mcp`. It\n * reuses the transport-agnostic server from `@takuhon/mcp` (the same core\n * catalog the CLI serves) and connects it to the SDK's **Web Standard**\n * Streamable HTTP transport, which speaks `fetch` `Request`/`Response` and runs\n * on Workers.\n *\n * Stateless by design — no Durable Object, no new binding, no session: each\n * request builds a fresh server + transport (the SDK requires a fresh transport\n * per request in stateless mode, and skips session validation), reads the\n * profile from the existing KV, and answers. `enableJsonResponse` returns a\n * single JSON body instead of an SSE stream, which suits read-only\n * request/response with no server-initiated messages.\n *\n * Unauthenticated public read, at parity with `GET /api/profile`: every answer\n * is privacy-filtered by core and no admin/write surface is exposed.\n */\n\nimport { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';\nimport { NotFoundError, type Takuhon } from '@takuhon/core';\nimport { createTakuhonMcpServer } from '@takuhon/mcp';\n\nimport { KvTakuhonStorage } from './kv-storage.js';\n\nconst MCP_SERVER_NAME = 'takuhon';\n\n/**\n * Handle one MCP request against the KV-backed profile. Builds a fresh,\n * stateless server + transport per request and returns its `Response` with\n * `nosniff` and a no-store cache policy layered on.\n */\nexport async function serveMcp(\n request: Request,\n kv: KVNamespace,\n fallback: () => Takuhon,\n): Promise<Response> {\n const storage = new KvTakuhonStorage(kv);\n const loadProfile = async (): Promise<Takuhon> => {\n try {\n return (await storage.getProfile()).data;\n } catch (e) {\n // Before the first admin write KV is empty; serve the bundled fixture,\n // exactly as the public API's loadProfile does.\n if (e instanceof NotFoundError) return fallback();\n throw e;\n }\n };\n\n const server = createTakuhonMcpServer({ loadProfile, name: MCP_SERVER_NAME });\n // Stateless: `sessionIdGenerator: undefined` disables sessions, and the SDK\n // requires a fresh transport per request in this mode (it rejects reuse to\n // avoid cross-client message-id collisions). `enableJsonResponse` returns a\n // buffered JSON body rather than an SSE stream.\n const transport = new WebStandardStreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n await server.connect(transport);\n\n // The server / transport are per-request locals with no cross-request state,\n // so the Worker can let them be collected when the request ends.\n const response = await transport.handleRequest(request);\n\n const headers = new Headers(response.headers);\n headers.set('x-content-type-options', 'nosniff');\n if (!headers.has('cache-control')) headers.set('cache-control', 'no-store');\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n","import {\n ACCEPTED_IMAGE_MIME_TYPES,\n detectImageMime,\n IMAGE_EXTENSIONS,\n NotFoundError,\n readImageInfo,\n type AcceptedImageMime,\n type AssetOptions,\n type AssetRecord,\n type TakuhonAssetStorage,\n} from '@takuhon/core';\n\n/** Key prefix every uploaded asset lives under (`security.md` §4.6). */\nconst ASSET_KEY_PREFIX = 'assets/';\n\n/** Extension used when the bytes are not one of the accepted image types. */\nconst DEFAULT_EXTENSION = 'bin';\n\n/**\n * Long-lived immutable cache policy for delivered assets. Object keys embed a\n * timestamp and a random hash, so a given key never names different bytes —\n * the response can be cached forever.\n */\nexport const ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable';\n\n/** Narrow an arbitrary content-type string to an accepted image MIME, or null. */\nfunction asAcceptedMime(value: string | undefined): AcceptedImageMime | null {\n if (value === undefined) return null;\n return (ACCEPTED_IMAGE_MIME_TYPES as readonly string[]).includes(value)\n ? (value as AcceptedImageMime)\n : null;\n}\n\n/** Seconds since the Unix epoch, used as the leading object-key component. */\nfunction timestampSeconds(): number {\n return Math.floor(Date.now() / 1000);\n}\n\n/**\n * Four hex characters of cryptographic randomness. Combined with the timestamp\n * this defeats key enumeration (`security.md` §4.7) without an idempotency\n * guarantee — every upload gets a fresh key.\n */\nfunction shortHash(): string {\n const bytes = new Uint8Array(2);\n crypto.getRandomValues(bytes);\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n}\n\n/**\n * Cloudflare R2 implementation of the {@link TakuhonAssetStorage} contract.\n *\n * The bytes arrive already validated and metadata-stripped by `@takuhon/api`'s\n * admin app (magic-byte check, size / dimension / frame limits, EXIF removal),\n * so this adapter only persists them and mints the public URL. Object keys\n * follow `assets/{timestamp}-{shortHash}.{ext}` (`security.md` §4.6); the file\n * extension comes from `@takuhon/core`'s {@link IMAGE_EXTENSIONS}.\n *\n * Dimensions are read back from the container header via\n * {@link readImageInfo} (no pixel decode) so the returned {@link AssetRecord}\n * is complete; this is the same header-only parse the API layer already\n * performed and matches the deliberate \"no codec\" approach.\n *\n * Assets are served by the Worker's `GET /assets/*` proxy route rather than a\n * public R2 bucket, so `publicUrl` is an absolute URL on the Worker's own\n * origin when `publicBaseUrl` is supplied, falling back to the relative `url`\n * otherwise.\n */\nexport class R2TakuhonAssetStorage implements TakuhonAssetStorage {\n private readonly bucket: R2Bucket;\n private readonly publicBaseUrl: string;\n\n constructor(bucket: R2Bucket, options: { publicBaseUrl?: string } = {}) {\n this.bucket = bucket;\n this.publicBaseUrl = options.publicBaseUrl ?? '';\n }\n\n async putAsset(file: File | Blob, options?: AssetOptions): Promise<AssetRecord> {\n const bytes = new Uint8Array(await file.arrayBuffer());\n // Trust the API-validated content-type when present; otherwise authenticate\n // from the bytes so the adapter is self-sufficient for direct callers.\n const mime = asAcceptedMime(options?.contentType) ?? detectImageMime(bytes);\n const ext = mime !== null ? IMAGE_EXTENSIONS[mime] : DEFAULT_EXTENSION;\n const contentType = mime ?? options?.contentType ?? file.type ?? 'application/octet-stream';\n\n const key = `${ASSET_KEY_PREFIX}${String(timestampSeconds())}-${shortHash()}.${ext}`;\n await this.bucket.put(key, bytes, { httpMetadata: { contentType } });\n\n const info = mime !== null ? readImageInfo(bytes, mime) : null;\n const url = `/${key}`;\n return {\n id: key,\n url,\n publicUrl: this.absoluteUrl(url),\n mimeType: contentType,\n size: bytes.length,\n width: info?.width,\n height: info?.height,\n createdAt: new Date().toISOString(),\n };\n }\n\n async getPublicUrl(assetId: string): Promise<string> {\n const head = await this.bucket.head(assetId);\n if (head === null) throw new NotFoundError(`No asset is stored at R2 key \"${assetId}\".`);\n return this.absoluteUrl(`/${assetId}`);\n }\n\n async deleteAsset(assetId: string): Promise<void> {\n // R2 delete is idempotent: deleting an absent key is a no-op, matching the\n // contract.\n await this.bucket.delete(assetId);\n }\n\n async listAssets(): Promise<AssetRecord[]> {\n const records: AssetRecord[] = [];\n let cursor: string | undefined;\n do {\n const page = await this.bucket.list({ prefix: ASSET_KEY_PREFIX, cursor });\n for (const object of page.objects) {\n const url = `/${object.key}`;\n records.push({\n id: object.key,\n url,\n publicUrl: this.absoluteUrl(url),\n mimeType: object.httpMetadata?.contentType ?? 'application/octet-stream',\n size: object.size,\n createdAt: object.uploaded.toISOString(),\n });\n }\n cursor = page.truncated ? page.cursor : undefined;\n } while (cursor !== undefined);\n return records;\n }\n\n private absoluteUrl(path: string): string {\n return this.publicBaseUrl !== '' ? `${this.publicBaseUrl}${path}` : path;\n }\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB,uBAAuB;AAClD,SAAS,gBAA8B;AACvC,SAAS,YAAY;;;ACdrB;AAAA,EACE,eAAiB;AAAA,EACjB,SAAW;AAAA,IACT,aAAe;AAAA,MACb,IAAM;AAAA,MACN,IAAM;AAAA,IACR;AAAA,IACA,SAAW;AAAA,MACT,IAAM;AAAA,MACN,IAAM;AAAA,IACR;AAAA,IACA,KAAO;AAAA,MACL,IAAM;AAAA,MACN,IAAM;AAAA,IACR;AAAA,IACA,QAAU;AAAA,MACR,KAAO;AAAA,MACP,KAAO;AAAA,QACL,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAY;AAAA,MACV,SAAW;AAAA,MACX,QAAU;AAAA,MACV,UAAY;AAAA,QACV,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,SAAW;AAAA,QACT,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA,EACA,OAAS;AAAA,IACP;AAAA,MACE,IAAM;AAAA,MACN,MAAQ;AAAA,MACR,OAAS,EAAE,IAAM,gBAAgB;AAAA,MACjC,KAAO;AAAA,MACP,UAAY;AAAA,MACZ,OAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,IAAM;AAAA,MACN,MAAQ;AAAA,MACR,KAAO;AAAA,MACP,UAAY;AAAA,MACZ,OAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,IAAM;AAAA,MACN,MAAQ;AAAA,MACR,KAAO;AAAA,MACP,OAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,IAAM;AAAA,MACN,MAAQ;AAAA,MACR,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,KAAO;AAAA,MACP,OAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,IAAM;AAAA,MACN,MAAQ;AAAA,MACR,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,KAAO;AAAA,MACP,SAAW;AAAA,MACX,OAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,SAAW;AAAA,IACT;AAAA,MACE,IAAM;AAAA,MACN,cAAgB;AAAA,QACd,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,MAAQ;AAAA,QACN,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,WAAa;AAAA,MACb,SAAW;AAAA,MACX,WAAa;AAAA,MACb,KAAO;AAAA,MACP,OAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,IAAM;AAAA,MACN,cAAgB;AAAA,QACd,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,MAAQ;AAAA,QACN,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,WAAa;AAAA,MACb,SAAW;AAAA,MACX,OAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,UAAY;AAAA,IACV;AAAA,MACE,IAAM;AAAA,MACN,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,KAAO;AAAA,MACP,MAAQ,CAAC,iBAAiB,WAAW,YAAY;AAAA,MACjD,iBAAmB;AAAA,MACnB,WAAa;AAAA,MACb,aAAe;AAAA,MACf,OAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,IAAM;AAAA,MACN,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,KAAO;AAAA,MACP,MAAQ,CAAC,iBAAiB,OAAO,eAAe;AAAA,MAChD,WAAa;AAAA,MACb,SAAW;AAAA,MACX,OAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,IAAM;AAAA,MACN,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,OAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,QAAU;AAAA,IACR,EAAE,IAAM,cAAc,OAAS,cAAc,UAAY,eAAe,OAAS,EAAE;AAAA,IACnF,EAAE,IAAM,SAAS,OAAS,SAAS,UAAY,eAAe,OAAS,EAAE;AAAA,IACzE,EAAE,IAAM,YAAY,OAAS,YAAY,UAAY,UAAU,OAAS,EAAE;AAAA,IAC1E,EAAE,IAAM,QAAQ,OAAS,QAAQ,UAAY,UAAU,OAAS,EAAE;AAAA,IAClE,EAAE,IAAM,aAAa,OAAS,aAAa,UAAY,eAAe,OAAS,EAAE;AAAA,IACjF,EAAE,IAAM,cAAc,OAAS,cAAc,UAAY,eAAe,OAAS,EAAE;AAAA,IACnF,EAAE,IAAM,iBAAiB,OAAS,iBAAiB,UAAY,UAAU,OAAS,EAAE;AAAA,IACpF,EAAE,IAAM,cAAc,OAAS,mBAAmB,UAAY,YAAY,OAAS,EAAE;AAAA,EACvF;AAAA,EACA,gBAAkB;AAAA,IAChB;AAAA,MACE,IAAM;AAAA,MACN,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,qBAAuB;AAAA,QACrB,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,WAAa;AAAA,MACb,gBAAkB;AAAA,MAClB,cAAgB;AAAA,MAChB,KAAO;AAAA,MACP,OAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,IAAM;AAAA,MACN,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,qBAAuB;AAAA,QACrB,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,WAAa;AAAA,MACb,gBAAkB;AAAA,MAClB,cAAgB;AAAA,MAChB,KAAO;AAAA,MACP,OAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,aAAe;AAAA,IACb;AAAA,MACE,IAAM;AAAA,MACN,cAAgB;AAAA,QACd,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,MAAQ;AAAA,QACN,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,WAAa;AAAA,MACb,SAAW;AAAA,MACX,WAAa;AAAA,MACb,KAAO;AAAA,MACP,OAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,cAAgB;AAAA,IACd;AAAA,MACE,IAAM;AAAA,MACN,cAAgB;AAAA,QACd,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,MAAQ;AAAA,QACN,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,WAAa;AAAA,MACb,SAAW;AAAA,MACX,WAAa;AAAA,MACb,KAAO;AAAA,MACP,OAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,QAAU;AAAA,IACR;AAAA,MACE,IAAM;AAAA,MACN,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,QAAU;AAAA,QACR,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,MAAQ;AAAA,MACR,KAAO;AAAA,MACP,OAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,WAAa;AAAA,IACX;AAAA,MACE,IAAM;AAAA,MACN,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,QAAU;AAAA,QACR,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,cAAgB;AAAA,QACd,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,OAAS;AAAA,MACT,WAAa;AAAA,MACb,SAAW;AAAA,MACX,WAAa;AAAA,MACb,KAAO;AAAA,MACP,OAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,cAAgB;AAAA,IACd;AAAA,MACE,IAAM;AAAA,MACN,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,WAAa;AAAA,QACX,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,MAAQ;AAAA,MACR,KAAO;AAAA,MACP,KAAO;AAAA,MACP,WAAa,CAAC,cAAc,eAAe;AAAA,MAC3C,OAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,WAAa;AAAA,IACX;AAAA,MACE,IAAM;AAAA,MACN,UAAY;AAAA,MACZ,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,aAAe;AAAA,MACf,OAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,IAAM;AAAA,MACN,UAAY;AAAA,MACZ,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,aAAe;AAAA,MACf,OAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,SAAW;AAAA,IACT;AAAA,MACE,IAAM;AAAA,MACN,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,UAAY;AAAA,QACV,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,cAAgB;AAAA,MAChB,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,gBAAkB;AAAA,MAClB,gBAAkB;AAAA,MAClB,OAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,SAAW;AAAA,IACT;AAAA,MACE,IAAM;AAAA,MACN,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,cAAgB;AAAA,MAChB,QAAU;AAAA,MACV,QAAU;AAAA,MACV,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,YAAc;AAAA,MACd,WAAa;AAAA,MACb,KAAO;AAAA,MACP,aAAe,CAAC,YAAY;AAAA,MAC5B,OAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,IAAM;AAAA,MACN,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,cAAgB;AAAA,MAChB,QAAU;AAAA,MACV,QAAU;AAAA,MACV,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,YAAc;AAAA,MACd,KAAO;AAAA,MACP,OAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,YAAc;AAAA,IACZ;AAAA,MACE,IAAM;AAAA,MACN,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,OAAS;AAAA,MACT,MAAQ;AAAA,MACR,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,KAAO;AAAA,MACP,OAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,IAAM;AAAA,MACN,OAAS;AAAA,QACP,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,OAAS;AAAA,MACT,MAAQ;AAAA,MACR,oBAAsB;AAAA,MACtB,aAAe;AAAA,QACb,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,KAAO;AAAA,MACP,OAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,iBAAmB;AAAA,IACjB;AAAA,MACE,IAAM;AAAA,MACN,MAAQ;AAAA,QACN,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,QAAU;AAAA,QACR,MAAQ;AAAA,QACR,UAAY;AAAA,UACV,IAAM;AAAA,UACN,IAAM;AAAA,QACR;AAAA,QACA,KAAO;AAAA,MACT;AAAA,MACA,cAAgB;AAAA,QACd,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,MAAQ;AAAA,MACR,iBAAmB;AAAA,MACnB,OAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,IAAM;AAAA,MACN,MAAQ;AAAA,QACN,IAAM;AAAA,QACN,IAAM;AAAA,MACR;AAAA,MACA,QAAU;AAAA,QACR,MAAQ;AAAA,QACR,UAAY;AAAA,UACV,IAAM;AAAA,UACN,IAAM;AAAA,QACR;AAAA,MACF;AAAA,MACA,MAAQ;AAAA,MACR,iBAAmB;AAAA,MACnB,OAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,SAAW;AAAA,IACT,OAAS;AAAA,IACT,WAAa;AAAA,IACb,SAAW;AAAA,EACb;AAAA,EACA,UAAY;AAAA,IACV,eAAiB;AAAA,IACjB,gBAAkB;AAAA,IAClB,kBAAoB,CAAC,MAAM,IAAI;AAAA,IAC/B,OAAS;AAAA,IACT,eAAiB;AAAA,IACjB,cAAgB;AAAA,IAChB,WAAa;AAAA,IACb,iBAAmB;AAAA,IACnB,iBAAmB;AAAA,MACjB,EAAE,IAAM,eAAe,OAAS,EAAE,IAAM,eAAe,IAAM,6CAAU,EAAE;AAAA,MACzE;AAAA,QACE,IAAM;AAAA,QACN,OAAS,EAAE,IAAM,0BAA0B,IAAM,iFAAgB;AAAA,MACnE;AAAA,MACA,EAAE,IAAM,YAAY,OAAS,EAAE,IAAM,aAAa,IAAM,eAAK,EAAE;AAAA,IACjE;AAAA,EACF;AAAA,EACA,MAAQ;AAAA,IACN,WAAa;AAAA,IACb,WAAa;AAAA,IACb,WAAa;AAAA,IACb,gBAAkB;AAAA,MAChB,QAAU;AAAA,MACV,KAAO;AAAA,MACP,aAAe;AAAA,QACb,MAAQ;AAAA,QACR,KAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,SAAW;AAAA,MACT,mBAAqB;AAAA,MACrB,qBAAuB;AAAA,IACzB;AAAA,EACF;AACF;;;AChfA;AAAA,EACE;AAAA,EACA;AAAA,EACA,iBAAAA;AAAA,EACA;AAAA,OAEK;;;AC5BP,SAAS,0BAAuE;AAQzE,IAAM,kBAAkB;AAWxB,IAAM,oBAAN,MAAmD;AAAA,EACxD,YAA6B,IAAiB;AAAjB;AAAA,EAAkB;AAAA,EAAlB;AAAA,EAE7B,MAAM,sBAAwD;AAC5D,UAAM,MAAM,MAAM,KAAK,GAAG,IAAI,iBAAiB,MAAM;AACrD,QAAI,QAAQ,KAAM,QAAO;AACzB,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,MAAM,GAAG;AAAA,IACzB,QAAQ;AAGN,aAAO;AAAA,IACT;AACA,WAAO,mBAAmB,MAAM,IAAI,SAAS;AAAA,EAC/C;AAAA,EAEA,MAAM,qBAAqB,UAA2C;AACpE,UAAM,KAAK,GAAG,IAAI,iBAAiB,KAAK,UAAU,QAAQ,CAAC;AAAA,EAC7D;AACF;;;ACvCA,SAAS,eAAe,qBAAwD;AAEzE,IAAM,SAAS;AAiBf,IAAM,mBAAN,MAAiD;AAAA,EACtD,YAA6B,IAAiB;AAAjB;AAAA,EAAkB;AAAA,EAAlB;AAAA,EAE7B,MAAM,aAA0D;AAC9D,UAAM,SAAS,MAAM,KAAK,GAAG,gBAAqC,QAAQ,MAAM;AAChF,QAAI,OAAO,UAAU,QAAQ,CAAC,OAAO,UAAU,SAAS;AACtD,YAAM,IAAI,cAAc,mCAAmC,MAAM,IAAI;AAAA,IACvE;AACA,WAAO,EAAE,MAAM,OAAO,OAAO,SAAS,OAAO,SAAS,QAAQ;AAAA,EAChE;AAAA,EAEA,MAAM,YAAY,MAAe,SAAgD;AAC/E,QAAI,YAAY,QAAW;AACzB,YAAM,UAAU,MAAM,KAAK,GAAG,gBAAqC,QAAQ,MAAM;AACjF,YAAM,iBAAiB,QAAQ,UAAU;AACzC,UAAI,mBAAmB,SAAS;AAC9B,cAAM,IAAI;AAAA,UACR,uCAAuC,OAAO,qBAAqB,kBAAkB,QAAQ;AAAA,UAC7F,EAAE,eAAe;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AACA,UAAM,UAAU,OAAO,WAAW;AAClC,UAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,UAAM,KAAK,GAAG,IAAI,QAAQ,KAAK,UAAU,IAAI,GAAG;AAAA,MAC9C,UAAU,EAAE,SAAS,UAAU;AAAA,IACjC,CAAC;AACD,WAAO,EAAE,QAAQ;AAAA,EACnB;AAAA,EAEA,MAAM,gBAA+B;AACnC,UAAM,KAAK,GAAG,OAAO,MAAM;AAAA,EAC7B;AACF;;;AFdO,IAAM,6BAA6B;AAO1C,eAAsB,iBACpB,SACA,IACA,UACmB;AACnB,MAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,QAAQ;AACzD,WAAO,IAAI,SAAS,sBAAsB,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3D;AAEA,QAAM,WAAW,MAAgB,IAAI,SAAS,aAAa,EAAE,QAAQ,IAAI,CAAC;AAK1E,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,IAAI,iBAAiB,EAAE,EAAE,WAAW,GAAG;AAAA,EAC1D,SAAS,GAAG;AAEV,QAAI,aAAaC,eAAe,WAAU,SAAS;AAAA,QAC9C,OAAM;AAAA,EACb;AACA,MAAI,QAAQ,SAAS,UAAU,YAAY,KAAM,QAAO,SAAS;AAEjE,QAAM,WAAW,MAAM,IAAI,kBAAkB,EAAE,EAAE,oBAAoB;AACrE,MAAI,aAAa,KAAM,QAAO,SAAS;AAEvC,QAAM,UACJ,IAAI,IAAI,QAAQ,GAAG,EAAE,aAAa,IAAI,OAAO,MAAM,SAAS,eAAe;AAC7E,QAAM,MAAM,kBAAkB,UAAU,EAAE,QAAQ,CAAC;AAGnD,MAAI,QAAQ,GAAI,QAAO,SAAS;AAEhC,SAAO,IAAI,SAAS,QAAQ,WAAW,SAAS,OAAO,KAAK;AAAA,IAC1D,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,iBAAiB;AAAA;AAAA;AAAA,MAGjB,0BAA0B;AAAA,IAC5B;AAAA,EACF,CAAC;AACH;;;AG3EA,SAAS,uBAAuB,uBAA6C;AAC7E;AAAA,EACE,iBAAAC;AAAA,OAIK;AAqCP,SAAS,cAAc,MAAc,SAAkC;AACrE,MAAI,MAAM;AACV,aAAW,UAAU,CAAC,QAAQ,aAAa,QAAQ,WAAW,GAAG;AAC/D,QAAI,WAAW,UAAa,WAAW,GAAI,OAAM,IAAI,MAAM,MAAM,EAAE,KAAK,KAAK;AAAA,EAC/E;AACA,SAAO;AACT;AAOA,eAAsB,aAAa,MAAwD;AACzF,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,KAAK,eAAe,WAAW,GAAG;AAAA,EACrD,SAAS,KAAK;AACZ,QAAI,eAAeA,kBAAiB,KAAK,UAAU;AACjD,gBAAU,KAAK,SAAS;AAAA,IAC1B,WAAW,eAAeA,gBAAe;AACvC,aAAO,EAAE,QAAQ,WAAW,QAAQ,qBAAqB,UAAU,CAAC,EAAE;AAAA,IACxE,OAAO;AACL,YAAM;AAAA,IACR;AAAA,EACF;AAEA,QAAM,SAAS,QAAQ,SAAS;AAChC,MAAI,QAAQ,YAAY,MAAM;AAC5B,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,UAAU,CAAC;AAAA,IACb;AAAA,EACF;AACA,QAAM,YAAY,OAAO,QAAQ,aAAa,UAAa,OAAO,OAAO,aAAa;AACtF,QAAM,cAAc,OAAO,UAAU,aAAa,UAAa,OAAO,SAAS,aAAa;AAC5F,MAAI,CAAC,aAAa,CAAC,aAAa;AAC9B,WAAO,EAAE,QAAQ,WAAW,QAAQ,6CAA6C,UAAU,CAAC,EAAE;AAAA,EAChG;AAEA,QAAM,WAAoC,CAAC;AAC3C,QAAM,WAAW,MAAM,sBAAsB,QAAQ,KAAK,SAAS;AAAA,IACjE,OAAO,KAAK;AAAA,IACZ,KAAK,KAAK;AAAA,IACV,SAAS,CAAC,QAAQ,QAAQ;AACxB,YAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC9D,eAAS,KAAK,EAAE,QAAQ,SAAS,cAAc,QAAQ,KAAK,OAAO,EAAE,CAAC;AAAA,IACxE;AAAA,EACF,CAAC;AAED,MAAI,gBAAgB,QAAQ,GAAG;AAG7B,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,KAAK,gBAAgB,qBAAqB,QAAQ;AACxD,SAAO,EAAE,QAAQ,UAAU,SAAS;AACtC;;;AC5FO,IAAM,wBAAN,MAAmD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASxD,YACmB,UACjB,MACA;AAFiB;AAGjB,SAAK,SAAS,KAAK,OAAO,QAAQ,OAAO,EAAE;AAC3C,SAAK,QAAQ,KAAK,SAAS,CAAC,MAAM,IAAI;AAAA,EACxC;AAAA,EALmB;AAAA,EATF;AAAA,EACA;AAAA,EAejB,MAAM,iBAAgC;AACpC,UAAM,KAAK,MAAM;AAAA,EACnB;AAAA,EAEA,MAAM,iBAAgC;AACpC,UAAM,KAAK,MAAM;AAAA,EACnB;AAAA,EAEA,MAAc,QAAuB;AACnC,UAAM,QAAQ,KAAK,SAAS;AAC5B,UAAM,UAAU,CAAC,KAAK,gBAAgB,eAAe,eAAe;AACpE,eAAW,QAAQ,KAAK,OAAO;AAC7B,YAAM,IAAI,SAAS,mBAAmB,IAAI,CAAC;AAC3C,cAAQ,KAAK,eAAe,CAAC,IAAI,cAAc,CAAC,EAAE;AAAA,IACpD;AACA,eAAW,QAAQ,SAAS;AAC1B,YAAM,MAAM,OAAO,IAAI,QAAQ,KAAK,SAAS,IAAI,GAAG,EAAE,cAAc,KAAK,CAAC;AAAA,IAC5E;AAAA,EACF;AACF;;;ACtDO,IAAM,qBAAkC,CAAC,UAA4B;AAC1E,UAAQ,IAAI,KAAK,UAAU,KAAK,CAAC;AACnC;;;ACAA,IAAM,2BAA2B;AAe1B,SAAS,wBACd,QACA,SACmB;AACnB,QAAM,WAAW,SAAS,YAAY;AAGtC,QAAM,UAAwB,SAAS,UAAU,CAAC,OAAO,SAAS,MAAM,OAAO,IAAI;AAEnF,SAAO;AAAA,IACL,MAAM,OAAO,OAAO,SAAS;AAC3B,UAAI,CAAC,MAAO,QAAO;AACnB,YAAM,OAAO,IAAI,SAAS;AAC1B,WAAK,OAAO,UAAU,MAAM;AAC5B,WAAK,OAAO,YAAY,KAAK;AAC7B,UAAI,SAAS,SAAU,MAAK,OAAO,YAAY,QAAQ,QAAQ;AAE/D,UAAI;AACJ,UAAI;AACF,mBAAW,MAAM,QAAQ,UAAU,EAAE,QAAQ,QAAQ,MAAM,KAAK,CAAC;AAAA,MACnE,QAAQ;AACN,eAAO;AAAA,MACT;AACA,UAAI,CAAC,SAAS,GAAI,QAAO;AAEzB,YAAM,UAAW,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,IAAI;AACvD,aAAO,SAAS,YAAY;AAAA,IAC9B;AAAA,EACF;AACF;AAoCA,IAAM,eAAuC;AAAA,EAC3C,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAEA,SAAS,WAAW,OAAuB;AACzC,SAAO,MAAM,QAAQ,YAAY,CAAC,SAAS,aAAa,IAAI,KAAK,IAAI;AACvE;AAGA,SAAS,eAAe,OAAuB;AAC7C,SAAO,MAAM,QAAQ,YAAY,GAAG,EAAE,KAAK;AAC7C;AAOO,SAAS,kBACd,SACA,SAA0D,CAAC,GAC7C;AACd,QAAM,SAAS,OAAO,eAAe,KAAK;AAC1C,QAAM,UAAU;AAAA,IACd,SAAS,GAAG,MAAM,IAAI,QAAQ,KAAK,KAAK,gBAAgB,QAAQ,KAAK;AAAA,EACvE;AAEA,QAAM,SAAyC;AAAA,IAC7C,CAAC,QAAQ,QAAQ,KAAK;AAAA,IACtB,CAAC,UAAU,QAAQ,MAAM;AAAA,IACzB,CAAC,QAAQ,QAAQ,OAAO;AAAA,IACxB,CAAC,cAAc,QAAQ,SAAS;AAAA,IAChC,CAAC,WAAW,QAAQ,MAAM,OAAO;AAAA,IACjC,CAAC,MAAM,QAAQ,MAAM,EAAE;AAAA,IACvB,CAAC,YAAY,QAAQ,MAAM,UAAU;AAAA,EACvC;AACA,QAAM,UAAU,OAAO,OAAO,CAAC,UAAqC,MAAM,CAAC,MAAM,MAAS;AAE1F,QAAM,OAAO;AAAA,IACX,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA,GAAG,QAAQ,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,GAAG,GAAG,KAAK,KAAK,EAAE;AAAA,EACrD,EAAE,KAAK,IAAI;AAEX,QAAM,OAAO;AAAA,IACX,kDAAkD,WAAW,QAAQ,OAAO,CAAC;AAAA,IAC7E;AAAA,IACA;AAAA,IACA,QAAQ,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,GAAG,WAAW,GAAG,CAAC,KAAK,WAAW,KAAK,CAAC,EAAE,EAAE,KAAK,MAAM;AAAA,IACrF;AAAA,EACF,EAAE,KAAK,EAAE;AAET,SAAO,EAAE,SAAS,MAAM,KAAK;AAC/B;AAOO,SAAS,yBACd,SACA,QACgB;AAChB,SAAO;AAAA,IACL,MAAM,KAAK,SAAS;AAClB,YAAM,OAAO,kBAAkB,SAAS,MAAM;AAC9C,YAAM,QAAQ,KAAK;AAAA,QACjB,IAAI,OAAO;AAAA,QACX,MAAM,OAAO;AAAA,QACb,SAAS,QAAQ;AAAA,QACjB,SAAS,KAAK;AAAA,QACd,MAAM,KAAK;AAAA,QACX,MAAM,KAAK;AAAA,MACb,CAAC;AAAA,IACH;AAAA,EACF;AACF;;;ACzJA,SAAS,gDAAgD;AACzD,SAAS,iBAAAC,sBAAmC;AAC5C,SAAS,8BAA8B;AAIvC,IAAM,kBAAkB;AAOxB,eAAsB,SACpB,SACA,IACA,UACmB;AACnB,QAAM,UAAU,IAAI,iBAAiB,EAAE;AACvC,QAAM,cAAc,YAA8B;AAChD,QAAI;AACF,cAAQ,MAAM,QAAQ,WAAW,GAAG;AAAA,IACtC,SAAS,GAAG;AAGV,UAAI,aAAaC,eAAe,QAAO,SAAS;AAChD,YAAM;AAAA,IACR;AAAA,EACF;AAEA,QAAM,SAAS,uBAAuB,EAAE,aAAa,MAAM,gBAAgB,CAAC;AAK5E,QAAM,YAAY,IAAI,yCAAyC;AAAA,IAC7D,oBAAoB;AAAA,IACpB,oBAAoB;AAAA,EACtB,CAAC;AACD,QAAM,OAAO,QAAQ,SAAS;AAI9B,QAAM,WAAW,MAAM,UAAU,cAAc,OAAO;AAEtD,QAAM,UAAU,IAAI,QAAQ,SAAS,OAAO;AAC5C,UAAQ,IAAI,0BAA0B,SAAS;AAC/C,MAAI,CAAC,QAAQ,IAAI,eAAe,EAAG,SAAQ,IAAI,iBAAiB,UAAU;AAC1E,SAAO,IAAI,SAAS,SAAS,MAAM;AAAA,IACjC,QAAQ,SAAS;AAAA,IACjB,YAAY,SAAS;AAAA,IACrB;AAAA,EACF,CAAC;AACH;;;AC1EA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAAC;AAAA,EACA;AAAA,OAKK;AAGP,IAAM,mBAAmB;AAGzB,IAAM,oBAAoB;AAOnB,IAAM,sBAAsB;AAGnC,SAAS,eAAe,OAAqD;AAC3E,MAAI,UAAU,OAAW,QAAO;AAChC,SAAQ,0BAAgD,SAAS,KAAK,IACjE,QACD;AACN;AAGA,SAAS,mBAA2B;AAClC,SAAO,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACrC;AAOA,SAAS,YAAoB;AAC3B,QAAM,QAAQ,IAAI,WAAW,CAAC;AAC9B,SAAO,gBAAgB,KAAK;AAC5B,SAAO,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC1E;AAqBO,IAAM,wBAAN,MAA2D;AAAA,EAC/C;AAAA,EACA;AAAA,EAEjB,YAAY,QAAkB,UAAsC,CAAC,GAAG;AACtE,SAAK,SAAS;AACd,SAAK,gBAAgB,QAAQ,iBAAiB;AAAA,EAChD;AAAA,EAEA,MAAM,SAAS,MAAmB,SAA8C;AAC9E,UAAM,QAAQ,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC;AAGrD,UAAM,OAAO,eAAe,SAAS,WAAW,KAAK,gBAAgB,KAAK;AAC1E,UAAM,MAAM,SAAS,OAAO,iBAAiB,IAAI,IAAI;AACrD,UAAM,cAAc,QAAQ,SAAS,eAAe,KAAK,QAAQ;AAEjE,UAAM,MAAM,GAAG,gBAAgB,GAAG,OAAO,iBAAiB,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,GAAG;AAClF,UAAM,KAAK,OAAO,IAAI,KAAK,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC;AAEnE,UAAM,OAAO,SAAS,OAAO,cAAc,OAAO,IAAI,IAAI;AAC1D,UAAM,MAAM,IAAI,GAAG;AACnB,WAAO;AAAA,MACL,IAAI;AAAA,MACJ;AAAA,MACA,WAAW,KAAK,YAAY,GAAG;AAAA,MAC/B,UAAU;AAAA,MACV,MAAM,MAAM;AAAA,MACZ,OAAO,MAAM;AAAA,MACb,QAAQ,MAAM;AAAA,MACd,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC;AAAA,EACF;AAAA,EAEA,MAAM,aAAa,SAAkC;AACnD,UAAM,OAAO,MAAM,KAAK,OAAO,KAAK,OAAO;AAC3C,QAAI,SAAS,KAAM,OAAM,IAAIA,eAAc,iCAAiC,OAAO,IAAI;AACvF,WAAO,KAAK,YAAY,IAAI,OAAO,EAAE;AAAA,EACvC;AAAA,EAEA,MAAM,YAAY,SAAgC;AAGhD,UAAM,KAAK,OAAO,OAAO,OAAO;AAAA,EAClC;AAAA,EAEA,MAAM,aAAqC;AACzC,UAAM,UAAyB,CAAC;AAChC,QAAI;AACJ,OAAG;AACD,YAAM,OAAO,MAAM,KAAK,OAAO,KAAK,EAAE,QAAQ,kBAAkB,OAAO,CAAC;AACxE,iBAAW,UAAU,KAAK,SAAS;AACjC,cAAM,MAAM,IAAI,OAAO,GAAG;AAC1B,gBAAQ,KAAK;AAAA,UACX,IAAI,OAAO;AAAA,UACX;AAAA,UACA,WAAW,KAAK,YAAY,GAAG;AAAA,UAC/B,UAAU,OAAO,cAAc,eAAe;AAAA,UAC9C,MAAM,OAAO;AAAA,UACb,WAAW,OAAO,SAAS,YAAY;AAAA,QACzC,CAAC;AAAA,MACH;AACA,eAAS,KAAK,YAAY,KAAK,SAAS;AAAA,IAC1C,SAAS,WAAW;AACpB,WAAO;AAAA,EACT;AAAA,EAEQ,YAAY,MAAsB;AACxC,WAAO,KAAK,kBAAkB,KAAK,GAAG,KAAK,aAAa,GAAG,IAAI,KAAK;AAAA,EACtE;AACF;;;AVfA,SAAS,aAAa,KAAmC;AACvD,MAAI,QAAQ,UAAa,QAAQ,GAAI,QAAO,CAAC;AAC7C,SAAO,IACJ,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,CAAC,MAAM,MAAM,EAAE;AAC3B;AAGA,SAAS,cAAc,UAA2B;AAChD,SAAO,aAAa,YAAY,SAAS,WAAW,SAAS;AAC/D;AAQA,SAAS,YAAY,UAA2B;AAC9C,SAAO,SAAS,WAAW,UAAU;AACvC;AAOA,SAAS,UAAU,UAA2B;AAC5C,SAAO,aAAa;AACtB;AAQA,SAAS,kBAAkB,UAA2B;AACpD,SAAO,aAAa;AACtB;AAQA,SAAS,oBAAoB,UAA2B;AACtD,SAAO,aAAa,wBAAwB,aAAa;AAC3D;AAQA,SAAS,mBAAmB,SAAkB,KAAoB;AAChE,MAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,QAAQ;AACzD,WAAO,IAAI,SAAS,sBAAsB,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3D;AACA,QAAM,OAAO,IAAI,aAAa;AAC9B,QAAM,UAAU,IAAI,QAAQ;AAC5B,UAAQ,IAAI,gBAAgB,OAAO,mCAAmC,yBAAyB;AAC/F,UAAQ,IAAI,0BAA0B,SAAS;AAC/C,UAAQ,IAAI,iBAAiB,sBAAsB;AACnD,QAAM,OAAO,QAAQ,WAAW,SAAS,OAAO,OAAO,kBAAkB;AACzE,SAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,QAAQ,CAAC;AACpD;AAUA,eAAe,aACb,SACA,KACA,UACA,KACmB;AACnB,QAAM,UAAU,IAAI,iBAAiB,IAAI,UAAU;AACnD,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,QAAQ,WAAW,GAAG;AAAA,EACzC,QAAQ;AACN,cAAU,SAAS;AAAA,EACrB;AAEA,QAAM,UAAU,QAAQ,SAAS;AACjC,MAAI,SAAS,YAAY,MAAM;AAE7B,WAAO,IAAI,SAAS,KAAK,UAAU,EAAE,IAAI,OAAO,OAAO,YAAY,CAAC,GAAG;AAAA,MACrE,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mCAAmC,iBAAiB,WAAW;AAAA,IAC5F,CAAC;AAAA,EACH;AAGA,QAAM,UAAU,IAAI;AACpB,SAAO,cAAc,SAAS;AAAA,IAC5B,UAAU,wBAAwB,IAAI,4BAA4B,EAAE;AAAA,IACpE,WAAW,yBAAyB,SAAS;AAAA,MAC3C,IAAI,IAAI,sBAAsB;AAAA,MAC9B,MAAM,EAAE,OAAO,IAAI,wBAAwB,GAAG;AAAA,MAC9C,GAAI,QAAQ,gBAAgB,EAAE,eAAe,QAAQ,cAAc,IAAI,CAAC;AAAA,IAC1E,CAAC;AAAA;AAAA,IAED,QAAQ,EAAE,gBAAgB,CAAC,IAAI,MAAM,EAAE;AAAA,IACvC,UAAU,CAAC,QAAQ;AACjB,YAAM,KAAM,IAAuC;AACnD,YAAM,UAAU,OAAO,IAAI,YAAY,WAAW,GAAG,UAAU;AAC/D,YAAM,KAAK,IAAI,QAAQ,IAAI,kBAAkB,KAAK;AAClD,aAAO,EAAE,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC,GAAI,GAAI,KAAK,EAAE,GAAG,IAAI,CAAC,EAAG;AAAA,IAClE;AAAA,EACF,CAAC;AACH;AAQA,eAAe,WAAW,SAAkB,QAAkB,KAA6B;AACzF,MAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,QAAQ;AACzD,WAAO,IAAI,SAAS,sBAAsB,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3D;AAGA,QAAM,MAAM,IAAI,SAAS,MAAM,CAAC;AAChC,QAAM,SAAS,MAAM,OAAO,IAAI,GAAG;AACnC,MAAI,WAAW,MAAM;AACnB,WAAO,IAAI,SAAS,aAAa,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClD;AACA,QAAM,UAAU,IAAI,QAAQ;AAC5B,QAAM,cAAc,OAAO,cAAc;AACzC,MAAI,gBAAgB,OAAW,SAAQ,IAAI,gBAAgB,WAAW;AAGtE,UAAQ,IAAI,0BAA0B,SAAS;AAC/C,UAAQ,IAAI,iBAAiB,mBAAmB;AAChD,UAAQ,IAAI,QAAQ,OAAO,QAAQ;AACnC,QAAM,OAAO,QAAQ,WAAW,SAAS,OAAO,OAAO;AACvD,SAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,QAAQ,CAAC;AACpD;AASA,eAAe,cAAc,SAAkB,QAAiB,KAA6B;AAC3F,MAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,QAAQ;AACzD,WAAO,IAAI,SAAS,sBAAsB,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3D;AAIA,QAAM,OAAO,IAAI,SAAS,MAAM,SAAS,MAAM;AAC/C,QAAM,YAAY,SAAS,KAAK,MAAM;AACtC,QAAM,gBAAgB,MAAM,OAAO,MAAM,IAAI,QAAQ,IAAI,IAAI,WAAW,IAAI,MAAM,GAAG,OAAO,CAAC;AAC7F,QAAM,UAAU,IAAI,QAAQ,cAAc,OAAO;AACjD,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,0BAA0B,CAAC,GAAG;AACvE,YAAQ,IAAI,MAAM,KAAK;AAAA,EACzB;AAGA,UAAQ,OAAO,MAAM;AACrB,SAAO,IAAI,SAAS,cAAc,MAAM;AAAA,IACtC,QAAQ,cAAc;AAAA,IACtB,YAAY,cAAc;AAAA,IAC1B;AAAA,EACF,CAAC;AACH;AAeO,SAAS,oBAAoB,MAGlC;AACA,SAAO;AAAA,IACL,MAAM,SAAkB,KAAwC;AAC9D,YAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAK/B,UAAI,IAAI,UAAU,cAAc,IAAI,QAAQ,GAAG;AAC7C,eAAO,cAAc,SAAS,IAAI,QAAQ,GAAG;AAAA,MAC/C;AAKA,UAAI,IAAI,cAAc,YAAY,IAAI,QAAQ,GAAG;AAC/C,eAAO,WAAW,SAAS,IAAI,YAAY,GAAG;AAAA,MAChD;AAIA,UAAI,UAAU,IAAI,QAAQ,GAAG;AAC3B,eAAO,SAAS,SAAS,IAAI,YAAY,KAAK,QAAQ;AAAA,MACxD;AAMA,UAAI,kBAAkB,IAAI,QAAQ,GAAG;AACnC,eAAO,iBAAiB,SAAS,IAAI,YAAY,KAAK,QAAQ;AAAA,MAChE;AAKA,UAAI,oBAAoB,IAAI,QAAQ,GAAG;AACrC,eAAO,mBAAmB,SAAS,GAAG;AAAA,MACxC;AAKA,UACE,IAAI,aAAa,kBACjB,QAAQ,WAAW,UACnB,IAAI,uBACJ;AACA,eAAO,aAAa,SAAS,KAAK,KAAK,UAAU,GAAG;AAAA,MACtD;AAEA,YAAM,UAAU,IAAI,iBAAiB,IAAI,UAAU;AAKnD,YAAM,eAAe,IAAI,aACrB,IAAI,sBAAsB,IAAI,YAAY,EAAE,eAAe,IAAI,OAAO,CAAC,IACvE;AACJ,YAAM,cAA2B,IAAI,sBAAsB,MAAM,OAAO,SAAS;AAAA,QAC/E,QAAQ,IAAI;AAAA,MACd,CAAC;AACD,YAAM,cAA2B;AASjC,YAAM,SAAS,IAAI,KAAK,EAAE,SAAS,oBAAoB,CAAC;AACxD,aAAO;AAAA,QAAS,CAAC,MACf,gBAAgB,GAAG;AAAA,UACjB,MAAM,YAAY;AAAA,UAClB,QAAQ;AAAA,UACR,OAAO;AAAA,UACP,QAAQ,oBAAoB,IAAI,IAAI,EAAE,IAAI,GAAG,EAAE,QAAQ;AAAA,QACzD,CAAC;AAAA,MACH;AACA,aAAO;AAAA,QACL;AAAA,QACA,kBAAkB;AAAA,UAChB;AAAA,UACA;AAAA,UACA,eAAe,MAAM,IAAI;AAAA,UACzB,iBAAiB,MAAM,aAAa,IAAI,oBAAoB;AAAA,UAC5D;AAAA,UACA;AAAA,QACF,CAAC;AAAA,MACH;AACA,aAAO,MAAM,UAAU,iBAAiB,CAAC;AACzC,aAAO;AAAA,QACL;AAAA,QACA,gBAAgB;AAAA,UACd;AAAA,UACA,UAAU,KAAK;AAAA;AAAA;AAAA,UAGf,iBAAiB,IAAI,kBAAkB,IAAI,UAAU;AAAA;AAAA,UAErD,SAAS;AAAA,QACX,CAAC;AAAA,MACH;AAEA,aAAO,OAAO,MAAM,SAAS,GAAG;AAAA,IAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,MAAM,UAAU,aAAkC,KAAU,MAAwB;AAClF,YAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,UAAI;AACF,cAAM,SAAS,MAAM,aAAa;AAAA,UAChC,gBAAgB,IAAI,iBAAiB,IAAI,UAAU;AAAA,UACnD,iBAAiB,IAAI,kBAAkB,IAAI,UAAU;AAAA,UACrD,SAAS;AAAA,YACP,aAAa,IAAI;AAAA,YACjB,aAAa,IAAI;AAAA,UACnB;AAAA,UACA,UAAU,KAAK;AAAA,QACjB,CAAC;AACD,cAAM,OACJ,OAAO,WAAW,WACd,0BACA,OAAO,WAAW,UAChB,0BACA;AACR,gBAAQ;AAAA,UACN,KAAK,UAAU,EAAE,MAAM,WAAW,QAAQ,OAAO,QAAQ,UAAU,OAAO,SAAS,CAAC;AAAA,QACtF;AAAA,MACF,SAAS,KAAK;AACZ,gBAAQ;AAAA,UACN,KAAK,UAAU;AAAA,YACb,MAAM;AAAA,YACN;AAAA,YACA,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,YACvD,UAAU,CAAC;AAAA,UACb,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,kBAA2B;AAClC,QAAM,IAAI,SAAS,eAAW;AAC9B,MAAI,CAAC,EAAE,GAAI,OAAM,IAAI,MAAM,oCAAoC;AAC/D,SAAO,EAAE;AACX;AAEA,IAAO,gBAAQ,oBAAoB,EAAE,UAAU,gBAAgB,CAAC;","names":["NotFoundError","NotFoundError","NotFoundError","NotFoundError","NotFoundError","NotFoundError"]}
1
+ {"version":3,"sources":["../src/index.ts","../../../examples/personal-profile/takuhon.json","../src/activity-svg.ts","../src/kv-activity-storage.ts","../src/kv-storage.ts","../src/activity-sync.ts","../src/admin/cloudflare-cache-purger.ts","../src/admin/console-audit-logger.ts","../src/contact.ts","../src/mcp.ts","../src/r2-storage.ts"],"sourcesContent":["import {\n ERROR_SLUGS,\n adminAssetSecurityHeaders,\n createAdminApiApp,\n createAdminUiApp,\n createPublicApp,\n localePrefixGetPath,\n problemResponse,\n type AuditLogger,\n type CachePurger,\n type PublicRenderOptions,\n} from '@takuhon/api';\nimport { handleContact } from '@takuhon/contact';\nimport { contactWidgetCss, contactWidgetJs } from '@takuhon/contact/assets';\nimport { validate, type Takuhon } from '@takuhon/core';\nimport { Hono } from 'hono';\n\nimport exampleJson from '../../../examples/personal-profile/takuhon.json' with { type: 'json' };\n\nimport { serveActivitySvg } from './activity-svg.js';\nimport { syncActivity } from './activity-sync.js';\nimport { CloudflareCachePurger } from './admin/cloudflare-cache-purger.js';\nimport { consoleAuditLogger } from './admin/console-audit-logger.js';\nimport {\n createSendEmailTransport,\n createTurnstileVerifier,\n type SendEmailBinding,\n} from './contact.js';\nimport { KvActivityStorage } from './kv-activity-storage.js';\nimport { KvTakuhonStorage } from './kv-storage.js';\nimport { serveMcp } from './mcp.js';\nimport { ASSET_CACHE_CONTROL, R2TakuhonAssetStorage } from './r2-storage.js';\n\nexport { buildInquiryEmail, createSendEmailTransport, createTurnstileVerifier } from './contact.js';\nexport type {\n InquiryEmail,\n SendEmailBinding,\n SendEmailTransportConfig,\n TurnstileVerifierOptions,\n} from './contact.js';\n\nexport interface Env {\n TAKUHON_KV: KVNamespace;\n /**\n * Admin bearer token. Provision via `wrangler secret put TAKUHON_ADMIN_TOKEN`.\n * Leave unset to disable admin writes entirely (every PUT/DELETE returns 401).\n */\n TAKUHON_ADMIN_TOKEN?: string;\n /**\n * Comma-separated Origin allowlist for browser-originating admin requests.\n * Empty / unset disables the check (deploy without a configured allowlist is\n * acceptable when the admin UI is same-origin; documented in the README).\n */\n TAKUHON_ADMIN_ORIGIN?: string;\n /**\n * Workers Assets binding holding the bundled admin SPA (`apps/admin`). When\n * present, `/admin/*` is served from it under a strict CSP; when absent, the\n * Worker falls back to the inline `createAdminUiApp` editor, so deployments\n * without Workers Assets configured still have a working admin.\n */\n ASSETS?: Fetcher;\n /**\n * R2 bucket holding uploaded image assets. Optional, mirroring {@link ASSETS}:\n * when bound, `POST /api/admin/assets` stores images here and `GET /assets/*`\n * serves them; when absent, the upload endpoint stays unregistered (404) and\n * avatars remain URL-only.\n */\n TAKUHON_R2?: R2Bucket;\n /**\n * GitHub token for the scheduled activity sync. Optional: languages are\n * fetched unauthenticated without it; the contribution calendar (GraphQL,\n * token-only) is simply skipped. Provision via\n * `wrangler secret put TAKUHON_GITHUB_TOKEN` — never in `wrangler.toml`.\n */\n TAKUHON_GITHUB_TOKEN?: string;\n /**\n * WakaTime API key for the scheduled activity sync; required to read coding\n * time (WakaTime has no unauthenticated mode). Provision via\n * `wrangler secret put TAKUHON_WAKATIME_KEY` — never in `wrangler.toml`.\n * Only flows through the sync step; never persisted.\n */\n TAKUHON_WAKATIME_KEY?: string;\n /**\n * Cloudflare `send_email` binding that delivers contact-form submissions.\n * Optional, like {@link ASSETS} / {@link TAKUHON_R2}: when bound, `POST\n * /api/contact` is mounted (and additionally requires `settings.contact`\n * `.enabled`); when absent the endpoint stays unmounted, so a stray POST\n * 405s and the contact form is effectively off regardless of the profile.\n * Provision with a `[[send_email]]` block in `wrangler.toml`.\n */\n TAKUHON_CONTACT_EMAIL?: SendEmailBinding;\n /**\n * Verified destination inbox for contact submissions (must be a\n * Cloudflare Email Routing-verified address). A missing value degrades to a\n * failed delivery (502), never a crash.\n */\n TAKUHON_CONTACT_TO?: string;\n /**\n * From address for contact emails; must be on a domain you control\n * (e.g. `noreply@example.com`). A missing value degrades to a 502.\n */\n TAKUHON_CONTACT_FROM?: string;\n /**\n * Turnstile secret key for server-side `siteverify`. Provision via\n * `wrangler secret put TAKUHON_TURNSTILE_SECRET` — never in `wrangler.toml`.\n * A missing secret degrades to a rejected challenge (422), never a crash.\n * The public site key is not here — it lives in `settings.contact`\n * `.turnstileSiteKey` because it is embedded in the page.\n */\n TAKUHON_TURNSTILE_SECRET?: string;\n}\n\n/** Options accepted by {@link createTakuhonWorker}. */\nexport interface CreateTakuhonWorkerOptions {\n /**\n * Lazy producer for the fallback Takuhon document served when KV has no\n * stored profile yet. Called at most once per Worker invocation, on the\n * cold path where the storage layer returns no entry. Implementations\n * typically import a bundled `takuhon.json`, validate it once, and return\n * the resulting value.\n */\n readonly fallback: () => Takuhon;\n /**\n * First-party host composition for the server-rendered profile page: renderer\n * `slots` / `labels` / `omitSections` plus an optional CSP extension (see\n * {@link PublicRenderOptions}). Lets a host worker inject its own C-group\n * (PWA registration, analytics beacon, extra `<head>` tags) and widen the\n * page CSP to allow those scripts. Omitted (the turnkey default) leaves the\n * page and its strict CSP untouched.\n */\n readonly render?: PublicRenderOptions;\n}\n\nfunction parseOrigins(raw: string | undefined): string[] {\n if (raw === undefined || raw === '') return [];\n return raw\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s !== '');\n}\n\n/** Admin UI (not the `/api/admin` API) request paths served from the SPA bundle. */\nfunction isAdminUiPath(pathname: string): boolean {\n return pathname === '/admin' || pathname.startsWith('/admin/');\n}\n\n/**\n * Public asset-delivery paths served from R2. Matches only the literal\n * `/assets/...` prefix, so a locale-prefixed `/{locale}/assets/...` is left to\n * the router (which 404s it) — asset delivery is intentionally locale-agnostic,\n * like `/health` and the admin surface.\n */\nfunction isAssetPath(pathname: string): boolean {\n return pathname.startsWith('/assets/');\n}\n\n/**\n * The read-only MCP endpoint. Matches only the literal `/mcp`, so a\n * locale-prefixed `/{locale}/mcp` is left to the router (which 404s it) — the\n * endpoint is intentionally locale-agnostic, like `/health` and `/assets/*`.\n */\nfunction isMcpPath(pathname: string): boolean {\n return pathname === '/mcp';\n}\n\n/**\n * The read-only activity-badge endpoint. Matches only the literal\n * `/activity.svg`, so a locale-prefixed `/{locale}/activity.svg` is left to the\n * router (which 404s it) — the endpoint is intentionally locale-agnostic, like\n * `/health`, `/assets/*`, and `/mcp`.\n */\nfunction isActivitySvgPath(pathname: string): boolean {\n return pathname === '/activity.svg';\n}\n\n/**\n * The bundled contact-widget assets. Matches only the literal\n * `/contact-widget.{js,css}` (locale-agnostic, like `/mcp` and `/activity.svg`);\n * these are the URLs the server-rendered page references when the profile\n * enables the contact form.\n */\nfunction isContactWidgetPath(pathname: string): boolean {\n return pathname === '/contact-widget.js' || pathname === '/contact-widget.css';\n}\n\n/**\n * Serve a bundled contact-widget asset (`GET`/`HEAD /contact-widget.{js,css}`).\n * The content is inlined into the Worker from `@takuhon/contact/assets`, so no\n * asset-hosting binding is needed; `nosniff` keeps the served type honest and a\n * moderate cache lets it refresh on the next deploy.\n */\nfunction serveContactWidget(request: Request, url: URL): Response {\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n return new Response('Method Not Allowed', { status: 405 });\n }\n const isJs = url.pathname === '/contact-widget.js';\n const headers = new Headers();\n headers.set('content-type', isJs ? 'text/javascript; charset=utf-8' : 'text/css; charset=utf-8');\n headers.set('x-content-type-options', 'nosniff');\n headers.set('cache-control', 'public, max-age=3600');\n const body = request.method === 'HEAD' ? null : isJs ? contactWidgetJs : contactWidgetCss;\n return new Response(body, { status: 200, headers });\n}\n\n/**\n * Handle a contact-form submission (`POST /api/contact`). The caller has\n * already confirmed the `send_email` binding is bound; this additionally\n * requires `settings.contact.enabled` (so disabling the form turns off the\n * endpoint too) and then runs the portable `handleContact` pipeline with the\n * Cloudflare Turnstile + `send_email` seams. Stateless and never throws: a\n * missing secret / recipient degrades to a 422 / 502 inside the pipeline.\n */\nasync function serveContact(\n request: Request,\n env: Env,\n fallback: () => Takuhon,\n url: URL,\n): Promise<Response> {\n const storage = new KvTakuhonStorage(env.TAKUHON_KV);\n let profile: Takuhon;\n try {\n profile = (await storage.getProfile()).data;\n } catch {\n profile = fallback();\n }\n\n const contact = profile.settings.contact;\n if (contact?.enabled !== true) {\n // Form disabled in the profile: behave as if the endpoint were not mounted.\n return new Response(JSON.stringify({ ok: false, error: 'not_found' }), {\n status: 404,\n headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' },\n });\n }\n\n // The binding presence is the caller's gate; assert it for the type.\n const binding = env.TAKUHON_CONTACT_EMAIL!;\n return handleContact(request, {\n verifier: createTurnstileVerifier(env.TAKUHON_TURNSTILE_SECRET ?? ''),\n transport: createSendEmailTransport(binding, {\n to: env.TAKUHON_CONTACT_TO ?? '',\n from: { email: env.TAKUHON_CONTACT_FROM ?? '' },\n ...(contact.subjectPrefix ? { subjectPrefix: contact.subjectPrefix } : {}),\n }),\n // Same-origin only: the widget POSTs from the page it is embedded on.\n config: { allowedOrigins: [url.origin] },\n readMeta: (req) => {\n const cf = (req as { cf?: { country?: unknown } }).cf;\n const country = typeof cf?.country === 'string' ? cf.country : undefined;\n const ip = req.headers.get('cf-connecting-ip') ?? undefined;\n return { ...(country ? { country } : {}), ...(ip ? { ip } : {}) };\n },\n });\n}\n\n/**\n * Serve an uploaded asset from R2 (`GET`/`HEAD /assets/*`). This is the public,\n * unauthenticated delivery proxy: the bucket stays private and the Worker\n * mediates every read so it can force `X-Content-Type-Options: nosniff`\n * (`security.md` §4.7) and an immutable cache policy. A missing object is 404.\n */\nasync function serveAsset(request: Request, bucket: R2Bucket, url: URL): Promise<Response> {\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n return new Response('Method Not Allowed', { status: 405 });\n }\n // The R2 object key is the pathname without its leading slash, e.g.\n // `/assets/1715432100-a8f3.webp` → `assets/1715432100-a8f3.webp`.\n const key = url.pathname.slice(1);\n const object = await bucket.get(key);\n if (object === null) {\n return new Response('Not Found', { status: 404 });\n }\n const headers = new Headers();\n const contentType = object.httpMetadata?.contentType;\n if (contentType !== undefined) headers.set('content-type', contentType);\n // SECURITY-CRITICAL (security.md §4.7): force nosniff so a stored object can\n // never be reinterpreted as an active type (e.g. HTML) by the browser.\n headers.set('x-content-type-options', 'nosniff');\n headers.set('cache-control', ASSET_CACHE_CONTROL);\n headers.set('etag', object.httpEtag);\n const body = request.method === 'HEAD' ? null : object.body;\n return new Response(body, { status: 200, headers });\n}\n\n/**\n * Serve the admin SPA from the Workers Assets binding. The bundle's files live\n * at the assets root, so the `/admin` prefix is stripped before lookup; `/admin`\n * and `/admin/` map to `index.html`. The strict admin CSP / security headers are\n * applied to every response (the binding's own headers would otherwise cache\n * the operator-only UI and omit the policy).\n */\nasync function serveAdminSpa(request: Request, assets: Fetcher, url: URL): Promise<Response> {\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n return new Response('Method Not Allowed', { status: 405 });\n }\n // Strip the `/admin` mount prefix; `/admin` and `/admin/` map to the bundle\n // root `/` (which Workers Assets serves as index.html). Requesting\n // `/index.html` directly would 307-redirect under `auto-trailing-slash`.\n const rest = url.pathname.slice('/admin'.length);\n const assetPath = rest === '' ? '/' : rest;\n const assetResponse = await assets.fetch(new Request(new URL(assetPath, url.origin), request));\n const headers = new Headers(assetResponse.headers);\n for (const [name, value] of Object.entries(adminAssetSecurityHeaders())) {\n headers.set(name, value);\n }\n // The admin UI is `private, no-store`, so a conditional-request ETag carried\n // over from the binding would be misleading — drop it.\n headers.delete('etag');\n return new Response(assetResponse.body, {\n status: assetResponse.status,\n statusText: assetResponse.statusText,\n headers,\n });\n}\n\n/**\n * Build a Cloudflare Worker handler for the takuhon adapter. Wires\n * `@takuhon/api`'s public/admin app factories to the KV-backed storage,\n * Cloudflare edge cache purger, and console audit logger that ship with\n * this package.\n *\n * This is the entry point used by projects scaffolded with\n * `create-takuhon`: their `src/index.ts` imports `createTakuhonWorker`,\n * passes a `fallback` that loads the project's own `takuhon.json`, and\n * `export default`s the returned handler. The default export of this\n * module is a convenience that calls the same factory with the monorepo's\n * bundled `personal-profile` fixture.\n */\nexport function createTakuhonWorker(opts: CreateTakuhonWorkerOptions): {\n fetch: (request: Request, env: Env) => Response | Promise<Response>;\n scheduled: (controller: ScheduledController, env: Env, ctx: ExecutionContext) => Promise<void>;\n} {\n return {\n fetch(request: Request, env: Env): Response | Promise<Response> {\n const url = new URL(request.url);\n\n // Serve the bundled admin SPA when a Workers Assets binding is present.\n // Without it, the request falls through to the inline editor mounted on\n // the router below, so admin stays available either way.\n if (env.ASSETS && isAdminUiPath(url.pathname)) {\n return serveAdminSpa(request, env.ASSETS, url);\n }\n\n // Serve uploaded assets from R2 when the bucket is bound. Without it,\n // `/assets/*` falls through to the router and 404s, matching the\n // unregistered upload endpoint.\n if (env.TAKUHON_R2 && isAssetPath(url.pathname)) {\n return serveAsset(request, env.TAKUHON_R2, url);\n }\n\n // Read-only MCP endpoint. Stateless (no Durable Object / session); reads\n // the profile from the same KV the public API uses.\n if (isMcpPath(url.pathname)) {\n return serveMcp(request, env.TAKUHON_KV, opts.fallback);\n }\n\n // Read-only activity badge. Renders the synced snapshot as a\n // self-contained SVG image (?theme=dark for the dark variant), re-checking\n // the settings.activity opt-in on every request — parity with\n // GET /api/activity.\n if (isActivitySvgPath(url.pathname)) {\n return serveActivitySvg(request, env.TAKUHON_KV, opts.fallback);\n }\n\n // Bundled contact-widget assets, referenced by the server-rendered page\n // when the contact form is enabled. Served from the Worker bundle, so no\n // asset binding is required (parity with /mcp and /activity.svg).\n if (isContactWidgetPath(url.pathname)) {\n return serveContactWidget(request, url);\n }\n\n // Contact-form submission. Mounted only when the send_email binding is\n // bound; serveContact additionally requires settings.contact.enabled.\n // Without the binding the POST falls through to the public app's 405.\n if (\n url.pathname === '/api/contact' &&\n request.method === 'POST' &&\n env.TAKUHON_CONTACT_EMAIL\n ) {\n return serveContact(request, env, opts.fallback, url);\n }\n\n const storage = new KvTakuhonStorage(env.TAKUHON_KV);\n // Enable image uploads only when an R2 bucket is bound; otherwise the\n // admin API leaves `POST /assets` unregistered, so uploads are disabled\n // and avatars stay URL-only. The public URL is built on this request's\n // origin since the Worker proxies delivery from its own `/assets/*` route.\n const assetStorage = env.TAKUHON_R2\n ? new R2TakuhonAssetStorage(env.TAKUHON_R2, { publicBaseUrl: url.origin })\n : undefined;\n const cachePurger: CachePurger = new CloudflareCachePurger(() => caches.default, {\n origin: url.origin,\n });\n const auditLogger: AuditLogger = consoleAuditLogger;\n\n // `getPath` strips a leading `/{locale}` prefix before route\n // matching. This is the production-critical placement: Hono's\n // `route()` flattens each mounted sub-app's routes into this router\n // and dispatches with this router's `getPath` only, so a `getPath`\n // set on the public sub-app alone would not run here. The shared\n // function's allowlist guard never strips admin remainders, so\n // `/api/admin/*` and `/admin/*` mounts stay locale-agnostic.\n const router = new Hono({ getPath: localePrefixGetPath });\n router.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 router.route(\n '/api/admin',\n createAdminApiApp({\n storage,\n assetStorage,\n getAdminToken: () => env.TAKUHON_ADMIN_TOKEN,\n getAdminOrigins: () => parseOrigins(env.TAKUHON_ADMIN_ORIGIN),\n cachePurger,\n auditLogger,\n }),\n );\n router.route('/admin', createAdminUiApp());\n router.route(\n '/',\n createPublicApp({\n storage,\n fallback: opts.fallback,\n // Serves `GET /api/activity` from the synced snapshot; the route\n // 404s while no snapshot is stored or activity is not enabled.\n activityStorage: new KvActivityStorage(env.TAKUHON_KV),\n // Advertise the read-only MCP endpoint in `/.well-known/takuhon.json`.\n mcpPath: '/mcp',\n // Host composition (slots / labels / omitSections / CSP), passed\n // straight through to the profile page. Undefined by default.\n render: opts.render,\n }),\n );\n\n return router.fetch(request, env);\n },\n\n /**\n * Cron-driven activity sync (enable with a `[triggers] crons` entry in\n * `wrangler.toml`; daily is the recommended cadence). MUST NOT throw: a\n * failed sync keeps the last-known snapshot and surfaces through the\n * structured log line below (captured by Workers Tail / Logpush), never by\n * failing the cron run.\n */\n async scheduled(_controller: ScheduledController, env: Env, _ctx: ExecutionContext) {\n const timestamp = new Date().toISOString();\n try {\n const result = await syncActivity({\n profileStorage: new KvTakuhonStorage(env.TAKUHON_KV),\n activityStorage: new KvActivityStorage(env.TAKUHON_KV),\n secrets: {\n githubToken: env.TAKUHON_GITHUB_TOKEN,\n wakatimeKey: env.TAKUHON_WAKATIME_KEY,\n },\n fallback: opts.fallback,\n });\n const type =\n result.status === 'synced'\n ? 'activity.sync.success'\n : result.status === 'empty'\n ? 'activity.sync.failure'\n : 'activity.sync.skipped';\n console.log(\n JSON.stringify({ type, timestamp, reason: result.reason, failures: result.failures }),\n );\n } catch (err) {\n console.error(\n JSON.stringify({\n type: 'activity.sync.failure',\n timestamp,\n reason: err instanceof Error ? err.message : String(err),\n failures: [],\n }),\n );\n }\n },\n };\n}\n\nfunction bundledFallback(): Takuhon {\n const r = validate(exampleJson);\n if (!r.ok) throw new Error('Bundled fixture failed validation.');\n return r.data;\n}\n\nexport default createTakuhonWorker({ fallback: bundledFallback });\n","{\n \"schemaVersion\": \"1.4.0\",\n \"profile\": {\n \"displayName\": {\n \"en\": \"Pat Rivera\",\n \"ja\": \"パット・リベラ\"\n },\n \"tagline\": {\n \"en\": \"Open-source maintainer and accessibility advocate\",\n \"ja\": \"オープンソースメンテナ / アクセシビリティ実践者\"\n },\n \"bio\": {\n \"en\": \"Pat Rivera is a fictional persona used to exercise every field of the takuhon profile schema. They maintain a handful of open-source libraries focused on accessibility tooling and frequently speak at local meetups.\",\n \"ja\": \"Pat Rivera は takuhon プロフィール schema の全フィールドを示すための架空の人物です。アクセシビリティ系のオープンソースライブラリを保守し、地域コミュニティで登壇しています。\"\n },\n \"avatar\": {\n \"url\": \"/assets/avatar.webp\",\n \"alt\": {\n \"en\": \"Pat Rivera smiling, wearing round glasses, in front of a soft gradient.\",\n \"ja\": \"ソフトなグラデーションを背景に微笑む Pat Rivera。丸眼鏡を着用。\"\n }\n },\n \"location\": {\n \"country\": \"PT\",\n \"region\": \"Lisbon\",\n \"locality\": {\n \"en\": \"Lisbon\",\n \"ja\": \"リスボン\"\n },\n \"display\": {\n \"en\": \"Lisbon, Portugal\",\n \"ja\": \"ポルトガル・リスボン\"\n }\n }\n },\n \"links\": [\n {\n \"id\": \"website\",\n \"type\": \"website\",\n \"label\": { \"en\": \"Personal site\" },\n \"url\": \"https://example.com/pat\",\n \"featured\": true,\n \"order\": 0\n },\n {\n \"id\": \"github\",\n \"type\": \"github\",\n \"url\": \"https://example.com/pat/github\",\n \"featured\": true,\n \"order\": 1\n },\n {\n \"id\": \"mastodon\",\n \"type\": \"mastodon\",\n \"url\": \"https://example.social/@pat\",\n \"order\": 2\n },\n {\n \"id\": \"blog\",\n \"type\": \"blog\",\n \"label\": {\n \"en\": \"Notes on accessible UI\",\n \"ja\": \"アクセシブル UI の覚え書き\"\n },\n \"url\": \"https://example.com/pat/blog\",\n \"order\": 3\n },\n {\n \"id\": \"newsletter\",\n \"type\": \"custom\",\n \"label\": {\n \"en\": \"Weekly newsletter\",\n \"ja\": \"週次ニュースレター\"\n },\n \"url\": \"https://example.com/pat/newsletter\",\n \"iconUrl\": \"https://example.com/assets/icons/newsletter.svg\",\n \"order\": 4\n }\n ],\n \"careers\": [\n {\n \"id\": \"stellar-ux\",\n \"organization\": {\n \"en\": \"Stellar UX Studio\",\n \"ja\": \"ステラ UX スタジオ\"\n },\n \"role\": {\n \"en\": \"Principal Accessibility Engineer\",\n \"ja\": \"プリンシパル・アクセシビリティエンジニア\"\n },\n \"description\": {\n \"en\": \"Lead the accessibility engineering practice across product surfaces, drive WCAG 2.2 conformance reviews, and mentor a team of five engineers.\",\n \"ja\": \"プロダクト全体のアクセシビリティ設計を統括。WCAG 2.2 適合レビューを主導し、5 名のエンジニアをメンタリング。\"\n },\n \"startDate\": \"2023-04\",\n \"endDate\": null,\n \"isCurrent\": true,\n \"url\": \"https://example.com/stellar\",\n \"order\": 0\n },\n {\n \"id\": \"harbor-labs\",\n \"organization\": {\n \"en\": \"Harbor Labs\",\n \"ja\": \"ハーバーラボ\"\n },\n \"role\": {\n \"en\": \"Senior Frontend Engineer\",\n \"ja\": \"シニアフロントエンドエンジニア\"\n },\n \"description\": {\n \"en\": \"Built the design system foundation and an accessible component library used across nine internal products.\",\n \"ja\": \"デザインシステム基盤と、社内 9 プロダクトで利用されるアクセシブルなコンポーネントライブラリを設計。\"\n },\n \"startDate\": \"2019-06\",\n \"endDate\": \"2023-03\",\n \"order\": 1\n }\n ],\n \"projects\": [\n {\n \"id\": \"axe-helpers\",\n \"title\": {\n \"en\": \"axe-helpers\",\n \"ja\": \"axe-helpers\"\n },\n \"role\": {\n \"en\": \"Author & maintainer\",\n \"ja\": \"作者・メンテナ\"\n },\n \"description\": {\n \"en\": \"A tiny set of utilities that wraps axe-core for use in component-level integration tests.\",\n \"ja\": \"コンポーネント単位の統合テスト向けに axe-core をラップする小規模ユーティリティ集。\"\n },\n \"url\": \"https://example.com/axe-helpers\",\n \"tags\": [\"accessibility\", \"testing\", \"typescript\"],\n \"relatedCareerId\": \"stellar-ux\",\n \"startDate\": \"2023-09\",\n \"highlighted\": true,\n \"order\": 0\n },\n {\n \"id\": \"color-contrast-cli\",\n \"title\": {\n \"en\": \"color-contrast-cli\",\n \"ja\": \"color-contrast-cli\"\n },\n \"description\": {\n \"en\": \"Command-line tool that audits design tokens for WCAG contrast ratios.\",\n \"ja\": \"デザイントークンの WCAG コントラスト比を監査するコマンドラインツール。\"\n },\n \"url\": \"https://example.com/color-contrast-cli\",\n \"tags\": [\"accessibility\", \"cli\", \"design-tokens\"],\n \"startDate\": \"2021-02\",\n \"endDate\": \"2022-08\",\n \"order\": 1\n },\n {\n \"id\": \"meetup-talks\",\n \"title\": {\n \"en\": \"Local meetup talks\",\n \"ja\": \"地域コミュニティ登壇\"\n },\n \"order\": 2\n }\n ],\n \"skills\": [\n { \"id\": \"typescript\", \"label\": \"TypeScript\", \"category\": \"programming\", \"order\": 0 },\n { \"id\": \"react\", \"label\": \"React\", \"category\": \"programming\", \"order\": 1 },\n { \"id\": \"wcag-2-2\", \"label\": \"WCAG 2.2\", \"category\": \"design\", \"order\": 2 },\n { \"id\": \"aria\", \"label\": \"ARIA\", \"category\": \"design\", \"order\": 3 },\n { \"id\": \"storybook\", \"label\": \"Storybook\", \"category\": \"programming\", \"order\": 4 },\n { \"id\": \"playwright\", \"label\": \"Playwright\", \"category\": \"programming\", \"order\": 5 },\n {\n \"id\": \"design-tokens\",\n \"label\": { \"en\": \"Design tokens\", \"ja\": \"デザイントークン\" },\n \"category\": \"design\",\n \"order\": 6\n },\n { \"id\": \"portuguese\", \"label\": \"Portuguese (B2)\", \"category\": \"language\", \"order\": 7 }\n ],\n \"certifications\": [\n {\n \"id\": \"iaap-cpacc-2022\",\n \"title\": {\n \"en\": \"Certified Professional in Accessibility Core Competencies (CPACC)\",\n \"ja\": \"アクセシビリティ専門家認定 (CPACC)\"\n },\n \"issuingOrganization\": {\n \"en\": \"International Association of Accessibility Professionals\",\n \"ja\": \"国際アクセシビリティ専門家協会\"\n },\n \"issueDate\": \"2022-06\",\n \"expirationDate\": null,\n \"credentialId\": \"CPACC-2022-PR-09231\",\n \"url\": \"https://example.org/iaap/credentials/cpacc\",\n \"order\": 0\n },\n {\n \"id\": \"iaap-was-2023\",\n \"title\": {\n \"en\": \"Web Accessibility Specialist (WAS)\",\n \"ja\": \"Web アクセシビリティスペシャリスト (WAS)\"\n },\n \"issuingOrganization\": {\n \"en\": \"International Association of Accessibility Professionals\",\n \"ja\": \"国際アクセシビリティ専門家協会\"\n },\n \"issueDate\": \"2023-03\",\n \"expirationDate\": \"2026-03\",\n \"credentialId\": \"WAS-2023-PR-04522\",\n \"url\": \"https://example.org/iaap/credentials/was\",\n \"order\": 1\n }\n ],\n \"memberships\": [\n {\n \"id\": \"iaap-senior\",\n \"organization\": {\n \"en\": \"International Association of Accessibility Professionals\",\n \"ja\": \"国際アクセシビリティ専門家協会\"\n },\n \"role\": {\n \"en\": \"Senior Member\",\n \"ja\": \"シニアメンバー\"\n },\n \"description\": {\n \"en\": \"Active participant in the Web Accessibility special interest group, contributing to monthly working sessions on WCAG 2.2 interpretation.\",\n \"ja\": \"Web Accessibility 専門部会に参加し、WCAG 2.2 解釈の月例セッションに貢献。\"\n },\n \"startDate\": \"2022-09\",\n \"endDate\": null,\n \"isCurrent\": true,\n \"url\": \"https://example.org/iaap\",\n \"order\": 0\n }\n ],\n \"volunteering\": [\n {\n \"id\": \"code-org-instructor\",\n \"organization\": {\n \"en\": \"Code.org\",\n \"ja\": \"Code.org\"\n },\n \"role\": {\n \"en\": \"Volunteer Instructor\",\n \"ja\": \"ボランティア講師\"\n },\n \"cause\": {\n \"en\": \"Education\",\n \"ja\": \"教育\"\n },\n \"description\": {\n \"en\": \"Taught introductory programming concepts to middle-school students in weekend workshops, with a focus on inclusive curriculum design.\",\n \"ja\": \"週末ワークショップで中学生にプログラミング入門を指導。インクルーシブなカリキュラム設計を重視。\"\n },\n \"startDate\": \"2021-09\",\n \"endDate\": \"2024-06\",\n \"isCurrent\": false,\n \"url\": \"https://example.org/code-org\",\n \"secondaryLink\": {\n \"url\": \"https://github.com/code-dot-org\",\n \"label\": {\n \"en\": \"GitHub organization\",\n \"ja\": \"GitHub Organization\"\n }\n },\n \"order\": 0\n }\n ],\n \"honors\": [\n {\n \"id\": \"wai-recognized-contributor-2024\",\n \"title\": {\n \"en\": \"Recognized Contributor, Web Accessibility Initiative\",\n \"ja\": \"Web Accessibility Initiative 貢献者表彰\"\n },\n \"issuer\": {\n \"en\": \"W3C Web Accessibility Initiative\",\n \"ja\": \"W3C Web Accessibility Initiative\"\n },\n \"description\": {\n \"en\": \"Recognized for sustained reviews of the WCAG 2.2 Authoring Practices documentation and for contributions to non-visual focus management patterns.\",\n \"ja\": \"WCAG 2.2 Authoring Practices ドキュメントの継続レビュー、および非視覚的フォーカス管理パターンへの貢献を評価。\"\n },\n \"date\": \"2024-04\",\n \"url\": \"https://example.org/wai/recognitions/2024\",\n \"order\": 0\n }\n ],\n \"education\": [\n {\n \"id\": \"westbrook-bsc-2018\",\n \"institution\": {\n \"en\": \"Westbrook University\",\n \"ja\": \"ウェストブルック大学\"\n },\n \"degree\": {\n \"en\": \"BSc in Cognitive Science\",\n \"ja\": \"認知科学学士\"\n },\n \"fieldOfStudy\": {\n \"en\": \"Human-Computer Interaction\",\n \"ja\": \"ヒューマン・コンピュータ・インタラクション\"\n },\n \"description\": {\n \"en\": \"Senior thesis on screen-reader interaction patterns for non-visual users navigating dynamic web interfaces.\",\n \"ja\": \"卒業研究は動的 Web インターフェースを操作する非視覚ユーザー向けスクリーンリーダー対話パターンの分析。\"\n },\n \"grade\": \"magna cum laude\",\n \"startDate\": \"2014-09\",\n \"endDate\": \"2018-06\",\n \"isCurrent\": false,\n \"url\": \"https://example.org/westbrook\",\n \"order\": 0\n }\n ],\n \"publications\": [\n {\n \"id\": \"design-tokens-wcag-2024\",\n \"title\": {\n \"en\": \"Auditing Design Tokens for WCAG 2.2 Conformance\",\n \"ja\": \"デザイントークンの WCAG 2.2 適合性監査\"\n },\n \"publisher\": {\n \"en\": \"ACM SIGACCESS\",\n \"ja\": \"ACM SIGACCESS\"\n },\n \"description\": {\n \"en\": \"A method for statically analyzing design-token files to surface color-contrast violations before they reach implementation.\",\n \"ja\": \"実装前にデザイントークンファイルを静的解析し、色コントラスト違反を発見する手法を提案。\"\n },\n \"date\": \"2024-03\",\n \"url\": \"https://example.org/papers/design-tokens-wcag\",\n \"doi\": \"10.1145/3678901.3678910\",\n \"coAuthors\": [\"Jamie Chen\", \"Sofia Almeida\"],\n \"order\": 0\n }\n ],\n \"languages\": [\n {\n \"id\": \"lang-en\",\n \"language\": \"en\",\n \"displayName\": {\n \"en\": \"English\",\n \"ja\": \"英語\"\n },\n \"proficiency\": \"native\",\n \"order\": 0\n },\n {\n \"id\": \"lang-pt\",\n \"language\": \"pt\",\n \"displayName\": {\n \"en\": \"Portuguese\",\n \"ja\": \"ポルトガル語\"\n },\n \"proficiency\": \"intermediate\",\n \"order\": 1\n }\n ],\n \"courses\": [\n {\n \"id\": \"coursera-advanced-a11y-2023\",\n \"title\": {\n \"en\": \"Advanced Web Accessibility Patterns\",\n \"ja\": \"Web アクセシビリティ応用パターン\"\n },\n \"provider\": {\n \"en\": \"Coursera (University of Michigan)\",\n \"ja\": \"Coursera (ミシガン大学)\"\n },\n \"courseNumber\": \"UMICH-A11Y-301\",\n \"description\": {\n \"en\": \"Covered ARIA authoring practices, accessible component patterns, and assistive technology testing workflows.\",\n \"ja\": \"ARIA オーサリングプラクティス、アクセシブルコンポーネントパターン、支援技術テストワークフローを扱う。\"\n },\n \"completionDate\": \"2023-11\",\n \"certificateUrl\": \"https://example.org/certificates/coursera/UMICH-A11Y-301\",\n \"order\": 0\n }\n ],\n \"patents\": [\n {\n \"id\": \"us-2024-a11y-test\",\n \"title\": {\n \"en\": \"Method and System for Automated Screen-Reader Output Verification\",\n \"ja\": \"スクリーンリーダー出力の自動検証手法およびシステム\"\n },\n \"patentNumber\": \"US 99,999,999 B2\",\n \"office\": \"USPTO\",\n \"status\": \"issued\",\n \"description\": {\n \"en\": \"An automated harness that captures screen-reader output and compares it against an authoring-time accessibility annotation.\",\n \"ja\": \"スクリーンリーダーの出力を自動的にキャプチャし、オーサリング時のアクセシビリティ注釈と照合する自動化ハーネス。\"\n },\n \"filingDate\": \"2022-04\",\n \"grantDate\": \"2024-03\",\n \"url\": \"https://patents.example.org/us-11987654\",\n \"coInventors\": [\"Jamie Chen\"],\n \"order\": 0\n },\n {\n \"id\": \"us-pending-focus\",\n \"title\": {\n \"en\": \"Predictive Focus Management for Single-Page Applications\",\n \"ja\": \"シングルページアプリケーション向け予測的フォーカス管理\"\n },\n \"patentNumber\": \"US 99/999,999\",\n \"office\": \"USPTO\",\n \"status\": \"pending\",\n \"description\": {\n \"en\": \"An algorithm that predicts the next likely focus target based on user navigation patterns to reduce focus loss in dynamic content.\",\n \"ja\": \"ユーザーのナビゲーションパターンから次のフォーカス候補を予測し、動的コンテンツにおけるフォーカス喪失を低減するアルゴリズム。\"\n },\n \"filingDate\": \"2024-08\",\n \"url\": \"https://patents.example.org/us-pending-focus\",\n \"order\": 1\n }\n ],\n \"testScores\": [\n {\n \"id\": \"celpe-bras-2023\",\n \"title\": {\n \"en\": \"CELPE-Bras (Brazilian Portuguese Proficiency)\",\n \"ja\": \"CELPE-Bras (ブラジルポルトガル語能力試験)\"\n },\n \"score\": \"Upper Intermediate\",\n \"date\": \"2023-05\",\n \"description\": {\n \"en\": \"Brazilian government certificate of proficiency in Portuguese as a foreign language.\",\n \"ja\": \"ブラジル政府認定の外国語としてのポルトガル語能力証明書。\"\n },\n \"url\": \"https://example.org/scores/celpe-bras-2023\",\n \"order\": 0\n },\n {\n \"id\": \"gre-general-2013\",\n \"title\": {\n \"en\": \"GRE General Test\",\n \"ja\": \"GRE 一般試験\"\n },\n \"score\": \"332 / 340\",\n \"date\": \"2013-10\",\n \"relatedEducationId\": \"westbrook-bsc-2018\",\n \"description\": {\n \"en\": \"Combined verbal and quantitative reasoning score submitted with the Westbrook University application.\",\n \"ja\": \"ウェストブルック大学出願時に提出した言語・数的推論の合計スコア。\"\n },\n \"url\": \"https://example.org/scores/gre-general-2013\",\n \"order\": 1\n }\n ],\n \"recommendations\": [\n {\n \"id\": \"rec-jordan-stellar\",\n \"body\": {\n \"en\": \"Pat is one of the most rigorous accessibility engineers I have worked with. They turned our design-token audit into a repeatable process and mentored the whole team on assistive-technology testing.\",\n \"ja\": \"Pat は私が共に働いた中で最も厳格なアクセシビリティエンジニアの一人です。デザイントークン監査を再現可能なプロセスに変え、支援技術テストについてチーム全体を指導してくれました。\"\n },\n \"author\": {\n \"name\": \"Jordan Avery\",\n \"headline\": {\n \"en\": \"Engineering Manager at Stellar UX\",\n \"ja\": \"Stellar UX エンジニアリングマネージャー\"\n },\n \"url\": \"https://example.org/in/jordan-avery\"\n },\n \"relationship\": {\n \"en\": \"Managed Pat directly at Stellar UX\",\n \"ja\": \"Stellar UX で Pat を直属で管理\"\n },\n \"date\": \"2023-09\",\n \"relatedCareerId\": \"stellar-ux\",\n \"order\": 0\n },\n {\n \"id\": \"rec-sofia-harbor\",\n \"body\": {\n \"en\": \"I collaborated with Pat on a screen-reader testing harness at Harbor Labs. Their attention to non-visual user journeys raised the bar for the entire engineering org.\",\n \"ja\": \"Harbor Labs でスクリーンリーダーのテストハーネスを Pat と共同開発しました。非視覚ユーザーの導線への配慮は、エンジニアリング組織全体の基準を引き上げました。\"\n },\n \"author\": {\n \"name\": \"Sofia Almeida\",\n \"headline\": {\n \"en\": \"Staff Engineer at Harbor Labs\",\n \"ja\": \"Harbor Labs スタッフエンジニア\"\n }\n },\n \"date\": \"2021-06\",\n \"relatedCareerId\": \"harbor-labs\",\n \"order\": 1\n }\n ],\n \"highlights\": [\n {\n \"id\": \"post-axe-helpers-launch\",\n \"platform\": \"github\",\n \"url\": \"https://example.com/pat/axe-helpers\",\n \"image\": \"/assets/highlights/axe-helpers.webp\",\n \"alt\": {\n \"en\": \"Terminal screenshot of axe-helpers running in a test suite.\",\n \"ja\": \"テストスイートで axe-helpers が動作するターミナルのスクリーンショット。\"\n },\n \"title\": {\n \"en\": \"axe-helpers hits 1.0\",\n \"ja\": \"axe-helpers が 1.0 に到達\"\n },\n \"description\": {\n \"en\": \"The accessibility test utilities are now stable, with docs and recipes.\",\n \"ja\": \"アクセシビリティテストユーティリティが安定版に。ドキュメントとレシピも整備。\"\n },\n \"postedAt\": \"2024-05-18\",\n \"tags\": [\"accessibility\", \"release\"],\n \"order\": 0\n },\n {\n \"id\": \"post-a11y-meetup-talk\",\n \"platform\": \"event\",\n \"url\": \"https://example.org/meetups/a11y-lisbon-2024\",\n \"image\": \"/assets/highlights/a11y-meetup.webp\",\n \"alt\": {\n \"en\": \"Pat presenting on stage at a local accessibility meetup.\",\n \"ja\": \"地域のアクセシビリティ勉強会で登壇する Pat。\"\n },\n \"title\": {\n \"en\": \"Talk: Testing for non-visual journeys\",\n \"ja\": \"登壇: 非視覚的な導線をテストする\"\n },\n \"postedAt\": \"2024-03-09\",\n \"order\": 1\n }\n ],\n \"contact\": {\n \"email\": \"pat@example.com\",\n \"showEmail\": false,\n \"formUrl\": \"https://example.com/pat/contact\"\n },\n \"settings\": {\n \"defaultLocale\": \"en\",\n \"fallbackLocale\": \"en\",\n \"availableLocales\": [\"en\", \"ja\"],\n \"theme\": \"default\",\n \"showPoweredBy\": true,\n \"enableJsonLd\": true,\n \"enableApi\": true,\n \"enableAnalytics\": false,\n \"skillCategories\": [\n { \"id\": \"programming\", \"label\": { \"en\": \"Programming\", \"ja\": \"プログラミング\" } },\n {\n \"id\": \"design\",\n \"label\": { \"en\": \"Design & accessibility\", \"ja\": \"デザイン・アクセシビリティ\" }\n },\n { \"id\": \"language\", \"label\": { \"en\": \"Languages\", \"ja\": \"言語\" } }\n ],\n \"sectionOrder\": [\"about\", \"projects\", \"careers\", \"skills\"],\n \"sectionLabels\": {\n \"projects\": { \"en\": \"Selected work\", \"ja\": \"主な制作\" },\n \"volunteering\": { \"en\": \"Open-source contributions\", \"ja\": \"OSS コントリビューション\" }\n },\n \"highlightsIntro\": {\n \"en\": \"A few recent posts and talks.\",\n \"ja\": \"最近の投稿と登壇をいくつか。\"\n }\n },\n \"meta\": {\n \"createdAt\": \"2026-01-15T09:00:00Z\",\n \"updatedAt\": \"2026-05-12T08:30:00Z\",\n \"generator\": \"Takuhon\",\n \"contentLicense\": {\n \"spdxId\": \"CC-BY-4.0\",\n \"url\": \"https://creativecommons.org/licenses/by/4.0/\",\n \"attribution\": {\n \"name\": \"Pat Rivera\",\n \"url\": \"https://example.com/pat\"\n }\n },\n \"privacy\": {\n \"hideCredentialIds\": true,\n \"hideEducationGrades\": true\n }\n }\n}\n","/**\n * Read-only activity-badge endpoint (`GET`/`HEAD /activity.svg`) for the\n * Cloudflare adapter.\n *\n * Renders the synced developer-activity snapshot as a self-contained SVG card,\n * suitable for embedding as an image in a GitHub profile README (served through\n * the Camo image proxy) or anywhere an `<img>` can point at a public URL. The\n * markup is the same `renderActivitySvg` output the profile page embeds, so the\n * badge never drifts from the in-page dashboard.\n *\n * Public and unauthenticated, at parity with `GET /api/activity`: the owner's\n * opt-in (`settings.activity.enabled`) is re-checked on every request, so\n * disabling the feature 404s the badge immediately even while a stale snapshot\n * is still stored. `?theme=dark` selects the dark palette; anything else\n * (including no query) renders the default light card.\n *\n * The card carries its own opaque background, so it stays legible on a dark\n * README. The response is `nosniff` `image/svg+xml` with a ~4h public cache\n * policy — the badge is fronted by GitHub's Camo cache, so the origin is hit\n * rarely.\n */\n\nimport {\n DARK_PALETTE,\n LIGHT_PALETTE,\n NotFoundError,\n renderActivitySvg,\n type Takuhon,\n} from '@takuhon/core';\n\nimport { KvActivityStorage } from './kv-activity-storage.js';\nimport { KvTakuhonStorage } from './kv-storage.js';\n\n/**\n * ~4 hours, matching github-readme-stats' default. README badges are cached at\n * Camo, so a longer TTL keeps origin traffic minimal at the cost of a bounded\n * delay before a fresh sync shows up.\n */\nexport const ACTIVITY_SVG_CACHE_CONTROL = 'public, max-age=14400';\n\n/**\n * Render `GET`/`HEAD /activity.svg` against the KV-backed profile and snapshot.\n * Builds its own storage wrappers from `kv` (no shared state) and falls back to\n * the bundled profile when KV is empty, exactly as the public API does.\n */\nexport async function serveActivitySvg(\n request: Request,\n kv: KVNamespace,\n fallback: () => Takuhon,\n): Promise<Response> {\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n return new Response('Method Not Allowed', { status: 405 });\n }\n\n const notFound = (): Response => new Response('Not Found', { status: 404 });\n\n // Re-check the owner's opt-in on every request (parity with GET /api/activity):\n // disabling settings.activity must 404 the badge immediately, even while a\n // stale snapshot is still stored.\n let profile: Takuhon;\n try {\n profile = (await new KvTakuhonStorage(kv).getProfile()).data;\n } catch (e) {\n // KV is empty before the first admin write; serve the bundled profile.\n if (e instanceof NotFoundError) profile = fallback();\n else throw e;\n }\n if (profile.settings.activity?.enabled !== true) return notFound();\n\n const snapshot = await new KvActivityStorage(kv).getActivitySnapshot();\n if (snapshot === null) return notFound();\n\n const palette =\n new URL(request.url).searchParams.get('theme') === 'dark' ? DARK_PALETTE : LIGHT_PALETTE;\n const svg = renderActivitySvg(snapshot, { palette });\n // An opted-in but metric-less snapshot renders to ''. There is no image to\n // serve, so treat it like a missing snapshot rather than emit an empty badge.\n if (svg === '') return notFound();\n\n return new Response(request.method === 'HEAD' ? null : svg, {\n status: 200,\n headers: {\n 'content-type': 'image/svg+xml; charset=utf-8',\n 'cache-control': ACTIVITY_SVG_CACHE_CONTROL,\n // Force nosniff so the SVG can't be reinterpreted as another type\n // (security.md §4.7), matching the /assets/* delivery proxy.\n 'x-content-type-options': 'nosniff',\n },\n });\n}\n","import { isActivitySnapshot, type ActivitySnapshot, type ActivityStorage } from '@takuhon/core';\n\n/**\n * KV key holding the synced developer-activity snapshot. A second key in the\n * same namespace as the profile (`TAKUHON_DATA`): the snapshot is a sibling\n * document, machine-written by the scheduled sync and deliberately kept out of\n * the canonical profile document.\n */\nexport const ACTIVITY_KV_KEY = 'TAKUHON_ACTIVITY';\n\n/**\n * Cloudflare KV implementation of the `ActivityStorage` contract.\n *\n * Reads are forgiving: an absent, unparseable, or malformed value resolves to\n * `null` (never throws), because a missing snapshot is the normal pre-sync /\n * opt-out state and the renderer simply omits the activity section. Writes are\n * last-writer-wins with no version metadata — the snapshot carries its own\n * `lastSyncedAt`, and the only writer is the scheduled sync.\n */\nexport class KvActivityStorage implements ActivityStorage {\n constructor(private readonly kv: KVNamespace) {}\n\n async getActivitySnapshot(): Promise<ActivitySnapshot | null> {\n const raw = await this.kv.get(ACTIVITY_KV_KEY, 'text');\n if (raw === null) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // A corrupt snapshot is treated as absent, not fatal: the next sync\n // rewrites it whole, and the renderer omits the section meanwhile.\n return null;\n }\n return isActivitySnapshot(parsed) ? parsed : null;\n }\n\n async saveActivitySnapshot(snapshot: ActivitySnapshot): Promise<void> {\n await this.kv.put(ACTIVITY_KV_KEY, JSON.stringify(snapshot));\n }\n}\n","import { ConflictError, NotFoundError, type Takuhon, type TakuhonStorage } from '@takuhon/core';\n\nexport const KV_KEY = 'TAKUHON_DATA';\n\nexport interface KvMetadata {\n version: string;\n updatedAt: string;\n}\n\n/**\n * Cloudflare KV implementation of the `TakuhonStorage` contract. Stores the\n * profile document as JSON under a single key (`TAKUHON_DATA`) and tracks the\n * optimistic-locking token inside KV value metadata.\n *\n * `version` is a fresh UUIDv4 on every successful write. Callers compare it\n * verbatim against the `If-Match` precondition; mismatches raise\n * `ConflictError` with `currentVersion` so the API layer can build the RFC\n * 7807 envelope without an extra round trip.\n */\nexport class KvTakuhonStorage implements TakuhonStorage {\n constructor(private readonly kv: KVNamespace) {}\n\n async getProfile(): Promise<{ data: Takuhon; version: string }> {\n const result = await this.kv.getWithMetadata<Takuhon, KvMetadata>(KV_KEY, 'json');\n if (result.value === null || !result.metadata?.version) {\n throw new NotFoundError(`No profile is stored at KV key \"${KV_KEY}\".`);\n }\n return { data: result.value, version: result.metadata.version };\n }\n\n async saveProfile(data: Takuhon, ifMatch?: string): Promise<{ version: string }> {\n if (ifMatch !== undefined) {\n const current = await this.kv.getWithMetadata<Takuhon, KvMetadata>(KV_KEY, 'json');\n const currentVersion = current.metadata?.version;\n if (currentVersion !== ifMatch) {\n throw new ConflictError(\n `If-Match preconditioned on version \"${ifMatch}\" but current is \"${currentVersion ?? 'absent'}\".`,\n { currentVersion },\n );\n }\n }\n const version = crypto.randomUUID();\n const updatedAt = new Date().toISOString();\n await this.kv.put(KV_KEY, JSON.stringify(data), {\n metadata: { version, updatedAt } satisfies KvMetadata,\n });\n return { version };\n }\n\n async deleteProfile(): Promise<void> {\n await this.kv.delete(KV_KEY);\n }\n}\n","/**\n * Scheduled developer-activity sync for the Cloudflare worker.\n *\n * The cron-driven counterpart of the CLI's `takuhon activity sync`: it reads\n * `settings.activity` from the stored profile, fetches the configured GitHub /\n * WakaTime metrics through `@takuhon/activity`, and persists the snapshot via\n * an `ActivityStorage` — so public rendering stays a static read.\n *\n * The function reports its outcome instead of throwing wherever it can, and a\n * sync that gathers no data never overwrites a good snapshot: the last-known\n * one is kept and the result says so, letting the `scheduled` handler log the\n * failure without failing the cron run.\n */\n\nimport { fetchActivitySnapshot, isEmptySnapshot, type ActivitySecrets } from '@takuhon/activity';\nimport {\n NotFoundError,\n type ActivityStorage,\n type Takuhon,\n type TakuhonStorage,\n} from '@takuhon/core';\n\nexport interface ActivitySyncOptions {\n /** Source of the profile whose `settings.activity` configures the sync. */\n profileStorage: TakuhonStorage;\n /** Destination for the synced snapshot. */\n activityStorage: ActivityStorage;\n /** Secrets from the Worker env; never persisted. */\n secrets: ActivitySecrets;\n /** Fallback profile used when storage has none (same as the public app). */\n fallback?: () => Takuhon;\n /** HTTP client override for tests. Defaults to the global `fetch`. */\n fetch?: typeof fetch;\n /** Clock for `lastSyncedAt`; injectable for deterministic tests. */\n now?: () => Date;\n}\n\n/** A per-source fetch failure, with any secret value already redacted. */\nexport interface ActivitySourceFailure {\n source: string;\n message: string;\n}\n\nexport interface ActivitySyncResult {\n /**\n * `synced` — a snapshot was gathered and stored. `skipped` — activity is not\n * configured / enabled, so nothing was attempted. `empty` — every configured\n * source came back empty or failing; the last-known snapshot was kept.\n */\n status: 'synced' | 'skipped' | 'empty';\n /** Why a `skipped` / `empty` run did not store anything. */\n reason?: string;\n /** Per-source failures reported by the fetcher (may be non-empty on `synced`). */\n failures: ActivitySourceFailure[];\n}\n\n/** Remove any literal occurrence of the secrets from a string before it is logged. */\nfunction redactSecrets(text: string, secrets: ActivitySecrets): string {\n let out = text;\n for (const secret of [secrets.githubToken, secrets.wakatimeKey]) {\n if (secret !== undefined && secret !== '') out = out.split(secret).join('***');\n }\n return out;\n}\n\n/**\n * Run one activity sync. Resolves with a result for every configuration state;\n * it only rejects if the storage layer itself fails (the `scheduled` handler\n * catches that, so a broken sync never fails the cron run).\n */\nexport async function syncActivity(opts: ActivitySyncOptions): Promise<ActivitySyncResult> {\n let profile: Takuhon;\n try {\n profile = (await opts.profileStorage.getProfile()).data;\n } catch (err) {\n if (err instanceof NotFoundError && opts.fallback) {\n profile = opts.fallback();\n } else if (err instanceof NotFoundError) {\n return { status: 'skipped', reason: 'no stored profile', failures: [] };\n } else {\n throw err;\n }\n }\n\n const config = profile.settings.activity;\n if (config?.enabled !== true) {\n return {\n status: 'skipped',\n reason: 'activity is not enabled in settings.activity',\n failures: [],\n };\n }\n const hasGithub = config.github?.username !== undefined && config.github.username !== '';\n const hasWakatime = config.wakatime?.username !== undefined && config.wakatime.username !== '';\n if (!hasGithub && !hasWakatime) {\n return { status: 'skipped', reason: 'no github or wakatime username configured', failures: [] };\n }\n\n const failures: ActivitySourceFailure[] = [];\n const snapshot = await fetchActivitySnapshot(config, opts.secrets, {\n fetch: opts.fetch,\n now: opts.now,\n onError: (source, err) => {\n const detail = err instanceof Error ? err.message : String(err);\n failures.push({ source, message: redactSecrets(detail, opts.secrets) });\n },\n });\n\n if (isEmptySnapshot(snapshot)) {\n // Never replace a good snapshot with an empty one (design §6): keep the\n // last-known document and report the dry run.\n return {\n status: 'empty',\n reason: 'the sync gathered no activity data; kept the last-known snapshot',\n failures,\n };\n }\n\n await opts.activityStorage.saveActivitySnapshot(snapshot);\n return { status: 'synced', failures };\n}\n","import type { CachePurger } from '@takuhon/api';\n\nexport interface CloudflareCachePurgerOptions {\n /**\n * Absolute origin (e.g. `https://example.com`) used to build the URLs\n * passed to `Cache.delete`. The Worker derives this from the incoming\n * request's URL so the same code works under any production hostname.\n */\n origin: string;\n /**\n * Locale codes to include when purging language-keyed cache entries.\n * Cloudflare caches `?lang=` query variants as distinct keys; we purge\n * a representative set on every write. Adapters can extend the list to\n * cover other locales the deploy serves.\n */\n langs?: string[];\n}\n\n/**\n * `CachePurger` backed by Cloudflare's colo-local `caches.default`.\n *\n * Limitations (documented in the adapter README):\n * - Cloudflare's `Cache.delete` clears the current colo only, not the\n * entire edge. Other colos honour the response's `Cache-Control`\n * `s-maxage` (5 minutes today) before refreshing.\n * - Truly global invalidation requires the REST `/purge_cache` API,\n * which needs a zone-scoped token; that's deferred to a later phase.\n */\nexport class CloudflareCachePurger implements CachePurger {\n private readonly origin: string;\n private readonly langs: string[];\n\n /**\n * `getCache` is a thunk so the Workers-only `caches` global is touched\n * lazily — public-only requests on this Worker never run admin handlers\n * and must not pay (or fail under Node tests) for the lookup.\n */\n constructor(\n private readonly getCache: () => Cache,\n opts: CloudflareCachePurgerOptions,\n ) {\n this.origin = opts.origin.replace(/\\/$/, '');\n this.langs = opts.langs ?? ['en', 'ja'];\n }\n\n async profileUpdated(): Promise<void> {\n await this.purge();\n }\n\n async profileDeleted(): Promise<void> {\n await this.purge();\n }\n\n private async purge(): Promise<void> {\n const cache = this.getCache();\n const targets = ['/', '/api/profile', '/api/jsonld', '/takuhon.json'];\n for (const lang of this.langs) {\n const q = `?lang=${encodeURIComponent(lang)}`;\n targets.push(`/api/profile${q}`, `/api/jsonld${q}`);\n }\n for (const path of targets) {\n await cache.delete(new Request(this.origin + path), { ignoreMethod: true });\n }\n }\n}\n","import type { AuditEvent, AuditLogger } from '@takuhon/api';\n\n/**\n * `AuditLogger` that writes a single line of JSON per event to `console.log`.\n *\n * Cloudflare captures these via Workers Tail / Logpush, where they can be\n * routed to R2, S3, or any downstream SIEM. Token bodies never reach this\n * sink — the upstream middleware only emits `sha256:<hex>` digests in\n * `actor.tokenHash`.\n */\nexport const consoleAuditLogger: AuditLogger = (event: AuditEvent): void => {\n console.log(JSON.stringify(event));\n};\n","/**\n * Cloudflare implementations of the `@takuhon/contact` adapter seams.\n *\n * Two pieces — a Turnstile {@link ChallengeVerifier} and a `send_email`-binding\n * {@link EmailTransport} — are the only Cloudflare-specific parts of the contact\n * feature. Everything else (validation, the request pipeline) lives in the\n * portable `@takuhon/contact` core, so the feature can be re-homed on another\n * host by swapping just these.\n */\n\nimport type { ChallengeVerifier, EmailTransport, Inquiry } from '@takuhon/contact';\n\nconst TURNSTILE_SITEVERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';\n\n/** Options for {@link createTurnstileVerifier}. */\nexport interface TurnstileVerifierOptions {\n /** Override the siteverify endpoint (for tests). */\n endpoint?: string;\n /** Injected `fetch` (for tests). Defaults to the global `fetch`. */\n fetch?: typeof fetch;\n}\n\n/**\n * Create a {@link ChallengeVerifier} backed by Cloudflare Turnstile's\n * server-side `siteverify` endpoint. The secret is used server-side only and is\n * never exposed to the client.\n */\nexport function createTurnstileVerifier(\n secret: string,\n options?: TurnstileVerifierOptions,\n): ChallengeVerifier {\n const endpoint = options?.endpoint ?? TURNSTILE_SITEVERIFY_URL;\n // Always invoke fetch as a free function (never as a method) so the runtime\n // never sees a rebound `this` — cf. the @takuhon/activity illegal-invocation fix.\n const doFetch: typeof fetch = options?.fetch ?? ((input, init) => fetch(input, init));\n\n return {\n async verify(token, context) {\n if (!token) return false;\n const form = new FormData();\n form.append('secret', secret);\n form.append('response', token);\n if (context?.remoteIp) form.append('remoteip', context.remoteIp);\n\n let response: Response;\n try {\n response = await doFetch(endpoint, { method: 'POST', body: form });\n } catch {\n return false;\n }\n if (!response.ok) return false;\n\n const outcome = (await response.json().catch(() => null)) as { success?: boolean } | null;\n return outcome?.success === true;\n },\n };\n}\n\n/**\n * Minimal structural shape of the Cloudflare `send_email` binding we depend on\n * (the object-form `send`). Declared here rather than imported so the adapter\n * does not pin a specific `@cloudflare/workers-types` shape; the real binding\n * satisfies it structurally.\n */\nexport interface SendEmailBinding {\n send(message: {\n to: string | string[];\n from: { email: string; name?: string };\n replyTo?: string;\n subject: string;\n text?: string;\n html?: string;\n }): Promise<unknown>;\n}\n\n/** Configuration for {@link createSendEmailTransport}. */\nexport interface SendEmailTransportConfig {\n /** Verified destination address — the site owner's inbox. */\n to: string;\n /** From address; must be on a domain you control (e.g. `noreply@tak3.jp`). */\n from: { email: string; name?: string };\n /** Optional subject prefix, e.g. `\"[me.tak3.jp contact]\"`. */\n subjectPrefix?: string;\n}\n\n/** A built notification email. */\nexport interface InquiryEmail {\n subject: string;\n text: string;\n html: string;\n}\n\nconst HTML_ESCAPES: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n};\n\nfunction escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => HTML_ESCAPES[char] ?? char);\n}\n\n/** Strip CR/LF so a value is safe to use in an email header (the subject). */\nfunction sanitizeHeader(value: string): string {\n return value.replace(/[\\r\\n]+/g, ' ').trim();\n}\n\n/**\n * Build the owner-facing notification email for an inquiry: a plain-text body\n * plus an HTML body with every untrusted field escaped. The visitor's message\n * is rendered as text and never interpolated into HTML unescaped.\n */\nexport function buildInquiryEmail(\n inquiry: Inquiry,\n config: Pick<SendEmailTransportConfig, 'subjectPrefix'> = {},\n): InquiryEmail {\n const prefix = config.subjectPrefix?.trim();\n const subject = sanitizeHeader(\n prefix ? `${prefix} ${inquiry.email}` : `Contact from ${inquiry.email}`,\n );\n\n const fields: [string, string | undefined][] = [\n ['From', inquiry.email],\n ['Locale', inquiry.locale],\n ['Page', inquiry.pageUrl],\n ['User-Agent', inquiry.userAgent],\n ['Country', inquiry.meta?.country],\n ['IP', inquiry.meta?.ip],\n ['Received', inquiry.meta?.receivedAt],\n ];\n const present = fields.filter((entry): entry is [string, string] => entry[1] !== undefined);\n\n const text = [\n inquiry.message,\n '',\n '—',\n ...present.map(([key, value]) => `${key}: ${value}`),\n ].join('\\n');\n\n const html = [\n `<p style=\"white-space:pre-wrap;margin:0 0 1em\">${escapeHtml(inquiry.message)}</p>`,\n '<hr>',\n '<p style=\"color:#555;font-size:13px;line-height:1.6\">',\n present.map(([key, value]) => `${escapeHtml(key)}: ${escapeHtml(value)}`).join('<br>'),\n '</p>',\n ].join('');\n\n return { subject, text, html };\n}\n\n/**\n * Create an {@link EmailTransport} that delivers an inquiry to the owner's inbox\n * via the Cloudflare `send_email` binding. `Reply-To` is set to the visitor's\n * address so the owner can reply straight from their normal mail app.\n */\nexport function createSendEmailTransport(\n binding: SendEmailBinding,\n config: SendEmailTransportConfig,\n): EmailTransport {\n return {\n async send(inquiry) {\n const mail = buildInquiryEmail(inquiry, config);\n await binding.send({\n to: config.to,\n from: config.from,\n replyTo: inquiry.email,\n subject: mail.subject,\n text: mail.text,\n html: mail.html,\n });\n },\n };\n}\n","/**\n * Read-only MCP endpoint (`GET`/`POST /mcp`) for the Cloudflare adapter.\n *\n * The deployed profile is exposed over the Model Context Protocol so any MCP\n * client can read it — the remote counterpart of the CLI's `takuhon mcp`. It\n * reuses the transport-agnostic server from `@takuhon/mcp` (the same core\n * catalog the CLI serves) and connects it to the SDK's **Web Standard**\n * Streamable HTTP transport, which speaks `fetch` `Request`/`Response` and runs\n * on Workers.\n *\n * Stateless by design — no Durable Object, no new binding, no session: each\n * request builds a fresh server + transport (the SDK requires a fresh transport\n * per request in stateless mode, and skips session validation), reads the\n * profile from the existing KV, and answers. `enableJsonResponse` returns a\n * single JSON body instead of an SSE stream, which suits read-only\n * request/response with no server-initiated messages.\n *\n * Unauthenticated public read, at parity with `GET /api/profile`: every answer\n * is privacy-filtered by core and no admin/write surface is exposed.\n */\n\nimport { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';\nimport { NotFoundError, type Takuhon } from '@takuhon/core';\nimport { createTakuhonMcpServer } from '@takuhon/mcp';\n\nimport { KvTakuhonStorage } from './kv-storage.js';\n\nconst MCP_SERVER_NAME = 'takuhon';\n\n/**\n * Handle one MCP request against the KV-backed profile. Builds a fresh,\n * stateless server + transport per request and returns its `Response` with\n * `nosniff` and a no-store cache policy layered on.\n */\nexport async function serveMcp(\n request: Request,\n kv: KVNamespace,\n fallback: () => Takuhon,\n): Promise<Response> {\n const storage = new KvTakuhonStorage(kv);\n const loadProfile = async (): Promise<Takuhon> => {\n try {\n return (await storage.getProfile()).data;\n } catch (e) {\n // Before the first admin write KV is empty; serve the bundled fixture,\n // exactly as the public API's loadProfile does.\n if (e instanceof NotFoundError) return fallback();\n throw e;\n }\n };\n\n const server = createTakuhonMcpServer({ loadProfile, name: MCP_SERVER_NAME });\n // Stateless: `sessionIdGenerator: undefined` disables sessions, and the SDK\n // requires a fresh transport per request in this mode (it rejects reuse to\n // avoid cross-client message-id collisions). `enableJsonResponse` returns a\n // buffered JSON body rather than an SSE stream.\n const transport = new WebStandardStreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n await server.connect(transport);\n\n // The server / transport are per-request locals with no cross-request state,\n // so the Worker can let them be collected when the request ends.\n const response = await transport.handleRequest(request);\n\n const headers = new Headers(response.headers);\n headers.set('x-content-type-options', 'nosniff');\n if (!headers.has('cache-control')) headers.set('cache-control', 'no-store');\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n","import {\n ACCEPTED_IMAGE_MIME_TYPES,\n detectImageMime,\n IMAGE_EXTENSIONS,\n NotFoundError,\n readImageInfo,\n type AcceptedImageMime,\n type AssetOptions,\n type AssetRecord,\n type TakuhonAssetStorage,\n} from '@takuhon/core';\n\n/** Key prefix every uploaded asset lives under (`security.md` §4.6). */\nconst ASSET_KEY_PREFIX = 'assets/';\n\n/** Extension used when the bytes are not one of the accepted image types. */\nconst DEFAULT_EXTENSION = 'bin';\n\n/**\n * Long-lived immutable cache policy for delivered assets. Object keys embed a\n * timestamp and a random hash, so a given key never names different bytes —\n * the response can be cached forever.\n */\nexport const ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable';\n\n/** Narrow an arbitrary content-type string to an accepted image MIME, or null. */\nfunction asAcceptedMime(value: string | undefined): AcceptedImageMime | null {\n if (value === undefined) return null;\n return (ACCEPTED_IMAGE_MIME_TYPES as readonly string[]).includes(value)\n ? (value as AcceptedImageMime)\n : null;\n}\n\n/** Seconds since the Unix epoch, used as the leading object-key component. */\nfunction timestampSeconds(): number {\n return Math.floor(Date.now() / 1000);\n}\n\n/**\n * Four hex characters of cryptographic randomness. Combined with the timestamp\n * this defeats key enumeration (`security.md` §4.7) without an idempotency\n * guarantee — every upload gets a fresh key.\n */\nfunction shortHash(): string {\n const bytes = new Uint8Array(2);\n crypto.getRandomValues(bytes);\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n}\n\n/**\n * Cloudflare R2 implementation of the {@link TakuhonAssetStorage} contract.\n *\n * The bytes arrive already validated and metadata-stripped by `@takuhon/api`'s\n * admin app (magic-byte check, size / dimension / frame limits, EXIF removal),\n * so this adapter only persists them and mints the public URL. Object keys\n * follow `assets/{timestamp}-{shortHash}.{ext}` (`security.md` §4.6); the file\n * extension comes from `@takuhon/core`'s {@link IMAGE_EXTENSIONS}.\n *\n * Dimensions are read back from the container header via\n * {@link readImageInfo} (no pixel decode) so the returned {@link AssetRecord}\n * is complete; this is the same header-only parse the API layer already\n * performed and matches the deliberate \"no codec\" approach.\n *\n * Assets are served by the Worker's `GET /assets/*` proxy route rather than a\n * public R2 bucket, so `publicUrl` is an absolute URL on the Worker's own\n * origin when `publicBaseUrl` is supplied, falling back to the relative `url`\n * otherwise.\n */\nexport class R2TakuhonAssetStorage implements TakuhonAssetStorage {\n private readonly bucket: R2Bucket;\n private readonly publicBaseUrl: string;\n\n constructor(bucket: R2Bucket, options: { publicBaseUrl?: string } = {}) {\n this.bucket = bucket;\n this.publicBaseUrl = options.publicBaseUrl ?? '';\n }\n\n async putAsset(file: File | Blob, options?: AssetOptions): Promise<AssetRecord> {\n const bytes = new Uint8Array(await file.arrayBuffer());\n // Trust the API-validated content-type when present; otherwise authenticate\n // from the bytes so the adapter is self-sufficient for direct callers.\n const mime = asAcceptedMime(options?.contentType) ?? detectImageMime(bytes);\n const ext = mime !== null ? IMAGE_EXTENSIONS[mime] : DEFAULT_EXTENSION;\n const contentType = mime ?? options?.contentType ?? file.type ?? 'application/octet-stream';\n\n const key = `${ASSET_KEY_PREFIX}${String(timestampSeconds())}-${shortHash()}.${ext}`;\n await this.bucket.put(key, bytes, { httpMetadata: { contentType } });\n\n const info = mime !== null ? readImageInfo(bytes, mime) : null;\n const url = `/${key}`;\n return {\n id: key,\n url,\n publicUrl: this.absoluteUrl(url),\n mimeType: contentType,\n size: bytes.length,\n width: info?.width,\n height: info?.height,\n createdAt: new Date().toISOString(),\n };\n }\n\n async getPublicUrl(assetId: string): Promise<string> {\n const head = await this.bucket.head(assetId);\n if (head === null) throw new NotFoundError(`No asset is stored at R2 key \"${assetId}\".`);\n return this.absoluteUrl(`/${assetId}`);\n }\n\n async deleteAsset(assetId: string): Promise<void> {\n // R2 delete is idempotent: deleting an absent key is a no-op, matching the\n // contract.\n await this.bucket.delete(assetId);\n }\n\n async listAssets(): Promise<AssetRecord[]> {\n const records: AssetRecord[] = [];\n let cursor: string | undefined;\n do {\n const page = await this.bucket.list({ prefix: ASSET_KEY_PREFIX, cursor });\n for (const object of page.objects) {\n const url = `/${object.key}`;\n records.push({\n id: object.key,\n url,\n publicUrl: this.absoluteUrl(url),\n mimeType: object.httpMetadata?.contentType ?? 'application/octet-stream',\n size: object.size,\n createdAt: object.uploaded.toISOString(),\n });\n }\n cursor = page.truncated ? page.cursor : undefined;\n } while (cursor !== undefined);\n return records;\n }\n\n private absoluteUrl(path: string): string {\n return this.publicBaseUrl !== '' ? `${this.publicBaseUrl}${path}` : path;\n }\n}\n"],"mappings":"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","names":["NotFoundError","NotFoundError","NotFoundError","NotFoundError","NotFoundError","NotFoundError"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takuhon/cloudflare",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Cloudflare Workers adapter for takuhon — public API, Workers Assets, KV, R2 integrations",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Takuhon contributors",
@@ -46,11 +46,11 @@
46
46
  "dependencies": {
47
47
  "@modelcontextprotocol/sdk": "^1.29.0",
48
48
  "hono": "^4.12.21",
49
- "@takuhon/api": "1.3.0",
50
- "@takuhon/contact": "1.3.0",
51
- "@takuhon/core": "1.3.0",
52
- "@takuhon/activity": "1.3.0",
53
- "@takuhon/mcp": "1.3.0"
49
+ "@takuhon/activity": "1.4.0",
50
+ "@takuhon/api": "1.4.0",
51
+ "@takuhon/contact": "1.4.0",
52
+ "@takuhon/core": "1.4.0",
53
+ "@takuhon/mcp": "1.4.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@cloudflare/workers-types": "^4.20260518.1",