@legenki/print2medusa 0.1.0 → 0.2.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.
@@ -3,10 +3,65 @@ const jsxRuntime = require("react/jsx-runtime");
3
3
  const adminSdk = require("@medusajs/admin-sdk");
4
4
  const ui = require("@medusajs/ui");
5
5
  const react = require("react");
6
+ const ATTENTION_STATES = /* @__PURE__ */ new Set(["failed", "canceled", "onhold"]);
7
+ const PrintfulOrderWidget = ({ data }) => {
8
+ const metadata = data.metadata ?? {};
9
+ const printfulOrderId = metadata.printful_order_id;
10
+ if (!printfulOrderId) {
11
+ return null;
12
+ }
13
+ const status = metadata.printful_status;
14
+ const statusUpdatedAt = metadata.printful_status_updated_at;
15
+ const shipments = metadata.printful_shipments ?? [];
16
+ const isAttention = status ? ATTENTION_STATES.has(status) : false;
17
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
18
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
19
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Printful" }),
20
+ status ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: isAttention ? "red" : "green", children: status }) : null
21
+ ] }),
22
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
23
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", children: [
24
+ "Printful order: ",
25
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: printfulOrderId })
26
+ ] }),
27
+ shipments.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-3 pt-1", children: shipments.map((shipment) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-0.5", children: [
28
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
29
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", children: [
30
+ shipment.carrier ?? "Unknown carrier",
31
+ shipment.service ? ` · ${shipment.service}` : ""
32
+ ] }),
33
+ shipment.reshipment ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "purple", size: "2xsmall", children: "Reshipment" }) : null
34
+ ] }),
35
+ shipment.reshipment ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Replacement parcel — no fulfillment is recorded for it." }) : null,
36
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: shipment.ship_date ? `Shipped ${new Date(shipment.ship_date).toLocaleDateString()}` : "Ship date pending" }),
37
+ shipment.tracking_number ? shipment.tracking_url ? /* @__PURE__ */ jsxRuntime.jsx(
38
+ "a",
39
+ {
40
+ href: shipment.tracking_url,
41
+ target: "_blank",
42
+ rel: "noreferrer noopener",
43
+ className: "text-ui-fg-interactive text-sm hover:underline",
44
+ children: shipment.tracking_number
45
+ }
46
+ ) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: shipment.tracking_number }) : null
47
+ ] }, shipment.id)) }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "No shipments yet." }),
48
+ statusUpdatedAt ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle pt-1", children: [
49
+ "Last synced: ",
50
+ new Date(statusUpdatedAt).toLocaleString()
51
+ ] }) : null
52
+ ] })
53
+ ] });
54
+ };
55
+ adminSdk.defineWidgetConfig({
56
+ zone: "order.details.side.before"
57
+ });
6
58
  const PrintfulSyncWidget = () => {
7
59
  const [status, setStatus] = react.useState(null);
8
60
  const [loading, setLoading] = react.useState(false);
9
61
  const [syncing, setSyncing] = react.useState(false);
62
+ const [webhook, setWebhook] = react.useState(null);
63
+ const [webhookLoading, setWebhookLoading] = react.useState(false);
64
+ const [registering, setRegistering] = react.useState(false);
10
65
  const loadStatus = async () => {
11
66
  setLoading(true);
12
67
  try {
@@ -24,9 +79,52 @@ const PrintfulSyncWidget = () => {
24
79
  setLoading(false);
25
80
  }
26
81
  };
82
+ const loadWebhook = async () => {
83
+ setWebhookLoading(true);
84
+ try {
85
+ const res = await fetch("/admin/printful/webhook", {
86
+ credentials: "include"
87
+ });
88
+ if (!res.ok) {
89
+ throw new Error(`Status ${res.status}`);
90
+ }
91
+ const data = await res.json();
92
+ setWebhook(data);
93
+ } catch (e) {
94
+ console.error(e);
95
+ } finally {
96
+ setWebhookLoading(false);
97
+ }
98
+ };
27
99
  react.useEffect(() => {
28
100
  void loadStatus();
101
+ void loadWebhook();
29
102
  }, []);
103
+ const onRegisterWebhook = async () => {
104
+ setRegistering(true);
105
+ try {
106
+ const res = await fetch("/admin/printful/webhook", {
107
+ method: "POST",
108
+ credentials: "include",
109
+ headers: { "Content-Type": "application/json" },
110
+ body: JSON.stringify({ base_url: window.location.origin })
111
+ });
112
+ if (!res.ok) {
113
+ const text = await res.text();
114
+ throw new Error(text || `Registration failed (${res.status})`);
115
+ }
116
+ ui.toast.success("Printful webhook registered", {
117
+ description: "The new configuration replaced any previous Printful webhook."
118
+ });
119
+ await loadWebhook();
120
+ } catch (e) {
121
+ ui.toast.error("Failed to register webhook", {
122
+ description: e instanceof Error ? e.message : String(e)
123
+ });
124
+ } finally {
125
+ setRegistering(false);
126
+ }
127
+ };
30
128
  const onSync = async () => {
31
129
  var _a, _b, _c;
32
130
  setSyncing(true);
@@ -93,7 +191,41 @@ const PrintfulSyncWidget = () => {
93
191
  latest.products_failed ?? 0
94
192
  ] }),
95
193
  latest.error_message ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: latest.error_message.slice(0, 300) }) : null
