OpenAPI
Your service speaks gRPC and Connect, but Swagger UI, API catalogs and SDK generators may need an OpenAPI document. Generate it from the same proto contracts instead of describing the API twice.
Proto options → OpenAPI security
Change the annotation. Inspect the spec.
Keep the RPC unchanged. Switch its auth option and see what the overlay writes into the generated OpenAPI document.
Selected proto method
resolveMethodAuth(method)
- public
true- internal
false- policy
(unset)
OpenAPI security fragment
security: [] explicitly removes the operation’s credential requirement.
{
"paths": {
"/shop.v1.InventoryService/CheckStock": {
"post": {
"security": [],
"x-connectum-public": true
}
}
}
}
Live authz overlay using resolveMethodAuth from @connectum/auth. The base spec is pre-generated; buf/protoc and an RPC server do not run in this page. The controls edit compiled method options, not arbitrary proto text.
Successful build-time overlay. Editing requires JavaScript.
Inspect the complete document after this overlay
The fragment above shows only the fields written by the overlay. The full document retains the generated request, response and error schemas.
{
"openapi": "3.1.0",
"info": {
"title": "shop.v1",
"version": "1.0.0"
},
"paths": {
"/shop.v1.InventoryService/CheckStock": {
"post": {
"tags": [
"shop.v1.InventoryService"
],
"summary": "CheckStock",
"operationId": "shop.v1.InventoryService.CheckStock",
"parameters": [
{
"name": "Connect-Protocol-Version",
"in": "header",
"required": true,
"schema": {
"$ref": "#/components/schemas/connect-protocol-version"
}
},
{
"name": "Connect-Timeout-Ms",
"in": "header",
"schema": {
"$ref": "#/components/schemas/connect-timeout-header"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/shop.v1.CheckStockRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/shop.v1.CheckStockResponse"
}
}
}
},
"default": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/connect.error"
}
}
}
}
},
"security": [],
"x-connectum-public": true
}
},
"/shop.v1.InventoryService/WatchStock": {}
},
"components": {
"schemas": {
"connect-protocol-version": {
"type": "number",
"title": "Connect-Protocol-Version",
"enum": [
1
],
"description": "Define the version of the Connect protocol",
"const": 1
},
"connect-timeout-header": {
"type": "number",
"title": "Connect-Timeout-Ms",
"description": "Define the timeout, in ms"
},
"connect.error": {
"type": "object",
"properties": {
"code": {
"type": "string",
"examples": [
"not_found"
],
"enum": [
"canceled",
"unknown",
"invalid_argument",
"deadline_exceeded",
"not_found",
"already_exists",
"permission_denied",
"resource_exhausted",
"failed_precondition",
"aborted",
"out_of_range",
"unimplemented",
"internal",
"unavailable",
"data_loss",
"unauthenticated"
],
"description": "The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]."
},
"message": {
"type": "string",
"description": "A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client."
},
"details": {
"type": "array",
"items": {
"$ref": "#/components/schemas/connect.error_details.Any"
},
"description": "A list of messages that carry the error details. There is no limit on the number of messages."
}
},
"title": "Connect Error",
"additionalProperties": true,
"description": "Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation"
},
"connect.error_details.Any": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field."
},
"value": {
"type": "string",
"format": "binary",
"description": "The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field."
},
"debug": {
"oneOf": [
{
"type": "object",
"title": "Any",
"additionalProperties": true,
"description": "Detailed error information."
}
],
"discriminator": {
"propertyName": "type"
},
"title": "Debug",
"description": "Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic."
}
},
"additionalProperties": true,
"description": "Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details."
},
"shop.v1.CheckStockRequest": {
"type": "object",
"properties": {
"sku": {
"type": "string",
"title": "sku"
}
},
"title": "CheckStockRequest",
"additionalProperties": false
},
"shop.v1.CheckStockResponse": {
"type": "object",
"properties": {
"units": {
"type": "integer",
"title": "units",
"format": "int32"
}
},
"title": "CheckStockResponse",
"additionalProperties": false
}
},
"securitySchemes": {}
},
"security": [],
"tags": [
{
"name": "shop.v1.InventoryService"
}
]
}
Read the overlay and generation input
The page calls this function on a fresh base document and an edited method descriptor. The public/internal conflict guard belongs to this example overlay; reading the options alone does not validate that combination.
import type { DescMethod } from '@bufbuild/protobuf';
import { resolveMethodAuth } from './generated/auth-reader.mjs';
export type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject;
export type JsonObject = { [key: string]: JsonValue };
const object = (value: JsonValue | undefined): value is JsonObject => typeof value === 'object' && value !== null && !Array.isArray(value);
export function jsonDocument(value: unknown): JsonObject {
const json = (item: unknown): item is JsonValue => {
if (item === null || typeof item === 'string' || typeof item === 'boolean') return true;
if (typeof item === 'number') return Number.isFinite(item);
if (Array.isArray(item)) return item.every(json);
return typeof item === 'object' && item !== null && Object.values(item).every(json);
};
if (!json(value) || !object(value)) throw new Error('The base document must be a JSON object.');
return value;
}
/** credential metadata does not grant runtime access; always patch a fresh base. */
export function overlayMethod(base: JsonObject, method: DescMethod) {
const document = structuredClone(base);
const path = `/${method.parent.typeName}/${method.name}`;
const paths = document.paths;
if (!object(paths)) throw new Error('The base OpenAPI document has no paths object.');
const item = paths[path];
const operation = object(item) ? item.post : undefined;
if (!object(operation)) return { kind: 'skipped' as const, document, path };
const auth = resolveMethodAuth(method);
document.components ??= {};
if (!object(document.components)) throw new Error('The base components value is not an object.');
document.components.securitySchemes ??= {};
const schemes = document.components.securitySchemes;
if (!object(schemes)) throw new Error('The base securitySchemes value is not an object.');
if (auth.public) {
operation.security = [];
operation['x-connectum-public'] = true;
} else if (auth.internal) {
schemes.internalToken = { type: 'apiKey', in: 'header', name: 'x-internal-token' };
operation.security = [{ internalToken: [] }];
operation['x-connectum-internal'] = true;
} else {
schemes.bearerAuth = { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' };
operation.security = [{ bearerAuth: [] }];
}
if (!auth.public && auth.requires?.roles.length) operation['x-connectum-required-roles'] = [...auth.requires.roles];
if (!auth.public && auth.requires?.scopes.length) operation['x-connectum-required-scopes'] = [...auth.requires.scopes];
return { kind: 'patched' as const, document, path, auth };
}
/** retain the complete generated document while showing only this overlay's fields. */
export function securityFragment(document: JsonObject, path: string): JsonObject {
const item = object(document.paths) ? document.paths[path] : undefined;
const operation = object(item) ? item.post : undefined;
const fields: JsonObject = {};
if (object(operation)) {
for (const [name, value] of Object.entries(operation)) {
if (name === 'security' || name.startsWith('x-connectum-')) fields[name] = value;
}
}
const components = document.components;
const schemes = object(components) && object(components.securitySchemes) ? components.securitySchemes : {};
return {
paths: { [path]: object(operation) ? { post: fields } : {} },
...(Object.keys(schemes).length ? { components: { securitySchemes: schemes } } : {}),
};
}
The checked-in base was generated with protoc-gen-connect-openapi. A normalization step supplies the required info.version. Regenerating it needs network access; ordinary site builds use the committed artifact. Generator and input versions are recorded in the provenance files.
pnpm openapi:generate
pnpm openapi:base
pnpm openapi:inspect --annotation roles
syntax = "proto3";
package shop.v1;
import "connectum/auth/v1/options.proto";
service InventoryService {
rpc CheckStock(CheckStockRequest) returns (CheckStockResponse) {
option (connectum.auth.v1.method_auth) = { public: true };
}
rpc WatchStock(CheckStockRequest) returns (stream CheckStockResponse) {
option (connectum.auth.v1.method_auth) = { public: true };
}
}
message CheckStockRequest {
string sku = 1;
}
message CheckStockResponse {
int32 units = 1;
}
Compare all annotations without JavaScript
CheckStock · Public
{
"paths": {
"/shop.v1.InventoryService/CheckStock": {
"post": {
"security": [],
"x-connectum-public": true
}
}
}
}
CheckStock · User JWT
{
"paths": {
"/shop.v1.InventoryService/CheckStock": {
"post": {
"security": [
{
"bearerAuth": []
}
]
}
}
},
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
}
}
CheckStock · Role + scope
{
"paths": {
"/shop.v1.InventoryService/CheckStock": {
"post": {
"security": [
{
"bearerAuth": []
}
],
"x-connectum-required-roles": [
"warehouse"
],
"x-connectum-required-scopes": [
"stock:read"
]
}
}
},
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
}
}
CheckStock · Internal service
{
"paths": {
"/shop.v1.InventoryService/CheckStock": {
"post": {
"security": [
{
"internalToken": []
}
],
"x-connectum-internal": true,
"x-connectum-required-roles": [
"orders"
]
}
}
},
"components": {
"securitySchemes": {
"internalToken": {
"type": "apiKey",
"in": "header",
"name": "x-internal-token"
}
}
}
}
CheckStock · Deny policy
{
"paths": {
"/shop.v1.InventoryService/CheckStock": {
"post": {
"security": [
{
"bearerAuth": []
}
]
}
}
},
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
}
}
CheckStock · Conflicting options
@connectum/auth: shop.v1.InventoryService/CheckStock is marked both public and internal, which contradicts itself (public skips every check, internal requires a trusted peer). To make the method public set "public: true, internal: false" on it; to make it internal set "internal: true, public: false" (a method-level "public: false" also overrides a "public: true" inherited from the service).
WatchStock · Public
{
"paths": {
"/shop.v1.InventoryService/WatchStock": {}
}
}
WatchStock · User JWT
{
"paths": {
"/shop.v1.InventoryService/WatchStock": {}
}
}
WatchStock · Role + scope
{
"paths": {
"/shop.v1.InventoryService/WatchStock": {}
}
}
WatchStock · Internal service
{
"paths": {
"/shop.v1.InventoryService/WatchStock": {}
}
}
WatchStock · Deny policy
{
"paths": {
"/shop.v1.InventoryService/WatchStock": {}
}
}
WatchStock · Conflicting options
{
"paths": {
"/shop.v1.InventoryService/WatchStock": {}
}
}
How the generated document and runtime policy relate
One shared policy reader
The overlay uses the same annotation reader as proto-based authorization:
import { resolveMethodAuth } from '@connectum/auth/proto';
const auth = resolveMethodAuth(method);This excerpt reads one generated method descriptor. The complete generation recipe shows how to write its result into the spec. It is an example script, not a connectum openapi command.
Go deeper
- Generate and inspect the artifact.
- Understand omitted streaming operations and other limits.
- Expose REST through a gateway.
Find a section from the full guide
The detailed material is preserved on a separate page. Earlier section links still work here.