@novasamatech/host-chat 0.8.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,103 @@
1
+ import { Enum, Hex, Nullable, Status } from '@novasamatech/scale';
2
+ import { Bytes, Option, Struct, Vector, _void, compact, str, u64 } from 'scale-ts';
3
+ import { FileVariant } from './attachment.js';
4
+ const AccountIdCodec = Bytes(32);
5
+ const PublicKeyCodec = Bytes(65);
6
+ export const TextContent = str;
7
+ export const RichTextContent = Struct({
8
+ text: Option(TextContent),
9
+ attachments: Option(Vector(FileVariant)),
10
+ });
11
+ export const Platform = Status('Android', 'iOS');
12
+ export const TokenContent = Struct({
13
+ token: Hex(),
14
+ platform: Platform,
15
+ });
16
+ export const SendContent = Struct({
17
+ amount: compact,
18
+ assetId: Nullable(str), // 0x scale encoded assetId or number, null for native
19
+ blockHash: Hex(32), // fixed 32 byte array
20
+ extrinsicHash: Hex(32), // fixed 32 byte array
21
+ });
22
+ export const ReactionContent = Struct({
23
+ messageId: str, // encoded as string
24
+ emoji: str,
25
+ });
26
+ export const ReplyContent = Struct({
27
+ messageId: str,
28
+ ownContent: RichTextContent,
29
+ });
30
+ export const EditContent = Struct({
31
+ messageId: str,
32
+ newContent: RichTextContent,
33
+ });
34
+ // V2 multi-device roster mutations carried as chat-content variants.
35
+ // Android's `ChatMessageStatementContent.DeviceAdded`/`DeviceRemoved` use
36
+ // the `AccountId` / `EncodedPublicKey` wrapper types without `@FixedLength`,
37
+ // so substrate-sdk-android falls through to length-prefixed `Vec<u8>` on the
38
+ // wire. We accept that shape here (`Bytes()` = compact-length-prefixed bytes)
39
+ // instead of fixed-size to stay tolerant of Android's emitter. Other variants
40
+ // that use `@FixedLength` on raw `ByteArray` (e.g. `DeviceInfoContent` below)
41
+ // stay fixed-size and continue to use AccountIdCodec/PublicKeyCodec.
42
+ export const DeviceAddedContent = Struct({
43
+ statementAccountId: Bytes(),
44
+ encryptionPublicKey: Bytes(),
45
+ });
46
+ export const DeviceRemovedContent = Struct({
47
+ statementAccountId: Bytes(),
48
+ });
49
+ // Legacy single-device accept (index 14, iOS V1). Wire: bare `String`.
50
+ // Superseded by `deviceChatAccepted` (index 20); keep for backward decode.
51
+ export const ChatAcceptedContent = Struct({
52
+ messageId: str,
53
+ });
54
+ // Per-device descriptor for the multi-device accept. Matches iOS `Chat.PeerDevice`
55
+ // and Android `DeviceInfoScale`.
56
+ export const DeviceInfoContent = Struct({
57
+ statementAccountId: AccountIdCodec,
58
+ encryptionPublicKey: PublicKeyCodec,
59
+ });
60
+ // Multi-device accept (index 20, per chat spec v0.1
61
+ // https://hackmd.io/@1JCaGppGSUqHtJilikYaKw/Ski9naYdWe):
62
+ // DeviceChatAccepted = { requestId: String, device: DeviceInfo }
63
+ // Sent via identity-level session SessionId(B, A) encrypted with K(A,B);
64
+ // identity-level encryption lets all of A's devices decrypt without per-device
65
+ // envelope. Index 19 is reserved (placeholder slot between deviceRemoved=18
66
+ // and deviceChatAccepted=20).
67
+ export const DeviceChatAcceptedContent = Struct({
68
+ requestId: str,
69
+ device: DeviceInfoContent,
70
+ });
71
+ // Note: enum indices MUST match iOS/Android SCALE codecs.
72
+ // Indices are auto-assigned sequentially, so order matters.
73
+ export const MessageContent = Enum({
74
+ text: TextContent, // 0
75
+ token: TokenContent, // 1
76
+ send: SendContent, // 2
77
+ contactAdded: _void, // 3
78
+ reacted: ReactionContent, // 4
79
+ reactionRemoved: ReactionContent, // 5
80
+ _reserved6: _void, // 6 — reserved (unused)
81
+ reply: ReplyContent, // 7
82
+ dataChannelOffer: _void, // 8
83
+ dataChannelAnswer: _void, // 9
84
+ dataChannelCandidates: _void, // 10
85
+ _reserved11: _void, // 11 — reserved (unused)
86
+ edit: EditContent, // 12
87
+ leftChat: _void, // 13
88
+ chatAccepted: ChatAcceptedContent, // 14 (legacy single-device accept, iOS V1)
89
+ richText: RichTextContent, // 15
90
+ _reserved16: _void, // 16 — reserved (android `coinagePayment`, unused on desktop)
91
+ deviceAdded: DeviceAddedContent, // 17
92
+ deviceRemoved: DeviceRemovedContent, // 18
93
+ _reserved19: _void, // 19 — reserved (placeholder so deviceChatAccepted lands on the spec'd index 20)
94
+ deviceChatAccepted: DeviceChatAcceptedContent, // 20 (multi-device accept, spec v0.1)
95
+ });
96
+ export const VersionedMessageContent = Enum({
97
+ v1: MessageContent,
98
+ });
99
+ export const ChatMessage = Struct({
100
+ messageId: str,
101
+ timestamp: u64,
102
+ versioned: VersionedMessageContent,
103
+ });
@@ -0,0 +1 @@
1
+ export declare function toError(err: unknown): Error;
@@ -0,0 +1,9 @@
1
+ export function toError(err) {
2
+ if (err instanceof Error) {
3
+ return err;
4
+ }
5
+ if (err) {
6
+ return new Error(err.toString());
7
+ }
8
+ return new Error('Unknown error occurred.');
9
+ }
@@ -0,0 +1 @@
1
+ export { createAccountService } from './accountService.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createAccountService } from './accountService.js';
@@ -0,0 +1,9 @@
1
+ import { ResultAsync } from 'neverthrow';
2
+ import type { CodecType } from 'scale-ts';
3
+ import type { LocalMessage } from './codec/localMessage.js';
4
+ export type MessagesRepository = {
5
+ save(message: CodecType<typeof LocalMessage>): ResultAsync<void, Error>;
6
+ update(message: CodecType<typeof LocalMessage>): ResultAsync<void, Error>;
7
+ read(): ResultAsync<CodecType<typeof LocalMessage>[], Error>;
8
+ clear(): ResultAsync<void, Error>;
9
+ };
@@ -0,0 +1 @@
1
+ import { ResultAsync } from 'neverthrow';
@@ -0,0 +1,25 @@
1
+ import type { CodecType } from 'scale-ts';
2
+ import type { LocalMessage } from './codec/localMessage.js';
3
+ import type { MessageContent } from './codec/message.js';
4
+ export type ChatSession = {
5
+ /**
6
+ * Send a message to the peer
7
+ * @param content Message content
8
+ * @returns Message ID
9
+ */
10
+ send(content: CodecType<typeof MessageContent>): Promise<{
11
+ messageId: string;
12
+ }>;
13
+ markAsRead: (messages: string[]) => void;
14
+ onMessage(callback: (message: CodecType<typeof LocalMessage>) => void): void;
15
+ /**
16
+ * Leave the chat session.
17
+ * This will send a leave message to the peer, remove the session from the storage, and unsubscribe from the messages.
18
+ */
19
+ leave(): Promise<void>;
20
+ /**
21
+ * Dispose session
22
+ */
23
+ close: () => void;
24
+ };
25
+ export declare function createChatSession(): ChatSession;
@@ -0,0 +1,99 @@
1
+ import { nanoid } from 'nanoid';
2
+ // type Params = {
3
+ // localAccount: LocalSessionAccount;
4
+ // remoteAccount: RemoteSessionAccount;
5
+ // statementStore: StatementStoreAdapter;
6
+ // messageRepository: MessagesRepository;
7
+ // secret: Uint8Array;
8
+ // };
9
+ export function createChatSession() {
10
+ // const prover = createSr25519Prover(secret);
11
+ // const encryption = createEncryption(remoteAccount.publicKey);
12
+ // const session = createSession({ localAccount, remoteAccount, prover, encryption, statementStore });
13
+ //
14
+ // const unsubscribe = session.subscribe(ChatMessageCodec, async messages => {
15
+ // messages.forEach(message => {
16
+ // if (message.type !== 'request') return;
17
+ // const localMessage: CodecType<typeof LocalMessage> = {
18
+ // remote:
19
+ // message.payload.status === 'parsed'
20
+ // ? enumValue('message', message.payload.value)
21
+ // : enumValue('unsupported', message.payload.value),
22
+ // peerId: remoteAccount.accountId,
23
+ // status: enumValue('incoming', 'new'),
24
+ // order: BigInt(Date.now()),
25
+ // };
26
+ //
27
+ // onMessage(localMessage);
28
+ // });
29
+ // });
30
+ //
31
+ // const generateMessageId = () => nanoid(12);
32
+ //
33
+ // const sendMessage = async (content: CodecType<typeof MessageContent>): Promise<string> => {
34
+ // const messageId = generateMessageId();
35
+ // const timestamp = BigInt(Date.now());
36
+ //
37
+ // const chatMessage: CodecType<typeof ChatMessage> = {
38
+ // messageId,
39
+ // timestamp,
40
+ // versioned: {
41
+ // tag: 'v1',
42
+ // value: content,
43
+ // },
44
+ // };
45
+ //
46
+ // await session.request(ChatMessageCodec, chatMessage);
47
+ //
48
+ // // Notify status update callback if provided
49
+ // if (onStatusUpdate) {
50
+ // await onStatusUpdate(messageId, 'sent');
51
+ // }
52
+ //
53
+ // return messageId;
54
+ // };
55
+ //
56
+ // // Update message status
57
+ // const updateMessageStatus = async (
58
+ // messageId: string,
59
+ // status: CodecType<typeof OutgoingStatus | typeof IncomingStatus>,
60
+ // ): Promise<void> => {
61
+ // if (onStatusUpdate) {
62
+ // await onStatusUpdate(messageId, status);
63
+ // }
64
+ // };
65
+ //
66
+ // // Get peer ID
67
+ // const getPeerId = (): Uint8Array => {
68
+ // return remoteAccount.accountId;
69
+ // };
70
+ //
71
+ // // Close session
72
+ // const close = (): void => {
73
+ // unsubscribe();
74
+ // };
75
+ //
76
+ // // Auto-initialize if callback provided
77
+ // if (onMessage) {
78
+ // initializeSubscription();
79
+ // }
80
+ return {
81
+ send() {
82
+ return Promise.resolve({ messageId: nanoid(12) });
83
+ },
84
+ markAsRead() {
85
+ /* empty */
86
+ },
87
+ onMessage() {
88
+ return () => {
89
+ /* empty */
90
+ };
91
+ },
92
+ leave() {
93
+ return Promise.resolve();
94
+ },
95
+ close() {
96
+ /* empty */
97
+ },
98
+ };
99
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@novasamatech/host-chat",
3
+ "type": "module",
4
+ "version": "0.8.0-1",
5
+ "description": "Host statement store chat integration",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/paritytech/triangle-js-sdks.git"
10
+ },
11
+ "keywords": [
12
+ "polkadot"
13
+ ],
14
+ "scripts": {
15
+ "papi:update": "papi update"
16
+ },
17
+ "main": "dist/index.js",
18
+ "exports": {
19
+ "./package.json": "./package.json",
20
+ ".": {
21
+ "#/source": "./src/index.ts",
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "./codec/message": {
26
+ "types": "./dist/codec/message.d.ts",
27
+ "default": "./dist/codec/message.js"
28
+ },
29
+ "./codec/attachment": {
30
+ "types": "./dist/codec/attachment.d.ts",
31
+ "default": "./dist/codec/attachment.js"
32
+ },
33
+ "./session": {
34
+ "types": "./dist/session.d.ts",
35
+ "default": "./dist/session.js"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "README.md"
41
+ ],
42
+ "dependencies": {
43
+ "@novasamatech/scale": "0.8.0-1",
44
+ "@novasamatech/statement-store": "0.8.0-1",
45
+ "@novasamatech/storage-adapter": "0.8.0-1",
46
+ "nanoid": "5.1.9",
47
+ "neverthrow": "^8.2.0"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }