@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.
Files changed (58) hide show
  1. package/README.md +51 -0
  2. package/dist/chunk-7MRE4ZVI.mjs +662 -0
  3. package/dist/chunk-AQFC7YAX.mjs +27 -0
  4. package/dist/chunk-EI7SJH5K.mjs +85 -0
  5. package/dist/chunk-NTTAVKRT.mjs +89 -0
  6. package/dist/chunk-RVKW5KBT.mjs +269 -0
  7. package/dist/chunk-V5QWJVLC.mjs +662 -0
  8. package/dist/chunk-VDPKDC3R.mjs +869 -0
  9. package/dist/chunk-WOITG5TG.mjs +84 -0
  10. package/dist/chunk-XAHJQRBJ.mjs +269 -0
  11. package/dist/chunk-XEQ6U3JQ.mjs +269 -0
  12. package/dist/cli/index.js +3797 -632
  13. package/dist/cli/index.mjs +453 -36
  14. package/dist/config-7UT7GYSN.mjs +16 -0
  15. package/dist/config-DDTQQBN7.mjs +14 -0
  16. package/dist/config-MD45VGWD.mjs +14 -0
  17. package/dist/config-ZKNHII2A.mjs +8 -0
  18. package/dist/daemon/index.d.mts +168 -0
  19. package/dist/daemon/index.d.ts +168 -0
  20. package/dist/daemon/index.js +3182 -0
  21. package/dist/daemon/index.mjs +1548 -0
  22. package/dist/errors-BMPseAnM.d.mts +47 -0
  23. package/dist/errors-BMPseAnM.d.ts +47 -0
  24. package/dist/errors-CZdRoYyw.d.ts +332 -0
  25. package/dist/errors-Chjq1Mev.d.mts +332 -0
  26. package/dist/index.d.mts +249 -2
  27. package/dist/index.d.ts +249 -2
  28. package/dist/index.js +1247 -3
  29. package/dist/index.mjs +227 -2
  30. package/dist/listeners-B5Vy9Ao5.d.ts +372 -0
  31. package/dist/listeners-BbYIaNCs.d.mts +372 -0
  32. package/dist/listeners-CP2A9J_2.d.ts +372 -0
  33. package/dist/listeners-CTRSofnm.d.mts +372 -0
  34. package/dist/listeners-CYI-YwIF.d.mts +372 -0
  35. package/dist/listeners-DRwITBW_.d.mts +372 -0
  36. package/dist/listeners-DrMrvFT5.d.ts +372 -0
  37. package/dist/listeners-MNAnpZj-.d.mts +372 -0
  38. package/dist/listeners-PZI7iT85.d.ts +372 -0
  39. package/dist/listeners-QJeEtLbV.d.ts +372 -0
  40. package/dist/listeners-hp0Ib2Ox.d.ts +372 -0
  41. package/dist/listeners-jLwetUnx.d.mts +372 -0
  42. package/dist/mcp.d.mts +7 -2
  43. package/dist/mcp.d.ts +7 -2
  44. package/dist/mcp.js +92 -1
  45. package/dist/mcp.mjs +1 -1
  46. package/dist/sdk-4OvXPr8E.d.mts +1054 -0
  47. package/dist/sdk-4OvXPr8E.d.ts +1054 -0
  48. package/dist/sdk-CJ9g5lFo.d.mts +772 -0
  49. package/dist/sdk-CJ9g5lFo.d.ts +772 -0
  50. package/dist/sdk-CN1ezZrI.d.mts +1054 -0
  51. package/dist/sdk-CN1ezZrI.d.ts +1054 -0
  52. package/dist/sdk-DD1OeGRJ.d.mts +871 -0
  53. package/dist/sdk-DD1OeGRJ.d.ts +871 -0
  54. package/dist/sdk-Xw0BjsLd.d.mts +1054 -0
  55. package/dist/sdk-Xw0BjsLd.d.ts +1054 -0
  56. package/dist/types-GnRAfH-h.d.mts +489 -0
  57. package/dist/types-GnRAfH-h.d.ts +489 -0
  58. package/package.json +17 -5
@@ -0,0 +1,84 @@
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
+ return config.llm?.baseUrl || "http://localhost:1234/v1";
72
+ case "rainfall":
73
+ default:
74
+ return config.baseUrl || "https://api.rainfall.com";
75
+ }
76
+ }
77
+
78
+ export {
79
+ loadConfig,
80
+ saveConfig,
81
+ getLLMConfig,
82
+ isLocalProvider,
83
+ getProviderBaseUrl
84
+ };
@@ -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(32, passwordBytes);
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
+ };
@@ -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(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
+ export {
267
+ EdgeNodeSecurity,
268
+ createEdgeNodeSecurity
269
+ };