@decentrl/event-store 0.0.6 → 0.0.8

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.
@@ -1,4 +1,4 @@
1
- import { type EventStoreConfig, type PaginatedResult, type PublishOptions, type QueryOptions } from './types';
1
+ import { type EventStoreConfig, type PaginatedResult, type PublicEventResult, type PublicPublishOptions, type PublicQueryOptions, type PublishOptions, type QueryOptions } from './types';
2
2
  export declare class DecentrlEventStore {
3
3
  private config;
4
4
  constructor(config: EventStoreConfig);
@@ -40,6 +40,20 @@ export declare class DecentrlEventStore {
40
40
  }): Promise<PaginatedResult<T & {
41
41
  _mediatorEventId?: string;
42
42
  }>>;
43
+ /**
44
+ * Publish a public (unencrypted, signed) event to the mediator.
45
+ */
46
+ publishPublicEvent(event: string, options: PublicPublishOptions): Promise<{
47
+ publicEventId: string;
48
+ }>;
49
+ /**
50
+ * Delete a public event from the mediator.
51
+ */
52
+ deletePublicEvent(publicEventId: string): Promise<void>;
53
+ /**
54
+ * Fetch public events from any publisher (no identity needed).
55
+ */
56
+ static fetchPublicEvents(options: PublicQueryOptions): Promise<PaginatedResult<PublicEventResult>>;
43
57
  /**
44
58
  * Shared logic: filter by known senders, decrypt, store locally, acknowledge.
45
59
  */
@@ -1 +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"}
1
+ {"version":3,"file":"event-store.d.ts","sourceRoot":"","sources":["../src/event-store.ts"],"names":[],"mappings":"AAqBA,OAAO,EACN,KAAK,gBAAgB,EAErB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,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;IACG,kBAAkB,CACvB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,oBAAoB,GAC3B,OAAO,CAAC;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;IAwCrC;;OAEG;IACG,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4B7D;;OAEG;WACU,iBAAiB,CAC7B,OAAO,EAAE,kBAAkB,GACzB,OAAO,CAAC,eAAe,CAAC,iBAAiB,CAAC,CAAC;IA2D9C;;OAEG;YACW,kBAAkB;IAgFhC;;OAEG;YACW,eAAe;IAkD7B;;OAEG;YACW,YAAY;IA2C1B;;OAEG;YACW,kBAAkB;IAyChC;;OAEG;YACW,wBAAwB;CAmBtC"}
@@ -1,5 +1,6 @@
1
1
  import { base64Decode, decryptString, encryptString, generateEncryptedTag, multibaseDecode, signJsonObject, verifyJsonSignature, } from '@decentrl/crypto';
2
2
  import { generateDirectAuthenticatedMediatorCommand, generateTwoWayPrivateMediatorCommand, } from '@decentrl/identity/communication-channels/mediator/direct-authenticated/command/command.service';
3
+ import { generateOneWayPublicMediatorCommand } from '@decentrl/identity/communication-channels/mediator/one-way-public/command/command.service';
3
4
  import axiosModule from 'axios';
4
5
  import { EventStoreError, } from './types';
5
6
  const DEFAULT_TIMEOUT_MS = 30_000;
@@ -187,6 +188,88 @@ export class DecentrlEventStore {
187
188
  throw new EventStoreError(`Failed to query unprocessed events: ${error instanceof Error ? error.message : String(error)}`, 'QUERY_FAILED', { error });
188
189
  }
189
190
  }
