@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,450 @@
1
+ import { base64Decode, base64Encode, decryptString, deriveSharedSecret, encryptString, generateEncryptedTag, generateIdentityKeys, } from '@decentrl/crypto';
2
+ import { DecentrlEventStore } from '@decentrl/event-store';
3
+ import { generateDirectAuthenticatedMediatorCommand } from '@decentrl/identity/communication-channels/mediator/direct-authenticated/command/command.service';
4
+ import { createCommunicationContractRequest, decryptContractRequest, generateContractId, signCommunicationContract, } from '@decentrl/identity/communication-contract/communication-contract.service';
5
+ import { createDecentrlDidFromKeys } from '@decentrl/identity/did-decentrl/did-decentrl.service';
6
+ import { resolveDid } from '@decentrl/identity/did-resolver/resolver';
7
+ import { resolveMediatorServiceEndpoint } from '@decentrl/identity/mediator/mediator.resolver';
8
+ import axios from 'axios';
9
+ import { DecentrlSDKError } from './errors.js';
10
+ import { deserializeKeys, serializeIdentity } from './identity-serialization.js';
11
+ const DEFAULT_TIMEOUT_MS = 30_000;
12
+ const MEDIATOR_CONTRACT_LIFETIME_SECONDS = 3600 * 24 * 365; // 1 year
13
+ const DEFAULT_CONTRACT_LIFETIME_SECONDS = 3600 * 24; // 24 hours
14
+ export class DirectTransport {
15
+ identity = null;
16
+ eventStore = null;
17
+ activeContracts = [];
18
+ httpPost;
19
+ onEphemeralKeysChanged;
20
+ pendingEphemeralKeys = new Map();
21
+ knownRootSecrets = new Map(); // contractId → rootSecret (survives refreshContracts)
22
+ renewalInFlight = new Set();
23
+ supersededContracts = new Map(); // contractId → supersededAt
24
+ confirmedNewContracts = new Set(); // contractIds confirmed by counterparty events
25
+ // Staging set: filled by processContractCleanup(), drained by refreshContracts().
26
+ // Must call refreshContracts() after cleanup to apply purges.
27
+ purgedContractIds = new Set();
28
+ constructor(options) {
29
+ this.httpPost =
30
+ options?.httpPost ??
31
+ ((url, data, opts) => axios.post(url, data, { timeout: opts?.timeout ?? DEFAULT_TIMEOUT_MS }));
32
+ this.onEphemeralKeysChanged = options?.onEphemeralKeysChanged;
33
+ }
34
+ // --- Identity ---
35
+ async createIdentity(options) {
36
+ const { alias, mediatorDid } = options;
37
+ const keys = generateIdentityKeys();
38
+ const did = createDecentrlDidFromKeys(alias, keys, mediatorDid);
39
+ const mediatorEndpoint = await resolveMediatorServiceEndpoint(mediatorDid);
40
+ const { encryptedPayload, ephemeralKeyPair } = await createCommunicationContractRequest(did, mediatorDid, keys, MEDIATOR_CONTRACT_LIFETIME_SECONDS);
41
+ const registrationCommand = generateDirectAuthenticatedMediatorCommand(did, `${did}#signing`, mediatorDid, {
42
+ type: 'REQUEST_COMMUNICATION_CONTRACT',
43
+ encrypted_contract_request: encryptedPayload,
44
+ requestor_ephemeral_public_key: base64Encode(ephemeralKeyPair.publicKey),
45
+ }, keys);
46
+ const response = await this.httpPost(mediatorEndpoint, registrationCommand);
47
+ if (response.data.type !== 'SUCCESS' ||
48
+ response.data.code !== 'MEDIATOR_REGISTRATION_SUCCESS') {
49
+ throw new Error('Failed to register with mediator');
50
+ }
51
+ this.identity = {
52
+ did,
53
+ alias,
54
+ mediatorDid,
55
+ mediatorEndpoint,
56
+ keys,
57
+ mediatorContract: response.data.payload.signed_communication_contract,
58
+ };
59
+ this.eventStore = null;
60
+ return serializeIdentity(this.identity);
61
+ }
62
+ getIdentity() {
63
+ return this.identity ? serializeIdentity(this.identity) : null;
64
+ }
65
+ loadIdentity(serialized) {
66
+ this.identity = {
67
+ did: serialized.did,
68
+ alias: serialized.alias,
69
+ mediatorDid: serialized.mediatorDid,
70
+ mediatorEndpoint: serialized.mediatorEndpoint,
71
+ keys: deserializeKeys(serialized),
72
+ mediatorContract: serialized.mediatorContract,
73
+ };
74
+ this.eventStore = null;
75
+ }
76
+ loadContracts(contracts) {
77
+ this.activeContracts = [...contracts];
78
+ for (const c of contracts) {
79
+ if (c.rootSecret) {
80
+ this.knownRootSecrets.set(c.id, c.rootSecret);
81
+ }
82
+ }
83
+ this.eventStore = null;
84
+ }
85
+ loadEphemeralKeys(keys) {
86
+ this.pendingEphemeralKeys = new Map(Object.entries(keys));
87
+ }
88
+ getEphemeralKeys() {
89
+ return Object.fromEntries(this.pendingEphemeralKeys);
90
+ }
91
+ // --- Events ---
92
+ async publishEvent(envelope, options) {
93
+ const eventStore = this.requireEventStore();
94
+ await eventStore.publishEvent(envelope, {
95
+ tags: options.tags,
96
+ recipient: options.recipient,
97
+ ephemeral: options.ephemeral,
98
+ });
99
+ }
100
+ async processPendingEvents() {
101
+ const eventStore = this.requireEventStore();
102
+ return eventStore.processPendingEvents();
103
+ }
104
+ async processPreFetchedPendingEvents(rawEvents) {
105
+ const eventStore = this.requireEventStore();
106
+ return eventStore.processPreFetchedPendingEvents(rawEvents);
107
+ }
108
+ async queryEvents(options) {
109
+ const eventStore = this.requireEventStore();
110
+ return eventStore.queryEvents(options);
111
+ }
112
+ async updateEventTags(events) {
113
+ const identity = this.requireIdentity();
114
+ const encrypted = events.map((e) => ({
115
+ eventId: e.eventId,
116
+ encryptedTags: e.tags.map((tag) => generateEncryptedTag(identity.keys.signing.privateKey, tag)),
117
+ }));
118
+ const eventStore = this.requireEventStore();
119
+ await eventStore.updateEventTags(encrypted);
120
+ }
121
+ async queryUnprocessedEvents(pagination) {
122
+ const eventStore = this.requireEventStore();
123
+ return eventStore.queryUnprocessedEvents(pagination);
124
+ }
125
+ // --- Contracts ---
126
+ async requestContract(recipientDid, expiresIn = DEFAULT_CONTRACT_LIFETIME_SECONDS) {
127
+ const identity = this.requireIdentity();
128
+ const { request, ephemeralKeyPair, encryptedPayload } = await createCommunicationContractRequest(identity.did, recipientDid, identity.keys, expiresIn);
129
+ const ephemeralKeyKey = `${identity.did}|${recipientDid}|${request.communication_contract.timestamp}`;
130
+ const encryptedEphemeralKey = encryptString(base64Encode(ephemeralKeyPair.privateKey), identity.keys.storageKey);
131
+ this.pendingEphemeralKeys.set(ephemeralKeyKey, encryptedEphemeralKey);
132
+ await this.notifyEphemeralKeysChanged();
133
+ const command = generateDirectAuthenticatedMediatorCommand(identity.did, `${identity.did}#signing`, recipientDid, {
134
+ type: 'REQUEST_COMMUNICATION_CONTRACT',
135
+ encrypted_contract_request: encryptedPayload,
136
+ requestor_ephemeral_public_key: base64Encode(ephemeralKeyPair.publicKey),
137
+ }, identity.keys);
138
+ const response = await this.httpPost(identity.mediatorEndpoint, command);
139
+ if (response.data.type !== 'SUCCESS') {
140
+ throw new Error('Failed to request communication contract');
141
+ }
142
+ }
143
+ async getPendingContracts() {
144
+ const identity = this.requireIdentity();
145
+ const command = generateDirectAuthenticatedMediatorCommand(identity.did, `${identity.did}#signing`, identity.mediatorDid, {
146
+ type: 'QUERY_PENDING_COMMUNICATION_CONTRACT_REQUESTS',
147
+ pagination: { page: 0, page_size: 50 },
148
+ }, identity.keys);
149
+ const response = await this.httpPost(identity.mediatorEndpoint, command);
150
+ if (response.data.type !== 'SUCCESS') {
151
+ throw new Error('Failed to query pending contract requests');
152
+ }
153
+ return response.data.payload.pending_communication_contract_requests.map((req) => ({
154
+ id: req.id,
155
+ senderDid: req.sender_did,
156
+ encryptedPayload: req.encrypted_contract_request,
157
+ requestorEphemeralPublicKey: req.requestor_ephemeral_public_key,
158
+ }));
159
+ }
160
+ async acceptContract(pendingId, encryptedPayload, requestorEphemeralPublicKey) {
161
+ const identity = this.requireIdentity();
162
+ const contractRequest = decryptContractRequest(encryptedPayload, identity.keys.encryption.privateKey, requestorEphemeralPublicKey);
163
+ const acknowledgeCommand = generateDirectAuthenticatedMediatorCommand(identity.did, `${identity.did}#signing`, identity.mediatorDid, {
164
+ type: 'ACKNOWLEDGE_PENDING_COMMUNICATION_CONTRACT_REQUESTS',
165
+ communication_contract_ids: [pendingId],
166
+ }, identity.keys);
167
+ await this.httpPost(identity.mediatorEndpoint, acknowledgeCommand);
168
+ const { signedContract, rootSecret } = await signCommunicationContract(contractRequest, identity.keys);
169
+ const contractId = generateContractId(signedContract.communication_contract);
170
+ const saveCommand = generateDirectAuthenticatedMediatorCommand(identity.did, `${identity.did}#signing`, identity.mediatorDid, {
171
+ type: 'SAVE_COMMUNICATION_CONTRACT',
172
+ signed_communication_contract: signedContract,
173
+ }, identity.keys);
174
+ await this.httpPost(identity.mediatorEndpoint, saveCommand);
175
+ const responseCommand = generateDirectAuthenticatedMediatorCommand(identity.did, `${identity.did}#signing`, contractRequest.communication_contract.requestor_did, {
176
+ type: 'COMMUNICATION_CONTRACT_RESPONSE',
177
+ signed_communication_contract: signedContract,
178
+ }, identity.keys);
179
+ await this.httpPost(identity.mediatorEndpoint, responseCommand);
180
+ const participantDid = contractRequest.communication_contract.requestor_did;
181
+ const participantDoc = await resolveDid(participantDid);
182
+ const stored = {
183
+ id: contractId,
184
+ participantDid,
185
+ participantAlias: participantDoc?.alias?.[0],
186
+ signedCommunicationContract: signedContract,
187
+ rootSecret: base64Encode(rootSecret),
188
+ createdAt: Date.now(),
189
+ status: 'active',
190
+ };
191
+ this.knownRootSecrets.set(contractId, stored.rootSecret);
192
+ this.activeContracts = [...this.activeContracts, stored];
193
+ }
194
+ async refreshContracts() {
195
+ const identity = this.requireIdentity();
196
+ const command = generateDirectAuthenticatedMediatorCommand(identity.did, `${identity.did}#signing`, identity.mediatorDid, {
197
+ type: 'QUERY_COMMUNICATION_CONTRACTS',
198
+ filter: {},
199
+ pagination: { page: 0, page_size: 100 },
200
+ }, identity.keys);
201
+ const response = await this.httpPost(identity.mediatorEndpoint, command);
202
+ if (response.data.type !== 'SUCCESS') {
203
+ throw new Error('Failed to query communication contracts');
204
+ }
205
+ const contracts = [];
206
+ for (const contract of response.data.payload.communication_contracts) {
207
+ const signed = contract.signed_communication_contract;
208
+ const participantDid = signed.communication_contract.requestor_did === identity.did
209
+ ? signed.communication_contract.recipient_did
210
+ : signed.communication_contract.requestor_did;
211
+ const isExpired = signed.communication_contract.expires_at * 1000 < Date.now();
212
+ const contractId = generateContractId(signed.communication_contract);
213
+ let rootSecret = '';
214
+ const existingContract = this.activeContracts.find((c) => c.id === contractId);
215
+ const knownSecret = this.knownRootSecrets.get(contractId);
216
+ if (knownSecret) {
217
+ rootSecret = knownSecret;
218
+ }
219
+ else if (existingContract?.rootSecret) {
220
+ rootSecret = existingContract.rootSecret;
221
+ }
222
+ else {
223
+ const ephemeralKeyKey = `${identity.did}|${participantDid}|${signed.communication_contract.timestamp}`;
224
+ const encryptedEphemeralKey = this.pendingEphemeralKeys.get(ephemeralKeyKey);
225
+ if (encryptedEphemeralKey) {
226
+ const ephemeralPrivateKeyB64 = decryptString(encryptedEphemeralKey, identity.keys.storageKey);
227
+ const ephemeralPrivateKey = base64Decode(ephemeralPrivateKeyB64);
228
+ // DH(requestor_ephemeral_private, recipient_ephemeral_public)
229
+ const recipientEphemeralPublicKey = signed.communication_contract.recipient_encryption_public_key;
230
+ if (recipientEphemeralPublicKey) {
231
+ const secret = deriveSharedSecret(ephemeralPrivateKey, base64Decode(recipientEphemeralPublicKey));
232
+ rootSecret = base64Encode(secret);
233
+ this.knownRootSecrets.set(contractId, rootSecret);
234
+ this.pendingEphemeralKeys.delete(ephemeralKeyKey);
235
+ await this.notifyEphemeralKeysChanged();
236
+ }
237
+ }
238
+ }
239
+ let participantAlias = existingContract?.participantAlias;
240
+ if (!participantAlias) {
241
+ const participantDoc = await resolveDid(participantDid);
242
+ participantAlias = participantDoc?.alias?.[0];
243
+ }
244
+ contracts.push({
245
+ id: contractId,
246
+ participantDid,
247
+ participantAlias,
248
+ signedCommunicationContract: signed,
249
+ rootSecret,
250
+ createdAt: Date.now(),
251
+ status: isExpired ? 'expired' : 'active',
252
+ });
253
+ }
254
+ this.activeContracts = contracts.filter((c) => c.status === 'active' && !this.purgedContractIds.has(c.id));
255
+ this.purgedContractIds.clear();
256
+ return contracts;
257
+ }
258
+ getActiveContracts() {
259
+ return this.activeContracts;
260
+ }
261
+ async processAutoRenewals(threshold = 0.2) {
262
+ await this.initiateRenewals(threshold);
263
+ await this.autoAcceptRenewals();
264
+ }
265
+ async initiateRenewals(threshold) {
266
+ const identity = this.requireIdentity();
267
+ const now = Date.now();
268
+ for (const contract of this.activeContracts) {
269
+ const cc = contract.signedCommunicationContract.communication_contract;
270
+ const lifetimeMs = (cc.expires_at - cc.timestamp) * 1000;
271
+ const elapsedMs = now - cc.timestamp * 1000;
272
+ if (elapsedMs < lifetimeMs * (1 - threshold)) {
273
+ continue;
274
+ }
275
+ // Deterministic initiator: lower DID always sends the renewal request
276
+ if (identity.did >= contract.participantDid) {
277
+ continue;
278
+ }
279
+ if (this.renewalInFlight.has(contract.participantDid)) {
280
+ continue;
281
+ }
282
+ this.renewalInFlight.add(contract.participantDid);
283
+ try {
284
+ const lifetimeSeconds = cc.expires_at - cc.timestamp;
285
+ await this.requestContract(contract.participantDid, lifetimeSeconds);
286
+ }
287
+ catch (err) {
288
+ console.error('[Decentrl] Auto-renewal request failed:', err);
289
+ }
290
+ finally {
291
+ this.renewalInFlight.delete(contract.participantDid);
292
+ }
293
+ }
294
+ }
295
+ async autoAcceptRenewals() {
296
+ const identity = this.requireIdentity();
297
+ const activeDids = new Map();
298
+ for (const contract of this.activeContracts) {
299
+ const existing = activeDids.get(contract.participantDid);
300
+ if (!existing ||
301
+ contract.signedCommunicationContract.communication_contract.expires_at >
302
+ existing.signedCommunicationContract.communication_contract.expires_at) {
303
+ activeDids.set(contract.participantDid, contract);
304
+ }
305
+ }
306
+ let pending;
307
+ try {
308
+ pending = await this.getPendingContracts();
309
+ }
310
+ catch (err) {
311
+ console.error('[Decentrl] Auto-renewal: failed to fetch pending contracts:', err);
312
+ return;
313
+ }
314
+ for (const req of pending) {
315
+ const existingContract = activeDids.get(req.senderDid);
316
+ if (!existingContract) {
317
+ continue;
318
+ }
319
+ try {
320
+ const decrypted = decryptContractRequest(req.encryptedPayload, identity.keys.encryption.privateKey, req.requestorEphemeralPublicKey);
321
+ const existingCc = existingContract.signedCommunicationContract.communication_contract;
322
+ const existingLifetime = existingCc.expires_at - existingCc.timestamp;
323
+ const newCc = decrypted.communication_contract;
324
+ const newLifetime = newCc.expires_at - newCc.timestamp;
325
+ if (existingLifetime !== newLifetime) {
326
+ continue;
327
+ }
328
+ await this.acceptContract(req.id, req.encryptedPayload, req.requestorEphemeralPublicKey);
329
+ }
330
+ catch (err) {
331
+ console.error('[Decentrl] Auto-renewal accept failed:', err);
332
+ }
333
+ }
334
+ }
335
+ async processContractCleanup() {
336
+ const now = Date.now();
337
+ const CLEANUP_TIMEOUT_MS = 7 * 24 * 60 * 60 * 1000;
338
+ // Group active contracts by participantDid
339
+ const byParticipant = new Map();
340
+ for (const contract of this.activeContracts) {
341
+ const list = byParticipant.get(contract.participantDid) ?? [];
342
+ list.push(contract);
343
+ byParticipant.set(contract.participantDid, list);
344
+ }
345
+ const toArchive = [];
346
+ for (const [, contracts] of byParticipant) {
347
+ if (contracts.length < 2) {
348
+ continue;
349
+ }
350
+ contracts.sort((a, b) => b.signedCommunicationContract.communication_contract.expires_at -
351
+ a.signedCommunicationContract.communication_contract.expires_at);
352
+ const newestId = contracts[0].id;
353
+ for (let i = 1; i < contracts.length; i++) {
354
+ const old = contracts[i];
355
+ if (!this.supersededContracts.has(old.id)) {
356
+ this.supersededContracts.set(old.id, now);
357
+ }
358
+ const supersededAt = this.supersededContracts.get(old.id);
359
+ const isTimedOut = now - supersededAt >= CLEANUP_TIMEOUT_MS;
360
+ const isCounterpartyConfirmed = this.confirmedNewContracts.has(newestId);
361
+ if (isTimedOut || isCounterpartyConfirmed) {
362
+ this.purgedContractIds.add(old.id);
363
+ this.supersededContracts.delete(old.id);
364
+ toArchive.push({ contract: old, supersededBy: newestId });
365
+ }
366
+ }
367
+ }
368
+ // Remove purged contracts immediately so getActiveContracts() reflects the change.
369
+ // refreshContracts() will also filter them (and clear the staging set).
370
+ if (this.purgedContractIds.size > 0) {
371
+ this.activeContracts = this.activeContracts.filter((c) => !this.purgedContractIds.has(c.id));
372
+ }
373
+ // Publish archived events (best-effort)
374
+ if (toArchive.length > 0 && this.identity) {
375
+ const identity = this.identity;
376
+ for (const { contract, supersededBy } of toArchive) {
377
+ try {
378
+ const cc = contract.signedCommunicationContract.communication_contract;
379
+ await this.requireEventStore().publishEvent({
380
+ type: 'decentrl.contract.archived',
381
+ data: {
382
+ contractId: contract.id,
383
+ participantDid: contract.participantDid,
384
+ participantAlias: contract.participantAlias,
385
+ timestamp: cc.timestamp,
386
+ expiresAt: cc.expires_at,
387
+ supersededAt: now,
388
+ supersededBy,
389
+ },
390
+ meta: {
391
+ senderDid: identity.did,
392
+ timestamp: now,
393
+ eventId: crypto.randomUUID(),
394
+ },
395
+ }, { tags: ['decentrl.contract.archived'] });
396
+ }
397
+ catch (err) {
398
+ console.error('[Decentrl] Failed to archive contract:', err);
399
+ }
400
+ }
401
+ }
402
+ }
403
+ async getContractHistory() {
404
+ const eventStore = this.requireEventStore();
405
+ const result = await eventStore.queryEvents({
406
+ tags: ['decentrl.contract.archived'],
407
+ });
408
+ return result.data
409
+ .filter((e) => {
410
+ const d = e.data;
411
+ return (d != null &&
412
+ typeof d === 'object' &&
413
+ 'contractId' in d &&
414
+ 'participantDid' in d &&
415
+ 'supersededBy' in d);
416
+ })
417
+ .map((e) => e.data);
418
+ }
419
+ dispose() {
420
+ this.eventStore = null;
421
+ }
422
+ // --- Internal helpers ---
423
+ async notifyEphemeralKeysChanged() {
424
+ await this.onEphemeralKeysChanged?.(Object.fromEntries(this.pendingEphemeralKeys));
425
+ }
426
+ requireIdentity() {
427
+ if (!this.identity) {
428
+ throw new DecentrlSDKError('Identity not initialized. Call createIdentity() or loadIdentity() first.', 'IDENTITY_NOT_INITIALIZED');
429
+ }
430
+ return this.identity;
431
+ }
432
+ requireEventStore() {
433
+ const identity = this.requireIdentity();
434
+ if (!this.eventStore) {
435
+ this.eventStore = new DecentrlEventStore({
436
+ identity: {
437
+ did: identity.did,
438
+ keys: identity.keys,
439
+ mediatorEndpoint: identity.mediatorEndpoint,
440
+ mediatorDid: identity.mediatorDid,
441
+ },
442
+ communicationContracts: () => this.activeContracts,
443
+ onContractUsed: (contractId) => {
444
+ this.confirmedNewContracts.add(contractId);
445
+ },
446
+ });
447
+ }
448
+ return this.eventStore;
449
+ }
450
+ }
@@ -0,0 +1,7 @@
1
+ export type DecentrlSDKErrorCode = 'IDENTITY_NOT_INITIALIZED' | 'IDENTITY_ALREADY_EXISTS' | 'CONTRACT_NOT_FOUND' | 'SCHEMA_VALIDATION_FAILED' | 'UNKNOWN_EVENT_TYPE' | 'SYNC_FAILED' | 'PUBLISH_FAILED' | 'QUERY_FAILED' | 'QUERY_NOT_SUPPORTED' | 'MEDIATOR_ERROR' | 'NO_TRANSPORT';
2
+ export declare class DecentrlSDKError extends Error {
3
+ code: DecentrlSDKErrorCode;
4
+ details?: unknown | undefined;
5
+ constructor(message: string, code: DecentrlSDKErrorCode, details?: unknown | undefined);
6
+ }
7
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAC7B,0BAA0B,GAC1B,yBAAyB,GACzB,oBAAoB,GACpB,0BAA0B,GAC1B,oBAAoB,GACpB,aAAa,GACb,gBAAgB,GAChB,cAAc,GACd,qBAAqB,GACrB,gBAAgB,GAChB,cAAc,CAAC;AAElB,qBAAa,gBAAiB,SAAQ,KAAK;IAGlC,IAAI,EAAE,oBAAoB;IAC1B,OAAO,CAAC,EAAE,OAAO;gBAFxB,OAAO,EAAE,MAAM,EACR,IAAI,EAAE,oBAAoB,EAC1B,OAAO,CAAC,EAAE,OAAO,YAAA;CAKzB"}
package/dist/errors.js ADDED
@@ -0,0 +1,10 @@
1
+ export class DecentrlSDKError extends Error {
2
+ code;
3
+ details;
4
+ constructor(message, code, details) {
5
+ super(message);
6
+ this.code = code;
7
+ this.details = details;
8
+ this.name = 'DecentrlSDKError';
9
+ }
10
+ }
@@ -0,0 +1,19 @@
1
+ import type { StateStore } from './state-store.js';
2
+ import type { EventDefinitions, EventEnvelope, EventMeta, InferState, StateDefinitions } from './types.js';
3
+ export declare class EventProcessor<TEvents extends EventDefinitions, TState extends StateDefinitions<TEvents>> {
4
+ private eventDefinitions;
5
+ private stateDefinitions;
6
+ private stateStore;
7
+ private processedEventIds;
8
+ private eventListeners;
9
+ constructor(eventDefinitions: TEvents, stateDefinitions: TState, stateStore: StateStore<InferState<TState>>);
10
+ onEvent(listener: (envelope: EventEnvelope) => void): () => void;
11
+ validate(eventType: string, data: unknown): void;
12
+ computeTags(eventType: string, data: unknown): string[];
13
+ computeTagsSafe(eventType: string, data: unknown): string[];
14
+ buildEnvelope(eventType: string, data: unknown, meta: EventMeta): EventEnvelope;
15
+ processEvent(envelope: EventEnvelope): boolean;
16
+ processBatch(envelopes: EventEnvelope[]): number;
17
+ reset(): void;
18
+ }
19
+ //# sourceMappingURL=event-processor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-processor.d.ts","sourceRoot":"","sources":["../src/event-processor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EACX,gBAAgB,EAChB,aAAa,EACb,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,MAAM,YAAY,CAAC;AAIpB,qBAAa,cAAc,CAC1B,OAAO,SAAS,gBAAgB,EAChC,MAAM,SAAS,gBAAgB,CAAC,OAAO,CAAC;IAMvC,OAAO,CAAC,gBAAgB;IACxB,OAAO,CAAC,gBAAgB;IACxB,OAAO,CAAC,UAAU;IANnB,OAAO,CAAC,iBAAiB,CAAqB;IAC9C,OAAO,CAAC,cAAc,CAAgD;gBAG7D,gBAAgB,EAAE,OAAO,EACzB,gBAAgB,EAAE,MAAM,EACxB,UAAU,EAAE,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IAGnD,OAAO,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,IAAI,GAAG,MAAM,IAAI;IAMhE,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI;IAoBhD,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE;IAYvD,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE;IAQ3D,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,GAAG,aAAa;IAI/E,YAAY,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO;IAwC9C,YAAY,CAAC,SAAS,EAAE,aAAa,EAAE,GAAG,MAAM;IAYhD,KAAK,IAAI,IAAI;CAIb"}
@@ -0,0 +1,93 @@
1
+ import { DecentrlSDKError } from './errors.js';
2
+ import { evaluateTagTemplates } from './tag-templates.js';
3
+ const MAX_DEDUP_SIZE = 10_000;
4
+ export class EventProcessor {
5
+ eventDefinitions;
6
+ stateDefinitions;
7
+ stateStore;
8
+ processedEventIds = new Set();
9
+ eventListeners = new Set();
10
+ constructor(eventDefinitions, stateDefinitions, stateStore) {
11
+ this.eventDefinitions = eventDefinitions;
12
+ this.stateDefinitions = stateDefinitions;
13
+ this.stateStore = stateStore;
14
+ }
15
+ onEvent(listener) {
16
+ this.eventListeners.add(listener);
17
+ return () => this.eventListeners.delete(listener);
18
+ }
19
+ validate(eventType, data) {
20
+ const definition = this.eventDefinitions[eventType];
21
+ if (!definition) {
22
+ throw new DecentrlSDKError(`Unknown event type: ${eventType}`, 'UNKNOWN_EVENT_TYPE', {
23
+ eventType,
24
+ });
25
+ }
26
+ const result = definition.schema.safeParse(data);
27
+ if (!result.success) {
28
+ throw new DecentrlSDKError(`Schema validation failed for event type "${eventType}": ${result.error.message}`, 'SCHEMA_VALIDATION_FAILED', { eventType, errors: result.error.issues });
29
+ }
30
+ }
31
+ computeTags(eventType, data) {
32
+ const definition = this.eventDefinitions[eventType];
33
+ if (!definition) {
34
+ throw new DecentrlSDKError(`Unknown event type: ${eventType}`, 'UNKNOWN_EVENT_TYPE', {
35
+ eventType,
36
+ });
37
+ }
38
+ return evaluateTagTemplates(definition.tags, data);
39
+ }
40
+ computeTagsSafe(eventType, data) {
41
+ try {
42
+ return this.computeTags(eventType, data);
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ }
48
+ buildEnvelope(eventType, data, meta) {
49
+ return { type: eventType, data, meta };
50
+ }
51
+ processEvent(envelope) {
52
+ const isEphemeral = envelope.meta.ephemeral === true;
53
+ if (!isEphemeral) {
54
+ if (this.processedEventIds.has(envelope.meta.eventId)) {
55
+ return false;
56
+ }
57
+ if (this.processedEventIds.size >= MAX_DEDUP_SIZE) {
58
+ // Set iterates in insertion order — first value is the oldest
59
+ const oldest = this.processedEventIds.values().next().value;
60
+ this.processedEventIds.delete(oldest);
61
+ }
62
+ this.processedEventIds.add(envelope.meta.eventId);
63
+ }
64
+ const currentState = this.stateStore.getState();
65
+ for (const sliceKey of Object.keys(this.stateDefinitions)) {
66
+ const sliceDef = this.stateDefinitions[sliceKey];
67
+ // Dynamic reducer lookup — TS can't prove type safety for runtime event type dispatch
68
+ const reducer = sliceDef.reduce[envelope.type];
69
+ if (reducer) {
70
+ const currentSlice = currentState[sliceKey];
71
+ const newSlice = reducer(currentSlice, envelope.data, envelope.meta);
72
+ this.stateStore.setSlice(sliceKey, newSlice);
73
+ }
74
+ }
75
+ for (const listener of this.eventListeners) {
76
+ listener(envelope);
77
+ }
78
+ return true;
79
+ }
80
+ processBatch(envelopes) {
81
+ let newCount = 0;
82
+ for (const envelope of envelopes) {
83
+ if (this.processEvent(envelope)) {
84
+ newCount++;
85
+ }
86
+ }
87
+ return newCount;
88
+ }
89
+ reset() {
90
+ this.processedEventIds.clear();
91
+ this.eventListeners.clear();
92
+ }
93
+ }
@@ -0,0 +1,22 @@
1
+ import type { DecentrlTransport } from './transport.js';
2
+ import type { IdentityState, SerializedIdentity } from './types.js';
3
+ type IdentityChangeListener = (identity: IdentityState | null) => void;
4
+ export declare class IdentityManager {
5
+ private identity;
6
+ private listeners;
7
+ private transport;
8
+ setTransport(transport: DecentrlTransport): void;
9
+ getIdentity(): IdentityState | null;
10
+ requireIdentity(): IdentityState;
11
+ create(options: {
12
+ alias: string;
13
+ mediatorDid: string;
14
+ }): Promise<IdentityState>;
15
+ load(serialized: SerializedIdentity): IdentityState;
16
+ serialize(): SerializedIdentity;
17
+ reset(): void;
18
+ onChange(listener: IdentityChangeListener): () => void;
19
+ private notify;
20
+ }
21
+ export {};
22
+ //# sourceMappingURL=identity-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-manager.d.ts","sourceRoot":"","sources":["../src/identity-manager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEpE,KAAK,sBAAsB,GAAG,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,KAAK,IAAI,CAAC;AAEvE,qBAAa,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,SAAS,CAAqC;IACtD,OAAO,CAAC,SAAS,CAAkC;IAEnD,YAAY,CAAC,SAAS,EAAE,iBAAiB,GAAG,IAAI;IAIhD,WAAW,IAAI,aAAa,GAAG,IAAI;IAInC,eAAe,IAAI,aAAa;IAW1B,MAAM,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,CAAC;IAiBrF,IAAI,CAAC,UAAU,EAAE,kBAAkB,GAAG,aAAa;IAkBnD,SAAS,IAAI,kBAAkB;IAM/B,KAAK,IAAI,IAAI;IAKb,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,GAAG,MAAM,IAAI;IAQtD,OAAO,CAAC,MAAM;CAKd"}