ampless 1.0.0-alpha.40 → 1.0.0-alpha.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +112 -2
- package/docs/plugin-author-guide.ja.md +181 -9
- package/docs/plugin-author-guide.md +233 -14
- package/package.json +12 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
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';
|
|
@@ -302,7 +304,7 @@ type TrustLevel = 'untrusted' | 'trusted' | 'privileged';
|
|
|
302
304
|
* capability today is harmless, but the runtime won't expose any new
|
|
303
305
|
* surface for it until the matching phase ships.
|
|
304
306
|
*/
|
|
305
|
-
type PluginCapability = 'publicHead' | 'publicBody' | 'metadata' | 'eventHooks' | 'adminSettings' | 'writePublicAsset' | 'schema' | 'publicHtmlForPost' | 'secretSettings' | 'contentFields' | 'adminPage' | 'serverRoute' | 'network' | 'scheduler' | 'storageWrite' | 'privilegedSystem' | 'cspReady';
|
|
307
|
+
type PluginCapability = 'publicHead' | 'publicBody' | 'metadata' | 'eventHooks' | 'adminSettings' | 'writePublicAsset' | 'schema' | 'publicHtmlForPost' | 'secretSettings' | 'contentFields' | 'publicPostScript' | 'adminPage' | 'serverRoute' | 'network' | 'scheduler' | 'storageWrite' | 'privilegedSystem' | 'cspReady';
|
|
306
308
|
/**
|
|
307
309
|
* Loading strategy for `script` / `inlineScript` descriptors.
|
|
308
310
|
*
|
|
@@ -971,6 +973,80 @@ interface PluginSettingsManifest {
|
|
|
971
973
|
*/
|
|
972
974
|
version?: number;
|
|
973
975
|
}
|
|
976
|
+
/**
|
|
977
|
+
* Minimal tiptap node shape passed to a `contentFields` `tiptap` renderer.
|
|
978
|
+
* Mirrors the structural fields plugins typically read (`type`, `attrs`,
|
|
979
|
+
* `content`, `marks`, `text`) without coupling to a specific tiptap
|
|
980
|
+
* version. Plugins should treat unknown fields conservatively.
|
|
981
|
+
*/
|
|
982
|
+
interface TiptapRenderNode {
|
|
983
|
+
type: string;
|
|
984
|
+
attrs?: Record<string, unknown>;
|
|
985
|
+
content?: readonly TiptapRenderNode[];
|
|
986
|
+
marks?: readonly {
|
|
987
|
+
type: string;
|
|
988
|
+
attrs?: Record<string, unknown>;
|
|
989
|
+
}[];
|
|
990
|
+
text?: string;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Match object passed to a `contentFields` `markdown-url` renderer. The
|
|
994
|
+
* runtime walks the markdown body via `marked.lexer`, tests the trimmed
|
|
995
|
+
* single-line URL of each `paragraph` token against the registered
|
|
996
|
+
* pattern, and on match calls `render({ match, raw }, ctx)` so the
|
|
997
|
+
* plugin can extract capture groups (e.g. the YouTube video id) and
|
|
998
|
+
* return a ReactNode for that paragraph.
|
|
999
|
+
*/
|
|
1000
|
+
interface MarkdownEmbedMatch {
|
|
1001
|
+
match: RegExpMatchArray;
|
|
1002
|
+
raw: string;
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Renderer registered via `AmplessPlugin.contentFields`. Two variants:
|
|
1006
|
+
*
|
|
1007
|
+
* - `tiptap`: keyed by `nodeType` (e.g. `'amplessYoutube'`). The runtime
|
|
1008
|
+
* walks tiptap docs and, when the walker encounters a node whose
|
|
1009
|
+
* `type` matches `nodeType`, calls `render(node, ctx)` to produce a
|
|
1010
|
+
* ReactNode in place of the default switch-based HTML emission.
|
|
1011
|
+
*
|
|
1012
|
+
* - `markdown-url`: keyed by a fully-anchored `RegExp` (the pattern
|
|
1013
|
+
* should use `^...$`). The runtime tokenizes markdown via
|
|
1014
|
+
* `marked.lexer`, looks for `paragraph` tokens whose entire content
|
|
1015
|
+
* is a single URL (autolink / bare URL / `[text](url)` with matching
|
|
1016
|
+
* text), tests the URL against `pattern`, and on match calls
|
|
1017
|
+
* `render({ match, raw }, ctx)` for that paragraph.
|
|
1018
|
+
*
|
|
1019
|
+
* Each `nodeType` / `pattern.source` may be registered by at most one
|
|
1020
|
+
* plugin — duplicate registration throws at `createPluginHead` time.
|
|
1021
|
+
*/
|
|
1022
|
+
type ContentFieldRenderer = {
|
|
1023
|
+
kind: 'tiptap';
|
|
1024
|
+
nodeType: string;
|
|
1025
|
+
render(node: TiptapRenderNode, ctx: PluginPublicRenderContext): ReactNode;
|
|
1026
|
+
} | {
|
|
1027
|
+
kind: 'markdown-url';
|
|
1028
|
+
pattern: RegExp;
|
|
1029
|
+
render(m: MarkdownEmbedMatch, ctx: PluginPublicRenderContext): ReactNode;
|
|
1030
|
+
};
|
|
1031
|
+
/**
|
|
1032
|
+
* Descriptor returned by `AmplessPlugin.publicPostScript(post, ctx)`.
|
|
1033
|
+
* The runtime aggregates descriptors across every post passed to
|
|
1034
|
+
* `ampless.publicPostScriptsForPage(posts)` and emits each unique `id`
|
|
1035
|
+
* once as `<script src={src} async defer />`.
|
|
1036
|
+
*
|
|
1037
|
+
* `id` must be a stable, plugin-defined identifier (e.g.
|
|
1038
|
+
* `'amplessTweet:widgets'`). Multiple posts on the same page that all
|
|
1039
|
+
* need the same script collapse into a single tag.
|
|
1040
|
+
*
|
|
1041
|
+
* `src` must be an absolute `http://` or `https://` URL — relative
|
|
1042
|
+
* paths and other schemes are dropped with a warning.
|
|
1043
|
+
*/
|
|
1044
|
+
interface PublicPostScriptDescriptor {
|
|
1045
|
+
id: string;
|
|
1046
|
+
src: string;
|
|
1047
|
+
async?: boolean;
|
|
1048
|
+
defer?: boolean;
|
|
1049
|
+
}
|
|
974
1050
|
interface AmplessPlugin {
|
|
975
1051
|
name: string;
|
|
976
1052
|
/**
|
|
@@ -1152,6 +1228,40 @@ interface AmplessPlugin {
|
|
|
1152
1228
|
* capability.
|
|
1153
1229
|
*/
|
|
1154
1230
|
publicHtmlForPost?(post: Post, ctx: PluginPublicRenderContext): readonly PublicPostHtmlDescriptor[];
|
|
1231
|
+
/**
|
|
1232
|
+
* In-body content renderers (Phase 7 `contentFields` capability).
|
|
1233
|
+
* Plugins register tiptap node renderers (keyed by `nodeType`) and/or
|
|
1234
|
+
* markdown URL renderers (keyed by `RegExp`) here. The runtime walks
|
|
1235
|
+
* post bodies during `ampless.renderBody(post)` and, on match, calls
|
|
1236
|
+
* the renderer to obtain a `ReactNode` for that fragment in place of
|
|
1237
|
+
* the default HTML output.
|
|
1238
|
+
*
|
|
1239
|
+
* Duplicate `nodeType` / `pattern.source` across plugins throws at
|
|
1240
|
+
* `createPluginHead` construction time (eager config-time error). v1
|
|
1241
|
+
* does not support multi-instance plugins registering the same
|
|
1242
|
+
* renderer twice.
|
|
1243
|
+
*
|
|
1244
|
+
* Plugins implementing this should declare the `'contentFields'`
|
|
1245
|
+
* capability.
|
|
1246
|
+
*/
|
|
1247
|
+
contentFields?: readonly ContentFieldRenderer[];
|
|
1248
|
+
/**
|
|
1249
|
+
* Page-level script descriptors (Phase 7 `publicPostScript`
|
|
1250
|
+
* capability). Themes call `ampless.publicPostScriptsForPage(posts)`
|
|
1251
|
+
* after rendering post body / featured body; the runtime invokes
|
|
1252
|
+
* `publicPostScript(post, ctx)` for each plugin × post pair, collects
|
|
1253
|
+
* the descriptors, dedupes by `id`, and emits each unique entry as
|
|
1254
|
+
* `<script src={src} async defer />`.
|
|
1255
|
+
*
|
|
1256
|
+
* Primary use case: x.com (Twitter) `widgets.js` to hydrate
|
|
1257
|
+
* `<blockquote class="twitter-tweet">` blocks emitted by
|
|
1258
|
+
* `contentFields`. YouTube embeds don't need this (the iframe loads
|
|
1259
|
+
* its own script).
|
|
1260
|
+
*
|
|
1261
|
+
* Plugins implementing this should declare the `'publicPostScript'`
|
|
1262
|
+
* capability.
|
|
1263
|
+
*/
|
|
1264
|
+
publicPostScript?(post: Post, ctx: PluginPublicRenderContext): readonly PublicPostScriptDescriptor[];
|
|
1155
1265
|
/**
|
|
1156
1266
|
* Dynamic OG image renderer. The dispatcher route (e.g.
|
|
1157
1267
|
* `app/og/[slug]/route.ts`) reads this and feeds the element into
|
|
@@ -1766,4 +1876,4 @@ declare function pickDefaultEntrypoint(files: readonly {
|
|
|
1766
1876
|
|
|
1767
1877
|
declare const VERSION = "0.0.1";
|
|
1768
1878
|
|
|
1769
|
-
export { type AmplessEvent, type AmplessPlugin, type AuthContext, type BundleExtractResult, type CacheConfig, type CacheStrategy, 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 HistoryConfig, type ImageDisplay, type KvItem, type KvStore, type LinkListItem, type ListOptions, type ListPostHistoryOptions, type LocalizedString, MAX_BUNDLE_BYTES, 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 PostMetadata, type PostRevision, type PostRevisionConnection, type PostStatus, type PostsProvider, type PublicBodyDescriptor, type PublicHeadDescriptor, type PublicPostBodyDescriptor, type PublicPostHtmlDescriptor, type PublicPostHtmlPosition, type Role, SITE_CONFIG_PK, type ScriptStrategy, type SiteSettingsEventPayload, type SiteSettingsEventType, type StaticPostBody, type StaticPostFileMeta, 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, type TrustedPluginRuntimeContext, 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, isValidPluginKey, listPostHistory, listPosts, listSiteSettings, mimeTypeFor, parseColorPair, parseLinkList, pickDefaultEntrypoint, resolveLocalized, resolvePluginSettings, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, stringifyLinkList, stripCommonPrefix, themeSettingKey, unflattenSettings, updatePost, validateBundle, validateBundlePath, validatePluginSettingValue, validatePublicAssetKey, validateThemeValue };
|
|
1879
|
+
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 PostMetadata, type PostRevision, type PostRevisionConnection, type PostStatus, 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, 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 TiptapRenderNode, type TrustLevel, type TrustedPluginRuntimeContext, 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, isValidPluginKey, listPostHistory, listPosts, listSiteSettings, mimeTypeFor, parseColorPair, parseLinkList, pickDefaultEntrypoint, resolveLocalized, resolvePluginSettings, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, stringifyLinkList, stripCommonPrefix, themeSettingKey, unflattenSettings, updatePost, validateBundle, validateBundlePath, validatePluginSettingValue, validatePublicAssetKey, validateThemeValue };
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
設計の経緯と背景は [`docs/architecture/08-plugin-architecture.md`](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.md) に集約。本ページはその実装ハンドブック側です。
|
|
15
15
|
|
|
16
|
+
> **ポジショニング**: ampless はエンジニア向けのカスタマイズベース CMS です — プラグインは**サイトエンジニアが `cms.config.ts` でインポート + 設定する npm dep** です。エンジニアは他の npm ライブラリと同様にインストール前に各 dep を審査します(Astro integration / Next.js plugin パターン)。このガイドで説明する trust framework(`trust_level`、capabilities、IAM スコープ付き Lambda)は v1 において**ファーストパーティプラグインの code organization**として実装されています — どの trust 階層の Lambda が各イベントフックを実行するか、各階層が保有する IAM 権限、そして狭い範囲のポイントでのみ runtime hard gate を適用する(最も重要: `settings.secret` は `trust_level: 'trusted'` を要求、シークレット読み取りに trusted Lambda の IAM 権限が必要なため)。ほとんどの capability 宣言は runtime の hard gate ではなく soft warning + admin ラベル + 将来の allow-list surface です。任意の未審査サードパーティ untrusted プラグインを自動的に安全に動かすための marketplace-grade automatic sandbox としては**設計されていません**。マーケットプレイス + ランタイムサンドボックスは v2.0+ の探索であり、v1 の保証ではありません。詳細な trust model は [`docs/architecture/08-plugin-architecture.md#trust-model-v1-scope`](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.md#trust-model-v1-scope) を参照してください。
|
|
17
|
+
|
|
16
18
|
---
|
|
17
19
|
|
|
18
20
|
## 0. テーマとプラグインの境界線
|
|
@@ -287,13 +289,15 @@ runtime がチェックする内容:
|
|
|
287
289
|
|
|
288
290
|
## 4. `trust_level` の選び方
|
|
289
291
|
|
|
292
|
+
trust 階層は v1 において**ファーストパーティプラグインの code organization**として実装されています — イベントフックがどの IAM スコープ付き Lambda で実行されるか、その Lambda が保有する権限を決定します。これはエンジニアが審査した npm dep 向けの code organization surface であり、任意のサードパーティ未審査プラグインを自動的に安全に動かすための marketplace-grade automatic sandbox ではありません(上記の[ポジショニング注記](#ampless-プラグインの書き方)と[詳細な trust model](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.md#trust-model-v1-scope) を参照)。
|
|
293
|
+
|
|
290
294
|
3 階層、選択基準は **イベントフック (hooks) が何を必要とするか** で決まります (sync サーフェスは IAM に触れない):
|
|
291
295
|
|
|
292
296
|
| 階層 | IAM | 今日動くもの | 用途 |
|
|
293
297
|
|---|---|---|---|
|
|
294
298
|
| `untrusted` | なし (SQS consume のみ) | 同期サーフェス + イベントフック | head/body descriptor、webhook 配送、コンテンツ変換 |
|
|
295
299
|
| `trusted` | 投稿読み出し、`public/plugins/<instanceId ?? name>/...` への書き込み | 同期サーフェス + イベントフック | RSS フィード、sitemap、計算済み JSON インデックス |
|
|
296
|
-
| `privileged` |
|
|
300
|
+
| `privileged` | 予約(v2.0+ 探索のみ) | 同期サーフェスのみ — イベントフックはサイレントフィルタ(警告ログあり) | 将来のマーケットプレイス探索: SES、secret、private S3 |
|
|
297
301
|
|
|
298
302
|
> **`privileged` プラグイン作者へ:** `trust_level: 'privileged'` とイベント
|
|
299
303
|
> `hooks` を宣言した場合、**現時点でフックは実行されません**。両 processor が
|
|
@@ -301,8 +305,10 @@ runtime がチェックする内容:
|
|
|
301
305
|
> `console.warn` を出力するため、サイレントドロップは可視化されます。
|
|
302
306
|
> 同期サーフェス(`publicHead`、`publicBodyEnd`、`metadata`、`publicBodyForPost`、
|
|
303
307
|
> `publicHtmlForPost`)は `trust_level` に関わらず正常に動き、警告も出ません。
|
|
304
|
-
> privileged Lambda
|
|
305
|
-
>
|
|
308
|
+
> privileged Lambda プロビジョニングは v2.0+ の探索項目です — ampless が
|
|
309
|
+
> プラグインマーケットプレイスを構築する場合、すでに `'privileged'` を宣言した
|
|
310
|
+
> プラグインは自動的に新しい tier を使えるようになります。
|
|
311
|
+
> v1 ファーストパーティプラグインは trusted Lambda の現行 IAM スコープに収まる用途で `trust_level: 'trusted'` を使ってください。すなわち Post / KvStore / PluginSecret / PostTag の読み取り、`public/plugins/*` への S3 書き込み、外向き HTTP (AWS IAM 認証を必要としないもの) です。このスコープ外の要件 — SES、private S3 プレフィックス、自前の IAM プリンシパルを必要とする AWS API 呼び出し — は v2.0+ privileged Lambda 探索の対象で、v1 `trusted` には収まりません。
|
|
306
312
|
|
|
307
313
|
決め方の目安:
|
|
308
314
|
|
|
@@ -540,7 +546,7 @@ export default function readingTimePlugin() {
|
|
|
540
546
|
```tsx
|
|
541
547
|
{postBody} {/* publicBodyForPost — JSON-LD */}
|
|
542
548
|
{html.beforeContent} {/* publicHtmlForPost — beforeContent スロット */}
|
|
543
|
-
<div className="prose"
|
|
549
|
+
<div className="prose">{await ampless.renderBody(post)}</div>
|
|
544
550
|
{html.afterContent} {/* publicHtmlForPost — afterContent スロット */}
|
|
545
551
|
```
|
|
546
552
|
|
|
@@ -678,6 +684,166 @@ npm 公開のスタンドアロンプラグインの場合は、`package.json#am
|
|
|
678
684
|
|
|
679
685
|
---
|
|
680
686
|
|
|
687
|
+
## 6b. 投稿本文の差し替え: `contentFields` (Phase 7)
|
|
688
|
+
|
|
689
|
+
`contentFields` capability は、投稿本文の一部(tiptap ノード、または markdown の単独行 URL)を、プラグインが管理する React サブツリーで差し替える機能。`@ampless/plugin-youtube` / `@ampless/plugin-x-embed` が `https://youtu.be/...` URL を iframe プレーヤーに、`https://x.com/<handle>/status/...` URL を tweet blockquote に展開するために使う。
|
|
690
|
+
|
|
691
|
+
### 形
|
|
692
|
+
|
|
693
|
+
```ts
|
|
694
|
+
import { definePlugin, type ContentFieldRenderer } from 'ampless'
|
|
695
|
+
|
|
696
|
+
definePlugin({
|
|
697
|
+
// ...
|
|
698
|
+
capabilities: ['contentFields'],
|
|
699
|
+
contentFields: [
|
|
700
|
+
{
|
|
701
|
+
kind: 'tiptap',
|
|
702
|
+
nodeType: 'amplessYoutube',
|
|
703
|
+
render: (node, ctx) => <YouTubeEmbed videoId={String(node.attrs?.videoId)} />,
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
kind: 'markdown-url',
|
|
707
|
+
pattern: /^https:\/\/youtu\.be\/([\w-]{11})$/,
|
|
708
|
+
render: ({ match }, ctx) => <YouTubeEmbed videoId={match[1]!} />,
|
|
709
|
+
},
|
|
710
|
+
],
|
|
711
|
+
})
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
各 renderer は `ampless.renderBody(post)` 内で本文を歩く runtime から server-side で呼び出される。戻り値は `ReactNode`。`PluginPublicRenderContext` (`ctx`) は `publicHead` / `publicBodyEnd` と同じものなので、`ctx.setting<T>(key)` も同じように使える。
|
|
715
|
+
|
|
716
|
+
### 2 種類の kind
|
|
717
|
+
|
|
718
|
+
- **`tiptap`** — `nodeType` (例: `'amplessYoutube'`) でキー付け。runtime の tiptap walker が `type` 一致のノードを見つけたら renderer を呼ぶ。デフォルトの switch-case レンダリングはバイパスされ、プラグインがサブツリー全体を所有する。
|
|
719
|
+
- **`markdown-url`** — anchored な `RegExp` (`^...$`) でキー付け。runtime は `marked.lexer` で markdown をトークン化し、内容全体が単独 URL の paragraph トークン (autolink / bare URL / `[text](url)`) のみについて pattern を試す。最初にマッチしたものが勝ち。capture group は `match[1]`, `match[2]`, ... でアクセス可能。
|
|
720
|
+
|
|
721
|
+
### 命名と一意性
|
|
722
|
+
|
|
723
|
+
- first-party プラグインは `ampless...` の camelCase プレフィックス(`amplessYoutube`, `amplessTweet` 等)を使い、コミュニティ製の `nodeType` と衝突しないようにしている。
|
|
724
|
+
- runtime は同じ `nodeType` / `pattern.source` の重複登録を起動時に throw で拒否する。先勝ち。v1 は multi-instance 非対応。
|
|
725
|
+
|
|
726
|
+
### markdown URL pattern のルール
|
|
727
|
+
|
|
728
|
+
- **必ず `^...$` で anchor する**。anchor なしだと段落内の URL が誤マッチし、周辺テキストが壊れる。
|
|
729
|
+
- runtime はマッチ前に前後の whitespace を trim するので、pattern 側で `\s*` を書く必要はない。
|
|
730
|
+
- 段落の唯一のトークンが `[caption](url)` の markdown link なら受理する。`[caption with link](url)` のように前後にテキストが混在するケースは対象外(正しい挙動: prose は本文に残すべきで、video embed にすべきではない)。
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## 6c. ページレベルスクリプト: `publicPostScript` (Phase 7)
|
|
735
|
+
|
|
736
|
+
`publicPostScript` capability は、ページ上の投稿が必要とする `<script>` タグをプラグインから出力するためのもの。runtime は安定 `id` で dedupe するので、同一ページ内の複数 embed は 1 つの script タグに集約される。`@ampless/plugin-x-embed` がページに tweet embed がある場合のみ `https://platform.twitter.com/widgets.js` を 1 度だけ注入するのに使われている。
|
|
737
|
+
|
|
738
|
+
### 形
|
|
739
|
+
|
|
740
|
+
```ts
|
|
741
|
+
definePlugin({
|
|
742
|
+
capabilities: ['contentFields', 'publicPostScript'],
|
|
743
|
+
publicPostScript(post, ctx) {
|
|
744
|
+
if (!hasTweetIn(post)) return []
|
|
745
|
+
return [
|
|
746
|
+
{
|
|
747
|
+
id: 'amplessTweet:widgets',
|
|
748
|
+
src: 'https://platform.twitter.com/widgets.js',
|
|
749
|
+
async: true,
|
|
750
|
+
},
|
|
751
|
+
]
|
|
752
|
+
},
|
|
753
|
+
})
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
### テーマからの呼び出し
|
|
757
|
+
|
|
758
|
+
テーマは投稿本文の出力後に `{await ampless.publicPostScriptsForPage(posts)}` を呼ぶ。first-party テーマは post 詳細ページ / home ページ (featured 表示時) で自動的に呼び出す:
|
|
759
|
+
|
|
760
|
+
```tsx
|
|
761
|
+
<div>{await ampless.renderBody(post)}</div>
|
|
762
|
+
{await ampless.publicPostScriptsForPage([post])}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
runtime は:
|
|
766
|
+
|
|
767
|
+
1. プラグイン × post 組ごとに `publicPostScript(post, ctx)` を呼ぶ。
|
|
768
|
+
2. `id` が空 / 非 string、`src` が http(s) でない、descriptor がオブジェクトでない、等を drop。
|
|
769
|
+
3. `id` で dedupe(先勝ち)。
|
|
770
|
+
4. `<Fragment>` で `<script src={src} async defer />` を出力。
|
|
771
|
+
|
|
772
|
+
### CSP の扱い
|
|
773
|
+
|
|
774
|
+
runtime は `src` のホスト allowlist を強制しない。CSP はサイト側エンジニアの責任(`next.config.ts` / middleware で `script-src` に script ホスト(例: `platform.twitter.com`)を追加)。各プラグインの README に書いてある。
|
|
775
|
+
|
|
776
|
+
---
|
|
777
|
+
|
|
778
|
+
## 6d. Admin エディタ拡張の配線 (Phase 7)
|
|
779
|
+
|
|
780
|
+
admin エディタに tiptap Node 拡張を提供するプラグインは、別途 `./editor` subpath の client-side エントリを出荷する。テンプレート側で `_editor-bootstrap.tsx` から配線する:
|
|
781
|
+
|
|
782
|
+
```tsx
|
|
783
|
+
// templates/_shared/app/(admin)/admin/_editor-bootstrap.tsx
|
|
784
|
+
'use client'
|
|
785
|
+
import { installAdminEditorExtensions } from '@ampless/admin/editor'
|
|
786
|
+
import { youtubeEditor } from '@ampless/plugin-youtube/editor'
|
|
787
|
+
import { tweetEditor } from '@ampless/plugin-x-embed/editor'
|
|
788
|
+
|
|
789
|
+
export function EditorBootstrap({ children }: { children: React.ReactNode }) {
|
|
790
|
+
installAdminEditorExtensions([
|
|
791
|
+
youtubeEditor.extension,
|
|
792
|
+
tweetEditor.extension,
|
|
793
|
+
])
|
|
794
|
+
return <>{children}</>
|
|
795
|
+
}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
その上で layout に渡す:
|
|
799
|
+
|
|
800
|
+
```tsx
|
|
801
|
+
// templates/_shared/app/(admin)/admin/layout.tsx
|
|
802
|
+
import { createAdminLayout } from '@ampless/admin/pages'
|
|
803
|
+
import { EditorBootstrap } from './_editor-bootstrap'
|
|
804
|
+
export default createAdminLayout(admin, { editorBootstrap: EditorBootstrap })
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
`installAdminEditorExtensions` は idempotent で、render 時に client component 内で呼ばれる。admin の `<TiptapEditor>` は毎回 render 時に登録済みリストを built-in extensions の末尾に spread する。
|
|
808
|
+
|
|
809
|
+
### エディタプレビューのパイプライン
|
|
810
|
+
|
|
811
|
+
admin の edit / new post フォームは preview ペインを `<iframe sandbox="allow-scripts">` で表示する。`srcDoc` はテンプレート側 server action が返す HTML:
|
|
812
|
+
|
|
813
|
+
```tsx
|
|
814
|
+
// templates/_shared/app/(admin)/admin/_actions/render-preview.tsx
|
|
815
|
+
'use server'
|
|
816
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
817
|
+
import type { Post } from 'ampless'
|
|
818
|
+
import { admin } from '@/lib/admin'
|
|
819
|
+
|
|
820
|
+
export async function renderPreviewHtml(draft: Post): Promise<string> {
|
|
821
|
+
const ampless = await admin.getAmpless()
|
|
822
|
+
const node = (
|
|
823
|
+
<>
|
|
824
|
+
{await ampless.renderBody(draft)}
|
|
825
|
+
{await ampless.publicPostScriptsForPage([draft])}
|
|
826
|
+
</>
|
|
827
|
+
)
|
|
828
|
+
return renderToStaticMarkup(node)
|
|
829
|
+
}
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
page factory は `renderPreviewAction` オプションでこの action を thread する:
|
|
833
|
+
|
|
834
|
+
```tsx
|
|
835
|
+
// templates/_shared/app/(admin)/admin/posts/[postId]/page.tsx
|
|
836
|
+
import { admin } from '@/lib/admin'
|
|
837
|
+
import { createEditPostPage } from '@ampless/admin/pages'
|
|
838
|
+
import { renderPreviewHtml } from '../../_actions/render-preview'
|
|
839
|
+
|
|
840
|
+
export default createEditPostPage(admin, { renderPreviewAction: renderPreviewHtml })
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
iframe の `sandbox="allow-scripts"` は `allow-same-origin` を含まないので、preview の中身は opaque origin で動作する → widget script は admin の DOM に触れない。トレードオフ: widget が自身の origin の storage にアクセスできない場合に正しく動かない可能性がある。Phase 7 時点の dogfood では YouTube iframe / x.com widgets.js とも opaque origin で動作することを確認済み。将来 non-opaque origin が必要な widget が出てきた場合、escape hatch は別 subdomain の preview route + 適切な CSP — sandbox flag を緩めるのは取らない。
|
|
844
|
+
|
|
845
|
+
---
|
|
846
|
+
|
|
681
847
|
## 7. 非同期イベントフック
|
|
682
848
|
|
|
683
849
|
`hooks` は SQS から到着したイベントを trust_level に対応する processor Lambda が受けて実行します。
|
|
@@ -862,12 +1028,18 @@ Secret settings を使うと、trusted プラグインが認証情報 (Webhook
|
|
|
862
1028
|
- 公開 render surface (`publicHead` など) からは読めない。
|
|
863
1029
|
- **field manifest 検証の範囲**: admin クライアントは UX フィードバック目的で `pattern` / `maxLength` / `required` を検証するが、Lambda 側は **汎用の 10,000 文字ハードキャップと安全文字サニタイザのみ**を強制する。admin/editor が AppSync mutation を直接呼ぶと field 単位の制約は迂回できる。設計上意図したもので、admin/editor は secret 設定を許可された信頼されたオペレータと位置づけている。manifest チェックは UX ガイダンスであり、セキュリティ境界ではない。
|
|
864
1030
|
|
|
865
|
-
###
|
|
1031
|
+
### 要件と `definePlugin()` の挙動
|
|
1032
|
+
|
|
1033
|
+
`settings.secret` は `definePlugin()` 時に 4 つの observable な挙動を持ちます(これは v1 のファーストパーティ organization のシークレットアクセス hard gate; [plugin.ts:1004-1019](https://github.com/heavymoons/ampless/blob/main/packages/ampless/src/plugin.ts#L1004-L1019) 参照):
|
|
866
1034
|
|
|
867
|
-
|
|
1035
|
+
1. **`settings.secret` 非空 + `trust_level !== 'trusted'`** → `definePlugin()` が **throw**。untrusted と privileged Lambda は `PluginSecret` テーブルへの IAM read アクセスを持たない; trusted Lambda の IAM 権限が必要。
|
|
1036
|
+
2. **`settings.secret` 非空 + `capabilities` 宣言済み + `capabilities` に `'secretSettings'` が含まれない** → **soft 不一致 warning**。`'schema'` / `'publicHtmlForPost'` の既存 capability-mismatch パターンと同じ。
|
|
1037
|
+
3. **`settings.secret` 非空 + `capabilities` 未定義**(`capabilities` 配列を持たない legacy プラグイン)→ **warning なし**。`capabilities` が `undefined` のとき不一致チェックをスキップ、後方互換のため。
|
|
1038
|
+
4. **`capabilities: ['secretSettings']` 宣言だけで `settings.secret` フィールドなし** → **no-op**。warning も throw もなし。
|
|
868
1039
|
|
|
869
|
-
|
|
870
|
-
|
|
1040
|
+
`settings.secret` を使うには以下も必要です:
|
|
1041
|
+
1. `trust_level: 'trusted'`(上記 #1 の要件; それ以外は `definePlugin()` が throw)。
|
|
1042
|
+
2. `capabilities` に `'secretSettings'` を含める(`capabilities` が定義されているとき省略すると console.warn)。
|
|
871
1043
|
3. **鍵の初回セットアップ** — プロジェクトルートで実行:
|
|
872
1044
|
```sh
|
|
873
1045
|
npx create-ampless setup-encryption-key
|
|
@@ -1234,7 +1406,7 @@ it('admin が空文字保存した場合は空配列', () => {
|
|
|
1234
1406
|
- [`packages/plugin-plausible`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-plausible) — `data-*` attrs 付きの単一 `<script>` descriptor、`required` な URL field(self-hosted Plausible 上書き対応)
|
|
1235
1407
|
- [`packages/plugin-rss`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-rss) — trusted、非同期 hooks + `writePublicAsset`
|
|
1236
1408
|
- [`packages/plugin-seo`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-seo) — `metadata()` + `siteMetadata()`
|
|
1237
|
-
- [`packages/plugin-webhook`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-webhook) —
|
|
1409
|
+
- [`packages/plugin-webhook`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-webhook) — trusted hook + 外向き HTTP + `secretSettings` (admin 管理の signing secret、Phase 6a)
|
|
1238
1410
|
- [`packages/plugin-og-image`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-og-image) — `ogImage` ルートレンダラ
|
|
1239
1411
|
- [`packages/plugin-schema-jsonld`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-schema-jsonld) — `publicBodyForPost` + `schema` capability、投稿単位 Article JSON-LD。(Phase 4)
|
|
1240
1412
|
|
|
@@ -19,6 +19,8 @@ per-post body injection, the async event hooks, and admin-managed
|
|
|
19
19
|
The design rationale is in [`docs/architecture/08-plugin-architecture.md`](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.md);
|
|
20
20
|
this page is the hands-on companion.
|
|
21
21
|
|
|
22
|
+
> **Positioning**: ampless is a customization-based CMS for engineers — plugins are **npm dependencies that the site engineer imports + configures in `cms.config.ts`**. The engineer audits each dep before installing, the way they would for any other npm library (Astro integration / Next.js plugin pattern). The trust framework described in this guide (`trust_level`, capabilities, IAM-scoped Lambdas) is implemented in v1 as **first-party plugin organization** — it decides which trust tier's Lambda runs each event hook, which IAM permissions each tier holds, and applies hard runtime gates only at narrowly-scoped points (most notably: `settings.secret` requires `trust_level: 'trusted'` because secret read needs the trusted Lambda's IAM permission). Most capability declarations are soft warnings + admin labels + future allow-list surfaces, not hard runtime gates. It is **not** designed as a marketplace-grade automatic sandbox that safely runs arbitrary untrusted third-party plugins. Marketplace + runtime sandbox is a v2.0+ exploration, not a v1 guarantee. See [`docs/architecture/08-plugin-architecture.md#trust-model-v1-scope`](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.md#trust-model-v1-scope) for the full trust model.
|
|
23
|
+
|
|
22
24
|
---
|
|
23
25
|
|
|
24
26
|
## 0. Theme vs Plugin Boundary
|
|
@@ -358,6 +360,8 @@ not a runtime block.
|
|
|
358
360
|
|
|
359
361
|
## 4. Picking a `trust_level`
|
|
360
362
|
|
|
363
|
+
The trust tiers are implemented in v1 as **first-party plugin organization** — they decide which IAM-scoped Lambda runs your event hooks and which permissions that Lambda holds. This is a code organization surface for engineer-audited npm deps, not a marketplace-grade automatic sandbox for arbitrary third-party untrusted plugins (see the [Positioning note](#writing-an-ampless-plugin) above and the [full trust model](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.md#trust-model-v1-scope)).
|
|
364
|
+
|
|
361
365
|
Three tiers, picked by what the plugin needs to do **inside
|
|
362
366
|
event hooks** (the sync surfaces — metadata, head, body — don't
|
|
363
367
|
touch IAM):
|
|
@@ -366,7 +370,7 @@ touch IAM):
|
|
|
366
370
|
|---|---|---|---|
|
|
367
371
|
| `untrusted` | none (SQS consume only) | sync surfaces + event hooks | head/body descriptors, webhook delivery, content transforms |
|
|
368
372
|
| `trusted` | read posts, write `public/plugins/<instanceId ?? name>/...` | sync surfaces + event hooks | RSS feed, sitemap, computed JSON indexes |
|
|
369
|
-
| `privileged` | reserved | sync surfaces only — event hooks are silently filtered out (warning logged) | future: SES, secrets, private S3 |
|
|
373
|
+
| `privileged` | reserved (v2.0+ exploration only) | sync surfaces only — event hooks are silently filtered out (warning logged) | future marketplace exploration: SES, secrets, private S3 |
|
|
370
374
|
|
|
371
375
|
> **Warning for `privileged` plugin authors:** If you declare
|
|
372
376
|
> `trust_level: 'privileged'` with event `hooks` today, **your hooks
|
|
@@ -375,9 +379,10 @@ touch IAM):
|
|
|
375
379
|
> the drop is visible. Sync render surfaces (`publicHead`,
|
|
376
380
|
> `publicBodyEnd`, `metadata`, `publicBodyForPost`, `publicHtmlForPost`)
|
|
377
381
|
> work normally regardless of `trust_level` and emit no warning.
|
|
378
|
-
>
|
|
379
|
-
>
|
|
380
|
-
>
|
|
382
|
+
> `privileged` Lambda provisioning is a v2.0+ exploration item — if
|
|
383
|
+
> AmpLess later builds a plugin marketplace, that PR will automatically
|
|
384
|
+
> pick up plugins that already declared `'privileged'`.
|
|
385
|
+
> v1 first-party plugins should use `trust_level: 'trusted'` for capabilities that fit within the trusted Lambda's existing IAM scope: Post / KvStore / PluginSecret / PostTag read, `public/plugins/*` S3 write, and outbound HTTP (no AWS-IAM-authenticated calls). Requirements that fall outside that scope — SES, private S3 prefixes, calling AWS APIs that need an IAM principal of their own — are v2.0+ privileged-Lambda exploration material and don't fit into v1 `trusted`.
|
|
381
386
|
|
|
382
387
|
Rule of thumb:
|
|
383
388
|
|
|
@@ -673,7 +678,7 @@ the slots:
|
|
|
673
678
|
```tsx
|
|
674
679
|
{postBody} {/* publicBodyForPost — JSON-LD */}
|
|
675
680
|
{html.beforeContent} {/* publicHtmlForPost — beforeContent slot */}
|
|
676
|
-
<div className="prose"
|
|
681
|
+
<div className="prose">{await ampless.renderBody(post)}</div>
|
|
677
682
|
{html.afterContent} {/* publicHtmlForPost — afterContent slot */}
|
|
678
683
|
```
|
|
679
684
|
|
|
@@ -889,6 +894,218 @@ manifest and the factory return value:
|
|
|
889
894
|
|
|
890
895
|
---
|
|
891
896
|
|
|
897
|
+
## 6b. In-body content renderers: `contentFields` (Phase 7)
|
|
898
|
+
|
|
899
|
+
The `contentFields` capability lets a plugin replace specific fragments
|
|
900
|
+
of a post body — tiptap nodes or single-line markdown URLs — with a
|
|
901
|
+
React subtree it controls. Used by the first-party `@ampless/plugin-youtube`
|
|
902
|
+
and `@ampless/plugin-x-embed` packages to expand `https://youtu.be/...`
|
|
903
|
+
URLs into iframe players and `https://x.com/<handle>/status/...` URLs
|
|
904
|
+
into tweet blockquotes.
|
|
905
|
+
|
|
906
|
+
### Shape
|
|
907
|
+
|
|
908
|
+
```ts
|
|
909
|
+
import { definePlugin, type ContentFieldRenderer } from 'ampless'
|
|
910
|
+
|
|
911
|
+
definePlugin({
|
|
912
|
+
// ...
|
|
913
|
+
capabilities: ['contentFields'],
|
|
914
|
+
contentFields: [
|
|
915
|
+
{
|
|
916
|
+
kind: 'tiptap',
|
|
917
|
+
nodeType: 'amplessYoutube',
|
|
918
|
+
render: (node, ctx) => <YouTubeEmbed videoId={String(node.attrs?.videoId)} />,
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
kind: 'markdown-url',
|
|
922
|
+
pattern: /^https:\/\/youtu\.be\/([\w-]{11})$/,
|
|
923
|
+
render: ({ match }, ctx) => <YouTubeEmbed videoId={match[1]!} />,
|
|
924
|
+
},
|
|
925
|
+
],
|
|
926
|
+
})
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
Each renderer is called server-side by the runtime when it walks a post
|
|
930
|
+
body during `ampless.renderBody(post)`. The return value must be a
|
|
931
|
+
`ReactNode`. The plugin's `PluginPublicRenderContext` (`ctx`) is the
|
|
932
|
+
same context handed to `publicHead` / `publicBodyEnd`, so `ctx.setting<T>(key)`
|
|
933
|
+
works exactly the same way.
|
|
934
|
+
|
|
935
|
+
### The two kinds
|
|
936
|
+
|
|
937
|
+
- **`tiptap`** — keyed by `nodeType` (a string, e.g. `'amplessYoutube'`).
|
|
938
|
+
The runtime's tiptap walker calls the renderer whenever it encounters
|
|
939
|
+
a node whose `type` matches `nodeType`. Default switch-case rendering
|
|
940
|
+
is bypassed; the plugin owns the subtree.
|
|
941
|
+
- **`markdown-url`** — keyed by an anchored `RegExp` (`^...$`). The
|
|
942
|
+
runtime tokenizes markdown with `marked.lexer` and, for any
|
|
943
|
+
`paragraph` token whose entire content is a single URL (autolink,
|
|
944
|
+
bare URL, or `[text](url)` markdown link), tests the URL against
|
|
945
|
+
each registered pattern. The first match wins; capture groups are
|
|
946
|
+
exposed via `match[1]`, `match[2]`, etc.
|
|
947
|
+
|
|
948
|
+
### Naming and uniqueness
|
|
949
|
+
|
|
950
|
+
- First-party plugins use the `ampless...` camelCase prefix
|
|
951
|
+
(`amplessYoutube`, `amplessTweet`) so the namespace stays clear of
|
|
952
|
+
community-contributed `nodeType`s.
|
|
953
|
+
- The runtime rejects duplicate `nodeType` / `pattern.source` at
|
|
954
|
+
startup with a thrown error. The first plugin to register a given key
|
|
955
|
+
wins; the second one fails fast. Multi-instance v1 is not supported.
|
|
956
|
+
|
|
957
|
+
### Markdown URL pattern rules
|
|
958
|
+
|
|
959
|
+
- **Always anchor with `^...$`** so the pattern only matches paragraphs
|
|
960
|
+
whose entire content is a single URL. A pattern without anchors would
|
|
961
|
+
match URLs embedded mid-paragraph and break the surrounding text.
|
|
962
|
+
- The runtime trims leading/trailing whitespace before matching, so
|
|
963
|
+
patterns don't need to account for `\s*` around the URL.
|
|
964
|
+
- Inline `[caption](url)` markdown links are accepted when the link is
|
|
965
|
+
the paragraph's only token. `[caption with link](url)` mixed with
|
|
966
|
+
surrounding text is NOT matched (correct behaviour: the prose
|
|
967
|
+
belongs in the post, not a video embed).
|
|
968
|
+
|
|
969
|
+
---
|
|
970
|
+
|
|
971
|
+
## 6c. Page-level scripts: `publicPostScript` (Phase 7)
|
|
972
|
+
|
|
973
|
+
The `publicPostScript` capability lets a plugin emit a `<script>` tag
|
|
974
|
+
that any post on the page needs. The runtime dedupes by stable `id`
|
|
975
|
+
so multiple embeds in one or several posts collapse to one script tag.
|
|
976
|
+
Used by `@ampless/plugin-x-embed` to inject
|
|
977
|
+
`https://platform.twitter.com/widgets.js` once per page that has any
|
|
978
|
+
tweet embed.
|
|
979
|
+
|
|
980
|
+
### Shape
|
|
981
|
+
|
|
982
|
+
```ts
|
|
983
|
+
definePlugin({
|
|
984
|
+
capabilities: ['contentFields', 'publicPostScript'],
|
|
985
|
+
publicPostScript(post, ctx) {
|
|
986
|
+
if (!hasTweetIn(post)) return []
|
|
987
|
+
return [
|
|
988
|
+
{
|
|
989
|
+
id: 'amplessTweet:widgets',
|
|
990
|
+
src: 'https://platform.twitter.com/widgets.js',
|
|
991
|
+
async: true,
|
|
992
|
+
},
|
|
993
|
+
]
|
|
994
|
+
},
|
|
995
|
+
})
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
### Theme integration
|
|
999
|
+
|
|
1000
|
+
Themes call `{await ampless.publicPostScriptsForPage(posts)}` after
|
|
1001
|
+
rendering each post body. First-party themes do this automatically in
|
|
1002
|
+
their post detail page and home page (when a featured post is shown):
|
|
1003
|
+
|
|
1004
|
+
```tsx
|
|
1005
|
+
<div>{await ampless.renderBody(post)}</div>
|
|
1006
|
+
{await ampless.publicPostScriptsForPage([post])}
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
The runtime:
|
|
1010
|
+
|
|
1011
|
+
1. Calls `publicPostScript(post, ctx)` for each plugin × post pair.
|
|
1012
|
+
2. Drops descriptors with empty/non-string `id`, with `src` that fails
|
|
1013
|
+
the http(s) allowlist, or that aren't objects.
|
|
1014
|
+
3. Dedupes by `id` (first arrival wins).
|
|
1015
|
+
4. Emits a `<Fragment>` of `<script src={src} async defer />` elements.
|
|
1016
|
+
|
|
1017
|
+
### CSP considerations
|
|
1018
|
+
|
|
1019
|
+
The runtime does **not** enforce a host allowlist on `src`. CSP is the
|
|
1020
|
+
site engineer's responsibility — add the script host (e.g.
|
|
1021
|
+
`platform.twitter.com`) to `script-src` in `next.config.ts` /
|
|
1022
|
+
middleware. This is documented in each plugin's README.
|
|
1023
|
+
|
|
1024
|
+
---
|
|
1025
|
+
|
|
1026
|
+
## 6d. Admin editor extension wiring (Phase 7)
|
|
1027
|
+
|
|
1028
|
+
Plugins that contribute tiptap Node extensions to the admin editor
|
|
1029
|
+
ship a separate client-side entry under the `./editor` subpath. The
|
|
1030
|
+
template wires it up in `_editor-bootstrap.tsx`:
|
|
1031
|
+
|
|
1032
|
+
```tsx
|
|
1033
|
+
// templates/_shared/app/(admin)/admin/_editor-bootstrap.tsx
|
|
1034
|
+
'use client'
|
|
1035
|
+
import { installAdminEditorExtensions } from '@ampless/admin/editor'
|
|
1036
|
+
import { youtubeEditor } from '@ampless/plugin-youtube/editor'
|
|
1037
|
+
import { tweetEditor } from '@ampless/plugin-x-embed/editor'
|
|
1038
|
+
|
|
1039
|
+
export function EditorBootstrap({ children }: { children: React.ReactNode }) {
|
|
1040
|
+
installAdminEditorExtensions([
|
|
1041
|
+
youtubeEditor.extension,
|
|
1042
|
+
tweetEditor.extension,
|
|
1043
|
+
])
|
|
1044
|
+
return <>{children}</>
|
|
1045
|
+
}
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
Then thread it into the layout:
|
|
1049
|
+
|
|
1050
|
+
```tsx
|
|
1051
|
+
// templates/_shared/app/(admin)/admin/layout.tsx
|
|
1052
|
+
import { createAdminLayout } from '@ampless/admin/pages'
|
|
1053
|
+
import { EditorBootstrap } from './_editor-bootstrap'
|
|
1054
|
+
export default createAdminLayout(admin, { editorBootstrap: EditorBootstrap })
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
`installAdminEditorExtensions` is idempotent and runs at render time
|
|
1058
|
+
inside a client component. The admin's `<TiptapEditor>` spreads the
|
|
1059
|
+
registered list onto its built-in extensions on every render.
|
|
1060
|
+
|
|
1061
|
+
### Editor preview pipeline
|
|
1062
|
+
|
|
1063
|
+
The admin's edit / new post forms render the preview pane in an
|
|
1064
|
+
`<iframe sandbox="allow-scripts">` whose `srcDoc` is the HTML returned
|
|
1065
|
+
by the template's server action:
|
|
1066
|
+
|
|
1067
|
+
```tsx
|
|
1068
|
+
// templates/_shared/app/(admin)/admin/_actions/render-preview.tsx
|
|
1069
|
+
'use server'
|
|
1070
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
1071
|
+
import type { Post } from 'ampless'
|
|
1072
|
+
import { admin } from '@/lib/admin'
|
|
1073
|
+
|
|
1074
|
+
export async function renderPreviewHtml(draft: Post): Promise<string> {
|
|
1075
|
+
const ampless = await admin.getAmpless()
|
|
1076
|
+
const node = (
|
|
1077
|
+
<>
|
|
1078
|
+
{await ampless.renderBody(draft)}
|
|
1079
|
+
{await ampless.publicPostScriptsForPage([draft])}
|
|
1080
|
+
</>
|
|
1081
|
+
)
|
|
1082
|
+
return renderToStaticMarkup(node)
|
|
1083
|
+
}
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
Page factories thread the action down via the `renderPreviewAction`
|
|
1087
|
+
option:
|
|
1088
|
+
|
|
1089
|
+
```tsx
|
|
1090
|
+
// templates/_shared/app/(admin)/admin/posts/[postId]/page.tsx
|
|
1091
|
+
import { admin } from '@/lib/admin'
|
|
1092
|
+
import { createEditPostPage } from '@ampless/admin/pages'
|
|
1093
|
+
import { renderPreviewHtml } from '../../_actions/render-preview'
|
|
1094
|
+
|
|
1095
|
+
export default createEditPostPage(admin, { renderPreviewAction: renderPreviewHtml })
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
The iframe's `sandbox="allow-scripts"` (without `allow-same-origin`)
|
|
1099
|
+
keeps preview content in an opaque origin so widget scripts cannot
|
|
1100
|
+
reach the admin's DOM. The trade-off is that some widgets may misbehave
|
|
1101
|
+
when they can't access their own origin's storage; the YouTube iframe
|
|
1102
|
+
embed and x.com widgets.js both work under these constraints in
|
|
1103
|
+
dogfood as of Phase 7. If a future widget needs a non-opaque preview
|
|
1104
|
+
origin, the escape hatch is a separate preview route on a different
|
|
1105
|
+
subdomain with appropriate CSP — not relaxing the sandbox flag.
|
|
1106
|
+
|
|
1107
|
+
---
|
|
1108
|
+
|
|
892
1109
|
## 7. Async event hooks
|
|
893
1110
|
|
|
894
1111
|
`hooks` runs inside the trust_level-matched processor Lambda when an
|
|
@@ -1150,16 +1367,18 @@ etc. — but completely wrong for a webhook signing secret.
|
|
|
1150
1367
|
(`publicHead`, `publicBodyEnd`, `publicBodyForPost`,
|
|
1151
1368
|
`publicHtmlForPost`) — those surfaces only see `ctx.setting()`.
|
|
1152
1369
|
|
|
1153
|
-
### Requirements
|
|
1370
|
+
### Requirements and `definePlugin()` behaviour
|
|
1371
|
+
|
|
1372
|
+
`settings.secret` has four observable behaviours at `definePlugin()` time (this is the v1 first-party organization hard gate for secret access; see [plugin.ts:1004-1019](https://github.com/heavymoons/ampless/blob/main/packages/ampless/src/plugin.ts#L1004-L1019)):
|
|
1154
1373
|
|
|
1155
|
-
|
|
1374
|
+
1. **`settings.secret` non-empty + `trust_level !== 'trusted'`** → `definePlugin()` **throws**. Untrusted and privileged Lambdas have no IAM read access to the `PluginSecret` table; the trusted Lambda's IAM permission is required.
|
|
1375
|
+
2. **`settings.secret` non-empty + `capabilities` declared + `'secretSettings'` missing from `capabilities`** → **soft mismatch warning**. Matches the existing capability-mismatch pattern for `'schema'` / `'publicHtmlForPost'`.
|
|
1376
|
+
3. **`settings.secret` non-empty + `capabilities` undefined** (legacy plugin without a `capabilities` array) → **no warning**. The mismatch check is skipped when `capabilities` is `undefined`, for backward compatibility.
|
|
1377
|
+
4. **`capabilities: ['secretSettings']` declared with no `settings.secret` field** → **no-op**. Neither warning nor throw.
|
|
1156
1378
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
2. `'secretSettings'` in `capabilities` — required so admin UI and
|
|
1161
|
-
future allow-lists can gate the capability. Omitting it produces
|
|
1162
|
-
a console warning (same pattern as `'schema'` vs `publicBodyForPost`).
|
|
1379
|
+
To use `settings.secret`, you also need:
|
|
1380
|
+
1. `trust_level: 'trusted'` (requirement #1 above; `definePlugin()` throws otherwise).
|
|
1381
|
+
2. `'secretSettings'` in `capabilities` (omitting it when `capabilities` is defined produces a console warning).
|
|
1163
1382
|
3. **One-time key setup** — run from your project root:
|
|
1164
1383
|
```sh
|
|
1165
1384
|
npx create-ampless setup-encryption-key
|
|
@@ -1676,7 +1895,7 @@ Worked examples to crib from:
|
|
|
1676
1895
|
- [`packages/plugin-plausible`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-plausible) — single `<script>` descriptor with `data-*` attrs and a required URL field (self-hosted Plausible override).
|
|
1677
1896
|
- [`packages/plugin-rss`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-rss) — trusted, async event hooks + `writePublicAsset`.
|
|
1678
1897
|
- [`packages/plugin-seo`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-seo) — `metadata()` + `siteMetadata()`.
|
|
1679
|
-
- [`packages/plugin-webhook`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-webhook) —
|
|
1898
|
+
- [`packages/plugin-webhook`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-webhook) — trusted hook with outbound HTTP + `secretSettings` (admin-managed signing secret, Phase 6a).
|
|
1680
1899
|
- [`packages/plugin-og-image`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-og-image) — `ogImage` route renderer.
|
|
1681
1900
|
- [`packages/plugin-schema-jsonld`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-schema-jsonld) — `publicBodyForPost` + `schema` capability; per-post Article JSON-LD. (Phase 4)
|
|
1682
1901
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ampless",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.42",
|
|
4
4
|
"description": "Serverless CMS for AWS Amplify",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,6 +32,17 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@jsquash/webp": "^1.5.0"
|
|
34
34
|
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@types/react": "^19"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"@types/react": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/react": "^19.2.15"
|
|
45
|
+
},
|
|
35
46
|
"keywords": [
|
|
36
47
|
"cms",
|
|
37
48
|
"aws",
|