@notabene/javascript-sdk 2.9.0-next.1 → 2.9.0-next.3

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.
@@ -1,7 +1,12 @@
1
1
  import fc from 'fast-check';
2
2
  import { beforeEach, describe, expect, it, vi } from 'vitest';
3
- import type { DID } from '../../types';
4
- import { ConnectionManager, TransactionType } from '../connections';
3
+ import type { DID, RefreshSource } from '../../types';
4
+ import {
5
+ ConnectionManager,
6
+ ConnectionStatus,
7
+ getRefreshResult,
8
+ TransactionType,
9
+ } from '../connections';
5
10
  import { seal } from '../encryption';
6
11
 
7
12
  // Mock fetch globally
@@ -13,30 +18,32 @@ const arbDID = fc
13
18
  .tuple(fc.string(), fc.string())
14
19
  .map(([method, id]) => `did:${method}:${id}` as DID);
15
20
 
21
+ const arbTx = fc.record({
22
+ requestId: fc.option(fc.uuid(), { nil: undefined }),
23
+ customer: fc.option(
24
+ fc.record({
25
+ name: fc.string(),
26
+ email: fc.option(fc.string(), { nil: undefined }),
27
+ phone: fc.option(fc.string(), { nil: undefined }),
28
+ type: fc.constant(undefined), // Optional PersonType
29
+ accountNumber: fc.option(fc.string(), { nil: undefined }),
30
+ did: fc.option(arbDID, { nil: undefined }),
31
+ verified: fc.option(fc.boolean(), { nil: undefined }),
32
+ website: fc.option(fc.webUrl(), { nil: undefined }),
33
+ geographicAddress: fc.option(fc.constant(undefined), {
34
+ nil: undefined,
35
+ }),
36
+ nationalIdentification: fc.option(fc.constant(undefined), {
37
+ nil: undefined,
38
+ }),
39
+ }),
40
+ { nil: undefined },
41
+ ),
42
+ });
43
+
16
44
  // Test helper to create arbitrary ComponentRequests
17
45
  const arbComponentRequest = fc.record({
18
- tx: fc.record({
19
- requestId: fc.option(fc.uuid(), { nil: undefined }),
20
- customer: fc.option(
21
- fc.record({
22
- name: fc.string(),
23
- email: fc.option(fc.string(), { nil: undefined }),
24
- phone: fc.option(fc.string(), { nil: undefined }),
25
- type: fc.constant(undefined), // Optional PersonType
26
- accountNumber: fc.option(fc.string(), { nil: undefined }),
27
- did: fc.option(arbDID, { nil: undefined }),
28
- verified: fc.option(fc.boolean(), { nil: undefined }),
29
- website: fc.option(fc.webUrl(), { nil: undefined }),
30
- geographicAddress: fc.option(fc.constant(undefined), {
31
- nil: undefined,
32
- }),
33
- nationalIdentification: fc.option(fc.constant(undefined), {
34
- nil: undefined,
35
- }),
36
- }),
37
- { nil: undefined },
38
- ),
39
- }),
46
+ tx: arbTx,
40
47
  authToken: fc.option(fc.string(), { nil: undefined }),
41
48
  txOptions: fc.option(fc.record({}), { nil: undefined }),
42
49
  });
