sbuilder-mcp 0.87.0 → 0.88.1
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/CHANGELOG.md +20 -0
- package/CHANGELOG.vi.md +20 -0
- package/README.md +1 -1
- package/README.vi.md +1 -1
- package/dist/catalog/api.generated.js +125 -4
- package/dist/catalog/deadkeys.generated.js +2 -2
- package/dist/catalog/shapes.generated.js +56 -4
- package/dist/catalog/source.generated.js +2 -2
- package/dist/domains/site/readiness.js +7 -6
- package/dist/domains/site/review.js +18 -0
- package/dist/domains/site/seedcopy.js +28 -0
- package/dist/domains/site/storepage.js +26 -3
- package/dist/tools/live.js +3 -1
- package/dist/tools/page.js +25 -5
- package/dist/transport/auth.js +66 -7
- package/dist/transport/http.js +15 -1
- package/dist/transport/media.js +5 -0
- package/dist/transport/socket.js +16 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,26 @@ All notable changes to this project are documented in this file.
|
|
|
6
6
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.88.1] - 2026-10-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- The API catalog is refreshed to 853 operations, including device sign-in for the CLI; the device approval operation cannot be called with a session.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Every tool call keeps working after the access token expires, because a rejected session token is refreshed once and the call is retried once; API keys are never refreshed.
|
|
16
|
+
- Concurrent rejections share a single token refresh, and signing out or back in during a refresh is no longer overwritten.
|
|
17
|
+
- `sb_media_upload` and `sb_live_join` reconnects refresh the token before it expires, so uploads and live sessions survive past the token lifetime.
|
|
18
|
+
|
|
19
|
+
## [0.88.0] - 2026-10-12
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `sb_review` reports `default_seed_copy` for pages built earlier that still carry the English seed copy, such as "Add To Cart", on a Vietnamese site, with the Vietnamese wording to use instead.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `sb_page_create` writes the store page seeds in Vietnamese for a Vietnamese site, taking the locale from the site's settings when none is passed.
|
|
26
|
+
- `sb_page_create` uses English thank-you copy on the completed-order page of an `en-US` site.
|
|
27
|
+
- `sb_review` words its `categoryImages` note to say that a collection tile without an image now shows a placeholder instead of a broken image.
|
|
28
|
+
|
|
9
29
|
## [0.87.0] - 2026-10-12
|
|
10
30
|
|
|
11
31
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,26 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
|
|
|
6
6
|
Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.88.1] - 2026-10-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Catalog API được làm mới lên 853 operation, gồm đăng nhập bằng thiết bị cho CLI; operation duyệt thiết bị không gọi được bằng phiên đăng nhập.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Mọi lời gọi tool tiếp tục chạy sau khi access token hết hạn, vì token phiên bị từ chối sẽ được làm mới một lần và lời gọi được thử lại một lần; API key không bao giờ bị làm mới.
|
|
16
|
+
- Các lần bị từ chối đồng thời dùng chung một lượt làm mới token, và việc đăng xuất hoặc đăng nhập lại trong lúc làm mới không còn bị ghi đè.
|
|
17
|
+
- `sb_media_upload` và việc kết nối lại của `sb_live_join` làm mới token trước khi hết hạn, nên upload và phiên live vẫn chạy được sau thời hạn của token.
|
|
18
|
+
|
|
19
|
+
## [0.88.0] - 2026-10-12
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `sb_review` báo `default_seed_copy` cho trang dựng trước đó còn mang chữ mẫu tiếng Anh, như "Add To Cart", trên site tiếng Việt, kèm bản tiếng Việt nên dùng.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `sb_page_create` viết seed trang cửa hàng bằng tiếng Việt cho site tiếng Việt, lấy locale từ cài đặt của site khi không truyền.
|
|
26
|
+
- `sb_page_create` dùng câu cảm ơn tiếng Anh cho trang hoàn tất đơn hàng của site `en-US`.
|
|
27
|
+
- `sb_review` sửa câu báo `categoryImages`: ô danh mục thiếu ảnh giờ hiện placeholder thay vì ảnh vỡ.
|
|
28
|
+
|
|
9
29
|
## [0.87.0] - 2026-10-12
|
|
10
30
|
|
|
11
31
|
### Added
|
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ in this client: one would put the very key the platform exists to hold back into
|
|
|
118
118
|
| `sb_store` | Run a store flow that must happen in a fixed order — `checkout` (the four writes that make a working one), `checkout_sync` (the store's current payment and delivery methods into every existing order form, checkout page republished), `form` (any of the platform's 17 templates with its own field document), `chrome` (one shared header or footer on a real site menu — desktop menu, mobile drawer, cart and account icons), `menu` (a menu node bound to the site's menu, its links resolved), `overlay_attach` (a pop-up or quick view on the open page), `cart` (the site's cart drawer, when it has none; `relocalize` puts an existing one in the site's language) and `app` (a built-in app plus the pages it needs), `global_attach` / `global_detach` (put an EXISTING shared section on the open page, or take it off), `global_promote` (make a section on the page a NEW shared section) and `template_save` (save a section as a reusable template) |
|
|
119
119
|
| `sb_undo` | Undo / redo this session's writes — page and form edits and `sb_api_call` PUTs; a page is refused if someone else saved it since. The platform's versions and history (`sb_page_version`) outlive this process |
|
|
120
120
|
|
|
121
|
-
Twenty-eight tools, **
|
|
121
|
+
Twenty-eight tools, **853 API operations** (316 of the 397 writes carrying a body shape read
|
|
122
122
|
off the handler), 123 elements, 94 binding sources. `sb_api_find`
|
|
123
123
|
is an index rather than a tool per endpoint, so the tool list stays short while everything
|
|
124
124
|
the platform can do stays reachable — and operations added to the platform arrive with the
|
package/README.vi.md
CHANGED
|
@@ -115,7 +115,7 @@ nhét ngược lại vào mọi bản cài.
|
|
|
115
115
|
| `sb_store` | Chạy một luồng cửa hàng bắt buộc đúng thứ tự — `checkout` (bốn lệnh ghi tạo nên trang thanh toán), `checkout_sync` (đưa phương thức thanh toán và giao hàng hiện tại vào mọi form đặt hàng đã có, publish lại trang thanh toán), `form` (một trong 17 template của nền tảng kèm field document của nó), `chrome` (một header hoặc footer dùng chung trên menu thật của site — menu desktop, drawer mobile, icon giỏ hàng và tài khoản), `menu` (một node menu bind vào menu của site, link đã phân giải), `overlay_attach` (một pop-up hay quick view trên trang đang mở), `cart` (ngăn giỏ hàng của site, khi chưa có; `relocalize` đưa ngăn giỏ đã có về ngôn ngữ của site), `app` (một app dựng sẵn kèm những trang nó cần), và `global_attach` / `global_detach` (đặt một section dùng chung ĐÃ CÓ lên trang đang mở, hoặc gỡ ra), `global_promote` (biến một section trên trang thành section dùng chung MỚI) và `template_save` (lưu một section làm mẫu dùng lại) |
|
|
116
116
|
| `sb_undo` | Hoàn tác / làm lại các thao tác ghi trong phiên — chỉnh sửa trang, form và PUT qua `sb_api_call`; trang bị từ chối nếu người khác đã lưu sau đó. Version và history của nền tảng (`sb_page_version`) sống lâu hơn process này |
|
|
117
117
|
|
|
118
|
-
Hai mươi tám tool, **
|
|
118
|
+
Hai mươi tám tool, **853 operation API** (316 trong 397 lệnh ghi có hình dạng body đọc thẳng
|
|
119
119
|
từ handler), 123 element, 94 nguồn binding. `sb_api_find` là
|
|
120
120
|
một chỉ mục chứ không phải mỗi endpoint một tool, nên danh sách tool vẫn ngắn trong khi mọi
|
|
121
121
|
thứ nền tảng làm được vẫn với tới — và operation mới thêm bên nền tảng sẽ tự có sau lần
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const SWAGGER_SOURCE = {
|
|
2
|
-
"operations":
|
|
3
|
-
"definitions":
|
|
4
|
-
"bodyCarrying":
|
|
5
|
-
"bodyUndescribed":
|
|
2
|
+
"operations": 853,
|
|
3
|
+
"definitions": 282,
|
|
4
|
+
"bodyCarrying": 380,
|
|
5
|
+
"bodyUndescribed": 106,
|
|
6
6
|
"generatedFrom": "server/docs/swagger.json"
|
|
7
7
|
};
|
|
8
8
|
export const API_OPERATIONS = [
|
|
@@ -1900,6 +1900,90 @@ export const API_OPERATIONS = [
|
|
|
1900
1900
|
"bodyRef": null,
|
|
1901
1901
|
"credential": "none"
|
|
1902
1902
|
},
|
|
1903
|
+
{
|
|
1904
|
+
"id": "post:/api/auth/device/approve",
|
|
1905
|
+
"method": "POST",
|
|
1906
|
+
"path": "/api/auth/device/approve",
|
|
1907
|
+
"tags": [
|
|
1908
|
+
"auth"
|
|
1909
|
+
],
|
|
1910
|
+
"summary": "Approve or deny a pending device login",
|
|
1911
|
+
"params": [
|
|
1912
|
+
{
|
|
1913
|
+
"name": "body",
|
|
1914
|
+
"in": "body",
|
|
1915
|
+
"required": true,
|
|
1916
|
+
"type": "object",
|
|
1917
|
+
"description": "userCode + decision (approve|deny)"
|
|
1918
|
+
}
|
|
1919
|
+
],
|
|
1920
|
+
"bodyDescribed": false,
|
|
1921
|
+
"bodyRef": null,
|
|
1922
|
+
"credential": "none"
|
|
1923
|
+
},
|
|
1924
|
+
{
|
|
1925
|
+
"id": "post:/api/auth/device/inspect",
|
|
1926
|
+
"method": "POST",
|
|
1927
|
+
"path": "/api/auth/device/inspect",
|
|
1928
|
+
"tags": [
|
|
1929
|
+
"auth"
|
|
1930
|
+
],
|
|
1931
|
+
"summary": "Describe a pending device login by its typed user code",
|
|
1932
|
+
"params": [
|
|
1933
|
+
{
|
|
1934
|
+
"name": "body",
|
|
1935
|
+
"in": "body",
|
|
1936
|
+
"required": true,
|
|
1937
|
+
"type": "object",
|
|
1938
|
+
"description": "userCode"
|
|
1939
|
+
}
|
|
1940
|
+
],
|
|
1941
|
+
"bodyDescribed": false,
|
|
1942
|
+
"bodyRef": null,
|
|
1943
|
+
"credential": "none"
|
|
1944
|
+
},
|
|
1945
|
+
{
|
|
1946
|
+
"id": "post:/api/auth/device/start",
|
|
1947
|
+
"method": "POST",
|
|
1948
|
+
"path": "/api/auth/device/start",
|
|
1949
|
+
"tags": [
|
|
1950
|
+
"auth"
|
|
1951
|
+
],
|
|
1952
|
+
"summary": "Start a browser device login (sb CLI)",
|
|
1953
|
+
"params": [
|
|
1954
|
+
{
|
|
1955
|
+
"name": "body",
|
|
1956
|
+
"in": "body",
|
|
1957
|
+
"required": true,
|
|
1958
|
+
"type": "object",
|
|
1959
|
+
"description": "client + clientVersion"
|
|
1960
|
+
}
|
|
1961
|
+
],
|
|
1962
|
+
"bodyDescribed": false,
|
|
1963
|
+
"bodyRef": null,
|
|
1964
|
+
"credential": "none"
|
|
1965
|
+
},
|
|
1966
|
+
{
|
|
1967
|
+
"id": "post:/api/auth/device/token",
|
|
1968
|
+
"method": "POST",
|
|
1969
|
+
"path": "/api/auth/device/token",
|
|
1970
|
+
"tags": [
|
|
1971
|
+
"auth"
|
|
1972
|
+
],
|
|
1973
|
+
"summary": "Poll a device login; returns a token pair once approved",
|
|
1974
|
+
"params": [
|
|
1975
|
+
{
|
|
1976
|
+
"name": "body",
|
|
1977
|
+
"in": "body",
|
|
1978
|
+
"required": true,
|
|
1979
|
+
"type": "object",
|
|
1980
|
+
"description": "deviceCode"
|
|
1981
|
+
}
|
|
1982
|
+
],
|
|
1983
|
+
"bodyDescribed": false,
|
|
1984
|
+
"bodyRef": null,
|
|
1985
|
+
"credential": "none"
|
|
1986
|
+
},
|
|
1903
1987
|
{
|
|
1904
1988
|
"id": "post:/api/auth/forgot-password",
|
|
1905
1989
|
"method": "POST",
|
|
@@ -27046,6 +27130,43 @@ export const API_OPERATIONS = [
|
|
|
27046
27130
|
}
|
|
27047
27131
|
];
|
|
27048
27132
|
export const API_DEFINITIONS = {
|
|
27133
|
+
"github_com_webbuilder_server_internal_auth.DeviceRequest": {
|
|
27134
|
+
"type": "object",
|
|
27135
|
+
"properties": {
|
|
27136
|
+
"client": {
|
|
27137
|
+
"type": "string"
|
|
27138
|
+
},
|
|
27139
|
+
"clientVersion": {
|
|
27140
|
+
"type": "string"
|
|
27141
|
+
},
|
|
27142
|
+
"createdAt": {
|
|
27143
|
+
"type": "string"
|
|
27144
|
+
},
|
|
27145
|
+
"expiresAt": {
|
|
27146
|
+
"type": "string"
|
|
27147
|
+
}
|
|
27148
|
+
}
|
|
27149
|
+
},
|
|
27150
|
+
"github_com_webbuilder_server_internal_auth.DeviceStart": {
|
|
27151
|
+
"type": "object",
|
|
27152
|
+
"properties": {
|
|
27153
|
+
"deviceCode": {
|
|
27154
|
+
"type": "string"
|
|
27155
|
+
},
|
|
27156
|
+
"expiresIn": {
|
|
27157
|
+
"type": "integer"
|
|
27158
|
+
},
|
|
27159
|
+
"interval": {
|
|
27160
|
+
"type": "integer"
|
|
27161
|
+
},
|
|
27162
|
+
"userCode": {
|
|
27163
|
+
"type": "string"
|
|
27164
|
+
},
|
|
27165
|
+
"verificationUri": {
|
|
27166
|
+
"type": "string"
|
|
27167
|
+
}
|
|
27168
|
+
}
|
|
27169
|
+
},
|
|
27049
27170
|
"github_com_webbuilder_server_internal_codefiles.CodeFile": {
|
|
27050
27171
|
"type": "object",
|
|
27051
27172
|
"properties": {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export const SHAPE_SOURCE = {
|
|
2
|
-
"writeOperations":
|
|
3
|
-
"shaped":
|
|
4
|
-
"fromHandlers":
|
|
2
|
+
"writeOperations": 397,
|
|
3
|
+
"shaped": 316,
|
|
4
|
+
"fromHandlers": 306,
|
|
5
5
|
"fromSwaggerOnly": 10,
|
|
6
6
|
"withReadOnly": 30,
|
|
7
|
-
"structsRead":
|
|
7
|
+
"structsRead": 3237
|
|
8
8
|
};
|
|
9
9
|
export const REQUEST_SHAPES = {
|
|
10
10
|
"post:/api/sites/{id}/ai-credits/purchase": {
|
|
@@ -656,6 +656,58 @@ export const REQUEST_SHAPES = {
|
|
|
656
656
|
"source": "go",
|
|
657
657
|
"goType": "(inline)"
|
|
658
658
|
},
|
|
659
|
+
"post:/api/auth/device/start": {
|
|
660
|
+
"fields": [
|
|
661
|
+
{
|
|
662
|
+
"name": "client",
|
|
663
|
+
"type": "string"
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
"name": "clientVersion",
|
|
667
|
+
"type": "string"
|
|
668
|
+
}
|
|
669
|
+
],
|
|
670
|
+
"source": "go",
|
|
671
|
+
"goType": "(inline)"
|
|
672
|
+
},
|
|
673
|
+
"post:/api/auth/device/inspect": {
|
|
674
|
+
"fields": [
|
|
675
|
+
{
|
|
676
|
+
"name": "userCode",
|
|
677
|
+
"type": "string"
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
"name": "decision",
|
|
681
|
+
"type": "string"
|
|
682
|
+
}
|
|
683
|
+
],
|
|
684
|
+
"source": "go",
|
|
685
|
+
"goType": "deviceDecision"
|
|
686
|
+
},
|
|
687
|
+
"post:/api/auth/device/approve": {
|
|
688
|
+
"fields": [
|
|
689
|
+
{
|
|
690
|
+
"name": "userCode",
|
|
691
|
+
"type": "string"
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
"name": "decision",
|
|
695
|
+
"type": "string"
|
|
696
|
+
}
|
|
697
|
+
],
|
|
698
|
+
"source": "go",
|
|
699
|
+
"goType": "deviceDecision"
|
|
700
|
+
},
|
|
701
|
+
"post:/api/auth/device/token": {
|
|
702
|
+
"fields": [
|
|
703
|
+
{
|
|
704
|
+
"name": "deviceCode",
|
|
705
|
+
"type": "string"
|
|
706
|
+
}
|
|
707
|
+
],
|
|
708
|
+
"source": "go",
|
|
709
|
+
"goType": "(inline)"
|
|
710
|
+
},
|
|
659
711
|
"post:/api/auth/register": {
|
|
660
712
|
"fields": [
|
|
661
713
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GENERATED by scripts/gen-catalog.ts — do not edit by hand.
|
|
2
2
|
// The platform commit every other generated file here describes.
|
|
3
3
|
export const PLATFORM_SOURCE = {
|
|
4
|
-
"commit": "
|
|
5
|
-
"committedAt": "2026-10-
|
|
4
|
+
"commit": "e69eb03e217da1518783d08db53c776038293778",
|
|
5
|
+
"committedAt": "2026-10-12T11:10:11+07:00",
|
|
6
6
|
"dirty": false
|
|
7
7
|
};
|
|
@@ -612,11 +612,11 @@ export function readinessGaps(input) {
|
|
|
612
612
|
'/api/media/{siteId}/from-url ingests straight from a url in one hop.',
|
|
613
613
|
});
|
|
614
614
|
}
|
|
615
|
-
// A COLLECTION TILE WITH NO PICTURE
|
|
616
|
-
// render/nodes/collection-media
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
// Asked only where a
|
|
615
|
+
// A COLLECTION TILE WITH NO PICTURE. Measured on the sb_category_strip a store
|
|
616
|
+
// built here put on its home page: render/nodes/collection-media wrote
|
|
617
|
+
// `<img src="">`, two broken icons above the category names. web_builder PR
|
|
618
|
+
// #118 (74ca59dd2) gives it the image element's grey placeholder instead —
|
|
619
|
+
// better, still not the category. Asked only where a tile is on screen.
|
|
620
620
|
if ((input.categoriesUnpictured ?? 0) > 0 &&
|
|
621
621
|
[...input.pageNodes, ...(input.globalNodes ?? [])].some((n) => n.data?.type === 'collection-media')) {
|
|
622
622
|
const n = input.categoriesUnpictured;
|
|
@@ -624,7 +624,8 @@ export function readinessGaps(input) {
|
|
|
624
624
|
id: 'categoryImages',
|
|
625
625
|
draft: false,
|
|
626
626
|
problem: `${n} product categor${n === 1 ? 'y has' : 'ies have'} no image, and this page shows category ` +
|
|
627
|
-
|
|
627
|
+
"tiles — each draws the platform's grey placeholder (a broken-image icon on a platform " +
|
|
628
|
+
'older than web_builder 74ca59dd2) where the picture goes.',
|
|
628
629
|
fix: 'Upload a picture (sb_media_upload), then PUT the category back with image: url ' +
|
|
629
630
|
'(sb_api_call put:/api/sites/{siteId}/product-categories/{id}, merge:true).',
|
|
630
631
|
});
|
|
@@ -3,6 +3,7 @@ import { childrenOf, childrenWithSatellites, isOverlay, isSharedSection, pageChi
|
|
|
3
3
|
import { STUCK_STATE, stickyBlockedBy, stuckHostOf, isPinnedNode } from './sticky.js';
|
|
4
4
|
import { bandOf, isGlobal } from './traps.js';
|
|
5
5
|
import { exampleCopy } from './examplecopy.js';
|
|
6
|
+
import { SEED_COPY_VI, langOf } from './seedcopy.js';
|
|
6
7
|
import { HOVER_STATE, hoverHome } from './hover.js';
|
|
7
8
|
import { ELEMENTS, BINDING_SOURCES, BOUND_SPECIALS, FIRST_CHILD_ONLY, SATELLITE_RULES, ELEMENT_SEEDS } from '../../catalog/elements.generated.js';
|
|
8
9
|
import { fill } from './findings.js';
|
|
@@ -867,6 +868,23 @@ opts = {}) {
|
|
|
867
868
|
fix: fill('default_seed_copy', { id, key }),
|
|
868
869
|
});
|
|
869
870
|
}
|
|
871
|
+
// THE STORE SEEDS' ENGLISH COPY ON A VIETNAMESE SITE ("Add To Cart") —
|
|
872
|
+
// fine on an English site, so asked only when the site's language is
|
|
873
|
+
// known. sb_page_create now seeds it in Vietnamese; this is for pages
|
|
874
|
+
// made before that, or with locale:"en" on a Vietnamese site.
|
|
875
|
+
else if (typeof value === 'string' &&
|
|
876
|
+
SEED_COPY_VI[value] &&
|
|
877
|
+
langOf(opts.siteLocale) === 'vi') {
|
|
878
|
+
out.push({
|
|
879
|
+
code: 'default_seed_copy',
|
|
880
|
+
nodeId: id,
|
|
881
|
+
type,
|
|
882
|
+
problem: `Still the platform's English seed copy (${JSON.stringify(value)}) on a Vietnamese site — ` +
|
|
883
|
+
`"${SEED_COPY_VI[value]}" is what this storefront should say.`,
|
|
884
|
+
key,
|
|
885
|
+
fix: fill('default_seed_copy', { id, key }),
|
|
886
|
+
});
|
|
887
|
+
}
|
|
870
888
|
// A BUILT-IN LAYOUT'S EXAMPLE COPY — see examplecopy.ts.
|
|
871
889
|
else if (typeof value === 'string' && exampleCopy().has(value)) {
|
|
872
890
|
out.push({
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE STORE SEEDS' ENGLISH COPY, IN VIETNAMESE — applied when the page is made
|
|
3
|
+
* for a Vietnamese site.
|
|
4
|
+
*
|
|
5
|
+
* The platform seeds in English on purpose (editor/src/element/emptyState.ts:
|
|
6
|
+
* "a seed is a seed", decided 2026-09-17) and a table per language was offered
|
|
7
|
+
* there and declined. Here the agent IS the author, and it knows the site's
|
|
8
|
+
* language: measured on a store built end to end, the product page published
|
|
9
|
+
* "Add To Cart" / "Buy It Now" to a Vietnamese storefront and every page
|
|
10
|
+
* reviewed clean. sb_page_create seeds through this; sb_review (which reads the
|
|
11
|
+
* site's locale) reports a page still carrying the English. Exact strings only; a seed string
|
|
12
|
+
* this table does not carry is left as it was — and test/store-page-seeds
|
|
13
|
+
* fails when codegen brings one in, so the table cannot fall behind silently.
|
|
14
|
+
*/
|
|
15
|
+
export const SEED_COPY_VI = {
|
|
16
|
+
'Add To Cart': 'Thêm vào giỏ',
|
|
17
|
+
'Buy It Now': 'Mua ngay',
|
|
18
|
+
'This item is unavailable': 'Sản phẩm tạm hết',
|
|
19
|
+
'It may have been removed. Have a look at the rest of the shop.': 'Sản phẩm có thể đã ngừng bán. Mời bạn xem các sản phẩm khác.',
|
|
20
|
+
'No products yet': 'Chưa có sản phẩm',
|
|
21
|
+
'New arrivals will show up here. Check back soon.': 'Sản phẩm mới sẽ sớm có ở đây — mời bạn quay lại sau.',
|
|
22
|
+
'No posts yet': 'Chưa có bài viết',
|
|
23
|
+
'New stories will show up here. Check back soon.': 'Bài viết mới sẽ sớm có ở đây.',
|
|
24
|
+
};
|
|
25
|
+
/** The language part of a locale tag — "vi-VN", "vi_VN" and "VI" are all "vi". */
|
|
26
|
+
export function langOf(locale) {
|
|
27
|
+
return String(locale ?? '').toLowerCase().split(/[-_]/)[0];
|
|
28
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SEED_COPY_VI, langOf } from './seedcopy.js';
|
|
1
2
|
import { COMPLETION_HEADLINE, COMPLETION_HEADLINE_SENTINEL, PAGE_LAYOUT_SEEDS, PAGE_TYPES, STORE_PAGE_SEEDS } from '../../catalog/storepages.generated.js';
|
|
2
3
|
import { BINDING_SOURCES } from '../../catalog/elements.generated.js';
|
|
3
4
|
import { withFreshIds } from './ids.js';
|
|
@@ -40,22 +41,44 @@ export function seededTypes() {
|
|
|
40
41
|
* An explicit `headline` wins over both — a merchant's own wording is not the
|
|
41
42
|
* client's to overrule.
|
|
42
43
|
*/
|
|
44
|
+
/** Rewrite every exact seed string the table carries, in place. */
|
|
45
|
+
function localizeSeed(doc, table) {
|
|
46
|
+
for (const n of Object.values(doc.nodes)) {
|
|
47
|
+
const sp = n.specials;
|
|
48
|
+
if (!sp)
|
|
49
|
+
continue;
|
|
50
|
+
for (const k of ['text', 'label']) {
|
|
51
|
+
const v = sp[k];
|
|
52
|
+
if (typeof v === 'string' && table[v])
|
|
53
|
+
sp[k] = table[v];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export { SEED_COPY_VI } from './seedcopy.js';
|
|
43
58
|
export function seedDocument(type, opts = {}) {
|
|
44
59
|
const seed = STORE_PAGE_SEEDS[type];
|
|
45
60
|
if (!seed)
|
|
46
61
|
return null;
|
|
47
62
|
const raw = JSON.stringify(seed);
|
|
63
|
+
const vi = langOf(opts.locale ?? 'vi') === 'vi';
|
|
64
|
+
const out = (doc) => {
|
|
65
|
+
if (vi)
|
|
66
|
+
localizeSeed(doc, SEED_COPY_VI);
|
|
67
|
+
return withFreshIds(doc);
|
|
68
|
+
};
|
|
48
69
|
// Fresh ids per create, so two pages from one seed share nothing but ROOT.
|
|
49
70
|
if (!raw.includes(COMPLETION_HEADLINE_SENTINEL)) {
|
|
50
|
-
return
|
|
71
|
+
return out(JSON.parse(raw));
|
|
51
72
|
}
|
|
52
|
-
|
|
73
|
+
// Normalized: a site whose locale is "en-US" reads its own language here too.
|
|
74
|
+
const tag = langOf(opts.locale);
|
|
75
|
+
const lang = tag && tag in COMPLETION_HEADLINE ? tag : 'vi';
|
|
53
76
|
const headline = opts.headline || COMPLETION_HEADLINE[lang];
|
|
54
77
|
// JSON.stringify already escaped the seed; the substitute has to be escaped
|
|
55
78
|
// the same way or an apostrophe in a merchant's own headline breaks the
|
|
56
79
|
// document. Slicing the quotes off a stringified string is that escaping.
|
|
57
80
|
const escaped = JSON.stringify(headline).slice(1, -1);
|
|
58
|
-
return
|
|
81
|
+
return out(JSON.parse(raw.split(COMPLETION_HEADLINE_SENTINEL).join(escaped)));
|
|
59
82
|
}
|
|
60
83
|
/**
|
|
61
84
|
* What to put on an ENTITY TEMPLATE that opens empty, or null.
|
package/dist/tools/live.js
CHANGED
|
@@ -272,7 +272,9 @@ export function liveTokenFor(ctx) {
|
|
|
272
272
|
export function joinRoom(ctx, session, siteId) {
|
|
273
273
|
const tokenFn = liveTokenFor(ctx);
|
|
274
274
|
const wsBase = ctx.base.replace(/^http/, 'ws').replace(/\/$/, '');
|
|
275
|
-
const socket = new RealtimeSocket(`${wsBase}/api/realtime/ws?site=${encodeURIComponent(siteId)}`, tokenFn
|
|
275
|
+
const socket = new RealtimeSocket(`${wsBase}/api/realtime/ws?site=${encodeURIComponent(siteId)}`, tokenFn, undefined,
|
|
276
|
+
// A key never expires this way; a session renews near its end of life.
|
|
277
|
+
ctx.apiKey ? undefined : () => ctx.session.ensureFresh());
|
|
276
278
|
const live = new LiveSession(socket, {
|
|
277
279
|
onRemote: (patches) => session.applyRemote(patches),
|
|
278
280
|
onDesync: (reason) => session.roomDesync(reason),
|
package/dist/tools/page.js
CHANGED
|
@@ -47,8 +47,8 @@ import { projectList, PAGE_FIELDS, TEMPLATE_FIELDS } from './project.js';
|
|
|
47
47
|
* copies of a directive is how one of them quietly loses it. The fix for each
|
|
48
48
|
* KIND is sent once under `fixes`, and the directive once per process.
|
|
49
49
|
*/
|
|
50
|
-
export function reviewField(ctx, doc, formTypes, pageType, formDocType, formRedirects) {
|
|
51
|
-
const all = reviewDesign(doc, { formTypes, pageType, formDocType, formRedirects });
|
|
50
|
+
export function reviewField(ctx, doc, formTypes, pageType, formDocType, formRedirects, siteLocale) {
|
|
51
|
+
const all = reviewDesign(doc, { formTypes, pageType, formDocType, formRedirects, siteLocale });
|
|
52
52
|
if (all.length === 0)
|
|
53
53
|
return {};
|
|
54
54
|
// MAINTENANCE IS ADVICE, kept out of `findings`: it works for a visitor, so
|
|
@@ -1807,6 +1807,7 @@ export function registerPageTools(server, ctx) {
|
|
|
1807
1807
|
// The page's TYPE says which record it supplies — `no_data_context` stays
|
|
1808
1808
|
// silent without it rather than guessing.
|
|
1809
1809
|
let pageType;
|
|
1810
|
+
let siteLocale;
|
|
1810
1811
|
try {
|
|
1811
1812
|
const { siteId, pageId } = session.location();
|
|
1812
1813
|
const input = await gatherReadiness(ctx, siteId, Object.values(doc.doc.nodes), pageId);
|
|
@@ -1817,6 +1818,7 @@ export function registerPageTools(server, ctx) {
|
|
|
1817
1818
|
return f.id && a?.action === 'redirect' && (a.url || a.pageId) ? [f.id] : [];
|
|
1818
1819
|
}));
|
|
1819
1820
|
pageType = input.pages?.find((p) => p.id === pageId)?.type;
|
|
1821
|
+
siteLocale = input.siteLocale;
|
|
1820
1822
|
const gaps = readinessGaps(input);
|
|
1821
1823
|
const lang = siteLanguage(input.siteLocale, input.pageNodes);
|
|
1822
1824
|
const said = lang ? ctx.notices.once(`site-language:${siteId}`, lang) : undefined;
|
|
@@ -1830,7 +1832,7 @@ export function registerPageTools(server, ctx) {
|
|
|
1830
1832
|
catch {
|
|
1831
1833
|
// Readiness is additional information, never the reason a review fails.
|
|
1832
1834
|
}
|
|
1833
|
-
const field = reviewField(ctx, doc, formTypes, pageType, session.formType(), formRedirects);
|
|
1835
|
+
const field = reviewField(ctx, doc, formTypes, pageType, session.formType(), formRedirects, siteLocale);
|
|
1834
1836
|
// Clean = no finding a visitor meets; `advice` (maintenance) rides beside the verdict.
|
|
1835
1837
|
const clean = !field.findings;
|
|
1836
1838
|
return text({
|
|
@@ -2237,7 +2239,7 @@ export function registerPageTools(server, ctx) {
|
|
|
2237
2239
|
settings: z.record(z.unknown()).optional(),
|
|
2238
2240
|
seed: z.boolean().optional().describe('Default true; false creates a blank page.'),
|
|
2239
2241
|
chrome: z.boolean().optional().describe("Carry the site's header and footer, default true"),
|
|
2240
|
-
locale: z.string().optional().describe("vi
|
|
2242
|
+
locale: z.string().optional().describe("vi or en — a store seed's wording; defaults to the site's language, else vi."),
|
|
2241
2243
|
headline: z.string().optional().describe("The complete page's thank-you line."),
|
|
2242
2244
|
dry_run: z.boolean().optional(),
|
|
2243
2245
|
},
|
|
@@ -2420,7 +2422,25 @@ export function registerPageTools(server, ctx) {
|
|
|
2420
2422
|
let seeded = null;
|
|
2421
2423
|
const newId = res.page?.id;
|
|
2422
2424
|
if ((willSeed || layout) && typeof newId === 'string' && newId) {
|
|
2423
|
-
|
|
2425
|
+
// The SITE's language when the caller named none — the seed's English
|
|
2426
|
+
// copy is rewritten for a Vietnamese site (SEED_COPY_VI). Unreadable
|
|
2427
|
+
// settings fall back to the old default rather than block the create.
|
|
2428
|
+
const lang = locale ??
|
|
2429
|
+
(layout
|
|
2430
|
+
? undefined
|
|
2431
|
+
: await request({
|
|
2432
|
+
base: ctx.base,
|
|
2433
|
+
method: 'GET',
|
|
2434
|
+
path: `/api/sites/${encodeURIComponent(site_id)}/settings`,
|
|
2435
|
+
token: siteToken(ctx),
|
|
2436
|
+
fetchImpl: ctx.fetchImpl,
|
|
2437
|
+
})
|
|
2438
|
+
.then((r) => {
|
|
2439
|
+
const l = r?.settings?.locale;
|
|
2440
|
+
return typeof l === 'string' && l ? l : undefined;
|
|
2441
|
+
})
|
|
2442
|
+
.catch(() => undefined));
|
|
2443
|
+
const document = layout ? layoutDocument(layout) : seedDocument(type ?? '', { locale: lang, headline });
|
|
2424
2444
|
if (document) {
|
|
2425
2445
|
try {
|
|
2426
2446
|
await request({
|
package/dist/transport/auth.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { request } from './http.js';
|
|
1
|
+
import { onUnauthorized, request } from './http.js';
|
|
2
2
|
/**
|
|
3
3
|
* A user session against the platform's private API.
|
|
4
4
|
*
|
|
5
5
|
* `token()` is a GETTER, deliberately, and every consumer must call it per use
|
|
6
|
-
* rather than capture its result. The access token
|
|
6
|
+
* rather than capture its result. The access token's lifetime is the operator's (AUTH_ACCESS_TTL) and it rotates
|
|
7
7
|
* on refresh; a client holding the string it was built with replays an expired
|
|
8
8
|
* token forever, and the failure is SILENT — a rejected socket auth still fires
|
|
9
9
|
* onopen, so there is no error event and nothing in any UI. The editor shipped
|
|
@@ -15,12 +15,39 @@ export class Session {
|
|
|
15
15
|
base;
|
|
16
16
|
fetchImpl;
|
|
17
17
|
access = null;
|
|
18
|
+
/** When the access token stops working (ms epoch), from the pair's expiresIn; 0 = unknown. */
|
|
19
|
+
expiresAt = 0;
|
|
18
20
|
refreshToken = null;
|
|
19
21
|
name = '';
|
|
20
22
|
constructor(base, fetchImpl) {
|
|
21
23
|
this.base = base;
|
|
22
24
|
this.fetchImpl = fetchImpl;
|
|
23
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Answer a 401 on THIS session's current token with one refresh — see
|
|
28
|
+
* onUnauthorized in http.ts. Concurrent 401s share one refresh: refresh
|
|
29
|
+
* tokens are single-use, and presenting one twice ends the session.
|
|
30
|
+
* Registered while signed in only, so a signed-out session holds nothing.
|
|
31
|
+
*/
|
|
32
|
+
unrenew = null;
|
|
33
|
+
listen() {
|
|
34
|
+
this.unrenew ??= onUnauthorized(async (stale) => {
|
|
35
|
+
// A request that carried a token THIS session already replaced (a second
|
|
36
|
+
// 401 landing after the first one renewed) takes the current one.
|
|
37
|
+
if (this.retired.has(stale) && this.access)
|
|
38
|
+
return this.access;
|
|
39
|
+
if (stale !== this.access || !this.refreshToken)
|
|
40
|
+
return null;
|
|
41
|
+
this.renewing ??= this.refresh().finally(() => {
|
|
42
|
+
this.renewing = null;
|
|
43
|
+
});
|
|
44
|
+
await this.renewing;
|
|
45
|
+
return this.access;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
renewing = null;
|
|
49
|
+
/** Access tokens this session has replaced, so a late 401 on one still renews. */
|
|
50
|
+
retired = new Set();
|
|
24
51
|
get userName() {
|
|
25
52
|
return this.name;
|
|
26
53
|
}
|
|
@@ -52,37 +79,65 @@ export class Session {
|
|
|
52
79
|
}));
|
|
53
80
|
this.access = out.tokens.accessToken;
|
|
54
81
|
this.refreshToken = out.tokens.refreshToken;
|
|
82
|
+
this.expiresAt = expiry(out.tokens.expiresIn);
|
|
55
83
|
this.name = out.user?.name ?? '';
|
|
84
|
+
this.listen();
|
|
56
85
|
}
|
|
57
86
|
/** Rotate. The platform issues a NEW refresh token each time; keep that one. */
|
|
58
87
|
async refresh() {
|
|
59
|
-
|
|
88
|
+
const rt = this.refreshToken;
|
|
89
|
+
if (!rt)
|
|
60
90
|
throw new Error('sbuilder: no refresh token — log in first');
|
|
61
91
|
const out = (await request({
|
|
62
92
|
base: this.base,
|
|
63
93
|
method: 'POST',
|
|
64
94
|
path: '/api/auth/refresh',
|
|
65
|
-
body: { refreshToken:
|
|
95
|
+
body: { refreshToken: rt },
|
|
66
96
|
fetchImpl: this.fetchImpl,
|
|
67
97
|
}));
|
|
98
|
+
// Signed out, or signed in afresh, while this was in flight: the pair it
|
|
99
|
+
// brought back belongs to a session that no longer exists here.
|
|
100
|
+
if (this.refreshToken !== rt)
|
|
101
|
+
return;
|
|
102
|
+
if (this.access)
|
|
103
|
+
this.retired.add(this.access);
|
|
68
104
|
this.access = out.tokens.accessToken;
|
|
69
105
|
this.refreshToken = out.tokens.refreshToken;
|
|
106
|
+
this.expiresAt = expiry(out.tokens.expiresIn);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Renew BEFORE a call that cannot retry itself — a media upload sends its
|
|
110
|
+
* own fetch, outside the 401 retry in http.ts. Within 30s of the lifetime the
|
|
111
|
+
* pair was issued with, refresh first (shared with any 401 renewal in flight).
|
|
112
|
+
*/
|
|
113
|
+
async ensureFresh() {
|
|
114
|
+
if (!this.refreshToken || !this.expiresAt || Date.now() < this.expiresAt - 30_000)
|
|
115
|
+
return;
|
|
116
|
+
this.renewing ??= this.refresh().finally(() => {
|
|
117
|
+
this.renewing = null;
|
|
118
|
+
});
|
|
119
|
+
await this.renewing;
|
|
70
120
|
}
|
|
71
121
|
/**
|
|
72
122
|
* Sign this device out. Best effort, never throws.
|
|
73
123
|
*
|
|
74
124
|
* EVERY password sign-in is its own DEVICE SESSION since web_builder 9db1b2a9e,
|
|
75
125
|
* capped at 10 per account; past the cap the one that has gone longest
|
|
76
|
-
* without a REFRESH is deleted. This client
|
|
77
|
-
* lives 30 days), so a process that exits without
|
|
78
|
-
* held, and about ten agent runs later the evicted session is the merchant's
|
|
126
|
+
* without a REFRESH is deleted. This client refreshes only when refused (the
|
|
127
|
+
* access token lives 30 days by default), so a process that exits without
|
|
128
|
+
* signing out leaves a slot held, and about ten agent runs later the evicted session is the merchant's
|
|
79
129
|
* own editor — signed out mid-edit, by nothing they can see. A SIGKILLed
|
|
80
130
|
* process still leaks its slot; nothing on this side can prevent that.
|
|
81
131
|
*/
|
|
82
132
|
async logout() {
|
|
133
|
+
// A renewal in flight is about to rotate the refresh token; revoke the one it
|
|
134
|
+
// brings back, not the one it just spent (presenting a used one ends the session).
|
|
135
|
+
await this.renewing?.catch(() => undefined);
|
|
83
136
|
const refreshToken = this.refreshToken;
|
|
84
137
|
this.access = null;
|
|
85
138
|
this.refreshToken = null;
|
|
139
|
+
this.unrenew?.();
|
|
140
|
+
this.unrenew = null;
|
|
86
141
|
if (!refreshToken)
|
|
87
142
|
return;
|
|
88
143
|
try {
|
|
@@ -99,3 +154,7 @@ export class Session {
|
|
|
99
154
|
}
|
|
100
155
|
}
|
|
101
156
|
}
|
|
157
|
+
/** expiresIn seconds → an epoch deadline; 0 when the pair carried none. */
|
|
158
|
+
function expiry(seconds) {
|
|
159
|
+
return typeof seconds === 'number' && seconds > 0 ? Date.now() + seconds * 1000 : 0;
|
|
160
|
+
}
|
package/dist/transport/http.js
CHANGED
|
@@ -222,7 +222,12 @@ async function announced(opts, siteId, pageId, doc) {
|
|
|
222
222
|
}
|
|
223
223
|
return out;
|
|
224
224
|
}
|
|
225
|
-
|
|
225
|
+
const renewers = new Set();
|
|
226
|
+
export function onUnauthorized(r) {
|
|
227
|
+
renewers.add(r);
|
|
228
|
+
return () => renewers.delete(r);
|
|
229
|
+
}
|
|
230
|
+
async function send(opts, retried = false) {
|
|
226
231
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
227
232
|
// Identity rides on EVERY call rather than on a handshake of its own. There is
|
|
228
233
|
// no "connect" request to hang it off — the first thing this server does is
|
|
@@ -243,6 +248,15 @@ async function send(opts) {
|
|
|
243
248
|
headers,
|
|
244
249
|
body: body === undefined ? undefined : multipart ? body : JSON.stringify(body),
|
|
245
250
|
});
|
|
251
|
+
if (res.status === 401 && opts.token && !retried) {
|
|
252
|
+
for (const renew of renewers) {
|
|
253
|
+
const fresh = await renew(opts.token).catch(() => null);
|
|
254
|
+
if (fresh && fresh !== opts.token) {
|
|
255
|
+
void res.body?.cancel().catch(() => undefined); // release the refused response
|
|
256
|
+
return send({ ...opts, token: fresh }, true);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
246
260
|
if (opts.binary && res.ok)
|
|
247
261
|
return new Uint8Array(await res.arrayBuffer());
|
|
248
262
|
const raw = await res.text();
|
package/dist/transport/media.js
CHANGED
|
@@ -86,6 +86,9 @@ export function typeForName(name) {
|
|
|
86
86
|
*/
|
|
87
87
|
async function fromUrl(ctx, siteId, url, source) {
|
|
88
88
|
const doFetch = ctx.fetchImpl ?? fetch;
|
|
89
|
+
// Its own fetch, so the 401 renewal in http.ts never sees it — renew first.
|
|
90
|
+
if (!ctx.apiKey && ctx.session.loggedIn())
|
|
91
|
+
await ctx.session.ensureFresh().catch(() => undefined);
|
|
89
92
|
const res = await doFetch(`${ctx.base.replace(/\/$/, '')}/api/media/${encodeURIComponent(siteId)}/from-url`, {
|
|
90
93
|
method: 'POST',
|
|
91
94
|
headers: {
|
|
@@ -311,6 +314,8 @@ export async function fetchPublic(ctx, doFetch, url) {
|
|
|
311
314
|
}
|
|
312
315
|
export async function uploadMedia(ctx, siteId, source) {
|
|
313
316
|
const doFetch = ctx.fetchImpl ?? fetch;
|
|
317
|
+
if (!ctx.apiKey && ctx.session.loggedIn())
|
|
318
|
+
await ctx.session.ensureFresh().catch(() => undefined);
|
|
314
319
|
// THE SERVER'S OWN DOOR FIRST, when the source is a URL.
|
|
315
320
|
//
|
|
316
321
|
// `POST /api/media/{siteId}/from-url` fetches it where the platform already
|
package/dist/transport/socket.js
CHANGED
|
@@ -17,6 +17,7 @@ export class RealtimeSocket {
|
|
|
17
17
|
url;
|
|
18
18
|
token;
|
|
19
19
|
factory;
|
|
20
|
+
prepare;
|
|
20
21
|
ws = null;
|
|
21
22
|
closed = false;
|
|
22
23
|
attempt = 0;
|
|
@@ -42,10 +43,17 @@ export class RealtimeSocket {
|
|
|
42
43
|
* expired token on every reconnect, and the failure is SILENT — a rejected
|
|
43
44
|
* auth still fires `onopen`, so no error event ever surfaces.
|
|
44
45
|
*/
|
|
45
|
-
token, factory = (u) => new WebSocket(u)
|
|
46
|
+
token, factory = (u) => new WebSocket(u),
|
|
47
|
+
/**
|
|
48
|
+
* Run before each RECONNECT — renews a session token near its end of life,
|
|
49
|
+
* since the getter above is synchronous and a reconnect is the moment an
|
|
50
|
+
* expired token would be presented. Failure never blocks the attempt.
|
|
51
|
+
*/
|
|
52
|
+
prepare) {
|
|
46
53
|
this.url = url;
|
|
47
54
|
this.token = token;
|
|
48
55
|
this.factory = factory;
|
|
56
|
+
this.prepare = prepare;
|
|
49
57
|
}
|
|
50
58
|
get attempts() {
|
|
51
59
|
return this.attempt;
|
|
@@ -127,7 +135,13 @@ export class RealtimeSocket {
|
|
|
127
135
|
if (this.closed)
|
|
128
136
|
return;
|
|
129
137
|
const wait = Math.min(RETRY_BASE_MS * 2 ** this.attempt, RETRY_MAX_MS);
|
|
130
|
-
this.timer = setTimeout(() =>
|
|
138
|
+
this.timer = setTimeout(() => {
|
|
139
|
+
if (!this.prepare)
|
|
140
|
+
return this.connect();
|
|
141
|
+
void this.prepare()
|
|
142
|
+
.catch(() => undefined)
|
|
143
|
+
.then(() => this.connect());
|
|
144
|
+
}, wait);
|
|
131
145
|
};
|
|
132
146
|
ws.onerror = () => {
|
|
133
147
|
// Genuinely rare: a policy close is not an error. Nothing to do here —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.88.1",
|
|
4
4
|
"description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/sbuilder-mcp",
|
|
6
6
|
"type": "module",
|