headroom-cms 0.1.9 → 0.1.11

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 (154) hide show
  1. package/README.md +11 -6
  2. package/admin/assets/{AdminsPage-Bt_ekZen.js → AdminsPage-BnzH9TL3.js} +1 -1
  3. package/admin/assets/AllContentPage-BtObN6oy.js +1 -0
  4. package/admin/assets/{ApiKeysPage-BfWCxGhC.js → ApiKeysPage-DEAa8eyC.js} +1 -1
  5. package/admin/assets/AuditPage-BN9yNsxh.js +1 -0
  6. package/admin/assets/BlockEditor-3wnisTOZ.js +176 -0
  7. package/admin/assets/BlockEditor-CQpF8tYb.css +1 -0
  8. package/admin/assets/BlockTypeEditPage-C2evAESK.js +1 -0
  9. package/admin/assets/BlockTypesPage-Dhkho6T_.js +1 -0
  10. package/admin/assets/{BulkActionBar-TRiXXLQd.js → BulkActionBar-BxdfUSrN.js} +1 -1
  11. package/admin/assets/CollectionEditPage-lOb4hEZy.js +1 -0
  12. package/admin/assets/{CollectionsPage-ClplrxNn.js → CollectionsPage-CgtOloa1.js} +1 -1
  13. package/admin/assets/{ContentCreatePage-DfYcEH1u.js → ContentCreatePage-LeQjahp_.js} +1 -1
  14. package/admin/assets/ContentEditPage-xczr4d_h.js +1 -0
  15. package/admin/assets/ContentField-pilCbdnA.js +1 -0
  16. package/admin/assets/ContentListPage-BAKDn1Xy.js +1 -0
  17. package/admin/assets/CustomBlockPreview-DNnTFM0z.js +479 -0
  18. package/admin/assets/FieldRenderer-DiOKvkWV.js +2 -0
  19. package/admin/assets/FilterBar-BZoa63zh.js +1 -0
  20. package/admin/assets/FloatingComposerController-D4uLQfUX-BMIvFCoE.js +1 -0
  21. package/admin/assets/IconPicker-CpIgiQTC.js +3 -0
  22. package/admin/assets/{LoginPage-DutieANA.js → LoginPage-D9ZsGLIi.js} +1 -1
  23. package/admin/assets/MediaField-CxccCFGQ.js +1 -0
  24. package/admin/assets/MediaPage-QvMaH2YJ.js +1 -0
  25. package/admin/assets/Pagination-Df9nQ7Z0.js +1 -0
  26. package/admin/assets/RelationshipPicker-B3Ftmqxp.js +1 -0
  27. package/admin/assets/{SiteSettingsPage-BtCC3RKc.js → SiteSettingsPage-6NvH7CiQ.js} +1 -1
  28. package/admin/assets/{SiteUserEditPage-ClHmp0T-.js → SiteUserEditPage-D5VaQ1Xq.js} +1 -1
  29. package/admin/assets/SiteUsersPage-BYVduiqs.js +1 -0
  30. package/admin/assets/{SitesPage-Bw_WBN6v.js → SitesPage-rfWWE0yK.js} +1 -1
  31. package/admin/assets/{SubmissionDetailPage-DS08LGxd.js → SubmissionDetailPage-BSUR685F.js} +1 -1
  32. package/admin/assets/SubmissionEditPage-DjLXHjWU.js +1 -0
  33. package/admin/assets/SubmissionListPage-DBxNEvde.js +1 -0
  34. package/admin/assets/{TagInput-BILCaC9b.js → TagInput-57c4DG1w.js} +1 -1
  35. package/admin/assets/{TagsPage-DdeZokow.js → TagsPage-BEO5AwCv.js} +1 -1
  36. package/admin/assets/{UsersPage-B0vLxjrg.js → UsersPage-BpIRorJ1.js} +1 -1
  37. package/admin/assets/{WebhookEditPage-SlJE4d3z.js → WebhookEditPage-D5xgi56h.js} +1 -1
  38. package/admin/assets/{WebhooksPage-C6lGZLpr.js → WebhooksPage-BY7AaiGr.js} +1 -1
  39. package/admin/assets/{card-hXVtlM0q.js → card-C9hfyHXf.js} +1 -1
  40. package/admin/assets/checkbox-DVJcwUt1.js +1 -0
  41. package/admin/assets/{collapsible-B414SspL.js → collapsible-D3d29uJp.js} +1 -1
  42. package/admin/assets/{command-fvBFHye4.js → command-Bfmj0MEL.js} +1 -1
  43. package/admin/assets/contentStatus-CkPi9Dh6.js +1 -0
  44. package/admin/assets/{core.esm-B_kcYf6n.js → core.esm-DdQHdRkd.js} +2 -2
  45. package/admin/assets/index-BB9Syqw2.css +1 -0
  46. package/admin/assets/index-Ce5pmRMj.js +18 -0
  47. package/admin/assets/media-url-DdCoIedP.js +1 -0
  48. package/admin/assets/popover-CzaQYEEP.js +1 -0
  49. package/admin/assets/radix-C5ZmWuuL.js +51 -0
  50. package/admin/assets/select-CrRhFGIi.js +1 -0
  51. package/admin/assets/serializeToText-2VrsuRUh.js +2 -0
  52. package/admin/assets/{sortable.esm-QyXA6fio.js → sortable.esm-qVEMoaTg.js} +1 -1
  53. package/admin/assets/{table-DLoIbCQ5.js → table-_3bMY0_z.js} +1 -1
  54. package/admin/assets/{textarea-vSXNxwTe.js → textarea-6fq0R6VV.js} +1 -1
  55. package/admin/assets/useAdminResolver-BJNPz3OG.js +1 -0
  56. package/admin/assets/useContent-Bs7nel7C.js +1 -0
  57. package/admin/assets/useContentSearch-B3aTjuCu.js +1 -0
  58. package/admin/assets/{useMedia-e3sqWm_t.js → useMedia-ae3s_ajC.js} +1 -1
  59. package/admin/assets/usePageTitle-C1r1-C00.js +1 -0
  60. package/admin/assets/useSiteUsers-DIaqgNSp.js +1 -0
  61. package/admin/assets/{useTags-f7AVSLuj.js → useTags-B-HgMVwo.js} +1 -1
  62. package/admin/assets/{useWebhooks-BH_r8-Mo.js → useWebhooks-BvZjUJkJ.js} +1 -1
  63. package/admin/assets/yjs-tXBm_srz.js +5 -0
  64. package/admin/favicon-16x16.png +0 -0
  65. package/admin/favicon-32x32.png +0 -0
  66. package/admin/icons/icon-180x180.png +0 -0
  67. package/admin/icons/icon-192x192.png +0 -0
  68. package/admin/icons/icon-512x512.png +0 -0
  69. package/admin/icons/maskable-icon-512x512.png +0 -0
  70. package/admin/index.html +3 -3
  71. package/admin/sw.js +1 -1
  72. package/dist/admin-site.d.ts +16 -2
  73. package/dist/admin-site.d.ts.map +1 -1
  74. package/dist/admin-site.js +10 -3
  75. package/dist/admin-site.js.map +1 -1
  76. package/dist/api.d.ts +7 -0
  77. package/dist/api.d.ts.map +1 -1
  78. package/dist/api.js +8 -1
  79. package/dist/api.js.map +1 -1
  80. package/dist/cdn-api.d.ts +25 -0
  81. package/dist/cdn-api.d.ts.map +1 -0
  82. package/dist/{cdn.js → cdn-api.js} +7 -139
  83. package/dist/cdn-api.js.map +1 -0
  84. package/dist/cdn-media.d.ts +26 -0
  85. package/dist/cdn-media.d.ts.map +1 -0
  86. package/dist/cdn-media.js +202 -0
  87. package/dist/cdn-media.js.map +1 -0
  88. package/dist/collaboration.d.ts +55 -0
  89. package/dist/collaboration.d.ts.map +1 -0
  90. package/dist/collaboration.js +141 -0
  91. package/dist/collaboration.js.map +1 -0
  92. package/dist/index.d.ts +27 -3
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +47 -12
  95. package/dist/index.js.map +1 -1
  96. package/dist/storage.d.ts +2 -0
  97. package/dist/storage.d.ts.map +1 -1
  98. package/dist/storage.js +33 -0
  99. package/dist/storage.js.map +1 -1
  100. package/lambda/api/bootstrap +0 -0
  101. package/lambda/image-lambda/node_modules/.package-lock.json +3 -3
  102. package/lambda/image-lambda/node_modules/semver/README.md +19 -4
  103. package/lambda/image-lambda/node_modules/semver/bin/semver.js +14 -10
  104. package/lambda/image-lambda/node_modules/semver/functions/truncate.js +48 -0
  105. package/lambda/image-lambda/node_modules/semver/index.js +2 -0
  106. package/lambda/image-lambda/node_modules/semver/internal/re.js +1 -1
  107. package/lambda/image-lambda/node_modules/semver/package.json +3 -3
  108. package/lambda/image-lambda/node_modules/semver/range.bnf +5 -4
  109. package/lambda/webhook-worker/bootstrap +0 -0
  110. package/package.json +1 -1
  111. package/src/admin-site.ts +26 -5
  112. package/src/api.ts +15 -1
  113. package/src/{cdn.ts → cdn-api.ts} +8 -161
  114. package/src/cdn-media.ts +250 -0
  115. package/src/collaboration.ts +187 -0
  116. package/src/index.ts +77 -14
  117. package/src/sst-env.d.ts +28 -0
  118. package/src/storage.ts +35 -0
  119. package/admin/assets/AllContentPage-CFqEMAl9.js +0 -1
  120. package/admin/assets/AuditPage-BE0XIUl2.js +0 -1
  121. package/admin/assets/BlockEditor-6wqsThJ7.js +0 -179
  122. package/admin/assets/BlockEditor-Cp_wZ2xN.css +0 -1
  123. package/admin/assets/BlockTypeEditPage-CuNJfZw0.js +0 -1
  124. package/admin/assets/BlockTypesPage-BIMBVxBs.js +0 -1
  125. package/admin/assets/CollectionEditPage-BqX_0cC2.js +0 -1
  126. package/admin/assets/ContentEditPage-D3Rvlktk.js +0 -2
  127. package/admin/assets/ContentListPage-zmO8Is4d.js +0 -1
  128. package/admin/assets/CustomBlockPreview-C6HqS4xv.js +0 -479
  129. package/admin/assets/FieldBuilder-36tfpSyM.js +0 -3
  130. package/admin/assets/FilterBar-DhRwTqFv.js +0 -1
  131. package/admin/assets/MediaField-J2TLG_fu.js +0 -1
  132. package/admin/assets/MediaPage-DZZKMGF4.js +0 -1
  133. package/admin/assets/RelationshipPicker-CDFs4TMW.js +0 -1
  134. package/admin/assets/SiteUsersPage-AyJvcVM7.js +0 -1
  135. package/admin/assets/SubmissionEditPage-Brf-DK2X.js +0 -1
  136. package/admin/assets/SubmissionListPage-DNMzQZHS.js +0 -1
  137. package/admin/assets/checkbox-WGrS3sUr.js +0 -1
  138. package/admin/assets/contentStatus-BmaiYVOm.js +0 -1
  139. package/admin/assets/index-Cir9tY_P.js +0 -18
  140. package/admin/assets/index-DACBYsKM.css +0 -1
  141. package/admin/assets/media-url-DIg_vSyf.js +0 -1
  142. package/admin/assets/popover-D5_HjjUC.js +0 -1
  143. package/admin/assets/radix-C1kb_NqW.js +0 -51
  144. package/admin/assets/select-_uJYxzeZ.js +0 -1
  145. package/admin/assets/serializeToText-DR_WnxiI.js +0 -2
  146. package/admin/assets/useAdminResolver-D-LlmquD.js +0 -1
  147. package/admin/assets/useContent-e8beBIuq.js +0 -1
  148. package/admin/assets/useContentSearch-DOjveB9t.js +0 -1
  149. package/admin/assets/useDebouncedValue-C-cQUcLG.js +0 -1
  150. package/admin/assets/usePageTitle-BNSba9_L.js +0 -1
  151. package/admin/assets/useSiteUsers-BdnvuM2E.js +0 -1
  152. package/dist/cdn.d.ts +0 -27
  153. package/dist/cdn.d.ts.map +0 -1
  154. package/dist/cdn.js.map +0 -1
