@wix/agent-skills 1.19.0 → 1.19.2

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 (80) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +33 -31
  3. package/skills/wix-app/references/DASHBOARD_PAGE.md +6 -10
  4. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +17 -8
  5. package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +53 -131
  6. package/skills/wix-app/references/dashboard-page/UX_SUCCESS_MODEL.md +1 -2
  7. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +88 -73
  8. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +46 -22
  9. package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +20 -6
  10. package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +9 -0
  11. package/skills/wix-app/references/editor-react-component/FUNCTION-HANDLERS.md +71 -31
  12. package/skills/wix-app/references/editor-react-component/PARTS.md +5 -2
  13. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +4 -5
  14. package/skills/wix-app/scripts/pkg-root.cjs +73 -0
  15. package/skills/wix-app/scripts/scan-a11y-code.cjs +310 -40
  16. package/skills/wix-app/scripts/scan-a11y-eslint.cjs +118 -24
  17. package/skills/wix-app/scripts/scan-a11y-render.cjs +551 -0
  18. package/skills/wix-app/scripts/scan-a11y-review.cjs +373 -0
  19. package/skills/wix-base44-connector/SKILL.md +67 -43
  20. package/skills/wix-base44-connector/scripts/utils.cjs +39 -3
  21. package/skills/wix-docs/SKILL.md +47 -20
  22. package/skills/wix-headless/SKILL.md +1 -0
  23. package/skills/wix-headless/entry/bootstrap.mjs +106 -50
  24. package/skills/wix-headless/entry/skill.md +5 -3
  25. package/skills/wix-headless/references/CAPABILITIES.md +7 -2
  26. package/skills/wix-headless/references/SDK_HANDOFF.md +2 -1
  27. package/skills/wix-headless/references/inline-recipes/experience-store.md +48 -0
  28. package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +350 -114
  29. package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +1 -1
  30. package/skills/wix-headless/references/managed/AUTHENTICATION.md +25 -5
  31. package/skills/wix-headless/references/managed/CREATE.md +5 -0
  32. package/skills/wix-headless-fast/SKILL.md +25 -7
  33. package/skills/wix-headless-fast/entry/skill.md +23 -14
  34. package/skills/wix-headless-fast/install/fast-path.mjs +48 -5
  35. package/skills/wix-headless-fast/install/pin-agents-md.mjs +57 -0
  36. package/skills/wix-headless-fast/references/shared/CUSTOM_OPERATIONS.md +3 -3
  37. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +204 -35
  38. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +146 -36
  39. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-store.ts +8 -2
  40. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +95 -20
  41. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +21 -2
  42. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +104 -18
  43. package/skills/wix-manage/SKILL.md +16 -10
  44. package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +4 -1
  45. package/skills/wix-manage/references/bookings/create-appointment-service.md +3 -1
  46. package/skills/wix-manage/references/bookings/create-class-service.md +3 -1
  47. package/skills/wix-manage/references/bookings/create-course-service.md +4 -2
  48. package/skills/wix-manage/references/bookings/end-to-end-booking-flow.md +68 -3
  49. package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-flow-seasonal-promotion.md +1 -1
  50. package/skills/wix-manage/references/google-ads/billing-and-payment.md +3 -3
  51. package/skills/wix-manage/references/google-ads/create-performance-max-campaign.md +8 -8
  52. package/skills/wix-manage/references/google-ads/get-campaign-suggestions.md +4 -4
  53. package/skills/wix-manage/references/google-ads/google-ads-dashboard-navigation.md +1 -1
  54. package/skills/wix-manage/references/google-ads/install-and-create-account.md +9 -9
  55. package/skills/wix-manage/references/google-ads/manage-campaign-success-guide.md +108 -14
  56. package/skills/wix-manage/references/google-ads/query-campaign-analytics.md +7 -7
  57. package/skills/wix-manage/references/seo/manage-content-plan.md +182 -136
  58. package/skills/wix-manage/references/seo/manage-google-search-console.md +272 -0
  59. package/skills/wix-manage/references/sites/upload-static-site.md +283 -0
  60. package/skills/wix-manage/references/stores/change-store-currency.md +1 -1
  61. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +3 -1
  62. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +3 -1
  63. package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
  64. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +69 -27
  65. package/skills/wix-vibe-headless/references/storefront/app/components/CartDrawer.jsx +32 -14
  66. package/skills/wix-vibe-headless/references/storefront/app/context/CartContext.jsx +14 -3
  67. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +41 -35
  68. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductDetail.js +77 -30
  69. package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-cart.js +31 -2
  70. package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-catalog.js +31 -6
  71. package/skills/wix-app/references/dashboard-page/COLLECTION_TOOLKIT.md +0 -80
  72. package/skills/wix-app/references/dashboard-page/ENTITY_PAGE_TOOLKIT.md +0 -140
  73. package/skills/wix-app/references/dashboard-page/PATTERNS_BUNDLE_READING.md +0 -112
  74. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductCard.tsx +0 -100
  75. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductDetailView.tsx +0 -130
  76. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductGrid.tsx +0 -48
  77. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ShopView.tsx +0 -66
  78. package/skills/wix-headless-fast/references/storefront/app/components/storefront/VariantPicker.tsx +0 -103
  79. package/skills/wix-headless-fast/references/storefront/app-astro/pages/products/[slug].astro +0 -56
  80. package/skills/wix-headless-fast/references/storefront/app-astro/pages/shop.astro +0 -23
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // oxlint-disable no-console preserve-caught-error
5
+
6
+ /**
7
+ * Accessibility review for Editor React Components: one command, one JSON report.
8
+ *
9
+ * node <SKILL_ROOT>/scripts/scan-a11y-review.cjs <component-dir | files...>
10
+ *
11
+ * Runs the jsx-a11y ESLint scanner and the Babel semantic/contract scanner in
12
+ * process, spawns the render audit (SSR + jsdom + axe-core) per component
13
+ * folder, and groups the findings by rule.
14
+ *
15
+ * Exit codes: 0 every scanner ran and found nothing, 1 findings, 2 a scanner
16
+ * could not run or could not load the component (never treat 2 as clean).
17
+ * Test hook: `A11Y_SKIP_RENDER=1` skips the render audit; the review is then
18
+ * inconclusive (exit 2) even with no findings.
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { spawnSync } = require('child_process');
24
+
25
+ const ROOT = process.cwd();
26
+ const RENDER_SCRIPT = path.join(__dirname, 'scan-a11y-render.cjs');
27
+ const RENDER_TIMEOUT_MS = Number(process.env.A11Y_RENDER_TIMEOUT_MS) || 20_000;
28
+ const MAX_LOCATIONS_PER_GROUP = 5;
29
+ const SEVERITY_RANK = { high: 0, medium: 1, low: 2 };
30
+ const CONFIDENCE_RANK = { high: 0, medium: 1, low: 2, unknown: 3 };
31
+
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ // Arguments and discovery
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+
36
+ function parseArgs(argv) {
37
+ const options = { targets: [], render: process.env.A11Y_SKIP_RENDER !== '1' };
38
+ for (const arg of argv) {
39
+ if (arg.startsWith('--')) throw new Error(`Unknown option ${arg}`);
40
+ options.targets.push(arg);
41
+ }
42
+ return options;
43
+ }
44
+
45
+ const isSource = (name) =>
46
+ /\.(tsx|jsx)$/.test(name) && !/\.(stories|test|spec)\.[jt]sx$/.test(name);
47
+
48
+ function listSources(dir, acc = []) {
49
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
50
+ const full = path.join(dir, entry.name);
51
+ if (entry.isDirectory()) {
52
+ if (entry.name !== 'node_modules') listSources(full, acc);
53
+ } else if (entry.isFile() && isSource(entry.name)) acc.push(full);
54
+ }
55
+ return acc;
56
+ }
57
+
58
+ function isComponentDir(dir) {
59
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return false;
60
+ const names = fs.readdirSync(dir);
61
+ return names.includes('component.tsx') || names.some((name) => name.endsWith('.props.ts'));
62
+ }
63
+
64
+ function findComponentDir(file) {
65
+ let dir = path.dirname(file);
66
+ for (let depth = 0; depth < 6 && !isComponentDir(dir); depth++) {
67
+ const parent = path.dirname(dir);
68
+ if (parent === dir) return path.dirname(file);
69
+ dir = parent;
70
+ }
71
+ return isComponentDir(dir) ? dir : path.dirname(file);
72
+ }
73
+
74
+ /** Expand targets (component folders, a components root, or files) into files and component folders. */
75
+ function discoverFiles(targets) {
76
+ const files = new Set();
77
+ const componentDirs = new Set();
78
+ for (const target of targets) {
79
+ const resolved = path.resolve(target);
80
+ if (!fs.existsSync(resolved)) throw new Error(`Path not found: ${target}`);
81
+ if (fs.statSync(resolved).isDirectory()) {
82
+ if (isComponentDir(resolved)) componentDirs.add(resolved);
83
+ else {
84
+ for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
85
+ const child = path.join(resolved, entry.name);
86
+ if (entry.isDirectory() && isComponentDir(child)) componentDirs.add(child);
87
+ }
88
+ }
89
+ for (const file of listSources(resolved)) files.add(file);
90
+ } else {
91
+ files.add(resolved);
92
+ componentDirs.add(findComponentDir(resolved));
93
+ }
94
+ }
95
+ return { files: [...files].toSorted(), componentDirs: [...componentDirs].toSorted() };
96
+ }
97
+
98
+ // ─────────────────────────────────────────────────────────────────────────────
99
+ // Scanners
100
+ // ─────────────────────────────────────────────────────────────────────────────
101
+
102
+ async function runScanner(name, load) {
103
+ const started = Date.now();
104
+ try {
105
+ const value = await load();
106
+ const parseErrors = (value.meta && value.meta.parseErrors) || [];
107
+ if (parseErrors.length) {
108
+ throw new Error(parseErrors.map((e) => `${e.file}: ${e.message}`).join('; '));
109
+ }
110
+ return { ok: true, ms: Date.now() - started, value };
111
+ } catch (error) {
112
+ const message = error && error.message ? error.message : String(error);
113
+ return { ok: false, ms: Date.now() - started, error: `${name}: ${message}` };
114
+ }
115
+ }
116
+
117
+ function eslintFindings(report) {
118
+ return report.findings.map((f) => ({
119
+ rule: f.rule,
120
+ source: 'eslint',
121
+ severity: 'high',
122
+ confidence: 'high',
123
+ message: f.message,
124
+ at: `${f.file}:${f.line}:${f.column}`,
125
+ }));
126
+ }
127
+
128
+ function semanticFindings(report) {
129
+ return report.findings.map((f) => {
130
+ const contract = f.sourceKind === 'contract';
131
+ const severity =
132
+ contract || f.confidence === 'high' ? 'high' : f.confidence === 'medium' ? 'medium' : 'low';
133
+ return {
134
+ rule: f.rule,
135
+ source: contract ? 'contract' : 'semantic',
136
+ severity,
137
+ confidence: f.confidence,
138
+ message: f.message,
139
+ detail: contract ? null : f.evidence,
140
+ at: `${f.file}:${f.line}:${f.column}`,
141
+ };
142
+ });
143
+ }
144
+
145
+ function runRenderChild(componentDir) {
146
+ const relativeDir = path.relative(ROOT, componentDir) || '.';
147
+ const started = Date.now();
148
+ const result = spawnSync(process.execPath, [RENDER_SCRIPT, componentDir], {
149
+ cwd: ROOT,
150
+ encoding: 'utf8',
151
+ timeout: RENDER_TIMEOUT_MS,
152
+ killSignal: 'SIGKILL',
153
+ maxBuffer: 8 * 1024 * 1024,
154
+ env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'development' },
155
+ });
156
+ const base = { componentDir: relativeDir, ms: Date.now() - started };
157
+
158
+ if (result.error && result.error.code === 'ETIMEDOUT') {
159
+ return {
160
+ ...base,
161
+ ok: false,
162
+ reason: 'timeout',
163
+ findings: [
164
+ {
165
+ rule: 'render-timeout',
166
+ source: 'render',
167
+ severity: 'high',
168
+ confidence: 'high',
169
+ message: `Rendering ${relativeDir} did not finish within ${RENDER_TIMEOUT_MS} ms. Rerun once; if it repeats, the first render never settles.`,
170
+ },
171
+ ],
172
+ };
173
+ }
174
+
175
+ let report = null;
176
+ try {
177
+ const lastLine = String(result.stdout || '')
178
+ .trim()
179
+ .split('\n')
180
+ .filter(Boolean)
181
+ .pop();
182
+ report = lastLine ? JSON.parse(lastLine) : null;
183
+ } catch {
184
+ report = null;
185
+ }
186
+ if (!report) {
187
+ const error = truncate(result.stderr || `exit ${result.status}`, 400);
188
+ return { ...base, ok: false, reason: 'crash', error, findings: [] };
189
+ }
190
+ if (!report.ok) {
191
+ return {
192
+ ...base,
193
+ ok: false,
194
+ reason: report.reason || 'error',
195
+ error: report.error || null,
196
+ findings: [],
197
+ };
198
+ }
199
+ return {
200
+ ...base,
201
+ ok: true,
202
+ entry: report.entry,
203
+ preview: report.preview,
204
+ ssr: report.ssr,
205
+ axeRules: report.axe ? report.axe.rulesRun : null,
206
+ notChecked: report.notChecked || [],
207
+ findings: report.findings.map((f) => ({
208
+ ...f,
209
+ source: 'render',
210
+ at: f.target ? `${relativeDir} ${f.target}` : relativeDir,
211
+ })),
212
+ };
213
+ }
214
+
215
+ // ─────────────────────────────────────────────────────────────────────────────
216
+ // Report
217
+ // ─────────────────────────────────────────────────────────────────────────────
218
+
219
+ function truncate(text, length) {
220
+ const compact = String(text || '').replace(/\s+/g, ' ');
221
+ return compact.length > length ? `${compact.slice(0, length - 1)}…` : compact;
222
+ }
223
+
224
+ /** One entry per (source, rule, message); repeated nodes become a count plus a few locations. */
225
+ function groupFindings(findings, { capPerGroup = MAX_LOCATIONS_PER_GROUP } = {}) {
226
+ const groups = new Map();
227
+ for (const f of findings) {
228
+ const severity = f.severity in SEVERITY_RANK ? f.severity : 'medium';
229
+ const confidence = f.confidence in CONFIDENCE_RANK ? f.confidence : 'medium';
230
+ const key = `${f.source}|${f.rule}|${f.message}`;
231
+ if (!groups.has(key)) {
232
+ groups.set(key, {
233
+ rule: f.rule,
234
+ src: f.source,
235
+ sev: severity,
236
+ conf: confidence,
237
+ n: 0,
238
+ at: [],
239
+ html: f.html || null,
240
+ msg: f.message,
241
+ detail: f.detail || null,
242
+ helpUrl: f.helpUrl || null,
243
+ });
244
+ }
245
+ const group = groups.get(key);
246
+ group.n++;
247
+ if (f.at && group.at.length < capPerGroup && !group.at.includes(f.at)) group.at.push(f.at);
248
+ if (SEVERITY_RANK[severity] < SEVERITY_RANK[group.sev]) group.sev = severity;
249
+ if (CONFIDENCE_RANK[confidence] < CONFIDENCE_RANK[group.conf]) group.conf = confidence;
250
+ }
251
+ return [...groups.values()].toSorted(
252
+ (a, b) =>
253
+ SEVERITY_RANK[a.sev] - SEVERITY_RANK[b.sev] ||
254
+ CONFIDENCE_RANK[a.conf] - CONFIDENCE_RANK[b.conf] ||
255
+ b.n - a.n ||
256
+ a.rule.localeCompare(b.rule),
257
+ );
258
+ }
259
+
260
+ function renderLabel(result) {
261
+ if (result.ok) {
262
+ if (!result.ssr || !result.ssr.ok) return `render: ssr FAILED (${result.entry || '?'})`;
263
+ return `render ok (${result.entry || '?'}, axe ${result.axeRules ?? '-'})`;
264
+ }
265
+ return `render FAILED (${result.reason})`;
266
+ }
267
+
268
+ function buildSummary(groups, scanners, { renderEnabled = true } = {}) {
269
+ const counts = { high: 0, medium: 0, low: 0, groups: groups.length, findings: 0 };
270
+ for (const group of groups) {
271
+ counts[group.sev] += group.n;
272
+ counts.findings += group.n;
273
+ }
274
+ // A timeout is itself a finding; any other render failure (missing tooling,
275
+ // a module the audit cannot load) or a skipped render means the audit did
276
+ // not run, never clean.
277
+ const staticFailed = !scanners.eslint.ok || !scanners.semantic.ok;
278
+ const renderBroken =
279
+ !renderEnabled || scanners.render.some((r) => !r.ok && r.reason !== 'timeout');
280
+ const status =
281
+ staticFailed || renderBroken ? 'error' : counts.findings > 0 ? 'findings' : 'clean';
282
+ const severities = ['high', 'medium', 'low']
283
+ .filter((level) => counts[level] > 0)
284
+ .map((level) => `${counts[level]} ${level}`);
285
+ const line = [
286
+ `A11Y ${counts.groups} groups/${counts.findings} findings${severities.length ? ` (${severities.join(', ')})` : ''}`,
287
+ `eslint ${scanners.eslint.ok ? 'ok' : 'FAILED'}`,
288
+ `semantic ${scanners.semantic.ok ? 'ok' : 'FAILED'}`,
289
+ renderEnabled
290
+ ? scanners.render.map(renderLabel).join(' · ') || 'render: no component folder'
291
+ : 'render SKIPPED (inconclusive)',
292
+ ].join(' · ');
293
+ return { status, line, counts };
294
+ }
295
+
296
+ const computeExitCode = (summary) =>
297
+ summary.status === 'error' ? 2 : summary.counts.findings > 0 ? 1 : 0;
298
+
299
+ async function review(options) {
300
+ const { files, componentDirs } = discoverFiles(options.targets);
301
+ // A wrong path (a typo, a parent folder) must never read as clean.
302
+ if (componentDirs.length === 0) {
303
+ throw new Error(
304
+ `No component folder under ${options.targets.join(', ')}: expected component.tsx or *.props.ts.`,
305
+ );
306
+ }
307
+
308
+ // ESLint rejects an empty file list; a folder with no JSX still gets its render audit.
309
+ const eslint = await runScanner('eslint', () =>
310
+ files.length ? require('./scan-a11y-eslint.cjs').scan(files) : { findings: [] },
311
+ );
312
+ const semantic = await runScanner('semantic', () =>
313
+ files.length ? require('./scan-a11y-code.cjs').scan(files) : { findings: [] },
314
+ );
315
+ const render = options.render ? componentDirs.map(runRenderChild) : [];
316
+
317
+ const findings = [
318
+ ...(eslint.ok ? eslintFindings(eslint.value) : []),
319
+ ...(semantic.ok ? semanticFindings(semantic.value) : []),
320
+ ...render.flatMap((r) => r.findings),
321
+ ];
322
+ const groups = groupFindings(findings);
323
+
324
+ const notChecked = new Set(options.render ? [] : ['render audit (skipped)']);
325
+ for (const r of render) {
326
+ for (const item of r.notChecked || []) notChecked.add(item);
327
+ }
328
+
329
+ const scanners = {
330
+ eslint: { ok: eslint.ok, ms: eslint.ms, error: eslint.error || null },
331
+ semantic: { ok: semantic.ok, ms: semantic.ms, error: semantic.error || null },
332
+ render: render.map(({ findings: _findings, notChecked: _notChecked, ...rest }) => rest),
333
+ };
334
+ const summary = buildSummary(groups, scanners, { renderEnabled: options.render });
335
+ return {
336
+ summary,
337
+ findings: groups,
338
+ scanners,
339
+ files: files.map((file) => path.relative(ROOT, file)),
340
+ notChecked: [...notChecked],
341
+ exitCode: computeExitCode(summary),
342
+ };
343
+ }
344
+
345
+ async function main() {
346
+ let options;
347
+ try {
348
+ options = parseArgs(process.argv.slice(2));
349
+ if (options.targets.length === 0) throw new Error('No component directory or files specified.');
350
+ } catch (error) {
351
+ console.error(
352
+ `${error.message}\nUsage: node <SKILL_ROOT>/scripts/scan-a11y-review.cjs <component-dir | files...>`,
353
+ );
354
+ process.exit(2);
355
+ }
356
+ try {
357
+ const report = await review(options);
358
+ console.log(JSON.stringify(report));
359
+ process.exit(report.exitCode);
360
+ } catch (error) {
361
+ console.log(
362
+ JSON.stringify({
363
+ summary: { status: 'error', line: error.message, counts: { findings: 0 } },
364
+ findings: [],
365
+ }),
366
+ );
367
+ process.exit(2);
368
+ }
369
+ }
370
+
371
+ module.exports = { review, discoverFiles, groupFindings, buildSummary, computeExitCode };
372
+
373
+ if (require.main === module) main();
@@ -23,8 +23,8 @@ they also surface from a search that began at the methods.
23
23
 