191
+ /**
192
+ * Publish a public (unencrypted, signed) event to the mediator.
193
+ */
194
+ async publishPublicEvent(event, options) {
195
+ const { identity } = this.config;
196
+ const timestamp = Math.floor(Date.now() / 1000);
197
+ const signedData = {
198
+ channel_id: options.channelId,
199
+ event,
200
+ tags: options.tags,
201
+ timestamp,
202
+ };
203
+ const eventSignature = signJsonObject(signedData, identity.keys.signing.privateKey);
204
+ const command = generateOneWayPublicMediatorCommand(identity.did, `${identity.did}#signing`, identity.mediatorDid, {
205
+ type: 'PUBLISH_PUBLIC_EVENT',
206
+ channel_id: options.channelId,
207
+ event,
208
+ tags: options.tags,
209
+ timestamp,
210
+ event_signature: eventSignature,
211
+ }, identity.keys);
212
+ const response = await axios.post(identity.mediatorEndpoint, command);
213
+ if (response.data.type !== 'SUCCESS') {
214
+ throw new EventStoreError('Failed to publish public event', 'PUBLISH_FAILED', response.data);
215
+ }
216
+ return { publicEventId: response.data.public_event_id };
217
+ }
218
+ /**
219
+ * Delete a public event from the mediator.
220
+ */
221
+ async deletePublicEvent(publicEventId) {
222
+ const { identity } = this.config;
223
+ const command = generateOneWayPublicMediatorCommand(identity.did, `${identity.did}#signing`, identity.mediatorDid, {
224
+ type: 'DELETE_PUBLIC_EVENT',
225
+ public_event_id: publicEventId,
226
+ }, identity.keys);
227
+ const response = await axios.post(identity.mediatorEndpoint, command);
228
+ if (response.data.type === 'ERROR') {
229
+ throw new EventStoreError(`Failed to delete public event: ${response.data.code}`, 'DELETE_FAILED', response.data);
230
+ }
231
+ }
232
+ /**
233
+ * Fetch public events from any publisher (no identity needed).
234
+ */
235
+ static async fetchPublicEvents(options) {
236
+ const params = new URLSearchParams();
237
+ if (options.channelId) {
238
+ params.set('channel_id', options.channelId);
239
+ }
240
+ if (options.tags?.length) {
241
+ params.set('tags', options.tags.join(','));
242
+ }
243
+ if (options.afterTimestamp) {
244
+ params.set('after', String(options.afterTimestamp));
245
+ }
246
+ if (options.beforeTimestamp) {
247
+ params.set('before', String(options.beforeTimestamp));
248
+ }
249
+ if (options.pagination) {
250
+ params.set('page', String(options.pagination.page));
251
+ params.set('page_size', String(options.pagination.pageSize));
252
+ }
253
+ const url = `${options.mediatorEndpoint}/public/${options.publisherDid}?${params.toString()}`;
254
+ const response = await axiosModule.get(url, {
255
+ timeout: DEFAULT_TIMEOUT_MS,
256
+ });
257
+ return {
258
+ data: response.data.events.map((e) => ({
259
+ id: e.id,
260
+ channelId: e.channel_id,
261
+ event: e.event,
262
+ tags: e.tags,
263
+ timestamp: e.timestamp,
264
+ eventSignature: e.event_signature,
265
+ })),
266
+ pagination: {
267
+ page: response.data.pagination.page,
268
+ pageSize: response.data.pagination.page_size,
269
+ total: response.data.pagination.total,
270
+ },
271
+ };
272
+ }
190
273
  /**
191
274
  * Shared logic: filter by known senders, decrypt, store locally, acknowledge.
192
275
  */
@@ -232,7 +315,8 @@ export class DecentrlEventStore {
232
315
  }
233
316
  }
234
317
  const event = JSON.parse(envelope.event);
235
- if (!event?.meta?.ephemeral) {
318
+ const eventWithMeta = event;
319
+ if (!eventWithMeta?.meta?.ephemeral) {
236
320
  await this.storeReceivedEvent(event, pendingEvent.sender_did, contract.id);
237
321
  }
238
322
  processedEvents.push(event);
@@ -13,12 +13,31 @@ vi.mock('@decentrl/identity/communication-channels/mediator/direct-authenticated
13
13
  generateDirectAuthenticatedMediatorCommand: vi.fn(() => ({ mock: 'command' })),
14
14
  generateTwoWayPrivateMediatorCommand: vi.fn(() => ({ mock: 'two-way-command' })),
15
15
  }));
