@synopackageland/templates 0.9.9 → 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 (137) hide show
  1. package/README.md +29 -29
  2. package/package.json +2 -2
  3. package/templates/_shared/AGENTS.md +53 -15
  4. package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
  5. package/templates/native-go/backend/cmd/app/main.go +15 -1
  6. package/templates/native-go/backend/cmd/app/main_test.go +33 -0
  7. package/templates/native-go/backend/go.mod +1 -1
  8. package/templates/native-go/backend/go.sum +2 -2
  9. package/templates/native-go/i18n/messages/de.json +4 -0
  10. package/templates/native-go/i18n/messages/en.json +4 -0
  11. package/templates/native-go/i18n/messages/es.json +4 -0
  12. package/templates/native-go/i18n/messages/fr.json +4 -0
  13. package/templates/native-go/i18n/messages/it.json +4 -0
  14. package/templates/native-go/i18n/messages/ja.json +4 -0
  15. package/templates/native-go/i18n/messages/ko.json +4 -0
  16. package/templates/native-go/i18n/messages/zh-CN.json +4 -0
  17. package/templates/native-go/i18n/messages/zh-TW.json +4 -0
  18. package/templates/native-go/ui/app.js +2 -1
  19. package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +24 -3
  20. package/templates/native-go-jobs/frontend/test/background-page.test.ts +4 -4
  21. package/templates/native-go-root-ops/backend/internal/app/executor.go +2 -65
  22. package/templates/native-go-root-ops/backend/internal/app/executor_test.go +10 -82
  23. package/templates/native-go-root-ops/frontend/src/ExecutorGate.vue +16 -0
  24. package/templates/native-go-root-ops/frontend/test/executor-gate.test.ts +60 -0
  25. package/templates/native-go-vue/README.md +24 -8
  26. package/templates/native-go-vue/frontend/package-lock.json +9 -9
  27. package/templates/native-go-vue/frontend/package.json +2 -2
  28. package/templates/native-go-vue/frontend/src/App.vue +36 -36
  29. package/templates/native-go-vue/frontend/src/ExecutorGate.vue +10 -0
  30. package/templates/native-go-vue/frontend/test/apps.test.ts +65 -12
  31. package/templates/native-go-vue/frontend/test/context.ts +8 -0
  32. package/templates/native-go-vue/frontend/test/native-controls.test.ts +3 -4
  33. package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +2 -2
  34. package/templates/native-node/README.md +153 -0
  35. package/templates/native-node/backend/app/package.json +4 -0
  36. package/templates/native-node/backend/app/permissions.json +5 -0
  37. package/templates/native-node/backend/package-lock.json +59 -0
  38. package/templates/native-node/backend/package.json +17 -0
  39. package/templates/native-node/backend/src/delegate.ts +5 -0
  40. package/templates/native-node/backend/src/jobs.ts +5 -0
  41. package/templates/native-node/backend/src/main.ts +4 -0
  42. package/templates/native-node/backend/src/params.ts +11 -0
  43. package/templates/native-node/backend/src/root-ops.ts +5 -0
  44. package/templates/native-node/backend/src/routes.ts +97 -0
  45. package/templates/native-node/backend/src/service.ts +22 -0
  46. package/templates/native-node/backend/test/routes.test.ts +75 -0
  47. package/templates/native-node/backend/test/support.ts +67 -0
  48. package/templates/native-node/backend/tsconfig.build.json +8 -0
  49. package/templates/native-node/backend/tsconfig.json +18 -0
  50. package/templates/native-node/dsm-package.yaml +80 -0
  51. package/templates/native-node/gitignore +4 -0
  52. package/templates/native-node-delegate/backend/src/delegate.ts +35 -0
  53. package/templates/native-node-delegate/backend/test/delegate.test.ts +51 -0
  54. package/templates/native-node-jobs/backend/src/jobs.ts +38 -0
  55. package/templates/native-node-jobs/backend/test/jobs.test.ts +73 -0
  56. package/templates/native-node-root-ops/backend/src/root-ops.ts +16 -0
  57. package/templates/native-node-root-ops/backend/test/root-ops.test.ts +23 -0
  58. package/templates/native-node-vue/README.md +56 -0
  59. package/templates/native-node-vue/gitignore +4 -0
  60. package/templates/native-python/README.md +150 -0
  61. package/templates/native-python/backend/app/delegate.py +5 -0
  62. package/templates/native-python/backend/app/jobs.py +5 -0
  63. package/templates/native-python/backend/app/main.py +5 -0
  64. package/templates/native-python/backend/app/params.py +10 -0
  65. package/templates/native-python/backend/app/permissions.json +5 -0
  66. package/templates/native-python/backend/app/root_ops.py +5 -0
  67. package/templates/native-python/backend/app/routes.py +107 -0
  68. package/templates/native-python/backend/tests/support.py +72 -0
  69. package/templates/native-python/backend/tests/test_routes.py +73 -0
  70. package/templates/native-python/dsm-package.yaml +77 -0
  71. package/templates/native-python/gitignore +2 -0
  72. package/templates/native-python-delegate/backend/app/delegate.py +42 -0
  73. package/templates/native-python-delegate/backend/tests/test_delegate.py +58 -0
  74. package/templates/native-python-jobs/backend/app/jobs.py +40 -0
  75. package/templates/native-python-jobs/backend/tests/test_jobs.py +72 -0
  76. package/templates/native-python-root-ops/backend/app/root_ops.py +18 -0
  77. package/templates/native-python-root-ops/backend/tests/test_root_ops.py +34 -0
  78. package/templates/native-python-vue/README.md +56 -0
  79. package/templates/native-python-vue/gitignore +3 -0
  80. package/templates/native-rust/README.md +74 -296
  81. package/templates/native-rust/backend/Cargo.lock +6 -6
  82. package/templates/native-rust/backend/Cargo.toml +2 -10
  83. package/templates/native-rust/backend/src/delegate.rs +6 -0
  84. package/templates/native-rust/backend/src/jobs.rs +6 -0
  85. package/templates/native-rust/backend/src/main.rs +12 -0
  86. package/templates/native-rust/backend/src/root_ops.rs +6 -0
  87. package/templates/native-rust/backend/src/routes.rs +352 -0
  88. package/templates/native-rust/backend/src/test_support.rs +91 -0
  89. package/templates/native-rust/dsm-package.yaml +10 -21
  90. package/templates/native-rust-delegate/backend/src/delegate.rs +171 -0
  91. package/templates/native-rust-jobs/backend/src/jobs.rs +240 -0
  92. package/templates/native-rust-root-ops/backend/src/root_ops.rs +83 -0
  93. package/templates/native-rust-vue/README.md +35 -34
  94. package/templates/native-rust/backend/THIRD_PARTY_NOTICES.txt +0 -207
  95. package/templates/native-rust/backend/src/app/background/mod.rs +0 -424
  96. package/templates/native-rust/backend/src/app/background/owner.rs +0 -77
  97. package/templates/native-rust/backend/src/app/background/owner_tests.rs +0 -174
  98. package/templates/native-rust/backend/src/app/background/schedule.rs +0 -126
  99. package/templates/native-rust/backend/src/app/background/schedule_tests.rs +0 -108
  100. package/templates/native-rust/backend/src/app/background/tests.rs +0 -505
  101. package/templates/native-rust/backend/src/app/background/work.rs +0 -78
  102. package/templates/native-rust/backend/src/app/background/work_tests.rs +0 -123
  103. package/templates/native-rust/backend/src/app/background_routes.rs +0 -22
  104. package/templates/native-rust/backend/src/app/delegate_config.rs +0 -188
  105. package/templates/native-rust/backend/src/app/delegate_routes.rs +0 -22
  106. package/templates/native-rust/backend/src/app/identity.rs +0 -56
  107. package/templates/native-rust/backend/src/app/migrations.rs +0 -79
  108. package/templates/native-rust/backend/src/app/mod.rs +0 -30
  109. package/templates/native-rust/backend/src/app/portal.rs +0 -317
  110. package/templates/native-rust/backend/src/app/root_routes.rs +0 -23
  111. package/templates/native-rust/backend/src/app/routes.rs +0 -315
  112. package/templates/native-rust/backend/src/app/session_files.rs +0 -19
  113. package/templates/native-rust/backend/src/app/shared_folder.rs +0 -383
  114. package/templates/native-rust/backend/src/app/test_support.rs +0 -202
  115. package/templates/native-rust/backend/src/app/uninstall.rs +0 -38
  116. package/templates/native-rust/backend/src/bin/api.rs +0 -72
  117. package/templates/native-rust/backend/src/bin/background-worker.rs +0 -37
  118. package/templates/native-rust/backend/src/build_env.rs +0 -155
  119. package/templates/native-rust/backend/src/lib.rs +0 -6
  120. package/templates/native-rust/backend/src/permissions.rs +0 -54
  121. package/templates/native-rust/backend/src/process.rs +0 -91
  122. package/templates/native-rust/i18n/glossary.json +0 -665
  123. package/templates/native-rust/i18n/messages/de.json +0 -32
  124. package/templates/native-rust/i18n/messages/en.json +0 -32
  125. package/templates/native-rust/i18n/messages/es.json +0 -32
  126. package/templates/native-rust/i18n/messages/fr.json +0 -32
  127. package/templates/native-rust/i18n/messages/it.json +0 -32
  128. package/templates/native-rust/i18n/messages/ja.json +0 -32
  129. package/templates/native-rust/i18n/messages/ko.json +0 -32
  130. package/templates/native-rust/i18n/messages/zh-CN.json +0 -32
  131. package/templates/native-rust/i18n/messages/zh-TW.json +0 -32
  132. package/templates/native-rust/ui/app.css +0 -45
  133. package/templates/native-rust/ui/app.js +0 -111
  134. package/templates/native-rust-delegate/backend/src/app/delegate_routes.rs +0 -92
  135. package/templates/native-rust-delegate/backend/src/bin/background-worker.rs +0 -52
  136. package/templates/native-rust-jobs/backend/src/app/background_routes.rs +0 -109
  137. package/templates/native-rust-root-ops/backend/src/app/root_routes.rs +0 -475
