openmates 0.27.0-alpha.62 → 0.27.0-alpha.63

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.
@@ -8,6 +8,7 @@ import {
8
8
  getCredentialStorageMode,
9
9
  installPrivacyModel,
10
10
  loadAnonymousId,
11
+ loadCachedDirectChatAccountKey,
11
12
  loadLocalTeamKey,
12
13
  loadSession,
13
14
  loadStoredSessionApiUrl,
@@ -25,6 +26,7 @@ import {
25
26
  readPrivacyPreferences,
26
27
  resolveStateDir,
27
28
  saveAnonymousId,
29
+ saveCachedDirectChatAccountKey,
28
30
  saveLocalTeamKey,
29
31
  saveSession,
30
32
  saveSyncCache,
@@ -34,7 +36,7 @@ import {
34
36
  verifyInstalledModel,
35
37
  verifyPrivacyRuntime,
36
38
  withSessionRefreshLock
37
- } from "./chunk-5SFXCJJ4.js";
39
+ } from "./chunk-Q6PGA2B3.js";
38
40
  import {
39
41
  RemoteAccessReplayGuard,
40
42
  createRemoteAccessHandshake,
@@ -1634,6 +1636,183 @@ async function prepareCliMessagePrivacy(text7, memories, existing = [], scope2 =
1634
1636
 
1635
1637
  // src/client.ts
1636
1638
  import { createHash as createHash13, randomBytes as randomBytes4, randomUUID as randomUUID9, timingSafeEqual } from "crypto";
1639
+
1640
+ // ../pairing-crypto/src/directChatCrypto.ts
1641
+ import nacl from "tweetnacl";
1642
+ var PREFIX = "OMDM1.";
1643
+ var MAX_ENVELOPE_LENGTH = 2048;
1644
+ var MAX_PAYLOAD_BYTES = 768;
1645
+ var BASE64URL = /^[A-Za-z0-9_-]+$/;
1646
+ var KEY_ID = /^[0-9a-f]{64}$/;
1647
+ var encoder = new TextEncoder();
1648
+ var decoder = new TextDecoder("utf-8", { fatal: true });
1649
+ function ownedBuffer(bytes2) {
1650
+ const copy = new Uint8Array(bytes2.length);
1651
+ copy.set(bytes2);
1652
+ return copy.buffer;
1653
+ }
1654
+ function directChatBase64Url(bytes2) {
1655
+ let binary = "";
1656
+ for (let offset = 0; offset < bytes2.length; offset += 8192) {
1657
+ binary += String.fromCharCode(...bytes2.subarray(offset, offset + 8192));
1658
+ }
1659
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
1660
+ }
1661
+ function directChatBase64UrlBytes(value, length, field3 = "key", maxLength = length ?? MAX_PAYLOAD_BYTES) {
1662
+ if (typeof value !== "string" || value.length < 1 || value.length > Math.ceil(maxLength * 4 / 3) + 2 || !BASE64URL.test(value)) {
1663
+ throw new Error(`Invalid direct-chat ${field3}`);
1664
+ }
1665
+ let decoded;
1666
+ try {
1667
+ const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - value.length % 4) % 4));
1668
+ decoded = Uint8Array.from(binary, (character) => character.charCodeAt(0));
1669
+ } catch {
1670
+ throw new Error(`Invalid direct-chat ${field3}`);
1671
+ }
1672
+ if (length !== void 0 && decoded.length !== length || decoded.length > maxLength || directChatBase64Url(decoded) !== value) throw new Error(`Invalid direct-chat ${field3}`);
1673
+ return decoded;
1674
+ }
1675
+ function boundedIdentity(identity3) {
1676
+ if (!identity3 || typeof identity3 !== "object") throw new Error("Invalid direct-chat identity");
1677
+ for (const value of [identity3.teamId, identity3.chatId, identity3.recipientHash]) {
1678
+ if (typeof value !== "string" || value.length < 1 || value.length > 128 || !/^[A-Za-z0-9_-]+$/.test(value)) {
1679
+ throw new Error("Invalid direct-chat identity");
1680
+ }
1681
+ }
1682
+ if (!KEY_ID.test(identity3.keyId)) throw new Error("Invalid direct-chat key ID");
1683
+ }
1684
+ function validNonzeroKey(value, field3) {
1685
+ if (value.length !== 32 || value.every((byte) => byte === 0)) throw new Error(`Invalid direct-chat ${field3}`);
1686
+ }
1687
+ function validatePublicKey(value) {
1688
+ const bytes2 = directChatBase64UrlBytes(value, 32, "public key");
1689
+ validNonzeroKey(bytes2, "public key");
1690
+ const probe = nacl.scalarMult(new Uint8Array(32).fill(7), bytes2);
1691
+ if (probe.every((byte) => byte === 0)) throw new Error("Invalid direct-chat public key");
1692
+ return bytes2;
1693
+ }
1694
+ async function directChatKeyId(publicKey) {
1695
+ const bytes2 = validatePublicKey(publicKey);
1696
+ const digest4 = new Uint8Array(await crypto.subtle.digest("SHA-256", ownedBuffer(bytes2)));
1697
+ return [...digest4].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1698
+ }
1699
+ async function generateIdentity() {
1700
+ const pair = nacl.box.keyPair();
1701
+ const publicKey = directChatBase64Url(pair.publicKey);
1702
+ return { publicKey, privateKey: directChatBase64Url(pair.secretKey), keyId: await directChatKeyId(publicKey) };
1703
+ }
1704
+ async function sealDirectChatKey(rawKey, identity3, recipientPublicKey) {
1705
+ boundedIdentity(identity3);
1706
+ validNonzeroKey(rawKey, "chat key");
1707
+ const recipient = validatePublicKey(recipientPublicKey);
1708
+ if (identity3.keyId !== await directChatKeyId(recipientPublicKey)) throw new Error("Direct-chat recipient key ID mismatch");
1709
+ const ephemeral = nacl.box.keyPair();
1710
+ const nonce = nacl.randomBytes(24);
1711
+ const payload = encoder.encode(JSON.stringify({
1712
+ v: 1,
1713
+ team_id: identity3.teamId,
1714
+ chat_id: identity3.chatId,
1715
+ recipient_hash: identity3.recipientHash,
1716
+ key_id: identity3.keyId,
1717
+ key: directChatBase64Url(rawKey)
1718
+ }));
1719
+ const ciphertext = nacl.box(payload, nonce, recipient, ephemeral.secretKey);
1720
+ const envelope = PREFIX + directChatBase64Url(encoder.encode(JSON.stringify({
1721
+ v: 1,
1722
+ ephemeral_public_key: directChatBase64Url(ephemeral.publicKey),
1723
+ nonce: directChatBase64Url(nonce),
1724
+ ciphertext: directChatBase64Url(ciphertext)
1725
+ })));
1726
+ ephemeral.secretKey.fill(0);
1727
+ if (envelope.length > MAX_ENVELOPE_LENGTH) throw new Error("Direct-chat envelope too large");
1728
+ return envelope;
1729
+ }
1730
+ async function openDirectChatKey(envelope, expected, privateKey) {
1731
+ boundedIdentity(expected);
1732
+ if (typeof envelope !== "string" || !envelope.startsWith(PREFIX) || envelope.length > MAX_ENVELOPE_LENGTH) {
1733
+ throw new Error("Invalid direct-chat envelope");
1734
+ }
1735
+ const encoded = envelope.slice(PREFIX.length);
1736
+ const decoded = directChatBase64UrlBytes(encoded, void 0, "envelope");
1737
+ if (decoded.length > MAX_PAYLOAD_BYTES) throw new Error("Direct-chat envelope too large");
1738
+ let parsed;
1739
+ try {
1740
+ parsed = JSON.parse(decoder.decode(decoded));
1741
+ } catch {
1742
+ throw new Error("Invalid direct-chat envelope");
1743
+ }
1744
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Invalid direct-chat envelope");
1745
+ const outer = parsed;
1746
+ if (Object.keys(outer).sort().join(",") !== "ciphertext,ephemeral_public_key,nonce,v" || outer.v !== 1) {
1747
+ throw new Error("Invalid direct-chat envelope version or fields");
1748
+ }
1749
+ const sender = validatePublicKey(String(outer.ephemeral_public_key));
1750
+ const nonce = directChatBase64UrlBytes(String(outer.nonce), 24, "nonce");
1751
+ const ciphertext = directChatBase64UrlBytes(String(outer.ciphertext), void 0, "ciphertext", 512);
1752
+ if (ciphertext.length < 17) throw new Error("Invalid direct-chat ciphertext");
1753
+ const secret = directChatBase64UrlBytes(privateKey, 32, "private key");
1754
+ validNonzeroKey(secret, "private key");
1755
+ const ownPublic = directChatBase64Url(nacl.scalarMult.base(secret));
1756
+ if (await directChatKeyId(ownPublic) !== expected.keyId) throw new Error("Direct-chat private key ID mismatch");
1757
+ const plaintext = nacl.box.open(ciphertext, nonce, sender, secret);
1758
+ if (!plaintext) throw new Error("Direct-chat key authentication failed");
1759
+ let inner;
1760
+ try {
1761
+ inner = JSON.parse(decoder.decode(plaintext));
1762
+ } catch {
1763
+ throw new Error("Invalid direct-chat key payload");
1764
+ }
1765
+ if (!inner || typeof inner !== "object" || Array.isArray(inner)) throw new Error("Invalid direct-chat key payload");
1766
+ const payload = inner;
1767
+ if (Object.keys(payload).sort().join(",") !== "chat_id,key,key_id,recipient_hash,team_id,v" || payload.v !== 1 || payload.team_id !== expected.teamId || payload.chat_id !== expected.chatId || payload.recipient_hash !== expected.recipientHash || payload.key_id !== expected.keyId) {
1768
+ throw new Error("Direct-chat key binding mismatch");
1769
+ }
1770
+ const key = directChatBase64UrlBytes(String(payload.key), 32, "chat key");
1771
+ validNonzeroKey(key, "chat key");
1772
+ return key;
1773
+ }
1774
+
1775
+ // src/directChats.ts
1776
+ function validatePublicChatEncryptionKey(value) {
1777
+ const row3 = value;
1778
+ if (!row3 || typeof row3.public_key !== "string" || typeof row3.key_id !== "string" || !/^[0-9a-f]{64}$/.test(row3.key_id)) {
1779
+ throw new Error("Invalid member chat encryption key");
1780
+ }
1781
+ directChatBase64UrlBytes(row3.public_key, 32, "public key");
1782
+ return { key_id: row3.key_id, public_key: row3.public_key };
1783
+ }
1784
+ async function validatePublicChatEncryptionKeyId(value) {
1785
+ const key = validatePublicChatEncryptionKey(value);
1786
+ if (await directChatKeyId(key.public_key) !== key.key_id) throw new Error("Member chat encryption key ID mismatch");
1787
+ return key;
1788
+ }
1789
+ async function recoverAccountChatIdentity(record10, masterKey) {
1790
+ if (record10.version !== 1 || record10.algorithm !== "x25519-xsalsa20poly1305") throw new Error("Unsupported account chat encryption key");
1791
+ const publicKey = await validatePublicChatEncryptionKeyId(record10);
1792
+ if (typeof record10.encrypted_private_key !== "string" || record10.encrypted_private_key.length > 2048) throw new Error("Invalid account chat private key backup");
1793
+ const encoded = record10.encrypted_private_key;
1794
+ if (!/^[A-Za-z0-9+/_-]+={0,2}$/.test(encoded)) throw new Error("Invalid account chat private key backup");
1795
+ const privateKey = await decryptWithAesGcmCombined(Buffer.from(encoded, "base64url").toString("base64"), masterKey);
1796
+ if (!privateKey) throw new Error("Could not decrypt account chat private key");
1797
+ const secret = directChatBase64UrlBytes(privateKey, 32, "private key");
1798
+ if (secret.every((byte) => byte === 0)) throw new Error("Invalid account chat private key");
1799
+ const nacl2 = (await import("tweetnacl")).default;
1800
+ if (directChatBase64Url(nacl2.scalarMult.base(secret)) !== publicKey.public_key) throw new Error("Account chat key pair mismatch");
1801
+ return { publicKey: publicKey.public_key, privateKey, keyId: publicKey.key_id };
1802
+ }
1803
+ async function createAccountChatIdentity(masterKey) {
1804
+ const identity3 = await generateIdentity();
1805
+ const backup = await encryptWithAesGcmCombined(identity3.privateKey, masterKey);
1806
+ return { identity: identity3, record: {
1807
+ version: 1,
1808
+ algorithm: "x25519-xsalsa20poly1305",
1809
+ key_id: identity3.keyId,
1810
+ public_key: identity3.publicKey,
1811
+ encrypted_private_key: Buffer.from(backup, "base64").toString("base64url")
1812
+ } };
1813
+ }
1814
+
1815
+ // src/client.ts
1637
1816
  import { arch, platform, release } from "os";