96
- ] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "No sync runs yet." }) })
194
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "No sync runs yet." }) }),
195
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
196
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
197
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Webhook" }),
198
+ /* @__PURE__ */ jsxRuntime.jsx(
199
+ ui.Button,
200
+ {
201
+ size: "small",
202
+ variant: "secondary",
203
+ onClick: () => void onRegisterWebhook(),
204
+ isLoading: registering,
205
+ disabled: webhookLoading || registering || !(webhook == null ? void 0 : webhook.secret_set),
206
+ children: "Register webhook"
207
+ }
208
+ )
209
+ ] }),
210
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Printful allows only one webhook configuration per store. Registering replaces any existing Printful webhook URL and event list — including one set up by another integration or a previous install of this plugin." }),
211
+ webhookLoading && !webhook ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: "Loading webhook configuration…" }) : webhook ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
212
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", children: [
213
+ "Current URL:",
214
+ " ",
215
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: webhook.current.url ?? "Not configured" })
216
+ ] }),
217
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
218
+ "Current events:",
219
+ " ",
220
+ webhook.current.types.length > 0 ? webhook.current.types.join(", ") : "None"
221
+ ] }),
222
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
223
+ "Will register: ",
224
+ webhook.configured_types.join(", ")
225
+ ] }),
226
+ !webhook.secret_set ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: "Set the webhookSecret plugin option before registering a webhook." }) : null
227
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Unable to load webhook configuration." })
228
+ ] })
97
229
  ] });
98
230
  };
