@synopackageland /应用程序
用于Synology Package Land应用程序后端的Node SDK
TypeScript或JavaScript。节点22或更高版本(DSM软件包中心的)
Node.js_v22),仅ESM,没有运行时
依赖关系。
包的CGI是synopkgland Go运行时(bridge/cmd/runtime). 它
检查请求的来源、DSM会话、入口门和方法
政策 permissions.json 和截止日期,然后才启动应用程序,
作为包帐户,并通过桥接通道将调用交给它
(spec/bridge/v1):在stdin和
标准输出,每个调用一个进程。应用程序永远不会以root身份运行,也永远不会看到
DSM会话;它向运行时请求的会话需要什么
ctx.runtime.
一个应用程序
import fs from 'node:fs'
import path from 'node:path'
import { App, AppError } from '@synopackageland/app'
const app = new App()
app.method('hello.greet', (ctx) => ({ message: `Hello, ${ctx.user}` }))
app.method('hello.save', (ctx, params: { note?: unknown } | null) => {
if (typeof params?.note !== 'string') throw new AppError('bad_request', 'note must be a string')
const notes = path.join(process.env.SYNOPKGLAND_DATA_DIR!, 'notes')
fs.mkdirSync(notes, { recursive: true, mode: 0o700 })
fs.writeFileSync(path.join(notes, `${ctx.requestId}.txt`), params.note, { flag: 'wx' })
return { bytes: Buffer.byteLength(params.note) }
})
await app.run()
浏览器调用它 @synopackageland/sdk 和其他方法一样:
{"method":"hello.greet"} 答案 {"success":true,"data":{"message":"Hello, alice"}}.
这些方法必须是包中的方法 bridge.json (methods);
运行时从那里注册它的路由,然后一个App用
其他方法被拒绝。他们的政策是 permissions.json,至于Go
应用程序。
API
new App(options?)
options.log(error):发生意外错误的地方。默认是这样写的 到stderr, DSM将其保存在包的日志中。浏览器只能看到internal.
app.method(name, handler)
注册一个方法并返回App。 name 是 <group>.<name> 在较低的
例(^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$); 注册两次的名称
抛出。
handler(ctx, params) 返回数据,或者它的一个承诺:任何JSON值,
undefined 是 null. params 请求是 params 当浏览器发送
它。的类型参数 method<P> 只注释它;检查值
在你使用之前。
app.run()
回答运行时在stdin上发送的一个调用,然后退出。它拒绝
以root身份启动(退出状态1) SYNOPKGLAND_BRIDGE 不是 stdio
(退出状态2),因此它只在运行时下运行。当它服务的时候,什么
应用程序写入 process.stdout, console.log 包括,到stderr:
Stdout是通道。开始的子进程 stdio: 'inherit'
仍然会分享它;给它 'ignore' 或管道。
node <entry> --describe 而是打印一行JSON,
{"sdk":"node/…","versions":[1],"methods":[…]},因此包构建可以
对照检查方法 bridge.json 和 permissions.json.
app.describe() 返回相同的对象。
app.serve(input, output)
在任意一对流上响应一个调用,并在结果为时进行解析
写的。 run 在标准输入和标准输出上使用它;测试将其用于
PassThrough 对,播放运行时的一边的通道。
Context
| 成员 | |
|---|---|
user | 打来电话的DSM用户;对于公共方法为空 |
admin | 用户是否是DSM管理员 |
locale | 第一个 Accept-Language 标签,或者为空 |
requestId | 请求的ID, 32个十六进制数字,也在运行时日志中 |
deadline | 当运行时结束请求时,在 Date.now() 毫秒 |
deadlineMs | 运行时发送调用时剩余的时间,以毫秒为单位 |
signal | 一个 AbortSignal 请求结束时中止; signal.reason 是 'deadline' 或者 'canceled' |
cancelled | 是否 signal 是流产 |
runtime(op, params?) | 代表调用者向运行时请求服务;解析到它的结果 |
所有这些都来自运行时经过验证的调用方。一个 Context 属于
一个电话。
错误
throw new AppError(code, message) 浏览器回答
{"success":false,"error":{"code":…,"message":…}}. code 要么是
- 应用程序自己的代码,
^[a-z][a-z0-9_]{0,63}$,除了运行时 预约(session_required,admin_required,app_privilege_denied,auth_unavailable,unknown_method,cross_origin,origin_required,request_timeout,request_canceled,request_too_large,json_required,method_not_allowed,app_unavailable),或 - 的大写拒绝
spec/protocol/v1/errors.json,例如SCOPE_DENIED,这@synopackageland/sdk翻译。
任何其他代码抛出 TypeError,它和其他错误一样,回答
浏览器 internal 并写下为什么会出错。 message 被削减到1024字节
utf - 8。 AppError.badRequest(message) 是 new AppError('bad_request', message).
处理程序抛出的任何其他数据JSON都无法编码(a) BigInt,一个
周期),答案 internal; 错误会变成 log,永远不要切换到浏览器。
服务
ctx.runtime(op, params) 向运行时发送服务请求并解析为
结果,或者 null 当它一无所有时。当运行时拒绝时,它会拒绝
与 ServiceError (op, code, message): unknown_service 对于op来说
运行时不提供,否则服务的代码。一个 ServiceError 一个
当处理程序的代码是协议v1时,它不会捕获到达浏览器
拒绝,就是拒绝。 internal 否则。一个 op 这不是服务名称,或者
params JSON不能编码,拒绝使用 TypeError 在任何事情发生之前
发送。
运行时提供功能的服务 bridge.json
(files.*, portal.status, delegate.*, bg.*, root.*),列于
它们的参数是 spec/bridge/v1.
它们代表经过验证的调用者,在请求的DSM会话中
需要一个,然后以电话结束。
app.method('notes.list', (ctx) => ctx.runtime('files.list', { path: '/home/notes' }))
转移
一个方法 transfers 名字 bridge.json 是HTTP上传或下载。
它的 params 是 {query, upload: {path, size}} 对于上传,使用假脱机文件
阅读,和 {query, download: {path}} 对于一个下载,一个假脱机文件
先写后回答 {name, content_type?}
(spec/bridge/v1)。
截止日期和取消
运行时拥有截止日期。当它通过或浏览器退出时,
运行时发送 cancel 和 ctx.signal 中止;一秒钟后,它杀死了
App的进程组。通过 ctx.signal 来 fetch, fs 还有计时器
他们的请求是:
app.method('feed.fetch', async (ctx) => {
const reply = await fetch('https://example.com/feed.json', { signal: ctx.signal })
return reply.json()
})
一次 signal 中止,等待 ctx.runtime 电话,还有后来的电话
询问,拒绝 ServiceError 代码 request_canceled. 如果通道
早结束, signal 中止与 'canceled'.
数字
应用程序获取值,而不是JSON字面量: 2.50 到达, 2.5,以及整数
大于2^53会失去精度。携带64位的id和大小作为字符串。
测试应用
serve 接受任何流,所以测试可以播放运行时:
import { PassThrough } from 'node:stream'
import { createInterface } from 'node:readline'
const toApp = new PassThrough(), fromApp = new PassThrough()
const lines = createInterface({ input: fromApp })[Symbol.asyncIterator]()
const next = async () => JSON.parse((await lines.next()).value)
app.serve(toApp, fromApp)
toApp.write(JSON.stringify({ t: 'hello', versions: [1] }) + '\n')
await next() // {t: 'hello', version: 1, sdk: 'node/…', methods: [...]}
toApp.write(JSON.stringify({ t: 'call', id: '1', method: 'hello.greet', params: null,
ctx: { user: 'alice', admin: false, locale: 'en', request_id: '0'.repeat(32), deadline_ms: 60000 } }) + '\n')
await next() // {t: 'result', id: '1', ok: true, data: {message: 'Hello, alice'}}
在一个包裹里
SPK的“App”目录为 /var/packages/<package>/target/app。这个
运行时开始 <entry> 有
/var/packages/<node.package>/target/usr/local/bin/node,所以SPK取决于
包裹中心的包裹(install_dep_packages). SDK没有
依赖项:包构建副本 package.json 和 dist/ 来
app/node_modules/@synopackageland/app/,或将应用程序和SDK捆绑在一起
放到一个文件中。
开发
npm ci
npm run generate # src/frames.ts and src/refusals.ts from spec/
npm test # type-checks src and test, then node:test
npm run build # dist/: ESM and .d.ts
src/frames.ts (json-schema-to-typescript,从
spec/bridge/v1/frames.schema.json), src/refusals.ts (从
spec/protocol/v1/errors.json)生成并提交;测试失败
当他们不同于 spec/. scripts/gate.sh 还运行测试
bridge的golden runner和Docker中Node 22上的hello示例
(scripts/node22-test.sh)。
具有桥接能力 webapi:true 和批准的遗嘱执行人档案,现有的
运行时服务接口支持配置文件授予和调用:
const grant = await ctx.runtime('webapi.grant', {scope: 'calendar', lifetime: 'until-revoked'})
const result = await ctx.runtime('webapi.call', {delegation_id: (grant as {delegation_id:string}).delegation_id,
api: 'SYNO.Cal.Event', method: 'list', version: 6, params: {}})
webapi.grants 列出呼叫者的授权; webapi.revoke 撤销。回复
包含完整的DSM信封。没有突变重试;调和未知的结果。
参见spec/bridge/v1和executor/README。Md为固定的权限和限制。
DSM文件会话
默认的Go运行时不再将浏览器DSM会话转发到File
站在HTTP上。其遗留 files.list, files.info 和 files.read
服务返回 unsupported; 包装器永远不会收到DSM证书。
使用前端SDK的同源文件客户端进行交互操作。
后端写入或授予需要独立的调用方绑定授权;
只有浏览器的应用程序不需要执行器。请参阅存储库的
docs/dsm-session-transport.md 查看App迁移检查表。