designlang 7.2.0 → 9.0.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +154 -13
  3. package/bin/design-extract.js +94 -1
  4. package/package.json +9 -3
  5. package/src/config.js +2 -0
  6. package/src/crawler.js +55 -6
  7. package/src/drift.js +137 -0
  8. package/src/extractors/accessibility.js +44 -1
  9. package/src/extractors/colors.js +50 -12
  10. package/src/extractors/component-anatomy.js +123 -0
  11. package/src/extractors/motion.js +184 -0
  12. package/src/extractors/scoring.js +49 -30
  13. package/src/extractors/voice.js +96 -0
  14. package/src/formatters/markdown.js +88 -0
  15. package/src/formatters/motion-tokens.js +22 -0
  16. package/src/index.js +14 -0
  17. package/src/lint.js +198 -0
  18. package/src/visual-diff.js +116 -0
  19. package/.github/FUNDING.yml +0 -1
  20. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
  21. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  22. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -28
  23. package/.github/og-preview.png +0 -0
  24. package/.github/workflows/manavarya-bot.yml +0 -17
  25. package/chrome-extension/README.md +0 -41
  26. package/chrome-extension/icons/favicon.svg +0 -7
  27. package/chrome-extension/icons/icon-128.png +0 -0
  28. package/chrome-extension/icons/icon-16.png +0 -0
  29. package/chrome-extension/icons/icon-32.png +0 -0
  30. package/chrome-extension/icons/icon-48.png +0 -0
  31. package/chrome-extension/manifest.json +0 -26
  32. package/chrome-extension/popup.html +0 -167
  33. package/chrome-extension/popup.js +0 -59
  34. package/docs/superpowers/plans/2026-04-18-designlang-v7.md +0 -1121
  35. package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +0 -150
  36. package/docs/superpowers/specs/2026-04-18-website-redesign-design.md +0 -120
  37. package/docs/superpowers/specs/2026-04-19-designlang-v7-1-design.md +0 -111
  38. package/tests/cli.test.js +0 -84
  39. package/tests/cookies.test.js +0 -98
  40. package/tests/extractors.test.js +0 -792
  41. package/tests/formatters.test.js +0 -709
  42. package/tests/interaction-states.test.js +0 -62
  43. package/tests/mcp.test.js +0 -68
  44. package/tests/modern-css.test.js +0 -104
  45. package/tests/routes-reconciliation.test.js +0 -120
  46. package/tests/utils.test.js +0 -413
  47. package/tests/wide-gamut.test.js +0 -90
  48. package/website/.claude/launch.json +0 -11
  49. package/website/AGENTS.md +0 -5
  50. package/website/CLAUDE.md +0 -1
  51. package/website/README.md +0 -36
  52. package/website/app/api/extract/route.js +0 -245
  53. package/website/app/components/A11ySlider.js +0 -369
  54. package/website/app/components/Comparison.js +0 -286
  55. package/website/app/components/CssHealth.js +0 -243
  56. package/website/app/components/Extractor.js +0 -184
  57. package/website/app/components/HeroExtractor.js +0 -455
  58. package/website/app/components/Marginalia.js +0 -3
  59. package/website/app/components/McpSection.js +0 -223
  60. package/website/app/components/PlatformTabs.js +0 -250
  61. package/website/app/components/RegionsComponents.js +0 -429
  62. package/website/app/components/Rule.js +0 -13
  63. package/website/app/components/Specimens.js +0 -237
  64. package/website/app/components/StructuredData.js +0 -144
  65. package/website/app/components/TokenBrowser.js +0 -344
  66. package/website/app/components/token-browser-sample.js +0 -65
  67. package/website/app/globals.css +0 -505
  68. package/website/app/icon.svg +0 -7
  69. package/website/app/layout.js +0 -126
  70. package/website/app/opengraph-image.js +0 -170
  71. package/website/app/page.js +0 -399
  72. package/website/app/robots.js +0 -15
  73. package/website/app/seo-config.js +0 -82
  74. package/website/app/sitemap.js +0 -18
  75. package/website/jsconfig.json +0 -7
  76. package/website/lib/cache.js +0 -73
  77. package/website/lib/rate-limit.js +0 -30
  78. package/website/lib/rate-limit.test.js +0 -55
  79. package/website/lib/specimens.json +0 -86
  80. package/website/lib/token-helpers.js +0 -70
  81. package/website/lib/url-safety.js +0 -103
  82. package/website/lib/url-safety.test.js +0 -116
  83. package/website/lib/zip-files.js +0 -15
  84. package/website/next.config.mjs +0 -15
  85. package/website/package-lock.json +0 -1353
  86. package/website/package.json +0 -19
  87. package/website/public/favicon.svg +0 -7
  88. package/website/public/logo-specimen.svg +0 -76
  89. package/website/public/mark.svg +0 -12
  90. package/website/public/site.webmanifest +0 -13
