sbuilder-mcp 0.88.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 +10 -0
- package/CHANGELOG.vi.md +10 -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/tools/live.js +3 -1
- 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,16 @@ 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
|
+
|
|
9
19
|
## [0.88.0] - 2026-10-12
|
|
10
20
|
|
|
11
21
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,16 @@ 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
|
+
|
|
9
19
|
## [0.88.0] - 2026-10-12
|
|
10
20
|
|
|
11
21
|
### 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
|
};
|
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/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.88.
|
|
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",
|