@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 +53 -0
- package/README.md +10 -3
- package/dist/index.cjs +187 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -42
- package/dist/index.d.ts +154 -42
- package/dist/index.js +187 -22
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +246 -31
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +174 -50
- package/dist/react.d.ts +174 -50
- package/dist/react.js +246 -31
- package/dist/react.js.map +1 -1
- package/package.json +3 -3
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}
|
|
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
|
|
1699
|
-
const
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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([
|
|
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 ===
|
|
2084
|
+
role: userId === me ? "owner" : "member",
|
|
1928
2085
|
organization_id: org,
|
|
1929
|
-
created_by:
|
|
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:
|
|
2097
|
+
sender_id: me,
|
|
1940
2098
|
organization_id: org,
|
|
1941
|
-
created_by:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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",
|
|
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:
|
|
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
|
|
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
|
|
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) {
|