@catandbox/schrodinger-web-adapter 0.1.27 → 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.
@@ -1,5 +1,169 @@
1
1
  import { renderStatusBadge } from "./status-badge";
2
2
  import { setInnerHtml } from "./dom-utils";
3
+ function loadThree() {
4
+ return new Promise((resolve, reject) => {
5
+ if (typeof THREE !== "undefined") {
6
+ resolve();
7
+ return;
8
+ }
9
+ const s = document.createElement("script");
10
+ s.src = "https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js";
11
+ s.onload = () => resolve();
12
+ s.onerror = reject;
13
+ document.head.appendChild(s);
14
+ });
15
+ }
16
+ function initHydrogenLogo(el) {
17
+ // Math helpers
18
+ function factorial(n) {
19
+ let r = 1;
20
+ for (let i = 2; i <= n; i++)
21
+ r *= i;
22
+ return r;
23
+ }
24
+ function assocLaguerre(p, q, x) {
25
+ if (p === 0)
26
+ return 1;
27
+ if (p === 1)
28
+ return 1 + q - x;
29
+ let lm2 = 1, lm1 = 1 + q - x, val = 0;
30
+ for (let k = 2; k <= p; k++) {
31
+ val = ((2 * k - 1 + q - x) * lm1 - (k - 1 + q) * lm2) / k;
32
+ lm2 = lm1;
33
+ lm1 = val;
34
+ }
35
+ return val;
36
+ }
37
+ function assocLegendre(l, m, x) {
38
+ let pmm = 1.0;
39
+ if (m > 0) {
40
+ const s = Math.sqrt((1 - x) * (1 + x));
41
+ let f = 1;
42
+ for (let i = 1; i <= m; i++) {
43
+ pmm *= -f * s;
44
+ f += 2;
45
+ }
46
+ }
47
+ if (l === m)
48
+ return pmm;
49
+ let pmmp1 = x * (2 * m + 1) * pmm;
50
+ if (l === m + 1)
51
+ return pmmp1;
52
+ let pll = 0;
53
+ for (let ll = m + 2; ll <= l; ll++) {
54
+ pll = (x * (2 * ll - 1) * pmmp1 - (ll + m - 1) * pmm) / (ll - m);
55
+ pmm = pmmp1;
56
+ pmmp1 = pll;
57
+ }
58
+ return pll;
59
+ }
60
+ function sphericalHarmonicReal(l, m, theta, phi) {
61
+ const absM = Math.abs(m);
62
+ const norm = Math.sqrt(((2 * l + 1) / (4 * Math.PI)) * (factorial(l - absM) / factorial(l + absM)));
63
+ const plm = assocLegendre(l, absM, Math.cos(theta));
64
+ if (m > 0)
65
+ return norm * plm * Math.cos(m * phi) * Math.SQRT2;
66
+ if (m < 0)
67
+ return norm * plm * Math.sin(absM * phi) * Math.SQRT2;
68
+ return norm * plm;
69
+ }
70
+ function radialWaveFunction(n, l, r) {
71
+ const rho = (2 * r) / n;
72
+ const norm = Math.sqrt(Math.pow(2 / n, 3) * factorial(n - l - 1) / (2 * n * Math.pow(factorial(n + l), 3)));
73
+ return norm * Math.exp(-rho / 2) * Math.pow(rho, l) * assocLaguerre(n - l - 1, 2 * l + 1, rho);
74
+ }
75
+ function psiProb(n, l, m, r, theta, phi) {
76
+ const R = radialWaveFunction(n, l, r);
77
+ const Y = sphericalHarmonicReal(l, m, theta, phi);
78
+ return R * R * Y * Y;
79
+ }
80
+ // Generate points via rejection sampling
81
+ const n = 4, l = 3, m = 1, maxR = 45, numPoints = 80000;
82
+ let maxProb = 0;
83
+ for (let i = 0; i < 5000; i++) {
84
+ const r = Math.random() * maxR;
85
+ const theta = Math.acos(2 * Math.random() - 1);
86
+ const phi = Math.random() * 2 * Math.PI;
87
+ const p = psiProb(n, l, m, r, theta, phi) * r * r;
88
+ if (p > maxProb)
89
+ maxProb = p;
90
+ }
91
+ const pts = [], cols = [], szs = [];
92
+ let accepted = 0;
93
+ const sc = 0.7;
94
+ for (let i = 0; i < numPoints * 30 && accepted < numPoints; i++) {
95
+ const r = Math.random() * maxR;
96
+ const theta = Math.acos(2 * Math.random() - 1);
97
+ const phi = Math.random() * 2 * Math.PI;
98
+ const prob = psiProb(n, l, m, r, theta, phi) * r * r;
99
+ const thr = prob / maxProb;
100
+ if (Math.random() < thr) {
101
+ pts.push(r * Math.sin(theta) * Math.cos(phi) * sc, r * Math.sin(theta) * Math.sin(phi) * sc, r * Math.cos(theta) * sc);
102
+ const t = r / maxR;
103
+ cols.push(0.3 + 0.6 * t, 0.5 * (1 - t) + 0.2 * t, 0.95 - 0.2 * t);
104
+ szs.push(0.08 + 0.15 * thr);
105
+ accepted++;
106
+ }
107
+ }
108
+ const scene = new THREE.Scene();
109
+ const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 1000);
110
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
111
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
112
+ renderer.setSize(80, 80);
113
+ renderer.setClearColor(0x08090d, 1);
114
+ el.appendChild(renderer.domElement);
115
+ const geometry = new THREE.BufferGeometry();
116
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(pts, 3));
117
+ geometry.setAttribute("color", new THREE.Float32BufferAttribute(cols, 3));
118
+ geometry.setAttribute("size", new THREE.Float32BufferAttribute(szs, 1));
119
+ const material = new THREE.ShaderMaterial({
120
+ vertexShader: `
121
+ attribute float size;
122
+ varying vec3 vColor;
123
+ void main() {
124
+ vColor = color;
125
+ vec4 mv = modelViewMatrix * vec4(position, 1.0);
126
+ gl_PointSize = size * (200.0 / -mv.z);
127
+ gl_Position = projectionMatrix * mv;
128
+ }
129
+ `,
130
+ fragmentShader: `
131
+ varying vec3 vColor;
132
+ void main() {
133
+ float d = length(gl_PointCoord - vec2(0.5));
134
+ if (d > 0.5) discard;
135
+ float a = smoothstep(0.5, 0.1, d) * 0.7;
136
+ gl_FragColor = vec4(vColor, a);
137
+ }
138
+ `,
139
+ vertexColors: true,
140
+ transparent: true,
141
+ depthWrite: false,
142
+ blending: THREE.AdditiveBlending,
143
+ });
144
+ scene.add(new THREE.Points(geometry, material));
145
+ const nucGeo = new THREE.SphereGeometry(0.25, 12, 12);
146
+ scene.add(new THREE.Mesh(nucGeo, new THREE.MeshBasicMaterial({ color: 0xffffff })));
147
+ let theta = 0;
148
+ const phi = 0.85, radius = 42;
149
+ function updateCam() {
150
+ camera.position.set(radius * Math.sin(phi) * Math.sin(theta), radius * Math.cos(phi), radius * Math.sin(phi) * Math.cos(theta));
151
+ camera.lookAt(0, 0, 0);
152
+ }
153
+ let rafId;
154
+ function animate() {
155
+ rafId = requestAnimationFrame(animate);
156
+ theta += 0.003;
157
+ updateCam();
158
+ renderer.render(scene, camera);
159
+ }
160
+ updateCam();
161
+ animate();
162
+ return () => {
163
+ cancelAnimationFrame(rafId);
164
+ renderer.dispose();
165
+ };
166
+ }
3
167
  function formatDate(timestamp) {
4
168
  return new Date(timestamp * 1000).toLocaleDateString(undefined, {
5
169
  month: "short",
@@ -35,10 +199,11 @@ const ORDER_OPTIONS = [
35
199
  { value: "oldest", label: "Oldest first" }
36
200
  ];
37
201
  export async function renderTicketList(container, client, emitter) {
38
- // Load tickets and categories in parallel
202
+ // Load tickets, categories and Three.js in parallel
39
203
  const [result, portalConfig] = await Promise.all([
40
204
  client.listTickets({}).catch(() => ({ items: [] })),
41
- client.getPortalConfig().catch(() => ({ categories: [], aliases: [] }))
205
+ client.getPortalConfig().catch(() => ({ categories: [], aliases: [] })),
206
+ loadThree().catch(() => { })
42
207
  ]);
43
208
  const allTickets = result.items;
44
209
  const categories = portalConfig.categories;
@@ -57,10 +222,10 @@ export async function renderTicketList(container, client, emitter) {
57
222
  <s-box padding="large">
58
223
  <s-stack gap="large">
59
224
  <div style="display:flex; justify-content:space-between; align-items:center;">
60
- <s-stack gap="extraTight">
61
- <s-text variant="headingMd">Support Tickets</s-text>
62
- <s-text variant="bodySm" tone="subdued">View and manage your support requests</s-text>
63
- </s-stack>
225
+ <div style="display:flex; align-items:center; gap:12px;">
226
+ <div id="sch-logo-el" style="width:80px; height:80px; border-radius:8px; overflow:hidden; flex-shrink:0; background:#08090d;"></div>
227
+ <span style="font-size:18px; font-weight:700; color:#0f172a;">Schrödinger helpdesk by Cat and box</span>
228
+ </div>
64
229
  <s-button variant="primary" id="sch-new-ticket-btn">
65
230
  <span style="display:inline-flex; align-items:center; gap:4px;">
66
231
  <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M10 3a1 1 0 0 1 1 1v5h5a1 1 0 1 1 0 2h-5v5a1 1 0 1 1-2 0v-5H4a1 1 0 1 1 0-2h5V4a1 1 0 0 1 1-1z"/></svg>
@@ -96,6 +261,11 @@ export async function renderTicketList(container, client, emitter) {
96
261
  container
97
262
  .querySelector("#sch-new-ticket-btn")
98
263
  ?.addEventListener("click", () => emitter.emit("ticket:create", undefined));
264
+ // Init hydrogen orbital logo
265
+ const logoEl = container.querySelector("#sch-logo-el");
266
+ if (logoEl && typeof THREE !== "undefined") {
267
+ initHydrogenLogo(logoEl);
268
+ }
99
269
  function getSelectValue(id) {
100
270
  const el = container.querySelector(`#${id}`);
101
271
  return el?.value ?? "";
@@ -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 {};
@@ -0,0 +1,353 @@
1
+ import { signSchrodingerRequest } from "./signing";
2
+ import { ShopifyAuthError, createPrincipalContext, verifyShopifyWebhookHmac } from "./shopifyAuth";
3
+ const DEFAULT_BASE_PATH = "/support/api";
4
+ const DEFAULT_PREFILL_CAPS = {
5
+ title: 120,
6
+ category: 80,
7
+ description: 10_000
8
+ };
9
+ export function createShopifyProxyHandler(options) {
10
+ const basePath = normalizePathPrefix(options.basePath ?? DEFAULT_BASE_PATH);
11
+ return async (request) => {
12
+ const requestId = request.headers.get("X-Request-Id") ?? options.requestIdFactory?.() ?? crypto.randomUUID();
13
+ const fetchImpl = options.fetchImpl ?? fetch;
14
+ try {
15
+ const url = new URL(request.url);
16
+ const relativePath = toRelativePath(url.pathname, basePath);
17
+ if (relativePath === null) {
18
+ return jsonError(404, "NOT_FOUND", "Route not found", requestId);
19
+ }
20
+ const principal = await createPrincipalContext(request, {
21
+ shopifyApiKey: options.shopifyApiKey,
22
+ shopifyApiSecret: options.shopifyApiSecret
23
+ });
24
+ const resolution = await resolveProxyRequest(request, relativePath, url.searchParams, principal);
25
+ const upstreamUrl = new URL(`${trimTrailingSlash(options.schApiBaseUrl)}${resolution.upstreamPath}`);
26
+ resolution.query.forEach((value, key) => {
27
+ upstreamUrl.searchParams.set(key, value);
28
+ });
29
+ const signed = await signSchrodingerRequest({
30
+ env: {
31
+ schAppId: options.schAppId,
32
+ schKeyId: options.schKeyId,
33
+ schSecret: options.schSecret
34
+ },
35
+ method: request.method,
36
+ path: resolution.upstreamPath,
37
+ query: upstreamUrl.searchParams,
38
+ body: resolution.body
39
+ });
40
+ const upstreamHeaders = new Headers(signed.headers);
41
+ upstreamHeaders.set("X-Request-Id", requestId);
42
+ upstreamHeaders.set(options.principalContextHeaderName ?? "X-Sch-Principal-Context", JSON.stringify({
43
+ shopDomain: principal.shopDomain,
44
+ tenantExternalId: principal.tenantExternalId,
45
+ principalExternalId: principal.principalExternalId,
46
+ displayName: principal.displayName,
47
+ email: principal.email
48
+ }));
49
+ if (signed.rawBody) {
50
+ upstreamHeaders.set("content-type", "application/json");
51
+ }
52
+ const upstreamResponse = await fetchImpl(upstreamUrl, {
53
+ method: request.method,
54
+ headers: upstreamHeaders,
55
+ body: signed.rawBody || null
56
+ });
57
+ return await copyResponse(upstreamResponse, requestId);
58
+ }
59
+ catch (error) {
60
+ if (error instanceof ShopifyAuthError) {
61
+ return jsonError(error.status, error.code, error.message, requestId);
62
+ }
63
+ if (error instanceof ProxyRouteError) {
64
+ return jsonError(error.status, error.code, error.message, requestId);
65
+ }
66
+ return jsonError(500, "INTERNAL_ERROR", "Unexpected proxy error", requestId);
67
+ }
68
+ };
69
+ }
70
+ export function parsePrefillRoute(input, options) {
71
+ const url = typeof input === "string" ? new URL(input, "https://local.invalid") : input;
72
+ const caps = { ...DEFAULT_PREFILL_CAPS, ...(options.caps ?? {}) };
73
+ const errors = {};
74
+ const title = (url.searchParams.get("title") ?? "").trim();
75
+ const categoryRaw = (url.searchParams.get("category") ?? "").trim();
76
+ const description = (url.searchParams.get("description") ?? "").trim();
77
+ const boundedTitle = enforceLength("title", title, caps.title, errors);
78
+ const boundedCategory = enforceLength("category", categoryRaw, caps.category, errors);
79
+ const boundedDescription = enforceLength("description", description, caps.description, errors);
80
+ const categoryId = boundedCategory
81
+ ? options.categories.some((category) => category.id === boundedCategory)
82
+ ? boundedCategory
83
+ : null
84
+ : null;
85
+ if (boundedCategory && !categoryId) {
86
+ errors.category = "Prefill category is not available for this integration";
87
+ }
88
+ return {
89
+ title: boundedTitle,
90
+ categoryId,
91
+ description: boundedDescription,
92
+ errors,
93
+ isValid: Object.keys(errors).length === 0
94
+ };
95
+ }
96
+ export function createShopifyWebhookHandlers(options) {
97
+ return {
98
+ handleCustomersDataRequestWebhook: (request) => forwardWebhook(request, "customers/data_request", "/admin/gdpr/data-request", options),
99
+ handleCustomersRedactWebhook: (request) => forwardWebhook(request, "customers/redact", "/admin/gdpr/customer-redact", options),
100
+ handleShopRedactWebhook: (request) => forwardWebhook(request, "shop/redact", "/admin/gdpr/shop-redact", options),
101
+ handleAppUninstalledWebhook: (request) => forwardWebhook(request, "app/uninstalled", "/admin/gdpr/shop-redact", options, true)
102
+ };
103
+ }
104
+ export async function handleCustomersDataRequestWebhook(request, options) {
105
+ return forwardWebhook(request, "customers/data_request", "/admin/gdpr/data-request", options);
106
+ }
107
+ export async function handleCustomersRedactWebhook(request, options) {
108
+ return forwardWebhook(request, "customers/redact", "/admin/gdpr/customer-redact", options);
109
+ }
110
+ export async function handleShopRedactWebhook(request, options) {
111
+ return forwardWebhook(request, "shop/redact", "/admin/gdpr/shop-redact", options);
112
+ }
113
+ export async function handleAppUninstalledWebhook(request, options) {
114
+ return forwardWebhook(request, "app/uninstalled", "/admin/gdpr/shop-redact", options, true);
115
+ }
116
+ async function resolveProxyRequest(request, relativePath, requestQuery, principal) {
117
+ if (request.method === "GET" && relativePath === "/categories") {
118
+ return {
119
+ upstreamPath: "/v1/categories",
120
+ query: new URLSearchParams(requestQuery),
121
+ body: undefined
122
+ };
123
+ }
124
+ if (request.method === "GET" && relativePath === "/portal-config") {
125
+ return {
126
+ upstreamPath: "/v1/portal-config",
127
+ query: new URLSearchParams(requestQuery),
128
+ body: undefined
129
+ };
130
+ }
131
+ if (request.method === "GET" && relativePath === "/tickets") {
132
+ const query = new URLSearchParams(requestQuery);
133
+ query.set("tenantExternalId", principal.tenantExternalId);
134
+ return {
135
+ upstreamPath: "/v1/tickets",
136
+ query,
137
+ body: undefined
138
+ };
139
+ }
140
+ const ticketMatch = relativePath.match(/^\/tickets\/([^/]+)$/);
141
+ if (request.method === "GET" && ticketMatch) {
142
+ const query = new URLSearchParams(requestQuery);
143
+ query.set("tenantExternalId", principal.tenantExternalId);
144
+ return {
145
+ upstreamPath: `/v1/tickets/${encodeURIComponent(decodeURIComponent(ticketMatch[1] ?? ""))}`,
146
+ query,
147
+ body: undefined
148
+ };
149
+ }
150
+ if (request.method === "POST" && relativePath === "/tickets") {
151
+ const payload = await parseOptionalJsonBody(request);
152
+ return {
153
+ upstreamPath: "/v1/tickets",
154
+ query: new URLSearchParams(requestQuery),
155
+ body: {
156
+ ...payload,
157
+ tenantExternalId: principal.tenantExternalId,
158
+ principalExternalId: principal.principalExternalId
159
+ }
160
+ };
161
+ }
162
+ const messageMatch = relativePath.match(/^\/tickets\/([^/]+)\/messages$/);
163
+ if (request.method === "POST" && messageMatch) {
164
+ const payload = await parseOptionalJsonBody(request);
165
+ return {
166
+ upstreamPath: `/v1/tickets/${encodeURIComponent(decodeURIComponent(messageMatch[1] ?? ""))}/messages`,
167
+ query: new URLSearchParams(requestQuery),
168
+ body: {
169
+ ...payload,
170
+ tenantExternalId: principal.tenantExternalId,
171
+ principalExternalId: principal.principalExternalId
172
+ }
173
+ };
174
+ }
175
+ const actionMatch = relativePath.match(/^\/tickets\/([^/]+)\/(close|archive|reopen)$/);
176
+ if (request.method === "POST" && actionMatch) {
177
+ return {
178
+ upstreamPath: `/v1/tickets/${encodeURIComponent(decodeURIComponent(actionMatch[1] ?? ""))}/${actionMatch[2]}`,
179
+ query: new URLSearchParams(requestQuery),
180
+ body: {
181
+ tenantExternalId: principal.tenantExternalId
182
+ }
183
+ };
184
+ }
185
+ const ticketRatingMatch = relativePath.match(/^\/tickets\/([^/]+)\/ratings$/);
186
+ if (request.method === "POST" && ticketRatingMatch) {
187
+ const payload = await parseOptionalJsonBody(request);
188
+ return {
189
+ upstreamPath: `/v1/tickets/${encodeURIComponent(decodeURIComponent(ticketRatingMatch[1] ?? ""))}/ratings`,
190
+ query: new URLSearchParams(requestQuery),
191
+ body: {
192
+ ...payload,
193
+ tenantExternalId: principal.tenantExternalId,
194
+ principalExternalId: principal.principalExternalId
195
+ }
196
+ };
197
+ }
198
+ const messageRatingMatch = relativePath.match(/^\/messages\/([^/]+)\/ratings$/);
199
+ if (request.method === "POST" && messageRatingMatch) {
200
+ const payload = await parseOptionalJsonBody(request);
201
+ return {
202
+ upstreamPath: `/v1/messages/${encodeURIComponent(decodeURIComponent(messageRatingMatch[1] ?? ""))}/ratings`,
203
+ query: new URLSearchParams(requestQuery),
204
+ body: {
205
+ ...payload,
206
+ tenantExternalId: principal.tenantExternalId,
207
+ principalExternalId: principal.principalExternalId
208
+ }
209
+ };
210
+ }
211
+ if (request.method === "POST" && relativePath === "/uploads/init") {
212
+ const payload = await parseOptionalJsonBody(request);
213
+ return {
214
+ upstreamPath: "/v1/uploads/init",
215
+ query: new URLSearchParams(requestQuery),
216
+ body: {
217
+ ...payload,
218
+ tenantExternalId: principal.tenantExternalId,
219
+ principalExternalId: principal.principalExternalId
220
+ }
221
+ };
222
+ }
223
+ if (request.method === "POST" && relativePath === "/uploads/complete") {
224
+ const payload = await parseOptionalJsonBody(request);
225
+ return {
226
+ upstreamPath: "/v1/uploads/complete",
227
+ query: new URLSearchParams(requestQuery),
228
+ body: {
229
+ ...payload,
230
+ tenantExternalId: principal.tenantExternalId,
231
+ principalExternalId: principal.principalExternalId
232
+ }
233
+ };
234
+ }
235
+ throw new ProxyRouteError("NOT_FOUND", "Adapter route not found", 404);
236
+ }
237
+ async function forwardWebhook(request, topic, adminPath, options, disablePortalAccess = false) {
238
+ const requestId = request.headers.get("X-Request-Id") ?? options.requestIdFactory?.() ?? crypto.randomUUID();
239
+ const rawBody = await request.clone().text();
240
+ const hmacHeader = request.headers.get("X-Shopify-Hmac-Sha256") ?? request.headers.get("x-shopify-hmac-sha256");
241
+ const validHmac = await verifyShopifyWebhookHmac({
242
+ rawBody,
243
+ hmacHeader,
244
+ shopifyApiSecret: options.shopifyApiSecret
245
+ });
246
+ if (!validHmac) {
247
+ return jsonError(401, "INVALID_WEBHOOK_SIGNATURE", "Shopify webhook HMAC validation failed", requestId);
248
+ }
249
+ if (!options.schAdminApiToken) {
250
+ return jsonError(500, "MISSING_SCH_ADMIN_API_TOKEN", "schAdminApiToken is required for GDPR webhook forwarding", requestId);
251
+ }
252
+ const payload = parseWebhookPayload(rawBody, requestId);
253
+ if (payload instanceof Response) {
254
+ return payload;
255
+ }
256
+ if (disablePortalAccess) {
257
+ const shopDomain = inferWebhookShopDomain(payload);
258
+ if (!shopDomain) {
259
+ return jsonError(422, "INVALID_WEBHOOK_PAYLOAD", "Missing shop domain in uninstall webhook", requestId);
260
+ }
261
+ await options.disablePortalAccess?.({ shopDomain, payload, topic });
262
+ }
263
+ const url = new URL(`${trimTrailingSlash(options.schApiBaseUrl)}${adminPath}`);
264
+ const fetchImpl = options.fetchImpl ?? fetch;
265
+ const upstream = await fetchImpl(url, {
266
+ method: "POST",
267
+ headers: {
268
+ Authorization: `Bearer ${options.schAdminApiToken}`,
269
+ "content-type": "application/json",
270
+ "X-Request-Id": requestId
271
+ },
272
+ body: rawBody
273
+ });
274
+ return await copyResponse(upstream, requestId);
275
+ }
276
+ async function parseOptionalJsonBody(request) {
277
+ const raw = await request.clone().text();
278
+ if (!raw.trim()) {
279
+ return {};
280
+ }
281
+ try {
282
+ return JSON.parse(raw);
283
+ }
284
+ catch {
285
+ throw new ProxyRouteError("INVALID_JSON", "Request body must be valid JSON", 400);
286
+ }
287
+ }
288
+ function parseWebhookPayload(rawBody, requestId) {
289
+ try {
290
+ return JSON.parse(rawBody);
291
+ }
292
+ catch {
293
+ return jsonError(400, "INVALID_JSON", "Webhook payload must be valid JSON", requestId);
294
+ }
295
+ }
296
+ function inferWebhookShopDomain(payload) {
297
+ const candidate = payload.shop_domain ?? payload.myshopify_domain ?? payload.shopDomain;
298
+ return typeof candidate === "string" && candidate.trim() ? candidate.trim().toLowerCase() : null;
299
+ }
300
+ function normalizePathPrefix(path) {
301
+ const withSlash = path.startsWith("/") ? path : `/${path}`;
302
+ return withSlash.replace(/\/+$/, "");
303
+ }
304
+ function toRelativePath(pathname, basePath) {
305
+ if (pathname === basePath) {
306
+ return "/";
307
+ }
308
+ if (pathname.startsWith(`${basePath}/`)) {
309
+ return pathname.slice(basePath.length);
310
+ }
311
+ return null;
312
+ }
313
+ function trimTrailingSlash(value) {
314
+ return value.replace(/\/+$/, "");
315
+ }
316
+ async function copyResponse(response, requestId) {
317
+ const headers = new Headers(response.headers);
318
+ if (!headers.get("X-Request-Id")) {
319
+ headers.set("X-Request-Id", requestId);
320
+ }
321
+ const body = await response.arrayBuffer();
322
+ return new Response(body, { status: response.status, statusText: response.statusText, headers });
323
+ }
324
+ function jsonError(status, code, message, requestId) {
325
+ return new Response(JSON.stringify({
326
+ error: code,
327
+ message,
328
+ requestId
329
+ }), {
330
+ status,
331
+ headers: {
332
+ "content-type": "application/json; charset=utf-8",
333
+ "X-Request-Id": requestId
334
+ }
335
+ });
336
+ }
337
+ function enforceLength(field, value, maxLength, errors) {
338
+ if (value.length <= maxLength) {
339
+ return value;
340
+ }
341
+ errors[field] = `${field} exceeds ${maxLength} characters`;
342
+ return value.slice(0, maxLength);
343
+ }
344
+ class ProxyRouteError extends Error {
345
+ code;
346
+ status;
347
+ constructor(code, message, status) {
348
+ super(message);
349
+ this.name = "ProxyRouteError";
350
+ this.code = code;
351
+ this.status = status;
352
+ }
353
+ }
@@ -0,0 +1,28 @@
1
+ import type { PrincipalContext, ShopifySessionVerificationOptions } from "./types";
2
+ interface ShopifySessionClaims {
3
+ aud?: string | string[];
4
+ dest?: string;
5
+ email?: string;
6
+ exp?: number;
7
+ iat?: number;
8
+ iss?: string;
9
+ nbf?: number;
10
+ sub?: string;
11
+ user_email?: string;
12
+ user_name?: string;
13
+ [key: string]: unknown;
14
+ }
15
+ export declare class ShopifyAuthError extends Error {
16
+ readonly code: string;
17
+ readonly status: number;
18
+ constructor(code: string, message: string, status?: number);
19
+ }
20
+ export declare function extractShopifySessionToken(request: Request): string | null;
21
+ export declare function createPrincipalContext(request: Request, options: ShopifySessionVerificationOptions): Promise<PrincipalContext>;
22
+ export declare function verifyShopifySessionToken(token: string, options: ShopifySessionVerificationOptions): Promise<ShopifySessionClaims>;
23
+ export declare function verifyShopifyWebhookHmac(input: {
24
+ rawBody: string;
25
+ hmacHeader: string | null;
26
+ shopifyApiSecret: string;
27
+ }): Promise<boolean>;
28
+ export {};