create-ampless 0.2.0-alpha.8 → 1.0.0-alpha.39

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.
Files changed (87) hide show
  1. package/README.ja.md +77 -0
  2. package/README.md +4 -1
  3. package/dist/index.js +670 -88
  4. package/dist/templates/_shared/AGENTS.ja.md +104 -0
  5. package/dist/templates/_shared/AGENTS.md +104 -0
  6. package/dist/templates/_shared/README.ja.md +175 -0
  7. package/dist/templates/_shared/README.md +175 -0
  8. package/dist/templates/_shared/RUNBOOK.ja.md +120 -0
  9. package/dist/templates/_shared/RUNBOOK.md +24 -51
  10. package/dist/templates/_shared/amplify/auth/post-confirmation/resource.ts +6 -0
  11. package/dist/templates/_shared/amplify/backend.custom.ts +41 -0
  12. package/dist/templates/_shared/amplify/backend.ts +12 -2
  13. package/dist/templates/_shared/amplify/data/get-published-post.js +9 -12
  14. package/dist/templates/_shared/amplify/data/list-posts-by-tag.js +6 -9
  15. package/dist/templates/_shared/amplify/data/list-published-posts.js +10 -11
  16. package/dist/templates/_shared/amplify/data/resource.custom.ts +32 -0
  17. package/dist/templates/_shared/amplify/data/resource.ts +26 -13
  18. package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +4 -0
  19. package/dist/templates/_shared/amplify/functions/mcp-handler/handler.ts +1 -0
  20. package/dist/templates/_shared/amplify/functions/mcp-handler/resource.ts +11 -0
  21. package/dist/templates/_shared/amplify/functions/user-admin/handler.ts +4 -0
  22. package/dist/templates/_shared/amplify/functions/user-admin/resource.ts +12 -0
  23. package/dist/templates/_shared/app/(admin)/admin/mcp-tokens/page.tsx +5 -0
  24. package/dist/templates/_shared/app/(admin)/admin/users/page.tsx +5 -0
  25. package/dist/templates/_shared/app/globals.css +55 -39
  26. package/dist/templates/_shared/app/layout.tsx +24 -11
  27. package/dist/templates/_shared/app/site/[siteId]/r/[slug]/[[...path]]/route.ts +17 -0
  28. package/dist/templates/_shared/cms.config.ts +1 -23
  29. package/dist/templates/_shared/components/site-chrome/site-sidebar.tsx +2 -3
  30. package/dist/templates/_shared/lib/admin.ts +9 -7
  31. package/dist/templates/_shared/lib/posts-public.ts +1 -1
  32. package/dist/templates/_shared/package.json +11 -9
  33. package/dist/templates/_shared/proxy.ts +3 -3
  34. package/dist/templates/blog/README.ja.md +22 -0
  35. package/dist/templates/blog/README.md +17 -47
  36. package/dist/templates/blog/manifest.ts +1 -1
  37. package/dist/templates/blog/pages/feed.ts +3 -4
  38. package/dist/templates/blog/pages/home.tsx +5 -8
  39. package/dist/templates/blog/pages/post.tsx +7 -7
  40. package/dist/templates/blog/pages/sitemap.ts +3 -4
  41. package/dist/templates/blog/pages/tag.tsx +4 -4
  42. package/dist/templates/blog/tokens.css +26 -40
  43. package/dist/templates/corporate/README.ja.md +18 -0
  44. package/dist/templates/corporate/README.md +12 -14
  45. package/dist/templates/corporate/pages/feed.ts +2 -3
  46. package/dist/templates/corporate/pages/home.tsx +5 -8
  47. package/dist/templates/corporate/pages/post.tsx +7 -7
  48. package/dist/templates/corporate/pages/sitemap.ts +2 -3
  49. package/dist/templates/corporate/pages/tag.tsx +4 -4
  50. package/dist/templates/corporate/tokens.css +17 -39
  51. package/dist/templates/dads/README.ja.md +31 -0
  52. package/dist/templates/dads/README.md +13 -17
  53. package/dist/templates/dads/pages/feed.ts +2 -3
  54. package/dist/templates/dads/pages/home.tsx +5 -8
  55. package/dist/templates/dads/pages/post.tsx +7 -7
  56. package/dist/templates/dads/pages/sitemap.ts +2 -3
  57. package/dist/templates/dads/pages/tag.tsx +4 -4
  58. package/dist/templates/dads/tokens.css +22 -42
  59. package/dist/templates/docs/README.ja.md +24 -0
  60. package/dist/templates/docs/README.md +10 -13
  61. package/dist/templates/docs/pages/feed.ts +2 -3
  62. package/dist/templates/docs/pages/home.tsx +5 -6
  63. package/dist/templates/docs/pages/post.tsx +8 -8
  64. package/dist/templates/docs/pages/sitemap.ts +2 -3
  65. package/dist/templates/docs/pages/tag.tsx +5 -5
  66. package/dist/templates/docs/tokens.css +17 -39
  67. package/dist/templates/landing/README.ja.md +20 -0
  68. package/dist/templates/landing/README.md +14 -19
  69. package/dist/templates/landing/pages/feed.ts +3 -4
  70. package/dist/templates/landing/pages/home.tsx +5 -8
  71. package/dist/templates/landing/pages/post.tsx +7 -7
  72. package/dist/templates/landing/pages/sitemap.ts +2 -3
  73. package/dist/templates/landing/pages/tag.tsx +4 -4
  74. package/dist/templates/landing/tokens.css +17 -39
  75. package/dist/templates/minimal/README.ja.md +14 -0
  76. package/dist/templates/minimal/README.md +9 -47
  77. package/dist/templates/minimal/pages/feed.ts +3 -4
  78. package/dist/templates/minimal/pages/home.tsx +3 -4
  79. package/dist/templates/minimal/pages/post.tsx +6 -6
  80. package/dist/templates/minimal/pages/sitemap.ts +3 -4
  81. package/dist/templates/minimal/pages/tag.tsx +3 -3
  82. package/dist/templates/minimal/tokens.css +17 -39
  83. package/package.json +1 -1
  84. package/dist/templates/_shared/app/(admin)/admin/sites/page.tsx +0 -5
  85. package/dist/templates/_shared/app/site/[siteId]/raw/[slug]/route.ts +0 -5
  86. package/dist/templates/_shared/lib/admin-site-client.ts +0 -10
  87. package/dist/templates/_shared/lib/admin-site.ts +0 -12
