feat(config): group app and worker URLs #23

Merged
eleith merged 1 commits from feat/service-url-config into main 2026-11-12 16:19:06 +00:00
27 changed files with 178 additions and 53 deletions

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
'@goto/web': minor
'@goto/worker': minor
'@goto/cli': minor
---
Group application addresses under `url.app` and `url.worker`, replacing `site.url` directly. Default the private worker base URL to `http://goto-worker:9000`. Keep site branding separate and preserve existing application-origin behavior.
+2 -2
View File
@@ -9,8 +9,8 @@ const env = {
DUMMY_SECRET: 'dummy-session-secret-with-at-least-thirty-two-characters-do-not-print',
DUMMY_DESTINATION: 'https://destination.example.test/private-do-not-print'
};
const validYaml = `site:
url: https://admin.example.test
const validYaml = `url:
app: https://admin.example.test
auth:
secret: \${DUMMY_SECRET}
credentials:
+1 -1
View File
@@ -139,7 +139,7 @@ async function start() {
await writeFile(
join(directory, 'config/goto.yaml'),
JSON.stringify({
site: { url: origin },
url: { app: origin },
auth: { credentials: { username: 'hmr-test', password: 'hmr-test-only-password' } },
domains: {
[`go.localhost:${address.port}`]: {
+1 -1
View File
@@ -24,7 +24,7 @@ await new Promise((resolve, reject) =>
);
process.env.PORT = String(address.port);
const configuration = {
site: { url: `http://localhost:${address.port}` },
url: { app: `http://localhost:${address.port}` },
auth: { credentials: { username: 'example', password: 'e2e-only-password' } },
analytics: { retention_months: Number(process.env.GOTO_E2E_RETENTION_MONTHS ?? '3') },
domains: {
+1 -1
View File
@@ -30,7 +30,7 @@ function event(
function application(): Application {
const prepared = validateConfig(
{
site: { url: 'https://admin.example.org' },
url: { app: 'https://admin.example.org' },
auth: { credentials: { username: 'example', password: 'test-password' } },
domains: { 'go.example.org': { links: {} } }
},
+1 -1
View File
@@ -34,7 +34,7 @@ export const applicationContext: Handle = ({ event, resolve }) => {
export const adminOriginGate: Handle = ({ event, resolve }) => {
if (
event.route.id?.startsWith('/(admin)') &&
event.url.origin !== event.locals.snapshot?.configuration.site.url
event.url.origin !== event.locals.snapshot?.configuration.url.app
) {
return new Response('Not found', { status: 404, headers: { 'cache-control': 'no-store' } });
}
+2 -2
View File
@@ -32,7 +32,7 @@ function prepared(
) {
return validateConfig(
{
site: { url: 'https://admin.example.test' },
url: { app: 'https://admin.example.test' },
auth: { credentials: { username: 'example', password: 'test-only-password' } },
domains: { 'go.example.test': { links: { '/docs': { url: destination } } } },
...changes
@@ -96,7 +96,7 @@ test('publishes valid changes, ignores equivalent data, and retains last good da
});
test.each([
{ site: { url: 'https://different-admin.example.test' } },
{ url: { app: 'https://different-admin.example.test' } },
{ auth: { credentials: { username: 'changed', password: 'private-changed-password' } } },
{ database: { path: './data/different.sqlite' } }
])('rejects the whole candidate when startup settings change: %j', async (changes) => {
+1 -1
View File
@@ -29,7 +29,7 @@ function reportReloadIssue(message: string): void {
function startupSettings({ configuration }: PreparedConfiguration) {
return {
siteUrl: configuration.site.url,
appUrl: configuration.url.app,
auth: configuration.auth,
databasePath: configuration.database.path,
collectionEnabled: configuration.analytics.retention_months !== 0
+1 -1
View File
@@ -12,7 +12,7 @@ const foreignCookies =
function authentication(origin: string) {
const { configuration } = validateConfig(
{
site: { url: origin },
url: { app: origin },
auth: { secret: 'cookie-test-secret-with-at-least-thirty-two-characters', credentials },
domains: { 'go.example.test': { links: {} } }
},
+2 -2
View File
@@ -15,7 +15,7 @@ export interface GotoSession {
}
export function createAuthentication(cfg: Immutable<GotoConfiguration>) {
const origin = cfg.site.url;
const origin = cfg.url.app;
const secret = resolveAuthSecret(cfg.auth.secret);
return betterAuth<BetterAuthOptions & { baseURL: string }>({
secret,
@@ -58,7 +58,7 @@ export const authHandle: Handle = async ({ event, resolve }) => {
const adminRoute = event.route.id?.startsWith('/(admin)');
const authPath = event.url.pathname === '/auth' || event.url.pathname.startsWith('/auth/');
if (
event.url.origin !== event.locals.snapshot?.configuration.site.url ||
event.url.origin !== event.locals.snapshot?.configuration.url.app ||
(!adminRoute && !authPath)
) {
return resolve(event);
+1 -1
View File
@@ -12,7 +12,7 @@ type AuthOptions = Pick<BetterAuthOptions, 'plugins' | 'onAPIError'>;
export function buildAuthOptions(cfg: Immutable<GotoConfiguration>): AuthOptions {
if (cfg.auth.credentials) return credentialsAuth(cfg.auth.credentials, 'admin@goto.invalid');
if (cfg.auth.oidc) return oidcAuth(cfg.auth.oidc, cfg.site.url);
if (cfg.auth.oidc) return oidcAuth(cfg.auth.oidc, cfg.url.app);
throw new Error('an authentication strategy is required');
}
+1 -1
View File
@@ -8,7 +8,7 @@ import { prepareSnapshot } from './snapshot.js';
const prepared = validateConfig(
{
site: { url: 'https://admin.example.test' },
url: { app: 'https://admin.example.test' },
auth: { credentials: { username: 'example', password: 'unit-only-password' } },
domains: {
'go.example.test': {
+1 -1
View File
@@ -30,7 +30,7 @@ export function handleIngress(
}
return redirect.response;
}
if (ingress.url.origin === snapshot.configuration.site.url) {
if (ingress.url.origin === snapshot.configuration.url.app) {
bindApplication(request, application, ingress.url.origin, snapshot);
return;
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { prepareSnapshot } from './snapshot.js';
function application(): Application {
const prepared = validateConfig(
{
site: { url: 'https://admin.example.org' },
url: { app: 'https://admin.example.org' },
auth: { credentials: { username: 'example', password: 'test-password' } },
domains: {}
},
@@ -14,7 +14,7 @@ import { prepareSnapshot } from './snapshot.js';
function application(): Application {
const prepared = validateConfig(
{
site: { url: 'https://admin.example.org' },
url: { app: 'https://admin.example.org' },
auth: { credentials: { username: 'example', password: 'test-password' } },
domains: {}
},
@@ -97,7 +97,7 @@ test('carries the trusted request origin separately from the configured admin or
bindApplication(incoming, captured, 'http://localhost:5173');
expect(requestOrigin(incoming)).toBe('http://localhost:5173');
expect(requestApplication(incoming)?.currentSnapshot().configuration.site.url).toBe(
expect(requestApplication(incoming)?.currentSnapshot().configuration.url.app).toBe(
'https://admin.example.org'
);
});
@@ -111,7 +111,8 @@ test('an existing request retains its snapshot when the application publishes an
current = prepareSnapshot(
validateConfig(
{
site: { url: 'https://admin.example.org', title: 'Updated' },
url: { app: 'https://admin.example.org' },
site: { title: 'Updated' },
auth: { credentials: { username: 'example', password: 'test-password' } },
domains: {}
},
+1 -1
View File
@@ -68,7 +68,7 @@ async function checkStartup(mode, port = 0) {
await writeFile(
join(directory, 'config/goto.yaml'),
JSON.stringify({
site: { url: 'http://admin.example.test' },
url: { app: 'http://admin.example.test' },
auth: { credentials: { username: 'smoke', password: 'private-worker-smoke-password' } },
analytics: { retention_months: mode === 'retention-change' ? 9 : 0 },
domains: {}
+1 -1
View File
@@ -25,7 +25,7 @@ afterEach(async () => {
async function writeConfiguration(worker_path = './state/worker.sqlite'): Promise<void> {
const configuration = {
site: { url: 'http://admin.example.test' },
url: { app: 'http://admin.example.test' },
auth: { credentials: { username: 'test', password: 'worker-test-only-password' } },
database: { path: './data/visits.sqlite', worker_path },
analytics: { retention_months: 0 },
+4 -1
View File
@@ -1,8 +1,11 @@
# yaml-language-server: $schema=https://goto.example.net/schema.json
version: 1
url:
app: https://goto.example.net
worker: http://goto-worker:9000
site:
url: https://goto.example.net
title: Goto
appearance:
+25 -2
View File
@@ -5,6 +5,7 @@
"required": ["auth", "domains"],
"properties": {
"version": { "default": 1, "const": 1, "type": "number" },
"url": { "default": {}, "$ref": "#/$defs/Url" },
"site": { "default": {}, "$ref": "#/$defs/Site" },
"appearance": { "default": {}, "$ref": "#/$defs/Appearance" },
"auth": { "$ref": "#/$defs/Auth" },
@@ -49,7 +50,22 @@
}
]
},
"SiteUrl": {
"BaseUrl": {
"description": "An absolute HTTP or HTTPS base URL without embedded credentials, query or fragment. A base path and trailing slash are allowed.",
"type": "string",
"anyOf": [
{
"type": "string",
"format": "uri",
"pattern": "^[Hh][Tt][Tt][Pp][Ss]?://[^\\s/@?#]+(?:/[^\\s?#]*)?(?![\\s\\S])"
},
{
"type": "string",
"pattern": "(?:^|[^$])(?:\\$\\$)*\\$\\{[A-Z_][A-Z0-9_]*(?::-[^}]*)?\\}"
}
]
},
"AppUrl": {
"description": "The canonical admin HTTP(S) origin, without credentials, path, trailing slash, query or fragment.",
"type": "string",
"anyOf": [
@@ -135,11 +151,18 @@
]
},
"Fallback": { "anyOf": [{ "const": 404, "type": "number" }, { "$ref": "#/$defs/LinkUrl" }] },
"Url": {
"additionalProperties": false,
"type": "object",
"properties": {
"app": { "default": "http://localhost:3000", "$ref": "#/$defs/AppUrl" },
"worker": { "default": "http://goto-worker:9000", "$ref": "#/$defs/BaseUrl" }
}
},
"Site": {
"additionalProperties": false,
"type": "object",
"properties": {
"url": { "default": "http://localhost:3000", "$ref": "#/$defs/SiteUrl" },
"title": {
"default": "Goto",
"type": "string",
+11 -5
View File
@@ -22,14 +22,20 @@ describe('Goto editor schema', () => {
it('relaxes defaulted fields while keeping authentication, domain links and destinations required', () => {
const editor = createEditorSchema();
expect(editor.required).toEqual(['auth', 'domains']);
for (const name of ['Site', 'Appearance', 'Database', 'Analytics', 'Defaults']) {
for (const name of ['Url', 'Site', 'Appearance', 'Database', 'Analytics', 'Defaults']) {
expect(definition(editor, name).required).toBeUndefined();
}
expect(property(editor, 'site')).toEqual({ $ref: '#/$defs/Site', default: {} });
expect(property(definition(editor, 'Site'), 'url')).toEqual({
$ref: '#/$defs/SiteUrl',
expect(object(definition(editor, 'Site').properties)).not.toHaveProperty('url');
expect(property(editor, 'url')).toEqual({ $ref: '#/$defs/Url', default: {} });
expect(property(definition(editor, 'Url'), 'app')).toEqual({
$ref: '#/$defs/AppUrl',
default: 'http://localhost:3000'
});
expect(property(definition(editor, 'Url'), 'worker')).toEqual({
$ref: '#/$defs/BaseUrl',
default: 'http://goto-worker:9000'
});
expect(definition(editor, 'DomainConfiguration').required).toEqual(['links']);
expect(definition(editor, 'LinkConfiguration').required).toEqual(['url']);
expect(property(definition(editor, 'LinkConfiguration'), 'url')).toEqual({
@@ -88,7 +94,7 @@ describe('Goto editor schema', () => {
it('maps runtime formats to editor formats and keeps literal constraints separate from tokens', () => {
const editor = createEditorSchema();
for (const name of ['HttpUrl', 'LinkUrl', 'SiteUrl', 'Instant']) {
for (const name of ['HttpUrl', 'LinkUrl', 'AppUrl', 'BaseUrl', 'Instant']) {
const canonical = configurationReferences.find((schema) => schema.$id === name)!;
const adapted = definition(editor, name);
expect(branches(adapted)[0]).toEqual({
@@ -271,7 +277,7 @@ describe('Goto editor schema', () => {
const first = JSON.stringify(createEditorSchema());
expect(JSON.stringify(createEditorSchema())).toBe(first);
expect(JSON.stringify([GotoConfigurationSchema, configurationReferences])).toBe(before);
for (const name of ['HttpUrl', 'LinkUrl', 'SiteUrl', 'Instant', 'AuthSecret']) {
for (const name of ['HttpUrl', 'LinkUrl', 'AppUrl', 'BaseUrl', 'Instant', 'AuthSecret']) {
const canonical = configurationReferences.find((schema) => schema.$id === name)!;
for (const value of ['${VALUE}', false, 1]) {
expect(Value.Check(canonical, value)).toBe(false);
+2 -1
View File
@@ -115,7 +115,8 @@ function adaptSchemaForEditor(
if (
node.format === 'goto-http-url' ||
node.format === 'goto-link-url' ||
node.format === 'goto-site-url'
node.format === 'goto-app-url' ||
node.format === 'goto-base-url'
) {
node.format = 'uri';
} else if (node.format === 'goto-instant') {
+60 -5
View File
@@ -21,7 +21,8 @@ describe('configuration loading', () => {
const { configuration } = validateConfig(input, options);
expect(configuration).toMatchObject({
version: 1,
site: { url: 'http://localhost:3000', title: 'Goto' },
url: { app: 'http://localhost:3000', worker: 'http://goto-worker:9000' },
site: { title: 'Goto' },
database: { path: './data/goto.sqlite', worker_path: './data/worker.sqlite' },
analytics: { retention_months: 3 },
defaults: { status_code: 302, forward_query: false, fallback: 404 }
@@ -64,24 +65,28 @@ describe('configuration loading', () => {
[{ PORT: '4321' }, 'http://localhost:4321'],
[{ PORT: '80' }, 'http://localhost']
])('derives an omitted origin from injected environment %j', (env, url) => {
expect(validateConfig(configurationInput(), { env }).configuration.site.url).toBe(url);
expect(validateConfig(configurationInput(), { env }).configuration.url.app).toBe(url);
});
it('validates interpolated values and preserves an explicit canonical origin', () => {
const input = {
...configurationInput({ '/docs': { url: '${DESTINATION}' } }),
site: { url: '${ADMIN_ORIGIN}' },
url: { app: '${ADMIN_ORIGIN}', worker: '${WORKER_BASE}' },
auth: { ...configurationInput().auth, secret: '${SECRET}' }
};
const env = {
ADMIN_ORIGIN: 'https://admin.example.com',
WORKER_BASE: 'http://worker.example.com:9100/private/',
DESTINATION: 'https://example.com',
SECRET: 's'.repeat(32),
PORT: 'invalid'
};
const { configuration } = validateConfig(input, { env });
expect(configuration.site.url).toBe(env.ADMIN_ORIGIN);
expect(configuration.url).toEqual({ app: env.ADMIN_ORIGIN, worker: env.WORKER_BASE });
expect(configuration.auth.secret).toBe(env.SECRET);
expect(() =>
validateConfig(input, { env: { ...env, WORKER_BASE: 'http://worker.example.com?secret' } })
).toThrowError(new ConfigError());
for (const destination of [
'https://private:secret@example.com',
'https://example.com/\r\nsecret'
@@ -95,6 +100,54 @@ describe('configuration loading', () => {
).toThrowError(new ConfigError());
});
it('defaults omitted URL fields independently', () => {
const input = configurationInput();
expect(validateConfig({ ...input, url: {} }, options).configuration.url).toEqual({
app: 'http://localhost:3000',
worker: 'http://goto-worker:9000'
});
expect(
validateConfig({ ...input, url: { app: 'https://admin.example.com' } }, options).configuration
.url.worker
).toBe('http://goto-worker:9000');
expect(
validateConfig(
{ ...input, url: { worker: 'https://worker.example.com/private/' } },
{ env: { NODE_ENV: 'development' } }
).configuration.url
).toEqual({ app: 'http://localhost:5173', worker: 'https://worker.example.com/private/' });
});
it.each([
'ftp://worker.example.com',
'http://user:password@worker.example.com',
'http://@worker.example.com',
'http://worker.example.com?token=secret',
'http://worker.example.com?',
'http://worker.example.com#fragment',
'http://worker.example.com#',
'http://worker.example.com/private path'
])('rejects invalid worker base URLs %j', (worker) => {
expect(() => validateConfig({ ...configurationInput(), url: { worker } }, options)).toThrow(
ConfigError
);
});
it('rejects the old site URL key without accepting it as an app URL', () => {
for (const url of [undefined, { app: 'https://admin.example.com' }]) {
expect(() =>
validateConfig(
{
...configurationInput(),
...(url === undefined ? {} : { url }),
site: { url: 'https://old.example.com', title: 'Custom title' }
},
options
)
).toThrow(ConfigError);
}
});
it('defaults the OIDC name after resolving authentication placeholders', () => {
const env = {
SECRET: 's'.repeat(32),
@@ -137,6 +190,8 @@ describe('configuration loading', () => {
{ unknown: 'private-value' },
{ site: null },
{ site: [] },
{ url: null },
{ url: [] },
{ version: Infinity },
{ auth: {} },
{
@@ -146,7 +201,7 @@ describe('configuration loading', () => {
}
},
{ auth: { ...configurationInput().auth, secret: 'too-short-private-secret' } },
{ site: { url: 'https://ADMIN.example.com/' } },
{ url: { app: 'https://ADMIN.example.com/' } },
{ defaults: { forward_query: null } },
{ domains: [] },
{ domains: { 'go.example.com': { links: [] } } },
+7 -7
View File
@@ -64,7 +64,7 @@ export function validateConfig(raw: unknown, options: ConfigOptions = {}): Prepa
assertSafeInput(raw);
const copy = structuredClone(raw);
const env = options.env ?? process.env;
defaultSiteUrl(copy, env);
defaultAppUrl(copy, env);
const expanded = interpolate(copy, env);
const defaulted = Value.Default(GotoConfigurationSchema, configurationReferences, expanded);
assertArrayShapesPreserved(raw, defaulted);
@@ -81,11 +81,11 @@ export function validateConfig(raw: unknown, options: ConfigOptions = {}): Prepa
}
}
function defaultSiteUrl(raw: Record<string, unknown>, env: ConfigEnvironment): void {
if (!Object.hasOwn(raw, 'site')) raw.site = {};
const site = raw.site;
if (typeof site !== 'object' || site === null || Array.isArray(site)) return;
if (Object.hasOwn(site, 'url')) return;
function defaultAppUrl(raw: Record<string, unknown>, env: ConfigEnvironment): void {
if (!Object.hasOwn(raw, 'url')) raw.url = {};
const url = raw.url;
if (typeof url !== 'object' || url === null || Array.isArray(url)) return;
if (Object.hasOwn(url, 'app')) return;
const port = env.PORT ?? (env.NODE_ENV === 'development' ? '5173' : '3000');
const number = Number(port);
if (
@@ -99,6 +99,6 @@ function defaultSiteUrl(raw: Record<string, unknown>, env: ConfigEnvironment): v
) {
throw new ConfigError();
}
(site as Record<string, unknown>).url =
(url as Record<string, unknown>).app =
number === 80 ? 'http://localhost' : `http://localhost:${number}`;
}
+1 -1
View File
@@ -204,7 +204,7 @@ describe('effective domains', () => {
it('rejects the admin host, noncanonical paths and reversed or empty validity windows', () => {
const input = configurationInput();
expect(() =>
validateConfig({ ...input, site: { url: 'https://go.example.com' } }, options)
validateConfig({ ...input, url: { app: 'https://go.example.com' } }, options)
).toThrow(ConfigError);
expect(() =>
validateConfig(configurationInput({ '/docs/': { url: 'https://example.com' } }), options)
+1 -1
View File
@@ -49,7 +49,7 @@ export function prepareDomains(
authoredDefaults: object
): Record<string, PreparedDomain> {
const domains: Record<string, PreparedDomain> = Object.create(null);
const adminHost = normalizeHost(new URL(configuration.site.url).host);
const adminHost = normalizeHost(new URL(configuration.url.app).host);
for (const [key, domain] of Object.entries(configuration.domains)) {
const host = normalizeHost(key);
if (!host || host === adminHost || Object.hasOwn(domains, host)) {
+37 -8
View File
@@ -33,7 +33,18 @@ FormatRegistry.Set('goto-link-url', (value) => {
return url !== undefined && url.username === '' && url.password === '';
});
FormatRegistry.Set('goto-site-url', (value) => {
FormatRegistry.Set('goto-base-url', (value) => {
const url = parseHttpUrl(value);
return (
url !== undefined &&
url.username === '' &&
url.password === '' &&
!value.includes('?') &&
!value.includes('#')
);
});
FormatRegistry.Set('goto-app-url', (value) => {
const url = parseHttpUrl(value);
return url !== undefined && url.port !== '0' && value === url.origin;
});
@@ -44,7 +55,7 @@ const ipv6Pattern = '\\[[0-9a-f:.]+\\]';
const hostPattern = `(?:${hostnamePattern}|${ipv6Pattern})`;
const httpsPortPattern = `(?::(?!443${end})[1-9][0-9]*)?`;
const httpPortPattern = `(?::(?!80${end})[1-9][0-9]*)?`;
const siteOriginPattern = `^(?:https://${hostPattern}${httpsPortPattern}|http://${hostPattern}${httpPortPattern})${end}`;
const appOriginPattern = `^(?:https://${hostPattern}${httpsPortPattern}|http://${hostPattern}${httpPortPattern})${end}`;
const instantPattern = `^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})${end}`;
FormatRegistry.Set('goto-instant', (value) => {
@@ -71,10 +82,18 @@ const LinkUrlSchema = Type.String({
description: 'An absolute HTTP or HTTPS destination without embedded credentials.'
});
const SiteUrlSchema = Type.String({
$id: 'SiteUrl',
format: 'goto-site-url',
pattern: siteOriginPattern,
const BaseUrlSchema = Type.String({
$id: 'BaseUrl',
format: 'goto-base-url',
pattern: `^[Hh][Tt][Tt][Pp][Ss]?://[^\\s/@?#]+(?:/[^\\s?#]*)?${end}`,
description:
'An absolute HTTP or HTTPS base URL without embedded credentials, query or fragment. A base path and trailing slash are allowed.'
});
const AppUrlSchema = Type.String({
$id: 'AppUrl',
format: 'goto-app-url',
pattern: appOriginPattern,
description:
'The canonical admin HTTP(S) origin, without credentials, path, trailing slash, query or fragment.'
});
@@ -118,9 +137,16 @@ const FallbackSchema = Type.Union([Type.Literal(404), ref(LinkUrlSchema)], {
$id: 'Fallback'
});
const UrlSchema = Type.Object(
{
app: ref(AppUrlSchema, { default: 'http://localhost:3000' }),
worker: ref(BaseUrlSchema, { default: 'http://goto-worker:9000' })
},
{ $id: 'Url', additionalProperties: false }
);
const SiteSchema = Type.Object(
{
url: ref(SiteUrlSchema, { default: 'http://localhost:3000' }),
title: Type.String({ ...nonBlankText, default: 'Goto' }),
logo_path: Type.Optional(ref(LocalImagePathSchema)),
favicon_path: Type.Optional(ref(LocalImagePathSchema))
@@ -255,6 +281,7 @@ const DomainConfigurationSchema = Type.Object(
export const GotoConfigurationSchema = Type.Object(
{
version: Type.Literal(1, { default: 1 }),
url: ref(UrlSchema, { default: {} }),
site: ref(SiteSchema, { default: {} }),
appearance: ref(AppearanceSchema, { default: {} }),
auth: ref(AuthSchema),
@@ -271,7 +298,8 @@ export const GotoConfigurationSchema = Type.Object(
export const configurationReferences: TSchema[] = [
HttpUrlSchema,
LinkUrlSchema,
SiteUrlSchema,
BaseUrlSchema,
AppUrlSchema,
InstantSchema,
AuthSecretSchema,
LocalImagePathSchema,
@@ -279,6 +307,7 @@ export const configurationReferences: TSchema[] = [
TagsSchema,
RedirectStatusSchema,
FallbackSchema,
UrlSchema,
SiteSchema,
AppearanceSchema,
OidcAuthSchema,
+1 -1
View File
@@ -14,7 +14,7 @@ import { generateVisits, seedWindowStart } from './seed-visits-data.js';
const script = fileURLToPath(new URL('./seed-visits.js', import.meta.url));
const loader = fileURLToPath(new URL('../tools/register.mjs', import.meta.url));
const fixture = {
site: { url: 'https://admin.example.test' },
url: { app: 'https://admin.example.test' },
auth: { credentials: { username: 'test', password: 'seed-test-only-password' } },
domains: {
'one.example.test': { links: { '/docs': { url: 'https://example.org/docs' } } },