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..3f0e864 100644
--- a/README.md
+++ b/README.md
@@ -51,11 +51,13 @@ Ignite runs JavaScript/TypeScript code in **secure, isolated Docker containers**
| Metric | Value |
|--------|-------|
-| **Runtimes** | Bun, Node |
+| **Runtimes** | Bun (default), Node, Deno, QuickJS |
| **Base Images** | Alpine (minimal) |
| **Platforms** | Linux x64/ARM64, macOS x64/ARM64 |
| **Dependencies** | Docker only |
+Note: Bun is the default runtime. Other runtimes are supported but increase the security attack surface; use them only when required and review service code and dependencies carefully.
+
## Install
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..7721e80 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 ` | `default` | Template to use |
+| `--runtime ` | `bun` | Runtime: `bun`, `node`, `deno`, `quickjs` (with optional version: `bun@1.2`) |
+| `--path ` | `./` | Custom path for the service directory |
**Examples:**
```bash
-# Create Bun service
+# Create Bun service (default)
ignite init my-service
# Create Node.js service
ignite init my-service --runtime node
+
+# Create with specific version
+ignite init my-service --runtime node@20
+
+# Create Deno service
+ignite init my-service --runtime deno
+
+# Create QuickJS service (fast cold start)
+ignite init my-service --runtime quickjs
```
**Generated Files:**
@@ -84,6 +95,7 @@ ignite run [options]
| Option | Default | Description |
|--------|---------|-------------|
| `--input ` | `{}` | Input data as JSON string |
+| `--runtime ` | (from service.yaml) | Override runtime (e.g., `node@20`, `bun@1.2`) |
| `--skip-preflight` | `false` | Skip safety checks |
| `--json` | `false` | Output results as JSON |
| `--audit` | `false` | Run with security audit (blocks network, read-only filesystem) |
@@ -97,6 +109,9 @@ ignite run ./my-service
# With input data
ignite run ./my-service --input '{"name": "World"}'
+# Override runtime version
+ignite run ./my-service --runtime node@22
+
# Skip preflight (development only)
ignite run ./my-service --skip-preflight
@@ -271,6 +286,141 @@ ignite report ./my-service --format json --output report.json
---
+### ignite lock
+
+Create or update environment manifest (`ignite.lock`) for reproducible builds.
+
+```bash
+ignite lock [options]
+```
+
+**Arguments:**
+
+| Argument | Description |
+|----------|-------------|
+| `path` | Path to service directory |
+
+**Options:**
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| `--update` | `false` | Update existing manifest |
+| `--check` | `false` | Check for drift without modifying |
+
+**Examples:**
+
+```bash
+# Create ignite.lock
+ignite lock ./my-service
+
+# Update existing manifest
+ignite lock ./my-service --update
+
+# Check for environment drift (CI/CD)
+ignite lock ./my-service --check
+```
+
+**Generated File (`ignite.lock`):**
+
+```yaml
+version: "1.0"
+runtime:
+ name: bun
+ version: "1.3"
+lockfile: bun.lockb
+checksums:
+ package.json: sha256:abc123...
+ bun.lockb: sha256:def456...
+createdAt: "2024-01-15T10:30:00.000Z"
+```
+
+**Exit Codes:**
+
+| Code | Meaning |
+|------|---------|
+| 0 | Success / No drift detected |
+| 1 | Drift detected (with `--check`) |
+
+---
+
+### ignite env
+
+Display environment information and available runtimes.
+
+```bash
+ignite env [path] [options]
+```
+
+**Arguments:**
+
+| Argument | Description |
+|----------|-------------|
+| `path` | Path to service directory (optional) |
+
+**Options:**
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| `--runtimes` | `false` | List all supported runtimes |
+
+**Examples:**
+
+```bash
+# Show service environment info
+ignite env ./my-service
+
+# List all available runtimes
+ignite env --runtimes
+```
+
+**Output (service info):**
+
+```
+Service: my-service
+Runtime: bun@1.3
+
+Environment: Locked
+ Runtime: bun@1.3
+ Locked at: 2024-01-15T10:30:00.000Z
+ Lockfile: bun.lockb
+
+✓ Environment matches manifest
+```
+
+**Output (runtimes list):**
+
+```
+Supported Runtimes:
+
+ bun
+ Default entry: index.ts
+ Extensions: .ts, .js, .tsx, .jsx
+ Versions: 1.0, 1.1, 1.2, 1.3 (default: 1.3)
+
+ node
+ Default entry: index.js
+ Extensions: .js, .mjs, .cjs
+ Versions: 18, 20, 22 (default: 20)
+
+ deno
+ Default entry: index.ts
+ Extensions: .ts, .js, .tsx, .jsx
+ Versions: 1.40, 1.41, 1.42, 2.0 (default: 2.0)
+
+ quickjs
+ Default entry: index.js
+ Extensions: .js
+ Versions: latest (default: latest)
+
+Usage examples:
+ service.yaml: runtime: bun
+ service.yaml: runtime: bun@1.2
+ service.yaml: runtime: node@20
+ ignite run . --runtime node@22
+```
+
+---
+
## HTTP API
Base URL: `http://localhost:3000` (default)
@@ -477,24 +627,49 @@ Execute a service.
service:
# Required fields
name: string # Service identifier (alphanumeric, hyphens)
- runtime: string # "bun" or "node"
+ runtime: string # Runtime with optional version (see below)
entry: string # Entry file path
# Optional fields
memoryMb: number # Memory limit (default: 128)
+ cpuLimit: number # CPU limit in cores (default: 1)
timeoutMs: number # Timeout (default: 30000)
env: object # Environment variables
dependencies: array # Explicit dependencies (auto-detected by default)
```
+**Supported Runtimes:**
+
+| Runtime | Versions | Default Entry | Notes |
+|---------|----------|---------------|-------|
+| `bun` | 1.0, 1.1, 1.2, 1.3 | index.ts | TypeScript native, fastest |
+| `node` | 18, 20, 22 | index.js | Node.js compatibility |
+| `deno` | 1.40, 1.41, 1.42, 2.0 | index.ts | Secure by default |
+| `quickjs` | latest | index.js | Ultra-fast cold start (~10ms) |
+
+Security note: Bun is the default runtime. Using other runtimes increases the attack surface; only use them when required and keep runtime versions pinned.
+
+**Runtime Version Syntax:**
+
+```yaml
+# Use default version
+runtime: bun
+
+# Specify version
+runtime: bun@1.2
+runtime: node@20
+runtime: deno@2.0
+```
+
**Full Example:**
```yaml
service:
name: my-service
- runtime: bun
+ runtime: bun@1.3
entry: index.ts
memoryMb: 256
+ cpuLimit: 0.5
timeoutMs: 60000
env:
NODE_ENV: production
diff --git a/docs/architecture.md b/docs/architecture.md
index 60cc05a..751c822 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -4,7 +4,7 @@
Ignite is a secure execution sandbox for JavaScript/TypeScript code. It runs code in isolated Docker containers with network blocking, filesystem restrictions, and security auditing. Designed for AI agents, untrusted code execution, and isolated microservices.
-**Bun-first** with Node.js support.
+**Bun-first** runtime.
## Package Structure
@@ -14,7 +14,6 @@ ignite/
│ ├── cli/ # Command-line interface
│ ├── core/ # Framework core logic
│ ├── runtime-bun/ # Bun runtime adapter
-│ ├── runtime-node/ # Node.js runtime adapter
│ └── shared/ # Shared types and utilities
└── examples/ # Example services
```
@@ -25,9 +24,11 @@ ignite/
Parses `service.yaml` configuration and validates service structure.
### Runtime Registry
-Manages runtime configurations for different execution environments:
-- **bun**: Bun runtime with native TypeScript support
-- **node**: Node.js runtime for JavaScript
+Manages runtime configuration for the execution environment:
+- **bun**: Bun runtime with native TypeScript support (default)
+- **node**: Node.js runtime for JS compatibility
+- **deno**: Deno runtime with secure defaults
+- **quickjs**: QuickJS runtime for minimal overhead
### Docker Runtime
Manages Docker image building and container execution.
@@ -57,7 +58,7 @@ service.yaml
│
▼
┌─────────────────┐
-│Runtime Registry │──► Select Bun or Node.js
+│Runtime Registry │──► Select runtime (Bun default)
└────────┬────────┘
│
▼
@@ -67,7 +68,7 @@ service.yaml
│
▼
┌─────────────────┐
-│ Execution Engine│──► Docker (Bun/Node)
+│ Execution Engine│──► Docker (Bun)
└────────┬────────┘
│
▼
@@ -85,13 +86,15 @@ Each service runs in its own Docker container with:
- Environment variable injection
- Metrics emission via entrypoint wrapper
+Security note: Bun is the default runtime. Supporting additional runtimes increases the attack surface, so use them only when required and keep versions pinned.
+
## Runtime Registry
The runtime registry (`packages/core/src/runtime/runtime-registry.ts`) provides:
```typescript
interface RuntimeConfig {
- name: RuntimeName; // 'bun' | 'node'
+ name: RuntimeName; // 'bun' (default), 'node', 'deno', 'quickjs'
dockerfileDir: string; // Directory containing Dockerfile
defaultEntry: string; // Default entry file
fileExtensions: string[]; // Supported file extensions
@@ -100,7 +103,7 @@ interface RuntimeConfig {
## Adding New Runtimes
-To add a new runtime:
+To add a new runtime in the future:
1. Create `packages/runtime-/Dockerfile`
2. Add entry to runtime registry
diff --git a/docs/threat-model.md b/docs/threat-model.md
index 74f0831..794fcf9 100644
--- a/docs/threat-model.md
+++ b/docs/threat-model.md
@@ -20,14 +20,14 @@ Ignite aims to provide **defense-in-depth** for executing untrusted JavaScript/T
┌─────────────────────────────────────────────────────────────┐
│ HOST SYSTEM │
│ ┌───────────────────────────────────────────────────────┐ │
-│ │ DOCKER DAEMON │ │
+│ │ DOCKER DAEMON │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
-│ │ │ IGNITE CONTAINER │ │ │
+│ │ │ IGNITE CONTAINER │ │ │
│ │ │ ┌─────────────────────────────────────────────┐│ │ │
│ │ │ │ UNTRUSTED CODE ││ │ │
│ │ │ │ ││ │ │
-│ │ │ │ This is where AI-generated or user code ││ │ │
-│ │ │ │ executes. Assume fully malicious. ││ │ │
+│ │ │ │ This is where AI-generated or user code ││ │ │
+│ │ │ │ executes. Assume fully malicious. ││ │ │
│ │ │ └─────────────────────────────────────────────┘│ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
diff --git a/docs/walkthrough.md b/docs/walkthrough.md
index de35391..1e91c71 100644
--- a/docs/walkthrough.md
+++ b/docs/walkthrough.md
@@ -143,7 +143,7 @@ ignite run . --input '{"data": [10, 20, 30], "operation": "average"}'
service:
# Required
name: my-service # Service identifier
- runtime: bun # "bun" or "node"
+ runtime: bun # bun, node, deno, quickjs (optional version with @)
entry: index.ts # Entry file
# Resource Limits
@@ -192,18 +192,14 @@ service:
entry: index.ts
```
-### Node Runtime
+### Runtime
-Best for:
-- CommonJS compatibility
-- Specific npm packages
-- Node-specific APIs
+Ignite supports Bun, Node, Deno, and QuickJS runtimes. Bun is the default and recommended option.
-```yaml
-service:
- runtime: node
- entry: index.js # Compile TS first, or use JS
-```
+**Security considerations:**
+- Additional runtimes increase the attack surface and dependency complexity.
+- Use non-Bun runtimes only when required by your code or dependencies.
+- Audit dependencies and keep runtime versions pinned (e.g., `node@20`, `deno@2.0`) to reduce drift.
### Using Dependencies
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..93990b4
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,20 @@
+import tsParser from '@typescript-eslint/parser';
+
+export default [
+ {
+ ignores: [
+ 'bin/**',
+ 'dist/**',
+ 'coverage/**',
+ 'node_modules/**',
+ ],
+ },
+ {
+ files: ['**/*.{js,mjs,cjs,ts,tsx}'],
+ languageOptions: {
+ parser: tsParser,
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ },
+ },
+];
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/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/package.json b/package.json
index 1850e2e..74ee065 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ignite",
- "version": "0.6.0",
+ "version": "0.6.1",
"private": true,
"description": "Secure JS/TS code execution in Docker with sandboxing for AI agents, untrusted code, and microservices",
"workspaces": [
@@ -12,6 +12,7 @@
"build:binaries": "bun run scripts/build-binaries.ts",
"dev": "bun run --filter './packages/*' dev",
"clean": "bun run --filter './packages/*' clean && rm -rf bin dist",
+ "lint": "eslint .",
"typecheck": "tsc --noEmit",
"test": "NODE_OPTIONS='--experimental-vm-modules' jest --runInBand",
"test:unit": "NODE_OPTIONS='--experimental-vm-modules' jest --testPathIgnorePatterns=docker-execution --testPathIgnorePatterns=server",
@@ -25,7 +26,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"
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..0edcab3 100644
--- a/packages/cli/src/commands/init.ts
+++ b/packages/cli/src/commands/init.ts
@@ -1,14 +1,15 @@
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';
+import { parseRuntime } from '@ignite/shared';
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 +21,26 @@ 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 runtimeName = parseRuntime(runtime).name;
+ const startCmd = runtimeName === 'bun' ? `bun run ${entry}` :
+ runtimeName === 'deno' ? `deno run ${entry}` :
+ runtimeName === 'quickjs' ? `qjs ${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 +74,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();
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/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/policy.ts b/packages/core/src/security/policy.ts
index 0b87ad7..1aa5534 100644
--- a/packages/core/src/security/policy.ts
+++ b/packages/core/src/security/policy.ts
@@ -8,13 +8,9 @@ export interface PolicyFile {
security?: {
network?: {
enabled?: boolean;
- allowedHosts?: string[];
- allowedPorts?: number[];
};
filesystem?: {
readOnly?: boolean;
- allowedWritePaths?: string[];
- blockedReadPaths?: string[];
};
process?: {
allowSpawn?: boolean;
@@ -49,14 +45,10 @@ 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 = {
@@ -79,6 +71,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..72463af 100644
--- a/packages/core/src/security/security.types.ts
+++ b/packages/core/src/security/security.types.ts
@@ -6,14 +6,10 @@ export interface SecurityPolicy {
export interface NetworkPolicy {
enabled: boolean;
- allowedHosts?: string[];
- allowedPorts?: number[];
}
export interface FilesystemPolicy {
readOnly: boolean;
- allowedWritePaths?: string[];
- blockedReadPaths?: string[];
}
export interface ProcessPolicy {
@@ -53,8 +49,6 @@ 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/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/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/__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/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/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;
+}
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) {