@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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/cli/config.ts
|
|
2
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
var CONFIG_DIR = join(homedir(), ".rainfall");
|
|
6
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
7
|
+
function loadConfig() {
|
|
8
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function saveConfig(config) {
|
|
18
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
19
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
loadConfig,
|
|
26
|
+
saveConfig
|
|
27
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/cli/config.ts
|
|
2
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
var CONFIG_DIR = join(homedir(), ".rainfall");
|
|
6
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
7
|
+
function loadConfig() {
|
|
8
|
+
let config = {};
|
|
9
|
+
if (existsSync(CONFIG_FILE)) {
|
|
10
|
+
try {
|
|
11
|
+
config = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
12
|
+
} catch {
|
|
13
|
+
config = {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (process.env.RAINFALL_API_KEY) {
|
|
17
|
+
config.apiKey = process.env.RAINFALL_API_KEY;
|
|
18
|
+
}
|
|
19
|
+
if (process.env.RAINFALL_BASE_URL) {
|
|
20
|
+
config.baseUrl = process.env.RAINFALL_BASE_URL;
|
|
21
|
+
}
|
|
22
|
+
if (!config.llm) {
|
|
23
|
+
config.llm = { provider: "rainfall" };
|
|
24
|
+
}
|
|
25
|
+
if (process.env.OPENAI_API_KEY) {
|
|
26
|
+
config.llm.provider = config.llm.provider || "openai";
|
|
27
|
+
config.llm.apiKey = process.env.OPENAI_API_KEY;
|
|
28
|
+
}
|
|
29
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
30
|
+
config.llm.provider = "anthropic";
|
|
31
|
+
config.llm.apiKey = process.env.ANTHROPIC_API_KEY;
|
|
32
|
+
}
|
|
33
|
+
if (process.env.OLLAMA_HOST || process.env.OLLAMA_URL) {
|
|
34
|
+
config.llm.provider = "ollama";
|
|
35
|
+
config.llm.baseUrl = process.env.OLLAMA_HOST || process.env.OLLAMA_URL;
|
|
36
|
+
}
|
|
37
|
+
if (process.env.LLM_MODEL) {
|
|
38
|
+
config.llm.model = process.env.LLM_MODEL;
|
|
39
|
+
}
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
42
|
+
function saveConfig(config) {
|
|
43
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
44
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
47
|
+
}
|
|
48
|
+
function getLLMConfig(config) {
|
|
49
|
+
const defaults = {
|
|
50
|
+
provider: "rainfall",
|
|
51
|
+
apiKey: config.apiKey || "",
|
|
52
|
+
baseUrl: config.baseUrl || "https://api.rainfall.com",
|
|
53
|
+
model: "llama-3.3-70b-versatile",
|
|
54
|
+
options: {}
|
|
55
|
+
};
|
|
56
|
+
return { ...defaults, ...config.llm };
|
|
57
|
+
}
|
|
58
|
+
function isLocalProvider(config) {
|
|
59
|
+
return config.llm?.provider === "ollama" || config.llm?.provider === "local";
|
|
60
|
+
}
|
|
61
|
+
function getProviderBaseUrl(config) {
|
|
62
|
+
const provider = config.llm?.provider || "rainfall";
|
|
63
|
+
switch (provider) {
|
|
64
|
+
case "openai":
|
|
65
|
+
return config.llm?.baseUrl || "https://api.openai.com/v1";
|
|
66
|
+
case "anthropic":
|
|
67
|
+
return config.llm?.baseUrl || "https://api.anthropic.com/v1";
|
|
68
|
+
case "ollama":
|
|
69
|
+
return config.llm?.baseUrl || "http://localhost:11434/v1";
|
|
70
|
+
case "local":
|
|
71
|
+
case "custom":
|
|
72
|
+
return config.llm?.baseUrl || "http://localhost:1234/v1";
|
|
73
|
+
case "rainfall":
|
|
74
|
+
default:
|
|
75
|
+
return config.baseUrl || "https://api.rainfall.com";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
loadConfig,
|
|
81
|
+
saveConfig,
|
|
82
|
+
getLLMConfig,
|
|
83
|
+
isLocalProvider,
|
|
84
|
+
getProviderBaseUrl
|
|
85
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/cli/config.ts
|
|
2
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
var CONFIG_DIR = join(homedir(), ".rainfall");
|
|
6
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
7
|
+
function getConfigDir() {
|
|
8
|
+
return CONFIG_DIR;
|
|
9
|
+
}
|
|
10
|
+
function loadConfig() {
|
|
11
|
+
let config = {};
|
|
12
|
+
if (existsSync(CONFIG_FILE)) {
|
|
13
|
+
try {
|
|
14
|
+
config = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
15
|
+
} catch {
|
|
16
|
+
config = {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (process.env.RAINFALL_API_KEY) {
|
|
20
|
+
config.apiKey = process.env.RAINFALL_API_KEY;
|
|
21
|
+
}
|
|
22
|
+
if (process.env.RAINFALL_BASE_URL) {
|
|
23
|
+
config.baseUrl = process.env.RAINFALL_BASE_URL;
|
|
24
|
+
}
|
|
25
|
+
if (!config.llm) {
|
|
26
|
+
config.llm = { provider: "rainfall" };
|
|
27
|
+
}
|
|
28
|
+
if (process.env.OPENAI_API_KEY) {
|
|
29
|
+
config.llm.provider = config.llm.provider || "openai";
|
|
30
|
+
config.llm.apiKey = process.env.OPENAI_API_KEY;
|
|
31
|
+
}
|
|
32
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
33
|
+
config.llm.provider = "anthropic";
|
|
34
|
+
config.llm.apiKey = process.env.ANTHROPIC_API_KEY;
|
|
35
|
+
}
|
|
36
|
+
if (process.env.OLLAMA_HOST || process.env.OLLAMA_URL) {
|
|
37
|
+
config.llm.provider = "ollama";
|
|
38
|
+
config.llm.baseUrl = process.env.OLLAMA_HOST || process.env.OLLAMA_URL;
|
|
39
|
+
}
|
|
40
|
+
if (process.env.LLM_MODEL) {
|
|
41
|
+
config.llm.model = process.env.LLM_MODEL;
|
|
42
|
+
}
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
45
|
+
function saveConfig(config) {
|
|
46
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
47
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
50
|
+
}
|
|
51
|
+
function getLLMConfig(config) {
|
|
52
|
+
const defaults = {
|
|
53
|
+
provider: "rainfall",
|
|
54
|
+
apiKey: config.apiKey || "",
|
|
55
|
+
baseUrl: config.baseUrl || "https://api.rainfall.com",
|
|
56
|
+
model: "llama-3.3-70b-versatile",
|
|
57
|
+
options: {}
|
|
58
|
+
};
|
|
59
|
+
return { ...defaults, ...config.llm };
|
|
60
|
+
}
|
|
61
|
+
function isLocalProvider(config) {
|
|
62
|
+
return config.llm?.provider === "ollama" || config.llm?.provider === "local";
|
|
63
|
+
}
|
|
64
|
+
function getProviderBaseUrl(config) {
|
|
65
|
+
const provider = config.llm?.provider || "rainfall";
|
|
66
|
+
switch (provider) {
|
|
67
|
+
case "openai":
|
|
68
|
+
return config.llm?.baseUrl || "https://api.openai.com/v1";
|
|
69
|
+
case "anthropic":
|
|
70
|
+
return config.llm?.baseUrl || "https://api.anthropic.com/v1";
|
|
71
|
+
case "ollama":
|
|
72
|
+
return config.llm?.baseUrl || "http://localhost:11434/v1";
|
|
73
|
+
case "local":
|
|
74
|
+
case "custom":
|
|
75
|
+
return config.llm?.baseUrl || "http://localhost:1234/v1";
|
|
76
|
+
case "rainfall":
|
|
77
|
+
default:
|
|
78
|
+
return config.baseUrl || "https://api.rainfall.com";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
getConfigDir,
|
|
84
|
+
loadConfig,
|
|
85
|
+
saveConfig,
|
|
86
|
+
getLLMConfig,
|
|
87
|
+
isLocalProvider,
|
|
88
|
+
getProviderBaseUrl
|
|
89
|
+
};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// src/security/edge-node.ts
|
|
2
|
+
import * as sodium from "libsodium-wrappers";
|
|
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(passwordBytes, void 0, 32);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
async function createEdgeNodeSecurity(options = {}) {
|
|
261
|
+
const security = new EdgeNodeSecurity(options);
|
|
262
|
+
await security.initialize();
|
|
263
|
+
return security;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export {
|
|
267
|
+
EdgeNodeSecurity,
|
|
268
|
+
createEdgeNodeSecurity
|
|
269
|
+
};
|