openmates 0.15.0-alpha.29 → 0.15.0-alpha.30
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-LPJME5IC.js → chunk-XG7OFIXM.js} +188 -30
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +21 -3
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -39,14 +39,14 @@ function base64ToBytes(input) {
|
|
|
39
39
|
function bytesToBase64(input) {
|
|
40
40
|
return Buffer.from(input).toString("base64");
|
|
41
41
|
}
|
|
42
|
+
function bytesToBase64Url(input) {
|
|
43
|
+
return Buffer.from(input).toString("base64url");
|
|
44
|
+
}
|
|
42
45
|
function toArrayBuffer(input) {
|
|
43
46
|
const output = new ArrayBuffer(input.byteLength);
|
|
44
47
|
new Uint8Array(output).set(input);
|
|
45
48
|
return output;
|
|
46
49
|
}
|
|
47
|
-
function bytesToBase64Url(input) {
|
|
48
|
-
return Buffer.from(input).toString("base64url");
|
|
49
|
-
}
|
|
50
50
|
function base64UrlToBytes(input, field, expectedLength) {
|
|
51
51
|
if (!input || input.includes("=")) {
|
|
52
52
|
throw new Error(`${field} must be non-empty unpadded base64url`);
|
|
@@ -60,6 +60,17 @@ function base64UrlToBytes(input, field, expectedLength) {
|
|
|
60
60
|
}
|
|
61
61
|
return decoded;
|
|
62
62
|
}
|
|
63
|
+
async function deriveTeamInviteKey(input) {
|
|
64
|
+
const secretBytes = base64UrlToBytes(input.inviteSecret, "invite secret", 32);
|
|
65
|
+
const salt = await sha256(new TextEncoder().encode("openmates:team-invite:v1"));
|
|
66
|
+
const info = concatBytes(
|
|
67
|
+
lengthPrefix(new TextEncoder().encode(input.recipientEmail.trim().toLowerCase())),
|
|
68
|
+
lengthPrefix(new TextEncoder().encode(input.inviteId)),
|
|
69
|
+
lengthPrefix(new TextEncoder().encode(input.teamId)),
|
|
70
|
+
lengthPrefix(new TextEncoder().encode(input.origin.replace(/\/$/, "")))
|
|
71
|
+
);
|
|
72
|
+
return hkdfSha256(secretBytes, salt, info);
|
|
73
|
+
}
|
|
63
74
|
function uint32(value, field) {
|
|
64
75
|
if (!Number.isInteger(value) || value < 0 || value > 4294967295) {
|
|
65
76
|
throw new Error(`${field} must be an unsigned 32-bit integer`);
|
|
@@ -1053,6 +1064,9 @@ function clearSession() {
|
|
|
1053
1064
|
if (onDisk?.emailEncryptionKeyStorage) {
|
|
1054
1065
|
deleteMasterKey(onDisk.emailEncryptionKeyStorage, `${onDisk.hashedEmail}:email`);
|
|
1055
1066
|
}
|
|
1067
|
+
if (onDisk?.activeTeamId) {
|
|
1068
|
+
deleteLocalTeamKey(onDisk.hashedEmail, onDisk.activeTeamId);
|
|
1069
|
+
}
|
|
1056
1070
|
if (existsSync2(filePath)) {
|
|
1057
1071
|
rmSync(filePath);
|
|
1058
1072
|
}
|
|
@@ -1089,6 +1103,41 @@ function buildSession(onDisk, masterKey, emailEncryptionKey) {
|
|
|
1089
1103
|
};
|
|
1090
1104
|
}
|
|
1091
1105
|
var SYNC_CACHE_FILE = "sync_cache.json";
|
|
1106
|
+
var LOCAL_TEAM_KEYS_FILE = "team_keys.json";
|
|
1107
|
+
function teamKeyStorageId(hashedEmail, teamId) {
|
|
1108
|
+
const digest = createHash3("sha256").update(teamId).digest("hex").slice(0, 32);
|
|
1109
|
+
return `${hashedEmail}:team:${digest}`;
|
|
1110
|
+
}
|
|
1111
|
+
function saveLocalTeamKey(hashedEmail, teamId, teamKeyB64) {
|
|
1112
|
+
const storageId = teamKeyStorageId(hashedEmail, teamId);
|
|
1113
|
+
const result = storeMasterKey(teamKeyB64, storageId);
|
|
1114
|
+
const filePath = join(ensureStateDir(), LOCAL_TEAM_KEYS_FILE);
|
|
1115
|
+
const keys = readJsonFile(filePath) ?? { teams: {} };
|
|
1116
|
+
keys.teams[storageId] = {
|
|
1117
|
+
storage: result.type,
|
|
1118
|
+
...result.type === "encrypted" ? { encryptedData: result.encryptedData } : {},
|
|
1119
|
+
...result.type === "plaintext" ? { plaintextKeyB64: teamKeyB64 } : {}
|
|
1120
|
+
};
|
|
1121
|
+
writeJsonFile(filePath, keys);
|
|
1122
|
+
}
|
|
1123
|
+
function loadLocalTeamKey(hashedEmail, teamId) {
|
|
1124
|
+
const storageId = teamKeyStorageId(hashedEmail, teamId);
|
|
1125
|
+
const filePath = join(ensureStateDir(), LOCAL_TEAM_KEYS_FILE);
|
|
1126
|
+
const entry = readJsonFile(filePath)?.teams[storageId];
|
|
1127
|
+
if (!entry) return null;
|
|
1128
|
+
if (entry.storage === "plaintext") return entry.plaintextKeyB64 ?? null;
|
|
1129
|
+
return retrieveMasterKey(entry.storage, storageId, entry.encryptedData);
|
|
1130
|
+
}
|
|
1131
|
+
function deleteLocalTeamKey(hashedEmail, teamId) {
|
|
1132
|
+
const storageId = teamKeyStorageId(hashedEmail, teamId);
|
|
1133
|
+
deleteMasterKey("keychain", storageId);
|
|
1134
|
+
const filePath = join(ensureStateDir(), LOCAL_TEAM_KEYS_FILE);
|
|
1135
|
+
const keys = readJsonFile(filePath);
|
|
1136
|
+
if (keys?.teams[storageId]) {
|
|
1137
|
+
delete keys.teams[storageId];
|
|
1138
|
+
writeJsonFile(filePath, keys);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1092
1141
|
function syncCacheFile(teamId) {
|
|
1093
1142
|
if (!teamId) return SYNC_CACHE_FILE;
|
|
1094
1143
|
const digest = createHash3("sha256").update(teamId).digest("hex").slice(0, 32);
|
|
@@ -4784,14 +4833,17 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
4784
4833
|
this.requireSession();
|
|
4785
4834
|
const response = await this.http.get("/v1/teams", this.getCliRequestHeaders());
|
|
4786
4835
|
if (!response.ok) throw new Error(`Team list failed with HTTP ${response.status}`);
|
|
4787
|
-
|
|
4836
|
+
const teams = response.data.teams ?? [];
|
|
4837
|
+
await Promise.all(teams.map((team) => this.cacheTeamKeyFromRecord(team)));
|
|
4838
|
+
return teams;
|
|
4788
4839
|
}
|
|
4789
4840
|
async createTeam(input) {
|
|
4790
4841
|
this.requireSession();
|
|
4791
4842
|
const now = Math.floor(Date.now() / 1e3);
|
|
4792
4843
|
const teamKeyBytes = randomBytes3(32);
|
|
4844
|
+
const teamId = input.teamId ?? randomUUID5();
|
|
4793
4845
|
const payload = {
|
|
4794
|
-
team_id:
|
|
4846
|
+
team_id: teamId,
|
|
4795
4847
|
slug: input.slug ?? void 0,
|
|
4796
4848
|
encrypted_name: input.encryptedName ?? await encryptWithAesGcmCombined(input.name ?? "Untitled team", teamKeyBytes),
|
|
4797
4849
|
encrypted_description: input.encryptedDescription ?? (input.description ? await encryptWithAesGcmCombined(input.description, teamKeyBytes) : void 0),
|
|
@@ -4802,14 +4854,26 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
4802
4854
|
};
|
|
4803
4855
|
const response = await this.http.post("/v1/teams", payload, this.getCliRequestHeaders());
|
|
4804
4856
|
if (!response.ok || !response.data.team) throw new Error(`Team create failed with HTTP ${response.status}`);
|
|
4857
|
+
if (!input.encryptedTeamKey) {
|
|
4858
|
+
saveLocalTeamKey(this.requireSession().hashedEmail, teamId, bytesToBase64(teamKeyBytes));
|
|
4859
|
+
}
|
|
4805
4860
|
return response.data.team;
|
|
4806
4861
|
}
|
|
4807
4862
|
async getTeam(teamId) {
|
|
4808
4863
|
this.requireSession();
|
|
4809
4864
|
const response = await this.http.get(`/v1/teams/${encodeURIComponent(teamId)}`, this.getCliRequestHeaders());
|
|
4810
4865
|
if (!response.ok || !response.data.team) throw new Error(`Team get failed with HTTP ${response.status}`);
|
|
4866
|
+
await this.cacheTeamKeyFromRecord(response.data.team);
|
|
4811
4867
|
return response.data.team;
|
|
4812
4868
|
}
|
|
4869
|
+
async cacheTeamKeyFromRecord(team) {
|
|
4870
|
+
const session = this.requireSession();
|
|
4871
|
+
const teamId = typeof team.team_id === "string" ? team.team_id : null;
|
|
4872
|
+
const encryptedTeamKey = typeof team.encrypted_team_key === "string" ? team.encrypted_team_key : null;
|
|
4873
|
+
if (!teamId || !encryptedTeamKey || loadLocalTeamKey(session.hashedEmail, teamId)) return;
|
|
4874
|
+
const teamKey = await decryptBytesWithAesGcm(encryptedTeamKey, this.getMasterKeyBytes());
|
|
4875
|
+
if (teamKey) saveLocalTeamKey(session.hashedEmail, teamId, bytesToBase64(teamKey));
|
|
4876
|
+
}
|
|
4813
4877
|
async updateTeam(teamId, input) {
|
|
4814
4878
|
this.requireSession();
|
|
4815
4879
|
const response = await this.http.patch(`/v1/teams/${encodeURIComponent(teamId)}`, {
|
|
@@ -4825,21 +4889,81 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
4825
4889
|
if (!response.ok) throw new Error(`Team delete failed with HTTP ${response.status}`);
|
|
4826
4890
|
return { success: response.data.success === true };
|
|
4827
4891
|
}
|
|
4892
|
+
async loadTeamKeyBytes(teamId) {
|
|
4893
|
+
const session = this.requireSession();
|
|
4894
|
+
const localTeamKey = loadLocalTeamKey(session.hashedEmail, teamId);
|
|
4895
|
+
if (localTeamKey) return base64ToBytes(localTeamKey);
|
|
4896
|
+
const team = await this.getTeam(teamId);
|
|
4897
|
+
const encryptedTeamKey = typeof team.encrypted_team_key === "string" ? team.encrypted_team_key : null;
|
|
4898
|
+
if (!encryptedTeamKey) return null;
|
|
4899
|
+
const teamKey = await decryptBytesWithAesGcm(encryptedTeamKey, this.getMasterKeyBytes());
|
|
4900
|
+
if (!teamKey) return null;
|
|
4901
|
+
saveLocalTeamKey(session.hashedEmail, teamId, bytesToBase64(teamKey));
|
|
4902
|
+
return teamKey;
|
|
4903
|
+
}
|
|
4828
4904
|
async createTeamInvite(teamId, input) {
|
|
4829
4905
|
this.requireSession();
|
|
4830
|
-
const
|
|
4831
|
-
|
|
4906
|
+
const inviteId = typeof input.invite_id === "string" ? input.invite_id : randomUUID5();
|
|
4907
|
+
const payload = {
|
|
4908
|
+
invite_id: inviteId,
|
|
4832
4909
|
created_at: input.created_at ?? Math.floor(Date.now() / 1e3),
|
|
4833
4910
|
...input
|
|
4911
|
+
};
|
|
4912
|
+
const recipientEmail = typeof input.recipient_email === "string" ? input.recipient_email.trim().toLowerCase() : null;
|
|
4913
|
+
const explicitInviteSecret = typeof input.invite_secret === "string" ? input.invite_secret : null;
|
|
4914
|
+
const cachedTeamKey = loadLocalTeamKey(this.requireSession().hashedEmail, teamId);
|
|
4915
|
+
const inviteSecret = explicitInviteSecret ?? (recipientEmail && cachedTeamKey ? bytesToBase64Url(randomBytes3(32)) : null);
|
|
4916
|
+
if (recipientEmail && inviteSecret) {
|
|
4917
|
+
const teamKey = cachedTeamKey ? base64ToBytes(cachedTeamKey) : await this.loadTeamKeyBytes(teamId);
|
|
4918
|
+
if (!teamKey && explicitInviteSecret) throw new Error("Unable to load local team key for encrypted invite.");
|
|
4919
|
+
if (teamKey) {
|
|
4920
|
+
const origin = deriveAppUrl(this.apiUrl);
|
|
4921
|
+
const inviteKey = await deriveTeamInviteKey({ recipientEmail, inviteSecret, inviteId, teamId, origin });
|
|
4922
|
+
payload.encrypted_invite_team_key = await encryptBytesWithAesGcm(teamKey, inviteKey);
|
|
4923
|
+
payload.invite_key_kdf_context = {
|
|
4924
|
+
v: 1,
|
|
4925
|
+
kdf: "HKDF-SHA256",
|
|
4926
|
+
cipher: "AES-256-GCM",
|
|
4927
|
+
team_id: teamId,
|
|
4928
|
+
invite_id: inviteId,
|
|
4929
|
+
origin
|
|
4930
|
+
};
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
const response = await this.http.post(`/v1/teams/${encodeURIComponent(teamId)}/invites`, {
|
|
4934
|
+
...payload,
|
|
4935
|
+
invite_secret: void 0
|
|
4834
4936
|
}, this.getCliRequestHeaders());
|
|
4835
4937
|
if (!response.ok || !response.data.invite) throw new Error(`Team invite create failed with HTTP ${response.status}`);
|
|
4938
|
+
return inviteSecret ? { ...response.data.invite, invite_secret: inviteSecret, invite_url: `${deriveAppUrl(this.apiUrl)}/teams/invites/${inviteId}#key=${inviteSecret}` } : response.data.invite;
|
|
4939
|
+
}
|
|
4940
|
+
async getTeamInvite(inviteId) {
|
|
4941
|
+
this.requireSession();
|
|
4942
|
+
const response = await this.http.get(`/v1/teams/invites/${encodeURIComponent(inviteId)}`, this.getCliRequestHeaders());
|
|
4943
|
+
if (!response.ok || !response.data.invite) throw new Error(`Team invite get failed with HTTP ${response.status}`);
|
|
4836
4944
|
return response.data.invite;
|
|
4837
4945
|
}
|
|
4838
|
-
async acceptTeamInvite(inviteId) {
|
|
4946
|
+
async acceptTeamInvite(inviteId, input = {}) {
|
|
4839
4947
|
this.requireSession();
|
|
4840
|
-
const
|
|
4841
|
-
if (
|
|
4842
|
-
|
|
4948
|
+
const payload = { accepted_at: Math.floor(Date.now() / 1e3) };
|
|
4949
|
+
if (input.inviteSecret) {
|
|
4950
|
+
const invite = await this.getTeamInvite(inviteId);
|
|
4951
|
+
const context = invite.invite_key_kdf_context;
|
|
4952
|
+
const teamId = typeof context?.team_id === "string" ? context.team_id : null;
|
|
4953
|
+
const origin = typeof context?.origin === "string" ? context.origin : deriveAppUrl(this.apiUrl);
|
|
4954
|
+
const encryptedInviteTeamKey = typeof invite.encrypted_invite_team_key === "string" ? invite.encrypted_invite_team_key : null;
|
|
4955
|
+
if (!teamId || !encryptedInviteTeamKey) throw new Error("Team invite is missing encrypted key material.");
|
|
4956
|
+
if (!input.recipientEmail) throw new Error("Accepting an encrypted team invite requires --email <recipient-email>.");
|
|
4957
|
+
const recipientEmail = input.recipientEmail;
|
|
4958
|
+
const inviteKey = await deriveTeamInviteKey({ recipientEmail, inviteSecret: input.inviteSecret, inviteId, teamId, origin });
|
|
4959
|
+
const teamKey = await decryptBytesWithAesGcm(encryptedInviteTeamKey, inviteKey);
|
|
4960
|
+
if (!teamKey) throw new Error("Unable to decrypt team invite key.");
|
|
4961
|
+
payload.encrypted_team_key = await encryptBytesWithAesGcm(teamKey, this.getMasterKeyBytes());
|
|
4962
|
+
saveLocalTeamKey(this.requireSession().hashedEmail, teamId, bytesToBase64(teamKey));
|
|
4963
|
+
}
|
|
4964
|
+
const response = await this.http.post(`/v1/teams/invites/${encodeURIComponent(inviteId)}/accept`, payload, this.getCliRequestHeaders());
|
|
4965
|
+
if (!response.ok || !response.data.access_request && !response.data.membership) throw new Error(`Team invite accept failed with HTTP ${response.status}`);
|
|
4966
|
+
return { access_request: response.data.access_request, membership: response.data.membership, status: response.data.status, status_label: response.data.status_label };
|
|
4843
4967
|
}
|
|
4844
4968
|
async declineTeamInvite(inviteId) {
|
|
4845
4969
|
this.requireSession();
|
|
@@ -5554,26 +5678,33 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
5554
5678
|
async decryptChatKey(encryptedChatKey, masterKey) {
|
|
5555
5679
|
return decryptBytesWithAesGcm(encryptedChatKey, masterKey);
|
|
5556
5680
|
}
|
|
5557
|
-
|
|
5681
|
+
getContextWrapperEncryptedChatKey(cache, chatId, teamId = null) {
|
|
5558
5682
|
const hashedChatId = createHash7("sha256").update(chatId).digest("hex");
|
|
5683
|
+
const hashedTeamId = teamId ? createHash7("sha256").update(teamId).digest("hex") : null;
|
|
5559
5684
|
const wrapper = (cache?.chatKeyWrappers ?? []).find(
|
|
5560
|
-
(entry) => entry.key_type === "master" && entry.hashed_chat_id === hashedChatId && typeof entry.encrypted_chat_key === "string"
|
|
5685
|
+
(entry) => entry.key_type === (teamId ? "team" : "master") && entry.hashed_chat_id === hashedChatId && (!hashedTeamId || entry.hashed_team_id === hashedTeamId) && typeof entry.encrypted_chat_key === "string"
|
|
5561
5686
|
);
|
|
5562
5687
|
return typeof wrapper?.encrypted_chat_key === "string" ? wrapper.encrypted_chat_key : null;
|
|
5563
5688
|
}
|
|
5564
|
-
async
|
|
5689
|
+
async getChatWrappingKey(teamId, masterKey) {
|
|
5690
|
+
if (!teamId) return masterKey;
|
|
5691
|
+
const teamKey = await this.loadTeamKeyBytes(teamId);
|
|
5692
|
+
if (!teamKey) throw new Error(`Team key not available locally for team '${teamId}'. Run 'openmates teams show ${teamId}' and try again.`);
|
|
5693
|
+
return teamKey;
|
|
5694
|
+
}
|
|
5695
|
+
async resolveChatKey(cache, cached, wrappingKey, teamId = null) {
|
|
5565
5696
|
const chatId = String(cached.details.id ?? "");
|
|
5566
|
-
const wrapperKey = chatId ? this.
|
|
5697
|
+
const wrapperKey = chatId ? this.getContextWrapperEncryptedChatKey(cache, chatId, teamId) : null;
|
|
5567
5698
|
const encryptedChatKey = wrapperKey ?? (typeof cached.details.encrypted_chat_key === "string" ? cached.details.encrypted_chat_key : null);
|
|
5568
|
-
return encryptedChatKey ? this.decryptChatKey(encryptedChatKey,
|
|
5699
|
+
return encryptedChatKey ? this.decryptChatKey(encryptedChatKey, wrappingKey) : null;
|
|
5569
5700
|
}
|
|
5570
5701
|
/**
|
|
5571
5702
|
* Decrypt a single chat record from the sync cache into a ChatListItem.
|
|
5572
5703
|
*/
|
|
5573
|
-
async decryptChatListItem(cached,
|
|
5704
|
+
async decryptChatListItem(cached, wrappingKey, cache = loadSyncCache(), teamId = null) {
|
|
5574
5705
|
const d = cached.details;
|
|
5575
5706
|
const id = String(d.id ?? "");
|
|
5576
|
-
const chatKeyBytes = await this.resolveChatKey(cache, cached,
|
|
5707
|
+
const chatKeyBytes = await this.resolveChatKey(cache, cached, wrappingKey, teamId);
|
|
5577
5708
|
const title = typeof d.encrypted_title === "string" && chatKeyBytes ? await decryptWithAesGcmCombined(d.encrypted_title, chatKeyBytes) : null;
|
|
5578
5709
|
const summary = typeof d.encrypted_chat_summary === "string" && chatKeyBytes ? await decryptWithAesGcmCombined(
|
|
5579
5710
|
d.encrypted_chat_summary,
|
|
@@ -5591,14 +5722,16 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
5591
5722
|
};
|
|
5592
5723
|
}
|
|
5593
5724
|
async listChats(limit = 10, page = 1, options = {}) {
|
|
5725
|
+
const teamId = this.resolveTeamContext(options);
|
|
5594
5726
|
const cache = await this.ensureSynced(false, [], options);
|
|
5595
5727
|
const masterKey = this.getMasterKeyBytes();
|
|
5728
|
+
const wrappingKey = await this.getChatWrappingKey(teamId, masterKey);
|
|
5596
5729
|
const total = cache.chats.length;
|
|
5597
5730
|
const offset = (page - 1) * limit;
|
|
5598
5731
|
const slice = cache.chats.slice(offset, offset + limit);
|
|
5599
5732
|
const output = [];
|
|
5600
5733
|
for (const chat of slice) {
|
|
5601
|
-
output.push(await this.decryptChatListItem(chat,
|
|
5734
|
+
output.push(await this.decryptChatListItem(chat, wrappingKey, cache, teamId));
|
|
5602
5735
|
}
|
|
5603
5736
|
return {
|
|
5604
5737
|
chats: output,
|
|
@@ -5760,12 +5893,14 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
5760
5893
|
};
|
|
5761
5894
|
}
|
|
5762
5895
|
async searchChats(query, options = {}) {
|
|
5896
|
+
const teamId = this.resolveTeamContext(options);
|
|
5763
5897
|
const cache = await this.ensureSynced(false, [], options);
|
|
5764
5898
|
const masterKey = this.getMasterKeyBytes();
|
|
5899
|
+
const wrappingKey = await this.getChatWrappingKey(teamId, masterKey);
|
|
5765
5900
|
const normalized = query.trim().toLowerCase();
|
|
5766
5901
|
const results = [];
|
|
5767
5902
|
for (const cached of cache.chats) {
|
|
5768
|
-
const item = await this.decryptChatListItem(cached,
|
|
5903
|
+
const item = await this.decryptChatListItem(cached, wrappingKey, cache, teamId);
|
|
5769
5904
|
const title = (item.title ?? "").toLowerCase();
|
|
5770
5905
|
const summary = (item.summary ?? "").toLowerCase();
|
|
5771
5906
|
const cat = (item.category ?? "").toLowerCase();
|
|
@@ -5788,8 +5923,10 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
5788
5923
|
* @param query Full UUID, 8-char short ID, or chat title.
|
|
5789
5924
|
*/
|
|
5790
5925
|
async getChatMessages(query, options = {}) {
|
|
5926
|
+
const teamId = this.resolveTeamContext(options);
|
|
5791
5927
|
const cache = await this.ensureSynced(true, [], options);
|
|
5792
5928
|
const masterKey = this.getMasterKeyBytes();
|
|
5929
|
+
const wrappingKey = await this.getChatWrappingKey(teamId, masterKey);
|
|
5793
5930
|
const normalized = query.trim().toLowerCase();
|
|
5794
5931
|
let found;
|
|
5795
5932
|
if (query === "__last__" || query.toLowerCase() === "last") {
|
|
@@ -5807,7 +5944,7 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
5807
5944
|
}
|
|
5808
5945
|
if (!found) {
|
|
5809
5946
|
for (const cached of cache.chats) {
|
|
5810
|
-
const item = await this.decryptChatListItem(cached,
|
|
5947
|
+
const item = await this.decryptChatListItem(cached, wrappingKey, cache, teamId);
|
|
5811
5948
|
const title = (item.title ?? "").toLowerCase();
|
|
5812
5949
|
if (title === normalized) {
|
|
5813
5950
|
found = cached;
|
|
@@ -5817,7 +5954,7 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
5817
5954
|
}
|
|
5818
5955
|
if (!found) {
|
|
5819
5956
|
for (const cached of cache.chats) {
|
|
5820
|
-
const item = await this.decryptChatListItem(cached,
|
|
5957
|
+
const item = await this.decryptChatListItem(cached, wrappingKey, cache, teamId);
|
|
5821
5958
|
const title = (item.title ?? "").toLowerCase();
|
|
5822
5959
|
if (title.includes(normalized)) {
|
|
5823
5960
|
found = cached;
|
|
@@ -5830,8 +5967,8 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
5830
5967
|
`Chat '${query}' not found. Try 'openmates chats search "${query}"' to browse matches.`
|
|
5831
5968
|
);
|
|
5832
5969
|
}
|
|
5833
|
-
const chatItem = await this.decryptChatListItem(found,
|
|
5834
|
-
const chatKeyBytes = await this.resolveChatKey(cache, found,
|
|
5970
|
+
const chatItem = await this.decryptChatListItem(found, wrappingKey, cache, teamId);
|
|
5971
|
+
const chatKeyBytes = await this.resolveChatKey(cache, found, wrappingKey, teamId);
|
|
5835
5972
|
const messages = [];
|
|
5836
5973
|
for (const raw of found.messages) {
|
|
5837
5974
|
const m = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
@@ -6145,11 +6282,12 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
6145
6282
|
const refreshChatId = String(cachedMatch?.details.id ?? chatId);
|
|
6146
6283
|
const cache = await this.ensureSynced(true, [refreshChatId], { teamId });
|
|
6147
6284
|
const masterKey = this.getMasterKeyBytes();
|
|
6285
|
+
const wrappingKey = await this.getChatWrappingKey(teamId, masterKey);
|
|
6148
6286
|
const found = cache.chats.find(
|
|
6149
6287
|
(c) => String(c.details.id ?? "") === chatId || String(c.details.id ?? "").startsWith(chatId)
|
|
6150
6288
|
);
|
|
6151
6289
|
if (!found) return [];
|
|
6152
|
-
const chatKeyBytes = await this.resolveChatKey(cache, found,
|
|
6290
|
+
const chatKeyBytes = await this.resolveChatKey(cache, found, wrappingKey, teamId);
|
|
6153
6291
|
if (!chatKeyBytes) return [];
|
|
6154
6292
|
const encSuggestions = typeof found.details.encrypted_follow_up_request_suggestions === "string" ? found.details.encrypted_follow_up_request_suggestions : null;
|
|
6155
6293
|
if (!encSuggestions) return [];
|
|
@@ -6230,6 +6368,7 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
6230
6368
|
let savedTurnId = null;
|
|
6231
6369
|
if (!params.incognito) {
|
|
6232
6370
|
const masterKey = this.getMasterKeyBytes();
|
|
6371
|
+
const wrappingKey = await this.getChatWrappingKey(teamId, masterKey);
|
|
6233
6372
|
if (isNewChat) {
|
|
6234
6373
|
chatKeyBytes = globalThis.crypto ? new Uint8Array(globalThis.crypto.getRandomValues(new Uint8Array(32))) : new Uint8Array(
|
|
6235
6374
|
(await import("crypto")).webcrypto.getRandomValues(
|
|
@@ -6238,7 +6377,7 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
6238
6377
|
);
|
|
6239
6378
|
encryptedChatKey = await encryptBytesWithAesGcm(
|
|
6240
6379
|
chatKeyBytes,
|
|
6241
|
-
|
|
6380
|
+
wrappingKey
|
|
6242
6381
|
);
|
|
6243
6382
|
} else {
|
|
6244
6383
|
const cache = loadSyncCache(teamId) ?? await this.ensureSynced(false, [], { teamId });
|
|
@@ -6249,7 +6388,7 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
6249
6388
|
baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
|
|
6250
6389
|
const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
|
|
6251
6390
|
if (encKey) {
|
|
6252
|
-
chatKeyBytes = await decryptBytesWithAesGcm(encKey,
|
|
6391
|
+
chatKeyBytes = await decryptBytesWithAesGcm(encKey, wrappingKey);
|
|
6253
6392
|
encryptedChatKey = encKey;
|
|
6254
6393
|
}
|
|
6255
6394
|
}
|
|
@@ -6867,6 +7006,7 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
6867
7006
|
ws,
|
|
6868
7007
|
jobs: pendingTaskUpdateJobs,
|
|
6869
7008
|
events: taskEvents,
|
|
7009
|
+
teamId,
|
|
6870
7010
|
activeChatId: chatId,
|
|
6871
7011
|
activeChatKeyBytes: chatKeyBytes,
|
|
6872
7012
|
fallbackUserMessageId: messageId,
|
|
@@ -6913,17 +7053,18 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
6913
7053
|
const handledJobIds = /* @__PURE__ */ new Set();
|
|
6914
7054
|
if (params.jobs.length === 0) return handledJobIds;
|
|
6915
7055
|
const masterKey = this.getMasterKeyBytes();
|
|
7056
|
+
const wrappingKey = await this.getChatWrappingKey(params.teamId ?? null, masterKey);
|
|
6916
7057
|
let decryptedTasksCache = null;
|
|
6917
7058
|
const eventByJobId = new Map(params.events.map((event) => [event.task_update_job_id, event]));
|
|
6918
7059
|
const resolveChatKey = async (targetChatId) => {
|
|
6919
7060
|
if (targetChatId === params.activeChatId && params.activeChatKeyBytes) return params.activeChatKeyBytes;
|
|
6920
7061
|
const findChat = (chats) => chats?.find((chat) => String(chat.details.id ?? "") === targetChatId);
|
|
6921
|
-
const targetChat = findChat(params.syncedChats) ?? findChat(loadSyncCache()?.chats);
|
|
7062
|
+
const targetChat = findChat(params.syncedChats) ?? findChat(loadSyncCache(params.teamId)?.chats);
|
|
6922
7063
|
const encryptedTargetChatKey = typeof targetChat?.details.encrypted_chat_key === "string" ? targetChat.details.encrypted_chat_key : null;
|
|
6923
7064
|
if (!encryptedTargetChatKey) {
|
|
6924
7065
|
throw new Error(`Encrypted chat key not found for task update job target '${targetChatId}'. Sync and try again.`);
|
|
6925
7066
|
}
|
|
6926
|
-
const targetChatKey = await decryptBytesWithAesGcm(encryptedTargetChatKey,
|
|
7067
|
+
const targetChatKey = await decryptBytesWithAesGcm(encryptedTargetChatKey, wrappingKey);
|
|
6927
7068
|
if (!targetChatKey) {
|
|
6928
7069
|
throw new Error(`Failed to decrypt chat key for task update job target '${targetChatId}'.`);
|
|
6929
7070
|
}
|
|
@@ -7598,6 +7739,21 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
7598
7739
|
}
|
|
7599
7740
|
return response.data;
|
|
7600
7741
|
}
|
|
7742
|
+
async getRaw(path, apiKey) {
|
|
7743
|
+
const headers = {
|
|
7744
|
+
...this.getCliRequestHeaders(),
|
|
7745
|
+
Accept: "image/svg+xml,application/octet-stream"
|
|
7746
|
+
};
|
|
7747
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
7748
|
+
const response = await this.http.getBinary(path, headers);
|
|
7749
|
+
if (!response.ok) {
|
|
7750
|
+
throw new Error(`Raw resource request failed with HTTP ${response.status}`);
|
|
7751
|
+
}
|
|
7752
|
+
return {
|
|
7753
|
+
contentType: response.headers.get("content-type") ?? "application/octet-stream",
|
|
7754
|
+
data: response.data
|
|
7755
|
+
};
|
|
7756
|
+
}
|
|
7601
7757
|
// -------------------------------------------------------------------------
|
|
7602
7758
|
// Travel: booking link resolution
|
|
7603
7759
|
// -------------------------------------------------------------------------
|
|
@@ -9788,6 +9944,7 @@ Required: ${schema.required.join(", ")}`
|
|
|
9788
9944
|
ws,
|
|
9789
9945
|
jobs: pendingTaskUpdateJobs,
|
|
9790
9946
|
events: [],
|
|
9947
|
+
teamId,
|
|
9791
9948
|
activeChatId: null,
|
|
9792
9949
|
activeChatKeyBytes: null,
|
|
9793
9950
|
fallbackUserMessageId: null,
|
|
@@ -9817,6 +9974,7 @@ Required: ${schema.required.join(", ")}`
|
|
|
9817
9974
|
async persistPendingAIResponsesFromSync(ws, chats, pendingResponses, teamId) {
|
|
9818
9975
|
if (pendingResponses.length === 0) return;
|
|
9819
9976
|
const masterKey = this.getMasterKeyBytes();
|
|
9977
|
+
const wrappingKey = await this.getChatWrappingKey(teamId ?? null, masterKey);
|
|
9820
9978
|
for (const pending of pendingResponses) {
|
|
9821
9979
|
const chatId = pending.chat_id;
|
|
9822
9980
|
const messageId = pending.message_id;
|
|
@@ -9840,7 +9998,7 @@ Required: ${schema.required.join(", ")}`
|
|
|
9840
9998
|
} catch {
|
|
9841
9999
|
}
|
|
9842
10000
|
}
|
|
9843
|
-
const chatKeyBytes = await this.resolveChatKey(loadSyncCache(teamId), chat,
|
|
10001
|
+
const chatKeyBytes = await this.resolveChatKey(loadSyncCache(teamId), chat, wrappingKey, teamId ?? null);
|
|
9844
10002
|
if (!chatKeyBytes) continue;
|
|
9845
10003
|
const completedAt = normalizeUnixSeconds(
|
|
9846
10004
|
pending.fired_at,
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -486,6 +486,16 @@ interface TeamCreateInput {
|
|
|
486
486
|
encryptedZeroBalance?: string;
|
|
487
487
|
createdAt?: number;
|
|
488
488
|
}
|
|
489
|
+
interface TeamInviteCreateInput extends Record<string, unknown> {
|
|
490
|
+
role?: Exclude<TeamRole, "owner">;
|
|
491
|
+
recipient_email?: string;
|
|
492
|
+
invite_id?: string;
|
|
493
|
+
invite_secret?: string;
|
|
494
|
+
}
|
|
495
|
+
interface TeamInviteAcceptInput {
|
|
496
|
+
inviteSecret?: string | null;
|
|
497
|
+
recipientEmail?: string | null;
|
|
498
|
+
}
|
|
489
499
|
type LearningModeAgeGroup = "under_10" | "10_12" | "13_15" | "16_18" | "adult";
|
|
490
500
|
interface LearningModeStatus {
|
|
491
501
|
enabled: boolean;
|
|
@@ -1214,12 +1224,15 @@ declare class OpenMatesClient {
|
|
|
1214
1224
|
listTeams(): Promise<TeamRecord[]>;
|
|
1215
1225
|
createTeam(input: TeamCreateInput): Promise<TeamRecord>;
|
|
1216
1226
|
getTeam(teamId: string): Promise<TeamRecord>;
|
|
1227
|
+
private cacheTeamKeyFromRecord;
|
|
1217
1228
|
updateTeam(teamId: string, input: Record<string, unknown>): Promise<TeamRecord>;
|
|
1218
1229
|
deleteTeam(teamId: string): Promise<{
|
|
1219
1230
|
success: boolean;
|
|
1220
1231
|
}>;
|
|
1221
|
-
|
|
1222
|
-
|
|
1232
|
+
private loadTeamKeyBytes;
|
|
1233
|
+
createTeamInvite(teamId: string, input: TeamInviteCreateInput): Promise<Record<string, unknown>>;
|
|
1234
|
+
getTeamInvite(inviteId: string): Promise<Record<string, unknown>>;
|
|
1235
|
+
acceptTeamInvite(inviteId: string, input?: TeamInviteAcceptInput): Promise<Record<string, unknown>>;
|
|
1223
1236
|
declineTeamInvite(inviteId: string): Promise<{
|
|
1224
1237
|
success: boolean;
|
|
1225
1238
|
}>;
|
|
@@ -1355,7 +1368,8 @@ declare class OpenMatesClient {
|
|
|
1355
1368
|
* encrypt/decrypt the chat's title, category, messages, etc.
|
|
1356
1369
|
*/
|
|
1357
1370
|
private decryptChatKey;
|
|
1358
|
-
private
|
|
1371
|
+
private getContextWrapperEncryptedChatKey;
|
|
1372
|
+
private getChatWrappingKey;
|
|
1359
1373
|
private resolveChatKey;
|
|
1360
1374
|
/**
|
|
1361
1375
|
* Decrypt a single chat record from the sync cache into a ChatListItem.
|
|
@@ -1557,6 +1571,10 @@ declare class OpenMatesClient {
|
|
|
1557
1571
|
fallbackToken?: string;
|
|
1558
1572
|
} | null>;
|
|
1559
1573
|
getCodeRunStatus(path: string, apiKey?: string): Promise<Record<string, unknown>>;
|
|
1574
|
+
getRaw(path: string, apiKey?: string): Promise<{
|
|
1575
|
+
contentType: string;
|
|
1576
|
+
data: Uint8Array;
|
|
1577
|
+
}>;
|
|
1560
1578
|
/**
|
|
1561
1579
|
* Look up a booking URL for a flight using its booking_token.
|
|
1562
1580
|
*
|
package/dist/index.js
CHANGED