@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.
Files changed (63) hide show
  1. package/benchmarks/bench.ts +272 -0
  2. package/dist/auth.d.ts +20 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +69 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/blackboard.d.ts +84 -0
  7. package/dist/blackboard.d.ts.map +1 -0
  8. package/dist/blackboard.js +502 -0
  9. package/dist/blackboard.js.map +1 -0
  10. package/dist/cli.d.ts +7 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +102 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/conditions.d.ts +27 -0
  15. package/dist/conditions.d.ts.map +1 -0
  16. package/dist/conditions.js +240 -0
  17. package/dist/conditions.js.map +1 -0
  18. package/dist/decay.d.ts +21 -0
  19. package/dist/decay.d.ts.map +1 -0
  20. package/dist/decay.js +88 -0
  21. package/dist/decay.js.map +1 -0
  22. package/dist/index.d.ts +13 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +13 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/rate-limiter.d.ts +21 -0
  27. package/dist/rate-limiter.d.ts.map +1 -0
  28. package/dist/rate-limiter.js +75 -0
  29. package/dist/rate-limiter.js.map +1 -0
  30. package/dist/server.d.ts +63 -0
  31. package/dist/server.d.ts.map +1 -0
  32. package/dist/server.js +401 -0
  33. package/dist/server.js.map +1 -0
  34. package/dist/store.d.ts +54 -0
  35. package/dist/store.d.ts.map +1 -0
  36. package/dist/store.js +55 -0
  37. package/dist/store.js.map +1 -0
  38. package/dist/types.d.ts +247 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +26 -0
  41. package/dist/types.js.map +1 -0
  42. package/dist/validation.d.ts +296 -0
  43. package/dist/validation.d.ts.map +1 -0
  44. package/dist/validation.js +205 -0
  45. package/dist/validation.js.map +1 -0
  46. package/eslint.config.js +26 -0
  47. package/package.json +66 -0
  48. package/src/auth.ts +89 -0
  49. package/src/blackboard.test.ts +287 -0
  50. package/src/blackboard.ts +651 -0
  51. package/src/cli.ts +116 -0
  52. package/src/conditions.ts +305 -0
  53. package/src/conformance.test.ts +686 -0
  54. package/src/decay.ts +103 -0
  55. package/src/index.ts +24 -0
  56. package/src/rate-limiter.ts +104 -0
  57. package/src/server.integration.test.ts +436 -0
  58. package/src/server.ts +500 -0
  59. package/src/store.ts +108 -0
  60. package/src/types.ts +314 -0
  61. package/src/validation.ts +251 -0
  62. package/tsconfig.eslint.json +5 -0
  63. 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
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "include": ["src/**/*", "src/**/*.test.ts"],
4
+ "exclude": ["node_modules", "dist"]
5
+ }
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
+ }