@stapel/chat-react 0.1.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.
Files changed (195) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/MODULE.md +118 -0
  3. package/README.md +126 -0
  4. package/dist/api/chatApi.d.ts +49 -0
  5. package/dist/api/chatApi.d.ts.map +1 -0
  6. package/dist/api/chatApi.js +46 -0
  7. package/dist/api/chatApi.js.map +1 -0
  8. package/dist/api/extensions.d.ts +19 -0
  9. package/dist/api/extensions.d.ts.map +1 -0
  10. package/dist/api/extensions.js +19 -0
  11. package/dist/api/extensions.js.map +1 -0
  12. package/dist/api/generated/schema.d.ts +547 -0
  13. package/dist/api/generated/schema.d.ts.map +1 -0
  14. package/dist/api/generated/schema.js +2 -0
  15. package/dist/api/generated/schema.js.map +1 -0
  16. package/dist/api/types.d.ts +75 -0
  17. package/dist/api/types.d.ts.map +1 -0
  18. package/dist/api/types.js +2 -0
  19. package/dist/api/types.js.map +1 -0
  20. package/dist/default/ConversationListPanel.d.ts +22 -0
  21. package/dist/default/ConversationListPanel.d.ts.map +1 -0
  22. package/dist/default/ConversationListPanel.js +49 -0
  23. package/dist/default/ConversationListPanel.js.map +1 -0
  24. package/dist/default/ConversationThreadPanel.d.ts +21 -0
  25. package/dist/default/ConversationThreadPanel.d.ts.map +1 -0
  26. package/dist/default/ConversationThreadPanel.js +51 -0
  27. package/dist/default/ConversationThreadPanel.js.map +1 -0
  28. package/dist/default/ErrorAlert.d.ts +8 -0
  29. package/dist/default/ErrorAlert.d.ts.map +1 -0
  30. package/dist/default/ErrorAlert.js +26 -0
  31. package/dist/default/ErrorAlert.js.map +1 -0
  32. package/dist/default/StartChatButton.d.ts +17 -0
  33. package/dist/default/StartChatButton.d.ts.map +1 -0
  34. package/dist/default/StartChatButton.js +18 -0
  35. package/dist/default/StartChatButton.js.map +1 -0
  36. package/dist/default/index.d.ts +21 -0
  37. package/dist/default/index.d.ts.map +1 -0
  38. package/dist/default/index.js +18 -0
  39. package/dist/default/index.js.map +1 -0
  40. package/dist/flows/errors.d.ts +12 -0
  41. package/dist/flows/errors.d.ts.map +1 -0
  42. package/dist/flows/errors.js +13 -0
  43. package/dist/flows/errors.js.map +1 -0
  44. package/dist/flows/freshness.d.ts +69 -0
  45. package/dist/flows/freshness.d.ts.map +1 -0
  46. package/dist/flows/freshness.js +227 -0
  47. package/dist/flows/freshness.js.map +1 -0
  48. package/dist/flows/registry.d.ts +26 -0
  49. package/dist/flows/registry.d.ts.map +1 -0
  50. package/dist/flows/registry.js +24 -0
  51. package/dist/flows/registry.js.map +1 -0
  52. package/dist/headless/ChatProvider.d.ts +22 -0
  53. package/dist/headless/ChatProvider.d.ts.map +1 -0
  54. package/dist/headless/ChatProvider.js +18 -0
  55. package/dist/headless/ChatProvider.js.map +1 -0
  56. package/dist/headless/ConversationList.d.ts +51 -0
  57. package/dist/headless/ConversationList.d.ts.map +1 -0
  58. package/dist/headless/ConversationList.js +47 -0
  59. package/dist/headless/ConversationList.js.map +1 -0
  60. package/dist/headless/ConversationThread.d.ts +50 -0
  61. package/dist/headless/ConversationThread.d.ts.map +1 -0
  62. package/dist/headless/ConversationThread.js +64 -0
  63. package/dist/headless/ConversationThread.js.map +1 -0
  64. package/dist/headless/MessageComposer.d.ts +39 -0
  65. package/dist/headless/MessageComposer.d.ts.map +1 -0
  66. package/dist/headless/MessageComposer.js +45 -0
  67. package/dist/headless/MessageComposer.js.map +1 -0
  68. package/dist/headless/StartDirectChat.d.ts +40 -0
  69. package/dist/headless/StartDirectChat.d.ts.map +1 -0
  70. package/dist/headless/StartDirectChat.js +46 -0
  71. package/dist/headless/StartDirectChat.js.map +1 -0
  72. package/dist/i18n/errorsMap.d.ts +10 -0
  73. package/dist/i18n/errorsMap.d.ts.map +1 -0
  74. package/dist/i18n/errorsMap.js +20 -0
  75. package/dist/i18n/errorsMap.js.map +1 -0
  76. package/dist/i18n/es.d.ts +23 -0
  77. package/dist/i18n/es.d.ts.map +1 -0
  78. package/dist/i18n/es.js +75 -0
  79. package/dist/i18n/es.js.map +1 -0
  80. package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
  81. package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
  82. package/dist/i18n/generated/errors.es.gen.js +58 -0
  83. package/dist/i18n/generated/errors.es.gen.js.map +1 -0
  84. package/dist/i18n/generated/errors.gen.d.ts +359 -0
  85. package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
  86. package/dist/i18n/generated/errors.gen.js +183 -0
  87. package/dist/i18n/generated/errors.gen.js.map +1 -0
  88. package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
  89. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
  90. package/dist/i18n/generated/errors.ru.gen.js +58 -0
  91. package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
  92. package/dist/i18n/keys.d.ts +78 -0
  93. package/dist/i18n/keys.d.ts.map +1 -0
  94. package/dist/i18n/keys.js +130 -0
  95. package/dist/i18n/keys.js.map +1 -0
  96. package/dist/i18n/ru.d.ts +28 -0
  97. package/dist/i18n/ru.d.ts.map +1 -0
  98. package/dist/i18n/ru.js +80 -0
  99. package/dist/i18n/ru.js.map +1 -0
  100. package/dist/index.d.ts +61 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +55 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/model/context.d.ts +11 -0
  105. package/dist/model/context.d.ts.map +1 -0
  106. package/dist/model/context.js +16 -0
  107. package/dist/model/context.js.map +1 -0
  108. package/dist/model/limits.d.ts +12 -0
  109. package/dist/model/limits.d.ts.map +1 -0
  110. package/dist/model/limits.js +12 -0
  111. package/dist/model/limits.js.map +1 -0
  112. package/dist/model/mutations.d.ts +66 -0
  113. package/dist/model/mutations.d.ts.map +1 -0
  114. package/dist/model/mutations.js +156 -0
  115. package/dist/model/mutations.js.map +1 -0
  116. package/dist/model/queries.d.ts +33 -0
  117. package/dist/model/queries.d.ts.map +1 -0
  118. package/dist/model/queries.js +93 -0
  119. package/dist/model/queries.js.map +1 -0
  120. package/dist/model/queryKeys.d.ts +8 -0
  121. package/dist/model/queryKeys.d.ts.map +1 -0
  122. package/dist/model/queryKeys.js +26 -0
  123. package/dist/model/queryKeys.js.map +1 -0
  124. package/dist/model/readMarker.d.ts +25 -0
  125. package/dist/model/readMarker.d.ts.map +1 -0
  126. package/dist/model/readMarker.js +31 -0
  127. package/dist/model/readMarker.js.map +1 -0
  128. package/dist/model/runtime.d.ts +50 -0
  129. package/dist/model/runtime.d.ts.map +1 -0
  130. package/dist/model/runtime.js +27 -0
  131. package/dist/model/runtime.js.map +1 -0
  132. package/dist/model/threadWindow.d.ts +95 -0
  133. package/dist/model/threadWindow.d.ts.map +1 -0
  134. package/dist/model/threadWindow.js +100 -0
  135. package/dist/model/threadWindow.js.map +1 -0
  136. package/dist/nav/manifest.d.ts +23 -0
  137. package/dist/nav/manifest.d.ts.map +1 -0
  138. package/dist/nav/manifest.js +15 -0
  139. package/dist/nav/manifest.js.map +1 -0
  140. package/dist/realtime/chatSocket.d.ts +93 -0
  141. package/dist/realtime/chatSocket.d.ts.map +1 -0
  142. package/dist/realtime/chatSocket.js +199 -0
  143. package/dist/realtime/chatSocket.js.map +1 -0
  144. package/dist/realtime/frames.d.ts +123 -0
  145. package/dist/realtime/frames.d.ts.map +1 -0
  146. package/dist/realtime/frames.js +123 -0
  147. package/dist/realtime/frames.js.map +1 -0
  148. package/dist/realtime/streams.d.ts +49 -0
  149. package/dist/realtime/streams.d.ts.map +1 -0
  150. package/dist/realtime/streams.js +52 -0
  151. package/dist/realtime/streams.js.map +1 -0
  152. package/llms.txt +89 -0
  153. package/manifest.json +733 -0
  154. package/nav-manifest.json +25 -0
  155. package/package.json +114 -0
  156. package/src/analytics/generated/events.json +7 -0
  157. package/src/api/chatApi.ts +142 -0
  158. package/src/api/extensions.ts +18 -0
  159. package/src/api/generated/schema.ts +549 -0
  160. package/src/api/types.ts +87 -0
  161. package/src/default/ConversationListPanel.tsx +163 -0
  162. package/src/default/ConversationThreadPanel.tsx +245 -0
  163. package/src/default/ErrorAlert.tsx +43 -0
  164. package/src/default/StartChatButton.tsx +78 -0
  165. package/src/default/index.ts +20 -0
  166. package/src/flows/errors.ts +16 -0
  167. package/src/flows/freshness.ts +315 -0
  168. package/src/flows/registry.ts +36 -0
  169. package/src/headless/ChatProvider.tsx +23 -0
  170. package/src/headless/ConversationList.tsx +92 -0
  171. package/src/headless/ConversationThread.tsx +107 -0
  172. package/src/headless/MessageComposer.tsx +86 -0
  173. package/src/headless/StartDirectChat.tsx +75 -0
  174. package/src/i18n/errorsMap.ts +31 -0
  175. package/src/i18n/es.ts +95 -0
  176. package/src/i18n/generated/errors.es.gen.ts +64 -0
  177. package/src/i18n/generated/errors.gen.ts +214 -0
  178. package/src/i18n/generated/errors.json +408 -0
  179. package/src/i18n/generated/errors.ru.gen.ts +64 -0
  180. package/src/i18n/keys.ts +152 -0
  181. package/src/i18n/ru.ts +98 -0
  182. package/src/index.ts +189 -0
  183. package/src/model/context.tsx +25 -0
  184. package/src/model/limits.ts +11 -0
  185. package/src/model/mutations.ts +208 -0
  186. package/src/model/queries.ts +120 -0
  187. package/src/model/queryKeys.ts +32 -0
  188. package/src/model/readMarker.ts +32 -0
  189. package/src/model/runtime.ts +90 -0
  190. package/src/model/threadWindow.ts +173 -0
  191. package/src/nav/manifest.ts +36 -0
  192. package/src/realtime/chatSocket.ts +304 -0
  193. package/src/realtime/frames.ts +229 -0
  194. package/src/realtime/streams.ts +85 -0
  195. package/tsconfig.json +26 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ # @stapel/chat-react
