@rainfall-devkit/sdk 0.2.0 → 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/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,12 +17,21 @@ 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,
@@ -30,13 +41,16 @@ __export(index_exports, {
30
41
  RainfallListenerRegistry: () => RainfallListenerRegistry,
31
42
  RainfallNetworkedExecutor: () => RainfallNetworkedExecutor,
32
43
  RateLimitError: () => RateLimitError,
44
+ SecureEdgeClient: () => SecureEdgeClient,
33
45
  ServerError: () => ServerError,
34
46
  TimeoutError: () => TimeoutError,
35
47
  ToolNotFoundError: () => ToolNotFoundError,
36
48
  VERSION: () => VERSION,
37
49
  ValidationError: () => ValidationError,
38
50
  createCronWorkflow: () => createCronWorkflow,
39
- createFileWatcherWorkflow: () => createFileWatcherWorkflow
51
+ createEdgeNodeSecurity: () => createEdgeNodeSecurity,
52
+ createFileWatcherWorkflow: () => createFileWatcherWorkflow,
53
+ createSecureEdgeClient: () => createSecureEdgeClient
40
54
  });
41
55
  module.exports = __toCommonJS(index_exports);
42
56
 
@@ -1550,11 +1564,482 @@ function createCronWorkflow(name, cron, workflow) {
1550
1564
  };
1551
1565
  }
1552
1566
 
1567
+ // src/security/edge-node.ts
1568
+ var import_libsodium_wrappers_sumo = __toESM(require("libsodium-wrappers-sumo"));
1569
+ var EdgeNodeSecurity = class {
1570
+ sodiumReady;
1571
+ backendSecret;
1572
+ keyPair;
1573
+ constructor(options = {}) {
1574
+ this.sodiumReady = import_libsodium_wrappers_sumo.default.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 = import_libsodium_wrappers_sumo.default.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 = 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
+ const message = new TextEncoder().encode(plaintext);
1722
+ const ciphertext = import_libsodium_wrappers_sumo.default.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 = import_libsodium_wrappers_sumo.default.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 = import_libsodium_wrappers_sumo.default.randombytes_buf(import_libsodium_wrappers_sumo.default.crypto_secretbox_NONCEBYTES);
1764
+ const message = new TextEncoder().encode(plaintext);
1765
+ const ciphertext = import_libsodium_wrappers_sumo.default.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 = import_libsodium_wrappers_sumo.default.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 = import_libsodium_wrappers_sumo.default.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 import_libsodium_wrappers_sumo.default.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
+
1553
2037
  // src/index.ts
1554
2038
  var VERSION = "0.1.0";
1555
2039
  // Annotate the CommonJS export names for ESM import in node:
1556
2040
  0 && (module.exports = {
1557
2041
  AuthenticationError,
2042
+ EdgeNodeSecurity,
1558
2043
  NetworkError,
1559
2044
  NotFoundError,
1560
2045
  Rainfall,
@@ -1564,11 +2049,14 @@ var VERSION = "0.1.0";
1564
2049
  RainfallListenerRegistry,
1565
2050
  RainfallNetworkedExecutor,
1566
2051
  RateLimitError,
2052
+ SecureEdgeClient,
1567
2053
  ServerError,
1568
2054
  TimeoutError,
1569
2055
  ToolNotFoundError,
1570
2056
  VERSION,
1571
2057
  ValidationError,
1572
2058
  createCronWorkflow,
1573
- createFileWatcherWorkflow
2059
+ createEdgeNodeSecurity,
2060
+ createFileWatcherWorkflow,
2061
+ createSecureEdgeClient
1574
2062
  });