import { z } from 'zod'; import { commaSeparatedList, seconds } from './helpers.js'; import type { RuntimeConfigSection } from '../types.js'; const nullableString = z.string().nullable(); const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; const aliasEntry = z.object({ uuid: z.string().regex(UUID_REGEX, 'Invalid UUID'), password: z.string(), }); /** * Accepts either: * - a `Record` (DB-stored shape), or * - the comma-separated env string format `alias:uuid:password,alias:uuid:password,...`. */ const aliasedConfigurations = z.union([ z.record(z.string(), aliasEntry), z.string().transform((value, ctx) => { const out: Record = {}; if (!value.trim()) return out; for (const entry of value.split(',').map((e) => e.trim())) { if (!entry) continue; const [alias, uuid, password] = entry.split(':'); if (!alias || !uuid || !password) { ctx.addIssue({ code: 'custom', message: `Invalid alias entry "${entry}". Expected alias:uuid:password.`, }); return z.NEVER; } if (!UUID_REGEX.test(uuid)) { ctx.addIssue({ code: 'custom', message: `Invalid UUID for alias "${alias}".`, }); return z.NEVER; } out[alias] = { uuid, password }; } return out; }), ]); const provideStreamData = z.union([ z.boolean(), z.array(z.string()), z.null(), z.string().transform((value) => { const lower = value.trim().toLowerCase(); if (lower === 'true' || lower === '1') return true; if (lower === 'false' || lower === '0') return false; return value .split(',') .map((ip) => ip.trim()) .filter((ip) => ip.length > 0); }), ]); export const apiSchema = { authRequired: { schema: z.boolean(), default: false, label: 'Require authentication for the config page', description: 'When true, /stremio/configure requires a valid login session (any user in AIOSTREAMS_AUTH) and the config-write gate (CONFIG_ACCESS_KEY) is enforced. When false, the config page is public.', env: 'AIOSTREAMS_AUTH_REQUIRED', requiresRestart: false, secret: false, }, configAccessKey: { schema: nullableString, default: null, label: 'Config access key', description: 'Single key embedded in a config and checked on create/update/serve. If unset while authRequired is true, one is generated and persisted automatically. Rotating it invalidates every existing config until re-saved.', env: 'CONFIG_ACCESS_KEY', requiresRestart: false, secret: true, }, sessionTtlSeconds: { schema: seconds, default: 86400, label: 'Session lifetime', description: 'Lifetime of a login session before the user must log in again. Defaults to 24 hours (1d).', env: 'SESSION_TTL_SECONDS', requiresRestart: false, secret: false, }, aliasedConfigurations: { schema: aliasedConfigurations, default: {} as Record, label: 'Aliased configurations', description: 'Map of aliases to {uuid, password} accessible at /stremio/u//manifest.json. Env-supplied form: comma-separated `alias:uuid:password` entries.', env: 'ALIASED_CONFIGURATIONS', requiresRestart: false, secret: true, }, enableSearchApi: { schema: z.boolean(), default: true, label: 'Enable search API', description: 'When true, the /api/search endpoint is mounted and reachable.', env: 'ENABLE_SEARCH_API', requiresRestart: true, secret: false, }, provideStreamData: { schema: provideStreamData, default: null, label: 'Provide stream data', description: 'Whether stream metadata is included in Stremio stream responses. `null` (default) auto-detects from User-Agent (AIOStreams/* always gets it). `true`/`false` overrides for everyone. An IP list enables it only for matching request IPs.', env: 'PROVIDE_STREAM_DATA', requiresRestart: false, secret: false, }, exposeUserCount: { schema: z.boolean(), default: false, label: 'Expose user count', description: 'Include the total user count on the public status endpoint.', env: 'EXPOSE_USER_COUNT', requiresRestart: false, secret: false, }, stremioAddonsConfigIssuer: { schema: nullableString, default: 'https://stremio-addons.net', label: 'Stremio Addons Config issuer', description: 'Issuer URL declared in the manifest for the Stremio Addons Config integration.', env: 'STREMIO_ADDONS_CONFIG_ISSUER', requiresRestart: false, secret: false, }, stremioAddonsConfigSignature: { schema: nullableString, default: null, label: 'Stremio Addons Config signature', description: 'Signed JWT for the Stremio Addons Config integration. Both issuer and signature must be set for the manifest field to be emitted.', env: 'STREMIO_ADDONS_CONFIG_SIGNATURE', requiresRestart: false, secret: true, }, trustedIps: { schema: commaSeparatedList, default: ['172.17.0.0/16', '127.0.0.1/32', '::1/128'], label: 'Trusted IPs', description: 'Comma-separated list of trusted IPs / CIDR ranges. Used when determining the requesting IP. User IP is always trusted via headers regardless of this setting.', env: 'TRUSTED_IPS', requiresRestart: false, secret: false, }, } as const satisfies RuntimeConfigSection;