@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.
Files changed (62) hide show
  1. package/README.md +9 -9
  2. package/package.json +1 -1
  3. package/templates/_shared/AGENTS.md +6 -5
  4. package/templates/_shared/CLAUDE.md +2 -0
  5. package/templates/compose/dsm-package.yaml +1 -1
  6. package/templates/native-go/README.md +28 -11
  7. package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
  8. package/templates/native-go/backend/cmd/app/main.go +9 -2
  9. package/templates/native-go/backend/go.mod +1 -1
  10. package/templates/native-go/backend/go.sum +2 -2
  11. package/templates/native-go/backend/internal/api/operations.json +5 -0
  12. package/templates/native-go/backend/internal/api/permissions.go +7 -0
  13. package/templates/native-go/backend/internal/api/permissions.json +3 -1
  14. package/templates/native-go/backend/internal/app/routes.go +2 -46
  15. package/templates/native-go/backend/internal/app/routes_test.go +4 -25
  16. package/templates/native-go/dsm-package.yaml +2 -28
  17. package/templates/native-go/examples/operations.json +112 -0
  18. package/templates/native-go/operations.json +27 -0
  19. package/templates/native-go/ui/app.js +0 -25
  20. package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +3 -4
  21. package/templates/native-go-root-ops/frontend/test/executor-gate.test.ts +7 -6
  22. package/templates/native-go-vue/frontend/package-lock.json +4 -4
  23. package/templates/native-go-vue/frontend/package.json +1 -1
  24. package/templates/native-go-vue/frontend/src/App.vue +5 -34
  25. package/templates/native-go-vue/frontend/test/apps.test.ts +19 -25
  26. package/templates/native-go-vue/frontend/test/native-controls.test.ts +4 -1
  27. package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +5 -3
  28. package/templates/native-node/README.md +27 -7
  29. package/templates/native-node/backend/app/permissions.json +3 -1
  30. package/templates/native-node/backend/package-lock.json +1 -1
  31. package/templates/native-node/backend/src/routes.ts +5 -43
  32. package/templates/native-node/backend/test/routes.test.ts +3 -23
  33. package/templates/native-node/dsm-package.yaml +3 -29
  34. package/templates/native-node/examples/operations.json +112 -0
  35. package/templates/native-node/operations.json +27 -0
  36. package/templates/native-node-delegate/operations.patch.json +20 -0
  37. package/templates/native-node-jobs/operations.patch.json +52 -0
  38. package/templates/native-node-root-ops/operations.patch.json +20 -0
  39. package/templates/native-python/README.md +27 -7
  40. package/templates/native-python/backend/app/permissions.json +3 -1
  41. package/templates/native-python/backend/app/routes.py +2 -41
  42. package/templates/native-python/backend/tests/test_routes.py +3 -19
  43. package/templates/native-python/dsm-package.yaml +3 -29
  44. package/templates/native-python/examples/operations.json +112 -0
  45. package/templates/native-python/operations.json +27 -0
  46. package/templates/native-python-delegate/operations.patch.json +20 -0
  47. package/templates/native-python-jobs/operations.patch.json +52 -0
  48. package/templates/native-python-root-ops/operations.patch.json +20 -0
  49. package/templates/native-rust/README.md +28 -8
  50. package/templates/native-rust/backend/Cargo.lock +2 -2
  51. package/templates/native-rust/backend/Cargo.toml +1 -1
  52. package/templates/native-rust/backend/permissions.json +3 -1
  53. package/templates/native-rust/backend/src/routes.rs +11 -149
  54. package/templates/native-rust/dsm-package.yaml +3 -29
  55. package/templates/native-rust/examples/operations.json +112 -0
  56. package/templates/native-rust/operations.json +27 -0
  57. package/templates/native-rust-delegate/backend/Cargo.lock +2 -2
  58. package/templates/native-rust-delegate/backend/Cargo.toml +1 -1
  59. package/templates/native-rust-delegate/backend/src/delegate.rs +7 -1
  60. package/templates/native-rust-delegate/operations.patch.json +20 -0
  61. package/templates/native-rust-jobs/operations.patch.json +52 -0
  62. 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("greeting.get") same-origin POST + DSM session
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.43.0 defaults to 300-second requests and a 302-second browser wait.
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.43.0/docs/request-deadlines.md).
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, what the backend reports, and (for administrators) a form that stores a
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` | Who may call each of your methods | Yes |
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
- all access rules live in `backend/permissions.json`:
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; use `routes::internal(e)` for anything unexpected, which logs it and answers `internal`. `ctx.runtime` returns the runtime's refusal as an `Error`, so `?` passes its code on to the browser.
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.43.0"
108
- source = "git+https://github.com/synopackageland/syno-pkg-land-sdk?tag=v0.43.0#263d2a4fbab040756d406b76dd8272bca9db9707"
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.43.0" }
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,5 +1,7 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "default": "administrators",
4
- "methods": {}
4
+ "methods": {
5
+ "app.status": "administrators"
6
+ }
5
7
  }
@@ -1,21 +1,19 @@
1
- //! The App's methods. Register a method in `build` and list it in
2
- //! dsm-package.yaml's `backend.methods`; give it a rule in permissions.json
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::io::{ErrorKind, Write};
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, Error, Params};
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 greeting_defaults_to_hello() {
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
- let reply = call(
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
- "greeting.set",
344
- params.clone(),
206
+ method,
207
+ Value::Null,
345
208
  "admin",
346
209
  &Services::new(),
347
210
  );
348
- assert_eq!(reply.code(), "bad_request", "{params}");
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, greeting.get, greeting.set]
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
- # Explicit AI commands. Internal/UI methods are not automatically exposed.
95
+ # Operation metadata has one source. Generate/check before building.
96
96
  ctl:
97
97
  enabled: true
98
- commands:
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.43.0"
174
- source = "git+https://github.com/synopackageland/syno-pkg-land-sdk?tag=v0.43.0#263d2a4fbab040756d406b76dd8272bca9db9707"
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.43.0" }
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, internal, utc_now};
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
+ }