@wix/web5-core 1.56.0 → 1.57.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.
Files changed (197) hide show
  1. package/dist/cjs/client/wixAuthFetch.js +2 -2
  2. package/dist/cjs/client/wixAuthFetch.js.map +1 -1
  3. package/dist/cjs/component/componentDefinitions/HeroEntitySectionDefinition.js +20 -15
  4. package/dist/cjs/component/componentDefinitions/HeroEntitySectionDefinition.js.map +1 -1
  5. package/dist/cjs/component/componentDefinitions/HeroSectionDefinition.js +12 -4
  6. package/dist/cjs/component/componentDefinitions/HeroSectionDefinition.js.map +1 -1
  7. package/dist/cjs/component/componentDefinitions/listItemsSectionDefinition.js +24 -29
  8. package/dist/cjs/component/componentDefinitions/listItemsSectionDefinition.js.map +1 -1
  9. package/dist/cjs/component/componentParser.js +8 -1
  10. package/dist/cjs/component/componentParser.js.map +1 -1
  11. package/dist/cjs/component/types.js.map +1 -1
  12. package/dist/cjs/components/placement/PlacementResponseRenderer.css +0 -4
  13. package/dist/cjs/components/placement/PlacementResponseRenderer.js +11 -33
  14. package/dist/cjs/components/placement/PlacementResponseRenderer.js.map +1 -1
  15. package/dist/cjs/components/placement/buildPlacementDependencies.js.map +1 -1
  16. package/dist/cjs/components/ui/OptimizedImage.js +59 -14
  17. package/dist/cjs/components/ui/OptimizedImage.js.map +1 -1
  18. package/dist/cjs/components/ui/SectionSkeleton.css +19 -0
  19. package/dist/cjs/components/ui/SectionSkeleton.js +8 -4
  20. package/dist/cjs/components/ui/SectionSkeleton.js.map +1 -1
  21. package/dist/cjs/components/ui/UserQuery.css +72 -0
  22. package/dist/cjs/components/ui/UserQuery.js +131 -37
  23. package/dist/cjs/components/ui/UserQuery.js.map +1 -1
  24. package/dist/cjs/context/UserQueryContext.js +34 -0
  25. package/dist/cjs/context/UserQueryContext.js.map +1 -0
  26. package/dist/cjs/entity/defaultExtractor.js +9 -2
  27. package/dist/cjs/entity/defaultExtractor.js.map +1 -1
  28. package/dist/cjs/entity/fetchEntityListData.js +13 -26
  29. package/dist/cjs/entity/fetchEntityListData.js.map +1 -1
  30. package/dist/cjs/entity/index.js +5 -1
  31. package/dist/cjs/entity/index.js.map +1 -1
  32. package/dist/cjs/entity/listEntityItems.js +90 -0
  33. package/dist/cjs/entity/listEntityItems.js.map +1 -0
  34. package/dist/cjs/hostScope.js +63 -0
  35. package/dist/cjs/hostScope.js.map +1 -0
  36. package/dist/cjs/index.js +38 -13
  37. package/dist/cjs/index.js.map +1 -1
  38. package/dist/cjs/patternValidator/validator.test.js +61 -61
  39. package/dist/cjs/patternValidator/validator.test.js.map +1 -1
  40. package/dist/cjs/registry/ComponentRegistry.js +22 -2
  41. package/dist/cjs/registry/ComponentRegistry.js.map +1 -1
  42. package/dist/cjs/registry/index.js +1 -3
  43. package/dist/cjs/registry/index.js.map +1 -1
  44. package/dist/cjs/registry/types.js.map +1 -1
  45. package/dist/cjs/styles/base-utilities.css +35 -0
  46. package/dist/cjs/styles/components.css +1 -0
  47. package/dist/cjs/styles/host-reset.css +81 -0
  48. package/dist/cjs/styles/tailwind-theme.css +6 -6
  49. package/dist/cjs/utils/backendEnvironment.js +78 -0
  50. package/dist/cjs/utils/backendEnvironment.js.map +1 -0
  51. package/dist/cjs/utils/entityLinkParser.js +3 -1
  52. package/dist/cjs/utils/entityLinkParser.js.map +1 -1
  53. package/dist/cjs/utils/imageBackdrop.js +269 -0
  54. package/dist/cjs/utils/imageBackdrop.js.map +1 -0
  55. package/dist/cjs/utils/intentExtractor.js +20 -9
  56. package/dist/cjs/utils/intentExtractor.js.map +1 -1
  57. package/dist/cjs/utils/productBackHandoff.js +106 -0
  58. package/dist/cjs/utils/productBackHandoff.js.map +1 -0
  59. package/dist/cjs/utils/refreshPrompts.js +72 -0
  60. package/dist/cjs/utils/refreshPrompts.js.map +1 -0
  61. package/dist/esm/client/wixAuthFetch.js +2 -2
  62. package/dist/esm/client/wixAuthFetch.js.map +1 -1
  63. package/dist/esm/component/componentDefinitions/HeroEntitySectionDefinition.js +20 -15
  64. package/dist/esm/component/componentDefinitions/HeroEntitySectionDefinition.js.map +1 -1
  65. package/dist/esm/component/componentDefinitions/HeroSectionDefinition.js +12 -4
  66. package/dist/esm/component/componentDefinitions/HeroSectionDefinition.js.map +1 -1
  67. package/dist/esm/component/componentDefinitions/listItemsSectionDefinition.js +25 -30
  68. package/dist/esm/component/componentDefinitions/listItemsSectionDefinition.js.map +1 -1
  69. package/dist/esm/component/componentParser.js +8 -1
  70. package/dist/esm/component/componentParser.js.map +1 -1
  71. package/dist/esm/component/types.js.map +1 -1
  72. package/dist/esm/components/placement/PlacementResponseRenderer.css +0 -4
  73. package/dist/esm/components/placement/PlacementResponseRenderer.js +1 -16
  74. package/dist/esm/components/placement/PlacementResponseRenderer.js.map +1 -1
  75. package/dist/esm/components/placement/buildPlacementDependencies.js.map +1 -1
  76. package/dist/esm/components/ui/OptimizedImage.js +50 -5
  77. package/dist/esm/components/ui/OptimizedImage.js.map +1 -1
  78. package/dist/esm/components/ui/SectionSkeleton.css +19 -0
  79. package/dist/esm/components/ui/SectionSkeleton.js +5 -1
  80. package/dist/esm/components/ui/SectionSkeleton.js.map +1 -1
  81. package/dist/esm/components/ui/UserQuery.css +72 -0
  82. package/dist/esm/components/ui/UserQuery.js +52 -6
  83. package/dist/esm/components/ui/UserQuery.js.map +1 -1
  84. package/dist/esm/context/UserQueryContext.js +21 -0
  85. package/dist/esm/context/UserQueryContext.js.map +1 -0
  86. package/dist/esm/entity/defaultExtractor.js +9 -2
  87. package/dist/esm/entity/defaultExtractor.js.map +1 -1
  88. package/dist/esm/entity/fetchEntityListData.js +14 -26
  89. package/dist/esm/entity/fetchEntityListData.js.map +1 -1
  90. package/dist/esm/entity/index.js +1 -0
  91. package/dist/esm/entity/index.js.map +1 -1
  92. package/dist/esm/entity/listEntityItems.js +85 -0
  93. package/dist/esm/entity/listEntityItems.js.map +1 -0
  94. package/dist/esm/hostScope.js +59 -0
  95. package/dist/esm/hostScope.js.map +1 -0
  96. package/dist/esm/index.js +13 -4
  97. package/dist/esm/index.js.map +1 -1
  98. package/dist/esm/patternValidator/validator.test.js +61 -61
  99. package/dist/esm/patternValidator/validator.test.js.map +1 -1
  100. package/dist/esm/registry/ComponentRegistry.js +22 -2
  101. package/dist/esm/registry/ComponentRegistry.js.map +1 -1
  102. package/dist/esm/registry/index.js +0 -1
  103. package/dist/esm/registry/index.js.map +1 -1
  104. package/dist/esm/registry/types.js.map +1 -1
  105. package/dist/esm/styles/base-utilities.css +35 -0
  106. package/dist/esm/styles/components.css +1 -0
  107. package/dist/esm/styles/host-reset.css +81 -0
  108. package/dist/esm/styles/tailwind-theme.css +6 -6
  109. package/dist/esm/utils/backendEnvironment.js +71 -0
  110. package/dist/esm/utils/backendEnvironment.js.map +1 -0
  111. package/dist/esm/utils/entityLinkParser.js +3 -1
  112. package/dist/esm/utils/entityLinkParser.js.map +1 -1
  113. package/dist/esm/utils/imageBackdrop.js +262 -0
  114. package/dist/esm/utils/imageBackdrop.js.map +1 -0
  115. package/dist/esm/utils/intentExtractor.js +21 -12
  116. package/dist/esm/utils/intentExtractor.js.map +1 -1
  117. package/dist/esm/utils/productBackHandoff.js +100 -0
  118. package/dist/esm/utils/productBackHandoff.js.map +1 -0
  119. package/dist/esm/utils/refreshPrompts.js +67 -0
  120. package/dist/esm/utils/refreshPrompts.js.map +1 -0
  121. package/dist/types/component/componentDefinitions/HeroEntitySectionDefinition.d.ts.map +1 -1
  122. package/dist/types/component/componentDefinitions/HeroSectionDefinition.d.ts.map +1 -1
  123. package/dist/types/component/componentDefinitions/listItemsSectionDefinition.d.ts +2 -2
  124. package/dist/types/component/componentDefinitions/listItemsSectionDefinition.d.ts.map +1 -1
  125. package/dist/types/component/componentParser.d.ts.map +1 -1
  126. package/dist/types/component/types.d.ts +1 -5
  127. package/dist/types/component/types.d.ts.map +1 -1
  128. package/dist/types/components/placement/PlacementResponseRenderer.d.ts +1 -1
  129. package/dist/types/components/placement/PlacementResponseRenderer.d.ts.map +1 -1
  130. package/dist/types/components/placement/buildPlacementDependencies.d.ts +1 -2
  131. package/dist/types/components/placement/buildPlacementDependencies.d.ts.map +1 -1
  132. package/dist/types/components/ui/OptimizedImage.d.ts +9 -0
  133. package/dist/types/components/ui/OptimizedImage.d.ts.map +1 -1
  134. package/dist/types/components/ui/SectionSkeleton.d.ts.map +1 -1
  135. package/dist/types/components/ui/UserQuery.d.ts +21 -0
  136. package/dist/types/components/ui/UserQuery.d.ts.map +1 -1
  137. package/dist/types/context/UserQueryContext.d.ts +12 -0
  138. package/dist/types/context/UserQueryContext.d.ts.map +1 -0
  139. package/dist/types/entity/defaultExtractor.d.ts.map +1 -1
  140. package/dist/types/entity/fetchEntityListData.d.ts +0 -2
  141. package/dist/types/entity/fetchEntityListData.d.ts.map +1 -1
  142. package/dist/types/entity/index.d.ts +1 -0
  143. package/dist/types/entity/index.d.ts.map +1 -1
  144. package/dist/types/entity/listEntityItems.d.ts +40 -0
  145. package/dist/types/entity/listEntityItems.d.ts.map +1 -0
  146. package/dist/types/hostScope.d.ts +83 -0
  147. package/dist/types/hostScope.d.ts.map +1 -0
  148. package/dist/types/index.d.ts +11 -7
  149. package/dist/types/index.d.ts.map +1 -1
  150. package/dist/types/registry/ComponentRegistry.d.ts +8 -0
  151. package/dist/types/registry/ComponentRegistry.d.ts.map +1 -1
  152. package/dist/types/registry/index.d.ts +0 -1
  153. package/dist/types/registry/index.d.ts.map +1 -1
  154. package/dist/types/registry/types.d.ts +5 -6
  155. package/dist/types/registry/types.d.ts.map +1 -1
  156. package/dist/types/utils/backendEnvironment.d.ts +30 -0
  157. package/dist/types/utils/backendEnvironment.d.ts.map +1 -0
  158. package/dist/types/utils/entityLinkParser.d.ts.map +1 -1
  159. package/dist/types/utils/imageBackdrop.d.ts +102 -0
  160. package/dist/types/utils/imageBackdrop.d.ts.map +1 -0
  161. package/dist/types/utils/intentExtractor.d.ts +21 -4
  162. package/dist/types/utils/intentExtractor.d.ts.map +1 -1
  163. package/dist/types/utils/productBackHandoff.d.ts +43 -0
  164. package/dist/types/utils/productBackHandoff.d.ts.map +1 -0
  165. package/dist/types/utils/refreshPrompts.d.ts +32 -0
  166. package/dist/types/utils/refreshPrompts.d.ts.map +1 -0
  167. package/package.json +4 -3
  168. package/src/components/placement/PlacementResponseRenderer.css +265 -0
  169. package/src/components/ui/Disclaimer.css +35 -0
  170. package/src/components/ui/FeedbackBar.css +267 -0
  171. package/src/components/ui/PlacementLoader.css +101 -0
  172. package/src/components/ui/PromptEntryEmptyState.css +172 -0
  173. package/src/components/ui/SearchSection.css +538 -0
  174. package/src/components/ui/SectionSkeleton.css +128 -0
  175. package/src/components/ui/UserQuery.css +72 -0
  176. package/src/styles/base-utilities.css +35 -0
  177. package/src/styles/components.css +1 -0
  178. package/src/styles/host-reset.css +81 -0
  179. package/src/styles/tailwind-theme.css +6 -6
  180. package/dist/cjs/registry/Intent.js +0 -19
  181. package/dist/cjs/registry/Intent.js.map +0 -1
  182. package/dist/cjs/utils/conversationApi.js +0 -52
  183. package/dist/cjs/utils/conversationApi.js.map +0 -1
  184. package/dist/cjs/utils/conversationMode.js +0 -23
  185. package/dist/cjs/utils/conversationMode.js.map +0 -1
  186. package/dist/esm/registry/Intent.js +0 -15
  187. package/dist/esm/registry/Intent.js.map +0 -1
  188. package/dist/esm/utils/conversationApi.js +0 -45
  189. package/dist/esm/utils/conversationApi.js.map +0 -1
  190. package/dist/esm/utils/conversationMode.js +0 -18
  191. package/dist/esm/utils/conversationMode.js.map +0 -1
  192. package/dist/types/registry/Intent.d.ts +0 -12
  193. package/dist/types/registry/Intent.d.ts.map +0 -1
  194. package/dist/types/utils/conversationApi.d.ts +0 -26
  195. package/dist/types/utils/conversationApi.d.ts.map +0 -1
  196. package/dist/types/utils/conversationMode.d.ts +0 -16
  197. package/dist/types/utils/conversationMode.d.ts.map +0 -1
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.analyzeBackdrop = analyzeBackdrop;
5
+ exports.buildProbeUrl = buildProbeUrl;
6
+ exports.computeContentBBox = computeContentBBox;
7
+ exports.loadBackdropAnalysis = loadBackdropAnalysis;
8
+ exports.loadImagePixels = loadImagePixels;
9
+ var _imageUtils = require("./image-utils");
10
+ /**
11
+ * Result of inspecting an image's edges to decide how it should sit in a slot
12
+ * whose aspect ratio differs from the image's own.
13
+ *
14
+ * The problem: a single CSS `object-fit` can't serve both kinds of imagery a
15
+ * conversational page surfaces. Product cutouts (subject on a seamless white
16
+ * sweep, or a transparent PNG) must never be cropped — `cover` chops the
17
+ * product. Full-bleed scenes (a model in a street, a flat-lay that fills the
18
+ * frame) look broken when letterboxed — they want `cover`.
19
+ *
20
+ * We can't tell these apart from the resolver metadata (it only exposes url +
21
+ * optional dimensions), so we sniff the pixels: sample the image's border ring
22
+ * and ask "is there a consistent backdrop colour behind the subject?".
23
+ * - Consistent border → seamless backdrop → render `contain` and paint the
24
+ * slot that backdrop colour, so the "letterbox" becomes an invisible
25
+ * extension of the image's own sweep (white shoe → white fill; green
26
+ * sweater sweep → green fill). The whole subject is always visible.
27
+ * - Noisy border → edge-to-edge scene → render `cover`; cropping the
28
+ * margins is safe and letterboxing would look wrong.
29
+ */
30
+
31
+ const NO_BACKDROP = {
32
+ hasBackdrop: false
33
+ };
34
+
35
+ /** Pixels at/beyond this alpha are treated as opaque. */
36
+ const OPAQUE_ALPHA = 250;
37
+ /** A border is "transparent" when at least this fraction of it is see-through. */
38
+ const TRANSPARENT_BORDER_FRACTION = 0.6;
39
+ /** Max per-channel distance from the median for a pixel to count as "backdrop". */
40
+ const COLOR_TOLERANCE = 28;
41
+ /** A border is a "backdrop" when at least this fraction sits near the median. */
42
+ const BACKDROP_AGREEMENT = 0.85;
43
+ function median(values) {
44
+ if (values.length === 0) {
45
+ return 0;
46
+ }
47
+ const sorted = [...values].sort((a, b) => a - b);
48
+ const mid = Math.floor(sorted.length / 2);
49
+ return sorted.length % 2 === 0 ? Math.round((sorted[mid - 1] + sorted[mid]) / 2) : sorted[mid];
50
+ }
51
+
52
+ /**
53
+ * Inspect the 1px border ring of an RGBA buffer and classify the backdrop.
54
+ * Pure and synchronous so it can be unit-tested without a DOM.
55
+ */
56
+ function analyzeBackdrop(data, width, height) {
57
+ if (width < 3 || height < 3 || data.length < width * height * 4) {
58
+ return NO_BACKDROP;
59
+ }
60
+ const reds = [];
61
+ const greens = [];
62
+ const blues = [];
63
+ let transparentCount = 0;
64
+ let total = 0;
65
+ const sample = (x, y) => {
66
+ const i = (y * width + x) * 4;
67
+ total += 1;
68
+ if (data[i + 3] < OPAQUE_ALPHA) {
69
+ transparentCount += 1;
70
+ return;
71
+ }
72
+ reds.push(data[i]);
73
+ greens.push(data[i + 1]);
74
+ blues.push(data[i + 2]);
75
+ };
76
+ for (let x = 0; x < width; x += 1) {
77
+ sample(x, 0);
78
+ sample(x, height - 1);
79
+ }
80
+ for (let y = 1; y < height - 1; y += 1) {
81
+ sample(0, y);
82
+ sample(width - 1, y);
83
+ }
84
+ if (total === 0) {
85
+ return NO_BACKDROP;
86
+ }
87
+
88
+ // Mostly-transparent border → a cutout PNG. Let the page background show.
89
+ if (transparentCount / total >= TRANSPARENT_BORDER_FRACTION) {
90
+ return {
91
+ hasBackdrop: true,
92
+ backgroundColor: 'transparent'
93
+ };
94
+ }
95
+ const opaque = reds.length;
96
+ if (opaque === 0) {
97
+ return NO_BACKDROP;
98
+ }
99
+ const mr = median(reds);
100
+ const mg = median(greens);
101
+ const mb = median(blues);
102
+ let agree = 0;
103
+ for (let k = 0; k < opaque; k += 1) {
104
+ if (Math.abs(reds[k] - mr) <= COLOR_TOLERANCE && Math.abs(greens[k] - mg) <= COLOR_TOLERANCE && Math.abs(blues[k] - mb) <= COLOR_TOLERANCE) {
105
+ agree += 1;
106
+ }
107
+ }
108
+ if (agree / opaque >= BACKDROP_AGREEMENT) {
109
+ return {
110
+ hasBackdrop: true,
111
+ backgroundColor: `rgb(${mr}, ${mg}, ${mb})`
112
+ };
113
+ }
114
+ return NO_BACKDROP;
115
+ }
116
+
117
+ /**
118
+ * Bounding box of the non-background content in a probed image, in sample-grid
119
+ * coordinates (0..size-1), plus the sampled background colour. Used by callers
120
+ * that need *where the subject sits* (e.g. crop-safety in a fixed-aspect slot),
121
+ * which is a different question from `analyzeBackdrop`'s "is there a backdrop".
122
+ */
123
+
124
+ /**
125
+ * Find the bounding box of the subject by treating the top-left pixel as the
126
+ * background colour and flagging every pixel that differs from it (or is
127
+ * transparent). Pure and synchronous. Returns null for an all-background frame.
128
+ *
129
+ * `backgroundTolerance`/`alphaThreshold` default to the values tuned for
130
+ * uniform studio catalog shots; callers can override.
131
+ */
132
+ function computeContentBBox(data, size, options) {
133
+ const tol = (options == null ? void 0 : options.backgroundTolerance) ?? 12;
134
+ const alpha = (options == null ? void 0 : options.alphaThreshold) ?? 16;
135
+ if (size < 1 || data.length < size * size * 4) {
136
+ return null;
137
+ }
138
+ const br = data[0];
139
+ const bg = data[1];
140
+ const bb = data[2];
141
+ const isBg = i => data[i + 3] < alpha || Math.abs(data[i] - br) < tol && Math.abs(data[i + 1] - bg) < tol && Math.abs(data[i + 2] - bb) < tol;
142
+ let top = size;
143
+ let bottom = -1;
144
+ let left = size;
145
+ let right = -1;
146
+ for (let y = 0; y < size; y += 1) {
147
+ for (let x = 0; x < size; x += 1) {
148
+ if (!isBg((y * size + x) * 4)) {
149
+ top = Math.min(top, y);
150
+ bottom = Math.max(bottom, y);
151
+ left = Math.min(left, x);
152
+ right = Math.max(right, x);
153
+ }
154
+ }
155
+ }
156
+ if (bottom < 0) {
157
+ return null;
158
+ }
159
+ return {
160
+ top,
161
+ bottom,
162
+ left,
163
+ right,
164
+ background: {
165
+ r: br,
166
+ g: bg,
167
+ b: bb
168
+ }
169
+ };
170
+ }
171
+
172
+ /** Square sample grid for the probe — small enough to be cheap, big enough to
173
+ * read edges and locate a subject. */
174
+ const PROBE_BOX = 64;
175
+
176
+ /**
177
+ * Build the URL of a tiny, aspect-preserving (`fit`) version of an image so we
178
+ * can read its true edges. Wix media is downscaled by the CDN; other URLs are
179
+ * probed at full size (still drawn onto a small canvas).
180
+ */
181
+ function buildProbeUrl(effectiveFilename) {
182
+ const normalized = (0, _imageUtils.normalizeImageUrl)(effectiveFilename);
183
+ if (!normalized.includes('static.wixstatic.com')) {
184
+ return normalized;
185
+ }
186
+ const match = normalized.match(/media\/([^/?]+)/);
187
+ if (!(match != null && match[1])) {
188
+ return normalized;
189
+ }
190
+ return (0, _imageUtils.getResizedImageUrl)(match[1], PROBE_BOX, PROBE_BOX, {
191
+ quality: 70,
192
+ imageMode: 'fit'
193
+ });
194
+ }
195
+
196
+ /** RGBA pixels of a probed image, drawn onto a `size`×`size` grid, alongside the
197
+ * source's true intrinsic dimensions (needed for aspect-aware crop math). */
198
+
199
+ /**
200
+ * Shared probe plumbing: load a (downscaled, for Wix) version of the image
201
+ * cross-origin, draw it onto a `size`×`size` canvas, and return its pixels plus
202
+ * intrinsic dimensions. Resolves to `null` on any failure — missing DOM, a
203
+ * data/blob source, a load error, or a tainted canvas (if CORS ever regresses)
204
+ * — so every caller can fall back to its own default. Browser-only.
205
+ *
206
+ * Both the backdrop classifier (`loadBackdropAnalysis`) and bounding-box callers
207
+ * build on this so the canvas/CORS/taint handling lives in exactly one place.
208
+ */
209
+ function loadImagePixels(effectiveFilename, options, signal) {
210
+ const size = (options == null ? void 0 : options.size) ?? PROBE_BOX;
211
+ return new Promise(resolve => {
212
+ if (typeof document === 'undefined' || !effectiveFilename) {
213
+ resolve(null);
214
+ return;
215
+ }
216
+ const probeUrl = buildProbeUrl(effectiveFilename);
217
+ if (probeUrl.startsWith('data:') || probeUrl.startsWith('blob:')) {
218
+ resolve(null);
219
+ return;
220
+ }
221
+ const img = new Image();
222
+ img.crossOrigin = 'anonymous';
223
+ img.decoding = 'async';
224
+ img.onload = () => {
225
+ if (signal != null && signal.aborted) {
226
+ resolve(null);
227
+ return;
228
+ }
229
+ try {
230
+ const canvas = document.createElement('canvas');
231
+ canvas.width = size;
232
+ canvas.height = size;
233
+ const ctx = canvas.getContext('2d', {
234
+ willReadFrequently: true
235
+ });
236
+ if (!ctx) {
237
+ resolve(null);
238
+ return;
239
+ }
240
+ ctx.drawImage(img, 0, 0, size, size);
241
+ const {
242
+ data
243
+ } = ctx.getImageData(0, 0, size, size);
244
+ resolve({
245
+ data,
246
+ size,
247
+ naturalWidth: img.naturalWidth || size,
248
+ naturalHeight: img.naturalHeight || size
249
+ });
250
+ } catch {
251
+ // Tainted canvas or any read failure → let the caller default.
252
+ resolve(null);
253
+ }
254
+ };
255
+ img.onerror = () => resolve(null);
256
+ img.src = probeUrl;
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Load a probe image and analyze its backdrop. Resolves to `NO_BACKDROP` on any
262
+ * failure so callers fall back to their default fit. Browser-only.
263
+ */
264
+ function loadBackdropAnalysis(effectiveFilename, signal) {
265
+ return loadImagePixels(effectiveFilename, {
266
+ size: PROBE_BOX
267
+ }, signal).then(pixels => pixels ? analyzeBackdrop(pixels.data, pixels.size, pixels.size) : NO_BACKDROP);
268
+ }
269
+ //# sourceMappingURL=imageBackdrop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_imageUtils","require","NO_BACKDROP","hasBackdrop","OPAQUE_ALPHA","TRANSPARENT_BORDER_FRACTION","COLOR_TOLERANCE","BACKDROP_AGREEMENT","median","values","length","sorted","sort","a","b","mid","Math","floor","round","analyzeBackdrop","data","width","height","reds","greens","blues","transparentCount","total","sample","x","y","i","push","backgroundColor","opaque","mr","mg","mb","agree","k","abs","computeContentBBox","size","options","tol","backgroundTolerance","alpha","alphaThreshold","br","bg","bb","isBg","top","bottom","left","right","min","max","background","r","g","PROBE_BOX","buildProbeUrl","effectiveFilename","normalized","normalizeImageUrl","includes","match","getResizedImageUrl","quality","imageMode","loadImagePixels","signal","Promise","resolve","document","probeUrl","startsWith","img","Image","crossOrigin","decoding","onload","aborted","canvas","createElement","ctx","getContext","willReadFrequently","drawImage","getImageData","naturalWidth","naturalHeight","onerror","src","loadBackdropAnalysis","then","pixels"],"sources":["../../../src/utils/imageBackdrop.ts"],"sourcesContent":["import { getResizedImageUrl, normalizeImageUrl } from './image-utils';\n\n/**\n * Result of inspecting an image's edges to decide how it should sit in a slot\n * whose aspect ratio differs from the image's own.\n *\n * The problem: a single CSS `object-fit` can't serve both kinds of imagery a\n * conversational page surfaces. Product cutouts (subject on a seamless white\n * sweep, or a transparent PNG) must never be cropped — `cover` chops the\n * product. Full-bleed scenes (a model in a street, a flat-lay that fills the\n * frame) look broken when letterboxed — they want `cover`.\n *\n * We can't tell these apart from the resolver metadata (it only exposes url +\n * optional dimensions), so we sniff the pixels: sample the image's border ring\n * and ask \"is there a consistent backdrop colour behind the subject?\".\n * - Consistent border → seamless backdrop → render `contain` and paint the\n * slot that backdrop colour, so the \"letterbox\" becomes an invisible\n * extension of the image's own sweep (white shoe → white fill; green\n * sweater sweep → green fill). The whole subject is always visible.\n * - Noisy border → edge-to-edge scene → render `cover`; cropping the\n * margins is safe and letterboxing would look wrong.\n */\nexport interface BackdropAnalysis {\n /** True when the border reads as a seamless backdrop (uniform or transparent). */\n hasBackdrop: boolean;\n /**\n * CSS colour to paint behind a `contain`ed image so the letterbox blends.\n * `'transparent'` for cutouts with an alpha border (the page background then\n * shows through). Undefined when `hasBackdrop` is false.\n */\n backgroundColor?: string;\n}\n\nconst NO_BACKDROP: BackdropAnalysis = { hasBackdrop: false };\n\n/** Pixels at/beyond this alpha are treated as opaque. */\nconst OPAQUE_ALPHA = 250;\n/** A border is \"transparent\" when at least this fraction of it is see-through. */\nconst TRANSPARENT_BORDER_FRACTION = 0.6;\n/** Max per-channel distance from the median for a pixel to count as \"backdrop\". */\nconst COLOR_TOLERANCE = 28;\n/** A border is a \"backdrop\" when at least this fraction sits near the median. */\nconst BACKDROP_AGREEMENT = 0.85;\n\nfunction median(values: number[]): number {\n if (values.length === 0) {\n return 0;\n }\n const sorted = [...values].sort((a, b) => a - b);\n const mid = Math.floor(sorted.length / 2);\n return sorted.length % 2 === 0\n ? Math.round((sorted[mid - 1] + sorted[mid]) / 2)\n : sorted[mid];\n}\n\n/**\n * Inspect the 1px border ring of an RGBA buffer and classify the backdrop.\n * Pure and synchronous so it can be unit-tested without a DOM.\n */\nexport function analyzeBackdrop(\n data: Uint8ClampedArray,\n width: number,\n height: number,\n): BackdropAnalysis {\n if (width < 3 || height < 3 || data.length < width * height * 4) {\n return NO_BACKDROP;\n }\n\n const reds: number[] = [];\n const greens: number[] = [];\n const blues: number[] = [];\n let transparentCount = 0;\n let total = 0;\n\n const sample = (x: number, y: number) => {\n const i = (y * width + x) * 4;\n total += 1;\n if (data[i + 3] < OPAQUE_ALPHA) {\n transparentCount += 1;\n return;\n }\n reds.push(data[i]);\n greens.push(data[i + 1]);\n blues.push(data[i + 2]);\n };\n\n for (let x = 0; x < width; x += 1) {\n sample(x, 0);\n sample(x, height - 1);\n }\n for (let y = 1; y < height - 1; y += 1) {\n sample(0, y);\n sample(width - 1, y);\n }\n\n if (total === 0) {\n return NO_BACKDROP;\n }\n\n // Mostly-transparent border → a cutout PNG. Let the page background show.\n if (transparentCount / total >= TRANSPARENT_BORDER_FRACTION) {\n return { hasBackdrop: true, backgroundColor: 'transparent' };\n }\n\n const opaque = reds.length;\n if (opaque === 0) {\n return NO_BACKDROP;\n }\n\n const mr = median(reds);\n const mg = median(greens);\n const mb = median(blues);\n\n let agree = 0;\n for (let k = 0; k < opaque; k += 1) {\n if (\n Math.abs(reds[k] - mr) <= COLOR_TOLERANCE &&\n Math.abs(greens[k] - mg) <= COLOR_TOLERANCE &&\n Math.abs(blues[k] - mb) <= COLOR_TOLERANCE\n ) {\n agree += 1;\n }\n }\n\n if (agree / opaque >= BACKDROP_AGREEMENT) {\n return { hasBackdrop: true, backgroundColor: `rgb(${mr}, ${mg}, ${mb})` };\n }\n\n return NO_BACKDROP;\n}\n\n/**\n * Bounding box of the non-background content in a probed image, in sample-grid\n * coordinates (0..size-1), plus the sampled background colour. Used by callers\n * that need *where the subject sits* (e.g. crop-safety in a fixed-aspect slot),\n * which is a different question from `analyzeBackdrop`'s \"is there a backdrop\".\n */\nexport interface ContentBBox {\n top: number;\n bottom: number;\n left: number;\n right: number;\n /** Background colour, sampled from the top-left pixel. */\n background: { r: number; g: number; b: number };\n}\n\n/**\n * Find the bounding box of the subject by treating the top-left pixel as the\n * background colour and flagging every pixel that differs from it (or is\n * transparent). Pure and synchronous. Returns null for an all-background frame.\n *\n * `backgroundTolerance`/`alphaThreshold` default to the values tuned for\n * uniform studio catalog shots; callers can override.\n */\nexport function computeContentBBox(\n data: Uint8ClampedArray,\n size: number,\n options?: { backgroundTolerance?: number; alphaThreshold?: number },\n): ContentBBox | null {\n const tol = options?.backgroundTolerance ?? 12;\n const alpha = options?.alphaThreshold ?? 16;\n if (size < 1 || data.length < size * size * 4) {\n return null;\n }\n\n const br = data[0];\n const bg = data[1];\n const bb = data[2];\n const isBg = (i: number) =>\n data[i + 3] < alpha ||\n (Math.abs(data[i] - br) < tol &&\n Math.abs(data[i + 1] - bg) < tol &&\n Math.abs(data[i + 2] - bb) < tol);\n\n let top = size;\n let bottom = -1;\n let left = size;\n let right = -1;\n for (let y = 0; y < size; y += 1) {\n for (let x = 0; x < size; x += 1) {\n if (!isBg((y * size + x) * 4)) {\n top = Math.min(top, y);\n bottom = Math.max(bottom, y);\n left = Math.min(left, x);\n right = Math.max(right, x);\n }\n }\n }\n\n if (bottom < 0) {\n return null;\n }\n return { top, bottom, left, right, background: { r: br, g: bg, b: bb } };\n}\n\n/** Square sample grid for the probe — small enough to be cheap, big enough to\n * read edges and locate a subject. */\nconst PROBE_BOX = 64;\n\n/**\n * Build the URL of a tiny, aspect-preserving (`fit`) version of an image so we\n * can read its true edges. Wix media is downscaled by the CDN; other URLs are\n * probed at full size (still drawn onto a small canvas).\n */\nexport function buildProbeUrl(effectiveFilename: string): string {\n const normalized = normalizeImageUrl(effectiveFilename);\n if (!normalized.includes('static.wixstatic.com')) {\n return normalized;\n }\n const match = normalized.match(/media\\/([^/?]+)/);\n if (!match?.[1]) {\n return normalized;\n }\n return getResizedImageUrl(match[1], PROBE_BOX, PROBE_BOX, {\n quality: 70,\n imageMode: 'fit',\n });\n}\n\n/** RGBA pixels of a probed image, drawn onto a `size`×`size` grid, alongside the\n * source's true intrinsic dimensions (needed for aspect-aware crop math). */\nexport interface ImagePixels {\n data: Uint8ClampedArray;\n size: number;\n naturalWidth: number;\n naturalHeight: number;\n}\n\n/**\n * Shared probe plumbing: load a (downscaled, for Wix) version of the image\n * cross-origin, draw it onto a `size`×`size` canvas, and return its pixels plus\n * intrinsic dimensions. Resolves to `null` on any failure — missing DOM, a\n * data/blob source, a load error, or a tainted canvas (if CORS ever regresses)\n * — so every caller can fall back to its own default. Browser-only.\n *\n * Both the backdrop classifier (`loadBackdropAnalysis`) and bounding-box callers\n * build on this so the canvas/CORS/taint handling lives in exactly one place.\n */\nexport function loadImagePixels(\n effectiveFilename: string,\n options?: { size?: number },\n signal?: { aborted: boolean },\n): Promise<ImagePixels | null> {\n const size = options?.size ?? PROBE_BOX;\n return new Promise((resolve) => {\n if (typeof document === 'undefined' || !effectiveFilename) {\n resolve(null);\n return;\n }\n const probeUrl = buildProbeUrl(effectiveFilename);\n if (probeUrl.startsWith('data:') || probeUrl.startsWith('blob:')) {\n resolve(null);\n return;\n }\n\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.decoding = 'async';\n\n img.onload = () => {\n if (signal?.aborted) {\n resolve(null);\n return;\n }\n try {\n const canvas = document.createElement('canvas');\n canvas.width = size;\n canvas.height = size;\n const ctx = canvas.getContext('2d', { willReadFrequently: true });\n if (!ctx) {\n resolve(null);\n return;\n }\n ctx.drawImage(img, 0, 0, size, size);\n const { data } = ctx.getImageData(0, 0, size, size);\n resolve({\n data,\n size,\n naturalWidth: img.naturalWidth || size,\n naturalHeight: img.naturalHeight || size,\n });\n } catch {\n // Tainted canvas or any read failure → let the caller default.\n resolve(null);\n }\n };\n img.onerror = () => resolve(null);\n img.src = probeUrl;\n });\n}\n\n/**\n * Load a probe image and analyze its backdrop. Resolves to `NO_BACKDROP` on any\n * failure so callers fall back to their default fit. Browser-only.\n */\nexport function loadBackdropAnalysis(\n effectiveFilename: string,\n signal?: { aborted: boolean },\n): Promise<BackdropAnalysis> {\n return loadImagePixels(effectiveFilename, { size: PROBE_BOX }, signal).then(\n (pixels) =>\n pixels\n ? analyzeBackdrop(pixels.data, pixels.size, pixels.size)\n : NO_BACKDROP,\n );\n}\n"],"mappings":"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","ignoreList":[]}
@@ -3,21 +3,32 @@
3
3
  exports.__esModule = true;
4
4
  exports.extractIntentFromMarkdown = extractIntentFromMarkdown;
5
5
  exports.getIntentFromMarkdown = getIntentFromMarkdown;
6
- var _Intent = require("../registry/Intent");
7
6
  var _htmlCommentParser = require("./htmlCommentParser");
8
- const INTENT_VALUES = new Set(Object.values(_Intent.Intent));
9
- function normalizeIntent(intent) {
7
+ /**
8
+ * Controls how a raw intent string is validated / defaulted during extraction.
9
+ * Core holds NO intent list of its own — the taxonomy is client-specific. A
10
+ * caller supplies the client's list (typically `registry.getKnownIntents()`) to
11
+ * make the FE honour that client's intents; with no list, any emitted intent is
12
+ * accepted verbatim (resolution then falls back to the section default when the
13
+ * intent has no registered variant).
14
+ */
15
+
16
+ function normalizeIntent(intent, validIntents) {
10
17
  if (!intent) {
11
18
  return null;
12
19
  }
13
20
  const normalized = intent.trim().toUpperCase();
14
- return INTENT_VALUES.has(normalized) ? normalized : null;
21
+ if (!validIntents) {
22
+ return normalized;
23
+ }
24
+ const allowed = new Set(validIntents.map(value => value.toUpperCase()));
25
+ return allowed.has(normalized) ? normalized : null;
15
26
  }
16
27
  function normalizeState(state) {
17
28
  const normalized = state == null ? void 0 : state.trim().toLowerCase();
18
29
  return normalized === 'partial' ? 'partial' : 'full';
19
30
  }
20
- function extractIntentFromMarkdown(markdown) {
31
+ function extractIntentFromMarkdown(markdown, options) {
21
32
  if (!markdown) {
22
33
  return null;
23
34
  }
@@ -27,7 +38,7 @@ function extractIntentFromMarkdown(markdown) {
27
38
  }
28
39
  const comment = match[0];
29
40
  const metadata = (0, _htmlCommentParser.parseHtmlComment)(comment);
30
- const intent = normalizeIntent(metadata.intent);
41
+ const intent = normalizeIntent(metadata.intent, options == null ? void 0 : options.validIntents);
31
42
  if (!intent) {
32
43
  return null;
33
44
  }
@@ -38,9 +49,9 @@ function extractIntentFromMarkdown(markdown) {
38
49
  state: normalizeState(metadata.state)
39
50
  };
40
51
  }
41
- function getIntentFromMarkdown(markdown, defaultIntent = _Intent.Intent.DISCOVERY) {
42
- return extractIntentFromMarkdown(markdown) ?? {
43
- intent: defaultIntent,
52
+ function getIntentFromMarkdown(markdown, options) {
53
+ return extractIntentFromMarkdown(markdown, options) ?? {
54
+ intent: (options == null ? void 0 : options.defaultIntent) ?? '',
44
55
  state: 'full'
45
56
  };
46
57
  }
@@ -1 +1 @@
1
- {"version":3,"names":["_Intent","require","_htmlCommentParser","INTENT_VALUES","Set","Object","values","Intent","normalizeIntent","intent","normalized","trim","toUpperCase","has","normalizeState","state","toLowerCase","extractIntentFromMarkdown","markdown","match","comment","metadata","parseHtmlComment","stage","reason","getIntentFromMarkdown","defaultIntent","DISCOVERY"],"sources":["../../../src/utils/intentExtractor.ts"],"sourcesContent":["import { Intent } from '../registry/Intent';\nimport { parseHtmlComment } from './htmlCommentParser';\n\nexport type ResponseState = 'full' | 'partial';\n\nexport interface IntentInfo {\n intent: Intent;\n stage?: string;\n reason?: string;\n state?: ResponseState;\n}\n\nconst INTENT_VALUES = new Set<Intent>(Object.values(Intent) as Intent[]);\n\nfunction normalizeIntent(intent?: string): Intent | null {\n if (!intent) {\n return null;\n }\n const normalized = intent.trim().toUpperCase() as Intent;\n return INTENT_VALUES.has(normalized) ? normalized : null;\n}\n\nfunction normalizeState(state?: string): ResponseState {\n const normalized = state?.trim().toLowerCase();\n return normalized === 'partial' ? 'partial' : 'full';\n}\n\nexport function extractIntentFromMarkdown(markdown: string): IntentInfo | null {\n if (!markdown) {\n return null;\n }\n\n const match = markdown.match(/^\\s*<!--[\\s\\S]*?-->/);\n if (!match) {\n return null;\n }\n\n const comment = match[0];\n const metadata = parseHtmlComment(comment);\n const intent = normalizeIntent(metadata.intent);\n\n if (!intent) {\n return null;\n }\n\n return {\n intent,\n stage: metadata.stage,\n reason: metadata.reason,\n state: normalizeState(metadata.state),\n };\n}\n\nexport function getIntentFromMarkdown(\n markdown: string,\n defaultIntent: Intent = Intent.DISCOVERY,\n): IntentInfo {\n return (\n extractIntentFromMarkdown(markdown) ?? {\n intent: defaultIntent,\n state: 'full',\n }\n );\n}\n"],"mappings":";;;;;AAAA,IAAAA,OAAA,GAAAC,OAAA;AACA,IAAAC,kBAAA,GAAAD,OAAA;AAWA,MAAME,aAAa,GAAG,IAAIC,GAAG,CAASC,MAAM,CAACC,MAAM,CAACC,cAAM,CAAa,CAAC;AAExE,SAASC,eAAeA,CAACC,MAAe,EAAiB;EACvD,IAAI,CAACA,MAAM,EAAE;IACX,OAAO,IAAI;EACb;EACA,MAAMC,UAAU,GAAGD,MAAM,CAACE,IAAI,CAAC,CAAC,CAACC,WAAW,CAAC,CAAW;EACxD,OAAOT,aAAa,CAACU,GAAG,CAACH,UAAU,CAAC,GAAGA,UAAU,GAAG,IAAI;AAC1D;AAEA,SAASI,cAAcA,CAACC,KAAc,EAAiB;EACrD,MAAML,UAAU,GAAGK,KAAK,oBAALA,KAAK,CAAEJ,IAAI,CAAC,CAAC,CAACK,WAAW,CAAC,CAAC;EAC9C,OAAON,UAAU,KAAK,SAAS,GAAG,SAAS,GAAG,MAAM;AACtD;AAEO,SAASO,yBAAyBA,CAACC,QAAgB,EAAqB;EAC7E,IAAI,CAACA,QAAQ,EAAE;IACb,OAAO,IAAI;EACb;EAEA,MAAMC,KAAK,GAAGD,QAAQ,CAACC,KAAK,CAAC,qBAAqB,CAAC;EACnD,IAAI,CAACA,KAAK,EAAE;IACV,OAAO,IAAI;EACb;EAEA,MAAMC,OAAO,GAAGD,KAAK,CAAC,CAAC,CAAC;EACxB,MAAME,QAAQ,GAAG,IAAAC,mCAAgB,EAACF,OAAO,CAAC;EAC1C,MAAMX,MAAM,GAAGD,eAAe,CAACa,QAAQ,CAACZ,MAAM,CAAC;EAE/C,IAAI,CAACA,MAAM,EAAE;IACX,OAAO,IAAI;EACb;EAEA,OAAO;IACLA,MAAM;IACNc,KAAK,EAAEF,QAAQ,CAACE,KAAK;IACrBC,MAAM,EAAEH,QAAQ,CAACG,MAAM;IACvBT,KAAK,EAAED,cAAc,CAACO,QAAQ,CAACN,KAAK;EACtC,CAAC;AACH;AAEO,SAASU,qBAAqBA,CACnCP,QAAgB,EAChBQ,aAAqB,GAAGnB,cAAM,CAACoB,SAAS,EAC5B;EACZ,OACEV,yBAAyB,CAACC,QAAQ,CAAC,IAAI;IACrCT,MAAM,EAAEiB,aAAa;IACrBX,KAAK,EAAE;EACT,CAAC;AAEL","ignoreList":[]}
1
+ {"version":3,"names":["_htmlCommentParser","require","normalizeIntent","intent","validIntents","normalized","trim","toUpperCase","allowed","Set","map","value","has","normalizeState","state","toLowerCase","extractIntentFromMarkdown","markdown","options","match","comment","metadata","parseHtmlComment","stage","reason","getIntentFromMarkdown","defaultIntent"],"sources":["../../../src/utils/intentExtractor.ts"],"sourcesContent":["import { parseHtmlComment } from './htmlCommentParser';\n\nexport type ResponseState = 'full' | 'partial';\n\nexport interface IntentInfo {\n intent: string;\n stage?: string;\n reason?: string;\n state?: ResponseState;\n}\n\n/**\n * Controls how a raw intent string is validated / defaulted during extraction.\n * Core holds NO intent list of its own — the taxonomy is client-specific. A\n * caller supplies the client's list (typically `registry.getKnownIntents()`) to\n * make the FE honour that client's intents; with no list, any emitted intent is\n * accepted verbatim (resolution then falls back to the section default when the\n * intent has no registered variant).\n */\nexport interface IntentExtractionOptions {\n /**\n * The intents to accept (matched case-insensitively). An emitted intent\n * outside this list is treated as absent. When omitted, no filtering is\n * applied and the emitted intent passes through as-is.\n */\n validIntents?: readonly string[];\n /** Intent assumed when the markdown carries no intent annotation. Defaults to ''. */\n defaultIntent?: string;\n}\n\nfunction normalizeIntent(\n intent: string | undefined,\n validIntents?: readonly string[],\n): string | null {\n if (!intent) {\n return null;\n }\n const normalized = intent.trim().toUpperCase();\n if (!validIntents) {\n return normalized;\n }\n const allowed = new Set(validIntents.map((value) => value.toUpperCase()));\n return allowed.has(normalized) ? normalized : null;\n}\n\nfunction normalizeState(state?: string): ResponseState {\n const normalized = state?.trim().toLowerCase();\n return normalized === 'partial' ? 'partial' : 'full';\n}\n\nexport function extractIntentFromMarkdown(\n markdown: string,\n options?: IntentExtractionOptions,\n): IntentInfo | null {\n if (!markdown) {\n return null;\n }\n\n const match = markdown.match(/^\\s*<!--[\\s\\S]*?-->/);\n if (!match) {\n return null;\n }\n\n const comment = match[0];\n const metadata = parseHtmlComment(comment);\n const intent = normalizeIntent(metadata.intent, options?.validIntents);\n\n if (!intent) {\n return null;\n }\n\n return {\n intent,\n stage: metadata.stage,\n reason: metadata.reason,\n state: normalizeState(metadata.state),\n };\n}\n\nexport function getIntentFromMarkdown(\n markdown: string,\n options?: IntentExtractionOptions,\n): IntentInfo {\n return (\n extractIntentFromMarkdown(markdown, options) ?? {\n intent: options?.defaultIntent ?? '',\n state: 'full',\n }\n );\n}\n"],"mappings":";;;;;AAAA,IAAAA,kBAAA,GAAAC,OAAA;AAWA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAYA,SAASC,eAAeA,CACtBC,MAA0B,EAC1BC,YAAgC,EACjB;EACf,IAAI,CAACD,MAAM,EAAE;IACX,OAAO,IAAI;EACb;EACA,MAAME,UAAU,GAAGF,MAAM,CAACG,IAAI,CAAC,CAAC,CAACC,WAAW,CAAC,CAAC;EAC9C,IAAI,CAACH,YAAY,EAAE;IACjB,OAAOC,UAAU;EACnB;EACA,MAAMG,OAAO,GAAG,IAAIC,GAAG,CAACL,YAAY,CAACM,GAAG,CAAEC,KAAK,IAAKA,KAAK,CAACJ,WAAW,CAAC,CAAC,CAAC,CAAC;EACzE,OAAOC,OAAO,CAACI,GAAG,CAACP,UAAU,CAAC,GAAGA,UAAU,GAAG,IAAI;AACpD;AAEA,SAASQ,cAAcA,CAACC,KAAc,EAAiB;EACrD,MAAMT,UAAU,GAAGS,KAAK,oBAALA,KAAK,CAAER,IAAI,CAAC,CAAC,CAACS,WAAW,CAAC,CAAC;EAC9C,OAAOV,UAAU,KAAK,SAAS,GAAG,SAAS,GAAG,MAAM;AACtD;AAEO,SAASW,yBAAyBA,CACvCC,QAAgB,EAChBC,OAAiC,EACd;EACnB,IAAI,CAACD,QAAQ,EAAE;IACb,OAAO,IAAI;EACb;EAEA,MAAME,KAAK,GAAGF,QAAQ,CAACE,KAAK,CAAC,qBAAqB,CAAC;EACnD,IAAI,CAACA,KAAK,EAAE;IACV,OAAO,IAAI;EACb;EAEA,MAAMC,OAAO,GAAGD,KAAK,CAAC,CAAC,CAAC;EACxB,MAAME,QAAQ,GAAG,IAAAC,mCAAgB,EAACF,OAAO,CAAC;EAC1C,MAAMjB,MAAM,GAAGD,eAAe,CAACmB,QAAQ,CAAClB,MAAM,EAAEe,OAAO,oBAAPA,OAAO,CAAEd,YAAY,CAAC;EAEtE,IAAI,CAACD,MAAM,EAAE;IACX,OAAO,IAAI;EACb;EAEA,OAAO;IACLA,MAAM;IACNoB,KAAK,EAAEF,QAAQ,CAACE,KAAK;IACrBC,MAAM,EAAEH,QAAQ,CAACG,MAAM;IACvBV,KAAK,EAAED,cAAc,CAACQ,QAAQ,CAACP,KAAK;EACtC,CAAC;AACH;AAEO,SAASW,qBAAqBA,CACnCR,QAAgB,EAChBC,OAAiC,EACrB;EACZ,OACEF,yBAAyB,CAACC,QAAQ,EAAEC,OAAO,CAAC,IAAI;IAC9Cf,MAAM,EAAE,CAAAe,OAAO,oBAAPA,OAAO,CAAEQ,aAAa,KAAI,EAAE;IACpCZ,KAAK,EAAE;EACT,CAAC;AAEL","ignoreList":[]}
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.PRODUCT_BACK_SESSION_KEY = void 0;
5
+ exports.readProductBackHandoff = readProductBackHandoff;
6
+ exports.writeProductBackHandoff = writeProductBackHandoff;
7
+ /**
8
+ * Product-page → full-app "back" context hand-off (DL #114).
9
+ *
10
+ * A PDP placement writes the product context to `sessionStorage` immediately
11
+ * before redirecting the visitor to the full Web5 page; the app reads it once
12
+ * on load and renders a back-to-product badge in the conversation header.
13
+ *
14
+ * The context never enters the URL, so there is no shareable/tamperable link
15
+ * that could spoof the header (the security driver in DL #114). The hand-off
16
+ * relies on the redirect being a same-origin, same-tab top-level navigation
17
+ * (`searchRedirectUrl`, e.g. `/ai`), which is what keeps `sessionStorage`
18
+ * readable on the other side. Cross-origin → nothing is read → no badge.
19
+ */
20
+ const PRODUCT_BACK_SESSION_KEY = exports.PRODUCT_BACK_SESSION_KEY = 'web5:productBack';
21
+ const isBrowser = () => typeof window !== 'undefined' && typeof window.sessionStorage !== 'undefined';
22
+ function readMeta(property) {
23
+ var _el$getAttribute;
24
+ if (typeof document === 'undefined') {
25
+ return undefined;
26
+ }
27
+ const el = document.querySelector(`meta[property="${property}"], meta[name="${property}"]`);
28
+ const content = el == null || (_el$getAttribute = el.getAttribute('content')) == null ? void 0 : _el$getAttribute.trim();
29
+ return content || undefined;
30
+ }
31
+
32
+ /**
33
+ * Resolve a meta-tag image URL to an absolute `https:` URL. Handles the
34
+ * protocol-relative (`//cdn…`) and root-relative (`/img…`) forms some stores
35
+ * (e.g. Shopify themes) emit, which the reader's `https:`-only check would
36
+ * otherwise drop. Returns undefined for anything that can't be made absolute.
37
+ */
38
+ function readImageMeta() {
39
+ const raw = readMeta('og:image') ?? readMeta('twitter:image');
40
+ if (!raw || typeof window === 'undefined') {
41
+ return raw;
42
+ }
43
+ let url = raw;
44
+ if (url.startsWith('//')) {
45
+ url = `https:${url}`;
46
+ } else if (url.startsWith('/')) {
47
+ url = `${window.location.origin}${url}`;
48
+ }
49
+ return url;
50
+ }
51
+
52
+ /**
53
+ * Write the back-context just before navigating away from a product page.
54
+ * Values default to the page's own first-party Open Graph tags and URL, so a
55
+ * standard PDP needs no extra wiring; pass `overrides` to supply exact data.
56
+ * No-op outside the browser or when no product title can be determined.
57
+ */
58
+ function writeProductBackHandoff(overrides) {
59
+ if (!isBrowser()) {
60
+ return;
61
+ }
62
+ const title = (overrides == null ? void 0 : overrides.title) ?? readMeta('og:title');
63
+ if (!title) {
64
+ // Nothing meaningful to show — don't write a badge-less entry.
65
+ return;
66
+ }
67
+ const handoff = {
68
+ title,
69
+ imageUrl: (overrides == null ? void 0 : overrides.imageUrl) ?? readImageMeta(),
70
+ backUrl: (overrides == null ? void 0 : overrides.backUrl) ?? window.location.href,
71
+ ts: Date.now()
72
+ };
73
+ try {
74
+ window.sessionStorage.setItem(PRODUCT_BACK_SESSION_KEY, JSON.stringify(handoff));
75
+ } catch {
76
+ // sessionStorage unavailable / quota exceeded — the badge simply won't show.
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Read and immediately delete the back-context (consume-once: survives exactly
82
+ * one page load, so a reload or bfcache restore won't replay it). Returns null
83
+ * when absent or unparseable. Callers must still validate the values before use
84
+ * (freshness window, allowed-domain check on `backUrl`, `https:` on `imageUrl`).
85
+ */
86
+ function readProductBackHandoff() {
87
+ if (!isBrowser()) {
88
+ return null;
89
+ }
90
+ let raw = null;
91
+ try {
92
+ raw = window.sessionStorage.getItem(PRODUCT_BACK_SESSION_KEY);
93
+ window.sessionStorage.removeItem(PRODUCT_BACK_SESSION_KEY);
94
+ } catch {
95
+ return null;
96
+ }
97
+ if (!raw) {
98
+ return null;
99
+ }
100
+ try {
101
+ return JSON.parse(raw);
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+ //# sourceMappingURL=productBackHandoff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["PRODUCT_BACK_SESSION_KEY","exports","isBrowser","window","sessionStorage","readMeta","property","_el$getAttribute","document","undefined","el","querySelector","content","getAttribute","trim","readImageMeta","raw","url","startsWith","location","origin","writeProductBackHandoff","overrides","title","handoff","imageUrl","backUrl","href","ts","Date","now","setItem","JSON","stringify","readProductBackHandoff","getItem","removeItem","parse"],"sources":["../../../src/utils/productBackHandoff.ts"],"sourcesContent":["/**\n * Product-page → full-app \"back\" context hand-off (DL #114).\n *\n * A PDP placement writes the product context to `sessionStorage` immediately\n * before redirecting the visitor to the full Web5 page; the app reads it once\n * on load and renders a back-to-product badge in the conversation header.\n *\n * The context never enters the URL, so there is no shareable/tamperable link\n * that could spoof the header (the security driver in DL #114). The hand-off\n * relies on the redirect being a same-origin, same-tab top-level navigation\n * (`searchRedirectUrl`, e.g. `/ai`), which is what keeps `sessionStorage`\n * readable on the other side. Cross-origin → nothing is read → no badge.\n */\nexport const PRODUCT_BACK_SESSION_KEY = 'web5:productBack';\n\nexport interface ProductBackHandoff {\n /** Product display name (first-party; e.g. the PDP `og:title`). */\n title?: string;\n /** Product thumbnail URL (first-party; e.g. the PDP `og:image`). */\n imageUrl?: string;\n /** The product page to return to — the PDP's own URL. */\n backUrl?: string;\n /** Epoch ms when written; the reader drops stale entries. */\n ts?: number;\n}\n\nconst isBrowser = (): boolean =>\n typeof window !== 'undefined' && typeof window.sessionStorage !== 'undefined';\n\nfunction readMeta(property: string): string | undefined {\n if (typeof document === 'undefined') {\n return undefined;\n }\n const el = document.querySelector(\n `meta[property=\"${property}\"], meta[name=\"${property}\"]`,\n );\n const content = el?.getAttribute('content')?.trim();\n return content || undefined;\n}\n\n/**\n * Resolve a meta-tag image URL to an absolute `https:` URL. Handles the\n * protocol-relative (`//cdn…`) and root-relative (`/img…`) forms some stores\n * (e.g. Shopify themes) emit, which the reader's `https:`-only check would\n * otherwise drop. Returns undefined for anything that can't be made absolute.\n */\nfunction readImageMeta(): string | undefined {\n const raw = readMeta('og:image') ?? readMeta('twitter:image');\n if (!raw || typeof window === 'undefined') {\n return raw;\n }\n let url = raw;\n if (url.startsWith('//')) {\n url = `https:${url}`;\n } else if (url.startsWith('/')) {\n url = `${window.location.origin}${url}`;\n }\n return url;\n}\n\n/**\n * Write the back-context just before navigating away from a product page.\n * Values default to the page's own first-party Open Graph tags and URL, so a\n * standard PDP needs no extra wiring; pass `overrides` to supply exact data.\n * No-op outside the browser or when no product title can be determined.\n */\nexport function writeProductBackHandoff(overrides?: {\n title?: string;\n imageUrl?: string;\n backUrl?: string;\n}): void {\n if (!isBrowser()) {\n return;\n }\n const title = overrides?.title ?? readMeta('og:title');\n if (!title) {\n // Nothing meaningful to show — don't write a badge-less entry.\n return;\n }\n const handoff: ProductBackHandoff = {\n title,\n imageUrl: overrides?.imageUrl ?? readImageMeta(),\n backUrl: overrides?.backUrl ?? window.location.href,\n ts: Date.now(),\n };\n try {\n window.sessionStorage.setItem(\n PRODUCT_BACK_SESSION_KEY,\n JSON.stringify(handoff),\n );\n } catch {\n // sessionStorage unavailable / quota exceeded — the badge simply won't show.\n }\n}\n\n/**\n * Read and immediately delete the back-context (consume-once: survives exactly\n * one page load, so a reload or bfcache restore won't replay it). Returns null\n * when absent or unparseable. Callers must still validate the values before use\n * (freshness window, allowed-domain check on `backUrl`, `https:` on `imageUrl`).\n */\nexport function readProductBackHandoff(): ProductBackHandoff | null {\n if (!isBrowser()) {\n return null;\n }\n let raw: string | null = null;\n try {\n raw = window.sessionStorage.getItem(PRODUCT_BACK_SESSION_KEY);\n window.sessionStorage.removeItem(PRODUCT_BACK_SESSION_KEY);\n } catch {\n return null;\n }\n if (!raw) {\n return null;\n }\n try {\n return JSON.parse(raw) as ProductBackHandoff;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.shouldRefreshPrompts = exports.getRefreshPromptsExpiry = exports.enableRefreshPrompts = exports.disableRefreshPrompts = exports.REFRESH_PROMPTS_WINDOW_MS = exports.REFRESH_PROMPTS_UNTIL_KEY = void 0;
5
+ /**
6
+ * Refresh-prompts flag (debug-only).
7
+ *
8
+ * When active, the Turn RPC request (`POST /v1/turn`, DL #110) carries
9
+ * `additionalData.refresh_prompts: 'true'`, asking the BE to re-pull prompts
10
+ * instead of serving cached ones.
11
+ *
12
+ * Keeping it on slows the app, so the flag self-expires: rather than a bare
13
+ * boolean we persist an expiry timestamp in
14
+ * `localStorage["web5_refresh_prompts_until"]`. `shouldRefreshPrompts()` is
15
+ * true only while now < expiry, so the cap holds even across reloads or if a
16
+ * UI timer never fires. Lives in web5-core so the request builder and any
17
+ * surface honor the same toggle.
18
+ */
19
+
20
+ const REFRESH_PROMPTS_UNTIL_KEY = exports.REFRESH_PROMPTS_UNTIL_KEY = 'web5_refresh_prompts_until';
21
+
22
+ /** How long the flag stays active once enabled (2 minutes). */
23
+ const REFRESH_PROMPTS_WINDOW_MS = exports.REFRESH_PROMPTS_WINDOW_MS = 2 * 60 * 1000;
24
+
25
+ /**
26
+ * Read the expiry timestamp (ms epoch) from localStorage. Returns 0 when
27
+ * unset, invalid, or storage is unavailable.
28
+ */
29
+ const getRefreshPromptsExpiry = () => {
30
+ try {
31
+ const raw = localStorage.getItem(REFRESH_PROMPTS_UNTIL_KEY);
32
+ if (!raw) {
33
+ return 0;
34
+ }
35
+ const ts = Number(raw);
36
+ return Number.isFinite(ts) ? ts : 0;
37
+ } catch {
38
+ // localStorage can throw in privacy mode / non-DOM environments.
39
+ return 0;
40
+ }
41
+ };
42
+
43
+ /** Whether the refresh-prompts flag is currently active (within its window). */
44
+ exports.getRefreshPromptsExpiry = getRefreshPromptsExpiry;
45
+ const shouldRefreshPrompts = () => getRefreshPromptsExpiry() > Date.now();
46
+
47
+ /**
48
+ * Activate the flag for `windowMs` from now and return the new expiry
49
+ * timestamp. Re-enabling restarts the window.
50
+ */
51
+ exports.shouldRefreshPrompts = shouldRefreshPrompts;
52
+ const enableRefreshPrompts = (windowMs = REFRESH_PROMPTS_WINDOW_MS) => {
53
+ const until = Date.now() + windowMs;
54
+ try {
55
+ localStorage.setItem(REFRESH_PROMPTS_UNTIL_KEY, String(until));
56
+ } catch {
57
+ // Ignore — flag simply won't persist.
58
+ }
59
+ return until;
60
+ };
61
+
62
+ /** Deactivate the flag immediately (proactive off). */
63
+ exports.enableRefreshPrompts = enableRefreshPrompts;
64
+ const disableRefreshPrompts = () => {
65
+ try {
66
+ localStorage.removeItem(REFRESH_PROMPTS_UNTIL_KEY);
67
+ } catch {
68
+ // Ignore.
69
+ }
70
+ };
71
+ exports.disableRefreshPrompts = disableRefreshPrompts;
72
+ //# sourceMappingURL=refreshPrompts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["REFRESH_PROMPTS_UNTIL_KEY","exports","REFRESH_PROMPTS_WINDOW_MS","getRefreshPromptsExpiry","raw","localStorage","getItem","ts","Number","isFinite","shouldRefreshPrompts","Date","now","enableRefreshPrompts","windowMs","until","setItem","String","disableRefreshPrompts","removeItem"],"sources":["../../../src/utils/refreshPrompts.ts"],"sourcesContent":["/**\n * Refresh-prompts flag (debug-only).\n *\n * When active, the Turn RPC request (`POST /v1/turn`, DL #110) carries\n * `additionalData.refresh_prompts: 'true'`, asking the BE to re-pull prompts\n * instead of serving cached ones.\n *\n * Keeping it on slows the app, so the flag self-expires: rather than a bare\n * boolean we persist an expiry timestamp in\n * `localStorage[\"web5_refresh_prompts_until\"]`. `shouldRefreshPrompts()` is\n * true only while now < expiry, so the cap holds even across reloads or if a\n * UI timer never fires. Lives in web5-core so the request builder and any\n * surface honor the same toggle.\n */\n\nexport const REFRESH_PROMPTS_UNTIL_KEY = 'web5_refresh_prompts_until';\n\n/** How long the flag stays active once enabled (2 minutes). */\nexport const REFRESH_PROMPTS_WINDOW_MS = 2 * 60 * 1000;\n\n/**\n * Read the expiry timestamp (ms epoch) from localStorage. Returns 0 when\n * unset, invalid, or storage is unavailable.\n */\nexport const getRefreshPromptsExpiry = (): number => {\n try {\n const raw = localStorage.getItem(REFRESH_PROMPTS_UNTIL_KEY);\n if (!raw) {\n return 0;\n }\n const ts = Number(raw);\n return Number.isFinite(ts) ? ts : 0;\n } catch {\n // localStorage can throw in privacy mode / non-DOM environments.\n return 0;\n }\n};\n\n/** Whether the refresh-prompts flag is currently active (within its window). */\nexport const shouldRefreshPrompts = (): boolean =>\n getRefreshPromptsExpiry() > Date.now();\n\n/**\n * Activate the flag for `windowMs` from now and return the new expiry\n * timestamp. Re-enabling restarts the window.\n */\nexport const enableRefreshPrompts = (\n windowMs: number = REFRESH_PROMPTS_WINDOW_MS,\n): number => {\n const until = Date.now() + windowMs;\n try {\n localStorage.setItem(REFRESH_PROMPTS_UNTIL_KEY, String(until));\n } catch {\n // Ignore — flag simply won't persist.\n }\n return until;\n};\n\n/** Deactivate the flag immediately (proactive off). */\nexport const disableRefreshPrompts = (): void => {\n try {\n localStorage.removeItem(REFRESH_PROMPTS_UNTIL_KEY);\n } catch {\n // Ignore.\n }\n};\n"],"mappings":";;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEO,MAAMA,yBAAyB,GAAAC,OAAA,CAAAD,yBAAA,GAAG,4BAA4B;;AAErE;AACO,MAAME,yBAAyB,GAAAD,OAAA,CAAAC,yBAAA,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI;;AAEtD;AACA;AACA;AACA;AACO,MAAMC,uBAAuB,GAAGA,CAAA,KAAc;EACnD,IAAI;IACF,MAAMC,GAAG,GAAGC,YAAY,CAACC,OAAO,CAACN,yBAAyB,CAAC;IAC3D,IAAI,CAACI,GAAG,EAAE;MACR,OAAO,CAAC;IACV;IACA,MAAMG,EAAE,GAAGC,MAAM,CAACJ,GAAG,CAAC;IACtB,OAAOI,MAAM,CAACC,QAAQ,CAACF,EAAE,CAAC,GAAGA,EAAE,GAAG,CAAC;EACrC,CAAC,CAAC,MAAM;IACN;IACA,OAAO,CAAC;EACV;AACF,CAAC;;AAED;AAAAN,OAAA,CAAAE,uBAAA,GAAAA,uBAAA;AACO,MAAMO,oBAAoB,GAAGA,CAAA,KAClCP,uBAAuB,CAAC,CAAC,GAAGQ,IAAI,CAACC,GAAG,CAAC,CAAC;;AAExC;AACA;AACA;AACA;AAHAX,OAAA,CAAAS,oBAAA,GAAAA,oBAAA;AAIO,MAAMG,oBAAoB,GAAGA,CAClCC,QAAgB,GAAGZ,yBAAyB,KACjC;EACX,MAAMa,KAAK,GAAGJ,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGE,QAAQ;EACnC,IAAI;IACFT,YAAY,CAACW,OAAO,CAAChB,yBAAyB,EAAEiB,MAAM,CAACF,KAAK,CAAC,CAAC;EAChE,CAAC,CAAC,MAAM;IACN;EAAA;EAEF,OAAOA,KAAK;AACd,CAAC;;AAED;AAAAd,OAAA,CAAAY,oBAAA,GAAAA,oBAAA;AACO,MAAMK,qBAAqB,GAAGA,CAAA,KAAY;EAC/C,IAAI;IACFb,YAAY,CAACc,UAAU,CAACnB,yBAAyB,CAAC;EACpD,CAAC,CAAC,MAAM;IACN;EAAA;AAEJ,CAAC;AAACC,OAAA,CAAAiB,qBAAA,GAAAA,qBAAA","ignoreList":[]}
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Wix-authenticated `fetch` for Web5 BE calls from a host (third-party) origin.
3
3
  *
4
- * `wixapis.com`'s `/wix-assistant-web5/v1/conversation` endpoint refuses
4
+ * `wixapis.com`'s `/wix-assistant-web5/v1/turn` endpoint refuses
5
5
  * cross-origin anonymous calls (HTTP 500) — the only auth path that survives
6
6
  * CORS from a host (e.g. Shopify) origin is `@wix/sdk`'s `OAuthStrategy`, which
7
7
  * supplies the `Authorization` header the BE allowlists.
8
8
  *
9
- * Shared by every embed bundle that talks to the conversation endpoint — the
9
+ * Shared by every embed bundle that talks to the turn endpoint — the
10
10
  * placement bundle and the input-widget chip generator — so they all attach
11
11
  * the same token. Token caching uses the same `localStorage` key format
12
12
  * (`web5_tokens_<clientId>`) as `web50-server-ui`'s token cache, so the loader,
@@ -1 +1 @@
1
- {"version":3,"names":["createClient","OAuthStrategy","TOKEN_CACHE_KEY_PREFIX","tokenKey","clientId","loadTokens","client","raw","localStorage","getItem","auth","setTokens","JSON","parse","removeItem","saveTokens","setItem","stringify","getTokens","createWixAuthFetch","Error","input","init","res","fetchWithAuth"],"sources":["../../../src/client/wixAuthFetch.ts"],"sourcesContent":["/**\n * Wix-authenticated `fetch` for Web5 BE calls from a host (third-party) origin.\n *\n * `wixapis.com`'s `/wix-assistant-web5/v1/conversation` endpoint refuses\n * cross-origin anonymous calls (HTTP 500) — the only auth path that survives\n * CORS from a host (e.g. Shopify) origin is `@wix/sdk`'s `OAuthStrategy`, which\n * supplies the `Authorization` header the BE allowlists.\n *\n * Shared by every embed bundle that talks to the conversation endpoint — the\n * placement bundle and the input-widget chip generator — so they all attach\n * the same token. Token caching uses the same `localStorage` key format\n * (`web5_tokens_<clientId>`) as `web50-server-ui`'s token cache, so the loader,\n * full app, placement, and input widget share one OAuth token instead of each\n * burning a `/oauth2/token` round-trip per page load.\n */\nimport { createClient, OAuthStrategy } from '@wix/sdk';\n\nconst TOKEN_CACHE_KEY_PREFIX = 'web5_tokens_';\n\ninterface SdkClient {\n fetchWithAuth: typeof fetch;\n auth: {\n setTokens: (tokens: unknown) => void;\n getTokens: () => unknown;\n };\n}\n\nfunction tokenKey(clientId: string): string {\n return `${TOKEN_CACHE_KEY_PREFIX}${clientId}`;\n}\n\nfunction loadTokens(client: SdkClient, clientId: string): void {\n try {\n const raw = localStorage.getItem(tokenKey(clientId));\n if (!raw) return;\n client.auth.setTokens(JSON.parse(raw));\n } catch {\n try {\n localStorage.removeItem(tokenKey(clientId));\n } catch {\n /* ignore */\n }\n }\n}\n\nfunction saveTokens(client: SdkClient, clientId: string): void {\n try {\n localStorage.setItem(\n tokenKey(clientId),\n JSON.stringify(client.auth.getTokens()),\n );\n } catch {\n // privacy mode / quota — in-memory token still works for this session\n }\n}\n\n/**\n * Build a `fetch`-shaped function pre-bound to the given Wix client id. The\n * returned value is drop-in compatible with any `fetchImpl` option\n * (`placementFetch`, `fetchContextualChips`, …).\n *\n * Each call loads cached tokens before the request, then saves the (possibly\n * refreshed) tokens after the response headers arrive — we deliberately do NOT\n * wait for the streaming body; the SDK's refresh runs alongside the stream and\n * the saved tokens benefit subsequent calls.\n */\nexport function createWixAuthFetch(clientId: string): typeof fetch {\n if (!clientId) {\n throw new Error('[web5-core] createWixAuthFetch: clientId is empty');\n }\n const client = createClient({\n auth: OAuthStrategy({ clientId }),\n }) as unknown as SdkClient;\n\n return (async (input: RequestInfo | URL, init?: RequestInit) => {\n loadTokens(client, clientId);\n const res = await client.fetchWithAuth(input, init);\n saveTokens(client, clientId);\n return res;\n }) as typeof fetch;\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASA,YAAY,EAAEC,aAAa,QAAQ,UAAU;AAEtD,MAAMC,sBAAsB,GAAG,cAAc;AAU7C,SAASC,QAAQA,CAACC,QAAgB,EAAU;EAC1C,OAAO,GAAGF,sBAAsB,GAAGE,QAAQ,EAAE;AAC/C;AAEA,SAASC,UAAUA,CAACC,MAAiB,EAAEF,QAAgB,EAAQ;EAC7D,IAAI;IACF,MAAMG,GAAG,GAAGC,YAAY,CAACC,OAAO,CAACN,QAAQ,CAACC,QAAQ,CAAC,CAAC;IACpD,IAAI,CAACG,GAAG,EAAE;IACVD,MAAM,CAACI,IAAI,CAACC,SAAS,CAACC,IAAI,CAACC,KAAK,CAACN,GAAG,CAAC,CAAC;EACxC,CAAC,CAAC,MAAM;IACN,IAAI;MACFC,YAAY,CAACM,UAAU,CAACX,QAAQ,CAACC,QAAQ,CAAC,CAAC;IAC7C,CAAC,CAAC,MAAM;MACN;IAAA;EAEJ;AACF;AAEA,SAASW,UAAUA,CAACT,MAAiB,EAAEF,QAAgB,EAAQ;EAC7D,IAAI;IACFI,YAAY,CAACQ,OAAO,CAClBb,QAAQ,CAACC,QAAQ,CAAC,EAClBQ,IAAI,CAACK,SAAS,CAACX,MAAM,CAACI,IAAI,CAACQ,SAAS,CAAC,CAAC,CACxC,CAAC;EACH,CAAC,CAAC,MAAM;IACN;EAAA;AAEJ;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,kBAAkBA,CAACf,QAAgB,EAAgB;EACjE,IAAI,CAACA,QAAQ,EAAE;IACb,MAAM,IAAIgB,KAAK,CAAC,mDAAmD,CAAC;EACtE;EACA,MAAMd,MAAM,GAAGN,YAAY,CAAC;IAC1BU,IAAI,EAAET,aAAa,CAAC;MAAEG;IAAS,CAAC;EAClC,CAAC,CAAyB;EAE1B,OAAQ,OAAOiB,KAAwB,EAAEC,IAAkB,KAAK;IAC9DjB,UAAU,CAACC,MAAM,EAAEF,QAAQ,CAAC;IAC5B,MAAMmB,GAAG,GAAG,MAAMjB,MAAM,CAACkB,aAAa,CAACH,KAAK,EAAEC,IAAI,CAAC;IACnDP,UAAU,CAACT,MAAM,EAAEF,QAAQ,CAAC;IAC5B,OAAOmB,GAAG;EACZ,CAAC;AACH","ignoreList":[]}
1
+ {"version":3,"names":["createClient","OAuthStrategy","TOKEN_CACHE_KEY_PREFIX","tokenKey","clientId","loadTokens","client","raw","localStorage","getItem","auth","setTokens","JSON","parse","removeItem","saveTokens","setItem","stringify","getTokens","createWixAuthFetch","Error","input","init","res","fetchWithAuth"],"sources":["../../../src/client/wixAuthFetch.ts"],"sourcesContent":["/**\n * Wix-authenticated `fetch` for Web5 BE calls from a host (third-party) origin.\n *\n * `wixapis.com`'s `/wix-assistant-web5/v1/turn` endpoint refuses\n * cross-origin anonymous calls (HTTP 500) — the only auth path that survives\n * CORS from a host (e.g. Shopify) origin is `@wix/sdk`'s `OAuthStrategy`, which\n * supplies the `Authorization` header the BE allowlists.\n *\n * Shared by every embed bundle that talks to the turn endpoint — the\n * placement bundle and the input-widget chip generator — so they all attach\n * the same token. Token caching uses the same `localStorage` key format\n * (`web5_tokens_<clientId>`) as `web50-server-ui`'s token cache, so the loader,\n * full app, placement, and input widget share one OAuth token instead of each\n * burning a `/oauth2/token` round-trip per page load.\n */\nimport { createClient, OAuthStrategy } from '@wix/sdk';\n\nconst TOKEN_CACHE_KEY_PREFIX = 'web5_tokens_';\n\ninterface SdkClient {\n fetchWithAuth: typeof fetch;\n auth: {\n setTokens: (tokens: unknown) => void;\n getTokens: () => unknown;\n };\n}\n\nfunction tokenKey(clientId: string): string {\n return `${TOKEN_CACHE_KEY_PREFIX}${clientId}`;\n}\n\nfunction loadTokens(client: SdkClient, clientId: string): void {\n try {\n const raw = localStorage.getItem(tokenKey(clientId));\n if (!raw) return;\n client.auth.setTokens(JSON.parse(raw));\n } catch {\n try {\n localStorage.removeItem(tokenKey(clientId));\n } catch {\n /* ignore */\n }\n }\n}\n\nfunction saveTokens(client: SdkClient, clientId: string): void {\n try {\n localStorage.setItem(\n tokenKey(clientId),\n JSON.stringify(client.auth.getTokens()),\n );\n } catch {\n // privacy mode / quota — in-memory token still works for this session\n }\n}\n\n/**\n * Build a `fetch`-shaped function pre-bound to the given Wix client id. The\n * returned value is drop-in compatible with any `fetchImpl` option\n * (`placementFetch`, `fetchContextualChips`, …).\n *\n * Each call loads cached tokens before the request, then saves the (possibly\n * refreshed) tokens after the response headers arrive — we deliberately do NOT\n * wait for the streaming body; the SDK's refresh runs alongside the stream and\n * the saved tokens benefit subsequent calls.\n */\nexport function createWixAuthFetch(clientId: string): typeof fetch {\n if (!clientId) {\n throw new Error('[web5-core] createWixAuthFetch: clientId is empty');\n }\n const client = createClient({\n auth: OAuthStrategy({ clientId }),\n }) as unknown as SdkClient;\n\n return (async (input: RequestInfo | URL, init?: RequestInit) => {\n loadTokens(client, clientId);\n const res = await client.fetchWithAuth(input, init);\n saveTokens(client, clientId);\n return res;\n }) as typeof fetch;\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASA,YAAY,EAAEC,aAAa,QAAQ,UAAU;AAEtD,MAAMC,sBAAsB,GAAG,cAAc;AAU7C,SAASC,QAAQA,CAACC,QAAgB,EAAU;EAC1C,OAAO,GAAGF,sBAAsB,GAAGE,QAAQ,EAAE;AAC/C;AAEA,SAASC,UAAUA,CAACC,MAAiB,EAAEF,QAAgB,EAAQ;EAC7D,IAAI;IACF,MAAMG,GAAG,GAAGC,YAAY,CAACC,OAAO,CAACN,QAAQ,CAACC,QAAQ,CAAC,CAAC;IACpD,IAAI,CAACG,GAAG,EAAE;IACVD,MAAM,CAACI,IAAI,CAACC,SAAS,CAACC,IAAI,CAACC,KAAK,CAACN,GAAG,CAAC,CAAC;EACxC,CAAC,CAAC,MAAM;IACN,IAAI;MACFC,YAAY,CAACM,UAAU,CAACX,QAAQ,CAACC,QAAQ,CAAC,CAAC;IAC7C,CAAC,CAAC,MAAM;MACN;IAAA;EAEJ;AACF;AAEA,SAASW,UAAUA,CAACT,MAAiB,EAAEF,QAAgB,EAAQ;EAC7D,IAAI;IACFI,YAAY,CAACQ,OAAO,CAClBb,QAAQ,CAACC,QAAQ,CAAC,EAClBQ,IAAI,CAACK,SAAS,CAACX,MAAM,CAACI,IAAI,CAACQ,SAAS,CAAC,CAAC,CACxC,CAAC;EACH,CAAC,CAAC,MAAM;IACN;EAAA;AAEJ;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,kBAAkBA,CAACf,QAAgB,EAAgB;EACjE,IAAI,CAACA,QAAQ,EAAE;IACb,MAAM,IAAIgB,KAAK,CAAC,mDAAmD,CAAC;EACtE;EACA,MAAMd,MAAM,GAAGN,YAAY,CAAC;IAC1BU,IAAI,EAAET,aAAa,CAAC;MAAEG;IAAS,CAAC;EAClC,CAAC,CAAyB;EAE1B,OAAQ,OAAOiB,KAAwB,EAAEC,IAAkB,KAAK;IAC9DjB,UAAU,CAACC,MAAM,EAAEF,QAAQ,CAAC;IAC5B,MAAMmB,GAAG,GAAG,MAAMjB,MAAM,CAACkB,aAAa,CAACH,KAAK,EAAEC,IAAI,CAAC;IACnDP,UAAU,CAACT,MAAM,EAAEF,QAAQ,CAAC;IAC5B,OAAOmB,GAAG;EACZ,CAAC;AACH","ignoreList":[]}