From cb1343b1640c172d2f0999adbadd363dd0accb2f Mon Sep 17 00:00:00 2001 From: eleith Date: Tue, 27 Oct 2026 12:28:14 -0700 Subject: [PATCH] Expose public configuration editor schema --- .changeset/public-config-schema.md | 5 +++ apps/web/e2e/schema.e2e.ts | 45 +++++++++++++++++++ .../routes/(public)/schema.json/+server.ts | 11 +++++ docs/config.md | 6 +++ docs/directory.example.yaml | 1 + 5 files changed, 68 insertions(+) create mode 100644 .changeset/public-config-schema.md create mode 100644 apps/web/e2e/schema.e2e.ts create mode 100644 apps/web/src/routes/(public)/schema.json/+server.ts diff --git a/.changeset/public-config-schema.md b/.changeset/public-config-schema.md new file mode 100644 index 0000000..b494f85 --- /dev/null +++ b/.changeset/public-config-schema.md @@ -0,0 +1,5 @@ +--- +'@directory/web': minor +--- + +Serve the bundled configuration editor schema publicly at `/schema.json`, without exposing instance settings or credentials. diff --git a/apps/web/e2e/schema.e2e.ts b/apps/web/e2e/schema.e2e.ts new file mode 100644 index 0000000..fc4df41 --- /dev/null +++ b/apps/web/e2e/schema.e2e.ts @@ -0,0 +1,45 @@ +import editorSchema from '@directory/config/schema.json' with { type: 'json' }; +import { expect, test } from '@playwright/test'; +import { + configuration, + privateSentinels, + siteDescription, + siteTitle +} from './support/configuration.js'; +import { startServer } from './support/server.js'; + +test('the production editor schema is public and contains no instance configuration', async () => { + const server = await startServer({ configuration: configuration() }); + try { + await server.ready(); + const home = await server.fetch('/', { headers: { accept: 'application/json' } }); + expect(home.status).toBe(401); + for (const method of ['GET', 'HEAD']) { + const response = await server.fetch('/schema.json?include=config&path=/private.yaml', { + method + }); + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('application/schema+json; charset=utf-8'); + expect(response.headers.get('cache-control')).toContain('no-store'); + expect(response.headers.get('x-content-type-options')).toBe('nosniff'); + expect(response.headers.has('location')).toBe(false); + const text = await response.text(); + if (method === 'HEAD') { + expect(text).toBe(''); + continue; + } + expect(JSON.parse(text)).toEqual(editorSchema); + for (const value of [ + ...privateSentinels, + siteTitle, + siteDescription, + server.path, + server.origin + ]) { + expect(text).not.toContain(value); + } + } + } finally { + expect(await server.stop()).toEqual({ code: 0, signal: null }); + } +}); diff --git a/apps/web/src/routes/(public)/schema.json/+server.ts b/apps/web/src/routes/(public)/schema.json/+server.ts new file mode 100644 index 0000000..f239f4a --- /dev/null +++ b/apps/web/src/routes/(public)/schema.json/+server.ts @@ -0,0 +1,11 @@ +import { json } from '@sveltejs/kit'; +import editorSchema from '@directory/config/schema.json' with { type: 'json' }; + +export function GET(): Response { + return json(editorSchema, { + headers: { + 'content-type': 'application/schema+json; charset=utf-8', + 'cache-control': 'no-store' + } + }); +} diff --git a/docs/config.md b/docs/config.md index 344d833..8db6f0a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -132,6 +132,12 @@ We all could use a little validation every now and then. We publish a json schema to give you little red squigglies but you can also use our cli when you are in a pinch. +Find the schema at `/schema.json`: + +```yaml +# yaml-language-server: $schema=http://localhost:3000/schema.json +``` + From the folder containing `compose.yaml`: ```sh diff --git a/docs/directory.example.yaml b/docs/directory.example.yaml index 0c0368e..8f4a8b0 100644 --- a/docs/directory.example.yaml +++ b/docs/directory.example.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=http://localhost:3000/schema.json # Replace the placeholder addresses with your own. # Sign-in is disabled. Uncomment one auth example below to enable it. version: 1 -- 2.54.0