From 334794a4352561e87b9a4470df2df72481441000 Mon Sep 17 00:00:00 2001 From: 9Line Admin Date: Sat, 29 Aug 2026 22:30:16 -0700 Subject: [PATCH 1/2] fix: allow owned 9line embed mode --- package.json | 3 ++- tests/embed-policy.test.mjs | 19 +++++++++++++++++++ vercel.json | 16 ++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/embed-policy.test.mjs diff --git a/package.json b/package.json index 47430ea..ceec9db 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "test:embed-policy": "node --test tests/embed-policy.test.mjs" }, "dependencies": { "@gcs/analytics": "file:./vendor/gcs-analytics", diff --git a/tests/embed-policy.test.mjs b/tests/embed-policy.test.mjs new file mode 100644 index 0000000..a1944ab --- /dev/null +++ b/tests/embed-policy.test.mjs @@ -0,0 +1,19 @@ +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' +import test from 'node:test' + +const config = JSON.parse(await readFile(new URL('../vercel.json', import.meta.url), 'utf8')) +const matchesEmbed = (condition) => + condition?.type === 'query' && condition.key === 'embed' && condition.value === '9line' +const headerValue = (rule, key) => rule.headers.find((header) => header.key === key)?.value + +test('only the owned 9Line embed mode relaxes Roadrunner framing', () => { + const standalone = config.headers.find((rule) => rule.missing?.some(matchesEmbed)) + const embed = config.headers.find((rule) => rule.has?.some(matchesEmbed)) + + assert.equal(headerValue(standalone, 'X-Frame-Options'), 'SAMEORIGIN') + assert.match(headerValue(standalone, 'Content-Security-Policy'), /frame-ancestors 'none'/) + assert.equal(headerValue(embed, 'X-Frame-Options'), undefined) + assert.match(headerValue(embed, 'Content-Security-Policy'), /frame-ancestors https:\/\/9line\.dev(?:;|$)/) + assert.doesNotMatch(headerValue(embed, 'Content-Security-Policy'), /frame-ancestors[^;]*\*/) +}) diff --git a/vercel.json b/vercel.json index 4b35681..23a8fd0 100644 --- a/vercel.json +++ b/vercel.json @@ -28,6 +28,9 @@ "headers": [ { "source": "/(.*)", + "missing": [ + { "type": "query", "key": "embed", "value": "9line" } + ], "headers": [ { "key": "X-Content-Type-Options", "value": "nosniff" }, { "key": "X-Frame-Options", "value": "SAMEORIGIN" }, @@ -37,6 +40,19 @@ { "key": "Content-Security-Policy", "value": "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; media-src 'self'; connect-src 'self' https://api.9line.dev https://www.google-analytics.com https://region1.google-analytics.com https://analytics.google.com https://stats.g.doubleclick.net; frame-ancestors 'none'" } ] }, + { + "source": "/(.*)", + "has": [ + { "type": "query", "key": "embed", "value": "9line" } + ], + "headers": [ + { "key": "X-Content-Type-Options", "value": "nosniff" }, + { "key": "X-XSS-Protection", "value": "1; mode=block" }, + { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }, + { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }, + { "key": "Content-Security-Policy", "value": "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; media-src 'self'; connect-src 'self' https://api.9line.dev https://www.google-analytics.com https://region1.google-analytics.com https://analytics.google.com https://stats.g.doubleclick.net; frame-ancestors https://9line.dev" } + ] + }, { "source": "/assets/(.*)", "headers": [ From b91fca81c6f6913017a75d7972dde5dd11b30add Mon Sep 17 00:00:00 2001 From: 9Line Admin Date: Sat, 29 Aug 2026 23:06:05 -0700 Subject: [PATCH 2/2] fix: anchor owned embed query --- tests/embed-policy.test.mjs | 2 +- vercel.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/embed-policy.test.mjs b/tests/embed-policy.test.mjs index a1944ab..406800b 100644 --- a/tests/embed-policy.test.mjs +++ b/tests/embed-policy.test.mjs @@ -4,7 +4,7 @@ import test from 'node:test' const config = JSON.parse(await readFile(new URL('../vercel.json', import.meta.url), 'utf8')) const matchesEmbed = (condition) => - condition?.type === 'query' && condition.key === 'embed' && condition.value === '9line' + condition?.type === 'query' && condition.key === 'embed' && condition.value === '^9line$' const headerValue = (rule, key) => rule.headers.find((header) => header.key === key)?.value test('only the owned 9Line embed mode relaxes Roadrunner framing', () => { diff --git a/vercel.json b/vercel.json index 23a8fd0..780a413 100644 --- a/vercel.json +++ b/vercel.json @@ -29,7 +29,7 @@ { "source": "/(.*)", "missing": [ - { "type": "query", "key": "embed", "value": "9line" } + { "type": "query", "key": "embed", "value": "^9line$" } ], "headers": [ { "key": "X-Content-Type-Options", "value": "nosniff" }, @@ -43,7 +43,7 @@ { "source": "/(.*)", "has": [ - { "type": "query", "key": "embed", "value": "9line" } + { "type": "query", "key": "embed", "value": "^9line$" } ], "headers": [ { "key": "X-Content-Type-Options", "value": "nosniff" },