Skip to content

barddoo/trustline

Repository files navigation

Trustline

npm version docs

Service identity and authorization for modern JavaScript runtimes.

Warning: Trustline is not production-ready yet. Do not use it in production environments. Use it only in side projects.

Trustline is a machine-to-machine authentication library for internal services. It is designed around dedicated core and integration entry points:

  • trustline: provider, guard, memory storage, and shared core exports
  • trustline/client: token fetching and caching for outgoing requests
  • trustline/frameworks/*: framework adapters for receiving services
  • trustline/adapters/*: SQL storage adapters

The package now ships the first full stack: provider, client, guard, framework adapters, memory storage, and SQL storage adapters for SQLite, Postgres, and MySQL.

Installation

Trustline is intended to be consumed as the trustline package:

bun add trustline

Or with npm:

npm install trustline

Install only the integrations you use. For example, Express users install express; SQLite users install better-sqlite3 and kysely.

Example installs:

npm install trustline express
npm install trustline better-sqlite3 kysely

If you are working from this repository before package publication, build the package locally and install or link it from the repo source.

Quick start

Provider:

import { createProvider, memoryStorage } from "trustline";

const provider = createProvider({
  issuer: "https://auth.internal",
  storage: memoryStorage(),
  env: "production",
});

const service = await provider.clients.create({
  name: "order-processor",
  scopes: ["read:orders", "write:inventory"],
});

Provisioning from SQLite-backed provider state can also be done with the standalone admin binary:

trustline-cli client create \
  --issuer https://auth.internal \
  --sqlite-path ./trustline.sqlite \
  --name order-processor \
  --scope read:orders \
  --scope write:inventory

Client:

import { createClient } from "trustline/client";

const client = createClient({
  tokenUrl: "https://auth.internal/token",
  clientId: service.clientId,
  clientSecret: service.clientSecret,
  audience: "inventory-service",
});

const token = await client.getToken();

Guard:

import { createGuard } from "trustline";

const guard = createGuard({
  issuer: "https://auth.internal",
  audience: "inventory-service",
  scopes: ["read:orders"],
  env: "production",
});

const identity = await guard.verify(token);

Trustline derives the JWKS endpoint automatically for verification:

issuer: https://auth.internal
jwks:   https://auth.internal/.well-known/jwks.json

Operational controls already implemented in the current provider include requested-scope narrowing, token revocation by jti, client disable and re-enable, client token cutoffs, and signing key rotation with overlap windows.

Bun

Trustline does not need a Bun-specific adapter. Bun already uses the standard Web Request and Response APIs, so use the provider's handle() method directly and call guard.verify() inside your fetch handler.

import { createGuard, createProvider, memoryStorage } from "trustline";

const provider = createProvider({
  issuer: "https://auth.internal",
  storage: memoryStorage(),
});

Bun.serve({
  port: 3000,
  fetch: provider.handle,
});

const guard = createGuard({
  issuer: "https://auth.internal",
  audience: "inventory-service",
});

Bun.serve({
  port: 4000,
  fetch: async (request) => {
    const header = request.headers.get("authorization");
    const token = header?.replace(/^Bearer\s+/, "") ?? "";
    const identity = await guard.verify(token);

    return Response.json({
      caller: identity.name ?? identity.clientId,
    });
  },
});

Express

import express from "express";
import { createGuard, createProvider, memoryStorage } from "trustline";
import {
  createExpressGuard,
  createExpressProvider,
  type TrustlineRequest,
} from "trustline/frameworks/express";

const app = express();

const provider = createProvider({
  issuer: "https://auth.internal",
  storage: memoryStorage(),
});

const guard = createGuard({
  issuer: "https://auth.internal",
  audience: "inventory-service",
});

app.use(createExpressProvider(provider));
app.use(createExpressGuard(guard));

app.get("/internal", (request: TrustlineRequest, response) => {
  response.json({
    caller: request.trustline?.name ?? request.trustline?.clientId,
  });
});

API

Current public API includes:

interface ProviderOptions {
  issuer: string;
  storage: StorageAdapter;
  signing?: {
    algorithm?: "ES256" | "RS256";
    privateKey?: string;
    keyId?: string;
  };
  token?: {
    ttl?: number;
  };
  hooks?: {
    onEvent?(event: ProviderEvent): void | Promise<void>;
  };
  env?: string;
}

interface GuardOptions {
  issuer: string;
  jwksUrl?: string;
  audience?: string | string[];
  scopes?: string[];
  env?: string;
  clockTolerance?: number;
  hooks?: {
    onEvent?(event: GuardEvent): void | Promise<void>;
  };
}

interface ServiceIdentity {
  clientId: string;
  name: string | null;
  scopes: string[];
  env: string | null;
  raw: Record<string, unknown>;
}

interface ProviderClient {
  clientId: string;
  name: string;
  scopes: string[];
  active: boolean;
  lastSeenAt: Date | null;
  secretLastRotatedAt: Date | null;
  nextSecretExpiresAt: Date | null;
  hasPendingSecretRotation: boolean;
}

Adapter surface:

  • provider.handle(request)
  • provider.clients.create/list/get/rename/updateScopes/rotateSecret/revoke/disable/enable/invalidateTokensBefore/clearTokensInvalidBefore
  • provider.keys.rotate
  • provider.tokens.revoke
  • guard.verify(token)
  • createExpressProvider(provider)
  • createExpressGuard(guard)
  • createFastifyProvider(provider)
  • createFastifyGuard(guard)
  • createHonoProvider(provider)
  • createHonoGuard(guard)

Supported signing algorithms:

  • RS256
  • ES256

Bundled storage adapters via dedicated subpaths:

  • memoryStorage()
  • sqliteStorage(path | database)
  • postgresStorage(pool)
  • mysqlStorage(pool)

SQL adapters follow the Better Auth-style pattern of receiving ready-made database handles:

import Database from "better-sqlite3";
import { createPool as createMysqlPool } from "mysql2";
import { Pool as PostgresPool } from "pg";
import { mysqlStorage } from "trustline/adapters/mysql";
import { postgresStorage } from "trustline/adapters/postgres";
import { sqliteStorage } from "trustline/adapters/sqlite";

const sqlite = sqliteStorage(new Database("./trustline.sqlite"));
const postgres = postgresStorage(
  new PostgresPool({ connectionString: process.env.DATABASE_URL })
);
const mysql = mysqlStorage(createMysqlPool(process.env.DATABASE_URL!));

trustline/client now accepts an optional async token cache interface for sharing access tokens across instances while keeping the default in-memory cache behavior.

Client secrets are only returned by provider.clients.create() and provider.clients.rotateSecret(). provider.clients.get() and provider.clients.list() return ProviderClient metadata and never expose hashed secrets.

Development

Build the package:

bun run build

Run type checks:

bun run typecheck

Run tests:

bun run test

Run formatting and lint checks:

bun run check

License

MIT

About

Service identity and authorization for modern JavaScript runtimes.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors