@synopackageland/templates 0.26.4 → 0.27.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 (38) hide show
  1. package/README.md +24 -6
  2. package/package.json +1 -1
  3. package/templates/_shared/AGENTS.md +5 -2
  4. package/templates/compose/dsm-package.yaml +1 -1
  5. package/templates/native-go/README.md +4 -0
  6. package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
  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/backend/internal/app/background.go +9 -2
  10. package/templates/native-go/backend/internal/app/background_test.go +5 -2
  11. package/templates/native-go-delegate/backend/internal/app/delegate.go +0 -6
  12. package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +14 -2
  13. package/templates/native-go-jobs/backend/internal/app/background_routes_test.go +17 -3
  14. package/templates/native-go-jobs/frontend/test/background-page.test.ts +5 -43
  15. package/templates/native-go-vue/frontend/package-lock.json +8 -8
  16. package/templates/native-go-vue/frontend/package.json +2 -2
  17. package/templates/native-go-vue/frontend/src/App.vue +3 -4
  18. package/templates/native-go-vue/frontend/src/i18n/de.json +2 -1
  19. package/templates/native-go-vue/frontend/src/i18n/en.json +2 -1
  20. package/templates/native-go-vue/frontend/src/i18n/es.json +2 -1
  21. package/templates/native-go-vue/frontend/src/i18n/fr.json +2 -1
  22. package/templates/native-go-vue/frontend/src/i18n/it.json +2 -1
  23. package/templates/native-go-vue/frontend/src/i18n/ja.json +2 -1
  24. package/templates/native-go-vue/frontend/src/i18n/ko.json +2 -1
  25. package/templates/native-go-vue/frontend/src/i18n/zh-CN.json +2 -1
  26. package/templates/native-go-vue/frontend/src/i18n/zh-TW.json +2 -1
  27. package/templates/native-node/README.md +5 -1
  28. package/templates/native-node/dsm-package.yaml +1 -1
  29. package/templates/native-node-jobs/backend/src/jobs.ts +5 -26
  30. package/templates/native-node-jobs/backend/test/jobs.test.ts +7 -41
  31. package/templates/native-python/README.md +5 -1
  32. package/templates/native-python/dsm-package.yaml +1 -1
  33. package/templates/native-python-jobs/backend/app/jobs.py +4 -27
  34. package/templates/native-python-jobs/backend/tests/test_jobs.py +5 -37
  35. package/templates/native-rust/README.md +5 -1
  36. package/templates/native-rust/backend/Cargo.lock +2 -2
  37. package/templates/native-rust/backend/Cargo.toml +1 -1
  38. package/templates/native-rust-jobs/backend/src/jobs.rs +14 -141
package/README.md CHANGED
@@ -22,8 +22,8 @@ daemon:
22
22
  window_seconds: 300
23
23
  ```
24
24
 
25
- Templates 0.26.4 pin SDK 0.26.0, including the default controller SDK for
26
- binary/Node daemons. Use CLI 0.26.0 or later. Go Apps select
25
+ Templates 0.27.0 inherit UI 0.14.9 from 0.26.5 and pin SDK 0.27.0 in native and Compose projects.
26
+ Use CLI 0.27.0 or later. Go Apps select
27
27
  their controller from their Go SDK requirement; an identity gateway selects it
28
28
  from `identity_gateway.sdk`. The CLI rejects older selected versions, even for
29
29
  an explicit `mode: off`, rather than dropping the declaration. A local SDK source
@@ -39,7 +39,7 @@ for declaration bounds, SDK selection, build/inspect checks and diagnostics.
39
39
 
40
40
  An opt-in [Shared Folder permission example](examples/shared-folder-permission/README.md)
41
41
  shows the Go status-handler registration and a Vue notice for startup checks and
42
- storage failures. It uses the same SDK 0.26.0 release pinned by the default scaffolds.
42
+ storage failures. It uses the same SDK 0.27.0 release pinned by the default scaffolds.
43
43
 
44
44
  An opt-in [native UI plus DSM console example](examples/dual-entry-console/README.md)
45
45
  shows the `console` declaration for an App that wraps a web service behind the
@@ -79,14 +79,22 @@ calls each one from the generated script of the same name (`synopkgland skill sh
79
79
  The placeholders `__PACKAGE__` (also the Go module path), `__DISPLAY_NAME__` and
80
80
  `__BACKGROUND_ENABLED__` are substituted by `init`.
81
81
 
82
- `backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.26.0`; that line is
82
+ `backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.27.0`; that line is
83
83
  the single source of the SDK version and `frontend/package.json` pins
84
84
  `@synopackageland/sdk` to the same version. Python and Node.js name it in
85
85
  `backend.sdk`, Rust in the `syno-pkg-land-app` tag of `backend/Cargo.toml`
86
- (and `Cargo.lock`); all of them pin 0.26.0. The Vue frontend pins
87
- `@synopackageland/ui` 0.14.8. SDK 0.26.0 accepts UI `>=0.10.1 <0.15.0`,
86
+ (and `Cargo.lock`); all of them pin 0.27.0. The Vue frontend pins
87
+ `@synopackageland/ui` 0.14.9. SDK 0.27.0 accepts UI `>=0.10.1 <0.15.0`,
88
88
  so no UI peer override is needed. Tests use jsdom 30 for UI floating-layer styles.
89
89
 
