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
|
@@ -1,11 +1,12 @@
|
|
|
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
|
-
export function
|
|
8
|
-
const { api,
|
|
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
|