@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/index.d.cts CHANGED
@@ -207,7 +207,11 @@ interface Page<TItem, TCursor> {
207
207
  /** What the host injects. Identity ONLY — every hard part is in the package. */
208
208
  interface MessagingIdentity {
209
209
  readonly userId: UserId;
210
- readonly organizationId: OrganizationId;
210
+ /**
211
+ * The active organization, or `null` when none is selected. Reads never
212
+ * depend on it; it is only where a NEW conversation lands.
213
+ */
214
+ readonly organizationId: OrganizationId | null;
211
215
  }
212
216
  interface DraftMessage {
213
217
  readonly conversationId: ConversationId;
@@ -628,7 +632,9 @@ declare function createOutbox(options: OutboxOptions): Outbox;
628
632
  * errors in 0.6 seconds. A missing session is a NORMAL lifecycle moment — it
629
633
  * warns and retries; it is never a red error.
630
634
  */
631
- type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
635
+ type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured"
636
+ /** A NEW conversation needs an organization to land in and none is selected. */
637
+ | "organization-required" | "unknown";
632
638
  declare class MessagingError extends Error {
633
639
  readonly code: MessagingErrorCode;
634
640
  /** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
@@ -920,15 +926,21 @@ interface RepositoryOptions {
920
926
  */
921
927
  client: MessagingSupabaseClient;
922
928
  /**
923
- * The active organization. Every conversation and message write carries it
924
- * explicitly (R5).
929
+ * The active organization, or `null`/omitted when none is selected.
930
+ *
931
+ * 🚨 READS NEVER NEED IT (access belongs to the person, 2026-09-23). Every
932
+ * read here is answered by RLS as the session's user; the organization is
933
+ * only the DESTINATION of a NEW conversation. Without one, starting a
934
+ * conversation is refused with `organization-required` (the host asks the
935
+ * person to choose), and a message or member added to an EXISTING
936
+ * conversation is stamped with that conversation's own organization.
925
937
  *
926
938
  * 🚨 THERE IS NO `userId` HERE, AND THERE WILL NOT BE ONE (DD-241). Who is
927
939
  * acting is read from `client`'s own session — see `session-identity.ts` for
928
940
  * the class that closed and why a handed-in id could not be made safe by
929
941
  * convention. A caller that still passes `identity` is IGNORED and told so.
930
942
  */
931
- organizationId: string;
943
+ organizationId?: string | null | undefined;
932
944
  /**
933
945
  * INTERNAL. The React provider already runs one session reader for its own
934
946
  * render loop and hands it here so the two cannot answer differently and so
@@ -949,8 +961,11 @@ interface RepositoryOptions {
949
961
  now?: (() => number) | undefined;
950
962
  }
951
963
  interface MessagingRepository {
952
- /** The organization every write here is stamped with. */
953
- readonly organizationId: OrganizationId;
964
+ /**
965
+ * The active organization new conversations land in, or `null` when none is
966
+ * selected — reads work either way.
967
+ */
968
+ readonly organizationId: OrganizationId | null;
954
969
  /**
955
970
  * Who the session says is acting — awaited, because the first `getSession()`
956
971
  * is a real read. Rejects with `session-unavailable` when nobody is signed
@@ -1311,9 +1326,13 @@ declare function formatLastSeen(lastSeenMs: number | null, now?: number): string
1311
1326
 
1312
1327
  declare function projectUserSummary(row: Record<string, unknown>): UserSummary;
1313
1328
  declare function projectMessageAction(value: unknown): MessageAction | null;
1314
- declare function projectMessage(row: Record<string, unknown>, fallbackOrganizationId: OrganizationId): Message;
1329
+ declare function projectMessage(row: Record<string, unknown>,
1330
+ /** The active organization, or `null` when none is selected (reads never need one). */
1331
+ fallbackOrganizationId: OrganizationId | null): Message;
1315
1332
  declare function projectParticipantRole(value: unknown): ParticipantRole;
1316
- declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId, fallbackOrganizationId: OrganizationId): ConversationSummary;
1333
+ declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId,
1334
+ /** The active organization, or `null` when none is selected (reads never need one). */
1335
+ fallbackOrganizationId: OrganizationId | null): ConversationSummary;
1317
1336
 
1318
1337
  /**
1319
1338
  * MATRX REFERENCES — a message can name a platform entity, and everything it
package/dist/index.d.ts CHANGED
@@ -207,7 +207,11 @@ interface Page<TItem, TCursor> {
207
207
  /** What the host injects. Identity ONLY — every hard part is in the package. */
208
208
  interface MessagingIdentity {
209
209
  readonly userId: UserId;
210
- readonly organizationId: OrganizationId;
210
+ /**
211
+ * The active organization, or `null` when none is selected. Reads never
212
+ * depend on it; it is only where a NEW conversation lands.
213
+ */
214
+ readonly organizationId: OrganizationId | null;
211
215
  }
