@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,367 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { defineDecentrlApp } from './define-app.js';
4
+ import { DirectTransport } from './direct-transport.js';
5
+
6
+ const MEDIATOR_DID = 'did:web:localhost';
7
+
8
+ // -- Define a chat app using the SDK --
9
+
10
+ const chatApp = defineDecentrlApp({
11
+ events: {
12
+ 'chat.create': {
13
+ schema: z.object({
14
+ id: z.string(),
15
+ name: z.string(),
16
+ participants: z.array(z.string()),
17
+ createdBy: z.string(),
18
+ }),
19
+ tags: ['chat', 'chat:${id}'],
20
+ },
21
+ 'chat.message': {
22
+ schema: z.object({
23
+ chatId: z.string(),
24
+ messageId: z.string(),
25
+ text: z.string(),
26
+ sentBy: z.string(),
27
+ }),
28
+ tags: ['chat:${chatId}', 'message'],
29
+ },
30
+ },
31
+ state: {
32
+ chats: {
33
+ initial: [] as Array<{
34
+ id: string;
35
+ name: string;
36
+ participants: string[];
37
+ }>,
38
+ reduce: {
39
+ 'chat.create': (chats, event) => [
40
+ ...chats,
41
+ {
42
+ id: event.id,
43
+ name: event.name,
44
+ participants: event.participants,
45
+ },
46
+ ],
47
+ },
48
+ },
49
+ messages: {
50
+ initial: {} as Record<string, Array<{ id: string; text: string; sentBy: string }>>,
51
+ reduce: {
52
+ 'chat.message': (msgs, event) => ({
53
+ ...msgs,
54
+ [event.chatId]: [
55
+ ...(msgs[event.chatId] || []),
56
+ { id: event.messageId, text: event.text, sentBy: event.sentBy },
57
+ ],
58
+ }),
59
+ },
60
+ },
61
+ },
62
+ });
63
+
64
+ type ChatClient = ReturnType<typeof chatApp.createClient>;
65
+
66
+ describe('SDK E2E', () => {
67
+ // ------- defineDecentrlApp + createClient -------
68
+
69
+ describe('defineDecentrlApp', () => {
70
+ it('should create a client with initial state', () => {
71
+ const client = chatApp.createClient({ mediatorDid: MEDIATOR_DID });
72
+
73
+ const state = client.getState();
74
+ expect(state.chats).toEqual([]);
75
+ expect(state.messages).toEqual({});
76
+ });
77
+ });
78
+
79
+ // ------- Identity creation -------
80
+
81
+ describe('Identity', () => {
82
+ let client: ChatClient;
83
+
84
+ beforeAll(() => {
85
+ client = chatApp.createClient({ mediatorDid: MEDIATOR_DID });
86
+ });
87
+
88
+ afterAll(() => {
89
+ client.reset();
90
+ });
91
+
92
+ it('should create an identity and register with mediator', async () => {
93
+ const identity = await client.identity.create({
94
+ alias: 'identity-test-user',
95
+ mediatorDid: MEDIATOR_DID,
96
+ });
97
+
98
+ expect(identity.did).toContain('did:decentrl:');
99
+ expect(identity.alias).toBe('identity-test-user');
100
+ expect(identity.mediatorDid).toBe(MEDIATOR_DID);
101
+ expect(identity.mediatorEndpoint).toBe('http://localhost:8080');
102
+ expect(identity.keys.signing.privateKey).toBeInstanceOf(Uint8Array);
103
+ expect(identity.keys.encryption.privateKey).toBeInstanceOf(Uint8Array);
104
+ expect(identity.mediatorContract).toBeDefined();
105
+ });
106
+
107
+ it('should throw when creating identity twice', async () => {
108
+ await expect(
109
+ client.identity.create({ alias: 'duplicate', mediatorDid: MEDIATOR_DID }),
110
+ ).rejects.toThrow('Identity already exists');
111
+ });
112
+
113
+ it('should serialize and load identity', () => {
114
+ const serialized = client.identity.serialize();
115
+ expect(serialized.did).toBe(client.identity.getIdentity()!.did);
116
+ expect(typeof serialized.keys.signing.privateKey).toBe('string');
117
+
118
+ const client2 = chatApp.createClient({ mediatorDid: MEDIATOR_DID });
119
+ const loaded = client2.identity.load(serialized);
120
+ expect(loaded.did).toBe(serialized.did);
121
+ expect(loaded.keys.signing.privateKey).toBeInstanceOf(Uint8Array);
122
+ client2.reset();
123
+ });
124
+
125
+ it('should throw IDENTITY_NOT_INITIALIZED when requireIdentity called without identity', () => {
126
+ const emptyClient = chatApp.createClient({ mediatorDid: MEDIATOR_DID });
127
+ expect(() => emptyClient.identity.requireIdentity()).toThrow('Identity not initialized');
128
+ });
129
+ });
130
+
131
+ // ------- Full workflow: Alice <-> Bob -------
132
+
133
+ describe('Full workflow: contracts, publish, state, sync', () => {
134
+ let alice: ChatClient;
135
+ let bob: ChatClient;
136
+
137
+ beforeAll(async () => {
138
+ alice = chatApp.createClient({
139
+ mediatorDid: MEDIATOR_DID,
140
+ transport: new DirectTransport(),
141
+ });
142
+ bob = chatApp.createClient({
143
+ mediatorDid: MEDIATOR_DID,
144
+ transport: new DirectTransport(),
145
+ });
146
+
147
+ await alice.identity.create({ alias: 'alice-e2e', mediatorDid: MEDIATOR_DID });
148
+ await bob.identity.create({ alias: 'bob-e2e', mediatorDid: MEDIATOR_DID });
149
+ });
150
+
151
+ afterAll(() => {
152
+ alice.stopSync();
153
+ bob.stopSync();
154
+ });
155
+
156
+ // -- Contract request/accept flow --
157
+
158
+ it('Alice should request a contract with Bob', async () => {
159
+ await alice.contracts.request(bob.identity.getIdentity()!.did);
160
+ });
161
+
162
+ it("Bob should see Alice's pending contract request", async () => {
163
+ const pending = await bob.contracts.getPending();
164
+ expect(pending.length).toBe(1);
165
+ expect(pending[0].senderDid).toBe(alice.identity.getIdentity()!.did);
166
+ });
167
+
168
+ it("Bob should accept Alice's contract request", async () => {
169
+ const pending = await bob.contracts.getPending();
170
+ await bob.contracts.accept(
171
+ pending[0].id,
172
+ pending[0].encryptedPayload,
173
+ pending[0].requestorEphemeralPublicKey,
174
+ );
175
+ });
176
+
177
+ it('Both should see active contracts after refresh', async () => {
178
+ await alice.contracts.refresh();
179
+ await bob.contracts.refresh();
180
+
181
+ const aliceContracts = alice.contracts.getActiveContracts();
182
+ const bobContracts = bob.contracts.getActiveContracts();
183
+
184
+ expect(aliceContracts.length).toBeGreaterThanOrEqual(1);
185
+ expect(bobContracts.length).toBeGreaterThanOrEqual(1);
186
+
187
+ const aliceContractWithBob = aliceContracts.find(
188
+ (c) => c.participantDid === bob.identity.getIdentity()!.did,
189
+ );
190
+ expect(aliceContractWithBob).toBeDefined();
191
+ expect(aliceContractWithBob!.status).toBe('active');
192
+ });
193
+
194
+ // -- Publish + local state --
195
+
196
+ it('Alice should publish a chat.create event and state updates locally', async () => {
197
+ const bobDid = bob.identity.getIdentity()!.did;
198
+
199
+ await alice.publish(
200
+ 'chat.create',
201
+ {
202
+ id: 'chat-1',
203
+ name: 'Alice & Bob',
204
+ participants: [alice.identity.getIdentity()!.did, bobDid],
205
+ createdBy: alice.identity.getIdentity()!.did,
206
+ },
207
+ { recipient: bobDid },
208
+ );
209
+
210
+ const state = alice.getState();
211
+ expect(state.chats).toHaveLength(1);
212
+ expect(state.chats[0].name).toBe('Alice & Bob');
213
+ expect(state.messages).toEqual({});
214
+ });
215
+
216
+ it('Alice should publish a chat.message event and state updates locally', async () => {
217
+ const bobDid = bob.identity.getIdentity()!.did;
218
+
219
+ await alice.publish(
220
+ 'chat.message',
221
+ {
222
+ chatId: 'chat-1',
223
+ messageId: 'msg-1',
224
+ text: 'Hello Bob!',
225
+ sentBy: alice.identity.getIdentity()!.did,
226
+ },
227
+ { recipient: bobDid },
228
+ );
229
+
230
+ const state = alice.getState();
231
+ expect(state.chats).toHaveLength(1);
232
+ expect(state.messages['chat-1']).toHaveLength(1);
233
+ expect(state.messages['chat-1'][0].text).toBe('Hello Bob!');
234
+ });
235
+
236
+ // -- Schema validation --
237
+
238
+ it('should reject invalid event data', async () => {
239
+ await expect(
240
+ alice.publish('chat.create', {
241
+ id: 'bad',
242
+ // missing required fields
243
+ } as any),
244
+ ).rejects.toThrow('Schema validation failed');
245
+ });
246
+
247
+ it('should reject unknown event type', async () => {
248
+ await expect(alice.publish('unknown.event' as any, {} as any)).rejects.toThrow(
249
+ 'Unknown event type',
250
+ );
251
+ });
252
+
253
+ // -- Sync: Bob receives Alice's events --
254
+
255
+ it("Bob should receive Alice's events via sync", async () => {
256
+ await bob.sync();
257
+
258
+ const state = bob.getState();
259
+ expect(state.chats.length).toBeGreaterThanOrEqual(1);
260
+
261
+ const chat = state.chats.find((c) => c.id === 'chat-1');
262
+ expect(chat).toBeDefined();
263
+ expect(chat!.name).toBe('Alice & Bob');
264
+
265
+ const chatMsgs = state.messages['chat-1'];
266
+ expect(chatMsgs).toBeDefined();
267
+ expect(chatMsgs.length).toBeGreaterThanOrEqual(1);
268
+ expect(chatMsgs[0].text).toBe('Hello Bob!');
269
+ });
270
+
271
+ // -- Tag-based queries --
272
+
273
+ it('Alice should query her own events by tag', async () => {
274
+ const result = await alice.query({ tags: ['chat'] });
275
+ expect(result.data.length).toBeGreaterThanOrEqual(1);
276
+
277
+ const createEvent = result.data.find((e) => e.type === 'chat.create');
278
+ expect(createEvent).toBeDefined();
279
+ expect(createEvent!.data).toMatchObject({ id: 'chat-1', name: 'Alice & Bob' });
280
+ });
281
+
282
+ it('Alice should query by specific chat tag', async () => {
283
+ const result = await alice.query({ tags: ['chat:chat-1'] });
284
+ expect(result.data.length).toBeGreaterThanOrEqual(2); // chat.create + chat.message
285
+
286
+ const types = result.data.map((e) => e.type);
287
+ expect(types).toContain('chat.create');
288
+ expect(types).toContain('chat.message');
289
+ });
290
+
291
+ it('Alice should query messages by message tag', async () => {
292
+ const result = await alice.query({ tags: ['message'] });
293
+ expect(result.data.length).toBeGreaterThanOrEqual(1);
294
+ expect(result.data.every((e) => e.type === 'chat.message')).toBe(true);
295
+ });
296
+
297
+ it('Bob should query received events by tag after sync', async () => {
298
+ const result = await bob.query({ tags: ['chat:chat-1'] });
299
+ expect(result.data.length).toBeGreaterThanOrEqual(1);
300
+
301
+ const createEvent = result.data.find((e) => e.type === 'chat.create');
302
+ expect(createEvent).toBeDefined();
303
+ });
304
+
305
+ // -- Dedup --
306
+
307
+ it('sync should dedup already-processed events', async () => {
308
+ const stateBefore = bob.getState();
309
+ const chatCountBefore = stateBefore.chats.length;
310
+ const msgCountBefore = (stateBefore.messages['chat-1'] || []).length;
311
+
312
+ await bob.sync();
313
+
314
+ const stateAfter = bob.getState();
315
+ expect(stateAfter.chats.length).toBe(chatCountBefore);
316
+ expect((stateAfter.messages['chat-1'] || []).length).toBe(msgCountBefore);
317
+ });
318
+
319
+ // -- Subscribe --
320
+
321
+ it('subscribe should fire on state changes', async () => {
322
+ const states: Array<ReturnType<typeof alice.getState>> = [];
323
+ const unsub = alice.subscribe((s) => states.push(s));
324
+
325
+ await alice.publish(
326
+ 'chat.message',
327
+ {
328
+ chatId: 'chat-1',
329
+ messageId: 'msg-2',
330
+ text: 'Another message',
331
+ sentBy: alice.identity.getIdentity()!.did,
332
+ },
333
+ { recipient: bob.identity.getIdentity()!.did },
334
+ );
335
+
336
+ unsub();
337
+
338
+ expect(states.length).toBeGreaterThanOrEqual(1);
339
+ const last = states[states.length - 1];
340
+ expect(last.messages['chat-1'].some((m) => m.text === 'Another message')).toBe(true);
341
+ });
342
+
343
+ // -- Reset --
344
+
345
+ it('reset should clear all state', () => {
346
+ const client = chatApp.createClient({ mediatorDid: MEDIATOR_DID });
347
+ client.identity.load(alice.identity.serialize());
348
+
349
+ client
350
+ .publish('chat.create', {
351
+ id: 'temp',
352
+ name: 'temp',
353
+ participants: [],
354
+ createdBy: 'test',
355
+ })
356
+ .catch(() => {
357
+ // May fail without proper contracts — fine for this test
358
+ });
359
+
360
+ client.reset();
361
+
362
+ expect(client.getState().chats).toEqual([]);
363
+ expect(client.getState().messages).toEqual({});
364
+ expect(client.identity.getIdentity()).toBeNull();
365
+ });
366
+ });
367
+ });
@@ -0,0 +1,42 @@
1
+ import type { StateListener } from './types.js';
2
+
3
+ export class StateStore<TState extends Record<string, unknown>> {
4
+ private state: TState;
5
+ private listeners = new Set<StateListener<TState>>();
6
+
7
+ constructor(initialState: TState) {
8
+ this.state = { ...initialState };
9
+ }
10
+
11
+ getState = (): TState => {
12
+ return this.state;
13
+ };
14
+
15
+ setSlice<K extends keyof TState>(key: K, value: TState[K]): void {
16
+ if (this.state[key] === value) {
17
+ return;
18
+ }
19
+
20
+ this.state = { ...this.state, [key]: value };
21
+ this.notify();
22
+ }
23
+
24
+ subscribe = (listener: StateListener<TState>): (() => void) => {
25
+ this.listeners.add(listener);
26
+
27
+ return () => {
28
+ this.listeners.delete(listener);
29
+ };
30
+ };
31
+
32
+ reset(initialState: TState): void {
33
+ this.state = { ...initialState };
34
+ this.notify();
35
+ }
36
+
37
+ private notify(): void {
38
+ for (const listener of this.listeners) {
39
+ listener(this.state);
40
+ }
41
+ }
42
+ }