@takuhon/api 0.6.1 → 0.8.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,6 +1,7 @@
1
1
  import { Context, Hono } from 'hono';
2
2
  import { ContentfulStatusCode } from 'hono/utils/http-status';
3
- import { TakuhonStorage, Takuhon, LocalizedTakuhon } from '@takuhon/core';
3
+ import { TakuhonStorage, Takuhon } from '@takuhon/core';
4
+ export { applyPublicPrivacyFilter } from '@takuhon/core';
4
5
 
5
6
  /**
6
7
  * RFC 7807 problem type slugs used by takuhon. The 11 below are the
@@ -67,60 +68,6 @@ interface PublicAppDeps {
67
68
  }
68
69
  declare function createPublicApp(deps: PublicAppDeps): Hono;
69
70
 
70
- /**
71
- * Public-endpoint privacy filter for takuhon profile documents.
72
- *
73
- * Strips fields that the spec's privacy posture marks as opt-in for public
74
- * exposure, before the response reaches a public reader. The filter is
75
- * deliberately conservative: when `meta.privacy` is absent the most
76
- * restrictive interpretation applies (everything sensitive is hidden), and
77
- * the operator must explicitly opt into disclosure by setting the relevant
78
- * flag to `false`.
79
- *
80
- * Fields filtered:
81
- *
82
- * - `certifications[*].credentialId` — hidden when
83
- * `meta.privacy.hideCredentialIds !== false` (default true).
84
- * - `education[*].grade` — hidden when
85
- * `meta.privacy.hideEducationGrades !== false` (default true).
86
- * - `contact.email` — hidden when `contact.showEmail !== true`. The
87
- * documented contract has required this since the original `Contact`
88
- * shape but the 0.1.x runtime never actually applied the filter; bringing
89
- * it under the same helper in 0.2.0 closes that drift.
90
- *
91
- * `patents[*].patentNumber` is **not** filtered. Patent numbers are public
92
- * records (issued patents are published by the granting office) and Spec
93
- * §6.21 explicitly excludes them from the privacy block.
94
- *
95
- * Behavior:
96
- *
97
- * - Pure function. The input is never mutated; a shallow-copied result is
98
- * returned with only the touched arrays / objects replaced.
99
- * - When no filter applies (every flag opts into disclosure), the original
100
- * reference is returned as-is so callers can compare by identity.
101
- * - Admin endpoints (`/api/admin/*`, including `/api/admin/export`) MUST NOT
102
- * call this helper — they always serve the full document to authenticated
103
- * callers.
104
- */
105
-
106
- /**
107
- * Union of the two profile shapes that traverse the API response path. The
108
- * fields the filter touches (`certifications[*].credentialId`,
109
- * `education[*].grade`, `contact.email`) are structurally identical
110
- * between {@link Takuhon} and {@link LocalizedTakuhon}, so the same logic
111
- * applies to either shape.
112
- */
113
- type FilterableProfile = Takuhon | LocalizedTakuhon;
114
- /**
115
- * Return a privacy-filtered copy of `profile` suitable for public responses.
116
- *
117
- * @param profile Either a raw {@link Takuhon} (as served by `/takuhon.json`)
118
- * or a locale-resolved {@link LocalizedTakuhon} (as served
119
- * by `/api/profile` and `/api/jsonld`). The output preserves
120
- * the input's exact shape.
121
- */
122
- declare function applyPublicPrivacyFilter<T extends FilterableProfile>(profile: T): T;
123
-
124
71
  /**
125
72
  * Remainder paths that may legitimately follow a `/{locale}` segment.
126
73
  *
@@ -250,4 +197,4 @@ declare function createAdminApiApp(deps: AdminApiAppDeps): Hono;
250
197
  */
251
198
  declare function createAdminUiApp(): Hono;
252
199
 
