n8n-nodes-dominusnode 1.0.0

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 (42) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +99 -0
  4. package/dist/credentials/DominusNodeApi.credentials.d.ts +7 -0
  5. package/dist/credentials/DominusNodeApi.credentials.js +42 -0
  6. package/dist/credentials/DominusNodeApi.credentials.js.map +1 -0
  7. package/dist/index.d.ts +4 -0
  8. package/dist/index.js +12 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/nodes/DominusNodeProxy/DominusNodeProxy.node.d.ts +24 -0
  11. package/dist/nodes/DominusNodeProxy/DominusNodeProxy.node.js +436 -0
  12. package/dist/nodes/DominusNodeProxy/DominusNodeProxy.node.js.map +1 -0
  13. package/dist/nodes/DominusNodeUsage/DominusNodeUsage.node.d.ts +13 -0
  14. package/dist/nodes/DominusNodeUsage/DominusNodeUsage.node.js +105 -0
  15. package/dist/nodes/DominusNodeUsage/DominusNodeUsage.node.js.map +1 -0
  16. package/dist/nodes/DominusNodeWallet/DominusNodeWallet.node.d.ts +33 -0
  17. package/dist/nodes/DominusNodeWallet/DominusNodeWallet.node.js +656 -0
  18. package/dist/nodes/DominusNodeWallet/DominusNodeWallet.node.js.map +1 -0
  19. package/dist/shared/auth.d.ts +74 -0
  20. package/dist/shared/auth.js +264 -0
  21. package/dist/shared/auth.js.map +1 -0
  22. package/dist/shared/constants.d.ts +9 -0
  23. package/dist/shared/constants.js +13 -0
  24. package/dist/shared/constants.js.map +1 -0
  25. package/dist/shared/ssrf.d.ts +42 -0
  26. package/dist/shared/ssrf.js +252 -0
  27. package/dist/shared/ssrf.js.map +1 -0
  28. package/package.json +41 -0
  29. package/src/credentials/DominusNodeApi.credentials.ts +39 -0
  30. package/src/index.ts +4 -0
  31. package/src/nodes/DominusNodeProxy/DominusNodeProxy.node.ts +459 -0
  32. package/src/nodes/DominusNodeUsage/DominusNodeUsage.node.ts +130 -0
  33. package/src/nodes/DominusNodeWallet/DominusNodeWallet.node.ts +898 -0
  34. package/src/shared/auth.ts +272 -0
  35. package/src/shared/constants.ts +11 -0
  36. package/src/shared/ssrf.ts +257 -0
  37. package/tests/DominusNodeProxy.test.ts +281 -0
  38. package/tests/DominusNodeUsage.test.ts +250 -0
  39. package/tests/DominusNodeWallet.test.ts +591 -0
  40. package/tests/ssrf.test.ts +238 -0
  41. package/tsconfig.json +18 -0
  42. package/vitest.config.ts +8 -0
