initial wcag-ada

This commit is contained in:
Boki 2025-06-28 11:11:34 -04:00
parent 042b8cb83a
commit d52cfe7de2
112 changed files with 9069 additions and 0 deletions

View file

@ -0,0 +1,67 @@
import { ConfigManager, createAppConfig } from '@stock-bot/core-config';
import { wcagAppConfigSchema, type WcagAppConfig } from './schemas/wcag-app.schema';
import * as path from 'path';
let configInstance: ConfigManager<WcagAppConfig> | null = null;
/**
* Initialize the WCAG-ADA configuration
* @param serviceName - Optional service name for service-specific overrides
*/
export function initializeWcagConfig(
serviceName?: 'api' | 'dashboard' | 'worker'
): WcagAppConfig {
if (!configInstance) {
configInstance = createAppConfig(wcagAppConfigSchema, {
configPath: path.join(__dirname, '../config'),
envPrefix: 'WCAG_',
});
}
const config = configInstance.initialize(wcagAppConfigSchema);
// Apply service-specific overrides if provided
if (serviceName && config.services[serviceName]) {
const serviceConfig = config.services[serviceName];
// Override port if specified for the service
if (serviceConfig.port && process.env.PORT) {
serviceConfig.port = parseInt(process.env.PORT, 10);
}
}
return config;
}
/**
* Get the current WCAG configuration instance
* @throws Error if configuration hasn't been initialized
*/
export function getWcagConfig(): WcagAppConfig {
if (!configInstance) {
throw new Error('WCAG configuration not initialized. Call initializeWcagConfig() first.');
}
return configInstance.get();
}
/**
* Get a specific configuration value by path
* @param path - Dot-notation path to the configuration value
*/
export function getConfigValue<T = unknown>(path: string): T {
if (!configInstance) {
throw new Error('WCAG configuration not initialized. Call initializeWcagConfig() first.');
}
return configInstance.getValue<T>(path);
}
/**
* Check if a configuration path exists
* @param path - Dot-notation path to check
*/
export function hasConfigValue(path: string): boolean {
if (!configInstance) {
return false;
}
return configInstance.has(path);
}

View file

@ -0,0 +1,106 @@
// Main configuration exports
export {
initializeWcagConfig,
getWcagConfig,
getConfigValue,
hasConfigValue,
} from './config-instance';
// Schema exports
export { wcagAppConfigSchema, type WcagAppConfig } from './schemas/wcag-app.schema';
export { scannerConfigSchema, type ScannerConfig } from './schemas/scanner.schema';
export { workerConfigSchema, type WorkerConfig } from './schemas/worker.schema';
export { featuresConfigSchema, type FeaturesConfig } from './schemas/features.schema';
export { providersConfigSchema, type ProvidersConfig } from './schemas/providers.schema';
// Helper functions
import { getWcagConfig } from './config-instance';
import type { ScannerConfig, WorkerConfig, FeaturesConfig, ProvidersConfig } from './schemas';
/**
* Get scanner configuration
*/
export function getScannerConfig(): ScannerConfig {
return getWcagConfig().scanner;
}
/**
* Get worker configuration
*/
export function getWorkerConfig(): WorkerConfig {
return getWcagConfig().worker;
}
/**
* Get features configuration
*/
export function getFeaturesConfig(): FeaturesConfig {
return getWcagConfig().features;
}
/**
* Get providers configuration
*/
export function getProvidersConfig(): ProvidersConfig {
return getWcagConfig().providers;
}
/**
* Get service configuration
*/
export function getServiceConfig(service: 'api' | 'dashboard' | 'worker') {
return getWcagConfig().services[service];
}
/**
* Check if a feature is enabled
*/
export function isFeatureEnabled(feature: string): boolean {
const features = getFeaturesConfig();
const parts = feature.split('.');
let current: any = features;
for (const part of parts) {
if (typeof current !== 'object' || !(part in current)) {
return false;
}
current = current[part];
}
return current === true || (typeof current === 'object' && current.enabled === true);
}
/**
* Get database configuration
*/
export function getDatabaseConfig() {
return getWcagConfig().database;
}
/**
* Get Redis configuration for queues
*/
export function getRedisConfig() {
return getWcagConfig().worker.redis;
}
/**
* Get storage configuration
*/
export function getStorageConfig() {
return getWcagConfig().providers.storage;
}
/**
* Get compliance configuration
*/
export function getComplianceConfig() {
return getWcagConfig().compliance;
}
/**
* Get subscription tier configuration
*/
export function getSubscriptionConfig(tier: 'starter' | 'professional' | 'enterprise') {
return getWcagConfig().subscriptions.tiers[tier];
}

