-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoptions.js
More file actions
118 lines (105 loc) · 4.51 KB
/
Copy pathoptions.js
File metadata and controls
118 lines (105 loc) · 4.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// Single source of truth for the common card option surface.
//
// Every endpoint accepts the same baseline params (theme, font, hide_*,
// border_radius, card_width, color overrides). Defining the parsing logic
// here means new themes / new common options propagate to all endpoints with
// one edit instead of N. Endpoints with extra params still parse those
// locally — `parseCardOptions` only owns the shared surface.
const { getTheme, applyOverrides } = require("./themes");
const { parseBoolean, parseIntSafe, parseRadius } = require("./utils");
const { fetchExternalTheme } = require("./theme-url");
// Maps the public ?<color>_color= query keys to the palette keys consumed by
// `applyOverrides`. Centralized so `parseCardOptions` and `resolveCardOptions`
// stay in sync when a new color slot is added.
const COLOR_OVERRIDE_PARAMS = {
bg: "bg_color",
text: "text_color",
title: "title_color",
icon: "icon_color",
border: "border_color",
accent: "accent_color",
};
function readColorOverrides(params) {
const out = {};
for (const [key, paramName] of Object.entries(COLOR_OVERRIDE_PARAMS)) {
out[key] = params.get(paramName);
}
return out;
}
// Use a fixed base origin instead of `req.headers.host`. The host header is
// untrusted in serverless environments and we only want the path + query
// parts of `req.url` anyway, so passing a constant origin avoids any chance
// of host-header injection ever surfacing through `searchParams`.
const FIXED_PARSE_BASE = "http://profilekit.local";
function parseSearchParams(req) {
return new URL(req.url, FIXED_PARSE_BASE).searchParams;
}
// Outer bounds on `?card_width=`. Anything outside this range falls back to
// the 495 default. The upper bound protects against ?card_width=99999 style
// DoS attempts (SVG memory + CDN bandwidth); the lower bound keeps the card
// wide enough to fit the fixed padding + title without clipping.
const CARD_WIDTH_MIN = 200;
const CARD_WIDTH_MAX = 1600;
function parseCardOptions(params) {
const theme = params.get("theme") || "dark";
const colors = getTheme(theme, readColorOverrides(params));
return {
theme,
colors,
font: params.get("font"),
title: params.get("title"),
hideBorder: parseBoolean(params.get("hide_border")),
hideTitle: parseBoolean(params.get("hide_title")),
hideBar: parseBoolean(params.get("hide_bar")),
borderRadius: parseRadius(params.get("border_radius"), undefined),
cardWidth: params.has("card_width")
? parseIntSafe(params.get("card_width"), 495, CARD_WIDTH_MIN, CARD_WIDTH_MAX)
: undefined,
};
}
// `prefetched` lets /api/stack avoid N+1 gist fetches: stack.js resolves the
// top-level theme_url once, then passes { url, palette } (or { url, error })
// here for every child slot whose theme_url matches. A child that overrides
// with `<card>.theme_url=<different>` falls through to the live fetch path.
async function resolveCardOptions(params, prefetched = null) {
const opts = parseCardOptions(params);
const themeUrl = params.get("theme_url");
if (!themeUrl) return { opts, themeError: null };
if (prefetched && prefetched.url === themeUrl) {
if (prefetched.error) return { opts, themeError: prefetched.error };
const colors = applyOverrides(prefetched.palette, readColorOverrides(params));
return { opts: { ...opts, colors }, themeError: null };
}
try {
const externalPalette = await fetchExternalTheme(themeUrl);
// External palette becomes the base; per-param color overrides still
// win on top, mirroring the precedence of `?bg_color=` over `?theme=`.
const colors = applyOverrides(externalPalette, readColorOverrides(params));
return { opts: { ...opts, colors }, themeError: null };
} catch (err) {
// Fall back to whatever parseCardOptions resolved (the built-in `?theme=`
// or default dark). The handler surfaces `themeError` as an
// `X-Theme-Error` response header so callers can debug their payload.
return { opts, themeError: err.message };
}
}
// Single-shot prefetch for a top-level theme_url. Used by /api/stack to
// resolve the gist once and reuse for every slot via `resolveCardOptions`'s
// `prefetched` arg.
async function prefetchExternalTheme(themeUrl) {
if (!themeUrl) return null;
try {
const palette = await fetchExternalTheme(themeUrl);
return { url: themeUrl, palette };
} catch (err) {
return { url: themeUrl, error: err.message };
}
}
module.exports = {
parseCardOptions,
resolveCardOptions,
prefetchExternalTheme,
parseSearchParams,
CARD_WIDTH_MIN,
CARD_WIDTH_MAX,
};