package/README.md CHANGED
@@ -7,15 +7,15 @@ the files an App owns; the SDK (`github.com/synopackageland/syno-pkg-land-sdk`,
7
7
  | Template | What it adds |
8
8
  | --- | --- |
9
9
  | `native-go` | Base: `dsm-package.yaml`, Go module (`backend/cmd/{app,background-worker}`, `backend/internal/app`, `backend/internal/api/permissions.json`), plain `ui/`, i18n |
10
- | `native-go-vue` | Vue shell in `frontend/` (`App.vue`, `main.ts`, app i18n, `CapabilityPanels.vue`) |
10
+ | `native-go-vue` | Vue shell in `frontend/` (`App.vue`, `main.ts`, app i18n, `CapabilityPanels.vue`, `ExecutorGate.vue`) |
11
11
  | `native-go-jobs` | Background job routes in `internal/app` and their page test |
12
12
  | `native-go-delegate` | Delegate routes, the delegate background worker, the delegate `CapabilityPanels.vue` |
13
- | `native-go-root-ops` | Executor routes in `internal/app` and an example `root/whoami.sh` |
14
- | `native-rust` | Base: `dsm-package.yaml` with `backend.kind: rust`, Cargo package `app` (`backend/src/bin/{api,background-worker}.rs`, `backend/src/app`, `backend/permissions.json`, committed `Cargo.lock`, `rust-toolchain.toml`), plain `ui/`, i18n |
15
- | `native-rust-vue` | Only the Vue README and gitignore; the shell itself is `native-go-vue`'s `frontend/` |
16
- | `native-rust-jobs` | `backend/src/app/background_routes.rs`: the background job routes and their tests |
17
- | `native-rust-delegate` | `delegate_routes.rs` and the delegate `background-worker.rs` |
18
- | `native-rust-root-ops` | `root_routes.rs`: the executor routes and their tests |
13
+ | `native-go-root-ops` | The SDK's executor routes and `root.run` in `internal/app`, an example `root/whoami.sh`, and the Vue `ExecutorGate.vue` that holds the pages until the executor is approved |
14
+ | `native-python` | Base: `dsm-package.yaml` (`backend.runtime: python`), the App in `backend/app` (`routes.py`, stub capability modules, `permissions.json`), unittest tests |
15
+ | `native-node` | Base: `dsm-package.yaml` (`backend.runtime: node`), TypeScript in `backend/src` compiled into `backend/app`, node:test tests, `package-lock.json` |
16
+ | `native-rust` | Base: `dsm-package.yaml` (`backend.runtime: rust`, `methods`), the Cargo project in `backend` on the `syno-pkg-land-app` crate, `Cargo.lock`, `rust-toolchain.toml` |
17
+ | `native-{python,node,rust}-{jobs,root-ops,delegate}` | The capability's example module, replacing the base's stub, with its tests |
18
+ | `native-{python,node,rust}-vue` | The README and `.gitignore` of the Vue shell |
19
19
  | `binary` | A prebuilt static CGI (`example/main.go`) with a plain UI |
20
20
  | `compose` | Container Manager project (`compose.yaml`, `synology-app.yaml`) |
21
21
  | `_shared` | Agent guides, template license, Go license texts, third-party notice writer |
@@ -23,25 +23,22 @@ the files an App owns; the SDK (`github.com/synopackageland/syno-pkg-land-sdk`,
23
23
  Overlays are copied over `native-go` in the order root-ops, vue, jobs, delegate.
24
24
  An overlay may replace `internal/app/*_none.go` (the CLI deletes the matching
25
25
  `_none.go` file); nothing outside `internal/app` and the Vue shell differs.
26
+ The Python, Node.js and Rust templates borrow the Go template's UI: `ui/` and
27
+ `i18n/` from `native-go`, `frontend/` from `native-go-vue` (and the jobs and
28
+ delegate overlays' frontend files), `root/whoami.sh` from `native-go-root-ops`.
29
+ Their overlays are copied in the order vue, root-ops, jobs, delegate; delegate
30
+ also makes `app.status` and `report.save` `authenticated`, and the Rust
31
+ manifest gets the overlays' methods. `init` copies the App SDK into
32
+ `backend/vendor` (Python `synopkgland_app`; Node `@synopackageland/app`, its
33
+ package.json without devDependencies and scripts, and `dist/`).
26
34
  The placeholders `__PACKAGE__` (also the Go module path), `__DISPLAY_NAME__` and
27
35
  `__BACKGROUND_ENABLED__` are substituted by `init`.
28
36
 
29
- Rust has no build tags, so the `native-rust-*` overlays overwrite same-named
30
- base files (`background_routes.rs`, `delegate_routes.rs`, `root_routes.rs`,
31
- `bin/background-worker.rs`) and nothing is removed. They carry only the
32
- backend. A Rust project borrows the native-go overlays' non-backend files: the
33
- Vue shell from `native-go-vue` (then `native-rust-vue`), `frontend/` of
34
- `native-go-jobs` and `native-go-delegate`, and `root/whoami.sh` of
35
- `native-go-root-ops`. A delegate project without root-ops also takes
36
- `native-rust-root-ops`'s `root_routes.rs`, the executor shell Go borrows as
37
- `executor.go`. `scripts/assemble.sh --language rust` does exactly this.
38
-
39
- `backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.9.9`; that line is
37
+ `backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.10.1`; that line is
40
38
  the single source of the SDK version and `frontend/package.json` pins
41
- `@synopackageland/sdk` to the same version. A Rust project takes it from the
42
- `tag` of the `syno-pkg-land-sdk` git dependency in `backend/Cargo.toml`, which
43
- `Cargo.lock` repeats; build-time values reach it through the contract's
44
- `SYNOPKGLAND_*` environment variables.
39
+ `@synopackageland/sdk` to the same version. Python and Node.js name it in
40
+ `backend.sdk`, Rust in the `syno-pkg-land-app` tag of `backend/Cargo.toml`
41
+ (and `Cargo.lock`); all of them pin 0.10.1.
45
42
 
46
43
  ## Notice writer
47
44
 
@@ -62,9 +59,10 @@ and `UI-THIRD_PARTY.md` in the output directory; other files there stay.
62
59
  ```sh
63
60
  export GOPRIVATE='github.com/synopackageland/*' # the SDK repository is private
64
61
  export SYNOPKGLAND_SDK_DIR=~/Public/synopackageland/sdk # optional sdk checkout: Go module at the root, ts/
62
+ export SYNOPKGLAND_DOCKER_SSH=my-docker-host # optional: the Docker engine for the Linux tests
65
63
  scripts/assemble.sh /tmp/app vue delegate # one combination, as init lays it out
66
- scripts/assemble.sh --language rust /tmp/app vue delegate # the same, for native-rust
67
- scripts/gate.sh # layout, every combination (Go and Rust), binary, npm pack
64
+ scripts/assemble.sh --language rust /tmp/app-rs plain jobs # python, node or rust
65
+ scripts/gate.sh # layout, every combination, binary, npm pack
68
66
  ```
69
67
 
70
68
  Without `SYNOPKGLAND_SDK_DIR` the combinations use the released SDK that
@@ -72,11 +70,13 @@ Without `SYNOPKGLAND_SDK_DIR` the combinations use the released SDK that
72
70
  `replace github.com/synopackageland/syno-pkg-land-sdk => $SYNOPKGLAND_SDK_DIR`
73
71
  and installs `$SYNOPKGLAND_SDK_DIR/ts` as an `npm pack` tarball (not a link, so Vue
74
72
  resolves once). With `SYNOPKGLAND_UI_TARBALL` set it installs that tarball as
75
- `@synopackageland/ui`, for a UI version not yet on npm. The Rust gate runs `cargo fmt --check`, `cargo clippy -D warnings`, `cargo test --locked`
76
- and a target clippy for the three DSM targets per combination; `rustup` installs the
77
- toolchain `backend/rust-toolchain.toml` pins. With `SYNOPKGLAND_SDK_DIR`, `scripts/rust-env.sh`
78
- (sourced by the gate) makes cargo fetch the SDK's pinned tag from that checkout instead of
79
- GitHub, leaving the Cargo files as shipped. CI is not available; `scripts/gate.sh` is the gate.
73
+ `@synopackageland/ui`, for a UI version not yet on npm.
74
+ The Python and Node.js combinations need `SYNOPKGLAND_SDK_DIR` (its `python/`,
75
+ and `node/` built with `npm ci && npm run build`) for their App SDK, and the
76
+ Rust ones fetch the SDK crate from it (`scripts/rust-env.sh`), so it must hold
77
+ the tag `Cargo.lock` pins. The gate runs each language's tests on Linux in
78
+ Docker (`scripts/bridge-linux-test.sh`: Python 3.8, Node.js 18, Rust).
79
+ CI is not available; `scripts/gate.sh` is the gate.
80
80
 
81
81
  SDK usage, protocol and API documentation: <https://github.com/synopackageland/syno-pkg-land-sdk>.
82
82
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@synopackageland/templates",
3
- "version": "0.9.9",
4
- "description": "Project templates for synopkgland init: native-go and native-rust (plain or Vue, with jobs, delegate and root-ops overlays), binary and compose.",
3
+ "version": "0.10.1",
4
+ "description": "Project templates for synopkgland init: native-go, native-python, native-node and native-rust (plain or Vue, with jobs, delegate and root-ops overlays), binary and compose.",
5
5
  "license": "MIT",
6
6
  "repository": {"type": "git", "url": "git+https://github.com/synopackageland/syno-pkg-land-templates.git"},
7
7
  "exports": {
@@ -213,20 +213,58 @@ sweep forward the cancel instead of admitting it.
213
213
  `Service.OperationStatus`, `Service.ExportExample` (the SDK's
214
214
  `delegate/example.go`).
215
215
 
216
+ ## Python, Node.js and Rust backends
217
+
218
+ The SDK runtime is the CGI: it checks the DSM session, the entry gate and
219
+ permissions.json, then runs the App once per call, as the package account, and
220
+ answers the browser itself for every refusal. The App only registers methods
221
+ and asks the runtime for services with `ctx.runtime(op, params)`: `files.read`
222
+ (the caller's file, `files: session`), `bg.submit`/`bg.get`/`bg.cancel` (the
223
+ caller's jobs, `jobs: true`, whose worker hashes each job's input), `root.run`
224
+ (`rootOps`), `delegate.move_from_staging` (`delegate`) and `portal.status`. A
225
+ service refusal is a code the browser may see; pass it on. The admin guard
226
+ inside a shared method is `ctx.admin`. State lives in
227
+ `SYNOPKGLAND_DATA_DIR`. `synopkgland add` does not change these backends: choose
228
+ their capabilities with `synopkgland init --with`.
229
+
230
+ ## Python backend
231
+
232
+ - Methods: `backend/app/routes.py` (`build` registers them, the capability
233
+ modules `jobs.py`, `root_ops.py`, `delegate.py` add theirs); permissions:
234
+ `backend/app/permissions.json`. DSM runs it with its own python3 (3.8):
235
+ standard library only, nothing newer than 3.8.
236
+ - Params: `fields(params, name=type)` (`params.py`); errors:
237
+ `Error(code, message)`, `Error.bad_request(message)`; anything else is
238
+ `internal`. `ctx.runtime` raises the runtime's refusal as an `Error`.
239
+ - Test with `(cd backend && python3 -m unittest discover -s tests)`;
240
+ `tests/support.py`'s `call` plays the runtime, with the services a test answers.
241
+
242
+ ## Node.js backend
243
+
244
+ - Methods: `backend/src/routes.ts` (`build` registers them, the capability
245
+ modules `jobs.ts`, `root-ops.ts`, `delegate.ts` add theirs); permissions:
246
+ `backend/app/permissions.json`. TypeScript with `"module": "NodeNext"`;
247
+ `npm run build` compiles it into `backend/app`, which the SPK ships, so run it
248
+ before `synopkgland check` or `build`.
249
+ - Params: `strings(params, ...names)` (`params.ts`); errors:
250
+ `new AppError(code, message)`, `AppError.badRequest(message)`. Call services
251
+ through `service(ctx, op, params)` (`service.ts`), which passes the runtime's
252
+ refusals on; a bare `ctx.runtime` refusal would reach the browser as `internal`.
253
+ - Test with `(cd backend && npm ci && npm test && npm run typecheck)`; `test/support.ts`'s
254
+ `call` plays the runtime.
255
+
216
256
  ## Rust backend
217
257
 
218
- - Routes: `backend/src/app/routes.rs`; App permissions:
219
- `backend/permissions.json`. Test with `(cd backend && cargo test --locked)`,
220
- then `cargo clippy --locked --all-targets -- -D warnings` and
221
- `cargo fmt --check`.
222
- - Admin guard inside a shared route: `api::require_admin(caller)`.
223
- - Jobs: `background::Work` is `Arc<dyn Fn(&Context, &mut Task<'_>) ->
224
- Result<JobResult, BoxError>>`; call `t.report(Progress {..})` and
225
- `t.checkpoint(state)`, and resume from `t.job.checkpoint`. The template's
226
- Work is `app::hash_work` (`app/background/work.rs`, hash state through
227
- `sha256::Sha256::save`/`restore`), set as `Runner.work` in
228
- `bin/background-worker.rs` and as `delegate::Service.work` with delegate.
229
- - Uninstall hook: `app::uninstall(data_dir, delete_state)`
230
- (`app/uninstall.rs`), reached by `api.cgi uninstall`.
231
- - Delegate: `Service::destination`, `move_from_staging`, `cancel_operation`,
232
- `operation_status`, `export_example` (the SDK's `delegate/example.rs`).
258
+ - Methods: `backend/src/routes.rs` (`build` registers them, the capability
259
+ modules `jobs.rs`, `root_ops.rs`, `delegate.rs` add theirs); permissions:
260
+ `backend/permissions.json`. The build cross-compiles the binary, which cannot
261
+ run on the build machine: list every method in `backend.methods` of
262
+ dsm-package.yaml too.
263
+ - Params: a `#[serde(deny_unknown_fields)]` struct and `params.decode()?`;
264
+ errors: `Error::new(code, message)`, `Error::bad_request(message)`,
265
+ `routes::internal(e)` for the unexpected. `ctx.runtime::<T>(op, &params)?`
266
+ passes the runtime's refusal on.
267
+ - Test with `(cd backend && cargo test --locked)`, then
268
+ `cargo clippy --locked --all-targets -- -D warnings` and `cargo fmt --check`;
269
+ `test_support::call` plays the runtime. `rust-toolchain.toml` pins the
270
+ toolchain and the DSM targets; keep `Cargo.lock` committed.
@@ -1,6 +1,6 @@
1
1
  Native Go package build dependencies
2
2
 
3
- github.com/synopackageland/syno-pkg-land-sdk v0.9.9 (MIT)
3
+ github.com/synopackageland/syno-pkg-land-sdk v0.10.1 (MIT)
4
4
 
5
5
  Copyright (c) 2026 synopkgland
6
6
 
@@ -11,6 +11,8 @@ import (
11
11
  "net/http"
12
12
  "net/http/cgi"
13
13
  "os"
14
+ "strconv"
15
+ "time"
14
16
 
15
17
  "github.com/synopackageland/syno-pkg-land-sdk/api"
16
18
  "github.com/synopackageland/syno-pkg-land-sdk/dsm"
@@ -40,8 +42,20 @@ var (
40
42
  // "on" when dsm-package.yaml declares portal: serves portal.status;
41
43
  // "executor" below DSM 7.2, where the executor registers the entry.
42
44
  portal = ""
45
+ // backend.timeout_seconds, 60 unless set.
46
+ requestTimeout = ""
43
47
  )
44
48
 
49
+ // deadline is the CGI request deadline: requestTimeout seconds, or 60 when it
50
+ // is unset or not a positive number.
51
+ func deadline() api.DeadlineOption {
52
+ seconds, err := strconv.Atoi(requestTimeout)
53
+ if err != nil || seconds <= 0 {
54
+ seconds = 60
55
+ }
56
+ return api.Timeout(time.Duration(seconds) * time.Second)
57
+ }
58
+
45
59
  func main() {
46
60
  log.SetFlags(0)
47
61
  log.SetPrefix(packageID + ": ")
@@ -104,7 +118,7 @@ func main() {
104
118
  if standalone != nil {
105
119
  handler = standalone.Wrap(handler)
106
120
  }
107
- if err := cgi.Serve(api.WithRequestDeadline(handler)); err != nil {
121
+ if err := cgi.Serve(api.WithRequestDeadline(handler, deadline())); err != nil {
108
122
  log.Fatal(err)
109
123
  }
110
124
  }
@@ -0,0 +1,33 @@
1
+ package main
2
+
3
+ import (
4
+ "net/http"
5
+ "net/http/httptest"
6
+ "testing"
7
+ "time"
8
+
9
+ "github.com/synopackageland/syno-pkg-land-sdk/api"
10
+ )
11
+
12
+ func TestRequestDeadlineFollowsTheBuildSetting(t *testing.T) {
13
+ saved := requestTimeout
14
+ t.Cleanup(func() { requestTimeout = saved })
15
+ for setting, want := range map[string]time.Duration{
16
+ "": 60 * time.Second,
17
+ "120": 120 * time.Second,
18
+ "1": time.Second,
19
+ "0": 60 * time.Second,
20
+ "-5": 60 * time.Second,
21
+ "x": 60 * time.Second,
22
+ } {
23
+ requestTimeout = setting
24
+ var left time.Duration
25
+ api.WithRequestDeadline(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
26
+ d, _ := r.Context().Deadline()
27
+ left = time.Until(d)
28
+ }), deadline()).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("POST", "/", nil))
29
+ if left > want || left < want-time.Second {
30
+ t.Errorf("requestTimeout %q: deadline in %v, want %v", setting, left, want)
31
+ }
32
+ }
33
+ }
@@ -4,6 +4,6 @@ go 1.25.0
4
4
 
5
5
  toolchain go1.26.9
6
6
 
7
- require github.com/synopackageland/syno-pkg-land-sdk v0.9.9
7
+ require github.com/synopackageland/syno-pkg-land-sdk v0.10.1
8
8
 
9
9
  require golang.org/x/sys v0.44.0 // indirect
@@ -1,4 +1,4 @@
1
- github.com/synopackageland/syno-pkg-land-sdk v0.9.9 h1:TUsblZtIwlBO0DpBTUOj6gd8+xTi60Y0CwkKNb+OR3M=
2
- github.com/synopackageland/syno-pkg-land-sdk v0.9.9/go.mod h1:hlsvaVuH2wsAoR8jzI1Rg/NeejQSFpAaN6zpIWsVbjI=
1
+ github.com/synopackageland/syno-pkg-land-sdk v0.10.1 h1:XpuacZIACIiBljWRDGedVQ7SCRZnFqqeCBaGzENsnRo=
2
+ github.com/synopackageland/syno-pkg-land-sdk v0.10.1/go.mod h1:hlsvaVuH2wsAoR8jzI1Rg/NeejQSFpAaN6zpIWsVbjI=
3
3
  golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
4
4
  golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
@@ -18,6 +18,10 @@
18
18
  "error.request_canceled": "Dieser Vorgang kann nicht ausgeführt werden. Bitte versuchen Sie es später erneut.",
19
19
  "error.session_required": "Verbindung abgelaufen. Melden Sie sich erneut an.",
20
20
  "error.unknown_method": "Dieser Vorgang kann nicht ausgeführt werden. Bitte versuchen Sie es später erneut.",
21
+ "error.app_unavailable": "Die App ist derzeit nicht verfügbar. Versuchen Sie es später erneut.",
22
+ "error.limit_exceeded": "Die Datei ist größer als zulässig.",
23
+ "error.read_failed": "Die Datei kann nicht gelesen werden. Bitte versuchen Sie es später erneut.",
24
+ "error.identity_unavailable": "Dieses Benutzerkonto kann nicht ermittelt werden. Bitte versuchen Sie es später erneut.",
21
25
  "greeting.adminOnly": "Nur Mitglieder der Gruppe administrators können den Gruß ändern.",
22
26
  "greeting.label": "Neuer Gruß",
23
27
  "greeting.save": "Speichern",
@@ -18,6 +18,10 @@
18
18
  "error.request_canceled": "Unable to perform this operation. Please try again later.",
19
19
  "error.session_required": "Connection expired. Please sign in again.",
20
20
  "error.unknown_method": "Unable to perform this operation. Please try again later.",
21
+ "error.app_unavailable": "The App is not available right now. Try again later.",
22
+ "error.limit_exceeded": "The file is larger than allowed.",
23
+ "error.read_failed": "Unable to read the file. Please try again later.",
24
+ "error.identity_unavailable": "Unable to identify this user account. Please try again later.",
21
25
  "greeting.adminOnly": "Only members of the administrators group can change the greeting.",
22
26
  "greeting.label": "New greeting",
23
27
  "greeting.save": "Save",
@@ -18,6 +18,10 @@
18
18
  "error.request_canceled": "No se puede realizar esta operación. Vuelva a intentarlo más tarde.",
19
19
  "error.session_required": "Conexión caducada. Vuelva a conectarse.",
20
20
  "error.unknown_method": "No se puede realizar esta operación. Vuelva a intentarlo más tarde.",
21
+ "error.app_unavailable": "La App no está disponible en este momento. Vuelva a intentarlo más tarde.",
22
+ "error.limit_exceeded": "El archivo supera el tamaño permitido.",
23
+ "error.read_failed": "No se puede leer el archivo. Vuelva a intentarlo más tarde.",
24
+ "error.identity_unavailable": "No se puede identificar esta cuenta de usuario. Vuelva a intentarlo más tarde.",
21
25
  "greeting.adminOnly": "Solo los miembros del grupo administrators pueden cambiar el saludo.",
22
26
  "greeting.label": "Nuevo saludo",
23
27
  "greeting.save": "Guardar",
@@ -18,6 +18,10 @@
18
18
  "error.request_canceled": "Impossible d'effectuer cette opération. Veuillez réessayer ultérieurement.",
19
19
  "error.session_required": "La connexion a expiré. Reconnectez-vous.",
20
20
  "error.unknown_method": "Impossible d'effectuer cette opération. Veuillez réessayer ultérieurement.",
21
+ "error.app_unavailable": "L’App n’est pas disponible pour le moment. Réessayez plus tard.",
22
+ "error.limit_exceeded": "Le fichier dépasse la taille autorisée.",
23
+ "error.read_failed": "Impossible de lire le fichier. Veuillez réessayer ultérieurement.",
24
+ "error.identity_unavailable": "Impossible d'identifier ce compte utilisateur. Veuillez réessayer ultérieurement.",
21
25
  "greeting.adminOnly": "Seuls les membres du groupe administrators peuvent modifier le message d'accueil.",
22
26
  "greeting.label": "Nouveau message d'accueil",
23
27
  "greeting.save": "Sauvegarder",
@@ -18,6 +18,10 @@
18
18
  "error.request_canceled": "Impossibile eseguire questa operazione. Riprovare più tardi.",
19
19
  "error.session_required": "Connessione scaduta. Accedere nuovamente.",
20
20
  "error.unknown_method": "Impossibile eseguire questa operazione. Riprovare più tardi.",
21
+ "error.app_unavailable": "L’App non è al momento disponibile. Riprovare più tardi.",
22
+ "error.limit_exceeded": "Il file supera le dimensioni consentite.",
23
+ "error.read_failed": "Impossibile leggere il file. Riprovare più tardi.",
24
+ "error.identity_unavailable": "Impossibile identificare questo account utente. Riprovare più tardi.",
21
25
  "greeting.adminOnly": "Solo i membri del gruppo administrators possono modificare il saluto.",
22
26
  "greeting.label": "Nuovo saluto",
23
27
  "greeting.save": "Salva",
@@ -18,6 +18,10 @@
18
18
  "error.request_canceled": "この操作は実行できません。後でもう一度お試しください。",
19
19
  "error.session_required": "接続の期限が切れています。もう 1 度サイン インしてください。",
20
20
  "error.unknown_method": "この操作は実行できません。後でもう一度お試しください。",
21
+ "error.app_unavailable": "App は現在使用できません。しばらくしてから再試行してください。",
22
+ "error.limit_exceeded": "ファイルが許可されたサイズを超えています。",
23
+ "error.read_failed": "ファイルを読み取れません。後でもう一度お試しください。",
24
+ "error.identity_unavailable": "このユーザー アカウントを識別できません。後でもう一度お試しください。",
21
25
  "greeting.adminOnly": "あいさつを変更できるのは administrators グループのメンバーのみです。",
22
26
  "greeting.label": "新しいあいさつ",
23
27
  "greeting.save": "保存",
@@ -18,6 +18,10 @@
18
18
  "error.request_canceled": "이 작업을 수행할 수 없습니다. 나중에 다시 시도하십시오.",
19
19
  "error.session_required": "연결 만료됨. 다시 로그인 해 주십시오.",
20
20
  "error.unknown_method": "이 작업을 수행할 수 없습니다. 나중에 다시 시도하십시오.",
21
+ "error.app_unavailable": "현재 App을 사용할 수 없습니다. 나중에 다시 시도하십시오.",
22
+ "error.limit_exceeded": "파일이 허용된 크기를 초과합니다.",
23
+ "error.read_failed": "파일을 읽을 수 없습니다. 나중에 다시 시도하십시오.",
24
+ "error.identity_unavailable": "이 사용자 계정을 확인할 수 없습니다. 나중에 다시 시도하십시오.",
21
25
  "greeting.adminOnly": "administrators 그룹의 구성원만 인사말을 변경할 수 있습니다.",
22
26
  "greeting.label": "새 인사말",
23
27
  "greeting.save": "저장",
@@ -18,6 +18,10 @@
18
18
  "error.request_canceled": "无法执行此操作。请稍后再试。",
19
19
  "error.session_required": "联机超时,请重新登录。",
20
20
  "error.unknown_method": "无法执行此操作。请稍后再试。",
21
+ "error.app_unavailable": "App 当前不可用,请稍后重试。",
22
+ "error.limit_exceeded": "文件超过允许的大小。",
23
+ "error.read_failed": "无法读取文件。请稍后再试。",
24
+ "error.identity_unavailable": "无法识别此用户帐号。请稍后再试。",
21
25
  "greeting.adminOnly": "只有 administrators 群组的成员可以更改问候语。",
22
26
  "greeting.label": "新的问候语",
23
27
  "greeting.save": "保存",
@@ -18,6 +18,10 @@
18
18
  "error.request_canceled": "無法執行此操作。請稍後再試。",
19
19
  "error.session_required": "連線逾時。請重新登入。",
20
20
  "error.unknown_method": "無法執行此操作。請稍後再試。",
21
+ "error.app_unavailable": "App 目前無法使用,請稍後再試。",
22
+ "error.limit_exceeded": "檔案超過允許的大小。",
23
+ "error.read_failed": "無法讀取檔案。請稍後再試。",
24
+ "error.identity_unavailable": "無法識別此使用者帳號。請稍後再試。",
21
25
  "greeting.adminOnly": "只有 administrators 群組的成員可以變更問候語。",
22
26
  "greeting.label": "新的問候語",
23
27
  "greeting.save": "儲存",
@@ -71,7 +71,8 @@
71
71
  [[t("status.user"), data.admin ? t("status.userAdmin", { user: data.user }) : data.user],
72
72
  [t("status.package"), data.package],
73
73
  [t("status.version"), data.version],
74
- [t("status.backend"), data.go_version + " · " + data.platform],
74
+ // A Go backend reports go_version; a Python, Node.js or Rust App reports language.
75
+ [t("status.backend"), (data.language || data.go_version) + " · " + data.platform],
75
76
  [t("status.uid"), String(data.process.uid)]].forEach(function (row) {
76
77
  status.appendChild(el("dt", { text: row[0] }));
77
78
  status.appendChild(el("dd", { text: row[1] }));
@@ -5,6 +5,8 @@ import {AppRoot} from '@synopackageland/ui'
5
5
  import type {BackgroundSaveFacade} from '@synopackageland/sdk'
6
6
  import CapabilityPanels from '../src/CapabilityPanels.vue'
7
7
  import {mount} from '../src/main'
8
+ import {holdsPages} from '../src/ExecutorGate.vue'
9
+ import {dsmWindow} from './context'
8
10
  const cleanup:Array<()=>void>=[]
9
11
  afterEach(()=>cleanup.splice(0).forEach(fn=>fn()))
10
12
  async function settle(){for(let i=0;i<30;i++){await Promise.resolve();await nextTick()}}
@@ -66,7 +68,7 @@ test('the app mount forwards ctx.executor to administrator setup only',async()=>
66
68
  throw {code:'unknown_method'}
67
69
  })
68
70
  const root=document.createElement('div');document.body.append(root)
69
- const app=mount(root,{locale:'en',call,reopen:()=>()=>{},executor:{status,authorize,control:vi.fn(async()=>({state:'ready' as const}))}} as any)
71
+ const app=mount(root,dsmWindow({call,executor:{status,authorize,control:vi.fn(async()=>({state:'ready' as const}))} as never}))
70
72
  cleanup.push(()=>{app.unmount();root.remove()});await settle()
71
73
  if(admin){
72
74
  expect(status).toHaveBeenCalled()
@@ -75,8 +77,9 @@ test('the app mount forwards ctx.executor to administrator setup only',async()=>
75
77
  password.closest('form')!.dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));await settle()
76
78
  expect(authorize).toHaveBeenCalledWith('one-time')
77
79
  }else{
78
- expect(status).not.toHaveBeenCalled()
80
+ // With root-ops the App's pages also wait for the executor, which reads its status.
79
81
  expect(root.querySelector('input[type=password]')).toBeNull()
82
+ expect(authorize).not.toHaveBeenCalled()
80
83
  }
81
84
  }
82
85
  })
@@ -90,7 +93,7 @@ test('the header Refresh reloads the save destinations',async()=>{
90
93
  throw {code:'unknown_method'}
91
94
  })
92
95
  const root=document.createElement('div');document.body.append(root)
93
- const app=mount(root,{locale:'en',call,reopen:()=>()=>{}} as any)
96
+ const app=mount(root,dsmWindow({call}))
94
97
  cleanup.push(()=>{app.unmount();root.remove()});await settle()
95
98
  const reads=()=>call.mock.calls.filter(([method])=>method==='delegate.destination').length
96
99
  const before=reads();expect(before).toBeGreaterThan(0)
@@ -102,3 +105,21 @@ test('the panels show a skeleton while the delegate status loads',async()=>{
102
105
  const root=render({call,locale:'en'});await settle()
103
106
  expect(root.querySelector('[data-ui-skeleton-line]')).not.toBeNull()
104
107
  })
108
+ // AppShell's ExecutorNotice: a delegate App's pages stay while the executor waits for approval.
109
+ // With root-ops, ExecutorGate holds the pages instead (its own test).
110
+ test.skipIf(holdsPages)('an executor awaiting approval adds a notice above the pages and holds none of them back',async()=>{
111
+ const status=vi.fn(async()=>({state:'needs-admin-approval' as const}))
112
+ const call=vi.fn(async(method:string)=>{
113
+ if(method==='app.status')return {package:'DelegateApp',version:'1',user:'admin',admin:true}
114
+ if(method==='greeting.get')return {text:'Hello'}
115
+ if(method==='delegate.status')return {state:'needs-admin-approval'}
116
+ if(method==='delegate.list')return {delegations:[]}
117
+ throw {code:'unknown_method'}
118
+ })
119
+ const root=document.createElement('div');document.body.append(root)
120
+ const app=mount(root,dsmWindow({call,user:{name:'admin',isAdmin:true},executor:{status,authorize:vi.fn(),control:vi.fn()} as never}))
121
+ cleanup.push(()=>{app.unmount();root.remove()});await settle()
122
+ expect(root.textContent).toContain('Some features are unavailable until you approve system access for this app.')
123
+ expect(root.textContent).toContain('Package status')
124
+ expect((root.querySelector('textarea') as HTMLTextAreaElement).value).toBe('Hello')
125
+ })
@@ -2,7 +2,7 @@
2
2
  import {expect,test,vi} from 'vitest'
3
3
  import {nextTick} from 'vue'
4
4
  import {mount} from '../src/main'
5
- import type {Context} from '@synopackageland/sdk'
5
+ import {dsmWindow} from './context'
6
6
  async function settle(){for(let i=0;i<20;i++){await Promise.resolve();await nextTick()}}
7
7
  // The CLI turns the background page on for jobs (and delegate) projects only.
8
8
  test('a jobs project mounts the background panel for the signed-in user',async()=>{
@@ -11,7 +11,7 @@ test('a jobs project mounts the background panel for the signed-in user',async()
11
11
  throw {code:'unknown_method'}
12
12
  })
13
13
  const root=document.createElement('div');document.body.append(root)
14
- const app=mount(root,{locale:'en',call,reopen:()=>()=>{}} as unknown as Context)
14
+ const app=mount(root,dsmWindow({call}))
15
15
  try{
16
16
  await settle()
17
17
  expect(call.mock.calls.some(([method])=>method.startsWith('bg.'))).toBe(true)
@@ -29,7 +29,7 @@ test('the background panel pauses a running job and resumes a paused one',async(
29
29
  return {}
30
30
  })
31
31
  const root=document.createElement('div');document.body.append(root)
32
- const app=mount(root,{locale:'en',call,reopen:()=>()=>{}} as unknown as Context)
32
+ const app=mount(root,dsmWindow({call}))
33
33
  const button=(id:string,label:string)=>Array.from(root.querySelectorAll(`[data-job-id="${id}"] button`)).find(b=>b.textContent===label) as HTMLButtonElement|undefined
34
34
  try{
35
35
  await settle()
@@ -48,7 +48,7 @@ test('the header Refresh reloads the background panel',async()=>{
48
48
  return {}
49
49
  })
50
50
  const root=document.createElement('div');document.body.append(root)
51
- const app=mount(root,{locale:'en',call,reopen:()=>()=>{}} as unknown as Context)
51
+ const app=mount(root,dsmWindow({call}))
52
52
  const reads=()=>call.mock.calls.filter(([method])=>method==='bg.jobs.list').length
53
53
  try{
54
54
  await settle();const before=reads()
@@ -5,79 +5,16 @@ import (
5
5
  "encoding/json"
6
6
  "errors"
7
7
  "net/http"
8
- "os"
9
8
  "path/filepath"
10
- "strings"
11
9
  "time"
12
10
 
13
11
  "github.com/synopackageland/syno-pkg-land-sdk/api"
14
12
  "github.com/synopackageland/syno-pkg-land-sdk/delegate"
15
13
  )
16
14
 
15
+ // executor.status and executor.control are the SDK's, as the bridge runtime serves them.
17
16
  func (cfg Config) rootRoutes() []api.Route {
18
- return []api.Route{
19
- api.Handle("executor.control", cfg.executorControl),
20
- api.Handle("executor.status", cfg.executorStatus),
21
- api.HandleRequest("root.run", cfg.executorRootRun),
22
- }
23
- }
24
-
25
- // API permissions default to administrators for unlisted methods. Check here
26
- // too: control mutates the fixed package-owned control file before DSM run.
27
- func (cfg Config) executorControl(_ context.Context, caller api.Caller, raw json.RawMessage) (any, error) {
28
- if !caller.Admin() {
29
- return nil, errors.New("admin_required")
30
- }
31
- var in struct {
32
- Action string `json:"action"`
33
- }
34
- if err := decodeParams(raw, &in); err != nil {
35
- return nil, err
36
- }
37
- if in.Action != "start" && in.Action != "stop" && in.Action != "purge" {
38
- return nil, api.BadRequest("executor_control_invalid")
39
- }
40
- dir := filepath.Join(cfg.DataDir, "executor")
41
- if err := os.MkdirAll(dir, 0700); err != nil {
42
- return nil, err
43
- }
44
- info, err := os.Lstat(dir)
45
- if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
46
- return nil, errors.New("executor_control_directory_invalid")
47
- }
48
- if err := writeFileAtomic(filepath.Join(dir, "control"), []byte(in.Action)); err != nil {
49
- return nil, err
50
- }
51
- return map[string]bool{"written": true}, nil
52
- }
53
-
54
- // The executor closes its socket while paused: the package is stopped, or an
55
- // upgrade changed the pins and awaits administrator approval.
56
- func (cfg Config) executorStatus(ctx context.Context, _ api.Caller, _ json.RawMessage) (any, error) {
57
- var status json.RawMessage
58
- err := cfg.executor().Call(ctx, "status", nil, nil, &status)
59
- var refusal *api.Error
60
- if errors.As(err, &refusal) && refusal.Code == delegate.CodeExecutorUnavailable {
61
- next := "Start the package."
62
- if cfg.startRequested() {
63
- next = "The package is running: an administrator approves background access again in Administrator setup; the executor log names the cause."
64
- }
65
- return map[string]any{"ready": false, "state": "paused", "reason": "executor_not_listening", "next": next}, nil
66
- }
67
- return status, err
68
- }
69
-
70
- // Package start and stop write the lifecycle mailbox; its last request tells a
71
- // stopped package from a running one whose executor stays paused.
72
- func (cfg Config) startRequested() bool {
73
- f, err := os.Open(filepath.Join(cfg.DataDir, "executor", "control"))
74
- if err != nil {
75
- return false
76
- }
77
- defer f.Close()
78
- b := make([]byte, 16)
79
- n, _ := f.Read(b)
80
- return strings.TrimSpace(string(b[:n])) == "start"
17
+ return append(delegate.ExecutorRoutes(cfg.executor(), cfg.DataDir), api.HandleRequest("root.run", cfg.executorRootRun))
81
18
  }
82
19
 
83
20
  func (cfg Config) executorRootRun(ctx context.Context, caller api.Caller, req *http.Request, raw json.RawMessage) (any, error) {