ampless 1.0.0-alpha.36 → 1.0.0-alpha.38
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 +142 -1
- package/dist/index.js +5 -0
- package/docs/plugin-author-guide.ja.md +87 -1
- package/docs/plugin-author-guide.md +114 -1
- package/package.json +1 -1
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;
|
|
@@ -1287,6 +1361,12 @@ interface Config {
|
|
|
1287
1361
|
* field is absent.
|
|
1288
1362
|
*/
|
|
1289
1363
|
cache?: CacheConfig;
|
|
1364
|
+
/**
|
|
1365
|
+
* Post revision-history knobs. The event-dispatcher Lambda snapshots
|
|
1366
|
+
* each post save into the `PostHistory` table; this controls how long
|
|
1367
|
+
* those snapshots are retained.
|
|
1368
|
+
*/
|
|
1369
|
+
history?: HistoryConfig;
|
|
1290
1370
|
}
|
|
1291
1371
|
/**
|
|
1292
1372
|
* Tunables for middleware-computed `Cache-Control`. See
|
|
@@ -1311,6 +1391,20 @@ interface CacheConfig {
|
|
|
1311
1391
|
*/
|
|
1312
1392
|
deepTtlSeconds?: number;
|
|
1313
1393
|
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Post revision-history tunables, read by the event-dispatcher Lambda
|
|
1396
|
+
* (via the template shell) when snapshotting each post save.
|
|
1397
|
+
*/
|
|
1398
|
+
interface HistoryConfig {
|
|
1399
|
+
/**
|
|
1400
|
+
* Days to retain each post revision before DynamoDB TTL deletes it.
|
|
1401
|
+
* Default 0 = keep forever (no `ttl` attribute is written). DynamoDB TTL
|
|
1402
|
+
* deletes within ~48h after expiry, so effective retention is
|
|
1403
|
+
* retentionDays + up to ~2 days. Changing this only affects revisions
|
|
1404
|
+
* written afterward — existing rows keep their original expiry.
|
|
1405
|
+
*/
|
|
1406
|
+
retentionDays?: number;
|
|
1407
|
+
}
|
|
1314
1408
|
type Role = 'reader' | 'editor' | 'admin';
|
|
1315
1409
|
interface AuthContext {
|
|
1316
1410
|
userId: string;
|
|
@@ -1341,6 +1435,45 @@ interface ListOptions {
|
|
|
1341
1435
|
type CreatePostInput = Omit<Post, 'postId'> & {
|
|
1342
1436
|
postId?: string;
|
|
1343
1437
|
};
|
|
1438
|
+
/**
|
|
1439
|
+
* One in-memory snapshot of a Post at save time, read back from the
|
|
1440
|
+
* `PostHistory` table (written by the event-dispatcher Lambda on each
|
|
1441
|
+
* Post INSERT/MODIFY — see packages/backend/src/events/dispatcher.ts).
|
|
1442
|
+
*
|
|
1443
|
+
* `body` is already decoded from its AWSJSON wire form (the provider
|
|
1444
|
+
* runs `decodeAwsJson` before handing the row up), so it matches the
|
|
1445
|
+
* `Post['body']` shape for the declared `format`.
|
|
1446
|
+
*/
|
|
1447
|
+
interface PostRevision {
|
|
1448
|
+
/** Deterministic id `${postId}#${revisedAt}` — unique per save. */
|
|
1449
|
+
postHistoryId: string;
|
|
1450
|
+
postId: string;
|
|
1451
|
+
/** ISO 8601 save time. Also the `byPost` GSI sort key (newest-first). */
|
|
1452
|
+
revisedAt: string;
|
|
1453
|
+
title?: string;
|
|
1454
|
+
slug?: string;
|
|
1455
|
+
excerpt?: string;
|
|
1456
|
+
format?: ContentFormat;
|
|
1457
|
+
body?: unknown;
|
|
1458
|
+
status?: PostStatus;
|
|
1459
|
+
publishedAt?: string;
|
|
1460
|
+
tags?: string[];
|
|
1461
|
+
metadata?: PostMetadata;
|
|
1462
|
+
}
|
|
1463
|
+
/** Pagination options for `listPostHistory`. */
|
|
1464
|
+
interface ListPostHistoryOptions {
|
|
1465
|
+
limit?: number;
|
|
1466
|
+
nextToken?: string;
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* A page of revisions newest-first. `nextToken` feeds straight back into
|
|
1470
|
+
* `ListPostHistoryOptions.nextToken` for the next page; `undefined` means
|
|
1471
|
+
* no more rows.
|
|
1472
|
+
*/
|
|
1473
|
+
interface PostRevisionConnection {
|
|
1474
|
+
items: PostRevision[];
|
|
1475
|
+
nextToken?: string;
|
|
1476
|
+
}
|
|
1344
1477
|
interface PostsProvider {
|
|
1345
1478
|
list(opts?: ListOptions): Promise<Post[]>;
|
|
1346
1479
|
get(slug: string): Promise<Post | null>;
|
|
@@ -1348,6 +1481,7 @@ interface PostsProvider {
|
|
|
1348
1481
|
create(data: CreatePostInput): Promise<Post>;
|
|
1349
1482
|
update(postId: string, data: Partial<Post>): Promise<Post>;
|
|
1350
1483
|
remove(postId: string): Promise<void>;
|
|
1484
|
+
listPostHistory(postId: string, options?: ListPostHistoryOptions): Promise<PostRevisionConnection>;
|
|
1351
1485
|
}
|
|
1352
1486
|
declare function setPostsProvider(p: PostsProvider): void;
|
|
1353
1487
|
declare function hasPostsProvider(): boolean;
|
|
@@ -1357,6 +1491,13 @@ declare function getPostById(postId: string): Promise<Post | null>;
|
|
|
1357
1491
|
declare function createPost(data: CreatePostInput): Promise<Post>;
|
|
1358
1492
|
declare function updatePost(postId: string, data: Partial<Post>): Promise<Post>;
|
|
1359
1493
|
declare function deletePost(postId: string): Promise<void>;
|
|
1494
|
+
/**
|
|
1495
|
+
* List a post's revision history, newest-first. Backed by the
|
|
1496
|
+
* `PostHistory` `byPost` GSI. Requires a configured provider (the dummy
|
|
1497
|
+
* fallback has no history) — returns an empty connection otherwise so
|
|
1498
|
+
* callers (e.g. the admin history panel) degrade gracefully.
|
|
1499
|
+
*/
|
|
1500
|
+
declare function listPostHistory(postId: string, options?: ListPostHistoryOptions): Promise<PostRevisionConnection>;
|
|
1360
1501
|
|
|
1361
1502
|
interface KvItem<T = unknown> {
|
|
1362
1503
|
pk: string;
|
|
@@ -1619,4 +1760,4 @@ declare function pickDefaultEntrypoint(files: readonly {
|
|
|
1619
1760
|
|
|
1620
1761
|
declare const VERSION = "0.0.1";
|
|
1621
1762
|
|
|
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 };
|
|
1763
|
+
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 };
|
package/dist/index.js
CHANGED
|
@@ -83,6 +83,10 @@ async function deletePost(postId) {
|
|
|
83
83
|
if (!provider) throw new Error("No posts provider configured. Call setPostsProvider() first.");
|
|
84
84
|
return provider.remove(postId);
|
|
85
85
|
}
|
|
86
|
+
async function listPostHistory(postId, options) {
|
|
87
|
+
if (!provider) return { items: [] };
|
|
88
|
+
return provider.listPostHistory(postId, options);
|
|
89
|
+
}
|
|
86
90
|
|
|
87
91
|
// src/kv.ts
|
|
88
92
|
var store = null;
|
|
@@ -794,6 +798,7 @@ export {
|
|
|
794
798
|
hasPostsProvider,
|
|
795
799
|
isTagListUrl,
|
|
796
800
|
isValidPluginKey,
|
|
801
|
+
listPostHistory,
|
|
797
802
|
listPosts,
|
|
798
803
|
listSiteSettings,
|
|
799
804
|
mimeTypeFor,
|
|
@@ -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?: {
|
|
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?: {
|
|
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
|