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/src/image.ts ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Image Infrastructure
3
+ *
4
+ * Sharp-based image transformation Lambda with HMAC-signed URLs.
5
+ * Supports dev mode (Node.js source) and package mode (pre-bundled handler).
6
+ */
7
+
8
+ import path from "path";
9
+ import type { StorageResources } from "./storage.js";
10
+
11
+ export interface ImageArgs {
12
+ contentBucket: StorageResources["contentBucket"];
13
+ pkgRoot: string;
14
+ dev?: {
15
+ /** SST handler string, e.g. "packages/image-lambda/index.handler" */
16
+ handler: string;
17
+ };
18
+ }
19
+
20
+ export function createImage(name: string, args: ImageArgs) {
21
+ const imageSigningSecret = new sst.Secret(`${name}ImageSigningSecret`);
22
+
23
+ let imageLambda: sst.aws.Function;
24
+ if (args.dev) {
25
+ imageLambda = new sst.aws.Function(`${name}ImageLambda`, {
26
+ handler: args.dev.handler,
27
+ runtime: "nodejs22.x",
28
+ architecture: "arm64",
29
+ timeout: "30 seconds",
30
+ memory: "1536 MB",
31
+ url: {
32
+ authorization: "iam",
33
+ },
34
+ environment: {
35
+ CONTENT_BUCKET: args.contentBucket.name,
36
+ IMAGE_SIGNING_SECRET: imageSigningSecret.value,
37
+ },
38
+ link: [args.contentBucket, imageSigningSecret],
39
+ });
40
+ } else {
41
+ imageLambda = new sst.aws.Function(`${name}ImageLambda`, {
42
+ bundle: path.join(args.pkgRoot, "lambda/image-lambda"),
43
+ handler: "index.handler",
44
+ runtime: "nodejs22.x",
45
+ architecture: "arm64",
46
+ timeout: "30 seconds",
47
+ memory: "1536 MB",
48
+ url: {
49
+ authorization: "iam",
50
+ },
51
+ environment: {
52
+ CONTENT_BUCKET: args.contentBucket.name,
53
+ IMAGE_SIGNING_SECRET: imageSigningSecret.value,
54
+ },
55
+ link: [args.contentBucket, imageSigningSecret],
56
+ });
57
+ }
58
+
59
+ return { imageSigningSecret, imageLambda };
60
+ }
61
+
62
+ export type ImageResources = ReturnType<typeof createImage>;
package/src/index.ts ADDED
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Headroom CMS — SST Component
3
+ *
4
+ * A single HeadroomCMS class that provisions the full CMS infrastructure:
5
+ * DynamoDB tables, S3, Cognito, Lambda functions, CloudFront, and Admin UI.
6
+ */
7
+
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import { createStorage } from "./storage.js";
11
+ import { createAuth } from "./auth.js";
12
+ import { createWebhooks } from "./webhooks.js";
13
+ import { createImage } from "./image.js";
14
+ import { createApi } from "./api.js";
15
+ import { createCdn } from "./cdn.js";
16
+ import { createAdminSite } from "./admin-site.js";
17
+
18
+ /**
19
+ * Resolve the headroom-cms package root directory.
20
+ *
21
+ * We can't rely on import.meta.url because SST's esbuild bundler inlines
22
+ * all config code into a single .mjs file under .sst/platform/, which makes
23
+ * import.meta.url point to the bundled output instead of the original source.
24
+ *
25
+ * Instead, walk up from process.cwd() looking for node_modules/headroom-cms.
26
+ */
27
+ function resolvePkgRoot(): string {
28
+ let dir = process.cwd();
29
+ while (true) {
30
+ const candidate = path.join(dir, "node_modules", "headroom-cms");
31
+ if (fs.existsSync(candidate)) {
32
+ return fs.realpathSync(candidate);
33
+ }
34
+ const parent = path.dirname(dir);
35
+ if (parent === dir) break;
36
+ dir = parent;
37
+ }
38
+ throw new Error(
39
+ "Could not find headroom-cms package in node_modules. " +
40
+ "Ensure it is installed as a dependency.",
41
+ );
42
+ }
43
+
44
+ // Re-export resource types for consumers
45
+ export type { StorageResources } from "./storage.js";
46
+ export type { AuthResources } from "./auth.js";
47
+ export type { WebhookResources } from "./webhooks.js";
48
+ export type { ImageResources } from "./image.js";
49
+ export type { ApiResources } from "./api.js";
50
+ export type { CdnResources } from "./cdn.js";
51
+ export type { AdminSiteResources } from "./admin-site.js";
52
+
53
+ export interface HeadroomCMSArgs {
54
+ /**
55
+ * Email address for sending admin invitations and password resets.
56
+ * Must be verified in SES (Headroom creates the SES identity automatically,
57
+ * but you must click the verification link sent to this address).
58
+ */
59
+ senderEmail: string;
60
+
61
+ /**
62
+ * Custom domain for the CDN (API endpoint).
63
+ * If not provided, uses the default CloudFront domain.
64
+ */
65
+ domain?: {
66
+ name: string;
67
+ /** ACM certificate ARN (must be in us-east-1 for CloudFront) */
68
+ certificateArn: string;
69
+ };
70
+
71
+ /**
72
+ * Custom domain for the admin UI.
73
+ * If not provided, uses the default CloudFront domain.
74
+ */
75
+ adminDomain?: {
76
+ name: string;
77
+ /** ACM certificate ARN (must be in us-east-1 for CloudFront) */
78
+ certificateArn: string;
79
+ };
80
+
81
+ /**
82
+ * CloudFront price class.
83
+ * @default "PriceClass_100" (US, Canada, Europe)
84
+ */
85
+ priceClass?: "PriceClass_100" | "PriceClass_200" | "PriceClass_All";
86
+
87
+ /**
88
+ * API cache TTL in seconds.
89
+ * @default 3600 (1 hour)
90
+ */
91
+ apiCacheTtl?: number;
92
+
93
+ /**
94
+ * Cognito password policy overrides.
95
+ */
96
+ passwordPolicy?: {
97
+ minimumLength?: number;
98
+ requireLowercase?: boolean;
99
+ requireUppercase?: boolean;
100
+ requireNumbers?: boolean;
101
+ requireSymbols?: boolean;
102
+ };
103
+
104
+ /**
105
+ * Development overrides. Used when developing Headroom itself.
106
+ * When set, Lambda functions are compiled from source (requires Go + Node.js).
107
+ */
108
+ dev?: {
109
+ /** Go source path for API Lambda, e.g. "packages/api" */
110
+ apiHandler: string;
111
+ /** Go source path for webhook worker, e.g. "packages/webhook-worker" */
112
+ webhookWorkerHandler: string;
113
+ /** SST handler for custom message function, e.g. "packages/functions/custom-message.handler" */
114
+ customMessageHandler: string;
115
+ /** SST handler for image Lambda, e.g. "packages/image-lambda/index.handler" */
116
+ imageLambdaHandler: string;
117
+ /** Path to admin UI source, e.g. "packages/admin" */
118
+ adminPath: string;
119
+ };
120
+ }
121
+
122
+ export class HeadroomCMS {
123
+ public readonly apiUrl: $util.Output<string>;
124
+ public readonly cdnUrl: $util.Output<string>;
125
+ public readonly adminUrl: $util.Output<string>;
126
+ public readonly userPoolId: $util.Output<string>;
127
+ public readonly userPoolClientId: $util.Output<string>;
128
+
129
+ public readonly outputs: Record<string, $util.Output<string>>;
130
+
131
+ constructor(name: string, args: HeadroomCMSArgs) {
132
+ // Only need pkgRoot for package mode (pre-built artifacts)
133
+ const pkgRoot = args.dev ? "" : resolvePkgRoot();
134
+
135
+ // 1. Storage (DynamoDB tables, S3 bucket, KVS)
136
+ const storage = createStorage(name);
137
+
138
+ // 2. Auth (Cognito, SES, custom message Lambda)
139
+ const auth = createAuth(name, {
140
+ senderEmail: args.senderEmail,
141
+ passwordPolicy: args.passwordPolicy,
142
+ pkgRoot,
143
+ dev: args.dev
144
+ ? { handler: args.dev.customMessageHandler }
145
+ : undefined,
146
+ });
147
+
148
+ // 3. Webhooks (DynamoDB tables, SQS queues, worker Lambda)
149
+ const webhooks = createWebhooks(name, {
150
+ sites: storage.sites,
151
+ pkgRoot,
152
+ dev: args.dev
153
+ ? { handler: args.dev.webhookWorkerHandler }
154
+ : undefined,
155
+ });
156
+
157
+ // 4. Image Lambda (Sharp transform with HMAC-signed URLs)
158
+ const image = createImage(name, {
159
+ contentBucket: storage.contentBucket,
160
+ pkgRoot,
161
+ dev: args.dev
162
+ ? { handler: args.dev.imageLambdaHandler }
163
+ : undefined,
164
+ });
165
+
166
+ // 5. API Lambda (Go handler with access to all resources)
167
+ const api = createApi(name, {
168
+ storage,
169
+ auth,
170
+ webhooks,
171
+ image,
172
+ pkgRoot,
173
+ dev: args.dev
174
+ ? { handler: args.dev.apiHandler }
175
+ : undefined,
176
+ });
177
+
178
+ // 6. CDN (CloudFront distribution + edge functions)
179
+ const cdn = createCdn(name, {
180
+ api,
181
+ image,
182
+ contentBucket: storage.contentBucket,
183
+ kvs: storage.kvs,
184
+ priceClass: args.priceClass,
185
+ apiCacheTtl: args.apiCacheTtl,
186
+ domain: args.domain,
187
+ });
188
+
189
+ // 7. Admin UI (static site)
190
+ const admin = createAdminSite(name, {
191
+ api,
192
+ cdn,
193
+ auth,
194
+ pkgRoot,
195
+ dev: args.dev
196
+ ? { adminPath: args.dev.adminPath }
197
+ : undefined,
198
+ domain: args.adminDomain,
199
+ });
200
+
201
+ // Expose outputs
202
+ this.apiUrl = api.api.url;
203
+ this.cdnUrl = cdn.url;
204
+ this.adminUrl = admin.url;
205
+ this.userPoolId = auth.userPool.id;
206
+ this.userPoolClientId = auth.userPoolClient.id;
207
+
208
+ this.outputs = {
209
+ api: api.api.url,
210
+ cdn: cdn.url,
211
+ admin: admin.url,
212
+ userPoolId: auth.userPool.id,
213
+ userPoolClientId: auth.userPoolClient.id,
214
+ };
215
+ }
216
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Minimal ambient type declarations for SST v3 and Pulumi globals.
3
+ *
4
+ * In the dev repo, SST provides these as runtime globals via .sst/platform/.
5
+ * This file declares just enough type information for tsc compilation.
6
+ * At runtime, SST provides the full implementations.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Pulumi Output type (minimal)
11
+ // ---------------------------------------------------------------------------
12
+ interface PulumiOutput<T> {
13
+ apply<U>(fn: (value: T) => U | PulumiOutput<U> | Promise<U>): PulumiOutput<U>;
14
+ }
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // SST namespace
18
+ // ---------------------------------------------------------------------------
19
+ declare namespace sst {
20
+ class Secret {
21
+ constructor(name: string, ...args: any[]);
22
+ value: PulumiOutput<string>;
23
+ }
24
+
25
+ namespace aws {
26
+ class Dynamo {
27
+ constructor(name: string, args: any, opts?: any);
28
+ name: PulumiOutput<string>;
29
+ arn: PulumiOutput<string>;
30
+ }
31
+
32
+ class Bucket {
33
+ constructor(name: string, args?: any, opts?: any);
34
+ name: PulumiOutput<string>;
35
+ arn: PulumiOutput<string>;
36
+ }
37
+
38
+ class Function {
39
+ constructor(name: string, args: any, opts?: any);
40
+ url: PulumiOutput<string>;
41
+ name: PulumiOutput<string>;
42
+ arn: PulumiOutput<string>;
43
+ }
44
+
45
+ class CognitoUserPool {
46
+ constructor(name: string, args?: any, opts?: any);
47
+ id: PulumiOutput<string>;
48
+ arn: PulumiOutput<string>;
49
+ addClient(name: string, args?: any): CognitoUserPoolClient;
50
+ }
51
+
52
+ class CognitoUserPoolClient {
53
+ id: PulumiOutput<string>;
54
+ }
55
+
56
+ class Queue {
57
+ constructor(name: string, args?: any, opts?: any);
58
+ url: PulumiOutput<string>;
59
+ arn: PulumiOutput<string>;
60
+ subscribe(handler: any, opts?: any): void;
61
+ }
62
+
63
+ class StaticSite {
64
+ constructor(name: string, args?: any, opts?: any);
65
+ url: PulumiOutput<string>;
66
+ }
67
+ }
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // AWS (Pulumi) namespace
72
+ // ---------------------------------------------------------------------------
73
+ declare namespace aws {
74
+ function getRegionOutput(): { name: PulumiOutput<string> };
75
+
76
+ namespace cloudfront {
77
+ class KeyValueStore {
78
+ constructor(name: string, args?: any, opts?: any);
79
+ arn: PulumiOutput<string>;
80
+ id: PulumiOutput<string>;
81
+ }
82
+
83
+ class Function {
84
+ constructor(name: string, args?: any, opts?: any);
85
+ arn: PulumiOutput<string>;
86
+ }
87
+
88
+ class CachePolicy {
89
+ constructor(name: string, args?: any, opts?: any);
90
+ id: PulumiOutput<string>;
91
+ }
92
+
93
+ class OriginRequestPolicy {
94
+ constructor(name: string, args?: any, opts?: any);
95
+ id: PulumiOutput<string>;
96
+ }
97
+
98
+ class OriginAccessControl {
99
+ constructor(name: string, args?: any, opts?: any);
100
+ id: PulumiOutput<string>;
101
+ }
102
+
103
+ class Distribution {
104
+ constructor(name: string, args?: any, opts?: any);
105
+ arn: PulumiOutput<string>;
106
+ domainName: PulumiOutput<string>;
107
+ }
108
+ }
109
+
110
+ namespace ses {
111
+ class EmailIdentity {
112
+ constructor(name: string, args?: any, opts?: any);
113
+ arn: PulumiOutput<string>;
114
+ }
115
+ }
116
+
117
+ namespace lambda {
118
+ class Permission {
119
+ constructor(name: string, args?: any, opts?: any);
120
+ }
121
+
122
+ class EventSourceMapping {
123
+ constructor(name: string, args?: any, opts?: any);
124
+ }
125
+ }
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // SST globals
130
+ // ---------------------------------------------------------------------------
131
+ declare const $app: {
132
+ readonly name: string;
133
+ readonly stage: string;
134
+ };
135
+
136
+ declare const $interpolate: (
137
+ strings: TemplateStringsArray,
138
+ ...values: any[]
139
+ ) => PulumiOutput<string>;
140
+
141
+ declare namespace $util {
142
+ type Output<T> = PulumiOutput<T>;
143
+ }
package/src/storage.ts ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Storage Infrastructure
3
+ *
4
+ * 9 DynamoDB tables + S3 bucket + CloudFront KeyValueStore.
5
+ * All resources are prefixed with the component name.
6
+ */
7
+
8
+ export function createStorage(name: string) {
9
+ const sites = new sst.aws.Dynamo(`${name}Sites`, {
10
+ fields: {
11
+ pk: "string",
12
+ sk: "string",
13
+ },
14
+ primaryIndex: { hashKey: "pk", rangeKey: "sk" },
15
+ });
16
+
17
+ const content = new sst.aws.Dynamo(`${name}Content`, {
18
+ fields: {
19
+ pk: "string",
20
+ sk: "string",
21
+ gsiPk: "string",
22
+ gsiSk: "string",
23
+ lastDraftAtSk: "string",
24
+ collectionGsiPk: "string",
25
+ titleSk: "string",
26
+ },
27
+ primaryIndex: { hashKey: "pk", rangeKey: "sk" },
28
+ globalIndexes: {
29
+ byPublishedDate: { hashKey: "gsiPk", rangeKey: "gsiSk" },
30
+ byLastDraftAt: { hashKey: "pk", rangeKey: "lastDraftAtSk" },
31
+ byCollectionTitle: { hashKey: "collectionGsiPk", rangeKey: "titleSk" },
32
+ },
33
+ });
34
+
35
+ const draftContent = new sst.aws.Dynamo(`${name}DraftContent`, {
36
+ fields: {
37
+ pk: "string",
38
+ sk: "string",
39
+ },
40
+ primaryIndex: { hashKey: "pk", rangeKey: "sk" },
41
+ });
42
+
43
+ const blocks = new sst.aws.Dynamo(`${name}Blocks`, {
44
+ fields: {
45
+ pk: "string",
46
+ },
47
+ primaryIndex: { hashKey: "pk" },
48
+ });
49
+
50
+ const media = new sst.aws.Dynamo(`${name}Media`, {
51
+ fields: {
52
+ pk: "string",
53
+ sk: "string",
54
+ uploadedAt: "number",
55
+ folderGsiPk: "string",
56
+ },
57
+ primaryIndex: { hashKey: "pk", rangeKey: "sk" },
58
+ globalIndexes: {
59
+ byDate: { hashKey: "pk", rangeKey: "uploadedAt" },
60
+ byFolder: { hashKey: "folderGsiPk", rangeKey: "uploadedAt" },
61
+ },
62
+ });
63
+
64
+ const collections = new sst.aws.Dynamo(`${name}Collections`, {
65
+ fields: {
66
+ pk: "string",
67
+ sk: "string",
68
+ },
69
+ primaryIndex: { hashKey: "pk", rangeKey: "sk" },
70
+ });
71
+
72
+ const blockTypes = new sst.aws.Dynamo(`${name}BlockTypes`, {
73
+ fields: {
74
+ pk: "string",
75
+ sk: "string",
76
+ },
77
+ primaryIndex: { hashKey: "pk", rangeKey: "sk" },
78
+ });
79
+
80
+ const adminAudit = new sst.aws.Dynamo(`${name}AdminAudit`, {
81
+ fields: {
82
+ pk: "string",
83
+ sk: "string",
84
+ },
85
+ primaryIndex: { hashKey: "pk", rangeKey: "sk" },
86
+ });
87
+
88
+ const relationships = new sst.aws.Dynamo(`${name}Relationships`, {
89
+ fields: {
90
+ pk: "string",
91
+ sk: "string",
92
+ targetPk: "string",
93
+ targetSk: "string",
94
+ },
95
+ primaryIndex: { hashKey: "pk", rangeKey: "sk" },
96
+ globalIndexes: {
97
+ byTarget: { hashKey: "targetPk", rangeKey: "targetSk" },
98
+ },
99
+ });
100
+
101
+ const contentBucket = new sst.aws.Bucket(`${name}ContentBucket`, {
102
+ versioning: true,
103
+ access: "cloudfront",
104
+ transform: {
105
+ bucket: (args: any) => {
106
+ args.lifecycleRules = [
107
+ {
108
+ id: "cleanup-pending-uploads",
109
+ enabled: true,
110
+ expirations: [{ days: 1 }],
111
+ tags: { status: "pending" },
112
+ },
113
+ ];
114
+ },
115
+ },
116
+ });
117
+
118
+ const kvs = new aws.cloudfront.KeyValueStore(`${name}KVS`, {
119
+ name: $interpolate`${$app.name}-${$app.stage}-kvs`,
120
+ comment: "Headroom CMS edge data: API keys and site versions",
121
+ });
122
+
123
+ return {
124
+ sites,
125
+ content,
126
+ draftContent,
127
+ blocks,
128
+ media,
129
+ collections,
130
+ blockTypes,
131
+ adminAudit,
132
+ relationships,
133
+ contentBucket,
134
+ kvs,
135
+ };
136
+ }
137
+
138
+ export type StorageResources = ReturnType<typeof createStorage>;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Webhook Infrastructure
3
+ *
4
+ * 2 DynamoDB tables + 2 SQS queues + webhook worker Lambda.
5
+ * Supports dev mode (Go source) and package mode (pre-compiled binary).
6
+ */
7
+
8
+ import path from "path";
9
+ import type { StorageResources } from "./storage.js";
10
+
11
+ export interface WebhookArgs {
12
+ sites: StorageResources["sites"];
13
+ pkgRoot: string;
14
+ dev?: {
15
+ /** Go source path, e.g. "packages/webhook-worker" */
16
+ handler: string;
17
+ };
18
+ }
19
+
20
+ export function createWebhooks(name: string, args: WebhookArgs) {
21
+ const webhooks = new sst.aws.Dynamo(`${name}Webhooks`, {
22
+ fields: {
23
+ pk: "string",
24
+ sk: "string",
25
+ },
26
+ primaryIndex: { hashKey: "pk", rangeKey: "sk" },
27
+ });
28
+
29
+ const webhookDeliveries = new sst.aws.Dynamo(`${name}WebhookDeliveries`, {
30
+ fields: {
31
+ pk: "string",
32
+ sk: "string",
33
+ },
34
+ primaryIndex: { hashKey: "pk", rangeKey: "sk" },
35
+ ttl: "ttl",
36
+ });
37
+
38
+ const webhookDeliveryDLQ = new sst.aws.Queue(`${name}WebhookDeliveryDLQ`, {
39
+ transform: {
40
+ queue: (queueArgs: any) => {
41
+ queueArgs.messageRetentionSeconds = 14 * 24 * 60 * 60; // 14 days
42
+ },
43
+ },
44
+ });
45
+
46
+ const webhookDeliveryQueue = new sst.aws.Queue(
47
+ `${name}WebhookDeliveryQueue`,
48
+ {
49
+ dlq: {
50
+ queue: webhookDeliveryDLQ.arn,
51
+ retry: 5,
52
+ },
53
+ transform: {
54
+ queue: (queueArgs: any) => {
55
+ queueArgs.visibilityTimeoutSeconds = 60;
56
+ queueArgs.messageRetentionSeconds = 4 * 24 * 60 * 60; // 4 days
57
+ },
58
+ },
59
+ },
60
+ );
61
+
62
+ // Webhook worker Lambda: processes SQS messages and delivers webhooks.
63
+ // Note: We use a manual Function + EventSourceMapping instead of
64
+ // queue.subscribe() to avoid a duplicate LambdaEncryptionKey issue
65
+ // caused by SST's dynamic import creating a separate Function class instance.
66
+ const workerConfig = args.dev
67
+ ? {
68
+ handler: args.dev.handler,
69
+ runtime: "go" as const,
70
+ }
71
+ : {
72
+ bundle: path.join(args.pkgRoot, "lambda/webhook-worker"),
73
+ handler: "bootstrap",
74
+ runtime: "provided.al2023" as const,
75
+ architecture: "arm64" as const,
76
+ };
77
+
78
+ const webhookWorker = new sst.aws.Function(`${name}WebhookWorker`, {
79
+ ...workerConfig,
80
+ memory: "256 MB",
81
+ timeout: "30 seconds",
82
+ environment: {
83
+ WEBHOOK_DELIVERIES_TABLE: webhookDeliveries.name,
84
+ WEBHOOKS_TABLE: webhooks.name,
85
+ SITES_TABLE: args.sites.name,
86
+ },
87
+ link: [webhookDeliveries, webhooks, args.sites],
88
+ permissions: [
89
+ {
90
+ actions: [
91
+ "sqs:ReceiveMessage",
92
+ "sqs:DeleteMessage",
93
+ "sqs:GetQueueAttributes",
94
+ ],
95
+ resources: [webhookDeliveryQueue.arn],
96
+ },
97
+ ],
98
+ });
99
+
100
+ new aws.lambda.EventSourceMapping(`${name}WebhookWorkerEventSource`, {
101
+ eventSourceArn: webhookDeliveryQueue.arn,
102
+ functionName: webhookWorker.name,
103
+ batchSize: 1,
104
+ });
105
+
106
+ return {
107
+ webhooks,
108
+ webhookDeliveries,
109
+ webhookDeliveryQueue,
110
+ webhookDeliveryDLQ,
111
+ };
112
+ }
113
+
114
+ export type WebhookResources = ReturnType<typeof createWebhooks>;