@redseat/api 0.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,238 @@
1
+ /**
2
+ * Cross-platform encryption/decryption functions
3
+ * Supports the same formats as src/lib/crypt.ts
4
+ */
5
+ import { getCryptoSubtle, getCryptoRandomValues, uint8ArrayFromBase64Url, arrayBufferToBase64, base64ToBase64Url, } from './crypto';
6
+ // Salt constants matching existing implementation
7
+ const TEXT_SALT = 'a1209660b32cca003630cb963f730b54';
8
+ const FILE_SALT = 'e5709660b22ab0803630cb963f703b83';
9
+ /**
10
+ * Derive a CryptoKey from a passphrase using PBKDF2
11
+ * @param passPhrase - The passphrase to derive the key from
12
+ * @param type - 'text' for text encryption, 'file' for file encryption
13
+ * @returns A CryptoKey suitable for AES-CBC encryption/decryption
14
+ */
15
+ export async function deriveKey(passPhrase, type) {
16
+ const crypto = getCryptoSubtle();
17
+ const saltB64 = type === 'text' ? TEXT_SALT : FILE_SALT;
18
+ const saltBuffer = uint8ArrayFromBase64Url(saltB64);
19
+ const passphraseKey = new TextEncoder().encode(passPhrase);
20
+ // Import the passphrase as a key for PBKDF2
21
+ const keyMaterial = await crypto.importKey('raw', passphraseKey, {
22
+ name: 'PBKDF2',
23
+ }, false, ['deriveBits', 'deriveKey']);
24
+ // Derive the actual encryption key
25
+ // Normalize saltBuffer to ensure it's a proper BufferSource
26
+ const salt = saltBuffer instanceof Uint8Array ? new Uint8Array(saltBuffer) : saltBuffer;
27
+ return await crypto.deriveKey({
28
+ name: 'PBKDF2',
29
+ salt: salt,
30
+ iterations: 1000,
31
+ hash: 'SHA-1',
32
+ }, keyMaterial, {
33
+ name: 'AES-CBC',
34
+ length: 256,
35
+ }, false, ['encrypt', 'decrypt']);
36
+ }
37
+ /**
38
+ * Generate a random 16-byte IV (Initialization Vector)
39
+ */
40
+ export function getRandomIV() {
41
+ const getRandomValues = getCryptoRandomValues();
42
+ return getRandomValues(new Uint8Array(16));
43
+ }
44
+ /**
45
+ * Encrypt a text string
46
+ * Returns format: `${base64ToBase64Url(IV)}.${base64ToBase64Url(encryptedData)}`
47
+ */
48
+ export async function encryptText(key, text) {
49
+ const crypto = getCryptoSubtle();
50
+ const iv = getRandomIV();
51
+ const textData = new TextEncoder().encode(text);
52
+ // Normalize iv to ensure it's a proper BufferSource
53
+ const ivBuffer = new Uint8Array(iv);
54
+ const encrypted = await crypto.encrypt({
55
+ name: 'AES-CBC',
56
+ iv: ivBuffer,
57
+ }, key, textData);
58
+ const ivB64 = base64ToBase64Url(arrayBufferToBase64(iv));
59
+ const encryptedB64 = base64ToBase64Url(arrayBufferToBase64(encrypted));
60
+ return `${ivB64}.${encryptedB64}`;
61
+ }
62
+ /**
63
+ * Decrypt a text string
64
+ * Expects format: `${base64ToBase64Url(IV)}.${base64ToBase64Url(encryptedData)}`
65
+ * Or can accept IV separately
66
+ */
67
+ export async function decryptText(key, encryptedText, iv) {
68
+ const crypto = getCryptoSubtle();
69
+ let extractedIv;
70
+ let encryptedData;
71
+ if (iv) {
72
+ extractedIv = iv;
73
+ encryptedData = encryptedText;
74
+ }
75
+ else {
76
+ const parts = encryptedText.split('.');
77
+ if (parts.length !== 2) {
78
+ throw new Error('Invalid encrypted text format. Expected format: IV.encryptedData');
79
+ }
80
+ extractedIv = uint8ArrayFromBase64Url(parts[0]);
81
+ encryptedData = parts[1];
82
+ }
83
+ const encryptedBuffer = uint8ArrayFromBase64Url(encryptedData);
84
+ // Normalize iv and buffer to ensure they're proper BufferSource
85
+ const ivBuffer = extractedIv instanceof Uint8Array ? new Uint8Array(extractedIv) : extractedIv;
86
+ const bufferSource = encryptedBuffer instanceof Uint8Array ? new Uint8Array(encryptedBuffer) : encryptedBuffer;
87
+ const decrypted = await crypto.decrypt({
88
+ name: 'AES-CBC',
89
+ iv: ivBuffer,
90
+ }, key, bufferSource);
91
+ return new TextDecoder('utf-8').decode(decrypted);
92
+ }
93
+ /**
94
+ * Encrypt a buffer (ArrayBuffer or Uint8Array)
95
+ */
96
+ export async function encryptBuffer(key, iv, buffer) {
97
+ const crypto = getCryptoSubtle();
98
+ // Normalize iv and buffer to ensure they're proper BufferSource
99
+ const ivBuffer = iv instanceof Uint8Array ? new Uint8Array(iv) : iv;
100
+ const bufferSource = buffer instanceof Uint8Array ? new Uint8Array(buffer) : buffer;
101
+ return await crypto.encrypt({
102
+ name: 'AES-CBC',
103
+ iv: ivBuffer,
104
+ }, key, bufferSource);
105
+ }
106
+ /**
107
+ * Decrypt a buffer (ArrayBuffer or Uint8Array)
108
+ */
109
+ export async function decryptBuffer(key, iv, buffer) {
110
+ const crypto = getCryptoSubtle();
111
+ // Normalize iv and buffer to ensure they're proper BufferSource
112
+ const ivBuffer = iv instanceof Uint8Array ? new Uint8Array(iv) : iv;
113
+ const bufferSource = buffer instanceof Uint8Array ? new Uint8Array(buffer) : buffer;
114
+ return await crypto.decrypt({
115
+ name: 'AES-CBC',
116
+ iv: ivBuffer,
117
+ }, key, bufferSource);
118
+ }
119
+ /**
120
+ * Encrypt a file with thumbnail and metadata
121
+ * Structure: [16 bytes IV][4 bytes thumb size][4 bytes info size][32 bytes thumb mime][256 bytes file mime][T bytes encrypted thumb][I bytes encrypted info][encrypted file data]
122
+ */
123
+ export async function encryptFile(key, options) {
124
+ const iv = getRandomIV();
125
+ // Encrypt thumbnail and file buffer
126
+ const encThumb = await encryptBuffer(key, iv, options.thumb);
127
+ const encBuffer = await encryptBuffer(key, iv, options.buffer);
128
+ // Create metadata buffers
129
+ const thumbMimeBuffer = stringToArrayBuffer(options.thumbMime, 32);
130
+ const fileMimeBuffer = stringToArrayBuffer(options.mime, 256);
131
+ // Create the encrypted file structure
132
+ // Use big-endian to match toBytesInt32 behavior (false = big-endian in DataView API)
133
+ const thumbSizeBytes = new ArrayBuffer(4);
134
+ new DataView(thumbSizeBytes).setInt32(0, encThumb.byteLength, false); // big-endian (false = big-endian)
135
+ const infoSizeBytes = new ArrayBuffer(4);
136
+ new DataView(infoSizeBytes).setInt32(0, 0, false); // info size is 0, big-endian
137
+ // Combine all parts
138
+ const totalLength = 16 + 4 + 4 + 32 + 256 + encThumb.byteLength + 0 + encBuffer.byteLength;
139
+ const result = new Uint8Array(totalLength);
140
+ let offset = 0;
141
+ // IV (16 bytes)
142
+ result.set(new Uint8Array(iv), offset);
143
+ offset += 16;
144
+ // Thumb size (4 bytes)
145
+ result.set(new Uint8Array(thumbSizeBytes), offset);
146
+ offset += 4;
147
+ // Info size (4 bytes)
148
+ result.set(new Uint8Array(infoSizeBytes), offset);
149
+ offset += 4;
150
+ // Thumb mime (32 bytes)
151
+ result.set(new Uint8Array(thumbMimeBuffer), offset);
152
+ offset += 32;
153
+ // File mime (256 bytes)
154
+ result.set(new Uint8Array(fileMimeBuffer), offset);
155
+ offset += 256;
156
+ // Encrypted thumb
157
+ result.set(new Uint8Array(encThumb), offset);
158
+ offset += encThumb.byteLength;
159
+ // Encrypted file data
160
+ result.set(new Uint8Array(encBuffer), offset);
161
+ // Encrypt filename
162
+ const binaryFilename = new TextEncoder().encode(options.filename);
163
+ const encFilename = await encryptBuffer(key, iv, binaryFilename);
164
+ const filenameB64 = base64ToBase64Url(arrayBufferToBase64(encFilename));
165
+ return {
166
+ filename: filenameB64,
167
+ ivb64: arrayBufferToBase64(iv),
168
+ thumbsize: encThumb.byteLength,
169
+ blob: result.buffer,
170
+ };
171
+ }
172
+ /**
173
+ * Decrypt a file buffer
174
+ * Extracts and decrypts the file data from the encrypted file structure
175
+ */
176
+ export async function decryptFile(key, buffer) {
177
+ const crypto = getCryptoSubtle();
178
+ const dv = new DataView(buffer, 0);
179
+ const iv = buffer.slice(0, 16);
180
+ // Read thumb size (handle endianness - try big-endian first since we write big-endian, then little-endian for backward compatibility)
181
+ let targetThumbSize = dv.getInt32(16, false); // big-endian (default, matches toBytesInt32)
182
+ if (targetThumbSize > 999999 || targetThumbSize < 0) {
183
+ targetThumbSize = dv.getInt32(16, true); // little-endian fallback for backward compatibility
184
+ }
185
+ const infoSize = dv.getInt32(20, false); // big-endian (default, matches toBytesInt32)
186
+ // Calculate offset to encrypted file data
187
+ const fileDataOffset = 16 + 4 + 4 + 32 + 256 + targetThumbSize + infoSize;
188
+ const encryptedFileData = buffer.slice(fileDataOffset);
189
+ return await crypto.decrypt({
190
+ name: 'AES-CBC',
191
+ iv: iv,
192
+ }, key, encryptedFileData);
193
+ }
194
+ /**
195
+ * Decrypt a file thumbnail
196
+ * Extracts and decrypts the thumbnail from the encrypted file structure
197
+ */
198
+ export async function decryptFileThumb(key, buffer) {
199
+ const crypto = getCryptoSubtle();
200
+ const dv = new DataView(buffer, 0);
201
+ const iv = buffer.slice(0, 16);
202
+ // Read thumb size (big-endian first to match toBytesInt32, then little-endian fallback)
203
+ let thumbSize = dv.getInt32(16, false); // big-endian (matches toBytesInt32 which uses false)
204
+ if (thumbSize > 999999 || thumbSize < 0) {
205
+ thumbSize = dv.getInt32(16, true); // little-endian fallback for backward compatibility
206
+ }
207
+ // If thumbSize is 0, return empty buffer
208
+ if (thumbSize === 0) {
209
+ return new ArrayBuffer(0);
210
+ }
211
+ // Encrypted thumb starts after: IV (16) + thumbSize (4) + infoSize (4) + thumbMime (32) + fileMime (256) = 312
212
+ const thumbOffset = 16 + 4 + 4 + 32 + 256;
213
+ const encryptedThumb = buffer.slice(thumbOffset, thumbOffset + thumbSize);
214
+ return await crypto.decrypt({
215
+ name: 'AES-CBC',
216
+ iv: iv,
217
+ }, key, encryptedThumb);
218
+ }
219
+ /**
220
+ * Encrypt a filename
221
+ * Returns base64url encoded encrypted filename
222
+ */
223
+ export async function encryptFilename(key, filename, iv) {
224
+ const binaryFilename = new TextEncoder().encode(filename);
225
+ const encFilename = await encryptBuffer(key, iv, binaryFilename);
226
+ return base64ToBase64Url(arrayBufferToBase64(encFilename));
227
+ }
228
+ /**
229
+ * Helper function to convert string to ArrayBuffer with fixed length
230
+ */
231
+ function stringToArrayBuffer(str, length) {
232
+ const buffer = new ArrayBuffer(length);
233
+ const view = new Uint8Array(buffer);
234
+ const encoder = new TextEncoder();
235
+ const encoded = encoder.encode(str);
236
+ view.set(encoded.slice(0, length));
237
+ return buffer;
238
+ }
@@ -0,0 +1,8 @@
1
+ export * from './client';
2
+ export * from './auth';
3
+ export * from './interfaces';
4
+ export * from './library';
5
+ export * from './server';
6
+ export * from './crypto';
7
+ export * from './encryption';
8
+ export * from './upload';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from './client';
2
+ export * from './auth';
3
+ export * from './interfaces';
4
+ export * from './library';
5
+ export * from './server';
6
+ export * from './crypto';
7
+ export * from './encryption';
8
+ export * from './upload';
@@ -0,0 +1,311 @@
1
+ export declare enum FileTypes {
2
+ directory = "directory",
3
+ photo = "photo",
4
+ video = "video",
5
+ archive = "archive",
6
+ album = "album",
7
+ channel = "channel",
8
+ other = "other"
9
+ }
10
+ export interface SerieInMedia {
11
+ id: string;
12
+ season?: number;
13
+ episode?: number;
14
+ episodeTo?: number;
15
+ }
16
+ export interface IFile {
17
+ id: string;
18
+ name: string;
19
+ description?: string;
20
+ size: number;
21
+ type: FileTypes;
22
+ mimetype: string;
23
+ modified: number;
24
+ created: number;
25
+ added: number;
26
+ starred?: any;
27
+ width: number;
28
+ height: number;
29
+ phash?: string;
30
+ thumbhash?: string;
31
+ acodecs?: string[];
32
+ achan?: number[];
33
+ vcodecs?: string[];
34
+ fps?: number;
35
+ bitrate?: number;
36
+ colorSpace?: string;
37
+ icc?: string;
38
+ mp?: number;
39
+ focal?: number;
40
+ iso?: number;
41
+ fNumber?: number;
42
+ sspeed?: string;
43
+ orientation?: number;
44
+ duration?: any;
45
+ progress?: number;
46
+ thumb?: any;
47
+ rating?: number;
48
+ tags?: {
49
+ id: string;
50
+ conf?: number;
51
+ }[];
52
+ people?: {
53
+ id: string;
54
+ conf?: number;
55
+ }[];
56
+ series?: SerieInMedia[];
57
+ movie?: string;
58
+ long?: number;
59
+ lat?: number;
60
+ params?: any;
61
+ iv?: string;
62
+ pages?: number;
63
+ thumbv?: number;
64
+ thumbsize?: number;
65
+ model?: string;
66
+ originalId?: string;
67
+ }
68
+ export declare enum LibraryTypes {
69
+ 'Photos' = "photos",
70
+ 'Shows' = "shows",
71
+ 'Movies' = "movies",
72
+ 'IPTV' = "iptv"
73
+ }
74
+ export declare enum LibrarySources {
75
+ 'Path' = "PathProvider",
76
+ 'Virtual' = "VirtualProvider",
77
+ 'Plugin' = "PluginProvider"
78
+ }
79
+ export declare enum LibraryRole {
80
+ none = "none",
81
+ display = "display",
82
+ read = "read",
83
+ write = "write",
84
+ share = "share",
85
+ admin = "admin"
86
+ }
87
+ export interface ILibrary {
88
+ id?: string;
89
+ name: string;
90
+ type: LibraryTypes;
91
+ source?: LibrarySources;
92
+ roles?: LibraryRole[];
93
+ crypt?: boolean;
94
+ hidden?: boolean;
95
+ status?: string;
96
+ }
97
+ export interface ITag {
98
+ id?: string;
99
+ name: string;
100
+ parent?: string;
101
+ path?: string;
102
+ count?: number;
103
+ starred?: boolean;
104
+ hidden?: boolean;
105
+ alt?: string[];
106
+ generated?: boolean;
107
+ waiting?: boolean;
108
+ }
109
+ export declare enum LinkType {
110
+ 'profile' = "profile",
111
+ 'post' = "post",
112
+ 'other' = "other"
113
+ }
114
+ export interface IPerson {
115
+ id?: string;
116
+ name: string;
117
+ alt?: string[];
118
+ socials?: {
119
+ id: string;
120
+ type: LinkType | undefined;
121
+ platform: string;
122
+ }[];
123
+ posterv?: number;
124
+ portrait?: boolean;
125
+ birthday?: number;
126
+ waiting?: boolean;
127
+ generated?: boolean;
128
+ }
129
+ export interface ISerie {
130
+ id?: string;
131
+ name: string;
132
+ alt?: string[];
133
+ tags?: string[];
134
+ people?: string[];
135
+ year?: number;
136
+ poster?: boolean;
137
+ imdb?: string;
138
+ slug?: string;
139
+ tmdb?: number;
140
+ trakt?: number;
141
+ tvdb?: number;
142
+ otherids?: string;
143
+ status?: string;
144
+ imdbRating?: number;
145
+ imdbVotes?: number;
146
+ images?: {
147
+ poster?: string;
148
+ background?: string;
149
+ };
150
+ posterv?: number;
151
+ backgroundv?: number;
152
+ cardv?: number;
153
+ waiting?: boolean;
154
+ }
155
+ export interface IEpisode {
156
+ id?: string;
157
+ serie: string;
158
+ name: string;
159
+ season?: number;
160
+ number?: number;
161
+ abs?: number;
162
+ overview?: string;
163
+ airdate?: number;
164
+ duration?: number;
165
+ type?: string;
166
+ alt?: string[];
167
+ imdb?: string;
168
+ slug?: string;
169
+ tmdb?: number;
170
+ trakt?: number;
171
+ tvdb?: number;
172
+ otherids?: string;
173
+ modified?: number;
174
+ watched?: number;
175
+ images?: {
176
+ poster?: string;
177
+ background?: string;
178
+ };
179
+ }
180
+ export interface IMovie {
181
+ id?: string;
182
+ name: string;
183
+ year?: number;
184
+ }
185
+ export interface IServer {
186
+ id: string;
187
+ name: string;
188
+ url: string;
189
+ port?: number;
190
+ }
191
+ export interface MediaRequest {
192
+ sort?: string;
193
+ order?: 'ASC' | 'DESC';
194
+ tags?: string[];
195
+ people?: string[];
196
+ series?: string[];
197
+ movies?: string[];
198
+ minSize?: number;
199
+ maxSize?: number;
200
+ pageKey?: string;
201
+ limit?: number;
202
+ }
203
+ export interface IBackup {
204
+ id: string;
205
+ name: string;
206
+ path: string;
207
+ password?: string;
208
+ library: string;
209
+ source: string;
210
+ plugin?: string;
211
+ credentials?: string;
212
+ credentialsName?: string;
213
+ schedule: string;
214
+ filter?: MediaRequest;
215
+ last?: number;
216
+ size?: number;
217
+ }
218
+ export interface IBackupFile {
219
+ backup: string;
220
+ library: string;
221
+ file: string;
222
+ id?: string;
223
+ path?: string;
224
+ hash?: string;
225
+ sourcehash?: string;
226
+ size?: number;
227
+ added: number;
228
+ modified: number;
229
+ iv?: string;
230
+ thumbsize?: number;
231
+ infoSize?: number;
232
+ error?: string;
233
+ }
234
+ export interface ExternalImage {
235
+ url: string;
236
+ type: string;
237
+ }
238
+ export interface IChannel {
239
+ id: string;
240
+ name: string;
241
+ series: string[];
242
+ lang?: string;
243
+ versions: IChannelVersion[];
244
+ type?: FileTypes;
245
+ }
246
+ export interface IChannelVersion {
247
+ id: string;
248
+ name: string;
249
+ source: string;
250
+ quality?: string;
251
+ description?: string;
252
+ }
253
+ export interface UnassignedFace {
254
+ id: string;
255
+ media: string;
256
+ box: any;
257
+ confidence: number;
258
+ }
259
+ export interface UnassignedFacesResponse {
260
+ faces: UnassignedFace[];
261
+ total: number;
262
+ }
263
+ export interface Cluster {
264
+ id: string;
265
+ faces: UnassignedFace[];
266
+ representativeFace: UnassignedFace;
267
+ }
268
+ export interface AssignFacesRequest {
269
+ faceIds: string[];
270
+ personId: string;
271
+ }
272
+ export interface AssignFacesResponse {
273
+ assigned: number;
274
+ }
275
+ export interface CreatePersonFromClusterRequest {
276
+ name?: string;
277
+ }
278
+ export interface CreatePersonFromClusterResponse {
279
+ person: IPerson;
280
+ }
281
+ export interface SplitClusterRequest {
282
+ faceIds?: string[];
283
+ }
284
+ export interface SplitClusterResponse {
285
+ newClusters: Cluster[];
286
+ }
287
+ export interface UnassignFaceRequest {
288
+ reason?: string;
289
+ }
290
+ export interface UnassignFaceResponse {
291
+ unassigned: number;
292
+ }
293
+ export interface DeleteFaceResponse {
294
+ deleted: number;
295
+ }
296
+ export interface AssignedFace extends UnassignedFace {
297
+ person: IPerson;
298
+ }
299
+ export interface MergePeopleRequest {
300
+ sourcePersonId: string;
301
+ targetPersonId: string;
302
+ }
303
+ export interface MergePeopleResponse {
304
+ merged: boolean;
305
+ }
306
+ export interface ClusterFacesResponse {
307
+ clusters: number;
308
+ }
309
+ export interface IChannelUpdate {
310
+ [key: string]: any;
311
+ }
@@ -0,0 +1,38 @@
1
+ export var FileTypes;
2
+ (function (FileTypes) {
3
+ FileTypes["directory"] = "directory";
4
+ FileTypes["photo"] = "photo";
5
+ FileTypes["video"] = "video";
6
+ FileTypes["archive"] = "archive";
7
+ FileTypes["album"] = "album";
8
+ FileTypes["channel"] = "channel";
9
+ FileTypes["other"] = "other";
10
+ })(FileTypes || (FileTypes = {}));
11
+ export var LibraryTypes;
12
+ (function (LibraryTypes) {
13
+ LibraryTypes["Photos"] = "photos";
14
+ LibraryTypes["Shows"] = "shows";
15
+ LibraryTypes["Movies"] = "movies";
16
+ LibraryTypes["IPTV"] = "iptv";
17
+ })(LibraryTypes || (LibraryTypes = {}));
18
+ export var LibrarySources;
19
+ (function (LibrarySources) {
20
+ LibrarySources["Path"] = "PathProvider";
21
+ LibrarySources["Virtual"] = "VirtualProvider";
22
+ LibrarySources["Plugin"] = "PluginProvider";
23
+ })(LibrarySources || (LibrarySources = {}));
24
+ export var LibraryRole;
25
+ (function (LibraryRole) {
26
+ LibraryRole["none"] = "none";
27
+ LibraryRole["display"] = "display";
28
+ LibraryRole["read"] = "read";
29
+ LibraryRole["write"] = "write";
30
+ LibraryRole["share"] = "share";
31
+ LibraryRole["admin"] = "admin";
32
+ })(LibraryRole || (LibraryRole = {}));
33
+ export var LinkType;
34
+ (function (LinkType) {
35
+ LinkType["profile"] = "profile";
36
+ LinkType["post"] = "post";
37
+ LinkType["other"] = "other";
38
+ })(LinkType || (LinkType = {}));