Skip to content

Latest commit

 

History

History
195 lines (156 loc) · 8.13 KB

File metadata and controls

195 lines (156 loc) · 8.13 KB

@netscript/service

JSR CI Docs

The service runtime for NetScript: turn an oRPC router into a running Hono service with health probes, OpenAPI, Scalar docs, request tracing, and graceful shutdown — in one call.

A production service is never just its handlers. It needs CORS, request logging, an OpenAPI document, live/ready health probes an orchestrator can poll, tracing on every request, and a shutdown path that drains in-flight work. This package materializes all of it from the oRPC router you already have: defineService() stands up the full runtime in one call, and createService() composes the same stages explicitly when a service needs a bespoke stack.

Authentication and authorization ship as an opt-in subpath with provider-agnostic ports, so a service that needs guarding adds it without dragging auth machinery into every service that does not.

Why teams use it

  • One-call presetdefineService(router, options) wires CORS, logging, OpenAPI JSON, Scalar docs, RPC, service info, and health, then starts the listener and returns a RunningService handle with addr and an idempotent stop().
  • Fluent buildercreateService(router, config) composes the same stages step by step, then serve() starts a listener or build() returns a mountable app.
  • Health probeswithHealth() adds /health, /health/live, and /health/ready; healthChecks.database, .kv, .service, and .custom cover common dependencies.
  • Graceful lifecycleonShutdown() registers LIFO teardown hooks; serve() drains in-flight requests, installs SIGINT/SIGTERM handlers, and accepts an external AbortSignal.
  • One app-wide budgetcreateRuntimeHost() invokes existing service, worker, queue, and database drains in deterministic phase order and returns one aggregate report.
  • Tracing on every request — the builder registers tracing middleware as the outermost layer on every service, so each request gets a server span with W3C propagation and the service name recorded, with no per-service wiring.
  • Opt-in auth./auth ships authentication and authorization ports plus static-credential, trusted-header, and scope-authorizer factories, kept off the import graph until used.

Architecture

flowchart LR
    R["oRPC router"] --> D["defineService()<br/>or createService()"]
    D --> M["Middleware stack<br/>tracing · CORS · logging · auth"]
    M --> E["Endpoints<br/>/rpc · /api · OpenAPI · Scalar docs"]
    M --> H["Health<br/>/health · /health/live · /health/ready"]
    D --> G["Graceful shutdown<br/>drain · LIFO hooks · signals"]
Loading

Install

deno add jsr:@netscript/service@<version>

Pin <version> to match your installed CLI; bare jsr:@netscript/* specifiers do not resolve on the pre-release line. Generated NetScript service entrypoints already import the pinned entry.

Quick example

import { defineService } from '@netscript/service';
import { router } from './router.ts';

// One call materializes the Hono + oRPC runtime and starts the listener:
// CORS, request logging, OpenAPI JSON, Scalar docs, RPC, service info, and health.
const service = await defineService(router, {
  name: 'users',
  version: '1.0.0',
  port: 3001,
  openapi: { title: 'Users API', description: 'User management service' },
});

// RunningService handle: addr + idempotent graceful stop() for tests and supervisors.
console.log(`listening on :${service.addr.port}`);
await service.stop();

Compose every in-process runtime behind one bounded shutdown handle without replacing its own drain:

import { createRuntimeHost } from '@netscript/service';

const host = createRuntimeHost({
  timeoutMs: 15_000,
  drains: [
    { id: 'api', phase: 'service', drain: () => service.stop() },
    { id: 'jobs', phase: 'workers', drain: () => workers.stop('shutdown') },
    { id: 'messages', phase: 'queue', drain: () => queue.stop() },
    { id: 'primary-db', phase: 'database', drain: () => database.disconnect() },
  ],
});

const report = await host.shutdown('SIGTERM');

The host drains service → workers → queue → database, preserving registration order inside each phase. Rejected drains are reported and do not prevent later phases. If the one shared budget expires, the active outcome is timed-out, remaining drains are skipped, and shutdown() returns without waiting indefinitely for the slow resource.

Reach for createService() when a service needs explicit, stage-by-stage composition — and pull in ./auth to guard it:

import { createService } from '@netscript/service';
import {
  createScopeAuthorizer,
  createStaticCredentialAuthenticator,
} from '@netscript/service/auth';

const authenticator = createStaticCredentialAuthenticator({
  credentials: {
    'local-token': { subject: 'service:orders', scopes: ['orders:read'], roles: ['service'] },
  },
});

const authorizer = createScopeAuthorizer({
  rules: [{
    match: (request) => request.path.startsWith('/api/orders'),
    requireScopes: ['orders:read'],
  }],
});

const running = await createService(router, { name: 'orders', version: '1.0.0' })
  .withAuthn({ authenticator })
  .withAuthz({ authorizer })
  .withRPC()
  .withHealth()
  .serve({ port: 3001 });

await running.stop();

The defineService() preset accepts the same ports through its auth option, so generated entrypoints opt in without leaving the one-call surface:

import { defineService } from '@netscript/service';
import { createScopeAuthorizer, createTrustedHeaderAuthenticator } from '@netscript/service/auth';

const running = await defineService(router, {
  name: 'orders',
  port: 3001,
  auth: {
    authn: {
      authenticator: createTrustedHeaderAuthenticator({
        subjectHeader: 'x-authenticated-user',
        scopesHeader: 'x-authenticated-scopes',
      }),
    },
    authz: {
      authorizer: createScopeAuthorizer({
        rules: [{
          match: (request) => request.path.startsWith('/api/orders'),
          requireScopes: ['orders:read'],
        }],
      }),
    },
  },
});

await running.stop();

API at a glance

Entry What it gives you
. defineService, createService, createRuntimeHost, healthChecks, HEALTH_STATUS, handler factories (createRPCHandler, createOpenAPISpec, createScalarDocs, …)
./auth createStaticCredentialAuthenticator, createTrustedHeaderAuthenticator, createScopeAuthorizer, and the authn/authz port types

The always-current symbol list is deno doc jsr:@netscript/service@<version>.

Docs

Compatibility

Requires Deno 2.x — the runtime listens through Deno.serve and installs Deno.addSignalListener handlers. Services need --allow-net (listener and health probes) and --allow-env; database and KV health checks add the permissions of the client they probe.

License

Apache-2.0 — see LICENSE. Published to JSR with cryptographically verified provenance.