@synopackageland/templates 0.9.8 → 0.10.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 (133) hide show
  1. package/README.md +27 -27
  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/glossary.json +3 -1
  10. package/templates/native-go/i18n/messages/de.json +6 -0
  11. package/templates/native-go/i18n/messages/en.json +6 -0
  12. package/templates/native-go/i18n/messages/es.json +6 -0
  13. package/templates/native-go/i18n/messages/fr.json +6 -0
  14. package/templates/native-go/i18n/messages/it.json +6 -0
  15. package/templates/native-go/i18n/messages/ja.json +6 -0
  16. package/templates/native-go/i18n/messages/ko.json +6 -0
  17. package/templates/native-go/i18n/messages/zh-CN.json +6 -0
  18. package/templates/native-go/i18n/messages/zh-TW.json +6 -0
  19. package/templates/native-go/ui/app.js +2 -1
  20. package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +3 -2
  21. package/templates/native-go-jobs/frontend/test/background-page.test.ts +4 -4
  22. package/templates/native-go-vue/README.md +19 -7
  23. package/templates/native-go-vue/frontend/package-lock.json +9 -9
  24. package/templates/native-go-vue/frontend/package.json +2 -2
  25. package/templates/native-go-vue/frontend/src/App.vue +31 -36
  26. package/templates/native-go-vue/frontend/test/apps.test.ts +65 -12
  27. package/templates/native-go-vue/frontend/test/context.ts +8 -0
  28. package/templates/native-go-vue/frontend/test/native-controls.test.ts +3 -4
  29. package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +2 -2
  30. package/templates/native-node/README.md +153 -0
  31. package/templates/native-node/backend/app/package.json +4 -0
  32. package/templates/native-node/backend/app/permissions.json +5 -0
  33. package/templates/native-node/backend/package-lock.json +59 -0
  34. package/templates/native-node/backend/package.json +17 -0
  35. package/templates/native-node/backend/src/delegate.ts +5 -0
  36. package/templates/native-node/backend/src/jobs.ts +5 -0
  37. package/templates/native-node/backend/src/main.ts +4 -0
  38. package/templates/native-node/backend/src/params.ts +11 -0
  39. package/templates/native-node/backend/src/root-ops.ts +5 -0
  40. package/templates/native-node/backend/src/routes.ts +97 -0
  41. package/templates/native-node/backend/src/service.ts +22 -0
  42. package/templates/native-node/backend/test/routes.test.ts +75 -0
  43. package/templates/native-node/backend/test/support.ts +67 -0
  44. package/templates/native-node/backend/tsconfig.build.json +8 -0
  45. package/templates/native-node/backend/tsconfig.json +18 -0
  46. package/templates/native-node/dsm-package.yaml +80 -0
  47. package/templates/native-node/gitignore +4 -0
  48. package/templates/native-node-delegate/backend/src/delegate.ts +35 -0
  49. package/templates/native-node-delegate/backend/test/delegate.test.ts +51 -0
  50. package/templates/native-node-jobs/backend/src/jobs.ts +38 -0
  51. package/templates/native-node-jobs/backend/test/jobs.test.ts +73 -0
  52. package/templates/native-node-root-ops/backend/src/root-ops.ts +16 -0
  53. package/templates/native-node-root-ops/backend/test/root-ops.test.ts +23 -0
  54. package/templates/native-node-vue/README.md +56 -0
  55. package/templates/native-node-vue/gitignore +4 -0
  56. package/templates/native-python/README.md +150 -0
  57. package/templates/native-python/backend/app/delegate.py +5 -0
  58. package/templates/native-python/backend/app/jobs.py +5 -0
  59. package/templates/native-python/backend/app/main.py +5 -0
  60. package/templates/native-python/backend/app/params.py +10 -0
  61. package/templates/native-python/backend/app/permissions.json +5 -0
  62. package/templates/native-python/backend/app/root_ops.py +5 -0
  63. package/templates/native-python/backend/app/routes.py +107 -0
  64. package/templates/native-python/backend/tests/support.py +72 -0
  65. package/templates/native-python/backend/tests/test_routes.py +73 -0
  66. package/templates/native-python/dsm-package.yaml +77 -0
  67. package/templates/native-python/gitignore +2 -0
  68. package/templates/native-python-delegate/backend/app/delegate.py +42 -0
  69. package/templates/native-python-delegate/backend/tests/test_delegate.py +58 -0
  70. package/templates/native-python-jobs/backend/app/jobs.py +40 -0
  71. package/templates/native-python-jobs/backend/tests/test_jobs.py +72 -0
  72. package/templates/native-python-root-ops/backend/app/root_ops.py +18 -0
  73. package/templates/native-python-root-ops/backend/tests/test_root_ops.py +34 -0
  74. package/templates/native-python-vue/README.md +56 -0
  75. package/templates/native-python-vue/gitignore +3 -0
  76. package/templates/native-rust/README.md +74 -296
  77. package/templates/native-rust/backend/Cargo.lock +6 -6
  78. package/templates/native-rust/backend/Cargo.toml +2 -10
  79. package/templates/native-rust/backend/src/delegate.rs +6 -0
  80. package/templates/native-rust/backend/src/jobs.rs +6 -0
  81. package/templates/native-rust/backend/src/main.rs +12 -0
  82. package/templates/native-rust/backend/src/root_ops.rs +6 -0
  83. package/templates/native-rust/backend/src/routes.rs +352 -0
  84. package/templates/native-rust/backend/src/test_support.rs +91 -0
  85. package/templates/native-rust/dsm-package.yaml +10 -21
  86. package/templates/native-rust-delegate/backend/src/delegate.rs +171 -0
  87. package/templates/native-rust-jobs/backend/src/jobs.rs +240 -0
  88. package/templates/native-rust-root-ops/backend/src/root_ops.rs +83 -0
  89. package/templates/native-rust-vue/README.md +35 -34
  90. package/templates/native-rust/backend/THIRD_PARTY_NOTICES.txt +0 -207
  91. package/templates/native-rust/backend/src/app/background/mod.rs +0 -424
  92. package/templates/native-rust/backend/src/app/background/owner.rs +0 -77
  93. package/templates/native-rust/backend/src/app/background/owner_tests.rs +0 -174
  94. package/templates/native-rust/backend/src/app/background/schedule.rs +0 -126
  95. package/templates/native-rust/backend/src/app/background/schedule_tests.rs +0 -108
  96. package/templates/native-rust/backend/src/app/background/tests.rs +0 -505
  97. package/templates/native-rust/backend/src/app/background/work.rs +0 -78
  98. package/templates/native-rust/backend/src/app/background/work_tests.rs +0 -123
  99. package/templates/native-rust/backend/src/app/background_routes.rs +0 -22
  100. package/templates/native-rust/backend/src/app/delegate_config.rs +0 -188
  101. package/templates/native-rust/backend/src/app/delegate_routes.rs +0 -22
  102. package/templates/native-rust/backend/src/app/identity.rs +0 -56
  103. package/templates/native-rust/backend/src/app/migrations.rs +0 -79
  104. package/templates/native-rust/backend/src/app/mod.rs +0 -30
  105. package/templates/native-rust/backend/src/app/portal.rs +0 -317
  106. package/templates/native-rust/backend/src/app/root_routes.rs +0 -23
  107. package/templates/native-rust/backend/src/app/routes.rs +0 -315
  108. package/templates/native-rust/backend/src/app/session_files.rs +0 -19
  109. package/templates/native-rust/backend/src/app/shared_folder.rs +0 -383
  110. package/templates/native-rust/backend/src/app/test_support.rs +0 -202
  111. package/templates/native-rust/backend/src/app/uninstall.rs +0 -38
  112. package/templates/native-rust/backend/src/bin/api.rs +0 -72
  113. package/templates/native-rust/backend/src/bin/background-worker.rs +0 -37
  114. package/templates/native-rust/backend/src/build_env.rs +0 -155
  115. package/templates/native-rust/backend/src/lib.rs +0 -6
  116. package/templates/native-rust/backend/src/permissions.rs +0 -54
  117. package/templates/native-rust/backend/src/process.rs +0 -91
  118. package/templates/native-rust/i18n/glossary.json +0 -663
  119. package/templates/native-rust/i18n/messages/de.json +0 -30
  120. package/templates/native-rust/i18n/messages/en.json +0 -30
  121. package/templates/native-rust/i18n/messages/es.json +0 -30
  122. package/templates/native-rust/i18n/messages/fr.json +0 -30
  123. package/templates/native-rust/i18n/messages/it.json +0 -30
  124. package/templates/native-rust/i18n/messages/ja.json +0 -30
  125. package/templates/native-rust/i18n/messages/ko.json +0 -30
  126. package/templates/native-rust/i18n/messages/zh-CN.json +0 -30
  127. package/templates/native-rust/i18n/messages/zh-TW.json +0 -30
  128. package/templates/native-rust/ui/app.css +0 -45
  129. package/templates/native-rust/ui/app.js +0 -111
  130. package/templates/native-rust-delegate/backend/src/app/delegate_routes.rs +0 -92
  131. package/templates/native-rust-delegate/backend/src/bin/background-worker.rs +0 -52
  132. package/templates/native-rust-jobs/backend/src/app/background_routes.rs +0 -109
  133. package/templates/native-rust-root-ops/backend/src/app/root_routes.rs +0 -475
