@catandbox/schrodinger-web-adapter 0.1.28 → 0.1.32

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/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # @catandbox/schrodinger-web-adapter
2
+
3
+ Framework-agnostic web adapter for [Schrodinger](https://github.com/yetone/schrodinger) — an AI-powered headless helpdesk built on Cloudflare Workers.
4
+
5
+ This package provides everything needed to embed a full customer-facing support portal into any web app, plus the server-side Shopify proxy and webhook utilities. **No React dependency** — the UI renders with [Shopify Polaris Web Components](https://shopify.dev/docs/api/app-home/polaris-web-components).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @catandbox/schrodinger-web-adapter
11
+ ```
12
+
13
+ ## Package exports
14
+
15
+ | Entry point | Contents |
16
+ |---|---|
17
+ | `@catandbox/schrodinger-web-adapter/client` | Portal UI renderer + `SupportApiClient` |
18
+ | `@catandbox/schrodinger-web-adapter/server` | Shopify proxy handler, webhook forwarders, auth utilities |
19
+ | `@catandbox/schrodinger-web-adapter/signer` | HMAC request signing primitives |
20
+
21
+ ---
22
+
23
+ ## Client — portal UI
24
+
25
+ Renders a complete support portal (ticket list → detail → new ticket form) into any `HTMLElement`. Requires Polaris Web Components to be loaded on the page.
26
+
27
+ ```ts
28
+ import { renderSupportPortal } from "@catandbox/schrodinger-web-adapter/client";
29
+
30
+ const container = document.getElementById("support-root")!;
31
+
32
+ await renderSupportPortal(container, {
33
+ basePath: "/support/api", // path where the server proxy is mounted
34
+ // headers: { "Authorization": "Bearer ..." },
35
+ // getHeaders: async () => ({ "Authorization": `Bearer ${await getToken()}` }),
36
+ });
37
+ ```
38
+
39
+ ### Individual renderers
40
+
41
+ You can also render each view independently:
42
+
43
+ ```ts
44
+ import {
45
+ renderTicketList,
46
+ renderTicketDetail,
47
+ renderNewTicketForm,
48
+ EventEmitter,
49
+ } from "@catandbox/schrodinger-web-adapter/client";
50
+
51
+ const emitter = new EventEmitter();
52
+
53
+ await renderTicketList(container, client, emitter);
54
+ // emitter emits "ticket:select", "ticket:create"
55
+
56
+ await renderTicketDetail(container, client, ticketId, emitter);
57
+ // emitter emits "ticket:back", "ticket:closed", "ticket:reopened"
58
+
59
+ await renderNewTicketForm(container, client, categories, emitter);
60
+ // emitter emits "ticket:created", "ticket:cancel"
61
+ ```
62
+
63
+ ### SupportApiClient
64
+
65
+ The API client is re-exported for convenience — see [`@catandbox/schrodinger-contracts`](https://www.npmjs.com/package/@catandbox/schrodinger-contracts) for full documentation.
66
+
67
+ ```ts
68
+ import { SupportApiClient } from "@catandbox/schrodinger-web-adapter/client";
69
+
70
+ const client = new SupportApiClient({ basePath: "/support/api" });
71
+ const { items } = await client.listTickets();
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Server — Shopify proxy
77
+
78
+ The server module provides a ready-made HTTP proxy that sits between your Shopify app's frontend and the Schrodinger API. It verifies the Shopify session token on every request and signs outbound calls with HMAC credentials.
79
+
80
+ ```ts
81
+ import { createShopifyProxyHandler } from "@catandbox/schrodinger-web-adapter/server";
82
+
83
+ const handler = createShopifyProxyHandler({
84
+ schApiBaseUrl: "https://your-schrodinger-api.example.com",
85
+ schAppId: env.SCH_APP_ID,
86
+ schKeyId: env.SCH_KEY_ID,
87
+ schSecret: env.SCH_SECRET,
88
+ shopifyApiKey: env.SHOPIFY_API_KEY,
89
+ shopifyApiSecret: env.SHOPIFY_API_SECRET,
90
+ basePath: "/support/api", // optional, default: "/support/api"
91
+ });
92
+
93
+ // Cloudflare Workers / any fetch-based runtime:
94
+ export default {
95
+ async fetch(request: Request): Promise<Response> {
96
+ return handler(request);
97
+ },
98
+ };
99
+ ```
100
+
101
+ ### GDPR webhook forwarding
102
+
103
+ ```ts
104
+ import { createShopifyWebhookHandlers } from "@catandbox/schrodinger-web-adapter/server";
105
+
106
+ const webhooks = createShopifyWebhookHandlers({
107
+ schApiBaseUrl: "https://your-schrodinger-api.example.com",
108
+ schAdminApiToken: env.SCH_ADMIN_API_TOKEN,
109
+ shopifyApiKey: env.SHOPIFY_API_KEY,
110
+ shopifyApiSecret: env.SHOPIFY_API_SECRET,
111
+ // called on app/uninstalled to revoke portal access:
112
+ disablePortalAccess: async ({ shopDomain }) => { /* ... */ },
113
+ });
114
+
115
+ // Wire up to your router:
116
+ router.post("/webhooks/customers/data_request", webhooks.handleCustomersDataRequestWebhook);
117
+ router.post("/webhooks/customers/redact", webhooks.handleCustomersRedactWebhook);
118
+ router.post("/webhooks/shop/redact", webhooks.handleShopRedactWebhook);
119
+ router.post("/webhooks/app/uninstalled", webhooks.handleAppUninstalledWebhook);
120
+ ```
121
+
122
+ ### Auth utilities
123
+
124
+ ```ts
125
+ import {
126
+ createPrincipalContext,
127
+ verifyShopifySessionToken,
128
+ verifyShopifyWebhookHmac,
129
+ ShopifyAuthError,
130
+ } from "@catandbox/schrodinger-web-adapter/server";
131
+
132
+ // Verify a Shopify session JWT and extract principal info
133
+ const principal = await createPrincipalContext(request, {
134
+ shopifyApiKey: env.SHOPIFY_API_KEY,
135
+ shopifyApiSecret: env.SHOPIFY_API_SECRET,
136
+ });
137
+ // principal.tenantExternalId — shop domain
138
+ // principal.principalExternalId — customer identifier
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Signer — HMAC request signing
144
+
145
+ Low-level primitives for signing requests to the Schrodinger API with `HMAC-SHA256`. Useful when making server-to-server calls outside of the proxy handler.
146
+
147
+ ```ts
148
+ import { signRequest, sha256Hex } from "@catandbox/schrodinger-web-adapter/signer";
149
+
150
+ const headers = await signRequest({
151
+ appId: "my-app",
152
+ keyId: "key-1",
153
+ keySecret: "secret",
154
+ timestamp: Math.floor(Date.now() / 1000),
155
+ nonce: crypto.randomUUID(),
156
+ method: "POST",
157
+ path: "/v1/tickets",
158
+ rawBody: JSON.stringify({ title: "Hello" }),
159
+ });
160
+
161
+ // headers["X-Sch-AppId"], ["X-Sch-KeyId"], ["X-Sch-Timestamp"],
162
+ // ["X-Sch-Nonce"], ["Content-SHA256"], ["X-Sch-Signature"]
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Requirements
168
+
169
+ - **Runtime**: any environment with `fetch`, `crypto.subtle`, and `TextEncoder` (Cloudflare Workers, modern browsers, Node.js ≥ 18)
170
+ - **UI**: Polaris Web Components must be loaded on the page before calling client renderers
171
+
172
+ ## License
173
+
174
+ MIT
@@ -1,4 +1,4 @@
1
- import type { SupportApiClient } from "@catandbox/schrodinger-shopify-adapter/client";
1
+ import type { SupportApiClient } from "@catandbox/schrodinger-contracts";
2
2
  interface SelectedFile {
3
3
  file: File;
4
4
  id: string;
@@ -2,8 +2,8 @@
2
2
  * Client-side exports — Vanilla TS + Polaris Web Components UI.
3
3
  * No React dependency.
4
4
  */
5
- export { SupportApiClient, SupportApiError } from "@catandbox/schrodinger-shopify-adapter/client";
6
- export type { ClientHeadersProvider, ListTicketsParams, SupportCategory, SupportClientOptions, TicketDetailData, UploadCompleteInput, UploadInitResult, UploadInputFile } from "@catandbox/schrodinger-shopify-adapter/client";
5
+ export { SupportApiClient, SupportApiError } from "@catandbox/schrodinger-contracts";
6
+ export type { ClientHeadersProvider, ListTicketsParams, SupportCategory, SupportClientOptions, TicketDetailData, UploadCompleteInput, UploadInitResult, UploadInputFile } from "@catandbox/schrodinger-contracts";
7
7
  export { renderSupportPortal } from "./portal";
8
8
  export type { SupportPortalOptions } from "./portal";
9
9
  export { renderTicketList } from "./ticket-list";
@@ -2,8 +2,7 @@
2
2
  * Client-side exports — Vanilla TS + Polaris Web Components UI.
3
3
  * No React dependency.
4
4
  */
5
- // Re-export the pure-fetch API client from the React adapter
6
- export { SupportApiClient, SupportApiError } from "@catandbox/schrodinger-shopify-adapter/client";
5
+ export { SupportApiClient, SupportApiError } from "@catandbox/schrodinger-contracts";
7
6
  // Vanilla rendering functions
8
7
  export { renderSupportPortal } from "./portal";
9
8
  export { renderTicketList } from "./ticket-list";
@@ -1,4 +1,4 @@
1
- import type { SupportApiClient, SupportCategory } from "@catandbox/schrodinger-shopify-adapter/client";
1
+ import type { SupportApiClient, SupportCategory } from "@catandbox/schrodinger-contracts";
2
2
  import type { EventEmitter } from "./events";
3
3
  export interface NewTicketFormEvents {
4
4
  [key: string]: unknown;
@@ -1,4 +1,4 @@
1
- import type { SupportClientOptions, SupportCategory } from "@catandbox/schrodinger-shopify-adapter/client";
1
+ import type { SupportClientOptions, SupportCategory } from "@catandbox/schrodinger-contracts";
2
2
  export interface SupportPortalOptions extends SupportClientOptions {
3
3
  /** Pre-loaded categories. If omitted, categories are fetched from the API. */
4
4
  categories?: SupportCategory[];
@@ -1,4 +1,4 @@
1
- import { SupportApiClient } from "@catandbox/schrodinger-shopify-adapter/client";
1
+ import { SupportApiClient } from "@catandbox/schrodinger-contracts";
2
2
  import { EventEmitter } from "./events";
3
3
  import { renderTicketList } from "./ticket-list";
4
4
  import { renderTicketDetail } from "./ticket-detail";
@@ -1,9 +1,9 @@
1
1
  const STATUS_CONFIG = {
2
2
  Active: { label: "Active", tone: "info" },
3
- InProgress: { label: "In Progress", tone: "attention" },
3
+ InProgress: { label: "In Progress", tone: "caution" },
4
4
  AwaitingResponse: { label: "Awaiting Response", tone: "warning" },
5
5
  Closed: { label: "Closed", tone: "success" },
6
- Archived: { label: "Archived", tone: "subdued" }
6
+ Archived: { label: "Archived", tone: "neutral" }
7
7
  };
8
8
  export function renderStatusBadge(status) {
9
9
  const config = STATUS_CONFIG[status] ?? { label: status, tone: "subdued" };
@@ -1,4 +1,4 @@
1
- import type { SupportApiClient } from "@catandbox/schrodinger-shopify-adapter/client";
1
+ import type { SupportApiClient } from "@catandbox/schrodinger-contracts";
2
2
  import type { EventEmitter } from "./events";
3
3
  export interface TicketDetailEvents {
4
4
  [key: string]: unknown;
@@ -28,9 +28,9 @@ export async function renderTicketDetail(container, client, ticketId, emitter) {
28
28
  container.innerHTML = `
29
29
  <s-card>
30
30
  <s-box padding="large">
31
- <div style="display:flex; justify-content:center; padding:32px 0;">
32
- <s-spinner size="small"></s-spinner>
33
- </div>
31
+ <s-stack direction="inline" justifyContent="center" padding="large none">
32
+ <s-spinner></s-spinner>
33
+ </s-stack>
34
34
  </s-box>
35
35
  </s-card>
36
36
  `;
@@ -44,89 +44,89 @@ export async function renderTicketDetail(container, client, ticketId, emitter) {
44
44
  const aliasMap = new Map(portalConfig.aliases.map((a) => [a.id, a]));
45
45
  const assignedAlias = ticket.assignedAliasId ? (aliasMap.get(ticket.assignedAliasId) ?? null) : null;
46
46
  setInnerHtml(container, `
47
- <s-card>
48
- <s-box padding="large">
49
- <s-stack gap="large">
50
- <div>
51
- <s-button variant="plain" id="sch-back-btn">
52
- <span style="display:inline-flex; align-items:center; gap:4px;">
53
- <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M12.707 15.707a1 1 0 0 1-1.414 0l-5-5a1 1 0 0 1 0-1.414l5-5a1 1 0 1 1 1.414 1.414L8.414 10l4.293 4.293a1 1 0 0 1 0 1.414z"/></svg>
54
- All tickets
55
- </span>
56
- </s-button>
57
- </div>
47
+ <s-stack gap="base">
48
+ <s-card>
49
+ <s-box padding="large">
50
+ <s-stack gap="large">
51
+ <div>
52
+ <s-button variant="tertiary" id="sch-back-btn">
53
+ <span style="display:inline-flex; align-items:center; gap:4px;">
54
+ <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M12.707 15.707a1 1 0 0 1-1.414 0l-5-5a1 1 0 0 1 0-1.414l5-5a1 1 0 1 1 1.414 1.414L8.414 10l4.293 4.293a1 1 0 0 1 0 1.414z"/></svg>
55
+ All tickets
56
+ </span>
57
+ </s-button>
58
+ </div>
58
59
 
59
- <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:16px;">
60
- <div style="min-width:0; flex:1;">
61
- <s-text variant="headingLg">${escapeHtml(ticket.title)}</s-text>
62
- <div style="margin-top:4px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
63
- ${renderStatusBadge(ticket.status)}
64
- <s-text variant="bodySm" tone="subdued">#${ticket.id.slice(0, 8)}</s-text>
65
- <s-text variant="bodySm" tone="subdued">&middot;</s-text>
66
- <s-text variant="bodySm" tone="subdued">Created ${formatFullDate(ticket.createdAt)}</s-text>
60
+ <s-stack direction="inline" justifyContent="space-between" alignItems="start" gap="base">
61
+ <div style="min-width:0; flex:1;">
62
+ <s-heading>${escapeHtml(ticket.title)}</s-heading>
63
+ <s-stack direction="inline" alignItems="center" gap="small" style="margin-top:4px; flex-wrap:wrap;">
64
+ ${renderStatusBadge(ticket.status)}
65
+ <s-text tone="neutral">#${ticket.id.slice(0, 8)}</s-text>
66
+ <s-text tone="neutral">&middot;</s-text>
67
+ <s-text tone="neutral">Created ${formatFullDate(ticket.createdAt)}</s-text>
68
+ </s-stack>
67
69
  </div>
68
- </div>
69
- ${!isOpen ? `<s-button id="sch-reopen-ticket-btn" size="slim">Reopen</s-button>` : ""}
70
- </div>
71
- </s-stack>
72
- </s-box>
73
- </s-card>
70
+ ${!isOpen ? `<s-button id="sch-reopen-ticket-btn">Reopen</s-button>` : ""}
71
+ </s-stack>
72
+ </s-stack>
73
+ </s-box>
74
+ </s-card>
74
75
 
75
- ${assignedAlias
76
+ ${assignedAlias
76
77
  ? `
77
- <s-card style="margin-top:16px;">
78
- <s-box padding="base large">
79
- <div style="display:flex; align-items:center; gap:10px;">
80
- <div style="width:36px; height:36px; border-radius:50%; background:var(--p-color-bg-fill-info, #e4f0fe); display:flex; align-items:center; justify-content:center; flex-shrink:0;">
81
- <span style="font-size:13px; font-weight:700; color:var(--p-color-text-info, #2c6ecb);">${escapeHtml(getInitials(assignedAlias.displayName))}</span>
82
- </div>
83
- <div>
84
- <s-text variant="bodySm" fontWeight="semibold">${escapeHtml(assignedAlias.displayName)}</s-text>
85
- <div><s-text variant="bodySm" tone="subdued">${escapeHtml(assignedAlias.department)}</s-text></div>
86
- </div>
87
- </div>
88
- </s-box>
89
- </s-card>`
78
+ <s-card>
79
+ <s-box padding="base large">
80
+ <s-stack direction="inline" alignItems="center" gap="small">
81
+ <s-avatar size="base" initials="${escapeHtml(getInitials(assignedAlias.displayName))}"${assignedAlias.avatarUrl ? ` src="${escapeHtml(assignedAlias.avatarUrl)}"` : ""}></s-avatar>
82
+ <div>
83
+ <s-text type="strong">${escapeHtml(assignedAlias.displayName)}</s-text>
84
+ <div><s-text tone="neutral">${escapeHtml(assignedAlias.department)}</s-text></div>
85
+ </div>
86
+ </s-stack>
87
+ </s-box>
88
+ </s-card>`
90
89
  : ""}
91
90
 
92
- <div id="sch-messages-list" style="margin-top:16px;">
93
- ${messages.length === 0
91
+ <div id="sch-messages-list">
92
+ ${messages.length === 0
94
93
  ? `
95
- <s-card>
96
- <s-box padding="large">
97
- <div style="text-align:center; padding:24px 0;">
98
- <s-text tone="subdued">No messages yet. Our team will respond shortly.</s-text>
99
- </div>
100
- </s-box>
101
- </s-card>`
102
- : `<div style="display:flex; flex-direction:column; gap:2px;">
103
- ${messages.map((msg) => renderMessage(msg, aliasMap.get(msg.authorAliasId ?? "") ?? null)).join("")}
104
- </div>`}
105
- </div>
94
+ <s-card>
95
+ <s-box padding="large">
96
+ <div style="text-align:center; padding:24px 0;">
97
+ <s-text tone="neutral">No messages yet. Our team will respond shortly.</s-text>
98
+ </div>
99
+ </s-box>
100
+ </s-card>`
101
+ : `<s-stack gap="small-100">
102
+ ${messages.map((msg) => renderMessage(msg, aliasMap.get(msg.authorAliasId ?? "") ?? null)).join("")}
103
+ </s-stack>`}
104
+ </div>
106
105
 
107
- ${isOpen
106
+ ${isOpen
108
107
  ? `
109
- <s-card style="margin-top:16px;">
110
- <s-box padding="large">
111
- <div id="sch-reply-section">
112
- <s-stack gap="base">
113
- <s-text variant="headingSm">Reply</s-text>
114
- ${renderFormattingField({
108
+ <s-card>
109
+ <s-box padding="large">
110
+ <div id="sch-reply-section">
111
+ <s-stack gap="base">
112
+ <s-heading>Reply</s-heading>
113
+ ${renderFormattingField({
115
114
  id: "sch-reply-body",
116
115
  rows: 4,
117
116
  placeholder: "Write your reply...",
118
117
  extraStyle: "resize:vertical"
119
118
  })}
120
- <div id="sch-reply-files"></div>
121
- <div style="display:flex; justify-content:space-between; align-items:center;">
122
- <s-button id="sch-close-ticket-btn">Close Ticket</s-button>
123
- <s-button variant="primary" id="sch-send-reply-btn">Send Reply</s-button>
124
- </div>
125
- </s-stack>
126
- </div>
127
- </s-box>
128
- </s-card>`
119
+ <div id="sch-reply-files"></div>
120
+ <s-stack direction="inline" justifyContent="space-between" alignItems="center">
121
+ <s-button id="sch-close-ticket-btn">Close Ticket</s-button>
122
+ <s-button variant="primary" id="sch-send-reply-btn">Send Reply</s-button>
123
+ </s-stack>
124
+ </s-stack>
125
+ </div>
126
+ </s-box>
127
+ </s-card>`
129
128
  : ""}
129
+ </s-stack>
130
130
  `);
131
131
  // Back button
132
132
  container.querySelector("#sch-back-btn")?.addEventListener("click", () => {
@@ -198,10 +198,10 @@ export async function renderTicketDetail(container, client, ticketId, emitter) {
198
198
  <s-card>
199
199
  <s-box padding="large">
200
200
  <s-stack gap="base">
201
- <s-banner tone="critical">
202
- <s-text>Failed to load ticket: ${escapeHtml(error instanceof Error ? error.message : String(error))}</s-text>
201
+ <s-banner tone="critical" heading="Failed to load ticket">
202
+ <s-text>${escapeHtml(error instanceof Error ? error.message : String(error))}</s-text>
203
203
  </s-banner>
204
- <s-button variant="plain" id="sch-back-btn">
204
+ <s-button variant="tertiary" id="sch-back-btn">
205
205
  <span style="display:inline-flex; align-items:center; gap:4px;">
206
206
  <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M12.707 15.707a1 1 0 0 1-1.414 0l-5-5a1 1 0 0 1 0-1.414l5-5a1 1 0 1 1 1.414 1.414L8.414 10l4.293 4.293a1 1 0 0 1 0 1.414z"/></svg>
207
207
  All tickets
@@ -230,39 +230,35 @@ function renderMessage(msg, alias) {
230
230
  if (isSystem) {
231
231
  return `
232
232
  <div style="text-align:center; padding:12px 0;">
233
- <s-text variant="bodySm" tone="subdued">${escapeHtml(msg.bodyPlain)}</s-text>
233
+ <s-text tone="neutral">${escapeHtml(msg.bodyPlain)}</s-text>
234
234
  </div>
235
235
  `;
236
236
  }
237
- const avatarBg = isAgent
238
- ? "var(--p-color-bg-fill-info, #e4f0fe)"
239
- : "var(--p-color-bg-fill-success, #cdfee1)";
240
- const avatarColor = isAgent
241
- ? "var(--p-color-text-info, #2c6ecb)"
242
- : "var(--p-color-text-success, #008060)";
243
- const avatarLetter = isAgent ? (alias ? getInitials(alias.displayName) : "S") : "Y";
237
+ const avatarInitials = isAgent ? (alias ? getInitials(alias.displayName) : "S") : "Y";
238
+ const avatarSrc = isAgent && alias?.avatarUrl ? ` src="${escapeHtml(alias.avatarUrl)}"` : "";
244
239
  const label = isAgent ? (alias ? alias.displayName : "Support") : "You";
245
240
  const bubbleBg = isCustomer
246
241
  ? "var(--p-color-bg-fill-info, #e4f0fe)"
247
242
  : "var(--p-color-bg-surface-secondary, #f6f6f7)";
248
- const align = isCustomer ? "flex-end" : "flex-start";
249
- const flexDir = isCustomer ? "row-reverse" : "row";
250
- return `
251
- <div style="display:flex; align-items:flex-start; gap:10px; flex-direction:${flexDir}; padding:10px 0; max-width:85%${isCustomer ? "; margin-left:auto" : ""}">
252
- <div style="width:32px; height:32px; border-radius:50%; background:${avatarBg}; display:flex; align-items:center; justify-content:center; flex-shrink:0;">
253
- <span style="font-size:13px; font-weight:600; color:${avatarColor};">${avatarLetter}</span>
254
- </div>
255
- <div style="min-width:0; text-align:${isCustomer ? "right" : "left"};">
256
- <div style="display:flex; align-items:baseline; gap:8px; justify-content:${align}; margin-bottom:4px;">
257
- <s-text variant="bodySm" fontWeight="semibold">${label}</s-text>
258
- <s-text variant="bodySm" tone="subdued">${formatTimestamp(msg.createdAt)}</s-text>
259
- </div>
260
- <div style="background:${bubbleBg}; padding:10px 14px; border-radius:12px; display:inline-block; text-align:left; max-width:100%;">
261
- <s-text variant="bodyMd" style="white-space:pre-wrap; word-break:break-word;">${formatBodyText(msg.bodyPlain)}</s-text>
262
- </div>
243
+ const avatar = `<s-avatar size="small" initials="${escapeHtml(avatarInitials)}"${avatarSrc}></s-avatar>`;
244
+ const content = `
245
+ <div style="min-width:0;">
246
+ <s-stack direction="inline" alignItems="baseline" gap="small" justifyContent="${isCustomer ? "end" : "start"}" style="margin-bottom:4px;">
247
+ <s-text type="strong">${label}</s-text>
248
+ <s-text tone="neutral">${formatTimestamp(msg.createdAt)}</s-text>
249
+ </s-stack>
250
+ <div style="background:${bubbleBg}; padding:10px 14px; border-radius:12px; display:inline-block; text-align:left; max-width:100%;">
251
+ <s-text style="white-space:pre-wrap; word-break:break-word;">${formatBodyText(msg.bodyPlain)}</s-text>
263
252
  </div>
264
253
  </div>
265
254
  `;
255
+ return `
256
+ <div style="padding:10px 0; max-width:85%;${isCustomer ? " margin-left:auto;" : ""}">
257
+ <s-stack direction="inline" alignItems="start" gap="small">
258
+ ${isCustomer ? content + avatar : avatar + content}
259
+ </s-stack>
260
+ </div>
261
+ `;
266
262
  }
267
263
  function showError(container, message) {
268
264
  const existing = container.querySelector("#sch-error-banner");
@@ -271,7 +267,7 @@ function showError(container, message) {
271
267
  const banner = document.createElement("div");
272
268
  banner.id = "sch-error-banner";
273
269
  setInnerHtml(banner, `
274
- <s-banner tone="critical" style="margin-bottom:12px;">
270
+ <s-banner tone="critical" heading="Error" style="margin-bottom:12px;">
275
271
  <s-text>${escapeHtml(message)}</s-text>
276
272
  </s-banner>
277
273
  `);
@@ -1,4 +1,4 @@
1
- import type { SupportApiClient } from "@catandbox/schrodinger-shopify-adapter/client";
1
+ import type { SupportApiClient } from "@catandbox/schrodinger-contracts";
2
2
  import type { EventEmitter } from "./events";
3
3
  export interface TicketListEvents {
4
4
  [key: string]: unknown;
@@ -1,6 +1,4 @@
1
- /**
2
- * Server-side exports re-exported from the React adapter's server module.
3
- * These are already framework-agnostic (no React dependency).
4
- */
5
- export type { AdapterEnvironment, PrincipalContext, ProxyHandlerOptions, ShopifySessionVerificationOptions, WebhookForwardingOptions } from "@catandbox/schrodinger-shopify-adapter/server";
6
- export { ShopifyAuthError, createPrincipalContext, extractShopifySessionToken, verifyShopifySessionToken, verifyShopifyWebhookHmac, signSchrodingerRequest, createShopifyProxyHandler, createShopifyWebhookHandlers, handleAppUninstalledWebhook, handleCustomersDataRequestWebhook, handleCustomersRedactWebhook, handleShopRedactWebhook, parsePrefillRoute } from "@catandbox/schrodinger-shopify-adapter/server";
1
+ export type { AdapterEnvironment, PrefillLengthCaps, PrefillParseOptions, PrefillState, PrincipalContext, ProxyHandlerOptions, ShopifySessionVerificationOptions, WebhookForwardingOptions } from "./types";
2
+ export { ShopifyAuthError, createPrincipalContext, extractShopifySessionToken, verifyShopifySessionToken, verifyShopifyWebhookHmac } from "./shopifyAuth";
3
+ export { signSchrodingerRequest } from "./signing";
4
+ export { createShopifyProxyHandler, createShopifyWebhookHandlers, handleAppUninstalledWebhook, handleCustomersDataRequestWebhook, handleCustomersRedactWebhook, handleShopRedactWebhook, parsePrefillRoute } from "./routes";
@@ -1 +1,3 @@
1
- export { ShopifyAuthError, createPrincipalContext, extractShopifySessionToken, verifyShopifySessionToken, verifyShopifyWebhookHmac, signSchrodingerRequest, createShopifyProxyHandler, createShopifyWebhookHandlers, handleAppUninstalledWebhook, handleCustomersDataRequestWebhook, handleCustomersRedactWebhook, handleShopRedactWebhook, parsePrefillRoute } from "@catandbox/schrodinger-shopify-adapter/server";
1
+ export { ShopifyAuthError, createPrincipalContext, extractShopifySessionToken, verifyShopifySessionToken, verifyShopifyWebhookHmac } from "./shopifyAuth";
2
+ export { signSchrodingerRequest } from "./signing";
3
+ export { createShopifyProxyHandler, createShopifyWebhookHandlers, handleAppUninstalledWebhook, handleCustomersDataRequestWebhook, handleCustomersRedactWebhook, handleShopRedactWebhook, parsePrefillRoute } from "./routes";
@@ -0,0 +1,15 @@
1
+ import type { PrefillParseOptions, PrefillState, ProxyHandlerOptions, WebhookForwardingOptions } from "./types";
2
+ export declare function createShopifyProxyHandler(options: ProxyHandlerOptions): (request: Request) => Promise<Response>;
3
+ export declare function parsePrefillRoute(input: string | URL, options: PrefillParseOptions): PrefillState;
4
+ interface ShopifyWebhookHandlers {
5
+ handleCustomersDataRequestWebhook: (request: Request) => Promise<Response>;
6
+ handleCustomersRedactWebhook: (request: Request) => Promise<Response>;
7
+ handleShopRedactWebhook: (request: Request) => Promise<Response>;
8
+ handleAppUninstalledWebhook: (request: Request) => Promise<Response>;
9
+ }
10
+ export declare function createShopifyWebhookHandlers(options: WebhookForwardingOptions): ShopifyWebhookHandlers;
11
+ export declare function handleCustomersDataRequestWebhook(request: Request, options: WebhookForwardingOptions): Promise<Response>;
12
+ export declare function handleCustomersRedactWebhook(request: Request, options: WebhookForwardingOptions): Promise<Response>;
13
+ export declare function handleShopRedactWebhook(request: Request, options: WebhookForwardingOptions): Promise<Response>;
14
+ export declare function handleAppUninstalledWebhook(request: Request, options: WebhookForwardingOptions): Promise<Response>;
15
+ export {};