@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.
@@ -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;
@@ -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 readonly _r2Key;
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 readonly _storageType;
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 readonly _expiresAt;
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.
@@ -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 = shouldCompress(resolvedMimeType);
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 compressed = aesGcmDecrypt(aesKey, iv, ciphertext);
455
- if (!compressed) {
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
- return await MajikCompressor.decompress(compressed);
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 compressed = aesGcmDecrypt(aesKey, iv, ciphertext);
513
- if (!compressed) {
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 bytes = await MajikCompressor.decompress(compressed);
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.8",
5
+ "version": "0.0.10",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Zelijah",
8
8
  "main": "./dist/index.js",