@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.
- package/README.md +95 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.js +59 -0
- package/dist/compat.d.ts +9 -0
- package/dist/compat.js +13 -0
- package/dist/detect/layers.d.ts +40 -0
- package/dist/detect/layers.js +34 -0
- package/dist/detect/merge.d.ts +30 -0
- package/dist/detect/merge.js +50 -0
- package/dist/detect/override.d.ts +9 -0
- package/dist/detect/override.js +33 -0
- package/dist/detect/pipeline.d.ts +10 -0
- package/dist/detect/pipeline.js +75 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +51 -0
- package/dist/model/defaults.d.ts +8 -0
- package/dist/model/defaults.js +25 -0
- package/dist/model/known.d.ts +31 -0
- package/dist/model/known.js +9 -0
- package/dist/model/types.d.ts +95 -0
- package/dist/model/types.js +2 -0
- package/dist/predicates.d.ts +9 -0
- package/dist/predicates.js +15 -0
- package/dist/registry/create.d.ts +17 -0
- package/dist/registry/create.js +87 -0
- package/dist/registry/defaults.d.ts +23 -0
- package/dist/registry/defaults.js +63 -0
- package/dist/registry/index.d.ts +11 -0
- package/dist/registry/index.js +20 -0
- package/dist/registry/parse.d.ts +14 -0
- package/dist/registry/parse.js +89 -0
- package/dist/registry/types.d.ts +91 -0
- package/dist/registry/types.js +2 -0
- package/dist/rules/apply.d.ts +13 -0
- package/dist/rules/apply.js +40 -0
- package/dist/rules/edge.d.ts +44 -0
- package/dist/rules/edge.js +66 -0
- package/dist/rules/headers.d.ts +48 -0
- package/dist/rules/headers.js +157 -0
- package/dist/rules/request-headers.d.ts +61 -0
- package/dist/rules/request-headers.js +113 -0
- package/dist/rules/user-agent.d.ts +25 -0
- package/dist/rules/user-agent.js +212 -0
- package/dist/serialize.d.ts +12 -0
- package/dist/serialize.js +23 -0
- 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
|
+
];
|