@fishawack/lab-env 5.4.0-beta.1 → 5.5.0-beta.1

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.
@@ -0,0 +1,517 @@
1
+ "use strict";
2
+
3
+ const expect = require("chai").expect;
4
+ const path = require("path");
5
+ const fs = require("fs-extra");
6
+ const glob = require("glob");
7
+ const { isEqual } = require("lodash");
8
+
9
+ const {
10
+ pullS3,
11
+ parseBucketAndPrefix,
12
+ getLocalFiles,
13
+ createClient,
14
+ } = require("../commands/helpers/content-pull.js");
15
+
16
+ const {
17
+ pullRequests,
18
+ download,
19
+ rewrite,
20
+ urlJoin,
21
+ } = require("../commands/helpers/content-request.js");
22
+
23
+ const fixtureDir = path.join(__dirname, "_fixtures", "content");
24
+ const outputDir = path.join(fixtureDir, "_Build", "content");
25
+ const snapshotPath = path.join(fixtureDir, ".tmp", "content.json");
26
+
27
+ function loadFixtureConfig(branch) {
28
+ const config = fs.readJSONSync(path.join(fixtureDir, "config.json"));
29
+ const branchConfig = config.attributes.targets[branch] || {};
30
+
31
+ return {
32
+ ...config.attributes,
33
+ ...branchConfig,
34
+ };
35
+ }
36
+
37
+ describe("content", () => {
38
+ describe("parseBucketAndPrefix", () => {
39
+ it("Should parse bucket only when no slash", () => {
40
+ const result = parseBucketAndPrefix("my-bucket");
41
+ expect(result.Bucket).to.equal("my-bucket");
42
+ expect(result.Prefix).to.equal("");
43
+ });
44
+
45
+ it("Should parse bucket and prefix when slash present", () => {
46
+ const result = parseBucketAndPrefix("my-bucket/some/prefix");
47
+ expect(result.Bucket).to.equal("my-bucket");
48
+ expect(result.Prefix).to.equal("some/prefix");
49
+ });
50
+
51
+ it("Should handle single level prefix", () => {
52
+ const result = parseBucketAndPrefix("my-bucket/folder");
53
+ expect(result.Bucket).to.equal("my-bucket");
54
+ expect(result.Prefix).to.equal("folder");
55
+ });
56
+ });
57
+
58
+ describe("getLocalFiles", () => {
59
+ it("Should return empty array for non-existent directory", () => {
60
+ const files = getLocalFiles(path.join(fixtureDir, "non-existent"));
61
+ expect(files).to.be.an("array").that.is.empty;
62
+ });
63
+
64
+ it("Should return file list with relative paths", () => {
65
+ const files = getLocalFiles(path.join(fixtureDir, "content-0"));
66
+ expect(files).to.be.an("array").that.is.not.empty;
67
+ expect(files[0]).to.have.property("relativePath");
68
+ expect(files[0]).to.have.property("mtime");
69
+ expect(files[0]).to.have.property("size");
70
+ });
71
+ });
72
+
73
+ describe("urlJoin", () => {
74
+ it("Should join base URL with path segments", () => {
75
+ const result = urlJoin(
76
+ "https://example.com",
77
+ "/api/v1/",
78
+ "endpoint",
79
+ );
80
+ expect(result).to.contain("example.com");
81
+ expect(result).to.contain("endpoint");
82
+ });
83
+
84
+ it("Should handle trailing slashes", () => {
85
+ const result = urlJoin(
86
+ "https://example.com/",
87
+ "api/v1/",
88
+ "endpoint",
89
+ );
90
+ expect(result).to.include("api/v1/endpoint");
91
+ });
92
+ });
93
+
94
+ describe("content:pull — S3", () => {
95
+ beforeEach(() => {
96
+ fs.removeSync(outputDir);
97
+ });
98
+
99
+ it("Should pull down assets from s3 via aws sdk", async () => {
100
+ const config = loadFixtureConfig("aws-s3");
101
+
102
+ await pullS3(config.content[0], path.join(outputDir, "content-0"));
103
+
104
+ expect(glob.sync(path.join(outputDir, "content-0/**/*"))).to.be.an(
105
+ "array",
106
+ ).that.is.not.empty;
107
+ });
108
+
109
+ it("Should pull down assets from s3 when nested", async () => {
110
+ const config = loadFixtureConfig("aws-s3-nested");
111
+
112
+ await pullS3(config.content[0], path.join(outputDir, "content-0"));
113
+
114
+ expect(
115
+ fs.readFileSync(path.join(outputDir, "content-0/test.txt"), {
116
+ encoding: "utf8",
117
+ }),
118
+ ).to.equal("core-test-suite-content-nested");
119
+ });
120
+
121
+ it("Should store in directory with key name when defined", async () => {
122
+ const config = loadFixtureConfig("aws-s3-key");
123
+
124
+ await pullS3(config.content[0], path.join(outputDir, "custom-key"));
125
+
126
+ expect(glob.sync(path.join(outputDir, "custom-key/**/*"))).to.be.an(
127
+ "array",
128
+ ).that.is.not.empty;
129
+ });
130
+
131
+ it("Should sync new assets from s3 (bidirectional)", async () => {
132
+ const config = loadFixtureConfig("aws-s3-sync");
133
+ const saveTo = path.join(outputDir, "custom-key");
134
+ const profile = config.content[0]["aws-s3"];
135
+ const client = createClient(profile);
136
+ const { Bucket, Prefix } = parseBucketAndPrefix(
137
+ config.content[0].location,
138
+ );
139
+
140
+ // Clean up any previous sync test file
141
+ try {
142
+ const { DeleteObjectCommand } = require("@aws-sdk/client-s3");
143
+ await client.send(
144
+ new DeleteObjectCommand({
145
+ Bucket,
146
+ Key: Prefix
147
+ ? `${Prefix}/sync-test.txt`
148
+ : "sync-test.txt",
149
+ }),
150
+ );
151
+ } catch {
152
+ /* may not exist */
153
+ }
154
+
155
+ // First pull to get existing content
156
+ await pullS3(config.content[0], saveTo);
157
+
158
+ // Create a local file to sync up
159
+ fs.mkdirpSync(saveTo);
160
+ fs.writeFileSync(path.join(saveTo, "sync-test.txt"), "sync-test");
161
+
162
+ // Second pull should upload local file then download remote
163
+ await pullS3(config.content[0], saveTo);
164
+
165
+ // Clean local and re-pull — the synced file should come back
166
+ fs.removeSync(saveTo);
167
+ await pullS3(config.content[0], saveTo);
168
+
169
+ expect(() =>
170
+ fs.readFileSync(path.join(saveTo, "sync-test.txt"), {
171
+ encoding: "utf8",
172
+ }),
173
+ ).to.not.throw();
174
+
175
+ // Cleanup remote sync test file
176
+ try {
177
+ const { DeleteObjectCommand } = require("@aws-sdk/client-s3");
178
+ await client.send(
179
+ new DeleteObjectCommand({
180
+ Bucket,
181
+ Key: Prefix
182
+ ? `${Prefix}/sync-test.txt`
183
+ : "sync-test.txt",
184
+ }),
185
+ );
186
+ } catch {
187
+ /* cleanup best-effort */
188
+ }
189
+ });
190
+
191
+ it("Should not re-download files when local is newer (--update semantics)", async () => {
192
+ const config = loadFixtureConfig("aws-s3");
193
+ const saveTo = path.join(outputDir, "content-0");
194
+
195
+ // First pull
196
+ await pullS3(config.content[0], saveTo);
197
+
198
+ const files = glob.sync(path.join(saveTo, "**/*"), {
199
+ nodir: true,
200
+ });
201
+ expect(files.length).to.be.greaterThan(0);
202
+
203
+ // Touch all local files to make them "newer"
204
+ const futureTime = new Date(Date.now() + 86400000);
205
+ files.forEach((f) => fs.utimesSync(f, futureTime, futureTime));
206
+
207
+ // Record mtime before second pull
208
+ const mtimesBefore = {};
209
+ files.forEach((f) => {
210
+ mtimesBefore[f] = fs.statSync(f).mtimeMs;
211
+ });
212
+
213
+ // Second pull should skip all files
214
+ await pullS3(config.content[0], saveTo);
215
+
216
+ // Verify files were not overwritten
217
+ files.forEach((f) => {
218
+ expect(fs.statSync(f).mtimeMs).to.equal(mtimesBefore[f]);
219
+ });
220
+ });
221
+
222
+ it("Should skip non-S3 protocols with warning", async () => {
223
+ const config = loadFixtureConfig("ftp-legacy");
224
+
225
+ // pullS3 won't be called for non-s3 — this is handled in content.js
226
+ // Verify the config doesn't have aws-s3 key
227
+ expect(config.content[0]["aws-s3"]).to.be.undefined;
228
+ expect(config.content[0].ftp).to.equal("ftp-fishawack.egnyte.com");
229
+ });
230
+
231
+ after(() => {
232
+ fs.removeSync(outputDir);
233
+ });
234
+ });
235
+
236
+ describe("config change detection", () => {
237
+ beforeEach(() => {
238
+ fs.removeSync(outputDir);
239
+ fs.removeSync(path.join(fixtureDir, ".tmp"));
240
+ });
241
+
242
+ it("Should clean content folders when content property is an empty array", () => {
243
+ // Pre-populate content directory
244
+ fs.copySync(
245
+ path.join(fixtureDir, "content-0"),
246
+ path.join(outputDir, "content-0"),
247
+ );
248
+
249
+ const config = loadFixtureConfig("empty");
250
+
251
+ expect(config.content).to.be.an("array").that.is.empty;
252
+ expect(glob.sync(path.join(outputDir, "**/*"))).to.be.an("array")
253
+ .that.is.not.empty;
254
+ });
255
+
256
+ it("Should detect when content config has not changed", () => {
257
+ const config = loadFixtureConfig("aws-s3");
258
+
259
+ // Save a snapshot
260
+ fs.mkdirpSync(path.dirname(snapshotPath));
261
+ fs.writeFileSync(snapshotPath, JSON.stringify(config.content));
262
+
263
+ // Compare — should be equal
264
+ const prev = JSON.parse(
265
+ fs.readFileSync(snapshotPath, { encoding: "utf8" }),
266
+ );
267
+ expect(isEqual(prev, config.content)).to.be.true;
268
+ });
269
+
270
+ it("Should detect when content config has changed", () => {
271
+ const configOld = loadFixtureConfig("single");
272
+ const configNew = loadFixtureConfig("double");
273
+
274
+ // Save old snapshot
275
+ fs.mkdirpSync(path.dirname(snapshotPath));
276
+ fs.writeFileSync(snapshotPath, JSON.stringify(configOld.content));
277
+
278
+ const prev = JSON.parse(
279
+ fs.readFileSync(snapshotPath, { encoding: "utf8" }),
280
+ );
281
+ expect(isEqual(prev, configNew.content)).to.be.false;
282
+ });
283
+
284
+ it("Should handle missing previous snapshot gracefully", () => {
285
+ // No snapshot file exists
286
+ expect(fs.existsSync(snapshotPath)).to.be.false;
287
+
288
+ let prev = null;
289
+ try {
290
+ prev = JSON.parse(
291
+ fs.readFileSync(snapshotPath, { encoding: "utf8" }),
292
+ );
293
+ } catch {
294
+ // expected
295
+ }
296
+
297
+ expect(prev).to.be.null;
298
+ });
299
+
300
+ it("Should clean content when config changes between pulls", async () => {
301
+ const configDouble = loadFixtureConfig("double");
302
+
303
+ // Simulate first pull creating two content dirs
304
+ fs.mkdirpSync(path.join(outputDir, "content-0"));
305
+ fs.mkdirpSync(path.join(outputDir, "custom-key"));
306
+ fs.writeFileSync(
307
+ path.join(outputDir, "content-0/file.txt"),
308
+ "test",
309
+ );
310
+ fs.writeFileSync(
311
+ path.join(outputDir, "custom-key/file.txt"),
312
+ "test",
313
+ );
314
+
315
+ // Save snapshot for "double" config
316
+ fs.mkdirpSync(path.dirname(snapshotPath));
317
+ fs.writeFileSync(
318
+ snapshotPath,
319
+ JSON.stringify(configDouble.content),
320
+ );
321
+
322
+ expect(glob.sync(path.join(outputDir, "*")).length).to.equal(2);
323
+
324
+ // Now switch to "single" config — should detect the change
325
+ const configSingle = loadFixtureConfig("single");
326
+ const prev = JSON.parse(
327
+ fs.readFileSync(snapshotPath, { encoding: "utf8" }),
328
+ );
329
+ expect(isEqual(prev, configSingle.content)).to.be.false;
330
+ });
331
+
332
+ it("Should not clean content when config unchanged between pulls", () => {
333
+ const config = loadFixtureConfig("double");
334
+
335
+ // Simulate content dirs
336
+ fs.mkdirpSync(path.join(outputDir, "content-0"));
337
+ fs.mkdirpSync(path.join(outputDir, "custom-key"));
338
+ fs.mkdirpSync(path.join(outputDir, "fake-content"));
339
+
340
+ // Save snapshot
341
+ fs.mkdirpSync(path.dirname(snapshotPath));
342
+ fs.writeFileSync(snapshotPath, JSON.stringify(config.content));
343
+
344
+ // Reload and compare — should be unchanged
345
+ const prev = JSON.parse(
346
+ fs.readFileSync(snapshotPath, { encoding: "utf8" }),
347
+ );
348
+ expect(isEqual(prev, config.content)).to.be.true;
349
+
350
+ // Fake-content should survive (not cleaned)
351
+ expect(glob.sync(path.join(outputDir, "*")).length).to.equal(3);
352
+ });
353
+
354
+ it("Should skip cleaning when content property is undefined", () => {
355
+ // Pre-populate content
356
+ fs.copySync(
357
+ path.join(fixtureDir, "content-0"),
358
+ path.join(outputDir, "content-0"),
359
+ );
360
+
361
+ const config = loadFixtureConfig("handover");
362
+
363
+ // "handover" target has no content property
364
+ expect(config.content).to.be.undefined;
365
+ // Content should remain untouched
366
+ expect(glob.sync(path.join(outputDir, "**/*"))).to.be.an("array")
367
+ .that.is.not.empty;
368
+ });
369
+
370
+ after(() => {
371
+ fs.removeSync(outputDir);
372
+ fs.removeSync(path.join(fixtureDir, ".tmp"));
373
+ });
374
+ });
375
+
376
+ describe("content:request — HTTP APIs", () => {
377
+ describe("contentful", () => {
378
+ const saveTo = path.join(outputDir, "content-0");
379
+
380
+ before(async () => {
381
+ fs.removeSync(outputDir);
382
+
383
+ const config = loadFixtureConfig("contentful");
384
+
385
+ await pullRequests(config.content, fixtureDir + "/_Build");
386
+ });
387
+
388
+ it("Should save to json files", () => {
389
+ expect(
390
+ fs
391
+ .lstatSync(path.join(saveTo, "media/layout.json"))
392
+ .isFile(),
393
+ ).to.be.true;
394
+ expect(
395
+ fs
396
+ .lstatSync(path.join(saveTo, "media/layoutCopy.json"))
397
+ .isFile(),
398
+ ).to.be.true;
399
+ });
400
+
401
+ it("Should download images", () => {
402
+ const mediaFiles = glob.sync(
403
+ path.join(saveTo, "media/**/*.png"),
404
+ );
405
+ expect(mediaFiles).to.be.an("array").that.is.not.empty;
406
+ });
407
+
408
+ after(() => {
409
+ fs.removeSync(outputDir);
410
+ });
411
+ });
412
+ });
413
+
414
+ describe("rewrite", () => {
415
+ const saveTo = path.join(outputDir, "rewrite-test");
416
+
417
+ beforeEach(() => {
418
+ fs.removeSync(saveTo);
419
+ fs.mkdirpSync(saveTo);
420
+ });
421
+
422
+ it("Should rewrite remote URLs to local media paths", async () => {
423
+ // Create a mock JSON file with remote URLs
424
+ const mockData = JSON.stringify({
425
+ image: "https://example.com/wp-content/uploads/2024/photo.jpg",
426
+ text: "hello world",
427
+ nested: {
428
+ img: "https://example.com/wp-content/uploads/2024/nested.png",
429
+ },
430
+ });
431
+ fs.writeFileSync(path.join(saveTo, "test.json"), mockData);
432
+
433
+ await rewrite({
434
+ ext: "json",
435
+ type: "wp",
436
+ saveTo,
437
+ bundle: "",
438
+ find: "^https.*/wp-content/uploads",
439
+ });
440
+
441
+ const result = fs.readFileSync(path.join(saveTo, "test.json"), {
442
+ encoding: "utf8",
443
+ });
444
+
445
+ expect(result).to.contain("media/content");
446
+ expect(result).to.not.contain("wp-content/uploads");
447
+ });
448
+
449
+ it("Should unwrap contentful response during rewrite", async () => {
450
+ // Contentful responses are arrays, rewrite should extract first element
451
+ const mockData = JSON.stringify([
452
+ { title: "Entry 1", body: "Content" },
453
+ { title: "Entry 2", body: "More content" },
454
+ ]);
455
+ fs.writeFileSync(path.join(saveTo, "test.json"), mockData);
456
+
457
+ await rewrite({
458
+ ext: "json",
459
+ type: "contentful",
460
+ saveTo,
461
+ bundle: "",
462
+ find: "//images\\.ctfassets\\.net/(.*?)/(.*?)/(.*?)/",
463
+ });
464
+
465
+ const result = JSON.parse(
466
+ fs.readFileSync(path.join(saveTo, "test.json"), {
467
+ encoding: "utf8",
468
+ }),
469
+ );
470
+
471
+ // Should be unwrapped to first element, not an array
472
+ expect(result).to.have.property("title", "Entry 1");
473
+ });
474
+
475
+ after(() => {
476
+ fs.removeSync(saveTo);
477
+ });
478
+ });
479
+
480
+ describe("download", () => {
481
+ const saveTo = path.join(outputDir, "download-test");
482
+
483
+ beforeEach(() => {
484
+ fs.removeSync(saveTo);
485
+ fs.mkdirpSync(saveTo);
486
+ });
487
+
488
+ it("Should extract unique URLs matching find pattern from JSON", async () => {
489
+ // Create JSON that references assets
490
+ const mockData = JSON.stringify({
491
+ images: [
492
+ "https://example.com/wp-content/uploads/photo1.jpg",
493
+ "https://example.com/wp-content/uploads/photo2.jpg",
494
+ "https://example.com/wp-content/uploads/photo1.jpg",
495
+ ],
496
+ text: "not a URL",
497
+ });
498
+ fs.writeFileSync(path.join(saveTo, "test.json"), mockData);
499
+
500
+ // download will try to fetch these URLs — they'll fail but
501
+ // we verify the function doesn't throw
502
+ await download({
503
+ path: "https://example.com",
504
+ ext: "json",
505
+ saveTo,
506
+ bundle: "",
507
+ find: "^https.*/wp-content/uploads",
508
+ });
509
+ // If we get here without throwing, the URL extraction and
510
+ // deduplication logic works correctly
511
+ });
512
+
513
+ after(() => {
514
+ fs.removeSync(saveTo);
515
+ });
516
+ });
517
+ });
package/cli.js CHANGED
@@ -54,6 +54,7 @@ const args = hideBin(process.argv);
54
54
  "start",
