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 CHANGED
@@ -449,7 +449,7 @@ function buildTemplate(config) {
449
449
  CachePolicyId: { Ref: "CachePolicy" },
450
450
  LambdaFunctionAssociations: [
451
451
  {
452
- EventType: "origin-response",
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-response") {
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-response",
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-response") {
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-response",
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-response") {
771
+ if (assoc.EventType === "origin-request") {
772
772
  assoc.LambdaFunctionARN = newVersionArn;
773
773
  }
774
774
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qlara",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Runtime ISR for static React apps — dynamic routing and SEO metadata for statically exported Next.js apps on AWS",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -1,20 +1,21 @@
1
1
  /**
2
- * Lambda@Edge origin-response handler for Qlara.
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. S3 returns 403 (file doesn't exist, OAC treats missing as 403)
12
- * 3. This origin-response handler intercepts:
13
- * a. Invokes the renderer Lambda synchronously
14
- * b. Renderer fetches metadata from the data source, patches fallback HTML with SEO metadata
15
- * c. Renderer uploads product/5.html to S3 and returns the rendered HTML
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 CloudFrontResponseEvent {
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
- // Lambda@Edge read-only headers — must be preserved from the original response
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: CloudFrontResponseEvent
265
- ): Promise<CloudFrontResponse> {
266
- const record = event.Records[0].cf;
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. If response is 200 (file exists in S3), pass through
273
- if (status !== 403 && status !== 404) {
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 response;
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, response);
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, response);
305
+ return buildHtmlResponse(patchedHtml);
306
306
  }
307
307
  }
308
308
 
309
- // 5. No match or no fallback — return original error
310
- return response;
309
+ // 5. No match or no fallback — forward to origin (S3 will return 403/404)
310
+ return request;
311
311
  }