@msssystems/mss-link-sdk 0.1.2 → 0.1.5

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,7 @@
1
+ export type LocalCryptoErrorCode = 'corrupt_session' | 'missing_local_session' | 'unsupported_envelope' | 'local_crypto_unavailable' | 'unknown';
2
+ export declare class LocalCryptoError extends Error {
3
+ readonly code: LocalCryptoErrorCode;
4
+ readonly cause?: unknown;
5
+ constructor(code: LocalCryptoErrorCode, message: string, cause?: unknown);
6
+ }
7
+ export declare function normalizeLocalCryptoError(error: unknown): LocalCryptoError;
@@ -0,0 +1,36 @@
1
+ export class LocalCryptoError extends Error {
2
+ code;
3
+ cause;
4
+ constructor(code, message, cause) {
5
+ super(message);
6
+ this.name = 'LocalCryptoError';
7
+ this.code = code;
8
+ this.cause = cause;
9
+ }
10
+ }
11
+ export function normalizeLocalCryptoError(error) {
12
+ if (error instanceof LocalCryptoError) {
13
+ return error;
14
+ }
15
+ const message = error instanceof Error ? error.message : String(error);
16
+ const normalizedMessage = message.toLowerCase();
17
+ if (normalizedMessage.includes('corrupt') ||
18
+ normalizedMessage.includes('ratchet')) {
19
+ return new LocalCryptoError('corrupt_session', message, error);
20
+ }
21
+ if (normalizedMessage.includes('missing') ||
22
+ normalizedMessage.includes('not found') ||
23
+ normalizedMessage.includes('session')) {
24
+ return new LocalCryptoError('missing_local_session', message, error);
25
+ }
26
+ if (normalizedMessage.includes('unsupported') ||
27
+ normalizedMessage.includes('envelope')) {
28
+ return new LocalCryptoError('unsupported_envelope', message, error);
29
+ }
30
+ if (normalizedMessage.includes('indexeddb') ||
31
+ normalizedMessage.includes('wasm') ||
32
+ normalizedMessage.includes('browser')) {
33
+ return new LocalCryptoError('local_crypto_unavailable', message, error);
34
+ }
35
+ return new LocalCryptoError('unknown', message, error);
36
+ }
@@ -1,5 +1,12 @@
1
1
  export * from './api-base-url';
2
2
  export * from './client-options';
3
+ export * from './decryption-errors';
4
+ export * from './local-crypto-health';
5
+ export * from './local-crypto-health.contracts';
6
+ export * from './media-message.contracts';
7
+ export * from './media-message-metadata';
3
8
  export * from './message.contracts';
9
+ export * from './message-decryption.contracts';
10
+ export * from './message-decryption-state';
4
11
  export * from './message.mapper';
5
12
  export * from './mss-link-browser-client';
@@ -1,5 +1,12 @@
1
1
  export * from './api-base-url';
2
2
  export * from './client-options';
3
+ export * from './decryption-errors';
4
+ export * from './local-crypto-health';
5
+ export * from './local-crypto-health.contracts';
6
+ export * from './media-message.contracts';
7
+ export * from './media-message-metadata';
3
8
  export * from './message.contracts';
9
+ export * from './message-decryption.contracts';
10
+ export * from './message-decryption-state';
4
11
  export * from './message.mapper';
5
12
  export * from './mss-link-browser-client';
