ampless 1.0.0-alpha.36 → 1.0.0-alpha.37

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
@@ -896,6 +896,80 @@ interface PluginSettingsManifest {
896
896
  * capability warns.
897
897
  */
898
898
  secret?: readonly PluginSecretField[];
899
+ /**
900
+ * Optional manifest shape version (Phase 1 reservation: runtime
901
+ * no-op).
902
+ *
903
+ * Plugin authors bump this integer when they change the shape of
904
+ * `public` or `secret` field arrays in a way that today's
905
+ * write/read paths cannot transparently absorb — i.e. when a field
906
+ * is renamed, when a field's `type` changes incompatibly, or when
907
+ * a field's semantic meaning shifts (same key + same type but the
908
+ * value should be re-interpreted).
909
+ *
910
+ * **Phase 1 scope**: the field is type-only. The runtime does NOT
911
+ * read it today, does NOT persist it next to stored values, and
912
+ * does NOT trigger any migration. Today's behaviour around shape
913
+ * changes continues to apply, but it differs between `public` and
914
+ * `secret` because the two travel through entirely different
915
+ * write/read paths:
916
+ *
917
+ * - `settings.public` goes through `resolvePluginSettings`
918
+ * (`packages/ampless/src/plugin-settings.ts`), which iterates
919
+ * `manifest.public` and falls back to `field.default` per
920
+ * field. So for public fields, additions resolve via `default`,
921
+ * deletions become orphan KvStore rows that the resolver
922
+ * silently skips, and incompatible type changes fall through
923
+ * to `default` when the stored value fails validation.
924
+ * - `settings.secret` is never read by `resolvePluginSettings`.
925
+ * The admin UI writes individual values through the
926
+ * `setPluginSecret` AppSync mutation; trusted hooks read them
927
+ * individually by key via `ctx.secret<T>(key)` which goes
928
+ * directly to the `PluginSecret` DynamoDB table. There is no
929
+ * `manifest.default` fallback for secrets (the type forbids
930
+ * `default` on secret fields). Additions show up in the admin
931
+ * UI when the manifest changes; removals leave the stored
932
+ * ciphertext row orphaned (no resolver runs over it); renames
933
+ * and type changes mean the old key's stored row is never
934
+ * read again until an operator deletes it manually.
935
+ *
936
+ * None of these paths produce a signal to the plugin author.
937
+ *
938
+ * **Future migration PR**: may persist the active manifest
939
+ * version somewhere alongside stored values and may compare it
940
+ * to `manifest.version` at resolve time to detect mismatch. The
941
+ * exact storage location, comparison timing, and mismatch
942
+ * response are all design territory for that future PR — this
943
+ * reservation fixes the field name and type on the manifest,
944
+ * nothing more.
945
+ *
946
+ * Important scope distinction: this reservation covers the
947
+ * `version` *field name and type* on `PluginSettingsManifest`
948
+ * only. The actual migration mechanism (a `migrate` hook, an
949
+ * admin-driven flow, batch resolve-time-rewrite, etc.) is a
950
+ * separate design that lands with its own PR. Declaring
951
+ * `version` today does NOT pre-wire a migration body — when the
952
+ * future migration PR ships, plugins that want to provide a
953
+ * migration body will need to re-publish to add it. Existing
954
+ * plugins that omit `version` are unaffected by this addition;
955
+ * plugins that want to participate in the future migration
956
+ * detection path can opt in today by declaring `version: 1`,
957
+ * instead of having to re-publish later just to add the
958
+ * version declaration once the migration PR ships. The
959
+ * migration body itself (and any future `migrate` hook
960
+ * signature) is NOT reserved by this PR.
961
+ *
962
+ * Recommended values: positive integer, start at 1. Declare
963
+ * `version: 1` when the manifest first gains a `version` field,
964
+ * then bump by 1 on each shape-breaking release. Do NOT use
965
+ * `0` / negative numbers / floats — the `number` type accepts
966
+ * them but the semantics for those values are reserved for the
967
+ * future migration PR (`0` may be conflated with "no version
968
+ * declared" / legacy / pre-v1). Skip the field entirely if you
969
+ * do not care about migration support (the default, current
970
+ * behaviour).
971
+ */
972
+ version?: number;
899
973
  }
