create-ampless 0.2.0-alpha.3 → 0.2.0-alpha.30
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/README.ja.md +77 -0
- package/README.md +40 -1
- package/dist/index.js +2171 -47
- package/dist/templates/_shared/AGENTS.ja.md +104 -0
- package/dist/templates/_shared/AGENTS.md +104 -0
- package/dist/templates/_shared/README.ja.md +181 -0
- package/dist/templates/_shared/README.md +181 -0
- package/dist/templates/_shared/RUNBOOK.ja.md +150 -0
- package/dist/templates/_shared/RUNBOOK.md +19 -1
- package/dist/templates/_shared/amplify/auth/post-confirmation/resource.ts +6 -0
- package/dist/templates/_shared/amplify/backend.custom.ts +41 -0
- package/dist/templates/_shared/amplify/backend.ts +12 -2
- package/dist/templates/_shared/amplify/data/resource.custom.ts +33 -0
- package/dist/templates/_shared/amplify/data/resource.ts +43 -18
- package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +4 -0
- package/dist/templates/_shared/amplify/functions/mcp-handler/handler.ts +1 -0
- package/dist/templates/_shared/amplify/functions/mcp-handler/resource.ts +11 -0
- package/dist/templates/_shared/amplify/functions/user-admin/handler.ts +4 -0
- package/dist/templates/_shared/amplify/functions/user-admin/resource.ts +12 -0
- package/dist/templates/_shared/amplify.yml +18 -0
- package/dist/templates/_shared/app/(admin)/admin/mcp-tokens/page.tsx +5 -0
- package/dist/templates/_shared/app/(admin)/admin/users/page.tsx +5 -0
- package/dist/templates/_shared/app/globals.css +55 -39
- package/dist/templates/_shared/app/layout.tsx +22 -1
- package/dist/templates/_shared/app/site/[siteId]/[...path]/route.ts +17 -0
- package/dist/templates/_shared/lib/admin.ts +9 -7
- package/dist/templates/_shared/lib/amplify.ts +17 -6
- package/dist/templates/_shared/package.json +11 -9
- package/dist/templates/blog/README.ja.md +22 -0
- package/dist/templates/blog/README.md +17 -47
- package/dist/templates/blog/tokens.css +26 -40
- package/dist/templates/corporate/README.ja.md +18 -0
- package/dist/templates/corporate/README.md +12 -14
- package/dist/templates/corporate/tokens.css +17 -39
- package/dist/templates/dads/README.ja.md +31 -0
- package/dist/templates/dads/README.md +13 -17
- package/dist/templates/dads/tokens.css +22 -42
- package/dist/templates/docs/README.ja.md +24 -0
- package/dist/templates/docs/README.md +10 -13
- package/dist/templates/docs/tokens.css +17 -39
- package/dist/templates/landing/README.ja.md +20 -0
- package/dist/templates/landing/README.md +14 -19
- package/dist/templates/landing/tokens.css +17 -39
- package/dist/templates/minimal/README.ja.md +14 -0
- package/dist/templates/minimal/README.md +9 -47
- package/dist/templates/minimal/tokens.css +17 -39
- package/package.json +2 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
> English: [RUNBOOK.md](./RUNBOOK.md)
|
|
2
|
+
>
|
|
3
|
+
|
|
4
|
+
# ランブック
|
|
5
|
+
|
|
6
|
+
ampless で構築したサイトで、ときどき必要になる運用作業の手順集です。毎日やるわけではないけれど、いざ必要になったときに迷わずに進めるためのものです。
|
|
7
|
+
|
|
8
|
+
日常的な使い方(コマンド、管理画面、テーマ、プラグイン、デプロイなど)は [README.ja.md](./README.ja.md) を参照してください。
|
|
9
|
+
|
|
10
|
+
## 目次
|
|
11
|
+
|
|
12
|
+
- [AppSync API キー(自動更新)](#appsync-api-キー自動更新)
|
|
13
|
+
- [よくある運用](#よくある運用)
|
|
14
|
+
- [ユーザーの昇格 / 降格](#promote--demote-a-user)
|
|
15
|
+
- [パスワードリセット(管理者上書き)](#reset-a-users-password-admin-override)
|
|
16
|
+
- [Post テーブルのバックアップから復元](#restore-from-a-post-table-backup)
|
|
17
|
+
- [失敗したプラグインイベントを確認する](#inspect-failed-plugin-events)
|
|
18
|
+
- [マルチサイト / カスタムドメイン](#multi-site--custom-domains)
|
|
19
|
+
- [シングルドメイン運用](#single-domain-operation)
|
|
20
|
+
- [マルチサイト時の注意:SSR キャッシュは強制無効化](#multi-site-mode-caveat-ssr-caching-is-force-disabled)
|
|
21
|
+
- [カスタムドメインを Amplify Hosting に追加する](#adding-a-custom-domain-to-amplify-hosting)
|
|
22
|
+
|
|
23
|
+
## AppSync API キー(自動更新)
|
|
24
|
+
|
|
25
|
+
パブリックブログの読み取り(`listPublishedPosts`、`getPublishedPost`、`listPostsByTag`)は AppSync API キーで保護されています。このキーは `amplify_outputs.json` に含まれているため、**公開サイトを訪問した誰もが確認できる**状態にあります。低信頼度のクレデンシャルとして扱ってください。このキーが持つ権限は上記 3 つのカスタムクエリを呼び出すことのみであり、それらのクエリは `status === 'published'` の行しか返しません。
|
|
26
|
+
|
|
27
|
+
### なぜ API キーなのか(Identity Pool ゲストロールではなく)?
|
|
28
|
+
|
|
29
|
+
Amplify Gen 2 の `a.handler.custom` リゾルバーは `allow.guest()` や `allow.authenticated('identityPool')` をサポートしておらず、apiKey / userPool / lambda / group / owner のみが使用可能です。v0.1 では簡潔さを優先して API キーを選択しました。パブリック読み取りを Lambda 関数データソース(`a.handler.function`)に移行することは v0.2 の候補です。
|
|
30
|
+
|
|
31
|
+
### 自動更新 — ローテーション手順は不要
|
|
32
|
+
|
|
33
|
+
`api-key-renewer` Lambda(`amplify/functions/api-key-renewer/` を参照)は、毎月 1 日の UTC 03:00 に EventBridge スケジュールで起動します。`AppSync.UpdateApiKey` を呼び出して既存キーの `expires` を「現時点 + 364 日」に延長するため、以下が保証されます:
|
|
34
|
+
|
|
35
|
+
- キー ID は変化しない
|
|
36
|
+
- `amplify_outputs.json` は引き続き有効
|
|
37
|
+
- Next.js アプリの再ビルドは不要
|
|
38
|
+
- 常に約 334 日以上の残有効期限が維持される
|
|
39
|
+
|
|
40
|
+
手動で確認または実行したい場合:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# 現在の有効期限を確認
|
|
44
|
+
aws appsync list-api-keys \
|
|
45
|
+
--region <amplify_outputs.json の data.aws_region> \
|
|
46
|
+
--api-id <amplify_outputs.json の data.url から導出した api-id>
|
|
47
|
+
|
|
48
|
+
# 手動実行(サンドボックスを長期間停止した後など)
|
|
49
|
+
aws lambda invoke \
|
|
50
|
+
--function-name $(aws lambda list-functions \
|
|
51
|
+
--query "Functions[?contains(FunctionName,'api-key-renewer')].FunctionName | [0]" \
|
|
52
|
+
--output text) \
|
|
53
|
+
/tmp/out.json && cat /tmp/out.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### キーの漏洩が疑われる場合
|
|
57
|
+
|
|
58
|
+
有効期限の延長ではなく、キーの値そのものをローテーションする即時対応を行います:
|
|
59
|
+
|
|
60
|
+
1. `amplify/data/resource.ts` のコメントを編集して CFN 更新を強制する
|
|
61
|
+
2. `npx ampx sandbox`(サンドボックス)または `npx ampx pipeline-deploy ...`(本番)を実行 — Amplify がキーの値を再生成する
|
|
62
|
+
3. Next.js アプリを再デプロイして SSR が新しい `data.api_key` を参照するようにする
|
|
63
|
+
|
|
64
|
+
## 一般的な操作
|
|
65
|
+
|
|
66
|
+
### ユーザーのグループ昇格 / 降格
|
|
67
|
+
|
|
68
|
+
AWS Cognito コンソールから操作します:
|
|
69
|
+
|
|
70
|
+
1. User Pool → Users → 対象ユーザーを選択
|
|
71
|
+
2. Group memberships → グループに追加 / グループから削除
|
|
72
|
+
3. ユーザーに一度サインアウトしてサインインし直してもらい、新しいクレームを適用させる
|
|
73
|
+
|
|
74
|
+
グループの種類: `ampless-admin`(フル CRUD + 運用操作)、`ampless-editor`(コンテンツ CRUD)、`ampless-reader`(将来の REST/MCP API 利用者向けに予約済み)。
|
|
75
|
+
|
|
76
|
+
### ユーザーパスワードのリセット(管理者による上書き)
|
|
77
|
+
|
|
78
|
+
ロックアウトされており、メールによる復旧が利用できない場合:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
aws cognito-idp admin-set-user-password \
|
|
82
|
+
--user-pool-id <amplify_outputs.json の auth.user_pool_id> \
|
|
83
|
+
--region <リージョン> \
|
|
84
|
+
--username <メールアドレス> \
|
|
85
|
+
--password '<新しいパスワード>' --permanent
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`/login` ページにはセルフサービスの「パスワードを忘れた場合」フローもあります。
|
|
89
|
+
|
|
90
|
+
### Post テーブルのバックアップからの復元
|
|
91
|
+
|
|
92
|
+
DynamoDB のポイントインタイムリカバリ(PITR)は v0.1 の `defineData` では有効になっていません。AWS コンソール → DynamoDB → Tables → `<投稿テーブル名>` → Backups → Edit PITR から手動で有効化してください。有効化後は `aws dynamodb restore-table-to-point-in-time` で新しいテーブルに復元し、その後アイテムをライブテーブルに移行する作業が必要です。
|
|
93
|
+
|
|
94
|
+
### 失敗したプラグインイベントの確認
|
|
95
|
+
|
|
96
|
+
処理に失敗したプロセッサー呼び出しは、`amplify/backend.ts` で作成された共有イベント DLQ(`EventsDlq`)に送られます。SQS コンソールまたは `aws sqs receive-message --queue-url <dlq-url> --max-number-of-messages 10` でメッセージを確認してください。v0.1 には自動アラームがないため、定期的な手動確認を推奨します。あるいは `ApproximateNumberOfMessagesVisible` に CloudWatch アラームを設定してください。
|
|
97
|
+
|
|
98
|
+
## マルチサイト / カスタムドメイン
|
|
99
|
+
|
|
100
|
+
ampless は 1 つの Amplify Hosting デプロイメントから複数のサイトを配信できます。各サイトは `siteId` で識別され、`cms.config.ts` 内で 1 つ以上のホスト名に紐づけられます:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
sites: {
|
|
104
|
+
blog: {
|
|
105
|
+
domains: ['blog.example.com', 'www.example.com'],
|
|
106
|
+
name: 'My Blog',
|
|
107
|
+
url: 'https://blog.example.com',
|
|
108
|
+
},
|
|
109
|
+
docs: {
|
|
110
|
+
domains: ['docs.example.com'],
|
|
111
|
+
name: 'Docs',
|
|
112
|
+
url: 'https://docs.example.com',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
ミドルウェア(`middleware.ts`)が受信リクエストの `Host` を `siteId` にマッピングし、パスを `/_sites/{siteId}/...` に内部リライトします。サブドメインと完全に分離されたドメインはアプリケーション層では等価です — AWS 側の設定のみが異なります。
|
|
118
|
+
|
|
119
|
+
### シングルドメイン運用
|
|
120
|
+
|
|
121
|
+
`sites` が未定義または 1 エントリのみの場合、ampless はシングルサイトモード(`siteId='default'`)で動作します。SSR レスポンスはルートごとのキャッシュディレクティブに従います(ルートごとに `Cache-Control: public, s-maxage=...` を指定することで CloudFront キャッシュを利用できます)。
|
|
122
|
+
|
|
123
|
+
### マルチサイトモードの注意事項:SSR キャッシュの強制無効化
|
|
124
|
+
|
|
125
|
+
2 つ以上のサイトが設定されている場合、ミドルウェアはすべてのパブリックレスポンスに `Cache-Control: private, no-store` を付加します。これは Amplify Hosting の CloudFront がキャッシュキーに `Host` を含まないため、キャッシュを有効にすると `https://site1/foo` と `https://site2/foo` がエッジでコンテンツを混在させてしまうためです。このトレードオフとして、すべてのパブリック読み取りが Lambda にヒットします。この制約を解消するには、Amplify Hosting から独自管理の CloudFront + Open Next スタックへの移行が必要です(ロードマップ:v1.0 以降)。
|
|
126
|
+
|
|
127
|
+
### Amplify Hosting へのカスタムドメイン追加
|
|
128
|
+
|
|
129
|
+
バインドしたいドメインごとに以下を実施します:
|
|
130
|
+
|
|
131
|
+
1. **Amplify Hosting コンソール** → アプリ → **Domain management** → **Add domain** を選択。
|
|
132
|
+
2. 頂点ドメイン(`example.com`)と接続したいサブドメインを入力します。Amplify が ACM 証明書と CloudFront SAN エントリを自動でプロビジョニングします。
|
|
133
|
+
3. DNS を更新:
|
|
134
|
+
- **Route 53 / Amplify 管理の DNS プロバイダー**:Amplify が CNAME を作成してくれるので、確認するだけです。
|
|
135
|
+
- **外部 DNS**(Cloudflare、Squarespace など):Amplify が表示する CNAME / DNS 検証レコードをコピーします。ACM のメール検証もフォールバックとして使用できます。
|
|
136
|
+
4. **Domain activation** が完了するまで待ちます(通常 15〜60 分。証明書の検証が最も時間がかかります)。
|
|
137
|
+
5. 新しいドメインを `cms.config.ts` の対応する `sites.{id}.domains[]` に追加して再デプロイします:
|
|
138
|
+
```bash
|
|
139
|
+
git add cms.config.ts && git commit -m "feat: add docs.example.com"
|
|
140
|
+
git push # Amplify Hosting が自動検出します
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
エンドツーエンドで確認:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
curl -I https://docs.example.com/ # 200 とドキュメントサイトの HTML
|
|
147
|
+
curl -sI https://docs.example.com/ | grep -i cache # Cache-Control: private, no-store
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
リクエストが `404 Site not found` を返す場合、そのホストはいずれの `sites.*.domains[]` にも登録されていません — 設定を修正して再デプロイしてください。
|
|
@@ -1,6 +1,24 @@
|
|
|
1
|
+
> 日本語版: [RUNBOOK.ja.md](./RUNBOOK.ja.md)
|
|
2
|
+
>
|
|
3
|
+
|
|
1
4
|
# Runbook
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
Step-by-step recipes for occasional operations on an ampless-powered site — the things you don't do every day but want a reliable procedure for when you do.
|
|
7
|
+
|
|
8
|
+
For day-to-day usage (commands, admin UI tour, themes, plugins, deploying), start with [README.md](./README.md).
|
|
9
|
+
|
|
10
|
+
## Contents
|
|
11
|
+
|
|
12
|
+
- [AppSync API key (auto-renewed)](#appsync-api-key-auto-renewed)
|
|
13
|
+
- [Common operations](#common-operations)
|
|
14
|
+
- [Promote / demote a user](#promote--demote-a-user)
|
|
15
|
+
- [Reset a user's password (admin override)](#reset-a-users-password-admin-override)
|
|
16
|
+
- [Restore from a Post-table backup](#restore-from-a-post-table-backup)
|
|
17
|
+
- [Inspect failed plugin events](#inspect-failed-plugin-events)
|
|
18
|
+
- [Multi-site / custom domains](#multi-site--custom-domains)
|
|
19
|
+
- [Single domain operation](#single-domain-operation)
|
|
20
|
+
- [Multi-site mode caveat: SSR caching is force-disabled](#multi-site-mode-caveat-ssr-caching-is-force-disabled)
|
|
21
|
+
- [Adding a custom domain to Amplify Hosting](#adding-a-custom-domain-to-amplify-hosting)
|
|
4
22
|
|
|
5
23
|
## AppSync API key (auto-renewed)
|
|
6
24
|
|
|
@@ -3,4 +3,10 @@ import { defineFunction } from '@aws-amplify/backend'
|
|
|
3
3
|
export const postConfirmation = defineFunction({
|
|
4
4
|
name: 'post-confirmation',
|
|
5
5
|
entry: './handler.ts',
|
|
6
|
+
// Auth trigger — assign to the auth nested stack so CFN doesn't
|
|
7
|
+
// place it in a separate `function` stack that ends up in a
|
|
8
|
+
// circular dependency loop with auth (which references this
|
|
9
|
+
// function) and storage/data (which auth happens to reach via
|
|
10
|
+
// cross-stack refs the function adds).
|
|
11
|
+
resourceGroupName: 'auth',
|
|
6
12
|
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AmplessBackend } from '@ampless/backend'
|
|
2
|
+
|
|
3
|
+
// Custom backend extensions for this project.
|
|
4
|
+
//
|
|
5
|
+
// `amplify/backend.ts` calls `customizeBackend(backend)` right after
|
|
6
|
+
// ampless's baseline wiring. Mutate the passed `backend` instance to
|
|
7
|
+
// add Lambda permissions, attach event sources, register custom CDK
|
|
8
|
+
// constructs, etc.
|
|
9
|
+
//
|
|
10
|
+
// This file is NEVER overwritten by `create-ampless upgrade` —
|
|
11
|
+
// `amplify/backend.ts` is, so keep your customizations here.
|
|
12
|
+
//
|
|
13
|
+
// (Function-export rather than side-effect import on purpose: a
|
|
14
|
+
// top-level `import { backend } from './backend.js'` would create an
|
|
15
|
+
// ESM circular dependency, leaving `backend` in its TDZ at evaluation
|
|
16
|
+
// time. The factory pattern sidesteps that.)
|
|
17
|
+
//
|
|
18
|
+
// Example: granting an existing ampless Lambda extra IAM permissions:
|
|
19
|
+
//
|
|
20
|
+
// import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'
|
|
21
|
+
//
|
|
22
|
+
// export function customizeBackend(backend: AmplessBackend): void {
|
|
23
|
+
// backend.processorTrusted.resources.lambda.addToRolePolicy(
|
|
24
|
+
// new PolicyStatement({
|
|
25
|
+
// effect: Effect.ALLOW,
|
|
26
|
+
// actions: ['ses:SendEmail'],
|
|
27
|
+
// resources: ['*'],
|
|
28
|
+
// })
|
|
29
|
+
// )
|
|
30
|
+
// }
|
|
31
|
+
//
|
|
32
|
+
// Example: adding your own Lambda function — define it in
|
|
33
|
+
// `amplify/functions/<name>/resource.ts`, import it here, and attach
|
|
34
|
+
// it inside `customizeBackend` via `backend.createStack(...)` or by
|
|
35
|
+
// granting it permissions on existing resources.
|
|
36
|
+
//
|
|
37
|
+
// If you don't need any extensions, leave the body empty.
|
|
38
|
+
|
|
39
|
+
export function customizeBackend(_backend: AmplessBackend): void {
|
|
40
|
+
// No-op by default.
|
|
41
|
+
}
|
|
@@ -8,13 +8,17 @@ import { eventDispatcher } from './events/dispatcher/resource.js'
|
|
|
8
8
|
import { processorTrusted } from './events/processor-trusted/resource.js'
|
|
9
9
|
import { processorUntrusted } from './events/processor-untrusted/resource.js'
|
|
10
10
|
import { apiKeyRenewer } from './functions/api-key-renewer/resource.js'
|
|
11
|
+
import { mcpHandler } from './functions/mcp-handler/resource.js'
|
|
12
|
+
import { userAdmin } from './functions/user-admin/resource.js'
|
|
13
|
+
import { customizeBackend } from './backend.custom.js'
|
|
11
14
|
|
|
12
15
|
// `defineAmplessBackend` provisions auth, data, storage, the event
|
|
13
16
|
// system (DynamoDB Streams → SQS-trusted / SQS-untrusted → trust_level
|
|
14
17
|
// Lambdas), the AppSync API key renewer, and every IAM / CORS /
|
|
15
18
|
// password policy override. Add custom CDK constructs / IAM policies
|
|
16
|
-
//
|
|
17
|
-
// returns the same object Amplify Gen 2's
|
|
19
|
+
// in `amplify/backend.custom.ts` by mutating the `backend` instance —
|
|
20
|
+
// `defineAmplessBackend` returns the same object Amplify Gen 2's
|
|
21
|
+
// `defineBackend` does.
|
|
18
22
|
const backend = defineAmplessBackend({
|
|
19
23
|
auth,
|
|
20
24
|
data,
|
|
@@ -24,6 +28,12 @@ const backend = defineAmplessBackend({
|
|
|
24
28
|
processorTrusted,
|
|
25
29
|
processorUntrusted,
|
|
26
30
|
apiKeyRenewer,
|
|
31
|
+
mcpHandler,
|
|
32
|
+
userAdmin,
|
|
27
33
|
})
|
|
28
34
|
|
|
35
|
+
// Run user-defined customizations after baseline wiring. `backend.custom.ts`
|
|
36
|
+
// is never overwritten by `create-ampless upgrade`.
|
|
37
|
+
customizeBackend(backend)
|
|
38
|
+
|
|
29
39
|
export default backend
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Custom data schema models for this project.
|
|
2
|
+
//
|
|
3
|
+
// `amplify/data/resource.ts` calls `customSchemaModels(a)` and spreads
|
|
4
|
+
// the result alongside ampless's built-in models in the same
|
|
5
|
+
// `a.schema({...})` call, which is the only way Amplify Gen 2's
|
|
6
|
+
// `ClientSchema<typeof schema>` inference picks up extra models.
|
|
7
|
+
//
|
|
8
|
+
// This file is NEVER overwritten by `create-ampless upgrade` —
|
|
9
|
+
// `amplify/data/resource.ts` is, so keep your custom models here.
|
|
10
|
+
//
|
|
11
|
+
// Example:
|
|
12
|
+
//
|
|
13
|
+
// export function customSchemaModels(a: any) {
|
|
14
|
+
// return {
|
|
15
|
+
// Bookmark: a
|
|
16
|
+
// .model({
|
|
17
|
+
// siteId: a.string().required(),
|
|
18
|
+
// bookmarkId: a.id().required(),
|
|
19
|
+
// url: a.string().required(),
|
|
20
|
+
// title: a.string(),
|
|
21
|
+
// })
|
|
22
|
+
// .identifier(['siteId', 'bookmarkId'])
|
|
23
|
+
// .authorization((allow: any) => [allow.groups(['ampless-admin', 'ampless-editor'])]),
|
|
24
|
+
// }
|
|
25
|
+
// }
|
|
26
|
+
//
|
|
27
|
+
// `a` is the Amplify Gen 2 schema builder; see Amplify Data docs for
|
|
28
|
+
// the full DSL. Return an empty object if no customizations are needed.
|
|
29
|
+
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
export function customSchemaModels(_a: any): Record<string, unknown> {
|
|
32
|
+
return {}
|
|
33
|
+
}
|
|
@@ -1,27 +1,52 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
1
3
|
import { a, defineData, type ClientSchema } from '@aws-amplify/backend'
|
|
2
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
amplessSchemaModels,
|
|
6
|
+
amplessSchemaAuthorization,
|
|
7
|
+
defaultAuthorizationModes,
|
|
8
|
+
} from '@ampless/backend'
|
|
9
|
+
import { userAdmin } from '../functions/user-admin/resource.js'
|
|
10
|
+
import { mcpHandler } from '../functions/mcp-handler/resource.js'
|
|
11
|
+
import { customSchemaModels } from './resource.custom.js'
|
|
12
|
+
|
|
13
|
+
// AppSync's `a.handler.custom({ entry })` paths are resolved by CDK
|
|
14
|
+
// relative to the file that called `a.handler.custom`. When the call
|
|
15
|
+
// originates inside `@ampless/backend` (via amplessSchemaModels), the
|
|
16
|
+
// CDK synth tries to resolve `./list-published-posts.js` relative to
|
|
17
|
+
// `node_modules/@ampless/backend/dist/index.js` and fails with
|
|
18
|
+
// `UnresolvedEntryPathError`. Anchor the resolver paths at THIS file's
|
|
19
|
+
// directory so the result is unambiguous.
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
3
21
|
|
|
4
22
|
// Ampless's built-in models (Post / Page / Media / Taxonomy / PostTag /
|
|
5
23
|
// KvStore) plus the three public-read custom queries
|
|
6
24
|
// (listPublishedPosts / getPublishedPost / listPostsByTag).
|
|
7
25
|
//
|
|
8
|
-
// Add project-specific models
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
// Add project-specific models in `amplify/data/resource.custom.ts` —
|
|
27
|
+
// that file is never overwritten by `create-ampless upgrade`.
|
|
28
|
+
const resolverPaths = {
|
|
29
|
+
listPublishedPosts: resolve(__dirname, 'list-published-posts.js'),
|
|
30
|
+
getPublishedPost: resolve(__dirname, 'get-published-post.js'),
|
|
31
|
+
listPostsByTag: resolve(__dirname, 'list-posts-by-tag.js'),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const schema = a
|
|
35
|
+
.schema({
|
|
36
|
+
...amplessSchemaModels(a, {
|
|
37
|
+
resolverPaths,
|
|
38
|
+
userAdminFunction: userAdmin,
|
|
39
|
+
}),
|
|
40
|
+
...customSchemaModels(a),
|
|
41
|
+
})
|
|
42
|
+
// Schema-level `.authorization()` is the only place resource auth is
|
|
43
|
+
// honoured in `@aws-amplify/data-schema` — model-level callbacks have
|
|
44
|
+
// `resource` destructured out of `allow`. The helper returns an empty
|
|
45
|
+
// array when no Lambda refs are supplied, so the schema stays
|
|
46
|
+
// unaffected if MCP is later removed.
|
|
47
|
+
.authorization((allow) => amplessSchemaAuthorization(allow, {
|
|
48
|
+
mcpHandlerFunction: mcpHandler,
|
|
49
|
+
}))
|
|
25
50
|
|
|
26
51
|
export type Schema = ClientSchema<typeof schema>
|
|
27
52
|
export const data = defineData({
|
|
@@ -6,4 +6,8 @@ export const processorUntrusted = defineFunction({
|
|
|
6
6
|
// Untrusted plugins do pure JS work; modest memory.
|
|
7
7
|
memoryMB: 256,
|
|
8
8
|
timeoutSeconds: 30,
|
|
9
|
+
// Pin to the data stack so all event-system Lambdas live together
|
|
10
|
+
// (dispatcher / processor-trusted are also `data`) and the function
|
|
11
|
+
// stack doesn't bridge data ↔ storage ↔ auth in a CFN cycle.
|
|
12
|
+
resourceGroupName: 'data',
|
|
9
13
|
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { handler } from '@ampless/backend/functions/mcp-handler'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineFunction } from '@aws-amplify/backend'
|
|
2
|
+
|
|
3
|
+
export const mcpHandler = defineFunction({
|
|
4
|
+
name: 'mcp-handler',
|
|
5
|
+
entry: './handler.ts',
|
|
6
|
+
// Co-locate with the data stack so the function has the KvStore
|
|
7
|
+
// table ARN available without cross-stack references at synth time.
|
|
8
|
+
resourceGroupName: 'data',
|
|
9
|
+
memoryMB: 512,
|
|
10
|
+
timeoutSeconds: 30,
|
|
11
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineFunction } from '@aws-amplify/backend'
|
|
2
|
+
|
|
3
|
+
// `resourceGroupName: 'data'` co-locates this Lambda with the AppSync
|
|
4
|
+
// stack. Required because ampless wires userAdmin as a custom GraphQL
|
|
5
|
+
// resolver (data → function) while api-key-renewer reads graphqlApi
|
|
6
|
+
// (function → data); leaving userAdmin in the default `function` stack
|
|
7
|
+
// turns those two arrows into a CloudFormation circular dependency.
|
|
8
|
+
export const userAdmin = defineFunction({
|
|
9
|
+
name: 'user-admin',
|
|
10
|
+
entry: './handler.ts',
|
|
11
|
+
resourceGroupName: 'data',
|
|
12
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
frontend:
|
|
3
|
+
phases:
|
|
4
|
+
preBuild:
|
|
5
|
+
commands:
|
|
6
|
+
- npm install
|
|
7
|
+
build:
|
|
8
|
+
commands:
|
|
9
|
+
- npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
|
|
10
|
+
- npm run build
|
|
11
|
+
artifacts:
|
|
12
|
+
baseDirectory: .next
|
|
13
|
+
files:
|
|
14
|
+
- '**/*'
|
|
15
|
+
cache:
|
|
16
|
+
paths:
|
|
17
|
+
- node_modules/**/*
|
|
18
|
+
- .next/cache/**/*
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Tailwind v4 doesn't scan `node_modules/` by default. The admin UI
|
|
5
|
+
* (`@ampless/admin`) and runtime UI bits (`@ampless/runtime`) ship
|
|
6
|
+
* compiled JS that uses Tailwind utility classes; without these
|
|
7
|
+
* explicit sources, every utility used only inside those packages
|
|
8
|
+
* (e.g. the admin sidebar's `flex`, `fixed`, `md:sticky`, ...) gets
|
|
9
|
+
* tree-shaken out of the final CSS, and the admin layout falls back
|
|
10
|
+
* to browser defaults — sidebar overlaps main content, etc.
|
|
11
|
+
*/
|
|
12
|
+
@source '../node_modules/@ampless/admin/dist';
|
|
13
|
+
@source '../node_modules/@ampless/runtime/dist';
|
|
14
|
+
|
|
2
15
|
@plugin '@tailwindcss/typography';
|
|
3
16
|
/* DADS theme plugin from Digital Agency. Loaded globally so the
|
|
4
17
|
* `templates/dads/` theme can use DADS-conformant color variables
|
|
@@ -36,6 +49,31 @@
|
|
|
36
49
|
--radius-sm: calc(var(--radius) - 4px);
|
|
37
50
|
}
|
|
38
51
|
|
|
52
|
+
/*
|
|
53
|
+
* Color scheme:
|
|
54
|
+
*
|
|
55
|
+
* - `:root { color-scheme: light dark }` opts the page into both modes
|
|
56
|
+
* and asks the browser to pick based on the visitor's
|
|
57
|
+
* `prefers-color-scheme` setting.
|
|
58
|
+
* - `html[data-color-scheme='light' | 'dark']` lets the site admin pin
|
|
59
|
+
* one mode regardless of system preference. The attribute is written
|
|
60
|
+
* by `app/layout.tsx` from the per-site setting
|
|
61
|
+
* (`theme.colorScheme`, default `'auto'` = no attribute).
|
|
62
|
+
* - Token values below (and in each theme's `tokens.css`) use the CSS
|
|
63
|
+
* `light-dark(L, D)` function so a single declaration covers both
|
|
64
|
+
* modes — the active `color-scheme` selects between L and D at
|
|
65
|
+
* paint time.
|
|
66
|
+
*/
|
|
67
|
+
:root {
|
|
68
|
+
color-scheme: light dark;
|
|
69
|
+
}
|
|
70
|
+
html[data-color-scheme='light'] {
|
|
71
|
+
color-scheme: light;
|
|
72
|
+
}
|
|
73
|
+
html[data-color-scheme='dark'] {
|
|
74
|
+
color-scheme: dark;
|
|
75
|
+
}
|
|
76
|
+
|
|
39
77
|
/*
|
|
40
78
|
* Default tokens — used as fallback when no theme's `[data-theme=...]`
|
|
41
79
|
* selector matches (e.g. body has no data-theme attribute, or admin
|
|
@@ -45,49 +83,27 @@
|
|
|
45
83
|
* `<body data-theme="...">` attribute makes the scoped selector match.
|
|
46
84
|
*/
|
|
47
85
|
:root {
|
|
48
|
-
--background: oklch(1 0 0);
|
|
49
|
-
--foreground: oklch(0.145 0 0);
|
|
50
|
-
--card: oklch(1 0 0);
|
|
51
|
-
--card-foreground: oklch(0.145 0 0);
|
|
52
|
-
--primary: oklch(0.205 0 0);
|
|
53
|
-
--primary-foreground: oklch(0.985 0 0);
|
|
54
|
-
--secondary: oklch(0.97 0 0);
|
|
55
|
-
--secondary-foreground: oklch(0.205 0 0);
|
|
56
|
-
--muted: oklch(0.97 0 0);
|
|
57
|
-
--muted-foreground: oklch(0.556 0 0);
|
|
58
|
-
--accent: oklch(0.97 0 0);
|
|
59
|
-
--accent-foreground: oklch(0.205 0 0);
|
|
60
|
-
--destructive: oklch(0.577 0.245 27.325);
|
|
61
|
-
--destructive-foreground: oklch(0.985 0 0);
|
|
62
|
-
--border: oklch(0.922 0 0);
|
|
63
|
-
--input: oklch(0.922 0 0);
|
|
64
|
-
--ring: oklch(0.708 0 0);
|
|
86
|
+
--background: light-dark(oklch(1 0 0), oklch(0.145 0 0));
|
|
87
|
+
--foreground: light-dark(oklch(0.145 0 0), oklch(0.985 0 0));
|
|
88
|
+
--card: light-dark(oklch(1 0 0), oklch(0.145 0 0));
|
|
89
|
+
--card-foreground: light-dark(oklch(0.145 0 0), oklch(0.985 0 0));
|
|
90
|
+
--primary: light-dark(oklch(0.205 0 0), oklch(0.985 0 0));
|
|
91
|
+
--primary-foreground: light-dark(oklch(0.985 0 0), oklch(0.205 0 0));
|
|
92
|
+
--secondary: light-dark(oklch(0.97 0 0), oklch(0.269 0 0));
|
|
93
|
+
--secondary-foreground: light-dark(oklch(0.205 0 0), oklch(0.985 0 0));
|
|
94
|
+
--muted: light-dark(oklch(0.97 0 0), oklch(0.269 0 0));
|
|
95
|
+
--muted-foreground: light-dark(oklch(0.556 0 0), oklch(0.708 0 0));
|
|
96
|
+
--accent: light-dark(oklch(0.97 0 0), oklch(0.269 0 0));
|
|
97
|
+
--accent-foreground: light-dark(oklch(0.205 0 0), oklch(0.985 0 0));
|
|
98
|
+
--destructive: light-dark(oklch(0.577 0.245 27.325), oklch(0.396 0.141 25.723));
|
|
99
|
+
--destructive-foreground: light-dark(oklch(0.985 0 0), oklch(0.985 0 0));
|
|
100
|
+
--border: light-dark(oklch(0.922 0 0), oklch(0.269 0 0));
|
|
101
|
+
--input: light-dark(oklch(0.922 0 0), oklch(0.269 0 0));
|
|
102
|
+
--ring: light-dark(oklch(0.708 0 0), oklch(0.439 0 0));
|
|
65
103
|
--radius: 0.5rem;
|
|
66
104
|
--ampless-body-font: system-ui, -apple-system, sans-serif;
|
|
67
105
|
}
|
|
68
106
|
|
|
69
|
-
@media (prefers-color-scheme: dark) {
|
|
70
|
-
:root {
|
|
71
|
-
--background: oklch(0.145 0 0);
|
|
72
|
-
--foreground: oklch(0.985 0 0);
|
|
73
|
-
--card: oklch(0.145 0 0);
|
|
74
|
-
--card-foreground: oklch(0.985 0 0);
|
|
75
|
-
--primary: oklch(0.985 0 0);
|
|
76
|
-
--primary-foreground: oklch(0.205 0 0);
|
|
77
|
-
--secondary: oklch(0.269 0 0);
|
|
78
|
-
--secondary-foreground: oklch(0.985 0 0);
|
|
79
|
-
--muted: oklch(0.269 0 0);
|
|
80
|
-
--muted-foreground: oklch(0.708 0 0);
|
|
81
|
-
--accent: oklch(0.269 0 0);
|
|
82
|
-
--accent-foreground: oklch(0.985 0 0);
|
|
83
|
-
--destructive: oklch(0.396 0.141 25.723);
|
|
84
|
-
--destructive-foreground: oklch(0.985 0 0);
|
|
85
|
-
--border: oklch(0.269 0 0);
|
|
86
|
-
--input: oklch(0.269 0 0);
|
|
87
|
-
--ring: oklch(0.439 0 0);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
107
|
body {
|
|
92
108
|
font-family: var(--ampless-body-font, system-ui, -apple-system, sans-serif);
|
|
93
109
|
background: var(--background);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
import { headers } from 'next/headers'
|
|
3
3
|
import { DEFAULT_SITE_ID } from 'ampless'
|
|
4
|
+
import { validateColorScheme } from '@ampless/runtime'
|
|
4
5
|
import { Providers } from './providers'
|
|
5
6
|
import { siteMetadata } from '@/lib/seo'
|
|
6
7
|
import { loadThemeConfig, renderThemeCss } from '@/lib/theme-config'
|
|
@@ -25,8 +26,28 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|
|
25
26
|
const themeCss = renderThemeCss(theme.cssVars)
|
|
26
27
|
const locale = getLocale()
|
|
27
28
|
const dict = getDictionary(locale)
|
|
29
|
+
// `data-color-scheme` pins the visitor to light or dark regardless
|
|
30
|
+
// of their system `prefers-color-scheme`. `'auto'` (the default)
|
|
31
|
+
// means we don't emit the attribute at all, so `globals.css`'s
|
|
32
|
+
// `:root { color-scheme: light dark }` lets the browser follow the
|
|
33
|
+
// system setting. Themes use `light-dark()` in their tokens.css so a
|
|
34
|
+
// single declaration covers both modes; the active `color-scheme`
|
|
35
|
+
// selects which value is rendered.
|
|
36
|
+
//
|
|
37
|
+
// The admin's theme-settings iframe preview passes the unsaved
|
|
38
|
+
// selection via `?previewColorScheme=<mode>`, which middleware
|
|
39
|
+
// forwards as `x-preview-color-scheme`. When that header is present
|
|
40
|
+
// it overrides the stored site setting so the preview updates live.
|
|
41
|
+
const previewHeader = h.get('x-preview-color-scheme')
|
|
42
|
+
const effectiveColorScheme = previewHeader
|
|
43
|
+
? validateColorScheme(previewHeader)
|
|
44
|
+
: theme.colorScheme
|
|
45
|
+
const htmlProps: { lang: string; 'data-color-scheme'?: 'light' | 'dark' } = { lang: locale }
|
|
46
|
+
if (effectiveColorScheme !== 'auto') {
|
|
47
|
+
htmlProps['data-color-scheme'] = effectiveColorScheme
|
|
48
|
+
}
|
|
28
49
|
return (
|
|
29
|
-
<html
|
|
50
|
+
<html {...htmlProps}>
|
|
30
51
|
<head>
|
|
31
52
|
{/* Inline `:root` overrides come AFTER globals.css so they win
|
|
32
53
|
against the static defaults. Validated values only — see
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ampless } from '@/lib/ampless'
|
|
2
|
+
import { createStaticRouteHandler } from '@ampless/runtime/routes'
|
|
3
|
+
|
|
4
|
+
// Catch-all handler for `format: 'static'` post bundles. Lives here
|
|
5
|
+
// instead of under a dedicated `/static/` prefix so the published URL
|
|
6
|
+
// stays a clean `/<slug>/…`, matching how a developer would host a
|
|
7
|
+
// hand-written landing page locally. The runtime handler enforces
|
|
8
|
+
// `format === 'static'` and 404s for any other post format reaching
|
|
9
|
+
// this route.
|
|
10
|
+
//
|
|
11
|
+
// Routing precedence: Next.js prefers the more specific
|
|
12
|
+
// `[siteId]/[slug]/page.tsx` for single-segment URLs, so the regular
|
|
13
|
+
// theme post dispatcher keeps handling normal posts. This catch-all
|
|
14
|
+
// only fires for multi-segment requests (e.g. `/promo/index.html`,
|
|
15
|
+
// `/promo/assets/style.css`).
|
|
16
|
+
export const dynamic = 'force-dynamic'
|
|
17
|
+
export const GET = createStaticRouteHandler(ampless)
|