@@ -0,0 +1,9 @@
1
+ import type { LocalCryptoErrorCode } from './decryption-errors';
2
+ export type LocalCryptoHealthStatus = 'ready' | 'recovery_required' | 'corrupt' | 'unavailable';
3
+ export type LocalCryptoHealthErrorCode = LocalCryptoErrorCode;
4
+ export interface LocalCryptoHealth {
5
+ ready: boolean;
6
+ status: LocalCryptoHealthStatus;
7
+ errorCode?: LocalCryptoHealthErrorCode;
8
+ checkedAt: string;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { LocalCryptoHealth } from './local-crypto-health.contracts';
2
+ export declare function checkLocalCryptoHealth(checkReady: () => Promise<void>): Promise<LocalCryptoHealth>;
@@ -0,0 +1,29 @@
1
+ import { normalizeLocalCryptoError } from './decryption-errors';
2
+ export async function checkLocalCryptoHealth(checkReady) {
3
+ try {
4
+ await checkReady();
5
+ return {
6
+ ready: true,
7
+ status: 'ready',
8
+ checkedAt: new Date().toISOString(),
9
+ };
10
+ }
11
+ catch (error) {
12
+ const normalizedError = normalizeLocalCryptoError(error);
13
+ return {
14
+ ready: false,
15
+ status: getLocalCryptoHealthStatus(normalizedError.code),
16
+ errorCode: normalizedError.code,
17
+ checkedAt: new Date().toISOString(),
18
+ };
19
+ }
20
+ }
21
+ function getLocalCryptoHealthStatus(errorCode) {
22
+ if (errorCode === 'corrupt_session') {
23
+ return 'corrupt';
24
+ }
25
+ if (errorCode === 'local_crypto_unavailable') {
26
+ return 'unavailable';
27
+ }
28
+ return 'recovery_required';
29
+ }
@@ -0,0 +1,4 @@
1
+ import type { MediaMessageAttachmentInput, MediaMessageMetadataPayload } from './media-message.contracts';
2
+ export declare function buildMediaMessageMetadataPayload(attachments: MediaMessageAttachmentInput[], caption?: string): MediaMessageMetadataPayload;
3
+ export declare function serializeMediaMessageMetadata(payload: MediaMessageMetadataPayload): string;
4
+ export declare function extractMediaAssetIds(attachments: MediaMessageAttachmentInput[]): string[];
@@ -0,0 +1,56 @@
1
+ export function buildMediaMessageMetadataPayload(attachments, caption) {
2
+ if (attachments.length === 0) {
3
+ throw new Error('Media message must contain at least one attachment.');
4
+ }
5
+ const payload = {
6
+ schema: 'mss.link.media-message.v1',
7
+ attachments: attachments.map(normalizeAttachment),
8
+ };
9
+ if (caption && caption.trim().length > 0) {
10
+ payload.caption = caption.trim();
11
+ }
12
+ return payload;
13
+ }
14
+ export function serializeMediaMessageMetadata(payload) {
15
+ return JSON.stringify({
16
+ schema: payload.schema,
17
+ attachments: payload.attachments.map((attachment) => ({
18
+ assetId: attachment.assetId,
19
+ mimeType: attachment.mimeType,
20
+ size: attachment.size,
21
+ ...(attachment.originalName
22
+ ? { originalName: attachment.originalName }
23
+ : {}),
24
+ ...(attachment.uploadedAt ? { uploadedAt: attachment.uploadedAt } : {}),
25
+ })),
26
+ ...(payload.caption ? { caption: payload.caption } : {}),
27
+ });
28
+ }
29
+ export function extractMediaAssetIds(attachments) {
30
+ return attachments.map((attachment) => attachment.assetId);
31
+ }
32
+ function normalizeAttachment(attachment) {
33
+ const assetId = attachment.assetId.trim();
34
+ const mimeType = attachment.mimeType.trim();
35
+ const size = String(attachment.size);
36
+ if (!assetId) {
37
+ throw new Error('Media attachment assetId is required.');
38
+ }
39
+ if (!mimeType) {
40
+ throw new Error('Media attachment mimeType is required.');
41
+ }
42
+ if (!size || size === 'NaN') {
43
+ throw new Error('Media attachment size is required.');
44
+ }
45
+ return {
46
+ assetId,
47
+ mimeType,
48
+ size,
49
+ ...(attachment.originalName?.trim()
50
+ ? { originalName: attachment.originalName.trim() }
51
+ : {}),
52
+ ...(attachment.uploadedAt?.trim()
53
+ ? { uploadedAt: attachment.uploadedAt.trim() }
54
+ : {}),
55
+ };
56
+ }
@@ -0,0 +1,25 @@
1
+ export interface MediaMessageAttachmentInput {
2
+ assetId: string;
3
+ mimeType: string;
4
+ size: string | number;
5
+ originalName?: string;
6
+ uploadedAt?: string;
7
+ }
8
+ export interface SendMediaMessageParams {
9
+ chatId: string;
10
+ targetUserId: string;
11
+ attachments: MediaMessageAttachmentInput[];
12
+ caption?: string;
13
+ }
14
+ export interface MediaMessageAttachmentMetadata {
15
+ assetId: string;
16
+ mimeType: string;
17
+ size: string;
18
+ originalName?: string;
19
+ uploadedAt?: string;
20
+ }
21
+ export interface MediaMessageMetadataPayload {
22
+ schema: 'mss.link.media-message.v1';
23
+ attachments: MediaMessageAttachmentMetadata[];
24
+ caption?: string;
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { CreateMessageDecryptionStateIndexInput, EncryptedMessageRef, MessageDecryptStateIndex } from './message-decryption.contracts';
2
+ export declare function createMessageDecryptionStateIndex<TMessage extends EncryptedMessageRef>({ messages, decryptedMessages, isDecrypting, decryptError, }: CreateMessageDecryptionStateIndexInput<TMessage>): MessageDecryptStateIndex;
@@ -0,0 +1,81 @@
1
+ import { normalizeLocalCryptoError } from './decryption-errors';
2
+ export function createMessageDecryptionStateIndex({ messages, decryptedMessages, isDecrypting, decryptError, }) {
3
+ const decryptedById = createDecryptedMessageIndex(decryptedMessages);
4
+ const index = new Map();
5
+ for (const message of messages) {
6
+ const decrypted = findDecryptedMessage(decryptedById, message);
7
+ const state = createMessageDecryptState({
8
+ message,
9
+ decrypted,
10
+ isDecrypting,
11
+ decryptError,
12
+ });
13
+ index.set(message.id, state);
14
+ if (message.clientMessageId) {
15
+ index.set(message.clientMessageId, state);
16
+ }
17
+ }
18
+ return index;
19
+ }
20
+ function createMessageDecryptState({ message, decrypted, isDecrypting, decryptError, }) {
21
+ const baseState = createBaseMessageDecryptState(message);
22
+ if (decrypted?.text) {
23
+ return {
24
+ ...baseState,
25
+ status: 'decrypted',
26
+ text: decrypted.text,
27
+ };
28
+ }
29
+ if (isDecrypting) {
30
+ return {
31
+ ...baseState,
32
+ status: 'pending',
33
+ reason: 'local_sync_in_progress',
34
+ };
35
+ }
36
+ if (decryptError) {
37
+ return {
38
+ ...baseState,
39
+ status: 'failed',
40
+ reason: 'local_crypto_error',
41
+ errorCode: getDecryptErrorCode(decryptError),
42
+ };
43
+ }
44
+ return {
45
+ ...baseState,
46
+ status: 'recovery_required',
47
+ reason: 'local_message_missing',
48
+ errorCode: 'missing_local_session',
49
+ };
50
+ }
51
+ function createBaseMessageDecryptState(message) {
52
+ if (message.clientMessageId) {
53
+ return {
54
+ messageId: message.id,
55
+ clientMessageId: message.clientMessageId,
56
+ };
57
+ }
58
+ return {
59
+ messageId: message.id,
60
+ };
61
+ }
62
+ function createDecryptedMessageIndex(messages) {
63
+ const index = new Map();
64
+ for (const message of messages) {
65
+ index.set(message.id, message);
66
+ }
67
+ return index;
68
+ }
69
+ function findDecryptedMessage(index, message) {
70
+ const byMessageId = index.get(message.id);
71
+ if (byMessageId) {
72
+ return byMessageId;
73
+ }
74
+ if (message.clientMessageId) {
75
+ return index.get(message.clientMessageId);
76
+ }
77
+ return undefined;
78
+ }
79
+ function getDecryptErrorCode(error) {
80
+ return normalizeLocalCryptoError(error).code;
81
+ }
@@ -0,0 +1,24 @@
1
+ import type { LocalCryptoErrorCode } from './decryption-errors';
2
+ import type { DecryptedMessage } from './message.contracts';
3
+ export type MessageDecryptStatus = 'decrypted' | 'pending' | 'failed' | 'recovery_required';
4
+ export type MessageDecryptReason = 'local_sync_in_progress' | 'local_message_missing' | 'local_crypto_error';
5
+ export type MessageDecryptErrorCode = LocalCryptoErrorCode;
6
+ export interface EncryptedMessageRef {
7
+ id: string;
8
+ clientMessageId?: string | null;
9
+ }
10
+ export interface MessageDecryptState {
11
+ messageId: string;
12
+ clientMessageId?: string | null;
13
+ status: MessageDecryptStatus;
14
+ reason?: MessageDecryptReason;
15
+ errorCode?: MessageDecryptErrorCode;
16
+ text?: string;
17
+ }
18
+ export type MessageDecryptStateIndex = Map<string, MessageDecryptState>;
19
+ export interface CreateMessageDecryptionStateIndexInput<TMessage extends EncryptedMessageRef = EncryptedMessageRef> {
20
+ messages: TMessage[];
21
+ decryptedMessages: DecryptedMessage[];
22
+ isDecrypting: boolean;
23
+ decryptError: unknown;
24
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,6 @@
1
1
  import type { MssLinkBrowserClientOptions } from './client-options';
2
+ import type { LocalCryptoHealth } from './local-crypto-health.contracts';
3
+ import type { SendMediaMessageParams } from './media-message.contracts';
2
4
  import type { DecryptedMessage } from './message.contracts';
3
5
  export declare class MssLinkBrowserClient {
4
6
  private readonly userId;
@@ -16,12 +18,14 @@ export declare class MssLinkBrowserClient {
16
18
  constructor(options: MssLinkBrowserClientOptions);
17
19
  setAccessToken(accessToken: string | null): void;
18
20
  ensureReady(): Promise<void>;
21
+ getLocalCryptoHealth(): Promise<LocalCryptoHealth>;
19
22
  sendMessage(params: {
20
23
  chatId: string;
21
24
  targetUserId: string;
22
25
  text: string;
23
26
  kind?: string;
24
27
  }): Promise<string>;
28
+ sendMediaMessage(params: SendMediaMessageParams): Promise<string>;
25
29
  syncMessages(): Promise<void>;
26
30
  getLocalMessages(chatId: string): Promise<DecryptedMessage[]>;
27
31
  reset(): void;
@@ -1,4 +1,7 @@
1
1
  import { normalizeProductApiBaseUrl } from './api-base-url';
2
+ import { normalizeLocalCryptoError } from './decryption-errors';
3
+ import { checkLocalCryptoHealth } from './local-crypto-health';
4
+ import { buildMediaMessageMetadataPayload, extractMediaAssetIds, serializeMediaMessageMetadata, } from './media-message-metadata';
2
5
  import { mapLocalSdkMessage } from './message.mapper';
3
6
  import { WasmCallQueue } from './wasm-call-queue';
4
7
  const DEFAULT_DEVICE_ID = 1;
@@ -43,20 +46,56 @@ export class MssLinkBrowserClient {
43
46
  this.wasmClient?.setAccessToken(accessToken);
44
47
  }
45
48
  async ensureReady() {
46
- await this.runWithClient(async (client) => {
47
- await client.ensureDeviceRegistered(this.registrationId, this.signedPrekeyId, this.initialOneTimePrekeyCount);
48
- await client.replenishOneTimePreKeys(this.minOneTimePrekeys, this.replenishOneTimePrekeys);
49
- });
49
+ try {
50
+ await this.runWithClient(async (client) => {
51
+ await client.ensureDeviceRegistered(this.registrationId, this.signedPrekeyId, this.initialOneTimePrekeyCount);
52
+ await client.replenishOneTimePreKeys(this.minOneTimePrekeys, this.replenishOneTimePrekeys);
53
+ });
54
+ }
55
+ catch (error) {
56
+ throw normalizeLocalCryptoError(error);
57
+ }
58
+ }
59
+ async getLocalCryptoHealth() {
60
+ return checkLocalCryptoHealth(() => this.ensureReady());
50
61
  }
51
62
  async sendMessage(params) {
52
- return this.runWithClient((client) => client.sendMessage(params.chatId, params.targetUserId, params.text, params.kind ?? 'TEXT'));
63
+ try {
64
+ await this.ensureReady();
65
+ return this.runWithClient((client) => client.sendMessage(params.chatId, params.targetUserId, params.text, params.kind ?? 'TEXT'));
66
+ }
67
+ catch (error) {
68
+ throw normalizeLocalCryptoError(error);
69
+ }
70
+ }
71
+ async sendMediaMessage(params) {
72
+ try {
73
+ await this.ensureReady();
74
+ const payload = buildMediaMessageMetadataPayload(params.attachments, params.caption);
75
+ const metadataJson = serializeMediaMessageMetadata(payload);
76
+ const mediaAssetIds = extractMediaAssetIds(params.attachments);
77
+ return this.runWithClient((client) => client.sendMediaMessage(params.chatId, params.targetUserId, metadataJson, mediaAssetIds));
78
+ }
79
+ catch (error) {
80
+ throw normalizeLocalCryptoError(error);
81
+ }
53
82
  }
54
83
  async syncMessages() {
55
- await this.runWithClient((client) => client.syncMessages());
84
+ try {
85
+ await this.runWithClient((client) => client.syncMessages());
86
+ }
87
+ catch (error) {
88
+ throw normalizeLocalCryptoError(error);
89
+ }
56
90
  }
57
91
  async getLocalMessages(chatId) {
58
- const rawMessages = await this.runWithClient((client) => client.getLocalMessages(chatId));
59
- return rawMessages.map((message) => mapLocalSdkMessage(message));
92
+ try {
93
+ const rawMessages = await this.runWithClient((client) => client.getLocalMessages(chatId));
94
+ return rawMessages.map((message) => mapLocalSdkMessage(message));
95
+ }
96
+ catch (error) {
97
+ throw normalizeLocalCryptoError(error);
98
+ }
60
99
  }
61
100
  reset() {
62
101
  this.wasmClient?.free();
@@ -78,7 +117,7 @@ export class MssLinkBrowserClient {
78
117
  return this.wasmClient;
79
118
  }
80
119
  loadWasmModule() {
81
- this.wasmModulePromise ??= import('@msssystems/mss-crypto-wasm');
120
+ this.wasmModulePromise ??= import('@msssystems/mss-crypto-wasm').then((module) => module);
82
121
  return this.wasmModulePromise;
83
122
  }
84
123
  }
@@ -1,6 +1,9 @@
1
1
  import type { WasmMssClient } from '@msssystems/mss-crypto-wasm';
2
- export type WasmMssClientConstructor = new (userId: string, deviceId: number, apiBaseUrl: string) => WasmMssClient;
2
+ export interface MssLinkWasmClient extends WasmMssClient {
3
+ sendMediaMessage(chatId: string, targetUserId: string, metadataJson: string, mediaAssetIds: string[]): Promise<string>;
4
+ }
5
+ export type WasmMssClientConstructor = new (userId: string, deviceId: number, apiBaseUrl: string) => MssLinkWasmClient;
3
6
  export interface MssCryptoWasmModule {
4
7
  WasmMssClient: WasmMssClientConstructor;
5
8
  }
6
- export type WasmMssClientInstance = WasmMssClient;
9
+ export type WasmMssClientInstance = MssLinkWasmClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msssystems/mss-link-sdk",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "Official managed application SDK for MSS ecosystem",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,7 +53,7 @@
53
53
  "registry": "https://registry.npmjs.org/"
54
54
  },
55
55
  "peerDependencies": {
56
- "@msssystems/mss-crypto-wasm": "^0.1.16",
56
+ "@msssystems/mss-crypto-wasm": "^0.1.17",
57
57
  "@nestjs/common": "^10.0.0 || ^11.0.0",
58
58
  "@nestjs/core": "^10.0.0 || ^11.0.0",
59
59
  "express": "^4.0.0 || ^5.0.0",
@@ -77,7 +77,7 @@
77
77
  }
78
78
  },
79
79
  "devDependencies": {
80
- "@msssystems/mss-crypto-wasm": "^0.1.16",
80
+ "@msssystems/mss-crypto-wasm": "^0.1.17",
81
81
  "@nestjs/common": "^11.0.1",
82
82
  "@nestjs/core": "^11.0.1",
83
83
  "@types/express": "^5.0.0",