actly 1.1.5 → 1.2.0
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 +232 -434
- package/dist/core/act.cjs +353 -35
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +355 -37
- package/dist/core/act.js.map +1 -1
- package/dist/core/executor.cjs +2 -0
- package/dist/core/executor.d.ts +6 -1
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +2 -0
- package/dist/core/executor.js.map +1 -1
- package/dist/core/health.cjs +34 -0
- package/dist/core/health.d.ts +18 -0
- package/dist/core/health.d.ts.map +1 -0
- package/dist/core/health.js +28 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/shutdown.cjs +60 -0
- package/dist/core/shutdown.d.ts +8 -0
- package/dist/core/shutdown.d.ts.map +1 -0
- package/dist/core/shutdown.js +56 -0
- package/dist/core/shutdown.js.map +1 -0
- package/dist/core/tenant.cjs +71 -0
- package/dist/core/tenant.d.ts +29 -0
- package/dist/core/tenant.d.ts.map +1 -0
- package/dist/core/tenant.js +68 -0
- package/dist/core/tenant.js.map +1 -0
- package/dist/errors.cjs +182 -0
- package/dist/errors.d.ts +151 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +171 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.cjs +44 -4
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/observability.cjs +45 -0
- package/dist/observability.d.ts +145 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +43 -0
- package/dist/observability.js.map +1 -0
- package/dist/policies/bulkhead.cjs +97 -0
- package/dist/policies/bulkhead.d.ts +3 -0
- package/dist/policies/bulkhead.d.ts.map +1 -0
- package/dist/policies/bulkhead.js +95 -0
- package/dist/policies/bulkhead.js.map +1 -0
- package/dist/policies/cache.cjs +168 -31
- package/dist/policies/cache.d.ts +17 -4
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +168 -31
- package/dist/policies/cache.js.map +1 -1
- package/dist/policies/circuitBreaker.cjs +85 -0
- package/dist/policies/circuitBreaker.d.ts +3 -0
- package/dist/policies/circuitBreaker.d.ts.map +1 -0
- package/dist/policies/circuitBreaker.js +83 -0
- package/dist/policies/circuitBreaker.js.map +1 -0
- package/dist/policies/dedupe.cjs +74 -45
- package/dist/policies/dedupe.d.ts +10 -28
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +74 -45
- package/dist/policies/dedupe.js.map +1 -1
- package/dist/policies/rateLimit.cjs +35 -0
- package/dist/policies/rateLimit.d.ts +3 -0
- package/dist/policies/rateLimit.d.ts.map +1 -0
- package/dist/policies/rateLimit.js +33 -0
- package/dist/policies/rateLimit.js.map +1 -0
- package/dist/policies/retry.cjs +45 -8
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +45 -8
- package/dist/policies/retry.js.map +1 -1
- package/dist/policies/timeout.cjs +25 -53
- package/dist/policies/timeout.d.ts +2 -33
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +31 -48
- package/dist/policies/timeout.js.map +1 -1
- package/dist/state/store.cjs +3 -3
- package/dist/state/store.d.ts +3 -3
- package/dist/state/store.js +3 -3
- package/dist/stores/memory.cjs +126 -53
- package/dist/stores/memory.d.ts +35 -16
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +125 -53
- package/dist/stores/memory.js.map +1 -1
- package/dist/types/index.d.ts +176 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/abort.cjs +108 -38
- package/dist/utils/abort.d.ts +43 -19
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +108 -38
- package/dist/utils/abort.js.map +1 -1
- package/dist/utils/abortPool.cjs +26 -0
- package/dist/utils/abortPool.d.ts +4 -0
- package/dist/utils/abortPool.d.ts.map +1 -0
- package/dist/utils/abortPool.js +22 -0
- package/dist/utils/abortPool.js.map +1 -0
- package/dist/utils/backoff.cjs +12 -1
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +12 -1
- package/dist/utils/backoff.js.map +1 -1
- package/dist/utils/key.cjs +77 -0
- package/dist/utils/key.d.ts +29 -0
- package/dist/utils/key.d.ts.map +1 -0
- package/dist/utils/key.js +75 -0
- package/dist/utils/key.js.map +1 -0
- package/dist/utils/limits.cjs +40 -0
- package/dist/utils/limits.d.ts +39 -0
- package/dist/utils/limits.d.ts.map +1 -0
- package/dist/utils/limits.js +38 -0
- package/dist/utils/limits.js.map +1 -0
- package/dist/utils/sanitize.cjs +29 -0
- package/dist/utils/sanitize.d.ts +3 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +26 -0
- package/dist/utils/sanitize.js.map +1 -0
- package/dist/utils/validate.cjs +90 -16
- package/dist/utils/validate.d.ts +11 -0
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +85 -16
- package/dist/utils/validate.js.map +1 -1
- package/package.json +6 -5
package/dist/utils/backoff.cjs
CHANGED
|
@@ -33,11 +33,22 @@ function computeDelay(attempt, opts) {
|
|
|
33
33
|
delay = max;
|
|
34
34
|
delay = Math.min(delay, max);
|
|
35
35
|
// Step 3: jitter
|
|
36
|
+
// All jitter variants produce a delay in [0, delay].
|
|
37
|
+
// The `decorrelated` formula `base + random() * (delay - base)` assumes
|
|
38
|
+
// delay >= base, which can be violated when maxDelay caps below base.
|
|
39
|
+
// Fix: clamp the final result to [0, delay] to guarantee the cap holds.
|
|
36
40
|
switch (opts.jitter ?? 'full') {
|
|
37
41
|
case 'none': return delay;
|
|
38
42
|
case 'full': return Math.random() * delay;
|
|
39
43
|
case 'equal': return delay / 2 + Math.random() * delay / 2;
|
|
40
|
-
case 'decorrelated':
|
|
44
|
+
case 'decorrelated': {
|
|
45
|
+
// If delay < base (maxDelay capped below base), decorrelated degrades
|
|
46
|
+
// to full jitter — the formula would otherwise produce values > delay.
|
|
47
|
+
const lo = Math.min(base, delay);
|
|
48
|
+
const hi = delay;
|
|
49
|
+
const result = lo + Math.random() * (hi - lo);
|
|
50
|
+
return Math.max(0, Math.min(result, delay));
|
|
51
|
+
}
|
|
41
52
|
default: return delay;
|
|
42
53
|
}
|
|
43
54
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../../src/utils/backoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../../src/utils/backoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CAoCxE"}
|
package/dist/utils/backoff.js
CHANGED
|
@@ -30,11 +30,22 @@ export function computeDelay(attempt, opts) {
|
|
|
30
30
|
delay = max;
|
|
31
31
|
delay = Math.min(delay, max);
|
|
32
32
|
// Step 3: jitter
|
|
33
|
+
// All jitter variants produce a delay in [0, delay].
|
|
34
|
+
// The `decorrelated` formula `base + random() * (delay - base)` assumes
|
|
35
|
+
// delay >= base, which can be violated when maxDelay caps below base.
|
|
36
|
+
// Fix: clamp the final result to [0, delay] to guarantee the cap holds.
|
|
33
37
|
switch (opts.jitter ?? 'full') {
|
|
34
38
|
case 'none': return delay;
|
|
35
39
|
case 'full': return Math.random() * delay;
|
|
36
40
|
case 'equal': return delay / 2 + Math.random() * delay / 2;
|
|
37
|
-
case 'decorrelated':
|
|
41
|
+
case 'decorrelated': {
|
|
42
|
+
// If delay < base (maxDelay capped below base), decorrelated degrades
|
|
43
|
+
// to full jitter — the formula would otherwise produce values > delay.
|
|
44
|
+
const lo = Math.min(base, delay);
|
|
45
|
+
const hi = delay;
|
|
46
|
+
const result = lo + Math.random() * (hi - lo);
|
|
47
|
+
return Math.max(0, Math.min(result, delay));
|
|
48
|
+
}
|
|
38
49
|
default: return delay;
|
|
39
50
|
}
|
|
40
51
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backoff.js","sourceRoot":"","sources":["../../src/utils/backoff.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,IAAkB;IAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,CAAA;IAC9B,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IAExB,kBAAkB;IAClB,IAAI,KAAa,CAAA;IACjB,QAAQ,IAAI,CAAC,OAAO,IAAI,MAAM,EAAE,CAAC;QAC/B,KAAK,QAAQ;YAAO,KAAK,GAAG,IAAI,GAAG,OAAO,CAAC;YAAC,MAAK;QACjD,KAAK,aAAa;YAAE,KAAK,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;YAAC,MAAK;QAC5D,OAAO,CAAC,CAAY,KAAK,GAAG,IAAI,CAAA;IAClC,CAAC;IAED,+DAA+D;IAC/D,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,iBAAiB,CAAA;IACrD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,KAAK,GAAG,GAAG,CAAA;IACxC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAE5B,iBAAiB;IACjB,QAAQ,IAAI,CAAC,MAAM,IAAI,MAAM,EAAE,CAAC;QAC9B,KAAK,MAAM,CAAC,CAAS,OAAO,KAAK,CAAA;QACjC,KAAK,MAAM,CAAC,CAAS,OAAO,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,CAAA;QACjD,KAAK,OAAO,CAAC,CAAQ,OAAO,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,GAAG,CAAC,CAAA;QACjE,KAAK,cAAc,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"backoff.js","sourceRoot":"","sources":["../../src/utils/backoff.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,IAAkB;IAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,CAAA;IAC9B,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IAExB,kBAAkB;IAClB,IAAI,KAAa,CAAA;IACjB,QAAQ,IAAI,CAAC,OAAO,IAAI,MAAM,EAAE,CAAC;QAC/B,KAAK,QAAQ;YAAO,KAAK,GAAG,IAAI,GAAG,OAAO,CAAC;YAAC,MAAK;QACjD,KAAK,aAAa;YAAE,KAAK,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;YAAC,MAAK;QAC5D,OAAO,CAAC,CAAY,KAAK,GAAG,IAAI,CAAA;IAClC,CAAC;IAED,+DAA+D;IAC/D,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,iBAAiB,CAAA;IACrD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,KAAK,GAAG,GAAG,CAAA;IACxC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAE5B,iBAAiB;IACjB,qDAAqD;IACrD,wEAAwE;IACxE,sEAAsE;IACtE,wEAAwE;IACxE,QAAQ,IAAI,CAAC,MAAM,IAAI,MAAM,EAAE,CAAC;QAC9B,KAAK,MAAM,CAAC,CAAS,OAAO,KAAK,CAAA;QACjC,KAAK,MAAM,CAAC,CAAS,OAAO,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,CAAA;QACjD,KAAK,OAAO,CAAC,CAAQ,OAAO,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,GAAG,CAAC,CAAA;QACjE,KAAK,cAAc,CAAC,CAAC,CAAC;YACpB,sEAAsE;YACtE,uEAAuE;YACvE,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;YAChC,MAAM,EAAE,GAAG,KAAK,CAAA;YAChB,MAAM,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,CAAA;YAC7C,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;QAC7C,CAAC;QACD,OAAO,CAAC,CAAa,OAAO,KAAK,CAAA;IACnC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Key sanitisation — the first line of defence against:
|
|
4
|
+
*
|
|
5
|
+
* - **Prototype pollution**: `__proto__`, `constructor`, `prototype` as keys
|
|
6
|
+
* bypass namespace isolation in plain-object store adapters.
|
|
7
|
+
* - **Control-char injection**: `\x00`–`\x1f`, `\x7f` break some store
|
|
8
|
+
* backends (Redis Lua, HTTP header logs, JSON-LD contexts).
|
|
9
|
+
* - **CRLF injection**: `\r\n` in keys can break naive log/HTTP serializers.
|
|
10
|
+
* - **Memory exhaustion**: unbounded key length lets a caller store a 10MB
|
|
11
|
+
* string as a key, bloating the store and blocking iteration.
|
|
12
|
+
* - **Namespace collision**: user keys starting with `dedupe:`, `cache:`,
|
|
13
|
+
* or `__inflight:` would clobber internal slots.
|
|
14
|
+
*
|
|
15
|
+
* # Why a dedicated module?
|
|
16
|
+
*
|
|
17
|
+
* Key validation is security-sensitive. Centralising it here means every
|
|
18
|
+
* call site (`act()`, `invalidate()`, `withStore().invalidate()`) applies
|
|
19
|
+
* the same rules. Local inlining risks drift.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.sanitizeKey = sanitizeKey;
|
|
23
|
+
const limits_js_1 = require("./limits.js");
|
|
24
|
+
const RESERVED_PREFIXES = ['dedupe:', 'cache:', '__inflight:', '__tenant:', 'tenant:'];
|
|
25
|
+
/**
|
|
26
|
+
* Strings that, if used as Map keys, are safe — but if used as plain-object
|
|
27
|
+
* keys (e.g. a naive store adapter) enable prototype pollution. Reject them
|
|
28
|
+
* regardless of store type: the contract is "your key is safe everywhere".
|
|
29
|
+
*/
|
|
30
|
+
const FORBIDDEN_LITERALS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
31
|
+
/**
|
|
32
|
+
* Reject any character in the C0 control range, DEL (0x7f), or CRLF.
|
|
33
|
+
* Allow TAB (0x09) and LF (0x0a) since some callers embed newlines in
|
|
34
|
+
* structured keys legitimately — but CR is always forbidden.
|
|
35
|
+
*/
|
|
36
|
+
const UNSAFE_CHAR = /[\x00-\x08\x0b\x0c\x0d\x0e-\x1f\x7f]/;
|
|
37
|
+
/**
|
|
38
|
+
* Validate a user-supplied key. Throws synchronously on invalid input.
|
|
39
|
+
*
|
|
40
|
+
* Programmer errors throw — they must not be swallowed into an `ActFailure`
|
|
41
|
+
* because the caller's code is broken.
|
|
42
|
+
*
|
|
43
|
+
* @returns the same `key` (for chaining); never transforms it.
|
|
44
|
+
*/
|
|
45
|
+
function sanitizeKey(key) {
|
|
46
|
+
if (typeof key !== 'string') {
|
|
47
|
+
throw new TypeError(`Actly: key must be a string, got ${typeName(key)}`);
|
|
48
|
+
}
|
|
49
|
+
if (key.length === 0) {
|
|
50
|
+
throw new RangeError("Actly: key must be non-empty. An empty key collapses every caller " +
|
|
51
|
+
"onto the same dedupe/cache slot — almost certainly a bug.");
|
|
52
|
+
}
|
|
53
|
+
if (key.length > limits_js_1.LIMITS.MAX_KEY_LENGTH) {
|
|
54
|
+
throw new RangeError(`Actly: key length ${key.length} exceeds limit ${limits_js_1.LIMITS.MAX_KEY_LENGTH}. ` +
|
|
55
|
+
`Long keys bloat stores and slow iteration. Hash externally if you need longer keys.`);
|
|
56
|
+
}
|
|
57
|
+
if (FORBIDDEN_LITERALS.has(key)) {
|
|
58
|
+
throw new RangeError(`Actly: key ${JSON.stringify(key)} is forbidden (prototype-pollution vector). ` +
|
|
59
|
+
`Pick a different key.`);
|
|
60
|
+
}
|
|
61
|
+
if (UNSAFE_CHAR.test(key)) {
|
|
62
|
+
throw new RangeError(`Actly: key contains control characters or CRLF, which break store backends ` +
|
|
63
|
+
`and log serializers. Got ${JSON.stringify(key)}.`);
|
|
64
|
+
}
|
|
65
|
+
for (const prefix of RESERVED_PREFIXES) {
|
|
66
|
+
if (key.startsWith(prefix)) {
|
|
67
|
+
throw new RangeError(`Actly: key must not start with reserved prefix "${prefix}" ` +
|
|
68
|
+
`(got ${JSON.stringify(key)}). These namespaces are used internally.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return key;
|
|
72
|
+
}
|
|
73
|
+
function typeName(v) {
|
|
74
|
+
if (v === null)
|
|
75
|
+
return 'null';
|
|
76
|
+
return typeof v;
|
|
77
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key sanitisation — the first line of defence against:
|
|
3
|
+
*
|
|
4
|
+
* - **Prototype pollution**: `__proto__`, `constructor`, `prototype` as keys
|
|
5
|
+
* bypass namespace isolation in plain-object store adapters.
|
|
6
|
+
* - **Control-char injection**: `\x00`–`\x1f`, `\x7f` break some store
|
|
7
|
+
* backends (Redis Lua, HTTP header logs, JSON-LD contexts).
|
|
8
|
+
* - **CRLF injection**: `\r\n` in keys can break naive log/HTTP serializers.
|
|
9
|
+
* - **Memory exhaustion**: unbounded key length lets a caller store a 10MB
|
|
10
|
+
* string as a key, bloating the store and blocking iteration.
|
|
11
|
+
* - **Namespace collision**: user keys starting with `dedupe:`, `cache:`,
|
|
12
|
+
* or `__inflight:` would clobber internal slots.
|
|
13
|
+
*
|
|
14
|
+
* # Why a dedicated module?
|
|
15
|
+
*
|
|
16
|
+
* Key validation is security-sensitive. Centralising it here means every
|
|
17
|
+
* call site (`act()`, `invalidate()`, `withStore().invalidate()`) applies
|
|
18
|
+
* the same rules. Local inlining risks drift.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Validate a user-supplied key. Throws synchronously on invalid input.
|
|
22
|
+
*
|
|
23
|
+
* Programmer errors throw — they must not be swallowed into an `ActFailure`
|
|
24
|
+
* because the caller's code is broken.
|
|
25
|
+
*
|
|
26
|
+
* @returns the same `key` (for chaining); never transforms it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function sanitizeKey(key: string): string;
|
|
29
|
+
//# sourceMappingURL=key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../../src/utils/key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAoBH;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAqC/C"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key sanitisation — the first line of defence against:
|
|
3
|
+
*
|
|
4
|
+
* - **Prototype pollution**: `__proto__`, `constructor`, `prototype` as keys
|
|
5
|
+
* bypass namespace isolation in plain-object store adapters.
|
|
6
|
+
* - **Control-char injection**: `\x00`–`\x1f`, `\x7f` break some store
|
|
7
|
+
* backends (Redis Lua, HTTP header logs, JSON-LD contexts).
|
|
8
|
+
* - **CRLF injection**: `\r\n` in keys can break naive log/HTTP serializers.
|
|
9
|
+
* - **Memory exhaustion**: unbounded key length lets a caller store a 10MB
|
|
10
|
+
* string as a key, bloating the store and blocking iteration.
|
|
11
|
+
* - **Namespace collision**: user keys starting with `dedupe:`, `cache:`,
|
|
12
|
+
* or `__inflight:` would clobber internal slots.
|
|
13
|
+
*
|
|
14
|
+
* # Why a dedicated module?
|
|
15
|
+
*
|
|
16
|
+
* Key validation is security-sensitive. Centralising it here means every
|
|
17
|
+
* call site (`act()`, `invalidate()`, `withStore().invalidate()`) applies
|
|
18
|
+
* the same rules. Local inlining risks drift.
|
|
19
|
+
*/
|
|
20
|
+
import { LIMITS } from './limits.js';
|
|
21
|
+
const RESERVED_PREFIXES = ['dedupe:', 'cache:', '__inflight:', '__tenant:', 'tenant:'];
|
|
22
|
+
/**
|
|
23
|
+
* Strings that, if used as Map keys, are safe — but if used as plain-object
|
|
24
|
+
* keys (e.g. a naive store adapter) enable prototype pollution. Reject them
|
|
25
|
+
* regardless of store type: the contract is "your key is safe everywhere".
|
|
26
|
+
*/
|
|
27
|
+
const FORBIDDEN_LITERALS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
28
|
+
/**
|
|
29
|
+
* Reject any character in the C0 control range, DEL (0x7f), or CRLF.
|
|
30
|
+
* Allow TAB (0x09) and LF (0x0a) since some callers embed newlines in
|
|
31
|
+
* structured keys legitimately — but CR is always forbidden.
|
|
32
|
+
*/
|
|
33
|
+
const UNSAFE_CHAR = /[\x00-\x08\x0b\x0c\x0d\x0e-\x1f\x7f]/;
|
|
34
|
+
/**
|
|
35
|
+
* Validate a user-supplied key. Throws synchronously on invalid input.
|
|
36
|
+
*
|
|
37
|
+
* Programmer errors throw — they must not be swallowed into an `ActFailure`
|
|
38
|
+
* because the caller's code is broken.
|
|
39
|
+
*
|
|
40
|
+
* @returns the same `key` (for chaining); never transforms it.
|
|
41
|
+
*/
|
|
42
|
+
export function sanitizeKey(key) {
|
|
43
|
+
if (typeof key !== 'string') {
|
|
44
|
+
throw new TypeError(`Actly: key must be a string, got ${typeName(key)}`);
|
|
45
|
+
}
|
|
46
|
+
if (key.length === 0) {
|
|
47
|
+
throw new RangeError("Actly: key must be non-empty. An empty key collapses every caller " +
|
|
48
|
+
"onto the same dedupe/cache slot — almost certainly a bug.");
|
|
49
|
+
}
|
|
50
|
+
if (key.length > LIMITS.MAX_KEY_LENGTH) {
|
|
51
|
+
throw new RangeError(`Actly: key length ${key.length} exceeds limit ${LIMITS.MAX_KEY_LENGTH}. ` +
|
|
52
|
+
`Long keys bloat stores and slow iteration. Hash externally if you need longer keys.`);
|
|
53
|
+
}
|
|
54
|
+
if (FORBIDDEN_LITERALS.has(key)) {
|
|
55
|
+
throw new RangeError(`Actly: key ${JSON.stringify(key)} is forbidden (prototype-pollution vector). ` +
|
|
56
|
+
`Pick a different key.`);
|
|
57
|
+
}
|
|
58
|
+
if (UNSAFE_CHAR.test(key)) {
|
|
59
|
+
throw new RangeError(`Actly: key contains control characters or CRLF, which break store backends ` +
|
|
60
|
+
`and log serializers. Got ${JSON.stringify(key)}.`);
|
|
61
|
+
}
|
|
62
|
+
for (const prefix of RESERVED_PREFIXES) {
|
|
63
|
+
if (key.startsWith(prefix)) {
|
|
64
|
+
throw new RangeError(`Actly: key must not start with reserved prefix "${prefix}" ` +
|
|
65
|
+
`(got ${JSON.stringify(key)}). These namespaces are used internally.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return key;
|
|
69
|
+
}
|
|
70
|
+
function typeName(v) {
|
|
71
|
+
if (v === null)
|
|
72
|
+
return 'null';
|
|
73
|
+
return typeof v;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key.js","sourceRoot":"","sources":["../../src/utils/key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,MAAM,iBAAiB,GAAG,CAAC,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,CAAU,CAAA;AAE/F;;;;GAIG;AACH,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC,CAAA;AAE7E;;;;GAIG;AACH,MAAM,WAAW,GAAG,sCAAsC,CAAA;AAE1D;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,MAAM,IAAI,SAAS,CAAC,oCAAoC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAC1E,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,UAAU,CAClB,oEAAoE;YACpE,2DAA2D,CAC5D,CAAA;IACH,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,cAAc,EAAE,CAAC;QACvC,MAAM,IAAI,UAAU,CAClB,qBAAqB,GAAG,CAAC,MAAM,kBAAkB,MAAM,CAAC,cAAc,IAAI;YAC1E,qFAAqF,CACtF,CAAA;IACH,CAAC;IACD,IAAI,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,UAAU,CAClB,cAAc,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,8CAA8C;YAC/E,uBAAuB,CACxB,CAAA;IACH,CAAC;IACD,IAAI,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,UAAU,CAClB,6EAA6E;YAC7E,4BAA4B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CACnD,CAAA;IACH,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,iBAAiB,EAAE,CAAC;QACvC,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,UAAU,CAClB,mDAAmD,MAAM,IAAI;gBAC7D,QAAQ,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,0CAA0C,CACtE,CAAA;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAS,QAAQ,CAAC,CAAU;IAC1B,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,MAAM,CAAA;IAC7B,OAAO,OAAO,CAAC,CAAA;AACjB,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Centralised numeric limits for all actly inputs.
|
|
4
|
+
*
|
|
5
|
+
* # Why central?
|
|
6
|
+
*
|
|
7
|
+
* Every numeric input that flows into actly (retry.attempts, timeout.ms,
|
|
8
|
+
* cache.ttl, etc.) must be bounded to prevent:
|
|
9
|
+
* - memory exhaustion (e.g. `cache: { ttl: 1e15 }` → 31 700-year TTL)
|
|
10
|
+
* - CPU exhaustion (e.g. `retry: { attempts: 1e9 }`)
|
|
11
|
+
* - timer overflow (e.g. `setTimeout(fn, 1e16)` wraps to 1ms on some engines)
|
|
12
|
+
*
|
|
13
|
+
* The values are deliberately generous: they prevent pathological abuse,
|
|
14
|
+
* not legitimate use. If you need to exceed one of these limits, you almost
|
|
15
|
+
* certainly have a bug.
|
|
16
|
+
*
|
|
17
|
+
* These limits are constants — they are not runtime-configurable. If you
|
|
18
|
+
* need different limits, fork the source or wrap `act()` with your own
|
|
19
|
+
* validation layer.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.LIMITS = void 0;
|
|
23
|
+
exports.LIMITS = Object.freeze({
|
|
24
|
+
/** Maximum key length in characters. Prevents multi-MB keys bloating stores. */
|
|
25
|
+
MAX_KEY_LENGTH: 1024,
|
|
26
|
+
/** Hard ceiling on `retry.attempts`. 100 attempts ≈ 100 fn invocations. */
|
|
27
|
+
MAX_RETRY_ATTEMPTS: 100,
|
|
28
|
+
/** Maximum per-attempt / total timeout in ms. ~27 hours. */
|
|
29
|
+
MAX_TIMEOUT_MS: 100_000_000,
|
|
30
|
+
/** Maximum cache TTL in ms. ~24 hours. */
|
|
31
|
+
MAX_CACHE_TTL: 86_400_000,
|
|
32
|
+
/** Maximum delay between retries in ms. ~5 minutes. */
|
|
33
|
+
MAX_RETRY_DELAY_MS: 300_000,
|
|
34
|
+
/** Maximum dedupe inflightTtl in ms. ~24 hours. */
|
|
35
|
+
MAX_INFLIGHT_TTL: 86_400_000,
|
|
36
|
+
/** Default bound for InMemoryStore when used as the module-level default. */
|
|
37
|
+
DEFAULT_STORE_MAX_SIZE: 10_000,
|
|
38
|
+
/** Background sweep interval for the default store. */
|
|
39
|
+
DEFAULT_STORE_CLEANUP_INTERVAL_MS: 60_000,
|
|
40
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralised numeric limits for all actly inputs.
|
|
3
|
+
*
|
|
4
|
+
* # Why central?
|
|
5
|
+
*
|
|
6
|
+
* Every numeric input that flows into actly (retry.attempts, timeout.ms,
|
|
7
|
+
* cache.ttl, etc.) must be bounded to prevent:
|
|
8
|
+
* - memory exhaustion (e.g. `cache: { ttl: 1e15 }` → 31 700-year TTL)
|
|
9
|
+
* - CPU exhaustion (e.g. `retry: { attempts: 1e9 }`)
|
|
10
|
+
* - timer overflow (e.g. `setTimeout(fn, 1e16)` wraps to 1ms on some engines)
|
|
11
|
+
*
|
|
12
|
+
* The values are deliberately generous: they prevent pathological abuse,
|
|
13
|
+
* not legitimate use. If you need to exceed one of these limits, you almost
|
|
14
|
+
* certainly have a bug.
|
|
15
|
+
*
|
|
16
|
+
* These limits are constants — they are not runtime-configurable. If you
|
|
17
|
+
* need different limits, fork the source or wrap `act()` with your own
|
|
18
|
+
* validation layer.
|
|
19
|
+
*/
|
|
20
|
+
export declare const LIMITS: Readonly<{
|
|
21
|
+
/** Maximum key length in characters. Prevents multi-MB keys bloating stores. */
|
|
22
|
+
MAX_KEY_LENGTH: 1024;
|
|
23
|
+
/** Hard ceiling on `retry.attempts`. 100 attempts ≈ 100 fn invocations. */
|
|
24
|
+
MAX_RETRY_ATTEMPTS: 100;
|
|
25
|
+
/** Maximum per-attempt / total timeout in ms. ~27 hours. */
|
|
26
|
+
MAX_TIMEOUT_MS: 100000000;
|
|
27
|
+
/** Maximum cache TTL in ms. ~24 hours. */
|
|
28
|
+
MAX_CACHE_TTL: 86400000;
|
|
29
|
+
/** Maximum delay between retries in ms. ~5 minutes. */
|
|
30
|
+
MAX_RETRY_DELAY_MS: 300000;
|
|
31
|
+
/** Maximum dedupe inflightTtl in ms. ~24 hours. */
|
|
32
|
+
MAX_INFLIGHT_TTL: 86400000;
|
|
33
|
+
/** Default bound for InMemoryStore when used as the module-level default. */
|
|
34
|
+
DEFAULT_STORE_MAX_SIZE: 10000;
|
|
35
|
+
/** Background sweep interval for the default store. */
|
|
36
|
+
DEFAULT_STORE_CLEANUP_INTERVAL_MS: 60000;
|
|
37
|
+
}>;
|
|
38
|
+
export type Limits = typeof LIMITS;
|
|
39
|
+
//# sourceMappingURL=limits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../../src/utils/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,eAAO,MAAM,MAAM;IACjB,gFAAgF;;IAGhF,2EAA2E;;IAG3E,4DAA4D;;IAG5D,0CAA0C;;IAG1C,uDAAuD;;IAGvD,mDAAmD;;IAGnD,6EAA6E;;IAG7E,uDAAuD;;EAEvD,CAAA;AAEF,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAA"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralised numeric limits for all actly inputs.
|
|
3
|
+
*
|
|
4
|
+
* # Why central?
|
|
5
|
+
*
|
|
6
|
+
* Every numeric input that flows into actly (retry.attempts, timeout.ms,
|
|
7
|
+
* cache.ttl, etc.) must be bounded to prevent:
|
|
8
|
+
* - memory exhaustion (e.g. `cache: { ttl: 1e15 }` → 31 700-year TTL)
|
|
9
|
+
* - CPU exhaustion (e.g. `retry: { attempts: 1e9 }`)
|
|
10
|
+
* - timer overflow (e.g. `setTimeout(fn, 1e16)` wraps to 1ms on some engines)
|
|
11
|
+
*
|
|
12
|
+
* The values are deliberately generous: they prevent pathological abuse,
|
|
13
|
+
* not legitimate use. If you need to exceed one of these limits, you almost
|
|
14
|
+
* certainly have a bug.
|
|
15
|
+
*
|
|
16
|
+
* These limits are constants — they are not runtime-configurable. If you
|
|
17
|
+
* need different limits, fork the source or wrap `act()` with your own
|
|
18
|
+
* validation layer.
|
|
19
|
+
*/
|
|
20
|
+
export const LIMITS = Object.freeze({
|
|
21
|
+
/** Maximum key length in characters. Prevents multi-MB keys bloating stores. */
|
|
22
|
+
MAX_KEY_LENGTH: 1024,
|
|
23
|
+
/** Hard ceiling on `retry.attempts`. 100 attempts ≈ 100 fn invocations. */
|
|
24
|
+
MAX_RETRY_ATTEMPTS: 100,
|
|
25
|
+
/** Maximum per-attempt / total timeout in ms. ~27 hours. */
|
|
26
|
+
MAX_TIMEOUT_MS: 100_000_000,
|
|
27
|
+
/** Maximum cache TTL in ms. ~24 hours. */
|
|
28
|
+
MAX_CACHE_TTL: 86_400_000,
|
|
29
|
+
/** Maximum delay between retries in ms. ~5 minutes. */
|
|
30
|
+
MAX_RETRY_DELAY_MS: 300_000,
|
|
31
|
+
/** Maximum dedupe inflightTtl in ms. ~24 hours. */
|
|
32
|
+
MAX_INFLIGHT_TTL: 86_400_000,
|
|
33
|
+
/** Default bound for InMemoryStore when used as the module-level default. */
|
|
34
|
+
DEFAULT_STORE_MAX_SIZE: 10_000,
|
|
35
|
+
/** Background sweep interval for the default store. */
|
|
36
|
+
DEFAULT_STORE_CLEANUP_INTERVAL_MS: 60_000,
|
|
37
|
+
});
|
|
38
|
+
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/utils/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAClC,gFAAgF;IAChF,cAAc,EAAE,IAAI;IAEpB,2EAA2E;IAC3E,kBAAkB,EAAE,GAAG;IAEvB,4DAA4D;IAC5D,cAAc,EAAE,WAAW;IAE3B,0CAA0C;IAC1C,aAAa,EAAE,UAAU;IAEzB,uDAAuD;IACvD,kBAAkB,EAAE,OAAO;IAE3B,mDAAmD;IACnD,gBAAgB,EAAE,UAAU;IAE5B,6EAA6E;IAC7E,sBAAsB,EAAE,MAAM;IAE9B,uDAAuD;IACvD,iCAAiC,EAAE,MAAM;CAC1C,CAAC,CAAA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitizeErrorMessage = sanitizeErrorMessage;
|
|
4
|
+
exports.sanitizeError = sanitizeError;
|
|
5
|
+
const FORBIDDEN_CHARS = /[\x00-\x1f\x7f]/g;
|
|
6
|
+
const HTML_ENTITIES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
7
|
+
function sanitizeErrorMessage(msg) {
|
|
8
|
+
let str;
|
|
9
|
+
if (msg instanceof Error) {
|
|
10
|
+
str = msg.message;
|
|
11
|
+
}
|
|
12
|
+
else if (typeof msg === 'string') {
|
|
13
|
+
str = msg;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
str = String(msg ?? '');
|
|
17
|
+
}
|
|
18
|
+
str = str.replace(/[&<>"']/g, (c) => HTML_ENTITIES[c]);
|
|
19
|
+
str = str.replace(FORBIDDEN_CHARS, '');
|
|
20
|
+
return str;
|
|
21
|
+
}
|
|
22
|
+
function sanitizeError(err) {
|
|
23
|
+
if (err instanceof Error) {
|
|
24
|
+
const sanitized = new Error(sanitizeErrorMessage(err.message));
|
|
25
|
+
sanitized.name = err.name;
|
|
26
|
+
return sanitized;
|
|
27
|
+
}
|
|
28
|
+
return sanitizeErrorMessage(err);
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize.ts"],"names":[],"mappings":"AAGA,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAYzD;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAOnD"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const FORBIDDEN_CHARS = /[\x00-\x1f\x7f]/g;
|
|
2
|
+
const HTML_ENTITIES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
3
|
+
export function sanitizeErrorMessage(msg) {
|
|
4
|
+
let str;
|
|
5
|
+
if (msg instanceof Error) {
|
|
6
|
+
str = msg.message;
|
|
7
|
+
}
|
|
8
|
+
else if (typeof msg === 'string') {
|
|
9
|
+
str = msg;
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
str = String(msg ?? '');
|
|
13
|
+
}
|
|
14
|
+
str = str.replace(/[&<>"']/g, (c) => HTML_ENTITIES[c]);
|
|
15
|
+
str = str.replace(FORBIDDEN_CHARS, '');
|
|
16
|
+
return str;
|
|
17
|
+
}
|
|
18
|
+
export function sanitizeError(err) {
|
|
19
|
+
if (err instanceof Error) {
|
|
20
|
+
const sanitized = new Error(sanitizeErrorMessage(err.message));
|
|
21
|
+
sanitized.name = err.name;
|
|
22
|
+
return sanitized;
|
|
23
|
+
}
|
|
24
|
+
return sanitizeErrorMessage(err);
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=sanitize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../../src/utils/sanitize.ts"],"names":[],"mappings":"AAAA,MAAM,eAAe,GAAG,kBAAkB,CAAA;AAC1C,MAAM,aAAa,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAA;AAE9F,MAAM,UAAU,oBAAoB,CAAC,GAAY;IAC/C,IAAI,GAAW,CAAA;IACf,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;QACzB,GAAG,GAAG,GAAG,CAAC,OAAO,CAAA;IACnB,CAAC;SAAM,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACnC,GAAG,GAAG,GAAG,CAAA;IACX,CAAC;SAAM,CAAC;QACN,GAAG,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAA;IACzB,CAAC;IACD,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAA+B,CAAC,CAAC,CAAA;IACpF,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAA;IACtC,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;QACzB,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAA;QAC9D,SAAS,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAA;QACzB,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,OAAO,oBAAoB,CAAC,GAAG,CAAC,CAAA;AAClC,CAAC"}
|
package/dist/utils/validate.cjs
CHANGED
|
@@ -6,36 +6,52 @@ exports.assertTimeoutOptions = assertTimeoutOptions;
|
|
|
6
6
|
exports.assertCacheOptions = assertCacheOptions;
|
|
7
7
|
exports.assertDedupeOptions = assertDedupeOptions;
|
|
8
8
|
exports.assertOptions = assertOptions;
|
|
9
|
+
exports.assertAuditOptions = assertAuditOptions;
|
|
10
|
+
exports.assertCircuitBreakerOptions = assertCircuitBreakerOptions;
|
|
11
|
+
exports.assertBulkheadOptions = assertBulkheadOptions;
|
|
12
|
+
exports.assertRateLimitOptions = assertRateLimitOptions;
|
|
13
|
+
exports.assertHedgeOptions = assertHedgeOptions;
|
|
14
|
+
const limits_js_1 = require("./limits.js");
|
|
15
|
+
const key_js_1 = require("./key.js");
|
|
9
16
|
/**
|
|
10
17
|
* Validate user-facing option shapes. Throws `RangeError` / `TypeError` on
|
|
11
18
|
* invalid input — these are programmer errors, not runtime failures, so
|
|
12
19
|
* throwing (rather than returning an `ActFailure`) is the right call.
|
|
13
20
|
*
|
|
14
21
|
* Called once at the top of `act()` so policies can assume well-formed input.
|
|
22
|
+
*
|
|
23
|
+
* # Caps
|
|
24
|
+
*
|
|
25
|
+
* Every numeric input is bounded by {@link LIMITS}. This prevents memory
|
|
26
|
+
* exhaustion (huge TTLs), CPU exhaustion (huge retry counts), and timer
|
|
27
|
+
* overflow (huge delays). See `limits.ts` for rationale.
|
|
15
28
|
*/
|
|
16
29
|
function assertKey(key) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (key.length === 0) {
|
|
21
|
-
throw new RangeError("Actly: key must be non-empty. An empty key collapses every caller " +
|
|
22
|
-
"onto the same dedupe/cache slot — almost certainly a bug.");
|
|
23
|
-
}
|
|
24
|
-
// Reject reserved internal prefixes so user keys cannot collide with
|
|
25
|
-
// dedupe/cache namespace prefixes.
|
|
26
|
-
if (key.startsWith('dedupe:') || key.startsWith('cache:') || key.startsWith('__inflight:')) {
|
|
27
|
-
throw new RangeError(`Actly: key must not start with reserved prefix "dedupe:", "cache:", or "__inflight:" (got ${JSON.stringify(key)}).`);
|
|
28
|
-
}
|
|
30
|
+
// Delegated to the dedicated sanitiser — keeps key rules in one place
|
|
31
|
+
// for security centralisation. See `key.ts` for the full rule set.
|
|
32
|
+
(0, key_js_1.sanitizeKey)(key);
|
|
29
33
|
}
|
|
30
34
|
function assertRetryOptions(opts) {
|
|
31
35
|
if (!Number.isInteger(opts.attempts) || opts.attempts < 1) {
|
|
32
36
|
throw new RangeError(`Actly: retry.attempts must be a positive integer, got ${opts.attempts}`);
|
|
33
37
|
}
|
|
38
|
+
if (opts.attempts > limits_js_1.LIMITS.MAX_RETRY_ATTEMPTS) {
|
|
39
|
+
throw new RangeError(`Actly: retry.attempts ${opts.attempts} exceeds limit ${limits_js_1.LIMITS.MAX_RETRY_ATTEMPTS}. ` +
|
|
40
|
+
`If you genuinely need more, use an outer supervisor.`);
|
|
41
|
+
}
|
|
34
42
|
if (opts.delayMs !== undefined) {
|
|
35
|
-
assertNonNegativeFinite('retry.delayMs', opts.delayMs);
|
|
43
|
+
assertNonNegativeFinite('retry.delayMs', opts.delayMs, limits_js_1.LIMITS.MAX_RETRY_DELAY_MS);
|
|
36
44
|
}
|
|
37
45
|
if (opts.maxDelay !== undefined) {
|
|
38
|
-
assertNonNegativeFinite('retry.maxDelay', opts.maxDelay);
|
|
46
|
+
assertNonNegativeFinite('retry.maxDelay', opts.maxDelay, limits_js_1.LIMITS.MAX_RETRY_DELAY_MS);
|
|
47
|
+
}
|
|
48
|
+
if (opts.backoff !== undefined && !BACKOFF_MODES.has(opts.backoff)) {
|
|
49
|
+
throw new RangeError(`Actly: retry.backoff must be one of ${[...BACKOFF_MODES].map((m) => JSON.stringify(m)).join(' | ')}, ` +
|
|
50
|
+
`got ${JSON.stringify(opts.backoff)}`);
|
|
51
|
+
}
|
|
52
|
+
if (opts.jitter !== undefined && !JITTER_MODES.has(opts.jitter)) {
|
|
53
|
+
throw new RangeError(`Actly: retry.jitter must be one of ${[...JITTER_MODES].map((m) => JSON.stringify(m)).join(' | ')}, ` +
|
|
54
|
+
`got ${JSON.stringify(opts.jitter)}`);
|
|
39
55
|
}
|
|
40
56
|
if (opts.shouldRetry !== undefined && typeof opts.shouldRetry !== 'function') {
|
|
41
57
|
throw new TypeError(`Actly: retry.shouldRetry must be a function, got ${typeof opts.shouldRetry}`);
|
|
@@ -45,15 +61,21 @@ function assertTimeoutOptions(opts, field) {
|
|
|
45
61
|
if (typeof opts.ms !== 'number' || !Number.isFinite(opts.ms) || opts.ms <= 0) {
|
|
46
62
|
throw new RangeError(`Actly: ${field}.ms must be a positive finite number, got ${opts.ms}`);
|
|
47
63
|
}
|
|
64
|
+
if (opts.ms > limits_js_1.LIMITS.MAX_TIMEOUT_MS) {
|
|
65
|
+
throw new RangeError(`Actly: ${field}.ms ${opts.ms} exceeds limit ${limits_js_1.LIMITS.MAX_TIMEOUT_MS}.`);
|
|
66
|
+
}
|
|
48
67
|
}
|
|
49
68
|
function assertCacheOptions(opts) {
|
|
50
69
|
if (typeof opts.ttl !== 'number' || !Number.isFinite(opts.ttl) || opts.ttl <= 0) {
|
|
51
70
|
throw new RangeError(`Actly: cache.ttl must be a positive finite number, got ${opts.ttl}`);
|
|
52
71
|
}
|
|
72
|
+
if (opts.ttl > limits_js_1.LIMITS.MAX_CACHE_TTL) {
|
|
73
|
+
throw new RangeError(`Actly: cache.ttl ${opts.ttl} exceeds limit ${limits_js_1.LIMITS.MAX_CACHE_TTL} (~24h).`);
|
|
74
|
+
}
|
|
53
75
|
}
|
|
54
76
|
function assertDedupeOptions(opts) {
|
|
55
77
|
if (opts.inflightTtl !== undefined) {
|
|
56
|
-
assertNonNegativeFinite('dedupe.inflightTtl', opts.inflightTtl);
|
|
78
|
+
assertNonNegativeFinite('dedupe.inflightTtl', opts.inflightTtl, limits_js_1.LIMITS.MAX_INFLIGHT_TTL);
|
|
57
79
|
}
|
|
58
80
|
}
|
|
59
81
|
function assertOptions(options) {
|
|
@@ -71,9 +93,61 @@ function assertOptions(options) {
|
|
|
71
93
|
if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) {
|
|
72
94
|
throw new TypeError(`Actly: signal must be an AbortSignal, got ${options.signal === null ? 'null' : typeof options.signal}`);
|
|
73
95
|
}
|
|
96
|
+
if (options.circuitBreaker)
|
|
97
|
+
assertCircuitBreakerOptions(options.circuitBreaker);
|
|
98
|
+
if (options.bulkhead)
|
|
99
|
+
assertBulkheadOptions(options.bulkhead);
|
|
100
|
+
if (options.rateLimit)
|
|
101
|
+
assertRateLimitOptions(options.rateLimit);
|
|
102
|
+
if (options.hedge)
|
|
103
|
+
assertHedgeOptions(options.hedge);
|
|
104
|
+
if (options.audit)
|
|
105
|
+
assertAuditOptions(options.audit);
|
|
74
106
|
}
|
|
75
|
-
function
|
|
107
|
+
function assertAuditOptions(opts) {
|
|
108
|
+
if (typeof opts.log !== 'function') {
|
|
109
|
+
throw new TypeError(`Actly: audit.log must be a function, got ${opts.log === null ? 'null' : typeof opts.log}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function assertCircuitBreakerOptions(opts) {
|
|
113
|
+
if (!Number.isInteger(opts.threshold) || opts.threshold < 1) {
|
|
114
|
+
throw new RangeError(`Actly: circuitBreaker.threshold must be a positive integer, got ${opts.threshold}`);
|
|
115
|
+
}
|
|
116
|
+
if (typeof opts.cooldownMs !== 'number' || !Number.isFinite(opts.cooldownMs) || opts.cooldownMs <= 0) {
|
|
117
|
+
throw new RangeError(`Actly: circuitBreaker.cooldownMs must be a positive finite number, got ${opts.cooldownMs}`);
|
|
118
|
+
}
|
|
119
|
+
if (opts.resetTimeoutMs !== undefined) {
|
|
120
|
+
assertNonNegativeFinite('circuitBreaker.resetTimeoutMs', opts.resetTimeoutMs, Number.POSITIVE_INFINITY);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function assertBulkheadOptions(opts) {
|
|
124
|
+
if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent < 1) {
|
|
125
|
+
throw new RangeError(`Actly: bulkhead.maxConcurrent must be a positive integer, got ${opts.maxConcurrent}`);
|
|
126
|
+
}
|
|
127
|
+
if (opts.queueTimeoutMs !== undefined) {
|
|
128
|
+
assertNonNegativeFinite('bulkhead.queueTimeoutMs', opts.queueTimeoutMs, Number.POSITIVE_INFINITY);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function assertRateLimitOptions(opts) {
|
|
132
|
+
if (!Number.isInteger(opts.maxCalls) || opts.maxCalls < 1) {
|
|
133
|
+
throw new RangeError(`Actly: rateLimit.maxCalls must be a positive integer, got ${opts.maxCalls}`);
|
|
134
|
+
}
|
|
135
|
+
if (typeof opts.windowMs !== 'number' || !Number.isFinite(opts.windowMs) || opts.windowMs <= 0) {
|
|
136
|
+
throw new RangeError(`Actly: rateLimit.windowMs must be a positive finite number, got ${opts.windowMs}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function assertHedgeOptions(opts) {
|
|
140
|
+
if (typeof opts.delayMs !== 'number' || !Number.isFinite(opts.delayMs) || opts.delayMs <= 0) {
|
|
141
|
+
throw new RangeError(`Actly: hedge.delayMs must be a positive finite number, got ${opts.delayMs}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function assertNonNegativeFinite(field, value, max) {
|
|
76
145
|
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
77
146
|
throw new RangeError(`Actly: ${field} must be a non-negative finite number, got ${value}`);
|
|
78
147
|
}
|
|
148
|
+
if (value > max) {
|
|
149
|
+
throw new RangeError(`Actly: ${field} ${value} exceeds limit ${max}.`);
|
|
150
|
+
}
|
|
79
151
|
}
|
|
152
|
+
const BACKOFF_MODES = new Set(['none', 'linear', 'exponential']);
|
|
153
|
+
const JITTER_MODES = new Set(['none', 'full', 'equal', 'decorrelated']);
|
package/dist/utils/validate.d.ts
CHANGED
|
@@ -5,6 +5,12 @@ import type { ActOptions, CacheOptions, DedupeOptions, RetryOptions, TimeoutOpti
|
|
|
5
5
|
* throwing (rather than returning an `ActFailure`) is the right call.
|
|
6
6
|
*
|
|
7
7
|
* Called once at the top of `act()` so policies can assume well-formed input.
|
|
8
|
+
*
|
|
9
|
+
* # Caps
|
|
10
|
+
*
|
|
11
|
+
* Every numeric input is bounded by {@link LIMITS}. This prevents memory
|
|
12
|
+
* exhaustion (huge TTLs), CPU exhaustion (huge retry counts), and timer
|
|
13
|
+
* overflow (huge delays). See `limits.ts` for rationale.
|
|
8
14
|
*/
|
|
9
15
|
export declare function assertKey(key: string): void;
|
|
10
16
|
export declare function assertRetryOptions(opts: RetryOptions): void;
|
|
@@ -12,4 +18,9 @@ export declare function assertTimeoutOptions(opts: TimeoutOptions, field: string
|
|
|
12
18
|
export declare function assertCacheOptions(opts: CacheOptions): void;
|
|
13
19
|
export declare function assertDedupeOptions(opts: DedupeOptions): void;
|
|
14
20
|
export declare function assertOptions(options: ActOptions): void;
|
|
21
|
+
export declare function assertAuditOptions(opts: import('../types/index.js').AuditOptions): void;
|
|
22
|
+
export declare function assertCircuitBreakerOptions(opts: import('../types/index.js').CircuitBreakerOptions): void;
|
|
23
|
+
export declare function assertBulkheadOptions(opts: import('../types/index.js').BulkheadOptions): void;
|
|
24
|
+
export declare function assertRateLimitOptions(opts: import('../types/index.js').RateLimitOptions): void;
|
|
25
|
+
export declare function assertHedgeOptions(opts: import('../types/index.js').HedgeOptions): void;
|
|
15
26
|
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/utils/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,cAAc,EACf,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/utils/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,cAAc,EACf,MAAM,mBAAmB,CAAA;AAI1B;;;;;;;;;;;;GAYG;AAEH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI3C;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAmC3D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAW9E;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAW3D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAI7D;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAkBvD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,mBAAmB,EAAE,YAAY,GAAG,IAAI,CAMvF;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,OAAO,mBAAmB,EAAE,qBAAqB,GAAG,IAAI,CAczG;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,mBAAmB,EAAE,eAAe,GAAG,IAAI,CAS7F;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,mBAAmB,EAAE,gBAAgB,GAAG,IAAI,CAW/F;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,mBAAmB,EAAE,YAAY,GAAG,IAAI,CAMvF"}
|