From f235b377a08f0de05ff69ffdc047c75cf3923950 Mon Sep 17 00:00:00 2001 From: dev-dami Date: Sun, 25 Jan 2026 21:20:28 +0100 Subject: [PATCH 1/6] feat(core): add runtime plugins and env locking --- install.sh | 1 - packages/core/src/execution/execute.ts | 82 +++---- packages/core/src/index.ts | 30 ++- packages/core/src/runtime/docker-runtime.ts | 2 + packages/core/src/runtime/environment.ts | 186 ++++++++++++++++ packages/core/src/runtime/runtime-plugin.ts | 208 ++++++++++++++++++ packages/core/src/runtime/runtime-registry.ts | 78 +++++-- packages/core/src/runtime/runtime.types.ts | 1 + packages/core/src/security/audit.ts | 41 +--- packages/core/src/security/policy.ts | 23 +- packages/core/src/security/security.types.ts | 21 +- packages/runtime-node/Dockerfile | 40 ---- packages/runtime-node/package.json | 6 - packages/shared/src/types.ts | 38 +++- scripts/build-binaries.ts | 9 +- scripts/release.ts | 1 - 16 files changed, 549 insertions(+), 218 deletions(-) create mode 100644 packages/core/src/runtime/environment.ts create mode 100644 packages/core/src/runtime/runtime-plugin.ts delete mode 100644 packages/runtime-node/Dockerfile delete mode 100644 packages/runtime-node/package.json diff --git a/install.sh b/install.sh index bf08b8a..112bee7 100755 --- a/install.sh +++ b/install.sh @@ -241,7 +241,6 @@ do_install() { chmod +x "$BIN_DIR/ignite" [ -d "$tmp_dir/runtime-bun" ] && { mkdir -p "$INSTALL_DIR/runtime-bun"; cp -r "$tmp_dir/runtime-bun/"* "$INSTALL_DIR/runtime-bun/" 2>/dev/null || true; } - [ -d "$tmp_dir/runtime-node" ] && { mkdir -p "$INSTALL_DIR/runtime-node"; cp -r "$tmp_dir/runtime-node/"* "$INSTALL_DIR/runtime-node/" 2>/dev/null || true; } rm -rf "$tmp_dir" echo -e " ${GREEN}✓${NC} Installed to $BIN_DIR/ignite" >&2 diff --git a/packages/core/src/execution/execute.ts b/packages/core/src/execution/execute.ts index fe7d14d..dd7267a 100644 --- a/packages/core/src/execution/execute.ts +++ b/packages/core/src/execution/execute.ts @@ -1,49 +1,13 @@ -import { resolve } from 'node:path'; -import { ExecutionError, logger, type ExecutionMetrics } from '@ignite/shared'; +import { join } from 'node:path'; +import { mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { ExecutionError, logger, type ExecutionMetrics, parseRuntime } from '@ignite/shared'; import type { LoadedService } from '../service/service.types.js'; import { dockerBuild, dockerRun, isDockerAvailable } from '../runtime/docker-runtime.js'; import { getRuntimeConfig } from '../runtime/runtime-registry.js'; import { parseMetrics } from './metrics.js'; -function findRuntimeRoot(): string { - const { statSync } = require('node:fs'); - const { homedir } = require('node:os'); - - const locations = [ - process.env['IGNITE_HOME'], - resolve(homedir(), '.ignite'), - resolve(process.cwd(), 'packages'), - resolve(process.cwd(), '..', 'packages'), - ].filter(Boolean) as string[]; - - for (const loc of locations) { - try { - const bunRuntime = resolve(loc, 'runtime-bun', 'Dockerfile'); - const nodeRuntime = resolve(loc, 'runtime-node', 'Dockerfile'); - if (statSync(bunRuntime).isFile() || statSync(nodeRuntime).isFile()) { - return loc; - } - } catch { - continue; - } - } - - for (const loc of locations) { - try { - const packagesPath = resolve(loc); - if (statSync(packagesPath).isDirectory()) { - return packagesPath; - } - } catch { - continue; - } - } - throw new Error( - 'Could not find Ignite runtime files. ' + - 'Set IGNITE_HOME environment variable or reinstall Ignite.' - ); -} export interface ExecuteOptions { input?: unknown; @@ -85,6 +49,7 @@ export async function executeService( imageName, containerName, memoryLimitMb: service.config.service.memoryMb, + cpuLimit: service.config.service.cpuLimit, timeoutMs: service.config.service.timeoutMs, workDir: '/app', volumes: [ @@ -130,17 +95,32 @@ export async function buildServiceImage( ): Promise { const runtime = service.config.service.runtime; const runtimeConfig = getRuntimeConfig(runtime); - const runtimeRoot = findRuntimeRoot(); - const runtimePath = resolve(runtimeRoot, runtimeConfig.dockerfileDir); - - await dockerBuild({ - contextPath: service.servicePath, - dockerfilePath: `${runtimePath}/Dockerfile`, - imageName, - buildArgs: { - ENTRY_FILE: service.config.service.entry, - }, - }); + const spec = parseRuntime(runtime); + const version = spec.version ?? runtimeConfig.version; + + const dockerfileContent = runtimeConfig.plugin.generateDockerfile(version); + + const tempDir = join(tmpdir(), `ignite-build-${Date.now()}`); + const dockerfilePath = join(tempDir, 'Dockerfile'); + + try { + mkdirSync(tempDir, { recursive: true }); + writeFileSync(dockerfilePath, dockerfileContent, 'utf-8'); + + await dockerBuild({ + contextPath: service.servicePath, + dockerfilePath, + imageName, + buildArgs: { + ENTRY_FILE: service.config.service.entry, + }, + }); + } finally { + try { + rmSync(tempDir, { recursive: true, force: true }); + } catch { + } + } } export function getLastExecutionMs(serviceName: string): number | undefined { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9b16286..462363a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -13,8 +13,36 @@ export type { ExecuteOptions } from './execution/execute.js'; export { createReport, formatReportAsText } from './report/report.js'; export { generateWarnings } from './report/warnings.js'; -export { getRuntimeConfig, isValidRuntime, getSupportedRuntimes } from './runtime/runtime-registry.js'; +export { + getRuntimeConfig, + isValidRuntime, + getSupportedRuntimes, + registerRuntime, + unregisterRuntime, + getRuntimePlugin, + getSupportedVersions, +} from './runtime/runtime-registry.js'; export type { RuntimeConfig } from './runtime/runtime-registry.js'; +export { + createRuntimePlugin, + BUILTIN_RUNTIMES, + BUN_RUNTIME, + NODE_RUNTIME, + DENO_RUNTIME, + QUICKJS_RUNTIME, +} from './runtime/runtime-plugin.js'; +export type { RuntimePlugin, RuntimePluginConfig } from './runtime/runtime-plugin.js'; + +export { + loadEnvironmentManifest, + saveEnvironmentManifest, + createEnvironmentManifest, + checkEnvironmentDrift, + lockEnvironment, + formatEnvironmentInfo, +} from './runtime/environment.js'; +export type { EnvironmentInfo } from './runtime/environment.js'; + export { parseAuditFromOutput, formatSecurityAudit, DEFAULT_POLICY, loadPolicyFile, policyToDockerOptions } from './security/index.js'; export type { SecurityPolicy, SecurityAudit, SecurityEvent, SecuritySummary } from './security/index.js'; diff --git a/packages/core/src/runtime/docker-runtime.ts b/packages/core/src/runtime/docker-runtime.ts index de166cd..df842c5 100644 --- a/packages/core/src/runtime/docker-runtime.ts +++ b/packages/core/src/runtime/docker-runtime.ts @@ -43,6 +43,8 @@ export async function dockerRun(options: DockerRunOptions): Promise { + const manifestPath = join(servicePath, MANIFEST_FILENAME); + + try { + const content = await readFile(manifestPath, 'utf-8'); + return parseYaml(content) as EnvironmentManifest; + } catch { + return null; + } +} + +export async function saveEnvironmentManifest( + servicePath: string, + manifest: EnvironmentManifest +): Promise { + const manifestPath = join(servicePath, MANIFEST_FILENAME); + const content = stringifyYaml(manifest); + await writeFile(manifestPath, content, 'utf-8'); +} + +export async function createEnvironmentManifest( + servicePath: string, + runtime: string +): Promise { + const spec = parseRuntime(runtime); + const config = getRuntimeConfig(runtime); + const version = spec.version ?? config.version ?? 'latest'; + + const checksums: Record = {}; + + const filesToHash = [ + 'package.json', + 'bun.lockb', + 'package-lock.json', + 'yarn.lock', + 'pnpm-lock.yaml', + ]; + + for (const file of filesToHash) { + const filePath = join(servicePath, file); + try { + const content = await readFile(filePath); + checksums[file] = createHash('sha256').update(content).digest('hex'); + } catch { + } + } + + const lockfile = await detectLockfile(servicePath); + + return { + version: '1.0', + runtime: { name: spec.name, version }, + lockfile: lockfile ?? undefined, + checksums, + createdAt: new Date().toISOString(), + }; +} + +export async function checkEnvironmentDrift( + servicePath: string, + currentRuntime: string +): Promise { + const manifest = await loadEnvironmentManifest(servicePath); + + if (!manifest) { + return { + manifest: null, + isLocked: false, + isDrift: false, + }; + } + + const driftDetails: string[] = []; + const currentSpec = parseRuntime(currentRuntime); + + if (manifest.runtime.name !== currentSpec.name) { + driftDetails.push( + `Runtime changed: ${manifest.runtime.name} -> ${currentSpec.name}` + ); + } + + if (currentSpec.version && manifest.runtime.version !== currentSpec.version) { + driftDetails.push( + `Version changed: ${manifest.runtime.version} -> ${currentSpec.version}` + ); + } + + for (const [file, expectedHash] of Object.entries(manifest.checksums)) { + const filePath = join(servicePath, file); + try { + const content = await readFile(filePath); + const actualHash = createHash('sha256').update(content).digest('hex'); + if (actualHash !== expectedHash) { + driftDetails.push(`File modified: ${file}`); + } + } catch { + driftDetails.push(`File removed: ${file}`); + } + } + + return { + manifest, + isLocked: true, + isDrift: driftDetails.length > 0, + driftDetails: driftDetails.length > 0 ? driftDetails : undefined, + }; +} + +export async function lockEnvironment( + servicePath: string, + runtime: string +): Promise { + const manifest = await createEnvironmentManifest(servicePath, runtime); + await saveEnvironmentManifest(servicePath, manifest); + return manifest; +} + +async function detectLockfile(servicePath: string): Promise { + const lockfiles = [ + 'bun.lockb', + 'package-lock.json', + 'yarn.lock', + 'pnpm-lock.yaml', + ]; + + for (const lockfile of lockfiles) { + try { + await stat(join(servicePath, lockfile)); + return lockfile; + } catch { + continue; + } + } + + return null; +} + +export function formatEnvironmentInfo(info: EnvironmentInfo): string { + const lines: string[] = []; + + if (!info.isLocked) { + lines.push('Environment: Not locked'); + lines.push(' Run `ignite lock` to create ignite.lock'); + return lines.join('\n'); + } + + const manifest = info.manifest!; + lines.push(`Environment: Locked`); + lines.push(` Runtime: ${formatRuntime(manifest.runtime)}`); + lines.push(` Locked at: ${manifest.createdAt}`); + + if (manifest.lockfile) { + lines.push(` Lockfile: ${manifest.lockfile}`); + } + + if (info.isDrift) { + lines.push(''); + lines.push('WARNING: Environment drift detected:'); + for (const detail of info.driftDetails ?? []) { + lines.push(` - ${detail}`); + } + lines.push(''); + lines.push(' Run `ignite lock --update` to update the manifest'); + } else { + lines.push(''); + lines.push('Environment matches manifest'); + } + + return lines.join('\n'); +} diff --git a/packages/core/src/runtime/runtime-plugin.ts b/packages/core/src/runtime/runtime-plugin.ts new file mode 100644 index 0000000..f1d653d --- /dev/null +++ b/packages/core/src/runtime/runtime-plugin.ts @@ -0,0 +1,208 @@ +export interface RuntimePlugin { + name: string; + supportedVersions?: string[]; + defaultVersion?: string; + defaultEntry: string; + fileExtensions: string[]; + generateDockerfile(version?: string): string; +} + +export interface RuntimePluginConfig { + name: string; + baseImage: string; + defaultEntry: string; + fileExtensions: string[]; + packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun' | 'none'; + installCommand?: string; + runCommand: string; + entrypointExt?: string; + supportedVersions?: string[]; + defaultVersion?: string; + customEntrypoint?: string; +} + +const ENTRYPOINT_SCRIPT = `const entryFile = process.env.ENTRY_FILE || "index.ts"; +const startTime = Date.now(); + +async function run() { + try { + await import("/app/" + entryFile); + } catch (err) { + console.error(err); + process.exit(1); + } +} + +run().finally(() => { + const initTime = Date.now() - startTime; + const mem = Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100; + process.stderr.write("IGNITE_INIT_TIME:" + initTime + "\\n"); + process.stderr.write("IGNITE_MEMORY_MB:" + mem + "\\n"); +});`; + +export function createRuntimePlugin(config: RuntimePluginConfig): RuntimePlugin { + const lockfileMap: Record = { + npm: 'package-lock.json*', + yarn: 'yarn.lock*', + pnpm: 'pnpm-lock.yaml*', + bun: 'bun.lockb*', + none: '', + }; + + const installCommandMap: Record = { + npm: 'npm ci --omit=dev 2>/dev/null || npm install --omit=dev', + yarn: 'yarn install --production --frozen-lockfile 2>/dev/null || yarn install --production', + pnpm: 'pnpm install --prod --frozen-lockfile 2>/dev/null || pnpm install --prod', + bun: 'bun install --production --frozen-lockfile 2>/dev/null || bun install --production', + none: 'true', + }; + + const pm = config.packageManager ?? 'bun'; + const lockfile = lockfileMap[pm] ?? ''; + const installCmd = config.installCommand ?? installCommandMap[pm] ?? 'true'; + const ext = config.entrypointExt ?? (pm === 'bun' ? 'ts' : 'mjs'); + const entryScript = config.customEntrypoint ?? ENTRYPOINT_SCRIPT; + + return { + name: config.name, + supportedVersions: config.supportedVersions, + defaultVersion: config.defaultVersion, + defaultEntry: config.defaultEntry, + fileExtensions: config.fileExtensions, + + generateDockerfile(version?: string): string { + let baseImage = config.baseImage; + if (version) { + const versionPattern = /:[\w.-]+(-alpine)?$/; + if (versionPattern.test(baseImage)) { + const suffix = baseImage.includes('-alpine') ? '-alpine' : ''; + baseImage = baseImage.replace(versionPattern, `:${version}${suffix}`); + } + } + + const copyLockfile = lockfile ? `COPY package.json ${lockfile} ./` : 'COPY package.json* ./'; + const installStep = pm !== 'none' + ? `RUN if [ -f package.json ]; then ${installCmd}; fi` + : ''; + + return `FROM ${baseImage} + +RUN adduser -D -u 1001 ignite + +WORKDIR /app + +${copyLockfile} +${installStep} + +COPY --chown=ignite:ignite . . + +ARG ENTRY_FILE=${config.defaultEntry} +ENV ENTRY_FILE=\${ENTRY_FILE} + +RUN printf '%s\\n' \\ +${entryScript.split('\n').map(line => ` '${line.replace(/'/g, "'\\''")}' \\`).join('\n')} + > /entrypoint.${ext} && chown ignite:ignite /entrypoint.${ext} + +USER ignite + +CMD ["${config.runCommand}", "/entrypoint.${ext}"] +`; + }, + }; +} + +export const BUN_RUNTIME: RuntimePlugin = createRuntimePlugin({ + name: 'bun', + baseImage: 'oven/bun:1.3-alpine', + defaultEntry: 'index.ts', + fileExtensions: ['.ts', '.js', '.tsx', '.jsx'], + packageManager: 'bun', + runCommand: 'bun', + entrypointExt: 'ts', + supportedVersions: ['1.0', '1.1', '1.2', '1.3'], + defaultVersion: '1.3', +}); + +export const NODE_RUNTIME: RuntimePlugin = createRuntimePlugin({ + name: 'node', + baseImage: 'node:20-alpine', + defaultEntry: 'index.js', + fileExtensions: ['.js', '.mjs', '.cjs'], + packageManager: 'npm', + runCommand: 'node', + entrypointExt: 'mjs', + supportedVersions: ['18', '20', '22'], + defaultVersion: '20', +}); + +export const DENO_RUNTIME: RuntimePlugin = createRuntimePlugin({ + name: 'deno', + baseImage: 'denoland/deno:alpine-2.0', + defaultEntry: 'index.ts', + fileExtensions: ['.ts', '.js', '.tsx', '.jsx'], + packageManager: 'none', + runCommand: 'deno run --allow-env --allow-read=/app', + entrypointExt: 'ts', + supportedVersions: ['1.40', '1.41', '1.42', '2.0'], + defaultVersion: '2.0', +}); + +export const QUICKJS_RUNTIME: RuntimePlugin = { + name: 'quickjs', + supportedVersions: ['2024-01-13', '2023-12-09', 'latest'], + defaultVersion: 'latest', + defaultEntry: 'index.js', + fileExtensions: ['.js'], + + generateDockerfile(_version?: string): string { + return `FROM alpine:3.19 AS builder + +RUN apk add --no-cache git make gcc musl-dev + +WORKDIR /build +RUN git clone --depth 1 https://github.com/nickg/quickjs.git . && \\ + make qjs && \\ + strip qjs + +FROM alpine:3.19 + +RUN adduser -D -u 1001 ignite + +COPY --from=builder /build/qjs /usr/local/bin/qjs + +WORKDIR /app + +COPY --chown=ignite:ignite . . + +ARG ENTRY_FILE=index.js +ENV ENTRY_FILE=\${ENTRY_FILE} + +RUN printf '%s\\n' \\ + 'const entryFile = std.getenv("ENTRY_FILE") || "index.js";' \\ + 'const startTime = Date.now();' \\ + '' \\ + 'try {' \\ + ' std.loadScript("/app/" + entryFile);' \\ + '} catch (err) {' \\ + ' console.log("Error:", err);' \\ + ' std.exit(1);' \\ + '}' \\ + '' \\ + 'const initTime = Date.now() - startTime;' \\ + 'std.err.puts("IGNITE_INIT_TIME:" + initTime + "\\\\n");' \\ + 'std.err.puts("IGNITE_MEMORY_MB:0\\\\n");' \\ + > /entrypoint.js && chown ignite:ignite /entrypoint.js + +USER ignite + +CMD ["qjs", "--std", "/entrypoint.js"] +`; + }, +}; + +export const BUILTIN_RUNTIMES: Record = { + bun: BUN_RUNTIME, + node: NODE_RUNTIME, + deno: DENO_RUNTIME, + quickjs: QUICKJS_RUNTIME, +}; diff --git a/packages/core/src/runtime/runtime-registry.ts b/packages/core/src/runtime/runtime-registry.ts index 9397a1d..62c7ec9 100644 --- a/packages/core/src/runtime/runtime-registry.ts +++ b/packages/core/src/runtime/runtime-registry.ts @@ -1,35 +1,69 @@ -import type { RuntimeName } from '@ignite/shared'; +import { parseRuntime, type RuntimeSpec } from '@ignite/shared'; +import { BUILTIN_RUNTIMES, type RuntimePlugin } from './runtime-plugin.js'; export interface RuntimeConfig { - name: RuntimeName; + name: string; + version?: string; dockerfileDir: string; defaultEntry: string; fileExtensions: string[]; + plugin: RuntimePlugin; } -const runtimeConfigs: Record = { - node: { - name: 'node', - dockerfileDir: 'runtime-node', - defaultEntry: 'index.js', - fileExtensions: ['.js', '.mjs', '.cjs'], - }, - bun: { - name: 'bun', - dockerfileDir: 'runtime-bun', - defaultEntry: 'index.ts', - fileExtensions: ['.ts', '.js', '.tsx', '.jsx'], - }, -}; +const customRuntimes = new Map(); -export function getRuntimeConfig(runtime: RuntimeName): RuntimeConfig { - return runtimeConfigs[runtime]; +export function registerRuntime(plugin: RuntimePlugin): void { + customRuntimes.set(plugin.name, plugin); } -export function isValidRuntime(runtime: string): runtime is RuntimeName { - return runtime === 'node' || runtime === 'bun'; +export function unregisterRuntime(name: string): boolean { + return customRuntimes.delete(name); } -export function getSupportedRuntimes(): RuntimeName[] { - return Object.keys(runtimeConfigs) as RuntimeName[]; +export function getRegisteredRuntimes(): string[] { + return [...Object.keys(BUILTIN_RUNTIMES), ...customRuntimes.keys()]; } + +export function getRuntimePlugin(name: string): RuntimePlugin | undefined { + return customRuntimes.get(name) ?? BUILTIN_RUNTIMES[name]; +} + +export function getRuntimeConfig(runtime: string): RuntimeConfig { + const spec = parseRuntime(runtime); + const plugin = getRuntimePlugin(spec.name); + + if (!plugin) { + throw new Error( + `Unknown runtime: ${spec.name}. ` + + `Available runtimes: ${getRegisteredRuntimes().join(', ')}. ` + + `Register custom runtimes with registerRuntime().` + ); + } + + const version = spec.version ?? plugin.defaultVersion; + + return { + name: spec.name, + version, + dockerfileDir: `runtime-${spec.name}`, + defaultEntry: plugin.defaultEntry, + fileExtensions: plugin.fileExtensions, + plugin, + }; +} + +export function isValidRuntime(runtime: string): boolean { + const spec = parseRuntime(runtime); + return getRuntimePlugin(spec.name) !== undefined; +} + +export function getSupportedRuntimes(): string[] { + return getRegisteredRuntimes(); +} + +export function getSupportedVersions(runtimeName: string): string[] { + const plugin = getRuntimePlugin(runtimeName); + return plugin?.supportedVersions ?? []; +} + +export { type RuntimePlugin, createRuntimePlugin } from './runtime-plugin.js'; diff --git a/packages/core/src/runtime/runtime.types.ts b/packages/core/src/runtime/runtime.types.ts index 440a997..9b8aa2f 100644 --- a/packages/core/src/runtime/runtime.types.ts +++ b/packages/core/src/runtime/runtime.types.ts @@ -9,6 +9,7 @@ export interface DockerRunOptions { imageName: string; containerName: string; memoryLimitMb: number; + cpuLimit?: number; timeoutMs: number; workDir: string; volumes: VolumeMount[]; diff --git a/packages/core/src/security/audit.ts b/packages/core/src/security/audit.ts index 82da6fd..4e02941 100644 --- a/packages/core/src/security/audit.ts +++ b/packages/core/src/security/audit.ts @@ -25,11 +25,6 @@ export function parseAuditFromOutput( { pattern: /permission denied.*?['"]?([\/\w.-]+)['"]?/gi, action: 'blocked' as const }, ]; - const processPatterns = [ - { pattern: /spawn.*EACCES/gi, action: 'spawn' as const }, - { pattern: /child_process.*blocked/gi, action: 'spawn' as const }, - ]; - const combined = stdout + '\n' + stderr; for (const { pattern, action } of networkPatterns) { @@ -61,20 +56,6 @@ export function parseAuditFromOutput( } } - for (const { pattern, action } of processPatterns) { - const matches = combined.matchAll(pattern); - for (const match of matches) { - events.push({ - type: 'process', - action, - target: extractCommand(match[0]) || 'unknown command', - timestamp: now, - allowed: false, - details: match[0], - }); - } - } - const summary = calculateSummary(events); return { events, summary, policy }; @@ -95,15 +76,9 @@ function extractPath(text: string): string | null { return pathMatch?.[1] ?? null; } -function extractCommand(text: string): string | null { - const cmdMatch = text.match(/spawn\s+['"]?(\w+)['"]?/i); - return cmdMatch?.[1] ?? null; -} - function calculateSummary(events: SecurityEvent[]): SecuritySummary { const networkEvents = events.filter(e => e.type === 'network'); const filesystemEvents = events.filter(e => e.type === 'filesystem'); - const processEvents = events.filter(e => e.type === 'process'); const hasViolations = events.some(e => !e.allowed); @@ -113,8 +88,6 @@ function calculateSummary(events: SecurityEvent[]): SecuritySummary { filesystemReads: filesystemEvents.filter(e => e.action === 'read').length, filesystemWrites: filesystemEvents.filter(e => e.action === 'write').length, filesystemBlocked: filesystemEvents.filter(e => !e.allowed).length, - processSpawns: processEvents.length, - processBlocked: processEvents.filter(e => !e.allowed).length, overallStatus: hasViolations ? 'violations' : 'clean', }; } @@ -136,7 +109,6 @@ export function formatSecurityAudit(audit: SecurityAudit): string { lines.push(` ${DIM}Policy:${NC}`); lines.push(` Network: ${audit.policy.network.enabled ? `${YELLOW}enabled${NC}` : `${GREEN}blocked${NC}`}`); lines.push(` Filesystem: ${audit.policy.filesystem.readOnly ? `${GREEN}read-only${NC}` : `${YELLOW}read-write${NC}`}`); - lines.push(` Process spawn: ${audit.policy.process.allowSpawn ? `${YELLOW}allowed${NC}` : `${GREEN}blocked${NC}`}`); lines.push(''); if (audit.events.length === 0) { @@ -147,7 +119,6 @@ export function formatSecurityAudit(audit: SecurityAudit): string { const networkEvents = audit.events.filter(e => e.type === 'network'); const fsEvents = audit.events.filter(e => e.type === 'filesystem'); - const procEvents = audit.events.filter(e => e.type === 'process'); if (networkEvents.length > 0) { lines.push(''); @@ -168,16 +139,6 @@ export function formatSecurityAudit(audit: SecurityAudit): string { lines.push(` ${icon} ${event.action}: ${event.target} ${DIM}(${status})${NC}`); } } - - if (procEvents.length > 0) { - lines.push(''); - lines.push(` ${BOLD}Process${NC}`); - for (const event of procEvents) { - const icon = event.allowed ? `${GREEN}✓${NC}` : `${RED}✗${NC}`; - const status = event.allowed ? 'allowed' : 'blocked'; - lines.push(` ${icon} ${event.action}: ${event.target} ${DIM}(${status})${NC}`); - } - } } lines.push(''); @@ -187,7 +148,7 @@ export function formatSecurityAudit(audit: SecurityAudit): string { if (summary.overallStatus === 'clean') { lines.push(` ${GREEN}✓ Security Status: CLEAN${NC}`); } else { - const violations = summary.networkBlocked + summary.filesystemBlocked + summary.processBlocked; + const violations = summary.networkBlocked + summary.filesystemBlocked; lines.push(` ${RED}✗ Security Status: ${violations} VIOLATION(S) BLOCKED${NC}`); } lines.push(''); diff --git a/packages/core/src/security/policy.ts b/packages/core/src/security/policy.ts index 0b87ad7..948875c 100644 --- a/packages/core/src/security/policy.ts +++ b/packages/core/src/security/policy.ts @@ -1,24 +1,16 @@ import { readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import { parse as parseYaml } from 'yaml'; -import type { SecurityPolicy, NetworkPolicy, FilesystemPolicy, ProcessPolicy } from './security.types.js'; +import type { SecurityPolicy, NetworkPolicy, FilesystemPolicy } from './security.types.js'; import { DEFAULT_POLICY } from './security.types.js'; export interface PolicyFile { security?: { network?: { enabled?: boolean; - allowedHosts?: string[]; - allowedPorts?: number[]; }; filesystem?: { readOnly?: boolean; - allowedWritePaths?: string[]; - blockedReadPaths?: string[]; - }; - process?: { - allowSpawn?: boolean; - allowedCommands?: string[]; }; }; } @@ -49,22 +41,13 @@ function mergePolicies(base: SecurityPolicy, file: PolicyFile): SecurityPolicy { const network: NetworkPolicy = { enabled: security.network?.enabled ?? base.network.enabled, - allowedHosts: security.network?.allowedHosts ?? base.network.allowedHosts, - allowedPorts: security.network?.allowedPorts ?? base.network.allowedPorts, }; const filesystem: FilesystemPolicy = { readOnly: security.filesystem?.readOnly ?? base.filesystem.readOnly, - allowedWritePaths: security.filesystem?.allowedWritePaths ?? base.filesystem.allowedWritePaths, - blockedReadPaths: security.filesystem?.blockedReadPaths ?? base.filesystem.blockedReadPaths, - }; - - const process: ProcessPolicy = { - allowSpawn: security.process?.allowSpawn ?? base.process.allowSpawn, - allowedCommands: security.process?.allowedCommands ?? base.process.allowedCommands, }; - return { network, filesystem, process }; + return { network, filesystem }; } export function policyToDockerOptions(policy: SecurityPolicy): { @@ -79,6 +62,6 @@ export function policyToDockerOptions(policy: SecurityPolicy): { readOnlyRootfs: policy.filesystem.readOnly, dropCapabilities: true, noNewPrivileges: true, - tmpfsPaths: policy.filesystem.allowedWritePaths ?? ['/tmp'], + tmpfsPaths: ['/tmp'], }; } diff --git a/packages/core/src/security/security.types.ts b/packages/core/src/security/security.types.ts index 2dbd4cd..a17a11a 100644 --- a/packages/core/src/security/security.types.ts +++ b/packages/core/src/security/security.types.ts @@ -1,29 +1,19 @@ export interface SecurityPolicy { network: NetworkPolicy; filesystem: FilesystemPolicy; - process: ProcessPolicy; } export interface NetworkPolicy { enabled: boolean; - allowedHosts?: string[]; - allowedPorts?: number[]; } export interface FilesystemPolicy { readOnly: boolean; - allowedWritePaths?: string[]; - blockedReadPaths?: string[]; -} - -export interface ProcessPolicy { - allowSpawn: boolean; - allowedCommands?: string[]; } export interface SecurityEvent { - type: 'network' | 'filesystem' | 'process'; - action: 'read' | 'write' | 'connect' | 'spawn' | 'blocked'; + type: 'network' | 'filesystem'; + action: 'read' | 'write' | 'connect' | 'blocked'; target: string; timestamp: number; allowed: boolean; @@ -42,8 +32,6 @@ export interface SecuritySummary { filesystemReads: number; filesystemWrites: number; filesystemBlocked: number; - processSpawns: number; - processBlocked: number; overallStatus: 'clean' | 'violations'; } @@ -53,10 +41,5 @@ export const DEFAULT_POLICY: SecurityPolicy = { }, filesystem: { readOnly: true, - allowedWritePaths: ['/tmp'], - blockedReadPaths: ['/etc/passwd', '/etc/shadow', '/etc/hosts', '/proc', '/sys'], - }, - process: { - allowSpawn: false, }, }; diff --git a/packages/runtime-node/Dockerfile b/packages/runtime-node/Dockerfile deleted file mode 100644 index b2cf11e..0000000 --- a/packages/runtime-node/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -FROM node:20-alpine - -RUN adduser -D -u 1001 ignite - -WORKDIR /app - -COPY package*.json ./ -RUN if [ -f package-lock.json ]; then npm ci --only=production; \ - elif [ -f package.json ]; then npm install --only=production; \ - fi - -COPY --chown=ignite:ignite . . - -ARG ENTRY_FILE=index.js -ENV ENTRY_FILE=${ENTRY_FILE} - -RUN printf '%s\n' \ - 'const entryFile = process.env.ENTRY_FILE || "index.js";' \ - 'const startTime = Date.now();' \ - '' \ - 'async function run() {' \ - ' try {' \ - ' await import("/app/" + entryFile);' \ - ' } catch (err) {' \ - ' console.error(err);' \ - ' process.exit(1);' \ - ' }' \ - '}' \ - '' \ - 'run().finally(() => {' \ - ' const initTime = Date.now() - startTime;' \ - ' const mem = Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100;' \ - ' process.stderr.write("IGNITE_INIT_TIME:" + initTime + "\\n");' \ - ' process.stderr.write("IGNITE_MEMORY_MB:" + mem + "\\n");' \ - '});' \ - > /entrypoint.mjs && chown ignite:ignite /entrypoint.mjs - -USER ignite - -CMD ["node", "/entrypoint.mjs"] diff --git a/packages/runtime-node/package.json b/packages/runtime-node/package.json deleted file mode 100644 index b9ef55e..0000000 --- a/packages/runtime-node/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "@ignite/runtime-node", - "version": "0.6.0", - "private": true, - "description": "Node.js runtime adapter for Ignite" -} diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 8935348..12a96af 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -1,16 +1,35 @@ -export type RuntimeName = 'node' | 'bun'; - export interface ServiceConfig { service: { name: string; - runtime: RuntimeName; + runtime: string; entry: string; memoryMb: number; + cpuLimit?: number; timeoutMs: number; env?: Record; }; } +export interface RuntimeSpec { + name: string; + version?: string; +} + +export function parseRuntime(runtime: string): RuntimeSpec { + const atIndex = runtime.lastIndexOf('@'); + if (atIndex > 0) { + return { + name: runtime.slice(0, atIndex), + version: runtime.slice(atIndex + 1), + }; + } + return { name: runtime }; +} + +export function formatRuntime(spec: RuntimeSpec): string { + return spec.version ? `${spec.name}@${spec.version}` : spec.name; +} + export interface ExecutionMetrics { executionTimeMs: number; memoryUsageMb: number; @@ -50,11 +69,10 @@ export interface Warning { suggestion?: string; } -export interface RuntimeConfig { - imageName: string; - containerName: string; - memoryLimit: string; - timeoutMs: number; - workDir: string; - env: Record; +export interface EnvironmentManifest { + version: string; + runtime: RuntimeSpec; + lockfile?: string; + checksums: Record; + createdAt: string; } diff --git a/scripts/build-binaries.ts b/scripts/build-binaries.ts index f4b5f83..721e2c0 100755 --- a/scripts/build-binaries.ts +++ b/scripts/build-binaries.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun import { $ } from "bun"; -import { mkdir, rm, copyFile, readdir } from "node:fs/promises"; +import { mkdir, rm, copyFile } from "node:fs/promises"; import { join } from "node:path"; const ROOT = join(import.meta.dir, ".."); @@ -44,16 +44,11 @@ async function build() { console.log("\n3. Copying runtime Dockerfiles..."); await mkdir(join(DIST_DIR, "runtime-bun"), { recursive: true }); - await mkdir(join(DIST_DIR, "runtime-node"), { recursive: true }); await copyFile( join(ROOT, "packages/runtime-bun/Dockerfile"), join(DIST_DIR, "runtime-bun/Dockerfile") ); - await copyFile( - join(ROOT, "packages/runtime-node/Dockerfile"), - join(DIST_DIR, "runtime-node/Dockerfile") - ); console.log("\n4. Creating release archives..."); for (const { name, target } of TARGETS) { @@ -61,7 +56,7 @@ async function build() { const tarName = `${name}.tar.gz`; try { - await $`tar -czvf ${join(DIST_DIR, tarName)} -C ${BIN_DIR} ${name} -C ${DIST_DIR} runtime-bun runtime-node`.quiet(); + await $`tar -czvf ${join(DIST_DIR, tarName)} -C ${BIN_DIR} ${name} -C ${DIST_DIR} runtime-bun`.quiet(); console.log(` ✓ Created ${tarName}`); } catch (err) { console.error(` ✗ Failed to create ${tarName}`); diff --git a/scripts/release.ts b/scripts/release.ts index 07fad43..c9bd56a 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -78,7 +78,6 @@ async function updatePackageVersion(version: string): Promise { "packages/http/package.json", "packages/shared/package.json", "packages/runtime-bun/package.json", - "packages/runtime-node/package.json", ]; for (const pkgPath of packages) { From 121dac1fa2ba4da59eac7ecc7b5238fc6aeda6b7 Mon Sep 17 00:00:00 2001 From: dev-dami Date: Sun, 25 Jan 2026 21:20:41 +0100 Subject: [PATCH 2/6] feat(cli): add env/lock commands and runtime override --- packages/cli/src/commands/env.ts | 68 ++++++++++++++++++++++++++ packages/cli/src/commands/init.ts | 80 ++++++++++++++++--------------- packages/cli/src/commands/lock.ts | 46 ++++++++++++++++++ packages/cli/src/commands/run.ts | 12 ++++- packages/cli/src/index.ts | 16 +++++++ 5 files changed, 183 insertions(+), 39 deletions(-) create mode 100644 packages/cli/src/commands/env.ts create mode 100644 packages/cli/src/commands/lock.ts diff --git a/packages/cli/src/commands/env.ts b/packages/cli/src/commands/env.ts new file mode 100644 index 0000000..a237dcf --- /dev/null +++ b/packages/cli/src/commands/env.ts @@ -0,0 +1,68 @@ +import { loadService, checkEnvironmentDrift, formatEnvironmentInfo, getSupportedRuntimes, getSupportedVersions, getRuntimePlugin } from '@ignite/core'; +import { logger } from '@ignite/shared'; + +interface EnvOptions { + runtimes?: boolean; +} + +export async function envCommand(servicePath: string | undefined, options: EnvOptions): Promise { + try { + if (options.runtimes) { + listRuntimes(); + return; + } + + if (!servicePath) { + logger.error('Service path required. Use `ignite env ` or `ignite env --runtimes`'); + process.exit(1); + } + + const service = await loadService(servicePath); + const serviceName = service.config.service.name; + const runtime = service.config.service.runtime; + + logger.info(`Environment info for ${serviceName}`); + console.log(''); + + console.log(`Service: ${serviceName}`); + console.log(`Runtime: ${runtime}`); + console.log(''); + + const info = await checkEnvironmentDrift(service.servicePath, runtime); + console.log(formatEnvironmentInfo(info)); + + } catch (err) { + logger.error(`Failed to get environment info: ${(err as Error).message}`); + process.exit(1); + } +} + +function listRuntimes(): void { + console.log('Supported Runtimes:\n'); + + const runtimes = getSupportedRuntimes(); + + for (const name of runtimes) { + const plugin = getRuntimePlugin(name); + if (!plugin) continue; + + const versions = getSupportedVersions(name); + const defaultVersion = plugin.defaultVersion ?? 'latest'; + + console.log(` ${name}`); + console.log(` Default entry: ${plugin.defaultEntry}`); + console.log(` Extensions: ${plugin.fileExtensions.join(', ')}`); + + if (versions.length > 0) { + console.log(` Versions: ${versions.join(', ')} (default: ${defaultVersion})`); + } + + console.log(''); + } + + console.log('Usage examples:'); + console.log(' service.yaml: runtime: bun'); + console.log(' service.yaml: runtime: bun@1.2'); + console.log(' service.yaml: runtime: node@20'); + console.log(' ignite run . --runtime node@22'); +} diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 8124880..b15fff4 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -1,14 +1,14 @@ import { writeFile, mkdir } from 'node:fs/promises'; import { join } from 'node:path'; -import { logger, type RuntimeName } from '@ignite/shared'; +import { logger } from '@ignite/shared'; +import { isValidRuntime, getRuntimeConfig } from '@ignite/core'; interface InitOptions { path?: string; runtime?: string; } -function getServiceYamlTemplate(serviceName: string, runtime: RuntimeName): string { - const entry = runtime === 'bun' ? 'index.ts' : 'index.js'; +function getServiceYamlTemplate(serviceName: string, runtime: string, entry: string): string { return `service: name: ${serviceName} runtime: ${runtime} @@ -20,44 +20,24 @@ function getServiceYamlTemplate(serviceName: string, runtime: RuntimeName): stri `; } -const NODE_INDEX_TEMPLATE = `const input = process.env.IGNITE_INPUT ? JSON.parse(process.env.IGNITE_INPUT) : {}; - -async function handler(event) { - console.log('Received event:', event); - - return { - statusCode: 200, - body: { message: 'Hello from Ignite!', input: event } - }; -} - -handler(input) - .then(result => { - console.log(JSON.stringify(result, null, 2)); - process.exit(0); - }) - .catch(err => { - console.error(err); - process.exit(1); - }); -`; - -function getPackageJsonTemplate(serviceName: string, runtime: RuntimeName): string { - const entry = runtime === 'bun' ? 'index.ts' : 'index.js'; +function getPackageJsonTemplate(serviceName: string, runtime: string, entry: string): string { + const startCmd = runtime === 'bun' ? `bun run ${entry}` : + runtime === 'deno' ? `deno run ${entry}` : + `node ${entry}`; return JSON.stringify({ name: serviceName, version: '1.0.0', type: 'module', main: entry, scripts: { - start: runtime === 'bun' ? `bun run ${entry}` : `node ${entry}`, + start: startCmd, preflight: 'ignite preflight .', run: 'ignite run .' } }, null, 2) + '\n'; } -const BUN_INDEX_TEMPLATE = `interface Event { +const TS_INDEX_TEMPLATE = `interface Event { [key: string]: unknown; } @@ -91,31 +71,55 @@ handler(input) }); `; +const JS_INDEX_TEMPLATE = `const input = process.env.IGNITE_INPUT ? JSON.parse(process.env.IGNITE_INPUT) : {}; + +async function handler(event) { + console.log('Received event:', event); + + return { + statusCode: 200, + body: { message: 'Hello from Ignite!', input: event } + }; +} + +handler(input) + .then(result => { + console.log(JSON.stringify(result, null, 2)); + process.exit(0); + }) + .catch(err => { + console.error(err); + process.exit(1); + }); +`; + export async function initCommand(serviceName: string, options: InitOptions): Promise { - const runtime = (options.runtime as RuntimeName) ?? 'bun'; + const runtime = options.runtime ?? 'bun'; - if (runtime !== 'node' && runtime !== 'bun') { - logger.error(`Invalid runtime: ${options.runtime}. Must be 'node' or 'bun'.`); + if (!isValidRuntime(runtime)) { + logger.error(`Invalid runtime: ${runtime}. Use a valid runtime like 'bun', 'node', 'deno', or 'quickjs'.`); process.exit(1); } + const runtimeConfig = getRuntimeConfig(runtime); + const entry = runtimeConfig.defaultEntry; + const isTs = entry.endsWith('.ts') || entry.endsWith('.tsx'); + const targetPath = options.path ?? serviceName; const absolutePath = join(process.cwd(), targetPath); - const entryFile = runtime === 'bun' ? 'index.ts' : 'index.js'; - const indexTemplate = runtime === 'bun' ? BUN_INDEX_TEMPLATE : NODE_INDEX_TEMPLATE; try { await mkdir(absolutePath, { recursive: true }); - await writeFile(join(absolutePath, 'service.yaml'), getServiceYamlTemplate(serviceName, runtime)); - await writeFile(join(absolutePath, 'package.json'), getPackageJsonTemplate(serviceName, runtime)); - await writeFile(join(absolutePath, entryFile), indexTemplate); + await writeFile(join(absolutePath, 'service.yaml'), getServiceYamlTemplate(serviceName, runtime, entry)); + await writeFile(join(absolutePath, 'package.json'), getPackageJsonTemplate(serviceName, runtime, entry)); + await writeFile(join(absolutePath, entry), isTs ? TS_INDEX_TEMPLATE : JS_INDEX_TEMPLATE); logger.success(`Initialized ${runtime} service "${serviceName}" at ${absolutePath}`); logger.info(''); logger.info('Next steps:'); logger.info(` 1. cd ${targetPath}`); - logger.info(` 2. Edit ${entryFile} with your function logic`); + logger.info(` 2. Edit ${entry} with your function logic`); logger.info(' 3. Run: ignite preflight .'); logger.info(' 4. Run: ignite run .'); } catch (err) { diff --git a/packages/cli/src/commands/lock.ts b/packages/cli/src/commands/lock.ts new file mode 100644 index 0000000..bffeed4 --- /dev/null +++ b/packages/cli/src/commands/lock.ts @@ -0,0 +1,46 @@ +import { loadService, lockEnvironment, checkEnvironmentDrift, formatEnvironmentInfo } from '@ignite/core'; +import { logger } from '@ignite/shared'; + +interface LockOptions { + update?: boolean; + check?: boolean; +} + +export async function lockCommand(servicePath: string, options: LockOptions): Promise { + try { + const service = await loadService(servicePath); + const serviceName = service.config.service.name; + const runtime = service.config.service.runtime; + + if (options.check) { + logger.info(`Checking environment for ${serviceName}...`); + const info = await checkEnvironmentDrift(service.servicePath, runtime); + console.log(formatEnvironmentInfo(info)); + + if (info.isDrift) { + process.exit(1); + } + return; + } + + const existingInfo = await checkEnvironmentDrift(service.servicePath, runtime); + + if (existingInfo.isLocked && !options.update) { + logger.warn('Environment already locked. Use --update to refresh the manifest.'); + console.log(formatEnvironmentInfo(existingInfo)); + return; + } + + logger.info(`Locking environment for ${serviceName}...`); + const manifest = await lockEnvironment(service.servicePath, runtime); + + logger.success(`Created ignite.lock for ${serviceName}`); + console.log(` Runtime: ${manifest.runtime.name}@${manifest.runtime.version}`); + console.log(` Lockfile: ${manifest.lockfile ?? 'none detected'}`); + console.log(` Checksums: ${Object.keys(manifest.checksums).length} file(s)`); + + } catch (err) { + logger.error(`Lock failed: ${(err as Error).message}`); + process.exit(1); + } +} diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index 66ea620..45b22c1 100644 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -1,4 +1,4 @@ -import { loadService, executeService, runPreflight, createReport, formatReportAsText, getImageName, buildServiceImage, parseAuditFromOutput, formatSecurityAudit, DEFAULT_POLICY } from '@ignite/core'; +import { loadService, executeService, runPreflight, createReport, formatReportAsText, getImageName, buildServiceImage, parseAuditFromOutput, formatSecurityAudit, DEFAULT_POLICY, isValidRuntime } from '@ignite/core'; import { logger, ConfigError } from '@ignite/shared'; interface RunOptions { @@ -6,11 +6,21 @@ interface RunOptions { skipPreflight?: boolean; json?: boolean; audit?: boolean; + runtime?: string; } export async function runCommand(servicePath: string, options: RunOptions): Promise { try { const service = await loadService(servicePath); + + if (options.runtime) { + if (!isValidRuntime(options.runtime)) { + throw new ConfigError(`Invalid runtime: ${options.runtime}`); + } + service.config.service.runtime = options.runtime; + logger.info(`Runtime override: ${options.runtime}`); + } + const serviceName = service.config.service.name; const imageName = getImageName(serviceName); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a0edfc3..8745ef4 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -5,6 +5,8 @@ import { runCommand } from './commands/run.js'; import { preflightCommand } from './commands/preflight.js'; import { reportCommand } from './commands/report.js'; import { serveCommand } from './commands/serve.js'; +import { lockCommand } from './commands/lock.js'; +import { envCommand } from './commands/env.js'; const program = new Command(); @@ -24,6 +26,7 @@ program .command('run ') .description('Execute a service in an isolated container') .option('-i, --input ', 'JSON input to pass to the service') + .option('-r, --runtime ', 'Override runtime (e.g., node@20, bun@1.2)') .option('--skip-preflight', 'Skip preflight checks before execution') .option('--json', 'Output results as JSON') .option('--audit', 'Run with security audit (blocks network, read-only filesystem)') @@ -43,4 +46,17 @@ program program.addCommand(serveCommand); +program + .command('lock ') + .description('Create or update ignite.lock manifest for reproducible environments') + .option('-u, --update', 'Update existing manifest') + .option('-c, --check', 'Check for environment drift without modifying') + .action(lockCommand); + +program + .command('env [service]') + .description('Show environment info and available runtimes') + .option('--runtimes', 'List all supported runtimes and versions') + .action(envCommand); + program.parse(); From 5fe05519083bf86ea209a5bf04d6721343ea57fa Mon Sep 17 00:00:00 2001 From: dev-dami Date: Sun, 25 Jan 2026 21:20:52 +0100 Subject: [PATCH 3/6] feat(validation): add docker name validation --- examples/image-resizer/{index.js => index.ts} | 22 +- examples/image-resizer/service.yaml | 4 +- .../src/__tests__/docker-execution.test.ts | 6 - .../core/src/__tests__/load-service.test.ts | 2 +- .../src/__tests__/runtime-registry.test.ts | 103 +++++++-- packages/core/src/service/load-service.ts | 14 +- packages/http/src/__tests__/security.test.ts | 211 ++++++++++++++++++ packages/http/src/__tests__/server.test.ts | 13 ++ packages/http/src/server.ts | 32 +-- .../shared/src/__tests__/validation.test.ts | 130 +++++++++++ packages/shared/src/index.ts | 1 + packages/shared/src/validation.ts | 30 +++ 12 files changed, 514 insertions(+), 54 deletions(-) rename examples/image-resizer/{index.js => index.ts} (58%) create mode 100644 packages/http/src/__tests__/security.test.ts create mode 100644 packages/shared/src/__tests__/validation.test.ts create mode 100644 packages/shared/src/validation.ts diff --git a/examples/image-resizer/index.js b/examples/image-resizer/index.ts similarity index 58% rename from examples/image-resizer/index.js rename to examples/image-resizer/index.ts index 8d6f42f..d2c5969 100644 --- a/examples/image-resizer/index.js +++ b/examples/image-resizer/index.ts @@ -1,6 +1,22 @@ -const input = process.env.IGNITE_INPUT ? JSON.parse(process.env.IGNITE_INPUT) : {}; +interface ResizeEvent { + width?: number; + height?: number; + format?: string; +} + +interface ResizeResponse { + statusCode: number; + body: { + message: string; + dimensions: { width: number; height: number }; + format: string; + processingTimeMs: number; + }; +} + +const input: ResizeEvent = process.env.IGNITE_INPUT ? JSON.parse(process.env.IGNITE_INPUT) : {}; -async function resizeImage(event) { +async function resizeImage(event: ResizeEvent): Promise { const { width = 100, height = 100, format = 'jpeg' } = event; const startTime = Date.now(); @@ -20,7 +36,7 @@ async function resizeImage(event) { }; } -function simulateImageProcessing(width, height) { +function simulateImageProcessing(width: number, height: number): Promise { return new Promise((resolve) => { const complexity = (width * height) / 10000; const delay = Math.min(100 + complexity * 10, 1000); diff --git a/examples/image-resizer/service.yaml b/examples/image-resizer/service.yaml index f77b66d..c91cfcd 100644 --- a/examples/image-resizer/service.yaml +++ b/examples/image-resizer/service.yaml @@ -1,7 +1,7 @@ service: name: image-resizer - runtime: node - entry: index.js + runtime: bun + entry: index.ts memoryMb: 128 timeoutMs: 5000 env: diff --git a/packages/core/src/__tests__/docker-execution.test.ts b/packages/core/src/__tests__/docker-execution.test.ts index 9104627..24675e5 100644 --- a/packages/core/src/__tests__/docker-execution.test.ts +++ b/packages/core/src/__tests__/docker-execution.test.ts @@ -24,12 +24,6 @@ describe('Docker Execution', () => { }); describe('given Docker is available', () => { - beforeEach(function () { - if (!dockerAvailable) { - pending('Docker is not available'); - } - }); - describe('buildServiceImage', () => { it('builds the hello-bun service image', async () => { if (!dockerAvailable) return; diff --git a/packages/core/src/__tests__/load-service.test.ts b/packages/core/src/__tests__/load-service.test.ts index a3325b2..a02983b 100644 --- a/packages/core/src/__tests__/load-service.test.ts +++ b/packages/core/src/__tests__/load-service.test.ts @@ -22,7 +22,7 @@ describe('loadService', () => { const service = await loadService(servicePath); expect(service.config.service.name).toBe('image-resizer'); - expect(service.config.service.runtime).toBe('node'); + expect(service.config.service.runtime).toBe('bun'); }); }); diff --git a/packages/core/src/__tests__/runtime-registry.test.ts b/packages/core/src/__tests__/runtime-registry.test.ts index c87f622..4dfd64b 100644 --- a/packages/core/src/__tests__/runtime-registry.test.ts +++ b/packages/core/src/__tests__/runtime-registry.test.ts @@ -1,27 +1,46 @@ -import { getRuntimeConfig, isValidRuntime, getSupportedRuntimes } from '../runtime/runtime-registry'; +import { describe, it, expect, afterEach } from '@jest/globals'; +import { getRuntimeConfig, isValidRuntime, getSupportedRuntimes, registerRuntime, unregisterRuntime, getRuntimePlugin } from '../runtime/runtime-registry'; +import { createRuntimePlugin } from '../runtime/runtime-plugin'; describe('RuntimeRegistry', () => { describe('getRuntimeConfig', () => { it('returns correct config for bun runtime', () => { const config = getRuntimeConfig('bun'); - expect(config).toEqual({ - name: 'bun', - dockerfileDir: 'runtime-bun', - defaultEntry: 'index.ts', - fileExtensions: ['.ts', '.js', '.tsx', '.jsx'], - }); + expect(config.name).toBe('bun'); + expect(config.dockerfileDir).toBe('runtime-bun'); + expect(config.defaultEntry).toBe('index.ts'); + expect(config.fileExtensions).toEqual(['.ts', '.js', '.tsx', '.jsx']); + expect(config.version).toBe('1.3'); + expect(config.plugin).toBeDefined(); }); it('returns correct config for node runtime', () => { const config = getRuntimeConfig('node'); - expect(config).toEqual({ - name: 'node', - dockerfileDir: 'runtime-node', - defaultEntry: 'index.js', - fileExtensions: ['.js', '.mjs', '.cjs'], - }); + expect(config.name).toBe('node'); + expect(config.dockerfileDir).toBe('runtime-node'); + expect(config.defaultEntry).toBe('index.js'); + expect(config.version).toBe('20'); + }); + + it('returns correct config for quickjs runtime', () => { + const config = getRuntimeConfig('quickjs'); + + expect(config.name).toBe('quickjs'); + expect(config.dockerfileDir).toBe('runtime-quickjs'); + expect(config.defaultEntry).toBe('index.js'); + }); + + it('parses versioned runtime string', () => { + const config = getRuntimeConfig('bun@1.2'); + + expect(config.name).toBe('bun'); + expect(config.version).toBe('1.2'); + }); + + it('throws for unknown runtime', () => { + expect(() => getRuntimeConfig('unknown')).toThrow('Unknown runtime: unknown'); }); }); @@ -34,20 +53,72 @@ describe('RuntimeRegistry', () => { expect(isValidRuntime('node')).toBe(true); }); + it('returns true for deno', () => { + expect(isValidRuntime('deno')).toBe(true); + }); + + it('returns true for quickjs', () => { + expect(isValidRuntime('quickjs')).toBe(true); + }); + + it('returns true for versioned runtime', () => { + expect(isValidRuntime('bun@1.3')).toBe(true); + }); + it('returns false for invalid runtime', () => { - expect(isValidRuntime('deno')).toBe(false); + expect(isValidRuntime('python')).toBe(false); expect(isValidRuntime('')).toBe(false); expect(isValidRuntime('nodejs')).toBe(false); }); }); describe('getSupportedRuntimes', () => { - it('returns all supported runtimes', () => { + it('returns all built-in runtimes', () => { const runtimes = getSupportedRuntimes(); expect(runtimes).toContain('bun'); expect(runtimes).toContain('node'); - expect(runtimes).toHaveLength(2); + expect(runtimes).toContain('deno'); + expect(runtimes).toContain('quickjs'); + expect(runtimes.length).toBeGreaterThanOrEqual(4); + }); + }); + + describe('custom runtime registration', () => { + const customRuntime = createRuntimePlugin({ + name: 'custom-test', + baseImage: 'alpine:latest', + defaultEntry: 'main.js', + fileExtensions: ['.js'], + packageManager: 'none', + runCommand: 'node', + }); + + afterEach(() => { + unregisterRuntime('custom-test'); + }); + + it('allows registering custom runtimes', () => { + registerRuntime(customRuntime); + + expect(isValidRuntime('custom-test')).toBe(true); + expect(getSupportedRuntimes()).toContain('custom-test'); + }); + + it('allows unregistering custom runtimes', () => { + registerRuntime(customRuntime); + expect(isValidRuntime('custom-test')).toBe(true); + + unregisterRuntime('custom-test'); + expect(isValidRuntime('custom-test')).toBe(false); + }); + + it('returns plugin for custom runtime', () => { + registerRuntime(customRuntime); + + const plugin = getRuntimePlugin('custom-test'); + expect(plugin).toBeDefined(); + expect(plugin?.name).toBe('custom-test'); }); }); }); diff --git a/packages/core/src/service/load-service.ts b/packages/core/src/service/load-service.ts index 2d94dcf..d5b5b9e 100644 --- a/packages/core/src/service/load-service.ts +++ b/packages/core/src/service/load-service.ts @@ -1,7 +1,7 @@ import { readFile, stat, readdir } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { parse as parseYaml } from 'yaml'; -import { ServiceError, type ServiceConfig } from '@ignite/shared'; +import { ServiceError, type ServiceConfig, validateDockerName } from '@ignite/shared'; import type { LoadedService, ServiceValidation } from './service.types.js'; import { isValidRuntime, getSupportedRuntimes } from '../runtime/runtime-registry.js'; @@ -68,9 +68,9 @@ function validateServiceConfig(config: unknown): ServiceValidation { errors.push('service.name is required'); } else { const name = service['name'] as string; - const dockerNameRegex = /^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$|^[a-z0-9]$/; - if (!dockerNameRegex.test(name)) { - errors.push('service.name must be lowercase alphanumeric with hyphens (1-63 chars, Docker compatible)'); + const validation = validateDockerName(name); + if (!validation.valid) { + errors.push(`service.name invalid: ${validation.error}`); } } @@ -90,6 +90,12 @@ function validateServiceConfig(config: unknown): ServiceValidation { errors.push('service.memoryMb must be a positive number'); } + if (service['cpuLimit'] !== undefined) { + if (typeof service['cpuLimit'] !== 'number' || service['cpuLimit'] <= 0) { + errors.push('service.cpuLimit must be a positive number'); + } + } + if (typeof service['timeoutMs'] !== 'number' || service['timeoutMs'] <= 0) { errors.push('service.timeoutMs must be a positive number'); } diff --git a/packages/http/src/__tests__/security.test.ts b/packages/http/src/__tests__/security.test.ts new file mode 100644 index 0000000..4e5bb97 --- /dev/null +++ b/packages/http/src/__tests__/security.test.ts @@ -0,0 +1,211 @@ +import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'; +import { join } from 'node:path'; +import { isDockerAvailable } from '@ignite/core'; + +const EXAMPLES_PATH = join(process.cwd(), 'examples'); + +describe('HTTP Server Security', () => { + describe('Authentication', () => { + let handleWithAuth: (request: Request) => Promise; + let handleNoAuth: (request: Request) => Promise; + let stopWithAuth: () => void; + let stopNoAuth: () => void; + + beforeAll(async () => { + const { createServer } = await import('../server.js'); + + const serverWithAuth = createServer({ + port: 0, + host: 'localhost', + servicesPath: EXAMPLES_PATH, + apiKey: 'test-secret-key', + }); + handleWithAuth = serverWithAuth.handle; + stopWithAuth = serverWithAuth.stop; + + const serverNoAuth = createServer({ + port: 0, + host: 'localhost', + servicesPath: EXAMPLES_PATH, + }); + handleNoAuth = serverNoAuth.handle; + stopNoAuth = serverNoAuth.stop; + }); + + afterAll(() => { + stopWithAuth?.(); + stopNoAuth?.(); + }); + + it('allows health check without auth', async () => { + const request = new Request('http://localhost/health', { method: 'GET' }); + const response = await handleWithAuth(request); + expect(response.status).toBe(200); + }); + + it('rejects requests without auth header when apiKey is set', async () => { + const request = new Request('http://localhost/services', { method: 'GET' }); + const response = await handleWithAuth(request); + expect(response.status).toBe(401); + const data = await response.json() as { error?: string }; + expect(data.error).toContain('Unauthorized'); + }); + + it('rejects requests with wrong auth token', async () => { + const request = new Request('http://localhost/services', { + method: 'GET', + headers: { 'Authorization': 'Bearer wrong-token' }, + }); + const response = await handleWithAuth(request); + expect(response.status).toBe(401); + }); + + it('accepts requests with correct auth token', async () => { + const request = new Request('http://localhost/services', { + method: 'GET', + headers: { 'Authorization': 'Bearer test-secret-key' }, + }); + const response = await handleWithAuth(request); + expect(response.status).toBe(200); + }); + + it('allows all requests when apiKey is not set', async () => { + const request = new Request('http://localhost/services', { method: 'GET' }); + const response = await handleNoAuth(request); + expect(response.status).toBe(200); + }); + }); + + describe('Rate Limiting', () => { + let handle: (request: Request) => Promise; + let stop: () => void; + + beforeAll(async () => { + const { createServer } = await import('../server.js'); + const server = createServer({ + port: 0, + host: 'localhost', + servicesPath: EXAMPLES_PATH, + rateLimit: 3, + rateLimitWindow: 60000, + }); + handle = server.handle; + stop = server.stop; + }); + + afterAll(() => { + stop?.(); + }); + + it('allows requests within rate limit', async () => { + const request = new Request('http://localhost/services', { + method: 'GET', + headers: { 'X-Forwarded-For': 'rate-test-1' }, + }); + + const response1 = await handle(request); + expect(response1.status).toBe(200); + + const response2 = await handle(new Request('http://localhost/services', { + method: 'GET', + headers: { 'X-Forwarded-For': 'rate-test-1' }, + })); + expect(response2.status).toBe(200); + }); + + it('blocks requests exceeding rate limit', async () => { + const makeRequest = () => new Request('http://localhost/services', { + method: 'GET', + headers: { 'X-Forwarded-For': 'rate-test-exceed' }, + }); + + await handle(makeRequest()); + await handle(makeRequest()); + await handle(makeRequest()); + + const response = await handle(makeRequest()); + expect(response.status).toBe(429); + expect(response.headers.get('Retry-After')).toBeDefined(); + }); + + it('tracks rate limits per client IP', async () => { + const request1 = new Request('http://localhost/services', { + method: 'GET', + headers: { 'X-Forwarded-For': 'client-a' }, + }); + const request2 = new Request('http://localhost/services', { + method: 'GET', + headers: { 'X-Forwarded-For': 'client-b' }, + }); + + const responseA = await handle(request1); + const responseB = await handle(request2); + + expect(responseA.status).toBe(200); + expect(responseB.status).toBe(200); + }); + }); + + describe('Service Name Validation', () => { + let handle: (request: Request) => Promise; + let stop: () => void; + let dockerAvailable = false; + + beforeAll(async () => { + dockerAvailable = await isDockerAvailable(); + const { createServer } = await import('../server.js'); + const server = createServer({ + port: 0, + host: 'localhost', + servicesPath: EXAMPLES_PATH, + }); + handle = server.handle; + stop = server.stop; + }); + + afterAll(() => { + stop?.(); + }); + + it('rejects path traversal attempts with ..', async () => { + const request = new Request('http://localhost/services/hello..world/execute', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }); + const response = await handle(request); + expect(response.status).toBe(400); + const data = await response.json() as { error?: string }; + expect(data.error).toContain('invalid'); + }); + + it('rejects service names with backslashes', async () => { + const request = new Request('http://localhost/services/path%5Cto%5Cservice/execute', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }); + const response = await handle(request); + expect(response.status).toBe(400); + }); + + it('rejects uppercase service names', async () => { + const request = new Request('http://localhost/services/HelloWorld/execute', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }); + const response = await handle(request); + expect(response.status).toBe(400); + }); + + it('accepts valid lowercase service names', async () => { + if (!dockerAvailable) return; + const request = new Request('http://localhost/services/hello-bun/preflight', { + method: 'GET', + }); + const response = await handle(request); + expect(response.status).toBe(200); + }, 120000); + }); +}); diff --git a/packages/http/src/__tests__/server.test.ts b/packages/http/src/__tests__/server.test.ts index 8b06966..7ecdbe7 100644 --- a/packages/http/src/__tests__/server.test.ts +++ b/packages/http/src/__tests__/server.test.ts @@ -1,4 +1,5 @@ import { join } from 'node:path'; +import { isDockerAvailable } from '@ignite/core'; const EXAMPLES_PATH = join(process.cwd(), 'examples'); @@ -33,8 +34,11 @@ interface ExecuteResponse { describe('HTTP Server', () => { let handle: (request: Request) => Promise; + let stop: () => void; + let dockerAvailable = false; beforeAll(async () => { + dockerAvailable = await isDockerAvailable(); const { createServer } = await import('../server.js'); const server = createServer({ port: 0, @@ -42,6 +46,11 @@ describe('HTTP Server', () => { servicesPath: EXAMPLES_PATH, }); handle = server.handle; + stop = server.stop; + }); + + afterAll(() => { + stop?.(); }); describe('GET /health', () => { @@ -71,6 +80,7 @@ describe('HTTP Server', () => { describe('GET /services/:serviceName/preflight', () => { it('returns preflight results for valid service', async () => { + if (!dockerAvailable) return; const request = new Request('http://localhost/services/hello-bun/preflight', { method: 'GET' }); const response = await handle(request); const data = (await response.json()) as PreflightResponse; @@ -93,6 +103,7 @@ describe('HTTP Server', () => { describe('POST /services/:serviceName/execute', () => { it('executes service without input', async () => { + if (!dockerAvailable) return; const request = new Request('http://localhost/services/hello-bun/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -109,6 +120,7 @@ describe('HTTP Server', () => { }, 120000); it('executes service with input', async () => { + if (!dockerAvailable) return; const request = new Request('http://localhost/services/hello-bun/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -123,6 +135,7 @@ describe('HTTP Server', () => { }, 120000); it('skips preflight when requested', async () => { + if (!dockerAvailable) return; const request = new Request('http://localhost/services/hello-bun/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/packages/http/src/server.ts b/packages/http/src/server.ts index 722f99b..ef91ae1 100644 --- a/packages/http/src/server.ts +++ b/packages/http/src/server.ts @@ -2,7 +2,7 @@ import { Elysia, t } from 'elysia'; import { cors } from '@elysiajs/cors'; import { join, resolve } from 'node:path'; import { loadService, executeService, runPreflight, getImageName, buildServiceImage } from '@ignite/core'; -import { logger } from '@ignite/shared'; +import { logger, validateDockerName } from '@ignite/shared'; import type { ServiceExecutionRequest, ServiceExecutionResponse, @@ -23,27 +23,11 @@ export interface ServerOptions { rateLimitWindow?: number; } -// Service name validation: lowercase alphanumeric with hyphens, 2-63 chars (Docker compatible) -const SERVICE_NAME_REGEX = /^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$|^[a-z0-9]$/; - -/** - * Validates and sanitizes service name to prevent path traversal and ensure Docker compatibility - */ function validateServiceName(name: string): { valid: boolean; error?: string } { - if (!name || typeof name !== 'string') { - return { valid: false, error: 'Service name is required' }; + const result = validateDockerName(name); + if (!result.valid) { + return { valid: false, error: `Service name invalid: ${result.error}` }; } - - // Block path traversal attempts - if (name.includes('..') || name.includes('/') || name.includes('\\')) { - return { valid: false, error: 'Service name contains invalid characters' }; - } - - // Validate Docker-compatible naming - if (!SERVICE_NAME_REGEX.test(name)) { - return { valid: false, error: 'Service name must be lowercase alphanumeric with hyphens (1-63 chars)' }; - } - return { valid: true }; } @@ -98,7 +82,8 @@ export function createServer(options: ServerOptions = {}) { const resolvedServicesPath = resolve(servicesPath); const rateLimiter = createRateLimiter(rateLimit, rateLimitWindow); - const cleanupInterval = setInterval(() => rateLimiter.cleanup(), rateLimitWindow); + let cleanupInterval: ReturnType | undefined = + setInterval(() => rateLimiter.cleanup(), rateLimitWindow); const app = new Elysia() .use(cors()) @@ -270,8 +255,11 @@ export function createServer(options: ServerOptions = {}) { return app; }, stop: () => { - if (isRunning) { + if (cleanupInterval) { clearInterval(cleanupInterval); + cleanupInterval = undefined; + } + if (isRunning) { app.stop(); isRunning = false; logger.info('Ignite HTTP server stopped'); diff --git a/packages/shared/src/__tests__/validation.test.ts b/packages/shared/src/__tests__/validation.test.ts new file mode 100644 index 0000000..29e52e4 --- /dev/null +++ b/packages/shared/src/__tests__/validation.test.ts @@ -0,0 +1,130 @@ +import { describe, it, expect } from '@jest/globals'; +import { validateDockerName, isValidDockerName, DOCKER_NAME_REGEX } from '../validation.js'; + +describe('validateDockerName', () => { + describe('valid names', () => { + it('accepts single lowercase letter', () => { + expect(validateDockerName('a')).toEqual({ valid: true }); + }); + + it('accepts single digit', () => { + expect(validateDockerName('1')).toEqual({ valid: true }); + }); + + it('accepts lowercase alphanumeric', () => { + expect(validateDockerName('hello123')).toEqual({ valid: true }); + }); + + it('accepts hyphens in middle', () => { + expect(validateDockerName('hello-world')).toEqual({ valid: true }); + }); + + it('accepts multiple hyphens', () => { + expect(validateDockerName('my-cool-service')).toEqual({ valid: true }); + }); + + it('accepts 63 character name', () => { + const name = 'a'.repeat(63); + expect(validateDockerName(name)).toEqual({ valid: true }); + }); + }); + + describe('invalid names', () => { + it('rejects empty string', () => { + const result = validateDockerName(''); + expect(result.valid).toBe(false); + expect(result.error).toContain('required'); + }); + + it('rejects null/undefined', () => { + expect(validateDockerName(null as unknown as string).valid).toBe(false); + expect(validateDockerName(undefined as unknown as string).valid).toBe(false); + }); + + it('rejects uppercase letters', () => { + const result = validateDockerName('HelloWorld'); + expect(result.valid).toBe(false); + }); + + it('rejects leading hyphen', () => { + const result = validateDockerName('-hello'); + expect(result.valid).toBe(false); + }); + + it('rejects trailing hyphen', () => { + const result = validateDockerName('hello-'); + expect(result.valid).toBe(false); + }); + + it('rejects underscores', () => { + const result = validateDockerName('hello_world'); + expect(result.valid).toBe(false); + }); + + it('rejects spaces', () => { + const result = validateDockerName('hello world'); + expect(result.valid).toBe(false); + }); + + it('rejects 64+ character name', () => { + const name = 'a'.repeat(64); + expect(validateDockerName(name).valid).toBe(false); + }); + }); + + describe('path traversal prevention', () => { + it('rejects double dots', () => { + const result = validateDockerName('../etc'); + expect(result.valid).toBe(false); + expect(result.error).toContain('invalid characters'); + }); + + it('rejects forward slash', () => { + const result = validateDockerName('path/to/service'); + expect(result.valid).toBe(false); + expect(result.error).toContain('invalid characters'); + }); + + it('rejects backslash', () => { + const result = validateDockerName('path\\to\\service'); + expect(result.valid).toBe(false); + expect(result.error).toContain('invalid characters'); + }); + + it('rejects hidden traversal attempts', () => { + expect(validateDockerName('..').valid).toBe(false); + expect(validateDockerName('a..b').valid).toBe(false); + expect(validateDockerName('service/..').valid).toBe(false); + }); + }); +}); + +describe('isValidDockerName', () => { + it('returns true for valid names', () => { + expect(isValidDockerName('my-service')).toBe(true); + }); + + it('returns false for invalid names', () => { + expect(isValidDockerName('../exploit')).toBe(false); + }); +}); + +describe('DOCKER_NAME_REGEX', () => { + it('matches single char', () => { + expect(DOCKER_NAME_REGEX.test('a')).toBe(true); + expect(DOCKER_NAME_REGEX.test('1')).toBe(true); + }); + + it('matches multi-char without hyphens at edges', () => { + expect(DOCKER_NAME_REGEX.test('ab')).toBe(true); + expect(DOCKER_NAME_REGEX.test('a-b')).toBe(true); + }); + + it('rejects hyphen at start for multi-char', () => { + expect(DOCKER_NAME_REGEX.test('-ab')).toBe(false); + }); + + it('rejects hyphen at end for multi-char', () => { + expect(DOCKER_NAME_REGEX.test('ab-')).toBe(false); + }); +}); diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index cb87d1d..7e0f48a 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,4 +1,5 @@ export * from './types.js'; export * from './errors.js'; +export * from './validation.js'; export { logger, Logger } from './logger.js'; export type { LogLevel } from './logger.js'; diff --git a/packages/shared/src/validation.ts b/packages/shared/src/validation.ts new file mode 100644 index 0000000..e796308 --- /dev/null +++ b/packages/shared/src/validation.ts @@ -0,0 +1,30 @@ +/** + * Docker name: lowercase alphanumeric with hyphens, 1-63 chars + * Single char names allowed, multi-char cannot start/end with hyphen + */ +export const DOCKER_NAME_REGEX = /^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$|^[a-z0-9]$/; + +export interface ValidationResult { + valid: boolean; + error?: string; +} + +export function validateDockerName(name: string): ValidationResult { + if (!name || typeof name !== 'string') { + return { valid: false, error: 'Name is required' }; + } + + if (name.includes('..') || name.includes('/') || name.includes('\\')) { + return { valid: false, error: 'Name contains invalid characters' }; + } + + if (!DOCKER_NAME_REGEX.test(name)) { + return { valid: false, error: 'Name must be lowercase alphanumeric with hyphens (1-63 chars)' }; + } + + return { valid: true }; +} + +export function isValidDockerName(name: string): boolean { + return validateDockerName(name).valid; +} From d20a229b6cae7206c2a440f4954992c88bb04e98 Mon Sep 17 00:00:00 2001 From: dev-dami Date: Sun, 25 Jan 2026 21:21:26 +0100 Subject: [PATCH 4/6] chore: update docs and tooling --- AGENTS.md | 1 + CONTRIBUTING.md | 3 +- README.md | 2 +- bun.lock | 209 +++++++++++++++++++++++++++++++++++++------ docs/api.md | 183 +++++++++++++++++++++++++++++++++++-- docs/architecture.md | 14 ++- docs/walkthrough.md | 13 +-- eslint.config.mjs | 20 +++++ package.json | 4 + 9 files changed, 397 insertions(+), 52 deletions(-) create mode 100644 AGENTS.md create mode 100644 eslint.config.mjs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a4e2f3e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +Use Bun for all package installs and scripts. Never use npm. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5bf8ebc..732c23a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,8 +50,7 @@ ignite/ │ ├── core/ # Core functionality (loader, preflight, execution) │ ├── http/ # HTTP server │ ├── shared/ # Shared types and utilities -│ ├── runtime-bun/ # Bun runtime Dockerfile -│ └── runtime-node/ # Node runtime Dockerfile +│ └── runtime-bun/ # Bun runtime Dockerfile ├── examples/ # Example services ├── docs/ # Documentation └── scripts/ # Build scripts diff --git a/README.md b/README.md index 2fd8079..c7d3a09 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Ignite runs JavaScript/TypeScript code in **secure, isolated Docker containers** | Metric | Value | |--------|-------| -| **Runtimes** | Bun, Node | +| **Runtimes** | Bun | | **Base Images** | Alpine (minimal) | | **Platforms** | Linux x64/ARM64, macOS x64/ARM64 | | **Dependencies** | Docker only | diff --git a/bun.lock b/bun.lock index edfc8b0..47a52a5 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,10 @@ "@types/bun": "latest", "@types/jest": "^30.0.0", "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^8.53.1", + "@typescript-eslint/parser": "^8.53.1", "elysia": "^1.4.22", + "eslint": "^9.39.2", "jest": "^30.2.0", "ts-jest": "^29.4.6", "typescript": "^5.3.0", @@ -29,7 +32,7 @@ }, "packages/cli": { "name": "@ignite/cli", - "version": "0.1.0", + "version": "0.6.0", "bin": { "ignite": "./dist/index.js", }, @@ -46,7 +49,7 @@ }, "packages/core": { "name": "@ignite/core", - "version": "0.1.0", + "version": "0.6.0", "dependencies": { "@ignite/shared": "workspace:*", "yaml": "^2.3.0", @@ -58,7 +61,7 @@ }, "packages/http": { "name": "@ignite/http", - "version": "0.1.0", + "version": "0.6.0", "dependencies": { "@elysiajs/cors": "^1.4.1", "@ignite/core": "workspace:*", @@ -72,15 +75,11 @@ }, "packages/runtime-bun": { "name": "@ignite/runtime-bun", - "version": "0.1.0", - }, - "packages/runtime-node": { - "name": "@ignite/runtime-node", - "version": "0.1.0", + "version": "0.6.0", }, "packages/shared": { "name": "@ignite/shared", - "version": "0.1.0", + "version": "0.6.0", "dependencies": { "zario": "^0.4.5", }, @@ -171,6 +170,32 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], + + "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@ignite/cli": ["@ignite/cli@workspace:packages/cli"], "@ignite/core": ["@ignite/core@workspace:packages/core"], @@ -179,8 +204,6 @@ "@ignite/runtime-bun": ["@ignite/runtime-bun@workspace:packages/runtime-bun"], - "@ignite/runtime-node": ["@ignite/runtime-node@workspace:packages/runtime-node"], - "@ignite/shared": ["@ignite/shared@workspace:packages/shared"], "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], @@ -263,6 +286,8 @@ "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], @@ -271,6 +296,8 @@ "@types/jest": ["@types/jest@30.0.0", "", { "dependencies": { "expect": "^30.0.0", "pretty-format": "^30.0.0" } }, "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/node": ["@types/node@20.19.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g=="], "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], @@ -279,6 +306,26 @@ "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/type-utils": "8.53.1", "@typescript-eslint/utils": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.1", "@typescript-eslint/types": "^8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1" } }, "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.53.1", "", {}, "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.1", "@typescript-eslint/tsconfig-utils": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], @@ -319,6 +366,12 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -327,7 +380,7 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "babel-jest": ["babel-jest@30.2.0", "", { "dependencies": { "@jest/transform": "30.2.0", "@types/babel__core": "^7.20.5", "babel-plugin-istanbul": "^7.0.1", "babel-preset-jest": "30.2.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.11.0 || ^8.0.0-0" } }, "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw=="], @@ -343,7 +396,7 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.9.17", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ=="], - "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -395,6 +448,8 @@ "dedent": ["dedent@1.7.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], "detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="], @@ -413,10 +468,26 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "exact-mirror": ["exact-mirror@0.2.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="], "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], @@ -427,15 +498,27 @@ "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], @@ -453,6 +536,10 @@ "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], @@ -467,8 +554,12 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "image-resizer": ["image-resizer@workspace:examples/image-resizer"], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], @@ -479,10 +570,14 @@ "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-generator-fn": ["is-generator-fn@2.1.0", "", {}, "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -553,22 +648,34 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], @@ -585,7 +692,7 @@ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -613,14 +720,18 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -639,8 +750,12 @@ "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "pure-rand": ["pure-rand@7.0.1", "", {}, "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ=="], "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -649,7 +764,7 @@ "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="], - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -693,16 +808,22 @@ "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + "ts-jest": ["ts-jest@29.4.6", "", { "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.3", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest", "jest-util"], "bin": { "ts-jest": "cli.js" } }, "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -719,12 +840,16 @@ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="], "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -753,6 +878,10 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], @@ -761,25 +890,39 @@ "@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + "@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "@istanbuljs/load-nyc-config/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "resolve-cwd/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - "test-exclude/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + + "test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -791,6 +934,22 @@ "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], } } diff --git a/docs/api.md b/docs/api.md index b715d91..75dcde3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -10,6 +10,8 @@ Complete reference for Ignite CLI commands and HTTP API endpoints. - [ignite preflight](#ignite-preflight) - [ignite serve](#ignite-serve) - [ignite report](#ignite-report) + - [ignite lock](#ignite-lock) + - [ignite env](#ignite-env) - [HTTP API](#http-api) - [Health Check](#get-health) - [List Services](#get-services) @@ -41,17 +43,26 @@ ignite init [options] | Option | Default | Description | |--------|---------|-------------| -| `--runtime ` | `bun` | Runtime: `bun` or `node` | -| `--template