A typed TypeScript client for the public DFOS API at https://api.dfos.com.
The API itself — endpoints, parameters, response shapes — is documented at
docs.dfos.com/api; this package is the typed way
to call it.
It is three things and nothing else:
openapi.json— a committed snapshot of the API's OpenAPI spec (the same document served athttps://api.dfos.com/openapi.json).src/generated/api.ts— types generated from that snapshot by openapi-typescript. Committed, so the diff in a spec refresh shows what actually changed at the type level.src/index.ts— a thin wrapper around openapi-fetch that sets the base URL and leaves a seam for a customfetch.
The API is the source of truth. This package is derived from it.
npm install @metalabel/dfos-apiRequires Node 22 or newer, or any runtime with a global fetch.
import { createDfosApi } from '@metalabel/dfos-api';
const api = createDfosApi();
const { data, error } = await api.GET('/spaces/{space}', {
params: { path: { space: 'home' } },
});
if (error) {
console.error(error);
} else {
console.log(data.displayName, data.did);
}Paths, path parameters, query parameters, and response bodies are all typed
from the spec. data is present on a 2xx response and error on everything
else — one of the two is always set.
Options:
| Option | Default | What it does |
|---|---|---|
baseUrl |
https://api.dfos.com/v1 |
Point the client at another deployment. |
fetch |
the global fetch |
Supply your own fetch (see "Signed requests" below). |
Everything else — retries, timeouts, caching — is your fetch's job, not this package's.
Most of the API is anonymous GETs, and the default fetch is all you need. The
rest describes or acts as one specific person and takes a proof. Which routes
those are, which proof profiles each accepts, and which action tokens it demands
is declared in the spec itself — the machine-readable convention is the
"Advertising in OpenAPI" section of
API-AUTH,
and the spec's own info.description walks the classes in prose. In outline:
- Anonymous — the default and most of the surface. No header; one projection for everyone.
- Gated —
GET /v1/profile, the four membership routes,GET /v1/credential,GET /v1/feed, andGET /v1/spaces/{space}/posts/{postId}/comments. Each answers about the person the proof names; route semantics live at profile, memberships, and credential. - Optional-auth —
GET /v1/spaces/{space}/postsandGET /v1/spaces/{space}/posts/{postId}: anonymous with no header, and the granting user's own projection with a proof whose grant covers the space underread:posts(conventions). - Writes — every non-
GET: posts (write:posts), comments (write:comments), and upvotes on either (write:upvotes), as the granting user in the spaces the grant covers (conventions).
An application acts for a user by the access they granted it through
Sign In With DFOS: the
setup recipe
takes an application from zero to a credential,
local apps
covers CLIs and agents with no domain to stand behind, and
credentials
explains what the grant carries. A grant names actions and places: the account
tokens live on the API as a whole, while read:posts and the write:* tokens
are space-level and cover either every space the user belongs to or the spaces
named at consent. Consent may narrow an ask, so read what was actually granted
from the credential's attenuation — GET /v1/credential returns it as
attenuation — rather than assuming the request was honored whole.
Calling a gated route or a write on a user's behalf takes that credential plus a
fresh request proof signed per call. Both arrive through the fetch seam —
createApiAuthFetch from @metalabel/dfos-client (v0.54.0+) builds a signing
fetch:
import { createDfosApi } from '@metalabel/dfos-api';
import { createApiAuthFetch } from '@metalabel/dfos-client/api-auth'; // v0.54.0+
const api = createDfosApi({
fetch: createApiAuthFetch({ credential, kid, sign }),
});
const { data, error } = await api.GET('/profile');
const upvote = await api.PUT('/spaces/{space}/posts/{postId}/upvote', {
params: { path: { space: 'home', postId: 'post_6encc4akrze2ah9kntzd9t' } },
});The same signing fetch serves every gated route and every write — which route a
call may use is the credential's business, not the client's. The adapter signs
exactly the Request the client composes, buffering request bodies in full,
refusing plaintext requests to non-loopback hosts, and never following
redirects. From v0.54.0 it also mints a fresh jti for every non-GET request
(its jti option: 'writes' by default, 'always', or 'never'), which is
what the write tier requires — what the API asks of a write, and how it answers
a repeated one, is in
conventions.
The byte contract and the two headers are specified in
API-AUTH; the
signing itself lives in @metalabel/dfos-client, not here.
Reading and writing your own data takes no credential: every route that accepts
one except GET /v1/credential also accepts a bare identity proof signed by one
of your own keys, which opens that DID's own data and own actions
(conventions).
signApiIdentityRequest and buildApiIdentityHeaders from
@metalabel/dfos-client/api-auth produce the proof and its header, which you set
on your own fetch; generateJti() mints the jti a write signed this way
carries.
The API adds fields and enum members without a version bump, so write clients that tolerate what they don't recognize. The full contract — what can change without notice and what never will — is docs.dfos.com/docs/api/compatibility.
pnpm update-specThat regenerates the spec from the platform monorepo's contract (a local
checkout, DFOS_PLATFORM_REPO, default ../metalabel-dfos — maintainers
only), rewrites openapi.json (2-space indent, trailing newline, so diffs
stay readable), and regenerates src/generated/api.ts. Pass --live to
fetch the deployed spec at https://api.dfos.com/openapi.json instead.
Refreshes are request-driven from the platform repo rather than polled on a
schedule, and track the merged contract rather than the deployed API — so
pre-1.0, a fresh snapshot may briefly describe an endpoint that has merged
but not yet deployed. CI checks the reverse direction: the committed types
must be exactly what the committed snapshot generates.
- API reference: https://docs.dfos.com/api
- Route guides: profile, memberships, credential, compatibility
- Sign In With DFOS: setup, credentials, local apps
- Protocol specs: SIWD, API-AUTH
- DFOS CLI (
dfos login, credentials for local tools): https://github.com/metalabel/dfos/tree/main/packages/dfos-cli - SIWD demo, end to end: https://github.com/metalabel/dfos/tree/main/examples/siwd-demo
- OpenAPI spec: https://api.dfos.com/openapi.json
MIT