99
231
  adminSdk.defineWidgetConfig({
@@ -101,6 +233,11 @@ adminSdk.defineWidgetConfig({
101
233
  });
102
234
  const i18nTranslations0 = {};
103
235
  const widgetModule = { widgets: [
236
+ {
237
+ Component: PrintfulOrderWidget,
238
+ zone: ["order.details.side.before"],
239
+ widgetId: "Widget-537b"
240
+ },
104
241
  {
105
242
  Component: PrintfulSyncWidget,
106
243
  zone: ["product.list.before"],
@@ -1,11 +1,66 @@
1
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import { defineWidgetConfig } from "@medusajs/admin-sdk";
3
- import { Container, Heading, Text, Button, toast } from "@medusajs/ui";
3
+ import { Container, Heading, Badge, Text, Button, toast } from "@medusajs/ui";
4
4
  import { useState, useEffect } from "react";
5
+ const ATTENTION_STATES = /* @__PURE__ */ new Set(["failed", "canceled", "onhold"]);
6
+ const PrintfulOrderWidget = ({ data }) => {
7
+ const metadata = data.metadata ?? {};
8
+ const printfulOrderId = metadata.printful_order_id;
9
+ if (!printfulOrderId) {
10
+ return null;
11
+ }
12
+ const status = metadata.printful_status;
13
+ const statusUpdatedAt = metadata.printful_status_updated_at;
14
+ const shipments = metadata.printful_shipments ?? [];
15
+ const isAttention = status ? ATTENTION_STATES.has(status) : false;
16
+ return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
17
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
18
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Printful" }),
19
+ status ? /* @__PURE__ */ jsx(Badge, { color: isAttention ? "red" : "green", children: status }) : null
20
+ ] }),
21
+ /* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
22
+ /* @__PURE__ */ jsxs(Text, { size: "small", children: [
23
+ "Printful order: ",
24
+ /* @__PURE__ */ jsx("strong", { children: printfulOrderId })
25
+ ] }),
26
+ shipments.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3 pt-1", children: shipments.map((shipment) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
27
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
28
+ /* @__PURE__ */ jsxs(Text, { size: "small", children: [
29
+ shipment.carrier ?? "Unknown carrier",
30
+ shipment.service ? ` · ${shipment.service}` : ""
31
+ ] }),
32
+ shipment.reshipment ? /* @__PURE__ */ jsx(Badge, { color: "purple", size: "2xsmall", children: "Reshipment" }) : null
33
+ ] }),
34
+ shipment.reshipment ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Replacement parcel — no fulfillment is recorded for it." }) : null,
35
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: shipment.ship_date ? `Shipped ${new Date(shipment.ship_date).toLocaleDateString()}` : "Ship date pending" }),
36
+ shipment.tracking_number ? shipment.tracking_url ? /* @__PURE__ */ jsx(
37
+ "a",
38
+ {
39
+ href: shipment.tracking_url,
40
+ target: "_blank",
41
+ rel: "noreferrer noopener",
42
+ className: "text-ui-fg-interactive text-sm hover:underline",
43
+ children: shipment.tracking_number
44
+ }
45
+ ) : /* @__PURE__ */ jsx(Text, { size: "small", children: shipment.tracking_number }) : null
46
+ ] }, shipment.id)) }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "No shipments yet." }),
47
+ statusUpdatedAt ? /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle pt-1", children: [
48
+ "Last synced: ",
49
+ new Date(statusUpdatedAt).toLocaleString()
50
+ ] }) : null
51
+ ] })
52
+ ] });
53
+ };
54
+ defineWidgetConfig({
55
+ zone: "order.details.side.before"
56
+ });
5
57
  const PrintfulSyncWidget = () => {
6
58
  const [status, setStatus] = useState(null);
7
59
  const [loading, setLoading] = useState(false);
8
60
  const [syncing, setSyncing] = useState(false);
61
+ const [webhook, setWebhook] = useState(null);
62
+ const [webhookLoading, setWebhookLoading] = useState(false);
63
+ const [registering, setRegistering] = useState(false);
9
64
  const loadStatus = async () => {
10
65
  setLoading(true);
11
66
  try {
@@ -23,9 +78,52 @@ const PrintfulSyncWidget = () => {
23
78
  setLoading(false);
24
79
  }
25
80
  };
81
+ const loadWebhook = async () => {
82
+ setWebhookLoading(true);
83
+ try {
84
+ const res = await fetch("/admin/printful/webhook", {
85
+ credentials: "include"
86
+ });
87
+ if (!res.ok) {
88
+ throw new Error(`Status ${res.status}`);
89
+ }
90
+ const data = await res.json();
91
+ setWebhook(data);
92
+ } catch (e) {
93
+ console.error(e);
94
+ } finally {
95
+ setWebhookLoading(false);
96
+ }
97
+ };
26
98
  useEffect(() => {
27
99
  void loadStatus();
100
+ void loadWebhook();
28
101
  }, []);
102
+ const onRegisterWebhook = async () => {
103
+ setRegistering(true);
104
+ try {
105
+ const res = await fetch("/admin/printful/webhook", {
106
+ method: "POST",
107
+ credentials: "include",
108
+ headers: { "Content-Type": "application/json" },
109
+ body: JSON.stringify({ base_url: window.location.origin })
110
+ });
111
+ if (!res.ok) {
112
+ const text = await res.text();
113
+ throw new Error(text || `Registration failed (${res.status})`);
114
+ }
115
+ toast.success("Printful webhook registered", {
116
+ description: "The new configuration replaced any previous Printful webhook."
117
+ });
118
+ await loadWebhook();
119
+ } catch (e) {
120
+ toast.error("Failed to register webhook", {
121
+ description: e instanceof Error ? e.message : String(e)
122
+ });
123
+ } finally {
124
+ setRegistering(false);
125
+ }
126
+ };
29
127
  const onSync = async () => {
30
128
  var _a, _b, _c;
31
129
  setSyncing(true);
@@ -92,7 +190,41 @@ const PrintfulSyncWidget = () => {
92
190
  latest.products_failed ?? 0
93
191
  ] }),
94
192
  latest.error_message ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-error", children: latest.error_message.slice(0, 300) }) : null
95
- ] }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "No sync runs yet." }) })
193
+ ] }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "No sync runs yet." }) }),
194
+ /* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
195
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
196
+ /* @__PURE__ */ jsx(Heading, { level: "h3", children: "Webhook" }),
197
+ /* @__PURE__ */ jsx(
198
+ Button,
199
+ {
200
+ size: "small",
201
+ variant: "secondary",
202
+ onClick: () => void onRegisterWebhook(),
203
+ isLoading: registering,
204
+ disabled: webhookLoading || registering || !(webhook == null ? void 0 : webhook.secret_set),
205
+ children: "Register webhook"
206
+ }
207
+ )
208
+ ] }),
209
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Printful allows only one webhook configuration per store. Registering replaces any existing Printful webhook URL and event list — including one set up by another integration or a previous install of this plugin." }),
210
+ webhookLoading && !webhook ? /* @__PURE__ */ jsx(Text, { size: "small", children: "Loading webhook configuration…" }) : webhook ? /* @__PURE__ */ jsxs(Fragment, { children: [
211
+ /* @__PURE__ */ jsxs(Text, { size: "small", children: [
212
+ "Current URL:",
213
+ " ",
214
+ /* @__PURE__ */ jsx("strong", { children: webhook.current.url ?? "Not configured" })
215
+ ] }),
216
+ /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
217
+ "Current events:",
218
+ " ",
219
+ webhook.current.types.length > 0 ? webhook.current.types.join(", ") : "None"
220
+ ] }),
221
+ /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
222
+ "Will register: ",
223
+ webhook.configured_types.join(", ")
224
+ ] }),
225
+ !webhook.secret_set ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-error", children: "Set the webhookSecret plugin option before registering a webhook." }) : null
226
+ ] }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Unable to load webhook configuration." })
227
+ ] })
96
228
  ] });
97
229
  };