55
55
  "setup",
56
56
  "test",
57
+ "scan",
57
58
  "production",
58
59
  "run",
59
60
  "connect",
@@ -1,9 +1,37 @@
1
1
  const execSync = require("child_process").execSync;
2
2
  const fs = require("fs-extra");
3
+ const path = require("path");
4
+ const { isEqual } = require("lodash");
3
5
  const utilities = require("./create/libs/utilities");
4
6
  const _ = require("../globals.js");
5
7
  const { getConfigurations, findRepository } = require("../hub.js");
6
8
  const glob = require("glob");
9
+ const { pullS3 } = require("./helpers/content-pull.js");
10
+ const { pullRequests } = require("./helpers/content-request.js");
11
+
12
+ const SNAPSHOT_PATH = ".tmp/content.json";
13
+
14
+ function detectConfigChange(currentContent) {
15
+ let prev = null;
16
+ try {
17
+ prev = JSON.parse(fs.readFileSync(SNAPSHOT_PATH, { encoding: "utf8" }));
18
+ } catch {
19
+ // No previous snapshot
20
+ }
21
+
22
+ if (prev && !isEqual(prev, currentContent)) {
23
+ return prev;
24
+ }
25
+
26
+ return null;
27
+ }
28
+
29
+ function saveConfigSnapshot(content) {
30
+ fs.mkdirpSync(path.dirname(SNAPSHOT_PATH));
31
+ fs.writeFileSync(SNAPSHOT_PATH, JSON.stringify(content), {
32
+ encoding: "utf8",
33
+ });
34
+ }
7
35
 
