qlara 0.1.5 → 0.1.6
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/dist/aws.cjs +2 -2
- package/dist/aws.js +2 -2
- package/dist/cli.js +2 -2
- package/package.json +1 -1
- package/src/provider/aws/edge-handler.ts +55 -55
package/dist/aws.cjs
CHANGED
|
@@ -449,7 +449,7 @@ function buildTemplate(config) {
|
|
|
449
449
|
CachePolicyId: { Ref: "CachePolicy" },
|
|
450
450
|
LambdaFunctionAssociations: [
|
|
451
451
|
{
|
|
452
|
-
EventType: "origin-
|
|
452
|
+
EventType: "origin-request",
|
|
453
453
|
LambdaFunctionARN: { Ref: "EdgeHandlerVersion" },
|
|
454
454
|
IncludeBody: false
|
|
455
455
|
}
|
|
@@ -764,7 +764,7 @@ async function updateCloudFrontEdgeVersion(cf, distributionId, newVersionArn) {
|
|
|
764
764
|
const lambdaAssociations = config.DefaultCacheBehavior?.LambdaFunctionAssociations;
|
|
765
765
|
if (lambdaAssociations?.Items) {
|
|
766
766
|
for (const assoc of lambdaAssociations.Items) {
|
|
767
|
-
if (assoc.EventType === "origin-
|
|
767
|
+
if (assoc.EventType === "origin-request") {
|
|
768
768
|
assoc.LambdaFunctionARN = newVersionArn;
|
|
769
769
|
}
|
|
770
770
|
}
|
package/dist/aws.js
CHANGED
|
@@ -446,7 +446,7 @@ function buildTemplate(config) {
|
|
|
446
446
|
CachePolicyId: { Ref: "CachePolicy" },
|
|
447
447
|
LambdaFunctionAssociations: [
|
|
448
448
|
{
|
|
449
|
-
EventType: "origin-
|
|
449
|
+
EventType: "origin-request",
|
|
450
450
|
LambdaFunctionARN: { Ref: "EdgeHandlerVersion" },
|
|
451
451
|
IncludeBody: false
|
|
452
452
|
}
|
|
@@ -760,7 +760,7 @@ async function updateCloudFrontEdgeVersion(cf, distributionId, newVersionArn) {
|
|
|
760
760
|
const lambdaAssociations = config.DefaultCacheBehavior?.LambdaFunctionAssociations;
|
|
761
761
|
if (lambdaAssociations?.Items) {
|
|
762
762
|
for (const assoc of lambdaAssociations.Items) {
|
|
763
|
-
if (assoc.EventType === "origin-
|
|
763
|
+
if (assoc.EventType === "origin-request") {
|
|
764
764
|
assoc.LambdaFunctionARN = newVersionArn;
|
|
765
765
|
}
|
|
766
766
|
}
|
package/dist/cli.js
CHANGED
|
@@ -457,7 +457,7 @@ function buildTemplate(config) {
|
|
|
457
457
|
CachePolicyId: { Ref: "CachePolicy" },
|
|
458
458
|
LambdaFunctionAssociations: [
|
|
459
459
|
{
|
|
460
|
-
EventType: "origin-
|
|
460
|
+
EventType: "origin-request",
|
|
461
461
|
LambdaFunctionARN: { Ref: "EdgeHandlerVersion" },
|
|
462
462
|
IncludeBody: false
|
|
463
463
|
}
|
|
@@ -768,7 +768,7 @@ async function updateCloudFrontEdgeVersion(cf, distributionId, newVersionArn) {
|
|
|
768
768
|
const lambdaAssociations = config.DefaultCacheBehavior?.LambdaFunctionAssociations;
|
|
769
769
|
if (lambdaAssociations?.Items) {
|
|
770
770
|
for (const assoc of lambdaAssociations.Items) {
|
|
771
|
-
if (assoc.EventType === "origin-
|
|
771
|
+
if (assoc.EventType === "origin-request") {
|
|
772
772
|
assoc.LambdaFunctionARN = newVersionArn;
|
|
773
773
|
}
|
|
774
774
|
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lambda@Edge origin-
|
|
2
|
+
* Lambda@Edge origin-request handler for Qlara.
|
|
3
3
|
*
|
|
4
4
|
* This file is bundled into a self-contained ZIP and deployed to Lambda@Edge.
|
|
5
5
|
* It does NOT run in the developer's Node.js — it runs at CloudFront edge locations.
|
|
6
6
|
*
|
|
7
7
|
* Config values are injected at bundle time via esbuild `define` (Lambda@Edge has no env vars).
|
|
8
8
|
*
|
|
9
|
+
* Runs as an **origin-request** trigger so that generated responses are cached by CloudFront.
|
|
10
|
+
* (Origin-response generated responses are NOT cached — documented AWS behavior.)
|
|
11
|
+
*
|
|
9
12
|
* Flow for a request to /product/5:
|
|
10
13
|
* 1. CloudFront viewer-request rewrites /product/5 → /product/5.html
|
|
11
|
-
* 2.
|
|
12
|
-
* 3.
|
|
13
|
-
* a.
|
|
14
|
-
* b.
|
|
15
|
-
*
|
|
16
|
-
* d. Edge handler serves the fully rendered HTML (first request gets full SEO)
|
|
17
|
-
* e. Subsequent requests hit S3 directly (page is cached)
|
|
14
|
+
* 2. CloudFront checks edge cache → miss → fires this origin-request handler
|
|
15
|
+
* 3. Handler checks S3 for product/5.html:
|
|
16
|
+
* a. File exists → return it directly (CloudFront caches it)
|
|
17
|
+
* b. File doesn't exist → invoke renderer → return rendered HTML (CloudFront caches it)
|
|
18
|
+
* 4. Subsequent requests hit CloudFront edge cache directly (~10-30ms)
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
@@ -48,18 +49,21 @@ interface CloudFrontResponse {
|
|
|
48
49
|
statusDescription: string;
|
|
49
50
|
headers: Record<string, Array<{ key: string; value: string }>>;
|
|
50
51
|
body?: string;
|
|
52
|
+
bodyEncoding?: string;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
interface CloudFrontRequest {
|
|
54
56
|
uri: string;
|
|
55
57
|
querystring: string;
|
|
58
|
+
method: string;
|
|
59
|
+
headers: Record<string, Array<{ key: string; value: string }>>;
|
|
60
|
+
origin?: Record<string, unknown>;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
interface
|
|
63
|
+
interface CloudFrontRequestEvent {
|
|
59
64
|
Records: Array<{
|
|
60
65
|
cf: {
|
|
61
66
|
request: CloudFrontRequest;
|
|
62
|
-
response: CloudFrontResponse;
|
|
63
67
|
};
|
|
64
68
|
}>;
|
|
65
69
|
}
|
|
@@ -227,66 +231,62 @@ async function invokeRenderer(uri: string, match: RouteMatch): Promise<string |
|
|
|
227
231
|
|
|
228
232
|
// ── Response builder ─────────────────────────────────────────────
|
|
229
233
|
|
|
230
|
-
|
|
231
|
-
const READ_ONLY_HEADERS = ['transfer-encoding', 'via'];
|
|
232
|
-
|
|
233
|
-
function buildHtmlResponse(
|
|
234
|
-
html: string,
|
|
235
|
-
originalResponse: CloudFrontResponse
|
|
236
|
-
): CloudFrontResponse {
|
|
237
|
-
const headers: Record<string, Array<{ key: string; value: string }>> = {
|
|
238
|
-
'content-type': [
|
|
239
|
-
{ key: 'Content-Type', value: 'text/html; charset=utf-8' },
|
|
240
|
-
],
|
|
241
|
-
'cache-control': [
|
|
242
|
-
{ key: 'Cache-Control', value: `public, max-age=0, s-maxage=${__QLARA_CACHE_TTL__}, stale-while-revalidate=60` },
|
|
243
|
-
],
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
// Preserve read-only headers from the original response to avoid 502
|
|
247
|
-
for (const headerName of READ_ONLY_HEADERS) {
|
|
248
|
-
if (originalResponse.headers[headerName]) {
|
|
249
|
-
headers[headerName] = originalResponse.headers[headerName];
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
234
|
+
function buildHtmlResponse(html: string): CloudFrontResponse {
|
|
253
235
|
return {
|
|
254
236
|
status: '200',
|
|
255
237
|
statusDescription: 'OK',
|
|
256
|
-
headers
|
|
238
|
+
headers: {
|
|
239
|
+
'content-type': [
|
|
240
|
+
{ key: 'Content-Type', value: 'text/html; charset=utf-8' },
|
|
241
|
+
],
|
|
242
|
+
'cache-control': [
|
|
243
|
+
{ key: 'Cache-Control', value: `public, max-age=0, s-maxage=${__QLARA_CACHE_TTL__}, stale-while-revalidate=60` },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
257
246
|
body: html,
|
|
247
|
+
bodyEncoding: 'text',
|
|
258
248
|
};
|
|
259
249
|
}
|
|
260
250
|
|
|
261
251
|
// ── Handler ──────────────────────────────────────────────────────
|
|
262
252
|
|
|
263
253
|
export async function handler(
|
|
264
|
-
event:
|
|
265
|
-
): Promise<CloudFrontResponse> {
|
|
266
|
-
const
|
|
267
|
-
const response = record.response;
|
|
268
|
-
const request = record.request;
|
|
254
|
+
event: CloudFrontRequestEvent
|
|
255
|
+
): Promise<CloudFrontRequest | CloudFrontResponse> {
|
|
256
|
+
const request = event.Records[0].cf.request;
|
|
269
257
|
const uri = request.uri;
|
|
270
|
-
const status = parseInt(response.status, 10);
|
|
271
258
|
|
|
272
|
-
// 1.
|
|
273
|
-
|
|
274
|
-
return response;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// At this point, the file does NOT exist in S3 (403 from OAC or 404)
|
|
278
|
-
|
|
279
|
-
// 2. Skip non-HTML file requests — these are never dynamic routes
|
|
280
|
-
// e.g. /product/20.txt (RSC flight data), /product/20.json, etc.
|
|
259
|
+
// 1. Non-HTML file requests → forward to S3 origin (handled normally by CloudFront)
|
|
260
|
+
// e.g. /product/20.txt (RSC flight data), /product/20.json, JS, CSS, images, etc.
|
|
281
261
|
const nonHtmlExt = uri.match(/\.([a-z0-9]+)$/)?.[1];
|
|
282
262
|
if (nonHtmlExt && nonHtmlExt !== 'html') {
|
|
283
|
-
return
|
|
263
|
+
return request;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 2. Try to get the file from S3 directly
|
|
267
|
+
const s3Key = uri.replace(/^\//, '');
|
|
268
|
+
if (s3Key) {
|
|
269
|
+
try {
|
|
270
|
+
const s3Response = await s3.send(
|
|
271
|
+
new GetObjectCommand({
|
|
272
|
+
Bucket: __QLARA_BUCKET_NAME__,
|
|
273
|
+
Key: s3Key,
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
const body = await s3Response.Body?.transformToString('utf-8');
|
|
277
|
+
if (body) {
|
|
278
|
+
// File exists in S3 → return it directly (CloudFront will cache this)
|
|
279
|
+
return buildHtmlResponse(body);
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
// File doesn't exist in S3 — continue to dynamic route handling
|
|
283
|
+
}
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
// 3. Fetch manifest and check if this URL matches a Qlara dynamic route
|
|
287
|
-
const manifest = await getManifest();
|
|
288
287
|
// Strip .html suffix that the URL rewrite function adds before matching
|
|
289
288
|
const cleanUri = uri.replace(/\.html$/, '');
|
|
289
|
+
const manifest = await getManifest();
|
|
290
290
|
const match = manifest ? matchRoute(cleanUri, manifest.routes) : null;
|
|
291
291
|
|
|
292
292
|
// 4. If route matches: invoke renderer synchronously to get fully rendered HTML
|
|
@@ -295,17 +295,17 @@ export async function handler(
|
|
|
295
295
|
const renderedHtml = await invokeRenderer(cleanUri, match);
|
|
296
296
|
|
|
297
297
|
if (renderedHtml) {
|
|
298
|
-
return buildHtmlResponse(renderedHtml
|
|
298
|
+
return buildHtmlResponse(renderedHtml);
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
// Renderer failed — fall back to unpatched fallback HTML (no SEO, but page still works)
|
|
302
302
|
const fallbackHtml = await getFallbackHtml(match.route);
|
|
303
303
|
if (fallbackHtml) {
|
|
304
304
|
const patchedHtml = patchFallback(fallbackHtml, match.params);
|
|
305
|
-
return buildHtmlResponse(patchedHtml
|
|
305
|
+
return buildHtmlResponse(patchedHtml);
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
// 5. No match or no fallback — return
|
|
310
|
-
return
|
|
309
|
+
// 5. No match or no fallback — forward to origin (S3 will return 403/404)
|
|
310
|
+
return request;
|
|
311
311
|
}
|