90
+ SDK 0.27.0 moves interactive NAS file operations to same-origin browser calls.
91
+ The unused server-only `bg.browse` and file-import scaffold now return
92
+ `unsupported`; custom file handlers need the SDK's
93
+ [session transport migration](https://github.com/synopackageland/syno-pkg-land-sdk/blob/v0.27.0/docs/dsm-session-transport.md).
94
+ Protected executor scaffolds need a released CLI with SDK 0.27.0 executor/lifecycle
95
+ digests; CLI 0.26.0 does not include them and refuses those builds. Rebuilt protected
96
+ executors follow normal administrator approval. No NAS acceptance is claimed.
97
+
90
98
  UI and i18n text, including the DSM glossary, omit terminal sentence periods.
91
99
  Keep punctuation between sentences and intentional loading ellipses.
92
100
 
@@ -206,3 +214,13 @@ the single-call path. Each batch result is handled separately; the shell stays
206
214
  visible while reads run. The backend permissions remain authoritative.
207
215
 
208
216
  Native Node Apps use the official [`Node.js_v22`](https://www.synology.com/en-us/dsm/packages/Node.js_v22) package. The official release history lists DSM 7 series; installation also depends on the model and DSM version supported by Package Center. This dependency does not certify all DSM 7 releases or install Node.js on unsupported models.
217
+
218
+ ### Temporarily disabled NAS hashing example
219
+
220
+ SDK 0.27.0 no longer relays browser DSM sessions from a backend. This release
221
+ explicitly disables the template NAS file hashing example: Go production
222
+ `bg.browse`/`bg.jobs.submit` and bridge `hash.submit` return `unsupported` before
223
+ reading a file or creating a job. The Vue background example page displays an
224
+ unavailable notice. Existing job list/status/pause/resume/cancel/schedule APIs
225
+ and workers remain available for App-owned input; no SDK API or special option
226
+ is introduced. A safe browser-owned input flow is follow-up work.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/templates",
3
- "version": "0.26.4",
3
+ "version": "0.27.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": {
@@ -110,6 +110,10 @@ may redistribute; never copy Synology's.
110
110
 
111
111
  ## Background jobs (`jobs`)
112
112
 
113
+ The NAS hashing example is temporarily disabled pending browser-owned input
114
+ migration. Do not use backend DSM session replay or `files.read` to restore it.
115
+ Existing workers and job-control APIs remain available for App-owned input.
116
+
113
117
  The worker runs the App's Work for one claimed job at a time, as the package
114
118
  account. The Work receives the job's Task: it reads the private input copy,
115
119
  reports progress (`done`/`total`, `bytes`/`totalBytes`, `item`) and saves a
@@ -265,8 +269,7 @@ sweep forward the cancel instead of admitting it.
265
269
  The SDK runtime is the CGI: it checks the DSM session, the entry gate and
266
270
  permissions.json, then runs the App once per call, as the package account, and
267
271
  answers the browser itself for every refusal. The App only registers methods
268
- and asks the runtime for services with `ctx.runtime(op, params)`: `files.read`
269
- (the caller's file, `files: session`), `bg.submit`/`bg.get`/`bg.cancel` (the
272
+ and asks the runtime for services with `ctx.runtime(op, params)`: `bg.submit`/`bg.get`/`bg.cancel` (the
270
273
  caller's jobs, `jobs: true`, whose worker hashes each job's input), `root.run`
271
274
  (`rootOps`), `delegate.move_from_staging` (`delegate`) and `portal.status`. A
272
275
  service refusal is a code the browser may see; pass it on. The admin guard
@@ -14,7 +14,7 @@ compose:
14
14
  web: {service: web, port: 8080, health: {path: /, status: 200}}
15
15
  network: {egress: isolated}
16
16
  identity_gateway:
17
- sdk: 0.26.0
17
+ sdk: 0.27.0
18
18
  mode: none
19
19
  listen: {http: 28000}
20
20
  # Web Apps default to new-page through the shared app-launch resolver.
@@ -399,3 +399,7 @@ SDK's `docs/shared-folder.md` for the whole flow.
399
399
  | Target CPUs | x86_64 and aarch64 DSM models (one SPK each). armv7 (DSM 7.2+ platforms `alpine`, `alpine4k`, `armada38x`, `monaco`) is opt-in: add it to `arches`; it covers the Go CGI (plain or Vue UI), jobs, daemon, root-ops and delegate. armv7 jobs, daemon and executor (root-ops, delegate) are built and checked locally but have not been verified on a NAS |
400
400
  | DSM | 7.1 or later declared (`os_min_ver`, change it with `init --min-dsm` or `build --min-dsm`). Installed and tested on DSM 7.4 and 7.3.2 (x86_64). armv7 was installed and tested on DSM 7.1.1 (`alpine4k`, Linux 3.10, where the bridge runs from the package tmp fallback) with administrator, AppPriv and unauthenticated requests; `armada38x` and `monaco` have not been run on a NAS. No x86_64 Linux 3.10 model has been tested. aarch64 SPKs are built the same way but have not been run on a NAS. |
401
401
  | Not supported | armv7 platforms only DSM 7.0/7.1 ship (`armada370`, `armada375`, `armadaxp`, `comcerto2k`); DSM 6, running anything as root without an administrator's approval |
402
+
403
+ The NAS file hashing example is temporarily disabled pending browser-owned
404
+ input migration. NAS import submission returns `unsupported` without reading a
405
+ file or creating a job; existing worker and job-control APIs remain available.
@@ -1,6 +1,6 @@
1
1
  Native Go package build dependencies
2
2
 
3
- github.com/synopackageland/syno-pkg-land-sdk v0.26.0 (MIT)
3
+ github.com/synopackageland/syno-pkg-land-sdk v0.27.0 (MIT)
4
4
 
5
5
  Copyright (c) 2026 synopkgland
6
6
 
@@ -4,6 +4,6 @@ go 1.25.0
4
4
 
5
5
  toolchain go1.26.10
6
6
 
7
- require github.com/synopackageland/syno-pkg-land-sdk v0.26.0
7
+ require github.com/synopackageland/syno-pkg-land-sdk v0.27.0
8
8
 
9
9
  require golang.org/x/sys v0.44.0 // indirect
@@ -1,4 +1,4 @@
1
- github.com/synopackageland/syno-pkg-land-sdk v0.26.0 h1:74lMYZURXwF2ffMRhG+T8u1XF8HsR04+NFjweRuKFw8=
2
- github.com/synopackageland/syno-pkg-land-sdk v0.26.0/go.mod h1:8GwREGbczc8mAk4Gv8FUHaukXXCUstjzkovQrNV59Cc=
1
+ github.com/synopackageland/syno-pkg-land-sdk v0.27.0 h1:DyMQUEf6mIzEW2afdfGjWh5FM1R6qM6YW2LFD7/yZeI=
2
+ github.com/synopackageland/syno-pkg-land-sdk v0.27.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=
@@ -42,10 +42,11 @@ type backgroundAPI struct {
42
42
  }
43
43
 
44
44
  // BackgroundRoutes is the production assembly of the opt-in background jobs
45
- // API. `synopkgland init --with jobs` wires it in through background.go's
45
+ // API. The NAS hashing example is disabled until browser-owned input is wired.
46
+ // Existing job control routes remain available. `synopkgland init --with jobs` wires it in through background.go's
46
47
  // replacement of background_none.go; nothing else references it.
47
48
  func BackgroundRoutes(dataDir string) []api.Route {
48
- return newBackgroundRoutes(background.NewRequestStore(filepath.Join(dataDir, "background")), filestation.Local(dataDir))
49
+ return newBackgroundRoutes(background.NewRequestStore(filepath.Join(dataDir, "background")), nil)
49
50
  }
50
51
 
51
52
  // Every request authenticates against DSM afresh. Owner routes are available
@@ -129,6 +130,9 @@ func (b backgroundAPI) browse(ctx context.Context, c api.Caller, r *http.Request
129
130
  if in.Offset < 0 {
130
131
  return nil, api.BadRequest("offset must not be negative")
131
132
  }
133
+ if b.files == nil {
134
+ return nil, &api.Error{Code: "unsupported", Message: "The NAS file hashing example is disabled pending browser input migration. Existing job control APIs remain available."}
135
+ }
132
136
  return b.files.Browse(ctx, c, r, in.Folder, in.Offset)
133
137
  }
134
138
 
@@ -148,6 +152,9 @@ func (b backgroundAPI) submit(ctx context.Context, c api.Caller, r *http.Request
148
152
  if !access.Owner.Valid() {
149
153
  return nil, &api.Error{Code: "identity_unavailable", Message: "DSM user UID could not be resolved"}
150
154
  }
155
+ if b.files == nil {
156
+ return nil, &api.Error{Code: "unsupported", Message: "The NAS file hashing example is disabled pending browser input migration. Existing job control APIs remain available."}
157
+ }
151
158
  data, err := b.files.ReadFile(ctx, c, r, in.Folder, in.Relative)
152
159
  if err != nil {
153
160
  return nil, err
@@ -6,6 +6,7 @@ import (
6
6
  "fmt"
7
7
  "net/http"
8
8
  "net/http/httptest"
9
+ "os"
9
10
  "path/filepath"
10
11
  "strings"
11
12
  "testing"
@@ -136,12 +137,14 @@ func TestBackgroundStatusReportsLimitsAndWorker(t *testing.T) {
136
137
  if limits["maxInputBytes"].(float64) != 512*1024 || limits["maxQueued"].(float64) != 16 || limits["maxTotalBytes"].(float64) != 16<<20 || limits["jobTimeoutSeconds"].(float64) != 60 {
137
138
  t.Fatalf("limits: %v", limits)
138
139
  }
139
- if err := e.store.Heartbeat(context.Background(), background.WorkerInfo{PID: 9, UID: 255853, Username: "pkg"}); err != nil {
140
+ info := background.WorkerInfo{PID: os.Getpid(), UID: os.Getuid(), Username: "pkg"}
141
+ if err := e.store.Heartbeat(context.Background(), info); err != nil {
140
142
  t.Fatal(err)
141
143
  }
144
+ t.Cleanup(func() { e.store.ClearHeartbeat(info) })
142
145
  _, out = e.call(alice, "bg.status", `{}`)
143
146
  w := data(out)["worker"].(map[string]any)
144
- if w["alive"] != true || w["uid"].(float64) != 255853 {
147
+ if w["alive"] != true || w["uid"].(float64) != float64(info.UID) || w["pid"].(float64) != float64(info.PID) {
145
148
  t.Fatalf("worker: %v", w)
146
149
  }
147
150
  }
@@ -4,7 +4,6 @@ import (
4
4
  "github.com/synopackageland/syno-pkg-land-sdk/api"
5
5
  "github.com/synopackageland/syno-pkg-land-sdk/background"
6
6
  "github.com/synopackageland/syno-pkg-land-sdk/delegate"
7
- "github.com/synopackageland/syno-pkg-land-sdk/filestation"
8
7
  "path/filepath"
9
8
  )
10
9
 
@@ -16,10 +15,5 @@ func DelegateRoutes(cfg Config, scope string) []api.Route {
16
15
  if err == nil {
17
16
  service.Destinations, service.Operations, service.Lifetime = settings.Destinations, settings.Operations, settings.Lifetime
18
17
  }
19
- if cfg.SessionFiles != nil {
20
- service.SessionFiles = cfg.SessionFiles
21
- } else {
22
- service.SessionFiles = filestation.Local(cfg.DataDir)
23
- }
24
18
  return service.Routes()
25
19
  }
@@ -38,22 +38,34 @@ test('the capability surface uses an explicit long-running facade without legacy
38
38
  test('the default surface binds the real routes and the session-only picker',async()=>{
39
39
  const grant={id:'g1',key:'save',path:'/home',username:'alice',lifetime:'until-revoked'}
40
40
  let selected=false
41
+ const fetch=vi.fn(async(url:string,init?:RequestInit)=>{
42
+ expect(url).toBe('/webapi/entry.cgi')
43
+ const body=init?.body as URLSearchParams
44
+ expect(body.get('api')).toBe('SYNO.FileStation.List')
45
+ expect(body.get('method')).toBe('list_share')
46
+ expect(body.get('SynoToken')).toBe('fixture-token')
47
+ expect(init?.credentials).toBe('same-origin')
48
+ return new Response(JSON.stringify({success:true,data:{shares:[{name:'home',path:'/home',isdir:true}],offset:0,total:1}}))
49
+ })
50
+ vi.stubGlobal('fetch',fetch);cleanup.push(()=>vi.unstubAllGlobals())
41
51
  const call=vi.fn(async(method:string,params?:unknown)=>{
52
+ if(method==='nas.session.status')return {mode:'browser-session'}
42
53
  if(method==='delegate.status')return {state:'ready',disabled:false,lifetime:'until-revoked',destinations:['save']}
43
54
  if(method==='delegate.destination')return {key:'save',selection:{scope:'home',root:'/home',generation:0},destination:selected?grant:null,status:selected?'active':'missing'}
44
55
  if(method==='delegate.list')return {delegations:selected?[{delegation_id:'g1',destination:'save',path:'/home',subject:{username:'alice'},lifetime:'until-revoked',revoked:false}]:[]}
45
56
  if(method==='delegate.operations.list')return {operations:[]}
46
- if(method==='bg.browse')return {shares:[{name:'home',path:'/home',isdir:true}],offset:0,total:1}
47
57
  if(method==='delegate.select_destination'){selected=true;return grant}
48
58
  if(method==='delegate.example.export')return {delegation_id:'g1',job_id:(params as {job_id:string}).job_id}
49
59
  throw {code:'unexpected'}
50
60
  })
51
- const root=render({call,locale:'en'});await settle()
61
+ const root=render({call,locale:'en',files:{token:()=> 'fixture-token'}});await settle()
52
62
  button(root,'Choose folder').click();await settle()
53
63
  // The SDK picker selects a folder with a row click.
54
64
  const row=root.querySelector<HTMLElement>('tr[data-row-key="/home"]')!;expect(row).toBeTruthy();row.dispatchEvent(new MouseEvent('click',{bubbles:true}));await settle()
55
65
  button(root,'Confirm selection').click();await settle()
56
66
  expect(call).toHaveBeenCalledWith('delegate.select_destination',{key:'save',path:'/home'})
67
+ expect(fetch).toHaveBeenCalledTimes(1)
68
+ expect(call).not.toHaveBeenCalledWith('bg.browse',expect.anything())
57
69
  // The SDK hides the sample save unless an App opts in; the template opts in with sample-save.
58
70
  button(root,'Save example file').click();await settle()
59
71
  expect(call).toHaveBeenCalledWith('delegate.example.export',expect.objectContaining({key:'save'}))
@@ -16,7 +16,7 @@ import (
16
16
 
17
17
  func init() {
18
18
  // The generic route tests need valid params for every background route.
19
- // The fixture has no DSM cookie; import is refused before reading a file.
19
+ // The SDK refuses retired backend session relays before reading a file.
20
20
  // No jobs or schedules exist in its fresh data directory.
21
21
  const id = `{"id":"missing"}`
22
22
  sampleParams["bg.jobs.purge"] = `{"owner":{"user":"missing","uid":"9999"}}`
@@ -38,8 +38,8 @@ func init() {
38
38
  sampleParams["bg.jobs.pause"] = id
39
39
  sampleParams["bg.jobs.resume"] = id
40
40
  sampleParams["bg.jobs.result"] = id
41
- sampleOutcome["bg.browse"] = "session_required"
42
- sampleOutcome["bg.jobs.submit"] = "session_required"
41
+ sampleOutcome["bg.browse"] = "unsupported"
42
+ sampleOutcome["bg.jobs.submit"] = "unsupported"
43
43
  sampleOutcome["bg.jobs.get"] = "not_found"
44
44
  sampleOutcome["bg.jobs.cancel"] = "not_found"
45
45
  sampleOutcome["bg.jobs.pause"] = "not_found"
@@ -90,3 +90,17 @@ func TestBackgroundRequestSubmitBusyAndProductionConstruction(t *testing.T) {
90
90
  t.Fatal(w.Body.String())
91
91
  }
92
92
  }
93
+
94
+ func TestProductionNASHashExampleRefusesWithoutCreatingJobs(t *testing.T) {
95
+ cfg := testConfig(t)
96
+ for _, method := range []string{"bg.browse", "bg.jobs.submit"} {
97
+ code, _ := call(t, cfg, alice, method, sampleParams[method])
98
+ if code != "unsupported" {
99
+ t.Fatalf("%s: %s", method, code)
100
+ }
101
+ }
102
+ jobs, err := background.NewStore(filepath.Join(cfg.DataDir, "background")).List(context.Background())
103
+ if err != nil || len(jobs) != 0 {
104
+ t.Fatalf("disabled example created jobs: %v %v", jobs, err)
105
+ }
106
+ }
@@ -4,8 +4,8 @@ import {nextTick} from 'vue'
4
4
  import {mount} from '../src/main'
5
5
  import {dsmWindow} from './context'
6
6
  async function settle(){for(let i=0;i<20;i++){await Promise.resolve();await nextTick()}}
7
- // The CLI turns the background page on for jobs (and delegate) projects only.
8
- test('a jobs project mounts the background panel for the signed-in user',async()=>{
7
+ // The file-import example is explicitly unavailable until browser input is migrated.
8
+ test('a jobs project explains the disabled example without submitting or reading files',async()=>{
9
9
  const call=vi.fn(async(method:string)=>{
10
10
  if(method==='app.status')return {package:'Jobs',version:'1',user:'alice',admin:false}
11
11
  throw {code:'unknown_method'}
@@ -14,46 +14,8 @@ test('a jobs project mounts the background panel for the signed-in user',async()
14
14
  const app=mount(root,dsmWindow({call}))
15
15
  try{
16
16
  await settle()
17
- expect(call.mock.calls.some(([method])=>method.startsWith('bg.'))).toBe(true)
18
- expect(root.querySelector('[data-page="background"]')).not.toBeNull()
19
- }finally{app.unmount();root.remove()}
20
- })
21
- test('the background panel pauses a running job and resumes a paused one',async()=>{
22
- const submitter={user:'alice',uid:'1024'}
23
- const jobs=[{id:'run',name:'a.bin',state:'running',submitter},{id:'held',name:'b.bin',state:'paused',pauseReason:'user',submitter}]
24
- const call=vi.fn(async(method:string):Promise<unknown>=>{
25
- if(method==='app.status')return {package:'Jobs',version:'1',user:'alice',admin:false}
26
- if(method==='bg.status')return {owner:submitter,admin:false,revision:1,worker:{username:'pkg',alive:true},limits:{maxInputBytes:1024,maxQueued:8,maxOwnerPending:4,maxOwnerBytes:4096,jobTimeoutSeconds:60}}
27
- if(method==='bg.jobs.list')return {jobs}
28
- if(method==='bg.schedules.list')return {schedules:[]}
29
- return {}
30
- })
31
- const root=document.createElement('div');document.body.append(root)
32
- const app=mount(root,dsmWindow({call}))
33
- const button=(id:string,label:string)=>Array.from(root.querySelectorAll(`[data-job-id="${id}"] button`)).find(b=>b.textContent===label) as HTMLButtonElement|undefined
34
- try{
35
- await settle()
36
- button('run','Pause')!.click();await settle()
37
- expect(call).toHaveBeenCalledWith('bg.jobs.pause',{id:'run'})
38
- button('held','Resume')!.click();await settle()
39
- expect(call).toHaveBeenCalledWith('bg.jobs.resume',{id:'held'})
40
- }finally{app.unmount();root.remove()}
41
- })
42
- test('the header Refresh reloads the background panel',async()=>{
43
- const call=vi.fn(async(method:string):Promise<unknown>=>{
44
- if(method==='app.status')return {package:'Jobs',version:'1',user:'alice',admin:false}
45
- if(method==='bg.status')return {owner:{user:'alice',uid:'1024'},admin:false,revision:1,worker:{username:'pkg',alive:true},limits:{maxInputBytes:1024,maxQueued:8,maxOwnerPending:4,maxOwnerBytes:4096,jobTimeoutSeconds:60}}
46
- if(method==='bg.jobs.list')return {jobs:[]}
47
- if(method==='bg.schedules.list')return {schedules:[]}
48
- return {}
49
- })
50
- const root=document.createElement('div');document.body.append(root)
51
- const app=mount(root,dsmWindow({call}))
52
- const reads=()=>call.mock.calls.filter(([method])=>method==='bg.jobs.list').length
53
- try{
54
- await settle();const before=reads()
55
- // The header button comes first; the panels below may have their own.
56
- ;(Array.from(root.querySelectorAll('button')).find(b=>b.textContent==='Refresh') as HTMLButtonElement).click();await settle()
57
- expect(reads()).toBeGreaterThan(before)
17
+ expect(root.querySelector('[data-background-example-disabled]')?.textContent).toContain('disabled')
18
+ expect(root.querySelector('[data-background-panel]')).toBeNull()
19
+ expect(call.mock.calls.some(([method])=>method.startsWith('bg.'))).toBe(false)
58
20
  }finally{app.unmount();root.remove()}
59
21
  })
@@ -7,8 +7,8 @@
7
7
  "name": "native-dsm-vue-app",
8
8
  "dependencies": {
9
9
  "@lucide/vue": "1.48.0",
10
- "@synopackageland/sdk": "0.26.0",
11
- "@synopackageland/ui": "0.14.8",
10
+ "@synopackageland/sdk": "0.27.0",
11
+ "@synopackageland/ui": "0.14.9",
12
12
  "vue": "3.5.43"
13
13
  },
14
14
  "devDependencies": {
@@ -1152,9 +1152,9 @@
1152
1152
  }
1153
1153
  },
1154
1154
  "node_modules/@synopackageland/sdk": {
1155
- "version": "0.26.0",
1156
- "resolved": "https://registry.npmjs.org/@synopackageland/sdk/-/sdk-0.26.0.tgz",
1157
- "integrity": "sha512-f6nKQzA+Bn+ilHAtJdoksPg/t3beA0Nxo1tOvFrTIqSuzB8CeMCQ5zZ263NigFvV3yDYwM9T4AD4C0Iw/LXf5Q==",
1155
+ "version": "0.27.0",
1156
+ "resolved": "https://registry.npmjs.org/@synopackageland/sdk/-/sdk-0.27.0.tgz",
1157
+ "integrity": "sha512-KIFeYhzmiH0QFAD8SutTvSXCiynx/D84ydOROGPS9B5VcHkLU8uqtD9Mwac9G0UY7mPITioGJuJGXpGytFPCVg==",
1158
1158
  "license": "MIT",
1159
1159
  "peerDependencies": {
1160
1160
  "@lucide/vue": "^1.48.0",
@@ -1171,9 +1171,9 @@
1171
1171
  }
1172
1172
  },
1173
1173
  "node_modules/@synopackageland/ui": {
1174
- "version": "0.14.8",
1175
- "resolved": "https://registry.npmjs.org/@synopackageland/ui/-/ui-0.14.8.tgz",
1176
- "integrity": "sha512-PcSAUSHdlM8ZCiRS9i6xRvT/BoBV5oYAM1DTFTUDyohgeNwnMqENaVuhv8uvpGRGXncoXB1K1B7nZBAyQTopnQ==",
1174
+ "version": "0.14.9",
1175
+ "resolved": "https://registry.npmjs.org/@synopackageland/ui/-/ui-0.14.9.tgz",
1176
+ "integrity": "sha512-Vr925mORKVuWJH/EaEvaSUF4nxsHP0SWUYwwMjM009MFw+UHaABoEtasAliCDsnI7CnlggLcexO3ad5slm9IVw==",
1177
1177
  "license": "MIT",
1178
1178
  "dependencies": {
1179
1179
  "@internationalized/date": "3.12.4",
@@ -8,8 +8,8 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@lucide/vue": "1.48.0",
11
- "@synopackageland/sdk": "0.26.0",
12
- "@synopackageland/ui": "0.14.8",
11
+ "@synopackageland/sdk": "0.27.0",
12
+ "@synopackageland/ui": "0.14.9",
13
13
  "vue": "3.5.43"
14
14
  },
15
15
  "devDependencies": {
@@ -2,7 +2,7 @@
2
2
  import {computed, onMounted, onUnmounted, ref, watch} from 'vue'
3
3
  import {Button, Callout, Form, FormActions, FormItem, KeyValueRow, SettingsSection, Textarea} from '@synopackageland/ui'
4
4
  import {bridgeErrorCopy, sharedFolderAccessFor, type AppStatus, type Context, type ShellPage} from '@synopackageland/sdk'
5
- import {AppShell, BackgroundPanel, FilesPage, LoginPage, PortalSettings, SharedFolderSettings, createSession, shellPages} from '@synopackageland/sdk/vue'
5
+ import {AppShell, FilesPage, LoginPage, PortalSettings, SharedFolderSettings, createSession, shellPages} from '@synopackageland/sdk/vue'
6
6
  import {appCopy} from './i18n'
7
7
  import CapabilityPanels from './CapabilityPanels.vue'
8
8
  import ExecutorGate, {holdsPages} from './ExecutorGate.vue'
@@ -66,11 +66,10 @@ async function refresh() {
66
66
  }
67
67
  // The header Refresh also reloads the panels below and reads the portal again.
68
68
  const panels = ref<{refresh(): unknown}>()
69
- const background = ref<{refresh(): unknown}>()
70
69
  const portalReads = ref(0)
71
70
  const portalKey = computed(() => `${app.value?.user}:${portalReads.value}`)
72
71
  function refreshAll() {
73
- void refresh(); void panels.value?.refresh(); void background.value?.refresh(); portalReads.value++
72
+ void refresh(); void panels.value?.refresh(); portalReads.value++
74
73
  }
75
74
  async function save() {
76
75
  if(disposed || busy.value) return
@@ -122,7 +121,7 @@ onUnmounted(()=>{disposed=true;unsubscribe?.();login.dispose();text.value=''})
122
121
  <CapabilityPanels v-if="app" ref="panels" :key="app.user" :ctx="ctx" :admin="app.admin" />
123
122
  <FilesPage v-if="page==='status'" :ctx="ctx" />
124
123
  <div data-page="background" v-show="page==='background'">
125
- <BackgroundPanel v-if="backgroundEnabled && app" ref="background" :key="app.user" :ctx="ctx" />
124
+ <Callout v-if="backgroundEnabled && app" tone="warning" data-background-example-disabled>{{ copy.backgroundExampleUnavailable }}</Callout>
126
125
  </div>
127
126
  </div>
128
127
  </ExecutorGate>
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "Paketname",
19
19
  "user": "Benutzer",
20
- "version": "Version"
20
+ "version": "Version",
21
+ "backgroundExampleUnavailable": "Das NAS-Datei-Hash-Beispiel ist bis zur Umstellung auf Browser-Eingaben deaktiviert. Bestehende APIs zur Auftragssteuerung bleiben verfügbar"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "Package name",
19
19
  "user": "User",
20
- "version": "Version"
20
+ "version": "Version",
21
+ "backgroundExampleUnavailable": "The NAS file hashing example is disabled pending browser input migration. Existing job control APIs remain available"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "Nombre del paquete",
19
19
  "user": "Usuario",
20
- "version": "Versión"
20
+ "version": "Versión",
21
+ "backgroundExampleUnavailable": "El ejemplo de hash de archivos NAS está desactivado hasta migrar la entrada al navegador. Las API de control de trabajos siguen disponibles"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "Nom du paquet",
19
19
  "user": "Utilisateur",
20
- "version": "Version"
20
+ "version": "Version",
21
+ "backgroundExampleUnavailable": "L’exemple de hachage de fichiers NAS est désactivé en attendant la migration des entrées du navigateur. Les API de contrôle des tâches restent disponibles"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "Nome pacchetto",
19
19
  "user": "Utente",
20
- "version": "Versione"
20
+ "version": "Versione",
21
+ "backgroundExampleUnavailable": "L’esempio di hashing dei file NAS è disabilitato in attesa della migrazione dell’input dal browser. Le API di controllo dei processi restano disponibili"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "パッケージ名",
19
19
  "user": "ユーザー",
20
- "version": "バージョン"
20
+ "version": "バージョン",
21
+ "backgroundExampleUnavailable": "NAS ファイルのハッシュ例はブラウザー入力への移行まで無効です。既存のジョブ制御 API は利用できます"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "패키지 이름",
19
19
  "user": "사용자",
20
- "version": "버전"
20
+ "version": "버전",
21
+ "backgroundExampleUnavailable": "브라우저 입력 전환이 완료될 때까지 NAS 파일 해시 예제를 사용할 수 없습니다. 기존 작업 제어 API는 계속 사용할 수 있습니다"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "套件名称",
19
19
  "user": "用户帐号",
20
- "version": "版本"
20
+ "version": "版本",
21
+ "backgroundExampleUnavailable": "本版本暂停 NAS 文件哈希示例,待浏览器输入流程完成后恢复。现有任务控制 API 仍可使用"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "套件名稱",
19
19
  "user": "使用者帳號",
20
- "version": "版本"
20
+ "version": "版本",
21
+ "backgroundExampleUnavailable": "本版本暫停 NAS 檔案雜湊範例,待瀏覽器輸入流程完成後恢復。既有工作控制 API 仍可使用"
21
22
  }
@@ -111,7 +111,7 @@ rebuilding the SPK.
111
111
 
112
112
  | `--with` | Methods | Runtime services |
113
113
  | --- | --- | --- |
114
- | `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | `files.read` with the caller's session, `bg.submit`, `bg.get`, `bg.cancel` |
114
+ | `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | `hash.submit` is disabled pending browser input migration; `bg.get` and `bg.cancel` remain available |
115
115
  | `root-ops` | `whoami.run` | `root.run` of `root/whoami.sh`, once an administrator approves the executor |
116
116
  | `delegate` | `report.save` | `delegate.move_from_staging` into a folder the caller chose |
117
117
 
@@ -158,3 +158,7 @@ ticks **Also delete the app data listed above** in the uninstall wizard.
158
158
  | Build hosts | macOS, Linux |
159
159
  | Target CPUs | x86_64 and aarch64 DSM models (one SPK each); armv7 is opt-in in `arches`, built but not NAS-verified |
160
160
  | DSM | 7.1 or later declared (`os_min_ver`) |
161
+
162
+ The NAS file hashing example is temporarily disabled pending browser-owned
163
+ input migration. NAS import submission returns `unsupported` without reading a
164
+ file or creating a job; existing worker and job-control APIs remain available.
@@ -49,7 +49,7 @@ backend:
49
49
  runtime: node
50
50
  dir: backend/app
51
51
  entry: main.js
52
- sdk: 0.26.0
52
+ sdk: 0.27.0
53
53
  node: { package: Node.js_v22 }
54
54
 
55
55
  ui:
@@ -1,38 +1,17 @@
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
- import { copyFileSync, mkdirSync, unlinkSync } from 'node:fs'
8
- import { randomUUID } from 'node:crypto'
9
- import { basename, join } from 'node:path'
1
+ // The NAS hashing example is disabled until browser-owned input is wired.
2
+ // Keep caller-bound job controls; never relay a browser session from the backend.
10
3
  import { AppError, type App, type Context } from '@synopackageland/app'
11
4
  import { strings } from './params.js'
12
5
  import type { Config } from './routes.js'
13
6
  import { service } from './service.js'
14
7
 
15
- /** A job's input is at most 512 KiB, copied into the job when it is submitted. */
16
- const MAX_INPUT = 512 * 1024
17
-
18
8
  export function register(app: App, cfg: Config): void {
19
9
  app.method('hash.submit', (ctx, params) => submit(cfg, ctx, params))
20
10
  app.method('hash.get', (ctx, params) => service(ctx, 'bg.get', strings(params, 'id')))
21
11
  app.method('hash.cancel', (ctx, params) => service(ctx, 'bg.cancel', strings(params, 'id')))
22
12
  }
23
13
 
24
- async function submit(cfg: Config, ctx: Context, params: unknown) {
25
- const { key, path } = strings(params, 'key', 'path')
26
- const read = await service<{ path: string; size: number }>(ctx, 'files.read', { path })
27
- if (read.size > MAX_INPUT) throw new AppError('limit_exceeded', 'The file is larger than allowed.')
28
- // bg.submit takes its input from the data directory; the call's spool is elsewhere.
29
- const inputs = join(cfg.dataDir, 'hash-inputs')
30
- mkdirSync(inputs, { recursive: true, mode: 0o700 })
31
- const input = join(inputs, randomUUID())
32
- copyFileSync(read.path, input)
33
- try {
34
- return await service(ctx, 'bg.submit', { key, name: basename(path), input })
35
- } finally {
36
- unlinkSync(input)
37
- }
14
+ async function submit(_cfg: Config, _ctx: Context, params: unknown) {
15
+ strings(params, 'key', 'path')
16
+ throw new AppError('unsupported', "The NAS file hashing example is disabled pending browser input migration. Existing job control APIs remain available.")
38
17
  }
@@ -1,60 +1,26 @@
1
1
  import assert from 'node:assert/strict'
2
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { mkdirSync, mkdtempSync, readdirSync, rmSync } from 'node:fs'
3
3
  import { tmpdir } from 'node:os'
4
- import { join, sep } from 'node:path'
4
+ import { join } from 'node:path'
5
5
  import { afterEach, beforeEach, test } from 'node:test'
6
6
  import { build, type Config } from '../src/routes.js'
7
- import { Refusal, call } from './support.js'
7
+ import { call } from './support.js'
8
8
 
9
9
  const JOB = { job: { id: 'j1', state: 'queued' }, duplicate: false }
10
10
  let root: string
11
11
  let cfg: Config
12
- let submitted: Record<string, unknown>[]
13
12
 
14
13
  beforeEach(() => {
15
14
  root = mkdtempSync(join(tmpdir(), 'app-'))
16
15
  cfg = { packageId: 'ExampleApp', dataDir: join(root, 'var'), packagesRoot: join(root, 'packages') }
17
16
  mkdirSync(cfg.dataDir)
18
- submitted = []
19
17
  })
20
18
  afterEach(() => rmSync(root, { recursive: true, force: true }))
21
19
 
22
- function spool(content: Buffer) {
23
- const path = join(root, 'spool')
24
- writeFileSync(path, content)
25
- return { path, size: content.length }
26
- }
27
-
28
- // The runtime reads the input file while it answers bg.submit.
29
- function submit(params: unknown) {
30
- const p = params as { input: string }
31
- submitted.push({ ...p, content: readFileSync(p.input, 'utf8') })
32
- return JOB
33
- }
34
-
35
- test('hash.submit hashes a copy of the caller\'s file', async () => {
36
- const services = { 'files.read': () => spool(Buffer.from('hello')), 'bg.submit': submit }
37
- const reply = await call(build(cfg), 'hash.submit', { key: 'k1', path: '/home/report.txt' }, { services })
38
- assert.deepEqual(reply.data, JOB)
39
- assert.deepEqual(reply.services.map(([op]) => op), ['files.read', 'bg.submit'])
40
- assert.deepEqual(reply.services[0]![1], { path: '/home/report.txt' })
41
- const [job] = submitted as [{ key: string; name: string; content: string; input: string }]
42
- assert.deepEqual([job.key, job.name, job.content], ['k1', 'report.txt', 'hello'])
43
- assert.ok(job.input.startsWith(cfg.dataDir + sep), job.input)
44
- assert.equal(existsSync(job.input), false, 'the copy is removed once submitted')
45
- })
46
-
47
- test('hash.submit refuses a file over the job input limit', async () => {
48
- const services = { 'files.read': () => spool(Buffer.alloc(512 * 1024 + 1)), 'bg.submit': submit }
49
- const reply = await call(build(cfg), 'hash.submit', { key: 'k1', path: '/home/big.bin' }, { services })
50
- assert.equal(reply.error?.code, 'limit_exceeded')
51
- assert.deepEqual(submitted, [])
52
- })
53
-
54
- test('hash.submit passes on the runtime\'s refusal', async () => {
55
- const services = { 'files.read': () => { throw new Refusal('read_failed', 'Unable to read the file.') } }
56
- const reply = await call(build(cfg), 'hash.submit', { key: 'k1', path: '/home/a' }, { services })
57
- assert.equal(reply.error?.code, 'read_failed')
20
+ test('the NAS hash example refuses without services or persistent writes', async () => {
21
+ const reply = await call(build(cfg), 'hash.submit', { key: 'k1', path: '/home/report.txt' })
22
+ assert.deepEqual([reply.error?.code, reply.services], ['unsupported', []])
23
+ assert.deepEqual(readdirSync(cfg.dataDir), [])
58
24
  })
59
25
 
60
26
  test('hash.submit refuses invalid params before any service', async () => {
@@ -108,7 +108,7 @@ rebuilding the SPK.
108
108
 
109
109
  | `--with` | Methods | Runtime services |
110
110
  | --- | --- | --- |
111
- | `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | `files.read` with the caller's session, `bg.submit`, `bg.get`, `bg.cancel` |
111
+ | `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | `hash.submit` is disabled pending browser input migration; `bg.get` and `bg.cancel` remain available |
112
112
  | `root-ops` | `whoami.run` | `root.run` of `root/whoami.sh`, once an administrator approves the executor |
113
113
  | `delegate` | `report.save` | `delegate.move_from_staging` into a folder the caller chose |
114
114
 
@@ -155,3 +155,7 @@ ticks **Also delete the app data listed above** in the uninstall wizard.
155
155
  | Build hosts | macOS, Linux |
156
156
  | Target CPUs | x86_64 and aarch64 DSM models (one SPK each); armv7 is opt-in in `arches`, built but not NAS-verified |
157
157
  | DSM | 7.1 or later declared (`os_min_ver`) |
158
+
159
+ The NAS file hashing example is temporarily disabled pending browser-owned
160
+ input migration. NAS import submission returns `unsupported` without reading a
161
+ file or creating a job; existing worker and job-control APIs remain available.
@@ -47,7 +47,7 @@ backend:
47
47
  runtime: python
48
48
  dir: backend/app
49
49
  entry: main.py
50
- sdk: 0.26.0
50
+ sdk: 0.27.0
51
51
 
52
52
  ui:
53
53
  # Non-UI-SDK frontends open a full page; Portal is a separate opt-in.
@@ -1,22 +1,10 @@
1
- """Background jobs: hash a NAS file the caller may read.
1
+ """The NAS hashing example is disabled until browser-owned input is wired.
2
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.
3
+ Caller-bound job controls remain available; no backend session replay is used.
7
4
  """
8
-
9
- import os
10
- import shutil
11
- import tempfile
12
-
13
5
  from synopkgland_app import Error
14
-
15
6
  from params import fields
16
7
 
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
8
 
21
9
  def register(app, cfg):
22
10
  app.method("hash.submit")(lambda ctx, params: submit(cfg, ctx, params))
@@ -25,16 +13,5 @@ def register(app, cfg):
25
13
 
26
14
 
27
15
  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)
16
+ fields(params, key=str, path=str)
17
+ raise Error("unsupported", "The NAS file hashing example is disabled pending browser input migration. Existing job control APIs remain available.")
@@ -2,7 +2,7 @@ import os
2
2
  import tempfile
3
3
  import unittest
4
4
 
5
- from support import Error, call
5
+ from support import call
6
6
 
7
7
  import routes
8
8
 
@@ -16,43 +16,11 @@ class JobsTest(unittest.TestCase):
16
16
  self.data = os.path.join(self.dir.name, "var")
17
17
  os.mkdir(self.data)
18
18
  self.app = routes.build(routes.Config("ExampleApp", self.data))
19
- self.submitted = []
20
19
 
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")
20
+ def test_nas_hash_example_refuses_without_services_or_writes(self):
21
+ reply = call(self.app, "hash.submit", {"key": "k1", "path": "/home/report.txt"})
22
+ self.assertEqual((reply.error["code"], reply.services), ("unsupported", []))
23
+ self.assertEqual(os.listdir(self.data), [])
56
24
 
57
25
  def test_submit_refuses_invalid_params_before_any_service(self):
58
26
  for params in [None, {"key": "k1"}, {"key": 1, "path": "/home/a"}, {"key": "k1", "path": "/home/a", "x": 1}]:
@@ -117,7 +117,7 @@ rebuilding the SPK.
117
117
 
118
118
  | `--with` | Methods | Runtime services |
119
119
  | --- | --- | --- |
120
- | `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | `files.read` with the caller's session, `bg.submit`, `bg.get`, `bg.cancel` |
120
+ | `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | `hash.submit` is disabled pending browser input migration; `bg.get` and `bg.cancel` remain available |
121
121
  | `root-ops` | `whoami.run` | `root.run` of `root/whoami.sh`, once an administrator approves the executor |
122
122
  | `delegate` | `report.save` | `delegate.move_from_staging` into a folder the caller chose |
123
123
 
@@ -164,3 +164,7 @@ ticks **Also delete the app data listed above** in the uninstall wizard.
164
164
  | Build hosts | macOS, Linux |
165
165
  | Target CPUs | x86_64 and aarch64 DSM models (one SPK each); armv7 is opt-in in `arches`, built but not NAS-verified |
166
166
  | DSM | 7.1 or later declared (`os_min_ver`) |
167
+
168
+ The NAS file hashing example is temporarily disabled pending browser-owned
169
+ input migration. NAS import submission returns `unsupported` without reading a
170
+ file or creating a job; existing worker and job-control APIs remain available.
@@ -104,8 +104,8 @@ dependencies = [
104
104
 
105
105
  [[package]]
106
106
  name = "syno-pkg-land-app"
107
- version = "0.26.0"
108
- source = "git+https://github.com/synopackageland/syno-pkg-land-sdk?tag=v0.26.0#ff3b079cf034e12ce69e936e4fefb010cd010b3d"
107
+ version = "0.27.0"
108
+ source = "git+https://github.com/synopackageland/syno-pkg-land-sdk?tag=v0.27.0#a10253ad1c94b647f4b626d9b99591b5dc2571a1"
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.26.0" }
12
+ syno-pkg-land-app = { git = "https://github.com/synopackageland/syno-pkg-land-sdk", tag = "v0.27.0" }
13
13
 
14
14
  [profile.release]
15
15
  # A handler panic becomes an "internal" reply; abort would end the App without one.
@@ -1,90 +1,38 @@
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
- use crate::routes::{Config, internal};
1
+ //! NAS hashing is disabled until browser-owned input is wired; job controls remain.
2
+ use crate::routes::Config;
9
3
  use serde::Deserialize;
10
4
  use serde_json::{Value, json};
11
- use std::os::unix::fs::DirBuilderExt;
12
- use std::path::{Path, PathBuf};
13
5
  use syno_pkg_land_app::{App, Context, Error, Params};
14
6
 
15
- /// A job's input is at most 512 KiB, copied into the job when it is submitted.
16
- const MAX_INPUT: u64 = 512 * 1024;
17
-
18
7
  #[derive(Deserialize)]
19
8
  #[serde(deny_unknown_fields)]
20
9
  struct Submit {
21
10
  key: String,
22
11
  path: String,
23
12
  }
24
-
25
13
  #[derive(Deserialize)]
26
14
  #[serde(deny_unknown_fields)]
27
15
  struct JobId {
28
16
  id: String,
29
17
  }
30
18
 
31
- #[derive(Deserialize)]
32
- struct Read {
33
- path: PathBuf,
34
- size: u64,
35
- }
36
-
37
- pub fn register(app: &mut App, cfg: &Config) {
38
- let cfg = cfg.clone();
39
- app.method("hash.submit", move |ctx, params| submit(&cfg, ctx, params));
19
+ pub fn register(app: &mut App, _cfg: &Config) {
20
+ app.method("hash.submit", |_ctx, params| -> Result<Value, Error> {
21
+ let Submit { key, path } = params.decode()?;
22
+ let _ = (key, path);
23
+ Err(Error::new("unsupported", "The NAS file hashing example is disabled pending browser input migration. Existing job control APIs remain available."))
24
+ });
40
25
  app.method("hash.get", |ctx, params| job(ctx, "bg.get", params));
41
26
  app.method("hash.cancel", |ctx, params| job(ctx, "bg.cancel", params));
42
27
  }
43
-
44
28
  fn job(ctx: &Context, op: &str, params: &Params) -> Result<Value, Error> {
45
29
  let JobId { id } = params.decode()?;
46
30
  ctx.runtime(op, &json!({"id": id}))
47
31
  }
48
-
49
- fn submit(cfg: &Config, ctx: &Context, params: &Params) -> Result<Value, Error> {
50
- let p: Submit = params.decode()?;
51
- let read: Read = ctx.runtime("files.read", &json!({"path": p.path}))?;
52
- if read.size > MAX_INPUT {
53
- return Err(Error::new(
54
- "limit_exceeded",
55
- "The file is larger than allowed.",
56
- ));
57
- }
58
- // bg.submit takes its input from the data directory; the call's spool is elsewhere.
59
- let inputs = cfg.data_dir.join("hash-inputs");
60
- std::fs::DirBuilder::new()
61
- .recursive(true)
62
- .mode(0o700)
63
- .create(&inputs)
64
- .map_err(internal)?;
65
- let copy = inputs.join(&ctx.request_id);
66
- std::fs::copy(&read.path, &copy).map_err(internal)?;
67
- let name = Path::new(&p.path)
68
- .file_name()
69
- .map_or(String::new(), |n| n.to_string_lossy().into_owned());
70
- let submitted = ctx.runtime(
71
- "bg.submit",
72
- &json!({"key": p.key, "name": name, "input": copy}),
73
- );
74
- let _ = std::fs::remove_file(&copy);
75
- submitted
76
- }
77
-
78
32
  #[cfg(test)]
79
33
  mod tests {
80
34
  use super::*;
81
35
  use crate::test_support::{Services, TempDir, call};
82
- use serde_json::{Value, json};
83
- use std::sync::{Arc, Mutex};
84
-
85
- /// What bg.submit was asked: its params and the input it read.
86
- type Submitted = Arc<Mutex<Vec<(Value, Vec<u8>)>>>;
87
-
88
36
  fn setup() -> (TempDir, Config) {
89
37
  let dir = TempDir::new();
90
38
  let cfg = Config {
@@ -100,94 +48,19 @@ mod tests {
100
48
  json!({"job": {"id": "j1", "state": "queued"}, "duplicate": false})
101
49
  }
102
50
 
103
- /// files.read answers a spool file with content; bg.submit records its params and the input it read.
104
- fn services(dir: &TempDir, content: Vec<u8>, submitted: &Submitted) -> Services {
105
- let spool = dir.path().join("spool");
106
- std::fs::write(&spool, &content).unwrap();
107
- let read = json!({"path": spool, "size": content.len()});
108
- let submitted = submitted.clone();
109
- [
110
- (
111
- "files.read",
112
- Box::new(move |_: &Value| Ok(read.clone())) as _,
113
- ),
114
- (
115
- "bg.submit",
116
- Box::new(move |p: &Value| {
117
- // The runtime reads the input file while it answers bg.submit.
118
- let input = std::fs::read(p["input"].as_str().unwrap()).unwrap();
119
- submitted.lock().unwrap().push((p.clone(), input));
120
- Ok(job())
121
- }) as _,
122
- ),
123
- ]
124
- .into()
125
- }
126
-
127
- #[test]
128
- fn submit_hashes_a_copy_of_the_callers_file() {
129
- let (dir, cfg) = setup();
130
- let submitted = Arc::new(Mutex::new(Vec::new()));
131
- let services = services(&dir, b"hello".to_vec(), &submitted);
132
- let reply = call(
133
- crate::routes::build(cfg.clone()),
134
- "hash.submit",
135
- json!({"key": "k1", "path": "/home/report.txt"}),
136
- "alice",
137
- &services,
138
- );
139
- assert_eq!(reply.data, job());
140
- let ops: Vec<_> = reply.services.iter().map(|(op, _)| op.as_str()).collect();
141
- assert_eq!(ops, ["files.read", "bg.submit"]);
142
- assert_eq!(reply.services[0].1, json!({"path": "/home/report.txt"}));
143
- let submitted = submitted.lock().unwrap();
144
- let (params, content) = &submitted[0];
145
- assert_eq!(
146
- (&params["key"], &params["name"], content.as_slice()),
147
- (&json!("k1"), &json!("report.txt"), b"hello".as_slice())
148
- );
149
- let input = params["input"].as_str().unwrap();
150
- assert!(input.starts_with(cfg.data_dir.to_str().unwrap()), "{input}");
151
- assert!(
152
- !std::path::Path::new(input).exists(),
153
- "the copy is removed once submitted"
154
- );
155
- }
156
-
157
51
  #[test]
158
- fn submit_refuses_a_file_over_the_job_input_limit() {
159
- let (dir, cfg) = setup();
160
- let submitted = Arc::new(Mutex::new(Vec::new()));
161
- let services = services(&dir, vec![0; MAX_INPUT as usize + 1], &submitted);
162
- let reply = call(
163
- crate::routes::build(cfg),
164
- "hash.submit",
165
- json!({"key": "k1", "path": "/home/big.bin"}),
166
- "alice",
167
- &services,
168
- );
169
- assert_eq!(reply.code(), "limit_exceeded");
170
- assert!(submitted.lock().unwrap().is_empty());
171
- }
172
-
173
- #[test]
174
- fn submit_passes_on_the_runtimes_refusal() {
52
+ fn nas_hash_example_refuses_without_services_or_writes() {
175
53
  let (_dir, cfg) = setup();
176
- let services: Services = [(
177
- "files.read",
178
- Box::new(|_: &Value| Err(Error::new("read_failed", "Unable to read the file."))) as _,
179
- )]
180
- .into();
181
54
  let reply = call(
182
- crate::routes::build(cfg),
55
+ crate::routes::build(cfg.clone()),
183
56
  "hash.submit",
184
- json!({"key": "k1", "path": "/home/a"}),
57
+ json!({"key":"k1","path":"/home/report.txt"}),
185
58
  "alice",
186
- &services,
59
+ &Services::new(),
187
60
  );
188
- assert_eq!(reply.code(), "read_failed");
61
+ assert_eq!((reply.code(), reply.services.len()), ("unsupported", 0));
62
+ assert_eq!(std::fs::read_dir(&cfg.data_dir).unwrap().count(), 0);
189
63
  }
190
-
191
64
  #[test]
192
65
  fn submit_refuses_invalid_params_before_any_service() {
193
66
  let (_dir, cfg) = setup();