8
36
  module.exports = [
9
37
  "content",
@@ -34,9 +62,95 @@ module.exports = [
34
62
  "title",
35
63
  ),
36
64
  );
37
- } else {
65
+ } else if (_.pkg?.scripts?.content) {
66
+ // Fallback: project has its own content script, delegate to core
67
+ console.log(
68
+ utilities.colorize(
69
+ "Project has a content script in package.json, falling back to core...",
70
+ "warning",
71
+ ),
72
+ );
38
73
  _.command("core", `npm run content`);
74
+ } else {
75
+ const contentConfig = _.coreConfig?.attributes?.content;
76
+ const srcBase = _.coreConfig?.attributes?.src || "_Build";
77
+
78
+ if (!contentConfig || contentConfig.length === 0) {
79
+ console.log(
80
+ utilities.colorize(
81
+ "No content config found. Skipping...",
82
+ "warning",
83
+ ),
84
+ );
85
+ } else {
86
+ // Detect config changes and clean stale content
87
+ const prevContent = detectConfigChange(contentConfig);
88
+ if (prevContent) {
89
+ console.log(
90
+ utilities.colorize(
91
+ "Content config has changed. Removing existing content...",
92
+ "warning",
93
+ ),
94
+ );
95
+ fs.removeSync(`${srcBase}/content`);
96
+ prevContent
97
+ .filter((d) => d.saveTo)
98
+ .forEach((d) => fs.removeSync(d.saveTo));
99
+ }
100
+
101
+ // content:pull — S3-based content
102
+ for (const [i, d] of contentConfig.entries()) {
103
+ if (d.location) {
104
+ if (!d["aws-s3"]) {
105
+ console.log(
106
+ utilities.colorize(
107
+ `Skipping "${d.location}" — only aws-s3 protocol is supported. FTP/SSH/LFTP have been removed.`,
108
+ "warning",
109
+ ),
110
+ );
111
+ continue;
112
+ }
113
+
114
+ const saveTo =
115
+ d.saveTo ||
116
+ `${srcBase}/content/${d.key || `content-${i}`}`;
117
+
118
+ console.log(
119
+ utilities.colorize(
120
+ `Pulling content from: ${d.location}`,
121
+ "title",
122
+ ),
123
+ );
124
+
125
+ await pullS3(d, saveTo);
126
+
127
+ console.log(
128
+ utilities.colorize(
129
+ `Content pulled from: ${d.location}`,
130
+ "success",
131
+ ),
132
+ );
133
+ }
134
+ }
135
+
136
+ // content:request — HTTP API-based content
137
+ const requestItems = contentConfig.filter((d) => d.url);
138
+ if (requestItems.length > 0) {
139
+ console.log(
140
+ utilities.colorize(
141
+ "Fetching content from API endpoints...",
142
+ "title",
143
+ ),
144
+ );
145
+ await pullRequests(contentConfig, srcBase);
146
+ }
147
+
148
+ saveConfigSnapshot(contentConfig);
149
+ }
150
+ }
39
151
 