212
216
  interface DraftMessage {
213
217
  readonly conversationId: ConversationId;
@@ -628,7 +632,9 @@ declare function createOutbox(options: OutboxOptions): Outbox;
628
632
  * errors in 0.6 seconds. A missing session is a NORMAL lifecycle moment — it
629
633
  * warns and retries; it is never a red error.
630
634
  */
631
- type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
635
+ type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured"
636
+ /** A NEW conversation needs an organization to land in and none is selected. */
637
+ | "organization-required" | "unknown";
632
638
  declare class MessagingError extends Error {
633
639
  readonly code: MessagingErrorCode;
634
640
  /** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
@@ -920,15 +926,21 @@ interface RepositoryOptions {
920
926
  */
921
927
  client: MessagingSupabaseClient;
922
928
  /**
923
- * The active organization. Every conversation and message write carries it
924
- * explicitly (R5).
929
+ * The active organization, or `null`/omitted when none is selected.
930
+ *
931
+ * 🚨 READS NEVER NEED IT (access belongs to the person, 2026-09-23). Every
932
+ * read here is answered by RLS as the session's user; the organization is
933
+ * only the DESTINATION of a NEW conversation. Without one, starting a
934
+ * conversation is refused with `organization-required` (the host asks the
935
+ * person to choose), and a message or member added to an EXISTING
936
+ * conversation is stamped with that conversation's own organization.
925
937
  *
926
938
  * 🚨 THERE IS NO `userId` HERE, AND THERE WILL NOT BE ONE (DD-241). Who is
927
939
  * acting is read from `client`'s own session — see `session-identity.ts` for
928
940
  * the class that closed and why a handed-in id could not be made safe by
929
941
  * convention. A caller that still passes `identity` is IGNORED and told so.
930
942
  */
931
- organizationId: string;
943
+ organizationId?: string | null | undefined;
932
944
  /**
933
945
  * INTERNAL. The React provider already runs one session reader for its own
934
946
  * render loop and hands it here so the two cannot answer differently and so
@@ -949,8 +961,11 @@ interface RepositoryOptions {
949
961
  now?: (() => number) | undefined;
950
962
  }
951
963
  interface MessagingRepository {
952
- /** The organization every write here is stamped with. */
953
- readonly organizationId: OrganizationId;
964
+ /**
965
+ * The active organization new conversations land in, or `null` when none is
966
+ * selected — reads work either way.
967
+ */
968
+ readonly organizationId: OrganizationId | null;
954
969
  /**
955
970
  * Who the session says is acting — awaited, because the first `getSession()`
956
971
  * is a real read. Rejects with `session-unavailable` when nobody is signed
@@ -1311,9 +1326,13 @@ declare function formatLastSeen(lastSeenMs: number | null, now?: number): string
1311
1326
 
1312
1327
  declare function projectUserSummary(row: Record<string, unknown>): UserSummary;
1313
1328
  declare function projectMessageAction(value: unknown): MessageAction | null;
1314
- declare function projectMessage(row: Record<string, unknown>, fallbackOrganizationId: OrganizationId): Message;
1329
+ declare function projectMessage(row: Record<string, unknown>,
1330
+ /** The active organization, or `null` when none is selected (reads never need one). */
1331
+ fallbackOrganizationId: OrganizationId | null): Message;
1315
1332
  declare function projectParticipantRole(value: unknown): ParticipantRole;
1316
- declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId, fallbackOrganizationId: OrganizationId): ConversationSummary;
1333
+ declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId,
1334
+ /** The active organization, or `null` when none is selected (reads never need one). */
1335
+ fallbackOrganizationId: OrganizationId | null): ConversationSummary;
1317
1336
 
