@synopackageland/cli 0.10.6 → 0.10.8
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 +70 -0
- package/dist/e2e/dsm.d.ts +13 -1
- package/dist/e2e/dsm.js +28 -4
- package/dist/runtime/executor-digests.json +11 -0
- package/dist/standalone/dsm-window.js +10 -6
- package/dist/standalone/portal-window.d.ts +3 -0
- package/dist/standalone/portal-window.js +29 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.8 (2026-10-15)
|
|
4
|
+
|
|
5
|
+
Builds with SDK 0.10.7 and templates 0.10.8, as 0.10.7 did: `init` still
|
|
6
|
+
writes SDK v0.10.7, `@synopackageland/sdk` 0.10.7 and `@synopackageland/ui`
|
|
7
|
+
0.9.8, and the executor digest table is unchanged, so an App rebuilt on it
|
|
8
|
+
keeps its approval unless its executor or policy changes. Only the DSM
|
|
9
|
+
window's `ctx.call` changes. The CLI still needs SDK 0.10.5 or later: the
|
|
10
|
+
SDK's Gate, `useLifecycleRetry`, `appLifecycle` and `ExecutorApproval` already
|
|
11
|
+
read an `APP_STOPPED` that arrives at once and check again; SDK 0.10.7 changed
|
|
12
|
+
only its protocol README's wording, and nothing in the CLI depends on it.
|
|
13
|
+
|
|
14
|
+
繁中摘要:搭配 SDK 0.10.7、templates 0.10.8(UI 0.9.8),digest 不變;SDK 下限維持 0.10.5
|
|
15
|
+
(SDK 的 Gate/`useLifecycleRetry`/`appLifecycle`/`ExecutorApproval` 本來就處理立即回的
|
|
16
|
+
`APP_STOPPED`)。只改 DSM window 的 `ctx.call`。
|
|
17
|
+
|
|
18
|
+
- `ctx.call` rejects `APP_STOPPED` at once: a stopped package waits for the
|
|
19
|
+
user to start it in Package Center, so the SDK's stopped copy shows within a
|
|
20
|
+
page's next read instead of 30 s later (Download Hub 29.3 s, t-0527; Reclaim
|
|
21
|
+
showed its old content or loading, t-0528). `APP_STARTING` and
|
|
22
|
+
`EXECUTOR_STARTING` repeat for 4 s of waiting (0.5, 1, 2, 0.5 s), so a short
|
|
23
|
+
start still shows the page nothing and the SDK's starting copy follows a
|
|
24
|
+
longer one within its own 4 s recheck. `executor.control`, which the window's
|
|
25
|
+
approval starts the executor with, still repeats a start for 30 s; the SDK's
|
|
26
|
+
status reads are still not repeated.
|
|
27
|
+
繁中:套件停止時 `ctx.call` 立即回 `APP_STOPPED`;啟動中最多重試 4 s(`executor.control` 維持 30 s)。
|
|
28
|
+
|
|
29
|
+
### Behavior changes
|
|
30
|
+
|
|
31
|
+
- An App rebuilt with CLI 0.10.8 shows a stopped package's copy within its
|
|
32
|
+
page's next read: `ctx.call` rejects `APP_STOPPED` at once instead of after
|
|
33
|
+
about 30 s, and repeats `APP_STARTING` and `EXECUTOR_STARTING` for at most
|
|
34
|
+
about 4 s. The App's code needs no change; only its devDependency
|
|
35
|
+
`@synopackageland/cli` 0.10.8 and its lockfile, then a rebuild.
|
|
36
|
+
- An e2e test that waits only for the Gate's stopped title
|
|
37
|
+
(`authorization.stopped.title`) can miss it: `ExecutorApproval` now takes
|
|
38
|
+
over first and shows its own stopped copy. Accept either of the SDK's stopped
|
|
39
|
+
copies.
|
|
40
|
+
繁中:App 升到 CLI 0.10.8 重新 build 後,套件停止時頁面立即收到 `APP_STOPPED`(不再等 30 s),
|
|
41
|
+
啟動中最多重試約 4 s;App 不需改程式。E2E 若只等 Gate 的停止標題,應接受 SDK 的任一停止文案。
|
|
42
|
+
|
|
43
|
+
## 0.10.7 (2026-10-15)
|
|
44
|
+
|
|
45
|
+
Builds with SDK 0.10.7 and templates 0.10.8: `init` writes SDK v0.10.7 (Go
|
|
46
|
+
module, Rust crate tag and `backend.sdk`), `@synopackageland/sdk` 0.10.7 and
|
|
47
|
+
`@synopackageland/ui` 0.9.8. The executor digest table adds SDK 0.10.7, whose
|
|
48
|
+
executor and lifecycle digests equal 0.10.6, so an App rebuilt on it keeps its
|
|
49
|
+
approval unless its executor or policy changes. The CLI still needs SDK
|
|
50
|
+
0.10.5 or later: `ctx.portal.lastStatus()` is new and optional, SDK 0.10.7's
|
|
51
|
+
`PortalSettings` keeps its placeholder rows without it, and older SDKs do not
|
|
52
|
+
read it; nothing in the CLI depends on SDK 0.10.7. An App gets the settings
|
|
53
|
+
page without placeholder movement only with both SDK 0.10.7 and a DSM window
|
|
54
|
+
built by CLI 0.10.7.
|
|
55
|
+
|
|
56
|
+
繁中摘要:搭配 SDK 0.10.7、templates 0.10.8(UI 0.9.8);digest 與 0.10.6 相同;SDK 下限維持
|
|
57
|
+
0.10.5(`ctx.portal.lastStatus()` 是選用的新增,舊 SDK 不讀、SDK 0.10.7 沒有它就畫佔位列)。
|
|
58
|
+
e2e 新增 `openUntilLoaded`(`signIn` 遇 `net::ERR_ABORTED` 最多重開約 30 s)、遮蔽 socket.io `sid=`。
|
|
59
|
+
|
|
60
|
+
- `signIn` and `dsmLoginSetup` keep reopening DSM's login page while the
|
|
61
|
+
navigation fails with `net::ERR_ABORTED`, for up to about 30 s, after a saved
|
|
62
|
+
profile is refused (error 119); they then fail with a clear error. Any other
|
|
63
|
+
error still fails at once. The helper is exported as
|
|
64
|
+
`openUntilLoaded(app, path, {timeoutMs, pauseMs})`.
|
|
65
|
+
- e2e output and report masking also masks the `sid=` query parameter of DSM's
|
|
66
|
+
socket.io URLs, which carries the connection id.
|
|
67
|
+
- `ctx.portal` reads the portal status when the DSM window opens and keeps
|
|
68
|
+
the newest one it read (`lastStatus()`); a `status()` asked for while a read
|
|
69
|
+
runs shares it. The SDK's `PortalSettings` starts from it, so a settings
|
|
70
|
+
page no longer moves when its placeholder rows become the real section
|
|
71
|
+
(Reclaim: 100 px, t-0520).
|
|
72
|
+
|
|
3
73
|
## 0.10.6 (2026-10-15)
|
|
4
74
|
|
|
5
75
|
Builds with SDK 0.10.6 and templates 0.10.7: `init` writes SDK v0.10.6 (Go
|
package/dist/e2e/dsm.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export declare function freePort(): Promise<number>;
|
|
|
32
32
|
* command's exit code.
|
|
33
33
|
*/
|
|
34
34
|
export declare function runWithTunnel(command: readonly string[], outer?: Env): Promise<number>;
|
|
35
|
-
/** `text` with DSM's session cookies, X-SYNO-TOKEN, SynoToken and
|
|
35
|
+
/** `text` with DSM's session cookies, X-SYNO-TOKEN, SynoToken, _sid and sid values replaced by ***. */
|
|
36
36
|
export declare function redactDsmSecrets(text: string): string;
|
|
37
37
|
/**
|
|
38
38
|
* Makes an e2e output directory private (directories 700, files 600) and masks
|
|
@@ -140,6 +140,18 @@ export declare function dsmSessionLive(page: Pick<SessionPage, "evaluate">): Pro
|
|
|
140
140
|
* thrown.
|
|
141
141
|
*/
|
|
142
142
|
export declare function openPath(app: Pick<App, "open">, path: string): Promise<void>;
|
|
143
|
+
/**
|
|
144
|
+
* `app.open(path)`, repeated while it fails with net::ERR_ABORTED: a refused
|
|
145
|
+
* session's old desktop JS keeps navigating and aborted several opens in a row.
|
|
146
|
+
* Gives up with a clear error after `timeoutMs` (30 s); any other failure is
|
|
147
|
+
* thrown at once.
|
|
148
|
+
*/
|
|
149
|
+
export declare function openUntilLoaded(app: Pick<App, "open">, path: string, { timeoutMs, pauseMs, now, sleep }?: {
|
|
150
|
+
timeoutMs?: number | undefined;
|
|
151
|
+
pauseMs?: number | undefined;
|
|
152
|
+
now?: (() => number) | undefined;
|
|
153
|
+
sleep?: ((ms: number) => Promise<void>) | undefined;
|
|
154
|
+
}): Promise<void>;
|
|
143
155
|
/**
|
|
144
156
|
* Restores the saved profile of `user`; signs in with the account file only
|
|
145
157
|
* when that profile is missing, no longer reaches the desktop, or gives a
|
package/dist/e2e/dsm.js
CHANGED
|
@@ -138,10 +138,10 @@ const mask = "***";
|
|
|
138
138
|
const cookieHeader = /(\bcookie"?\s*[:=]\s*"?)([^"\\\r\n]*)/gi;
|
|
139
139
|
const sessionCookieNames = ["id", "did", "_SSID"];
|
|
140
140
|
const sessionCookie = new RegExp(`(^|;\\s*)(${sessionCookieNames.join("|")})=[^;\\s]*`, "gi");
|
|
141
|
-
// The X-SYNO-TOKEN header, a SynoToken parameter or field,
|
|
141
|
+
// The X-SYNO-TOKEN header, a SynoToken parameter or field, a web API's _sid parameter and a socket.io connection's sid parameter.
|
|
142
142
|
const tokenValue = /(\b(?:x-syno-token|synotoken)"?\s*[:=]\s*"?)[^"\\\s,;&}]+/gi;
|
|
143
|
-
const sidParameter = /([?&]
|
|
144
|
-
/** `text` with DSM's session cookies, X-SYNO-TOKEN, SynoToken and
|
|
143
|
+
const sidParameter = /([?&]_?sid=)[^&"\\\s]+/gi;
|
|
144
|
+
/** `text` with DSM's session cookies, X-SYNO-TOKEN, SynoToken, _sid and sid values replaced by ***. */
|
|
145
145
|
export function redactDsmSecrets(text) {
|
|
146
146
|
return text
|
|
147
147
|
.replace(cookieHeader, (_match, header, cookies) => header + cookies.replace(sessionCookie, `$1$2=${mask}`))
|
|
@@ -419,6 +419,30 @@ export async function openPath(app, path) {
|
|
|
419
419
|
await app.open(path);
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* `app.open(path)`, repeated while it fails with net::ERR_ABORTED: a refused
|
|
424
|
+
* session's old desktop JS keeps navigating and aborted several opens in a row.
|
|
425
|
+
* Gives up with a clear error after `timeoutMs` (30 s); any other failure is
|
|
426
|
+
* thrown at once.
|
|
427
|
+
*/
|
|
428
|
+
export async function openUntilLoaded(app, path, { timeoutMs = 30_000, pauseMs = 1_000, now = Date.now, sleep = (ms) => new Promise(done => setTimeout(done, ms)) } = {}) {
|
|
429
|
+
const deadline = now() + timeoutMs;
|
|
430
|
+
for (;;) {
|
|
431
|
+
try {
|
|
432
|
+
await app.open(path);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
const message = String(error?.message ?? error);
|
|
437
|
+
if (!/net::ERR_ABORTED/.test(message))
|
|
438
|
+
throw error;
|
|
439
|
+
if (now() + pauseMs >= deadline) {
|
|
440
|
+
throw new Error(`Opening ${path} still aborted after ${Math.round(timeoutMs / 1000)} s of retries: ${message}`);
|
|
441
|
+
}
|
|
442
|
+
await sleep(pauseMs);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
422
446
|
async function restoredSessionLive(page) {
|
|
423
447
|
return await desktopOrLogin(page) === "desktop" && await dsmSessionLive(page);
|
|
424
448
|
}
|
|
@@ -442,7 +466,7 @@ export async function signIn(app, user = "admin", env = process.env) {
|
|
|
442
466
|
// A refused session still shows the desktop; dropping its cookies shows the login form.
|
|
443
467
|
if (profile) {
|
|
444
468
|
await context.clearCookies();
|
|
445
|
-
await
|
|
469
|
+
await openUntilLoaded(app, "/");
|
|
446
470
|
}
|
|
447
471
|
const account = readAccount(accountFile(user, env), user);
|
|
448
472
|
await page.locator(loginSelector).fill(account.username);
|
|
@@ -75,5 +75,16 @@
|
|
|
75
75
|
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
76
76
|
},
|
|
77
77
|
"goToolchain": "go1.26.9"
|
|
78
|
+
},
|
|
79
|
+
"0.10.7": {
|
|
80
|
+
"x86_64": "4ba7040761a719e74d6b047ef68b9415e658ebb8b373b69555b8c307524e26ba",
|
|
81
|
+
"aarch64": "ba80b389e3291419389b66be7b0a0e1085d3dfec55c9c651fecae427279bb3d5",
|
|
82
|
+
"armv7": "49474467b9c9a7f31b30b5053b70eb4f71a95f2c06fbf3566e8de8f16972a299",
|
|
83
|
+
"lifecycle": {
|
|
84
|
+
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
85
|
+
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
86
|
+
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
87
|
+
},
|
|
88
|
+
"goToolchain": "go1.26.9"
|
|
78
89
|
}
|
|
79
90
|
}
|
|
@@ -192,17 +192,21 @@ ${ns}.callOnce = function (method, params, options, requestTimeout) {
|
|
|
192
192
|
};
|
|
193
193
|
|
|
194
194
|
// APP_STOPPED, APP_STARTING and EXECUTOR_STARTING mean the executor op was not
|
|
195
|
-
// sent
|
|
196
|
-
//
|
|
197
|
-
//
|
|
195
|
+
// sent. A stopped package waits for the user to start it in Package Center, so
|
|
196
|
+
// APP_STOPPED rejects at once (t-0529). A start ends by itself: a call repeats
|
|
197
|
+
// half a second, then doubling, for 4 s of waiting in all, so a short start
|
|
198
|
+
// shows the page nothing and the SDK's starting copy follows a long one within
|
|
199
|
+
// its own 4 s recheck; executor.control, which the window's approval starts the
|
|
200
|
+
// executor with, repeats up to 8 s apart for 30 s. Each attempt keeps its own
|
|
201
|
+
// deadline; past the wait it rejects with the last refusal, and leaving the
|
|
198
202
|
// page during a wait rejects request_canceled at once. The SDK's status reads
|
|
199
203
|
// are not repeated: its panels show the stopped or starting package and check
|
|
200
|
-
// again on their own
|
|
201
|
-
${ns}.retryCodes = ["
|
|
204
|
+
// again on their own (t-0503 R1).
|
|
205
|
+
${ns}.retryCodes = ["APP_STARTING", "EXECUTOR_STARTING"];
|
|
202
206
|
${ns}.statusReads = ["delegate.status", "authorization.status", "executor.status"];
|
|
203
207
|
${ns}.call = function (method, params, options, requestTimeout) {
|
|
204
208
|
var signal = options && options.signal;
|
|
205
|
-
var left = ${ns}.statusReads.indexOf(method)
|
|
209
|
+
var left = ${ns}.statusReads.indexOf(method) >= 0 ? 0 : method === "executor.control" ? 30000 : 4000;
|
|
206
210
|
var delay = 500;
|
|
207
211
|
var attempt = function () {
|
|
208
212
|
return ${ns}.callOnce(method, params, options, requestTimeout).catch(function (error) {
|
|
@@ -17,6 +17,9 @@ export declare function portalCatalogs(sdkDir: string): PortalCatalogs;
|
|
|
17
17
|
* status or reject `{code}`. With `options.entry` (below DSM 7.2) the
|
|
18
18
|
* backend's `portal.status` also reports the executor's entry, and
|
|
19
19
|
* administrators get `register()` and `unregister()`; `openControlPanel()` opens Login Portal > Applications.
|
|
20
|
+
* `lastStatus()` is the newest status read; the access reads one when it is
|
|
21
|
+
* created, as the window opens, and a `status()` asked for while a read runs
|
|
22
|
+
* shares it.
|
|
20
23
|
* `messages` is the SDK's panel text in the window's locale; `mountPanel(element)`
|
|
21
24
|
* renders a plain panel for UIs without their own.
|
|
22
25
|
*/
|
|
@@ -24,6 +24,9 @@ export function portalCatalogs(sdkDir) {
|
|
|
24
24
|
* status or reject `{code}`. With `options.entry` (below DSM 7.2) the
|
|
25
25
|
* backend's `portal.status` also reports the executor's entry, and
|
|
26
26
|
* administrators get `register()` and `unregister()`; `openControlPanel()` opens Login Portal > Applications.
|
|
27
|
+
* `lastStatus()` is the newest status read; the access reads one when it is
|
|
28
|
+
* created, as the window opens, and a `status()` asked for while a read runs
|
|
29
|
+
* shares it.
|
|
27
30
|
* `messages` is the SDK's panel text in the window's locale; `mountPanel(element)`
|
|
28
31
|
* renders a plain panel for UIs without their own.
|
|
29
32
|
*/
|
|
@@ -88,29 +91,49 @@ export const portalRuntime = (catalogs) => `function createPortalAccess(options)
|
|
|
88
91
|
// The backend reports cleanup-required while a register or unregister runs,
|
|
89
92
|
// so a status read waits for the change, whatever its result.
|
|
90
93
|
var changing = Promise.resolve();
|
|
91
|
-
|
|
94
|
+
// last: the newest status read, which the SDK's PortalSettings shows before it
|
|
95
|
+
// reads again. A read that runs serves every status() asked for meanwhile,
|
|
96
|
+
// until a change starts; the change reads afresh.
|
|
97
|
+
var last, pending, reads = 0;
|
|
98
|
+
function read() {
|
|
99
|
+
var n = ++reads;
|
|
92
100
|
return changing.then(function () {
|
|
93
101
|
if (options.entry) return fromEntry();
|
|
94
102
|
return options.isAdmin() ? fromDsm() : fromBackend();
|
|
103
|
+
}).then(function (out) {
|
|
104
|
+
if (n === reads) last = out;
|
|
105
|
+
return out;
|
|
95
106
|
});
|
|
96
107
|
}
|
|
108
|
+
function status() {
|
|
109
|
+
if (!pending) {
|
|
110
|
+
pending = read();
|
|
111
|
+
var done = function () { pending = undefined; };
|
|
112
|
+
pending.then(done, done);
|
|
113
|
+
}
|
|
114
|
+
return pending;
|
|
115
|
+
}
|
|
97
116
|
function change(method) {
|
|
98
117
|
if (!options.isAdmin()) return Promise.reject({ code: "admin_required" });
|
|
99
118
|
var call = options.call(method, {});
|
|
100
119
|
changing = call.then(function () {}, function () {});
|
|
101
|
-
|
|
120
|
+
pending = undefined;
|
|
121
|
+
return call.then(read, function (error) {
|
|
102
122
|
throw { code: error && error.code === "ADMIN_REQUIRED" ? "admin_required" : "PORTAL_FAILED" };
|
|
103
123
|
});
|
|
104
124
|
}
|
|
105
125
|
function set(alias) {
|
|
106
126
|
if (!options.isAdmin()) return Promise.reject({ code: "admin_required" });
|
|
107
|
-
return options.send("SYNO.Core.AppPortal", "set", 2, { id: options.appId, alias: alias }).then(
|
|
127
|
+
return options.send("SYNO.Core.AppPortal", "set", 2, { id: options.appId, alias: alias }).then(read, function (error) {
|
|
108
128
|
throw { code: ERRORS[error && error.code] || "portal_unavailable" };
|
|
109
129
|
});
|
|
110
130
|
}
|
|
111
131
|
var access = {
|
|
112
132
|
messages: messages,
|
|
113
133
|
status: status,
|
|
134
|
+
lastStatus: function () {
|
|
135
|
+
return last;
|
|
136
|
+
},
|
|
114
137
|
apply: function (alias) {
|
|
115
138
|
if (typeof alias !== "string" || !ALIAS.test(alias)) return Promise.reject({ code: "portal_alias_invalid" });
|
|
116
139
|
if (RESERVED.indexOf(alias) >= 0) return Promise.reject({ code: "portal_alias_reserved" });
|
|
@@ -180,5 +203,8 @@ export const portalRuntime = (catalogs) => `function createPortalAccess(options)
|
|
|
180
203
|
dispose: function () { disposed = true; element.replaceChildren(); }
|
|
181
204
|
};
|
|
182
205
|
}
|
|
206
|
+
// The window reads the status when it opens, so a settings page shows the
|
|
207
|
+
// portal section as it is (t-0520).
|
|
208
|
+
status();
|
|
183
209
|
return access;
|
|
184
210
|
}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.8",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"prepublishOnly": "npm run build"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@synopackageland/templates": "0.10.
|
|
28
|
+
"@synopackageland/templates": "0.10.8",
|
|
29
29
|
"ajv": "^8.17.1",
|
|
30
30
|
"commander": "^13.1.0",
|
|
31
31
|
"tar": "^7.4.3",
|