@selvajs/server 0.2.0-beta.1
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/LICENSE +21 -0
- package/dist/compute/__tests__/client-cache.test.d.ts +11 -0
- package/dist/compute/__tests__/client-cache.test.d.ts.map +1 -0
- package/dist/compute/__tests__/client-cache.test.js +149 -0
- package/dist/compute/__tests__/client-cache.test.js.map +1 -0
- package/dist/compute/__tests__/rate-limit.test.d.ts +2 -0
- package/dist/compute/__tests__/rate-limit.test.d.ts.map +1 -0
- package/dist/compute/__tests__/rate-limit.test.js +62 -0
- package/dist/compute/__tests__/rate-limit.test.js.map +1 -0
- package/dist/compute/__tests__/remote-definition.test.d.ts +11 -0
- package/dist/compute/__tests__/remote-definition.test.d.ts.map +1 -0
- package/dist/compute/__tests__/remote-definition.test.js +114 -0
- package/dist/compute/__tests__/remote-definition.test.js.map +1 -0
- package/dist/compute/__tests__/safe-url.test.d.ts +2 -0
- package/dist/compute/__tests__/safe-url.test.d.ts.map +1 -0
- package/dist/compute/__tests__/safe-url.test.js +71 -0
- package/dist/compute/__tests__/safe-url.test.js.map +1 -0
- package/dist/compute/__tests__/solve-pipeline.test.d.ts +11 -0
- package/dist/compute/__tests__/solve-pipeline.test.d.ts.map +1 -0
- package/dist/compute/__tests__/solve-pipeline.test.js +245 -0
- package/dist/compute/__tests__/solve-pipeline.test.js.map +1 -0
- package/dist/compute/__tests__/transform-input.test.d.ts +14 -0
- package/dist/compute/__tests__/transform-input.test.d.ts.map +1 -0
- package/dist/compute/__tests__/transform-input.test.js +114 -0
- package/dist/compute/__tests__/transform-input.test.js.map +1 -0
- package/dist/compute/client-cache.d.ts +104 -0
- package/dist/compute/client-cache.d.ts.map +1 -0
- package/dist/compute/client-cache.js +147 -0
- package/dist/compute/client-cache.js.map +1 -0
- package/dist/compute/index.d.ts +8 -0
- package/dist/compute/index.d.ts.map +1 -0
- package/dist/compute/index.js +9 -0
- package/dist/compute/index.js.map +1 -0
- package/dist/compute/limits.d.ts +102 -0
- package/dist/compute/limits.d.ts.map +1 -0
- package/dist/compute/limits.js +76 -0
- package/dist/compute/limits.js.map +1 -0
- package/dist/compute/rate-limit.d.ts +49 -0
- package/dist/compute/rate-limit.d.ts.map +1 -0
- package/dist/compute/rate-limit.js +49 -0
- package/dist/compute/rate-limit.js.map +1 -0
- package/dist/compute/remote-definition.d.ts +53 -0
- package/dist/compute/remote-definition.d.ts.map +1 -0
- package/dist/compute/remote-definition.js +110 -0
- package/dist/compute/remote-definition.js.map +1 -0
- package/dist/compute/safe-url.d.ts +15 -0
- package/dist/compute/safe-url.d.ts.map +1 -0
- package/dist/compute/safe-url.js +195 -0
- package/dist/compute/safe-url.js.map +1 -0
- package/dist/compute/solve-pipeline.d.ts +127 -0
- package/dist/compute/solve-pipeline.d.ts.map +1 -0
- package/dist/compute/solve-pipeline.js +195 -0
- package/dist/compute/solve-pipeline.js.map +1 -0
- package/dist/compute/transform-input.d.ts +20 -0
- package/dist/compute/transform-input.d.ts.map +1 -0
- package/dist/compute/transform-input.js +47 -0
- package/dist/compute/transform-input.js.map +1 -0
- package/dist/definitions/__tests__/load-for-render.test.d.ts +8 -0
- package/dist/definitions/__tests__/load-for-render.test.d.ts.map +1 -0
- package/dist/definitions/__tests__/load-for-render.test.js +196 -0
- package/dist/definitions/__tests__/load-for-render.test.js.map +1 -0
- package/dist/definitions/__tests__/schema-extraction.test.d.ts +11 -0
- package/dist/definitions/__tests__/schema-extraction.test.d.ts.map +1 -0
- package/dist/definitions/__tests__/schema-extraction.test.js +115 -0
- package/dist/definitions/__tests__/schema-extraction.test.js.map +1 -0
- package/dist/definitions/definition-service.d.ts +77 -0
- package/dist/definitions/definition-service.d.ts.map +1 -0
- package/dist/definitions/definition-service.js +184 -0
- package/dist/definitions/definition-service.js.map +1 -0
- package/dist/definitions/index.d.ts +4 -0
- package/dist/definitions/index.d.ts.map +1 -0
- package/dist/definitions/index.js +8 -0
- package/dist/definitions/index.js.map +1 -0
- package/dist/definitions/load-for-render.d.ts +61 -0
- package/dist/definitions/load-for-render.d.ts.map +1 -0
- package/dist/definitions/load-for-render.js +165 -0
- package/dist/definitions/load-for-render.js.map +1 -0
- package/dist/definitions/schema-extraction.d.ts +22 -0
- package/dist/definitions/schema-extraction.d.ts.map +1 -0
- package/dist/definitions/schema-extraction.js +89 -0
- package/dist/definitions/schema-extraction.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/__tests__/create-selva-providers.test.d.ts +7 -0
- package/dist/providers/__tests__/create-selva-providers.test.d.ts.map +1 -0
- package/dist/providers/__tests__/create-selva-providers.test.js +135 -0
- package/dist/providers/__tests__/create-selva-providers.test.js.map +1 -0
- package/dist/providers/create-selva-providers.d.ts +53 -0
- package/dist/providers/create-selva-providers.d.ts.map +1 -0
- package/dist/providers/create-selva-providers.js +152 -0
- package/dist/providers/create-selva-providers.js.map +1 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +6 -0
- package/dist/providers/index.js.map +1 -0
- package/package.json +60 -0
- package/src/compute/__tests__/client-cache.test.ts +164 -0
- package/src/compute/__tests__/rate-limit.test.ts +69 -0
- package/src/compute/__tests__/remote-definition.test.ts +140 -0
- package/src/compute/__tests__/safe-url.test.ts +72 -0
- package/src/compute/__tests__/solve-pipeline.test.ts +294 -0
- package/src/compute/__tests__/transform-input.test.ts +155 -0
- package/src/compute/client-cache.ts +278 -0
- package/src/compute/index.ts +48 -0
- package/src/compute/limits.ts +147 -0
- package/src/compute/rate-limit.ts +83 -0
- package/src/compute/remote-definition.ts +149 -0
- package/src/compute/safe-url.ts +186 -0
- package/src/compute/solve-pipeline.ts +324 -0
- package/src/compute/transform-input.ts +52 -0
- package/src/definitions/__tests__/load-for-render.test.ts +244 -0
- package/src/definitions/__tests__/schema-extraction.test.ts +137 -0
- package/src/definitions/definition-service.ts +243 -0
- package/src/definitions/index.ts +22 -0
- package/src/definitions/load-for-render.ts +272 -0
- package/src/definitions/schema-extraction.ts +117 -0
- package/src/index.ts +18 -0
- package/src/providers/__tests__/create-selva-providers.test.ts +172 -0
- package/src/providers/__tests__/fixtures/override-config.mjs +8 -0
- package/src/providers/create-selva-providers.ts +241 -0
- package/src/providers/index.ts +12 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote-definition fetch: SSRF-guarded, size-capped, timed, and TTL-cached.
|
|
3
|
+
*
|
|
4
|
+
* A solve can reference its `.gh` by remote URL instead of a stored blob. That
|
|
5
|
+
* URL is attacker-controllable (a share-token caller supplies `definitionUrl`),
|
|
6
|
+
* so every fetch runs through {@link assertSafeRemoteDefinitionUrl} (literal +
|
|
7
|
+
* DNS host validation, blocking private/loopback/link-local targets and
|
|
8
|
+
* literal-encoding bypasses), streams the body with an early size cap, and
|
|
9
|
+
* bounds the wall time with a deadline.
|
|
10
|
+
*
|
|
11
|
+
* The TTL cache is a per-fetcher `Map<url, {data, fetchedAt}>` so repeated
|
|
12
|
+
* solves of the same remote definition skip the network. It's created by
|
|
13
|
+
* {@link createRemoteDefinitionFetcher} rather than being module-global, so a
|
|
14
|
+
* consuming app can hold one fetcher per process (the app binding does) or
|
|
15
|
+
* several isolated ones (tests) without shared state leaking across them.
|
|
16
|
+
*/
|
|
17
|
+
import { assertSafeRemoteDefinitionUrl } from './safe-url.js';
|
|
18
|
+
/** Once the cache exceeds this many entries, evict the oldest 10 by `fetchedAt`. */
|
|
19
|
+
const CACHE_MAX_ENTRIES = 50;
|
|
20
|
+
const CACHE_EVICT_BATCH = 10;
|
|
21
|
+
/**
|
|
22
|
+
* Read a response body into memory, aborting (and throwing) as soon as the
|
|
23
|
+
* running byte total exceeds `maxBytes`. Falls back to `arrayBuffer()` only when
|
|
24
|
+
* the body isn't a readable stream (older fetch impls). Exported for direct use
|
|
25
|
+
* and reuse; the fetcher below wraps it with SSRF + TTL.
|
|
26
|
+
*/
|
|
27
|
+
export async function readBodyWithCap(response, maxBytes, controller) {
|
|
28
|
+
if (!response.body) {
|
|
29
|
+
const buffer = await response.arrayBuffer();
|
|
30
|
+
if (buffer.byteLength > maxBytes)
|
|
31
|
+
throw new Error('Remote definition exceeds size limit');
|
|
32
|
+
return new Uint8Array(buffer);
|
|
33
|
+
}
|
|
34
|
+
const reader = response.body.getReader();
|
|
35
|
+
const chunks = [];
|
|
36
|
+
let total = 0;
|
|
37
|
+
while (true) {
|
|
38
|
+
const { done, value } = await reader.read();
|
|
39
|
+
if (done)
|
|
40
|
+
break;
|
|
41
|
+
total += value.byteLength;
|
|
42
|
+
if (total > maxBytes) {
|
|
43
|
+
controller.abort();
|
|
44
|
+
throw new Error('Remote definition exceeds size limit');
|
|
45
|
+
}
|
|
46
|
+
chunks.push(value);
|
|
47
|
+
}
|
|
48
|
+
const out = new Uint8Array(total);
|
|
49
|
+
let offset = 0;
|
|
50
|
+
for (const chunk of chunks) {
|
|
51
|
+
out.set(chunk, offset);
|
|
52
|
+
offset += chunk.byteLength;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build a remote-definition fetcher owning its own TTL cache. Each fetcher's
|
|
58
|
+
* cache is independent, so one per process shares warm bytes while test fetchers
|
|
59
|
+
* stay isolated.
|
|
60
|
+
*/
|
|
61
|
+
export function createRemoteDefinitionFetcher(config) {
|
|
62
|
+
const cache = new Map();
|
|
63
|
+
return {
|
|
64
|
+
async load(url) {
|
|
65
|
+
// Resolves the host and rejects when any resolved IP is private/loopback/
|
|
66
|
+
// link-local — covers literal-encoding bypasses (integer/octal/hex/short-form,
|
|
67
|
+
// IPv4-mapped IPv6) and public names that point inward. Throws on rejection.
|
|
68
|
+
await assertSafeRemoteDefinitionUrl(url);
|
|
69
|
+
const now = config.now();
|
|
70
|
+
const cached = cache.get(url);
|
|
71
|
+
if (cached && now - cached.fetchedAt < config.cacheTtlMs) {
|
|
72
|
+
return cached.data;
|
|
73
|
+
}
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
const timeout = setTimeout(() => controller.abort(), config.fetchTimeoutMs);
|
|
76
|
+
let data;
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(url, {
|
|
79
|
+
signal: controller.signal,
|
|
80
|
+
redirect: 'error'
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
84
|
+
}
|
|
85
|
+
// Reject early when the server declares an oversized body.
|
|
86
|
+
const declared = Number(response.headers.get('content-length'));
|
|
87
|
+
if (Number.isFinite(declared) && declared > config.maxBytes) {
|
|
88
|
+
throw new Error('Remote definition exceeds size limit');
|
|
89
|
+
}
|
|
90
|
+
// Stream and count rather than `arrayBuffer()` — a missing/lying
|
|
91
|
+
// content-length must not let an unbounded body buffer into memory
|
|
92
|
+
// before the cap is checked. Abort the moment we cross the limit.
|
|
93
|
+
data = await readBodyWithCap(response, config.maxBytes, controller);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
clearTimeout(timeout);
|
|
97
|
+
}
|
|
98
|
+
cache.set(url, { data, fetchedAt: now });
|
|
99
|
+
if (cache.size > CACHE_MAX_ENTRIES) {
|
|
100
|
+
const entries = Array.from(cache.entries());
|
|
101
|
+
entries.sort((a, b) => a[1].fetchedAt - b[1].fetchedAt);
|
|
102
|
+
for (let i = 0; i < CACHE_EVICT_BATCH; i++) {
|
|
103
|
+
cache.delete(entries[i][0]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=remote-definition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-definition.js","sourceRoot":"","sources":["../../src/compute/remote-definition.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous literal pre-filter. Rejects bad schemes, loopback hostnames, and
|
|
3
|
+
* any IP literal (in any encoding) that resolves to a blocked address. Does NOT
|
|
4
|
+
* perform DNS — a public *name* that points at a private IP passes here and is
|
|
5
|
+
* caught by `assertSafeRemoteDefinitionUrl`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isSafeRemoteDefinitionUrl(raw: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Full SSRF check: literal pre-filter, then resolve every A/AAAA record and
|
|
10
|
+
* reject if *any* resolved IP is blocked. Returns the original URL on success
|
|
11
|
+
* so callers can `await assertSafeRemoteDefinitionUrl(url)` inline. Throws on
|
|
12
|
+
* any rejection.
|
|
13
|
+
*/
|
|
14
|
+
export declare function assertSafeRemoteDefinitionUrl(raw: string): Promise<string>;
|
|
15
|
+
//# sourceMappingURL=safe-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-url.d.ts","sourceRoot":"","sources":["../../src/compute/safe-url.ts"],"names":[],"mappings":"AA4HA;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CA2B9D;AAED;;;;;GAKG;AACH,wBAAsB,6BAA6B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoBhF"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { lookup } from 'node:dns/promises';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
/**
|
|
4
|
+
* SSRF guard for `loadRemoteDefinition`. Without it an authenticated user could
|
|
5
|
+
* submit `http://169.254.169.254/...` (cloud metadata) or any intranet endpoint
|
|
6
|
+
* and the server would fetch it on their behalf.
|
|
7
|
+
*
|
|
8
|
+
* Two layers:
|
|
9
|
+
*
|
|
10
|
+
* 1. `isSafeRemoteDefinitionUrl` — synchronous literal pre-filter. Rejects
|
|
11
|
+
* protocol, hostname literals, and every IP *encoding* that maps to a
|
|
12
|
+
* blocked address (dotted-decimal, integer, octal, hex, short-form,
|
|
13
|
+
* IPv4-mapped IPv6). Fast and DNS-free; safe to call anywhere.
|
|
14
|
+
*
|
|
15
|
+
* 2. `assertSafeRemoteDefinitionUrl` — resolves the hostname via DNS and
|
|
16
|
+
* re-checks the *resolved* IP. This is the real defense: it catches a
|
|
17
|
+
* public name that resolves to a private IP, which the literal filter
|
|
18
|
+
* can't see. Still not bulletproof against rebinding (the IP can change
|
|
19
|
+
* between this lookup and fetch's own); for that the caller would have to
|
|
20
|
+
* connect to the resolved IP directly. Connecting by IP is a larger change
|
|
21
|
+
* — this closes the practical bypasses (literal encodings + a DNS record
|
|
22
|
+
* that points inward) which is what an attacker actually reaches for.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* True when `ip` (a normalized IPv4 or IPv6 literal from `isIP`/`dns.lookup`)
|
|
26
|
+
* is loopback, private, link-local, or otherwise must-not-fetch. IPv4-mapped
|
|
27
|
+
* IPv6 (`::ffff:a.b.c.d`) is unwrapped to its embedded IPv4 first so the v4
|
|
28
|
+
* rules apply.
|
|
29
|
+
*/
|
|
30
|
+
function isBlockedIp(ip) {
|
|
31
|
+
let addr = ip.toLowerCase();
|
|
32
|
+
// Unwrap IPv4-mapped IPv6 so the embedded address is judged by the IPv4
|
|
33
|
+
// rules below. `URL` compresses the dotted form to hex (`::ffff:127.0.0.1`
|
|
34
|
+
// → `::ffff:7f00:1`), so handle both spellings.
|
|
35
|
+
const mappedDotted = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(addr);
|
|
36
|
+
if (mappedDotted) {
|
|
37
|
+
addr = mappedDotted[1];
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const mappedHex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(addr);
|
|
41
|
+
if (mappedHex) {
|
|
42
|
+
const hi = parseInt(mappedHex[1], 16);
|
|
43
|
+
const lo = parseInt(mappedHex[2], 16);
|
|
44
|
+
addr = [(hi >> 8) & 0xff, hi & 0xff, (lo >> 8) & 0xff, lo & 0xff].join('.');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (isIP(addr) === 4) {
|
|
48
|
+
const octets = addr.split('.').map(Number);
|
|
49
|
+
if (octets.length !== 4 || octets.some((o) => o > 255))
|
|
50
|
+
return true; // malformed → block
|
|
51
|
+
const [a, b] = octets;
|
|
52
|
+
if (a === 0)
|
|
53
|
+
return true; // 0.0.0.0/8 (incl. 0.0.0.0)
|
|
54
|
+
if (a === 127)
|
|
55
|
+
return true; // loopback
|
|
56
|
+
if (a === 10)
|
|
57
|
+
return true; // RFC1918
|
|
58
|
+
if (a === 192 && b === 168)
|
|
59
|
+
return true; // RFC1918
|
|
60
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
61
|
+
return true; // RFC1918
|
|
62
|
+
if (a === 169 && b === 254)
|
|
63
|
+
return true; // link-local + cloud metadata
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
// IPv6 literals.
|
|
67
|
+
if (addr === '::1' || addr === '::')
|
|
68
|
+
return true; // loopback / unspecified
|
|
69
|
+
if (/^(fc|fd)[0-9a-f]{0,2}:/.test(addr))
|
|
70
|
+
return true; // unique-local fc00::/7
|
|
71
|
+
if (/^fe[89ab][0-9a-f]:/.test(addr))
|
|
72
|
+
return true; // link-local fe80::/10
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Parse a bare-IPv4 hostname in any encoding browsers/glibc accept — integer
|
|
77
|
+
* (`2130706433`), octal (`0177.0.0.1`), hex (`0x7f000001`), and short forms
|
|
78
|
+
* (`127.1`) — into a canonical dotted-decimal string. Returns null when the
|
|
79
|
+
* host is not a numeric IPv4 literal (i.e. it's a real name or already-valid
|
|
80
|
+
* dotted-quad, which callers handle separately).
|
|
81
|
+
*/
|
|
82
|
+
function canonicalizeNumericIpv4(host) {
|
|
83
|
+
// Already a clean dotted-quad — let isIP handle it directly.
|
|
84
|
+
if (isIP(host) === 4)
|
|
85
|
+
return host;
|
|
86
|
+
const rawParts = host.split('.');
|
|
87
|
+
if (rawParts.length === 0 || rawParts.length > 4)
|
|
88
|
+
return null;
|
|
89
|
+
const parseUInt = (s) => {
|
|
90
|
+
if (s === '')
|
|
91
|
+
return null;
|
|
92
|
+
let n;
|
|
93
|
+
if (/^0x[0-9a-f]+$/i.test(s))
|
|
94
|
+
n = parseInt(s, 16);
|
|
95
|
+
else if (/^0[0-7]+$/.test(s))
|
|
96
|
+
n = parseInt(s, 8);
|
|
97
|
+
else if (/^[0-9]+$/.test(s))
|
|
98
|
+
n = parseInt(s, 10);
|
|
99
|
+
else
|
|
100
|
+
return null;
|
|
101
|
+
return Number.isFinite(n) ? n : null;
|
|
102
|
+
};
|
|
103
|
+
const nums = rawParts.map(parseUInt);
|
|
104
|
+
if (nums.some((n) => n === null))
|
|
105
|
+
return null;
|
|
106
|
+
const parts = nums;
|
|
107
|
+
// inet_aton semantics: the final part absorbs all remaining low-order bytes.
|
|
108
|
+
// 1 part → 32-bit value; 2 → a.(24-bit); 3 → a.b.(16-bit); 4 → a.b.c.d.
|
|
109
|
+
let value;
|
|
110
|
+
switch (parts.length) {
|
|
111
|
+
case 1:
|
|
112
|
+
value = parts[0];
|
|
113
|
+
break;
|
|
114
|
+
case 2:
|
|
115
|
+
if (parts[0] > 0xff || parts[1] > 0xffffff)
|
|
116
|
+
return null;
|
|
117
|
+
value = (parts[0] << 24) | parts[1];
|
|
118
|
+
break;
|
|
119
|
+
case 3:
|
|
120
|
+
if (parts[0] > 0xff || parts[1] > 0xff || parts[2] > 0xffff)
|
|
121
|
+
return null;
|
|
122
|
+
value = (parts[0] << 24) | (parts[1] << 16) | parts[2];
|
|
123
|
+
break;
|
|
124
|
+
default:
|
|
125
|
+
if (parts.some((p) => p > 0xff))
|
|
126
|
+
return null;
|
|
127
|
+
value = (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
|
|
128
|
+
}
|
|
129
|
+
value = value >>> 0; // force unsigned 32-bit
|
|
130
|
+
if (value > 0xffffffff)
|
|
131
|
+
return null;
|
|
132
|
+
return [(value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff].join('.');
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Synchronous literal pre-filter. Rejects bad schemes, loopback hostnames, and
|
|
136
|
+
* any IP literal (in any encoding) that resolves to a blocked address. Does NOT
|
|
137
|
+
* perform DNS — a public *name* that points at a private IP passes here and is
|
|
138
|
+
* caught by `assertSafeRemoteDefinitionUrl`.
|
|
139
|
+
*/
|
|
140
|
+
export function isSafeRemoteDefinitionUrl(raw) {
|
|
141
|
+
let parsed;
|
|
142
|
+
try {
|
|
143
|
+
parsed = new URL(raw);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
149
|
+
return false;
|
|
150
|
+
let host = parsed.hostname.toLowerCase();
|
|
151
|
+
if (!host)
|
|
152
|
+
return false;
|
|
153
|
+
// Hostname literals that never reach DNS.
|
|
154
|
+
if (host === 'localhost' || host.endsWith('.localhost'))
|
|
155
|
+
return false;
|
|
156
|
+
// Strip the brackets URL keeps around IPv6 literals.
|
|
157
|
+
const isBracketed = host.startsWith('[') && host.endsWith(']');
|
|
158
|
+
if (isBracketed)
|
|
159
|
+
host = host.slice(1, -1);
|
|
160
|
+
if (isIP(host) === 6)
|
|
161
|
+
return !isBlockedIp(host);
|
|
162
|
+
// Numeric IPv4 in any encoding → canonicalize, then judge.
|
|
163
|
+
const canonical = canonicalizeNumericIpv4(host);
|
|
164
|
+
if (canonical)
|
|
165
|
+
return !isBlockedIp(canonical);
|
|
166
|
+
// A real hostname — passes the literal filter; DNS resolution is the gate.
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Full SSRF check: literal pre-filter, then resolve every A/AAAA record and
|
|
171
|
+
* reject if *any* resolved IP is blocked. Returns the original URL on success
|
|
172
|
+
* so callers can `await assertSafeRemoteDefinitionUrl(url)` inline. Throws on
|
|
173
|
+
* any rejection.
|
|
174
|
+
*/
|
|
175
|
+
export async function assertSafeRemoteDefinitionUrl(raw) {
|
|
176
|
+
if (!isSafeRemoteDefinitionUrl(raw)) {
|
|
177
|
+
throw new Error('Remote definition URL is not allowed');
|
|
178
|
+
}
|
|
179
|
+
const host = new URL(raw).hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
180
|
+
// Already an IP literal — isSafeRemoteDefinitionUrl validated it; no DNS.
|
|
181
|
+
if (isIP(host) !== 0 || canonicalizeNumericIpv4(host))
|
|
182
|
+
return raw;
|
|
183
|
+
let resolved;
|
|
184
|
+
try {
|
|
185
|
+
resolved = await lookup(host, { all: true });
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
throw new Error('Remote definition URL is not allowed');
|
|
189
|
+
}
|
|
190
|
+
if (resolved.length === 0 || resolved.some((r) => isBlockedIp(r.address))) {
|
|
191
|
+
throw new Error('Remote definition URL is not allowed');
|
|
192
|
+
}
|
|
193
|
+
return raw;
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=safe-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-url.js","sourceRoot":"","sources":["../../src/compute/safe-url.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-agnostic solve pipeline — the callable core lifted out of the app's
|
|
3
|
+
* `/api/compute` route (embeddable-server-layer plan, K3).
|
|
4
|
+
*
|
|
5
|
+
* Given an already-resolved solve context — the `.gh` bytes, the input params +
|
|
6
|
+
* user values, and a warm `SolveScheduler` (from `createClientCache`) — this
|
|
7
|
+
* runs the framework-free half of a solve:
|
|
8
|
+
*
|
|
9
|
+
* input tree build → scheduler.solve (with abort/timeout classification) →
|
|
10
|
+
* JSON serialize (size-guarded) → optional gzip → Server-Timing envelope.
|
|
11
|
+
*
|
|
12
|
+
* It returns a discriminated {@link SolveOutcome}: `ok` carries the ready-to-send
|
|
13
|
+
* {@link SolveEnvelope} (body + headers + phase metrics), while the error
|
|
14
|
+
* variants (`timeout`, `client_abort`, `too_large`, `compute_error`) name a
|
|
15
|
+
* failure the transport maps to its own status code. Nothing here throws for an
|
|
16
|
+
* expected failure and nothing here touches auth, the database, share tokens,
|
|
17
|
+
* rate limits, or metric sinks — those stay app policy in the route.
|
|
18
|
+
*
|
|
19
|
+
* The Server-Timing string it emits IS the versioned wire contract (audit D5):
|
|
20
|
+
* see {@link COMPUTE_CONTRACT_VERSION} and {@link COMPUTE_VERSION_HEADER}.
|
|
21
|
+
*/
|
|
22
|
+
import { type GrasshopperComputeResponse } from '@selvajs/compute';
|
|
23
|
+
import type { SchemaInput } from '@selvajs/schemas';
|
|
24
|
+
import type { CachedClient } from './client-cache.js';
|
|
25
|
+
/** Current compute-response contract version. Additive header; bump on shape change. */
|
|
26
|
+
export declare const COMPUTE_CONTRACT_VERSION: 1;
|
|
27
|
+
/** Response header carrying {@link COMPUTE_CONTRACT_VERSION}. */
|
|
28
|
+
export declare const COMPUTE_VERSION_HEADER = "X-Selva-Compute-Version";
|
|
29
|
+
/** A schema input carrying the optional numeric bounds the transform reads. */
|
|
30
|
+
export type PipelineInput = SchemaInput & {
|
|
31
|
+
minimum?: number;
|
|
32
|
+
maximum?: number;
|
|
33
|
+
stepSize?: number;
|
|
34
|
+
};
|
|
35
|
+
export interface SolvePipelineArgs {
|
|
36
|
+
/** The `.gh` bytes to solve (stored blob or remote fetch — resolved by the caller). */
|
|
37
|
+
definitionSource: Uint8Array;
|
|
38
|
+
/** Persisted input params; only those with a `paramType` are sent to the solve. */
|
|
39
|
+
inputs: PipelineInput[];
|
|
40
|
+
/** User-chosen values keyed by input id; missing keys fall back to the schema default. */
|
|
41
|
+
values: Record<string, unknown>;
|
|
42
|
+
/** Warm client bundle from `createClientCache().getClient(...)`. */
|
|
43
|
+
client: CachedClient;
|
|
44
|
+
/** Longest a serialized result may be (bytes) before a 413. */
|
|
45
|
+
responseMaxBytes: number;
|
|
46
|
+
/** Solve-deadline millis — used only to phrase the timeout message. */
|
|
47
|
+
maxSolveDurationMs: number;
|
|
48
|
+
/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */
|
|
49
|
+
acceptEncoding: string;
|
|
50
|
+
/**
|
|
51
|
+
* Request abort signal, forwarded to the scheduler so a client disconnect
|
|
52
|
+
* cancels the upstream compute call. Its `aborted` flag also disambiguates a
|
|
53
|
+
* client disconnect from the scheduler's own deadline firing.
|
|
54
|
+
*/
|
|
55
|
+
signal: AbortSignal;
|
|
56
|
+
/**
|
|
57
|
+
* Wall-clock origin (`performance.now()` captured at the top of the request)
|
|
58
|
+
* so the pipeline's `load`/`total` phase timings line up with the caller's
|
|
59
|
+
* pre-solve prep. The caller owns everything before this and stamps `defLoadMs`.
|
|
60
|
+
*/
|
|
61
|
+
loadStartMs: number;
|
|
62
|
+
/** Pre-solve "load" phase duration the caller already measured (auth + DB + fetch). */
|
|
63
|
+
defLoadMs: number;
|
|
64
|
+
/**
|
|
65
|
+
* Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*`
|
|
66
|
+
* Server-Timing entries. The caller assembles these; the pipeline only echoes.
|
|
67
|
+
*/
|
|
68
|
+
prepMarks?: [string, number][];
|
|
69
|
+
}
|
|
70
|
+
/** Phase timings the pipeline measured; the caller uses them for debug logging. */
|
|
71
|
+
export interface SolvePhaseMetrics {
|
|
72
|
+
treeBuildMs: number;
|
|
73
|
+
solveMs: number;
|
|
74
|
+
serializeMs: number;
|
|
75
|
+
gzipMs: number;
|
|
76
|
+
serverTotalMs: number;
|
|
77
|
+
/** Uncompressed serialized length in bytes. */
|
|
78
|
+
serializedBytes: number;
|
|
79
|
+
/** Gzipped length in bytes, or null when compression was skipped. */
|
|
80
|
+
compressedBytes: number | null;
|
|
81
|
+
}
|
|
82
|
+
/** A ready-to-send response: body + headers + the solve result + phase metrics. */
|
|
83
|
+
export interface SolveEnvelope {
|
|
84
|
+
/** The response body — a gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */
|
|
85
|
+
body: string | Uint8Array;
|
|
86
|
+
/** `'gzip'` when the body is compressed; absent otherwise. */
|
|
87
|
+
encoding?: 'gzip';
|
|
88
|
+
/** Fully-assembled response headers (Content-Type/-Length/-Encoding, Vary, Server-Timing, version). */
|
|
89
|
+
headers: Record<string, string>;
|
|
90
|
+
/** The solved definition — the caller reads `errors`/`warnings` for its metric record. */
|
|
91
|
+
result: GrasshopperComputeResponse;
|
|
92
|
+
/** Phase timings, for the caller's optional debug breakdown. */
|
|
93
|
+
metrics: SolvePhaseMetrics;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Discriminated result. `ok` carries the envelope; every other variant names an
|
|
97
|
+
* expected failure the transport maps to a status code (the app route maps
|
|
98
|
+
* timeout→504, client_abort→499, too_large→413; `compute_error` re-surfaces the
|
|
99
|
+
* original error for the generic 500/503 path). `durationMs` on the error
|
|
100
|
+
* variants is the solve wall time up to the failure, for the metric record.
|
|
101
|
+
*/
|
|
102
|
+
export type SolveOutcome = {
|
|
103
|
+
kind: 'ok';
|
|
104
|
+
envelope: SolveEnvelope;
|
|
105
|
+
solveMs: number;
|
|
106
|
+
errorCount: number;
|
|
107
|
+
warningCount: number;
|
|
108
|
+
} | {
|
|
109
|
+
kind: 'timeout';
|
|
110
|
+
durationMs: number;
|
|
111
|
+
message: string;
|
|
112
|
+
} | {
|
|
113
|
+
kind: 'client_abort';
|
|
114
|
+
durationMs: number;
|
|
115
|
+
} | {
|
|
116
|
+
kind: 'too_large';
|
|
117
|
+
} | {
|
|
118
|
+
kind: 'compute_error';
|
|
119
|
+
durationMs: number;
|
|
120
|
+
error: unknown;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Run the framework-free half of a solve and produce a typed outcome. See the
|
|
124
|
+
* module doc for the phase sequence and the app/library boundary.
|
|
125
|
+
*/
|
|
126
|
+
export declare function runSolvePipeline(args: SolvePipelineArgs): Promise<SolveOutcome>;
|
|
127
|
+
//# sourceMappingURL=solve-pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"solve-pipeline.d.ts","sourceRoot":"","sources":["../../src/compute/solve-pipeline.ts"],"names":[],"mappings":"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"}
|