@ai-matrx/messaging 0.11.9 → 0.12.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
1
  # Changelog — `@ai-matrx/messaging`
2
2
 
3
+ ## 0.12.0
4
+
5
+ **BREAKING — messaging reads the acting user from the session, never from you (DD-241).**
6
+
7
+ `<MessagingProvider userId>` and `createMessagingRepository({ identity })` are gone. Who is
8
+ acting is now read from the Supabase client you already pass, through its own
9
+ `auth.getSession()` and `auth.onAuthStateChange`.
10
+
11
+ ### Why
12
+
13
+ A user id handed across a package boundary is a COPY, and the only host that exists took its
14
+ copy from Redux at boot. Nothing rewrites that copy when the domain-wide auth cookie rotates to
15
+ another account, when the person signs out in another tab, or when a refresh returns a different
16
+ subject. In `@ai-matrx/meet` the same defect made reads fail at 403 in tabs 8.8 h and 32.9 h old
17
+ (DD-240, meet 0.6.0). Here it is worse, because in messaging the id is a **write** argument: it
18
+ lands on the wire as `p_user1_id`, `sender_id`, `created_by`, `updated_by` and
19
+ `.eq("user_id", …)`. A stale one does not merely get refused — where RLS lets it through it
20
+ writes a row attributed to somebody the tab cannot prove it is.
21
+
22
+ The same client that mints the JWT on every PostgREST request now answers "who is this?", so the
23
+ id in the argument and the id in the token cannot disagree.
24
+
25
+ ### Consumer action
26
+
27
+ 1. **Delete the `userId` prop** from `<MessagingProvider>`. Pass the browser Supabase client your
28
+ app signs in with — that client IS the identity now. One still passed at run time (a stale
29
+ bundle, a JavaScript consumer) is ignored and reported through `onDiagnostic` at `error` with
30
+ the remedy; it is never taken and never dropped in silence.
31
+ 2. **Replace `createMessagingRepository({ client, identity: { userId, organizationId } })` with
32
+ `createMessagingRepository({ client, organizationId })`.** A stray `identity` is likewise
33
+ ignored and reported through `onDiagnostic`.
34
+ 3. **A service that took a `currentUserId` argument no longer needs one** — and should stop
35
+ accepting one, because an argument nobody can supply safely is a door, not a convenience.
36
+ 4. `repository.identity` is replaced by `repository.organizationId` and the awaited
37
+ `repository.actingUserId()`.
38
+
39
+ ### Behaviour
40
+
41
+ - The provider is INERT until the first `getSession()` lands — nothing user-scoped is issued on
42
+ a guess, which is the page-load race that used to spend its first tick reading as a user the
43
+ tab could not prove it was.
44
+ - An account switch under an open tab rebuilds the runtime; a sign-out makes it inert.
45
+ - Nobody signed in is `session-unavailable` with a remedy, and it rides the existing single
46
+ `resolveSession` retry — a write landing between sign-in and token arrival heals instead of
47
+ becoming a red error.
48
+ - A client with no `auth` at all is a named `misconfigured` failure, never a silent "nobody".
49
+
50
+ ### Proof
51
+
52
+ `src/react/session-identity.test.tsx` (10 cases) asserts the ARGUMENT ON THE WIRE, and every case
53
+ still passes the 0.11.x shape with a different person's id in it: against the pre-fix source all
54
+ ten fail naming the handed-in copy; against this version all ten pass with the session's user.
55
+
3
56
  ## 0.11.9
4
57
 
