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/api.ts ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * API Infrastructure
3
+ *
4
+ * Go Lambda function serving the Headroom API with access to all resources.
5
+ * Supports dev mode (Go source with live reload) and package mode (pre-compiled binary).
6
+ */
7
+
8
+ import path from "path";
9
+ import type { StorageResources } from "./storage.js";
10
+ import type { AuthResources } from "./auth.js";
11
+ import type { WebhookResources } from "./webhooks.js";
12
+ import type { ImageResources } from "./image.js";
13
+
14
+ export interface ApiArgs {
15
+ storage: StorageResources;
16
+ auth: AuthResources;
17
+ webhooks: WebhookResources;
18
+ image: ImageResources;
19
+ pkgRoot: string;
20
+ dev?: {
21
+ /** Go source path, e.g. "packages/api" */
22
+ handler: string;
23
+ };
24
+ }
25
+
26
+ export function createApi(name: string, args: ApiArgs) {
27
+ const { storage, auth, webhooks, image } = args;
28
+
29
+ const handlerConfig = args.dev
30
+ ? {
31
+ handler: args.dev.handler,
32
+ runtime: "go" as const,
33
+ }
34
+ : {
35
+ bundle: path.join(args.pkgRoot, "lambda/api"),
36
+ handler: "bootstrap",
37
+ runtime: "provided.al2023" as const,
38
+ architecture: "arm64" as const,
39
+ };
40
+
41
+ const api = new sst.aws.Function(`${name}Api`, {
42
+ ...handlerConfig,
43
+ timeout: "60 seconds",
44
+ url: {
45
+ cors: false,
46
+ },
47
+ environment: {
48
+ SITES_TABLE: storage.sites.name,
49
+ CONTENT_TABLE: storage.content.name,
50
+ DRAFT_CONTENT_TABLE: storage.draftContent.name,
51
+ BLOCKS_TABLE: storage.blocks.name,
52
+ MEDIA_TABLE: storage.media.name,
53
+ COLLECTIONS_TABLE: storage.collections.name,
54
+ BLOCK_TYPES_TABLE: storage.blockTypes.name,
55
+ ADMIN_AUDIT_TABLE: storage.adminAudit.name,
56
+ CONTENT_BUCKET: storage.contentBucket.name,
57
+ USER_POOL_ID: auth.userPool.id,
58
+ USER_POOL_CLIENT_ID: auth.userPoolClient.id,
59
+ KVS_ARN: storage.kvs.arn,
60
+ AWS_REGION_NAME: aws.getRegionOutput().name,
61
+ WEBHOOKS_TABLE: webhooks.webhooks.name,
62
+ WEBHOOK_DELIVERIES_TABLE: webhooks.webhookDeliveries.name,
63
+ WEBHOOK_QUEUE_URL: webhooks.webhookDeliveryQueue.url,
64
+ IMAGE_SIGNING_SECRET: image.imageSigningSecret.value,
65
+ IMAGE_LAMBDA_NAME: image.imageLambda.name,
66
+ RELATIONSHIPS_TABLE: storage.relationships.name,
67
+ },
68
+ link: [
69
+ storage.sites,
70
+ storage.content,
71
+ storage.draftContent,
72
+ storage.blocks,
73
+ storage.media,
74
+ storage.collections,
75
+ storage.blockTypes,
76
+ storage.adminAudit,
77
+ storage.contentBucket,
78
+ webhooks.webhooks,
79
+ webhooks.webhookDeliveries,
80
+ webhooks.webhookDeliveryQueue,
81
+ image.imageSigningSecret,
82
+ storage.relationships,
83
+ ],
84
+ permissions: [
85
+ {
86
+ actions: [
87
+ "cloudfront-keyvaluestore:DescribeKeyValueStore",
88
+ "cloudfront-keyvaluestore:PutKey",
89
+ "cloudfront-keyvaluestore:DeleteKey",
90
+ "cloudfront-keyvaluestore:GetKey",
91
+ ],
92
+ resources: [storage.kvs.arn],
93
+ },
94
+ {
95
+ actions: [
96
+ "cognito-idp:ListUsers",
97
+ "cognito-idp:AdminCreateUser",
98
+ "cognito-idp:AdminDeleteUser",
99
+ "cognito-idp:AdminGetUser",
100
+ ],
101
+ resources: [auth.userPool.arn],
102
+ },
103
+ {
104
+ actions: ["lambda:InvokeFunction"],
105
+ resources: [image.imageLambda.arn],
106
+ },
107
+ ],
108
+ });
109
+
110
+ return { api };
111
+ }
112
+
113
+ export type ApiResources = ReturnType<typeof createApi>;
package/src/auth.ts ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Authentication Infrastructure
3
+ *
4
+ * Cognito User Pool, SES email identity, and custom message Lambda.
5
+ * Supports dev mode (source handler) and package mode (pre-bundled handler).
6
+ */
7
+
8
+ import path from "path";
9
+
10
+ export interface AuthArgs {
11
+ senderEmail: string;
12
+ passwordPolicy?: {
13
+ minimumLength?: number;
14
+ requireLowercase?: boolean;
15
+ requireUppercase?: boolean;
16
+ requireNumbers?: boolean;
17
+ requireSymbols?: boolean;
18
+ };
19
+ pkgRoot: string;
20
+ dev?: {
21
+ /** SST handler string, e.g. "packages/functions/custom-message.handler" */
22
+ handler: string;
23
+ };
24
+ }
25
+
26
+ export function createAuth(name: string, args: AuthArgs) {
27
+ const emailIdentity = new aws.ses.EmailIdentity(`${name}EmailIdentity`, {
28
+ email: args.senderEmail,
29
+ });
30
+
31
+ // Custom message Lambda: customizes Cognito invite emails
32
+ let customMessageFn: sst.aws.Function;
33
+ if (args.dev) {
34
+ customMessageFn = new sst.aws.Function(`${name}CustomMessage`, {
35
+ handler: args.dev.handler,
36
+ runtime: "nodejs20.x",
37
+ });
38
+ } else {
39
+ customMessageFn = new sst.aws.Function(`${name}CustomMessage`, {
40
+ bundle: path.join(args.pkgRoot, "lambda/functions/custom-message"),
41
+ handler: "index.handler",
42
+ runtime: "nodejs20.x",
43
+ });
44
+ }
45
+
46
+ const pp = args.passwordPolicy ?? {};
47
+
48
+ const userPool = new sst.aws.CognitoUserPool(`${name}UserPool`, {
49
+ usernames: ["email"],
50
+ transform: {
51
+ userPool: {
52
+ passwordPolicy: {
53
+ minimumLength: pp.minimumLength ?? 12,
54
+ requireLowercase: pp.requireLowercase ?? true,
55
+ requireUppercase: pp.requireUppercase ?? true,
56
+ requireNumbers: pp.requireNumbers ?? true,
57
+ requireSymbols: pp.requireSymbols ?? true,
58
+ },
59
+ accountRecoverySetting: {
60
+ recoveryMechanisms: [{ name: "verified_email", priority: 1 }],
61
+ },
62
+ lambdaConfig: {
63
+ customMessage: customMessageFn.arn,
64
+ },
65
+ emailConfiguration: {
66
+ emailSendingAccount: "DEVELOPER",
67
+ sourceArn: emailIdentity.arn,
68
+ fromEmailAddress: `Headroom CMS <${args.senderEmail}>`,
69
+ },
70
+ mfaConfiguration: "OPTIONAL",
71
+ softwareTokenMfaConfiguration: {
72
+ enabled: true,
73
+ },
74
+ },
75
+ },
76
+ });
77
+
78
+ // Grant Cognito permission to invoke the custom message Lambda
79
+ new aws.lambda.Permission(`${name}CustomMessagePermission`, {
80
+ action: "lambda:InvokeFunction",
81
+ function: customMessageFn.arn,
82
+ principal: "cognito-idp.amazonaws.com",
83
+ sourceArn: userPool.arn,
84
+ });
85
+
86
+ const userPoolClient = userPool.addClient(`${name}Client`, {
87
+ transform: {
88
+ client: {
89
+ explicitAuthFlows: [
90
+ "ALLOW_USER_SRP_AUTH",
91
+ "ALLOW_REFRESH_TOKEN_AUTH",
92
+ "ALLOW_ADMIN_USER_PASSWORD_AUTH",
93
+ ],
94
+ accessTokenValidity: 1,
95
+ idTokenValidity: 1,
96
+ refreshTokenValidity: 365,
97
+ tokenValidityUnits: {
98
+ accessToken: "hours",
99
+ idToken: "hours",
100
+ refreshToken: "days",
101
+ },
102
+ preventUserExistenceErrors: "ENABLED",
103
+ },
104
+ },
105
+ });
106
+
107
+ return { userPool, userPoolClient };
108
+ }
109
+
110
+ export type AuthResources = ReturnType<typeof createAuth>;
package/src/cdn.ts ADDED
@@ -0,0 +1,449 @@
1
+ /**
2
+ * CDN Infrastructure
3
+ *
4
+ * CloudFront distribution with edge authentication, version-based caching,
5
+ * direct S3 media serving, and Sharp Lambda image transforms.
6
+ */
7
+
8
+ import type { StorageResources } from "./storage.js";
9
+ import type { ApiResources } from "./api.js";
10
+ import type { ImageResources } from "./image.js";
11
+
12
+ export interface CdnArgs {
13
+ api: ApiResources;
14
+ image: ImageResources;
15
+ contentBucket: StorageResources["contentBucket"];
16
+ kvs: StorageResources["kvs"];
17
+ priceClass?: "PriceClass_100" | "PriceClass_200" | "PriceClass_All";
18
+ apiCacheTtl?: number;
19
+ domain?: {
20
+ name: string;
21
+ certificateArn: string;
22
+ };
23
+ }
24
+
25
+ export function createCdn(name: string, args: CdnArgs) {
26
+ const { api, image, contentBucket, kvs } = args;
27
+ const apiCacheTtl = args.apiCacheTtl ?? 3600;
28
+
29
+ // Extract KVS UUID from ARN (cf.kvs() needs the UUID, not the name)
30
+ const kvsUuid = kvs.arn.apply((arn: string) => arn.split("/").pop()!);
31
+
32
+ // =========================================================================
33
+ // CloudFront Function: Edge Auth
34
+ // =========================================================================
35
+ const edgeFunction = new aws.cloudfront.Function(`${name}EdgeAuth`, {
36
+ name: $interpolate`${$app.name}-${$app.stage}-edge-auth`,
37
+ runtime: "cloudfront-js-2.0",
38
+ publish: true,
39
+ keyValueStoreAssociations: [kvs.arn],
40
+ code: $interpolate`
41
+ import cf from 'cloudfront';
42
+ import crypto from 'crypto';
43
+
44
+ const kvsId = '${kvsUuid}';
45
+ const kvsHandle = cf.kvs(kvsId);
46
+
47
+ async function handler(event) {
48
+ const request = event.request;
49
+ const uri = request.uri;
50
+ const headers = request.headers;
51
+
52
+ // Skip auth for admin paths (use JWT auth at origin)
53
+ if (uri.startsWith('/v1/admin/')) {
54
+ return request;
55
+ }
56
+
57
+ // Skip auth for health endpoint
58
+ if (uri === '/health') {
59
+ return request;
60
+ }
61
+
62
+ // Skip auth for non-API paths
63
+ if (!uri.startsWith('/v1/')) {
64
+ return request;
65
+ }
66
+
67
+ // Extract site from path: /v1/{site}/...
68
+ var pathParts = uri.split('/');
69
+ if (pathParts.length < 3) {
70
+ return {
71
+ statusCode: 400,
72
+ statusDescription: 'Bad Request',
73
+ body: { encoding: 'text', data: 'Invalid path' }
74
+ };
75
+ }
76
+
77
+ var site = pathParts[2];
78
+
79
+ // Get API key from header
80
+ var apiKeyHeader = headers['x-headroom-key'];
81
+ if (!apiKeyHeader) {
82
+ return {
83
+ statusCode: 401,
84
+ statusDescription: 'Unauthorized',
85
+ headers: { 'content-type': { value: 'application/json' } },
86
+ body: { encoding: 'text', data: JSON.stringify({ error: 'API key required' }) }
87
+ };
88
+ }
89
+
90
+ // Hash the API key (needed for KVS lookup key)
91
+ var keyHash = crypto.createHash('sha256').update(apiKeyHeader.value).digest('hex');
92
+ var hashPrefix = keyHash.substring(0, 8);
93
+
94
+ // Fetch API key hash and version in parallel
95
+ var results = await Promise.all([
96
+ kvsHandle.get('site:' + site + ':key:' + hashPrefix).catch(function() { return null; }),
97
+ kvsHandle.get('site:' + site + ':version').catch(function() { return '0'; }),
98
+ ]);
99
+
100
+ var storedHash = results[0];
101
+ var versionResult = results[1];
102
+
103
+ // Validate API key - compare full hash
104
+ if (!storedHash || storedHash !== keyHash) {
105
+ return {
106
+ statusCode: 403,
107
+ statusDescription: 'Forbidden',
108
+ headers: { 'content-type': { value: 'application/json' } },
109
+ body: { encoding: 'text', data: JSON.stringify({ error: 'Invalid API key' }) }
110
+ };
111
+ }
112
+
113
+ // Add version header for cache key discrimination
114
+ request.headers['x-site-version'] = { value: versionResult || '0' };
115
+
116
+ return request;
117
+ }
118
+ `,
119
+ });
120
+
121
+ // =========================================================================
122
+ // CloudFront Function: Media Rewrite
123
+ // =========================================================================
124
+ const mediaRewriteFunction = new aws.cloudfront.Function(
125
+ `${name}MediaRewrite`,
126
+ {
127
+ name: $interpolate`${$app.name}-${$app.stage}-media-rewrite`,
128
+ runtime: "cloudfront-js-2.0",
129
+ publish: true,
130
+ code: `
131
+ function handler(event) {
132
+ var request = event.request;
133
+ var uri = request.uri;
134
+
135
+ // Rewrite /media/{site}/{mediaId}/{file} → /sites/{site}/media/{mediaId}/{file}
136
+ var parts = uri.split('/');
137
+ // parts: ['', 'media', '{site}', '{mediaId}', '{file}']
138
+ if (parts.length >= 5 && parts[1] === 'media') {
139
+ var site = parts[2];
140
+ var mediaId = parts[3];
141
+ var rest = parts.slice(4).join('/');
142
+ request.uri = '/sites/' + site + '/media/' + mediaId + '/' + rest;
143
+ }
144
+
145
+ return request;
146
+ }
147
+ `,
148
+ },
149
+ );
150
+
151
+ // =========================================================================
152
+ // Cache Policies
153
+ // =========================================================================
154
+
155
+ const apiCachePolicy = new aws.cloudfront.CachePolicy(
156
+ `${name}APICachePolicy`,
157
+ {
158
+ name: $interpolate`${$app.name}-${$app.stage}-api-cache`,
159
+ comment:
160
+ "Cache policy for Headroom API with version-based cache busting",
161
+ defaultTtl: apiCacheTtl,
162
+ maxTtl: 86400,
163
+ minTtl: 0,
164
+ parametersInCacheKeyAndForwardedToOrigin: {
165
+ cookiesConfig: { cookieBehavior: "none" },
166
+ headersConfig: {
167
+ headerBehavior: "whitelist",
168
+ headers: { items: ["x-site-version"] },
169
+ },
170
+ queryStringsConfig: {
171
+ queryStringBehavior: "whitelist",
172
+ queryStrings: {
173
+ items: ["collection", "limit", "cursor", "before", "after", "tag"],
174
+ },
175
+ },
176
+ enableAcceptEncodingBrotli: true,
177
+ enableAcceptEncodingGzip: true,
178
+ },
179
+ },
180
+ );
181
+
182
+ const imageCachePolicy = new aws.cloudfront.CachePolicy(
183
+ `${name}ImageCachePolicy`,
184
+ {
185
+ name: $interpolate`${$app.name}-${$app.stage}-image-cache`,
186
+ comment: "Cache policy for transformed images (immutable)",
187
+ defaultTtl: 31536000,
188
+ maxTtl: 31536000,
189
+ minTtl: 31536000,
190
+ parametersInCacheKeyAndForwardedToOrigin: {
191
+ cookiesConfig: { cookieBehavior: "none" },
192
+ headersConfig: { headerBehavior: "none" },
193
+ queryStringsConfig: {
194
+ queryStringBehavior: "whitelist",
195
+ queryStrings: {
196
+ items: ["w", "h", "fit", "format", "q", "sig"],
197
+ },
198
+ },
199
+ enableAcceptEncodingBrotli: true,
200
+ enableAcceptEncodingGzip: true,
201
+ },
202
+ },
203
+ );
204
+
205
+ const mediaCachePolicy = new aws.cloudfront.CachePolicy(
206
+ `${name}MediaCachePolicy`,
207
+ {
208
+ name: $interpolate`${$app.name}-${$app.stage}-media-cache`,
209
+ comment: "Cache policy for original media files (immutable)",
210
+ defaultTtl: 31536000,
211
+ maxTtl: 31536000,
212
+ minTtl: 31536000,
213
+ parametersInCacheKeyAndForwardedToOrigin: {
214
+ cookiesConfig: { cookieBehavior: "none" },
215
+ headersConfig: { headerBehavior: "none" },
216
+ queryStringsConfig: { queryStringBehavior: "none" },
217
+ enableAcceptEncodingBrotli: true,
218
+ enableAcceptEncodingGzip: true,
219
+ },
220
+ },
221
+ );
222
+
223
+ // =========================================================================
224
+ // Origin Request Policy
225
+ // =========================================================================
226
+
227
+ const originRequestPolicy = new aws.cloudfront.OriginRequestPolicy(
228
+ `${name}OriginRequestPolicy`,
229
+ {
230
+ name: $interpolate`${$app.name}-${$app.stage}-origin-request`,
231
+ comment: "Forward necessary headers to Lambda origin",
232
+ cookiesConfig: { cookieBehavior: "none" },
233
+ headersConfig: {
234
+ headerBehavior: "whitelist",
235
+ headers: {
236
+ items: [
237
+ "x-headroom-key",
238
+ "content-type",
239
+ "accept",
240
+ "x-site-version",
241
+ ],
242
+ },
243
+ },
244
+ queryStringsConfig: { queryStringBehavior: "all" },
245
+ },
246
+ );
247
+
248
+ // Managed AllViewerExceptHostHeader policy for admin paths
249
+ const adminOriginRequestPolicyId =
250
+ "b689b0a8-53d0-40ab-baf2-68738e2966ac";
251
+
252
+ // =========================================================================
253
+ // Origin Access Controls (OAC)
254
+ // =========================================================================
255
+
256
+ const mediaOAC = new aws.cloudfront.OriginAccessControl(`${name}MediaOAC`, {
257
+ name: $interpolate`${$app.name}-${$app.stage}-media-oac`,
258
+ description: "OAC for S3 media origin",
259
+ originAccessControlOriginType: "s3",
260
+ signingBehavior: "always",
261
+ signingProtocol: "sigv4",
262
+ });
263
+
264
+ const imageOAC = new aws.cloudfront.OriginAccessControl(`${name}ImageOAC`, {
265
+ name: $interpolate`${$app.name}-${$app.stage}-image-oac`,
266
+ description: "OAC for image transform Lambda origin",
267
+ originAccessControlOriginType: "lambda",
268
+ signingBehavior: "always",
269
+ signingProtocol: "sigv4",
270
+ });
271
+
272
+ // =========================================================================
273
+ // CloudFront Distribution
274
+ // =========================================================================
275
+
276
+ const originDomain = api.api.url.apply((url: string) => {
277
+ const parsed = new URL(url);
278
+ return parsed.hostname;
279
+ });
280
+
281
+ const imageLambdaDomain = image.imageLambda.url.apply((url: string) => {
282
+ const parsed = new URL(url);
283
+ return parsed.hostname;
284
+ });
285
+
286
+ const s3RegionalDomain = $interpolate`${contentBucket.name}.s3.${aws.getRegionOutput().name}.amazonaws.com`;
287
+
288
+ const priceClass = args.priceClass ?? "PriceClass_100";
289
+
290
+ // Build aliases and certificate config for custom domain
291
+ const aliases = args.domain ? [args.domain.name] : undefined;
292
+ const viewerCertificate = args.domain
293
+ ? {
294
+ acmCertificateArn: args.domain.certificateArn,
295
+ sslSupportMethod: "sni-only" as const,
296
+ minimumProtocolVersion: "TLSv1.2_2021" as const,
297
+ }
298
+ : {
299
+ cloudfrontDefaultCertificate: true,
300
+ };
301
+
302
+ const distribution = new aws.cloudfront.Distribution(
303
+ `${name}Distribution`,
304
+ {
305
+ enabled: true,
306
+ comment: $interpolate`Headroom CMS API - ${$app.stage}`,
307
+ httpVersion: "http2and3",
308
+ priceClass,
309
+ aliases,
310
+
311
+ origins: [
312
+ {
313
+ originId: "api",
314
+ domainName: originDomain,
315
+ customOriginConfig: {
316
+ httpPort: 80,
317
+ httpsPort: 443,
318
+ originProtocolPolicy: "https-only",
319
+ originSslProtocols: ["TLSv1.2"],
320
+ },
321
+ },
322
+ {
323
+ originId: "media-s3",
324
+ domainName: s3RegionalDomain,
325
+ originAccessControlId: mediaOAC.id,
326
+ s3OriginConfig: {
327
+ originAccessIdentity: "",
328
+ },
329
+ },
330
+ {
331
+ originId: "image-lambda",
332
+ domainName: imageLambdaDomain,
333
+ originAccessControlId: imageOAC.id,
334
+ customOriginConfig: {
335
+ httpPort: 80,
336
+ httpsPort: 443,
337
+ originProtocolPolicy: "https-only",
338
+ originSslProtocols: ["TLSv1.2"],
339
+ },
340
+ },
341
+ ],
342
+
343
+ defaultCacheBehavior: {
344
+ targetOriginId: "api",
345
+ viewerProtocolPolicy: "redirect-to-https",
346
+ allowedMethods: [
347
+ "GET",
348
+ "HEAD",
349
+ "OPTIONS",
350
+ "PUT",
351
+ "POST",
352
+ "PATCH",
353
+ "DELETE",
354
+ ],
355
+ cachedMethods: ["GET", "HEAD", "OPTIONS"],
356
+ compress: true,
357
+ cachePolicyId: apiCachePolicy.id,
358
+ originRequestPolicyId: originRequestPolicy.id,
359
+ functionAssociations: [
360
+ {
361
+ eventType: "viewer-request",
362
+ functionArn: edgeFunction.arn,
363
+ },
364
+ ],
365
+ },
366
+
367
+ orderedCacheBehaviors: [
368
+ // Admin paths: no caching, forwards Authorization header
369
+ {
370
+ pathPattern: "/v1/admin/*",
371
+ targetOriginId: "api",
372
+ viewerProtocolPolicy: "redirect-to-https",
373
+ allowedMethods: [
374
+ "GET",
375
+ "HEAD",
376
+ "OPTIONS",
377
+ "PUT",
378
+ "POST",
379
+ "PATCH",
380
+ "DELETE",
381
+ ],
382
+ cachedMethods: ["GET", "HEAD", "OPTIONS"],
383
+ compress: true,
384
+ cachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
385
+ originRequestPolicyId: adminOriginRequestPolicyId,
386
+ },
387
+ // Media originals: served directly from S3 via OAC
388
+ {
389
+ pathPattern: "/media/*",
390
+ targetOriginId: "media-s3",
391
+ viewerProtocolPolicy: "redirect-to-https",
392
+ allowedMethods: ["GET", "HEAD", "OPTIONS"],
393
+ cachedMethods: ["GET", "HEAD", "OPTIONS"],
394
+ compress: true,
395
+ cachePolicyId: mediaCachePolicy.id,
396
+ functionAssociations: [
397
+ {
398
+ eventType: "viewer-request",
399
+ functionArn: mediaRewriteFunction.arn,
400
+ },
401
+ ],
402
+ },
403
+ // Image transforms: served via Sharp Lambda
404
+ {
405
+ pathPattern: "/img/*",
406
+ targetOriginId: "image-lambda",
407
+ viewerProtocolPolicy: "redirect-to-https",
408
+ allowedMethods: ["GET", "HEAD", "OPTIONS"],
409
+ cachedMethods: ["GET", "HEAD", "OPTIONS"],
410
+ compress: true,
411
+ cachePolicyId: imageCachePolicy.id,
412
+ },
413
+ // Health endpoint: no caching, no auth
414
+ {
415
+ pathPattern: "/health",
416
+ targetOriginId: "api",
417
+ viewerProtocolPolicy: "redirect-to-https",
418
+ allowedMethods: ["GET", "HEAD"],
419
+ cachedMethods: ["GET", "HEAD"],
420
+ compress: true,
421
+ cachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
422
+ originRequestPolicyId: originRequestPolicy.id,
423
+ },
424
+ ],
425
+
426
+ restrictions: {
427
+ geoRestriction: { restrictionType: "none" },
428
+ },
429
+
430
+ viewerCertificate,
431
+ },
432
+ );
433
+
434
+ // Allow CloudFront to invoke the image Lambda via OAC
435
+ new aws.lambda.Permission(`${name}ImageLambdaCFPermission`, {
436
+ action: "lambda:InvokeFunctionUrl",
437
+ function: image.imageLambda.name,
438
+ principal: "cloudfront.amazonaws.com",
439
+ sourceArn: distribution.arn,
440
+ });
441
+
442
+ const url = args.domain
443
+ ? $interpolate`https://${args.domain.name}`
444
+ : $interpolate`https://${distribution.domainName}`;
445
+
446
+ return { distribution, url };
447
+ }
448
+
449
+ export type CdnResources = ReturnType<typeof createCdn>;