@synopackageland/templates 0.38.0 → 0.39.0
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/README.md +9 -9
- package/package.json +1 -1
- package/templates/_shared/AGENTS.md +6 -5
- package/templates/_shared/CLAUDE.md +2 -0
- package/templates/compose/dsm-package.yaml +1 -1
- package/templates/native-go/README.md +28 -11
- package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
- package/templates/native-go/backend/cmd/app/main.go +9 -2
- package/templates/native-go/backend/go.mod +1 -1
- package/templates/native-go/backend/go.sum +2 -2
- package/templates/native-go/backend/internal/api/operations.json +5 -0
- package/templates/native-go/backend/internal/api/permissions.go +7 -0
- package/templates/native-go/backend/internal/api/permissions.json +3 -1
- package/templates/native-go/backend/internal/app/routes.go +2 -46
- package/templates/native-go/backend/internal/app/routes_test.go +4 -25
- package/templates/native-go/dsm-package.yaml +2 -28
- package/templates/native-go/examples/operations.json +112 -0
- package/templates/native-go/operations.json +27 -0
- package/templates/native-go/ui/app.js +0 -25
- package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +3 -4
- package/templates/native-go-root-ops/frontend/test/executor-gate.test.ts +7 -6
- package/templates/native-go-vue/frontend/package-lock.json +4 -4
- package/templates/native-go-vue/frontend/package.json +1 -1
- package/templates/native-go-vue/frontend/src/App.vue +5 -34
- package/templates/native-go-vue/frontend/test/apps.test.ts +19 -25
- package/templates/native-go-vue/frontend/test/native-controls.test.ts +4 -1
- package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +5 -3
- package/templates/native-node/README.md +27 -7
- package/templates/native-node/backend/app/permissions.json +3 -1
- package/templates/native-node/backend/package-lock.json +1 -1
- package/templates/native-node/backend/src/routes.ts +5 -43
- package/templates/native-node/backend/test/routes.test.ts +3 -23
- package/templates/native-node/dsm-package.yaml +3 -29
- package/templates/native-node/examples/operations.json +112 -0
- package/templates/native-node/operations.json +27 -0
- package/templates/native-node-delegate/operations.patch.json +20 -0
- package/templates/native-node-jobs/operations.patch.json +52 -0
- package/templates/native-node-root-ops/operations.patch.json +20 -0
- package/templates/native-python/README.md +27 -7
- package/templates/native-python/backend/app/permissions.json +3 -1
- package/templates/native-python/backend/app/routes.py +2 -41
- package/templates/native-python/backend/tests/test_routes.py +3 -19
- package/templates/native-python/dsm-package.yaml +3 -29
- package/templates/native-python/examples/operations.json +112 -0
- package/templates/native-python/operations.json +27 -0
- package/templates/native-python-delegate/operations.patch.json +20 -0
- package/templates/native-python-jobs/operations.patch.json +52 -0
- package/templates/native-python-root-ops/operations.patch.json +20 -0
- package/templates/native-rust/README.md +28 -8
- package/templates/native-rust/backend/Cargo.lock +2 -2
- package/templates/native-rust/backend/Cargo.toml +1 -1
- package/templates/native-rust/backend/permissions.json +3 -1
- package/templates/native-rust/backend/src/routes.rs +11 -149
- package/templates/native-rust/dsm-package.yaml +3 -29
- package/templates/native-rust/examples/operations.json +112 -0
- package/templates/native-rust/operations.json +27 -0
- package/templates/native-rust-delegate/backend/Cargo.lock +2 -2
- package/templates/native-rust-delegate/backend/Cargo.toml +1 -1
- package/templates/native-rust-delegate/backend/src/delegate.rs +7 -1
- package/templates/native-rust-delegate/operations.patch.json +20 -0
- package/templates/native-rust-jobs/operations.patch.json +52 -0
- package/templates/native-rust-root-ops/operations.patch.json +20 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"operations": {
|
|
3
|
+
"hash.submit": {
|
|
4
|
+
"summary": "Start a file hash job",
|
|
5
|
+
"ai": "hidden",
|
|
6
|
+
"effect": "write",
|
|
7
|
+
"permission": "administrators",
|
|
8
|
+
"cli": [
|
|
9
|
+
"hash",
|
|
10
|
+
"submit"
|
|
11
|
+
],
|
|
12
|
+
"paramsSchema": {},
|
|
13
|
+
"resultSchema": {},
|
|
14
|
+
"flow": {
|
|
15
|
+
"type": "invoke",
|
|
16
|
+
"method": "hash.submit"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"hash.get": {
|
|
20
|
+
"summary": "Read a file hash job",
|
|
21
|
+
"ai": "hidden",
|
|
22
|
+
"effect": "read",
|
|
23
|
+
"permission": "administrators",
|
|
24
|
+
"cli": [
|
|
25
|
+
"hash",
|
|
26
|
+
"get"
|
|
27
|
+
],
|
|
28
|
+
"paramsSchema": {},
|
|
29
|
+
"resultSchema": {},
|
|
30
|
+
"flow": {
|
|
31
|
+
"type": "invoke",
|
|
32
|
+
"method": "hash.get"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"hash.cancel": {
|
|
36
|
+
"summary": "Cancel a file hash job",
|
|
37
|
+
"ai": "hidden",
|
|
38
|
+
"effect": "write",
|
|
39
|
+
"permission": "administrators",
|
|
40
|
+
"cli": [
|
|
41
|
+
"hash",
|
|
42
|
+
"cancel"
|
|
43
|
+
],
|
|
44
|
+
"paramsSchema": {},
|
|
45
|
+
"resultSchema": {},
|
|
46
|
+
"flow": {
|
|
47
|
+
"type": "invoke",
|
|
48
|
+
"method": "hash.cancel"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"operations": {
|
|
3
|
+
"whoami.run": {
|
|
4
|
+
"summary": "Run the approved identity operation",
|
|
5
|
+
"ai": "hidden",
|
|
6
|
+
"effect": "write",
|
|
7
|
+
"permission": "administrators",
|
|
8
|
+
"cli": [
|
|
9
|
+
"whoami",
|
|
10
|
+
"run"
|
|
11
|
+
],
|
|
12
|
+
"paramsSchema": {},
|
|
13
|
+
"resultSchema": {},
|
|
14
|
+
"flow": {
|
|
15
|
+
"type": "invoke",
|
|
16
|
+
"method": "whoami.run"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -6,7 +6,7 @@ runs as its own DSM package account, needs no containers, and has no root by def
|
|
|
6
6
|
|
|
7
7
|
```text
|
|
8
8
|
DSM main menu → DSM window (ui/app.js)
|
|
9
|
-
│ ctx.call("
|
|
9
|
+
│ ctx.call("app.status") same-origin POST + DSM session
|
|
10
10
|
▼
|
|
11
11
|
/webman/3rdparty/__PACKAGE__/cgi/api.cgi (the SDK runtime, runs as package user)
|
|
12
12
|
1. transport guards: POST, JSON, same origin, ≤ 1 MiB, bounded JSON complexity
|
|
@@ -17,7 +17,7 @@ DSM main menu → DSM window (ui/app.js)
|
|
|
17
17
|
|
|
18
18
|
## Requests and long work
|
|
19
19
|
|
|
20
|
-
SDK/App 0.
|
|
20
|
+
SDK/App 0.47.0 defaults to 300-second requests and a 302-second browser wait.
|
|
21
21
|
Explicit `backend.timeout_seconds` and earlier parent deadlines still apply;
|
|
22
22
|
rebuild the App to use the new SDK. For long NAS work, enable the existing jobs
|
|
23
23
|
capability: return admission and the original job ID promptly, execute in the
|
|
@@ -27,7 +27,7 @@ last queued/running state intact. After a lost admission reply, reuse the same
|
|
|
27
27
|
idempotency key and input instead of replaying a mutation with a new key.
|
|
28
28
|
Jobs default to 30 minutes; approved executor limits remain separate. ctl keeps
|
|
29
29
|
its 62-second transport default; use an explicit `--timeout` or an existing
|
|
30
|
-
nonblocking job. See the SDK's [request deadline contract](https://github.com/synopackageland/syno-pkg-land-sdk/blob/v0.
|
|
30
|
+
nonblocking job. See the SDK's [request deadline contract](https://github.com/synopackageland/syno-pkg-land-sdk/blob/v0.47.0/docs/request-deadlines.md).
|
|
31
31
|
|
|
32
32
|
## Quick start
|
|
33
33
|
|
|
@@ -35,6 +35,8 @@ Needs Node.js with `synopkgland` installed, Rust through rustup, Zig and `cargo-
|
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
(cd backend && cargo test --locked) # method tests
|
|
38
|
+
synopkgland ctl generate
|
|
39
|
+
synopkgland check
|
|
38
40
|
synopkgland build # → dist/__PACKAGE__-0.1.0-0001-{x86_64,aarch64}.spk
|
|
39
41
|
synopkgland inspect dist/__PACKAGE__-0.1.0-0001-x86_64.spk
|
|
40
42
|
```
|
|
@@ -42,8 +44,7 @@ synopkgland inspect dist/__PACKAGE__-0.1.0-0001-x86_64.spk
|
|
|
42
44
|
Install: Package Center → Settings → General → Trust Level "Any publisher",
|
|
43
45
|
then Manual Install with the SPK for your NAS's CPU. Open
|
|
44
46
|
**__DISPLAY_NAME__** from the DSM main menu: it shows who you are signed in
|
|
45
|
-
as
|
|
46
|
-
greeting in the package's data folder.
|
|
47
|
+
as and what the backend reports.
|
|
47
48
|
|
|
48
49
|
## Project layout
|
|
49
50
|
|
|
@@ -55,7 +56,7 @@ greeting in the package's data folder.
|
|
|
55
56
|
| `i18n/glossary.json` | DSM's own UI terms, and which messages must use them | Add terms |
|
|
56
57
|
| `backend/src/routes.rs` | Your methods: `build` registers them | Yes — your backend |
|
|
57
58
|
| `backend/src/jobs.rs`, `root_ops.rs`, `delegate.rs` | Capability examples (stubs unless `init --with` added them) | Yes |
|
|
58
|
-
| `backend/permissions.json` |
|
|
59
|
+
| `backend/permissions.json` | Generated method policy | No |
|
|
59
60
|
| `backend/src/main.rs` | Runs the App on the runtime's channel | Rarely |
|
|
60
61
|
| `backend/src/test_support.rs` | `call` plays the runtime in `cargo test` | Rarely |
|
|
61
62
|
| `backend/Cargo.toml`, `backend/Cargo.lock` | Your crate; the `syno-pkg-land-app` dependency's `tag` pins the SDK, and the committed lock file pins everything else | SDK upgrades |
|
|
@@ -69,7 +70,8 @@ runtime CGI at `ui/cgi/api.cgi`.
|
|
|
69
70
|
## Add a method
|
|
70
71
|
|
|
71
72
|
Register it in `build`. Every method defaults to DSM administrator membership;
|
|
72
|
-
|
|
73
|
+
declare metadata and access rules in `operations.json`; `ctl generate` writes
|
|
74
|
+
`backend/permissions.json`:
|
|
73
75
|
|
|
74
76
|
```rust
|
|
75
77
|
pub fn build(cfg: Config) -> App {
|
|
@@ -94,7 +96,7 @@ fn list_notes(_cfg: &Config, ctx: &Context, params: &Params) -> Result<Value, Er
|
|
|
94
96
|
|
|
95
97
|
List the method in `backend.methods` in dsm-package.yaml, then add a test next to it with `call(build(cfg), "notes.list", json!({"folder": "work"}), "admin", &services)`. Call it from the UI with `ctx.call("notes.list", { folder: "work" })`.
|
|
96
98
|
|
|
97
|
-
Return `Error::new(code, message)` (or `Error::bad_request(message)`) for an error the browser may see;
|
|
99
|
+
Return `Error::new(code, message)` (or `Error::bad_request(message)`) for an error the browser may see; log unexpected failures server-side and return a generic internal error. `ctx.runtime` returns the runtime's refusal as an `Error`, so `?` passes its code on to the browser.
|
|
98
100
|
|
|
99
101
|
The runtime refuses before your App runs: `session_required`, `admin_required`,
|
|
100
102
|
`unknown_method`, `request_timeout` and the transport guards' codes. The UI
|
|
@@ -188,3 +190,21 @@ bounded private copy for the worker; no browser session or NAS picker is used.
|
|
|
188
190
|
Submit with `hash.submit`; use `hash.get` for progress and `hash.cancel` to
|
|
189
191
|
cancel. These handlers call the existing `bg.submit`, `bg.get` and `bg.cancel`
|
|
190
192
|
services; the runtime checks the input path and binds ownership to the caller.
|
|
193
|
+
|
|
194
|
+
## Operation registry
|
|
195
|
+
|
|
196
|
+
Declare each App method once in `operations.json`, including its English summary,
|
|
197
|
+
params/result schemas, effect, permission, flow and required `ai` class. The
|
|
198
|
+
SDK contributes its own hidden operation catalog for enabled capability routes.
|
|
199
|
+
Register the handler, then run `synopkgland ctl generate`, `synopkgland ctl
|
|
200
|
+
generate --check` and `synopkgland check`. Build refuses generated drift or a
|
|
201
|
+
route/spec mismatch. Never maintain commands or operation descriptions in the
|
|
202
|
+
manifest or skill by hand. Read the generated `generated/operations.md`.
|
|
203
|
+
|
|
204
|
+
`examples/operations.json` illustrates read, preview, draft, human-only and hidden.
|
|
205
|
+
It is documentation only: its operations have no handlers and are never shipped
|
|
206
|
+
as commands. Copy a declaration only when implementing a real App capability.
|
|
207
|
+
Draft writes require a non-mutating preview and explicit human confirmation.
|
|
208
|
+
Long operations use the existing job flow; tools own polling and pagination.
|
|
209
|
+
|
|
210
|
+
Use a matching SDK/CLI/templates release containing the registry support.
|
|
@@ -104,8 +104,8 @@ dependencies = [
|
|
|
104
104
|
|
|
105
105
|
[[package]]
|
|
106
106
|
name = "syno-pkg-land-app"
|
|
107
|
-
version = "0.
|
|
108
|
-
source = "git+https://github.com/synopackageland/syno-pkg-land-sdk?tag=v0.
|
|
107
|
+
version = "0.47.0"
|
|
108
|
+
source = "git+https://github.com/synopackageland/syno-pkg-land-sdk?tag=v0.47.0#c486d2ce888248e9e68a67908810c62f968d601e"
|
|
109
109
|
dependencies = [
|
|
110
110
|
"libc",
|
|
111
111
|
"serde",
|
|
@@ -9,7 +9,7 @@ libc = "0.2"
|
|
|
9
9
|
serde = { version = "1", features = ["derive"] }
|
|
10
10
|
serde_json = "1"
|
|
11
11
|
# The SDK version is the tag; `synopkgland check` reads it from Cargo.lock.
|
|
12
|
-
syno-pkg-land-app = { git = "https://github.com/synopackageland/syno-pkg-land-sdk", tag = "v0.
|
|
12
|
+
syno-pkg-land-app = { git = "https://github.com/synopackageland/syno-pkg-land-sdk", tag = "v0.47.0" }
|
|
13
13
|
|
|
14
14
|
[profile.release]
|
|
15
15
|
# A handler panic becomes an "internal" reply; abort would end the App without one.
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
//!
|
|
2
|
-
//!
|
|
3
|
-
//! unless only administrators may call it. The runtime has checked the DSM
|
|
4
|
-
//! session, the entry gate and that rule before a handler runs.
|
|
1
|
+
//! Register real handlers here; operations.json owns metadata and permissions.
|
|
2
|
+
//! Run ctl generate and ctl generate --check after adding a handler.
|
|
5
3
|
|
|
6
|
-
use serde::{Deserialize, Serialize};
|
|
7
4
|
use serde_json::{Value, json};
|
|
8
5
|
use std::fs;
|
|
9
|
-
use std::
|
|
10
|
-
use std::path::{Path, PathBuf};
|
|
6
|
+
use std::path::PathBuf;
|
|
11
7
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
12
|
-
use syno_pkg_land_app::{App, Context
|
|
8
|
+
use syno_pkg_land_app::{App, Context};
|
|
13
9
|
|
|
14
10
|
/// Where the App's state lives; the runtime sets the environment.
|
|
15
11
|
#[derive(Clone)]
|
|
16
12
|
pub struct Config {
|
|
17
13
|
pub package: String,
|
|
18
14
|
/// The package's own data directory (/var/packages/<package>/var), kept across upgrades.
|
|
15
|
+
// Capability overlays use this persistent state directory.
|
|
16
|
+
#[allow(dead_code)]
|
|
19
17
|
pub data_dir: PathBuf,
|
|
20
18
|
pub packages_root: PathBuf,
|
|
21
19
|
}
|
|
@@ -35,12 +33,6 @@ pub fn build(cfg: Config) -> App {
|
|
|
35
33
|
let mut app = App::new();
|
|
36
34
|
let c = cfg.clone();
|
|
37
35
|
app.method("app.status", move |ctx, _| Ok(status(&c, ctx)));
|
|
38
|
-
let c = cfg.clone();
|
|
39
|
-
app.method("greeting.get", move |_, _| get_greeting(&c));
|
|
40
|
-
let c = cfg.clone();
|
|
41
|
-
app.method("greeting.set", move |ctx, params| {
|
|
42
|
-
set_greeting(&c, ctx, params)
|
|
43
|
-
});
|
|
44
36
|
// Each capability module is a stub until `synopkgland init --with` adds its example.
|
|
45
37
|
crate::jobs::register(&mut app, &cfg);
|
|
46
38
|
crate::root_ops::register(&mut app, &cfg);
|
|
@@ -77,72 +69,6 @@ fn installed_version(cfg: &Config) -> String {
|
|
|
77
69
|
.unwrap_or_default()
|
|
78
70
|
}
|
|
79
71
|
|
|
80
|
-
#[derive(Serialize, Deserialize)]
|
|
81
|
-
struct Greeting {
|
|
82
|
-
text: String,
|
|
83
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
84
|
-
updated_by: Option<String>,
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
#[derive(Deserialize)]
|
|
88
|
-
#[serde(deny_unknown_fields)]
|
|
89
|
-
struct SetGreeting {
|
|
90
|
-
text: String,
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
fn greeting_path(cfg: &Config) -> PathBuf {
|
|
94
|
-
cfg.data_dir.join("greeting.json")
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
fn get_greeting(cfg: &Config) -> Result<Greeting, Error> {
|
|
98
|
-
match fs::read(greeting_path(cfg)) {
|
|
99
|
-
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Greeting {
|
|
100
|
-
text: "Hello".into(),
|
|
101
|
-
updated_by: None,
|
|
102
|
-
}),
|
|
103
|
-
Err(e) => Err(internal(e)),
|
|
104
|
-
Ok(data) => serde_json::from_slice(&data).map_err(internal),
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
fn set_greeting(cfg: &Config, ctx: &Context, params: &Params) -> Result<Greeting, Error> {
|
|
109
|
-
let SetGreeting { text } = params.decode()?;
|
|
110
|
-
if !(1..=200).contains(&text.chars().count()) {
|
|
111
|
-
return Err(Error::bad_request("text must be 1 to 200 characters"));
|
|
112
|
-
}
|
|
113
|
-
let greeting = Greeting {
|
|
114
|
-
text,
|
|
115
|
-
updated_by: Some(ctx.user.clone()),
|
|
116
|
-
};
|
|
117
|
-
write_file_atomic(
|
|
118
|
-
&greeting_path(cfg),
|
|
119
|
-
&serde_json::to_vec(&greeting).map_err(internal)?,
|
|
120
|
-
)
|
|
121
|
-
.map_err(internal)?;
|
|
122
|
-
Ok(greeting)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/// An unexpected failure: logged to stderr (the package's log), `internal` for the browser.
|
|
126
|
-
pub fn internal(e: impl std::fmt::Display) -> Error {
|
|
127
|
-
eprintln!("{e}");
|
|
128
|
-
Error::new("internal", "internal error")
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/// Replaces path in one rename, so readers never see half a file.
|
|
132
|
-
pub fn write_file_atomic(path: &Path, data: &[u8]) -> std::io::Result<()> {
|
|
133
|
-
let nanos = SystemTime::now()
|
|
134
|
-
.duration_since(UNIX_EPOCH)
|
|
135
|
-
.map_or(0, |d| d.subsec_nanos());
|
|
136
|
-
let tmp = path.with_file_name(format!(".tmp-{}-{nanos}", std::process::id()));
|
|
137
|
-
let written = fs::File::create_new(&tmp)
|
|
138
|
-
.and_then(|mut f| f.write_all(data))
|
|
139
|
-
.and_then(|_| fs::rename(&tmp, path));
|
|
140
|
-
if written.is_err() {
|
|
141
|
-
let _ = fs::remove_file(&tmp);
|
|
142
|
-
}
|
|
143
|
-
written
|
|
144
|
-
}
|
|
145
|
-
|
|
146
72
|
/// Now as 2006-01-02T15:04:05Z.
|
|
147
73
|
pub fn utc_now() -> String {
|
|
148
74
|
let secs = SystemTime::now()
|
|
@@ -185,13 +111,6 @@ mod tests {
|
|
|
185
111
|
(dir, cfg)
|
|
186
112
|
}
|
|
187
113
|
|
|
188
|
-
fn files(cfg: &Config) -> Vec<String> {
|
|
189
|
-
fs::read_dir(&cfg.data_dir)
|
|
190
|
-
.unwrap()
|
|
191
|
-
.map(|e| e.unwrap().file_name().into_string().unwrap())
|
|
192
|
-
.collect()
|
|
193
|
-
}
|
|
194
|
-
|
|
195
114
|
#[test]
|
|
196
115
|
fn status_reports_the_caller_and_the_installed_version() {
|
|
197
116
|
let (_dir, cfg) = setup();
|
|
@@ -279,74 +198,17 @@ mod tests {
|
|
|
279
198
|
}
|
|
280
199
|
|
|
281
200
|
#[test]
|
|
282
|
-
fn
|
|
283
|
-
let (_dir, cfg) = setup();
|
|
284
|
-
let reply = call(
|
|
285
|
-
build(cfg),
|
|
286
|
-
"greeting.get",
|
|
287
|
-
Value::Null,
|
|
288
|
-
"admin",
|
|
289
|
-
&Services::new(),
|
|
290
|
-
);
|
|
291
|
-
assert_eq!(reply.data, json!({"text": "Hello"}));
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
#[test]
|
|
295
|
-
fn greeting_set_stores_the_text_and_who_set_it() {
|
|
201
|
+
fn sample_operations_are_absent() {
|
|
296
202
|
let (_dir, cfg) = setup();
|
|
297
|
-
|
|
298
|
-
build(cfg.clone()),
|
|
299
|
-
"greeting.set",
|
|
300
|
-
json!({"text": "Hi 👋"}),
|
|
301
|
-
"alice",
|
|
302
|
-
&Services::new(),
|
|
303
|
-
);
|
|
304
|
-
assert_eq!(reply.data, json!({"text": "Hi 👋", "updated_by": "alice"}));
|
|
305
|
-
let reply = call(
|
|
306
|
-
build(cfg.clone()),
|
|
307
|
-
"greeting.get",
|
|
308
|
-
Value::Null,
|
|
309
|
-
"admin",
|
|
310
|
-
&Services::new(),
|
|
311
|
-
);
|
|
312
|
-
assert_eq!(reply.data, json!({"text": "Hi 👋", "updated_by": "alice"}));
|
|
313
|
-
assert_eq!(files(&cfg), ["greeting.json"]);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
#[test]
|
|
317
|
-
fn greeting_counts_characters_not_bytes() {
|
|
318
|
-
let (_dir, cfg) = setup();
|
|
319
|
-
let reply = call(
|
|
320
|
-
build(cfg),
|
|
321
|
-
"greeting.set",
|
|
322
|
-
json!({"text": "é".repeat(200)}),
|
|
323
|
-
"admin",
|
|
324
|
-
&Services::new(),
|
|
325
|
-
);
|
|
326
|
-
assert_eq!(reply.code(), "");
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
#[test]
|
|
330
|
-
fn greeting_set_refuses_invalid_params_and_keeps_the_old_text() {
|
|
331
|
-
let (_dir, cfg) = setup();
|
|
332
|
-
for params in [
|
|
333
|
-
Value::Null,
|
|
334
|
-
json!([]),
|
|
335
|
-
json!({}),
|
|
336
|
-
json!({"text": ""}),
|
|
337
|
-
json!({"text": "x".repeat(201)}),
|
|
338
|
-
json!({"text": 5}),
|
|
339
|
-
json!({"text": "hi", "extra": 1}),
|
|
340
|
-
] {
|
|
203
|
+
for method in ["greeting.get", "greeting.set"] {
|
|
341
204
|
let reply = call(
|
|
342
205
|
build(cfg.clone()),
|
|
343
|
-
|
|
344
|
-
|
|
206
|
+
method,
|
|
207
|
+
Value::Null,
|
|
345
208
|
"admin",
|
|
346
209
|
&Services::new(),
|
|
347
210
|
);
|
|
348
|
-
|
|
211
|
+
assert_ne!(reply.code(), "");
|
|
349
212
|
}
|
|
350
|
-
assert!(files(&cfg).is_empty());
|
|
351
213
|
}
|
|
352
214
|
}
|
|
@@ -48,7 +48,7 @@ backend:
|
|
|
48
48
|
runtime: rust
|
|
49
49
|
dir: backend
|
|
50
50
|
entry: app
|
|
51
|
-
methods: [app.status
|
|
51
|
+
methods: [app.status]
|
|
52
52
|
|
|
53
53
|
ui:
|
|
54
54
|
# Non-UI-SDK frontends open a full page; Portal is a separate opt-in.
|
|
@@ -92,33 +92,7 @@ capabilities: {}
|
|
|
92
92
|
# backup:
|
|
93
93
|
# hyper_backup: false
|
|
94
94
|
|
|
95
|
-
#
|
|
95
|
+
# Operation metadata has one source. Generate/check before building.
|
|
96
96
|
ctl:
|
|
97
97
|
enabled: true
|
|
98
|
-
|
|
99
|
-
status.show:
|
|
100
|
-
cli: [status]
|
|
101
|
-
summary: Show this App's status and verified caller
|
|
102
|
-
effect: read
|
|
103
|
-
flow: {type: invoke, method: app.status}
|
|
104
|
-
paramsSchema: {type: object, additionalProperties: false}
|
|
105
|
-
examples: [{}]
|
|
106
|
-
greeting.get:
|
|
107
|
-
cli: [greeting, get]
|
|
108
|
-
summary: Read the greeting
|
|
109
|
-
effect: read
|
|
110
|
-
flow: {type: invoke, method: greeting.get}
|
|
111
|
-
paramsSchema: {type: object, additionalProperties: false}
|
|
112
|
-
examples: [{}]
|
|
113
|
-
greeting.set:
|
|
114
|
-
cli: [greeting, set]
|
|
115
|
-
summary: Set the greeting
|
|
116
|
-
effect: write
|
|
117
|
-
flow: {type: invoke, method: greeting.set}
|
|
118
|
-
paramsSchema:
|
|
119
|
-
type: object
|
|
120
|
-
required: [text]
|
|
121
|
-
additionalProperties: false
|
|
122
|
-
properties:
|
|
123
|
-
text: {type: string, minLength: 1, maxLength: 200, description: Greeting text, examples: [Hello]}
|
|
124
|
-
examples: [{text: Hello}]
|
|
98
|
+
registry: operations.json
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "synopkgland.api",
|
|
3
|
+
"version": 2,
|
|
4
|
+
"operations": {
|
|
5
|
+
"example.read": {
|
|
6
|
+
"summary": "Read a resource",
|
|
7
|
+
"ai": "read",
|
|
8
|
+
"cli": [
|
|
9
|
+
"example",
|
|
10
|
+
"read"
|
|
11
|
+
],
|
|
12
|
+
"effect": "read",
|
|
13
|
+
"permission": "administrators",
|
|
14
|
+
"paramsSchema": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false
|
|
17
|
+
},
|
|
18
|
+
"resultSchema": {
|
|
19
|
+
"type": "object"
|
|
20
|
+
},
|
|
21
|
+
"flow": {
|
|
22
|
+
"type": "invoke",
|
|
23
|
+
"method": "example.read"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"example.preview": {
|
|
27
|
+
"summary": "Preview a resource change",
|
|
28
|
+
"ai": "preview",
|
|
29
|
+
"cli": [
|
|
30
|
+
"example",
|
|
31
|
+
"preview"
|
|
32
|
+
],
|
|
33
|
+
"effect": "read",
|
|
34
|
+
"permission": "administrators",
|
|
35
|
+
"paramsSchema": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"additionalProperties": false
|
|
38
|
+
},
|
|
39
|
+
"resultSchema": {
|
|
40
|
+
"type": "object"
|
|
41
|
+
},
|
|
42
|
+
"flow": {
|
|
43
|
+
"type": "invoke",
|
|
44
|
+
"method": "example.preview"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"example.draft": {
|
|
48
|
+
"summary": "Draft a resource change for human confirmation",
|
|
49
|
+
"ai": "draft",
|
|
50
|
+
"cli": [
|
|
51
|
+
"example",
|
|
52
|
+
"draft"
|
|
53
|
+
],
|
|
54
|
+
"effect": "write",
|
|
55
|
+
"permission": "administrators",
|
|
56
|
+
"paramsSchema": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": false
|
|
59
|
+
},
|
|
60
|
+
"resultSchema": {
|
|
61
|
+
"type": "object"
|
|
62
|
+
},
|
|
63
|
+
"flow": {
|
|
64
|
+
"type": "invoke",
|
|
65
|
+
"method": "example.draft"
|
|
66
|
+
},
|
|
67
|
+
"preview": "example.preview"
|
|
68
|
+
},
|
|
69
|
+
"example.human": {
|
|
70
|
+
"summary": "Delete a resource after human review",
|
|
71
|
+
"ai": "human-only",
|
|
72
|
+
"cli": [
|
|
73
|
+
"example",
|
|
74
|
+
"human"
|
|
75
|
+
],
|
|
76
|
+
"effect": "destructive",
|
|
77
|
+
"permission": "administrators",
|
|
78
|
+
"paramsSchema": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"additionalProperties": false
|
|
81
|
+
},
|
|
82
|
+
"resultSchema": {
|
|
83
|
+
"type": "object"
|
|
84
|
+
},
|
|
85
|
+
"flow": {
|
|
86
|
+
"type": "invoke",
|
|
87
|
+
"method": "example.human"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"example.hidden": {
|
|
91
|
+
"summary": "Read internal state",
|
|
92
|
+
"ai": "hidden",
|
|
93
|
+
"cli": [
|
|
94
|
+
"example",
|
|
95
|
+
"hidden"
|
|
96
|
+
],
|
|
97
|
+
"effect": "read",
|
|
98
|
+
"permission": "administrators",
|
|
99
|
+
"paramsSchema": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"additionalProperties": false
|
|
102
|
+
},
|
|
103
|
+
"resultSchema": {
|
|
104
|
+
"type": "object"
|
|
105
|
+
},
|
|
106
|
+
"flow": {
|
|
107
|
+
"type": "invoke",
|
|
108
|
+
"method": "example.hidden"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "synopkgland.api",
|
|
3
|
+
"version": 2,
|
|
4
|
+
"operations": {
|
|
5
|
+
"app.status": {
|
|
6
|
+
"summary": "Read App status",
|
|
7
|
+
"ai": "read",
|
|
8
|
+
"cli": [
|
|
9
|
+
"app",
|
|
10
|
+
"status"
|
|
11
|
+
],
|
|
12
|
+
"effect": "read",
|
|
13
|
+
"permission": "administrators",
|
|
14
|
+
"paramsSchema": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false
|
|
17
|
+
},
|
|
18
|
+
"resultSchema": {
|
|
19
|
+
"type": "object"
|
|
20
|
+
},
|
|
21
|
+
"flow": {
|
|
22
|
+
"type": "invoke",
|
|
23
|
+
"method": "app.status"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -170,8 +170,8 @@ dependencies = [
|
|
|
170
170
|
|
|
171
171
|
[[package]]
|
|
172
172
|
name = "syno-pkg-land-app"
|
|
173
|
-
version = "0.
|
|
174
|
-
source = "git+https://github.com/synopackageland/syno-pkg-land-sdk?tag=v0.
|
|
173
|
+
version = "0.47.0"
|
|
174
|
+
source = "git+https://github.com/synopackageland/syno-pkg-land-sdk?tag=v0.47.0#c486d2ce888248e9e68a67908810c62f968d601e"
|
|
175
175
|
dependencies = [
|
|
176
176
|
"libc",
|
|
177
177
|
"serde",
|
|
@@ -10,7 +10,7 @@ libc = "0.2"
|
|
|
10
10
|
serde = { version = "1", features = ["derive"] }
|
|
11
11
|
serde_json = "1"
|
|
12
12
|
# The SDK version is the tag; `synopkgland check` reads it from Cargo.lock.
|
|
13
|
-
syno-pkg-land-app = { git = "https://github.com/synopackageland/syno-pkg-land-sdk", tag = "v0.
|
|
13
|
+
syno-pkg-land-app = { git = "https://github.com/synopackageland/syno-pkg-land-sdk", tag = "v0.47.0" }
|
|
14
14
|
|
|
15
15
|
[profile.release]
|
|
16
16
|
# A handler panic becomes an "internal" reply; abort would end the App without one.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//! executor moves it to the caller's destination with the caller's own
|
|
4
4
|
//! permission, so the package never needs write access to that folder.
|
|
5
5
|
|
|
6
|
-
use crate::routes::{Config,
|
|
6
|
+
use crate::routes::{Config, utc_now};
|
|
7
7
|
use serde::Deserialize;
|
|
8
8
|
use serde_json::{Value, json};
|
|
9
9
|
use sha2::{Digest, Sha256};
|
|
@@ -12,6 +12,12 @@ use std::io::{ErrorKind, Write};
|
|
|
12
12
|
use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt};
|
|
13
13
|
use syno_pkg_land_app::{App, Context, Error, Params};
|
|
14
14
|
|
|
15
|
+
// Log unexpected failures privately; return a stable code to the caller.
|
|
16
|
+
fn internal(e: impl std::fmt::Display) -> Error {
|
|
17
|
+
eprintln!("{e}");
|
|
18
|
+
Error::new("internal", "internal error")
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
#[derive(Deserialize)]
|
|
16
22
|
#[serde(deny_unknown_fields)]
|
|
17
23
|
struct SaveReport {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"operations": {
|
|
3
|
+
"report.save": {
|
|
4
|
+
"summary": "Save an example report",
|
|
5
|
+
"ai": "hidden",
|
|
6
|
+
"effect": "write",
|
|
7
|
+
"permission": "authenticated",
|
|
8
|
+
"cli": [
|
|
9
|
+
"report",
|
|
10
|
+
"save"
|
|
11
|
+
],
|
|
12
|
+
"paramsSchema": {},
|
|
13
|
+
"resultSchema": {},
|
|
14
|
+
"flow": {
|
|
15
|
+
"type": "invoke",
|
|
16
|
+
"method": "report.save"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|