View file

@ -0,0 +1,89 @@
import { z } from 'zod';
export const featuresConfigSchema = z.object({
// Scanner features
screenshots: z.object({
enabled: z.boolean().default(true),
quality: z.number().min(0).max(100).default(80),
fullPage: z.boolean().default(false),
}).default({}),
customRules: z.object({
enabled: z.boolean().default(true),
maxRules: z.number().default(50),
}).default({}),
multiPage: z.object({
enabled: z.boolean().default(true),
maxPages: z.number().default(10),
maxDepth: z.number().default(3),
}).default({}),
// API features
apiKeys: z.object({
enabled: z.boolean().default(true),
maxPerUser: z.number().default(5),
}).default({}),
webhooks: z.object({
enabled: z.boolean().default(true),
maxPerWebsite: z.number().default(3),
retryAttempts: z.number().default(3),
}).default({}),
// Report features
reports: z.object({
pdf: z.object({
enabled: z.boolean().default(true),
watermark: z.boolean().default(true),
}).default({}),
excel: z.object({
enabled: z.boolean().default(false),
}).default({}),
scheduling: z.object({
enabled: z.boolean().default(true),
}).default({}),
}).default({}),
// Compliance features
compliance: z.object({
wcag20: z.boolean().default(true),
wcag21: z.boolean().default(true),
wcag22: z.boolean().default(true),
section508: z.boolean().default(false),
ada: z.boolean().default(true),
}).default({}),
// Integration features
integrations: z.object({
github: z.object({
enabled: z.boolean().default(true),
}).default({}),
slack: z.object({
enabled: z.boolean().default(true),
}).default({}),
teams: z.object({
enabled: z.boolean().default(false),
}).default({}),
jira: z.object({
enabled: z.boolean().default(false),
}).default({}),
}).default({}),
// Enterprise features
enterprise: z.object({
sso: z.object({
enabled: z.boolean().default(false),
providers: z.array(z.enum(['saml', 'oauth', 'ldap'])).default([]),
}).default({}),
whiteLabel: z.object({
enabled: z.boolean().default(false),
}).default({}),
audit: z.object({
enabled: z.boolean().default(true),
retention: z.number().default(365), // days
}).default({}),
}).default({}),
});
export type FeaturesConfig = z.infer<typeof featuresConfigSchema>;

View file

