@ressjs/platform 0.6.0-experimental.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 (46) hide show
  1. package/README.md +95 -0
  2. package/dist/client.d.ts +32 -0
  3. package/dist/client.js +59 -0
  4. package/dist/compat.d.ts +9 -0
  5. package/dist/compat.js +13 -0
  6. package/dist/detect/layers.d.ts +40 -0
  7. package/dist/detect/layers.js +34 -0
  8. package/dist/detect/merge.d.ts +30 -0
  9. package/dist/detect/merge.js +50 -0
  10. package/dist/detect/override.d.ts +9 -0
  11. package/dist/detect/override.js +33 -0
  12. package/dist/detect/pipeline.d.ts +10 -0
  13. package/dist/detect/pipeline.js +75 -0
  14. package/dist/index.d.ts +26 -0
  15. package/dist/index.js +51 -0
  16. package/dist/model/defaults.d.ts +8 -0
  17. package/dist/model/defaults.js +25 -0
  18. package/dist/model/known.d.ts +31 -0
  19. package/dist/model/known.js +9 -0
  20. package/dist/model/types.d.ts +95 -0
  21. package/dist/model/types.js +2 -0
  22. package/dist/predicates.d.ts +9 -0
  23. package/dist/predicates.js +15 -0
  24. package/dist/registry/create.d.ts +17 -0
  25. package/dist/registry/create.js +87 -0
  26. package/dist/registry/defaults.d.ts +23 -0
  27. package/dist/registry/defaults.js +63 -0
  28. package/dist/registry/index.d.ts +11 -0
  29. package/dist/registry/index.js +20 -0
  30. package/dist/registry/parse.d.ts +14 -0
  31. package/dist/registry/parse.js +89 -0
  32. package/dist/registry/types.d.ts +91 -0
  33. package/dist/registry/types.js +2 -0
  34. package/dist/rules/apply.d.ts +13 -0
  35. package/dist/rules/apply.js +40 -0
  36. package/dist/rules/edge.d.ts +44 -0
  37. package/dist/rules/edge.js +66 -0
  38. package/dist/rules/headers.d.ts +48 -0
  39. package/dist/rules/headers.js +157 -0
  40. package/dist/rules/request-headers.d.ts +61 -0
  41. package/dist/rules/request-headers.js +113 -0
  42. package/dist/rules/user-agent.d.ts +25 -0
  43. package/dist/rules/user-agent.js +212 -0
  44. package/dist/serialize.d.ts +12 -0
  45. package/dist/serialize.js +23 -0
  46. package/package.json +48 -0
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EDGE_PRESETS = void 0;
4
+ exports.readEdgeHeaders = readEdgeHeaders;
5
+ const headers_1 = require("./headers");
6
+ /**
7
+ * Definiciones listas para redes conocidas.
8
+ *
9
+ * Ninguna está activa por defecto: el proyecto declara la suya.
10
+ *
11
+ * ```ts
12
+ * createPlatformRegistry({ edge: EDGE_PRESETS.cloudflare })
13
+ * ```
14
+ *
15
+ * No hace falta un preset para funcionar. Cualquier red que sepa reescribir
16
+ * cabeceras —Workers, Lambda@Edge, VCL, EdgeWorkers— puede setear
17
+ * `x-ressjs-device` directamente, que el framework ya lee sin configurar nada y
18
+ * sin atarse a nadie.
19
+ */
20
+ exports.EDGE_PRESETS = {
21
+ cloudflare: [
22
+ {
23
+ header: 'cf-device-type',
24
+ axis: 'device',
25
+ map: { mobile: 'phone', tablet: 'tablet', desktop: 'desktop' },
26
+ },
27
+ ],
28
+ cloudfront: [
29
+ { header: 'cloudfront-is-smarttv-viewer', axis: 'device', map: { true: 'tv' } },
30
+ { header: 'cloudfront-is-tablet-viewer', axis: 'device', map: { true: 'tablet' } },
31
+ { header: 'cloudfront-is-mobile-viewer', axis: 'device', map: { true: 'phone' } },
32
+ { header: 'cloudfront-is-desktop-viewer', axis: 'device', map: { true: 'desktop' } },
33
+ ],
34
+ akamai: [
35
+ {
36
+ header: 'x-akamai-device-type',
37
+ axis: 'device',
38
+ map: { mobile: 'phone', tablet: 'tablet', desktop: 'desktop', tv: 'tv' },
39
+ },
40
+ ],
41
+ };
42
+ /**
43
+ * Lee lo que la red ya resolvió, según las reglas que el proyecto declaró.
44
+ *
45
+ * El orden de las reglas decide: la primera que aporta un eje lo fija, igual que
46
+ * en el resto del pipeline.
47
+ */
48
+ function readEdgeHeaders(headers, rules) {
49
+ const result = {};
50
+ for (const rule of rules) {
51
+ if (result[rule.axis])
52
+ continue;
53
+ const raw = (0, headers_1.header)(headers, rule.header)?.toLowerCase();
54
+ if (!raw)
55
+ continue;
56
+ const value = rule.map[raw];
57
+ if (!value)
58
+ continue;
59
+ result[rule.axis] = {
60
+ value,
61
+ confidence: rule.confidence ?? 'high',
62
+ rule: `edge:${rule.header}`,
63
+ };
64
+ }
65
+ return result;
66
+ }
@@ -0,0 +1,48 @@
1
+ import type { Confidence, NativeAppInfo } from '../model/types';
2
+ /** Cabeceras de una petición, tal como las entrega un servidor HTTP. */
3
+ export type Headers = Record<string, string | string[] | undefined>;
4
+ /** Lo que una capa afirma, con la confianza de cada afirmación. */
5
+ export interface LayerResult {
6
+ webview?: {
7
+ value: boolean;
8
+ confidence: Confidence;
9
+ rule: string;
10
+ };
11
+ os?: {
12
+ value: string;
13
+ confidence: Confidence;
14
+ rule: string;
15
+ };
16
+ device?: {
17
+ value: string;
18
+ confidence: Confidence;
19
+ rule: string;
20
+ };
21
+ nativeApp?: Partial<NativeAppInfo>;
22
+ capabilities?: readonly string[];
23
+ }
24
+ /** Lee una cabecera colapsando el caso de valores repetidos. */
25
+ export declare function header(headers: Headers, name: string): string | undefined;
26
+ /**
27
+ * Contrato v2: el cliente declara lo que es.
28
+ *
29
+ * Cada cabecera aporta **sólo** el campo que nombra. Ninguna completa un campo
30
+ * que el cliente no declaró — eso es lo que hacía que una tableta con React
31
+ * Native se sirviera como teléfono.
32
+ */
33
+ export declare function readExplicitHeaders(headers: Headers): LayerResult;
34
+ /**
35
+ * Contrato v1 y cabeceras de proveedor.
36
+ *
37
+ * Los clientes ya publicados mandan estas cabeceras y no se pueden actualizar de
38
+ * golpe, así que se traducen. La tabla es declarada a propósito: es lo que
39
+ * permite verificar que ninguna entrada aporta un campo que el cliente no dijo.
40
+ */
41
+ export declare function readLegacyHeaders(headers: Headers): LayerResult;
42
+ /**
43
+ * Pistas de plataforma que el navegador manda por su cuenta.
44
+ *
45
+ * `sec-ch-ua-mobile: ?0` no aporta nada: cubre escritorio, tableta y televisor
46
+ * por igual, así que afirmar `desktop` con eso sería inventar.
47
+ */
48
+ export declare function readClientHints(headers: Headers): LayerResult;
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.header = header;
4
+ exports.readExplicitHeaders = readExplicitHeaders;
5
+ exports.readLegacyHeaders = readLegacyHeaders;
6
+ exports.readClientHints = readClientHints;
7
+ /** Lee una cabecera colapsando el caso de valores repetidos. */
8
+ function header(headers, name) {
9
+ const raw = headers[name] ?? headers[name.toLowerCase()];
10
+ const value = Array.isArray(raw) ? raw[0] : raw;
11
+ const trimmed = value?.trim();
12
+ return trimmed ? trimmed : undefined;
13
+ }
14
+ const isTrue = (v) => v === 'true' || v === '1' || v === 'yes';
15
+ const isFalse = (v) => v === 'false' || v === '0' || v === 'no';
16
+ /**
17
+ * Contrato v2: el cliente declara lo que es.
18
+ *
19
+ * Cada cabecera aporta **sólo** el campo que nombra. Ninguna completa un campo
20
+ * que el cliente no declaró — eso es lo que hacía que una tableta con React
21
+ * Native se sirviera como teléfono.
22
+ */
23
+ function readExplicitHeaders(headers) {
24
+ const result = {};
25
+ const webview = header(headers, 'x-ressjs-webview');
26
+ if (webview && (isTrue(webview) || isFalse(webview))) {
27
+ result.webview = {
28
+ value: isTrue(webview),
29
+ confidence: 'exact',
30
+ rule: 'h2:x-ressjs-webview',
31
+ };
32
+ }
33
+ const os = header(headers, 'x-ressjs-os');
34
+ if (os)
35
+ result.os = { value: os.toLowerCase(), confidence: 'exact', rule: 'h2:x-ressjs-os' };
36
+ const device = header(headers, 'x-ressjs-device');
37
+ if (device) {
38
+ result.device = {
39
+ value: device.toLowerCase(),
40
+ confidence: 'exact',
41
+ rule: 'h2:x-ressjs-device',
42
+ };
43
+ }
44
+ const appId = header(headers, 'x-ressjs-app-id');
45
+ const appVersion = header(headers, 'x-ressjs-app-version');
46
+ const osVersion = header(headers, 'x-ressjs-os-version');
47
+ if (appId || appVersion || osVersion) {
48
+ result.nativeApp = { id: appId, version: appVersion, osVersion };
49
+ // Una app que se identifica está, por definición, embebiendo la página.
50
+ if (appId && !result.webview) {
51
+ result.webview = { value: true, confidence: 'exact', rule: 'h2:x-ressjs-app-id' };
52
+ }
53
+ }
54
+ const capabilities = header(headers, 'x-ressjs-capabilities');
55
+ if (capabilities) {
56
+ result.capabilities = capabilities
57
+ .split(',')
58
+ .map((c) => c.trim())
59
+ .filter(Boolean);
60
+ }
61
+ return result;
62
+ }
63
+ /**
64
+ * Contrato v1 y cabeceras de proveedor.
65
+ *
66
+ * Los clientes ya publicados mandan estas cabeceras y no se pueden actualizar de
67
+ * golpe, así que se traducen. La tabla es declarada a propósito: es lo que
68
+ * permite verificar que ninguna entrada aporta un campo que el cliente no dijo.
69
+ */
70
+ function readLegacyHeaders(headers) {
71
+ const result = {};
72
+ const nativeApp = {};
73
+ const platform = header(headers, 'x-ressjs-platform')?.toLowerCase();
74
+ if (platform === 'webview' || platform === 'native') {
75
+ result.webview = { value: true, confidence: 'high', rule: 'h1:x-ressjs-platform' };
76
+ }
77
+ const webview = header(headers, 'x-webview');
78
+ if (!result.webview && webview && isTrue(webview)) {
79
+ result.webview = { value: true, confidence: 'high', rule: 'h1:x-webview' };
80
+ }
81
+ const rn = header(headers, 'x-rn-platform')?.toLowerCase();
82
+ if (rn) {
83
+ result.os = { value: rn, confidence: 'high', rule: 'h1:x-rn-platform' };
84
+ if (!result.webview) {
85
+ result.webview = { value: true, confidence: 'high', rule: 'h1:x-rn-platform' };
86
+ }
87
+ nativeApp.id ??= 'react-native';
88
+ }
89
+ const os = header(headers, 'x-ressjs-os')?.toLowerCase();
90
+ if (os && !result.os)
91
+ result.os = { value: os, confidence: 'high', rule: 'h1:x-ressjs-os' };
92
+ const generic = header(headers, 'x-platform')?.toLowerCase();
93
+ if (generic && !result.os) {
94
+ result.os = { value: generic, confidence: 'medium', rule: 'h1:x-platform' };
95
+ }
96
+ const device = header(headers, 'x-ressjs-device')?.toLowerCase();
97
+ if (device) {
98
+ result.device = { value: device, confidence: 'high', rule: 'h1:x-ressjs-device' };
99
+ }
100
+ const requestedWith = header(headers, 'x-requested-with');
101
+ if (requestedWith && !result.webview) {
102
+ result.webview = { value: true, confidence: 'medium', rule: 'h1:x-requested-with' };
103
+ result.os ??= { value: 'android', confidence: 'medium', rule: 'h1:x-requested-with' };
104
+ }
105
+ const osVersion = header(headers, 'x-ressjs-version');
106
+ if (osVersion)
107
+ nativeApp.osVersion ??= osVersion;
108
+ const appVersion = header(headers, 'x-ressjs-app-version') ?? header(headers, 'x-rn-version');
109
+ if (appVersion)
110
+ nativeApp.version ??= appVersion;
111
+ if (Object.keys(nativeApp).length > 0)
112
+ result.nativeApp = nativeApp;
113
+ return result;
114
+ }
115
+ /**
116
+ * Pistas de plataforma que el navegador manda por su cuenta.
117
+ *
118
+ * `sec-ch-ua-mobile: ?0` no aporta nada: cubre escritorio, tableta y televisor
119
+ * por igual, así que afirmar `desktop` con eso sería inventar.
120
+ */
121
+ function readClientHints(headers) {
122
+ const result = {};
123
+ const platform = header(headers, 'sec-ch-ua-platform')?.replace(/"/g, '').toLowerCase();
124
+ const os = CLIENT_HINT_OS[platform ?? ''];
125
+ if (os)
126
+ result.os = { value: os, confidence: 'high', rule: 'ch:sec-ch-ua-platform' };
127
+ const formFactors = header(headers, 'sec-ch-ua-form-factors')?.toLowerCase();
128
+ if (formFactors) {
129
+ const device = /tv/.test(formFactors)
130
+ ? 'tv'
131
+ : /tablet/.test(formFactors)
132
+ ? 'tablet'
133
+ : /mobile/.test(formFactors)
134
+ ? 'phone'
135
+ : /desktop/.test(formFactors)
136
+ ? 'desktop'
137
+ : undefined;
138
+ if (device) {
139
+ result.device = { value: device, confidence: 'high', rule: 'ch:sec-ch-ua-form-factors' };
140
+ }
141
+ }
142
+ const mobile = header(headers, 'sec-ch-ua-mobile');
143
+ if (!result.device && mobile === '?1') {
144
+ // El hint no distingue teléfono de tableta; sólo afirma que es chico.
145
+ result.device = { value: 'phone', confidence: 'medium', rule: 'ch:sec-ch-ua-mobile' };
146
+ }
147
+ return result;
148
+ }
149
+ const CLIENT_HINT_OS = {
150
+ android: 'android',
151
+ ios: 'ios',
152
+ windows: 'windows',
153
+ macos: 'macos',
154
+ linux: 'linux',
155
+ 'chrome os': 'linux',
156
+ chromeos: 'linux',
157
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Las cabeceras que la detección lee.
3
+ *
4
+ * Existe una sola lista y de ella salen dos cosas: lo que el pipeline consulta y
5
+ * lo que la respuesta declara en `Vary`. Que sean la misma es lo que impide que
6
+ * se desincronicen la próxima vez que se agregue una cabecera — y una respuesta
7
+ * que varía por una cabecera sin declararla es servida por un CDN al cliente
8
+ * equivocado.
9
+ */
10
+ /** Contrato v2: el cliente declara lo que es. */
11
+ export declare const HEADERS_V2: readonly ["x-ressjs-webview", "x-ressjs-os", "x-ressjs-device", "x-ressjs-app-id", "x-ressjs-app-version", "x-ressjs-os-version", "x-ressjs-capabilities"];
12
+ /** Contrato v1 y cabeceras de proveedor, que se siguen aceptando. */
13
+ export declare const HEADERS_LEGACY: readonly ["x-ressjs-platform", "x-ressjs-version", "x-webview", "x-rn-platform", "x-rn-version", "x-platform", "x-requested-with"];
14
+ /** Pistas que el navegador manda por su cuenta. */
15
+ export declare const HEADERS_CLIENT_HINTS: readonly ["sec-ch-ua-platform", "sec-ch-ua-form-factors", "sec-ch-ua-mobile"];
16
+ /** Forzado de plataforma para depurar. Sólo se lee si está habilitado. */
17
+ export declare const HEADER_OVERRIDE = "x-ress-force-platform";
18
+ /** Todo lo que la detección puede llegar a leer de una petición. */
19
+ export declare const PLATFORM_REQUEST_HEADERS: readonly ["x-ressjs-webview", "x-ressjs-os", "x-ressjs-device", "x-ressjs-app-id", "x-ressjs-app-version", "x-ressjs-os-version", "x-ressjs-capabilities", "x-ressjs-platform", "x-ressjs-version", "x-webview", "x-rn-platform", "x-rn-version", "x-platform", "x-requested-with", "sec-ch-ua-platform", "sec-ch-ua-form-factors", "sec-ch-ua-mobile"];
20
+ /**
21
+ * Lo que se declara en `Vary` cuando la detección está activa.
22
+ *
23
+ * `User-Agent` va primero porque es el de mayor cardinalidad y el que un CDN va
24
+ * a querer normalizar en el borde.
25
+ */
26
+ export declare const PLATFORM_VARY_HEADERS: readonly ["User-Agent", "x-ressjs-webview", "x-ressjs-os", "x-ressjs-device", "x-ressjs-app-id", "x-ressjs-app-version", "x-ressjs-os-version", "x-ressjs-capabilities", "x-ressjs-platform", "x-ressjs-version", "x-webview", "x-rn-platform", "x-rn-version", "x-platform", "x-requested-with", "sec-ch-ua-platform", "sec-ch-ua-form-factors", "sec-ch-ua-mobile"];
27
+ /**
28
+ * Qué cabeceras alimentan cada eje.
29
+ *
30
+ * Sirve para declarar en `Vary` sólo lo que puede cambiar **esta** respuesta. Una
31
+ * página que no tiene variantes no depende de ninguna; una que sólo distingue
32
+ * televisores depende de las de `device` y de nada más.
33
+ *
34
+ * Declarar las dieciocho en toda respuesta es correcto pero caro: cada campo
35
+ * multiplica las entradas que una caché intermedia tiene que guardar, y hay
36
+ * proveedores que directamente ignoran un `Vary` largo.
37
+ */
38
+ export declare const HEADERS_BY_AXIS: {
39
+ readonly context: readonly ["x-ressjs-webview", "x-ressjs-app-id", "x-ressjs-platform", "x-webview", "x-rn-platform", "x-requested-with"];
40
+ readonly os: readonly ["x-ressjs-os", "x-rn-platform", "x-platform", "x-ressjs-platform", "sec-ch-ua-platform"];
41
+ readonly device: readonly ["x-ressjs-device", "sec-ch-ua-form-factors", "sec-ch-ua-mobile"];
42
+ };
43
+ /**
44
+ * Las cabeceras a declarar para una respuesta que varía por estos ejes.
45
+ *
46
+ * `User-Agent` afecta a los tres y se lista primero: es el de mayor cardinalidad
47
+ * y el que conviene normalizar en el borde. Sin ejes, la lista es vacía — y una
48
+ * respuesta que no varía por nada no debe declarar `Vary`.
49
+ */
50
+ export declare function varyHeadersForAxes(axes: readonly ('context' | 'os' | 'device')[], options?: {
51
+ includeUserAgent?: boolean;
52
+ /**
53
+ * Cabeceras que el proyecto declaró para su red de distribución. Se suman
54
+ * porque también cambian la respuesta; si no declaró ninguna, no aparece
55
+ * ninguna, que es lo que evita declarar campos que nadie manda.
56
+ */
57
+ edge?: readonly {
58
+ header: string;
59
+ axis: string;
60
+ }[];
61
+ }): string[];
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ /**
3
+ * Las cabeceras que la detección lee.
4
+ *
5
+ * Existe una sola lista y de ella salen dos cosas: lo que el pipeline consulta y
6
+ * lo que la respuesta declara en `Vary`. Que sean la misma es lo que impide que
7
+ * se desincronicen la próxima vez que se agregue una cabecera — y una respuesta
8
+ * que varía por una cabecera sin declararla es servida por un CDN al cliente
9
+ * equivocado.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.HEADERS_BY_AXIS = exports.PLATFORM_VARY_HEADERS = exports.PLATFORM_REQUEST_HEADERS = exports.HEADER_OVERRIDE = exports.HEADERS_CLIENT_HINTS = exports.HEADERS_LEGACY = exports.HEADERS_V2 = void 0;
13
+ exports.varyHeadersForAxes = varyHeadersForAxes;
14
+ /** Contrato v2: el cliente declara lo que es. */
15
+ exports.HEADERS_V2 = [
16
+ 'x-ressjs-webview',
17
+ 'x-ressjs-os',
18
+ 'x-ressjs-device',
19
+ 'x-ressjs-app-id',
20
+ 'x-ressjs-app-version',
21
+ 'x-ressjs-os-version',
22
+ 'x-ressjs-capabilities',
23
+ ];
24
+ /** Contrato v1 y cabeceras de proveedor, que se siguen aceptando. */
25
+ exports.HEADERS_LEGACY = [
26
+ 'x-ressjs-platform',
27
+ 'x-ressjs-version',
28
+ 'x-webview',
29
+ 'x-rn-platform',
30
+ 'x-rn-version',
31
+ 'x-platform',
32
+ 'x-requested-with',
33
+ ];
34
+ /** Pistas que el navegador manda por su cuenta. */
35
+ exports.HEADERS_CLIENT_HINTS = [
36
+ 'sec-ch-ua-platform',
37
+ 'sec-ch-ua-form-factors',
38
+ 'sec-ch-ua-mobile',
39
+ ];
40
+ /** Forzado de plataforma para depurar. Sólo se lee si está habilitado. */
41
+ exports.HEADER_OVERRIDE = 'x-ress-force-platform';
42
+ /** Todo lo que la detección puede llegar a leer de una petición. */
43
+ exports.PLATFORM_REQUEST_HEADERS = [
44
+ ...exports.HEADERS_V2,
45
+ ...exports.HEADERS_LEGACY,
46
+ ...exports.HEADERS_CLIENT_HINTS,
47
+ ];
48
+ /**
49
+ * Lo que se declara en `Vary` cuando la detección está activa.
50
+ *
51
+ * `User-Agent` va primero porque es el de mayor cardinalidad y el que un CDN va
52
+ * a querer normalizar en el borde.
53
+ */
54
+ exports.PLATFORM_VARY_HEADERS = [
55
+ 'User-Agent',
56
+ ...exports.PLATFORM_REQUEST_HEADERS,
57
+ ];
58
+ /**
59
+ * Qué cabeceras alimentan cada eje.
60
+ *
61
+ * Sirve para declarar en `Vary` sólo lo que puede cambiar **esta** respuesta. Una
62
+ * página que no tiene variantes no depende de ninguna; una que sólo distingue
63
+ * televisores depende de las de `device` y de nada más.
64
+ *
65
+ * Declarar las dieciocho en toda respuesta es correcto pero caro: cada campo
66
+ * multiplica las entradas que una caché intermedia tiene que guardar, y hay
67
+ * proveedores que directamente ignoran un `Vary` largo.
68
+ */
69
+ exports.HEADERS_BY_AXIS = {
70
+ context: [
71
+ 'x-ressjs-webview',
72
+ 'x-ressjs-app-id',
73
+ 'x-ressjs-platform',
74
+ 'x-webview',
75
+ 'x-rn-platform',
76
+ 'x-requested-with',
77
+ ],
78
+ os: [
79
+ 'x-ressjs-os',
80
+ 'x-rn-platform',
81
+ 'x-platform',
82
+ 'x-ressjs-platform',
83
+ 'sec-ch-ua-platform',
84
+ ],
85
+ device: [
86
+ 'x-ressjs-device',
87
+ 'sec-ch-ua-form-factors',
88
+ 'sec-ch-ua-mobile',
89
+ ],
90
+ };
91
+ /**
92
+ * Las cabeceras a declarar para una respuesta que varía por estos ejes.
93
+ *
94
+ * `User-Agent` afecta a los tres y se lista primero: es el de mayor cardinalidad
95
+ * y el que conviene normalizar en el borde. Sin ejes, la lista es vacía — y una
96
+ * respuesta que no varía por nada no debe declarar `Vary`.
97
+ */
98
+ function varyHeadersForAxes(axes, options = {}) {
99
+ if (axes.length === 0)
100
+ return [];
101
+ const fields = new Set();
102
+ if (options.includeUserAgent ?? true)
103
+ fields.add('User-Agent');
104
+ for (const axis of axes) {
105
+ for (const header of exports.HEADERS_BY_AXIS[axis])
106
+ fields.add(header);
107
+ }
108
+ for (const rule of options.edge ?? []) {
109
+ if (axes.includes(rule.axis))
110
+ fields.add(rule.header);
111
+ }
112
+ return [...fields];
113
+ }
@@ -0,0 +1,25 @@
1
+ import type { UserAgentRule } from '../registry/types';
2
+ /**
3
+ * Reglas de detección por User-Agent.
4
+ *
5
+ * Son datos, no condicionales: se leen de corrido, se auditan y un proyecto
6
+ * puede agregar las suyas sin tocar el código que las recorre.
7
+ *
8
+ * **El orden es normativo**, no un detalle de implementación. Los bloques van de
9
+ * lo específico a lo genérico, y esa es exactamente la propiedad que el modelo
10
+ * anterior no tenía: ahí la primera rama que matcheaba devolvía un resultado
11
+ * completo, así que un Android TV se resolvía como teléfono Android antes de que
12
+ * ninguna regla de televisor llegara a evaluarse.
13
+ *
14
+ * | Bloque | Qué reconoce |
15
+ * |---|---|
16
+ * | 100 | televisores y aparatos de streaming |
17
+ * | 200 | contenedores de escritorio |
18
+ * | 300 | páginas incrustadas en una app |
19
+ * | 400 | sistema operativo |
20
+ * | 500 | forma del aparato |
21
+ *
22
+ * Ninguna regla corta la evaluación: cada una aporta lo que sabe y las que
23
+ * siguen completan lo que falta. Un campo ya resuelto no se sobrescribe.
24
+ */
25
+ export declare const DEFAULT_RULES: readonly UserAgentRule[];
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_RULES = void 0;
4
+ /**
5
+ * Reglas de detección por User-Agent.
6
+ *
7
+ * Son datos, no condicionales: se leen de corrido, se auditan y un proyecto
8
+ * puede agregar las suyas sin tocar el código que las recorre.
9
+ *
10
+ * **El orden es normativo**, no un detalle de implementación. Los bloques van de
11
+ * lo específico a lo genérico, y esa es exactamente la propiedad que el modelo
12
+ * anterior no tenía: ahí la primera rama que matcheaba devolvía un resultado
13
+ * completo, así que un Android TV se resolvía como teléfono Android antes de que
14
+ * ninguna regla de televisor llegara a evaluarse.
15
+ *
16
+ * | Bloque | Qué reconoce |
17
+ * |---|---|
18
+ * | 100 | televisores y aparatos de streaming |
19
+ * | 200 | contenedores de escritorio |
20
+ * | 300 | páginas incrustadas en una app |
21
+ * | 400 | sistema operativo |
22
+ * | 500 | forma del aparato |
23
+ *
24
+ * Ninguna regla corta la evaluación: cada una aporta lo que sabe y las que
25
+ * siguen completan lo que falta. Un campo ya resuelto no se sobrescribe.
26
+ */
27
+ exports.DEFAULT_RULES = [
28
+ // ---------------------------------------------------------------- 100 · TV
29
+ // Van primero porque casi todos los televisores mienten: se presentan como
30
+ // Linux, como Android o como un navegador de escritorio.
31
+ {
32
+ name: 'ua:tizen',
33
+ test: /\bTizen\b/i,
34
+ set: { os: 'tizen', device: 'tv' },
35
+ confidence: 'high',
36
+ priority: 100,
37
+ },
38
+ {
39
+ name: 'ua:webos',
40
+ test: /\b(?:web0S|webOS)\b.*\b(?:TV|SmartTV|LargeScreen)\b|\bWebAppManager\b/i,
41
+ set: { os: 'webos', device: 'tv' },
42
+ confidence: 'high',
43
+ priority: 101,
44
+ },
45
+ {
46
+ name: 'ua:android-tv',
47
+ test: /\bAndroid\b.*\b(?:TV|BRAVIA|AFT[A-Z0-9]+|SHIELD|Chromecast|GoogleTV)\b/i,
48
+ set: { os: 'android', device: 'tv' },
49
+ confidence: 'high',
50
+ priority: 102,
51
+ },
52
+ {
53
+ name: 'ua:fire-tv',
54
+ test: /\bAFT[A-Z0-9]+\b/,
55
+ set: { os: 'android', device: 'tv' },
56
+ confidence: 'high',
57
+ priority: 103,
58
+ },
59
+ {
60
+ name: 'ua:chromecast',
61
+ test: /\bCrKey\b/,
62
+ set: { os: 'android', device: 'tv' },
63
+ confidence: 'high',
64
+ priority: 104,
65
+ },
66
+ {
67
+ name: 'ua:generic-smarttv',
68
+ test: /\b(?:SMART-TV|SmartTV|HbbTV|NetCast|Viera|AppleTV|Roku|PlayStation|Xbox)\b/i,
69
+ set: { device: 'tv' },
70
+ confidence: 'medium',
71
+ priority: 110,
72
+ },
73
+ // ------------------------------------------- 200 · contenedor de escritorio
74
+ // Una app de escritorio que muestra HTML embebe la página igual que un
75
+ // WebView de teléfono: la diferencia es el aparato, no el alojamiento.
76
+ {
77
+ name: 'ua:electron',
78
+ test: /\bElectron\/[\d.]+/,
79
+ set: { webview: true, device: 'desktop' },
80
+ confidence: 'high',
81
+ priority: 200,
82
+ },
83
+ {
84
+ name: 'ua:tauri',
85
+ test: /\bTauri\/[\d.]+/i,
86
+ set: { webview: true, device: 'desktop' },
87
+ confidence: 'high',
88
+ priority: 201,
89
+ },
90
+ // ---------------------------------------------- 300 · incrustada en una app
91
+ {
92
+ name: 'ua:react-native',
93
+ test: /\bReactNative\b/i,
94
+ set: { webview: true },
95
+ confidence: 'high',
96
+ priority: 300,
97
+ },
98
+ {
99
+ name: 'ua:android-webview',
100
+ test: /;\s*wv[);]/,
101
+ set: { webview: true, os: 'android' },
102
+ confidence: 'high',
103
+ priority: 301,
104
+ },
105
+ {
106
+ name: 'ua:android-webview-legacy',
107
+ test: /\bAndroid\b.*\bVersion\/[\d.]+\b.*\bChrome\//,
108
+ set: { webview: true, os: 'android' },
109
+ confidence: 'medium',
110
+ priority: 302,
111
+ },
112
+ {
113
+ // Un WKWebView omite el token `Safari` que Safari móvil sí trae. Es
114
+ // heurístico y por eso la confianza es media.
115
+ name: 'ua:ios-webview',
116
+ test: /\((?:iPhone|iPad|iPod);(?:(?!Safari).)*$/,
117
+ set: { webview: true, os: 'ios' },
118
+ confidence: 'medium',
119
+ priority: 303,
120
+ },
121
+ {
122
+ name: 'ua:inapp-browser',
123
+ test: /\b(?:FBAN|FBAV|Instagram|Line\/|Twitter|GSA\/|MicroMessenger)\b/,
124
+ set: { webview: true },
125
+ confidence: 'medium',
126
+ priority: 304,
127
+ },
128
+ // --------------------------------------------------------------- 400 · OS
129
+ {
130
+ // Sólo tokens delimitados: el modelo anterior buscaba la subcadena `ios`, y
131
+ // por eso cualquier User-Agent que dijera `axios` se detectaba como iOS.
132
+ name: 'ua:ios',
133
+ test: /\b(?:iPhone|iPad|iPod)\b|\biOS\b|\biPhone OS\b/,
134
+ set: { os: 'ios' },
135
+ confidence: 'high',
136
+ priority: 400,
137
+ },
138
+ {
139
+ name: 'ua:android',
140
+ test: /\bAndroid\b/,
141
+ set: { os: 'android' },
142
+ confidence: 'high',
143
+ priority: 401,
144
+ },
145
+ {
146
+ name: 'ua:windows',
147
+ test: /\bWindows NT\b|\bWindows\b/,
148
+ set: { os: 'windows' },
149
+ confidence: 'high',
150
+ priority: 402,
151
+ },
152
+ {
153
+ name: 'ua:macos',
154
+ test: /\bMac OS X\b|\bMacintosh\b/,
155
+ set: { os: 'macos' },
156
+ confidence: 'high',
157
+ priority: 403,
158
+ },
159
+ {
160
+ name: 'ua:linux',
161
+ test: /\b(?:Linux|X11|CrOS)\b/,
162
+ set: { os: 'linux' },
163
+ confidence: 'medium',
164
+ priority: 404,
165
+ },
166
+ // ----------------------------------------------------------- 500 · aparato
167
+ {
168
+ name: 'ua:ipad',
169
+ test: /\biPad\b/,
170
+ set: { device: 'tablet' },
171
+ confidence: 'high',
172
+ priority: 500,
173
+ },
174
+ {
175
+ name: 'ua:iphone',
176
+ test: /\b(?:iPhone|iPod)\b/,
177
+ set: { device: 'phone' },
178
+ confidence: 'high',
179
+ priority: 501,
180
+ },
181
+ {
182
+ // Android sin el token `Mobile` es una tableta. Es la convención que
183
+ // Google documenta, y es lo que hace que un Galaxy Tab deje de detectarse
184
+ // como un escritorio.
185
+ name: 'ua:android-tablet',
186
+ test: /\bAndroid\b(?:(?!\bMobile\b).)*$/,
187
+ set: { device: 'tablet' },
188
+ confidence: 'medium',
189
+ priority: 502,
190
+ },
191
+ {
192
+ name: 'ua:android-phone',
193
+ test: /\bAndroid\b.*\bMobile\b/,
194
+ set: { device: 'phone' },
195
+ confidence: 'high',
196
+ priority: 503,
197
+ },
198
+ {
199
+ name: 'ua:tablet-generic',
200
+ test: /\b(?:Tablet|Silk|Kindle|PlayBook)\b/i,
201
+ set: { device: 'tablet' },
202
+ confidence: 'medium',
203
+ priority: 510,
204
+ },
205
+ {
206
+ name: 'ua:mobile-generic',
207
+ test: /\b(?:Mobile|Mobi|Phone)\b/i,
208
+ set: { device: 'phone' },
209
+ confidence: 'low',
210
+ priority: 511,
211
+ },
212
+ ];