98
230
  defineWidgetConfig({
@@ -100,6 +232,11 @@ defineWidgetConfig({
100
232
  });
101
233
  const i18nTranslations0 = {};
102
234
  const widgetModule = { widgets: [
235
+ {
236
+ Component: PrintfulOrderWidget,
237
+ zone: ["order.details.side.before"],
238
+ widgetId: "Widget-537b"
239
+ },
103
240
  {
104
241
  Component: PrintfulSyncWidget,
105
242
  zone: ["product.list.before"],
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GET = GET;
4
+ exports.POST = POST;
5
+ const printful_1 = require("../../../../modules/printful");
6
+ const webhook_events_1 = require("../../../../utils/webhook-events");
7
+ /**
8
+ * Printful returns the webhook URL we registered, which embeds our secret
9
+ * token as its last path segment (see POST below). The GET here is
10
+ * admin-authenticated, so returning the URL at all is defensible — the whole
11
+ * point of this endpoint is to show the current config before an admin
12
+ * overwrites it. But the secret is a bearer credential (anyone who has it can
13
+ * forge webhook deliveries prior to the timing-safe check even being
14
+ * relevant, since it's compared as-is), and this endpoint is polled on every
15
+ * widget mount, so there is no reason to put it on the wire repeatedly. Mask
16
+ * everything after the last path segment so the UI can still show host+path
17
+ * shape and confirm "yes, a Printful webhook is registered here" without
18
+ * re-exposing the live token on every page load.
19
+ */
20
+ function maskSecretInUrl(url) {
21
+ if (!url) {
22
+ return url;
23
+ }
24
+ try {
25
+ const parsed = new URL(url);
26
+ const segments = parsed.pathname.split("/").filter(Boolean);
27
+ if (segments.length > 0) {
28
+ segments[segments.length - 1] = "••••••••";
29
+ parsed.pathname = `/${segments.join("/")}`;
30
+ }
31
+ return parsed.toString();
32
+ }
33
+ catch {
34
+ return url;
35
+ }
36
+ }
37
+ async function GET(req, res) {
38
+ const printful = req.scope.resolve(printful_1.PRINTFUL_MODULE);
39
+ const client = await printful.getClient();
40
+ const options = await printful.getOptions();
41
+ const current = await client.getWebhookConfig();
42
+ res.status(200).json({
43
+ current: {
44
+ ...current,
45
+ url: maskSecretInUrl(current.url),
46
+ },
47
+ configured_types: webhook_events_1.PRINTFUL_WEBHOOK_TYPES,
48
+ secret_set: Boolean(options.webhookSecret),
49
+ });
50
+ }
51
+ async function POST(req, res) {
52
+ const printful = req.scope.resolve(printful_1.PRINTFUL_MODULE);
53
+ const client = await printful.getClient();
54
+ const options = await printful.getOptions();
55
+ if (!options.webhookSecret) {
56
+ res.status(400).json({
57
+ message: "Set the webhookSecret plugin option before registering a webhook",
58
+ });
59
+ return;
60
+ }
61
+ const body = (req.body ?? {});
62
+ if (!body.base_url) {
63
+ res.status(400).json({ message: "base_url is required" });
64
+ return;
65
+ }
66
+ const url = `${body.base_url.replace(/\/$/, "")}/hooks/printful/${options.webhookSecret}`;
67
+ // Printful keeps one config per store, so this replaces the whole allowlist.
68
+ const updated = await client.setWebhookConfig(url, [
69
+ ...webhook_events_1.PRINTFUL_WEBHOOK_TYPES,
70
+ ]);
71
+ res.status(200).json({
72
+ current: {
73
+ ...updated,
74
+ url: maskSecretInUrl(updated.url),
75
+ },
76
+ });
77
+ }
78
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3ByaW50ZnVsL3dlYmhvb2svcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFtQ0Esa0JBZUM7QUFFRCxvQkFnQ0M7QUFuRkQsMkRBQThEO0FBRTlELHFFQUF5RTtBQUV6RTs7Ozs7Ozs7Ozs7O0dBWUc7QUFDSCxTQUFTLGVBQWUsQ0FBQyxHQUFrQjtJQUN6QyxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUM7UUFDVCxPQUFPLEdBQUcsQ0FBQTtJQUNaLENBQUM7SUFDRCxJQUFJLENBQUM7UUFDSCxNQUFNLE1BQU0sR0FBRyxJQUFJLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQTtRQUMzQixNQUFNLFFBQVEsR0FBRyxNQUFNLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUE7UUFDM0QsSUFBSSxRQUFRLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO1lBQ3hCLFFBQVEsQ0FBQyxRQUFRLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxHQUFHLFVBQVUsQ0FBQTtZQUMxQyxNQUFNLENBQUMsUUFBUSxHQUFHLElBQUksUUFBUSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFBO1FBQzVDLENBQUM7UUFDRCxPQUFPLE1BQU0sQ0FBQyxRQUFRLEVBQUUsQ0FBQTtJQUMxQixDQUFDO0lBQUMsTUFBTSxDQUFDO1FBQ1AsT0FBTyxHQUFHLENBQUE7SUFDWixDQUFDO0FBQ0gsQ0FBQztBQUVNLEtBQUssVUFBVSxHQUFHLENBQUMsR0FBa0IsRUFBRSxHQUFtQjtJQUMvRCxNQUFNLFFBQVEsR0FBMEIsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsMEJBQWUsQ0FBQyxDQUFBO0lBQzFFLE1BQU0sTUFBTSxHQUFHLE1BQU0sUUFBUSxDQUFDLFNBQVMsRUFBRSxDQUFBO0lBQ3pDLE1BQU0sT0FBTyxHQUFHLE1BQU0sUUFBUSxDQUFDLFVBQVUsRUFBRSxDQUFBO0lBRTNDLE1BQU0sT0FBTyxHQUFHLE1BQU0sTUFBTSxDQUFDLGdCQUFnQixFQUFFLENBQUE7SUFFL0MsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUM7UUFDbkIsT0FBTyxFQUFFO1lBQ1AsR0FBRyxPQUFPO1lBQ1YsR0FBRyxFQUFFLGVBQWUsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDO1NBQ2xDO1FBQ0QsZ0JBQWdCLEVBQUUsdUNBQXNCO1FBQ3hDLFVBQVUsRUFBRSxPQUFPLENBQUMsT0FBTyxDQUFDLGFBQWEsQ0FBQztLQUMzQyxDQUFDLENBQUE7QUFDSixDQUFDO0FBRU0sS0FBSyxVQUFVLElBQUksQ0FBQyxHQUFrQixFQUFFLEdBQW1CO0lBQ2hFLE1BQU0sUUFBUSxHQUEwQixHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQywwQkFBZSxDQUFDLENBQUE7SUFDMUUsTUFBTSxNQUFNLEdBQUcsTUFBTSxRQUFRLENBQUMsU0FBUyxFQUFFLENBQUE7SUFDekMsTUFBTSxPQUFPLEdBQUcsTUFBTSxRQUFRLENBQUMsVUFBVSxFQUFFLENBQUE7SUFFM0MsSUFBSSxDQUFDLE9BQU8sQ0FBQyxhQUFhLEVBQUUsQ0FBQztRQUMzQixHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQztZQUNuQixPQUFPLEVBQ0wsa0VBQWtFO1NBQ3JFLENBQUMsQ0FBQTtRQUNGLE9BQU07SUFDUixDQUFDO0lBRUQsTUFBTSxJQUFJLEdBQUcsQ0FBQyxHQUFHLENBQUMsSUFBSSxJQUFJLEVBQUUsQ0FBMEIsQ0FBQTtJQUN0RCxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDO1FBQ25CLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxDQUFDLEVBQUUsT0FBTyxFQUFFLHNCQUFzQixFQUFFLENBQUMsQ0FBQTtRQUN6RCxPQUFNO0lBQ1IsQ0FBQztJQUVELE1BQU0sR0FBRyxHQUFHLEdBQUcsSUFBSSxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxtQkFBbUIsT0FBTyxDQUFDLGFBQWEsRUFBRSxDQUFBO0lBRXpGLDZFQUE2RTtJQUM3RSxNQUFNLE9BQU8sR0FBRyxNQUFNLE1BQU0sQ0FBQyxnQkFBZ0IsQ0FBQyxHQUFHLEVBQUU7UUFDakQsR0FBRyx1Q0FBc0I7S0FDMUIsQ0FBQyxDQUFBO0lBRUYsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUM7UUFDbkIsT0FBTyxFQUFFO1lBQ1AsR0FBRyxPQUFPO1lBQ1YsR0FBRyxFQUFFLGVBQWUsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDO1NBQ2xDO0tBQ0YsQ0FBQyxDQUFBO0FBQ0osQ0FBQyJ9
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.POST = POST;
7
+ const utils_1 = require("@medusajs/framework/utils");
8
+ const printful_1 = require("../../../../modules/printful");
9
+ const webhook_events_1 = require("../../../../utils/webhook-events");
10
+ const apply_order_status_1 = __importDefault(require("../../../../workflows/apply-order-status"));
11
+ /**
12
+ * Public Printful webhook endpoint.
13
+ *
14
+ * The payload is a trigger, not a source of truth: we persist it, answer 200,
15
+ * and let the workflow re-read GET /orders/{id} for the real state. Never log
16
+ * the request URL or token.
17
+ */
18
+ async function POST(req, res) {
19
+ const printful = req.scope.resolve(printful_1.PRINTFUL_MODULE);
20
+ const logger = req.scope.resolve(utils_1.ContainerRegistrationKeys.LOGGER);
21
+ const options = await printful.getOptions();
22
+ const provided = req.params.token;
23
+ if (!(0, webhook_events_1.verifyWebhookToken)(options.webhookSecret, provided)) {
24
+ // 404, not 401: do not confirm that this path exists.
25
+ res.status(404).json({ message: "Not found" });
26
+ return;
27
+ }
28
+ const payload = (req.body ?? {});
29
+ const type = payload.type ?? "unknown";
30
+ const printfulOrderId = (0, webhook_events_1.extractOrderId)(payload);
31
+ if (!printfulOrderId) {
32
+ res.status(200).json({ received: true, ignored: "no_order_id" });
33
+ return;
34
+ }
35
+ let eventId;
36
+ try {
37
+ eventId = (0, webhook_events_1.deriveEventId)(payload);
38
+ }
39
+ catch {
40
+ // Malformed or hostile payload — refuse it without a 5xx, which would make
41
+ // Printful retry something that can never succeed.
42
+ res.status(400).json({ message: "Malformed payload" });
43
+ return;
44
+ }
45
+ const handled = webhook_events_1.PRINTFUL_WEBHOOK_TYPES.includes(type);
46
+ let event;
47
+ try {
48
+ event = await printful.recordWebhookEvent({
49
+ event_id: eventId,
50
+ type,
51
+ printful_order_id: printfulOrderId,
52
+ printful_shipment_id: (0, webhook_events_1.extractShipmentId)(payload),
53
+ payload: payload,
54
+ status: handled ? "received" : "ignored",
55
+ });
56
+ }
57
+ catch (err) {
58
+ // Only storage failures reach here. 500 is correct: we want Printful to retry.
59
+ logger.error(`Printful: failed to store webhook event ${eventId}: ${err instanceof Error ? err.message : String(err)}`);
60
+ res.status(500).json({ message: "Failed to store event" });
61
+ return;
62
+ }
63
+ // Redelivery of an event we already hold: absorb it.
64
+ if (!event) {
65
+ res.status(200).json({ received: true, event_id: eventId, duplicate: true });
66
+ return;
67
+ }
68
+ res.status(200).json({ received: true, event_id: eventId });
69
+ if (handled) {
70
+ // Fire-and-forget: the durable row plus the retry job (once it lands) are
71
+ // the intended safety net, so losing this in-process attempt costs at most
72
+ // one retry interval.
73
+ void (0, apply_order_status_1.default)(req.scope)
74
+ .run({ input: { event_row_id: event.id } })
75
+ .catch((err) => {
76
+ logger.error(`Printful: apply failed for event ${eventId}: ${err instanceof Error ? err.message : String(err)}`);
77
+ });
78
+ }
79
+ }
80
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2hvb2tzL3ByaW50ZnVsL1t0b2tlbl0vcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFxQkEsb0JBOEVDO0FBbEdELHFEQUFxRTtBQUNyRSwyREFBOEQ7QUFFOUQscUVBT3lDO0FBQ3pDLGtHQUErRTtBQUUvRTs7Ozs7O0dBTUc7QUFDSSxLQUFLLFVBQVUsSUFBSSxDQUFDLEdBQWtCLEVBQUUsR0FBbUI7SUFDaEUsTUFBTSxRQUFRLEdBQTBCLEdBQUcsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLDBCQUFlLENBQUMsQ0FBQTtJQUMxRSxNQUFNLE1BQU0sR0FBcUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQ2hFLGlDQUF5QixDQUFDLE1BQU0sQ0FDakMsQ0FBQTtJQUNELE1BQU0sT0FBTyxHQUFHLE1BQU0sUUFBUSxDQUFDLFVBQVUsRUFBRSxDQUFBO0lBRTNDLE1BQU0sUUFBUSxHQUFHLEdBQUcsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFBO0lBQ2pDLElBQUksQ0FBQyxJQUFBLG1DQUFrQixFQUFDLE9BQU8sQ0FBQyxhQUFhLEVBQUUsUUFBUSxDQUFDLEVBQUUsQ0FBQztRQUN6RCxzREFBc0Q7UUFDdEQsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUMsRUFBRSxPQUFPLEVBQUUsV0FBVyxFQUFFLENBQUMsQ0FBQTtRQUM5QyxPQUFNO0lBQ1IsQ0FBQztJQUVELE1BQU0sT0FBTyxHQUFHLENBQUMsR0FBRyxDQUFDLElBQUksSUFBSSxFQUFFLENBQTJCLENBQUE7SUFDMUQsTUFBTSxJQUFJLEdBQUcsT0FBTyxDQUFDLElBQUksSUFBSSxTQUFTLENBQUE7SUFDdEMsTUFBTSxlQUFlLEdBQUcsSUFBQSwrQkFBYyxFQUFDLE9BQU8sQ0FBQyxDQUFBO0lBRS9DLElBQUksQ0FBQyxlQUFl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
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const http_1 = require("@medusajs/framework/http");
4
+ const webhook_path_1 = require("../utils/webhook-path");
5
+ exports.default = (0, http_1.defineMiddlewares)({
6
+ routes: [
7
+ {
8
+ matcher: "/hooks/printful/*",
9
+ /**
10
+ * Sized deliberately, for two reasons.
11
+ *
12
+ * Correctness: Express defaults to 100KB, and a real `package_shipped`
13
+ * body for a ~25-line-item order already exceeds that (~132KB; ~262KB at
14
+ * 50 items). Under the default, Printful deliveries for large orders are
15
+ * rejected outright.
16
+ *
17
+ * Secrecy: that rejection is a 413 raised by the GLOBAL body-parser,
18
+ * which Medusa registers at `app.use("/", ...)` before any route-scoped
19
+ * middleware (@medusajs/framework/dist/http/router.js). Such an error
20
+ * goes straight to the error handler without ever running the redaction
21
+ * below, so the live secret in the path would be logged in cleartext.
22
+ * Raising the limit keeps legitimate traffic from ever reaching that
23
+ * path. This config IS honoured despite the global registration: the
24
+ * parser resolves its config per request via a RoutesFinder lookup on
25
+ * `req.path` (dist/http/middlewares/bodyparser.js).
26
+ */
27
+ bodyParser: { sizeLimit: webhook_path_1.WEBHOOK_BODY_SIZE_LIMIT },
28
+ middlewares: [
29
+ (req, _res, next) => {
30
+ // Medusa's error handler logs `Error ${statusCode} at ${req.path}`
31
+ // (@medusajs/framework/dist/http/middlewares/error-handler.js), and
32
+ // our path carries the webhook secret as a segment. Redact it before
33
+ // any downstream handler or the error middleware can write it out.
34
+ //
35
+ // Medusa registers plugin middleware with app.use(matcher, handler),
36
+ // so inside THIS function Express has already stripped the matched
37
+ // prefix: req.path is "/" and the secret sits in req.baseUrl. The
38
+ // full path is only reassembled downstream, which is why we redact
39
+ // req.originalUrl and pin req.path to the redacted full path — the
40
+ // value the error handler ends up reading.
41
+ //
42
+ // Verified: this does not affect req.params.token (param extraction
43
+ // happens in the route layer from the untouched URL), so the route's
44
+ // auth check is unaffected.
45
+ const redacted = (0, webhook_path_1.redactWebhookPath)(req.originalUrl);
46
+ Object.defineProperty(req, "path", {
47
+ value: redacted,
48
+ configurable: true,
49
+ });
50
+ Object.defineProperty(req, "originalUrl", {
51
+ value: redacted,
52
+ configurable: true,
53
+ });
54
+ next();
55
+ },
56
+ ],
57
+ },
58
+ ],
59
+ });
60
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWlkZGxld2FyZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvYXBpL21pZGRsZXdhcmVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsbURBQTREO0FBQzVELHdEQUc4QjtBQUU5QixrQkFBZSxJQUFBLHdCQUFpQixFQUFDO0lBQy9CLE1BQU0sRUFBRTtRQUNOO1lBQ0UsT0FBTyxFQUFFLG1CQUFtQjtZQUM1Qjs7Ozs7Ozs7Ozs7Ozs7Ozs7ZUFpQkc7WUFDSCxVQUFVLEVBQUUsRUFBRSxTQUFTLEVBQUUsc0NBQXVCLEVBQUU7WUFDbEQsV0FBVyxFQUFFO2dCQUNYLENBQUMsR0FBRyxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsRUFBRTtvQkFDbEIsbUVBQW1FO29CQUNuRSxvRUFBb0U7b0JBQ3BFLHFFQUFxRTtvQkFDckUsbUVBQW1FO29CQUNuRSxFQUFFO29CQUNGLHFFQUFxRTtvQkFDckUsbUVBQW1FO29CQUNuRSxrRUFBa0U7b0JBQ2xFLG1FQUFtRTtvQkFDbkUsbUVBQW1FO29CQUNuRSwyQ0FBMkM7b0JBQzNDLEVBQUU7b0JBQ0Ysb0VBQW9FO29CQUNwRSxxRUFBcUU7b0JBQ3JFLDRCQUE0QjtvQkFDNUIsTUFBTSxRQUFRLEdBQUcsSUFBQSxnQ0FBaUIsRUFBQyxHQUFHLENBQUMsV0FBVyxDQUFDLENBQUE7b0JBQ25ELE1BQU0sQ0FBQyxjQUFjLENBQUMsR0FBRyxFQUFFLE1BQU0sRUFBRTt3QkFDakMsS0FBSyxFQUFFLFFBQVE7d0JBQ2YsWUFBWSxFQUFFLElBQUk7cUJBQ25CLENBQUMsQ0FBQTtvQkFDRixNQUFNLENBQUMsY0FBYyxDQUFDLEdBQUcsRUFBRSxhQUFhLEVBQUU7d0JBQ3hDLEtBQUssRUFBRSxRQUFRO3dCQUNmLFlBQVksRUFBRSxJQUFJO3FCQUNuQixDQUFDLENBQUE7b0JBQ0YsSUFBSSxFQUFFLENBQUE7Z0JBQ1IsQ0FBQzthQUNGO1NBQ0Y7S0FDRjtDQUNGLENBQUMsQ0FBQSJ9
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.config = void 0;
7
+ exports.default = retryPrintfulWebhookEvents;
8
+ const utils_1 = require("@medusajs/framework/utils");
9
+ const printful_1 = require("../modules/printful");
10
+ const apply_order_status_1 = __importDefault(require("../workflows/apply-order-status"));
11
+ /**
12
+ * Drains webhook events that were stored but not applied — most often because
13
+ * the order link had not been written yet when the webhook landed, or because
14
+ * the process died between storing the event and applying it.
15
+ *
16
+ * This is the safety net the webhook route's fire-and-forget dispatch relies on:
17
+ * the route's in-process attempt is only an optimization to avoid waiting a
18
+ * retry interval in the common case.
19
+ */
20
+ /** Events drained per sweep. */
21
+ const BATCH_SIZE = 50;
22
+ /**
23
+ * Consecutive failures before the sweep gives up for this run. A database or
24
+ * Printful outage fails every event identically, and grinding through the whole
25
+ * batch each tick buys nothing — the remaining events keep their backoff and
26
+ * are picked up by the next sweep.
27
+ */
28
+ const CONSECUTIVE_FAILURE_LIMIT = 5;
29
+ async function retryPrintfulWebhookEvents(container) {
30
+ const printful = container.resolve(printful_1.PRINTFUL_MODULE);
31
+ const logger = container.resolve(utils_1.ContainerRegistrationKeys.LOGGER);
32
+ const due = await printful.listDueWebhookEvents(BATCH_SIZE);
33
+ if (!due.length) {
34
+ return;
35
+ }
36
+ logger.info(`Printful: retrying ${due.length} webhook event(s)`);
37
+ if (due.length === BATCH_SIZE) {
38
+ // A full batch means the backlog may exceed what one sweep can drain.
39
+ logger.info(`Printful: retry batch is full (${BATCH_SIZE}); backlog may be growing`);
40
+ }
41
+ let consecutiveFailures = 0;
42
+ for (const event of due) {
43
+ try {
44
+ await (0, apply_order_status_1.default)(container).run({
45
+ input: { event_row_id: event.id },
46
+ });
47
+ consecutiveFailures = 0;
48
+ }
49
+ catch (err) {
50
+ // The workflow records its own failure state; a throw here means the
51
+ // sweep should continue rather than abandon the remaining events.
52
+ consecutiveFailures += 1;
53
+ logger.error(`Printful: retry failed for event ${event.event_id}: ${err instanceof Error ? err.message : String(err)}`);
54
+ if (consecutiveFailures >= CONSECUTIVE_FAILURE_LIMIT) {
55
+ logger.error(`Printful: aborting retry sweep after ${consecutiveFailures} consecutive failures`);
56
+ return;
57
+ }
58
+ }
59
+ }
60
+ }
61
+ exports.config = {
62
+ name: "printful-retry-webhook-events",
63
+ schedule: "*/5 * * * *",
64
+ };
65
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmV0cnktd2ViaG9vay1ldmVudHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvam9icy9yZXRyeS13ZWJob29rLWV2ZW50cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUEwQkEsNkNBK0NDO0FBeEVELHFEQUFxRTtBQUNyRSxrREFBcUQ7QUFFckQseUZBQXNFO0FBRXRFOzs7Ozs7OztHQVFHO0FBQ0gsZ0NBQWdDO0FBQ2hDLE1BQU0sVUFBVSxHQUFHLEVBQUUsQ0FBQTtBQUVyQjs7Ozs7R0FLRztBQUNILE1BQU0seUJBQXlCLEdBQUcsQ0FBQyxDQUFBO0FBRXBCLEtBQUssVUFBVSwwQkFBMEIsQ0FDdEQsU0FBMEI7SUFFMUIsTUFBTSxRQUFRLEdBQTBCLFNBQVMsQ0FBQyxPQUFPLENBQUMsMEJBQWUsQ0FBQyxDQUFBO0lBQzFFLE1BQU0sTUFBTSxHQUNWLFNBQVMsQ0FBQyxPQUFPLENBQUMsaUNBQXlCLENBQUMsTUFBTSxDQUFDLENBQUE7SUFFckQsTUFBTSxHQUFHLEdBQUcsTUFBTSxRQUFRLENBQUMsb0JBQW9CLENBQUMsVUFBVSxDQUFDLENBQUE7SUFDM0QsSUFBSSxDQUFDLEdBQUcsQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUNoQixPQUFNO0lBQ1IsQ0FBQztJQUVELE1BQU0sQ0FBQyxJQUFJLENBQUMsc0JBQXNCLEdBQUcsQ0FBQyxNQUFNLG1CQUFtQixDQUFDLENBQUE7SUFFaEUsSUFBSSxHQUFHLENBQUMsTUFBTSxLQUFLLFVBQVUsRUFBRSxDQUFDO1FBQzlCLHNFQUFzRTtRQUN0RSxNQUFNLENBQUMsSUFBSSxDQUNULGtDQUFrQyxVQUFVLDJCQUEyQixDQUN4RSxDQUFBO0lBQ0gsQ0FBQztJQUVELElBQUksbUJBQW1CLEdBQUcsQ0FBQyxDQUFBO0lBRTNCLEtBQUssTUFBTSxLQUFLLElBQUksR0FBRyxFQUFFLENBQUM7UUFDeEIsSUFBSSxDQUFDO1lBQ0gsTUFBTSxJQUFBLDRCQUF3QixFQUFDLFNBQVMsQ0FBQyxDQUFDLEdBQUcsQ0FBQztnQkFDNUMsS0FBSyxFQUFFLEVBQUUsWUFBWSxFQUFFLEtBQUssQ0FBQyxFQUFFLEVBQUU7YUFDbEMsQ0FBQyxDQUFBO1lBQ0YsbUJBQW1CLEdBQUcsQ0FBQyxDQUFBO1FBQ3pCLENBQUM7UUFBQyxPQUFPLEdBQUcsRUFBRSxDQUFDO1lBQ2IscUVBQXFFO1lBQ3JFLGtFQUFrRTtZQUNsRSxtQkFBbUIsSUFBSSxDQUFDLENBQUE7WUFDeEIsTUFBTSxDQUFDLEtBQUssQ0FDVixvQ0FBb0MsS0FBSyxDQUFDLFFBQVEsS0FDaEQsR0FBRyxZQUFZLEtBQUssQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FDakQsRUFBRSxDQUNILENBQUE7WUFFRCxJQUFJLG1CQUFtQixJQUFJLHlCQUF5QixFQUFFLENBQUM7Z0JBQ3JELE1BQU0sQ0FBQyxLQUFLLENBQ1Ysd0NBQXdDLG1CQUFtQix1QkFBdUIsQ0FDbkYsQ0FBQTtnQkFDRCxPQUFNO1lBQ1IsQ0FBQztRQUNILENBQUM7SUFDSCxDQUFDO0FBQ0gsQ0FBQztBQUVZLFFBQUEsTUFBTSxHQUFHO0lBQ3BCLElBQUksRUFBRSwrQkFBK0I7SUFDckMsUUFBUSxFQUFFLGFBQWE7Q0FDeEIsQ0FBQSJ9