@majikah/majik-file 0.0.8 → 0.0.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.
- package/dist/core/types.d.ts +4 -0
- package/dist/majik-file.d.ts +24 -3
- package/dist/majik-file.js +69 -8
- package/package.json +1 -1
package/dist/core/types.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export interface MjkbSinglePayload {
|
|
|
53
53
|
n: string | null;
|
|
54
54
|
/** Original MIME type (e.g. "image/png"). Short key keeps the binary compact. */
|
|
55
55
|
m: string | null;
|
|
56
|
+
/** Usage context — determines downstream UX and access control. */
|
|
57
|
+
c: FileContext | null;
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
60
|
* JSON payload embedded in a group .mjkb binary.
|
|
@@ -66,6 +68,8 @@ export interface MjkbGroupPayload {
|
|
|
66
68
|
n: string | null;
|
|
67
69
|
/** Original MIME type (e.g. "image/png"). Short key keeps the binary compact. */
|
|
68
70
|
m: string | null;
|
|
71
|
+
/** Usage context — determines downstream UX and access control. */
|
|
72
|
+
c: FileContext | null;
|
|
69
73
|
}
|
|
70
74
|
export type MjkbPayload = MjkbSinglePayload | MjkbGroupPayload;
|
|
71
75
|
export declare function isMjkbGroupPayload(p: MjkbPayload): p is MjkbGroupPayload;
|
package/dist/majik-file.d.ts
CHANGED
|
@@ -50,21 +50,21 @@ import type { MajikFileJSON, CreateOptions, MajikFileIdentity, MajikFileStats, F
|
|
|
50
50
|
export declare class MajikFile {
|
|
51
51
|
private readonly _id;
|
|
52
52
|
private readonly _userId;
|
|
53
|
-
private
|
|
53
|
+
private _r2Key;
|
|
54
54
|
private readonly _originalName;
|
|
55
55
|
private readonly _mimeType;
|
|
56
56
|
private readonly _sizeOriginal;
|
|
57
57
|
private readonly _sizeStored;
|
|
58
58
|
private readonly _fileHash;
|
|
59
59
|
private readonly _encryptionIv;
|
|
60
|
-
private
|
|
60
|
+
private _storageType;
|
|
61
61
|
private _isShared;
|
|
62
62
|
private _shareToken;
|
|
63
63
|
private readonly _context;
|
|
64
64
|
private readonly _chatMessageId;
|
|
65
65
|
private readonly _threadMessageId;
|
|
66
66
|
private readonly _conversationId;
|
|
67
|
-
private
|
|
67
|
+
private _expiresAt;
|
|
68
68
|
private readonly _timestamp;
|
|
69
69
|
private _lastUpdate;
|
|
70
70
|
private readonly _isGroup;
|
|
@@ -171,6 +171,27 @@ export declare class MajikFile {
|
|
|
171
171
|
* @throws MajikFileError if _binary is not loaded or decryption fails.
|
|
172
172
|
*/
|
|
173
173
|
decryptBinary(identity: Pick<MajikFileIdentity, "fingerprint" | "mlKemSecretKey">): Promise<Uint8Array>;
|
|
174
|
+
/**
|
|
175
|
+
* Mutate the storage type in-place and rebuild the R2 key to match.
|
|
176
|
+
*
|
|
177
|
+
* This is intentionally a low-level escape hatch. Prefer the convenience
|
|
178
|
+
* wrappers `setPermanent()` and `setTemporary(days?)` which enforce the
|
|
179
|
+
* required invariants automatically.
|
|
180
|
+
*
|
|
181
|
+
* @throws MajikFileError when switching to temporary without an expiresAt,
|
|
182
|
+
* or if the instance has no userId / fileHash yet.
|
|
183
|
+
*/
|
|
184
|
+
setStorageType(type: StorageType, expiresAt: string | null): void;
|
|
185
|
+
/**
|
|
186
|
+
* Switch to permanent storage. Clears any expiry date and updates the R2 key.
|
|
187
|
+
*/
|
|
188
|
+
setPermanent(): void;
|
|
189
|
+
/**
|
|
190
|
+
* Switch to temporary storage with an optional TTL.
|
|
191
|
+
*
|
|
192
|
+
* @param days Days until expiry. Defaults to 15 to match R2 lifecycle policy.
|
|
193
|
+
*/
|
|
194
|
+
setTemporary(days?: number): void;
|
|
174
195
|
/**
|
|
175
196
|
* Serialise metadata to a plain object matching the `majik_files` Supabase table.
|
|
176
197
|
* The encrypted binary (_binary) is intentionally excluded.
|
package/dist/majik-file.js
CHANGED
|
@@ -214,7 +214,7 @@ export class MajikFile {
|
|
|
214
214
|
* @throws MajikFileError on validation or crypto failure
|
|
215
215
|
*/
|
|
216
216
|
static async create(options) {
|
|
217
|
-
const { data, identity, context, recipients = [], originalName = null, mimeType: rawMimeType = null, isTemporary = false, isShared = false, id = generateUUID(), bypassSizeLimit = false, expiresAt = null, chatMessageId = null, threadMessageId = null, conversationId = null, userId } = options;
|
|
217
|
+
const { data, identity, context, recipients = [], originalName = null, mimeType: rawMimeType = null, isTemporary = false, isShared = false, id = generateUUID(), bypassSizeLimit = false, expiresAt = null, chatMessageId = null, threadMessageId = null, conversationId = null, userId, } = options;
|
|
218
218
|
// ── Input validation ─────────────────────────────────────────────────
|
|
219
219
|
if (!data)
|
|
220
220
|
throw MajikFileError.invalidInput("data is required");
|
|
@@ -292,7 +292,9 @@ export class MajikFile {
|
|
|
292
292
|
// WebP and therefore skipped here — WebP is already codec-compressed.
|
|
293
293
|
// For user_upload and thread_attachment, compressible images (PNG, BMP,
|
|
294
294
|
// TIFF, SVG, etc.) are Zstd-compressed at level 22.
|
|
295
|
-
const compressible =
|
|
295
|
+
const compressible = context === "user_upload" || context === "thread_attachment"
|
|
296
|
+
? true
|
|
297
|
+
: shouldCompress(resolvedMimeType);
|
|
296
298
|
const compressed = compressible
|
|
297
299
|
? await MajikCompressor.compress(processedBytes)
|
|
298
300
|
: processedBytes;
|
|
@@ -317,6 +319,7 @@ export class MajikFile {
|
|
|
317
319
|
mlKemCipherText: arrayToBase64(mlKemCT),
|
|
318
320
|
n: originalName ?? null,
|
|
319
321
|
m: resolvedMimeType ?? null,
|
|
322
|
+
c: context ?? null,
|
|
320
323
|
};
|
|
321
324
|
}
|
|
322
325
|
else {
|
|
@@ -349,6 +352,7 @@ export class MajikFile {
|
|
|
349
352
|
keys,
|
|
350
353
|
n: originalName ?? null,
|
|
351
354
|
m: resolvedMimeType ?? null,
|
|
355
|
+
c: context ?? null,
|
|
352
356
|
};
|
|
353
357
|
}
|
|
354
358
|
// ── 6. Encode .mjkb ───────────────────────────────────────────────
|
|
@@ -451,11 +455,17 @@ export class MajikFile {
|
|
|
451
455
|
else {
|
|
452
456
|
throw MajikFileError.formatError(".mjkb payload JSON is neither a single nor group payload");
|
|
453
457
|
}
|
|
454
|
-
const
|
|
455
|
-
if (!
|
|
458
|
+
const decrypted = aesGcmDecrypt(aesKey, iv, ciphertext);
|
|
459
|
+
if (!decrypted) {
|
|
456
460
|
throw MajikFileError.decryptionFailed("Decryption failed — wrong key or corrupted .mjkb file");
|
|
457
461
|
}
|
|
458
|
-
|
|
462
|
+
const compressible = payload.c === "user_upload" || payload.c === "thread_attachment"
|
|
463
|
+
? true
|
|
464
|
+
: shouldCompress(payload.m);
|
|
465
|
+
const returnData = compressible
|
|
466
|
+
? await MajikCompressor.decompress(decrypted)
|
|
467
|
+
: decrypted;
|
|
468
|
+
return returnData;
|
|
459
469
|
}
|
|
460
470
|
catch (err) {
|
|
461
471
|
if (err instanceof MajikFileError)
|
|
@@ -509,11 +519,16 @@ export class MajikFile {
|
|
|
509
519
|
else {
|
|
510
520
|
throw MajikFileError.formatError(".mjkb payload JSON is neither a single nor group payload");
|
|
511
521
|
}
|
|
512
|
-
const
|
|
513
|
-
if (!
|
|
522
|
+
const decrypted = aesGcmDecrypt(aesKey, iv, ciphertext);
|
|
523
|
+
if (!decrypted) {
|
|
514
524
|
throw MajikFileError.decryptionFailed("Decryption failed — wrong key or corrupted .mjkb file");
|
|
515
525
|
}
|
|
516
|
-
const
|
|
526
|
+
const compressible = payload.c === "user_upload" || payload.c === "thread_attachment"
|
|
527
|
+
? true
|
|
528
|
+
: shouldCompress(payload.m);
|
|
529
|
+
const bytes = compressible
|
|
530
|
+
? await MajikCompressor.decompress(decrypted)
|
|
531
|
+
: decrypted;
|
|
517
532
|
// Extract original filename and MIME type from the payload.
|
|
518
533
|
// Written at encryption time as short keys n/m to keep the binary compact.
|
|
519
534
|
// Older .mjkb files without these fields return null — callers should fall
|
|
@@ -539,6 +554,52 @@ export class MajikFile {
|
|
|
539
554
|
throw MajikFileError.missingBinary();
|
|
540
555
|
return MajikFile.decrypt(this._binary, identity);
|
|
541
556
|
}
|
|
557
|
+
// ── STORAGE TYPE MUTATION ─────────────────────────────────────────────────
|
|
558
|
+
/**
|
|
559
|
+
* Mutate the storage type in-place and rebuild the R2 key to match.
|
|
560
|
+
*
|
|
561
|
+
* This is intentionally a low-level escape hatch. Prefer the convenience
|
|
562
|
+
* wrappers `setPermanent()` and `setTemporary(days?)` which enforce the
|
|
563
|
+
* required invariants automatically.
|
|
564
|
+
*
|
|
565
|
+
* @throws MajikFileError when switching to temporary without an expiresAt,
|
|
566
|
+
* or if the instance has no userId / fileHash yet.
|
|
567
|
+
*/
|
|
568
|
+
setStorageType(type, expiresAt) {
|
|
569
|
+
if (!["permanent", "temporary"].includes(type)) {
|
|
570
|
+
throw MajikFileError.invalidInput(`setStorageType: type must be "permanent" or "temporary" (got "${type}")`);
|
|
571
|
+
}
|
|
572
|
+
if (type === "temporary" && !expiresAt) {
|
|
573
|
+
throw MajikFileError.invalidInput("setStorageType: expiresAt is required when switching to temporary. " +
|
|
574
|
+
"Use setTemporary(days?) instead.");
|
|
575
|
+
}
|
|
576
|
+
// Rebuild R2 key — context is preserved, only the storage tier changes.
|
|
577
|
+
// chat_image files are context-scoped and must never be rekeyed here.
|
|
578
|
+
if (this._context === "chat_image") {
|
|
579
|
+
throw MajikFileError.invalidInput("setStorageType: chat_image files are conversation-scoped and cannot change storage type.");
|
|
580
|
+
}
|
|
581
|
+
const newR2Key = type === "temporary"
|
|
582
|
+
? buildTemporaryR2Key(this._userId, this._fileHash)
|
|
583
|
+
: buildPermanentR2Key(this._userId, this._fileHash);
|
|
584
|
+
this._storageType = type;
|
|
585
|
+
this._expiresAt = type === "temporary" ? expiresAt : null;
|
|
586
|
+
this._r2Key = newR2Key;
|
|
587
|
+
this._lastUpdate = new Date().toISOString();
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Switch to permanent storage. Clears any expiry date and updates the R2 key.
|
|
591
|
+
*/
|
|
592
|
+
setPermanent() {
|
|
593
|
+
this.setStorageType("permanent", null);
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Switch to temporary storage with an optional TTL.
|
|
597
|
+
*
|
|
598
|
+
* @param days Days until expiry. Defaults to 15 to match R2 lifecycle policy.
|
|
599
|
+
*/
|
|
600
|
+
setTemporary(days = 15) {
|
|
601
|
+
this.setStorageType("temporary", MajikFile.buildExpiryDate(days));
|
|
602
|
+
}
|
|
542
603
|
// ── SERIALISATION ─────────────────────────────────────────────────────────
|
|
543
604
|
/**
|
|
544
605
|
* Serialise metadata to a plain object matching the `majik_files` Supabase table.
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@majikah/majik-file",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "Majik File is the core cryptographic engine for secure file handling in the Majikah ecosystem. It provides a post-quantum secure \"MJKB\" format designed for file encryption, multi-recipient key encapsulation, and transparent compression using NIST-standardized algorithms.",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.10",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Zelijah",
|
|
8
8
|
"main": "./dist/index.js",
|