@@ -0,0 +1,120 @@
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
+ - [カスタムドメイン](#custom-domains)
19
+ - [カスタムドメインを Amplify Hosting に追加する](#adding-a-custom-domain-to-amplify-hosting)
20
+
21
+ ## AppSync API キー(自動更新)
22
+
23
+ パブリックブログの読み取り(`listPublishedPosts`、`getPublishedPost`、`listPostsByTag`)は AppSync API キーで保護されています。このキーは `amplify_outputs.json` に含まれているため、**公開サイトを訪問した誰もが確認できる**状態にあります。低信頼度のクレデンシャルとして扱ってください。このキーが持つ権限は上記 3 つのカスタムクエリを呼び出すことのみであり、それらのクエリは `status === 'published'` の行しか返しません。
24
+
25
+ ### なぜ API キーなのか(Identity Pool ゲストロールではなく)?
26
+
27
+ 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 の候補です。
28
+
29
+ ### 自動更新 — ローテーション手順は不要
30
+
31
+ `api-key-renewer` Lambda(`amplify/functions/api-key-renewer/` を参照)は、毎月 1 日の UTC 03:00 に EventBridge スケジュールで起動します。`AppSync.UpdateApiKey` を呼び出して既存キーの `expires` を「現時点 + 364 日」に延長するため、以下が保証されます:
32
+
33
+ - キー ID は変化しない
34
+ - `amplify_outputs.json` は引き続き有効
35
+ - Next.js アプリの再ビルドは不要
36
+ - 常に約 334 日以上の残有効期限が維持される
37
+
38
+ 手動で確認または実行したい場合:
39
+
40
+ ```bash
41
+ # 現在の有効期限を確認
42
+ aws appsync list-api-keys \
43
+ --region <amplify_outputs.json の data.aws_region> \
44
+ --api-id <amplify_outputs.json の data.url から導出した api-id>
45
+
46
+ # 手動実行(サンドボックスを長期間停止した後など)
47
+ aws lambda invoke \
48
+ --function-name $(aws lambda list-functions \
49
+ --query "Functions[?contains(FunctionName,'api-key-renewer')].FunctionName | [0]" \
50
+ --output text) \
51
+ /tmp/out.json && cat /tmp/out.json
52
+ ```
53
+
54
+ ### キーの漏洩が疑われる場合
55
+
56
+ 有効期限の延長ではなく、キーの値そのものをローテーションする即時対応を行います:
57
+
58
+ 1. `amplify/data/resource.ts` のコメントを編集して CFN 更新を強制する
59
+ 2. `npx ampx sandbox`(サンドボックス)または `npx ampx pipeline-deploy ...`(本番)を実行 — Amplify がキーの値を再生成する
60
+ 3. Next.js アプリを再デプロイして SSR が新しい `data.api_key` を参照するようにする
61
+
62
+ ## 一般的な操作
63
+
64
+ ### ユーザーのグループ昇格 / 降格
65
+
66
+ AWS Cognito コンソールから操作します:
67
+
68
+ 1. User Pool → Users → 対象ユーザーを選択
69
+ 2. Group memberships → グループに追加 / グループから削除
70
+ 3. ユーザーに一度サインアウトしてサインインし直してもらい、新しいクレームを適用させる
71
+
72
+ グループの種類: `ampless-admin`(フル CRUD + 運用操作)、`ampless-editor`(コンテンツ CRUD)、`ampless-reader`(将来の REST/MCP API 利用者向けに予約済み)。
73
+
74
+ ### ユーザーパスワードのリセット(管理者による上書き)
75
+
76
+ ロックアウトされており、メールによる復旧が利用できない場合:
77
+
78
+ ```bash
79
+ aws cognito-idp admin-set-user-password \
80
+ --user-pool-id <amplify_outputs.json の auth.user_pool_id> \
81
+ --region <リージョン> \
82
+ --username <メールアドレス> \
83
+ --password '<新しいパスワード>' --permanent
84
+ ```
85
+
86
+ `/login` ページにはセルフサービスの「パスワードを忘れた場合」フローもあります。
87
+
88
+ ### Post テーブルのバックアップからの復元
89
+
90
+ DynamoDB のポイントインタイムリカバリ(PITR)は v0.1 の `defineData` では有効になっていません。AWS コンソール → DynamoDB → Tables → `<投稿テーブル名>` → Backups → Edit PITR から手動で有効化してください。有効化後は `aws dynamodb restore-table-to-point-in-time` で新しいテーブルに復元し、その後アイテムをライブテーブルに移行する作業が必要です。
91
+
92
+ ### 失敗したプラグインイベントの確認
93
+
94
+ 処理に失敗したプロセッサー呼び出しは、`amplify/backend.ts` で作成された共有イベント DLQ(`EventsDlq`)に送られます。SQS コンソールまたは `aws sqs receive-message --queue-url <dlq-url> --max-number-of-messages 10` でメッセージを確認してください。v0.1 には自動アラームがないため、定期的な手動確認を推奨します。あるいは `ApproximateNumberOfMessagesVisible` に CloudWatch アラームを設定してください。
95
+
96
+ ## カスタムドメイン
97
+
98
+ ampless は 1 Amplify デプロイ = 1 サイト。複数サイトを別ドメインで配信したい場合は、サイトごとに Amplify 環境を分けてデプロイしてください。
99
+
100
+ ### Amplify Hosting へのカスタムドメイン追加
101
+
102
+ バインドしたいドメインごとに以下を実施します:
103
+
104
+ 1. **Amplify Hosting コンソール** → アプリ → **Domain management** → **Add domain** を選択。
105
+ 2. 頂点ドメイン(`example.com`)と接続したいサブドメインを入力します。Amplify が ACM 証明書と CloudFront SAN エントリを自動でプロビジョニングします。
106
+ 3. DNS を更新:
107
+ - **Route 53 / Amplify 管理の DNS プロバイダー**:Amplify が CNAME を作成してくれるので、確認するだけです。
108
+ - **外部 DNS**(Cloudflare、Squarespace など):Amplify が表示する CNAME / DNS 検証レコードをコピーします。ACM のメール検証もフォールバックとして使用できます。
109
+ 4. **Domain activation** が完了するまで待ちます(通常 15〜60 分。証明書の検証が最も時間がかかります)。
110
+ 5. `cms.config.ts` の `site.url` を新しい canonical URL に更新し、commit して push します:
111
+ ```bash
112
+ git add cms.config.ts && git commit -m "feat: bind docs.example.com"
113
+ git push # Amplify Hosting が自動検出します
114
+ ```
115
+
116
+ エンドツーエンドで確認:
117
+
118
+ ```bash
119
+ curl -I https://docs.example.com/ # 200 とサイトの HTML
120
+ ```
@@ -1,6 +1,22 @@
1
+ > 日本語版: [RUNBOOK.ja.md](./RUNBOOK.ja.md)
2
+ >
3
+
1
4
  # Runbook
2
5
 
3
- Operational tasks for an ampless-powered site.
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
+ - [Custom domains](#custom-domains)
19
+ - [Adding a custom domain to Amplify Hosting](#adding-a-custom-domain-to-amplify-hosting)
4
20
 
5
21
  ## AppSync API key (auto-renewed)
6
22
 
@@ -99,49 +115,10 @@ or `aws sqs receive-message --queue-url <dlq-url> --max-number-of-messages 10`.
99
115
  There's no automated alarm in v0.1 — periodic manual checks recommended,
100
116
  or wire up a CloudWatch alarm on `ApproximateNumberOfMessagesVisible`.
101
117
 
102
- ## Multi-site / custom domains
103
-
104
- ampless can serve multiple sites from one Amplify Hosting deployment.
105
- Each site is identified by a `siteId` and bound to one or more
106
- hostnames via `cms.config.ts`:
107
-
108
- ```ts
109
- sites: {
110
- blog: {
111
- domains: ['blog.example.com', 'www.example.com'],
112
- name: 'My Blog',
113
- url: 'https://blog.example.com',
114
- },
115
- docs: {
116
- domains: ['docs.example.com'],
117
- name: 'Docs',
118
- url: 'https://docs.example.com',
119
- },
120
- },
121
- ```
122
-
123
- The middleware (`middleware.ts`) maps incoming `Host` to a `siteId` and
124
- internally rewrites the path to `/_sites/{siteId}/...`. Subdomains and
125
- fully separate domains are equivalent at the application layer — only
126
- the AWS-side wiring differs.
127
-
128
- ### Single domain operation
129
-
130
- If `sites` is undefined or has only one entry, ampless runs in
131
- single-site mode (`siteId='default'`). SSR responses follow each page's
132
- own caching directives (so you can opt into CloudFront caching with
133
- `Cache-Control: public, s-maxage=...` per route).
118
+ ## Custom domains
134
119
 
135
- ### Multi-site mode caveat: SSR caching is force-disabled
136
-
137
- When two or more sites are declared, the middleware adds
138
- `Cache-Control: private, no-store` to every public response. This is
139
- because Amplify Hosting's CloudFront does not include `Host` in its
140
- cache key — leaving caching on would let `https://site1/foo` and
141
- `https://site2/foo` cross-contaminate at the edge. The trade-off is
142
- that every public read hits Lambda. Lifting it requires moving off
143
- Amplify Hosting onto a self-managed CloudFront + Open Next stack
144
- (roadmap: post-v1.0).
120
+ ampless runs one site per Amplify deployment. To serve multiple sites
121
+ on different domains, deploy a separate Amplify environment per site.
145
122
 
146
123
  ### Adding a custom domain to Amplify Hosting
147
124
 
@@ -160,19 +137,15 @@ For each domain you want to bind:
160
137
  also works as a fallback.
161
138
  4. Wait for the **Domain activation** to finish (typically 15–60
162
139
  minutes; certificate validation is the slow step).
163
- 5. Add the new domain to the matching `sites.{id}.domains[]` in
164
- `cms.config.ts` and redeploy:
140
+ 5. Update `cms.config.ts` so `site.url` reflects the new canonical URL,
141
+ commit, and push to redeploy:
165
142
  ```bash
166
- git add cms.config.ts && git commit -m "feat: add docs.example.com"
143
+ git add cms.config.ts && git commit -m "feat: bind docs.example.com"
167
144
  git push # Amplify Hosting picks it up
168
145
  ```
169
146
 
170
147
  Verify end-to-end:
171
148
 
172
149
  ```bash
173
- curl -I https://docs.example.com/ # 200 with the docs site's HTML
174
- curl -sI https://docs.example.com/ | grep -i cache # Cache-Control: private, no-store
150
+ curl -I https://docs.example.com/ # 200 with your site's HTML
175
151
  ```
176
-
177
- If the request returns `404 Site not found` instead, the host is not
178
- listed in any `sites.*.domains[]` — fix the config and redeploy.
@@ -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
- // below by mutating the returned `backend` — `defineAmplessBackend`
17
- // returns the same object Amplify Gen 2's `defineBackend` does.
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
@@ -2,24 +2,21 @@ import { util } from '@aws-appsync/utils'
2
2
 
3
3
  // AppSync JS resolver: returns a single published post by slug.
4
4
  //
5
- // Reads the `bySiteIdSlug` GSI: PK = `${siteId}#${slug}`. A given
6
- // (site, slug) tuple identifies at most one row, so this is an O(1)
7
- // PK Query — no scan, no filter, no per-partition limit issues.
5
+ // Reads the `bySlug` GSI: PK = slug. A slug identifies at most one row
6
+ // (uniqueness enforced at the admin form level), so this is an O(1) PK
7
+ // Query — no scan, no filter, no per-partition limit issues.
8
8
  //
9
- // Drafts are dropped in the response handler. The admin form
10
- // enforces a unique slug per site at the application layer, but if
11
- // somehow draft + published share a slug we prefer the published row.
9
+ // Drafts are dropped in the response handler. If draft + published
10
+ // somehow share a slug we prefer the published row.
12
11
  export function request(ctx) {
13
- const siteId = ctx.args.siteId ?? 'default'
14
12
  const slug = ctx.args.slug
15
- const partition = `${siteId}#${slug}`
16
13
  return {
17
14
  operation: 'Query',
18
- index: 'bySiteIdSlug',
15
+ index: 'bySlug',
19
16
  query: {
20
- expression: '#siteIdSlug = :siteIdSlug',
21
- expressionNames: { '#siteIdSlug': 'siteIdSlug' },
22
- expressionValues: util.dynamodb.toMapValues({ ':siteIdSlug': partition }),
17
+ expression: '#slug = :slug',
18
+ expressionNames: { '#slug': 'slug' },
19
+ expressionValues: util.dynamodb.toMapValues({ ':slug': slug }),
23
20
  },
24
21
  limit: 5,
25
22
  }
@@ -2,23 +2,21 @@ import { util } from '@aws-appsync/utils'
2
2
 
3
3
  // AppSync JS resolver: list published posts for a given tag, newest first.
4
4
  // Reads the denormalized PostTag table where:
5
- // PK = `${siteId}#${tag}`
5
+ // PK = tag
6
6
  // SK = `${publishedAt}#${postId}` (so descending SK = newest first)
7
7
  //
8
8
  // Authorization is enforced by AppSync; the resolver itself only encodes
9
- // the tag/site partition condition. Drafts never appear here because the
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
12
  export function request(ctx) {
13
- const { siteId = 'default', tag, limit, nextToken } = ctx.args
13
+ const { tag, limit, nextToken } = ctx.args
14
14
  return {
15
15
  operation: 'Query',
16
16
  query: {
17
- expression: '#siteIdTag = :siteIdTag',
18
- expressionNames: { '#siteIdTag': 'siteIdTag' },
19
- expressionValues: util.dynamodb.toMapValues({
20
- ':siteIdTag': `${siteId}#${tag}`,
21
- }),
17
+ expression: '#tag = :tag',
18
+ expressionNames: { '#tag': 'tag' },
19
+ expressionValues: util.dynamodb.toMapValues({ ':tag': tag }),
22
20
  },
23
21
  scanIndexForward: false, // newest first (SK descends)
24
22
  limit: limit ?? 20,
@@ -33,7 +31,6 @@ export function response(ctx) {
33
31
  // PublicPost shape `listPublishedPosts` returns; the detail view should
34
32
  // call `getPublishedPost(slug)` for the full body.
35
33
  const items = (ctx.result.items ?? []).map((row) => ({
36
- siteId: row.siteId,
37
34
  postId: row.postId,
38
35
  slug: row.slug,
39
36
  title: row.title,
@@ -1,23 +1,22 @@
1
1
  import { util } from '@aws-appsync/utils'
2
2
 
3
- // AppSync JS resolver: list a site's published posts, newest first.
3
+ // AppSync JS resolver: list published posts, newest first.
4
4
  //
5
- // Reads the `bySiteIdStatus` GSI:
6
- // PK = `${siteId}#${status}` (denormalized field set by writers)
5
+ // Reads the `byStatus` GSI:
6
+ // PK = status (denormalized field set by writers)
7
7
  // SK = publishedAt
8
- // so a single Query reads only one site's published partition. Drafts
9
- // never appear because the PK condition pins status='published'.
8
+ // so a single Query reads only the `published` partition. Drafts never
9
+ // appear because the PK condition pins status='published'.
10
10
  //
11
11
  // Date-range filtering (`from`, `to`) is pushed into the SK condition,
12
12
  // so DynamoDB only reads the matching range. `nextToken` paginates
13
13
  // without re-issuing a fresh query.
14
14
  export function request(ctx) {
15
- const { siteId = 'default', from, to, limit, nextToken } = ctx.args
15
+ const { from, to, limit, nextToken } = ctx.args
16
16
 
17
- const partition = `${siteId}#published`
18
- let keyExpression = '#siteIdStatus = :siteIdStatus'
19
- const expressionNames = { '#siteIdStatus': 'siteIdStatus' }
20
- const expressionValueMap = { ':siteIdStatus': partition }
17
+ let keyExpression = '#status = :status'
18
+ const expressionNames = { '#status': 'status' }
19
+ const expressionValueMap = { ':status': 'published' }
21
20
 
22
21
  if (from && to) {
23
22
  keyExpression += ' AND #publishedAt BETWEEN :from AND :to'
@@ -36,7 +35,7 @@ export function request(ctx) {
36
35
 
37
36
  return {
38
37
  operation: 'Query',
39
- index: 'bySiteIdStatus',
38
+ index: 'byStatus',
40
39
  query: {
41
40
  expression: keyExpression,
42
41
  expressionNames,
@@ -0,0 +1,32 @@
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
+ // bookmarkId: a.id().required(),
18
+ // url: a.string().required(),
19
+ // title: a.string(),
20
+ // })
21
+ // .identifier(['bookmarkId'])
22
+ // .authorization((allow: any) => [allow.groups(['ampless-admin', 'ampless-editor'])]),
23
+ // }
24
+ // }
25
+ //
26
+ // `a` is the Amplify Gen 2 schema builder; see Amplify Data docs for
27
+ // the full DSL. Return an empty object if no customizations are needed.
28
+
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ export function customSchemaModels(_a: any): Record<string, unknown> {
31
+ return {}
32
+ }
@@ -1,7 +1,14 @@
1
1
  import { fileURLToPath } from 'node:url'
2
2
  import { dirname, resolve } from 'node:path'
3
3
  import { a, defineData, type ClientSchema } from '@aws-amplify/backend'
4
- import { amplessSchemaModels, defaultAuthorizationModes } from '@ampless/backend'
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'
5
12
 
6
13
  // AppSync's `a.handler.custom({ entry })` paths are resolved by CDK
7
14
  // relative to the file that called `a.handler.custom`. When the call
@@ -16,24 +23,30 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
16
23
  // KvStore) plus the three public-read custom queries
17
24
  // (listPublishedPosts / getPublishedPost / listPostsByTag).
18
25
  //
19
- // Add project-specific models alongside the spread:
20
- //
21
- // const schema = a.schema({
22
- // ...amplessSchemaModels(a, { resolverPaths }),
23
- // MyCustomModel: a
24
- // .model({ siteId: a.string().required(), foo: a.string() })
25
- // .identifier(['siteId', 'foo'])
26
- // .authorization((allow) => [allow.groups(['ampless-admin'])]),
27
- // })
26
+ // Add project-specific models in `amplify/data/resource.custom.ts`
27
+ // that file is never overwritten by `create-ampless upgrade`.
28
28
  const resolverPaths = {
29
29
  listPublishedPosts: resolve(__dirname, 'list-published-posts.js'),
30
30
  getPublishedPost: resolve(__dirname, 'get-published-post.js'),
31
31
  listPostsByTag: resolve(__dirname, 'list-posts-by-tag.js'),
32
32
  }
33
33
 
34
- const schema = a.schema({
35
- ...amplessSchemaModels(a, { resolverPaths }),
36
- })
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
+ }))
37
50
 
38
51
  export type Schema = ClientSchema<typeof schema>
39
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,4 @@
1
+ // Re-exported from @ampless/backend so the package can ship Lambda
2
+ // handler updates via `npm update`. Amplify's esbuild follows this
3
+ // import and bundles the real handler into the Lambda artifact.
4
+ export { handler } from '@ampless/backend/auth/user-admin'
@@ -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,5 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createMcpTokensPage } from '@ampless/admin/pages'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+ export default createMcpTokensPage(admin)
@@ -0,0 +1,5 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createUsersListPage } from '@ampless/admin/pages'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+ export default createUsersListPage(admin)