@@ -0,0 +1,58 @@
1
+ import os
2
+ import stat
3
+ import tempfile
4
+ import unittest
5
+
6
+ from support import Error, call
7
+
8
+ import routes
9
+
10
+ SAVED = {"state": "queued"}
11
+
12
+
13
+ class DelegateTest(unittest.TestCase):
14
+ def setUp(self):
15
+ self.dir = tempfile.TemporaryDirectory()
16
+ self.addCleanup(self.dir.cleanup)
17
+ self.staging = os.path.join(self.dir.name, "executor-staging")
18
+ self.app = routes.build(routes.Config("ExampleApp", self.dir.name))
19
+
20
+ def save(self, job_id="job-1", user="alice", move=lambda p: SAVED):
21
+ params = {"delegation_id": "d1", "job_id": job_id}
22
+ return call(self.app, "report.save", params, user=user, admin=False, services={"delegate.move_from_staging": move})
23
+
24
+ def test_save_moves_a_private_report_to_the_callers_destination(self):
25
+ reply = self.save()
26
+ self.assertEqual(reply.data, SAVED)
27
+ path = os.path.join(self.staging, "report-job-1.txt")
28
+ with open(path, encoding="utf-8") as f:
29
+ self.assertIn("alice", f.read())
30
+ self.assertEqual(stat.S_IMODE(os.stat(path).st_mode), 0o600)
31
+ move = {"delegation_id": "d1", "job_id": "job-1", "staging": "report-job-1.txt", "name": "report-job-1.txt", "size": os.path.getsize(path)}
32
+ self.assertEqual(reply.services, [("delegate.move_from_staging", move)])
33
+
34
+ def test_a_repeated_save_sends_the_same_file_again(self):
35
+ first = self.save().services
36
+ path = os.path.join(self.staging, "report-job-1.txt")
37
+ with open(path, encoding="utf-8") as f:
38
+ report = f.read()
39
+ self.assertEqual(self.save(user="bob").services, first)
40
+ with open(path, encoding="utf-8") as f:
41
+ self.assertEqual(f.read(), report)
42
+
43
+ def test_save_passes_on_the_executors_refusal(self):
44
+ def denied(params):
45
+ raise Error("DELEGATION_DENIED", "The destination is no longer allowed.")
46
+
47
+ self.assertEqual(self.save(move=denied).error["code"], "DELEGATION_DENIED")
48
+
49
+ def test_save_refuses_invalid_params_before_writing(self):
50
+ for params in [None, {"delegation_id": "d1"}, {"delegation_id": "d1", "job_id": "../x"}, {"delegation_id": "d1", "job_id": ""}, {"delegation_id": 1, "job_id": "j"}]:
51
+ with self.subTest(params=params):
52
+ reply = call(self.app, "report.save", params)
53
+ self.assertEqual((reply.error["code"], reply.services), ("bad_request", []))
54
+ self.assertFalse(os.path.exists(self.staging))
55
+
56
+
57
+ if __name__ == "__main__":
58
+ unittest.main()
@@ -0,0 +1,40 @@
1
+ """Background jobs: hash a NAS file the caller may read.
2
+
3
+ The SDK's worker runs each job as the package account: it hashes the job's
4
+ input (SHA-256) and keeps the result for the job's owner, the caller. The
5
+ runtime serves files.read with the caller's DSM session and bg.* for the
6
+ caller's own jobs only.
7
+ """
8
+
9
+ import os
10
+ import shutil
11
+ import tempfile
12
+
13
+ from synopkgland_app import Error
14
+
15
+ from params import fields
16
+
17
+ # A job's input is at most 512 KiB, copied into the job when it is submitted.
18
+ MAX_INPUT = 512 * 1024
19
+
20
+
21
+ def register(app, cfg):
22
+ app.method("hash.submit")(lambda ctx, params: submit(cfg, ctx, params))
23
+ app.method("hash.get")(lambda ctx, params: ctx.runtime("bg.get", fields(params, id=str)))
24
+ app.method("hash.cancel")(lambda ctx, params: ctx.runtime("bg.cancel", fields(params, id=str)))
25
+
26
+
27
+ def submit(cfg, ctx, params):
28
+ p = fields(params, key=str, path=str)
29
+ read = ctx.runtime("files.read", {"path": p["path"]})
30
+ if read["size"] > MAX_INPUT:
31
+ raise Error("limit_exceeded", "The file is larger than allowed.")
32
+ # bg.submit takes its input from the data directory; the call's spool is elsewhere.
33
+ inputs = os.path.join(cfg.data_dir, "hash-inputs")
34
+ os.makedirs(inputs, mode=0o700, exist_ok=True)
35
+ with open(read["path"], "rb") as source, tempfile.NamedTemporaryFile(dir=inputs, delete=False) as copy:
36
+ shutil.copyfileobj(source, copy)
37
+ try:
38
+ return ctx.runtime("bg.submit", {"key": p["key"], "name": os.path.basename(p["path"]), "input": copy.name})
39
+ finally:
40
+ os.unlink(copy.name)
@@ -0,0 +1,72 @@
1
+ import os
2
+ import tempfile
3
+ import unittest
4
+
5
+ from support import Error, call
6
+
7
+ import routes
8
+
9
+ JOB = {"job": {"id": "j1", "state": "queued"}, "duplicate": False}
10
+
11
+
12
+ class JobsTest(unittest.TestCase):
13
+ def setUp(self):
14
+ self.dir = tempfile.TemporaryDirectory()
15
+ self.addCleanup(self.dir.cleanup)
16
+ self.data = os.path.join(self.dir.name, "var")
17
+ os.mkdir(self.data)
18
+ self.app = routes.build(routes.Config("ExampleApp", self.data))
19
+ self.submitted = []
20
+
21
+ def spool(self, content):
22
+ path = os.path.join(self.dir.name, "spool")
23
+ with open(path, "wb") as f:
24
+ f.write(content)
25
+ return {"path": path, "size": len(content)}
26
+
27
+ def submit(self, params):
28
+ # The runtime reads the input file while it answers bg.submit.
29
+ with open(params["input"], "rb") as f:
30
+ self.submitted.append(dict(params, content=f.read()))
31
+ return JOB
32
+
33
+ def test_submit_hashes_a_copy_of_the_callers_file(self):
34
+ services = {"files.read": lambda p: self.spool(b"hello"), "bg.submit": self.submit}
35
+ reply = call(self.app, "hash.submit", {"key": "k1", "path": "/home/report.txt"}, services=services)
36
+ self.assertEqual(reply.data, JOB)
37
+ self.assertEqual([op for op, _ in reply.services], ["files.read", "bg.submit"])
38
+ self.assertEqual(reply.services[0][1], {"path": "/home/report.txt"})
39
+ [submitted] = self.submitted
40
+ self.assertEqual((submitted["key"], submitted["name"], submitted["content"]), ("k1", "report.txt", b"hello"))
41
+ self.assertTrue(submitted["input"].startswith(self.data + os.sep))
42
+ self.assertFalse(os.path.exists(submitted["input"]), "the copy is removed once submitted")
43
+
44
+ def test_submit_refuses_a_file_over_the_job_input_limit(self):
45
+ services = {"files.read": lambda p: self.spool(b"x" * (512 * 1024 + 1)), "bg.submit": self.submit}
46
+ reply = call(self.app, "hash.submit", {"key": "k1", "path": "/home/big.bin"}, services=services)
47
+ self.assertEqual(reply.error["code"], "limit_exceeded")
48
+ self.assertEqual(self.submitted, [])
49
+
50
+ def test_submit_passes_on_the_runtimes_refusal(self):
51
+ def unreadable(params):
52
+ raise Error("read_failed", "Unable to read the file.")
53
+
54
+ reply = call(self.app, "hash.submit", {"key": "k1", "path": "/home/a"}, services={"files.read": unreadable})
55
+ self.assertEqual(reply.error["code"], "read_failed")
56
+
57
+ def test_submit_refuses_invalid_params_before_any_service(self):
58
+ for params in [None, {"key": "k1"}, {"key": 1, "path": "/home/a"}, {"key": "k1", "path": "/home/a", "x": 1}]:
59
+ with self.subTest(params=params):
60
+ reply = call(self.app, "hash.submit", params)
61
+ self.assertEqual((reply.error["code"], reply.services), ("bad_request", []))
62
+
63
+ def test_get_and_cancel_act_on_the_callers_job(self):
64
+ for method, op in [("hash.get", "bg.get"), ("hash.cancel", "bg.cancel")]:
65
+ with self.subTest(method=method):
66
+ reply = call(self.app, method, {"id": "j1"}, services={op: lambda p: JOB["job"]})
67
+ self.assertEqual((reply.data, reply.services), (JOB["job"], [(op, {"id": "j1"})]))
68
+ self.assertEqual(call(self.app, method, {}).error["code"], "bad_request")
69
+
70
+
71
+ if __name__ == "__main__":
72
+ unittest.main()
@@ -0,0 +1,18 @@
1
+ """Root operations: run root/whoami.sh as root.
2
+
3
+ The executor runs only the root programs an administrator approved, and
4
+ checks the caller's DSM session and Application Privileges itself; the App
5
+ names the program and never runs anything as root.
6
+ """
7
+
8
+ from synopkgland_app import Error
9
+
10
+
11
+ def register(app, cfg):
12
+ app.method("whoami.run")(lambda ctx, params: whoami(ctx, params))
13
+
14
+
15
+ def whoami(ctx, params):
16
+ if params not in (None, {}):
17
+ raise Error.bad_request("whoami.run takes no params")
18
+ return ctx.runtime("root.run", {"name": "whoami"})
@@ -0,0 +1,34 @@
1
+ import tempfile
2
+ import unittest
3
+
4
+ from support import Error, call
5
+
6
+ import routes
7
+
8
+
9
+ class RootOpsTest(unittest.TestCase):
10
+ def setUp(self):
11
+ self.dir = tempfile.TemporaryDirectory()
12
+ self.addCleanup(self.dir.cleanup)
13
+ self.app = routes.build(routes.Config("ExampleApp", self.dir.name))
14
+
15
+ def test_whoami_runs_the_approved_root_program(self):
16
+ output = {"exit_code": 0, "stdout": "0\n", "stderr": ""}
17
+ for params in [None, {}]:
18
+ with self.subTest(params=params):
19
+ reply = call(self.app, "whoami.run", params, services={"root.run": lambda p: output})
20
+ self.assertEqual((reply.data, reply.services), (output, [("root.run", {"name": "whoami"})]))
21
+
22
+ def test_whoami_passes_on_the_executors_refusal(self):
23
+ def paused(params):
24
+ raise Error("EXECUTOR_PAUSED", "The executor is paused.")
25
+
26
+ reply = call(self.app, "whoami.run", {}, services={"root.run": paused})
27
+ self.assertEqual(reply.error["code"], "EXECUTOR_PAUSED")
28
+
29
+ def test_whoami_takes_no_params(self):
30
+ self.assertEqual(call(self.app, "whoami.run", {"name": "other"}).error["code"], "bad_request")
31
+
32
+
33
+ if __name__ == "__main__":
34
+ unittest.main()
@@ -0,0 +1,56 @@
1
+ # Native Python with Vue
2
+
3
+ The Vue shell is the same as the Go template's. Edit `frontend/src/App.vue`
4
+ and `main.ts` to build a DSM-window app with the public `@synopackageland/ui`
5
+ components and embedded theme. The shell, sign-in, files, portal, jobs and
6
+ delegate components come from `@synopackageland/sdk/vue` (types from
7
+ `@synopackageland/sdk`); its version must equal the `backend.sdk` in dsm-package.yaml. See
8
+ <https://github.com/synopackageland/syno-pkg-land-sdk> for the components and their props.
9
+
10
+ ```sh
11
+ cd frontend
12
+ npm ci
13
+ npm test
14
+ npm run build
15
+ ```
16
+
17
+ The base app reads package status and edits the package greeting through
18
+ `ctx.call`. Errors show the app's own `frontend/src/i18n` text, or the SDK's
19
+ `bridgeErrorCopy` for the codes the bridge runtime adds (`app_unavailable`
20
+ and the services' refusals). Each window has its own Vue mount, request state
21
+ and host subscriptions; unmount disposes subscriptions and ignores late responses.
22
+
23
+ The `--with jobs`, `root-ops` and `delegate` examples add their methods to
24
+ the Python App; the shell's panels call them with `ctx.call`. The SDK's
25
+ background-jobs page and executor settings call the runtime's own browser
26
+ methods (`bg.*`, `executor.*`); check the SDK release notes for the ones a
27
+ bridge App's runtime serves.
28
+
29
+ Use the installed `@synopackageland/ui`; package.json pins its version.
30
+ `App.vue` lays the shell out with the SDK's `AppShell` (`@synopackageland/sdk/vue`):
31
+ `pages` from `shellPages`, the header buttons in `#actions` and the pages in its
32
+ default slot. Given `ctx`, it follows the launch. In a DSM desktop window it
33
+ keeps `wide-layout="window"`, even maximized; the DSM host sets the 700px
34
+ minimum window width. A Portal or standalone launch owns the whole page
35
+ (`ctx.fullPage`, from the generated window): `AppShell` takes
36
+ `wide-layout="web"` (the SaaS layout at 1200px or wider, mobile on a phone) and
37
+ a device-width viewport, undone at unmount, since DSM's page has no
38
+ device-width viewport. Its full-page sidebar names the App with
39
+ `ctx.displayName` and `ctx.appIcon` (without them the page title and icon) and
40
+ shows the signed-in DSM account and its role from `ctx.user`. See the ui's
41
+ `docs/adaptive-layout.md`.
42
+ `AppShell`'s `app-root` wraps it in an app-local `AppRoot` (app modal scope, on
43
+ a full page as well, bounded at `height: 100%`) with the UI locale from
44
+ `locale`; do not add another `AppRoot`.
45
+ In a DSM window (not a full page) that `AppRoot` takes
46
+ `dialog-bounds="viewport"`, so Dialogs can be dragged out of the window, and
47
+ pressing one calls `ctx.host.activateWindow()` to bring the window to the
48
+ front. Elsewhere Dialogs stay inside the App. A confirmed sign-in need sets
49
+ `denied`: the `#denied` slot's `LoginPage` takes the place of the App's pages.
50
+ Import `themes/dsm7-embedded.css` and assign each mount a unique
51
+ `app.config.idPrefix` before mounting.
52
+
53
+ Start with AGENTS.md, `synopkgland skill list` and `synopkgland skill path`.
54
+ Read `synopkgland skill show ui-vue` for direct Vue component work. For UI
55
+ discovery, run `npx --no syno-pkg-land-ui catalog --json` and
56
+ `npx --no syno-pkg-land-ui describe Button --json` from the installed frontend.
@@ -0,0 +1,3 @@
1
+ dist/
2
+ __pycache__/
3
+ node_modules/