Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy-demo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: npm run --workspace=@cortex-docs/demo-api deploy
- name: Deploy static.cortexdocs.dev as assets only
- name: Deploy static.cortexdocs.dev and its referrer registry
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Expand Down
14 changes: 12 additions & 2 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,17 @@ The demo deployment workflow performs these actions:
1. Build all packages.
2. Make sure that the configured Cloudflare zone is active.
3. Deploy the demo API Worker to `api.demo.cortexdocs.dev`.
4. Deploy the logo to `static.cortexdocs.dev` with Cloudflare Static Assets.
4. Deploy the logo to `static.cortexdocs.dev` with Cloudflare Static Assets and record each
distinct referrer hostname in D1.
5. Build the complete docs UI as static files.
6. Deploy the static files to `demo.cortexdocs.dev`.
7. Make sure that Cloudflare Static Assets serves each demo page.
8. Block unknown paths before they invoke the demo API Worker.
9. Limit valid demo API requests to 30 requests for each IP address during 10 seconds.

Requests to the two static hosts do not use the daily Workers request allowance. Only `Try now` requests invoke the demo API Worker.
Requests to the demo site do not use the daily Workers request allowance. Badge image requests
invoke a small Worker so it can store new referrer hostnames. `Try now` requests invoke the demo
API Worker.

The release workflow performs these actions:

Expand Down Expand Up @@ -137,3 +140,10 @@ Run this command to preview the product docs build:
```bash
npm run --workspace=@cortex-docs/docs-ui docs:preview
```

Run this command to list the hostnames that have loaded the Built with Cortex badge:

```bash
npx wrangler d1 execute cortex-badge-referrers --remote \
--command="SELECT hostname, first_seen_at FROM badge_referrer_hosts ORDER BY first_seen_at DESC"
```
87 changes: 87 additions & 0 deletions packages/demo-api/__tests__/static.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, expect, it, vi } from 'vitest';
import staticWorker, { getReferrerHostname } from '../src/static';

function createRuntime() {
const rows = new Set<string>();
const statements: string[] = [];
const pending: Promise<unknown>[] = [];
let boundHostname = '';

const database = {
prepare: vi.fn((statement: string) => {
statements.push(statement);
return {
bind: vi.fn((hostname: string) => {
boundHostname = hostname;
return {
run: vi.fn(async () => {
rows.add(boundHostname);
return { success: true };
}),
};
}),
};
}),
} as unknown as D1Database;
const assets = {
fetch: vi.fn(
async () => new Response('<svg />', { headers: { 'Content-Type': 'image/svg+xml' } }),
),
} as unknown as Fetcher;
const context = {
waitUntil: vi.fn((promise: Promise<unknown>) => pending.push(promise)),
} as unknown as ExecutionContext;

return { assets, context, database, pending, rows, statements };
}

describe('static asset Worker', () => {
it('normalizes HTTP referrer hostnames', () => {
expect(getReferrerHostname('https://WWW.Example.com./docs/page?query=1')).toBe(
'www.example.com',
);
expect(getReferrerHostname('mailto:hello@example.com')).toBeNull();
expect(getReferrerHostname('not a URL')).toBeNull();
expect(getReferrerHostname(null)).toBeNull();
});

it('stores each badge referrer only once and still serves the asset', async () => {
const runtime = createRuntime();
const request = new Request('https://static.cortexdocs.dev/images/built-with-cortex.svg', {
headers: { Referer: 'https://Example.com/docs' },
});
const env = { ASSETS: runtime.assets, BADGE_REFERRERS: runtime.database };

const first = await staticWorker.fetch(request, env, runtime.context);
const second = await staticWorker.fetch(request, env, runtime.context);
await Promise.all(runtime.pending);

expect(first.status).toBe(200);
expect(second.headers.get('Content-Type')).toBe('image/svg+xml');
expect(runtime.rows).toEqual(new Set(['example.com']));
expect(runtime.statements).toHaveLength(2);
expect(runtime.statements[0]).toContain('INSERT OR IGNORE');
});

it('does not store missing referrers or HEAD requests', async () => {
const runtime = createRuntime();
const env = { ASSETS: runtime.assets, BADGE_REFERRERS: runtime.database };

await staticWorker.fetch(
new Request('https://static.cortexdocs.dev/images/built-with-cortex.svg'),
env,
runtime.context,
);
await staticWorker.fetch(
new Request('https://static.cortexdocs.dev/images/built-with-cortex.svg', {
method: 'HEAD',
headers: { Referer: 'https://example.com/' },
}),
env,
runtime.context,
);

expect(runtime.pending).toHaveLength(0);
expect(runtime.rows.size).toBe(0);
});
});
8 changes: 7 additions & 1 deletion packages/demo-api/__tests__/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,26 @@ describe('demo API Worker', () => {
await expect(response.json()).resolves.toEqual({ status: 'ok', runtime: 'cloudflare-worker' });
});