@@ -1,11 +1,12 @@
1
1
  /**
2
- * CDN Infrastructure
2
+ * API CDN Infrastructure
3
3
  *
4
- * CloudFront distribution with edge authentication, version-based caching,
5
- * direct S3 media serving, and Sharp Lambda image transforms.
4
+ * CloudFront distribution for the Headroom API only — `/v1/*` and `/health`.
5
+ * Edge-auth function + KVS association live here. No `/media/*` or `/img/*`
6
+ * behaviors; those are served by the separate media CDN (see cdn-media.ts).
6
7
  */
7
- export function createCdn(name, args) {
8
- const { api, image, contentBucket, kvs } = args;
8
+ export function createApiCdn(name, args) {
9
+ const { api, kvs } = args;
9
10
  const apiCacheTtl = args.apiCacheTtl ?? 3600;
10
11
  // Extract KVS UUID from ARN (cf.kvs() needs the UUID, not the name)
11
12
  const kvsUuid = kvs.arn.apply((arn) => arn.split("/").pop());
@@ -87,32 +88,6 @@ export function createCdn(name, args) {
87
88
  return request;
88
89
  }
89
90
  `,
90
- });
91
- // =========================================================================
92
- // CloudFront Function: Media Rewrite
93
- // =========================================================================
94
- const mediaRewriteFunction = new aws.cloudfront.Function(`${name}MediaRewrite`, {
95
- name: $interpolate `${$app.name}-${$app.stage}-media-rewrite`,
96
- runtime: "cloudfront-js-2.0",
97
- publish: true,
98
- code: `
99
- function handler(event) {
100
- var request = event.request;
101
- var uri = request.uri;
102
-
103
- // Rewrite /media/{site}/{mediaId}/{file} → /sites/{site}/media/{mediaId}/{file}
104
- var parts = uri.split('/');
105
- // parts: ['', 'media', '{site}', '{mediaId}', '{file}']
106
- if (parts.length >= 5 && parts[1] === 'media') {
107
- var site = parts[2];
108
- var mediaId = parts[3];
109
- var rest = parts.slice(4).join('/');
110
- request.uri = '/sites/' + site + '/media/' + mediaId + '/' + rest;
111
- }
112
-
113
- return request;
114
- }
115
- `,
116
91
  });
117
92
  // =========================================================================
118
93
  // Cache Policies
@@ -144,39 +119,6 @@ export function createCdn(name, args) {
144
119
  enableAcceptEncodingGzip: true,
145
120
  },
146
121
  });
147
- const imageCachePolicy = new aws.cloudfront.CachePolicy(`${name}ImageCachePolicy`, {
148
- name: $interpolate `${$app.name}-${$app.stage}-image-cache`,
149
- comment: "Cache policy for transformed images (immutable)",
150
- defaultTtl: 31536000,
151
- maxTtl: 31536000,
152
- minTtl: 31536000,
153
- parametersInCacheKeyAndForwardedToOrigin: {
154
- cookiesConfig: { cookieBehavior: "none" },
155
- headersConfig: { headerBehavior: "none" },
156
- queryStringsConfig: {
157
- queryStringBehavior: "whitelist",
158
- queryStrings: {
159
- items: ["w", "h", "fit", "format", "q", "sig"],
160
- },
161
- },
162
- enableAcceptEncodingBrotli: true,
163
- enableAcceptEncodingGzip: true,
164
- },
165
- });
166
- const mediaCachePolicy = new aws.cloudfront.CachePolicy(`${name}MediaCachePolicy`, {
167
- name: $interpolate `${$app.name}-${$app.stage}-media-cache`,
168
- comment: "Cache policy for original media files (immutable)",
169
- defaultTtl: 31536000,
170
- maxTtl: 31536000,
171
- minTtl: 31536000,
172
- parametersInCacheKeyAndForwardedToOrigin: {
173
- cookiesConfig: { cookieBehavior: "none" },
174
- headersConfig: { headerBehavior: "none" },
175
- queryStringsConfig: { queryStringBehavior: "none" },
176
- enableAcceptEncodingBrotli: true,
177
- enableAcceptEncodingGzip: true,
178
- },
179
- });
180
122
  const versionCachePolicy = new aws.cloudfront.CachePolicy(`${name}VersionCachePolicy`, {
181
123
  name: $interpolate `${$app.name}-${$app.stage}-version-cache`,
182
124
  comment: "Short-TTL cache for /version to absorb client polling",
@@ -225,34 +167,12 @@ export function createCdn(name, args) {
225
167
  // authenticated behaviors below.
226
168
  const cachingDisabledPolicyId = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad";
227
169
  // =========================================================================
228
- // Origin Access Controls (OAC)
229
- // =========================================================================
230
- const mediaOAC = new aws.cloudfront.OriginAccessControl(`${name}MediaOAC`, {
231
- name: $interpolate `${$app.name}-${$app.stage}-media-oac`,
232
- description: "OAC for S3 media origin",
233
- originAccessControlOriginType: "s3",
234
- signingBehavior: "always",
235
- signingProtocol: "sigv4",
236
- });
237
- const imageOAC = new aws.cloudfront.OriginAccessControl(`${name}ImageOAC`, {
238
- name: $interpolate `${$app.name}-${$app.stage}-image-oac`,
239
- description: "OAC for image transform Lambda origin",
240
- originAccessControlOriginType: "lambda",
241
- signingBehavior: "always",
242
- signingProtocol: "sigv4",
243
- });
244
- // =========================================================================
245
170
  // CloudFront Distribution
246
171
  // =========================================================================
247
172
  const originDomain = api.api.url.apply((url) => {
248
173
  const parsed = new URL(url);
249
174
  return parsed.hostname;
250
175
  });
251
- const imageLambdaDomain = image.imageLambda.url.apply((url) => {
252
- const parsed = new URL(url);
253
- return parsed.hostname;
254
- });
255
- const s3RegionalDomain = $interpolate `${contentBucket.name}.s3.${aws.getRegionOutput().name}.amazonaws.com`;
256
176
  const priceClass = args.priceClass ?? "PriceClass_100";
257
177
  // Build aliases and certificate config for custom domain
258
178
  const aliases = args.domain ? [args.domain.name] : undefined;
@@ -282,25 +202,6 @@ export function createCdn(name, args) {
282
202
  originSslProtocols: ["TLSv1.2"],
283
203
  },
284
204
  },
285
- {
286
- originId: "media-s3",
287
- domainName: s3RegionalDomain,
288
- originAccessControlId: mediaOAC.id,
289
- s3OriginConfig: {
290
- originAccessIdentity: "",
291
- },
292
- },
293
- {
294
- originId: "image-lambda",
295
- domainName: imageLambdaDomain,
296
- originAccessControlId: imageOAC.id,
297
- customOriginConfig: {
298
- httpPort: 80,
299
- httpsPort: 443,
300
- originProtocolPolicy: "https-only",
301
- originSslProtocols: ["TLSv1.2"],
302
- },
303
- },
304
205
  ],
305
206
  defaultCacheBehavior: {
306
207
  targetOriginId: "api",
@@ -484,32 +385,6 @@ export function createCdn(name, args) {
484
385
  },
485
386
  ],
486
387
  },
487
- // Media originals: served directly from S3 via OAC
488
- {
489
- pathPattern: "/media/*",
490
- targetOriginId: "media-s3",
491
- viewerProtocolPolicy: "redirect-to-https",
492
- allowedMethods: ["GET", "HEAD", "OPTIONS"],
493
- cachedMethods: ["GET", "HEAD", "OPTIONS"],
494
- compress: true,
495
- cachePolicyId: mediaCachePolicy.id,
496
- functionAssociations: [
497
- {
498
- eventType: "viewer-request",
499
- functionArn: mediaRewriteFunction.arn,
500
- },
501
- ],
502
- },
503
- // Image transforms: served via Sharp Lambda
504
- {
505
- pathPattern: "/img/*",
506
- targetOriginId: "image-lambda",
507
- viewerProtocolPolicy: "redirect-to-https",
508
- allowedMethods: ["GET", "HEAD", "OPTIONS"],
509
- cachedMethods: ["GET", "HEAD", "OPTIONS"],
510
- compress: true,
511
- cachePolicyId: imageCachePolicy.id,
512
- },
513
388
  // Health endpoint: no caching, no auth
514
389
  {
515
390
  pathPattern: "/health",
@@ -527,16 +402,9 @@ export function createCdn(name, args) {
527
402
  },
528
403
  viewerCertificate,
529
404
  });
530
- // Allow CloudFront to invoke the image Lambda via OAC
531
- new aws.lambda.Permission(`${name}ImageLambdaCFPermission`, {
532
- action: "lambda:InvokeFunctionUrl",
533
- function: image.imageLambda.name,
534
- principal: "cloudfront.amazonaws.com",
535
- sourceArn: distribution.arn,
536
- });
537
405
  const url = args.domain
538
406
  ? $interpolate `https://${args.domain.name}`
539
407
  : $interpolate `https://${distribution.domainName}`;
540
408
  return { distribution, url };
541
409
  }
542
- //# sourceMappingURL=cdn.js.map
410
+ //# sourceMappingURL=cdn-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdn-api.js","sourceRoot":"","sources":["../src/cdn-api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Media CDN Infrastructure
3
+ *
4
+ * CloudFront distribution for the public media surface — `/media/*` and
5
+ * `/img/*` only. No edge auth (`/media/*` is OAC-public, `/img/*` is HMAC
6
+ * signed at the Sharp Lambda). The default behavior targets the S3 origin
7
+ * with no rewrite, so unmatched paths return AccessDenied from S3 — defense
8
+ * in depth.
9
+ */
10
+ import type { StorageResources } from "./storage.js";
11
+ import type { ImageResources } from "./image.js";
12
+ export interface MediaCdnArgs {
13
+ image: ImageResources;
14
+ contentBucket: StorageResources["contentBucket"];
15
+ priceClass?: "PriceClass_100" | "PriceClass_200" | "PriceClass_All";
16
+ domain?: {
17
+ name: string;
18
+ certificateArn: string;
19
+ };
20
+ }
21
+ export declare function createMediaCdn(name: string, args: MediaCdnArgs): {
22
+ distribution: aws.cloudfront.Distribution;
23
+ url: PulumiOutput<string>;
24
+ };
25
+ export type MediaCdnResources = ReturnType<typeof createMediaCdn>;
26
+ //# sourceMappingURL=cdn-media.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdn-media.d.ts","sourceRoot":"","sources":["../src/cdn-media.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,cAAc,CAAC;IACtB,aAAa,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAC;IACjD,UAAU,CAAC,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,gBAAgB,CAAC;IACpE,MAAM,CAAC,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY;;;EAgO9D;AAED,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Media CDN Infrastructure
3
+ *
4
+ * CloudFront distribution for the public media surface — `/media/*` and
5
+ * `/img/*` only. No edge auth (`/media/*` is OAC-public, `/img/*` is HMAC
6
+ * signed at the Sharp Lambda). The default behavior targets the S3 origin
7
+ * with no rewrite, so unmatched paths return AccessDenied from S3 — defense
8
+ * in depth.
9
+ */
10
+ export function createMediaCdn(name, args) {
11
+ const { image, contentBucket } = args;
12
+ // =========================================================================
13
+ // CloudFront Function: Media Rewrite
14
+ // =========================================================================
15
+ const mediaRewriteFunction = new aws.cloudfront.Function(`${name}MediaRewrite`, {
16
+ name: $interpolate `${$app.name}-${$app.stage}-media-rewrite`,
17
+ runtime: "cloudfront-js-2.0",
18
+ publish: true,
19
+ code: `
20
+ function handler(event) {
21
+ var request = event.request;
22
+ var uri = request.uri;
23
+
24
+ // Rewrite /media/{site}/{mediaId}/{file} → /sites/{site}/media/{mediaId}/{file}
25
+ var parts = uri.split('/');
26
+ // parts: ['', 'media', '{site}', '{mediaId}', '{file}']
27
+ if (parts.length >= 5 && parts[1] === 'media') {
28
+ var site = parts[2];
29
+ var mediaId = parts[3];
30
+ var rest = parts.slice(4).join('/');
31
+ request.uri = '/sites/' + site + '/media/' + mediaId + '/' + rest;
32
+ }
33
+
34
+ return request;
35
+ }
36
+ `,
37
+ });
38
+ // =========================================================================
39
+ // Cache Policies
40
+ // =========================================================================
41
+ const imageCachePolicy = new aws.cloudfront.CachePolicy(`${name}ImageCachePolicy`, {
42
+ name: $interpolate `${$app.name}-${$app.stage}-image-cache`,
43
+ comment: "Cache policy for transformed images (immutable)",
44
+ defaultTtl: 31536000,
45
+ maxTtl: 31536000,
46
+ minTtl: 31536000,
47
+ parametersInCacheKeyAndForwardedToOrigin: {
48
+ cookiesConfig: { cookieBehavior: "none" },
49
+ headersConfig: { headerBehavior: "none" },
50
+ queryStringsConfig: {
51
+ queryStringBehavior: "whitelist",
52
+ queryStrings: {
53
+ items: ["w", "h", "fit", "format", "q", "sig"],
54
+ },
55
+ },
56
+ enableAcceptEncodingBrotli: true,
57
+ enableAcceptEncodingGzip: true,
58
+ },
59
+ });
60
+ const mediaCachePolicy = new aws.cloudfront.CachePolicy(`${name}MediaCachePolicy`, {
61
+ name: $interpolate `${$app.name}-${$app.stage}-media-cache`,
62
+ comment: "Cache policy for original media files (immutable)",
63
+ defaultTtl: 31536000,
64
+ maxTtl: 31536000,
65
+ minTtl: 31536000,
66
+ parametersInCacheKeyAndForwardedToOrigin: {
67
+ cookiesConfig: { cookieBehavior: "none" },
68
+ headersConfig: { headerBehavior: "none" },
69
+ queryStringsConfig: { queryStringBehavior: "none" },
70
+ enableAcceptEncodingBrotli: true,
71
+ enableAcceptEncodingGzip: true,
72
+ },
73
+ });
74
+ // =========================================================================
75
+ // Origin Access Controls (OAC)
76
+ // =========================================================================
77
+ const mediaOAC = new aws.cloudfront.OriginAccessControl(`${name}MediaOAC`, {
78
+ name: $interpolate `${$app.name}-${$app.stage}-media-oac`,
79
+ description: "OAC for S3 media origin",
80
+ originAccessControlOriginType: "s3",
81
+ signingBehavior: "always",
82
+ signingProtocol: "sigv4",
83
+ });
84
+ const imageOAC = new aws.cloudfront.OriginAccessControl(`${name}ImageOAC`, {
85
+ name: $interpolate `${$app.name}-${$app.stage}-image-oac`,
86
+ description: "OAC for image transform Lambda origin",
87
+ originAccessControlOriginType: "lambda",
88
+ signingBehavior: "always",
89
+ signingProtocol: "sigv4",
90
+ });
91
+ // =========================================================================
92
+ // CloudFront Distribution
93
+ // =========================================================================
94
+ const imageLambdaDomain = image.imageLambda.url.apply((url) => {
95
+ const parsed = new URL(url);
96
+ return parsed.hostname;
97
+ });
98
+ const s3RegionalDomain = $interpolate `${contentBucket.name}.s3.${aws.getRegionOutput().name}.amazonaws.com`;
99
+ const priceClass = args.priceClass ?? "PriceClass_100";
100
+ // Build aliases and certificate config for custom domain
101
+ const aliases = args.domain ? [args.domain.name] : undefined;
102
+ const viewerCertificate = args.domain
103
+ ? {
104
+ acmCertificateArn: args.domain.certificateArn,
105
+ sslSupportMethod: "sni-only",
106
+ minimumProtocolVersion: "TLSv1.2_2021",
107
+ }
108
+ : {
109
+ cloudfrontDefaultCertificate: true,
110
+ };
111
+ // Fresh resource ID `${name}MediaDistribution` (parallel to
112
+ // `${name}ApiDistribution` in cdn-api.ts) — same blue/green rationale.
113
+ const distribution = new aws.cloudfront.Distribution(`${name}MediaDistribution`, {
114
+ enabled: true,
115
+ comment: $interpolate `Headroom CMS Media - ${$app.stage}`,
116
+ httpVersion: "http2and3",
117
+ priceClass,
118
+ aliases,
119
+ origins: [
120
+ {
121
+ originId: "media-s3",
122
+ domainName: s3RegionalDomain,
123
+ originAccessControlId: mediaOAC.id,
124
+ s3OriginConfig: {
125
+ originAccessIdentity: "",
126
+ },
127
+ },
128
+ {
129
+ originId: "image-lambda",
130
+ domainName: imageLambdaDomain,
131
+ originAccessControlId: imageOAC.id,
132
+ customOriginConfig: {
133
+ httpPort: 80,
134
+ httpsPort: 443,
135
+ originProtocolPolicy: "https-only",
136
+ originSslProtocols: ["TLSv1.2"],
137
+ },
138
+ },
139
+ ],
140
+ // Default behavior: target media-s3 with mediaCachePolicy and NO
141
+ // viewer-request rewrite. Anything not under /media/* or /img/* hits
142
+ // S3 with the original URI and returns 403 AccessDenied — defense in
143
+ // depth so a typo can't accidentally rewrite to a real S3 key.
144
+ defaultCacheBehavior: {
145
+ targetOriginId: "media-s3",
146
+ viewerProtocolPolicy: "redirect-to-https",
147
+ allowedMethods: ["GET", "HEAD", "OPTIONS"],
148
+ cachedMethods: ["GET", "HEAD", "OPTIONS"],
149
+ compress: true,
150
+ cachePolicyId: mediaCachePolicy.id,
151
+ },
152
+ orderedCacheBehaviors: [
153
+ // Media originals: served directly from S3 via OAC. Explicit (not
154
+ // implicit-via-default) so the rewrite never runs on unrelated paths.
155
+ {
156
+ pathPattern: "/media/*",
157
+ targetOriginId: "media-s3",
158
+ viewerProtocolPolicy: "redirect-to-https",
159
+ allowedMethods: ["GET", "HEAD", "OPTIONS"],
160
+ cachedMethods: ["GET", "HEAD", "OPTIONS"],
161
+ compress: true,
162
+ cachePolicyId: mediaCachePolicy.id,
163
+ functionAssociations: [
164
+ {
165
+ eventType: "viewer-request",
166
+ functionArn: mediaRewriteFunction.arn,
167
+ },
168
+ ],
169
+ },
170
+ // Image transforms: served via Sharp Lambda
171
+ {
172
+ pathPattern: "/img/*",
173
+ targetOriginId: "image-lambda",
174
+ viewerProtocolPolicy: "redirect-to-https",
175
+ allowedMethods: ["GET", "HEAD", "OPTIONS"],
176
+ cachedMethods: ["GET", "HEAD", "OPTIONS"],
177
+ compress: true,
178
+ cachePolicyId: imageCachePolicy.id,
179
+ },
180
+ ],
181
+ restrictions: {
182
+ geoRestriction: { restrictionType: "none" },
183
+ },
184
+ viewerCertificate,
185
+ });
186
+ // Allow CloudFront to invoke the image Lambda via OAC.
187
+ // CRITICAL: sourceArn must reference the MEDIA distribution's ARN. Pointing
188
+ // it at the API CDN ARN would silently break image transforms in a way
189
+ // that's hard to spot from logs. (Single most-important consistency point
190
+ // per the design doc.)
191
+ new aws.lambda.Permission(`${name}ImageLambdaCFPermission`, {
192
+ action: "lambda:InvokeFunctionUrl",
193
+ function: image.imageLambda.name,
194
+ principal: "cloudfront.amazonaws.com",
195
+ sourceArn: distribution.arn,
196
+ });
197
+ const url = args.domain
198
+ ? $interpolate `https://${args.domain.name}`
199
+ : $interpolate `https://${distribution.domainName}`;
200
+ return { distribution, url };
201
+ }
202
+ //# sourceMappingURL=cdn-media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdn-media.js","sourceRoot":"","sources":["../src/cdn-media.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Collaboration Infrastructure
3
+ *
4
+ * API Gateway WebSocket API + DynamoDB Collab table + Node.js Lambda handler.
5
+ * Deployed at all times but dormant for solo editing — no Lambda invocations
6
+ * and no Collab table writes happen until collaboration is opted into.
7
+ *
8
+ * Supports dev mode (Node.js source with live reload) and package mode
9
+ * (pre-bundled handler).
10
+ *
11
+ * Split into two helpers:
12
+ * - `createCollabTable` — provisions just the DynamoDB Collab table.
13
+ * Must be called BEFORE `createApi`, because the Go API links the
14
+ * Collab table for ticket reads/writes.
15
+ * - `createCollabHandler` — provisions the WebSocket API + Lambda and
16
+ * wires `API_URL` into the Lambda environment so `snapshotToDraft`
17
+ * can `fetch` back into the Go API on `$disconnect`. Must be called
18
+ * AFTER `createApi`, because it needs `api.api.url`.
19
+ *
20
+ * The two halves are merged into a single `CollaborationResources` value
21
+ * by `index.ts` so downstream consumers (admin-site, etc.) see one shape.
22
+ */
23
+ import type { StorageResources } from "./storage.js";
24
+ import type { AuthResources } from "./auth.js";
25
+ import type { ApiResources } from "./api.js";
26
+ export interface CollabTableArgs {
27
+ }
28
+ export declare function createCollabTable(name: string, _args?: CollabTableArgs): {
29
+ collabTable: sst.aws.Dynamo;
30
+ };
31
+ export type CollabTableResources = ReturnType<typeof createCollabTable>;
32
+ export interface CollabHandlerArgs {
33
+ storage: StorageResources;
34
+ auth: AuthResources;
35
+ api: ApiResources;
36
+ collabTable: CollabTableResources["collabTable"];
37
+ pkgRoot: string;
38
+ dev?: {
39
+ /** SST handler string, e.g. "packages/collab/src/index.handler" */
40
+ handler: string;
41
+ };
42
+ }
43
+ export declare function createCollabHandler(name: string, args: CollabHandlerArgs): {
44
+ wsApi: aws.apigatewayv2.Api;
45
+ wsHandler: sst.aws.Function;
46
+ wsUrl: PulumiOutput<string>;
47
+ };
48
+ export type CollabHandlerResources = ReturnType<typeof createCollabHandler>;
49
+ /**
50
+ * Combined collaboration resource shape — the union of the table half
51
+ * (created early) and the handler half (created late). Downstream consumers
52
+ * (admin-site, public outputs) see a single object.
53
+ */
54
+ export type CollaborationResources = CollabTableResources & CollabHandlerResources;
55
+ //# sourceMappingURL=collaboration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collaboration.d.ts","sourceRoot":"","sources":["../src/collaboration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C,MAAM,WAAW,eAAe;CAE/B;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,eAAoB;;EAgB1E;AAED,MAAM,MAAM,oBAAoB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAExE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,gBAAgB,CAAC;IAC1B,IAAI,EAAE,aAAa,CAAC;IACpB,GAAG,EAAE,YAAY,CAAC;IAClB,WAAW,EAAE,oBAAoB,CAAC,aAAa,CAAC,CAAC;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE;QACJ,mEAAmE;QACnE,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB;;;;EAiHxE;AAED,MAAM,MAAM,sBAAsB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAE5E;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG,oBAAoB,GAAG,sBAAsB,CAAC"}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Collaboration Infrastructure
3
+ *
4
+ * API Gateway WebSocket API + DynamoDB Collab table + Node.js Lambda handler.
5
+ * Deployed at all times but dormant for solo editing — no Lambda invocations
6
+ * and no Collab table writes happen until collaboration is opted into.
7
+ *
8
+ * Supports dev mode (Node.js source with live reload) and package mode
9
+ * (pre-bundled handler).
10
+ *
11
+ * Split into two helpers:
12
+ * - `createCollabTable` — provisions just the DynamoDB Collab table.
13
+ * Must be called BEFORE `createApi`, because the Go API links the
14
+ * Collab table for ticket reads/writes.
15
+ * - `createCollabHandler` — provisions the WebSocket API + Lambda and
16
+ * wires `API_URL` into the Lambda environment so `snapshotToDraft`
17
+ * can `fetch` back into the Go API on `$disconnect`. Must be called
18
+ * AFTER `createApi`, because it needs `api.api.url`.
19
+ *
20
+ * The two halves are merged into a single `CollaborationResources` value
21
+ * by `index.ts` so downstream consumers (admin-site, etc.) see one shape.
22
+ */
23
+ import path from "path";
24
+ export function createCollabTable(name, _args = {}) {
25
+ // DynamoDB table for collaboration sessions + YJS state
26
+ const collabTable = new sst.aws.Dynamo(`${name}Collab`, {
27
+ fields: {
28
+ pk: "string",
29
+ sk: "string",
30
+ },
31
+ primaryIndex: { hashKey: "pk", rangeKey: "sk" },
32
+ // PascalCase to match the convention used throughout the collab TS
33
+ // package (RoomManager, YjsPersistence) and the Go ticket writer
34
+ // (repository/collab.go). DynamoDB attribute names are case-sensitive;
35
+ // the TTL attribute name must match what writers actually emit.
36
+ ttl: "ExpiresAt",
37
+ });
38
+ return { collabTable };
39
+ }
40
+ export function createCollabHandler(name, args) {
41
+ const { storage, auth, api, collabTable } = args;
42
+ // WebSocket API via raw Pulumi (SST v4 doesn't have a WebSocket component)
43
+ const wsApi = new aws.apigatewayv2.Api(`${name}WsApi`, {
44
+ protocolType: "WEBSOCKET",
45
+ routeSelectionExpression: "$request.body.action",
46
+ });
47
+ const handlerConfig = args.dev
48
+ ? {
49
+ handler: args.dev.handler,
50
+ runtime: "nodejs22.x",
51
+ }
52
+ : {
53
+ bundle: path.join(args.pkgRoot, "packages/collab"),
54
+ handler: "index.handler",
55
+ runtime: "nodejs22.x",
56
+ };
57
+ const wsHandler = new sst.aws.Function(`${name}CollabHandler`, {
58
+ ...handlerConfig,
59
+ // jsdom is pulled in transitively via @blocknote/server-util (used in
60
+ // src/yjs/converter.ts to convert YJS state to BlockNote blocks during
61
+ // the disconnect snapshot). esbuild does not auto-bundle jsdom's
62
+ // xhr-sync-worker.js because it's spawned dynamically at runtime
63
+ // (`new Worker(__dirname + "/xhr-sync-worker.js")`), so the worker file
64
+ // is missing on disk in the deployed Lambda and cold-starts fail with
65
+ // "Cannot find module './xhr-sync-worker.js'". Adding jsdom to
66
+ // `nodejs.install` excludes it from the esbuild bundle and instead
67
+ // installs it (with its full directory tree, including the worker file)
68
+ // into the function's node_modules/ at deploy time.
69
+ nodejs: {
70
+ install: ["jsdom"],
71
+ },
72
+ timeout: "30 seconds",
73
+ memory: "512 MB",
74
+ environment: {
75
+ COLLAB_TABLE: collabTable.name,
76
+ DRAFT_CONTENT_TABLE: storage.draftContent.name,
77
+ BLOCKS_TABLE: storage.blocks.name,
78
+ SITES_TABLE: storage.sites.name,
79
+ USER_POOL_ID: auth.userPool.id,
80
+ USER_POOL_CLIENT_ID: auth.userPoolClient.id,
81
+ WS_API_ENDPOINT: $interpolate `https://${wsApi.id}.execute-api.${aws.getRegionOutput().name}.amazonaws.com/${$app.stage}`,
82
+ // Required by `snapshotToDraft` (packages/collab/src/draft/snapshot.ts)
83
+ // which fetches `${apiUrl}/v1/admin/sites/{host}/content/{id}/draft`
84
+ // on `$disconnect` to persist the YJS room state back to DraftContent.
85
+ // Without this, the URL becomes `undefined/v1/admin/...` and the fetch
86
+ // throws "Failed to parse URL", crashing the disconnect Lambda.
87
+ API_URL: api.api.url,
88
+ // Shared with the Go API. Sent as `X-Headroom-Internal` on the
89
+ // disconnect snapshot PUT so the Go JWT middleware accepts the
90
+ // internal service-to-service call. See packages/api/internal/middleware/jwt.go.
91
+ INTERNAL_SECRET: storage.internalSecret.value,
92
+ // Phase 7.1 "Large documents": S3 bucket for YJS state that exceeds
93
+ // DynamoDB's 400KB per-item cap. See `YjsPersistence.saveState`.
94
+ COLLAB_STATE_BUCKET: storage.collabStateBucket.name,
95
+ },
96
+ link: [
97
+ collabTable,
98
+ storage.draftContent,
99
+ storage.blocks,
100
+ storage.sites,
101
+ storage.internalSecret,
102
+ // S3 link grants the Lambda IAM permissions for s3:GetObject /
103
+ // s3:PutObject on the bucket via SST's resource binding.
104
+ storage.collabStateBucket,
105
+ ],
106
+ permissions: [
107
+ {
108
+ actions: ["execute-api:ManageConnections"],
109
+ resources: [$interpolate `arn:aws:execute-api:*:*:${wsApi.id}/*`],
110
+ },
111
+ ],
112
+ });
113
+ // Wire up $connect, $default, $disconnect routes
114
+ const integration = new aws.apigatewayv2.Integration(`${name}WsIntegration`, {
115
+ apiId: wsApi.id,
116
+ integrationType: "AWS_PROXY",
117
+ integrationUri: wsHandler.nodes.function.invokeArn,
118
+ });
119
+ for (const routeKey of ["$connect", "$default", "$disconnect"]) {
120
+ new aws.apigatewayv2.Route(`${name}WsRoute${routeKey}`, {
121
+ apiId: wsApi.id,
122
+ routeKey,
123
+ target: $interpolate `integrations/${integration.id}`,
124
+ });
125
+ }
126
+ new aws.apigatewayv2.Stage(`${name}WsStage`, {
127
+ apiId: wsApi.id,
128
+ name: $app.stage,
129
+ autoDeploy: true,
130
+ });
131
+ // Grant API Gateway permission to invoke Lambda
132
+ new aws.lambda.Permission(`${name}WsPermission`, {
133
+ action: "lambda:InvokeFunction",
134
+ function: wsHandler.nodes.function.name,
135
+ principal: "apigateway.amazonaws.com",
136
+ sourceArn: $interpolate `${wsApi.executionArn}/*/*`,
137
+ });
138
+ const wsUrl = $interpolate `wss://${wsApi.id}.execute-api.${aws.getRegionOutput().name}.amazonaws.com/${$app.stage}`;
139
+ return { wsApi, wsHandler, wsUrl };
140
+ }
141
+ //# sourceMappingURL=collaboration.js.map