@nivalos/lithium.js 1.1.0 → 1.6.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 +31 -6
- package/bin/lithium.js +0 -0
- package/client/index.js +9 -3
- package/client/net.js +156 -24
- package/client/search.js +37 -0
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -17,13 +17,14 @@ A flexible web proxy framework to make your skid dream a reality.
|
|
|
17
17
|
- **`lithium doctor`**: one command to check your whole install
|
|
18
18
|
- **Events + history**: `on("navigate", ...)`, `back()`/`forward()`/`reload()`/`search()`, works the same for every proxy
|
|
19
19
|
- **Debug mode + network log**: `debug: true` for verbose tracing, `network_log()`/`on("request", ...)` for a devtools-lite view of the proxied page's `fetch`/XHR traffic
|
|
20
|
-
- **Header policy**: block/allow specific headers, or bypass
|
|
20
|
+
- **Header policy**: block/allow/override specific headers, spoof device fingerprint (`navigator`/`screen`), or bypass interception entirely with `passthrough` mode
|
|
21
|
+
- **Custom search engines**: register your own, with per-engine header overrides
|
|
21
22
|
- **Modular design**: clean separation of client and server code, custom backends are first-class
|
|
22
23
|
|
|
23
24
|
## Installation
|
|
24
25
|
|
|
25
26
|
```bash
|
|
26
|
-
npm install lithium.js
|
|
27
|
+
npm install @nivalos/lithium.js
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
Scramjet 2.x is published under the `alpha` tag and Lithium pins the exact
|
|
@@ -214,16 +215,40 @@ Lithium's Node server never sees the proxied site's raw HTTP (that goes browser
|
|
|
214
215
|
```javascript
|
|
215
216
|
await init_lithium({
|
|
216
217
|
headers: {
|
|
217
|
-
mode: "filter",
|
|
218
|
-
block: ["x-frame-options"],
|
|
219
|
-
allow: null,
|
|
218
|
+
mode: "filter", // "filter" (default) or "passthrough" (touch nothing)
|
|
219
|
+
block: ["x-frame-options"], // stripped from requests AND responses, case-insensitive
|
|
220
|
+
allow: null, // if an array, ONLY these header names survive
|
|
221
|
+
set: { "x-custom": "value" }, // added/overridden on outgoing requests
|
|
222
|
+
searchEngineHeaders: { "x-a": "b" }, // extra `set` overrides, ONLY on requests to the current search engine's origin
|
|
223
|
+
referrer: "https://example.com/", // passed to fetch()'s own `referrer` option (Referer can't be set via headers at all)
|
|
224
|
+
device: { // spoofs navigator/screen on the proxied page, see "Device spoofing" below
|
|
225
|
+
userAgent: "Mozilla/5.0 (...) Custom/1.0",
|
|
226
|
+
platform: "Win32",
|
|
227
|
+
},
|
|
220
228
|
},
|
|
221
229
|
})
|
|
222
|
-
set_header_policy({ mode: "passthrough" }) // change
|
|
230
|
+
set_header_policy({ mode: "passthrough" }) // change any of this any time, no re-init needed
|
|
223
231
|
```
|
|
224
232
|
|
|
225
233
|
**Scope, honestly:** this rebuilds the `Response` object `fetch()` hands to the page, so it's real for anything the page's own JS reads. `XMLHttpRequest` traffic is logged but the policy isn't enforced on it. `<img>`, `<script src>`, `<link>`, and CSS loads never go through JS at all, so they're invisible to this — there's no hook point for them at this layer.
|
|
226
234
|
|
|
235
|
+
**A real browser restriction, not a Lithium limitation:** a set of header names — `Cookie`, `Host`, `Origin`, `Connection`, anything starting with `Sec-`/`Proxy-`, and a few others — are "forbidden request headers" per the Fetch spec. No page JS anywhere can set them; the browser silently drops them when it builds the real request. `Referer` is on that list too, which is why it's handled separately via `referrer` above rather than through `set`/`block`/`allow` (put it in `set.referer` if that's more convenient — Lithium notices and routes it to the right place for you). `User-Agent` isn't spec-forbidden anymore, but Chrome still silently drops it from `fetch()` regardless (a long-standing Chromium quirk); Firefox honors it. `network_log()` entries include a `caveats` note whenever a header you configured falls into one of these categories, so you're not left guessing why it didn't take.
|
|
236
|
+
|
|
237
|
+
### Device spoofing
|
|
238
|
+
|
|
239
|
+
The `device` option overrides `navigator`/`screen` properties (`userAgent`, `platform`, `vendor`, `language`, `languages`, `maxTouchPoints`, `hardwareConcurrency`, `deviceMemory`, `userAgentData`, `screen: { width, height, ... }`) on the proxied page, so the *site's own JavaScript* — feature detection, analytics, fingerprinting — sees a different device. This is unrestricted (none of the forbidden-header rules above apply to JS properties), but it only fools client-side JS, not a server reading real HTTP headers, and only for reads that happen after this installs (same per-navigation timing caveat as everything else in this section).
|
|
240
|
+
|
|
241
|
+
## Custom search engines
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
import { search_engines } from "lithium.js" // or a client-only import in the browser
|
|
245
|
+
|
|
246
|
+
search_engines.register("bing", (query) => `https://www.bing.com/search?q=${encodeURIComponent(query)}`)
|
|
247
|
+
await init_lithium({ searchEngine: "bing" }) // or: config.searchEngine = "bing" any time
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`search()`/`navigate()` with a non-URL query, and the `searchEngineHeaders` scoping above, all use whichever engine is currently set. An unregistered name silently falls back to Google rather than breaking navigation.
|
|
251
|
+
|
|
227
252
|
## Network log
|
|
228
253
|
|
|
229
254
|
```javascript
|
package/bin/lithium.js
CHANGED
|
File without changes
|
package/client/index.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// navigate("example.com")
|
|
9
9
|
|
|
10
10
|
import * as net from "./net.js"
|
|
11
|
+
import { search_engines, build_search_url, search_origin } from "./search.js"
|
|
12
|
+
export { search_engines }
|
|
11
13
|
|
|
12
14
|
// the server injects this into your html (see server/index.js)
|
|
13
15
|
const server_config = () => (typeof window !== "undefined" && window.__LITHIUM_CONFIG__) || {}
|
|
@@ -91,7 +93,9 @@ export const config = {
|
|
|
91
93
|
set debug(on) { state.debug = Boolean(on) },
|
|
92
94
|
// read-only snapshot (deep enough that mutating it can't affect live state);
|
|
93
95
|
// use set_header_policy() to change it live
|
|
94
|
-
|
|
96
|
+
// read-only deep-enough snapshot (mutating it can't affect live state);
|
|
97
|
+
// use set_header_policy() to change it live
|
|
98
|
+
get headers() { return JSON.parse(JSON.stringify(state.header_policy)) },
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
// change the header policy without a full re-init (see net.js for what it can/can't do)
|
|
@@ -216,6 +220,7 @@ function get_iframe(create = true) {
|
|
|
216
220
|
emit("request", entry)
|
|
217
221
|
},
|
|
218
222
|
dbg,
|
|
223
|
+
search_origin: () => search_origin(engine()),
|
|
219
224
|
})
|
|
220
225
|
} catch (err) {
|
|
221
226
|
dbg("network hook install failed:", err)
|
|
@@ -346,9 +351,10 @@ export function is_url(v = "") {
|
|
|
346
351
|
)
|
|
347
352
|
}
|
|
348
353
|
|
|
354
|
+
// engine can be "google", "duckduckgo", or anything registered via
|
|
355
|
+
// search_engines.register()
|
|
349
356
|
export function search_url(query, engine = "google") {
|
|
350
|
-
|
|
351
|
-
return engine === "duckduckgo" ? `https://duckduckgo.com/?q=${q}` : `https://www.google.com/search?q=${q}`
|
|
357
|
+
return build_search_url(query, engine)
|
|
352
358
|
}
|
|
353
359
|
|
|
354
360
|
// turn whatever was typed into a full url (or a search url)
|
package/client/net.js
CHANGED
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
// real site, as bytes, not parsed HTTP). So this works client-side instead:
|
|
6
6
|
// it's installed on the proxied iframe's window (same-origin, since that's
|
|
7
7
|
// how UV/Scramjet serve proxied pages) and wraps fetch()/XMLHttpRequest.
|
|
8
|
-
// - fetch(): full control
|
|
9
|
-
//
|
|
10
|
-
// own code sees it.
|
|
8
|
+
// - fetch(): full control over request headers going out and the Response
|
|
9
|
+
// the page's own code sees coming back.
|
|
11
10
|
// - XMLHttpRequest: observed only (logged), the header policy is NOT
|
|
12
11
|
// enforced on it — rebuilding a native XHR's headers isn't practical.
|
|
13
12
|
// - Anything that isn't fetch/XHR (<img>, <script src>, <link>, CSS,
|
|
@@ -15,15 +14,62 @@
|
|
|
15
14
|
// - Only requests made AFTER this installs are seen. It installs on the
|
|
16
15
|
// iframe's "load" event, so very early/synchronous requests on that
|
|
17
16
|
// page can be missed.
|
|
17
|
+
//
|
|
18
|
+
// A real browser restriction this can't route around: a set of header names
|
|
19
|
+
// ("forbidden request headers" in the Fetch spec — Cookie, Host, Origin,
|
|
20
|
+
// Connection, and anything starting with Sec-/Proxy-, among others) can't be
|
|
21
|
+
// set from page JS at all; the browser silently drops them when it builds
|
|
22
|
+
// the actual request, no matter what's in the Headers object handed to
|
|
23
|
+
// fetch(). Referer is spec-forbidden the same way, but fetch() has a
|
|
24
|
+
// dedicated `referrer` option that DOES work, so that one's routed there
|
|
25
|
+
// instead. User-Agent is no longer spec-forbidden, but Chrome still silently
|
|
26
|
+
// drops it from fetch() requests regardless (a long-standing Chromium
|
|
27
|
+
// quirk); Firefox honors it. See is_unforgeable_header()/header_set_caveat().
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_POLICY = {
|
|
30
|
+
mode: "filter",
|
|
31
|
+
block: [],
|
|
32
|
+
allow: null,
|
|
33
|
+
set: {}, // headers to add/override on outgoing requests (subject to the browser restrictions above)
|
|
34
|
+
searchEngineHeaders: {}, // extra overrides, applied ONLY when the request's origin is the current search engine's
|
|
35
|
+
device: null, // { userAgent, platform, vendor, language, languages, maxTouchPoints, hardwareConcurrency, deviceMemory, userAgentData, screen } spoofed on navigator/screen
|
|
36
|
+
referrer: undefined, // passed straight to fetch()'s `referrer` option
|
|
37
|
+
referrerPolicy: undefined,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const UNFORGEABLE_HEADERS = new Set([
|
|
41
|
+
"accept-charset", "accept-encoding", "access-control-request-headers", "access-control-request-method",
|
|
42
|
+
"connection", "content-length", "cookie", "cookie2", "date", "dnt", "expect", "host", "keep-alive",
|
|
43
|
+
"origin", "permissions-policy", "referer", "set-cookie", "te", "trailer", "transfer-encoding", "upgrade", "via",
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
// true for header names the browser will silently refuse to send from page
|
|
47
|
+
// JS no matter what Lithium does — see the file header for why
|
|
48
|
+
export function is_unforgeable_header(name) {
|
|
49
|
+
const lk = String(name).toLowerCase()
|
|
50
|
+
return UNFORGEABLE_HEADERS.has(lk) || lk.startsWith("sec-") || lk.startsWith("proxy-")
|
|
51
|
+
}
|
|
18
52
|
|
|
19
|
-
|
|
53
|
+
// a one-line explanation for a header name in a `set`/`searchEngineHeaders`
|
|
54
|
+
// map, or null if there's nothing special to say about it
|
|
55
|
+
export function header_set_caveat(name) {
|
|
56
|
+
const lk = String(name).toLowerCase()
|
|
57
|
+
if (lk === "user-agent") return "not spec-forbidden, but Chrome silently drops it from fetch() requests anyway (Chromium bug 571722); Firefox honors it"
|
|
58
|
+
if (lk === "referer" || lk === "referrer") return "Referer can't be set via headers at all; use policy.referrer instead (Lithium routes set.referer there for you)"
|
|
59
|
+
if (is_unforgeable_header(lk)) return "forbidden by the Fetch spec — the browser drops it silently, there's no workaround from page JS"
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// mode: "passthrough" -> do nothing (headers pass exactly as the proxy
|
|
64
|
+
// already delivered them, including ignoring set/searchEngineHeaders);
|
|
65
|
+
// "filter" -> apply block/allow, then (for request headers only) set/searchEngineHeaders
|
|
66
|
+
// direction: "request" | "response" — set/searchEngineHeaders/referrer only make sense for
|
|
67
|
+
// outgoing requests (they're about how WE identify ourselves), not responses
|
|
68
|
+
// search_engine_match: whether this request's origin is the current search engine's,
|
|
69
|
+
// enabling searchEngineHeaders on top of set
|
|
70
|
+
export function apply_policy(headers, policy, { direction = "request", search_engine_match = false } = {}) {
|
|
71
|
+
if (policy.mode === "passthrough") return { headers, blocked: [], overridden: [], caveats: [], referrer: undefined }
|
|
20
72
|
|
|
21
|
-
// mode: "passthrough" -> do nothing (headers pass exactly as the proxy already
|
|
22
|
-
// delivered them); "filter" -> apply block/allow
|
|
23
|
-
// block: header names (case-insensitive) to strip
|
|
24
|
-
// allow: if an array, ONLY these header names survive (block still applies on top)
|
|
25
|
-
export function apply_policy(headers, policy) {
|
|
26
|
-
if (policy.mode === "passthrough") return { headers, blocked: [] }
|
|
27
73
|
const out = new Headers()
|
|
28
74
|
const blocked = []
|
|
29
75
|
const block = new Set((policy.block ?? []).map((h) => h.toLowerCase()))
|
|
@@ -36,19 +82,92 @@ export function apply_policy(headers, policy) {
|
|
|
36
82
|
}
|
|
37
83
|
out.append(k, v)
|
|
38
84
|
}
|
|
39
|
-
|
|
85
|
+
|
|
86
|
+
if (direction !== "request") return { headers: out, blocked, overridden: [], caveats: [], referrer: undefined }
|
|
87
|
+
|
|
88
|
+
const overrides = { ...(policy.set ?? {}), ...(search_engine_match ? (policy.searchEngineHeaders ?? {}) : {}) }
|
|
89
|
+
const overridden = []
|
|
90
|
+
const caveats = []
|
|
91
|
+
let referrer = policy.referrer
|
|
92
|
+
for (const [name, value] of Object.entries(overrides)) {
|
|
93
|
+
const lk = name.toLowerCase()
|
|
94
|
+
if (lk === "referer" || lk === "referrer") {
|
|
95
|
+
referrer ??= value // an explicit policy.referrer still wins over one smuggled in via `set`
|
|
96
|
+
caveats.push({ name, note: header_set_caveat(name) })
|
|
97
|
+
continue // don't bother putting a header in that fetch() would drop anyway
|
|
98
|
+
}
|
|
99
|
+
out.set(name, value)
|
|
100
|
+
overridden.push(name)
|
|
101
|
+
const note = header_set_caveat(name)
|
|
102
|
+
if (note) caveats.push({ name, note })
|
|
103
|
+
}
|
|
104
|
+
return { headers: out, blocked, overridden, caveats, referrer }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Overrides properties of navigator/screen on the proxied window so the
|
|
108
|
+
// SITE'S OWN JS (feature detection, analytics, fingerprinting) sees a
|
|
109
|
+
// different device. This is unrestricted (no Fetch-spec forbidden list to
|
|
110
|
+
// fight), unlike request headers — but it only fools JS, not a server that
|
|
111
|
+
// inspects real HTTP headers, and it only takes effect for code that reads
|
|
112
|
+
// these properties AFTER this installs (see the file header).
|
|
113
|
+
// Returns which property names were successfully overridden.
|
|
114
|
+
export function apply_device_profile(win, device) {
|
|
115
|
+
if (!device || !win?.navigator) return []
|
|
116
|
+
const applied = []
|
|
117
|
+
const nav = win.navigator
|
|
118
|
+
const simple = ["userAgent", "platform", "vendor", "language", "languages", "maxTouchPoints", "hardwareConcurrency", "deviceMemory"]
|
|
119
|
+
for (const key of simple) {
|
|
120
|
+
if (device[key] === undefined) continue
|
|
121
|
+
try {
|
|
122
|
+
Object.defineProperty(nav, key, { get: () => device[key], configurable: true })
|
|
123
|
+
applied.push(key)
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
if (device.userAgentData) {
|
|
127
|
+
const uad = device.userAgentData
|
|
128
|
+
try {
|
|
129
|
+
Object.defineProperty(nav, "userAgentData", {
|
|
130
|
+
configurable: true,
|
|
131
|
+
get: () => ({
|
|
132
|
+
brands: uad.brands ?? [],
|
|
133
|
+
mobile: Boolean(uad.mobile),
|
|
134
|
+
platform: uad.platform ?? "",
|
|
135
|
+
toJSON: () => ({ brands: uad.brands ?? [], mobile: Boolean(uad.mobile), platform: uad.platform ?? "" }),
|
|
136
|
+
getHighEntropyValues: async (hints) =>
|
|
137
|
+
Object.fromEntries((hints ?? []).map((h) => [h, h in uad ? uad[h] : h === "brands" ? uad.brands ?? [] : h === "mobile" ? Boolean(uad.mobile) : h === "platform" ? uad.platform ?? "" : null])),
|
|
138
|
+
}),
|
|
139
|
+
})
|
|
140
|
+
applied.push("userAgentData")
|
|
141
|
+
} catch {}
|
|
142
|
+
}
|
|
143
|
+
if (device.screen && win.screen) {
|
|
144
|
+
for (const [key, value] of Object.entries(device.screen)) {
|
|
145
|
+
try {
|
|
146
|
+
Object.defineProperty(win.screen, key, { get: () => value, configurable: true })
|
|
147
|
+
applied.push(`screen.${key}`)
|
|
148
|
+
} catch {}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return applied
|
|
40
152
|
}
|
|
41
153
|
|
|
42
154
|
const headers_to_object = (headers) => Object.fromEntries(headers.entries())
|
|
43
155
|
|
|
44
156
|
// win: the proxied iframe's contentWindow
|
|
45
|
-
// get_policy()
|
|
46
|
-
// emit(entry)
|
|
47
|
-
// dbg(...)
|
|
48
|
-
|
|
157
|
+
// get_policy() -> current policy (read live, so changes apply mid-session)
|
|
158
|
+
// emit(entry) -> called once per completed request
|
|
159
|
+
// dbg(...) -> debug logger
|
|
160
|
+
// search_origin() -> the current search engine's origin, or null (for searchEngineHeaders scoping)
|
|
161
|
+
export function install(win, { get_policy, emit, dbg = () => {}, search_origin = () => null }) {
|
|
49
162
|
if (!win || win.__lithiumNetHooked) return
|
|
50
163
|
win.__lithiumNetHooked = true
|
|
51
164
|
|
|
165
|
+
const initial_policy = get_policy()
|
|
166
|
+
if (initial_policy.device) {
|
|
167
|
+
const applied = apply_device_profile(win, initial_policy.device)
|
|
168
|
+
dbg(`device profile applied: ${applied.length ? applied.join(", ") : "(nothing — properties weren't configurable here)"}`)
|
|
169
|
+
}
|
|
170
|
+
|
|
52
171
|
const orig_fetch = win.fetch?.bind(win)
|
|
53
172
|
if (orig_fetch) {
|
|
54
173
|
win.fetch = async (input, init = {}) => {
|
|
@@ -56,16 +175,29 @@ export function install(win, { get_policy, emit, dbg = () => {} }) {
|
|
|
56
175
|
const url = typeof input === "string" ? input : input.url
|
|
57
176
|
const method = (init.method || (input instanceof Request ? input.method : "GET") || "GET").toUpperCase()
|
|
58
177
|
const started = win.performance?.now?.() ?? Date.now()
|
|
178
|
+
const origin = search_origin()
|
|
179
|
+
const is_search_request = Boolean(origin) && url.startsWith(origin)
|
|
59
180
|
|
|
60
181
|
let req_headers = new Headers(init.headers || (input instanceof Request ? input.headers : undefined) || {})
|
|
61
|
-
const {
|
|
62
|
-
|
|
182
|
+
const req_result = apply_policy(req_headers, policy, { direction: "request", search_engine_match: is_search_request })
|
|
183
|
+
req_headers = req_result.headers
|
|
184
|
+
if (req_result.blocked.length) dbg(`blocked request headers on ${url}: ${req_result.blocked.join(", ")}`)
|
|
185
|
+
for (const c of req_result.caveats) if (c.note) dbg(`request header "${c.name}" on ${url}: ${c.note}`)
|
|
186
|
+
|
|
187
|
+
const finish = (fields) =>
|
|
188
|
+
emit({
|
|
189
|
+
type: "fetch", method, url, ts: Date.now(), duration: Math.round((win.performance?.now?.() ?? Date.now()) - started),
|
|
190
|
+
requestHeaders: headers_to_object(req_headers), blockedRequestHeaders: req_result.blocked, blockedResponseHeaders: [], responseHeaders: null,
|
|
191
|
+
...fields,
|
|
192
|
+
})
|
|
63
193
|
|
|
64
|
-
const
|
|
194
|
+
const fetch_init = { ...init, headers: req_headers }
|
|
195
|
+
if (req_result.referrer !== undefined) fetch_init.referrer = req_result.referrer
|
|
196
|
+
if (policy.referrerPolicy) fetch_init.referrerPolicy = policy.referrerPolicy
|
|
65
197
|
|
|
66
198
|
let response
|
|
67
199
|
try {
|
|
68
|
-
response = await orig_fetch(input instanceof Request ? new Request(input, { headers:
|
|
200
|
+
response = await orig_fetch(input instanceof Request ? new Request(input, { headers: req_headers }) : input, fetch_init)
|
|
69
201
|
} catch (err) {
|
|
70
202
|
finish({ status: 0, ok: false, error: err.message })
|
|
71
203
|
throw err
|
|
@@ -75,12 +207,12 @@ export function install(win, { get_policy, emit, dbg = () => {} }) {
|
|
|
75
207
|
finish({ status: response.status, ok: response.ok, responseHeaders: headers_to_object(response.headers) })
|
|
76
208
|
return response
|
|
77
209
|
}
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
finish({ status: response.status, ok: response.ok, responseHeaders: headers_to_object(
|
|
81
|
-
if (!
|
|
210
|
+
const res_result = apply_policy(response.headers, policy, { direction: "response" })
|
|
211
|
+
if (res_result.blocked.length) dbg(`blocked response headers on ${url}: ${res_result.blocked.join(", ")}`)
|
|
212
|
+
finish({ status: response.status, ok: response.ok, responseHeaders: headers_to_object(res_result.headers), blockedResponseHeaders: res_result.blocked })
|
|
213
|
+
if (!res_result.blocked.length) return response
|
|
82
214
|
const body = await response.clone().blob()
|
|
83
|
-
return new Response(body, { status: response.status, statusText: response.statusText, headers:
|
|
215
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: res_result.headers })
|
|
84
216
|
}
|
|
85
217
|
}
|
|
86
218
|
|
package/client/search.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Search engine registry. Built-ins are "google" and "duckduckgo"; register
|
|
2
|
+
// your own with a name and a function that turns a query into a URL.
|
|
3
|
+
// search_engines.register("bing", (q) => `https://www.bing.com/search?q=${encodeURIComponent(q)}`)
|
|
4
|
+
// set_header_policy set the default via init_lithium({ searchEngine: "bing" })
|
|
5
|
+
|
|
6
|
+
const engines = new Map([
|
|
7
|
+
["google", (q) => `https://www.google.com/search?q=${encodeURIComponent(q)}`],
|
|
8
|
+
["duckduckgo", (q) => `https://duckduckgo.com/?q=${encodeURIComponent(q)}`],
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
export const search_engines = {
|
|
12
|
+
list: () => [...engines.keys()],
|
|
13
|
+
has: (name) => engines.has(name),
|
|
14
|
+
register(name, url_builder) {
|
|
15
|
+
if (typeof name !== "string" || !name) throw new TypeError('search_engines.register(name, urlBuilder): name must be a non-empty string')
|
|
16
|
+
if (typeof url_builder !== "function") throw new TypeError(`search_engines.register("${name}", urlBuilder): urlBuilder must be a function (query) => url`)
|
|
17
|
+
engines.set(name, url_builder)
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// falls back to "google" if the name isn't registered (never throws on lookup;
|
|
22
|
+
// registering a bad one throws above, but using an unknown name shouldn't
|
|
23
|
+
// break a running page over a typo)
|
|
24
|
+
export function build_search_url(query, name) {
|
|
25
|
+
const build = engines.get(name) ?? engines.get("google")
|
|
26
|
+
return build(String(query).trim())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// the origin a search actually goes to, used to scope search-engine-only
|
|
30
|
+
// header overrides (see net.js). null if the engine's URL is malformed.
|
|
31
|
+
export function search_origin(name) {
|
|
32
|
+
try {
|
|
33
|
+
return new URL(build_search_url("x", name)).origin
|
|
34
|
+
} catch {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nivalos/lithium.js",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Creating a proxy has never been easier with Lithium.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/index.js",
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./server/index.js"
|
|
12
12
|
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/Lithium-Foundry/Lithium.js.git"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
13
20
|
"scripts": {
|
|
14
21
|
"start": "node server/start.js",
|
|
15
22
|
"test": "echo \"no automated tests are bundled, run `npx lithium doctor` to check your install\""
|