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.
@@ -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
- return response.data.teams ?? [];
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: input.teamId ?? randomUUID5(),
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 response = await this.http.post(`/v1/teams/${encodeURIComponent(teamId)}/invites`, {
4831
- invite_id: input.invite_id ?? randomUUID5(),
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 response = await this.http.post(`/v1/teams/invites/${encodeURIComponent(inviteId)}/accept`, { accepted_at: Math.floor(Date.now() / 1e3) }, this.getCliRequestHeaders());
4841
- if (!response.ok || !response.data.access_request) throw new Error(`Team invite accept failed with HTTP ${response.status}`);
4842
- return { access_request: response.data.access_request, status_label: response.data.status_label };
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
- getMasterWrapperEncryptedChatKey(cache, chatId) {
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 resolveChatKey(cache, cached, masterKey) {
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.getMasterWrapperEncryptedChatKey(cache, chatId) : null;
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, masterKey) : null;
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, masterKey, cache = loadSyncCache()) {
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, masterKey);
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, masterKey, cache));
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, masterKey);
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, masterKey, cache);
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, masterKey);
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, masterKey, cache);
5834
- const chatKeyBytes = await this.resolveChatKey(cache, found, masterKey);
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, masterKey);
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
- masterKey
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, masterKey);
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, masterKey);
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, masterKey);
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
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getExtForLang,
4
4
  serializeToYaml
5
- } from "./chunk-LPJME5IC.js";
5
+ } from "./chunk-XG7OFIXM.js";
6
6
  import "./chunk-AXNRPVLE.js";
7
7
  export {
8
8
  getExtForLang,
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
- createTeamInvite(teamId: string, input: Record<string, unknown>): Promise<Record<string, unknown>>;
1222
- acceptTeamInvite(inviteId: string): Promise<Record<string, unknown>>;
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 getMasterWrapperEncryptedChatKey;
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
@@ -18,7 +18,7 @@ import {
18
18
  reconcileAuthoritativeChats,
19
19
  renderSupportInfo,
20
20
  serializeToYaml
21
- } from "./chunk-LPJME5IC.js";
21
+ } from "./chunk-XG7OFIXM.js";
22
22
  import "./chunk-AXNRPVLE.js";
23
23
  export {
24
24
  APP_SKILL_METADATA,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmates",
3
- "version": "0.15.0-alpha.29",
3
+ "version": "0.15.0-alpha.30",
4
4
  "description": "OpenMates CLI and SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",