1318
1337
  /**
1319
1338
  * MATRX REFERENCES — a message can name a platform entity, and everything it
package/dist/index.js CHANGED
@@ -861,7 +861,7 @@ function projectMessage(row, fallbackOrganizationId) {
861
861
  id: requiredStr(row, "id", operation),
862
862
  conversationId: requiredStr(row, "conversation_id", operation),
863
863
  senderId: requiredStr(row, "sender_id", operation),
864
- organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
864
+ organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
865
865
  content: typeof row["content"] === "string" ? row["content"] : "",
866
866
  kind: MESSAGE_KINDS.has(kindRaw) ? kindRaw : "text",
867
867
  // A row read from the DB is at least `sent`. `sending`/`failed` are outbox
@@ -909,7 +909,7 @@ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
909
909
  groupName,
910
910
  groupImageUrl,
911
911
  createdBy: createdBy === null ? null : createdBy,
912
- organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
912
+ organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
913
913
  createdAt,
914
914
  updatedAt,
915
915
  metadata: jsonObject(row["metadata"])
@@ -1242,7 +1242,11 @@ function createMessagingEngine(options) {
1242
1242
  optimisticMessage({
1243
1243
  conversationId: entry.draft.conversationId,
1244
1244
  senderId: identity.userId,
1245
- organizationId: identity.organizationId,
1245
+ // Client-only until the confirmed row replaces it: the active
1246
+ // organization, else the conversation's own.
1247
+ organizationId: identity.organizationId ?? store.snapshot().conversations.find(
1248
+ (item) => item.conversation.id === entry.draft.conversationId
1249
+ )?.conversation.organizationId ?? "",
1246
1250
  content: entry.draft.content,
1247
1251
  clientMessageId: entry.clientMessageId,
1248
1252
  ...entry.draft.kind !== void 0 ? { kind: entry.draft.kind } : {},
@@ -1774,12 +1778,15 @@ var RPCS = {
1774
1778
  };
1775
1779
  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";
1776
1780
  var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
1781
+ function optionalOrg(organizationId) {
1782
+ return typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
1783
+ }
1777
1784
  function requireOrg(organizationId, operation) {
1778
- if (typeof organizationId !== "string" || organizationId.length === 0) {
1785
+ if (organizationId === null) {
1779
1786
  throw new MessagingError(
1780
- "misconfigured",
1781
- `${operation}: no organization_id`,
1782
- "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."
1787
+ "organization-required",
1788
+ `${operation}: no organization selected`,
1789
+ "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."
1783
1790
  );
1784
1791
  }
1785
1792
  return organizationId;
@@ -1791,7 +1798,7 @@ function sleep(ms) {
1791
1798
  return new Promise((resolve) => setTimeout(resolve, jittered));
1792
1799
  }
1793
1800
  function createMessagingRepository(options) {
1794
- const org = requireOrg(options.organizationId, "createMessagingRepository");
1801
+ const org = optionalOrg(options.organizationId);
1795
1802
  const handedIn = options.identity;
1796
1803
  if (handedIn !== void 0 && handedIn !== null && handedIn.userId !== void 0) {
1797
1804
  options.onDiagnostic?.(
@@ -1861,6 +1868,11 @@ function createMessagingRepository(options) {
1861
1868
  });
1862
1869
  return found;
1863
1870
  }
1871
+ async function organizationOfExisting(conversationId) {
1872
+ if (org !== null) return org;
1873
+ const conversation = await repository.getConversation(conversationId);
1874
+ return conversation.organizationId;
1875
+ }
1864
1876
  const repository = {
1865
1877
  organizationId: org,
1866
1878
  actingUserId,
@@ -1993,6 +2005,7 @@ function createMessagingRepository(options) {
1993
2005
  },
1994
2006
  async getOrCreateDirectConversation(otherUserId) {
1995
2007
  const operation = "getOrCreateDirectConversation";
2008
+ const destination = requireOrg(org, operation);
1996
2009
  const me = await actingUser(operation);
1997
2010
  const id = await withSessionRetry(
1998
2011
  operation,
@@ -2004,7 +2017,7 @@ function createMessagingRepository(options) {
2004
2017
  // not a preference.
2005
2018
  p_user1_id: me,
2006
2019
  p_user2_id: otherUserId,
2007
- p_organization_id: org
2020
+ p_organization_id: destination
2008
2021
  },
2009
2022
  operation
2010
2023
  )
@@ -2016,13 +2029,14 @@ function createMessagingRepository(options) {
2016
2029
  },
2017
2030
  async createGroupConversation({ name, memberIds }) {
2018
2031
  const operation = "createGroupConversation";
2032
+ const destination = requireOrg(org, operation);
2019
2033
  const me = await actingUser(operation);
2020
2034
  const { data, error } = await withSessionRetry(
2021
2035
  operation,
2022
2036
  async () => db().from(TABLES.conversations).insert({
2023
2037
  type: "group",
2024
2038
  group_name: name,
2025
- organization_id: org,
2039
+ organization_id: destination,
2026
2040
  created_by: me
2027
2041
  }).select("id").single()
2028
2042
  );
@@ -2037,7 +2051,7 @@ function createMessagingRepository(options) {
2037
2051
  conversation_id: conversationId,
2038
2052
  user_id: userId,
2039
2053
  role: userId === me ? "owner" : "member",
2040
- organization_id: org,
2054
+ organization_id: destination,
2041
2055
  created_by: me
2042
2056
  }))
2043
2057
  );
@@ -2047,10 +2061,11 @@ function createMessagingRepository(options) {
2047
2061
  async insertMessage(draft, clientMessageId) {
2048
2062
  const operation = "insertMessage";
2049
2063
  const me = await actingUser(operation);
2064
+ const destination = await organizationOfExisting(draft.conversationId);
2050
2065
  const { data, error } = await db().from(TABLES.messages).insert({
2051
2066
  conversation_id: draft.conversationId,
2052
2067
  sender_id: me,
2053
- organization_id: org,
2068
+ organization_id: destination,
2054
2069
  created_by: me,
2055
2070
  content: draft.content,
2056
2071
  message_type: draft.kind ?? "text",
@@ -2111,12 +2126,13 @@ function createMessagingRepository(options) {
2111
2126
  async addMembers(conversationId, memberIds) {
2112
2127
  const operation = "addMembers";
2113
2128
  const me = await actingUser(operation);
2129
+ const destination = await organizationOfExisting(conversationId);
2114
2130
  const { error } = await db().from(TABLES.participants).insert(
2115
2131
  memberIds.map((userId) => ({
2116
2132
  conversation_id: conversationId,
2117
2133
  user_id: userId,
2118
2134
  role: "member",
2119
- organization_id: org,
2135
+ organization_id: destination,
2120
2136
  created_by: me
2121
2137
  }))
2122
2138
  );