@synopackageland/cli 0.9.7 → 0.10.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.
Files changed (111) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +5 -0
  3. package/dist/add.js +7 -8
  4. package/dist/build-spk/dsm-arch.js +1 -1
  5. package/dist/build-spk/dsm-icons.d.ts +8 -1
  6. package/dist/build-spk/dsm-icons.js +26 -12
  7. package/dist/build-spk/dsm-version.d.ts +1 -1
  8. package/dist/build-spk/dsm-version.js +4 -4
  9. package/dist/build-spk/gateway.js +2 -2
  10. package/dist/build-spk/generator.js +1 -1
  11. package/dist/build-spk/png.js +16 -16
  12. package/dist/capabilities.js +1 -1
  13. package/dist/capability-overlays.d.ts +16 -5
  14. package/dist/capability-overlays.js +30 -16
  15. package/dist/cli.js +107 -107
  16. package/dist/compose/analyze.js +23 -23
  17. package/dist/compose/privilege.js +12 -12
  18. package/dist/contract/i18n.js +3 -3
  19. package/dist/contract/icon.js +2 -2
  20. package/dist/contract/posix-all-shares.js +2 -2
  21. package/dist/contract/schema-validate.js +3 -3
  22. package/dist/contract/validate.js +16 -16
  23. package/dist/contract/web-launch.d.ts +2 -1
  24. package/dist/contract/web-launch.js +3 -2
  25. package/dist/deploy/deploy.js +8 -8
  26. package/dist/deploy/homes-values.js +1 -1
  27. package/dist/deploy/identity-shared.js +1 -1
  28. package/dist/deploy/native-install-values.js +1 -1
  29. package/dist/deploy/param-values.js +4 -4
  30. package/dist/deploy/plan.d.ts +1 -1
  31. package/dist/deploy/port-preflight.js +3 -3
  32. package/dist/deploy/run-as-values.js +2 -2
  33. package/dist/deploy/share-permission-preflight.js +6 -6
  34. package/dist/deploy/share-values.js +3 -3
  35. package/dist/error-registry.js +369 -346
  36. package/dist/errors.js +1 -1
  37. package/dist/host/dsm-port-profile.d.ts +1 -1
  38. package/dist/host/dsm-port-profile.js +1 -1
  39. package/dist/info.js +11 -11
  40. package/dist/init.d.ts +2 -0
  41. package/dist/init.js +85 -38
  42. package/dist/inspect.js +5 -5
  43. package/dist/portal/alias.d.ts +1 -1
  44. package/dist/portal/alias.js +4 -4
  45. package/dist/runtime/executor-build.js +12 -1
  46. package/dist/runtime/executor-digests.json +14 -91
  47. package/dist/runtime/executor-inspect.js +11 -4
  48. package/dist/runtime/executor-policy.d.ts +9 -2
  49. package/dist/runtime/executor-policy.js +27 -8
  50. package/dist/runtime/root-programs.d.ts +47 -0
  51. package/dist/runtime/root-programs.js +153 -0
  52. package/dist/runtime/sdk-source.d.ts +5 -4
  53. package/dist/runtime/sdk-source.js +8 -4
  54. package/dist/skill.js +14 -14
  55. package/dist/spk/check-info.js +17 -17
  56. package/dist/spk/check-metadata.js +18 -18
  57. package/dist/spk/check-payload.js +10 -10
  58. package/dist/spk/rules.d.ts +57 -57
  59. package/dist/spk/rules.js +50 -50
  60. package/dist/spk/validate-spk.js +5 -5
  61. package/dist/standalone/api-permissions.js +1 -1
  62. package/dist/standalone/background.d.ts +3 -1
  63. package/dist/standalone/background.js +8 -4
  64. package/dist/standalone/binary.js +7 -7
  65. package/dist/standalone/bridge-app.d.ts +23 -0
  66. package/dist/standalone/bridge-app.js +141 -0
  67. package/dist/standalone/bridge-backend.d.ts +40 -0
  68. package/dist/standalone/bridge-backend.js +95 -0
  69. package/dist/standalone/bridge-build.d.ts +21 -0
  70. package/dist/standalone/bridge-build.js +62 -0
  71. package/dist/standalone/bridge-json.d.ts +16 -0
  72. package/dist/standalone/bridge-json.js +59 -0
  73. package/dist/standalone/bridge-runtime.d.ts +16 -0
  74. package/dist/standalone/bridge-runtime.js +46 -0
  75. package/dist/standalone/bridge-rust.d.ts +16 -0
  76. package/dist/standalone/bridge-rust.js +89 -0
  77. package/dist/standalone/build.js +18 -14
  78. package/dist/standalone/daemon.js +6 -6
  79. package/dist/standalone/dsm-window.d.ts +10 -5
  80. package/dist/standalone/dsm-window.js +69 -18
  81. package/dist/standalone/i18n.js +18 -18
  82. package/dist/standalone/inspect-bridge.d.ts +31 -0
  83. package/dist/standalone/inspect-bridge.js +111 -0
  84. package/dist/standalone/inspect-spk.d.ts +1 -1
  85. package/dist/standalone/inspect-spk.js +77 -69
  86. package/dist/standalone/lifecycle.js +22 -3
  87. package/dist/standalone/manifest.d.ts +17 -9
  88. package/dist/standalone/manifest.js +143 -65
  89. package/dist/standalone/portal-window.js +14 -7
  90. package/dist/standalone/request-timeout.d.ts +9 -0
  91. package/dist/standalone/request-timeout.js +13 -0
  92. package/dist/standalone/root-ops.d.ts +23 -2
  93. package/dist/standalone/root-ops.js +118 -13
  94. package/dist/standalone/sdk-check.js +4 -4
  95. package/dist/standalone/shared-folder.js +7 -7
  96. package/dist/standalone/spk.d.ts +2 -0
  97. package/dist/standalone/spk.js +30 -11
  98. package/dist/standalone/uninstall-hook.js +22 -4
  99. package/dist/standalone/uninstall.js +7 -7
  100. package/dist/standalone/window-size.d.ts +17 -0
  101. package/dist/standalone/window-size.js +31 -0
  102. package/dist/types.d.ts +1 -1
  103. package/dist/validate.js +13 -13
  104. package/package.json +2 -2
  105. package/schema/bridge-manifest.v1.schema.json +86 -0
  106. package/skills/reference/create.md +76 -2
  107. package/skills/reference/ui-json.md +2 -1
  108. package/skills/reference/ui-vue.md +9 -3
  109. package/skills/reference/wrap.md +1 -1
  110. package/dist/standalone/rust-build.d.ts +0 -35
  111. package/dist/standalone/rust-build.js +0 -127
