headroom-cms 0.1.1

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 (237) hide show
  1. package/README.md +282 -0
  2. package/admin/assets/AdminsPage-CnrQqwKA.js +1 -0
  3. package/admin/assets/AllContentPage-ByN1h3PP.js +1 -0
  4. package/admin/assets/ApiKeysPage-FgNHZPBS.js +1 -0
  5. package/admin/assets/AuditPage-DAPpo-sj.js +1 -0
  6. package/admin/assets/BlockEditor-CZTwex-o.js +179 -0
  7. package/admin/assets/BlockEditor-Cp_wZ2xN.css +1 -0
  8. package/admin/assets/BlockTypeEditPage-Buuwbx1P.js +1 -0
  9. package/admin/assets/BlockTypesPage-Dj0qmsqX.js +1 -0
  10. package/admin/assets/BulkActionBar-BMcUBJSH.js +1 -0
  11. package/admin/assets/CollectionEditPage-CLgQu2HS.js +1 -0
  12. package/admin/assets/CollectionsPage-BnCaxALz.js +1 -0
  13. package/admin/assets/ContentCreatePage-CJI326o-.js +1 -0
  14. package/admin/assets/ContentEditPage-A4i8P2Jd.js +3 -0
  15. package/admin/assets/ContentListPage-Bc4mBIkB.js +1 -0
  16. package/admin/assets/CustomBlockPreview-CCssn6vF.js +479 -0
  17. package/admin/assets/FieldBuilder-YJGSk0nY.js +3 -0
  18. package/admin/assets/LoginPage-Jrne8-Wr.js +1 -0
  19. package/admin/assets/MediaPage-DfPQBmNf.css +1 -0
  20. package/admin/assets/MediaPage-_qNXqsZg.js +1 -0
  21. package/admin/assets/SiteSettingsPage-CoZnavij.js +1 -0
  22. package/admin/assets/SitesPage-ETqFT3nO.js +1 -0
  23. package/admin/assets/TagsPage-BGpp0XZM.js +1 -0
  24. package/admin/assets/UsersPage-CKRJpAb6.js +1 -0
  25. package/admin/assets/WebhookEditPage-BOcLe5OJ.js +1 -0
  26. package/admin/assets/WebhooksPage-Czco583Y.js +1 -0
  27. package/admin/assets/badge-0Z1nL6DI.js +1 -0
  28. package/admin/assets/card-D1-S-QZ6.js +1 -0
  29. package/admin/assets/check-BGA0ADyt.js +1 -0
  30. package/admin/assets/checkbox-BPqrj_XS.js +1 -0
  31. package/admin/assets/command-ChD319uJ.js +1 -0
  32. package/admin/assets/contentStatus-DfWHjFVB.js +1 -0
  33. package/admin/assets/copy-BqH9rXYM.js +1 -0
  34. package/admin/assets/core.esm-Csvubn5Q.js +5 -0
  35. package/admin/assets/format-CZ9bpk32.js +1 -0
  36. package/admin/assets/index-DOqKbrpW.css +1 -0
  37. package/admin/assets/index-Ds50UTAc.js +18 -0
  38. package/admin/assets/lib-BrI1UB_t.js +38 -0
  39. package/admin/assets/media-url-DIg_vSyf.js +1 -0
  40. package/admin/assets/module-RjUF93sV.js +716 -0
  41. package/admin/assets/native-48B9X9Wg.js +1 -0
  42. package/admin/assets/plus-BgHSYWJN.js +1 -0
  43. package/admin/assets/radix-DQ3amgxj.js +51 -0
  44. package/admin/assets/react-vendor-DNVhVxD7.js +4 -0
  45. package/admin/assets/search-DIzcfCVh.js +1 -0
  46. package/admin/assets/select-CJXZv4wv.js +1 -0
  47. package/admin/assets/sortable.esm-Zh-9QRSf.js +1 -0
  48. package/admin/assets/table-B3EHrN_H.js +1 -0
  49. package/admin/assets/tanstack-BO6c-AOu.js +1 -0
  50. package/admin/assets/trash-2-Gny2Upn-.js +1 -0
  51. package/admin/assets/useAdminResolver-BsQc_N4z.js +1 -0
  52. package/admin/assets/useContent-CSobIico.js +1 -0
  53. package/admin/assets/useDebouncedValue-Bf8UizjU.js +1 -0
  54. package/admin/assets/useMedia-CQnmMz4N.js +1 -0
  55. package/admin/assets/useTags-CYqbj5cK.js +1 -0
  56. package/admin/assets/useWebhooks-DXgtQ3aU.js +1 -0
  57. package/admin/index.html +21 -0
  58. package/admin/vite.svg +1 -0
  59. package/dist/admin-site.d.ts +30 -0
  60. package/dist/admin-site.d.ts.map +1 -0
  61. package/dist/admin-site.js +80 -0
  62. package/dist/admin-site.js.map +1 -0
  63. package/dist/api.d.ts +26 -0
  64. package/dist/api.d.ts.map +1 -0
  65. package/dist/api.js +91 -0
  66. package/dist/api.js.map +1 -0
  67. package/dist/auth.d.ts +27 -0
  68. package/dist/auth.d.ts.map +1 -0
  69. package/dist/auth.js +86 -0
  70. package/dist/auth.js.map +1 -0
  71. package/dist/cdn.d.ts +27 -0
  72. package/dist/cdn.d.ts.map +1 -0
  73. package/dist/cdn.js +382 -0
  74. package/dist/cdn.js.map +1 -0
  75. package/dist/image.d.ts +21 -0
  76. package/dist/image.d.ts.map +1 -0
  77. package/dist/image.js +48 -0
  78. package/dist/image.js.map +1 -0
  79. package/dist/index.d.ts +85 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +124 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/storage.d.ts +21 -0
  84. package/dist/storage.d.ts.map +1 -0
  85. package/dist/storage.js +125 -0
  86. package/dist/storage.js.map +1 -0
  87. package/dist/webhooks.d.ts +23 -0
  88. package/dist/webhooks.d.ts.map +1 -0
  89. package/dist/webhooks.js +91 -0
  90. package/dist/webhooks.js.map +1 -0
  91. package/lambda/api/bootstrap +0 -0
  92. package/lambda/api/resource.enc +0 -0
  93. package/lambda/functions/custom-message/index.mjs +112 -0
  94. package/lambda/functions/custom-message/resource.enc +1 -0
  95. package/lambda/image-lambda/index.mjs +188 -0
  96. package/lambda/image-lambda/node_modules/.package-lock.json +160 -0
  97. package/lambda/image-lambda/node_modules/@img/sharp-libvips-linux-arm64/README.md +46 -0
  98. package/lambda/image-lambda/node_modules/@img/sharp-libvips-linux-arm64/lib/glib-2.0/include/glibconfig.h +220 -0
  99. package/lambda/image-lambda/node_modules/@img/sharp-libvips-linux-arm64/lib/index.js +1 -0
  100. package/lambda/image-lambda/node_modules/@img/sharp-libvips-linux-arm64/lib/libvips-cpp.so.42 +0 -0
  101. package/lambda/image-lambda/node_modules/@img/sharp-libvips-linux-arm64/package.json +42 -0
  102. package/lambda/image-lambda/node_modules/@img/sharp-libvips-linux-arm64/versions.json +30 -0
  103. package/lambda/image-lambda/node_modules/@img/sharp-linux-arm64/LICENSE +191 -0
  104. package/lambda/image-lambda/node_modules/@img/sharp-linux-arm64/README.md +18 -0
  105. package/lambda/image-lambda/node_modules/@img/sharp-linux-arm64/lib/sharp-linux-arm64.node +0 -0
  106. package/lambda/image-lambda/node_modules/@img/sharp-linux-arm64/package.json +46 -0
  107. package/lambda/image-lambda/node_modules/color/LICENSE +21 -0
  108. package/lambda/image-lambda/node_modules/color/README.md +123 -0
  109. package/lambda/image-lambda/node_modules/color/index.js +496 -0
  110. package/lambda/image-lambda/node_modules/color/package.json +47 -0
  111. package/lambda/image-lambda/node_modules/color-convert/CHANGELOG.md +54 -0
  112. package/lambda/image-lambda/node_modules/color-convert/LICENSE +21 -0
  113. package/lambda/image-lambda/node_modules/color-convert/README.md +68 -0
  114. package/lambda/image-lambda/node_modules/color-convert/conversions.js +839 -0
  115. package/lambda/image-lambda/node_modules/color-convert/index.js +81 -0
  116. package/lambda/image-lambda/node_modules/color-convert/package.json +48 -0
  117. package/lambda/image-lambda/node_modules/color-convert/route.js +97 -0
  118. package/lambda/image-lambda/node_modules/color-name/LICENSE +8 -0
  119. package/lambda/image-lambda/node_modules/color-name/README.md +11 -0
  120. package/lambda/image-lambda/node_modules/color-name/index.js +152 -0
  121. package/lambda/image-lambda/node_modules/color-name/package.json +28 -0
  122. package/lambda/image-lambda/node_modules/color-string/LICENSE +21 -0
  123. package/lambda/image-lambda/node_modules/color-string/README.md +62 -0
  124. package/lambda/image-lambda/node_modules/color-string/index.js +242 -0
  125. package/lambda/image-lambda/node_modules/color-string/package.json +39 -0
  126. package/lambda/image-lambda/node_modules/detect-libc/LICENSE +201 -0
  127. package/lambda/image-lambda/node_modules/detect-libc/README.md +163 -0
  128. package/lambda/image-lambda/node_modules/detect-libc/index.d.ts +14 -0
  129. package/lambda/image-lambda/node_modules/detect-libc/lib/detect-libc.js +313 -0
  130. package/lambda/image-lambda/node_modules/detect-libc/lib/elf.js +39 -0
  131. package/lambda/image-lambda/node_modules/detect-libc/lib/filesystem.js +51 -0
  132. package/lambda/image-lambda/node_modules/detect-libc/lib/process.js +24 -0
  133. package/lambda/image-lambda/node_modules/detect-libc/package.json +44 -0
  134. package/lambda/image-lambda/node_modules/is-arrayish/LICENSE +21 -0
  135. package/lambda/image-lambda/node_modules/is-arrayish/README.md +16 -0
  136. package/lambda/image-lambda/node_modules/is-arrayish/index.js +9 -0
  137. package/lambda/image-lambda/node_modules/is-arrayish/package.json +45 -0
  138. package/lambda/image-lambda/node_modules/semver/LICENSE +15 -0
  139. package/lambda/image-lambda/node_modules/semver/README.md +665 -0
  140. package/lambda/image-lambda/node_modules/semver/bin/semver.js +191 -0
  141. package/lambda/image-lambda/node_modules/semver/classes/comparator.js +143 -0
  142. package/lambda/image-lambda/node_modules/semver/classes/index.js +7 -0
  143. package/lambda/image-lambda/node_modules/semver/classes/range.js +557 -0
  144. package/lambda/image-lambda/node_modules/semver/classes/semver.js +333 -0
  145. package/lambda/image-lambda/node_modules/semver/functions/clean.js +8 -0
  146. package/lambda/image-lambda/node_modules/semver/functions/cmp.js +54 -0
  147. package/lambda/image-lambda/node_modules/semver/functions/coerce.js +62 -0
  148. package/lambda/image-lambda/node_modules/semver/functions/compare-build.js +9 -0
  149. package/lambda/image-lambda/node_modules/semver/functions/compare-loose.js +5 -0
  150. package/lambda/image-lambda/node_modules/semver/functions/compare.js +7 -0
  151. package/lambda/image-lambda/node_modules/semver/functions/diff.js +60 -0
  152. package/lambda/image-lambda/node_modules/semver/functions/eq.js +5 -0
  153. package/lambda/image-lambda/node_modules/semver/functions/gt.js +5 -0
  154. package/lambda/image-lambda/node_modules/semver/functions/gte.js +5 -0
  155. package/lambda/image-lambda/node_modules/semver/functions/inc.js +21 -0
  156. package/lambda/image-lambda/node_modules/semver/functions/lt.js +5 -0
  157. package/lambda/image-lambda/node_modules/semver/functions/lte.js +5 -0
  158. package/lambda/image-lambda/node_modules/semver/functions/major.js +5 -0
  159. package/lambda/image-lambda/node_modules/semver/functions/minor.js +5 -0
  160. package/lambda/image-lambda/node_modules/semver/functions/neq.js +5 -0
  161. package/lambda/image-lambda/node_modules/semver/functions/parse.js +18 -0
  162. package/lambda/image-lambda/node_modules/semver/functions/patch.js +5 -0
  163. package/lambda/image-lambda/node_modules/semver/functions/prerelease.js +8 -0
  164. package/lambda/image-lambda/node_modules/semver/functions/rcompare.js +5 -0
  165. package/lambda/image-lambda/node_modules/semver/functions/rsort.js +5 -0
  166. package/lambda/image-lambda/node_modules/semver/functions/satisfies.js +12 -0
  167. package/lambda/image-lambda/node_modules/semver/functions/sort.js +5 -0
  168. package/lambda/image-lambda/node_modules/semver/functions/valid.js +8 -0
  169. package/lambda/image-lambda/node_modules/semver/index.js +91 -0
  170. package/lambda/image-lambda/node_modules/semver/internal/constants.js +37 -0
  171. package/lambda/image-lambda/node_modules/semver/internal/debug.js +11 -0
  172. package/lambda/image-lambda/node_modules/semver/internal/identifiers.js +29 -0
  173. package/lambda/image-lambda/node_modules/semver/internal/lrucache.js +42 -0
  174. package/lambda/image-lambda/node_modules/semver/internal/parse-options.js +17 -0
  175. package/lambda/image-lambda/node_modules/semver/internal/re.js +223 -0
  176. package/lambda/image-lambda/node_modules/semver/package.json +78 -0
  177. package/lambda/image-lambda/node_modules/semver/preload.js +4 -0
  178. package/lambda/image-lambda/node_modules/semver/range.bnf +16 -0
  179. package/lambda/image-lambda/node_modules/semver/ranges/gtr.js +6 -0
  180. package/lambda/image-lambda/node_modules/semver/ranges/intersects.js +9 -0
  181. package/lambda/image-lambda/node_modules/semver/ranges/ltr.js +6 -0
  182. package/lambda/image-lambda/node_modules/semver/ranges/max-satisfying.js +27 -0
  183. package/lambda/image-lambda/node_modules/semver/ranges/min-satisfying.js +26 -0
  184. package/lambda/image-lambda/node_modules/semver/ranges/min-version.js +63 -0
  185. package/lambda/image-lambda/node_modules/semver/ranges/outside.js +82 -0
  186. package/lambda/image-lambda/node_modules/semver/ranges/simplify.js +49 -0
  187. package/lambda/image-lambda/node_modules/semver/ranges/subset.js +249 -0
  188. package/lambda/image-lambda/node_modules/semver/ranges/to-comparators.js +10 -0
  189. package/lambda/image-lambda/node_modules/semver/ranges/valid.js +13 -0
  190. package/lambda/image-lambda/node_modules/sharp/LICENSE +191 -0
  191. package/lambda/image-lambda/node_modules/sharp/README.md +118 -0
  192. package/lambda/image-lambda/node_modules/sharp/install/check.js +41 -0
  193. package/lambda/image-lambda/node_modules/sharp/lib/channel.js +174 -0
  194. package/lambda/image-lambda/node_modules/sharp/lib/colour.js +180 -0
  195. package/lambda/image-lambda/node_modules/sharp/lib/composite.js +210 -0
  196. package/lambda/image-lambda/node_modules/sharp/lib/constructor.js +452 -0
  197. package/lambda/image-lambda/node_modules/sharp/lib/index.d.ts +1754 -0
  198. package/lambda/image-lambda/node_modules/sharp/lib/index.js +16 -0
  199. package/lambda/image-lambda/node_modules/sharp/lib/input.js +658 -0
  200. package/lambda/image-lambda/node_modules/sharp/lib/is.js +169 -0
  201. package/lambda/image-lambda/node_modules/sharp/lib/libvips.js +203 -0
  202. package/lambda/image-lambda/node_modules/sharp/lib/operation.js +958 -0
  203. package/lambda/image-lambda/node_modules/sharp/lib/output.js +1587 -0
  204. package/lambda/image-lambda/node_modules/sharp/lib/resize.js +587 -0
  205. package/lambda/image-lambda/node_modules/sharp/lib/sharp.js +114 -0
  206. package/lambda/image-lambda/node_modules/sharp/lib/utility.js +296 -0
  207. package/lambda/image-lambda/node_modules/sharp/package.json +222 -0
  208. package/lambda/image-lambda/node_modules/sharp/src/binding.gyp +280 -0
  209. package/lambda/image-lambda/node_modules/sharp/src/common.cc +1091 -0
  210. package/lambda/image-lambda/node_modules/sharp/src/common.h +393 -0
  211. package/lambda/image-lambda/node_modules/sharp/src/metadata.cc +320 -0
  212. package/lambda/image-lambda/node_modules/sharp/src/metadata.h +85 -0
  213. package/lambda/image-lambda/node_modules/sharp/src/operations.cc +475 -0
  214. package/lambda/image-lambda/node_modules/sharp/src/operations.h +125 -0
  215. package/lambda/image-lambda/node_modules/sharp/src/pipeline.cc +1758 -0
  216. package/lambda/image-lambda/node_modules/sharp/src/pipeline.h +393 -0
  217. package/lambda/image-lambda/node_modules/sharp/src/sharp.cc +40 -0
  218. package/lambda/image-lambda/node_modules/sharp/src/stats.cc +183 -0
  219. package/lambda/image-lambda/node_modules/sharp/src/stats.h +59 -0
  220. package/lambda/image-lambda/node_modules/sharp/src/utilities.cc +269 -0
  221. package/lambda/image-lambda/node_modules/sharp/src/utilities.h +19 -0
  222. package/lambda/image-lambda/node_modules/simple-swizzle/LICENSE +21 -0
  223. package/lambda/image-lambda/node_modules/simple-swizzle/README.md +43 -0
  224. package/lambda/image-lambda/node_modules/simple-swizzle/index.js +29 -0
  225. package/lambda/image-lambda/node_modules/simple-swizzle/package.json +36 -0
  226. package/lambda/webhook-worker/bootstrap +0 -0
  227. package/lambda/webhook-worker/resource.enc +0 -0
  228. package/package.json +50 -0
  229. package/src/admin-site.ts +108 -0
  230. package/src/api.ts +113 -0
  231. package/src/auth.ts +110 -0
  232. package/src/cdn.ts +449 -0
  233. package/src/image.ts +62 -0
  234. package/src/index.ts +216 -0
  235. package/src/sst-env.d.ts +143 -0
  236. package/src/storage.ts +138 -0
  237. package/src/webhooks.ts +114 -0
