@ai-matrx/messaging 0.13.1 → 0.14.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/dist/react.d.cts CHANGED
@@ -208,7 +208,11 @@ interface Page<TItem, TCursor> {
208
208
  /** What the host injects. Identity ONLY — every hard part is in the package. */
209
209
  interface MessagingIdentity {
210
210
  readonly userId: UserId;
211
- readonly organizationId: OrganizationId;
211
+ /**
212
+ * The active organization, or `null` when none is selected. Reads never
213
+ * depend on it; it is only where a NEW conversation lands.
214
+ */
215
+ readonly organizationId: OrganizationId | null;
212
216
  }
213
217
  interface DraftMessage {
214
218
  readonly conversationId: ConversationId;
@@ -525,7 +529,9 @@ declare function createOutbox(options: OutboxOptions): Outbox;
525
529
  * errors in 0.6 seconds. A missing session is a NORMAL lifecycle moment — it
526
530
  * warns and retries; it is never a red error.
527
531
  */
528
- type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
532
+ type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured"
533
+ /** A NEW conversation needs an organization to land in and none is selected. */
534
+ | "organization-required" | "unknown";
529
535
  declare class MessagingError extends Error {
530
536
  readonly code: MessagingErrorCode;
531
537
  /** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
@@ -817,15 +823,21 @@ interface RepositoryOptions {
817
823
  */
818
824
  client: MessagingSupabaseClient;
819
825
  /**
820
- * The active organization. Every conversation and message write carries it
821
- * explicitly (R5).
826
+ * The active organization, or `null`/omitted when none is selected.
827
+ *
828
+ * 🚨 READS NEVER NEED IT (access belongs to the person, 2026-09-23). Every
829
+ * read here is answered by RLS as the session's user; the organization is
830
+ * only the DESTINATION of a NEW conversation. Without one, starting a
831
+ * conversation is refused with `organization-required` (the host asks the
832
+ * person to choose), and a message or member added to an EXISTING
833
+ * conversation is stamped with that conversation's own organization.
822
834
  *
823
835
  * 🚨 THERE IS NO `userId` HERE, AND THERE WILL NOT BE ONE (DD-241). Who is
824
836
  * acting is read from `client`'s own session — see `session-identity.ts` for
825
837
  * the class that closed and why a handed-in id could not be made safe by
826
838
  * convention. A caller that still passes `identity` is IGNORED and told so.
827
839
  */
828
- organizationId: string;
840
+ organizationId?: string | null | undefined;
829
841
  /**
830
842
  * INTERNAL. The React provider already runs one session reader for its own
831
843
  * render loop and hands it here so the two cannot answer differently and so
@@ -846,8 +858,11 @@ interface RepositoryOptions {
846
858
  now?: (() => number) | undefined;
847
859
  }
848
860
  interface MessagingRepository {
849
- /** The organization every write here is stamped with. */
850
- readonly organizationId: OrganizationId;
861
+ /**
862
+ * The active organization new conversations land in, or `null` when none is
863
+ * selected — reads work either way.
864
+ */
865
+ readonly organizationId: OrganizationId | null;
851
866
  /**
852
867
  * Who the session says is acting — awaited, because the first `getSession()`
853
868
  * is a real read. Rejects with `session-unavailable` when nobody is signed
@@ -1269,7 +1284,12 @@ interface MessagingProviderProps {
1269
1284
  * (DD-241). There is no `userId` prop, on purpose.
1270
1285
  */
1271
1286
  client: MessagingSupabaseClient | null | undefined;
1272
- /** The active organization. Every write carries it (R5). */
1287
+ /**
1288
+ * The active organization, or `null` when none is selected. Messaging runs
1289
+ * without one: every read is the person's own. It is the destination of a
1290
+ * NEW conversation (refused as `organization-required` without it) and of
1291
+ * AI runs (the AI actions do not render without it).
1292
+ */
1273
1293
  organizationId: string | null | undefined;
1274
1294
  /**
1275
1295
  * The Matrx API transport for AI features. Omit to run messaging without AI;
@@ -1778,9 +1798,13 @@ declare function formatLastSeen(lastSeenMs: number | null, now?: number): string
1778
1798
 
1779
1799
  declare function projectUserSummary(row: Record<string, unknown>): UserSummary;
1780
1800
  declare function projectMessageAction(value: unknown): MessageAction | null;
1781
- declare function projectMessage(row: Record<string, unknown>, fallbackOrganizationId: OrganizationId): Message;
1801
+ declare function projectMessage(row: Record<string, unknown>,
1802
+ /** The active organization, or `null` when none is selected (reads never need one). */
1803
+ fallbackOrganizationId: OrganizationId | null): Message;
1782
1804
  declare function projectParticipantRole(value: unknown): ParticipantRole;
1783
- declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId, fallbackOrganizationId: OrganizationId): ConversationSummary;
1805
+ declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId,
1806
+ /** The active organization, or `null` when none is selected (reads never need one). */
1807
+ fallbackOrganizationId: OrganizationId | null): ConversationSummary;
1784
1808
 
