@selvajs/server 1.0.0-beta.9 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -20
- package/dist/access/project-access-input.d.ts +13 -16
- package/dist/access/project-access-input.d.ts.map +1 -1
- package/dist/access/project-access-input.js +2 -2
- package/dist/access/project-access-input.js.map +1 -1
- package/dist/compute/idempotency.d.ts +18 -22
- package/dist/compute/idempotency.d.ts.map +1 -1
- package/dist/compute/idempotency.js +10 -14
- package/dist/compute/idempotency.js.map +1 -1
- package/dist/compute/index.d.ts.map +1 -1
- package/dist/compute/index.js +3 -5
- package/dist/compute/index.js.map +1 -1
- package/dist/compute/limits.d.ts +70 -86
- package/dist/compute/limits.d.ts.map +1 -1
- package/dist/compute/limits.js +27 -56
- package/dist/compute/limits.js.map +1 -1
- package/dist/compute/rate-limit.d.ts +41 -53
- package/dist/compute/rate-limit.d.ts.map +1 -1
- package/dist/compute/rate-limit.js +27 -35
- package/dist/compute/rate-limit.js.map +1 -1
- package/dist/compute/remote-definition.d.ts +8 -22
- package/dist/compute/remote-definition.d.ts.map +1 -1
- package/dist/compute/remote-definition.js +9 -19
- package/dist/compute/remote-definition.js.map +1 -1
- package/dist/compute/safe-url.d.ts +7 -5
- package/dist/compute/safe-url.d.ts.map +1 -1
- package/dist/compute/safe-url.js +26 -28
- package/dist/compute/safe-url.js.map +1 -1
- package/dist/definitions/definition-service.d.ts +13 -38
- package/dist/definitions/definition-service.d.ts.map +1 -1
- package/dist/definitions/definition-service.js +25 -53
- package/dist/definitions/definition-service.js.map +1 -1
- package/dist/definitions/index.d.ts +2 -2
- package/dist/definitions/index.d.ts.map +1 -1
- package/dist/definitions/index.js +1 -5
- package/dist/definitions/index.js.map +1 -1
- package/dist/definitions/load-for-render.d.ts +23 -20
- package/dist/definitions/load-for-render.d.ts.map +1 -1
- package/dist/definitions/load-for-render.js +55 -41
- package/dist/definitions/load-for-render.js.map +1 -1
- package/dist/definitions/schema-extraction.d.ts +36 -16
- package/dist/definitions/schema-extraction.d.ts.map +1 -1
- package/dist/definitions/schema-extraction.js +65 -28
- package/dist/definitions/schema-extraction.js.map +1 -1
- package/dist/errors/SentryErrorReporter.d.ts +2 -3
- package/dist/errors/SentryErrorReporter.d.ts.map +1 -1
- package/dist/errors/SentryErrorReporter.js +10 -14
- package/dist/errors/SentryErrorReporter.js.map +1 -1
- package/dist/http/body-size.d.ts +8 -11
- package/dist/http/body-size.d.ts.map +1 -1
- package/dist/http/body-size.js +8 -11
- package/dist/http/body-size.js.map +1 -1
- package/dist/http/redirect.d.ts +3 -7
- package/dist/http/redirect.d.ts.map +1 -1
- package/dist/http/redirect.js +8 -8
- package/dist/http/redirect.js.map +1 -1
- package/dist/http/route-classifier.d.ts +20 -24
- package/dist/http/route-classifier.d.ts.map +1 -1
- package/dist/http/route-classifier.js +6 -7
- package/dist/http/route-classifier.js.map +1 -1
- package/dist/http/security-headers.d.ts +13 -15
- package/dist/http/security-headers.d.ts.map +1 -1
- package/dist/http/security-headers.js +10 -12
- package/dist/http/security-headers.js.map +1 -1
- package/dist/logging/PinoLogger.d.ts +8 -14
- package/dist/logging/PinoLogger.d.ts.map +1 -1
- package/dist/logging/PinoLogger.js +29 -35
- package/dist/logging/PinoLogger.js.map +1 -1
- package/dist/logging/requestId.d.ts +20 -28
- package/dist/logging/requestId.d.ts.map +1 -1
- package/dist/logging/requestId.js +29 -37
- package/dist/logging/requestId.js.map +1 -1
- package/dist/ops/index.d.ts.map +1 -1
- package/dist/ops/index.js +2 -1
- package/dist/ops/index.js.map +1 -1
- package/dist/ops/semver.d.ts +22 -24
- package/dist/ops/semver.d.ts.map +1 -1
- package/dist/ops/semver.js +22 -25
- package/dist/ops/semver.js.map +1 -1
- package/dist/providers/create-selva-providers.d.ts +19 -27
- package/dist/providers/create-selva-providers.d.ts.map +1 -1
- package/dist/providers/create-selva-providers.js +28 -34
- package/dist/providers/create-selva-providers.js.map +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +2 -4
- package/dist/providers/index.js.map +1 -1
- package/dist/tokens/token-codec.d.ts +24 -27
- package/dist/tokens/token-codec.d.ts.map +1 -1
- package/dist/tokens/token-codec.js +18 -21
- package/dist/tokens/token-codec.js.map +1 -1
- package/package.json +5 -5
package/dist/compute/limits.js
CHANGED
|
@@ -1,34 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compute/solve server limits, resolved from an injected env map.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
4
|
+
* Env comes in as a parameter — nothing ambient is read — so this is importable
|
|
5
|
+
* from any runtime and testable without env plumbing. Defaults are sized for
|
|
6
|
+
* typical interactive use (sliders + small uploads).
|
|
15
7
|
*
|
|
16
8
|
* Client-side counterparts (the in-flight compute throttle and slider debounce)
|
|
17
|
-
*
|
|
18
|
-
*
|
|
9
|
+
* are bundled into the client in `@selvajs/ui` and can't read env — they carry
|
|
10
|
+
* their own defaults.
|
|
19
11
|
*/
|
|
20
12
|
import { NoopLogger } from '@selvajs/platform';
|
|
21
13
|
const MB = 1024 * 1024;
|
|
22
14
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* silence rather than unsolicited stdout writes. The app passes its real logger.
|
|
15
|
+
* Default logger for the env-parsing warnings below. This is library code, so an
|
|
16
|
+
* embedder that wires nothing gets silence rather than unsolicited stdout writes.
|
|
26
17
|
*/
|
|
27
18
|
const noop = new NoopLogger();
|
|
28
|
-
/**
|
|
29
|
-
* Parse a positive integer env value. Returns `fallback` when absent, and warns
|
|
30
|
-
* + falls back when present but not a finite positive number.
|
|
31
|
-
*/
|
|
19
|
+
/** Parse a positive integer env value; absent, or present but not finite and positive, falls back. */
|
|
32
20
|
export function readPositiveInt(env, name, fallback, logger = noop) {
|
|
33
21
|
const raw = env[name];
|
|
34
22
|
if (!raw)
|
|
@@ -45,12 +33,7 @@ export function readPositiveInt(env, name, fallback, logger = noop) {
|
|
|
45
33
|
}
|
|
46
34
|
return Math.floor(parsed);
|
|
47
35
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Parse a non-negative integer env value — like {@link readPositiveInt} but `0`
|
|
50
|
-
* is a valid value (used by knobs where `0` means "disable", e.g. a cache
|
|
51
|
-
* budget). Returns `fallback` when absent, and warns + falls back when present
|
|
52
|
-
* but not a finite non-negative number.
|
|
53
|
-
*/
|
|
36
|
+
/** Like {@link readPositiveInt}, but `0` is valid — knobs where it means "disable". */
|
|
54
37
|
export function readNonNegativeInt(env, name, fallback, logger = noop) {
|
|
55
38
|
const raw = env[name];
|
|
56
39
|
if (!raw)
|
|
@@ -68,16 +51,12 @@ export function readNonNegativeInt(env, name, fallback, logger = noop) {
|
|
|
68
51
|
return Math.floor(parsed);
|
|
69
52
|
}
|
|
70
53
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* name is present — the old value plus a warning naming both. Setting both is not
|
|
75
|
-
* an error: the new name wins silently, which is what an operator mid-migration
|
|
76
|
-
* expects.
|
|
54
|
+
* Fall back to a variable's previous name, warning when the old one is used.
|
|
55
|
+
* Setting both is not an error: the new name wins silently, which is what an
|
|
56
|
+
* operator mid-migration expects.
|
|
77
57
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* surfaces as a memory number nobody is watching.
|
|
58
|
+
* A var that quietly stops being read looks exactly like one that is working —
|
|
59
|
+
* the failure only surfaces as a memory number nobody is watching.
|
|
81
60
|
*/
|
|
82
61
|
function readRenamed(env, name, oldName, logger) {
|
|
83
62
|
if (env[name] !== undefined || env[oldName] === undefined)
|
|
@@ -90,8 +69,8 @@ function readRenamed(env, name, oldName, logger) {
|
|
|
90
69
|
return { ...env, [name]: env[oldName] };
|
|
91
70
|
}
|
|
92
71
|
/**
|
|
93
|
-
* Parse a boolean env flag. Accepts `true/1/yes/on`
|
|
94
|
-
*
|
|
72
|
+
* Parse a boolean env flag. Accepts `true/1/yes/on` and `false/0/no/off`
|
|
73
|
+
* (case-insensitive); any other spelling warns and falls back.
|
|
95
74
|
*/
|
|
96
75
|
export function readBool(env, name, fallback, logger = noop) {
|
|
97
76
|
const raw = env[name];
|
|
@@ -111,20 +90,17 @@ export function readBool(env, name, fallback, logger = noop) {
|
|
|
111
90
|
return fallback;
|
|
112
91
|
}
|
|
113
92
|
/**
|
|
114
|
-
* Resolve every server-side compute limit from an env map.
|
|
115
|
-
*
|
|
116
|
-
* this once at its composition root with `$env/dynamic/private`.
|
|
93
|
+
* Resolve every server-side compute limit from an env map. The app calls this
|
|
94
|
+
* once at its composition root with `$env/dynamic/private`.
|
|
117
95
|
*/
|
|
118
96
|
export function resolveComputeLimits(env, logger = noop) {
|
|
119
|
-
//
|
|
120
|
-
//
|
|
97
|
+
// Deprecation shims, so each var says what it HOLDS and what it bounds — one
|
|
98
|
+
// solve — rather than a vague "duration". Drop one minor version on.
|
|
121
99
|
env = readRenamed(env, 'COMPUTE_DEFINITION_CACHE_MB', 'COMPUTE_DEFINITION_BYTE_CACHE_MB', logger);
|
|
122
100
|
env = readRenamed(env, 'COMPUTE_SOLVE_CACHE_MB', 'COMPUTE_RESPONSE_CACHE_MB', logger);
|
|
123
101
|
env = readRenamed(env, 'REMOTE_DEFINITION_CACHE_TTL_MS', 'DEFINITION_CACHE_TTL_MS', logger);
|
|
124
|
-
// Renamed 2026-08 into the COMPUTE_* namespace it belongs to, and to say what it
|
|
125
|
-
// bounds — one solve — rather than a vague "duration".
|
|
126
102
|
env = readRenamed(env, 'COMPUTE_SOLVE_DEADLINE_MS', 'MAX_SOLVE_DURATION_MS', logger);
|
|
127
|
-
//
|
|
103
|
+
// Clean-cut rename with no old-name dual-read: deliberate pre-1.0 breaking change.
|
|
128
104
|
const maxDefinitionFileSize = readPositiveInt(env, 'MAX_DEFINITION_FILE_SIZE_BYTES', 50 * MB, logger);
|
|
129
105
|
return {
|
|
130
106
|
solveDeadlineMs: readPositiveInt(env, 'COMPUTE_SOLVE_DEADLINE_MS', 100_000, logger),
|
|
@@ -134,26 +110,21 @@ export function resolveComputeLimits(env, logger = noop) {
|
|
|
134
110
|
maxImageFileSize: readPositiveInt(env, 'MAX_IMAGE_FILE_SIZE_BYTES', 10 * MB, logger),
|
|
135
111
|
computeRequestMaxBytes: readPositiveInt(env, 'COMPUTE_REQUEST_MAX_BYTES', 210 * MB, logger),
|
|
136
112
|
computeResponseMaxBytes: readPositiveInt(env, 'COMPUTE_RESPONSE_MAX_BYTES', 300 * MB, logger),
|
|
137
|
-
// Tracks the upload cap so a remote URL can't smuggle a larger file.
|
|
138
113
|
remoteDefinitionMaxBytes: maxDefinitionFileSize,
|
|
139
114
|
remoteDefinitionFetchTimeoutMs: readPositiveInt(env, 'REMOTE_DEFINITION_FETCH_TIMEOUT_MS', 30_000, logger),
|
|
140
115
|
remoteDefinitionCacheTtlMs: readPositiveInt(env, 'REMOTE_DEFINITION_CACHE_TTL_MS', 5 * 60 * 1000, logger),
|
|
141
116
|
computeReuseDefinitionCache: readBool(env, 'COMPUTE_REUSE_DEFINITION_CACHE', true, logger),
|
|
142
117
|
computeServerCachesolve: readBool(env, 'COMPUTE_SERVER_CACHESOLVE', true, logger),
|
|
143
118
|
computeCacheErroredSolves: readBool(env, 'COMPUTE_CACHE_ERRORED_SOLVES', false, logger),
|
|
144
|
-
// Both
|
|
145
|
-
//
|
|
146
|
-
//
|
|
119
|
+
// Both default to off — nothing is rejected until an operator who has measured
|
|
120
|
+
// their pool opts in. readNonNegativeInt throughout, so `0` reads as "disabled"
|
|
121
|
+
// rather than as an invalid value that falls back.
|
|
147
122
|
computeMaxQueueDepth: readNonNegativeInt(env, 'COMPUTE_MAX_QUEUE_DEPTH', 0, logger),
|
|
148
123
|
computeQueueWaitMs: readNonNegativeInt(env, 'COMPUTE_QUEUE_WAIT_MS', 0, logger),
|
|
149
|
-
//
|
|
150
|
-
// definitions warm without pinning gigabytes. readNonNegativeInt so `0`
|
|
151
|
-
// (disable) is honored rather than treated as invalid.
|
|
124
|
+
// 256 MB holds a handful of typical definitions warm without pinning gigabytes.
|
|
152
125
|
computeDefinitionCacheBytes: readNonNegativeInt(env, 'COMPUTE_DEFINITION_CACHE_MB', 256, logger) * MB,
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
// letting worst-case entries pin gigabytes (audit C2). PER warm client —
|
|
156
|
-
// see the field doc for the ×16 worst case.
|
|
126
|
+
// 256 MB keeps a scrub session's recent solves warm; PER warm client, so see
|
|
127
|
+
// the field doc for the ×16 worst case.
|
|
157
128
|
computeSolveCacheBytes: readNonNegativeInt(env, 'COMPUTE_SOLVE_CACHE_MB', 256, logger) * MB
|
|
158
129
|
};
|
|
159
130
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/compute/limits.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/compute/limits.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,41 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-key fixed-window rate limiter for the compute-solve endpoint.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Per-key fixed-window rate limiter for the compute-solve endpoint. Solves are
|
|
3
|
+
* the one expensive endpoint in a Selva-engine app — without a cap a single user
|
|
4
|
+
* or share-link consumer burns compute budget far faster than any other surface
|
|
5
|
+
* allows.
|
|
6
6
|
*
|
|
7
7
|
* Design:
|
|
8
8
|
* - Process-local `Map`. Multi-instance deployments see N× the per-key rate —
|
|
9
|
-
* acceptable as a first line
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* with the number of distinct keys ever seen (every user, share link and
|
|
15
|
-
* login IP, forever). Two inline mechanisms keep it bounded, both driven by
|
|
9
|
+
* acceptable as a first line. State lives behind this factory so a shared-store
|
|
10
|
+
* implementation can slot in later without touching call sites.
|
|
11
|
+
* - Bounded. Nothing revisits a bucket that is never checked again, so an
|
|
12
|
+
* unswept `Map` grows with every distinct key ever seen — every user, share
|
|
13
|
+
* link and login IP, forever. Two mechanisms keep it bounded, both driven by
|
|
16
14
|
* `check`/`peek` rather than a timer: a timer would keep the event loop alive
|
|
17
15
|
* and give this module a lifecycle its callers don't have.
|
|
18
16
|
* 1. Amortized sweep — every `sweepIntervalMs` (default: one window), drop
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* 2. Hard cap — if live buckets alone exceed `maxKeys`, evict
|
|
22
|
-
*
|
|
23
|
-
* protect, so a distinct-key flood can't pin unbounded memory. Eviction
|
|
17
|
+
* expired buckets. `check` already treats them as absent, so dropping
|
|
18
|
+
* them changes no verdict.
|
|
19
|
+
* 2. Hard cap — if live buckets alone exceed `maxKeys`, evict those closest
|
|
20
|
+
* to resetting; they have the least budget left to protect. Eviction
|
|
24
21
|
* forgives a key's counter, so `maxKeys` is sized to be unreachable in
|
|
25
22
|
* normal operation and only bites under a key-space attack.
|
|
26
|
-
* - Fixed window
|
|
27
|
-
*
|
|
28
|
-
* per-user auth already prevent unbounded abuse
|
|
29
|
-
* - Keyed by caller — the app passes `user:{userId}` for authenticated solves
|
|
30
|
-
* `share:{linkId}` for share-token solves, so anonymous consumers of one
|
|
31
|
-
* don't share a bucket with the link's owner.
|
|
23
|
+
* - Fixed window rather than sliding — simpler, no per-request bookkeeping. The
|
|
24
|
+
* burst-at-window-edges behaviour is fine here: share-link `maxSolves` and
|
|
25
|
+
* per-user auth already prevent unbounded abuse, and this flattens spikes.
|
|
26
|
+
* - Keyed by caller — the app passes `user:{userId}` for authenticated solves
|
|
27
|
+
* and `share:{linkId}` for share-token solves, so anonymous consumers of one
|
|
28
|
+
* link don't share a bucket with the link's owner.
|
|
32
29
|
*
|
|
33
|
-
*
|
|
34
|
-
* reads no env of its own.
|
|
30
|
+
* Window and cap are injected from `ComputeLimits`; this module reads no env.
|
|
35
31
|
*
|
|
36
|
-
* Not compute-only despite the name: `peek` + `clear`
|
|
37
|
-
*
|
|
38
|
-
*
|
|
32
|
+
* Not compute-only despite the name: `peek` + `clear` also serve failure-counting
|
|
33
|
+
* flows — login limiting keys by IP, records only failures via `check`, and
|
|
34
|
+
* forgives on success via `clear`.
|
|
39
35
|
*/
|
|
40
36
|
export interface RateLimitResult {
|
|
41
37
|
allowed: boolean;
|
|
@@ -48,54 +44,46 @@ export interface ComputeRateLimiterConfig {
|
|
|
48
44
|
/** Max requests admitted per key per window. */
|
|
49
45
|
maxPerWindow: number;
|
|
50
46
|
/**
|
|
51
|
-
* Max retained buckets
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* Defaults to {@link DEFAULT_MAX_KEYS}.
|
|
47
|
+
* Max retained buckets; defaults to {@link DEFAULT_MAX_KEYS}. Evicting forgives
|
|
48
|
+
* that key's counter, so this is a memory backstop against a distinct-key
|
|
49
|
+
* flood, not a tuning knob — size it well above the plausible number of
|
|
50
|
+
* concurrently-active keys.
|
|
56
51
|
*/
|
|
57
52
|
maxKeys?: number;
|
|
58
53
|
/**
|
|
59
|
-
* How often (ms) to sweep expired buckets
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* leaving only the `maxKeys` cap (test seam).
|
|
54
|
+
* How often (ms) to sweep expired buckets. Defaults to `windowMs` — every
|
|
55
|
+
* bucket swept is already past its reset, so sweeping more often buys nothing.
|
|
56
|
+
* `0` disables the sweep, leaving only the `maxKeys` cap (test seam).
|
|
63
57
|
*/
|
|
64
58
|
sweepIntervalMs?: number;
|
|
65
59
|
}
|
|
66
60
|
/** Default {@link ComputeRateLimiterConfig.maxKeys}. */
|
|
67
61
|
export declare const DEFAULT_MAX_KEYS = 100000;
|
|
68
62
|
export interface ComputeRateLimiter {
|
|
69
|
-
/**
|
|
70
|
-
* Check + record one request against `key`. Increments the counter on allow.
|
|
71
|
-
* Returns `{ allowed: false, retryAfter }` once the bucket is full.
|
|
72
|
-
*/
|
|
63
|
+
/** Check and record one request against `key`, incrementing the counter on allow. */
|
|
73
64
|
check(key: string): RateLimitResult;
|
|
74
65
|
/**
|
|
75
|
-
* Check WITHOUT recording
|
|
76
|
-
*
|
|
77
|
-
*
|
|
66
|
+
* Check WITHOUT recording — for flows that count only selected outcomes. Login
|
|
67
|
+
* limiting gates the attempt with `peek`, records failures with `check`, and
|
|
68
|
+
* calls `clear` on success.
|
|
78
69
|
*/
|
|
79
70
|
peek(key: string): RateLimitResult;
|
|
80
|
-
/** Drop `key`'s bucket —
|
|
71
|
+
/** Drop `key`'s bucket — a successful login forgives prior failures. */
|
|
81
72
|
clear(key: string): void;
|
|
82
73
|
/**
|
|
83
|
-
* Requests recorded against `key`
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
74
|
+
* Requests recorded against `key` this window; `0` when the bucket is absent or
|
|
75
|
+
* expired. Which key a caller charges is a security property — an anonymous
|
|
76
|
+
* share solve must not spend the link owner's budget — and an allow/deny
|
|
77
|
+
* verdict alone can't express it.
|
|
87
78
|
*/
|
|
88
79
|
count(key: string): number;
|
|
89
80
|
/** Test seam — wipes in-memory state. Production code never calls this. */
|
|
90
81
|
reset(): void;
|
|
91
|
-
/** Retained bucket count, including any not yet swept.
|
|
82
|
+
/** Retained bucket count, including any not yet swept. Tests/observability. */
|
|
92
83
|
size(): number;
|
|
93
84
|
/** The resolved config, exposed for time-based test assertions. */
|
|
94
85
|
readonly config: Readonly<ComputeRateLimiterConfig>;
|
|
95
86
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Build a process-local fixed-window rate limiter. Each limiter owns its own
|
|
98
|
-
* bucket `Map`, so distinct endpoints/keys-spaces don't collide.
|
|
99
|
-
*/
|
|
87
|
+
/** Each limiter owns its own bucket `Map`, so distinct key-spaces don't collide. */
|
|
100
88
|
export declare function createComputeRateLimiter(config: ComputeRateLimiterConfig): ComputeRateLimiter;
|
|
101
89
|
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/compute/rate-limit.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/compute/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACxC,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wDAAwD;AACxD,eAAO,MAAM,gBAAgB,SAAU,CAAC;AAExC,MAAM,WAAW,kBAAkB;IAClC,qFAAqF;IACrF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;IACpC;;;;OAIG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;IACnC,wEAAwE;IACxE,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;;OAKG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,2EAA2E;IAC3E,KAAK,IAAI,IAAI,CAAC;IACd,+EAA+E;IAC/E,IAAI,IAAI,MAAM,CAAC;IACf,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,wBAAwB,CAAC,CAAC;CACpD;AAOD,oFAAoF;AACpF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,wBAAwB,GAAG,kBAAkB,CA6F7F"}
|
|
@@ -1,48 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-key fixed-window rate limiter for the compute-solve endpoint.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Per-key fixed-window rate limiter for the compute-solve endpoint. Solves are
|
|
3
|
+
* the one expensive endpoint in a Selva-engine app — without a cap a single user
|
|
4
|
+
* or share-link consumer burns compute budget far faster than any other surface
|
|
5
|
+
* allows.
|
|
6
6
|
*
|
|
7
7
|
* Design:
|
|
8
8
|
* - Process-local `Map`. Multi-instance deployments see N× the per-key rate —
|
|
9
|
-
* acceptable as a first line
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* with the number of distinct keys ever seen (every user, share link and
|
|
15
|
-
* login IP, forever). Two inline mechanisms keep it bounded, both driven by
|
|
9
|
+
* acceptable as a first line. State lives behind this factory so a shared-store
|
|
10
|
+
* implementation can slot in later without touching call sites.
|
|
11
|
+
* - Bounded. Nothing revisits a bucket that is never checked again, so an
|
|
12
|
+
* unswept `Map` grows with every distinct key ever seen — every user, share
|
|
13
|
+
* link and login IP, forever. Two mechanisms keep it bounded, both driven by
|
|
16
14
|
* `check`/`peek` rather than a timer: a timer would keep the event loop alive
|
|
17
15
|
* and give this module a lifecycle its callers don't have.
|
|
18
16
|
* 1. Amortized sweep — every `sweepIntervalMs` (default: one window), drop
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* 2. Hard cap — if live buckets alone exceed `maxKeys`, evict
|
|
22
|
-
*
|
|
23
|
-
* protect, so a distinct-key flood can't pin unbounded memory. Eviction
|
|
17
|
+
* expired buckets. `check` already treats them as absent, so dropping
|
|
18
|
+
* them changes no verdict.
|
|
19
|
+
* 2. Hard cap — if live buckets alone exceed `maxKeys`, evict those closest
|
|
20
|
+
* to resetting; they have the least budget left to protect. Eviction
|
|
24
21
|
* forgives a key's counter, so `maxKeys` is sized to be unreachable in
|
|
25
22
|
* normal operation and only bites under a key-space attack.
|
|
26
|
-
* - Fixed window
|
|
27
|
-
*
|
|
28
|
-
* per-user auth already prevent unbounded abuse
|
|
29
|
-
* - Keyed by caller — the app passes `user:{userId}` for authenticated solves
|
|
30
|
-
* `share:{linkId}` for share-token solves, so anonymous consumers of one
|
|
31
|
-
* don't share a bucket with the link's owner.
|
|
23
|
+
* - Fixed window rather than sliding — simpler, no per-request bookkeeping. The
|
|
24
|
+
* burst-at-window-edges behaviour is fine here: share-link `maxSolves` and
|
|
25
|
+
* per-user auth already prevent unbounded abuse, and this flattens spikes.
|
|
26
|
+
* - Keyed by caller — the app passes `user:{userId}` for authenticated solves
|
|
27
|
+
* and `share:{linkId}` for share-token solves, so anonymous consumers of one
|
|
28
|
+
* link don't share a bucket with the link's owner.
|
|
32
29
|
*
|
|
33
|
-
*
|
|
34
|
-
* reads no env of its own.
|
|
30
|
+
* Window and cap are injected from `ComputeLimits`; this module reads no env.
|
|
35
31
|
*
|
|
36
|
-
* Not compute-only despite the name: `peek` + `clear`
|
|
37
|
-
*
|
|
38
|
-
*
|
|
32
|
+
* Not compute-only despite the name: `peek` + `clear` also serve failure-counting
|
|
33
|
+
* flows — login limiting keys by IP, records only failures via `check`, and
|
|
34
|
+
* forgives on success via `clear`.
|
|
39
35
|
*/
|
|
40
36
|
/** Default {@link ComputeRateLimiterConfig.maxKeys}. */
|
|
41
37
|
export const DEFAULT_MAX_KEYS = 100_000;
|
|
42
|
-
/**
|
|
43
|
-
* Build a process-local fixed-window rate limiter. Each limiter owns its own
|
|
44
|
-
* bucket `Map`, so distinct endpoints/keys-spaces don't collide.
|
|
45
|
-
*/
|
|
38
|
+
/** Each limiter owns its own bucket `Map`, so distinct key-spaces don't collide. */
|
|
46
39
|
export function createComputeRateLimiter(config) {
|
|
47
40
|
const { windowMs, maxPerWindow } = config;
|
|
48
41
|
const maxKeys = Math.max(1, Math.floor(config.maxKeys ?? DEFAULT_MAX_KEYS));
|
|
@@ -55,13 +48,12 @@ export function createComputeRateLimiter(config) {
|
|
|
55
48
|
const buckets = new Map();
|
|
56
49
|
let nextSweepAt = Number.POSITIVE_INFINITY;
|
|
57
50
|
/**
|
|
58
|
-
* Drop dead buckets, then enforce the hard cap.
|
|
59
|
-
* so the limiter needs no timer of its own. Runs at most once per
|
|
51
|
+
* Drop dead buckets, then enforce the hard cap. Runs at most once per
|
|
60
52
|
* `sweepIntervalMs` — except when the cap is already breached, which must be
|
|
61
53
|
* enforced on the spot regardless of the sweep clock.
|
|
62
54
|
*
|
|
63
|
-
* `headroom` is how many buckets the caller is about to insert
|
|
64
|
-
* enforced against the post-insert size so `maxKeys` is a true ceiling rather
|
|
55
|
+
* `headroom` is how many buckets the caller is about to insert. The cap is
|
|
56
|
+
* enforced against the post-insert size, so `maxKeys` is a true ceiling rather
|
|
65
57
|
* than one the very next write steps over.
|
|
66
58
|
*/
|
|
67
59
|
function maybeSweep(now, headroom = 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/compute/rate-limit.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/compute/rate-limit.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Remote-definition fetch: SSRF-guarded, size-capped, timed, and TTL-cached.
|
|
3
3
|
*
|
|
4
|
-
* A solve can reference its `.gh` by remote URL instead of a stored blob
|
|
5
|
-
* URL is attacker-controllable
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
4
|
+
* A solve can reference its `.gh` by remote URL instead of a stored blob, and
|
|
5
|
+
* that URL is attacker-controllable — a share-token caller supplies
|
|
6
|
+
* `definitionUrl`. Hence {@link assertSafeRemoteDefinitionUrl}, the early size
|
|
7
|
+
* cap, and the deadline.
|
|
16
8
|
*/
|
|
17
9
|
import { type ILogger } from '@selvajs/platform';
|
|
18
10
|
/** Injected limits — the app derives these from its env-resolved `ComputeLimits`. */
|
|
@@ -23,16 +15,11 @@ export interface RemoteDefinitionConfig {
|
|
|
23
15
|
fetchTimeoutMs: number;
|
|
24
16
|
/** How long a fetched definition stays warm in the in-process cache. */
|
|
25
17
|
cacheTtlMs: number;
|
|
26
|
-
/**
|
|
27
|
-
* Current epoch millis. Injected (not read via `Date.now()` ambiently) so the
|
|
28
|
-
* TTL is testable and the module stays side-effect-free. The app binding
|
|
29
|
-
* passes `() => Date.now()`.
|
|
30
|
-
*/
|
|
18
|
+
/** Current epoch millis. Injected so the TTL is testable; the app passes `() => Date.now()`. */
|
|
31
19
|
now: () => number;
|
|
32
20
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* unsolicited stdout writes.
|
|
21
|
+
* Receives the SSRF guard's block diagnostics. Defaults to `NoopLogger` so an
|
|
22
|
+
* embedder that wires nothing gets silence rather than unsolicited stdout writes.
|
|
36
23
|
*/
|
|
37
24
|
logger?: ILogger;
|
|
38
25
|
}
|
|
@@ -47,8 +34,7 @@ export interface RemoteDefinitionFetcher {
|
|
|
47
34
|
/**
|
|
48
35
|
* Read a response body into memory, aborting (and throwing) as soon as the
|
|
49
36
|
* running byte total exceeds `maxBytes`. Falls back to `arrayBuffer()` only when
|
|
50
|
-
* the body isn't a readable stream (older fetch impls).
|
|
51
|
-
* and reuse; the fetcher below wraps it with SSRF + TTL.
|
|
37
|
+
* the body isn't a readable stream (older fetch impls).
|
|
52
38
|
*/
|
|
53
39
|
export declare function readBodyWithCap(response: Response, maxBytes: number, controller: AbortController): Promise<Uint8Array>;
|
|
54
40
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-definition.d.ts","sourceRoot":"","sources":["../../src/compute/remote-definition.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"remote-definition.d.ts","sourceRoot":"","sources":["../../src/compute/remote-definition.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE7D,qFAAqF;AACrF,MAAM,WAAW,sBAAsB;IACtC,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,cAAc,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACvC;;;;OAIG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACvC;AAMD;;;;GAIG;AACH,wBAAsB,eAAe,CACpC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,eAAe,GACzB,OAAO,CAAC,UAAU,CAAC,CA4BrB;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC5C,MAAM,EAAE,sBAAsB,GAC5B,uBAAuB,CAqDzB"}
|
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Remote-definition fetch: SSRF-guarded, size-capped, timed, and TTL-cached.
|
|
3
3
|
*
|
|
4
|
-
* A solve can reference its `.gh` by remote URL instead of a stored blob
|
|
5
|
-
* URL is attacker-controllable
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
4
|
+
* A solve can reference its `.gh` by remote URL instead of a stored blob, and
|
|
5
|
+
* that URL is attacker-controllable — a share-token caller supplies
|
|
6
|
+
* `definitionUrl`. Hence {@link assertSafeRemoteDefinitionUrl}, the early size
|
|
7
|
+
* cap, and the deadline.
|
|
16
8
|
*/
|
|
17
9
|
import { assertSafeRemoteDefinitionUrl } from './safe-url.js';
|
|
18
10
|
import { NoopLogger } from '@selvajs/platform';
|
|
@@ -22,8 +14,7 @@ const CACHE_EVICT_BATCH = 10;
|
|
|
22
14
|
/**
|
|
23
15
|
* Read a response body into memory, aborting (and throwing) as soon as the
|
|
24
16
|
* running byte total exceeds `maxBytes`. Falls back to `arrayBuffer()` only when
|
|
25
|
-
* the body isn't a readable stream (older fetch impls).
|
|
26
|
-
* and reuse; the fetcher below wraps it with SSRF + TTL.
|
|
17
|
+
* the body isn't a readable stream (older fetch impls).
|
|
27
18
|
*/
|
|
28
19
|
export async function readBodyWithCap(response, maxBytes, controller) {
|
|
29
20
|
if (!response.body) {
|
|
@@ -64,9 +55,8 @@ export function createRemoteDefinitionFetcher(config) {
|
|
|
64
55
|
const logger = config.logger ?? new NoopLogger();
|
|
65
56
|
return {
|
|
66
57
|
async load(url) {
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
// IPv4-mapped IPv6) and public names that point inward. Throws on rejection.
|
|
58
|
+
// Before the cache read, not after: a URL must never serve warm bytes
|
|
59
|
+
// without passing the guard.
|
|
70
60
|
await assertSafeRemoteDefinitionUrl(url, logger);
|
|
71
61
|
const now = config.now();
|
|
72
62
|
const cached = cache.get(url);
|
|
@@ -89,9 +79,9 @@ export function createRemoteDefinitionFetcher(config) {
|
|
|
89
79
|
if (Number.isFinite(declared) && declared > config.maxBytes) {
|
|
90
80
|
throw new Error('Remote definition exceeds size limit');
|
|
91
81
|
}
|
|
92
|
-
// Stream and count rather than `arrayBuffer()` — a missing
|
|
82
|
+
// Stream and count rather than `arrayBuffer()` — a missing or lying
|
|
93
83
|
// content-length must not let an unbounded body buffer into memory
|
|
94
|
-
// before the cap is checked.
|
|
84
|
+
// before the cap is checked. Aborts the moment we cross the limit.
|
|
95
85
|
data = await readBodyWithCap(response, config.maxBytes, controller);
|
|
96
86
|
}
|
|
97
87
|
finally {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-definition.js","sourceRoot":"","sources":["../../src/compute/remote-definition.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"remote-definition.js","sourceRoot":"","sources":["../../src/compute/remote-definition.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { type ILogger } from '@selvajs/platform';
|
|
2
2
|
/**
|
|
3
|
-
* Synchronous literal pre-filter
|
|
4
|
-
* any IP literal (in any encoding) that
|
|
3
|
+
* Synchronous literal pre-filter: rejects bad schemes, loopback hostnames, and
|
|
4
|
+
* any IP literal (in any encoding) that maps to a blocked address. Does NOT
|
|
5
5
|
* perform DNS — a public *name* that points at a private IP passes here and is
|
|
6
|
-
* caught by `assertSafeRemoteDefinitionUrl`.
|
|
6
|
+
* caught only by `assertSafeRemoteDefinitionUrl`.
|
|
7
7
|
*/
|
|
8
8
|
export declare function isSafeRemoteDefinitionUrl(raw: string): boolean;
|
|
9
9
|
/**
|
|
10
10
|
* Full SSRF check: literal pre-filter, then resolve every A/AAAA record and
|
|
11
11
|
* reject if *any* resolved IP is blocked. Returns the original URL on success
|
|
12
|
-
* so callers can `await assertSafeRemoteDefinitionUrl(url)` inline.
|
|
13
|
-
*
|
|
12
|
+
* so callers can `await assertSafeRemoteDefinitionUrl(url)` inline.
|
|
13
|
+
*
|
|
14
|
+
* Every rejection throws the same generic message so the caller gets no oracle;
|
|
15
|
+
* which host was blocked and by which layer is logged server-side instead.
|
|
14
16
|
*/
|
|
15
17
|
export declare function assertSafeRemoteDefinitionUrl(raw: string, logger?: ILogger): Promise<string>;
|
|
16
18
|
//# sourceMappingURL=safe-url.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-url.d.ts","sourceRoot":"","sources":["../../src/compute/safe-url.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"safe-url.d.ts","sourceRoot":"","sources":["../../src/compute/safe-url.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AA0H7D;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CA2B9D;AAED;;;;;;;GAOG;AACH,wBAAsB,6BAA6B,CAClD,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,OAA0B,GAChC,OAAO,CAAC,MAAM,CAAC,CAqCjB"}
|