@msssystems/mss-link-sdk 0.1.8 → 0.1.10

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,40 @@
1
+ import type { DriveItemType, DriveWrappedItemKey } from './drive-sharing.contracts';
2
+ export type DriveRecipientWrappingKeyAlgorithm = 'AES-256-GCM';
3
+ export type DriveWrappedItemKeyAlgorithm = 'MSS-DRIVE-AES-256-GCM-WRAPPED-ITEM-KEY-v1';
4
+ export interface DriveRecipientWrappingKey {
5
+ algorithm: DriveRecipientWrappingKeyAlgorithm;
6
+ keyId: string;
7
+ rawKeyBase64: string;
8
+ }
9
+ export interface DriveRecipientKeyTarget {
10
+ accountId?: string;
11
+ userId?: string;
12
+ username?: string;
13
+ wrappingKey: DriveRecipientWrappingKey;
14
+ }
15
+ export interface DriveItemContentKeyEnvelope {
16
+ algorithm?: 'AES-256-GCM' | string | null;
17
+ keyId?: string | null;
18
+ nonce?: string | null;
19
+ wrappedKey: string;
20
+ }
21
+ export interface CreateDriveWrappedItemKeyForRecipientInput {
22
+ itemId: string;
23
+ itemType: DriveItemType;
24
+ contentKey: DriveItemContentKeyEnvelope;
25
+ recipient: DriveRecipientKeyTarget;
26
+ }
27
+ export interface DriveWrappedItemKeyPayload {
28
+ contentAlgorithm: string;
29
+ contentKey: string;
30
+ contentKeyId?: string;
31
+ contentNonce?: string;
32
+ createdAt: string;
33
+ itemId: string;
34
+ itemType: DriveItemType;
35
+ schema: 'mss.link.drive.item-key.v1';
36
+ }
37
+ export interface DriveWrappedItemKeyForRecipientResult {
38
+ payload: DriveWrappedItemKeyPayload;
39
+ wrappedItemKey: DriveWrappedItemKey;
40
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { CreateDriveWrappedItemKeyForRecipientInput, DriveWrappedItemKeyForRecipientResult } from './drive-key-wrapping.contracts';
2
+ export declare function createDriveWrappedItemKeyForRecipient(input: CreateDriveWrappedItemKeyForRecipientInput): Promise<DriveWrappedItemKeyForRecipientResult>;
@@ -0,0 +1,133 @@
1
+ const CONTENT_KEY_SCHEMA = 'mss.link.drive.item-key.v1';
2
+ const WRAPPED_ITEM_KEY_ALGORITHM = 'MSS-DRIVE-AES-256-GCM-WRAPPED-ITEM-KEY-v1';
3
+ const AES_GCM_NONCE_LENGTH = 12;
4
+ const AES_256_KEY_LENGTH = 32;
5
+ export async function createDriveWrappedItemKeyForRecipient(input) {
6
+ const itemId = normalizeRequiredText(input.itemId, 'Drive item id is required for key wrapping.');
7
+ const itemType = input.itemType;
8
+ const recipient = normalizeRecipient(input.recipient);
9
+ const contentKey = normalizeContentKey(input.contentKey);
10
+ const wrappingKey = await importRecipientWrappingKey(recipient.wrappingKey);
11
+ const nonce = globalThis.crypto.getRandomValues(new Uint8Array(AES_GCM_NONCE_LENGTH));
12
+ const payload = buildWrappedItemKeyPayload({
13
+ contentKey,
14
+ itemId,
15
+ itemType,
16
+ });
17
+ const plaintext = new TextEncoder().encode(JSON.stringify(payload));
18
+ const additionalData = buildWrappingAdditionalData({
19
+ itemId,
20
+ itemType,
21
+ recipientId: recipient.wrappingKey.keyId,
22
+ });
23
+ const encrypted = await globalThis.crypto.subtle.encrypt({
24
+ additionalData: toArrayBuffer(additionalData),
25
+ iv: nonce,
26
+ name: 'AES-GCM',
27
+ }, wrappingKey, toArrayBuffer(plaintext));
28
+ return {
29
+ payload,
30
+ wrappedItemKey: {
31
+ encryptedItemKey: encodeBase64Url(new Uint8Array(encrypted)),
32
+ keyAlgorithm: WRAPPED_ITEM_KEY_ALGORITHM,
33
+ keyNonce: encodeBase64Url(nonce),
34
+ keyRecipientId: recipient.wrappingKey.keyId,
35
+ },
36
+ };
37
+ }
38
+ function buildWrappedItemKeyPayload({ contentKey, itemId, itemType, }) {
39
+ return {
40
+ schema: CONTENT_KEY_SCHEMA,
41
+ itemId,
42
+ itemType,
43
+ contentAlgorithm: contentKey.algorithm,
44
+ contentKey: contentKey.wrappedKey,
45
+ createdAt: new Date().toISOString(),
46
+ ...(contentKey.keyId ? { contentKeyId: contentKey.keyId } : {}),
47
+ ...(contentKey.nonce ? { contentNonce: contentKey.nonce } : {}),
48
+ };
49
+ }
50
+ function normalizeRecipient(recipient) {
51
+ const accountId = normalizeOptionalText(recipient.accountId) ?? '';
52
+ const userId = normalizeOptionalText(recipient.userId) ?? '';
53
+ const username = normalizeOptionalText(recipient.username)?.replace(/^@/, '') ?? '';
54
+ const wrappingKey = normalizeRecipientWrappingKey(recipient.wrappingKey);
55
+ if (!accountId && !userId && !username) {
56
+ throw new Error('Drive share recipient must include accountId, userId or username.');
57
+ }
58
+ return {
59
+ accountId,
60
+ userId,
61
+ username,
62
+ wrappingKey,
63
+ };
64
+ }
65
+ function normalizeRecipientWrappingKey(key) {
66
+ if (key.algorithm !== 'AES-256-GCM') {
67
+ throw new Error(`Unsupported Drive recipient wrapping key algorithm: ${key.algorithm}`);
68
+ }
69
+ return {
70
+ algorithm: key.algorithm,
71
+ keyId: normalizeRequiredText(key.keyId, 'Drive recipient wrapping key id is required.'),
72
+ rawKeyBase64: normalizeRequiredText(key.rawKeyBase64, 'Drive recipient wrapping key material is required.'),
73
+ };
74
+ }
75
+ function normalizeContentKey(contentKey) {
76
+ const algorithm = contentKey.algorithm?.trim() || 'AES-256-GCM';
77
+ if (algorithm !== 'AES-256-GCM') {
78
+ throw new Error(`Unsupported Drive item content key algorithm: ${algorithm}`);
79
+ }
80
+ return {
81
+ algorithm,
82
+ keyId: normalizeOptionalText(contentKey.keyId) ?? '',
83
+ nonce: normalizeOptionalText(contentKey.nonce) ?? '',
84
+ wrappedKey: normalizeRequiredText(contentKey.wrappedKey, 'Drive item content key is required.'),
85
+ };
86
+ }
87
+ async function importRecipientWrappingKey(key) {
88
+ const keyBytes = decodeBase64Url(key.rawKeyBase64);
89
+ if (keyBytes.byteLength !== AES_256_KEY_LENGTH) {
90
+ throw new Error('Drive recipient wrapping key must be 32 bytes for AES-256-GCM.');
91
+ }
92
+ return globalThis.crypto.subtle.importKey('raw', toArrayBuffer(keyBytes), {
93
+ name: 'AES-GCM',
94
+ }, false, ['encrypt']);
95
+ }
96
+ function buildWrappingAdditionalData({ itemId, itemType, recipientId, }) {
97
+ return new TextEncoder().encode(`${CONTENT_KEY_SCHEMA}:${itemType}:${itemId}:${recipientId}`);
98
+ }
99
+ function normalizeRequiredText(value, message) {
100
+ const normalized = normalizeOptionalText(value);
101
+ if (!normalized) {
102
+ throw new Error(message);
103
+ }
104
+ return normalized;
105
+ }
106
+ function normalizeOptionalText(value) {
107
+ const normalized = value?.trim();
108
+ return normalized ? normalized : undefined;
109
+ }
110
+ function decodeBase64Url(value) {
111
+ const normalized = value.trim().replace(/-/g, '+').replace(/_/g, '/');
112
+ const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), '=');
113
+ const decoded = globalThis.atob(padded);
114
+ const bytes = new Uint8Array(decoded.length);
115
+ for (let index = 0; index < decoded.length; index += 1) {
116
+ bytes[index] = decoded.charCodeAt(index);
117
+ }
118
+ return bytes;
119
+ }
120
+ function encodeBase64Url(bytes) {
121
+ let binary = '';
122
+ for (const byte of bytes) {
123
+ binary += String.fromCharCode(byte);
124
+ }
125
+ return globalThis
126
+ .btoa(binary)
127
+ .replace(/\+/g, '-')
128
+ .replace(/\//g, '_')
129
+ .replace(/=+$/g, '');
130
+ }
131
+ function toArrayBuffer(bytes) {
132
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
133
+ }
@@ -0,0 +1,64 @@
1
+ export type DrivePermissionRole = 'editor' | 'owner' | 'viewer';
2
+ export type DriveEditablePermissionRole = Exclude<DrivePermissionRole, 'owner'>;
3
+ export type DriveVisibility = 'private' | 'shared';
4
+ export type DriveItemType = 'file' | 'folder';
5
+ export type DriveShareTargetType = 'user';
6
+ export interface DriveShareTarget {
7
+ type: DriveShareTargetType;
8
+ userId?: string;
9
+ username?: string;
10
+ }
11
+ export interface DriveWrappedItemKey {
12
+ encryptedItemKey: string;
13
+ keyAlgorithm?: string;
14
+ keyNonce?: string;
15
+ keyRecipientId?: string;
16
+ }
17
+ export interface CreateDriveShareGrantInput {
18
+ role: DriveEditablePermissionRole;
19
+ target: DriveShareTarget;
20
+ wrappedItemKey: DriveWrappedItemKey;
21
+ }
22
+ export interface DriveShareGrantPayload {
23
+ encryptedItemKey: string;
24
+ keyAlgorithm?: string;
25
+ keyNonce?: string;
26
+ keyRecipientId?: string;
27
+ role: 'EDITOR' | 'VIEWER';
28
+ target: {
29
+ type: 'user';
30
+ userId?: string;
31
+ username?: string;
32
+ };
33
+ }
34
+ export interface DriveAccessPrincipal {
35
+ accountId: string;
36
+ avatarUrl: string | null;
37
+ displayName: string | null;
38
+ userId: string | null;
39
+ username: string | null;
40
+ }
41
+ export interface DriveAccessGrant {
42
+ createdAt: string;
43
+ encryptedItemKey: string;
44
+ fileId: string | null;
45
+ folderId: string | null;
46
+ grantee: DriveAccessPrincipal;
47
+ granteeAccountId: string;
48
+ id: string;
49
+ itemType: 'FILE' | 'FOLDER';
50
+ keyAlgorithm: string;
51
+ keyNonce: string | null;
52
+ keyRecipientId: string | null;
53
+ ownerAccountId: string;
54
+ role: 'EDITOR' | 'OWNER' | 'VIEWER';
55
+ updatedAt: string;
56
+ }
57
+ export interface DriveAccessResponse {
58
+ currentUserRole: 'EDITOR' | 'OWNER' | 'VIEWER';
59
+ grants: DriveAccessGrant[];
60
+ itemId: string;
61
+ itemType: 'FILE' | 'FOLDER';
62
+ owner: DriveAccessPrincipal;
63
+ visibility: 'PRIVATE' | 'SHARED';
64
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { CreateDriveShareGrantInput, DriveEditablePermissionRole, DriveShareGrantPayload, DriveShareTarget, DriveWrappedItemKey } from './drive-sharing.contracts';
2
+ export declare function createDriveShareGrantPayload(input: CreateDriveShareGrantInput): DriveShareGrantPayload;
3
+ export declare function normalizeDriveShareTarget(target: DriveShareTarget): DriveShareGrantPayload['target'];
4
+ export declare function normalizeWrappedItemKey(key: DriveWrappedItemKey): Omit<DriveShareGrantPayload, 'role' | 'target'>;
5
+ export declare function toApiDrivePermissionRole(role: DriveEditablePermissionRole): DriveShareGrantPayload['role'];
@@ -0,0 +1,51 @@
1
+ const DEFAULT_KEY_ALGORITHM = 'X25519-AES-256-GCM';
2
+ export function createDriveShareGrantPayload(input) {
3
+ return {
4
+ ...normalizeWrappedItemKey(input.wrappedItemKey),
5
+ role: toApiDrivePermissionRole(input.role),
6
+ target: normalizeDriveShareTarget(input.target),
7
+ };
8
+ }
9
+ export function normalizeDriveShareTarget(target) {
10
+ if (target.type !== 'user') {
11
+ throw new Error('Drive sharing supports only selected users in v1.');
12
+ }
13
+ const userId = target.userId?.trim();
14
+ const username = target.username?.trim().replace(/^@/, '').toLowerCase();
15
+ if (!userId && !username) {
16
+ throw new Error('Drive share target must include userId or username.');
17
+ }
18
+ return {
19
+ type: 'user',
20
+ ...(userId ? { userId } : {}),
21
+ ...(username ? { username } : {}),
22
+ };
23
+ }
24
+ export function normalizeWrappedItemKey(key) {
25
+ const encryptedItemKey = key.encryptedItemKey.trim();
26
+ const keyAlgorithm = (key.keyAlgorithm?.trim() || DEFAULT_KEY_ALGORITHM).trim();
27
+ const keyNonce = key.keyNonce?.trim();
28
+ const keyRecipientId = key.keyRecipientId?.trim();
29
+ if (!encryptedItemKey) {
30
+ throw new Error('Drive share requires encrypted wrapped item key.');
31
+ }
32
+ if (!keyAlgorithm) {
33
+ throw new Error('Drive share key algorithm is required.');
34
+ }
35
+ return {
36
+ encryptedItemKey,
37
+ keyAlgorithm,
38
+ ...(keyNonce ? { keyNonce } : {}),
39
+ ...(keyRecipientId ? { keyRecipientId } : {}),
40
+ };
41
+ }
42
+ export function toApiDrivePermissionRole(role) {
43
+ switch (role) {
44
+ case 'editor':
45
+ return 'EDITOR';
46
+ case 'viewer':
47
+ return 'VIEWER';
48
+ default:
49
+ throw new Error('Drive owner role cannot be granted through sharing.');
50
+ }
51
+ }
@@ -3,6 +3,10 @@ export * from './client-options';
3
3
  export * from './decryption-errors';
4
4
  export * from './drive-file-runtime';
5
5
  export * from './drive-file-runtime.contracts';
6
+ export * from './drive-key-wrapping';
7
+ export * from './drive-key-wrapping.contracts';
8
+ export * from './drive-sharing';
9
+ export * from './drive-sharing.contracts';
6
10
  export * from './local-crypto-health';
7
11
  export * from './local-crypto-health.contracts';
8
12
  export * from './media-crypto.contracts';
@@ -3,6 +3,10 @@ export * from './client-options';
3
3
  export * from './decryption-errors';
4
4
  export * from './drive-file-runtime';
5
5
  export * from './drive-file-runtime.contracts';
6
+ export * from './drive-key-wrapping';
7
+ export * from './drive-key-wrapping.contracts';
8
+ export * from './drive-sharing';
9
+ export * from './drive-sharing.contracts';
6
10
  export * from './local-crypto-health';
7
11
  export * from './local-crypto-health.contracts';
8
12
  export * from './media-crypto.contracts';
@@ -4,6 +4,7 @@ import type { DecryptMediaBlobInput, DecryptedMediaBlobResult, EncryptedMediaBlo
4
4
  import type { SendMediaMessageParams } from './media-message.contracts';
5
5
  import type { DecryptedMessage } from './message.contracts';
6
6
  import type { DecryptDriveFileMetadataInput, DecryptDriveFolderMetadataInput, DecryptedDriveFileMetadata, DecryptedDriveFolderMetadata, EncryptedDriveFileMetadata, EncryptedDriveFolderMetadata, EncryptDriveFileMetadataInput, EncryptDriveFolderMetadataInput } from './storage-metadata-crypto.contracts';
7
+ import type { CreateDriveWrappedItemKeyForRecipientInput, DriveWrappedItemKeyForRecipientResult } from './drive-key-wrapping.contracts';
7
8
  export declare class MssLinkBrowserClient {
8
9
  private readonly userId;
9
10
  private readonly apiBaseUrl;
@@ -34,6 +35,7 @@ export declare class MssLinkBrowserClient {
34
35
  decryptDriveFolderMetadata(input: DecryptDriveFolderMetadataInput): Promise<DecryptedDriveFolderMetadata>;
35
36
  encryptDriveFileMetadata(input: EncryptDriveFileMetadataInput): Promise<EncryptedDriveFileMetadata>;
36
37
  decryptDriveFileMetadata(input: DecryptDriveFileMetadataInput): Promise<DecryptedDriveFileMetadata>;
38
+ createDriveWrappedItemKeyForRecipient(input: CreateDriveWrappedItemKeyForRecipientInput): Promise<DriveWrappedItemKeyForRecipientResult>;
37
39
  syncMessages(): Promise<void>;
38
40
  getLocalMessages(chatId: string): Promise<DecryptedMessage[]>;
39
41
  reset(): void;
@@ -4,6 +4,7 @@ import { checkLocalCryptoHealth } from './local-crypto-health';
4
4
  import { buildMediaMessageMetadataPayload, extractMediaAssetIds, serializeMediaMessageMetadata, } from './media-message-metadata';
5
5
  import { mapLocalSdkMessage } from './message.mapper';
6
6
  import { buildDecryptedDriveFileMetadata, buildDecryptedDriveFolderMetadata, buildEncryptedDriveFileMetadata, buildEncryptedDriveFolderMetadata, normalizeDriveFileMetadataInput, normalizeDriveFolderMetadataInput, normalizeEncryptedDriveFileMetadataInput, normalizeEncryptedDriveFolderMetadataInput, } from './storage-metadata-crypto';
7
+ import { createDriveWrappedItemKeyForRecipient } from './drive-key-wrapping';
7
8
  import { WasmCallQueue } from './wasm-call-queue';
8
9
  const DEFAULT_DEVICE_ID = 1;
9
10
  const DEFAULT_REGISTRATION_ID = 1;
@@ -209,6 +210,9 @@ export class MssLinkBrowserClient {
209
210
  throw normalizeLocalCryptoError(error);
210
211
  }
211
212
  }
213
+ createDriveWrappedItemKeyForRecipient(input) {
214
+ return createDriveWrappedItemKeyForRecipient(input);
215
+ }
212
216
  async syncMessages() {
213
217
  try {
214
218
  await this.runWithClient((client) => client.syncMessages());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msssystems/mss-link-sdk",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Official managed application SDK for MSS ecosystem",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",