16
- vi.mock('axios', () => ({
17
- default: {
18
- post: vi.fn(async () => ({ data: { type: 'SUCCESS', payload: {} } })),
19
- },
16
+ vi.mock('@decentrl/identity/communication-channels/mediator/one-way-public/command/command.service', () => ({
17
+ generateOneWayPublicMediatorCommand: vi.fn(() => ({ mock: 'one-way-public-command' })),
20
18
  }));
19
+ vi.mock('axios', () => {
20
+ const post = vi.fn(async () => ({ data: { type: 'SUCCESS', payload: {} } }));
21
+ const get = vi.fn(async () => ({
22
+ data: {
23
+ publisher_did: 'did:decentrl:bob',
24
+ events: [
25
+ {
26
+ id: 'pub-evt-1',
27
+ channel_id: 'blog',
28
+ event: '{"type":"blog.post","data":{"title":"Hello"}}',
29
+ tags: ['blog'],
30
+ timestamp: 1710000000,
31
+ event_signature: 'sig-1',
32
+ },
33
+ ],
34
+ pagination: { page: 0, page_size: 20, total: 1 },
35
+ },
36
+ }));
37
+ return { default: { post, get }, post, get };
38
+ });
21
39
  import { decryptString } from '@decentrl/crypto';
40
+ import axios from 'axios';
22
41
  import { DecentrlEventStore } from './event-store.js';
23
42
  const makeContract = (opts) => ({
24
43
  id: opts.id,
@@ -249,25 +268,135 @@ describe('DecentrlEventStore', () => {
249
268
  vi.mocked(decryptString).mockReset();
250
269
  });
251
270
  it('stores locally for non-ephemeral events', async () => {
252
- const axios = await import('axios');
271
+ const axiosModule = await import('axios');
253
272
  const eventStore = new DecentrlEventStore({
254
273
  identity: mockIdentity,
255
274
  communicationContracts: () => [],
256
275
  });
257
276
  await eventStore.publishEvent({ type: 'test' }, { tags: ['tag1'] });
258
277
  // Should have called axios.post for SAVE_EVENTS
259
- expect(axios.default.post).toHaveBeenCalled();
278
+ expect(axiosModule.default.post).toHaveBeenCalled();
260
279
  });
261
280
  it('skips local storage for ephemeral events without recipient', async () => {
262
- const axios = await import('axios');
263
- vi.mocked(axios.default.post).mockClear();
281
+ const axiosModule = await import('axios');
282
+ vi.mocked(axiosModule.default.post).mockClear();
264
283
  const eventStore = new DecentrlEventStore({
265
284
  identity: mockIdentity,
266
285
  communicationContracts: () => [],
267
286
  });
268
287
  await eventStore.publishEvent({ type: 'test' }, { tags: ['tag1'], ephemeral: true });
269
288
  // Should NOT have called axios.post since no recipient and ephemeral
270
- expect(axios.default.post).not.toHaveBeenCalled();
289
+ expect(axiosModule.default.post).not.toHaveBeenCalled();
290
+ });
291
+ });
292
+ describe('publishPublicEvent', () => {
293
+ beforeEach(() => {
294
+ vi.mocked(axios.post).mockResolvedValue({
295
+ data: { type: 'SUCCESS', public_event_id: 'pub-123' },
296
+ });
297
+ });
298
+ it('signs the event data and sends a ONE_WAY_PUBLIC command', async () => {
299
+ const { signJsonObject } = await import('@decentrl/crypto');
300
+ const { generateOneWayPublicMediatorCommand } = await import('@decentrl/identity/communication-channels/mediator/one-way-public/command/command.service');
301
+ const eventStore = new DecentrlEventStore({
302
+ identity: mockIdentity,
303
+ communicationContracts: () => [],
304
+ });
305
+ const result = await eventStore.publishPublicEvent('{"type":"test","data":{}}', {
306
+ channelId: 'blog',
307
+ tags: ['blog', 'test'],
308
+ });
309
+ expect(result.publicEventId).toBe('pub-123');
310
+ expect(signJsonObject).toHaveBeenCalled();
311
+ expect(generateOneWayPublicMediatorCommand).toHaveBeenCalled();
312
+ expect(axios.post).toHaveBeenCalledWith('http://mediator', { mock: 'one-way-public-command' }, { timeout: 30000 });
313
+ });
314
+ it('uses seconds for timestamp', async () => {
315
+ const { signJsonObject } = await import('@decentrl/crypto');
316
+ const eventStore = new DecentrlEventStore({
317
+ identity: mockIdentity,
318
+ communicationContracts: () => [],
319
+ });
320
+ await eventStore.publishPublicEvent('test', { channelId: 'blog', tags: [] });
321
+ const signedData = vi.mocked(signJsonObject).mock.calls[0][0];
322
+ // Timestamp should be in seconds (roughly current time / 1000)
323
+ expect(signedData.timestamp).toBeLessThan(Date.now());
324
+ expect(signedData.timestamp).toBeGreaterThan(Date.now() / 1000 - 10);
325
+ });
326
+ it('throws on error response', async () => {
327
+ vi.mocked(axios.post).mockResolvedValue({
328
+ data: { type: 'ERROR', code: 'INVALID_EVENT_SIGNATURE' },
329
+ });
330
+ const eventStore = new DecentrlEventStore({
331
+ identity: mockIdentity,
332
+ communicationContracts: () => [],
333
+ });
334
+ await expect(eventStore.publishPublicEvent('test', { channelId: 'blog', tags: [] })).rejects.toThrow('Failed to publish public event');
335
+ });
336
+ });
337
+ describe('deletePublicEvent', () => {
338
+ it('sends a DELETE_PUBLIC_EVENT command', async () => {
339
+ vi.mocked(axios.post).mockResolvedValue({
340
+ data: { type: 'SUCCESS' },
341
+ });
342
+ const eventStore = new DecentrlEventStore({
343
+ identity: mockIdentity,
344
+ communicationContracts: () => [],
345
+ });
346
+ await expect(eventStore.deletePublicEvent('pub-123')).resolves.toBeUndefined();
347
+ expect(axios.post).toHaveBeenCalled();
348
+ });
349
+ it('throws on error response', async () => {
350
+ vi.mocked(axios.post).mockResolvedValue({
351
+ data: { type: 'ERROR', code: 'PUBLIC_EVENT_NOT_FOUND' },
352
+ });
353
+ const eventStore = new DecentrlEventStore({
354
+ identity: mockIdentity,
355
+ communicationContracts: () => [],
356
+ });
357
+ await expect(eventStore.deletePublicEvent('nonexistent')).rejects.toThrow('Failed to delete public event');
358
+ });
359
+ });
360
+ describe('fetchPublicEvents (static)', () => {
361
+ it('fetches events via GET without requiring identity', async () => {
362
+ const result = await DecentrlEventStore.fetchPublicEvents({
363
+ mediatorEndpoint: 'http://mediator',
364
+ publisherDid: 'did:decentrl:bob',
365
+ channelId: 'blog',
366
+ });
367
+ expect(result.data).toHaveLength(1);
368
+ expect(result.data[0].id).toBe('pub-evt-1');
369
+ expect(result.data[0].channelId).toBe('blog');
370
+ expect(result.data[0].timestamp).toBe(1710000000);
371
+ expect(result.pagination.total).toBe(1);
372
+ });
373
+ it('passes query parameters to the GET URL', async () => {
374
+ vi.mocked(axios.get).mockClear();
375
+ await DecentrlEventStore.fetchPublicEvents({
376
+ mediatorEndpoint: 'http://mediator',
377
+ publisherDid: 'did:decentrl:bob',
378
+ channelId: 'blog',
379
+ tags: ['tag1', 'tag2'],
380
+ afterTimestamp: 1710000000,
381
+ pagination: { page: 2, pageSize: 10 },
382
+ });
383
+ const url = vi.mocked(axios.get).mock.calls[0][0];
384
+ expect(url).toContain('/public/did:decentrl:bob');
385
+ expect(url).toContain('channel_id=blog');
386
+ expect(url).toContain('tags=tag1%2Ctag2');
387
+ expect(url).toContain('after=1710000000');
388
+ expect(url).toContain('page=2');
389
+ expect(url).toContain('page_size=10');
390
+ });
391
+ it('maps snake_case response to camelCase', async () => {
392
+ const result = await DecentrlEventStore.fetchPublicEvents({
393
+ mediatorEndpoint: 'http://mediator',
394
+ publisherDid: 'did:decentrl:bob',
395
+ });
396
+ // Verify camelCase mapping
397
+ expect(result.data[0].channelId).toBe('blog');
398
+ expect(result.data[0].eventSignature).toBe('sig-1');
399
+ expect(result.pagination.pageSize).toBe(20);
271
400
  });
272
401
  });
273
402
  });
package/dist/types.d.ts CHANGED
@@ -45,6 +45,32 @@ export interface PaginatedResult<T> {
45
45
  data: T[];
46
46
  pagination: PaginationMeta;
47
47
  }
48
+ export interface PublicPublishOptions {
49
+ channelId: string;
50
+ tags: string[];
51
+ }
52
+ export interface PublicQueryOptions {
53
+ mediatorEndpoint: string;
54
+ publisherDid: string;
55
+ channelId?: string;
56
+ tags?: string[];
57
+ /** Unix timestamp in seconds */
58
+ afterTimestamp?: number;
59
+ /** Unix timestamp in seconds */
60
+ beforeTimestamp?: number;
61
+ pagination?: {
62
+ page: number;
63
+ pageSize: number;
64
+ };
65
+ }
66
+ export interface PublicEventResult {
67
+ id: string;
68
+ channelId: string;
69
+ event: string;
70
+ tags: string[];
71
+ timestamp: number;
72
+ eventSignature: string;
73
+ }
48
74
  export declare class EventStoreError extends Error {
49
75
  code: string;
50
76
  details?: unknown | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,yEAAyE,CAAC;AAE3H,MAAM,WAAW,gBAAgB;IAChC,QAAQ,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,oBAAoB,CAAC;QAC3B,gBAAgB,EAAE,MAAM,CAAC;QACzB,WAAW,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,sBAAsB,EAAE,MAAM,oBAAoB,EAAE,CAAC;IACrD,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;CACtE;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,2BAA2B,EAAE,2BAA2B,CAAC;IACzD,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe,CAAC,CAAC;IACjC,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,UAAU,EAAE,cAAc,CAAC;CAC3B;AAED,qBAAa,eAAgB,SAAQ,KAAK;IAGjC,IAAI,EAAE,MAAM;IACZ,OAAO,CAAC,EAAE,OAAO;gBAFxB,OAAO,EAAE,MAAM,EACR,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,YAAA;CAKzB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,yEAAyE,CAAC;AAE3H,MAAM,WAAW,gBAAgB;IAChC,QAAQ,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,oBAAoB,CAAC;QAC3B,gBAAgB,EAAE,MAAM,CAAC;QACzB,WAAW,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,sBAAsB,EAAE,MAAM,oBAAoB,EAAE,CAAC;IACrD,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;CACtE;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,2BAA2B,EAAE,2BAA2B,CAAC;IACzD,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe,CAAC,CAAC;IACjC,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,UAAU,EAAE,cAAc,CAAC;CAC3B;AAID,MAAM,WAAW,oBAAoB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,EAAE,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IAClC,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,gCAAgC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gCAAgC;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAChD;AAED,MAAM,WAAW,iBAAiB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,eAAgB,SAAQ,KAAK;IAGjC,IAAI,EAAE,MAAM;IACZ,OAAO,CAAC,EAAE,OAAO;gBAFxB,OAAO,EAAE,MAAM,EACR,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,YAAA;CAKzB"}
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@decentrl/event-store",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Event-driven storage and communication library for decentrl",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "dependencies": {
8
8
  "axios": "^1.6.0",
9
- "@decentrl/identity": "0.0.6",
10
- "@decentrl/crypto": "0.0.6"
9
+ "@decentrl/crypto": "0.0.8",
10
+ "@decentrl/identity": "0.0.8"
11
11
  },
12
12
  "devDependencies": {
13
13
  "typescript": "^5.0.0",
@@ -19,13 +19,37 @@ vi.mock(
19
19
  }),
20
20
  );
21
21
 
22
- vi.mock('axios', () => ({
23
- default: {
24
- post: vi.fn(async () => ({ data: { type: 'SUCCESS', payload: {} } })),
25
- },
26
- }));
22
+ vi.mock(
23
+ '@decentrl/identity/communication-channels/mediator/one-way-public/command/command.service',
24
+ () => ({
25
+ generateOneWayPublicMediatorCommand: vi.fn(() => ({ mock: 'one-way-public-command' })),
26
+ }),
27
+ );
28
+
29
+ vi.mock('axios', () => {
30
+ const post = vi.fn(async () => ({ data: { type: 'SUCCESS', payload: {} } }));
31
+ const get = vi.fn(async () => ({
32
+ data: {
33
+ publisher_did: 'did:decentrl:bob',
34
+ events: [
35
+ {
36
+ id: 'pub-evt-1',
37
+ channel_id: 'blog',
38
+ event: '{"type":"blog.post","data":{"title":"Hello"}}',
39
+ tags: ['blog'],
40
+ timestamp: 1710000000,
41
+ event_signature: 'sig-1',
42
+ },
43
+ ],
44
+ pagination: { page: 0, page_size: 20, total: 1 },
45
+ },
46
+ }));
47
+
48
+ return { default: { post, get }, post, get };
49
+ });
27
50
 
28
51
  import { decryptString } from '@decentrl/crypto';
52
+ import axios from 'axios';
29
53
  import { DecentrlEventStore } from './event-store.js';
30
54
  import type { StoredSignedContract } from './types.js';
31
55
 
@@ -310,7 +334,7 @@ describe('DecentrlEventStore', () => {
310
334
  });
311
335
 
312
336
  it('stores locally for non-ephemeral events', async () => {
313
- const axios = await import('axios');
337
+ const axiosModule = await import('axios');
314
338
 
315
339
  const eventStore = new DecentrlEventStore({
316
340
  identity: mockIdentity,
@@ -320,12 +344,12 @@ describe('DecentrlEventStore', () => {
320
344
  await eventStore.publishEvent({ type: 'test' }, { tags: ['tag1'] });
321
345
 
322
346
  // Should have called axios.post for SAVE_EVENTS
323
- expect(axios.default.post).toHaveBeenCalled();
347
+ expect(axiosModule.default.post).toHaveBeenCalled();
324
348
  });
325
349
 
326
350
  it('skips local storage for ephemeral events without recipient', async () => {
327
- const axios = await import('axios');
328
- vi.mocked(axios.default.post).mockClear();
351
+ const axiosModule = await import('axios');
352
+ vi.mocked(axiosModule.default.post).mockClear();
329
353
 
330
354
  const eventStore = new DecentrlEventStore({
331
355
  identity: mockIdentity,
@@ -335,7 +359,152 @@ describe('DecentrlEventStore', () => {
335
359
  await eventStore.publishEvent({ type: 'test' }, { tags: ['tag1'], ephemeral: true });
336
360
 
337
361
  // Should NOT have called axios.post since no recipient and ephemeral
338
- expect(axios.default.post).not.toHaveBeenCalled();
362
+ expect(axiosModule.default.post).not.toHaveBeenCalled();
363
+ });
364
+ });
365
+
366
+ describe('publishPublicEvent', () => {
367
+ beforeEach(() => {
368
+ vi.mocked(axios.post).mockResolvedValue({
369
+ data: { type: 'SUCCESS', public_event_id: 'pub-123' },
370
+ });
371
+ });
372
+
373
+ it('signs the event data and sends a ONE_WAY_PUBLIC command', async () => {
374
+ const { signJsonObject } = await import('@decentrl/crypto');
375
+ const { generateOneWayPublicMediatorCommand } = await import(
376
+ '@decentrl/identity/communication-channels/mediator/one-way-public/command/command.service'
377
+ );
378
+
379
+ const eventStore = new DecentrlEventStore({
380
+ identity: mockIdentity,
381
+ communicationContracts: () => [],
382
+ });
383
+
384
+ const result = await eventStore.publishPublicEvent('{"type":"test","data":{}}', {
385
+ channelId: 'blog',
386
+ tags: ['blog', 'test'],
387
+ });
388
+
389
+ expect(result.publicEventId).toBe('pub-123');
390
+ expect(signJsonObject).toHaveBeenCalled();
391
+ expect(generateOneWayPublicMediatorCommand).toHaveBeenCalled();
392
+ expect(axios.post).toHaveBeenCalledWith(
393
+ 'http://mediator',
394
+ { mock: 'one-way-public-command' },
395
+ { timeout: 30000 },
396
+ );
397
+ });
398
+
399
+ it('uses seconds for timestamp', async () => {
400
+ const { signJsonObject } = await import('@decentrl/crypto');
401
+
402
+ const eventStore = new DecentrlEventStore({
403
+ identity: mockIdentity,
404
+ communicationContracts: () => [],
405
+ });
406
+
407
+ await eventStore.publishPublicEvent('test', { channelId: 'blog', tags: [] });
408
+
409
+ const signedData = vi.mocked(signJsonObject).mock.calls[0][0] as Record<string, unknown>;
410
+ // Timestamp should be in seconds (roughly current time / 1000)
411
+ expect(signedData.timestamp).toBeLessThan(Date.now());
412
+ expect(signedData.timestamp).toBeGreaterThan(Date.now() / 1000 - 10);
413
+ });
414
+
415
+ it('throws on error response', async () => {
416
+ vi.mocked(axios.post).mockResolvedValue({
417
+ data: { type: 'ERROR', code: 'INVALID_EVENT_SIGNATURE' },
418
+ });
419
+
420
+ const eventStore = new DecentrlEventStore({
421
+ identity: mockIdentity,
422
+ communicationContracts: () => [],
423
+ });
424
+
425
+ await expect(
426
+ eventStore.publishPublicEvent('test', { channelId: 'blog', tags: [] }),
427
+ ).rejects.toThrow('Failed to publish public event');
428
+ });
429
+ });
430
+
431
+ describe('deletePublicEvent', () => {
432
+ it('sends a DELETE_PUBLIC_EVENT command', async () => {
433
+ vi.mocked(axios.post).mockResolvedValue({
434
+ data: { type: 'SUCCESS' },
435
+ });
436
+
437
+ const eventStore = new DecentrlEventStore({
438
+ identity: mockIdentity,
439
+ communicationContracts: () => [],
440
+ });
441
+
442
+ await expect(eventStore.deletePublicEvent('pub-123')).resolves.toBeUndefined();
443
+ expect(axios.post).toHaveBeenCalled();
444
+ });
445
+
446
+ it('throws on error response', async () => {
447
+ vi.mocked(axios.post).mockResolvedValue({
448
+ data: { type: 'ERROR', code: 'PUBLIC_EVENT_NOT_FOUND' },
449
+ });
450
+
451
+ const eventStore = new DecentrlEventStore({
452
+ identity: mockIdentity,
453
+ communicationContracts: () => [],
454
+ });
455
+
456
+ await expect(eventStore.deletePublicEvent('nonexistent')).rejects.toThrow(
457
+ 'Failed to delete public event',
458
+ );
459
+ });
460
+ });
461
+
462
+ describe('fetchPublicEvents (static)', () => {
463
+ it('fetches events via GET without requiring identity', async () => {
464
+ const result = await DecentrlEventStore.fetchPublicEvents({
465
+ mediatorEndpoint: 'http://mediator',
466
+ publisherDid: 'did:decentrl:bob',
467
+ channelId: 'blog',
468
+ });
469
+
470
+ expect(result.data).toHaveLength(1);
471
+ expect(result.data[0].id).toBe('pub-evt-1');
472
+ expect(result.data[0].channelId).toBe('blog');
473
+ expect(result.data[0].timestamp).toBe(1710000000);
474
+ expect(result.pagination.total).toBe(1);
475
+ });
476
+
477
+ it('passes query parameters to the GET URL', async () => {
478
+ vi.mocked(axios.get).mockClear();
479
+
480
+ await DecentrlEventStore.fetchPublicEvents({
481
+ mediatorEndpoint: 'http://mediator',
482
+ publisherDid: 'did:decentrl:bob',
483
+ channelId: 'blog',
484
+ tags: ['tag1', 'tag2'],
485
+ afterTimestamp: 1710000000,
486
+ pagination: { page: 2, pageSize: 10 },
487
+ });
488
+
489
+ const url = vi.mocked(axios.get).mock.calls[0][0] as string;
490
+ expect(url).toContain('/public/did:decentrl:bob');
491
+ expect(url).toContain('channel_id=blog');
492
+ expect(url).toContain('tags=tag1%2Ctag2');
493
+ expect(url).toContain('after=1710000000');
494
+ expect(url).toContain('page=2');
495
+ expect(url).toContain('page_size=10');
496
+ });
497
+
498
+ it('maps snake_case response to camelCase', async () => {
499
+ const result = await DecentrlEventStore.fetchPublicEvents({
500
+ mediatorEndpoint: 'http://mediator',
501
+ publisherDid: 'did:decentrl:bob',
502
+ });
503
+
504
+ // Verify camelCase mapping
505
+ expect(result.data[0].channelId).toBe('blog');
506
+ expect(result.data[0].eventSignature).toBe('sig-1');
507
+ expect(result.pagination.pageSize).toBe(20);
339
508
  });
340
509
  });
341
510
  });
@@ -16,11 +16,16 @@ import type { QueryEventsMediatorCommandResponse } from '@decentrl/identity/comm
16
16
  import type { QueryPendingEventsMediatorCommandResponse } from '@decentrl/identity/communication-channels/mediator/direct-authenticated/command/query-pending-events.schema';
17
17
  import type { SaveEventsMediatorCommandResponse } from '@decentrl/identity/communication-channels/mediator/direct-authenticated/command/save-events.schema';
18
18
  import type { UpdateEventTagsMediatorCommandResponse } from '@decentrl/identity/communication-channels/mediator/direct-authenticated/command/update-event-tags.schema';
19
+ import type { PublishPublicEventResponse } from '@decentrl/identity/communication-channels/mediator/one-way-public/command/command.schema';
20
+ import { generateOneWayPublicMediatorCommand } from '@decentrl/identity/communication-channels/mediator/one-way-public/command/command.service';
19
21
  import axiosModule from 'axios';
20
22
  import {
21
23
  type EventStoreConfig,
22
24
  EventStoreError,
23
25
  type PaginatedResult,
26
+ type PublicEventResult,
27
+ type PublicPublishOptions,
28
+ type PublicQueryOptions,
24
29
  type PublishOptions,
25
30
  type QueryOptions,
26
31
  } from './types';
@@ -307,6 +312,147 @@ export class DecentrlEventStore {
307
312
  }
308
313
  }
309
314
 
315
+ /**
316
+ * Publish a public (unencrypted, signed) event to the mediator.
317
+ */
318
+ async publishPublicEvent(
319
+ event: string,
320
+ options: PublicPublishOptions,
321
+ ): Promise<{ publicEventId: string }> {
322
+ const { identity } = this.config;
323
+ const timestamp = Math.floor(Date.now() / 1000);
324
+
325
+ const signedData = {
326
+ channel_id: options.channelId,
327
+ event,
328
+ tags: options.tags,
329
+ timestamp,
330
+ };
331
+
332
+ const eventSignature = signJsonObject(signedData, identity.keys.signing.privateKey);
333
+
334
+ const command = generateOneWayPublicMediatorCommand(
335
+ identity.did,
336
+ `${identity.did}#signing`,
337
+ identity.mediatorDid,
338
+ {
339
+ type: 'PUBLISH_PUBLIC_EVENT',
340
+ channel_id: options.channelId,
341
+ event,
342
+ tags: options.tags,
343
+ timestamp,
344
+ event_signature: eventSignature,
345
+ },
346
+ identity.keys,
347
+ );
348
+
349
+ const response = await axios.post<PublishPublicEventResponse>(
350
+ identity.mediatorEndpoint,
351
+ command,
352
+ );
353
+
354
+ if (response.data.type !== 'SUCCESS') {
355
+ throw new EventStoreError('Failed to publish public event', 'PUBLISH_FAILED', response.data);
356
+ }
357
+
358
+ return { publicEventId: response.data.public_event_id };
359
+ }
360
+
361
+ /**
362
+ * Delete a public event from the mediator.
363
+ */
364
+ async deletePublicEvent(publicEventId: string): Promise<void> {
365
+ const { identity } = this.config;
366
+
367
+ const command = generateOneWayPublicMediatorCommand(
368
+ identity.did,
369
+ `${identity.did}#signing`,
370
+ identity.mediatorDid,
371
+ {
372
+ type: 'DELETE_PUBLIC_EVENT',
373
+ public_event_id: publicEventId,
374
+ },
375
+ identity.keys,
376
+ );
377
+
378
+ const response = await axios.post<{ type: string; code?: string }>(
379
+ identity.mediatorEndpoint,
380
+ command,
381
+ );
382
+
383
+ if (response.data.type === 'ERROR') {
384
+ throw new EventStoreError(
385
+ `Failed to delete public event: ${response.data.code}`,
386
+ 'DELETE_FAILED',
387
+ response.data,
388
+ );
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Fetch public events from any publisher (no identity needed).
394
+ */
395
+ static async fetchPublicEvents(
396
+ options: PublicQueryOptions,
397
+ ): Promise<PaginatedResult<PublicEventResult>> {
398
+ const params = new URLSearchParams();
399
+
400
+ if (options.channelId) {
401
+ params.set('channel_id', options.channelId);
402
+ }
403
+
404
+ if (options.tags?.length) {
405
+ params.set('tags', options.tags.join(','));
406
+ }
407
+
408
+ if (options.afterTimestamp) {
409
+ params.set('after', String(options.afterTimestamp));
410
+ }
411
+
412
+ if (options.beforeTimestamp) {
413
+ params.set('before', String(options.beforeTimestamp));
414
+ }
415
+
416
+ if (options.pagination) {
417
+ params.set('page', String(options.pagination.page));
418
+ params.set('page_size', String(options.pagination.pageSize));
419
+ }
420
+
421
+ const url = `${options.mediatorEndpoint}/public/${options.publisherDid}?${params.toString()}`;
422
+
423
+ interface PublicEventsResponse {
424
+ events: Array<{
425
+ id: string;
426
+ channel_id: string;
427
+ event: string;
428
+ tags: string[];
429
+ timestamp: number;
430
+ event_signature: string;
431
+ }>;
432
+ pagination: { page: number; page_size: number; total: number };
433
+ }
434
+
435
+ const response = await axiosModule.get<PublicEventsResponse>(url, {
436
+ timeout: DEFAULT_TIMEOUT_MS,
437
+ });
438
+
439
+ return {
440
+ data: response.data.events.map((e) => ({
441
+ id: e.id,
442
+ channelId: e.channel_id,
443
+ event: e.event,
444
+ tags: e.tags,
445
+ timestamp: e.timestamp,
446
+ eventSignature: e.event_signature,
447
+ })),
448
+ pagination: {
449
+ page: response.data.pagination.page,
450
+ pageSize: response.data.pagination.page_size,
451
+ total: response.data.pagination.total,
452
+ },
453
+ };
454
+ }
455
+
310
456
  /**
311
457
  * Shared logic: filter by known senders, decrypt, store locally, acknowledge.
312
458
  */
@@ -370,7 +516,9 @@ export class DecentrlEventStore {
370
516
 
371
517
  const event = JSON.parse(envelope.event) as T;
372
518
 
373
- if (!(event as any)?.meta?.ephemeral) {
519
+ const eventWithMeta = event as { meta?: { ephemeral?: boolean } };
520
+
521
+ if (!eventWithMeta?.meta?.ephemeral) {
374
522
  await this.storeReceivedEvent(event, pendingEvent.sender_did, contract.id);
375
523
  }
376
524
 
package/src/types.ts CHANGED
@@ -49,6 +49,34 @@ export interface PaginatedResult<T> {
49
49
  pagination: PaginationMeta;
50
50
  }
51
51
 
52
+ // --- Public Events ---
53
+
54
+ export interface PublicPublishOptions {
55
+ channelId: string;
56
+ tags: string[];
57
+ }
58
+
59
+ export interface PublicQueryOptions {
60
+ mediatorEndpoint: string;
61
+ publisherDid: string;
62
+ channelId?: string;
63
+ tags?: string[];
64
+ /** Unix timestamp in seconds */
65
+ afterTimestamp?: number;
66
+ /** Unix timestamp in seconds */
67
+ beforeTimestamp?: number;
68
+ pagination?: { page: number; pageSize: number };
69
+ }
70
+
71
+ export interface PublicEventResult {
72
+ id: string;
73
+ channelId: string;
74
+ event: string;
75
+ tags: string[];
76
+ timestamp: number;
77
+ eventSignature: string;
78
+ }
79
+
52
80
  export class EventStoreError extends Error {
53
81
  constructor(
54
82
  message: string,