feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
{
"openapi" : "3.0.3" ,
"info" : {
"title" : "Navidrome API" ,
"version" : "1.0.0" ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"description" : "Navidrome API v1. Spec-first, additive within v1. Clients discover implemented\ncapability modules through `GET /capabilities` and never sniff versions.\n\nEnums are open: new values may be added to any enum within v1. Clients must\naccept values they do not recognise instead of failing.\n\nEvery operation declares `x-stability-level`: `alpha` operations may change or\ndisappear without notice, `beta` and `stable` operations only change additively.\nA level is only ever raised, never lowered.\n\n`HEAD` is accepted wherever `GET` is. A `405` response lists the allowed methods\nin its `Allow` header.\n\nOperations that need a grant declare `security: [{bearerAuth: []}]` and the scope they need in\n`x-scope` (OpenAPI 3.0 does not allow scopes on bearer schemes). Clients send the grant secret as\n`Authorization: Bearer \u003csecret\u003e`. A revoked grant stops working within one minute at most.\n" ,
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
"license" : {
"name" : "GPL-3.0" ,
"url" : "https://www.gnu.org/licenses/gpl-3.0.html"
}
} ,
"servers" : [
{
"url" : "/api/v1"
}
] ,
"tags" : [
{
"name" : "server" ,
"description" : "Server discovery and the published OpenAPI document."
2026-09-26 01:50:16 -04:00
} ,
{
"name" : "auth" ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"description" : "Grants and login methods."
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
}
] ,
"paths" : {
"/server" : {
"get" : {
"operationId" : "getServerInfo" ,
"x-module" : "core" ,
"x-stability-level" : "alpha" ,
"tags" : [
"server"
] ,
2026-09-26 01:35:19 -04:00
"security" : [ ] ,
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
"summary" : "Describe the server" ,
2026-09-26 01:57:43 -04:00
"description" : "Returns the public server description. No authentication required.\nCapability modules are listed by `GET /capabilities`.\n" ,
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
"responses" : {
"200" : {
"description" : "Server description." ,
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/ServerInfo"
}
}
}
} ,
"500" : {
"$ref" : "#/components/responses/InternalError"
}
}
}
} ,
2026-09-26 01:57:43 -04:00
"/capabilities" : {
"get" : {
"operationId" : "getCapabilities" ,
"x-module" : "core" ,
"x-stability-level" : "alpha" ,
"tags" : [
"server"
] ,
"summary" : "List implemented capability modules" ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"description" : "The capability modules this server implements. Any valid grant may read it, whatever its scopes." ,
2026-09-26 01:57:43 -04:00
"security" : [
{
"bearerAuth" : [ ]
}
] ,
"responses" : {
"200" : {
"description" : "Implemented modules." ,
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Capabilities"
}
}
}
} ,
"401" : {
"$ref" : "#/components/responses/Unauthorized"
} ,
"500" : {
"$ref" : "#/components/responses/InternalError"
}
}
}
} ,
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
"/openapi.json" : {
"get" : {
"operationId" : "getOpenAPISpecJSON" ,
"x-module" : "core" ,
"x-stability-level" : "alpha" ,
"tags" : [
"server"
] ,
2026-09-26 01:35:19 -04:00
"security" : [ ] ,
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
"summary" : "Get the OpenAPI document (JSON)" ,
"description" : "The bundled OpenAPI document of the running server version. Supports ETag revalidation." ,
"responses" : {
"200" : {
"description" : "The OpenAPI document." ,
"headers" : {
"ETag" : {
"$ref" : "#/components/headers/ETag"
}
} ,
"content" : {
"application/json" : {
"schema" : {
"type" : "object" ,
"description" : "OpenAPI 3.0 document."
}
}
}
} ,
"304" : {
"$ref" : "#/components/responses/NotModified"
}
}
}
} ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"/auth/grants" : {
"get" : {
"operationId" : "listGrants" ,
2026-09-26 01:50:16 -04:00
"x-module" : "core" ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"x-scope" : "read" ,
2026-09-26 01:50:16 -04:00
"x-stability-level" : "alpha" ,
"tags" : [
"auth"
] ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"summary" : "List my grants" ,
"description" : "The caller's grants, most recently used first. Grants idle long enough to have expired are not listed." ,
2026-09-26 01:50:16 -04:00
"security" : [
{
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"bearerAuth" : [ ]
2026-09-26 01:50:16 -04:00
}
] ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"parameters" : [
{
"$ref" : "#/components/parameters/offset"
} ,
{
"$ref" : "#/components/parameters/limit"
2026-09-26 01:50:16 -04:00
}
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
] ,
2026-09-26 01:50:16 -04:00
"responses" : {
"200" : {
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"description" : "A page of grants." ,
2026-09-26 01:50:16 -04:00
"content" : {
"application/json" : {
"schema" : {
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"$ref" : "#/components/schemas/GrantList"
2026-09-26 01:50:16 -04:00
}
}
}
} ,
"400" : {
"$ref" : "#/components/responses/BadRequest"
} ,
"401" : {
"$ref" : "#/components/responses/Unauthorized"
} ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"403" : {
"$ref" : "#/components/responses/Forbidden"
2026-09-26 01:50:16 -04:00
} ,
"500" : {
"$ref" : "#/components/responses/InternalError"
}
}
}
} ,
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
"/openapi.yaml" : {
"get" : {
"operationId" : "getOpenAPISpecYAML" ,
"x-module" : "core" ,
"x-stability-level" : "alpha" ,
"tags" : [
"server"
] ,
2026-09-26 01:35:19 -04:00
"security" : [ ] ,
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
"summary" : "Get the OpenAPI document (YAML)" ,
"description" : "The bundled OpenAPI document of the running server version. Supports ETag revalidation." ,
"responses" : {
"200" : {
"description" : "The OpenAPI document." ,
"headers" : {
"ETag" : {
"$ref" : "#/components/headers/ETag"
}
} ,
"content" : {
"application/yaml" : {
"schema" : {
"type" : "object" ,
"description" : "OpenAPI 3.0 document."
}
}
}
} ,
"304" : {
"$ref" : "#/components/responses/NotModified"
}
}
}
2026-09-26 01:50:16 -04:00
} ,
"/auth/grants/{id}" : {
"delete" : {
"operationId" : "revokeGrant" ,
"x-module" : "core" ,
"x-scope" : "read" ,
"x-stability-level" : "alpha" ,
"tags" : [
"auth"
] ,
"summary" : "Revoke one of my grants" ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"description" : "Revokes the grant; requests with its secret fail from then on. Another user's grant id answers 404." ,
2026-09-26 01:50:16 -04:00
"security" : [
{
"bearerAuth" : [ ]
}
] ,
"parameters" : [
{
"name" : "id" ,
"in" : "path" ,
"required" : true ,
"description" : "Grant id." ,
"schema" : {
"type" : "string" ,
"maxLength" : 64
}
}
] ,
"responses" : {
"204" : {
"description" : "Revoked."
} ,
"400" : {
"$ref" : "#/components/responses/BadRequest"
} ,
"401" : {
"$ref" : "#/components/responses/Unauthorized"
} ,
"403" : {
"$ref" : "#/components/responses/Forbidden"
} ,
"404" : {
"$ref" : "#/components/responses/NotFound"
} ,
"500" : {
"$ref" : "#/components/responses/InternalError"
}
}
}
} ,
"/auth/logout" : {
"post" : {
"operationId" : "logout" ,
"x-module" : "core" ,
"x-scope" : "read" ,
"x-stability-level" : "alpha" ,
"tags" : [
"auth"
] ,
"summary" : "Log out" ,
"description" : "Revokes the grant that made this request." ,
"security" : [
{
"bearerAuth" : [ ]
}
] ,
"responses" : {
"200" : {
"description" : "Logged out." ,
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/LogoutResponse"
}
}
}
} ,
"401" : {
"$ref" : "#/components/responses/Unauthorized"
} ,
"403" : {
"$ref" : "#/components/responses/Forbidden"
} ,
"500" : {
"$ref" : "#/components/responses/InternalError"
}
}
}
} ,
"/auth/login" : {
"post" : {
"operationId" : "login" ,
"x-module" : "password" ,
"x-stability-level" : "alpha" ,
"tags" : [
"auth"
] ,
"summary" : "Log in with a password" ,
"description" : "Checks the username and password and returns a new grant. Unknown user and wrong password fail the same way." ,
"security" : [ ] ,
"requestBody" : {
"description" : "The credentials and a description of the client." ,
"required" : true ,
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/CredentialsRequest"
}
}
}
} ,
"responses" : {
"200" : {
"description" : "The new grant." ,
fix(api): consolidate grant deletes and harden API v1 auth after review
- GrantRepository keeps three deletes: DeleteForUser, DeleteStaleEpochs
(replaces DeleteOtherEpochs and DeleteIfEpoch) and DeleteIdle. Delete(id)
is gone; the idle path in ResolveGrant now calls DeleteIdle, so a grant
renewed by another node between the read and the delete survives.
settleEpoch deletes the user's grants below the snapshot's epoch, which
is safe outside the transaction because epochs only move forward.
- The "dead grants on an older epoch are only deleted when presented"
policy note moves from the repository to core ListGrants.
- SQL trace logging no longer prints the args of property writes (signing
keys) or user password writes, both encrypted with a key that may be the
public default. The SQL statement is still logged.
- The spec gate rejects JSON body keys that differ from a declared property
only in case. kin-openapi validates exact names while encoding/json
decodes case-insensitively, so {"scopes":[],"Scopes":null} minted a
token with every scope and a "Client" key skipped maxLength.
It also rejects data after the first JSON value, which the handlers'
decoder ignores and which let a body skip the alias check.
- The liveness cache trims its eviction log on evict, not only on put, so
evict-only traffic stays bounded; the floor still drops stale fills.
- createAccessToken, login and setupFirstAdmin declare Cache-Control:
no-store on their success responses.
2026-09-26 12:52:20 -04:00
"headers" : {
"Cache-Control" : {
"$ref" : "#/components/headers/CacheControlNoStore"
}
} ,
2026-09-26 01:50:16 -04:00
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/GrantCreated"
}
}
}
} ,
"400" : {
"$ref" : "#/components/responses/BadRequest"
} ,
"401" : {
"$ref" : "#/components/responses/Unauthorized"
} ,
"413" : {
"$ref" : "#/components/responses/PayloadTooLarge"
} ,
"429" : {
"$ref" : "#/components/responses/TooManyRequests"
} ,
"500" : {
"$ref" : "#/components/responses/InternalError"
}
}
}
} ,
"/auth/setup" : {
"post" : {
"operationId" : "setupFirstAdmin" ,
"x-module" : "password" ,
"x-stability-level" : "alpha" ,
"tags" : [
"auth"
] ,
"summary" : "Create the first admin" ,
"description" : "Creates the first administrator while `setupRequired` is true and returns a grant for it. Answers 409 `setup_complete` once any user exists. A server with no setup step always answers 409." ,
"security" : [ ] ,
"requestBody" : {
"description" : "The credentials and a description of the client." ,
"required" : true ,
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/CredentialsRequest"
}
}
}
} ,
"responses" : {
"201" : {
"description" : "The admin was created." ,
fix(api): consolidate grant deletes and harden API v1 auth after review
- GrantRepository keeps three deletes: DeleteForUser, DeleteStaleEpochs
(replaces DeleteOtherEpochs and DeleteIfEpoch) and DeleteIdle. Delete(id)
is gone; the idle path in ResolveGrant now calls DeleteIdle, so a grant
renewed by another node between the read and the delete survives.
settleEpoch deletes the user's grants below the snapshot's epoch, which
is safe outside the transaction because epochs only move forward.
- The "dead grants on an older epoch are only deleted when presented"
policy note moves from the repository to core ListGrants.
- SQL trace logging no longer prints the args of property writes (signing
keys) or user password writes, both encrypted with a key that may be the
public default. The SQL statement is still logged.
- The spec gate rejects JSON body keys that differ from a declared property
only in case. kin-openapi validates exact names while encoding/json
decodes case-insensitively, so {"scopes":[],"Scopes":null} minted a
token with every scope and a "Client" key skipped maxLength.
It also rejects data after the first JSON value, which the handlers'
decoder ignores and which let a body skip the alias check.
- The liveness cache trims its eviction log on evict, not only on put, so
evict-only traffic stays bounded; the floor still drops stale fills.
- createAccessToken, login and setupFirstAdmin declare Cache-Control:
no-store on their success responses.
2026-09-26 12:52:20 -04:00
"headers" : {
"Cache-Control" : {
"$ref" : "#/components/headers/CacheControlNoStore"
}
} ,
2026-09-26 01:50:16 -04:00
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/GrantCreated"
}
}
}
} ,
"400" : {
"$ref" : "#/components/responses/BadRequest"
} ,
"409" : {
"$ref" : "#/components/responses/Conflict"
} ,
"413" : {
"$ref" : "#/components/responses/PayloadTooLarge"
} ,
"429" : {
"$ref" : "#/components/responses/TooManyRequests"
} ,
"500" : {
"$ref" : "#/components/responses/InternalError"
}
}
}
} ,
"/auth/password" : {
"post" : {
"operationId" : "changePassword" ,
"x-module" : "password" ,
"x-scope" : "password" ,
"x-stability-level" : "alpha" ,
"tags" : [
"auth"
] ,
"summary" : "Change my password" ,
2026-09-26 02:21:25 -04:00
"description" : "Changes the caller's password. By default every other grant of the user is revoked; the calling grant survives. On Navidrome the change also ends the user's sessions on its other APIs, regardless of `revokeOtherGrants`, which only covers API v1 grants. Answers 409 `password_managed_externally` when the password is not stored by this server." ,
2026-09-26 01:50:16 -04:00
"security" : [
{
"bearerAuth" : [ ]
}
] ,
"requestBody" : {
"description" : "The current and the new password." ,
"required" : true ,
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/PasswordChangeRequest"
}
}
}
} ,
"responses" : {
"204" : {
"description" : "Password changed."
} ,
"400" : {
"$ref" : "#/components/responses/BadRequest"
} ,
"401" : {
"$ref" : "#/components/responses/Unauthorized"
} ,
"403" : {
"$ref" : "#/components/responses/Forbidden"
} ,
"409" : {
"$ref" : "#/components/responses/Conflict"
} ,
"413" : {
"$ref" : "#/components/responses/PayloadTooLarge"
} ,
"429" : {
"$ref" : "#/components/responses/TooManyRequests"
} ,
"500" : {
"$ref" : "#/components/responses/InternalError"
}
}
}
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
}
} ,
"components" : {
"securitySchemes" : {
"bearerAuth" : {
"type" : "http" ,
"scheme" : "bearer" ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"description" : "Grant secret from a login method (`POST /auth/login`, `POST /auth/setup`). Opaque. The required scope is in each operation's `x-scope`."
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
}
} ,
"schemas" : {
"ServerInfo" : {
"type" : "object" ,
"description" : "Public server description. Everything an add-server screen needs before login." ,
"required" : [
"name" ,
"serverVersion" ,
"specVersion" ,
"setupRequired" ,
"loginMethods"
] ,
"properties" : {
"name" : {
"type" : "string" ,
"description" : "Human-readable server product name."
} ,
"serverVersion" : {
"type" : "string" ,
"description" : "Version of the running server build."
} ,
"specVersion" : {
"type" : "string" ,
"description" : "Version of the OpenAPI document this server implements."
} ,
"setupRequired" : {
"type" : "boolean" ,
"description" : "True until the first admin user has been created."
} ,
"loginMethods" : {
2026-09-26 01:57:43 -04:00
"$ref" : "#/components/schemas/LoginMethods"
}
}
} ,
"LoginMethods" : {
"type" : "object" ,
"description" : "Login methods this server accepts, keyed by method. A missing key means the method is not offered.\nKeys are optional on purpose: discovery is read by clients of any version against servers of any\nversion, so new methods are added as new optional keys. Clients ignore keys they do not know.\n" ,
"properties" : {
"password" : {
"$ref" : "#/components/schemas/PasswordLoginMethod"
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
}
}
} ,
2026-09-26 01:57:43 -04:00
"PasswordLoginMethod" : {
"type" : "object" ,
"description" : "Username and password login (`POST /auth/login`). No settings yet."
} ,
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
"Problem" : {
"type" : "object" ,
"description" : "RFC 9457 problem details, returned for every 4xx and 5xx response." ,
"required" : [
"title" ,
"status" ,
"code"
] ,
"properties" : {
"type" : {
"type" : "string" ,
"description" : "URI reference identifying the problem type. Omitted while the problem carries no semantics\nbeyond its HTTP status code, which RFC 9457 defines as `about:blank`. Problems with their\nown semantics get their own URI; switch on `code` instead.\n"
} ,
"title" : {
"type" : "string" ,
"description" : "Short human-readable summary, the same for all occurrences of this problem type."
} ,
"status" : {
"type" : "integer" ,
"description" : "HTTP status code of this response."
} ,
"detail" : {
"type" : "string" ,
2026-09-26 01:50:16 -04:00
"description" : "Human-readable explanation specific to this occurrence. Omitted unless the server marked the text as safe to show clients."
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
} ,
"code" : {
"type" : "string" ,
"description" : "Machine-readable error code, and the value clients switch on. New codes may be added." ,
"enum" : [
"validation" ,
"unauthorized" ,
"forbidden" ,
2026-09-26 01:35:19 -04:00
"insufficient_scope" ,
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
"not_found" ,
"method_not_allowed" ,
2026-09-26 01:35:19 -04:00
"setup_complete" ,
"password_managed_externally" ,
"payload_too_large" ,
"rate_limited" ,
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
"unavailable" ,
"internal"
]
} ,
2026-09-26 01:35:19 -04:00
"referenceId" : {
"type" : "string" ,
"description" : "Present on internal errors. Quote it when reporting a problem; it tags the server's log lines for this request."
} ,
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
"errors" : {
"type" : "array" ,
"description" : "Per-field failures. Present only when `code` is `validation`." ,
"items" : {
"$ref" : "#/components/schemas/ValidationError"
}
}
}
} ,
"ValidationError" : {
"type" : "object" ,
"description" : "One field-level validation failure." ,
"required" : [
"field" ,
"message"
] ,
"properties" : {
"field" : {
"type" : "string" ,
"description" : "Name of the offending query parameter, path parameter, or body field (dotted for nested)."
} ,
"message" : {
"type" : "string" ,
"description" : "Why the value was rejected."
}
}
2026-09-26 01:50:16 -04:00
} ,
2026-09-26 01:57:43 -04:00
"Capabilities" : {
"type" : "object" ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"description" : "Capability modules this server implements, keyed by module. Keys are optional; a missing key means the\nmodule is not implemented. New modules are added as new optional keys. These are server facts, not what\nthe calling grant may use.\n" ,
2026-09-26 01:57:43 -04:00
"properties" : {
"core" : {
"$ref" : "#/components/schemas/CoreCapability"
} ,
"password" : {
"$ref" : "#/components/schemas/PasswordCapability"
}
}
} ,
"CoreCapability" : {
"type" : "object" ,
"description" : "The mandatory core module." ,
"required" : [
"version"
] ,
"properties" : {
"version" : {
"type" : "integer" ,
"description" : "Module version. Bumped only on semantic change."
}
}
} ,
"PasswordCapability" : {
"type" : "object" ,
"description" : "The password login module (login, first-admin setup, password change)." ,
"required" : [
"version"
] ,
"properties" : {
"version" : {
"type" : "integer" ,
"description" : "Module version. Bumped only on semantic change."
}
}
} ,
2026-09-26 01:50:16 -04:00
"GrantList" : {
"type" : "object" ,
"description" : "A page of the caller's grants." ,
"required" : [
"items" ,
"total" ,
"offset" ,
"limit"
] ,
"properties" : {
"items" : {
"type" : "array" ,
"description" : "Grants on this page, by last use, most recent first; never-used grants last." ,
"items" : {
"$ref" : "#/components/schemas/Grant"
}
} ,
"total" : {
"type" : "integer" ,
"description" : "Total number of grants."
} ,
"offset" : {
"type" : "integer" ,
"description" : "Zero-based index of the first returned item."
} ,
"limit" : {
"type" : "integer" ,
"description" : "Maximum number of items in this page."
}
}
} ,
"LogoutResponse" : {
"type" : "object" ,
"description" : "Result of a logout." ,
"required" : [
"logoutUrl"
] ,
"properties" : {
"logoutUrl" : {
"type" : "string" ,
"nullable" : true ,
"description" : "Where to send the browser to finish logging out of an external provider. Null when there is nothing more to do."
}
}
} ,
"CredentialsRequest" : {
"type" : "object" ,
"description" : "Username, password and client description for a login or first-admin setup." ,
"required" : [
"username" ,
"password" ,
"client"
] ,
"properties" : {
"username" : {
"type" : "string" ,
"minLength" : 1 ,
"maxLength" : 255 ,
"description" : "Login name."
} ,
"password" : {
"type" : "string" ,
"minLength" : 1 ,
"maxLength" : 1024 ,
"description" : "Password."
} ,
"client" : {
"type" : "string" ,
"minLength" : 1 ,
"maxLength" : 64 ,
"description" : "Name of the client app."
} ,
"clientVersion" : {
"type" : "string" ,
"maxLength" : 32 ,
"description" : "Version of the client app."
} ,
"name" : {
"type" : "string" ,
"minLength" : 1 ,
"maxLength" : 64 ,
"description" : "Label for this grant. Defaults to `client`."
} ,
"scopes" : {
"type" : "array" ,
"maxItems" : 32 ,
"description" : "Scopes the grant may hold. Omit for `all`." ,
"items" : {
"$ref" : "#/components/schemas/ScopeRequest"
}
}
}
} ,
"GrantCreated" : {
"type" : "object" ,
"description" : "Returned by every login method. The secret is shown only here; store it and never parse it." ,
"required" : [
"secret" ,
"grant" ,
"user"
] ,
"properties" : {
"secret" : {
"type" : "string" ,
"maxLength" : 512 ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"description" : "Opaque grant secret. Send it as `Authorization: Bearer \u003csecret\u003e`."
2026-09-26 01:50:16 -04:00
} ,
"grant" : {
"description" : "The new grant." ,
"allOf" : [
{
"$ref" : "#/components/schemas/Grant"
}
]
} ,
"user" : {
"description" : "The user the grant belongs to." ,
"allOf" : [
{
"$ref" : "#/components/schemas/AuthUser"
}
]
}
}
} ,
"PasswordChangeRequest" : {
"type" : "object" ,
"description" : "Change the caller's own password." ,
"required" : [
"currentPassword" ,
"newPassword"
] ,
"properties" : {
"currentPassword" : {
"type" : "string" ,
"minLength" : 1 ,
"maxLength" : 1024 ,
"description" : "The current password."
} ,
"newPassword" : {
"type" : "string" ,
"minLength" : 1 ,
"maxLength" : 1024 ,
"description" : "The new password."
} ,
"revokeOtherGrants" : {
"type" : "boolean" ,
"default" : true ,
"description" : "Revoke every other grant of the user. The calling grant always survives. Default true."
}
}
} ,
"Grant" : {
"type" : "object" ,
"description" : "A long-lived grant held by one client of one user." ,
"required" : [
"id" ,
"name" ,
"client" ,
"clientVersion" ,
"scopes" ,
"provider" ,
"createdAt" ,
"lastUsedAt" ,
"lastUsedIp" ,
"current"
] ,
"properties" : {
"id" : {
"type" : "string" ,
"description" : "Grant id."
} ,
"name" : {
"type" : "string" ,
"description" : "Label shown to the user."
} ,
"client" : {
"type" : "string" ,
"description" : "Name of the client app that holds the grant."
} ,
"clientVersion" : {
"type" : "string" ,
"nullable" : true ,
"description" : "Version of the client app, when it sent one."
} ,
"scopes" : {
"type" : "array" ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"description" : "Scopes this grant carries." ,
2026-09-26 01:50:16 -04:00
"items" : {
"$ref" : "#/components/schemas/Scope"
}
} ,
"provider" : {
"type" : "string" ,
"description" : "How the grant was created, for example `password` or `setup`. Free-form; new values may appear."
} ,
"createdAt" : {
"type" : "string" ,
"format" : "date-time" ,
"description" : "When the grant was created."
} ,
"lastUsedAt" : {
"type" : "string" ,
"format" : "date-time" ,
"nullable" : true ,
"description" : "When the grant was last used, at a coarse granularity. Null until first use."
} ,
"lastUsedIp" : {
"type" : "string" ,
"nullable" : true ,
"description" : "Client IP of the last use. Null until first use."
} ,
"current" : {
"type" : "boolean" ,
"description" : "True for the grant that made this request."
}
}
} ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"Scope" : {
"type" : "string" ,
"description" : "A permission scope. Scopes mirror capability modules; `x:write` includes `x`. `all` appears only on\ngrants and means every scope the user is entitled to, now and in future releases. New scopes may be added.\n" ,
"enum" : [
"all" ,
"read" ,
"password"
]
} ,
"ScopeRequest" : {
"type" : "string" ,
"description" : "A requested scope. Scopes the server does not know are dropped, not rejected, so newer clients keep working." ,
"pattern" : "^[a-z][a-z-]*(:write)?$" ,
"maxLength" : 64
} ,
2026-09-26 01:50:16 -04:00
"AuthUser" : {
"type" : "object" ,
"description" : "The user a grant belongs to." ,
"required" : [
"id" ,
"userName" ,
"name" ,
"isAdmin" ,
"passwordChangeable"
] ,
"properties" : {
"id" : {
"type" : "string" ,
"description" : "User id."
} ,
"userName" : {
"type" : "string" ,
"description" : "Login name."
} ,
"name" : {
"type" : "string" ,
"description" : "Display name."
} ,
"isAdmin" : {
"type" : "boolean" ,
"description" : "Whether the user is an administrator."
} ,
"passwordChangeable" : {
"type" : "boolean" ,
"description" : "Whether `POST /auth/password` can change this user's password. Clients hide \"change password\" when false."
}
}
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
}
} ,
"responses" : {
"InternalError" : {
"description" : "Unexpected server failure. Details are in the server log." ,
"content" : {
"application/problem+json" : {
"schema" : {
"$ref" : "#/components/schemas/Problem"
}
}
}
} ,
2026-09-26 01:57:43 -04:00
"Unauthorized" : {
"description" : "Missing, invalid, or expired credentials." ,
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
"headers" : {
2026-09-26 01:57:43 -04:00
"WWW-Authenticate" : {
"$ref" : "#/components/headers/WWWAuthenticate"
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
}
2026-09-26 01:57:43 -04:00
} ,
2026-09-26 01:50:16 -04:00
"content" : {
"application/problem+json" : {
"schema" : {
"$ref" : "#/components/schemas/Problem"
}
}
}
} ,
2026-09-26 01:57:43 -04:00
"NotModified" : {
"description" : "Not modified." ,
2026-09-26 01:50:16 -04:00
"headers" : {
2026-09-26 01:57:43 -04:00
"ETag" : {
"$ref" : "#/components/headers/ETag"
2026-09-26 01:50:16 -04:00
}
2026-09-26 01:57:43 -04:00
}
} ,
"BadRequest" : {
"description" : "The request is malformed or fails validation." ,
2026-09-26 01:50:16 -04:00
"content" : {
"application/problem+json" : {
"schema" : {
"$ref" : "#/components/schemas/Problem"
}
}
}
} ,
"Forbidden" : {
"description" : "The caller is authenticated but not allowed to do this." ,
"headers" : {
"WWW-Authenticate" : {
"$ref" : "#/components/headers/WWWAuthenticate"
}
} ,
"content" : {
"application/problem+json" : {
"schema" : {
"$ref" : "#/components/schemas/Problem"
}
}
}
} ,
"NotFound" : {
"description" : "No such resource or endpoint." ,
"content" : {
"application/problem+json" : {
"schema" : {
"$ref" : "#/components/schemas/Problem"
}
}
}
} ,
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"PayloadTooLarge" : {
"description" : "The request body is too large (`payload_too_large`)." ,
"content" : {
"application/problem+json" : {
"schema" : {
"$ref" : "#/components/schemas/Problem"
}
}
}
} ,
2026-09-26 01:50:16 -04:00
"TooManyRequests" : {
"description" : "Rate limited (`rate_limited`). Retry after the `Retry-After` seconds." ,
"headers" : {
"Retry-After" : {
"description" : "Seconds to wait before retrying." ,
"schema" : {
"type" : "integer"
}
}
} ,
"content" : {
"application/problem+json" : {
"schema" : {
"$ref" : "#/components/schemas/Problem"
}
}
}
} ,
"Conflict" : {
"description" : "The request conflicts with the server's state, for example `setup_complete` or `password_managed_externally`." ,
"content" : {
"application/problem+json" : {
"schema" : {
"$ref" : "#/components/schemas/Problem"
}
}
}
}
} ,
"parameters" : {
"offset" : {
"name" : "offset" ,
"in" : "query" ,
"description" : "Zero-based index of the first item to return." ,
"required" : false ,
"schema" : {
"type" : "integer" ,
"minimum" : 0 ,
"default" : 0
}
} ,
"limit" : {
"name" : "limit" ,
"in" : "query" ,
"description" : "Maximum number of items to return." ,
"required" : false ,
"schema" : {
"type" : "integer" ,
"minimum" : 1 ,
"maximum" : 2000 ,
"default" : 100
}
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
}
} ,
"headers" : {
2026-09-26 01:57:43 -04:00
"WWWAuthenticate" : {
"description" : "RFC 6750 Bearer challenge, for example `Bearer error=\"insufficient_scope\", scope=\"read\"`." ,
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
"schema" : {
"type" : "string"
}
2026-09-26 01:50:16 -04:00
} ,
2026-09-26 01:57:43 -04:00
"ETag" : {
"description" : "Entity tag for `If-None-Match` revalidation." ,
2026-09-26 01:50:16 -04:00
"schema" : {
"type" : "string"
}
fix(api): consolidate grant deletes and harden API v1 auth after review
- GrantRepository keeps three deletes: DeleteForUser, DeleteStaleEpochs
(replaces DeleteOtherEpochs and DeleteIfEpoch) and DeleteIdle. Delete(id)
is gone; the idle path in ResolveGrant now calls DeleteIdle, so a grant
renewed by another node between the read and the delete survives.
settleEpoch deletes the user's grants below the snapshot's epoch, which
is safe outside the transaction because epochs only move forward.
- The "dead grants on an older epoch are only deleted when presented"
policy note moves from the repository to core ListGrants.
- SQL trace logging no longer prints the args of property writes (signing
keys) or user password writes, both encrypted with a key that may be the
public default. The SQL statement is still logged.
- The spec gate rejects JSON body keys that differ from a declared property
only in case. kin-openapi validates exact names while encoding/json
decodes case-insensitively, so {"scopes":[],"Scopes":null} minted a
token with every scope and a "Client" key skipped maxLength.
It also rejects data after the first JSON value, which the handlers'
decoder ignores and which let a body skip the alias check.
- The liveness cache trims its eviction log on evict, not only on put, so
evict-only traffic stays bounded; the floor still drops stale fills.
- createAccessToken, login and setupFirstAdmin declare Cache-Control:
no-store on their success responses.
2026-09-26 12:52:20 -04:00
} ,
"CacheControlNoStore" : {
refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.
Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".
Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.
Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.
Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
"description" : "Always `no-store`, because the response carries a secret." ,
fix(api): consolidate grant deletes and harden API v1 auth after review
- GrantRepository keeps three deletes: DeleteForUser, DeleteStaleEpochs
(replaces DeleteOtherEpochs and DeleteIfEpoch) and DeleteIdle. Delete(id)
is gone; the idle path in ResolveGrant now calls DeleteIdle, so a grant
renewed by another node between the read and the delete survives.
settleEpoch deletes the user's grants below the snapshot's epoch, which
is safe outside the transaction because epochs only move forward.
- The "dead grants on an older epoch are only deleted when presented"
policy note moves from the repository to core ListGrants.
- SQL trace logging no longer prints the args of property writes (signing
keys) or user password writes, both encrypted with a key that may be the
public default. The SQL statement is still logged.
- The spec gate rejects JSON body keys that differ from a declared property
only in case. kin-openapi validates exact names while encoding/json
decodes case-insensitively, so {"scopes":[],"Scopes":null} minted a
token with every scope and a "Client" key skipped maxLength.
It also rejects data after the first JSON value, which the handlers'
decoder ignores and which let a body skip the alias check.
- The liveness cache trims its eviction log on evict, not only on put, so
evict-only traffic stays bounded; the floor still drops stale fills.
- createAccessToken, login and setupFirstAdmin declare Cache-Control:
no-store on their success responses.
2026-09-26 12:52:20 -04:00
"schema" : {
"type" : "string" ,
"enum" : [
"no-store"
]
}
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling
vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.
* fix(api): pin the Redocly CLI version
Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.
* fix(api): bundle the OpenAPI spec with vacuum
vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.
* fix(api): make spec lint rules cover all schemas and error codes
nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.
* feat(api): embed the bundled OpenAPI spec and expose its version
* feat(api): generate the v1 server interface with oapi-codegen
* feat(api): add RFC 9457 problem responses for API v1
* feat(api): add API v1 router with /server discovery and spec routes
* fix(api): serve the OpenAPI document without range support
* feat(api): mount API v1 behind the DevAPIv1 flag
* chore(ci): lint, regenerate and diff the OpenAPI v1 spec
* refactor(api): tighten spec version access, lint rules and test naming
* refactor(api): simplify spec routes, tests and OpenAPI tooling
Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.
Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.
* refactor(api): raise the list limit maximum to 2000 and drop the flag test
* feat(api): treat added enum values as non-breaking
Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.
* feat(api): gate breaking changes on x-stability-level
Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.
* feat(api): declare loginMethods as an enum
Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.
* feat(api): send Allow on 405 and answer HEAD wherever GET is routed
chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.
* refactor(api): hash the spec ETag with xxh3
The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.
* docs(api): explain the about:blank problem type
* feat(api): make code the problem identifier and omit a blank type
RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.
* fix(api): advertise the configured base path in the served OpenAPI spec
With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.
Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
}
}
}
}