@@ -0,0 +1,89 @@
1
+ import { chmodSync, copyFileSync, mkdirSync, rmSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { SDK_GIT_URL } from "../runtime/sdk-source.js";
5
+ import { checkCgiBinary } from "./binary.js";
6
+ import { RUST_BIN_DIR } from "./bridge-json.js";
7
+ import { StandaloneBuildError } from "./spk.js";
8
+ /** The glibc loader of DSM's ARMv7 models, the one interpreter an armv7 Rust App may name. */
9
+ export const ARMV7_INTERPRETER = "/lib/ld-linux-armhf.so.3";
10
+ /**
11
+ * x86_64 and aarch64 are static musl binaries. armv7 links against glibc
12
+ * (2.17, older than any DSM 7), because musl's 64-bit time calls fail on
13
+ * DSM's ARMv7 kernels; zig provides both, so any host can build all three.
14
+ */
15
+ export const RUST_TARGETS = {
16
+ x86_64: { triple: "x86_64-unknown-linux-musl" },
17
+ aarch64: { triple: "aarch64-unknown-linux-musl" },
18
+ armv7: { triple: "armv7-unknown-linux-gnueabihf.2.17", interpreter: ARMV7_INTERPRETER },
19
+ };
20
+ const RUSTUP_HINT = "Install Rust with rustup (https://rustup.rs), run rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl armv7-unknown-linux-gnueabihf, and make sure the cargo on PATH is rustup's (not Homebrew's rust).";
21
+ /** Cross-compiles the App's binary for one architecture into `work/app-<arch>/bin`; returns that App directory. */
22
+ export async function buildRustApp(m, arch, work, run) {
23
+ const backend = m.backend;
24
+ const target = RUST_TARGETS[arch];
25
+ const result = await run("cargo", [
26
+ "zigbuild", "--release", "--locked", "--target", target.triple, "--bin", backend.entry, "--message-format=json-render-diagnostics",
27
+ ], { cwd: backend.dir, env: cargoEnv(backend.dir), capture: true });
28
+ if (result.missing)
29
+ fail(`cargo was not found. ${RUSTUP_HINT}`);
30
+ if (result.status !== 0) {
31
+ const hint = /no such command: `?zigbuild/.test(result.stderr) ? "Run cargo install cargo-zigbuild and install zig (https://ziglang.org/download/)."
32
+ : /can't find crate for `(core|std)`/.test(result.stderr) ? RUSTUP_HINT : "";
33
+ fail(`cargo zigbuild (${arch}) failed. ${hint}\n${result.stderr}`);
34
+ }
35
+ const app = join(work, `app-${arch}`);
36
+ rmSync(app, { recursive: true, force: true });
37
+ mkdirSync(join(app, RUST_BIN_DIR), { recursive: true });
38
+ const out = join(app, RUST_BIN_DIR, backend.entry);
39
+ copyFileSync(artifact(result.stdout ?? "", backend.entry), out);
40
+ chmodSync(out, 0o755);
41
+ const problem = checkCgiBinary(work, out, arch, target.interpreter);
42
+ if (problem)
43
+ throw new StandaloneBuildError(problem.code, problem.message);
44
+ return app;
45
+ }
46
+ /**
47
+ * The environment of one reproducible cargo build: the CLI's flags replace any
48
+ * inherited RUSTFLAGS, paths are remapped, symbols and build IDs dropped, and
49
+ * panics unwind so the SDK answers `internal` instead of the process ending.
50
+ * Cargo fetches the private SDK repository through git, with the user's credentials.
51
+ */
52
+ function cargoEnv(dir) {
53
+ const env = { ...process.env };
54
+ delete env.RUSTFLAGS;
55
+ const cargoHome = env.CARGO_HOME || join(homedir(), ".cargo");
56
+ env.CARGO_ENCODED_RUSTFLAGS = [
57
+ `--remap-path-prefix=${dir}=/build`,
58
+ `--remap-path-prefix=${cargoHome}=/cargo`,
59
+ "-Cstrip=symbols",
60
+ "-Clink-arg=-Wl,--build-id=none",
61
+ // zig notes that it ignores rustc's own -Wl,-O1; such linker output is no warning of the App. Link errors still fail.
62
+ "-Alinker_messages",
63
+ ].join("\x1f");
64
+ env.CARGO_PROFILE_RELEASE_PANIC = "unwind";
65
+ env.SOURCE_DATE_EPOCH = "0";
66
+ env.CARGO_NET_GIT_FETCH_WITH_CLI = "true";
67
+ if (env.SYNOPKGLAND_SDK_DIR) {
68
+ // Development only: git reads the SDK repository from this checkout, which must hold the locked tag.
69
+ const n = Number(env.GIT_CONFIG_COUNT ?? 0);
70
+ env.GIT_CONFIG_COUNT = String(n + 1);
71
+ env[`GIT_CONFIG_KEY_${n}`] = `url.file://${env.SYNOPKGLAND_SDK_DIR}.insteadOf`;
72
+ env[`GIT_CONFIG_VALUE_${n}`] = SDK_GIT_URL;
73
+ }
74
+ return env;
75
+ }
76
+ /** The executable Cargo reported for `bin`. */
77
+ function artifact(messages, bin) {
78
+ for (const line of messages.split("\n")) {
79
+ if (!line.startsWith("{"))
80
+ continue;
81
+ const message = JSON.parse(line);
82
+ if (message.reason === "compiler-artifact" && message.target?.kind?.includes("bin") && message.target.name === bin && message.executable)
83
+ return message.executable;
84
+ }
85
+ return fail(`cargo zigbuild produced no bin ${bin}; backend.entry must be the Cargo project's binary name.`);
86
+ }
87
+ function fail(message) {
88
+ throw new StandaloneBuildError("STANDALONE_RUST_BUILD_FAILED", message);
89
+ }
@@ -7,13 +7,15 @@ import { goBuildEnv } from "../build-spk/dsm-arch.js";
7
7
  import { resolveMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, portalByExecutor, portalMinDsmMessage } from "../build-spk/dsm-version.js";
8
8
  import { checkCgiBinary } from "./binary.js";
9
9
  import { loadStandaloneManifest } from "./manifest.js";
10
- import { buildRustBackend } from "./rust-build.js";
11
10
  import { assembleStandaloneSpk, StandaloneBuildError } from "./spk.js";
12
11
  import { buildDaemonControl } from "./daemon.js";
13
12
  import { buildExecutor } from "../runtime/executor-build.js";
14
13
  import { BACKGROUND_WORKER_MAIN } from "./background.js";
15
14
  import { readApiPermissions } from "./api-permissions.js";
16
15
  import { checkJobs, commitOutputs, forEachArch } from "./arch-jobs.js";
16
+ import { isBridgeBackend } from "./bridge-backend.js";
17
+ import { requestTimeoutSeconds } from "./request-timeout.js";
18
+ import { buildBridge, prepareBridge } from "./bridge-build.js";
17
19
  /** Argv only, never a shell; output streams to the terminal except stderr, which is also kept, and captured stdout. */
18
20
  export const runCommand = (command, args, options) => new Promise((done) => {
19
21
  const child = spawn(command, args, {
@@ -88,7 +90,7 @@ export async function buildStandalone(options) {
88
90
  const arches = options.arches ?? manifest.arches;
89
91
  for (const arch of arches) {
90
92
  if (!manifest.arches.includes(arch)) {
91
- throw new StandaloneBuildError("STANDALONE_ARCH_NOT_DECLARED", `${arch} 不在 dsm-package.yaml 的 arches 內;要建置它,請先把 ${arch} 加進 arches。`);
93
+ throw new StandaloneBuildError("STANDALONE_ARCH_NOT_DECLARED", `${arch} is not in the arches of dsm-package.yaml; to build it, add ${arch} to arches first.`);
92
94
  }
93
95
  }
94
96
  await buildUi(manifest, options.run ?? runCommand);
@@ -96,6 +98,7 @@ export async function buildStandalone(options) {
96
98
  const work = mkdtempSync(join(tmpdir(), "syno-standalone-build-"));
97
99
  const warnings = new Set();
98
100
  try {
101
+ const bridge = await prepareBridge(manifest, work);
99
102
  const outputs = await commitOutputs(outDir, (stage) => forEachArch(arches, jobs, async (arch) => {
100
103
  const run = runnerFor(options.run, jobs, arch);
101
104
  const archWork = join(work, arch);
@@ -103,10 +106,10 @@ export async function buildStandalone(options) {
103
106
  const executor = await buildExecutor(manifest, arch, archWork, run);
104
107
  executor?.warnings.forEach(warning => warnings.add(warning));
105
108
  const builtManifest = executor ? { ...manifest, executor } : manifest;
106
- const { cgi: cgiPath, worker: backgroundWorker } = await backendFor(builtManifest, arch, archWork, run);
109
+ const { cgi: cgiPath, worker: backgroundWorker, app } = await backendFor(builtManifest, arch, archWork, run, bridge);
107
110
  const outputPath = join(stage, `${manifest.package}-${manifest.spkVersion}-${arch}.spk`);
108
111
  const daemonControl = await buildDaemonControl(manifest, arch, archWork, run);
109
- await assembleStandaloneSpk({ manifest: builtManifest, arch, cgiPath, outputPath, daemonControl, backgroundWorker, executor });
112
+ await assembleStandaloneSpk({ manifest: builtManifest, arch, cgiPath, outputPath, daemonControl, backgroundWorker, executor, app });
110
113
  return outputPath;
111
114
  }));
112
115
  return { outputs, warnings: [...warnings] };
@@ -124,16 +127,16 @@ async function buildUi(manifest, run) {
124
127
  const result = await run(command, args, { cwd: build.cwd, env: { ...process.env } });
125
128
  if (result.missing || result.status !== 0) {
126
129
  const installHint = command === "npm" && existsSync(join(build.cwd, "package.json")) && !existsSync(join(build.cwd, "node_modules"))
127
- ? `\nUI 依賴尚未安裝。請在 ${build.cwd} 執行 npm ${existsSync(join(build.cwd, "package-lock.json")) ? "ci" : "install"},然後重新執行 build。`
130
+ ? `\nThe UI dependencies are not installed. Run npm ${existsSync(join(build.cwd, "package-lock.json")) ? "ci" : "install"} in ${build.cwd}, then run build again.`
128
131
  : "";
129
- throw new StandaloneBuildError("STANDALONE_UI_BUILD_FAILED", `UI build(${build.command.join(" ")})失敗。${installHint}\n${result.stderr}`);
132
+ throw new StandaloneBuildError("STANDALONE_UI_BUILD_FAILED", `UI build (${build.command.join(" ")}) failed. ${installHint}\n${result.stderr}`);
130
133
  }
131
134
  }
132
- /** The checked CGI of one architecture and, with capabilities.jobs, its fixed worker. */
133
- async function backendFor(manifest, arch, work, run) {
135
+ /** The checked CGI of one architecture and, with capabilities.jobs, its fixed worker; or the runtime and App of a bridge App. */
136
+ async function backendFor(manifest, arch, work, run, bridge) {
134
137
  const backend = manifest.backend;
135
- if (backend.kind === "rust")
136
- return buildRustBackend(manifest, arch, work, run);
138
+ if (isBridgeBackend(backend))
139
+ return buildBridge(manifest, bridge, arch, work, run);
137
140
  const cgi = backend.kind === "binary" ? backend.files[arch] : await buildGoCgi(manifest, backend, arch, work, run);
138
141
  const problem = checkCgiBinary(backend.kind === "go" ? work : manifest.root, cgi, arch);
139
142
  if (problem) {
@@ -149,7 +152,7 @@ async function buildGoCgi(manifest, backend, arch, work, run) {
149
152
  }
150
153
  const module = goModulePath(backend.dir);
151
154
  if (!module)
152
- throw new StandaloneBuildError("STANDALONE_GO_BUILD_FAILED", `${join(backend.dir, "go.mod")} 沒有 module 行;backend 必須是一個 Go module。`);
155
+ throw new StandaloneBuildError("STANDALONE_GO_BUILD_FAILED", `${join(backend.dir, "go.mod")} has no module line; backend must be a Go module.`);
153
156
  const out = join(work, `api-${arch}.cgi`);
154
157
  const ldflags = [
155
158
  "-s",
@@ -157,6 +160,7 @@ async function buildGoCgi(manifest, backend, arch, work, run) {
157
160
  "-buildid=",
158
161
  `-X main.packageID=${manifest.package}`,
159
162
  `-X main.version=${manifest.spkVersion}`,
163
+ `-X main.requestTimeout=${requestTimeoutSeconds(backend)}`,
160
164
  `-X ${module}/internal/api.expectedPermissionsDigest=${manifest.apiPermissionsDigest}`,
161
165
  `-X ${SDK_MODULE}/api.expectedSDKPermissionsDigest=${manifest.apiSDKPermissionsDigest}`,
162
166
  ...delegateBuildFlags(manifest),
@@ -171,11 +175,11 @@ async function buildGoCgi(manifest, backend, arch, work, run) {
171
175
  env: { ...process.env, ...goBuildEnv(arch) },
172
176
  });
173
177
  if (result.missing) {
174
- throw new StandaloneBuildError("STANDALONE_GO_MISSING", "找不到 go 指令。請安裝 Go 1.25 以上版本(https://go.dev/dl/),或改用 backend.binary 打包既有執行檔。");
178
+ throw new StandaloneBuildError("STANDALONE_GO_MISSING", "The go command was not found. Install Go 1.25 or later (https://go.dev/dl/), or package an existing executable with backend.binary.");
175
179
  }
176
180
  if (result.status !== 0) {
177
181
  const fetchingSdk = result.stderr.includes(`${SDK_MODULE}@`) ? privateSdkHint() : "";
178
- throw new StandaloneBuildError("STANDALONE_GO_BUILD_FAILED", `go build(${arch})失敗。\n${result.stderr}${fetchingSdk}`);
182
+ throw new StandaloneBuildError("STANDALONE_GO_BUILD_FAILED", `go build (${arch}) failed.\n${result.stderr}${fetchingSdk}`);
179
183
  }
180
184
  return out;
181
185
  }
@@ -192,7 +196,7 @@ async function buildBackgroundWorker(manifest, arch, work, run) {
192
196
  env: { ...process.env, ...goBuildEnv(arch) },
193
197
  });
194
198
  if (result.missing || result.status !== 0) {
195
- throw new StandaloneBuildError("STANDALONE_BACKGROUND_BUILD_FAILED", `background worker(${BACKGROUND_WORKER_MAIN})build 失敗;這個專案的 backend 需要含該 package。\n${result.stderr}`);
199
+ throw new StandaloneBuildError("STANDALONE_BACKGROUND_BUILD_FAILED", `background worker (${BACKGROUND_WORKER_MAIN}) build failed; this project's backend must contain that package.\n${result.stderr}`);
196
200
  }
197
201
  const problem = checkCgiBinary(work, out, arch);
198
202
  if (problem)
@@ -18,7 +18,7 @@ export const relativeExec = (v) => typeof v === "string" && v.length <= 200
18
18
  export function readDaemon(value, root, arches, fail) {
19
19
  if (value === undefined)
20
20
  return undefined;
21
- const invalid = () => { fail({ code: "STANDALONE_DAEMON_INVALID", message: "daemon 只接受固定 exec、targets、args、env 與 timeouts;路徑須在專案內,環境不接受 loader/shell/system 變數。" }); return undefined; };
21
+ const invalid = () => { fail({ code: "STANDALONE_DAEMON_INVALID", message: "daemon accepts only fixed exec, targets, args, env and timeouts; paths must be inside the project, and the environment does not accept loader/shell/system variables." }); return undefined; };
22
22
  if (!record(value) || Object.keys(value).some(k => !fields.has(k)))
23
23
  return invalid();
24
24
  if (!relativeExec(value.exec))
@@ -85,7 +85,7 @@ export function copyDaemonPayload(manifest, arch, target, controller) {
85
85
  const stat = lstatSync(from);
86
86
  const rel = relative(realpathSync(manifest.root), realpathSync(from));
87
87
  if (rel.startsWith("..") || isAbsolute(rel) || stat.isSymbolicLink())
88
- invalid(`daemon payload 路徑不可含 symlink 或跳出專案:${from}`);
88
+ invalid(`A daemon payload path must not contain a symlink or escape the project: ${from}`);
89
89
  if (stat.isDirectory()) {
90
90
  mkdirSync(to, { recursive: true });
91
91
  chmodSync(to, 0o755);
@@ -95,22 +95,22 @@ export function copyDaemonPayload(manifest, arch, target, controller) {
95
95
  else if (stat.isFile()) {
96
96
  total += stat.size;
97
97
  if (total > 512 * 1024 * 1024 || (stat.mode & 0o6000))
98
- invalid(`daemon payload 不接受 setid 或超過 512 MiB:${from}`);
98
+ invalid(`The daemon payload does not accept setid bits or more than 512 MiB: ${from}`);
99
99
  copyFileSync(from, to);
100
100
  chmodSync(to, stat.mode & 0o111 ? 0o755 : 0o644);
101
101
  }
102
102
  else
103
- invalid(`daemon payload 只接受一般檔案及目錄:${from}`);
103
+ invalid(`The daemon payload accepts only regular files and directories: ${from}`);
104
104
  };
105
105
  try {
106
106
  if (!lstatSync(source).isDirectory())
107
- invalid("daemon.targets 必須指向目錄。");
107
+ invalid("daemon.targets must point at a directory.");
108
108
  walk(source, join(target, "payload"));
109
109
  }
110
110
  catch (error) {
111
111
  if (error instanceof StandaloneBuildError)
112
112
  throw error;
113
- invalid(`無法讀取 daemon payload:${String(error)}`);
113
+ invalid(`Cannot read the daemon payload: ${String(error)}`);
114
114
  }
115
115
  const exec = join(source, daemon.exec);
116
116
  const problem = checkCgiBinary(manifest.root, exec, arch);
@@ -1,5 +1,6 @@
1
1
  import type { EntryMode, StandaloneManifest } from "./manifest.js";
2
2
  import { type AppTitle } from "../build-spk/ui-texts.js";
3
+ import { type PackageIcons } from "../build-spk/dsm-icons.js";
3
4
  /** Same path the generated wrapper POSTs to, and the payload location of the CGI. */
4
5
  export declare const CGI_PAYLOAD_PATH = "ui/cgi/api.cgi";
5
6
  export declare function cgiUrl(packageId: string): string;
@@ -19,12 +20,16 @@ export declare function windowScriptName(manifest: Pick<StandaloneManifest, "bui
19
20
  export declare function entryLauncherAccess(entry: EntryMode): Record<string, unknown>;
20
21
  export declare function appTitle(manifest: StandaloneManifest): AppTitle;
21
22
  /** `ui/config`: registers the DSM main-menu app and its window class. */
22
- export declare function renderUiConfig(manifest: StandaloneManifest): string;
23
+ export declare function renderUiConfig(manifest: StandaloneManifest, icons?: PackageIcons): string;
23
24
  /**
24
25
  * The DSM AppWindow that hosts the project's UI. It loads app/app.js and
25
- * app/app.css once, then calls the app's mount(root, ctx). `ctx.call` is the
26
- * only transport: same-origin POST with DSM's session cookie and SynoToken.
26
+ * app/app.css once, then calls the app's mount(root, ctx). `ctx.call(method,
27
+ * params, {signal})` is the only transport: same-origin POST with DSM's session
28
+ * cookie and SynoToken, given up past `ctx.requestTimeout` like the SDK's.
27
29
  * `ctx.t` / `ctx.locale` translate with the catalogs embedded at build time.
28
- * `assetVersion` busts browser caches when the UI changes.
30
+ * `assetVersion` busts browser caches when the UI changes. A Portal/standalone
31
+ * launch owns the whole page: `ctx.fullPage` tells the app to lay out full-page
32
+ * (and add a device-width viewport), and `ctx.appIcon` is the package icon a
33
+ * full-page shell shows beside the name, where a DSM window has a title bar.
29
34
  */
30
- export declare function renderWindowScript(manifest: StandaloneManifest, assetVersion: string): string;
35
+ export declare function renderWindowScript(manifest: StandaloneManifest, assetVersion: string, icons?: PackageIcons): string;
@@ -6,8 +6,13 @@ import { permitsOrdinaryUsers } from "./api-permissions.js";
6
6
  import { PORTAL_RUNTIME } from "./portal-window.js";
7
7
  import { portalByExecutor } from "../build-spk/dsm-version.js";
8
8
  import { uiTitleFields } from "../build-spk/ui-texts.js";
9
+ import { packageIcons, windowIconFile, windowIconTemplate } from "../build-spk/dsm-icons.js";
10
+ import { DEFAULT_WINDOW, FULL_PAGE_MIN_WIDTH } from "./window-size.js";
11
+ import { requestTimeoutSeconds } from "./request-timeout.js";
9
12
  /** Same path the generated wrapper POSTs to, and the payload location of the CGI. */
10
13
  export const CGI_PAYLOAD_PATH = "ui/cgi/api.cgi";
14
+ /** The SDK's clientTimeout(requestTimeout) waits this much past the backend deadline. */
15
+ const CLIENT_TIMEOUT_MARGIN = 2000;
11
16
  export function cgiUrl(packageId) {
12
17
  return `/webman/3rdparty/${packageId}/cgi/api.cgi`;
13
18
  }
@@ -68,7 +73,8 @@ export function appTitle(manifest) {
68
73
  return { displayname: manifest.displayname, description: manifest.description, info: manifest.i18n?.info };
69
74
  }
70
75
  /** `ui/config`: registers the DSM main-menu app and its window class. */
71
- export function renderUiConfig(manifest) {
76
+ export function renderUiConfig(manifest, icons = packageIcons(manifest.icon)) {
77
+ const icon = windowIconTemplate(icons);
72
78
  const ns = `SYNO.SDS.${manifest.package}`;
73
79
  const titleFields = uiTitleFields(appTitle(manifest));
74
80
  return `${JSON.stringify({
@@ -76,7 +82,7 @@ export function renderUiConfig(manifest) {
76
82
  [`${ns}.Instance`]: {
77
83
  type: "app",
78
84
  ...titleFields,
79
- icon: "images/app_{0}.png",
85
+ icon,
80
86
  appWindow: `${ns}.MainWindow`,
81
87
  allowMultiInstance: false,
82
88
  ...launcherAccess(manifest),
@@ -86,7 +92,7 @@ export function renderUiConfig(manifest) {
86
92
  type: "lib",
87
93
  title: titleFields.title,
88
94
  texts: titleFields.texts,
89
- icon: "images/app_{0}.png",
95
+ icon,
90
96
  depend: [],
91
97
  },
92
98
  },
@@ -94,37 +100,68 @@ export function renderUiConfig(manifest) {
94
100
  }
95
101
  /**
96
102
  * The DSM AppWindow that hosts the project's UI. It loads app/app.js and
97
- * app/app.css once, then calls the app's mount(root, ctx). `ctx.call` is the
98
- * only transport: same-origin POST with DSM's session cookie and SynoToken.
103
+ * app/app.css once, then calls the app's mount(root, ctx). `ctx.call(method,
104
+ * params, {signal})` is the only transport: same-origin POST with DSM's session
105
+ * cookie and SynoToken, given up past `ctx.requestTimeout` like the SDK's.
99
106
  * `ctx.t` / `ctx.locale` translate with the catalogs embedded at build time.
100
- * `assetVersion` busts browser caches when the UI changes.
107
+ * `assetVersion` busts browser caches when the UI changes. A Portal/standalone
108
+ * launch owns the whole page: `ctx.fullPage` tells the app to lay out full-page
109
+ * (and add a device-width viewport), and `ctx.appIcon` is the package icon a
110
+ * full-page shell shows beside the name, where a DSM window has a title bar.
101
111
  */
102
- export function renderWindowScript(manifest, assetVersion) {
112
+ export function renderWindowScript(manifest, assetVersion, icons = packageIcons(manifest.icon)) {
103
113
  const pkg = manifest.package;
104
114
  const ns = `SYNO.SDS.${pkg}`;
105
115
  const p = JSON.stringify(pkg);
106
116
  const root = manifest.executor ? renderRootParts(ns, manifest) : undefined;
117
+ const size = manifest.ui.window ?? DEFAULT_WINDOW;
118
+ const requestTimeout = requestTimeoutSeconds(manifest.backend) * 1000;
107
119
  return `/* Generated by synopkgland build. Edit your UI in the project's ui folder. */
108
120
  Ext.ns(${JSON.stringify(ns)});
109
121
 
110
- ${ns}.call = function (method, params) {
122
+ // The SDK's clientTimeout: past the backend's request deadline
123
+ // (ctx.requestTimeout, from backend.timeout_seconds), so its request_timeout
124
+ // reply arrives first. options.signal aborts the fetch, for a page the user leaves.
125
+ ${ns}.call = function (method, params, options, requestTimeout) {
126
+ var signal = options && options.signal;
127
+ var canceled = { code: "request_canceled", message: "The request was canceled" };
128
+ if (signal && signal.aborted) {
129
+ return Promise.reject(canceled);
130
+ }
111
131
  var headers = { "Content-Type": "application/json" };
112
132
  var token = SYNO.SDS.Session && SYNO.SDS.Session.SynoToken;
113
133
  if (token) {
114
134
  headers["X-SYNO-TOKEN"] = token;
115
135
  }
136
+ var controller = new AbortController();
137
+ var failure = null;
138
+ var stop = function (error) {
139
+ failure = failure || error;
140
+ controller.abort();
141
+ };
142
+ var leave = function () { stop(canceled); };
143
+ var timer = setTimeout(function () {
144
+ stop({ code: "request_timeout", message: "The NAS did not answer in time" });
145
+ }, (requestTimeout || ${requestTimeout}) + ${CLIENT_TIMEOUT_MARGIN});
146
+ if (signal) signal.addEventListener("abort", leave);
147
+ var done = function () {
148
+ clearTimeout(timer);
149
+ if (signal) signal.removeEventListener("abort", leave);
150
+ };
116
151
  return fetch(${JSON.stringify(cgiUrl(pkg))}, {
117
152
  method: "POST",
118
153
  credentials: "same-origin",
119
154
  headers: headers,
120
- body: JSON.stringify({ method: method, params: params || {} })
155
+ body: JSON.stringify({ method: method, params: params || {} }),
156
+ signal: controller.signal
121
157
  }).then(function (response) {
122
158
  return response.json().catch(function () {
123
159
  return { success: false, error: { code: "bad_response", message: "HTTP " + response.status } };
124
160
  });
125
161
  }, function () {
126
- return { success: false, error: { code: "network", message: "The NAS did not answer" } };
162
+ return { success: false, error: failure || { code: "network", message: "The NAS did not answer" } };
127
163
  }).then(function (body) {
164
+ done();
128
165
  if (body && body.success === true) {
129
166
  return body.data;
130
167
  }
@@ -173,6 +210,13 @@ ${root ? root.definitions : ""}${manifest.portal ? renderPortalDefinition(ns) :
173
210
  document.head.appendChild(js);
174
211
  };
175
212
 
213
+ // A Portal/standalone launch is the whole page, on a phone as well: no desktop
214
+ // minimum width, and the app lays itself out full-page.
215
+ ${ns}.fullPage = function () {
216
+ try { return typeof _S === "function" && _S("standalone") === true; } catch (error) { return false; }
217
+ };
218
+ ${ns}.appIcon = ${JSON.stringify(`webman/3rdparty/${pkg}/${windowIconFile(icons, 64)}`)};
219
+
176
220
  // DSM reopens a window at the size it was last left at, even one saved on a
177
221
  // larger screen, and only moves a window that does not fit: keep it inside.
178
222
  ${ns}.fitDesktop = function (size) {
@@ -191,7 +235,7 @@ Ext.define(${JSON.stringify(`${ns}.MainWindow`)}, {
191
235
  extend: "SYNO.SDS.AppWindow",
192
236
  constructor: function (config) {
193
237
  this.callParent([${ns}.fitDesktop(Ext.apply({
194
- width: 960, height: 640, minWidth: 700, minHeight: 420, layout: "fit",
238
+ width: ${size.width}, height: ${size.height}, minWidth: ${ns}.fullPage() ? ${FULL_PAGE_MIN_WIDTH} : ${size.minWidth}, minHeight: ${size.minHeight}, layout: "fit",
195
239
  ${root ? root.html : `html: '<div class="dsmapp-root" style="height:100%"></div>'`}
196
240
  }, config))]);
197
241
  },
@@ -212,6 +256,8 @@ Ext.define(${JSON.stringify(`${ns}.MainWindow`)}, {
212
256
  me.reopenHandlers = me.reopenEvents.handlers;
213
257
  var lang = "enu";
214
258
  try { if (typeof _S === "function") lang = _S("lang"); } catch (error) {}
259
+ var ctx = { requestTimeout: ${requestTimeout} };
260
+ var call = function (method, params, options) { return ${ns}.call(method, params, options, ctx.requestTimeout); };
215
261
  ${root ? root.mount : ""} ${ns}.loadApp(function (app) {
216
262
  if (me.mountToken !== mountToken) return;
217
263
  me.mountPending = false;
@@ -221,18 +267,20 @@ ${root ? root.mount : ""} ${ns}.loadApp(function (app) {
221
267
  me.hostAccess = ${ns}.createHost({ appWindow: me, locale: i18n.locale });
222
268
  root.lang = i18n.locale;
223
269
  try {
224
- me.mounted = app.mount(root, {
270
+ me.mounted = app.mount(root, Ext.apply(ctx, {
225
271
  displayName: ${JSON.stringify(manifest.displayname)},
226
- call: ${ns}.call,
272
+ call: call,
227
273
  files: {endpoint: ${JSON.stringify(cgiUrl(pkg))}, token: function () { return SYNO.SDS.Session && SYNO.SDS.Session.SynoToken; }},
228
274
  reopen: me.reopenEvents.subscribe,
229
275
  user: ${ns}.sessionUser(),${manifest.portal ? `\n portal: ${renderPortalAccess(ns, manifest)},` : ""}
230
276
  host: me.hostAccess,
231
277
  appWindow: me,
278
+ fullPage: ${ns}.fullPage(),
279
+ appIcon: ${ns}.appIcon,
232
280
  lang: lang,
233
281
  locale: i18n.locale,
234
282
  t: i18n.t${root ? `,\n executor: access` : ""}
235
- }) || {};
283
+ })) || {};
236
284
  } catch (error) {
237
285
  me.hostAccess.dispose();
238
286
  me.hostAccess = null;
@@ -283,7 +331,7 @@ function renderPortalAccess(ns, manifest) {
283
331
  defaultAlias: ${JSON.stringify(manifest.portal.alias)},${portalByExecutor(manifest.os_min_ver) ? "\n entry: true," : ""}
284
332
  isAdmin: function () { return ${ns}.sessionUser().isAdmin; },
285
333
  send: ${ns}.webApi(me),
286
- call: ${ns}.call,
334
+ call: call,
287
335
  launch: function (app, params) { SYNO.SDS.AppLaunch(app, params); },
288
336
  location: window.location,
289
337
  locale: ${ns}.createI18n(lang, ${JSON.stringify(Object.fromEntries(Object.keys(DSM_LANGS).map((locale) => [locale, {}])))}).locale
@@ -315,6 +363,9 @@ const ROOT_PANEL_CSS = [
315
363
  ".dsmapp-rootpanel-actions button{margin-right:8px;padding:4px 14px}",
316
364
  ".dsmapp-rootpanel-primary{background:#057feb;color:#fff;border:1px solid #057feb;border-radius:3px}",
317
365
  ".dsmapp-rootpanel-hint{color:#526070;font-size:12px}.dsmapp-rootpanel [hidden]{display:none}",
366
+ // The panels keep their height; the App root fills the rest of the window body, as without root operations.
367
+ ".dsmapp-rootwindow{display:flex;flex-direction:column;height:100%}",
368
+ ".dsmapp-rootwindow>.dsmapp-rootpanels{flex:none}.dsmapp-rootwindow>.dsmapp-root{flex:1 1 0;min-height:0}",
318
369
  ].join("");
319
370
  /**
320
371
  * A Vue app approves in the SDK's ExecutorSettings, which also carries an
@@ -326,7 +377,7 @@ function settingsOnlyApproval(manifest) {
326
377
  /**
327
378
  * The capabilities.rootOps: true additions to the window: the exact tasks and panel
328
379
  * text, the access runtime wired to DSM's own password confirmation and the
329
- * AppWindow's WebAPI channel, and the panel above the app.
380
+ * AppWindow's WebAPI channel, and the panel above the app, which fills the rest.
330
381
  */
331
382
  function renderRootParts(ns, manifest) {
332
383
  return {
@@ -346,7 +397,7 @@ ${PASSWORD_CONFIRM}
346
397
 
347
398
  `,
348
399
  html: `autoScroll: true,
349
- html: '<style>${ROOT_PANEL_CSS}</style><div class="dsmapp-rootpanels"></div><div class="dsmapp-root"></div>'`,
400
+ html: '<style>${ROOT_PANEL_CSS}</style><div class="dsmapp-rootwindow"><div class="dsmapp-rootpanels"></div><div class="dsmapp-root"></div></div>'`,
350
401
  mount: ` var session = function () {
351
402
  var current = SYNO.SDS.Session || {};
352
403
  return { name: current.user, admin: current.is_admin === true };
@@ -357,7 +408,7 @@ ${PASSWORD_CONFIRM}
357
408
  user: session,
358
409
  send: ${ns}.webApi(me),
359
410
  confirmPassword: ${ns}.createPasswordConfirm(me),
360
- call: ${ns}.call,
411
+ call: call,
361
412
  wait: function (ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }
362
413
  });
363
414
  ${settingsOnlyApproval(manifest) ? "" : ` var panels = me.body && me.body.dom.querySelector(".dsmapp-rootpanels");
@@ -23,15 +23,15 @@ export function readInfoTranslations(value, check) {
23
23
  return {};
24
24
  }
25
25
  if (!isRecord(value)) {
26
- return check.invalid("必須以語言為 key,例如 zh-TW: { displayname, description }。");
26
+ return check.invalid("It must be keyed by language, for example zh-TW: { displayname, description }.");
27
27
  }
28
28
  const info = {};
29
29
  for (const [locale, text] of Object.entries(value)) {
30
30
  if (!isLocale(locale) || locale === "en") {
31
- return check.invalid(`的「${locale}」不是支援的翻譯語言;英文寫在頂層 displayname / description。`);
31
+ return check.invalid(`"${locale}" is not a supported translation language; English goes in the top-level displayname / description.`);
32
32
  }
33
33
  if (!isRecord(text) || Object.keys(text).some((key) => !INFO_TEXT_FIELDS.has(key))) {
34
- return check.invalid(`.${locale} 只接受 displayname 與 description。`);
34
+ return check.invalid(`.${locale} accepts only displayname and description.`);
35
35
  }
36
36
  const entry = {};
37
37
  for (const field of INFO_TEXT_FIELDS) {
@@ -66,14 +66,14 @@ function readCatalog(file, locale) {
66
66
  raw = JSON.parse(readFileSync(file, "utf8"));
67
67
  }
68
68
  catch (error) {
69
- return invalid(`不是合法的 JSON:${String(error)}`);
69
+ return invalid(`is not valid JSON: ${String(error)}`);
70
70
  }
71
71
  if (!isRecord(raw)) {
72
- return invalid("必須是 { \"key\": \"text\" } 的 JSON 物件。");
72
+ return invalid("It must be a { \"key\": \"text\" } JSON object.");
73
73
  }
74
74
  for (const [key, text] of Object.entries(raw)) {
75
75
  if (!MESSAGE_KEY.test(key) || typeof text !== "string") {
76
- return invalid(`的「${key}」必須是點分隔的 key(例如 greeting.save)對應一段文字。`);
76
+ return invalid(`"${key}" must be a dot-separated key (for example greeting.save) mapped to a text.`);
77
77
  }
78
78
  }
79
79
  return { catalog: raw };
@@ -88,7 +88,7 @@ export function loadCatalogs(dir) {
88
88
  if (!isLocale(locale)) {
89
89
  errors.push({
90
90
  code: "STANDALONE_I18N_LOCALE_UNSUPPORTED",
91
- message: `${name}:只支援 ${LOCALES.join(", ")};其他 DSM 語言會顯示英文。`,
91
+ message: `${name}: only ${LOCALES.join(", ")} are supported; every other DSM language shows English.`,
92
92
  });
93
93
  continue;
94
94
  }
@@ -103,7 +103,7 @@ export function loadCatalogs(dir) {
103
103
  const en = catalogs.en;
104
104
  if (!en) {
105
105
  if (!errors.some((e) => e.code === "STANDALONE_I18N_CATALOG_INVALID")) {
106
- errors.push({ code: "STANDALONE_I18N_MESSAGES_MISSING", message: `${dir} 缺少 en.json(所有語言的後備文字)。` });
106
+ errors.push({ code: "STANDALONE_I18N_MESSAGES_MISSING", message: `${dir} is missing en.json (the fallback text for every language).` });
107
107
  }
108
108
  return { ok: false, errors };
109
109
  }
@@ -117,30 +117,30 @@ function compareWithEnglish(locale, catalog, en) {
117
117
  const extra = Object.keys(catalog).filter((key) => !(key in en));
118
118
  if (missing.length > 0 || extra.length > 0) {
119
119
  const parts = [
120
- missing.length > 0 ? `缺少 ${missing.join(", ")}` : "",
121
- extra.length > 0 ? `多出 ${extra.join(", ")}` : "",
120
+ missing.length > 0 ? `missing ${missing.join(", ")}` : "",
121
+ extra.length > 0 ? `extra ${extra.join(", ")}` : "",
122
122
  ].filter(Boolean);
123
- return [{ code: "STANDALONE_I18N_KEYS_MISMATCH", message: `${locale}.json 與 en.json 的 key 不同:${parts.join(";")}。` }];
123
+ return [{ code: "STANDALONE_I18N_KEYS_MISMATCH", message: `${locale}.json keys differ from en.json: ${parts.join("; ")}.` }];
124
124
  }
125
125
  const drifted = Object.keys(en).filter((key) => placeholders(catalog[key]) !== placeholders(en[key]));
126
126
  return drifted.length === 0
127
127
  ? []
128
128
  : [{
129
129
  code: "STANDALONE_I18N_PLACEHOLDER_MISMATCH",
130
- message: `${locale}.json 的 ${drifted.join(", ")} 與 en.json 的 {placeholder} 不一致。`,
130
+ message: `${locale}.json has ${drifted.join(", ")} with placeholders that differ from en.json.`,
131
131
  }];
132
132
  }
133
133
  function readGlossary(file) {
134
- const invalid = (why) => ({ code: "STANDALONE_I18N_GLOSSARY_INVALID", message: `${file}:${why}` });
134
+ const invalid = (why) => ({ code: "STANDALONE_I18N_GLOSSARY_INVALID", message: `${file}: ${why}` });
135
135
  let raw;
136
136
  try {
137
137
  raw = JSON.parse(readFileSync(file, "utf8"));
138
138
  }
139
139
  catch (error) {
140
- return invalid(`不是合法的 JSON:${String(error)}`);
140
+ return invalid(`is not valid JSON: ${String(error)}`);
141
141
  }
142
142
  if (!isRecord(raw) || !Array.isArray(raw.terms)) {
143
- return invalid("必須是 { \"terms\": [...] }。");
143
+ return invalid("It must be { \"terms\": [...] }.");
144
144
  }
145
145
  const terms = [];
146
146
  for (const term of raw.terms) {
@@ -150,7 +150,7 @@ function readGlossary(file) {
150
150
  && Object.entries(term.text).every(([locale, text]) => isLocale(locale) && typeof text === "string" && text !== "")
151
151
  && (term.keys === undefined || (Array.isArray(term.keys) && term.keys.every((key) => typeof key === "string")));
152
152
  if (!ok) {
153
- return invalid(`term ${isRecord(term) ? JSON.stringify(term.id) : "?"} 需要 id、以支援語言為 key 的 text,以及選填的 keys 清單。`);
153
+ return invalid(`term ${isRecord(term) ? JSON.stringify(term.id) : "?"} needs id, text keyed by a supported language, and an optional keys list.`);
154
154
  }
155
155
  terms.push({ id: term.id, text: term.text, keys: term.keys ?? [] });
156
156
  }
@@ -173,7 +173,7 @@ export function checkGlossary(file, catalogs) {
173
173
  for (const term of terms) {
174
174
  for (const key of term.keys) {
175
175
  if (!(key in catalogs.en)) {
176
- errors.push({ code: "STANDALONE_I18N_GLOSSARY_INVALID", message: `glossary term ${term.id} 指向不存在的 key ${key}。` });
176
+ errors.push({ code: "STANDALONE_I18N_GLOSSARY_INVALID", message: `glossary term ${term.id} points at a key that does not exist: ${key}.` });
177
177
  continue;
178
178
  }
179
179
  for (const [locale, catalog] of Object.entries(catalogs)) {
@@ -181,7 +181,7 @@ export function checkGlossary(file, catalogs) {
181
181
  if (official !== undefined && !containsTerm(catalog[key], official, locale)) {
182
182
  errors.push({
183
183
  code: "STANDALONE_I18N_GLOSSARY_MISMATCH",
184
- message: `${locale}.json 的 ${key}「${catalog[key]}」沒有使用 Synology 用語「${official}」(glossary term ${term.id})。`,
184
+ message: `${locale}.json ${key} "${catalog[key]}" does not use the Synology term "${official}" (glossary term ${term.id}).`,
185
185
  });
186
186
  }
187
187
  }
@@ -0,0 +1,31 @@
1
+ type Payload = Array<{
2
+ path: string;
3
+ mode: string;
4
+ }>;
5
+ export interface BridgeJson {
6
+ runtime?: string;
7
+ entry?: string;
8
+ methods?: string[];
9
+ node?: {
10
+ package?: string;
11
+ };
12
+ capabilities?: {
13
+ root?: boolean;
14
+ delegate?: {
15
+ executor_digest?: string;
16
+ policy_digest?: string;
17
+ };
18
+ };
19
+ }
20
+ /**
21
+ * A Python, Node or Rust App: the runtime CGI binds bridge.json and
22
+ * permissions.json by digest, the App lives in target/app (never under ui/),
23
+ * and its entry, SDK and dependencies are in the package.
24
+ */
25
+ export declare function checkBridgeApp(dir: string, backend: string, info: Record<string, string>, payload: Payload, findings: string[]): Promise<BridgeJson | undefined>;
26
+ /** root and delegate reach the package's executor; delegate pins the one this SPK ships. */
27
+ export declare function checkBridgeExecutor(bridge: BridgeJson, executor: {
28
+ executorDigest: string;
29
+ policyDigest: string;
30
+ } | undefined, findings: string[]): void;
31
+ export {};