@@ -0,0 +1,281 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mock n8n-workflow before importing the node
5
+ // ---------------------------------------------------------------------------
6
+
7
+ vi.mock("n8n-workflow", () => ({
8
+ NodeOperationError: class NodeOperationError extends Error {
9
+ constructor(node: unknown, message: string, extra?: unknown) {
10
+ super(message);
11
+ this.name = "NodeOperationError";
12
+ }
13
+ },
14
+ }));
15
+
16
+ // Mock dns/promises for DNS rebinding tests
17
+ vi.mock("dns/promises", () => ({
18
+ default: {
19
+ resolve4: vi.fn().mockResolvedValue(["93.184.216.34"]),
20
+ resolve6: vi.fn().mockRejectedValue(new Error("no AAAA")),
21
+ },
22
+ }));
23
+
24
+ import { DominusNodeProxy } from "../src/nodes/DominusNodeProxy/DominusNodeProxy.node";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Helpers — mock IExecuteFunctions
28
+ // ---------------------------------------------------------------------------
29
+
30
+ function createMockExecuteFunctions(overrides: {
31
+ operation: string;
32
+ params?: Record<string, unknown>;
33
+ credentials?: Record<string, unknown>;
34
+ continueOnFail?: boolean;
35
+ }) {
36
+ const params: Record<string, unknown> = {
37
+ operation: overrides.operation,
38
+ method: "GET",
39
+ proxyType: "dc",
40
+ country: "",
41
+ headers: {},
42
+ ...overrides.params,
43
+ };
44
+
45
+ const creds = {
46
+ apiKey: "dn_test_abc123",
47
+ baseUrl: "https://api.dominusnode.com",
48
+ proxyHost: "proxy.dominusnode.com",
49
+ proxyPort: 8080,
50
+ ...overrides.credentials,
51
+ };
52
+
53
+ return {
54
+ getInputData: () => [{ json: {} }],
55
+ getNodeParameter: (name: string, _index: number, fallback?: unknown) => {
56
+ return params[name] ?? fallback;
57
+ },
58
+ getCredentials: vi.fn().mockResolvedValue(creds),
59
+ getNode: () => ({ name: "DomiNode Proxy" }),
60
+ continueOnFail: () => overrides.continueOnFail ?? false,
61
+ };
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Tests
66
+ // ---------------------------------------------------------------------------
67
+
68
+ describe("DominusNodeProxy", () => {
69
+ const node = new DominusNodeProxy();
70
+
71
+ it("has correct description metadata", () => {
72
+ expect(node.description.name).toBe("dominusNodeProxy");
73
+ expect(node.description.displayName).toBe("DomiNode Proxy");
74
+ expect(node.description.credentials).toEqual([
75
+ { name: "dominusNodeApi", required: true },
76
+ ]);
77
+ });
78
+
79
+ it("has three operations", () => {
80
+ const opProp = node.description.properties.find((p) => p.name === "operation");
81
+ expect(opProp).toBeDefined();
82
+ expect((opProp as any).options).toHaveLength(3);
83
+ });
84
+ });
85
+
86
+ describe("DominusNodeProxy.execute - proxied fetch validation", () => {
87
+ let node: DominusNodeProxy;
88
+ let originalFetch: typeof globalThis.fetch;
89
+
90
+ beforeEach(() => {
91
+ node = new DominusNodeProxy();
92
+ originalFetch = globalThis.fetch;
93
+ });
94
+
95
+ afterEach(() => {
96
+ globalThis.fetch = originalFetch;
97
+ });
98
+
99
+ it("rejects empty URL", async () => {
100
+ const mockFns = createMockExecuteFunctions({
101
+ operation: "proxiedFetch",
102
+ params: { url: "" },
103
+ });
104
+
105
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/URL is required/);
106
+ });
107
+
108
+ it("rejects localhost URL (SSRF)", async () => {
109
+ const mockFns = createMockExecuteFunctions({
110
+ operation: "proxiedFetch",
111
+ params: { url: "http://localhost/admin" },
112
+ });
113
+
114
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/localhost/);
115
+ });
116
+
117
+ it("rejects private IP (192.168.x.x)", async () => {
118
+ const mockFns = createMockExecuteFunctions({
119
+ operation: "proxiedFetch",
120
+ params: { url: "http://192.168.1.1/admin" },
121
+ });
122
+
123
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/private/i);
124
+ });
125
+
126
+ it("rejects cloud metadata endpoint (169.254.169.254)", async () => {
127
+ const mockFns = createMockExecuteFunctions({
128
+ operation: "proxiedFetch",
129
+ params: { url: "http://169.254.169.254/latest/meta-data/" },
130
+ });
131
+
132
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/blocked/i);
133
+ });
134
+
135
+ it("rejects hex-encoded loopback (0x7f000001)", async () => {
136
+ const mockFns = createMockExecuteFunctions({
137
+ operation: "proxiedFetch",
138
+ params: { url: "http://0x7f000001/" },
139
+ });
140
+
141
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/private/i);
142
+ });
143
+
144
+ it("rejects decimal-encoded loopback (2130706433)", async () => {
145
+ const mockFns = createMockExecuteFunctions({
146
+ operation: "proxiedFetch",
147
+ params: { url: "http://2130706433/" },
148
+ });
149
+
150
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/private/i);
151
+ });
152
+
153
+ it("rejects .localhost TLD", async () => {
154
+ const mockFns = createMockExecuteFunctions({
155
+ operation: "proxiedFetch",
156
+ params: { url: "http://evil.localhost/" },
157
+ });
158
+
159
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/localhost/);
160
+ });
161
+
162
+ it("rejects .internal hostname", async () => {
163
+ const mockFns = createMockExecuteFunctions({
164
+ operation: "proxiedFetch",
165
+ params: { url: "http://db.internal/" },
166
+ });
167
+
168
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/internal/);
169
+ });
170
+
171
+ it("rejects embedded credentials in URL", async () => {
172
+ const mockFns = createMockExecuteFunctions({
173
+ operation: "proxiedFetch",
174
+ params: { url: "http://user:pass@example.com/" },
175
+ });
176
+
177
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/credentials/);
178
+ });
179
+
180
+ it("rejects file:// protocol", async () => {
181
+ const mockFns = createMockExecuteFunctions({
182
+ operation: "proxiedFetch",
183
+ params: { url: "file:///etc/passwd" },
184
+ });
185
+
186
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/protocols/);
187
+ });
188
+
189
+ it("rejects OFAC sanctioned country (IR)", async () => {
190
+ const mockFns = createMockExecuteFunctions({
191
+ operation: "proxiedFetch",
192
+ params: { url: "https://example.com", country: "IR" },
193
+ });
194
+
195
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/OFAC/);
196
+ });
197
+
198
+ it("rejects OFAC sanctioned country (KP)", async () => {
199
+ const mockFns = createMockExecuteFunctions({
200
+ operation: "proxiedFetch",
201
+ params: { url: "https://example.com", country: "KP" },
202
+ });
203
+
204
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/OFAC/);
205
+ });
206
+
207
+ it("rejects OFAC sanctioned country (RU)", async () => {
208
+ const mockFns = createMockExecuteFunctions({
209
+ operation: "proxiedFetch",
210
+ params: { url: "https://example.com", country: "RU" },
211
+ });
212
+
213
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/OFAC/);
214
+ });
215
+
216
+ it("sanitizes credentials in error messages with continueOnFail", async () => {
217
+ const mockFns = createMockExecuteFunctions({
218
+ operation: "proxiedFetch",
219
+ params: { url: "" },
220
+ continueOnFail: true,
221
+ });
222
+
223
+ const result = await node.execute.call(mockFns as any);
224
+ // Should not throw, but return error in json
225
+ expect(result[0][0].json).toHaveProperty("error");
226
+ // Make sure no raw API key appears
227
+ const errorMsg = result[0][0].json.error as string;
228
+ expect(errorMsg).not.toContain("dn_test_abc123");
229
+ });
230
+
231
+ it("rejects missing API key", async () => {
232
+ const mockFns = createMockExecuteFunctions({
233
+ operation: "proxiedFetch",
234
+ params: { url: "https://example.com" },
235
+ credentials: { apiKey: "" },
236
+ });
237
+
238
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/API Key is required/);
239
+ });
240
+ });
241
+
242
+ describe("DominusNodeProxy.execute - DNS rebinding protection", () => {
243
+ let node: DominusNodeProxy;
244
+ let originalFetch: typeof globalThis.fetch;
245
+
246
+ beforeEach(() => {
247
+ node = new DominusNodeProxy();
248
+ originalFetch = globalThis.fetch;
249
+ });
250
+
251
+ afterEach(() => {
252
+ globalThis.fetch = originalFetch;
253
+ vi.restoreAllMocks();
254
+ });
255
+
256
+ it("blocks hostname resolving to private IP", async () => {
257
+ // Override the dns mock for this test
258
+ const dnsModule = await import("dns/promises");
259
+ vi.mocked(dnsModule.default.resolve4).mockResolvedValueOnce(["127.0.0.1"]);
260
+
261
+ const mockFns = createMockExecuteFunctions({
262
+ operation: "proxiedFetch",
263
+ params: { url: "https://evil-rebind.example.com/" },
264
+ });
265
+
266
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/private IP/);
267
+ });
268
+
269
+ it("blocks hostname resolving to private IPv6", async () => {
270
+ const dnsModule = await import("dns/promises");
271
+ vi.mocked(dnsModule.default.resolve4).mockResolvedValueOnce(["93.184.216.34"]);
272
+ vi.mocked(dnsModule.default.resolve6).mockResolvedValueOnce(["::1"]);
273
+
274
+ const mockFns = createMockExecuteFunctions({
275
+ operation: "proxiedFetch",
276
+ params: { url: "https://evil-rebind6.example.com/" },
277
+ });
278
+
279
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/private IPv6/);
280
+ });
281
+ });
@@ -0,0 +1,250 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mock n8n-workflow
5
+ // ---------------------------------------------------------------------------
6
+
7
+ vi.mock("n8n-workflow", () => ({
8
+ NodeOperationError: class NodeOperationError extends Error {
9
+ constructor(node: unknown, message: string, extra?: unknown) {
10
+ super(message);
11
+ this.name = "NodeOperationError";
12
+ }
13
+ },
14
+ }));
15
+
16
+ import { DominusNodeUsage } from "../src/nodes/DominusNodeUsage/DominusNodeUsage.node";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers — mock IExecuteFunctions
20
+ // ---------------------------------------------------------------------------
21
+
22
+ function createMockExecuteFunctions(overrides: {
23
+ operation: string;
24
+ params?: Record<string, unknown>;
25
+ credentials?: Record<string, unknown>;
26
+ continueOnFail?: boolean;
27
+ }) {
28
+ const params: Record<string, unknown> = {
29
+ operation: overrides.operation,
30
+ ...overrides.params,
31
+ };
32
+
33
+ const creds = {
34
+ apiKey: "dn_test_abc123",
35
+ baseUrl: "http://localhost:3000",
36
+ ...overrides.credentials,
37
+ };
38
+
39
+ return {
40
+ getInputData: () => [{ json: {} }],
41
+ getNodeParameter: (name: string, _index: number, fallback?: unknown) => {
42
+ return params[name] ?? fallback;
43
+ },
44
+ getCredentials: vi.fn().mockResolvedValue(creds),
45
+ getNode: () => ({ name: "DomiNode Usage" }),
46
+ continueOnFail: () => overrides.continueOnFail ?? false,
47
+ };
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Mocked fetch for API calls
52
+ // ---------------------------------------------------------------------------
53
+
54
+ let originalFetch: typeof globalThis.fetch;
55
+
56
+ beforeEach(() => {
57
+ originalFetch = globalThis.fetch;
58
+ globalThis.fetch = vi.fn().mockImplementation((url: string) => {
59
+ if (typeof url === "string" && url.includes("/api/auth/verify-key")) {
60
+ return Promise.resolve({
61
+ ok: true,
62
+ status: 200,
63
+ text: () => Promise.resolve('{"token": "jwt-mock-token"}'),
64
+ headers: new Headers({ "content-length": "30" }),
65
+ });
66
+ }
67
+ // Default: mock API call success with usage data
68
+ return Promise.resolve({
69
+ ok: true,
70
+ status: 200,
71
+ text: () =>
72
+ Promise.resolve(
73
+ '{"totalBytes": 1073741824, "totalCostCents": 300, "records": []}',
74
+ ),
75
+ headers: new Headers({ "content-length": "60" }),
76
+ });
77
+ });
78
+ });
79
+
80
+ afterEach(() => {
81
+ globalThis.fetch = originalFetch;
82
+ });
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Tests
86
+ // ---------------------------------------------------------------------------
87
+
88
+ describe("DominusNodeUsage - metadata", () => {
89
+ it("has correct description metadata", () => {
90
+ const node = new DominusNodeUsage();
91
+ expect(node.description.name).toBe("dominusNodeUsage");
92
+ expect(node.description.displayName).toBe("DomiNode Usage");
93
+ expect(node.description.credentials).toEqual([
94
+ { name: "dominusNodeApi", required: true },
95
+ ]);
96
+ });
97
+
98
+ it("has one operation", () => {
99
+ const node = new DominusNodeUsage();
100
+ const opProp = node.description.properties.find((p) => p.name === "operation");
101
+ expect(opProp).toBeDefined();
102
+ expect((opProp as any).options).toHaveLength(1);
103
+ });
104
+
105
+ it("has period options: day, week, month", () => {
106
+ const node = new DominusNodeUsage();
107
+ const periodProp = node.description.properties.find((p) => p.name === "period");
108
+ expect(periodProp).toBeDefined();
109
+ const values = (periodProp as any).options.map((o: any) => o.value);
110
+ expect(values).toEqual(["day", "week", "month"]);
111
+ });
112
+ });
113
+
114
+ describe("DominusNodeUsage.execute - checkUsage", () => {
115
+ it("returns usage data for month period", async () => {
116
+ const node = new DominusNodeUsage();
117
+ const mockFns = createMockExecuteFunctions({
118
+ operation: "checkUsage",
119
+ params: { period: "month" },
120
+ });
121
+
122
+ const result = await node.execute.call(mockFns as any);
123
+ expect(result[0]).toHaveLength(1);
124
+ expect(result[0][0].json).toHaveProperty("totalBytes", 1073741824);
125
+ expect(result[0][0].json).toHaveProperty("totalCostCents", 300);
126
+ });
127
+
128
+ it("returns usage data for day period", async () => {
129
+ const node = new DominusNodeUsage();
130
+ const mockFns = createMockExecuteFunctions({
131
+ operation: "checkUsage",
132
+ params: { period: "day" },
133
+ });
134
+
135
+ const result = await node.execute.call(mockFns as any);
136
+ expect(result[0]).toHaveLength(1);
137
+ expect(result[0][0].json).toHaveProperty("totalBytes");
138
+ });
139
+
140
+ it("returns usage data for week period", async () => {
141
+ const node = new DominusNodeUsage();
142
+ const mockFns = createMockExecuteFunctions({
143
+ operation: "checkUsage",
144
+ params: { period: "week" },
145
+ });
146
+
147
+ const result = await node.execute.call(mockFns as any);
148
+ expect(result[0]).toHaveLength(1);
149
+ expect(result[0][0].json).toHaveProperty("totalBytes");
150
+ });
151
+
152
+ it("passes since/until ISO dates (not days integer)", async () => {
153
+ const node = new DominusNodeUsage();
154
+ const mockFns = createMockExecuteFunctions({
155
+ operation: "checkUsage",
156
+ params: { period: "week" },
157
+ });
158
+
159
+ await node.execute.call(mockFns as any);
160
+
161
+ // Verify that the fetch call included since/until params
162
+ const fetchCalls = (globalThis.fetch as any).mock.calls;
163
+ const usageCall = fetchCalls.find(
164
+ (call: any) => typeof call[0] === "string" && call[0].includes("/api/usage"),
165
+ );
166
+ expect(usageCall).toBeDefined();
167
+ const url = usageCall[0] as string;
168
+ expect(url).toContain("since=");
169
+ expect(url).toContain("until=");
170
+ // Verify ISO date format
171
+ const params = new URLSearchParams(url.split("?")[1]);
172
+ const since = params.get("since");
173
+ expect(since).toMatch(/^\d{4}-\d{2}-\d{2}T/);
174
+ });
175
+
176
+ it("rejects missing API key", async () => {
177
+ const node = new DominusNodeUsage();
178
+ const mockFns = createMockExecuteFunctions({
179
+ operation: "checkUsage",
180
+ params: { period: "month" },
181
+ credentials: { apiKey: "" },
182
+ });
183
+
184
+ await expect(node.execute.call(mockFns as any)).rejects.toThrow(/API Key is required/);
185
+ });
186
+
187
+ it("returns error in json when continueOnFail is true", async () => {
188
+ // Make the API call fail
189
+ globalThis.fetch = vi.fn().mockImplementation((url: string) => {
190
+ if (typeof url === "string" && url.includes("/api/auth/verify-key")) {
191
+ return Promise.resolve({
192
+ ok: false,
193
+ status: 401,
194
+ text: () => Promise.resolve('{"error": "Invalid API key dn_live_secret123"}'),
195
+ headers: new Headers({ "content-length": "50" }),
196
+ });
197
+ }
198
+ return Promise.resolve({
199
+ ok: false,
200
+ status: 500,
201
+ text: () => Promise.resolve('{"error": "Internal error"}'),
202
+ headers: new Headers({ "content-length": "30" }),
203
+ });
204
+ });
205
+
206
+ const node = new DominusNodeUsage();
207
+ const mockFns = createMockExecuteFunctions({
208
+ operation: "checkUsage",
209
+ params: { period: "month" },
210
+ continueOnFail: true,
211
+ });
212
+
213
+ const result = await node.execute.call(mockFns as any);
214
+ expect(result[0][0].json).toHaveProperty("error");
215
+ // Ensure credentials are sanitized
216
+ const errorMsg = result[0][0].json.error as string;
217
+ expect(errorMsg).not.toContain("dn_live_secret123");
218
+ });
219
+
220
+ it("handles API error with credential sanitization", async () => {
221
+ globalThis.fetch = vi.fn().mockImplementation((url: string) => {
222
+ if (typeof url === "string" && url.includes("/api/auth/verify-key")) {
223
+ return Promise.resolve({
224
+ ok: true,
225
+ status: 200,
226
+ text: () => Promise.resolve('{"token": "jwt-mock-token"}'),
227
+ headers: new Headers({ "content-length": "30" }),
228
+ });
229
+ }
230
+ return Promise.resolve({
231
+ ok: false,
232
+ status: 500,
233
+ text: () => Promise.resolve('{"error": "dn_live_abc123 unauthorized"}'),
234
+ headers: new Headers({ "content-length": "50" }),
235
+ });
236
+ });
237
+
238
+ const node = new DominusNodeUsage();
239
+ const mockFns = createMockExecuteFunctions({
240
+ operation: "checkUsage",
241
+ params: { period: "month" },
242
+ continueOnFail: true,
243
+ });
244
+
245
+ const result = await node.execute.call(mockFns as any);
246
+ const errorMsg = result[0][0].json.error as string;
247
+ expect(errorMsg).not.toContain("dn_live_abc123");
248
+ expect(errorMsg).toContain("***");
249
+ });
250
+ });