package/README.md ADDED
@@ -0,0 +1,282 @@
1
+ # Headroom CMS
2
+
3
+ Serverless headless CMS for AWS, deployed with [SST](https://sst.dev).
4
+
5
+ Headroom provisions a complete CMS stack — DynamoDB, S3, Lambda, CloudFront, Cognito, and an admin UI — from a single SST component. No Go toolchain or build step required; pre-compiled binaries and a pre-built admin UI are included in the package.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npm create headroom my-cms
11
+ cd my-cms
12
+ npx sst deploy --stage production
13
+ ./scripts/create-admin.sh admin@example.com 'YourPassword123!'
14
+ ```
15
+
16
+ After deploying, the CLI prints three URLs:
17
+
18
+ - **API** — the Lambda function URL for the content API
19
+ - **CDN** — the CloudFront distribution (cached, edge-authenticated)
20
+ - **Admin** — the admin UI
21
+
22
+ Open the Admin URL and sign in with the credentials you just created.
23
+
24
+ ## Prerequisites
25
+
26
+ - **Node.js 22+** and **pnpm** (or npm)
27
+ - **AWS credentials** configured (`aws configure` or environment variables)
28
+ - **SST v3** (installed automatically as a dev dependency)
29
+ - **AWS CLI** (for the `create-admin.sh` and `get-token.sh` helper scripts)
30
+ - **jq** (used by helper scripts to read SST outputs)
31
+
32
+ ## Configuration Reference
33
+
34
+ Your `sst.config.ts` imports `HeadroomCMS` and passes a configuration object:
35
+
36
+ ```typescript
37
+ /// <reference path="./.sst/platform/config.d.ts" />
38
+ import { HeadroomCMS } from "headroom-cms";
39
+
40
+ export default $config({
41
+ app(input) {
42
+ return {
43
+ name: "my-cms",
44
+ removal: input?.stage === "production" ? "retain" : "remove",
45
+ protect: input?.stage === "production",
46
+ home: "aws",
47
+ };
48
+ },
49
+ async run() {
50
+ const cms = new HeadroomCMS("CMS", {
51
+ senderEmail: "cms@mycompany.com",
52
+
53
+ // All options below are optional
54
+ domain: {
55
+ name: "api.mycompany.com",
56
+ certificateArn: "arn:aws:acm:us-east-1:...:certificate/...",
57
+ },
58
+ adminDomain: {
59
+ name: "cms.mycompany.com",
60
+ certificateArn: "arn:aws:acm:us-east-1:...:certificate/...",
61
+ },
62
+ priceClass: "PriceClass_100",
63
+ apiCacheTtl: 3600,
64
+ passwordPolicy: {
65
+ minimumLength: 12,
66
+ requireLowercase: true,
67
+ requireUppercase: true,
68
+ requireNumbers: true,
69
+ requireSymbols: true,
70
+ },
71
+ });
72
+
73
+ return cms.outputs;
74
+ },
75
+ });
76
+ ```
77
+
78
+ ### Options
79
+
80
+ | Option | Type | Default | Description |
81
+ |--------|------|---------|-------------|
82
+ | `senderEmail` | `string` | **(required)** | Email for admin invitations and password resets. Must be verified in SES (see [Troubleshooting](#troubleshooting)). |
83
+ | `domain` | `{ name, certificateArn }` | CloudFront default | Custom domain for the CDN / API endpoint. |
84
+ | `adminDomain` | `{ name, certificateArn }` | CloudFront default | Custom domain for the admin UI. |
85
+ | `priceClass` | `"PriceClass_100"` \| `"PriceClass_200"` \| `"PriceClass_All"` | `"PriceClass_100"` | CloudFront price class. `100` = US/Canada/Europe, `200` adds Asia/Africa, `All` = global. |
86
+ | `apiCacheTtl` | `number` | `3600` | API response cache TTL in seconds at the CloudFront edge. |
87
+ | `passwordPolicy` | `object` | See below | Cognito password policy overrides. |
88
+
89
+ #### Password Policy Defaults
90
+
91
+ | Field | Default |
92
+ |-------|---------|
93
+ | `minimumLength` | `12` |
94
+ | `requireLowercase` | `true` |
95
+ | `requireUppercase` | `true` |
96
+ | `requireNumbers` | `true` |
97
+ | `requireSymbols` | `true` |
98
+
99
+ ### Outputs
100
+
101
+ The component returns these outputs (available in `.sst/outputs.json` after deploy):
102
+
103
+ | Output | Description |
104
+ |--------|-------------|
105
+ | `api` | Lambda function URL for the Go API |
106
+ | `cdn` | CloudFront distribution URL (cached, edge-authenticated) |
107
+ | `admin` | Admin UI URL |
108
+ | `userPoolId` | Cognito User Pool ID |
109
+ | `userPoolClientId` | Cognito User Pool Client ID |
110
+
111
+ ## Custom Domains
112
+
113
+ To use custom domains for the CDN or admin UI:
114
+
115
+ 1. **Request an ACM certificate** in **us-east-1** (required for CloudFront):
116
+
117
+ ```bash
118
+ aws acm request-certificate \
119
+ --domain-name api.mycompany.com \
120
+ --validation-method DNS \
121
+ --region us-east-1
122
+ ```
123
+
124
+ 2. **Validate the certificate** by adding the DNS records ACM provides (check the AWS Console or use `aws acm describe-certificate`).
125
+
126
+ 3. **Add the domain config** to your `sst.config.ts`:
127
+
128
+ ```typescript
129
+ const cms = new HeadroomCMS("CMS", {
130
+ senderEmail: "cms@mycompany.com",
131
+ domain: {
132
+ name: "api.mycompany.com",
133
+ certificateArn: "arn:aws:acm:us-east-1:123456789:certificate/abc-123",
134
+ },
135
+ });
136
+ ```
137
+
138
+ 4. **Deploy**, then create a **CNAME** (or alias) DNS record pointing your domain to the CloudFront distribution domain shown in the deploy output.
139
+
140
+ Repeat for `adminDomain` if you want a custom domain for the admin UI.
141
+
142
+ ## Updating
143
+
144
+ ```bash
145
+ pnpm update headroom-cms
146
+ npx sst deploy --stage production
147
+ ```
148
+
149
+ SST detects changes in the component and updates Lambda code, admin UI assets, and any infrastructure changes automatically.
150
+
151
+ ### Version Policy
152
+
153
+ Headroom follows semantic versioning:
154
+
155
+ - **Patch** (0.1.x) — Bug fixes, no infrastructure changes
156
+ - **Minor** (0.x.0) — New features, backward-compatible infrastructure additions
157
+ - **Major** (x.0.0) — Breaking changes to config interface or infrastructure that may require manual steps
158
+
159
+ ## Troubleshooting
160
+
161
+ ### SES Email Verification
162
+
163
+ Headroom creates an SES email identity for your `senderEmail` automatically, but AWS requires you to verify it. Check the inbox for that address and click the verification link. Until verified, admin invitation emails won't send.
164
+
165
+ If your AWS account is still in the SES sandbox, you can only send to verified email addresses. Request production access in the AWS Console under SES > Account dashboard.
166
+
167
+ ### IAM Permissions
168
+
169
+ The deploying user/role needs broad permissions to create DynamoDB tables, S3 buckets, Lambda functions, CloudFront distributions, Cognito user pools, SQS queues, and IAM roles. If you get `AccessDenied` errors during deploy, ensure your credentials have `AdministratorAccess` or an equivalent policy.
170
+
171
+ ### Deploy Errors
172
+
173
+ - **"Resource already exists"** — You may have resources from a previous deployment in the same AWS account/region. Use a different SST stage name or clean up the old resources.
174
+ - **"CREATE_FAILED ... custom-message Lambda"** — This usually means the Node.js runtime version isn't available in your region. Ensure you're deploying to a standard region.
175
+ - **Timeout during deploy** — CloudFront distribution creation can take 5-15 minutes on first deploy. This is normal.
176
+
177
+ ### Helper Scripts
178
+
179
+ The scaffolded project includes two helper scripts in `scripts/`:
180
+
181
+ - **`create-admin.sh <email> <password>`** — Creates a Cognito admin user. Password must be 12+ characters with uppercase, lowercase, numbers, and symbols.
182
+ - **`get-token.sh <email> <password>`** — Returns a JWT access token for testing admin API endpoints.
183
+
184
+ Both scripts read from `.sst/outputs.json`, so you must deploy before running them.
185
+
186
+ ## What Gets Deployed
187
+
188
+ A single `HeadroomCMS` component creates:
189
+
190
+ - **11 DynamoDB tables** (sites, content, collections, block types, media, API keys, tags, audit, relationships, webhooks, webhook deliveries)
191
+ - **1 S3 bucket** (media files and content bodies)
192
+ - **1 CloudFront KVS** (edge auth cache and site version tracking)
193
+ - **1 Cognito User Pool + Client** (admin authentication)
194
+ - **1 SES Email Identity** (admin invitations)
195
+ - **4 Lambda functions** (API, webhook worker, image transform, custom message)
196
+ - **1 CloudFront Distribution** with 2 CloudFront Functions (edge auth, media URL rewrite)
197
+ - **1 Admin UI** (static site on CloudFront)
198
+ - **2 SQS queues** (webhook delivery + dead letter queue)
199
+
200
+ ## Advanced: Forking
201
+
202
+ If you need to modify Go API handlers, add custom admin UI pages, change DynamoDB schemas, or make other changes that the configuration options don't cover, you can eject to a full source checkout.
203
+
204
+ This is a one-way migration: once ejected, updates come from merging upstream changes rather than `pnpm update`.
205
+
206
+ ### When to Eject
207
+
208
+ The packaged component covers most use cases. Eject only when you need to:
209
+
210
+ - Add custom API endpoints or middleware in the Go Lambda
211
+ - Add custom admin UI pages or block editor plugins
212
+ - Change DynamoDB table schemas
213
+ - Wire additional Lambda functions into the same infrastructure
214
+ - Replace a subsystem entirely (e.g., swap Cognito for a different auth provider)
215
+
216
+ ### Ejection Steps
217
+
218
+ 1. **Clone at your current version:**
219
+
220
+ ```bash
221
+ git clone --branch v0.1.0 https://github.com/cykod/headroom.git my-cms-custom
222
+ cd my-cms-custom
223
+ pnpm install
224
+ ```
225
+
226
+ 2. **Copy your SST state** so SST recognizes existing AWS resources:
227
+
228
+ ```bash
229
+ cp -r ../my-cms/.sst ./
230
+ ```
231
+
232
+ 3. **Update `sst.config.ts`** to use local source with the `dev` option:
233
+
234
+ ```typescript
235
+ /// <reference path="./.sst/platform/config.d.ts" />
236
+ import { HeadroomCMS } from "./packages/headroom-cms/src/index.js";
237
+
238
+ export default $config({
239
+ app(input) {
240
+ return {
241
+ name: "my-cms", // must match your previous app name
242
+ removal: input?.stage === "production" ? "retain" : "remove",
243
+ protect: input?.stage === "production",
244
+ home: "aws",
245
+ };
246
+ },
247
+ async run() {
248
+ const cms = new HeadroomCMS("CMS", {
249
+ senderEmail: "cms@mycompany.com",
250
+ dev: {
251
+ apiHandler: "packages/api",
252
+ webhookWorkerHandler: "packages/webhook-worker",
253
+ customMessageHandler: "packages/functions/custom-message.handler",
254
+ imageLambdaHandler: "packages/image-lambda/index.handler",
255
+ adminPath: "packages/admin",
256
+ },
257
+ });
258
+
259
+ return cms.outputs;
260
+ },
261
+ });
262
+ ```
263
+
264
+ 4. **Deploy to verify** the state transfer (should be a no-op if config matches):
265
+
266
+ ```bash
267
+ npx sst deploy --stage production
268
+ ```
269
+
270
+ 5. **Start customizing.** Key directories:
271
+
272
+ | Directory | What it is |
273
+ |-----------|-----------|
274
+ | `packages/api/` | Go Lambda API — add routes in `main.go`, handlers in `internal/handler/` |
275
+ | `packages/admin/` | React admin UI — add pages in `src/pages/`, components in `src/components/` |
276
+ | `packages/headroom-cms/src/` | SST infrastructure component — modify DynamoDB schemas, add resources |
277
+ | `packages/image-lambda/` | Sharp image transform Lambda |
278
+ | `packages/webhook-worker/` | Webhook delivery worker |
279
+
280
+ ## License
281
+
282
+ PolyForm Noncommercial 1.0.0
@@ -0,0 +1 @@
1
+ import{j as e}from"./tanstack-BO6c-AOu.js";import{u as H,r as x}from"./react-vendor-DNVhVxD7.js";import{n as L,u as q,a2 as B,a3 as _,o as z,a4 as M,a5 as V,S as y,D as G,h as $,B as l,i as J,j as K,k as Q,l as W,I as X,m as Y,s as i}from"./index-Ds50UTAc.js";import{u as Z}from"./useAdminResolver-BsQc_N4z.js";import{T as ee,a as se,b,c as p,d as ae,e as j}from"./table-B3EHrN_H.js";import{P as ie,a as te,b as ne,C as re,c as oe,d as le,e as de,f as ce,g as me}from"./command-ChD319uJ.js";import{S as ue,a as he,b as xe,c as pe,d as C}from"./select-CJXZv4wv.js";import{P as je}from"./plus-BgHSYWJN.js";import{T as ve}from"./trash-2-Gny2Upn-.js";import"./radix-DQ3amgxj.js";import"./search-DIzcfCVh.js";import"./check-BGA0ADyt.js";const fe=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["line",{x1:"19",x2:"19",y1:"8",y2:"14",key:"1bvyxn"}],["line",{x1:"22",x2:"16",y1:"11",y2:"11",key:"1shjgl"}]],ge=L("user-plus",fe);function Re(){const{host:d}=H(),{user:N}=q(),S=B(),{data:A,isLoading:I}=_(d),{data:T}=z(),c=M(d),m=V(d),n=A?.admins??[],E=T?.items??[],r=S==="admin",v=n.map(s=>s.id),{resolve:w}=Z(v),[D,f]=x.useState(!1),[P,g]=x.useState(!1),[u,h]=x.useState("");if(I)return e.jsxs("div",{className:"space-y-2",children:[e.jsx(y,{className:"h-8 w-64"}),Array.from({length:3}).map((s,a)=>e.jsx(y,{className:"h-12 w-full"},a))]});const R=E.filter(s=>!v.includes(s.sub));async function k(s){try{await c.mutateAsync({admins:[...n,{id:s,role:"editor"}]}),i.success("Admin added"),f(!1)}catch(a){i.error("Failed to add admin",{description:a instanceof Error?a.message:void 0})}}async function U(s){if(s.preventDefault(),!!u.trim())try{const a=await m.mutateAsync({email:u.trim()});a.existed?i.success("Existing user added to site"):i.success("Invite email sent",{description:`An invite has been sent to ${a.email}`}),h(""),g(!1)}catch(a){i.error("Failed to invite user",{description:a instanceof Error?a.message:void 0})}}async function O(s){try{await c.mutateAsync({admins:n.filter(a=>a.id!==s)}),i.success("Admin removed")}catch(a){i.error("Failed to remove admin",{description:a instanceof Error?a.message:void 0})}}async function F(s,a){const o=n.map(t=>t.id===s?{...t,role:a}:t);if(!o.some(t=>t.role==="admin")){i.error("At least one admin role is required");return}try{await c.mutateAsync({admins:o}),i.success("Role updated")}catch(t){i.error("Failed to update role",{description:t instanceof Error?t.message:void 0})}}return e.jsxs("div",{children:[e.jsx("h1",{className:"mb-6 text-2xl font-semibold",children:"Admins"}),r&&e.jsxs("div",{className:"mb-6 flex gap-2",children:[e.jsxs(G,{open:P,onOpenChange:s=>{g(s),s||h("")},children:[e.jsx($,{asChild:!0,children:e.jsxs(l,{children:[e.jsx(ge,{className:"mr-2 h-4 w-4"}),"Invite by email"]})}),e.jsxs(J,{children:[e.jsxs(K,{children:[e.jsx(Q,{children:"Invite User to Site"}),e.jsx(W,{className:"sr-only",children:"Send an invite email to add a user to this site"})]}),e.jsxs("form",{onSubmit:U,className:"space-y-4",children:[e.jsx("p",{className:"text-muted-foreground text-sm",children:"Enter an email address. If the user doesn't have an account yet, they'll receive an invite email."}),e.jsx(X,{type:"email",value:u,onChange:s=>h(s.target.value),placeholder:"email@example.com",required:!0}),e.jsx(Y,{children:e.jsx(l,{type:"submit",disabled:m.isPending,children:m.isPending?"Inviting...":"Send Invite"})})]})]})]}),e.jsxs(ie,{open:D,onOpenChange:f,children:[e.jsx(te,{asChild:!0,children:e.jsxs(l,{variant:"outline",children:[e.jsx(je,{className:"mr-2 h-4 w-4"}),"Add existing user"]})}),e.jsx(ne,{className:"w-72 p-0",align:"start",children:e.jsxs(re,{children:[e.jsx(oe,{placeholder:"Search users..."}),e.jsxs(le,{children:[e.jsx(de,{children:"No users found."}),e.jsx(ce,{children:R.map(s=>e.jsx(me,{value:s.email,onSelect:()=>k(s.sub),children:s.email},s.sub))})]})]})})]})]}),n.length===0?e.jsx("p",{className:"text-muted-foreground",children:"No admins configured."}):e.jsxs(ee,{children:[e.jsx(se,{children:e.jsxs(b,{children:[e.jsx(p,{children:"Email"}),e.jsx(p,{children:"Role"}),r&&e.jsx(p,{})]})}),e.jsx(ae,{children:n.map(s=>{const a=s.id===N?.sub;return e.jsxs(b,{children:[e.jsxs(j,{className:"text-sm",children:[w(s.id),a&&e.jsx("span",{className:"ml-2 text-muted-foreground",children:"(you)"})]}),e.jsx(j,{children:r&&!a?e.jsxs(ue,{value:s.role,onValueChange:o=>F(s.id,o),children:[e.jsx(he,{className:"w-28",children:e.jsx(xe,{})}),e.jsxs(pe,{children:[e.jsx(C,{value:"admin",children:"Admin"}),e.jsx(C,{value:"editor",children:"Editor"})]})]}):e.jsx("span",{className:"text-sm capitalize",children:s.role})}),r&&e.jsx(j,{className:"text-right",children:!a&&e.jsx(l,{variant:"ghost",size:"sm",onClick:()=>O(s.id),title:"Remove from site",children:e.jsx(ve,{className:"h-4 w-4"})})})]},s.id)})})]})]})}export{Re as AdminsPage};
@@ -0,0 +1 @@
1
+ import{j as s}from"./tanstack-BO6c-AOu.js";import{u as P,e as T,r as p}from"./react-vendor-DNVhVxD7.js";import{u as U,a as I,b as D,c as L}from"./useContent-CSobIico.js";import{m as M}from"./media-url-DIg_vSyf.js";import{t as R,s as c,S as z,B as E}from"./index-Ds50UTAc.js";import{B as g}from"./badge-0Z1nL6DI.js";import{C as b}from"./checkbox-BPqrj_XS.js";import{T as H,a as F,b as S,c as o,d as V,e as r}from"./table-B3EHrN_H.js";import{f as _}from"./format-CZ9bpk32.js";import{B as q}from"./BulkActionBar-BMcUBJSH.js";import"./radix-DQ3amgxj.js";import"./check-BGA0ADyt.js";function le(){const{host:i}=P(),k=T(),{collections:C}=R(),[w,N]=p.useState(),[n,a]=p.useState(new Set),{data:d,isLoading:v}=U(i,w),m=I(i),x=D(i),f=L(i),y=new Map(C.map(e=>[e.name,e.label])),h=d?.items.map(e=>e.contentId)??[],j=h.length>0&&h.every(e=>n.has(e));function A(e){a(l=>{const t=new Set(l);return t.has(e)?t.delete(e):t.add(e),t})}function $(){a(j?new Set:new Set(h))}const B=m.isPending||x.isPending||f.isPending;return s.jsxs("div",{children:[s.jsxs("div",{className:"mb-6",children:[s.jsx("h1",{className:"text-2xl font-semibold",children:"All Content"}),s.jsx("p",{className:"text-muted-foreground text-sm",children:"Recently updated content across all collections."})]}),n.size>0&&s.jsx(q,{selectedCount:n.size,onPublish:async()=>{const e=[...n],t=(await m.mutateAsync(e)).filter(u=>u.status==="rejected").length;t?c.error(`${t} of ${e.length} failed to publish`):c.success(`${e.length} item(s) published`),a(new Set)},onUnpublish:async()=>{const e=[...n],t=(await x.mutateAsync(e)).filter(u=>u.status==="rejected").length;t?c.error(`${t} of ${e.length} failed to unpublish`):c.success(`${e.length} item(s) unpublished`),a(new Set)},onDelete:async()=>{const e=[...n],t=(await f.mutateAsync(e)).filter(u=>u.status==="rejected").length;t?c.error(`${t} of ${e.length} failed to delete`):c.success(`${e.length} item(s) deleted`),a(new Set)},onClear:()=>a(new Set),isPending:B}),v?s.jsx("div",{className:"space-y-2",children:Array.from({length:5}).map((e,l)=>s.jsx(z,{className:"h-12 w-full"},l))}):d?.items.length?s.jsxs(s.Fragment,{children:[s.jsxs(H,{children:[s.jsx(F,{children:s.jsxs(S,{children:[s.jsx(o,{className:"w-10",children:s.jsx(b,{checked:j,onCheckedChange:$,"aria-label":"Select all"})}),s.jsx(o,{children:"Title"}),s.jsx(o,{children:"Slug"}),s.jsx(o,{children:"Collection"}),s.jsx(o,{children:"Status"}),s.jsx(o,{children:"Updated"})]})}),s.jsx(V,{children:d.items.map(e=>s.jsxs(S,{className:"cursor-pointer",onClick:()=>k(`/sites/${i}/content/${e.collection}/${e.contentId}`),children:[s.jsx(r,{onClick:l=>l.stopPropagation(),children:s.jsx(b,{checked:n.has(e.contentId),onCheckedChange:()=>A(e.contentId),"aria-label":`Select ${e.title||"Untitled"}`})}),s.jsx(r,{className:"font-medium",children:s.jsxs("div",{className:"flex items-center gap-3",children:[!!e.coverUrl&&s.jsx("img",{src:M(e.coverUrl),alt:"",className:"h-8 w-8 rounded object-cover flex-shrink-0"}),s.jsx("span",{children:e.title||"Untitled"})]})}),s.jsx(r,{className:"text-muted-foreground text-sm",children:e.slug||"—"}),s.jsx(r,{children:s.jsx(g,{variant:"outline",children:y.get(e.collection)??e.collection})}),s.jsx(r,{children:s.jsx(g,{variant:e.publishedAt?"default":"secondary",children:e.publishedAt?"Published":"Draft"})}),s.jsx(r,{className:"text-muted-foreground",children:e.publishedAt?_(e.publishedAt):"—"})]},e.contentId))})]}),d.hasMore&&s.jsx("div",{className:"mt-4 flex justify-center",children:s.jsx(E,{variant:"outline",onClick:()=>N(d.cursor??void 0),children:"Load More"})})]}):s.jsx("p",{className:"text-muted-foreground",children:"No content yet."})]})}export{le as AllContentPage};
@@ -0,0 +1 @@
1
+ import{u as L,a as N,b as A,j as e}from"./tanstack-BO6c-AOu.js";import{u as E,r as m}from"./react-vendor-DNVhVxD7.js";import{E as f,B as r,S as q,D as K,i as D,j as w,k as I,l as T,I as g,m as p,L as O,s as x}from"./index-Ds50UTAc.js";import{T as G,a as Q,b as k,c as d,d as R,e as u}from"./table-B3EHrN_H.js";import{f as C}from"./format-CZ9bpk32.js";import{P as $}from"./plus-BgHSYWJN.js";import{T as B}from"./trash-2-Gny2Upn-.js";import{C as H}from"./copy-BqH9rXYM.js";import"./radix-DQ3amgxj.js";function U(s){const t=f();return L({queryKey:["sites",s,"api-keys"],queryFn:async()=>({items:(await t.apiFetch(`/v1/admin/sites/${s}/api-keys`)).keys}),enabled:!!s})}function z(s){const t=f(),n=N();return A({mutationFn:i=>t.apiFetch(`/v1/admin/sites/${s}/api-keys`,{method:"POST",body:JSON.stringify(i)}),onSuccess:()=>n.invalidateQueries({queryKey:["sites",s,"api-keys"]})})}function J(s,t){const n=f(),i=N();return A({mutationFn:()=>n.apiFetch(`/v1/admin/sites/${s}/api-keys/${t}`,{method:"DELETE"}),onSuccess:()=>i.invalidateQueries({queryKey:["sites",s,"api-keys"]})})}function te(){const{host:s}=E(),{data:t,isLoading:n}=U(s),i=z(s),[h,l]=m.useState(!1),[b,y]=m.useState(""),[o,j]=m.useState(null),[P,v]=m.useState(null);async function F(a){a.preventDefault();try{const c=await i.mutateAsync({label:b});j(c.key),y("")}catch(c){x.error("Failed to create API key",{description:c instanceof Error?c.message:void 0})}}function S(){o&&(navigator.clipboard.writeText(o),x.success("Copied to clipboard"))}return e.jsxs("div",{children:[e.jsxs("div",{className:"mb-6 flex items-center justify-between",children:[e.jsx("h1",{className:"text-2xl font-semibold",children:"API Keys"}),e.jsxs(r,{onClick:()=>l(!0),children:[e.jsx($,{className:"mr-2 h-4 w-4"}),"Generate Key"]})]}),n?e.jsx("div",{className:"space-y-2",children:Array.from({length:3}).map((a,c)=>e.jsx(q,{className:"h-12 w-full"},c))}):t?.items.length?e.jsxs(G,{children:[e.jsx(Q,{children:e.jsxs(k,{children:[e.jsx(d,{children:"Label"}),e.jsx(d,{children:"Key ID"}),e.jsx(d,{children:"Created"}),e.jsx(d,{children:"Last Used"}),e.jsx(d,{})]})}),e.jsx(R,{children:t.items.map(a=>e.jsxs(k,{children:[e.jsx(u,{className:"font-medium",children:a.label}),e.jsx(u,{className:"font-mono text-xs",children:a.keyId}),e.jsx(u,{className:"text-muted-foreground",children:C(a.createdAt)}),e.jsx(u,{className:"text-muted-foreground",children:a.lastUsedAt?C(a.lastUsedAt):"Never"}),e.jsx(u,{children:e.jsx(r,{variant:"ghost",size:"sm",onClick:()=>v(a.keyId),children:e.jsx(B,{className:"h-4 w-4"})})})]},a.keyId))})]}):e.jsx("p",{className:"text-muted-foreground",children:"No API keys."}),e.jsx(K,{open:h,onOpenChange:a=>{l(a),a||(j(null),y(""))},children:e.jsxs(D,{children:[e.jsxs(w,{children:[e.jsx(I,{children:o?"API Key Generated":"Generate API Key"}),e.jsx(T,{className:"sr-only",children:"Create a new API key for site access"})]}),o?e.jsxs("div",{className:"space-y-4",children:[e.jsx("p",{className:"text-sm text-muted-foreground",children:"Copy this key now. You won't be able to see it again."}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(g,{value:o,readOnly:!0,className:"font-mono text-xs"}),e.jsx(r,{variant:"outline",size:"icon",onClick:S,children:e.jsx(H,{className:"h-4 w-4"})})]}),e.jsx(p,{children:e.jsx(r,{onClick:()=>{l(!1),j(null)},children:"Done"})})]}):e.jsxs("form",{onSubmit:F,className:"space-y-4",children:[e.jsxs("div",{className:"space-y-2",children:[e.jsx(O,{htmlFor:"key-label",children:"Label"}),e.jsx(g,{id:"key-label",value:b,onChange:a=>y(a.target.value),required:!0,autoFocus:!0})]}),e.jsxs(p,{children:[e.jsx(r,{variant:"outline",onClick:()=>l(!1),children:"Cancel"}),e.jsx(r,{type:"submit",disabled:i.isPending,children:"Generate"})]})]})]})}),e.jsx(M,{host:s,keyId:P,onClose:()=>v(null)})]})}function M({host:s,keyId:t,onClose:n}){const i=J(s,t??"");async function h(){try{await i.mutateAsync(),x.success("API key revoked"),n()}catch(l){x.error("Failed to revoke",{description:l instanceof Error?l.message:void 0})}}return e.jsx(K,{open:!!t,onOpenChange:()=>n(),children:e.jsxs(D,{children:[e.jsxs(w,{children:[e.jsx(I,{children:"Revoke API key?"}),e.jsx(T,{children:"This action cannot be undone. Any applications using this key will lose access."})]}),e.jsxs(p,{children:[e.jsx(r,{variant:"outline",onClick:n,children:"Cancel"}),e.jsx(r,{variant:"destructive",onClick:h,children:"Revoke"})]})]})})}export{te as ApiKeysPage};
@@ -0,0 +1 @@
1
+ import{u as x,j as e}from"./tanstack-BO6c-AOu.js";import{u as h,r as f}from"./react-vendor-DNVhVxD7.js";import{E as j,S as b,B as g}from"./index-Ds50UTAc.js";import{u as p}from"./useAdminResolver-BsQc_N4z.js";import{T as A,a as N,b as c,c as n,d as T,e as o}from"./table-B3EHrN_H.js";import{f as y}from"./format-CZ9bpk32.js";import"./radix-DQ3amgxj.js";function S(a,t){const d=j(),s=new URLSearchParams;t?.action&&s.set("action",t.action),t?.before&&s.set("before",String(t.before));const r=s.toString();return x({queryKey:["sites",a,"audit",t?.action??"",t?.before??""],queryFn:()=>d.apiFetch(`/v1/admin/sites/${a}/audit${r?`?${r}`:""}`),enabled:!!a})}function C(){const{host:a}=h(),[t,d]=f.useState(),{data:s,isLoading:r}=S(a,{before:t}),l=s?.items.map(i=>i.adminId)??[],{resolve:m}=p(l);return e.jsxs("div",{children:[e.jsx("h1",{className:"mb-6 text-2xl font-semibold",children:"Audit Log"}),r?e.jsx("div",{className:"space-y-2",children:Array.from({length:5}).map((i,u)=>e.jsx(b,{className:"h-12 w-full"},u))}):s?.items.length?e.jsxs(e.Fragment,{children:[e.jsxs(A,{children:[e.jsx(N,{children:e.jsxs(c,{children:[e.jsx(n,{children:"Action"}),e.jsx(n,{children:"Admin"}),e.jsx(n,{children:"Status"}),e.jsx(n,{children:"Time"})]})}),e.jsx(T,{children:s.items.map(i=>e.jsxs(c,{children:[e.jsx(o,{className:"font-medium",children:i.action}),e.jsx(o,{className:"text-sm",children:m(i.adminId)}),e.jsx(o,{children:e.jsx("span",{className:"text-muted-foreground",children:i.status})}),e.jsx(o,{className:"text-muted-foreground",children:y(i.createdAt)})]},i.eventId))})]}),s.hasMore&&s.items.length>0&&e.jsx("div",{className:"mt-4 flex justify-center",children:e.jsx(g,{variant:"outline",onClick:()=>d(s.items[s.items.length-1].createdAt),children:"Load More"})})]}):e.jsx("p",{className:"text-muted-foreground",children:"No audit events."})]})}export{C as AuditPage};