@decentrl/sdk 0.0.7 → 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.
- package/dist/client.d.ts +15 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +74 -14
- package/dist/define-app.d.ts +5 -5
- package/dist/define-app.d.ts.map +1 -1
- package/dist/direct-transport.d.ts +32 -0
- package/dist/direct-transport.d.ts.map +1 -1
- package/dist/direct-transport.js +26 -0
- package/dist/event-processor.d.ts +12 -2
- package/dist/event-processor.d.ts.map +1 -1
- package/dist/event-processor.js +100 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/public-channel-reader.d.ts +21 -0
- package/dist/public-channel-reader.d.ts.map +1 -0
- package/dist/public-channel-reader.js +169 -0
- package/dist/transport.d.ts +32 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/types.d.ts +84 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/client.ts +132 -19
- package/src/define-app.ts +19 -5
- package/src/direct-transport-public.test.ts +139 -0
- package/src/direct-transport.ts +55 -0
- package/src/event-processor-public.test.ts +343 -0
- package/src/event-processor.ts +144 -5
- package/src/index.ts +11 -0
- package/src/public-channel-reader.ts +239 -0
- package/src/sdk.e2e.test.ts +165 -0
- package/src/transport.ts +32 -0
- package/src/types.ts +138 -13
package/src/event-processor.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/sdk.e2e.test.ts
CHANGED
|
@@ -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
|
}
|