@rainfall-devkit/sdk 0.1.8 → 0.2.1
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 +51 -0
- package/dist/chunk-7MRE4ZVI.mjs +662 -0
- package/dist/chunk-AQFC7YAX.mjs +27 -0
- package/dist/chunk-EI7SJH5K.mjs +85 -0
- package/dist/chunk-NTTAVKRT.mjs +89 -0
- package/dist/chunk-RVKW5KBT.mjs +269 -0
- package/dist/chunk-V5QWJVLC.mjs +662 -0
- package/dist/chunk-VDPKDC3R.mjs +869 -0
- package/dist/chunk-WOITG5TG.mjs +84 -0
- package/dist/chunk-XAHJQRBJ.mjs +269 -0
- package/dist/chunk-XEQ6U3JQ.mjs +269 -0
- package/dist/cli/index.js +3797 -632
- package/dist/cli/index.mjs +453 -36
- package/dist/config-7UT7GYSN.mjs +16 -0
- package/dist/config-DDTQQBN7.mjs +14 -0
- package/dist/config-MD45VGWD.mjs +14 -0
- package/dist/config-ZKNHII2A.mjs +8 -0
- package/dist/daemon/index.d.mts +168 -0
- package/dist/daemon/index.d.ts +168 -0
- package/dist/daemon/index.js +3182 -0
- package/dist/daemon/index.mjs +1548 -0
- package/dist/errors-BMPseAnM.d.mts +47 -0
- package/dist/errors-BMPseAnM.d.ts +47 -0
- package/dist/errors-CZdRoYyw.d.ts +332 -0
- package/dist/errors-Chjq1Mev.d.mts +332 -0
- package/dist/index.d.mts +249 -2
- package/dist/index.d.ts +249 -2
- package/dist/index.js +1247 -3
- package/dist/index.mjs +227 -2
- package/dist/listeners-B5Vy9Ao5.d.ts +372 -0
- package/dist/listeners-BbYIaNCs.d.mts +372 -0
- package/dist/listeners-CP2A9J_2.d.ts +372 -0
- package/dist/listeners-CTRSofnm.d.mts +372 -0
- package/dist/listeners-CYI-YwIF.d.mts +372 -0
- package/dist/listeners-DRwITBW_.d.mts +372 -0
- package/dist/listeners-DrMrvFT5.d.ts +372 -0
- package/dist/listeners-MNAnpZj-.d.mts +372 -0
- package/dist/listeners-PZI7iT85.d.ts +372 -0
- package/dist/listeners-QJeEtLbV.d.ts +372 -0
- package/dist/listeners-hp0Ib2Ox.d.ts +372 -0
- package/dist/listeners-jLwetUnx.d.mts +372 -0
- package/dist/mcp.d.mts +7 -2
- package/dist/mcp.d.ts +7 -2
- package/dist/mcp.js +92 -1
- package/dist/mcp.mjs +1 -1
- package/dist/sdk-4OvXPr8E.d.mts +1054 -0
- package/dist/sdk-4OvXPr8E.d.ts +1054 -0
- package/dist/sdk-CJ9g5lFo.d.mts +772 -0
- package/dist/sdk-CJ9g5lFo.d.ts +772 -0
- package/dist/sdk-CN1ezZrI.d.mts +1054 -0
- package/dist/sdk-CN1ezZrI.d.ts +1054 -0
- package/dist/sdk-DD1OeGRJ.d.mts +871 -0
- package/dist/sdk-DD1OeGRJ.d.ts +871 -0
- package/dist/sdk-Xw0BjsLd.d.mts +1054 -0
- package/dist/sdk-Xw0BjsLd.d.ts +1054 -0
- package/dist/types-GnRAfH-h.d.mts +489 -0
- package/dist/types-GnRAfH-h.d.ts +489 -0
- package/package.json +17 -5
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,23 +17,40 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
31
|
var index_exports = {};
|
|
22
32
|
__export(index_exports, {
|
|
23
33
|
AuthenticationError: () => AuthenticationError,
|
|
34
|
+
EdgeNodeSecurity: () => EdgeNodeSecurity,
|
|
24
35
|
NetworkError: () => NetworkError,
|
|
25
36
|
NotFoundError: () => NotFoundError,
|
|
26
37
|
Rainfall: () => Rainfall,
|
|
27
38
|
RainfallClient: () => RainfallClient,
|
|
39
|
+
RainfallDaemonContext: () => RainfallDaemonContext,
|
|
28
40
|
RainfallError: () => RainfallError,
|
|
41
|
+
RainfallListenerRegistry: () => RainfallListenerRegistry,
|
|
42
|
+
RainfallNetworkedExecutor: () => RainfallNetworkedExecutor,
|
|
29
43
|
RateLimitError: () => RateLimitError,
|
|
44
|
+
SecureEdgeClient: () => SecureEdgeClient,
|
|
30
45
|
ServerError: () => ServerError,
|
|
31
46
|
TimeoutError: () => TimeoutError,
|
|
32
47
|
ToolNotFoundError: () => ToolNotFoundError,
|
|
33
48
|
VERSION: () => VERSION,
|
|
34
|
-
ValidationError: () => ValidationError
|
|
49
|
+
ValidationError: () => ValidationError,
|
|
50
|
+
createCronWorkflow: () => createCronWorkflow,
|
|
51
|
+
createEdgeNodeSecurity: () => createEdgeNodeSecurity,
|
|
52
|
+
createFileWatcherWorkflow: () => createFileWatcherWorkflow,
|
|
53
|
+
createSecureEdgeClient: () => createSecureEdgeClient
|
|
35
54
|
});
|
|
36
55
|
module.exports = __toCommonJS(index_exports);
|
|
37
56
|
|
|
@@ -348,6 +367,49 @@ var RainfallClient = class {
|
|
|
348
367
|
sleep(ms) {
|
|
349
368
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
350
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* OpenAI-compatible chat completions with tool support
|
|
372
|
+
*/
|
|
373
|
+
async chatCompletions(params) {
|
|
374
|
+
const { subscriber_id, ...body } = params;
|
|
375
|
+
if (body.stream) {
|
|
376
|
+
const url = `${this.baseUrl}/olympic/subscribers/${subscriber_id}/v1/chat/completions`;
|
|
377
|
+
const response = await fetch(url, {
|
|
378
|
+
method: "POST",
|
|
379
|
+
headers: {
|
|
380
|
+
"x-api-key": this.apiKey,
|
|
381
|
+
"Content-Type": "application/json",
|
|
382
|
+
"Accept": "text/event-stream"
|
|
383
|
+
},
|
|
384
|
+
body: JSON.stringify(body)
|
|
385
|
+
});
|
|
386
|
+
if (!response.ok) {
|
|
387
|
+
const error = await response.text();
|
|
388
|
+
throw new RainfallError(`Chat completions failed: ${error}`, "CHAT_ERROR");
|
|
389
|
+
}
|
|
390
|
+
if (!response.body) {
|
|
391
|
+
throw new RainfallError("No response body", "CHAT_ERROR");
|
|
392
|
+
}
|
|
393
|
+
return response.body;
|
|
394
|
+
}
|
|
395
|
+
return this.request(
|
|
396
|
+
`/olympic/subscribers/${subscriber_id}/v1/chat/completions`,
|
|
397
|
+
{
|
|
398
|
+
method: "POST",
|
|
399
|
+
body
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* List available models (OpenAI-compatible format)
|
|
405
|
+
*/
|
|
406
|
+
async listModels(subscriberId) {
|
|
407
|
+
const sid = subscriberId || this.subscriberId || await this.ensureSubscriberId();
|
|
408
|
+
const result = await this.request(
|
|
409
|
+
`/olympic/subscribers/${sid}/v1/models`
|
|
410
|
+
);
|
|
411
|
+
return result.data || [];
|
|
412
|
+
}
|
|
351
413
|
};
|
|
352
414
|
|
|
353
415
|
// src/namespaces/integrations.ts
|
|
@@ -517,7 +579,12 @@ function createAI(client) {
|
|
|
517
579
|
chat: (params) => client.executeTool("xai-chat-completions", params),
|
|
518
580
|
complete: (params) => client.executeTool("fim", params),
|
|
519
581
|
classify: (params) => client.executeTool("jina-document-classifier", params),
|
|
520
|
-
segment: (params) => client.executeTool("jina-text-segmenter", params)
|
|
582
|
+
segment: (params) => client.executeTool("jina-text-segmenter", params),
|
|
583
|
+
/**
|
|
584
|
+
* OpenAI-compatible chat completions with full tool support
|
|
585
|
+
* This is the recommended method for multi-turn conversations with tools
|
|
586
|
+
*/
|
|
587
|
+
chatCompletions: (params) => client.chatCompletions(params)
|
|
521
588
|
};
|
|
522
589
|
}
|
|
523
590
|
|
|
@@ -797,22 +864,1199 @@ var Rainfall = class {
|
|
|
797
864
|
getRateLimitInfo() {
|
|
798
865
|
return this.client.getRateLimitInfo();
|
|
799
866
|
}
|
|
867
|
+
/**
|
|
868
|
+
* OpenAI-compatible chat completions with tool support
|
|
869
|
+
*
|
|
870
|
+
* @example
|
|
871
|
+
* ```typescript
|
|
872
|
+
* // Simple chat
|
|
873
|
+
* const response = await rainfall.chatCompletions({
|
|
874
|
+
* subscriber_id: 'my-subscriber',
|
|
875
|
+
* messages: [{ role: 'user', content: 'Hello!' }],
|
|
876
|
+
* model: 'llama-3.3-70b-versatile'
|
|
877
|
+
* });
|
|
878
|
+
*
|
|
879
|
+
* // With tools
|
|
880
|
+
* const response = await rainfall.chatCompletions({
|
|
881
|
+
* subscriber_id: 'my-subscriber',
|
|
882
|
+
* messages: [{ role: 'user', content: 'Search for AI news' }],
|
|
883
|
+
* tools: [{ type: 'function', function: { name: 'web-search' } }],
|
|
884
|
+
* enable_stacked: true
|
|
885
|
+
* });
|
|
886
|
+
*
|
|
887
|
+
* // Streaming
|
|
888
|
+
* const stream = await rainfall.chatCompletions({
|
|
889
|
+
* subscriber_id: 'my-subscriber',
|
|
890
|
+
* messages: [{ role: 'user', content: 'Tell me a story' }],
|
|
891
|
+
* stream: true
|
|
892
|
+
* });
|
|
893
|
+
* ```
|
|
894
|
+
*/
|
|
895
|
+
async chatCompletions(params) {
|
|
896
|
+
return this.client.chatCompletions(params);
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* List available models (OpenAI-compatible format)
|
|
900
|
+
*
|
|
901
|
+
* @example
|
|
902
|
+
* ```typescript
|
|
903
|
+
* const models = await rainfall.listModels();
|
|
904
|
+
* console.log(models); // [{ id: 'llama-3.3-70b-versatile', ... }]
|
|
905
|
+
* ```
|
|
906
|
+
*/
|
|
907
|
+
async listModels(subscriberId) {
|
|
908
|
+
return this.client.listModels(subscriberId);
|
|
909
|
+
}
|
|
800
910
|
};
|
|
801
911
|
|
|
912
|
+
// src/services/networked.ts
|
|
913
|
+
var RainfallNetworkedExecutor = class {
|
|
914
|
+
rainfall;
|
|
915
|
+
options;
|
|
916
|
+
edgeNodeId;
|
|
917
|
+
jobCallbacks = /* @__PURE__ */ new Map();
|
|
918
|
+
resultPollingInterval;
|
|
919
|
+
constructor(rainfall, options = {}) {
|
|
920
|
+
this.rainfall = rainfall;
|
|
921
|
+
this.options = {
|
|
922
|
+
wsPort: 8765,
|
|
923
|
+
httpPort: 8787,
|
|
924
|
+
hostname: process.env.HOSTNAME || "local-daemon",
|
|
925
|
+
capabilities: {
|
|
926
|
+
localExec: true,
|
|
927
|
+
fileWatch: true,
|
|
928
|
+
passiveListen: true
|
|
929
|
+
},
|
|
930
|
+
...options
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Register this edge node with the Rainfall backend
|
|
935
|
+
*/
|
|
936
|
+
async registerEdgeNode() {
|
|
937
|
+
const capabilities = this.buildCapabilitiesList();
|
|
938
|
+
try {
|
|
939
|
+
const result = await this.rainfall.executeTool("register-edge-node", {
|
|
940
|
+
hostname: this.options.hostname,
|
|
941
|
+
capabilities,
|
|
942
|
+
wsPort: this.options.wsPort,
|
|
943
|
+
httpPort: this.options.httpPort,
|
|
944
|
+
version: "0.1.0"
|
|
945
|
+
});
|
|
946
|
+
this.edgeNodeId = result.edgeNodeId;
|
|
947
|
+
console.log(`\u{1F310} Edge node registered with Rainfall as ${this.edgeNodeId}`);
|
|
948
|
+
return this.edgeNodeId;
|
|
949
|
+
} catch (error) {
|
|
950
|
+
this.edgeNodeId = `edge-${this.options.hostname}-${Date.now()}`;
|
|
951
|
+
console.log(`\u{1F310} Edge node running in local mode (ID: ${this.edgeNodeId})`);
|
|
952
|
+
return this.edgeNodeId;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Unregister this edge node on shutdown
|
|
957
|
+
*/
|
|
958
|
+
async unregisterEdgeNode() {
|
|
959
|
+
if (!this.edgeNodeId) return;
|
|
960
|
+
try {
|
|
961
|
+
await this.rainfall.executeTool("unregister-edge-node", {
|
|
962
|
+
edgeNodeId: this.edgeNodeId
|
|
963
|
+
});
|
|
964
|
+
console.log(`\u{1F310} Edge node ${this.edgeNodeId} unregistered`);
|
|
965
|
+
} catch {
|
|
966
|
+
}
|
|
967
|
+
if (this.resultPollingInterval) {
|
|
968
|
+
clearInterval(this.resultPollingInterval);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Queue a tool execution for distributed processing
|
|
973
|
+
* Non-blocking - returns immediately with a job ID
|
|
974
|
+
*/
|
|
975
|
+
async queueToolExecution(toolId, params, options = {}) {
|
|
976
|
+
const executionMode = options.executionMode || "any";
|
|
977
|
+
try {
|
|
978
|
+
const result = await this.rainfall.executeTool("queue-job", {
|
|
979
|
+
toolId,
|
|
980
|
+
params,
|
|
981
|
+
executionMode,
|
|
982
|
+
requesterEdgeNodeId: this.edgeNodeId
|
|
983
|
+
});
|
|
984
|
+
if (options.callback) {
|
|
985
|
+
this.jobCallbacks.set(result.jobId, options.callback);
|
|
986
|
+
this.startResultPolling();
|
|
987
|
+
}
|
|
988
|
+
return result.jobId;
|
|
989
|
+
} catch (error) {
|
|
990
|
+
if (executionMode === "local-only" || executionMode === "any") {
|
|
991
|
+
try {
|
|
992
|
+
const result = await this.rainfall.executeTool(toolId, params);
|
|
993
|
+
if (options.callback) {
|
|
994
|
+
options.callback(result);
|
|
995
|
+
}
|
|
996
|
+
return `local-${Date.now()}`;
|
|
997
|
+
} catch (execError) {
|
|
998
|
+
if (options.callback) {
|
|
999
|
+
options.callback(null, String(execError));
|
|
1000
|
+
}
|
|
1001
|
+
throw execError;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
throw error;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Get status of a queued job
|
|
1009
|
+
*/
|
|
1010
|
+
async getJobStatus(jobId) {
|
|
1011
|
+
try {
|
|
1012
|
+
const result = await this.rainfall.executeTool("get-job-status", {
|
|
1013
|
+
jobId
|
|
1014
|
+
});
|
|
1015
|
+
return result.job;
|
|
1016
|
+
} catch {
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Subscribe to job results via polling (WebSocket fallback)
|
|
1022
|
+
* In the future, this will use WebSocket push from ApresMoi
|
|
1023
|
+
*/
|
|
1024
|
+
async subscribeToResults(callback) {
|
|
1025
|
+
console.log("\u{1F4E1} Subscribed to job results via Rainfall (polling mode)");
|
|
1026
|
+
this.onResultReceived = callback;
|
|
1027
|
+
}
|
|
1028
|
+
onResultReceived;
|
|
1029
|
+
/**
|
|
1030
|
+
* Start polling for job results (fallback until WebSocket push is ready)
|
|
1031
|
+
*/
|
|
1032
|
+
startResultPolling() {
|
|
1033
|
+
if (this.resultPollingInterval) return;
|
|
1034
|
+
this.resultPollingInterval = setInterval(async () => {
|
|
1035
|
+
for (const [jobId, callback] of this.jobCallbacks) {
|
|
1036
|
+
try {
|
|
1037
|
+
const job = await this.getJobStatus(jobId);
|
|
1038
|
+
if (job?.status === "completed" || job?.status === "failed") {
|
|
1039
|
+
callback(job.result, job.error);
|
|
1040
|
+
this.jobCallbacks.delete(jobId);
|
|
1041
|
+
if (this.onResultReceived) {
|
|
1042
|
+
this.onResultReceived(jobId, job.result, job.error);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
} catch {
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (this.jobCallbacks.size === 0 && this.resultPollingInterval) {
|
|
1049
|
+
clearInterval(this.resultPollingInterval);
|
|
1050
|
+
this.resultPollingInterval = void 0;
|
|
1051
|
+
}
|
|
1052
|
+
}, 2e3);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Claim a job for execution on this edge node
|
|
1056
|
+
*/
|
|
1057
|
+
async claimJob() {
|
|
1058
|
+
try {
|
|
1059
|
+
const result = await this.rainfall.executeTool("claim-job", {
|
|
1060
|
+
edgeNodeId: this.edgeNodeId,
|
|
1061
|
+
capabilities: this.buildCapabilitiesList()
|
|
1062
|
+
});
|
|
1063
|
+
return result.job;
|
|
1064
|
+
} catch {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Submit job result after execution
|
|
1070
|
+
*/
|
|
1071
|
+
async submitJobResult(jobId, result, error) {
|
|
1072
|
+
try {
|
|
1073
|
+
await this.rainfall.executeTool("submit-job-result", {
|
|
1074
|
+
jobId,
|
|
1075
|
+
edgeNodeId: this.edgeNodeId,
|
|
1076
|
+
result,
|
|
1077
|
+
error
|
|
1078
|
+
});
|
|
1079
|
+
} catch {
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Get this edge node's ID
|
|
1084
|
+
*/
|
|
1085
|
+
getEdgeNodeId() {
|
|
1086
|
+
return this.edgeNodeId;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Build capabilities list from options
|
|
1090
|
+
*/
|
|
1091
|
+
buildCapabilitiesList() {
|
|
1092
|
+
const caps = this.options.capabilities || {};
|
|
1093
|
+
const list = [];
|
|
1094
|
+
if (caps.localExec) list.push("local-exec");
|
|
1095
|
+
if (caps.fileWatch) list.push("file-watch");
|
|
1096
|
+
if (caps.passiveListen) list.push("passive-listen");
|
|
1097
|
+
if (caps.browser) list.push("browser");
|
|
1098
|
+
if (caps.custom) list.push(...caps.custom);
|
|
1099
|
+
return list;
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
// src/services/context.ts
|
|
1104
|
+
var RainfallDaemonContext = class {
|
|
1105
|
+
rainfall;
|
|
1106
|
+
options;
|
|
1107
|
+
localMemories = /* @__PURE__ */ new Map();
|
|
1108
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1109
|
+
executionHistory = [];
|
|
1110
|
+
currentSessionId;
|
|
1111
|
+
constructor(rainfall, options = {}) {
|
|
1112
|
+
this.rainfall = rainfall;
|
|
1113
|
+
this.options = {
|
|
1114
|
+
maxLocalMemories: 1e3,
|
|
1115
|
+
maxMessageHistory: 100,
|
|
1116
|
+
maxExecutionHistory: 500,
|
|
1117
|
+
sessionTtl: 24 * 60 * 60 * 1e3,
|
|
1118
|
+
// 24 hours
|
|
1119
|
+
...options
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Initialize the context - load recent memories from cloud
|
|
1124
|
+
*/
|
|
1125
|
+
async initialize() {
|
|
1126
|
+
try {
|
|
1127
|
+
const recentMemories = await this.rainfall.memory.recall({
|
|
1128
|
+
query: "daemon:context",
|
|
1129
|
+
topK: this.options.maxLocalMemories
|
|
1130
|
+
});
|
|
1131
|
+
for (const memory of recentMemories) {
|
|
1132
|
+
this.localMemories.set(memory.id, {
|
|
1133
|
+
id: memory.id,
|
|
1134
|
+
content: memory.content,
|
|
1135
|
+
keywords: memory.keywords || [],
|
|
1136
|
+
timestamp: memory.timestamp,
|
|
1137
|
+
source: memory.source,
|
|
1138
|
+
metadata: memory.metadata
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
console.log(`\u{1F9E0} Loaded ${this.localMemories.size} memories into context`);
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
console.warn("\u26A0\uFE0F Could not sync memories:", error instanceof Error ? error.message : error);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Create or get a session
|
|
1148
|
+
*/
|
|
1149
|
+
getSession(sessionId) {
|
|
1150
|
+
if (sessionId && this.sessions.has(sessionId)) {
|
|
1151
|
+
const session = this.sessions.get(sessionId);
|
|
1152
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1153
|
+
return session;
|
|
1154
|
+
}
|
|
1155
|
+
const newSession = {
|
|
1156
|
+
id: sessionId || `session-${Date.now()}`,
|
|
1157
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1158
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1159
|
+
variables: {},
|
|
1160
|
+
messageHistory: []
|
|
1161
|
+
};
|
|
1162
|
+
this.sessions.set(newSession.id, newSession);
|
|
1163
|
+
this.currentSessionId = newSession.id;
|
|
1164
|
+
return newSession;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Get the current active session
|
|
1168
|
+
*/
|
|
1169
|
+
getCurrentSession() {
|
|
1170
|
+
if (this.currentSessionId) {
|
|
1171
|
+
return this.sessions.get(this.currentSessionId);
|
|
1172
|
+
}
|
|
1173
|
+
return void 0;
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Set the current active session
|
|
1177
|
+
*/
|
|
1178
|
+
setCurrentSession(sessionId) {
|
|
1179
|
+
if (this.sessions.has(sessionId)) {
|
|
1180
|
+
this.currentSessionId = sessionId;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Add a message to the current session history
|
|
1185
|
+
*/
|
|
1186
|
+
addMessage(role, content) {
|
|
1187
|
+
const session = this.getCurrentSession();
|
|
1188
|
+
if (!session) return;
|
|
1189
|
+
session.messageHistory.push({
|
|
1190
|
+
role,
|
|
1191
|
+
content,
|
|
1192
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1193
|
+
});
|
|
1194
|
+
if (session.messageHistory.length > this.options.maxMessageHistory) {
|
|
1195
|
+
session.messageHistory = session.messageHistory.slice(-this.options.maxMessageHistory);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Store a memory (local + cloud sync)
|
|
1200
|
+
*/
|
|
1201
|
+
async storeMemory(content, options = {}) {
|
|
1202
|
+
const id = `mem-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1203
|
+
const entry = {
|
|
1204
|
+
id,
|
|
1205
|
+
content,
|
|
1206
|
+
keywords: options.keywords || [],
|
|
1207
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1208
|
+
source: options.source || "daemon",
|
|
1209
|
+
metadata: options.metadata
|
|
1210
|
+
};
|
|
1211
|
+
this.localMemories.set(id, entry);
|
|
1212
|
+
try {
|
|
1213
|
+
await this.rainfall.memory.create({
|
|
1214
|
+
content,
|
|
1215
|
+
keywords: [...options.keywords || [], "daemon:context"],
|
|
1216
|
+
metadata: {
|
|
1217
|
+
...options.metadata,
|
|
1218
|
+
daemonMemoryId: id,
|
|
1219
|
+
source: options.source || "daemon"
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
console.warn("\u26A0\uFE0F Could not sync memory to cloud:", error instanceof Error ? error.message : error);
|
|
1224
|
+
}
|
|
1225
|
+
this.trimLocalMemories();
|
|
1226
|
+
return id;
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Recall memories by query
|
|
1230
|
+
*/
|
|
1231
|
+
async recallMemories(query, topK = 5) {
|
|
1232
|
+
const localResults = Array.from(this.localMemories.values()).filter(
|
|
1233
|
+
(m) => m.content.toLowerCase().includes(query.toLowerCase()) || m.keywords.some((k) => k.toLowerCase().includes(query.toLowerCase()))
|
|
1234
|
+
).slice(0, topK);
|
|
1235
|
+
try {
|
|
1236
|
+
const cloudResults = await this.rainfall.memory.recall({ query, topK });
|
|
1237
|
+
const seen = new Set(localResults.map((r) => r.id));
|
|
1238
|
+
for (const mem of cloudResults) {
|
|
1239
|
+
if (!seen.has(mem.id)) {
|
|
1240
|
+
localResults.push({
|
|
1241
|
+
id: mem.id,
|
|
1242
|
+
content: mem.content,
|
|
1243
|
+
keywords: mem.keywords || [],
|
|
1244
|
+
timestamp: mem.timestamp,
|
|
1245
|
+
source: mem.source,
|
|
1246
|
+
metadata: mem.metadata
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
} catch {
|
|
1251
|
+
}
|
|
1252
|
+
return localResults.slice(0, topK);
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Set a session variable
|
|
1256
|
+
*/
|
|
1257
|
+
setVariable(key, value) {
|
|
1258
|
+
const session = this.getCurrentSession();
|
|
1259
|
+
if (session) {
|
|
1260
|
+
session.variables[key] = value;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Get a session variable
|
|
1265
|
+
*/
|
|
1266
|
+
getVariable(key) {
|
|
1267
|
+
const session = this.getCurrentSession();
|
|
1268
|
+
return session?.variables[key];
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Record a tool execution
|
|
1272
|
+
*/
|
|
1273
|
+
recordExecution(toolId, params, result, options = { duration: 0 }) {
|
|
1274
|
+
const record = {
|
|
1275
|
+
id: `exec-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
1276
|
+
toolId,
|
|
1277
|
+
params,
|
|
1278
|
+
result,
|
|
1279
|
+
error: options.error,
|
|
1280
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1281
|
+
duration: options.duration,
|
|
1282
|
+
edgeNodeId: options.edgeNodeId
|
|
1283
|
+
};
|
|
1284
|
+
this.executionHistory.push(record);
|
|
1285
|
+
if (this.executionHistory.length > this.options.maxExecutionHistory) {
|
|
1286
|
+
this.executionHistory = this.executionHistory.slice(-this.options.maxExecutionHistory);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Get recent execution history
|
|
1291
|
+
*/
|
|
1292
|
+
getExecutionHistory(limit = 10) {
|
|
1293
|
+
return this.executionHistory.slice(-limit).reverse();
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Get execution statistics
|
|
1297
|
+
*/
|
|
1298
|
+
getExecutionStats() {
|
|
1299
|
+
const stats = {
|
|
1300
|
+
total: this.executionHistory.length,
|
|
1301
|
+
successful: 0,
|
|
1302
|
+
failed: 0,
|
|
1303
|
+
averageDuration: 0,
|
|
1304
|
+
byTool: {}
|
|
1305
|
+
};
|
|
1306
|
+
let totalDuration = 0;
|
|
1307
|
+
for (const exec of this.executionHistory) {
|
|
1308
|
+
if (exec.error) {
|
|
1309
|
+
stats.failed++;
|
|
1310
|
+
} else {
|
|
1311
|
+
stats.successful++;
|
|
1312
|
+
}
|
|
1313
|
+
totalDuration += exec.duration;
|
|
1314
|
+
stats.byTool[exec.toolId] = (stats.byTool[exec.toolId] || 0) + 1;
|
|
1315
|
+
}
|
|
1316
|
+
stats.averageDuration = stats.total > 0 ? totalDuration / stats.total : 0;
|
|
1317
|
+
return stats;
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Clear old sessions based on TTL
|
|
1321
|
+
*/
|
|
1322
|
+
cleanupSessions() {
|
|
1323
|
+
const now = Date.now();
|
|
1324
|
+
const ttl = this.options.sessionTtl;
|
|
1325
|
+
for (const [id, session] of this.sessions) {
|
|
1326
|
+
const lastActivity = new Date(session.lastActivity).getTime();
|
|
1327
|
+
if (now - lastActivity > ttl) {
|
|
1328
|
+
this.sessions.delete(id);
|
|
1329
|
+
if (this.currentSessionId === id) {
|
|
1330
|
+
this.currentSessionId = void 0;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Get context summary for debugging
|
|
1337
|
+
*/
|
|
1338
|
+
getStatus() {
|
|
1339
|
+
return {
|
|
1340
|
+
memoriesCached: this.localMemories.size,
|
|
1341
|
+
activeSessions: this.sessions.size,
|
|
1342
|
+
currentSession: this.currentSessionId,
|
|
1343
|
+
executionHistorySize: this.executionHistory.length
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
trimLocalMemories() {
|
|
1347
|
+
if (this.localMemories.size <= this.options.maxLocalMemories) return;
|
|
1348
|
+
const entries = Array.from(this.localMemories.entries()).sort((a, b) => new Date(a[1].timestamp).getTime() - new Date(b[1].timestamp).getTime());
|
|
1349
|
+
const toRemove = entries.slice(0, entries.length - this.options.maxLocalMemories);
|
|
1350
|
+
for (const [id] of toRemove) {
|
|
1351
|
+
this.localMemories.delete(id);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
// src/services/listeners.ts
|
|
1357
|
+
var RainfallListenerRegistry = class {
|
|
1358
|
+
rainfall;
|
|
1359
|
+
context;
|
|
1360
|
+
executor;
|
|
1361
|
+
watchers = /* @__PURE__ */ new Map();
|
|
1362
|
+
cronIntervals = /* @__PURE__ */ new Map();
|
|
1363
|
+
eventHistory = [];
|
|
1364
|
+
maxEventHistory = 100;
|
|
1365
|
+
constructor(rainfall, context, executor) {
|
|
1366
|
+
this.rainfall = rainfall;
|
|
1367
|
+
this.context = context;
|
|
1368
|
+
this.executor = executor;
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Register a file watcher
|
|
1372
|
+
* Note: Actual file watching requires fs.watch or chokidar
|
|
1373
|
+
* This is the registry - actual watching is done by the daemon
|
|
1374
|
+
*/
|
|
1375
|
+
async registerFileWatcher(config) {
|
|
1376
|
+
console.log(`\u{1F441}\uFE0F Registering file watcher: ${config.name} (${config.watchPath})`);
|
|
1377
|
+
const existing = Array.from(this.watchers.keys());
|
|
1378
|
+
if (existing.includes(config.id)) {
|
|
1379
|
+
await this.unregisterFileWatcher(config.id);
|
|
1380
|
+
}
|
|
1381
|
+
this.watchers.set(config.id, {
|
|
1382
|
+
stop: () => {
|
|
1383
|
+
console.log(`\u{1F441}\uFE0F Stopped file watcher: ${config.name}`);
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
await this.context.storeMemory(`File watcher registered: ${config.name}`, {
|
|
1387
|
+
keywords: ["listener", "file-watcher", config.name],
|
|
1388
|
+
metadata: { config }
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Unregister a file watcher
|
|
1393
|
+
*/
|
|
1394
|
+
async unregisterFileWatcher(id) {
|
|
1395
|
+
const watcher = this.watchers.get(id);
|
|
1396
|
+
if (watcher) {
|
|
1397
|
+
watcher.stop();
|
|
1398
|
+
this.watchers.delete(id);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Register a cron trigger
|
|
1403
|
+
*/
|
|
1404
|
+
async registerCronTrigger(config) {
|
|
1405
|
+
console.log(`\u23F0 Registering cron trigger: ${config.name} (${config.cron})`);
|
|
1406
|
+
if (this.cronIntervals.has(config.id)) {
|
|
1407
|
+
clearInterval(this.cronIntervals.get(config.id));
|
|
1408
|
+
this.cronIntervals.delete(config.id);
|
|
1409
|
+
}
|
|
1410
|
+
const interval = this.parseCronToMs(config.cron);
|
|
1411
|
+
if (interval) {
|
|
1412
|
+
const intervalId = setInterval(async () => {
|
|
1413
|
+
await this.handleCronTick(config);
|
|
1414
|
+
}, interval);
|
|
1415
|
+
this.cronIntervals.set(config.id, intervalId);
|
|
1416
|
+
}
|
|
1417
|
+
await this.context.storeMemory(`Cron trigger registered: ${config.name}`, {
|
|
1418
|
+
keywords: ["listener", "cron", config.name],
|
|
1419
|
+
metadata: { config }
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Unregister a cron trigger
|
|
1424
|
+
*/
|
|
1425
|
+
unregisterCronTrigger(id) {
|
|
1426
|
+
const interval = this.cronIntervals.get(id);
|
|
1427
|
+
if (interval) {
|
|
1428
|
+
clearInterval(interval);
|
|
1429
|
+
this.cronIntervals.delete(id);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Handle a file event
|
|
1434
|
+
*/
|
|
1435
|
+
async handleFileEvent(watcherId, eventType, filePath) {
|
|
1436
|
+
const event = {
|
|
1437
|
+
id: `evt-${Date.now()}`,
|
|
1438
|
+
type: "file",
|
|
1439
|
+
source: watcherId,
|
|
1440
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1441
|
+
data: { eventType, filePath }
|
|
1442
|
+
};
|
|
1443
|
+
this.recordEvent(event);
|
|
1444
|
+
console.log(`\u{1F4C1} File event: ${eventType} ${filePath}`);
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Handle a cron tick
|
|
1448
|
+
*/
|
|
1449
|
+
async handleCronTick(config) {
|
|
1450
|
+
const event = {
|
|
1451
|
+
id: `evt-${Date.now()}`,
|
|
1452
|
+
type: "cron",
|
|
1453
|
+
source: config.id,
|
|
1454
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1455
|
+
data: { cron: config.cron }
|
|
1456
|
+
};
|
|
1457
|
+
this.recordEvent(event);
|
|
1458
|
+
console.log(`\u23F0 Cron tick: ${config.name}`);
|
|
1459
|
+
for (const step of config.workflow) {
|
|
1460
|
+
try {
|
|
1461
|
+
await this.executor.queueToolExecution(step.toolId, {
|
|
1462
|
+
...step.params,
|
|
1463
|
+
_event: event
|
|
1464
|
+
});
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
console.error(`\u274C Workflow step failed: ${step.toolId}`, error);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Trigger a manual event (for testing or programmatic triggers)
|
|
1472
|
+
*/
|
|
1473
|
+
async triggerManual(name, data = {}) {
|
|
1474
|
+
const event = {
|
|
1475
|
+
id: `evt-${Date.now()}`,
|
|
1476
|
+
type: "manual",
|
|
1477
|
+
source: name,
|
|
1478
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1479
|
+
data
|
|
1480
|
+
};
|
|
1481
|
+
this.recordEvent(event);
|
|
1482
|
+
console.log(`\u{1F446} Manual trigger: ${name}`);
|
|
1483
|
+
await this.context.storeMemory(`Manual trigger fired: ${name}`, {
|
|
1484
|
+
keywords: ["trigger", "manual", name],
|
|
1485
|
+
metadata: { event }
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Get recent events
|
|
1490
|
+
*/
|
|
1491
|
+
getRecentEvents(limit = 10) {
|
|
1492
|
+
return this.eventHistory.slice(-limit).reverse();
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Get active listeners status
|
|
1496
|
+
*/
|
|
1497
|
+
getStatus() {
|
|
1498
|
+
return {
|
|
1499
|
+
fileWatchers: this.watchers.size,
|
|
1500
|
+
cronTriggers: this.cronIntervals.size,
|
|
1501
|
+
recentEvents: this.eventHistory.length
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Stop all listeners
|
|
1506
|
+
*/
|
|
1507
|
+
async stopAll() {
|
|
1508
|
+
for (const [id] of this.watchers) {
|
|
1509
|
+
await this.unregisterFileWatcher(id);
|
|
1510
|
+
}
|
|
1511
|
+
for (const [id] of this.cronIntervals) {
|
|
1512
|
+
this.unregisterCronTrigger(id);
|
|
1513
|
+
}
|
|
1514
|
+
console.log("\u{1F6D1} All listeners stopped");
|
|
1515
|
+
}
|
|
1516
|
+
recordEvent(event) {
|
|
1517
|
+
this.eventHistory.push(event);
|
|
1518
|
+
if (this.eventHistory.length > this.maxEventHistory) {
|
|
1519
|
+
this.eventHistory = this.eventHistory.slice(-this.maxEventHistory);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Simple cron parser - converts basic cron expressions to milliseconds
|
|
1524
|
+
* Supports: @hourly, @daily, @weekly, and simple intervals like every N minutes
|
|
1525
|
+
*/
|
|
1526
|
+
parseCronToMs(cron) {
|
|
1527
|
+
switch (cron) {
|
|
1528
|
+
case "@hourly":
|
|
1529
|
+
return 60 * 60 * 1e3;
|
|
1530
|
+
case "@daily":
|
|
1531
|
+
return 24 * 60 * 60 * 1e3;
|
|
1532
|
+
case "@weekly":
|
|
1533
|
+
return 7 * 24 * 60 * 60 * 1e3;
|
|
1534
|
+
case "@minutely":
|
|
1535
|
+
return 60 * 1e3;
|
|
1536
|
+
}
|
|
1537
|
+
const match = cron.match(/^\*\/(\d+)\s/);
|
|
1538
|
+
if (match) {
|
|
1539
|
+
const minutes = parseInt(match[1], 10);
|
|
1540
|
+
if (minutes > 0 && minutes <= 60) {
|
|
1541
|
+
return minutes * 60 * 1e3;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
console.warn(`\u26A0\uFE0F Unrecognized cron pattern "${cron}", using 1 minute interval`);
|
|
1545
|
+
return 60 * 1e3;
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
function createFileWatcherWorkflow(name, watchPath, options) {
|
|
1549
|
+
return {
|
|
1550
|
+
id: `fw-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
1551
|
+
name,
|
|
1552
|
+
watchPath,
|
|
1553
|
+
pattern: options.pattern,
|
|
1554
|
+
events: options.events || ["create"],
|
|
1555
|
+
workflow: options.workflow
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
function createCronWorkflow(name, cron, workflow) {
|
|
1559
|
+
return {
|
|
1560
|
+
id: `cron-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
1561
|
+
name,
|
|
1562
|
+
cron,
|
|
1563
|
+
workflow
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// src/security/edge-node.ts
|
|
1568
|
+
var sodium = __toESM(require("libsodium-wrappers"));
|
|
1569
|
+
var EdgeNodeSecurity = class {
|
|
1570
|
+
sodiumReady;
|
|
1571
|
+
backendSecret;
|
|
1572
|
+
keyPair;
|
|
1573
|
+
constructor(options = {}) {
|
|
1574
|
+
this.sodiumReady = sodium.ready;
|
|
1575
|
+
this.backendSecret = options.backendSecret;
|
|
1576
|
+
this.keyPair = options.keyPair;
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Initialize libsodium
|
|
1580
|
+
*/
|
|
1581
|
+
async initialize() {
|
|
1582
|
+
await this.sodiumReady;
|
|
1583
|
+
}
|
|
1584
|
+
// ============================================================================
|
|
1585
|
+
// JWT Token Management
|
|
1586
|
+
// ============================================================================
|
|
1587
|
+
/**
|
|
1588
|
+
* Generate a JWT token for an edge node
|
|
1589
|
+
* Note: In production, this is done by the backend. This is for testing.
|
|
1590
|
+
*/
|
|
1591
|
+
generateJWT(edgeNodeId, subscriberId, expiresInDays = 30) {
|
|
1592
|
+
if (!this.backendSecret) {
|
|
1593
|
+
throw new Error("Backend secret not configured");
|
|
1594
|
+
}
|
|
1595
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1596
|
+
const exp = now + expiresInDays * 24 * 60 * 60;
|
|
1597
|
+
const jti = this.generateTokenId();
|
|
1598
|
+
const payload = {
|
|
1599
|
+
sub: edgeNodeId,
|
|
1600
|
+
iss: "rainfall-backend",
|
|
1601
|
+
iat: now,
|
|
1602
|
+
exp,
|
|
1603
|
+
jti,
|
|
1604
|
+
scope: ["edge:heartbeat", "edge:claim", "edge:submit", "edge:queue"]
|
|
1605
|
+
};
|
|
1606
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
1607
|
+
const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
|
|
1608
|
+
const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));
|
|
1609
|
+
const signature = this.hmacSha256(
|
|
1610
|
+
`${encodedHeader}.${encodedPayload}`,
|
|
1611
|
+
this.backendSecret
|
|
1612
|
+
);
|
|
1613
|
+
const encodedSignature = this.base64UrlEncode(signature);
|
|
1614
|
+
return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Validate a JWT token
|
|
1618
|
+
*/
|
|
1619
|
+
validateJWT(token) {
|
|
1620
|
+
const parts = token.split(".");
|
|
1621
|
+
if (parts.length !== 3) {
|
|
1622
|
+
throw new Error("Invalid JWT format");
|
|
1623
|
+
}
|
|
1624
|
+
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
1625
|
+
if (this.backendSecret) {
|
|
1626
|
+
const expectedSignature = this.hmacSha256(
|
|
1627
|
+
`${encodedHeader}.${encodedPayload}`,
|
|
1628
|
+
this.backendSecret
|
|
1629
|
+
);
|
|
1630
|
+
const expectedEncoded = this.base64UrlEncode(expectedSignature);
|
|
1631
|
+
if (!this.timingSafeEqual(encodedSignature, expectedEncoded)) {
|
|
1632
|
+
throw new Error("Invalid JWT signature");
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
const payload = JSON.parse(this.base64UrlDecode(encodedPayload));
|
|
1636
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1637
|
+
if (payload.exp < now) {
|
|
1638
|
+
throw new Error("JWT token expired");
|
|
1639
|
+
}
|
|
1640
|
+
if (payload.iss !== "rainfall-backend") {
|
|
1641
|
+
throw new Error("Invalid JWT issuer");
|
|
1642
|
+
}
|
|
1643
|
+
return {
|
|
1644
|
+
edgeNodeId: payload.sub,
|
|
1645
|
+
subscriberId: payload.sub,
|
|
1646
|
+
// Same as edge node ID for now
|
|
1647
|
+
scopes: payload.scope,
|
|
1648
|
+
expiresAt: payload.exp
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Extract bearer token from Authorization header
|
|
1653
|
+
*/
|
|
1654
|
+
extractBearerToken(authHeader) {
|
|
1655
|
+
if (!authHeader) return null;
|
|
1656
|
+
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
1657
|
+
return match ? match[1] : null;
|
|
1658
|
+
}
|
|
1659
|
+
// ============================================================================
|
|
1660
|
+
// ACL Enforcement
|
|
1661
|
+
// ============================================================================
|
|
1662
|
+
/**
|
|
1663
|
+
* Check if an edge node is allowed to perform an action on a job
|
|
1664
|
+
* Rule: Edge nodes can only access jobs for their own subscriber
|
|
1665
|
+
*/
|
|
1666
|
+
checkACL(check) {
|
|
1667
|
+
if (check.subscriberId !== check.jobSubscriberId) {
|
|
1668
|
+
return {
|
|
1669
|
+
allowed: false,
|
|
1670
|
+
reason: `Edge node ${check.edgeNodeId} cannot access jobs from subscriber ${check.jobSubscriberId}`
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
const allowedActions = ["heartbeat", "claim", "submit", "queue"];
|
|
1674
|
+
if (!allowedActions.includes(check.action)) {
|
|
1675
|
+
return {
|
|
1676
|
+
allowed: false,
|
|
1677
|
+
reason: `Unknown action: ${check.action}`
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
return { allowed: true };
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Middleware-style ACL check for job operations
|
|
1684
|
+
*/
|
|
1685
|
+
requireSameSubscriber(edgeNodeSubscriberId, jobSubscriberId, operation) {
|
|
1686
|
+
const result = this.checkACL({
|
|
1687
|
+
edgeNodeId: edgeNodeSubscriberId,
|
|
1688
|
+
subscriberId: edgeNodeSubscriberId,
|
|
1689
|
+
jobSubscriberId,
|
|
1690
|
+
action: operation
|
|
1691
|
+
});
|
|
1692
|
+
if (!result.allowed) {
|
|
1693
|
+
throw new Error(result.reason);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
// ============================================================================
|
|
1697
|
+
// Encryption (Libsodium)
|
|
1698
|
+
// ============================================================================
|
|
1699
|
+
/**
|
|
1700
|
+
* Generate a new Ed25519 key pair for an edge node
|
|
1701
|
+
*/
|
|
1702
|
+
async generateKeyPair() {
|
|
1703
|
+
await this.sodiumReady;
|
|
1704
|
+
const keyPair = sodium.crypto_box_keypair();
|
|
1705
|
+
return {
|
|
1706
|
+
publicKey: this.bytesToBase64(keyPair.publicKey),
|
|
1707
|
+
privateKey: this.bytesToBase64(keyPair.privateKey)
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Encrypt job parameters for a target edge node using its public key
|
|
1712
|
+
*/
|
|
1713
|
+
async encryptForEdgeNode(plaintext, targetPublicKeyBase64) {
|
|
1714
|
+
await this.sodiumReady;
|
|
1715
|
+
if (!this.keyPair) {
|
|
1716
|
+
throw new Error("Local key pair not configured");
|
|
1717
|
+
}
|
|
1718
|
+
const targetPublicKey = this.base64ToBytes(targetPublicKeyBase64);
|
|
1719
|
+
const ephemeralKeyPair = sodium.crypto_box_keypair();
|
|
1720
|
+
const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES);
|
|
1721
|
+
const message = new TextEncoder().encode(plaintext);
|
|
1722
|
+
const ciphertext = sodium.crypto_box_easy(
|
|
1723
|
+
message,
|
|
1724
|
+
nonce,
|
|
1725
|
+
targetPublicKey,
|
|
1726
|
+
ephemeralKeyPair.privateKey
|
|
1727
|
+
);
|
|
1728
|
+
return {
|
|
1729
|
+
ciphertext: this.bytesToBase64(ciphertext),
|
|
1730
|
+
nonce: this.bytesToBase64(nonce),
|
|
1731
|
+
ephemeralPublicKey: this.bytesToBase64(ephemeralKeyPair.publicKey)
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Decrypt job parameters received from the backend
|
|
1736
|
+
*/
|
|
1737
|
+
async decryptFromBackend(encrypted) {
|
|
1738
|
+
await this.sodiumReady;
|
|
1739
|
+
if (!this.keyPair) {
|
|
1740
|
+
throw new Error("Local key pair not configured");
|
|
1741
|
+
}
|
|
1742
|
+
const privateKey = this.base64ToBytes(this.keyPair.privateKey);
|
|
1743
|
+
const ephemeralPublicKey = this.base64ToBytes(encrypted.ephemeralPublicKey);
|
|
1744
|
+
const nonce = this.base64ToBytes(encrypted.nonce);
|
|
1745
|
+
const ciphertext = this.base64ToBytes(encrypted.ciphertext);
|
|
1746
|
+
const decrypted = sodium.crypto_box_open_easy(
|
|
1747
|
+
ciphertext,
|
|
1748
|
+
nonce,
|
|
1749
|
+
ephemeralPublicKey,
|
|
1750
|
+
privateKey
|
|
1751
|
+
);
|
|
1752
|
+
if (!decrypted) {
|
|
1753
|
+
throw new Error("Decryption failed - invalid ciphertext or keys");
|
|
1754
|
+
}
|
|
1755
|
+
return new TextDecoder().decode(decrypted);
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Encrypt job parameters for local storage (using secretbox)
|
|
1759
|
+
*/
|
|
1760
|
+
async encryptLocal(plaintext, key) {
|
|
1761
|
+
await this.sodiumReady;
|
|
1762
|
+
const keyBytes = this.deriveKey(key);
|
|
1763
|
+
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
|
1764
|
+
const message = new TextEncoder().encode(plaintext);
|
|
1765
|
+
const ciphertext = sodium.crypto_secretbox_easy(message, nonce, keyBytes);
|
|
1766
|
+
return {
|
|
1767
|
+
ciphertext: this.bytesToBase64(ciphertext),
|
|
1768
|
+
nonce: this.bytesToBase64(nonce)
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Decrypt locally stored job parameters
|
|
1773
|
+
*/
|
|
1774
|
+
async decryptLocal(encrypted, key) {
|
|
1775
|
+
await this.sodiumReady;
|
|
1776
|
+
const keyBytes = this.deriveKey(key);
|
|
1777
|
+
const nonce = this.base64ToBytes(encrypted.nonce);
|
|
1778
|
+
const ciphertext = this.base64ToBytes(encrypted.ciphertext);
|
|
1779
|
+
const decrypted = sodium.crypto_secretbox_open_easy(ciphertext, nonce, keyBytes);
|
|
1780
|
+
if (!decrypted) {
|
|
1781
|
+
throw new Error("Local decryption failed");
|
|
1782
|
+
}
|
|
1783
|
+
return new TextDecoder().decode(decrypted);
|
|
1784
|
+
}
|
|
1785
|
+
// ============================================================================
|
|
1786
|
+
// Utility Methods
|
|
1787
|
+
// ============================================================================
|
|
1788
|
+
generateTokenId() {
|
|
1789
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
1790
|
+
}
|
|
1791
|
+
base64UrlEncode(str) {
|
|
1792
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1793
|
+
}
|
|
1794
|
+
base64UrlDecode(str) {
|
|
1795
|
+
const padding = "=".repeat((4 - str.length % 4) % 4);
|
|
1796
|
+
const base64 = str.replace(/-/g, "+").replace(/_/g, "/") + padding;
|
|
1797
|
+
return atob(base64);
|
|
1798
|
+
}
|
|
1799
|
+
hmacSha256(message, secret) {
|
|
1800
|
+
const key = new TextEncoder().encode(secret);
|
|
1801
|
+
const msg = new TextEncoder().encode(message);
|
|
1802
|
+
const hash = sodium.crypto_auth(msg, key);
|
|
1803
|
+
return this.bytesToBase64(hash);
|
|
1804
|
+
}
|
|
1805
|
+
timingSafeEqual(a, b) {
|
|
1806
|
+
if (a.length !== b.length) return false;
|
|
1807
|
+
let result = 0;
|
|
1808
|
+
for (let i = 0; i < a.length; i++) {
|
|
1809
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
1810
|
+
}
|
|
1811
|
+
return result === 0;
|
|
1812
|
+
}
|
|
1813
|
+
bytesToBase64(bytes) {
|
|
1814
|
+
const binString = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
|
|
1815
|
+
return btoa(binString);
|
|
1816
|
+
}
|
|
1817
|
+
base64ToBytes(base64) {
|
|
1818
|
+
const binString = atob(base64);
|
|
1819
|
+
return Uint8Array.from(binString, (m) => m.charCodeAt(0));
|
|
1820
|
+
}
|
|
1821
|
+
deriveKey(password) {
|
|
1822
|
+
const passwordBytes = new TextEncoder().encode(password);
|
|
1823
|
+
return sodium.crypto_generichash(32, passwordBytes, null);
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
async function createEdgeNodeSecurity(options = {}) {
|
|
1827
|
+
const security = new EdgeNodeSecurity(options);
|
|
1828
|
+
await security.initialize();
|
|
1829
|
+
return security;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// src/security/edge-client.ts
|
|
1833
|
+
var import_fs = require("fs");
|
|
1834
|
+
var import_path = require("path");
|
|
1835
|
+
var SecureEdgeClient = class {
|
|
1836
|
+
client;
|
|
1837
|
+
security;
|
|
1838
|
+
edgeNodeId;
|
|
1839
|
+
edgeNodeSecret;
|
|
1840
|
+
keysPath;
|
|
1841
|
+
jwtPayload;
|
|
1842
|
+
keyPair;
|
|
1843
|
+
constructor(config) {
|
|
1844
|
+
this.client = config.client;
|
|
1845
|
+
this.edgeNodeId = config.edgeNodeId;
|
|
1846
|
+
this.edgeNodeSecret = config.edgeNodeSecret;
|
|
1847
|
+
this.keysPath = config.keysPath;
|
|
1848
|
+
this.security = new EdgeNodeSecurity({
|
|
1849
|
+
backendSecret: config.backendSecret
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Initialize the secure client
|
|
1854
|
+
*/
|
|
1855
|
+
async initialize() {
|
|
1856
|
+
await this.security.initialize();
|
|
1857
|
+
await this.loadKeyPair();
|
|
1858
|
+
this.jwtPayload = this.security.validateJWT(this.edgeNodeSecret);
|
|
1859
|
+
if (this.jwtPayload.edgeNodeId !== this.edgeNodeId) {
|
|
1860
|
+
throw new Error("JWT edge node ID mismatch");
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Load key pair from disk
|
|
1865
|
+
*/
|
|
1866
|
+
async loadKeyPair() {
|
|
1867
|
+
const publicKeyPath = (0, import_path.join)(this.keysPath, "edge-node.pub");
|
|
1868
|
+
const privateKeyPath = (0, import_path.join)(this.keysPath, "edge-node.key");
|
|
1869
|
+
if (!(0, import_fs.existsSync)(publicKeyPath) || !(0, import_fs.existsSync)(privateKeyPath)) {
|
|
1870
|
+
throw new Error("Key pair not found. Run: rainfall edge generate-keys");
|
|
1871
|
+
}
|
|
1872
|
+
this.keyPair = {
|
|
1873
|
+
publicKey: (0, import_fs.readFileSync)(publicKeyPath, "utf-8"),
|
|
1874
|
+
privateKey: (0, import_fs.readFileSync)(privateKeyPath, "utf-8")
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Get public key for sharing with backend
|
|
1879
|
+
*/
|
|
1880
|
+
getPublicKey() {
|
|
1881
|
+
if (!this.keyPair) {
|
|
1882
|
+
throw new Error("Key pair not loaded");
|
|
1883
|
+
}
|
|
1884
|
+
return this.keyPair.publicKey;
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Send heartbeat with authentication
|
|
1888
|
+
*/
|
|
1889
|
+
async heartbeat() {
|
|
1890
|
+
this.requireAuth();
|
|
1891
|
+
return this.client.request("/edge/heartbeat", {
|
|
1892
|
+
method: "POST",
|
|
1893
|
+
body: {
|
|
1894
|
+
edgeNodeId: this.edgeNodeId,
|
|
1895
|
+
timestamp: Date.now()
|
|
1896
|
+
},
|
|
1897
|
+
headers: {
|
|
1898
|
+
"Authorization": `Bearer ${this.edgeNodeSecret}`
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Claim a job from the queue
|
|
1904
|
+
*/
|
|
1905
|
+
async claimJob() {
|
|
1906
|
+
this.requireAuth();
|
|
1907
|
+
const job = await this.client.request("/edge/claim-job", {
|
|
1908
|
+
method: "POST",
|
|
1909
|
+
body: {
|
|
1910
|
+
edgeNodeId: this.edgeNodeId,
|
|
1911
|
+
subscriberId: this.jwtPayload.subscriberId
|
|
1912
|
+
},
|
|
1913
|
+
headers: {
|
|
1914
|
+
"Authorization": `Bearer ${this.edgeNodeSecret}`
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
if (job && job.encrypted && job.params) {
|
|
1918
|
+
const decrypted = await this.decryptJobParams(job.params);
|
|
1919
|
+
return { ...job, params: decrypted };
|
|
1920
|
+
}
|
|
1921
|
+
return job;
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Submit job result
|
|
1925
|
+
*/
|
|
1926
|
+
async submitJobResult(result) {
|
|
1927
|
+
this.requireAuth();
|
|
1928
|
+
let encryptedOutput;
|
|
1929
|
+
if (result.output) {
|
|
1930
|
+
encryptedOutput = await this.encryptJobResult(result.output);
|
|
1931
|
+
}
|
|
1932
|
+
await this.client.request("/edge/submit-job-result", {
|
|
1933
|
+
method: "POST",
|
|
1934
|
+
body: {
|
|
1935
|
+
edgeNodeId: this.edgeNodeId,
|
|
1936
|
+
subscriberId: this.jwtPayload.subscriberId,
|
|
1937
|
+
result: {
|
|
1938
|
+
...result,
|
|
1939
|
+
output: encryptedOutput,
|
|
1940
|
+
encrypted: !!encryptedOutput
|
|
1941
|
+
}
|
|
1942
|
+
},
|
|
1943
|
+
headers: {
|
|
1944
|
+
"Authorization": `Bearer ${this.edgeNodeSecret}`
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
/**
|
|
1949
|
+
* Queue a job for processing
|
|
1950
|
+
*/
|
|
1951
|
+
async queueJob(type, params, targetPublicKey) {
|
|
1952
|
+
this.requireAuth();
|
|
1953
|
+
let encryptedParams;
|
|
1954
|
+
let encrypted = false;
|
|
1955
|
+
if (targetPublicKey) {
|
|
1956
|
+
encryptedParams = await this.encryptJobParamsForTarget(
|
|
1957
|
+
JSON.stringify(params),
|
|
1958
|
+
targetPublicKey
|
|
1959
|
+
);
|
|
1960
|
+
encrypted = true;
|
|
1961
|
+
}
|
|
1962
|
+
return this.client.request("/edge/queue-job", {
|
|
1963
|
+
method: "POST",
|
|
1964
|
+
body: {
|
|
1965
|
+
edgeNodeId: this.edgeNodeId,
|
|
1966
|
+
subscriberId: this.jwtPayload.subscriberId,
|
|
1967
|
+
job: {
|
|
1968
|
+
type,
|
|
1969
|
+
params: encryptedParams || JSON.stringify(params),
|
|
1970
|
+
encrypted
|
|
1971
|
+
}
|
|
1972
|
+
},
|
|
1973
|
+
headers: {
|
|
1974
|
+
"Authorization": `Bearer ${this.edgeNodeSecret}`
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Decrypt job params received from backend
|
|
1980
|
+
*/
|
|
1981
|
+
async decryptJobParams(encryptedParams) {
|
|
1982
|
+
const encrypted = JSON.parse(encryptedParams);
|
|
1983
|
+
return this.security.decryptFromBackend(encrypted);
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Encrypt job result for sending to backend
|
|
1987
|
+
*/
|
|
1988
|
+
async encryptJobResult(output) {
|
|
1989
|
+
const encrypted = await this.security.encryptLocal(output, this.keyPair.privateKey);
|
|
1990
|
+
return JSON.stringify(encrypted);
|
|
1991
|
+
}
|
|
1992
|
+
/**
|
|
1993
|
+
* Encrypt job params for a specific target edge node
|
|
1994
|
+
*/
|
|
1995
|
+
async encryptJobParamsForTarget(params, targetPublicKey) {
|
|
1996
|
+
const encrypted = await this.security.encryptForEdgeNode(params, targetPublicKey);
|
|
1997
|
+
return JSON.stringify(encrypted);
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Check if client is authenticated
|
|
2001
|
+
*/
|
|
2002
|
+
requireAuth() {
|
|
2003
|
+
if (!this.jwtPayload) {
|
|
2004
|
+
throw new Error("Client not authenticated. Call initialize() first.");
|
|
2005
|
+
}
|
|
2006
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2007
|
+
if (this.jwtPayload.expiresAt < now) {
|
|
2008
|
+
throw new Error("JWT token expired. Re-register edge node.");
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
/**
|
|
2012
|
+
* Get current authentication status
|
|
2013
|
+
*/
|
|
2014
|
+
getAuthStatus() {
|
|
2015
|
+
if (!this.jwtPayload) {
|
|
2016
|
+
return { authenticated: false };
|
|
2017
|
+
}
|
|
2018
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2019
|
+
return {
|
|
2020
|
+
authenticated: this.jwtPayload.expiresAt > now,
|
|
2021
|
+
edgeNodeId: this.jwtPayload.edgeNodeId,
|
|
2022
|
+
subscriberId: this.jwtPayload.subscriberId,
|
|
2023
|
+
expiresAt: this.jwtPayload.expiresAt,
|
|
2024
|
+
scopes: this.jwtPayload.scopes
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
};
|
|
2028
|
+
async function createSecureEdgeClient(client, options) {
|
|
2029
|
+
const secureClient = new SecureEdgeClient({
|
|
2030
|
+
client,
|
|
2031
|
+
...options
|
|
2032
|
+
});
|
|
2033
|
+
await secureClient.initialize();
|
|
2034
|
+
return secureClient;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
802
2037
|
// src/index.ts
|
|
803
2038
|
var VERSION = "0.1.0";
|
|
804
2039
|
// Annotate the CommonJS export names for ESM import in node:
|
|
805
2040
|
0 && (module.exports = {
|
|
806
2041
|
AuthenticationError,
|
|
2042
|
+
EdgeNodeSecurity,
|
|
807
2043
|
NetworkError,
|
|
808
2044
|
NotFoundError,
|
|
809
2045
|
Rainfall,
|
|
810
2046
|
RainfallClient,
|
|
2047
|
+
RainfallDaemonContext,
|
|
811
2048
|
RainfallError,
|
|
2049
|
+
RainfallListenerRegistry,
|
|
2050
|
+
RainfallNetworkedExecutor,
|
|
812
2051
|
RateLimitError,
|
|
2052
|
+
SecureEdgeClient,
|
|
813
2053
|
ServerError,
|
|
814
2054
|
TimeoutError,
|
|
815
2055
|
ToolNotFoundError,
|
|
816
2056
|
VERSION,
|
|
817
|
-
ValidationError
|
|
2057
|
+
ValidationError,
|
|
2058
|
+
createCronWorkflow,
|
|
2059
|
+
createEdgeNodeSecurity,
|
|
2060
|
+
createFileWatcherWorkflow,
|
|
2061
|
+
createSecureEdgeClient
|
|
818
2062
|
});
|