@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.
- package/LICENSE +1 -1
- package/README.md +40 -0
- package/dist/compute/client-cache.d.ts +7 -0
- package/dist/compute/client-cache.d.ts.map +1 -1
- package/dist/compute/client-cache.js +12 -1
- package/dist/compute/client-cache.js.map +1 -1
- package/dist/compute/index.d.ts +2 -2
- package/dist/compute/index.d.ts.map +1 -1
- package/dist/compute/index.js +2 -2
- package/dist/compute/index.js.map +1 -1
- package/dist/compute/limits.d.ts +26 -5
- package/dist/compute/limits.d.ts.map +1 -1
- package/dist/compute/limits.js +51 -25
- package/dist/compute/limits.js.map +1 -1
- package/dist/compute/rate-limit.d.ts +40 -0
- package/dist/compute/rate-limit.d.ts.map +1 -1
- package/dist/compute/rate-limit.js +72 -1
- package/dist/compute/rate-limit.js.map +1 -1
- package/dist/compute/remote-definition.d.ts +7 -0
- package/dist/compute/remote-definition.d.ts.map +1 -1
- package/dist/compute/remote-definition.js +3 -1
- package/dist/compute/remote-definition.js.map +1 -1
- package/dist/compute/safe-url.d.ts +2 -1
- package/dist/compute/safe-url.d.ts.map +1 -1
- package/dist/compute/safe-url.js +31 -1
- package/dist/compute/safe-url.js.map +1 -1
- package/dist/compute/solve-cache-key.d.ts +5 -6
- package/dist/compute/solve-cache-key.d.ts.map +1 -1
- package/dist/compute/solve-cache-key.js +4 -3
- package/dist/compute/solve-cache-key.js.map +1 -1
- package/dist/compute/solve-cache-single-flight.d.ts +8 -1
- package/dist/compute/solve-cache-single-flight.d.ts.map +1 -1
- package/dist/compute/solve-cache-single-flight.js +4 -2
- package/dist/compute/solve-cache-single-flight.js.map +1 -1
- package/dist/compute/solve-pipeline.d.ts +31 -0
- package/dist/compute/solve-pipeline.d.ts.map +1 -1
- package/dist/compute/solve-pipeline.js +42 -1
- package/dist/compute/solve-pipeline.js.map +1 -1
- package/dist/definitions/load-for-render.d.ts +10 -1
- package/dist/definitions/load-for-render.d.ts.map +1 -1
- package/dist/definitions/load-for-render.js +33 -6
- package/dist/definitions/load-for-render.js.map +1 -1
- package/dist/logging/PinoLogger.d.ts +39 -0
- package/dist/logging/PinoLogger.d.ts.map +1 -0
- package/dist/logging/PinoLogger.js +189 -0
- package/dist/logging/PinoLogger.js.map +1 -0
- package/dist/logging/index.d.ts +3 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/logging/index.js +5 -0
- package/dist/logging/index.js.map +1 -0
- package/dist/logging/requestId.d.ts +45 -0
- package/dist/logging/requestId.d.ts.map +1 -0
- package/dist/logging/requestId.js +82 -0
- package/dist/logging/requestId.js.map +1 -0
- package/dist/providers/create-selva-providers.d.ts +11 -1
- package/dist/providers/create-selva-providers.d.ts.map +1 -1
- package/dist/providers/create-selva-providers.js +21 -8
- package/dist/providers/create-selva-providers.js.map +1 -1
- package/package.json +23 -19
- package/dist/access/__tests__/project-access-input.test.d.ts +0 -2
- package/dist/access/__tests__/project-access-input.test.d.ts.map +0 -1
- package/dist/access/__tests__/project-access-input.test.js +0 -146
- package/dist/access/__tests__/project-access-input.test.js.map +0 -1
- package/dist/compute/__tests__/client-cache.test.d.ts +0 -11
- package/dist/compute/__tests__/client-cache.test.d.ts.map +0 -1
- package/dist/compute/__tests__/client-cache.test.js +0 -218
- package/dist/compute/__tests__/client-cache.test.js.map +0 -1
- package/dist/compute/__tests__/definition-byte-cache.test.d.ts +0 -2
- package/dist/compute/__tests__/definition-byte-cache.test.d.ts.map +0 -1
- package/dist/compute/__tests__/definition-byte-cache.test.js +0 -107
- package/dist/compute/__tests__/definition-byte-cache.test.js.map +0 -1
- package/dist/compute/__tests__/rate-limit.test.d.ts +0 -2
- package/dist/compute/__tests__/rate-limit.test.d.ts.map +0 -1
- package/dist/compute/__tests__/rate-limit.test.js +0 -98
- package/dist/compute/__tests__/rate-limit.test.js.map +0 -1
- package/dist/compute/__tests__/remote-definition.test.d.ts +0 -11
- package/dist/compute/__tests__/remote-definition.test.d.ts.map +0 -1
- package/dist/compute/__tests__/remote-definition.test.js +0 -114
- package/dist/compute/__tests__/remote-definition.test.js.map +0 -1
- package/dist/compute/__tests__/safe-url.test.d.ts +0 -2
- package/dist/compute/__tests__/safe-url.test.d.ts.map +0 -1
- package/dist/compute/__tests__/safe-url.test.js +0 -71
- package/dist/compute/__tests__/safe-url.test.js.map +0 -1
- package/dist/compute/__tests__/solve-cache.test.d.ts +0 -13
- package/dist/compute/__tests__/solve-cache.test.d.ts.map +0 -1
- package/dist/compute/__tests__/solve-cache.test.js +0 -234
- package/dist/compute/__tests__/solve-cache.test.js.map +0 -1
- package/dist/compute/__tests__/solve-pipeline.test.d.ts +0 -11
- package/dist/compute/__tests__/solve-pipeline.test.d.ts.map +0 -1
- package/dist/compute/__tests__/solve-pipeline.test.js +0 -557
- package/dist/compute/__tests__/solve-pipeline.test.js.map +0 -1
- package/dist/compute/__tests__/transform-input.test.d.ts +0 -14
- package/dist/compute/__tests__/transform-input.test.d.ts.map +0 -1
- package/dist/compute/__tests__/transform-input.test.js +0 -120
- package/dist/compute/__tests__/transform-input.test.js.map +0 -1
- package/dist/definitions/__tests__/definition-service.test.d.ts +0 -14
- package/dist/definitions/__tests__/definition-service.test.d.ts.map +0 -1
- package/dist/definitions/__tests__/definition-service.test.js +0 -95
- package/dist/definitions/__tests__/definition-service.test.js.map +0 -1
- package/dist/definitions/__tests__/load-for-render.test.d.ts +0 -8
- package/dist/definitions/__tests__/load-for-render.test.d.ts.map +0 -1
- package/dist/definitions/__tests__/load-for-render.test.js +0 -196
- package/dist/definitions/__tests__/load-for-render.test.js.map +0 -1
- package/dist/definitions/__tests__/schema-extraction.test.d.ts +0 -11
- package/dist/definitions/__tests__/schema-extraction.test.d.ts.map +0 -1
- package/dist/definitions/__tests__/schema-extraction.test.js +0 -115
- package/dist/definitions/__tests__/schema-extraction.test.js.map +0 -1
- package/dist/http/__tests__/body-size.test.d.ts +0 -2
- package/dist/http/__tests__/body-size.test.d.ts.map +0 -1
- package/dist/http/__tests__/body-size.test.js +0 -24
- package/dist/http/__tests__/body-size.test.js.map +0 -1
- package/dist/http/__tests__/route-classifier.test.d.ts +0 -2
- package/dist/http/__tests__/route-classifier.test.d.ts.map +0 -1
- package/dist/http/__tests__/route-classifier.test.js +0 -54
- package/dist/http/__tests__/route-classifier.test.js.map +0 -1
- package/dist/http/__tests__/safe-redirect.test.d.ts +0 -2
- package/dist/http/__tests__/safe-redirect.test.d.ts.map +0 -1
- package/dist/http/__tests__/safe-redirect.test.js +0 -44
- package/dist/http/__tests__/safe-redirect.test.js.map +0 -1
- package/dist/http/__tests__/security-headers.test.d.ts +0 -2
- package/dist/http/__tests__/security-headers.test.d.ts.map +0 -1
- package/dist/http/__tests__/security-headers.test.js +0 -24
- package/dist/http/__tests__/security-headers.test.js.map +0 -1
- package/dist/ops/__tests__/semver.test.d.ts +0 -2
- package/dist/ops/__tests__/semver.test.d.ts.map +0 -1
- package/dist/ops/__tests__/semver.test.js +0 -38
- package/dist/ops/__tests__/semver.test.js.map +0 -1
- package/dist/providers/__tests__/create-selva-providers.test.d.ts +0 -7
- package/dist/providers/__tests__/create-selva-providers.test.d.ts.map +0 -1
- package/dist/providers/__tests__/create-selva-providers.test.js +0 -135
- package/dist/providers/__tests__/create-selva-providers.test.js.map +0 -1
- package/dist/tokens/__tests__/token-codec.test.d.ts +0 -2
- package/dist/tokens/__tests__/token-codec.test.d.ts.map +0 -1
- package/dist/tokens/__tests__/token-codec.test.js +0 -50
- package/dist/tokens/__tests__/token-codec.test.js.map +0 -1
- package/src/access/__tests__/project-access-input.test.ts +0 -175
- package/src/access/index.ts +0 -9
- package/src/access/project-access-input.ts +0 -145
- package/src/compute/__tests__/client-cache.test.ts +0 -244
- package/src/compute/__tests__/definition-byte-cache.test.ts +0 -129
- package/src/compute/__tests__/rate-limit.test.ts +0 -106
- package/src/compute/__tests__/remote-definition.test.ts +0 -140
- package/src/compute/__tests__/safe-url.test.ts +0 -72
- package/src/compute/__tests__/solve-cache.test.ts +0 -268
- package/src/compute/__tests__/solve-pipeline.test.ts +0 -660
- package/src/compute/__tests__/transform-input.test.ts +0 -162
- package/src/compute/client-cache.ts +0 -321
- package/src/compute/definition-byte-cache.ts +0 -168
- package/src/compute/index.ts +0 -82
- package/src/compute/limits.ts +0 -237
- package/src/compute/memory-solve-cache.ts +0 -0
- package/src/compute/rate-limit.ts +0 -110
- package/src/compute/remote-definition.ts +0 -149
- package/src/compute/safe-url.ts +0 -186
- package/src/compute/solve-cache-envelope.ts +0 -98
- package/src/compute/solve-cache-key.ts +0 -85
- package/src/compute/solve-cache-single-flight.ts +0 -59
- package/src/compute/solve-pipeline.ts +0 -603
- package/src/compute/transform-input.ts +0 -66
- package/src/definitions/__tests__/definition-service.test.ts +0 -131
- package/src/definitions/__tests__/load-for-render.test.ts +0 -244
- package/src/definitions/__tests__/schema-extraction.test.ts +0 -137
- package/src/definitions/definition-service.ts +0 -247
- package/src/definitions/index.ts +0 -22
- package/src/definitions/load-for-render.ts +0 -272
- package/src/definitions/schema-extraction.ts +0 -117
- package/src/errors/SentryErrorReporter.ts +0 -88
- package/src/errors/index.ts +0 -3
- package/src/http/__tests__/body-size.test.ts +0 -27
- package/src/http/__tests__/route-classifier.test.ts +0 -60
- package/src/http/__tests__/safe-redirect.test.ts +0 -47
- package/src/http/__tests__/security-headers.test.ts +0 -29
- package/src/http/body-size.ts +0 -24
- package/src/http/index.ts +0 -10
- package/src/http/redirect.ts +0 -16
- package/src/http/route-classifier.ts +0 -74
- package/src/http/security-headers.ts +0 -42
- package/src/index.ts +0 -27
- package/src/ops/__tests__/semver.test.ts +0 -42
- package/src/ops/index.ts +0 -3
- package/src/ops/semver.ts +0 -60
- package/src/providers/__tests__/create-selva-providers.test.ts +0 -172
- package/src/providers/__tests__/fixtures/override-config.mjs +0 -8
- package/src/providers/create-selva-providers.ts +0 -241
- package/src/providers/index.ts +0 -12
- package/src/tokens/__tests__/token-codec.test.ts +0 -63
- package/src/tokens/index.ts +0 -8
- package/src/tokens/token-codec.ts +0 -86
package/src/compute/index.ts
DELETED
|
@@ -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';
|
package/src/compute/limits.ts
DELETED
|
@@ -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
|
-
}
|