@decentrl/sdk 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.
@@ -2,13 +2,22 @@ import { DecentrlSDKError } from './errors.js';
2
2
  import type { StateStore } from './state-store.js';
3
3
  import { evaluateTagTemplates } from './tag-templates.js';
4
4
  import type {
5
+ ChannelDefinitions,
5
6
  EventDefinitions,
6
7
  EventEnvelope,
7
8
  EventMeta,
8
9
  InferState,
10
+ PublicEventDefinitions,
11
+ PublicEventEnvelope,
12
+ PublicEventMeta,
9
13
  StateDefinitions,
10
14
  } from './types.js';
11
15
 
16
+ // Dynamic reducer lookup type — TS can't statically verify runtime event type dispatch,
17
+ // so we use this explicit type for the cast instead of `any`.
18
+ type DynamicReducer<TMeta> = ((state: unknown, data: unknown, meta: TMeta) => unknown) | undefined;
19
+ type ReducerMap = Record<string, DynamicReducer<EventMeta | PublicEventMeta>>;
20
+
12
21
  const MAX_DEDUP_SIZE = 10_000;
13
22
 
14
23
  export class EventProcessor<
@@ -17,11 +26,14 @@ export class EventProcessor<
17
26
  > {
18
27
  private processedEventIds = new Set<string>();
19
28
  private eventListeners = new Set<(envelope: EventEnvelope) => void>();
29
+ private publicEventListeners = new Set<(envelope: PublicEventEnvelope) => void>();
20
30
 
21
31
  constructor(
22
32
  private eventDefinitions: TEvents,
23
33
  private stateDefinitions: TState,
24
34
  private stateStore: StateStore<InferState<TState>>,
35
+ private publicEventDefinitions?: PublicEventDefinitions,
36
+ private channelDefinitions?: ChannelDefinitions,
25
37
  ) {}
26
38
 
27
39
  onEvent(listener: (envelope: EventEnvelope) => void): () => void {
@@ -30,6 +42,12 @@ export class EventProcessor<
30
42
  return () => this.eventListeners.delete(listener);
31
43
  }
32
44
 
45
+ onPublicEvent(listener: (envelope: PublicEventEnvelope) => void): () => void {
46
+ this.publicEventListeners.add(listener);
47
+
48
+ return () => this.publicEventListeners.delete(listener);
49
+ }
50
+
33
51
  validate(eventType: string, data: unknown): void {
34
52
  const definition = this.eventDefinitions[eventType];
35
53
 
@@ -50,6 +68,26 @@ export class EventProcessor<
50
68
  }
51
69
  }
52
70
 
71
+ validatePublicEvent(eventType: string, data: unknown): void {
72
+ const definition = this.publicEventDefinitions?.[eventType];
73
+
74
+ if (!definition) {
75
+ throw new DecentrlSDKError(`Unknown public event type: ${eventType}`, 'UNKNOWN_EVENT_TYPE', {
76
+ eventType,
77
+ });
78
+ }
79
+
80
+ const result = definition.schema.safeParse(data);
81
+
82
+ if (!result.success) {
83
+ throw new DecentrlSDKError(
84
+ `Schema validation failed for public event type "${eventType}": ${result.error.message}`,
85
+ 'SCHEMA_VALIDATION_FAILED',
86
+ { eventType, errors: result.error.issues },
87
+ );
88
+ }
89
+ }
90
+
53
91
  computeTags(eventType: string, data: unknown): string[] {
54
92
  const definition = this.eventDefinitions[eventType];
55
93
 
@@ -62,6 +100,30 @@ export class EventProcessor<
62
100
  return evaluateTagTemplates(definition.tags, data);
63
101
  }
64
102
 
