@crvouga/mockingbird-service-intercom 1.0.0 → 1.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.
package/CHANGELOG.md CHANGED
@@ -1,15 +1,17 @@
1
1
  # Changelog — @crvouga/mockingbird-service-intercom
2
2
 
3
+ ## 1.1.0 (2026-09-25)
4
+
5
+ ### Features
6
+
7
+ - metadata-only outbox, opt-in member webhook topics, and promote to ready ([4d635e9](https://github.com/crvouga/mockingbird/commit/4d635e90e1938dd5814ac33c10c92c768321a15b))
8
+
3
9
  ## 1.0.0 (2026-09-24)
4
10
 
5
11
  ### ⚠️ Breaking changes
6
12
 
7
13
  - vendor branding for every service; purge internal mocks and consumer names ([f689534](https://github.com/crvouga/mockingbird/commit/f6895343543550dafcb7d76b426cae7635e03226))
8
14
 
9
- ### Dependencies
10
-
11
- - `@crvouga/mockingbird-service-sqlite`
12
-
13
15
  ## 0.2.0 (2026-09-23)
14
16
 
15
17
  ### Features
package/README.md CHANGED
@@ -9,7 +9,8 @@ our backend makes:
9
9
  `?display_as=plaintext`, and cursor-paginated search;
10
10
  - **identity:** `/admins` and `/me`.
11
11
 
12
- Admin replies and close/open send Intercom's `notification_event` webhooks, signed with
12
+ Admin replies and close/open (and, for a receiver that subscribes to them, member messages and
13
+ replies) send Intercom's `notification_event` webhooks, signed with
13
14
  `X-Hub-Signature: sha1=<hex HMAC-SHA1>`, to both of our receivers: the backend's
14
15
  `POST /messaging/webhook` and the EMR's `POST /v1/webhooks/intercom`.
15
16
 
@@ -36,6 +37,10 @@ npx mockingbird-intercom serve --port 8807 \
36
37
  --webhook-secret "$INTERCOM_WEBHOOK_SECRET"
37
38
  ```
38
39
 
40
+ Add `--webhook-events conversation.user.created,conversation.user.replied,conversation.admin.replied,conversation.admin.closed,conversation.admin.opened`
41
+ to subscribe `--webhook-url` to the member topics too; `--emr-webhook-url` keeps the admin-only
42
+ default.
43
+
39
44
  | Consumer | Setting |
40
45
  | --- | --- |
41
46
  | Member messaging adapter (`intercom-messaging.adapter.ts`) | `INTERCOM_API_BASE_URL=http://127.0.0.1:8807` (already read), `INTERCOM_ACCESS_TOKEN=<any>` |
@@ -124,11 +129,14 @@ The mock sends webhooks for these events:
124
129
  | Close (`/parts` or `POST /__admin/conversations/:id/close`) | `conversation.admin.closed` |
125
130
  | Open (`/parts` or `POST /__admin/conversations/:id/open`) | `conversation.admin.opened` |
126
131
  | Admin-initiated conversation (`POST /__admin/conversations`) | `conversation.admin.single.created` |
132
+ | Member starts a conversation (`POST /conversations`) | `conversation.user.created` (opt-in) |
133
+ | Member reply (`/reply` with `type: "user"`) | `conversation.user.replied` (opt-in) |
127
134
 
128
- Snooze and assignment publish `conversation.admin.snoozed` and `.assigned`, but the default
129
- endpoints do not subscribe to them. Member messages and notes send nothing: the EMR treats every
130
- `notification_event` it receives as an admin event, so a real subscription must not include
131
- member topics either.
135
+ An endpoint receives the four admin topics above unless it lists its own `events`. The member
136
+ topics, `conversation.admin.snoozed` and `.assigned` are published but opt-in: subscribe with
137
+ `--webhook-events`, `webhooks.urls: [{url, events}]` or `PUT /__admin/webhook-endpoints`. The EMR
138
+ treats every `notification_event` it receives as an admin event, so its subscription must not
139
+ include member topics. Notes send nothing.
132
140
 
133
141
  Each delivery is a JSON body posted with `X-Hub-Signature: sha1=<hex HMAC-SHA1(secret, rawBody)>`:
134
142
 
@@ -156,7 +164,8 @@ Each delivery is a JSON body posted with `X-Hub-Signature: sha1=<hex HMAC-SHA1(s
156
164
 
157
165
  | Route | Effect |
158
166
  | --- | --- |
159
- | `POST /__admin/conversations/:id/admin-reply` | `{adminId? (default: first admin), body, messageType?: "comment" \| "note"}` appends an admin part and fires the webhook. |
167
+ | `POST /__admin/conversations/:id/admin-reply` | `{adminId? \| admin_id? (default: first admin), body, messageType?: "comment" \| "note"}` appends an admin part ("the care team replied") and fires the webhook. |
168
+ | `GET /__admin/outbox?since=&to=&limit=`, `GET /__admin/outbox/:id` | What the client sent through `POST /conversations` and `/reply`, oldest first: `{id, operation, conversationId, contactId, partId, partType ("source" for the opening message), authorType, authorId, hasBody, bodyLength, attachmentCount, at, createdAt}`. **Metadata only**: the body is never stored. `to=` matches the conversation or contact id; `since=` takes epoch ms or ISO-8601 on the mock clock. The admin plane's own replies are not listed. |
160
169
  | `POST /__admin/conversations/:id/close`, `…/open` | `{adminId?}` closes or reopens as an admin and fires the webhook. |
161
170
  | `POST /__admin/conversations` | `{contactId \| externalId, adminId?, body}` creates an admin-initiated conversation. The EMR then looks the member up by `contacts[0].external_id`. |
162
171
  | `GET /__admin/conversations`, `GET /__admin/contacts` | The namespace's records. |
@@ -205,14 +214,14 @@ never records message bodies or contact details.
205
214
  | --- | --- | --- |
206
215
  | `IntercomAPI` | class | The in-process mock: `fetch(request)`, `reset()`, `appendPart(…)`, `manage(…)`, `startAdminConversation(…)`, `conversations()`, `contacts()`, `conversationBody(…)`, `contactBody(…)`, `error(…)`, `state`. Options: `sqlite`, `now`, `wallClock`, `namespace`, `admins`, `settings`, `onWebhook`. |
207
216
  | `IntercomError` | class | A vendor error (status, code, message) raised by the admin-facing methods. |
208
- | `createRuntime` | function | The mock with the full service contract (health, admin, namespaces, credentials, presets, webhooks). Options: `webhooks: {urls, secret?, events?, retryDelaysMs?, fetch?}`, `admins`, `settings`, `clock`, `wallClock`, `seed`, `adminKey`, `onLog`, `sqlite`. |
217
+ | `createRuntime` | function | The mock with the full service contract (health, admin, namespaces, credentials, presets, webhooks). Options: `webhooks: {urls, secret?, events?, retryDelaysMs?, fetch?}` (a `urls` entry may be `{url, events}` to override `events` for that URL), `admins`, `settings`, `clock`, `wallClock`, `seed`, `adminKey`, `onLog`, `sqlite`. |
209
218
  | `INTERCOM_PRESETS` | object | Every named fault preset. |
210
219
  | `INTERCOM_NAMESPACE` | string | The service name, `"intercom"`. |
211
- | `INTERCOM_TOPICS` | array | Every webhook topic the mock can send. |
220
+ | `INTERCOM_TOPICS` | array | Every webhook topic the mock can send, including the opt-in member topics. |
212
221
  | `HUB_SIGNATURE_HEADER`, `signHub` | values | `X-Hub-Signature` and the `sha1=<hex>` signer, for your own receivers. |
213
222
  | `DEFAULT_ADMINS`, `DEFAULT_SETTINGS` | values | The seeded admins and default settings. |
214
223
  | `toHtml`, `toPlaintext`, `encodeCursor`, `decodeCursor` | functions | Body rendering and the search cursor format. |
215
224
  | `document`, `operationIds`, `supportedOperationIds` | values | The vendored OpenAPI contract and its operation ids. |
216
- | `createServer`, `serveTarget`, `DEFAULT_PORT` (`./server`) | Node | Serve over `node:http`; the `serve` CLI target (`--webhook-url`, `--emr-webhook-url`, `--webhook-secret`, `--access-token`); port 8807. |
225
+ | `createServer`, `serveTarget`, `DEFAULT_PORT` (`./server`) | Node | Serve over `node:http`; the `serve` CLI target (`--webhook-url`, `--webhook-events`, `--emr-webhook-url`, `--webhook-secret`, `--access-token`); port 8807. |
217
226
 
218
227
  Part of [mockingbird](https://github.com/crvouga/mockingbird).
@@ -1149,6 +1149,73 @@ var createMetrics = () => {
1149
1149
  };
1150
1150
  };
1151
1151
 
1152
+ // ../core/dist/outbox.js
1153
+ var OutboxStore = class {
1154
+ items;
1155
+ constructor(sqlite, namespace, name = "outbox") {
1156
+ this.items = new Collection(sqlite, namespace, name);
1157
+ }
1158
+ record(item) {
1159
+ this.items.insert(item.id, item);
1160
+ return item;
1161
+ }
1162
+ get(id) {
1163
+ return this.items.get(id);
1164
+ }
1165
+ update(id, item) {
1166
+ this.items.update(id, item);
1167
+ }
1168
+ /** Oldest first, so a suite reads messages in the order they were sent. */
1169
+ list(query = {}) {
1170
+ const to = query.to?.toLowerCase();
1171
+ const matched = this.items.list({ order: "oldest" }).map((row) => row.value).filter((item) => {
1172
+ if (to !== void 0) {
1173
+ const recipients = Array.isArray(item.to) ? item.to : [item.to];
1174
+ if (!recipients.some((r) => r.toLowerCase() === to))
1175
+ return false;
1176
+ }
1177
+ if (query.since !== void 0 && Date.parse(item.createdAt) < query.since)
1178
+ return false;
1179
+ if (query.where && !query.where(item))
1180
+ return false;
1181
+ return true;
1182
+ });
1183
+ return query.limit !== void 0 ? matched.slice(-Math.max(0, query.limit)) : matched;
1184
+ }
1185
+ };
1186
+ var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
1187
+ var parseSince = (value) => {
1188
+ if (value === null)
1189
+ return void 0;
1190
+ const parsed = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
1191
+ return Number.isNaN(parsed) ? null : parsed;
1192
+ };
1193
+ var outboxAdminRoutes = (runtime, pick, filter) => ({
1194
+ "GET /outbox": ({ url, namespace }) => {
1195
+ const since = parseSince(url.searchParams.get("since"));
1196
+ if (since === null) {
1197
+ return json2(400, {
1198
+ error: { type: "mockingbird_admin", message: "since: expected epoch ms or ISO-8601" }
1199
+ });
1200
+ }
1201
+ const limit = url.searchParams.get("limit");
1202
+ const where = filter?.(url.searchParams);
1203
+ const to = url.searchParams.get("to");
1204
+ return json2(200, {
1205
+ messages: pick(runtime.instance(namespace)).list({
1206
+ ...to !== null ? { to } : {},
1207
+ ...since !== void 0 ? { since } : {},
1208
+ ...where ? { where } : {},
1209
+ ...limit !== null && /^\d+$/.test(limit) ? { limit: Number(limit) } : {}
1210
+ })
1211
+ });
1212
+ },
1213
+ "GET /outbox/:id": ({ params, namespace }) => {
1214
+ const item = pick(runtime.instance(namespace)).get(params.id);
1215
+ return item ? json2(200, item) : json2(404, { error: { type: "mockingbird_admin", message: `no message ${params.id}` } });
1216
+ }
1217
+ });
1218
+
1152
1219
  // ../../core/dist/timeline.js
1153
1220
  var BRANCH_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
1154
1221
  var Timeline = class {
@@ -1531,7 +1598,7 @@ var restoreNamespace = (sqlite, namespace, snapshot) => {
1531
1598
  };
1532
1599
 
1533
1600
  // ../core/dist/version.js
1534
- var PACKAGE_VERSION = true ? "1.0.0" : UNRELEASED_VERSION;
1601
+ var PACKAGE_VERSION = true ? "1.1.0" : UNRELEASED_VERSION;
1535
1602
 
1536
1603
  // ../core/dist/signing.js
1537
1604
  var encoder = new TextEncoder();
@@ -1854,8 +1921,8 @@ var createWebhookHub = (options) => {
1854
1921
  };
1855
1922
  return hub;
1856
1923
  };
1857
- var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
1858
- var adminError2 = (status, message) => json2(status, { error: { type: "mockingbird_admin", message } });
1924
+ var json3 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
1925
+ var adminError2 = (status, message) => json3(status, { error: { type: "mockingbird_admin", message } });
1859
1926
  var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1860
1927
  var parseEndpoint = (value) => {
1861
1928
  if (!isRecord3(value) || typeof value.url !== "string")
@@ -1886,7 +1953,7 @@ var parseEndpoint = (value) => {
1886
1953
  return endpoint;
1887
1954
  };
1888
1955
  var webhookAdminRoutes = (hub) => ({
1889
- "GET /webhooks": ({ url, namespace }) => json2(200, {
1956
+ "GET /webhooks": ({ url, namespace }) => json3(200, {
1890
1957
  deliveries: hub.deliveries(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((d) => {
1891
1958
  const type = url.searchParams.get("type");
1892
1959
  return type === null || d.type === type;
@@ -1894,17 +1961,17 @@ var webhookAdminRoutes = (hub) => ({
1894
1961
  }),
1895
1962
  "GET /webhooks/events": ({ url, namespace }) => {
1896
1963
  const type = url.searchParams.get("type");
1897
- return json2(200, {
1964
+ return json3(200, {
1898
1965
  events: hub.messages(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((m) => type === null || m.type === type).map((m) => ({ ...m, payload: parsePayload(m) }))
1899
1966
  });
1900
1967
  },
1901
1968
  "POST /webhooks/:id/replay": async ({ params }) => {
1902
1969
  const replayed = await hub.replay(params.id);
1903
- return replayed ? json2(200, replayed) : adminError2(404, `no delivery ${params.id}`);
1970
+ return replayed ? json3(200, replayed) : adminError2(404, `no delivery ${params.id}`);
1904
1971
  },
1905
1972
  "POST /webhooks/flush": async () => {
1906
1973
  await hub.flush();
1907
- return json2(200, { status: "ok" });
1974
+ return json3(200, { status: "ok" });
1908
1975
  },
1909
1976
  "POST /webhooks/faults": ({ body, namespace }) => {
1910
1977
  if (!isRecord3(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
@@ -1914,9 +1981,9 @@ var webhookAdminRoutes = (hub) => ({
1914
1981
  if (typeof body.count === "number")
1915
1982
  fault.count = body.count;
1916
1983
  hub.fault(namespace, fault);
1917
- return json2(201, { namespace, ...fault });
1984
+ return json3(201, { namespace, ...fault });
1918
1985
  },
1919
- "GET /webhook-endpoints": ({ namespace }) => json2(200, {
1986
+ "GET /webhook-endpoints": ({ namespace }) => json3(200, {
1920
1987
  endpoints: hub.endpoints(namespace).map(({ secret, ...rest }) => ({
1921
1988
  ...rest,
1922
1989
  secret: secret ? "(set)" : null
@@ -1934,11 +2001,11 @@ var webhookAdminRoutes = (hub) => ({
1934
2001
  parsed.push(endpoint);
1935
2002
  }
1936
2003
  const set = hub.setEndpoints(namespace, parsed);
1937
- return json2(200, { endpoints: set.map((e) => ({ ...e, secret: e.secret ? "(set)" : null })) });
2004
+ return json3(200, { endpoints: set.map((e) => ({ ...e, secret: e.secret ? "(set)" : null })) });
1938
2005
  },
1939
2006
  "DELETE /webhook-endpoints": ({ namespace }) => {
1940
2007
  hub.setEndpoints(namespace, []);
1941
- return json2(200, { status: "ok" });
2008
+ return json3(200, { status: "ok" });
1942
2009
  }
1943
2010
  });
1944
2011
  var parsePayload = (message) => {
@@ -2683,6 +2750,7 @@ var IntercomState = class {
2683
2750
  this.admins = new Collection(sqlite, namespace, "admins");
2684
2751
  this.settings = new Collection(sqlite, namespace, "settings");
2685
2752
  this.counters = new Collection(sqlite, namespace, "counters");
2753
+ this.outbox = new OutboxStore(sqlite, namespace, "outbox");
2686
2754
  this.workspaceId = "mockapp";
2687
2755
  this.ensureSeeded();
2688
2756
  }
@@ -2693,6 +2761,7 @@ var IntercomState = class {
2693
2761
  admins;
2694
2762
  settings;
2695
2763
  counters;
2764
+ outbox;
2696
2765
  workspaceId;
2697
2766
  ensureSeeded() {
2698
2767
  if (this.admins.count() === 0) {
@@ -2810,8 +2879,8 @@ var INTERCOM_PRESETS = {
2810
2879
  webhook: { mode: "drop" }
2811
2880
  }
2812
2881
  };
2813
- var json3 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
2814
- var adminError3 = (status, message) => json3(status, { error: { type: "mockingbird_admin", message } });
2882
+ var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
2883
+ var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
2815
2884
  var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2816
2885
  var guard = (run) => {
2817
2886
  try {
@@ -2826,8 +2895,9 @@ var adminRoutes = (runtime) => {
2826
2895
  const conversation = (namespace, id) => api(namespace).state.conversations.get(id);
2827
2896
  const defaultAdmin = (namespace) => api(namespace).state.admins.list({ order: "oldest" }).at(0)?.value.id ?? "";
2828
2897
  return {
2829
- "GET /contacts": ({ namespace }) => json3(200, { contacts: api(namespace).contacts() }),
2830
- "GET /conversations": ({ namespace }) => json3(200, { conversations: api(namespace).conversations() }),
2898
+ ...outboxAdminRoutes(runtime, (instance) => instance.state.outbox),
2899
+ "GET /contacts": ({ namespace }) => json4(200, { contacts: api(namespace).contacts() }),
2900
+ "GET /conversations": ({ namespace }) => json4(200, { conversations: api(namespace).conversations() }),
2831
2901
  "POST /conversations": ({ body, namespace }) => guard(() => {
2832
2902
  if (!isRecord5(body) || typeof body.body !== "string") {
2833
2903
  return adminError3(400, 'expected {"contactId" | "externalId", "adminId"?, "body"}');
@@ -2840,7 +2910,7 @@ var adminRoutes = (runtime) => {
2840
2910
  adminId: typeof body.adminId === "string" ? body.adminId : defaultAdmin(namespace),
2841
2911
  body: body.body
2842
2912
  });
2843
- return json3(201, created);
2913
+ return json4(201, created);
2844
2914
  }),
2845
2915
  "POST /conversations/:id/admin-reply": ({ params, body, namespace }) => guard(() => {
2846
2916
  const found = conversation(namespace, params.id);
@@ -2848,32 +2918,33 @@ var adminRoutes = (runtime) => {
2848
2918
  if (!isRecord5(body) || typeof body.body !== "string") {
2849
2919
  return adminError3(
2850
2920
  400,
2851
- 'expected {"adminId"?, "body", "messageType"?: "comment" | "note"}'
2921
+ 'expected {"adminId" | "admin_id"?, "body", "messageType"?: "comment" | "note"}'
2852
2922
  );
2853
2923
  }
2854
2924
  const instance = api(namespace);
2925
+ const requested = body.adminId ?? body.admin_id;
2855
2926
  const author = instance.state.admins.get(
2856
- typeof body.adminId === "string" ? body.adminId : defaultAdmin(namespace)
2927
+ typeof requested === "string" ? requested : defaultAdmin(namespace)
2857
2928
  );
2858
- if (!author) return adminError3(404, `no admin ${String(body.adminId)}`);
2929
+ if (!author) return adminError3(404, `no admin ${String(requested)}`);
2859
2930
  const next = instance.appendPart(found, {
2860
2931
  partType: body.messageType === "note" ? "note" : "comment",
2861
2932
  author: { type: "admin", id: author.id, name: author.name, email: author.email },
2862
2933
  body: toHtml(body.body)
2863
2934
  });
2864
- return json3(200, next);
2935
+ return json4(200, next);
2865
2936
  }),
2866
2937
  "POST /conversations/:id/close": ({ params, body, namespace }) => guard(() => {
2867
2938
  const found = conversation(namespace, params.id);
2868
2939
  if (!found) return adminError3(404, `no conversation ${params.id}`);
2869
2940
  const adminId = isRecord5(body) && typeof body.adminId === "string" ? body.adminId : defaultAdmin(namespace);
2870
- return json3(200, api(namespace).manage(found, { action: "close", adminId }));
2941
+ return json4(200, api(namespace).manage(found, { action: "close", adminId }));
2871
2942
  }),
2872
2943
  "POST /conversations/:id/open": ({ params, body, namespace }) => guard(() => {
2873
2944
  const found = conversation(namespace, params.id);
2874
2945
  if (!found) return adminError3(404, `no conversation ${params.id}`);
2875
2946
  const adminId = isRecord5(body) && typeof body.adminId === "string" ? body.adminId : defaultAdmin(namespace);
2876
- return json3(200, api(namespace).manage(found, { action: "open", adminId }));
2947
+ return json4(200, api(namespace).manage(found, { action: "open", adminId }));
2877
2948
  }),
2878
2949
  "PUT /admins": ({ body, namespace }) => {
2879
2950
  const list = Array.isArray(body) ? body : isRecord5(body) ? body.admins : void 0;
@@ -2895,9 +2966,9 @@ var adminRoutes = (runtime) => {
2895
2966
  team_ids: []
2896
2967
  });
2897
2968
  }
2898
- return json3(200, { admins: state.admins.list({ order: "oldest" }).map((row) => row.value) });
2969
+ return json4(200, { admins: state.admins.list({ order: "oldest" }).map((row) => row.value) });
2899
2970
  },
2900
- "GET /settings": ({ namespace }) => json3(200, api(namespace).state.current()),
2971
+ "GET /settings": ({ namespace }) => json4(200, api(namespace).state.current()),
2901
2972
  "PUT /settings": ({ body, namespace }) => {
2902
2973
  if (!isRecord5(body)) return adminError3(400, "expected a JSON object");
2903
2974
  const patch = {};
@@ -2911,7 +2982,7 @@ var adminRoutes = (runtime) => {
2911
2982
  }
2912
2983
  patch.customAttributes = body.customAttributes === null ? null : body.customAttributes.map(String);
2913
2984
  }
2914
- return json3(200, api(namespace).state.update(patch));
2985
+ return json4(200, api(namespace).state.update(patch));
2915
2986
  }
2916
2987
  };
2917
2988
  };
@@ -2924,14 +2995,15 @@ var createRuntime2 = (options = {}) => {
2924
2995
  ...hooks?.retryDelaysMs ? { retryDelaysMs: hooks.retryDelaysMs } : {},
2925
2996
  ...hooks?.fetch ? { fetch: hooks.fetch } : {},
2926
2997
  ...options.wallClock ? { now: options.wallClock } : {},
2927
- endpoints: (hooks?.urls ?? []).map(
2928
- (url, index) => ({
2998
+ endpoints: (hooks?.urls ?? []).map((entry, index) => {
2999
+ const target = typeof entry === "string" ? { url: entry } : entry;
3000
+ return {
2929
3001
  id: `we_intercom_${index}`,
2930
- url,
3002
+ url: target.url,
2931
3003
  ...hooks?.secret ? { secret: hooks.secret } : {},
2932
- events: [...hooks?.events ?? DEFAULT_TOPICS]
2933
- })
2934
- )
3004
+ events: [...target.events ?? hooks?.events ?? DEFAULT_TOPICS]
3005
+ };
3006
+ })
2935
3007
  });
2936
3008
  const runtime = createRuntime({
2937
3009
  name: INTERCOM_NAMESPACE,
@@ -2972,7 +3044,9 @@ var INTERCOM_TOPICS = [
2972
3044
  "conversation.admin.opened",
2973
3045
  "conversation.admin.snoozed",
2974
3046
  "conversation.admin.assigned",
2975
- "conversation.admin.single.created"
3047
+ "conversation.admin.single.created",
3048
+ "conversation.user.created",
3049
+ "conversation.user.replied"
2976
3050
  ];
2977
3051
  var CONTACT_FIELDS = [
2978
3052
  "id",
@@ -3407,6 +3481,17 @@ var IntercomAPI = class {
3407
3481
  parts: []
3408
3482
  };
3409
3483
  this.state.conversations.insert(conversation.id, conversation);
3484
+ const text2 = String(body.body);
3485
+ this.recordSent({
3486
+ operation: "CreateConversation",
3487
+ conversation,
3488
+ partId: null,
3489
+ partType: "source",
3490
+ author: conversation.source.author,
3491
+ bodyLength: text2.length,
3492
+ attachmentCount: 0
3493
+ });
3494
+ this.notify("conversation.user.created", conversation, []);
3410
3495
  return annotateResponse(
3411
3496
  jsonRes(200, {
3412
3497
  type: "user_message",
@@ -3577,6 +3662,15 @@ var IntercomAPI = class {
3577
3662
  body: body === void 0 ? null : toHtml(body),
3578
3663
  attachments: stored
3579
3664
  });
3665
+ this.recordSent({
3666
+ operation: "ReplyConversation",
3667
+ conversation: next,
3668
+ partId,
3669
+ partType: messageType,
3670
+ author,
3671
+ bodyLength: body?.length ?? 0,
3672
+ attachmentCount: stored.length
3673
+ });
3580
3674
  return annotateResponse(jsonRes(200, this.conversationBody(next, { parts: next.parts })), {
3581
3675
  ids: { conversationId: next.id, partId }
3582
3676
  });
@@ -3611,8 +3705,29 @@ var IntercomAPI = class {
3611
3705
  };
3612
3706
  this.state.conversations.update(conversation.id, next);
3613
3707
  if (byAdmin && visible) this.notify("conversation.admin.replied", next, [part]);
3708
+ if (input.author.type === "user") this.notify("conversation.user.replied", next, [part]);
3614
3709
  return next;
3615
3710
  }
3711
+ /** Record what the API client sent, for `GET /__admin/outbox`: metadata only, never the body. */
3712
+ recordSent(input) {
3713
+ const at = new Date(this.now()).toISOString();
3714
+ this.state.outbox.record({
3715
+ id: `sent_${this.state.next("sent")}`,
3716
+ to: [input.conversation.id, input.conversation.contactId],
3717
+ createdAt: at,
3718
+ at,
3719
+ operation: input.operation,
3720
+ conversationId: input.conversation.id,
3721
+ contactId: input.conversation.contactId,
3722
+ partId: input.partId,
3723
+ partType: input.partType,
3724
+ authorType: input.author.type === "admin" ? "admin" : "user",
3725
+ authorId: input.author.id,
3726
+ hasBody: input.bodyLength > 0,
3727
+ bodyLength: input.bodyLength,
3728
+ attachmentCount: input.attachmentCount
3729
+ });
3730
+ }
3616
3731
  manageConversation(context) {
3617
3732
  return this.withConversation(context, (conversation) => {
3618
3733
  const body = this.json(context);
@@ -3832,4 +3947,4 @@ export {
3832
3947
  IntercomError,
3833
3948
  IntercomAPI
3834
3949
  };
3835
- //# sourceMappingURL=chunk-OSBG2XJ7.js.map
3950
+ //# sourceMappingURL=chunk-DOY3BICO.js.map