@selvajs/server 0.2.0-beta.3 → 0.2.0-beta.5

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 (188) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +40 -0
  3. package/dist/compute/client-cache.d.ts +7 -0
  4. package/dist/compute/client-cache.d.ts.map +1 -1
  5. package/dist/compute/client-cache.js +12 -1
  6. package/dist/compute/client-cache.js.map +1 -1
  7. package/dist/compute/index.d.ts +2 -2
  8. package/dist/compute/index.d.ts.map +1 -1
  9. package/dist/compute/index.js +2 -2
  10. package/dist/compute/index.js.map +1 -1
  11. package/dist/compute/limits.d.ts +26 -5
  12. package/dist/compute/limits.d.ts.map +1 -1
  13. package/dist/compute/limits.js +51 -25
  14. package/dist/compute/limits.js.map +1 -1
  15. package/dist/compute/rate-limit.d.ts +40 -0
  16. package/dist/compute/rate-limit.d.ts.map +1 -1
  17. package/dist/compute/rate-limit.js +72 -1
  18. package/dist/compute/rate-limit.js.map +1 -1
  19. package/dist/compute/remote-definition.d.ts +7 -0
  20. package/dist/compute/remote-definition.d.ts.map +1 -1
  21. package/dist/compute/remote-definition.js +3 -1
  22. package/dist/compute/remote-definition.js.map +1 -1
  23. package/dist/compute/safe-url.d.ts +2 -1
  24. package/dist/compute/safe-url.d.ts.map +1 -1
  25. package/dist/compute/safe-url.js +31 -1
  26. package/dist/compute/safe-url.js.map +1 -1
  27. package/dist/compute/solve-cache-key.d.ts +5 -6
  28. package/dist/compute/solve-cache-key.d.ts.map +1 -1
  29. package/dist/compute/solve-cache-key.js +4 -3
  30. package/dist/compute/solve-cache-key.js.map +1 -1
  31. package/dist/compute/solve-cache-single-flight.d.ts +8 -1
  32. package/dist/compute/solve-cache-single-flight.d.ts.map +1 -1
  33. package/dist/compute/solve-cache-single-flight.js +4 -2
  34. package/dist/compute/solve-cache-single-flight.js.map +1 -1
  35. package/dist/compute/solve-pipeline.d.ts +31 -0
  36. package/dist/compute/solve-pipeline.d.ts.map +1 -1
  37. package/dist/compute/solve-pipeline.js +42 -1
  38. package/dist/compute/solve-pipeline.js.map +1 -1
  39. package/dist/definitions/load-for-render.d.ts +10 -1
  40. package/dist/definitions/load-for-render.d.ts.map +1 -1
  41. package/dist/definitions/load-for-render.js +33 -6
  42. package/dist/definitions/load-for-render.js.map +1 -1
  43. package/dist/logging/PinoLogger.d.ts +39 -0
  44. package/dist/logging/PinoLogger.d.ts.map +1 -0
  45. package/dist/logging/PinoLogger.js +189 -0
  46. package/dist/logging/PinoLogger.js.map +1 -0
  47. package/dist/logging/index.d.ts +3 -0
  48. package/dist/logging/index.d.ts.map +1 -0
  49. package/dist/logging/index.js +5 -0
  50. package/dist/logging/index.js.map +1 -0
  51. package/dist/logging/requestId.d.ts +45 -0
  52. package/dist/logging/requestId.d.ts.map +1 -0
  53. package/dist/logging/requestId.js +82 -0
  54. package/dist/logging/requestId.js.map +1 -0
  55. package/dist/providers/create-selva-providers.d.ts +11 -1
  56. package/dist/providers/create-selva-providers.d.ts.map +1 -1
  57. package/dist/providers/create-selva-providers.js +21 -8
  58. package/dist/providers/create-selva-providers.js.map +1 -1
  59. package/package.json +23 -19
  60. package/dist/access/__tests__/project-access-input.test.d.ts +0 -2
  61. package/dist/access/__tests__/project-access-input.test.d.ts.map +0 -1
  62. package/dist/access/__tests__/project-access-input.test.js +0 -146
  63. package/dist/access/__tests__/project-access-input.test.js.map +0 -1
  64. package/dist/compute/__tests__/client-cache.test.d.ts +0 -11
  65. package/dist/compute/__tests__/client-cache.test.d.ts.map +0 -1
  66. package/dist/compute/__tests__/client-cache.test.js +0 -218
  67. package/dist/compute/__tests__/client-cache.test.js.map +0 -1
  68. package/dist/compute/__tests__/definition-byte-cache.test.d.ts +0 -2
  69. package/dist/compute/__tests__/definition-byte-cache.test.d.ts.map +0 -1
  70. package/dist/compute/__tests__/definition-byte-cache.test.js +0 -107
  71. package/dist/compute/__tests__/definition-byte-cache.test.js.map +0 -1
  72. package/dist/compute/__tests__/rate-limit.test.d.ts +0 -2
  73. package/dist/compute/__tests__/rate-limit.test.d.ts.map +0 -1
  74. package/dist/compute/__tests__/rate-limit.test.js +0 -98
  75. package/dist/compute/__tests__/rate-limit.test.js.map +0 -1
  76. package/dist/compute/__tests__/remote-definition.test.d.ts +0 -11
  77. package/dist/compute/__tests__/remote-definition.test.d.ts.map +0 -1
  78. package/dist/compute/__tests__/remote-definition.test.js +0 -114
  79. package/dist/compute/__tests__/remote-definition.test.js.map +0 -1
  80. package/dist/compute/__tests__/safe-url.test.d.ts +0 -2
  81. package/dist/compute/__tests__/safe-url.test.d.ts.map +0 -1
  82. package/dist/compute/__tests__/safe-url.test.js +0 -71
  83. package/dist/compute/__tests__/safe-url.test.js.map +0 -1
  84. package/dist/compute/__tests__/solve-cache.test.d.ts +0 -13
  85. package/dist/compute/__tests__/solve-cache.test.d.ts.map +0 -1
  86. package/dist/compute/__tests__/solve-cache.test.js +0 -234
  87. package/dist/compute/__tests__/solve-cache.test.js.map +0 -1
  88. package/dist/compute/__tests__/solve-pipeline.test.d.ts +0 -11
  89. package/dist/compute/__tests__/solve-pipeline.test.d.ts.map +0 -1
  90. package/dist/compute/__tests__/solve-pipeline.test.js +0 -557
  91. package/dist/compute/__tests__/solve-pipeline.test.js.map +0 -1
  92. package/dist/compute/__tests__/transform-input.test.d.ts +0 -14
  93. package/dist/compute/__tests__/transform-input.test.d.ts.map +0 -1
  94. package/dist/compute/__tests__/transform-input.test.js +0 -120
  95. package/dist/compute/__tests__/transform-input.test.js.map +0 -1
  96. package/dist/definitions/__tests__/definition-service.test.d.ts +0 -14
  97. package/dist/definitions/__tests__/definition-service.test.d.ts.map +0 -1
  98. package/dist/definitions/__tests__/definition-service.test.js +0 -95
  99. package/dist/definitions/__tests__/definition-service.test.js.map +0 -1
  100. package/dist/definitions/__tests__/load-for-render.test.d.ts +0 -8
  101. package/dist/definitions/__tests__/load-for-render.test.d.ts.map +0 -1
  102. package/dist/definitions/__tests__/load-for-render.test.js +0 -196
  103. package/dist/definitions/__tests__/load-for-render.test.js.map +0 -1
  104. package/dist/definitions/__tests__/schema-extraction.test.d.ts +0 -11
  105. package/dist/definitions/__tests__/schema-extraction.test.d.ts.map +0 -1
  106. package/dist/definitions/__tests__/schema-extraction.test.js +0 -115
  107. package/dist/definitions/__tests__/schema-extraction.test.js.map +0 -1
  108. package/dist/http/__tests__/body-size.test.d.ts +0 -2
  109. package/dist/http/__tests__/body-size.test.d.ts.map +0 -1
  110. package/dist/http/__tests__/body-size.test.js +0 -24
  111. package/dist/http/__tests__/body-size.test.js.map +0 -1
  112. package/dist/http/__tests__/route-classifier.test.d.ts +0 -2
  113. package/dist/http/__tests__/route-classifier.test.d.ts.map +0 -1
  114. package/dist/http/__tests__/route-classifier.test.js +0 -54
  115. package/dist/http/__tests__/route-classifier.test.js.map +0 -1
  116. package/dist/http/__tests__/safe-redirect.test.d.ts +0 -2
  117. package/dist/http/__tests__/safe-redirect.test.d.ts.map +0 -1
  118. package/dist/http/__tests__/safe-redirect.test.js +0 -44
  119. package/dist/http/__tests__/safe-redirect.test.js.map +0 -1
  120. package/dist/http/__tests__/security-headers.test.d.ts +0 -2
  121. package/dist/http/__tests__/security-headers.test.d.ts.map +0 -1
  122. package/dist/http/__tests__/security-headers.test.js +0 -24
  123. package/dist/http/__tests__/security-headers.test.js.map +0 -1
  124. package/dist/ops/__tests__/semver.test.d.ts +0 -2
  125. package/dist/ops/__tests__/semver.test.d.ts.map +0 -1
  126. package/dist/ops/__tests__/semver.test.js +0 -38
  127. package/dist/ops/__tests__/semver.test.js.map +0 -1
  128. package/dist/providers/__tests__/create-selva-providers.test.d.ts +0 -7
  129. package/dist/providers/__tests__/create-selva-providers.test.d.ts.map +0 -1
  130. package/dist/providers/__tests__/create-selva-providers.test.js +0 -135
  131. package/dist/providers/__tests__/create-selva-providers.test.js.map +0 -1
  132. package/dist/tokens/__tests__/token-codec.test.d.ts +0 -2
  133. package/dist/tokens/__tests__/token-codec.test.d.ts.map +0 -1
  134. package/dist/tokens/__tests__/token-codec.test.js +0 -50
  135. package/dist/tokens/__tests__/token-codec.test.js.map +0 -1
  136. package/src/access/__tests__/project-access-input.test.ts +0 -175
  137. package/src/access/index.ts +0 -9
  138. package/src/access/project-access-input.ts +0 -145
  139. package/src/compute/__tests__/client-cache.test.ts +0 -244
  140. package/src/compute/__tests__/definition-byte-cache.test.ts +0 -129
  141. package/src/compute/__tests__/rate-limit.test.ts +0 -106
  142. package/src/compute/__tests__/remote-definition.test.ts +0 -140
  143. package/src/compute/__tests__/safe-url.test.ts +0 -72
  144. package/src/compute/__tests__/solve-cache.test.ts +0 -268
  145. package/src/compute/__tests__/solve-pipeline.test.ts +0 -660
  146. package/src/compute/__tests__/transform-input.test.ts +0 -162
  147. package/src/compute/client-cache.ts +0 -321
  148. package/src/compute/definition-byte-cache.ts +0 -168
  149. package/src/compute/index.ts +0 -82
  150. package/src/compute/limits.ts +0 -237
  151. package/src/compute/memory-solve-cache.ts +0 -0
  152. package/src/compute/rate-limit.ts +0 -110
  153. package/src/compute/remote-definition.ts +0 -149
  154. package/src/compute/safe-url.ts +0 -186
  155. package/src/compute/solve-cache-envelope.ts +0 -98
  156. package/src/compute/solve-cache-key.ts +0 -85
  157. package/src/compute/solve-cache-single-flight.ts +0 -59
  158. package/src/compute/solve-pipeline.ts +0 -603
  159. package/src/compute/transform-input.ts +0 -66
  160. package/src/definitions/__tests__/definition-service.test.ts +0 -131
  161. package/src/definitions/__tests__/load-for-render.test.ts +0 -244
  162. package/src/definitions/__tests__/schema-extraction.test.ts +0 -137
  163. package/src/definitions/definition-service.ts +0 -247
  164. package/src/definitions/index.ts +0 -22
  165. package/src/definitions/load-for-render.ts +0 -272
  166. package/src/definitions/schema-extraction.ts +0 -117
  167. package/src/errors/SentryErrorReporter.ts +0 -88
  168. package/src/errors/index.ts +0 -3
  169. package/src/http/__tests__/body-size.test.ts +0 -27
  170. package/src/http/__tests__/route-classifier.test.ts +0 -60
  171. package/src/http/__tests__/safe-redirect.test.ts +0 -47
  172. package/src/http/__tests__/security-headers.test.ts +0 -29
  173. package/src/http/body-size.ts +0 -24
  174. package/src/http/index.ts +0 -10
  175. package/src/http/redirect.ts +0 -16
  176. package/src/http/route-classifier.ts +0 -74
  177. package/src/http/security-headers.ts +0 -42
  178. package/src/index.ts +0 -27
  179. package/src/ops/__tests__/semver.test.ts +0 -42
  180. package/src/ops/index.ts +0 -3
  181. package/src/ops/semver.ts +0 -60
  182. package/src/providers/__tests__/create-selva-providers.test.ts +0 -172
  183. package/src/providers/__tests__/fixtures/override-config.mjs +0 -8
  184. package/src/providers/create-selva-providers.ts +0 -241
  185. package/src/providers/index.ts +0 -12
  186. package/src/tokens/__tests__/token-codec.test.ts +0 -63
  187. package/src/tokens/index.ts +0 -8
  188. package/src/tokens/token-codec.ts +0 -86
