openmates 0.11.0-alpha.7 → 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.
- package/dist/{chunk-T77MVTFX.js → chunk-QJE5DL63.js} +423 -105
- package/dist/chunk-SFTCIVE2.js +131 -0
- package/dist/cli.js +2 -1
- package/dist/index.d.ts +40 -0
- package/dist/index.js +2 -1
- package/dist/uploadService-3CAJXU4L.js +8 -0
- package/package.json +1 -1
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
uploadFile
|
|
3
|
+
} from "./chunk-SFTCIVE2.js";
|
|
4
|
+
|
|
1
5
|
// src/client.ts
|
|
2
6
|
import { randomUUID } from "crypto";
|
|
3
7
|
import { platform as platform2, release } from "os";
|
|
@@ -87,6 +91,16 @@ async function decryptWithAesGcmCombined(encryptedWithIvB64, rawKeyBytes) {
|
|
|
87
91
|
return null;
|
|
88
92
|
}
|
|
89
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
|
+
}
|
|
90
104
|
async function decryptBytesWithAesGcm(encryptedWithIvB64, rawKeyBytes) {
|
|
91
105
|
try {
|
|
92
106
|
const combined = base64ToBytes(encryptedWithIvB64);
|
|
@@ -186,6 +200,23 @@ var OpenMatesHttpClient = class {
|
|
|
186
200
|
async patch(path, body, headers = {}) {
|
|
187
201
|
return this.request("PATCH", path, body, headers);
|
|
188
202
|
}
|
|
203
|
+
async getBinary(path, headers = {}) {
|
|
204
|
+
const url = `${this.apiUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
205
|
+
const requestHeaders = {
|
|
206
|
+
Accept: "application/pdf,application/octet-stream",
|
|
207
|
+
...headers
|
|
208
|
+
};
|
|
209
|
+
const cookieHeader = this.formatCookieHeader();
|
|
210
|
+
if (cookieHeader) requestHeaders.Cookie = cookieHeader;
|
|
211
|
+
const response = await fetch(url, { method: "GET", headers: requestHeaders });
|
|
212
|
+
this.captureCookies(response);
|
|
213
|
+
return {
|
|
214
|
+
ok: response.ok,
|
|
215
|
+
status: response.status,
|
|
216
|
+
data: new Uint8Array(await response.arrayBuffer()),
|
|
217
|
+
headers: response.headers
|
|
218
|
+
};
|
|
219
|
+
}
|
|
189
220
|
async request(method, path, body, headers = {}) {
|
|
190
221
|
const url = `${this.apiUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
191
222
|
const requestHeaders = {
|
|
@@ -533,6 +564,18 @@ function saveSession(session) {
|
|
|
533
564
|
} else if (result.type === "plaintext") {
|
|
534
565
|
onDisk.masterKeyExportedB64 = session.masterKeyExportedB64;
|
|
535
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
|
+
}
|
|
536
579
|
writeJsonFile(filePath, onDisk);
|
|
537
580
|
if (result.type !== "plaintext") {
|
|
538
581
|
process.stderr.write("Decrypting data...\n");
|
|
@@ -543,17 +586,19 @@ function loadSession() {
|
|
|
543
586
|
const onDisk = readJsonFile(filePath);
|
|
544
587
|
if (!onDisk) return null;
|
|
545
588
|
let masterKey = null;
|
|
589
|
+
let emailEncryptionKey = null;
|
|
546
590
|
if (!onDisk.masterKeyStorage) {
|
|
547
591
|
masterKey = onDisk.masterKeyExportedB64 ?? null;
|
|
548
592
|
if (masterKey) {
|
|
549
|
-
|
|
593
|
+
emailEncryptionKey = getEmailEncryptionKeyFromDisk(onDisk);
|
|
594
|
+
const session = buildSession(onDisk, masterKey, emailEncryptionKey);
|
|
550
595
|
try {
|
|
551
596
|
saveSession(session);
|
|
552
597
|
process.stderr.write("Decrypting data...\n");
|
|
553
598
|
} catch {
|
|
554
599
|
}
|
|
555
600
|
}
|
|
556
|
-
return masterKey ? buildSession(onDisk, masterKey) : null;
|
|
601
|
+
return masterKey ? buildSession(onDisk, masterKey, getEmailEncryptionKeyFromDisk(onDisk)) : null;
|
|
557
602
|
}
|
|
558
603
|
switch (onDisk.masterKeyStorage) {
|
|
559
604
|
case "keychain":
|
|
@@ -577,7 +622,7 @@ function loadSession() {
|
|
|
577
622
|
);
|
|
578
623
|
return null;
|
|
579
624
|
}
|
|
580
|
-
return buildSession(onDisk, masterKey);
|
|
625
|
+
return buildSession(onDisk, masterKey, getEmailEncryptionKeyFromDisk(onDisk));
|
|
581
626
|
}
|
|
582
627
|
function clearSession() {
|
|
583
628
|
const filePath = join(ensureStateDir(), "session.json");
|
|
@@ -585,17 +630,36 @@ function clearSession() {
|
|
|
585
630
|
if (onDisk?.masterKeyStorage) {
|
|
586
631
|
deleteMasterKey(onDisk.masterKeyStorage, onDisk.hashedEmail);
|
|
587
632
|
}
|
|
633
|
+
if (onDisk?.emailEncryptionKeyStorage) {
|
|
634
|
+
deleteMasterKey(onDisk.emailEncryptionKeyStorage, `${onDisk.hashedEmail}:email`);
|
|
635
|
+
}
|
|
588
636
|
if (existsSync2(filePath)) {
|
|
589
637
|
rmSync(filePath);
|
|
590
638
|
}
|
|
591
639
|
}
|
|
592
|
-
function
|
|
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) {
|
|
593
656
|
return {
|
|
594
657
|
apiUrl: onDisk.apiUrl,
|
|
595
658
|
sessionId: onDisk.sessionId,
|
|
596
659
|
wsToken: onDisk.wsToken,
|
|
597
660
|
cookies: onDisk.cookies,
|
|
598
661
|
masterKeyExportedB64: masterKey,
|
|
662
|
+
emailEncryptionKeyB64: emailEncryptionKey,
|
|
599
663
|
hashedEmail: onDisk.hashedEmail,
|
|
600
664
|
userEmailSalt: onDisk.userEmailSalt,
|
|
601
665
|
createdAt: onDisk.createdAt,
|
|
@@ -1807,6 +1871,7 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
1807
1871
|
authorizerDeviceName: complete.data.authorizer_device_name ?? null,
|
|
1808
1872
|
autoLogoutMinutes: complete.data.auto_logout_minutes ?? null
|
|
1809
1873
|
};
|
|
1874
|
+
await this.hydrateEmailEncryptionKey(session);
|
|
1810
1875
|
saveSession(session);
|
|
1811
1876
|
}
|
|
1812
1877
|
async whoAmI() {
|
|
@@ -2767,6 +2832,124 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
2767
2832
|
}
|
|
2768
2833
|
return response.data;
|
|
2769
2834
|
}
|
|
2835
|
+
async listInvoices() {
|
|
2836
|
+
this.requireSession();
|
|
2837
|
+
const response = await this.http.get(
|
|
2838
|
+
"/v1/payments/invoices",
|
|
2839
|
+
this.getCliRequestHeaders()
|
|
2840
|
+
);
|
|
2841
|
+
if (!response.ok) {
|
|
2842
|
+
throw new Error(`Failed to fetch invoices (HTTP ${response.status})`);
|
|
2843
|
+
}
|
|
2844
|
+
return { invoices: response.data.invoices ?? [] };
|
|
2845
|
+
}
|
|
2846
|
+
async downloadInvoice(invoiceId) {
|
|
2847
|
+
return this.downloadPaymentPdf(
|
|
2848
|
+
`/v1/payments/invoices/${encodeURIComponent(invoiceId)}/download`,
|
|
2849
|
+
`Invoice_${invoiceId}.pdf`
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2852
|
+
async downloadCreditNote(invoiceId) {
|
|
2853
|
+
return this.downloadPaymentPdf(
|
|
2854
|
+
`/v1/payments/invoices/${encodeURIComponent(invoiceId)}/credit-note/download`,
|
|
2855
|
+
`CreditNote_${invoiceId}.pdf`
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
async requestRefund(invoiceId) {
|
|
2859
|
+
const session = this.requireSession();
|
|
2860
|
+
const emailEncryptionKey = await this.ensureEmailEncryptionKey(session);
|
|
2861
|
+
const response = await this.http.post(
|
|
2862
|
+
"/v1/payments/refund",
|
|
2863
|
+
{ invoice_id: invoiceId, email_encryption_key: emailEncryptionKey },
|
|
2864
|
+
this.getCliRequestHeaders()
|
|
2865
|
+
);
|
|
2866
|
+
if (!response.ok) {
|
|
2867
|
+
throw new Error(`Refund request failed (HTTP ${response.status})`);
|
|
2868
|
+
}
|
|
2869
|
+
return response.data;
|
|
2870
|
+
}
|
|
2871
|
+
async updateUsername(username) {
|
|
2872
|
+
return this.settingsPost("user/username", { username });
|
|
2873
|
+
}
|
|
2874
|
+
async updateProfileImage(filePath) {
|
|
2875
|
+
const { uploadProfileImage } = await import("./uploadService-3CAJXU4L.js");
|
|
2876
|
+
const result = await uploadProfileImage(filePath, this.requireSession());
|
|
2877
|
+
if (result.status === "rejected") {
|
|
2878
|
+
throw new Error(result.detail ?? "Profile image rejected by content safety checks.");
|
|
2879
|
+
}
|
|
2880
|
+
if (result.status === "account_deleted") {
|
|
2881
|
+
throw new Error("Account deleted due to repeated profile image policy violations.");
|
|
2882
|
+
}
|
|
2883
|
+
if (result.status !== "ok") {
|
|
2884
|
+
throw new Error(result.detail ?? `Profile image upload failed with status '${result.status}'.`);
|
|
2885
|
+
}
|
|
2886
|
+
return result;
|
|
2887
|
+
}
|
|
2888
|
+
async getNewsletterCategories() {
|
|
2889
|
+
this.requireSession();
|
|
2890
|
+
const response = await this.http.get(
|
|
2891
|
+
"/v1/newsletter/categories",
|
|
2892
|
+
this.getCliRequestHeaders()
|
|
2893
|
+
);
|
|
2894
|
+
if (!response.ok) {
|
|
2895
|
+
throw new Error(`Failed to fetch newsletter categories (HTTP ${response.status})`);
|
|
2896
|
+
}
|
|
2897
|
+
return response.data;
|
|
2898
|
+
}
|
|
2899
|
+
async updateNewsletterCategories(categories) {
|
|
2900
|
+
this.requireSession();
|
|
2901
|
+
const response = await this.http.patch(
|
|
2902
|
+
"/v1/newsletter/categories",
|
|
2903
|
+
{ categories },
|
|
2904
|
+
this.getCliRequestHeaders()
|
|
2905
|
+
);
|
|
2906
|
+
if (!response.ok) {
|
|
2907
|
+
throw new Error(`Failed to update newsletter categories (HTTP ${response.status})`);
|
|
2908
|
+
}
|
|
2909
|
+
return response.data;
|
|
2910
|
+
}
|
|
2911
|
+
async subscribeNewsletter(email, language = "en", darkmode = false) {
|
|
2912
|
+
const response = await this.http.post(
|
|
2913
|
+
"/v1/newsletter/subscribe",
|
|
2914
|
+
{ email, language, darkmode },
|
|
2915
|
+
this.getCliRequestHeaders()
|
|
2916
|
+
);
|
|
2917
|
+
if (!response.ok) {
|
|
2918
|
+
throw new Error(`Newsletter subscribe failed (HTTP ${response.status})`);
|
|
2919
|
+
}
|
|
2920
|
+
return response.data;
|
|
2921
|
+
}
|
|
2922
|
+
async confirmNewsletter(token) {
|
|
2923
|
+
const response = await this.http.get(
|
|
2924
|
+
`/v1/newsletter/confirm/${encodeURIComponent(token)}`,
|
|
2925
|
+
this.getCliRequestHeaders()
|
|
2926
|
+
);
|
|
2927
|
+
if (!response.ok) {
|
|
2928
|
+
throw new Error(`Newsletter confirmation failed (HTTP ${response.status})`);
|
|
2929
|
+
}
|
|
2930
|
+
return response.data;
|
|
2931
|
+
}
|
|
2932
|
+
async unsubscribeNewsletter(token) {
|
|
2933
|
+
const response = await this.http.get(
|
|
2934
|
+
`/v1/newsletter/unsubscribe/${encodeURIComponent(token)}`,
|
|
2935
|
+
this.getCliRequestHeaders()
|
|
2936
|
+
);
|
|
2937
|
+
if (!response.ok) {
|
|
2938
|
+
throw new Error(`Newsletter unsubscribe failed (HTTP ${response.status})`);
|
|
2939
|
+
}
|
|
2940
|
+
return response.data;
|
|
2941
|
+
}
|
|
2942
|
+
async updateEmailNotificationSettings(payload) {
|
|
2943
|
+
const { ws } = await this.openWsClient();
|
|
2944
|
+
try {
|
|
2945
|
+
const ackPromise = ws.waitForMessage("email_notification_settings_ack");
|
|
2946
|
+
ws.send("email_notification_settings", payload);
|
|
2947
|
+
const ack = await ackPromise;
|
|
2948
|
+
return ack.payload;
|
|
2949
|
+
} finally {
|
|
2950
|
+
ws.close();
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2770
2953
|
// -------------------------------------------------------------------------
|
|
2771
2954
|
// Daily Inspirations
|
|
2772
2955
|
// -------------------------------------------------------------------------
|
|
@@ -3231,6 +3414,45 @@ Required: ${schema.required.join(", ")}`
|
|
|
3231
3414
|
if (path.startsWith("/")) return path;
|
|
3232
3415
|
return `/v1/settings/${path}`;
|
|
3233
3416
|
}
|
|
3417
|
+
async downloadPaymentPdf(path, fallbackFilename) {
|
|
3418
|
+
this.requireSession();
|
|
3419
|
+
const response = await this.http.getBinary(path, this.getCliRequestHeaders());
|
|
3420
|
+
if (!response.ok) {
|
|
3421
|
+
throw new Error(`Download failed (HTTP ${response.status})`);
|
|
3422
|
+
}
|
|
3423
|
+
return {
|
|
3424
|
+
filename: filenameFromContentDisposition(response.headers.get("content-disposition")) ?? fallbackFilename,
|
|
3425
|
+
data: response.data
|
|
3426
|
+
};
|
|
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
|
+
}
|
|
3234
3456
|
requireSession() {
|
|
3235
3457
|
if (!this.session) {
|
|
3236
3458
|
throw new Error("Not logged in. Run `openmates login`.");
|
|
@@ -3606,6 +3828,15 @@ function parseNewChatSuggestionText(text) {
|
|
|
3606
3828
|
const skillId = raw.slice(dashIdx + 1);
|
|
3607
3829
|
return { body, appId, skillId };
|
|
3608
3830
|
}
|
|
3831
|
+
function filenameFromContentDisposition(header2) {
|
|
3832
|
+
if (!header2) return null;
|
|
3833
|
+
const encoded = /filename\*=UTF-8''([^;]+)/i.exec(header2)?.[1];
|
|
3834
|
+
if (encoded) return decodeURIComponent(encoded);
|
|
3835
|
+
const quoted = /filename="([^"]+)"/i.exec(header2)?.[1];
|
|
3836
|
+
if (quoted) return quoted;
|
|
3837
|
+
const plain = /filename=([^;]+)/i.exec(header2)?.[1];
|
|
3838
|
+
return plain?.trim() ?? null;
|
|
3839
|
+
}
|
|
3609
3840
|
function sleep(ms) {
|
|
3610
3841
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
3611
3842
|
}
|
|
@@ -4819,97 +5050,6 @@ function formatEmbedsForMessage(embeds) {
|
|
|
4819
5050
|
return "\n" + embeds.map((e) => e.referenceBlock).join("\n");
|
|
4820
5051
|
}
|
|
4821
5052
|
|
|
4822
|
-
// src/uploadService.ts
|
|
4823
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
4824
|
-
import { basename as basename2 } from "path";
|
|
4825
|
-
var UPLOAD_MAX_ATTEMPTS = 3;
|
|
4826
|
-
var UPLOAD_RETRY_DELAY_MS = 2e3;
|
|
4827
|
-
function getUploadUrl(apiUrl) {
|
|
4828
|
-
try {
|
|
4829
|
-
const url = new URL(apiUrl);
|
|
4830
|
-
if (url.hostname === "localhost") return "http://localhost:8001";
|
|
4831
|
-
} catch {
|
|
4832
|
-
}
|
|
4833
|
-
return "https://upload.openmates.org";
|
|
4834
|
-
}
|
|
4835
|
-
function getUploadOrigin(apiUrl) {
|
|
4836
|
-
try {
|
|
4837
|
-
const url = new URL(apiUrl);
|
|
4838
|
-
if (url.hostname === "localhost") return "http://localhost:5173";
|
|
4839
|
-
if (url.hostname.startsWith("api.")) {
|
|
4840
|
-
return `${url.protocol}//app.${url.hostname.slice(4)}`;
|
|
4841
|
-
}
|
|
4842
|
-
} catch {
|
|
4843
|
-
}
|
|
4844
|
-
return "https://app.openmates.org";
|
|
4845
|
-
}
|
|
4846
|
-
async function uploadFile(filePath, session) {
|
|
4847
|
-
const filename = basename2(filePath);
|
|
4848
|
-
const fileBytes = readFileSync4(filePath);
|
|
4849
|
-
const uploadUrl = `${getUploadUrl(session.apiUrl)}/v1/upload/file`;
|
|
4850
|
-
const origin = getUploadOrigin(session.apiUrl);
|
|
4851
|
-
const cookies = [];
|
|
4852
|
-
if (session.cookies?.auth_refresh_token) {
|
|
4853
|
-
cookies.push(`auth_refresh_token=${session.cookies.auth_refresh_token}`);
|
|
4854
|
-
}
|
|
4855
|
-
let response;
|
|
4856
|
-
let lastError;
|
|
4857
|
-
for (let attempt = 1; attempt <= UPLOAD_MAX_ATTEMPTS; attempt++) {
|
|
4858
|
-
try {
|
|
4859
|
-
const blob = new Blob([fileBytes]);
|
|
4860
|
-
const formData = new FormData();
|
|
4861
|
-
formData.append("file", blob, filename);
|
|
4862
|
-
response = await fetch(uploadUrl, {
|
|
4863
|
-
method: "POST",
|
|
4864
|
-
body: formData,
|
|
4865
|
-
headers: {
|
|
4866
|
-
Origin: origin,
|
|
4867
|
-
...cookies.length > 0 ? { Cookie: cookies.join("; ") } : {}
|
|
4868
|
-
},
|
|
4869
|
-
signal: AbortSignal.timeout(10 * 60 * 1e3)
|
|
4870
|
-
// 10-minute timeout
|
|
4871
|
-
});
|
|
4872
|
-
break;
|
|
4873
|
-
} catch (error) {
|
|
4874
|
-
lastError = error;
|
|
4875
|
-
if (attempt === UPLOAD_MAX_ATTEMPTS) break;
|
|
4876
|
-
await new Promise((resolve4) => setTimeout(resolve4, UPLOAD_RETRY_DELAY_MS));
|
|
4877
|
-
}
|
|
4878
|
-
}
|
|
4879
|
-
if (!response) {
|
|
4880
|
-
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
4881
|
-
throw new Error(message || "Upload request failed.");
|
|
4882
|
-
}
|
|
4883
|
-
if (!response.ok) {
|
|
4884
|
-
const status = response.status;
|
|
4885
|
-
let errorMessage;
|
|
4886
|
-
switch (status) {
|
|
4887
|
-
case 401:
|
|
4888
|
-
errorMessage = "Authentication failed. Run `openmates login` to re-authenticate.";
|
|
4889
|
-
break;
|
|
4890
|
-
case 413:
|
|
4891
|
-
errorMessage = "File too large (maximum 100 MB).";
|
|
4892
|
-
break;
|
|
4893
|
-
case 415:
|
|
4894
|
-
errorMessage = "Unsupported file type.";
|
|
4895
|
-
break;
|
|
4896
|
-
case 422: {
|
|
4897
|
-
const body = await response.text().catch(() => "");
|
|
4898
|
-
errorMessage = body.includes("malware") ? "File rejected: malware detected." : body.includes("content_safety") ? "File rejected: content safety violation." : `Upload validation failed: ${body}`;
|
|
4899
|
-
break;
|
|
4900
|
-
}
|
|
4901
|
-
case 429:
|
|
4902
|
-
errorMessage = "Upload rate limit exceeded. Try again in a minute.";
|
|
4903
|
-
break;
|
|
4904
|
-
default:
|
|
4905
|
-
errorMessage = `Upload failed (HTTP ${status}).`;
|
|
4906
|
-
}
|
|
4907
|
-
throw new Error(errorMessage);
|
|
4908
|
-
}
|
|
4909
|
-
const data = await response.json();
|
|
4910
|
-
return data;
|
|
4911
|
-
}
|
|
4912
|
-
|
|
4913
5053
|
// src/embedRenderers.ts
|
|
4914
5054
|
var str = (v) => typeof v === "string" && v.length > 0 ? v : null;
|
|
4915
5055
|
var DIRECT_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -5959,13 +6099,13 @@ function formatTs(ts) {
|
|
|
5959
6099
|
|
|
5960
6100
|
// src/server.ts
|
|
5961
6101
|
import { execSync, spawn as nodeSpawn } from "child_process";
|
|
5962
|
-
import { copyFileSync, existsSync as existsSync5, readFileSync as
|
|
6102
|
+
import { copyFileSync, existsSync as existsSync5, readFileSync as readFileSync5, rmSync as rmSync3 } from "fs";
|
|
5963
6103
|
import { createInterface as createInterface2 } from "readline";
|
|
5964
6104
|
import { homedir as homedir5 } from "os";
|
|
5965
6105
|
import { join as join3, resolve as resolve3 } from "path";
|
|
5966
6106
|
|
|
5967
6107
|
// src/serverConfig.ts
|
|
5968
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as
|
|
6108
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
5969
6109
|
import { homedir as homedir4 } from "os";
|
|
5970
6110
|
import { join as join2, resolve as resolve2 } from "path";
|
|
5971
6111
|
var STATE_DIR = join2(homedir4(), ".openmates");
|
|
@@ -5986,7 +6126,7 @@ function loadServerConfig() {
|
|
|
5986
6126
|
const filePath = join2(STATE_DIR, CONFIG_FILE);
|
|
5987
6127
|
if (!existsSync4(filePath)) return null;
|
|
5988
6128
|
try {
|
|
5989
|
-
return JSON.parse(
|
|
6129
|
+
return JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
5990
6130
|
} catch {
|
|
5991
6131
|
return null;
|
|
5992
6132
|
}
|
|
@@ -6064,7 +6204,7 @@ function requireGit() {
|
|
|
6064
6204
|
}
|
|
6065
6205
|
function hasLlmCredentials(envPath) {
|
|
6066
6206
|
if (!existsSync5(envPath)) return false;
|
|
6067
|
-
const content =
|
|
6207
|
+
const content = readFileSync5(envPath, "utf-8");
|
|
6068
6208
|
for (const line of content.split("\n")) {
|
|
6069
6209
|
const trimmed = line.trim();
|
|
6070
6210
|
if (trimmed.startsWith("#") || !trimmed) continue;
|
|
@@ -7673,6 +7813,8 @@ var SETTINGS_EXECUTABLE_COMMANDS = [
|
|
|
7673
7813
|
{ path: ["account", "export", "manifest"], description: "Show account export manifest", examples: ["openmates settings account export manifest --json"] },
|
|
7674
7814
|
{ path: ["account", "export", "data"], description: "Fetch account export data", examples: ["openmates settings account export data --json"] },
|
|
7675
7815
|
{ path: ["account", "import-chat"], description: "Import a CLI chat export file", examples: ["openmates settings account import-chat ./chat.yml", "openmates settings account import-chat ./payload.json"] },
|
|
7816
|
+
{ path: ["account", "username", "set"], description: "Change account username", examples: ["openmates settings account username set alice_123"] },
|
|
7817
|
+
{ path: ["account", "profile-picture", "set"], description: "Upload a profile picture", examples: ["openmates settings account profile-picture set ./avatar.jpg"] },
|
|
7676
7818
|
{ path: ["account", "chats", "stats"], description: "Show chat statistics", examples: ["openmates settings account chats stats"] },
|
|
7677
7819
|
{ path: ["account", "delete", "preview"], description: "Preview account deletion impact", examples: ["openmates settings account delete preview"] },
|
|
7678
7820
|
{ path: ["account", "storage", "overview"], description: "Show storage overview", examples: ["openmates settings account storage overview"] },
|
|
@@ -7689,9 +7831,16 @@ var SETTINGS_EXECUTABLE_COMMANDS = [
|
|
|
7689
7831
|
{ path: ["billing", "usage", "summaries"], description: "Show usage summaries", examples: ["openmates settings billing usage summaries"] },
|
|
7690
7832
|
{ path: ["billing", "usage", "daily"], description: "Show daily usage overview", examples: ["openmates settings billing usage daily"] },
|
|
7691
7833
|
{ path: ["billing", "usage", "export"], description: "Export usage data", examples: ["openmates settings billing usage export --json"] },
|
|
7834
|
+
{ path: ["billing", "invoices", "list"], description: "List invoices", examples: ["openmates settings billing invoices list --json"] },
|
|
7835
|
+
{ path: ["billing", "invoices", "download"], description: "Download an invoice PDF", examples: ["openmates settings billing invoices download <invoice-id> --output ./invoices"] },
|
|
7836
|
+
{ path: ["billing", "invoices", "credit-note"], description: "Download a credit note PDF", examples: ["openmates settings billing invoices credit-note <invoice-id> --output ./invoices"] },
|
|
7837
|
+
{ path: ["billing", "invoices", "refund"], description: "Request a refund for an invoice", examples: ["openmates settings billing invoices refund <invoice-id> --yes"] },
|
|
7692
7838
|
{ path: ["billing", "gift-card", "redeem"], description: "Redeem a gift card", examples: ["openmates settings billing gift-card redeem ABCD-1234"] },
|
|
7693
7839
|
{ path: ["billing", "gift-card", "list"], description: "List redeemed gift cards", examples: ["openmates settings billing gift-card list"] },
|
|
7694
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"] },
|
|
7841
|
+
{ path: ["notifications", "status"], description: "Show notification settings", examples: ["openmates settings notifications status --json"] },
|
|
7842
|
+
{ path: ["notifications", "email", "set"], description: "Configure email notifications", examples: ["openmates settings notifications email set --enabled true --email you@example.com --ai-responses true --backup-reminder true --webhook-chats true"] },
|
|
7843
|
+
{ path: ["notifications", "backup", "set"], description: "Configure backup reminder emails", examples: ["openmates settings notifications backup set --enabled true --interval 30 --email you@example.com"] },
|
|
7695
7844
|
{ path: ["reminders", "list"], description: "List active reminders", examples: ["openmates settings reminders list"] },
|
|
7696
7845
|
{ path: ["reminders", "update"], description: "Update a reminder", examples: ["openmates settings reminders update <id> --enabled false"] },
|
|
7697
7846
|
{ path: ["reminders", "delete"], description: "Delete a reminder", examples: ["openmates settings reminders delete <id> --yes"] },
|
|
@@ -7699,12 +7848,18 @@ var SETTINGS_EXECUTABLE_COMMANDS = [
|
|
|
7699
7848
|
{ path: ["developers", "api-keys", "revoke"], description: "Revoke an API key", examples: ["openmates settings developers api-keys revoke <key-id> --yes"] },
|
|
7700
7849
|
{ path: ["report-issue", "create"], description: "Report an issue", examples: ['openmates settings report-issue create --title "Bug" --body "What happened"'] },
|
|
7701
7850
|
{ path: ["report-issue", "status"], description: "Show issue status", examples: ["openmates settings report-issue status <issue-id>"] },
|
|
7851
|
+
{ path: ["mates", "list"], description: "List available mates", examples: ["openmates settings mates list"] },
|
|
7852
|
+
{ path: ["mates", "info"], description: "Show mate details", examples: ["openmates settings mates info software_development"] },
|
|
7853
|
+
{ path: ["mates", "consent"], description: "Record mate settings consent", examples: ["openmates settings mates consent --yes"] },
|
|
7854
|
+
{ path: ["newsletter", "categories"], description: "Show newsletter category preferences", examples: ["openmates settings newsletter categories"] },
|
|
7855
|
+
{ path: ["newsletter", "categories", "set"], description: "Set newsletter category preferences", examples: ["openmates settings newsletter categories set --updates true --tips true --daily false"] },
|
|
7856
|
+
{ path: ["newsletter", "subscribe"], description: "Subscribe an email to the newsletter", examples: ["openmates settings newsletter subscribe you@example.com --language en"] },
|
|
7857
|
+
{ path: ["newsletter", "confirm"], description: "Confirm newsletter subscription token", examples: ["openmates settings newsletter confirm <token>"] },
|
|
7858
|
+
{ path: ["newsletter", "unsubscribe"], description: "Unsubscribe with newsletter token", examples: ["openmates settings newsletter unsubscribe <token>"] },
|
|
7702
7859
|
{ path: ["memories"], description: "Manage encrypted memories", examples: ["openmates settings memories list", `openmates settings memories create --app-id code --item-type projects --data '{"name":"OpenMates"}'`] }
|
|
7703
7860
|
];
|
|
7704
7861
|
var SETTINGS_INFO_COMMANDS = [
|
|
7705
|
-
{ path: ["account", "username"], description: "Username management is not CLI-ready yet", webPath: "account/username", reason: "The current web flow writes encrypted profile fields; CLI support needs a dedicated encryption UX first.", examples: ["openmates settings account username --help"] },
|
|
7706
7862
|
{ path: ["account", "email"], description: "Email changes are web-only", webPath: "account/email", reason: "Email changes require a guided identity verification flow.", examples: ["openmates settings account email"] },
|
|
7707
|
-
{ path: ["account", "profile-picture"], description: "Profile picture changes are not CLI-ready yet", webPath: "account/profile-picture", reason: "The upload/update flow needs image validation and parity with the browser uploader.", examples: ["openmates settings account profile-picture"] },
|
|
7708
7863
|
{ path: ["account", "delete"], description: "Account deletion is web-only", webPath: "account/delete", reason: "Account deletion requires browser-based reauthentication and explicit confirmation.", examples: ["openmates settings account delete"] },
|
|
7709
7864
|
{ path: ["security"], description: "Security settings are web-only", webPath: "account/security", reason: "Security settings require browser APIs or high-risk reauthentication.", examples: ["openmates settings security"] },
|
|
7710
7865
|
{ path: ["security", "passkeys"], description: "Passkeys are web-only", webPath: "account/security/passkeys", reason: "Passkeys require WebAuthn browser APIs.", examples: ["openmates settings security passkeys"] },
|
|
@@ -7715,16 +7870,12 @@ var SETTINGS_INFO_COMMANDS = [
|
|
|
7715
7870
|
{ path: ["billing", "buy-credits"], description: "Credit purchase is web-only", webPath: "billing/buy-credits", reason: "Payment checkout must use the browser/payment provider UI.", examples: ["openmates settings billing buy-credits"] },
|
|
7716
7871
|
{ path: ["billing", "gift-card", "buy"], description: "Gift card purchase is web-only", webPath: "billing/gift-cards/buy", reason: "Payment checkout must use the browser/payment provider UI.", examples: ["openmates settings billing gift-card buy"] },
|
|
7717
7872
|
{ path: ["billing", "auto-topup", "monthly"], description: "Monthly auto top-up is web-only for now", webPath: "billing/auto-topup/monthly", reason: "Recurring payment setup needs a payment-flow audit before CLI support.", examples: ["openmates settings billing auto-topup monthly"] },
|
|
7718
|
-
{ path: ["billing", "invoices"], description: "Invoice management is web-only for now", webPath: "billing/invoices", reason: "Invoice download support needs auth-gated file streaming parity first.", examples: ["openmates settings billing invoices"] },
|
|
7719
7873
|
{ path: ["privacy", "personal-data"], description: "Personal data management is not CLI-ready yet", webPath: "privacy/hide-personal-data", reason: "The CLI needs a dedicated encrypted personal-data UX before exposing writes.", examples: ["openmates settings privacy personal-data"] },
|
|
7720
|
-
{ path: ["notifications"], description: "Notification preferences are not CLI-ready yet", webPath: "notifications", reason: "Backend preference endpoints need an audit before terminal writes are exposed.", examples: ["openmates settings notifications"] },
|
|
7721
7874
|
{ path: ["shared", "tip"], description: "Tips are web-only", webPath: "shared/tip", reason: "Payment checkout must use the browser/payment provider UI.", examples: ["openmates settings shared tip"] },
|
|
7722
|
-
{ path: ["mates"], description: "Mate browsing is available through mentions for now", webPath: "mates", reason: "Use @mate mentions in chat; rich mate settings remain browser-first.", examples: ["openmates mentions list --type mate"] },
|
|
7723
7875
|
{ path: ["developers", "api-keys", "create"], description: "API key creation is web-only", webPath: "developers/api-keys", reason: "API key secrets are shown once and need the browser approval flow.", examples: ["openmates settings developers api-keys create"] },
|
|
7724
7876
|
{ path: ["developers", "devices"], description: "Developer devices are web-only", webPath: "developers/devices", reason: "Device approvals and revocations are sensitive.", examples: ["openmates settings developers devices"] },
|
|
7725
7877
|
{ path: ["developers", "webhooks"], description: "Developer webhooks are not CLI-ready yet", webPath: "developers/webhooks", reason: "Webhook CRUD needs a backend/API audit before CLI support.", examples: ["openmates settings developers webhooks"] },
|
|
7726
7878
|
{ path: ["support"], description: "Support payments are web-only", webPath: "support", reason: "Payment flows must use the browser/payment provider UI.", examples: ["openmates settings support"] },
|
|
7727
|
-
{ path: ["newsletter"], description: "Newsletter settings are not CLI-ready yet", webPath: "newsletter", reason: "Newsletter API flow needs an audit before CLI support.", examples: ["openmates settings newsletter"] },
|
|
7728
7879
|
{ path: ["incognito", "info"], description: "Explain incognito mode", reason: "Incognito chats are sent without saving chat history. The CLI stores no incognito transcript.", examples: ['openmates chats incognito "Private question"'] },
|
|
7729
7880
|
{ path: ["server"], description: "Server admin settings are web/admin-only", webPath: "server", reason: "Use `openmates server --help` for self-hosted terminal server management.", examples: ["openmates server status"] }
|
|
7730
7881
|
];
|
|
@@ -7762,6 +7913,11 @@ function parseRequiredNumber(value, flag) {
|
|
|
7762
7913
|
if (!Number.isFinite(parsed)) throw new Error(`Invalid ${flag}: ${value}`);
|
|
7763
7914
|
return parsed;
|
|
7764
7915
|
}
|
|
7916
|
+
function parseOptionalBoolean(value, fallback, label) {
|
|
7917
|
+
if (value === void 0) return fallback;
|
|
7918
|
+
if (typeof value === "boolean") return value;
|
|
7919
|
+
return parseOnOff(value, label);
|
|
7920
|
+
}
|
|
7765
7921
|
function parseDataOrFlags(flags, booleanFlags) {
|
|
7766
7922
|
if (typeof flags.data === "string") return JSON.parse(flags.data);
|
|
7767
7923
|
const body = {};
|
|
@@ -7873,6 +8029,37 @@ function parseYamlScalar(value) {
|
|
|
7873
8029
|
}
|
|
7874
8030
|
return trimmed;
|
|
7875
8031
|
}
|
|
8032
|
+
async function saveDownloadedDocument(document, output) {
|
|
8033
|
+
const { mkdir, writeFile } = await import("fs/promises");
|
|
8034
|
+
const { join: join4, basename: basename2, dirname } = await import("path");
|
|
8035
|
+
const target = typeof output === "string" ? output : ".";
|
|
8036
|
+
const filename = basename2(document.filename || "document.pdf");
|
|
8037
|
+
const filePath = target.endsWith(".pdf") ? target : join4(target, filename);
|
|
8038
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
8039
|
+
await writeFile(filePath, document.data);
|
|
8040
|
+
return filePath;
|
|
8041
|
+
}
|
|
8042
|
+
function printMates(json) {
|
|
8043
|
+
const mates = Object.entries(MATE_NAMES).map(([id, name]) => ({ id, name, mention: `@mate:${id}` }));
|
|
8044
|
+
if (json) {
|
|
8045
|
+
printJson2(mates);
|
|
8046
|
+
return;
|
|
8047
|
+
}
|
|
8048
|
+
header("Mates");
|
|
8049
|
+
for (const mate of mates) console.log(`${mate.id.padEnd(28)} ${mate.name.padEnd(10)} ${mate.mention}`);
|
|
8050
|
+
}
|
|
8051
|
+
function printMateInfo(mateId, json) {
|
|
8052
|
+
const name = MATE_NAMES[mateId];
|
|
8053
|
+
if (!name) throw new Error(`Unknown mate '${mateId}'. Run 'openmates settings mates list'.`);
|
|
8054
|
+
const data = { id: mateId, name, mention: `@mate:${mateId}` };
|
|
8055
|
+
if (json) {
|
|
8056
|
+
printJson2(data);
|
|
8057
|
+
return;
|
|
8058
|
+
}
|
|
8059
|
+
header(`${name} (${mateId})`);
|
|
8060
|
+
console.log(`Mention: ${data.mention}`);
|
|
8061
|
+
console.log(`Use: openmates chats send "${data.mention} <message>"`);
|
|
8062
|
+
}
|
|
7876
8063
|
async function confirmOrExit(question) {
|
|
7877
8064
|
const rl = await import("readline");
|
|
7878
8065
|
const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -7957,6 +8144,18 @@ async function handleSettings(client, subcommand, rest, flags) {
|
|
|
7957
8144
|
);
|
|
7958
8145
|
return;
|
|
7959
8146
|
}
|
|
8147
|
+
if (matches(tokens, ["account", "username", "set"])) {
|
|
8148
|
+
const username = rest[2];
|
|
8149
|
+
if (!username) throw new Error("Missing username. Example: openmates settings account username set alice_123");
|
|
8150
|
+
await printSettingsMutationResult(client.updateUsername(username), flags);
|
|
8151
|
+
return;
|
|
8152
|
+
}
|
|
8153
|
+
if (matches(tokens, ["account", "profile-picture", "set"])) {
|
|
8154
|
+
const file = rest[2];
|
|
8155
|
+
if (!file) throw new Error("Missing image file. Example: openmates settings account profile-picture set ./avatar.jpg");
|
|
8156
|
+
await printSettingsMutationResult(client.updateProfileImage(file), flags);
|
|
8157
|
+
return;
|
|
8158
|
+
}
|
|
7960
8159
|
if (matches(tokens, ["account", "chats", "stats"])) {
|
|
7961
8160
|
await printSettingsResult(client.settingsGet("chats"), flags);
|
|
7962
8161
|
return;
|
|
@@ -8046,6 +8245,35 @@ async function handleSettings(client, subcommand, rest, flags) {
|
|
|
8046
8245
|
await printSettingsResult(client.settingsGet("usage"), flags);
|
|
8047
8246
|
return;
|
|
8048
8247
|
}
|
|
8248
|
+
if (matches(tokens, ["billing", "invoices", "list"])) {
|
|
8249
|
+
await printSettingsResult(client.listInvoices(), flags);
|
|
8250
|
+
return;
|
|
8251
|
+
}
|
|
8252
|
+
if (matches(tokens, ["billing", "invoices", "download"])) {
|
|
8253
|
+
const invoiceId = rest[2];
|
|
8254
|
+
if (!invoiceId) throw new Error("Missing invoice ID.");
|
|
8255
|
+
const document = await client.downloadInvoice(invoiceId);
|
|
8256
|
+
const filePath = await saveDownloadedDocument(document, flags.output);
|
|
8257
|
+
if (flags.json === true) printJson2({ path: filePath, filename: document.filename });
|
|
8258
|
+
else console.log(`\x1B[32m\u2713\x1B[0m Invoice saved to ${filePath}`);
|
|
8259
|
+
return;
|
|
8260
|
+
}
|
|
8261
|
+
if (matches(tokens, ["billing", "invoices", "credit-note"])) {
|
|
8262
|
+
const invoiceId = rest[2];
|
|
8263
|
+
if (!invoiceId) throw new Error("Missing invoice ID.");
|
|
8264
|
+
const document = await client.downloadCreditNote(invoiceId);
|
|
8265
|
+
const filePath = await saveDownloadedDocument(document, flags.output);
|
|
8266
|
+
if (flags.json === true) printJson2({ path: filePath, filename: document.filename });
|
|
8267
|
+
else console.log(`\x1B[32m\u2713\x1B[0m Credit note saved to ${filePath}`);
|
|
8268
|
+
return;
|
|
8269
|
+
}
|
|
8270
|
+
if (matches(tokens, ["billing", "invoices", "refund"])) {
|
|
8271
|
+
const invoiceId = rest[2];
|
|
8272
|
+
if (!invoiceId) throw new Error("Missing invoice ID.");
|
|
8273
|
+
if (flags.yes !== true) await confirmOrExit(`Request refund for invoice ${invoiceId}? [y/N] `);
|
|
8274
|
+
await printSettingsMutationResult(client.requestRefund(invoiceId), flags);
|
|
8275
|
+
return;
|
|
8276
|
+
}
|
|
8049
8277
|
if (matches(tokens, ["billing", "gift-card", "redeem"]) || subcommand === "gift-card" && rest[0] === "redeem") {
|
|
8050
8278
|
const code = matches(tokens, ["billing", "gift-card", "redeem"]) ? rest[2] : rest[1];
|
|
8051
8279
|
if (!code) throw new Error("Missing gift card code.");
|
|
@@ -8079,6 +8307,48 @@ async function handleSettings(client, subcommand, rest, flags) {
|
|
|
8079
8307
|
);
|
|
8080
8308
|
return;
|
|
8081
8309
|
}
|
|
8310
|
+
if (matches(tokens, ["notifications", "status"])) {
|
|
8311
|
+
const user = await client.whoAmI();
|
|
8312
|
+
const status = {
|
|
8313
|
+
enabled: user.email_notifications_enabled ?? false,
|
|
8314
|
+
preferences: user.email_notification_preferences ?? {},
|
|
8315
|
+
backup_reminder_interval_days: user.backup_reminder_interval_days ?? null,
|
|
8316
|
+
encrypted_notification_email_configured: Boolean(user.encrypted_notification_email)
|
|
8317
|
+
};
|
|
8318
|
+
flags.json === true ? printJson2(status) : printGenericObject(status);
|
|
8319
|
+
return;
|
|
8320
|
+
}
|
|
8321
|
+
if (matches(tokens, ["notifications", "email", "set"])) {
|
|
8322
|
+
const enabled = parseOnOff(String(flags.enabled ?? ""), "email notifications");
|
|
8323
|
+
const email = typeof flags.email === "string" ? flags.email : null;
|
|
8324
|
+
if (enabled && !email) throw new Error("Provide --email when enabling email notifications.");
|
|
8325
|
+
const preferences = {
|
|
8326
|
+
aiResponses: parseOptionalBoolean(flags["ai-responses"], true, "AI response notifications"),
|
|
8327
|
+
backupReminder: parseOptionalBoolean(flags["backup-reminder"], false, "backup reminder notifications"),
|
|
8328
|
+
webhookChats: parseOptionalBoolean(flags["webhook-chats"], false, "webhook chat notifications")
|
|
8329
|
+
};
|
|
8330
|
+
await printSettingsMutationResult(
|
|
8331
|
+
client.updateEmailNotificationSettings({ enabled, email, preferences }),
|
|
8332
|
+
flags
|
|
8333
|
+
);
|
|
8334
|
+
return;
|
|
8335
|
+
}
|
|
8336
|
+
if (matches(tokens, ["notifications", "backup", "set"])) {
|
|
8337
|
+
const enabled = parseOnOff(String(flags.enabled ?? ""), "backup reminders");
|
|
8338
|
+
const email = typeof flags.email === "string" ? flags.email : null;
|
|
8339
|
+
const interval = parseRequiredNumber(flags.interval, "--interval");
|
|
8340
|
+
if (enabled && !email) throw new Error("Provide --email when enabling backup reminders.");
|
|
8341
|
+
await printSettingsMutationResult(
|
|
8342
|
+
client.updateEmailNotificationSettings({
|
|
8343
|
+
enabled,
|
|
8344
|
+
email,
|
|
8345
|
+
preferences: { aiResponses: false, backupReminder: enabled },
|
|
8346
|
+
backup_reminder_interval_days: interval
|
|
8347
|
+
}),
|
|
8348
|
+
flags
|
|
8349
|
+
);
|
|
8350
|
+
return;
|
|
8351
|
+
}
|
|
8082
8352
|
if (matches(tokens, ["reminders", "list"])) {
|
|
8083
8353
|
await printSettingsResult(client.settingsGet("reminders"), flags);
|
|
8084
8354
|
return;
|
|
@@ -8121,6 +8391,54 @@ async function handleSettings(client, subcommand, rest, flags) {
|
|
|
8121
8391
|
await printSettingsResult(client.settingsGet(`issues/${id}/status`), flags);
|
|
8122
8392
|
return;
|
|
8123
8393
|
}
|
|
8394
|
+
if (matches(tokens, ["mates", "list"])) {
|
|
8395
|
+
printMates(flags.json === true);
|
|
8396
|
+
return;
|
|
8397
|
+
}
|
|
8398
|
+
if (matches(tokens, ["mates", "info"])) {
|
|
8399
|
+
const mateId = rest[1];
|
|
8400
|
+
if (!mateId) throw new Error("Missing mate ID. Example: openmates settings mates info software_development");
|
|
8401
|
+
printMateInfo(mateId, flags.json === true);
|
|
8402
|
+
return;
|
|
8403
|
+
}
|
|
8404
|
+
if (matches(tokens, ["mates", "consent"])) {
|
|
8405
|
+
if (flags.yes !== true) await confirmOrExit("Record consent for mate settings? [y/N] ");
|
|
8406
|
+
await printSettingsMutationResult(client.settingsPost("user/consent/mates", { consent: true }), flags);
|
|
8407
|
+
return;
|
|
8408
|
+
}
|
|
8409
|
+
if (matches(tokens, ["newsletter", "categories"]) && tokens.length === 2) {
|
|
8410
|
+
await printSettingsResult(client.getNewsletterCategories(), flags);
|
|
8411
|
+
return;
|
|
8412
|
+
}
|
|
8413
|
+
if (matches(tokens, ["newsletter", "categories", "set"])) {
|
|
8414
|
+
const categories = {
|
|
8415
|
+
updates_and_announcements: parseOptionalBoolean(flags.updates, true, "updates newsletter"),
|
|
8416
|
+
tips_and_tricks: parseOptionalBoolean(flags.tips, true, "tips newsletter"),
|
|
8417
|
+
daily_inspirations: parseOptionalBoolean(flags.daily, true, "daily inspirations newsletter")
|
|
8418
|
+
};
|
|
8419
|
+
await printSettingsMutationResult(client.updateNewsletterCategories(categories), flags);
|
|
8420
|
+
return;
|
|
8421
|
+
}
|
|
8422
|
+
if (matches(tokens, ["newsletter", "subscribe"])) {
|
|
8423
|
+
const email = rest[1];
|
|
8424
|
+
if (!email) throw new Error("Missing email. Example: openmates settings newsletter subscribe you@example.com");
|
|
8425
|
+
const language = typeof flags.language === "string" ? flags.language : "en";
|
|
8426
|
+
const darkmode = parseOptionalBoolean(flags.darkmode, false, "newsletter dark mode");
|
|
8427
|
+
await printSettingsMutationResult(client.subscribeNewsletter(email, language, darkmode), flags);
|
|
8428
|
+
return;
|
|
8429
|
+
}
|
|
8430
|
+
if (matches(tokens, ["newsletter", "confirm"])) {
|
|
8431
|
+
const token = rest[1];
|
|
8432
|
+
if (!token) throw new Error("Missing confirmation token.");
|
|
8433
|
+
await printSettingsMutationResult(client.confirmNewsletter(token), flags);
|
|
8434
|
+
return;
|
|
8435
|
+
}
|
|
8436
|
+
if (matches(tokens, ["newsletter", "unsubscribe"])) {
|
|
8437
|
+
const token = rest[1];
|
|
8438
|
+
if (!token) throw new Error("Missing unsubscribe token.");
|
|
8439
|
+
await printSettingsMutationResult(client.unsubscribeNewsletter(token), flags);
|
|
8440
|
+
return;
|
|
8441
|
+
}
|
|
8124
8442
|
if (subcommand === "memories") {
|
|
8125
8443
|
await handleMemories(client, rest, flags);
|
|
8126
8444
|
return;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// src/uploadService.ts
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { basename, extname } from "path";
|
|
4
|
+
var UPLOAD_MAX_ATTEMPTS = 3;
|
|
5
|
+
var UPLOAD_RETRY_DELAY_MS = 2e3;
|
|
6
|
+
var PROFILE_IMAGE_MAX_SIZE_BYTES = 300 * 1024;
|
|
7
|
+
function getUploadUrl(apiUrl) {
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(apiUrl);
|
|
10
|
+
if (url.hostname === "localhost") return "http://localhost:8001";
|
|
11
|
+
} catch {
|
|
12
|
+
}
|
|
13
|
+
return "https://upload.openmates.org";
|
|
14
|
+
}
|
|
15
|
+
function getUploadOrigin(apiUrl) {
|
|
16
|
+
try {
|
|
17
|
+
const url = new URL(apiUrl);
|
|
18
|
+
if (url.hostname === "localhost") return "http://localhost:5173";
|
|
19
|
+
if (url.hostname.startsWith("api.")) {
|
|
20
|
+
return `${url.protocol}//app.${url.hostname.slice(4)}`;
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
return "https://app.openmates.org";
|
|
25
|
+
}
|
|
26
|
+
async function uploadFile(filePath, session) {
|
|
27
|
+
const filename = basename(filePath);
|
|
28
|
+
const fileBytes = readFileSync(filePath);
|
|
29
|
+
const uploadUrl = `${getUploadUrl(session.apiUrl)}/v1/upload/file`;
|
|
30
|
+
const origin = getUploadOrigin(session.apiUrl);
|
|
31
|
+
const cookies = [];
|
|
32
|
+
if (session.cookies?.auth_refresh_token) {
|
|
33
|
+
cookies.push(`auth_refresh_token=${session.cookies.auth_refresh_token}`);
|
|
34
|
+
}
|
|
35
|
+
let response;
|
|
36
|
+
let lastError;
|
|
37
|
+
for (let attempt = 1; attempt <= UPLOAD_MAX_ATTEMPTS; attempt++) {
|
|
38
|
+
try {
|
|
39
|
+
const blob = new Blob([fileBytes]);
|
|
40
|
+
const formData = new FormData();
|
|
41
|
+
formData.append("file", blob, filename);
|
|
42
|
+
response = await fetch(uploadUrl, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
body: formData,
|
|
45
|
+
headers: {
|
|
46
|
+
Origin: origin,
|
|
47
|
+
...cookies.length > 0 ? { Cookie: cookies.join("; ") } : {}
|
|
48
|
+
},
|
|
49
|
+
signal: AbortSignal.timeout(10 * 60 * 1e3)
|
|
50
|
+
// 10-minute timeout
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
lastError = error;
|
|
55
|
+
if (attempt === UPLOAD_MAX_ATTEMPTS) break;
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, UPLOAD_RETRY_DELAY_MS));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!response) {
|
|
60
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
61
|
+
throw new Error(message || "Upload request failed.");
|
|
62
|
+
}
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const status = response.status;
|
|
65
|
+
let errorMessage;
|
|
66
|
+
switch (status) {
|
|
67
|
+
case 401:
|
|
68
|
+
errorMessage = "Authentication failed. Run `openmates login` to re-authenticate.";
|
|
69
|
+
break;
|
|
70
|
+
case 413:
|
|
71
|
+
errorMessage = "File too large (maximum 100 MB).";
|
|
72
|
+
break;
|
|
73
|
+
case 415:
|
|
74
|
+
errorMessage = "Unsupported file type.";
|
|
75
|
+
break;
|
|
76
|
+
case 422: {
|
|
77
|
+
const body = await response.text().catch(() => "");
|
|
78
|
+
errorMessage = body.includes("malware") ? "File rejected: malware detected." : body.includes("content_safety") ? "File rejected: content safety violation." : `Upload validation failed: ${body}`;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case 429:
|
|
82
|
+
errorMessage = "Upload rate limit exceeded. Try again in a minute.";
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
errorMessage = `Upload failed (HTTP ${status}).`;
|
|
86
|
+
}
|
|
87
|
+
throw new Error(errorMessage);
|
|
88
|
+
}
|
|
89
|
+
const data = await response.json();
|
|
90
|
+
return data;
|
|
91
|
+
}
|
|
92
|
+
function getProfileImageMime(filename) {
|
|
93
|
+
const ext = extname(filename).toLowerCase();
|
|
94
|
+
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
|
95
|
+
if (ext === ".png") return "image/png";
|
|
96
|
+
throw new Error("Profile images must be JPEG or PNG files.");
|
|
97
|
+
}
|
|
98
|
+
async function uploadProfileImage(filePath, session) {
|
|
99
|
+
const filename = basename(filePath);
|
|
100
|
+
const fileBytes = readFileSync(filePath);
|
|
101
|
+
const contentType = getProfileImageMime(filename);
|
|
102
|
+
if (fileBytes.byteLength > PROFILE_IMAGE_MAX_SIZE_BYTES) {
|
|
103
|
+
throw new Error("Profile image must be 300 KB or smaller. Resize/compress the image and try again.");
|
|
104
|
+
}
|
|
105
|
+
const uploadUrl = `${getUploadUrl(session.apiUrl)}/v1/upload/profile-image`;
|
|
106
|
+
const origin = getUploadOrigin(session.apiUrl);
|
|
107
|
+
const cookies = [];
|
|
108
|
+
if (session.cookies?.auth_refresh_token) {
|
|
109
|
+
cookies.push(`auth_refresh_token=${session.cookies.auth_refresh_token}`);
|
|
110
|
+
}
|
|
111
|
+
const formData = new FormData();
|
|
112
|
+
formData.append("file", new Blob([fileBytes], { type: contentType }), filename);
|
|
113
|
+
const response = await fetch(uploadUrl, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
body: formData,
|
|
116
|
+
headers: {
|
|
117
|
+
Origin: origin,
|
|
118
|
+
...cookies.length > 0 ? { Cookie: cookies.join("; ") } : {}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
const data = await response.json().catch(() => ({}));
|
|
122
|
+
if (!response.ok && !data.status) {
|
|
123
|
+
throw new Error(data.detail ?? `Profile image upload failed (HTTP ${response.status}).`);
|
|
124
|
+
}
|
|
125
|
+
return data;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export {
|
|
129
|
+
uploadFile,
|
|
130
|
+
uploadProfileImage
|
|
131
|
+
};
|
package/dist/cli.js
CHANGED
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;
|
|
@@ -293,6 +294,23 @@ interface OpenMatesClientOptions {
|
|
|
293
294
|
apiUrl?: string;
|
|
294
295
|
session?: OpenMatesSession;
|
|
295
296
|
}
|
|
297
|
+
interface InvoiceListItem {
|
|
298
|
+
id: string;
|
|
299
|
+
order_id?: string | null;
|
|
300
|
+
date: string;
|
|
301
|
+
amount: string;
|
|
302
|
+
credits_purchased: number;
|
|
303
|
+
filename: string;
|
|
304
|
+
is_gift_card?: boolean;
|
|
305
|
+
refunded_at?: string | null;
|
|
306
|
+
refund_status?: string | null;
|
|
307
|
+
currency?: string | null;
|
|
308
|
+
provider?: string | null;
|
|
309
|
+
}
|
|
310
|
+
interface DownloadedDocument {
|
|
311
|
+
filename: string;
|
|
312
|
+
data: Uint8Array;
|
|
313
|
+
}
|
|
296
314
|
/**
|
|
297
315
|
* Derive the web app URL from the API URL so the pair token is always looked
|
|
298
316
|
* up on the same backend the CLI created it on.
|
|
@@ -467,6 +485,25 @@ declare class OpenMatesClient {
|
|
|
467
485
|
message: string;
|
|
468
486
|
}>;
|
|
469
487
|
listRedeemedGiftCards(): Promise<unknown>;
|
|
488
|
+
listInvoices(): Promise<{
|
|
489
|
+
invoices: InvoiceListItem[];
|
|
490
|
+
}>;
|
|
491
|
+
downloadInvoice(invoiceId: string): Promise<DownloadedDocument>;
|
|
492
|
+
downloadCreditNote(invoiceId: string): Promise<DownloadedDocument>;
|
|
493
|
+
requestRefund(invoiceId: string): Promise<unknown>;
|
|
494
|
+
updateUsername(username: string): Promise<unknown>;
|
|
495
|
+
updateProfileImage(filePath: string): Promise<unknown>;
|
|
496
|
+
getNewsletterCategories(): Promise<unknown>;
|
|
497
|
+
updateNewsletterCategories(categories: Record<string, boolean>): Promise<unknown>;
|
|
498
|
+
subscribeNewsletter(email: string, language?: string, darkmode?: boolean): Promise<unknown>;
|
|
499
|
+
confirmNewsletter(token: string): Promise<unknown>;
|
|
500
|
+
unsubscribeNewsletter(token: string): Promise<unknown>;
|
|
501
|
+
updateEmailNotificationSettings(payload: {
|
|
502
|
+
enabled: boolean;
|
|
503
|
+
email?: string | null;
|
|
504
|
+
preferences: Record<string, boolean>;
|
|
505
|
+
backup_reminder_interval_days?: number;
|
|
506
|
+
}): Promise<unknown>;
|
|
470
507
|
/**
|
|
471
508
|
* Fetch today's daily inspirations.
|
|
472
509
|
*
|
|
@@ -600,6 +637,9 @@ declare class OpenMatesClient {
|
|
|
600
637
|
*/
|
|
601
638
|
buildMentionContext(): Promise<MentionContext>;
|
|
602
639
|
private normalizePath;
|
|
640
|
+
private downloadPaymentPdf;
|
|
641
|
+
private ensureEmailEncryptionKey;
|
|
642
|
+
private hydrateEmailEncryptionKey;
|
|
603
643
|
private requireSession;
|
|
604
644
|
private getMasterKeyBytes;
|
|
605
645
|
private getValidSessionFromDisk;
|
package/dist/index.js
CHANGED