@@ -68,6 +75,7 @@ describe('ConnectionManager', () => {
68
75
  const mockResponse = {
69
76
  id: 'test-id',
70
77
  version: 1,
78
+ status: 'active',
71
79
  metadata,
72
80
  sealed: ['encrypted-data'],
73
81
  };
@@ -92,6 +100,7 @@ describe('ConnectionManager', () => {
92
100
  expect(result).toEqual({
93
101
  id: mockResponse.id,
94
102
  metadata: mockResponse.metadata,
103
+ status: mockResponse.status,
95
104
  version: mockResponse.version,
96
105
  data: request,
97
106
  key: expect.any(String),
@@ -124,6 +133,9 @@ describe('ConnectionManager', () => {
124
133
 
125
134
  describe('update', () => {
126
135
  it('should successfully update an existing connection', async () => {
136
+ const testData = { requestId: 'test-123' };
137
+ const sealed = await seal(testData);
138
+
127
139
  await fc.assert(
128
140
  fc.asyncProperty(
129
141
  fc.uuid(),
@@ -134,8 +146,9 @@ describe('ConnectionManager', () => {
134
146
  const mockResponse = {
135
147
  id,
136
148
  metadata: arbConnectionMetadata,
149
+ status: 'completed',
137
150
  version: version + 1,
138
- sealed: ['new-encrypted-data'],
151
+ sealed: [sealed.ciphertext],
139
152
  };
140
153
 
141
154
  fetchMock.mockResolvedValueOnce({
@@ -143,7 +156,13 @@ describe('ConnectionManager', () => {
143
156
  json: async () => mockResponse,
144
157
  });
145
158
 
146
- const result = await manager.update(id, request, version);
159
+ const result = await manager.update(
160
+ id,
161
+ request,
162
+ version,
163
+ 'completed',
164
+ sealed.key,
165
+ );
147
166
 
148
167
  // Verify fetch was called correctly
149
168
  expect(fetchMock).toHaveBeenCalledWith(`${testEndpoint}/${id}`, {
@@ -158,16 +177,19 @@ describe('ConnectionManager', () => {
158
177
  expect(result).toEqual({
159
178
  id: mockResponse.id,
160
179
  version: mockResponse.version,
180
+ status: mockResponse.status,
161
181
  metadata: mockResponse.metadata,
162
182
  data: request,
163
- key: expect.any(String),
183
+ key: sealed.key,
164
184
  });
165
185
  },
166
186
  ),
167
187
  );
168
188
  });
169
189
 
170
- it('should throw error on failed update', async () => {
190
+ it('should throw error on failed update due to invalid key', async () => {
191
+ const { key: newKey } = await seal('testData');
192
+
171
193
  await fc.assert(
172
194
  fc.asyncProperty(
173
195
  fc.uuid(),
@@ -180,9 +202,9 @@ describe('ConnectionManager', () => {
180
202
  text: async () => 'Update failed',
181
203
  });
182
204
 
183
- await expect(manager.update(id, request, version)).rejects.toThrow(
184
- 'Failed to update connection',
185
- );
205
+ await expect(
206
+ manager.update(id, request, version, 'active', newKey),
207
+ ).rejects.toThrow('Failed to update connection');
186
208
  },
187
209
  ),
188
210
  );
@@ -201,6 +223,7 @@ describe('ConnectionManager', () => {
201
223
  const mockResponse = {
202
224
  id,
203
225
  version: 1,
226
+ status: 'active',
204
227
  metadata: arbConnectionMetadata,
205
228
  sealed: [sealed.ciphertext], // Use the real ciphertext
206
229
  };
@@ -221,6 +244,7 @@ describe('ConnectionManager', () => {
221
244
  expect(result).toEqual({
222
245
  id: mockResponse.id,
223
246
  version: mockResponse.version,
247
+ status: mockResponse.status,
224
248
  metadata: mockResponse.metadata,
225
249
  data: testData, // Should match our original test data
226
250
  key: sealed.key,
@@ -281,4 +305,133 @@ describe('ConnectionManager', () => {
281
305
  );
282
306
  });
283
307
  });
308
+
309
+ describe('getRefreshResult', async () => {
310
+ // Create test data and sealed object
311
+ const tx = { requestId: 'test-123' };
312
+ const result = { txCreate: { requestId: 'test-123' } };
313
+ const testData = { tx, result };
314
+ const sealed = await seal(testData);
315
+
316
+ // Create a refresh source with the real key
317
+ const refreshSource: RefreshSource = {
318
+ url: 'https://test-endpoint.com',
319
+ key: sealed.key,
320
+ };
321
+
322
+ // Common metadata for all tests
323
+ const metadata = {
324
+ participants: ['participant1'],
325
+ nodeUrl: 'https://node-url.com',
326
+ transactionType: 'withdraw' as TransactionType,
327
+ };
328
+
329
+ // Helper function to create mock responses
330
+ const createMockResponse = (
331
+ status: ConnectionStatus,
332
+ sealedData = sealed.ciphertext,
333
+ ) => ({
334
+ id: 'test-id',
335
+ metadata,
336
+ status,
337
+ sealed: [sealedData],
338
+ version: 1,
339
+ });
340
+
341
+ it('should return basic data for closed status', async () => {
342
+ const mockResponse = createMockResponse('closed');
343
+
344
+ fetchMock.mockResolvedValueOnce({
345
+ ok: true,
346
+ json: async () => mockResponse,
347
+ });
348
+
349
+ const result = await getRefreshResult(refreshSource);
350
+
351
+ expect(result).toEqual({
352
+ id: mockResponse.id,
353
+ metadata: mockResponse.metadata,
354
+ status: 'closed',
355
+ });
356
+ });
357
+
358
+ it('should return data with tx for active status', async () => {
359
+ const mockResponse = createMockResponse('active');
360
+
361
+ fetchMock.mockResolvedValueOnce({
362
+ ok: true,
363
+ json: async () => mockResponse,
364
+ });
365
+
366
+ const result = await getRefreshResult(refreshSource);
367
+
368
+ expect(result).toEqual({
369
+ id: mockResponse.id,
370
+ metadata: mockResponse.metadata,
371
+ status: 'active',
372
+ tx,
373
+ });
374
+ });
375
+
376
+ it('should return data with result for completed status', async () => {
377
+ // Create sealed data with a result
378
+ const transactionResponse = { success: true, data: { id: 'test-id' } };
379
+ const completedSealed = await seal({
380
+ tx: testData,
381
+ result: transactionResponse,
382
+ });
383
+
384
+ // Create a refresh source with the new key
385
+ const completedRefreshSource: RefreshSource = {
386
+ url: 'https://test-endpoint.com',
387
+ key: completedSealed.key,
388
+ };
389
+
390
+ const mockResponse = createMockResponse(
391
+ 'completed',
392
+ completedSealed.ciphertext,
393
+ );
394
+
395
+ fetchMock.mockResolvedValueOnce({
396
+ ok: true,
397
+ json: async () => mockResponse,
398
+ });
399
+
400
+ const result = await getRefreshResult(completedRefreshSource);
401
+
402
+ expect(result).toEqual({
403
+ id: mockResponse.id,
404
+ metadata: mockResponse.metadata,
405
+ status: 'completed',
406
+ result: transactionResponse,
407
+ });
408
+ });
409
+
410
+ it('should throw error when no sealed data is present', async () => {
411
+ fetchMock.mockResolvedValueOnce({
412
+ ok: true,
413
+ json: async () => ({
414
+ id: 'test-id',
415
+ metadata,
416
+ status: 'active' as ConnectionStatus,
417
+ sealed: null,
418
+ }),
419
+ });
420
+
421
+ await expect(getRefreshResult(refreshSource)).rejects.toThrow(
422
+ 'Data missing from server response',
423
+ );
424
+ });
425
+
426
+ it('should throw error on failed request', async () => {
427
+ fetchMock.mockResolvedValueOnce({
428
+ ok: false,
429
+ text: async () => 'Request failed',
430
+ });
431
+
432
+ await expect(getRefreshResult(refreshSource)).rejects.toThrow(
433
+ 'Failed to get connection',
434
+ );
435
+ });
436
+ });
284
437
  });
@@ -1,11 +1,19 @@
1
- import type { ComponentRequest, TransactionOptions, UUID } from '../types';
1
+ import type {
2
+ ComponentRequest,
3
+ RefreshSource,
4
+ TransactionOptions,
5
+ TransactionResponse,
6
+ UUID,
7
+ } from '../types';
2
8
  import { seal, unseal } from './encryption';
3
9
 
4
10
  export type TransactionType = 'withdraw' | 'deposit';
11
+ export type ConnectionStatus = 'active' | 'completed' | 'closed';
5
12
  export interface ConnectionData<T extends ComponentRequest> {
6
13
  tx: T;
7
14
  authToken?: string;
8
15
  txOptions?: TransactionOptions;
16
+ result?: TransactionResponse<T>;
9
17
  }
10
18
 
11
19
  export interface ConnectionMetadata {
@@ -16,11 +24,69 @@ export interface ConnectionMetadata {
16
24
  export interface ConnectionResponse<T extends ComponentRequest> {
17
25
  id: UUID;
18
26
  version: number;
27
+ status: ConnectionStatus;
19
28
  metadata: ConnectionMetadata;
20
29
  data: ConnectionData<T>;
21
30
  key: string;
22
31
  }
23
32
 
33
+ export interface ConnectionResult<T extends ComponentRequest> {
34
+ id: UUID;
35
+ metadata: ConnectionMetadata;
36
+ status: ConnectionStatus;
37
+ tx?: T;
38
+ result?: TransactionResponse<T>;
39
+ }
40
+
41
+ export async function getRefreshResult<T extends ComponentRequest>(
42
+ refreshSource: RefreshSource,
43
+ ): Promise<ConnectionResult<T>> {
44
+ const response = await fetch(refreshSource.url, {
45
+ method: 'GET',
46
+ });
47
+
48
+ if (!response.ok) {
49
+ throw new Error(`Failed to get connection: ${await response.text()}`);
50
+ }
51
+
52
+ const result = await response.json();
53
+
54
+ if (!result.id || !result.metadata || !result.status || !result.sealed) {
55
+ throw new Error('Data missing from server response');
56
+ }
57
+
58
+ const basicData = {
59
+ id: result.id,
60
+ metadata: result.metadata,
61
+ status: result.status,
62
+ };
63
+
64
+ if (result.status === 'closed') {
65
+ return basicData;
66
+ }
67
+
68
+ // Get the latest sealed data
69
+ const latestSealed = result.sealed[result.sealed.length - 1];
70
+
71
+ // Decrypt the data
72
+ const data = await unseal<ConnectionData<T>>({
73
+ ciphertext: latestSealed,
74
+ key: refreshSource.key,
75
+ });
76
+
77
+ if (result.status === 'completed') {
78
+ return {
79
+ ...basicData,
80
+ result: data.result,
81
+ };
82
+ }
83
+
84
+ return {
85
+ ...basicData,
86
+ tx: data.tx,
87
+ };
88
+ }
89
+
24
90
  /**
25
91
  * Manages encrypted connections using Cloudflare Durable Objects
26
92
  */
@@ -68,6 +134,7 @@ export class ConnectionManager {
68
134
  return {
69
135
  id: result.id,
70
136
  version: result.version,
137
+ status: result.status,
71
138
  metadata: metadata,
72
139
  data: data,
73
140
  key: sealed.key,
@@ -85,14 +152,17 @@ export class ConnectionManager {
85
152
  id: UUID,
86
153
  data: ConnectionData<T>,
87
154
  version: number,
155
+ status: ConnectionStatus,
156
+ key: string,
88
157
  ): Promise<ConnectionResponse<T>> {
89
158
  // Encrypt the new data
90
- const sealed = await seal(data);
159
+ const sealed = await seal(data, key);
91
160
 
92
161
  // Prepare the request body
93
162
  const body = {
94
163
  sealed: sealed.ciphertext,
95
164
  version,
165
+ status,
96
166
  };
97
167
 
98
168
  // Update the connection
@@ -114,6 +184,7 @@ export class ConnectionManager {
114
184
  id: result.id,
115
185
  metadata: result.metadata,
116
186
  version: result.version,
187
+ status: result.status,
117
188
  data: data,
118
189
  key: sealed.key,
119
190
  };
@@ -151,6 +222,7 @@ export class ConnectionManager {
151
222
 
152
223
  return {
153
224
  id: result.id,
225
+ status: result.status,
154
226
  version: result.version,
155
227
  metadata: result.metadata,
156
228
  data,
@@ -14,13 +14,15 @@ export interface Sealed {
14
14
  * @param data Data to encrypt
15
15
  * @returns Promise resolving to a Sealed object containing ciphertext and key
16
16
  */
17
- export async function seal<T>(data: T): Promise<Sealed> {
17
+ export async function seal<T>(data: T, existingKey?: string): Promise<Sealed> {
18
18
  // Convert the object to a JSON string
19
19
  const plaintext = JSON.stringify(data);
20
20
  const encoder = new TextEncoder();
21
21
 
22
- // Generate a random encryption key and IV
23
- const rawKey = crypto.getRandomValues(new Uint8Array(32)); // 256-bit key
22
+ // Restore or generate a random encryption key
23
+ const rawKey = existingKey
24
+ ? base64ToUint8Array(existingKey)
25
+ : crypto.getRandomValues(new Uint8Array(32));
24
26
  const key = await crypto.subtle.importKey(
25
27
  'raw',
26
28
  rawKey,