ampless 1.0.0-alpha.34 → 1.0.0-alpha.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -633,6 +633,28 @@ interface PluginRuntimeContext {
633
633
  */
634
634
  writePublicAsset(key: string, body: string | Uint8Array, contentType: string): Promise<string>;
635
635
  }
636
+ /**
637
+ * Context passed to the `uninstall` lifecycle hook (Phase 1
638
+ * reservation: runtime no-op — see `AmplessPlugin.uninstall`).
639
+ *
640
+ * Phase 1: structurally identical to `PluginRuntimeContext`. The
641
+ * dedicated type exists so future cleanup helpers
642
+ * (`deletePublicAsset`, `deletePluginSetting`, `deletePluginSecret`)
643
+ * can be added here without exposing them to regular event hooks,
644
+ * which take `PluginRuntimeContext` and should not be able to
645
+ * delete state from arbitrary plugin areas.
646
+ *
647
+ * Phase 1 scope is limited to: (a) reserving the type name, (b)
648
+ * reserving the hook signature `(ctx: PluginUninstallContext) =>
649
+ * Promise<void>`. Cleanup helper methods are NOT included in
650
+ * Phase 1 — plugin authors writing `await ctx.deletePublicAsset(...)`
651
+ * today would hit a TS error. The actual cleanup body lands when
652
+ * the future lifecycle-dispatch PR adds the helper methods to this
653
+ * type (additive, no breaking change for plugins that declared an
654
+ * empty `uninstall` body in advance).
655
+ */
656
+ interface PluginUninstallContext extends PluginRuntimeContext {
657
+ }
636
658
  /**
637
659
  * Extended runtime context for **trusted** hook handlers. Adds
638
660
  * `secret<T>(key)` — an async accessor that reads from the isolated
@@ -934,6 +956,52 @@ interface AmplessPlugin {
934
956
  hooks?: {
935
957
  [K in EventType]?: PluginEventHandler<K>;
936
958
  };
959
+ /**
960
+ * Lifecycle hook called when this plugin is removed from
961
+ * `cms.config.ts` (Phase 1 reservation: runtime no-op).
962
+ *
963
+ * Today the runtime does not detect plugin removal and does not
964
+ * invoke this hook — orphan data left behind by an uninstalled
965
+ * plugin must be cleaned up manually by the operator. A future
966
+ * lifecycle-dispatch PR will need to solve the underlying
967
+ * problem that a plugin that has been deleted from `cms.config.ts`
968
+ * no longer has a callable factory in memory — `Config.plugins`
969
+ * (see `packages/ampless/src/types.ts`) only carries currently-
970
+ * active plugin objects. Possible future approaches:
971
+ *
972
+ * - Two-stage `cms.config.ts` flag — `{ plugin: myPlugin(), pendingRemoval: true }`
973
+ * keeps the plugin loadable while the runtime calls `uninstall`,
974
+ * then the operator removes the entry once cleanup succeeds.
975
+ * - Explicit `npx ampless uninstall <name>` CLI command — keeps
976
+ * the plugin imported during the call by reading the prior
977
+ * `cms.config.ts` entry, fires `uninstall`, then mutates the
978
+ * file.
979
+ * - Persist prior manifest + `packageName` to DDB/disk so the
980
+ * runtime can `await import(packageName)` to re-acquire the
981
+ * factory — assumes the npm package is still installed.
982
+ *
983
+ * The exact mechanism is deferred to the lifecycle-dispatch PR.
984
+ * What this reservation locks in: when `uninstall` does fire,
985
+ * it runs in a trusted-Lambda IAM context with cleanup grants for
986
+ * the five plugin-owned data areas (see
987
+ * docs/architecture/08-plugin-architecture.md
988
+ * §"Plugin-owned data areas"). Idempotency is the plugin author's
989
+ * responsibility — the hook may be invoked more than once
990
+ * (SQS at-least-once or operator-retry).
991
+ *
992
+ * **Phase 1 reservation scope**: only the hook name and signature
993
+ * are reserved. The ctx does NOT yet carry cleanup helpers
994
+ * (`deletePublicAsset` / `deletePluginSetting` /
995
+ * `deletePluginSecret`) — writing `await ctx.deletePublicAsset(...)`
996
+ * today is a TS error. The recommended Phase 1 declaration is an
997
+ * **empty body** (`async (_ctx) => {}`); the actual cleanup body
998
+ * lands when the lifecycle-dispatch PR adds the helpers. Plugins
999
+ * that declared the empty-body uninstall today will pick up the
1000
+ * cleanup invocation events without re-publishing for the signature
1001
+ * change, but a re-publish is required to add the actual cleanup
1002
+ * body.
1003
+ */
1004
+ uninstall?: (ctx: PluginUninstallContext) => Promise<void>;
937
1005
  /**
938
1006
  * Per-post metadata generator. Pure function, called from Next.js
939
1007
  * generateMetadata(). Must not have side effects.
@@ -1551,4 +1619,4 @@ declare function pickDefaultEntrypoint(files: readonly {
1551
1619
 
1552
1620
  declare const VERSION = "0.0.1";
1553
1621
 
1554
- 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 ImageDisplay, type KvItem, type KvStore, type LinkListItem, type ListOptions, 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 PluginUrlField, type Post, type PostIndexEventPayload, type PostIndexEventType, type PostMetadata, 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, listPosts, listSiteSettings, mimeTypeFor, parseColorPair, parseLinkList, pickDefaultEntrypoint, resolveLocalized, resolvePluginSettings, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, stringifyLinkList, stripCommonPrefix, themeSettingKey, unflattenSettings, updatePost, validateBundle, validateBundlePath, validatePluginSettingValue, validatePublicAssetKey, validateThemeValue };
1622
+ 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 ImageDisplay, type KvItem, type KvStore, type LinkListItem, type ListOptions, 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 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, listPosts, listSiteSettings, mimeTypeFor, parseColorPair, parseLinkList, pickDefaultEntrypoint, resolveLocalized, resolvePluginSettings, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, stringifyLinkList, stripCommonPrefix, themeSettingKey, unflattenSettings, updatePost, validateBundle, validateBundlePath, validatePluginSettingValue, validatePublicAssetKey, validateThemeValue };
package/dist/index.js CHANGED
@@ -224,6 +224,14 @@ function definePlugin(p) {
224
224
  );
225
225
  }
226
226
  }
227
+ if (p.trust_level === "privileged") {
228
+ const hasHooks = p.hooks && Object.keys(p.hooks).length > 0 || (p.capabilities?.includes("eventHooks") ?? false);
229
+ if (hasHooks) {
230
+ console.warn(
231
+ `[ampless] Plugin "${p.name}" declares trust_level: 'privileged' with event hooks, but no privileged Lambda is provisioned yet. Hooks will not execute. Sync render surfaces (publicHead / metadata / publicBodyForPost / etc.) work normally regardless of trust_level. See docs/architecture/08-plugin-architecture.md#trust-levels for the future plan.`
232
+ );
233
+ }
234
+ }
227
235
  return p;
228
236
  }
229
237
 
@@ -279,11 +279,20 @@ runtime がチェックする内容:
279
279
 
280
280
  3 階層、選択基準は **イベントフック (hooks) が何を必要とするか** で決まります (sync サーフェスは IAM に触れない):
281
281
 
282
- | 階層 | IAM | 用途 |
283
- |---|---|---|
284
- | `untrusted` | なし (SQS consume のみ) | head/body descriptor、webhook 配送、コンテンツ変換 |
285
- | `trusted` | 投稿読み出し、`public/plugins/<instanceId ?? name>/...` への書き込み | RSS フィード、sitemap、計算済み JSON インデックス |
286
- | `privileged` | 予約 | 将来: SES、secret、private S3 |
282
+ | 階層 | IAM | 今日動くもの | 用途 |
283
+ |---|---|---|---|
284
+ | `untrusted` | なし (SQS consume のみ) | 同期サーフェス + イベントフック | head/body descriptor、webhook 配送、コンテンツ変換 |
285
+ | `trusted` | 投稿読み出し、`public/plugins/<instanceId ?? name>/...` への書き込み | 同期サーフェス + イベントフック | RSS フィード、sitemap、計算済み JSON インデックス |
286
+ | `privileged` | 予約 | 同期サーフェスのみ — イベントフックはサイレントフィルタ(警告ログあり) | 将来: SES、secret、private S3 |
287
+
288
+ > **`privileged` プラグイン作者へ:** `trust_level: 'privileged'` とイベント
289
+ > `hooks` を宣言した場合、**現時点でフックは実行されません**。両 processor が
290
+ > privileged プラグインをフィルタアウトし、一致する SQS イベントごとに
291
+ > `console.warn` を出力するため、サイレントドロップは可視化されます。
292
+ > 同期サーフェス(`publicHead`、`publicBodyEnd`、`metadata`、`publicBodyForPost`、
293
+ > `publicHtmlForPost`)は `trust_level` に関わらず正常に動き、警告も出ません。
294
+ > privileged Lambda が用意された際、`'privileged'` を宣言済みのプラグインは
295
+ > 自動的に新しい tier を使えるようになります — コードの変更は不要です。
287
296
 
288
297
  決め方の目安:
289
298
 
@@ -291,7 +300,7 @@ runtime がチェックする内容:
291
300
  - **hooks から投稿を読みたい (publish 時にフィードを再生成等)** → `trusted`
292
301
  - **`public/plugins/*` 以外への S3 PutObject や他の AWS API が必要** → 今はプラグインで ship せず、privileged 層を待つかプラグイン外で実装
293
302
 
294
- trust level がズレているプラグインは「権限不足で sliently fail」または「不要に強い権限を持つ」のどちらか。階層を切り替えて再デプロイすれば直ります。
303
+ trust level がズレているプラグインは「権限不足でサイレントに fail」または「不要に強い権限を持つ」のどちらか。階層を切り替えて再デプロイすれば直ります。
295
304
 
296
305
  ---
297
306
 
@@ -964,6 +973,67 @@ admin は再デプロイなしにいつでも secret をローテーションで
964
973
 
965
974
  ---
966
975
 
976
+ ## 9b. プラグインのデータの保存場所
977
+
978
+ プラグインが所有するデータは以下の 5 つのストレージ領域に置かれる可能性があり、**現状の書き込み経路は領域ごとに異なります**。3 つのファミリに分かれます:
979
+
980
+ - **KvStore** — admin/editor が AppSync 経由で書き込みます。プラグインの hook には KvStore write helper は提供されていません。
981
+ - **PluginSecret + PluginSecretIndicator** — `plugin-secret-handler` Lambda が書き込みます。admin/editor が `setPluginSecret` / `clearPluginSecret` AppSync mutation を呼ぶと、handler Lambda が DDB に書く流れです。trusted processor は `ctx.secret<T>()` で `PluginSecret` を読み取りますが、どちらの secret テーブルにも書き込みません。
982
+ - **S3 `public/plugins/{instanceId ?? name}/*`** — trusted Lambda の hook context (`ctx.writePublicAsset(...)`) から書き込みます。プラグインの hook が直接書き込めるのはこの領域だけです。
983
+
984
+ それ以外 — `Post`、`Page`、`Media`、`PostTag` DynamoDB テーブル、`public/site-settings.json` S3 ミラー、他プラグインの namespace — への書き込みは禁止です。現状 runtime が強制しているわけではなく、信頼(および将来の IAM 強化)によって担保されます。
985
+
986
+ | 領域 | パス / 識別子 | アクセスレベル | Phase |
987
+ |---|---|---|---|
988
+ | KvStore(admin 設定) | DynamoDB `pk='siteconfig'`、`sk='plugins.<instanceId>.<fieldKey>'` | admin/editor が AppSync 経由で書く(プラグインの hook context には KvStore write helper は現状提供されていない) | Phase 2 |
989
+ | KvStore(runtime 状態/キャッシュ) | DynamoDB `pk='pluginstate:<plugin>:...'`(TTL 任意) | admin/editor が AppSync 経由で書く(プラグインの hook context には KvStore write helper は現状提供されていない) | 現行 |
990
+ | PluginSecret | DynamoDB `PluginSecret` テーブル、`sk='plugins.<instanceId>.<fieldKey>'` | `trusted` 限定(IAM 専用 AppSync 認証) | Phase 6a |
991
+ | PluginSecretIndicator | DynamoDB `PluginSecretIndicator` テーブル、`sk='plugins.<instanceId>.<fieldKey>'` | `trusted` + admin/editor(indicator 読み取り) | Phase 6a |
992
+ | S3 プラグイン成果物 | `public/plugins/{instanceId ?? name}/*` | `trusted` 限定(`writePublicAsset`) | Phase 3 |
993
+
994
+ **cleanup は自動ではありません。** `cms.config.ts` からプラグインを外しても、5 領域のデータは自動削除されません。将来の lifecycle-dispatch PR が `uninstall` フックの起動メカニズムを追加するまで(§9c 参照)、オペレータによる手動削除が必要です。
995
+
996
+ **独自 DynamoDB テーブル。** プラグインが ampless スキーマ外に独自の DynamoDB テーブルを持つ場合、lifecycle 管理(アンインストール時の cleanup を含む)はプラグイン著者の責任です。ampless は外部テーブルを把握しておらず、将来の `uninstall` cleanup grant は上記 5 領域のみをカバーします。
997
+
998
+ 詳細な設計根拠と IAM grant 設計については [`docs/architecture/08-plugin-architecture.ja.md`](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.ja.md#プラグインが所有するデータ領域) を参照してください。
999
+
1000
+ ---
1001
+
1002
+ ## 9c. `uninstall` フック(Phase 1 予約)
1003
+
1004
+ `AmplessPlugin.uninstall` は **Phase 1 型予約** です — 現在 runtime はこれを呼び出しません。hook 名とシグネチャをプラグインコードが出回る前に確定し、将来の lifecycle-dispatch PR が名前・形を変更せずに起動を配線できるようにするのが目的です。
1005
+
1006
+ **Phase 1 スコープ**: hook 名とシグネチャのみ予約。`ctx` には cleanup helper (`deletePublicAsset` / `deletePluginSetting` / `deletePluginSecret`) がまだありません — 今日 `await ctx.deletePublicAsset(...)` と書くと TypeScript エラーです。これらの helper が lifecycle-dispatch PR で追加される際は `PluginUninstallContext` への追加(additive)であり、空のボディを宣言済みのプラグインへの破壊変更はありません。
1007
+
1008
+ **今日の推奨宣言** — 空のボディ:
1009
+
1010
+ ```ts
1011
+ // 例: S3 成果物と secret を書く trusted plugin
1012
+ definePlugin({
1013
+ name: 'my-trusted-plugin',
1014
+ apiVersion: 1,
1015
+ trust_level: 'trusted',
1016
+ capabilities: ['eventHooks', 'writePublicAsset', 'secretSettings'],
1017
+ hooks: { 'content.published': async (_evt, ctx) => { /* ... */ } },
1018
+ uninstall: async (_ctx) => {
1019
+ // Phase 1 予約: runtime は今日このフックを呼び出しません。
1020
+ // また `ctx` には cleanup helper
1021
+ // (`deletePublicAsset` / `deletePluginSetting` /
1022
+ // `deletePluginSecret`) がまだありません。
1023
+ // 空ボディを宣言しておくのが forward-compat の推奨形です —
1024
+ // 将来 lifecycle-dispatch PR がリリースされたとき、helper が
1025
+ // `PluginUninstallContext` に追加され、そこで cleanup ボディを
1026
+ // 実装します。今日の空宣言を再パブリッシュしなくても呼び出し
1027
+ // イベントは受け取れますが、実際の cleanup ボディを追加するには
1028
+ // 再パブリッシュが必要です。
1029
+ },
1030
+ })
1031
+ ```
1032
+
1033
+ **冪等性。** lifecycle-dispatch PR がリリースされると、`uninstall` フックは trusted Lambda の IAM コンテキストで実行されます。SQS 配送は at-least-once なので cleanup ボディが複数回実行される可能性があります。安全にリトライできるよう設計してください(S3 の `deleteObject` は冪等、DDB の conditional delete も key が消えていれば safe)。
1034
+
1035
+ ---
1036
+
967
1037
  ## 10. ウォークスルー: GA4 を Phase 1 から Phase 2 に移行する
968
1038
 
969
1039
  Phase 1 の GA4 プラグインは measurement ID を constructor 引数で受けていました。Phase 2 では後方互換のためにその引数を残しつつ、値は `ctx.setting()` 経由で読みます。
@@ -341,11 +341,22 @@ Three tiers, picked by what the plugin needs to do **inside
341
341
  event hooks** (the sync surfaces — metadata, head, body — don't
342
342
  touch IAM):
343
343
 
344
- | Tier | IAM | Used by |
345
- |---|---|---|
346
- | `untrusted` | none (SQS consume only) | head/body descriptors, webhook delivery, content transforms |
347
- | `trusted` | read posts, write `public/plugins/<instanceId ?? name>/...` | RSS feed, sitemap, computed JSON indexes |
348
- | `privileged` | reserved | future: SES, secrets, private S3 |
344
+ | Tier | IAM | What runs today | Used by |
345
+ |---|---|---|---|
346
+ | `untrusted` | none (SQS consume only) | sync surfaces + event hooks | head/body descriptors, webhook delivery, content transforms |
347
+ | `trusted` | read posts, write `public/plugins/<instanceId ?? name>/...` | sync surfaces + event hooks | RSS feed, sitemap, computed JSON indexes |
348
+ | `privileged` | reserved | sync surfaces only — event hooks are silently filtered out (warning logged) | future: SES, secrets, private S3 |
349
+
350
+ > **Warning for `privileged` plugin authors:** If you declare
351
+ > `trust_level: 'privileged'` with event `hooks` today, **your hooks
352
+ > WILL NOT EXECUTE**. Both event processors filter out privileged
353
+ > plugins and emit a `console.warn` on every matching SQS event so
354
+ > the drop is visible. Sync render surfaces (`publicHead`,
355
+ > `publicBodyEnd`, `metadata`, `publicBodyForPost`, `publicHtmlForPost`)
356
+ > work normally regardless of `trust_level` and emit no warning.
357
+ > When the privileged Lambda provisioning PR lands, plugins that
358
+ > already declared `'privileged'` will automatically pick up the new
359
+ > tier — nothing needs to change in your code.
349
360
 
350
361
  Rule of thumb:
351
362
 
@@ -1312,6 +1323,99 @@ describe('webhookPlugin signing', () => {
1312
1323
 
1313
1324
  ---
1314
1325
 
1326
+ ## 9b. Where your plugin's data lives
1327
+
1328
+ Plugin-owned data may live in the five storage areas listed below; the
1329
+ **current write paths differ by area** and fall into three families:
1330
+
1331
+ - **KvStore** — admin/editor write through AppSync. Plugin hooks have no
1332
+ KvStore write helper today.
1333
+ - **PluginSecret + PluginSecretIndicator** — written by the
1334
+ `plugin-secret-handler` Lambda, which is invoked by admin/editor through
1335
+ the `setPluginSecret` / `clearPluginSecret` AppSync mutations. The
1336
+ trusted processor reads `PluginSecret` via `ctx.secret<T>()` but does
1337
+ NOT write to either secret table.
1338
+ - **S3 `public/plugins/{instanceId ?? name}/*`** — written by the trusted
1339
+ Lambda's hook context (`ctx.writePublicAsset(...)`). This is the only
1340
+ area a plugin hook writes to directly today.
1341
+
1342
+ All other areas — the `Post`, `Page`, `Media`, and `PostTag` DynamoDB
1343
+ tables, the `public/site-settings.json` S3 mirror, and any other plugin's
1344
+ namespace — are off-limits. The runtime does not enforce this today; it is
1345
+ a contract enforced by trust (and future IAM hardening).
1346
+
1347
+ | Area | Path / identifier | Access level | Phase |
1348
+ |---|---|---|---|
1349
+ | KvStore (admin settings) | DynamoDB `pk='siteconfig'`, `sk='plugins.<instanceId>.<fieldKey>'` | admin/editor via AppSync; plugin hook write helper is not provided today | Phase 2 |
1350
+ | KvStore (runtime state/cache) | DynamoDB `pk='pluginstate:<plugin>:...'` with optional TTL | admin/editor via AppSync; plugin hook write helper is not provided today | Current |
1351
+ | PluginSecret | DynamoDB `PluginSecret` table, `sk='plugins.<instanceId>.<fieldKey>'` | `trusted` only (IAM-only AppSync auth) | Phase 6a |
1352
+ | PluginSecretIndicator | DynamoDB `PluginSecretIndicator` table, `sk='plugins.<instanceId>.<fieldKey>'` | `trusted` + admin/editor (read indicator) | Phase 6a |
1353
+ | S3 plugin assets | `public/plugins/{instanceId ?? name}/*` | `trusted` only (`writePublicAsset`) | Phase 3 |
1354
+
1355
+ **Cleanup is not automatic.** Removing a plugin from `cms.config.ts` leaves
1356
+ orphan data in all five areas. Manual operator cleanup is required until the
1357
+ future lifecycle-dispatch PR ships the invocation mechanism for the `uninstall`
1358
+ hook (see §9c below).
1359
+
1360
+ **Custom DynamoDB tables.** If your plugin provisions its own DynamoDB table
1361
+ outside the ampless schema, lifecycle management (including cleanup on uninstall)
1362
+ is your responsibility. ampless has no visibility into external tables and the
1363
+ future `uninstall` cleanup grants cover only the five areas above.
1364
+
1365
+ See [`docs/architecture/08-plugin-architecture.md`](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.md#plugin-owned-data-areas)
1366
+ for the full rationale and the IAM grant design.
1367
+
1368
+ ---
1369
+
1370
+ ## 9c. `uninstall` hook (Phase 1 reservation)
1371
+
1372
+ `AmplessPlugin.uninstall` is a **Phase 1 type reservation** — the runtime does
1373
+ not call it today. Its purpose is to lock in the hook name and signature before
1374
+ any plugin code ships, so the future lifecycle-dispatch PR can wire the
1375
+ invocation without renaming or reshaping anything.
1376
+
1377
+ **Phase 1 scope**: only the hook name and signature are reserved. The `ctx`
1378
+ does **not** yet carry cleanup helpers (`deletePublicAsset` /
1379
+ `deletePluginSetting` / `deletePluginSecret`) — writing
1380
+ `await ctx.deletePublicAsset(...)` today is a TypeScript error. When those
1381
+ helpers land (in the lifecycle-dispatch PR), they are added to
1382
+ `PluginUninstallContext` additively, with no breaking change to plugins that
1383
+ declared an empty body in advance.
1384
+
1385
+ **Recommended declaration today** — an empty body:
1386
+
1387
+ ```ts
1388
+ // Example: a trusted plugin that writes assets and stores secrets
1389
+ definePlugin({
1390
+ name: 'my-trusted-plugin',
1391
+ apiVersion: 1,
1392
+ trust_level: 'trusted',
1393
+ capabilities: ['eventHooks', 'writePublicAsset', 'secretSettings'],
1394
+ hooks: { 'content.published': async (_evt, ctx) => { /* ... */ } },
1395
+ uninstall: async (_ctx) => {
1396
+ // Phase 1 reservation: the runtime does not invoke this hook
1397
+ // today, AND `ctx` does not yet carry cleanup helpers
1398
+ // (`deletePublicAsset` / `deletePluginSetting` /
1399
+ // `deletePluginSecret`). Declaring an empty body is the
1400
+ // recommended forward-compat shape — when the future
1401
+ // lifecycle-dispatch PR ships, the helpers land on
1402
+ // `PluginUninstallContext` additively, and you fill in the
1403
+ // body THEN. Plugins that shipped the empty declaration
1404
+ // today do not need to re-publish for the signature change,
1405
+ // but a re-publish is required to add the actual cleanup
1406
+ // body.
1407
+ },
1408
+ })
1409
+ ```
1410
+
1411
+ **Idempotency.** When the lifecycle-dispatch PR ships, the `uninstall` hook
1412
+ runs in a trusted-Lambda IAM context. The SQS delivery is at-least-once, so
1413
+ your cleanup body may run more than once — design it to be safe to retry
1414
+ (e.g. `deleteObject` is idempotent on S3; a conditional `delete` on DDB is
1415
+ safe if the key is already gone).
1416
+
1417
+ ---
1418
+
1315
1419
  ## 10. Walk-through: migrating GA4 from Phase 1 to Phase 2
1316
1420
 
1317
1421
  The Phase 1 GA4 plugin took the measurement ID through a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ampless",
3
- "version": "1.0.0-alpha.34",
3
+ "version": "1.0.0-alpha.36",
4
4
  "description": "Serverless CMS for AWS Amplify",
5
5
  "license": "MIT",
6
6
  "type": "module",