@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,273 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ vi.mock('@decentrl/crypto', () => ({
3
+ base64Decode: vi.fn((s) => new TextEncoder().encode(s)),
4
+ base64Encode: vi.fn((u) => new TextDecoder().decode(u)),
5
+ decryptString: vi.fn(),
6
+ encryptString: vi.fn((plaintext) => `encrypted:${plaintext}`),
7
+ generateEncryptedTag: vi.fn((_key, tag) => `etag:${tag}`),
8
+ signJsonObject: vi.fn(() => 'mock-signature'),
9
+ verifyJsonSignature: vi.fn(() => true),
10
+ multibaseDecode: vi.fn(() => new Uint8Array(32)),
11
+ }));
12
+ vi.mock('@decentrl/identity/communication-channels/mediator/direct-authenticated/command/command.service', () => ({
13
+ generateDirectAuthenticatedMediatorCommand: vi.fn(() => ({ mock: 'command' })),
14
+ generateTwoWayPrivateMediatorCommand: vi.fn(() => ({ mock: 'two-way-command' })),
15
+ }));
16
+ vi.mock('axios', () => ({
17
+ default: {
18
+ post: vi.fn(async () => ({ data: { type: 'SUCCESS', payload: {} } })),
19
+ },
20
+ }));
21
+ import { decryptString } from '@decentrl/crypto';
22
+ import { DecentrlEventStore } from './event-store.js';
23
+ const makeContract = (opts) => ({
24
+ id: opts.id,
25
+ participantDid: opts.participantDid,
26
+ signedCommunicationContract: {
27
+ communication_contract: {
28
+ requestor_did: 'did:decentrl:alice',
29
+ recipient_did: opts.participantDid,
30
+ requestor_signing_key_id: 'did:decentrl:alice#signing',
31
+ recipient_signing_key_id: `${opts.participantDid}#signing`,
32
+ requestor_encryption_public_key: 'key-a',
33
+ recipient_encryption_public_key: 'key-b',
34
+ expires_at: opts.expiresAt,
35
+ timestamp: opts.timestamp,
36
+ },
37
+ requestor_signature: 'sig-a',
38
+ recipient_signature: 'sig-b',
39
+ },
40
+ rootSecret: opts.rootSecret ?? 'secret-key',
41
+ });
42
+ const mockIdentity = {
43
+ did: 'did:decentrl:alice',
44
+ keys: {
45
+ signing: {
46
+ privateKey: new Uint8Array(32),
47
+ publicKey: new Uint8Array(32),
48
+ },
49
+ encryption: {
50
+ privateKey: new Uint8Array(32),
51
+ publicKey: new Uint8Array(32),
52
+ },
53
+ storageKey: new Uint8Array(32),
54
+ },
55
+ mediatorEndpoint: 'http://mediator',
56
+ mediatorDid: 'did:web:mediator',
57
+ };
58
+ describe('DecentrlEventStore', () => {
59
+ describe('processPreFetchedPendingEvents', () => {
60
+ it('tries multiple contracts for decryption', async () => {
61
+ const now = Math.floor(Date.now() / 1000);
62
+ const oldContract = makeContract({
63
+ id: 'old-contract',
64
+ participantDid: 'did:decentrl:bob',
65
+ expiresAt: now + 3600,
66
+ timestamp: now,
67
+ rootSecret: 'old-secret',
68
+ });
69
+ const newContract = makeContract({
70
+ id: 'new-contract',
71
+ participantDid: 'did:decentrl:bob',
72
+ expiresAt: now + 7200,
73
+ timestamp: now + 100,
74
+ rootSecret: 'new-secret',
75
+ });
76
+ const mockDecrypt = vi.mocked(decryptString);
77
+ mockDecrypt
78
+ .mockImplementationOnce(() => {
79
+ // First attempt with new-contract key fails
80
+ throw new Error('decryption failed');
81
+ })
82
+ .mockImplementationOnce(() => {
83
+ // Second attempt with old-contract key succeeds
84
+ return JSON.stringify({
85
+ contract_id: 'old-contract',
86
+ event: JSON.stringify({ type: 'test', data: 'hello' }),
87
+ timestamp: now,
88
+ signature: 'mock-signature',
89
+ });
90
+ });
91
+ const eventStore = new DecentrlEventStore({
92
+ identity: mockIdentity,
93
+ communicationContracts: () => [oldContract, newContract],
94
+ });
95
+ const rawEvents = [
96
+ {
97
+ id: 'event-1',
98
+ sender_did: 'did:decentrl:bob',
99
+ payload: 'encrypted-payload',
100
+ },
101
+ ];
102
+ const result = await eventStore.processPreFetchedPendingEvents(rawEvents);
103
+ expect(result).toHaveLength(1);
104
+ expect(mockDecrypt).toHaveBeenCalledTimes(2);
105
+ });
106
+ it('skips events from unknown senders', async () => {
107
+ const now = Math.floor(Date.now() / 1000);
108
+ const contract = makeContract({
109
+ id: 'c1',
110
+ participantDid: 'did:decentrl:bob',
111
+ expiresAt: now + 3600,
112
+ timestamp: now,
113
+ });
114
+ const eventStore = new DecentrlEventStore({
115
+ identity: mockIdentity,
116
+ communicationContracts: () => [contract],
117
+ });
118
+ const rawEvents = [
119
+ {
120
+ id: 'event-1',
121
+ sender_did: 'did:decentrl:unknown',
122
+ payload: 'encrypted-payload',
123
+ },
124
+ ];
125
+ const result = await eventStore.processPreFetchedPendingEvents(rawEvents);
126
+ expect(result).toHaveLength(0);
127
+ });
128
+ it('calls onContractUsed when event is successfully decrypted', async () => {
129
+ const now = Math.floor(Date.now() / 1000);
130
+ const contract = makeContract({
131
+ id: 'test-contract',
132
+ participantDid: 'did:decentrl:bob',
133
+ expiresAt: now + 3600,
134
+ timestamp: now,
135
+ });
136
+ const mockDecrypt = vi.mocked(decryptString);
137
+ mockDecrypt.mockReturnValue(JSON.stringify({
138
+ contract_id: 'test-contract',
139
+ event: JSON.stringify({ type: 'test', data: 'hello' }),
140
+ timestamp: now,
141
+ signature: 'mock-signature',
142
+ }));
143
+ const onContractUsed = vi.fn();
144
+ const eventStore = new DecentrlEventStore({
145
+ identity: mockIdentity,
146
+ communicationContracts: () => [contract],
147
+ onContractUsed,
148
+ });
149
+ const rawEvents = [
150
+ {
151
+ id: 'event-1',
152
+ sender_did: 'did:decentrl:bob',
153
+ payload: 'encrypted-payload',
154
+ },
155
+ ];
156
+ await eventStore.processPreFetchedPendingEvents(rawEvents);
157
+ expect(onContractUsed).toHaveBeenCalledWith('test-contract', 'did:decentrl:bob');
158
+ });
159
+ it('returns empty array when no active contracts exist', async () => {
160
+ const eventStore = new DecentrlEventStore({
161
+ identity: mockIdentity,
162
+ communicationContracts: () => [],
163
+ });
164
+ const rawEvents = [
165
+ {
166
+ id: 'event-1',
167
+ sender_did: 'did:decentrl:bob',
168
+ payload: 'encrypted-payload',
169
+ },
170
+ ];
171
+ const result = await eventStore.processPreFetchedPendingEvents(rawEvents);
172
+ expect(result).toHaveLength(0);
173
+ });
174
+ it('skips events that no contract can decrypt', async () => {
175
+ const now = Math.floor(Date.now() / 1000);
176
+ const contract = makeContract({
177
+ id: 'c1',
178
+ participantDid: 'did:decentrl:bob',
179
+ expiresAt: now + 3600,
180
+ timestamp: now,
181
+ });
182
+ const mockDecrypt = vi.mocked(decryptString);
183
+ mockDecrypt.mockImplementation(() => {
184
+ throw new Error('decryption failed');
185
+ });
186
+ const eventStore = new DecentrlEventStore({
187
+ identity: mockIdentity,
188
+ communicationContracts: () => [contract],
189
+ });
190
+ const rawEvents = [
191
+ {
192
+ id: 'event-1',
193
+ sender_did: 'did:decentrl:bob',
194
+ payload: 'encrypted-payload',
195
+ },
196
+ ];
197
+ const result = await eventStore.processPreFetchedPendingEvents(rawEvents);
198
+ expect(result).toHaveLength(0);
199
+ });
200
+ it('prefers contract with latest expires_at for decryption', async () => {
201
+ const now = Math.floor(Date.now() / 1000);
202
+ const oldContract = makeContract({
203
+ id: 'old',
204
+ participantDid: 'did:decentrl:bob',
205
+ expiresAt: now + 3600,
206
+ timestamp: now,
207
+ rootSecret: 'old-key',
208
+ });
209
+ const newContract = makeContract({
210
+ id: 'new',
211
+ participantDid: 'did:decentrl:bob',
212
+ expiresAt: now + 7200,
213
+ timestamp: now + 100,
214
+ rootSecret: 'new-key',
215
+ });
216
+ const decryptCalls = [];
217
+ const mockDecrypt = vi.mocked(decryptString);
218
+ mockDecrypt.mockImplementation((_payload, key) => {
219
+ const keyStr = new TextDecoder().decode(key);
220
+ decryptCalls.push(keyStr);
221
+ if (keyStr === 'new-key') {
222
+ return JSON.stringify({
223
+ contract_id: 'new',
224
+ event: JSON.stringify({ type: 'test' }),
225
+ timestamp: now,
226
+ signature: 'mock-signature',
227
+ });
228
+ }
229
+ throw new Error('wrong key');
230
+ });
231
+ const eventStore = new DecentrlEventStore({
232
+ identity: mockIdentity,
233
+ communicationContracts: () => [oldContract, newContract],
234
+ });
235
+ const rawEvents = [
236
+ {
237
+ id: 'event-1',
238
+ sender_did: 'did:decentrl:bob',
239
+ payload: 'encrypted-payload',
240
+ },
241
+ ];
242
+ await eventStore.processPreFetchedPendingEvents(rawEvents);
243
+ // Should try new-key first (latest expires_at)
244
+ expect(decryptCalls[0]).toBe('new-key');
245
+ });
246
+ });
247
+ describe('publishEvent', () => {
248
+ beforeEach(() => {
249
+ vi.mocked(decryptString).mockReset();
250
+ });
251
+ it('stores locally for non-ephemeral events', async () => {
252
+ const axios = await import('axios');
253
+ const eventStore = new DecentrlEventStore({
254
+ identity: mockIdentity,
255
+ communicationContracts: () => [],
256
+ });
257
+ await eventStore.publishEvent({ type: 'test' }, { tags: ['tag1'] });
258
+ // Should have called axios.post for SAVE_EVENTS
259
+ expect(axios.default.post).toHaveBeenCalled();
260
+ });
261
+ it('skips local storage for ephemeral events without recipient', async () => {
262
+ const axios = await import('axios');
263
+ vi.mocked(axios.default.post).mockClear();
264
+ const eventStore = new DecentrlEventStore({
265
+ identity: mockIdentity,
266
+ communicationContracts: () => [],
267
+ });
268
+ await eventStore.publishEvent({ type: 'test' }, { tags: ['tag1'], ephemeral: true });
269
+ // Should NOT have called axios.post since no recipient and ephemeral
270
+ expect(axios.default.post).not.toHaveBeenCalled();
271
+ });
272
+ });
273
+ });
@@ -0,0 +1,4 @@
1
+ export { DecentrlEventStore } from './event-store';
2
+ export type { EventStoreConfig, PaginatedResult, PaginationMeta, PublishOptions, QueryOptions, StoredSignedContract, } from './types';
3
+ export { EventStoreError } from './types';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,YAAY,EACX,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,cAAc,EACd,YAAY,EACZ,oBAAoB,GACpB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { DecentrlEventStore } from './event-store';
2
+ export { EventStoreError } from './types';
@@ -0,0 +1,53 @@
1
+ import type { DecentrlIdentityKeys } from '@decentrl/crypto';
2
+ import type { SignedCommunicationContract } from '@decentrl/identity/communication-contract/communication-contract.schema';
3
+ export interface EventStoreConfig {
4
+ identity: {
5
+ did: string;
6
+ keys: DecentrlIdentityKeys;
7
+ mediatorEndpoint: string;
8
+ mediatorDid: string;
9
+ };
10
+ communicationContracts: () => StoredSignedContract[];
11
+ onContractUsed?: (contractId: string, participantDid: string) => void;
12
+ }
13
+ /**
14
+ * Minimal contract shape needed by the event store.
15
+ * The canonical definition lives in @decentrl/sdk types.
16
+ */
17
+ export interface StoredSignedContract {
18
+ id: string;
19
+ participantDid: string;
20
+ signedCommunicationContract: SignedCommunicationContract;
21
+ rootSecret: string;
22
+ }
23
+ export interface PublishOptions {
24
+ recipient?: string;
25
+ tags: string[];
26
+ ephemeral?: boolean;
27
+ }
28
+ export interface QueryOptions {
29
+ tags?: string[];
30
+ participantDid?: string;
31
+ afterTimestamp?: number;
32
+ beforeTimestamp?: number;
33
+ pagination?: {
34
+ page: number;
35
+ pageSize: number;
36
+ };
37
+ unprocessedOnly?: boolean;
38
+ }
39
+ export interface PaginationMeta {
40
+ page: number;
41
+ pageSize: number;
42
+ total: number;
43
+ }
44
+ export interface PaginatedResult<T> {
45
+ data: T[];
46
+ pagination: PaginationMeta;
47
+ }
48
+ export declare class EventStoreError extends Error {
49
+ code: string;
50
+ details?: unknown | undefined;
51
+ constructor(message: string, code: string, details?: unknown | undefined);
52
+ }
53
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +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"}
package/dist/types.js ADDED
@@ -0,0 +1,10 @@
1
+ export class EventStoreError extends Error {
2
+ code;
3
+ details;
4
+ constructor(message, code, details) {
5
+ super(message);
6
+ this.code = code;
7
+ this.details = details;
8
+ this.name = 'EventStoreError';
9
+ }
10
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@decentrl/event-store",
3
+ "version": "0.0.1",
4
+ "description": "Event-driven storage and communication library for decentrl",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "dependencies": {
8
+ "axios": "^1.6.0",
9
+ "@decentrl/crypto": "0.0.1",
10
+ "@decentrl/identity": "0.0.1"
11
+ },
12
+ "devDependencies": {
13
+ "typescript": "^5.0.0",
14
+ "vitest": "3.2.4"
15
+ },
16
+ "peerDependencies": {
17
+ "typescript": "^5.0.0"
18
+ },
19
+ "type": "module",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "test": "vitest run",
26
+ "test:unit": "vitest run",
27
+ "dev": "tsc --watch"
28
+ }
29
+ }