900
974
  interface AmplessPlugin {
901
975
  name: string;
@@ -185,7 +185,11 @@ interface AmplessPlugin {
185
185
  publicBodyForPost?(post: Post, ctx): readonly PublicPostBodyDescriptor[]
186
186
  publicHtmlForPost?(post: Post, ctx): readonly PublicPostHtmlDescriptor[]
187
187
  ogImage?: OgImageConfig
188
- settings?: { public?: readonly PluginSettingField[] }
188
+ settings?: {
189
+ public?: readonly PluginSettingField[]
190
+ secret?: readonly PluginSecretField[]
191
+ version?: number // Phase 1 reservation; runtime ignores
192
+ }
189
193
  }
190
194
  ```
191
195
 
@@ -1034,6 +1038,88 @@ definePlugin({
1034
1038
 
1035
1039
  ---
1036
1040
 
1041
+ ## 9d. settings の形状を変えるとき(Phase 1 予約)
1042
+
1043
+ ### 今日の挙動: `public` と `secret` は別の経路を辿る
1044
+
1045
+ 形状変更は今日の runtime で silently 吸収されますが、実際の挙動は `settings.public` と `settings.secret` で異なります。両者は完全に別の write/read パスを使っているためです。
1046
+
1047
+ #### `settings.public`(`resolvePluginSettings` の寛容な resolver)
1048
+
1049
+ `resolvePluginSettings`([packages/ampless/src/plugin-settings.ts](packages/ampless/src/plugin-settings.ts))は `manifest.public` のみをイテレートし、field ごとに `field.default` にフォールバックします。resolver は `manifest.secret` を一切見ません。
1050
+
1051
+ | 変更 | 今日の挙動(public フィールド) |
1052
+ |---|---|
1053
+ | **フィールド追加** | 新フィールドは `manifest.default` から解決されます。ストレージに値がないため default が使われます。 |
1054
+ | **フィールド削除** | KvStore に orphan row が残ります。`resolvePluginSettings` は現在の manifest にない key を silently skip します。 |
1055
+ | **フィールド改名**(`endpoint` → `url`) | 削除 + 追加として扱われます。旧値は到達不能(orphan)になり、新フィールドは `default` から解決されます。 |
1056
+ | **型を非互換に変更** | 新しい validator がストレージの値に対して実行されます。通過すれば値が使われ、失敗すれば `default`(または `undefined`)にフォールバックします。 |
1057
+
1058
+ #### `settings.secret`(admin UI + `PluginSecret` + `ctx.secret()`、寛容な resolver なし)
1059
+
1060
+ Secret フィールドは `resolvePluginSettings` から一切読まれません。別の経路を辿ります:
1061
+
1062
+ - admin UI が `setPluginSecret` AppSync mutation 経由で値を 1 つずつ書き、`plugin-secret-handler` Lambda が暗号化して `PluginSecret` DynamoDB テーブルに格納
1063
+ - trusted hook が `ctx.secret<T>(key)` で key 単位で直接 `PluginSecret` から復号読み取り
1064
+ - `PluginSecretField` 型は `default` を持てない(型レベルで禁止)。manifest レベルのフォールバックは存在しない
1065
+
1066
+ | 変更 | 今日の挙動(secret フィールド) |
1067
+ |---|---|
1068
+ | **フィールド追加** | admin UI に新フィールドが表示されます。admin が値を設定するまで `ctx.secret<T>(key)` は `undefined` を返します。 |
1069
+ | **フィールド削除** | admin UI から消えますが、`PluginSecret` 内の暗号化された row は orphan として残ります。resolver は走らないため、operator が手動で削除する必要があります。 |
1070
+ | **フィールド改名** | 旧 key の暗号化された row は orphan になります(resolver / cleanup なし)。新 key は未設定として表示され、admin が新 key に値を入れ直す必要があります。 |
1071
+ | **型を非互換に変更** | `validatePluginSettingValue` は write 時のみ実行されます。既存の暗号化値は read 時には影響を受けず、`ctx.secret<T>(key)` は最後に書かれた値を返します。新しい入力を admin が保存しようとすると新 validator で reject される、というだけです。 |
1072
+
1073
+ これらのケースでエラーや警告は発生しません。形状変更後の挙動を確認するには、プラグイン著者が手動でストレージの値を確認する必要があります。
1074
+
1075
+ ### `version` 予約について
1076
+
1077
+ `PluginSettingsManifest.version?: number` は **Phase 1 型予約** です。runtime は今日このフィールドを読みません。宣言しても上記の寛容な resolver の挙動は変わりません。
1078
+
1079
+ この予約は、将来の migration PR がストレージの値に manifest の version をどこかに保存し、resolve 時に `manifest.version` と比較してミスマッチを検出できるようにするためのものです。ミスマッチ時の応答(warn / skip / in-place migration / admin 主導フローなど)はその future PR の設計領域です。
1080
+
1081
+ **`version` を宣言しても今日保証されないこと:**
1082
+
1083
+ - migration ボディは実行されません。
1084
+ - ストレージの値の再検証・再 default も行われません。
1085
+ - `migrate` hook のシグネチャは予約されていません(それは別の future 設計)。
1086
+
1087
+ **`version` を今日宣言することで得られるもの:**
1088
+
1089
+ - その PR がリリースされた後に `version` フィールドを追加するためだけの再パブリッシュが不要になります(将来の migration 検出パスに最初から参加できます)。
1090
+ - manifest に versioned な形状であることを将来のメンテナーに伝えます。
1091
+
1092
+ ### 推奨パターン
1093
+
1094
+ | シナリオ | 推奨 |
1095
+ |---|---|
1096
+ | 追加のみの変更(新しいオプションフィールド、default あり) | `version` の bump 不要。寛容な resolver が吸収します。 |
1097
+ | 非互換変更(改名、型変更、意味的変化) | `version` を 1 増やします。 |
1098
+ | 既存 manifest に初めて `version` を追加する | `version: 1` から始めます。 |
1099
+
1100
+ **正の整数、1 始まりを使用してください。** `0`、負数、小数は使わないでください。`number` 型はそれらを受け入れますが、将来の migration PR が `0` / undefined に特別な意味(legacy / pre-v1 との混同を避けるため)を予約する可能性があります。
1101
+
1102
+ ### コード例
1103
+
1104
+ ```ts
1105
+ definePlugin({
1106
+ name: 'my-plugin',
1107
+ apiVersion: 1,
1108
+ trust_level: 'untrusted',
1109
+ capabilities: ['adminSettings'],
1110
+ settings: {
1111
+ version: 2, // ← Phase 1 reservation。今日の runtime は無視します。
1112
+ public: [
1113
+ { type: 'url', key: 'webhookUrl', label: 'Webhook URL', required: true },
1114
+ ],
1115
+ },
1116
+ })
1117
+ ```
1118
+
1119
+ 将来の migration PR がリリースされると、すでに `version` を宣言しているプラグインは自動的に検出パスに乗ります。実際の migration ボディを提供したいプラグインは、その PR がリリースされてから再パブリッシュしてボディを追加する必要があります。`version` を省略したプラグインは引き続き現在の寛容な resolver の挙動のままで変化ありません。
1120
+
1121
+ ---
1122
+
1037
1123
  ## 10. ウォークスルー: GA4 を Phase 1 から Phase 2 に移行する
1038
1124
 
1039
1125
  Phase 1 の GA4 プラグインは measurement ID を constructor 引数で受けていました。Phase 2 では後方互換のためにその引数を残しつつ、値は `ctx.setting()` 経由で読みます。
@@ -213,7 +213,11 @@ interface AmplessPlugin {
213
213
  publicBodyForPost?(post: Post, ctx): readonly PublicPostBodyDescriptor[]
214
214
  publicHtmlForPost?(post: Post, ctx): readonly PublicPostHtmlDescriptor[]
215
215
  ogImage?: OgImageConfig
216
- settings?: { public?: readonly PluginSettingField[] }
216
+ settings?: {
217
+ public?: readonly PluginSettingField[]
218
+ secret?: readonly PluginSecretField[]
219
+ version?: number // Phase 1 reservation; runtime ignores
220
+ }
217
221
  }
218
222
  ```
219
223
 
@@ -1416,6 +1420,115 @@ safe if the key is already gone).
1416
1420
 
