@branta-ops/branta 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Branta JavaScript SDK
2
+
3
+ Package contains functionality to assist JavaScript projects with making requests to Branta's server.
4
+
5
+ ## Installation
6
+
7
+ Install via npm:
8
+
9
+ ```bash
10
+ npm install branta
11
+ ```
12
+
13
+ ## Quick Start
14
+ ```js
15
+ import * as branta from "branta";
16
+
17
+ const client = new branta.V2BrantaClient(
18
+ new branta.BrantaClientOptions({
19
+ baseUrl: branta.BrantaBaseServerUrl.Localhost,
20
+ defaultApiKey:
21
+ "<api-key-here>",
22
+ }),
23
+ );
24
+
25
+ var payments = await client.getPayments(
26
+ "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
27
+ );
28
+ console.log(payments);
29
+
30
+ if (payments.length == 0) {
31
+ console.log("Creating Payment...");
32
+ await client.addPayment({
33
+ description: "Testing description",
34
+ destinations: [
35
+ {
36
+ value: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
37
+ zk: false,
38
+ },
39
+ ],
40
+ ttl: "600",
41
+ });
42
+ }
43
+ ```
44
+
45
+ ## Feature Support
46
+
47
+ - [X] Per Environment configuration
48
+ - [X] V2 Get Payment by address
49
+ - [ ] V2 Get Payment by QR Code
50
+ - [X] V2 Get decrypted Zero Knowledge by address and secret
51
+ - [X] V2 Add Payment
52
+ - [ ] V2 Payment by Parent Platform with HMAC
53
+ - [X] V2 Add Zero Knowledge Payment with secret
54
+ - [X] V2 Check API key valid
package/jest.config.js ADDED
@@ -0,0 +1,4 @@
1
+ export default {
2
+ testEnvironment: 'node',
3
+ transform: {}
4
+ };
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@branta-ops/branta",
3
+ "version": "0.0.1",
4
+ "description": "A JavaScript SDK for the Branta API",
5
+ "homepage": "https://github.com/BrantaOps/branta-js#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/BrantaOps/branta-js/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/BrantaOps/branta-js.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "",
15
+ "type": "module",
16
+ "main": "src/index.js",
17
+ "scripts": {
18
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --collect-coverage",
19
+ "script": "npm link branta && node test/script.js"
20
+ },
21
+ "devDependencies": {
22
+ "jest": "^30.2.0"
23
+ }
24
+ }
@@ -0,0 +1,7 @@
1
+ const BrantaServerBaseUrl = {
2
+ Staging: { value: 0, url: "https://staging.guardrail.branta.pro" },
3
+ Production: { value: 1, url: "https://guardrail.branta.pro" },
4
+ Localhost: { value: 2, url: "http://localhost:3000" },
5
+ };
6
+
7
+ export default BrantaServerBaseUrl;
@@ -0,0 +1,10 @@
1
+ class BrantaClientOptions {
2
+ baseUrl = null;
3
+ defaultApiKey = null;
4
+
5
+ constructor(options = {}) {
6
+ Object.assign(this, options);
7
+ }
8
+ }
9
+
10
+ export default BrantaClientOptions;
@@ -0,0 +1,8 @@
1
+ class BrantaPaymentException extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "BrantaPaymentException";
5
+ }
6
+ }
7
+
8
+ export default BrantaPaymentException
@@ -0,0 +1,83 @@
1
+ class AesEncryption {
2
+ /**
3
+ * Encrypts a string value using AES-GCM with a secret key
4
+ * @param {string} value - The plaintext to encrypt
5
+ * @param {string} secret - The secret key (will be hashed with SHA-256)
6
+ * @returns {Promise<string>} Base64-encoded encrypted data (iv + ciphertext + tag)
7
+ */
8
+ static async encrypt(value, secret) {
9
+ const encoder = new TextEncoder();
10
+ const secretData = encoder.encode(secret);
11
+ const keyData = await crypto.subtle.digest('SHA-256', secretData);
12
+
13
+ const iv = crypto.getRandomValues(new Uint8Array(12));
14
+
15
+ const key = await crypto.subtle.importKey(
16
+ 'raw',
17
+ keyData,
18
+ { name: 'AES-GCM', length: 256 },
19
+ false,
20
+ ['encrypt']
21
+ );
22
+
23
+ const plaintext = encoder.encode(value);
24
+ const encrypted = await crypto.subtle.encrypt(
25
+ { name: 'AES-GCM', iv: iv, tagLength: 128 },
26
+ key,
27
+ plaintext
28
+ );
29
+
30
+ const encryptedArray = new Uint8Array(encrypted);
31
+
32
+ const ciphertext = encryptedArray.slice(0, -16);
33
+ const tag = encryptedArray.slice(-16);
34
+
35
+ const result = new Uint8Array(iv.length + ciphertext.length + tag.length);
36
+ result.set(iv, 0);
37
+ result.set(ciphertext, iv.length);
38
+ result.set(tag, iv.length + ciphertext.length);
39
+
40
+ return btoa(String.fromCharCode(...result));
41
+ }
42
+
43
+ /**
44
+ * Decrypts an encrypted string using AES-GCM with a secret key
45
+ * @param {string} encryptedValue - Base64-encoded encrypted data
46
+ * @param {string} secret - The secret key (will be hashed with SHA-256)
47
+ * @returns {Promise<string>} The decrypted plaintext
48
+ */
49
+ static async decrypt(encryptedValue, secret) {
50
+ const encryptedData = Uint8Array.from(atob(encryptedValue), c => c.charCodeAt(0));
51
+
52
+ const encoder = new TextEncoder();
53
+ const secretData = encoder.encode(secret);
54
+ const keyData = await crypto.subtle.digest('SHA-256', secretData);
55
+
56
+ const iv = encryptedData.slice(0, 12);
57
+ const tag = encryptedData.slice(-16);
58
+ const ciphertext = encryptedData.slice(12, -16);
59
+
60
+ const ciphertextWithTag = new Uint8Array(ciphertext.length + tag.length);
61
+ ciphertextWithTag.set(ciphertext, 0);
62
+ ciphertextWithTag.set(tag, ciphertext.length);
63
+
64
+ const key = await crypto.subtle.importKey(
65
+ 'raw',
66
+ keyData,
67
+ { name: 'AES-GCM', length: 256 },
68
+ false,
69
+ ['decrypt']
70
+ );
71
+
72
+ const decrypted = await crypto.subtle.decrypt(
73
+ { name: 'AES-GCM', iv: iv, tagLength: 128 },
74
+ key,
75
+ ciphertextWithTag
76
+ );
77
+
78
+ const decoder = new TextDecoder();
79
+ return decoder.decode(decrypted);
80
+ }
81
+ }
82
+
83
+ export default AesEncryption;
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import V2BrantaClient from "./v2/client.js";
2
+ import BrantaClientOptions from "./classes/brantaClientOptions.js";
3
+ import BrantaBaseServerUrl from "./classes/brantaBaseServerUrl.js";
4
+
5
+ export { V2BrantaClient, BrantaClientOptions, BrantaBaseServerUrl };
@@ -0,0 +1,125 @@
1
+ import AesEncryption from "../helpers/aes.js";
2
+ import BrantaPaymentException from "../classes/brantaPaymentException.js";
3
+
4
+ export class V2BrantaClient {
5
+ constructor(brantaClientOptions) {
6
+ this._defaultOptions = brantaClientOptions;
7
+ }
8
+
9
+ async getPayments(address, options = null) {
10
+ const httpClient = this._createClient(options);
11
+ const response = await httpClient.get(`/v2/payments/${address}`);
12
+
13
+ if (!response.ok || response.headers.get("content-length") === "0") {
14
+ return [];
15
+ }
16
+
17
+ const data = await response.json();
18
+ return data;
19
+ }
20
+
21
+ async getZKPayment(address, secret, options = null) {
22
+ const payments = await this.getPayments(address, options);
23
+
24
+ for (const payment of payments) {
25
+ for (const destination of payment.destinations) {
26
+ if (destination.isZk === false) continue;
27
+ destination.value = await AesEncryption.decrypt(destination.value, secret);
28
+ }
29
+ }
30
+
31
+ return payments;
32
+ }
33
+
34
+ async addPayment(payment, options = null) {
35
+ const httpClient = this._createClient(options);
36
+ this._setApiKey(httpClient, options);
37
+
38
+ const response = await httpClient.post("/v2/payments", payment);
39
+
40
+ if (!response.ok) {
41
+ throw new BrantaPaymentException(response.status.toString());
42
+ }
43
+
44
+ const responseBody = await response.text();
45
+ return JSON.parse(responseBody);
46
+ }
47
+
48
+ async addZKPayment(payment, options = null) {
49
+ const secret = crypto.randomUUID();
50
+
51
+ for (const destination of payment.destinations) {
52
+ if (destination.isZk === false) continue;
53
+ destination.value = await AesEncryption.encrypt(destination.value, secret);
54
+ }
55
+
56
+ const responsePayment = await this.addPayment(payment, options);
57
+ return { payment: responsePayment, secret };
58
+ }
59
+
60
+ async isApiKeyValid(options = null) {
61
+ const httpClient = this._createClient(options);
62
+ this._setApiKey(httpClient, options);
63
+
64
+ const response = await fetch(
65
+ `${httpClient.baseURL}/v2/api-keys/health-check`,
66
+ {
67
+ headers: httpClient.headers,
68
+ },
69
+ );
70
+
71
+ return response.ok;
72
+ }
73
+
74
+ _createClient(options) {
75
+ const baseUrl = options?.baseUrl ?? this._defaultOptions?.baseUrl;
76
+
77
+ if (!baseUrl?.url) {
78
+ throw new Error("Branta: BaseUrl is a required option.");
79
+ }
80
+
81
+ const fullBaseUrl = baseUrl.url;
82
+
83
+ return {
84
+ baseURL: fullBaseUrl,
85
+ headers: {},
86
+ async get(url, config = {}) {
87
+ const response = await fetch(`${this.baseURL}${url}`, {
88
+ method: "GET",
89
+ headers: { ...this.headers, ...config?.headers },
90
+ signal: config?.signal,
91
+ });
92
+ return response;
93
+ },
94
+ async post(url, data, config = {}) {
95
+ const response = await fetch(`${this.baseURL}${url}`, {
96
+ method: "POST",
97
+ headers: {
98
+ "Content-Type": "application/json",
99
+ ...this.headers,
100
+ ...config?.headers,
101
+ },
102
+ body: JSON.stringify(data),
103
+ signal: config?.signal,
104
+ });
105
+ return response;
106
+ },
107
+ };
108
+ }
109
+
110
+ _setApiKey(httpClient, options) {
111
+ const apiKey =
112
+ options?.defaultApiKey ?? this._defaultOptions?.defaultApiKey;
113
+
114
+ if (!apiKey) {
115
+ throw new BrantaPaymentException("Unauthorized");
116
+ }
117
+
118
+ httpClient.headers = {
119
+ ...httpClient.headers,
120
+ Authorization: `Bearer ${apiKey}`,
121
+ };
122
+ }
123
+ }
124
+
125
+ export default V2BrantaClient;
@@ -0,0 +1,35 @@
1
+ import { describe, test, expect } from "@jest/globals";
2
+ import AesEncryption from "./../../src/helpers/aes";
3
+
4
+ describe("AesEncryption", () => {
5
+ test("should encrypt and decrypt a bitcoin address", async () => {
6
+ const plaintext = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa";
7
+ const secret = "mySecret123";
8
+
9
+ const encrypted = await AesEncryption.encrypt(plaintext, secret);
10
+ const decrypted = await AesEncryption.decrypt(encrypted, secret);
11
+
12
+ expect(decrypted).toBe(plaintext);
13
+ });
14
+
15
+ test("should produce different ciphertext with different secrets", async () => {
16
+ const plaintext = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa";
17
+ const secret1 = "secret1";
18
+ const secret2 = "secret2";
19
+
20
+ const encrypted1 = await AesEncryption.encrypt(plaintext, secret1);
21
+ const encrypted2 = await AesEncryption.encrypt(plaintext, secret2);
22
+
23
+ expect(encrypted1).not.toBe(encrypted2);
24
+ });
25
+
26
+ test("should decrypt text", async () => {
27
+ const encrypted =
28
+ "pQerSFV+fievHP+guYoGJjx1CzFFrYWHAgWrLhn5473Z19M6+WMScLd1hsk808AEF/x+GpZKmNacFBf5BbQ=";
29
+ const secret1 = "1234";
30
+
31
+ const decrypted = await AesEncryption.decrypt(encrypted, secret1);
32
+
33
+ expect(decrypted).toBe("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
34
+ });
35
+ });
package/test/script.js ADDED
@@ -0,0 +1,28 @@
1
+ import * as branta from "branta";
2
+
3
+ const client = new branta.V2BrantaClient(
4
+ new branta.BrantaClientOptions({
5
+ baseUrl: branta.BrantaBaseServerUrl.Localhost,
6
+ defaultApiKey:
7
+ "<api-key-here>",
8
+ }),
9
+ );
10
+
11
+ var payments = await client.getPayments(
12
+ "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
13
+ );
14
+ console.log(payments);
15
+
16
+ if (payments.length == 0) {
17
+ console.log("Creating Payment...");
18
+ await client.addPayment({
19
+ description: "Testing description",
20
+ destinations: [
21
+ {
22
+ value: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
23
+ zk: false,
24
+ },
25
+ ],
26
+ ttl: "600",
27
+ });
28
+ }
@@ -0,0 +1,310 @@
1
+ import { describe, test, expect, jest, beforeEach } from "@jest/globals";
2
+ import V2BrantaClient from "../../src/v2/client.js";
3
+ import BrantaPaymentException from "../../src/classes/brantaPaymentException.js";
4
+ import AesEncryption from "../../src/helpers/aes.js";
5
+
6
+ describe("V2BrantaClient", () => {
7
+ let client;
8
+ let mockFetch;
9
+ const defaultOptions = {
10
+ baseUrl: { url: "http://localhost:3000" },
11
+ defaultApiKey: "test-api-key",
12
+ };
13
+
14
+ const testPayments = [
15
+ {
16
+ destinations: [
17
+ {
18
+ value: "123",
19
+ isZk: false,
20
+ },
21
+ ],
22
+ },
23
+ {
24
+ destinations: [
25
+ {
26
+ value: "456",
27
+ isZk: false,
28
+ },
29
+ ],
30
+ },
31
+ ];
32
+
33
+ beforeEach(() => {
34
+ client = new V2BrantaClient(defaultOptions);
35
+ mockFetch = jest.fn();
36
+ global.fetch = mockFetch;
37
+ jest.clearAllMocks();
38
+ });
39
+
40
+ describe("getPayments", () => {
41
+ test("should return payments", async () => {
42
+ const address = "test-address";
43
+ mockFetch.mockResolvedValue({
44
+ ok: true,
45
+ headers: {
46
+ get: () => "100",
47
+ },
48
+ json: async () => testPayments,
49
+ });
50
+
51
+ const result = await client.getPayments(address);
52
+
53
+ expect(result).not.toBeNull();
54
+ expect(result).toHaveLength(2);
55
+ expect(result[0].destinations[0].value).toBe("123");
56
+ expect(result[1].destinations[0].value).toBe("456");
57
+ });
58
+
59
+ test("should return empty list on non-success status code", async () => {
60
+ const address = "test-address";
61
+ mockFetch.mockResolvedValue({
62
+ ok: false,
63
+ headers: {
64
+ get: () => "0",
65
+ },
66
+ });
67
+
68
+ const result = await client.getPayments(address);
69
+
70
+ expect(result).not.toBeNull();
71
+ expect(result).toHaveLength(0);
72
+ });
73
+
74
+ test("should return empty list on null content", async () => {
75
+ const address = "test-address";
76
+ mockFetch.mockResolvedValue({
77
+ ok: true,
78
+ headers: {
79
+ get: () => "0",
80
+ },
81
+ });
82
+
83
+ const result = await client.getPayments(address);
84
+
85
+ expect(result).not.toBeNull();
86
+ expect(result).toHaveLength(0);
87
+ });
88
+
89
+ test("should use custom options", async () => {
90
+ const address = "test-address";
91
+ const customOptions = {
92
+ baseUrl: { url: "https://production.example.com" },
93
+ };
94
+
95
+ mockFetch.mockResolvedValue({
96
+ ok: true,
97
+ headers: {
98
+ get: () => "2",
99
+ },
100
+ json: async () => [],
101
+ });
102
+
103
+ await client.getPayments(address, customOptions);
104
+
105
+ expect(mockFetch).toHaveBeenCalledWith(
106
+ "https://production.example.com/v2/payments/test-address",
107
+ expect.any(Object),
108
+ );
109
+ });
110
+
111
+ test("should throw exception if baseUrl not set", async () => {
112
+ client = new V2BrantaClient({});
113
+
114
+ await expect(client.getPayments("test-address")).rejects.toThrow(
115
+ "Branta: BaseUrl is a required option.",
116
+ );
117
+ });
118
+ });
119
+
120
+ describe("getZKPayment", () => {
121
+ test("should decrypt ZK destination values", async () => {
122
+ const encryptedValue =
123
+ "pQerSFV+fievHP+guYoGJjx1CzFFrYWHAgWrLhn5473Z19M6+WMScLd1hsk808AEF/x+GpZKmNacFBf5BbQ=";
124
+ const payments = [
125
+ {
126
+ destinations: [
127
+ { isZk: true, value: encryptedValue },
128
+ { isZk: false, value: "plain-value" },
129
+ ],
130
+ },
131
+ ];
132
+
133
+ mockFetch.mockResolvedValue({
134
+ ok: true,
135
+ headers: {
136
+ get: () => "100",
137
+ },
138
+ json: async () => JSON.parse(JSON.stringify(payments)),
139
+ });
140
+
141
+ const result = await client.getZKPayment(encryptedValue, "1234");
142
+
143
+ expect(result).not.toBeNull();
144
+ expect(result).toHaveLength(1);
145
+ expect(result[0].destinations[0].value).toBe(
146
+ "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
147
+ );
148
+ });
149
+
150
+ test("should return unmodified payments with no ZK destinations", async () => {
151
+ const payments = [
152
+ {
153
+ destinations: [{ isZk: false, value: "plain-value" }],
154
+ },
155
+ ];
156
+
157
+ mockFetch.mockResolvedValue({
158
+ ok: true,
159
+ headers: {
160
+ get: () => "100",
161
+ },
162
+ json: async () => JSON.parse(JSON.stringify(payments)),
163
+ });
164
+
165
+ const result = await client.getZKPayment("plain-value", "test-secret");
166
+
167
+ expect(result).not.toBeNull();
168
+ expect(result).toHaveLength(1);
169
+ expect(result[0].destinations[0].value).toBe("plain-value");
170
+ });
171
+ });
172
+
173
+ describe("addPayment", () => {
174
+ test("should throw exception when no API key is provided", async () => {
175
+ const payment = testPayments[0];
176
+ const clientWithoutApiKey = new V2BrantaClient({
177
+ baseUrl: { url: "https://production.example.com" },
178
+ defaultApiKey: null,
179
+ });
180
+
181
+ await expect(clientWithoutApiKey.addPayment(payment)).rejects.toThrow(
182
+ BrantaPaymentException,
183
+ );
184
+ });
185
+
186
+ test("should use custom API key", async () => {
187
+ const payment = testPayments[0];
188
+ const customOptions = {
189
+ baseUrl: { url: "https://production.example.com" },
190
+ defaultApiKey: "custom-api-key",
191
+ };
192
+
193
+ mockFetch.mockResolvedValue({
194
+ ok: true,
195
+ text: async () => JSON.stringify(testPayments[0]),
196
+ });
197
+
198
+ await client.addPayment(payment, customOptions);
199
+
200
+ expect(mockFetch).toHaveBeenCalledWith(
201
+ "https://production.example.com/v2/payments",
202
+ expect.objectContaining({
203
+ headers: expect.objectContaining({
204
+ Authorization: "Bearer custom-api-key",
205
+ }),
206
+ }),
207
+ );
208
+ });
209
+
210
+ test("should throw exception on failed response", async () => {
211
+ const payment = testPayments[0];
212
+
213
+ mockFetch.mockResolvedValue({
214
+ ok: false,
215
+ status: 400,
216
+ });
217
+
218
+ await expect(client.addPayment(payment)).rejects.toThrow(
219
+ BrantaPaymentException,
220
+ );
221
+ });
222
+
223
+ test("should return parsed response on success", async () => {
224
+ const payment = testPayments[0];
225
+ const expectedResponse = { ...payment, id: "12345" };
226
+
227
+ mockFetch.mockResolvedValue({
228
+ ok: true,
229
+ text: async () => JSON.stringify(expectedResponse),
230
+ });
231
+
232
+ const result = await client.addPayment(payment);
233
+
234
+ expect(result).toEqual(expectedResponse);
235
+ });
236
+ });
237
+
238
+ describe("addZKPayment", () => {
239
+ test("should encrypt ZK destinations and return payment with secret", async () => {
240
+ const plainText = "plain-value";
241
+ const payment = {
242
+ destinations: [
243
+ { isZk: true, value: plainText },
244
+ { isZk: false, value: "other-value" },
245
+ ],
246
+ };
247
+
248
+ mockFetch.mockResolvedValue({
249
+ ok: true,
250
+ text: async () => JSON.stringify(payment),
251
+ });
252
+
253
+ const result = await client.addZKPayment(payment);
254
+
255
+ const zkPayment = result.payment.destinations.find(
256
+ (d) => d.isZk == true,
257
+ ).value;
258
+
259
+ expect(await AesEncryption.decrypt(zkPayment, result.secret)).toBe(
260
+ plainText,
261
+ );
262
+
263
+ expect(result.payment).toBeDefined();
264
+ });
265
+ });
266
+
267
+ describe("isApiKeyValid", () => {
268
+ test("should return true for valid API key", async () => {
269
+ mockFetch.mockResolvedValue({
270
+ ok: true,
271
+ });
272
+
273
+ const result = await client.isApiKeyValid();
274
+
275
+ expect(result).toBe(true);
276
+ });
277
+
278
+ test("should return false for invalid API key", async () => {
279
+ mockFetch.mockResolvedValue({
280
+ ok: false,
281
+ });
282
+
283
+ const result = await client.isApiKeyValid();
284
+
285
+ expect(result).toBe(false);
286
+ });
287
+
288
+ test("should use custom options", async () => {
289
+ const customOptions = {
290
+ baseUrl: { url: "https://production.example.com" },
291
+ defaultApiKey: "custom-key",
292
+ };
293
+
294
+ mockFetch.mockResolvedValue({
295
+ ok: true,
296
+ });
297
+
298
+ await client.isApiKeyValid(customOptions);
299
+
300
+ expect(mockFetch).toHaveBeenCalledWith(
301
+ "https://production.example.com/v2/api-keys/health-check",
302
+ expect.objectContaining({
303
+ headers: expect.objectContaining({
304
+ Authorization: "Bearer custom-key",
305
+ }),
306
+ }),
307
+ );
308
+ });
309
+ });
310
+ });