From df2315087d3179be336dbd19ee53785ae6845dcd Mon Sep 17 00:00:00 2001 From: SinachPat Date: Sat, 15 Aug 2026 20:18:34 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Sprint=201=20remainder=20=E2=80=94=20Po?= =?UTF-8?q?stgres=20store,=20Docker=20client,=20warm=20pool,=20sessions=20?= =?UTF-8?q?route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PostgresUserStore (Queryable) + migration + env-gated wiring - DockerodeClient behind injected DockerEngine - ImageManager, WarmPool, Dockerfile.wordpress, docker-compose - POST /sessions spins up sandbox (503 when unconfigured) - ADRs 0014-0015; 68 unit tests green --- .env.example | 5 + api/__tests__/routes/sessions.test.ts | 40 ++ .../sandbox/dockerode-client.test.ts | 71 ++ api/__tests__/sandbox/image-manager.test.ts | 19 + .../services/postgres-user-store.test.ts | 44 ++ api/__tests__/services/warm-pool.test.ts | 48 ++ api/migrations/001_init.sql | 6 + api/package.json | 6 +- api/src/app.ts | 14 +- api/src/index.ts | 27 +- api/src/routes/auth.ts | 4 +- api/src/routes/sessions.ts | 16 + api/src/sandbox/dockerode-client.ts | 43 ++ api/src/sandbox/image-manager.ts | 28 + api/src/services/postgres-user-store.ts | 32 + api/src/services/user-store.ts | 8 +- api/src/services/warm-pool.ts | 25 + docs/decisions/0014-postgres-queryable.md | 26 + .../decisions/0015-dockerode-engine-gating.md | 27 + docs/decisions/README.md | 2 + infrastructure/docker/Dockerfile.wordpress | 4 + infrastructure/docker/docker-compose.yml | 30 + pnpm-lock.yaml | 605 ++++++++++++++++++ 23 files changed, 1119 insertions(+), 11 deletions(-) create mode 100644 api/__tests__/routes/sessions.test.ts create mode 100644 api/__tests__/sandbox/dockerode-client.test.ts create mode 100644 api/__tests__/sandbox/image-manager.test.ts create mode 100644 api/__tests__/services/postgres-user-store.test.ts create mode 100644 api/__tests__/services/warm-pool.test.ts create mode 100644 api/migrations/001_init.sql create mode 100644 api/src/routes/sessions.ts create mode 100644 api/src/sandbox/dockerode-client.ts create mode 100644 api/src/sandbox/image-manager.ts create mode 100644 api/src/services/postgres-user-store.ts create mode 100644 api/src/services/warm-pool.ts create mode 100644 docs/decisions/0014-postgres-queryable.md create mode 100644 docs/decisions/0015-dockerode-engine-gating.md create mode 100644 infrastructure/docker/Dockerfile.wordpress create mode 100644 infrastructure/docker/docker-compose.yml diff --git a/.env.example b/.env.example index a30b1db..f68cbcd 100644 --- a/.env.example +++ b/.env.example @@ -18,4 +18,9 @@ LLM_FALLBACK_PROVIDER= # Sandbox DOCKER_HOST= +# Set to 1 to enable sandbox spin-up via Docker (leave unset to return 503 on /sessions). +WUR_ENABLE_SANDBOX= +WUR_IMAGE=wursor-base:latest +WUR_WEB_PORT=8080 +PREVIEW_BASE_URL=http://localhost:8080 WARM_POOL_HOT_SPARES=2 diff --git a/api/__tests__/routes/sessions.test.ts b/api/__tests__/routes/sessions.test.ts new file mode 100644 index 0000000..306a835 --- /dev/null +++ b/api/__tests__/routes/sessions.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest'; +import { buildApp } from '../../src/app.ts'; +import { SandboxManager } from '../../src/services/sandbox-manager.ts'; +import type { DockerClient, SandboxInfo } from '../../src/sandbox/docker-client.ts'; + +class FakeDocker implements DockerClient { + async createSandbox(): Promise { + return { id: 'sb-1', status: 'running' }; + } + + async destroySandbox(): Promise {} + + async status(): Promise { + return undefined; + } +} + +describe('POST /sessions', () => { + it('spins up a sandbox and returns a preview URL', async () => { + const manager = new SandboxManager(new FakeDocker(), { + image: 'wursor-base:latest', + previewBaseUrl: 'https://preview.wursor.dev', + }); + const app = await buildApp({ sandboxManager: manager }); + + const res = await app.inject({ method: 'POST', url: '/sessions' }); + expect(res.statusCode).toBe(201); + const body = res.json(); + expect(body.sandboxId).toBe('sb-1'); + expect(body.previewUrl).toBe('https://preview.wursor.dev/sb-1'); + expect(body.sessionId).toBeTruthy(); + }); + + it('returns 503 when no sandbox manager is configured', async () => { + const app = await buildApp(); + const res = await app.inject({ method: 'POST', url: '/sessions' }); + expect(res.statusCode).toBe(503); + expect(res.json().error).toBe('sandbox_not_configured'); + }); +}); diff --git a/api/__tests__/sandbox/dockerode-client.test.ts b/api/__tests__/sandbox/dockerode-client.test.ts new file mode 100644 index 0000000..a1dccbf --- /dev/null +++ b/api/__tests__/sandbox/dockerode-client.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from 'vitest'; +import { DockerodeClient, type ContainerLike, type DockerEngine } from '../../src/sandbox/dockerode-client.ts'; + +class FakeEngine implements DockerEngine { + createdImages: string[] = []; + names: string[] = []; + removed: string[] = []; + + constructor( + private readonly opts: { running?: boolean; failInspect?: boolean } = {}, + ) {} + + private container(): ContainerLike { + return { + start: async () => {}, + inspect: async () => { + if (this.opts.failInspect) { + throw new Error('not found'); + } + return { Id: 'id-1', State: { Running: this.opts.running ?? true } }; + }, + remove: async () => { + this.removed.push('id-1'); + }, + }; + } + + async createContainer(spec: { Image: string; name: string }): Promise { + this.createdImages.push(spec.Image); + this.names.push(spec.name); + return this.container(); + } + + getContainer(_id: string): ContainerLike { + return this.container(); + } +} + +describe('DockerodeClient', () => { + it('spins up a sandbox container from an image and reports it running', async () => { + const engine = new FakeEngine(); + const client = new DockerodeClient(engine); + + const info = await client.createSandbox('wursor-base:latest'); + expect(info).toEqual({ id: 'id-1', status: 'running' }); + expect(engine.createdImages).toEqual(['wursor-base:latest']); + expect(engine.names[0]?.startsWith('wursor-')).toBe(true); + }); + + it('reports destroyed when the container is not running after start', async () => { + const engine = new FakeEngine({ running: false }); + const client = new DockerodeClient(engine); + + expect(await client.createSandbox('wursor-base:latest')).toEqual({ id: 'id-1', status: 'destroyed' }); + }); + + it('destroys a sandbox container', async () => { + const engine = new FakeEngine(); + const client = new DockerodeClient(engine); + + await client.destroySandbox('id-1'); + expect(engine.removed).toEqual(['id-1']); + }); + + it('returns undefined status when the container is gone', async () => { + const engine = new FakeEngine({ failInspect: true }); + const client = new DockerodeClient(engine); + + expect(await client.status('id-1')).toBeUndefined(); + }); +}); diff --git a/api/__tests__/sandbox/image-manager.test.ts b/api/__tests__/sandbox/image-manager.test.ts new file mode 100644 index 0000000..cb1a302 --- /dev/null +++ b/api/__tests__/sandbox/image-manager.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest'; +import { ImageManager } from '../../src/sandbox/image-manager.ts'; + +describe('ImageManager', () => { + const manager = new ImageManager({ baseImage: 'wursor-base:latest', webPort: 8080 }); + + it('returns the pre-baked image ref', () => { + expect(manager.imageRef()).toBe('wursor-base:latest'); + }); + + it('produces a container spec with the web port and management label', () => { + expect(manager.containerSpec('wursor-abc')).toEqual({ + image: 'wursor-base:latest', + name: 'wursor-abc', + ports: [{ container: 80, host: 8080 }], + labels: { 'wursor.managed': 'true' }, + }); + }); +}); diff --git a/api/__tests__/services/postgres-user-store.test.ts b/api/__tests__/services/postgres-user-store.test.ts new file mode 100644 index 0000000..9481bba --- /dev/null +++ b/api/__tests__/services/postgres-user-store.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { PostgresUserStore } from '../../src/services/postgres-user-store.ts'; +import type { Queryable } from '../../src/services/postgres-user-store.ts'; + +class FakeDb implements Queryable { + calls: Array<{ text: string; values?: unknown[] }> = []; + rows: unknown[] = []; + + async query(text: string, values?: unknown[]) { + this.calls.push({ text, values }); + return { rows: this.rows }; + } +} + +describe('PostgresUserStore', () => { + it('returns a user when the email exists', async () => { + const db = new FakeDb(); + db.rows = [{ id: 'u1', email: 'a@example.com', password_hash: 'hash' }]; + const store = new PostgresUserStore(db); + + const user = await store.findByEmail('a@example.com'); + expect(user).toEqual({ id: 'u1', email: 'a@example.com', passwordHash: 'hash' }); + expect(db.calls[0]?.text).toContain('SELECT'); + }); + + it('returns undefined when the email is absent', async () => { + const db = new FakeDb(); + db.rows = []; + const store = new PostgresUserStore(db); + + expect(await store.findByEmail('nope@example.com')).toBeUndefined(); + }); + + it('inserts a new user and returns it', async () => { + const db = new FakeDb(); + const store = new PostgresUserStore(db); + + const created = await store.create({ id: 'u1', email: 'a@example.com', passwordHash: 'hash' }); + expect(created).toEqual({ id: 'u1', email: 'a@example.com', passwordHash: 'hash' }); + const insert = db.calls[0]; + expect(insert?.text).toContain('INSERT INTO users'); + expect(insert?.values).toEqual(['u1', 'a@example.com', 'hash']); + }); +}); diff --git a/api/__tests__/services/warm-pool.test.ts b/api/__tests__/services/warm-pool.test.ts new file mode 100644 index 0000000..35be9f9 --- /dev/null +++ b/api/__tests__/services/warm-pool.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { WarmPool, computeSpares } from '../../src/services/warm-pool.ts'; +import type { DockerClient, SandboxInfo } from '../../src/sandbox/docker-client.ts'; + +class FakeDocker implements DockerClient { + created: string[] = []; + + async createSandbox(image: string): Promise { + this.created.push(image); + return { id: `sb-${this.created.length}`, status: 'running' }; + } + + async destroySandbox(): Promise {} + + async status(): Promise { + return undefined; + } +} + +describe('computeSpares', () => { + it('returns the shortfall', () => { + expect(computeSpares(1, 3)).toBe(2); + }); + + it('never returns a negative number', () => { + expect(computeSpares(5, 2)).toBe(0); + }); +}); + +describe('WarmPool', () => { + it('tops up the pool to the target hot-spare count', async () => { + const docker = new FakeDocker(); + const pool = new WarmPool(docker, { image: 'wursor-base:latest', hotSpares: 2 }); + + const result = await pool.topUp(0); + expect(result.created).toBe(2); + expect(docker.created).toEqual(['wursor-base:latest', 'wursor-base:latest']); + }); + + it('does nothing when the pool is already full', async () => { + const docker = new FakeDocker(); + const pool = new WarmPool(docker, { image: 'wursor-base:latest', hotSpares: 2 }); + + const result = await pool.topUp(3); + expect(result.created).toBe(0); + expect(docker.created).toEqual([]); + }); +}); diff --git a/api/migrations/001_init.sql b/api/migrations/001_init.sql new file mode 100644 index 0000000..9cd1038 --- /dev/null +++ b/api/migrations/001_init.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); diff --git a/api/package.json b/api/package.json index e019ca8..f904f83 100644 --- a/api/package.json +++ b/api/package.json @@ -8,10 +8,14 @@ "start": "tsx src/index.ts" }, "dependencies": { - "fastify": "^5.2.0" + "dockerode": "^5.0.1", + "fastify": "^5.2.0", + "pg": "^8.23.0" }, "devDependencies": { + "@types/dockerode": "^4.0.1", "@types/node": "^22.10.0", + "@types/pg": "^8.21.0", "tsx": "^4.20.3", "typescript": "^5.9.2", "vitest": "^3.2.4" diff --git a/api/src/app.ts b/api/src/app.ts index 5b7aada..cfa9860 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -1,11 +1,19 @@ import Fastify from 'fastify'; import { authRoutes } from './routes/auth.ts'; -import { InMemoryUserStore } from './services/user-store.ts'; +import { sessionRoutes } from './routes/sessions.ts'; +import { InMemoryUserStore, type UserStore } from './services/user-store.ts'; +import type { SandboxManager } from './services/sandbox-manager.ts'; -export async function buildApp() { +export type BuildAppOptions = { + userStore?: UserStore; + sandboxManager?: SandboxManager; +}; + +export async function buildApp(opts: BuildAppOptions = {}) { const app = Fastify(); - const store = new InMemoryUserStore(); + const store = opts.userStore ?? new InMemoryUserStore(); app.get('/health', async () => ({ ok: true })); await authRoutes(app, store); + await sessionRoutes(app, opts.sandboxManager); return app; } diff --git a/api/src/index.ts b/api/src/index.ts index 4942369..f417d26 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -1,6 +1,31 @@ +import Docker from 'dockerode'; +import { Pool } from 'pg'; import { buildApp } from './app.ts'; +import { DockerodeClient } from './sandbox/dockerode-client.ts'; +import { ImageManager } from './sandbox/image-manager.ts'; +import { PostgresUserStore } from './services/postgres-user-store.ts'; +import { SandboxManager } from './services/sandbox-manager.ts'; +import { InMemoryUserStore } from './services/user-store.ts'; const port = Number(process.env.PORT ?? 3000); -const app = await buildApp(); + +const userStore = process.env.DATABASE_URL + ? new PostgresUserStore(new Pool({ connectionString: process.env.DATABASE_URL })) + : new InMemoryUserStore(); + +const imageManager = new ImageManager({ + baseImage: process.env.WUR_IMAGE ?? 'wursor-base:latest', + webPort: Number(process.env.WUR_WEB_PORT ?? 8080), +}); + +const sandboxManager = + process.env.WUR_ENABLE_SANDBOX === '1' + ? new SandboxManager(new DockerodeClient(new Docker()), { + image: imageManager.imageRef(), + previewBaseUrl: process.env.PREVIEW_BASE_URL ?? 'http://localhost:8080', + }) + : undefined; + +const app = await buildApp({ userStore, sandboxManager }); await app.listen({ port, host: '0.0.0.0' }); diff --git a/api/src/routes/auth.ts b/api/src/routes/auth.ts index e08d0c8..9a868c3 100644 --- a/api/src/routes/auth.ts +++ b/api/src/routes/auth.ts @@ -20,11 +20,11 @@ export async function authRoutes(app: FastifyInstance, store: UserStore): Promis if (password === undefined || password.length < 8) { return reply.status(400).send({ error: 'weak_password' }); } - if (store.findByEmail(email) !== undefined) { + if ((await store.findByEmail(email)) !== undefined) { return reply.status(409).send({ error: 'email_exists' }); } - const user = store.create({ id: newId(), email, passwordHash: hashPassword(password) }); + const user = await store.create({ id: newId(), email, passwordHash: hashPassword(password) }); return reply.status(201).send({ user: { id: user.id, email: user.email }, sessionToken: newToken() }); }); } diff --git a/api/src/routes/sessions.ts b/api/src/routes/sessions.ts new file mode 100644 index 0000000..2f82980 --- /dev/null +++ b/api/src/routes/sessions.ts @@ -0,0 +1,16 @@ +import type { FastifyInstance } from 'fastify'; +import { newId } from '../lib/crypto.ts'; +import type { SandboxManager } from '../services/sandbox-manager.ts'; + +export async function sessionRoutes( + app: FastifyInstance, + sandboxManager: SandboxManager | undefined, +): Promise { + app.post('/sessions', async (_request, reply) => { + if (sandboxManager === undefined) { + return reply.status(503).send({ error: 'sandbox_not_configured' }); + } + const { sandboxId, previewUrl } = await sandboxManager.start(); + return reply.status(201).send({ sessionId: newId(), sandboxId, previewUrl }); + }); +} diff --git a/api/src/sandbox/dockerode-client.ts b/api/src/sandbox/dockerode-client.ts new file mode 100644 index 0000000..2e9f96c --- /dev/null +++ b/api/src/sandbox/dockerode-client.ts @@ -0,0 +1,43 @@ +import Docker from 'dockerode'; +import { randomUUID } from 'node:crypto'; +import type { DockerClient, SandboxInfo } from './docker-client.ts'; + +export type ContainerInspect = { + Id: string; + State: { Running: boolean }; +}; + +export type ContainerLike = { + start(): Promise; + inspect(): Promise; + remove(opts?: { force?: boolean }): Promise; +}; + +export type DockerEngine = { + createContainer(opts: { Image: string; name: string }): Promise; + getContainer(id: string): ContainerLike; +}; + +export class DockerodeClient implements DockerClient { + constructor(private readonly engine: DockerEngine = new Docker() as unknown as DockerEngine) {} + + async createSandbox(image: string): Promise { + const container = await this.engine.createContainer({ Image: image, name: `wursor-${randomUUID()}` }); + await container.start(); + const info = await container.inspect(); + return { id: info.Id, status: info.State.Running ? 'running' : 'destroyed' }; + } + + async destroySandbox(id: string): Promise { + await this.engine.getContainer(id).remove({ force: true }); + } + + async status(id: string): Promise { + try { + const info = await this.engine.getContainer(id).inspect(); + return { id: info.Id, status: info.State.Running ? 'running' : 'destroyed' }; + } catch { + return undefined; + } + } +} diff --git a/api/src/sandbox/image-manager.ts b/api/src/sandbox/image-manager.ts new file mode 100644 index 0000000..426cc5e --- /dev/null +++ b/api/src/sandbox/image-manager.ts @@ -0,0 +1,28 @@ +export type ImageManagerOptions = { + baseImage: string; + webPort: number; +}; + +export type SandboxContainerSpec = { + image: string; + name: string; + ports: { container: number; host: number }[]; + labels: Record; +}; + +export class ImageManager { + constructor(private readonly opts: ImageManagerOptions) {} + + imageRef(): string { + return this.opts.baseImage; + } + + containerSpec(name: string): SandboxContainerSpec { + return { + image: this.opts.baseImage, + name, + ports: [{ container: 80, host: this.opts.webPort }], + labels: { 'wursor.managed': 'true' }, + }; + } +} diff --git a/api/src/services/postgres-user-store.ts b/api/src/services/postgres-user-store.ts new file mode 100644 index 0000000..63ab3b5 --- /dev/null +++ b/api/src/services/postgres-user-store.ts @@ -0,0 +1,32 @@ +import type { User, UserStore } from './user-store.ts'; + +export type Queryable = { + query(text: string, values?: unknown[]): Promise<{ rows: unknown[] }>; +}; + +type UserRow = { + id: string; + email: string; + password_hash: string; +}; + +export class PostgresUserStore implements UserStore { + constructor(private readonly db: Queryable) {} + + async findByEmail(email: string): Promise { + const result = await this.db.query('SELECT id, email, password_hash FROM users WHERE email = $1', [email]); + const row = result.rows[0] as UserRow | undefined; + return row === undefined + ? undefined + : { id: row.id, email: row.email, passwordHash: row.password_hash }; + } + + async create(user: User): Promise { + await this.db.query('INSERT INTO users (id, email, password_hash) VALUES ($1, $2, $3)', [ + user.id, + user.email, + user.passwordHash, + ]); + return user; + } +} diff --git a/api/src/services/user-store.ts b/api/src/services/user-store.ts index 2328013..edb2c6f 100644 --- a/api/src/services/user-store.ts +++ b/api/src/services/user-store.ts @@ -5,18 +5,18 @@ export type User = { }; export interface UserStore { - findByEmail(email: string): User | undefined; - create(user: User): User; + findByEmail(email: string): Promise; + create(user: User): Promise; } export class InMemoryUserStore implements UserStore { private byEmail = new Map(); - findByEmail(email: string): User | undefined { + async findByEmail(email: string): Promise { return this.byEmail.get(email); } - create(user: User): User { + async create(user: User): Promise { this.byEmail.set(user.email, user); return user; } diff --git a/api/src/services/warm-pool.ts b/api/src/services/warm-pool.ts new file mode 100644 index 0000000..d676698 --- /dev/null +++ b/api/src/services/warm-pool.ts @@ -0,0 +1,25 @@ +import type { DockerClient } from '../sandbox/docker-client.ts'; + +export function computeSpares(current: number, target: number): number { + return Math.max(0, target - current); +} + +export type WarmPoolOptions = { + image: string; + hotSpares: number; +}; + +export class WarmPool { + constructor( + private readonly docker: DockerClient, + private readonly opts: WarmPoolOptions, + ) {} + + async topUp(currentRunning: number): Promise<{ created: number }> { + const need = computeSpares(currentRunning, this.opts.hotSpares); + for (let i = 0; i < need; i += 1) { + await this.docker.createSandbox(this.opts.image); + } + return { created: need }; + } +} diff --git a/docs/decisions/0014-postgres-queryable.md b/docs/decisions/0014-postgres-queryable.md new file mode 100644 index 0000000..3b84432 --- /dev/null +++ b/docs/decisions/0014-postgres-queryable.md @@ -0,0 +1,26 @@ +# 14. Postgres user store via a Queryable boundary; schema in SQL migrations + +- **Status:** Accepted +- **Date:** 2026-08-15 + +## Context + +ADR 0012 deferred Postgres behind the `UserStore` interface. Sprint 1 now ships the real implementation. Postgres is not available in the dev environment, so the store had to be testable without a database. + +## Decision + +`PostgresUserStore` uses `node-postgres` (`pg`) but depends on a minimal `Queryable` interface (`query(text, values)`) instead of `pg.Pool` directly. `UserStore` methods became async. The `users` table lives in `api/migrations/001_init.sql`. `index.ts` selects Postgres when `DATABASE_URL` is set, else the in-memory store. + +### Options considered + +- ORM (Prisma/Drizzle) with migrations. +- Raw `pg` behind a `Queryable` interface (chosen). + +### Rejected + +- ORM — adds tooling and a codegen step for a two-statement surface; the raw SQL is reviewable and the interface keeps tests database-free. + +## Consequences + +- `InMemoryUserStore` and `PostgresUserStore` share the same async `UserStore` contract; swapping is env-driven. +- Auth data is durable when `DATABASE_URL` is configured; the migration must be applied before first use. diff --git a/docs/decisions/0015-dockerode-engine-gating.md b/docs/decisions/0015-dockerode-engine-gating.md new file mode 100644 index 0000000..015010f --- /dev/null +++ b/docs/decisions/0015-dockerode-engine-gating.md @@ -0,0 +1,27 @@ +# 15. Docker daemon client via dockerode behind an injected engine; sandbox gated by env + +- **Status:** Accepted +- **Date:** 2026-08-15 + +## Context + +ADR 0013 deferred the real Docker client. Sprint 1 now ships it. There is still no Docker daemon in the dev environment, so the client had to be testable without one. + +## Decision + +`DockerodeClient` implements `DockerClient` using `dockerode`, but depends on an injected `DockerEngine` (a minimal `createContainer`/`getContainer` surface) instead of `dockerode` directly. The API enables it via `WUR_ENABLE_SANDBOX=1`; when off, `POST /sessions` returns 503. `index.ts` constructs `new Docker()` (dockerode auto-detects `DOCKER_HOST` or the unix socket). + +### Options considered + +- Raw Docker Engine HTTP over the unix socket. +- dockerode behind an injected engine (chosen). + +### Rejected + +- Raw HTTP — dockerode already handles unix sockets, TLS, and API version negotiation; reimplementing it is pure waste. + +## Consequences + +- `DockerodeClient` is unit-tested with a fake engine; only the daemon wiring remains to be verified on a Docker host. +- Sandbox spin-up is opt-in, so local dev without Docker still boots and `auth`/`sessions` behave predictably (503 on sessions). +- Port mapping is a Sprint 1 placeholder (single fixed host port); dynamic port allocation and preview proxying are follow-ups. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 8d705d8..c7571fb 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -27,6 +27,8 @@ Each ADR is a single file following the [Nygard format](https://cognitect.com/bl | [0011](0011-fastify-react-stack.md) | Fastify is the API server; React + Vite is the web shell | Accepted | | [0012](0012-in-memory-user-store.md) | In-memory user store behind a UserStore interface; Postgres deferred | Accepted | | [0013](0013-docker-boundary-mock.md) | Sandbox orchestration mocks the Docker boundary; real daemon client deferred | Accepted | +| [0014](0014-postgres-queryable.md) | Postgres user store via a Queryable boundary; schema in SQL migrations | Accepted | +| [0015](0015-dockerode-engine-gating.md) | Docker daemon client via dockerode behind an injected engine; sandbox gated by env | Accepted | ## How to add one diff --git a/infrastructure/docker/Dockerfile.wordpress b/infrastructure/docker/Dockerfile.wordpress new file mode 100644 index 0000000..4ce28a0 --- /dev/null +++ b/infrastructure/docker/Dockerfile.wordpress @@ -0,0 +1,4 @@ +FROM wordpress:6.7-php8.2 + +RUN curl -fsSL https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o /usr/local/bin/wp \ + && chmod +x /usr/local/bin/wp diff --git a/infrastructure/docker/docker-compose.yml b/infrastructure/docker/docker-compose.yml new file mode 100644 index 0000000..fe682ea --- /dev/null +++ b/infrastructure/docker/docker-compose.yml @@ -0,0 +1,30 @@ +services: + db: + image: mysql:8.0 + environment: + MYSQL_DATABASE: wordpress + MYSQL_USER: wordpress + MYSQL_PASSWORD: wordpress + MYSQL_ROOT_PASSWORD: root + volumes: + - db_data:/var/lib/mysql + + wordpress: + build: + context: . + dockerfile: Dockerfile.wordpress + depends_on: + - db + ports: + - "8080:80" + environment: + WORDPRESS_DB_HOST: db + WORDPRESS_DB_USER: wordpress + WORDPRESS_DB_PASSWORD: wordpress + WORDPRESS_DB_NAME: wordpress + volumes: + - wp_data:/var/www/html + +volumes: + db_data: + wp_data: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 834dd45..f668424 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,13 +10,25 @@ importers: api: dependencies: + dockerode: + specifier: ^5.0.1 + version: 5.0.1 fastify: specifier: ^5.2.0 version: 5.12.0 + pg: + specifier: ^8.23.0 + version: 8.23.0 devDependencies: + '@types/dockerode': + specifier: ^4.0.1 + version: 4.0.1 '@types/node': specifier: ^22.10.0 version: 22.20.1 + '@types/pg': + specifier: ^8.21.0 + version: 8.21.0 tsx: specifier: ^4.20.3 version: 4.23.12 @@ -107,6 +119,9 @@ packages: resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} + '@balena/dockerignore@1.0.2': + resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + '@bramus/specificity@2.4.2': resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true @@ -330,9 +345,26 @@ packages: '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@grpc/grpc-js@1.14.4': + resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} engines: {node: ^22.20 || ^24.12 || >=25} @@ -351,6 +383,33 @@ packages: engines: {node: '>=20'} hasBin: true + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + '@rolldown/binding-android-arm64@1.2.4': resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -626,12 +685,24 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/docker-modem@3.0.6': + resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} + + '@types/dockerode@4.0.1': + resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@22.20.1': resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + '@types/pg@8.21.0': + resolution: {integrity: sha512-AYdtudzabjLZgVgRZmAnU8bAnVUXzuJX2IYHeSIiIHm68olD+LgQYCGWdtcNYnP0uq9c4S4NibVG3Ni7VbKW7Q==} + '@types/react-dom@19.2.4': resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} peerDependencies: @@ -640,6 +711,9 @@ packages: '@types/react@19.2.18': resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + '@types/ssh2@1.15.5': + resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} + '@vitejs/plugin-react@6.0.5': resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -700,6 +774,10 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} @@ -711,6 +789,9 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -722,9 +803,25 @@ packages: avvio@9.3.0: resolution: {integrity: sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buildcheck@0.0.7: + resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} + engines: {node: '>=10.0.0'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -737,10 +834,28 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + css-tree@3.2.1: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -779,12 +894,26 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + docker-modem@5.0.7: + resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==} + engines: {node: '>= 8.0'} + + dockerode@5.0.1: + resolution: {integrity: sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==} + engines: {node: '>= 14.17'} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + entities@8.0.0: resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} engines: {node: '>=20.19.0'} @@ -797,6 +926,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -841,6 +974,9 @@ packages: resolution: {integrity: sha512-JtyUgATO7qxRp2zKhrmWof74Mqxc1ikbwpwMY97p8ipuTj2QtreA4gK2JNAF6SOqqHnYYkwMUvsgQVi2AJxIyw==} engines: {node: '>=20'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -851,18 +987,32 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ipaddr.js@2.5.0: resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==} engines: {node: '>= 10'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -964,6 +1114,12 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} @@ -985,9 +1141,15 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nan@2.28.0: + resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==} + nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -997,6 +1159,9 @@ packages: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + parse5@8.0.1: resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} @@ -1007,6 +1172,40 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.16.0: + resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.23.0: + resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1038,6 +1237,22 @@ packages: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -1048,6 +1263,13 @@ packages: process-warning@5.1.0: resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} + engines: {node: '>=12.0.0'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1067,6 +1289,10 @@ packages: resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -1078,6 +1304,10 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -1103,6 +1333,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex2@5.1.1: resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} hasBin: true @@ -1111,6 +1344,9 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -1139,16 +1375,34 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + split-ca@1.0.1: + resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} + engines: {node: '>=10.16.0'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -1159,6 +1413,13 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + thread-stream@4.2.0: resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} engines: {node: '>=20'} @@ -1209,11 +1470,17 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -1221,6 +1488,9 @@ packages: resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} engines: {node: '>=22.19.0'} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1362,6 +1632,13 @@ packages: engines: {node: '>=8'} hasBin: true + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -1369,6 +1646,22 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + snapshots: '@adobe/css-tools@4.5.0': {} @@ -1398,6 +1691,8 @@ snapshots: '@babel/runtime@7.29.7': {} + '@balena/dockerignore@1.0.2': {} + '@bramus/specificity@2.4.2': dependencies: css-tree: 3.2.1 @@ -1529,8 +1824,29 @@ snapshots: '@fastify/forwarded': 3.0.2 ipaddr.js: 2.5.0 + '@grpc/grpc-js@1.14.4': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.5 + yargs: 17.7.3 + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.5 + yargs: 17.7.3 + '@jridgewell/sourcemap-codec@1.5.5': {} + '@js-sdsl/ordered-map@4.4.2': {} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': optional: true @@ -1542,6 +1858,26 @@ snapshots: dependencies: playwright: 1.62.1 + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + '@rolldown/binding-android-arm64@1.2.4': optional: true @@ -1707,12 +2043,33 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/docker-modem@3.0.6': + dependencies: + '@types/node': 22.20.1 + '@types/ssh2': 1.15.5 + + '@types/dockerode@4.0.1': + dependencies: + '@types/docker-modem': 3.0.6 + '@types/node': 22.20.1 + '@types/ssh2': 1.15.5 + '@types/estree@1.0.9': {} + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/node@22.20.1': dependencies: undici-types: 6.21.0 + '@types/pg@8.21.0': + dependencies: + '@types/node': 22.20.1 + pg-protocol: 1.16.0 + pg-types: 2.2.0 + '@types/react-dom@19.2.4(@types/react@19.2.18)': dependencies: '@types/react': 19.2.18 @@ -1721,6 +2078,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/ssh2@1.15.5': + dependencies: + '@types/node': 18.19.130 + '@vitejs/plugin-react@6.0.5(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.2)(tsx@4.23.12))': dependencies: '@rolldown/pluginutils': 1.0.1 @@ -1783,6 +2144,10 @@ snapshots: ansi-regex@5.0.1: {} + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + ansi-styles@5.2.0: {} aria-query@5.3.0: @@ -1791,6 +2156,10 @@ snapshots: aria-query@5.3.2: {} + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + assertion-error@2.0.1: {} atomic-sleep@1.0.0: {} @@ -1800,10 +2169,30 @@ snapshots: '@fastify/error': 4.2.0 fastq: 1.20.1 + base64-js@1.5.1: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buildcheck@0.0.7: + optional: true + cac@6.7.14: {} chai@5.3.3: @@ -1816,8 +2205,28 @@ snapshots: check-error@2.1.3: {} + chownr@1.1.4: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + cookie@1.1.1: {} + cpu-features@0.0.10: + dependencies: + buildcheck: 0.0.7 + nan: 2.28.0 + optional: true + css-tree@3.2.1: dependencies: mdn-data: 2.27.1 @@ -1846,10 +2255,36 @@ snapshots: detect-libc@2.1.2: {} + docker-modem@5.0.7: + dependencies: + debug: 4.4.3 + readable-stream: 3.6.2 + split-ca: 1.0.1 + ssh2: 1.17.0 + transitivePeerDependencies: + - supports-color + + dockerode@5.0.1: + dependencies: + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.14.4 + '@grpc/proto-loader': 0.7.15 + docker-modem: 5.0.7 + protobufjs: 7.6.5 + tar-fs: 2.1.5 + transitivePeerDependencies: + - supports-color + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} + emoji-regex@8.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + entities@8.0.0: {} es-module-lexer@1.7.0: {} @@ -1883,6 +2318,8 @@ snapshots: '@esbuild/win32-ia32': 0.28.2 '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -1942,22 +2379,32 @@ snapshots: fast-querystring: 1.1.2 safe-regex2: 5.1.1 + fs-constants@1.0.0: {} + fsevents@2.3.2: optional: true fsevents@2.3.3: optional: true + get-caller-file@2.0.5: {} + html-encoding-sniffer@6.0.0: dependencies: '@exodus/bytes': 1.15.1 transitivePeerDependencies: - '@noble/hashes' + ieee754@1.2.1: {} + indent-string@4.0.0: {} + inherits@2.0.4: {} + ipaddr.js@2.5.0: {} + is-fullwidth-code-point@3.0.0: {} + is-potential-custom-element-name@1.0.1: {} js-tokens@4.0.0: {} @@ -2051,6 +2498,10 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 + lodash.camelcase@4.3.0: {} + + long@5.3.2: {} + loupe@3.2.1: {} lru-cache@11.5.2: {} @@ -2065,12 +2516,21 @@ snapshots: min-indent@1.0.1: {} + mkdirp-classic@0.5.3: {} + ms@2.1.3: {} + nan@2.28.0: + optional: true + nanoid@3.3.18: {} on-exit-leak-free@2.1.2: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + parse5@8.0.1: dependencies: entities: 8.0.0 @@ -2079,6 +2539,41 @@ snapshots: pathval@2.0.1: {} + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.23.0): + dependencies: + pg: 8.23.0 + + pg-protocol@1.16.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.23.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.23.0) + pg-protocol: 1.16.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@4.0.5: {} @@ -2117,6 +2612,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -2127,6 +2632,25 @@ snapshots: process-warning@5.1.0: {} + protobufjs@7.6.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 22.20.1 + long: 5.3.2 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} quick-format-unescaped@4.0.4: {} @@ -2140,6 +2664,12 @@ snapshots: react@19.2.8: {} + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + real-require@0.2.0: {} real-require@1.0.0: {} @@ -2149,6 +2679,8 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + require-directory@2.1.1: {} + require-from-string@2.0.2: {} ret@0.5.0: {} @@ -2209,12 +2741,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.4 fsevents: 2.3.3 + safe-buffer@5.2.1: {} + safe-regex2@5.1.1: dependencies: ret: 0.5.0 safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -2235,12 +2771,36 @@ snapshots: source-map-js@1.2.1: {} + split-ca@1.0.1: {} + split2@4.2.0: {} + ssh2@1.17.0: + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.28.0 + stackback@0.0.2: {} std-env@3.10.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -2251,6 +2811,21 @@ snapshots: symbol-tree@3.2.4: {} + tar-fs@2.1.5: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + thread-stream@4.2.0: dependencies: real-require: 1.0.0 @@ -2292,12 +2867,18 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tweetnacl@0.14.5: {} + typescript@5.9.3: {} + undici-types@5.26.5: {} + undici-types@6.21.0: {} undici@8.10.0: {} + util-deprecate@1.0.2: {} + vite-node@3.2.4(@types/node@22.20.1)(lightningcss@1.33.0)(tsx@4.23.12): dependencies: cac: 6.7.14 @@ -2417,6 +2998,30 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1