Skip to content

Wrap a library as an adapter

You want to use an npm library (or a remote HTTP API) from Bynk. Wrap it in an adapter: declare the capability contract in Bynk, implement it in a TypeScript binding, and consume it like any other capability.

Name the adapter for the capability it provides. Declare the capability, any boundary types, and an external (bodiless) provides. Name the binding module and pin its npm dependency.

Configuration the binding needs — here the signing secret — is a capability dependency (consumes bynk { Secrets } + given Secrets), not an operation parameter (v0.18).

adapter tokens {
binding "./tokens.binding.ts" requires { "jose": "^5" }
consumes bynk { Secrets }
exports capability { Jwt }
exports transparent { Claims, JwtError }
type Claims = { sub: String, exp: Int }
type JwtError = enum { Invalid, Expired }
capability Jwt {
fn sign(claims: Claims) -> Effect[String]
fn verify(token: String) -> Effect[Result[Claims, JwtError]]
}
provides Jwt = JoseJwt given Secrets
}

The binding lives beside the adapter source at the path the binding clause names. implements Jwt against the generated interface is the contract — tsc --strict enforces it. Construct boundary values through the emitted constructors (Ok/Err, the sum type’s JwtError.Invalid, a Claims object literal) — never hand-rolled tag shapes.

The provider’s given names arrive as a by-name deps object in the class constructor — the keys are the given names, and tsc checks them.

tokens.binding.ts
import * as jose from "jose";
import type { Jwt, Claims } from "./tokens.js";
import { JwtError } from "./tokens.js"; // emitted variant constructors
import type { Secrets } from "./bynk.js";
import { Ok, Err, type Result } from "./runtime.js";
export class JoseJwt implements Jwt {
constructor(private deps: { Secrets: Secrets }) {}
async sign(claims: Claims): Promise<string> {
return await new jose.SignJWT({ ...claims })
.setProtectedHeader({ alg: "HS256" })
.sign(new TextEncoder().encode(await this.secret()));
}
async verify(token: string): Promise<Result<Claims, JwtError>> {
try {
const { payload } = await jose.jwtVerify(
token,
new TextEncoder().encode(await this.secret()),
);
return Ok({ sub: String(payload.sub), exp: Number(payload.exp) });
} catch {
return Err(JwtError.Invalid);
}
}
private async secret(): Promise<string> {
const s = await this.deps.Secrets.get("JWT_SECRET");
return s.tag === "Some" ? s.value : "";
}
}

A remote API is the same shape with no npm dependency — drop the requires clause and take given bynk.Fetch instead of calling the global fetch, mapping the typed Response to a Result.

context auth.sessions {
consumes tokens { Jwt } -- flatten `Jwt` into the local namespace
service login {
on call() -> Effect[String] given Jwt {
let token <- Jwt.sign(Claims { sub: "u1", exp: 0 })
token
}
}
}

Compile: the adapter’s interface module and the binding are emitted into the output, the npm dependency is folded into package.json, and the composition root instantiates the binding’s class and injects it. To swap the real implementation in a test, mocks Jwt = … { … } at the same seam.