103
+ computePublicTags(eventType: string, data: unknown): string[] {
104
+ const definition = this.publicEventDefinitions?.[eventType];
105
+
106
+ if (!definition) {
107
+ throw new DecentrlSDKError(`Unknown public event type: ${eventType}`, 'UNKNOWN_EVENT_TYPE', {
108
+ eventType,
109
+ });
110
+ }
111
+
112
+ return evaluateTagTemplates(definition.tags, data);
113
+ }
114
+
115
+ getPublicChannelId(eventType: string): string {
116
+ const definition = this.publicEventDefinitions?.[eventType];
117
+
118
+ if (!definition) {
119
+ throw new DecentrlSDKError(`Unknown public event type: ${eventType}`, 'UNKNOWN_EVENT_TYPE', {
120
+ eventType,
121
+ });
122
+ }
123
+
124
+ return definition.channelId;
125
+ }
126
+
65
127
  computeTagsSafe(eventType: string, data: unknown): string[] {
66
128
  try {
67
129
  return this.computeTags(eventType, data);
@@ -83,7 +145,6 @@ export class EventProcessor<
83
145
  }
84
146
 
85
147
  if (this.processedEventIds.size >= MAX_DEDUP_SIZE) {
86
- // Set iterates in insertion order — first value is the oldest
87
148
  const oldest = this.processedEventIds.values().next().value!;
88
149
  this.processedEventIds.delete(oldest);
89
150
  }
@@ -95,10 +156,7 @@ export class EventProcessor<
95
156
 
96
157
  for (const sliceKey of Object.keys(this.stateDefinitions)) {
97
158
  const sliceDef = this.stateDefinitions[sliceKey];
98
- // Dynamic reducer lookup TS can't prove type safety for runtime event type dispatch
99
- const reducer = sliceDef.reduce[envelope.type] as
100
- | ((state: any, data: any, meta: EventMeta) => any)
101
- | undefined;
159
+ const reducer = (sliceDef.reduce as ReducerMap)[envelope.type];
102
160
 
103
161
  if (reducer) {
104
162
  const currentSlice = currentState[sliceKey as keyof InferState<TState>];
@@ -114,6 +172,86 @@ export class EventProcessor<
114
172
  return true;
115
173
  }
116
174
 
175
+ processPublicEvent(envelope: PublicEventEnvelope): boolean {
176
+ const dedupKey = `public:${envelope.meta.eventId}`;
177
+
178
+ if (this.processedEventIds.has(dedupKey)) {
179
+ return false;
180
+ }
181
+
182
+ if (this.processedEventIds.size >= MAX_DEDUP_SIZE) {
183
+ const oldest = this.processedEventIds.values().next().value!;
184
+ this.processedEventIds.delete(oldest);
185
+ }
186
+
187
+ this.processedEventIds.add(dedupKey);
188
+
189
+ const reducerKey = `public:${envelope.type}`;
190
+ const currentState = this.stateStore.getState();
191
+
192
+ for (const sliceKey of Object.keys(this.stateDefinitions)) {
193
+ const sliceDef = this.stateDefinitions[sliceKey];
194
+ const reducer = (sliceDef.reduce as ReducerMap)[reducerKey];
195
+
196
+ if (reducer) {
197
+ const currentSlice = currentState[sliceKey as keyof InferState<TState>];
198
+ const newSlice = reducer(currentSlice, envelope.data, envelope.meta);
199
+ this.stateStore.setSlice(sliceKey as keyof InferState<TState>, newSlice);
200
+ }
201
+ }
202
+
203
+ for (const listener of this.publicEventListeners) {
204
+ listener(envelope);
205
+ }
206
+
207
+ return true;
208
+ }
209
+
210
+ processChannelEvent(envelope: PublicEventEnvelope): boolean {
211
+ const dedupKey = `channel:${envelope.meta.eventId}`;
212
+
213
+ if (this.processedEventIds.has(dedupKey)) {
214
+ return false;
215
+ }
216
+
217
+ if (this.processedEventIds.size >= MAX_DEDUP_SIZE) {
218
+ const oldest = this.processedEventIds.values().next().value!;
219
+ this.processedEventIds.delete(oldest);
220
+ }
221
+
222
+ this.processedEventIds.add(dedupKey);
223
+
224
+ const reducerKey = `channel:${envelope.type}`;
225
+ const currentState = this.stateStore.getState();
226
+
227
+ for (const sliceKey of Object.keys(this.stateDefinitions)) {
228
+ const sliceDef = this.stateDefinitions[sliceKey];
229
+ const reducer = (sliceDef.reduce as ReducerMap)[reducerKey];
230
+
231
+ if (reducer) {
232
+ const currentSlice = currentState[sliceKey as keyof InferState<TState>];
233
+ const newSlice = reducer(currentSlice, envelope.data, envelope.meta);
234
+ this.stateStore.setSlice(sliceKey as keyof InferState<TState>, newSlice);
235
+ }
236
+ }
237
+
238
+ for (const listener of this.publicEventListeners) {
239
+ listener(envelope);
240
+ }
241
+
242
+ return true;
243
+ }
244
+
245
+ validateChannelEvent(eventType: string, data: unknown): boolean {
246
+ const definition = this.channelDefinitions?.[eventType];
247
+
248
+ if (!definition) {
249
+ return false;
250
+ }
251
+
252
+ return definition.schema.safeParse(data).success;
253
+ }
254
+
117
255
  processBatch(envelopes: EventEnvelope[]): number {
118
256
  let newCount = 0;
119
257
 
@@ -129,5 +267,6 @@ export class EventProcessor<
129
267
  reset(): void {
130
268
  this.processedEventIds.clear();
131
269
  this.eventListeners.clear();
270
+ this.publicEventListeners.clear();
132
271
  }
133
272
  }
package/src/index.ts CHANGED
@@ -9,11 +9,16 @@ export { DecentrlSDKError } from './errors.js';
9
9
  export { EventProcessor } from './event-processor.js';
10
10
  export { IdentityManager } from './identity-manager.js';
11
11
  export type { PersistOptions } from './persistence.js';
12
+ export { PublicChannelReader } from './public-channel-reader.js';
12
13
  export { StateStore } from './state-store.js';
13
14
  export { SyncManager } from './sync-manager.js';
14
15
  export type { DecentrlTransport } from './transport.js';
15
16
  export type {
16
17
  ArchivedContract,
18
+ ChannelDefinition,
19
+ ChannelDefinitions,
20
+ ChannelFetchOptions,
21
+ ChannelSubscriptionConfig,
17
22
  DecentrlAppConfig,
18
23
  DecentrlClientConfig,
19
24
  EventDefinition,
@@ -25,6 +30,12 @@ export type {
25
30
  PaginatedResult,
26
31
  PaginationMeta,
27
32
  PendingContractRequest,
33
+ PublicEventDefinition,
34
+ PublicEventDefinitions,
35
+ PublicEventEnvelope,
36
+ PublicEventMeta,
37
+ PublicEventResult,
38
+ PublicQueryOptions,
28
39
  PublishOptions,
29
40
  QueryOptions,
30
41
  SerializedIdentity,
@@ -0,0 +1,239 @@
1
+ import { multibaseDecode, verifyJsonSignature } from '@decentrl/crypto';
2
+ import { resolveMediatorServiceEndpoint } from '@decentrl/identity/mediator/mediator.resolver';
3
+ import type { EventProcessor } from './event-processor.js';
4
+ import type { DecentrlTransport } from './transport.js';
5
+ import type {
6
+ ChannelDefinitions,
7
+ ChannelFetchOptions,
8
+ ChannelSubscriptionConfig,
9
+ EventDefinitions,
10
+ PaginatedResult,
11
+ PublicEventEnvelope,
12
+ PublicEventResult,
13
+ StateDefinitions,
14
+ } from './types.js';
15
+
16
+ interface ActiveSubscription {
17
+ intervalId: ReturnType<typeof setInterval>;
18
+ }
19
+
20
+ export class PublicChannelReader<
21
+ TEvents extends EventDefinitions,
22
+ TState extends StateDefinitions<TEvents>,
23
+ > {
24
+ private subscriptions = new Map<string, ActiveSubscription>();
25
+ private mediatorEndpointCache = new Map<string, string>();
26
+ private signingKeyCache = new Map<string, Uint8Array>();
27
+
28
+ constructor(
29
+ private transport: DecentrlTransport,
30
+ private eventProcessor: EventProcessor<TEvents, TState>,
31
+ private channelDefinitions?: ChannelDefinitions,
32
+ ) {}
33
+
34
+ subscribe(config: ChannelSubscriptionConfig): { unsubscribe: () => void } {
35
+ const subId = crypto.randomUUID();
36
+ const pollIntervalMs = config.pollIntervalMs ?? 10_000;
37
+
38
+ let afterTimestamp = Math.floor(Date.now() / 1000);
39
+
40
+ const poll = async () => {
41
+ try {
42
+ const mediatorEndpoint = await this.resolveMediatorEndpoint(config.did);
43
+
44
+ if (!this.transport.fetchPublicEvents) {
45
+ return;
46
+ }
47
+
48
+ const result = await this.transport.fetchPublicEvents({
49
+ mediatorEndpoint,
50
+ publisherDid: config.did,
51
+ channelId: config.channelId,
52
+ tags: config.tags,
53
+ afterTimestamp,
54
+ });
55
+
56
+ for (const event of result.events) {
57
+ const parsed = await this.parseAndVerifyPublicEvent(event, config.did);
58
+
59
+ if (parsed) {
60
+ this.eventProcessor.processChannelEvent(parsed);
61
+ }
62
+ }
63
+
64
+ if (result.events.length > 0) {
65
+ const maxTimestamp = Math.max(...result.events.map((e) => e.timestamp));
66
+ afterTimestamp = maxTimestamp;
67
+ }
68
+ } catch (err) {
69
+ console.warn('[Decentrl] Public channel poll failed:', err);
70
+ }
71
+ };
72
+
73
+ poll();
74
+
75
+ const intervalId = setInterval(poll, pollIntervalMs);
76
+ this.subscriptions.set(subId, { intervalId });
77
+
78
+ return {
79
+ unsubscribe: () => {
80
+ const sub = this.subscriptions.get(subId);
81
+
82
+ if (sub) {
83
+ clearInterval(sub.intervalId);
84
+ this.subscriptions.delete(subId);
85
+ }
86
+ },
87
+ };
88
+ }
89
+
90
+ async fetch(options: ChannelFetchOptions): Promise<PaginatedResult<PublicEventResult>> {
91
+ const mediatorEndpoint =
92
+ options.mediatorEndpoint ?? (await this.resolveMediatorEndpoint(options.did));
93
+
94
+ if (!this.transport.fetchPublicEvents) {
95
+ return { data: [], pagination: { page: 0, pageSize: 20, total: 0 } };
96
+ }
97
+
98
+ const result = await this.transport.fetchPublicEvents({
99
+ mediatorEndpoint,
100
+ publisherDid: options.did,
101
+ channelId: options.channelId,
102
+ tags: options.tags,
103
+ afterTimestamp: options.afterTimestamp,
104
+ beforeTimestamp: options.beforeTimestamp,
105
+ page: options.pagination?.page,
106
+ pageSize: options.pagination?.pageSize,
107
+ });
108
+
109
+ return {
110
+ data: result.events,
111
+ pagination: result.pagination,
112
+ };
113
+ }
114
+
115
+ dispose(): void {
116
+ for (const sub of this.subscriptions.values()) {
117
+ clearInterval(sub.intervalId);
118
+ }
119
+
120
+ this.subscriptions.clear();
121
+ }
122
+
123
+ private async parseAndVerifyPublicEvent(
124
+ raw: {
125
+ id: string;
126
+ channelId: string;
127
+ event: string;
128
+ tags: string[];
129
+ timestamp: number;
130
+ eventSignature: string;
131
+ },
132
+ publisherDid: string,
133
+ ): Promise<PublicEventEnvelope | null> {
134
+ try {
135
+ // Verify signature over { channel_id, event, tags, timestamp }
136
+ const signingKey = this.resolveSigningKeyFromDid(publisherDid);
137
+
138
+ if (signingKey) {
139
+ const signedData = {
140
+ channel_id: raw.channelId,
141
+ event: raw.event,
142
+ tags: raw.tags,
143
+ timestamp: raw.timestamp,
144
+ };
145
+
146
+ const isValid = verifyJsonSignature(signedData, raw.eventSignature, signingKey);
147
+
148
+ if (!isValid) {
149
+ console.warn(`[Decentrl] Invalid signature on public event ${raw.id}, skipping`);
150
+
151
+ return null;
152
+ }
153
+ }
154
+
155
+ const parsed = JSON.parse(raw.event);
156
+ const eventType = parsed.type;
157
+
158
+ if (!eventType || typeof eventType !== 'string') {
159
+ return null;
160
+ }
161
+
162
+ if (
163
+ this.channelDefinitions &&
164
+ !this.eventProcessor.validateChannelEvent(eventType, parsed.data)
165
+ ) {
166
+ return null;
167
+ }
168
+
169
+ return {
170
+ type: eventType,
171
+ data: parsed.data,
172
+ meta: {
173
+ publisherDid,
174
+ timestamp: raw.timestamp,
175
+ eventId: raw.id,
176
+ channelId: raw.channelId,
177
+ },
178
+ tags: raw.tags,
179
+ };
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+
185
+ private resolveSigningKeyFromDid(did: string): Uint8Array | null {
186
+ const cached = this.signingKeyCache.get(did);
187
+
188
+ if (cached) {
189
+ return cached;
190
+ }
191
+
192
+ // did:decentrl:alias:signingKey:encKey:mediatorHost
193
+ if (!did.startsWith('did:decentrl:')) {
194
+ return null;
195
+ }
196
+
197
+ const parts = did.replace('did:decentrl:', '').split(':');
198
+
199
+ if (parts.length < 2) {
200
+ return null;
201
+ }
202
+
203
+ try {
204
+ const key = multibaseDecode(parts[1]);
205
+ this.signingKeyCache.set(did, key);
206
+
207
+ return key;
208
+ } catch {
209
+ return null;
210
+ }
211
+ }
212
+
213
+ private async resolveMediatorEndpoint(did: string): Promise<string> {
214
+ const cached = this.mediatorEndpointCache.get(did);
215
+
216
+ if (cached) {
217
+ return cached;
218
+ }
219
+
220
+ const parts = did.replace('did:decentrl:', '').split(':');
221
+
222
+ if (parts.length >= 4) {
223
+ const mediatorDid = `did:web:${parts[3]}`;
224
+ const endpoint = await resolveMediatorServiceEndpoint(mediatorDid);
225
+ this.mediatorEndpointCache.set(did, endpoint);
226
+
227
+ return endpoint;
228
+ }
229
+
230
+ if (did.startsWith('did:web:')) {
231
+ const endpoint = await resolveMediatorServiceEndpoint(did);
232
+ this.mediatorEndpointCache.set(did, endpoint);
233
+
234
+ return endpoint;
235
+ }
236
+
237
+ throw new Error(`Cannot resolve mediator endpoint for DID: ${did}`);
238
+ }
239
+ }
@@ -364,4 +364,169 @@ describe('SDK E2E', () => {
364
364
  expect(client.identity.getIdentity()).toBeNull();
365
365
  });
366
366
  });
367
+
368
+ // ------- Public Events -------
369
+
370
+ describe('Public Events', () => {
371
+ const blogApp = defineDecentrlApp({
372
+ events: {},
373
+ state: {
374
+ posts: {
375
+ initial: [] as Array<{ id: string; title: string; publisherDid: string }>,
376
+ reduce: {
377
+ 'public:blog.post': (
378
+ posts,
379
+ data: { title: string; body: string },
380
+ meta: { eventId: string; publisherDid: string },
381
+ ) => [
382
+ ...posts,
383
+ { id: meta.eventId, title: data.title, publisherDid: meta.publisherDid },
384
+ ],
385
+ },
386
+ },
387
+ },
388
+ publicEvents: {
389
+ 'blog.post': {
390
+ schema: z.object({
391
+ title: z.string(),
392
+ body: z.string(),
393
+ }),
394
+ tags: ['blog', 'post:${title}'],
395
+ channelId: 'blog',
396
+ },
397
+ },
398
+ });
399
+
400
+ let publisher: ReturnType<typeof blogApp.createClient>;
401
+
402
+ beforeAll(async () => {
403
+ publisher = blogApp.createClient({ mediatorDid: MEDIATOR_DID });
404
+ await publisher.identity.create({ alias: 'publisher', mediatorDid: MEDIATOR_DID });
405
+ });
406
+
407
+ afterAll(() => {
408
+ publisher.reset();
409
+ });
410
+
411
+ it('should publish a public event', async () => {
412
+ const result = await publisher.publishPublic('blog.post', {
413
+ title: 'Hello World',
414
+ body: 'This is my first post',
415
+ });
416
+
417
+ expect(result.publicEventId).toBeDefined();
418
+ expect(typeof result.publicEventId).toBe('string');
419
+ });
420
+
421
+ it('should reject invalid public event data', async () => {
422
+ await expect(publisher.publishPublic('blog.post', { title: 123 } as any)).rejects.toThrow(
423
+ 'Schema validation failed',
424
+ );
425
+ });
426
+
427
+ it('should reject unknown public event type', async () => {
428
+ await expect(publisher.publishPublic('blog.unknown', { foo: 'bar' })).rejects.toThrow(
429
+ 'Unknown public event type',
430
+ );
431
+ });
432
+
433
+ it('should update local state via public: reducer', async () => {
434
+ const state = publisher.getState();
435
+ expect(state.posts.length).toBeGreaterThanOrEqual(1);
436
+ expect(state.posts[0].title).toBe('Hello World');
437
+ });
438
+
439
+ it('should query public events via GET endpoint', async () => {
440
+ const publisherDid = publisher.identity.getIdentity()!.did;
441
+ const result = await publisher.queryPublicEvents({
442
+ publisherDid,
443
+ channelId: 'blog',
444
+ });
445
+
446
+ expect(result.data.length).toBeGreaterThanOrEqual(1);
447
+ expect(result.data[0].channelId).toBe('blog');
448
+ expect(result.pagination.total).toBeGreaterThanOrEqual(1);
449
+
450
+ // Verify the event content is parseable
451
+ const parsed = JSON.parse(result.data[0].event);
452
+ expect(parsed.type).toBe('blog.post');
453
+ expect(parsed.data.title).toBe('Hello World');
454
+ });
455
+
456
+ it('should filter by tags', async () => {
457
+ const publisherDid = publisher.identity.getIdentity()!.did;
458
+ const result = await publisher.queryPublicEvents({
459
+ publisherDid,
460
+ tags: ['blog'],
461
+ });
462
+
463
+ expect(result.data.length).toBeGreaterThanOrEqual(1);
464
+ });
465
+
466
+ it('should return 404 for unregistered publisher', async () => {
467
+ await expect(
468
+ publisher.queryPublicEvents({
469
+ publisherDid: 'did:decentrl:nonexistent:key:key:localhost',
470
+ }),
471
+ ).rejects.toThrow();
472
+ });
473
+
474
+ it('should publish multiple and paginate', async () => {
475
+ await publisher.publishPublic('blog.post', {
476
+ title: 'Second Post',
477
+ body: 'More content',
478
+ });
479
+ await publisher.publishPublic('blog.post', {
480
+ title: 'Third Post',
481
+ body: 'Even more content',
482
+ });
483
+
484
+ const publisherDid = publisher.identity.getIdentity()!.did;
485
+ const page1 = await publisher.queryPublicEvents({
486
+ publisherDid,
487
+ channelId: 'blog',
488
+ pagination: { page: 0, pageSize: 2 },
489
+ });
490
+
491
+ expect(page1.data.length).toBe(2);
492
+ expect(page1.pagination.total).toBeGreaterThanOrEqual(3);
493
+ });
494
+
495
+ it('should delete a public event', async () => {
496
+ const publisherDid = publisher.identity.getIdentity()!.did;
497
+
498
+ // Get events
499
+ const before = await publisher.queryPublicEvents({
500
+ publisherDid,
501
+ channelId: 'blog',
502
+ });
503
+
504
+ const eventToDelete = before.data[0];
505
+ await publisher.deletePublicEvent(eventToDelete.id);
506
+
507
+ const after = await publisher.queryPublicEvents({
508
+ publisherDid,
509
+ channelId: 'blog',
510
+ });
511
+
512
+ expect(after.pagination.total).toBe(before.pagination.total - 1);
513
+ expect(after.data.find((e) => e.id === eventToDelete.id)).toBeUndefined();
514
+ });
515
+
516
+ it('should include event_signature that can be verified', async () => {
517
+ const publisherDid = publisher.identity.getIdentity()!.did;
518
+ const result = await publisher.queryPublicEvents({
519
+ publisherDid,
520
+ channelId: 'blog',
521
+ });
522
+
523
+ expect(result.data.length).toBeGreaterThanOrEqual(1);
524
+
525
+ for (const event of result.data) {
526
+ expect(event.eventSignature).toBeDefined();
527
+ expect(typeof event.eventSignature).toBe('string');
528
+ expect(event.eventSignature.length).toBeGreaterThan(0);
529
+ }
530
+ });
531
+ });
367
532
  });
package/src/transport.ts CHANGED
@@ -61,5 +61,37 @@ export interface DecentrlTransport {
61
61
  pageSize: number;
62
62
  }): Promise<import('./types.js').PaginatedResult<EventEnvelope>>;
63
63
 
64
+ // Optional: publish public events (requires identity)
65
+ publishPublicEvent?(options: {
66
+ channelId: string;
67
+ event: string;
68
+ tags: string[];
69
+ }): Promise<{ publicEventId: string }>;
70
+
71
+ // Optional: delete public events (requires identity)
72
+ deletePublicEvent?(publicEventId: string, eventType?: string): Promise<void>;
73
+
74
+ // Optional: fetch public events (no identity needed)
75
+ fetchPublicEvents?(options: {
76
+ mediatorEndpoint: string;
77
+ publisherDid: string;
78
+ channelId?: string;
79
+ tags?: string[];
80
+ afterTimestamp?: number;
81
+ beforeTimestamp?: number;
82
+ page?: number;
83
+ pageSize?: number;
84
+ }): Promise<{
85
+ events: Array<{
86
+ id: string;
87
+ channelId: string;
88
+ event: string;
89
+ tags: string[];
90
+ timestamp: number;
91
+ eventSignature: string;
92
+ }>;
93
+ pagination: { page: number; pageSize: number; total: number };
94
+ }>;
95
+
64
96
  dispose(): void;
65
97
  }