@@ -1,7 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "paths": {
4
- "@/*": ["./*"]
5
- }
6
- }
7
- }
@@ -1,73 +0,0 @@
1
- // Vercel Blob-backed cache for extraction payloads.
2
- //
3
- // TTL: 24h, enforced on read by checking `generatedAt` in the stored payload.
4
- // Key: normalized URL → SHA-256 hex.
5
- // Gracefully no-ops when BLOB_READ_WRITE_TOKEN is absent (local dev).
6
-
7
- import { createHash } from 'node:crypto';
8
-
9
- const TTL_MS = 24 * 60 * 60 * 1000;
10
- let warnedMissingToken = false;
11
-
12
- export function cacheKey(url) {
13
- const normalized = String(url).trim().toLowerCase();
14
- return createHash('sha256').update(normalized).digest('hex');
15
- }
16
-
17
- function hasBlob() {
18
- if (!process.env.BLOB_READ_WRITE_TOKEN) {
19
- if (!warnedMissingToken) {
20
- console.log('[cache] BLOB_READ_WRITE_TOKEN not set — skipping cache');
21
- warnedMissingToken = true;
22
- }
23
- return false;
24
- }
25
- return true;
26
- }
27
-
28
- function blobPath(key) {
29
- return `extract-cache/${key}.json`;
30
- }
31
-
32
- export async function getCached(key) {
33
- if (!hasBlob()) return null;
34
- try {
35
- const { list } = await import('@vercel/blob');
36
- const path = blobPath(key);
37
- const result = await list({ prefix: path, limit: 1 });
38
- const blob = result.blobs?.find((b) => b.pathname === path);
39
- if (!blob) return null;
40
-
41
- const res = await fetch(blob.url, { cache: 'no-store' });
42
- if (!res.ok) return null;
43
- const payload = await res.json();
44
-
45
- const generatedAt = typeof payload?.generatedAt === 'number' ? payload.generatedAt : 0;
46
- if (Date.now() - generatedAt > TTL_MS) return null;
47
- if (!payload?.design) return null;
48
- return { design: payload.design };
49
- } catch (err) {
50
- console.error('[cache] read failed', err?.message);
51
- return null;
52
- }
53
- }
54
-
55
- export async function putCached(key, { design }) {
56
- if (!hasBlob()) return;
57
- try {
58
- const { put } = await import('@vercel/blob');
59
- const payload = {
60
- generatedAt: Date.now(),
61
- expiresAt: Date.now() + TTL_MS,
62
- design,
63
- };
64
- await put(blobPath(key), JSON.stringify(payload), {
65
- access: 'public',
66
- contentType: 'application/json',
67
- addRandomSuffix: false,
68
- allowOverwrite: true,
69
- });
70
- } catch (err) {
71
- console.error('[cache] write failed', err?.message);
72
- }
73
- }
@@ -1,30 +0,0 @@
1
- // Per-key in-memory rate limiter.
2
- //
3
- // Vercel Fluid Compute reuses function instances across concurrent requests
4
- // within a region, so this Map is *per-instance*, not global. An attacker can
5
- // spread requests across regions to bypass it — that's acceptable for Wave 2
6
- // because we pair this at the middleware layer with Vercel BotID (out of scope
7
- // for PR B). The per-instance bound still blunts accidental hammering.
8
-
9
- const hits = new Map();
10
-
11
- export function checkRate(key, { limit = 3, windowMs = 24 * 60 * 60 * 1000 } = {}) {
12
- const now = Date.now();
13
- const entry = hits.get(key);
14
-
15
- if (!entry || now - entry.firstAt >= windowMs) {
16
- hits.set(key, { count: 1, firstAt: now });
17
- return { allowed: true, remaining: Math.max(0, limit - 1), resetAt: now + windowMs };
18
- }
19
-
20
- entry.count += 1;
21
- const resetAt = entry.firstAt + windowMs;
22
- const allowed = entry.count <= limit;
23
- const remaining = Math.max(0, limit - entry.count);
24
- return { allowed, remaining, resetAt };
25
- }
26
-
27
- // Test-only: wipe state. Safe to call from production code — no-op effect.
28
- export function _resetRateLimit() {
29
- hits.clear();
30
- }
@@ -1,55 +0,0 @@
1
- import { test, beforeEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { checkRate, _resetRateLimit } from './rate-limit.js';
4
-
5
- beforeEach(() => _resetRateLimit());
6
-
7
- test('first three requests allowed, fourth blocked', () => {
8
- const key = 'ip-1';
9
- assert.equal(checkRate(key).allowed, true);
10
- assert.equal(checkRate(key).allowed, true);
11
- assert.equal(checkRate(key).allowed, true);
12
- assert.equal(checkRate(key).allowed, false);
13
- });
14
-
15
- test('remaining counts down', () => {
16
- const key = 'ip-2';
17
- assert.equal(checkRate(key).remaining, 2);
18
- assert.equal(checkRate(key).remaining, 1);
19
- assert.equal(checkRate(key).remaining, 0);
20
- assert.equal(checkRate(key).remaining, 0);
21
- });
22
-
23
- test('different keys have independent budgets', () => {
24
- checkRate('a');
25
- checkRate('a');
26
- checkRate('a');
27
- assert.equal(checkRate('a').allowed, false);
28
- assert.equal(checkRate('b').allowed, true);
29
- });
30
-
31
- test('window reset after expiry', async () => {
32
- const key = 'ip-3';
33
- const opts = { limit: 2, windowMs: 30 };
34
- assert.equal(checkRate(key, opts).allowed, true);
35
- assert.equal(checkRate(key, opts).allowed, true);
36
- assert.equal(checkRate(key, opts).allowed, false);
37
- await new Promise((r) => setTimeout(r, 40));
38
- const after = checkRate(key, opts);
39
- assert.equal(after.allowed, true);
40
- assert.equal(after.remaining, 1);
41
- });
42
-
43
- test('custom limit respected', () => {
44
- const key = 'ip-4';
45
- const opts = { limit: 5, windowMs: 60000 };
46
- for (let i = 0; i < 5; i++) assert.equal(checkRate(key, opts).allowed, true);
47
- assert.equal(checkRate(key, opts).allowed, false);
48
- });
49
-
50
- test('resetAt advances with the first-seen time', () => {
51
- const key = 'ip-5';
52
- const { resetAt: a } = checkRate(key, { limit: 3, windowMs: 60000 });
53
- const { resetAt: b } = checkRate(key, { limit: 3, windowMs: 60000 });
54
- assert.equal(a, b);
55
- });
@@ -1,86 +0,0 @@
1
- [
2
- {
3
- "host": "stripe.com",
4
- "display": "Stripe",
5
- "year": 2026,
6
- "framework": "Next.js",
7
- "accent": "#635BFF",
8
- "palette": ["#635BFF", "#0A2540", "#F7FAFC", "#1A1F36", "#F6F9FC"],
9
- "fontDisplay": "Sohne Breit",
10
- "fontBody": "Sohne",
11
- "radius": "8px",
12
- "a11y": 94,
13
- "designScore": 91,
14
- "note": "Tight palette, disciplined spacing scale. The gradient hero is one of the few remaining brand indulgences."
15
- },
16
- {
17
- "host": "vercel.com",
18
- "display": "Vercel",
19
- "year": 2026,
20
- "framework": "Next.js",
21
- "accent": "#0A0A0A",
22
- "palette": ["#0A0A0A", "#FAFAFA", "#EAEAEA", "#A1A1A1", "#0070F3"],
23
- "fontDisplay": "Geist",
24
- "fontBody": "Geist",
25
- "radius": "6px",
26
- "a11y": 92,
27
- "designScore": 88,
28
- "note": "A monochrome system with a single blue for links. Geist carries almost the entire identity."
29
- },
30
- {
31
- "host": "linear.app",
32
- "display": "Linear",
33
- "year": 2026,
34
- "framework": "Next.js",
35
- "accent": "#5E6AD2",
36
- "palette": ["#5E6AD2", "#08090A", "#1C1D1F", "#D2D4E6", "#F7F8F8"],
37
- "fontDisplay": "Inter Tight",
38
- "fontBody": "Inter",
39
- "radius": "8px",
40
- "a11y": 90,
41
- "designScore": 89,
42
- "note": "Dark-first marketing with a precise purple. Every surface has been rounded to exactly 8px."
43
- },
44
- {
45
- "host": "github.com",
46
- "display": "GitHub",
47
- "year": 2026,
48
- "framework": "Rails",
49
- "accent": "#0969DA",
50
- "palette": ["#0969DA", "#0D1117", "#F6F8FA", "#1F2328", "#D0D7DE"],
51
- "fontDisplay": "Mona Sans",
52
- "fontBody": "Mona Sans",
53
- "radius": "6px",
54
- "a11y": 96,
55
- "designScore": 82,
56
- "note": "Primer is the reason. Densely informational and accessible — but the marketing pages drift."
57
- },
58
- {
59
- "host": "figma.com",
60
- "display": "Figma",
61
- "year": 2026,
62
- "framework": "Ember",
63
- "accent": "#F24E1E",
64
- "palette": ["#F24E1E", "#0D0D0D", "#FFFFFF", "#A259FF", "#1ABCFE"],
65
- "fontDisplay": "Whyte",
66
- "fontBody": "Whyte",
67
- "radius": "5px",
68
- "a11y": 88,
69
- "designScore": 85,
70
- "note": "The only site where the brand colors from the logo are still visible across the whole product."
71
- },
72
- {
73
- "host": "apple.com",
74
- "display": "Apple",
75
- "year": 2026,
76
- "framework": "Custom",
77
- "accent": "#0071E3",
78
- "palette": ["#0071E3", "#1D1D1F", "#F5F5F7", "#86868B", "#FBFBFD"],
79
- "fontDisplay": "SF Pro Display",
80
- "fontBody": "SF Pro Text",
81
- "radius": "12px",
82
- "a11y": 93,
83
- "designScore": 94,
84
- "note": "Four grays, one blue, SF Pro. The spacing and type ramp is the most imitated system on the web."
85
- }
86
- ]
@@ -1,70 +0,0 @@
1
- // Browser-friendly DTCG helpers — mirrors src/formatters/_token-ref.js contract.
2
- // resolveRef: follow {ref} chains, guard cycles.
3
- // flattenTokens: walk primitive.*/semantic.* into a flat row list.
4
-
5
- const REF_PATTERN = /^\{([^}]+)\}$/;
6
-
7
- function parseRef(value) {
8
- if (typeof value !== 'string') return null;
9
- const m = value.match(REF_PATTERN);
10
- return m ? m[1] : null;
11
- }
12
-
13
- function getAtPath(tokens, path) {
14
- const parts = path.split('.');
15
- let node = tokens;
16
- for (const part of parts) {
17
- if (node == null || typeof node !== 'object') return undefined;
18
- node = node[part];
19
- }
20
- return node;
21
- }
22
-
23
- export function resolveRef(tokens, pathOrRefString, seen = new Set()) {
24
- if (pathOrRefString == null) return undefined;
25
- const refPath = parseRef(pathOrRefString);
26
- const path = refPath != null ? refPath : String(pathOrRefString);
27
- if (seen.has(path)) return undefined;
28
- seen.add(path);
29
-
30
- const node = getAtPath(tokens, path);
31
- if (node == null) return undefined;
32
-
33
- if (typeof node === 'object' && '$value' in node) {
34
- const inner = node.$value;
35
- const innerRef = parseRef(inner);
36
- if (innerRef) return resolveRef(tokens, innerRef, seen);
37
- return inner;
38
- }
39
-
40
- const innerRef = parseRef(node);
41
- if (innerRef) return resolveRef(tokens, innerRef, seen);
42
-
43
- return node;
44
- }
45
-
46
- // Walk a DTCG subtree and collect every leaf token (one with $value).
47
- function walk(node, prefix, out, layer) {
48
- if (node == null || typeof node !== 'object') return;
49
- if ('$value' in node) {
50
- out.push({ layer, path: prefix, $type: node.$type, $value: node.$value });
51
- return;
52
- }
53
- for (const key of Object.keys(node)) {
54
- if (key.startsWith('$')) continue;
55
- const next = prefix ? `${prefix}.${key}` : key;
56
- walk(node[key], next, out, layer);
57
- }
58
- }
59
-
60
- export function flattenTokens(tokens, opts = { layer: 'all' }) {
61
- const layer = opts.layer || 'all';
62
- const rows = [];
63
- if (layer === 'primitive' || layer === 'all') {
64
- walk(tokens.primitive, 'primitive', rows, 'primitive');
65
- }
66
- if (layer === 'semantic' || layer === 'all') {
67
- walk(tokens.semantic, 'semantic', rows, 'semantic');
68
- }
69
- return rows;
70
- }
@@ -1,103 +0,0 @@
1
- // URL safety guard for the extraction endpoint.
2
- //
3
- // Validates a raw user-supplied URL against common SSRF vectors. Pure function —
4
- // no DNS lookups (deferred to Wave 3 when we pair with BotID at middleware).
5
- //
6
- // Allowed: http(s) URLs on port 80/443, public hostnames.
7
- // Rejected: private IPs (v4 + v6), loopback, link-local, localhost, *.local,
8
- // *.localhost, non-http(s) schemes, non-standard ports.
9
-
10
- const IPV4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
11
-
12
- function isPrivateIPv4(host) {
13
- const m = host.match(IPV4);
14
- if (!m) return false;
15
- const [a, b] = [Number(m[1]), Number(m[2])];
16
- if (a < 0 || a > 255 || b < 0 || b > 255 || Number(m[3]) > 255 || Number(m[4]) > 255) return true;
17
- if (a === 10) return true; // 10.0.0.0/8
18
- if (a === 127) return true; // 127.0.0.0/8
19
- if (a === 169 && b === 254) return true; // 169.254.0.0/16
20
- if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12
21
- if (a === 192 && b === 168) return true; // 192.168.0.0/16
22
- if (a === 0) return true; // 0.0.0.0/8
23
- return false;
24
- }
25
-
26
- // Normalize an IPv6 literal: strip surrounding brackets, lowercase.
27
- function stripV6Brackets(host) {
28
- if (host.startsWith('[') && host.endsWith(']')) return host.slice(1, -1);
29
- return host;
30
- }
31
-
32
- function isIPv6Literal(host) {
33
- // Very loose — anything with a colon that isn't a port-suffixed hostname.
34
- return host.includes(':');
35
- }
36
-
37
- function isPrivateIPv6(rawHost) {
38
- const host = stripV6Brackets(rawHost).toLowerCase();
39
- if (!isIPv6Literal(host)) return false;
40
- // Normalize abbreviated forms conservatively.
41
- if (host === '::1' || host === '0:0:0:0:0:0:0:1') return true; // loopback
42
- if (host === '::' || host === '0:0:0:0:0:0:0:0') return true; // unspecified
43
- // IPv4-mapped / compat: ::ffff:127.0.0.1 → treat the embedded IPv4
44
- const v4mapped = host.match(/::ffff:(\d+\.\d+\.\d+\.\d+)/);
45
- if (v4mapped && isPrivateIPv4(v4mapped[1])) return true;
46
- // fc00::/7 — unique local addresses (fc.. or fd..)
47
- if (/^f[cd][0-9a-f]{0,2}:/.test(host)) return true;
48
- // fe80::/10 — link-local
49
- if (/^fe[89ab][0-9a-f]?:/.test(host)) return true;
50
- return false;
51
- }
52
-
53
- function isLocalHostname(host) {
54
- const h = host.toLowerCase();
55
- if (h === 'localhost') return true;
56
- if (h.endsWith('.local')) return true;
57
- if (h.endsWith('.localhost')) return true;
58
- return false;
59
- }
60
-
61
- export function validateTargetUrl(rawUrl) {
62
- if (typeof rawUrl !== 'string' || !rawUrl.trim()) {
63
- return { ok: false, reason: 'URL is required', status: 400 };
64
- }
65
- let input = rawUrl.trim();
66
- if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(input)) {
67
- input = `https://${input}`;
68
- }
69
-
70
- let parsed;
71
- try { parsed = new URL(input); } catch {
72
- return { ok: false, reason: 'Invalid URL', status: 400 };
73
- }
74
-
75
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
76
- return { ok: false, reason: 'Only http(s) URLs are allowed', status: 400 };
77
- }
78
-
79
- // Port check: URL leaves `port` empty for defaults (80 for http, 443 for https).
80
- // Explicit 80/443 are fine; anything else is rejected.
81
- if (parsed.port && parsed.port !== '80' && parsed.port !== '443') {
82
- return { ok: false, reason: 'Port not allowed (only 80/443)', status: 400 };
83
- }
84
-
85
- const hostname = parsed.hostname;
86
- if (!hostname) {
87
- return { ok: false, reason: 'Missing hostname', status: 400 };
88
- }
89
-
90
- if (isLocalHostname(hostname)) {
91
- return { ok: false, reason: 'Local hostnames are not allowed', status: 400 };
92
- }
93
-
94
- if (IPV4.test(hostname) && isPrivateIPv4(hostname)) {
95
- return { ok: false, reason: 'Private IP addresses are not allowed', status: 400 };
96
- }
97
-
98
- if (isPrivateIPv6(hostname)) {
99
- return { ok: false, reason: 'Private IP addresses are not allowed', status: 400 };
100
- }
101
-
102
- return { ok: true, url: parsed.toString() };
103
- }
@@ -1,116 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { validateTargetUrl } from './url-safety.js';
4
-
5
- test('accepts https URL', () => {
6
- const r = validateTargetUrl('https://stripe.com');
7
- assert.equal(r.ok, true);
8
- assert.equal(r.url, 'https://stripe.com/');
9
- });
10
-
11
- test('accepts http URL on port 80', () => {
12
- const r = validateTargetUrl('http://example.com:80');
13
- assert.equal(r.ok, true);
14
- });
15
-
16
- test('normalizes bare hostname to https', () => {
17
- const r = validateTargetUrl('example.com');
18
- assert.equal(r.ok, true);
19
- assert.equal(r.url, 'https://example.com/');
20
- });
21
-
22
- test('rejects localhost', () => {
23
- const r = validateTargetUrl('http://localhost');
24
- assert.equal(r.ok, false);
25
- assert.equal(r.status, 400);
26
- });
27
-
28
- test('rejects *.local hostnames', () => {
29
- const r = validateTargetUrl('https://printer.local');
30
- assert.equal(r.ok, false);
31
- });
32
-
33
- test('rejects *.localhost hostnames', () => {
34
- const r = validateTargetUrl('https://app.localhost');
35
- assert.equal(r.ok, false);
36
- });
37
-
38
- test('rejects 127.0.0.1', () => {
39
- const r = validateTargetUrl('http://127.0.0.1');
40
- assert.equal(r.ok, false);
41
- });
42
-
43
- test('rejects 10.x private range', () => {
44
- const r = validateTargetUrl('http://10.1.2.3');
45
- assert.equal(r.ok, false);
46
- });
47
-
48
- test('rejects 192.168.x private range', () => {
49
- const r = validateTargetUrl('http://192.168.0.1');
50
- assert.equal(r.ok, false);
51
- });
52
-
53
- test('rejects 172.16–31.x private range', () => {
54
- const r = validateTargetUrl('http://172.20.0.5');
55
- assert.equal(r.ok, false);
56
- });
57
-
58
- test('allows 172.15.x (outside private range)', () => {
59
- const r = validateTargetUrl('http://172.15.0.1');
60
- assert.equal(r.ok, true);
61
- });
62
-
63
- test('rejects 169.254.x link-local', () => {
64
- const r = validateTargetUrl('http://169.254.0.1');
65
- assert.equal(r.ok, false);
66
- });
67
-
68
- test('rejects file:// scheme', () => {
69
- const r = validateTargetUrl('file://evil');
70
- assert.equal(r.ok, false);
71
- });
72
-
73
- test('rejects javascript: scheme', () => {
74
- const r = validateTargetUrl('javascript:alert(1)');
75
- assert.equal(r.ok, false);
76
- });
77
-
78
- test('rejects non-80/443 port', () => {
79
- const r = validateTargetUrl('https://example.com:22');
80
- assert.equal(r.ok, false);
81
- });
82
-
83
- test('rejects port 3000', () => {
84
- const r = validateTargetUrl('https://example.com:3000');
85
- assert.equal(r.ok, false);
86
- });
87
-
88
- test('rejects IPv6 loopback ::1', () => {
89
- const r = validateTargetUrl('http://[::1]');
90
- assert.equal(r.ok, false);
91
- });
92
-
93
- test('rejects IPv6 [::1]:3000', () => {
94
- const r = validateTargetUrl('http://[::1]:3000');
95
- assert.equal(r.ok, false);
96
- });
97
-
98
- test('rejects IPv6 unique-local fc00::', () => {
99
- const r = validateTargetUrl('http://[fc00::1]');
100
- assert.equal(r.ok, false);
101
- });
102
-
103
- test('rejects IPv6 link-local fe80::', () => {
104
- const r = validateTargetUrl('http://[fe80::1]');
105
- assert.equal(r.ok, false);
106
- });
107
-
108
- test('rejects empty string', () => {
109
- const r = validateTargetUrl('');
110
- assert.equal(r.ok, false);
111
- });
112
-
113
- test('rejects garbage', () => {
114
- const r = validateTargetUrl('http://');
115
- assert.equal(r.ok, false);
116
- });
@@ -1,15 +0,0 @@
1
- // Client-side helper — turns a {filename: string} map into a Blob URL.
2
- // The caller is responsible for calling URL.revokeObjectURL on the returned url.
3
-
4
- export async function zipFilesToUrl(files, { name = 'designlang-output' } = {}) {
5
- const JSZip = (await import('jszip')).default;
6
- const zip = new JSZip();
7
- for (const [filename, content] of Object.entries(files)) {
8
- zip.file(filename, content);
9
- }
10
- const blob = await zip.generateAsync({ type: 'blob' });
11
- return {
12
- url: URL.createObjectURL(blob),
13
- filename: `${name}.zip`,
14
- };
15
- }
@@ -1,15 +0,0 @@
1
- /** @type {import('next').NextConfig} */
2
- const nextConfig = {
3
- turbopack: {},
4
- webpack: (config, { isServer }) => {
5
- if (isServer) {
6
- // Don't bundle playwright — emit require('playwright-core') at runtime instead
7
- config.externals.push({
8
- 'playwright': 'commonjs playwright-core',
9
- });
10
- }
11
- return config;
12
- },
13
- };
14
-
15
- export default nextConfig;