Publish DevRunbook source
Managed validation / full (push) Successful in 3m18s

This commit is contained in:
DevRunbook release export
2026-09-03 04:09:17 +02:00
commit cfd2804e27
928 changed files with 161642 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@devrunbook/config",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint src --max-warnings=0",
"test": "vitest run",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"zod": "4.4.3"
},
"devDependencies": {
"@types/node": "24.13.3",
"typescript": "5.9.3",
"vitest": "4.1.10"
}
}
+47
View File
@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest'
import { parseEnvironment, tryParseEnvironment } from './index'
const valid = {
DATABASE_URL: 'postgresql://user:password@localhost:5432/devrunbook',
PUBLIC_BASE_URL: 'http://localhost:3000',
SESSION_SECRET: '01234567890123456789012345678901',
INTEGRATION_ENCRYPTION_KEY: Buffer.alloc(32, 7).toString('base64'),
INTEGRATION_ENCRYPTION_KEY_VERSION: 'v1',
CONTENT_ROOT: '/content',
ARTIFACT_ROOT: '/artifacts',
}
describe('parseEnvironment', () => {
it('applies documented safe defaults', () => {
const parsed = parseEnvironment(valid)
expect(parsed.REGISTRATION_MODE).toBe('closed')
expect(parsed.MAX_ARCHIVE_FILES).toBe(500)
expect(parsed.MAINTENANCE_MODE).toBe(false)
expect(parsed.GITEA_REQUEST_TIMEOUT_MS).toBe(15_000)
expect(parsed.GITEA_MAX_REDIRECTS).toBe(3)
expect(parsed.GITEA_MAX_FILE_BYTES).toBe(1_048_576)
expect(parsed.GITEA_MAX_FILES_PER_SNAPSHOT).toBe(200)
expect(parsed.INTEGRATION_ENCRYPTION_OLD_KEYS).toEqual({})
})
it('rejects short secrets', () => {
const result = tryParseEnvironment({ ...valid, SESSION_SECRET: 'short' })
expect(result.success).toBe(false)
})
it('parses a bounded old integration key ring and rejects malformed keys', () => {
const oldKey = Buffer.alloc(32, 9).toString('base64')
expect(
parseEnvironment({
...valid,
INTEGRATION_ENCRYPTION_OLD_KEYS: JSON.stringify({ legacy: oldKey }),
}).INTEGRATION_ENCRYPTION_OLD_KEYS,
).toEqual({ legacy: oldKey })
expect(
tryParseEnvironment({
...valid,
INTEGRATION_ENCRYPTION_OLD_KEYS: '{"legacy":"short"}',
}).success,
).toBe(false)
})
})
+122
View File
@@ -0,0 +1,122 @@
import { z } from 'zod'
const booleanString = z
.enum(['true', 'false'])
.transform((value) => value === 'true')
const integerString = (minimum: number, maximum: number) =>
z.coerce.number().int().min(minimum).max(maximum)
const integrationOldKeys = z
.string()
.default('{}')
.transform((value, context): Readonly<Record<string, string>> => {
let parsed: unknown
try {
parsed = JSON.parse(value)
} catch {
context.addIssue({
code: 'custom',
message: 'must be a JSON object of key versions to base64 keys',
})
return z.NEVER
}
if (
parsed === null ||
typeof parsed !== 'object' ||
Array.isArray(parsed)
) {
context.addIssue({
code: 'custom',
message: 'must be a JSON object of key versions to base64 keys',
})
return z.NEVER
}
const result: Record<string, string> = {}
for (const [version, key] of Object.entries(parsed)) {
if (
version.length === 0 ||
version.length > 64 ||
typeof key !== 'string' ||
Buffer.from(key, 'base64').byteLength !== 32
) {
context.addIssue({
code: 'custom',
message:
'each old key must have a 1-64 character version and a base64-encoded 32-byte value',
})
return z.NEVER
}
result[version] = key
}
return Object.freeze(result)
})
const environmentSchema = z.object({
DATABASE_URL: z.string().min(1),
PUBLIC_BASE_URL: z.url(),
SESSION_SECRET: z.string().min(32),
INTEGRATION_ENCRYPTION_KEY: z
.string()
.refine((value) => Buffer.from(value, 'base64').byteLength === 32, {
message: 'must be a base64-encoded 32-byte key',
}),
INTEGRATION_ENCRYPTION_KEY_VERSION: z.string().min(1).max(64),
INTEGRATION_ENCRYPTION_OLD_KEYS: integrationOldKeys,
CONTENT_ROOT: z.string().min(1),
ARTIFACT_ROOT: z.string().min(1),
BOOTSTRAP_TOKEN: z.string().min(16).optional(),
REGISTRATION_MODE: z.enum(['closed', 'invite']).default('closed'),
TRUSTED_PROXY_CIDRS: z.string().default(''),
MAINTENANCE_MODE: booleanString.default(false),
MAX_IMPORT_BYTES: integerString(1, 52_428_800).default(10_485_760),
MAX_EXPANDED_ARCHIVE_BYTES: integerString(1, 262_144_000).default(52_428_800),
MAX_ARCHIVE_FILES: integerString(1, 5_000).default(500),
MAX_SINGLE_FILE_BYTES: integerString(1, 26_214_400).default(5_242_880),
MAX_PROMPT_BYTES: integerString(1, 10_485_760).default(2_097_152),
MAX_EVIDENCE_BYTES: integerString(1, 2_097_152).default(262_144),
MAX_ARTIFACT_BYTES: integerString(1, 52_428_800).default(5_242_880),
GITEA_PRIVATE_NETWORK_POLICY: z
.enum(['deny', 'allow-explicit-hosts'])
.default('deny'),
GITEA_ALLOWED_HOSTS: z.string().default(''),
GITEA_REQUEST_TIMEOUT_MS: integerString(1_000, 60_000).default(15_000),
GITEA_MAX_REDIRECTS: integerString(0, 10).default(3),
GITEA_MAX_FILE_BYTES: integerString(1, 5_242_880).default(1_048_576),
GITEA_MAX_FILES_PER_SNAPSHOT: integerString(1, 500).default(200),
ARTIFACT_RETENTION_DAYS: integerString(1, 3_650).default(90),
AUDIT_RETENTION_DAYS: integerString(1, 3_650).default(180),
LOG_RETENTION_DAYS: integerString(1, 365).default(30),
SNAPSHOT_RETENTION_COUNT: integerString(1, 1_000).default(20),
LOG_LEVEL: z
.enum(['trace', 'debug', 'info', 'warn', 'error'])
.default('info'),
WORKER_POLL_INTERVAL_MS: integerString(100, 60_000).default(2_000),
JOB_LEASE_SECONDS: integerString(10, 3_600).default(60),
REPOSITORY_REFRESH_SCHEDULE_MS: integerString(60_000, 86_400_000).default(
300_000,
),
REPOSITORY_STALE_AFTER_HOURS: integerString(1, 720).default(24),
})
export type AppConfig = z.infer<typeof environmentSchema>
export function parseEnvironment(
environment: Record<string, string | undefined>,
): AppConfig {
return environmentSchema.parse(environment)
}
export function tryParseEnvironment(
environment: Record<string, string | undefined>,
) {
return environmentSchema.safeParse(environment)
}
export const redactedEnvironmentKeys = [
'DATABASE_URL',
'SESSION_SECRET',
'INTEGRATION_ENCRYPTION_KEY',
'INTEGRATION_ENCRYPTION_OLD_KEYS',
'BOOTSTRAP_TOKEN',
] as const
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"types": ["node"]
},
"include": ["src/**/*.ts"]
}