152
+ // Hub sync runs for both fallback and native paths
153
+ if (!argv.init) {
40
154
  try {
41
155
  if (process.env.HUB_URL) {
42
156
  console.log(`Syncing branch configurations from Hub...`);
@@ -122,7 +122,7 @@ module.exports = [
122
122
  name: "content-security-policy",
123
123
  message: "content-security-policy header value:",
124
124
  default:
125
- "default-src 'self' https: data: 'unsafe-inline';",
125
+ "default-src 'self' https: data: 'unsafe-inline' blob:;",
126
126
  validate: (input) => !!input.length,
127
127
  },
128
128
  {
@@ -45,7 +45,7 @@ function handler(event) {
45
45
  value: "max-age=31536000; includeSubDomains",
46
46
  },
47
47
  "content-security-policy": {
48
- value: "default-src 'self' https: data: 'unsafe-inline';",
48
+ value: "default-src 'self' https: data: 'unsafe-inline' blob:;",
49
49
  },
50
50
  "x-content-type-options": { value: "nosniff" },
51
51
  "x-frame-options": { value: "sameorigin" },
@@ -18,7 +18,7 @@ function handler(event) {
18
18
  value: "max-age=31536000; includeSubDomains",
19
19
  };
20
20
  headers["content-security-policy"] = {
21
- value: "default-src 'self' https: data: 'unsafe-inline';",
21
+ value: "default-src 'self' https: data: 'unsafe-inline' blob:;",
22
22
  };
23
23
  headers["x-content-type-options"] = { value: "nosniff" };
24
24
  headers["x-frame-options"] = { value: "sameorigin" };