@takuhon/api 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NOTICE CHANGED
@@ -17,7 +17,7 @@ types. These path definitions are derived from:
17
17
 
18
18
  - Simple Icons (https://simpleicons.org) — CC0-1.0 (public domain). All glyphs
19
19
  except LinkedIn: github, gitlab, x, mastodon, bluesky, instagram, youtube,
20
- threads, facebook, rss.
20
+ threads, facebook, rss, npm, linktree, wakatime, wordpress.
21
21
 
22
22
  - Bootstrap Icons (https://icons.getbootstrap.com) — MIT License,
23
23
  Copyright (c) 2019-2024 The Bootstrap Authors. The LinkedIn glyph only
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Context, Hono } from 'hono';
2
2
  import { ContentfulStatusCode } from 'hono/utils/http-status';
3
- import { TakuhonStorage, Takuhon, ActivityStorage, TakuhonAssetStorage, LocalizedTakuhon, ActivitySnapshot, NormalizedTakuhon, CvDocument } from '@takuhon/core';
3
+ import { LocalizedTakuhon, ActivitySnapshot, LabelKey, SectionKey, TakuhonStorage, Takuhon, ActivityStorage, TakuhonAssetStorage, NormalizedTakuhon, CvDocument } from '@takuhon/core';
4
4
  export { applyPublicPrivacyFilter } from '@takuhon/core';
5
5
 
6
6
  /**
@@ -56,6 +56,156 @@ interface ProblemResponseInput {
56
56
  }
57
57
  declare function problemResponse(c: Context, input: ProblemResponseInput): Response;
58
58
 
59
+ /**
60
+ * Pure HTML helpers shared by the static-site renderer ({@link
61
+ * import('./build-html.js')}) and the CV renderer ({@link
62
+ * import('./cv-html.js')}). All are string-in / string-out with no I/O, so they
63
+ * stay unit-testable and keep both renderers' escaping behavior identical.
64
+ */
65
+
66
+ /** Escape text for use in HTML element content or double/single-quoted attributes. */
67
+ declare function escapeHtml(value: string): string;
68
+
69
+ /**
70
+ * Pure HTML rendering for `takuhon build`.
71
+ *
72
+ * {@link renderProfileHtml} turns one locale-resolved {@link LocalizedTakuhon}
73
+ * into a complete, self-contained static HTML document: semantic markup for
74
+ * every profile section, an inline stylesheet, optional Schema.org JSON-LD,
75
+ * and the `<head>` metadata (`<title>`, description, canonical, hreflang
76
+ * alternates) the caller supplies.
77
+ *
78
+ * This is a deliberately separate, simpler surface from the React
79
+ * `@takuhon/ui` (which is delivered as CSS-Modules components needing a
80
+ * bundler). It reuses only `@takuhon/core` and has no DOM/browser dependency,
81
+ * so it renders in plain Node and is unit-testable as a pure string function.
82
+ *
83
+ * Security: every piece of profile-derived text is escaped before it reaches
84
+ * the markup ({@link escapeHtml}), and the JSON-LD payload is `<`/`>`/`&`
85
+ * unicode-escaped so it cannot break out of its `<script>` element.
86
+ */
87
+
88
+ /** One entry in the human-facing locale switcher. */
89
+ interface LocaleLink {
90
+ locale: string;
91
+ href: string;
92
+ current: boolean;
93
+ }
94
+ /** One `<link rel="alternate" hreflang>` entry. */
95
+ interface Alternate {
96
+ hreflang: string;
97
+ href: string;
98
+ }
99
+ interface RenderInput {
100
+ /** The locale-resolved document to render. */
101
+ localized: LocalizedTakuhon;
102
+ /** Absolute canonical URL for this page (only when `--base-url` was given). */
103
+ canonicalUrl?: string;
104
+ /** hreflang alternates (empty when no base URL is available). */
105
+ alternates: readonly Alternate[];
106
+ /** Human locale switcher links (rendered only when more than one locale). */
107
+ localeNav: readonly LocaleLink[];
108
+ /** Whether to emit Schema.org JSON-LD (mirrors `settings.enableJsonLd`). */
109
+ jsonLd: boolean;
110
+ /**
111
+ * Synced developer-activity snapshot, rendered as a self-owned inline SVG
112
+ * section. The caller gates it on `settings.activity.enabled`; an absent (or
113
+ * metric-less) snapshot omits the section entirely.
114
+ */
115
+ activitySnapshot?: ActivitySnapshot;
116
+ /**
117
+ * When set, embed the `@takuhon/contact` widget: a `<link>` to its stylesheet
118
+ * in `<head>` and a deferred `<script>` whose config travels as `data-*`
119
+ * attributes (no inline script, so the page CSP needs no `'unsafe-inline'`).
120
+ * The caller gates it on `settings.contact.enabled` and a present site key;
121
+ * the adapter is responsible for serving `/contact-widget.{js,css}` and for
122
+ * relaxing its CSP to allow the Turnstile origin.
123
+ */
124
+ contact?: {
125
+ siteKey: string;
126
+ endpoint?: string;
127
+ };
128
+ /**
129
+ * Copyright year for the footer license line (`© {year} {name} — {spdx}`).
130
+ * The renderer is pure, so the clock lives with the caller: a server/CLI
131
+ * passes `new Date().getFullYear()`; a test passes a fixed value. When absent,
132
+ * the license line omits the year (`© {name} — {spdx}`) so output stays
133
+ * deterministic by default.
134
+ */
135
+ year?: number;
136
+ /**
137
+ * First-party composition slots: raw HTML injected verbatim at three fixed
138
+ * points so a host can extend the page without forking the renderer (a page
139
+ * `<head>`, a trailing `<main>` section, a pre-`</body>` block). Unlike every
140
+ * other input, slot content is NOT escaped — it is the caller's own trusted
141
+ * markup (e.g. an og:image tag, a PWA manifest link, an analytics beacon, a
142
+ * bespoke section), never profile/user data. Absent slots emit nothing, so
143
+ * passing no slots is byte-identical to passing empty slots. See {@link
144
+ * RenderSlots}.
145
+ */
146
+ slots?: RenderSlots;
147
+ /**
148
+ * Localized overrides for the section headings and chrome labels, merged over
149
+ * the built-in pack chosen for the resolved locale ({@link pickLabelPack}: a
150
+ * Japanese base-language locale gets the Japanese pack, everything else
151
+ * English). A host supplies overrides only for the strings it wants to change
152
+ * (e.g. a bespoke heading); an absent field takes the pack default.
153
+ */
154
+ labels?: Partial<SectionLabels>;
155
+ /**
156
+ * Sections whose DEFAULT rendering to suppress in the visible body — so a host
157
+ * can replace one with its own markup via {@link RenderSlots.mainEnd} without
158
+ * it appearing twice. This affects the visible body ONLY: the embedded JSON-LD
159
+ * is still generated from the complete document, so structured data never
160
+ * loses a suppressed section. Absent / empty = every section renders.
161
+ */
162
+ omitSections?: readonly SectionKey[];
163
+ }
164
+ /**
165
+ * First-party raw-HTML injection points. Content is inserted verbatim (not
166
+ * escaped): it is the host's own trusted markup, not profile/user data. Keep it
167
+ * self-contained — the renderer makes no guarantees about it.
168
+ *
169
+ * CSP: the renderer does not set any Content-Security-Policy — the adapter does.
170
+ * The turnkey public app serves `script-src 'self'` (no `'unsafe-inline'`), so
171
+ * an inline `<script>` injected via `bodyEnd`/`head` is blocked there; prefer an
172
+ * external `<script src>` or ensure the host's own CSP permits the slot content.
173
+ *
174
+ * Reserved id: the renderer owns `<main id="main">` (the skip-link target). A
175
+ * slot MUST NOT inject another element with `id="main"`, or the skip anchor
176
+ * becomes ambiguous.
177
+ */
178
+ interface RenderSlots {
179
+ /**
180
+ * Injected in `<head>`, after the `<style>` block (e.g. OG image tags, a PWA
181
+ * manifest link, theme-color). NOTE: the renderer already owns the data-derived
182
+ * Open Graph tags (`og:type`/`og:title`/`og:description`/`og:url`); a host slot
183
+ * should supply only the asset tags it owns (`og:image`, `twitter:image`, …)
184
+ * and MUST NOT re-inject the renderer-owned ones, or they appear twice.
185
+ */
186
+ head?: string;
187
+ /** Injected at the end of `<main>`, after all rendered sections (e.g. a bespoke section). */
188
+ mainEnd?: string;
189
+ /**
190
+ * Injected before `</body>`, after the footer and contact script (e.g. an
191
+ * analytics beacon). Inline scripts here require a CSP that allows them (see
192
+ * the interface note) — the turnkey `script-src 'self'` would block them.
193
+ */
194
+ bodyEnd?: string;
195
+ }
196
+ /**
197
+ * Section headings (one per {@link SectionKey}) plus chrome labels (skip link,
198
+ * nav aria-labels, the other-links heading, the footer credit lead-in) the
199
+ * caller can localize. The full key set is the core {@link LabelKey} taxonomy,
200
+ * so this type, `settings.sectionLabels`, and {@link RenderInput.labels} stay in
201
+ * lock-step. The renderer ships Japanese and English packs and picks one by the
202
+ * resolved locale ({@link pickLabelPack}); `settings.sectionLabels` then
203
+ * `labels` override individual strings on top.
204
+ */
205
+ type SectionLabels = Record<LabelKey, string>;
206
+ /** Render a complete static HTML document for one locale-resolved profile. */
207
+ declare function renderProfileHtml(input: RenderInput): string;
208
+
59
209
  declare module 'hono' {
60
210
  interface ContextVariableMap {
61
211
  /**
@@ -64,6 +214,12 @@ declare module 'hono' {
64
214
  * that one response and the strict default everywhere else.
65
215
  */
66
216
  contactEnabled?: boolean;
217
+ /**
218
+ * Set by the `/` handler so the security-headers middleware serves the
219
+ * host-extended CSP (`deps.render.csp`) on the profile page only; every
220
+ * other route keeps the strict `'self'` policy.
221
+ */
222
+ profilePage?: boolean;
67
223
  }
68
224
  }
69
225
  interface PublicAppDeps {
@@ -92,7 +248,58 @@ interface PublicAppDeps {
92
248
  * endpoint they don't host.
93
249
  */
94
250
  mcpPath?: string;
251
+ /**
252
+ * First-party host composition applied to the server-rendered profile page
253
+ * (`GET /`): the renderer's `slots` / `labels` / `omitSections` (see
254
+ * {@link RenderInput}) plus an optional {@link PublicRenderCsp} that widens the
255
+ * page's Content-Security-Policy so injected slots can load their scripts.
256
+ * Absent (the turnkey default) leaves the page and its strict CSP untouched.
257
+ */
258
+ render?: PublicRenderOptions;
259
+ }
260
+ /**
261
+ * Declarative, additive extensions to the public profile page's
262
+ * Content-Security-Policy (see {@link PublicRenderOptions.csp}). Each list is
263
+ * appended to the corresponding base directive; `scriptHashes` (e.g.
264
+ * `'sha256-…'`) are appended to `script-src` so a specific inline script — such
265
+ * as a service-worker registration injected via a slot — is allowed WITHOUT a
266
+ * blanket `'unsafe-inline'`. Only the profile page's CSP is widened; every other
267
+ * public route keeps the strict `'self'` policy.
268
+ *
269
+ * Values are host-supplied deploy configuration (not user data), but each token
270
+ * is validated at construction and invalid entries are dropped (with a
271
+ * `console.warn` naming them):
272
+ * - Origin lists must be single CSP source expressions of printable-ASCII
273
+ * characters with no `;` or `,`, so a malformed value can neither inject a
274
+ * directive, split the header, nor crash header construction (a control
275
+ * character or non-Latin-1 codepoint would throw in `Headers.set`). The two
276
+ * most common blanket relaxations, `'unsafe-inline'` / `'unsafe-eval'`, are
277
+ * also rejected — they are the usual copy-paste footgun. This is deliberately
278
+ * narrow, NOT a comprehensive guardrail: `render.csp` is trusted host deploy
279
+ * config, so a host can still broaden its own `script-src` with e.g. `*` or a
280
+ * scheme source, exactly as it could by hand-writing the policy.
281
+ * - `scriptHashes` must be a CSP hash expression (`'sha256-…'` / `'sha384-…'` /
282
+ * `'sha512-…'`); anything else (including `'unsafe-inline'`) is dropped.
283
+ */
284
+ interface PublicRenderCsp {
285
+ /** Extra `script-src` origins (e.g. an analytics beacon's script host). */
286
+ scriptSrc?: readonly string[];
287
+ /** Extra `connect-src` origins (e.g. where a beacon reports back). */
288
+ connectSrc?: readonly string[];
289
+ /** `worker-src` origins (e.g. `'self'` for a PWA service worker). Adds the directive when set. */
290
+ workerSrc?: readonly string[];
291
+ /** `'sha256-…'` / `'sha384-…'` / `'sha512-…'` hashes for specific inline scripts, appended to `script-src`. */
292
+ scriptHashes?: readonly string[];
95
293
  }
294
+ /**
295
+ * Host composition for the server-rendered profile page: the renderer's
296
+ * first-party {@link RenderInput} seams (`slots` / `labels` / `omitSections`)
297
+ * plus an optional {@link PublicRenderCsp}. The CSP lives here, beside the slots
298
+ * whose scripts it authorizes.
299
+ */
300
+ type PublicRenderOptions = Pick<RenderInput, 'slots' | 'labels' | 'omitSections'> & {
301
+ csp?: PublicRenderCsp;
302
+ };
96
303
  declare function createPublicApp(deps: PublicAppDeps): Hono;
97
304
 
98
305
  /**
@@ -245,79 +452,6 @@ declare function createAdminUiApp(): Hono;
245
452
  */
246
453
  declare function adminAssetSecurityHeaders(): Record<string, string>;
247
454
 
248
- /**
249
- * Pure HTML helpers shared by the static-site renderer ({@link
250
- * import('./build-html.js')}) and the CV renderer ({@link
251
- * import('./cv-html.js')}). All are string-in / string-out with no I/O, so they
252
- * stay unit-testable and keep both renderers' escaping behavior identical.
253
- */
254
-
255
- /** Escape text for use in HTML element content or double/single-quoted attributes. */
256
- declare function escapeHtml(value: string): string;
257
-
258
- /**
259
- * Pure HTML rendering for `takuhon build`.
260
- *
261
- * {@link renderProfileHtml} turns one locale-resolved {@link LocalizedTakuhon}
262
- * into a complete, self-contained static HTML document: semantic markup for
263
- * every profile section, an inline stylesheet, optional Schema.org JSON-LD,
264
- * and the `<head>` metadata (`<title>`, description, canonical, hreflang
265
- * alternates) the caller supplies.
266
- *
267
- * This is a deliberately separate, simpler surface from the React
268
- * `@takuhon/ui` (which is delivered as CSS-Modules components needing a
269
- * bundler). It reuses only `@takuhon/core` and has no DOM/browser dependency,
270
- * so it renders in plain Node and is unit-testable as a pure string function.
271
- *
272
- * Security: every piece of profile-derived text is escaped before it reaches
273
- * the markup ({@link escapeHtml}), and the JSON-LD payload is `<`/`>`/`&`
274
- * unicode-escaped so it cannot break out of its `<script>` element.
275
- */
276
-
277
- /** One entry in the human-facing locale switcher. */
278
- interface LocaleLink {
279
- locale: string;
280
- href: string;
281
- current: boolean;
282
- }
283
- /** One `<link rel="alternate" hreflang>` entry. */
284
- interface Alternate {
285
- hreflang: string;
286
- href: string;
287
- }
288
- interface RenderInput {
289
- /** The locale-resolved document to render. */
290
- localized: LocalizedTakuhon;
291
- /** Absolute canonical URL for this page (only when `--base-url` was given). */
292
- canonicalUrl?: string;
293
- /** hreflang alternates (empty when no base URL is available). */
294
- alternates: readonly Alternate[];
295
- /** Human locale switcher links (rendered only when more than one locale). */
296
- localeNav: readonly LocaleLink[];
297
- /** Whether to emit Schema.org JSON-LD (mirrors `settings.enableJsonLd`). */
298
- jsonLd: boolean;
299
- /**
300
- * Synced developer-activity snapshot, rendered as a self-owned inline SVG
301
- * section. The caller gates it on `settings.activity.enabled`; an absent (or
302
- * metric-less) snapshot omits the section entirely.
303
- */
304
- activitySnapshot?: ActivitySnapshot;
305
- /**
306
- * When set, embed the `@takuhon/contact` widget: a `<link>` to its stylesheet
307
- * in `<head>` and a deferred `<script>` whose config travels as `data-*`
308
- * attributes (no inline script, so the page CSP needs no `'unsafe-inline'`).
309
- * The caller gates it on `settings.contact.enabled` and a present site key;
310
- * the adapter is responsible for serving `/contact-widget.{js,css}` and for
311
- * relaxing its CSP to allow the Turnstile origin.
312
- */
313
- contact?: {
314
- siteKey: string;
315
- endpoint?: string;
316
- };
317
- }
318
- /** Render a complete static HTML document for one locale-resolved profile. */
319
- declare function renderProfileHtml(input: RenderInput): string;
320
-
321
455
  /**
322
456
  * Shared static-site generation for `takuhon build` and `takuhon dev`.
323
457
  *
@@ -393,4 +527,4 @@ declare function generateSite(profile: NormalizedTakuhon, options?: GenerateOpti
393
527
  /** Render a complete static HTML résumé document for one locale-resolved CV. */
394
528
  declare function renderCvHtml(cv: CvDocument): string;
395
529
 
396
- export { type AdminApiAppDeps, type Alternate, type AuditEvent, type AuditEventType, type AuditLogger, type BuildProblemInput, type CachePurger, ERROR_SLUGS, type ErrorSlug, type GenerateOptions, LOCALE_AWARE_REMAINDERS, type LocaleLink, type ProblemDetails, type ProblemFieldError, type ProblemResponseInput, type PublicAppDeps, type RenderInput, type SitePage, adminAssetSecurityHeaders, buildProblem, createAdminApiApp, createAdminUiApp, createPublicApp, escapeHtml, generateSite, localePrefixGetPath, noopAuditLogger, noopCachePurger, pathLocaleFromUrl, problemResponse, renderCvHtml, renderProfileHtml, stripLocalePrefix };
530
+ export { type AdminApiAppDeps, type Alternate, type AuditEvent, type AuditEventType, type AuditLogger, type BuildProblemInput, type CachePurger, ERROR_SLUGS, type ErrorSlug, type GenerateOptions, LOCALE_AWARE_REMAINDERS, type LocaleLink, type ProblemDetails, type ProblemFieldError, type ProblemResponseInput, type PublicAppDeps, type PublicRenderCsp, type PublicRenderOptions, type RenderInput, type SitePage, adminAssetSecurityHeaders, buildProblem, createAdminApiApp, createAdminUiApp, createPublicApp, escapeHtml, generateSite, localePrefixGetPath, noopAuditLogger, noopCachePurger, pathLocaleFromUrl, problemResponse, renderCvHtml, renderProfileHtml, stripLocalePrefix };