@czap/edge 0.1.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.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Per-tenant theme compilation at the edge.
3
+ *
4
+ * Takes a flat map of design token definitions and produces CSS custom
5
+ * property declarations suitable for injection into the `<html>` element
6
+ * or a `<style>` block.
7
+ *
8
+ * This is a pure function with no side effects -- safe for edge runtime use.
9
+ *
10
+ * @module
11
+ */
12
+ const SAFE_PREFIX_PATTERN = /^[a-z0-9-]+$/;
13
+ const UNSAFE_CSS_VALUE_PATTERN = /[;{}<>]/;
14
+ // ---------------------------------------------------------------------------
15
+ // Internal helpers
16
+ // ---------------------------------------------------------------------------
17
+ /**
18
+ * Convert a token name to a valid CSS custom property name.
19
+ * Replaces dots and spaces with hyphens, lowercases, strips invalid chars.
20
+ */
21
+ function tokenToProperty(prefix, name) {
22
+ const sanitised = name
23
+ .toLowerCase()
24
+ .replace(/[.\s]+/g, '-')
25
+ .replace(/[^a-z0-9-_]/g, '');
26
+ return `--${prefix}-${sanitised}`;
27
+ }
28
+ function normalizePrefix(prefix) {
29
+ const normalized = prefix.toLowerCase();
30
+ if (!SAFE_PREFIX_PATTERN.test(normalized)) {
31
+ throw new Error(`Invalid theme prefix "${prefix}". Prefixes must contain only lowercase letters, digits, and hyphens.`);
32
+ }
33
+ return normalized;
34
+ }
35
+ /**
36
+ * Format a token value for CSS output.
37
+ * Numbers are emitted bare (no unit) so consumers can apply their own units.
38
+ */
39
+ function formatValue(value) {
40
+ const formatted = typeof value === 'number' ? String(value) : value;
41
+ if (UNSAFE_CSS_VALUE_PATTERN.test(formatted)) {
42
+ throw new Error(`Unsafe theme token value "${formatted}" cannot be serialized into CSS safely.`);
43
+ }
44
+ return formatted;
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Public API
48
+ // ---------------------------------------------------------------------------
49
+ /**
50
+ * Compile a set of design tokens into CSS custom property declarations.
51
+ *
52
+ * @param config - Token definitions and optional prefix.
53
+ * @returns CSS string and inline style string.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const result = compileTheme({
58
+ * tokens: { 'color.primary': '#3b82f6', 'spacing.base': 16 },
59
+ * prefix: 'czap',
60
+ * });
61
+ * // result.css =>
62
+ * // :root {
63
+ * // --czap-color-primary: #3b82f6;
64
+ * // --czap-spacing-base: 16;
65
+ * // }
66
+ * // result.inlineStyle =>
67
+ * // --czap-color-primary:#3b82f6;--czap-spacing-base:16
68
+ * ```
69
+ */
70
+ export function compileTheme(config) {
71
+ const prefix = normalizePrefix(config.prefix ?? 'czap');
72
+ const entries = Object.entries(config.tokens);
73
+ if (entries.length === 0) {
74
+ return { declarations: [], css: ':root {}', inlineStyle: '' };
75
+ }
76
+ const declarations = [];
77
+ const cssDeclarations = [];
78
+ const inlineParts = [];
79
+ for (const [name, value] of entries) {
80
+ const prop = tokenToProperty(prefix, name);
81
+ const formatted = formatValue(value);
82
+ declarations.push({ property: prop, value: formatted });
83
+ cssDeclarations.push(` ${prop}: ${formatted};`);
84
+ inlineParts.push(`${prop}:${formatted}`);
85
+ }
86
+ const css = `:root {\n${cssDeclarations.join('\n')}\n}`;
87
+ const inlineStyle = inlineParts.join(';');
88
+ return {
89
+ declarations: Object.freeze(declarations),
90
+ css,
91
+ inlineStyle,
92
+ };
93
+ }
94
+ //# sourceMappingURL=theme-compiler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-compiler.js","sourceRoot":"","sources":["../src/theme-compiler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA4CH,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAC3C,MAAM,wBAAwB,GAAG,SAAS,CAAC;AAE3C,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E;;;GAGG;AACH,SAAS,eAAe,CAAC,MAAc,EAAE,IAAY;IACnD,MAAM,SAAS,GAAG,IAAI;SACnB,WAAW,EAAE;SACb,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IAC/B,OAAO,KAAK,MAAM,IAAI,SAAS,EAAE,CAAC;AACpC,CAAC;AAED,SAAS,eAAe,CAAC,MAAc;IACrC,MAAM,UAAU,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IACxC,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CACb,yBAAyB,MAAM,uEAAuE,CACvG,CAAC;IACJ,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;GAGG;AACH,SAAS,WAAW,CAAC,KAAsB;IACzC,MAAM,SAAS,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACpE,IAAI,wBAAwB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CAAC,6BAA6B,SAAS,yCAAyC,CAAC,CAAC;IACnG,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,YAAY,CAAC,MAA0B;IACrD,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,CAAC;IACxD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAE9C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,GAAG,EAAE,UAAU,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;IAChE,CAAC;IAED,MAAM,YAAY,GAAuB,EAAE,CAAC;IAC5C,MAAM,eAAe,GAAa,EAAE,CAAC;IACrC,MAAM,WAAW,GAAa,EAAE,CAAC;IAEjC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,OAAO,EAAE,CAAC;QACpC,MAAM,IAAI,GAAG,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC3C,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QACrC,YAAY,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;QACxD,eAAe,CAAC,IAAI,CAAC,KAAK,IAAI,KAAK,SAAS,GAAG,CAAC,CAAC;QACjD,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,SAAS,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,MAAM,GAAG,GAAG,YAAY,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;IACxD,MAAM,WAAW,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAE1C,OAAO;QACL,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC;QACzC,GAAG;QACH,WAAW;KACZ,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@czap/edge",
3
+ "version": "0.1.0",
4
+ "description": "CDN-edge client hints, tier detection, and KV cache",
5
+ "license": "MIT",
6
+ "author": "Eassa Ayoub <eassa@heyoub.dev>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/heyoub/LiteShip",
10
+ "directory": "packages/edge"
11
+ },
12
+ "bugs": "https://github.com/heyoub/LiteShip/issues",
13
+ "homepage": "https://github.com/heyoub/LiteShip#readme",
14
+ "keywords": [
15
+ "czap",
16
+ "edge",
17
+ "cdn",
18
+ "client-hints",
19
+ "kv-cache",
20
+ "tier-detection",
21
+ "typescript"
22
+ ],
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "main": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js",
31
+ "development": "./src/index.ts"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src",
37
+ "LICENSE"
38
+ ],
39
+ "dependencies": {
40
+ "@czap/core": "0.1.0",
41
+ "@czap/detect": "0.1.0"
42
+ },
43
+ "peerDependencies": {
44
+ "effect": ">=4.0.0-beta.0"
45
+ },
46
+ "engines": {
47
+ "node": ">=22.0.0"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "scripts": {
53
+ "build": "tsc"
54
+ }
55
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Client Hints header parsing for edge-side device capability detection.
3
+ *
4
+ * Converts HTTP Client Hints headers into the same `ExtendedDeviceCapabilities`
5
+ * structure that `@czap/detect` uses, enabling reuse of the pure tier mapping
6
+ * functions at the edge without browser APIs.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import type { ExtendedDeviceCapabilities, GPUTier } from '@czap/detect';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /**
18
+ * Plain-object header bag accepted by {@link ClientHints.parseClientHints}.
19
+ *
20
+ * All names are lowercased because Client Hints headers are always lowercase
21
+ * in spec. Values that are missing simply fall back to conservative
22
+ * defaults during parsing.
23
+ */
24
+ export interface ClientHintsHeaders {
25
+ /** `Sec-CH-UA-Platform` (e.g. `"macOS"`, `"Windows"`). */
26
+ readonly 'sec-ch-ua-platform'?: string;
27
+ /** `Sec-CH-Device-Memory` in GiB (one of the standard buckets). */
28
+ readonly 'sec-ch-device-memory'?: string;
29
+ /** `Sec-CH-DPR` — devicePixelRatio as a decimal string. */
30
+ readonly 'sec-ch-dpr'?: string;
31
+ /** `Sec-CH-Viewport-Width` in CSS pixels. */
32
+ readonly 'sec-ch-viewport-width'?: string;
33
+ /** `Sec-CH-Viewport-Height` in CSS pixels. */
34
+ readonly 'sec-ch-viewport-height'?: string;
35
+ /** `Sec-CH-Prefers-Reduced-Motion` (`reduce` / `no-preference`). */
36
+ readonly 'sec-ch-prefers-reduced-motion'?: string;
37
+ /** `Sec-CH-Prefers-Color-Scheme` (`light` / `dark`). */
38
+ readonly 'sec-ch-prefers-color-scheme'?: string;
39
+ /** `Sec-CH-UA-Mobile` as a structured boolean (`?1` / `?0`). */
40
+ readonly 'sec-ch-ua-mobile'?: string;
41
+ /** `Sec-CH-UA` — full user-agent brand list. */
42
+ readonly 'sec-ch-ua'?: string;
43
+ /** `Save-Data` (`on`). */
44
+ readonly 'save-data'?: string;
45
+ /** `Downlink` estimate in Mb/s. */
46
+ readonly downlink?: string;
47
+ /** `ECT` effective connection type. */
48
+ readonly ect?: string;
49
+ /** `RTT` round-trip-time estimate in ms. */
50
+ readonly rtt?: string;
51
+ /** `User-Agent` fallback for GPU-tier heuristics. */
52
+ readonly 'user-agent'?: string;
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Internal helpers
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /**
60
+ * Type guard for Web API Headers objects (fetch Headers).
61
+ * Checks for the `get` method that distinguishes Headers from plain objects.
62
+ */
63
+ function isWebHeaders(value: ClientHintsHeaders | Headers): value is Headers {
64
+ return typeof (value as Record<string, unknown>).get === 'function';
65
+ }
66
+
67
+ /**
68
+ * Normalise a Headers-like input (Web API Headers or plain object) into
69
+ * a case-insensitive getter function.
70
+ */
71
+ function headerGetter(headers: ClientHintsHeaders | Headers): (name: string) => string | undefined {
72
+ if (isWebHeaders(headers)) {
73
+ return (name: string) => headers.get(name) ?? undefined;
74
+ }
75
+ // Client Hints headers are always lowercase in spec, but normalise anyway
76
+ const lower: Record<string, string> = {};
77
+ for (const [k, v] of Object.entries(headers)) {
78
+ if (v !== undefined) lower[k.toLowerCase()] = v;
79
+ }
80
+ return (name: string) => lower[name.toLowerCase()];
81
+ }
82
+
83
+ /**
84
+ * Parse a numeric header, returning undefined for missing / malformed values.
85
+ */
86
+ function parseFloat_(get: (name: string) => string | undefined, name: string): number | undefined {
87
+ const raw = get(name);
88
+ if (raw === undefined || raw === '') return undefined;
89
+ const n = Number.parseFloat(raw);
90
+ return Number.isFinite(n) ? n : undefined;
91
+ }
92
+
93
+ /**
94
+ * Clamp device memory to the set of valid values browsers actually report.
95
+ */
96
+ function clampMemory(raw: number): number {
97
+ const buckets = [0.25, 0.5, 1, 2, 4, 8] as const;
98
+ let closest: number = buckets[0]!;
99
+ for (const b of buckets) {
100
+ if (Math.abs(b - raw) < Math.abs(closest - raw)) closest = b;
101
+ }
102
+ return closest;
103
+ }
104
+
105
+ /**
106
+ * Crude GPU tier heuristic from User-Agent string.
107
+ * Without WebGL renderer info we can only make rough guesses.
108
+ */
109
+ function gpuTierFromUA(ua: string | undefined): GPUTier {
110
+ if (!ua) return 1;
111
+ const lower = ua.toLowerCase();
112
+
113
+ // Very low-end indicators
114
+ if (/kaios|nokia|feature/i.test(lower)) return 0;
115
+
116
+ // High-end mobile
117
+ if (/iphone\s*1[4-9]|iphone\s*[2-9]\d/i.test(lower)) return 2;
118
+ if (/sm-s9|sm-s2[4-9]|pixel\s*[8-9]/i.test(lower)) return 2;
119
+
120
+ // Desktop with common high-end hints
121
+ if (/windows nt.*win64|macintosh.*mac os x 1[4-9]/i.test(lower)) return 2;
122
+
123
+ // Default to low-mid -- conservative
124
+ return 1;
125
+ }
126
+
127
+ /**
128
+ * Map the ECT (effective connection type) string to a normalised form.
129
+ */
130
+ function normaliseECT(ect: string | undefined): string {
131
+ if (!ect) return '4g';
132
+ const lower = ect.toLowerCase().trim();
133
+ if (['slow-2g', '2g', '3g', '4g'].includes(lower)) return lower;
134
+ return '4g';
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Accept-CH / Critical-CH header values
139
+ // ---------------------------------------------------------------------------
140
+
141
+ const ALL_HINTS = [
142
+ 'Sec-CH-Device-Memory',
143
+ 'Sec-CH-DPR',
144
+ 'Sec-CH-Viewport-Width',
145
+ 'Sec-CH-Viewport-Height',
146
+ 'Sec-CH-Prefers-Reduced-Motion',
147
+ 'Sec-CH-Prefers-Color-Scheme',
148
+ 'Sec-CH-UA-Mobile',
149
+ 'Sec-CH-UA',
150
+ 'Sec-CH-UA-Platform',
151
+ 'Save-Data',
152
+ 'Downlink',
153
+ 'ECT',
154
+ 'RTT',
155
+ ] as const;
156
+
157
+ const CRITICAL_HINTS = [
158
+ 'Sec-CH-Prefers-Reduced-Motion',
159
+ 'Sec-CH-Prefers-Color-Scheme',
160
+ 'Sec-CH-UA-Mobile',
161
+ 'Sec-CH-Device-Memory',
162
+ ] as const;
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Public API -- namespace object pattern
166
+ // ---------------------------------------------------------------------------
167
+
168
+ /**
169
+ * Parse Client Hints headers into an {@link ExtendedDeviceCapabilities} structure.
170
+ *
171
+ * For properties that cannot be determined from headers (GPU tier, WebGPU
172
+ * support, CPU cores), conservative defaults are used.
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * import { ClientHints } from '@czap/edge';
177
+ *
178
+ * const caps = ClientHints.parseClientHints({
179
+ * 'sec-ch-device-memory': '8',
180
+ * 'sec-ch-dpr': '2',
181
+ * 'sec-ch-viewport-width': '1440',
182
+ * 'sec-ch-prefers-color-scheme': 'dark',
183
+ * 'sec-ch-ua-mobile': '?0',
184
+ * });
185
+ * console.log(caps.memory); // 8
186
+ * console.log(caps.devicePixelRatio); // 2
187
+ * console.log(caps.prefersColorScheme); // 'dark'
188
+ * ```
189
+ *
190
+ * @param headers - Client Hints headers (plain object or Web API Headers)
191
+ * @returns An {@link ExtendedDeviceCapabilities} structure
192
+ */
193
+ function parseClientHints(headers: ClientHintsHeaders | Headers): ExtendedDeviceCapabilities {
194
+ const get = headerGetter(headers);
195
+
196
+ // Memory
197
+ const rawMemory = parseFloat_(get, 'sec-ch-device-memory');
198
+ const memory = rawMemory !== undefined ? clampMemory(rawMemory) : 4;
199
+
200
+ // DPR
201
+ const dpr = parseFloat_(get, 'sec-ch-dpr') ?? 1;
202
+
203
+ // Viewport
204
+ const viewportWidth = parseFloat_(get, 'sec-ch-viewport-width') ?? 1920;
205
+ const viewportHeight = parseFloat_(get, 'sec-ch-viewport-height') ?? 1080;
206
+
207
+ // Preferences
208
+ const reducedMotionRaw = get('sec-ch-prefers-reduced-motion');
209
+ const prefersReducedMotion = reducedMotionRaw === 'reduce' || reducedMotionRaw === '"reduce"';
210
+
211
+ const colorSchemeRaw = get('sec-ch-prefers-color-scheme');
212
+ const prefersColorScheme: 'light' | 'dark' =
213
+ colorSchemeRaw === 'dark' || colorSchemeRaw === '"dark"' ? 'dark' : 'light';
214
+
215
+ // Touch (mobile hint)
216
+ const mobileRaw = get('sec-ch-ua-mobile');
217
+ const touchPrimary = mobileRaw === '?1' || mobileRaw === 'true';
218
+
219
+ // Save-Data
220
+ const saveDataRaw = get('save-data');
221
+ const saveData = saveDataRaw === 'on' || saveDataRaw === '1' || saveDataRaw === 'true';
222
+
223
+ // Network
224
+ const downlink = parseFloat_(get, 'downlink') ?? 10;
225
+ const ect = normaliseECT(get('ect'));
226
+
227
+ // GPU tier heuristic from UA
228
+ const gpu = gpuTierFromUA(get('user-agent'));
229
+
230
+ return {
231
+ // Base DeviceCapabilities
232
+ gpu,
233
+ cores: 4, // Conservative default -- not available via Client Hints
234
+ memory,
235
+ webgpu: false, // Cannot determine from headers
236
+ touchPrimary,
237
+ prefersReducedMotion,
238
+ prefersColorScheme,
239
+ viewportWidth,
240
+ viewportHeight,
241
+ devicePixelRatio: dpr,
242
+ connection: {
243
+ effectiveType: ect,
244
+ downlink,
245
+ saveData,
246
+ },
247
+
248
+ // Extended properties -- conservative defaults for edge
249
+ prefersContrast: 'no-preference',
250
+ forcedColors: false,
251
+ prefersReducedTransparency: false,
252
+ dynamicRange: 'standard',
253
+ colorGamut: 'srgb',
254
+ updateRate: 'fast',
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Generate the `Accept-CH` header value for requesting all useful Client Hints
260
+ * on subsequent requests.
261
+ *
262
+ * @example
263
+ * ```ts
264
+ * import { ClientHints } from '@czap/edge';
265
+ *
266
+ * const response = new Response('OK', {
267
+ * headers: { 'Accept-CH': ClientHints.acceptCHHeader() },
268
+ * });
269
+ * ```
270
+ *
271
+ * @returns A comma-separated list of Client Hint header names
272
+ */
273
+ function acceptCHHeader(): string {
274
+ return ALL_HINTS.join(', ');
275
+ }
276
+
277
+ /**
278
+ * Generate the `Critical-CH` header value for hints needed on the very first
279
+ * request (triggers a browser retry if missing).
280
+ *
281
+ * @example
282
+ * ```ts
283
+ * import { ClientHints } from '@czap/edge';
284
+ *
285
+ * const response = new Response('OK', {
286
+ * headers: {
287
+ * 'Accept-CH': ClientHints.acceptCHHeader(),
288
+ * 'Critical-CH': ClientHints.criticalCHHeader(),
289
+ * },
290
+ * });
291
+ * ```
292
+ *
293
+ * @returns A comma-separated list of critical Client Hint header names
294
+ */
295
+ function criticalCHHeader(): string {
296
+ return CRITICAL_HINTS.join(', ');
297
+ }
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // Namespace export
301
+ // ---------------------------------------------------------------------------
302
+
303
+ /**
304
+ * Client Hints namespace.
305
+ *
306
+ * Parses HTTP Client Hints headers into the same
307
+ * {@link ExtendedDeviceCapabilities} structure used by `@czap/detect`,
308
+ * enabling server-side / edge-side tier mapping without browser APIs.
309
+ * Also generates the `Accept-CH` and `Critical-CH` response headers needed
310
+ * to request hints from the browser.
311
+ *
312
+ * @example
313
+ * ```ts
314
+ * import { ClientHints } from '@czap/edge';
315
+ *
316
+ * // In an edge handler:
317
+ * const caps = ClientHints.parseClientHints(request.headers);
318
+ * const response = new Response(body, {
319
+ * headers: {
320
+ * 'Accept-CH': ClientHints.acceptCHHeader(),
321
+ * 'Critical-CH': ClientHints.criticalCHHeader(),
322
+ * },
323
+ * });
324
+ * ```
325
+ */
326
+ export const ClientHints = {
327
+ /** Parse Client Hints headers into {@link ExtendedDeviceCapabilities}. */
328
+ parseClientHints,
329
+ /** Produce the `Accept-CH` response header value listing all useful hints. */
330
+ acceptCHHeader,
331
+ /** Produce the `Critical-CH` response header value listing boot-required hints. */
332
+ criticalCHHeader,
333
+ } as const;
334
+
335
+ export declare namespace ClientHints {
336
+ /** Alias for {@link ClientHintsHeaders} — plain-object header bag shape. */
337
+ export type Headers = ClientHintsHeaders;
338
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Edge-side tier detection -- wraps the pure tier mapping functions from
3
+ * `@czap/detect` for use with HTTP Client Hints headers at the edge.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ import type { CapLevel } from '@czap/core';
9
+ import { tierFromCapabilities, motionTierFromCapabilities, designTierFromCapabilities } from '@czap/detect';
10
+ import type { DesignTier, MotionTier } from '@czap/detect';
11
+ import { ClientHints } from './client-hints.js';
12
+ import type { ClientHintsHeaders } from './client-hints.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Types
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /**
19
+ * Outcome of an edge-side tier detection sweep.
20
+ *
21
+ * All three fields use the same branded tier types as the client runtime,
22
+ * so downstream boundary evaluation and output gating reuse the exact
23
+ * code paths from `@czap/detect`.
24
+ */
25
+ export interface EdgeTierResult {
26
+ /** Highest {@link CapLevel} the device qualifies for. */
27
+ readonly capLevel: CapLevel;
28
+ /** Motion complexity tier permitted for this device. */
29
+ readonly motionTier: MotionTier;
30
+ /** Visual fidelity tier permitted for this device. */
31
+ readonly designTier: DesignTier;
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Public API
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /**
39
+ * Detect capability tiers from HTTP headers using Client Hints parsing
40
+ * and the same pure tier mapping functions used on the client.
41
+ */
42
+ function detectTier(headers: Headers | ClientHintsHeaders): EdgeTierResult {
43
+ const caps = ClientHints.parseClientHints(headers);
44
+ const capLevel = tierFromCapabilities(caps);
45
+ const motionTier = motionTierFromCapabilities(caps);
46
+ const designTier = designTierFromCapabilities(caps);
47
+ return { capLevel, motionTier, designTier };
48
+ }
49
+
50
+ /**
51
+ * Generate HTML data attribute string for injection into the `<html>` element.
52
+ *
53
+ * @example
54
+ * ```
55
+ * tierDataAttributes(result)
56
+ * // => 'data-czap-cap="reactive" data-czap-motion="animations" data-czap-design="enhanced"'
57
+ * ```
58
+ */
59
+ function tierDataAttributes(result: EdgeTierResult): string {
60
+ return `data-czap-cap="${result.capLevel}" data-czap-motion="${result.motionTier}" data-czap-design="${result.designTier}"`;
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Namespace export
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Edge tier detection namespace.
69
+ *
70
+ * Pairs {@link ClientHints.parseClientHints} with the pure tier-mapping
71
+ * functions from `@czap/detect` so the edge and the browser produce the
72
+ * same `capLevel`/`motionTier`/`designTier` triple for a given device.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * import { EdgeTier } from '@czap/edge';
77
+ *
78
+ * const result = EdgeTier.detectTier(request.headers);
79
+ * const html = `<html ${EdgeTier.tierDataAttributes(result)}>`;
80
+ * // `<html data-czap-cap="reactive" data-czap-motion="animations" data-czap-design="enhanced">`
81
+ * ```
82
+ */
83
+ export const EdgeTier = {
84
+ /** Detect {@link EdgeTierResult} from a `Headers`-like bag. */
85
+ detectTier,
86
+ /** Render an `EdgeTierResult` into `data-czap-*` attributes for the root HTML element. */
87
+ tierDataAttributes,
88
+ } as const;
89
+
90
+ export declare namespace EdgeTier {
91
+ /** Alias for {@link EdgeTierResult}. */
92
+ export type Result = EdgeTierResult;
93
+ }