@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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/dist/compute/__tests__/client-cache.test.d.ts +11 -0
  3. package/dist/compute/__tests__/client-cache.test.d.ts.map +1 -0
  4. package/dist/compute/__tests__/client-cache.test.js +149 -0
  5. package/dist/compute/__tests__/client-cache.test.js.map +1 -0
  6. package/dist/compute/__tests__/rate-limit.test.d.ts +2 -0
  7. package/dist/compute/__tests__/rate-limit.test.d.ts.map +1 -0
  8. package/dist/compute/__tests__/rate-limit.test.js +62 -0
  9. package/dist/compute/__tests__/rate-limit.test.js.map +1 -0
  10. package/dist/compute/__tests__/remote-definition.test.d.ts +11 -0
  11. package/dist/compute/__tests__/remote-definition.test.d.ts.map +1 -0
  12. package/dist/compute/__tests__/remote-definition.test.js +114 -0
  13. package/dist/compute/__tests__/remote-definition.test.js.map +1 -0
  14. package/dist/compute/__tests__/safe-url.test.d.ts +2 -0
  15. package/dist/compute/__tests__/safe-url.test.d.ts.map +1 -0
  16. package/dist/compute/__tests__/safe-url.test.js +71 -0
  17. package/dist/compute/__tests__/safe-url.test.js.map +1 -0
  18. package/dist/compute/__tests__/solve-pipeline.test.d.ts +11 -0
  19. package/dist/compute/__tests__/solve-pipeline.test.d.ts.map +1 -0
  20. package/dist/compute/__tests__/solve-pipeline.test.js +245 -0
  21. package/dist/compute/__tests__/solve-pipeline.test.js.map +1 -0
  22. package/dist/compute/__tests__/transform-input.test.d.ts +14 -0
  23. package/dist/compute/__tests__/transform-input.test.d.ts.map +1 -0
  24. package/dist/compute/__tests__/transform-input.test.js +114 -0
  25. package/dist/compute/__tests__/transform-input.test.js.map +1 -0
  26. package/dist/compute/client-cache.d.ts +104 -0
  27. package/dist/compute/client-cache.d.ts.map +1 -0
  28. package/dist/compute/client-cache.js +147 -0
  29. package/dist/compute/client-cache.js.map +1 -0
  30. package/dist/compute/index.d.ts +8 -0
  31. package/dist/compute/index.d.ts.map +1 -0
  32. package/dist/compute/index.js +9 -0
  33. package/dist/compute/index.js.map +1 -0
  34. package/dist/compute/limits.d.ts +102 -0
  35. package/dist/compute/limits.d.ts.map +1 -0
  36. package/dist/compute/limits.js +76 -0
  37. package/dist/compute/limits.js.map +1 -0
  38. package/dist/compute/rate-limit.d.ts +49 -0
  39. package/dist/compute/rate-limit.d.ts.map +1 -0
  40. package/dist/compute/rate-limit.js +49 -0
  41. package/dist/compute/rate-limit.js.map +1 -0
  42. package/dist/compute/remote-definition.d.ts +53 -0
  43. package/dist/compute/remote-definition.d.ts.map +1 -0
  44. package/dist/compute/remote-definition.js +110 -0
  45. package/dist/compute/remote-definition.js.map +1 -0
  46. package/dist/compute/safe-url.d.ts +15 -0
  47. package/dist/compute/safe-url.d.ts.map +1 -0
  48. package/dist/compute/safe-url.js +195 -0
  49. package/dist/compute/safe-url.js.map +1 -0
  50. package/dist/compute/solve-pipeline.d.ts +127 -0
  51. package/dist/compute/solve-pipeline.d.ts.map +1 -0
  52. package/dist/compute/solve-pipeline.js +195 -0
  53. package/dist/compute/solve-pipeline.js.map +1 -0
  54. package/dist/compute/transform-input.d.ts +20 -0
  55. package/dist/compute/transform-input.d.ts.map +1 -0
  56. package/dist/compute/transform-input.js +47 -0
  57. package/dist/compute/transform-input.js.map +1 -0
  58. package/dist/definitions/__tests__/load-for-render.test.d.ts +8 -0
  59. package/dist/definitions/__tests__/load-for-render.test.d.ts.map +1 -0
  60. package/dist/definitions/__tests__/load-for-render.test.js +196 -0
  61. package/dist/definitions/__tests__/load-for-render.test.js.map +1 -0
  62. package/dist/definitions/__tests__/schema-extraction.test.d.ts +11 -0
  63. package/dist/definitions/__tests__/schema-extraction.test.d.ts.map +1 -0
  64. package/dist/definitions/__tests__/schema-extraction.test.js +115 -0
  65. package/dist/definitions/__tests__/schema-extraction.test.js.map +1 -0
  66. package/dist/definitions/definition-service.d.ts +77 -0
  67. package/dist/definitions/definition-service.d.ts.map +1 -0
  68. package/dist/definitions/definition-service.js +184 -0
  69. package/dist/definitions/definition-service.js.map +1 -0
  70. package/dist/definitions/index.d.ts +4 -0
  71. package/dist/definitions/index.d.ts.map +1 -0
  72. package/dist/definitions/index.js +8 -0
  73. package/dist/definitions/index.js.map +1 -0
  74. package/dist/definitions/load-for-render.d.ts +61 -0
  75. package/dist/definitions/load-for-render.d.ts.map +1 -0
  76. package/dist/definitions/load-for-render.js +165 -0
  77. package/dist/definitions/load-for-render.js.map +1 -0
  78. package/dist/definitions/schema-extraction.d.ts +22 -0
  79. package/dist/definitions/schema-extraction.d.ts.map +1 -0
  80. package/dist/definitions/schema-extraction.js +89 -0
  81. package/dist/definitions/schema-extraction.js.map +1 -0
  82. package/dist/index.d.ts +18 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +18 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/providers/__tests__/create-selva-providers.test.d.ts +7 -0
  87. package/dist/providers/__tests__/create-selva-providers.test.d.ts.map +1 -0
  88. package/dist/providers/__tests__/create-selva-providers.test.js +135 -0
  89. package/dist/providers/__tests__/create-selva-providers.test.js.map +1 -0
  90. package/dist/providers/create-selva-providers.d.ts +53 -0
  91. package/dist/providers/create-selva-providers.d.ts.map +1 -0
  92. package/dist/providers/create-selva-providers.js +152 -0
  93. package/dist/providers/create-selva-providers.js.map +1 -0
  94. package/dist/providers/index.d.ts +2 -0
  95. package/dist/providers/index.d.ts.map +1 -0
  96. package/dist/providers/index.js +6 -0
  97. package/dist/providers/index.js.map +1 -0
  98. package/package.json +60 -0
  99. package/src/compute/__tests__/client-cache.test.ts +164 -0
  100. package/src/compute/__tests__/rate-limit.test.ts +69 -0
  101. package/src/compute/__tests__/remote-definition.test.ts +140 -0
  102. package/src/compute/__tests__/safe-url.test.ts +72 -0
  103. package/src/compute/__tests__/solve-pipeline.test.ts +294 -0
  104. package/src/compute/__tests__/transform-input.test.ts +155 -0
  105. package/src/compute/client-cache.ts +278 -0
  106. package/src/compute/index.ts +48 -0
  107. package/src/compute/limits.ts +147 -0
  108. package/src/compute/rate-limit.ts +83 -0
  109. package/src/compute/remote-definition.ts +149 -0
  110. package/src/compute/safe-url.ts +186 -0
  111. package/src/compute/solve-pipeline.ts +324 -0
  112. package/src/compute/transform-input.ts +52 -0
  113. package/src/definitions/__tests__/load-for-render.test.ts +244 -0
  114. package/src/definitions/__tests__/schema-extraction.test.ts +137 -0
  115. package/src/definitions/definition-service.ts +243 -0
  116. package/src/definitions/index.ts +22 -0
  117. package/src/definitions/load-for-render.ts +272 -0
  118. package/src/definitions/schema-extraction.ts +117 -0
  119. package/src/index.ts +18 -0
  120. package/src/providers/__tests__/create-selva-providers.test.ts +172 -0
  121. package/src/providers/__tests__/fixtures/override-config.mjs +8 -0
  122. package/src/providers/create-selva-providers.ts +241 -0
  123. 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":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;GAKG;AACH,SAAS,WAAW,CAAC,EAAU;IAC9B,IAAI,IAAI,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;IAE5B,wEAAwE;IACxE,2EAA2E;IAC3E,gDAAgD;IAChD,MAAM,YAAY,GAAG,+CAA+C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChF,IAAI,YAAY,EAAE,CAAC;QAClB,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC;SAAM,CAAC;QACP,MAAM,SAAS,GAAG,0CAA0C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxE,IAAI,SAAS,EAAE,CAAC;YACf,MAAM,EAAE,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACtC,MAAM,EAAE,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACtC,IAAI,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,IAAI,EAAE,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7E,CAAC;IACF,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC3C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC,CAAC,oBAAoB;QACzF,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,CAAC,4BAA4B;QACtD,IAAI,CAAC,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC,CAAC,WAAW;QACvC,IAAI,CAAC,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC,CAAC,UAAU;QACrC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC,CAAC,UAAU;QACnD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;YAAE,OAAO,IAAI,CAAC,CAAC,UAAU;QAC5D,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC,CAAC,8BAA8B;QACvE,OAAO,KAAK,CAAC;IACd,CAAC;IAED,iBAAiB;IACjB,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,CAAC,yBAAyB;IAC3E,IAAI,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,wBAAwB;IAC9E,IAAI,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,uBAAuB;IACzE,OAAO,KAAK,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,SAAS,uBAAuB,CAAC,IAAY;IAC5C,6DAA6D;IAC7D,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAElC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAE9D,MAAM,SAAS,GAAG,CAAC,CAAS,EAAiB,EAAE;QAC9C,IAAI,CAAC,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC;QAC1B,IAAI,CAAS,CAAC;QACd,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;aAC7C,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;aAC5C,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;;YAC5C,OAAO,IAAI,CAAC;QACjB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtC,CAAC,CAAC;IAEF,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACrC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9C,MAAM,KAAK,GAAG,IAAgB,CAAC;IAE/B,6EAA6E;IAC7E,yEAAyE;IACzE,IAAI,KAAa,CAAC;IAClB,QAAQ,KAAK,CAAC,MAAM,EAAE,CAAC;QACtB,KAAK,CAAC;YACL,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACjB,MAAM;QACP,KAAK,CAAC;YACL,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,QAAQ;gBAAE,OAAO,IAAI,CAAC;YACxD,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACpC,MAAM;QACP,KAAK,CAAC;YACL,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM;gBAAE,OAAO,IAAI,CAAC;YACzE,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACvD,MAAM;QACP;YACC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC;YAC7C,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3E,CAAC;IACD,KAAK,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,wBAAwB;IAC7C,IAAI,KAAK,GAAG,UAAU;QAAE,OAAO,IAAI,CAAC;IAEpC,OAAO,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,KAAK,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC,CAAC,IAAI,CAC7F,GAAG,CACH,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,GAAW;IACpD,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACJ,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAC;IACd,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAE9E,IAAI,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;IACzC,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IAExB,0CAA0C;IAC1C,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;QAAE,OAAO,KAAK,CAAC;IAEtE,qDAAqD;IACrD,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC/D,IAAI,WAAW;QAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAE1C,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAEhD,2DAA2D;IAC3D,MAAM,SAAS,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,SAAS;QAAE,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;IAE9C,2EAA2E;IAC3E,OAAO,IAAI,CAAC;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B,CAAC,GAAW;IAC9D,IAAI,CAAC,yBAAyB,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IACzD,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAEzE,0EAA0E;IAC1E,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,uBAAuB,CAAC,IAAI,CAAC;QAAE,OAAO,GAAG,CAAC;IAElE,IAAI,QAA+B,CAAC;IACpC,IAAI,CAAC;QACJ,QAAQ,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;QAC3E,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,GAAG,CAAC;AACZ,CAAC"}
@@ -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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"}