24
24
  In this skill:
25
25
 
26
- - **What are you building?** — route each feature to its identity: visitor token, admin token
27
- in a backend function, or admin ad hoc in exec_tool
26
+ - **What are you building?** — for visitors, for the owner, or management done for them —
27
+ decided per request from the user's intent, each on its own flow
28
28
  - **The helpers** — the `wx.*` loader every exec opens with
29
29
  - **Gather context** — `wx.context`, the report of what the site actually has
30
30
  - **Learn Wix** — find the APIs, learn their contracts
@@ -40,25 +40,41 @@ In this skill:
40
40
 
41
41
  ## What are you building?
42
42
 
43
- Choose the token by who the code acts for. A headless app can serve visitors, provide admin
44
- management tools, or do both.
45
-
46
- **A site for visitors** — use a visitor token for public reads and actions on behalf of the
47
- visitor, never the admin connector token. Call Wix directly from the browser through one shared
48
- visitor client (Write the code, below). Redirect sessions for Wix-hosted flows also require a
49
- visitor token minted for the headless OAuth app; see Visitor authentication and Wix-hosted flows below.
50
- Anyone can mint an anonymous visitor token from the
51
- OAuth app's public `clientId`; no visitor login is required. APIs for the "current visitor"
52
- use that token to identify whose data and state to access. This applies both to a standalone
53
- headless frontend and to a frontend extending an existing Wix site.
54
-
55
- **An admin tool for the owner** — use the admin connector token in backend functions implementing
56
- admin logic; keep it secret and server-side. The frontend calls those functions, not Wix with
57
- visitor tokens. A custom headless management site extending the Wix back office follows this
58
- flow too. Ad hoc management calls in `exec_tool` also use the admin token. Backend functions
59
- also handle work requiring the owner's permissions, such as webhooks, scheduled jobs, and
60
- explicitly authorized elevated operations. For an app with both visitor and admin features,
61
- keep each feature on its corresponding flow.
43
+ Wix has two identities, visitor and admin, and each has its own flow. Decide which one every
44
+ request is for before acting on it — from what the user wants, not from the technology:
45
+
46
+ - **Visitor** — the people using the site: browsing, booking, buying, signing in as a member.
47
+ Visitor token, called from the browser through one shared visitor client.
48
+ - **Admin, built** — a management surface of the owner's own: a custom view, a bulk operation,
49
+ an internal tool. Admin connector token, server-side in backend functions the frontend calls.
50
+ - **Admin, done for them** — the user wants the business managed now, not an app: create the
51
+ products, change a setting, import the contacts. Admin token, ad hoc in exec_tool; nothing
52
+ ships in the app.
53
+
54
+ The owner already has a full Wix dashboard for running the business — orders, products,
55
+ bookings, settings, and more — so an admin build is a choice the user makes, not a gap to fill by
56
+ default. When a request has both visitor and admin parts, each keeps its own flow.
57
+
58
+ When the request doesn't say which, ask the user — in their terms (who the thing is for), not in
59
+ technical ones (identities, tokens, flows). When you decide without asking, tell them what you
60
+ chose the same way.
61
+
62
+ The rules that follow from the split:
63
+
64
+ - **Checkout runs on a visitor token, never the admin token** — including a checkout this app
65
+ creates for the visitor. The admin token *will* hand back a checkout URL, and that is the trap:
66
+ it belongs to the site rather than the buyer (no cart persistence, no abandoned-checkout
67
+ recovery, no attribution), and a redirect session refuses it outright (`403`). Returns reach
68
+ this app only from URLs in the OAuth app's redirect list (Visitor authentication and
69
+ Wix-hosted flows, below).
70
+ - **No OAuth app yet is not a reason to use the admin token.** `wx.ensureOAuthApp` returns the
71
+ `clientId`, creating the app when the site has none — a visitor flow's first step. Anyone can
72
+ mint an anonymous visitor token from that public `clientId`; no visitor login is required.
73
+ - **No login still means visitors.** `asServiceRole` governs this app's own data access and says
74
+ nothing about which identity Wix sees: a public page's Wix calls run on a visitor token. Reach
75
+ for the admin token because a feature is owner-side, never because nobody signs in.
76
+ - **Owner-side work beyond pages** — webhooks, scheduled jobs, explicitly authorized elevated
77
+ operations — is admin flow too, in backend functions.
62
78
 
