@decentrl/event-store 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.
@@ -0,0 +1,4 @@
1
+
2
+ > @decentrl/event-store@0.1.0 build /home/runner/work/decentrl-network/decentrl-network/packages/shared-utils/event-store
3
+ > tsc
4
+
package/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # @decentrl/event-store
2
+
3
+ A library for event-driven storage and communication in the decentrl ecosystem. This library abstracts away the complexity of event handling, encryption, and mediator communication, allowing you to focus on business logic.
4
+
5
+ ## Features
6
+
7
+ - **Event Publishing**: Store events locally and optionally send to recipients
8
+ - **Event Querying**: Retrieve stored events with filtering and pagination
9
+ - **Pending Event Processing**: Handle incoming events from other participants
10
+ - **Automatic Encryption/Decryption**: Transparent handling of cryptographic operations
11
+ - **Type Safety**: Full TypeScript support with generic event types
12
+ - **Error Handling**: Consistent error handling with detailed error information
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pnpm add @decentrl/event-store
18
+ ```
19
+
20
+ ## Basic Usage
21
+
22
+ ### 1. Setup Event Store
23
+
24
+ ```typescript
25
+ import { DecentrlEventStore, EventStoreConfig } from '@decentrl/event-store';
26
+ import { useIdentityStore } from './stores/identityStore';
27
+ import { useSignedCommunicationContractsStore } from './stores/contractsStore';
28
+
29
+ const eventStoreConfig: EventStoreConfig = {
30
+ identity: {
31
+ did: identity.identityDid,
32
+ keys: identity.getIdentityKeys(),
33
+ mediatorEndpoint: identity.identityMediatorEndpoint,
34
+ mediatorDid: identity.mediatorDid,
35
+ },
36
+ communicationContracts: () => contractsStore.getActiveContracts(),
37
+ };
38
+
39
+ const eventStore = new DecentrlEventStore(eventStoreConfig);
40
+ ```
41
+
42
+ ### 2. Define Event Types
43
+
44
+ ```typescript
45
+ interface ChatCreateEvent {
46
+ type: "chat.create";
47
+ data: {
48
+ id: string;
49
+ participants: [string, string];
50
+ createdBy: string;
51
+ createdAt: Date;
52
+ };
53
+ }
54
+
55
+ interface MessageSendEvent {
56
+ type: "message.send";
57
+ data: {
58
+ id: string;
59
+ chatId: string;
60
+ content: string;
61
+ sender: string;
62
+ timestamp: Date;
63
+ };
64
+ }
65
+
66
+ type ChatEvent = ChatCreateEvent | MessageSendEvent;
67
+ ```
68
+
69
+ ### 3. Publish Events
70
+
71
+ ```typescript
72
+ // Publish event locally and send to recipient
73
+ await eventStore.publishEvent(chatCreateEvent, {
74
+ recipient: recipientDid,
75
+ tags: ["chat", `chat.${chatId}`, `participant.${recipientDid}`]
76
+ });
77
+
78
+ // Publish event locally only
79
+ await eventStore.publishEvent(messageEvent, {
80
+ tags: [`chat.${chatId}`]
81
+ });
82
+ ```
83
+
84
+ ### 4. Query Events
85
+
86
+ ```typescript
87
+ // Query all chat creation events
88
+ const chatEvents = await eventStore.queryEvents<ChatCreateEvent>({
89
+ tags: ["chat"]
90
+ });
91
+
92
+ // Query messages for specific chat
93
+ const messageEvents = await eventStore.queryEvents<MessageSendEvent>({
94
+ tags: [`chat.${chatId}`]
95
+ });
96
+
97
+ // Query with pagination and filters
98
+ const events = await eventStore.queryEvents<ChatEvent>({
99
+ tags: ["chat"],
100
+ participantDid: "did:decentrl:participant123",
101
+ afterTimestamp: Date.now() - 86400000, // Last 24 hours
102
+ pagination: { page: 0, pageSize: 50 }
103
+ });
104
+ ```
105
+
106
+ ### 5. Process Pending Events
107
+
108
+ ```typescript
109
+ // Process all pending events from known contacts
110
+ const newEvents = await eventStore.processPendingEvents<ChatEvent>();
111
+
112
+ newEvents.forEach(event => {
113
+ if (event.type === "chat.create") {
114
+ // Handle new chat
115
+ } else if (event.type === "message.send") {
116
+ // Handle new message
117
+ }
118
+ });
119
+ ```
120
+
121
+ ## API Reference
122
+
123
+ ### DecentrlEventStore
124
+
125
+ #### `publishEvent<T>(event: T, options: PublishOptions): Promise<void>`
126
+
127
+ Publishes an event, storing it locally and optionally sending to a recipient.
128
+
129
+ **Parameters:**
130
+ - `event`: The event object to publish
131
+ - `options.recipient?`: DID of the recipient (if sending to someone)
132
+ - `options.tags`: Array of tags for querying the event later
133
+
134
+ #### `queryEvents<T>(options?: QueryOptions): Promise<T[]>`
135
+
136
+ Queries stored events from the mediator.
137
+
138
+ **Parameters:**
139
+ - `options.tags?`: Filter by encrypted tags
140
+ - `options.participantDid?`: Filter by participant DID
141
+ - `options.afterTimestamp?`: Filter events after timestamp
142
+ - `options.beforeTimestamp?`: Filter events before timestamp
143
+ - `options.pagination?`: Pagination options
144
+
145
+ #### `processPendingEvents<T>(): Promise<T[]>`
146
+
147
+ Processes pending events from other participants and stores them locally.
148
+
149
+ **Returns:** Array of new events that were processed
150
+
151
+ ## Error Handling
152
+
153
+ The library provides structured error handling with the `EventStoreError` class:
154
+
155
+ ```typescript
156
+ import { EventStoreError } from '@decentrl/event-store';
157
+
158
+ try {
159
+ await eventStore.publishEvent(event, options);
160
+ } catch (error) {
161
+ if (error instanceof EventStoreError) {
162
+ console.error(`Event store error [${error.code}]:`, error.message);
163
+ console.error('Details:', error.details);
164
+ }
165
+ }
166
+ ```
167
+
168
+ ## Benefits Over Manual Implementation
169
+
170
+ - **90% Less Boilerplate**: Eliminates repetitive encryption, command generation, and HTTP handling
171
+ - **Consistent Error Handling**: Centralized error handling with detailed error information
172
+ - **Type Safety**: Full TypeScript support with generic event types
173
+ - **Testing**: Much easier to unit test business logic without infrastructure concerns
174
+ - **Maintainability**: Clear separation between business logic and infrastructure
175
+
176
+ ## Example: Before vs After
177
+
178
+ **Before (with manual implementation):**
179
+ ```typescript
180
+ // 140+ lines of infrastructure code for sending a message
181
+ sendMessage: async (chatId: string, content: string) => {
182
+ // Identity validation...
183
+ // Contract lookup...
184
+ // Encryption key derivation...
185
+ // Event encryption for recipient...
186
+ // Command generation...
187
+ // HTTP request...
188
+ // Self-encryption...
189
+ // Tag generation...
190
+ // Local storage command...
191
+ // Another HTTP request...
192
+ // Error handling...
193
+ }
194
+ ```
195
+
196
+ **After (with event store):**
197
+ ```typescript
198
+ // ~15 lines focused on business logic
199
+ sendMessage: async (chatId: string, content: string) => {
200
+ const message = { id: uuid(), chatId, content, sender: userDid, timestamp: new Date() };
201
+ const event = { type: "message.send", data: message };
202
+
203
+ await eventStore.publishEvent(event, {
204
+ recipient: recipientDid,
205
+ tags: [`chat.${chatId}`]
206
+ });
207
+
208
+ addMessageToState(message);
209
+ }
210
+ ```
211
+
212
+ ## Integration with Zustand Stores
213
+
214
+ See `example-integration.ts` for a complete example of how to refactor an existing Zustand store to use the event store library.
@@ -0,0 +1,64 @@
1
+ import { type EventStoreConfig, type PaginatedResult, type PublishOptions, type QueryOptions } from './types';
2
+ export declare class DecentrlEventStore {
3
+ private config;
4
+ constructor(config: EventStoreConfig);
5
+ /**
6
+ * Publish an event - stores locally and optionally sends to recipient
7
+ */
8
+ publishEvent<T>(event: T, options: PublishOptions): Promise<void>;
9
+ /**
10
+ * Query stored events from mediator
11
+ */
12
+ queryEvents<T>(options?: QueryOptions): Promise<PaginatedResult<T>>;
13
+ /**
14
+ * Process pending events from other participants
15
+ */
16
+ processPendingEvents<T>(): Promise<T[]>;
17
+ /**
18
+ * Process pre-fetched pending events (e.g. from WebSocket push).
19
+ * Same logic as processPendingEvents but skips the HTTP query.
20
+ */
21
+ processPreFetchedPendingEvents<T>(rawEvents: Array<{
22
+ id: string;
23
+ sender_did: string;
24
+ payload: string;
25
+ }>): Promise<T[]>;
26
+ /**
27
+ * Update event tags on the mediator (idempotent replace).
28
+ */
29
+ updateEventTags(events: Array<{
30
+ eventId: string;
31
+ encryptedTags: string[];
32
+ }>): Promise<void>;
33
+ /**
34
+ * Query events that haven't been processed by the app yet.
35
+ * Returns mediator event IDs alongside decrypted data for tag updates.
36
+ */
37
+ queryUnprocessedEvents<T>(pagination?: {
38
+ page: number;
39
+ pageSize: number;
40
+ }): Promise<PaginatedResult<T & {
41
+ _mediatorEventId?: string;
42
+ }>>;
43
+ /**
44
+ * Shared logic: filter by known senders, decrypt, store locally, acknowledge.
45
+ */
46
+ private decryptStoreAndAck;
47
+ /**
48
+ * Send event to recipient via two-way private channel
49
+ */
50
+ private sendToRecipient;
51
+ /**
52
+ * Store event locally with encrypted tags
53
+ */
54
+ private storeLocally;
55
+ /**
56
+ * Store received event with appropriate tags
57
+ */
58
+ private storeReceivedEvent;
59
+ /**
60
+ * Acknowledge processed pending events
61
+ */
62
+ private acknowledgePendingEvents;
63
+ }
64
+ //# sourceMappingURL=event-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-store.d.ts","sourceRoot":"","sources":["../src/event-store.ts"],"names":[],"mappings":"AAmBA,OAAO,EACN,KAAK,gBAAgB,EAErB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,MAAM,SAAS,CAAC;AAQjB,qBAAa,kBAAkB;IAClB,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,gBAAgB;IAE5C;;OAEG;IACG,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBvE;;OAEG;IACG,WAAW,CAAC,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IA2E7E;;OAEG;IACG,oBAAoB,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC;IAiD7C;;;OAGG;IACG,8BAA8B,CAAC,CAAC,EACrC,SAAS,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,GACnE,OAAO,CAAC,CAAC,EAAE,CAAC;IAgBf;;OAEG;IACG,eAAe,CACpB,MAAM,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,GACzD,OAAO,CAAC,IAAI,CAAC;IAoBhB;;;OAGG;IACG,sBAAsB,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE;QAC5C,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC,GAAG;QAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAkE/D;;OAEG;YACW,kBAAkB;IA8EhC;;OAEG;YACW,eAAe;IAkD7B;;OAEG;YACW,YAAY;IA2C1B;;OAEG;YACW,kBAAkB;IAyChC;;OAEG;YACW,wBAAwB;CAmBtC"}
@@ -0,0 +1,355 @@
1
+ import { base64Decode, decryptString, encryptString, generateEncryptedTag, multibaseDecode, signJsonObject, verifyJsonSignature, } from '@decentrl/crypto';
2
+ import { generateDirectAuthenticatedMediatorCommand, generateTwoWayPrivateMediatorCommand, } from '@decentrl/identity/communication-channels/mediator/direct-authenticated/command/command.service';
3
+ import axiosModule from 'axios';
4
+ import { EventStoreError, } from './types';
5
+ const DEFAULT_TIMEOUT_MS = 30_000;
6
+ const axios = {
7
+ post: (url, data) => axiosModule.post(url, data, { timeout: DEFAULT_TIMEOUT_MS }),
8
+ };
9
+ export class DecentrlEventStore {
10
+ config;
11
+ constructor(config) {
12
+ this.config = config;
13
+ }
14
+ /**
15
+ * Publish an event - stores locally and optionally sends to recipient
16
+ */
17
+ async publishEvent(event, options) {
18
+ try {
19
+ // 1. Handle recipient delivery (if specified)
20
+ if (options.recipient) {
21
+ await this.sendToRecipient(event, options.recipient);
22
+ }
23
+ // 2. Store locally for own queries (skip for ephemeral events)
24
+ if (!options.ephemeral) {
25
+ await this.storeLocally(event, options.tags, options.recipient);
26
+ }
27
+ }
28
+ catch (error) {
29
+ throw new EventStoreError(`Failed to publish event: ${error instanceof Error ? error.message : String(error)}`, 'PUBLISH_FAILED', { event, options, error });
30
+ }
31
+ }
32
+ /**
33
+ * Query stored events from mediator
34
+ */
35
+ async queryEvents(options = {}) {
36
+ try {
37
+ const { identity } = this.config;
38
+ const identityKeys = identity.keys;
39
+ const encryptedTags = options.tags?.map((tag) => generateEncryptedTag(identityKeys.signing.privateKey, tag));
40
+ const page = options.pagination?.page ?? 0;
41
+ const pageSize = options.pagination?.pageSize ?? 100;
42
+ const queryCommand = generateDirectAuthenticatedMediatorCommand(identity.did, `${identity.did}#signing`, identity.mediatorDid, {
43
+ type: 'QUERY_EVENTS',
44
+ filter: {
45
+ encrypted_tags: encryptedTags,
46
+ participant_did: options.participantDid,
47
+ after_timestamp: options.afterTimestamp,
48
+ before_timestamp: options.beforeTimestamp,
49
+ unprocessed_only: options.unprocessedOnly,
50
+ },
51
+ pagination: { page, page_size: pageSize },
52
+ }, identityKeys);
53
+ const response = await axios.post(identity.mediatorEndpoint, queryCommand);
54
+ if (response.data.type !== 'SUCCESS') {
55
+ throw new EventStoreError('Query failed', 'QUERY_FAILED', response.data);
56
+ }
57
+ const storageKey = identityKeys.storageKey;
58
+ const data = [];
59
+ for (const event of response.data.payload.events) {
60
+ try {
61
+ const decrypted = decryptString(event.payload, storageKey);
62
+ data.push(JSON.parse(decrypted));
63
+ }
64
+ catch (error) {
65
+ console.warn(`[EventStore] Decryption failure for event: ${error instanceof Error ? error.message : String(error)}`);
66
+ }
67
+ }
68
+ return {
69
+ data,
70
+ pagination: {
71
+ page: response.data.payload.pagination.page,
72
+ pageSize: response.data.payload.pagination.page_size,
73
+ total: response.data.payload.pagination.total,
74
+ },
75
+ };
76
+ }
77
+ catch (error) {
78
+ if (error instanceof EventStoreError) {
79
+ throw error;
80
+ }
81
+ throw new EventStoreError(`Failed to query events: ${error instanceof Error ? error.message : String(error)}`, 'QUERY_FAILED', { options, error });
82
+ }
83
+ }
84
+ /**
85
+ * Process pending events from other participants
86
+ */
87
+ async processPendingEvents() {
88
+ try {
89
+ const { identity, communicationContracts } = this.config;
90
+ const activeContracts = communicationContracts();
91
+ if (activeContracts.length === 0) {
92
+ return [];
93
+ }
94
+ // Query pending events
95
+ const queryCommand = generateDirectAuthenticatedMediatorCommand(identity.did, `${identity.did}#signing`, identity.mediatorDid, {
96
+ type: 'QUERY_PENDING_EVENTS',
97
+ filter: {},
98
+ pagination: { page: 0, page_size: 100 },
99
+ }, identity.keys);
100
+ const response = await axios.post(identity.mediatorEndpoint, queryCommand);
101
+ if (response.data.type !== 'SUCCESS') {
102
+ throw new EventStoreError('Failed to query pending events', 'PENDING_QUERY_FAILED', response.data);
103
+ }
104
+ return await this.decryptStoreAndAck(response.data.payload.pending_events);
105
+ }
106
+ catch (error) {
107
+ if (error instanceof EventStoreError) {
108
+ throw error;
109
+ }
110
+ throw new EventStoreError(`Failed to process pending events: ${error instanceof Error ? error.message : String(error)}`, 'PENDING_PROCESS_FAILED', { error });
111
+ }
112
+ }
113
+ /**
114
+ * Process pre-fetched pending events (e.g. from WebSocket push).
115
+ * Same logic as processPendingEvents but skips the HTTP query.
116
+ */
117
+ async processPreFetchedPendingEvents(rawEvents) {
118
+ try {
119
+ return await this.decryptStoreAndAck(rawEvents);
120
+ }
121
+ catch (error) {
122
+ if (error instanceof EventStoreError) {
123
+ throw error;
124
+ }
125
+ throw new EventStoreError(`Failed to process pre-fetched pending events: ${error instanceof Error ? error.message : String(error)}`, 'PENDING_PROCESS_FAILED', { error });
126
+ }
127
+ }
128
+ /**
129
+ * Update event tags on the mediator (idempotent replace).
130
+ */
131
+ async updateEventTags(events) {
132
+ const { identity } = this.config;
133
+ const command = generateDirectAuthenticatedMediatorCommand(identity.did, `${identity.did}#signing`, identity.mediatorDid, {
134
+ type: 'UPDATE_EVENT_TAGS',
135
+ events: events.map((e) => ({
136
+ event_id: e.eventId,
137
+ encrypted_tags: e.encryptedTags,
138
+ })),
139
+ }, identity.keys);
140
+ await axios.post(identity.mediatorEndpoint, command);
141
+ }
142
+ /**
143
+ * Query events that haven't been processed by the app yet.
144
+ * Returns mediator event IDs alongside decrypted data for tag updates.
145
+ */
146
+ async queryUnprocessedEvents(pagination) {
147
+ try {
148
+ const { identity } = this.config;
149
+ const identityKeys = identity.keys;
150
+ const page = pagination?.page ?? 0;
151
+ const pageSize = pagination?.pageSize ?? 100;
152
+ const queryCommand = generateDirectAuthenticatedMediatorCommand(identity.did, `${identity.did}#signing`, identity.mediatorDid, {
153
+ type: 'QUERY_EVENTS',
154
+ filter: { unprocessed_only: true },
155
+ pagination: { page, page_size: pageSize },
156
+ }, identityKeys);
157
+ const response = await axios.post(identity.mediatorEndpoint, queryCommand);
158
+ if (response.data.type !== 'SUCCESS') {
159
+ throw new EventStoreError('Query failed', 'QUERY_FAILED', response.data);
160
+ }
161
+ const storageKey = identityKeys.storageKey;
162
+ const data = [];
163
+ for (const event of response.data.payload.events) {
164
+ try {
165
+ const decrypted = decryptString(event.payload, storageKey);
166
+ const parsed = JSON.parse(decrypted);
167
+ parsed._mediatorEventId = event.id;
168
+ data.push(parsed);
169
+ }
170
+ catch (error) {
171
+ console.warn(`[EventStore] Decryption failure for event: ${error instanceof Error ? error.message : String(error)}`);
172
+ }
173
+ }
174
+ return {
175
+ data,
176
+ pagination: {
177
+ page: response.data.payload.pagination.page,
178
+ pageSize: response.data.payload.pagination.page_size,
179
+ total: response.data.payload.pagination.total,
180
+ },
181
+ };
182
+ }
183
+ catch (error) {
184
+ if (error instanceof EventStoreError) {
185
+ throw error;
186
+ }
187
+ throw new EventStoreError(`Failed to query unprocessed events: ${error instanceof Error ? error.message : String(error)}`, 'QUERY_FAILED', { error });
188
+ }
189
+ }
190
+ /**
191
+ * Shared logic: filter by known senders, decrypt, store locally, acknowledge.
192
+ */
193
+ async decryptStoreAndAck(pendingEvents) {
194
+ const { communicationContracts } = this.config;
195
+ const activeContracts = communicationContracts();
196
+ if (activeContracts.length === 0) {
197
+ return [];
198
+ }
199
+ const knownSenders = new Set(activeContracts.map((c) => c.participantDid));
200
+ const filtered = pendingEvents.filter((event) => knownSenders.has(event.sender_did));
201
+ const processedEvents = [];
202
+ const ackEventIds = [];
203
+ for (const pendingEvent of filtered) {
204
+ try {
205
+ const matchingContracts = activeContracts
206
+ .filter((c) => c.participantDid === pendingEvent.sender_did && c.rootSecret)
207
+ .sort((a, b) => b.signedCommunicationContract.communication_contract.expires_at -
208
+ a.signedCommunicationContract.communication_contract.expires_at);
209
+ let decrypted = null;
210
+ let contract = null;
211
+ for (const candidate of matchingContracts) {
212
+ try {
213
+ decrypted = decryptString(pendingEvent.payload, base64Decode(candidate.rootSecret));
214
+ contract = candidate;
215
+ break;
216
+ }
217
+ catch { }
218
+ }
219
+ if (!decrypted || !contract) {
220
+ console.warn(`[EventStore] No contract could decrypt event ${pendingEvent.id}`);
221
+ continue;
222
+ }
223
+ this.config.onContractUsed?.(contract.id, pendingEvent.sender_did);
224
+ const envelope = JSON.parse(decrypted);
225
+ const senderSigningKey = extractSigningKeyFromDid(pendingEvent.sender_did);
226
+ if (senderSigningKey && envelope.signature) {
227
+ const { signature, ...envelopeData } = envelope;
228
+ const isValid = verifyJsonSignature(envelopeData, signature, senderSigningKey);
229
+ if (!isValid) {
230
+ console.warn(`Invalid signature on event ${pendingEvent.id}, skipping`);
231
+ continue;
232
+ }
233
+ }
234
+ const event = JSON.parse(envelope.event);
235
+ if (!event?.meta?.ephemeral) {
236
+ await this.storeReceivedEvent(event, pendingEvent.sender_did, contract.id);
237
+ }
238
+ processedEvents.push(event);
239
+ ackEventIds.push(pendingEvent.id);
240
+ }
241
+ catch (error) {
242
+ console.warn(`Failed to process pending event ${pendingEvent.id}:`, error);
243
+ }
244
+ }
245
+ if (ackEventIds.length > 0) {
246
+ await this.acknowledgePendingEvents(ackEventIds);
247
+ }
248
+ return processedEvents;
249
+ }
250
+ /**
251
+ * Send event to recipient via two-way private channel
252
+ */
253
+ async sendToRecipient(event, recipientDid) {
254
+ const { identity, communicationContracts } = this.config;
255
+ const contract = communicationContracts()
256
+ .filter((c) => c.participantDid === recipientDid && c.rootSecret && c.signedCommunicationContract)
257
+ .sort((a, b) => b.signedCommunicationContract.communication_contract.expires_at -
258
+ a.signedCommunicationContract.communication_contract.expires_at)[0];
259
+ if (!contract) {
260
+ throw new EventStoreError(`No communication contract found with ${recipientDid}`, 'CONTRACT_NOT_FOUND', { recipientDid });
261
+ }
262
+ const rootSecret = base64Decode(contract.rootSecret);
263
+ const timestamp = Math.floor(Date.now() / 1000);
264
+ const envelopeData = {
265
+ contract_id: contract.id,
266
+ event: JSON.stringify(event),
267
+ timestamp,
268
+ };
269
+ const signature = signJsonObject(envelopeData, identity.keys.signing.privateKey);
270
+ const envelope = {
271
+ ...envelopeData,
272
+ signature,
273
+ };
274
+ const encryptedPayload = encryptString(JSON.stringify(envelope), rootSecret);
275
+ const twoWayPrivateCommand = generateTwoWayPrivateMediatorCommand(identity.did, `${identity.did}#signing`, recipientDid, encryptedPayload, identity.keys);
276
+ await axios.post(identity.mediatorEndpoint, twoWayPrivateCommand);
277
+ }
278
+ /**
279
+ * Store event locally with encrypted tags
280
+ */
281
+ async storeLocally(event, tags, recipientDid) {
282
+ const { identity, communicationContracts } = this.config;
283
+ const storageKey = identity.keys.storageKey;
284
+ const encryptedEventForSelf = encryptString(JSON.stringify(event), storageKey);
285
+ const encryptedTags = tags.map((tag) => generateEncryptedTag(identity.keys.signing.privateKey, tag));
286
+ let contractId;
287
+ if (recipientDid) {
288
+ const contract = communicationContracts().find((c) => c.participantDid === recipientDid);
289
+ contractId = contract?.id;
290
+ }
291
+ const saveEventCommand = generateDirectAuthenticatedMediatorCommand(identity.did, `${identity.did}#signing`, identity.mediatorDid, {
292
+ type: 'SAVE_EVENTS',
293
+ events: [
294
+ {
295
+ sender_did: identity.did,
296
+ recipient_did: recipientDid || identity.did,
297
+ contract_id: contractId,
298
+ payload: encryptedEventForSelf,
299
+ timestamp: Math.floor(Date.now() / 1000),
300
+ encrypted_tags: encryptedTags,
301
+ },
302
+ ],
303
+ }, identity.keys);
304
+ await axios.post(identity.mediatorEndpoint, saveEventCommand);
305
+ }
306
+ /**
307
+ * Store received event with appropriate tags
308
+ */
309
+ async storeReceivedEvent(event, senderDid, contractId) {
310
+ const { identity } = this.config;
311
+ const storageKey = identity.keys.storageKey;
312
+ const encryptedEventForSelf = encryptString(JSON.stringify(event), storageKey);
313
+ const participantTag = generateEncryptedTag(identity.keys.signing.privateKey, `participant.${senderDid}`);
314
+ const saveEventCommand = generateDirectAuthenticatedMediatorCommand(identity.did, `${identity.did}#signing`, identity.mediatorDid, {
315
+ type: 'SAVE_EVENTS',
316
+ events: [
317
+ {
318
+ sender_did: senderDid,
319
+ recipient_did: identity.did,
320
+ contract_id: contractId,
321
+ payload: encryptedEventForSelf,
322
+ timestamp: Math.floor(Date.now() / 1000),
323
+ encrypted_tags: [participantTag],
324
+ },
325
+ ],
326
+ }, identity.keys);
327
+ await axios.post(identity.mediatorEndpoint, saveEventCommand);
328
+ }
329
+ /**
330
+ * Acknowledge processed pending events
331
+ */
332
+ async acknowledgePendingEvents(eventIds) {
333
+ const { identity } = this.config;
334
+ const ackCommand = generateDirectAuthenticatedMediatorCommand(identity.did, `${identity.did}#signing`, identity.mediatorDid, {
335
+ type: 'ACKNOWLEDGE_PENDING_EVENTS',
336
+ event_ids: eventIds,
337
+ }, identity.keys);
338
+ await axios.post(identity.mediatorEndpoint, ackCommand);
339
+ }
340
+ }
341
+ const extractSigningKeyFromDid = (did) => {
342
+ if (!did.startsWith('did:decentrl:')) {
343
+ return null;
344
+ }
345
+ const parts = did.replace('did:decentrl:', '').split(':');
346
+ if (parts.length !== 4) {
347
+ return null;
348
+ }
349
+ try {
350
+ return multibaseDecode(parts[1]);
351
+ }
352
+ catch {
353
+ return null;
354
+ }
355
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=event-store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-store.test.d.ts","sourceRoot":"","sources":["../src/event-store.test.ts"],"names":[],"mappings":""}