@rainfall-devkit/sdk 0.2.2 → 0.2.3

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.
@@ -0,0 +1,436 @@
1
+ // src/security/edge-node.ts
2
+ import sodium from "libsodium-wrappers-sumo";
3
+ var EdgeNodeSecurity = class {
4
+ sodiumReady;
5
+ backendSecret;
6
+ keyPair;
7
+ constructor(options = {}) {
8
+ this.sodiumReady = sodium.ready;
9
+ this.backendSecret = options.backendSecret;
10
+ this.keyPair = options.keyPair;
11
+ }
12
+ /**
13
+ * Initialize libsodium
14
+ */
15
+ async initialize() {
16
+ await this.sodiumReady;
17
+ }
18
+ // ============================================================================
19
+ // JWT Token Management
20
+ // ============================================================================
21
+ /**
22
+ * Generate a JWT token for an edge node
23
+ * Note: In production, this is done by the backend. This is for testing.
24
+ */
25
+ generateJWT(edgeNodeId, subscriberId, expiresInDays = 30) {
26
+ if (!this.backendSecret) {
27
+ throw new Error("Backend secret not configured");
28
+ }
29
+ const now = Math.floor(Date.now() / 1e3);
30
+ const exp = now + expiresInDays * 24 * 60 * 60;
31
+ const jti = this.generateTokenId();
32
+ const payload = {
33
+ sub: edgeNodeId,
34
+ iss: "rainfall-backend",
35
+ iat: now,
36
+ exp,
37
+ jti,
38
+ scope: ["edge:heartbeat", "edge:claim", "edge:submit", "edge:queue"]
39
+ };
40
+ const header = { alg: "HS256", typ: "JWT" };
41
+ const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
42
+ const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));
43
+ const signature = this.hmacSha256(
44
+ `${encodedHeader}.${encodedPayload}`,
45
+ this.backendSecret
46
+ );
47
+ const encodedSignature = this.base64UrlEncode(signature);
48
+ return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
49
+ }
50
+ /**
51
+ * Validate a JWT token
52
+ */
53
+ validateJWT(token) {
54
+ const parts = token.split(".");
55
+ if (parts.length !== 3) {
56
+ throw new Error("Invalid JWT format");
57
+ }
58
+ const [encodedHeader, encodedPayload, encodedSignature] = parts;
59
+ if (this.backendSecret) {
60
+ const expectedSignature = this.hmacSha256(
61
+ `${encodedHeader}.${encodedPayload}`,
62
+ this.backendSecret
63
+ );
64
+ const expectedEncoded = this.base64UrlEncode(expectedSignature);
65
+ if (!this.timingSafeEqual(encodedSignature, expectedEncoded)) {
66
+ throw new Error("Invalid JWT signature");
67
+ }
68
+ }
69
+ const payload = JSON.parse(this.base64UrlDecode(encodedPayload));
70
+ const now = Math.floor(Date.now() / 1e3);
71
+ if (payload.exp < now) {
72
+ throw new Error("JWT token expired");
73
+ }
74
+ if (payload.iss !== "rainfall-backend") {
75
+ throw new Error("Invalid JWT issuer");
76
+ }
77
+ return {
78
+ edgeNodeId: payload.sub,
79
+ subscriberId: payload.sub,
80
+ // Same as edge node ID for now
81
+ scopes: payload.scope,
82
+ expiresAt: payload.exp
83
+ };
84
+ }
85
+ /**
86
+ * Extract bearer token from Authorization header
87
+ */
88
+ extractBearerToken(authHeader) {
89
+ if (!authHeader) return null;
90
+ const match = authHeader.match(/^Bearer\s+(.+)$/i);
91
+ return match ? match[1] : null;
92
+ }
93
+ // ============================================================================
94
+ // ACL Enforcement
95
+ // ============================================================================
96
+ /**
97
+ * Check if an edge node is allowed to perform an action on a job
98
+ * Rule: Edge nodes can only access jobs for their own subscriber
99
+ */
100
+ checkACL(check) {
101
+ if (check.subscriberId !== check.jobSubscriberId) {
102
+ return {
103
+ allowed: false,
104
+ reason: `Edge node ${check.edgeNodeId} cannot access jobs from subscriber ${check.jobSubscriberId}`
105
+ };
106
+ }
107
+ const allowedActions = ["heartbeat", "claim", "submit", "queue"];
108
+ if (!allowedActions.includes(check.action)) {
109
+ return {
110
+ allowed: false,
111
+ reason: `Unknown action: ${check.action}`
112
+ };
113
+ }
114
+ return { allowed: true };
115
+ }
116
+ /**
117
+ * Middleware-style ACL check for job operations
118
+ */
119
+ requireSameSubscriber(edgeNodeSubscriberId, jobSubscriberId, operation) {
120
+ const result = this.checkACL({
121
+ edgeNodeId: edgeNodeSubscriberId,
122
+ subscriberId: edgeNodeSubscriberId,
123
+ jobSubscriberId,
124
+ action: operation
125
+ });
126
+ if (!result.allowed) {
127
+ throw new Error(result.reason);
128
+ }
129
+ }
130
+ // ============================================================================
131
+ // Encryption (Libsodium)
132
+ // ============================================================================
133
+ /**
134
+ * Generate a new Ed25519 key pair for an edge node
135
+ */
136
+ async generateKeyPair() {
137
+ await this.sodiumReady;
138
+ const keyPair = sodium.crypto_box_keypair();
139
+ return {
140
+ publicKey: this.bytesToBase64(keyPair.publicKey),
141
+ privateKey: this.bytesToBase64(keyPair.privateKey)
142
+ };
143
+ }
144
+ /**
145
+ * Encrypt job parameters for a target edge node using its public key
146
+ */
147
+ async encryptForEdgeNode(plaintext, targetPublicKeyBase64) {
148
+ await this.sodiumReady;
149
+ if (!this.keyPair) {
150
+ throw new Error("Local key pair not configured");
151
+ }
152
+ const targetPublicKey = this.base64ToBytes(targetPublicKeyBase64);
153
+ const ephemeralKeyPair = sodium.crypto_box_keypair();
154
+ const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES);
155
+ const message = new TextEncoder().encode(plaintext);
156
+ const ciphertext = sodium.crypto_box_easy(
157
+ message,
158
+ nonce,
159
+ targetPublicKey,
160
+ ephemeralKeyPair.privateKey
161
+ );
162
+ return {
163
+ ciphertext: this.bytesToBase64(ciphertext),
164
+ nonce: this.bytesToBase64(nonce),
165
+ ephemeralPublicKey: this.bytesToBase64(ephemeralKeyPair.publicKey)
166
+ };
167
+ }
168
+ /**
169
+ * Decrypt job parameters received from the backend
170
+ */
171
+ async decryptFromBackend(encrypted) {
172
+ await this.sodiumReady;
173
+ if (!this.keyPair) {
174
+ throw new Error("Local key pair not configured");
175
+ }
176
+ const privateKey = this.base64ToBytes(this.keyPair.privateKey);
177
+ const ephemeralPublicKey = this.base64ToBytes(encrypted.ephemeralPublicKey);
178
+ const nonce = this.base64ToBytes(encrypted.nonce);
179
+ const ciphertext = this.base64ToBytes(encrypted.ciphertext);
180
+ const decrypted = sodium.crypto_box_open_easy(
181
+ ciphertext,
182
+ nonce,
183
+ ephemeralPublicKey,
184
+ privateKey
185
+ );
186
+ if (!decrypted) {
187
+ throw new Error("Decryption failed - invalid ciphertext or keys");
188
+ }
189
+ return new TextDecoder().decode(decrypted);
190
+ }
191
+ /**
192
+ * Encrypt job parameters for local storage (using secretbox)
193
+ */
194
+ async encryptLocal(plaintext, key) {
195
+ await this.sodiumReady;
196
+ const keyBytes = this.deriveKey(key);
197
+ const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
198
+ const message = new TextEncoder().encode(plaintext);
199
+ const ciphertext = sodium.crypto_secretbox_easy(message, nonce, keyBytes);
200
+ return {
201
+ ciphertext: this.bytesToBase64(ciphertext),
202
+ nonce: this.bytesToBase64(nonce)
203
+ };
204
+ }
205
+ /**
206
+ * Decrypt locally stored job parameters
207
+ */
208
+ async decryptLocal(encrypted, key) {
209
+ await this.sodiumReady;
210
+ const keyBytes = this.deriveKey(key);
211
+ const nonce = this.base64ToBytes(encrypted.nonce);
212
+ const ciphertext = this.base64ToBytes(encrypted.ciphertext);
213
+ const decrypted = sodium.crypto_secretbox_open_easy(ciphertext, nonce, keyBytes);
214
+ if (!decrypted) {
215
+ throw new Error("Local decryption failed");
216
+ }
217
+ return new TextDecoder().decode(decrypted);
218
+ }
219
+ // ============================================================================
220
+ // Utility Methods
221
+ // ============================================================================
222
+ generateTokenId() {
223
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
224
+ }
225
+ base64UrlEncode(str) {
226
+ return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
227
+ }
228
+ base64UrlDecode(str) {
229
+ const padding = "=".repeat((4 - str.length % 4) % 4);
230
+ const base64 = str.replace(/-/g, "+").replace(/_/g, "/") + padding;
231
+ return atob(base64);
232
+ }
233
+ hmacSha256(message, secret) {
234
+ const key = new TextEncoder().encode(secret);
235
+ const msg = new TextEncoder().encode(message);
236
+ const hash = sodium.crypto_auth(msg, key);
237
+ return this.bytesToBase64(hash);
238
+ }
239
+ timingSafeEqual(a, b) {
240
+ if (a.length !== b.length) return false;
241
+ let result = 0;
242
+ for (let i = 0; i < a.length; i++) {
243
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
244
+ }
245
+ return result === 0;
246
+ }
247
+ bytesToBase64(bytes) {
248
+ const binString = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
249
+ return btoa(binString);
250
+ }
251
+ base64ToBytes(base64) {
252
+ const binString = atob(base64);
253
+ return Uint8Array.from(binString, (m) => m.charCodeAt(0));
254
+ }
255
+ deriveKey(password) {
256
+ const passwordBytes = new TextEncoder().encode(password);
257
+ return sodium.crypto_generichash(32, passwordBytes, null);
258
+ }
259
+ };
260
+ async function createEdgeNodeSecurity(options = {}) {
261
+ const security = new EdgeNodeSecurity(options);
262
+ await security.initialize();
263
+ return security;
264
+ }
265
+
266
+ // src/cli/core/types.ts
267
+ var ToolHandlerRegistry = class {
268
+ handlers = [];
269
+ register(handler) {
270
+ this.handlers.push(handler);
271
+ }
272
+ findHandler(toolId) {
273
+ return this.handlers.find((h) => {
274
+ if (typeof h.toolId === "string") {
275
+ return h.toolId === toolId;
276
+ }
277
+ return h.toolId.test(toolId);
278
+ });
279
+ }
280
+ getAllHandlers() {
281
+ return [...this.handlers];
282
+ }
283
+ };
284
+ var globalHandlerRegistry = new ToolHandlerRegistry();
285
+
286
+ // src/cli/handlers/_registry.ts
287
+ var imageGenerationHandler = {
288
+ toolId: /image-generation|generate-image/,
289
+ async display(context) {
290
+ const { detectImageData, displayImage } = await import("./display-KKJPO6UA.mjs");
291
+ const { result, flags } = context;
292
+ const imageInfo = detectImageData(result);
293
+ if (imageInfo.hasImage && !flags.raw) {
294
+ try {
295
+ if (imageInfo.imageData) {
296
+ await displayImage(imageInfo.imageData);
297
+ return true;
298
+ }
299
+ } catch (error) {
300
+ console.warn("Failed to display image:", error instanceof Error ? error.message : error);
301
+ }
302
+ }
303
+ return false;
304
+ }
305
+ };
306
+ var finvizQuotesHandler = {
307
+ toolId: "finviz-quotes",
308
+ async preflight(context) {
309
+ const { parseValue } = await import("./param-parser-PAKCNDBX.mjs");
310
+ const params = { ...context.params };
311
+ if (params.tickers && typeof params.tickers === "string") {
312
+ params.tickers = parseValue(params.tickers, { type: "array", items: { type: "string" } });
313
+ }
314
+ return { params };
315
+ },
316
+ async display(context) {
317
+ const { result, flags } = context;
318
+ if (flags.raw) {
319
+ return false;
320
+ }
321
+ const obj = result;
322
+ const quotes = obj?.quotes;
323
+ if (Array.isArray(quotes) && quotes.length > 0) {
324
+ const { formatAsTable } = await import("./display-KKJPO6UA.mjs");
325
+ const tableData = quotes.map((q) => {
326
+ const quote = q;
327
+ const data = quote.data || {};
328
+ return {
329
+ Ticker: quote.ticker || data.Ticker || "-",
330
+ Price: data.Price || data.Close || "-",
331
+ Change: data.Change || "-",
332
+ Volume: data.Volume || "-",
333
+ "Market Cap": data.MarketCap || "-"
334
+ };
335
+ });
336
+ console.log(formatAsTable(tableData));
337
+ const summary = obj?.summary;
338
+ if (summary && typeof summary === "string") {
339
+ console.log(`
340
+ ${summary}`);
341
+ }
342
+ return true;
343
+ }
344
+ return false;
345
+ }
346
+ };
347
+ var csvQueryHandler = {
348
+ toolId: /query-csv|csv-query/,
349
+ async display(context) {
350
+ const { result, flags } = context;
351
+ if (flags.raw) {
352
+ return false;
353
+ }
354
+ if (Array.isArray(result) && result.length > 0) {
355
+ const { formatAsTable } = await import("./display-KKJPO6UA.mjs");
356
+ console.log(formatAsTable(result));
357
+ return true;
358
+ }
359
+ return false;
360
+ }
361
+ };
362
+ var webSearchHandler = {
363
+ toolId: /web-search|exa-web-search|perplexity/,
364
+ async display(context) {
365
+ const { result, flags } = context;
366
+ if (flags.raw) {
367
+ return false;
368
+ }
369
+ const obj = result;
370
+ if (obj.results && typeof obj.results === "string") {
371
+ console.log(obj.results);
372
+ return true;
373
+ }
374
+ if (obj.answer || obj.summary) {
375
+ console.log(obj.answer || obj.summary);
376
+ if (obj.sources && Array.isArray(obj.sources)) {
377
+ console.log("\n--- Sources ---");
378
+ obj.sources.forEach((source, i) => {
379
+ if (typeof source === "string") {
380
+ console.log(` ${i + 1}. ${source}`);
381
+ } else if (source && typeof source === "object") {
382
+ const s = source;
383
+ console.log(` ${i + 1}. ${s.title || s.url || JSON.stringify(source)}`);
384
+ }
385
+ });
386
+ }
387
+ return true;
388
+ }
389
+ return false;
390
+ }
391
+ };
392
+ var memoryRecallHandler = {
393
+ toolId: /memory-recall|recall/,
394
+ async display(context) {
395
+ const { result, flags } = context;
396
+ if (flags.raw) {
397
+ return false;
398
+ }
399
+ if (Array.isArray(result)) {
400
+ if (result.length === 0) {
401
+ console.log("No memories found.");
402
+ return true;
403
+ }
404
+ console.log(`Found ${result.length} memory(s):
405
+ `);
406
+ result.forEach((mem, i) => {
407
+ const memory = mem;
408
+ console.log(`\u2500`.repeat(60));
409
+ console.log(` ${i + 1}. ${memory.content || memory.text || JSON.stringify(memory).slice(0, 100)}`);
410
+ if (memory.similarity) {
411
+ console.log(` Similarity: ${(Number(memory.similarity) * 100).toFixed(1)}%`);
412
+ }
413
+ if (memory.keywords && Array.isArray(memory.keywords)) {
414
+ console.log(` Keywords: ${memory.keywords.join(", ")}`);
415
+ }
416
+ console.log();
417
+ });
418
+ return true;
419
+ }
420
+ return false;
421
+ }
422
+ };
423
+ function registerBuiltInHandlers(registry = globalHandlerRegistry) {
424
+ registry.register(imageGenerationHandler);
425
+ registry.register(finvizQuotesHandler);
426
+ registry.register(csvQueryHandler);
427
+ registry.register(webSearchHandler);
428
+ registry.register(memoryRecallHandler);
429
+ }
430
+ registerBuiltInHandlers();
431
+
432
+ export {
433
+ EdgeNodeSecurity,
434
+ createEdgeNodeSecurity,
435
+ globalHandlerRegistry
436
+ };