@msssystems/mss-link-sdk 0.1.3 → 0.1.6

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.
@@ -3,6 +3,9 @@ export * from './client-options';
3
3
  export * from './decryption-errors';
4
4
  export * from './local-crypto-health';
5
5
  export * from './local-crypto-health.contracts';
6
+ export * from './media-crypto.contracts';
7
+ export * from './media-message.contracts';
8
+ export * from './media-message-metadata';
6
9
  export * from './message.contracts';
7
10
  export * from './message-decryption.contracts';
8
11
  export * from './message-decryption-state';
@@ -3,6 +3,9 @@ export * from './client-options';
3
3
  export * from './decryption-errors';
4
4
  export * from './local-crypto-health';
5
5
  export * from './local-crypto-health.contracts';
6
+ export * from './media-crypto.contracts';
7
+ export * from './media-message.contracts';
8
+ export * from './media-message-metadata';
6
9
  export * from './message.contracts';
7
10
  export * from './message-decryption.contracts';
8
11
  export * from './message-decryption-state';
@@ -0,0 +1,28 @@
1
+ import type { MediaMessageAttachmentEncryptionInput, MediaMessageAttachmentInput, MediaMessageAttachmentMetadata } from './media-message.contracts';
2
+ export interface EncryptMediaBlobInput {
3
+ assetId: string;
4
+ blob: Blob;
5
+ mimeType: string;
6
+ size: string | number;
7
+ originalName?: string;
8
+ uploadedAt?: string;
9
+ }
10
+ export interface EncryptedMediaBlobResult {
11
+ encryptedBlob: Blob;
12
+ attachment: MediaMessageAttachmentInput;
13
+ encryption: MediaMessageAttachmentEncryptionInput;
14
+ }
15
+ export interface DecryptMediaBlobInput {
16
+ encryptedBlob: Blob;
17
+ metadata: MediaMessageAttachmentMetadata;
18
+ }
19
+ export interface DecryptedMediaBlobResult {
20
+ blob: Blob;
21
+ fileName?: string;
22
+ mimeType: string;
23
+ }
24
+ export interface WasmEncryptedMediaBytes {
25
+ ciphertext: Uint8Array;
26
+ key: string;
27
+ nonce: string;
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -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,93 @@
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
+ ...(attachment.encryption
26
+ ? { encryption: serializeAttachmentEncryption(attachment.encryption) }
27
+ : {}),
28
+ })),
29
+ ...(payload.caption ? { caption: payload.caption } : {}),
30
+ });
31
+ }
32
+ export function extractMediaAssetIds(attachments) {
33
+ return attachments.map((attachment) => attachment.assetId);
34
+ }
35
+ function normalizeAttachment(attachment) {
36
+ const assetId = attachment.assetId.trim();
37
+ const mimeType = attachment.mimeType.trim();
38
+ const size = String(attachment.size);
39
+ if (!assetId) {
40
+ throw new Error('Media attachment assetId is required.');
41
+ }
42
+ if (!mimeType) {
43
+ throw new Error('Media attachment mimeType is required.');
44
+ }
45
+ if (!size || size === 'NaN') {
46
+ throw new Error('Media attachment size is required.');
47
+ }
48
+ return {
49
+ assetId,
50
+ mimeType,
51
+ size,
52
+ ...(attachment.originalName?.trim()
53
+ ? { originalName: attachment.originalName.trim() }
54
+ : {}),
55
+ ...(attachment.uploadedAt?.trim()
56
+ ? { uploadedAt: attachment.uploadedAt.trim() }
57
+ : {}),
58
+ ...(attachment.encryption
59
+ ? { encryption: normalizeAttachmentEncryption(attachment.encryption) }
60
+ : {}),
61
+ };
62
+ }
63
+ function normalizeAttachmentEncryption(encryption) {
64
+ const keyId = encryption.keyId?.trim();
65
+ const nonce = encryption.nonce?.trim();
66
+ const wrappedKey = encryption.wrappedKey?.trim();
67
+ const algorithm = encryption.algorithm ?? 'AES-256-GCM';
68
+ if (algorithm !== 'AES-256-GCM') {
69
+ throw new Error('Unsupported media encryption algorithm.');
70
+ }
71
+ if (!nonce) {
72
+ throw new Error('Media encryption nonce is required.');
73
+ }
74
+ if (!wrappedKey) {
75
+ throw new Error('Media encryption wrappedKey is required.');
76
+ }
77
+ return {
78
+ schema: 'mss.link.media-encryption.v1',
79
+ algorithm,
80
+ ...(keyId ? { keyId } : {}),
81
+ nonce,
82
+ wrappedKey,
83
+ };
84
+ }
85
+ function serializeAttachmentEncryption(encryption) {
86
+ return {
87
+ schema: 'mss.link.media-encryption.v1',
88
+ algorithm: encryption.algorithm,
89
+ ...(encryption.keyId ? { keyId: encryption.keyId } : {}),
90
+ nonce: encryption.nonce,
91
+ wrappedKey: encryption.wrappedKey,
92
+ };
93
+ }
@@ -0,0 +1,41 @@
1
+ export interface MediaMessageAttachmentInput {
2
+ assetId: string;
3
+ mimeType: string;
4
+ size: string | number;
5
+ originalName?: string;
6
+ uploadedAt?: string;
7
+ encryption?: MediaMessageAttachmentEncryptionInput;
8
+ }
9
+ export interface MediaMessageAttachmentEncryptionInput {
10
+ schema?: 'mss.link.media-encryption.v1';
11
+ keyId?: string;
12
+ nonce?: string;
13
+ wrappedKey?: string;
14
+ algorithm?: 'AES-256-GCM';
15
+ }
16
+ export interface SendMediaMessageParams {
17
+ chatId: string;
18
+ targetUserId: string;
19
+ attachments: MediaMessageAttachmentInput[];
20
+ caption?: string;
21
+ }
22
+ export interface MediaMessageAttachmentMetadata {
23
+ assetId: string;
24
+ mimeType: string;
25
+ size: string;
26
+ originalName?: string;
27
+ uploadedAt?: string;
28
+ encryption?: MediaMessageAttachmentEncryptionMetadata;
29
+ }
30
+ export interface MediaMessageAttachmentEncryptionMetadata {
31
+ schema: 'mss.link.media-encryption.v1';
32
+ algorithm: 'AES-256-GCM';
33
+ keyId?: string;
34
+ nonce: string;
35
+ wrappedKey: string;
36
+ }
37
+ export interface MediaMessageMetadataPayload {
38
+ schema: 'mss.link.media-message.v1';
39
+ attachments: MediaMessageAttachmentMetadata[];
40
+ caption?: string;
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,7 @@
1
1
  import type { MssLinkBrowserClientOptions } from './client-options';
2
2
  import type { LocalCryptoHealth } from './local-crypto-health.contracts';
3
+ import type { DecryptMediaBlobInput, DecryptedMediaBlobResult, EncryptedMediaBlobResult, EncryptMediaBlobInput } from './media-crypto.contracts';
4
+ import type { SendMediaMessageParams } from './media-message.contracts';
3
5
  import type { DecryptedMessage } from './message.contracts';
4
6
  export declare class MssLinkBrowserClient {
5
7
  private readonly userId;
@@ -24,6 +26,9 @@ export declare class MssLinkBrowserClient {
24
26
  text: string;
25
27
  kind?: string;
26
28
  }): Promise<string>;
29
+ sendMediaMessage(params: SendMediaMessageParams): Promise<string>;
30
+ encryptMediaBlob(input: EncryptMediaBlobInput): Promise<EncryptedMediaBlobResult>;
31
+ decryptMediaBlob(input: DecryptMediaBlobInput): Promise<DecryptedMediaBlobResult>;
27
32
  syncMessages(): Promise<void>;
28
33
  getLocalMessages(chatId: string): Promise<DecryptedMessage[]>;
29
34
  reset(): void;
@@ -1,6 +1,7 @@
1
1
  import { normalizeProductApiBaseUrl } from './api-base-url';
2
2
  import { normalizeLocalCryptoError } from './decryption-errors';
3
3
  import { checkLocalCryptoHealth } from './local-crypto-health';
4
+ import { buildMediaMessageMetadataPayload, extractMediaAssetIds, serializeMediaMessageMetadata, } from './media-message-metadata';
4
5
  import { mapLocalSdkMessage } from './message.mapper';
5
6
  import { WasmCallQueue } from './wasm-call-queue';
6
7
  const DEFAULT_DEVICE_ID = 1;
@@ -67,6 +68,88 @@ export class MssLinkBrowserClient {
67
68
  throw normalizeLocalCryptoError(error);
68
69
  }
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
+ }
82
+ }
83
+ async encryptMediaBlob(input) {
84
+ try {
85
+ const plaintext = new Uint8Array(await input.blob.arrayBuffer());
86
+ const encrypted = await this.queue.run(async () => {
87
+ const module = await this.loadWasmModule();
88
+ return module.encryptMediaBytes(plaintext);
89
+ });
90
+ const encryptedBytes = new Uint8Array(encrypted.ciphertext);
91
+ return {
92
+ encryptedBlob: new Blob([encryptedBytes], {
93
+ type: 'application/octet-stream',
94
+ }),
95
+ encryption: {
96
+ schema: 'mss.link.media-encryption.v1',
97
+ algorithm: 'AES-256-GCM',
98
+ keyId: input.assetId,
99
+ nonce: encrypted.nonce,
100
+ wrappedKey: encrypted.key,
101
+ },
102
+ attachment: {
103
+ assetId: input.assetId,
104
+ mimeType: input.mimeType,
105
+ size: input.size,
106
+ ...(input.originalName ? { originalName: input.originalName } : {}),
107
+ ...(input.uploadedAt ? { uploadedAt: input.uploadedAt } : {}),
108
+ encryption: {
109
+ schema: 'mss.link.media-encryption.v1',
110
+ algorithm: 'AES-256-GCM',
111
+ keyId: input.assetId,
112
+ nonce: encrypted.nonce,
113
+ wrappedKey: encrypted.key,
114
+ },
115
+ },
116
+ };
117
+ }
118
+ catch (error) {
119
+ throw normalizeLocalCryptoError(error);
120
+ }
121
+ }
122
+ async decryptMediaBlob(input) {
123
+ const encryption = input.metadata.encryption;
124
+ if (!encryption) {
125
+ return {
126
+ blob: input.encryptedBlob,
127
+ mimeType: input.metadata.mimeType,
128
+ ...(input.metadata.originalName
129
+ ? { fileName: input.metadata.originalName }
130
+ : {}),
131
+ };
132
+ }
133
+ try {
134
+ const ciphertext = new Uint8Array(await input.encryptedBlob.arrayBuffer());
135
+ const plaintext = await this.queue.run(async () => {
136
+ const module = await this.loadWasmModule();
137
+ return module.decryptMediaBytes(ciphertext, encryption.wrappedKey, encryption.nonce);
138
+ });
139
+ return {
140
+ blob: new Blob([new Uint8Array(plaintext)], {
141
+ type: input.metadata.mimeType,
142
+ }),
143
+ mimeType: input.metadata.mimeType,
144
+ ...(input.metadata.originalName
145
+ ? { fileName: input.metadata.originalName }
146
+ : {}),
147
+ };
148
+ }
149
+ catch (error) {
150
+ throw normalizeLocalCryptoError(error);
151
+ }
152
+ }
70
153
  async syncMessages() {
71
154
  try {
72
155
  await this.runWithClient((client) => client.syncMessages());
@@ -104,7 +187,7 @@ export class MssLinkBrowserClient {
104
187
  return this.wasmClient;
105
188
  }
106
189
  loadWasmModule() {
107
- this.wasmModulePromise ??= import('@msssystems/mss-crypto-wasm');
190
+ this.wasmModulePromise ??= import('@msssystems/mss-crypto-wasm').then((module) => module);
108
191
  return this.wasmModulePromise;
109
192
  }
110
193
  }
@@ -1,6 +1,12 @@
1
1
  import type { WasmMssClient } from '@msssystems/mss-crypto-wasm';
2
- export type WasmMssClientConstructor = new (userId: string, deviceId: number, apiBaseUrl: string) => WasmMssClient;
2
+ import type { WasmEncryptedMediaBytes } from './media-crypto.contracts';
3
+ export interface MssLinkWasmClient extends WasmMssClient {
4
+ sendMediaMessage(chatId: string, targetUserId: string, metadataJson: string, mediaAssetIds: string[]): Promise<string>;
5
+ }
6
+ export type WasmMssClientConstructor = new (userId: string, deviceId: number, apiBaseUrl: string) => MssLinkWasmClient;
3
7
  export interface MssCryptoWasmModule {
4
8
  WasmMssClient: WasmMssClientConstructor;
9
+ encryptMediaBytes(plaintext: Uint8Array): WasmEncryptedMediaBytes;
10
+ decryptMediaBytes(ciphertext: Uint8Array, key: string, nonce: string): Uint8Array;
5
11
  }
6
- export type WasmMssClientInstance = WasmMssClient;
12
+ 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.3",
3
+ "version": "0.1.6",
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",