5
58
  Automatic changed-only republish (docs/metadata drift since the last tag — see
package/README.md CHANGED
@@ -145,7 +145,6 @@ export default function MessagesPage() {
145
145
  return (
146
146
  <MessagingProvider
147
147
  client={supabase}
148
- userId={user.id}
149
148
  organizationId={org.id}
150
149
  >
151
150
  <MessagingInbox />
@@ -157,12 +156,20 @@ export default function MessagesPage() {
157
156
  That is realtime, presence, typing, optimistic send, the offline outbox, references, actionable
158
157
  messages, keyset pagination, and the whole polished UI. There is nothing else to wire.
159
158
 
159
+ 🚨 **You did not tell it who you are, and you cannot (0.12.0, DD-241).** The acting user is read
160
+ from `client`'s own session — the same session that mints the JWT on every request — so pass the
161
+ browser Supabase client your app signs in with. A user id handed across this boundary is a COPY,
162
+ and a copy goes stale the moment the auth cookie rotates; in messaging it would land on the wire
163
+ as `sender_id` and `created_by`, writing rows attributed to somebody the tab cannot prove it is.
164
+ The provider is inert until the first session read lands, an account switch rebuilds it, and a
165
+ sign-out makes it inert again. Same rule on the framework-free path:
166
+ `createMessagingRepository({ client, organizationId })` — no `identity`.
167
+
160
168
  ### Adding the AI
161
169
 
162
170
  ```tsx
163
171
  <MessagingProvider
164
172
  client={supabase}
165
- userId={user.id}
166
173
  organizationId={org.id}
167
174
  transport={matrxTransport} // from @ai-matrx/agents/matrx
168
175
  agents={{
@@ -218,7 +225,7 @@ recognizable), and a second one under it makes each manager's writes look remote
218
225
 
219
226
  ```tsx
220
227
  <RealtimeProvider client={supabase} actorId={user.id}> {/* the app's ONE mount */}
221
- <MessagingProvider client={supabase} userId={user.id} organizationId={org.id}>
228
+ <MessagingProvider client={supabase} organizationId={org.id}>
222
229
  …
223
230
  ```
224
231
 
package/dist/index.cjs CHANGED
@@ -26,6 +26,7 @@ __export(src_exports, {
26
26
  MESSAGING_SCHEMA: () => MESSAGING_SCHEMA,
27
27
  MessagingError: () => MessagingError,
28
28
  RPCS: () => RPCS,
29
+ SERVER_SESSION_IDENTITY: () => SERVER_SESSION_IDENTITY,
29
30
  TABLES: () => TABLES,
30
31
  asClientMessageId: () => asClientMessageId,
31
32
  asConversationId: () => asConversationId,
@@ -42,6 +43,7 @@ __export(src_exports, {
42
43
  createMessagingStore: () => createMessagingStore,
43
44
  createOutbox: () => createOutbox,
44
45
  createReadCache: () => createReadCache,
46
+ createSessionIdentity: () => createSessionIdentity,
45
47
  createWebOutboxStorage: () => createWebOutboxStorage,
46
48
  effectiveActorKey: () => effectiveActorKey,
47
49
  extractReferences: () => extractReferences,
@@ -1659,6 +1661,145 @@ function formatLastSeen(lastSeenMs, now = Date.now()) {
1659
1661
  return `Active ${(0, import_format.formatRelativeTime)(lastSeenMs, { style: "short", now })}`;
1660
1662
  }
1661
1663
 
1664
+ // src/core/session-identity.ts
1665
+ var RESOLVING = Object.freeze({
1666
+ state: "resolving",
1667
+ userId: null
1668
+ });
1669
+ var SERVER_SESSION_IDENTITY = RESOLVING;
1670
+ function authOf(client) {
1671
+ const candidate = client.auth;
1672
+ if (candidate === null || typeof candidate !== "object") return null;
1673
+ const auth = candidate;
1674
+ if (typeof auth.getSession !== "function") return null;
1675
+ if (typeof auth.onAuthStateChange !== "function") return null;
1676
+ return auth;
1677
+ }
1678
+ function userIdOf(session) {
1679
+ const id = session?.user?.id;
1680
+ return typeof id === "string" && id.length > 0 ? asUserId(id) : null;
1681
+ }
1682
+ var NO_AUTH_SOURCE = {
1683
+ message: "Messaging cannot tell who is signed in: the Supabase client it was given has no auth session source.",
1684
+ remedy: "Pass your app's browser Supabase client \u2014 the same client it signs in with \u2014 to <MessagingProvider client={...}> or createMessagingRepository({ client }). Messaging reads the acting user from that client's session; there is no prop or option for a user id, on purpose (DD-241)."
1685
+ };
1686
+ function createSessionIdentity(options) {
1687
+ const { client, onDiagnostic } = options;
1688
+ const listeners = /* @__PURE__ */ new Set();
1689
+ let current = RESOLVING;
1690
+ let disposed = false;
1691
+ const publish = (state, userId) => {
1692
+ if (disposed) return;
1693
+ if (current.state === state && current.userId === userId) return;
1694
+ current = Object.freeze({ state, userId });
1695
+ for (const listener of [...listeners]) listener();
1696
+ };
1697
+ const subscribeOnly = (hasAuthSource) => ({
1698
+ hasAuthSource,
1699
+ snapshot: () => current,
1700
+ subscribe: (listener) => {
1701
+ listeners.add(listener);
1702
+ return () => {
1703
+ listeners.delete(listener);
1704
+ };
1705
+ },
1706
+ dispose: () => {
1707
+ disposed = true;
1708
+ listeners.clear();
1709
+ }
1710
+ });
1711
+ const auth = authOf(client);
1712
+ if (auth === null) {
1713
+ onDiagnostic?.({ level: "error", ...NO_AUTH_SOURCE });
1714
+ current = Object.freeze({ state: "resolved", userId: null });
1715
+ return subscribeOnly(false);
1716
+ }
1717
+ void (async () => {
1718
+ try {
1719
+ const result = await auth.getSession();
1720
+ publish("resolved", userIdOf(result?.data?.session));
1721
+ } catch (error) {
1722
+ onDiagnostic?.({
1723
+ level: "warn",
1724
+ message: `Messaging could not read the signed-in session: ${String(
1725
+ error?.message ?? error
1726
+ )}`,
1727
+ remedy: "Messages stay unavailable on this device until the session can be read. Reload the page; if it persists, sign in again."
1728
+ });
1729
+ publish("resolved", null);
1730
+ }
1731
+ })();
1732
+ const subscription = auth.onAuthStateChange((_event, session) => {
1733
+ publish("resolved", userIdOf(session));
1734
+ });
1735
+ const base = subscribeOnly(true);
1736
+ return {
1737
+ hasAuthSource: true,
1738
+ snapshot: base.snapshot,
1739
+ subscribe: base.subscribe,
1740
+ dispose: () => {
1741
+ base.dispose();
1742
+ try {
1743
+ subscription.data.subscription.unsubscribe();
1744
+ } catch {
1745
+ }
1746
+ }
1747
+ };
1748
+ }
1749
+ function actingUserIdOf(identity) {
1750
+ return new Promise((resolve, reject) => {
1751
+ const settle = () => {
1752
+ const snapshot = identity.snapshot();
1753
+ if (snapshot.state !== "resolved") return false;
1754
+ if (snapshot.userId === null) {
1755
+ reject(identity.hasAuthSource ? noSignedInUserError() : noAuthSourceError());
1756
+ return true;
1757
+ }
1758
+ resolve(snapshot.userId);
1759
+ return true;
1760
+ };
1761
+ if (settle()) return;
1762
+ const unsubscribe = identity.subscribe(() => {
1763
+ if (settle()) unsubscribe();
1764
+ });
1765
+ });
1766
+ }
1767
+ function noSignedInUserError() {
1768
+ return new MessagingError(
1769
+ "session-unavailable",
1770
+ "Messaging has no signed-in user: the Supabase client's session names nobody.",
1771
+ "Sign in, then try again. Messaging takes the acting user from the session of the client it was given \u2014 never from an id handed in by the caller (DD-241), because a handed-in id goes stale the moment the auth cookie rotates and lands on the wire as sender_id and created_by."
1772
+ );
1773
+ }
1774
+ function noAuthSourceError() {
1775
+ return new MessagingError(
1776
+ "misconfigured",
1777
+ NO_AUTH_SOURCE.message,
1778
+ NO_AUTH_SOURCE.remedy
1779
+ );
1780
+ }
1781
+ async function readActingUserId(client) {
1782
+ const auth = authOf(client);
1783
+ if (auth === null) throw noAuthSourceError();
1784
+ let session;
1785
+ try {
1786
+ const result = await auth.getSession();
1787
+ session = result?.data?.session;
1788
+ } catch (error) {
1789
+ throw new MessagingError(
1790
+ "session-unavailable",
1791
+ `Messaging could not read the signed-in session: ${String(
1792
+ error?.message ?? error
1793
+ )}`,
1794
+ "Reload the page and try again; if it persists, sign in again.",
1795
+ error
1796
+ );
1797
+ }
1798
+ const userId = userIdOf(session);
1799
+ if (userId === null) throw noSignedInUserError();
1800
+ return userId;
1801
+ }
1802
+
1662
1803
  // src/core/repository.ts
1663
1804
  var MESSAGING_SCHEMA = "communication";
1664
1805
  var MESSAGING_RPC_SCHEMA = "public";
@@ -1695,8 +1836,15 @@ function sleep(ms) {
1695
1836
  return new Promise((resolve) => setTimeout(resolve, jittered));
1696
1837
  }
1697
1838
  function createMessagingRepository(options) {
1698
- const { identity } = options;
1699
- const org = requireOrg(identity.organizationId, "createMessagingRepository");
1839
+ const org = requireOrg(options.organizationId, "createMessagingRepository");
1840
+ const handedIn = options.identity;
1841
+ if (handedIn !== void 0 && handedIn !== null && handedIn.userId !== void 0) {
1842
+ options.onDiagnostic?.(
1843
+ "createMessagingRepository({ identity: { userId } }) is ignored: since 0.12.0 messaging reads the acting user from the session of the client it was given (DD-241). Remove the option and pass { client, organizationId } \u2014 a handed-in id goes stale the moment the auth cookie rotates, and it lands on the wire as sender_id and created_by."
1844
+ );
1845
+ }
1846
+ const sessionIdentity = options.sessionIdentity;
1847
+ const actingUserId = () => sessionIdentity !== void 0 ? actingUserIdOf(sessionIdentity) : readActingUserId(options.client);
1700
1848
  const userCache = createReadCache({
1701
1849
  ttlMs: options.userTtlMs ?? 5 * 6e4,
1702
1850
  ...options.now !== void 0 ? { now: options.now } : {}
@@ -1733,6 +1881,9 @@ function createMessagingRepository(options) {
1733
1881
  }
1734
1882
  }
1735
1883
  }
1884
+ function actingUser(operation) {
1885
+ return withSessionRetry(operation, () => actingUserId());
1886
+ }
1736
1887
  async function rpc(fn, args, operation) {
1737
1888
  const { data, error } = await rpcDb().rpc(fn, args);
1738
1889
  if (error !== null) throw normalizeMessagingError(error, operation);
@@ -1756,17 +1907,19 @@ function createMessagingRepository(options) {
1756
1907
  return found;
1757
1908
  }
1758
1909
  const repository = {
1759
- identity,
1910
+ organizationId: org,
1911
+ actingUserId,
1760
1912
  async listConversations(args = {}) {
1761
1913
  const limit = args.limit ?? 30;
1762
1914
  const archived = args.archived ?? DEFAULT_MESSAGING_ARCHIVE_FILTER;
1763
1915
  const operation = "listConversations";
1916
+ const me = await actingUser(operation);
1764
1917
  const rows = await withSessionRetry(
1765
1918
  operation,
1766
1919
  () => rpc(
1767
1920
  RPCS.conversationsWithDetails,
1768
1921
  {
1769
- p_user_id: identity.userId,
1922
+ p_user_id: me,
1770
1923
  p_limit: limit + 1,
1771
1924
  p_before_sort_at: args.cursor?.beforeSortAt ?? null,
1772
1925
  p_before_conversation_id: args.cursor?.beforeConversationId ?? null,
@@ -1783,7 +1936,7 @@ function createMessagingRepository(options) {
1783
1936
  throw invalidResponse(operation, `${RPCS.conversationsWithDetails} did not return rows`);
1784
1937
  }
1785
1938
  const projected = (rows ?? []).map(
1786
- (row) => projectConversationSummary(row, identity.userId, org)
1939
+ (row) => projectConversationSummary(row, me, org)
1787
1940
  );
1788
1941
  const hasMore = projected.length > limit;
1789
1942
  const items = hasMore ? projected.slice(0, limit) : projected;
@@ -1797,12 +1950,13 @@ function createMessagingRepository(options) {
1797
1950
  async countArchivedConversations(args = {}) {
1798
1951
  const limit = args.limit ?? 100;
1799
1952
  const operation = "countArchivedConversations";
1953
+ const me = await actingUser(operation);
1800
1954
  const rows = await withSessionRetry(
1801
1955
  operation,
1802
1956
  () => rpc(
1803
1957
  RPCS.conversationsWithDetails,
1804
1958
  {
1805
- p_user_id: identity.userId,
1959
+ p_user_id: me,
1806
1960
  p_limit: limit + 1,
1807
1961
  p_before_sort_at: null,
1808
1962
  p_before_conversation_id: null,
@@ -1819,6 +1973,7 @@ function createMessagingRepository(options) {
1819
1973
  },
1820
1974
  async getConversation(id) {
1821
1975
  const operation = "getConversation";
1976
+ const me = await actingUser(operation);
1822
1977
  const { data, error } = await withSessionRetry(
1823
1978
  operation,
1824
1979
  async () => db().from(TABLES.conversations).select(CONVERSATION_COLUMNS).eq("id", id).single()
@@ -1839,7 +1994,7 @@ function createMessagingRepository(options) {
1839
1994
  participants: [],
1840
1995
  unread_count: 0
1841
1996
  },
1842
- identity.userId,
1997
+ me,
1843
1998
  org
1844
1999
  );
1845
2000
  return summary.conversation;
@@ -1883,6 +2038,7 @@ function createMessagingRepository(options) {
1883
2038
  },
1884
2039
  async getOrCreateDirectConversation(otherUserId) {
1885
2040
  const operation = "getOrCreateDirectConversation";
2041
+ const me = await actingUser(operation);
1886
2042
  const id = await withSessionRetry(
1887
2043
  operation,
1888
2044
  () => rpc(
@@ -1891,7 +2047,7 @@ function createMessagingRepository(options) {
1891
2047
  // The RPC's own guard requires an `authenticated` caller to pass
1892
2048
  // THEMSELVES as user1; passing them in the other slot is a denial,
1893
2049
  // not a preference.
1894
- p_user1_id: identity.userId,
2050
+ p_user1_id: me,
1895
2051
  p_user2_id: otherUserId,
1896
2052
  p_organization_id: org
1897
2053
  },
@@ -1905,13 +2061,14 @@ function createMessagingRepository(options) {
1905
2061
  },
1906
2062
  async createGroupConversation({ name, memberIds }) {
1907
2063
  const operation = "createGroupConversation";
2064
+ const me = await actingUser(operation);
1908
2065
  const { data, error } = await withSessionRetry(
1909
2066
  operation,
1910
2067
  async () => db().from(TABLES.conversations).insert({
1911
2068
  type: "group",
1912
2069
  group_name: name,
1913
2070
  organization_id: org,
1914
- created_by: identity.userId
2071
+ created_by: me
1915
2072
  }).select("id").single()
1916
2073
  );
1917
2074
  if (error !== null) throw normalizeMessagingError(error, operation);
@@ -1919,14 +2076,14 @@ function createMessagingRepository(options) {
1919
2076
  if (typeof conversationId !== "string") {
1920
2077
  throw invalidResponse(operation, "insert returned no conversation id");
1921
2078
  }
1922
- const members = [.../* @__PURE__ */ new Set([identity.userId, ...memberIds])];
2079
+ const members = [.../* @__PURE__ */ new Set([me, ...memberIds])];
1923
2080
  const { error: memberError } = await db().from(TABLES.participants).insert(
1924
2081
  members.map((userId) => ({
1925
2082
  conversation_id: conversationId,
1926
2083
  user_id: userId,
1927
- role: userId === identity.userId ? "owner" : "member",
2084
+ role: userId === me ? "owner" : "member",
1928
2085
  organization_id: org,
1929
- created_by: identity.userId
2086
+ created_by: me
1930
2087
  }))
1931
2088
  );
1932
2089
  if (memberError !== null) throw normalizeMessagingError(memberError, operation);
@@ -1934,11 +2091,12 @@ function createMessagingRepository(options) {
1934
2091
  },
1935
2092
  async insertMessage(draft, clientMessageId) {
1936
2093
  const operation = "insertMessage";
2094
+ const me = await actingUser(operation);
1937
2095
  const { data, error } = await db().from(TABLES.messages).insert({
1938
2096
  conversation_id: draft.conversationId,
1939
- sender_id: identity.userId,
2097
+ sender_id: me,
1940
2098
  organization_id: org,
1941
- created_by: identity.userId,
2099
+ created_by: me,
1942
2100
  content: draft.content,
1943
2101
  message_type: draft.kind ?? "text",
1944
2102
  status: "sent",
@@ -1960,10 +2118,11 @@ function createMessagingRepository(options) {
1960
2118
  },
1961
2119
  async editMessage(id, content) {
1962
2120
  const operation = "editMessage";
2121
+ const me = await actingUser(operation);
1963
2122
  const { data, error } = await db().from(TABLES.messages).update({
1964
2123
  content,
1965
2124
  edited_at: (/* @__PURE__ */ new Date()).toISOString(),
1966
- updated_by: identity.userId
2125
+ updated_by: me
1967
2126
  }).eq("id", id).select(MESSAGE_COLUMNS).single();
1968
2127
  if (error !== null) throw normalizeMessagingError(error, operation);
1969
2128
  if (data === null) throw invalidResponse(operation, "update returned no row");
@@ -1971,47 +2130,53 @@ function createMessagingRepository(options) {
1971
2130
  },
1972
2131
  async deleteMessage(id, forEveryone) {
1973
2132
  const operation = "deleteMessage";
2133
+ const me = await actingUser(operation);
1974
2134
  const { error } = await db().from(TABLES.messages).update({
1975
2135
  deleted_at: (/* @__PURE__ */ new Date()).toISOString(),
1976
2136
  deleted_for_everyone: forEveryone,
1977
- updated_by: identity.userId
2137
+ updated_by: me
1978
2138
  }).eq("id", id);
1979
2139
  if (error !== null) throw normalizeMessagingError(error, operation);
1980
2140
  },
1981
2141
  async markRead(conversationId, at) {
1982
2142
  const operation = "markRead";
1983
- const { error } = await db().from(TABLES.participants).update({ last_read_at: at ?? (/* @__PURE__ */ new Date()).toISOString(), updated_by: identity.userId }).eq("conversation_id", conversationId).eq("user_id", identity.userId);
2143
+ const me = await actingUser(operation);
2144
+ const { error } = await db().from(TABLES.participants).update({ last_read_at: at ?? (/* @__PURE__ */ new Date()).toISOString(), updated_by: me }).eq("conversation_id", conversationId).eq("user_id", me);
1984
2145
  if (error !== null) throw normalizeMessagingError(error, operation);
1985
2146
  },
1986
2147
  async setConversationFlags(conversationId, flags) {
1987
2148
  const operation = "setConversationFlags";
1988
- const patch = { updated_by: identity.userId };
2149
+ const me = await actingUser(operation);
2150
+ const patch = { updated_by: me };
1989
2151
  if (flags.isMuted !== void 0) patch["is_muted"] = flags.isMuted;
1990
2152
  if (flags.isArchived !== void 0) patch["is_archived"] = flags.isArchived;
1991
- const { error } = await db().from(TABLES.participants).update(patch).eq("conversation_id", conversationId).eq("user_id", identity.userId);
2153
+ const { error } = await db().from(TABLES.participants).update(patch).eq("conversation_id", conversationId).eq("user_id", me);
1992
2154
  if (error !== null) throw normalizeMessagingError(error, operation);
1993
2155
  },
1994
2156
  async addMembers(conversationId, memberIds) {
1995
2157
  const operation = "addMembers";
2158
+ const me = await actingUser(operation);
1996
2159
  const { error } = await db().from(TABLES.participants).insert(
1997
2160
  memberIds.map((userId) => ({
1998
2161
  conversation_id: conversationId,
1999
2162
  user_id: userId,
2000
2163
  role: "member",
2001
2164
  organization_id: org,
2002
- created_by: identity.userId
2165
+ created_by: me
2003
2166
  }))
2004
2167
  );
2005
2168
  if (error !== null) throw normalizeMessagingError(error, operation);
2006
2169
  },
2007
2170
  async removeMember(conversationId, memberId) {
2008
2171
  const operation = "removeMember";
2009
- const { error } = await db().from(TABLES.participants).update({ deleted_at: (/* @__PURE__ */ new Date()).toISOString(), updated_by: identity.userId }).eq("conversation_id", conversationId).eq("user_id", memberId);
2172
+ const me = await actingUser(operation);
2173
+ const { error } = await db().from(TABLES.participants).update({ deleted_at: (/* @__PURE__ */ new Date()).toISOString(), updated_by: me }).eq("conversation_id", conversationId).eq("user_id", memberId);
2010
2174
  if (error !== null) throw normalizeMessagingError(error, operation);
2011
2175
  },
2012
2176
  async setMemberRole(conversationId, memberId, role) {
2013
2177
  const operation = "setMemberRole";
2014
- const { error } = await db().from(TABLES.participants).update({ role, updated_by: identity.userId }).eq("conversation_id", conversationId).eq("user_id", memberId);
2178
+ const me = await actingUser(operation);
2179
+ const { error } = await db().from(TABLES.participants).update({ role, updated_by: me }).eq("conversation_id", conversationId).eq("user_id", memberId);
2015
2180
  if (error !== null) throw normalizeMessagingError(error, operation);
2016
2181
  },
2017
2182
  getUser(userId) {