package/MODULE.md ADDED
@@ -0,0 +1,118 @@
1
+ # @stapel/chat-react — module guide
2
+
3
+ Headless React pair for **stapel-chat**. This is the human companion to the
4
+ generated `llms.txt` (agent context) and `manifest.json` (machine catalog).
5
+
6
+ ## The one thing to understand first: the transport seam
7
+
8
+ stapel-chat can deliver the same journal two ways — the REST history, and its
9
+ own resumable WebSocket protocol (`ws/chat/<uuid:conversation_id>`, exported by
10
+ `stapel_chat.routing` since 0.2.2). This pair wires **both**, behind one hook,
11
+ and no component above that hook can tell which is running:
12
+
13
+ ```
14
+ useChatFreshness(streamKey, mapToQueryKeys, { fallbackRefetchInterval })
15
+ ├── socket → hello{last_seq} → welcome → replay… → replay_done → live
16
+ │ frames, seq-deduped; error{resync} → re-hydrate
17
+ └── polling → a visibility-aware tick, exponential backoff on failures
18
+ ```
19
+
20
+ Both ends do the same thing with what they learn: **refetch the thread query**,
21
+ whose query function advances the window BY SEQ
22
+ (`GET …/messages?direction=prev&anchor=<tip>`). A socket frame does not carry
23
+ its payload into the cache — it says "there is something after your tip" and
24
+ the store goes and gets it. That is why the screens, and their tests, are
25
+ written once.
26
+
27
+ Writes never go over the socket. The `send` frame is typed (a mirror must be
28
+ complete) and never emitted: its refusals carry socket-local codes
29
+ (`empty`, `too_long`, …) that are not in the module's error registry, so they
30
+ have no i18n key and no remediation. `POST …/messages` answers with the
31
+ persisted row and a real error envelope.
32
+
33
+ **When `@stapel/realtime` phase 1 lands**, `src/flows/freshness.ts` is the whole
34
+ migration: `createChatSocket` goes away, `useSignalInvalidate` takes its place,
35
+ and this pair's tests must stay green with no edits. Nothing above that file
36
+ imports `realtime/`, so the blast radius is checkable rather than promised.
37
+
38
+ ## Layers
39
+
40
+ - **api/** — `createChatApi(client)`; types alias the generated
41
+ `components["schemas"]` from stapel-chat's own `docs/schema.json` (never
42
+ parallel hand-written bodies). The support-operator half of the module
43
+ (`/support/queue`, assign / resolve / reopen) is deliberately absent — it is
44
+ an operator console, not the buyer-and-seller surface. `api/extensions.ts`
45
+ records the one request field that is NOT exposed and why.
46
+ - **realtime/** — the protocol, typed (`frames.ts`), the resumable client
47
+ (`chatSocket.ts`), and the stream keys plus the URL rule (`streams.ts`). No
48
+ React, no `@stapel/core`: plain protocol the substrate can subsume.
49
+ - **model/** — `chatQueryKeys` (one factory, `["chat"]` namespace),
50
+ `createChatRuntime` (which also resolves WHERE the socket is),
51
+ `threadWindow.ts` (the merge rules — the store's real logic, pure and
52
+ directly testable), `readMarker.ts`, queries and mutations.
53
+ - **flows/** — the transport seam (`freshness.ts`) and the error fold. The flow
54
+ REGISTRY is a zero-flow shim: stapel-chat annotates no `@flow_step`, and its
55
+ multi-step-ness lives in the transport, not in a server-declared funnel.
56
+ - **headless/** — render-prop components: `<ConversationList>`,
57
+ `<ConversationThread>`, `<MessageComposer>`, `<StartDirectChat>`, plus
58
+ `<ChatProvider>`. shadcn-copyable (frontend-standard §7).
59
+ - **default/** — the opt-in antd skin (`@stapel/chat-react/default`).
60
+ - **i18n/** — `CHAT_I18N_KEYS` + en bundle; the generated backend error bundle
61
+ is merged in so every `error.*` code has a fallback. See "Localization" below
62
+ for the one thing that is unusual here.
63
+ - **nav/** — one MEMBER entry (`chat.conversations`). The thread route is not a
64
+ menu destination and is mounted by the container under its own `:id` child.
65
+
66
+ ## The thread store, in three rules
67
+
68
+ 1. **`seq` is the order.** Gapless and total; ordering by `created_at` is an
69
+ upstream anti-pattern. Every merge sorts, dedupes and detects holes by seq.
70
+ 2. **A window is contiguous or it is rebuilt.** A tail page that does not touch
71
+ the tip (or that the paginator flags as truncated with `has_prev`) is a
72
+ hole; the window is re-read from the newest page rather than stitched. That
73
+ is the REST twin of the socket's `error{resync}`.
74
+ 3. **The same row can arrive twice** — the sender's own REST answer and the
75
+ socket's fan-out of it. `mergeMessage` drops a seq the window already holds,
76
+ which is what makes both paths idempotent.
77
+
78
+ ## Localization — the twelve keys this pair owns
79
+
80
+ stapel-chat ships **English only**: it has no `translations/` directory, so its
81
+ 12 error keys cannot appear in any locale catalogue upstream. `gen:errors` runs
82
+ for this pair with `ERRORS_LOCALE_EXEMPT_OWNERS=stapel_chat` and
83
+ `ERRORS_CATALOG_DIR` pointed at stapel-core's catalogue: the 42 cross-cutting
84
+ keys come from core in ru/es (typed `Partial`, so the gap is visible to
85
+ TypeScript), and the pair authors the 12 chat-owned texts in
86
+ `src/i18n/{ru,es}.ts` beside its UI copy. `test/i18n.test.ts` proves every
87
+ registry code resolves to a sentence in all three locales. When upstream ships
88
+ `translations/errors.{ru,es}.json`, those twelve lines are deleted and the
89
+ generated bundle covers them — the keys and the texts do not move.
90
+
91
+ Unlike notifications-react, the `es` subpath here carries hand-written UI copy
92
+ too: a marketplace's buyer-to-seller chat is the surface where a half-translated
93
+ screen shows most.
94
+
95
+ ## Extension seams (frontend-standard §7)
96
+
97
+ - The client is injected via `<ChatProvider>` / core's `StapelConfigProvider`
98
+ (per-module override) — pairs never hard-import a client.
99
+ - The socket transport is injected: `createChatRuntime({ realtime: { webSocket } })`
100
+ takes any factory with `send`/`close` plus four callbacks, which is how the
101
+ tests drive the protocol frame by frame without a network.
102
+ - `realtime.socketUrl: null` turns the socket half off for a deployment that has
103
+ no sockets (WSGI, no channel layer). Everything keeps working on the timer.
104
+ - The headless layer is fully replaceable (copy-and-own); the antd skin is a
105
+ separate entry point nobody has to import.
106
+
107
+ ## Not in this version
108
+
109
+ - **Attachments.** `SendMessageRequest.attachments` exists on the wire; wiring
110
+ it means wiring CDN upload rights into chat, and a control that is visible
111
+ but does nothing is worse than one that is absent.
112
+ - **The support console.** `/support/queue`, assign / resolve / reopen — an
113
+ operator surface, not this one.
114
+ - **A thread scoped to a listing.** `CreateConversationRequest.scope_key` is
115
+ ignored by the server (it resolves the scope itself), and a direct thread is
116
+ keyed by the participant PAIR — so buyer↔seller is one conversation across
117
+ every listing. A host that wants the listing named says so in the first
118
+ message.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # @stapel/chat-react
2
+
3
+ Headless React pair for **stapel-chat** (frontend-standard §2). Business +
4
+ state only in the main entry, zero visual opinion — any design layers on top.
5
+ Built on `@stapel/core` (typed client + `StapelApiError` envelope, token
6
+ refresh, verification-403 interception, i18n engine, analytics seam, TanStack
7
+ Query).
8
+
9
+ Its backend delivers the same journal two ways — the REST history and its own
10
+ resumable WebSocket protocol — and this pair wires **both, behind one seam**, so
11
+ a deployment with sockets and one without run the same screens. See `MODULE.md`
12
+ for the layer map and the seam's replacement criterion.
13
+
14
+ ## Install
15
+
16
+ ```
17
+ pnpm add @stapel/chat-react @stapel/core @tanstack/react-query react
18
+ ```
19
+
20
+ ## Wire the app once
21
+
22
+ ```tsx
23
+ import { createI18n, StapelProvider } from "@stapel/core";
24
+ import { ChatProvider, createChatRuntime, registerChatI18n } from "@stapel/chat-react";
25
+
26
+ const runtime = createChatRuntime({
27
+ baseUrl: "/chat/api/v1",
28
+ // Sockets are derived from baseUrl's origin (`/ws/chat/`, the module's
29
+ // canonical mount). On a deployment that has none, say so — it goes
30
+ // straight to polling instead of failing a handshake six times first:
31
+ // realtime: { socketUrl: null },
32
+ });
33
+ const i18n = createI18n({ locale: "en" });
34
+ registerChatI18n(i18n);
35
+
36
+ export function Root({ children }: { children: React.ReactNode }) {
37
+ return (
38
+ <StapelProvider client={runtime.client} i18n={i18n} cacheVersion="0.1.0">
39
+ <ChatProvider runtime={runtime}>{children}</ChatProvider>
40
+ </StapelProvider>
41
+ );
42
+ }
43
+ ```
44
+
45
+ ## The inbox
46
+
47
+ ```tsx
48
+ import { ConversationList } from "@stapel/chat-react";
49
+ import { matchList } from "@stapel/core";
50
+
51
+ <ConversationList>
52
+ {({ state }) =>
53
+ matchList(state, {
54
+ loading: () => <Spinner />,
55
+ failed: (error) => <ErrorPanel error={error} />,
56
+ empty: () => <p>{t("chat.list.empty")}</p>,
57
+ ready: (rows) => <List rows={rows} />, // rows carry `unread_count`
58
+ })
59
+ }
60
+ </ConversationList>;
61
+ ```
62
+
63
+ ## A thread
64
+
65
+ ```tsx
66
+ import { ConversationThread, MessageComposer } from "@stapel/chat-react";
67
+
68
+ <ConversationThread conversationId={id}>
69
+ {({ state, hasOlder, loadOlder, transport }) => (
70
+ /* `transport` is "socket" | "polling" | "idle" — a LABEL. Nothing about
71
+ the thread's behaviour depends on it; that is the seam's whole point. */
72
+ ...
73
+ )}
74
+ </ConversationThread>;
75
+
76
+ <MessageComposer conversationId={id}>
77
+ {({ value, setValue, availability, send }) => (
78
+ /* `availability` is blocked-with-a-reason or available — a disabled
79
+ control here always has a sentence to show. */
80
+ ...
81
+ )}
82
+ </MessageComposer>;
83
+ ```
84
+
85
+ The thread replays, then stays live; the read marker advances to the tip while
86
+ it is mounted and never moves backwards. Sending is REST — the persisted row
87
+ comes back with its `seq` and is folded into the window.
88
+
89
+ ## "Message the seller"
90
+
91
+ ```tsx
92
+ import { StartDirectChat } from "@stapel/chat-react";
93
+
94
+ <StartDirectChat sellerId={listing.seller_id} viewerId={me?.id} onOpened={(c) => navigate(`/account/chat/${c.id}`)}>
95
+ {({ availability, start }) => ...}
96
+ </StartDirectChat>;
97
+ ```
98
+
99
+ Get-or-create: a direct thread is keyed by the participant pair under a unique
100
+ constraint, so pressing twice — or writing to the same seller about a second
101
+ listing — lands in the same conversation.
102
+
103
+ ## The antd skin (opt-in)
104
+
105
+ ```tsx
106
+ import {
107
+ ConversationListPanel,
108
+ ConversationThreadPanel,
109
+ StartChatButton,
110
+ } from "@stapel/chat-react/default";
111
+ ```
112
+
113
+ Importing the subpath is the opt-in; consumers who bring their own visuals
114
+ never pull `antd` into their bundle.
115
+
116
+ ## Locales
117
+
118
+ ```tsx
119
+ import { registerChatI18nRu } from "@stapel/chat-react/i18n/ru";
120
+ import { registerChatI18nEs } from "@stapel/chat-react/i18n/es";
121
+ ```
122
+
123
+ Opt-in subpaths, so a host that ships one language carries one language. Both
124
+ locales are complete over the pair's UI copy and over every backend error code
125
+ the module can raise — including the twelve stapel-chat owns and does not
126
+ localize upstream (see `MODULE.md`).
@@ -0,0 +1,49 @@
1
+ import type { StapelClient } from "@stapel/core";
2
+ import type { ChatMessage, Conversation, ConversationKind, ConversationListParams, ConversationPage, MessageHistoryParams, MessagePage, SendMessageRequest } from "./types.js";
3
+ /**
4
+ * The pair's typed operation surface — one method per stapel-chat endpoint a
5
+ * buyer/seller client may call, bound to the injected {@link StapelClient}
6
+ * (the per-module override seam of frontend-standard §7.2). Paths are relative
7
+ * to the runtime's `baseUrl` (`/chat/api/v1`).
8
+ *
9
+ * **WRITES NEVER GO OVER THE SOCKET.** stapel-chat's `ChatConsumer` accepts a
10
+ * `send` frame, and this pair deliberately does not use it: a message is a
11
+ * durable row with a locked `seq` allocation, and the REST path is the one
12
+ * that answers with the persisted row (and with a real error envelope when it
13
+ * refuses). The socket is a delivery convenience over that journal — see
14
+ * `src/realtime/`.
15
+ *
16
+ * The support-operator half of the module (`/support/queue`, assign / resolve
17
+ * / reopen) is intentionally absent: it is an operator console, not the
18
+ * buyer-and-seller surface this pair ships (spec §4.5 "Not in MVP").
19
+ *
20
+ * These operations will be GENERATED from schema.json operationIds by gen-api
21
+ * v2; until then they are hand-authored here (the ONE legal home of path
22
+ * strings — `stapel/no-string-paths` §2.3 carve-out).
23
+ */
24
+ export interface ChatApi {
25
+ readonly client: StapelClient;
26
+ /** A page of the caller's conversations, newest activity first. */
27
+ conversations(params?: ConversationListParams): Promise<ConversationPage>;
28
+ /** One conversation (participant-only; 403 otherwise). */
29
+ conversation(conversationId: string): Promise<Conversation>;
30
+ /**
31
+ * Get-or-create a conversation. For `direct`, `participantIds` must name
32
+ * exactly one OTHER user: the thread is keyed by the (order-independent)
33
+ * participant pair, so a second call returns the same row — the idempotency
34
+ * "message the seller" is built on.
35
+ */
36
+ createConversation(kind: ConversationKind, participantIds?: readonly string[]): Promise<Conversation>;
37
+ /** A page of message history, anchored on `seq` (see {@link MessageHistoryParams}). */
38
+ messages(conversationId: string, params?: MessageHistoryParams): Promise<MessagePage>;
39
+ /** Append a message. Resolves with the persisted row, carrying its `seq`. */
40
+ sendMessage(conversationId: string, body: SendMessageRequest): Promise<ChatMessage>;
41
+ /**
42
+ * Advance the caller's read marker. The server never moves it backwards
43
+ * (`services.mark_read`), and neither does this pair — see
44
+ * `model/readMarker.ts`.
45
+ */
46
+ markRead(conversationId: string, uptoSeq: number): Promise<void>;
47
+ }
48
+ export declare function createChatApi(client: StapelClient): ChatApi;
49
+ //# sourceMappingURL=chatApi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chatApi.d.ts","sourceRoot":"","sources":["../../src/api/chatApi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAwB,MAAM,cAAc,CAAC;AACvE,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,oBAAoB,EACpB,WAAW,EACX,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAgCpB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAE9B,mEAAmE;IACnE,aAAa,CAAC,MAAM,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1E,0DAA0D;IAC1D,YAAY,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC5D;;;;;OAKG;IACH,kBAAkB,CAChB,IAAI,EAAE,gBAAgB,EACtB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,GACjC,OAAO,CAAC,YAAY,CAAC,CAAC;IACzB,uFAAuF;IACvF,QAAQ,CACN,cAAc,EAAE,MAAM,EACtB,MAAM,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,WAAW,CAAC,CAAC;IACxB,6EAA6E;IAC7E,WAAW,CACT,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,WAAW,CAAC,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CA2C3D"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * CSRF rule for cookie-authenticated browser clients (mirrors auth-react): the
3
+ * simplest SPA rule is to always send `X-Requested-With: XMLHttpRequest` on
4
+ * mutating requests. Header-token clients ignore it; it is harmless there, so
5
+ * every mutation carries it.
6
+ */
7
+ const CSRF_HEADERS = {
8
+ "X-Requested-With": "XMLHttpRequest",
9
+ };
10
+ function mutating(options) {
11
+ return {
12
+ ...options,
13
+ headers: { ...CSRF_HEADERS, ...options?.headers },
14
+ };
15
+ }
16
+ /** Anchor/direction/limit → a query object, omitting what was not asked for. */
17
+ function pageQuery(params) {
18
+ const query = {};
19
+ if (params?.anchor !== undefined)
20
+ query.anchor = params.anchor;
21
+ if (params?.direction !== undefined)
22
+ query.direction = params.direction;
23
+ if (params?.limit !== undefined)
24
+ query.limit = params.limit;
25
+ return query;
26
+ }
27
+ export function createChatApi(client) {
28
+ const conversationPath = (id) => `/conversations/${encodeURIComponent(id)}`;
29
+ return {
30
+ client,
31
+ conversations: (params) => client.get("/conversations", { query: pageQuery(params) }),
32
+ conversation: (conversationId) => client.get(conversationPath(conversationId)),
33
+ createConversation: (kind, participantIds) => client.post("/conversations", {
34
+ kind,
35
+ ...(participantIds !== undefined
36
+ ? { participant_ids: [...participantIds] }
37
+ : {}),
38
+ }, mutating()),
39
+ messages: (conversationId, params) => client.get(`${conversationPath(conversationId)}/messages`, {
40
+ query: pageQuery(params),
41
+ }),
42
+ sendMessage: (conversationId, body) => client.post(`${conversationPath(conversationId)}/messages`, body, mutating()),
43
+ markRead: (conversationId, uptoSeq) => client.post(`${conversationPath(conversationId)}/read`, { upto_seq: uptoSeq }, mutating()),
44
+ };
45
+ }
46
+ //# sourceMappingURL=chatApi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chatApi.js","sourceRoot":"","sources":["../../src/api/chatApi.ts"],"names":[],"mappings":"AAYA;;;;;GAKG;AACH,MAAM,YAAY,GAA2B;IAC3C,kBAAkB,EAAE,gBAAgB;CACrC,CAAC;AAEF,SAAS,QAAQ,CACf,OAAuD;IAEvD,OAAO;QACL,GAAG,OAAO;QACV,OAAO,EAAE,EAAE,GAAG,YAAY,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE;KAClD,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,SAAS,SAAS,CAChB,MAAiE;IAEjE,MAAM,KAAK,GAAoC,EAAE,CAAC;IAClD,IAAI,MAAM,EAAE,MAAM,KAAK,SAAS;QAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC/D,IAAI,MAAM,EAAE,SAAS,KAAK,SAAS;QAAE,KAAK,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;IACxE,IAAI,MAAM,EAAE,KAAK,KAAK,SAAS;QAAE,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC5D,OAAO,KAAK,CAAC;AACf,CAAC;AA0DD,MAAM,UAAU,aAAa,CAAC,MAAoB;IAChD,MAAM,gBAAgB,GAAG,CAAC,EAAU,EAAU,EAAE,CAC9C,kBAAkB,kBAAkB,CAAC,EAAE,CAAC,EAAE,CAAC;IAE7C,OAAO;QACL,MAAM;QAEN,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CACxB,MAAM,CAAC,GAAG,CAAC,gBAAgB,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;QAE5D,YAAY,EAAE,CAAC,cAAc,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC;QAE9E,kBAAkB,EAAE,CAAC,IAAI,EAAE,cAAc,EAAE,EAAE,CAC3C,MAAM,CAAC,IAAI,CACT,gBAAgB,EAChB;YACE,IAAI;YACJ,GAAG,CAAC,cAAc,KAAK,SAAS;gBAC9B,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,GAAG,cAAc,CAAC,EAAE;gBAC1C,CAAC,CAAC,EAAE,CAAC;SACR,EACD,QAAQ,EAAE,CACX;QAEH,QAAQ,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,CACnC,MAAM,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAAC,cAAc,CAAC,WAAW,EAAE;YACzD,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC;SACzB,CAAC;QAEJ,WAAW,EAAE,CAAC,cAAc,EAAE,IAAI,EAAE,EAAE,CACpC,MAAM,CAAC,IAAI,CACT,GAAG,gBAAgB,CAAC,cAAc,CAAC,WAAW,EAC9C,IAAI,EACJ,QAAQ,EAAE,CACX;QAEH,QAAQ,EAAE,CAAC,cAAc,EAAE,OAAO,EAAE,EAAE,CACpC,MAAM,CAAC,IAAI,CACT,GAAG,gBAAgB,CAAC,cAAc,CAAC,OAAO,EAC1C,EAAE,QAAQ,EAAE,OAAO,EAAE,EACrB,QAAQ,EAAE,CACX;KACJ,CAAC;AACJ,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Hand-authored API surface the codegen does not (yet) cover. Everything that
3
+ * CAN be derived from schema.json belongs in the generated operations
4
+ * (`api/chatApi.ts`), not here.
5
+ *
6
+ * ── One field the operation surface deliberately does NOT expose ────────────
7
+ *
8
+ * `CreateConversationRequest.scope_key` exists in the schema and its own
9
+ * description says the rest: *"Ignored — the scope is resolved server-side
10
+ * from the request"* (`views.ConversationListCreateView.post` calls
11
+ * `get_scope_provider().resolve(request)` and never reads the field). A client
12
+ * that sends a listing id there would believe it had scoped the thread to a
13
+ * listing; the server would key the direct thread by the participant PAIR, as
14
+ * it always does. Rather than ship a parameter that silently does nothing,
15
+ * `createConversation` has no `scopeKey` argument at all — the listing context
16
+ * belongs in the first MESSAGE, which is durable, visible, and actually read.
17
+ */
18
+ export {};
19
+ //# sourceMappingURL=extensions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../../src/api/extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Hand-authored API surface the codegen does not (yet) cover. Everything that
3
+ * CAN be derived from schema.json belongs in the generated operations
4
+ * (`api/chatApi.ts`), not here.
5
+ *
6
+ * ── One field the operation surface deliberately does NOT expose ────────────
7
+ *
8
+ * `CreateConversationRequest.scope_key` exists in the schema and its own
9
+ * description says the rest: *"Ignored — the scope is resolved server-side
10
+ * from the request"* (`views.ConversationListCreateView.post` calls
11
+ * `get_scope_provider().resolve(request)` and never reads the field). A client
12
+ * that sends a listing id there would believe it had scoped the thread to a
13
+ * listing; the server would key the direct thread by the participant PAIR, as
14
+ * it always does. Rather than ship a parameter that silently does nothing,
15
+ * `createConversation` has no `scopeKey` argument at all — the listing context
16
+ * belongs in the first MESSAGE, which is durable, visible, and actually read.
17
+ */
18
+ export {};
19
+ //# sourceMappingURL=extensions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extensions.js","sourceRoot":"","sources":["../../src/api/extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,CAAC"}