1785
1809
  /**
1786
1810
  * MATRX REFERENCES — a message can name a platform entity, and everything it
package/dist/react.d.ts CHANGED
@@ -208,7 +208,11 @@ interface Page<TItem, TCursor> {
208
208
  /** What the host injects. Identity ONLY — every hard part is in the package. */
209
209
  interface MessagingIdentity {
210
210
  readonly userId: UserId;
211
- readonly organizationId: OrganizationId;
211
+ /**
212
+ * The active organization, or `null` when none is selected. Reads never
213
+ * depend on it; it is only where a NEW conversation lands.
214
+ */
215
+ readonly organizationId: OrganizationId | null;
212
216
  }
213
217
  interface DraftMessage {
214
218
  readonly conversationId: ConversationId;
@@ -525,7 +529,9 @@ declare function createOutbox(options: OutboxOptions): Outbox;
525
529
  * errors in 0.6 seconds. A missing session is a NORMAL lifecycle moment — it
526
530
  * warns and retries; it is never a red error.
527
531
  */
528
- type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
532
+ type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured"
533
+ /** A NEW conversation needs an organization to land in and none is selected. */
534
+ | "organization-required" | "unknown";
529
535
  declare class MessagingError extends Error {
530
536
  readonly code: MessagingErrorCode;
531
537
  /** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
@@ -817,15 +823,21 @@ interface RepositoryOptions {
817
823
  */
818
824
  client: MessagingSupabaseClient;
819
825
  /**
820
- * The active organization. Every conversation and message write carries it
821
- * explicitly (R5).
826
+ * The active organization, or `null`/omitted when none is selected.
827
+ *
828
+ * 🚨 READS NEVER NEED IT (access belongs to the person, 2026-09-23). Every
829
+ * read here is answered by RLS as the session's user; the organization is
830
+ * only the DESTINATION of a NEW conversation. Without one, starting a
831
+ * conversation is refused with `organization-required` (the host asks the
832
+ * person to choose), and a message or member added to an EXISTING
833
+ * conversation is stamped with that conversation's own organization.
822
834
  *
823
835
  * 🚨 THERE IS NO `userId` HERE, AND THERE WILL NOT BE ONE (DD-241). Who is
824
836
  * acting is read from `client`'s own session — see `session-identity.ts` for
825
837
  * the class that closed and why a handed-in id could not be made safe by
826
838
  * convention. A caller that still passes `identity` is IGNORED and told so.
827
839
  */
828
- organizationId: string;
840
+ organizationId?: string | null | undefined;
829
841
  /**
830
842
  * INTERNAL. The React provider already runs one session reader for its own
831
843
  * render loop and hands it here so the two cannot answer differently and so
@@ -846,8 +858,11 @@ interface RepositoryOptions {
846
858
  now?: (() => number) | undefined;
847
859
  }
848
860
  interface MessagingRepository {
849
- /** The organization every write here is stamped with. */
850
- readonly organizationId: OrganizationId;
861
+ /**
862
+ * The active organization new conversations land in, or `null` when none is
863
+ * selected — reads work either way.
864
+ */
865
+ readonly organizationId: OrganizationId | null;
851
866
  /**
852
867
  * Who the session says is acting — awaited, because the first `getSession()`
853
868
  * is a real read. Rejects with `session-unavailable` when nobody is signed
@@ -1269,7 +1284,12 @@ interface MessagingProviderProps {
1269
1284
  * (DD-241). There is no `userId` prop, on purpose.
1270
1285
  */
1271
1286
  client: MessagingSupabaseClient | null | undefined;
1272
- /** The active organization. Every write carries it (R5). */
1287
+ /**
1288
+ * The active organization, or `null` when none is selected. Messaging runs
1289
+ * without one: every read is the person's own. It is the destination of a
1290
+ * NEW conversation (refused as `organization-required` without it) and of
1291
+ * AI runs (the AI actions do not render without it).
1292
+ */
1273
1293
  organizationId: string | null | undefined;
1274
1294
  /**
1275
1295
  * The Matrx API transport for AI features. Omit to run messaging without AI;
@@ -1778,9 +1798,13 @@ declare function formatLastSeen(lastSeenMs: number | null, now?: number): string
1778
1798
 
1779
1799
  declare function projectUserSummary(row: Record<string, unknown>): UserSummary;
1780
1800
  declare function projectMessageAction(value: unknown): MessageAction | null;
1781
- declare function projectMessage(row: Record<string, unknown>, fallbackOrganizationId: OrganizationId): Message;
1801
+ declare function projectMessage(row: Record<string, unknown>,
1802
+ /** The active organization, or `null` when none is selected (reads never need one). */
1803
+ fallbackOrganizationId: OrganizationId | null): Message;
1782
1804
  declare function projectParticipantRole(value: unknown): ParticipantRole;
1783
- declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId, fallbackOrganizationId: OrganizationId): ConversationSummary;
1805
+ declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId,
1806
+ /** The active organization, or `null` when none is selected (reads never need one). */
1807
+ fallbackOrganizationId: OrganizationId | null): ConversationSummary;
1784
1808
 
