@decentrl/sdk 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.
Files changed (74) hide show
  1. package/dist/client.d.ts +36 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +192 -0
  4. package/dist/contract-manager.d.ts +23 -0
  5. package/dist/contract-manager.d.ts.map +1 -0
  6. package/dist/contract-manager.js +91 -0
  7. package/dist/define-app.d.ts +8 -0
  8. package/dist/define-app.d.ts.map +1 -0
  9. package/dist/define-app.js +7 -0
  10. package/dist/direct-transport.d.ts +69 -0
  11. package/dist/direct-transport.d.ts.map +1 -0
  12. package/dist/direct-transport.js +450 -0
  13. package/dist/errors.d.ts +7 -0
  14. package/dist/errors.d.ts.map +1 -0
  15. package/dist/errors.js +10 -0
  16. package/dist/event-processor.d.ts +19 -0
  17. package/dist/event-processor.d.ts.map +1 -0
  18. package/dist/event-processor.js +93 -0
  19. package/dist/identity-manager.d.ts +22 -0
  20. package/dist/identity-manager.d.ts.map +1 -0
  21. package/dist/identity-manager.js +62 -0
  22. package/dist/identity-serialization.d.ts +5 -0
  23. package/dist/identity-serialization.d.ts.map +1 -0
  24. package/dist/identity-serialization.js +30 -0
  25. package/dist/index.d.ts +18 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +10 -0
  28. package/dist/persistence.d.ts +11 -0
  29. package/dist/persistence.d.ts.map +1 -0
  30. package/dist/persistence.js +82 -0
  31. package/dist/state-store.d.ts +12 -0
  32. package/dist/state-store.d.ts.map +1 -0
  33. package/dist/state-store.js +32 -0
  34. package/dist/sync-manager.d.ts +33 -0
  35. package/dist/sync-manager.d.ts.map +1 -0
  36. package/dist/sync-manager.js +244 -0
  37. package/dist/tag-templates.d.ts +2 -0
  38. package/dist/tag-templates.d.ts.map +1 -0
  39. package/dist/tag-templates.js +23 -0
  40. package/dist/test-helpers.d.ts +15 -0
  41. package/dist/test-helpers.d.ts.map +1 -0
  42. package/dist/test-helpers.js +65 -0
  43. package/dist/transport.d.ts +41 -0
  44. package/dist/transport.d.ts.map +1 -0
  45. package/dist/transport.js +1 -0
  46. package/dist/types.d.ts +131 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/types.js +1 -0
  49. package/dist/websocket-transport.d.ts +36 -0
  50. package/dist/websocket-transport.d.ts.map +1 -0
  51. package/dist/websocket-transport.js +160 -0
  52. package/package.json +35 -0
  53. package/src/client.ts +277 -0
  54. package/src/contract-manager.test.ts +207 -0
  55. package/src/contract-manager.ts +130 -0
  56. package/src/define-app.ts +25 -0
  57. package/src/direct-transport.test.ts +460 -0
  58. package/src/direct-transport.ts +729 -0
  59. package/src/errors.ts +23 -0
  60. package/src/event-processor.ts +133 -0
  61. package/src/identity-manager.ts +91 -0
  62. package/src/identity-serialization.ts +33 -0
  63. package/src/index.ts +43 -0
  64. package/src/persistence.ts +103 -0
  65. package/src/sdk.e2e.test.ts +367 -0
  66. package/src/state-store.ts +42 -0
  67. package/src/sync-manager.test.ts +414 -0
  68. package/src/sync-manager.ts +308 -0
  69. package/src/tag-templates.test.ts +111 -0
  70. package/src/tag-templates.ts +30 -0
  71. package/src/test-helpers.ts +88 -0
  72. package/src/transport.ts +65 -0
  73. package/src/types.ts +191 -0
  74. package/src/websocket-transport.ts +233 -0
