@parity/product-sdk-statement-store 0.1.0

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,583 @@
1
+ import { TopicFilter as TopicFilter$1, Statement } from '@novasamatech/sdk-statement';
2
+ export { Proof, TopicFilter as SdkTopicFilter, SignedStatement, Statement, SubmitResult, UnsignedStatement } from '@novasamatech/sdk-statement';
3
+
4
+ /** Maximum size of a single statement's data payload in bytes. */
5
+ declare const MAX_STATEMENT_SIZE = 512;
6
+ /** Maximum total storage per user in bytes. */
7
+ declare const MAX_USER_TOTAL = 1024;
8
+ /** Default time-to-live for published statements in seconds. */
9
+ declare const DEFAULT_TTL_SECONDS = 30;
10
+ /**
11
+ * A 32-byte blake2b-256 hash used as a statement topic.
12
+ *
13
+ * Topics allow subscribers to filter statements efficiently on the network.
14
+ * Create one with {@link createTopic}.
15
+ */
16
+ type TopicHash = Uint8Array & {
17
+ readonly __brand: "TopicHash";
18
+ };
19
+ /**
20
+ * A 32-byte blake2b-256 hash used as a statement channel identifier.
21
+ *
22
+ * Channels enable last-write-wins semantics: for a given channel,
23
+ * only the most recent statement (by timestamp) is kept.
24
+ * Create one with {@link createChannel}.
25
+ */
26
+ type ChannelHash = Uint8Array & {
27
+ readonly __brand: "ChannelHash";
28
+ };
29
+ /**
30
+ * Filter for statement subscriptions and queries.
31
+ *
32
+ * - `"any"` — matches all statements (no filtering).
33
+ * - `{ matchAll: [...] }` — matches statements that have **all** listed topics.
34
+ * - `{ matchAny: [...] }` — matches statements that have **any** listed topic.
35
+ */
36
+ type TopicFilter = "any" | {
37
+ matchAll: TopicHash[];
38
+ } | {
39
+ matchAny: TopicHash[];
40
+ };
41
+ /**
42
+ * A function that signs a message with an Sr25519 key.
43
+ *
44
+ * Takes the signature material bytes and returns a 64-byte Sr25519 signature.
45
+ * May be synchronous or asynchronous (e.g., when signing via a hardware wallet).
46
+ */
47
+ type StatementSigner = (message: Uint8Array) => Uint8Array | Promise<Uint8Array>;
48
+ /**
49
+ * An Sr25519 signer with its associated public key.
50
+ *
51
+ * Used by {@link StatementStoreClient.connect} to sign published statements
52
+ * when running outside a container (local mode).
53
+ */
54
+ interface StatementSignerWithKey {
55
+ /** 32-byte Sr25519 public key. */
56
+ publicKey: Uint8Array;
57
+ /** Signing function. */
58
+ sign: StatementSigner;
59
+ }
60
+ /**
61
+ * Credentials for connecting to the statement store.
62
+ *
63
+ * - **Host mode**: Inside a container, proof creation is delegated to the host API.
64
+ * The `accountId` is a `[ss58Address, chainPrefix]` tuple from product-sdk.
65
+ * - **Local mode**: Outside a container, statements are signed locally using the
66
+ * provided Sr25519 signer.
67
+ */
68
+ type ConnectionCredentials = {
69
+ mode: "host";
70
+ accountId: [string, number];
71
+ } | {
72
+ mode: "local";
73
+ signer: StatementSignerWithKey;
74
+ };
75
+ /**
76
+ * Configuration for {@link StatementStoreClient}.
77
+ *
78
+ * The client uses the Host API's native statement store protocol.
79
+ * The SDK is designed to run exclusively inside a host container.
80
+ */
81
+ interface StatementStoreConfig {
82
+ /**
83
+ * Application namespace used as the primary topic (topic1).
84
+ *
85
+ * All statements published by this client are tagged with `blake2b(appName)`.
86
+ * Subscribers filter on this topic to receive only relevant statements.
87
+ *
88
+ * @example "ss-webrtc", "mark3t-presence", "my-app"
89
+ */
90
+ appName: string;
91
+ /**
92
+ * Default time-to-live for published statements in seconds.
93
+ *
94
+ * Statements automatically expire after this duration.
95
+ * Can be overridden per-publish via {@link PublishOptions.ttlSeconds}.
96
+ *
97
+ * @default 30
98
+ */
99
+ defaultTtlSeconds?: number;
100
+ /**
101
+ * Provide a custom transport instead of auto-detection.
102
+ *
103
+ * When set, the client uses this transport directly.
104
+ * Useful for testing or advanced BYOD setups.
105
+ */
106
+ transport?: StatementTransport;
107
+ }
108
+ /**
109
+ * Options for publishing a single statement.
110
+ */
111
+ interface PublishOptions {
112
+ /**
113
+ * Channel name for last-write-wins semantics.
114
+ *
115
+ * When provided, the statement is tagged with `blake2b(channel)`.
116
+ * For a given channel, only the most recent statement is kept.
117
+ *
118
+ * @example "presence/peer-abc123", "handshake/alice-bob"
119
+ */
120
+ channel?: string;
121
+ /**
122
+ * Secondary topic for additional filtering.
123
+ *
124
+ * Hashed with blake2b and set as topic2. Useful for scoping
125
+ * statements to a specific room, document, or context.
126
+ *
127
+ * @example "doc-abc123", "room-456"
128
+ */
129
+ topic2?: string;
130
+ /**
131
+ * Time-to-live in seconds. Overrides {@link StatementStoreConfig.defaultTtlSeconds}.
132
+ */
133
+ ttlSeconds?: number;
134
+ /**
135
+ * Decryption key hint (32 bytes).
136
+ *
137
+ * Used by the `statement_posted` RPC method to filter statements.
138
+ * Typically set to the blake2b hash of the room or document ID.
139
+ */
140
+ decryptionKey?: Uint8Array;
141
+ }
142
+ /**
143
+ * A received statement with typed data and metadata.
144
+ *
145
+ * @typeParam T - The parsed data type (decoded from JSON).
146
+ */
147
+ interface ReceivedStatement<T = unknown> {
148
+ /** Parsed data payload. */
149
+ data: T;
150
+ /** Signer's public key hex (from proof), if present. */
151
+ signerHex?: string;
152
+ /** Channel hex, if present. */
153
+ channelHex?: string;
154
+ /** Topics array (hex strings). */
155
+ topics: string[];
156
+ /** Combined expiry value (upper 32 bits = timestamp, lower 32 bits = sequence). */
157
+ expiry?: bigint;
158
+ /** The full statement from the transport for advanced inspection. */
159
+ raw: Statement;
160
+ }
161
+ /** Handle returned by subscription methods. Call `unsubscribe()` to stop receiving events. */
162
+ interface Unsubscribable {
163
+ unsubscribe: () => void;
164
+ }
165
+ /**
166
+ * Low-level transport interface for statement store communication.
167
+ *
168
+ * Uses **HostTransport** — the Host API's native binary protocol.
169
+ *
170
+ * Most consumers should use {@link StatementStoreClient} instead of this interface directly.
171
+ */
172
+ interface StatementTransport {
173
+ /**
174
+ * Subscribe to statements matching a topic filter.
175
+ *
176
+ * @param filter - sdk-statement topic filter.
177
+ * @param onStatements - Called with batches of received statements.
178
+ * @param onError - Called when the subscription encounters an error.
179
+ * @returns A handle to unsubscribe.
180
+ */
181
+ subscribe(filter: TopicFilter$1, onStatements: (statements: Statement[]) => void, onError: (error: Error) => void): Unsubscribable;
182
+ /**
183
+ * Sign and submit a statement.
184
+ *
185
+ * - **Host mode**: delegates to the host API's `createProof` then `submit`.
186
+ * - **Local mode**: signs locally using `getStatementSigner` from sdk-statement, then submits.
187
+ *
188
+ * @param statement - The unsigned statement to sign and submit.
189
+ * @param credentials - Signing credentials (host accountId or local signer).
190
+ */
191
+ signAndSubmit(statement: Statement, credentials: ConnectionCredentials): Promise<void>;
192
+ /**
193
+ * Query existing statements from the store.
194
+ *
195
+ * Note: The host API subscription replays initial state, so this may not be needed.
196
+ */
197
+ query?(filter: TopicFilter$1): Promise<Statement[]>;
198
+ /** Destroy the transport and release all resources. */
199
+ destroy(): void;
200
+ }
201
+
202
+ /**
203
+ * High-level client for the Polkadot Statement Store.
204
+ *
205
+ * Provides a simple publish/subscribe API over the ephemeral statement store,
206
+ * handling topic management and signing through the host API.
207
+ *
208
+ * The SDK is designed to run exclusively inside a host container.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * import { StatementStoreClient } from "@parity/product-sdk-statement-store";
213
+ *
214
+ * // Inside a container (host mode)
215
+ * const client = new StatementStoreClient({ appName: "my-app" });
216
+ * await client.connect({ mode: "host", accountId: ["5Grw...", 42] });
217
+ *
218
+ * // Publish
219
+ * await client.publish({ type: "presence", peerId: "abc" }, {
220
+ * channel: "presence/abc",
221
+ * topic2: "room-123",
222
+ * });
223
+ *
224
+ * // Subscribe
225
+ * client.subscribe<{ type: string }>(statement => {
226
+ * console.log(statement.data.type);
227
+ * });
228
+ *
229
+ * // Cleanup
230
+ * client.destroy();
231
+ * ```
232
+ */
233
+ declare class StatementStoreClient {
234
+ private readonly config;
235
+ private transport;
236
+ private credentials;
237
+ private subscription;
238
+ private callbacks;
239
+ private connected;
240
+ private connectPromise;
241
+ /** Set by destroy() so doConnect() can abort cleanly if destroy races with an in-flight connect. */
242
+ private destroyed;
243
+ /**
244
+ * Track seen statements by channel hex to avoid re-delivering the same statement.
245
+ * Maps channel hex (or data hash) to the expiry value.
246
+ */
247
+ private seen;
248
+ /** Monotonic counter to ensure unique sequence numbers even within the same millisecond. */
249
+ private sequenceCounter;
250
+ /** Cached hex topic string for the app name, used as the primary subscription topic. */
251
+ private readonly appTopicHex;
252
+ constructor(config: StatementStoreConfig);
253
+ /**
254
+ * Connect to the statement store and start receiving statements.
255
+ *
256
+ * @param credentials - Connection credentials (host accountId or local signer).
257
+ * @throws {StatementConnectionError} If the transport cannot be established.
258
+ */
259
+ connect(credentials: ConnectionCredentials): Promise<void>;
260
+ /** @deprecated Use `connect({ mode: "local", signer })` instead. */
261
+ connect(signer: StatementSignerWithKey): Promise<void>;
262
+ private doConnect;
263
+ /**
264
+ * Publish typed data to the statement store.
265
+ *
266
+ * @typeParam T - The type of data being published.
267
+ * @param data - The value to publish (must be JSON-serializable, max 512 bytes).
268
+ * @param options - Optional channel, topic2, TTL, and decryption key overrides.
269
+ * @returns `true` if accepted, `false` if rejected or errored.
270
+ * @throws {StatementConnectionError} If not connected.
271
+ * @throws {StatementDataTooLargeError} If the encoded data exceeds 512 bytes.
272
+ */
273
+ publish<T>(data: T, options?: PublishOptions): Promise<boolean>;
274
+ /**
275
+ * Subscribe to incoming statements on this application's topic.
276
+ *
277
+ * @typeParam T - The expected data type (decoded from JSON).
278
+ * @param callback - Called for each new statement.
279
+ * @param options - Optional secondary topic filter.
280
+ * @returns A handle to unsubscribe.
281
+ */
282
+ subscribe<T>(callback: (statement: ReceivedStatement<T>) => void, options?: {
283
+ topic2?: string;
284
+ }): Unsubscribable;
285
+ /** Whether the client is connected and ready to publish/subscribe. */
286
+ isConnected(): boolean;
287
+ /**
288
+ * Get the signer's public key as a hex string (with 0x prefix).
289
+ *
290
+ * @returns The hex-encoded public key, or empty string if not connected or in host mode.
291
+ */
292
+ getPublicKeyHex(): string;
293
+ /**
294
+ * Destroy the client, unsubscribing and closing the transport.
295
+ *
296
+ * Safe to call multiple times. After destruction, the client cannot be reused.
297
+ */
298
+ destroy(): void;
299
+ private startSubscription;
300
+ /** Remove entries from the seen map whose expiry timestamp is in the past. */
301
+ private pruneSeenMap;
302
+ /**
303
+ * Process a received statement, dedup, parse, and deliver to callbacks.
304
+ * Returns true if the statement was new and delivered.
305
+ */
306
+ private handleStatementReceived;
307
+ private parseStatement;
308
+ /** Build an SdkTopicFilter for the app's primary topic. */
309
+ private buildFilter;
310
+ }
311
+
312
+ /**
313
+ * Higher-level abstraction providing last-write-wins channel semantics
314
+ * over the statement store.
315
+ *
316
+ * Each channel name maps to a single value. When a new value is written
317
+ * to a channel, it replaces the previous one if its timestamp is newer.
318
+ * This is ideal for presence announcements, signaling, and ephemeral state.
319
+ *
320
+ * @typeParam T - The type of values stored in channels.
321
+ * Values should include a `timestamp` field for ordering;
322
+ * if omitted, the current time is used automatically.
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * interface Presence {
327
+ * type: "presence";
328
+ * peerId: string;
329
+ * timestamp: number;
330
+ * }
331
+ *
332
+ * const channels = new ChannelStore<Presence>(client, { topic2: "doc-123" });
333
+ *
334
+ * // Write presence
335
+ * await channels.write("presence/peer-abc", {
336
+ * type: "presence",
337
+ * peerId: "abc",
338
+ * timestamp: Date.now(),
339
+ * });
340
+ *
341
+ * // Read all presences
342
+ * for (const [name, value] of channels.readAll()) {
343
+ * console.log(`${name}: ${value.peerId}`);
344
+ * }
345
+ *
346
+ * // React to changes
347
+ * channels.onChange((name, value, previous) => {
348
+ * console.log(`Channel ${name} updated`);
349
+ * });
350
+ *
351
+ * channels.destroy();
352
+ * ```
353
+ */
354
+ declare class ChannelStore<T extends {
355
+ timestamp?: number;
356
+ }> {
357
+ private readonly client;
358
+ private readonly topic2;
359
+ private readonly values;
360
+ /** Maps human-readable channel names to their hex hash keys, for consistent lookup. */
361
+ private readonly nameToHash;
362
+ private readonly changeCallbacks;
363
+ private subscription;
364
+ /**
365
+ * @param client - The connected {@link StatementStoreClient} to use.
366
+ * @param options - Optional secondary topic for scoping channels.
367
+ */
368
+ constructor(client: StatementStoreClient, options?: {
369
+ topic2?: string;
370
+ });
371
+ /**
372
+ * Write a value to a named channel.
373
+ *
374
+ * If the value doesn't include a `timestamp`, one is added automatically
375
+ * using `Date.now()`. The value is published to the statement store
376
+ * with the channel name as the statement channel.
377
+ *
378
+ * @param channelName - The channel name (e.g., "presence/peer-abc").
379
+ * @param value - The value to write.
380
+ * @returns `true` if the statement was accepted by the network.
381
+ */
382
+ write(channelName: string, value: T): Promise<boolean>;
383
+ /**
384
+ * Read the latest value for a channel by its human-readable name.
385
+ *
386
+ * Looks up the channel hash from the name, then retrieves the value.
387
+ * Also checks the hash directly for values received from the network
388
+ * before any local write established the name mapping.
389
+ *
390
+ * @param channelName - The channel name (e.g., "presence/peer-abc").
391
+ * @returns The latest value, or `undefined` if no value has been received.
392
+ */
393
+ read(channelName: string): T | undefined;
394
+ /**
395
+ * Read all channel values as a read-only map.
396
+ *
397
+ * Keys are hex-encoded channel hashes. Use {@link read} for
398
+ * lookup by human-readable name.
399
+ *
400
+ * @returns A map of channel hash to latest value.
401
+ */
402
+ readAll(): ReadonlyMap<string, T>;
403
+ /**
404
+ * Get the number of channels currently tracked.
405
+ */
406
+ get size(): number;
407
+ /**
408
+ * Subscribe to channel value changes.
409
+ *
410
+ * The callback is invoked whenever a channel value is updated
411
+ * (either from the network or from a local write).
412
+ *
413
+ * @param callback - Called with the channel key (hex hash for network-received, hex hash for local writes), new value, and previous value.
414
+ * @returns A handle to unsubscribe.
415
+ */
416
+ onChange(callback: (channelName: string, value: T, previous: T | undefined) => void): Unsubscribable;
417
+ /**
418
+ * Destroy the channel store and clean up subscriptions.
419
+ *
420
+ * Does not destroy the underlying client.
421
+ */
422
+ destroy(): void;
423
+ private handleStatement;
424
+ private updateChannel;
425
+ }
426
+
427
+ /**
428
+ * Create a 32-byte topic hash from a human-readable string.
429
+ *
430
+ * Uses blake2b-256 to hash the UTF-8 encoded string into a deterministic
431
+ * 32-byte topic identifier. Statements are tagged with topics so subscribers
432
+ * can filter efficiently on the network.
433
+ *
434
+ * @param name - A human-readable topic name (e.g., "ss-webrtc", "my-app").
435
+ * @returns A branded 32-byte topic hash.
436
+ *
437
+ * @example
438
+ * ```ts
439
+ * const topic = createTopic("ss-webrtc");
440
+ * // Use in subscriptions and publish options
441
+ * ```
442
+ */
443
+ declare function createTopic(name: string): TopicHash;
444
+ /**
445
+ * Create a 32-byte channel hash from a human-readable channel name.
446
+ *
447
+ * Channels enable last-write-wins semantics: for a given channel,
448
+ * only the most recent statement (by timestamp) is retained.
449
+ *
450
+ * @param name - A human-readable channel name (e.g., "presence/peer-abc").
451
+ * @returns A branded 32-byte channel hash.
452
+ *
453
+ * @example
454
+ * ```ts
455
+ * const channel = createChannel("presence/peer-abc123");
456
+ * ```
457
+ */
458
+ declare function createChannel(name: string): ChannelHash;
459
+ /**
460
+ * Convert a topic or channel hash to a hex string (with 0x prefix).
461
+ *
462
+ * @param hash - A 32-byte topic or channel hash.
463
+ * @returns Hex string with "0x" prefix.
464
+ */
465
+ declare function topicToHex(hash: Uint8Array): string;
466
+ /**
467
+ * Compare two topic or channel hashes for byte equality.
468
+ *
469
+ * @param a - First hash.
470
+ * @param b - Second hash.
471
+ * @returns True if the hashes are identical.
472
+ */
473
+ declare function topicsEqual(a: Uint8Array, b: Uint8Array): boolean;
474
+ /**
475
+ * Serialize a {@link TopicFilter} into the JSON-RPC format expected by statement store nodes.
476
+ *
477
+ * - `"any"` is passed through as the string `"any"`.
478
+ * - `{ matchAll: [...] }` becomes `{ matchAll: ["0x...", ...] }` with hex-encoded topics.
479
+ * - `{ matchAny: [...] }` becomes `{ matchAny: ["0x...", ...] }` with hex-encoded topics.
480
+ *
481
+ * @param filter - The topic filter to serialize.
482
+ * @returns A JSON-RPC compatible filter value.
483
+ */
484
+ declare function serializeTopicFilter(filter: TopicFilter): TopicFilter$1;
485
+
486
+ /** Convert a hex string (with or without 0x prefix) to bytes. */
487
+ declare function fromHex(hex: string): Uint8Array;
488
+ /** Convert bytes to a hex string with 0x prefix. */
489
+ declare function toHex(bytes: Uint8Array): string;
490
+ /**
491
+ * Encode a value as a JSON-serialized data payload for a statement.
492
+ *
493
+ * Serializes the value as JSON and encodes to UTF-8 bytes.
494
+ * Throws {@link StatementDataTooLargeError} if the result exceeds
495
+ * {@link MAX_STATEMENT_SIZE} bytes.
496
+ *
497
+ * @typeParam T - The type of value being encoded.
498
+ * @param value - The value to serialize.
499
+ * @returns UTF-8 encoded JSON bytes.
500
+ * @throws {StatementDataTooLargeError} If the encoded data exceeds 512 bytes.
501
+ */
502
+ declare function encodeData<T>(value: T): Uint8Array;
503
+ /**
504
+ * Decode a JSON-serialized data payload from statement bytes.
505
+ *
506
+ * @typeParam T - The expected parsed type.
507
+ * @param bytes - UTF-8 encoded JSON bytes.
508
+ * @returns The parsed value.
509
+ * @throws {StatementEncodingError} If the bytes are not valid UTF-8 or valid JSON.
510
+ */
511
+ declare function decodeData<T>(bytes: Uint8Array): T;
512
+
513
+ /**
514
+ * Create a statement store transport.
515
+ *
516
+ * Uses the Host API via `@parity/product-sdk-host` — the container's native
517
+ * statement store protocol (binary, not JSON-RPC). This is the only supported path.
518
+ *
519
+ * @throws {StatementConnectionError} If the host statement store is unavailable.
520
+ */
521
+ declare function createTransport(): Promise<StatementTransport>;
522
+
523
+ /**
524
+ * Base class for all statement store errors.
525
+ *
526
+ * Use `instanceof StatementStoreError` to catch any error originating
527
+ * from the statement store package.
528
+ */
529
+ declare class StatementStoreError extends Error {
530
+ constructor(message: string, options?: ErrorOptions);
531
+ }
532
+ /**
533
+ * A SCALE encoding or decoding operation failed.
534
+ *
535
+ * Thrown when statement bytes cannot be parsed (corrupt data, unknown field tags)
536
+ * or when encoding produces invalid output.
537
+ */
538
+ declare class StatementEncodingError extends StatementStoreError {
539
+ constructor(message: string, options?: ErrorOptions);
540
+ }
541
+ /**
542
+ * The statement store node rejected a submitted statement.
543
+ *
544
+ * Carries the raw RPC response detail for programmatic inspection.
545
+ */
546
+ declare class StatementSubmitError extends StatementStoreError {
547
+ /** The raw response from the RPC call. */
548
+ readonly detail: unknown;
549
+ constructor(detail: unknown);
550
+ }
551
+ /**
552
+ * Failed to set up or maintain a statement subscription.
553
+ *
554
+ * This is a non-fatal error — the client falls back to polling
555
+ * when subscriptions are unavailable.
556
+ */
557
+ declare class StatementSubscriptionError extends StatementStoreError {
558
+ constructor(message: string, options?: ErrorOptions);
559
+ }
560
+ /**
561
+ * Failed to connect to the statement store transport.
562
+ *
563
+ * Thrown when the WebSocket connection cannot be established
564
+ * or the chain-client's bulletin chain is not connected.
565
+ */
566
+ declare class StatementConnectionError extends StatementStoreError {
567
+ constructor(message: string, options?: ErrorOptions);
568
+ }
569
+ /**
570
+ * The statement data payload exceeds the maximum allowed size.
571
+ *
572
+ * The statement store protocol limits individual statement data
573
+ * to {@link MAX_STATEMENT_SIZE} bytes (512 bytes).
574
+ */
575
+ declare class StatementDataTooLargeError extends StatementStoreError {
576
+ /** The actual size of the data in bytes. */
577
+ readonly actualSize: number;
578
+ /** The maximum allowed size in bytes. */
579
+ readonly maxSize: number;
580
+ constructor(actualSize: number, maxSize?: number);
581
+ }
582
+
583
+ export { type ChannelHash, ChannelStore, type ConnectionCredentials, DEFAULT_TTL_SECONDS, MAX_STATEMENT_SIZE, MAX_USER_TOTAL, type PublishOptions, type ReceivedStatement, StatementConnectionError, StatementDataTooLargeError, StatementEncodingError, type StatementSigner, type StatementSignerWithKey, StatementStoreClient, type StatementStoreConfig, StatementStoreError, StatementSubmitError, StatementSubscriptionError, type StatementTransport, type TopicFilter, type TopicHash, type Unsubscribable, createChannel, createTopic, createTransport, decodeData, encodeData, fromHex, serializeTopicFilter, toHex, topicToHex, topicsEqual };