@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,233 @@
1
+ import { type DecentrlIdentityKeys, signJsonObject } from '@decentrl/crypto';
2
+
3
+ export type ConnectionStatus = 'disconnected' | 'connecting' | 'authenticating' | 'connected';
4
+
5
+ export interface WebSocketTransportCallbacks {
6
+ onPendingEvents: (events: Array<{ id: string; sender_did: string; payload: string }>) => void;
7
+ onContractsUpdated: () => void;
8
+ onStatusChange: (status: ConnectionStatus) => void;
9
+ }
10
+
11
+ export interface WebSocketTransportIdentity {
12
+ did: string;
13
+ keys: DecentrlIdentityKeys;
14
+ }
15
+
16
+ type WsMessage =
17
+ | { type: 'AUTH_SUCCESS' }
18
+ | { type: 'AUTH_FAILED'; error_code: string }
19
+ | { type: 'PENDING_EVENTS'; events: Array<{ id: string; sender_did: string; payload: string }> }
20
+ | { type: 'CONTRACTS_UPDATED' }
21
+ | { type: 'PING' };
22
+
23
+ const MAX_RECONNECT_ATTEMPTS = 10;
24
+ const BASE_RECONNECT_MS = 1000;
25
+ const MAX_RECONNECT_MS = 30_000;
26
+ const JITTER_FACTOR = 0.2;
27
+
28
+ export class WebSocketTransport {
29
+ private ws: WebSocket | null = null;
30
+ private status: ConnectionStatus = 'disconnected';
31
+ private reconnectAttempt = 0;
32
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
33
+ private shouldReconnect = false;
34
+
35
+ constructor(
36
+ private getIdentity: () => WebSocketTransportIdentity | null,
37
+ private getWsUrl: () => string | null,
38
+ private callbacks: WebSocketTransportCallbacks,
39
+ ) {}
40
+
41
+ connect(): void {
42
+ if (this.ws) {
43
+ return;
44
+ }
45
+
46
+ const wsUrl = this.getWsUrl();
47
+ const identity = this.getIdentity();
48
+
49
+ if (!wsUrl || !identity) {
50
+ console.warn('[Decentrl:WS] connect() skipped — wsUrl:', !!wsUrl, 'identity:', !!identity);
51
+
52
+ return;
53
+ }
54
+
55
+ console.debug('[Decentrl:WS] Connecting to', wsUrl);
56
+ this.shouldReconnect = true;
57
+ this.setStatus('connecting');
58
+
59
+ try {
60
+ this.ws = new WebSocket(wsUrl);
61
+ } catch (err) {
62
+ console.error('[Decentrl:WS] WebSocket constructor failed:', err);
63
+ this.setStatus('disconnected');
64
+ this.scheduleReconnect();
65
+
66
+ return;
67
+ }
68
+
69
+ this.ws.onopen = () => {
70
+ console.debug('[Decentrl:WS] Socket opened, authenticating...');
71
+ this.setStatus('authenticating');
72
+ this.sendAuthenticate(identity);
73
+ };
74
+
75
+ this.ws.onmessage = (event) => {
76
+ let data: WsMessage;
77
+
78
+ try {
79
+ data = JSON.parse(event.data as string);
80
+ } catch {
81
+ return;
82
+ }
83
+
84
+ this.handleMessage(data);
85
+ };
86
+
87
+ this.ws.onclose = (event) => {
88
+ console.debug(
89
+ '[Decentrl:WS] Socket closed — code:',
90
+ event.code,
91
+ 'reason:',
92
+ event.reason,
93
+ 'willReconnect:',
94
+ this.shouldReconnect,
95
+ );
96
+ this.ws = null;
97
+ this.setStatus('disconnected');
98
+
99
+ if (this.shouldReconnect) {
100
+ this.scheduleReconnect();
101
+ }
102
+ };
103
+
104
+ this.ws.onerror = () => {
105
+ console.error('[Decentrl:WS] Socket error (onclose will follow)');
106
+ };
107
+ }
108
+
109
+ disconnect(): void {
110
+ this.shouldReconnect = false;
111
+
112
+ if (this.reconnectTimer) {
113
+ clearTimeout(this.reconnectTimer);
114
+ this.reconnectTimer = null;
115
+ }
116
+
117
+ if (this.ws) {
118
+ this.ws.onclose = null;
119
+ this.ws.close();
120
+ this.ws = null;
121
+ }
122
+
123
+ this.reconnectAttempt = 0;
124
+ this.setStatus('disconnected');
125
+ }
126
+
127
+ getStatus(): ConnectionStatus {
128
+ return this.status;
129
+ }
130
+
131
+ private sendAuthenticate(identity: WebSocketTransportIdentity): void {
132
+ const payload = {
133
+ did: identity.did,
134
+ signing_key_id: `${identity.did}#signing`,
135
+ timestamp: Date.now(),
136
+ nonce: crypto.randomUUID(),
137
+ };
138
+
139
+ const signature = signJsonObject(payload, identity.keys.signing.privateKey);
140
+
141
+ this.send({
142
+ type: 'AUTHENTICATE',
143
+ ...payload,
144
+ signature,
145
+ });
146
+ }
147
+
148
+ private handleMessage(data: WsMessage): void {
149
+ if (data.type !== 'PING') {
150
+ console.debug(
151
+ '[Decentrl:WS] Received:',
152
+ data.type,
153
+ data.type === 'PENDING_EVENTS' ? `(${data.events.length} events)` : '',
154
+ );
155
+ }
156
+
157
+ switch (data.type) {
158
+ case 'AUTH_SUCCESS':
159
+ this.reconnectAttempt = 0;
160
+ this.setStatus('connected');
161
+ break;
162
+
163
+ case 'AUTH_FAILED':
164
+ console.error('[Decentrl:WS] Auth failed:', data.error_code);
165
+ // Don't reconnect on auth failure — would fail again
166
+ this.shouldReconnect = false;
167
+ this.ws?.close();
168
+ break;
169
+
170
+ case 'PENDING_EVENTS':
171
+ this.callbacks.onPendingEvents(data.events);
172
+ break;
173
+
174
+ case 'CONTRACTS_UPDATED':
175
+ this.callbacks.onContractsUpdated();
176
+ break;
177
+
178
+ case 'PING':
179
+ this.send({ type: 'PONG', timestamp: Date.now() });
180
+ break;
181
+ }
182
+ }
183
+
184
+ private send(message: object): void {
185
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
186
+ this.ws.send(JSON.stringify(message));
187
+ }
188
+ }
189
+
190
+ private setStatus(status: ConnectionStatus): void {
191
+ if (this.status !== status) {
192
+ console.debug('[Decentrl:WS] Status:', this.status, '→', status);
193
+ this.status = status;
194
+ this.callbacks.onStatusChange(status);
195
+ }
196
+ }
197
+
198
+ private scheduleReconnect(): void {
199
+ if (this.reconnectAttempt >= MAX_RECONNECT_ATTEMPTS) {
200
+ console.warn(
201
+ '[Decentrl:WS] Max reconnect attempts reached (%d), giving up',
202
+ MAX_RECONNECT_ATTEMPTS,
203
+ );
204
+
205
+ return;
206
+ }
207
+
208
+ const backoff = Math.min(BASE_RECONNECT_MS * 2 ** this.reconnectAttempt, MAX_RECONNECT_MS);
209
+ const jitter = backoff * JITTER_FACTOR * (Math.random() * 2 - 1);
210
+ const delay = Math.max(0, backoff + jitter);
211
+
212
+ this.reconnectAttempt++;
213
+ console.debug(
214
+ '[Decentrl:WS] Reconnecting in %dms (attempt %d/%d)',
215
+ Math.round(delay),
216
+ this.reconnectAttempt,
217
+ MAX_RECONNECT_ATTEMPTS,
218
+ );
219
+ this.reconnectTimer = setTimeout(() => {
220
+ this.reconnectTimer = null;
221
+ this.ws = null;
222
+ this.connect();
223
+ }, delay);
224
+ }
225
+ }
226
+
227
+ export function deriveWsUrl(httpEndpoint: string): string {
228
+ const url = new URL(httpEndpoint);
229
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
230
+ url.pathname = `${url.pathname.replace(/\/$/, '')}/ws`;
231
+
232
+ return url.toString();
233
+ }