253
- export { type AdminApiAppDeps, type AuditEvent, type AuditEventType, type AuditLogger, type BuildProblemInput, type CachePurger, ERROR_SLUGS, type ErrorSlug, LOCALE_AWARE_REMAINDERS, type ProblemDetails, type ProblemFieldError, type ProblemResponseInput, type PublicAppDeps, applyPublicPrivacyFilter, buildProblem, createAdminApiApp, createAdminUiApp, createPublicApp, localePrefixGetPath, noopAuditLogger, noopCachePurger, pathLocaleFromUrl, problemResponse, stripLocalePrefix };
200
+ export { type AdminApiAppDeps, type AuditEvent, type AuditEventType, type AuditLogger, type BuildProblemInput, type CachePurger, ERROR_SLUGS, type ErrorSlug, LOCALE_AWARE_REMAINDERS, type ProblemDetails, type ProblemFieldError, type ProblemResponseInput, type PublicAppDeps, buildProblem, createAdminApiApp, createAdminUiApp, createPublicApp, localePrefixGetPath, noopAuditLogger, noopCachePurger, pathLocaleFromUrl, problemResponse, stripLocalePrefix };
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ function problemResponse(c, input) {
45
45
  import {
46
46
  NotFoundError,
47
47
  SCHEMA_VERSION,
48
+ applyPublicPrivacyFilter,
48
49
  generateJsonLd,
49
50
  normalize,
50
51
  resolveLocale,
@@ -169,47 +170,6 @@ function pathLocaleFromUrl(url) {
169
170
  return stripLocalePrefix(getPathname(url)).locale;
170
171
  }
171
172
 
172
- // src/privacy-filter.ts
173
- function applyPublicPrivacyFilter(profile) {
174
- const hideCredentialIds = profile.meta.privacy?.hideCredentialIds !== false;
175
- const hideEducationGrades = profile.meta.privacy?.hideEducationGrades !== false;
176
- const allowEmail = profile.contact.showEmail === true;
177
- const stripCertifications = hideCredentialIds && hasAnyCredentialId(profile);
178
- const stripEducation = hideEducationGrades && hasAnyGrade(profile);
179
- const stripEmail = !allowEmail && profile.contact.email !== void 0;
180
- if (!stripCertifications && !stripEducation && !stripEmail) {
181
- return profile;
182
- }
183
- const out = { ...profile };
184
- if (stripCertifications) {
185
- out.certifications = profile.certifications.map(stripCredentialId);
186
- }
187
- if (stripEducation) {
188
- out.education = profile.education.map(stripGrade);
189
- }
190
- if (stripEmail) {
191
- const { email: _omit, ...rest } = profile.contact;
192
- out.contact = rest;
193
- }
194
- return out;
195
- }
196
- function hasAnyCredentialId(profile) {
197
- return profile.certifications.some((c) => c.credentialId !== void 0);
198
- }
199
- function hasAnyGrade(profile) {
200
- return profile.education.some((e) => e.grade !== void 0);
201
- }
202
- function stripCredentialId(item) {
203
- if (item.credentialId === void 0) return item;
204
- const { credentialId: _omit, ...rest } = item;
205
- return rest;
206
- }
207
- function stripGrade(item) {
208
- if (item.grade === void 0) return item;
209
- const { grade: _omit, ...rest } = item;
210
- return rest;
211
- }
212
-
213
173
  // src/public-app.ts
214
174
  var FALLBACK_VERSION = "bundled-fixture";
215
175
  var PUBLIC_CSP = [
@@ -339,6 +299,9 @@ function createPublicApp(deps) {
339
299
  return app;
340
300
  }
341
301
 
302
+ // src/index.ts
303
+ import { applyPublicPrivacyFilter as applyPublicPrivacyFilter2 } from "@takuhon/core";
304
+
342
305
  // src/admin/admin-api-app.ts
343
306
  import {
344
307
  ConflictError,
@@ -798,7 +761,7 @@ var noopCachePurger = {
798
761
  export {
799
762
  ERROR_SLUGS,
800
763
  LOCALE_AWARE_REMAINDERS,
801
- applyPublicPrivacyFilter,
764
+ applyPublicPrivacyFilter2 as applyPublicPrivacyFilter,
802
765
  buildProblem,
803
766
  createAdminApiApp,
804
767
  createAdminUiApp,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/error-envelope.ts","../src/public-app.ts","../src/locale-resolution.ts","../src/locale-prefix.ts","../src/privacy-filter.ts","../src/admin/admin-api-app.ts","../src/admin/bearer.ts","../src/admin/origin.ts","../src/admin/admin-ui-app.ts","../src/admin/admin-html.ts","../src/admin/audit-logger.ts","../src/admin/cache-purger.ts"],"sourcesContent":["import type { Context } from 'hono';\nimport type { ContentfulStatusCode } from 'hono/utils/http-status';\n\n/**\n * RFC 7807 problem type slugs used by takuhon. The 11 below are the\n * Spec-defined values (api.md §5.1); `methodNotAllowed` is added locally\n * for the 405 path that the Spec leaves unnamed.\n */\nexport const ERROR_SLUGS = {\n badRequest: 'bad-request',\n unauthorized: 'unauthorized',\n forbidden: 'forbidden',\n notFound: 'not-found',\n methodNotAllowed: 'method-not-allowed',\n conflict: 'conflict',\n payloadTooLarge: 'payload-too-large',\n unsupportedMediaType: 'unsupported-media-type',\n validationFailed: 'validation-failed',\n tooManyRequests: 'too-many-requests',\n internal: 'internal',\n serviceUnavailable: 'service-unavailable',\n} as const;\n\nexport type ErrorSlug = (typeof ERROR_SLUGS)[keyof typeof ERROR_SLUGS];\n\nconst TYPE_BASE = 'https://takuhon.org/errors';\n\nexport interface ProblemFieldError {\n path: string;\n message: string;\n}\n\nexport interface ProblemDetails {\n type: string;\n title: string;\n status: number;\n detail: string;\n instance: string;\n errors?: ProblemFieldError[];\n currentVersion?: string;\n}\n\nexport interface BuildProblemInput {\n slug: ErrorSlug;\n status: number;\n title: string;\n detail: string;\n instance: string;\n errors?: ProblemFieldError[];\n currentVersion?: string;\n}\n\nexport function buildProblem(input: BuildProblemInput): ProblemDetails {\n const out: ProblemDetails = {\n type: `${TYPE_BASE}/${input.slug}`,\n title: input.title,\n status: input.status,\n detail: input.detail,\n instance: input.instance,\n };\n if (input.errors !== undefined) out.errors = input.errors;\n if (input.currentVersion !== undefined) out.currentVersion = input.currentVersion;\n return out;\n}\n\nexport interface ProblemResponseInput {\n slug: ErrorSlug;\n status: ContentfulStatusCode;\n title: string;\n detail: string;\n errors?: ProblemFieldError[];\n currentVersion?: string;\n}\n\nexport function problemResponse(c: Context, input: ProblemResponseInput): Response {\n const body = buildProblem({\n slug: input.slug,\n status: input.status,\n title: input.title,\n detail: input.detail,\n instance: new URL(c.req.url).pathname,\n errors: input.errors,\n currentVersion: input.currentVersion,\n });\n return c.body(JSON.stringify(body), input.status, {\n 'content-type': 'application/problem+json; charset=utf-8',\n });\n}\n","import {\n NotFoundError,\n SCHEMA_VERSION,\n generateJsonLd,\n normalize,\n resolveLocale,\n schema,\n type Takuhon,\n type TakuhonStorage,\n} from '@takuhon/core';\nimport { Hono } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse } from './error-envelope.js';\nimport { localePrefixGetPath, pathLocaleFromUrl } from './locale-prefix.js';\nimport { resolveRequestLocales } from './locale-resolution.js';\nimport { applyPublicPrivacyFilter } from './privacy-filter.js';\n\nexport interface PublicAppDeps {\n storage: TakuhonStorage;\n /**\n * Returned when storage reports NotFoundError. Adapters that ship a\n * bundled example fixture (e.g. @takuhon/cloudflare) pass a thunk that\n * returns the validated document so initial-onboarding requests still\n * succeed before the first admin write.\n */\n fallback?: () => Takuhon;\n}\n\nconst FALLBACK_VERSION = 'bundled-fixture';\n\nconst PUBLIC_CSP = [\n \"default-src 'self'\",\n \"img-src 'self' data:\",\n \"style-src 'self' 'unsafe-inline'\",\n \"script-src 'self'\",\n \"font-src 'self'\",\n \"connect-src 'self'\",\n \"frame-ancestors 'none'\",\n \"base-uri 'self'\",\n \"form-action 'self'\",\n 'upgrade-insecure-requests',\n].join('; ');\n\nasync function loadProfile(deps: PublicAppDeps): Promise<{ data: Takuhon; version: string }> {\n try {\n return await deps.storage.getProfile();\n } catch (e) {\n if (e instanceof NotFoundError && deps.fallback) {\n return { data: deps.fallback(), version: FALLBACK_VERSION };\n }\n throw e;\n }\n}\n\nexport function createPublicApp(deps: PublicAppDeps): Hono {\n // `getPath` strips a leading `/{locale}` prefix (e.g. `/ja/api/profile`\n // → `/api/profile`) so the flat routes below match locale-prefixed\n // URLs. The same function is applied on the adapter's top-level router,\n // because Hono's `route()` flattens this app's routes into the parent\n // and dispatches with the parent's `getPath` only — setting it here\n // alone would be honored for direct `app.fetch()` (tests) but not in\n // production. Handlers recover the locale token from the original URL\n // (`c.req.url`), which `getPath` does not mutate.\n const app = new Hono({ getPath: localePrefixGetPath });\n\n app.use('*', async (c, next) => {\n await next();\n const h = c.res.headers;\n h.set('strict-transport-security', 'max-age=63072000; includeSubDomains; preload');\n h.set('x-content-type-options', 'nosniff');\n h.set('x-frame-options', 'DENY');\n h.set('referrer-policy', 'strict-origin-when-cross-origin');\n h.set('permissions-policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');\n h.set('content-security-policy', PUBLIC_CSP);\n });\n\n app.onError((err, c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.internal,\n status: 500,\n title: 'Internal Error',\n detail: err instanceof Error ? err.message : 'Unknown failure',\n }),\n );\n\n app.notFound((c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: `No route matches ${new URL(c.req.url).pathname}.`,\n }),\n );\n\n app.get('/', (c) => c.text('takuhon — visit /api/profile or /api/schema\\n'));\n\n // Liveness probe. Intentionally storage-independent: it reports that the\n // worker itself is serving requests, not that the profile store is\n // reachable. A readiness probe that also checks storage can be added\n // later under a separate path if deployment platforms need it.\n app.get('/health', (c) => {\n c.header('cache-control', 'no-store');\n return c.json({ status: 'ok', schemaVersion: SCHEMA_VERSION });\n });\n\n app.get('/api/profile', async (c) => {\n const { data, version } = await loadProfile(deps);\n const { locale, fallbackLocale } = resolveRequestLocales(\n c,\n data.settings.availableLocales,\n pathLocaleFromUrl(c.req.url),\n );\n const localized = applyPublicPrivacyFilter(\n resolveLocale(normalize(data), locale, fallbackLocale),\n );\n const body = {\n data: localized,\n meta: {\n schemaVersion: localized.schemaVersion,\n locale: localized.resolvedLocale,\n updatedAt: localized.meta.updatedAt,\n },\n };\n c.header('etag', `\"${version}\"`);\n c.header('cache-control', 'private, max-age=300');\n c.header('vary', 'Accept-Language, Cookie');\n return c.json(body);\n });\n\n app.get('/api/schema', (c) => c.json(schema));\n\n app.get('/api/jsonld', async (c) => {\n const { data, version } = await loadProfile(deps);\n const { locale, fallbackLocale } = resolveRequestLocales(\n c,\n data.settings.availableLocales,\n pathLocaleFromUrl(c.req.url),\n );\n const localized = applyPublicPrivacyFilter(\n resolveLocale(normalize(data), locale, fallbackLocale),\n );\n const ld = generateJsonLd(localized);\n c.header('etag', `\"${version}\"`);\n c.header('cache-control', 'private, max-age=300');\n c.header('vary', 'Accept-Language, Cookie');\n c.header('content-type', 'application/ld+json; charset=utf-8');\n return c.body(JSON.stringify(ld));\n });\n\n app.get('/takuhon.json', async (c) => {\n const { data, version } = await loadProfile(deps);\n const filtered = applyPublicPrivacyFilter(data);\n c.header('etag', `\"${version}\"`);\n c.header('cache-control', 'public, max-age=300');\n return c.json(filtered);\n });\n\n app.get('/.well-known/takuhon.json', (c) => {\n c.header('cache-control', 'public, max-age=3600');\n return c.json({\n schemaVersion: SCHEMA_VERSION,\n schemaUrl: '/api/schema',\n profile: '/api/profile',\n jsonld: '/api/jsonld',\n export: '/api/admin/export',\n canonical: '/takuhon.json',\n });\n });\n\n app.on(['POST', 'PUT', 'PATCH', 'DELETE'], '*', (c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.methodNotAllowed,\n status: 405,\n title: 'Method Not Allowed',\n detail: `${c.req.method} ${new URL(c.req.url).pathname} is not supported on the public app.`,\n }),\n );\n\n return app;\n}\n","/**\n * HTTP-layer locale resolution for the public app.\n *\n * Reads request-side locale candidates in this priority order:\n *\n * 1. `?lang=` query parameter\n * 2. URL path prefix (e.g. `/ja/`), passed in as `pathLocale`\n * 3. `takuhon_locale` cookie\n * 4. `Accept-Language` request header (q-value ordered)\n *\n * The URL-path candidate is extracted structurally by\n * `locale-prefix.ts` (`stripLocalePrefix` / `pathLocaleFromUrl`) and\n * handed to {@link resolveRequestLocales} as `pathLocale`; this module\n * does not parse the path itself. Settings-tier fallbacks\n * (`settings.defaultLocale`, `settings.fallbackLocale`,\n * `settings.availableLocales[0]`) are resolved inside `@takuhon/core`'s\n * `resolveLocale` and do not appear here.\n *\n * `resolveLocale` only exposes two caller slots (`locale`,\n * `fallbackLocale`). To avoid wasting them on tags the document can't\n * serve, candidates are filtered against `availableLocales` (case-\n * insensitive on the full tag or its primary subtag) and the matched\n * available locale token is substituted before forwarding, so a\n * primary-subtag match like `en` → `en-US` does not silently fall\n * through to the settings tier. Filtered candidates beyond the second\n * fall through to `resolveLocale`'s own settings-tier candidates, not\n * in request order — an acceptable loss given the contract.\n */\nimport type { Context } from 'hono';\nimport { getCookie } from 'hono/cookie';\n\nconst BCP47 = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/;\n\n// DoS guards. The header parser is exposed to untrusted client input,\n// so the byte budget and entry count are bounded before any per-token\n// work. Numbers are conservative defaults, not spec-derived.\nconst ACCEPT_LANG_MAX = 2048;\nconst ACCEPT_LANG_MAX_ENTRIES = 16;\nconst COOKIE_VALUE_MAX = 64;\n\nexport function isValidBcp47(tag: string): boolean {\n return BCP47.test(tag);\n}\n\ninterface AcceptLangEntry {\n readonly tag: string;\n readonly q: number;\n}\n\n/**\n * Parse an `Accept-Language` header into BCP-47 tags ordered by q\n * descending. Invalid or zero-quality entries and `*` wildcards are\n * dropped. Input larger than {@link ACCEPT_LANG_MAX} bytes or with more\n * than {@link ACCEPT_LANG_MAX_ENTRIES} comma-separated parts is\n * truncated before parsing.\n */\nexport function parseAcceptLanguage(header: string | null | undefined): string[] {\n if (!header) return [];\n const trimmed = header.length > ACCEPT_LANG_MAX ? header.slice(0, ACCEPT_LANG_MAX) : header;\n const parts = trimmed.split(',').slice(0, ACCEPT_LANG_MAX_ENTRIES);\n\n const entries: AcceptLangEntry[] = [];\n for (const rawPart of parts) {\n const segments = rawPart.split(';');\n const tagSegment = segments[0];\n if (tagSegment === undefined) continue;\n const tag = tagSegment.trim();\n if (tag === '' || tag === '*') continue;\n if (!isValidBcp47(tag)) continue;\n\n let q = 1;\n for (let i = 1; i < segments.length; i++) {\n const segment = segments[i];\n if (segment === undefined) continue;\n const match = /^\\s*q\\s*=\\s*([0-9.]+)\\s*$/i.exec(segment);\n if (!match) continue;\n const parsed = Number.parseFloat(match[1] ?? '');\n if (Number.isNaN(parsed)) {\n q = 1;\n } else if (parsed < 0 || parsed > 1) {\n // RFC 7231 §5.3.1 says values MUST be in [0, 1]; treat\n // out-of-range as missing (q=1) rather than dropping.\n q = 1;\n } else {\n q = parsed;\n }\n break;\n }\n if (q === 0) continue;\n\n entries.push({ tag, q });\n }\n\n // Stable sort: Array.prototype.sort is stable in ES2019+.\n entries.sort((a, b) => b.q - a.q);\n return entries.map((e) => e.tag);\n}\n\nfunction primarySubtag(tag: string): string {\n const dash = tag.indexOf('-');\n return (dash === -1 ? tag : tag.slice(0, dash)).toLowerCase();\n}\n\nfunction matchAvailable(tag: string, available: readonly string[]): string | undefined {\n const tagLower = tag.toLowerCase();\n const tagPrimary = primarySubtag(tag);\n // Prefer exact (case-insensitive) match over primary-subtag match so a\n // request that names a region explicitly wins over a region-stripped\n // alternative.\n for (const a of available) {\n if (a.toLowerCase() === tagLower) return a;\n }\n for (const a of available) {\n if (primarySubtag(a) === tagPrimary) return a;\n }\n return undefined;\n}\n\n/**\n * Resolve HTTP-layer locale candidates from the request — query, URL\n * path prefix, cookie, and `Accept-Language` in that priority order.\n * Returns the top two candidates that survive validation and the\n * `availableLocales` filter, after substituting the matched available\n * token so primary-subtag matches resolve correctly downstream.\n *\n * @param pathLocale The locale token extracted from a `/{locale}` path\n * prefix by `pathLocaleFromUrl`, or `undefined` when the request has\n * no path prefix. Inserted at priority #2 (after query, before\n * cookie).\n */\nexport function resolveRequestLocales(\n c: Context,\n available: readonly string[],\n pathLocale?: string,\n): { locale?: string; fallbackLocale?: string } {\n const raw: string[] = [];\n\n const query = c.req.query('lang');\n if (query !== undefined && isValidBcp47(query)) {\n raw.push(query);\n }\n\n if (pathLocale !== undefined && isValidBcp47(pathLocale)) {\n raw.push(pathLocale);\n }\n\n const cookie = getCookie(c, 'takuhon_locale');\n if (cookie !== undefined && cookie.length <= COOKIE_VALUE_MAX && isValidBcp47(cookie)) {\n raw.push(cookie);\n }\n\n const accept = c.req.header('accept-language');\n if (accept !== undefined) {\n raw.push(...parseAcceptLanguage(accept));\n }\n\n const seen = new Set<string>();\n const filtered: string[] = [];\n for (const tag of raw) {\n const matched = matchAvailable(tag, available);\n if (matched === undefined) continue;\n const key = matched.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n filtered.push(matched);\n if (filtered.length === 2) break;\n }\n\n const out: { locale?: string; fallbackLocale?: string } = {};\n if (filtered[0] !== undefined) out.locale = filtered[0];\n if (filtered[1] !== undefined) out.fallbackLocale = filtered[1];\n return out;\n}\n","/**\n * URL-path locale prefix handling for the public app.\n *\n * Implements locale resolution priority #2: a leading `/{locale}` path\n * segment, e.g. `/ja/api/profile`, ranked after the `?lang=` query (#1)\n * and before the `takuhon_locale` cookie (#3). This module is responsible\n * only for the *structural* concern — detecting and stripping the prefix\n * so the existing flat routes match — while the locale *value* it\n * extracts is fed into {@link resolveRequestLocales} at slot #2 by the\n * route handlers.\n *\n * The prefix is honored via Hono's `getPath` option rather than parametric\n * routes: {@link localePrefixGetPath} rewrites the match path so a request\n * to `/ja/api/profile` is routed to the `/api/profile` handler. Hono's\n * `route()` flattens a sub-app's routes into the parent and dispatches with\n * the *parent* router's `getPath` only, so the same function is applied on\n * both the standalone public app (for direct tests) and the adapter's\n * top-level router (production). The original request URL is untouched, so\n * handlers recover the locale token from `c.req.url`.\n */\nimport { isValidBcp47 } from './locale-resolution.js';\n\n/**\n * Remainder paths that may legitimately follow a `/{locale}` segment.\n *\n * This allowlist — NOT the BCP-47 shape check — is the load-bearing safety\n * mechanism. It keeps locale-agnostic paths (`/health`, `/api/schema`,\n * `/.well-known/*`, `/takuhon.json`) and admin paths (`/api/admin/*`,\n * `/admin/*`) from being misread as a locale prefix. Note that `api`\n * itself satisfies the BCP-47 primary-subtag shape (`[a-z]{2,3}`), so a\n * shape check alone would treat `/api/schema` as locale `api` + `/schema`;\n * the remainder allowlist is what prevents that.\n *\n * Keep this in sync with the locale-aware routes in `public-app.ts`.\n */\nexport const LOCALE_AWARE_REMAINDERS = ['/', '/api/profile', '/api/jsonld'] as const;\n\n/**\n * First-path segments that are reserved namespaces and must never be read\n * as a locale, even though they satisfy the BCP-47 shape. Without this,\n * a bare `/api` (segment `api` is a valid 2–3 letter primary subtag,\n * remainder defaults to the landing `/`) would alias the landing page\n * instead of 404ing. Other reserved roots (`admin`, `health`,\n * `takuhon.json`, `.well-known`) fail the BCP-47 shape check and need no\n * entry here; `api` is the only collision.\n */\nconst RESERVED_FIRST_SEGMENTS = new Set(['api']);\n\nfunction getPathname(url: string): string {\n return new URL(url).pathname;\n}\n\n/**\n * Split a leading `/{locale}` segment from `pathname` when — and only\n * when — the segment is BCP-47-shaped and the remainder is a locale-aware\n * route ({@link LOCALE_AWARE_REMAINDERS}).\n *\n * - `/ja/api/profile` → `{ locale: 'ja', path: '/api/profile' }`\n * - `/api/profile` → `{ path: '/api/profile' }` (remainder `/profile` not locale-aware)\n * - `/api/schema` → `{ path: '/api/schema' }` (the `api`-collision guard)\n * - `/ja/api/admin` → `{ path: '/ja/api/admin' }` (remainder `/api/admin` not locale-aware → 404, admin isolated)\n * - `/ja` and `/ja/` → `{ locale: 'ja', path: '/' }` (trailing slash normalized to landing)\n *\n * The returned `locale` is the raw path token; it is not matched against\n * `availableLocales` here (that happens downstream in\n * {@link resolveRequestLocales}), so an unknown-but-shaped prefix like\n * `/fr/` on an en/ja document strips structurally and then falls through\n * to the next resolution tier, mirroring `?lang=fr` semantics.\n */\nexport function stripLocalePrefix(pathname: string): { locale?: string; path: string } {\n // Match a leading single segment: `/seg` or `/seg/rest...`.\n const match = /^\\/([^/]+)(\\/.*)?$/.exec(pathname);\n if (match === null) return { path: pathname };\n\n const seg = match[1];\n if (seg === undefined || !isValidBcp47(seg)) return { path: pathname };\n\n // Reserved namespace segments (e.g. `api`) pass the BCP-47 shape but are\n // not locales; leave them for the route table (so `/api` 404s as before).\n if (RESERVED_FIRST_SEGMENTS.has(seg.toLowerCase())) return { path: pathname };\n\n // Normalize a bare `/ja` (no trailing content) to the landing remainder.\n const remainder = match[2] ?? '/';\n if (!(LOCALE_AWARE_REMAINDERS as readonly string[]).includes(remainder)) {\n return { path: pathname };\n }\n\n return { locale: seg, path: remainder };\n}\n\n/**\n * Hono `getPath` implementation: returns the locale-stripped path used for\n * route matching. Apply on every Hono router that dispatches the public\n * routes (the standalone public app and the adapter's top-level router).\n */\nexport function localePrefixGetPath(req: Request): string {\n return stripLocalePrefix(getPathname(req.url)).path;\n}\n\n/**\n * Extract the locale token from a request URL's path prefix, or `undefined`\n * when there is none. Used by route handlers to feed priority #2 into\n * {@link resolveRequestLocales}. Reads the original URL, which `getPath`\n * does not mutate.\n */\nexport function pathLocaleFromUrl(url: string): string | undefined {\n return stripLocalePrefix(getPathname(url)).locale;\n}\n","/**\n * Public-endpoint privacy filter for takuhon profile documents.\n *\n * Strips fields that the spec's privacy posture marks as opt-in for public\n * exposure, before the response reaches a public reader. The filter is\n * deliberately conservative: when `meta.privacy` is absent the most\n * restrictive interpretation applies (everything sensitive is hidden), and\n * the operator must explicitly opt into disclosure by setting the relevant\n * flag to `false`.\n *\n * Fields filtered:\n *\n * - `certifications[*].credentialId` — hidden when\n * `meta.privacy.hideCredentialIds !== false` (default true).\n * - `education[*].grade` — hidden when\n * `meta.privacy.hideEducationGrades !== false` (default true).\n * - `contact.email` — hidden when `contact.showEmail !== true`. The\n * documented contract has required this since the original `Contact`\n * shape but the 0.1.x runtime never actually applied the filter; bringing\n * it under the same helper in 0.2.0 closes that drift.\n *\n * `patents[*].patentNumber` is **not** filtered. Patent numbers are public\n * records (issued patents are published by the granting office) and Spec\n * §6.21 explicitly excludes them from the privacy block.\n *\n * Behavior:\n *\n * - Pure function. The input is never mutated; a shallow-copied result is\n * returned with only the touched arrays / objects replaced.\n * - When no filter applies (every flag opts into disclosure), the original\n * reference is returned as-is so callers can compare by identity.\n * - Admin endpoints (`/api/admin/*`, including `/api/admin/export`) MUST NOT\n * call this helper — they always serve the full document to authenticated\n * callers.\n */\n\nimport type { LocalizedTakuhon, Takuhon } from '@takuhon/core';\n\n/**\n * Union of the two profile shapes that traverse the API response path. The\n * fields the filter touches (`certifications[*].credentialId`,\n * `education[*].grade`, `contact.email`) are structurally identical\n * between {@link Takuhon} and {@link LocalizedTakuhon}, so the same logic\n * applies to either shape.\n */\ntype FilterableProfile = Takuhon | LocalizedTakuhon;\n\n/**\n * Return a privacy-filtered copy of `profile` suitable for public responses.\n *\n * @param profile Either a raw {@link Takuhon} (as served by `/takuhon.json`)\n * or a locale-resolved {@link LocalizedTakuhon} (as served\n * by `/api/profile` and `/api/jsonld`). The output preserves\n * the input's exact shape.\n */\nexport function applyPublicPrivacyFilter<T extends FilterableProfile>(profile: T): T {\n const hideCredentialIds = profile.meta.privacy?.hideCredentialIds !== false;\n const hideEducationGrades = profile.meta.privacy?.hideEducationGrades !== false;\n const allowEmail = profile.contact.showEmail === true;\n\n const stripCertifications = hideCredentialIds && hasAnyCredentialId(profile);\n const stripEducation = hideEducationGrades && hasAnyGrade(profile);\n const stripEmail = !allowEmail && profile.contact.email !== undefined;\n\n if (!stripCertifications && !stripEducation && !stripEmail) {\n return profile;\n }\n\n const out: FilterableProfile = { ...profile };\n\n if (stripCertifications) {\n out.certifications = profile.certifications.map(stripCredentialId) as T['certifications'];\n }\n\n if (stripEducation) {\n out.education = profile.education.map(stripGrade) as T['education'];\n }\n\n if (stripEmail) {\n const { email: _omit, ...rest } = profile.contact;\n out.contact = rest;\n }\n\n return out as T;\n}\n\nfunction hasAnyCredentialId(profile: FilterableProfile): boolean {\n return profile.certifications.some((c) => c.credentialId !== undefined);\n}\n\nfunction hasAnyGrade(profile: FilterableProfile): boolean {\n return profile.education.some((e) => e.grade !== undefined);\n}\n\nfunction stripCredentialId<T extends { credentialId?: string }>(item: T): T {\n if (item.credentialId === undefined) return item;\n const { credentialId: _omit, ...rest } = item;\n return rest as T;\n}\n\nfunction stripGrade<T extends { grade?: string }>(item: T): T {\n if (item.grade === undefined) return item;\n const { grade: _omit, ...rest } = item;\n return rest as T;\n}\n","import {\n ConflictError,\n NotFoundError,\n exportTakuhon,\n normalize,\n validate,\n type Takuhon,\n type TakuhonStorage,\n type ValidationError,\n} from '@takuhon/core';\nimport { Hono } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse, type ProblemFieldError } from '../error-envelope.js';\n\nimport type { AuditLogger } from './audit-logger.js';\nimport { bearerMiddleware, getActorTokenHash } from './bearer.js';\nimport type { CachePurger } from './cache-purger.js';\nimport { originMiddleware } from './origin.js';\n\n/**\n * Defense-in-depth CSP for JSON-only responses. Nothing renders here, so\n * `'none'` everywhere is the strongest stance the spec leaves room for.\n */\nconst ADMIN_API_CSP = [\"default-src 'none'\", \"frame-ancestors 'none'\", \"base-uri 'none'\"].join(\n '; ',\n);\n\nexport interface AdminApiAppDeps {\n storage: TakuhonStorage;\n /** Returns the configured admin token, or undefined if no secret is set. */\n getAdminToken: () => string | undefined;\n /** Allowlist of origins permitted for browser-originating admin requests. */\n getAdminOrigins: () => string[];\n cachePurger: CachePurger;\n auditLogger: AuditLogger;\n}\n\n/**\n * Map a core `ValidationError` to the RFC 7807 field-error shape. The leading\n * `#` produces a JSON Schema-style fragment reference (`#/profile/...`) that\n * matches the example in `api.md §5`.\n */\nfunction toFieldError(e: ValidationError): ProblemFieldError {\n return { path: `#${e.pointer}`, message: e.message };\n}\n\n/** Strip RFC 7232 double-quote delimiters from an `If-Match` header value. */\nfunction stripETag(raw: string): string {\n const trimmed = raw.trim();\n if (trimmed.startsWith('\"') && trimmed.endsWith('\"') && trimmed.length >= 2) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/**\n * Hono factory for `/api/admin/profile`. Mounted by adapters at `/api/admin`\n * (so the sub-app sees `/profile` as the route path).\n */\nexport function createAdminApiApp(deps: AdminApiAppDeps): Hono {\n const app = new Hono();\n\n app.use('*', async (c, next) => {\n await next();\n const h = c.res.headers;\n h.set('strict-transport-security', 'max-age=63072000; includeSubDomains; preload');\n h.set('x-content-type-options', 'nosniff');\n h.set('x-frame-options', 'DENY');\n h.set('referrer-policy', 'strict-origin-when-cross-origin');\n h.set('permissions-policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');\n h.set('content-security-policy', ADMIN_API_CSP);\n h.set('cache-control', 'private, no-store');\n });\n\n app.use('*', originMiddleware({ getAdminOrigins: deps.getAdminOrigins }));\n app.use(\n '*',\n bearerMiddleware({ getAdminToken: deps.getAdminToken, auditLogger: deps.auditLogger }),\n );\n\n app.onError((err, c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.internal,\n status: 500,\n title: 'Internal Error',\n detail: err instanceof Error ? err.message : 'Unknown failure',\n }),\n );\n\n app.notFound((c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: `No admin route matches ${new URL(c.req.url).pathname}.`,\n }),\n );\n\n app.put('/profile', async (c) => {\n const contentType = c.req.header('content-type') ?? '';\n if (!contentType.toLowerCase().startsWith('application/json')) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.unsupportedMediaType,\n status: 415,\n title: 'Unsupported Media Type',\n detail: `Content-Type must be application/json (got \"${contentType}\").`,\n });\n }\n\n let parsed: unknown;\n try {\n parsed = await c.req.json();\n } catch {\n return problemResponse(c, {\n slug: ERROR_SLUGS.badRequest,\n status: 400,\n title: 'Bad Request',\n detail: 'Request body is not valid JSON.',\n });\n }\n\n const result = validate(parsed);\n if (!result.ok) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.validationFailed,\n status: 422,\n title: 'Validation Failed',\n detail: `Schema validation failed (${String(result.errors.length)} error(s)).`,\n errors: result.errors.map(toFieldError),\n });\n }\n\n const data: Takuhon = result.data;\n const ifMatchRaw = c.req.header('if-match');\n const ifMatch = ifMatchRaw !== undefined ? stripETag(ifMatchRaw) : undefined;\n\n let saved: { version: string };\n try {\n saved = await deps.storage.saveProfile(data, ifMatch);\n } catch (e) {\n if (e instanceof ConflictError) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.conflict,\n status: 409,\n title: 'Conflict',\n detail: 'Stored profile version does not match If-Match.',\n currentVersion: e.currentVersion,\n });\n }\n throw e;\n }\n\n await deps.cachePurger.profileUpdated();\n\n const updatedAt = new Date().toISOString();\n const tokenHash = await getActorTokenHash(c);\n deps.auditLogger({\n type: 'admin.profile.update',\n timestamp: updatedAt,\n actor: { tokenHash },\n request: {\n method: 'PUT',\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n },\n result: { status: 200, version: saved.version },\n });\n\n return c.json({\n data: normalize(data),\n meta: {\n schemaVersion: data.schemaVersion,\n version: saved.version,\n updatedAt,\n },\n });\n });\n\n app.delete('/profile', async (c) => {\n await deps.storage.deleteProfile();\n await deps.cachePurger.profileDeleted();\n\n const tokenHash = await getActorTokenHash(c);\n deps.auditLogger({\n type: 'admin.profile.delete',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: {\n method: 'DELETE',\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n },\n result: { status: 204 },\n });\n\n return c.body(null, 204);\n });\n\n app.get('/export', async (c) => {\n let stored: { data: Takuhon; version: string };\n try {\n stored = await deps.storage.getProfile();\n } catch (e) {\n if (e instanceof NotFoundError) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: 'No profile has been saved yet; there is nothing to export.',\n });\n }\n throw e;\n }\n\n // Token holders receive the full document: the public privacy filter is\n // intentionally bypassed here (Spec §6.21). `exportTakuhon` with\n // `updateTimestamp: false` returns the stored document verbatim (raw\n // transport form, no envelope), so the body round-trips with\n // `importTakuhon` and preserves the real `meta.updatedAt`.\n const exported = exportTakuhon(stored.data, { updateTimestamp: false });\n\n const tokenHash = await getActorTokenHash(c);\n deps.auditLogger({\n type: 'admin.profile.export',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: {\n method: 'GET',\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n },\n result: { status: 200 },\n });\n\n return c.json(exported);\n });\n\n app.on(['POST', 'PATCH'], '/profile', (c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.methodNotAllowed,\n status: 405,\n title: 'Method Not Allowed',\n detail: `${c.req.method} ${new URL(c.req.url).pathname} is not supported on the admin app.`,\n }),\n );\n\n return app;\n}\n","import type { Context, Next } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse } from '../error-envelope.js';\n\nimport type { AuditLogger } from './audit-logger.js';\n\n/**\n * Constant-time byte comparison.\n *\n * Iterates over `max(len(a), len(b))` bytes so wall-clock cost is independent\n * of *where* a mismatch occurs and of length differences (within the same\n * length class). Length-mismatch is folded into the accumulator so the\n * boolean result still discriminates correctly.\n *\n * This is intentionally a from-scratch implementation rather than\n * `crypto.subtle.timingSafeEqual`, which Cloudflare Workers exposes but\n * Node lacks — `@takuhon/api` is adapter-neutral.\n */\nexport function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {\n const len = a.length > b.length ? a.length : b.length;\n let diff = a.length === b.length ? 0 : 1;\n for (let i = 0; i < len; i++) {\n const ai = a[i] ?? 0;\n const bi = b[i] ?? 0;\n diff |= ai ^ bi;\n }\n return diff === 0;\n}\n\n/** SHA-256 hex digest (lowercase) over a UTF-8 string. */\nexport async function sha256Hex(input: string): Promise<string> {\n const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));\n const bytes = new Uint8Array(buf);\n let out = '';\n for (const b of bytes) {\n out += b.toString(16).padStart(2, '0');\n }\n return out;\n}\n\n/**\n * Extracts the Bearer token from the request and returns a stable\n * `sha256:<hex>` digest used as the actor identity in audit logs. Returns\n * `sha256:absent` when no token is present.\n */\nexport async function getActorTokenHash(c: Context): Promise<string> {\n const header = c.req.header('authorization') ?? '';\n const m = /^Bearer\\s+(.+)$/i.exec(header);\n const token = m?.[1] ?? '';\n if (token === '') return 'sha256:absent';\n return `sha256:${await sha256Hex(token)}`;\n}\n\nexport interface BearerMiddlewareOptions {\n /**\n * Source of the expected admin token. Returns `undefined` when the deploy\n * has not provisioned a secret — in that case every request is rejected,\n * mirroring \"no admin access\" semantics.\n */\n getAdminToken: () => string | undefined;\n auditLogger: AuditLogger;\n}\n\n/**\n * Hono middleware that gates downstream handlers on a constant-time Bearer\n * token check. Emits an audit event for both success and failure.\n */\nexport function bearerMiddleware(opts: BearerMiddlewareOptions) {\n return async (c: Context, next: Next): Promise<Response | void> => {\n const expected = opts.getAdminToken();\n const header = c.req.header('authorization') ?? '';\n const match = /^Bearer\\s+(.+)$/i.exec(header);\n const presented = match?.[1];\n\n const isOk =\n expected !== undefined &&\n presented !== undefined &&\n constantTimeEqual(new TextEncoder().encode(presented), new TextEncoder().encode(expected));\n\n const tokenHash = await getActorTokenHash(c);\n const baseRequest = {\n method: c.req.method,\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n };\n\n if (!isOk) {\n opts.auditLogger({\n type: 'admin.auth.failure',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: baseRequest,\n result: { status: 401 },\n });\n return problemResponse(c, {\n slug: ERROR_SLUGS.unauthorized,\n status: 401,\n title: 'Unauthorized',\n detail: 'Bearer token missing or invalid.',\n });\n }\n\n opts.auditLogger({\n type: 'admin.auth.success',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: baseRequest,\n result: { status: 200 },\n });\n\n await next();\n };\n}\n","import type { Context, Next } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse } from '../error-envelope.js';\n\nexport interface OriginMiddlewareOptions {\n /**\n * Returns the allowlist of admin Origins (e.g.\n * `['https://admin.example.com']`). Empty list disables the check —\n * deploys are expected to populate the env before going to production,\n * with the trade-off documented in the adapter README.\n */\n getAdminOrigins: () => string[];\n}\n\n/**\n * Hono middleware that enforces a same-origin / allowlisted-origin policy\n * when configured. Requests without an `Origin` header (curl, server-to-\n * server, native apps) are allowed through; the Bearer token is the primary\n * auth boundary and the absence of `Origin` is itself an indicator that the\n * request did not originate from a browser CSRF context.\n */\nexport function originMiddleware(opts: OriginMiddlewareOptions) {\n return async (c: Context, next: Next): Promise<Response | void> => {\n const allow = opts.getAdminOrigins();\n if (allow.length === 0) {\n await next();\n return;\n }\n const origin = c.req.header('origin');\n if (origin !== undefined && !allow.includes(origin)) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.forbidden,\n status: 403,\n title: 'Forbidden',\n detail: `Origin ${origin} is not in the admin allowlist.`,\n });\n }\n await next();\n };\n}\n","import { Hono } from 'hono';\n\nimport { renderAdminHtml } from './admin-html.js';\n\n/**\n * Per-request CSP (security.md §1.3). Differs from the public CSP:\n * - `img-src` drops `data:` and adds `blob:` for client-side previews.\n * - `style-src` and `script-src` drop `unsafe-inline` and pin a nonce.\n * - `require-trusted-types-for 'script'` blocks DOM-XSS sinks.\n */\nfunction adminCsp(nonce: string): string {\n return [\n \"default-src 'self'\",\n \"img-src 'self' blob:\",\n `style-src 'self' 'nonce-${nonce}'`,\n `script-src 'self' 'nonce-${nonce}'`,\n \"font-src 'self'\",\n \"connect-src 'self'\",\n \"frame-ancestors 'none'\",\n \"base-uri 'self'\",\n \"form-action 'self'\",\n \"require-trusted-types-for 'script'\",\n 'upgrade-insecure-requests',\n ].join('; ');\n}\n\nfunction generateNonce(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n let bin = '';\n for (const b of bytes) {\n bin += String.fromCharCode(b);\n }\n return btoa(bin);\n}\n\n/**\n * Hono factory for the `/admin` HTML editor. Mounted by adapters at\n * `/admin` (so the sub-app sees `/` as its root path). Each request gets a\n * freshly-generated nonce shared between the CSP header and the inline\n * `<style>` / `<script>` tags.\n */\nexport function createAdminUiApp(): Hono {\n const app = new Hono();\n\n app.get('/', (c) => {\n const nonce = generateNonce();\n c.header('strict-transport-security', 'max-age=63072000; includeSubDomains; preload');\n c.header('x-content-type-options', 'nosniff');\n c.header('x-frame-options', 'DENY');\n c.header('referrer-policy', 'strict-origin-when-cross-origin');\n c.header('permissions-policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');\n c.header('content-security-policy', adminCsp(nonce));\n c.header('cache-control', 'private, no-store');\n return c.html(renderAdminHtml(nonce));\n });\n\n return app;\n}\n","/**\n * Inline HTML for the minimal admin editor served at `GET /admin`.\n *\n * Single-page, no build step: a token input, a JSON textarea preloaded from\n * `/takuhon.json`, Save (PUT) and Delete (DELETE) buttons. The page operates\n * under a strict CSP (`script-src 'self' 'nonce-<n>'`,\n * `style-src 'self' 'nonce-<n>'`, `require-trusted-types-for 'script'`), so\n * both the inline `<script>` and `<style>` blocks carry the request-scoped\n * nonce. We avoid `innerHTML`/`eval` so Trusted Types is non-disruptive.\n */\nexport function renderAdminHtml(nonce: string): string {\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>takuhon admin</title>\n<style nonce=\"${nonce}\">\nbody { font-family: system-ui, -apple-system, sans-serif; max-width: 960px; margin: 2rem auto; padding: 0 1rem; color: #222; }\nh1 { font-size: 1.5rem; }\np.note { color: #555; }\nlabel { display: block; margin: 1rem 0 0.25rem; font-weight: 600; }\ninput[type=password], textarea { width: 100%; box-sizing: border-box; padding: 0.5rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9rem; border: 1px solid #999; border-radius: 4px; }\ntextarea { min-height: 24rem; }\n.row { display: flex; gap: 0.5rem; margin-top: 1rem; }\nbutton { padding: 0.5rem 1rem; font-size: 0.95rem; border: 1px solid #444; background: #fafafa; border-radius: 4px; cursor: pointer; }\nbutton.danger { border-color: #b03; color: #b03; background: #fff5f5; }\n#status { margin-top: 1rem; padding: 0.75rem; border-radius: 4px; white-space: pre-wrap; word-break: break-word; }\n#status.ok { background: #e6f4ea; color: #1b5e20; border: 1px solid #82c891; }\n#status.err { background: #fdecea; color: #b71c1c; border: 1px solid #ef9a9a; }\nsmall.version { color: #555; }\n</style>\n</head>\n<body>\n<h1>takuhon admin</h1>\n<p class=\"note\">Edit the full <code>takuhon.json</code> document and Save. Optimistic locking via <code>If-Match</code> guards concurrent edits; the token is never sent over the URL.</p>\n<label for=\"token\">Admin token</label>\n<input id=\"token\" type=\"password\" autocomplete=\"off\" spellcheck=\"false\">\n<label for=\"payload\">takuhon.json <small class=\"version\" id=\"versionLabel\"></small></label>\n<textarea id=\"payload\" spellcheck=\"false\" autocapitalize=\"off\" autocomplete=\"off\"></textarea>\n<div class=\"row\">\n <button id=\"save\" type=\"button\">Save</button>\n <button id=\"delete\" type=\"button\" class=\"danger\">Delete profile</button>\n <button id=\"reload\" type=\"button\">Reload current</button>\n</div>\n<div id=\"status\" hidden></div>\n<script nonce=\"${nonce}\">\n(function () {\n var tokenEl = document.getElementById('token');\n var payloadEl = document.getElementById('payload');\n var versionEl = document.getElementById('versionLabel');\n var statusEl = document.getElementById('status');\n var ifMatch = '';\n\n function setStatus(message, ok) {\n statusEl.textContent = message;\n statusEl.className = ok ? 'ok' : 'err';\n statusEl.hidden = false;\n }\n function setVersion(etag) {\n ifMatch = etag || '';\n versionEl.textContent = ifMatch ? '(current version: ' + ifMatch + ')' : '(no stored version)';\n }\n function getToken() {\n var t = tokenEl.value.trim();\n if (!t) { setStatus('Admin token is required.', false); return null; }\n return t;\n }\n async function loadCurrent() {\n try {\n var res = await fetch('/takuhon.json', { cache: 'no-store' });\n if (!res.ok) { setStatus('Failed to load /takuhon.json: ' + res.status, false); return; }\n setVersion(res.headers.get('etag'));\n var json = await res.json();\n payloadEl.value = JSON.stringify(json, null, 2);\n setStatus('Loaded current profile.', true);\n } catch (e) {\n setStatus('Network error loading current profile: ' + (e && e.message ? e.message : String(e)), false);\n }\n }\n async function save() {\n var token = getToken();\n if (!token) return;\n var body;\n try {\n body = JSON.parse(payloadEl.value);\n } catch (e) {\n setStatus('JSON parse error: ' + (e && e.message ? e.message : String(e)), false);\n return;\n }\n var headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token };\n if (ifMatch) headers['If-Match'] = ifMatch;\n try {\n var res = await fetch('/api/admin/profile', { method: 'PUT', headers: headers, body: JSON.stringify(body) });\n var json = await res.json().catch(function () { return null; });\n if (res.ok && json && json.meta && json.meta.version) {\n setVersion('\"' + json.meta.version + '\"');\n setStatus('Saved. New version: ' + json.meta.version, true);\n } else {\n setStatus('Save failed (' + res.status + '): ' + (json ? JSON.stringify(json, null, 2) : 'no body'), false);\n }\n } catch (e) {\n setStatus('Network error during save: ' + (e && e.message ? e.message : String(e)), false);\n }\n }\n async function deleteProfile() {\n var token = getToken();\n if (!token) return;\n if (!confirm('Delete the profile? The bundled onboarding fixture will be shown until you save a new document.')) return;\n var headers = { 'Authorization': 'Bearer ' + token };\n try {\n var res = await fetch('/api/admin/profile', { method: 'DELETE', headers: headers });\n if (res.ok) {\n payloadEl.value = '';\n setVersion('');\n setStatus('Deleted.', true);\n } else {\n var json = await res.json().catch(function () { return null; });\n setStatus('Delete failed (' + res.status + '): ' + (json ? JSON.stringify(json, null, 2) : 'no body'), false);\n }\n } catch (e) {\n setStatus('Network error during delete: ' + (e && e.message ? e.message : String(e)), false);\n }\n }\n\n document.getElementById('save').addEventListener('click', save);\n document.getElementById('delete').addEventListener('click', deleteProfile);\n document.getElementById('reload').addEventListener('click', loadCurrent);\n loadCurrent();\n})();\n</script>\n</body>\n</html>\n`;\n}\n","/**\n * Structured audit-log emitter for admin actions (per security.md §5).\n *\n * Phase 3.4 covers the auth + profile-write events. Asset events\n * (`admin.asset.upload`, `admin.asset.delete`) join the union when Phase 3.5\n * lands. Adapters bind a concrete sink (Cloudflare uses `console.log`, which\n * Workers Tail / Logpush captures); tests inject a `vi.fn()` recorder.\n */\nexport type AuditEventType =\n | 'admin.auth.success'\n | 'admin.auth.failure'\n | 'admin.profile.update'\n | 'admin.profile.delete'\n | 'admin.profile.export'\n | 'admin.cache.purge';\n\nexport interface AuditEvent {\n type: AuditEventType;\n /** ISO-8601 UTC timestamp generated at the call site. */\n timestamp: string;\n /**\n * Actor identity. `tokenHash` is `sha256:<hex>` over the presented Bearer\n * token, or `sha256:absent` when no token was supplied. The raw token is\n * never logged.\n */\n actor?: { tokenHash: string };\n request: {\n method: string;\n path: string;\n /** Originating client IP from `cf-connecting-ip`; undefined off-Cloudflare. */\n ip?: string;\n };\n result: {\n status: number;\n /** Opaque storage version emitted by `TakuhonStorage.saveProfile`. */\n version?: string;\n };\n}\n\nexport type AuditLogger = (event: AuditEvent) => void;\n\n/** Default sink that discards events; useful for tests and bare runtimes. */\nexport const noopAuditLogger: AuditLogger = () => {\n /* no-op */\n};\n","/**\n * Cache invalidation contract for admin write paths.\n *\n * `@takuhon/api` stays adapter-neutral, so the actual edge-cache calls live\n * in each adapter (Cloudflare's `caches.default.delete`, future runtimes'\n * equivalents). Tests inject a recording implementation to assert that the\n * admin handlers fire the right purge after a successful write.\n */\nexport interface CachePurger {\n /** Called after a successful `PUT /api/admin/profile`. */\n profileUpdated(): Promise<void>;\n /** Called after a successful `DELETE /api/admin/profile`. */\n profileDeleted(): Promise<void>;\n}\n\n/**\n * No-op default: callers that don't run on an edge cache (Node dev server,\n * unit tests that don't care about purge calls) can pass this in.\n */\nexport const noopCachePurger: CachePurger = {\n profileUpdated: () => Promise.resolve(),\n profileDeleted: () => Promise.resolve(),\n};\n"],"mappings":"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","names":["NotFoundError","normalize","Hono","Hono","normalize","NotFoundError","Hono","Hono"]}
1
+ {"version":3,"sources":["../src/error-envelope.ts","../src/public-app.ts","../src/locale-resolution.ts","../src/locale-prefix.ts","../src/index.ts","../src/admin/admin-api-app.ts","../src/admin/bearer.ts","../src/admin/origin.ts","../src/admin/admin-ui-app.ts","../src/admin/admin-html.ts","../src/admin/audit-logger.ts","../src/admin/cache-purger.ts"],"sourcesContent":["import type { Context } from 'hono';\nimport type { ContentfulStatusCode } from 'hono/utils/http-status';\n\n/**\n * RFC 7807 problem type slugs used by takuhon. The 11 below are the\n * Spec-defined values (api.md §5.1); `methodNotAllowed` is added locally\n * for the 405 path that the Spec leaves unnamed.\n */\nexport const ERROR_SLUGS = {\n badRequest: 'bad-request',\n unauthorized: 'unauthorized',\n forbidden: 'forbidden',\n notFound: 'not-found',\n methodNotAllowed: 'method-not-allowed',\n conflict: 'conflict',\n payloadTooLarge: 'payload-too-large',\n unsupportedMediaType: 'unsupported-media-type',\n validationFailed: 'validation-failed',\n tooManyRequests: 'too-many-requests',\n internal: 'internal',\n serviceUnavailable: 'service-unavailable',\n} as const;\n\nexport type ErrorSlug = (typeof ERROR_SLUGS)[keyof typeof ERROR_SLUGS];\n\nconst TYPE_BASE = 'https://takuhon.org/errors';\n\nexport interface ProblemFieldError {\n path: string;\n message: string;\n}\n\nexport interface ProblemDetails {\n type: string;\n title: string;\n status: number;\n detail: string;\n instance: string;\n errors?: ProblemFieldError[];\n currentVersion?: string;\n}\n\nexport interface BuildProblemInput {\n slug: ErrorSlug;\n status: number;\n title: string;\n detail: string;\n instance: string;\n errors?: ProblemFieldError[];\n currentVersion?: string;\n}\n\nexport function buildProblem(input: BuildProblemInput): ProblemDetails {\n const out: ProblemDetails = {\n type: `${TYPE_BASE}/${input.slug}`,\n title: input.title,\n status: input.status,\n detail: input.detail,\n instance: input.instance,\n };\n if (input.errors !== undefined) out.errors = input.errors;\n if (input.currentVersion !== undefined) out.currentVersion = input.currentVersion;\n return out;\n}\n\nexport interface ProblemResponseInput {\n slug: ErrorSlug;\n status: ContentfulStatusCode;\n title: string;\n detail: string;\n errors?: ProblemFieldError[];\n currentVersion?: string;\n}\n\nexport function problemResponse(c: Context, input: ProblemResponseInput): Response {\n const body = buildProblem({\n slug: input.slug,\n status: input.status,\n title: input.title,\n detail: input.detail,\n instance: new URL(c.req.url).pathname,\n errors: input.errors,\n currentVersion: input.currentVersion,\n });\n return c.body(JSON.stringify(body), input.status, {\n 'content-type': 'application/problem+json; charset=utf-8',\n });\n}\n","import {\n NotFoundError,\n SCHEMA_VERSION,\n applyPublicPrivacyFilter,\n generateJsonLd,\n normalize,\n resolveLocale,\n schema,\n type Takuhon,\n type TakuhonStorage,\n} from '@takuhon/core';\nimport { Hono } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse } from './error-envelope.js';\nimport { localePrefixGetPath, pathLocaleFromUrl } from './locale-prefix.js';\nimport { resolveRequestLocales } from './locale-resolution.js';\n\nexport interface PublicAppDeps {\n storage: TakuhonStorage;\n /**\n * Returned when storage reports NotFoundError. Adapters that ship a\n * bundled example fixture (e.g. @takuhon/cloudflare) pass a thunk that\n * returns the validated document so initial-onboarding requests still\n * succeed before the first admin write.\n */\n fallback?: () => Takuhon;\n}\n\nconst FALLBACK_VERSION = 'bundled-fixture';\n\nconst PUBLIC_CSP = [\n \"default-src 'self'\",\n \"img-src 'self' data:\",\n \"style-src 'self' 'unsafe-inline'\",\n \"script-src 'self'\",\n \"font-src 'self'\",\n \"connect-src 'self'\",\n \"frame-ancestors 'none'\",\n \"base-uri 'self'\",\n \"form-action 'self'\",\n 'upgrade-insecure-requests',\n].join('; ');\n\nasync function loadProfile(deps: PublicAppDeps): Promise<{ data: Takuhon; version: string }> {\n try {\n return await deps.storage.getProfile();\n } catch (e) {\n if (e instanceof NotFoundError && deps.fallback) {\n return { data: deps.fallback(), version: FALLBACK_VERSION };\n }\n throw e;\n }\n}\n\nexport function createPublicApp(deps: PublicAppDeps): Hono {\n // `getPath` strips a leading `/{locale}` prefix (e.g. `/ja/api/profile`\n // → `/api/profile`) so the flat routes below match locale-prefixed\n // URLs. The same function is applied on the adapter's top-level router,\n // because Hono's `route()` flattens this app's routes into the parent\n // and dispatches with the parent's `getPath` only — setting it here\n // alone would be honored for direct `app.fetch()` (tests) but not in\n // production. Handlers recover the locale token from the original URL\n // (`c.req.url`), which `getPath` does not mutate.\n const app = new Hono({ getPath: localePrefixGetPath });\n\n app.use('*', async (c, next) => {\n await next();\n const h = c.res.headers;\n h.set('strict-transport-security', 'max-age=63072000; includeSubDomains; preload');\n h.set('x-content-type-options', 'nosniff');\n h.set('x-frame-options', 'DENY');\n h.set('referrer-policy', 'strict-origin-when-cross-origin');\n h.set('permissions-policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');\n h.set('content-security-policy', PUBLIC_CSP);\n });\n\n app.onError((err, c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.internal,\n status: 500,\n title: 'Internal Error',\n detail: err instanceof Error ? err.message : 'Unknown failure',\n }),\n );\n\n app.notFound((c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: `No route matches ${new URL(c.req.url).pathname}.`,\n }),\n );\n\n app.get('/', (c) => c.text('takuhon — visit /api/profile or /api/schema\\n'));\n\n // Liveness probe. Intentionally storage-independent: it reports that the\n // worker itself is serving requests, not that the profile store is\n // reachable. A readiness probe that also checks storage can be added\n // later under a separate path if deployment platforms need it.\n app.get('/health', (c) => {\n c.header('cache-control', 'no-store');\n return c.json({ status: 'ok', schemaVersion: SCHEMA_VERSION });\n });\n\n app.get('/api/profile', async (c) => {\n const { data, version } = await loadProfile(deps);\n const { locale, fallbackLocale } = resolveRequestLocales(\n c,\n data.settings.availableLocales,\n pathLocaleFromUrl(c.req.url),\n );\n const localized = applyPublicPrivacyFilter(\n resolveLocale(normalize(data), locale, fallbackLocale),\n );\n const body = {\n data: localized,\n meta: {\n schemaVersion: localized.schemaVersion,\n locale: localized.resolvedLocale,\n updatedAt: localized.meta.updatedAt,\n },\n };\n c.header('etag', `\"${version}\"`);\n c.header('cache-control', 'private, max-age=300');\n c.header('vary', 'Accept-Language, Cookie');\n return c.json(body);\n });\n\n app.get('/api/schema', (c) => c.json(schema));\n\n app.get('/api/jsonld', async (c) => {\n const { data, version } = await loadProfile(deps);\n const { locale, fallbackLocale } = resolveRequestLocales(\n c,\n data.settings.availableLocales,\n pathLocaleFromUrl(c.req.url),\n );\n const localized = applyPublicPrivacyFilter(\n resolveLocale(normalize(data), locale, fallbackLocale),\n );\n const ld = generateJsonLd(localized);\n c.header('etag', `\"${version}\"`);\n c.header('cache-control', 'private, max-age=300');\n c.header('vary', 'Accept-Language, Cookie');\n c.header('content-type', 'application/ld+json; charset=utf-8');\n return c.body(JSON.stringify(ld));\n });\n\n app.get('/takuhon.json', async (c) => {\n const { data, version } = await loadProfile(deps);\n const filtered = applyPublicPrivacyFilter(data);\n c.header('etag', `\"${version}\"`);\n c.header('cache-control', 'public, max-age=300');\n return c.json(filtered);\n });\n\n app.get('/.well-known/takuhon.json', (c) => {\n c.header('cache-control', 'public, max-age=3600');\n return c.json({\n schemaVersion: SCHEMA_VERSION,\n schemaUrl: '/api/schema',\n profile: '/api/profile',\n jsonld: '/api/jsonld',\n export: '/api/admin/export',\n canonical: '/takuhon.json',\n });\n });\n\n app.on(['POST', 'PUT', 'PATCH', 'DELETE'], '*', (c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.methodNotAllowed,\n status: 405,\n title: 'Method Not Allowed',\n detail: `${c.req.method} ${new URL(c.req.url).pathname} is not supported on the public app.`,\n }),\n );\n\n return app;\n}\n","/**\n * HTTP-layer locale resolution for the public app.\n *\n * Reads request-side locale candidates in this priority order:\n *\n * 1. `?lang=` query parameter\n * 2. URL path prefix (e.g. `/ja/`), passed in as `pathLocale`\n * 3. `takuhon_locale` cookie\n * 4. `Accept-Language` request header (q-value ordered)\n *\n * The URL-path candidate is extracted structurally by\n * `locale-prefix.ts` (`stripLocalePrefix` / `pathLocaleFromUrl`) and\n * handed to {@link resolveRequestLocales} as `pathLocale`; this module\n * does not parse the path itself. Settings-tier fallbacks\n * (`settings.defaultLocale`, `settings.fallbackLocale`,\n * `settings.availableLocales[0]`) are resolved inside `@takuhon/core`'s\n * `resolveLocale` and do not appear here.\n *\n * `resolveLocale` only exposes two caller slots (`locale`,\n * `fallbackLocale`). To avoid wasting them on tags the document can't\n * serve, candidates are filtered against `availableLocales` (case-\n * insensitive on the full tag or its primary subtag) and the matched\n * available locale token is substituted before forwarding, so a\n * primary-subtag match like `en` → `en-US` does not silently fall\n * through to the settings tier. Filtered candidates beyond the second\n * fall through to `resolveLocale`'s own settings-tier candidates, not\n * in request order — an acceptable loss given the contract.\n */\nimport type { Context } from 'hono';\nimport { getCookie } from 'hono/cookie';\n\nconst BCP47 = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/;\n\n// DoS guards. The header parser is exposed to untrusted client input,\n// so the byte budget and entry count are bounded before any per-token\n// work. Numbers are conservative defaults, not spec-derived.\nconst ACCEPT_LANG_MAX = 2048;\nconst ACCEPT_LANG_MAX_ENTRIES = 16;\nconst COOKIE_VALUE_MAX = 64;\n\nexport function isValidBcp47(tag: string): boolean {\n return BCP47.test(tag);\n}\n\ninterface AcceptLangEntry {\n readonly tag: string;\n readonly q: number;\n}\n\n/**\n * Parse an `Accept-Language` header into BCP-47 tags ordered by q\n * descending. Invalid or zero-quality entries and `*` wildcards are\n * dropped. Input larger than {@link ACCEPT_LANG_MAX} bytes or with more\n * than {@link ACCEPT_LANG_MAX_ENTRIES} comma-separated parts is\n * truncated before parsing.\n */\nexport function parseAcceptLanguage(header: string | null | undefined): string[] {\n if (!header) return [];\n const trimmed = header.length > ACCEPT_LANG_MAX ? header.slice(0, ACCEPT_LANG_MAX) : header;\n const parts = trimmed.split(',').slice(0, ACCEPT_LANG_MAX_ENTRIES);\n\n const entries: AcceptLangEntry[] = [];\n for (const rawPart of parts) {\n const segments = rawPart.split(';');\n const tagSegment = segments[0];\n if (tagSegment === undefined) continue;\n const tag = tagSegment.trim();\n if (tag === '' || tag === '*') continue;\n if (!isValidBcp47(tag)) continue;\n\n let q = 1;\n for (let i = 1; i < segments.length; i++) {\n const segment = segments[i];\n if (segment === undefined) continue;\n const match = /^\\s*q\\s*=\\s*([0-9.]+)\\s*$/i.exec(segment);\n if (!match) continue;\n const parsed = Number.parseFloat(match[1] ?? '');\n if (Number.isNaN(parsed)) {\n q = 1;\n } else if (parsed < 0 || parsed > 1) {\n // RFC 7231 §5.3.1 says values MUST be in [0, 1]; treat\n // out-of-range as missing (q=1) rather than dropping.\n q = 1;\n } else {\n q = parsed;\n }\n break;\n }\n if (q === 0) continue;\n\n entries.push({ tag, q });\n }\n\n // Stable sort: Array.prototype.sort is stable in ES2019+.\n entries.sort((a, b) => b.q - a.q);\n return entries.map((e) => e.tag);\n}\n\nfunction primarySubtag(tag: string): string {\n const dash = tag.indexOf('-');\n return (dash === -1 ? tag : tag.slice(0, dash)).toLowerCase();\n}\n\nfunction matchAvailable(tag: string, available: readonly string[]): string | undefined {\n const tagLower = tag.toLowerCase();\n const tagPrimary = primarySubtag(tag);\n // Prefer exact (case-insensitive) match over primary-subtag match so a\n // request that names a region explicitly wins over a region-stripped\n // alternative.\n for (const a of available) {\n if (a.toLowerCase() === tagLower) return a;\n }\n for (const a of available) {\n if (primarySubtag(a) === tagPrimary) return a;\n }\n return undefined;\n}\n\n/**\n * Resolve HTTP-layer locale candidates from the request — query, URL\n * path prefix, cookie, and `Accept-Language` in that priority order.\n * Returns the top two candidates that survive validation and the\n * `availableLocales` filter, after substituting the matched available\n * token so primary-subtag matches resolve correctly downstream.\n *\n * @param pathLocale The locale token extracted from a `/{locale}` path\n * prefix by `pathLocaleFromUrl`, or `undefined` when the request has\n * no path prefix. Inserted at priority #2 (after query, before\n * cookie).\n */\nexport function resolveRequestLocales(\n c: Context,\n available: readonly string[],\n pathLocale?: string,\n): { locale?: string; fallbackLocale?: string } {\n const raw: string[] = [];\n\n const query = c.req.query('lang');\n if (query !== undefined && isValidBcp47(query)) {\n raw.push(query);\n }\n\n if (pathLocale !== undefined && isValidBcp47(pathLocale)) {\n raw.push(pathLocale);\n }\n\n const cookie = getCookie(c, 'takuhon_locale');\n if (cookie !== undefined && cookie.length <= COOKIE_VALUE_MAX && isValidBcp47(cookie)) {\n raw.push(cookie);\n }\n\n const accept = c.req.header('accept-language');\n if (accept !== undefined) {\n raw.push(...parseAcceptLanguage(accept));\n }\n\n const seen = new Set<string>();\n const filtered: string[] = [];\n for (const tag of raw) {\n const matched = matchAvailable(tag, available);\n if (matched === undefined) continue;\n const key = matched.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n filtered.push(matched);\n if (filtered.length === 2) break;\n }\n\n const out: { locale?: string; fallbackLocale?: string } = {};\n if (filtered[0] !== undefined) out.locale = filtered[0];\n if (filtered[1] !== undefined) out.fallbackLocale = filtered[1];\n return out;\n}\n","/**\n * URL-path locale prefix handling for the public app.\n *\n * Implements locale resolution priority #2: a leading `/{locale}` path\n * segment, e.g. `/ja/api/profile`, ranked after the `?lang=` query (#1)\n * and before the `takuhon_locale` cookie (#3). This module is responsible\n * only for the *structural* concern — detecting and stripping the prefix\n * so the existing flat routes match — while the locale *value* it\n * extracts is fed into {@link resolveRequestLocales} at slot #2 by the\n * route handlers.\n *\n * The prefix is honored via Hono's `getPath` option rather than parametric\n * routes: {@link localePrefixGetPath} rewrites the match path so a request\n * to `/ja/api/profile` is routed to the `/api/profile` handler. Hono's\n * `route()` flattens a sub-app's routes into the parent and dispatches with\n * the *parent* router's `getPath` only, so the same function is applied on\n * both the standalone public app (for direct tests) and the adapter's\n * top-level router (production). The original request URL is untouched, so\n * handlers recover the locale token from `c.req.url`.\n */\nimport { isValidBcp47 } from './locale-resolution.js';\n\n/**\n * Remainder paths that may legitimately follow a `/{locale}` segment.\n *\n * This allowlist — NOT the BCP-47 shape check — is the load-bearing safety\n * mechanism. It keeps locale-agnostic paths (`/health`, `/api/schema`,\n * `/.well-known/*`, `/takuhon.json`) and admin paths (`/api/admin/*`,\n * `/admin/*`) from being misread as a locale prefix. Note that `api`\n * itself satisfies the BCP-47 primary-subtag shape (`[a-z]{2,3}`), so a\n * shape check alone would treat `/api/schema` as locale `api` + `/schema`;\n * the remainder allowlist is what prevents that.\n *\n * Keep this in sync with the locale-aware routes in `public-app.ts`.\n */\nexport const LOCALE_AWARE_REMAINDERS = ['/', '/api/profile', '/api/jsonld'] as const;\n\n/**\n * First-path segments that are reserved namespaces and must never be read\n * as a locale, even though they satisfy the BCP-47 shape. Without this,\n * a bare `/api` (segment `api` is a valid 2–3 letter primary subtag,\n * remainder defaults to the landing `/`) would alias the landing page\n * instead of 404ing. Other reserved roots (`admin`, `health`,\n * `takuhon.json`, `.well-known`) fail the BCP-47 shape check and need no\n * entry here; `api` is the only collision.\n */\nconst RESERVED_FIRST_SEGMENTS = new Set(['api']);\n\nfunction getPathname(url: string): string {\n return new URL(url).pathname;\n}\n\n/**\n * Split a leading `/{locale}` segment from `pathname` when — and only\n * when — the segment is BCP-47-shaped and the remainder is a locale-aware\n * route ({@link LOCALE_AWARE_REMAINDERS}).\n *\n * - `/ja/api/profile` → `{ locale: 'ja', path: '/api/profile' }`\n * - `/api/profile` → `{ path: '/api/profile' }` (remainder `/profile` not locale-aware)\n * - `/api/schema` → `{ path: '/api/schema' }` (the `api`-collision guard)\n * - `/ja/api/admin` → `{ path: '/ja/api/admin' }` (remainder `/api/admin` not locale-aware → 404, admin isolated)\n * - `/ja` and `/ja/` → `{ locale: 'ja', path: '/' }` (trailing slash normalized to landing)\n *\n * The returned `locale` is the raw path token; it is not matched against\n * `availableLocales` here (that happens downstream in\n * {@link resolveRequestLocales}), so an unknown-but-shaped prefix like\n * `/fr/` on an en/ja document strips structurally and then falls through\n * to the next resolution tier, mirroring `?lang=fr` semantics.\n */\nexport function stripLocalePrefix(pathname: string): { locale?: string; path: string } {\n // Match a leading single segment: `/seg` or `/seg/rest...`.\n const match = /^\\/([^/]+)(\\/.*)?$/.exec(pathname);\n if (match === null) return { path: pathname };\n\n const seg = match[1];\n if (seg === undefined || !isValidBcp47(seg)) return { path: pathname };\n\n // Reserved namespace segments (e.g. `api`) pass the BCP-47 shape but are\n // not locales; leave them for the route table (so `/api` 404s as before).\n if (RESERVED_FIRST_SEGMENTS.has(seg.toLowerCase())) return { path: pathname };\n\n // Normalize a bare `/ja` (no trailing content) to the landing remainder.\n const remainder = match[2] ?? '/';\n if (!(LOCALE_AWARE_REMAINDERS as readonly string[]).includes(remainder)) {\n return { path: pathname };\n }\n\n return { locale: seg, path: remainder };\n}\n\n/**\n * Hono `getPath` implementation: returns the locale-stripped path used for\n * route matching. Apply on every Hono router that dispatches the public\n * routes (the standalone public app and the adapter's top-level router).\n */\nexport function localePrefixGetPath(req: Request): string {\n return stripLocalePrefix(getPathname(req.url)).path;\n}\n\n/**\n * Extract the locale token from a request URL's path prefix, or `undefined`\n * when there is none. Used by route handlers to feed priority #2 into\n * {@link resolveRequestLocales}. Reads the original URL, which `getPath`\n * does not mutate.\n */\nexport function pathLocaleFromUrl(url: string): string | undefined {\n return stripLocalePrefix(getPathname(url)).locale;\n}\n","/**\n * @takuhon/api — Hono-based HTTP handlers and response builders for takuhon.\n *\n * Phase 3.3 introduced the public-app factory and the RFC 7807 envelope\n * helpers. Phase 3.4 adds the admin app factories (PUT/DELETE profile and\n * the inline `/admin` HTML editor) plus the `CachePurger` / `AuditLogger`\n * dependency-injection interfaces that adapters bind to a runtime.\n */\n\nexport {\n ERROR_SLUGS,\n buildProblem,\n problemResponse,\n type ErrorSlug,\n type ProblemDetails,\n type ProblemFieldError,\n type BuildProblemInput,\n type ProblemResponseInput,\n} from './error-envelope.js';\nexport { createPublicApp, type PublicAppDeps } from './public-app.js';\n// Re-exported from @takuhon/core (it is a pure transform over core types and\n// now lives there); kept here for backwards compatibility.\nexport { applyPublicPrivacyFilter } from '@takuhon/core';\nexport {\n LOCALE_AWARE_REMAINDERS,\n localePrefixGetPath,\n pathLocaleFromUrl,\n stripLocalePrefix,\n} from './locale-prefix.js';\n\nexport { createAdminApiApp, type AdminApiAppDeps } from './admin/admin-api-app.js';\nexport { createAdminUiApp } from './admin/admin-ui-app.js';\nexport {\n noopAuditLogger,\n type AuditEvent,\n type AuditEventType,\n type AuditLogger,\n} from './admin/audit-logger.js';\nexport { noopCachePurger, type CachePurger } from './admin/cache-purger.js';\n","import {\n ConflictError,\n NotFoundError,\n exportTakuhon,\n normalize,\n validate,\n type Takuhon,\n type TakuhonStorage,\n type ValidationError,\n} from '@takuhon/core';\nimport { Hono } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse, type ProblemFieldError } from '../error-envelope.js';\n\nimport type { AuditLogger } from './audit-logger.js';\nimport { bearerMiddleware, getActorTokenHash } from './bearer.js';\nimport type { CachePurger } from './cache-purger.js';\nimport { originMiddleware } from './origin.js';\n\n/**\n * Defense-in-depth CSP for JSON-only responses. Nothing renders here, so\n * `'none'` everywhere is the strongest stance the spec leaves room for.\n */\nconst ADMIN_API_CSP = [\"default-src 'none'\", \"frame-ancestors 'none'\", \"base-uri 'none'\"].join(\n '; ',\n);\n\nexport interface AdminApiAppDeps {\n storage: TakuhonStorage;\n /** Returns the configured admin token, or undefined if no secret is set. */\n getAdminToken: () => string | undefined;\n /** Allowlist of origins permitted for browser-originating admin requests. */\n getAdminOrigins: () => string[];\n cachePurger: CachePurger;\n auditLogger: AuditLogger;\n}\n\n/**\n * Map a core `ValidationError` to the RFC 7807 field-error shape. The leading\n * `#` produces a JSON Schema-style fragment reference (`#/profile/...`) that\n * matches the example in `api.md §5`.\n */\nfunction toFieldError(e: ValidationError): ProblemFieldError {\n return { path: `#${e.pointer}`, message: e.message };\n}\n\n/** Strip RFC 7232 double-quote delimiters from an `If-Match` header value. */\nfunction stripETag(raw: string): string {\n const trimmed = raw.trim();\n if (trimmed.startsWith('\"') && trimmed.endsWith('\"') && trimmed.length >= 2) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/**\n * Hono factory for `/api/admin/profile`. Mounted by adapters at `/api/admin`\n * (so the sub-app sees `/profile` as the route path).\n */\nexport function createAdminApiApp(deps: AdminApiAppDeps): Hono {\n const app = new Hono();\n\n app.use('*', async (c, next) => {\n await next();\n const h = c.res.headers;\n h.set('strict-transport-security', 'max-age=63072000; includeSubDomains; preload');\n h.set('x-content-type-options', 'nosniff');\n h.set('x-frame-options', 'DENY');\n h.set('referrer-policy', 'strict-origin-when-cross-origin');\n h.set('permissions-policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');\n h.set('content-security-policy', ADMIN_API_CSP);\n h.set('cache-control', 'private, no-store');\n });\n\n app.use('*', originMiddleware({ getAdminOrigins: deps.getAdminOrigins }));\n app.use(\n '*',\n bearerMiddleware({ getAdminToken: deps.getAdminToken, auditLogger: deps.auditLogger }),\n );\n\n app.onError((err, c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.internal,\n status: 500,\n title: 'Internal Error',\n detail: err instanceof Error ? err.message : 'Unknown failure',\n }),\n );\n\n app.notFound((c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: `No admin route matches ${new URL(c.req.url).pathname}.`,\n }),\n );\n\n app.put('/profile', async (c) => {\n const contentType = c.req.header('content-type') ?? '';\n if (!contentType.toLowerCase().startsWith('application/json')) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.unsupportedMediaType,\n status: 415,\n title: 'Unsupported Media Type',\n detail: `Content-Type must be application/json (got \"${contentType}\").`,\n });\n }\n\n let parsed: unknown;\n try {\n parsed = await c.req.json();\n } catch {\n return problemResponse(c, {\n slug: ERROR_SLUGS.badRequest,\n status: 400,\n title: 'Bad Request',\n detail: 'Request body is not valid JSON.',\n });\n }\n\n const result = validate(parsed);\n if (!result.ok) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.validationFailed,\n status: 422,\n title: 'Validation Failed',\n detail: `Schema validation failed (${String(result.errors.length)} error(s)).`,\n errors: result.errors.map(toFieldError),\n });\n }\n\n const data: Takuhon = result.data;\n const ifMatchRaw = c.req.header('if-match');\n const ifMatch = ifMatchRaw !== undefined ? stripETag(ifMatchRaw) : undefined;\n\n let saved: { version: string };\n try {\n saved = await deps.storage.saveProfile(data, ifMatch);\n } catch (e) {\n if (e instanceof ConflictError) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.conflict,\n status: 409,\n title: 'Conflict',\n detail: 'Stored profile version does not match If-Match.',\n currentVersion: e.currentVersion,\n });\n }\n throw e;\n }\n\n await deps.cachePurger.profileUpdated();\n\n const updatedAt = new Date().toISOString();\n const tokenHash = await getActorTokenHash(c);\n deps.auditLogger({\n type: 'admin.profile.update',\n timestamp: updatedAt,\n actor: { tokenHash },\n request: {\n method: 'PUT',\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n },\n result: { status: 200, version: saved.version },\n });\n\n return c.json({\n data: normalize(data),\n meta: {\n schemaVersion: data.schemaVersion,\n version: saved.version,\n updatedAt,\n },\n });\n });\n\n app.delete('/profile', async (c) => {\n await deps.storage.deleteProfile();\n await deps.cachePurger.profileDeleted();\n\n const tokenHash = await getActorTokenHash(c);\n deps.auditLogger({\n type: 'admin.profile.delete',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: {\n method: 'DELETE',\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n },\n result: { status: 204 },\n });\n\n return c.body(null, 204);\n });\n\n app.get('/export', async (c) => {\n let stored: { data: Takuhon; version: string };\n try {\n stored = await deps.storage.getProfile();\n } catch (e) {\n if (e instanceof NotFoundError) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: 'No profile has been saved yet; there is nothing to export.',\n });\n }\n throw e;\n }\n\n // Token holders receive the full document: the public privacy filter is\n // intentionally bypassed here (Spec §6.21). `exportTakuhon` with\n // `updateTimestamp: false` returns the stored document verbatim (raw\n // transport form, no envelope), so the body round-trips with\n // `importTakuhon` and preserves the real `meta.updatedAt`.\n const exported = exportTakuhon(stored.data, { updateTimestamp: false });\n\n const tokenHash = await getActorTokenHash(c);\n deps.auditLogger({\n type: 'admin.profile.export',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: {\n method: 'GET',\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n },\n result: { status: 200 },\n });\n\n return c.json(exported);\n });\n\n app.on(['POST', 'PATCH'], '/profile', (c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.methodNotAllowed,\n status: 405,\n title: 'Method Not Allowed',\n detail: `${c.req.method} ${new URL(c.req.url).pathname} is not supported on the admin app.`,\n }),\n );\n\n return app;\n}\n","import type { Context, Next } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse } from '../error-envelope.js';\n\nimport type { AuditLogger } from './audit-logger.js';\n\n/**\n * Constant-time byte comparison.\n *\n * Iterates over `max(len(a), len(b))` bytes so wall-clock cost is independent\n * of *where* a mismatch occurs and of length differences (within the same\n * length class). Length-mismatch is folded into the accumulator so the\n * boolean result still discriminates correctly.\n *\n * This is intentionally a from-scratch implementation rather than\n * `crypto.subtle.timingSafeEqual`, which Cloudflare Workers exposes but\n * Node lacks — `@takuhon/api` is adapter-neutral.\n */\nexport function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {\n const len = a.length > b.length ? a.length : b.length;\n let diff = a.length === b.length ? 0 : 1;\n for (let i = 0; i < len; i++) {\n const ai = a[i] ?? 0;\n const bi = b[i] ?? 0;\n diff |= ai ^ bi;\n }\n return diff === 0;\n}\n\n/** SHA-256 hex digest (lowercase) over a UTF-8 string. */\nexport async function sha256Hex(input: string): Promise<string> {\n const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));\n const bytes = new Uint8Array(buf);\n let out = '';\n for (const b of bytes) {\n out += b.toString(16).padStart(2, '0');\n }\n return out;\n}\n\n/**\n * Extracts the Bearer token from the request and returns a stable\n * `sha256:<hex>` digest used as the actor identity in audit logs. Returns\n * `sha256:absent` when no token is present.\n */\nexport async function getActorTokenHash(c: Context): Promise<string> {\n const header = c.req.header('authorization') ?? '';\n const m = /^Bearer\\s+(.+)$/i.exec(header);\n const token = m?.[1] ?? '';\n if (token === '') return 'sha256:absent';\n return `sha256:${await sha256Hex(token)}`;\n}\n\nexport interface BearerMiddlewareOptions {\n /**\n * Source of the expected admin token. Returns `undefined` when the deploy\n * has not provisioned a secret — in that case every request is rejected,\n * mirroring \"no admin access\" semantics.\n */\n getAdminToken: () => string | undefined;\n auditLogger: AuditLogger;\n}\n\n/**\n * Hono middleware that gates downstream handlers on a constant-time Bearer\n * token check. Emits an audit event for both success and failure.\n */\nexport function bearerMiddleware(opts: BearerMiddlewareOptions) {\n return async (c: Context, next: Next): Promise<Response | void> => {\n const expected = opts.getAdminToken();\n const header = c.req.header('authorization') ?? '';\n const match = /^Bearer\\s+(.+)$/i.exec(header);\n const presented = match?.[1];\n\n const isOk =\n expected !== undefined &&\n presented !== undefined &&\n constantTimeEqual(new TextEncoder().encode(presented), new TextEncoder().encode(expected));\n\n const tokenHash = await getActorTokenHash(c);\n const baseRequest = {\n method: c.req.method,\n path: new URL(c.req.url).pathname,\n ip: c.req.header('cf-connecting-ip'),\n };\n\n if (!isOk) {\n opts.auditLogger({\n type: 'admin.auth.failure',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: baseRequest,\n result: { status: 401 },\n });\n return problemResponse(c, {\n slug: ERROR_SLUGS.unauthorized,\n status: 401,\n title: 'Unauthorized',\n detail: 'Bearer token missing or invalid.',\n });\n }\n\n opts.auditLogger({\n type: 'admin.auth.success',\n timestamp: new Date().toISOString(),\n actor: { tokenHash },\n request: baseRequest,\n result: { status: 200 },\n });\n\n await next();\n };\n}\n","import type { Context, Next } from 'hono';\n\nimport { ERROR_SLUGS, problemResponse } from '../error-envelope.js';\n\nexport interface OriginMiddlewareOptions {\n /**\n * Returns the allowlist of admin Origins (e.g.\n * `['https://admin.example.com']`). Empty list disables the check —\n * deploys are expected to populate the env before going to production,\n * with the trade-off documented in the adapter README.\n */\n getAdminOrigins: () => string[];\n}\n\n/**\n * Hono middleware that enforces a same-origin / allowlisted-origin policy\n * when configured. Requests without an `Origin` header (curl, server-to-\n * server, native apps) are allowed through; the Bearer token is the primary\n * auth boundary and the absence of `Origin` is itself an indicator that the\n * request did not originate from a browser CSRF context.\n */\nexport function originMiddleware(opts: OriginMiddlewareOptions) {\n return async (c: Context, next: Next): Promise<Response | void> => {\n const allow = opts.getAdminOrigins();\n if (allow.length === 0) {\n await next();\n return;\n }\n const origin = c.req.header('origin');\n if (origin !== undefined && !allow.includes(origin)) {\n return problemResponse(c, {\n slug: ERROR_SLUGS.forbidden,\n status: 403,\n title: 'Forbidden',\n detail: `Origin ${origin} is not in the admin allowlist.`,\n });\n }\n await next();\n };\n}\n","import { Hono } from 'hono';\n\nimport { renderAdminHtml } from './admin-html.js';\n\n/**\n * Per-request CSP (security.md §1.3). Differs from the public CSP:\n * - `img-src` drops `data:` and adds `blob:` for client-side previews.\n * - `style-src` and `script-src` drop `unsafe-inline` and pin a nonce.\n * - `require-trusted-types-for 'script'` blocks DOM-XSS sinks.\n */\nfunction adminCsp(nonce: string): string {\n return [\n \"default-src 'self'\",\n \"img-src 'self' blob:\",\n `style-src 'self' 'nonce-${nonce}'`,\n `script-src 'self' 'nonce-${nonce}'`,\n \"font-src 'self'\",\n \"connect-src 'self'\",\n \"frame-ancestors 'none'\",\n \"base-uri 'self'\",\n \"form-action 'self'\",\n \"require-trusted-types-for 'script'\",\n 'upgrade-insecure-requests',\n ].join('; ');\n}\n\nfunction generateNonce(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n let bin = '';\n for (const b of bytes) {\n bin += String.fromCharCode(b);\n }\n return btoa(bin);\n}\n\n/**\n * Hono factory for the `/admin` HTML editor. Mounted by adapters at\n * `/admin` (so the sub-app sees `/` as its root path). Each request gets a\n * freshly-generated nonce shared between the CSP header and the inline\n * `<style>` / `<script>` tags.\n */\nexport function createAdminUiApp(): Hono {\n const app = new Hono();\n\n app.get('/', (c) => {\n const nonce = generateNonce();\n c.header('strict-transport-security', 'max-age=63072000; includeSubDomains; preload');\n c.header('x-content-type-options', 'nosniff');\n c.header('x-frame-options', 'DENY');\n c.header('referrer-policy', 'strict-origin-when-cross-origin');\n c.header('permissions-policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');\n c.header('content-security-policy', adminCsp(nonce));\n c.header('cache-control', 'private, no-store');\n return c.html(renderAdminHtml(nonce));\n });\n\n return app;\n}\n","/**\n * Inline HTML for the minimal admin editor served at `GET /admin`.\n *\n * Single-page, no build step: a token input, a JSON textarea preloaded from\n * `/takuhon.json`, Save (PUT) and Delete (DELETE) buttons. The page operates\n * under a strict CSP (`script-src 'self' 'nonce-<n>'`,\n * `style-src 'self' 'nonce-<n>'`, `require-trusted-types-for 'script'`), so\n * both the inline `<script>` and `<style>` blocks carry the request-scoped\n * nonce. We avoid `innerHTML`/`eval` so Trusted Types is non-disruptive.\n */\nexport function renderAdminHtml(nonce: string): string {\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>takuhon admin</title>\n<style nonce=\"${nonce}\">\nbody { font-family: system-ui, -apple-system, sans-serif; max-width: 960px; margin: 2rem auto; padding: 0 1rem; color: #222; }\nh1 { font-size: 1.5rem; }\np.note { color: #555; }\nlabel { display: block; margin: 1rem 0 0.25rem; font-weight: 600; }\ninput[type=password], textarea { width: 100%; box-sizing: border-box; padding: 0.5rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9rem; border: 1px solid #999; border-radius: 4px; }\ntextarea { min-height: 24rem; }\n.row { display: flex; gap: 0.5rem; margin-top: 1rem; }\nbutton { padding: 0.5rem 1rem; font-size: 0.95rem; border: 1px solid #444; background: #fafafa; border-radius: 4px; cursor: pointer; }\nbutton.danger { border-color: #b03; color: #b03; background: #fff5f5; }\n#status { margin-top: 1rem; padding: 0.75rem; border-radius: 4px; white-space: pre-wrap; word-break: break-word; }\n#status.ok { background: #e6f4ea; color: #1b5e20; border: 1px solid #82c891; }\n#status.err { background: #fdecea; color: #b71c1c; border: 1px solid #ef9a9a; }\nsmall.version { color: #555; }\n</style>\n</head>\n<body>\n<h1>takuhon admin</h1>\n<p class=\"note\">Edit the full <code>takuhon.json</code> document and Save. Optimistic locking via <code>If-Match</code> guards concurrent edits; the token is never sent over the URL.</p>\n<label for=\"token\">Admin token</label>\n<input id=\"token\" type=\"password\" autocomplete=\"off\" spellcheck=\"false\">\n<label for=\"payload\">takuhon.json <small class=\"version\" id=\"versionLabel\"></small></label>\n<textarea id=\"payload\" spellcheck=\"false\" autocapitalize=\"off\" autocomplete=\"off\"></textarea>\n<div class=\"row\">\n <button id=\"save\" type=\"button\">Save</button>\n <button id=\"delete\" type=\"button\" class=\"danger\">Delete profile</button>\n <button id=\"reload\" type=\"button\">Reload current</button>\n</div>\n<div id=\"status\" hidden></div>\n<script nonce=\"${nonce}\">\n(function () {\n var tokenEl = document.getElementById('token');\n var payloadEl = document.getElementById('payload');\n var versionEl = document.getElementById('versionLabel');\n var statusEl = document.getElementById('status');\n var ifMatch = '';\n\n function setStatus(message, ok) {\n statusEl.textContent = message;\n statusEl.className = ok ? 'ok' : 'err';\n statusEl.hidden = false;\n }\n function setVersion(etag) {\n ifMatch = etag || '';\n versionEl.textContent = ifMatch ? '(current version: ' + ifMatch + ')' : '(no stored version)';\n }\n function getToken() {\n var t = tokenEl.value.trim();\n if (!t) { setStatus('Admin token is required.', false); return null; }\n return t;\n }\n async function loadCurrent() {\n try {\n var res = await fetch('/takuhon.json', { cache: 'no-store' });\n if (!res.ok) { setStatus('Failed to load /takuhon.json: ' + res.status, false); return; }\n setVersion(res.headers.get('etag'));\n var json = await res.json();\n payloadEl.value = JSON.stringify(json, null, 2);\n setStatus('Loaded current profile.', true);\n } catch (e) {\n setStatus('Network error loading current profile: ' + (e && e.message ? e.message : String(e)), false);\n }\n }\n async function save() {\n var token = getToken();\n if (!token) return;\n var body;\n try {\n body = JSON.parse(payloadEl.value);\n } catch (e) {\n setStatus('JSON parse error: ' + (e && e.message ? e.message : String(e)), false);\n return;\n }\n var headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token };\n if (ifMatch) headers['If-Match'] = ifMatch;\n try {\n var res = await fetch('/api/admin/profile', { method: 'PUT', headers: headers, body: JSON.stringify(body) });\n var json = await res.json().catch(function () { return null; });\n if (res.ok && json && json.meta && json.meta.version) {\n setVersion('\"' + json.meta.version + '\"');\n setStatus('Saved. New version: ' + json.meta.version, true);\n } else {\n setStatus('Save failed (' + res.status + '): ' + (json ? JSON.stringify(json, null, 2) : 'no body'), false);\n }\n } catch (e) {\n setStatus('Network error during save: ' + (e && e.message ? e.message : String(e)), false);\n }\n }\n async function deleteProfile() {\n var token = getToken();\n if (!token) return;\n if (!confirm('Delete the profile? The bundled onboarding fixture will be shown until you save a new document.')) return;\n var headers = { 'Authorization': 'Bearer ' + token };\n try {\n var res = await fetch('/api/admin/profile', { method: 'DELETE', headers: headers });\n if (res.ok) {\n payloadEl.value = '';\n setVersion('');\n setStatus('Deleted.', true);\n } else {\n var json = await res.json().catch(function () { return null; });\n setStatus('Delete failed (' + res.status + '): ' + (json ? JSON.stringify(json, null, 2) : 'no body'), false);\n }\n } catch (e) {\n setStatus('Network error during delete: ' + (e && e.message ? e.message : String(e)), false);\n }\n }\n\n document.getElementById('save').addEventListener('click', save);\n document.getElementById('delete').addEventListener('click', deleteProfile);\n document.getElementById('reload').addEventListener('click', loadCurrent);\n loadCurrent();\n})();\n</script>\n</body>\n</html>\n`;\n}\n","/**\n * Structured audit-log emitter for admin actions (per security.md §5).\n *\n * Phase 3.4 covers the auth + profile-write events. Asset events\n * (`admin.asset.upload`, `admin.asset.delete`) join the union when Phase 3.5\n * lands. Adapters bind a concrete sink (Cloudflare uses `console.log`, which\n * Workers Tail / Logpush captures); tests inject a `vi.fn()` recorder.\n */\nexport type AuditEventType =\n | 'admin.auth.success'\n | 'admin.auth.failure'\n | 'admin.profile.update'\n | 'admin.profile.delete'\n | 'admin.profile.export'\n | 'admin.cache.purge';\n\nexport interface AuditEvent {\n type: AuditEventType;\n /** ISO-8601 UTC timestamp generated at the call site. */\n timestamp: string;\n /**\n * Actor identity. `tokenHash` is `sha256:<hex>` over the presented Bearer\n * token, or `sha256:absent` when no token was supplied. The raw token is\n * never logged.\n */\n actor?: { tokenHash: string };\n request: {\n method: string;\n path: string;\n /** Originating client IP from `cf-connecting-ip`; undefined off-Cloudflare. */\n ip?: string;\n };\n result: {\n status: number;\n /** Opaque storage version emitted by `TakuhonStorage.saveProfile`. */\n version?: string;\n };\n}\n\nexport type AuditLogger = (event: AuditEvent) => void;\n\n/** Default sink that discards events; useful for tests and bare runtimes. */\nexport const noopAuditLogger: AuditLogger = () => {\n /* no-op */\n};\n","/**\n * Cache invalidation contract for admin write paths.\n *\n * `@takuhon/api` stays adapter-neutral, so the actual edge-cache calls live\n * in each adapter (Cloudflare's `caches.default.delete`, future runtimes'\n * equivalents). Tests inject a recording implementation to assert that the\n * admin handlers fire the right purge after a successful write.\n */\nexport interface CachePurger {\n /** Called after a successful `PUT /api/admin/profile`. */\n profileUpdated(): Promise<void>;\n /** Called after a successful `DELETE /api/admin/profile`. */\n profileDeleted(): Promise<void>;\n}\n\n/**\n * No-op default: callers that don't run on an edge cache (Node dev server,\n * unit tests that don't care about purge calls) can pass this in.\n */\nexport const noopCachePurger: CachePurger = {\n profileUpdated: () => Promise.resolve(),\n profileDeleted: () => Promise.resolve(),\n};\n"],"mappings":"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","names":["applyPublicPrivacyFilter","NotFoundError","normalize","Hono","Hono","normalize","NotFoundError","Hono","Hono"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takuhon/api",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Hono-based HTTP handlers, RFC 7807 error envelope, and response builders for takuhon",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Takuhon contributors",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "hono": "^4.12.19",
47
- "@takuhon/core": "0.6.1"
47
+ "@takuhon/core": "0.8.0"
48
48
  },
49
49
  "scripts": {
50
50
  "typecheck": "tsc",