@substrat-run/contracts 0.91.1 → 0.92.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/dist/client-context.d.ts +104 -0
- package/dist/client-context.d.ts.map +1 -0
- package/dist/client-context.js +243 -0
- package/dist/client-context.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Where the request came from, as far as the edge could tell.
|
|
4
|
+
*
|
|
5
|
+
* `region` is the human name (`Stockholm County`), not a code — codes differ per
|
|
6
|
+
* country and a person reads the name. `timezone` is an IANA name, which is what a
|
|
7
|
+
* UI hands to `Intl.DateTimeFormat` to show the visitor's local time.
|
|
8
|
+
*/
|
|
9
|
+
export declare const clientGeo: z.ZodObject<{
|
|
10
|
+
country: z.ZodNullable<z.ZodString>;
|
|
11
|
+
region: z.ZodNullable<z.ZodString>;
|
|
12
|
+
city: z.ZodNullable<z.ZodString>;
|
|
13
|
+
timezone: z.ZodNullable<z.ZodString>;
|
|
14
|
+
continent: z.ZodNullable<z.ZodString>;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
export type ClientGeo = z.infer<typeof clientGeo>;
|
|
17
|
+
export declare const EMPTY_GEO: ClientGeo;
|
|
18
|
+
/**
|
|
19
|
+
* What kind of thing sent the request. Coarse on purpose: the question a person
|
|
20
|
+
* asks is "were they on a phone?", and a finer answer than this would be a guess
|
|
21
|
+
* dressed as a fact — user agents lie, and iPadOS reports itself as a Mac.
|
|
22
|
+
*/
|
|
23
|
+
export declare const clientDeviceKind: z.ZodEnum<{
|
|
24
|
+
bot: "bot";
|
|
25
|
+
desktop: "desktop";
|
|
26
|
+
mobile: "mobile";
|
|
27
|
+
tablet: "tablet";
|
|
28
|
+
unknown: "unknown";
|
|
29
|
+
}>;
|
|
30
|
+
export type ClientDeviceKind = z.infer<typeof clientDeviceKind>;
|
|
31
|
+
/** The browser and operating system, parsed out of the `User-Agent` into names a person reads. */
|
|
32
|
+
export declare const clientDevice: z.ZodObject<{
|
|
33
|
+
browser: z.ZodNullable<z.ZodString>;
|
|
34
|
+
browserVersion: z.ZodNullable<z.ZodString>;
|
|
35
|
+
os: z.ZodNullable<z.ZodString>;
|
|
36
|
+
osVersion: z.ZodNullable<z.ZodString>;
|
|
37
|
+
kind: z.ZodEnum<{
|
|
38
|
+
bot: "bot";
|
|
39
|
+
desktop: "desktop";
|
|
40
|
+
mobile: "mobile";
|
|
41
|
+
tablet: "tablet";
|
|
42
|
+
unknown: "unknown";
|
|
43
|
+
}>;
|
|
44
|
+
}, z.core.$strip>;
|
|
45
|
+
export type ClientDevice = z.infer<typeof clientDevice>;
|
|
46
|
+
export declare const clientContext: z.ZodObject<{
|
|
47
|
+
userAgent: z.ZodNullable<z.ZodString>;
|
|
48
|
+
language: z.ZodNullable<z.ZodString>;
|
|
49
|
+
device: z.ZodObject<{
|
|
50
|
+
browser: z.ZodNullable<z.ZodString>;
|
|
51
|
+
browserVersion: z.ZodNullable<z.ZodString>;
|
|
52
|
+
os: z.ZodNullable<z.ZodString>;
|
|
53
|
+
osVersion: z.ZodNullable<z.ZodString>;
|
|
54
|
+
kind: z.ZodEnum<{
|
|
55
|
+
bot: "bot";
|
|
56
|
+
desktop: "desktop";
|
|
57
|
+
mobile: "mobile";
|
|
58
|
+
tablet: "tablet";
|
|
59
|
+
unknown: "unknown";
|
|
60
|
+
}>;
|
|
61
|
+
}, z.core.$strip>;
|
|
62
|
+
geo: z.ZodObject<{
|
|
63
|
+
country: z.ZodNullable<z.ZodString>;
|
|
64
|
+
region: z.ZodNullable<z.ZodString>;
|
|
65
|
+
city: z.ZodNullable<z.ZodString>;
|
|
66
|
+
timezone: z.ZodNullable<z.ZodString>;
|
|
67
|
+
continent: z.ZodNullable<z.ZodString>;
|
|
68
|
+
}, z.core.$strip>;
|
|
69
|
+
}, z.core.$strip>;
|
|
70
|
+
export type ClientContext = z.infer<typeof clientContext>;
|
|
71
|
+
/**
|
|
72
|
+
* Turn a `User-Agent` into names a person reads.
|
|
73
|
+
*
|
|
74
|
+
* Hand-rolled and small on purpose. A full UA database is a dependency that
|
|
75
|
+
* changes weekly and answers questions nobody here asks (which Nokia model?); this
|
|
76
|
+
* answers the four a support agent does — which browser, which OS, phone or not,
|
|
77
|
+
* human or not — and stores the raw string beside them so a wrong answer can be
|
|
78
|
+
* re-read later. Absent or unrecognised input is `unknown`, never a throw: a request
|
|
79
|
+
* with a strange UA is still a request.
|
|
80
|
+
*/
|
|
81
|
+
export declare function parseUserAgent(userAgent: string | null | undefined): ClientDevice;
|
|
82
|
+
/** The subset of the web-standard `Headers` this needs — so a test hands in a plain object. */
|
|
83
|
+
export interface HeadersLike {
|
|
84
|
+
get(name: string): string | null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The first language tag of `Accept-Language`, or null. The header is a weighted
|
|
88
|
+
* list (`sv-SE,sv;q=0.9,en;q=0.8`) and browsers put the preferred one first, so the
|
|
89
|
+
* head is the answer; weights are not re-sorted, because no browser sends them out
|
|
90
|
+
* of order and a hand-written header that does is not worth a parser.
|
|
91
|
+
*/
|
|
92
|
+
export declare function preferredLanguage(acceptLanguage: string | null | undefined): string | null;
|
|
93
|
+
/**
|
|
94
|
+
* Build a `ClientContext` from a request's headers and whatever geo the host knows.
|
|
95
|
+
*
|
|
96
|
+
* This is the runtime-neutral half: the headers are the same on every host, and the
|
|
97
|
+
* geo is what differs — so a host with none (the node dev server) passes nothing and
|
|
98
|
+
* gets `EMPTY_GEO`, and one with some (`cloudflareClientContext`) passes what it
|
|
99
|
+
* normalised. A partial geo is filled out to the full shape so a row always has
|
|
100
|
+
* every column — and a field that is present but `undefined` counts as absent, since
|
|
101
|
+
* `Partial` admits it and a spread would carry it through to a schema that does not.
|
|
102
|
+
*/
|
|
103
|
+
export declare function clientContextOf(headers: HeadersLike, geo?: Partial<ClientGeo>): ClientContext;
|
|
104
|
+
//# sourceMappingURL=client-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-context.d.ts","sourceRoot":"","sources":["../src/client-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAgCxB;;;;;;GAMG;AACH,eAAO,MAAM,SAAS;;;;;;iBAOpB,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAElD,eAAO,MAAM,SAAS,EAAE,SAMtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,gBAAgB;;;;;;EAA4D,CAAC;AAC1F,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE,kGAAkG;AAClG,eAAO,MAAM,YAAY;;;;;;;;;;;;iBASvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;iBAOxB,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAiG1D;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,YAAY,CAsBjF;AAMD,+FAA+F;AAC/F,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAG1F;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,GAAG,aAAa,CAc7F"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Who is on the other end of a request — the CLIENT, not the principal.
|
|
4
|
+
*
|
|
5
|
+
* A principal says who is authorised; this says what they are holding and roughly
|
|
6
|
+
* where they are holding it: the browser and OS a support agent needs to reproduce
|
|
7
|
+
* a bug, the country and timezone that tell them it is 3 am for the person typing,
|
|
8
|
+
* the language the reply should probably be in. None of it is authority, and none of
|
|
9
|
+
* it is trusted for anything but display and triage.
|
|
10
|
+
*
|
|
11
|
+
* It is a NORMALISED shape, and that is the point of it. Every runtime hands these
|
|
12
|
+
* facts over differently — Cloudflare puts geo on `request.cf`, another edge puts it
|
|
13
|
+
* in headers, a node dev server has no geo at all — and a vertical that read the raw
|
|
14
|
+
* source would be a vertical that only runs on one host. So a host builds one of
|
|
15
|
+
* these (`@substrat-run/adapter-cloudflare` exports `cloudflareClientContext`), passes
|
|
16
|
+
* it in as operation input, and module code sees only this. The one part that is the
|
|
17
|
+
* same everywhere — the `User-Agent` and `Accept-Language` headers — is parsed here,
|
|
18
|
+
* so every host reads "Safari 17 on iOS" the same way.
|
|
19
|
+
*
|
|
20
|
+
* What is deliberately absent: the IP address. It is the most identifying field a
|
|
21
|
+
* request carries and the least useful to a person reading a conversation; the
|
|
22
|
+
* country and city it resolves to carry the useful part without the fingerprint.
|
|
23
|
+
* Every COLLECTED value is nullable, because every one is something a request may not
|
|
24
|
+
* say; the shape around them is not. `device` and `geo` are always present, and
|
|
25
|
+
* `device.kind` is always a kind — `'unknown'` when the parser could not tell — so a
|
|
26
|
+
* reader destructures without guarding and a row always has every column.
|
|
27
|
+
*/
|
|
28
|
+
/** ISO 3166-1 alpha-2, upper case. */
|
|
29
|
+
const countryCode = z.string().regex(/^[A-Z]{2}$/);
|
|
30
|
+
/**
|
|
31
|
+
* Where the request came from, as far as the edge could tell.
|
|
32
|
+
*
|
|
33
|
+
* `region` is the human name (`Stockholm County`), not a code — codes differ per
|
|
34
|
+
* country and a person reads the name. `timezone` is an IANA name, which is what a
|
|
35
|
+
* UI hands to `Intl.DateTimeFormat` to show the visitor's local time.
|
|
36
|
+
*/
|
|
37
|
+
export const clientGeo = z.object({
|
|
38
|
+
country: countryCode.nullable(),
|
|
39
|
+
region: z.string().nullable(),
|
|
40
|
+
city: z.string().nullable(),
|
|
41
|
+
timezone: z.string().nullable(),
|
|
42
|
+
/** Two-letter continent code as Cloudflare reports it (`EU`, `NA`, …). */
|
|
43
|
+
continent: z.string().regex(/^[A-Z]{2}$/).nullable(),
|
|
44
|
+
});
|
|
45
|
+
export const EMPTY_GEO = Object.freeze({
|
|
46
|
+
country: null,
|
|
47
|
+
region: null,
|
|
48
|
+
city: null,
|
|
49
|
+
timezone: null,
|
|
50
|
+
continent: null,
|
|
51
|
+
});
|
|
52
|
+
/**
|
|
53
|
+
* What kind of thing sent the request. Coarse on purpose: the question a person
|
|
54
|
+
* asks is "were they on a phone?", and a finer answer than this would be a guess
|
|
55
|
+
* dressed as a fact — user agents lie, and iPadOS reports itself as a Mac.
|
|
56
|
+
*/
|
|
57
|
+
export const clientDeviceKind = z.enum(['desktop', 'mobile', 'tablet', 'bot', 'unknown']);
|
|
58
|
+
/** The browser and operating system, parsed out of the `User-Agent` into names a person reads. */
|
|
59
|
+
export const clientDevice = z.object({
|
|
60
|
+
/** `Chrome`, `Safari`, `Firefox`, `Edge`, `Samsung Internet`, `Opera`, `Internet Explorer`, or a bot's name. */
|
|
61
|
+
browser: z.string().nullable(),
|
|
62
|
+
/** As reported, e.g. `126.0.6478.127`; show the major. */
|
|
63
|
+
browserVersion: z.string().nullable(),
|
|
64
|
+
/** `iOS`, `Android`, `macOS`, `Windows`, `Linux`, `ChromeOS`. */
|
|
65
|
+
os: z.string().nullable(),
|
|
66
|
+
osVersion: z.string().nullable(),
|
|
67
|
+
kind: clientDeviceKind,
|
|
68
|
+
});
|
|
69
|
+
export const clientContext = z.object({
|
|
70
|
+
/** The raw header, kept so a better parser can re-read it later. */
|
|
71
|
+
userAgent: z.string().nullable(),
|
|
72
|
+
/** The first tag of `Accept-Language` (BCP 47, e.g. `sv-SE`) — what the person asked to be spoken to in. */
|
|
73
|
+
language: z.string().nullable(),
|
|
74
|
+
device: clientDevice,
|
|
75
|
+
geo: clientGeo,
|
|
76
|
+
});
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// User-Agent parsing
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
const UNKNOWN_DEVICE = Object.freeze({
|
|
81
|
+
browser: null,
|
|
82
|
+
browserVersion: null,
|
|
83
|
+
os: null,
|
|
84
|
+
osVersion: null,
|
|
85
|
+
kind: 'unknown',
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* The bots worth naming. Anything else matching the generic tail is still a bot,
|
|
89
|
+
* just an unnamed one.
|
|
90
|
+
*/
|
|
91
|
+
const BOT_NAMES = [
|
|
92
|
+
[/Googlebot/i, 'Googlebot'],
|
|
93
|
+
[/bingbot/i, 'Bingbot'],
|
|
94
|
+
[/DuckDuckBot/i, 'DuckDuckBot'],
|
|
95
|
+
[/Slackbot/i, 'Slackbot'],
|
|
96
|
+
[/Twitterbot/i, 'Twitterbot'],
|
|
97
|
+
[/facebookexternalhit/i, 'Facebook'],
|
|
98
|
+
[/LinkedInBot/i, 'LinkedInBot'],
|
|
99
|
+
[/Applebot/i, 'Applebot'],
|
|
100
|
+
[/GPTBot/i, 'GPTBot'],
|
|
101
|
+
[/ClaudeBot/i, 'ClaudeBot'],
|
|
102
|
+
[/HeadlessChrome/i, 'Headless Chrome'],
|
|
103
|
+
[/Lighthouse/i, 'Lighthouse'],
|
|
104
|
+
[/curl\//i, 'curl'],
|
|
105
|
+
[/Wget\//i, 'wget'],
|
|
106
|
+
[/python-requests|python-urllib|aiohttp/i, 'Python'],
|
|
107
|
+
[/Go-http-client/i, 'Go'],
|
|
108
|
+
[/node-fetch|undici/i, 'Node'],
|
|
109
|
+
];
|
|
110
|
+
const BOT_GENERIC = /bot|crawl|spider|slurp|scrape|fetch|monitor|preview|headless/i;
|
|
111
|
+
/**
|
|
112
|
+
* Browsers in the order they must be tried. Every Chromium fork carries `Chrome/`
|
|
113
|
+
* and every WebKit browser carries `Safari/`, so the specific names go first and
|
|
114
|
+
* the two generic ones last.
|
|
115
|
+
*/
|
|
116
|
+
const BROWSERS = [
|
|
117
|
+
[/\bEdg(?:e|A|iOS)?\/([\d.]+)/, 'Edge'],
|
|
118
|
+
[/\bOPR\/([\d.]+)/, 'Opera'],
|
|
119
|
+
[/\bOpera[/ ]([\d.]+)/, 'Opera'],
|
|
120
|
+
[/\bSamsungBrowser\/([\d.]+)/, 'Samsung Internet'],
|
|
121
|
+
[/\bFirefox\/([\d.]+)/, 'Firefox'],
|
|
122
|
+
[/\bFxiOS\/([\d.]+)/, 'Firefox'],
|
|
123
|
+
[/\bCriOS\/([\d.]+)/, 'Chrome'],
|
|
124
|
+
[/\bChrome\/([\d.]+)/, 'Chrome'],
|
|
125
|
+
// Safari's own version is in `Version/`; the `Safari/` token is the WebKit build.
|
|
126
|
+
[/\bVersion\/([\d.]+).*\bSafari\//, 'Safari'],
|
|
127
|
+
[/\bMSIE ([\d.]+)/, 'Internet Explorer'],
|
|
128
|
+
[/\bTrident\/.*\brv:([\d.]+)/, 'Internet Explorer'],
|
|
129
|
+
];
|
|
130
|
+
/** `Windows NT 10.0` is Windows 10 (or 11 — the UA cannot tell them apart). */
|
|
131
|
+
const WINDOWS_NT = {
|
|
132
|
+
'10.0': '10',
|
|
133
|
+
'6.3': '8.1',
|
|
134
|
+
'6.2': '8',
|
|
135
|
+
'6.1': '7',
|
|
136
|
+
'6.0': 'Vista',
|
|
137
|
+
'5.1': 'XP',
|
|
138
|
+
};
|
|
139
|
+
function os(ua) {
|
|
140
|
+
let m;
|
|
141
|
+
if ((m = /\b(?:iPhone|iPad|iPod)\b.*?\bOS (\d+(?:_\d+)*)/.exec(ua)))
|
|
142
|
+
return { os: 'iOS', osVersion: m[1].replace(/_/g, '.') };
|
|
143
|
+
if (/\b(?:iPhone|iPad|iPod)\b/.test(ua))
|
|
144
|
+
return { os: 'iOS', osVersion: null };
|
|
145
|
+
if ((m = /\bAndroid (\d+(?:\.\d+)*)/.exec(ua)))
|
|
146
|
+
return { os: 'Android', osVersion: m[1] };
|
|
147
|
+
if (/\bAndroid\b/.test(ua))
|
|
148
|
+
return { os: 'Android', osVersion: null };
|
|
149
|
+
if ((m = /\bWindows NT (\d+\.\d+)/.exec(ua)))
|
|
150
|
+
return { os: 'Windows', osVersion: WINDOWS_NT[m[1]] ?? m[1] };
|
|
151
|
+
if (/\bWindows\b/.test(ua))
|
|
152
|
+
return { os: 'Windows', osVersion: null };
|
|
153
|
+
if ((m = /\bMac OS X (\d+(?:[_.]\d+)*)/.exec(ua)))
|
|
154
|
+
return { os: 'macOS', osVersion: m[1].replace(/_/g, '.') };
|
|
155
|
+
if (/\bMacintosh\b/.test(ua))
|
|
156
|
+
return { os: 'macOS', osVersion: null };
|
|
157
|
+
if (/\bCrOS\b/.test(ua))
|
|
158
|
+
return { os: 'ChromeOS', osVersion: null };
|
|
159
|
+
if (/\bLinux\b|\bX11\b/.test(ua))
|
|
160
|
+
return { os: 'Linux', osVersion: null };
|
|
161
|
+
return { os: null, osVersion: null };
|
|
162
|
+
}
|
|
163
|
+
function kindOf(ua, osName) {
|
|
164
|
+
if (/\biPad\b|\bTablet\b/i.test(ua))
|
|
165
|
+
return 'tablet';
|
|
166
|
+
// Android reports `Mobile` on phones and omits it on tablets.
|
|
167
|
+
if (osName === 'Android')
|
|
168
|
+
return /\bMobile\b/.test(ua) ? 'mobile' : 'tablet';
|
|
169
|
+
if (/\biPhone\b|\biPod\b|\bMobi/i.test(ua))
|
|
170
|
+
return 'mobile';
|
|
171
|
+
if (osName === 'Windows' || osName === 'macOS' || osName === 'Linux' || osName === 'ChromeOS')
|
|
172
|
+
return 'desktop';
|
|
173
|
+
return 'unknown';
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Turn a `User-Agent` into names a person reads.
|
|
177
|
+
*
|
|
178
|
+
* Hand-rolled and small on purpose. A full UA database is a dependency that
|
|
179
|
+
* changes weekly and answers questions nobody here asks (which Nokia model?); this
|
|
180
|
+
* answers the four a support agent does — which browser, which OS, phone or not,
|
|
181
|
+
* human or not — and stores the raw string beside them so a wrong answer can be
|
|
182
|
+
* re-read later. Absent or unrecognised input is `unknown`, never a throw: a request
|
|
183
|
+
* with a strange UA is still a request.
|
|
184
|
+
*/
|
|
185
|
+
export function parseUserAgent(userAgent) {
|
|
186
|
+
const ua = userAgent?.trim() ?? '';
|
|
187
|
+
if (!ua)
|
|
188
|
+
return UNKNOWN_DEVICE;
|
|
189
|
+
for (const [re, name] of BOT_NAMES)
|
|
190
|
+
if (re.test(ua))
|
|
191
|
+
return { ...UNKNOWN_DEVICE, browser: name, kind: 'bot' };
|
|
192
|
+
const platform = os(ua);
|
|
193
|
+
let browser = null;
|
|
194
|
+
let browserVersion = null;
|
|
195
|
+
for (const [re, name] of BROWSERS) {
|
|
196
|
+
const m = re.exec(ua);
|
|
197
|
+
if (m) {
|
|
198
|
+
browser = name;
|
|
199
|
+
browserVersion = m[1] ?? null;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// An unnamed crawler that still says "bot" somewhere: a browser never does, so the
|
|
204
|
+
// browser and OS it may spoof are kept (they are what it claims) and the kind is not.
|
|
205
|
+
const kind = BOT_GENERIC.test(ua) ? 'bot' : kindOf(ua, platform.os);
|
|
206
|
+
return { browser, browserVersion, os: platform.os, osVersion: platform.osVersion, kind };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The first language tag of `Accept-Language`, or null. The header is a weighted
|
|
210
|
+
* list (`sv-SE,sv;q=0.9,en;q=0.8`) and browsers put the preferred one first, so the
|
|
211
|
+
* head is the answer; weights are not re-sorted, because no browser sends them out
|
|
212
|
+
* of order and a hand-written header that does is not worth a parser.
|
|
213
|
+
*/
|
|
214
|
+
export function preferredLanguage(acceptLanguage) {
|
|
215
|
+
const first = acceptLanguage?.split(',')[0]?.split(';')[0]?.trim() ?? '';
|
|
216
|
+
return /^[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*$/.test(first) ? first : null;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Build a `ClientContext` from a request's headers and whatever geo the host knows.
|
|
220
|
+
*
|
|
221
|
+
* This is the runtime-neutral half: the headers are the same on every host, and the
|
|
222
|
+
* geo is what differs — so a host with none (the node dev server) passes nothing and
|
|
223
|
+
* gets `EMPTY_GEO`, and one with some (`cloudflareClientContext`) passes what it
|
|
224
|
+
* normalised. A partial geo is filled out to the full shape so a row always has
|
|
225
|
+
* every column — and a field that is present but `undefined` counts as absent, since
|
|
226
|
+
* `Partial` admits it and a spread would carry it through to a schema that does not.
|
|
227
|
+
*/
|
|
228
|
+
export function clientContextOf(headers, geo) {
|
|
229
|
+
const userAgent = headers.get('user-agent')?.trim() || null;
|
|
230
|
+
return {
|
|
231
|
+
userAgent,
|
|
232
|
+
language: preferredLanguage(headers.get('accept-language')),
|
|
233
|
+
device: parseUserAgent(userAgent),
|
|
234
|
+
geo: {
|
|
235
|
+
country: geo?.country ?? null,
|
|
236
|
+
region: geo?.region ?? null,
|
|
237
|
+
city: geo?.city ?? null,
|
|
238
|
+
timezone: geo?.timezone ?? null,
|
|
239
|
+
continent: geo?.continent ?? null,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=client-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-context.js","sourceRoot":"","sources":["../src/client-context.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@substrat-run/contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.92.0",
|
|
4
4
|
"description": "Substrat kernel contract schemas — Zod is the source of truth (master plan D-22); OAS/JSON Schema are emitted artifacts",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|