63
79
  ```
64
80
  visitor pages ──(visitor token)────────────────────────► wixapis.com
@@ -66,6 +82,11 @@ admin pages ──► base44/functions/… ──(admin token)────►
66
82
  exec_tool ──(admin token, ad hoc management)────────► wixapis.com
67
83
  ```
68
84
 
85
+ **The connected site owns its payments.** Whatever the visitor buys is paid on the Wix-hosted page
86
+ this app hands them off to, with the provider the site already has. Base44's
87
+ `<available_payment_providers>` note is about checkout that Base44 hosts instead — it does not
88
+ apply to these flows, and asks nothing of them, whichever providers it names or rules out.
89
+
69
90
  ## The helpers
70
91
 
71
92
  Research and probing run in exec_tool, and one loader opens every exec (execs share no state —
@@ -90,6 +111,7 @@ const wx = require(require("path").resolve(P));
90
111
  - `wx.bash(cmd)` — shell over saved files (GNU grep/sed; awk is mawk; no rg)
91
112
  - `wx.spec(docsUrl | code)` — a method's exact schema, plus the titles of the docs' own request examples saved at `examplesPath`; pass a hit's docsUrl (direct load), or raw code to query the index yourself
92
113
  - `wx.mgmtRecipes(q?)` — management-recipe index; no arg → categories, a word → matching recipes
114
+ - `wx.ensureOAuthApp(token, { name, redirectUris, redirectDomains })` — the headless OAuth app, found by name or created, with the redirect lists merged into whatever it already allows; returns `{ clientId, created }`. `clientId` is what the browser mints visitor tokens from. Run it whenever `context` reports no OAuth app, or before any Wix-hosted flow that returns to this app — see Visitor authentication and Wix-hosted flows
93
115
  - `wx.installApp(appDefId, siteId, token)` — install a Wix app on the site (Apps Installer). If discovery finds an API whose app isn't installed on the site, install it first — that's a one-call prerequisite, **not** a reason to fall back to a hand-built alternative. `appDefId` from `search` or the Apps-Created-by-Wix table; `siteId` from `context` (the site report)
94
116
 
95
117
  Search makes one combined request across REST, management recipes, and Headless by default.
@@ -114,6 +136,10 @@ One report: installed apps **with ids** (incl. Stores' catalog version — V1 vs
114
136
  endpoints), the OAuth app id (**also the visitor `clientId`**), locale, currency, CMS collections.
115
137
  An empty report = bad token, never an empty site.
116
138
 
139
+ No OAuth app in the report means the site has none **yet**. For anything a visitor operates,
140
+ create it with `wx.ensureOAuthApp` — one call, returns the `clientId` the frontend mints visitor
141
+ tokens from — and build the feature on the visitor flow.
142
+
117
143
  Reports over 4,000 characters are saved in full to a temporary Markdown file. The result includes
118
144
  its path, byte and line counts, and a heading outline. Read that file to inspect the site context.
119
145
 
@@ -228,7 +254,9 @@ Want to shape the result yourself? Pass raw code against the index (`lightIndex`
228
254
  await wx.spec(`
229
255
  const url = "<docsUrl from search/browse>"; // API method page, not a skill/article page
230
256
  const s = await getResourceSchemaByUrl(url);
231
- const m = s.methods.find(x => x.docsUrl === url);
257
+ // A method URL resolves to exactly that method — take methods[0]. Don't match on docsUrl:
258
+ // the reader normalizes the url, so equality against the one you passed can miss.
259
+ const m = s.methods[0];
232
260
  return {
233
261
  call: m.publicUrl, // callable https://www.wixapis.com/… URL
234
262
  body: m.requestBody?.content["application/json"].schema.properties,
@@ -327,13 +355,15 @@ return await wx.post("<a public read from Learn Wix>", { query: {} }, visitorTok
327
355
 
328
356
  **Any Wix-hosted flow that returns the visitor to your app needs the headless OAuth app's
329
357
  redirect config set.** This covers redirect sessions *and* sending a buyer to the Wix-hosted
330
- checkout `checkoutUrl` and back — any flow where Wix redirects to a URL on your app. Use the
331
- OAuth app's `clientId` from `wx.context()`; if the report has no OAuth app, create one with the
332
- admin connector token as shown below, then mint a visitor token from its `clientId` (anonymous
333
- visitors do not need to log in). **Always set `allowedRedirectUris` and `allowedRedirectDomains`
334
- when you create it** — an OAuth app created with a name only cannot complete any return, and the
335
- break is silent (create-checkout and the anonymous token still succeed) until a real buyer is
336
- redirected and stranded on Wix.
358
+ checkout `checkoutUrl` and back — any flow where Wix redirects to a URL on your app.
359
+
360
+ **Step one of any visitor flow: `wx.ensureOAuthApp` with this app's own URLs.** It returns the
361
+ `clientId` the frontend mints visitor tokens from (anonymous visitors do not need to log in),
362
+ creating the OAuth app when the site has none and adding your URLs to its lists when it already
363
+ has one. Returns only complete for URLs in those lists, and a missing one breaks silently —
364
+ create-checkout and the anonymous token still succeed — until a real buyer is redirected and
365
+ stranded on Wix. It is idempotent by name: never create a second OAuth app for the same Base44
366
+ app, or the first `clientId`'s returns stop working.
337
367
 
338
368
  ```js
339
369
  // Register BOTH of this app's own URLs — its preview URL and its published URL — so returns work
@@ -353,20 +383,14 @@ const returnDomains = appOrigins.map(origin => new URL(origin).hostname);
353
383
  // OAuth redirect configuration: exact login URLs versus domains for other returns.
354
384
  // https://dev.wix.com/docs/go-headless/authentication/setup/allow-redirect-uris-and-domains.md
355
385
  const { accessToken: adminToken } = await base44.asServiceRole.connectors.getConnection("wix");
356
- const { oAuthApp } = await wx.post("https://www.wixapis.com/oauth-app/v1/oauth-apps", {
357
- oAuthApp: {
358
- name: "My App",
359
- // Login callbacks: the authorization request's redirect URI must match exactly.
360
- allowedRedirectUris: loginCallbacks,
361
- // Returns from Wix-hosted flows: hostnames only, allowing URLs under each domain.
362
- allowedRedirectDomains: returnDomains,
363
- },
364
- }, adminToken);
365
- const clientId = oAuthApp.id; // Public visitor client ID, used by the frontend client above.
366
-
367
- // If destinations change later, update this OAuth app rather than creating another.
368
- // Read its existing lists and merge new entries before updating, preserving old entries.
369
- // https://dev.wix.com/docs/api-reference/business-management/headless/oauth-apps/update-oauth-app.md
386
+ const { clientId, created } = await wx.ensureOAuthApp(adminToken, {
387
+ name: "My App", // this app's name; the lookup key on later runs
388
+ redirectUris: loginCallbacks, // login callbacks: matched exactly by the auth request
389
+ redirectDomains: returnDomains, // returns from Wix-hosted flows: hostnames only
390
+ });
391
+ // clientId is the public visitor client id the frontend client above mints from.
392
+ // When destinations change later — a custom domain, a renamed app — call this again with the
393
+ // new URLs: it updates the same OAuth app and preserves the entries already there.
370
394
 
371
395
  ```
372
396
 
@@ -353,9 +353,13 @@ async function spec(arg) {
353
353
  const code = /^async function/.test(s) ? s
354
354
  : /^https:\/\/dev\.wix\.com\/docs\//.test(s) ? "async function(){ return await getResourceSchemaByUrl(" + JSON.stringify(s) + "); }"
355
355
  : "async function(){ " + s + " }";
356
- const { result } = await post("https://mcp.wix.com/api/code-mode/search", { code });
356
+ const { result, error } = await post("https://mcp.wix.com/api/code-mode/search", { code });
357
+ // The envelope is 200 even when the query throws — the cause is in `error`, not the status.
358
+ // Surfacing it matters: swallowing it turns "your code threw" into "nothing matched".
359
+ if (error)
360
+ return { error, note: "the query threw — read the message and fix the code; don't re-send it unchanged" };
357
361
  if (result == null || (Array.isArray(result) && !result.length))
358
- return { result, note: "empty — the query missed the shape; match your docsUrl against s.methods[].docsUrl" };
362
+ return { result, note: "the query ran but matched nothing — widen it, or check the url resolves" };
359
363
  const text = JSON.stringify(result, null, 1);
360
364
  if (text.length <= BUDGET) return result;
361
365
  let h = 5381;
@@ -424,4 +428,36 @@ async function installApp(appDefId, siteId, token) {
424
428
  { tenant: { tenantType: "SITE", id: siteId }, appInstance: { appDefId } }, token);
425
429
  }
426
430
 
427
- module.exports = { req, post, get, patch, put, del, clip, context, browse, search, page, bash, spec, mgmtRecipes, installApp };
431
+ // The headless OAuth app: the visitor client id, and the redirect config every Wix-hosted
432
+ // return needs. Idempotent by name — a site reached by the "connect an existing site" flow
433
+ // has none, and creating a second one for the same app strands the first app's returns.
434
+ // Redirect lists are merged, never replaced: an app that already serves a custom domain
435
+ // keeps it when a preview URL is added.
436
+ async function ensureOAuthApp(token, { name, redirectUris = [], redirectDomains = [] }) {
437
+ const { oAuthApps = [] } = await post(
438
+ "https://www.wixapis.com/oauth-app/v1/oauth-apps/query", { query: {} }, token);
439
+ const existing = oAuthApps.find((a) => a.name === name);
440
+ const merge = (a = [], b = []) => [...new Set([...a, ...b])];
441
+ if (!existing) {
442
+ const { oAuthApp } = await post("https://www.wixapis.com/oauth-app/v1/oauth-apps", {
443
+ oAuthApp: { name, allowedRedirectUris: redirectUris, allowedRedirectDomains: redirectDomains },
444
+ }, token);
445
+ return { clientId: oAuthApp.id, created: true, oAuthApp };
446
+ }
447
+ const allowedRedirectUris = merge(existing.allowedRedirectUris, redirectUris);
448
+ const allowedRedirectDomains = merge(existing.allowedRedirectDomains, redirectDomains);
449
+ const unchanged =
450
+ allowedRedirectUris.length === (existing.allowedRedirectUris || []).length &&
451
+ allowedRedirectDomains.length === (existing.allowedRedirectDomains || []).length;
452
+ if (unchanged) return { clientId: existing.id, created: false, oAuthApp: existing };
453
+ // The update docs disagree with themselves on the mask field (prose says `paths`, the
454
+ // curl example says `path`); send both so the call does not silently no-op.
455
+ const paths = ["allowedRedirectUris", "allowedRedirectDomains"];
456
+ const { oAuthApp } = await patch(
457
+ `https://www.wixapis.com/oauth-app/v1/oauth-apps/${existing.id}`,
458
+ { oAuthApp: { id: existing.id, allowedRedirectUris, allowedRedirectDomains },
459
+ mask: { paths, path: paths.join(",") } }, token);
460
+ return { clientId: (oAuthApp || existing).id, created: false, oAuthApp: oAuthApp || existing };
461
+ }
462
+
463
+ module.exports = { req, post, get, patch, put, del, clip, context, browse, search, page, bash, spec, mgmtRecipes, installApp, ensureOAuthApp };