qlara 0.1.0

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 ADDED
@@ -0,0 +1,1054 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/aws.ts
31
+ var aws_exports = {};
32
+ __export(aws_exports, {
33
+ aws: () => aws
34
+ });
35
+ module.exports = __toCommonJS(aws_exports);
36
+
37
+ // src/provider/aws/index.ts
38
+ var import_client_cloudformation = require("@aws-sdk/client-cloudformation");
39
+ var import_client_lambda = require("@aws-sdk/client-lambda");
40
+ var import_client_iam = require("@aws-sdk/client-iam");
41
+ var import_client_cloudfront = require("@aws-sdk/client-cloudfront");
42
+
43
+ // src/provider/aws/s3.ts
44
+ var import_client_s3 = require("@aws-sdk/client-s3");
45
+ var import_node_fs = require("fs");
46
+ var import_node_path = require("path");
47
+ var CONTENT_TYPES = {
48
+ ".html": "text/html; charset=utf-8",
49
+ ".js": "application/javascript",
50
+ ".mjs": "application/javascript",
51
+ ".css": "text/css",
52
+ ".json": "application/json",
53
+ ".png": "image/png",
54
+ ".jpg": "image/jpeg",
55
+ ".jpeg": "image/jpeg",
56
+ ".gif": "image/gif",
57
+ ".svg": "image/svg+xml",
58
+ ".ico": "image/x-icon",
59
+ ".webp": "image/webp",
60
+ ".woff": "font/woff",
61
+ ".woff2": "font/woff2",
62
+ ".ttf": "font/ttf",
63
+ ".txt": "text/plain",
64
+ ".xml": "application/xml",
65
+ ".webmanifest": "application/manifest+json",
66
+ ".map": "application/json"
67
+ };
68
+ function getContentType(filePath) {
69
+ const ext = (0, import_node_path.extname)(filePath).toLowerCase();
70
+ return CONTENT_TYPES[ext] || "application/octet-stream";
71
+ }
72
+ function getCacheControl(key) {
73
+ if (key.includes("_next/static/") || key.includes(".chunk.")) {
74
+ return "public, max-age=31536000, immutable";
75
+ }
76
+ if (key.endsWith(".html")) {
77
+ return "public, max-age=0, must-revalidate";
78
+ }
79
+ return "public, max-age=86400";
80
+ }
81
+ function createS3Client(region) {
82
+ return new import_client_s3.S3Client({ region });
83
+ }
84
+ function listFiles(dir) {
85
+ const files = [];
86
+ const entries = (0, import_node_fs.readdirSync)(dir);
87
+ for (const entry of entries) {
88
+ const fullPath = (0, import_node_path.join)(dir, entry);
89
+ const stat = (0, import_node_fs.statSync)(fullPath);
90
+ if (stat.isDirectory()) {
91
+ files.push(...listFiles(fullPath));
92
+ } else {
93
+ files.push(fullPath);
94
+ }
95
+ }
96
+ return files;
97
+ }
98
+ async function syncToS3(client, bucketName, buildDir) {
99
+ const files = listFiles(buildDir);
100
+ let uploaded = 0;
101
+ const batchSize = 10;
102
+ for (let i = 0; i < files.length; i += batchSize) {
103
+ const batch = files.slice(i, i + batchSize);
104
+ await Promise.all(
105
+ batch.map(async (filePath) => {
106
+ const key = (0, import_node_path.relative)(buildDir, filePath);
107
+ const body = (0, import_node_fs.readFileSync)(filePath);
108
+ const contentType = getContentType(filePath);
109
+ const cacheControl = getCacheControl(key);
110
+ await client.send(
111
+ new import_client_s3.PutObjectCommand({
112
+ Bucket: bucketName,
113
+ Key: key,
114
+ Body: body,
115
+ ContentType: contentType,
116
+ CacheControl: cacheControl
117
+ })
118
+ );
119
+ uploaded++;
120
+ })
121
+ );
122
+ }
123
+ return uploaded;
124
+ }
125
+ async function emptyBucket(client, bucketName) {
126
+ let continuationToken;
127
+ do {
128
+ const response = await client.send(
129
+ new import_client_s3.ListObjectsV2Command({
130
+ Bucket: bucketName,
131
+ ContinuationToken: continuationToken
132
+ })
133
+ );
134
+ if (response.Contents && response.Contents.length > 0) {
135
+ await client.send(
136
+ new import_client_s3.DeleteObjectsCommand({
137
+ Bucket: bucketName,
138
+ Delete: {
139
+ Objects: response.Contents.map((obj) => ({ Key: obj.Key }))
140
+ }
141
+ })
142
+ );
143
+ }
144
+ continuationToken = response.NextContinuationToken;
145
+ } while (continuationToken);
146
+ }
147
+
148
+ // src/provider/aws/cloudformation.ts
149
+ function buildTemplate(config) {
150
+ return {
151
+ AWSTemplateFormatVersion: "2010-09-09",
152
+ Description: `Qlara ISR infrastructure \u2014 ${config.stackName}`,
153
+ Resources: {
154
+ // ── S3 Bucket ──────────────────────────────────────────────
155
+ ContentBucket: {
156
+ Type: "AWS::S3::Bucket",
157
+ Properties: {
158
+ // No BucketName — let CloudFormation auto-generate a unique name
159
+ PublicAccessBlockConfiguration: {
160
+ BlockPublicAcls: true,
161
+ BlockPublicPolicy: true,
162
+ IgnorePublicAcls: true,
163
+ RestrictPublicBuckets: true
164
+ }
165
+ }
166
+ },
167
+ // ── S3 Bucket Policy (CloudFront OAC) ──────────────────────
168
+ ContentBucketPolicy: {
169
+ Type: "AWS::S3::BucketPolicy",
170
+ Properties: {
171
+ Bucket: { Ref: "ContentBucket" },
172
+ PolicyDocument: {
173
+ Statement: [
174
+ {
175
+ Effect: "Allow",
176
+ Principal: { Service: "cloudfront.amazonaws.com" },
177
+ Action: "s3:GetObject",
178
+ Resource: { "Fn::Sub": "${ContentBucket.Arn}/*" },
179
+ Condition: {
180
+ StringEquals: {
181
+ "AWS:SourceArn": {
182
+ "Fn::Sub": "arn:aws:cloudfront::${AWS::AccountId}:distribution/${Distribution}"
183
+ }
184
+ }
185
+ }
186
+ }
187
+ ]
188
+ }
189
+ }
190
+ },
191
+ // ── CloudFront Origin Access Control ───────────────────────
192
+ OriginAccessControl: {
193
+ Type: "AWS::CloudFront::OriginAccessControl",
194
+ Properties: {
195
+ OriginAccessControlConfig: {
196
+ Name: { "Fn::Sub": "${AWS::StackName}-oac" },
197
+ OriginAccessControlOriginType: "s3",
198
+ SigningBehavior: "always",
199
+ SigningProtocol: "sigv4"
200
+ }
201
+ }
202
+ },
203
+ // ── CloudFront Function (URL rewriting) ────────────────────
204
+ // Rewrites /product/42 → /product/42.html to match Next.js static export convention.
205
+ // Rewrites /about/ → /about/index.html for directory-style paths.
206
+ URLRewriteFunction: {
207
+ Type: "AWS::CloudFront::Function",
208
+ Properties: {
209
+ Name: { "Fn::Sub": "${AWS::StackName}-url-rewrite" },
210
+ AutoPublish: true,
211
+ FunctionConfig: {
212
+ Comment: "Append .html or /index.html to directory-like paths",
213
+ Runtime: "cloudfront-js-2.0"
214
+ },
215
+ FunctionCode: [
216
+ "function handler(event) {",
217
+ " var request = event.request;",
218
+ " var uri = request.uri;",
219
+ ' if (uri === "/") {',
220
+ ' request.uri = "/index.html";',
221
+ ' } else if (uri.endsWith("/")) {',
222
+ ' request.uri += "index.html";',
223
+ ' } else if (!uri.includes(".")) {',
224
+ ' request.uri += ".html";',
225
+ " }",
226
+ " return request;",
227
+ "}"
228
+ ].join("\n")
229
+ }
230
+ },
231
+ // ── IAM Role for Lambda@Edge ───────────────────────────────
232
+ EdgeHandlerRole: {
233
+ Type: "AWS::IAM::Role",
234
+ Properties: {
235
+ AssumeRolePolicyDocument: {
236
+ Version: "2012-10-17",
237
+ Statement: [
238
+ {
239
+ Effect: "Allow",
240
+ Principal: {
241
+ Service: ["lambda.amazonaws.com", "edgelambda.amazonaws.com"]
242
+ },
243
+ Action: "sts:AssumeRole"
244
+ }
245
+ ]
246
+ },
247
+ ManagedPolicyArns: [
248
+ "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
249
+ ],
250
+ Policies: [
251
+ {
252
+ PolicyName: "QlaraEdgePolicy",
253
+ PolicyDocument: {
254
+ Statement: [
255
+ {
256
+ Effect: "Allow",
257
+ Action: ["s3:GetObject"],
258
+ Resource: { "Fn::Sub": "${ContentBucket.Arn}/*" }
259
+ },
260
+ {
261
+ Effect: "Allow",
262
+ Action: ["lambda:InvokeFunction"],
263
+ Resource: { "Fn::GetAtt": ["RendererFunction", "Arn"] }
264
+ }
265
+ ]
266
+ }
267
+ }
268
+ ]
269
+ }
270
+ },
271
+ // ── Lambda@Edge Function ───────────────────────────────────
272
+ // Placeholder code — real handler is deployed via `qlara deploy`
273
+ EdgeHandlerFunction: {
274
+ Type: "AWS::Lambda::Function",
275
+ Properties: {
276
+ FunctionName: { "Fn::Sub": "${AWS::StackName}-edge-handler" },
277
+ Runtime: "nodejs20.x",
278
+ Handler: "edge-handler.handler",
279
+ Role: { "Fn::GetAtt": ["EdgeHandlerRole", "Arn"] },
280
+ Code: {
281
+ ZipFile: "exports.handler = async (event) => event.Records[0].cf.response;"
282
+ },
283
+ MemorySize: 128,
284
+ Timeout: 30
285
+ }
286
+ },
287
+ // Lambda@Edge requires a published version for CloudFront association
288
+ EdgeHandlerVersion: {
289
+ Type: "AWS::Lambda::Version",
290
+ Properties: {
291
+ FunctionName: { Ref: "EdgeHandlerFunction" },
292
+ Description: "Initial placeholder version"
293
+ }
294
+ },
295
+ // Permission for CloudFront/Lambda@Edge replication to invoke the function
296
+ EdgeHandlerPermission: {
297
+ Type: "AWS::Lambda::Permission",
298
+ Properties: {
299
+ FunctionName: { Ref: "EdgeHandlerFunction" },
300
+ Action: "lambda:GetFunction",
301
+ Principal: "edgelambda.amazonaws.com"
302
+ }
303
+ },
304
+ EdgeHandlerReplicationPermission: {
305
+ Type: "AWS::Lambda::Permission",
306
+ Properties: {
307
+ FunctionName: { Ref: "EdgeHandlerFunction" },
308
+ Action: "lambda:GetFunction",
309
+ Principal: "replicator.lambda.amazonaws.com"
310
+ }
311
+ },
312
+ // ── IAM Role for Renderer Lambda ───────────────────────────
313
+ RendererRole: {
314
+ Type: "AWS::IAM::Role",
315
+ Properties: {
316
+ AssumeRolePolicyDocument: {
317
+ Version: "2012-10-17",
318
+ Statement: [
319
+ {
320
+ Effect: "Allow",
321
+ Principal: { Service: "lambda.amazonaws.com" },
322
+ Action: "sts:AssumeRole"
323
+ }
324
+ ]
325
+ },
326
+ ManagedPolicyArns: [
327
+ "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
328
+ ],
329
+ Policies: [
330
+ {
331
+ PolicyName: "QlaraRendererPolicy",
332
+ PolicyDocument: {
333
+ Statement: [
334
+ {
335
+ Effect: "Allow",
336
+ Action: ["s3:PutObject", "s3:GetObject"],
337
+ Resource: { "Fn::Sub": "${ContentBucket.Arn}/*" }
338
+ },
339
+ {
340
+ Effect: "Allow",
341
+ Action: ["s3:ListBucket"],
342
+ Resource: { "Fn::GetAtt": ["ContentBucket", "Arn"] }
343
+ }
344
+ ]
345
+ }
346
+ }
347
+ ]
348
+ }
349
+ },
350
+ // ── Renderer Lambda ────────────────────────────────────────
351
+ // Placeholder code — real handler is deployed via `qlara deploy`
352
+ RendererFunction: {
353
+ Type: "AWS::Lambda::Function",
354
+ Properties: {
355
+ FunctionName: { "Fn::Sub": "${AWS::StackName}-renderer" },
356
+ Runtime: "nodejs20.x",
357
+ Handler: "renderer.handler",
358
+ Role: { "Fn::GetAtt": ["RendererRole", "Arn"] },
359
+ Code: {
360
+ ZipFile: "exports.handler = async () => ({ statusCode: 200 });"
361
+ },
362
+ MemorySize: 256,
363
+ Timeout: 30
364
+ }
365
+ },
366
+ // ── CloudFront Cache Policy ──────────────────────────────────
367
+ // Custom policy with MinTTL=0 so CloudFront respects origin Cache-Control headers.
368
+ // The edge handler sets max-age=0 for fallback responses (must always hit origin)
369
+ // and S3 objects have appropriate cache headers for static assets.
370
+ CachePolicy: {
371
+ Type: "AWS::CloudFront::CachePolicy",
372
+ Properties: {
373
+ CachePolicyConfig: {
374
+ Name: { "Fn::Sub": "${AWS::StackName}-cache-policy" },
375
+ Comment: "Qlara cache policy \u2014 respects origin Cache-Control headers",
376
+ MinTTL: 0,
377
+ DefaultTTL: 86400,
378
+ MaxTTL: 31536e3,
379
+ ParametersInCacheKeyAndForwardedToOrigin: {
380
+ CookiesConfig: { CookieBehavior: "none" },
381
+ HeadersConfig: { HeaderBehavior: "none" },
382
+ QueryStringsConfig: { QueryStringBehavior: "none" },
383
+ EnableAcceptEncodingGzip: true,
384
+ EnableAcceptEncodingBrotli: true
385
+ }
386
+ }
387
+ }
388
+ },
389
+ // ── CloudFront Distribution ────────────────────────────────
390
+ Distribution: {
391
+ Type: "AWS::CloudFront::Distribution",
392
+ Properties: {
393
+ DistributionConfig: {
394
+ Enabled: true,
395
+ DefaultRootObject: "index.html",
396
+ HttpVersion: "http2and3",
397
+ Origins: [
398
+ {
399
+ Id: "s3-origin",
400
+ DomainName: {
401
+ "Fn::GetAtt": ["ContentBucket", "RegionalDomainName"]
402
+ },
403
+ S3OriginConfig: {
404
+ OriginAccessIdentity: ""
405
+ // Empty when using OAC
406
+ },
407
+ OriginAccessControlId: { Ref: "OriginAccessControl" }
408
+ }
409
+ ],
410
+ DefaultCacheBehavior: {
411
+ TargetOriginId: "s3-origin",
412
+ ViewerProtocolPolicy: "redirect-to-https",
413
+ Compress: true,
414
+ // Custom cache policy with MinTTL=0 — respects origin Cache-Control
415
+ CachePolicyId: { Ref: "CachePolicy" },
416
+ LambdaFunctionAssociations: [
417
+ {
418
+ EventType: "origin-response",
419
+ LambdaFunctionARN: { Ref: "EdgeHandlerVersion" },
420
+ IncludeBody: false
421
+ }
422
+ ],
423
+ FunctionAssociations: [
424
+ {
425
+ EventType: "viewer-request",
426
+ FunctionARN: {
427
+ "Fn::GetAtt": ["URLRewriteFunction", "FunctionARN"]
428
+ }
429
+ }
430
+ ]
431
+ }
432
+ // No CustomErrorResponses — the edge handler manages 403/404
433
+ }
434
+ }
435
+ }
436
+ },
437
+ Outputs: {
438
+ BucketName: {
439
+ Value: { Ref: "ContentBucket" },
440
+ Description: "S3 bucket for static content"
441
+ },
442
+ DistributionId: {
443
+ Value: { Ref: "Distribution" },
444
+ Description: "CloudFront distribution ID"
445
+ },
446
+ DistributionDomain: {
447
+ Value: { "Fn::GetAtt": ["Distribution", "DomainName"] },
448
+ Description: "CloudFront distribution domain name"
449
+ },
450
+ EdgeFunctionArn: {
451
+ Value: { "Fn::GetAtt": ["EdgeHandlerFunction", "Arn"] },
452
+ Description: "Lambda@Edge function ARN"
453
+ },
454
+ RendererFunctionArn: {
455
+ Value: { "Fn::GetAtt": ["RendererFunction", "Arn"] },
456
+ Description: "Renderer Lambda function ARN"
457
+ }
458
+ }
459
+ };
460
+ }
461
+
462
+ // src/provider/aws/bundle.ts
463
+ var import_esbuild = require("esbuild");
464
+ var import_node_fs2 = require("fs");
465
+ var import_node_path2 = require("path");
466
+ var import_node_url = require("url");
467
+ var import_archiver = __toESM(require("archiver"), 1);
468
+ var import_node_stream = require("stream");
469
+ var import_meta = {};
470
+ var BUNDLE_DIR = (0, import_node_path2.join)(".qlara", "bundles");
471
+ function getModuleDir() {
472
+ if (typeof __dirname !== "undefined") {
473
+ return __dirname;
474
+ }
475
+ return (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
476
+ }
477
+ function resolveEntry(name) {
478
+ const moduleDir = getModuleDir();
479
+ const sameDirTs = (0, import_node_path2.resolve)(moduleDir, `${name}.ts`);
480
+ if ((0, import_node_fs2.existsSync)(sameDirTs)) return sameDirTs;
481
+ const srcTs = (0, import_node_path2.resolve)(moduleDir, "..", "src", "provider", "aws", `${name}.ts`);
482
+ if ((0, import_node_fs2.existsSync)(srcTs)) return srcTs;
483
+ return (0, import_node_path2.resolve)(moduleDir, `${name}.js`);
484
+ }
485
+ async function createZip(filePath, entryName) {
486
+ return new Promise((resolvePromise, reject) => {
487
+ const chunks = [];
488
+ const converter = new import_node_stream.Writable({
489
+ write(chunk, _encoding, callback) {
490
+ chunks.push(chunk);
491
+ callback();
492
+ }
493
+ });
494
+ const archive = (0, import_archiver.default)("zip", { zlib: { level: 9 } });
495
+ archive.on("error", reject);
496
+ converter.on("finish", () => resolvePromise(Buffer.concat(chunks)));
497
+ archive.pipe(converter);
498
+ archive.file(filePath, { name: entryName });
499
+ archive.finalize();
500
+ });
501
+ }
502
+ async function bundleEdgeHandler(config) {
503
+ (0, import_node_fs2.mkdirSync)(BUNDLE_DIR, { recursive: true });
504
+ const outfile = (0, import_node_path2.join)(BUNDLE_DIR, "edge-handler.js");
505
+ await (0, import_esbuild.build)({
506
+ entryPoints: [resolveEntry("edge-handler")],
507
+ bundle: true,
508
+ platform: "node",
509
+ target: "node20",
510
+ format: "cjs",
511
+ outfile,
512
+ minify: true,
513
+ define: {
514
+ __QLARA_BUCKET_NAME__: JSON.stringify(config.bucketName),
515
+ __QLARA_RENDERER_ARN__: JSON.stringify(config.rendererArn),
516
+ __QLARA_REGION__: JSON.stringify(config.region)
517
+ },
518
+ // Bundle everything — Lambda@Edge must be self-contained
519
+ external: []
520
+ });
521
+ return createZip(outfile, "edge-handler.js");
522
+ }
523
+ async function bundleRenderer(routeFile) {
524
+ (0, import_node_fs2.mkdirSync)(BUNDLE_DIR, { recursive: true });
525
+ const outfile = (0, import_node_path2.join)(BUNDLE_DIR, "renderer.js");
526
+ const alias = {};
527
+ if (routeFile) {
528
+ alias["__qlara_routes__"] = (0, import_node_path2.resolve)(routeFile);
529
+ } else {
530
+ const noopPath = (0, import_node_path2.join)(BUNDLE_DIR, "__qlara_noop_routes.js");
531
+ const { writeFileSync: writeFileSync2 } = await import("fs");
532
+ writeFileSync2(noopPath, "module.exports = { default: [] };");
533
+ alias["__qlara_routes__"] = (0, import_node_path2.resolve)(noopPath);
534
+ }
535
+ await (0, import_esbuild.build)({
536
+ entryPoints: [resolveEntry("renderer")],
537
+ bundle: true,
538
+ platform: "node",
539
+ target: "node20",
540
+ format: "cjs",
541
+ outfile,
542
+ minify: true,
543
+ alias,
544
+ external: []
545
+ });
546
+ return createZip(outfile, "renderer.js");
547
+ }
548
+
549
+ // src/provider/aws/constants.ts
550
+ var STACK_NAME_PREFIX = "qlara";
551
+
552
+ // src/fallback.ts
553
+ var import_node_fs3 = require("fs");
554
+ var import_node_path3 = require("path");
555
+ var FALLBACK_FILENAME = "_fallback.html";
556
+ var FALLBACK_PLACEHOLDER = "__QLARA_FALLBACK__";
557
+ function generateFallbackFromTemplate(templateHtml, routePattern) {
558
+ let fallback = templateHtml;
559
+ const paramNames = (routePattern.match(/:([^/]+)/g) || []).map((m) => m.slice(1));
560
+ fallback = fallback.replace(
561
+ /<title>[^<]*<\/title>/,
562
+ "<title>Loading...</title>"
563
+ );
564
+ fallback = fallback.replace(/<meta\s+(?:name|property)="(?:description|application-name|generator|creator|publisher|category|classification|abstract|referrer|keywords|author|robots|googlebot|og:[^"]*|twitter:[^"]*|fb:[^"]*|al:[^"]*|google-site-verification|y_key|yandex-verification|me|apple-mobile-web-app-[^"]*|format-detection|apple-itunes-app|pinterest-rich-pin)"\s+content="[^"]*"\s*\/?>/g, "");
565
+ fallback = fallback.replace(/<meta\s+content="[^"]*"\s+(?:name|property)="(?:description|application-name|generator|creator|publisher|category|classification|abstract|referrer|keywords|author|robots|googlebot|og:[^"]*|twitter:[^"]*|fb:[^"]*|al:[^"]*|google-site-verification|y_key|yandex-verification|me|apple-mobile-web-app-[^"]*|format-detection|apple-itunes-app|pinterest-rich-pin)"\s*\/?>/g, "");
566
+ fallback = fallback.replace(/<link\s+rel="(?:canonical|alternate|author|icon|shortcut icon|apple-touch-icon|apple-touch-startup-image|manifest|archives|assets|bookmarks|prev|next)"[^>]*\/?>/g, "");
567
+ fallback = fallback.replace(
568
+ /(<main[^>]*>)[\s\S]*?(<\/main>)/,
569
+ "$1<div>Loading...</div>$2"
570
+ );
571
+ const q = '\\\\"';
572
+ for (const param of paramNames) {
573
+ const propsRegex = new RegExp(
574
+ `\\{${q}${param}${q}:${q}[^"]+?${q}(,${q}initial${q}:(null|\\{[^}]*\\}))?\\}`,
575
+ "g"
576
+ );
577
+ fallback = fallback.replace(
578
+ propsRegex,
579
+ `{\\"${param}\\":\\"${FALLBACK_PLACEHOLDER}\\"}`
580
+ );
581
+ }
582
+ for (const param of paramNames) {
583
+ const segmentRegex = new RegExp(
584
+ `\\[${q}${param}${q},${q}[^"]+${q},${q}d${q}\\]`,
585
+ "g"
586
+ );
587
+ fallback = fallback.replace(
588
+ segmentRegex,
589
+ `[\\"${param}\\",\\"${FALLBACK_PLACEHOLDER}\\",\\"d\\"]`
590
+ );
591
+ }
592
+ const routeParts = routePattern.split("/").filter((p) => !p.startsWith(":"));
593
+ if (routeParts.length > 0) {
594
+ const prefix = routeParts.map((p) => `${q}${p}${q}`).join(",");
595
+ const urlSegmentRegex = new RegExp(
596
+ `(${q}c${q}:\\[${prefix},)${q}[^"]*${q}\\]`,
597
+ "g"
598
+ );
599
+ fallback = fallback.replace(
600
+ urlSegmentRegex,
601
+ `$1\\"${FALLBACK_PLACEHOLDER}\\"]`
602
+ );
603
+ }
604
+ fallback = fallback.replace(
605
+ /8:\{\\"metadata\\":\[[\s\S]*?\],\\"error\\":null,\\"digest\\":\\"?\$undefined\\?"\}/,
606
+ '8:{\\"metadata\\":[[\\"$\\",\\"title\\",\\"0\\",{\\"children\\":\\"Loading...\\"}]],\\"error\\":null,\\"digest\\":\\"$undefined\\"}'
607
+ );
608
+ return fallback;
609
+ }
610
+ function generateFallbacks(buildDir, routes) {
611
+ const generated = [];
612
+ for (const route of routes) {
613
+ const parts = route.pattern.replace(/^\//, "").split("/");
614
+ const dirParts = parts.filter((p) => !p.startsWith(":"));
615
+ const routeDir = (0, import_node_path3.join)(buildDir, ...dirParts);
616
+ if (!(0, import_node_fs3.existsSync)(routeDir)) {
617
+ console.warn(
618
+ `[qlara] Warning: No output directory for route ${route.pattern} at ${routeDir}`
619
+ );
620
+ continue;
621
+ }
622
+ const files = (0, import_node_fs3.readdirSync)(routeDir).filter(
623
+ (f) => f.endsWith(".html") && f !== FALLBACK_FILENAME
624
+ );
625
+ if (files.length === 0) {
626
+ console.warn(
627
+ `[qlara] Warning: No HTML files in ${routeDir} to create fallback template`
628
+ );
629
+ continue;
630
+ }
631
+ const templatePath = (0, import_node_path3.join)(routeDir, files[0]);
632
+ const templateHtml = (0, import_node_fs3.readFileSync)(templatePath, "utf-8");
633
+ const fallbackHtml = generateFallbackFromTemplate(templateHtml, route.pattern);
634
+ const fallbackPath = (0, import_node_path3.join)(routeDir, FALLBACK_FILENAME);
635
+ (0, import_node_fs3.writeFileSync)(fallbackPath, fallbackHtml);
636
+ const relativePath = (0, import_node_path3.join)(...dirParts, FALLBACK_FILENAME);
637
+ generated.push(relativePath);
638
+ console.log(`[qlara] Generated fallback: ${relativePath}`);
639
+ }
640
+ return generated;
641
+ }
642
+
643
+ // src/provider/aws/index.ts
644
+ async function getStackOutputs(cfn, stackName) {
645
+ const result = await cfn.send(
646
+ new import_client_cloudformation.DescribeStacksCommand({ StackName: stackName })
647
+ );
648
+ const stack = result.Stacks?.[0];
649
+ if (!stack || !stack.Outputs) {
650
+ throw new Error(
651
+ `[qlara/aws] Stack ${stackName} not found or has no outputs`
652
+ );
653
+ }
654
+ const outputs = {};
655
+ for (const output of stack.Outputs) {
656
+ if (output.OutputKey && output.OutputValue) {
657
+ outputs[output.OutputKey] = output.OutputValue;
658
+ }
659
+ }
660
+ return outputs;
661
+ }
662
+ function outputsToResources(stackName, region, outputs) {
663
+ return {
664
+ provider: "aws",
665
+ region,
666
+ stackName,
667
+ bucketName: outputs.BucketName,
668
+ distributionId: outputs.DistributionId,
669
+ distributionDomain: outputs.DistributionDomain,
670
+ edgeFunctionArn: outputs.EdgeFunctionArn,
671
+ rendererFunctionArn: outputs.RendererFunctionArn
672
+ };
673
+ }
674
+ var CACHING_OPTIMIZED_POLICY_ID = "658327ea-f89d-4fab-a63d-7e88639e58f6";
675
+ var QLARA_CACHE_POLICY_NAME = "qlara-cache-policy";
676
+ async function ensureCachePolicy(cf) {
677
+ const listResult = await cf.send(
678
+ new import_client_cloudfront.ListCachePoliciesCommand({ Type: "custom" })
679
+ );
680
+ const existing = listResult.CachePolicyList?.Items?.find(
681
+ (item) => item.CachePolicy?.CachePolicyConfig?.Name === QLARA_CACHE_POLICY_NAME
682
+ );
683
+ if (existing?.CachePolicy?.Id) {
684
+ return existing.CachePolicy.Id;
685
+ }
686
+ const createResult = await cf.send(
687
+ new import_client_cloudfront.CreateCachePolicyCommand({
688
+ CachePolicyConfig: {
689
+ Name: QLARA_CACHE_POLICY_NAME,
690
+ Comment: "Qlara cache policy \u2014 MinTTL=0, respects origin Cache-Control",
691
+ MinTTL: 0,
692
+ DefaultTTL: 86400,
693
+ MaxTTL: 31536e3,
694
+ ParametersInCacheKeyAndForwardedToOrigin: {
695
+ CookiesConfig: { CookieBehavior: "none" },
696
+ HeadersConfig: { HeaderBehavior: "none" },
697
+ QueryStringsConfig: { QueryStringBehavior: "none" },
698
+ EnableAcceptEncodingGzip: true,
699
+ EnableAcceptEncodingBrotli: true
700
+ }
701
+ }
702
+ })
703
+ );
704
+ const policyId = createResult.CachePolicy?.Id;
705
+ if (!policyId) {
706
+ throw new Error("[qlara/aws] Failed to create cache policy");
707
+ }
708
+ return policyId;
709
+ }
710
+ async function updateCloudFrontEdgeVersion(cf, distributionId, newVersionArn) {
711
+ const getResult = await cf.send(
712
+ new import_client_cloudfront.GetDistributionConfigCommand({ Id: distributionId })
713
+ );
714
+ const config = getResult.DistributionConfig;
715
+ const etag = getResult.ETag;
716
+ if (!config || !etag) {
717
+ throw new Error("[qlara/aws] Could not get distribution config");
718
+ }
719
+ const lambdaAssociations = config.DefaultCacheBehavior?.LambdaFunctionAssociations;
720
+ if (lambdaAssociations?.Items) {
721
+ for (const assoc of lambdaAssociations.Items) {
722
+ if (assoc.EventType === "origin-response") {
723
+ assoc.LambdaFunctionARN = newVersionArn;
724
+ }
725
+ }
726
+ }
727
+ const currentPolicyId = config.DefaultCacheBehavior?.CachePolicyId;
728
+ if (currentPolicyId === CACHING_OPTIMIZED_POLICY_ID) {
729
+ console.log("[qlara/aws] Replacing CachingOptimized with Qlara cache policy (MinTTL=0)...");
730
+ const qlaraPolicyId = await ensureCachePolicy(cf);
731
+ config.DefaultCacheBehavior.CachePolicyId = qlaraPolicyId;
732
+ }
733
+ await cf.send(
734
+ new import_client_cloudfront.UpdateDistributionCommand({
735
+ Id: distributionId,
736
+ DistributionConfig: config,
737
+ IfMatch: etag
738
+ })
739
+ );
740
+ }
741
+ function isByoi(config) {
742
+ return !!(config.bucketName && config.distributionId && config.distributionDomain && config.edgeFunctionArn && config.rendererFunctionArn);
743
+ }
744
+ function byoiResources(config) {
745
+ return {
746
+ provider: "aws",
747
+ region: "us-east-1",
748
+ stackName: "",
749
+ // No stack — externally managed
750
+ bucketName: config.bucketName,
751
+ distributionId: config.distributionId,
752
+ distributionDomain: config.distributionDomain,
753
+ edgeFunctionArn: config.edgeFunctionArn,
754
+ rendererFunctionArn: config.rendererFunctionArn
755
+ };
756
+ }
757
+ function aws(awsConfig = {}) {
758
+ const region = "us-east-1";
759
+ const byoi = isByoi(awsConfig);
760
+ const stackName = byoi ? "" : awsConfig.stackName || STACK_NAME_PREFIX;
761
+ return {
762
+ name: "aws",
763
+ config: { ...awsConfig },
764
+ async setup(_config) {
765
+ if (byoi) {
766
+ console.log("[qlara/aws] Using existing infrastructure (BYOI mode)");
767
+ return byoiResources(awsConfig);
768
+ }
769
+ const template = buildTemplate({ stackName, region });
770
+ const cfn = new import_client_cloudformation.CloudFormationClient({ region });
771
+ let stackExists = false;
772
+ try {
773
+ const existing = await cfn.send(
774
+ new import_client_cloudformation.DescribeStacksCommand({ StackName: stackName })
775
+ );
776
+ const status = existing.Stacks?.[0]?.StackStatus;
777
+ if (status === "ROLLBACK_COMPLETE" || status === "DELETE_FAILED") {
778
+ console.log(
779
+ `[qlara/aws] Found failed stack (${status}). Deleting before retry...`
780
+ );
781
+ await cfn.send(new import_client_cloudformation.DeleteStackCommand({ StackName: stackName }));
782
+ await (0, import_client_cloudformation.waitUntilStackDeleteComplete)(
783
+ { client: cfn, maxWaitTime: 300 },
784
+ { StackName: stackName }
785
+ );
786
+ console.log("[qlara/aws] Old stack deleted");
787
+ } else if (status) {
788
+ stackExists = true;
789
+ }
790
+ } catch {
791
+ }
792
+ if (stackExists) {
793
+ console.log(`[qlara/aws] Stack ${stackName} already exists`);
794
+ } else {
795
+ console.log(`[qlara/aws] Creating CloudFormation stack: ${stackName}`);
796
+ await cfn.send(
797
+ new import_client_cloudformation.CreateStackCommand({
798
+ StackName: stackName,
799
+ TemplateBody: JSON.stringify(template),
800
+ Capabilities: ["CAPABILITY_IAM"]
801
+ })
802
+ );
803
+ console.log(
804
+ "[qlara/aws] Waiting for stack creation (this may take a few minutes)..."
805
+ );
806
+ try {
807
+ await (0, import_client_cloudformation.waitUntilStackCreateComplete)(
808
+ { client: cfn, maxWaitTime: 600 },
809
+ { StackName: stackName }
810
+ );
811
+ } catch {
812
+ const events = await cfn.send(
813
+ new import_client_cloudformation.DescribeStackEventsCommand({ StackName: stackName })
814
+ );
815
+ const failures = (events.StackEvents || []).filter((e) => e.ResourceStatus?.includes("FAILED")).map((e) => ` ${e.LogicalResourceId}: ${e.ResourceStatusReason}`).slice(0, 5);
816
+ if (failures.length) {
817
+ console.error("[qlara/aws] Stack creation failed:");
818
+ failures.forEach((f) => console.error(f));
819
+ }
820
+ throw new Error("CloudFormation stack creation failed");
821
+ }
822
+ console.log("[qlara/aws] Stack created successfully");
823
+ }
824
+ const outputs = await getStackOutputs(cfn, stackName);
825
+ return outputsToResources(stackName, region, outputs);
826
+ },
827
+ async deploy(config, resources) {
828
+ const res = resources;
829
+ const buildDir = config.outputDir;
830
+ console.log("[qlara/aws] Generating fallback pages...");
831
+ const fallbacks = generateFallbacks(buildDir, config.routes);
832
+ console.log(`[qlara/aws] Generated ${fallbacks.length} fallback page(s)`);
833
+ console.log("[qlara/aws] Syncing build output to S3...");
834
+ const s3 = createS3Client(res.region);
835
+ const fileCount = await syncToS3(s3, res.bucketName, buildDir);
836
+ console.log(`[qlara/aws] Uploaded ${fileCount} files to S3`);
837
+ console.log("[qlara/aws] Bundling edge handler...");
838
+ const edgeZip = await bundleEdgeHandler({
839
+ bucketName: res.bucketName,
840
+ rendererArn: res.rendererFunctionArn,
841
+ region: res.region
842
+ });
843
+ const lambda = new import_client_lambda.LambdaClient({ region: res.region });
844
+ console.log("[qlara/aws] Waiting for edge handler to be ready...");
845
+ await (0, import_client_lambda.waitUntilFunctionUpdatedV2)(
846
+ { client: lambda, maxWaitTime: 120 },
847
+ { FunctionName: res.edgeFunctionArn }
848
+ );
849
+ await lambda.send(
850
+ new import_client_lambda.UpdateFunctionConfigurationCommand({
851
+ FunctionName: res.edgeFunctionArn,
852
+ Timeout: 30
853
+ })
854
+ );
855
+ await (0, import_client_lambda.waitUntilFunctionUpdatedV2)(
856
+ { client: lambda, maxWaitTime: 120 },
857
+ { FunctionName: res.edgeFunctionArn }
858
+ );
859
+ console.log("[qlara/aws] Deploying edge handler...");
860
+ await lambda.send(
861
+ new import_client_lambda.UpdateFunctionCodeCommand({
862
+ FunctionName: res.edgeFunctionArn,
863
+ ZipFile: edgeZip
864
+ })
865
+ );
866
+ await (0, import_client_lambda.waitUntilFunctionUpdatedV2)(
867
+ { client: lambda, maxWaitTime: 120 },
868
+ { FunctionName: res.edgeFunctionArn }
869
+ );
870
+ const versionResult = await lambda.send(
871
+ new import_client_lambda.PublishVersionCommand({
872
+ FunctionName: res.edgeFunctionArn
873
+ })
874
+ );
875
+ const newVersionArn = versionResult.FunctionArn;
876
+ if (!newVersionArn) {
877
+ throw new Error(
878
+ "[qlara/aws] Failed to publish new edge handler version"
879
+ );
880
+ }
881
+ console.log(
882
+ `[qlara/aws] Published edge handler version: ${newVersionArn}`
883
+ );
884
+ try {
885
+ await lambda.send(
886
+ new import_client_lambda.AddPermissionCommand({
887
+ FunctionName: res.edgeFunctionArn,
888
+ Qualifier: versionResult.Version,
889
+ StatementId: `CloudFrontInvoke-${versionResult.Version}`,
890
+ Action: "lambda:GetFunction",
891
+ Principal: "edgelambda.amazonaws.com"
892
+ })
893
+ );
894
+ } catch (err) {
895
+ if (!err.message?.includes("already exists")) {
896
+ throw err;
897
+ }
898
+ }
899
+ try {
900
+ await lambda.send(
901
+ new import_client_lambda.AddPermissionCommand({
902
+ FunctionName: res.edgeFunctionArn,
903
+ Qualifier: versionResult.Version,
904
+ StatementId: `ReplicatorInvoke-${versionResult.Version}`,
905
+ Action: "lambda:GetFunction",
906
+ Principal: "replicator.lambda.amazonaws.com"
907
+ })
908
+ );
909
+ } catch (err) {
910
+ if (!err.message?.includes("already exists")) {
911
+ throw err;
912
+ }
913
+ }
914
+ console.log("[qlara/aws] Updating CloudFront distribution...");
915
+ const cf = new import_client_cloudfront.CloudFrontClient({ region: res.region });
916
+ await updateCloudFrontEdgeVersion(cf, res.distributionId, newVersionArn);
917
+ console.log("[qlara/aws] Bundling renderer...");
918
+ const rendererZip = await bundleRenderer(config.routeFile);
919
+ await (0, import_client_lambda.waitUntilFunctionUpdatedV2)(
920
+ { client: lambda, maxWaitTime: 120 },
921
+ { FunctionName: res.rendererFunctionArn }
922
+ );
923
+ console.log("[qlara/aws] Deploying renderer...");
924
+ await lambda.send(
925
+ new import_client_lambda.UpdateFunctionCodeCommand({
926
+ FunctionName: res.rendererFunctionArn,
927
+ ZipFile: rendererZip
928
+ })
929
+ );
930
+ await (0, import_client_lambda.waitUntilFunctionUpdatedV2)(
931
+ { client: lambda, maxWaitTime: 120 },
932
+ { FunctionName: res.rendererFunctionArn }
933
+ );
934
+ console.log("[qlara/aws] Configuring renderer...");
935
+ await lambda.send(
936
+ new import_client_lambda.UpdateFunctionConfigurationCommand({
937
+ FunctionName: res.rendererFunctionArn,
938
+ Layers: [],
939
+ MemorySize: 256,
940
+ Timeout: 30,
941
+ Environment: {
942
+ Variables: config.env ?? {}
943
+ }
944
+ })
945
+ );
946
+ await (0, import_client_lambda.waitUntilFunctionUpdatedV2)(
947
+ { client: lambda, maxWaitTime: 120 },
948
+ { FunctionName: res.rendererFunctionArn }
949
+ );
950
+ console.log("[qlara/aws] Ensuring renderer permissions...");
951
+ try {
952
+ const cfn = new import_client_cloudformation.CloudFormationClient({ region: res.region });
953
+ const stackResources = await cfn.send(
954
+ new import_client_cloudformation.ListStackResourcesCommand({ StackName: res.stackName })
955
+ );
956
+ const rendererRole = stackResources.StackResourceSummaries?.find(
957
+ (r) => r.LogicalResourceId === "RendererRole"
958
+ );
959
+ const contentBucket = stackResources.StackResourceSummaries?.find(
960
+ (r) => r.LogicalResourceId === "ContentBucket"
961
+ );
962
+ if (rendererRole?.PhysicalResourceId && contentBucket?.PhysicalResourceId) {
963
+ const iam = new import_client_iam.IAMClient({ region: res.region });
964
+ const bucketArn = `arn:aws:s3:::${contentBucket.PhysicalResourceId}`;
965
+ await iam.send(
966
+ new import_client_iam.PutRolePolicyCommand({
967
+ RoleName: rendererRole.PhysicalResourceId,
968
+ PolicyName: "QlaraRendererS3Policy",
969
+ PolicyDocument: JSON.stringify({
970
+ Version: "2012-10-17",
971
+ Statement: [
972
+ {
973
+ Effect: "Allow",
974
+ Action: ["s3:GetObject", "s3:PutObject"],
975
+ Resource: `${bucketArn}/*`
976
+ },
977
+ {
978
+ Effect: "Allow",
979
+ Action: ["s3:ListBucket"],
980
+ Resource: bucketArn
981
+ }
982
+ ]
983
+ })
984
+ })
985
+ );
986
+ console.log("[qlara/aws] Renderer S3 permissions applied");
987
+ }
988
+ } catch (err) {
989
+ console.warn(
990
+ `[qlara/aws] Could not update renderer permissions: ${err.message}`
991
+ );
992
+ }
993
+ console.log("[qlara/aws] Invalidating CloudFront cache...");
994
+ await cf.send(
995
+ new import_client_cloudfront.CreateInvalidationCommand({
996
+ DistributionId: res.distributionId,
997
+ InvalidationBatch: {
998
+ CallerReference: `qlara-${Date.now()}`,
999
+ Paths: {
1000
+ Quantity: 1,
1001
+ Items: ["/*"]
1002
+ }
1003
+ }
1004
+ })
1005
+ );
1006
+ console.log("[qlara/aws] Deploy complete!");
1007
+ console.log(
1008
+ `[qlara/aws] Site available at: https://${res.distributionDomain}`
1009
+ );
1010
+ },
1011
+ async exists(_config) {
1012
+ if (byoi) {
1013
+ return byoiResources(awsConfig);
1014
+ }
1015
+ try {
1016
+ const cfn = new import_client_cloudformation.CloudFormationClient({ region });
1017
+ const outputs = await getStackOutputs(cfn, stackName);
1018
+ return outputsToResources(stackName, region, outputs);
1019
+ } catch {
1020
+ return null;
1021
+ }
1022
+ },
1023
+ async teardown(resources) {
1024
+ const res = resources;
1025
+ if (byoi) {
1026
+ console.log(
1027
+ "[qlara/aws] Infrastructure is externally managed (BYOI mode)."
1028
+ );
1029
+ console.log(
1030
+ "[qlara/aws] Skipping teardown \u2014 delete these resources manually."
1031
+ );
1032
+ return;
1033
+ }
1034
+ console.log("[qlara/aws] Emptying S3 bucket...");
1035
+ const s3 = createS3Client(res.region);
1036
+ await emptyBucket(s3, res.bucketName);
1037
+ console.log("[qlara/aws] Deleting CloudFormation stack...");
1038
+ const cfn = new import_client_cloudformation.CloudFormationClient({ region: res.region });
1039
+ await cfn.send(new import_client_cloudformation.DeleteStackCommand({ StackName: res.stackName }));
1040
+ console.log(
1041
+ "[qlara/aws] Waiting for stack deletion (this may take a few minutes)..."
1042
+ );
1043
+ await (0, import_client_cloudformation.waitUntilStackDeleteComplete)(
1044
+ { client: cfn, maxWaitTime: 600 },
1045
+ { StackName: res.stackName }
1046
+ );
1047
+ console.log("[qlara/aws] Infrastructure deleted");
1048
+ }
1049
+ };
1050
+ }
1051
+ // Annotate the CommonJS export names for ESM import in node:
1052
+ 0 && (module.exports = {
1053
+ aws
1054
+ });