@decentrl/sdk 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.
Files changed (74) hide show
  1. package/dist/client.d.ts +36 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +192 -0
  4. package/dist/contract-manager.d.ts +23 -0
  5. package/dist/contract-manager.d.ts.map +1 -0
  6. package/dist/contract-manager.js +91 -0
  7. package/dist/define-app.d.ts +8 -0
  8. package/dist/define-app.d.ts.map +1 -0
  9. package/dist/define-app.js +7 -0
  10. package/dist/direct-transport.d.ts +69 -0
  11. package/dist/direct-transport.d.ts.map +1 -0
  12. package/dist/direct-transport.js +450 -0
  13. package/dist/errors.d.ts +7 -0
  14. package/dist/errors.d.ts.map +1 -0
  15. package/dist/errors.js +10 -0
  16. package/dist/event-processor.d.ts +19 -0
  17. package/dist/event-processor.d.ts.map +1 -0
  18. package/dist/event-processor.js +93 -0
  19. package/dist/identity-manager.d.ts +22 -0
  20. package/dist/identity-manager.d.ts.map +1 -0
  21. package/dist/identity-manager.js +62 -0
  22. package/dist/identity-serialization.d.ts +5 -0
  23. package/dist/identity-serialization.d.ts.map +1 -0
  24. package/dist/identity-serialization.js +30 -0
  25. package/dist/index.d.ts +18 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +10 -0
  28. package/dist/persistence.d.ts +11 -0
  29. package/dist/persistence.d.ts.map +1 -0
  30. package/dist/persistence.js +82 -0
  31. package/dist/state-store.d.ts +12 -0
  32. package/dist/state-store.d.ts.map +1 -0
  33. package/dist/state-store.js +32 -0
  34. package/dist/sync-manager.d.ts +33 -0
  35. package/dist/sync-manager.d.ts.map +1 -0
  36. package/dist/sync-manager.js +244 -0
  37. package/dist/tag-templates.d.ts +2 -0
  38. package/dist/tag-templates.d.ts.map +1 -0
  39. package/dist/tag-templates.js +23 -0
  40. package/dist/test-helpers.d.ts +15 -0
  41. package/dist/test-helpers.d.ts.map +1 -0
  42. package/dist/test-helpers.js +65 -0
  43. package/dist/transport.d.ts +41 -0
  44. package/dist/transport.d.ts.map +1 -0
  45. package/dist/transport.js +1 -0
  46. package/dist/types.d.ts +131 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/types.js +1 -0
  49. package/dist/websocket-transport.d.ts +36 -0
  50. package/dist/websocket-transport.d.ts.map +1 -0
  51. package/dist/websocket-transport.js +160 -0
  52. package/package.json +35 -0
  53. package/src/client.ts +277 -0
  54. package/src/contract-manager.test.ts +207 -0
  55. package/src/contract-manager.ts +130 -0
  56. package/src/define-app.ts +25 -0
  57. package/src/direct-transport.test.ts +460 -0
  58. package/src/direct-transport.ts +729 -0
  59. package/src/errors.ts +23 -0
  60. package/src/event-processor.ts +133 -0
  61. package/src/identity-manager.ts +91 -0
  62. package/src/identity-serialization.ts +33 -0
  63. package/src/index.ts +43 -0
  64. package/src/persistence.ts +103 -0
  65. package/src/sdk.e2e.test.ts +367 -0
  66. package/src/state-store.ts +42 -0
  67. package/src/sync-manager.test.ts +414 -0
  68. package/src/sync-manager.ts +308 -0
  69. package/src/tag-templates.test.ts +111 -0
  70. package/src/tag-templates.ts +30 -0
  71. package/src/test-helpers.ts +88 -0
  72. package/src/transport.ts +65 -0
  73. package/src/types.ts +191 -0
  74. package/src/websocket-transport.ts +233 -0
