Skip to content

Commit 9b4e870

Browse files
sung17Deathgiver
andauthored
fix(webchat): accept the minted guest conversation id in message schemas (#1111)
`createGuestConversationId` mints `<workspaceId>:<uuid>`, but both the send mutation and the guest message list validated it with `zodBigintAsString()`, so every new visitor failed validation and the widget's send button never enabled. Validate with a dedicated `zodGuestConversationId()` that accepts the current form plus the legacy digits-only Snowflake still in localStorage. Co-authored-by: Deathgiver <anonymous@users.noreply.github.com>
1 parent cfa9311 commit 9b4e870

4 files changed

Lines changed: 78 additions & 2 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { describe, expect, test } from "vitest"
2+
import { createGuestConversationId } from "@/features/integration-webchat/lib/guest-conversation-id"
3+
import { createWebchatMessageRequest } from "@/features/messages/schema/mutation"
4+
import { listGuestMessagesRequest } from "@/features/messages/schema/query"
5+
6+
const WORKSPACE_ID = "11530439376896"
7+
const WEBCHAT_ID = "11616773281153024"
8+
9+
const baseInput = (guestConversationId: string) => ({
10+
text: "hello",
11+
workspaceId: WORKSPACE_ID,
12+
webchatId: WEBCHAT_ID,
13+
guestConversationId,
14+
})
15+
16+
describe("createWebchatMessageRequest — guestConversationId", () => {
17+
test("accepts the `<workspaceId>:<uuid>` id the server actually mints — a digits-only rule here leaves the widget's send button permanently disabled, since the form only enables it on formState.isValid", () => {
18+
const result = createWebchatMessageRequest.safeParse(
19+
baseInput(createGuestConversationId(WORKSPACE_ID)),
20+
)
21+
22+
expect(result.success).toBe(true)
23+
})
24+
25+
test("still accepts a legacy digits-only Snowflake id, which returning visitors carry in localStorage", () => {
26+
const result = createWebchatMessageRequest.safeParse(
27+
baseInput("11616773281153025"),
28+
)
29+
30+
expect(result.success).toBe(true)
31+
})
32+
33+
test("rejects an arbitrary string — the id is the only proof a caller owns the guest session, so the format stays tight", () => {
34+
const result = createWebchatMessageRequest.safeParse(
35+
baseInput("../../not-an-id"),
36+
)
37+
38+
expect(result.success).toBe(false)
39+
})
40+
41+
test("rejects a non-uuid suffix", () => {
42+
const result = createWebchatMessageRequest.safeParse(
43+
baseInput(`${WORKSPACE_ID}:guest-1`),
44+
)
45+
46+
expect(result.success).toBe(false)
47+
})
48+
})
49+
50+
describe("listGuestMessagesRequest — guestConversationId", () => {
51+
test("accepts the minted id so history loads for a new visitor", () => {
52+
const result = listGuestMessagesRequest.safeParse({
53+
workspaceId: WORKSPACE_ID,
54+
webchatId: WEBCHAT_ID,
55+
guestConversationId: createGuestConversationId(WORKSPACE_ID),
56+
})
57+
58+
expect(result.success).toBe(true)
59+
})
60+
})

apps/builder/src/features/integration-webchat/lib/guest-conversation-id.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { z } from "zod"
2+
13
// A cryptographically random (128-bit), unguessable id — not the sequential
24
// Snowflake createId(). The webchat access token is session-scoped only (see
35
// webchat-access-token.ts) and does not bind to this id, so possession of the
@@ -6,3 +8,15 @@
68
// valid token for a stranger's conversation just by guessing nearby ids.
79
export const createGuestConversationId = (workspaceId: string) =>
810
`${workspaceId}:${crypto.randomUUID()}`
11+
12+
// Two accepted shapes, and both must stay accepted: the `<workspaceId>:<uuid>`
13+
// form minted above, and the legacy digits-only Snowflake that returning
14+
// visitors still carry in localStorage (migrated by readLegacyGuestId). This is
15+
// deliberately NOT zodBigintAsString() — the current form is not digits-only,
16+
// and validating it as a bigint leaves every new visitor's message input
17+
// permanently invalid (the send button never enables).
18+
const GUEST_CONVERSATION_ID_REGEX =
19+
/^\d+(?::[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})?$/i
20+
21+
export const zodGuestConversationId = () =>
22+
z.string().regex(GUEST_CONVERSATION_ID_REGEX)

apps/builder/src/features/messages/schema/mutation.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { channelTypes } from "@chatbotx.io/database/partials"
22
import { zodBigintAsString } from "@chatbotx.io/utils"
33
import { z } from "zod"
4+
import { zodGuestConversationId } from "@/features/integration-webchat/lib/guest-conversation-id"
45

56
const MAX_FILE_SIZE = 5 * 1000 * 1000
67

@@ -115,7 +116,7 @@ export const createWebchatMessageRequest = z
115116
clientId: z.string().optional(),
116117
workspaceId: zodBigintAsString(),
117118
webchatId: zodBigintAsString(),
118-
guestConversationId: zodBigintAsString(),
119+
guestConversationId: zodGuestConversationId(),
119120
ref: z.string().optional(),
120121
parentUrl: z.url().max(2048).optional(),
121122
locale: z.string().max(35).optional(),

apps/builder/src/features/messages/schema/query.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { zodBigintAsString } from "@chatbotx.io/utils"
22
import { z } from "zod"
33
import { attachmentResource } from "@/features/attachments/schema/resource"
44
import { contactResource } from "@/features/contacts/schema/resource"
5+
import { zodGuestConversationId } from "@/features/integration-webchat/lib/guest-conversation-id"
56
import { userResource } from "@/features/users/schema/resource"
67
import { messageResource } from "./resource"
78

@@ -42,7 +43,7 @@ export const listGuestMessagesRequest = z.object({
4243
webchatId: zodBigintAsString(),
4344
perPage: z.coerce.number().optional().default(20),
4445
cursor: z.string().optional(),
45-
guestConversationId: zodBigintAsString(),
46+
guestConversationId: zodGuestConversationId(),
4647
accessToken: z.string().optional(),
4748
parentOrigin: z.string().optional(),
4849
})

0 commit comments

Comments
 (0)