@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
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ daemon:
|
|
|
22
22
|
window_seconds: 300
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Templates 0.38.0 pin UI 0.19.1 and SDK 0.
|
|
25
|
+
Templates 0.38.0 pin UI 0.19.1 and SDK 0.47.0 in native and Compose projects.
|
|
26
26
|
Use a CLI that supports executor manifest format 1. The Vue `AppShell` shows a
|
|
27
27
|
non-blocking **Reload** notice when an upgrade replaced the UI an open page
|
|
28
28
|
loaded. Go Apps select
|
|
@@ -87,16 +87,15 @@ the single source of the SDK version and `frontend/package.json` pins
|
|
|
87
87
|
`@synopackageland/sdk` to the same version. Python and Node.js name it in
|
|
88
88
|
`backend.sdk`, Rust in the `syno-pkg-land-app` tag of `backend/Cargo.toml`
|
|
89
89
|
(and `Cargo.lock`); all of them pin 0.41.1. The Vue frontend pins
|
|
90
|
-
`@synopackageland/ui` 0.19.1. SDK 0.
|
|
90
|
+
`@synopackageland/ui` 0.19.1. SDK 0.47.0 accepts UI `>=0.10.1 <0.20.0`,
|
|
91
91
|
so no UI peer override is needed. Tests use jsdom 30 for UI floating-layer styles.
|
|
92
92
|
|
|
93
93
|
Interactive NAS file operations use same-origin browser calls.
|
|
94
94
|
Custom file handlers use the SDK's
|
|
95
95
|
[session transport migration](https://github.com/synopackageland/syno-pkg-land-sdk/blob/v0.41.1/docs/dsm-session-transport.md).
|
|
96
|
-
The CLI reads SDK 0.
|
|
96
|
+
The CLI reads SDK 0.47.0's release manifest, which names immutable
|
|
97
97
|
executor/v0.39.0 and its executor digests. CLI distributions must resolve
|
|
98
|
-
templates 0.
|
|
99
|
-
templates dependency remains on the older minor.
|
|
98
|
+
templates 0.39.x for the current registry scaffolds.
|
|
100
99
|
|
|
101
100
|
Every App method must appear in the permissions table, including methods whose
|
|
102
101
|
permission equals `default: administrators`. Dynamic method names and custom
|
|
@@ -110,7 +109,7 @@ Keep punctuation between sentences and intentional loading ellipses.
|
|
|
110
109
|
|
|
111
110
|
## Request deadlines and background work
|
|
112
111
|
|
|
113
|
-
SDK/App 0.
|
|
112
|
+
SDK/App 0.47.0 defaults to 300 seconds for cooperative requests and 302 seconds
|
|
114
113
|
for the TS client and generated DSM window. The CLI reads the selected SDK's
|
|
115
114
|
timeout spec; `backend.timeout_seconds` explicitly overrides it for native
|
|
116
115
|
backends, and an earlier parent deadline still wins. Go assembly should fall
|
|
@@ -304,7 +303,7 @@ The bridge examples expose submission, lookup and cancellation through
|
|
|
304
303
|
|
|
305
304
|
## External access
|
|
306
305
|
|
|
307
|
-
With a compatible CLI and SDK 0.
|
|
306
|
+
With a compatible CLI and SDK 0.47.0, a gateway App may declare `identity_gateway.external_access: {connect: optional, origin_change: confirm, single_origin: true}` for an upstream that signs in on one fixed URL. The policy is forwarded to the packaged gateway. A single-origin App's `manual_origin` accepts an explicit HTTPS port; the `external-origin` installation setting remains restricted to port 443. The DSM launcher selects a provider destination, probes `/_spl/ping` for up to eight seconds, and explains failures before offering **Open anyway**. A configured single-origin App always opens its primary external origin.
|
|
308
307
|
|
|
309
308
|
|
|
310
309
|
Compose templates declare an app-named dedicated shared folder by default. DSM
|
|
@@ -351,5 +350,6 @@ for retry because the executor may already have admitted it. Other settled
|
|
|
351
350
|
sources use the SDK's explicit discard operation when the App deletes the job.
|
|
352
351
|
|
|
353
352
|
An opt-in [DSM path ingress migration guide](examples/path-ingress/README.md)
|
|
354
|
-
explains
|
|
355
|
-
|
|
353
|
+
explains the default `/synopkgland/<package>/` on the DSM origin and opt-in
|
|
354
|
+
custom lowercase paths for upstreams with a fixed base. Root `/` is unsupported;
|
|
355
|
+
installation checks route ownership before DSM registers the resource.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/templates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
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": {
|
|
@@ -54,13 +54,15 @@ Application Privileges allows for `SYNO.SDS.<package>.Instance`; DSM's install
|
|
|
54
54
|
default allows administrators only, so nobody else enters until an admin grants
|
|
55
55
|
access. One CGI check covers every route; failures refuse; changes apply within
|
|
56
56
|
30 seconds. Never re-implement AppPriv rules in the App.
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
Declare every App method once in operations.json with a required ai class. Run
|
|
58
|
+
`ctl generate` to generate permissions, routes, commands, AI tools and the App
|
|
59
|
+
reference; run `ctl generate --check` and `check` to refuse drift. Read generated
|
|
60
|
+
operation descriptions instead of maintaining a second list in a skill. Dynamic method names and unregistered HTTP dispatchers are rejected; use
|
|
59
61
|
fixed SDK route constructors. Use identity_gateway.upstream_auth_paths for
|
|
60
62
|
client API paths authenticated by the upstream.
|
|
61
63
|
`api.HandleHTTP("/proxy/", "app.proxy", handler)` serves raw HTTP
|
|
62
64
|
for literal, disjoint full-segment prefixes outside SDK reserved paths. Declare
|
|
63
|
-
the key as authenticated or administrators in
|
|
65
|
+
the key as authenticated or administrators in operations.json, then generate. Origin, entry,
|
|
64
66
|
identity and permission checks precede raw streaming/initial-upgrade handlers;
|
|
65
67
|
CGI itself cannot upgrade a connection. For package-private worker HTTP use
|
|
66
68
|
`ipc.ServeUnix(ctx, path, handler)` and `ipc.DialUnix`: owned mode-0600 sockets
|
|
@@ -76,8 +78,7 @@ dsm-package.yaml.
|
|
|
76
78
|
Mark routes ordinary users call `authenticated`, mark whole admin routes
|
|
77
79
|
`administrators`, guard global settings inside shared routes with
|
|
78
80
|
the SDK's require-admin check, and hide admin controls with `ctx.user?.isAdmin`
|
|
79
|
-
(UI only, never the authority).
|
|
80
|
-
call `greeting.set`.
|
|
81
|
+
(UI only, never the authority). Declare actual App permissions in operations.json and regenerate the policy.
|
|
81
82
|
|
|
82
83
|
## DSM Application Portal (`portal`)
|
|
83
84
|
|
|
@@ -23,7 +23,7 @@ compose:
|
|
|
23
23
|
web: {service: web, port: 8080, health: {path: /, status: 200}}
|
|
24
24
|
network: {egress: isolated}
|
|
25
25
|
identity_gateway:
|
|
26
|
-
sdk: 0.
|
|
26
|
+
sdk: 0.47.0
|
|
27
27
|
mode: none
|
|
28
28
|
listen: {http: 28000}
|
|
29
29
|
# Web Apps default to new-page through the shared app-launch resolver.
|
|
@@ -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 (backend/, 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, and Go 1.25+.
|
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
(cd backend && go test ./...) # auth policy + route 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
|
## Capabilities and schema 2
|
|
49
50
|
|
|
@@ -79,7 +80,7 @@ resolve them before retrying. Already-declared capabilities are a no-op.
|
|
|
79
80
|
| `i18n/messages/<locale>.json` | UI text in nine languages | Yes |
|
|
80
81
|
| `i18n/glossary.json` | DSM's own UI terms, and which messages must use them | Add terms |
|
|
81
82
|
| `backend/internal/app/` | Your API: routes and handlers | Yes — your backend |
|
|
82
|
-
| `backend/internal/api/permissions.json` |
|
|
83
|
+
| `backend/internal/api/permissions.json` | Generated method policy | No |
|
|
83
84
|
| `backend/cmd/app/` | Wires your routes to the SDK server | Rarely |
|
|
84
85
|
| `backend/go.mod` | Your module; `require github.com/synopackageland/syno-pkg-land-sdk` pins the SDK | SDK upgrades |
|
|
85
86
|
| `dist/` | Built SPKs (ignored by git) | No |
|
|
@@ -91,15 +92,13 @@ resolve them before retrying. Already-declared capabilities are a no-op.
|
|
|
91
92
|
## Add an API operation
|
|
92
93
|
|
|
93
94
|
Add a handler in `backend/internal/app/routes.go`. Every method defaults to
|
|
94
|
-
DSM administrator membership, resolved by its native SDK.
|
|
95
|
-
`
|
|
95
|
+
DSM administrator membership, resolved by its native SDK. Operation metadata and access rules live in `operations.json`;
|
|
96
|
+
`ctl generate` writes the runtime policy and launcher access:
|
|
96
97
|
|
|
97
98
|
```go
|
|
98
99
|
func Routes(cfg Config) []api.Route {
|
|
99
100
|
return []api.Route{
|
|
100
101
|
api.Handle("app.status", cfg.status),
|
|
101
|
-
api.Handle("greeting.get", cfg.getGreeting),
|
|
102
|
-
api.Handle("greeting.set", cfg.setGreeting),
|
|
103
102
|
api.Handle("notes.list", cfg.listNotes), // new: administrators by default
|
|
104
103
|
api.Handle("notes.purge", cfg.purgeNotes), // new: administrators by default
|
|
105
104
|
}
|
|
@@ -397,7 +396,7 @@ Use `install.sharedFolder: {mode: dedicated, key: media, name: __PACKAGE__, perm
|
|
|
397
396
|
for shared data. DSM creates the app-specific folder and grants the declared
|
|
398
397
|
package-account permission. The wizard defaults to it and validates any existing
|
|
399
398
|
folder choice before Next. The backend reports the real state: the `sharedfolder.status` operation (administrators only
|
|
400
|
-
unless you change
|
|
399
|
+
unless you change the operation registry) returns each folder with what the App needs, what the
|
|
401
400
|
package account has (`none`, `read`, `write`) and a `state` (`ready`, `pending`, `missing`,
|
|
402
401
|
`ambiguous`). `cfg.SharedFolderPath(key)` returns the path or an error that names the
|
|
403
402
|
Control Panel step instead of failing on a file read. Keep `SharedFolderRequired` in
|
|
@@ -426,3 +425,21 @@ Submit with `bg.jobs.submit`; use `bg.jobs.get` for progress and
|
|
|
426
425
|
`bg.jobs.cancel` to cancel. Pause/resume and existing schedule routes continue
|
|
427
426
|
to operate on retained imported copies. Schedules require explicit consent and
|
|
428
427
|
reprocess the fixed private copy, without rereading the source file.
|
|
428
|
+
|
|
429
|
+
## Operation registry
|
|
430
|
+
|
|
431
|
+
Declare each App method once in `operations.json`, including its English summary,
|
|
432
|
+
params/result schemas, effect, permission, flow and required `ai` class. The
|
|
433
|
+
SDK contributes its own hidden operation catalog for enabled capability routes.
|
|
434
|
+
Register the handler, then run `synopkgland ctl generate`, `synopkgland ctl
|
|
435
|
+
generate --check` and `synopkgland check`. Build refuses generated drift or a
|
|
436
|
+
route/spec mismatch. Never maintain commands or operation descriptions in the
|
|
437
|
+
manifest or skill by hand. Read the generated `generated/operations.md`.
|
|
438
|
+
|
|
439
|
+
`examples/operations.json` illustrates read, preview, draft, human-only and hidden.
|
|
440
|
+
It is documentation only: its operations have no handlers and are never shipped
|
|
441
|
+
as commands. Copy a declaration only when implementing a real App capability.
|
|
442
|
+
Draft writes require a non-mutating preview and explicit human confirmation.
|
|
443
|
+
Long operations use the existing job flow; tools own polling and pagination.
|
|
444
|
+
|
|
445
|
+
Use a matching SDK/CLI/templates release containing the registry support.
|
|
@@ -47,6 +47,8 @@ var (
|
|
|
47
47
|
portal = ""
|
|
48
48
|
// backend.timeout_seconds, api.DefaultRequestTimeout unless set.
|
|
49
49
|
requestTimeout = ""
|
|
50
|
+
// Build enables registry equality only for Apps that opt in to ctl.registry.
|
|
51
|
+
operationRegistry = ""
|
|
50
52
|
)
|
|
51
53
|
|
|
52
54
|
// deadline is the CGI request deadline: requestTimeout seconds, or
|
|
@@ -73,7 +75,7 @@ func main() {
|
|
|
73
75
|
if err != nil {
|
|
74
76
|
log.Fatal(err)
|
|
75
77
|
}
|
|
76
|
-
cfg := app.Config{PackageID: packageID, Version: version, DataDir: ".", Portal: portal != ""}
|
|
78
|
+
cfg := app.Config{PackageID: packageID, Version: version, DataDir: ".", Portal: portal != "", PortalExecutor: portal == "executor"}
|
|
77
79
|
if app.SessionFilesEnabled(fileStation) {
|
|
78
80
|
cfg.SessionFiles = filestation.Local(cfg.DataDir)
|
|
79
81
|
}
|
|
@@ -131,7 +133,12 @@ func main() {
|
|
|
131
133
|
if err != nil {
|
|
132
134
|
log.Fatal(err)
|
|
133
135
|
}
|
|
134
|
-
srv
|
|
136
|
+
var srv *api.Server
|
|
137
|
+
if operationRegistry == "on" {
|
|
138
|
+
srv, err = api.NewServerWithOperations(auth, appapi.Operations(), permissions, app.Routes(cfg)...)
|
|
139
|
+
} else {
|
|
140
|
+
srv, err = api.NewServerWithPermissions(auth, permissions, app.Routes(cfg)...)
|
|
141
|
+
}
|
|
135
142
|
if err != nil {
|
|
136
143
|
log.Fatal(err)
|
|
137
144
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
github.com/synopackageland/syno-pkg-land-sdk v0.
|
|
2
|
-
github.com/synopackageland/syno-pkg-land-sdk v0.
|
|
1
|
+
github.com/synopackageland/syno-pkg-land-sdk v0.47.0 h1:NYGAo/D1MTuC/rfHe61JpVdgBmiY4DW5C0AvNvNIVxU=
|
|
2
|
+
github.com/synopackageland/syno-pkg-land-sdk v0.47.0/go.mod h1:8GwREGbczc8mAk4Gv8FUHaukXXCUstjzkovQrNV59Cc=
|
|
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=
|
|
@@ -24,3 +24,10 @@ func Permissions() ([]byte, error) {
|
|
|
24
24
|
}
|
|
25
25
|
return permissionsJSON, nil
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
// operationsJSON is generated from the App registry and the SDK capability catalog.
|
|
29
|
+
//
|
|
30
|
+
//go:embed operations.json
|
|
31
|
+
var operationsJSON []byte
|
|
32
|
+
|
|
33
|
+
func Operations() []byte { return operationsJSON }
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
// Package app holds
|
|
2
|
-
//
|
|
3
|
-
// use the immutable SDK policy.
|
|
4
|
-
// unlisted methods require membership in DSM's administrators group.
|
|
1
|
+
// Package app holds the business handlers. Declare each operation once in
|
|
2
|
+
// operations.json; ctl generate writes the policy and SDK runtime registry.
|
|
5
3
|
//
|
|
6
4
|
//line __PACKAGE__/internal/app/routes.go:1
|
|
7
5
|
package app
|
|
@@ -10,13 +8,10 @@ import (
|
|
|
10
8
|
"bytes"
|
|
11
9
|
"context"
|
|
12
10
|
"encoding/json"
|
|
13
|
-
"errors"
|
|
14
|
-
"io/fs"
|
|
15
11
|
"os"
|
|
16
12
|
"path/filepath"
|
|
17
13
|
"runtime"
|
|
18
14
|
"time"
|
|
19
|
-
"unicode/utf8"
|
|
20
15
|
|
|
21
16
|
"github.com/synopackageland/syno-pkg-land-sdk/api"
|
|
22
17
|
"github.com/synopackageland/syno-pkg-land-sdk/filestation"
|
|
@@ -59,8 +54,6 @@ func Routes(cfg Config) []api.Route {
|
|
|
59
54
|
routes = append(routes, []api.Route{
|
|
60
55
|
api.Handle("app.status", cfg.status),
|
|
61
56
|
api.Handle("sharedfolder.status", cfg.sharedFolderStatus),
|
|
62
|
-
api.Handle("greeting.get", cfg.getGreeting),
|
|
63
|
-
api.Handle("greeting.set", cfg.setGreeting),
|
|
64
57
|
}...)
|
|
65
58
|
routes = append(routes, cfg.SessionFiles.Routes()...)
|
|
66
59
|
routes = append(routes, cfg.backgroundRoutes()...)
|
|
@@ -81,43 +74,6 @@ func (cfg Config) status(_ context.Context, caller api.Caller, _ json.RawMessage
|
|
|
81
74
|
}, nil
|
|
82
75
|
}
|
|
83
76
|
|
|
84
|
-
type greeting struct {
|
|
85
|
-
Text string `json:"text"`
|
|
86
|
-
UpdatedBy string `json:"updated_by,omitempty"`
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
func (cfg Config) greetingPath() string { return filepath.Join(cfg.DataDir, "greeting.json") }
|
|
90
|
-
|
|
91
|
-
func (cfg Config) getGreeting(context.Context, api.Caller, json.RawMessage) (any, error) {
|
|
92
|
-
data, err := os.ReadFile(cfg.greetingPath())
|
|
93
|
-
if errors.Is(err, fs.ErrNotExist) {
|
|
94
|
-
return greeting{Text: "Hello"}, nil
|
|
95
|
-
}
|
|
96
|
-
if err != nil {
|
|
97
|
-
return nil, err
|
|
98
|
-
}
|
|
99
|
-
var g greeting
|
|
100
|
-
return g, json.Unmarshal(data, &g)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
func (cfg Config) setGreeting(_ context.Context, caller api.Caller, params json.RawMessage) (any, error) {
|
|
104
|
-
var in struct {
|
|
105
|
-
Text string `json:"text"`
|
|
106
|
-
}
|
|
107
|
-
if err := decodeParams(params, &in); err != nil {
|
|
108
|
-
return nil, err
|
|
109
|
-
}
|
|
110
|
-
if n := utf8.RuneCountInString(in.Text); n < 1 || n > 200 {
|
|
111
|
-
return nil, api.BadRequest("text must be 1 to 200 characters")
|
|
112
|
-
}
|
|
113
|
-
g := greeting{Text: in.Text, UpdatedBy: caller.User()}
|
|
114
|
-
data, err := json.Marshal(g)
|
|
115
|
-
if err != nil {
|
|
116
|
-
return nil, err
|
|
117
|
-
}
|
|
118
|
-
return g, writeFileAtomic(cfg.greetingPath(), data)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
77
|
// decodeParams decodes params strictly: unknown fields are an error.
|
|
122
78
|
func decodeParams(params json.RawMessage, into any) error {
|
|
123
79
|
if len(params) == 0 {
|
|
@@ -109,8 +109,6 @@ var sampleOutcome = map[string]string{}
|
|
|
109
109
|
var sampleParams = map[string]string{
|
|
110
110
|
"app.status": `{}`,
|
|
111
111
|
"sharedfolder.status": `{}`,
|
|
112
|
-
"greeting.get": `{}`,
|
|
113
|
-
"greeting.set": `{"text":"Hi"}`,
|
|
114
112
|
}
|
|
115
113
|
|
|
116
114
|
func TestEveryRouteHasSampleParamsAndNoneIsPublic(t *testing.T) {
|
|
@@ -174,30 +172,11 @@ func TestStatusReportsPackageAndCaller(t *testing.T) {
|
|
|
174
172
|
}
|
|
175
173
|
}
|
|
176
174
|
|
|
177
|
-
func
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
t.Fatalf("default greeting = %v", got)
|
|
182
|
-
}
|
|
183
|
-
if code, out := call(t, cfg, alice, "greeting.set", `{"text":"Hi from DSM"}`); code != "" {
|
|
184
|
-
t.Fatalf("set: %q %v", code, out)
|
|
185
|
-
}
|
|
186
|
-
_, out = call(t, cfg, alice, "greeting.get", `{}`)
|
|
187
|
-
data := out["data"].(map[string]any)
|
|
188
|
-
if data["text"] != "Hi from DSM" || data["updated_by"] != "alice" {
|
|
189
|
-
t.Fatalf("after set: %v", data)
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
func TestGreetingSetValidatesText(t *testing.T) {
|
|
194
|
-
for _, params := range []string{`{}`, `{"text":""}`, `{"text":"` + strings.Repeat("x", 201) + `"}`, `{"text":7}`, `{"text":"ok","extra":1}`} {
|
|
195
|
-
cfg := testConfig(t)
|
|
196
|
-
code, _ := call(t, cfg, alice, "greeting.set", params)
|
|
197
|
-
if code != "bad_request" {
|
|
198
|
-
t.Errorf("%s: %q, want bad_request", params, code)
|
|
175
|
+
func TestSampleOperationsAreAbsent(t *testing.T) {
|
|
176
|
+
for _, method := range []string{"greeting.get", "greeting.set"} {
|
|
177
|
+
if code, _ := call(t, testConfig(t), alice, method, `{}`); code != "unknown_method" {
|
|
178
|
+
t.Fatalf("sample route %s remains: %s", method, code)
|
|
199
179
|
}
|
|
200
|
-
assertDataDirEmpty(t, cfg)
|
|
201
180
|
}
|
|
202
181
|
}
|
|
203
182
|
|
|
@@ -103,33 +103,7 @@ capabilities: {}
|
|
|
103
103
|
# items:
|
|
104
104
|
# - key: jobs # i18n key uninstall.state.jobs
|
|
105
105
|
|
|
106
|
-
#
|
|
106
|
+
# Operation metadata has one source. Generate/check before building.
|
|
107
107
|
ctl:
|
|
108
108
|
enabled: true
|
|
109
|
-
|
|
110
|
-
status.show:
|
|
111
|
-
cli: [status]
|
|
112
|
-
summary: Show this App's status and verified caller
|
|
113
|
-
effect: read
|
|
114
|
-
flow: {type: invoke, method: app.status}
|
|
115
|
-
paramsSchema: {type: object, additionalProperties: false}
|
|
116
|
-
examples: [{}]
|
|
117
|
-
greeting.get:
|
|
118
|
-
cli: [greeting, get]
|
|
119
|
-
summary: Read the greeting
|
|
120
|
-
effect: read
|
|
121
|
-
flow: {type: invoke, method: greeting.get}
|
|
122
|
-
paramsSchema: {type: object, additionalProperties: false}
|
|
123
|
-
examples: [{}]
|
|
124
|
-
greeting.set:
|
|
125
|
-
cli: [greeting, set]
|
|
126
|
-
summary: Set the greeting
|
|
127
|
-
effect: write
|
|
128
|
-
flow: {type: invoke, method: greeting.set}
|
|
129
|
-
paramsSchema:
|
|
130
|
-
type: object
|
|
131
|
-
required: [text]
|
|
132
|
-
additionalProperties: false
|
|
133
|
-
properties:
|
|
134
|
-
text: {type: string, minLength: 1, maxLength: 200, description: Greeting text, examples: [Hello]}
|
|
135
|
-
examples: [{text: Hello}]
|
|
109
|
+
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
|
+
}
|
|
@@ -36,23 +36,13 @@
|
|
|
36
36
|
function mount(root, ctx) {
|
|
37
37
|
var t = ctx.t;
|
|
38
38
|
var status = el("dl", { class: "dsmapp-facts" });
|
|
39
|
-
var greeting = el("p", { class: "dsmapp-greeting", text: t("app.loading") });
|
|
40
|
-
var setBy = el("p", { class: "dsmapp-note", hidden: "" });
|
|
41
|
-
var input = el("input", { type: "text", maxlength: "200", "aria-label": t("greeting.label"), placeholder: t("greeting.label") });
|
|
42
|
-
var save = el("button", { type: "submit", text: t("greeting.save") });
|
|
43
39
|
var refreshButton = el("button", { type: "button", class: "dsmapp-secondary", text: t("app.refresh") });
|
|
44
40
|
var message = el("p", { class: "dsmapp-message", role: "status" });
|
|
45
|
-
var form = el("form", { class: "dsmapp-form", hidden: "" }, [input, save]);
|
|
46
|
-
var adminOnly = el("p", { class: "dsmapp-note", hidden: "", text: t("greeting.adminOnly") });
|
|
47
41
|
var portal = ctx.portal ? el("section", { class: "dsmapp-portal" }) : null;
|
|
48
42
|
|
|
49
43
|
root.classList.add("dsmapp");
|
|
50
44
|
root.replaceChildren(
|
|
51
45
|
el("div", { class: "dsmapp-header" }, [el("h1", { text: t("app.title") }), refreshButton]),
|
|
52
|
-
greeting,
|
|
53
|
-
setBy,
|
|
54
|
-
form,
|
|
55
|
-
adminOnly,
|
|
56
46
|
message,
|
|
57
47
|
el("h2", { text: t("status.heading") }),
|
|
58
48
|
status
|
|
@@ -80,24 +70,9 @@
|
|
|
80
70
|
status.appendChild(el("dt", { text: row[0] }));
|
|
81
71
|
status.appendChild(el("dd", { text: row[1] }));
|
|
82
72
|
});
|
|
83
|
-
form.hidden = !data.admin;
|
|
84
|
-
adminOnly.hidden = data.admin;
|
|
85
|
-
}).catch(showError);
|
|
86
|
-
ctx.call("greeting.get").then(function (data) {
|
|
87
|
-
greeting.textContent = data.text;
|
|
88
|
-
setBy.textContent = data.updated_by ? t("greeting.setBy", { user: data.updated_by }) : "";
|
|
89
|
-
setBy.hidden = !data.updated_by;
|
|
90
73
|
}).catch(showError);
|
|
91
74
|
}
|
|
92
75
|
|
|
93
|
-
form.addEventListener("submit", function (event) {
|
|
94
|
-
event.preventDefault();
|
|
95
|
-
save.disabled = true;
|
|
96
|
-
ctx.call("greeting.set", { text: input.value })
|
|
97
|
-
.then(function () { input.value = ""; refresh(); })
|
|
98
|
-
.catch(showError)
|
|
99
|
-
.then(function () { save.disabled = false; });
|
|
100
|
-
});
|
|
101
76
|
refreshButton.addEventListener("click", refresh);
|
|
102
77
|
|
|
103
78
|
ctx.reopen(refresh);
|