package/src/errors.ts ADDED
@@ -0,0 +1,23 @@
1
+ export type DecentrlSDKErrorCode =
2
+ | 'IDENTITY_NOT_INITIALIZED'
3
+ | 'IDENTITY_ALREADY_EXISTS'
4
+ | 'CONTRACT_NOT_FOUND'
5
+ | 'SCHEMA_VALIDATION_FAILED'
6
+ | 'UNKNOWN_EVENT_TYPE'
7
+ | 'SYNC_FAILED'
8
+ | 'PUBLISH_FAILED'
9
+ | 'QUERY_FAILED'
10
+ | 'QUERY_NOT_SUPPORTED'
11
+ | 'MEDIATOR_ERROR'
12
+ | 'NO_TRANSPORT';
13
+
14
+ export class DecentrlSDKError extends Error {
15
+ constructor(
16
+ message: string,
17
+ public code: DecentrlSDKErrorCode,
18
+ public details?: unknown,
19
+ ) {
20
+ super(message);
21
+ this.name = 'DecentrlSDKError';
22
+ }
23
+ }
@@ -0,0 +1,133 @@
1
+ import { DecentrlSDKError } from './errors.js';
2
+ import type { StateStore } from './state-store.js';
3
+ import { evaluateTagTemplates } from './tag-templates.js';
4
+ import type {
5
+ EventDefinitions,
6
+ EventEnvelope,
7
+ EventMeta,
8
+ InferState,
9
+ StateDefinitions,
10
+ } from './types.js';
11
+
12
+ const MAX_DEDUP_SIZE = 10_000;
13
+
14
+ export class EventProcessor<
15
+ TEvents extends EventDefinitions,
16
+ TState extends StateDefinitions<TEvents>,
17
+ > {
18
+ private processedEventIds = new Set<string>();
19
+ private eventListeners = new Set<(envelope: EventEnvelope) => void>();
20
+
21
+ constructor(
22
+ private eventDefinitions: TEvents,
23
+ private stateDefinitions: TState,
24
+ private stateStore: StateStore<InferState<TState>>,
25
+ ) {}
26
+
27
+ onEvent(listener: (envelope: EventEnvelope) => void): () => void {
28
+ this.eventListeners.add(listener);
29
+
30
+ return () => this.eventListeners.delete(listener);
31
+ }
32
+
33
+ validate(eventType: string, data: unknown): void {
34
+ const definition = this.eventDefinitions[eventType];
35
+
36
+ if (!definition) {
37
+ throw new DecentrlSDKError(`Unknown event type: ${eventType}`, 'UNKNOWN_EVENT_TYPE', {
38
+ eventType,
39
+ });
40
+ }
41
+
42
+ const result = definition.schema.safeParse(data);
43
+
44
+ if (!result.success) {
45
+ throw new DecentrlSDKError(
46
+ `Schema validation failed for event type "${eventType}": ${result.error.message}`,
47
+ 'SCHEMA_VALIDATION_FAILED',
48
+ { eventType, errors: result.error.issues },
49
+ );
50
+ }
51
+ }
52
+
53
+ computeTags(eventType: string, data: unknown): string[] {
54
+ const definition = this.eventDefinitions[eventType];
55
+
56
+ if (!definition) {
57
+ throw new DecentrlSDKError(`Unknown event type: ${eventType}`, 'UNKNOWN_EVENT_TYPE', {
58
+ eventType,
59
+ });
60
+ }
61
+
62
+ return evaluateTagTemplates(definition.tags, data);
63
+ }
64
+
65
+ computeTagsSafe(eventType: string, data: unknown): string[] {
66
+ try {
67
+ return this.computeTags(eventType, data);
68
+ } catch {
69
+ return [];
70
+ }
71
+ }
72
+
73
+ buildEnvelope(eventType: string, data: unknown, meta: EventMeta): EventEnvelope {
74
+ return { type: eventType, data, meta };
75
+ }
76
+
77
+ processEvent(envelope: EventEnvelope): boolean {
78
+ const isEphemeral = envelope.meta.ephemeral === true;
79
+
80
+ if (!isEphemeral) {
81
+ if (this.processedEventIds.has(envelope.meta.eventId)) {
82
+ return false;
83
+ }
84
+
85
+ if (this.processedEventIds.size >= MAX_DEDUP_SIZE) {
86
+ // Set iterates in insertion order — first value is the oldest
87
+ const oldest = this.processedEventIds.values().next().value!;
88
+ this.processedEventIds.delete(oldest);
89
+ }
90
+
91
+ this.processedEventIds.add(envelope.meta.eventId);
92
+ }
93
+
94
+ const currentState = this.stateStore.getState();
95
+
96
+ for (const sliceKey of Object.keys(this.stateDefinitions)) {
97
+ 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;
102
+
103
+ if (reducer) {
104
+ const currentSlice = currentState[sliceKey as keyof InferState<TState>];
105
+ const newSlice = reducer(currentSlice, envelope.data, envelope.meta);
106
+ this.stateStore.setSlice(sliceKey as keyof InferState<TState>, newSlice);
107
+ }
108
+ }
109
+
110
+ for (const listener of this.eventListeners) {
111
+ listener(envelope);
112
+ }
113
+
114
+ return true;
115
+ }
116
+
117
+ processBatch(envelopes: EventEnvelope[]): number {
118
+ let newCount = 0;
119
+
120
+ for (const envelope of envelopes) {
121
+ if (this.processEvent(envelope)) {
122
+ newCount++;
123
+ }
124
+ }
125
+
126
+ return newCount;
127
+ }
128
+
129
+ reset(): void {
130
+ this.processedEventIds.clear();
131
+ this.eventListeners.clear();
132
+ }
133
+ }
@@ -0,0 +1,91 @@
1
+ import { DecentrlSDKError } from './errors.js';
2
+ import { deserializeKeys, serializeIdentity } from './identity-serialization.js';
3
+ import type { DecentrlTransport } from './transport.js';
4
+ import type { IdentityState, SerializedIdentity } from './types.js';
5
+
6
+ type IdentityChangeListener = (identity: IdentityState | null) => void;
7
+
8
+ export class IdentityManager {
9
+ private identity: IdentityState | null = null;
10
+ private listeners = new Set<IdentityChangeListener>();
11
+ private transport: DecentrlTransport | null = null;
12
+
13
+ setTransport(transport: DecentrlTransport): void {
14
+ this.transport = transport;
15
+ }
16
+
17
+ getIdentity(): IdentityState | null {
18
+ return this.identity;
19
+ }
20
+
21
+ requireIdentity(): IdentityState {
22
+ if (!this.identity) {
23
+ throw new DecentrlSDKError(
24
+ 'Identity not initialized. Call create() or load() first.',
25
+ 'IDENTITY_NOT_INITIALIZED',
26
+ );
27
+ }
28
+
29
+ return this.identity;
30
+ }
31
+
32
+ async create(options: { alias: string; mediatorDid: string }): Promise<IdentityState> {
33
+ if (this.identity) {
34
+ throw new DecentrlSDKError(
35
+ 'Identity already exists. Call reset() first to create a new one.',
36
+ 'IDENTITY_ALREADY_EXISTS',
37
+ );
38
+ }
39
+
40
+ if (!this.transport) {
41
+ throw new DecentrlSDKError('Transport not set', 'NO_TRANSPORT');
42
+ }
43
+
44
+ const serialized = await this.transport.createIdentity(options);
45
+
46
+ return this.load(serialized);
47
+ }
48
+
49
+ load(serialized: SerializedIdentity): IdentityState {
50
+ this.identity = {
51
+ did: serialized.did,
52
+ alias: serialized.alias,
53
+ mediatorDid: serialized.mediatorDid,
54
+ mediatorEndpoint: serialized.mediatorEndpoint,
55
+ keys: deserializeKeys(serialized),
56
+ mediatorContract: serialized.mediatorContract,
57
+ };
58
+
59
+ // Sync with transport
60
+ this.transport?.loadIdentity?.(serialized);
61
+
62
+ this.notify();
63
+
64
+ return this.identity;
65
+ }
66
+
67
+ serialize(): SerializedIdentity {
68
+ const identity = this.requireIdentity();
69
+
70
+ return serializeIdentity(identity);
71
+ }
72
+
73
+ reset(): void {
74
+ this.identity = null;
75
+ this.notify();
76
+ }
77
+
78
+ onChange(listener: IdentityChangeListener): () => void {
79
+ this.listeners.add(listener);
80
+
81
+ return () => {
82
+ this.listeners.delete(listener);
83
+ };
84
+ }
85
+
86
+ private notify(): void {
87
+ for (const listener of this.listeners) {
88
+ listener(this.identity);
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,33 @@
1
+ import { base64Decode, base64Encode, type DecentrlIdentityKeys } from '@decentrl/crypto';
2
+ import type { IdentityState, SerializedIdentity } from './types.js';
3
+
4
+ export const serializeIdentity = (identity: IdentityState): SerializedIdentity => ({
5
+ did: identity.did,
6
+ alias: identity.alias,
7
+ mediatorDid: identity.mediatorDid,
8
+ mediatorEndpoint: identity.mediatorEndpoint,
9
+ keys: {
10
+ signing: {
11
+ privateKey: base64Encode(identity.keys.signing.privateKey),
12
+ publicKey: base64Encode(identity.keys.signing.publicKey),
13
+ },
14
+ encryption: {
15
+ privateKey: base64Encode(identity.keys.encryption.privateKey),
16
+ publicKey: base64Encode(identity.keys.encryption.publicKey),
17
+ },
18
+ storageKey: base64Encode(identity.keys.storageKey),
19
+ },
20
+ mediatorContract: identity.mediatorContract,
21
+ });
22
+
23
+ export const deserializeKeys = (serialized: SerializedIdentity): DecentrlIdentityKeys => ({
24
+ signing: {
25
+ privateKey: base64Decode(serialized.keys.signing.privateKey),
26
+ publicKey: base64Decode(serialized.keys.signing.publicKey),
27
+ },
28
+ encryption: {
29
+ privateKey: base64Decode(serialized.keys.encryption.privateKey),
30
+ publicKey: base64Decode(serialized.keys.encryption.publicKey),
31
+ },
32
+ storageKey: base64Decode(serialized.keys.storageKey),
33
+ });
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ export { DecentrlClient } from './client.js';
2
+ export { ContractManager } from './contract-manager.js';
3
+ export type { DecentrlApp } from './define-app.js';
4
+ export { defineDecentrlApp } from './define-app.js';
5
+ export type { DirectTransportOptions, HttpPost } from './direct-transport.js';
6
+ export { DirectTransport } from './direct-transport.js';
7
+ export type { DecentrlSDKErrorCode } from './errors.js';
8
+ export { DecentrlSDKError } from './errors.js';
9
+ export { EventProcessor } from './event-processor.js';
10
+ export { IdentityManager } from './identity-manager.js';
11
+ export type { PersistOptions } from './persistence.js';
12
+ export { StateStore } from './state-store.js';
13
+ export { SyncManager } from './sync-manager.js';
14
+ export type { DecentrlTransport } from './transport.js';
15
+ export type {
16
+ ArchivedContract,
17
+ DecentrlAppConfig,
18
+ DecentrlClientConfig,
19
+ EventDefinition,
20
+ EventDefinitions,
21
+ EventEnvelope,
22
+ EventMeta,
23
+ IdentityState,
24
+ InferState,
25
+ PaginatedResult,
26
+ PaginationMeta,
27
+ PendingContractRequest,
28
+ PublishOptions,
29
+ QueryOptions,
30
+ SerializedIdentity,
31
+ SerializedKeyPair,
32
+ StateDefinitions,
33
+ StateListener,
34
+ StateSliceDefinition,
35
+ StoredSignedContract,
36
+ SyncOptions,
37
+ } from './types.js';
38
+ export type {
39
+ ConnectionStatus,
40
+ WebSocketTransportCallbacks,
41
+ WebSocketTransportIdentity,
42
+ } from './websocket-transport.js';
43
+ export { deriveWsUrl, WebSocketTransport } from './websocket-transport.js';
@@ -0,0 +1,103 @@
1
+ import type { SerializedIdentity } from './types.js';
2
+
3
+ export interface PersistOptions {
4
+ /** localStorage key prefix. Identity stored at `${key}:identity`, state at `${key}:state` */
5
+ key: string;
6
+ }
7
+
8
+ function getStorage(): Storage | null {
9
+ try {
10
+ return typeof localStorage !== 'undefined' ? localStorage : null;
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ export function loadPersistedIdentity(opts: PersistOptions): SerializedIdentity | null {
17
+ const storage = getStorage();
18
+
19
+ if (!storage) {
20
+ return null;
21
+ }
22
+
23
+ try {
24
+ const raw = storage.getItem(`${opts.key}:identity`);
25
+
26
+ if (!raw) {
27
+ return null;
28
+ }
29
+
30
+ return JSON.parse(raw) as SerializedIdentity;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ export function persistIdentity(opts: PersistOptions, identity: SerializedIdentity | null): void {
37
+ const storage = getStorage();
38
+
39
+ if (!storage) {
40
+ return;
41
+ }
42
+
43
+ try {
44
+ if (identity) {
45
+ storage.setItem(`${opts.key}:identity`, JSON.stringify(identity));
46
+ } else {
47
+ storage.removeItem(`${opts.key}:identity`);
48
+ }
49
+ } catch {
50
+ // Silently fail (quota exceeded, etc.)
51
+ }
52
+ }
53
+
54
+ export function loadPersistedState<T extends Record<string, unknown>>(
55
+ opts: PersistOptions,
56
+ ): T | null {
57
+ const storage = getStorage();
58
+
59
+ if (!storage) {
60
+ return null;
61
+ }
62
+
63
+ try {
64
+ const raw = storage.getItem(`${opts.key}:state`);
65
+
66
+ if (!raw) {
67
+ return null;
68
+ }
69
+
70
+ return JSON.parse(raw) as T;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ export function persistState(opts: PersistOptions, state: Record<string, unknown>): void {
77
+ const storage = getStorage();
78
+
79
+ if (!storage) {
80
+ return;
81
+ }
82
+
83
+ try {
84
+ storage.setItem(`${opts.key}:state`, JSON.stringify(state));
85
+ } catch {
86
+ // Silently fail
87
+ }
88
+ }
89
+
90
+ export function clearPersisted(opts: PersistOptions): void {
91
+ const storage = getStorage();
92
+
93
+ if (!storage) {
94
+ return;
95
+ }
96
+
97
+ try {
98
+ storage.removeItem(`${opts.key}:identity`);
99
+ storage.removeItem(`${opts.key}:state`);
100
+ } catch {
101
+ // Silently fail
102
+ }
103
+ }