openmates 0.11.0-alpha.8 → 0.11.0-alpha.9

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.
@@ -91,6 +91,16 @@ async function decryptWithAesGcmCombined(encryptedWithIvB64, rawKeyBytes) {
91
91
  return null;
92
92
  }
93
93
  }
94
+ async function deriveEmailEncryptionKeyB64(email, emailSaltB64) {
95
+ const encoder = new TextEncoder();
96
+ const emailBytes = encoder.encode(email);
97
+ const saltBytes = base64ToBytes(emailSaltB64);
98
+ const combined = new Uint8Array(emailBytes.length + saltBytes.length);
99
+ combined.set(emailBytes);
100
+ combined.set(saltBytes, emailBytes.length);
101
+ const hashBuffer = await cryptoApi.subtle.digest("SHA-256", toArrayBuffer(combined));
102
+ return bytesToBase64(new Uint8Array(hashBuffer));
103
+ }
94
104
  async function decryptBytesWithAesGcm(encryptedWithIvB64, rawKeyBytes) {
95
105
  try {
96
106
  const combined = base64ToBytes(encryptedWithIvB64);
@@ -554,6 +564,18 @@ function saveSession(session) {
554
564
  } else if (result.type === "plaintext") {
555
565
  onDisk.masterKeyExportedB64 = session.masterKeyExportedB64;
556
566
  }
567
+ if (session.emailEncryptionKeyB64) {
568
+ const emailKeyResult = storeMasterKey(
569
+ session.emailEncryptionKeyB64,
570
+ `${session.hashedEmail}:email`
571
+ );
572
+ onDisk.emailEncryptionKeyStorage = emailKeyResult.type;
573
+ if (emailKeyResult.type === "encrypted") {
574
+ onDisk.emailEncryptionKeyEncrypted = emailKeyResult.encryptedData;
575
+ } else if (emailKeyResult.type === "plaintext") {
576
+ onDisk.emailEncryptionKeyB64 = session.emailEncryptionKeyB64;
577
+ }
578
+ }
557
579
  writeJsonFile(filePath, onDisk);
558
580
  if (result.type !== "plaintext") {
559
581
  process.stderr.write("Decrypting data...\n");
@@ -564,17 +586,19 @@ function loadSession() {
564
586
  const onDisk = readJsonFile(filePath);
565
587
  if (!onDisk) return null;
566
588
  let masterKey = null;
589
+ let emailEncryptionKey = null;
567
590
  if (!onDisk.masterKeyStorage) {
568
591
  masterKey = onDisk.masterKeyExportedB64 ?? null;
569
592
  if (masterKey) {
570
- const session = buildSession(onDisk, masterKey);
593
+ emailEncryptionKey = getEmailEncryptionKeyFromDisk(onDisk);
594
+ const session = buildSession(onDisk, masterKey, emailEncryptionKey);
571
595
  try {
572
596
  saveSession(session);
573
597
  process.stderr.write("Decrypting data...\n");
574
598
  } catch {
575
599
  }
576
600
  }
577
- return masterKey ? buildSession(onDisk, masterKey) : null;
601
+ return masterKey ? buildSession(onDisk, masterKey, getEmailEncryptionKeyFromDisk(onDisk)) : null;
578
602
  }
579
603
  switch (onDisk.masterKeyStorage) {
580
604
  case "keychain":
@@ -598,7 +622,7 @@ function loadSession() {
598
622
  );
599
623
  return null;
600
624
  }
601
- return buildSession(onDisk, masterKey);
625
+ return buildSession(onDisk, masterKey, getEmailEncryptionKeyFromDisk(onDisk));
602
626
  }
603
627
  function clearSession() {
604
628
  const filePath = join(ensureStateDir(), "session.json");
@@ -606,17 +630,36 @@ function clearSession() {
606
630
  if (onDisk?.masterKeyStorage) {
607
631
  deleteMasterKey(onDisk.masterKeyStorage, onDisk.hashedEmail);
608
632
  }
633
+ if (onDisk?.emailEncryptionKeyStorage) {
634
+ deleteMasterKey(onDisk.emailEncryptionKeyStorage, `${onDisk.hashedEmail}:email`);
635
+ }
609
636
  if (existsSync2(filePath)) {
610
637
  rmSync(filePath);
611
638
  }
612
639
  }
613
- function buildSession(onDisk, masterKey) {
640
+ function getEmailEncryptionKeyFromDisk(onDisk) {
641
+ if (!onDisk.emailEncryptionKeyStorage) return onDisk.emailEncryptionKeyB64 ?? null;
642
+ switch (onDisk.emailEncryptionKeyStorage) {
643
+ case "keychain":
644
+ return retrieveMasterKey("keychain", `${onDisk.hashedEmail}:email`);
645
+ case "encrypted":
646
+ return retrieveMasterKey(
647
+ "encrypted",
648
+ `${onDisk.hashedEmail}:email`,
649
+ onDisk.emailEncryptionKeyEncrypted
650
+ );
651
+ case "plaintext":
652
+ return onDisk.emailEncryptionKeyB64 ?? null;
653
+ }
654
+ }
655
+ function buildSession(onDisk, masterKey, emailEncryptionKey) {
614
656
  return {
615
657
  apiUrl: onDisk.apiUrl,
616
658
  sessionId: onDisk.sessionId,
617
659
  wsToken: onDisk.wsToken,
618
660
  cookies: onDisk.cookies,
619
661
  masterKeyExportedB64: masterKey,
662
+ emailEncryptionKeyB64: emailEncryptionKey,
620
663
  hashedEmail: onDisk.hashedEmail,
621
664
  userEmailSalt: onDisk.userEmailSalt,
622
665
  createdAt: onDisk.createdAt,
@@ -1828,6 +1871,7 @@ var OpenMatesClient = class _OpenMatesClient {
1828
1871
  authorizerDeviceName: complete.data.authorizer_device_name ?? null,
1829
1872
  autoLogoutMinutes: complete.data.auto_logout_minutes ?? null
1830
1873
  };
1874
+ await this.hydrateEmailEncryptionKey(session);
1831
1875
  saveSession(session);
1832
1876
  }
1833
1877
  async whoAmI() {
@@ -2811,8 +2855,9 @@ var OpenMatesClient = class _OpenMatesClient {
2811
2855
  `CreditNote_${invoiceId}.pdf`
2812
2856
  );
2813
2857
  }
2814
- async requestRefund(invoiceId, emailEncryptionKey) {
2815
- this.requireSession();
2858
+ async requestRefund(invoiceId) {
2859
+ const session = this.requireSession();
2860
+ const emailEncryptionKey = await this.ensureEmailEncryptionKey(session);
2816
2861
  const response = await this.http.post(
2817
2862
  "/v1/payments/refund",
2818
2863
  { invoice_id: invoiceId, email_encryption_key: emailEncryptionKey },
@@ -3380,6 +3425,34 @@ Required: ${schema.required.join(", ")}`
3380
3425
  data: response.data
3381
3426
  };
3382
3427
  }
3428
+ async ensureEmailEncryptionKey(session) {
3429
+ if (session.emailEncryptionKeyB64) return session.emailEncryptionKeyB64;
3430
+ await this.hydrateEmailEncryptionKey(session);
3431
+ if (session.emailEncryptionKeyB64) return session.emailEncryptionKeyB64;
3432
+ throw new Error(
3433
+ "Email encryption key is missing. Run `openmates login` again to refresh your local encryption keys."
3434
+ );
3435
+ }
3436
+ async hydrateEmailEncryptionKey(session) {
3437
+ const response = await this.http.post(
3438
+ "/v1/auth/session",
3439
+ { session_id: session.sessionId },
3440
+ this.getCliRequestHeaders()
3441
+ );
3442
+ const encryptedEmail = response.data.user?.encrypted_email_with_master_key;
3443
+ if (!response.ok || !response.data.success || typeof encryptedEmail !== "string") {
3444
+ return;
3445
+ }
3446
+ const email = await decryptWithAesGcmCombined(
3447
+ encryptedEmail,
3448
+ base64ToBytes(session.masterKeyExportedB64)
3449
+ );
3450
+ if (!email) return;
3451
+ session.emailEncryptionKeyB64 = await deriveEmailEncryptionKeyB64(
3452
+ email,
3453
+ session.userEmailSalt
3454
+ );
3455
+ }
3383
3456
  requireSession() {
3384
3457
  if (!this.session) {
3385
3458
  throw new Error("Not logged in. Run `openmates login`.");
@@ -7761,7 +7834,7 @@ var SETTINGS_EXECUTABLE_COMMANDS = [
7761
7834
  { path: ["billing", "invoices", "list"], description: "List invoices", examples: ["openmates settings billing invoices list --json"] },
7762
7835
  { path: ["billing", "invoices", "download"], description: "Download an invoice PDF", examples: ["openmates settings billing invoices download <invoice-id> --output ./invoices"] },
7763
7836
  { path: ["billing", "invoices", "credit-note"], description: "Download a credit note PDF", examples: ["openmates settings billing invoices credit-note <invoice-id> --output ./invoices"] },
7764
- { path: ["billing", "invoices", "refund"], description: "Request a refund for an invoice", examples: ["openmates settings billing invoices refund <invoice-id> --email-encryption-key <base64>"] },
7837
+ { path: ["billing", "invoices", "refund"], description: "Request a refund for an invoice", examples: ["openmates settings billing invoices refund <invoice-id> --yes"] },
7765
7838
  { path: ["billing", "gift-card", "redeem"], description: "Redeem a gift card", examples: ["openmates settings billing gift-card redeem ABCD-1234"] },
7766
7839
  { path: ["billing", "gift-card", "list"], description: "List redeemed gift cards", examples: ["openmates settings billing gift-card list"] },
7767
7840
  { path: ["billing", "auto-topup", "low-balance", "set"], description: "Configure low-balance auto top-up", examples: ["openmates settings billing auto-topup low-balance set --enabled true --amount 1000 --currency eur --email you@example.com"] },
@@ -8196,11 +8269,9 @@ async function handleSettings(client, subcommand, rest, flags) {
8196
8269
  }
8197
8270
  if (matches(tokens, ["billing", "invoices", "refund"])) {
8198
8271
  const invoiceId = rest[2];
8199
- const emailEncryptionKey = typeof flags["email-encryption-key"] === "string" ? flags["email-encryption-key"] : void 0;
8200
8272
  if (!invoiceId) throw new Error("Missing invoice ID.");
8201
- if (!emailEncryptionKey) throw new Error("Missing --email-encryption-key. The backend requires the encrypted email key for refund requests.");
8202
8273
  if (flags.yes !== true) await confirmOrExit(`Request refund for invoice ${invoiceId}? [y/N] `);
8203
- await printSettingsMutationResult(client.requestRefund(invoiceId, emailEncryptionKey), flags);
8274
+ await printSettingsMutationResult(client.requestRefund(invoiceId), flags);
8204
8275
  return;
8205
8276
  }
8206
8277
  if (matches(tokens, ["billing", "gift-card", "redeem"]) || subcommand === "gift-card" && rest[0] === "redeem") {
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getExtForLang,
4
4
  serializeToYaml
5
- } from "./chunk-2YHQ7NP4.js";
5
+ } from "./chunk-QJE5DL63.js";
6
6
  import "./chunk-SFTCIVE2.js";
7
7
  export {
8
8
  getExtForLang,
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ interface OpenMatesSession {
7
7
  wsToken: string | null;
8
8
  cookies: Record<string, string>;
9
9
  masterKeyExportedB64: string;
10
+ emailEncryptionKeyB64?: string | null;
10
11
  hashedEmail: string;
11
12
  userEmailSalt: string;
12
13
  createdAt: number;
@@ -489,7 +490,7 @@ declare class OpenMatesClient {
489
490
  }>;
490
491
  downloadInvoice(invoiceId: string): Promise<DownloadedDocument>;
491
492
  downloadCreditNote(invoiceId: string): Promise<DownloadedDocument>;
492
- requestRefund(invoiceId: string, emailEncryptionKey: string): Promise<unknown>;
493
+ requestRefund(invoiceId: string): Promise<unknown>;
493
494
  updateUsername(username: string): Promise<unknown>;
494
495
  updateProfileImage(filePath: string): Promise<unknown>;
495
496
  getNewsletterCategories(): Promise<unknown>;
@@ -637,6 +638,8 @@ declare class OpenMatesClient {
637
638
  buildMentionContext(): Promise<MentionContext>;
638
639
  private normalizePath;
639
640
  private downloadPaymentPdf;
641
+ private ensureEmailEncryptionKey;
642
+ private hydrateEmailEncryptionKey;
640
643
  private requireSession;
641
644
  private getMasterKeyBytes;
642
645
  private getValidSessionFromDisk;
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  getExtForLang,
7
7
  parseNewChatSuggestionText,
8
8
  serializeToYaml
9
- } from "./chunk-2YHQ7NP4.js";
9
+ } from "./chunk-QJE5DL63.js";
10
10
  import "./chunk-SFTCIVE2.js";
11
11
  export {
12
12
  MATE_NAMES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmates",
3
- "version": "0.11.0-alpha.8",
3
+ "version": "0.11.0-alpha.9",
4
4
  "description": "OpenMates CLI and SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",