@wix/mcp 1.0.95 → 1.0.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/bin-standalone.js +352 -22
- package/build/cjs/index.cjs +910 -530
- package/build/cjs/toolkit.cjs +405 -46
- package/build/dts/api-call/index.js +23 -1
- package/build/dts/api-call/upload-headless-website-files.d.ts +31 -2
- package/build/dts/api-call/upload-headless-website-files.js +107 -15
- package/build/dts/code-mode/attachments.d.ts +9 -6
- package/build/dts/code-mode/attachments.js +23 -16
- package/build/dts/code-mode/index.js +2 -2
- package/build/dts/config/default-config.d.ts +1 -1
- package/build/dts/config/default-config.js +17 -16
- package/build/dts/docs/docs.js +64 -1
- package/build/dts/docs/guessed-url.d.ts +41 -0
- package/build/dts/docs/guessed-url.js +185 -0
- package/build/dts/infra/redact-personal-data.d.ts +1 -1
- package/build/dts/infra/redact-personal-data.js +72 -3
- package/build/dts/site-widget-tools/constants.d.ts +3 -3
- package/build/dts/site-widget-tools/constants.js +3 -3
- package/build/dts/site-widget-tools/pull-site-creation-job-tool/index.js +1 -1
- package/build/dts/site-widget-tools/search-site-templates-tool/index.js +10 -1
- package/build/dts/site-widget-tools/site-builder-tool/index.js +14 -2
- package/build/dts/site-widget-tools/suggested-domain-tool/index.js +20 -9
- package/build/dts/tool-utils.d.ts +8 -1
- package/build/dts/tool-utils.js +26 -2
- package/build/dts/toolkit.js +7 -1
- package/build/esm/index.js +793 -413
- package/build/esm/toolkit.js +405 -46
- package/package.json +2 -2
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { HttpClient } from '@wix/http-client';
|
|
3
3
|
import { logger } from '../infra/logger.js';
|
|
4
|
-
import { handleWixAPIResponse } from '../tool-utils.js';
|
|
4
|
+
import { buildErrorMessage, handleWixAPIResponse, missingSiteContext } from '../tool-utils.js';
|
|
5
5
|
import { rethrowIfUnauthorized } from '../auth/index.js';
|
|
6
6
|
import { isApiUrlAllowed } from './validation.js';
|
|
7
7
|
import { addUploadImageTool } from './upload-image-tool.js';
|
|
8
8
|
import { CALL_WIX_SITE_API_NO_EXECUTE_LINE, CALL_WIX_SITE_API_RECONNECT_LINE, defaultToolDescriptions } from '../config/default-config.js';
|
|
9
9
|
import { desc, WHY_NOT_EXECUTE_NO_EXECUTE, WHY_NOT_EXECUTE_RECONNECT } from '../config/param-descriptions.js';
|
|
10
10
|
import { MAX_ALLOWED_RESPONSE_CHARS } from '../infra/response-size-limits.js';
|
|
11
|
+
// ManageWixSite sends the account's credential and no site, which is right for the account
|
|
12
|
+
// and for sites as objects in it, and wrong for anything inside a site. The site call goes
|
|
13
|
+
// through ExecuteWixAPI where this server serves it, else through CallWixSiteAPI.
|
|
14
|
+
const siteToolCall = (executeServed) => executeServed
|
|
15
|
+
? "through ExecuteWixAPI with that site's `siteId` (`scope: \"site\"`), loading ExecuteWixAPI through your client's tool search if it isn't loaded"
|
|
16
|
+
: "through CallWixSiteAPI with that site's `siteId`";
|
|
17
|
+
const manageNoSiteHint = (executeServed) => `This is a site-level API: it acts inside a site, and ManageWixSite sends no site, so the same call fails here every time. Call the same URL, method and body ${siteToolCall(executeServed)}.`;
|
|
18
|
+
const manageEmpty403Hint = (executeServed) => `If this endpoint acts inside a site (its products, data, bookings, members, pages, publishing), it is site-level, and ManageWixSite sends no site: call the same URL, method and body ${siteToolCall(executeServed)}. If it acts on the account or on sites as objects in it, the account lacks the permission; don't retry.`;
|
|
11
19
|
export { addUploadImageTool } from './upload-image-tool.js';
|
|
12
20
|
const DEFAULT_MAX_RESPONSE_SIZE = 50000; // ~50KB cap
|
|
13
21
|
/**
|
|
@@ -325,6 +333,20 @@ export function addApiCallTool(server, strategy, allowedTools = ['CallWixSiteAPI
|
|
|
325
333
|
if (!response) {
|
|
326
334
|
throw new Error(`Failed to call Wix Account API: ${error}`);
|
|
327
335
|
}
|
|
336
|
+
// A site-level API called here gets no site, and the generic hint sends the agent
|
|
337
|
+
// back to the docs for an endpoint that was right all along.
|
|
338
|
+
const noSite = missingSiteContext(response.status, response.data);
|
|
339
|
+
if (noSite) {
|
|
340
|
+
throw new Error(buildErrorMessage({
|
|
341
|
+
status: response.status,
|
|
342
|
+
statusText: response.statusText,
|
|
343
|
+
requestId: response.headers?.['x-wix-request-id'] ?? null,
|
|
344
|
+
responseData: response.data,
|
|
345
|
+
retryHint: (noSite === 'certain'
|
|
346
|
+
? manageNoSiteHint
|
|
347
|
+
: manageEmpty403Hint)(!!options?.executeWixAPIServed)
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
328
350
|
return handleWixAPIResponse(response);
|
|
329
351
|
}
|
|
330
352
|
});
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { type SiteFile } from './site-files.js';
|
|
2
2
|
import { WixMcpServer } from '../wix-mcp-server.js';
|
|
3
|
+
import { type StartDeployWebsiteFetchOptions } from '../docs/start-deploy-website.js';
|
|
4
|
+
import type { HttpClient } from '@wix/http-client';
|
|
3
5
|
import { type McpAuthenticationStrategy } from '../auth/index.js';
|
|
4
6
|
export declare const UPLOAD_HEADLESS_WEBSITE_FILES_TOOL_NAME = "UploadHeadlessWebsiteFiles";
|
|
5
7
|
/** The largest site zip `bundle-upload-url` presigns (headless-business-setup's MAX_BUNDLE_BYTES). */
|
|
6
8
|
export declare const MAX_BUNDLE_BYTES: number;
|
|
9
|
+
/**
|
|
10
|
+
* The static files a build's client folder can hold and still release. Builds of up to 2,550
|
|
11
|
+
* released and every one from 4,818 failed (headless-business-setup logs, October 2026): the
|
|
12
|
+
* deployments service's reply outgrows its 4 MiB gRPC limit well before its 5,000-file cap.
|
|
13
|
+
* Both failures reach us as a bare 500.
|
|
14
|
+
*/
|
|
15
|
+
export declare const MAX_RELEASE_STATIC_FILES = 2500;
|
|
16
|
+
/** Code Store refuses a larger source zip (`REPOSITORY_ARCHIVE_TOO_LARGE`), and with it the release. */
|
|
17
|
+
export declare const MAX_SOURCE_ZIP_BYTES: number;
|
|
7
18
|
/**
|
|
8
19
|
* Attribution the drop endpoint stamps on its upload BI event (`source` from
|
|
9
20
|
* `campaign`, `agent` from `agent`, both ≤ 64 chars) — the same parser as the
|
|
@@ -18,6 +29,10 @@ export interface UploadHeadlessWebsiteFilesOptions {
|
|
|
18
29
|
fetchImpl?: typeof fetch;
|
|
19
30
|
/** Client identity for drop attribution when the server cannot tell (the toolkit's configured name). */
|
|
20
31
|
clientName?: string;
|
|
32
|
+
/** Where the headless guide is read from (the docs tools' portal, repo and commit overrides). */
|
|
33
|
+
guide?: StartDeployWebsiteFetchOptions;
|
|
34
|
+
/** Test seam: the headless guide's text, in place of the fetch. */
|
|
35
|
+
guideFetch?: (httpClient: HttpClient | undefined) => Promise<string | undefined>;
|
|
21
36
|
}
|
|
22
37
|
export declare const UPLOAD_HEADLESS_WEBSITE_FILES_PARAM_DESCRIPTIONS: {
|
|
23
38
|
readonly files: "The site files as text, so file bytes never pass through a code string. Sections delimited by a line `=== FILE: <path> ===` (text, raw) or `=== FILE: <path> base64 ===` (binary, base64); `<path>` is relative to the site root; a static site must include `index.html`. Use base64 for small binaries (icons, logos, fonts); large images/video belong behind a URL in the HTML, not in the bundle. Optional when `attachments` carries the files. A Wix Astro build (`.wix/build-metadata.json` and `dist/` as `astro build` with the Wix hosting adapter wrote them, zipped from the project root) needs no `index.html`: it is released to the site's Wix hosting instead of dropped, and the project's other files in the upload are stored as its source when the site's repository accepts it.";
|
|
@@ -39,6 +54,8 @@ export interface DropResult {
|
|
|
39
54
|
uploadId?: string;
|
|
40
55
|
files: string[];
|
|
41
56
|
}
|
|
57
|
+
/** The headless guide, inline, after a static drop: the same page StartHeadlessBuild returns. */
|
|
58
|
+
export declare function formatInlineGuide(guide: string | undefined): string;
|
|
42
59
|
export declare function formatDropResult(r: DropResult): string;
|
|
43
60
|
/** One presigned PUT: where the zip goes and the headers the storage requires on it. */
|
|
44
61
|
interface PresignedPut {
|
|
@@ -87,13 +104,25 @@ export declare function formatBundleReleaseResult(siteId: string, r: BundleRelea
|
|
|
87
104
|
* uploaded as files the same call again (the tool asks for new URLs itself).
|
|
88
105
|
*/
|
|
89
106
|
export type BundleRetry = 'prepare' | 'resend';
|
|
107
|
+
/**
|
|
108
|
+
* The permission every owner route (drop, the upload URL, the release, the files helpers)
|
|
109
|
+
* checks on the caller for the site. A 403 naming it is about the login, not the upload: the
|
|
110
|
+
* connected Wix account is not the one that owns the site, or its role there is too low.
|
|
111
|
+
*/
|
|
112
|
+
export declare const SITE_ACCESS_PERMISSION = "OAUTH_APP.APP_CREATE";
|
|
113
|
+
/**
|
|
114
|
+
* A 403 for the site itself, as the agent should act on it; null for any other error.
|
|
115
|
+
* `stagedKey` is the uploadKey of a build already PUT when the refusal came from the
|
|
116
|
+
* release: that upload is kept, and counts against the site's unreleased uploads.
|
|
117
|
+
*/
|
|
118
|
+
export declare function describeSiteAccessDenied(err: unknown, siteId?: string, stagedKey?: string): string | null;
|
|
90
119
|
/** The bundle-upload-url route's errors, as the agent should act on them. */
|
|
91
|
-
export declare function describeBundleUploadUrlError(err: unknown): string;
|
|
120
|
+
export declare function describeBundleUploadUrlError(err: unknown, siteId?: string): string;
|
|
92
121
|
/** The deployment label the service keeps is 200 characters; longer notes are cut, not refused. */
|
|
93
122
|
export declare const RELEASE_NOTES_MAX_CHARS = 200;
|
|
94
123
|
export declare function releaseNotesForService(notes: string): string;
|
|
95
124
|
/** The release-bundle route's errors, as the agent should act on them. */
|
|
96
|
-
export declare function describeBundleReleaseError(err: unknown, uploadKey: string, retry?: BundleRetry): string;
|
|
125
|
+
export declare function describeBundleReleaseError(err: unknown, uploadKey: string, retry?: BundleRetry, siteId?: string): string;
|
|
97
126
|
/**
|
|
98
127
|
* A build that came as files, released over the bundle routes: an upload URL for the
|
|
99
128
|
* site zip's size, the zip PUT there, the source zip PUT to the repository's URL when
|
|
@@ -33,6 +33,8 @@ import { buildSourceFiles, findHeadlessBuild, withoutEmptyStatics } from './head
|
|
|
33
33
|
import { fileParamNote } from '../config/file-params.js';
|
|
34
34
|
import { WIX_BUSINESS_FEATURES } from '../site-widget-tools/constants.js';
|
|
35
35
|
import { NO_PERSONAL_DATA } from '../docs/start-deploy-website.js';
|
|
36
|
+
import { HEADLESS_GUIDE_URL } from '../docs/start-headless-build.js';
|
|
37
|
+
import { fetchArticleContent } from '../docs/fetch-article.js';
|
|
36
38
|
import { logger } from '../infra/logger.js';
|
|
37
39
|
import { redactPersonalData } from '../infra/redact-personal-data.js';
|
|
38
40
|
import { defaultToolDescriptions } from '../config/default-config.js';
|
|
@@ -43,6 +45,15 @@ export const UPLOAD_HEADLESS_WEBSITE_FILES_TOOL_NAME = 'UploadHeadlessWebsiteFil
|
|
|
43
45
|
const HEADLESS_BUSINESS = 'https://www.wixapis.com/headless-business-setup/v1/headless-business';
|
|
44
46
|
/** The largest site zip `bundle-upload-url` presigns (headless-business-setup's MAX_BUNDLE_BYTES). */
|
|
45
47
|
export const MAX_BUNDLE_BYTES = 100 * 1024 * 1024;
|
|
48
|
+
/**
|
|
49
|
+
* The static files a build's client folder can hold and still release. Builds of up to 2,550
|
|
50
|
+
* released and every one from 4,818 failed (headless-business-setup logs, October 2026): the
|
|
51
|
+
* deployments service's reply outgrows its 4 MiB gRPC limit well before its 5,000-file cap.
|
|
52
|
+
* Both failures reach us as a bare 500.
|
|
53
|
+
*/
|
|
54
|
+
export const MAX_RELEASE_STATIC_FILES = 2_500;
|
|
55
|
+
/** Code Store refuses a larger source zip (`REPOSITORY_ARCHIVE_TOO_LARGE`), and with it the release. */
|
|
56
|
+
export const MAX_SOURCE_ZIP_BYTES = 64 * 1024 * 1024;
|
|
46
57
|
/**
|
|
47
58
|
* Attribution the drop endpoint stamps on its upload BI event (`source` from
|
|
48
59
|
* `campaign`, `agent` from `agent`, both ≤ 64 chars) — the same parser as the
|
|
@@ -75,6 +86,19 @@ export const UPLOAD_HEADLESS_WEBSITE_FILES_PARAM_DESCRIPTIONS = {
|
|
|
75
86
|
// The wix-manage skill's recipes, which the success reply points to for what comes next.
|
|
76
87
|
const UPLOAD_RECIPE_SKILL_URL = 'https://www.wix.com/skills/wix-manage/references/sites/upload-static-site.md';
|
|
77
88
|
const DOMAIN_RECIPE_SKILL_URL = 'https://www.wix.com/skills/wix-manage/references/domains/domain-search-purchase-and-connect.md';
|
|
89
|
+
/** The headless guide, inline, after a static drop: the same page StartHeadlessBuild returns. */
|
|
90
|
+
export function formatInlineGuide(guide) {
|
|
91
|
+
if (!guide || !guide.trim())
|
|
92
|
+
return '';
|
|
93
|
+
return [
|
|
94
|
+
'',
|
|
95
|
+
'---',
|
|
96
|
+
'',
|
|
97
|
+
`Wix's headless guide, the build of a site that needs Wix business features (also at https://www.wix.com/headless/skill.md):`,
|
|
98
|
+
'',
|
|
99
|
+
guide.trim()
|
|
100
|
+
].join('\n');
|
|
101
|
+
}
|
|
78
102
|
export function formatDropResult(r) {
|
|
79
103
|
const shown = r.files.slice(0, 60);
|
|
80
104
|
return [
|
|
@@ -87,7 +111,7 @@ export function formatDropResult(r) {
|
|
|
87
111
|
`Give the user both links, each on its own line and named for what it is: the live site, and the site's dashboard. Then offer a custom domain, bought through Wix or one they already own (${DOMAIN_RECIPE_SKILL_URL}).`,
|
|
88
112
|
'',
|
|
89
113
|
`- To change this site later: "Change it later" in ${UPLOAD_RECIPE_SKILL_URL}, with this metaSiteId (${r.metaSiteId}). The same site every time, never a new one.`,
|
|
90
|
-
`- Wix business features (${WIX_BUSINESS_FEATURES}):
|
|
114
|
+
`- When the site needs Wix business features (${WIX_BUSINESS_FEATURES}), a drop of static pages is not its build: a full Wix Headless site has those features run by Wix and its pages reading from them. That build is Wix's headless guide, https://www.wix.com/headless/skill.md (what StartHeadlessBuild returns; call it with startingPoint "an existing Wix Headless site", this site is one): it installs the solution on this site, seeds its content through the API, and puts up pages that fetch it, uploaded here again. A site with no business features is done.`
|
|
91
115
|
].join('\n');
|
|
92
116
|
}
|
|
93
117
|
/** The curl that PUTs a zip to its presigned URL, with the headers the route returned. */
|
|
@@ -115,7 +139,7 @@ export function formatPrepareResult(siteId, urls, size, given = {}) {
|
|
|
115
139
|
const lines = [
|
|
116
140
|
`Upload URLs ready for site ${siteId}.${expiry} Two steps, then call this tool again.`,
|
|
117
141
|
'',
|
|
118
|
-
`1. ${BUILD_ZIP_CONTENTS} Upload the zip you measured${size ? ` (${size} bytes)` : ''}; the URL takes a body of exactly that length, so a zip changed since needs a new prepare with its size:`,
|
|
142
|
+
`1. ${BUILD_ZIP_CONTENTS} Its client folder (\`dist/client\`) holds at most ${MAX_RELEASE_STATIC_FILES} files (\`find dist/client -type f | wc -l\`); a larger build fails the release. Upload the zip you measured${size ? ` (${size} bytes)` : ''}; the URL takes a body of exactly that length, so a zip changed since needs a new prepare with its size:`,
|
|
119
143
|
` ${curlPut('build.zip', urls, zipHeaders)}`
|
|
120
144
|
];
|
|
121
145
|
// The agent's own revision wins: it is the one its project started from. Given, it is echoed
|
|
@@ -127,7 +151,7 @@ export function formatPrepareResult(siteId, urls, size, given = {}) {
|
|
|
127
151
|
secondCall.sourceUploadId = s.uploadId;
|
|
128
152
|
if (!secondCall.expectedHeadRevisionId && s.headRevisionId)
|
|
129
153
|
secondCall.expectedHeadRevisionId = s.headRevisionId;
|
|
130
|
-
lines.push('', `2. ${SOURCE_ZIP_CONTENTS} Upload it:`, ` ${curlPut('source.zip', s, { 'content-type': 'application/zip' })}`, ...(s.expiresAt && s.expiresAt !== urls.expiresAt
|
|
154
|
+
lines.push('', `2. ${SOURCE_ZIP_CONTENTS} At most 64 MiB (\`wc -c < source.zip\`): a larger one fails the whole release. Leave large media and generated files out of it; if it is still larger, skip this upload and leave \`sourceUploadId\` out of the second call, and the site is released without its source. Upload it:`, ` ${curlPut('source.zip', s, { 'content-type': 'application/zip' })}`, ...(s.expiresAt && s.expiresAt !== urls.expiresAt
|
|
131
155
|
? [` This URL expires at ${s.expiresAt}.`]
|
|
132
156
|
: []), ...(s.headRevisionId
|
|
133
157
|
? [
|
|
@@ -141,6 +165,11 @@ export function formatPrepareResult(siteId, urls, size, given = {}) {
|
|
|
141
165
|
if (given.releaseNotes)
|
|
142
166
|
secondCall.releaseNotes = releaseNotesForService(given.releaseNotes);
|
|
143
167
|
lines.push('', `3. Call UploadHeadlessWebsiteFiles again with ${JSON.stringify(secondCall)} (plus \`calledStartDeployWebsite\`, \`agentIdentity\`, \`reason\`). No \`files\` or \`attachments\` on that call.`);
|
|
168
|
+
if (secondCall.sourceUploadId) {
|
|
169
|
+
const withoutSource = { ...secondCall };
|
|
170
|
+
delete withoutSource.sourceUploadId;
|
|
171
|
+
lines.push(` If you skipped the source upload in step 2, call with ${JSON.stringify(withoutSource)} instead: a \`sourceUploadId\` whose zip was never uploaded fails the release.`);
|
|
172
|
+
}
|
|
144
173
|
return lines.join('\n');
|
|
145
174
|
}
|
|
146
175
|
/** The release-bundle reply, for both the two-call form and a build uploaded as files. */
|
|
@@ -161,11 +190,37 @@ const startOver = (retry) => retry === 'prepare'
|
|
|
161
190
|
? "call with `prepare: true` and the zip's `size` again, upload the zips to the new URLs, and release with the new `uploadKey`"
|
|
162
191
|
: 'call this tool again with the same files';
|
|
163
192
|
const apiBody = (err) => err.response.data ? ` (${err.response.data.slice(0, 300)})` : '';
|
|
193
|
+
/**
|
|
194
|
+
* The permission every owner route (drop, the upload URL, the release, the files helpers)
|
|
195
|
+
* checks on the caller for the site. A 403 naming it is about the login, not the upload: the
|
|
196
|
+
* connected Wix account is not the one that owns the site, or its role there is too low.
|
|
197
|
+
*/
|
|
198
|
+
export const SITE_ACCESS_PERMISSION = 'OAUTH_APP.APP_CREATE';
|
|
199
|
+
/**
|
|
200
|
+
* A 403 for the site itself, as the agent should act on it; null for any other error.
|
|
201
|
+
* `stagedKey` is the uploadKey of a build already PUT when the refusal came from the
|
|
202
|
+
* release: that upload is kept, and counts against the site's unreleased uploads.
|
|
203
|
+
*/
|
|
204
|
+
export function describeSiteAccessDenied(err, siteId, stagedKey) {
|
|
205
|
+
if (!(err instanceof WixFetchError) ||
|
|
206
|
+
err.status !== 403 ||
|
|
207
|
+
!err.response.data.includes(SITE_ACCESS_PERMISSION)) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const site = siteId ? `site ${siteId}` : 'this site';
|
|
211
|
+
const outcome = stagedKey
|
|
212
|
+
? `Nothing was released; the build you uploaded is still staged under uploadKey ${stagedKey} and counts against the site's unreleased uploads, so do not call \`prepare\` again and do not send the files again. The same call fails the same way until the login changes; once connected as the owning account, release that staged upload by calling with \`uploadKey: "${stagedKey}"\` before preparing a new one.`
|
|
213
|
+
: 'Nothing was uploaded, and the same call fails the same way, so do not retry and do not stage another upload.';
|
|
214
|
+
return `The connected Wix login has no rights on ${site}: Wix refused it the \`${SITE_ACCESS_PERMISSION}\` permission there. That means the site belongs to another Wix account, or this login's role on the owning account is too low (a Basic Team Member cannot upload). ${outcome} Ask the user which Wix account owns the site. Reconnecting Wix helps only if they pick that account, as its owner or an admin, at the consent screen; otherwise the owner has to give this login a higher role on the account, or move the site to it.${apiBody(err)}`;
|
|
215
|
+
}
|
|
164
216
|
/** The bundle-upload-url route's errors, as the agent should act on them. */
|
|
165
|
-
export function describeBundleUploadUrlError(err) {
|
|
217
|
+
export function describeBundleUploadUrlError(err, siteId) {
|
|
166
218
|
if (!(err instanceof WixFetchError)) {
|
|
167
219
|
return err instanceof Error ? err.message : String(err);
|
|
168
220
|
}
|
|
221
|
+
const denied = describeSiteAccessDenied(err, siteId);
|
|
222
|
+
if (denied)
|
|
223
|
+
return denied;
|
|
169
224
|
const body = apiBody(err);
|
|
170
225
|
switch (err.status) {
|
|
171
226
|
case 400:
|
|
@@ -198,17 +253,33 @@ export function releaseNotesForService(notes) {
|
|
|
198
253
|
.trimEnd()}…`;
|
|
199
254
|
}
|
|
200
255
|
/** The release-bundle route's errors, as the agent should act on them. */
|
|
201
|
-
export function describeBundleReleaseError(err, uploadKey, retry = 'prepare') {
|
|
256
|
+
export function describeBundleReleaseError(err, uploadKey, retry = 'prepare', siteId) {
|
|
202
257
|
if (!(err instanceof WixFetchError)) {
|
|
203
258
|
return err instanceof Error ? err.message : String(err);
|
|
204
259
|
}
|
|
260
|
+
const denied = describeSiteAccessDenied(err, siteId, uploadKey);
|
|
261
|
+
if (denied)
|
|
262
|
+
return denied;
|
|
205
263
|
const body = apiBody(err);
|
|
206
264
|
const code = err.response.data ?? '';
|
|
265
|
+
const sameCall = `calling with \`uploadKey: "${uploadKey}"\`${retry === 'resend' ? ' (plus the `expectedHeadRevisionId` and `releaseNotes` you passed, if any; the source is stored with the next release)' : ' and the same ids and `releaseNotes`'}`;
|
|
266
|
+
// A staged zip is deleted once its release succeeds, and the source it stored moves the
|
|
267
|
+
// repository's head: after a dropped connection, a 404 or a 409 on the same key is the first
|
|
268
|
+
// release having gone through.
|
|
269
|
+
const afterDrop = 'If an earlier call with this `uploadKey` ended in a dropped connection, that release went through: the site is live with this build, so do not upload it again.';
|
|
270
|
+
// The gateway lost the connection or timed out (its own body, not the service's JSON) while
|
|
271
|
+
// the release ran on. The service usually completes it.
|
|
272
|
+
if (err.status >= 502 &&
|
|
273
|
+
err.status <= 504 &&
|
|
274
|
+
!code.includes('RELEASE_NOT_HELD') &&
|
|
275
|
+
!code.includes('{')) {
|
|
276
|
+
return `The connection to Wix dropped while the release ran (HTTP ${err.status}); the release most likely went through. Do not call \`prepare\` again and do not send the files again. Wait two minutes, then check once by ${sameCall}: an answer that nothing is staged for this key, or that the repository moved, means the first release went through and the site is live. Any other answer is about the release itself.${body}`;
|
|
277
|
+
}
|
|
207
278
|
switch (err.status) {
|
|
208
279
|
case 404:
|
|
209
|
-
return `Nothing uploaded for uploadKey ${uploadKey}: the build zip was not PUT to its URL,
|
|
280
|
+
return `Nothing uploaded for uploadKey ${uploadKey}: the build zip was not PUT to its URL, the URL expired, or a release of it already went through (a staged zip is deleted once released). ${afterDrop} Otherwise, to try again, ${startOver(retry)}.${body}`;
|
|
210
281
|
case 409:
|
|
211
|
-
return `The project's source repository moved since the upload URLs were made; nothing was released. ${retry === 'prepare' ? "
|
|
282
|
+
return `The project's source repository moved since the upload URLs were made; nothing was released by this call. ${afterDrop} Otherwise: ${retry === 'prepare' ? "call with `prepare: true` and the zip's `size` again, re-upload the source zip to the new URL, and release with the new `uploadKey`, `sourceUploadId` and `expectedHeadRevisionId`." : 'download the project again from the repository, carry your change over, and upload with `expectedHeadRevisionId` set to the revision you downloaded.'}${body}`;
|
|
212
283
|
case 403:
|
|
213
284
|
return `Wix refused to deploy this build onto the site's project; nothing was released, and the same call will be refused again. Do not call \`prepare\` again. Tell the user the release is blocked on Wix's side and give them the request id from the error.${body}`;
|
|
214
285
|
case 413:
|
|
@@ -230,13 +301,18 @@ export function describeBundleReleaseError(err, uploadKey, retry = 'prepare') {
|
|
|
230
301
|
return `The uploaded zip was not accepted. Zip \`dist/\` and \`.wix/build-metadata.json\` from the project root after \`astro build\` with the Wix hosting adapter, then ${startOver(retry)}. A static site goes in \`files\` or \`attachments\` instead, or is dropped as the StartDeployWebsite recipe shows.${body}`;
|
|
231
302
|
default:
|
|
232
303
|
if (err.status >= 500) {
|
|
233
|
-
const what =
|
|
304
|
+
const what = code.includes('RELEASE_NOT_HELD')
|
|
234
305
|
? 'The release could not hold its upload'
|
|
235
306
|
: `Wix's release step failed on its side (HTTP ${err.status})`;
|
|
307
|
+
// The two-call form's zips are the agent's own, so they can be over the limits the
|
|
308
|
+
// service reports only as a 500; the one-call form's upload is capped below them.
|
|
309
|
+
const limits = retry === 'prepare' && !code.includes('RELEASE_NOT_HELD')
|
|
310
|
+
? ` A build over Wix's limits fails here with no reason given: more than ${MAX_RELEASE_STATIC_FILES} files in its client folder (\`find dist/client -type f | wc -l\`), or a source zip over 64 MiB (\`wc -c < source.zip\`). If either is over, a retry fails the same way: make the build or the source zip smaller, then call with \`prepare: true\` again.`
|
|
311
|
+
: '';
|
|
236
312
|
// Both forms retry the staged upload itself: the two-call form with its uploadKey, the
|
|
237
313
|
// one-call form by switching to that key instead of sending the files again, which would
|
|
238
314
|
// stage another upload. One retry, then the user hears the request id.
|
|
239
|
-
return `${what}; nothing was released
|
|
315
|
+
return `${what}; nothing was released.${limits} The staged upload is kept under uploadKey ${uploadKey}, so do not call \`prepare\` again${limits ? ' for the same zips' : ''} and do not send the files again: every upload staged and not released counts against the site, which refuses new ones after a few. Retry once, after a minute, by ${sameCall}. If that fails too, stop and tell the user the release is blocked on Wix's side, with the request id from the error.${body}`;
|
|
240
316
|
}
|
|
241
317
|
return err.message;
|
|
242
318
|
}
|
|
@@ -358,6 +434,12 @@ export function addUploadHeadlessWebsiteFilesTool(server, options) {
|
|
|
358
434
|
server.clientName?.() ??
|
|
359
435
|
server.server?.getClientVersion?.()?.name;
|
|
360
436
|
const fetchImpl = options.fetchImpl ?? ((u, i) => fetch(u, i));
|
|
437
|
+
const guideFetch = options.guideFetch ??
|
|
438
|
+
(async (httpClient) => {
|
|
439
|
+
if (!httpClient)
|
|
440
|
+
return undefined;
|
|
441
|
+
return fetchArticleContent(httpClient, HEADLESS_GUIDE_URL, 'article', true, options.guide?.skillsPortalUrl, options.guide?.repo, options.guide?.commitHash);
|
|
442
|
+
});
|
|
361
443
|
const d = UPLOAD_HEADLESS_WEBSITE_FILES_PARAM_DESCRIPTIONS;
|
|
362
444
|
const registered = server.tool(UPLOAD_HEADLESS_WEBSITE_FILES_TOOL_NAME, toolDescriptions?.[UPLOAD_HEADLESS_WEBSITE_FILES_TOOL_NAME] ??
|
|
363
445
|
defaultToolDescriptions.UploadHeadlessWebsiteFiles, {
|
|
@@ -474,8 +556,8 @@ export function addUploadHeadlessWebsiteFilesTool(server, options) {
|
|
|
474
556
|
rethrowIfUnauthorized(err);
|
|
475
557
|
await authStrategy.enforceValidWixToken?.(err);
|
|
476
558
|
return errorResult(uploadKey
|
|
477
|
-
? describeBundleReleaseError(err, uploadKey)
|
|
478
|
-
: describeBundleUploadUrlError(err));
|
|
559
|
+
? describeBundleReleaseError(err, uploadKey, 'prepare', siteId)
|
|
560
|
+
: describeBundleUploadUrlError(err, siteId));
|
|
479
561
|
}
|
|
480
562
|
}
|
|
481
563
|
if (!files?.trim() && !(attachments && attachments.length)) {
|
|
@@ -571,8 +653,8 @@ export function addUploadHeadlessWebsiteFilesTool(server, options) {
|
|
|
571
653
|
rethrowIfUnauthorized(err);
|
|
572
654
|
await authStrategy.enforceValidWixToken?.(err);
|
|
573
655
|
return errorResult(stagedKey
|
|
574
|
-
? describeBundleReleaseError(err, stagedKey, 'resend')
|
|
575
|
-
: describeBundleUploadUrlError(err));
|
|
656
|
+
? describeBundleReleaseError(err, stagedKey, 'resend', siteId)
|
|
657
|
+
: describeBundleUploadUrlError(err, siteId));
|
|
576
658
|
}
|
|
577
659
|
}
|
|
578
660
|
// 2. Account auth (outside the try so UnauthorizedError reaches the wrapper).
|
|
@@ -590,6 +672,15 @@ export function addUploadHeadlessWebsiteFilesTool(server, options) {
|
|
|
590
672
|
siteId: metaSiteId,
|
|
591
673
|
upload: { path: 'drop', files: siteFiles.length }
|
|
592
674
|
});
|
|
675
|
+
// The headless guide rides along, inline: a static drop is not the build of a site
|
|
676
|
+
// that needs Wix business features, and the agent that stops here has read nothing else.
|
|
677
|
+
let guide;
|
|
678
|
+
try {
|
|
679
|
+
guide = await guideFetch(extra?.httpClient);
|
|
680
|
+
}
|
|
681
|
+
catch (e) {
|
|
682
|
+
logger.log(`[UploadHeadlessWebsiteFiles] guide fetch failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
683
|
+
}
|
|
593
684
|
return {
|
|
594
685
|
content: [
|
|
595
686
|
{
|
|
@@ -602,7 +693,8 @@ export function addUploadHeadlessWebsiteFilesTool(server, options) {
|
|
|
602
693
|
}) +
|
|
603
694
|
(releaseNotes
|
|
604
695
|
? '\n\nRelease notes are kept for a Wix Astro build only; a static drop has none, so yours were not stored.'
|
|
605
|
-
: '')
|
|
696
|
+
: '') +
|
|
697
|
+
formatInlineGuide(guide)
|
|
606
698
|
}
|
|
607
699
|
]
|
|
608
700
|
};
|
|
@@ -612,7 +704,7 @@ export function addUploadHeadlessWebsiteFilesTool(server, options) {
|
|
|
612
704
|
logger.log(`[UploadHeadlessWebsiteFiles] Error: ${message}`);
|
|
613
705
|
rethrowIfUnauthorized(err);
|
|
614
706
|
await authStrategy.enforceValidWixToken?.(err);
|
|
615
|
-
return errorResult(message);
|
|
707
|
+
return errorResult(describeSiteAccessDenied(err, siteId) ?? message);
|
|
616
708
|
}
|
|
617
709
|
});
|
|
618
710
|
// ChatGPT resolves `attachments` into download URLs only for params listed
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Chat file attachments for ExecuteWixAPI (`openai/fileParams`).
|
|
3
3
|
*
|
|
4
4
|
* ChatGPT resolves a file the user attached — or one the model wrote in its
|
|
5
5
|
* own sandbox — into `{ download_url, file_id, file_name?, mime_type? }` for a
|
|
6
|
-
* param the tool lists under `_meta["openai/fileParams"]`.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* param the tool lists under `_meta["openai/fileParams"]`. For ExecuteWixAPI the
|
|
7
|
+
* Wix AI Assistant (Aria) does the same by swapping an `attachment://<id>`
|
|
8
|
+
* reference in the tool args for a signed URL on its own Wix storage. The
|
|
9
|
+
* bytes never go through the model. We fetch each `download_url` host-side and
|
|
10
|
+
* hand the runtime ready-made base64 file entries, the same shape the `files` bundle
|
|
9
11
|
* parses into, so `files`, `wix.multipart()` and `wix.unzip()` are unchanged.
|
|
10
12
|
*
|
|
11
13
|
* This is additive: clients without the extension never populate the param
|
|
@@ -24,7 +26,8 @@ export interface AttachmentFile {
|
|
|
24
26
|
encoding: 'base64';
|
|
25
27
|
}
|
|
26
28
|
export declare const MAX_ATTACHMENTS_BYTES: number;
|
|
27
|
-
export declare
|
|
29
|
+
export declare const WIX_AI_ASSISTANT_ATTACHMENT_HOSTS: ReadonlySet<string>;
|
|
30
|
+
export declare function isAllowedAttachmentHost(hostname: string, extraHosts?: ReadonlySet<string>): boolean;
|
|
28
31
|
export declare class AttachmentError extends Error {
|
|
29
32
|
}
|
|
30
33
|
/**
|
|
@@ -33,5 +36,5 @@ export declare class AttachmentError extends Error {
|
|
|
33
36
|
* (ChatGPT passed a sandbox path or a bare id instead of a URL), a host we
|
|
34
37
|
* don't trust, a failed download, or more bytes than MAX_ATTACHMENTS_BYTES.
|
|
35
38
|
*/
|
|
36
|
-
export declare function resolveAttachments(attachments: unknown, fetchImpl?: typeof fetch): Promise<AttachmentFile[]>;
|
|
39
|
+
export declare function resolveAttachments(attachments: unknown, fetchImpl?: typeof fetch, extraHosts?: ReadonlySet<string>): Promise<AttachmentFile[]>;
|
|
37
40
|
//# sourceMappingURL=attachments.d.ts.map
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Chat file attachments for ExecuteWixAPI (`openai/fileParams`).
|
|
3
3
|
*
|
|
4
4
|
* ChatGPT resolves a file the user attached — or one the model wrote in its
|
|
5
5
|
* own sandbox — into `{ download_url, file_id, file_name?, mime_type? }` for a
|
|
6
|
-
* param the tool lists under `_meta["openai/fileParams"]`.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* param the tool lists under `_meta["openai/fileParams"]`. For ExecuteWixAPI the
|
|
7
|
+
* Wix AI Assistant (Aria) does the same by swapping an `attachment://<id>`
|
|
8
|
+
* reference in the tool args for a signed URL on its own Wix storage. The
|
|
9
|
+
* bytes never go through the model. We fetch each `download_url` host-side and
|
|
10
|
+
* hand the runtime ready-made base64 file entries, the same shape the `files` bundle
|
|
9
11
|
* parses into, so `files`, `wix.multipart()` and `wix.unzip()` are unchanged.
|
|
10
12
|
*
|
|
11
13
|
* This is additive: clients without the extension never populate the param
|
|
@@ -26,7 +28,7 @@ const MAX_REDIRECTS = 4;
|
|
|
26
28
|
// - Azure blob accounts behind them, one per OpenAI region.
|
|
27
29
|
// `blob.core.windows.net` is a shared namespace, so these are exact — a
|
|
28
30
|
// prefix could be registered by anyone. A new region shows up as an
|
|
29
|
-
// "is not a
|
|
31
|
+
// "is not a chat attachment host" error in BI; add it here.
|
|
30
32
|
const OAI_USERCONTENT_HOST = /^[a-z0-9-]+\.oaiusercontent\.com$/;
|
|
31
33
|
const OAI_BLOB_ACCOUNTS = new Set([
|
|
32
34
|
'australiaeast',
|
|
@@ -55,18 +57,23 @@ const OAI_BLOB_ACCOUNTS = new Set([
|
|
|
55
57
|
'westus2',
|
|
56
58
|
'westus3'
|
|
57
59
|
].map((region) => `oaisdmntpr${region}.blob.core.windows.net`));
|
|
58
|
-
export
|
|
60
|
+
export const WIX_AI_ASSISTANT_ATTACHMENT_HOSTS = new Set([
|
|
61
|
+
'wixmp-a85a29aaf55baa3e5171a82d.wixmp.com'
|
|
62
|
+
]);
|
|
63
|
+
export function isAllowedAttachmentHost(hostname, extraHosts) {
|
|
59
64
|
const h = hostname.toLowerCase();
|
|
60
|
-
return OAI_USERCONTENT_HOST.test(h) ||
|
|
65
|
+
return (OAI_USERCONTENT_HOST.test(h) ||
|
|
66
|
+
OAI_BLOB_ACCOUNTS.has(h) ||
|
|
67
|
+
!!extraHosts?.has(h));
|
|
61
68
|
}
|
|
62
69
|
/** https, default port, allowed host — applied to the first URL and every redirect. */
|
|
63
|
-
function checkAttachmentUrl(url) {
|
|
70
|
+
function checkAttachmentUrl(url, extraHosts) {
|
|
64
71
|
if (url.protocol !== 'https:')
|
|
65
72
|
return 'is not an https URL';
|
|
66
73
|
if (url.port)
|
|
67
74
|
return 'uses a non-default port';
|
|
68
|
-
if (!isAllowedAttachmentHost(url.hostname)) {
|
|
69
|
-
return `host "${url.hostname}" is not a
|
|
75
|
+
if (!isAllowedAttachmentHost(url.hostname, extraHosts)) {
|
|
76
|
+
return `host "${url.hostname}" is not a chat attachment host`;
|
|
70
77
|
}
|
|
71
78
|
return undefined;
|
|
72
79
|
}
|
|
@@ -98,7 +105,7 @@ function toBase64(bytes) {
|
|
|
98
105
|
* GET with manual redirects: every hop must pass checkAttachmentUrl, so an
|
|
99
106
|
* allowed host cannot bounce the server to an arbitrary or internal one.
|
|
100
107
|
*/
|
|
101
|
-
async function fetchAllowed(start, fetchImpl, label) {
|
|
108
|
+
async function fetchAllowed(start, fetchImpl, label, extraHosts) {
|
|
102
109
|
let url = start;
|
|
103
110
|
for (let hop = 0;; hop++) {
|
|
104
111
|
const res = await fetchImpl(url.toString(), { redirect: 'manual' });
|
|
@@ -117,7 +124,7 @@ async function fetchAllowed(start, fetchImpl, label) {
|
|
|
117
124
|
catch {
|
|
118
125
|
throw new AttachmentError(`${label}: redirect to an invalid URL.`);
|
|
119
126
|
}
|
|
120
|
-
const problem = checkAttachmentUrl(next);
|
|
127
|
+
const problem = checkAttachmentUrl(next, extraHosts);
|
|
121
128
|
if (problem) {
|
|
122
129
|
throw new AttachmentError(`${label}: the download redirected to a URL that ${problem}; refused.`);
|
|
123
130
|
}
|
|
@@ -166,7 +173,7 @@ async function readWithin(res, budget, overBudget) {
|
|
|
166
173
|
* (ChatGPT passed a sandbox path or a bare id instead of a URL), a host we
|
|
167
174
|
* don't trust, a failed download, or more bytes than MAX_ATTACHMENTS_BYTES.
|
|
168
175
|
*/
|
|
169
|
-
export async function resolveAttachments(attachments, fetchImpl = fetch) {
|
|
176
|
+
export async function resolveAttachments(attachments, fetchImpl = fetch, extraHosts) {
|
|
170
177
|
if (!Array.isArray(attachments) || attachments.length === 0)
|
|
171
178
|
return [];
|
|
172
179
|
const items = [];
|
|
@@ -185,9 +192,9 @@ export async function resolveAttachments(attachments, fetchImpl = fetch) {
|
|
|
185
192
|
if (!a || !url) {
|
|
186
193
|
throw new AttachmentError(`attachments[${i}] was not resolved to a download URL (got ${JSON.stringify(typeof raw === 'string' ? raw : ((a && a.download_url) ?? raw))}). \`attachments\` works only in ChatGPT, which swaps each file for a download URL before the call reaches Wix. Here it arrived as written, and Wix cannot read a path on your machine, so retrying the same value fails the same way. Upload another way. With a shell that reaches the internet, send the file from there (for a site, the StartDeployWebsite recipe's curl drop). Otherwise leave \`attachments\` out and put the files in \`files\`: text as is, and a binary as a \`=== FILE: <path> base64 ===\` entry only when it is a few KB (an icon, a logo), since a larger base64 payload does not survive your own output. A larger image goes in by public URL or through the user (the Media Manager).`);
|
|
187
194
|
}
|
|
188
|
-
const problem = checkAttachmentUrl(url);
|
|
195
|
+
const problem = checkAttachmentUrl(url, extraHosts);
|
|
189
196
|
if (problem) {
|
|
190
|
-
throw new AttachmentError(`attachments[${i}].download_url ${problem}.
|
|
197
|
+
throw new AttachmentError(`attachments[${i}].download_url ${problem}. \`attachments\` takes only files the chat platform attached and resolved; leave it out. For a file at a URL, import it in \`code\` with \`POST https://www.wixapis.com/site-media/v1/files/import\` and \`{ url }\` (Wix fetches it); for a small file you have as text, use the \`files\` bundle.`);
|
|
191
198
|
}
|
|
192
199
|
items.push({ a, url });
|
|
193
200
|
});
|
|
@@ -198,7 +205,7 @@ export async function resolveAttachments(attachments, fetchImpl = fetch) {
|
|
|
198
205
|
const label = `attachments[${i}] (${path})`;
|
|
199
206
|
let res;
|
|
200
207
|
try {
|
|
201
|
-
res = await fetchAllowed(url, fetchImpl, label);
|
|
208
|
+
res = await fetchAllowed(url, fetchImpl, label, extraHosts);
|
|
202
209
|
}
|
|
203
210
|
catch (e) {
|
|
204
211
|
if (e instanceof AttachmentError)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { AttachmentError, resolveAttachments } from './attachments.js';
|
|
2
|
+
import { AttachmentError, WIX_AI_ASSISTANT_ATTACHMENT_HOSTS, resolveAttachments } from './attachments.js';
|
|
3
3
|
import { fileParamNote } from '../config/file-params.js';
|
|
4
4
|
import { logger } from '../infra/logger.js';
|
|
5
5
|
import { defaultToolDescriptions } from '../config/default-config.js';
|
|
@@ -328,7 +328,7 @@ function addExecuteTool(server, options) {
|
|
|
328
328
|
// an agent-facing error, not a server failure.
|
|
329
329
|
let attachmentFiles = [];
|
|
330
330
|
try {
|
|
331
|
-
attachmentFiles = await resolveAttachments(attachments);
|
|
331
|
+
attachmentFiles = await resolveAttachments(attachments, undefined, WIX_AI_ASSISTANT_ATTACHMENT_HOSTS);
|
|
332
332
|
}
|
|
333
333
|
catch (e) {
|
|
334
334
|
if (e instanceof AttachmentError)
|
|
@@ -41,7 +41,7 @@ export declare const HELP_CENTER_IS_FOR_PEOPLE = "The Help Center is written for
|
|
|
41
41
|
* Each tool named here carries a comment pointing back to this text. A change to any of them (a
|
|
42
42
|
* name, what it is for, a new site-creation tool) updates this text and that list with it.
|
|
43
43
|
*/
|
|
44
|
-
export declare const SERVER_INSTRUCTIONS = "Wix: build and manage Wix sites. A new site is written in code and uploaded: StartDeployWebsite returns the upload guide; StartHeadlessBuild returns the headless guide, for a site that needs Wix business features (forms, a store, bookings, a CMS, members, payments).
|
|
44
|
+
export declare const SERVER_INSTRUCTIONS = "Wix: build and manage Wix sites. A new site is written in code and uploaded: StartDeployWebsite returns the upload guide; StartHeadlessBuild returns the headless guide, for a site that needs Wix business features (forms, a store, bookings, a CMS, members, payments). WixHarmonyEditorBuild or SearchSiteTemplates only when the user asked for Wix's editor, the Wix Harmony Editor, a template, Wix Studio or the Classic Editor. Call WixREADME first, except ListWixSites, UploadImageToWixSite, GetSuggestedDomains.";
|
|
45
45
|
/** The tools SERVER_INSTRUCTIONS names. */
|
|
46
46
|
export declare const SERVER_INSTRUCTIONS_TOOLS: string[];
|
|
47
47
|
export declare const SITE_CREATION_COPY: {
|