@europeum-ebsi/api-contract
v1.0.0-rc.2
Published
Generated types and scope grammar for the EBSI API's wire contract
Readme
EBSI API contract
Generated types and access-token scope grammar for @europeum-ebsi/api's wire contract.
Table of Contents
Installation
npm install @europeum-ebsi/api-contractUsage
Types
paths / components / operations / webhooks are generated from openapi.yaml (@europeum-ebsi/api's hand-written, spectral-linted OpenAPI 3.1 spec) via openapi-typescript. A small set of generic helpers look up a single endpoint's request/response shape by its path and HTTP method without hand-navigating the full paths tree:
import type { RequestBody, SuccessResponse } from "@europeum-ebsi/api-contract";
type CreateAccessTokenBody = RequestBody<"/auth/token", "post">;
type AccessTokenResponse = SuccessResponse<"/auth/token", "post">;Regenerate src/api-types.ts with pnpm run gen:api-types whenever openapi.yaml changes -- openapi.yaml documents @europeum-ebsi/api's actual implemented behaviour, not the other way around.
Not every route the API serves is part of the contract: GET / (a plain-text liveness string), GET /health (the Terminus health check, superseded for external monitoring by the documented GET /alive), and this document's own discovery endpoints (GET /openapi.json / GET /openapi.yaml) are deliberately left undocumented, since they're infrastructure rather than API surface.
OpenAPI template
openapi.yaml is a Mustache template. The tags are placed so that the raw file stays valid YAML, and only two kinds are supported (no unescaped variables, partials or comments):
- Sections named after a feature flag key, for the parts of the spec that document a feature behind that flag. They are rendered only when the flag is on (
{{#flag}}) or off ({{^flag}}). - Variables in
UPPER_SNAKE_CASE, such as{{DID_REGISTRY_CONTRACT_ADDRESS}}, for the values that depend on the environment.
description: |
Always documented.
{{#my-flag}}
Documented only when `my-flag` is on.
{{/my-flag}}
{{^my-flag}}
Documented only when `my-flag` is off.
{{/my-flag}}
The default contract address is `{{DID_REGISTRY_CONTRACT_ADDRESS}}`.@europeum-ebsi/api-contract/openapi exports the renderer. A flag that isn't given is considered off. The optional third argument gives the values of the variables; a variable that isn't given is rendered as <NAME>, e.g. <DID_REGISTRY_CONTRACT_ADDRESS>. A name can't be used both as a flag and as a variable:
import { readFileSync } from "node:fs";
import {
getOpenApiFeatureFlags,
getOpenApiVariables,
renderOpenApi,
} from "@europeum-ebsi/api-contract/openapi";
const template = readFileSync("openapi.yaml", "utf8");
getOpenApiFeatureFlags(template); // ["my-flag"]
getOpenApiVariables(template); // ["DID_REGISTRY_CONTRACT_ADDRESS"]
renderOpenApi(template, { "my-flag": true });
renderOpenApi(
template,
{ "my-flag": true },
{ DID_REGISTRY_CONTRACT_ADDRESS: "0x..." },
);@europeum-ebsi/api renders it at startup with the values of its feature flags and the configured contract addresses when serving GET /openapi.yaml and GET /openapi.json. The types generated by pnpm run gen:api-types come from the rendering with all the flags on, and pnpm run lint lints the renderings with all the flags on and all of them off.
Scopes
@europeum-ebsi/api-contract/scopes exports the access-token scope grammar (parsing, combinability rules, DCQL credential-query id derivation) shared between @europeum-ebsi/api's own authorisation logic and any client building requests against it (e.g. @europeum-ebsi/sdk).
import { parseScopeString } from "@europeum-ebsi/api-contract/scopes";
parseScopeString("openid did-registry:write");It also exports, per registry, the scope each contract function requires (DID_REGISTRY_FUNCTION_SCOPE, ISSUERS_REGISTRY_FUNCTION_SCOPE, POLICIES_REGISTRY_FUNCTION_SCOPE, SCHEMAS_REGISTRY_FUNCTION_SCOPE, TIMESTAMP_FUNCTION_SCOPE, TRACK_AND_TRACE_FUNCTION_SCOPE): the API's Ledger service rejects any function not listed, and clients use them to mint the right token. The tables are checked at compile time against each contract's ABI (see the FunctionScopes type). Look a function up with functionScope(table, functionName):
import {
DID_REGISTRY_FUNCTION_SCOPE,
functionScope,
} from "@europeum-ebsi/api-contract/scopes";
functionScope(DID_REGISTRY_FUNCTION_SCOPE, "insertDidDocument"); // "did-registry:invite"License
Copyright (C) 2026 European Union
This program is free software: you can redistribute it and/or modify it under the terms of the EUROPEAN UNION PUBLIC LICENCE v. 1.2 as published by the European Union.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the EUROPEAN UNION PUBLIC LICENCE v. 1.2 for further details.
You should have received a copy of the EUROPEAN UNION PUBLIC LICENCE v. 1.2. along with this program. If not, see https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12.