1417
1421
  ---
1418
1422
 
1423
+ ## 9d. When you change settings shape (Phase 1 reservation)
1424
+
1425
+ ### What happens today: `public` and `secret` travel through different paths
1426
+
1427
+ Shape changes are absorbed silently by today's runtime, but the actual
1428
+ behaviour differs between `settings.public` and `settings.secret` because
1429
+ they use entirely different write/read paths.
1430
+
1431
+ #### `settings.public` (lenient resolver via `resolvePluginSettings`)
1432
+
1433
+ `resolvePluginSettings` ([packages/ampless/src/plugin-settings.ts](packages/ampless/src/plugin-settings.ts))
1434
+ iterates `manifest.public` and falls back to `field.default` per field. The
1435
+ resolver never looks at `manifest.secret`.
1436
+
1437
+ | Change | Behaviour today (public fields) |
1438
+ |---|---|
1439
+ | **Field added** | New field resolves via `manifest.default` — stored value is absent, default takes over. |
1440
+ | **Field deleted** | Orphan row remains in KvStore. `resolvePluginSettings` silently skips keys that are not in the current manifest. |
1441
+ | **Field renamed** (`endpoint` → `url`) | Treated as deletion + addition: old value is unreachable (orphan), new field resolves via `default`. |
1442
+ | **Type changed incompatibly** | New validator runs on the stored value. If it passes, the value is used. If it fails, falls through to `default` (or `undefined`). |
1443
+
1444
+ #### `settings.secret` (admin UI + `PluginSecret` + `ctx.secret()`, no lenient resolver)
1445
+
1446
+ Secret fields are never read by `resolvePluginSettings`. They travel a
1447
+ separate path:
1448
+
1449
+ - The admin UI writes individual values through the `setPluginSecret`
1450
+ AppSync mutation, which the `plugin-secret-handler` Lambda encrypts and
1451
+ stores in the `PluginSecret` DynamoDB table.
1452
+ - Trusted hooks read them individually by key via `ctx.secret<T>(key)`,
1453
+ which goes directly to `PluginSecret` and decrypts.
1454
+ - The `PluginSecretField` type forbids `default` — there is no
1455
+ manifest-level fallback for secret values.
1456
+
1457
+ | Change | Behaviour today (secret fields) |
1458
+ |---|---|
1459
+ | **Field added** | New field appears in the admin UI. Until an admin sets a value, `ctx.secret<T>(key)` returns `undefined`. |
1460
+ | **Field deleted** | The field disappears from the admin UI, but the encrypted row in `PluginSecret` is orphaned. No resolver runs over it; an operator must delete it manually. |
1461
+ | **Field renamed** | Old key's encrypted row is orphaned (no resolver / cleanup). The new key shows up unset. Admin must re-enter the value under the new key. |
1462
+ | **Type changed incompatibly** | `validatePluginSettingValue` only runs at write time, so an existing stored ciphertext is unaffected on read; `ctx.secret<T>(key)` returns whatever was last written. A re-validate on admin save would reject incompatible new input. |
1463
+
1464
+ No error or warning is produced in any of these cases. Plugin authors must
1465
+ inspect their stored values manually if they need to verify behaviour
1466
+ after a shape change.
1467
+
1468
+ ### The `version` reservation
1469
+
1470
+ `PluginSettingsManifest.version?: number` is a **Phase 1 type
1471
+ reservation** — the runtime does NOT read it today. Declaring it has no
1472
+ effect on the lenient resolver above.
1473
+
1474
+ The reservation exists so that a future migration PR may persist the
1475
+ active manifest version somewhere alongside stored values and compare it to
1476
+ `manifest.version` at resolve time to detect mismatch. The exact mismatch
1477
+ response (warn / skip / migrate in-place / trigger an admin-driven flow) is
1478
+ design territory for that future PR.
1479
+
1480
+ **What declaring `version` today does NOT promise:**
1481
+
1482
+ - It does NOT trigger any migration body.
1483
+ - It does NOT cause the runtime to re-validate or re-default stored values.
1484
+ - It does NOT reserve a `migrate` hook signature — that is a separate future
1485
+ design.
1486
+
1487
+ **What declaring `version` today does do:**
1488
+
1489
+ - It positions the plugin to be picked up by the future migration detection
1490
+ path, without requiring a re-publish just to add the `version` field once
1491
+ that PR ships.
1492
+ - It communicates intent to future maintainers that this manifest has a
1493
+ versioned shape.
1494
+
1495
+ ### Recommended pattern
1496
+
1497
+ | Scenario | Recommendation |
1498
+ |---|---|
1499
+ | Additive change only (new optional field, default provided) | No `version` bump needed. Lenient resolver handles it. |
1500
+ | Non-additive change (rename, incompatible type change, semantic shift) | Bump `version` by 1. |
1501
+ | First time adding `version` to an existing manifest | Start at `version: 1`. |
1502
+
1503
+ **Use positive integers, starting at 1.** Do NOT use `0`, negative numbers,
1504
+ or floats — the `number` type accepts them but the future migration PR may
1505
+ reserve special semantics for `0` / undefined (legacy / pre-v1 conflation).
1506
+
1507
+ ### Code example
1508
+
1509
+ ```ts
1510
+ definePlugin({
1511
+ name: 'my-plugin',
1512
+ apiVersion: 1,
1513
+ trust_level: 'untrusted',
1514
+ capabilities: ['adminSettings'],
1515
+ settings: {
1516
+ version: 2, // ← Phase 1 reservation. Today runtime ignores.
1517
+ public: [
1518
+ { type: 'url', key: 'webhookUrl', label: 'Webhook URL', required: true },
1519
+ ],
1520
+ },
1521
+ })
1522
+ ```
1523
+
1524
+ When a future migration PR ships, plugins that have already declared
1525
+ `version` will be on the detection path automatically. Plugins that want to
1526
+ provide an actual migration body will need to re-publish after that PR ships
1527
+ to add the body. Plugins that omit `version` entirely continue with the
1528
+ current lenient-resolver behaviour — no change.
1529
+
1530
+ ---
1531
+
1419
1532
  ## 10. Walk-through: migrating GA4 from Phase 1 to Phase 2
1420
1533
 
1421
1534
  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.36",
3
+ "version": "1.0.0-alpha.37",
4
4
  "description": "Serverless CMS for AWS Amplify",
5
5
  "license": "MIT",
6
6
  "type": "module",