1785
1809
  /**
1786
1810
  * MATRX REFERENCES — a message can name a platform entity, and everything it
package/dist/react.js CHANGED
@@ -762,7 +762,7 @@ function projectMessage(row, fallbackOrganizationId) {
762
762
  id: requiredStr(row, "id", operation),
763
763
  conversationId: requiredStr(row, "conversation_id", operation),
764
764
  senderId: requiredStr(row, "sender_id", operation),
765
- organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
765
+ organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
766
766
  content: typeof row["content"] === "string" ? row["content"] : "",
767
767
  kind: MESSAGE_KINDS.has(kindRaw) ? kindRaw : "text",
768
768
  // A row read from the DB is at least `sent`. `sending`/`failed` are outbox
@@ -810,7 +810,7 @@ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
810
810
  groupName,
811
811
  groupImageUrl,
812
812
  createdBy: createdBy === null ? null : createdBy,
813
- organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
813
+ organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
814
814
  createdAt,
815
815
  updatedAt,
816
816
  metadata: jsonObject(row["metadata"])
@@ -1143,7 +1143,11 @@ function createMessagingEngine(options) {
1143
1143
  optimisticMessage({
1144
1144
  conversationId: entry.draft.conversationId,
1145
1145
  senderId: identity.userId,
1146
- organizationId: identity.organizationId,
1146
+ // Client-only until the confirmed row replaces it: the active
1147
+ // organization, else the conversation's own.
1148
+ organizationId: identity.organizationId ?? store.snapshot().conversations.find(
1149
+ (item) => item.conversation.id === entry.draft.conversationId
1150
+ )?.conversation.organizationId ?? "",
1147
1151
  content: entry.draft.content,
1148
1152
  clientMessageId: entry.clientMessageId,
1149
1153
  ...entry.draft.kind !== void 0 ? { kind: entry.draft.kind } : {},
@@ -1632,12 +1636,15 @@ var RPCS = {
1632
1636
  };
1633
1637
  var MESSAGE_COLUMNS = "id,conversation_id,sender_id,organization_id,content,message_type,status,reply_to_id,client_message_id,action_data,media_url,media_thumbnail_url,media_metadata,created_at,edited_at,deleted_at,deleted_for_everyone,metadata";
1634
1638
  var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
1639
+ function optionalOrg(organizationId) {
1640
+ return typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
1641
+ }
1635
1642
  function requireOrg(organizationId, operation) {
1636
- if (typeof organizationId !== "string" || organizationId.length === 0) {
1643
+ if (organizationId === null) {
1637
1644
  throw new MessagingError(
1638
- "misconfigured",
1639
- `${operation}: no organization_id`,
1640
- "Every conversation and message write carries an explicit organization_id (R5). Pass a real org on <MessagingProvider>; the package refuses the write rather than letting an unscoped row reach the database."
1645
+ "organization-required",
1646
+ `${operation}: no organization selected`,
1647
+ "A new conversation is filed under one organization. Ask the person to choose one, then pass it as organizationId on <MessagingProvider>. Reading and replying to existing conversations never needs it."
1641
1648
  );
1642
1649
  }
1643
1650
  return organizationId;
@@ -1649,7 +1656,7 @@ function sleep(ms) {
1649
1656
  return new Promise((resolve) => setTimeout(resolve, jittered));
1650
1657
  }
1651
1658
  function createMessagingRepository(options) {
1652
- const org = requireOrg(options.organizationId, "createMessagingRepository");
1659
+ const org = optionalOrg(options.organizationId);
1653
1660
  const handedIn = options.identity;
1654
1661
  if (handedIn !== void 0 && handedIn !== null && handedIn.userId !== void 0) {
1655
1662
  options.onDiagnostic?.(
@@ -1719,6 +1726,11 @@ function createMessagingRepository(options) {
1719
1726
  });
1720
1727
  return found;
1721
1728
  }
1729
+ async function organizationOfExisting(conversationId) {
1730
+ if (org !== null) return org;
1731
+ const conversation = await repository.getConversation(conversationId);
1732
+ return conversation.organizationId;
1733
+ }
1722
1734
  const repository = {
1723
1735
  organizationId: org,
1724
1736
  actingUserId,
@@ -1851,6 +1863,7 @@ function createMessagingRepository(options) {
1851
1863
  },
1852
1864
  async getOrCreateDirectConversation(otherUserId) {
1853
1865
  const operation = "getOrCreateDirectConversation";
1866
+ const destination = requireOrg(org, operation);
1854
1867
  const me = await actingUser(operation);
1855
1868
  const id = await withSessionRetry(
1856
1869
  operation,
@@ -1862,7 +1875,7 @@ function createMessagingRepository(options) {
1862
1875
  // not a preference.
1863
1876
  p_user1_id: me,
1864
1877
  p_user2_id: otherUserId,
1865
- p_organization_id: org
1878
+ p_organization_id: destination
1866
1879
  },
1867
1880
  operation
1868
1881
  )
@@ -1874,13 +1887,14 @@ function createMessagingRepository(options) {
1874
1887
  },
1875
1888
  async createGroupConversation({ name, memberIds }) {
1876
1889
  const operation = "createGroupConversation";
1890
+ const destination = requireOrg(org, operation);
1877
1891
  const me = await actingUser(operation);
1878
1892
  const { data, error } = await withSessionRetry(
1879
1893
  operation,
1880
1894
  async () => db().from(TABLES.conversations).insert({
1881
1895
  type: "group",
1882
1896
  group_name: name,
1883
- organization_id: org,
1897
+ organization_id: destination,
1884
1898
  created_by: me
1885
1899
  }).select("id").single()
1886
1900
  );
@@ -1895,7 +1909,7 @@ function createMessagingRepository(options) {
1895
1909
  conversation_id: conversationId,
1896
1910
  user_id: userId,
1897
1911
  role: userId === me ? "owner" : "member",
1898
- organization_id: org,
1912
+ organization_id: destination,
1899
1913
  created_by: me
1900
1914
  }))
1901
1915
  );
@@ -1905,10 +1919,11 @@ function createMessagingRepository(options) {
1905
1919
  async insertMessage(draft, clientMessageId) {
1906
1920
  const operation = "insertMessage";
1907
1921
  const me = await actingUser(operation);
1922
+ const destination = await organizationOfExisting(draft.conversationId);
1908
1923
  const { data, error } = await db().from(TABLES.messages).insert({
1909
1924
  conversation_id: draft.conversationId,
1910
1925
  sender_id: me,
1911
- organization_id: org,
1926
+ organization_id: destination,
1912
1927
  created_by: me,
1913
1928
  content: draft.content,
1914
1929
  message_type: draft.kind ?? "text",
@@ -1969,12 +1984,13 @@ function createMessagingRepository(options) {
1969
1984
  async addMembers(conversationId, memberIds) {
1970
1985
  const operation = "addMembers";
1971
1986
  const me = await actingUser(operation);
1987
+ const destination = await organizationOfExisting(conversationId);
1972
1988
  const { error } = await db().from(TABLES.participants).insert(
1973
1989
  memberIds.map((userId) => ({
1974
1990
  conversation_id: conversationId,
1975
1991
  user_id: userId,
1976
1992
  role: "member",
1977
- organization_id: org,
1993
+ organization_id: destination,
1978
1994
  created_by: me
1979
1995
  }))
1980
1996
  );
@@ -2085,7 +2101,7 @@ function MessagingProvider(props) {
2085
2101
  }, [strayUserId]);
2086
2102
  const session = useSessionUserId(client, props.onDiagnostic);
2087
2103
  const userId = session.snapshot.userId;
2088
- const ready = client != null && userId !== null && typeof organizationId === "string" && organizationId.length > 0;
2104
+ const ready = client != null && userId !== null;
2089
2105
  const hostProvidesRealtime = useIsRealtimeProviderMounted();
2090
2106
  const runtime = /* @__PURE__ */ jsx(
2091
2107
  MessagingRuntime,
@@ -2121,12 +2137,13 @@ function MessagingRuntime(props) {
2121
2137
  actionRegistry.register(handler);
2122
2138
  });
