create-ampless 1.0.0-alpha.108 → 1.0.0-alpha.111

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.
@@ -8,6 +8,13 @@ import { util } from '@aws-appsync/utils'
8
8
  //
9
9
  // Drafts are dropped in the response handler. If draft + published
10
10
  // somehow share a slug we prefer the published row.
11
+ //
12
+ // Scheduled-publish: a `published` post is visible only when its
13
+ // publishedAt is absent (= immediate) OR <= now (= already live).
14
+ // When publishedAt is present and in the future the post is treated as
15
+ // "not yet live" and is hidden from the public site. This check is
16
+ // server-authoritative: the AppSync resolver enforces it on every read
17
+ // without relying on any client-side filtering.
11
18
  export function request(ctx) {
12
19
  const slug = ctx.args.slug
13
20
  return {
@@ -25,6 +32,11 @@ export function request(ctx) {
25
32
  export function response(ctx) {
26
33
  if (ctx.error) util.error(ctx.error.message, ctx.error.type)
27
34
  const items = ctx.result.items ?? []
28
- const published = items.find((i) => i.status === 'published')
35
+ const now = util.time.nowISO8601()
36
+ // Hide a published post only when publishedAt exists AND is in the future
37
+ // (scheduled). Missing publishedAt = immediate publish.
38
+ const published = items.find(
39
+ (i) => i.status === 'published' && (!i.publishedAt || i.publishedAt <= now)
40
+ )
29
41
  return published ?? null
30
42
  }
@@ -9,14 +9,26 @@ import { util } from '@aws-appsync/utils'
9
9
  // the tag partition condition. Drafts never appear here because the
10
10
  // admin client only writes PostTag rows for posts whose status is
11
11
  // 'published'.
12
+ //
13
+ // Scheduled-publish: the SK is a composite of `${publishedAt}#${postId}`.
14
+ // ISO 8601 timestamps are fixed-width to millisecond precision, so lexical
15
+ // order == chronological order. We add an upper-bound SK condition
16
+ // `<= now + '#' + '￿'` to exclude PostTag rows whose publishedAt is in
17
+ // the future. U+FFFF sorts above every character that can appear in a real
18
+ // postId, so `now#￿` is strictly greater than `now#<any-postId>`:
19
+ // every postId suffix sharing the current-millisecond ISO prefix is
20
+ // included, while any SK whose timestamp prefix is later is excluded.
21
+ // (Written as an escape so this source file stays ASCII.)
12
22
  export function request(ctx) {
13
23
  const { tag, limit, nextToken } = ctx.args
24
+ const now = util.time.nowISO8601()
25
+ const upper = now + '#￿'
14
26
  return {
15
27
  operation: 'Query',
16
28
  query: {
17
- expression: '#tag = :tag',
18
- expressionNames: { '#tag': 'tag' },
19
- expressionValues: util.dynamodb.toMapValues({ ':tag': tag }),
29
+ expression: '#tag = :tag AND #pap <= :upper',
30
+ expressionNames: { '#tag': 'tag', '#pap': 'publishedAtPostId' },
31
+ expressionValues: util.dynamodb.toMapValues({ ':tag': tag, ':upper': upper }),
20
32
  },
21
33
  scanIndexForward: false, // newest first (SK descends)
22
34
  limit: limit ?? 20,
@@ -1,4 +1,4 @@
1
- import { util } from '@aws-appsync/utils'
1
+ import { util, runtime } from '@aws-appsync/utils'
2
2
 
3
3
  // AppSync JS resolver: list published posts, newest first.
4
4
  //
@@ -8,29 +8,40 @@ import { util } from '@aws-appsync/utils'
8
8
  // so a single Query reads only the `published` partition. Drafts never
9
9
  // appear because the PK condition pins status='published'.
10
10
  //
11
+ // Scheduled-publish: the upper bound of the SK range is always clamped
12
+ // to `now`. This ensures future-dated published posts (scheduled but not
13
+ // yet live) never appear in public listings. The upper bound is the lesser
14
+ // of `now` and any caller-supplied `to` argument.
15
+ //
16
+ // Reverse-range guard: if `from` is supplied and ends up > upper (e.g.
17
+ // the caller asks for a range entirely in the future), we return an empty
18
+ // result early via `runtime.earlyReturn` instead of issuing an inverted
19
+ // BETWEEN to DynamoDB (which DynamoDB rejects with a ValidationException).
20
+ //
11
21
  // Date-range filtering (`from`, `to`) is pushed into the SK condition,
12
22
  // so DynamoDB only reads the matching range. `nextToken` paginates
13
23
  // without re-issuing a fresh query.
14
24
  export function request(ctx) {
15
25
  const { from, to, limit, nextToken } = ctx.args
26
+ const now = util.time.nowISO8601()
27
+ // Always clamp the upper bound to now so future-dated posts are excluded.
28
+ const upper = to && to < now ? to : now
29
+
30
+ // If `from` is later than `upper` the range is empty — skip the query
31
+ // rather than letting DynamoDB see an inverted BETWEEN / >= condition.
32
+ if (from && from > upper) {
33
+ return runtime.earlyReturn({ items: [], nextToken: null })
34
+ }
16
35
 
17
36
  let keyExpression = '#status = :status'
18
- const expressionNames = { '#status': 'status' }
19
- const expressionValueMap = { ':status': 'published' }
37
+ const expressionNames = { '#status': 'status', '#publishedAt': 'publishedAt' }
38
+ const expressionValueMap = { ':status': 'published', ':upper': upper }
20
39
 
21
- if (from && to) {
22
- keyExpression += ' AND #publishedAt BETWEEN :from AND :to'
23
- expressionNames['#publishedAt'] = 'publishedAt'
24
- expressionValueMap[':from'] = from
25
- expressionValueMap[':to'] = to
26
- } else if (from) {
27
- keyExpression += ' AND #publishedAt >= :from'
28
- expressionNames['#publishedAt'] = 'publishedAt'
40
+ if (from) {
41
+ keyExpression += ' AND #publishedAt BETWEEN :from AND :upper'
29
42
  expressionValueMap[':from'] = from
30
- } else if (to) {
31
- keyExpression += ' AND #publishedAt <= :to'
32
- expressionNames['#publishedAt'] = 'publishedAt'
33
- expressionValueMap[':to'] = to
43
+ } else {
44
+ keyExpression += ' AND #publishedAt <= :upper'
34
45
  }
35
46
 
36
47
  return {
@@ -441,7 +441,7 @@ runtime は `body` を `sanitize-html` の厳格 allowlist で sanitize し(
441
441
  - **`attrs` allowlist**: `data-*`、`crossorigin`、`referrerpolicy`、`integrity`、`fetchpriority`、`loading`、`sandbox`、`allow`、`allowfullscreen`。それ以外は dev warn 付きで drop
442
442
  - **`inlineScript.id` は必須**。無いとプラグイン同士が似たスニペットを emit したときに dedup できず、dev warning も index 番号を指すだけで原因プラグインを特定できません
443
443
  - **id 重複**: 最後の出現が勝ち。dev warning でどの key が重複したか表示されます
444
- - **CSP nonce**: Phase 1 では伝搬しません。`nonce` attr は型上は宣言してありますが今のところ無視されます
444
+ - **CSP nonce**: `inlineScript.nonce` と `script.nonce` は型として受け入れられます(`'auto'` は将来の runtime スタンプ用 sentinel。文字列リテラルも可)。Phase 1 予約: runtime はフィールドを受け入れますが描画要素には伝搬しません。詳細は下記の [CSP nonce(Phase 1 予約)](#csp-nonce-phase-1-) を参照。
445
445
  - **strategy**: `afterInteractive` は外部 script に `async` を付ける。`lazyOnload` は `defer`。明示的な `async` / `defer` が常に勝ち。`beforeInteractive` は非対応
446
446
 
447
447
  ### runtime が描画する形
@@ -565,9 +565,124 @@ React 19 はさらに、クライアントコンポーネントのレンダー
565
565
 
566
566
  ---
567
567
 
568
+ ## 6a. 予約投稿とコンテンツイベント
569
+
570
+ ampless は**予約投稿**をサポートしています。`status: 'published'` かつ `publishedAt` が未来の投稿は、その時刻まで公開読み出しから隠されます。`publishedAt` を過ぎると、サイトの自然なキャッシュ有効期限(デフォルト ≤ ~5 分)の範囲内で公開されます — 秒単位の正確なトリガーはありません。
571
+
572
+ ### イベントは保存時に発火する(`publishedAt` のタイミングではない)
573
+
574
+ `content.published`(および `content.updated`)は DynamoDB Streams 経由で**投稿が保存されたとき**に emit されます。`publishedAt` が到来したときではありません。つまり、`content.published` に反応するプラグインは、投稿が未来日時の場合、**その投稿が公開される前**に実行されます。
575
+
576
+ 現在の投稿一覧から公開アセットを再構築する trusted プラグイン(RSS、sitemap、JSON インデックスなど)にとってはこれは問題ありません — `listPublishedPosts()` が未来日時の投稿をすでに除外しているため、再生成されたアセットはその投稿が公開されるまでリストに含まれません。
577
+
578
+ 一方、**外部通知プラグイン**(webhook、プッシュ通知、SNS 投稿など)には影響があります。投稿の URL が 404 を返しているまたはホームページにリダイレクトされている間に通知が配信されてしまいます。
579
+
580
+ ### 推奨パターン: `publishedAt` でゲートする
581
+
582
+ ディスパッチの前に `event.payload.publishedAt` を確認します。投稿が未来日時の場合はスキップまたは保留します:
583
+
584
+ ```ts
585
+ hooks: {
586
+ async 'content.published'(event, ctx) {
587
+ const { publishedAt } = event.payload
588
+
589
+ // 予約投稿には通知を送らない。イベントは保存時に発火するが、
590
+ // 投稿が公開されるのは publishedAt になってから。
591
+ if (publishedAt && new Date(publishedAt) > new Date()) {
592
+ return
593
+ }
594
+
595
+ // 投稿は今すぐ公開状態 — 通知しても安全。
596
+ await sendWebhook(event.payload, ctx)
597
+ },
598
+ }
599
+ ```
600
+
601
+ イベントペイロード内の `publishedAt` は UTC ISO 8601 文字列(`...Z`)です。`Date.now()` と比較する前に `new Date()` またはお好みの日付ライブラリでパースしてください。
602
+
603
+ ### 今後の対応
604
+
605
+ `content.published` の発火を保存時ではなく `publishedAt` のタイミングに合わせる機能 — EventBridge Scheduler または DynamoDB TTL トリガー Lambda によるスケジューラーコンポーネントが必要 — は計画中の機能強化です。現リリースのスコープには含まれていません。それまでの間は上記のパターンが通知プラグインにおける推奨ガードです。
606
+
607
+ オペレーター視点からの `publishedAt` セマンティクスの全体像は [`docs/scheduled-publishing.md`](https://github.com/heavymoons/ampless/blob/main/docs/scheduled-publishing.ja.md) を参照してください。
608
+
609
+ ---
610
+
611
+ ### CSP nonce(Phase 1 予約)
612
+
613
+ CSP(Content Security Policy)は本番公開サイトのほぼ必須要件です。nonce 伝搬を後から追加した場合に既存プラグインが一斉に動かなくなるのを防ぐため、ampless は今のうちに API サーフェスだけ予約しておきます(Phase 1 は完全 no-op)。
614
+
615
+ 3 層設計:
616
+
617
+ 1. **`ctx.cspNonce?: string`** — `PluginPublicRenderContext` の型に予約済み。今のところ常に `undefined`。runtime はこのフィールドをまだ populate しません。middleware/SSR nonce threading は将来の CSP RFP とともに landing します。
618
+
619
+ 2. **`descriptor.nonce: 'auto' | string`** — `inlineScript` と `script` の両 descriptor variant の型で受け入れられます。`'auto'` は将来の runtime スタンプ用 sentinel。その他の文字列は明示的リテラル。`undefined` は `nonce` 属性を emit しない(デフォルト、非 CSP サイトと後方互換)。Phase 1: runtime は受け入れますが伝搬しません。`nonce: 'auto'` を今日宣言することは前方互換性のヒントであり、描画 HTML を変更しません。
620
+
621
+ 3. **`'cspReady'` capability** — name-only の declarative バッジ。将来の admin UI / サニティチェックの対象になります。Phase 1 では runtime の cross-check や enforcement は一切なし。
622
+
623
+ **対応方法:**
624
+
625
+ ```ts
626
+ // src/index.ts
627
+ definePlugin({
628
+ name: 'my-plugin',
629
+ apiVersion: 1,
630
+ trust_level: 'untrusted',
631
+ capabilities: ['publicHead', 'cspReady'],
632
+ publicHead: () => [{
633
+ type: 'inlineScript',
634
+ id: 'my-snippet',
635
+ body: '...',
636
+ nonce: 'auto', // 前方互換性ヒント。Phase 1 では効果なし
637
+ }],
638
+ })
639
+ ```
640
+
641
+ npm 公開のスタンドアロンプラグインの場合は、`package.json#amplessPlugin.capabilities` も合わせて更新してください — static manifest と factory return value が一致しない場合、runtime cross-check が警告を出します:
642
+
643
+ ```json
644
+ {
645
+ "amplessPlugin": {
646
+ "apiVersion": 1,
647
+ "name": "my-plugin",
648
+ "trustLevel": "untrusted",
649
+ "capabilities": ["publicHead", "cspReady"]
650
+ }
651
+ }
652
+ ```
653
+
654
+ **`'cspReady'` の意味:**
655
+
656
+ - サイト全体の CSP 適合は middleware / レスポンスヘッダー / runtime が制御しない他の inline コンテンツにも依存します。
657
+ - middleware-driven nonce threading PR が landing した後は、`nonce: 'auto'` を持つ plugin-supplied script が runtime nonce スタンプの候補になります。
658
+ - `'cspReady'` は `create-ampless plugin --capabilities` には表示されません — reserved capability であり、scaffold はアクティブな enforcement を示唆しないよう除外しています。
659
+
660
+ ---
661
+
568
662
  ## 7. 非同期イベントフック
569
663
 
570
- `hooks` は SQS から到着したイベントを trust_level に対応する processor Lambda が受けて実行します。runtime context (`ctx`) の中身:
664
+ `hooks` は SQS から到着したイベントを trust_level に対応する processor Lambda が受けて実行します。
665
+
666
+ ### 戻り値の予約
667
+
668
+ `PluginEventHandler` の戻り値型は `Promise<void | PluginHookResult>`。
669
+ runtime は現状この戻り値を完全に無視する — 既存 plugin が
670
+ `Promise<void>` を返す形は migration 不要で動き続ける。
671
+ `PluginHookResult` は将来の directive (最初の用例として有力:
672
+ `metrics?: Record<string, number>` による observability emission) のた
673
+ めの予約で、今宣言するのは forward-compatibility のヒントとして
674
+ の扱い。注意: rewrite / cancel 系 directive は本 widening だけで
675
+ は有効化されない — `before:*` event の plugin 配線と payload 拡張
676
+ が別 PR で必要。
677
+
678
+ `PluginHookResult` には private な `__amplessPluginHookResult`
679
+ marker が付いており、union が `Promise<string>` / `Promise<number>`
680
+ 等の無関係な promise を silently 受け入れないようになっている —
681
+ plugin 作者がこの marker を明示的に設定する必要は無い。
682
+
683
+ ### Runtime context
684
+
685
+ runtime context (`ctx`) の中身:
571
686
 
572
687
  ```ts
573
688
  interface PluginRuntimeContext {
@@ -706,14 +821,146 @@ stored 値 (validated)
706
821
 
707
822
  Secret settings を使うと、trusted プラグインが認証情報 (Webhook 署名 secret・SMTP パスワード・外部 API トークン等) を admin UI 経由で保存・ローテーションできます。**公開サイトやブラウザ側コードに値が流れることはありません**。
708
823
 
709
- 詳しいリファレンスは [`packages/ampless/docs/plugin-author-guide.md`](../../packages/ampless/docs/plugin-author-guide.md) §9a を参照してください。概要:
824
+ ### なぜ `settings.public` API が違うのか
825
+
826
+ `settings.public` の値は公開 runtime に流れる設計です。`public/site-settings.json` にミラーされ、`ctx.setting()` で sync render surface から読めます。analytics の measurementId などには適切ですが、Webhook 署名 secret には絶対に使えません。
827
+
828
+ `settings.secret` はストレージモデルが構造的に異なります:
829
+
830
+ - KvStore とは **別テーブル** の `PluginSecret` DynamoDB テーブルに保存。admin/editor Cognito ユーザーは AppSync 経由でこのテーブルに**直接アクセスできない** — すべての書き込みは `setPluginSecret` mutation を通じて `plugin-secret-handler` Lambda 経由で行われる。
831
+ - 値は保存前に **AES-256-GCM 暗号化**される。admin ブラウザが plaintext を Lambda に TLS 経由で送信 → Lambda がバリデーション + `process.env.PLUGIN_SECRET_ENCRYPTION_KEY`(CDK が `amplify/secrets/encryption-key.ts` から注入)から鍵を読み取り + 暗号化 → ciphertext のみを DDB に書き込む。平文は DynamoDB に保存されず、ブラウザにも返らない。
832
+ - **脅威モデル(Phase 6a v2.2)**:
833
+
834
+ | 脅威 | 状態 |
835
+ |---|---|
836
+ | PluginSecret テーブルを閲覧する AWS Console オペレータ | ✓ 対策済み — ciphertext のみ、DDB に鍵なし |
837
+ | ソースリポジトリ / デプロイアーティファクトへのアクセス | ⚠ 対策なし — 鍵は `amplify/secrets/encryption-key.ts` に存在。リポジトリを private にし、アーティファクトアクセスを制限すること |
838
+ | 同一 Lambda 内の悪意ある trusted plugin | ✗ 対策なし — `process.env.PLUGIN_SECRET_ENCRYPTION_KEY` はプラグインコードから読める。真の分離 = per-plugin Lambda(privileged tier, ロードマップ) |
839
+ | S3 mirror 漏洩 | ✓ 対策済み — PluginSecret テーブルは mirror されない |
840
+
841
+ - trusted-processor Lambda が `node:crypto` で復号する。`ctx.secret<T>(key)` は平文 string を返す(ciphertext ではない)。
842
+ - S3 mirror 経路に絶対に流れない(mirror は KvStore のみを query する)。
843
+ - 公開 render surface (`publicHead` など) からは読めない。
844
+ - **field manifest 検証の範囲**: admin クライアントは UX フィードバック目的で `pattern` / `maxLength` / `required` を検証するが、Lambda 側は **汎用の 10,000 文字ハードキャップと安全文字サニタイザのみ**を強制する。admin/editor が AppSync mutation を直接呼ぶと field 単位の制約は迂回できる。設計上意図したもので、admin/editor は secret 設定を許可された信頼されたオペレータと位置づけている。manifest チェックは UX ガイダンスであり、セキュリティ境界ではない。
845
+
846
+ ### 要件
847
+
848
+ `settings.secret` には 3 つの要件があります:
849
+
850
+ 1. `trust_level: 'trusted'` — untrusted Lambda には PluginSecret table への DDB read 権限がない。他の trust level で宣言すると `definePlugin()` 時に throw する。
851
+ 2. `'secretSettings'` を `capabilities` に含める — admin UI や将来の allow-list から capability を参照できるようにするため必須。省略すると console.warn(`'schema'` vs `publicBodyForPost` の不整合パターンと同じ)。
852
+ 3. **鍵の初回セットアップ** — プロジェクトルートで実行:
853
+ ```sh
854
+ npx create-ampless setup-encryption-key
855
+ ```
856
+ 32 バイトのランダムな鍵を生成し、`amplify/secrets/encryption-key.ts` に書き込む。AWS 認証情報不要 — ローカルファイル操作のみ。
857
+
858
+ 次に `amplify/backend.ts` でその定数を import し、`defineAmplessBackend({ pluginSecretEncryptionKey })` に渡す。その後デプロイ(またはサンドボックス再起動)して Lambda env var に注入する。
859
+
860
+ public リポジトリの場合は `--gitignore` を渡してバージョン管理から除外し、鍵を別途配布する。
861
+
862
+ ### Dual-write 整合性
863
+
864
+ `setPluginSecret` と `clearPluginSecret` は各操作で **2 テーブル**に連続して書き込む: `PluginSecret`(ciphertext)と `PluginSecretIndicator`(存在タイムスタンプ)。2 回目の書き込みが失敗した場合、テーブルは以下の予測可能な状態になる:
865
+
866
+ | 障害ポイント | `PluginSecret` | `PluginSecretIndicator` | `ctx.secret()` | `hasPluginSecret()` |
867
+ |---|---|---|---|---|
868
+ | **set**: indicator PutItem 失敗 | ciphertext 存在 | 不在 | plaintext を返す ✓ | `false`(UI: 「未保存」) |
869
+ | **clear**: indicator DeleteItem 失敗 | 不在 | stale(古いタイムスタンプ) | `undefined` ✓ | `true`(UI: 「保存済み」) |
870
+
871
+ clear パスの失敗は「安全側」: secret は発火しなくなるが、UI は一時的に「保存済み」と表示する。set パスの失敗は軽微な UI 不整合: secret は機能するが、存在インジケータはリトライが成功するまで不在となる。
872
+
873
+ ### secret フィールドの宣言
874
+
875
+ ```ts
876
+ import { definePlugin } from 'ampless'
877
+
878
+ export default function webhookPlugin(opts?: { signingSecret?: string }) {
879
+ // constructor から渡された secret は closure-private な fallback として保持。
880
+ // manifest にも descriptor にも出さない。
881
+ const constructorSecret = opts?.signingSecret
882
+
883
+ return definePlugin({
884
+ name: 'webhook',
885
+ apiVersion: 1,
886
+ trust_level: 'trusted',
887
+ capabilities: ['eventHooks', 'secretSettings'],
888
+ settings: {
889
+ secret: [
890
+ {
891
+ type: 'text',
892
+ key: 'signingSecret',
893
+ label: { en: 'Webhook signing secret', ja: 'Webhook 署名 secret' },
894
+ maxLength: 256,
895
+ required: false,
896
+ // `default` は型レベルで除外されている。closure-private fallback を使うこと。
897
+ },
898
+ ],
899
+ },
900
+ hooks: {
901
+ async 'content.published'(event, ctx) {
902
+ // ctx.secret() は PluginSecret DDB table から読む。
903
+ // admin が未保存なら undefined を返す。
904
+ const storedSecret = await ctx.secret<string>('signingSecret')
905
+
906
+ // closure-private fallback: admin が未保存の場合 constructor 引数を使う。
907
+ // これで既存サイトとの後方互換を維持できる。
908
+ const secret = storedSecret ?? constructorSecret
909
+ if (!secret) return
910
+
911
+ // ... secret で署名して POST
912
+ },
913
+ },
914
+ })
915
+ }
916
+ ```
917
+
918
+ ### 重要: secret フィールドに `default` を書かない
919
+
920
+ `PluginSecretField` 型は `Omit<PluginTextField, 'default'> | Omit<PluginTextareaField, 'default'>` として定義されており、**`default` プロパティは型レベルで除去**されています。追加しようとすると TypeScript がエラーを出します。
921
+
922
+ 理由: `default` は admin UI のフォーム props (ブラウザに送出される)、静的 manifest の cross-check、JS bundle など複数の経路で漏洩します。認証情報に使えない設計です。
923
+
924
+ fallback 値がある場合は、プラグイン factory 関数の closure-private 変数として保持してください:
925
+
926
+ ```ts
927
+ // ✓ 正解 — closure-private、manifest に出さない
928
+ const constructorSecret = opts?.signingSecret
929
+
930
+ // ✗ 誤り — TypeScript エラー、さらに browser にも漏れる
931
+ settings: {
932
+ secret: [{
933
+ type: 'text',
934
+ key: 'signingSecret',
935
+ label: 'Secret',
936
+ default: opts?.signingSecret, // ← TS compile error
937
+ }],
938
+ }
939
+ ```
940
+
941
+ ### secret の読み出し: `ctx.secret<T>(key)`
942
+
943
+ `ctx.secret<T>(key)` は trusted hook handler 内でのみ利用できます (`processor-trusted.ts` が注入)。シグネチャ:
944
+
945
+ ```ts
946
+ ctx.secret<T = string>(key: string): Promise<T | undefined>
947
+ ```
948
+
949
+ - admin が未保存なら `undefined` を返す。
950
+ - `T` は convenience cast (ctx.setting と同じ)。値は常に string として保存される。
951
+ - 結果は per-invocation キャッシュされる。同 batch 内で同キーを 2 回呼んでも DDB 呼び出しと復号処理は 1 回ずつ。暗号化キーは Lambda env var から cold-start 時に decode される(DDB への余分な fetch は不要)。
952
+ - キャッシュされる値は **復号済みの平文** — ciphertext ではない。2 回目の呼び出しで再復号は発生しない。
953
+ - cache key は namespace 化される: `${instanceId ?? name}:${fieldKey}`。異なる plugin instance が同名フィールドを持っても混線しない。
954
+
955
+ ### admin UI
956
+
957
+ `settings.secret` を宣言すると、admin plugin settings ページの public フィールドの下に **Secret settings** セクションが表示されます。各フィールド:
958
+
959
+ - **未保存**: 通常テキスト入力 + Save ボタン。
960
+ - **保存済み**: マスク表示 `••••••••` + Replace + Clear ボタン。値は絶対に取得・表示されない。
961
+ - **編集中**: Replace クリック後 — 新値入力 + Save + Cancel。
710
962
 
711
- - `settings.secret` `trust_level: 'trusted'` + `'secretSettings'` capability 必須。鍵の初回セットアップ: `npx create-ampless setup-encryption-key` を実行して `amplify/secrets/encryption-key.ts` を生成し、`defineAmplessBackend({ pluginSecretEncryptionKey })` に渡してデプロイ(AWS 認証情報不要)。
712
- - フィールド型は `PluginSecretField` = `Omit<PluginTextField, 'default'> | Omit<PluginTextareaField, 'default'>`。`default` は型レベルで禁止。fallback は closure-private 変数で保持。
713
- - admin ブラウザは AppSync mutation (`setPluginSecret`) 経由で書き込む → `plugin-secret-handler` Lambda が env var から鍵取得・AES-256-GCM 暗号化・DDB PutItem。平文は DDB に保存されずブラウザにも返らない。`ctx.secret<T>(key)` は trusted Lambda が復号した平文を返す(`process.env.PLUGIN_SECRET_ENCRYPTION_KEY` を使用; DDB に鍵は保存しない)。per-invocation でキャッシュ。
714
- - admin UI が「保存済み」表示 (`••••••••`) + Replace + Clear を提供。値は取得・表示されない。
715
- - **Dual-write 整合性**: set/clear は 2 テーブルに連続して書く。2 回目失敗時 — set パス部分失敗は「secret 機能する・indicator 不在(UI: 未保存誤表示)」; clear パス部分失敗は「secret 削除済・indicator stale(UI: 保存済み誤表示、secret は発火しない安全側)」。
716
- - **field manifest 検証の範囲**: admin クライアントは UX フィードバック用に `pattern` / `maxLength` / `required` を検証するが、Lambda は **汎用 10,000 文字キャップと安全文字サニタイザのみ**を強制。admin/editor が AppSync mutation を直接呼ぶと field 単位の制約は迂回可能 — admin/editor は信頼されたオペレータと扱う設計のため、manifest チェックはセキュリティ境界ではなく UX ガイダンス。
963
+ admin は再デプロイなしにいつでも secret をローテーションできます。保存後 ~5〜10 秒以内に次の trusted Lambda 実行から新値が使われます。
717
964
 
718
965
  ---
719
966
 
@@ -540,8 +540,12 @@ a console warning** and never rendered.
540
540
  index numbers no one can map back to a plugin.
541
541
  - **Duplicate `id`**: the last occurrence wins. A dev warning prints
542
542
  which key was duplicated.
543
- - **CSP nonce**: not propagated in Phase 1. The `nonce` attr is
544
- declared in the type for forward-compat but discarded today.
543
+ - **CSP nonce**: `inlineScript.nonce` and `script.nonce` are accepted
544
+ by the type (`'auto'` is the sentinel for future runtime stamping;
545
+ any string is an explicit literal). Phase 1 reservation: the runtime
546
+ accepts the field but does not propagate it to the rendered element.
547
+ See [CSP nonce (Phase 1 reservation)](#csp-nonce-phase-1-reservation)
548
+ below.
545
549
  - **Strategy**: `afterInteractive` adds `async` to external scripts.
546
550
  `lazyOnload` adds `defer`. Explicit `async` / `defer` always
547
551
  wins. `beforeInteractive` is not supported.
@@ -717,10 +721,167 @@ nothing races against hydration.
717
721
 
718
722
  ---
719
723
 
724
+ ## 6a. Scheduled posts and content events
725
+
726
+ ampless supports **scheduled publishing**: a post with `status:
727
+ 'published'` and a future `publishedAt` is hidden from all public
728
+ reads until that time. Once `publishedAt` arrives, the post becomes
729
+ visible within the site's natural cache window (≤ ~5 minutes by
730
+ default) — there is no exact-time trigger.
731
+
732
+ ### Events fire at save time, not at `publishedAt`
733
+
734
+ `content.published` (and `content.updated`) are emitted via DynamoDB
735
+ Streams **when the post is saved**, not when `publishedAt` arrives.
736
+ This means a plugin that reacts to `content.published` will run
737
+ **before the post is publicly visible** when the post is future-dated.
738
+
739
+ For trusted plugins that rebuild public assets from the current post
740
+ list (RSS, sitemap, JSON indexes), this is harmless — `listPublishedPosts()`
741
+ already filters out future-dated posts, so the regenerated asset
742
+ simply omits the scheduled post until it goes live.
743
+
744
+ For **outbound-notification plugins** (webhook, push notification,
745
+ social post) the early fire matters: the notification will be
746
+ delivered to subscribers while the post URL still returns 404 or
747
+ redirects to the home page.
748
+
749
+ ### Recommended pattern: gate on `publishedAt`
750
+
751
+ Check `event.payload.publishedAt` before dispatching. Skip or defer
752
+ when the post is future-dated:
753
+
754
+ ```ts
755
+ hooks: {
756
+ async 'content.published'(event, ctx) {
757
+ const { publishedAt } = event.payload
758
+
759
+ // Skip notification for future-scheduled posts. The event fires
760
+ // at save time, but the post won't be public until publishedAt.
761
+ if (publishedAt && new Date(publishedAt) > new Date()) {
762
+ return
763
+ }
764
+
765
+ // Post is live now — safe to notify.
766
+ await sendWebhook(event.payload, ctx)
767
+ },
768
+ }
769
+ ```
770
+
771
+ The `publishedAt` value in the event payload is a UTC ISO 8601 string
772
+ (`...Z`). Parse it with `new Date()` or your preferred date library
773
+ before comparing against `Date.now()`.
774
+
775
+ ### Future work
776
+
777
+ Aligning event emission with the scheduled time — so `content.published`
778
+ fires at `publishedAt` rather than at save time — is a planned
779
+ enhancement. It requires a scheduler component (EventBridge Scheduler
780
+ or a DynamoDB TTL-triggered Lambda) and is not yet in scope for the
781
+ current release. Until then, the pattern above is the recommended
782
+ guard for notification plugins.
783
+
784
+ For a full description of `publishedAt` semantics from the operator's
785
+ perspective, see [`docs/scheduled-publishing.md`](https://github.com/heavymoons/ampless/blob/main/docs/scheduled-publishing.md).
786
+
787
+ ---
788
+
789
+ ### CSP nonce (Phase 1 reservation)
790
+
791
+ Content Security Policy (CSP) is a near-mandatory requirement for production
792
+ sites. To avoid breaking every plugin at once when nonce propagation lands,
793
+ ampless reserves the API surface today (Phase 1 no-op) so plugins can opt in
794
+ ahead of time.
795
+
796
+ The 3-layer design:
797
+
798
+ 1. **`ctx.cspNonce?: string`** on `PluginPublicRenderContext` — type-reserved
799
+ on the interface; always `undefined` today. The runtime does not populate
800
+ this field yet; reads resolve to `undefined`. Middleware/SSR nonce threading
801
+ lands with the future CSP RFP.
802
+
803
+ 2. **`descriptor.nonce: 'auto' | string`** — accepted by the type on both
804
+ `inlineScript` and `script` descriptor variants. `'auto'` is the sentinel
805
+ for future runtime stamping; any other string is an explicit literal;
806
+ `undefined` emits no `nonce` attribute (default, backward-compatible).
807
+ Phase 1: the runtime accepts but does not propagate it. Declaring
808
+ `nonce: 'auto'` today is a forward-compatibility hint and does not change
809
+ the rendered HTML.
810
+
811
+ 3. **`'cspReady'` capability** — a name-only declarative badge. Declaring it
812
+ signals intent; future admin UI / sanity checks may surface it. No runtime
813
+ cross-check or enforcement exists in Phase 1.
814
+
815
+ **How to be ready:**
816
+
817
+ ```ts
818
+ // src/index.ts
819
+ definePlugin({
820
+ name: 'my-plugin',
821
+ apiVersion: 1,
822
+ trust_level: 'untrusted',
823
+ capabilities: ['publicHead', 'cspReady'],
824
+ publicHead: () => [{
825
+ type: 'inlineScript',
826
+ id: 'my-snippet',
827
+ body: '...',
828
+ nonce: 'auto', // forward-compat hint; no effect in Phase 1
829
+ }],
830
+ })
831
+ ```
832
+
833
+ For standalone npm-published plugins, also update `package.json#amplessPlugin.capabilities`
834
+ to match — the runtime cross-check warns on disagreement between the static
835
+ manifest and the factory return value:
836
+
837
+ ```json
838
+ {
839
+ "amplessPlugin": {
840
+ "apiVersion": 1,
841
+ "name": "my-plugin",
842
+ "trustLevel": "untrusted",
843
+ "capabilities": ["publicHead", "cspReady"]
844
+ }
845
+ }
846
+ ```
847
+
848
+ **What "cspReady" means:**
849
+
850
+ - Site-level CSP compliance depends on middleware / response headers / other
851
+ inline content the runtime does not control.
852
+ - Once the middleware-driven nonce threading PR lands, plugin-supplied scripts
853
+ that carry `nonce: 'auto'` will become candidates for runtime nonce stamping.
854
+ - `'cspReady'` does **not** appear in `create-ampless plugin --capabilities`
855
+ output — it is a reserved capability and the scaffold excludes it to avoid
856
+ implying active enforcement.
857
+
858
+ ---
859
+
720
860
  ## 7. Async event hooks
721
861
 
722
862
  `hooks` runs inside the trust_level-matched processor Lambda when an
723
- event arrives via SQS. The runtime context (`ctx`) carries:
863
+ event arrives via SQS.
864
+
865
+ ### Return value reservation
866
+
867
+ `PluginEventHandler` returns `Promise<void | PluginHookResult>`. The
868
+ runtime currently ignores the return value entirely — existing
869
+ plugins returning `Promise<void>` keep working without migration.
870
+ `PluginHookResult` is reserved for a future directive (likely first:
871
+ `metrics?: Record<string, number>` for observability emission);
872
+ declaring it today is a forward-compatibility hint. Note: rewrite
873
+ or cancel directives are NOT enabled by this widening alone — they
874
+ require additional `before:*` event support and payload extensions
875
+ in separate PRs.
876
+
877
+ `PluginHookResult` carries a private `__amplessPluginHookResult`
878
+ marker so that the union does not silently accept unrelated promise
879
+ types (`Promise<string>` / `Promise<number>` etc.) — plugin authors
880
+ do not need to set this field.
881
+
882
+ ### Runtime context
883
+
884
+ The runtime context (`ctx`) carries:
724
885
 
725
886
  ```ts
726
887
  interface PluginRuntimeContext {
@@ -924,14 +1085,15 @@ etc. — but completely wrong for a webhook signing secret.
924
1085
 
925
1086
  - Stored in the `PluginSecret` DynamoDB table (separate from KvStore).
926
1087
  Admin/editor Cognito users have **no direct AppSync access** to this
927
- table — all writes go through the `setPluginSecret` mutation backed
928
- by the `plugin-secret-handler` Lambda.
1088
+ table — all writes go through the `setPluginSecret` mutation, which
1089
+ is backed by the `plugin-secret-handler` Lambda.
929
1090
  - Values are **AES-256-GCM encrypted** before reaching DynamoDB.
930
1091
  The admin browser sends the plaintext to the Lambda via AppSync
931
- (TLS); the Lambda validates, reads the 32-byte encryption key from
932
- `process.env.PLUGIN_SECRET_ENCRYPTION_KEY` (injected by CDK from
933
- `amplify/secrets/encryption-key.ts`), and writes only the ciphertext.
934
- The plaintext never rests in DynamoDB and never flows back to the browser.
1092
+ (TLS in transit); the Lambda validates it, reads the 32-byte
1093
+ encryption key from `process.env.PLUGIN_SECRET_ENCRYPTION_KEY`
1094
+ (injected by CDK from `amplify/secrets/encryption-key.ts`), encrypts
1095
+ the value, and writes only the ciphertext to DDB. The plaintext
1096
+ never rests in DynamoDB and never flows back to the browser.
935
1097
  - **Threat model (Phase 6a v2.2)**:
936
1098
 
937
1099
  | Threat | Status |
@@ -942,19 +1104,19 @@ etc. — but completely wrong for a webhook signing secret.
942
1104
  | S3 mirror leak | ✓ defeated — PluginSecret table never mirrored. |
943
1105
 
944
1106
  - The trusted-processor Lambda decrypts with `node:crypto` on read.
945
- `ctx.secret<T>(key)` returns the plaintext string, never ciphertext.
1107
+ `ctx.secret<T>(key)` returns the plaintext string, never the
1108
+ ciphertext.
946
1109
  - Never queried by the site-settings mirror path.
947
1110
  - Never passed to any public-render surface
948
- (`publicHead`, `publicBodyEnd`, `publicBodyForPost`,
949
- `publicHtmlForPost`) — those surfaces only see `ctx.setting()`.
950
1111
  - **Field-manifest validation scope**: the admin client validates
951
1112
  `pattern` / `maxLength` / `required` for UX feedback, but the
952
- Lambda only enforces a generic 10,000-character hard cap +
953
- safe-character sanitizer. An admin/editor calling the AppSync
954
- mutation directly can bypass field-level constraints — by design,
955
- since admin/editor are trusted operators authorised to set
956
- secrets. The manifest checks are UX guidance, not a security
957
- boundary.
1113
+ Lambda only enforces a generic 10,000-character hard cap + safe-
1114
+ character sanitizer. An admin/editor calling the AppSync mutation
1115
+ directly can bypass field-level constraints — by design, since
1116
+ admin/editor are trusted operators authorised to set secrets.
1117
+ The manifest checks are UX guidance, not a security boundary.
1118
+ (`publicHead`, `publicBodyEnd`, `publicBodyForPost`,
1119
+ `publicHtmlForPost`) — those surfaces only see `ctx.setting()`.
958
1120
 
959
1121
  ### Requirements
960
1122
 
@@ -970,25 +1132,33 @@ etc. — but completely wrong for a webhook signing secret.
970
1132
  ```sh
971
1133
  npx create-ampless setup-encryption-key
972
1134
  ```
973
- Generates a cryptographically random 32-byte key and writes it to
974
- `amplify/secrets/encryption-key.ts`. No AWS credentials required.
975
- Then import it in `amplify/backend.ts` and pass it to
1135
+ This generates a cryptographically random 32-byte key and writes
1136
+ it to `amplify/secrets/encryption-key.ts`. No AWS credentials
1137
+ required this is a local file operation only.
1138
+
1139
+ Then import the constant in `amplify/backend.ts` and pass it to
976
1140
  `defineAmplessBackend({ pluginSecretEncryptionKey })`. Redeploy (or
977
- restart the sandbox) to inject the key into Lambda env vars.
978
- For public repos, add `--gitignore` to exclude from version control.
1141
+ restart the sandbox) to inject the key into the Lambda env vars.
1142
+
1143
+ For public repos, pass `--gitignore` to exclude the key file from
1144
+ version control and distribute the key separately.
979
1145
 
980
1146
  ### Dual-write integrity
981
1147
 
982
- The `setPluginSecret` and `clearPluginSecret` operations each write to
983
- **two DynamoDB tables** in sequence. If the second write fails, the
984
- tables are left in a predictable state:
1148
+ The `setPluginSecret` and `clearPluginSecret` operations each write
1149
+ to **two DynamoDB tables** in sequence: `PluginSecret` (ciphertext)
1150
+ and `PluginSecretIndicator` (existence timestamp). If the second
1151
+ write fails, the tables are left in a predictable, documented state:
985
1152
 
986
- | Failure | `PluginSecret` | `PluginSecretIndicator` | `ctx.secret()` | `hasPluginSecret()` |
1153
+ | Failure point | `PluginSecret` | `PluginSecretIndicator` | `ctx.secret()` | `hasPluginSecret()` |
987
1154
  |---|---|---|---|---|
988
1155
  | **set**: indicator PutItem fails | ciphertext present | absent | returns plaintext ✓ | `false` (UI: "not saved") |
989
- | **clear**: indicator DeleteItem fails | absent | stale | `undefined` ✓ | `true` (UI: "saved") |
1156
+ | **clear**: indicator DeleteItem fails | absent | stale (old timestamp) | `undefined` ✓ | `true` (UI: "saved") |
990
1157
 
991
- The clear-path failure is the "safe side": the secret stops firing.
1158
+ The clear-path failure is the "safe side": the secret stops firing even
1159
+ though the UI briefly shows it as saved. The set-path failure is a minor
1160
+ UI inaccuracy: the secret is functional but the existence indicator is
1161
+ absent until a retry succeeds.
992
1162
 
993
1163
  ### Declaring secret fields
994
1164
 
@@ -1020,9 +1190,16 @@ export default function webhookPlugin(opts?: { signingSecret?: string }) {
1020
1190
  },
1021
1191
  hooks: {
1022
1192
  async 'content.published'(event, ctx) {
1193
+ // ctx.secret() reads from PluginSecret DDB table.
1194
+ // Returns undefined when no value has been saved by admin yet.
1023
1195
  const storedSecret = await ctx.secret<string>('signingSecret')
1196
+
1197
+ // Closure-private fallback: use the constructor argument when
1198
+ // the admin has not saved a value yet. This preserves backward
1199
+ // compatibility with sites that pass the secret at install time.
1024
1200
  const secret = storedSecret ?? constructorSecret
1025
- if (!secret) return
1201
+ if (!secret) return // no secret → skip signing
1202
+
1026
1203
  // ... use secret to sign and POST
1027
1204
  },
1028
1205
  },
@@ -1034,25 +1211,104 @@ export default function webhookPlugin(opts?: { signingSecret?: string }) {
1034
1211
 
1035
1212
  The type `PluginSecretField` is defined as `Omit<PluginTextField,
1036
1213
  'default'> | Omit<PluginTextareaField, 'default'>` — the `default`
1037
- property is **removed at the type level**. Use a closure-private
1038
- fallback variable instead; never put credentials in the manifest.
1214
+ property is **removed at the type level** and TypeScript will error
1215
+ if you try to add one.
1216
+
1217
+ This is intentional: `default` values propagate into admin UI form
1218
+ props (visible in the browser), static manifests cross-checked by
1219
+ the runtime, and JS bundles. For a credential, these are all leak
1220
+ paths.
1221
+
1222
+ If you have a fallback value (e.g. the constructor argument), keep
1223
+ it as a **closure-private variable** inside the plugin factory
1224
+ function — never in the manifest:
1225
+
1226
+ ```ts
1227
+ // ✓ correct — closure-private, never in the manifest
1228
+ const constructorSecret = opts?.signingSecret
1229
+
1230
+ // ✗ wrong — TypeScript error, would also leak to browser
1231
+ settings: {
1232
+ secret: [{
1233
+ type: 'text',
1234
+ key: 'signingSecret',
1235
+ label: 'Secret',
1236
+ default: opts?.signingSecret, // ← TS compile error
1237
+ }],
1238
+ }
1239
+ ```
1039
1240
 
1040
1241
  ### Reading secrets: `ctx.secret<T>(key)`
1041
1242
 
1042
- `ctx.secret<T>(key)` is only available in trusted hook handlers.
1043
- Returns `undefined` when no value has been saved by admin. Returns
1044
- the **decrypted plaintext** — never the ciphertext blob.
1045
- Results are per-invocation cached (plaintext, not ciphertext).
1046
- The encryption key is decoded from the Lambda env var at cold-start
1047
- (no extra DDB fetch).
1048
- Cache keys are namespaced by `${instanceId ?? name}:${fieldKey}`.
1243
+ `ctx.secret<T>(key)` is only available in trusted hook handlers
1244
+ (injected by `processor-trusted.ts`). The signature is:
1245
+
1246
+ ```ts
1247
+ ctx.secret<T = string>(key: string): Promise<T | undefined>
1248
+ ```
1249
+
1250
+ - Returns `undefined` when no value has been saved by admin yet.
1251
+ - The generic `T` is a convenience cast (same as `ctx.setting<T>()`).
1252
+ Values are always stored as strings; `T` defaults to `string`.
1253
+ - Results are per-invocation cached. Calling `ctx.secret('key')`
1254
+ twice in the same hook batch costs one DDB round-trip (and one
1255
+ decrypt). The encryption key is decoded from the Lambda env var
1256
+ at cold-start (no extra DDB fetch).
1257
+ - Cache keys are namespaced: `${instanceId ?? name}:${fieldKey}`.
1258
+ Two plugin instances both declaring `'signingSecret'` never get
1259
+ each other's values.
1260
+ - The cached value is the **decrypted plaintext** — not the
1261
+ ciphertext. There is no redundant decrypt on repeated calls.
1049
1262
 
1050
1263
  ### Admin UI
1051
1264
 
1052
1265
  When `settings.secret` is declared, the admin plugin settings page
1053
1266
  renders a **Secret settings** section below the public fields.
1054
- Admins see `••••••••` for stored values and can Replace or Clear
1055
- without deploying. Rotation takes effect within ~5–10 seconds.
1267
+ Each secret field shows:
1268
+
1269
+ - **Unset**: a plain text input + Save button.
1270
+ - **Stored**: a masked placeholder `••••••••` + Replace + Clear
1271
+ buttons. The actual value is never fetched or displayed.
1272
+ - **Editing**: after clicking Replace — new text input + Save + Cancel.
1273
+
1274
+ Admins can rotate a secret at any time without redeploying. The
1275
+ change takes effect on the next trusted-Lambda invocation (within
1276
+ ~5–10 seconds of saving).
1277
+
1278
+ ### Testing hooks that use `ctx.secret`
1279
+
1280
+ Mock `ctx.secret` alongside the other context methods:
1281
+
1282
+ ```ts
1283
+ import { describe, it, expect, vi } from 'vitest'
1284
+ import webhookPlugin from './index.js'
1285
+ import type { TrustedPluginRuntimeContext } from 'ampless'
1286
+
1287
+ function makeCtx(secrets: Record<string, string> = {}): TrustedPluginRuntimeContext {
1288
+ return {
1289
+ site: { name: 'Test', url: 'https://example.com' },
1290
+ listPublishedPosts: vi.fn().mockResolvedValue([]),
1291
+ writePublicAsset: vi.fn().mockResolvedValue(''),
1292
+ secret: vi.fn().mockImplementation(async (key: string) => secrets[key]),
1293
+ }
1294
+ }
1295
+
1296
+ describe('webhookPlugin signing', () => {
1297
+ it('uses admin-stored secret when available', async () => {
1298
+ const plugin = webhookPlugin()
1299
+ const ctx = makeCtx({ signingSecret: 'stored-secret' })
1300
+ await plugin.hooks?.['content.published']?.({ type: 'content.published', payload: {} as never }, ctx)
1301
+ // assert that the request was signed with 'stored-secret'
1302
+ })
1303
+
1304
+ it('falls back to constructor secret when admin has not saved one', async () => {
1305
+ const plugin = webhookPlugin({ signingSecret: 'fallback-secret' })
1306
+ const ctx = makeCtx({}) // no stored secret
1307
+ await plugin.hooks?.['content.published']?.({ type: 'content.published', payload: {} as never }, ctx)
1308
+ // assert that the request was signed with 'fallback-secret'
1309
+ })
1310
+ })
1311
+ ```
1056
1312
 
1057
1313
  ---
1058
1314
 
@@ -25,21 +25,21 @@
25
25
  "@tiptap/pm": "^3.23.6",
26
26
  "@tiptap/react": "^3.23.6",
27
27
  "@tiptap/starter-kit": "^3.23.6",
28
- "@ampless/plugin-analytics-ga4": "^0.2.0-alpha.17",
29
- "@ampless/plugin-cookie-consent": "^0.1.0-alpha.8",
30
- "@ampless/plugin-gtm": "^0.2.0-alpha.16",
31
- "@ampless/plugin-og-image": "^0.2.0-alpha.33",
32
- "@ampless/plugin-plausible": "^0.2.0-alpha.16",
33
- "@ampless/plugin-reading-time": "^0.1.0-alpha.6",
34
- "@ampless/plugin-rss": "^0.2.0-alpha.33",
35
- "@ampless/plugin-schema-jsonld": "^0.1.1-alpha.12",
36
- "@ampless/plugin-seo": "^0.2.0-alpha.33",
37
- "@ampless/plugin-webhook": "^0.2.0-alpha.34",
38
- "@ampless/admin": "^1.0.0-alpha.66",
39
- "@ampless/backend": "^1.0.0-alpha.54",
40
- "@ampless/runtime": "^1.0.0-alpha.42",
28
+ "@ampless/plugin-analytics-ga4": "^0.2.0-alpha.18",
29
+ "@ampless/plugin-cookie-consent": "^0.1.0-alpha.9",
30
+ "@ampless/plugin-gtm": "^0.2.0-alpha.17",
31
+ "@ampless/plugin-og-image": "^0.2.0-alpha.34",
32
+ "@ampless/plugin-plausible": "^0.2.0-alpha.17",
33
+ "@ampless/plugin-reading-time": "^0.1.0-alpha.7",
34
+ "@ampless/plugin-rss": "^0.2.0-alpha.34",
35
+ "@ampless/plugin-schema-jsonld": "^0.1.1-alpha.13",
36
+ "@ampless/plugin-seo": "^0.2.0-alpha.34",
37
+ "@ampless/plugin-webhook": "^0.2.0-alpha.35",
38
+ "@ampless/admin": "^1.0.0-alpha.67",
39
+ "@ampless/backend": "^1.0.0-alpha.56",
40
+ "@ampless/runtime": "^1.0.0-alpha.43",
41
41
  "@digital-go-jp/tailwind-theme-plugin": "^0.3.4",
42
- "ampless": "^1.0.0-alpha.33",
42
+ "ampless": "^1.0.0-alpha.34",
43
43
  "aws-amplify": "^6.17.0",
44
44
  "class-variance-authority": "^0.7.1",
45
45
  "clsx": "^2.1.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ampless",
3
- "version": "1.0.0-alpha.108",
3
+ "version": "1.0.0-alpha.111",
4
4
  "description": "Create a new ampless project",
5
5
  "license": "MIT",
6
6
  "type": "module",