@@ -0,0 +1,460 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { DirectTransport } from './direct-transport.js';
3
+ import { makeContract } from './test-helpers.js';
4
+
5
+ describe('DirectTransport', () => {
6
+ describe('processContractCleanup', () => {
7
+ let transport: DirectTransport;
8
+
9
+ beforeEach(() => {
10
+ transport = new DirectTransport({
11
+ httpPost: vi.fn(async () => ({ data: { type: 'SUCCESS' } })),
12
+ });
13
+ });
14
+
15
+ it('does nothing when only one contract per participant', () => {
16
+ const now = Math.floor(Date.now() / 1000);
17
+ const contract = makeContract({
18
+ id: 'only-one',
19
+ participantDid: 'did:decentrl:bob',
20
+ expiresAt: now + 3600,
21
+ timestamp: now,
22
+ });
23
+
24
+ // Inject contracts via the internal array
25
+ (transport as any).activeContracts = [contract];
26
+
27
+ transport.processContractCleanup();
28
+
29
+ expect(transport.getActiveContracts()).toHaveLength(1);
30
+ });
31
+
32
+ it('marks superseded contracts and purges on counterparty confirmation', () => {
33
+ const now = Math.floor(Date.now() / 1000);
34
+
35
+ const oldContract = makeContract({
36
+ id: 'old',
37
+ participantDid: 'did:decentrl:bob',
38
+ expiresAt: now + 3600,
39
+ timestamp: now,
40
+ });
41
+
42
+ const newContract = makeContract({
43
+ id: 'new',
44
+ participantDid: 'did:decentrl:bob',
45
+ expiresAt: now + 7200,
46
+ timestamp: now + 100,
47
+ });
48
+
49
+ (transport as any).activeContracts = [oldContract, newContract];
50
+
51
+ // First cleanup: mark as superseded, but don't purge yet
52
+ transport.processContractCleanup();
53
+ expect(transport.getActiveContracts()).toHaveLength(2);
54
+
55
+ // Simulate counterparty confirmation
56
+ (transport as any).confirmedNewContracts.add('new');
57
+
58
+ // Second cleanup: should purge old
59
+ transport.processContractCleanup();
60
+ expect(transport.getActiveContracts()).toHaveLength(1);
61
+ expect(transport.getActiveContracts()[0].id).toBe('new');
62
+ });
63
+
64
+ it('purges superseded contracts after 7-day timeout', () => {
65
+ const now = Math.floor(Date.now() / 1000);
66
+
67
+ const oldContract = makeContract({
68
+ id: 'old',
69
+ participantDid: 'did:decentrl:bob',
70
+ expiresAt: now + 3600,
71
+ timestamp: now,
72
+ });
73
+
74
+ const newContract = makeContract({
75
+ id: 'new',
76
+ participantDid: 'did:decentrl:bob',
77
+ expiresAt: now + 7200,
78
+ timestamp: now + 100,
79
+ });
80
+
81
+ (transport as any).activeContracts = [oldContract, newContract];
82
+
83
+ // First cleanup: mark as superseded
84
+ transport.processContractCleanup();
85
+ expect(transport.getActiveContracts()).toHaveLength(2);
86
+
87
+ // Advance time past 7 days
88
+ const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
89
+ vi.spyOn(Date, 'now').mockReturnValue(Date.now() + sevenDaysMs);
90
+
91
+ transport.processContractCleanup();
92
+ expect(transport.getActiveContracts()).toHaveLength(1);
93
+ expect(transport.getActiveContracts()[0].id).toBe('new');
94
+
95
+ vi.restoreAllMocks();
96
+ });
97
+
98
+ it('does not purge before timeout without confirmation', () => {
99
+ const now = Math.floor(Date.now() / 1000);
100
+
101
+ const oldContract = makeContract({
102
+ id: 'old',
103
+ participantDid: 'did:decentrl:bob',
104
+ expiresAt: now + 3600,
105
+ timestamp: now,
106
+ });
107
+
108
+ const newContract = makeContract({
109
+ id: 'new',
110
+ participantDid: 'did:decentrl:bob',
111
+ expiresAt: now + 7200,
112
+ timestamp: now + 100,
113
+ });
114
+
115
+ (transport as any).activeContracts = [oldContract, newContract];
116
+
117
+ // First cleanup: mark as superseded
118
+ transport.processContractCleanup();
119
+
120
+ // Advance time by 3 days (less than 7)
121
+ const threeDaysMs = 3 * 24 * 60 * 60 * 1000;
122
+ vi.spyOn(Date, 'now').mockReturnValue(Date.now() + threeDaysMs);
123
+
124
+ // Second cleanup: should NOT purge yet
125
+ transport.processContractCleanup();
126
+ expect(transport.getActiveContracts()).toHaveLength(2);
127
+
128
+ vi.restoreAllMocks();
129
+ });
130
+
131
+ it('handles multiple participants independently', () => {
132
+ const now = Math.floor(Date.now() / 1000);
133
+
134
+ const bobOld = makeContract({
135
+ id: 'bob-old',
136
+ participantDid: 'did:decentrl:bob',
137
+ expiresAt: now + 3600,
138
+ timestamp: now,
139
+ });
140
+
141
+ const bobNew = makeContract({
142
+ id: 'bob-new',
143
+ participantDid: 'did:decentrl:bob',
144
+ expiresAt: now + 7200,
145
+ timestamp: now + 100,
146
+ });
147
+
148
+ const carolOnly = makeContract({
149
+ id: 'carol-only',
150
+ participantDid: 'did:decentrl:carol',
151
+ expiresAt: now + 3600,
152
+ timestamp: now,
153
+ });
154
+
155
+ (transport as any).activeContracts = [bobOld, bobNew, carolOnly];
156
+
157
+ // Mark as superseded
158
+ transport.processContractCleanup();
159
+
160
+ // Confirm bob's new contract
161
+ (transport as any).confirmedNewContracts.add('bob-new');
162
+
163
+ transport.processContractCleanup();
164
+
165
+ const active = transport.getActiveContracts();
166
+ expect(active).toHaveLength(2);
167
+ expect(active.map((c) => c.id).sort()).toEqual(['bob-new', 'carol-only']);
168
+ });
169
+ });
170
+
171
+ describe('processAutoRenewals', () => {
172
+ let transport: DirectTransport;
173
+ let httpPost: ReturnType<typeof vi.fn>;
174
+
175
+ beforeEach(() => {
176
+ httpPost = vi.fn(async () => ({ data: { type: 'SUCCESS' } }));
177
+ transport = new DirectTransport({ httpPost });
178
+ });
179
+
180
+ it('initiates renewal when contract is near expiry and DID sorts lower', async () => {
181
+ const now = Date.now();
182
+ const nowSec = Math.floor(now / 1000);
183
+ const lifetime = 3600;
184
+ const timestamp = nowSec - lifetime * 0.9; // 90% elapsed
185
+
186
+ // "alice" < "bob" alphabetically, so alice initiates
187
+ const contract = makeContract({
188
+ id: 'c1',
189
+ participantDid: 'did:decentrl:bob',
190
+ expiresAt: timestamp + lifetime,
191
+ timestamp,
192
+ });
193
+ contract.signedCommunicationContract.communication_contract.requestor_did =
194
+ 'did:decentrl:alice';
195
+
196
+ // Set up identity
197
+ (transport as any).identity = {
198
+ did: 'did:decentrl:alice',
199
+ mediatorDid: 'did:web:mediator',
200
+ mediatorEndpoint: 'http://mediator',
201
+ keys: {
202
+ signing: {
203
+ privateKey: new Uint8Array(32),
204
+ publicKey: new Uint8Array(32),
205
+ },
206
+ encryption: {
207
+ privateKey: new Uint8Array(32),
208
+ publicKey: new Uint8Array(32),
209
+ },
210
+ storageKey: new Uint8Array(32),
211
+ },
212
+ };
213
+ (transport as any).activeContracts = [contract];
214
+
215
+ const requestSpy = vi.spyOn(transport, 'requestContract').mockResolvedValue();
216
+
217
+ await transport.processAutoRenewals(0.2);
218
+
219
+ expect(requestSpy).toHaveBeenCalledWith('did:decentrl:bob', lifetime);
220
+ });
221
+
222
+ it('does NOT initiate when DID sorts higher than participant', async () => {
223
+ const now = Date.now();
224
+ const nowSec = Math.floor(now / 1000);
225
+ const lifetime = 3600;
226
+ const timestamp = nowSec - lifetime * 0.9;
227
+
228
+ const contract = makeContract({
229
+ id: 'c1',
230
+ participantDid: 'did:decentrl:aaa', // alice > aaa
231
+ expiresAt: timestamp + lifetime,
232
+ timestamp,
233
+ });
234
+
235
+ (transport as any).identity = {
236
+ did: 'did:decentrl:alice',
237
+ mediatorDid: 'did:web:mediator',
238
+ mediatorEndpoint: 'http://mediator',
239
+ keys: {
240
+ signing: {
241
+ privateKey: new Uint8Array(32),
242
+ publicKey: new Uint8Array(32),
243
+ },
244
+ encryption: {
245
+ privateKey: new Uint8Array(32),
246
+ publicKey: new Uint8Array(32),
247
+ },
248
+ storageKey: new Uint8Array(32),
249
+ },
250
+ };
251
+ (transport as any).activeContracts = [contract];
252
+
253
+ const requestSpy = vi.spyOn(transport, 'requestContract').mockResolvedValue();
254
+
255
+ await transport.processAutoRenewals(0.2);
256
+
257
+ expect(requestSpy).not.toHaveBeenCalled();
258
+ });
259
+
260
+ it('does NOT initiate when contract is not near expiry', async () => {
261
+ const nowSec = Math.floor(Date.now() / 1000);
262
+ const lifetime = 3600;
263
+ const timestamp = nowSec - lifetime * 0.1; // Only 10% elapsed
264
+
265
+ const contract = makeContract({
266
+ id: 'c1',
267
+ participantDid: 'did:decentrl:bob',
268
+ expiresAt: timestamp + lifetime,
269
+ timestamp,
270
+ });
271
+
272
+ (transport as any).identity = {
273
+ did: 'did:decentrl:alice',
274
+ mediatorDid: 'did:web:mediator',
275
+ mediatorEndpoint: 'http://mediator',
276
+ keys: {
277
+ signing: {
278
+ privateKey: new Uint8Array(32),
279
+ publicKey: new Uint8Array(32),
280
+ },
281
+ encryption: {
282
+ privateKey: new Uint8Array(32),
283
+ publicKey: new Uint8Array(32),
284
+ },
285
+ storageKey: new Uint8Array(32),
286
+ },
287
+ };
288
+ (transport as any).activeContracts = [contract];
289
+
290
+ const requestSpy = vi.spyOn(transport, 'requestContract').mockResolvedValue();
291
+
292
+ await transport.processAutoRenewals(0.2);
293
+
294
+ expect(requestSpy).not.toHaveBeenCalled();
295
+ });
296
+
297
+ it('prevents duplicate in-flight renewals for the same DID', async () => {
298
+ const nowSec = Math.floor(Date.now() / 1000);
299
+ const lifetime = 3600;
300
+ const timestamp = nowSec - lifetime * 0.9;
301
+
302
+ const contract = makeContract({
303
+ id: 'c1',
304
+ participantDid: 'did:decentrl:bob',
305
+ expiresAt: timestamp + lifetime,
306
+ timestamp,
307
+ });
308
+
309
+ (transport as any).identity = {
310
+ did: 'did:decentrl:alice',
311
+ mediatorDid: 'did:web:mediator',
312
+ mediatorEndpoint: 'http://mediator',
313
+ keys: {
314
+ signing: {
315
+ privateKey: new Uint8Array(32),
316
+ publicKey: new Uint8Array(32),
317
+ },
318
+ encryption: {
319
+ privateKey: new Uint8Array(32),
320
+ publicKey: new Uint8Array(32),
321
+ },
322
+ storageKey: new Uint8Array(32),
323
+ },
324
+ };
325
+ (transport as any).activeContracts = [contract];
326
+
327
+ let resolveRequest: () => void;
328
+ const blockingPromise = new Promise<void>((resolve) => {
329
+ resolveRequest = resolve;
330
+ });
331
+
332
+ const requestSpy = vi.spyOn(transport, 'requestContract').mockReturnValue(blockingPromise);
333
+
334
+ // Start first renewal (will block)
335
+ const renewal1 = transport.processAutoRenewals(0.2);
336
+
337
+ // Start second renewal while first is in-flight
338
+ const renewal2 = transport.processAutoRenewals(0.2);
339
+
340
+ resolveRequest!();
341
+ await renewal1;
342
+ await renewal2;
343
+
344
+ // Should only request once
345
+ expect(requestSpy).toHaveBeenCalledTimes(1);
346
+ });
347
+
348
+ it('clears in-flight flag even when request fails', async () => {
349
+ const nowSec = Math.floor(Date.now() / 1000);
350
+ const lifetime = 3600;
351
+ const timestamp = nowSec - lifetime * 0.9;
352
+
353
+ const contract = makeContract({
354
+ id: 'c1',
355
+ participantDid: 'did:decentrl:bob',
356
+ expiresAt: timestamp + lifetime,
357
+ timestamp,
358
+ });
359
+
360
+ (transport as any).identity = {
361
+ did: 'did:decentrl:alice',
362
+ mediatorDid: 'did:web:mediator',
363
+ mediatorEndpoint: 'http://mediator',
364
+ keys: {
365
+ signing: {
366
+ privateKey: new Uint8Array(32),
367
+ publicKey: new Uint8Array(32),
368
+ },
369
+ encryption: {
370
+ privateKey: new Uint8Array(32),
371
+ publicKey: new Uint8Array(32),
372
+ },
373
+ storageKey: new Uint8Array(32),
374
+ },
375
+ };
376
+ (transport as any).activeContracts = [contract];
377
+
378
+ const requestSpy = vi
379
+ .spyOn(transport, 'requestContract')
380
+ .mockRejectedValueOnce(new Error('network error'))
381
+ .mockResolvedValueOnce();
382
+
383
+ await transport.processAutoRenewals(0.2);
384
+
385
+ // First call fails, but in-flight flag should be cleared
386
+ expect(requestSpy).toHaveBeenCalledTimes(1);
387
+
388
+ // Second call should work
389
+ await transport.processAutoRenewals(0.2);
390
+ expect(requestSpy).toHaveBeenCalledTimes(2);
391
+ });
392
+ });
393
+
394
+ describe('refreshContracts', () => {
395
+ it('filters out purged contracts', async () => {
396
+ const now = Math.floor(Date.now() / 1000);
397
+
398
+ const httpPost = vi.fn(async () => ({
399
+ data: {
400
+ type: 'SUCCESS',
401
+ payload: {
402
+ communication_contracts: [
403
+ {
404
+ signed_communication_contract: {
405
+ communication_contract: {
406
+ requestor_did: 'did:decentrl:alice',
407
+ recipient_did: 'did:decentrl:bob',
408
+ requestor_signing_key_id: 'did:decentrl:alice#signing',
409
+ recipient_signing_key_id: 'did:decentrl:bob#signing',
410
+ requestor_encryption_public_key: 'key-a',
411
+ recipient_encryption_public_key: 'key-b',
412
+ expires_at: now + 3600,
413
+ timestamp: now,
414
+ },
415
+ requestor_signature: 'sig-a',
416
+ recipient_signature: 'sig-b',
417
+ },
418
+ },
419
+ ],
420
+ },
421
+ },
422
+ }));
423
+
424
+ const transport = new DirectTransport({ httpPost });
425
+
426
+ (transport as any).identity = {
427
+ did: 'did:decentrl:alice',
428
+ mediatorDid: 'did:web:mediator',
429
+ mediatorEndpoint: 'http://mediator',
430
+ keys: {
431
+ signing: {
432
+ privateKey: new Uint8Array(32),
433
+ publicKey: new Uint8Array(32),
434
+ },
435
+ encryption: {
436
+ privateKey: new Uint8Array(32),
437
+ publicKey: new Uint8Array(32),
438
+ },
439
+ storageKey: new Uint8Array(32),
440
+ },
441
+ };
442
+
443
+ // Refresh to get the contract
444
+ const contracts = await transport.refreshContracts();
445
+ expect(contracts).toHaveLength(1);
446
+
447
+ const contractId = contracts[0].id;
448
+
449
+ // Add to purged set
450
+ (transport as any).purgedContractIds.add(contractId);
451
+
452
+ // Refresh again — purged contract should be filtered out
453
+ await transport.refreshContracts();
454
+ expect(transport.getActiveContracts()).toHaveLength(0);
455
+
456
+ // purgedContractIds should be cleared after refresh
457
+ expect((transport as any).purgedContractIds.size).toBe(0);
458
+ });
459
+ });
460
+ });