@venn-lang/auth
v0.7.5
Published
The auth namespace: build the header a request needs, or fetch a token for it.
Readme
@venn-lang/auth
The
authnamespace: build the header a request needs, or fetch a token for it.
Six of the seven verbs are pure computation, headers, signatures and codes worked out from what you
hand them. The seventh, auth.oauth2, is the one that talks to a token endpoint, and it goes
through the AuthClient port so the exchange is injected rather than hard-wired. Real cryptography
uses the global Web Crypto (crypto.subtle), so nothing here imports node:*.
Install
Nothing to install. @venn-lang/auth is part of the standard library, and the CLI and the language
server both load @venn-lang/stdlib, which lists every stdlib plugin. A file reaches the
namespace with one line.
import { auth } from "venn/auth"Usage
module demo.auth
import { auth } from "venn/auth"
import { assert } from "venn/assert"
flow "Signed in" {
step "a bearer header carries the token" {
const header = auth.bearer "tok123"
expect header.Authorization == "Bearer tok123"
}
step "a service account gets a token" {
const token = auth.oauth2 "svc-account" { grant: "client_credentials", scope: "orders:write" }
expect token.expires_in > 0
}
}Verbs
| Verb | Call | Result |
| --- | --- | --- |
| auth.bearer | auth.bearer "tok123" | auth.Headers |
| auth.basic | auth.basic "alice" "s3cret" | auth.Headers |
| auth.apikey | auth.apikey "KEY" { header: "X-Token" } | auth.Headers |
| auth.hmac | auth.hmac "s3cret" "payload" { algo: "sha512" } | string, lowercase hex |
| auth.totp | auth.totp "seed" { at: 0, period: 30, digits: 6 } | string |
| auth.jwt | auth.jwt { payload: { sub: "42" }, secret: "k" } | string |
| auth.oauth2 | auth.oauth2 "svc-account" { grant: "client_credentials" } | auth.Token |
bearer returns { Authorization: "Bearer <token>" }.
basic base64-encodes the UTF-8 bytes of user:pass into
{ Authorization: "Basic <…>" }, which is what RFC 7617 §2 requires. It used to call btoa on the
string, which encodes latin-1: auth.basic "user" "señha" went out as dXNlcjpzZfFoYQ== rather
than dXNlcjpzZcOxaGE=, so the far end read a different password, answered 401, and nothing here
said why. A password outside latin-1 threw a DOMException with no code and no line at all.
http.get "…" { basic: … } uses the same encoder, so the two agree byte for byte.
apikey puts the key under a header of your choosing. The header option defaults to
X-API-Key, so auth.apikey "KEY" gives { "X-API-Key": "KEY" }.
hmac takes the secret first and the payload second, so auth.hmac "s3cret" "payload" keys the
HMAC with s3cret. The algo option accepts sha1, sha256, sha384 and sha512, case and
dashes ignored. A name it does not recognise is refused with VN7005: signing a typo with SHA-256
produces a signature nothing verifies and says nothing about why. That list of names, and that
refusal, are the ones crypto.hmac uses; there is one of each in the repository.
totp computes an RFC 6238 code and is deterministic for a fixed at, which makes it usable in
a test. at is the time to compute at and period the step, both in seconds; digits defaults
to 6 and the result is zero-padded, which is why it is a string and not a number.
jwt takes nothing positionally. payload and secret are required options, header is
optional and is merged over the default { alg: "HS256", typ: "JWT" }. alg chooses the
signature, not just what the token claims: { header: { alg: "HS512" } } is signed with SHA-512.
It used to write the caller's alg into the signed bytes and sign with SHA-256 whatever it said, so
crypto.jwt.verify read the claim, hashed with SHA-512 and answered false for a token nothing had
tampered with. An alg no HMAC digest answers to is refused with VN7005 rather than minting a token
nothing can check.
oauth2 takes the principal positionally and grant, tokenUrl, scope and refresh as
options. It is the only verb here that reaches a port.
Where the digests come from
hmac, totp and jwt reach CryptoEnginePort for every digest, the same port crypto.hmac and
crypto.jwt.sign use, declared in @venn-lang/sdk because a plugin may not depend on
another plugin. They used to call the global crypto.subtle directly, which meant a host that bound
createFakeCryptoEngine made crypto.* reproducible and left auth.* on real WebCrypto: half a
run replayable, half not. A test that swaps the engine in now reaches all three.
The types it publishes
| Type | Shape |
| --- | --- |
| auth.Headers | map<string>. A map rather than a record, because auth.apikey names its own header and the key is only known at the call site. |
| auth.Token | { access_token, token_type, expires_in }, the OAuth2 wire shape. |
The AuthClient port
| | |
| --- | --- |
| id | venn.port.auth-client |
| version | 1 |
| requires | net |
| methods | token |
Two implementations ship together, which is what makes this a port rather than a module with a good interface:
createFakeAuthClient()returns a canned token derived from the principal, with no network. This is the one@venn-lang/stdlibbinds, soauth.oauth2resolves offline. Pass{ token: { expires_in: 60 } }to override any field of what it hands back.createRealAuthClient()is a stub. Every call throws aVennErrorwith codeVN8090, because this repository is the language and ships no live token exchange.
Both run the same conformance suite, authClientConformance in src/clients/auth-client.suite.ts:
a token resolves with a string access_token, a string token_type and a numeric expires_in.
The plugin as a whole declares requires: ["net"], so a host without the net capability is
refused with a legible diagnostic before the run starts, rather than failing somewhere inside
auth.oauth2.
API
| Export | What it is |
| --- | --- |
| authPlugin | The plugin definition: namespace auth, requires: ["net"]. Also the default export. |
| authActions | The seven action definitions: bearer, basic, apikey, hmac, totp, jwt, oauth2. |
| AuthClientPort | The port descriptor. |
| AuthClient | The port interface: token(request). |
| OAuthTokenRequest | What the port is asked: principal, and the optional grant, tokenUrl, scope, refresh. |
| OAuthToken | What it answers: { access_token, token_type, expires_in }. |
| createFakeAuthClient({ token? }) | The deterministic double. |
| createRealAuthClient() | The real client, stubbed to throw VN8090. |
| authTypeDefs | The TypeSpecs for auth.Headers and auth.Token, which the checker and the LSP read. |
Binding a different client means one entry in the runner's port list:
import { AuthClientPort, createFakeAuthClient } from "@venn-lang/auth";
import { createRunner } from "@venn-lang/runtime";
const runner = createRunner({
host,
plugins,
sink,
uri,
ports: [{ port: AuthClientPort, impl: createFakeAuthClient() }],
});See also
@venn-lang/httpfor the requests these headers are attached to.@venn-lang/cryptofor hashing and encoding outside an auth flow.@venn-lang/sdkfordefinePluginanddefineAction.
