@olonjs/cli 3.0.140 → 3.0.141
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.
|
@@ -1649,6 +1649,149 @@ cat << 'END_OF_FILE_CONTENT' > "index.html"
|
|
|
1649
1649
|
</html>
|
|
1650
1650
|
|
|
1651
1651
|
|
|
1652
|
+
END_OF_FILE_CONTENT
|
|
1653
|
+
echo "Creating middleware.ts..."
|
|
1654
|
+
cat << 'END_OF_FILE_CONTENT' > "middleware.ts"
|
|
1655
|
+
export const config = {
|
|
1656
|
+
matcher: ['/admin', '/admin/:path*'],
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
const COOKIE_NAME = '__olonjs_admin_session';
|
|
1660
|
+
const COOKIE_MAX_AGE = 3600;
|
|
1661
|
+
const JWT_MAX_AGE_SECONDS = 300;
|
|
1662
|
+
|
|
1663
|
+
function base64urlToBase64(str: string): string {
|
|
1664
|
+
return str.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (str.length % 4)) % 4);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function base64ToArrayBuffer(b64: string): ArrayBuffer {
|
|
1668
|
+
const binary = atob(b64);
|
|
1669
|
+
const bytes = new Uint8Array(binary.length);
|
|
1670
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1671
|
+
return bytes.buffer;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function pemToArrayBuffer(pem: string): ArrayBuffer {
|
|
1675
|
+
// Handle both actual newlines and literal \n sequences (common in Vercel env vars)
|
|
1676
|
+
const b64 = pem
|
|
1677
|
+
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
|
|
1678
|
+
.replace(/-----END PUBLIC KEY-----/g, '')
|
|
1679
|
+
.replace(/\\n/g, '')
|
|
1680
|
+
.replace(/\s+/g, '');
|
|
1681
|
+
return base64ToArrayBuffer(b64);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
async function importPublicKey(pem: string): Promise<CryptoKey> {
|
|
1685
|
+
return crypto.subtle.importKey(
|
|
1686
|
+
'spki',
|
|
1687
|
+
pemToArrayBuffer(pem),
|
|
1688
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
1689
|
+
false,
|
|
1690
|
+
['verify'],
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
interface JwtPayload {
|
|
1695
|
+
sub?: string;
|
|
1696
|
+
iat?: number;
|
|
1697
|
+
exp?: number;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
async function verifyAdminJwt(
|
|
1701
|
+
token: string,
|
|
1702
|
+
publicKey: CryptoKey,
|
|
1703
|
+
options: { checkExp: boolean } = { checkExp: true },
|
|
1704
|
+
): Promise<boolean> {
|
|
1705
|
+
try {
|
|
1706
|
+
const parts = token.split('.');
|
|
1707
|
+
if (parts.length !== 3) return false;
|
|
1708
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
1709
|
+
|
|
1710
|
+
const payload = JSON.parse(atob(base64urlToBase64(payloadB64))) as JwtPayload;
|
|
1711
|
+
if (payload.sub !== 'admin-access') return false;
|
|
1712
|
+
|
|
1713
|
+
if (options.checkExp) {
|
|
1714
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1715
|
+
if (typeof payload.exp !== 'number' || payload.exp < now) return false;
|
|
1716
|
+
if (typeof payload.iat !== 'number' || now - payload.iat > JWT_MAX_AGE_SECONDS) return false;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
const message = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
|
|
1720
|
+
const signature = base64ToArrayBuffer(base64urlToBase64(signatureB64));
|
|
1721
|
+
return crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, publicKey, signature, message);
|
|
1722
|
+
} catch {
|
|
1723
|
+
return false;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function parseCookies(header: string | null): Record<string, string> {
|
|
1728
|
+
if (!header) return {};
|
|
1729
|
+
return Object.fromEntries(
|
|
1730
|
+
header.split(';').map((c) => {
|
|
1731
|
+
const [k, ...v] = c.trim().split('=');
|
|
1732
|
+
return [k.trim(), decodeURIComponent(v.join('='))];
|
|
1733
|
+
}),
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function deny(hint: string): Response {
|
|
1738
|
+
console.error(`[admin-middleware] 401 reason: ${hint}`);
|
|
1739
|
+
return new Response('Unauthorized', { status: 401 });
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
export default async function middleware(request: Request): Promise<Response | undefined> {
|
|
1743
|
+
if (!process.env.VERCEL_ENV) return undefined;
|
|
1744
|
+
|
|
1745
|
+
const publicKeyPem = process.env.ADMIN_PUBLIC_KEY;
|
|
1746
|
+
if (!publicKeyPem) return deny('missing_public_key');
|
|
1747
|
+
|
|
1748
|
+
let publicKey: CryptoKey;
|
|
1749
|
+
try {
|
|
1750
|
+
publicKey = await importPublicKey(publicKeyPem);
|
|
1751
|
+
} catch (e) {
|
|
1752
|
+
return deny(`key_import_failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
const url = new URL(request.url);
|
|
1756
|
+
const cookies = parseCookies(request.headers.get('Cookie'));
|
|
1757
|
+
|
|
1758
|
+
// 1. Session cookie — signature check only; session lifetime governed by cookie Max-Age.
|
|
1759
|
+
// The JWT stored in the cookie was valid at issue time; exp is intentionally skipped
|
|
1760
|
+
// to allow 1h sessions without requiring the private key in the Edge Runtime.
|
|
1761
|
+
// parseCookies already decodes values — no extra decodeURIComponent needed here
|
|
1762
|
+
const sessionToken = cookies[COOKIE_NAME];
|
|
1763
|
+
if (sessionToken) {
|
|
1764
|
+
const cookieValid = await verifyAdminJwt(sessionToken, publicKey, { checkExp: false });
|
|
1765
|
+
console.error(`[admin-middleware] cookie check: present=${!!sessionToken} valid=${cookieValid}`);
|
|
1766
|
+
if (cookieValid) return undefined;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// 2. Bearer token in Authorization header — full validation including exp
|
|
1770
|
+
const authHeader = request.headers.get('Authorization') ?? '';
|
|
1771
|
+
const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
|
1772
|
+
if (bearerToken && (await verifyAdminJwt(bearerToken, publicKey))) {
|
|
1773
|
+
return undefined;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// 3. Token as query param — full validation; set session cookie and redirect to clean URL
|
|
1777
|
+
const queryToken = url.searchParams.get('token');
|
|
1778
|
+
if (queryToken && (await verifyAdminJwt(queryToken, publicKey))) {
|
|
1779
|
+
const cleanUrl = new URL(url.toString());
|
|
1780
|
+
cleanUrl.searchParams.delete('token');
|
|
1781
|
+
return new Response(null, {
|
|
1782
|
+
status: 302,
|
|
1783
|
+
headers: {
|
|
1784
|
+
Location: cleanUrl.toString(),
|
|
1785
|
+
// SameSite=Lax required: navigation originates from a cross-site platform (app.olon.it)
|
|
1786
|
+
// SameSite=Strict would block the cookie in the redirect follow-up request
|
|
1787
|
+
'Set-Cookie': `${COOKIE_NAME}=${encodeURIComponent(queryToken)}; Path=/admin; HttpOnly; Secure; SameSite=Lax; Max-Age=${COOKIE_MAX_AGE}`,
|
|
1788
|
+
},
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
return deny('token_invalid');
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1652
1795
|
END_OF_FILE_CONTENT
|
|
1653
1796
|
echo "Creating package.json..."
|
|
1654
1797
|
cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
@@ -1661,9 +1804,9 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1661
1804
|
"dev": "vite",
|
|
1662
1805
|
"dev:clean": "vite --force",
|
|
1663
1806
|
"verify:webmcp": "node scripts/webmcp-feature-check.mjs",
|
|
1664
|
-
"prebuild": "node scripts/sync-pages-to-public.mjs && node scripts/generate-llms-txt.mjs && node scripts/bake.mjs",
|
|
1807
|
+
"prebuild": "node scripts/sync-pages-to-public.mjs && node scripts/generate-llms-txt.mjs && node scripts/bake.mjs && node scripts/sitemap.mjs && node scripts/robots.mjs",
|
|
1665
1808
|
"build": "tsc && vite build",
|
|
1666
|
-
"dist": "bash ./src2Code.sh --template alpha src .cursor vercel.json components.json index.html tsconfig.json package.json tsconfig.node.json vite.config.ts scripts ",
|
|
1809
|
+
"dist": "bash ./src2Code.sh --template alpha src .cursor vercel.json components.json index.html tsconfig.json package.json tsconfig.node.json vite.config.ts scripts middleware.ts",
|
|
1667
1810
|
"preview": "vite preview",
|
|
1668
1811
|
"bake:email": "tsx scripts/bake-email.tsx",
|
|
1669
1812
|
"bakemail": "npm run bake:email --",
|
|
@@ -1674,7 +1817,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1674
1817
|
"@tiptap/extension-link": "^2.11.5",
|
|
1675
1818
|
"@tiptap/react": "^2.11.5",
|
|
1676
1819
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1677
|
-
"@olonjs/core": "^1.1.
|
|
1820
|
+
"@olonjs/core": "^1.1.5",
|
|
1678
1821
|
"class-variance-authority": "^0.7.1",
|
|
1679
1822
|
"clsx": "^2.1.1",
|
|
1680
1823
|
"lucide-react": "^0.474.0",
|
|
@@ -2239,6 +2382,149 @@ const outPath = path.join(rootDir, 'public', 'llms.txt');
|
|
|
2239
2382
|
fs.writeFileSync(outPath, llmsTxt, 'utf-8');
|
|
2240
2383
|
console.log('[generate-llms-txt] Written -> public/llms.txt');
|
|
2241
2384
|
|
|
2385
|
+
END_OF_FILE_CONTENT
|
|
2386
|
+
echo "Creating scripts/robots.mjs..."
|
|
2387
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/robots.mjs"
|
|
2388
|
+
import fs from 'fs';
|
|
2389
|
+
import path from 'path';
|
|
2390
|
+
import { fileURLToPath } from 'url';
|
|
2391
|
+
|
|
2392
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
2393
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
2394
|
+
|
|
2395
|
+
const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL
|
|
2396
|
+
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
|
2397
|
+
: 'http://localhost:5173';
|
|
2398
|
+
|
|
2399
|
+
const robotsTxt = `User-agent: *
|
|
2400
|
+
Allow: /
|
|
2401
|
+
Disallow: /api/
|
|
2402
|
+
|
|
2403
|
+
User-agent: GPTBot
|
|
2404
|
+
User-agent: ChatGPT-User
|
|
2405
|
+
User-agent: ClaudeBot
|
|
2406
|
+
User-agent: Claude-Web
|
|
2407
|
+
User-agent: PerplexityBot
|
|
2408
|
+
User-agent: OAI-SearchBot
|
|
2409
|
+
Allow: /
|
|
2410
|
+
Allow: /*.json
|
|
2411
|
+
Allow: /schemas/
|
|
2412
|
+
Allow: /llms.txt
|
|
2413
|
+
Allow: /mcp-manifest.json
|
|
2414
|
+
Disallow: /api/
|
|
2415
|
+
|
|
2416
|
+
Sitemap: ${baseUrl}/sitemap.xml
|
|
2417
|
+
`;
|
|
2418
|
+
|
|
2419
|
+
const outPath = path.join(rootDir, 'public', 'robots.txt');
|
|
2420
|
+
fs.writeFileSync(outPath, robotsTxt, 'utf-8');
|
|
2421
|
+
console.log('[robots] Written -> public/robots.txt');
|
|
2422
|
+
|
|
2423
|
+
END_OF_FILE_CONTENT
|
|
2424
|
+
echo "Creating scripts/sitemap.mjs..."
|
|
2425
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/sitemap.mjs"
|
|
2426
|
+
import fs from 'fs';
|
|
2427
|
+
import path from 'path';
|
|
2428
|
+
import { fileURLToPath } from 'url';
|
|
2429
|
+
|
|
2430
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
2431
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
2432
|
+
|
|
2433
|
+
const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL
|
|
2434
|
+
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
|
2435
|
+
: 'http://localhost:5173';
|
|
2436
|
+
|
|
2437
|
+
function listJsonFilesRecursive(dir) {
|
|
2438
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
2439
|
+
const files = [];
|
|
2440
|
+
for (const item of items) {
|
|
2441
|
+
const fullPath = path.join(dir, item.name);
|
|
2442
|
+
if (item.isDirectory()) {
|
|
2443
|
+
files.push(...listJsonFilesRecursive(fullPath));
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
if (item.isFile() && item.name.toLowerCase().endsWith('.json')) files.push(fullPath);
|
|
2447
|
+
}
|
|
2448
|
+
return files;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function toW3CDate(date) {
|
|
2452
|
+
return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
function urlEntry({ loc, lastmod, changefreq, priority, comment }) {
|
|
2456
|
+
const lines = [];
|
|
2457
|
+
if (comment) lines.push(` <!-- ${comment} -->`);
|
|
2458
|
+
lines.push(` <url>`);
|
|
2459
|
+
lines.push(` <loc>${loc}</loc>`);
|
|
2460
|
+
lines.push(` <lastmod>${lastmod}</lastmod>`);
|
|
2461
|
+
lines.push(` <changefreq>${changefreq}</changefreq>`);
|
|
2462
|
+
lines.push(` <priority>${priority}</priority>`);
|
|
2463
|
+
lines.push(` </url>`);
|
|
2464
|
+
return lines.join('\n');
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
function sectionComment(label) {
|
|
2468
|
+
const bar = '='.repeat(42);
|
|
2469
|
+
return [
|
|
2470
|
+
` <!-- ${bar} -->`,
|
|
2471
|
+
` <!-- ${label.padEnd(42)} -->`,
|
|
2472
|
+
` <!-- ${bar} -->`,
|
|
2473
|
+
].join('\n');
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
const pagesDir = path.join(rootDir, 'src', 'data', 'pages');
|
|
2477
|
+
const buildTime = toW3CDate(new Date());
|
|
2478
|
+
|
|
2479
|
+
const pageFiles = listJsonFilesRecursive(pagesDir);
|
|
2480
|
+
const pages = pageFiles.map((fullPath) => {
|
|
2481
|
+
const slug = path
|
|
2482
|
+
.relative(pagesDir, fullPath)
|
|
2483
|
+
.replace(/\\/g, '/')
|
|
2484
|
+
.replace(/\.json$/i, '');
|
|
2485
|
+
const lastmod = toW3CDate(fs.statSync(fullPath).mtime);
|
|
2486
|
+
return { slug, lastmod };
|
|
2487
|
+
});
|
|
2488
|
+
|
|
2489
|
+
const entries = [];
|
|
2490
|
+
|
|
2491
|
+
entries.push(sectionComment('GLOBAL AGENT DISCOVERY NODES'));
|
|
2492
|
+
entries.push(
|
|
2493
|
+
urlEntry({ loc: `${baseUrl}/llms.txt`, lastmod: buildTime, changefreq: 'weekly', priority: '1.0' }),
|
|
2494
|
+
);
|
|
2495
|
+
entries.push(
|
|
2496
|
+
urlEntry({ loc: `${baseUrl}/mcp-manifest.json`, lastmod: buildTime, changefreq: 'weekly', priority: '1.0' }),
|
|
2497
|
+
);
|
|
2498
|
+
|
|
2499
|
+
for (const { slug, lastmod } of pages) {
|
|
2500
|
+
const humanPath = slug === 'home' ? '/' : `/${slug}`;
|
|
2501
|
+
const label = `PAGE: ${slug.toUpperCase()}`;
|
|
2502
|
+
|
|
2503
|
+
entries.push(sectionComment(label));
|
|
2504
|
+
entries.push(
|
|
2505
|
+
urlEntry({ loc: `${baseUrl}${humanPath}`, lastmod, changefreq: 'daily', priority: '0.9', comment: 'Human UI' }),
|
|
2506
|
+
);
|
|
2507
|
+
entries.push(
|
|
2508
|
+
urlEntry({ loc: `${baseUrl}/${slug}.json`, lastmod, changefreq: 'daily', priority: '0.9', comment: 'Machine Payload' }),
|
|
2509
|
+
);
|
|
2510
|
+
entries.push(
|
|
2511
|
+
urlEntry({ loc: `${baseUrl}/schemas/${slug}.schema.json`, lastmod: buildTime, changefreq: 'weekly', priority: '0.8', comment: 'Machine Contract (Schema)' }),
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
const xml = [
|
|
2516
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
2517
|
+
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`,
|
|
2518
|
+
``,
|
|
2519
|
+
entries.join('\n'),
|
|
2520
|
+
``,
|
|
2521
|
+
`</urlset>`,
|
|
2522
|
+
].join('\n');
|
|
2523
|
+
|
|
2524
|
+
const outPath = path.join(rootDir, 'public', 'sitemap.xml');
|
|
2525
|
+
fs.writeFileSync(outPath, xml, 'utf-8');
|
|
2526
|
+
console.log('[sitemap] Written -> public/sitemap.xml');
|
|
2527
|
+
|
|
2242
2528
|
END_OF_FILE_CONTENT
|
|
2243
2529
|
echo "Creating scripts/sync-pages-to-public.mjs..."
|
|
2244
2530
|
cat << 'END_OF_FILE_CONTENT' > "scripts/sync-pages-to-public.mjs"
|
|
@@ -1649,6 +1649,149 @@ cat << 'END_OF_FILE_CONTENT' > "index.html"
|
|
|
1649
1649
|
</html>
|
|
1650
1650
|
|
|
1651
1651
|
|
|
1652
|
+
END_OF_FILE_CONTENT
|
|
1653
|
+
echo "Creating middleware.ts..."
|
|
1654
|
+
cat << 'END_OF_FILE_CONTENT' > "middleware.ts"
|
|
1655
|
+
export const config = {
|
|
1656
|
+
matcher: ['/admin', '/admin/:path*'],
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
const COOKIE_NAME = '__olonjs_admin_session';
|
|
1660
|
+
const COOKIE_MAX_AGE = 3600;
|
|
1661
|
+
const JWT_MAX_AGE_SECONDS = 300;
|
|
1662
|
+
|
|
1663
|
+
function base64urlToBase64(str: string): string {
|
|
1664
|
+
return str.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (str.length % 4)) % 4);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function base64ToArrayBuffer(b64: string): ArrayBuffer {
|
|
1668
|
+
const binary = atob(b64);
|
|
1669
|
+
const bytes = new Uint8Array(binary.length);
|
|
1670
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1671
|
+
return bytes.buffer;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function pemToArrayBuffer(pem: string): ArrayBuffer {
|
|
1675
|
+
// Handle both actual newlines and literal \n sequences (common in Vercel env vars)
|
|
1676
|
+
const b64 = pem
|
|
1677
|
+
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
|
|
1678
|
+
.replace(/-----END PUBLIC KEY-----/g, '')
|
|
1679
|
+
.replace(/\\n/g, '')
|
|
1680
|
+
.replace(/\s+/g, '');
|
|
1681
|
+
return base64ToArrayBuffer(b64);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
async function importPublicKey(pem: string): Promise<CryptoKey> {
|
|
1685
|
+
return crypto.subtle.importKey(
|
|
1686
|
+
'spki',
|
|
1687
|
+
pemToArrayBuffer(pem),
|
|
1688
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
1689
|
+
false,
|
|
1690
|
+
['verify'],
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
interface JwtPayload {
|
|
1695
|
+
sub?: string;
|
|
1696
|
+
iat?: number;
|
|
1697
|
+
exp?: number;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
async function verifyAdminJwt(
|
|
1701
|
+
token: string,
|
|
1702
|
+
publicKey: CryptoKey,
|
|
1703
|
+
options: { checkExp: boolean } = { checkExp: true },
|
|
1704
|
+
): Promise<boolean> {
|
|
1705
|
+
try {
|
|
1706
|
+
const parts = token.split('.');
|
|
1707
|
+
if (parts.length !== 3) return false;
|
|
1708
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
1709
|
+
|
|
1710
|
+
const payload = JSON.parse(atob(base64urlToBase64(payloadB64))) as JwtPayload;
|
|
1711
|
+
if (payload.sub !== 'admin-access') return false;
|
|
1712
|
+
|
|
1713
|
+
if (options.checkExp) {
|
|
1714
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1715
|
+
if (typeof payload.exp !== 'number' || payload.exp < now) return false;
|
|
1716
|
+
if (typeof payload.iat !== 'number' || now - payload.iat > JWT_MAX_AGE_SECONDS) return false;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
const message = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
|
|
1720
|
+
const signature = base64ToArrayBuffer(base64urlToBase64(signatureB64));
|
|
1721
|
+
return crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, publicKey, signature, message);
|
|
1722
|
+
} catch {
|
|
1723
|
+
return false;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function parseCookies(header: string | null): Record<string, string> {
|
|
1728
|
+
if (!header) return {};
|
|
1729
|
+
return Object.fromEntries(
|
|
1730
|
+
header.split(';').map((c) => {
|
|
1731
|
+
const [k, ...v] = c.trim().split('=');
|
|
1732
|
+
return [k.trim(), decodeURIComponent(v.join('='))];
|
|
1733
|
+
}),
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function deny(hint: string): Response {
|
|
1738
|
+
console.error(`[admin-middleware] 401 reason: ${hint}`);
|
|
1739
|
+
return new Response('Unauthorized', { status: 401 });
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
export default async function middleware(request: Request): Promise<Response | undefined> {
|
|
1743
|
+
if (!process.env.VERCEL_ENV) return undefined;
|
|
1744
|
+
|
|
1745
|
+
const publicKeyPem = process.env.ADMIN_PUBLIC_KEY;
|
|
1746
|
+
if (!publicKeyPem) return deny('missing_public_key');
|
|
1747
|
+
|
|
1748
|
+
let publicKey: CryptoKey;
|
|
1749
|
+
try {
|
|
1750
|
+
publicKey = await importPublicKey(publicKeyPem);
|
|
1751
|
+
} catch (e) {
|
|
1752
|
+
return deny(`key_import_failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
const url = new URL(request.url);
|
|
1756
|
+
const cookies = parseCookies(request.headers.get('Cookie'));
|
|
1757
|
+
|
|
1758
|
+
// 1. Session cookie — signature check only; session lifetime governed by cookie Max-Age.
|
|
1759
|
+
// The JWT stored in the cookie was valid at issue time; exp is intentionally skipped
|
|
1760
|
+
// to allow 1h sessions without requiring the private key in the Edge Runtime.
|
|
1761
|
+
// parseCookies already decodes values — no extra decodeURIComponent needed here
|
|
1762
|
+
const sessionToken = cookies[COOKIE_NAME];
|
|
1763
|
+
if (sessionToken) {
|
|
1764
|
+
const cookieValid = await verifyAdminJwt(sessionToken, publicKey, { checkExp: false });
|
|
1765
|
+
console.error(`[admin-middleware] cookie check: present=${!!sessionToken} valid=${cookieValid}`);
|
|
1766
|
+
if (cookieValid) return undefined;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// 2. Bearer token in Authorization header — full validation including exp
|
|
1770
|
+
const authHeader = request.headers.get('Authorization') ?? '';
|
|
1771
|
+
const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
|
1772
|
+
if (bearerToken && (await verifyAdminJwt(bearerToken, publicKey))) {
|
|
1773
|
+
return undefined;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// 3. Token as query param — full validation; set session cookie and redirect to clean URL
|
|
1777
|
+
const queryToken = url.searchParams.get('token');
|
|
1778
|
+
if (queryToken && (await verifyAdminJwt(queryToken, publicKey))) {
|
|
1779
|
+
const cleanUrl = new URL(url.toString());
|
|
1780
|
+
cleanUrl.searchParams.delete('token');
|
|
1781
|
+
return new Response(null, {
|
|
1782
|
+
status: 302,
|
|
1783
|
+
headers: {
|
|
1784
|
+
Location: cleanUrl.toString(),
|
|
1785
|
+
// SameSite=Lax required: navigation originates from a cross-site platform (app.olon.it)
|
|
1786
|
+
// SameSite=Strict would block the cookie in the redirect follow-up request
|
|
1787
|
+
'Set-Cookie': `${COOKIE_NAME}=${encodeURIComponent(queryToken)}; Path=/admin; HttpOnly; Secure; SameSite=Lax; Max-Age=${COOKIE_MAX_AGE}`,
|
|
1788
|
+
},
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
return deny('token_invalid');
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1652
1795
|
END_OF_FILE_CONTENT
|
|
1653
1796
|
echo "Creating package.json..."
|
|
1654
1797
|
cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
@@ -1661,9 +1804,9 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1661
1804
|
"dev": "vite",
|
|
1662
1805
|
"dev:clean": "vite --force",
|
|
1663
1806
|
"verify:webmcp": "node scripts/webmcp-feature-check.mjs",
|
|
1664
|
-
"prebuild": "node scripts/sync-pages-to-public.mjs && node scripts/generate-llms-txt.mjs && node scripts/bake.mjs",
|
|
1807
|
+
"prebuild": "node scripts/sync-pages-to-public.mjs && node scripts/generate-llms-txt.mjs && node scripts/bake.mjs && node scripts/sitemap.mjs && node scripts/robots.mjs",
|
|
1665
1808
|
"build": "tsc && vite build",
|
|
1666
|
-
"dist": "bash ./src2Code.sh --template alpha src .cursor vercel.json components.json index.html tsconfig.json package.json tsconfig.node.json vite.config.ts scripts ",
|
|
1809
|
+
"dist": "bash ./src2Code.sh --template alpha src .cursor vercel.json components.json index.html tsconfig.json package.json tsconfig.node.json vite.config.ts scripts middleware.ts",
|
|
1667
1810
|
"preview": "vite preview",
|
|
1668
1811
|
"bake:email": "tsx scripts/bake-email.tsx",
|
|
1669
1812
|
"bakemail": "npm run bake:email --",
|
|
@@ -1674,7 +1817,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1674
1817
|
"@tiptap/extension-link": "^2.11.5",
|
|
1675
1818
|
"@tiptap/react": "^2.11.5",
|
|
1676
1819
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1677
|
-
"@olonjs/core": "^1.1.
|
|
1820
|
+
"@olonjs/core": "^1.1.5",
|
|
1678
1821
|
"class-variance-authority": "^0.7.1",
|
|
1679
1822
|
"clsx": "^2.1.1",
|
|
1680
1823
|
"lucide-react": "^0.474.0",
|
|
@@ -2239,6 +2382,149 @@ const outPath = path.join(rootDir, 'public', 'llms.txt');
|
|
|
2239
2382
|
fs.writeFileSync(outPath, llmsTxt, 'utf-8');
|
|
2240
2383
|
console.log('[generate-llms-txt] Written -> public/llms.txt');
|
|
2241
2384
|
|
|
2385
|
+
END_OF_FILE_CONTENT
|
|
2386
|
+
echo "Creating scripts/robots.mjs..."
|
|
2387
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/robots.mjs"
|
|
2388
|
+
import fs from 'fs';
|
|
2389
|
+
import path from 'path';
|
|
2390
|
+
import { fileURLToPath } from 'url';
|
|
2391
|
+
|
|
2392
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
2393
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
2394
|
+
|
|
2395
|
+
const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL
|
|
2396
|
+
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
|
2397
|
+
: 'http://localhost:5173';
|
|
2398
|
+
|
|
2399
|
+
const robotsTxt = `User-agent: *
|
|
2400
|
+
Allow: /
|
|
2401
|
+
Disallow: /api/
|
|
2402
|
+
|
|
2403
|
+
User-agent: GPTBot
|
|
2404
|
+
User-agent: ChatGPT-User
|
|
2405
|
+
User-agent: ClaudeBot
|
|
2406
|
+
User-agent: Claude-Web
|
|
2407
|
+
User-agent: PerplexityBot
|
|
2408
|
+
User-agent: OAI-SearchBot
|
|
2409
|
+
Allow: /
|
|
2410
|
+
Allow: /*.json
|
|
2411
|
+
Allow: /schemas/
|
|
2412
|
+
Allow: /llms.txt
|
|
2413
|
+
Allow: /mcp-manifest.json
|
|
2414
|
+
Disallow: /api/
|
|
2415
|
+
|
|
2416
|
+
Sitemap: ${baseUrl}/sitemap.xml
|
|
2417
|
+
`;
|
|
2418
|
+
|
|
2419
|
+
const outPath = path.join(rootDir, 'public', 'robots.txt');
|
|
2420
|
+
fs.writeFileSync(outPath, robotsTxt, 'utf-8');
|
|
2421
|
+
console.log('[robots] Written -> public/robots.txt');
|
|
2422
|
+
|
|
2423
|
+
END_OF_FILE_CONTENT
|
|
2424
|
+
echo "Creating scripts/sitemap.mjs..."
|
|
2425
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/sitemap.mjs"
|
|
2426
|
+
import fs from 'fs';
|
|
2427
|
+
import path from 'path';
|
|
2428
|
+
import { fileURLToPath } from 'url';
|
|
2429
|
+
|
|
2430
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
2431
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
2432
|
+
|
|
2433
|
+
const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL
|
|
2434
|
+
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
|
2435
|
+
: 'http://localhost:5173';
|
|
2436
|
+
|
|
2437
|
+
function listJsonFilesRecursive(dir) {
|
|
2438
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
2439
|
+
const files = [];
|
|
2440
|
+
for (const item of items) {
|
|
2441
|
+
const fullPath = path.join(dir, item.name);
|
|
2442
|
+
if (item.isDirectory()) {
|
|
2443
|
+
files.push(...listJsonFilesRecursive(fullPath));
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
if (item.isFile() && item.name.toLowerCase().endsWith('.json')) files.push(fullPath);
|
|
2447
|
+
}
|
|
2448
|
+
return files;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function toW3CDate(date) {
|
|
2452
|
+
return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
function urlEntry({ loc, lastmod, changefreq, priority, comment }) {
|
|
2456
|
+
const lines = [];
|
|
2457
|
+
if (comment) lines.push(` <!-- ${comment} -->`);
|
|
2458
|
+
lines.push(` <url>`);
|
|
2459
|
+
lines.push(` <loc>${loc}</loc>`);
|
|
2460
|
+
lines.push(` <lastmod>${lastmod}</lastmod>`);
|
|
2461
|
+
lines.push(` <changefreq>${changefreq}</changefreq>`);
|
|
2462
|
+
lines.push(` <priority>${priority}</priority>`);
|
|
2463
|
+
lines.push(` </url>`);
|
|
2464
|
+
return lines.join('\n');
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
function sectionComment(label) {
|
|
2468
|
+
const bar = '='.repeat(42);
|
|
2469
|
+
return [
|
|
2470
|
+
` <!-- ${bar} -->`,
|
|
2471
|
+
` <!-- ${label.padEnd(42)} -->`,
|
|
2472
|
+
` <!-- ${bar} -->`,
|
|
2473
|
+
].join('\n');
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
const pagesDir = path.join(rootDir, 'src', 'data', 'pages');
|
|
2477
|
+
const buildTime = toW3CDate(new Date());
|
|
2478
|
+
|
|
2479
|
+
const pageFiles = listJsonFilesRecursive(pagesDir);
|
|
2480
|
+
const pages = pageFiles.map((fullPath) => {
|
|
2481
|
+
const slug = path
|
|
2482
|
+
.relative(pagesDir, fullPath)
|
|
2483
|
+
.replace(/\\/g, '/')
|
|
2484
|
+
.replace(/\.json$/i, '');
|
|
2485
|
+
const lastmod = toW3CDate(fs.statSync(fullPath).mtime);
|
|
2486
|
+
return { slug, lastmod };
|
|
2487
|
+
});
|
|
2488
|
+
|
|
2489
|
+
const entries = [];
|
|
2490
|
+
|
|
2491
|
+
entries.push(sectionComment('GLOBAL AGENT DISCOVERY NODES'));
|
|
2492
|
+
entries.push(
|
|
2493
|
+
urlEntry({ loc: `${baseUrl}/llms.txt`, lastmod: buildTime, changefreq: 'weekly', priority: '1.0' }),
|
|
2494
|
+
);
|
|
2495
|
+
entries.push(
|
|
2496
|
+
urlEntry({ loc: `${baseUrl}/mcp-manifest.json`, lastmod: buildTime, changefreq: 'weekly', priority: '1.0' }),
|
|
2497
|
+
);
|
|
2498
|
+
|
|
2499
|
+
for (const { slug, lastmod } of pages) {
|
|
2500
|
+
const humanPath = slug === 'home' ? '/' : `/${slug}`;
|
|
2501
|
+
const label = `PAGE: ${slug.toUpperCase()}`;
|
|
2502
|
+
|
|
2503
|
+
entries.push(sectionComment(label));
|
|
2504
|
+
entries.push(
|
|
2505
|
+
urlEntry({ loc: `${baseUrl}${humanPath}`, lastmod, changefreq: 'daily', priority: '0.9', comment: 'Human UI' }),
|
|
2506
|
+
);
|
|
2507
|
+
entries.push(
|
|
2508
|
+
urlEntry({ loc: `${baseUrl}/${slug}.json`, lastmod, changefreq: 'daily', priority: '0.9', comment: 'Machine Payload' }),
|
|
2509
|
+
);
|
|
2510
|
+
entries.push(
|
|
2511
|
+
urlEntry({ loc: `${baseUrl}/schemas/${slug}.schema.json`, lastmod: buildTime, changefreq: 'weekly', priority: '0.8', comment: 'Machine Contract (Schema)' }),
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
const xml = [
|
|
2516
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
2517
|
+
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`,
|
|
2518
|
+
``,
|
|
2519
|
+
entries.join('\n'),
|
|
2520
|
+
``,
|
|
2521
|
+
`</urlset>`,
|
|
2522
|
+
].join('\n');
|
|
2523
|
+
|
|
2524
|
+
const outPath = path.join(rootDir, 'public', 'sitemap.xml');
|
|
2525
|
+
fs.writeFileSync(outPath, xml, 'utf-8');
|
|
2526
|
+
console.log('[sitemap] Written -> public/sitemap.xml');
|
|
2527
|
+
|
|
2242
2528
|
END_OF_FILE_CONTENT
|
|
2243
2529
|
echo "Creating scripts/sync-pages-to-public.mjs..."
|
|
2244
2530
|
cat << 'END_OF_FILE_CONTENT' > "scripts/sync-pages-to-public.mjs"
|