diff --git a/Makefile b/Makefile index 430907f9..4e53c609 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ PACKAGER ?= deb MAKE_VARS = $(if $(PREFIX),PREFIX=$(PREFIX),) \ $(if $(DESTDIR),DESTDIR=$(DESTDIR),) -.PHONY: help deps build install deb rpm apk clean +.PHONY: help deps build install deb rpm apk clean dev help: @echo "opensource-server — delegates to each component's Makefile." @@ -20,6 +20,7 @@ help: @echo " rpm build .rpm packages, collected into ./dist" @echo " apk build .apk packages, collected into ./dist" @echo " clean remove build artifacts, staging, packages and ./dist" + @echo " dev run the Manager locally (SQLite, no Proxmox)" @echo " help show this message" @echo "" @echo "Variables: PREFIX (default /opt/opensource-server), DESTDIR (default /)." @@ -51,3 +52,8 @@ deb rpm apk: @echo "" @echo "Packages collected in dist/:" @ls -1 dist/ + +# Run the Manager locally (SQLite, no Proxmox). Delegates to create-a-container; +# forwards LOG_LEVEL when provided (e.g. `make dev LOG_LEVEL=trace`). +dev: + $(MAKE) -C create-a-container dev $(if $(LOG_LEVEL),LOG_LEVEL=$(LOG_LEVEL),) diff --git a/README.md b/README.md index d5d246b1..de6b424c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Full documentation lives in [`mie-opensource-landing/docs/`](mie-opensource-land | If you want to... | Read | |---|---| | Install and operate a production deployment | [Installation Guide](mie-opensource-landing/docs/admins/installation.md) | -| Run the full stack locally to develop or contribute | [Development Workflow](mie-opensource-landing/docs/developers/development-workflow.md) | +| Run the Manager locally to develop or contribute (`make dev`, or the full Docker stack) | [Development Workflow](mie-opensource-landing/docs/developers/development-workflow.md) | | Use a deployed cluster as an end user (create containers, etc.) | [User Getting Started](mie-opensource-landing/docs/users/getting-started.md) | | Contribute changes | [Contributing](mie-opensource-landing/docs/developers/contributing.md) | | Understand the system design | [System Architecture](mie-opensource-landing/docs/developers/system-architecture.md) | diff --git a/create-a-container/Makefile b/create-a-container/Makefile index 0d52453b..8888c403 100644 --- a/create-a-container/Makefile +++ b/create-a-container/Makefile @@ -23,10 +23,10 @@ help: @echo "create-a-container — builds the opensource-server package." @echo "" @echo "Targets:" - @echo " deps install dependencies (npm ci, server + client)" + @echo " deps install production dependencies (npm ci, server + client)" @echo " build build the web client bundle" @echo " install stage the app into DESTDIR (default /)" - @echo " dev run server + client watch (npm run dev)" + @echo " dev run the Manager locally (SQLite, no Proxmox)" @echo " deb build the .deb package" @echo " rpm build the .rpm package" @echo " apk build the .apk package" @@ -42,9 +42,43 @@ deps: build: deps npm --prefix client run build -# Full local development: server (nodemon) + client (vite watch) together. -dev: deps - npm run dev +# Run the Manager locally against SQLite, with a dummy (mock) hypervisor so +# containers can be "created" without Proxmox. No .env is needed — the server's +# defaults (SQLite, auto-generated session secret, non-production mode) are +# sufficient for development. +# +# Self-contained (dev needs a full install incl. devDependencies, unlike the +# production `deps` which omits them, so it isn't split into shared phony +# prerequisites): install server + client deps with `npm install +# --no-package-lock`. This is a fast, incremental install that keeps +# devDependencies and runs the server's patch-package postinstall, but never +# rewrites package-lock.json (so `make dev` leaves the lockfile untouched even +# when the local npm would otherwise renormalize it). Then run migrations, seed +# a localhost site + dummy node, build the client once, and run three processes +# together via concurrently: +# - server (nodemon, restarts on change) +# - job-runner (so container creation exercises the real POST /containers -> +# Job -> job-runner -> bin/create-container.js -> DummyApi path) +# - client build in watch mode (rebuilds client/dist, which the server serves) +# +# Override behavior on the command line, e.g.: +# make dev LOG_LEVEL=trace # log every SQL query +# +# Only forward LOG_LEVEL into the subprocess when it's actually provided on the +# command line. Setting it unconditionally (even to empty) would shadow any +# LOG_LEVEL in a local .env, since dotenv does not override existing env vars. +dev: LOG_LEVEL_PREFIX = $(if $(LOG_LEVEL),LOG_LEVEL=$(LOG_LEVEL) ,) +dev: + npm install --no-package-lock + npm --prefix client install --no-package-lock + npm run db:migrate + node bin/dev-bootstrap.js + npm --prefix client run build + @echo "Starting Manager (server + job-runner + client watch) at http://localhost:3000 ..." + $(LOG_LEVEL_PREFIX)npx concurrently -n server,jobs,client -c blue,magenta,green \ + "npx nodemon server.js" \ + "node job-runner.js" \ + "npm --prefix client run build:watch" install: build $(INSTALL) -d $(DESTBIN) diff --git a/create-a-container/bin/dev-bootstrap.js b/create-a-container/bin/dev-bootstrap.js new file mode 100644 index 00000000..f440322d --- /dev/null +++ b/create-a-container/bin/dev-bootstrap.js @@ -0,0 +1,96 @@ +#!/usr/bin/env node +/** + * dev-bootstrap.js + * + * Provisions the minimum data needed to use the Manager locally WITHOUT a real + * Proxmox cluster. Intended to be run by `make dev` (and safe to run by hand). + * + * It creates, idempotently: + * - a `localhost` Site, + * - a `dummy` Node (nodeType: 'dummy') that acts as a mock hypervisor, and + * - a `localhost` ExternalDomain (so image templates that expose an HTTP port, + * which auto-add an HTTP service, have a domain to bind to — otherwise the + * new-container form is unsubmittable). + * + * IMPORTANT: This is deliberately NOT wired into the `/auth/dev` login route. + * Doing so previously clobbered the Docker Compose bootstrap, which calls + * `POST /auth/dev` before creating its own "Development" site — stealing site + * id 1 and importing real nodes/containers into the wrong site. + * + * To stay clear of that path entirely, this script is a NO-OP if ANY Site + * already exists. The Docker stack always creates its own site, so running this + * there does nothing. It only ever populates a fresh, empty local database. + * + * Refuses to run when NODE_ENV === 'production'. + * + * Exit code 0 = success (including the intentional no-op). + */ + +const path = require('path'); +const db = require(path.join(__dirname, '..', 'models')); +const { sequelize, Site, Node, ExternalDomain } = db; + +const DUMMY_NODE_NAME = 'local-dummy'; + +async function main() { + if (process.env.NODE_ENV === 'production') { + console.error('dev-bootstrap: refusing to run in production'); + process.exit(1); + } + + await sequelize.authenticate(); + + // No-op if any site exists — never interfere with an existing environment + // (e.g. the Docker Compose "Development" site). + const existingSiteCount = await Site.count(); + if (existingSiteCount > 0) { + console.log( + `dev-bootstrap: ${existingSiteCount} site(s) already present — nothing to do.`, + ); + return; + } + + console.log('dev-bootstrap: empty database detected, provisioning local dev environment...'); + + const site = await Site.create({ + name: 'localhost', + internalDomain: 'localhost', + dhcpRange: '10.0.0.100,10.0.0.250', + subnetMask: '255.255.255.0', + gateway: '10.0.0.1', + dnsForwarders: '1.1.1.1,8.8.8.8', + externalIp: '127.0.0.1', + }); + console.log(` • Site "localhost" created (id=${site.id})`); + + const node = await Node.create({ + name: DUMMY_NODE_NAME, + nodeType: 'dummy', + siteId: site.id, + ipv4Address: '127.0.0.1', + // Placeholder credentials: a dummy node never talks to a real hypervisor, + // but these must be non-null so code that gates on "has API credentials" + // (e.g. the live container-status resolver) routes through node.api() and + // gets the DummyApi instead of treating the node as unreachable. + apiUrl: 'local', + tokenId: 'local', + secret: 'local', + nvidiaAvailable: false, + }); + console.log(` • Dummy Node "${node.name}" created (id=${node.id}, nodeType=dummy)`); + + const domain = await ExternalDomain.create({ + name: 'localhost', + siteId: site.id, + }); + console.log(` • ExternalDomain "localhost" created (id=${domain.id})`); + + console.log('dev-bootstrap: done. The Manager can now create containers locally (simulated).'); +} + +main() + .then(() => process.exit(0)) + .catch((err) => { + console.error('dev-bootstrap failed:', err.message); + process.exit(1); + }); diff --git a/create-a-container/config/config.js b/create-a-container/config/config.js index 02cb3ec4..8a334993 100644 --- a/create-a-container/config/config.js +++ b/create-a-container/config/config.js @@ -1,6 +1,12 @@ require('dotenv').config(); -const config = { dialect: process.env.DATABASE_DIALECT || 'sqlite' }; +// SQL query logging is noisy (e.g. the job-runner polls the Jobs table on every +// tick), so it is gated behind the most verbose log level. Only LOG_LEVEL=trace +// enables Sequelize query logging — in both development and production. Any +// other level (or unset) keeps it off. +const sqlLogging = (process.env.LOG_LEVEL || '').toLowerCase() === 'trace' ? console.log : false; + +const config = { dialect: process.env.DATABASE_DIALECT || 'sqlite', logging: sqlLogging }; if (config.dialect === 'mysql') { config.host = process.env.MYSQL_HOST; config.port = process.env.MYSQL_PORT; @@ -15,6 +21,20 @@ if (config.dialect === 'mysql') { config.database = process.env.POSTGRES_DATABASE; } else if (config.dialect === 'sqlite') { config.storage = process.env.SQLITE_STORAGE || 'data/database.sqlite'; + // SQLite is a development-only convenience (production uses Postgres). Use the + // actively-maintained @vscode/sqlite3 fork instead of the unmaintained sqlite3 + // package; it builds from source (no prebuilt-binary glibc mismatches) and is + // a dev dependency, so it is required lazily here — only when the sqlite + // dialect is actually selected — and never loaded in production. + try { + config.dialectModule = require('@vscode/sqlite3'); + } catch (err) { + throw new Error( + "DATABASE_DIALECT=sqlite requires the '@vscode/sqlite3' dev dependency. " + + "Install dev dependencies (e.g. `npm install` / `make dev`), or set " + + 'DATABASE_DIALECT=postgres for production.', + ); + } } else { throw new Error(`Unsupported Database Dialect: ${config.dialect}`); } @@ -22,8 +42,7 @@ if (config.dialect === 'mysql') { module.exports = { development: config, test: config, - production: { - ...config, - logging: false - }, + // Query logging is controlled solely by LOG_LEVEL (trace), so production uses + // the same config — it stays silent unless LOG_LEVEL=trace is set. + production: config, }; \ No newline at end of file diff --git a/create-a-container/example.env b/create-a-container/example.env index 35e0204f..93a7a35f 100644 --- a/create-a-container/example.env +++ b/create-a-container/example.env @@ -2,6 +2,11 @@ # you also need to configure the relevant variables for the selected dialect DATABASE_DIALECT= +# log verbosity. "trace" additionally logs every SQL query to the console +# (noisy — e.g. the job-runner polls the Jobs table on every tick), in both +# development and production. Any other value (or unset) keeps SQL logging off. +LOG_LEVEL= + # sqlite3 file path SQLITE_STORAGE= diff --git a/create-a-container/migrations/20260605000000-add-node-type-to-nodes.js b/create-a-container/migrations/20260605000000-add-node-type-to-nodes.js new file mode 100644 index 00000000..320b71e8 --- /dev/null +++ b/create-a-container/migrations/20260605000000-add-node-type-to-nodes.js @@ -0,0 +1,27 @@ +'use strict'; + +/** + * Adds a `nodeType` discriminator to Nodes so the app can select the right + * API client implementation (NodeApi surface): + * - 'proxmox' (default): real Proxmox host, uses ProxmoxApi. + * - 'dummy': dev-only mock hypervisor, uses DummyApi. + * + * Existing rows are backfilled to 'proxmox' via the column default, so every + * production node keeps its current behaviour. Portable across Postgres and + * SQLite (plain string column + default, no ENUM). + * + * @type {import('sequelize-cli').Migration} + */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn('Nodes', 'nodeType', { + type: Sequelize.STRING(50), + allowNull: false, + defaultValue: 'proxmox' + }); + }, + + async down(queryInterface) { + await queryInterface.removeColumn('Nodes', 'nodeType'); + } +}; diff --git a/create-a-container/models/node.js b/create-a-container/models/node.js index 6cdd991e..b3c79061 100644 --- a/create-a-container/models/node.js +++ b/create-a-container/models/node.js @@ -4,6 +4,7 @@ const { } = require('sequelize'); const https = require('https'); const ProxmoxApi = require('../utils/proxmox-api'); +const DummyApi = require('../utils/dummy-api'); module.exports = (sequelize, DataTypes) => { class Node extends Model { @@ -24,15 +25,25 @@ module.exports = (sequelize, DataTypes) => { } /** - * Create an authenticated ProxmoxApi client for this node. - * Detects whether stored credentials are username/password or API token - * based on presence of '!' in tokenId (Proxmox convention). - * @returns {Promise} Authenticated API client - * @throws {Error} If credentials are missing or authentication fails + * Create an API client for this node, selected by `nodeType`. + * + * - `proxmox` (default): returns an authenticated {@link ProxmoxApi}. + * Detects whether stored credentials are username/password or API token + * based on presence of '!' in tokenId (Proxmox convention). + * - `dummy`: returns a {@link DummyApi} that simulates provisioning so the + * full create-container code path can run locally without a hypervisor. + * + * @returns {Promise} API client implementing the NodeApi surface + * @throws {Error} If Proxmox configuration is missing or authentication fails (proxmox only) */ async api() { - if (!this.tokenId || !this.secret) { - throw new Error(`Node ${this.name}: Missing credentials (tokenId and secret required)`); + // Dummy nodes have no real hypervisor; return the simulated client. + if (this.nodeType === 'dummy') { + return new DummyApi(this); + } + + if (!this.apiUrl || !this.tokenId || !this.secret) { + throw new Error(`Node ${this.name}: Missing Proxmox configuration (apiUrl, tokenId, and secret are required)`); } const httpsAgent = new https.Agent({ @@ -61,6 +72,11 @@ module.exports = (sequelize, DataTypes) => { type: DataTypes.STRING(255), allowNull: false }, + nodeType: { + type: DataTypes.STRING(50), + allowNull: false, + defaultValue: 'proxmox' + }, ipv4Address: { type: DataTypes.STRING(15), allowNull: true diff --git a/create-a-container/package-lock.json b/create-a-container/package-lock.json index 7b8b1107..8b231139 100644 --- a/create-a-container/package-lock.json +++ b/create-a-container/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "hasInstallScript": true, "dependencies": { "argon2": "^0.44.0", "axios": "^1.16.0", @@ -18,14 +19,15 @@ "morgan": "^1.10.1", "nodemailer": "^8.0.9", "openid-client": "^5.7.1", + "patch-package": "^8.0.1", "pg": "^8.16.3", "sequelize": "^6.37.8", "sequelize-cli": "^6.6.3", - "sqlite3": "^6.0.1", "swagger-ui-express": "^5.0.1", "yamljs": "^0.3.0" }, "devDependencies": { + "@vscode/sqlite3": "5.1.12-vscode", "concurrently": "^9.1.0", "nodemon": "^3.1.10" } @@ -56,6 +58,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -125,6 +128,24 @@ "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", "license": "MIT" }, + "node_modules/@vscode/sqlite3": { + "version": "5.1.12-vscode", + "resolved": "https://registry.npmjs.org/@vscode/sqlite3/-/sqlite3-5.1.12-vscode.tgz", + "integrity": "sha512-WLTftbMtK3Ni0s+q46qtKJ2CFtA3YrS5N4GcrETDCxqNTQAvk1LlYlG3RwGE6vZLcUqPt3TCHobijYeNUhEQ9Q==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "node-addon-api": "^8.2.0", + "tar": "^7.5.4" + } + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "license": "BSD-2-Clause" + }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -246,26 +267,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -297,26 +298,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -375,7 +356,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -384,30 +364,6 @@ "node": ">=8" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -416,6 +372,24 @@ "node": ">= 0.8" } }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -447,7 +421,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -464,7 +437,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -480,7 +452,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -490,7 +461,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -528,11 +498,27 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -955,28 +941,21 @@ } } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "license": "MIT", "dependencies": { - "mimic-response": "^3.1.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/delayed-stream": { @@ -996,15 +975,6 @@ "node": ">= 0.8" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", @@ -1117,25 +1087,6 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1200,22 +1151,6 @@ "node": ">= 0.6" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "license": "Apache-2.0", - "optional": true - }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -1323,12 +1258,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -1363,7 +1292,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -1388,6 +1316,15 @@ "node": ">= 0.8" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/follow-redirects": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", @@ -1477,12 +1414,6 @@ "node": ">= 0.8" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -1571,12 +1502,6 @@ "node": ">= 0.4" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -1660,6 +1585,18 @@ "node": ">=4" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1717,26 +1654,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -1820,6 +1737,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1856,7 +1788,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -1867,6 +1798,24 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1941,6 +1890,25 @@ "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", "license": "MIT" }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", @@ -1953,6 +1921,24 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -2004,6 +1990,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -2023,18 +2022,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -2069,6 +2056,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -2077,12 +2065,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -2152,12 +2134,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -2166,18 +2142,6 @@ "node": ">= 0.6" } }, - "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "8.7.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", @@ -2187,31 +2151,6 @@ "node": "^18 || ^20 || >= 21" } }, - "node_modules/node-gyp": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", - "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", - "license": "MIT", - "optional": true, - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "tar": "^7.5.4", - "tinyglobby": "^0.2.12", - "undici": "^6.25.0", - "which": "^6.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -2222,58 +2161,6 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "license": "ISC", - "optional": true, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "license": "BlueOak-1.0.0", - "optional": true, - "engines": { - "node": ">=20" - } - }, - "node_modules/node-gyp/node_modules/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "license": "ISC", - "optional": true, - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-gyp/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "license": "ISC", - "optional": true, - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/nodemailer": { "version": "8.0.9", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.9.tgz", @@ -2357,6 +2244,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/oidc-token-hash": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", @@ -2393,6 +2289,22 @@ "wrappy": "1" } }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openid-client": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", @@ -2422,6 +2334,49 @@ "node": ">= 0.8" } }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2577,7 +2532,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -2625,43 +2579,6 @@ "node": ">=0.10.0" } }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "license": "ISC", - "optional": true, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -2696,16 +2613,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -2766,35 +2673,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3032,6 +2910,23 @@ "node": ">= 18" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -3151,51 +3046,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -3209,6 +3059,15 @@ "node": ">=10" } }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -3224,33 +3083,6 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, - "node_modules/sqlite3": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-6.0.1.tgz", - "integrity": "sha512-X0czUUMG2tmSqJpEQa3tCuZSHKIx8PwM53vLZzKp/o6Rpy25fiVfjdbnZ988M8+O3ZWR1ih0K255VumCb3MAnQ==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "^8.0.0", - "prebuild-install": "^7.1.3", - "tar": "^7.5.10" - }, - "engines": { - "node": ">=20.17.0" - }, - "optionalDependencies": { - "node-gyp": "12.x" - }, - "peerDependencies": { - "node-gyp": "12.x" - }, - "peerDependenciesMeta": { - "node-gyp": { - "optional": true - } - } - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -3259,15 +3091,6 @@ "node": ">= 0.8" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -3364,15 +3187,6 @@ "node": ">=8" } }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -3426,6 +3240,7 @@ "version": "7.5.16", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -3438,93 +3253,19 @@ "node": ">=18" } }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "license": "MIT", - "optional": true, - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "license": "MIT", - "optional": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=14.14" } }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -3574,18 +3315,6 @@ "dev": true, "license": "0BSD" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -3629,16 +3358,6 @@ "dev": true, "license": "MIT" }, - "node_modules/undici": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", - "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18.17" - } - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -3662,12 +3381,6 @@ "node": ">= 0.8" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -3835,11 +3548,27 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yamljs": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", diff --git a/create-a-container/package.json b/create-a-container/package.json index 78bdbf6d..15b7a26a 100644 --- a/create-a-container/package.json +++ b/create-a-container/package.json @@ -6,9 +6,10 @@ ] }, "scripts": { + "postinstall": "patch-package", "dev": "concurrently -n server,client -c blue,green \"nodemon server.js\" \"npm --prefix client run build:watch\"", "dev:server": "nodemon server.js", - "db:migrate": "sequelize db:migrate && sequelize db:seed:all", + "db:migrate": "sequelize-cli db:migrate && sequelize-cli db:seed:all", "job-runner": "node job-runner.js", "client:install": "npm --prefix client install", "client:dev": "npm --prefix client run dev", @@ -31,14 +32,15 @@ "morgan": "^1.10.1", "nodemailer": "^8.0.9", "openid-client": "^5.7.1", + "patch-package": "^8.0.1", "pg": "^8.16.3", "sequelize": "^6.37.8", "sequelize-cli": "^6.6.3", - "sqlite3": "^6.0.1", "swagger-ui-express": "^5.0.1", "yamljs": "^0.3.0" }, "devDependencies": { + "@vscode/sqlite3": "5.1.12-vscode", "concurrently": "^9.1.0", "nodemon": "^3.1.10" } diff --git a/create-a-container/patches/sequelize+6.37.8.patch b/create-a-container/patches/sequelize+6.37.8.patch new file mode 100644 index 00000000..b8235001 --- /dev/null +++ b/create-a-container/patches/sequelize+6.37.8.patch @@ -0,0 +1,17 @@ +diff --git a/node_modules/sequelize/lib/dialects/sqlite/query-interface.js b/node_modules/sequelize/lib/dialects/sqlite/query-interface.js +index c1626f6..3d43497 100644 +--- a/node_modules/sequelize/lib/dialects/sqlite/query-interface.js ++++ b/node_modules/sequelize/lib/dialects/sqlite/query-interface.js +@@ -149,10 +149,8 @@ class SQLiteQueryInterface extends QueryInterface { + data[prop].unique = false; + } + for (const index of indexes) { +- for (const field of index.fields) { +- if (index.unique !== void 0) { +- data[field.attribute].unique = index.unique; +- } ++ if (index.fields.length === 1 && index.unique !== void 0) { ++ data[index.fields[0].attribute].unique = index.unique; + } + } + const foreignKeys = await this.getForeignKeyReferencesForTable(tableName, options); diff --git a/create-a-container/routers/api/v1/containers.js b/create-a-container/routers/api/v1/containers.js index 8f6fbfe4..ad517589 100644 --- a/create-a-container/routers/api/v1/containers.js +++ b/create-a-container/routers/api/v1/containers.js @@ -288,11 +288,22 @@ router.post( if (!imageRef) throw new ApiError(400, 'invalid_request', 'template is required'); const templateName = normalizeDockerRef(imageRef); + // Select the least-loaded provisionable node in the site, balancing by + // container count. A node is provisionable if it's a dummy node or a node + // with full API configuration (apiUrl/tokenId/secret). This excludes + // half-configured nodes that would only fail later in node.api(). Beyond + // that, the NodeApi abstraction (`node.api()`) hides how a node is + // provisioned. const nodeWhere = { siteId: site.id, - apiUrl: { [Sequelize.Op.ne]: null }, - tokenId: { [Sequelize.Op.ne]: null }, - secret: { [Sequelize.Op.ne]: null }, + [Sequelize.Op.or]: [ + { nodeType: 'dummy' }, + { + apiUrl: { [Sequelize.Op.ne]: null }, + tokenId: { [Sequelize.Op.ne]: null }, + secret: { [Sequelize.Op.ne]: null }, + }, + ], }; if (wantsNvidia) nodeWhere.nvidiaAvailable = true; const node = await Node.findOne({ @@ -308,7 +319,7 @@ router.post( if (!node && wantsNvidia) { throw new ApiError(409, 'no_nvidia_node', 'No NVIDIA-capable nodes available in this site'); } - if (!node) throw new ApiError(409, 'no_node', 'No nodes with API access available in this site'); + if (!node) throw new ApiError(409, 'no_node', 'No provisionable nodes available in this site (a node needs API access or must be a dummy node)'); const container = await Container.create( { @@ -585,7 +596,11 @@ router.delete( if (httpServices.length > 0) { dnsWarnings = await manageDnsRecords(httpServices, site, 'delete'); } - if (container.containerId && node.apiUrl && node.tokenId) { + // Delete the backing VM through the node's API. The NodeApi abstraction + // (`node.api()`) hides the provider; a dummy node simply no-ops here. We + // only attempt this when the container was actually provisioned (has a + // VMID/containerId). + if (container.containerId) { try { const api = await node.api(); const config = await api.lxcConfig(node.name, container.containerId); @@ -599,7 +614,7 @@ router.delete( await api.deleteContainer(node.name, container.containerId, true, true); } catch (err) { if (err instanceof ApiError) throw err; - console.log(`Proxmox deletion skipped or failed: ${err.message}`); + console.log(`Node-side deletion skipped or failed: ${err.message}`); } } await container.destroy(); diff --git a/create-a-container/seeders/20251121000000-seed-groups.js b/create-a-container/seeders/20251121000000-seed-groups.js index 3bdee3c3..ad4978cc 100644 --- a/create-a-container/seeders/20251121000000-seed-groups.js +++ b/create-a-container/seeders/20251121000000-seed-groups.js @@ -5,22 +5,24 @@ const GID_MIN = 2000; /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { - await queryInterface.bulkInsert('Groups', [ - { - gidNumber: GID_MIN, - cn: 'sysadmins', - isAdmin: true, - createdAt: new Date(), - updatedAt: new Date() - }, - { - gidNumber: GID_MIN + 1, - cn: 'ldapusers', - isAdmin: false, - createdAt: new Date(), - updatedAt: new Date() - } - ], {}); + const now = new Date(); + const groups = [ + { gidNumber: GID_MIN, cn: 'sysadmins', isAdmin: true, createdAt: now, updatedAt: now }, + { gidNumber: GID_MIN + 1, cn: 'ldapusers', isAdmin: false, createdAt: now, updatedAt: now }, + ]; + + // Idempotent: only insert groups that don't already exist, so re-running + // seeders (e.g. `make dev`) doesn't violate the unique constraints on + // gidNumber/cn. + const [existing] = await queryInterface.sequelize.query( + `SELECT "gidNumber" FROM "Groups" WHERE "gidNumber" IN (:gids)`, + { replacements: { gids: groups.map((g) => g.gidNumber) } }, + ); + const existingGids = new Set(existing.map((r) => r.gidNumber)); + const toInsert = groups.filter((g) => !existingGids.has(g.gidNumber)); + if (toInsert.length === 0) return; + + await queryInterface.bulkInsert('Groups', toInsert, {}); }, async down(queryInterface, Sequelize) { diff --git a/create-a-container/seeders/20260120165612-push-notification-settings.js b/create-a-container/seeders/20260120165612-push-notification-settings.js index 071be2c5..b797f94c 100644 --- a/create-a-container/seeders/20260120165612-push-notification-settings.js +++ b/create-a-container/seeders/20260120165612-push-notification-settings.js @@ -3,20 +3,23 @@ /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { - await queryInterface.bulkInsert('Settings', [ - { - key: 'push_notification_url', - value: '', - createdAt: new Date(), - updatedAt: new Date() - }, - { - key: 'push_notification_enabled', - value: 'false', - createdAt: new Date(), - updatedAt: new Date() - } - ], {}); + const now = new Date(); + const settings = [ + { key: 'push_notification_url', value: '', createdAt: now, updatedAt: now }, + { key: 'push_notification_enabled', value: 'false', createdAt: now, updatedAt: now }, + ]; + + // Idempotent: only insert keys that don't already exist, so re-running + // seeders doesn't violate the unique constraint on Settings.key. + const [existing] = await queryInterface.sequelize.query( + `SELECT "key" FROM "Settings" WHERE "key" IN (:keys)`, + { replacements: { keys: settings.map((s) => s.key) } }, + ); + const existingKeys = new Set(existing.map((r) => r.key)); + const toInsert = settings.filter((s) => !existingKeys.has(s.key)); + if (toInsert.length === 0) return; + + await queryInterface.bulkInsert('Settings', toInsert, {}); }, async down(queryInterface, Sequelize) { diff --git a/create-a-container/seeders/20260313000000-push-notification-api-key.js b/create-a-container/seeders/20260313000000-push-notification-api-key.js index 5fdfeada..ff50e358 100644 --- a/create-a-container/seeders/20260313000000-push-notification-api-key.js +++ b/create-a-container/seeders/20260313000000-push-notification-api-key.js @@ -3,13 +3,16 @@ /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { + // Idempotent: skip if the key already exists, so re-running seeders doesn't + // violate the unique constraint on Settings.key. + const [existing] = await queryInterface.sequelize.query( + `SELECT "key" FROM "Settings" WHERE "key" = 'push_notification_api_key'`, + ); + if (existing.length > 0) return; + + const now = new Date(); await queryInterface.bulkInsert('Settings', [ - { - key: 'push_notification_api_key', - value: '', - createdAt: new Date(), - updatedAt: new Date() - } + { key: 'push_notification_api_key', value: '', createdAt: now, updatedAt: now }, ], {}); }, diff --git a/create-a-container/utils/dummy-api.js b/create-a-container/utils/dummy-api.js new file mode 100644 index 00000000..f1fbd626 --- /dev/null +++ b/create-a-container/utils/dummy-api.js @@ -0,0 +1,305 @@ +const crypto = require('crypto'); + +// Monotonic VMID source for dummy nodes. Seeded from the current time so that +// IDs are unique across separate processes (each `create-container.js` run gets +// its own DummyApi), and incremented so they are never reused within a process. +// Proxmox VMIDs go up to 999999999; we start in a high range to avoid colliding +// with anything a real node would allocate and to leave plenty of headroom. +let nextDummyVmid = 100000 + (Date.now() % 900000000); + +/** + * DummyApi + * + * A drop-in stand-in for {@link ProxmoxApi} used by "dummy" nodes (Node.nodeType + * === 'dummy'). It implements the same method surface so the real container + * provisioning code path — `bin/create-container.js`, run by the job-runner — + * executes end-to-end without a Proxmox hypervisor. + * + * This is deliberately NOT a short-circuit in the HTTP handler: every method a + * real run would call is exercised here. Only the Proxmox interactions are + * faked; everything else (job-runner, model updates, DNS/NetBox hooks, Docker + * registry digest lookups) runs exactly as it does in production. + * + * State is kept in-memory per process keyed by VMID so reads are consistent + * with writes (e.g. `updateLxcConfig` is reflected by a later `lxcConfig`). + * The job-runner spawns one process per job, so this lifetime is sufficient. + */ +class DummyApi { + /** + * @param {object} [node] - The Node model instance this client represents. + * Used only for plausible storage names and logging. + */ + constructor(node = {}) { + this.node = node; + /** @type {Map} VMID -> fake LXC config */ + this.configs = new Map(); + console.log(`[DummyApi] initialized for node "${node.name || 'dummy'}" (no Proxmox; provisioning is simulated)`); + } + + /** Generate a single uppercase hex byte. */ + static _hexByte() { + return crypto.randomBytes(1)[0].toString(16).padStart(2, '0').toUpperCase(); + } + + /** Build a plausible, locally-administered unicast MAC (02:00:00 prefix). */ + static _fakeMac() { + // 02:xx:xx has the locally-administered bit set and the multicast bit + // clear, so it's a valid unicast LAA — not a real vendor OUI. + return `02:00:00:${DummyApi._hexByte()}:${DummyApi._hexByte()}:${DummyApi._hexByte()}`; + } + + /** Build a plausible private IPv4 address derived from a VMID. */ + static _fakeIp(vmid) { + // Spread VMIDs across the 10.x.y.z space using both octets so two dev + // containers don't collide on the globally-unique ipv4Address column. + const n = Number(vmid) || 0; + const third = (Math.floor(n / 254) % 254) + 1; + const fourth = (n % 254) + 1; + return `10.0.${third}.${fourth}`; + } + + /** + * Ensure a config record exists for a VMID, seeding a net0 with a stable MAC. + * @param {number} vmid + * @returns {object} + */ + _ensureConfig(vmid) { + let cfg = this.configs.get(vmid); + if (!cfg) { + cfg = { + net0: `name=eth0,hwaddr=${DummyApi._fakeMac()},ip=dhcp,bridge=${this.node.networkBridge || 'vmbr0'}`, + cores: 4, + memory: 4096, + rootfs: `local:vm-${vmid}-disk-0,size=50G`, + }; + this.configs.set(vmid, cfg); + } + return cfg; + } + + // --- VMID allocation ------------------------------------------------------- + + async nextId() { + // Monotonic and never reused within this process; the time-based seed makes + // collisions across processes (and thus against the (nodeId, containerId) + // unique constraint) highly unlikely. + const vmid = nextDummyVmid++; + console.log(`[DummyApi] nextId -> ${vmid}`); + return vmid; + } + + // --- Storage --------------------------------------------------------------- + + /** + * Pretend the preferred storage exists and supports the requested content + * type, so `resolveStorage()` in create-container.js resolves cleanly. + */ + async datastores(node, content = null) { + const preferred = + content === 'vztmpl' + ? this.node.imageStorage || 'local' + : this.node.volumeStorage || 'local-lvm'; + return [ + { + storage: preferred, + type: 'dir', + content: content || 'rootdir', + enabled: 1, + active: 1, + total: 1024 * 1024 * 1024 * 1024, // 1 TiB + avail: 1024 * 1024 * 1024 * 1024, + used: 0, + }, + ]; + } + + /** + * Report the requested template as already present so the OCI pull path is + * skipped. For other content types (e.g. snippets), report nothing. + */ + async storageContents(node, storage, content = null) { + if (content === 'vztmpl') { + // create-container.js builds the expected volid as + // `${storage}:vztmpl/${filename}.tar` and checks for its presence. We + // can't know the exact filename here, so returning a permissive match is + // not possible; instead we return empty and rely on pullOciImage being a + // no-op. (Kept explicit for clarity.) + return []; + } + return []; + } + + async pullOciImage(node, storage, options = {}) { + console.log(`[DummyApi] pullOciImage(${options.reference || '?'}) -> simulated`); + return this._fakeUpid('imgpull'); + } + + // --- Container lifecycle --------------------------------------------------- + + _fakeUpid(kind) { + const rnd = crypto.randomBytes(4).toString('hex'); + return `UPID:dummy:0000${rnd}:00000000:00000000:${kind}::dummy@local:`; + } + + async createLxc(node, options = {}) { + const vmid = options.vmid; + const cfg = this._ensureConfig(vmid); + if (options.cores != null) cfg.cores = parseInt(options.cores, 10); + if (options.memory != null) cfg.memory = parseInt(options.memory, 10); + if (options.rootfs) { + // rootfs is passed as "storage:sizeGb" at create time; normalize to a + // readable size= form so parseRootfsSizeGb() can read it back. + const m = /:(\d+)$/.exec(options.rootfs); + if (m) cfg.rootfs = `${options.rootfs.split(':')[0]}:vm-${vmid}-disk-0,size=${m[1]}G`; + } + if (options.net0) { + // Preserve the generated hwaddr; create-container passes net0 without one. + const hwaddr = /hwaddr=([0-9A-Fa-f:]+)/.exec(cfg.net0)?.[1] || DummyApi._fakeMac(); + cfg.net0 = `${options.net0},hwaddr=${hwaddr}`; + } + console.log(`[DummyApi] createLxc vmid=${vmid} -> simulated`); + return this._fakeUpid('vzcreate'); + } + + async cloneLxc(node, vmid, newid, options = {}) { + this._ensureConfig(newid); + console.log(`[DummyApi] cloneLxc ${vmid} -> ${newid} (simulated)`); + return this._fakeUpid('vzclone'); + } + + async getLxcTemplates(node) { + // Surface whatever template name the container asked for as available, so + // the Proxmox-template branch of create-container.js can find it. + return [{ vmid: 8999, name: this._requestedTemplateName || 'dummy-template', template: 1 }]; + } + + async updateLxcConfig(node, vmid, config = {}) { + const cfg = this._ensureConfig(vmid); + Object.assign(cfg, config); + console.log(`[DummyApi] updateLxcConfig vmid=${vmid} keys=${Object.keys(config).join(',') || '(none)'}`); + } + + async lxcConfig(node, vmid) { + return { ...this._ensureConfig(vmid) }; + } + + async startLxc(node, vmid) { + const cfg = this._ensureConfig(vmid); + cfg._running = true; + console.log(`[DummyApi] startLxc vmid=${vmid} (simulated)`); + return this._fakeUpid('vzstart'); + } + + async stopLxc(node, vmid) { + const cfg = this._ensureConfig(vmid); + cfg._running = false; + console.log(`[DummyApi] stopLxc vmid=${vmid} (simulated)`); + return this._fakeUpid('vzstop'); + } + + async getLxcStatus(node, vmid) { + const cfg = this._ensureConfig(vmid); + return { status: cfg._running ? 'running' : 'stopped', vmid }; + } + + async deleteContainer(nodeName, vmid) { + this.configs.delete(vmid); + console.log(`[DummyApi] deleteContainer vmid=${vmid} (simulated)`); + return { data: null }; + } + + // --- Tasks ----------------------------------------------------------------- + + async taskStatus(node, upid) { + return { status: 'stopped', exitstatus: 'OK', upid }; + } + + /** Always succeeds immediately — there is no real task to poll. */ + async waitForTask(node, upid) { + console.log(`[DummyApi] waitForTask ${upid} -> OK (immediate)`); + return { status: 'stopped', exitstatus: 'OK' }; + } + + // --- Network introspection ------------------------------------------------- + + async lxcInterfaces(node, vmid) { + const cfg = this._ensureConfig(vmid); + const mac = /hwaddr=([0-9A-Fa-f:]+)/.exec(cfg.net0)?.[1] || DummyApi._fakeMac(); + return [ + { + name: 'eth0', + hwaddr: mac, + inet: `${DummyApi._fakeIp(vmid)}/24`, + 'ip-addresses': [ + { 'ip-address-type': 'inet', 'ip-address': DummyApi._fakeIp(vmid) }, + ], + }, + ]; + } + + async getLxcMacAddress(node, vmid) { + const cfg = this._ensureConfig(vmid); + const mac = /hwaddr=([0-9A-Fa-f:]+)/.exec(cfg.net0)?.[1] || null; + console.log(`[DummyApi] getLxcMacAddress vmid=${vmid} -> ${mac}`); + return mac; + } + + async getLxcIpAddress(node, vmid) { + const ip = DummyApi._fakeIp(vmid); + console.log(`[DummyApi] getLxcIpAddress vmid=${vmid} -> ${ip}`); + return ip; + } + + async getLxcNetworkInfo(node, vmid) { + return { + macAddress: await this.getLxcMacAddress(node, vmid), + ipv4Address: await this.getLxcIpAddress(node, vmid), + }; + } + + // --- ACL / realm (no-ops) -------------------------------------------------- + + async updateAcl() { + console.log('[DummyApi] updateAcl -> no-op'); + } + + async syncLdapRealm() { + console.log('[DummyApi] syncLdapRealm -> no-op'); + } + + // --- Misc read APIs used by routers --------------------------------------- + + async nodes() { + return [{ node: this.node.name || 'dummy', status: 'online', type: 'node' }]; + } + + /** + * Report a cluster-resources snapshot for this dummy node. The live status + * resolver (utils/container-status.js) calls this with type 'lxc' and treats + * any returned entry as a running LXC. DummyApi state is per-process, so we + * derive the snapshot from the database instead: every container on this + * dummy node that already has a VMID (containerId) is reported as running. + * This keeps simulated containers showing as `running` after creation. + * @param {string} [type] + * @returns {Promise>} + */ + async clusterResources(type = null) { + if (type && type !== 'lxc') return []; + if (this.node.id == null) return []; + // Lazy require to avoid a load-time cycle (models/node.js -> dummy-api.js). + const { Container } = require('../models'); + const containers = await Container.findAll({ + where: { nodeId: this.node.id, containerId: { [require('sequelize').Op.ne]: null } }, + attributes: ['containerId', 'hostname'], + }); + return containers.map((c) => ({ + vmid: c.containerId, + name: c.hostname, + type: 'lxc', + status: 'running', + node: this.node.name || 'dummy', + })); + } +} + +module.exports = DummyApi; diff --git a/mie-opensource-landing/docs/developers/development-workflow.md b/mie-opensource-landing/docs/developers/development-workflow.md index 9541aa88..0fca35ef 100644 --- a/mie-opensource-landing/docs/developers/development-workflow.md +++ b/mie-opensource-landing/docs/developers/development-workflow.md @@ -1,12 +1,76 @@ # Development Workflow +There are two ways to run the Manager locally: + +- **`make dev`** — a lightweight, Proxmox-free workflow for iterating on the + Manager web app itself (SQLite, runs on `localhost`). Start here for UI/API work. +- **Docker stack** — the full stack (Proxmox, Manager, docs, bootstrap) for + exercising real container provisioning against a virtualized Proxmox. + +## Run the Manager Locally (`make dev`) + +Use this when you're working on the Manager web app and don't need a real +hypervisor. It runs `create-a-container` against SQLite and uses a **dummy +node** as a mock hypervisor so you can create containers without Proxmox. + +```bash +make dev +``` + +This will: + +1. Install dependencies. This also applies the bundled + [`patch-package`](https://www.npmjs.com/package/patch-package) patches — + notably a backport of [sequelize#17583](https://github.com/sequelize/sequelize/pull/17583) + that fixes SQLite incorrectly adding a `UNIQUE` constraint to members of a + composite index. +2. Run database migrations. +3. Seed a local dev environment (a `localhost` site, a `dummy` node, and a + `localhost` external domain) — but only when the database is empty, so it + never interferes with the Docker stack's bootstrap. +4. Build the React client. +5. Start the **server** and the **job-runner** together, serving at + [http://localhost:3000](http://localhost:3000). + +No configuration is required — the server's defaults are sufficient for +development: it uses SQLite (`data/database.sqlite`), generates a session +secret on first run, and enables the dev login when not running in production. + +Environment variables can be passed on the command line, e.g. to log every SQL +query (verbose): + +```bash +make dev LOG_LEVEL=trace +``` + +Log in via the dev login button (no Proxmox or external IdP required). + +!!! note "How mock provisioning works" + Creating a container still goes through the real code path: + `POST /containers` → a `Job` row → the **job-runner** → `bin/create-container.js` + → `node.api()`. For a dummy node, `node.api()` returns a `DummyApi` (instead + of `ProxmoxApi`) that implements the same interface and simulates the + Proxmox calls, so the container lands `running` with a placeholder + VMID/MAC/IP. This is why `make dev` runs the job-runner alongside the server. + + The Docker registry digest lookup is **not** mocked, so resolving an image + requires network access (the same as production). + +!!! note "Don't mix node types in a site" + Node selection is provider-agnostic and does not distinguish dummy from real + nodes — it just picks the least-loaded node in the site. Keep dummy nodes in + dev-only sites; don't add one to a site that has real Proxmox nodes. To + exercise actual provisioning, use the Docker stack below. + +## Full Stack with Docker + The entire stack — Proxmox, Manager, docs, and bootstrap — runs locally inside Docker. -## Prerequisites +### Prerequisites - [Docker Desktop](https://www.docker.com/products/docker-desktop/) -## Start the Stack +### Start the Stack From the repository root: @@ -25,7 +89,7 @@ This brings up: | `zensical` | Rebuilds these docs on file changes | | Bootstrap (one-shot) | Configures the Manager container to use the virtualized Proxmox | -## Manager Image Selection +### Manager Image Selection By default the compose stack deploys `ghcr.io/mieweb/opensource-server/manager:latest`. Override the tag by setting `MANAGER_TAG` in your environment or in a `.env` file alongside `compose.yml`: @@ -41,7 +105,7 @@ The template download step checks your **local** Docker images first, so a local - Delete the cached tar file from the `local` volume, or - Recreate the volume (e.g., `docker compose down -v`). -## Persistent State and Full Reset +### Persistent State and Full Reset Proxmox state is persisted across container creation and destruction in two named volumes, so `docker compose down` followed by `docker compose up` keeps @@ -62,7 +126,7 @@ This deletes the cached template tarball, all stored images/volumes, and the cluster state, so the next `docker compose up` re-downloads the Manager template and re-runs the bootstrap from scratch. -## Endpoints +### Endpoints | URL | Description | |---|---| @@ -71,7 +135,7 @@ and re-runs the bootstrap from scratch. | `https://localhost` | Documentation site | | `https://manager.localhost` | Manager Web UI | -## Credentials and Shell Access +### Credentials and Shell Access **Proxmox Web UI:** `root` / `root` @@ -93,7 +157,7 @@ docker compose exec -it proxmox pct enter 100 docker compose exec -it proxmox pct exec 100 -- sudo -u postgres psql cluster_manager ``` -## Live Reload +### Live Reload The local git repository is mounted **read-only** into `/opt/opensource-server` on the Manager container, so source changes on the host are visible immediately. @@ -105,7 +169,7 @@ The local git repository is mounted **read-only** into `/opt/opensource-server` | Job runner | Restart manually | | Database migrations | Run manually in the proper server context (see below) | -### Frontend Rebuilds +#### Frontend Rebuilds The Manager serves the compiled React app from `create-a-container/client/dist`; it does **not** run the Vite dev server. Because the repository is mounted **read-only** into the Manager container, Vite can't write build output from there. Instead, the dedicated `client` service mounts the repository **read-write** and runs `vite build --watch`, rebuilding `client/dist` on the host whenever the client source changes. @@ -115,7 +179,7 @@ The Manager container's read-only bind mount still reflects those host changes, The `client` service performs an initial build before Proxmox starts serving (the `proxmox` service waits for it to become healthy), so a bundle always exists even on a clean checkout where `client/dist` isn't present yet. -### Run Database Migrations +#### Run Database Migrations ```bash docker compose exec proxmox pct exec 100 -- \ diff --git a/mie-opensource-landing/docs/developers/release-pipeline.md b/mie-opensource-landing/docs/developers/release-pipeline.md index 533996f9..6ea7d8b6 100644 --- a/mie-opensource-landing/docs/developers/release-pipeline.md +++ b/mie-opensource-landing/docs/developers/release-pipeline.md @@ -62,13 +62,15 @@ The top-level `Makefile` simply forwards these targets to every component and co ## Development -`make dev` runs the long-running watch loops: +Each component's `dev` target runs it locally: ```bash -make -C create-a-container dev # server (nodemon) + client (vite watch) +make -C create-a-container dev # Manager on localhost (SQLite, no Proxmox); see the Development Workflow guide make -C mie-opensource-landing dev # docs live server ``` +`make dev` at the repo root delegates to `create-a-container`. + ## Packaging with fpm Each component has a `.fpm` options file holding the static package metadata (name, architecture, dependencies, description, scripts, config files). The Makefile's `package` target stages the component into a `.pkg/buildroot` and runs [fpm](https://fpm.readthedocs.io/) with the dynamic options on the command line — output type, version (composed per format by `./package-version`), and the staging dir. fpm's `dir` input copies the staged tree verbatim from `-C .pkg/buildroot`, preserving symlinks (e.g. `node_modules/.bin/sequelize`) and the directory layout. The same definition produces deb, rpm, and apk, so `make rpm` and `make apk` also work.