ribaunt 0.2.3 → 0.2.4
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 +53 -8
- package/demo/lib/ribaunt-solver.wasm +0 -0
- package/demo/lib/solver-worker.js +174 -0
- package/demo/lib/solver-worker.js.map +1 -0
- package/demo/lib/solver.js +135 -0
- package/demo/lib/solver.js.map +1 -0
- package/demo/lib/wasm-solver.js +296 -0
- package/demo/lib/wasm-solver.js.map +1 -0
- package/demo/lib/widget-browser.js +9 -0
- package/demo/lib/widget-browser.js.map +1 -0
- package/demo/lib/widget.js +942 -0
- package/demo/lib/widget.js.map +1 -0
- package/demo/lib/worker-client.js +128 -0
- package/demo/lib/worker-client.js.map +1 -0
- package/dist/cjs/index.d.ts +24 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +160 -13
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/risk.d.ts +124 -0
- package/dist/cjs/risk.d.ts.map +1 -0
- package/dist/cjs/risk.js +213 -0
- package/dist/cjs/risk.js.map +1 -0
- package/dist/index.d.ts +24 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +158 -12
- package/dist/index.js.map +1 -1
- package/dist/ribaunt-solver.wasm +0 -0
- package/dist/risk.d.ts +124 -0
- package/dist/risk.d.ts.map +1 -0
- package/dist/risk.js +202 -0
- package/dist/risk.js.map +1 -0
- package/dist/solver-worker.d.ts.map +1 -1
- package/dist/solver-worker.js +135 -5
- package/dist/solver-worker.js.map +1 -1
- package/dist/solver.d.ts +1 -0
- package/dist/solver.d.ts.map +1 -1
- package/dist/solver.js +38 -3
- package/dist/solver.js.map +1 -1
- package/dist/wasm-solver.d.ts +29 -0
- package/dist/wasm-solver.d.ts.map +1 -0
- package/dist/wasm-solver.js +296 -0
- package/dist/wasm-solver.js.map +1 -0
- package/dist/widget-browser.d.ts +3 -2
- package/dist/widget-browser.d.ts.map +1 -1
- package/dist/widget-browser.js +1 -1
- package/dist/widget-browser.js.map +1 -1
- package/dist/widget-react.d.ts +3 -1
- package/dist/widget-react.d.ts.map +1 -1
- package/dist/widget-react.js +99 -15
- package/dist/widget-react.js.map +1 -1
- package/dist/widget.d.ts +35 -15
- package/dist/widget.d.ts.map +1 -1
- package/dist/widget.js +61 -10
- package/dist/widget.js.map +1 -1
- package/dist/worker-client.d.ts +7 -1
- package/dist/worker-client.d.ts.map +1 -1
- package/dist/worker-client.js +32 -5
- package/dist/worker-client.js.map +1 -1
- package/package.json +19 -7
- package/context7.json +0 -4
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ribaunt Risk Engine — programmable risk-assessment subsystem.
|
|
3
|
+
*
|
|
4
|
+
* This module is intentionally small, stateless, and caller-driven.
|
|
5
|
+
* All incoming signals are caller-supplied and treated as untrusted
|
|
6
|
+
* inputs. The default scorer is a transparent, deterministic heuristic
|
|
7
|
+
* (not an ML/fraud probability model) that can be inspected and replaced.
|
|
8
|
+
*
|
|
9
|
+
* Scoring is CPU-only, O(1) over the small set of known signals, and
|
|
10
|
+
* performs no I/O.
|
|
11
|
+
*/
|
|
12
|
+
import type { ClientCalibration, Workload, WorkloadBounds } from './index.js';
|
|
13
|
+
export interface RiskSignals {
|
|
14
|
+
ip?: string;
|
|
15
|
+
userAgent?: string;
|
|
16
|
+
accountAgeSeconds?: number;
|
|
17
|
+
requestVelocity?: number;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface RiskScorer {
|
|
21
|
+
score(signals: RiskSignals): number | Promise<number>;
|
|
22
|
+
}
|
|
23
|
+
export interface RiskThresholds {
|
|
24
|
+
challenge: number;
|
|
25
|
+
block: number;
|
|
26
|
+
}
|
|
27
|
+
export interface AssessWorkloadOptions extends WorkloadBounds {
|
|
28
|
+
targetDurationMs?: number;
|
|
29
|
+
calibration?: ClientCalibration;
|
|
30
|
+
}
|
|
31
|
+
export interface AssessOptions {
|
|
32
|
+
signals: RiskSignals;
|
|
33
|
+
scorer?: RiskScorer;
|
|
34
|
+
thresholds?: RiskThresholds;
|
|
35
|
+
workload?: AssessWorkloadOptions;
|
|
36
|
+
}
|
|
37
|
+
export interface RiskAssessment {
|
|
38
|
+
risk: number;
|
|
39
|
+
action: 'allow' | 'challenge' | 'block';
|
|
40
|
+
workload?: Workload;
|
|
41
|
+
}
|
|
42
|
+
export declare const DEFAULT_RISK_THRESHOLDS: RiskThresholds;
|
|
43
|
+
/**
|
|
44
|
+
* Validate thresholds. Throws if invalid rather than silently repairing.
|
|
45
|
+
* Requires 0 <= challenge < block <= 100
|
|
46
|
+
*/
|
|
47
|
+
export declare function validateRiskThresholds(thresholds: RiskThresholds): void;
|
|
48
|
+
/**
|
|
49
|
+
* Validate scorer output. Custom scorers must return a finite number
|
|
50
|
+
* between 0 and 100 inclusive. Do not silently clamp — reject as a
|
|
51
|
+
* configuration/programming error so broken policies are visible.
|
|
52
|
+
*/
|
|
53
|
+
export declare function validateScorerOutput(value: unknown): number;
|
|
54
|
+
export declare function clampRisk(value: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* Normalize account age to a bounded risk contribution (0..30).
|
|
57
|
+
*
|
|
58
|
+
* Younger accounts contribute more risk than older accounts.
|
|
59
|
+
* - Negative ages are invalid/ignored (0)
|
|
60
|
+
* - NaN / Infinity are ignored (0)
|
|
61
|
+
* - Very large values saturate to 0 rather than overflow (no dominance)
|
|
62
|
+
*
|
|
63
|
+
* Buckets are documented and deterministic:
|
|
64
|
+
* < 60s -> 30
|
|
65
|
+
* < 1h -> 25
|
|
66
|
+
* < 1d -> 20
|
|
67
|
+
* < 7d -> 15
|
|
68
|
+
* < 30d -> 10
|
|
69
|
+
* < 90d -> 5
|
|
70
|
+
* >= 90d -> 0
|
|
71
|
+
*/
|
|
72
|
+
export declare function normalizeAccountAge(value: unknown): number;
|
|
73
|
+
/**
|
|
74
|
+
* Normalize caller-derived request velocity to a bounded contribution (0..40).
|
|
75
|
+
*
|
|
76
|
+
* Higher velocity contributes more risk.
|
|
77
|
+
* - Negative values are invalid/ignored
|
|
78
|
+
* - Non-finite values are ignored
|
|
79
|
+
* - Saturates rather than growing without bound
|
|
80
|
+
*
|
|
81
|
+
* Buckets:
|
|
82
|
+
* < 1 -> 0
|
|
83
|
+
* < 5 -> 10
|
|
84
|
+
* < 20 -> 20
|
|
85
|
+
* < 60 -> 30
|
|
86
|
+
* < 200 -> 35
|
|
87
|
+
* >= 200 -> 40
|
|
88
|
+
*
|
|
89
|
+
* The caller-derived velocity is not assumed to be authoritative; it is
|
|
90
|
+
* a weak heuristic contributed to the total score.
|
|
91
|
+
*/
|
|
92
|
+
export declare function normalizeRequestVelocity(value: unknown): number;
|
|
93
|
+
/**
|
|
94
|
+
* Inspect user-agent signal as a weak signal only.
|
|
95
|
+
*
|
|
96
|
+
* - Missing or non-string UA -> 0 (not treated as strong signal)
|
|
97
|
+
* - Empty / whitespace-only UA -> 5
|
|
98
|
+
* - Very short UA (<10 chars) -> 10
|
|
99
|
+
* - Otherwise -> 0
|
|
100
|
+
*
|
|
101
|
+
* No fragile substring bans (e.g. `ua.includes('bot')`) are used.
|
|
102
|
+
* UA classification is not treated as trustworthy.
|
|
103
|
+
*/
|
|
104
|
+
export declare function scoreUserAgent(value: unknown): number;
|
|
105
|
+
/**
|
|
106
|
+
* IP is accepted for custom scorers but the default scorer does not
|
|
107
|
+
* pretend to know whether an IP is malicious. The safest v1 choice is
|
|
108
|
+
* to return 0 and let custom scorers handle IP reputation if needed.
|
|
109
|
+
*/
|
|
110
|
+
export declare function scoreIp(_value: unknown): number;
|
|
111
|
+
/**
|
|
112
|
+
* Deterministic default scorer. Transparent heuristic:
|
|
113
|
+
* signals -> normalizeAccountAge + normalizeRequestVelocity + scoreUserAgent + scoreIp -> clamp 0..100
|
|
114
|
+
*
|
|
115
|
+
* Unknown properties on RiskSignals are ignored by the default scorer;
|
|
116
|
+
* custom scorers may use them.
|
|
117
|
+
*
|
|
118
|
+
* This scorer is synchronous internally but exposed as async via the
|
|
119
|
+
* RiskScorer interface so callers can supply remote/model-based scorers
|
|
120
|
+
* without changing the API.
|
|
121
|
+
*/
|
|
122
|
+
export declare function defaultScore(signals: RiskSignals): number;
|
|
123
|
+
export declare const defaultScorer: RiskScorer;
|
|
124
|
+
//# sourceMappingURL=risk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"risk.d.ts","sourceRoot":"","sources":["../../src/risk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAI9E,MAAM,WAAW,WAAW;IAC1B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,qBAAsB,SAAQ,cAAc;IAC3D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,iBAAiB,CAAC;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,QAAQ,CAAC,EAAE,qBAAqB,CAAC;CAClC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,GAAG,WAAW,GAAG,OAAO,CAAC;IACxC,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAmBD,eAAO,MAAM,uBAAuB,EAAE,cAAyC,CAAC;AAIhF;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI,CAsBvE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAW3D;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG/C;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAW1D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAU/D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMrD;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAE/C;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAOzD;AAED,eAAO,MAAM,aAAa,EAAE,UAI3B,CAAC"}
|
package/dist/cjs/risk.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Ribaunt Risk Engine — programmable risk-assessment subsystem.
|
|
4
|
+
*
|
|
5
|
+
* This module is intentionally small, stateless, and caller-driven.
|
|
6
|
+
* All incoming signals are caller-supplied and treated as untrusted
|
|
7
|
+
* inputs. The default scorer is a transparent, deterministic heuristic
|
|
8
|
+
* (not an ML/fraud probability model) that can be inspected and replaced.
|
|
9
|
+
*
|
|
10
|
+
* Scoring is CPU-only, O(1) over the small set of known signals, and
|
|
11
|
+
* performs no I/O.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.defaultScorer = exports.DEFAULT_RISK_THRESHOLDS = void 0;
|
|
15
|
+
exports.validateRiskThresholds = validateRiskThresholds;
|
|
16
|
+
exports.validateScorerOutput = validateScorerOutput;
|
|
17
|
+
exports.clampRisk = clampRisk;
|
|
18
|
+
exports.normalizeAccountAge = normalizeAccountAge;
|
|
19
|
+
exports.normalizeRequestVelocity = normalizeRequestVelocity;
|
|
20
|
+
exports.scoreUserAgent = scoreUserAgent;
|
|
21
|
+
exports.scoreIp = scoreIp;
|
|
22
|
+
exports.defaultScore = defaultScore;
|
|
23
|
+
// ── Default policy values ──────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Sensible v1 defaults. These numbers are policy defaults, not claims
|
|
26
|
+
* about a statistically calibrated fraud model. Consumers should tune
|
|
27
|
+
* them to their application.
|
|
28
|
+
*
|
|
29
|
+
* Semantics:
|
|
30
|
+
* risk < challenge -> allow
|
|
31
|
+
* challenge <= risk < block -> challenge
|
|
32
|
+
* risk >= block -> block
|
|
33
|
+
*/
|
|
34
|
+
const _DEFAULT_RISK_THRESHOLDS = Object.freeze({
|
|
35
|
+
challenge: 40,
|
|
36
|
+
block: 80,
|
|
37
|
+
});
|
|
38
|
+
exports.DEFAULT_RISK_THRESHOLDS = _DEFAULT_RISK_THRESHOLDS;
|
|
39
|
+
// ── Validation helpers ─────────────────────────────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* Validate thresholds. Throws if invalid rather than silently repairing.
|
|
42
|
+
* Requires 0 <= challenge < block <= 100
|
|
43
|
+
*/
|
|
44
|
+
function validateRiskThresholds(thresholds) {
|
|
45
|
+
if (!thresholds || typeof thresholds !== 'object' || Array.isArray(thresholds)) {
|
|
46
|
+
throw new Error('Risk thresholds must be an object');
|
|
47
|
+
}
|
|
48
|
+
const c = thresholds.challenge;
|
|
49
|
+
const b = thresholds.block;
|
|
50
|
+
if (typeof c !== 'number' || !Number.isFinite(c)) {
|
|
51
|
+
throw new Error('Challenge threshold must be a finite number');
|
|
52
|
+
}
|
|
53
|
+
if (typeof b !== 'number' || !Number.isFinite(b)) {
|
|
54
|
+
throw new Error('Block threshold must be a finite number');
|
|
55
|
+
}
|
|
56
|
+
if (c < 0 || c > 100) {
|
|
57
|
+
throw new Error('Challenge threshold must be between 0 and 100');
|
|
58
|
+
}
|
|
59
|
+
if (b < 0 || b > 100) {
|
|
60
|
+
throw new Error('Block threshold must be between 0 and 100');
|
|
61
|
+
}
|
|
62
|
+
if (c >= b) {
|
|
63
|
+
throw new Error('Challenge threshold must be less than block threshold');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validate scorer output. Custom scorers must return a finite number
|
|
68
|
+
* between 0 and 100 inclusive. Do not silently clamp — reject as a
|
|
69
|
+
* configuration/programming error so broken policies are visible.
|
|
70
|
+
*/
|
|
71
|
+
function validateScorerOutput(value) {
|
|
72
|
+
if (typeof value !== 'number') {
|
|
73
|
+
throw new Error('Scorer must return a finite number between 0 and 100');
|
|
74
|
+
}
|
|
75
|
+
if (!Number.isFinite(value)) {
|
|
76
|
+
throw new Error('Scorer must return a finite number between 0 and 100');
|
|
77
|
+
}
|
|
78
|
+
if (value < 0 || value > 100) {
|
|
79
|
+
throw new Error('Scorer must return a number between 0 and 100');
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
function clampRisk(value) {
|
|
84
|
+
if (!Number.isFinite(value))
|
|
85
|
+
return 0;
|
|
86
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
87
|
+
}
|
|
88
|
+
// ── Normalizer / rule helpers (default scorer pipeline) ───────────────────
|
|
89
|
+
/**
|
|
90
|
+
* Normalize account age to a bounded risk contribution (0..30).
|
|
91
|
+
*
|
|
92
|
+
* Younger accounts contribute more risk than older accounts.
|
|
93
|
+
* - Negative ages are invalid/ignored (0)
|
|
94
|
+
* - NaN / Infinity are ignored (0)
|
|
95
|
+
* - Very large values saturate to 0 rather than overflow (no dominance)
|
|
96
|
+
*
|
|
97
|
+
* Buckets are documented and deterministic:
|
|
98
|
+
* < 60s -> 30
|
|
99
|
+
* < 1h -> 25
|
|
100
|
+
* < 1d -> 20
|
|
101
|
+
* < 7d -> 15
|
|
102
|
+
* < 30d -> 10
|
|
103
|
+
* < 90d -> 5
|
|
104
|
+
* >= 90d -> 0
|
|
105
|
+
*/
|
|
106
|
+
function normalizeAccountAge(value) {
|
|
107
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
if (value < 60)
|
|
111
|
+
return 30;
|
|
112
|
+
if (value < 3600)
|
|
113
|
+
return 25;
|
|
114
|
+
if (value < 86400)
|
|
115
|
+
return 20;
|
|
116
|
+
if (value < 604800)
|
|
117
|
+
return 15;
|
|
118
|
+
if (value < 2592000)
|
|
119
|
+
return 10;
|
|
120
|
+
if (value < 7776000)
|
|
121
|
+
return 5;
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Normalize caller-derived request velocity to a bounded contribution (0..40).
|
|
126
|
+
*
|
|
127
|
+
* Higher velocity contributes more risk.
|
|
128
|
+
* - Negative values are invalid/ignored
|
|
129
|
+
* - Non-finite values are ignored
|
|
130
|
+
* - Saturates rather than growing without bound
|
|
131
|
+
*
|
|
132
|
+
* Buckets:
|
|
133
|
+
* < 1 -> 0
|
|
134
|
+
* < 5 -> 10
|
|
135
|
+
* < 20 -> 20
|
|
136
|
+
* < 60 -> 30
|
|
137
|
+
* < 200 -> 35
|
|
138
|
+
* >= 200 -> 40
|
|
139
|
+
*
|
|
140
|
+
* The caller-derived velocity is not assumed to be authoritative; it is
|
|
141
|
+
* a weak heuristic contributed to the total score.
|
|
142
|
+
*/
|
|
143
|
+
function normalizeRequestVelocity(value) {
|
|
144
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
if (value < 1)
|
|
148
|
+
return 0;
|
|
149
|
+
if (value < 5)
|
|
150
|
+
return 10;
|
|
151
|
+
if (value < 20)
|
|
152
|
+
return 20;
|
|
153
|
+
if (value < 60)
|
|
154
|
+
return 30;
|
|
155
|
+
if (value < 200)
|
|
156
|
+
return 35;
|
|
157
|
+
return 40;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Inspect user-agent signal as a weak signal only.
|
|
161
|
+
*
|
|
162
|
+
* - Missing or non-string UA -> 0 (not treated as strong signal)
|
|
163
|
+
* - Empty / whitespace-only UA -> 5
|
|
164
|
+
* - Very short UA (<10 chars) -> 10
|
|
165
|
+
* - Otherwise -> 0
|
|
166
|
+
*
|
|
167
|
+
* No fragile substring bans (e.g. `ua.includes('bot')`) are used.
|
|
168
|
+
* UA classification is not treated as trustworthy.
|
|
169
|
+
*/
|
|
170
|
+
function scoreUserAgent(value) {
|
|
171
|
+
if (typeof value !== 'string')
|
|
172
|
+
return 0;
|
|
173
|
+
const trimmed = value.trim();
|
|
174
|
+
if (trimmed.length === 0)
|
|
175
|
+
return 5;
|
|
176
|
+
if (trimmed.length < 10)
|
|
177
|
+
return 10;
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* IP is accepted for custom scorers but the default scorer does not
|
|
182
|
+
* pretend to know whether an IP is malicious. The safest v1 choice is
|
|
183
|
+
* to return 0 and let custom scorers handle IP reputation if needed.
|
|
184
|
+
*/
|
|
185
|
+
function scoreIp(_value) {
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
// ── Default scorer ─────────────────────────────────────────────────────────
|
|
189
|
+
/**
|
|
190
|
+
* Deterministic default scorer. Transparent heuristic:
|
|
191
|
+
* signals -> normalizeAccountAge + normalizeRequestVelocity + scoreUserAgent + scoreIp -> clamp 0..100
|
|
192
|
+
*
|
|
193
|
+
* Unknown properties on RiskSignals are ignored by the default scorer;
|
|
194
|
+
* custom scorers may use them.
|
|
195
|
+
*
|
|
196
|
+
* This scorer is synchronous internally but exposed as async via the
|
|
197
|
+
* RiskScorer interface so callers can supply remote/model-based scorers
|
|
198
|
+
* without changing the API.
|
|
199
|
+
*/
|
|
200
|
+
function defaultScore(signals) {
|
|
201
|
+
const age = normalizeAccountAge(signals.accountAgeSeconds);
|
|
202
|
+
const velocity = normalizeRequestVelocity(signals.requestVelocity);
|
|
203
|
+
const ua = scoreUserAgent(signals.userAgent);
|
|
204
|
+
const ip = scoreIp(signals.ip);
|
|
205
|
+
const raw = age + velocity + ua + ip;
|
|
206
|
+
return clampRisk(raw);
|
|
207
|
+
}
|
|
208
|
+
exports.defaultScorer = {
|
|
209
|
+
async score(signals) {
|
|
210
|
+
return defaultScore(signals);
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
//# sourceMappingURL=risk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"risk.js","sourceRoot":"","sources":["../../src/risk.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { DEFAULT_RISK_THRESHOLDS } from './risk.js';
|
|
2
|
+
import type { AssessOptions, RiskAssessment } from './risk.js';
|
|
3
|
+
export type { AssessOptions, AssessWorkloadOptions, RiskAssessment, RiskScorer, RiskSignals, RiskThresholds } from './risk.js';
|
|
4
|
+
export { DEFAULT_RISK_THRESHOLDS };
|
|
1
5
|
export type ChallengeToken = string;
|
|
2
6
|
export interface ChallengeSolution {
|
|
3
7
|
nonce: string;
|
|
@@ -60,7 +64,7 @@ export interface VerifySolutionOptions {
|
|
|
60
64
|
rateLimiter?: RateLimiter;
|
|
61
65
|
onEvent?: (event: RibauntEvent) => void;
|
|
62
66
|
}
|
|
63
|
-
export type VerifyFailureReason = 'invalid-token' | 'expired-token' | 'invalid-solution' | 'context-mismatch' | 'replay-detected' | 'configuration-error';
|
|
67
|
+
export type VerifyFailureReason = 'invalid-token' | 'expired-token' | 'invalid-solution' | 'context-mismatch' | 'replay-detected' | 'replay-store-unavailable' | 'configuration-error';
|
|
64
68
|
export type VerifyWarningReason = VerifyFailureReason;
|
|
65
69
|
export interface VerifyWarning {
|
|
66
70
|
reason: VerifyWarningReason;
|
|
@@ -99,6 +103,25 @@ export declare class LocalReplayStore implements ReplayStore {
|
|
|
99
103
|
* Selects bounded proof-of-work using a server-owned risk floor and untrusted timing calibration.
|
|
100
104
|
*/
|
|
101
105
|
export declare function selectWorkload(options?: AdaptiveWorkloadOptions): Workload;
|
|
106
|
+
/**
|
|
107
|
+
* Programmable risk-assessment subsystem.
|
|
108
|
+
*
|
|
109
|
+
* All signals are caller-supplied and treated as untrusted inputs.
|
|
110
|
+
* The default scorer is a transparent heuristic (not an ML model).
|
|
111
|
+
* The returned `risk` is a bounded heuristic score, not a probability
|
|
112
|
+
* or identity confidence. The caller is responsible for obtaining
|
|
113
|
+
* trustworthy inputs.
|
|
114
|
+
*
|
|
115
|
+
* Flow:
|
|
116
|
+
* validate AssessOptions
|
|
117
|
+
* -> resolve scorer (custom or default)
|
|
118
|
+
* -> await scorer.score(signals)
|
|
119
|
+
* -> validate risk 0..100
|
|
120
|
+
* -> resolve thresholds (custom or DEFAULT_RISK_THRESHOLDS)
|
|
121
|
+
* -> apply policy (allow / challenge / block)
|
|
122
|
+
* -> if challenge, delegate to existing selectWorkload() with riskScore
|
|
123
|
+
*/
|
|
124
|
+
export declare function assess(options: AssessOptions): Promise<RiskAssessment>;
|
|
102
125
|
export declare function calibrateNode(iterations?: number): ClientCalibration;
|
|
103
126
|
export declare const calibrateClient: typeof calibrateNode;
|
|
104
127
|
export declare function createChallenge(difficulty?: number, amount?: number, ttlSeconds?: number): Promise<ChallengeToken[]>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,uBAAuB,EAIxB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EACV,aAAa,EAEb,cAAc,EAIf,MAAM,WAAW,CAAC;AAGnB,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC/H,OAAO,EAAE,uBAAuB,EAAE,CAAC;AAUnC,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC;AAEpC,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAwB,SAAQ,cAAc;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,iBAAiB,CAAC;CACjC;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,YAAY,GAAG,QAAQ,CAAC,CAAC;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CACzC;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D,WAAW,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvC;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,IAAI,kBAAkB;gBAEnB,OAAO,SAAwB;CAI5C;AAED,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEnE,MAAM,WAAW,qBAAqB;IACpC,gBAAgB,CAAC,EAAE,oBAAoB,CAAC;IACxC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAC7C,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CACzC;AAED,MAAM,MAAM,mBAAmB,GAC3B,eAAe,GACf,eAAe,GACf,kBAAkB,GAClB,kBAAkB,GAClB,iBAAiB,GACjB,0BAA0B,GAC1B,qBAAqB,CAAC;AAE1B,MAAM,MAAM,mBAAmB,GAAG,mBAAmB,CAAC;AAEtD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,mBAAmB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAChE;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,GAC1B;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7E,MAAM,MAAM,oBAAoB,GAC5B;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GACf;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE,MAAM,WAAW,qBAAqB;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,gBAAiB,YAAW,WAAW;IAClD,OAAO,CAAC,UAAU,CAA6B;IAEzC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIzD,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAatE,OAAO,CAAC,OAAO;CAMhB;AAkGD;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,uBAA4B,GAAG,QAAQ,CAuB9E;AAyDD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CA2D5E;AAED,wBAAgB,aAAa,CAAC,UAAU,SAAM,GAAG,iBAAiB,CAejE;AAED,eAAO,MAAM,eAAe,sBAAgB,CAAC;AAsD7C,wBAAsB,eAAe,CACnC,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,MAAM,EACf,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;AAC7B,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;AA4E5F,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,iBAAiB,GAAG,SAAS,CAAC;AACtH,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,iBAAiB,EAAE,GAAG,SAAS,CAAC;AA4E1H,wBAAsB,cAAc,CAClC,KAAK,EAAE,cAAc,GAAG,cAAc,EAAE,EACxC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,iBAAiB,GAAG,iBAAiB,EAAE,EACzF,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,oBAAoB,CAAC,CA0F/B"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import jwt from 'jsonwebtoken';
|
|
3
|
+
import { DEFAULT_RISK_THRESHOLDS, defaultScorer, validateRiskThresholds, validateScorerOutput, } from './risk.js';
|
|
4
|
+
export { DEFAULT_RISK_THRESHOLDS };
|
|
3
5
|
export class RateLimitedError extends Error {
|
|
4
6
|
constructor(message = 'Rate limit exceeded') {
|
|
5
7
|
super(message);
|
|
@@ -40,6 +42,9 @@ const DEFAULT_BOUNDS = {
|
|
|
40
42
|
minAmount: 1,
|
|
41
43
|
maxAmount: 8,
|
|
42
44
|
};
|
|
45
|
+
const MAX_WORKLOAD_DIFFICULTY = 64;
|
|
46
|
+
const MAX_WORKLOAD_AMOUNT = 64;
|
|
47
|
+
const MAX_WORKLOAD_CANDIDATES = 10000;
|
|
43
48
|
function assertFiniteInteger(value, name, minimum) {
|
|
44
49
|
if (!Number.isFinite(value))
|
|
45
50
|
throw new Error(`${name} must be a finite number`);
|
|
@@ -72,9 +77,25 @@ function assertRange(value, name, minimum, maximum) {
|
|
|
72
77
|
}
|
|
73
78
|
function normalizeBounds(options) {
|
|
74
79
|
const minDifficulty = assertFiniteInteger(options.minDifficulty ?? DEFAULT_BOUNDS.minDifficulty, 'Minimum difficulty', 1);
|
|
80
|
+
if (minDifficulty > MAX_WORKLOAD_DIFFICULTY) {
|
|
81
|
+
throw new Error(`Minimum difficulty must be at most ${MAX_WORKLOAD_DIFFICULTY}`);
|
|
82
|
+
}
|
|
75
83
|
const maxDifficulty = assertFiniteInteger(options.maxDifficulty ?? DEFAULT_BOUNDS.maxDifficulty, 'Maximum difficulty', minDifficulty);
|
|
84
|
+
if (maxDifficulty > MAX_WORKLOAD_DIFFICULTY) {
|
|
85
|
+
throw new Error(`Maximum difficulty must be at most ${MAX_WORKLOAD_DIFFICULTY}`);
|
|
86
|
+
}
|
|
76
87
|
const minAmount = assertFiniteInteger(options.minAmount ?? DEFAULT_BOUNDS.minAmount, 'Minimum amount', 1);
|
|
88
|
+
if (minAmount > MAX_WORKLOAD_AMOUNT) {
|
|
89
|
+
throw new Error(`Minimum amount must be at most ${MAX_WORKLOAD_AMOUNT}`);
|
|
90
|
+
}
|
|
77
91
|
const maxAmount = assertFiniteInteger(options.maxAmount ?? DEFAULT_BOUNDS.maxAmount, 'Maximum amount', minAmount);
|
|
92
|
+
if (maxAmount > MAX_WORKLOAD_AMOUNT) {
|
|
93
|
+
throw new Error(`Maximum amount must be at most ${MAX_WORKLOAD_AMOUNT}`);
|
|
94
|
+
}
|
|
95
|
+
const candidateCount = (maxDifficulty - minDifficulty + 1) * (maxAmount - minAmount + 1);
|
|
96
|
+
if (candidateCount > MAX_WORKLOAD_CANDIDATES) {
|
|
97
|
+
throw new Error(`Workload bounds too large: candidate count ${candidateCount} exceeds ${MAX_WORKLOAD_CANDIDATES}`);
|
|
98
|
+
}
|
|
78
99
|
return { minDifficulty, maxDifficulty, minAmount, maxAmount };
|
|
79
100
|
}
|
|
80
101
|
function closestWorkload(targetAttempts, bounds) {
|
|
@@ -112,6 +133,131 @@ export function selectWorkload(options = {}) {
|
|
|
112
133
|
const maximumAttempts = (16 ** bounds.maxDifficulty) * bounds.maxAmount;
|
|
113
134
|
return closestWorkload(Math.min(targetAttempts, maximumAttempts), bounds);
|
|
114
135
|
}
|
|
136
|
+
function validateAssessWorkloadOptions(workload) {
|
|
137
|
+
// Reuse same validation messages as normalizeBounds / selectWorkload for regression safety
|
|
138
|
+
if (workload.minDifficulty !== undefined) {
|
|
139
|
+
assertFiniteInteger(workload.minDifficulty, 'Minimum difficulty', 1);
|
|
140
|
+
}
|
|
141
|
+
if (workload.maxDifficulty !== undefined) {
|
|
142
|
+
// Need to ensure we validate max >= min (using resolved min)
|
|
143
|
+
const min = workload.minDifficulty !== undefined ? Math.floor(workload.minDifficulty) : DEFAULT_BOUNDS.minDifficulty;
|
|
144
|
+
assertFiniteInteger(workload.maxDifficulty, 'Maximum difficulty', min);
|
|
145
|
+
}
|
|
146
|
+
else if (workload.minDifficulty !== undefined) {
|
|
147
|
+
// If only minDifficulty provided, still need to ensure default max >= min
|
|
148
|
+
if (DEFAULT_BOUNDS.maxDifficulty < Math.floor(workload.minDifficulty)) {
|
|
149
|
+
throw new Error(`Maximum difficulty must be at least ${Math.floor(workload.minDifficulty)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (workload.minAmount !== undefined) {
|
|
153
|
+
assertFiniteInteger(workload.minAmount, 'Minimum amount', 1);
|
|
154
|
+
}
|
|
155
|
+
if (workload.maxAmount !== undefined) {
|
|
156
|
+
const minAmt = workload.minAmount !== undefined ? Math.floor(workload.minAmount) : DEFAULT_BOUNDS.minAmount;
|
|
157
|
+
assertFiniteInteger(workload.maxAmount, 'Maximum amount', minAmt);
|
|
158
|
+
}
|
|
159
|
+
else if (workload.minAmount !== undefined) {
|
|
160
|
+
if (DEFAULT_BOUNDS.maxAmount < Math.floor(workload.minAmount)) {
|
|
161
|
+
throw new Error(`Maximum amount must be at least ${Math.floor(workload.minAmount)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Cross-check when both are provided, normalizeBounds already checks but we also check explicit ordering
|
|
165
|
+
// Also need to handle case where both provided but max < min — already caught above with min as floor.
|
|
166
|
+
// For completeness, if both undefined, no check needed.
|
|
167
|
+
// Validate targetDurationMs
|
|
168
|
+
if (workload.targetDurationMs !== undefined) {
|
|
169
|
+
assertFiniteInteger(workload.targetDurationMs, 'Target duration', 1);
|
|
170
|
+
}
|
|
171
|
+
// Validate calibration
|
|
172
|
+
if (workload.calibration !== undefined) {
|
|
173
|
+
if (!workload.calibration || typeof workload.calibration !== 'object' || Array.isArray(workload.calibration)) {
|
|
174
|
+
throw new Error('Calibration must be an object');
|
|
175
|
+
}
|
|
176
|
+
const cal = workload.calibration;
|
|
177
|
+
assertFiniteInteger(cal.iterations, 'Calibration iterations', 1);
|
|
178
|
+
assertFiniteInteger(cal.durationMs, 'Calibration duration', 1);
|
|
179
|
+
}
|
|
180
|
+
// If any of min/max are provided, also run through normalizeBounds to ensure combined validation matches selectWorkload exactly
|
|
181
|
+
// This catches edge cases like non-finite values already handled, but ensures parity
|
|
182
|
+
if (workload.minDifficulty !== undefined ||
|
|
183
|
+
workload.maxDifficulty !== undefined ||
|
|
184
|
+
workload.minAmount !== undefined ||
|
|
185
|
+
workload.maxAmount !== undefined) {
|
|
186
|
+
normalizeBounds(workload);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Programmable risk-assessment subsystem.
|
|
191
|
+
*
|
|
192
|
+
* All signals are caller-supplied and treated as untrusted inputs.
|
|
193
|
+
* The default scorer is a transparent heuristic (not an ML model).
|
|
194
|
+
* The returned `risk` is a bounded heuristic score, not a probability
|
|
195
|
+
* or identity confidence. The caller is responsible for obtaining
|
|
196
|
+
* trustworthy inputs.
|
|
197
|
+
*
|
|
198
|
+
* Flow:
|
|
199
|
+
* validate AssessOptions
|
|
200
|
+
* -> resolve scorer (custom or default)
|
|
201
|
+
* -> await scorer.score(signals)
|
|
202
|
+
* -> validate risk 0..100
|
|
203
|
+
* -> resolve thresholds (custom or DEFAULT_RISK_THRESHOLDS)
|
|
204
|
+
* -> apply policy (allow / challenge / block)
|
|
205
|
+
* -> if challenge, delegate to existing selectWorkload() with riskScore
|
|
206
|
+
*/
|
|
207
|
+
export async function assess(options) {
|
|
208
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
209
|
+
throw new Error('AssessOptions must be an object');
|
|
210
|
+
}
|
|
211
|
+
const { signals, scorer, thresholds, workload } = options;
|
|
212
|
+
if (!signals || typeof signals !== 'object' || Array.isArray(signals)) {
|
|
213
|
+
throw new Error('signals must be an object');
|
|
214
|
+
}
|
|
215
|
+
if (scorer !== undefined) {
|
|
216
|
+
if (!scorer || typeof scorer !== 'object' || typeof scorer.score !== 'function') {
|
|
217
|
+
throw new Error('scorer must be an object with a score function');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (thresholds !== undefined) {
|
|
221
|
+
validateRiskThresholds(thresholds);
|
|
222
|
+
}
|
|
223
|
+
// Use a private immutable default via copy so consumer mutation of the exported
|
|
224
|
+
// DEFAULT_RISK_THRESHOLDS cannot change library behavior
|
|
225
|
+
const resolvedThresholds = thresholds
|
|
226
|
+
? { challenge: thresholds.challenge, block: thresholds.block }
|
|
227
|
+
: { challenge: DEFAULT_RISK_THRESHOLDS.challenge, block: DEFAULT_RISK_THRESHOLDS.block };
|
|
228
|
+
// Validate resolved copy as well (defensive)
|
|
229
|
+
validateRiskThresholds(resolvedThresholds);
|
|
230
|
+
if (workload !== undefined) {
|
|
231
|
+
if (!workload || typeof workload !== 'object' || Array.isArray(workload)) {
|
|
232
|
+
throw new Error('workload must be an object');
|
|
233
|
+
}
|
|
234
|
+
validateAssessWorkloadOptions(workload);
|
|
235
|
+
}
|
|
236
|
+
const activeScorer = scorer ?? defaultScorer;
|
|
237
|
+
const rawRisk = await activeScorer.score(signals);
|
|
238
|
+
const risk = validateScorerOutput(rawRisk);
|
|
239
|
+
const { challenge, block } = resolvedThresholds;
|
|
240
|
+
let action;
|
|
241
|
+
if (risk < challenge)
|
|
242
|
+
action = 'allow';
|
|
243
|
+
else if (risk < block)
|
|
244
|
+
action = 'challenge';
|
|
245
|
+
else
|
|
246
|
+
action = 'block';
|
|
247
|
+
if (action === 'challenge') {
|
|
248
|
+
// Reuse existing adaptive workload selection. Do not duplicate logic.
|
|
249
|
+
// Ensure assessed risk overrides any riskScore that might be present in workload (defensive copy).
|
|
250
|
+
const workloadOptions = {
|
|
251
|
+
...workload,
|
|
252
|
+
riskScore: risk,
|
|
253
|
+
};
|
|
254
|
+
// Ensure workload's riskScore is overridden even if spread included one
|
|
255
|
+
workloadOptions.riskScore = risk;
|
|
256
|
+
const resultWorkload = selectWorkload(workloadOptions);
|
|
257
|
+
return { risk, action, workload: resultWorkload };
|
|
258
|
+
}
|
|
259
|
+
return { risk, action };
|
|
260
|
+
}
|
|
115
261
|
export function calibrateNode(iterations = 128) {
|
|
116
262
|
if (!Number.isFinite(iterations) || iterations < 1) {
|
|
117
263
|
throw new Error('Calibration iterations must be at least 1');
|
|
@@ -138,20 +284,15 @@ function hashContext(context, jti) {
|
|
|
138
284
|
.update(context, 'utf8')
|
|
139
285
|
.digest('hex');
|
|
140
286
|
}
|
|
141
|
-
let cachedSecret;
|
|
142
287
|
function getSecret() {
|
|
143
288
|
const secret = process.env.RIBAUNT_SECRET;
|
|
144
289
|
if (!secret) {
|
|
145
|
-
cachedSecret = undefined;
|
|
146
290
|
throw new Error('RIBAUNT_SECRET environment variable is not set!');
|
|
147
291
|
}
|
|
148
292
|
if (Buffer.byteLength(secret, 'utf8') < 32) {
|
|
149
|
-
cachedSecret = undefined;
|
|
150
293
|
throw new Error('RIBAUNT_SECRET must be at least 32 bytes');
|
|
151
294
|
}
|
|
152
|
-
|
|
153
|
-
cachedSecret = secret;
|
|
154
|
-
return cachedSecret;
|
|
295
|
+
return secret;
|
|
155
296
|
}
|
|
156
297
|
function createSingleChallenge(difficulty, ttlSeconds, context) {
|
|
157
298
|
const jti = crypto.randomUUID();
|
|
@@ -357,14 +498,19 @@ export async function verifySolution(token, nonce, options) {
|
|
|
357
498
|
if (replayStore && jtis.length > 0) {
|
|
358
499
|
const expiresAt = Math.max(...validated.map((payload) => payload.expires));
|
|
359
500
|
let consumed;
|
|
360
|
-
|
|
361
|
-
if (
|
|
362
|
-
|
|
501
|
+
try {
|
|
502
|
+
if (jtis.length > 1) {
|
|
503
|
+
if (!replayStore.consumeMany) {
|
|
504
|
+
return warn('configuration-error', 'A replayStore with consumeMany is required for atomic batch verification', options);
|
|
505
|
+
}
|
|
506
|
+
consumed = await replayStore.consumeMany(jtis, expiresAt);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
consumed = await replayStore.consume(jtis[0], expiresAt);
|
|
363
510
|
}
|
|
364
|
-
consumed = await replayStore.consumeMany(jtis, expiresAt);
|
|
365
511
|
}
|
|
366
|
-
|
|
367
|
-
|
|
512
|
+
catch (error) {
|
|
513
|
+
return warn('replay-store-unavailable', 'verifySolution failed because the replay store could not be reached', options, error);
|
|
368
514
|
}
|
|
369
515
|
if (!consumed)
|
|
370
516
|
return warn('replay-detected', 'verifySolution rejected a replayed token', options);
|