ampless 1.0.0-alpha.9 → 1.0.0-beta.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +4 -4
- package/README.md +4 -4
- package/dist/index.d.ts +1510 -233
- package/dist/index.js +296 -7
- package/docs/plugin-author-guide.ja.md +1756 -0
- package/docs/plugin-author-guide.md +2415 -0
- package/package.json +14 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
1
3
|
type ContentEventType = 'content.created' | 'content.updated' | 'content.published' | 'content.unpublished' | 'content.deleted';
|
|
2
4
|
type MediaEventType = 'media.uploaded' | 'media.deleted';
|
|
3
5
|
type SiteSettingsEventType = 'site.settings.updated';
|
|
4
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Emitted on every Post mutation (INSERT / MODIFY / REMOVE) with both
|
|
8
|
+
* the previous and the next projection of the row. Drives index-style
|
|
9
|
+
* derivation: the built-in trusted-processor handler uses it to keep
|
|
10
|
+
* the `PostTag` denormalized index in sync without making every write
|
|
11
|
+
* path (admin, MCP, future REST clients) remember to call a helper.
|
|
12
|
+
*
|
|
13
|
+
* Plugins that maintain their own indexes (custom search, sitemaps
|
|
14
|
+
* with per-tag pages, etc.) can subscribe through the same hook
|
|
15
|
+
* surface as the other event types — the diff payload is already in
|
|
16
|
+
* the right shape for "compute add/remove/update".
|
|
17
|
+
*/
|
|
18
|
+
type PostIndexEventType = 'post.index.refresh';
|
|
19
|
+
type EventType = ContentEventType | MediaEventType | SiteSettingsEventType | PostIndexEventType;
|
|
5
20
|
/** Minimal projection of a Post item carried in events (no body, to keep payloads small). */
|
|
6
21
|
interface ContentEventPayload {
|
|
7
22
|
postId: string;
|
|
@@ -23,7 +38,17 @@ interface MediaEventPayload {
|
|
|
23
38
|
*/
|
|
24
39
|
interface SiteSettingsEventPayload {
|
|
25
40
|
}
|
|
26
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Diff payload for `post.index.refresh`. `previous` is null on INSERT;
|
|
43
|
+
* `next` is null on REMOVE; both populated on MODIFY. Subscribers
|
|
44
|
+
* compute the add / remove / update set from this — see the trusted
|
|
45
|
+
* processor's `rebuildPostTags` handler for the canonical example.
|
|
46
|
+
*/
|
|
47
|
+
interface PostIndexEventPayload {
|
|
48
|
+
previous: ContentEventPayload | null;
|
|
49
|
+
next: ContentEventPayload | null;
|
|
50
|
+
}
|
|
51
|
+
type EventPayloadOf<T extends EventType> = T extends ContentEventType ? ContentEventPayload : T extends MediaEventType ? MediaEventPayload : T extends SiteSettingsEventType ? SiteSettingsEventPayload : T extends PostIndexEventType ? PostIndexEventPayload : never;
|
|
27
52
|
interface AmplessEvent<T extends EventType = EventType> {
|
|
28
53
|
type: T;
|
|
29
54
|
payload: EventPayloadOf<T>;
|
|
@@ -46,7 +71,479 @@ declare function detectContentEvents(input: {
|
|
|
46
71
|
newStatus?: 'draft' | 'published';
|
|
47
72
|
}): ContentEventType[];
|
|
48
73
|
|
|
74
|
+
type ThemeFieldType = 'color' | 'text' | 'select' | 'image' | 'length' | 'fontFamily' | 'linkList';
|
|
75
|
+
/**
|
|
76
|
+
* Single entry in a `linkList` field. Stored as part of a JSON array
|
|
77
|
+
* under the field's storage key. `url` may be:
|
|
78
|
+
* - a relative path (`/about`)
|
|
79
|
+
* - an absolute URL (`https://example.com`)
|
|
80
|
+
* - a tag reference (`tag:guide`) — themes interpret this as
|
|
81
|
+
* "expand to a list of posts with this tag" rather than rendering
|
|
82
|
+
* a literal link.
|
|
83
|
+
*/
|
|
84
|
+
interface LinkListItem {
|
|
85
|
+
label: string;
|
|
86
|
+
url: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A user-facing string in a manifest. Either a plain string (rendered
|
|
90
|
+
* as-is, regardless of locale) or a per-locale map (the renderer picks
|
|
91
|
+
* the active locale, falling back to `en`, then to any value).
|
|
92
|
+
*
|
|
93
|
+
* Themes that ship in a single language can keep these as plain
|
|
94
|
+
* strings. The default themes use the map form so the same manifest
|
|
95
|
+
* works for both built-in dictionaries.
|
|
96
|
+
*/
|
|
97
|
+
type LocalizedString = string | Record<string, string>;
|
|
98
|
+
interface ThemeFieldBase {
|
|
99
|
+
/** Storage key. Persisted as `theme.{key}` in site settings. */
|
|
100
|
+
key: string;
|
|
101
|
+
label: LocalizedString;
|
|
102
|
+
description?: LocalizedString;
|
|
103
|
+
/** Optional UI grouping (e.g. 'Colors', 'Typography', 'Branding'). */
|
|
104
|
+
group?: LocalizedString;
|
|
105
|
+
/** Used when no override is set. Always a string for storage uniformity. */
|
|
106
|
+
default: string;
|
|
107
|
+
/**
|
|
108
|
+
* If set, the loader injects `${cssVar}: ${value}` into a `:root`
|
|
109
|
+
* style block on every public page, so CSS rules using
|
|
110
|
+
* `var(${cssVar})` pick up overrides at render time.
|
|
111
|
+
*
|
|
112
|
+
* Fields without `cssVar` (e.g. logo URL, header tagline) are exposed
|
|
113
|
+
* to template code via `loadThemeConfig()` instead.
|
|
114
|
+
*/
|
|
115
|
+
cssVar?: string;
|
|
116
|
+
}
|
|
117
|
+
interface ThemeColorField extends ThemeFieldBase {
|
|
118
|
+
type: 'color';
|
|
119
|
+
}
|
|
120
|
+
interface ThemeTextField extends ThemeFieldBase {
|
|
121
|
+
type: 'text';
|
|
122
|
+
maxLength?: number;
|
|
123
|
+
}
|
|
124
|
+
interface ThemeSelectField extends ThemeFieldBase {
|
|
125
|
+
type: 'select';
|
|
126
|
+
options: ReadonlyArray<{
|
|
127
|
+
value: string;
|
|
128
|
+
label: LocalizedString;
|
|
129
|
+
}>;
|
|
130
|
+
}
|
|
131
|
+
interface ThemeImageField extends ThemeFieldBase {
|
|
132
|
+
type: 'image';
|
|
133
|
+
}
|
|
134
|
+
interface ThemeLengthField extends ThemeFieldBase {
|
|
135
|
+
type: 'length';
|
|
136
|
+
}
|
|
137
|
+
interface ThemeFontFamilyField extends ThemeFieldBase {
|
|
138
|
+
type: 'fontFamily';
|
|
139
|
+
options: ReadonlyArray<{
|
|
140
|
+
value: string;
|
|
141
|
+
label: LocalizedString;
|
|
142
|
+
}>;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A repeatable list of {label, url} entries — used for nav menus,
|
|
146
|
+
* footer link sets, sidebar groups, etc. Stored in KvStore as a JSON
|
|
147
|
+
* string so it fits the existing `string`-valued site-settings cache.
|
|
148
|
+
*
|
|
149
|
+
* `default` is declared as a plain array for ergonomics; the loader
|
|
150
|
+
* stringifies it on the fly so manifest authors don't have to call
|
|
151
|
+
* JSON.stringify by hand.
|
|
152
|
+
*/
|
|
153
|
+
interface ThemeLinkListField extends Omit<ThemeFieldBase, 'default' | 'cssVar'> {
|
|
154
|
+
type: 'linkList';
|
|
155
|
+
default: ReadonlyArray<LinkListItem>;
|
|
156
|
+
/** Cap admin-supplied list length. Default 50. */
|
|
157
|
+
maxItems?: number;
|
|
158
|
+
}
|
|
159
|
+
type ThemeField = ThemeColorField | ThemeTextField | ThemeSelectField | ThemeImageField | ThemeLengthField | ThemeFontFamilyField | ThemeLinkListField;
|
|
160
|
+
interface ThemeManifest {
|
|
161
|
+
/** Theme directory name (`themes/<name>/`). */
|
|
162
|
+
name: string;
|
|
163
|
+
label: LocalizedString;
|
|
164
|
+
description?: LocalizedString;
|
|
165
|
+
fields: ReadonlyArray<ThemeField>;
|
|
166
|
+
}
|
|
167
|
+
declare function defineTheme(m: ThemeManifest): ThemeManifest;
|
|
168
|
+
/**
|
|
169
|
+
* Resolve a `LocalizedString` to a plain string for display.
|
|
170
|
+
* Strings pass through; maps pick `locale` → `fallback` → any value
|
|
171
|
+
* → empty. The empty fallback keeps the renderer from crashing on a
|
|
172
|
+
* malformed manifest while making the missing translation visible.
|
|
173
|
+
*/
|
|
174
|
+
declare function resolveLocalized(value: LocalizedString | undefined, locale: string, fallback?: string): string;
|
|
175
|
+
interface ThemeRouteContext<P = Record<string, string>> {
|
|
176
|
+
params: Promise<P>;
|
|
177
|
+
}
|
|
178
|
+
interface ThemeModule {
|
|
179
|
+
/** Stable identifier — must match the directory name and the value
|
|
180
|
+
* stored as `theme.active`. */
|
|
181
|
+
name: string;
|
|
182
|
+
manifest: ThemeManifest;
|
|
183
|
+
/**
|
|
184
|
+
* Server components rendered by the dispatcher routes
|
|
185
|
+
* (`app/page.tsx`, `app/[slug]/page.tsx`, `app/tag/[tag]/page.tsx`).
|
|
186
|
+
* Each theme MUST provide Home; Post / Tag are recommended but
|
|
187
|
+
* optional (dispatcher 404s when missing).
|
|
188
|
+
*/
|
|
189
|
+
components: {
|
|
190
|
+
Home: (ctx: ThemeRouteContext) => Promise<unknown> | unknown;
|
|
191
|
+
Post?: (ctx: ThemeRouteContext<{
|
|
192
|
+
slug: string;
|
|
193
|
+
}>) => Promise<unknown> | unknown;
|
|
194
|
+
Tag?: (ctx: ThemeRouteContext<{
|
|
195
|
+
tag: string;
|
|
196
|
+
}>) => Promise<unknown> | unknown;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Optional `generateMetadata` hooks called by the dispatcher. Same
|
|
200
|
+
* signature as the matching component's params.
|
|
201
|
+
*/
|
|
202
|
+
metadata?: {
|
|
203
|
+
Home?: (ctx: ThemeRouteContext) => Promise<unknown> | unknown;
|
|
204
|
+
Post?: (ctx: ThemeRouteContext<{
|
|
205
|
+
slug: string;
|
|
206
|
+
}>) => Promise<unknown> | unknown;
|
|
207
|
+
Tag?: (ctx: ThemeRouteContext<{
|
|
208
|
+
tag: string;
|
|
209
|
+
}>) => Promise<unknown> | unknown;
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* Optional route handlers for /feed.xml and /sitemap.xml. The
|
|
213
|
+
* dispatcher returns 404 if a theme doesn't provide them.
|
|
214
|
+
*/
|
|
215
|
+
routes?: {
|
|
216
|
+
feed?: (ctx: {
|
|
217
|
+
request: Request;
|
|
218
|
+
}) => Promise<Response>;
|
|
219
|
+
sitemap?: (ctx: {
|
|
220
|
+
request: Request;
|
|
221
|
+
}) => Promise<Response>;
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
declare function defineThemeModule(m: ThemeModule): ThemeModule;
|
|
225
|
+
/**
|
|
226
|
+
* Storage key used in KvStore. Prefix `theme.` keeps the namespace
|
|
227
|
+
* separate from `site.*` / `media.*` so unrelated tools can scan
|
|
228
|
+
* settings without colliding.
|
|
229
|
+
*/
|
|
230
|
+
declare function themeSettingKey(fieldKey: string): string;
|
|
231
|
+
/**
|
|
232
|
+
* Reject malformed or potentially-injectable values before they reach
|
|
233
|
+
* the KvStore. Admin/editor are trusted but typos and copy-paste
|
|
234
|
+
* mistakes shouldn't be able to break a site's CSS or sneak `</style>`
|
|
235
|
+
* into the inline tag the loader emits.
|
|
236
|
+
*
|
|
237
|
+
* Returns the normalized value, or null if the input is rejected.
|
|
238
|
+
*/
|
|
239
|
+
/**
|
|
240
|
+
* Split a stored color value into its light / dark components.
|
|
241
|
+
*
|
|
242
|
+
* Accepts two storage forms:
|
|
243
|
+
* - Single value: `oklch(...)` / `#abcdef` / etc. → `{ light: value, dark: null }`
|
|
244
|
+
* - Pair: `light-dark(L, D)` → `{ light: L, dark: D }`
|
|
245
|
+
*
|
|
246
|
+
* Splits on the top-level comma (depth-aware) so nested commas inside
|
|
247
|
+
* `rgb(...)` / `hsl(...)` don't trip the parser.
|
|
248
|
+
*/
|
|
249
|
+
declare function parseColorPair(value: string): {
|
|
250
|
+
light: string;
|
|
251
|
+
dark: string | null;
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Build the storage string for a color field. When `dark` is non-empty
|
|
255
|
+
* and differs from `light`, returns `light-dark(light, dark)`; otherwise
|
|
256
|
+
* returns the bare `light` value. The runtime emits the result verbatim
|
|
257
|
+
* into the inline `:root { --foo: <value> }` override; `light-dark()`
|
|
258
|
+
* is a Baseline-2024 CSS function so the browser picks per mode.
|
|
259
|
+
*/
|
|
260
|
+
declare function formatColorPair(light: string, dark?: string | null): string;
|
|
261
|
+
declare function validateThemeValue(field: ThemeField, raw: unknown): string | null;
|
|
262
|
+
/** Parse a stored linkList JSON value into typed items. Tolerant: bad
|
|
263
|
+
* shapes resolve to []. Throwing here would cascade into rendering. */
|
|
264
|
+
declare function parseLinkList(raw: string | undefined | null): LinkListItem[];
|
|
265
|
+
declare function stringifyLinkList(items: ReadonlyArray<LinkListItem>): string;
|
|
266
|
+
/**
|
|
267
|
+
* Detect a `tag:<name>` URL form. Themes use this to render a list of
|
|
268
|
+
* posts under a heading instead of a literal link — useful for docs
|
|
269
|
+
* sidebars and category-style nav.
|
|
270
|
+
*/
|
|
271
|
+
declare function isTagListUrl(url: string): {
|
|
272
|
+
tag: string;
|
|
273
|
+
} | null;
|
|
274
|
+
/**
|
|
275
|
+
* Resolve effective values for every manifest field, merging stored
|
|
276
|
+
* overrides on top of defaults. `stored` is the flat settings map keyed
|
|
277
|
+
* by `theme.{key}` — typically the output of `listSiteSettings()`
|
|
278
|
+
* filtered to theme entries.
|
|
279
|
+
*/
|
|
280
|
+
declare function resolveThemeValues(manifest: ThemeManifest, stored: Record<string, unknown>): Record<string, string>;
|
|
281
|
+
|
|
49
282
|
type TrustLevel = 'untrusted' | 'trusted' | 'privileged';
|
|
283
|
+
/**
|
|
284
|
+
* Plugin capability declarations. The runtime uses this list for
|
|
285
|
+
* declaration-vs-implementation reconciliation warnings and (in later
|
|
286
|
+
* phases) for `allowCapabilities` gating in `cms.config.ts`.
|
|
287
|
+
*
|
|
288
|
+
* Active capabilities:
|
|
289
|
+
* - `publicHead` / `publicBody`: descriptor-based head/body injection.
|
|
290
|
+
* - `metadata` / `eventHooks`: name-only declaration for existing surfaces.
|
|
291
|
+
* - `adminSettings`: admin-managed public settings manifest.
|
|
292
|
+
* - `writePublicAsset`: trusted hook context can write namespaced public assets.
|
|
293
|
+
* - `schema`: per-post body injection via `publicBodyForPost`,
|
|
294
|
+
* scoped to JSON-LD `<script type="application/ld+json">`. Themes
|
|
295
|
+
* render the descriptors by calling `ampless.publicBodyForPost(post)`
|
|
296
|
+
* in their post template.
|
|
297
|
+
* - `secretSettings`: admin-managed secret settings stored in the isolated
|
|
298
|
+
* `PluginSecret` DynamoDB model. Requires `trust_level: 'trusted'`.
|
|
299
|
+
* Trusted hooks access secrets via `ctx.secret<T>(key)`.
|
|
300
|
+
*
|
|
301
|
+
* Reserved capabilities are accepted by the type so that plugins can
|
|
302
|
+
* declare future intent, but the runtime does nothing with them yet.
|
|
303
|
+
* Declaring a reserved capability today is harmless, but the runtime
|
|
304
|
+
* won't expose any new surface for it until the matching phase ships.
|
|
305
|
+
*/
|
|
306
|
+
type PluginCapability = 'publicHead' | 'publicBody' | 'metadata' | 'eventHooks' | 'adminSettings' | 'writePublicAsset' | 'schema' | 'publicHtmlForPost' | 'secretSettings' | 'contentFields' | 'publicPostScript' | 'adminPage' | 'serverRoute' | 'network' | 'scheduler' | 'storageWrite' | 'privilegedSystem' | 'cspReady';
|
|
307
|
+
/**
|
|
308
|
+
* Loading strategy for `script` / `inlineScript` descriptors.
|
|
309
|
+
*
|
|
310
|
+
* - `afterInteractive` (default): load after hydration, non-blocking.
|
|
311
|
+
* The runtime adds `async` for external scripts when neither
|
|
312
|
+
* `async` nor `defer` is set explicitly. `inlineScript` is emitted
|
|
313
|
+
* inline as-is in Phase 1 (strategy is informational only for
|
|
314
|
+
* inline; see comments in `plugin-head.ts`).
|
|
315
|
+
* - `lazyOnload`: defer load further. Phase 1 maps this to `defer`
|
|
316
|
+
* for external scripts; full lazy-load (next/script's idle
|
|
317
|
+
* scheduling) is a future enhancement.
|
|
318
|
+
*
|
|
319
|
+
* `beforeInteractive` is intentionally excluded — App Router does not
|
|
320
|
+
* support emitting plain `<script>` elements that block hydration from
|
|
321
|
+
* the runtime layer, and the spec defers that case to the future
|
|
322
|
+
* developer extension surface.
|
|
323
|
+
*/
|
|
324
|
+
type ScriptStrategy = 'afterInteractive' | 'lazyOnload';
|
|
325
|
+
/**
|
|
326
|
+
* Context passed to `publicHead` / `publicBodyEnd`. Phase 1 carries
|
|
327
|
+
* only the site-wide config block. Phase 2 adds the `setting()`
|
|
328
|
+
* accessor for admin-managed `settings.public` values; per-route /
|
|
329
|
+
* per-post context lands in Phase 4 (`plugin-per-post-rfp.md`).
|
|
330
|
+
*/
|
|
331
|
+
interface PluginPublicRenderContext {
|
|
332
|
+
site: Config['site'];
|
|
333
|
+
/**
|
|
334
|
+
* Resolve a public setting value for the active plugin instance.
|
|
335
|
+
* Returns `stored ?? manifest.default ?? undefined`. Stored values
|
|
336
|
+
* are read at request time from the DDB → S3 site-settings cache
|
|
337
|
+
* pipeline. Bound by the runtime when the plugin declares
|
|
338
|
+
* `settings.public` — plugins without a manifest can still call
|
|
339
|
+
* this, in which case it always returns `undefined`.
|
|
340
|
+
*
|
|
341
|
+
* The generic type is a convenience cast: the runtime does not
|
|
342
|
+
* coerce values, so callers should pick `T` to match the field's
|
|
343
|
+
* declared type (`text` → `string`, `number` → `number`, etc.).
|
|
344
|
+
* Validation at admin save time guarantees stored values match the
|
|
345
|
+
* field's declared shape, so the cast is safe in practice.
|
|
346
|
+
*/
|
|
347
|
+
setting<T = unknown>(key: string): T | undefined;
|
|
348
|
+
/**
|
|
349
|
+
* Request-scoped CSP nonce reservation. Always `undefined` in Phase 1
|
|
350
|
+
* — the runtime does not populate this field yet. Middleware/SSR
|
|
351
|
+
* threading lands with the future CSP RFP. Plugin authors who want to
|
|
352
|
+
* be ready for the future stamping can declare
|
|
353
|
+
* `inlineScript.nonce: 'auto'` today; once the middleware-driven
|
|
354
|
+
* threading PR lands, those descriptors will become candidates for
|
|
355
|
+
* runtime nonce stamping.
|
|
356
|
+
*/
|
|
357
|
+
cspNonce?: string;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Descriptor returned by `publicHead()`. The runtime validates each
|
|
361
|
+
* entry (URL scheme denylist, `attrs` allowlist, id collision handling)
|
|
362
|
+
* and renders the surviving descriptors as React elements inside the
|
|
363
|
+
* root layout's `<head>`. Returning arbitrary `ReactNode` is
|
|
364
|
+
* intentionally not offered here — see
|
|
365
|
+
* `docs/architecture/08-plugin-architecture.md` §"Descriptor-based
|
|
366
|
+
* Head/Body Injection".
|
|
367
|
+
*/
|
|
368
|
+
type PublicHeadDescriptor = {
|
|
369
|
+
type: 'script';
|
|
370
|
+
/** Element id; doubles as the duplicate-detection key. */
|
|
371
|
+
id?: string;
|
|
372
|
+
src: string;
|
|
373
|
+
strategy?: ScriptStrategy;
|
|
374
|
+
async?: boolean;
|
|
375
|
+
defer?: boolean;
|
|
376
|
+
/** Allow-listed attributes only (data-*, crossorigin, referrerpolicy, ...). */
|
|
377
|
+
attrs?: Record<string, string | boolean>;
|
|
378
|
+
/**
|
|
379
|
+
* Optional CSP nonce. Same semantics as inlineScript.nonce
|
|
380
|
+
* (`'auto'` is the sentinel for future runtime stamping; any
|
|
381
|
+
* other string is an explicit literal; undefined emits no
|
|
382
|
+
* `nonce` attribute). Phase 1 reservation: runtime accepts
|
|
383
|
+
* but does not propagate. See `inlineScript.nonce` JSDoc for
|
|
384
|
+
* the full description.
|
|
385
|
+
*/
|
|
386
|
+
nonce?: 'auto' | string;
|
|
387
|
+
} | {
|
|
388
|
+
type: 'inlineScript';
|
|
389
|
+
/** Required for duplicate detection and dev warnings. */
|
|
390
|
+
id: string;
|
|
391
|
+
body: string;
|
|
392
|
+
strategy?: ScriptStrategy;
|
|
393
|
+
/**
|
|
394
|
+
* Optional MIME-like script type. Phase 4 allows
|
|
395
|
+
* `'application/ld+json'` only — when set, the runtime emits
|
|
396
|
+
* `<script type="application/ld+json">` and **auto-escapes** the
|
|
397
|
+
* `body` (`<`, `>`, `&`, U+2028, U+2029 → `\uXXXX`) so plugin
|
|
398
|
+
* authors cannot accidentally let a value break out of the
|
|
399
|
+
* script tag. This invariant is applied across all three
|
|
400
|
+
* surfaces (`publicHead` / `publicBodyEnd` / `publicBodyForPost`).
|
|
401
|
+
*
|
|
402
|
+
* Unsupported values are dropped (descriptor and warning).
|
|
403
|
+
* Surface-dependent strictness:
|
|
404
|
+
* - `publicHead` / `publicBodyEnd`: `undefined` (= default JS,
|
|
405
|
+
* backwards compatible) or `'application/ld+json'`.
|
|
406
|
+
* - `publicBodyForPost`: `'application/ld+json'` REQUIRED —
|
|
407
|
+
* the per-post body surface is scoped to JSON-LD only so
|
|
408
|
+
* the schema capability does not become a per-post arbitrary
|
|
409
|
+
* inline-JS channel. A future capability would open that
|
|
410
|
+
* explicitly.
|
|
411
|
+
*/
|
|
412
|
+
scriptType?: 'application/ld+json';
|
|
413
|
+
/**
|
|
414
|
+
* Optional CSP nonce.
|
|
415
|
+
*
|
|
416
|
+
* - `'auto'`: sentinel reserved for future runtime stamping.
|
|
417
|
+
* When the middleware/SSR CSP nonce threading PR lands, the
|
|
418
|
+
* runtime will read `ctx.cspNonce` from the request scope
|
|
419
|
+
* and stamp the rendered `<script>` tag automatically.
|
|
420
|
+
* - any other `string`: explicit nonce literal (advanced;
|
|
421
|
+
* rarely needed).
|
|
422
|
+
* - `undefined`: no `nonce` attribute emitted (default,
|
|
423
|
+
* backward-compatible with non-CSP sites).
|
|
424
|
+
*
|
|
425
|
+
* Phase 1 reservation: the runtime accepts the field but does
|
|
426
|
+
* not propagate it to the rendered element. Declaring
|
|
427
|
+
* `nonce: 'auto'` today is a forward-compatibility hint and
|
|
428
|
+
* does not change the rendered HTML.
|
|
429
|
+
*
|
|
430
|
+
* Note: TypeScript does not type-widen here — `string` already
|
|
431
|
+
* accepts `'auto'` as a literal. The semantic reservation is
|
|
432
|
+
* documented above.
|
|
433
|
+
*/
|
|
434
|
+
nonce?: string;
|
|
435
|
+
} | {
|
|
436
|
+
type: 'meta';
|
|
437
|
+
name?: string;
|
|
438
|
+
property?: string;
|
|
439
|
+
content: string;
|
|
440
|
+
} | {
|
|
441
|
+
type: 'link';
|
|
442
|
+
rel: string;
|
|
443
|
+
href: string;
|
|
444
|
+
as?: string;
|
|
445
|
+
/** Mapped to React's `type` attribute. */
|
|
446
|
+
typeAttr?: string;
|
|
447
|
+
} | {
|
|
448
|
+
type: 'noscript';
|
|
449
|
+
id?: string;
|
|
450
|
+
/**
|
|
451
|
+
* Raw HTML emitted inside `<noscript>` via React's
|
|
452
|
+
* `dangerouslySetInnerHTML`. This is an intentional escape hatch:
|
|
453
|
+
* descriptors otherwise constrain shape (typed props on `meta` /
|
|
454
|
+
* `link` / `script`), but `<noscript>` content is often vendor-
|
|
455
|
+
* supplied (analytics fallbacks, etc.) and cannot be modelled
|
|
456
|
+
* as typed props without an unbounded discriminated union.
|
|
457
|
+
*
|
|
458
|
+
* **Trust model.** This is part of the same public injection
|
|
459
|
+
* surface as `inlineScript` and `script`. It is **not** gated
|
|
460
|
+
* by the plugin's `trust_level` — `untrusted` plugins can emit
|
|
461
|
+
* a `noscript` descriptor too, and the runtime tests in
|
|
462
|
+
* `packages/runtime/src/plugin-head.test.ts` explicitly cover
|
|
463
|
+
* the untrusted case. The trust decision is **plugin install**:
|
|
464
|
+
* an admin installs a plugin, accepting whatever it eventually
|
|
465
|
+
* renders into `<head>` / `<body>`. The boundary is structurally
|
|
466
|
+
* the same as the [editor trust
|
|
467
|
+
* model](../../docs/architecture/04-access-layer-mcp.md#editor-trust-model-specification)
|
|
468
|
+
* — editors can already inject arbitrary `<script>` via post
|
|
469
|
+
* body anyway, so a per-plugin sandbox here wouldn't change the
|
|
470
|
+
* upper bound. If a tighter sandbox is needed, scope it at
|
|
471
|
+
* install time: do not install plugins you do not trust to
|
|
472
|
+
* render arbitrary HTML into your pages.
|
|
473
|
+
*
|
|
474
|
+
* **Author guidance.** Plugin authors are still responsible
|
|
475
|
+
* for the HTML being well-formed — in particular, do not embed
|
|
476
|
+
* `</noscript>` sequences mid-content (such a sequence breaks
|
|
477
|
+
* out of the element and the remainder of the string is parsed
|
|
478
|
+
* as page-level HTML). The runtime does not detect or mask
|
|
479
|
+
* this; the regression test in
|
|
480
|
+
* [`packages/runtime/src/plugin-head.test.ts`](../../packages/runtime/src/plugin-head.test.ts)
|
|
481
|
+
* pins the current passthrough behaviour so any future move
|
|
482
|
+
* to sanitization becomes a deliberate, reviewed change.
|
|
483
|
+
*/
|
|
484
|
+
html: string;
|
|
485
|
+
};
|
|
486
|
+
/**
|
|
487
|
+
* Descriptor returned by `publicBodyEnd()`. Supports the same
|
|
488
|
+
* `script` / `inlineScript` / `noscript` variants as the head, plus
|
|
489
|
+
* the body-only `iframe` variant (used by Google Tag Manager's
|
|
490
|
+
* `<noscript>`-style fallback frame, chat widgets, etc.).
|
|
491
|
+
*/
|
|
492
|
+
type PublicBodyDescriptor = Extract<PublicHeadDescriptor, {
|
|
493
|
+
type: 'script' | 'inlineScript' | 'noscript';
|
|
494
|
+
}> | {
|
|
495
|
+
type: 'iframe';
|
|
496
|
+
id?: string;
|
|
497
|
+
src: string;
|
|
498
|
+
title?: string;
|
|
499
|
+
width?: number;
|
|
500
|
+
height?: number;
|
|
501
|
+
attrs?: Record<string, string | boolean>;
|
|
502
|
+
};
|
|
503
|
+
/**
|
|
504
|
+
* Descriptor returned by `publicBodyForPost()` (Phase 4). Limited to
|
|
505
|
+
* the `inlineScript` variant with `scriptType: 'application/ld+json'`
|
|
506
|
+
* REQUIRED. This narrowing is deliberate:
|
|
507
|
+
* - The per-post body surface is for JSON-LD / structured data only.
|
|
508
|
+
* We do not want the `schema` capability to become a per-post
|
|
509
|
+
* arbitrary inline-JS channel.
|
|
510
|
+
* - `meta` / `link` belong in `<head>`; theme post pages render
|
|
511
|
+
* these descriptors inside `<body>`, so meta/link are excluded.
|
|
512
|
+
*
|
|
513
|
+
* If a future use case needs per-post arbitrary inline JS (e.g.
|
|
514
|
+
* Microsoft Clarity per-page tagging), open it through a new
|
|
515
|
+
* capability such as `publicPostScript` rather than relaxing this.
|
|
516
|
+
*/
|
|
517
|
+
type PublicPostBodyDescriptor = Extract<PublicHeadDescriptor, {
|
|
518
|
+
type: 'inlineScript';
|
|
519
|
+
}> & {
|
|
520
|
+
/** Required for `publicBodyForPost`; the runtime drops any descriptor
|
|
521
|
+
* whose scriptType is not 'application/ld+json'. */
|
|
522
|
+
scriptType: 'application/ld+json';
|
|
523
|
+
};
|
|
524
|
+
/**
|
|
525
|
+
* Slot positions for `publicHtmlForPost` descriptors. v1 ships two
|
|
526
|
+
* fixed slots; additional slots (beforeTitle / sidebar / etc) are
|
|
527
|
+
* deferred until dogfood reveals the need.
|
|
528
|
+
*/
|
|
529
|
+
type PublicPostHtmlPosition = 'beforeContent' | 'afterContent';
|
|
530
|
+
/**
|
|
531
|
+
* Per-post visible HTML descriptor returned by `publicHtmlForPost`.
|
|
532
|
+
* The runtime sanitizes `body` with `sanitize-html` under a strict
|
|
533
|
+
* allowlist before rendering; see the plugin-author guide for how
|
|
534
|
+
* trusted plugins compose with the public surface.
|
|
535
|
+
*
|
|
536
|
+
* `id` is a plugin-local short identifier (e.g. `'display'`). The
|
|
537
|
+
* runtime resolves it to `${instanceId ?? name}:${id}` when building
|
|
538
|
+
* the React wrapper key — plugin authors do not embed their own
|
|
539
|
+
* namespace in `id`.
|
|
540
|
+
*/
|
|
541
|
+
interface PublicPostHtmlDescriptor {
|
|
542
|
+
type: 'html';
|
|
543
|
+
id: string;
|
|
544
|
+
body: string;
|
|
545
|
+
position: PublicPostHtmlPosition;
|
|
546
|
+
}
|
|
50
547
|
/**
|
|
51
548
|
* Metadata-like object that maps cleanly onto Next.js `Metadata`. We keep
|
|
52
549
|
* the shape framework-agnostic so the core type doesn't depend on Next.js.
|
|
@@ -79,7 +576,48 @@ interface PluginMetadata {
|
|
|
79
576
|
types?: Record<string, string>;
|
|
80
577
|
};
|
|
81
578
|
}
|
|
82
|
-
|
|
579
|
+
/**
|
|
580
|
+
* Hook return value reservation (Phase 1: type only, runtime no-op).
|
|
581
|
+
*
|
|
582
|
+
* Today's runtime (after-event SQS Lambdas) ignores the return value
|
|
583
|
+
* entirely. The type is widened so a future PR can land additive
|
|
584
|
+
* directives without breaking plugin authors who publish in the
|
|
585
|
+
* meantime. Likely first use case:
|
|
586
|
+
*
|
|
587
|
+
* - `metrics?: Record<string, number>` — emit observability data
|
|
588
|
+
* without touching CloudWatch SDK directly from the plugin.
|
|
589
|
+
*
|
|
590
|
+
* `cancel` / `post` rewrite-style directives are NOT enabled by this
|
|
591
|
+
* type alone — they would also require `before:*` events to be wired
|
|
592
|
+
* to plugins (currently reserved, see events.ts) and payload shapes
|
|
593
|
+
* to expose mutable bodies. This PR widens the return surface only;
|
|
594
|
+
* richer semantics need follow-on PRs.
|
|
595
|
+
*
|
|
596
|
+
* The `readonly __amplessPluginHookResult?: never` marker is a
|
|
597
|
+
* type-level nominal tag: it prevents `Promise<void | PluginHookResult>`
|
|
598
|
+
* from silently accepting unrelated promise types like
|
|
599
|
+
* `Promise<string>` or `Promise<number>`. Plugin authors do not need
|
|
600
|
+
* to set this field — it is optional and exists only to constrain
|
|
601
|
+
* what the union accepts.
|
|
602
|
+
*/
|
|
603
|
+
interface PluginHookResult {
|
|
604
|
+
readonly __amplessPluginHookResult?: never;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Async event hook handler.
|
|
608
|
+
*
|
|
609
|
+
* The return value is reserved (see `PluginHookResult`). Today's
|
|
610
|
+
* runtime ignores it — returning `undefined` (`Promise<void>`) is
|
|
611
|
+
* the canonical "no-op" and matches existing plugin code without
|
|
612
|
+
* migration. Plugins that explicitly return a `PluginHookResult`
|
|
613
|
+
* object type-check today but do not change runtime behaviour
|
|
614
|
+
* until the matching capability PR lands.
|
|
615
|
+
*
|
|
616
|
+
* Non-PluginHookResult promise types (`Promise<string>`,
|
|
617
|
+
* `Promise<number>`, etc.) are rejected at compile time by the
|
|
618
|
+
* `__amplessPluginHookResult` private marker on `PluginHookResult`.
|
|
619
|
+
*/
|
|
620
|
+
type PluginEventHandler<T extends EventType = EventType> = (event: AmplessEvent<T>, ctx: PluginRuntimeContext) => Promise<void | PluginHookResult>;
|
|
83
621
|
/**
|
|
84
622
|
* Runtime services injected into hook handlers by the Lambda processor.
|
|
85
623
|
* Decoupling plugins from concrete AWS clients lets us swap implementations
|
|
@@ -96,6 +634,57 @@ interface PluginRuntimeContext {
|
|
|
96
634
|
*/
|
|
97
635
|
writePublicAsset(key: string, body: string | Uint8Array, contentType: string): Promise<string>;
|
|
98
636
|
}
|
|
637
|
+
/**
|
|
638
|
+
* Context passed to the `uninstall` lifecycle hook (Phase 1
|
|
639
|
+
* reservation: runtime no-op — see `AmplessPlugin.uninstall`).
|
|
640
|
+
*
|
|
641
|
+
* Phase 1: structurally identical to `PluginRuntimeContext`. The
|
|
642
|
+
* dedicated type exists so future cleanup helpers
|
|
643
|
+
* (`deletePublicAsset`, `deletePluginSetting`, `deletePluginSecret`)
|
|
644
|
+
* can be added here without exposing them to regular event hooks,
|
|
645
|
+
* which take `PluginRuntimeContext` and should not be able to
|
|
646
|
+
* delete state from arbitrary plugin areas.
|
|
647
|
+
*
|
|
648
|
+
* Phase 1 scope is limited to: (a) reserving the type name, (b)
|
|
649
|
+
* reserving the hook signature `(ctx: PluginUninstallContext) =>
|
|
650
|
+
* Promise<void>`. Cleanup helper methods are NOT included in
|
|
651
|
+
* Phase 1 — plugin authors writing `await ctx.deletePublicAsset(...)`
|
|
652
|
+
* today would hit a TS error. The actual cleanup body lands when
|
|
653
|
+
* the future lifecycle-dispatch PR adds the helper methods to this
|
|
654
|
+
* type (additive, no breaking change for plugins that declared an
|
|
655
|
+
* empty `uninstall` body in advance).
|
|
656
|
+
*/
|
|
657
|
+
interface PluginUninstallContext extends PluginRuntimeContext {
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Extended runtime context for **trusted** hook handlers. Adds
|
|
661
|
+
* `secret<T>(key)` — an async accessor that reads from the isolated
|
|
662
|
+
* `PluginSecret` DynamoDB model (which admin / editor groups cannot
|
|
663
|
+
* query). The result is per-invocation cached to avoid redundant DDB
|
|
664
|
+
* calls when the same key is read multiple times inside one hook batch.
|
|
665
|
+
*
|
|
666
|
+
* Cache key is `${instanceId ?? name}:${fieldKey}` to prevent
|
|
667
|
+
* cross-plugin collisions when two plugin instances declare the same
|
|
668
|
+
* `key` (e.g. both have a `'signingSecret'` field).
|
|
669
|
+
*
|
|
670
|
+
* Only available in the trusted processor (`processor-trusted.ts`).
|
|
671
|
+
* The untrusted processor never constructs a `TrustedPluginRuntimeContext`
|
|
672
|
+
* — untrusted hook handlers receive plain `PluginRuntimeContext`, which
|
|
673
|
+
* does not expose `secret`.
|
|
674
|
+
*/
|
|
675
|
+
interface TrustedPluginRuntimeContext extends PluginRuntimeContext {
|
|
676
|
+
/**
|
|
677
|
+
* Read a secret value stored under the plugin's namespace in the
|
|
678
|
+
* `PluginSecret` table. Returns `undefined` when no value has been
|
|
679
|
+
* saved yet. The generic `T` is a convenience cast (same pattern as
|
|
680
|
+
* `ctx.setting<T>()`) — values are always stored as strings, so `T`
|
|
681
|
+
* defaults to `string`.
|
|
682
|
+
*
|
|
683
|
+
* Per-invocation cached: calling `ctx.secret('key')` twice within
|
|
684
|
+
* the same SQS batch hit costs one DDB round-trip.
|
|
685
|
+
*/
|
|
686
|
+
secret<T = string>(key: string): Promise<T | undefined>;
|
|
687
|
+
}
|
|
99
688
|
/**
|
|
100
689
|
* A font registered with a plugin's OG image renderer. Satori (the engine
|
|
101
690
|
* inside Next.js `ImageResponse`) requires at least one font. We accept
|
|
@@ -133,15 +722,532 @@ interface OgImageConfig {
|
|
|
133
722
|
*/
|
|
134
723
|
render(ctx: OgImageRenderContext): Promise<unknown> | unknown;
|
|
135
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* Shared shape for every `PluginSettingField` variant. `T` is the
|
|
727
|
+
* decoded value type (string for text-like fields, number for number,
|
|
728
|
+
* boolean for boolean, etc.). All variants share `key` / `label` /
|
|
729
|
+
* `default` / `required`; type-specific constraints (e.g. `pattern`,
|
|
730
|
+
* `min`, `options`) live on the discriminated branches.
|
|
731
|
+
*/
|
|
732
|
+
interface PluginFieldBase<T> {
|
|
733
|
+
/**
|
|
734
|
+
* Storage key. Stored as `plugins.<instanceId>.<key>`. Must match
|
|
735
|
+
* `PLUGIN_KEY_PATTERN` (`/^[a-zA-Z0-9_-]+$/`) so the `pk.sk` dotted
|
|
736
|
+
* separator survives. Violations are skipped + warned by the
|
|
737
|
+
* runtime/admin normalization pass.
|
|
738
|
+
*/
|
|
739
|
+
key: string;
|
|
740
|
+
label: LocalizedString;
|
|
741
|
+
description?: LocalizedString;
|
|
742
|
+
/** Used by the runtime when no admin-stored value is present. */
|
|
743
|
+
default?: T;
|
|
744
|
+
/**
|
|
745
|
+
* When true, an empty / undefined value is rejected at save time
|
|
746
|
+
* and the resolver returns `undefined`. When false (default),
|
|
747
|
+
* string-like fields accept empty string as a valid "disabled"
|
|
748
|
+
* value while non-string fields still reject empty strings.
|
|
749
|
+
*/
|
|
750
|
+
required?: boolean;
|
|
751
|
+
/** Optional UI grouping (mirrors `ThemeField.group`). */
|
|
752
|
+
group?: LocalizedString;
|
|
753
|
+
}
|
|
754
|
+
interface PluginTextField extends PluginFieldBase<string> {
|
|
755
|
+
type: 'text';
|
|
756
|
+
maxLength?: number;
|
|
757
|
+
/** RegExp source (e.g. `'^G-[A-Z0-9]+$'`). Validated against
|
|
758
|
+
* non-empty values; empty string skips the check. */
|
|
759
|
+
pattern?: string;
|
|
760
|
+
/** Placeholder for the admin input. */
|
|
761
|
+
placeholder?: string;
|
|
762
|
+
}
|
|
763
|
+
interface PluginTextareaField extends PluginFieldBase<string> {
|
|
764
|
+
type: 'textarea';
|
|
765
|
+
maxLength?: number;
|
|
766
|
+
placeholder?: string;
|
|
767
|
+
/** Rendered rows hint for the admin textarea. */
|
|
768
|
+
rows?: number;
|
|
769
|
+
}
|
|
770
|
+
interface PluginBooleanField extends PluginFieldBase<boolean> {
|
|
771
|
+
type: 'boolean';
|
|
772
|
+
}
|
|
773
|
+
interface PluginNumberField extends PluginFieldBase<number> {
|
|
774
|
+
type: 'number';
|
|
775
|
+
min?: number;
|
|
776
|
+
max?: number;
|
|
777
|
+
step?: number;
|
|
778
|
+
}
|
|
779
|
+
interface PluginSelectField extends PluginFieldBase<string> {
|
|
780
|
+
type: 'select';
|
|
781
|
+
options: ReadonlyArray<{
|
|
782
|
+
value: string;
|
|
783
|
+
label: LocalizedString;
|
|
784
|
+
}>;
|
|
785
|
+
}
|
|
786
|
+
interface PluginUrlField extends PluginFieldBase<string> {
|
|
787
|
+
type: 'url';
|
|
788
|
+
placeholder?: string;
|
|
789
|
+
/** When true, relative paths (`/foo`, `./foo`) pass validation; default true. */
|
|
790
|
+
allowRelative?: boolean;
|
|
791
|
+
}
|
|
792
|
+
interface PluginCodeField extends PluginFieldBase<string> {
|
|
793
|
+
type: 'code';
|
|
794
|
+
/** Display-only language label (e.g. `'js'`, `'css'`, `'html'`). */
|
|
795
|
+
language?: string;
|
|
796
|
+
maxLength?: number;
|
|
797
|
+
placeholder?: string;
|
|
798
|
+
rows?: number;
|
|
799
|
+
}
|
|
800
|
+
interface PluginJsonField extends PluginFieldBase<unknown> {
|
|
801
|
+
type: 'json';
|
|
802
|
+
placeholder?: string;
|
|
803
|
+
rows?: number;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Sub-fields allowed inside a `PluginRepeatableField`. Restricted to
|
|
807
|
+
* the scalar-shape field types so the v1 admin editor can render each
|
|
808
|
+
* cell with the existing `renderScalarInput` seam. Code / json /
|
|
809
|
+
* repeatable are excluded — code is rarely useful per-item, json
|
|
810
|
+
* recurses into "json inside json" UX that we explicitly want to
|
|
811
|
+
* avoid, and nested repeatable is deferred.
|
|
812
|
+
*/
|
|
813
|
+
type PluginRepeatableSubField = PluginTextField | PluginTextareaField | PluginBooleanField | PluginNumberField | PluginSelectField | PluginUrlField;
|
|
814
|
+
interface PluginRepeatableField extends PluginFieldBase<ReadonlyArray<Readonly<Record<string, unknown>>>> {
|
|
815
|
+
type: 'repeatable';
|
|
816
|
+
/** Shape of each item — every item is a flat object keyed by sub-field `key`. */
|
|
817
|
+
fields: ReadonlyArray<PluginRepeatableSubField>;
|
|
818
|
+
/** Hard cap on item count (default 50). Exceeding rejects the whole field. */
|
|
819
|
+
maxItems?: number;
|
|
820
|
+
/** Minimum item count (default 0). Below rejects the whole field. */
|
|
821
|
+
minItems?: number;
|
|
822
|
+
/** Label for the "+ Add item" button in the admin editor. */
|
|
823
|
+
addLabel?: LocalizedString;
|
|
824
|
+
/**
|
|
825
|
+
* Sub-field key used as the per-item heading in the admin editor
|
|
826
|
+
* (e.g. `'id'` so categories[0] reads "analytics" not "Item 1").
|
|
827
|
+
* Falls back to "Item N" when absent or empty.
|
|
828
|
+
*/
|
|
829
|
+
itemLabelKey?: string;
|
|
830
|
+
}
|
|
831
|
+
type PluginSettingField = PluginTextField | PluginTextareaField | PluginBooleanField | PluginNumberField | PluginSelectField | PluginUrlField | PluginCodeField | PluginJsonField | PluginRepeatableField;
|
|
832
|
+
/**
|
|
833
|
+
* Static manifest declared in a plugin package's `package.json` under
|
|
834
|
+
* the `amplessPlugin` key. The plugin package must also expose
|
|
835
|
+
* `package.json` itself via `exports`:
|
|
836
|
+
*
|
|
837
|
+
* "exports": {
|
|
838
|
+
* ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" },
|
|
839
|
+
* "./package.json": "./package.json"
|
|
840
|
+
* }
|
|
841
|
+
*
|
|
842
|
+
* Without the subpath export, Node's package-exports gating rejects
|
|
843
|
+
* `import.meta.resolve('<pkg>/package.json')` and the runtime cannot
|
|
844
|
+
* load the manifest. Without `amplessPlugin`, the runtime skips
|
|
845
|
+
* cross-check silently and falls back to the existing per-factory
|
|
846
|
+
* capability mismatch checks (backward compatible).
|
|
847
|
+
*/
|
|
848
|
+
interface PluginPackageManifest {
|
|
849
|
+
/** Must match `AmplessPlugin.apiVersion`. Mismatch throws at runtime
|
|
850
|
+
* to prevent loading a plugin built against a different ampless API. */
|
|
851
|
+
apiVersion: 1;
|
|
852
|
+
/** Should match `AmplessPlugin.name`. Mismatch warns. */
|
|
853
|
+
name: string;
|
|
854
|
+
/** Should match `AmplessPlugin.trust_level`. Mismatch warns. */
|
|
855
|
+
trustLevel: TrustLevel;
|
|
856
|
+
/** Should match `AmplessPlugin.capabilities`. Disagreement warns. */
|
|
857
|
+
capabilities: readonly PluginCapability[];
|
|
858
|
+
/** Optional admin UI label. */
|
|
859
|
+
displayName?: LocalizedString;
|
|
860
|
+
/** Optional short description (1 line). */
|
|
861
|
+
description?: LocalizedString;
|
|
862
|
+
/** Optional docs / repo URL. */
|
|
863
|
+
homepage?: string;
|
|
864
|
+
/**
|
|
865
|
+
* Subpath (relative to the plugin's package.json) of the editor
|
|
866
|
+
* module to auto-wire. `update-ampless` reads this to regenerate
|
|
867
|
+
* `app/(admin)/admin/_editor-bootstrap.tsx`. Absent = no editor
|
|
868
|
+
* extension (= plugin doesn't ship a tiptap Node).
|
|
869
|
+
*
|
|
870
|
+
* The referenced module must export `editorExtension` as a named
|
|
871
|
+
* symbol of type tiptap Extension.
|
|
872
|
+
*/
|
|
873
|
+
editorExports?: string;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Secret field types for `settings.secret`. Restricted to `text` /
|
|
877
|
+
* `textarea` — the only types useful for opaque string secrets
|
|
878
|
+
* (API keys, signing secrets, SMTP passwords). More complex types
|
|
879
|
+
* (number, boolean, select, repeatable) are excluded: structured
|
|
880
|
+
* secrets are out of scope for v1.
|
|
881
|
+
*
|
|
882
|
+
* The `default` property is intentionally stripped via `Omit`. If it
|
|
883
|
+
* were allowed, the default value would propagate into the admin form
|
|
884
|
+
* props (visible in the browser), static manifests cross-checked by
|
|
885
|
+
* the runtime, and JS bundles — multiple leak paths for a value that
|
|
886
|
+
* must stay server-side. Plugin authors that have a constructor-time
|
|
887
|
+
* fallback value should keep it as a **closure-private variable** that
|
|
888
|
+
* is never exposed in the manifest (see the plugin author guide for
|
|
889
|
+
* the "closure-private fallback" pattern).
|
|
890
|
+
*/
|
|
891
|
+
type PluginSecretField = Omit<PluginTextField, 'default'> | Omit<PluginTextareaField, 'default'>;
|
|
892
|
+
/**
|
|
893
|
+
* Per-plugin settings declaration. Phase 2 implements `public`;
|
|
894
|
+
* Phase 6a adds `secret` (admin-only storage; never reaches the public
|
|
895
|
+
* runtime or S3 mirror).
|
|
896
|
+
*/
|
|
897
|
+
interface PluginSettingsManifest {
|
|
898
|
+
public?: readonly PluginSettingField[];
|
|
899
|
+
/**
|
|
900
|
+
* Admin-managed secret settings. Values are stored in the isolated
|
|
901
|
+
* `PluginSecret` DynamoDB model, which has no `read` authorization
|
|
902
|
+
* for admin / editor groups — only trusted Lambda IAM can read them.
|
|
903
|
+
* Secrets never reach the public runtime or the S3 site-settings
|
|
904
|
+
* mirror. Requires `trust_level: 'trusted'` and the
|
|
905
|
+
* `'secretSettings'` capability. Declaring this with an untrusted
|
|
906
|
+
* plugin throws at `definePlugin()` time; declaring it without the
|
|
907
|
+
* capability warns.
|
|
908
|
+
*/
|
|
909
|
+
secret?: readonly PluginSecretField[];
|
|
910
|
+
/**
|
|
911
|
+
* Optional manifest shape version (Phase 1 reservation: runtime
|
|
912
|
+
* no-op).
|
|
913
|
+
*
|
|
914
|
+
* Plugin authors bump this integer when they change the shape of
|
|
915
|
+
* `public` or `secret` field arrays in a way that today's
|
|
916
|
+
* write/read paths cannot transparently absorb — i.e. when a field
|
|
917
|
+
* is renamed, when a field's `type` changes incompatibly, or when
|
|
918
|
+
* a field's semantic meaning shifts (same key + same type but the
|
|
919
|
+
* value should be re-interpreted).
|
|
920
|
+
*
|
|
921
|
+
* **Phase 1 scope**: the field is type-only. The runtime does NOT
|
|
922
|
+
* read it today, does NOT persist it next to stored values, and
|
|
923
|
+
* does NOT trigger any migration. Today's behaviour around shape
|
|
924
|
+
* changes continues to apply, but it differs between `public` and
|
|
925
|
+
* `secret` because the two travel through entirely different
|
|
926
|
+
* write/read paths:
|
|
927
|
+
*
|
|
928
|
+
* - `settings.public` goes through `resolvePluginSettings`
|
|
929
|
+
* (`packages/ampless/src/plugin-settings.ts`), which iterates
|
|
930
|
+
* `manifest.public` and falls back to `field.default` per
|
|
931
|
+
* field. So for public fields, additions resolve via `default`,
|
|
932
|
+
* deletions become orphan KvStore rows that the resolver
|
|
933
|
+
* silently skips, and incompatible type changes fall through
|
|
934
|
+
* to `default` when the stored value fails validation.
|
|
935
|
+
* - `settings.secret` is never read by `resolvePluginSettings`.
|
|
936
|
+
* The admin UI writes individual values through the
|
|
937
|
+
* `setPluginSecret` AppSync mutation; trusted hooks read them
|
|
938
|
+
* individually by key via `ctx.secret<T>(key)` which goes
|
|
939
|
+
* directly to the `PluginSecret` DynamoDB table. There is no
|
|
940
|
+
* `manifest.default` fallback for secrets (the type forbids
|
|
941
|
+
* `default` on secret fields). Additions show up in the admin
|
|
942
|
+
* UI when the manifest changes; removals leave the stored
|
|
943
|
+
* ciphertext row orphaned (no resolver runs over it); renames
|
|
944
|
+
* and type changes mean the old key's stored row is never
|
|
945
|
+
* read again until an operator deletes it manually.
|
|
946
|
+
*
|
|
947
|
+
* None of these paths produce a signal to the plugin author.
|
|
948
|
+
*
|
|
949
|
+
* **Future migration PR**: may persist the active manifest
|
|
950
|
+
* version somewhere alongside stored values and may compare it
|
|
951
|
+
* to `manifest.version` at resolve time to detect mismatch. The
|
|
952
|
+
* exact storage location, comparison timing, and mismatch
|
|
953
|
+
* response are all design territory for that future PR — this
|
|
954
|
+
* reservation fixes the field name and type on the manifest,
|
|
955
|
+
* nothing more.
|
|
956
|
+
*
|
|
957
|
+
* Important scope distinction: this reservation covers the
|
|
958
|
+
* `version` *field name and type* on `PluginSettingsManifest`
|
|
959
|
+
* only. The actual migration mechanism (a `migrate` hook, an
|
|
960
|
+
* admin-driven flow, batch resolve-time-rewrite, etc.) is a
|
|
961
|
+
* separate design that lands with its own PR. Declaring
|
|
962
|
+
* `version` today does NOT pre-wire a migration body — when the
|
|
963
|
+
* future migration PR ships, plugins that want to provide a
|
|
964
|
+
* migration body will need to re-publish to add it. Existing
|
|
965
|
+
* plugins that omit `version` are unaffected by this addition;
|
|
966
|
+
* plugins that want to participate in the future migration
|
|
967
|
+
* detection path can opt in today by declaring `version: 1`,
|
|
968
|
+
* instead of having to re-publish later just to add the
|
|
969
|
+
* version declaration once the migration PR ships. The
|
|
970
|
+
* migration body itself (and any future `migrate` hook
|
|
971
|
+
* signature) is NOT reserved by this PR.
|
|
972
|
+
*
|
|
973
|
+
* Recommended values: positive integer, start at 1. Declare
|
|
974
|
+
* `version: 1` when the manifest first gains a `version` field,
|
|
975
|
+
* then bump by 1 on each shape-breaking release. Do NOT use
|
|
976
|
+
* `0` / negative numbers / floats — the `number` type accepts
|
|
977
|
+
* them but the semantics for those values are reserved for the
|
|
978
|
+
* future migration PR (`0` may be conflated with "no version
|
|
979
|
+
* declared" / legacy / pre-v1). Skip the field entirely if you
|
|
980
|
+
* do not care about migration support (the default, current
|
|
981
|
+
* behaviour).
|
|
982
|
+
*/
|
|
983
|
+
version?: number;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Minimal tiptap node shape passed to a `contentFields` `tiptap` renderer.
|
|
987
|
+
* Mirrors the structural fields plugins typically read (`type`, `attrs`,
|
|
988
|
+
* `content`, `marks`, `text`) without coupling to a specific tiptap
|
|
989
|
+
* version. Plugins should treat unknown fields conservatively.
|
|
990
|
+
*/
|
|
991
|
+
interface TiptapRenderNode {
|
|
992
|
+
type: string;
|
|
993
|
+
attrs?: Record<string, unknown>;
|
|
994
|
+
content?: readonly TiptapRenderNode[];
|
|
995
|
+
marks?: readonly {
|
|
996
|
+
type: string;
|
|
997
|
+
attrs?: Record<string, unknown>;
|
|
998
|
+
}[];
|
|
999
|
+
text?: string;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Adapter that converts a single tiptap node back to its markdown form.
|
|
1003
|
+
* Plugins that ship an embed node (e.g. amplessYoutube → bare YouTube URL)
|
|
1004
|
+
* export a map from nodeType to adapter from their `./editor` module
|
|
1005
|
+
* (named export `tiptapNodeToMarkdown`); `update-ampless` wires the map
|
|
1006
|
+
* into `installAdminTiptapNodeMarkdown` so the admin's
|
|
1007
|
+
* `tiptap → markdown` format switch can losslessly serialise the embed
|
|
1008
|
+
* (the round-trip back to tiptap goes through markdown's bare-URL paste
|
|
1009
|
+
* rule + `extractSingleUrl` in the runtime).
|
|
1010
|
+
*
|
|
1011
|
+
* Return `null` (or simply omit the nodeType from the map) to fall
|
|
1012
|
+
* through to the runtime's default markdown switch.
|
|
1013
|
+
*/
|
|
1014
|
+
type TiptapNodeToMarkdown = (node: TiptapRenderNode) => string | null;
|
|
1015
|
+
type TiptapNodeMarkdownAdapters = Readonly<Record<string, TiptapNodeToMarkdown>>;
|
|
1016
|
+
/**
|
|
1017
|
+
* Adapter that converts a single tiptap node to its canonical HTML placeholder
|
|
1018
|
+
* div form. Plugins that ship an embed node (e.g. amplessYoutube →
|
|
1019
|
+
* `<div data-ampless-youtube data-video-id="...">…</div>`) export a map from
|
|
1020
|
+
* nodeType to adapter from their `./editor` module (named export
|
|
1021
|
+
* `tiptapNodeToHtml`); `update-ampless` wires the map into
|
|
1022
|
+
* `installAdminTiptapNodeHtml` so the admin's `tiptap → html` format switch
|
|
1023
|
+
* can losslessly serialise the embed to the canonical placeholder div form.
|
|
1024
|
+
*
|
|
1025
|
+
* The canonical div is what tiptap's `Node.renderHTML` emits and what the
|
|
1026
|
+
* `Node.parseHTML` `tag: 'div[data-ampless-*]'` rule restores from. It is an
|
|
1027
|
+
* admin format-switch interchange form; public rendering expands embeds from
|
|
1028
|
+
* the `tiptap` / `markdown` walkers, while `format: 'html'` preserves the div
|
|
1029
|
+
* literally.
|
|
1030
|
+
*
|
|
1031
|
+
* The `markdown → html` direction is a 2-hop via `generateJSON(...)` so
|
|
1032
|
+
* plugins only need to export the `tiptap → html` adapter; the markdown side
|
|
1033
|
+
* reuses it via tiptap's parseHTML rules (no duplicate logic).
|
|
1034
|
+
*
|
|
1035
|
+
* Return `null` (or simply omit the nodeType from the map) to fall through to
|
|
1036
|
+
* the runtime's default HTML switch.
|
|
1037
|
+
*/
|
|
1038
|
+
type TiptapNodeToHtml = (node: TiptapRenderNode) => string | null;
|
|
1039
|
+
type TiptapNodeHtmlAdapters = Readonly<Record<string, TiptapNodeToHtml>>;
|
|
1040
|
+
/**
|
|
1041
|
+
* Match object passed to a `contentFields` `markdown-url` renderer. The
|
|
1042
|
+
* runtime walks the markdown body via `marked.lexer`, tests the trimmed
|
|
1043
|
+
* single-line URL of each `paragraph` token against the registered
|
|
1044
|
+
* pattern, and on match calls `render({ match, raw }, ctx)` so the
|
|
1045
|
+
* plugin can extract capture groups (e.g. the YouTube video id) and
|
|
1046
|
+
* return a ReactNode for that paragraph.
|
|
1047
|
+
*/
|
|
1048
|
+
interface MarkdownEmbedMatch {
|
|
1049
|
+
match: RegExpMatchArray;
|
|
1050
|
+
raw: string;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Renderer registered via `AmplessPlugin.contentFields`. Two variants:
|
|
1054
|
+
*
|
|
1055
|
+
* - `tiptap`: keyed by `nodeType` (e.g. `'amplessYoutube'`). The runtime
|
|
1056
|
+
* walks tiptap docs and, when the walker encounters a node whose
|
|
1057
|
+
* `type` matches `nodeType`, calls `render(node, ctx)` to produce a
|
|
1058
|
+
* ReactNode in place of the default switch-based HTML emission.
|
|
1059
|
+
*
|
|
1060
|
+
* - `markdown-url`: keyed by a fully-anchored `RegExp` (the pattern
|
|
1061
|
+
* should use `^...$`). The runtime tokenizes markdown via
|
|
1062
|
+
* `marked.lexer`, looks for `paragraph` tokens whose entire content
|
|
1063
|
+
* is a single URL (autolink / bare URL / `[text](url)` with matching
|
|
1064
|
+
* text), tests the URL against `pattern`, and on match calls
|
|
1065
|
+
* `render({ match, raw }, ctx)` for that paragraph.
|
|
1066
|
+
*
|
|
1067
|
+
* Each `nodeType` / `pattern.source` may be registered by at most one
|
|
1068
|
+
* plugin — duplicate registration throws at `createPluginHead` time.
|
|
1069
|
+
*/
|
|
1070
|
+
type ContentFieldRenderer = {
|
|
1071
|
+
kind: 'tiptap';
|
|
1072
|
+
nodeType: string;
|
|
1073
|
+
render(node: TiptapRenderNode, ctx: PluginPublicRenderContext): ReactNode;
|
|
1074
|
+
/**
|
|
1075
|
+
* Opt-in: expand this node's canonical placeholder div when it
|
|
1076
|
+
* appears in a `format: 'html'` post body on public render.
|
|
1077
|
+
*
|
|
1078
|
+
* The admin's `tiptap → html` format switch serialises an embed
|
|
1079
|
+
* node (e.g. `amplessYoutube`) to its canonical placeholder div
|
|
1080
|
+
* (`<div data-ampless-youtube data-video-id="…"><a href="…">…</a></div>`,
|
|
1081
|
+
* via the `tiptapNodeToHtml` adapter). Without `htmlPlaceholder`,
|
|
1082
|
+
* the public renderer of a `format: 'html'` post emits that div
|
|
1083
|
+
* literally — the placeholder shows as a bare div + link instead
|
|
1084
|
+
* of the live embed. (This was the documented limitation from the
|
|
1085
|
+
* canonical-html-embed work; declaring `htmlPlaceholder` resolves
|
|
1086
|
+
* it.)
|
|
1087
|
+
*
|
|
1088
|
+
* When declared, the runtime's public html walker finds each
|
|
1089
|
+
* **top-level** element carrying `flagAttr`, builds a
|
|
1090
|
+
* `TiptapRenderNode { type: nodeType, attrs: attrsFromElement(attribs) }`,
|
|
1091
|
+
* and calls this same `render` — so the `tiptap`, `markdown`, and
|
|
1092
|
+
* `html` formats all reach one renderer (no per-format divergence).
|
|
1093
|
+
*
|
|
1094
|
+
* - `flagAttr` is the marker attribute (e.g.
|
|
1095
|
+
* `'data-ampless-youtube'`). It is **matched case-insensitively**:
|
|
1096
|
+
* the runtime lowercases it at registration because htmlparser2
|
|
1097
|
+
* lowercases HTML attribute names while parsing, so declaring
|
|
1098
|
+
* `'data-My-Embed'` still matches `<div data-my-embed>` /
|
|
1099
|
+
* `<div DATA-MY-EMBED>`.
|
|
1100
|
+
* - `attrsFromElement` converts the div's HTML attributes (already
|
|
1101
|
+
* lowercased by the parser) into the tiptap node `attrs` that
|
|
1102
|
+
* `render` expects, with the correct types — e.g. a
|
|
1103
|
+
* `data-start="30"` string must become the `number 30` that
|
|
1104
|
+
* `render` reads.
|
|
1105
|
+
*
|
|
1106
|
+
* Only top-level placeholders expand; divs nested inside
|
|
1107
|
+
* `<blockquote>` / `<li>` / etc. stay literal. If `attrsFromElement`
|
|
1108
|
+
* or `render` throws, the runtime warns and falls back to the raw
|
|
1109
|
+
* placeholder slice (the embedded link stays clickable) rather than
|
|
1110
|
+
* dropping the engineer-authored content.
|
|
1111
|
+
*/
|
|
1112
|
+
htmlPlaceholder?: {
|
|
1113
|
+
flagAttr: string;
|
|
1114
|
+
attrsFromElement(attribs: Readonly<Record<string, string>>): Record<string, unknown>;
|
|
1115
|
+
};
|
|
1116
|
+
} | {
|
|
1117
|
+
kind: 'markdown-url';
|
|
1118
|
+
pattern: RegExp;
|
|
1119
|
+
render(m: MarkdownEmbedMatch, ctx: PluginPublicRenderContext): ReactNode;
|
|
1120
|
+
};
|
|
1121
|
+
/**
|
|
1122
|
+
* Descriptor returned by `AmplessPlugin.publicPostScript(post, ctx)`.
|
|
1123
|
+
* The runtime aggregates descriptors across every post passed to
|
|
1124
|
+
* `ampless.publicPostScriptsForPage(posts)` and emits each unique `id`
|
|
1125
|
+
* once as `<script src={src} async defer />`.
|
|
1126
|
+
*
|
|
1127
|
+
* `id` must be a stable, plugin-defined identifier (e.g.
|
|
1128
|
+
* `'amplessTweet:widgets'`). Multiple posts on the same page that all
|
|
1129
|
+
* need the same script collapse into a single tag.
|
|
1130
|
+
*
|
|
1131
|
+
* `src` must be an absolute `http://` or `https://` URL — relative
|
|
1132
|
+
* paths and other schemes are dropped with a warning.
|
|
1133
|
+
*/
|
|
1134
|
+
interface PublicPostScriptDescriptor {
|
|
1135
|
+
id: string;
|
|
1136
|
+
src: string;
|
|
1137
|
+
async?: boolean;
|
|
1138
|
+
defer?: boolean;
|
|
1139
|
+
}
|
|
136
1140
|
interface AmplessPlugin {
|
|
137
1141
|
name: string;
|
|
138
|
-
/**
|
|
1142
|
+
/**
|
|
1143
|
+
* Plugin API version. Currently `1` is the only supported value.
|
|
1144
|
+
* `apiVersion` is the breaking-change marker on the plugin contract;
|
|
1145
|
+
* additive changes (new optional fields, new reserved capabilities)
|
|
1146
|
+
* stay within `apiVersion: 1`. See the apiVersion bump policy in
|
|
1147
|
+
* `docs/architecture/08-plugin-architecture.md` for the full criteria.
|
|
1148
|
+
*/
|
|
139
1149
|
apiVersion: 1;
|
|
1150
|
+
/**
|
|
1151
|
+
* Optional npm package name (e.g. `'@scope/ampless-plugin-foo'` or
|
|
1152
|
+
* `'ampless-plugin-foo'`). When set, the runtime resolves
|
|
1153
|
+
* `<packageName>/package.json` at `createPluginHead` construction
|
|
1154
|
+
* time and cross-checks the static `amplessPlugin` manifest against
|
|
1155
|
+
* the factory return value (`apiVersion` mismatch throws,
|
|
1156
|
+
* `name` / `trustLevel` / `capabilities` mismatch warns).
|
|
1157
|
+
*
|
|
1158
|
+
* Plugins that omit this field — site-local plugins, first-party
|
|
1159
|
+
* plugins predating Phase 5, etc. — skip cross-check entirely and
|
|
1160
|
+
* fall back to the existing per-factory capability mismatch checks.
|
|
1161
|
+
* Backward compatible: existing plugins continue to work unchanged.
|
|
1162
|
+
*
|
|
1163
|
+
* For external `npm publish` plugins, `packageName` MUST match the
|
|
1164
|
+
* package's actual `name` and the package's `exports` MUST expose
|
|
1165
|
+
* `./package.json`. The scaffold tool (`npx create-ampless@beta plugin`)
|
|
1166
|
+
* handles both automatically.
|
|
1167
|
+
*/
|
|
1168
|
+
packageName?: string;
|
|
140
1169
|
trust_level: TrustLevel;
|
|
1170
|
+
/**
|
|
1171
|
+
* Stable per-install namespace. Defaults to `name` when omitted.
|
|
1172
|
+
* Distinguishes multiple instances of the same plugin (e.g. two GTM
|
|
1173
|
+
* containers, two GA4 measurement IDs). Multi-instance run-time
|
|
1174
|
+
* validation lands in Phase 3; Phase 1 only adds the field so plugin
|
|
1175
|
+
* authors can author against it now.
|
|
1176
|
+
*/
|
|
1177
|
+
instanceId?: string;
|
|
1178
|
+
/**
|
|
1179
|
+
* Human-readable label for admin UI surfaces (Phase 2 onward).
|
|
1180
|
+
* Plain string or per-locale map — see `LocalizedString`.
|
|
1181
|
+
*/
|
|
1182
|
+
displayName?: LocalizedString;
|
|
1183
|
+
/**
|
|
1184
|
+
* Declared capability list. The runtime uses this for
|
|
1185
|
+
* declaration-vs-implementation warnings (e.g. a plugin that
|
|
1186
|
+
* declares `publicBody` but defines no `publicBodyEnd`); later
|
|
1187
|
+
* phases will gate dangerous capabilities through `cms.config.ts`
|
|
1188
|
+
* `allowCapabilities`. Existing plugins that omit this field
|
|
1189
|
+
* continue to work unchanged.
|
|
1190
|
+
*/
|
|
1191
|
+
capabilities?: readonly PluginCapability[];
|
|
1192
|
+
/**
|
|
1193
|
+
* Public, admin-editable settings (Phase 2). Each declared field is
|
|
1194
|
+
* stored under `pk='siteconfig', sk='plugins.<instanceId>.<key>'`,
|
|
1195
|
+
* mirrored to S3 via the trusted processor, and surfaced to
|
|
1196
|
+
* `publicHead` / `publicBodyEnd` via `ctx.setting<T>(key)`. Plugins
|
|
1197
|
+
* without an admin UI continue to work — they just don't have a
|
|
1198
|
+
* `settings` block.
|
|
1199
|
+
*/
|
|
1200
|
+
settings?: PluginSettingsManifest;
|
|
141
1201
|
/** Async event hooks. Run in trust_level-matched Lambda. */
|
|
142
1202
|
hooks?: {
|
|
143
1203
|
[K in EventType]?: PluginEventHandler<K>;
|
|
144
1204
|
};
|
|
1205
|
+
/**
|
|
1206
|
+
* Lifecycle hook called when this plugin is removed from
|
|
1207
|
+
* `cms.config.ts` (Phase 1 reservation: runtime no-op).
|
|
1208
|
+
*
|
|
1209
|
+
* Today the runtime does not detect plugin removal and does not
|
|
1210
|
+
* invoke this hook — orphan data left behind by an uninstalled
|
|
1211
|
+
* plugin must be cleaned up manually by the operator. A future
|
|
1212
|
+
* lifecycle-dispatch PR will need to solve the underlying
|
|
1213
|
+
* problem that a plugin that has been deleted from `cms.config.ts`
|
|
1214
|
+
* no longer has a callable factory in memory — `Config.plugins`
|
|
1215
|
+
* (see `packages/ampless/src/types.ts`) only carries currently-
|
|
1216
|
+
* active plugin objects. Possible future approaches:
|
|
1217
|
+
*
|
|
1218
|
+
* - Two-stage `cms.config.ts` flag — `{ plugin: myPlugin(), pendingRemoval: true }`
|
|
1219
|
+
* keeps the plugin loadable while the runtime calls `uninstall`,
|
|
1220
|
+
* then the operator removes the entry once cleanup succeeds.
|
|
1221
|
+
* - Explicit `npx ampless uninstall <name>` CLI command — keeps
|
|
1222
|
+
* the plugin imported during the call by reading the prior
|
|
1223
|
+
* `cms.config.ts` entry, fires `uninstall`, then mutates the
|
|
1224
|
+
* file.
|
|
1225
|
+
* - Persist prior manifest + `packageName` to DDB/disk so the
|
|
1226
|
+
* runtime can `await import(packageName)` to re-acquire the
|
|
1227
|
+
* factory — assumes the npm package is still installed.
|
|
1228
|
+
*
|
|
1229
|
+
* The exact mechanism is deferred to the lifecycle-dispatch PR.
|
|
1230
|
+
* What this reservation locks in: when `uninstall` does fire,
|
|
1231
|
+
* it runs in a trusted-Lambda IAM context with cleanup grants for
|
|
1232
|
+
* the five plugin-owned data areas (see
|
|
1233
|
+
* docs/architecture/08-plugin-architecture.md
|
|
1234
|
+
* §"Plugin-owned data areas"). Idempotency is the plugin author's
|
|
1235
|
+
* responsibility — the hook may be invoked more than once
|
|
1236
|
+
* (SQS at-least-once or operator-retry).
|
|
1237
|
+
*
|
|
1238
|
+
* **Phase 1 reservation scope**: only the hook name and signature
|
|
1239
|
+
* are reserved. The ctx does NOT yet carry cleanup helpers
|
|
1240
|
+
* (`deletePublicAsset` / `deletePluginSetting` /
|
|
1241
|
+
* `deletePluginSecret`) — writing `await ctx.deletePublicAsset(...)`
|
|
1242
|
+
* today is a TS error. The recommended Phase 1 declaration is an
|
|
1243
|
+
* **empty body** (`async (_ctx) => {}`); the actual cleanup body
|
|
1244
|
+
* lands when the lifecycle-dispatch PR adds the helpers. Plugins
|
|
1245
|
+
* that declared the empty-body uninstall today will pick up the
|
|
1246
|
+
* cleanup invocation events without re-publishing for the signature
|
|
1247
|
+
* change, but a re-publish is required to add the actual cleanup
|
|
1248
|
+
* body.
|
|
1249
|
+
*/
|
|
1250
|
+
uninstall?: (ctx: PluginUninstallContext) => Promise<void>;
|
|
145
1251
|
/**
|
|
146
1252
|
* Per-post metadata generator. Pure function, called from Next.js
|
|
147
1253
|
* generateMetadata(). Must not have side effects.
|
|
@@ -151,6 +1257,101 @@ interface AmplessPlugin {
|
|
|
151
1257
|
* Site-level metadata (root layout). Returned per request.
|
|
152
1258
|
*/
|
|
153
1259
|
siteMetadata?(site: Config['site']): PluginMetadata;
|
|
1260
|
+
/**
|
|
1261
|
+
* Declarative head injection. Returns a list of validated
|
|
1262
|
+
* descriptors (script / inlineScript / meta / link / noscript). The
|
|
1263
|
+
* runtime collects every plugin's contribution at render time, runs
|
|
1264
|
+
* URL scheme + attrs validation, then emits React elements inside
|
|
1265
|
+
* the root layout's `<head>`. See `PublicHeadDescriptor`.
|
|
1266
|
+
*/
|
|
1267
|
+
publicHead?(ctx: PluginPublicRenderContext): readonly PublicHeadDescriptor[];
|
|
1268
|
+
/**
|
|
1269
|
+
* Same shape as `publicHead`, but the result is appended at the
|
|
1270
|
+
* end of `<body>` and additionally supports the `iframe` variant
|
|
1271
|
+
* (GTM no-script fallback frame, chat widgets, ...).
|
|
1272
|
+
*/
|
|
1273
|
+
publicBodyEnd?(ctx: PluginPublicRenderContext): readonly PublicBodyDescriptor[];
|
|
1274
|
+
/**
|
|
1275
|
+
* Per-post body descriptors (Phase 4). Themes render the result by
|
|
1276
|
+
* calling `ampless.publicBodyForPost(post)` in their post template;
|
|
1277
|
+
* the descriptors emit inside `<body>` (not `<head>`, which the
|
|
1278
|
+
* Next.js Metadata API cannot do for `<script>` tags). Primary use
|
|
1279
|
+
* case: JSON-LD `<script type="application/ld+json">` Article
|
|
1280
|
+
* schema. Restricted to inline-script descriptors with
|
|
1281
|
+
* `scriptType: 'application/ld+json'` — see
|
|
1282
|
+
* `PublicPostBodyDescriptor` for the rationale.
|
|
1283
|
+
*
|
|
1284
|
+
* The runtime auto-escapes `<`, `>`, `&`, U+2028, and U+2029 in the
|
|
1285
|
+
* body so plugin authors cannot accidentally let a value break out
|
|
1286
|
+
* of the script tag. The same escape is applied to any
|
|
1287
|
+
* `'application/ld+json'` descriptor returned from `publicHead` or
|
|
1288
|
+
* `publicBodyEnd`.
|
|
1289
|
+
*
|
|
1290
|
+
* Theme integration: first-party themes (blog / corporate / dads /
|
|
1291
|
+
* docs / landing / minimal) all render the result automatically.
|
|
1292
|
+
* Plugin authors who target custom themes should document the
|
|
1293
|
+
* theme-side render call in their plugin's README — when the theme
|
|
1294
|
+
* does not call `publicBodyForPost`, the plugin silently no-ops.
|
|
1295
|
+
*
|
|
1296
|
+
* Plugins implementing this should declare the `schema` capability.
|
|
1297
|
+
*/
|
|
1298
|
+
publicBodyForPost?(post: Post, ctx: PluginPublicRenderContext): readonly PublicPostBodyDescriptor[];
|
|
1299
|
+
/**
|
|
1300
|
+
* Per-post visible HTML descriptors (Phase 6d). Themes render the
|
|
1301
|
+
* result by calling `ampless.publicHtmlForPost(post)` in their post
|
|
1302
|
+
* template; descriptors emit inside `<body>` at the `beforeContent`
|
|
1303
|
+
* or `afterContent` slot relative to the post prose. Primary use
|
|
1304
|
+
* cases: reading-time badge, breadcrumb, share links, micro-format
|
|
1305
|
+
* annotations.
|
|
1306
|
+
*
|
|
1307
|
+
* Each descriptor's `body` is sanitized by the runtime under a
|
|
1308
|
+
* strict `sanitize-html` allowlist before rendering — plugin authors
|
|
1309
|
+
* may NOT call `dangerouslySetInnerHTML` themselves. The runtime
|
|
1310
|
+
* wraps each surviving entry in a keyed `<div>` with the sanitized
|
|
1311
|
+
* HTML.
|
|
1312
|
+
*
|
|
1313
|
+
* Plugin-local `id` (e.g. `'display'`) is namespace-resolved to
|
|
1314
|
+
* `${instanceId ?? name}:${id}` by the runtime. Plugin authors do
|
|
1315
|
+
* not embed their own namespace in `id`.
|
|
1316
|
+
*
|
|
1317
|
+
* Plugins implementing this should declare the `'publicHtmlForPost'`
|
|
1318
|
+
* capability.
|
|
1319
|
+
*/
|
|
1320
|
+
publicHtmlForPost?(post: Post, ctx: PluginPublicRenderContext): readonly PublicPostHtmlDescriptor[];
|
|
1321
|
+
/**
|
|
1322
|
+
* In-body content renderers (Phase 7 `contentFields` capability).
|
|
1323
|
+
* Plugins register tiptap node renderers (keyed by `nodeType`) and/or
|
|
1324
|
+
* markdown URL renderers (keyed by `RegExp`) here. The runtime walks
|
|
1325
|
+
* post bodies during `ampless.renderBody(post)` and, on match, calls
|
|
1326
|
+
* the renderer to obtain a `ReactNode` for that fragment in place of
|
|
1327
|
+
* the default HTML output.
|
|
1328
|
+
*
|
|
1329
|
+
* Duplicate `nodeType` / `pattern.source` across plugins throws at
|
|
1330
|
+
* `createPluginHead` construction time (eager config-time error). v1
|
|
1331
|
+
* does not support multi-instance plugins registering the same
|
|
1332
|
+
* renderer twice.
|
|
1333
|
+
*
|
|
1334
|
+
* Plugins implementing this should declare the `'contentFields'`
|
|
1335
|
+
* capability.
|
|
1336
|
+
*/
|
|
1337
|
+
contentFields?: readonly ContentFieldRenderer[];
|
|
1338
|
+
/**
|
|
1339
|
+
* Page-level script descriptors (Phase 7 `publicPostScript`
|
|
1340
|
+
* capability). Themes call `ampless.publicPostScriptsForPage(posts)`
|
|
1341
|
+
* after rendering post body / featured body; the runtime invokes
|
|
1342
|
+
* `publicPostScript(post, ctx)` for each plugin × post pair, collects
|
|
1343
|
+
* the descriptors, dedupes by `id`, and emits each unique entry as
|
|
1344
|
+
* `<script src={src} async defer />`.
|
|
1345
|
+
*
|
|
1346
|
+
* Primary use case: x.com (Twitter) `widgets.js` to hydrate
|
|
1347
|
+
* `<blockquote class="twitter-tweet">` blocks emitted by
|
|
1348
|
+
* `contentFields`. YouTube embeds don't need this (the iframe loads
|
|
1349
|
+
* its own script).
|
|
1350
|
+
*
|
|
1351
|
+
* Plugins implementing this should declare the `'publicPostScript'`
|
|
1352
|
+
* capability.
|
|
1353
|
+
*/
|
|
1354
|
+
publicPostScript?(post: Post, ctx: PluginPublicRenderContext): readonly PublicPostScriptDescriptor[];
|
|
154
1355
|
/**
|
|
155
1356
|
* Dynamic OG image renderer. The dispatcher route (e.g.
|
|
156
1357
|
* `app/og/[slug]/route.ts`) reads this and feeds the element into
|
|
@@ -188,6 +1389,22 @@ interface StaticPostBody {
|
|
|
188
1389
|
uploadedAt: string;
|
|
189
1390
|
}
|
|
190
1391
|
type PostStatus = 'draft' | 'published';
|
|
1392
|
+
/**
|
|
1393
|
+
* Per-post cache strategy. Middleware computes the response's
|
|
1394
|
+
* Cache-Control header from this value (plus `post.updatedAt` for the
|
|
1395
|
+
* `auto` case and the project's `cms.config.cache.*` knobs).
|
|
1396
|
+
*
|
|
1397
|
+
* - `'auto'` (default): cooldown by edit time. Posts updated within
|
|
1398
|
+
* the cooldown window (`cms.config.cache.cooldownMs`, default 1h)
|
|
1399
|
+
* emit a no-store header so editors see fresh content immediately.
|
|
1400
|
+
* Older posts emit a long s-maxage so the CDN serves them cheaply.
|
|
1401
|
+
* - `'deep'`: always long-cache (`cms.config.cache.deepTtlSeconds`,
|
|
1402
|
+
* default 1h). Use for posts whose content is fixed for the
|
|
1403
|
+
* foreseeable future.
|
|
1404
|
+
* - `'hot'`: always no-store. Use for posts whose content is rapidly
|
|
1405
|
+
* evolving or computed per request.
|
|
1406
|
+
*/
|
|
1407
|
+
type CacheStrategy = 'auto' | 'deep' | 'hot';
|
|
191
1408
|
/**
|
|
192
1409
|
* Free-form per-post metadata. The `metadata` JSON column carries
|
|
193
1410
|
* arbitrary key/value pairs; the runtime / themes / plugins each pick
|
|
@@ -196,16 +1413,44 @@ type PostStatus = 'draft' | 'published';
|
|
|
196
1413
|
*
|
|
197
1414
|
* Well-known keys:
|
|
198
1415
|
* - `no_layout`: when true, the public page is served as bare HTML
|
|
199
|
-
* (no theme chrome).
|
|
200
|
-
*
|
|
201
|
-
*
|
|
1416
|
+
* (no theme chrome). Middleware rewrites the request to the
|
|
1417
|
+
* internal `/raw/<slug>` handler for such posts and renders the
|
|
1418
|
+
* body verbatim — no Next.js root layout, no theme chrome.
|
|
1419
|
+
* - `cache`: see `CacheStrategy`. Overrides the default 'auto'
|
|
1420
|
+
* cache strategy for this post. Independent of `no_layout` —
|
|
1421
|
+
* applies uniformly to themed, no_layout, and static posts.
|
|
202
1422
|
*
|
|
203
1423
|
* Additional keys are passed through unchanged — themes and plugins
|
|
204
1424
|
* are free to store their own per-post state here (e.g. SEO overrides,
|
|
205
1425
|
* feature flags, A/B variants).
|
|
206
1426
|
*/
|
|
1427
|
+
/**
|
|
1428
|
+
* Per-file metadata recorded on a static post. The static route
|
|
1429
|
+
* reads this to decide whether to stream the bytes back through
|
|
1430
|
+
* Lambda (small files → cached by CloudFront) or to 302-redirect
|
|
1431
|
+
* to a presigned URL (large files → bypass the Lambda response
|
|
1432
|
+
* size envelope). Populated by `upload_static_bundle` /
|
|
1433
|
+
* `commit_static_post` at upload time so the read path never
|
|
1434
|
+
* issues a HEAD round-trip.
|
|
1435
|
+
*
|
|
1436
|
+
* `body.files` (the manifest's flat list) stays the source of truth
|
|
1437
|
+
* for "what's in the bundle"; this map is purely a delivery hint and
|
|
1438
|
+
* may be sparse when older bundles predate the migration.
|
|
1439
|
+
*/
|
|
1440
|
+
interface StaticPostFileMeta {
|
|
1441
|
+
size: number;
|
|
1442
|
+
mimeType: string;
|
|
1443
|
+
}
|
|
207
1444
|
interface PostMetadata {
|
|
208
1445
|
no_layout?: boolean;
|
|
1446
|
+
cache?: CacheStrategy;
|
|
1447
|
+
/**
|
|
1448
|
+
* For `format: 'static'` posts only. Keyed by the bundle-relative
|
|
1449
|
+
* path (same shape as `body.files` entries). Older bundles may
|
|
1450
|
+
* lack this map — readers MUST treat a missing entry as
|
|
1451
|
+
* "fall back to a HEAD lookup".
|
|
1452
|
+
*/
|
|
1453
|
+
files?: Record<string, StaticPostFileMeta>;
|
|
209
1454
|
[key: string]: unknown;
|
|
210
1455
|
}
|
|
211
1456
|
interface Post {
|
|
@@ -219,6 +1464,15 @@ interface Post {
|
|
|
219
1464
|
publishedAt?: string;
|
|
220
1465
|
tags?: string[];
|
|
221
1466
|
metadata?: PostMetadata;
|
|
1467
|
+
/**
|
|
1468
|
+
* DynamoDB auto-managed timestamp (ISO 8601). Surfaced through the
|
|
1469
|
+
* `PublicPost` projection so middleware can compute the
|
|
1470
|
+
* `metadata.cache='auto'` cooldown without re-fetching the model
|
|
1471
|
+
* row. Absent on optimistically-constructed posts (e.g. test
|
|
1472
|
+
* fixtures); the cache strategy treats absent values as "very old"
|
|
1473
|
+
* and emits a long s-maxage.
|
|
1474
|
+
*/
|
|
1475
|
+
updatedAt?: string;
|
|
222
1476
|
}
|
|
223
1477
|
interface Page {
|
|
224
1478
|
pageId: string;
|
|
@@ -229,12 +1483,25 @@ interface Page {
|
|
|
229
1483
|
status: PostStatus;
|
|
230
1484
|
publishedAt?: string;
|
|
231
1485
|
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Asset metadata recorded on the Media row. Currently the only
|
|
1488
|
+
* well-known key is `etag` (the S3 object ETag, captured at upload
|
|
1489
|
+
* time so the media-proxy route can emit it back to the client
|
|
1490
|
+
* without a HEAD round-trip). Free-form by design — themes and
|
|
1491
|
+
* plugins can add their own keys (image dimensions, EXIF strip
|
|
1492
|
+
* status, etc.) without a schema change.
|
|
1493
|
+
*/
|
|
1494
|
+
interface MediaMetadata {
|
|
1495
|
+
etag?: string;
|
|
1496
|
+
[key: string]: unknown;
|
|
1497
|
+
}
|
|
232
1498
|
interface Media {
|
|
233
1499
|
mediaId: string;
|
|
234
1500
|
src: string;
|
|
235
1501
|
mimeType: string;
|
|
236
1502
|
size: number;
|
|
237
1503
|
delivery: 'nextjs' | 's3-direct';
|
|
1504
|
+
metadata?: MediaMetadata;
|
|
238
1505
|
}
|
|
239
1506
|
type ImageDisplay = 'inline' | 'lightbox';
|
|
240
1507
|
/**
|
|
@@ -288,11 +1555,61 @@ interface Config {
|
|
|
288
1555
|
locale?: string;
|
|
289
1556
|
/**
|
|
290
1557
|
* Active plugins. Each entry is the result of a plugin factory call
|
|
291
|
-
* (e.g. `seoPlugin({ ... })`) or a raw AmplessPlugin object. Strings
|
|
292
|
-
* accepted
|
|
293
|
-
*
|
|
1558
|
+
* (e.g. `seoPlugin({ ... })`) or a raw AmplessPlugin object. Strings
|
|
1559
|
+
* are accepted by the type for tolerance but are ignored by the
|
|
1560
|
+
* runtime — only factory results and plugin objects take effect.
|
|
294
1561
|
*/
|
|
295
1562
|
plugins?: Array<AmplessPlugin | string>;
|
|
1563
|
+
/**
|
|
1564
|
+
* Cache strategy knobs read by the runtime's middleware when
|
|
1565
|
+
* computing `Cache-Control` for post responses. Each field is
|
|
1566
|
+
* optional — middleware applies the documented defaults when a
|
|
1567
|
+
* field is absent.
|
|
1568
|
+
*/
|
|
1569
|
+
cache?: CacheConfig;
|
|
1570
|
+
/**
|
|
1571
|
+
* Post revision-history knobs. The event-dispatcher Lambda snapshots
|
|
1572
|
+
* each post save into the `PostHistory` table; this controls how long
|
|
1573
|
+
* those snapshots are retained.
|
|
1574
|
+
*/
|
|
1575
|
+
history?: HistoryConfig;
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Tunables for middleware-computed `Cache-Control`. See
|
|
1579
|
+
* `PostMetadata.cache` / `CacheStrategy` for how the per-post override
|
|
1580
|
+
* interacts with these defaults.
|
|
1581
|
+
*/
|
|
1582
|
+
interface CacheConfig {
|
|
1583
|
+
/**
|
|
1584
|
+
* `cache: 'auto'` cooldown. Posts whose `updatedAt` is younger than
|
|
1585
|
+
* this many milliseconds emit a no-store header so editors see
|
|
1586
|
+
* fresh content immediately after a save. Default 3,600,000 (1h).
|
|
1587
|
+
*/
|
|
1588
|
+
cooldownMs?: number;
|
|
1589
|
+
/**
|
|
1590
|
+
* `cache: 'auto'` post-cooldown TTL, in seconds. Applied as both
|
|
1591
|
+
* `max-age` and `s-maxage` on the response. Default 300 (5 minutes).
|
|
1592
|
+
*/
|
|
1593
|
+
freshTtlSeconds?: number;
|
|
1594
|
+
/**
|
|
1595
|
+
* `cache: 'deep'` TTL, in seconds. Applied as both `max-age` and
|
|
1596
|
+
* `s-maxage`. Default 3600 (1 hour).
|
|
1597
|
+
*/
|
|
1598
|
+
deepTtlSeconds?: number;
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Post revision-history tunables, read by the event-dispatcher Lambda
|
|
1602
|
+
* (via the template shell) when snapshotting each post save.
|
|
1603
|
+
*/
|
|
1604
|
+
interface HistoryConfig {
|
|
1605
|
+
/**
|
|
1606
|
+
* Days to retain each post revision before DynamoDB TTL deletes it.
|
|
1607
|
+
* Default 0 = keep forever (no `ttl` attribute is written). DynamoDB TTL
|
|
1608
|
+
* deletes within ~48h after expiry, so effective retention is
|
|
1609
|
+
* retentionDays + up to ~2 days. Changing this only affects revisions
|
|
1610
|
+
* written afterward — existing rows keep their original expiry.
|
|
1611
|
+
*/
|
|
1612
|
+
retentionDays?: number;
|
|
296
1613
|
}
|
|
297
1614
|
type Role = 'reader' | 'editor' | 'admin';
|
|
298
1615
|
interface AuthContext {
|
|
@@ -315,15 +1632,82 @@ interface ContentTypeDefinition {
|
|
|
315
1632
|
label: string;
|
|
316
1633
|
fields: Record<string, FieldDefinition>;
|
|
317
1634
|
}
|
|
318
|
-
declare function defineSchema<T extends Record<string, ContentTypeDefinition>>(schema: T): T;
|
|
319
|
-
|
|
320
|
-
interface ListOptions {
|
|
1635
|
+
declare function defineSchema<T extends Record<string, ContentTypeDefinition>>(schema: T): T;
|
|
1636
|
+
|
|
1637
|
+
interface ListOptions {
|
|
1638
|
+
limit?: number;
|
|
1639
|
+
status?: 'draft' | 'published' | 'all';
|
|
1640
|
+
}
|
|
1641
|
+
type CreatePostInput = Omit<Post, 'postId'> & {
|
|
1642
|
+
postId?: string;
|
|
1643
|
+
};
|
|
1644
|
+
/**
|
|
1645
|
+
* One in-memory snapshot of a Post at save time, read back from the
|
|
1646
|
+
* `PostHistory` table (written by the event-dispatcher Lambda on each
|
|
1647
|
+
* Post INSERT/MODIFY — see packages/backend/src/events/dispatcher.ts).
|
|
1648
|
+
*
|
|
1649
|
+
* `body` is already decoded from its AWSJSON wire form (the provider
|
|
1650
|
+
* runs `decodeAwsJson` before handing the row up), so it matches the
|
|
1651
|
+
* `Post['body']` shape for the declared `format`.
|
|
1652
|
+
*/
|
|
1653
|
+
interface PostRevision {
|
|
1654
|
+
/** Deterministic id `${postId}#${revisedAt}` — unique per save. */
|
|
1655
|
+
postHistoryId: string;
|
|
1656
|
+
postId: string;
|
|
1657
|
+
/** ISO 8601 save time. Also the `byPost` GSI sort key (newest-first). */
|
|
1658
|
+
revisedAt: string;
|
|
1659
|
+
title?: string;
|
|
1660
|
+
slug?: string;
|
|
1661
|
+
excerpt?: string;
|
|
1662
|
+
format?: ContentFormat;
|
|
1663
|
+
body?: unknown;
|
|
1664
|
+
status?: PostStatus;
|
|
1665
|
+
publishedAt?: string;
|
|
1666
|
+
tags?: string[];
|
|
1667
|
+
metadata?: PostMetadata;
|
|
1668
|
+
}
|
|
1669
|
+
/** Pagination options for `listPostHistory`. */
|
|
1670
|
+
interface ListPostHistoryOptions {
|
|
321
1671
|
limit?: number;
|
|
1672
|
+
nextToken?: string;
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* A page of revisions newest-first. `nextToken` feeds straight back into
|
|
1676
|
+
* `ListPostHistoryOptions.nextToken` for the next page; `undefined` means
|
|
1677
|
+
* no more rows.
|
|
1678
|
+
*/
|
|
1679
|
+
interface PostRevisionConnection {
|
|
1680
|
+
items: PostRevision[];
|
|
1681
|
+
nextToken?: string;
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Lightweight row for admin list views — excludes body / metadata.
|
|
1685
|
+
* Body fields (tiptap JSON, markdown source, etc.) can be tens of KB per post;
|
|
1686
|
+
* projecting them out cuts per-post transfer size by 90%+ at scale.
|
|
1687
|
+
*
|
|
1688
|
+
* For list views that only need title / slug / status / dates / tags,
|
|
1689
|
+
* use `listPostSummaries` instead of `listPosts`.
|
|
1690
|
+
*
|
|
1691
|
+
* Scale note: for single-site blogs (hundreds to low thousands of posts),
|
|
1692
|
+
* full client-side fetch + sort/search is fast and eliminates GSI complexity.
|
|
1693
|
+
* If a site grows to many thousands of posts, revisit with a
|
|
1694
|
+
* (status, updatedAt) GSI + server-side pagination.
|
|
1695
|
+
*/
|
|
1696
|
+
interface PostSummary {
|
|
1697
|
+
postId: string;
|
|
1698
|
+
slug: string;
|
|
1699
|
+
title: string;
|
|
1700
|
+
excerpt?: string;
|
|
1701
|
+
status: 'draft' | 'published';
|
|
1702
|
+
publishedAt?: string;
|
|
1703
|
+
updatedAt?: string;
|
|
1704
|
+
tags: string[];
|
|
1705
|
+
}
|
|
1706
|
+
/** Options for `listPostSummaries`. */
|
|
1707
|
+
interface SummaryListOptions {
|
|
1708
|
+
/** default 'all' */
|
|
322
1709
|
status?: 'draft' | 'published' | 'all';
|
|
323
1710
|
}
|
|
324
|
-
type CreatePostInput = Omit<Post, 'postId'> & {
|
|
325
|
-
postId?: string;
|
|
326
|
-
};
|
|
327
1711
|
interface PostsProvider {
|
|
328
1712
|
list(opts?: ListOptions): Promise<Post[]>;
|
|
329
1713
|
get(slug: string): Promise<Post | null>;
|
|
@@ -331,6 +1715,18 @@ interface PostsProvider {
|
|
|
331
1715
|
create(data: CreatePostInput): Promise<Post>;
|
|
332
1716
|
update(postId: string, data: Partial<Post>): Promise<Post>;
|
|
333
1717
|
remove(postId: string): Promise<void>;
|
|
1718
|
+
listPostHistory(postId: string, options?: ListPostHistoryOptions): Promise<PostRevisionConnection>;
|
|
1719
|
+
/**
|
|
1720
|
+
* Return lightweight summaries for all posts (no body / metadata).
|
|
1721
|
+
* Implementations MUST page through all nextToken values to return the
|
|
1722
|
+
* complete list — the admin list view depends on this for accurate search
|
|
1723
|
+
* and sort.
|
|
1724
|
+
*
|
|
1725
|
+
* Optional: providers that do not implement this fall back to a best-effort
|
|
1726
|
+
* single-page result via `list()` (see `listPostSummaries` fallback path).
|
|
1727
|
+
* The admin provider always implements this.
|
|
1728
|
+
*/
|
|
1729
|
+
listSummaries?(opts?: SummaryListOptions): Promise<PostSummary[]>;
|
|
334
1730
|
}
|
|
335
1731
|
declare function setPostsProvider(p: PostsProvider): void;
|
|
336
1732
|
declare function hasPostsProvider(): boolean;
|
|
@@ -340,6 +1736,30 @@ declare function getPostById(postId: string): Promise<Post | null>;
|
|
|
340
1736
|
declare function createPost(data: CreatePostInput): Promise<Post>;
|
|
341
1737
|
declare function updatePost(postId: string, data: Partial<Post>): Promise<Post>;
|
|
342
1738
|
declare function deletePost(postId: string): Promise<void>;
|
|
1739
|
+
/**
|
|
1740
|
+
* List a post's revision history, newest-first. Backed by the
|
|
1741
|
+
* `PostHistory` `byPost` GSI. Requires a configured provider (the dummy
|
|
1742
|
+
* fallback has no history) — returns an empty connection otherwise so
|
|
1743
|
+
* callers (e.g. the admin history panel) degrade gracefully.
|
|
1744
|
+
*/
|
|
1745
|
+
declare function listPostHistory(postId: string, options?: ListPostHistoryOptions): Promise<PostRevisionConnection>;
|
|
1746
|
+
/**
|
|
1747
|
+
* Return lightweight summaries for all posts (no body / metadata).
|
|
1748
|
+
*
|
|
1749
|
+
* Delegates to `provider.listSummaries()` when available — the admin
|
|
1750
|
+
* provider implements this with a full nextToken loop and selectionSet
|
|
1751
|
+
* projection, ensuring all posts are returned without fetching large body
|
|
1752
|
+
* fields.
|
|
1753
|
+
*
|
|
1754
|
+
* **Fallback**: if the configured provider does not implement `listSummaries`,
|
|
1755
|
+
* falls back to a single page from `provider.list({ status })`. This is
|
|
1756
|
+
* best-effort only — it returns at most one page of results (no pagination).
|
|
1757
|
+
* Providers that need complete summary lists MUST implement `listSummaries`.
|
|
1758
|
+
* A `console.warn` is emitted once to make this visible.
|
|
1759
|
+
*
|
|
1760
|
+
* If no provider is configured, maps DUMMY_POSTS to summaries.
|
|
1761
|
+
*/
|
|
1762
|
+
declare function listPostSummaries(opts?: SummaryListOptions): Promise<PostSummary[]>;
|
|
343
1763
|
|
|
344
1764
|
interface KvItem<T = unknown> {
|
|
345
1765
|
pk: string;
|
|
@@ -361,8 +1781,8 @@ declare function hasKvStore(): boolean;
|
|
|
361
1781
|
/**
|
|
362
1782
|
* Access the injected `KvStore` directly. Use the site-setting helpers
|
|
363
1783
|
* (`getSiteSetting` etc.) when storing per-site configuration; reach
|
|
364
|
-
* for this lower-level handle
|
|
365
|
-
*
|
|
1784
|
+
* for this lower-level handle when you need a different PK namespace
|
|
1785
|
+
* (plugin caches, ad-hoc shared state, etc.).
|
|
366
1786
|
*/
|
|
367
1787
|
declare function getKvStore(): KvStore;
|
|
368
1788
|
declare const SITE_CONFIG_PK = "siteconfig";
|
|
@@ -418,9 +1838,8 @@ declare function escapeXml(s: string): string;
|
|
|
418
1838
|
* from DynamoDB (the path used by the trusted processor and the
|
|
419
1839
|
* MCP Lambda).
|
|
420
1840
|
*
|
|
421
|
-
* `decodeAwsJson`
|
|
422
|
-
* everything else as-is
|
|
423
|
-
* so legacy bare-string rows aren't lost.
|
|
1841
|
+
* `decodeAwsJson` handles both: pass strings through `JSON.parse`,
|
|
1842
|
+
* everything else as-is.
|
|
424
1843
|
*/
|
|
425
1844
|
/**
|
|
426
1845
|
* Serialise a value for an AWSJSON variable. `undefined` / `null` both
|
|
@@ -431,17 +1850,73 @@ declare function encodeAwsJson(value: unknown): string;
|
|
|
431
1850
|
/**
|
|
432
1851
|
* Deserialise an AWSJSON value from a GraphQL / DynamoDB read.
|
|
433
1852
|
* Tolerates both the wire-string shape and the auto-unmarshalled
|
|
434
|
-
* native value.
|
|
1853
|
+
* native value. Throws if the string is not valid JSON.
|
|
435
1854
|
*/
|
|
436
1855
|
declare function decodeAwsJson(value: unknown): unknown;
|
|
437
1856
|
|
|
438
1857
|
/**
|
|
439
1858
|
* Build the public S3 URL for an object key, in the regional virtual-host
|
|
440
|
-
* style: `https://{bucket}.s3.{region}.amazonaws.com/{key}`. The
|
|
1859
|
+
* style: `https://{bucket}.s3.{region}.amazonaws.com/{key}`. The
|
|
441
1860
|
* non-regional form (`s3.amazonaws.com`) issues redirects and is avoided.
|
|
442
1861
|
*/
|
|
443
1862
|
declare function formatPublicAssetUrl(bucket: string, region: string, key: string): string;
|
|
444
1863
|
|
|
1864
|
+
/**
|
|
1865
|
+
* Validate the user-supplied key for ctx.writePublicAsset.
|
|
1866
|
+
*
|
|
1867
|
+
* The trusted Lambda still has a processor-wide S3 grant, so path safety is
|
|
1868
|
+
* enforced at the runtime context boundary before the key is joined under the
|
|
1869
|
+
* plugin namespace.
|
|
1870
|
+
*/
|
|
1871
|
+
declare function validatePublicAssetKey(key: string): string | null;
|
|
1872
|
+
|
|
1873
|
+
declare const PLUGIN_KEY_PATTERN: RegExp;
|
|
1874
|
+
declare function isValidPluginKey(key: string): boolean;
|
|
1875
|
+
/**
|
|
1876
|
+
* Validate one raw value against a field definition. Returns the
|
|
1877
|
+
* (possibly-coerced) value on success, or `null` to signal rejection.
|
|
1878
|
+
*
|
|
1879
|
+
* Semantics:
|
|
1880
|
+
* - **string-like fields** (text / textarea / url / code) accept
|
|
1881
|
+
* `''` as valid when `required` is falsy. This is the "disable"
|
|
1882
|
+
* sentinel — e.g. `pattern: '^$|^G-...'` lets a GA4 plugin save
|
|
1883
|
+
* an empty measurementId to suppress the loader without dropping
|
|
1884
|
+
* the row.
|
|
1885
|
+
* - **non-string-like fields** (number / boolean / json / select)
|
|
1886
|
+
* always reject `''`. Allowing it would force callers to handle a
|
|
1887
|
+
* `string` reading where `number | undefined` is declared. To
|
|
1888
|
+
* "unset" these fields, delete the row (admin form's "Reset to
|
|
1889
|
+
* default" button).
|
|
1890
|
+
* - `undefined` is treated as "not set" by `resolvePluginSettings`
|
|
1891
|
+
* — `validatePluginSettingValue` itself returns `null` for it.
|
|
1892
|
+
* - Constraints (`pattern`, `min`, `max`, `maxLength`, `options`) run
|
|
1893
|
+
* only when the value is present and non-empty.
|
|
1894
|
+
*
|
|
1895
|
+
* The returned shape preserves the declared type — `text` stores a
|
|
1896
|
+
* sanitized string, `number` stores a `number`, `boolean` stores a
|
|
1897
|
+
* `boolean`, `json` stores the decoded value (object / array /
|
|
1898
|
+
* primitive — never the source string), etc.
|
|
1899
|
+
*/
|
|
1900
|
+
declare function validatePluginSettingValue(field: PluginSettingField, raw: unknown, mode?: 'strict' | 'lenient'): unknown | null;
|
|
1901
|
+
/**
|
|
1902
|
+
* Merge stored values on top of manifest defaults for one plugin
|
|
1903
|
+
* instance. Both sides are validated through `validatePluginSettingValue`
|
|
1904
|
+
* — stored values that fail validation (manual DDB tampering, schema
|
|
1905
|
+
* drift) fall back to the validated default; defaults that themselves
|
|
1906
|
+
* fail validation surface as `undefined`.
|
|
1907
|
+
*
|
|
1908
|
+
* `stored` is a flat key → value map keyed by the field's `key` (not
|
|
1909
|
+
* the full DDB SK). The runtime extracts it from the site-settings
|
|
1910
|
+
* snapshot using the `plugins.<instanceId>.` prefix.
|
|
1911
|
+
*
|
|
1912
|
+
* Validation runs on the default too because the default can come
|
|
1913
|
+
* from a plugin's constructor argument (e.g. GA4 takes a
|
|
1914
|
+
* `measurementId` option and passes it through as the field's
|
|
1915
|
+
* `default`). If the operator supplies a bogus value at install time
|
|
1916
|
+
* we'd rather surface `undefined` than render garbage.
|
|
1917
|
+
*/
|
|
1918
|
+
declare function resolvePluginSettings(manifest: PluginSettingsManifest | undefined, stored: Record<string, unknown>): Record<string, unknown>;
|
|
1919
|
+
|
|
445
1920
|
/**
|
|
446
1921
|
* Extract the first image URL from a post body. Used by plugins (e.g.
|
|
447
1922
|
* plugin-og-image) that want to derive an image from the post content
|
|
@@ -457,6 +1932,17 @@ declare function formatPublicAssetUrl(bucket: string, region: string, key: strin
|
|
|
457
1932
|
*/
|
|
458
1933
|
declare function extractFirstImageUrl(post: Post): string | null;
|
|
459
1934
|
|
|
1935
|
+
type PostListStatusFilter = 'all' | 'draft' | 'published';
|
|
1936
|
+
type PostListSort = 'updated-desc' | 'updated-asc' | 'published-desc' | 'published-asc' | 'title-asc' | 'title-desc';
|
|
1937
|
+
interface PostListFilterOptions {
|
|
1938
|
+
query?: string;
|
|
1939
|
+
status?: PostListStatusFilter;
|
|
1940
|
+
tag?: string;
|
|
1941
|
+
sort?: PostListSort;
|
|
1942
|
+
}
|
|
1943
|
+
declare function filterSortPostSummaries<T extends PostSummary>(rows: readonly T[], options?: PostListFilterOptions): T[];
|
|
1944
|
+
declare function collectTags(rows: readonly PostSummary[]): Map<string, number>;
|
|
1945
|
+
|
|
460
1946
|
/**
|
|
461
1947
|
* Pure helpers for `format: 'static'` post bundles. Kept platform-free
|
|
462
1948
|
* (no `File`, no Amplify Storage, no AWS SDK) so both the browser admin
|
|
@@ -545,215 +2031,6 @@ declare function pickDefaultEntrypoint(files: readonly {
|
|
|
545
2031
|
path: string;
|
|
546
2032
|
}[]): string;
|
|
547
2033
|
|
|
548
|
-
type ThemeFieldType = 'color' | 'text' | 'select' | 'image' | 'length' | 'fontFamily' | 'linkList';
|
|
549
|
-
/**
|
|
550
|
-
* Single entry in a `linkList` field. Stored as part of a JSON array
|
|
551
|
-
* under the field's storage key. `url` may be:
|
|
552
|
-
* - a relative path (`/about`)
|
|
553
|
-
* - an absolute URL (`https://example.com`)
|
|
554
|
-
* - a tag reference (`tag:guide`) — themes interpret this as
|
|
555
|
-
* "expand to a list of posts with this tag" rather than rendering
|
|
556
|
-
* a literal link.
|
|
557
|
-
*/
|
|
558
|
-
interface LinkListItem {
|
|
559
|
-
label: string;
|
|
560
|
-
url: string;
|
|
561
|
-
}
|
|
562
|
-
/**
|
|
563
|
-
* A user-facing string in a manifest. Either a plain string (rendered
|
|
564
|
-
* as-is, regardless of locale) or a per-locale map (the renderer picks
|
|
565
|
-
* the active locale, falling back to `en`, then to any value).
|
|
566
|
-
*
|
|
567
|
-
* Themes that ship in a single language can keep these as plain
|
|
568
|
-
* strings. The default themes use the map form so the same manifest
|
|
569
|
-
* works for both built-in dictionaries.
|
|
570
|
-
*/
|
|
571
|
-
type LocalizedString = string | Record<string, string>;
|
|
572
|
-
interface ThemeFieldBase {
|
|
573
|
-
/** Storage key. Persisted as `theme.{key}` in site settings. */
|
|
574
|
-
key: string;
|
|
575
|
-
label: LocalizedString;
|
|
576
|
-
description?: LocalizedString;
|
|
577
|
-
/** Optional UI grouping (e.g. 'Colors', 'Typography', 'Branding'). */
|
|
578
|
-
group?: LocalizedString;
|
|
579
|
-
/** Used when no override is set. Always a string for storage uniformity. */
|
|
580
|
-
default: string;
|
|
581
|
-
/**
|
|
582
|
-
* If set, the loader injects `${cssVar}: ${value}` into a `:root`
|
|
583
|
-
* style block on every public page, so CSS rules using
|
|
584
|
-
* `var(${cssVar})` pick up overrides at render time.
|
|
585
|
-
*
|
|
586
|
-
* Fields without `cssVar` (e.g. logo URL, header tagline) are exposed
|
|
587
|
-
* to template code via `loadThemeConfig()` instead.
|
|
588
|
-
*/
|
|
589
|
-
cssVar?: string;
|
|
590
|
-
}
|
|
591
|
-
interface ThemeColorField extends ThemeFieldBase {
|
|
592
|
-
type: 'color';
|
|
593
|
-
}
|
|
594
|
-
interface ThemeTextField extends ThemeFieldBase {
|
|
595
|
-
type: 'text';
|
|
596
|
-
maxLength?: number;
|
|
597
|
-
}
|
|
598
|
-
interface ThemeSelectField extends ThemeFieldBase {
|
|
599
|
-
type: 'select';
|
|
600
|
-
options: ReadonlyArray<{
|
|
601
|
-
value: string;
|
|
602
|
-
label: LocalizedString;
|
|
603
|
-
}>;
|
|
604
|
-
}
|
|
605
|
-
interface ThemeImageField extends ThemeFieldBase {
|
|
606
|
-
type: 'image';
|
|
607
|
-
}
|
|
608
|
-
interface ThemeLengthField extends ThemeFieldBase {
|
|
609
|
-
type: 'length';
|
|
610
|
-
}
|
|
611
|
-
interface ThemeFontFamilyField extends ThemeFieldBase {
|
|
612
|
-
type: 'fontFamily';
|
|
613
|
-
options: ReadonlyArray<{
|
|
614
|
-
value: string;
|
|
615
|
-
label: LocalizedString;
|
|
616
|
-
}>;
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* A repeatable list of {label, url} entries — used for nav menus,
|
|
620
|
-
* footer link sets, sidebar groups, etc. Stored in KvStore as a JSON
|
|
621
|
-
* string so it fits the existing `string`-valued site-settings cache.
|
|
622
|
-
*
|
|
623
|
-
* `default` is declared as a plain array for ergonomics; the loader
|
|
624
|
-
* stringifies it on the fly so manifest authors don't have to call
|
|
625
|
-
* JSON.stringify by hand.
|
|
626
|
-
*/
|
|
627
|
-
interface ThemeLinkListField extends Omit<ThemeFieldBase, 'default' | 'cssVar'> {
|
|
628
|
-
type: 'linkList';
|
|
629
|
-
default: ReadonlyArray<LinkListItem>;
|
|
630
|
-
/** Cap admin-supplied list length. Default 50. */
|
|
631
|
-
maxItems?: number;
|
|
632
|
-
}
|
|
633
|
-
type ThemeField = ThemeColorField | ThemeTextField | ThemeSelectField | ThemeImageField | ThemeLengthField | ThemeFontFamilyField | ThemeLinkListField;
|
|
634
|
-
interface ThemeManifest {
|
|
635
|
-
/** Theme directory name (`themes/<name>/`). */
|
|
636
|
-
name: string;
|
|
637
|
-
label: LocalizedString;
|
|
638
|
-
description?: LocalizedString;
|
|
639
|
-
fields: ReadonlyArray<ThemeField>;
|
|
640
|
-
}
|
|
641
|
-
declare function defineTheme(m: ThemeManifest): ThemeManifest;
|
|
642
|
-
/**
|
|
643
|
-
* Resolve a `LocalizedString` to a plain string for display.
|
|
644
|
-
* Strings pass through; maps pick `locale` → `fallback` → any value
|
|
645
|
-
* → empty. The empty fallback keeps the renderer from crashing on a
|
|
646
|
-
* malformed manifest while making the missing translation visible.
|
|
647
|
-
*/
|
|
648
|
-
declare function resolveLocalized(value: LocalizedString | undefined, locale: string, fallback?: string): string;
|
|
649
|
-
interface ThemeRouteContext<P = Record<string, string>> {
|
|
650
|
-
params: Promise<P & {
|
|
651
|
-
siteId: string;
|
|
652
|
-
}>;
|
|
653
|
-
}
|
|
654
|
-
interface ThemeModule {
|
|
655
|
-
/** Stable identifier — must match the directory name and the value
|
|
656
|
-
* stored as `theme.active`. */
|
|
657
|
-
name: string;
|
|
658
|
-
manifest: ThemeManifest;
|
|
659
|
-
/**
|
|
660
|
-
* Server components rendered by the dispatcher routes under
|
|
661
|
-
* `app/site/[siteId]/`. Each theme MUST provide Home; Post / Tag are
|
|
662
|
-
* recommended but optional (dispatcher 404s when missing).
|
|
663
|
-
*/
|
|
664
|
-
components: {
|
|
665
|
-
Home: (ctx: ThemeRouteContext) => Promise<unknown> | unknown;
|
|
666
|
-
Post?: (ctx: ThemeRouteContext<{
|
|
667
|
-
slug: string;
|
|
668
|
-
}>) => Promise<unknown> | unknown;
|
|
669
|
-
Tag?: (ctx: ThemeRouteContext<{
|
|
670
|
-
tag: string;
|
|
671
|
-
}>) => Promise<unknown> | unknown;
|
|
672
|
-
};
|
|
673
|
-
/**
|
|
674
|
-
* Optional `generateMetadata` hooks called by the dispatcher. Same
|
|
675
|
-
* signature as the matching component's params.
|
|
676
|
-
*/
|
|
677
|
-
metadata?: {
|
|
678
|
-
Home?: (ctx: ThemeRouteContext) => Promise<unknown> | unknown;
|
|
679
|
-
Post?: (ctx: ThemeRouteContext<{
|
|
680
|
-
slug: string;
|
|
681
|
-
}>) => Promise<unknown> | unknown;
|
|
682
|
-
Tag?: (ctx: ThemeRouteContext<{
|
|
683
|
-
tag: string;
|
|
684
|
-
}>) => Promise<unknown> | unknown;
|
|
685
|
-
};
|
|
686
|
-
/**
|
|
687
|
-
* Optional route handlers for /feed.xml and /sitemap.xml. The
|
|
688
|
-
* dispatcher returns 404 if a theme doesn't provide them.
|
|
689
|
-
*/
|
|
690
|
-
routes?: {
|
|
691
|
-
feed?: (ctx: {
|
|
692
|
-
request: Request;
|
|
693
|
-
}) => Promise<Response>;
|
|
694
|
-
sitemap?: (ctx: {
|
|
695
|
-
request: Request;
|
|
696
|
-
}) => Promise<Response>;
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
declare function defineThemeModule(m: ThemeModule): ThemeModule;
|
|
700
|
-
/**
|
|
701
|
-
* Storage key used in KvStore. Prefix `theme.` keeps the namespace
|
|
702
|
-
* separate from `site.*` / `media.*` so unrelated tools can scan
|
|
703
|
-
* settings without colliding.
|
|
704
|
-
*/
|
|
705
|
-
declare function themeSettingKey(fieldKey: string): string;
|
|
706
|
-
/**
|
|
707
|
-
* Reject malformed or potentially-injectable values before they reach
|
|
708
|
-
* the KvStore. Admin/editor are trusted but typos and copy-paste
|
|
709
|
-
* mistakes shouldn't be able to break a site's CSS or sneak `</style>`
|
|
710
|
-
* into the inline tag the loader emits.
|
|
711
|
-
*
|
|
712
|
-
* Returns the normalized value, or null if the input is rejected.
|
|
713
|
-
*/
|
|
714
|
-
/**
|
|
715
|
-
* Split a stored color value into its light / dark components.
|
|
716
|
-
*
|
|
717
|
-
* Accepts two storage forms:
|
|
718
|
-
* - Single value: `oklch(...)` / `#abcdef` / etc. → `{ light: value, dark: null }`
|
|
719
|
-
* - Pair: `light-dark(L, D)` → `{ light: L, dark: D }`
|
|
720
|
-
*
|
|
721
|
-
* Splits on the top-level comma (depth-aware) so nested commas inside
|
|
722
|
-
* `rgb(...)` / `hsl(...)` don't trip the parser.
|
|
723
|
-
*/
|
|
724
|
-
declare function parseColorPair(value: string): {
|
|
725
|
-
light: string;
|
|
726
|
-
dark: string | null;
|
|
727
|
-
};
|
|
728
|
-
/**
|
|
729
|
-
* Build the storage string for a color field. When `dark` is non-empty
|
|
730
|
-
* and differs from `light`, returns `light-dark(light, dark)`; otherwise
|
|
731
|
-
* returns the bare `light` value. The runtime emits the result verbatim
|
|
732
|
-
* into the inline `:root { --foo: <value> }` override; `light-dark()`
|
|
733
|
-
* is a Baseline-2024 CSS function so the browser picks per mode.
|
|
734
|
-
*/
|
|
735
|
-
declare function formatColorPair(light: string, dark?: string | null): string;
|
|
736
|
-
declare function validateThemeValue(field: ThemeField, raw: unknown): string | null;
|
|
737
|
-
/** Parse a stored linkList JSON value into typed items. Tolerant: bad
|
|
738
|
-
* shapes resolve to []. Throwing here would cascade into rendering. */
|
|
739
|
-
declare function parseLinkList(raw: string | undefined | null): LinkListItem[];
|
|
740
|
-
declare function stringifyLinkList(items: ReadonlyArray<LinkListItem>): string;
|
|
741
|
-
/**
|
|
742
|
-
* Detect a `tag:<name>` URL form. Themes use this to render a list of
|
|
743
|
-
* posts under a heading instead of a literal link — useful for docs
|
|
744
|
-
* sidebars and category-style nav.
|
|
745
|
-
*/
|
|
746
|
-
declare function isTagListUrl(url: string): {
|
|
747
|
-
tag: string;
|
|
748
|
-
} | null;
|
|
749
|
-
/**
|
|
750
|
-
* Resolve effective values for every manifest field, merging stored
|
|
751
|
-
* overrides on top of defaults. `stored` is the flat settings map keyed
|
|
752
|
-
* by `theme.{key}` — typically the output of `listSiteSettings()`
|
|
753
|
-
* filtered to theme entries.
|
|
754
|
-
*/
|
|
755
|
-
declare function resolveThemeValues(manifest: ThemeManifest, stored: Record<string, unknown>): Record<string, string>;
|
|
756
|
-
|
|
757
2034
|
declare const VERSION = "0.0.1";
|
|
758
2035
|
|
|
759
|
-
export { type AmplessEvent, type AmplessPlugin, type AuthContext, type BundleExtractResult, type Config, type ContentEventPayload, type ContentEventType, type ContentFormat, type ContentTypeDefinition, type CreatePostInput, DEFAULT_ENTRYPOINT, type DateFormat, type EventPayloadOf, type EventType, type ExtractedFile, type FieldDefinition, type FieldType, type ImageDisplay, type KvItem, type KvStore, type LinkListItem, type ListOptions, type LocalizedString, MAX_BUNDLE_BYTES, 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 PostMetadata, type PostStatus, type PostsProvider, type Role, SITE_CONFIG_PK, type SiteSettingsEventPayload, type SiteSettingsEventType, type StaticPostBody, type StreamEventName, TEXT_EXTENSIONS, 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, type ValidationIssue, bundlePrefix, createPost, decodeAwsJson, defineConfig, definePlugin, defineSchema, defineTheme, defineThemeModule, deletePost, deleteSiteSetting, detectContentEvents, encodeAwsJson, escapeXml, extractFirstImageUrl, findAbsolutePathRefs, flattenSettings, formatColorPair, formatDate, formatPublicAssetUrl, getKvStore, getPost, getPostById, getSiteSetting, hasKvStore, hasPostsProvider, isTagListUrl, listPosts, listSiteSettings, mimeTypeFor, parseColorPair, parseLinkList, pickDefaultEntrypoint, resolveLocalized, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, stringifyLinkList, stripCommonPrefix, themeSettingKey, unflattenSettings, updatePost, validateBundle, validateBundlePath, validateThemeValue };
|
|
2036
|
+
export { type AmplessEvent, type AmplessPlugin, type AuthContext, type BundleExtractResult, type CacheConfig, type CacheStrategy, type Config, type ContentEventPayload, type ContentEventType, type ContentFieldRenderer, type ContentFormat, type ContentTypeDefinition, type CreatePostInput, DEFAULT_ENTRYPOINT, type DateFormat, type EventPayloadOf, type EventType, type ExtractedFile, type FieldDefinition, type FieldType, type HistoryConfig, type ImageDisplay, type KvItem, type KvStore, type LinkListItem, type ListOptions, type ListPostHistoryOptions, type LocalizedString, MAX_BUNDLE_BYTES, type MarkdownEmbedMatch, type Media, type MediaEventPayload, type MediaEventType, type MediaMetadata, type MediaProcessingDefaults, type OgImageConfig, type OgImageFont, type OgImageRenderContext, PLUGIN_KEY_PATTERN, type Page, type PluginBooleanField, type PluginCapability, type PluginCodeField, type PluginEventHandler, type PluginHookResult, type PluginJsonField, type PluginMetadata, type PluginNumberField, type PluginPackageManifest, type PluginPublicRenderContext, type PluginRepeatableField, type PluginRepeatableSubField, type PluginRuntimeContext, type PluginSecretField, type PluginSelectField, type PluginSettingField, type PluginSettingsManifest, type PluginTextField, type PluginTextareaField, type PluginUninstallContext, type PluginUrlField, type Post, type PostIndexEventPayload, type PostIndexEventType, type PostListFilterOptions, type PostListSort, type PostListStatusFilter, type PostMetadata, type PostRevision, type PostRevisionConnection, type PostStatus, type PostSummary, type PostsProvider, type PublicBodyDescriptor, type PublicHeadDescriptor, type PublicPostBodyDescriptor, type PublicPostHtmlDescriptor, type PublicPostHtmlPosition, type PublicPostScriptDescriptor, type Role, SITE_CONFIG_PK, type ScriptStrategy, type SiteSettingsEventPayload, type SiteSettingsEventType, type StaticPostBody, type StaticPostFileMeta, type StreamEventName, type SummaryListOptions, TEXT_EXTENSIONS, 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 TiptapNodeHtmlAdapters, type TiptapNodeMarkdownAdapters, type TiptapNodeToHtml, type TiptapNodeToMarkdown, type TiptapRenderNode, type TrustLevel, type TrustedPluginRuntimeContext, VERSION, type ValidationIssue, bundlePrefix, collectTags, createPost, decodeAwsJson, defineConfig, definePlugin, defineSchema, defineTheme, defineThemeModule, deletePost, deleteSiteSetting, detectContentEvents, encodeAwsJson, escapeXml, extractFirstImageUrl, filterSortPostSummaries, findAbsolutePathRefs, flattenSettings, formatColorPair, formatDate, formatPublicAssetUrl, getKvStore, getPost, getPostById, getSiteSetting, hasKvStore, hasPostsProvider, isTagListUrl, isValidPluginKey, listPostHistory, listPostSummaries, listPosts, listSiteSettings, mimeTypeFor, parseColorPair, parseLinkList, pickDefaultEntrypoint, resolveLocalized, resolvePluginSettings, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, stringifyLinkList, stripCommonPrefix, themeSettingKey, unflattenSettings, updatePost, validateBundle, validateBundlePath, validatePluginSettingValue, validatePublicAssetKey, validateThemeValue };
|