@takuhon/cloudflare 0.11.0 → 0.13.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/README.md CHANGED
@@ -17,6 +17,10 @@ local edge cache, and `console.log`-based audit logging.
17
17
  | `GET` | `/admin` | `@takuhon/api` `createAdminUiApp` (HTML) |
18
18
  | `PUT` | `/api/admin/profile` | `@takuhon/api` `createAdminApiApp` |
19
19
  | `DELETE` | `/api/admin/profile` | `@takuhon/api` `createAdminApiApp` |
20
+ | `POST` | `/api/admin/assets` | `@takuhon/api` `createAdminApiApp` (R2)\* |
21
+ | `GET` | `/assets/*` | R2 delivery proxy (this package)\* |
22
+
23
+ \* Registered only when an R2 bucket is bound (see [Image uploads](#image-uploads-r2)).
20
24
 
21
25
  `POST` / `PATCH` on admin paths returns `405 Method Not Allowed`. Schema
22
26
  validation failures return `422 Unprocessable Entity` with an `errors[]`
@@ -154,12 +158,47 @@ Leave `TAKUHON_ADMIN_TOKEN` unset. Every admin write returns `401`. The
154
158
  `/admin` UI is still served but its Save / Delete actions will fail
155
159
  identically; treat that as a feature-flag for read-only deployments.
156
160
 
161
+ ### Image uploads (R2)
162
+
163
+ Image uploads are **optional** and off until an R2 bucket is bound as
164
+ `TAKUHON_R2`. Without it, `POST /api/admin/assets` is unregistered (404) and
165
+ avatars stay URL-only.
166
+
167
+ Create a bucket and uncomment the `[[r2_buckets]]` block in `wrangler.toml`:
168
+
169
+ ```sh
170
+ wrangler r2 bucket create takuhon-assets
171
+ ```
172
+
173
+ ```toml
174
+ [[r2_buckets]]
175
+ binding = "TAKUHON_R2"
176
+ bucket_name = "takuhon-assets"
177
+ ```
178
+
179
+ Once bound:
180
+
181
+ - `POST /api/admin/assets` (admin-authenticated, `multipart/form-data`, field
182
+ `file`) accepts JPEG / PNG / WebP / GIF up to 5 MB and 4096×4096px. The type
183
+ is authenticated from the magic bytes (not the declared `Content-Type`), and
184
+ EXIF / IPTC / XMP / color-profile metadata is stripped before storage
185
+ (`security.md` §4). It returns `201` with the asset record (`publicUrl`).
186
+ - Objects are stored under `assets/{timestamp}-{shortHash}.{ext}` and **served
187
+ by the Worker**, not a public R2 bucket: `GET /assets/*` proxies the bytes
188
+ with `X-Content-Type-Options: nosniff` and a long-lived immutable cache. The
189
+ bucket stays private, which keeps the door open for signed-URL delivery in a
190
+ later phase.
191
+
192
+ Asset delivery is intentionally locale-agnostic: only the literal `/assets/*`
193
+ prefix is served, so `/{locale}/assets/...` 404s.
194
+
157
195
  ## Limitations & deferred work
158
196
 
159
197
  | Concern | Status | Tracked phase |
160
198
  | ---------------------------------- | ------------------------------------- | ------------- |
161
199
  | `PATCH /api/admin/profile` | 405 (intentionally not implemented) | Phase 5+ |
162
- | `POST /api/admin/assets` (R2) | Not yet wired | Phase 3.5 |
200
+ | AVIF uploads | Rejected (415); other formats only | Phase 5+ |
201
+ | Signed-URL / private asset reads | Public proxy via `GET /assets/*` | Phase 5+ |
163
202
  | Global cache purge (REST API) | Colo-local only via `Cache.delete` | Phase 5+ |
164
203
  | CORS preflight for cross-origin | Not handled (admin UI is same-origin) | Phase 5+ |
165
204
  | CLI scaffolding (`create-takuhon`) | Minimal Wrangler bootstrap | Phase 3.6 |
package/dist/index.d.ts CHANGED
@@ -21,6 +21,27 @@ interface Env {
21
21
  * without Workers Assets configured still have a working admin.
22
22
  */
23
23
  ASSETS?: Fetcher;
24
+ /**
25
+ * R2 bucket holding uploaded image assets. Optional, mirroring {@link ASSETS}:
26
+ * when bound, `POST /api/admin/assets` stores images here and `GET /assets/*`
27
+ * serves them; when absent, the upload endpoint stays unregistered (404) and
28
+ * avatars remain URL-only.
29
+ */
30
+ TAKUHON_R2?: R2Bucket;
31
+ /**
32
+ * GitHub token for the scheduled activity sync. Optional: languages are
33
+ * fetched unauthenticated without it; the contribution calendar (GraphQL,
34
+ * token-only) is simply skipped. Provision via
35
+ * `wrangler secret put TAKUHON_GITHUB_TOKEN` — never in `wrangler.toml`.
36
+ */
37
+ TAKUHON_GITHUB_TOKEN?: string;
38
+ /**
39
+ * WakaTime API key for the scheduled activity sync; required to read coding
40
+ * time (WakaTime has no unauthenticated mode). Provision via
41
+ * `wrangler secret put TAKUHON_WAKATIME_KEY` — never in `wrangler.toml`.
42
+ * Only flows through the sync step; never persisted.
43
+ */
44
+ TAKUHON_WAKATIME_KEY?: string;
24
45
  }
25
46
  /** Options accepted by {@link createTakuhonWorker}. */
26
47
  interface CreateTakuhonWorkerOptions {
@@ -48,9 +69,11 @@ interface CreateTakuhonWorkerOptions {
48
69
  */
49
70
  declare function createTakuhonWorker(opts: CreateTakuhonWorkerOptions): {
50
71
  fetch: (request: Request, env: Env) => Response | Promise<Response>;
72
+ scheduled: (controller: ScheduledController, env: Env, ctx: ExecutionContext) => Promise<void>;
51
73
  };
52
74
  declare const _default: {
53
75
  fetch: (request: Request, env: Env) => Response | Promise<Response>;
76
+ scheduled: (controller: ScheduledController, env: Env, ctx: ExecutionContext) => Promise<void>;
54
77
  };
55
78
 
56
79
  export { type CreateTakuhonWorkerOptions, type Env, createTakuhonWorker, _default as default };
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { Hono } from "hono";
13
13
 
14
14
  // ../../examples/personal-profile/takuhon.json
15
15
  var takuhon_default = {
16
- schemaVersion: "0.4.0",
16
+ schemaVersion: "0.5.0",
17
17
  profile: {
18
18
  displayName: {
19
19
  en: "Pat Rivera",
@@ -524,6 +524,64 @@ var takuhon_default = {
524
524
  }
525
525
  };
526
526
 
527
+ // src/activity-sync.ts
528
+ import { fetchActivitySnapshot, isEmptySnapshot } from "@takuhon/activity";
529
+ import {
530
+ NotFoundError
531
+ } from "@takuhon/core";
532
+ function redactSecrets(text, secrets) {
533
+ let out = text;
534
+ for (const secret of [secrets.githubToken, secrets.wakatimeKey]) {
535
+ if (secret !== void 0 && secret !== "") out = out.split(secret).join("***");
536
+ }
537
+ return out;
538
+ }
539
+ async function syncActivity(opts) {
540
+ let profile;
541
+ try {
542
+ profile = (await opts.profileStorage.getProfile()).data;
543
+ } catch (err) {
544
+ if (err instanceof NotFoundError && opts.fallback) {
545
+ profile = opts.fallback();
546
+ } else if (err instanceof NotFoundError) {
547
+ return { status: "skipped", reason: "no stored profile", failures: [] };
548
+ } else {
549
+ throw err;
550
+ }
551
+ }
552
+ const config = profile.settings.activity;
553
+ if (config?.enabled !== true) {
554
+ return {
555
+ status: "skipped",
556
+ reason: "activity is not enabled in settings.activity",
557
+ failures: []
558
+ };
559
+ }
560
+ const hasGithub = config.github?.username !== void 0 && config.github.username !== "";
561
+ const hasWakatime = config.wakatime?.username !== void 0 && config.wakatime.username !== "";
562
+ if (!hasGithub && !hasWakatime) {
563
+ return { status: "skipped", reason: "no github or wakatime username configured", failures: [] };
564
+ }
565
+ const failures = [];
566
+ const snapshot = await fetchActivitySnapshot(config, opts.secrets, {
567
+ fetch: opts.fetch,
568
+ now: opts.now,
569
+ onError: (source, err) => {
570
+ const detail = err instanceof Error ? err.message : String(err);
571
+ failures.push({ source, message: redactSecrets(detail, opts.secrets) });
572
+ }
573
+ });
574
+ if (isEmptySnapshot(snapshot)) {
575
+ return {
576
+ status: "empty",
577
+ reason: "the sync gathered no activity data; kept the last-known snapshot",
578
+ failures
579
+ };
580
+ }
581
+ await opts.activityStorage.saveActivitySnapshot(snapshot);
582
+ return { status: "synced", failures };
583
+ }
584
+
527
585
  // src/admin/cloudflare-cache-purger.ts
528
586
  var CloudflareCachePurger = class {
529
587
  /**
@@ -563,8 +621,32 @@ var consoleAuditLogger = (event) => {
563
621
  console.log(JSON.stringify(event));
564
622
  };
565
623
 
624
+ // src/kv-activity-storage.ts
625
+ import { isActivitySnapshot } from "@takuhon/core";
626
+ var ACTIVITY_KV_KEY = "TAKUHON_ACTIVITY";
627
+ var KvActivityStorage = class {
628
+ constructor(kv) {
629
+ this.kv = kv;
630
+ }
631
+ kv;
632
+ async getActivitySnapshot() {
633
+ const raw = await this.kv.get(ACTIVITY_KV_KEY, "text");
634
+ if (raw === null) return null;
635
+ let parsed;
636
+ try {
637
+ parsed = JSON.parse(raw);
638
+ } catch {
639
+ return null;
640
+ }
641
+ return isActivitySnapshot(parsed) ? parsed : null;
642
+ }
643
+ async saveActivitySnapshot(snapshot) {
644
+ await this.kv.put(ACTIVITY_KV_KEY, JSON.stringify(snapshot));
645
+ }
646
+ };
647
+
566
648
  // src/kv-storage.ts
567
- import { ConflictError, NotFoundError } from "@takuhon/core";
649
+ import { ConflictError, NotFoundError as NotFoundError2 } from "@takuhon/core";
568
650
  var KV_KEY = "TAKUHON_DATA";
569
651
  var KvTakuhonStorage = class {
570
652
  constructor(kv) {
@@ -574,7 +656,7 @@ var KvTakuhonStorage = class {
574
656
  async getProfile() {
575
657
  const result = await this.kv.getWithMetadata(KV_KEY, "json");
576
658
  if (result.value === null || !result.metadata?.version) {
577
- throw new NotFoundError(`No profile is stored at KV key "${KV_KEY}".`);
659
+ throw new NotFoundError2(`No profile is stored at KV key "${KV_KEY}".`);
578
660
  }
579
661
  return { data: result.value, version: result.metadata.version };
580
662
  }
@@ -601,6 +683,89 @@ var KvTakuhonStorage = class {
601
683
  }
602
684
  };
603
685
 
686
+ // src/r2-storage.ts
687
+ import {
688
+ ACCEPTED_IMAGE_MIME_TYPES,
689
+ detectImageMime,
690
+ IMAGE_EXTENSIONS,
691
+ NotFoundError as NotFoundError3,
692
+ readImageInfo
693
+ } from "@takuhon/core";
694
+ var ASSET_KEY_PREFIX = "assets/";
695
+ var DEFAULT_EXTENSION = "bin";
696
+ var ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable";
697
+ function asAcceptedMime(value) {
698
+ if (value === void 0) return null;
699
+ return ACCEPTED_IMAGE_MIME_TYPES.includes(value) ? value : null;
700
+ }
701
+ function timestampSeconds() {
702
+ return Math.floor(Date.now() / 1e3);
703
+ }
704
+ function shortHash() {
705
+ const bytes = new Uint8Array(2);
706
+ crypto.getRandomValues(bytes);
707
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
708
+ }
709
+ var R2TakuhonAssetStorage = class {
710
+ bucket;
711
+ publicBaseUrl;
712
+ constructor(bucket, options = {}) {
713
+ this.bucket = bucket;
714
+ this.publicBaseUrl = options.publicBaseUrl ?? "";
715
+ }
716
+ async putAsset(file, options) {
717
+ const bytes = new Uint8Array(await file.arrayBuffer());
718
+ const mime = asAcceptedMime(options?.contentType) ?? detectImageMime(bytes);
719
+ const ext = mime !== null ? IMAGE_EXTENSIONS[mime] : DEFAULT_EXTENSION;
720
+ const contentType = mime ?? options?.contentType ?? file.type ?? "application/octet-stream";
721
+ const key = `${ASSET_KEY_PREFIX}${String(timestampSeconds())}-${shortHash()}.${ext}`;
722
+ await this.bucket.put(key, bytes, { httpMetadata: { contentType } });
723
+ const info = mime !== null ? readImageInfo(bytes, mime) : null;
724
+ const url = `/${key}`;
725
+ return {
726
+ id: key,
727
+ url,
728
+ publicUrl: this.absoluteUrl(url),
729
+ mimeType: contentType,
730
+ size: bytes.length,
731
+ width: info?.width,
732
+ height: info?.height,
733
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
734
+ };
735
+ }
736
+ async getPublicUrl(assetId) {
737
+ const head = await this.bucket.head(assetId);
738
+ if (head === null) throw new NotFoundError3(`No asset is stored at R2 key "${assetId}".`);
739
+ return this.absoluteUrl(`/${assetId}`);
740
+ }
741
+ async deleteAsset(assetId) {
742
+ await this.bucket.delete(assetId);
743
+ }
744
+ async listAssets() {
745
+ const records = [];
746
+ let cursor;
747
+ do {
748
+ const page = await this.bucket.list({ prefix: ASSET_KEY_PREFIX, cursor });
749
+ for (const object of page.objects) {
750
+ const url = `/${object.key}`;
751
+ records.push({
752
+ id: object.key,
753
+ url,
754
+ publicUrl: this.absoluteUrl(url),
755
+ mimeType: object.httpMetadata?.contentType ?? "application/octet-stream",
756
+ size: object.size,
757
+ createdAt: object.uploaded.toISOString()
758
+ });
759
+ }
760
+ cursor = page.truncated ? page.cursor : void 0;
761
+ } while (cursor !== void 0);
762
+ return records;
763
+ }
764
+ absoluteUrl(path) {
765
+ return this.publicBaseUrl !== "" ? `${this.publicBaseUrl}${path}` : path;
766
+ }
767
+ };
768
+
604
769
  // src/index.ts
605
770
  function parseOrigins(raw) {
606
771
  if (raw === void 0 || raw === "") return [];
@@ -609,6 +774,27 @@ function parseOrigins(raw) {
609
774
  function isAdminUiPath(pathname) {
610
775
  return pathname === "/admin" || pathname.startsWith("/admin/");
611
776
  }
777
+ function isAssetPath(pathname) {
778
+ return pathname.startsWith("/assets/");
779
+ }
780
+ async function serveAsset(request, bucket, url) {
781
+ if (request.method !== "GET" && request.method !== "HEAD") {
782
+ return new Response("Method Not Allowed", { status: 405 });
783
+ }
784
+ const key = url.pathname.slice(1);
785
+ const object = await bucket.get(key);
786
+ if (object === null) {
787
+ return new Response("Not Found", { status: 404 });
788
+ }
789
+ const headers = new Headers();
790
+ const contentType = object.httpMetadata?.contentType;
791
+ if (contentType !== void 0) headers.set("content-type", contentType);
792
+ headers.set("x-content-type-options", "nosniff");
793
+ headers.set("cache-control", ASSET_CACHE_CONTROL);
794
+ headers.set("etag", object.httpEtag);
795
+ const body = request.method === "HEAD" ? null : object.body;
796
+ return new Response(body, { status: 200, headers });
797
+ }
612
798
  async function serveAdminSpa(request, assets, url) {
613
799
  if (request.method !== "GET" && request.method !== "HEAD") {
614
800
  return new Response("Method Not Allowed", { status: 405 });
@@ -634,7 +820,11 @@ function createTakuhonWorker(opts) {
634
820
  if (env.ASSETS && isAdminUiPath(url.pathname)) {
635
821
  return serveAdminSpa(request, env.ASSETS, url);
636
822
  }
823
+ if (env.TAKUHON_R2 && isAssetPath(url.pathname)) {
824
+ return serveAsset(request, env.TAKUHON_R2, url);
825
+ }
637
826
  const storage = new KvTakuhonStorage(env.TAKUHON_KV);
827
+ const assetStorage = env.TAKUHON_R2 ? new R2TakuhonAssetStorage(env.TAKUHON_R2, { publicBaseUrl: url.origin }) : void 0;
638
828
  const cachePurger = new CloudflareCachePurger(() => caches.default, {
639
829
  origin: url.origin
640
830
  });
@@ -652,6 +842,7 @@ function createTakuhonWorker(opts) {
652
842
  "/api/admin",
653
843
  createAdminApiApp({
654
844
  storage,
845
+ assetStorage,
655
846
  getAdminToken: () => env.TAKUHON_ADMIN_TOKEN,
656
847
  getAdminOrigins: () => parseOrigins(env.TAKUHON_ADMIN_ORIGIN),
657
848
  cachePurger,
@@ -659,8 +850,51 @@ function createTakuhonWorker(opts) {
659
850
  })
660
851
  );
661
852
  router.route("/admin", createAdminUiApp());
662
- router.route("/", createPublicApp({ storage, fallback: opts.fallback }));
853
+ router.route(
854
+ "/",
855
+ createPublicApp({
856
+ storage,
857
+ fallback: opts.fallback,
858
+ // Serves `GET /api/activity` from the synced snapshot; the route
859
+ // 404s while no snapshot is stored or activity is not enabled.
860
+ activityStorage: new KvActivityStorage(env.TAKUHON_KV)
861
+ })
862
+ );
663
863
  return router.fetch(request, env);
864
+ },
865
+ /**
866
+ * Cron-driven activity sync (enable with a `[triggers] crons` entry in
867
+ * `wrangler.toml`; daily is the recommended cadence). MUST NOT throw: a
868
+ * failed sync keeps the last-known snapshot and surfaces through the
869
+ * structured log line below (captured by Workers Tail / Logpush), never by
870
+ * failing the cron run.
871
+ */
872
+ async scheduled(_controller, env, _ctx) {
873
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
874
+ try {
875
+ const result = await syncActivity({
876
+ profileStorage: new KvTakuhonStorage(env.TAKUHON_KV),
877
+ activityStorage: new KvActivityStorage(env.TAKUHON_KV),
878
+ secrets: {
879
+ githubToken: env.TAKUHON_GITHUB_TOKEN,
880
+ wakatimeKey: env.TAKUHON_WAKATIME_KEY
881
+ },
882
+ fallback: opts.fallback
883
+ });
884
+ const type = result.status === "synced" ? "activity.sync.success" : result.status === "empty" ? "activity.sync.failure" : "activity.sync.skipped";
885
+ console.log(
886
+ JSON.stringify({ type, timestamp, reason: result.reason, failures: result.failures })
887
+ );
888
+ } catch (err) {
889
+ console.error(
890
+ JSON.stringify({
891
+ type: "activity.sync.failure",
892
+ timestamp,
893
+ reason: err instanceof Error ? err.message : String(err),
894
+ failures: []
895
+ })
896
+ );
897
+ }
664
898
  }
665
899
  };
666
900
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../../../examples/personal-profile/takuhon.json","../src/admin/cloudflare-cache-purger.ts","../src/admin/console-audit-logger.ts","../src/kv-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 { validate, type Takuhon } from '@takuhon/core';\nimport { Hono } from 'hono';\n\nimport exampleJson from '../../../examples/personal-profile/takuhon.json' with { type: 'json' };\n\nimport { CloudflareCachePurger } from './admin/cloudflare-cache-purger.js';\nimport { consoleAuditLogger } from './admin/console-audit-logger.js';\nimport { KvTakuhonStorage } from './kv-storage.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\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 * 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} {\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 const storage = new KvTakuhonStorage(env.TAKUHON_KV);\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 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('/', createPublicApp({ storage, fallback: opts.fallback }));\n\n return router.fetch(request, env);\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\": \"0.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 \"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 },\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","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","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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../../../examples/personal-profile/takuhon.json","../src/activity-sync.ts","../src/admin/cloudflare-cache-purger.ts","../src/admin/console-audit-logger.ts","../src/kv-activity-storage.ts","../src/kv-storage.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 { validate, type Takuhon } from '@takuhon/core';\nimport { Hono } from 'hono';\n\nimport exampleJson from '../../../examples/personal-profile/takuhon.json' with { type: 'json' };\n\nimport { syncActivity } from './activity-sync.js';\nimport { CloudflareCachePurger } from './admin/cloudflare-cache-purger.js';\nimport { consoleAuditLogger } from './admin/console-audit-logger.js';\nimport { KvActivityStorage } from './kv-activity-storage.js';\nimport { KvTakuhonStorage } from './kv-storage.js';\nimport { ASSET_CACHE_CONTROL, R2TakuhonAssetStorage } from './r2-storage.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\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 * 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 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 }),\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\": \"0.5.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 },\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 * 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","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","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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takuhon/cloudflare",
3
- "version": "0.11.0",
3
+ "version": "0.13.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",
@@ -44,9 +44,10 @@
44
44
  "node": ">=22.0.0"
45
45
  },
46
46
  "dependencies": {
47
- "hono": "^4.12.19",
48
- "@takuhon/api": "0.11.0",
49
- "@takuhon/core": "0.11.0"
47
+ "hono": "^4.12.21",
48
+ "@takuhon/activity": "0.13.0",
49
+ "@takuhon/core": "0.13.0",
50
+ "@takuhon/api": "0.13.0"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@cloudflare/workers-types": "^4.20260518.1",