@ -0,0 +1,84 @@
import { z } from 'zod';
export const providersConfigSchema = z.object({
// Storage providers
storage: z.object({
type: z.enum(['local', 's3', 'gcs', 'azure']).default('local'),
local: z.object({
basePath: z.string().default('/tmp/wcag-ada'),
reports: z.string().default('reports'),
screenshots: z.string().default('screenshots'),
exports: z.string().default('exports'),
}).default({}),
s3: z.object({
enabled: z.boolean().default(false),
bucket: z.string().optional(),
region: z.string().default('us-east-1'),
accessKeyId: z.string().optional(),
secretAccessKey: z.string().optional(),
endpoint: z.string().optional(),
}).default({}),
}).default({}),
// Email providers
email: z.object({
enabled: z.boolean().default(true),
provider: z.enum(['smtp', 'sendgrid', 'ses', 'postmark']).default('smtp'),
from: z.object({
name: z.string().default('WCAG-ADA Compliance'),
email: z.string().email().default('noreply@wcag-ada.com'),
}).default({}),
smtp: z.object({
host: z.string().default('localhost'),
port: z.number().default(587),
secure: z.boolean().default(false),
auth: z.object({
user: z.string().optional(),
pass: z.string().optional(),
}).default({}),
}).default({}),
sendgrid: z.object({
apiKey: z.string().optional(),
}).default({}),
}).default({}),
// Authentication providers
auth: z.object({
jwt: z.object({
secret: z.string().default('change-me-in-production'),
expiresIn: z.string().default('7d'),
refreshExpiresIn: z.string().default('30d'),
}).default({}),
oauth: z.object({
google: z.object({
enabled: z.boolean().default(false),
clientId: z.string().optional(),
clientSecret: z.string().optional(),
}).default({}),
github: z.object({
enabled: z.boolean().default(false),
clientId: z.string().optional(),
clientSecret: z.string().optional(),
}).default({}),
}).default({}),
}).default({}),
// Analytics providers
analytics: z.object({
enabled: z.boolean().default(true),
provider: z.enum(['posthog', 'mixpanel', 'amplitude', 'custom']).default('posthog'),
posthog: z.object({
apiKey: z.string().optional(),
host: z.string().default('https://app.posthog.com'),
}).default({}),
}).default({}),
// CDN providers
cdn: z.object({
enabled: z.boolean().default(false),
provider: z.enum(['cloudflare', 'cloudfront', 'fastly']).default('cloudflare'),
baseUrl: z.string().optional(),
}).default({}),
});
export type ProvidersConfig = z.infer<typeof providersConfigSchema>;

View file

@ -0,0 +1,34 @@
import { z } from 'zod';
export const scannerConfigSchema = z.object({
concurrency: z.number().min(1).max(10).default(2),
timeout: z.number().min(30000).default(120000), // 2 minutes default
pageLoadTimeout: z.number().min(10000).default(30000),
headless: z.boolean().default(true),
blockResources: z.boolean().default(true),
viewport: z.object({
width: z.number().default(1280),
height: z.number().default(720),
deviceScaleFactor: z.number().default(1),
}).default({}),
browsers: z.object({
chromium: z.object({
enabled: z.boolean().default(true),
args: z.array(z.string()).default(['--no-sandbox', '--disable-setuid-sandbox']),
}).default({}),
}).default({}),
axe: z.object({
tags: z.array(z.string()).default(['wcag2aa', 'wcag21aa']),
resultTypes: z.array(z.enum(['violations', 'passes', 'incomplete', 'inapplicable']))
.default(['violations', 'passes', 'incomplete', 'inapplicable']),
}).default({}),
retries: z.object({
maxAttempts: z.number().default(3),
backoff: z.object({
type: z.enum(['exponential', 'fixed']).default('exponential'),
delay: z.number().default(2000),
}).default({}),
}).default({}),
});
export type ScannerConfig = z.infer<typeof scannerConfigSchema>;

View file

