npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@synopackageland/app

v0.19.1

Published

Node SDK for Synology Package Land Apps: the backend methods behind the synopkgland Go runtime (channel spec/bridge/v1)

Readme

@synopackageland/app

The Node SDK for the backend of a Synology Package Land App written in TypeScript or JavaScript. Node 18 or later (DSM Package Center's Node.js_v18, Node.js_v20, Node.js_v22), ESM only, no runtime dependencies.

The package's CGI is the synopkgland Go runtime (bridge/cmd/runtime). It checks the request's origin, the DSM session, the entry gate, the method's policy in permissions.json and the deadline, and only then starts the App, as the package account, and hands it the call over the bridge channel (spec/bridge/v1): JSON frames on stdin and stdout, one process per call. The App never runs as root and never sees the DSM session; what needs the session it asks of the runtime with ctx.runtime.

An App

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()

The browser calls it with @synopackageland/sdk as any other method: {"method":"hello.greet"} answers {"success":true,"data":{"message":"Hello, alice"}}.

The methods must be the ones in the package's bridge.json (methods); the runtime registers its routes from there, and an App that says hello with other methods is refused. Their policy is in permissions.json, as for a Go App.

API

new App(options?)

  • options.log(error): where an unexpected error goes. The default writes it to stderr, which DSM keeps in the package's log. The browser only sees internal.

app.method(name, handler)

Registers a method and returns the App. name is <group>.<name> in lower case (^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$); a name registered twice throws.

handler(ctx, params) returns the data, or a promise of it: any JSON value, undefined is null. params is the request's params as the browser sent it. The type parameter of method<P> only annotates it; check the value before you use it.

app.run()

Answers the one call the runtime sends on stdin, then exits. It refuses to start as root (exit status 1) and when SYNOPKGLAND_BRIDGE is not stdio (exit status 2), so it only runs under the runtime. While it serves, what the App writes to process.stdout, console.log included, goes to stderr: stdout is the channel. A child process started with stdio: 'inherit' would still share it; give it 'ignore' or pipes.

node <entry> --describe prints one JSON line instead, {"sdk":"node/…","versions":[1],"methods":[…]}, so the package build can check the methods against bridge.json and permissions.json. app.describe() returns the same object.

app.serve(input, output)

Answers one call on any pair of streams and resolves once the result is written. run uses it on stdin and stdout; tests use it on a PassThrough pair, playing the runtime's side of the channel.

Context

| Member | | |---|---| | user | The DSM user who called; empty for a public method | | admin | Whether the user is a DSM administrator | | locale | The first Accept-Language tag, or empty | | requestId | The request's ID, 32 hex digits, also in the runtime's log | | deadline | When the runtime ends the request, in Date.now() milliseconds | | deadlineMs | The time that was left when the runtime sent the call, in milliseconds | | signal | An AbortSignal aborted when the request is over; signal.reason is 'deadline' or 'canceled' | | cancelled | Whether signal is aborted | | runtime(op, params?) | Asks the runtime for a service on behalf of the caller; resolves to its result |

All of it comes from the runtime's verified caller. A Context belongs to one call.

Errors

throw new AppError(code, message) answers the browser {"success":false,"error":{"code":…,"message":…}}. code is either

  • the App's own code, ^[a-z][a-z0-9_]{0,63}$, except the runtime's reserved ones (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), or
  • an upper-case refusal of spec/protocol/v1/errors.json, such as SCOPE_DENIED, which @synopackageland/sdk translates.

Any other code throws TypeError, which, like any other error, answers the browser internal and writes why to stderr. message is cut to 1024 bytes of UTF-8. AppError.badRequest(message) is new AppError('bad_request', message).

Anything else a handler throws, data JSON cannot encode (a BigInt, a cycle), answers internal; the error goes to log, never to the browser.

Services

ctx.runtime(op, params) sends the runtime a service request and resolves to its result, or null when it has none. When the runtime refuses it rejects with ServiceError (op, code, message): unknown_service for an op the runtime does not offer, otherwise the service's code. A ServiceError a handler does not catch reaches the browser when its code is a protocol v1 refusal, and is internal otherwise. An op that is not a service name, or params JSON cannot encode, rejects with TypeError before anything is sent.

The runtime offers the services of the capabilities in bridge.json (files.*, portal.status, delegate.*, bg.*, root.*), listed with their params in spec/bridge/v1. They act for the verified caller, with the request's DSM session where they need one, and end with the call.

app.method('notes.list', (ctx) => ctx.runtime('files.list', { path: '/home/notes' }))

Transfers

A method transfers names in bridge.json is an HTTP upload or download. Its params is {query, upload: {path, size}} for an upload, a spool file to read, and {query, download: {path}} for a download, a spool file to write before answering {name, content_type?} (spec/bridge/v1).

Deadline and cancel

The runtime owns the deadline. When it passes, or the browser goes away, the runtime sends cancel and ctx.signal aborts; one second later it kills the App's process group. Pass ctx.signal to fetch, fs and timers so that they stop with the request:

app.method('feed.fetch', async (ctx) => {
  const reply = await fetch('https://example.com/feed.json', { signal: ctx.signal })
  return reply.json()
})

Once signal aborts, pending ctx.runtime calls, and later ones without asking, reject with ServiceError code request_canceled. If the channel ends early, signal aborts with 'canceled'.

Numbers

An App gets values, not JSON literals: 2.50 arrives as 2.5, and integers above 2^53 lose precision. Carry 64-bit IDs and sizes as strings.

Testing an App

serve takes any streams, so a test can play the runtime:

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'}}

In a package

The App directory of the SPK is /var/packages/<package>/target/app. The runtime starts <entry> there with /var/packages/<node.package>/target/usr/local/bin/node, so the SPK depends on that Package Center package (install_dep_packages). The SDK has no dependencies: the package build copies package.json and dist/ to app/node_modules/@synopackageland/app/, or bundles the App and the SDK into one file.

Development

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, from spec/bridge/v1/frames.schema.json) and src/refusals.ts (from spec/protocol/v1/errors.json) are generated and committed; a test fails when they differ from spec/. scripts/gate.sh also runs the tests, the bridge's golden runner and the hello example on Node 18 in Docker (scripts/node18-test.sh).

With bridge capability webapi:true and approved executor profiles, the existing runtime service interface supports profile grants and calls:

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 lists the caller's grants; webapi.revoke revokes one. Replies contain the full DSM envelope. No mutation retries; reconcile unknown outcomes. See spec/bridge/v1 and executor/README.md for the fixed permissions and limits.