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.
- package/README.md +11 -6
- package/admin/assets/{AdminsPage-Bt_ekZen.js → AdminsPage-BnzH9TL3.js} +1 -1
- package/admin/assets/AllContentPage-BtObN6oy.js +1 -0
- package/admin/assets/{ApiKeysPage-BfWCxGhC.js → ApiKeysPage-DEAa8eyC.js} +1 -1
- package/admin/assets/AuditPage-BN9yNsxh.js +1 -0
- package/admin/assets/BlockEditor-3wnisTOZ.js +176 -0
- package/admin/assets/BlockEditor-CQpF8tYb.css +1 -0
- package/admin/assets/BlockTypeEditPage-C2evAESK.js +1 -0
- package/admin/assets/BlockTypesPage-Dhkho6T_.js +1 -0
- package/admin/assets/{BulkActionBar-TRiXXLQd.js → BulkActionBar-BxdfUSrN.js} +1 -1
- package/admin/assets/CollectionEditPage-lOb4hEZy.js +1 -0
- package/admin/assets/{CollectionsPage-ClplrxNn.js → CollectionsPage-CgtOloa1.js} +1 -1
- package/admin/assets/{ContentCreatePage-DfYcEH1u.js → ContentCreatePage-LeQjahp_.js} +1 -1
- package/admin/assets/ContentEditPage-xczr4d_h.js +1 -0
- package/admin/assets/ContentField-pilCbdnA.js +1 -0
- package/admin/assets/ContentListPage-BAKDn1Xy.js +1 -0
- package/admin/assets/CustomBlockPreview-DNnTFM0z.js +479 -0
- package/admin/assets/FieldRenderer-DiOKvkWV.js +2 -0
- package/admin/assets/FilterBar-BZoa63zh.js +1 -0
- package/admin/assets/FloatingComposerController-D4uLQfUX-BMIvFCoE.js +1 -0
- package/admin/assets/IconPicker-CpIgiQTC.js +3 -0
- package/admin/assets/{LoginPage-DutieANA.js → LoginPage-D9ZsGLIi.js} +1 -1
- package/admin/assets/MediaField-CxccCFGQ.js +1 -0
- package/admin/assets/MediaPage-QvMaH2YJ.js +1 -0
- package/admin/assets/Pagination-Df9nQ7Z0.js +1 -0
- package/admin/assets/RelationshipPicker-B3Ftmqxp.js +1 -0
- package/admin/assets/{SiteSettingsPage-BtCC3RKc.js → SiteSettingsPage-6NvH7CiQ.js} +1 -1
- package/admin/assets/{SiteUserEditPage-ClHmp0T-.js → SiteUserEditPage-D5VaQ1Xq.js} +1 -1
- package/admin/assets/SiteUsersPage-BYVduiqs.js +1 -0
- package/admin/assets/{SitesPage-Bw_WBN6v.js → SitesPage-rfWWE0yK.js} +1 -1
- package/admin/assets/{SubmissionDetailPage-DS08LGxd.js → SubmissionDetailPage-BSUR685F.js} +1 -1
- package/admin/assets/SubmissionEditPage-DjLXHjWU.js +1 -0
- package/admin/assets/SubmissionListPage-DBxNEvde.js +1 -0
- package/admin/assets/{TagInput-BILCaC9b.js → TagInput-57c4DG1w.js} +1 -1
- package/admin/assets/{TagsPage-DdeZokow.js → TagsPage-BEO5AwCv.js} +1 -1
- package/admin/assets/{UsersPage-B0vLxjrg.js → UsersPage-BpIRorJ1.js} +1 -1
- package/admin/assets/{WebhookEditPage-SlJE4d3z.js → WebhookEditPage-D5xgi56h.js} +1 -1
- package/admin/assets/{WebhooksPage-C6lGZLpr.js → WebhooksPage-BY7AaiGr.js} +1 -1
- package/admin/assets/{card-hXVtlM0q.js → card-C9hfyHXf.js} +1 -1
- package/admin/assets/checkbox-DVJcwUt1.js +1 -0
- package/admin/assets/{collapsible-B414SspL.js → collapsible-D3d29uJp.js} +1 -1
- package/admin/assets/{command-fvBFHye4.js → command-Bfmj0MEL.js} +1 -1
- package/admin/assets/contentStatus-CkPi9Dh6.js +1 -0
- package/admin/assets/{core.esm-B_kcYf6n.js → core.esm-DdQHdRkd.js} +2 -2
- package/admin/assets/index-BB9Syqw2.css +1 -0
- package/admin/assets/index-Ce5pmRMj.js +18 -0
- package/admin/assets/media-url-DdCoIedP.js +1 -0
- package/admin/assets/popover-CzaQYEEP.js +1 -0
- package/admin/assets/radix-C5ZmWuuL.js +51 -0
- package/admin/assets/select-CrRhFGIi.js +1 -0
- package/admin/assets/serializeToText-2VrsuRUh.js +2 -0
- package/admin/assets/{sortable.esm-QyXA6fio.js → sortable.esm-qVEMoaTg.js} +1 -1
- package/admin/assets/{table-DLoIbCQ5.js → table-_3bMY0_z.js} +1 -1
- package/admin/assets/{textarea-vSXNxwTe.js → textarea-6fq0R6VV.js} +1 -1
- package/admin/assets/useAdminResolver-BJNPz3OG.js +1 -0
- package/admin/assets/useContent-Bs7nel7C.js +1 -0
- package/admin/assets/useContentSearch-B3aTjuCu.js +1 -0
- package/admin/assets/{useMedia-e3sqWm_t.js → useMedia-ae3s_ajC.js} +1 -1
- package/admin/assets/usePageTitle-C1r1-C00.js +1 -0
- package/admin/assets/useSiteUsers-DIaqgNSp.js +1 -0
- package/admin/assets/{useTags-f7AVSLuj.js → useTags-B-HgMVwo.js} +1 -1
- package/admin/assets/{useWebhooks-BH_r8-Mo.js → useWebhooks-BvZjUJkJ.js} +1 -1
- package/admin/assets/yjs-tXBm_srz.js +5 -0
- package/admin/favicon-16x16.png +0 -0
- package/admin/favicon-32x32.png +0 -0
- package/admin/icons/icon-180x180.png +0 -0
- package/admin/icons/icon-192x192.png +0 -0
- package/admin/icons/icon-512x512.png +0 -0
- package/admin/icons/maskable-icon-512x512.png +0 -0
- package/admin/index.html +3 -3
- package/admin/sw.js +1 -1
- package/dist/admin-site.d.ts +16 -2
- package/dist/admin-site.d.ts.map +1 -1
- package/dist/admin-site.js +10 -3
- package/dist/admin-site.js.map +1 -1
- package/dist/api.d.ts +7 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +8 -1
- package/dist/api.js.map +1 -1
- package/dist/cdn-api.d.ts +25 -0
- package/dist/cdn-api.d.ts.map +1 -0
- package/dist/{cdn.js → cdn-api.js} +7 -139
- package/dist/cdn-api.js.map +1 -0
- package/dist/cdn-media.d.ts +26 -0
- package/dist/cdn-media.d.ts.map +1 -0
- package/dist/cdn-media.js +202 -0
- package/dist/cdn-media.js.map +1 -0
- package/dist/collaboration.d.ts +55 -0
- package/dist/collaboration.d.ts.map +1 -0
- package/dist/collaboration.js +141 -0
- package/dist/collaboration.js.map +1 -0
- package/dist/index.d.ts +27 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -12
- package/dist/index.js.map +1 -1
- package/dist/storage.d.ts +2 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +33 -0
- package/dist/storage.js.map +1 -1
- package/lambda/api/bootstrap +0 -0
- package/lambda/image-lambda/node_modules/.package-lock.json +3 -3
- package/lambda/image-lambda/node_modules/semver/README.md +19 -4
- package/lambda/image-lambda/node_modules/semver/bin/semver.js +14 -10
- package/lambda/image-lambda/node_modules/semver/functions/truncate.js +48 -0
- package/lambda/image-lambda/node_modules/semver/index.js +2 -0
- package/lambda/image-lambda/node_modules/semver/internal/re.js +1 -1
- package/lambda/image-lambda/node_modules/semver/package.json +3 -3
- package/lambda/image-lambda/node_modules/semver/range.bnf +5 -4
- package/lambda/webhook-worker/bootstrap +0 -0
- package/package.json +1 -1
- package/src/admin-site.ts +26 -5
- package/src/api.ts +15 -1
- package/src/{cdn.ts → cdn-api.ts} +8 -161
- package/src/cdn-media.ts +250 -0
- package/src/collaboration.ts +187 -0
- package/src/index.ts +77 -14
- package/src/sst-env.d.ts +28 -0
- package/src/storage.ts +35 -0
- package/admin/assets/AllContentPage-CFqEMAl9.js +0 -1
- package/admin/assets/AuditPage-BE0XIUl2.js +0 -1
- package/admin/assets/BlockEditor-6wqsThJ7.js +0 -179
- package/admin/assets/BlockEditor-Cp_wZ2xN.css +0 -1
- package/admin/assets/BlockTypeEditPage-CuNJfZw0.js +0 -1
- package/admin/assets/BlockTypesPage-BIMBVxBs.js +0 -1
- package/admin/assets/CollectionEditPage-BqX_0cC2.js +0 -1
- package/admin/assets/ContentEditPage-D3Rvlktk.js +0 -2
- package/admin/assets/ContentListPage-zmO8Is4d.js +0 -1
- package/admin/assets/CustomBlockPreview-C6HqS4xv.js +0 -479
- package/admin/assets/FieldBuilder-36tfpSyM.js +0 -3
- package/admin/assets/FilterBar-DhRwTqFv.js +0 -1
- package/admin/assets/MediaField-J2TLG_fu.js +0 -1
- package/admin/assets/MediaPage-DZZKMGF4.js +0 -1
- package/admin/assets/RelationshipPicker-CDFs4TMW.js +0 -1
- package/admin/assets/SiteUsersPage-AyJvcVM7.js +0 -1
- package/admin/assets/SubmissionEditPage-Brf-DK2X.js +0 -1
- package/admin/assets/SubmissionListPage-DNMzQZHS.js +0 -1
- package/admin/assets/checkbox-WGrS3sUr.js +0 -1
- package/admin/assets/contentStatus-BmaiYVOm.js +0 -1
- package/admin/assets/index-Cir9tY_P.js +0 -18
- package/admin/assets/index-DACBYsKM.css +0 -1
- package/admin/assets/media-url-DIg_vSyf.js +0 -1
- package/admin/assets/popover-D5_HjjUC.js +0 -1
- package/admin/assets/radix-C1kb_NqW.js +0 -51
- package/admin/assets/select-_uJYxzeZ.js +0 -1
- package/admin/assets/serializeToText-DR_WnxiI.js +0 -2
- package/admin/assets/useAdminResolver-D-LlmquD.js +0 -1
- package/admin/assets/useContent-e8beBIuq.js +0 -1
- package/admin/assets/useContentSearch-DOjveB9t.js +0 -1
- package/admin/assets/useDebouncedValue-C-cQUcLG.js +0 -1
- package/admin/assets/usePageTitle-BNSba9_L.js +0 -1
- package/admin/assets/useSiteUsers-BdnvuM2E.js +0 -1
- package/dist/cdn.d.ts +0 -27
- package/dist/cdn.d.ts.map +0 -1
- package/dist/cdn.js.map +0 -1
package/src/api.ts
CHANGED
|
@@ -10,12 +10,19 @@ import type { StorageResources } from "./storage.js";
|
|
|
10
10
|
import type { AuthResources } from "./auth.js";
|
|
11
11
|
import type { WebhookResources } from "./webhooks.js";
|
|
12
12
|
import type { ImageResources } from "./image.js";
|
|
13
|
+
import type { CollabTableResources } from "./collaboration.js";
|
|
13
14
|
|
|
14
15
|
export interface ApiArgs {
|
|
15
16
|
storage: StorageResources;
|
|
16
17
|
auth: AuthResources;
|
|
17
18
|
webhooks: WebhookResources;
|
|
18
19
|
image: ImageResources;
|
|
20
|
+
/**
|
|
21
|
+
* Just the collab DynamoDB table — the API Lambda only links the table
|
|
22
|
+
* for ticket reads/writes, it does NOT depend on the WebSocket handler.
|
|
23
|
+
* (The WS handler is created AFTER the API because its env needs api.url.)
|
|
24
|
+
*/
|
|
25
|
+
collab: CollabTableResources;
|
|
19
26
|
senderEmail: string;
|
|
20
27
|
pkgRoot: string;
|
|
21
28
|
dev?: {
|
|
@@ -25,7 +32,7 @@ export interface ApiArgs {
|
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export function createApi(name: string, args: ApiArgs) {
|
|
28
|
-
const { storage, auth, webhooks, image } = args;
|
|
35
|
+
const { storage, auth, webhooks, image, collab } = args;
|
|
29
36
|
|
|
30
37
|
const handlerConfig = args.dev
|
|
31
38
|
? {
|
|
@@ -67,6 +74,11 @@ export function createApi(name: string, args: ApiArgs) {
|
|
|
67
74
|
RELATIONSHIPS_TABLE: storage.relationships.name,
|
|
68
75
|
SITE_USERS_TABLE: storage.siteUsers.name,
|
|
69
76
|
SES_FROM_EMAIL: args.senderEmail,
|
|
77
|
+
COLLAB_TABLE: collab.collabTable.name,
|
|
78
|
+
// Shared with the collab Lambda. The Go JWT middleware verifies
|
|
79
|
+
// `X-Headroom-Internal` against this on internal service calls
|
|
80
|
+
// (collab → draft snapshot). See packages/api/internal/middleware/jwt.go.
|
|
81
|
+
INTERNAL_SECRET: storage.internalSecret.value,
|
|
70
82
|
},
|
|
71
83
|
link: [
|
|
72
84
|
storage.sites,
|
|
@@ -84,6 +96,8 @@ export function createApi(name: string, args: ApiArgs) {
|
|
|
84
96
|
image.imageSigningSecret,
|
|
85
97
|
storage.relationships,
|
|
86
98
|
storage.siteUsers,
|
|
99
|
+
collab.collabTable,
|
|
100
|
+
storage.internalSecret,
|
|
87
101
|
],
|
|
88
102
|
permissions: [
|
|
89
103
|
{
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CDN Infrastructure
|
|
2
|
+
* API CDN Infrastructure
|
|
3
3
|
*
|
|
4
|
-
* CloudFront distribution
|
|
5
|
-
*
|
|
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
8
|
|
|
8
9
|
import type { StorageResources } from "./storage.js";
|
|
9
10
|
import type { ApiResources } from "./api.js";
|
|
10
|
-
import type { ImageResources } from "./image.js";
|
|
11
11
|
|
|
12
|
-
export interface
|
|
12
|
+
export interface ApiCdnArgs {
|
|
13
13
|
api: ApiResources;
|
|
14
|
-
image: ImageResources;
|
|
15
|
-
contentBucket: StorageResources["contentBucket"];
|
|
16
14
|
kvs: StorageResources["kvs"];
|
|
17
15
|
priceClass?: "PriceClass_100" | "PriceClass_200" | "PriceClass_All";
|
|
18
16
|
apiCacheTtl?: number;
|
|
@@ -22,8 +20,8 @@ export interface CdnArgs {
|
|
|
22
20
|
};
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
export function
|
|
26
|
-
const { api,
|
|
23
|
+
export function createApiCdn(name: string, args: ApiCdnArgs) {
|
|
24
|
+
const { api, kvs } = args;
|
|
27
25
|
const apiCacheTtl = args.apiCacheTtl ?? 3600;
|
|
28
26
|
|
|
29
27
|
// Extract KVS UUID from ARN (cf.kvs() needs the UUID, not the name)
|
|
@@ -109,36 +107,6 @@ export function createCdn(name: string, args: CdnArgs) {
|
|
|
109
107
|
`,
|
|
110
108
|
});
|
|
111
109
|
|
|
112
|
-
// =========================================================================
|
|
113
|
-
// CloudFront Function: Media Rewrite
|
|
114
|
-
// =========================================================================
|
|
115
|
-
const mediaRewriteFunction = new aws.cloudfront.Function(
|
|
116
|
-
`${name}MediaRewrite`,
|
|
117
|
-
{
|
|
118
|
-
name: $interpolate`${$app.name}-${$app.stage}-media-rewrite`,
|
|
119
|
-
runtime: "cloudfront-js-2.0",
|
|
120
|
-
publish: true,
|
|
121
|
-
code: `
|
|
122
|
-
function handler(event) {
|
|
123
|
-
var request = event.request;
|
|
124
|
-
var uri = request.uri;
|
|
125
|
-
|
|
126
|
-
// Rewrite /media/{site}/{mediaId}/{file} → /sites/{site}/media/{mediaId}/{file}
|
|
127
|
-
var parts = uri.split('/');
|
|
128
|
-
// parts: ['', 'media', '{site}', '{mediaId}', '{file}']
|
|
129
|
-
if (parts.length >= 5 && parts[1] === 'media') {
|
|
130
|
-
var site = parts[2];
|
|
131
|
-
var mediaId = parts[3];
|
|
132
|
-
var rest = parts.slice(4).join('/');
|
|
133
|
-
request.uri = '/sites/' + site + '/media/' + mediaId + '/' + rest;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return request;
|
|
137
|
-
}
|
|
138
|
-
`,
|
|
139
|
-
},
|
|
140
|
-
);
|
|
141
|
-
|
|
142
110
|
// =========================================================================
|
|
143
111
|
// Cache Policies
|
|
144
112
|
// =========================================================================
|
|
@@ -175,47 +143,6 @@ export function createCdn(name: string, args: CdnArgs) {
|
|
|
175
143
|
},
|
|
176
144
|
);
|
|
177
145
|
|
|
178
|
-
const imageCachePolicy = new aws.cloudfront.CachePolicy(
|
|
179
|
-
`${name}ImageCachePolicy`,
|
|
180
|
-
{
|
|
181
|
-
name: $interpolate`${$app.name}-${$app.stage}-image-cache`,
|
|
182
|
-
comment: "Cache policy for transformed images (immutable)",
|
|
183
|
-
defaultTtl: 31536000,
|
|
184
|
-
maxTtl: 31536000,
|
|
185
|
-
minTtl: 31536000,
|
|
186
|
-
parametersInCacheKeyAndForwardedToOrigin: {
|
|
187
|
-
cookiesConfig: { cookieBehavior: "none" },
|
|
188
|
-
headersConfig: { headerBehavior: "none" },
|
|
189
|
-
queryStringsConfig: {
|
|
190
|
-
queryStringBehavior: "whitelist",
|
|
191
|
-
queryStrings: {
|
|
192
|
-
items: ["w", "h", "fit", "format", "q", "sig"],
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
enableAcceptEncodingBrotli: true,
|
|
196
|
-
enableAcceptEncodingGzip: true,
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
const mediaCachePolicy = new aws.cloudfront.CachePolicy(
|
|
202
|
-
`${name}MediaCachePolicy`,
|
|
203
|
-
{
|
|
204
|
-
name: $interpolate`${$app.name}-${$app.stage}-media-cache`,
|
|
205
|
-
comment: "Cache policy for original media files (immutable)",
|
|
206
|
-
defaultTtl: 31536000,
|
|
207
|
-
maxTtl: 31536000,
|
|
208
|
-
minTtl: 31536000,
|
|
209
|
-
parametersInCacheKeyAndForwardedToOrigin: {
|
|
210
|
-
cookiesConfig: { cookieBehavior: "none" },
|
|
211
|
-
headersConfig: { headerBehavior: "none" },
|
|
212
|
-
queryStringsConfig: { queryStringBehavior: "none" },
|
|
213
|
-
enableAcceptEncodingBrotli: true,
|
|
214
|
-
enableAcceptEncodingGzip: true,
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
);
|
|
218
|
-
|
|
219
146
|
const versionCachePolicy = new aws.cloudfront.CachePolicy(
|
|
220
147
|
`${name}VersionCachePolicy`,
|
|
221
148
|
{
|
|
@@ -275,26 +202,6 @@ export function createCdn(name: string, args: CdnArgs) {
|
|
|
275
202
|
// authenticated behaviors below.
|
|
276
203
|
const cachingDisabledPolicyId = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad";
|
|
277
204
|
|
|
278
|
-
// =========================================================================
|
|
279
|
-
// Origin Access Controls (OAC)
|
|
280
|
-
// =========================================================================
|
|
281
|
-
|
|
282
|
-
const mediaOAC = new aws.cloudfront.OriginAccessControl(`${name}MediaOAC`, {
|
|
283
|
-
name: $interpolate`${$app.name}-${$app.stage}-media-oac`,
|
|
284
|
-
description: "OAC for S3 media origin",
|
|
285
|
-
originAccessControlOriginType: "s3",
|
|
286
|
-
signingBehavior: "always",
|
|
287
|
-
signingProtocol: "sigv4",
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
const imageOAC = new aws.cloudfront.OriginAccessControl(`${name}ImageOAC`, {
|
|
291
|
-
name: $interpolate`${$app.name}-${$app.stage}-image-oac`,
|
|
292
|
-
description: "OAC for image transform Lambda origin",
|
|
293
|
-
originAccessControlOriginType: "lambda",
|
|
294
|
-
signingBehavior: "always",
|
|
295
|
-
signingProtocol: "sigv4",
|
|
296
|
-
});
|
|
297
|
-
|
|
298
205
|
// =========================================================================
|
|
299
206
|
// CloudFront Distribution
|
|
300
207
|
// =========================================================================
|
|
@@ -304,13 +211,6 @@ export function createCdn(name: string, args: CdnArgs) {
|
|
|
304
211
|
return parsed.hostname;
|
|
305
212
|
});
|
|
306
213
|
|
|
307
|
-
const imageLambdaDomain = image.imageLambda.url.apply((url: string) => {
|
|
308
|
-
const parsed = new URL(url);
|
|
309
|
-
return parsed.hostname;
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
const s3RegionalDomain = $interpolate`${contentBucket.name}.s3.${aws.getRegionOutput().name}.amazonaws.com`;
|
|
313
|
-
|
|
314
214
|
const priceClass = args.priceClass ?? "PriceClass_100";
|
|
315
215
|
|
|
316
216
|
// Build aliases and certificate config for custom domain
|
|
@@ -345,25 +245,6 @@ export function createCdn(name: string, args: CdnArgs) {
|
|
|
345
245
|
originSslProtocols: ["TLSv1.2"],
|
|
346
246
|
},
|
|
347
247
|
},
|
|
348
|
-
{
|
|
349
|
-
originId: "media-s3",
|
|
350
|
-
domainName: s3RegionalDomain,
|
|
351
|
-
originAccessControlId: mediaOAC.id,
|
|
352
|
-
s3OriginConfig: {
|
|
353
|
-
originAccessIdentity: "",
|
|
354
|
-
},
|
|
355
|
-
},
|
|
356
|
-
{
|
|
357
|
-
originId: "image-lambda",
|
|
358
|
-
domainName: imageLambdaDomain,
|
|
359
|
-
originAccessControlId: imageOAC.id,
|
|
360
|
-
customOriginConfig: {
|
|
361
|
-
httpPort: 80,
|
|
362
|
-
httpsPort: 443,
|
|
363
|
-
originProtocolPolicy: "https-only",
|
|
364
|
-
originSslProtocols: ["TLSv1.2"],
|
|
365
|
-
},
|
|
366
|
-
},
|
|
367
248
|
],
|
|
368
249
|
|
|
369
250
|
defaultCacheBehavior: {
|
|
@@ -549,32 +430,6 @@ export function createCdn(name: string, args: CdnArgs) {
|
|
|
549
430
|
},
|
|
550
431
|
],
|
|
551
432
|
},
|
|
552
|
-
// Media originals: served directly from S3 via OAC
|
|
553
|
-
{
|
|
554
|
-
pathPattern: "/media/*",
|
|
555
|
-
targetOriginId: "media-s3",
|
|
556
|
-
viewerProtocolPolicy: "redirect-to-https",
|
|
557
|
-
allowedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
558
|
-
cachedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
559
|
-
compress: true,
|
|
560
|
-
cachePolicyId: mediaCachePolicy.id,
|
|
561
|
-
functionAssociations: [
|
|
562
|
-
{
|
|
563
|
-
eventType: "viewer-request",
|
|
564
|
-
functionArn: mediaRewriteFunction.arn,
|
|
565
|
-
},
|
|
566
|
-
],
|
|
567
|
-
},
|
|
568
|
-
// Image transforms: served via Sharp Lambda
|
|
569
|
-
{
|
|
570
|
-
pathPattern: "/img/*",
|
|
571
|
-
targetOriginId: "image-lambda",
|
|
572
|
-
viewerProtocolPolicy: "redirect-to-https",
|
|
573
|
-
allowedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
574
|
-
cachedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
575
|
-
compress: true,
|
|
576
|
-
cachePolicyId: imageCachePolicy.id,
|
|
577
|
-
},
|
|
578
433
|
// Health endpoint: no caching, no auth
|
|
579
434
|
{
|
|
580
435
|
pathPattern: "/health",
|
|
@@ -596,14 +451,6 @@ export function createCdn(name: string, args: CdnArgs) {
|
|
|
596
451
|
},
|
|
597
452
|
);
|
|
598
453
|
|
|
599
|
-
// Allow CloudFront to invoke the image Lambda via OAC
|
|
600
|
-
new aws.lambda.Permission(`${name}ImageLambdaCFPermission`, {
|
|
601
|
-
action: "lambda:InvokeFunctionUrl",
|
|
602
|
-
function: image.imageLambda.name,
|
|
603
|
-
principal: "cloudfront.amazonaws.com",
|
|
604
|
-
sourceArn: distribution.arn,
|
|
605
|
-
});
|
|
606
|
-
|
|
607
454
|
const url = args.domain
|
|
608
455
|
? $interpolate`https://${args.domain.name}`
|
|
609
456
|
: $interpolate`https://${distribution.domainName}`;
|
|
@@ -611,4 +458,4 @@ export function createCdn(name: string, args: CdnArgs) {
|
|
|
611
458
|
return { distribution, url };
|
|
612
459
|
}
|
|
613
460
|
|
|
614
|
-
export type
|
|
461
|
+
export type ApiCdnResources = ReturnType<typeof createApiCdn>;
|
package/src/cdn-media.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
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
|
+
|
|
11
|
+
import type { StorageResources } from "./storage.js";
|
|
12
|
+
import type { ImageResources } from "./image.js";
|
|
13
|
+
|
|
14
|
+
export interface MediaCdnArgs {
|
|
15
|
+
image: ImageResources;
|
|
16
|
+
contentBucket: StorageResources["contentBucket"];
|
|
17
|
+
priceClass?: "PriceClass_100" | "PriceClass_200" | "PriceClass_All";
|
|
18
|
+
domain?: {
|
|
19
|
+
name: string;
|
|
20
|
+
certificateArn: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createMediaCdn(name: string, args: MediaCdnArgs) {
|
|
25
|
+
const { image, contentBucket } = args;
|
|
26
|
+
|
|
27
|
+
// =========================================================================
|
|
28
|
+
// CloudFront Function: Media Rewrite
|
|
29
|
+
// =========================================================================
|
|
30
|
+
const mediaRewriteFunction = new aws.cloudfront.Function(
|
|
31
|
+
`${name}MediaRewrite`,
|
|
32
|
+
{
|
|
33
|
+
name: $interpolate`${$app.name}-${$app.stage}-media-rewrite`,
|
|
34
|
+
runtime: "cloudfront-js-2.0",
|
|
35
|
+
publish: true,
|
|
36
|
+
code: `
|
|
37
|
+
function handler(event) {
|
|
38
|
+
var request = event.request;
|
|
39
|
+
var uri = request.uri;
|
|
40
|
+
|
|
41
|
+
// Rewrite /media/{site}/{mediaId}/{file} → /sites/{site}/media/{mediaId}/{file}
|
|
42
|
+
var parts = uri.split('/');
|
|
43
|
+
// parts: ['', 'media', '{site}', '{mediaId}', '{file}']
|
|
44
|
+
if (parts.length >= 5 && parts[1] === 'media') {
|
|
45
|
+
var site = parts[2];
|
|
46
|
+
var mediaId = parts[3];
|
|
47
|
+
var rest = parts.slice(4).join('/');
|
|
48
|
+
request.uri = '/sites/' + site + '/media/' + mediaId + '/' + rest;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return request;
|
|
52
|
+
}
|
|
53
|
+
`,
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// =========================================================================
|
|
58
|
+
// Cache Policies
|
|
59
|
+
// =========================================================================
|
|
60
|
+
|
|
61
|
+
const imageCachePolicy = new aws.cloudfront.CachePolicy(
|
|
62
|
+
`${name}ImageCachePolicy`,
|
|
63
|
+
{
|
|
64
|
+
name: $interpolate`${$app.name}-${$app.stage}-image-cache`,
|
|
65
|
+
comment: "Cache policy for transformed images (immutable)",
|
|
66
|
+
defaultTtl: 31536000,
|
|
67
|
+
maxTtl: 31536000,
|
|
68
|
+
minTtl: 31536000,
|
|
69
|
+
parametersInCacheKeyAndForwardedToOrigin: {
|
|
70
|
+
cookiesConfig: { cookieBehavior: "none" },
|
|
71
|
+
headersConfig: { headerBehavior: "none" },
|
|
72
|
+
queryStringsConfig: {
|
|
73
|
+
queryStringBehavior: "whitelist",
|
|
74
|
+
queryStrings: {
|
|
75
|
+
items: ["w", "h", "fit", "format", "q", "sig"],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
enableAcceptEncodingBrotli: true,
|
|
79
|
+
enableAcceptEncodingGzip: true,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const mediaCachePolicy = new aws.cloudfront.CachePolicy(
|
|
85
|
+
`${name}MediaCachePolicy`,
|
|
86
|
+
{
|
|
87
|
+
name: $interpolate`${$app.name}-${$app.stage}-media-cache`,
|
|
88
|
+
comment: "Cache policy for original media files (immutable)",
|
|
89
|
+
defaultTtl: 31536000,
|
|
90
|
+
maxTtl: 31536000,
|
|
91
|
+
minTtl: 31536000,
|
|
92
|
+
parametersInCacheKeyAndForwardedToOrigin: {
|
|
93
|
+
cookiesConfig: { cookieBehavior: "none" },
|
|
94
|
+
headersConfig: { headerBehavior: "none" },
|
|
95
|
+
queryStringsConfig: { queryStringBehavior: "none" },
|
|
96
|
+
enableAcceptEncodingBrotli: true,
|
|
97
|
+
enableAcceptEncodingGzip: true,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// =========================================================================
|
|
103
|
+
// Origin Access Controls (OAC)
|
|
104
|
+
// =========================================================================
|
|
105
|
+
|
|
106
|
+
const mediaOAC = new aws.cloudfront.OriginAccessControl(`${name}MediaOAC`, {
|
|
107
|
+
name: $interpolate`${$app.name}-${$app.stage}-media-oac`,
|
|
108
|
+
description: "OAC for S3 media origin",
|
|
109
|
+
originAccessControlOriginType: "s3",
|
|
110
|
+
signingBehavior: "always",
|
|
111
|
+
signingProtocol: "sigv4",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const imageOAC = new aws.cloudfront.OriginAccessControl(`${name}ImageOAC`, {
|
|
115
|
+
name: $interpolate`${$app.name}-${$app.stage}-image-oac`,
|
|
116
|
+
description: "OAC for image transform Lambda origin",
|
|
117
|
+
originAccessControlOriginType: "lambda",
|
|
118
|
+
signingBehavior: "always",
|
|
119
|
+
signingProtocol: "sigv4",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// =========================================================================
|
|
123
|
+
// CloudFront Distribution
|
|
124
|
+
// =========================================================================
|
|
125
|
+
|
|
126
|
+
const imageLambdaDomain = image.imageLambda.url.apply((url: string) => {
|
|
127
|
+
const parsed = new URL(url);
|
|
128
|
+
return parsed.hostname;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const s3RegionalDomain = $interpolate`${contentBucket.name}.s3.${aws.getRegionOutput().name}.amazonaws.com`;
|
|
132
|
+
|
|
133
|
+
const priceClass = args.priceClass ?? "PriceClass_100";
|
|
134
|
+
|
|
135
|
+
// Build aliases and certificate config for custom domain
|
|
136
|
+
const aliases = args.domain ? [args.domain.name] : undefined;
|
|
137
|
+
const viewerCertificate = args.domain
|
|
138
|
+
? {
|
|
139
|
+
acmCertificateArn: args.domain.certificateArn,
|
|
140
|
+
sslSupportMethod: "sni-only" as const,
|
|
141
|
+
minimumProtocolVersion: "TLSv1.2_2021" as const,
|
|
142
|
+
}
|
|
143
|
+
: {
|
|
144
|
+
cloudfrontDefaultCertificate: true,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Fresh resource ID `${name}MediaDistribution` (parallel to
|
|
148
|
+
// `${name}ApiDistribution` in cdn-api.ts) — same blue/green rationale.
|
|
149
|
+
const distribution = new aws.cloudfront.Distribution(
|
|
150
|
+
`${name}MediaDistribution`,
|
|
151
|
+
{
|
|
152
|
+
enabled: true,
|
|
153
|
+
comment: $interpolate`Headroom CMS Media - ${$app.stage}`,
|
|
154
|
+
httpVersion: "http2and3",
|
|
155
|
+
priceClass,
|
|
156
|
+
aliases,
|
|
157
|
+
|
|
158
|
+
origins: [
|
|
159
|
+
{
|
|
160
|
+
originId: "media-s3",
|
|
161
|
+
domainName: s3RegionalDomain,
|
|
162
|
+
originAccessControlId: mediaOAC.id,
|
|
163
|
+
s3OriginConfig: {
|
|
164
|
+
originAccessIdentity: "",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
originId: "image-lambda",
|
|
169
|
+
domainName: imageLambdaDomain,
|
|
170
|
+
originAccessControlId: imageOAC.id,
|
|
171
|
+
customOriginConfig: {
|
|
172
|
+
httpPort: 80,
|
|
173
|
+
httpsPort: 443,
|
|
174
|
+
originProtocolPolicy: "https-only",
|
|
175
|
+
originSslProtocols: ["TLSv1.2"],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
|
|
180
|
+
// Default behavior: target media-s3 with mediaCachePolicy and NO
|
|
181
|
+
// viewer-request rewrite. Anything not under /media/* or /img/* hits
|
|
182
|
+
// S3 with the original URI and returns 403 AccessDenied — defense in
|
|
183
|
+
// depth so a typo can't accidentally rewrite to a real S3 key.
|
|
184
|
+
defaultCacheBehavior: {
|
|
185
|
+
targetOriginId: "media-s3",
|
|
186
|
+
viewerProtocolPolicy: "redirect-to-https",
|
|
187
|
+
allowedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
188
|
+
cachedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
189
|
+
compress: true,
|
|
190
|
+
cachePolicyId: mediaCachePolicy.id,
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
orderedCacheBehaviors: [
|
|
194
|
+
// Media originals: served directly from S3 via OAC. Explicit (not
|
|
195
|
+
// implicit-via-default) so the rewrite never runs on unrelated paths.
|
|
196
|
+
{
|
|
197
|
+
pathPattern: "/media/*",
|
|
198
|
+
targetOriginId: "media-s3",
|
|
199
|
+
viewerProtocolPolicy: "redirect-to-https",
|
|
200
|
+
allowedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
201
|
+
cachedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
202
|
+
compress: true,
|
|
203
|
+
cachePolicyId: mediaCachePolicy.id,
|
|
204
|
+
functionAssociations: [
|
|
205
|
+
{
|
|
206
|
+
eventType: "viewer-request",
|
|
207
|
+
functionArn: mediaRewriteFunction.arn,
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
// Image transforms: served via Sharp Lambda
|
|
212
|
+
{
|
|
213
|
+
pathPattern: "/img/*",
|
|
214
|
+
targetOriginId: "image-lambda",
|
|
215
|
+
viewerProtocolPolicy: "redirect-to-https",
|
|
216
|
+
allowedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
217
|
+
cachedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
218
|
+
compress: true,
|
|
219
|
+
cachePolicyId: imageCachePolicy.id,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
|
|
223
|
+
restrictions: {
|
|
224
|
+
geoRestriction: { restrictionType: "none" },
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
viewerCertificate,
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Allow CloudFront to invoke the image Lambda via OAC.
|
|
232
|
+
// CRITICAL: sourceArn must reference the MEDIA distribution's ARN. Pointing
|
|
233
|
+
// it at the API CDN ARN would silently break image transforms in a way
|
|
234
|
+
// that's hard to spot from logs. (Single most-important consistency point
|
|
235
|
+
// per the design doc.)
|
|
236
|
+
new aws.lambda.Permission(`${name}ImageLambdaCFPermission`, {
|
|
237
|
+
action: "lambda:InvokeFunctionUrl",
|
|
238
|
+
function: image.imageLambda.name,
|
|
239
|
+
principal: "cloudfront.amazonaws.com",
|
|
240
|
+
sourceArn: distribution.arn,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const url = args.domain
|
|
244
|
+
? $interpolate`https://${args.domain.name}`
|
|
245
|
+
: $interpolate`https://${distribution.domainName}`;
|
|
246
|
+
|
|
247
|
+
return { distribution, url };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export type MediaCdnResources = ReturnType<typeof createMediaCdn>;
|