feat(web): serve public configuration schema #92
No files matched your search
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@when/web': patch
|
||||
'@when/worker': patch
|
||||
'@when/cli': patch
|
||||
---
|
||||
|
||||
Serve the generated editor configuration schema publicly at /schema.json from the deployed package. Require cache revalidation and expose only the schema, never loaded configuration or credentials.
|
||||
@@ -0,0 +1,34 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { signInAsAdmin } from './support/auth.ts';
|
||||
|
||||
test('schema.json is public, production-packaged, and independent of instance configuration', async ({
|
||||
page,
|
||||
request
|
||||
}) => {
|
||||
const anonymous = await request.get('/schema.json');
|
||||
expect(anonymous.status()).toBe(200);
|
||||
expect(anonymous.headers()['content-type']).toBe('application/schema+json; charset=utf-8');
|
||||
expect(anonymous.headers()['cache-control']).toBe('public, max-age=0, must-revalidate');
|
||||
expect(anonymous.headers().location).toBeUndefined();
|
||||
const text = await anonymous.text();
|
||||
const schema = JSON.parse(text);
|
||||
expect(schema.title).toBe('When configuration');
|
||||
expect(schema.$schema).toBe('https://json-schema.org/draft/2020-12/schema');
|
||||
expect(schema.$defs.Meeting.required).toContain('title');
|
||||
expect(schema.$defs.Meeting.required).not.toContain('duration_minutes');
|
||||
for (const instanceValue of [
|
||||
'E2E Fixture',
|
||||
'e2e-admin-password',
|
||||
'e2e-smtp-password',
|
||||
'e2e-caldav-password',
|
||||
'e2e-video-password'
|
||||
]) {
|
||||
expect(text).not.toContain(instanceValue);
|
||||
}
|
||||
|
||||
await signInAsAdmin(page);
|
||||
const signedIn = await page.request.get('/schema.json?include=config&role=host');
|
||||
expect(signedIn.status()).toBe(200);
|
||||
const signedInSchema = await signedIn.json();
|
||||
expect(signedInSchema).toEqual(schema);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import editorSchema from '@when/config/schema.json' with { type: 'json' };
|
||||
|
||||
export function GET(): Response {
|
||||
return json(editorSchema, {
|
||||
headers: {
|
||||
'content-type': 'application/schema+json; charset=utf-8',
|
||||
'cache-control': 'public, max-age=0, must-revalidate'
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import editorSchema from '@when/config/schema.json' with { type: 'json' };
|
||||
import { GET } from './+server';
|
||||
|
||||
const getConfig = vi.hoisted(() =>
|
||||
vi.fn(() => {
|
||||
throw new Error('Schema must not read instance configuration');
|
||||
})
|
||||
);
|
||||
vi.mock('$lib/server/state', () => ({ getConfig }));
|
||||
|
||||
describe('GET /schema.json', () => {
|
||||
test('returns only the bundled editor schema with the schema media type', async () => {
|
||||
const response = GET();
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get('content-type')).toBe('application/schema+json; charset=utf-8');
|
||||
expect(response.headers.get('cache-control')).toBe('public, max-age=0, must-revalidate');
|
||||
const body = await response.json();
|
||||
expect(body).toEqual(editorSchema);
|
||||
expect(getConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not let a response consumer mutate the bundled schema for later requests', async () => {
|
||||
const response = GET();
|
||||
const body = await response.json();
|
||||
body.title = 'Not the schema';
|
||||
const next = GET();
|
||||
const nextBody = await next.json();
|
||||
expect(nextBody).toEqual(editorSchema);
|
||||
expect(getConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,10 @@
|
||||
"development": "./src/index.ts",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"import": "./dist/src/index.js"
|
||||
},
|
||||
"./schema.json": {
|
||||
"development": "./src/config.schema.json",
|
||||
"default": "./dist/src/config.schema.json"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import generatedSchema from './config.schema.json' with { type: 'json' };
|
||||
|
||||
function references(value: unknown): string[] {
|
||||
if (value === null || typeof value !== 'object') return [];
|
||||
const refs: string[] = [];
|
||||
if ('$ref' in value && typeof value.$ref === 'string') refs.push(value.$ref);
|
||||
for (const child of Object.values(value)) refs.push(...references(child));
|
||||
return refs;
|
||||
}
|
||||
|
||||
describe('public editor schema asset', () => {
|
||||
test('exports the generated schema and includes the same asset in the compiled package', async () => {
|
||||
// The production export is available after build, not during source typechecking.
|
||||
const schemaPath = '@when/config/schema.json';
|
||||
const exported = await import(schemaPath, { with: { type: 'json' } });
|
||||
expect(exported.default).toEqual(generatedSchema);
|
||||
const compiledText = readFileSync(
|
||||
new URL('../dist/src/config.schema.json', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
const compiled = JSON.parse(compiledText);
|
||||
expect(compiled).toEqual(generatedSchema);
|
||||
});
|
||||
|
||||
test('retains editor-friendly optional defaults and self-contained references', () => {
|
||||
expect(generatedSchema.$schema).toBe('https://json-schema.org/draft/2020-12/schema');
|
||||
expect(generatedSchema.properties.version.default).toBe(1);
|
||||
expect(generatedSchema.required).not.toContain('version');
|
||||
expect(generatedSchema.required).not.toContain('providers');
|
||||
expect(generatedSchema.$defs.Meeting.required).toContain('title');
|
||||
expect(generatedSchema.$defs.Meeting.required).not.toContain('duration_minutes');
|
||||
const refs = references(generatedSchema);
|
||||
expect(refs.length).toBeGreaterThan(0);
|
||||
for (const reference of refs) expect(reference).toMatch(/^#/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user