@rainfall-devkit/sdk 0.2.1 → 0.2.2
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/dist/chunk-KOCCGNEQ.mjs +269 -0
- package/dist/chunk-NCQVOLS4.mjs +269 -0
- package/dist/cli/index.js +12 -12
- package/dist/cli/index.mjs +1 -1
- package/dist/index.js +12 -12
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// src/security/edge-node.ts
|
|
2
|
+
import * as sodium from "libsodium-wrappers-sumo";
|
|
3
|
+
var EdgeNodeSecurity = class {
|
|
4
|
+
sodiumReady;
|
|
5
|
+
backendSecret;
|
|
6
|
+
keyPair;
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.sodiumReady = sodium.ready;
|
|
9
|
+
this.backendSecret = options.backendSecret;
|
|
10
|
+
this.keyPair = options.keyPair;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Initialize libsodium
|
|
14
|
+
*/
|
|
15
|
+
async initialize() {
|
|
16
|
+
await this.sodiumReady;
|
|
17
|
+
}
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// JWT Token Management
|
|
20
|
+
// ============================================================================
|
|
21
|
+
/**
|
|
22
|
+
* Generate a JWT token for an edge node
|
|
23
|
+
* Note: In production, this is done by the backend. This is for testing.
|
|
24
|
+
*/
|
|
25
|
+
generateJWT(edgeNodeId, subscriberId, expiresInDays = 30) {
|
|
26
|
+
if (!this.backendSecret) {
|
|
27
|
+
throw new Error("Backend secret not configured");
|
|
28
|
+
}
|
|
29
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
30
|
+
const exp = now + expiresInDays * 24 * 60 * 60;
|
|
31
|
+
const jti = this.generateTokenId();
|
|
32
|
+
const payload = {
|
|
33
|
+
sub: edgeNodeId,
|
|
34
|
+
iss: "rainfall-backend",
|
|
35
|
+
iat: now,
|
|
36
|
+
exp,
|
|
37
|
+
jti,
|
|
38
|
+
scope: ["edge:heartbeat", "edge:claim", "edge:submit", "edge:queue"]
|
|
39
|
+
};
|
|
40
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
41
|
+
const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
|
|
42
|
+
const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));
|
|
43
|
+
const signature = this.hmacSha256(
|
|
44
|
+
`${encodedHeader}.${encodedPayload}`,
|
|
45
|
+
this.backendSecret
|
|
46
|
+
);
|
|
47
|
+
const encodedSignature = this.base64UrlEncode(signature);
|
|
48
|
+
return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validate a JWT token
|
|
52
|
+
*/
|
|
53
|
+
validateJWT(token) {
|
|
54
|
+
const parts = token.split(".");
|
|
55
|
+
if (parts.length !== 3) {
|
|
56
|
+
throw new Error("Invalid JWT format");
|
|
57
|
+
}
|
|
58
|
+
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
59
|
+
if (this.backendSecret) {
|
|
60
|
+
const expectedSignature = this.hmacSha256(
|
|
61
|
+
`${encodedHeader}.${encodedPayload}`,
|
|
62
|
+
this.backendSecret
|
|
63
|
+
);
|
|
64
|
+
const expectedEncoded = this.base64UrlEncode(expectedSignature);
|
|
65
|
+
if (!this.timingSafeEqual(encodedSignature, expectedEncoded)) {
|
|
66
|
+
throw new Error("Invalid JWT signature");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const payload = JSON.parse(this.base64UrlDecode(encodedPayload));
|
|
70
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
71
|
+
if (payload.exp < now) {
|
|
72
|
+
throw new Error("JWT token expired");
|
|
73
|
+
}
|
|
74
|
+
if (payload.iss !== "rainfall-backend") {
|
|
75
|
+
throw new Error("Invalid JWT issuer");
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
edgeNodeId: payload.sub,
|
|
79
|
+
subscriberId: payload.sub,
|
|
80
|
+
// Same as edge node ID for now
|
|
81
|
+
scopes: payload.scope,
|
|
82
|
+
expiresAt: payload.exp
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Extract bearer token from Authorization header
|
|
87
|
+
*/
|
|
88
|
+
extractBearerToken(authHeader) {
|
|
89
|
+
if (!authHeader) return null;
|
|
90
|
+
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
91
|
+
return match ? match[1] : null;
|
|
92
|
+
}
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// ACL Enforcement
|
|
95
|
+
// ============================================================================
|
|
96
|
+
/**
|
|
97
|
+
* Check if an edge node is allowed to perform an action on a job
|
|
98
|
+
* Rule: Edge nodes can only access jobs for their own subscriber
|
|
99
|
+
*/
|
|
100
|
+
checkACL(check) {
|
|
101
|
+
if (check.subscriberId !== check.jobSubscriberId) {
|
|
102
|
+
return {
|
|
103
|
+
allowed: false,
|
|
104
|
+
reason: `Edge node ${check.edgeNodeId} cannot access jobs from subscriber ${check.jobSubscriberId}`
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const allowedActions = ["heartbeat", "claim", "submit", "queue"];
|
|
108
|
+
if (!allowedActions.includes(check.action)) {
|
|
109
|
+
return {
|
|
110
|
+
allowed: false,
|
|
111
|
+
reason: `Unknown action: ${check.action}`
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { allowed: true };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Middleware-style ACL check for job operations
|
|
118
|
+
*/
|
|
119
|
+
requireSameSubscriber(edgeNodeSubscriberId, jobSubscriberId, operation) {
|
|
120
|
+
const result = this.checkACL({
|
|
121
|
+
edgeNodeId: edgeNodeSubscriberId,
|
|
122
|
+
subscriberId: edgeNodeSubscriberId,
|
|
123
|
+
jobSubscriberId,
|
|
124
|
+
action: operation
|
|
125
|
+
});
|
|
126
|
+
if (!result.allowed) {
|
|
127
|
+
throw new Error(result.reason);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Encryption (Libsodium)
|
|
132
|
+
// ============================================================================
|
|
133
|
+
/**
|
|
134
|
+
* Generate a new Ed25519 key pair for an edge node
|
|
135
|
+
*/
|
|
136
|
+
async generateKeyPair() {
|
|
137
|
+
await this.sodiumReady;
|
|
138
|
+
const keyPair = sodium.crypto_box_keypair();
|
|
139
|
+
return {
|
|
140
|
+
publicKey: this.bytesToBase64(keyPair.publicKey),
|
|
141
|
+
privateKey: this.bytesToBase64(keyPair.privateKey)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Encrypt job parameters for a target edge node using its public key
|
|
146
|
+
*/
|
|
147
|
+
async encryptForEdgeNode(plaintext, targetPublicKeyBase64) {
|
|
148
|
+
await this.sodiumReady;
|
|
149
|
+
if (!this.keyPair) {
|
|
150
|
+
throw new Error("Local key pair not configured");
|
|
151
|
+
}
|
|
152
|
+
const targetPublicKey = this.base64ToBytes(targetPublicKeyBase64);
|
|
153
|
+
const ephemeralKeyPair = sodium.crypto_box_keypair();
|
|
154
|
+
const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES);
|
|
155
|
+
const message = new TextEncoder().encode(plaintext);
|
|
156
|
+
const ciphertext = sodium.crypto_box_easy(
|
|
157
|
+
message,
|
|
158
|
+
nonce,
|
|
159
|
+
targetPublicKey,
|
|
160
|
+
ephemeralKeyPair.privateKey
|
|
161
|
+
);
|
|
162
|
+
return {
|
|
163
|
+
ciphertext: this.bytesToBase64(ciphertext),
|
|
164
|
+
nonce: this.bytesToBase64(nonce),
|
|
165
|
+
ephemeralPublicKey: this.bytesToBase64(ephemeralKeyPair.publicKey)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Decrypt job parameters received from the backend
|
|
170
|
+
*/
|
|
171
|
+
async decryptFromBackend(encrypted) {
|
|
172
|
+
await this.sodiumReady;
|
|
173
|
+
if (!this.keyPair) {
|
|
174
|
+
throw new Error("Local key pair not configured");
|
|
175
|
+
}
|
|
176
|
+
const privateKey = this.base64ToBytes(this.keyPair.privateKey);
|
|
177
|
+
const ephemeralPublicKey = this.base64ToBytes(encrypted.ephemeralPublicKey);
|
|
178
|
+
const nonce = this.base64ToBytes(encrypted.nonce);
|
|
179
|
+
const ciphertext = this.base64ToBytes(encrypted.ciphertext);
|
|
180
|
+
const decrypted = sodium.crypto_box_open_easy(
|
|
181
|
+
ciphertext,
|
|
182
|
+
nonce,
|
|
183
|
+
ephemeralPublicKey,
|
|
184
|
+
privateKey
|
|
185
|
+
);
|
|
186
|
+
if (!decrypted) {
|
|
187
|
+
throw new Error("Decryption failed - invalid ciphertext or keys");
|
|
188
|
+
}
|
|
189
|
+
return new TextDecoder().decode(decrypted);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Encrypt job parameters for local storage (using secretbox)
|
|
193
|
+
*/
|
|
194
|
+
async encryptLocal(plaintext, key) {
|
|
195
|
+
await this.sodiumReady;
|
|
196
|
+
const keyBytes = this.deriveKey(key);
|
|
197
|
+
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
|
198
|
+
const message = new TextEncoder().encode(plaintext);
|
|
199
|
+
const ciphertext = sodium.crypto_secretbox_easy(message, nonce, keyBytes);
|
|
200
|
+
return {
|
|
201
|
+
ciphertext: this.bytesToBase64(ciphertext),
|
|
202
|
+
nonce: this.bytesToBase64(nonce)
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Decrypt locally stored job parameters
|
|
207
|
+
*/
|
|
208
|
+
async decryptLocal(encrypted, key) {
|
|
209
|
+
await this.sodiumReady;
|
|
210
|
+
const keyBytes = this.deriveKey(key);
|
|
211
|
+
const nonce = this.base64ToBytes(encrypted.nonce);
|
|
212
|
+
const ciphertext = this.base64ToBytes(encrypted.ciphertext);
|
|
213
|
+
const decrypted = sodium.crypto_secretbox_open_easy(ciphertext, nonce, keyBytes);
|
|
214
|
+
if (!decrypted) {
|
|
215
|
+
throw new Error("Local decryption failed");
|
|
216
|
+
}
|
|
217
|
+
return new TextDecoder().decode(decrypted);
|
|
218
|
+
}
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// Utility Methods
|
|
221
|
+
// ============================================================================
|
|
222
|
+
generateTokenId() {
|
|
223
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
224
|
+
}
|
|
225
|
+
base64UrlEncode(str) {
|
|
226
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
227
|
+
}
|
|
228
|
+
base64UrlDecode(str) {
|
|
229
|
+
const padding = "=".repeat((4 - str.length % 4) % 4);
|
|
230
|
+
const base64 = str.replace(/-/g, "+").replace(/_/g, "/") + padding;
|
|
231
|
+
return atob(base64);
|
|
232
|
+
}
|
|
233
|
+
hmacSha256(message, secret) {
|
|
234
|
+
const key = new TextEncoder().encode(secret);
|
|
235
|
+
const msg = new TextEncoder().encode(message);
|
|
236
|
+
const hash = sodium.crypto_auth(msg, key);
|
|
237
|
+
return this.bytesToBase64(hash);
|
|
238
|
+
}
|
|
239
|
+
timingSafeEqual(a, b) {
|
|
240
|
+
if (a.length !== b.length) return false;
|
|
241
|
+
let result = 0;
|
|
242
|
+
for (let i = 0; i < a.length; i++) {
|
|
243
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
244
|
+
}
|
|
245
|
+
return result === 0;
|
|
246
|
+
}
|
|
247
|
+
bytesToBase64(bytes) {
|
|
248
|
+
const binString = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
|
|
249
|
+
return btoa(binString);
|
|
250
|
+
}
|
|
251
|
+
base64ToBytes(base64) {
|
|
252
|
+
const binString = atob(base64);
|
|
253
|
+
return Uint8Array.from(binString, (m) => m.charCodeAt(0));
|
|
254
|
+
}
|
|
255
|
+
deriveKey(password) {
|
|
256
|
+
const passwordBytes = new TextEncoder().encode(password);
|
|
257
|
+
return sodium.crypto_generichash(32, passwordBytes, null);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
async function createEdgeNodeSecurity(options = {}) {
|
|
261
|
+
const security = new EdgeNodeSecurity(options);
|
|
262
|
+
await security.initialize();
|
|
263
|
+
return security;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export {
|
|
267
|
+
EdgeNodeSecurity,
|
|
268
|
+
createEdgeNodeSecurity
|
|
269
|
+
};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// src/security/edge-node.ts
|
|
2
|
+
import sodium from "libsodium-wrappers-sumo";
|
|
3
|
+
var EdgeNodeSecurity = class {
|
|
4
|
+
sodiumReady;
|
|
5
|
+
backendSecret;
|
|
6
|
+
keyPair;
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.sodiumReady = sodium.ready;
|
|
9
|
+
this.backendSecret = options.backendSecret;
|
|
10
|
+
this.keyPair = options.keyPair;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Initialize libsodium
|
|
14
|
+
*/
|
|
15
|
+
async initialize() {
|
|
16
|
+
await this.sodiumReady;
|
|
17
|
+
}
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// JWT Token Management
|
|
20
|
+
// ============================================================================
|
|
21
|
+
/**
|
|
22
|
+
* Generate a JWT token for an edge node
|
|
23
|
+
* Note: In production, this is done by the backend. This is for testing.
|
|
24
|
+
*/
|
|
25
|
+
generateJWT(edgeNodeId, subscriberId, expiresInDays = 30) {
|
|
26
|
+
if (!this.backendSecret) {
|
|
27
|
+
throw new Error("Backend secret not configured");
|
|
28
|
+
}
|
|
29
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
30
|
+
const exp = now + expiresInDays * 24 * 60 * 60;
|
|
31
|
+
const jti = this.generateTokenId();
|
|
32
|
+
const payload = {
|
|
33
|
+
sub: edgeNodeId,
|
|
34
|
+
iss: "rainfall-backend",
|
|
35
|
+
iat: now,
|
|
36
|
+
exp,
|
|
37
|
+
jti,
|
|
38
|
+
scope: ["edge:heartbeat", "edge:claim", "edge:submit", "edge:queue"]
|
|
39
|
+
};
|
|
40
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
41
|
+
const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
|
|
42
|
+
const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));
|
|
43
|
+
const signature = this.hmacSha256(
|
|
44
|
+
`${encodedHeader}.${encodedPayload}`,
|
|
45
|
+
this.backendSecret
|
|
46
|
+
);
|
|
47
|
+
const encodedSignature = this.base64UrlEncode(signature);
|
|
48
|
+
return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validate a JWT token
|
|
52
|
+
*/
|
|
53
|
+
validateJWT(token) {
|
|
54
|
+
const parts = token.split(".");
|
|
55
|
+
if (parts.length !== 3) {
|
|
56
|
+
throw new Error("Invalid JWT format");
|
|
57
|
+
}
|
|
58
|
+
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
59
|
+
if (this.backendSecret) {
|
|
60
|
+
const expectedSignature = this.hmacSha256(
|
|
61
|
+
`${encodedHeader}.${encodedPayload}`,
|
|
62
|
+
this.backendSecret
|
|
63
|
+
);
|
|
64
|
+
const expectedEncoded = this.base64UrlEncode(expectedSignature);
|
|
65
|
+
if (!this.timingSafeEqual(encodedSignature, expectedEncoded)) {
|
|
66
|
+
throw new Error("Invalid JWT signature");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const payload = JSON.parse(this.base64UrlDecode(encodedPayload));
|
|
70
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
71
|
+
if (payload.exp < now) {
|
|
72
|
+
throw new Error("JWT token expired");
|
|
73
|
+
}
|
|
74
|
+
if (payload.iss !== "rainfall-backend") {
|
|
75
|
+
throw new Error("Invalid JWT issuer");
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
edgeNodeId: payload.sub,
|
|
79
|
+
subscriberId: payload.sub,
|
|
80
|
+
// Same as edge node ID for now
|
|
81
|
+
scopes: payload.scope,
|
|
82
|
+
expiresAt: payload.exp
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Extract bearer token from Authorization header
|
|
87
|
+
*/
|
|
88
|
+
extractBearerToken(authHeader) {
|
|
89
|
+
if (!authHeader) return null;
|
|
90
|
+
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
91
|
+
return match ? match[1] : null;
|
|
92
|
+
}
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// ACL Enforcement
|
|
95
|
+
// ============================================================================
|
|
96
|
+
/**
|
|
97
|
+
* Check if an edge node is allowed to perform an action on a job
|
|
98
|
+
* Rule: Edge nodes can only access jobs for their own subscriber
|
|
99
|
+
*/
|
|
100
|
+
checkACL(check) {
|
|
101
|
+
if (check.subscriberId !== check.jobSubscriberId) {
|
|
102
|
+
return {
|
|
103
|
+
allowed: false,
|
|
104
|
+
reason: `Edge node ${check.edgeNodeId} cannot access jobs from subscriber ${check.jobSubscriberId}`
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const allowedActions = ["heartbeat", "claim", "submit", "queue"];
|
|
108
|
+
if (!allowedActions.includes(check.action)) {
|
|
109
|
+
return {
|
|
110
|
+
allowed: false,
|
|
111
|
+
reason: `Unknown action: ${check.action}`
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { allowed: true };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Middleware-style ACL check for job operations
|
|
118
|
+
*/
|
|
119
|
+
requireSameSubscriber(edgeNodeSubscriberId, jobSubscriberId, operation) {
|
|
120
|
+
const result = this.checkACL({
|
|
121
|
+
edgeNodeId: edgeNodeSubscriberId,
|
|
122
|
+
subscriberId: edgeNodeSubscriberId,
|
|
123
|
+
jobSubscriberId,
|
|
124
|
+
action: operation
|
|
125
|
+
});
|
|
126
|
+
if (!result.allowed) {
|
|
127
|
+
throw new Error(result.reason);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Encryption (Libsodium)
|
|
132
|
+
// ============================================================================
|
|
133
|
+
/**
|
|
134
|
+
* Generate a new Ed25519 key pair for an edge node
|
|
135
|
+
*/
|
|
136
|
+
async generateKeyPair() {
|
|
137
|
+
await this.sodiumReady;
|
|
138
|
+
const keyPair = sodium.crypto_box_keypair();
|
|
139
|
+
return {
|
|
140
|
+
publicKey: this.bytesToBase64(keyPair.publicKey),
|
|
141
|
+
privateKey: this.bytesToBase64(keyPair.privateKey)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Encrypt job parameters for a target edge node using its public key
|
|
146
|
+
*/
|
|
147
|
+
async encryptForEdgeNode(plaintext, targetPublicKeyBase64) {
|
|
148
|
+
await this.sodiumReady;
|
|
149
|
+
if (!this.keyPair) {
|
|
150
|
+
throw new Error("Local key pair not configured");
|
|
151
|
+
}
|
|
152
|
+
const targetPublicKey = this.base64ToBytes(targetPublicKeyBase64);
|
|
153
|
+
const ephemeralKeyPair = sodium.crypto_box_keypair();
|
|
154
|
+
const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES);
|
|
155
|
+
const message = new TextEncoder().encode(plaintext);
|
|
156
|
+
const ciphertext = sodium.crypto_box_easy(
|
|
157
|
+
message,
|
|
158
|
+
nonce,
|
|
159
|
+
targetPublicKey,
|
|
160
|
+
ephemeralKeyPair.privateKey
|
|
161
|
+
);
|
|
162
|
+
return {
|
|
163
|
+
ciphertext: this.bytesToBase64(ciphertext),
|
|
164
|
+
nonce: this.bytesToBase64(nonce),
|
|
165
|
+
ephemeralPublicKey: this.bytesToBase64(ephemeralKeyPair.publicKey)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Decrypt job parameters received from the backend
|
|
170
|
+
*/
|
|
171
|
+
async decryptFromBackend(encrypted) {
|
|
172
|
+
await this.sodiumReady;
|
|
173
|
+
if (!this.keyPair) {
|
|
174
|
+
throw new Error("Local key pair not configured");
|
|
175
|
+
}
|
|
176
|
+
const privateKey = this.base64ToBytes(this.keyPair.privateKey);
|
|
177
|
+
const ephemeralPublicKey = this.base64ToBytes(encrypted.ephemeralPublicKey);
|
|
178
|
+
const nonce = this.base64ToBytes(encrypted.nonce);
|
|
179
|
+
const ciphertext = this.base64ToBytes(encrypted.ciphertext);
|
|
180
|
+
const decrypted = sodium.crypto_box_open_easy(
|
|
181
|
+
ciphertext,
|
|
182
|
+
nonce,
|
|
183
|
+
ephemeralPublicKey,
|
|
184
|
+
privateKey
|
|
185
|
+
);
|
|
186
|
+
if (!decrypted) {
|
|
187
|
+
throw new Error("Decryption failed - invalid ciphertext or keys");
|
|
188
|
+
}
|
|
189
|
+
return new TextDecoder().decode(decrypted);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Encrypt job parameters for local storage (using secretbox)
|
|
193
|
+
*/
|
|
194
|
+
async encryptLocal(plaintext, key) {
|
|
195
|
+
await this.sodiumReady;
|
|
196
|
+
const keyBytes = this.deriveKey(key);
|
|
197
|
+
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
|
198
|
+
const message = new TextEncoder().encode(plaintext);
|
|
199
|
+
const ciphertext = sodium.crypto_secretbox_easy(message, nonce, keyBytes);
|
|
200
|
+
return {
|
|
201
|
+
ciphertext: this.bytesToBase64(ciphertext),
|
|
202
|
+
nonce: this.bytesToBase64(nonce)
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Decrypt locally stored job parameters
|
|
207
|
+
*/
|
|
208
|
+
async decryptLocal(encrypted, key) {
|
|
209
|
+
await this.sodiumReady;
|
|
210
|
+
const keyBytes = this.deriveKey(key);
|
|
211
|
+
const nonce = this.base64ToBytes(encrypted.nonce);
|
|
212
|
+
const ciphertext = this.base64ToBytes(encrypted.ciphertext);
|
|
213
|
+
const decrypted = sodium.crypto_secretbox_open_easy(ciphertext, nonce, keyBytes);
|
|
214
|
+
if (!decrypted) {
|
|
215
|
+
throw new Error("Local decryption failed");
|
|
216
|
+
}
|
|
217
|
+
return new TextDecoder().decode(decrypted);
|
|
218
|
+
}
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// Utility Methods
|
|
221
|
+
// ============================================================================
|
|
222
|
+
generateTokenId() {
|
|
223
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
224
|
+
}
|
|
225
|
+
base64UrlEncode(str) {
|
|
226
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
227
|
+
}
|
|
228
|
+
base64UrlDecode(str) {
|
|
229
|
+
const padding = "=".repeat((4 - str.length % 4) % 4);
|
|
230
|
+
const base64 = str.replace(/-/g, "+").replace(/_/g, "/") + padding;
|
|
231
|
+
return atob(base64);
|
|
232
|
+
}
|
|
233
|
+
hmacSha256(message, secret) {
|
|
234
|
+
const key = new TextEncoder().encode(secret);
|
|
235
|
+
const msg = new TextEncoder().encode(message);
|
|
236
|
+
const hash = sodium.crypto_auth(msg, key);
|
|
237
|
+
return this.bytesToBase64(hash);
|
|
238
|
+
}
|
|
239
|
+
timingSafeEqual(a, b) {
|
|
240
|
+
if (a.length !== b.length) return false;
|
|
241
|
+
let result = 0;
|
|
242
|
+
for (let i = 0; i < a.length; i++) {
|
|
243
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
244
|
+
}
|
|
245
|
+
return result === 0;
|
|
246
|
+
}
|
|
247
|
+
bytesToBase64(bytes) {
|
|
248
|
+
const binString = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
|
|
249
|
+
return btoa(binString);
|
|
250
|
+
}
|
|
251
|
+
base64ToBytes(base64) {
|
|
252
|
+
const binString = atob(base64);
|
|
253
|
+
return Uint8Array.from(binString, (m) => m.charCodeAt(0));
|
|
254
|
+
}
|
|
255
|
+
deriveKey(password) {
|
|
256
|
+
const passwordBytes = new TextEncoder().encode(password);
|
|
257
|
+
return sodium.crypto_generichash(32, passwordBytes, null);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
async function createEdgeNodeSecurity(options = {}) {
|
|
261
|
+
const security = new EdgeNodeSecurity(options);
|
|
262
|
+
await security.initialize();
|
|
263
|
+
return security;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export {
|
|
267
|
+
EdgeNodeSecurity,
|
|
268
|
+
createEdgeNodeSecurity
|
|
269
|
+
};
|
package/dist/cli/index.js
CHANGED
|
@@ -3278,13 +3278,13 @@ var import_child_process = require("child_process");
|
|
|
3278
3278
|
|
|
3279
3279
|
// src/security/edge-node.ts
|
|
3280
3280
|
init_cjs_shims();
|
|
3281
|
-
var
|
|
3281
|
+
var import_libsodium_wrappers_sumo = __toESM(require("libsodium-wrappers-sumo"));
|
|
3282
3282
|
var EdgeNodeSecurity = class {
|
|
3283
3283
|
sodiumReady;
|
|
3284
3284
|
backendSecret;
|
|
3285
3285
|
keyPair;
|
|
3286
3286
|
constructor(options = {}) {
|
|
3287
|
-
this.sodiumReady =
|
|
3287
|
+
this.sodiumReady = import_libsodium_wrappers_sumo.default.ready;
|
|
3288
3288
|
this.backendSecret = options.backendSecret;
|
|
3289
3289
|
this.keyPair = options.keyPair;
|
|
3290
3290
|
}
|
|
@@ -3414,7 +3414,7 @@ var EdgeNodeSecurity = class {
|
|
|
3414
3414
|
*/
|
|
3415
3415
|
async generateKeyPair() {
|
|
3416
3416
|
await this.sodiumReady;
|
|
3417
|
-
const keyPair =
|
|
3417
|
+
const keyPair = import_libsodium_wrappers_sumo.default.crypto_box_keypair();
|
|
3418
3418
|
return {
|
|
3419
3419
|
publicKey: this.bytesToBase64(keyPair.publicKey),
|
|
3420
3420
|
privateKey: this.bytesToBase64(keyPair.privateKey)
|
|
@@ -3429,10 +3429,10 @@ var EdgeNodeSecurity = class {
|
|
|
3429
3429
|
throw new Error("Local key pair not configured");
|
|
3430
3430
|
}
|
|
3431
3431
|
const targetPublicKey = this.base64ToBytes(targetPublicKeyBase64);
|
|
3432
|
-
const ephemeralKeyPair =
|
|
3433
|
-
const nonce =
|
|
3432
|
+
const ephemeralKeyPair = import_libsodium_wrappers_sumo.default.crypto_box_keypair();
|
|
3433
|
+
const nonce = import_libsodium_wrappers_sumo.default.randombytes_buf(import_libsodium_wrappers_sumo.default.crypto_box_NONCEBYTES);
|
|
3434
3434
|
const message = new TextEncoder().encode(plaintext);
|
|
3435
|
-
const ciphertext =
|
|
3435
|
+
const ciphertext = import_libsodium_wrappers_sumo.default.crypto_box_easy(
|
|
3436
3436
|
message,
|
|
3437
3437
|
nonce,
|
|
3438
3438
|
targetPublicKey,
|
|
@@ -3456,7 +3456,7 @@ var EdgeNodeSecurity = class {
|
|
|
3456
3456
|
const ephemeralPublicKey = this.base64ToBytes(encrypted.ephemeralPublicKey);
|
|
3457
3457
|
const nonce = this.base64ToBytes(encrypted.nonce);
|
|
3458
3458
|
const ciphertext = this.base64ToBytes(encrypted.ciphertext);
|
|
3459
|
-
const decrypted =
|
|
3459
|
+
const decrypted = import_libsodium_wrappers_sumo.default.crypto_box_open_easy(
|
|
3460
3460
|
ciphertext,
|
|
3461
3461
|
nonce,
|
|
3462
3462
|
ephemeralPublicKey,
|
|
@@ -3473,9 +3473,9 @@ var EdgeNodeSecurity = class {
|
|
|
3473
3473
|
async encryptLocal(plaintext, key) {
|
|
3474
3474
|
await this.sodiumReady;
|
|
3475
3475
|
const keyBytes = this.deriveKey(key);
|
|
3476
|
-
const nonce =
|
|
3476
|
+
const nonce = import_libsodium_wrappers_sumo.default.randombytes_buf(import_libsodium_wrappers_sumo.default.crypto_secretbox_NONCEBYTES);
|
|
3477
3477
|
const message = new TextEncoder().encode(plaintext);
|
|
3478
|
-
const ciphertext =
|
|
3478
|
+
const ciphertext = import_libsodium_wrappers_sumo.default.crypto_secretbox_easy(message, nonce, keyBytes);
|
|
3479
3479
|
return {
|
|
3480
3480
|
ciphertext: this.bytesToBase64(ciphertext),
|
|
3481
3481
|
nonce: this.bytesToBase64(nonce)
|
|
@@ -3489,7 +3489,7 @@ var EdgeNodeSecurity = class {
|
|
|
3489
3489
|
const keyBytes = this.deriveKey(key);
|
|
3490
3490
|
const nonce = this.base64ToBytes(encrypted.nonce);
|
|
3491
3491
|
const ciphertext = this.base64ToBytes(encrypted.ciphertext);
|
|
3492
|
-
const decrypted =
|
|
3492
|
+
const decrypted = import_libsodium_wrappers_sumo.default.crypto_secretbox_open_easy(ciphertext, nonce, keyBytes);
|
|
3493
3493
|
if (!decrypted) {
|
|
3494
3494
|
throw new Error("Local decryption failed");
|
|
3495
3495
|
}
|
|
@@ -3512,7 +3512,7 @@ var EdgeNodeSecurity = class {
|
|
|
3512
3512
|
hmacSha256(message, secret) {
|
|
3513
3513
|
const key = new TextEncoder().encode(secret);
|
|
3514
3514
|
const msg = new TextEncoder().encode(message);
|
|
3515
|
-
const hash =
|
|
3515
|
+
const hash = import_libsodium_wrappers_sumo.default.crypto_auth(msg, key);
|
|
3516
3516
|
return this.bytesToBase64(hash);
|
|
3517
3517
|
}
|
|
3518
3518
|
timingSafeEqual(a, b) {
|
|
@@ -3533,7 +3533,7 @@ var EdgeNodeSecurity = class {
|
|
|
3533
3533
|
}
|
|
3534
3534
|
deriveKey(password) {
|
|
3535
3535
|
const passwordBytes = new TextEncoder().encode(password);
|
|
3536
|
-
return
|
|
3536
|
+
return import_libsodium_wrappers_sumo.default.crypto_generichash(32, passwordBytes, null);
|
|
3537
3537
|
}
|
|
3538
3538
|
};
|
|
3539
3539
|
async function createEdgeNodeSecurity(options = {}) {
|
package/dist/cli/index.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -1565,13 +1565,13 @@ function createCronWorkflow(name, cron, workflow) {
|
|
|
1565
1565
|
}
|
|
1566
1566
|
|
|
1567
1567
|
// src/security/edge-node.ts
|
|
1568
|
-
var
|
|
1568
|
+
var import_libsodium_wrappers_sumo = __toESM(require("libsodium-wrappers-sumo"));
|
|
1569
1569
|
var EdgeNodeSecurity = class {
|
|
1570
1570
|
sodiumReady;
|
|
1571
1571
|
backendSecret;
|
|
1572
1572
|
keyPair;
|
|
1573
1573
|
constructor(options = {}) {
|
|
1574
|
-
this.sodiumReady =
|
|
1574
|
+
this.sodiumReady = import_libsodium_wrappers_sumo.default.ready;
|
|
1575
1575
|
this.backendSecret = options.backendSecret;
|
|
1576
1576
|
this.keyPair = options.keyPair;
|
|
1577
1577
|
}
|
|
@@ -1701,7 +1701,7 @@ var EdgeNodeSecurity = class {
|
|
|
1701
1701
|
*/
|
|
1702
1702
|
async generateKeyPair() {
|
|
1703
1703
|
await this.sodiumReady;
|
|
1704
|
-
const keyPair =
|
|
1704
|
+
const keyPair = import_libsodium_wrappers_sumo.default.crypto_box_keypair();
|
|
1705
1705
|
return {
|
|
1706
1706
|
publicKey: this.bytesToBase64(keyPair.publicKey),
|
|
1707
1707
|
privateKey: this.bytesToBase64(keyPair.privateKey)
|
|
@@ -1716,10 +1716,10 @@ var EdgeNodeSecurity = class {
|
|
|
1716
1716
|
throw new Error("Local key pair not configured");
|
|
1717
1717
|
}
|
|
1718
1718
|
const targetPublicKey = this.base64ToBytes(targetPublicKeyBase64);
|
|
1719
|
-
const ephemeralKeyPair =
|
|
1720
|
-
const nonce =
|
|
1719
|
+
const ephemeralKeyPair = import_libsodium_wrappers_sumo.default.crypto_box_keypair();
|
|
1720
|
+
const nonce = import_libsodium_wrappers_sumo.default.randombytes_buf(import_libsodium_wrappers_sumo.default.crypto_box_NONCEBYTES);
|
|
1721
1721
|
const message = new TextEncoder().encode(plaintext);
|
|
1722
|
-
const ciphertext =
|
|
1722
|
+
const ciphertext = import_libsodium_wrappers_sumo.default.crypto_box_easy(
|
|
1723
1723
|
message,
|
|
1724
1724
|
nonce,
|
|
1725
1725
|
targetPublicKey,
|
|
@@ -1743,7 +1743,7 @@ var EdgeNodeSecurity = class {
|
|
|
1743
1743
|
const ephemeralPublicKey = this.base64ToBytes(encrypted.ephemeralPublicKey);
|
|
1744
1744
|
const nonce = this.base64ToBytes(encrypted.nonce);
|
|
1745
1745
|
const ciphertext = this.base64ToBytes(encrypted.ciphertext);
|
|
1746
|
-
const decrypted =
|
|
1746
|
+
const decrypted = import_libsodium_wrappers_sumo.default.crypto_box_open_easy(
|
|
1747
1747
|
ciphertext,
|
|
1748
1748
|
nonce,
|
|
1749
1749
|
ephemeralPublicKey,
|
|
@@ -1760,9 +1760,9 @@ var EdgeNodeSecurity = class {
|
|
|
1760
1760
|
async encryptLocal(plaintext, key) {
|
|
1761
1761
|
await this.sodiumReady;
|
|
1762
1762
|
const keyBytes = this.deriveKey(key);
|
|
1763
|
-
const nonce =
|
|
1763
|
+
const nonce = import_libsodium_wrappers_sumo.default.randombytes_buf(import_libsodium_wrappers_sumo.default.crypto_secretbox_NONCEBYTES);
|
|
1764
1764
|
const message = new TextEncoder().encode(plaintext);
|
|
1765
|
-
const ciphertext =
|
|
1765
|
+
const ciphertext = import_libsodium_wrappers_sumo.default.crypto_secretbox_easy(message, nonce, keyBytes);
|
|
1766
1766
|
return {
|
|
1767
1767
|
ciphertext: this.bytesToBase64(ciphertext),
|
|
1768
1768
|
nonce: this.bytesToBase64(nonce)
|
|
@@ -1776,7 +1776,7 @@ var EdgeNodeSecurity = class {
|
|
|
1776
1776
|
const keyBytes = this.deriveKey(key);
|
|
1777
1777
|
const nonce = this.base64ToBytes(encrypted.nonce);
|
|
1778
1778
|
const ciphertext = this.base64ToBytes(encrypted.ciphertext);
|
|
1779
|
-
const decrypted =
|
|
1779
|
+
const decrypted = import_libsodium_wrappers_sumo.default.crypto_secretbox_open_easy(ciphertext, nonce, keyBytes);
|
|
1780
1780
|
if (!decrypted) {
|
|
1781
1781
|
throw new Error("Local decryption failed");
|
|
1782
1782
|
}
|
|
@@ -1799,7 +1799,7 @@ var EdgeNodeSecurity = class {
|
|
|
1799
1799
|
hmacSha256(message, secret) {
|
|
1800
1800
|
const key = new TextEncoder().encode(secret);
|
|
1801
1801
|
const msg = new TextEncoder().encode(message);
|
|
1802
|
-
const hash =
|
|
1802
|
+
const hash = import_libsodium_wrappers_sumo.default.crypto_auth(msg, key);
|
|
1803
1803
|
return this.bytesToBase64(hash);
|
|
1804
1804
|
}
|
|
1805
1805
|
timingSafeEqual(a, b) {
|
|
@@ -1820,7 +1820,7 @@ var EdgeNodeSecurity = class {
|
|
|
1820
1820
|
}
|
|
1821
1821
|
deriveKey(password) {
|
|
1822
1822
|
const passwordBytes = new TextEncoder().encode(password);
|
|
1823
|
-
return
|
|
1823
|
+
return import_libsodium_wrappers_sumo.default.crypto_generichash(32, passwordBytes, null);
|
|
1824
1824
|
}
|
|
1825
1825
|
};
|
|
1826
1826
|
async function createEdgeNodeSecurity(options = {}) {
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rainfall-devkit/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Official SDK for Rainfall API - 200+ tools for building AI-powered applications",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
77
77
|
"express": "^4",
|
|
78
|
-
"libsodium-wrappers": "^0.8.2",
|
|
78
|
+
"libsodium-wrappers-sumo": "^0.8.2",
|
|
79
79
|
"ws": "^8"
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|