@ -0,0 +1,111 @@
import { z } from 'zod';
import {
baseAppConfigSchema,
logConfigSchema,
databaseConfigSchema,
serviceConfigSchema,
} from '@stock-bot/core-config';
import { scannerConfigSchema } from './scanner.schema';
import { workerConfigSchema } from './worker.schema';
import { featuresConfigSchema } from './features.schema';
import { providersConfigSchema } from './providers.schema';
// Service-specific configurations
const wcagServicesSchema = z.object({
api: serviceConfigSchema.extend({
port: z.number().default(3001),
cors: z.object({
enabled: z.boolean().default(true),
origin: z.string().default('*'),
credentials: z.boolean().default(true),
}).default({}),
rateLimit: z.object({
enabled: z.boolean().default(true),
windowMs: z.number().default(900000), // 15 minutes
max: z.number().default(100),
keyGenerator: z.enum(['ip', 'userId', 'apiKey']).default('userId'),
}).default({}),
pagination: z.object({
defaultLimit: z.number().default(20),
maxLimit: z.number().default(100),
}).default({}),
}),
dashboard: serviceConfigSchema.extend({
port: z.number().default(3000),
apiUrl: z.string().default('http://localhost:3001'),
publicUrl: z.string().default('http://localhost:3000'),
}),
worker: serviceConfigSchema.extend({
port: z.number().default(3002), // For health checks
}),
});
// Main WCAG application configuration schema
export const wcagAppConfigSchema = baseAppConfigSchema.extend({
appName: z.literal('wcag-ada').default('wcag-ada'),
// Core configurations
log: logConfigSchema,
database: databaseConfigSchema,
// WCAG-specific configurations
scanner: scannerConfigSchema,
worker: workerConfigSchema,
features: featuresConfigSchema,
providers: providersConfigSchema,
// Service configurations
services: wcagServicesSchema.default({}),
// Business logic configurations
compliance: z.object({
defaultLevel: z.object({
standard: z.enum(['WCAG20', 'WCAG21', 'WCAG22']).default('WCAG21'),
level: z.enum(['A', 'AA', 'AAA']).default('AA'),
}).default({}),
passingScore: z.object({
A: z.number().min(0).max(100).default(95),
AA: z.number().min(0).max(100).default(98),
AAA: z.number().min(0).max(100).default(100),
}).default({}),
criticalCriteria: z.array(z.string()).default([
'1.1.1', // Non-text Content
'1.3.1', // Info and Relationships
'1.4.3', // Contrast (Minimum)
'2.1.1', // Keyboard
'2.1.2', // No Keyboard Trap
'2.4.1', // Bypass Blocks
'2.4.2', // Page Titled
'4.1.2', // Name, Role, Value
]),
}).default({}),
// Subscription/pricing tiers
subscriptions: z.object({
enabled: z.boolean().default(true),
tiers: z.object({
starter: z.object({
websites: z.number().default(5),
scansPerMonth: z.number().default(500),
users: z.number().default(1),
price: z.number().default(49),
}).default({}),
professional: z.object({
websites: z.number().default(25),
scansPerMonth: z.number().default(5000),
users: z.number().default(5),
price: z.number().default(149),
}).default({}),
enterprise: z.object({
websites: z.number().default(-1), // Unlimited
scansPerMonth: z.number().default(-1),
users: z.number().default(-1),
price: z.number().default(499),
}).default({}),
}).default({}),
}).default({}),
});
export type WcagAppConfig = z.infer<typeof wcagAppConfigSchema>;

View file

@ -0,0 +1,49 @@
import { z } from 'zod';
export const workerConfigSchema = z.object({
enabled: z.boolean().default(true),
concurrency: z.number().min(1).max(10).default(2),
queueName: z.string().default('accessibility-scans'),
redis: z.object({
host: z.string().default('localhost'),
port: z.number().default(6379),
password: z.string().optional(),
db: z.number().default(2), // Different DB for WCAG
maxRetriesPerRequest: z.number().nullable().default(null),
}).default({}),
jobs: z.object({
scan: z.object({
priority: z.number().default(0),
attempts: z.number().default(3),
backoff: z.object({
type: z.enum(['exponential', 'fixed']).default('exponential'),
delay: z.number().default(2000),
}).default({}),
timeout: z.number().default(300000), // 5 minutes
removeOnComplete: z.object({
age: z.number().default(86400), // 24 hours
count: z.number().default(100),
}).default({}),
removeOnFail: z.object({
age: z.number().default(604800), // 7 days
count: z.number().default(500),
}).default({}),
}).default({}),
report: z.object({
priority: z.number().default(1),
attempts: z.number().default(2),
backoff: z.object({
type: z.enum(['exponential', 'fixed']).default('exponential'),
delay: z.number().default(5000),
}).default({}),
timeout: z.number().default(600000), // 10 minutes
}).default({}),
}).default({}),
scheduler: z.object({
enabled: z.boolean().default(true),
interval: z.number().default(60000), // Check every minute
timezone: z.string().default('UTC'),
}).default({}),
});
export type WorkerConfig = z.infer<typeof workerConfigSchema>;