it('keeps the Built with Cortex logo in the assets-only deployment', () => {
it('keeps the Built with Cortex logo in the static deployment', () => {
const body = readFileSync(
new URL('../static/images/built-with-cortex.svg', import.meta.url),
'utf8',
);
const headers = readFileSync(new URL('../static/_headers', import.meta.url), 'utf8');
const workerConfig = readFileSync(new URL('../wrangler.jsonc', import.meta.url), 'utf8');
const staticWorkerConfig = readFileSync(
new URL('../wrangler.static.jsonc', import.meta.url),
'utf8',
);

expect(body).toContain('<title id="title">Built with Cortex</title>');
expect(body).toContain('font-size="12" font-weight="600">Cortex</text>');
expect(body).not.toContain('<rect');
expect(headers).toContain('Cache-Control: public,max-age=86400');
expect(headers).toContain('Cross-Origin-Resource-Policy: cross-origin');
expect(workerConfig).toContain('"directory": "static"');
expect(staticWorkerConfig).toContain('"run_worker_first": ["/images/built-with-cortex.svg"]');
expect(staticWorkerConfig).toContain('"binding": "BADGE_REFERRERS"');
});

it('returns the Petstore collection with CORS headers', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS badge_referrer_hosts (
hostname TEXT PRIMARY KEY COLLATE NOCASE,
first_seen_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) WITHOUT ROWID;
4 changes: 2 additions & 2 deletions packages/demo-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
"build": "tsc",
"dev": "wrangler dev --port 4010",
"deploy": "wrangler deploy",
"deploy:static": "wrangler deploy --config wrangler.static.jsonc",
"deploy:static": "node scripts/deploy-static.mjs",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"clean": "rm -rf .wrangler dist coverage"
"clean": "rm -rf .wrangler dist coverage wrangler.static.production.jsonc"
},
"dependencies": {
"graphql": "^16.14.0"
Expand Down
152 changes: 152 additions & 0 deletions packages/demo-api/scripts/deploy-static.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/env node

import { spawn } from 'node:child_process';
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const databaseName = 'cortex-badge-referrers';
const databaseBinding = 'BADGE_REFERRERS';
const databaseIdPlaceholder = '00000000-0000-0000-0000-000000000000';
const verificationHostname = 'docs.cortexdocs.dev';
const badgeUrl = 'https://static.cortexdocs.dev/images/built-with-cortex.svg';
const scriptDir = dirname(fileURLToPath(import.meta.url));
const packageDir = resolve(scriptDir, '..');
const sourceConfigPath = join(packageDir, 'wrangler.static.jsonc');
const productionConfigPath = join(packageDir, 'wrangler.static.production.jsonc');
const require = createRequire(import.meta.url);
const wranglerPackagePath = require.resolve('wrangler/package.json');
const wranglerPackage = require(wranglerPackagePath);
const wranglerCli = resolve(dirname(wranglerPackagePath), wranglerPackage.bin.wrangler);

function run(args, { capture = false } = {}) {
return new Promise((resolveCommand, rejectCommand) => {
let stdout = '';
let stderr = '';
const child = spawn(process.execPath, [wranglerCli, ...args], {
cwd: packageDir,
env: { ...process.env, CI: process.env.CI || 'true' },
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
});

if (capture) {
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
stdout += chunk;
});
child.stderr.on('data', (chunk) => {
stderr += chunk;
});
}

child.once('error', rejectCommand);
child.once('exit', (code, signal) => {
if (signal) {
rejectCommand(new Error(`Wrangler stopped with signal ${signal}.`));
} else if (code !== 0) {
rejectCommand(
new Error(
`Wrangler ${args.join(' ')} failed with code ${code}.${stderr ? `\n${stderr}` : ''}`,
),
);
} else {
resolveCommand({ stdout, stderr });
}
});
});
}

async function listDatabases() {
const { stdout } = await run(['d1', 'list', '--json'], { capture: true });
const databases = JSON.parse(stdout);
if (!Array.isArray(databases)) throw new Error('Wrangler returned an invalid D1 database list.');
return databases;
}

async function findOrCreateDatabase() {
let database = (await listDatabases()).find((candidate) => candidate.name === databaseName);
if (database) return database;

console.log(`Creating the ${databaseName} production D1 database.`);
await run(['d1', 'create', databaseName, '--location', 'eeur']);

for (let attempt = 1; attempt <= 5; attempt += 1) {
database = (await listDatabases()).find((candidate) => candidate.name === databaseName);
if (database) return database;
await new Promise((resolveDelay) => setTimeout(resolveDelay, attempt * 1000));
}

throw new Error(`The ${databaseName} database was created but could not be listed.`);
}

async function writeProductionConfig(databaseId) {
const source = await readFile(sourceConfigPath, 'utf8');
if (!source.includes(databaseIdPlaceholder)) {
throw new Error('The static Wrangler configuration is missing its D1 database placeholder.');
}
await writeFile(productionConfigPath, source.replace(databaseIdPlaceholder, databaseId));
}

async function countVerificationHostname() {
const query = [
'SELECT COUNT(*) AS records',
'FROM badge_referrer_hosts',
`WHERE hostname = '${verificationHostname}'`,
].join(' ');
const { stdout } = await run(
[
'd1',
'execute',
databaseBinding,
'--remote',
'--json',
'--config',
productionConfigPath,
'--command',
query,
],
{ capture: true },
);
const results = JSON.parse(stdout);
return Number(results?.[0]?.results?.[0]?.records ?? 0);
}

async function verifyDeployment() {
for (let attempt = 1; attempt <= 10; attempt += 1) {
const first = await fetch(badgeUrl, {
headers: { Referer: `https://${verificationHostname}/` },
});
const second = await fetch(badgeUrl, {
headers: { Referer: `https://${verificationHostname}/` },
});
if (first.ok && second.ok && (await countVerificationHostname()) === 1) {
console.log('Static badge deployment and referrer deduplication verified.');
return;
}
await new Promise((resolveDelay) => setTimeout(resolveDelay, attempt * 1000));
}

throw new Error('The static badge deployment did not record exactly one referrer hostname.');
}

try {
await mkdir(dirname(productionConfigPath), { recursive: true });
const database = await findOrCreateDatabase();
if (!database.uuid) throw new Error(`The ${databaseName} database does not have an ID.`);
await writeProductionConfig(database.uuid);
await run([
'd1',
'migrations',
'apply',
databaseBinding,
'--remote',
'--config',
productionConfigPath,
]);
await run(['deploy', '--config', productionConfigPath]);
await verifyDeployment();
} finally {
await rm(productionConfigPath, { force: true });
}
39 changes: 39 additions & 0 deletions packages/demo-api/src/static.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const badgePath = '/images/built-with-cortex.svg';
const insertReferrer = 'INSERT OR IGNORE INTO badge_referrer_hosts (hostname) VALUES (?)';

interface StaticAssetsEnv {
ASSETS: Fetcher;
BADGE_REFERRERS: D1Database;
}

export function getReferrerHostname(value: string | null): string | null {
if (!value) return null;

try {
const url = new URL(value);
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
return url.hostname.toLowerCase().replace(/\.$/, '') || null;
} catch {
return null;
}
}

async function storeReferrer(database: D1Database, hostname: string): Promise<void> {
try {
await database.prepare(insertReferrer).bind(hostname).run();
} catch (error) {
console.error('Unable to store the Built with Cortex referrer hostname.', error);
}
}

export default {
fetch(request: Request, env: StaticAssetsEnv, context: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
if (request.method === 'GET' && url.pathname === badgePath) {
const hostname = getReferrerHostname(request.headers.get('Referer'));
if (hostname) context.waitUntil(storeReferrer(env.BADGE_REFERRERS, hostname));
}

return env.ASSETS.fetch(request);
},
} satisfies ExportedHandler<StaticAssetsEnv>;
11 changes: 11 additions & 0 deletions packages/demo-api/wrangler.static.jsonc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"$schema": "../../node_modules/wrangler/config-schema.json",
"name": "cortex-static-assets",
"main": "src/static.ts",
"compatibility_date": "2026-08-24",
"workers_dev": false,
"routes": [
Expand All @@ -9,8 +10,18 @@
"custom_domain": true,
},
],
"d1_databases": [
{
"binding": "BADGE_REFERRERS",
"database_name": "cortex-badge-referrers",
"database_id": "00000000-0000-0000-0000-000000000000",
"migrations_dir": "migrations",
},
],
"assets": {
"directory": "static",
"binding": "ASSETS",
"run_worker_first": ["/images/built-with-cortex.svg"],
"html_handling": "none",
"not_found_handling": "none",
},
Expand Down