ampless 0.2.0-alpha.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.
@@ -0,0 +1,648 @@
1
+ type ContentEventType = 'content.created' | 'content.updated' | 'content.published' | 'content.unpublished' | 'content.deleted';
2
+ type MediaEventType = 'media.uploaded' | 'media.deleted';
3
+ type SiteSettingsEventType = 'site.settings.updated';
4
+ type EventType = ContentEventType | MediaEventType | SiteSettingsEventType;
5
+ /** Minimal projection of a Post item carried in events (no body, to keep payloads small). */
6
+ interface ContentEventPayload {
7
+ siteId: string;
8
+ postId: string;
9
+ slug: string;
10
+ title: string;
11
+ status: 'draft' | 'published';
12
+ publishedAt?: string;
13
+ tags?: string[];
14
+ }
15
+ interface MediaEventPayload {
16
+ siteId: string;
17
+ mediaId: string;
18
+ src: string;
19
+ mimeType: string;
20
+ }
21
+ /**
22
+ * Emitted whenever any setting under `siteconfig:{siteId}` in KvStore
23
+ * is created, updated, or removed. Subscribers (built-in or user
24
+ * plugins) can rebuild caches, theme assets, etc.
25
+ */
26
+ interface SiteSettingsEventPayload {
27
+ siteId: string;
28
+ }
29
+ type EventPayloadOf<T extends EventType> = T extends ContentEventType ? ContentEventPayload : T extends MediaEventType ? MediaEventPayload : T extends SiteSettingsEventType ? SiteSettingsEventPayload : never;
30
+ interface AmplessEvent<T extends EventType = EventType> {
31
+ type: T;
32
+ payload: EventPayloadOf<T>;
33
+ /** ISO 8601 timestamp of when the source mutation happened. */
34
+ timestamp: string;
35
+ }
36
+ /**
37
+ * Maps a single content mutation to the CMS-level events it represents.
38
+ * Always emits `content.updated` for any MODIFY so plugins that subscribe
39
+ * to "any change" reliably fire; status transitions add the matching
40
+ * published / unpublished event on top.
41
+ *
42
+ * Used by the DynamoDB Stream dispatcher Lambda — kept here so the same
43
+ * decision table is testable in plain Node without AWS deps.
44
+ */
45
+ type StreamEventName = 'INSERT' | 'MODIFY' | 'REMOVE';
46
+ declare function detectContentEvents(input: {
47
+ eventName: StreamEventName | string | undefined;
48
+ oldStatus?: 'draft' | 'published';
49
+ newStatus?: 'draft' | 'published';
50
+ }): ContentEventType[];
51
+
52
+ type TrustLevel = 'untrusted' | 'trusted' | 'privileged';
53
+ /**
54
+ * Metadata-like object that maps cleanly onto Next.js `Metadata`. We keep
55
+ * the shape framework-agnostic so the core type doesn't depend on Next.js.
56
+ */
57
+ interface PluginMetadata {
58
+ title?: string;
59
+ description?: string;
60
+ openGraph?: {
61
+ title?: string;
62
+ description?: string;
63
+ type?: string;
64
+ url?: string;
65
+ images?: Array<{
66
+ url: string;
67
+ width?: number;
68
+ height?: number;
69
+ alt?: string;
70
+ }>;
71
+ };
72
+ twitter?: {
73
+ card?: 'summary' | 'summary_large_image' | 'app' | 'player';
74
+ site?: string;
75
+ creator?: string;
76
+ title?: string;
77
+ description?: string;
78
+ images?: string[];
79
+ };
80
+ alternates?: {
81
+ canonical?: string;
82
+ types?: Record<string, string>;
83
+ };
84
+ }
85
+ type PluginEventHandler<T extends EventType = EventType> = (event: AmplessEvent<T>, ctx: PluginRuntimeContext) => Promise<void>;
86
+ /**
87
+ * Runtime services injected into hook handlers by the Lambda processor.
88
+ * Decoupling plugins from concrete AWS clients lets us swap implementations
89
+ * (sandbox, tests) without touching plugin code.
90
+ */
91
+ interface PluginRuntimeContext {
92
+ siteId: string;
93
+ /** Read-only view of the cms.config site block (name, url, description). */
94
+ site: Config['site'];
95
+ /** Read all published posts for the site (used by sitemap/RSS). */
96
+ listPublishedPosts(): Promise<Post[]>;
97
+ /**
98
+ * Persist a file under `public/plugins/{pluginName}/{key}` in the site's
99
+ * S3 bucket. Returns the public URL.
100
+ */
101
+ writePublicAsset(key: string, body: string | Uint8Array, contentType: string): Promise<string>;
102
+ }
103
+ /**
104
+ * A font registered with a plugin's OG image renderer. Satori (the engine
105
+ * inside Next.js `ImageResponse`) requires at least one font. We accept
106
+ * either eager `ArrayBuffer` data or a lazy loader so plugins can be
107
+ * imported without forcing the font fetch — the loader runs once per route
108
+ * invocation when the OG image is actually requested.
109
+ */
110
+ interface OgImageFont {
111
+ name: string;
112
+ data: ArrayBuffer | (() => Promise<ArrayBuffer>);
113
+ weight?: number;
114
+ style?: 'normal' | 'italic';
115
+ }
116
+ /**
117
+ * Context passed to a plugin's `ogImage.render`. The `image` helper fetches
118
+ * and decodes an image URL (WebP / AVIF → PNG) and returns a data URL
119
+ * usable inside the JSX, or null on fetch failure / unsupported format.
120
+ */
121
+ interface OgImageRenderContext {
122
+ post: Post;
123
+ site: Config['site'];
124
+ image(url: string): Promise<string | null>;
125
+ }
126
+ interface OgImageConfig {
127
+ fonts: OgImageFont[];
128
+ size?: {
129
+ width: number;
130
+ height: number;
131
+ };
132
+ /**
133
+ * Plugin authors return a React element. Typed loosely (`unknown`) here
134
+ * to avoid pulling React into ampless core's runtime / type
135
+ * dependencies — the OG image plugin package types it precisely on its
136
+ * side.
137
+ */
138
+ render(ctx: OgImageRenderContext): Promise<unknown> | unknown;
139
+ }
140
+ interface AmplessPlugin {
141
+ name: string;
142
+ /** Plugin API version. Currently 1; future versions will be additive. */
143
+ apiVersion: 1;
144
+ trust_level: TrustLevel;
145
+ /** Async event hooks. Run in trust_level-matched Lambda. */
146
+ hooks?: {
147
+ [K in EventType]?: PluginEventHandler<K>;
148
+ };
149
+ /**
150
+ * Per-post metadata generator. Pure function, called from Next.js
151
+ * generateMetadata(). Must not have side effects.
152
+ */
153
+ metadata?(post: Post, site: Config['site']): PluginMetadata;
154
+ /**
155
+ * Site-level metadata (root layout). Returned per request.
156
+ */
157
+ siteMetadata?(site: Config['site']): PluginMetadata;
158
+ /**
159
+ * Dynamic OG image renderer. The dispatcher route (e.g.
160
+ * `app/site/[siteId]/og/[slug]/route.ts`) reads this and feeds the
161
+ * element into Next.js `ImageResponse`. Only one plugin should set this
162
+ * — the route resolves the first plugin in `cms.config.plugins` that
163
+ * declares `ogImage`.
164
+ */
165
+ ogImage?: OgImageConfig;
166
+ }
167
+ declare function definePlugin(p: AmplessPlugin): AmplessPlugin;
168
+
169
+ type ContentFormat = 'tiptap' | 'markdown' | 'html';
170
+ type PostStatus = 'draft' | 'published';
171
+ interface Post {
172
+ postId: string;
173
+ siteId: string;
174
+ slug: string;
175
+ title: string;
176
+ excerpt?: string;
177
+ format: ContentFormat;
178
+ body: unknown;
179
+ status: PostStatus;
180
+ publishedAt?: string;
181
+ tags?: string[];
182
+ }
183
+ interface Page {
184
+ pageId: string;
185
+ siteId: string;
186
+ slug: string;
187
+ title: string;
188
+ format: ContentFormat;
189
+ body: unknown;
190
+ status: PostStatus;
191
+ publishedAt?: string;
192
+ }
193
+ interface Media {
194
+ mediaId: string;
195
+ siteId: string;
196
+ src: string;
197
+ mimeType: string;
198
+ size: number;
199
+ delivery: 'nextjs' | 's3-direct';
200
+ }
201
+ type ImageDisplay = 'inline' | 'lightbox';
202
+ /**
203
+ * How dates (publishedAt etc.) are rendered on public pages.
204
+ * - 'iso' YYYY-MM-DD (default; SSR-safe, locale-neutral)
205
+ * - 'locale' browser/server locale via Date.toLocaleDateString()
206
+ * (warning: server uses Node default locale, so SSR/CSR may diverge)
207
+ * - 'long' "April 27, 2026" (en-US long form)
208
+ */
209
+ type DateFormat = 'iso' | 'locale' | 'long';
210
+ interface MediaProcessingDefaults {
211
+ /** Clamp the longer edge to this many pixels (default 2400). */
212
+ maxDimension?: number;
213
+ /** Default output format (default 'webp'). */
214
+ format?: 'webp' | 'jpeg' | 'original';
215
+ /** Lossy quality 0..1 used when format is lossy (default 0.85). */
216
+ quality?: number;
217
+ /** Use lossless WebP for PNG inputs (default true). */
218
+ losslessForPng?: boolean;
219
+ }
220
+ /**
221
+ * Per-site override. The top-level `Config.site` provides defaults that
222
+ * fall through when these are unset. `domains` is required because it's
223
+ * how the middleware maps incoming requests to a siteId.
224
+ */
225
+ interface SiteConfig {
226
+ /** Hostnames this site responds to (subdomains, separate apex domains, both fine). */
227
+ domains: string[];
228
+ /** Override `Config.site.name` for this site. */
229
+ name?: string;
230
+ /** Override `Config.site.url` (canonical) for this site. */
231
+ url?: string;
232
+ /** Override `Config.site.description` for this site. */
233
+ description?: string;
234
+ }
235
+ interface Config {
236
+ site: {
237
+ name: string;
238
+ url: string;
239
+ description?: string;
240
+ };
241
+ media?: {
242
+ delivery?: 'nextjs' | 's3-direct';
243
+ /** How embedded images are presented on the public site. */
244
+ imageDisplay?: ImageDisplay;
245
+ /** Max content width for inline images (CSS value, default '100%'). */
246
+ imageMaxWidth?: string;
247
+ /** Defaults for the upload-time image processing UI. */
248
+ processing?: MediaProcessingDefaults;
249
+ };
250
+ /** How dates render on public pages. Default 'iso' (YYYY-MM-DD). */
251
+ dateFormat?: DateFormat;
252
+ /**
253
+ * IANA timezone for date display (e.g. 'Asia/Tokyo', 'America/New_York').
254
+ * Default 'UTC'. Used so SSR and CSR always produce the same string —
255
+ * relying on the runtime's local TZ would drift between Node (UTC in
256
+ * production) and the browser.
257
+ */
258
+ timezone?: string;
259
+ /**
260
+ * UI locale for the admin app. The scaffolded project ships
261
+ * `locales/<code>.json` dictionaries; defaults are `en` and `ja`.
262
+ * Add a new language by dropping `locales/<code>.json` in and
263
+ * updating the dictionary map in `lib/i18n.ts`. Per-site override is
264
+ * possible via the `locale` site setting.
265
+ */
266
+ locale?: string;
267
+ /**
268
+ * Multi-site configuration. When 2+ entries are declared, the runtime
269
+ * switches to multi-site mode (host-based routing + Cache-Control:
270
+ * private). When 0 or 1 entries, single-site mode is used.
271
+ */
272
+ sites?: Record<string, SiteConfig>;
273
+ /**
274
+ * Active plugins. Each entry is the result of a plugin factory call
275
+ * (e.g. `seoPlugin({ ... })`) or a raw AmplessPlugin object. Strings are
276
+ * accepted for backward compatibility with the legacy v0 config but are
277
+ * ignored by the runtime.
278
+ */
279
+ plugins?: Array<AmplessPlugin | string>;
280
+ }
281
+ type Role = 'reader' | 'editor' | 'admin';
282
+ interface AuthContext {
283
+ userId: string;
284
+ role: Role;
285
+ source: 'cognito' | 'api-key' | 'mcp';
286
+ }
287
+
288
+ declare function defineConfig(config: Config): Config;
289
+
290
+ type FieldType = 'string' | 'text' | 'number' | 'boolean' | 'datetime' | 'image' | 'reference';
291
+ interface FieldDefinition {
292
+ type: FieldType;
293
+ label?: string;
294
+ required?: boolean;
295
+ default?: unknown;
296
+ }
297
+ interface ContentTypeDefinition {
298
+ name: string;
299
+ label: string;
300
+ fields: Record<string, FieldDefinition>;
301
+ }
302
+ declare function defineSchema<T extends Record<string, ContentTypeDefinition>>(schema: T): T;
303
+
304
+ interface ListOptions {
305
+ siteId?: string;
306
+ limit?: number;
307
+ status?: 'draft' | 'published' | 'all';
308
+ }
309
+ type CreatePostInput = Omit<Post, 'postId'> & {
310
+ postId?: string;
311
+ };
312
+ interface PostsProvider {
313
+ list(opts?: ListOptions): Promise<Post[]>;
314
+ get(slug: string, opts?: {
315
+ siteId?: string;
316
+ }): Promise<Post | null>;
317
+ getById(postId: string, opts?: {
318
+ siteId?: string;
319
+ }): Promise<Post | null>;
320
+ create(data: CreatePostInput): Promise<Post>;
321
+ update(postId: string, data: Partial<Post>, opts?: {
322
+ siteId?: string;
323
+ }): Promise<Post>;
324
+ remove(postId: string, opts?: {
325
+ siteId?: string;
326
+ }): Promise<void>;
327
+ }
328
+ declare function setPostsProvider(p: PostsProvider): void;
329
+ declare function hasPostsProvider(): boolean;
330
+ declare function listPosts(opts?: ListOptions): Promise<Post[]>;
331
+ declare function getPost(slug: string, opts?: {
332
+ siteId?: string;
333
+ }): Promise<Post | null>;
334
+ declare function getPostById(postId: string, opts?: {
335
+ siteId?: string;
336
+ }): Promise<Post | null>;
337
+ declare function createPost(data: CreatePostInput): Promise<Post>;
338
+ declare function updatePost(postId: string, data: Partial<Post>, opts?: {
339
+ siteId?: string;
340
+ }): Promise<Post>;
341
+ declare function deletePost(postId: string, opts?: {
342
+ siteId?: string;
343
+ }): Promise<void>;
344
+
345
+ /**
346
+ * Single-site fallback identifier. Used when `cms.config.sites` is unset
347
+ * or empty — keeps existing single-site code paths working without
348
+ * special-casing.
349
+ */
350
+ declare const DEFAULT_SITE_ID = "default";
351
+ /**
352
+ * Resolve a hostname to a configured siteId.
353
+ *
354
+ * - Single-site mode (no `sites` defined or empty): always returns
355
+ * `DEFAULT_SITE_ID` regardless of host. The single site catches
356
+ * every request.
357
+ * - Multi-site mode: looks up the host in each site's `domains` list.
358
+ * Returns `null` if the host is not registered (caller should 404).
359
+ *
360
+ * The host comparison is case-insensitive; ports are not stripped here
361
+ * — pass the bare hostname (e.g. `'site-a.example.com'`).
362
+ */
363
+ declare function resolveSiteId(host: string, config: Config): string | null;
364
+ /**
365
+ * Multi-site mode iff two or more sites are declared. A single declared
366
+ * site is treated as single-site mode (the explicit declaration just lets
367
+ * the operator name the site, but no host disambiguation is needed).
368
+ */
369
+ declare function isMultiSite(config: Config): boolean;
370
+ /**
371
+ * Site-effective name / url / description for a given siteId. Per-site
372
+ * `sites.{id}.{name|url|description}` overrides the top-level `site.*`
373
+ * defaults; otherwise falls through.
374
+ */
375
+ declare function siteFor(siteId: string, config: Config): {
376
+ name: string;
377
+ url: string;
378
+ description?: string;
379
+ };
380
+ /**
381
+ * Build the denormalized GSI key for `bySiteIdStatus`. All Post writes
382
+ * (admin client, MCP tools) must set this so the public-read resolvers
383
+ * can do a single Query without table-level filtering.
384
+ */
385
+ declare function composeSiteIdStatus(siteId: string, status: PostStatus): string;
386
+ /**
387
+ * Build the denormalized GSI key for `bySiteIdSlug`. The public
388
+ * `getPublishedPost(slug)` resolver does an O(1) PK lookup against
389
+ * this index, so every Post write must set it alongside the slug.
390
+ */
391
+ declare function composeSiteIdSlug(siteId: string, slug: string): string;
392
+
393
+ interface KvItem<T = unknown> {
394
+ pk: string;
395
+ sk: string;
396
+ value: T;
397
+ /** Unix epoch seconds. Absent → persistent. */
398
+ ttl?: number;
399
+ }
400
+ interface KvStore {
401
+ get<T = unknown>(pk: string, sk: string): Promise<T | null>;
402
+ query<T = unknown>(pk: string): Promise<KvItem<T>[]>;
403
+ put(pk: string, sk: string, value: unknown, opts?: {
404
+ ttlSeconds?: number;
405
+ }): Promise<void>;
406
+ remove(pk: string, sk: string): Promise<void>;
407
+ }
408
+ declare function setKvStore(s: KvStore): void;
409
+ declare function hasKvStore(): boolean;
410
+ declare const SITE_CONFIG_PK: (siteId: string) => string;
411
+ declare function getSiteSetting<T = unknown>(siteId: string, key: string): Promise<T | null>;
412
+ declare function setSiteSetting(siteId: string, key: string, value: unknown): Promise<void>;
413
+ declare function deleteSiteSetting(siteId: string, key: string): Promise<void>;
414
+ /**
415
+ * Fetch every setting for a site as a flat map (`{ 'site.name': 'My Blog', ... }`).
416
+ * Use `unflattenSettings` to convert to the nested shape if needed.
417
+ */
418
+ declare function listSiteSettings(siteId?: string): Promise<Record<string, unknown>>;
419
+ declare function flattenSettings(obj: Record<string, unknown>, prefix?: string): Record<string, unknown>;
420
+ declare function unflattenSettings(flat: Record<string, unknown>): Record<string, unknown>;
421
+
422
+ /**
423
+ * Format a date for display in a fixed timezone. SSR-safe — the same
424
+ * (input, format, timezone) tuple yields the same string on Node and in
425
+ * the browser, so React hydration never drifts.
426
+ *
427
+ * @param input Date, ISO string, or epoch ms
428
+ * @param format 'iso' (YYYY-MM-DD), 'long' (April 27, 2026), 'locale'
429
+ * @param timezone IANA TZ name. Default 'UTC'.
430
+ */
431
+ declare function formatDate(input: Date | string | number, format?: DateFormat, timezone?: string): string;
432
+
433
+ declare function escapeXml(s: string): string;
434
+
435
+ /**
436
+ * Build the public S3 URL for an object key, in the regional virtual-host
437
+ * style: `https://{bucket}.s3.{region}.amazonaws.com/{key}`. The legacy
438
+ * non-regional form (`s3.amazonaws.com`) issues redirects and is avoided.
439
+ */
440
+ declare function formatPublicAssetUrl(bucket: string, region: string, key: string): string;
441
+
442
+ /**
443
+ * Extract the first image URL from a post body. Used by plugins (e.g.
444
+ * plugin-og-image) that want to derive an image from the post content
445
+ * without requiring authors to set an explicit featured image field.
446
+ *
447
+ * Returns null if the post has no image, or if the body shape doesn't
448
+ * match the declared `post.format`.
449
+ *
450
+ * Format handling:
451
+ * - 'tiptap' — body is the tiptap JSON tree; walks for `type === 'image'`
452
+ * - 'markdown' — body is a string; finds the first `![alt](src)`
453
+ * - 'html' — body is a string; finds the first `<img src="...">`
454
+ */
455
+ declare function extractFirstImageUrl(post: Post): string | null;
456
+
457
+ type ThemeFieldType = 'color' | 'text' | 'select' | 'image' | 'length' | 'fontFamily' | 'linkList';
458
+ /**
459
+ * Single entry in a `linkList` field. Stored as part of a JSON array
460
+ * under the field's storage key. `url` may be:
461
+ * - a relative path (`/about`)
462
+ * - an absolute URL (`https://example.com`)
463
+ * - a tag reference (`tag:guide`) — themes interpret this as
464
+ * "expand to a list of posts with this tag" rather than rendering
465
+ * a literal link.
466
+ */
467
+ interface LinkListItem {
468
+ label: string;
469
+ url: string;
470
+ }
471
+ /**
472
+ * A user-facing string in a manifest. Either a plain string (rendered
473
+ * as-is, regardless of locale) or a per-locale map (the renderer picks
474
+ * the active locale, falling back to `en`, then to any value).
475
+ *
476
+ * Themes that ship in a single language can keep these as plain
477
+ * strings. The default themes use the map form so the same manifest
478
+ * works for both built-in dictionaries.
479
+ */
480
+ type LocalizedString = string | Record<string, string>;
481
+ interface ThemeFieldBase {
482
+ /** Storage key. Persisted as `theme.{key}` in site settings. */
483
+ key: string;
484
+ label: LocalizedString;
485
+ description?: LocalizedString;
486
+ /** Optional UI grouping (e.g. 'Colors', 'Typography', 'Branding'). */
487
+ group?: LocalizedString;
488
+ /** Used when no override is set. Always a string for storage uniformity. */
489
+ default: string;
490
+ /**
491
+ * If set, the loader injects `${cssVar}: ${value}` into a `:root`
492
+ * style block on every public page, so CSS rules using
493
+ * `var(${cssVar})` pick up overrides at render time.
494
+ *
495
+ * Fields without `cssVar` (e.g. logo URL, header tagline) are exposed
496
+ * to template code via `loadThemeConfig()` instead.
497
+ */
498
+ cssVar?: string;
499
+ }
500
+ interface ThemeColorField extends ThemeFieldBase {
501
+ type: 'color';
502
+ }
503
+ interface ThemeTextField extends ThemeFieldBase {
504
+ type: 'text';
505
+ maxLength?: number;
506
+ }
507
+ interface ThemeSelectField extends ThemeFieldBase {
508
+ type: 'select';
509
+ options: ReadonlyArray<{
510
+ value: string;
511
+ label: LocalizedString;
512
+ }>;
513
+ }
514
+ interface ThemeImageField extends ThemeFieldBase {
515
+ type: 'image';
516
+ }
517
+ interface ThemeLengthField extends ThemeFieldBase {
518
+ type: 'length';
519
+ }
520
+ interface ThemeFontFamilyField extends ThemeFieldBase {
521
+ type: 'fontFamily';
522
+ options: ReadonlyArray<{
523
+ value: string;
524
+ label: LocalizedString;
525
+ }>;
526
+ }
527
+ /**
528
+ * A repeatable list of {label, url} entries — used for nav menus,
529
+ * footer link sets, sidebar groups, etc. Stored in KvStore as a JSON
530
+ * string so it fits the existing `string`-valued site-settings cache.
531
+ *
532
+ * `default` is declared as a plain array for ergonomics; the loader
533
+ * stringifies it on the fly so manifest authors don't have to call
534
+ * JSON.stringify by hand.
535
+ */
536
+ interface ThemeLinkListField extends Omit<ThemeFieldBase, 'default' | 'cssVar'> {
537
+ type: 'linkList';
538
+ default: ReadonlyArray<LinkListItem>;
539
+ /** Cap admin-supplied list length. Default 50. */
540
+ maxItems?: number;
541
+ }
542
+ type ThemeField = ThemeColorField | ThemeTextField | ThemeSelectField | ThemeImageField | ThemeLengthField | ThemeFontFamilyField | ThemeLinkListField;
543
+ interface ThemeManifest {
544
+ /** Theme directory name (`themes/<name>/`). */
545
+ name: string;
546
+ label: LocalizedString;
547
+ description?: LocalizedString;
548
+ fields: ReadonlyArray<ThemeField>;
549
+ }
550
+ declare function defineTheme(m: ThemeManifest): ThemeManifest;
551
+ /**
552
+ * Resolve a `LocalizedString` to a plain string for display.
553
+ * Strings pass through; maps pick `locale` → `fallback` → any value
554
+ * → empty. The empty fallback keeps the renderer from crashing on a
555
+ * malformed manifest while making the missing translation visible.
556
+ */
557
+ declare function resolveLocalized(value: LocalizedString | undefined, locale: string, fallback?: string): string;
558
+ interface ThemeRouteContext<P = Record<string, string>> {
559
+ params: Promise<P & {
560
+ siteId: string;
561
+ }>;
562
+ }
563
+ interface ThemeModule {
564
+ /** Stable identifier — must match the directory name and the value
565
+ * stored as `theme.active`. */
566
+ name: string;
567
+ manifest: ThemeManifest;
568
+ /**
569
+ * Server components rendered by the dispatcher routes under
570
+ * `app/site/[siteId]/`. Each theme MUST provide Home; Post / Tag are
571
+ * recommended but optional (dispatcher 404s when missing).
572
+ */
573
+ components: {
574
+ Home: (ctx: ThemeRouteContext) => Promise<unknown> | unknown;
575
+ Post?: (ctx: ThemeRouteContext<{
576
+ slug: string;
577
+ }>) => Promise<unknown> | unknown;
578
+ Tag?: (ctx: ThemeRouteContext<{
579
+ tag: string;
580
+ }>) => Promise<unknown> | unknown;
581
+ };
582
+ /**
583
+ * Optional `generateMetadata` hooks called by the dispatcher. Same
584
+ * signature as the matching component's params.
585
+ */
586
+ metadata?: {
587
+ Home?: (ctx: ThemeRouteContext) => Promise<unknown> | unknown;
588
+ Post?: (ctx: ThemeRouteContext<{
589
+ slug: string;
590
+ }>) => Promise<unknown> | unknown;
591
+ Tag?: (ctx: ThemeRouteContext<{
592
+ tag: string;
593
+ }>) => Promise<unknown> | unknown;
594
+ };
595
+ /**
596
+ * Optional route handlers for /feed.xml and /sitemap.xml. The
597
+ * dispatcher returns 404 if a theme doesn't provide them.
598
+ */
599
+ routes?: {
600
+ feed?: (ctx: {
601
+ siteId: string;
602
+ request: Request;
603
+ }) => Promise<Response>;
604
+ sitemap?: (ctx: {
605
+ siteId: string;
606
+ request: Request;
607
+ }) => Promise<Response>;
608
+ };
609
+ }
610
+ declare function defineThemeModule(m: ThemeModule): ThemeModule;
611
+ /**
612
+ * Storage key used in KvStore. Prefix `theme.` keeps the namespace
613
+ * separate from `site.*` / `media.*` so unrelated tools can scan
614
+ * settings without colliding.
615
+ */
616
+ declare function themeSettingKey(fieldKey: string): string;
617
+ /**
618
+ * Reject malformed or potentially-injectable values before they reach
619
+ * the KvStore. Admin/editor are trusted but typos and copy-paste
620
+ * mistakes shouldn't be able to break a site's CSS or sneak `</style>`
621
+ * into the inline tag the loader emits.
622
+ *
623
+ * Returns the normalized value, or null if the input is rejected.
624
+ */
625
+ declare function validateThemeValue(field: ThemeField, raw: unknown): string | null;
626
+ /** Parse a stored linkList JSON value into typed items. Tolerant: bad
627
+ * shapes resolve to []. Throwing here would cascade into rendering. */
628
+ declare function parseLinkList(raw: string | undefined | null): LinkListItem[];
629
+ declare function stringifyLinkList(items: ReadonlyArray<LinkListItem>): string;
630
+ /**
631
+ * Detect a `tag:<name>` URL form. Themes use this to render a list of
632
+ * posts under a heading instead of a literal link — useful for docs
633
+ * sidebars and category-style nav.
634
+ */
635
+ declare function isTagListUrl(url: string): {
636
+ tag: string;
637
+ } | null;
638
+ /**
639
+ * Resolve effective values for every manifest field, merging stored
640
+ * overrides on top of defaults. `stored` is the flat settings map keyed
641
+ * by `theme.{key}` — typically the output of `listSiteSettings(siteId)`
642
+ * filtered to theme entries.
643
+ */
644
+ declare function resolveThemeValues(manifest: ThemeManifest, stored: Record<string, unknown>): Record<string, string>;
645
+
646
+ declare const VERSION = "0.0.1";
647
+
648
+ export { type AmplessEvent, type AmplessPlugin, type AuthContext, type Config, type ContentEventPayload, type ContentEventType, type ContentFormat, type ContentTypeDefinition, type CreatePostInput, DEFAULT_SITE_ID, type DateFormat, type EventPayloadOf, type EventType, type FieldDefinition, type FieldType, type ImageDisplay, type KvItem, type KvStore, type LinkListItem, type ListOptions, type LocalizedString, type Media, type MediaEventPayload, type MediaEventType, type MediaProcessingDefaults, type OgImageConfig, type OgImageFont, type OgImageRenderContext, type Page, type PluginEventHandler, type PluginMetadata, type PluginRuntimeContext, type Post, type PostStatus, type PostsProvider, type Role, SITE_CONFIG_PK, type SiteConfig, type SiteSettingsEventPayload, type SiteSettingsEventType, type StreamEventName, type ThemeColorField, type ThemeField, type ThemeFieldType, type ThemeFontFamilyField, type ThemeImageField, type ThemeLengthField, type ThemeLinkListField, type ThemeManifest, type ThemeModule, type ThemeRouteContext, type ThemeSelectField, type ThemeTextField, type TrustLevel, VERSION, composeSiteIdSlug, composeSiteIdStatus, createPost, defineConfig, definePlugin, defineSchema, defineTheme, defineThemeModule, deletePost, deleteSiteSetting, detectContentEvents, escapeXml, extractFirstImageUrl, flattenSettings, formatDate, formatPublicAssetUrl, getPost, getPostById, getSiteSetting, hasKvStore, hasPostsProvider, isMultiSite, isTagListUrl, listPosts, listSiteSettings, parseLinkList, resolveLocalized, resolveSiteId, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, siteFor, stringifyLinkList, themeSettingKey, unflattenSettings, updatePost, validateThemeValue };