@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.
- package/dist/cjs/notabene.cjs +1 -1
- package/dist/cjs/notabene.d.ts +29 -2
- package/dist/cjs/package.json +1 -1
- package/dist/esm/notabene.d.ts +29 -2
- package/dist/esm/notabene.js +148 -115
- package/dist/esm/package.json +1 -1
- package/dist/notabene.d.ts +29 -2
- package/dist/notabene.js +148 -115
- package/package.json +1 -1
- package/src/notabene.ts +7 -0
- package/src/types.ts +13 -0
- package/src/utils/__tests__/connections.test.ts +184 -31
- package/src/utils/connections.ts +74 -2
- package/src/utils/encryption.ts +5 -3
|
@@ -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 {
|
|
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:
|
|
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: [
|
|
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(
|
|
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:
|
|
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(
|
|
184
|
-
|
|
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
|
});
|
package/src/utils/connections.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import type {
|
|
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,
|
package/src/utils/encryption.ts
CHANGED
|
@@ -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
|
-
//
|
|
23
|
-
const rawKey =
|
|
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,
|