@advicenxt/sbp-server 0.1.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/benchmarks/bench.ts +272 -0
- package/dist/auth.d.ts +20 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +69 -0
- package/dist/auth.js.map +1 -0
- package/dist/blackboard.d.ts +84 -0
- package/dist/blackboard.d.ts.map +1 -0
- package/dist/blackboard.js +502 -0
- package/dist/blackboard.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +102 -0
- package/dist/cli.js.map +1 -0
- package/dist/conditions.d.ts +27 -0
- package/dist/conditions.d.ts.map +1 -0
- package/dist/conditions.js +240 -0
- package/dist/conditions.js.map +1 -0
- package/dist/decay.d.ts +21 -0
- package/dist/decay.d.ts.map +1 -0
- package/dist/decay.js +88 -0
- package/dist/decay.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/rate-limiter.d.ts +21 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +75 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/server.d.ts +63 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +401 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +54 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +55 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +247 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +296 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +205 -0
- package/dist/validation.js.map +1 -0
- package/eslint.config.js +26 -0
- package/package.json +66 -0
- package/src/auth.ts +89 -0
- package/src/blackboard.test.ts +287 -0
- package/src/blackboard.ts +651 -0
- package/src/cli.ts +116 -0
- package/src/conditions.ts +305 -0
- package/src/conformance.test.ts +686 -0
- package/src/decay.ts +103 -0
- package/src/index.ts +24 -0
- package/src/rate-limiter.ts +104 -0
- package/src/server.integration.test.ts +436 -0
- package/src/server.ts +500 -0
- package/src/store.ts +108 -0
- package/src/types.ts +314 -0
- package/src/validation.ts +251 -0
- package/tsconfig.eslint.json +5 -0
- package/tsconfig.json +20 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SBP Core Types
|
|
3
|
+
* Stigmergic Blackboard Protocol v0.1
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// DECAY MODELS
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export interface ExponentialDecay {
|
|
11
|
+
type: "exponential";
|
|
12
|
+
half_life_ms: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LinearDecay {
|
|
16
|
+
type: "linear";
|
|
17
|
+
rate_per_ms: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface StepDecay {
|
|
21
|
+
type: "step";
|
|
22
|
+
steps: Array<{ at_ms: number; intensity: number }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ImmortalDecay {
|
|
26
|
+
type: "immortal";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type DecayModel = ExponentialDecay | LinearDecay | StepDecay | ImmortalDecay;
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// PHEROMONE
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
export interface Pheromone {
|
|
36
|
+
id: string;
|
|
37
|
+
trail: string;
|
|
38
|
+
type: string;
|
|
39
|
+
emitted_at: number;
|
|
40
|
+
last_reinforced_at: number;
|
|
41
|
+
initial_intensity: number;
|
|
42
|
+
decay_model: DecayModel;
|
|
43
|
+
payload: Record<string, unknown>;
|
|
44
|
+
source_agent?: string;
|
|
45
|
+
tags: string[];
|
|
46
|
+
ttl_floor: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PheromoneSnapshot {
|
|
50
|
+
id: string;
|
|
51
|
+
trail: string;
|
|
52
|
+
type: string;
|
|
53
|
+
current_intensity: number;
|
|
54
|
+
payload: Record<string, unknown>;
|
|
55
|
+
age_ms: number;
|
|
56
|
+
tags: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// TAG FILTERING
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
export interface TagFilter {
|
|
64
|
+
any?: string[];
|
|
65
|
+
all?: string[];
|
|
66
|
+
none?: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// SCENT CONDITIONS
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
export interface ThresholdCondition {
|
|
74
|
+
type: "threshold";
|
|
75
|
+
trail: string;
|
|
76
|
+
signal_type: string;
|
|
77
|
+
tags?: TagFilter;
|
|
78
|
+
aggregation: "sum" | "max" | "avg" | "count" | "any";
|
|
79
|
+
operator: ">=" | ">" | "<=" | "<" | "==" | "!=";
|
|
80
|
+
value: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CompositeCondition {
|
|
84
|
+
type: "composite";
|
|
85
|
+
operator: "and" | "or" | "not";
|
|
86
|
+
conditions: ScentCondition[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface RateCondition {
|
|
90
|
+
type: "rate";
|
|
91
|
+
trail: string;
|
|
92
|
+
signal_type: string;
|
|
93
|
+
metric: "emissions_per_second" | "intensity_delta";
|
|
94
|
+
window_ms: number;
|
|
95
|
+
operator: ">=" | ">" | "<=" | "<";
|
|
96
|
+
value: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface PatternCondition {
|
|
100
|
+
type: "pattern";
|
|
101
|
+
/** Sequence of pheromone types to match */
|
|
102
|
+
sequence: Array<{
|
|
103
|
+
trail: string;
|
|
104
|
+
signal_type: string;
|
|
105
|
+
min_intensity?: number;
|
|
106
|
+
}>;
|
|
107
|
+
/** Time window in which the full sequence must appear */
|
|
108
|
+
window_ms: number;
|
|
109
|
+
/** Whether the sequence must appear in order (default: true) */
|
|
110
|
+
ordered?: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type ScentCondition = ThresholdCondition | CompositeCondition | RateCondition | PatternCondition;
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// SCENT REGISTRATION
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
export type MergeStrategy = "reinforce" | "replace" | "max" | "add" | "new";
|
|
120
|
+
export type TriggerMode = "level" | "edge_rising" | "edge_falling";
|
|
121
|
+
|
|
122
|
+
export interface Scent {
|
|
123
|
+
scent_id: string;
|
|
124
|
+
agent_endpoint: string;
|
|
125
|
+
condition: ScentCondition;
|
|
126
|
+
cooldown_ms: number;
|
|
127
|
+
activation_payload: Record<string, unknown>;
|
|
128
|
+
trigger_mode: TriggerMode;
|
|
129
|
+
hysteresis: number;
|
|
130
|
+
max_execution_ms: number;
|
|
131
|
+
last_triggered_at: number | null;
|
|
132
|
+
last_condition_met: boolean;
|
|
133
|
+
context_trails?: string[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// OPERATION PARAMS & RESULTS
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
export interface EmitParams {
|
|
141
|
+
trail: string;
|
|
142
|
+
type: string;
|
|
143
|
+
intensity: number;
|
|
144
|
+
decay?: DecayModel;
|
|
145
|
+
payload?: Record<string, unknown>;
|
|
146
|
+
tags?: string[];
|
|
147
|
+
merge_strategy?: MergeStrategy;
|
|
148
|
+
source_agent?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface EmitResult {
|
|
152
|
+
pheromone_id: string;
|
|
153
|
+
action: "created" | "reinforced" | "replaced" | "merged";
|
|
154
|
+
previous_intensity?: number;
|
|
155
|
+
new_intensity: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface SniffParams {
|
|
159
|
+
trails?: string[];
|
|
160
|
+
types?: string[];
|
|
161
|
+
min_intensity?: number;
|
|
162
|
+
max_age_ms?: number;
|
|
163
|
+
tags?: TagFilter;
|
|
164
|
+
limit?: number;
|
|
165
|
+
include_evaporated?: boolean;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface AggregateStats {
|
|
169
|
+
count: number;
|
|
170
|
+
sum_intensity: number;
|
|
171
|
+
max_intensity: number;
|
|
172
|
+
avg_intensity: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface SniffResult {
|
|
176
|
+
timestamp: number;
|
|
177
|
+
pheromones: PheromoneSnapshot[];
|
|
178
|
+
aggregates: Record<string, AggregateStats>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface RegisterScentParams {
|
|
182
|
+
scent_id: string;
|
|
183
|
+
agent_endpoint: string;
|
|
184
|
+
condition: ScentCondition;
|
|
185
|
+
cooldown_ms?: number;
|
|
186
|
+
activation_payload?: Record<string, unknown>;
|
|
187
|
+
trigger_mode?: TriggerMode;
|
|
188
|
+
hysteresis?: number;
|
|
189
|
+
max_execution_ms?: number;
|
|
190
|
+
context_trails?: string[];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface RegisterScentResult {
|
|
194
|
+
scent_id: string;
|
|
195
|
+
status: "registered" | "updated";
|
|
196
|
+
current_condition_state: {
|
|
197
|
+
met: boolean;
|
|
198
|
+
partial?: Record<string, boolean>;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface DeregisterScentParams {
|
|
203
|
+
scent_id: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface DeregisterScentResult {
|
|
207
|
+
scent_id: string;
|
|
208
|
+
status: "deregistered" | "not_found";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface TriggerPayload {
|
|
212
|
+
scent_id: string;
|
|
213
|
+
triggered_at: number;
|
|
214
|
+
condition_snapshot: Record<string, { value: number; pheromone_ids: string[] }>;
|
|
215
|
+
context_pheromones: PheromoneSnapshot[];
|
|
216
|
+
activation_payload: Record<string, unknown>;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface EvaporateParams {
|
|
220
|
+
trail?: string;
|
|
221
|
+
types?: string[];
|
|
222
|
+
older_than_ms?: number;
|
|
223
|
+
below_intensity?: number;
|
|
224
|
+
tags?: TagFilter;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface EvaporateResult {
|
|
228
|
+
evaporated_count: number;
|
|
229
|
+
trails_affected: string[];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface InspectParams {
|
|
233
|
+
include?: Array<"trails" | "scents" | "stats">;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface TrailInfo {
|
|
237
|
+
name: string;
|
|
238
|
+
pheromone_count: number;
|
|
239
|
+
total_intensity: number;
|
|
240
|
+
avg_intensity: number;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface ScentInfo {
|
|
244
|
+
scent_id: string;
|
|
245
|
+
agent_endpoint: string;
|
|
246
|
+
condition_met: boolean;
|
|
247
|
+
in_cooldown: boolean;
|
|
248
|
+
last_triggered_at: number | null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface InspectResult {
|
|
252
|
+
timestamp: number;
|
|
253
|
+
trails?: TrailInfo[];
|
|
254
|
+
scents?: ScentInfo[];
|
|
255
|
+
stats?: {
|
|
256
|
+
total_pheromones: number;
|
|
257
|
+
active_pheromones: number;
|
|
258
|
+
total_scents: number;
|
|
259
|
+
uptime_ms: number;
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// JSON-RPC
|
|
265
|
+
// ============================================================================
|
|
266
|
+
|
|
267
|
+
export interface JsonRpcRequest<T = unknown> {
|
|
268
|
+
jsonrpc: "2.0";
|
|
269
|
+
id: string | number;
|
|
270
|
+
method: string;
|
|
271
|
+
params: T;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export interface JsonRpcSuccessResponse<T = unknown> {
|
|
275
|
+
jsonrpc: "2.0";
|
|
276
|
+
id: string | number;
|
|
277
|
+
result: T;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface JsonRpcErrorResponse {
|
|
281
|
+
jsonrpc: "2.0";
|
|
282
|
+
id: string | number | null;
|
|
283
|
+
error: {
|
|
284
|
+
code: number;
|
|
285
|
+
message: string;
|
|
286
|
+
data?: unknown;
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export type JsonRpcResponse<T = unknown> = JsonRpcSuccessResponse<T> | JsonRpcErrorResponse;
|
|
291
|
+
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// ERROR CODES
|
|
294
|
+
// ============================================================================
|
|
295
|
+
|
|
296
|
+
export const SBP_ERROR_CODES = {
|
|
297
|
+
TRAIL_NOT_FOUND: -32001,
|
|
298
|
+
SCENT_NOT_FOUND: -32002,
|
|
299
|
+
PAYLOAD_VALIDATION_FAILED: -32003,
|
|
300
|
+
RATE_LIMITED: -32004,
|
|
301
|
+
UNAUTHORIZED: -32005,
|
|
302
|
+
INVALID_CONDITION: -32006,
|
|
303
|
+
} as const;
|
|
304
|
+
|
|
305
|
+
export class SbpError extends Error {
|
|
306
|
+
constructor(
|
|
307
|
+
public code: number,
|
|
308
|
+
message: string,
|
|
309
|
+
public data?: unknown
|
|
310
|
+
) {
|
|
311
|
+
super(message);
|
|
312
|
+
this.name = "SbpError";
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SBP Input Validation
|
|
3
|
+
* Zod schemas for all JSON-RPC params and protocol messages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// DECAY MODEL SCHEMAS
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
const ExponentialDecaySchema = z.object({
|
|
13
|
+
type: z.literal("exponential"),
|
|
14
|
+
half_life_ms: z.number().positive(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const LinearDecaySchema = z.object({
|
|
18
|
+
type: z.literal("linear"),
|
|
19
|
+
rate_per_ms: z.number().positive(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const StepDecaySchema = z.object({
|
|
23
|
+
type: z.literal("step"),
|
|
24
|
+
steps: z
|
|
25
|
+
.array(
|
|
26
|
+
z.object({
|
|
27
|
+
at_ms: z.number().nonnegative(),
|
|
28
|
+
intensity: z.number().min(0).max(1),
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
.min(1),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const ImmortalDecaySchema = z.object({
|
|
35
|
+
type: z.literal("immortal"),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const DecayModelSchema = z.discriminatedUnion("type", [
|
|
39
|
+
ExponentialDecaySchema,
|
|
40
|
+
LinearDecaySchema,
|
|
41
|
+
StepDecaySchema,
|
|
42
|
+
ImmortalDecaySchema,
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// TAG FILTER SCHEMAS
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
const TagFilterSchema = z
|
|
50
|
+
.object({
|
|
51
|
+
any: z.array(z.string()).optional(),
|
|
52
|
+
all: z.array(z.string()).optional(),
|
|
53
|
+
none: z.array(z.string()).optional(),
|
|
54
|
+
})
|
|
55
|
+
.strict();
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// CONDITION SCHEMAS
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
const ThresholdConditionSchema = z.object({
|
|
62
|
+
type: z.literal("threshold"),
|
|
63
|
+
trail: z.string().min(1),
|
|
64
|
+
signal_type: z.string().min(1),
|
|
65
|
+
tags: TagFilterSchema.optional(),
|
|
66
|
+
aggregation: z.enum(["sum", "max", "avg", "count", "any"]),
|
|
67
|
+
operator: z.enum([">=", ">", "<=", "<", "==", "!="]),
|
|
68
|
+
value: z.number(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const RateConditionSchema = z.object({
|
|
72
|
+
type: z.literal("rate"),
|
|
73
|
+
trail: z.string().min(1),
|
|
74
|
+
signal_type: z.string().min(1),
|
|
75
|
+
metric: z.enum(["emissions_per_second", "intensity_delta"]),
|
|
76
|
+
window_ms: z.number().positive(),
|
|
77
|
+
operator: z.enum([">=", ">", "<=", "<"]),
|
|
78
|
+
value: z.number(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const PatternConditionSchema = z.object({
|
|
82
|
+
type: z.literal("pattern"),
|
|
83
|
+
sequence: z
|
|
84
|
+
.array(
|
|
85
|
+
z.object({
|
|
86
|
+
trail: z.string().min(1),
|
|
87
|
+
signal_type: z.string().min(1),
|
|
88
|
+
min_intensity: z.number().min(0).max(1).optional(),
|
|
89
|
+
})
|
|
90
|
+
)
|
|
91
|
+
.min(1),
|
|
92
|
+
window_ms: z.number().positive(),
|
|
93
|
+
ordered: z.boolean().optional(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Recursive condition schema using z.lazy for composite self-reference
|
|
97
|
+
const ScentConditionSchema: z.ZodType = z.lazy(() =>
|
|
98
|
+
z.union([
|
|
99
|
+
ThresholdConditionSchema,
|
|
100
|
+
z.object({
|
|
101
|
+
type: z.literal("composite"),
|
|
102
|
+
operator: z.enum(["and", "or", "not"]),
|
|
103
|
+
conditions: z.array(ScentConditionSchema).min(1),
|
|
104
|
+
}),
|
|
105
|
+
RateConditionSchema,
|
|
106
|
+
PatternConditionSchema,
|
|
107
|
+
])
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// OPERATION PARAM SCHEMAS
|
|
112
|
+
// ============================================================================
|
|
113
|
+
|
|
114
|
+
export const EmitParamsSchema = z.object({
|
|
115
|
+
trail: z.string().min(1, "Trail must be a non-empty string"),
|
|
116
|
+
type: z.string().min(1, "Type must be a non-empty string"),
|
|
117
|
+
intensity: z.number().min(0).max(1, "Intensity must be between 0 and 1"),
|
|
118
|
+
decay: DecayModelSchema.optional(),
|
|
119
|
+
payload: z.record(z.unknown()).optional(),
|
|
120
|
+
tags: z.array(z.string()).optional(),
|
|
121
|
+
merge_strategy: z.enum(["reinforce", "replace", "max", "add", "new"]).optional(),
|
|
122
|
+
source_agent: z.string().optional(),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export const SniffParamsSchema = z.object({
|
|
126
|
+
trails: z.array(z.string()).optional(),
|
|
127
|
+
types: z.array(z.string()).optional(),
|
|
128
|
+
min_intensity: z.number().min(0).max(1).optional(),
|
|
129
|
+
max_age_ms: z.number().positive().optional(),
|
|
130
|
+
tags: TagFilterSchema.optional(),
|
|
131
|
+
limit: z.number().int().positive().max(10000).optional(),
|
|
132
|
+
include_evaporated: z.boolean().optional(),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export const RegisterScentParamsSchema = z.object({
|
|
136
|
+
scent_id: z.string().min(1, "Scent ID must be a non-empty string"),
|
|
137
|
+
agent_endpoint: z.string().optional().default(""),
|
|
138
|
+
condition: ScentConditionSchema,
|
|
139
|
+
cooldown_ms: z.number().int().nonnegative().optional(),
|
|
140
|
+
activation_payload: z.record(z.unknown()).optional(),
|
|
141
|
+
trigger_mode: z.enum(["level", "edge_rising", "edge_falling"]).optional(),
|
|
142
|
+
hysteresis: z.number().nonnegative().optional(),
|
|
143
|
+
max_execution_ms: z.number().positive().optional(),
|
|
144
|
+
context_trails: z.array(z.string()).optional(),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export const DeregisterScentParamsSchema = z.object({
|
|
148
|
+
scent_id: z.string().min(1),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export const EvaporateParamsSchema = z.object({
|
|
152
|
+
trail: z.string().optional(),
|
|
153
|
+
types: z.array(z.string()).optional(),
|
|
154
|
+
older_than_ms: z.number().positive().optional(),
|
|
155
|
+
below_intensity: z.number().min(0).max(1).optional(),
|
|
156
|
+
tags: TagFilterSchema.optional(),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
export const InspectParamsSchema = z.object({
|
|
160
|
+
include: z.array(z.enum(["trails", "scents", "stats"])).optional(),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
export const SubscribeParamsSchema = z.object({
|
|
164
|
+
scent_id: z.string().min(1),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export const UnsubscribeParamsSchema = z.object({
|
|
168
|
+
scent_id: z.string().min(1),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// JSON-RPC ENVELOPE SCHEMA
|
|
173
|
+
// ============================================================================
|
|
174
|
+
|
|
175
|
+
export const JsonRpcRequestSchema = z.object({
|
|
176
|
+
jsonrpc: z.literal("2.0"),
|
|
177
|
+
id: z.union([z.string(), z.number()]),
|
|
178
|
+
method: z.string().min(1),
|
|
179
|
+
params: z.unknown().optional().default({}),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// VALIDATION HELPER
|
|
184
|
+
// ============================================================================
|
|
185
|
+
|
|
186
|
+
/** Map of method names to their param schemas */
|
|
187
|
+
const METHOD_SCHEMAS: Record<string, z.ZodType> = {
|
|
188
|
+
"sbp/emit": EmitParamsSchema,
|
|
189
|
+
"sbp/sniff": SniffParamsSchema,
|
|
190
|
+
"sbp/register_scent": RegisterScentParamsSchema,
|
|
191
|
+
"sbp/deregister_scent": DeregisterScentParamsSchema,
|
|
192
|
+
"sbp/evaporate": EvaporateParamsSchema,
|
|
193
|
+
"sbp/inspect": InspectParamsSchema,
|
|
194
|
+
"sbp/subscribe": SubscribeParamsSchema,
|
|
195
|
+
"sbp/unsubscribe": UnsubscribeParamsSchema,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export interface ValidationError {
|
|
199
|
+
code: -32600 | -32602;
|
|
200
|
+
message: string;
|
|
201
|
+
data?: unknown;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Validate a JSON-RPC request envelope.
|
|
206
|
+
* Returns the parsed request or a JSON-RPC error.
|
|
207
|
+
*/
|
|
208
|
+
export function validateEnvelope(
|
|
209
|
+
body: unknown
|
|
210
|
+
): { ok: true; request: z.infer<typeof JsonRpcRequestSchema> } | { ok: false; error: ValidationError } {
|
|
211
|
+
const result = JsonRpcRequestSchema.safeParse(body);
|
|
212
|
+
if (!result.success) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
error: {
|
|
216
|
+
code: -32600,
|
|
217
|
+
message: "Invalid JSON-RPC request",
|
|
218
|
+
data: result.error.issues.map((i) => ({ path: i.path.join("."), message: i.message })),
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return { ok: true, request: result.data };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Validate method-specific params.
|
|
227
|
+
* Returns the parsed params or a JSON-RPC error.
|
|
228
|
+
*/
|
|
229
|
+
export function validateParams(
|
|
230
|
+
method: string,
|
|
231
|
+
params: unknown
|
|
232
|
+
): { ok: true; params: unknown } | { ok: false; error: ValidationError } {
|
|
233
|
+
const schema = METHOD_SCHEMAS[method];
|
|
234
|
+
if (!schema) {
|
|
235
|
+
// Unknown methods are handled by the RPC router, not here
|
|
236
|
+
return { ok: true, params };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const result = schema.safeParse(params);
|
|
240
|
+
if (!result.success) {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
error: {
|
|
244
|
+
code: -32602,
|
|
245
|
+
message: `Invalid params for ${method}`,
|
|
246
|
+
data: result.error.issues.map((i) => ({ path: i.path.join("."), message: i.message })),
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return { ok: true, params: result.data };
|
|
251
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"resolveJsonModule": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
20
|
+
}
|