@@ -1,82 +0,0 @@
1
- // Compute/solve server building blocks — transport-agnostic, env-injected.
2
-
3
- export {
4
- resolveComputeLimits,
5
- readPositiveInt,
6
- readNonNegativeInt,
7
- readBool,
8
- type ComputeLimits,
9
- type EnvRecord
10
- } from './limits.js';
11
-
12
- export {
13
- createDefinitionByteCache,
14
- type DefinitionByteCache,
15
- type ByteCacheRef,
16
- type ByteCacheStats
17
- } from './definition-byte-cache.js';
18
-
19
- export {
20
- createMemorySolveResultCache,
21
- type MemorySolveResultCache,
22
- type SolveCacheStats
23
- } from './memory-solve-cache.js';
24
-
25
- export {
26
- deriveSolveCacheInputKey,
27
- type SolveCacheConfigSubset,
28
- type SolveCacheInputKey
29
- } from './solve-cache-key.js';
30
-
31
- export {
32
- encodeSolveCacheEntry,
33
- decodeSolveCacheEntry,
34
- gunzipEntryBody,
35
- type EnvelopeHeader,
36
- type DecodedSolveCacheEntry
37
- } from './solve-cache-envelope.js';
38
-
39
- export {
40
- createSolveCacheSingleFlight,
41
- type SolveCacheSingleFlight
42
- } from './solve-cache-single-flight.js';
43
-
44
- export {
45
- createComputeRateLimiter,
46
- type ComputeRateLimiter,
47
- type ComputeRateLimiterConfig,
48
- type RateLimitResult
49
- } from './rate-limit.js';
50
-
51
- export { isSafeRemoteDefinitionUrl, assertSafeRemoteDefinitionUrl } from './safe-url.js';
52
-
53
- export { transformInputParameter } from './transform-input.js';
54
-
55
- export {
56
- createClientCache,
57
- serverIdentity,
58
- type ClientCache,
59
- type ClientCacheConfig,
60
- type CachedClient,
61
- type ResolvedServer,
62
- type ServerIdentity
63
- } from './client-cache.js';
64
-
65
- export {
66
- createRemoteDefinitionFetcher,
67
- readBodyWithCap,
68
- type RemoteDefinitionFetcher,
69
- type RemoteDefinitionConfig
70
- } from './remote-definition.js';
71
-
72
- export {
73
- runSolvePipeline,
74
- COMPUTE_CONTRACT_VERSION,
75
- COMPUTE_VERSION_HEADER,
76
- type SolvePipelineArgs,
77
- type SolvePipelineCacheHook,
78
- type SolveOutcome,
79
- type SolveEnvelope,
80
- type SolvePhaseMetrics,
81
- type PipelineInput
82
- } from './solve-pipeline.js';
@@ -1,237 +0,0 @@
1
- /**
2
- * Compute/solve server limits, resolved from an injected env map.
3
- *
4
- * This is the transport- and framework-agnostic core: it takes a plain
5
- * `Record<string, string | undefined>` (the caller passes SvelteKit's
6
- * `$env/dynamic/private`, `process.env`, or any config source) and returns a
7
- * fully-resolved `ComputeLimits` object. No ambient `$env` / `process.env`
8
- * read happens here, so the module is importable from any runtime and testable
9
- * without env plumbing.
10
- *
11
- * Every knob is env-overridable so deployments can tune without code changes;
12
- * the defaults are sized for typical interactive use (sliders + small uploads).
13
- * Centralizing them surfaces the tradeoffs (raise the rate cap → also consider
14
- * the body cap) at one glance.
15
- *
16
- * Client-side counterparts (the in-flight compute throttle and slider debounce)
17
- * live in `@selvajs/ui` and are bundled into the client, so they can't read
18
- * env — they carry their own defaults.
19
- */
20
-
21
- const MB = 1024 * 1024;
22
-
23
- /** Env source: any string→string map. `$env/dynamic/private` satisfies this. */
24
- export type EnvRecord = Record<string, string | undefined>;
25
-
26
- /**
27
- * Parse a positive integer env value. Returns `fallback` when absent, and warns
28
- * + falls back when present but not a finite positive number.
29
- */
30
- export function readPositiveInt(env: EnvRecord, name: string, fallback: number): number {
31
- const raw = env[name];
32
- if (!raw) return fallback;
33
- const parsed = Number(raw);
34
- if (!Number.isFinite(parsed) || parsed <= 0) {
35
- console.warn(`[selva] Invalid ${name}=${raw}, falling back to ${fallback}`);
36
- return fallback;
37
- }
38
- return Math.floor(parsed);
39
- }
40
-
41
- /**
42
- * Parse a non-negative integer env value — like {@link readPositiveInt} but `0`
43
- * is a valid value (used by knobs where `0` means "disable", e.g. a cache
44
- * budget). Returns `fallback` when absent, and warns + falls back when present
45
- * but not a finite non-negative number.
46
- */
47
- export function readNonNegativeInt(env: EnvRecord, name: string, fallback: number): number {
48
- const raw = env[name];
49
- if (!raw) return fallback;
50
- const parsed = Number(raw);
51
- if (!Number.isFinite(parsed) || parsed < 0) {
52
- console.warn(`[selva] Invalid ${name}=${raw}, falling back to ${fallback}`);
53
- return fallback;
54
- }
55
- return Math.floor(parsed);
56
- }
57
-
58
- /**
59
- * Parse a boolean env flag. Accepts `true/1/yes/on` (case-insensitive) as true
60
- * and `false/0/no/off` as false; any other / absent value falls back.
61
- */
62
- export function readBool(env: EnvRecord, name: string, fallback: boolean): boolean {
63
- const raw = env[name];
64
- if (raw == null || raw === '') return fallback;
65
- const v = raw.trim().toLowerCase();
66
- if (v === 'true' || v === '1' || v === 'yes' || v === 'on') return true;
67
- if (v === 'false' || v === '0' || v === 'no' || v === 'off') return false;
68
- console.warn(`[selva] Invalid ${name}=${raw}, falling back to ${fallback}`);
69
- return fallback;
70
- }
71
-
72
- /** Fully-resolved server-side compute limits. */
73
- export interface ComputeLimits {
74
- /**
75
- * Longest the app waits for one solve. Enforced by the SolveScheduler
76
- * (`@selvajs/compute`), which propagates AbortSignal into the upstream
77
- * Compute call — a timeout actually cancels the work. Forwarded to the
78
- * client to drive the in-browser AbortController.
79
- *
80
- * Caveat: bounds only the parts of the stack we own. A reverse proxy or
81
- * serverless platform cap may shoot the request sooner; bumping past those
82
- * caps produces 502s, not longer solves.
83
- */
84
- maxSolveDurationMs: number;
85
- /** Fixed-window cap on /api/compute: window length + max requests per window. */
86
- rateLimitWindowMs: number;
87
- rateLimitMaxRequests: number;
88
- /**
89
- * Largest .gh definition accepted on upload AND the largest remote definition
90
- * fetched for compute — kept in lockstep so a remote URL can't smuggle a file
91
- * past the upload cap.
92
- */
93
- maxGhFileSize: number;
94
- maxImageFileSize: number;
95
- /**
96
- * /api/compute JSON *request* body cap (inputs + values, not the .gh). A
97
- * `file`-widget input embeds geometry as base64 in `values`; must stay <=
98
- * the adapter-node global BODY_SIZE_LIMIT or the global backstop rejects first.
99
- */
100
- computeRequestMaxBytes: number;
101
- /**
102
- * /api/compute JSON *response* cap. A `file`-typed output is base64-embedded
103
- * in the response; guards V8's ~512 MB single-string wall (a `JSON.stringify`
104
- * `RangeError`) and browser-tab OOM. Defensive backstop → clear 413 instead of
105
- * an opaque crash (real fix is out-of-band streaming, ADR 0003).
106
- */
107
- computeResponseMaxBytes: number;
108
- /** Hard cap on fetching a remote definition — tracks `maxGhFileSize`. */
109
- remoteDefinitionMaxBytes: number;
110
- /** Deadline on fetching a remote definition (slow-loris protection). */
111
- remoteDefinitionFetchTimeoutMs: number;
112
- /** In-process cache TTL for remote-fetched .gh bytes. */
113
- definitionCacheTtlMs: number;
114
- /**
115
- * Reuse the server's cached definition via a pointer instead of re-uploading
116
- * the binary each solve. SAFE only on a compute server that signals a
117
- * stale-pointer miss (VektorNode fork / a fork that throws) — a server that
118
- * returns an empty 200 would silently yield EMPTY geometry. Off for unknown
119
- * / standard rhino.compute.
120
- */
121
- computeReuseDefinitionCache: boolean;
122
- /**
123
- * Ask Rhino.Compute to cache solve RESULTS keyed on the full request and
124
- * return a cached result on an identical repeat. Server-wide (survives Selva
125
- * restarts, spans instances). Only helps IDENTICAL re-solves.
126
- */
127
- computeServerCachesolve: boolean;
128
- /**
129
- * Also cache solves that reported Grasshopper errors — correct for
130
- * error-by-design definitions (guarded Python, pruned branches) that still
131
- * produce valid geometry. Requires `computeServerCachesolve` and a server
132
- * honoring the flag. Conservative default off.
133
- */
134
- computeCacheErroredSolves: boolean;
135
- /**
136
- * Max in-flight solves this app instance sends to ONE compute server
137
- * (forwarded as the SolveScheduler's `maxConcurrent`; excess solves FIFO-queue).
138
- * Size it to the server's `compute.geometry` child count — Rhino.Compute
139
- * spawns N children per VM precisely to run N solves concurrently, and a
140
- * lower value idles that capacity while users queue (audit B6: the scheduler's
141
- * queue-mode default is 1, which serialized ALL solves per server). Multiple
142
- * app instances each apply their own cap.
143
- */
144
- computeMaxConcurrentSolves: number;
145
- /**
146
- * Backpressure — max solves allowed to WAIT in the per-server FIFO queue (i.e.
147
- * excluding the `computeMaxConcurrentSolves` already in flight). A solve that
148
- * arrives to a full queue is shed immediately (scheduler `QUEUE_FULL`, mapped
149
- * to HTTP 503 + `Retry-After`) instead of piling up unbounded. `0` = unbounded
150
- * (the pre-shedding default): the queue grows without limit, matching prior
151
- * behavior. Size it to roughly 2–3× `computeMaxConcurrentSolves` once the
152
- * Rhino pool's real capacity is known — shedding fast beats a hung request.
153
- */
154
- computeMaxQueueDepth: number;
155
- /**
156
- * Backpressure — longest (ms) a solve may sit QUEUED before it starts
157
- * executing; still-waiting past this it's shed (scheduler `QUEUE_TIMEOUT` →
158
- * HTTP 503 + `Retry-After`) rather than burning compute on a stale request.
159
- * Bounds tail latency: the scheduler's `timeoutMs` clock starts at execution,
160
- * so without this a solve's total wait is unbounded and invisible. `0` = no
161
- * queue deadline (the default). A sensible tuned value is ≈ `maxSolveDurationMs`.
162
- */
163
- computeQueueWaitMs: number;
164
- /**
165
- * Total-byte budget (bytes) for the in-process definition-byte cache keyed on
166
- * immutable version id. A warm entry lets a solve skip `storage.get` entirely,
167
- * and a pointer-known solve never loads bytes at all. `0` disables the cache
168
- * (every solve re-reads storage). Sized as a total-byte LRU because definitions
169
- * span KB→hundreds of MB; env value is MB, converted here.
170
- */
171
- computeDefinitionByteCacheBytes: number;
172
- /**
173
- * Default per-definition entry quota for the durable L2 solve cache (H1), used
174
- * when a definition's `solveCacheLimit` is absent (inherit). A definition may
175
- * override this; `0` on the definition turns caching off for it. This global
176
- * default `0` means the L2 is effectively off until an operator sets a quota —
177
- * safe because the memory backend also ships only under
178
- * `SOLVE_CACHE_PROVIDER=memory`. Counts, not bytes: authors think in "how many
179
- * solves to remember"; the byte backstop below is the operator's memory guard.
180
- */
181
- solveCacheDefaultMaxEntries: number;
182
- /**
183
- * Global byte backstop (bytes) across ALL definitions' L2 entries, evicted
184
- * global-LRU regardless of per-definition counts. Entries range KB→100s of MB,
185
- * so a count quota alone can't bound memory. `0` disables the backstop (rely on
186
- * per-definition counts only — not recommended in the memory backend). Env value
187
- * is MB, converted here. On Redis this moves to `maxmemory allkeys-lru`.
188
- */
189
- solveCacheMaxTotalBytes: number;
190
- }
191
-
192
- /**
193
- * Resolve every server-side compute limit from an env map. Pure: the same env
194
- * always yields the same limits, and nothing is read ambiently. The app calls
195
- * this once at its composition root with `$env/dynamic/private`.
196
- */
197
- export function resolveComputeLimits(env: EnvRecord): ComputeLimits {
198
- const maxGhFileSize = readPositiveInt(env, 'MAX_GH_FILE_SIZE_BYTES', 300 * MB);
199
- return {
200
- maxSolveDurationMs: readPositiveInt(env, 'MAX_SOLVE_DURATION_MS', 100_000),
201
- rateLimitWindowMs: readPositiveInt(env, 'COMPUTE_RATE_LIMIT_WINDOW_MS', 100_000),
202
- rateLimitMaxRequests: readPositiveInt(env, 'COMPUTE_RATE_LIMIT_MAX', 120),
203
- maxGhFileSize,
204
- maxImageFileSize: readPositiveInt(env, 'MAX_IMAGE_FILE_SIZE_BYTES', 10 * MB),
205
- computeRequestMaxBytes: readPositiveInt(env, 'COMPUTE_REQUEST_MAX_BYTES', 300 * MB),
206
- computeResponseMaxBytes: readPositiveInt(env, 'COMPUTE_RESPONSE_MAX_BYTES', 300 * MB),
207
- // Tracks the upload cap so a remote URL can't smuggle a larger file.
208
- remoteDefinitionMaxBytes: maxGhFileSize,
209
- remoteDefinitionFetchTimeoutMs: readPositiveInt(
210
- env,
211
- 'REMOTE_DEFINITION_FETCH_TIMEOUT_MS',
212
- 30_000
213
- ),
214
- definitionCacheTtlMs: readPositiveInt(env, 'DEFINITION_CACHE_TTL_MS', 5 * 60 * 1000),
215
- computeReuseDefinitionCache: readBool(env, 'COMPUTE_REUSE_DEFINITION_CACHE', true),
216
- computeServerCachesolve: readBool(env, 'COMPUTE_SERVER_CACHESOLVE', true),
217
- computeCacheErroredSolves: readBool(env, 'COMPUTE_CACHE_ERRORED_SOLVES', false),
218
- // 4 matches rhino.compute's default --childcount; tune to the actual VM.
219
- computeMaxConcurrentSolves: readPositiveInt(env, 'COMPUTE_MAX_CONCURRENT', 4),
220
- // Both 0 (unbounded / no deadline) by default — nothing sheds until an
221
- // operator who's measured their pool opts in. readNonNegativeInt so `0` is a
222
- // valid "disabled" value, not treated as invalid.
223
- computeMaxQueueDepth: readNonNegativeInt(env, 'COMPUTE_MAX_QUEUE_DEPTH', 0),
224
- computeQueueWaitMs: readNonNegativeInt(env, 'COMPUTE_QUEUE_WAIT_MS', 0),
225
- // Env is MB; 0 disables. Default 256 MB holds a handful of typical
226
- // definitions warm without pinning gigabytes. readNonNegativeInt so `0`
227
- // (disable) is honored rather than treated as invalid.
228
- computeDefinitionByteCacheBytes:
229
- readNonNegativeInt(env, 'COMPUTE_DEFINITION_BYTE_CACHE_MB', 256) * MB,
230
- // L2 durable solve cache (H1). Default per-definition quota 0 = inherit-to-off
231
- // until an operator opts in; the memory backend also only mounts under
232
- // SOLVE_CACHE_PROVIDER=memory, so nothing caches by accident. Byte backstop
233
- // 512 MB caps total heap use across all definitions (env is MB).
234
- solveCacheDefaultMaxEntries: readNonNegativeInt(env, 'SOLVE_CACHE_DEFAULT_MAX_ENTRIES', 0),
235
- solveCacheMaxTotalBytes: readNonNegativeInt(env, 'SOLVE_CACHE_MAX_TOTAL_MB', 512) * MB
236
- };
237
- }
Binary file
@@ -1,110 +0,0 @@
1
- /**
2
- * Per-key fixed-window rate limiter for the compute-solve endpoint. Compute
3
- * solves are the one expensive endpoint in a Selva-engine app — without a cap a
4
- * single authenticated user (or share-link consumer) can burn through compute
5
- * budget far faster than any other surface allows.
6
- *
7
- * Design:
8
- * - Process-local `Map`. Multi-instance deployments see N× the per-key rate —
9
- * acceptable as a first line; a distributed limiter is a follow-up. The state
10
- * lives behind this factory so a shared-store implementation can slot in
11
- * later without changing the call sites.
12
- * - Fixed window (vs sliding) — simpler, no per-request bookkeeping; the
13
- * "burst at window edges" mode is fine here (share-link `maxSolves` and
14
- * per-user auth already prevent unbounded abuse — this flattens short spikes).
15
- * - Keyed by caller — the app passes `user:{userId}` for authenticated solves,
16
- * `share:{linkId}` for share-token solves, so anonymous consumers of one link
17
- * don't share a bucket with the link's owner.
18
- *
19
- * The window + cap are injected (they live in `ComputeLimits`), so this module
20
- * reads no env of its own.
21
- *
22
- * Not compute-only despite the name: `peek` + `clear` make it usable for
23
- * failure-counting flows too (login limiting keys by IP, records only failed
24
- * attempts via `check`, and forgives on success via `clear`).
25
- */
26
-
27
- export interface RateLimitResult {
28
- allowed: boolean;
29
- /** Seconds until the bucket resets, when not allowed. */
30
- retryAfter?: number;
31
- }
32
-
33
- export interface ComputeRateLimiterConfig {
34
- /** Fixed-window length in ms. */
35
- windowMs: number;
36
- /** Max requests admitted per key per window. */
37
- maxPerWindow: number;
38
- }
39
-
40
- export interface ComputeRateLimiter {
41
- /**
42
- * Check + record one request against `key`. Increments the counter on allow.
43
- * Returns `{ allowed: false, retryAfter }` once the bucket is full.
44
- */
45
- check(key: string): RateLimitResult;
46
- /**
47
- * Check WITHOUT recording. For flows that only count selected outcomes —
48
- * e.g. login limiting, where `peek` gates the attempt and `check` records
49
- * only failures (a success calls `clear` instead).
50
- */
51
- peek(key: string): RateLimitResult;
52
- /** Drop `key`'s bucket — e.g. a successful login forgives prior failures. */
53
- clear(key: string): void;
54
- /** Test seam — wipes in-memory state. Production code never calls this. */
55
- reset(): void;
56
- /** The resolved config, exposed for time-based test assertions. */
57
- readonly config: Readonly<ComputeRateLimiterConfig>;
58
- }
59
-
60
- interface BucketEntry {
61
- count: number;
62
- resetAt: number;
63
- }
64
-
65
- /**
66
- * Build a process-local fixed-window rate limiter. Each limiter owns its own
67
- * bucket `Map`, so distinct endpoints/keys-spaces don't collide.
68
- */
69
- export function createComputeRateLimiter(config: ComputeRateLimiterConfig): ComputeRateLimiter {
70
- const { windowMs, maxPerWindow } = config;
71
- const buckets = new Map<string, BucketEntry>();
72
-
73
- return {
74
- config,
75
- check(key: string): RateLimitResult {
76
- const now = Date.now();
77
- const entry = buckets.get(key);
78
-
79
- if (!entry || now > entry.resetAt) {
80
- buckets.set(key, { count: 1, resetAt: now + windowMs });
81
- return { allowed: true };
82
- }
83
-
84
- if (entry.count >= maxPerWindow) {
85
- return { allowed: false, retryAfter: Math.ceil((entry.resetAt - now) / 1000) };
86
- }
87
-
88
- entry.count += 1;
89
- return { allowed: true };
90
- },
91
- peek(key: string): RateLimitResult {
92
- const now = Date.now();
93
- const entry = buckets.get(key);
94
-
95
- if (!entry || now > entry.resetAt) {
96
- return { allowed: true };
97
- }
98
- if (entry.count >= maxPerWindow) {
99
- return { allowed: false, retryAfter: Math.ceil((entry.resetAt - now) / 1000) };
100
- }
101
- return { allowed: true };
102
- },
103
- clear(key: string): void {
104
- buckets.delete(key);
105
- },
106
- reset(): void {
107
- buckets.clear();
108
- }
109
- };
110
- }
@@ -1,149 +0,0 @@
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
-
18
- import { assertSafeRemoteDefinitionUrl } from './safe-url.js';
19
-
20
- /** Injected limits — the app derives these from its env-resolved `ComputeLimits`. */
21
- export interface RemoteDefinitionConfig {
22
- /** Hard cap on the fetched body; a lying/absent content-length can't exceed it. */
23
- maxBytes: number;
24
- /** Deadline on the fetch (slow-loris protection). */
25
- fetchTimeoutMs: number;
26
- /** How long a fetched definition stays warm in the in-process cache. */
27
- cacheTtlMs: number;
28
- /**
29
- * Current epoch millis. Injected (not read via `Date.now()` ambiently) so the
30
- * TTL is testable and the module stays side-effect-free. The app binding
31
- * passes `() => Date.now()`.
32
- */
33
- now: () => number;
34
- }
35
-
36
- export interface RemoteDefinitionFetcher {
37
- /**
38
- * Fetch (or return cached) `.gh` bytes for a remote URL. Throws on an unsafe
39
- * host, a non-2xx response, an oversized body, or a timeout — the caller maps
40
- * the failure to a 400.
41
- */
42
- load(url: string): Promise<Uint8Array>;
43
- }
44
-
45
- /** Once the cache exceeds this many entries, evict the oldest 10 by `fetchedAt`. */
46
- const CACHE_MAX_ENTRIES = 50;
47
- const CACHE_EVICT_BATCH = 10;
48
-
49
- /**
50
- * Read a response body into memory, aborting (and throwing) as soon as the
51
- * running byte total exceeds `maxBytes`. Falls back to `arrayBuffer()` only when
52
- * the body isn't a readable stream (older fetch impls). Exported for direct use
53
- * and reuse; the fetcher below wraps it with SSRF + TTL.
54
- */
55
- export async function readBodyWithCap(
56
- response: Response,
57
- maxBytes: number,
58
- controller: AbortController
59
- ): Promise<Uint8Array> {
60
- if (!response.body) {
61
- const buffer = await response.arrayBuffer();
62
- if (buffer.byteLength > maxBytes) throw new Error('Remote definition exceeds size limit');
63
- return new Uint8Array(buffer);
64
- }
65
-
66
- const reader = response.body.getReader();
67
- const chunks: Uint8Array[] = [];
68
- let total = 0;
69
- while (true) {
70
- const { done, value } = await reader.read();
71
- if (done) break;
72
- total += value.byteLength;
73
- if (total > maxBytes) {
74
- controller.abort();
75
- throw new Error('Remote definition exceeds size limit');
76
- }
77
- chunks.push(value);
78
- }
79
-
80
- const out = new Uint8Array(total);
81
- let offset = 0;
82
- for (const chunk of chunks) {
83
- out.set(chunk, offset);
84
- offset += chunk.byteLength;
85
- }
86
- return out;
87
- }
88
-
89
- /**
90
- * Build a remote-definition fetcher owning its own TTL cache. Each fetcher's
91
- * cache is independent, so one per process shares warm bytes while test fetchers
92
- * stay isolated.
93
- */
94
- export function createRemoteDefinitionFetcher(
95
- config: RemoteDefinitionConfig
96
- ): RemoteDefinitionFetcher {
97
- const cache = new Map<string, { data: Uint8Array; fetchedAt: number }>();
98
-
99
- return {
100
- async load(url: string): Promise<Uint8Array> {
101
- // Resolves the host and rejects when any resolved IP is private/loopback/
102
- // link-local — covers literal-encoding bypasses (integer/octal/hex/short-form,
103
- // IPv4-mapped IPv6) and public names that point inward. Throws on rejection.
104
- await assertSafeRemoteDefinitionUrl(url);
105
-
106
- const now = config.now();
107
- const cached = cache.get(url);
108
- if (cached && now - cached.fetchedAt < config.cacheTtlMs) {
109
- return cached.data;
110
- }
111
-
112
- const controller = new AbortController();
113
- const timeout = setTimeout(() => controller.abort(), config.fetchTimeoutMs);
114
- let data: Uint8Array;
115
- try {
116
- const response = await fetch(url, {
117
- signal: controller.signal,
118
- redirect: 'error'
119
- });
120
- if (!response.ok) {
121
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
122
- }
123
- // Reject early when the server declares an oversized body.
124
- const declared = Number(response.headers.get('content-length'));
125
- if (Number.isFinite(declared) && declared > config.maxBytes) {
126
- throw new Error('Remote definition exceeds size limit');
127
- }
128
- // Stream and count rather than `arrayBuffer()` — a missing/lying
129
- // content-length must not let an unbounded body buffer into memory
130
- // before the cap is checked. Abort the moment we cross the limit.
131
- data = await readBodyWithCap(response, config.maxBytes, controller);
132
- } finally {
133
- clearTimeout(timeout);
134
- }
135
-
136
- cache.set(url, { data, fetchedAt: now });
137
-
138
- if (cache.size > CACHE_MAX_ENTRIES) {
139
- const entries = Array.from(cache.entries());
140
- entries.sort((a, b) => a[1].fetchedAt - b[1].fetchedAt);
141
- for (let i = 0; i < CACHE_EVICT_BATCH; i++) {
142
- cache.delete(entries[i][0]);
143
- }
144
- }
145
-
146
- return data;
147
- }
148
- };
149
- }