1638
1817
  import { createInterface as createInterface3 } from "readline/promises";
1639
1818
  import { stdin as stdin2, stdout } from "process";
@@ -2475,13 +2654,13 @@ var OpenMatesHttpClient = class {
2475
2654
  throw new Error("SSE response body is not readable");
2476
2655
  }
2477
2656
  const reader = response.body.getReader();
2478
- const decoder2 = new TextDecoder();
2657
+ const decoder3 = new TextDecoder();
2479
2658
  let buffer = "";
2480
2659
  try {
2481
2660
  while (true) {
2482
2661
  const { done, value } = await reader.read();
2483
2662
  if (done) break;
2484
- buffer += decoder2.decode(value, { stream: true });
2663
+ buffer += decoder3.decode(value, { stream: true });
2485
2664
  let separatorIndex = buffer.indexOf("\n\n");
2486
2665
  while (separatorIndex >= 0) {
2487
2666
  const rawEvent = buffer.slice(0, separatorIndex);
@@ -2701,12 +2880,12 @@ import { randomUUID as randomUUID3 } from "crypto";
2701
2880
 
2702
2881
  // ../ui/src/utils/projectIgnoredReadGrant.ts
2703
2882
  var GRANT_LIFETIME_MS = 5 * 60 * 1e3;
2704
- var encoder = new TextEncoder();
2883
+ var encoder2 = new TextEncoder();
2705
2884
  function validScope(scope2) {
2706
2885
  return [scope2.projectId, scope2.sourceId, scope2.requestId, scope2.chatId, scope2.operationId].every((value) => typeof value === "string" && value.length > 0 && value.length <= 128) && typeof scope2.path === "string" && scope2.path.length > 0 && scope2.path.length <= 4096 && !scope2.path.includes("\\") && !scope2.path.startsWith("/") && !/^[a-z]:/i.test(scope2.path) && !Array.from(scope2.path).some((value) => value.charCodeAt(0) < 32 || value.charCodeAt(0) === 127) && scope2.path.split("/").every((part) => part && part !== "." && part !== "..");
2707
2886
  }
2708
2887
  function material(grant) {
2709
- return encoder.encode(JSON.stringify([
2888
+ return encoder2.encode(JSON.stringify([
2710
2889
  "openmates-ignored-read-v1",
2711
2890
  grant.projectId,
2712
2891
  grant.sourceId,
@@ -9984,13 +10163,13 @@ import { client, ready, server } from "@serenity-kit/opaque";
9984
10163
  var PAIR_PIN_ALPHABET = "ABCDEFGHJKLMNPQRTUVWXY3468";
9985
10164
  var PIN_LENGTH = 6;
9986
10165
  var KSF = { "argon2id-custom": { memory: 8192, iterations: 3, parallelism: 1 } };
9987
- var encoder2 = new TextEncoder();
9988
- var decoder = new TextDecoder("utf-8", { fatal: true });
9989
- var BUNDLE_INFO = encoder2.encode("openmates-pair-v2/bundle");
9990
- var BASE64URL = /^[A-Za-z0-9_-]+$/;
10166
+ var encoder3 = new TextEncoder();
10167
+ var decoder2 = new TextDecoder("utf-8", { fatal: true });
10168
+ var BUNDLE_INFO = encoder3.encode("openmates-pair-v2/bundle");
10169
+ var BASE64URL2 = /^[A-Za-z0-9_-]+$/;
9991
10170
  var HEX_256 = /^[0-9a-f]{64}$/;
9992
10171
  var BUNDLE_KEYS = /* @__PURE__ */ new Set(["protocol_version", "master_key_exported", "grant_secret", "user_email_salt", "hashed_email", "user_id", "account_context"]);
9993
- function ownedBuffer(bytes2) {
10172
+ function ownedBuffer2(bytes2) {
9994
10173
  return bytes2;
9995
10174
  }
9996
10175
  function requireAscii(value, name, max = 128) {
@@ -10030,7 +10209,7 @@ function validateContext(context) {
10030
10209
  auto_logout_minutes: parsed[6]
10031
10210
  };
10032
10211
  if (createPairContext(fields) !== context) throw new Error("Noncanonical pairing context");
10033
- return { text: context, bytes: encoder2.encode(context), fields };
10212
+ return { text: context, bytes: encoder3.encode(context), fields };
10034
10213
  }
10035
10214
  function identifiers(context) {
10036
10215
  return { client: `openmates-pair-v2/client/${context}`, server: `openmates-pair-v2/server/${context}` };
@@ -10046,7 +10225,7 @@ function base64url(bytes2) {
10046
10225
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
10047
10226
  }
10048
10227
  function decodeBase64url(value, maxBytes) {
10049
- if (typeof value !== "string" || value.length < 1 || value.length > Math.ceil(maxBytes * 4 / 3) + 2 || !BASE64URL.test(value)) {
10228
+ if (typeof value !== "string" || value.length < 1 || value.length > Math.ceil(maxBytes * 4 / 3) + 2 || !BASE64URL2.test(value)) {
10050
10229
  throw new Error("Invalid pairing encoding");
10051
10230
  }
10052
10231
  let bytes2;
@@ -10073,7 +10252,7 @@ async function hashPairSecret(secret) {
10073
10252
  const bytes2 = decodeBase64url(secret, 32);
10074
10253
  if (bytes2.length !== 32) throw new Error("Invalid pairing secret");
10075
10254
  try {
10076
- const digest4 = new Uint8Array(await crypto.subtle.digest("SHA-256", ownedBuffer(bytes2)));
10255
+ const digest4 = new Uint8Array(await crypto.subtle.digest("SHA-256", ownedBuffer2(bytes2)));
10077
10256
  return [...digest4].map((byte) => byte.toString(16).padStart(2, "0")).join("");
10078
10257
  } finally {
10079
10258
  bytes2.fill(0);
@@ -10089,10 +10268,10 @@ async function deriveBundleKey(sessionKey, contextBytes) {
10089
10268
  const raw = decodeBase64url(sessionKey, 64);
10090
10269
  if (raw.length !== 64) throw new Error("Invalid pairing session key");
10091
10270
  try {
10092
- const salt = await crypto.subtle.digest("SHA-256", ownedBuffer(contextBytes));
10093
- const ikm = await crypto.subtle.importKey("raw", ownedBuffer(raw), "HKDF", false, ["deriveKey"]);
10271
+ const salt = await crypto.subtle.digest("SHA-256", ownedBuffer2(contextBytes));
10272
+ const ikm = await crypto.subtle.importKey("raw", ownedBuffer2(raw), "HKDF", false, ["deriveKey"]);
10094
10273
  return crypto.subtle.deriveKey(
10095
- { name: "HKDF", hash: "SHA-256", salt, info: ownedBuffer(BUNDLE_INFO) },
10274
+ { name: "HKDF", hash: "SHA-256", salt, info: ownedBuffer2(BUNDLE_INFO) },
10096
10275
  ikm,
10097
10276
  { name: "AES-GCM", length: 256 },
10098
10277
  false,
@@ -10168,13 +10347,13 @@ async function createPairReceiver(context, pin) {
10168
10347
  if (nonce.length !== 12) throw new Error("Invalid pairing IV");
10169
10348
  const ciphertext = decodeBase64url(encryptedBundle, 65536);
10170
10349
  const plaintext = new Uint8Array(await crypto.subtle.decrypt(
10171
- { name: "AES-GCM", iv: ownedBuffer(nonce), additionalData: ownedBuffer(binding.bytes), tagLength: 128 },
10350
+ { name: "AES-GCM", iv: ownedBuffer2(nonce), additionalData: ownedBuffer2(binding.bytes), tagLength: 128 },
10172
10351
  key,
10173
- ownedBuffer(ciphertext)
10352
+ ownedBuffer2(ciphertext)
10174
10353
  ));
10175
10354
  try {
10176
10355
  assertActive();
10177
- return validateBundle(JSON.parse(decoder.decode(plaintext)), binding.fields.authorizer_user_id);
10356
+ return validateBundle(JSON.parse(decoder2.decode(plaintext)), binding.fields.authorizer_user_id);
10178
10357
  } finally {
10179
10358
  nonce.fill(0);
10180
10359
  ciphertext.fill(0);
@@ -11366,6 +11545,8 @@ var OpenMatesClient = class _OpenMatesClient {
11366
11545
  session;
11367
11546
  http;
11368
11547
  explicitSession;
11548
+ directChatIdentity;
11549
+ directChatUserHash;
11369
11550
  constructor(options = {}) {
11370
11551
  this.explicitSession = options.session !== void 0;
11371
11552
  const diskSession = options.session ?? (options.loadStoredSession === false ? null : this.getValidSessionFromDisk());
@@ -11631,6 +11812,8 @@ var OpenMatesClient = class _OpenMatesClient {
11631
11812
  if (!response.ok) throw new Error(`Team list failed with HTTP ${response.status}`);
11632
11813
  if (!owner2.isCurrent()) throw new Error("Account changed while loading Teams.");
11633
11814
  const teams = response.data.teams ?? [];
11815
+ if (teams.length) await this.ensureDirectChatIdentity();
11816
+ if (!owner2.isCurrent()) throw new Error("Account changed while loading Teams.");
11634
11817
  await Promise.all(teams.map((team) => this.cacheTeamKeyFromRecord(team, owner2.isCurrent)));
11635
11818
  if (!owner2.isCurrent()) throw new Error("Account changed while loading Teams.");
11636
11819
  const teamIds = teams.map((team) => team.team_id).filter((teamId) => typeof teamId === "string" && teamId.length > 0);
@@ -11640,6 +11823,60 @@ var OpenMatesClient = class _OpenMatesClient {
11640
11823
  }
11641
11824
  return teams;
11642
11825
  }
11826
+ async openTeamDirectChat(teamId, recipientHash) {
11827
+ if (!teamId || !/^[0-9a-f]{64}$/.test(recipientHash)) throw new Error("A Team ID and member hash are required.");
11828
+ const owner2 = this.captureTeamAccountOwner();
11829
+ const [ownHash, ownIdentity] = await Promise.all([this.ownDirectChatHash(), this.ensureDirectChatIdentity()]);
11830
+ if (recipientHash === ownHash) throw new Error("Choose another Team member to message.");
11831
+ const cache = await this.ensureSynced(false, [], { teamId });
11832
+ if (!owner2.isCurrent()) throw new Error("Account changed while opening the direct chat.");
11833
+ const existing = cache.chats.find((row4) => row4.details.chat_visibility === "direct" && (!row4.details.team_id || row4.details.team_id === teamId) && (!row4.details.hashed_team_id || row4.details.hashed_team_id === computeSHA256(teamId)) && Array.isArray(row4.details.participant_user_hashes) && row4.details.participant_user_hashes.length === 2 && row4.details.participant_user_hashes.includes(ownHash) && row4.details.participant_user_hashes.includes(recipientHash));
11834
+ if (existing) {
11835
+ const wrapping2 = await this.getChatWrappingKey(teamId, this.getMasterKeyBytes());
11836
+ return this.decryptChatListItem(existing, wrapping2, cache, teamId);
11837
+ }
11838
+ const recipient = await this.http.get(
11839
+ `/v1/teams/${encodeURIComponent(teamId)}/members/${encodeURIComponent(recipientHash)}/chat-encryption-key`,
11840
+ this.getCliRequestHeaders()
11841
+ );
11842
+ if (recipient.status === 404) throw new Error("Direct message pending: this Team member has not enrolled a chat encryption key yet.");
11843
+ if (!recipient.ok) throw new Error(`Team member chat encryption key failed with HTTP ${recipient.status}`);
11844
+ const recipientKey = await validatePublicChatEncryptionKeyId(recipient.data);
11845
+ const chatId = randomUUID9();
11846
+ const chatKey = new Uint8Array(randomBytes4(32));
11847
+ const ownEnvelope = await sealDirectChatKey(chatKey, { teamId, chatId, recipientHash: ownHash, keyId: ownIdentity.keyId }, ownIdentity.publicKey);
11848
+ const recipientEnvelope = await sealDirectChatKey(chatKey, { teamId, chatId, recipientHash, keyId: recipientKey.key_id }, recipientKey.public_key);
11849
+ if (!owner2.isCurrent()) throw new Error("Account changed while opening the direct chat.");
11850
+ const response = await this.http.post(
11851
+ `/v1/teams/${encodeURIComponent(teamId)}/direct-chats`,
11852
+ {
11853
+ chat_id: chatId,
11854
+ recipient_user_hash: recipientHash,
11855
+ participant_key_envelopes: {
11856
+ [ownHash]: { key_id: ownIdentity.keyId, encrypted_chat_key: ownEnvelope },
11857
+ [recipientHash]: { key_id: recipientKey.key_id, encrypted_chat_key: recipientEnvelope }
11858
+ },
11859
+ encrypted_title: await encryptWithAesGcmCombined("Private Team message", chatKey)
11860
+ },
11861
+ this.getCliRequestHeaders()
11862
+ );
11863
+ if (!owner2.isCurrent()) throw new Error("Account changed while opening the direct chat.");
11864
+ if (!response.ok || !response.data.chat) throw new Error(`Direct chat creation failed with HTTP ${response.status}`);
11865
+ const details = response.data.chat;
11866
+ if (details.chat_visibility !== "direct" || details.team_id !== teamId || details.id !== chatId) {
11867
+ throw new Error("Direct chat creation returned mismatched metadata.");
11868
+ }
11869
+ const latest = loadSyncCache(teamId) ?? cache;
11870
+ const row3 = { details, messages: [] };
11871
+ saveSyncCache({
11872
+ ...latest,
11873
+ chats: [row3, ...latest.chats],
11874
+ loadedChatCount: latest.loadedChatCount + 1,
11875
+ totalChatCount: latest.totalChatCount + 1
11876
+ }, teamId);
11877
+ const wrapping = await this.getChatWrappingKey(teamId, this.getMasterKeyBytes());
11878
+ return this.decryptChatListItem(row3, wrapping, loadSyncCache(teamId), teamId);
11879
+ }
11643
11880
  async approveTeamName(name) {
11644
11881
  this.requireSession();
11645
11882
  const normalized = name.trim().toLowerCase();
@@ -11690,6 +11927,7 @@ var OpenMatesClient = class _OpenMatesClient {
11690
11927
  return response.data.team;
11691
11928
  }
11692
11929
  async getTeam(teamId) {
11930
+ await this.ensureDirectChatIdentity();
11693
11931
  const owner2 = this.captureTeamAccountOwner();
11694
11932
  const response = await this.http.get(`/v1/teams/${encodeURIComponent(teamId)}`, this.getCliRequestHeaders());
11695
11933
  if (!response.ok || !response.data.team) throw new Error(`Team get failed with HTTP ${response.status}`);
@@ -11739,6 +11977,7 @@ var OpenMatesClient = class _OpenMatesClient {
11739
11977
  }
11740
11978
  async listTeamMembers(teamId) {
11741
11979
  this.requireSession();
11980
+ await this.ensureDirectChatIdentity();
11742
11981
  const response = await this.http.get(`/v1/teams/${encodeURIComponent(teamId)}/members`, this.getCliRequestHeaders());
11743
11982
  if (!response.ok) throw new Error(`Team members failed with HTTP ${response.status}`);
11744
11983
  return Promise.all((response.data.members ?? []).map((member) => this.decryptTeamMember(teamId, member)));
@@ -12865,6 +13104,111 @@ var OpenMatesClient = class _OpenMatesClient {
12865
13104
  return response.data.user ?? {};
12866
13105
  });
12867
13106
  }
13107
+ async ownDirectChatHash() {
13108
+ const session = this.requireSession();
13109
+ const owner2 = this.captureTeamAccountOwner();
13110
+ const scope2 = `${session.apiUrl}:${session.sessionId}:${session.hashedEmail}:${session.masterKeyExportedB64}`;
13111
+ if (this.directChatUserHash?.scope !== scope2) {
13112
+ const cached = loadCachedDirectChatAccountKey(session, this.apiUrl);
13113
+ const promise = (cached ? Promise.resolve({ id: cached.accountId }) : this.whoAmI()).then((user2) => {
13114
+ const id = typeof user2.id === "string" ? user2.id : typeof user2.user_id === "string" ? user2.user_id : null;
13115
+ if (!id) throw new Error("Authenticated identity is required for direct chats.");
13116
+ if (!owner2.isCurrent()) throw new Error("Account changed while loading direct chat identity.");
13117
+ return createHash13("sha256").update(id).digest("hex");
13118
+ }).catch((error) => {
13119
+ if (!(error instanceof TypeError)) throw error;
13120
+ const cached2 = loadCachedDirectChatAccountKey(session, this.apiUrl);
13121
+ if (!cached2) throw error;
13122
+ if (!owner2.isCurrent()) throw new Error("Account changed while loading direct chat identity.");
13123
+ return createHash13("sha256").update(cached2.accountId).digest("hex");
13124
+ }).catch((error) => {
13125
+ if (this.directChatUserHash?.promise === promise) this.directChatUserHash = void 0;
13126
+ throw error;
13127
+ });
13128
+ this.directChatUserHash = { scope: scope2, promise };
13129
+ }
13130
+ return this.directChatUserHash.promise;
13131
+ }
13132
+ async ensureDirectChatIdentity() {
13133
+ const session = this.requireSession();
13134
+ const scope2 = `${session.apiUrl}:${session.sessionId}:${session.hashedEmail}:${session.masterKeyExportedB64}`;
13135
+ if (this.directChatIdentity?.scope !== scope2) {
13136
+ const promise = this.loadDirectChatIdentity().catch((error) => {
13137
+ if (this.directChatIdentity?.promise === promise) this.directChatIdentity = void 0;
13138
+ throw error;
13139
+ });
13140
+ this.directChatIdentity = { scope: scope2, promise };
13141
+ }
13142
+ return this.directChatIdentity.promise;
13143
+ }
13144
+ async loadDirectChatIdentity() {
13145
+ const owner2 = this.captureTeamAccountOwner();
13146
+ const masterKey = this.getMasterKeyBytes();
13147
+ const recover = async (record11) => {
13148
+ const identity4 = await recoverAccountChatIdentity(record11, masterKey);
13149
+ if (!owner2.isCurrent()) throw new Error("Account changed during chat encryption key recovery.");
13150
+ return identity4;
13151
+ };
13152
+ const local = loadCachedDirectChatAccountKey(this.requireSession(), this.apiUrl);
13153
+ if (local) return recover(local.record);
13154
+ const cacheVerifiedRecord = async (record11) => {
13155
+ try {
13156
+ const user2 = await this.whoAmI();
13157
+ if (!owner2.isCurrent()) return;
13158
+ const accountId = typeof user2.id === "string" ? user2.id : typeof user2.user_id === "string" ? user2.user_id : null;
13159
+ const trustedAccountId = loadTrustedAccountId();
13160
+ if (accountId && (trustedAccountId ? trustedAccountId === accountId : process.env.OPENMATES_ACCOUNT_GUARD !== "required")) {
13161
+ saveCachedDirectChatAccountKey(this.requireSession(), this.apiUrl, accountId, record11);
13162
+ }
13163
+ } catch {
13164
+ }
13165
+ };
13166
+ let existing;
13167
+ try {
13168
+ existing = await this.http.get(
13169
+ "/v1/account/chat-encryption-key",
13170
+ this.getCliRequestHeaders()
13171
+ );
13172
+ } catch (error) {
13173
+ if (!(error instanceof TypeError)) throw error;
13174
+ if (!owner2.isCurrent()) throw new Error("Account changed during chat encryption key recovery.");
13175
+ const cached = loadCachedDirectChatAccountKey(this.requireSession(), this.apiUrl);
13176
+ if (!cached) throw error;
13177
+ return recover(cached.record);
13178
+ }
13179
+ if (!existing.ok && existing.status !== 404) throw new Error(`Account chat encryption key failed with HTTP ${existing.status}`);
13180
+ if (!owner2.isCurrent()) throw new Error("Account changed during chat encryption key enrollment.");
13181
+ if (existing.data.key) {
13182
+ const identity4 = await recover(existing.data.key);
13183
+ await cacheVerifiedRecord(existing.data.key);
13184
+ if (!owner2.isCurrent()) throw new Error("Account changed during chat encryption key recovery.");
13185
+ return identity4;
13186
+ }
13187
+ const generated = await createAccountChatIdentity(masterKey);
13188
+ if (!owner2.isCurrent()) throw new Error("Account changed during chat encryption key enrollment.");
13189
+ const saved = await this.http.put(
13190
+ "/v1/account/chat-encryption-key",
13191
+ generated.record,
13192
+ this.getCliRequestHeaders()
13193
+ );
13194
+ if (saved.status === 409) {
13195
+ const current2 = await this.http.get(
13196
+ "/v1/account/chat-encryption-key",
13197
+ this.getCliRequestHeaders()
13198
+ );
13199
+ if (!owner2.isCurrent() || !current2.ok || !current2.data.key) throw new Error("Account chat encryption key enrollment conflicted. Retry.");
13200
+ const identity4 = await recover(current2.data.key);
13201
+ await cacheVerifiedRecord(current2.data.key);
13202
+ if (!owner2.isCurrent()) throw new Error("Account changed during chat encryption key recovery.");
13203
+ return identity4;
13204
+ }
13205
+ if (!owner2.isCurrent() || !saved.ok) throw new Error(`Account chat encryption key enrollment failed with HTTP ${saved.status}`);
13206
+ const record10 = saved.data.key ?? generated.record;
13207
+ const identity3 = await recover(record10);
13208
+ await cacheVerifiedRecord(record10);
13209
+ if (!owner2.isCurrent()) throw new Error("Account changed during chat encryption key recovery.");
13210
+ return identity3;
13211
+ }
12868
13212
  async getTopicPreferences() {
12869
13213
  const user2 = await this.whoAmI();
12870
13214
  return await this.decryptTopicPreferences(user2.encrypted_settings);
@@ -13231,6 +13575,20 @@ var OpenMatesClient = class _OpenMatesClient {
13231
13575
  }
13232
13576
  async resolveChatKey(cache, cached, wrappingKey, teamId = null) {
13233
13577
  const chatId = String(cached.details.id ?? "");
13578
+ if (cached.details.chat_visibility === "direct") {
13579
+ if (!teamId || !chatId || cached.details.team_id && cached.details.team_id !== teamId || cached.details.hashed_team_id && cached.details.hashed_team_id !== computeSHA256(teamId)) {
13580
+ throw new Error("Direct chat Team binding mismatch.");
13581
+ }
13582
+ const ownHash = await this.ownDirectChatHash();
13583
+ const participants = cached.details.participant_user_hashes;
13584
+ if (!Array.isArray(participants) || participants.length !== 2 || !participants.includes(ownHash)) {
13585
+ throw new Error("Direct chat participant binding mismatch.");
13586
+ }
13587
+ const encrypted = cached.details.encrypted_chat_key;
13588
+ if (typeof encrypted !== "string") throw new Error("Direct chat envelope unavailable.");
13589
+ const identity3 = await this.ensureDirectChatIdentity();
13590
+ return openDirectChatKey(encrypted, { teamId, chatId, recipientHash: ownHash, keyId: identity3.keyId }, identity3.privateKey);
13591
+ }
13234
13592
  const wrapperKey = chatId ? this.getContextWrapperEncryptedChatKey(cache, chatId, teamId) : null;
13235
13593
  const encryptedChatKey = wrapperKey ?? (typeof cached.details.encrypted_chat_key === "string" ? cached.details.encrypted_chat_key : null);
13236
13594
  return encryptedChatKey ? this.decryptChatKey(encryptedChatKey, wrappingKey) : null;
@@ -13262,6 +13620,8 @@ var OpenMatesClient = class _OpenMatesClient {
13262
13620
  const slug = chatKeyBytes && typeof d.encrypted_slug === "string" ? slugValue ?? "" : null;
13263
13621
  return {
13264
13622
  id,
13623
+ chatVisibility: d.chat_visibility === "direct" ? "direct" : teamId ? "team" : "personal",
13624
+ participantUserHashes: Array.isArray(d.participant_user_hashes) ? d.participant_user_hashes.filter((value) => typeof value === "string") : void 0,
13265
13625
  shortId: id.slice(0, 8),
13266
13626
  slug,
13267
13627
  title,
@@ -14408,17 +14768,40 @@ var OpenMatesClient = class _OpenMatesClient {
14408
14768
  const selectedTeamId = this.getActiveTeamId();
14409
14769
  const current2 = () => {
14410
14770
  const active = this.session;
14411
- if (!active || active.apiUrl !== owner2[0] || active.hashedEmail !== owner2[1] || active.sessionId !== owner2[2] || active.createdAt !== owner2[3] || active.masterKeyExportedB64 !== owner2[4] || options.preferCache && this.getActiveTeamId() !== selectedTeamId)
14771
+ if (!active || active.apiUrl !== owner2[0] || active.hashedEmail !== owner2[1] || active.sessionId !== owner2[2] || active.createdAt !== owner2[3] || active.masterKeyExportedB64 !== owner2[4] || this.getActiveTeamId() !== selectedTeamId || this.resolveTeamContext(options) !== teamId)
14412
14772
  throw new Error("Chat workspace changed");
14413
14773
  };
14414
- const cache = options.preferCache ? loadSyncCache(teamId) : await this.ensureSynced(false, [], options);
14774
+ let cache = options.preferCache ? loadSyncCache(teamId) : await this.ensureSynced(false, [], options);
14415
14775
  current2();
14416
14776
  if (!cache) throw new Error("Chat cache unavailable. Open the chat to refresh it.");
14777
+ const diskCacheAtStart = loadSyncCache(teamId);
14778
+ const diskChatIdsAtStart = diskCacheAtStart?.chats.map((chat) => String(chat.details.id));
14417
14779
  const localTeamKey = options.preferCache && teamId ? loadLocalTeamKey(owner2[1], teamId) : null;
14418
14780
  if (options.preferCache && teamId && !localTeamKey) throw new Error("Team key unavailable offline. Reconnect to refresh Team access.");
14419
14781
  const wrappingKey = localTeamKey ? base64ToBytes(localTeamKey) : await this.getChatWrappingKey(teamId, masterKey);
14420
14782
  current2();
14421
- const found = await this.resolveCachedChatForQuery(query, cache, wrappingKey, teamId);
14783
+ let found;
14784
+ let fetchedMetadata = false;
14785
+ try {
14786
+ found = await this.resolveCachedChatForQuery(query, cache, wrappingKey, teamId);
14787
+ } catch (error) {
14788
+ if (options.preferCache || !(error instanceof Error) || !/^Chat '.*' not found\./.test(error.message) || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(query)) throw error;
14789
+ const response2 = await this.http.post(
14790
+ this.appendTeamQuery("/v1/chats/metadata/batch", { teamId }),
14791
+ { chat_ids: [query] },
14792
+ this.getCliRequestHeaders()
14793
+ );
14794
+ current2();
14795
+ if (!response2.ok) throw new Error(`Chat metadata failed with HTTP ${response2.status}`);
14796
+ const details = response2.data.chats.find((chat) => String(chat.id ?? "") === query);
14797
+ if (!details) throw error;
14798
+ found = { details, messages: [] };
14799
+ fetchedMetadata = true;
14800
+ cache = { ...cache, chats: [...cache.chats, found], chatKeyWrappers: [
14801
+ ...cache.chatKeyWrappers ?? [],
14802
+ ...Array.isArray(details.chat_key_wrappers) ? details.chat_key_wrappers : []
14803
+ ] };
14804
+ }
14422
14805
  current2();
14423
14806
  const chatItem = await this.decryptChatListItem(found, wrappingKey, cache, teamId);
14424
14807
  current2();
@@ -14446,17 +14829,19 @@ var OpenMatesClient = class _OpenMatesClient {
14446
14829
  const rawMessages = Array.isArray(response.data.messages) ? response.data.messages : [];
14447
14830
  const messages = await this.decryptRawChatMessages(rawMessages, chatItem, chatKeyBytes, cache.embeds);
14448
14831
  current2();
14449
- if (options.preferCache && teamId && (options.direction ?? "latest") === "latest") {
14832
+ if ((options.direction ?? "latest") === "latest") {
14450
14833
  const latestCache = loadSyncCache(teamId);
14451
14834
  const row3 = latestCache?.chats.find((chat) => String(chat.details.id) === chatId);
14452
- if (row3 && row3.details.messages_v === found.details.messages_v && row3.details.encrypted_chat_key === found.details.encrypted_chat_key && row3.messages.length === found.messages.length && row3.messages.every((raw, index) => raw === found.messages[index])) {
14835
+ const sameVersion = row3 && row3.details.messages_v === found.details.messages_v && row3.details.encrypted_chat_key === found.details.encrypted_chat_key && row3.messages.length === found.messages.length && row3.messages.every((raw, index) => raw === found.messages[index]);
14836
+ if (latestCache && (sameVersion || fetchedMetadata && !row3 && diskCacheAtStart && latestCache.syncedAt === diskCacheAtStart.syncedAt && latestCache.totalChatCount === diskCacheAtStart.totalChatCount && latestCache.loadedChatCount === diskCacheAtStart.loadedChatCount && latestCache.chats.length === diskChatIdsAtStart?.length && latestCache.chats.every((chat, index) => String(chat.details.id) === diskChatIdsAtStart[index]))) {
14453
14837
  const canonicalId = (raw) => {
14454
14838
  const value = typeof raw === "string" ? JSON.parse(raw) : raw;
14455
14839
  return String(value.client_message_id ?? value.id ?? value.message_id ?? "");
14456
14840
  };
14457
- const existingById = new Map(row3.messages.map((raw, index) => [canonicalId(raw), index]));
14841
+ const existing = row3?.messages ?? [];
14842
+ const existingById = new Map(existing.map((raw, index) => [canonicalId(raw), index]));
14458
14843
  const overlap = rawMessages.find((raw) => !!canonicalId(raw) && existingById.has(canonicalId(raw)));
14459
- const older = response.data.has_more_before !== true ? [] : overlap ? row3.messages.slice(0, existingById.get(canonicalId(overlap))) : row3.messages;
14844
+ const older = response.data.has_more_before !== true ? [] : overlap ? existing.slice(0, existingById.get(canonicalId(overlap))) : existing;
14460
14845
  const byId = /* @__PURE__ */ new Map();
14461
14846
  for (const raw of [...older, ...rawMessages]) {
14462
14847
  const value = typeof raw === "string" ? JSON.parse(raw) : raw;
@@ -14466,8 +14851,21 @@ var OpenMatesClient = class _OpenMatesClient {
14466
14851
  }
14467
14852
  const merged = [...byId.values()].sort((a, b) => a.createdAt - b.createdAt).map((message) => message.raw);
14468
14853
  current2();
14469
- if (merged.length !== row3.messages.length || merged.some((raw, index) => raw !== row3.messages[index]))
14470
- saveSyncCache({ ...latestCache, chats: latestCache.chats.map((chat) => chat === row3 ? { ...row3, messages: merged } : chat) }, teamId);
14854
+ try {
14855
+ if (!row3) {
14856
+ const wrappers = Array.isArray(found.details.chat_key_wrappers) ? found.details.chat_key_wrappers : [];
14857
+ saveSyncCache({
14858
+ ...latestCache,
14859
+ chats: [...latestCache.chats, { details: found.details, messages: merged }],
14860
+ loadedChatCount: latestCache.loadedChatCount + 1,
14861
+ totalChatCount: Math.max(latestCache.totalChatCount, latestCache.loadedChatCount + 1),
14862
+ chatKeyWrappers: [...latestCache.chatKeyWrappers ?? [], ...wrappers]
14863
+ }, teamId);
14864
+ } else if (merged.length !== row3.messages.length || merged.some((raw, index) => raw !== row3.messages[index])) {
14865
+ saveSyncCache({ ...latestCache, chats: latestCache.chats.map((chat) => chat === row3 ? { ...row3, messages: merged } : chat) }, teamId);
14866
+ }
14867
+ } catch {
14868
+ }
14471
14869
  }
14472
14870
  }
14473
14871
  return {
@@ -15764,7 +16162,7 @@ var OpenMatesClient = class _OpenMatesClient {
15764
16162
  baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
15765
16163
  const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
15766
16164
  if (encKey) {
15767
- chatKeyBytes = await decryptBytesWithAesGcm(encKey, wrappingKey);
16165
+ chatKeyBytes = await this.resolveChatKey(cache, chat, wrappingKey, teamId);
15768
16166
  encryptedChatKey = encKey;
15769
16167
  if (!chatKeyBytes) throw new Error("Could not decrypt the saved chat key.");
15770
16168
  if (typeof chat.details.encrypted_title === "string") {
@@ -29019,6 +29417,7 @@ var OpenMates = class {
29019
29417
  sdkName;
29020
29418
  sdkSessionPromise;
29021
29419
  masterKeyPromise;
29420
+ chatIdentityPromise;
29022
29421
  constructor(options = {}) {
29023
29422
  this.apiKey = options.apiKey ?? process.env.OPENMATES_API_KEY;
29024
29423
  this.apiUrl = (options.apiUrl ?? DEFAULT_API_URL2).replace(/\/$/, "");
@@ -29123,6 +29522,34 @@ var OpenMates = class {
29123
29522
  sdkSession() {
29124
29523
  return this.getSdkSession();
29125
29524
  }
29525
+ async chatIdentity() {
29526
+ if (!this.chatIdentityPromise) this.chatIdentityPromise = this.loadChatIdentity().catch((error) => {
29527
+ this.chatIdentityPromise = void 0;
29528
+ throw error;
29529
+ });
29530
+ return this.chatIdentityPromise;
29531
+ }
29532
+ async loadChatIdentity() {
29533
+ const masterKey = await this.masterKey();
29534
+ let response;
29535
+ try {
29536
+ response = await this.get("/v1/account/chat-encryption-key");
29537
+ } catch (error) {
29538
+ if (!(error instanceof OpenMatesApiError) || error.status !== 404) throw error;
29539
+ response = { key: null };
29540
+ }
29541
+ if (response.key) return recoverAccountChatIdentity(response.key, masterKey);
29542
+ const generated = await createAccountChatIdentity(masterKey);
29543
+ try {
29544
+ const saved = await this.put("/v1/account/chat-encryption-key", generated.record);
29545
+ return recoverAccountChatIdentity(saved.key ?? generated.record, masterKey);
29546
+ } catch (error) {
29547
+ if (!(error instanceof OpenMatesApiError) || error.status !== 409) throw error;
29548
+ const current2 = await this.get("/v1/account/chat-encryption-key");
29549
+ if (!current2.key) throw new OpenMatesConfigError("Account chat encryption key enrollment conflicted; retry.");
29550
+ return recoverAccountChatIdentity(current2.key, masterKey);
29551
+ }
29552
+ }
29126
29553
  /** @internal Resource-key resolver shared by the Project namespace. */
29127
29554
  async _getProjectGrantKey(projectId) {
29128
29555
  const session = await this.getSdkSession();
@@ -29197,6 +29624,26 @@ var OpenMates = class {
29197
29624
  return { ...payload, chat: decryptedChat, messages, embeds };
29198
29625
  }
29199
29626
  async resolveLoadedChatKey(chat, chatKeyWrappers) {
29627
+ if (chat.chat_visibility === "direct") {
29628
+ const session = await this.sdkSession();
29629
+ const userId = session.user?.id;
29630
+ if (!userId) throw new OpenMatesConfigError("Authenticated identity is required for a direct chat");
29631
+ const ownHash = createHash14("sha256").update(userId).digest("hex");
29632
+ const participants = chat.participant_user_hashes;
29633
+ if (!Array.isArray(participants) || participants.length !== 2 || !participants.includes(ownHash)) {
29634
+ throw new OpenMatesConfigError("Direct chat participant binding mismatch");
29635
+ }
29636
+ const identity3 = await this.chatIdentity();
29637
+ if (typeof chat.team_id !== "string" || typeof chat.encrypted_chat_key !== "string") {
29638
+ throw new OpenMatesConfigError("Direct chat key metadata is incomplete");
29639
+ }
29640
+ return openDirectChatKey(chat.encrypted_chat_key, {
29641
+ teamId: chat.team_id,
29642
+ chatId: chat.id,
29643
+ recipientHash: ownHash,
29644
+ keyId: identity3.keyId
29645
+ }, identity3.privateKey);
29646
+ }
29200
29647
  const hashedChatId = createHash14("sha256").update(chat.id).digest("hex");
29201
29648
  const wrapper = (chatKeyWrappers ?? []).find(
29202
29649
  (entry) => entry.key_type === "master" && entry.hashed_chat_id === hashedChatId && typeof entry.encrypted_chat_key === "string"
@@ -32833,9 +33280,11 @@ var OpenMatesTeams = class {
32833
33280
  }
32834
33281
  async list() {
32835
33282
  const result = await this.client.get("/v1/teams");
33283
+ if (result.teams?.length) await this.client.chatIdentity();
32836
33284
  return result.teams ?? [];
32837
33285
  }
32838
33286
  async get(teamId) {
33287
+ await this.client.chatIdentity();
32839
33288
  const result = await this.client.get(`/v1/teams/${encodeURIComponent(teamId)}`);
32840
33289
  return result.team ?? result;
32841
33290
  }
@@ -32893,6 +33342,62 @@ var OpenMatesTeams = class {
32893
33342
  async members(teamId) {
32894
33343
  return this.client.get(`/v1/teams/${encodeURIComponent(teamId)}/members`);
32895
33344
  }
33345
+ async memberChatEncryptionKey(teamId, memberHash) {
33346
+ if (!/^[0-9a-f]{64}$/.test(memberHash)) throw new OpenMatesConfigError("Member hash must be a SHA-256 hex digest");
33347
+ try {
33348
+ const result = await this.client.get(
33349
+ `/v1/teams/${encodeURIComponent(teamId)}/members/${encodeURIComponent(memberHash)}/chat-encryption-key`
33350
+ );
33351
+ return validatePublicChatEncryptionKeyId(result);
33352
+ } catch (error) {
33353
+ if (error instanceof OpenMatesApiError && error.status === 404) {
33354
+ throw new OpenMatesConfigError("Direct message pending: this Team member has not enrolled a chat encryption key yet.");
33355
+ }
33356
+ throw error;
33357
+ }
33358
+ }
33359
+ async openDirectChat(teamId, recipientHash) {
33360
+ if (!teamId || !/^[0-9a-f]{64}$/.test(recipientHash)) throw new OpenMatesConfigError("A Team ID and member hash are required");
33361
+ const session = await this.client.sdkSession();
33362
+ const userId = session.user?.id;
33363
+ if (!userId) throw new OpenMatesConfigError("Authenticated identity is required for a direct chat");
33364
+ const ownHash = createHash14("sha256").update(userId).digest("hex");
33365
+ if (ownHash === recipientHash) throw new OpenMatesConfigError("Choose another Team member to message");
33366
+ const [ownIdentity, recipientKey] = await Promise.all([
33367
+ this.client.chatIdentity(),
33368
+ this.memberChatEncryptionKey(teamId, recipientHash)
33369
+ ]);
33370
+ const chatId = randomUUID10();
33371
+ const chatKey = new Uint8Array(randomBytes6(32));
33372
+ const ownEnvelope = await sealDirectChatKey(
33373
+ chatKey,
33374
+ { teamId, chatId, recipientHash: ownHash, keyId: ownIdentity.keyId },
33375
+ ownIdentity.publicKey
33376
+ );
33377
+ const recipientEnvelope = await sealDirectChatKey(
33378
+ chatKey,
33379
+ { teamId, chatId, recipientHash, keyId: recipientKey.key_id },
33380
+ recipientKey.public_key
33381
+ );
33382
+ const result = await this.client.request(
33383
+ `/v1/teams/${encodeURIComponent(teamId)}/direct-chats`,
33384
+ {
33385
+ chat_id: chatId,
33386
+ recipient_user_hash: recipientHash,
33387
+ participant_key_envelopes: {
33388
+ [ownHash]: { key_id: ownIdentity.keyId, encrypted_chat_key: ownEnvelope },
33389
+ [recipientHash]: { key_id: recipientKey.key_id, encrypted_chat_key: recipientEnvelope }
33390
+ },
33391
+ encrypted_title: await encryptWithAesGcmCombined("Private Team message", chatKey)
33392
+ }
33393
+ );
33394
+ if (!result.chat) throw new OpenMatesConfigError("Direct chat creation returned no metadata");
33395
+ const participants = result.chat.participant_user_hashes;
33396
+ if (result.chat.chat_visibility !== "direct" || result.chat.team_id !== teamId || result.chat.id !== chatId || !Array.isArray(participants) || participants.length !== 2 || !participants.includes(ownHash) || !participants.includes(recipientHash)) {
33397
+ throw new OpenMatesConfigError("Direct chat creation returned mismatched metadata");
33398
+ }
33399
+ return this.client.decryptChatMetadata(result.chat);
33400
+ }
32896
33401
  async decryptMemberProfile(teamKey, member) {
32897
33402
  const encrypted = member.encrypted_member_profile;
32898
33403
  if (typeof encrypted !== "string" || !encrypted) return { ...member, profile: null };
@@ -78429,6 +78934,14 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
78429
78934
  }
78430
78935
  },
78431
78936
  notifications: {
78937
+ team_direct_chat: {
78938
+ title: {
78939
+ text: "Private Team message"
78940
+ },
78941
+ body: {
78942
+ text: "You have a new private conversation in your Team."
78943
+ }
78944
+ },
78432
78945
  auto_logout: {
78433
78946
  title: {
78434
78947
  text: "Auto logout"
@@ -86624,6 +87137,18 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
86624
87137
  members_description: {
86625
87138
  text: "Manage your team members and their permissions."
86626
87139
  },
87140
+ direct_message_action: {
87141
+ text: "Message"
87142
+ },
87143
+ direct_message_scope: {
87144
+ text: "Only you and this member can read this chat."
87145
+ },
87146
+ direct_message_pending: {
87147
+ text: "Secure messaging setup is pending. Ask this member to open OpenMates."
87148
+ },
87149
+ direct_message_failed: {
87150
+ text: "Could not open a private message. Try again."
87151
+ },
86627
87152
  admins: {
86628
87153
  text: "Admins"
86629
87154
  },
@@ -99667,6 +100192,8 @@ var TUI_ACTIONS = [
99667
100192
  { label: "Show all workspace items", command: "/browse" },
99668
100193
  { label: "Use selected app skill", command: "/app-run" },
99669
100194
  { label: "Recent chats", command: "/chats" },
100195
+ { label: "Team members", command: "/members" },
100196
+ { label: "Message Team member", command: "/message" },
99670
100197
  { label: "Example chats", command: "/examples" },
99671
100198
  { label: "Projects", command: "/projects" },
99672
100199
  { label: "Tasks", command: "/tasks" },
@@ -102306,7 +102833,7 @@ function relativeTime(timestamp) {
102306
102833
  return `${Math.floor(seconds / 86400)}d ago`;
102307
102834
  }
102308
102835
  function renderStatus(state, width) {
102309
- return ["OpenMates", "", state.status ?? "Working..."].flatMap((line3) => wrap(line3, width));
102836
+ return ["OpenMates", "", ...(state.status ?? "Working...").split("\n")].flatMap((line3) => wrap(line3, width).map((text7) => line3 === "Message \xB7 Enter" ? pointerLine(text7, { kind: "key", name: "return" }) : line3 === "Next member \xB7 \u2193" ? pointerLine(text7, { kind: "key", name: "down" }) : text7));
102310
102837
  }
102311
102838
  function prepareMessageContent(content, width, state, questionBlocks = true) {
102312
102839
  const segments2 = parseMessageSegments(content, { preserveCodeFences: true }).map((segment) => segment.type === "embed" ? segment : {
@@ -105034,6 +105561,19 @@ function createChatModelPreferences(client2) {
105034
105561
  }
105035
105562
 
105036
105563
  // src/tui.ts
105564
+ var teamMemberActions = /* @__PURE__ */ new WeakMap();
105565
+ function showSelectedTeamMember(state) {
105566
+ const selection = teamMemberActions.get(state);
105567
+ if (!selection?.members.length) {
105568
+ state.status = "No other active Team members are available.";
105569
+ return;
105570
+ }
105571
+ const member = selection.members[state.selectedIndex] ?? selection.members[0];
105572
+ state.status = `Team members ${state.selectedIndex + 1}/${selection.members.length}
105573
+ ${member.label}
105574
+ Message \xB7 Enter
105575
+ Next member \xB7 \u2193`;
105576
+ }
105037
105577
  function defaultModeForStreams(input, output) {
105038
105578
  return input.isTTY === true && output.isTTY === true ? "tui" : "quickstart";
105039
105579
  }
@@ -105316,6 +105856,21 @@ async function handleKey(params) {
105316
105856
  modelShell,
105317
105857
  mentionShell
105318
105858
  };
105859
+ const memberActions = teamMemberActions.get(state);
105860
+ if (state.screen === "status" && memberActions && !memberActions.isCurrent()) teamMemberActions.delete(state);
105861
+ if (state.screen === "status" && memberActions && memberActions.isCurrent() && memberActions.teamId === client2.getActiveTeamId()) {
105862
+ if (memberActions.members.length > 0 && (key.name === "up" || key.name === "down")) {
105863
+ state.selectedIndex = (state.selectedIndex + (key.name === "down" ? 1 : -1) + memberActions.members.length) % memberActions.members.length;
105864
+ showSelectedTeamMember(state);
105865
+ render();
105866
+ return;
105867
+ }
105868
+ if (key.name === "return" && !state.input.trim()) {
105869
+ const member = memberActions.members[state.selectedIndex];
105870
+ if (member) await handleCommand({ command: `/message ${member.hash}`, state, client: client2, terminal, render, finish, modelShell });
105871
+ return;
105872
+ }
105873
+ }
105319
105874
  if (key.name === "mouseclick") {
105320
105875
  if (!key.mouse || key.ctrl || key.meta || key.shift) return;
105321
105876
  const action2 = pointerTargetAt(state, key.mouse.column, key.mouse.row, terminal.width, terminal.height);
@@ -105491,6 +106046,50 @@ async function handleCommand(params) {
105491
106046
  rememberDraft(state);
105492
106047
  state.input = "";
105493
106048
  state.inputCursor = null;
106049
+ if (name === "/members") {
106050
+ const teamId = client2.getActiveTeamId();
106051
+ if (!teamId) {
106052
+ state.status = "Select a Team to view members.";
106053
+ state.screen = "status";
106054
+ render();
106055
+ return;
106056
+ }
106057
+ const isCurrent = captureTuiWorkspaceOwner(client2);
106058
+ const members = await client2.listTeamMembers(teamId);
106059
+ if (!isCurrent() || client2.getActiveTeamId() !== teamId) return;
106060
+ const user2 = await client2.whoAmI();
106061
+ if (!isCurrent() || client2.getActiveTeamId() !== teamId) return;
106062
+ const ownId = typeof user2.id === "string" ? user2.id : typeof user2.user_id === "string" ? user2.user_id : "";
106063
+ teamMemberActions.set(state, { teamId, isCurrent, members: members.filter((member) => member.status === "active" && member.hashed_user_id && member.user_id !== ownId).map((member) => ({ hash: member.hashed_user_id, label: `${member.profile?.display_name || member.user_id} \xB7 ${member.role}` })) });
106064
+ state.selectedIndex = 0;
106065
+ state.screen = "status";
106066
+ showSelectedTeamMember(state);
106067
+ render();
106068
+ return;
106069
+ }
106070
+ if (name === "/message") {
106071
+ const teamId = client2.getActiveTeamId();
106072
+ if (!teamId) {
106073
+ state.status = "Select a Team to message a member.";
106074
+ state.screen = "status";
106075
+ render();
106076
+ return;
106077
+ }
106078
+ const memberHash = parts[0];
106079
+ if (!memberHash) {
106080
+ state.status = "Use /members to find a member, then /message <member-hash>.";
106081
+ state.screen = "status";
106082
+ render();
106083
+ return;
106084
+ }
106085
+ const chat = await client2.openTeamDirectChat(teamId, memberHash);
106086
+ if (client2.getActiveTeamId() !== teamId) return;
106087
+ state.recentChats = [chat, ...state.recentChats.filter((item) => item.id !== chat.id)];
106088
+ await openSavedChat({ state, client: client2, terminal, render, command: async () => {
106089
+ }, send: async () => {
106090
+ } }, chat.id);
106091
+ return;
106092
+ }
105494
106093
  if (name === "/privacy") {
105495
106094
  const action2 = parts[0] ?? "status";
105496
106095
  if (action2 === "later") {
@@ -115342,24 +115941,33 @@ async function handleTeams(client2, subcommand, rest, flags) {
115342
115941
  else printTeamRecord(team, client2.getActiveTeamId());
115343
115942
  return;
115344
115943
  }
115345
- if (subcommand === "members") {
115944
+ if (subcommand === "members" || subcommand === "member") {
115346
115945
  const action2 = rest[0];
115347
115946
  const teamId = requiredStringFlag(rest[1], "<team-id>");
115947
+ if (action2 === "message") {
115948
+ const memberHash = requiredStringFlag(rest[2], "<member-hash>");
115949
+ const chat = await client2.openTeamDirectChat(teamId, memberHash);
115950
+ const message = rest.slice(3).join(" ").trim();
115951
+ const response = message ? await client2.sendMessage({ message, chatId: chat.id, teamId }) : void 0;
115952
+ if (flags.json === true) printJson2({ chat_id: chat.id, chat, ...response ? { response } : {} });
115953
+ else console.log(message ? `Message sent in direct chat ${chat.id}` : `Direct chat: ${chat.id}`);
115954
+ return;
115955
+ }
115348
115956
  if (action2 === "list") {
115349
115957
  const members = (await client2.listTeamMembers(teamId)).map(publicTeamMember);
115350
115958
  if (flags.json === true) printJson2({ members });
115351
115959
  else if (!members.length) console.log("No Team members.");
115352
- else for (const member of members) console.log(`${member.display_name || member.user_id} ${member.role} ${member.status || ""} ${member.user_id}`.trim());
115960
+ else for (const member of members) console.log(`${member.display_name || member.user_id} ${member.role} ${member.status || ""} ${member.hashed_user_id || member.user_id}`.trim());
115353
115961
  return;
115354
115962
  }
115355
115963
  if (action2 === "show") {
115356
115964
  const userId = requiredStringFlag(rest[2], "<member-user-id>");
115357
115965
  const member = publicTeamMember(await client2.getTeamMember(teamId, userId));
115358
115966
  if (flags.json === true) printJson2({ member });
115359
- else console.log(`${member.display_name || member.user_id} ${member.role} ${member.status || ""} ${member.user_id}`.trim());
115967
+ else console.log(`${member.display_name || member.user_id} ${member.role} ${member.status || ""} ${member.hashed_user_id || member.user_id}`.trim());
115360
115968
  return;
115361
115969
  }
115362
- throw new Error("Use teams members list <team-id> or teams members show <team-id> <member-user-id>.");
115970
+ throw new Error("Use teams members list, show, or message <team-id> <member-hash> [text].");
115363
115971
  }
115364
115972
  if (subcommand === "profile") {
115365
115973
  const action2 = rest[0];
@@ -124613,6 +125221,7 @@ function printTeamsHelp() {
124613
125221
  openmates teams role <team-id> --user <user-id> --role admin|member|viewer [--json]
124614
125222
  openmates teams members list <team-id> [--json]
124615
125223
  openmates teams members show <team-id> <member-user-id> [--json]
125224
+ openmates teams member message <team-id> <member-hash> [text] [--json]
124616
125225
  openmates teams profile show <team-id> [--json]
124617
125226
  openmates teams profile edit <team-id> [--name <display-name>] [--avatar <value>] [--json]
124618
125227
  openmates teams remove-member <team-id> --user <user-id> [--json]