2123
2139
  }, [actionRegistry, handlers]);
2124
- const ready = client != null && manager !== null && sessionIdentity !== null && userId !== null && typeof organizationId === "string" && organizationId.length > 0;
2140
+ const ready = client != null && manager !== null && sessionIdentity !== null && userId !== null;
2141
+ const activeOrganizationId = typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
2125
2142
  const engine = useMemo(() => {
2126
2143
  if (!ready || manager === null || sessionIdentity === null || userId === null) return null;
2127
2144
  const identity = {
2128
2145
  userId,
2129
- organizationId
2146
+ organizationId: activeOrganizationId
2130
2147
  };
2131
2148
  const report = (event) => {
2132
2149
  (diagnosticRef.current ?? defaultDiagnostics)(event);
@@ -2137,7 +2154,7 @@ function MessagingRuntime(props) {
2137
2154
  // supabase-shape.ts); everything below this line works against the rich
2138
2155
  // contract, which `supabase-shape.test.ts` proves a real client honors.
2139
2156
  client,
2140
- organizationId,
2157
+ organizationId: activeOrganizationId,
2141
2158
  // THE SAME READER the render loop above uses. Two readers would be two
2142
2159
  // answers during the tick an account switch takes.
2143
2160
  sessionIdentity,
@@ -2170,14 +2187,14 @@ function MessagingRuntime(props) {
2170
2187
  }
2171
2188
  });
2172
2189
  return built;
2173
- }, [ready, manager, sessionIdentity, userId, organizationId]);
2190
+ }, [ready, manager, sessionIdentity, userId, activeOrganizationId]);
2174
2191
  useEffect(() => {
2175
2192
  if (engine === null) return void 0;
2176
2193
  void engine.start().catch((error) => {
2177
2194
  (diagnosticRef.current ?? defaultDiagnostics)({
2178
2195
  level: "error",
2179
2196
  message: `Messaging failed to start: ${String(error?.message ?? error)}`,
2180
- remedy: "Check the signed-in session and organization, then reload the surface."
2197
+ remedy: "Check the signed-in session, then reload the surface."
2181
2198
  });
2182
2199
  });
2183
2200
  return () => {
@@ -2191,9 +2208,10 @@ function MessagingRuntime(props) {
2191
2208
  const sourceFeature = props.sourceFeature;
2192
2209
  const ai = useMemo(() => {
2193
2210
  if (transport === void 0 || agents === void 0 || !ready) return null;
2211
+ if (activeOrganizationId === null) return null;
2194
2212
  return createMessagingAi({
2195
2213
  transport,
2196
- organizationId,
2214
+ organizationId: activeOrganizationId,
2197
2215
  agents,
2198
2216
  ...maxTranscriptMessages !== void 0 ? { maxTranscriptMessages } : {},
2199
2217
  ...sourceApp !== void 0 ? { sourceApp } : {},
@@ -2203,7 +2221,7 @@ function MessagingRuntime(props) {
2203
2221
  transport,
2204
2222
  agents,
2205
2223
  ready,
2206
- organizationId,
2224
+ activeOrganizationId,
2207
2225
  maxTranscriptMessages,
2208
2226
  sourceApp,
2209
2227
  sourceFeature
@@ -2254,7 +2272,7 @@ function useRequiredMessagingHost() {
2254
2272
  const host = useMessagingHost();
2255
2273
  if (host === null) {
2256
2274
  throw new Error(
2257
- "[@ai-matrx/messaging] No messaging host. Wrap this tree in <MessagingProvider client={supabase} userId={userId} organizationId={orgId}> and render it only once auth has resolved."
2275
+ "[@ai-matrx/messaging] No messaging host. Wrap this tree in <MessagingProvider client={supabase} organizationId={orgId}> and render it only once auth has resolved."
2258
2276
  );
2259
2277
  }
2260
2278
  return host;
@@ -2644,7 +2662,8 @@ function useMessageAction(message) {
2644
2662
  const context = {
2645
2663
  messageId: message.id,
2646
2664
  actorId: host.identity.userId,
2647
- organizationId: host.identity.organizationId,
2665
+ // The active organization, else the organization the message lives in.
2666
+ organizationId: host.identity.organizationId ?? message.organizationId,
2648
2667
  choice
2649
2668
  };
2650
2669
  host.actions.execute(action, context).then(setReceipt).catch((cause) => {