From 6f8276b303dd78cfa34b2c02c9523455089b3b9b Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Mon, 2 Jun 2025 23:32:22 -0700 Subject: [PATCH] chore(examples): multitenant deploys example --- examples/multitenant-deploys/Dockerfile | 27 + examples/multitenant-deploys/README.md | 121 + examples/multitenant-deploys/package.json | 23 + examples/multitenant-deploys/src/app.ts | 155 + examples/multitenant-deploys/src/index.ts | 10 + .../multitenant-deploys/tests/deploy.test.ts | 90 + examples/multitenant-deploys/tsconfig.json | 12 + examples/system-test-route/Dockerfile | 2 +- examples/system-test-route/package.json | 1 - examples/system-test-route/rivet.jsonc | 6 +- .../system-test-route/src/container/main.ts | 14 - examples/system-test-route/src/index.ts | 50 + .../system-test-route/src/isolate/main.ts | 22 - .../system-test-route/src/shared/server.ts | 60 - examples/system-test-route/tests/client.ts | 3418 ++++++++--------- examples/system-test-route/yarn.lock | 1325 +++++++ 16 files changed, 3524 insertions(+), 1812 deletions(-) create mode 100644 examples/multitenant-deploys/Dockerfile create mode 100644 examples/multitenant-deploys/README.md create mode 100644 examples/multitenant-deploys/package.json create mode 100644 examples/multitenant-deploys/src/app.ts create mode 100644 examples/multitenant-deploys/src/index.ts create mode 100644 examples/multitenant-deploys/tests/deploy.test.ts create mode 100644 examples/multitenant-deploys/tsconfig.json delete mode 100644 examples/system-test-route/src/container/main.ts create mode 100644 examples/system-test-route/src/index.ts delete mode 100644 examples/system-test-route/src/isolate/main.ts delete mode 100644 examples/system-test-route/src/shared/server.ts create mode 100644 examples/system-test-route/yarn.lock diff --git a/examples/multitenant-deploys/Dockerfile b/examples/multitenant-deploys/Dockerfile new file mode 100644 index 0000000000..14bf0bf02f --- /dev/null +++ b/examples/multitenant-deploys/Dockerfile @@ -0,0 +1,27 @@ +FROM node:18-alpine + +WORKDIR /app + +# Install rivet CLI +RUN apk add --no-cache curl unzip +RUN curl -fsSL https://get.rivet.gg/install.sh | sh + +# Copy package files and install dependencies +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile + +# Copy application code +COPY . . + +# Build the application +RUN yarn build + +# Expose the port the app will run on +EXPOSE 3000 + +# Create a non-root user +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +USER appuser + +# Start the application +CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/examples/multitenant-deploys/README.md b/examples/multitenant-deploys/README.md new file mode 100644 index 0000000000..e8fb6a20c3 --- /dev/null +++ b/examples/multitenant-deploys/README.md @@ -0,0 +1,121 @@ +# Multitenant Deploys for Rivet + +A simple Node.js service for handling multi-tenant deployments with Rivet. + +## Features + +- Accepts source code uploads via a multipart POST request +- Validates the presence of a Dockerfile +- Deploys the code to Rivet using `rivet publish` +- Sets up a custom domain route for the application + +## Getting Started + +### Prerequisites + +- Node.js +- [Rivet CLI](https://rivet.gg/docs/install) +- Rivet cloud token ([instructions on how to generate](https://rivet.gg/docs/tokens#cloud-token)) +- Rivet project ID + - For example if your project is at `https://hub.rivet.gg/projects/foobar`, the ID is `foobar` +- Rivet environment ID + - For example if your environment is at `https://hub.rivet.gg/projects/foobar/environments/prod`, the ID is `prod` + +### Environment Variables + +You'll need to set the following environment variables: + +```bash +RIVET_CLOUD_TOKEN=your_rivet_cloud_token +RIVET_PROJECT=your_project_id +RIVET_ENVIRONMENT=your_environment_name +PORT=3000 # Optional, defaults to 3000 +``` + +You can do this by using [`export`](https://askubuntu.com/a/58828) or [dotenv](https://www.npmjs.com/package/dotenv). + +### Developing + +```bash +yarn install +yarn dev +``` + +You can now use `POST http://locahlost:3000/deploy/my-app-id`. Read more about example usage below. + +### Testing + +```bash +yarn test +``` + +## API Usage + +`POST /deploy/:appId` + +**Request:** +- URL Path Parameter: + - `appId`: Unique identifier for the application (3-30 characters, lowercase alphanumeric with hyphens) +- Multipart form data containing: + - `Dockerfile`: A valid Dockerfile for the application (required) + - Additional files for the application + +**Response:** +```json +{ + "success": true, + "appId": "your-app-id", + "endpoint": "https://your-app-id.example.com" +} +``` + +## Example Usage + +```javascript +const appId = "my-app-id"; + +// Form data that includes project files +const formData = new FormData(); + +const serverContent = ` +const http = require("http"); +const server = http.createServer((req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("Hello from " + process.env.MY_ENV_VAR); +}); +server.listen(8080); +`; +const serverBlob = new Blob([serverContent], { + type: "application/octet-stream" +}); +formData.append("server.js", serverBlob, "server.js"); + +const dockerfileContent = ` +FROM node:22-alpine +WORKDIR /app +COPY . . + +# Set env var from build arg +ARG MY_ENV_VAR +ENV MY_ENV_VAR=$MY_ENV_VAR + +# Create a non-root user +RUN addgroup -S rivetgroup && adduser -S rivet -G rivetgroup +USER rivet + +CMD ["node", "server.js"] +`; +const dockerfileBlob = new Blob([dockerfileContent], { + type: "application/octet-stream" +}); +formData.append("Dockerfile", dockerfileBlob, "Dockerfile"); + +// Run the deploy +const response = fetch(`http://localhost:3000/deploy/${appId}`, { + method: "POST", + body: formData +}); +if (response.ok) { + const { endpoint } = await response.json(); +} +``` diff --git a/examples/multitenant-deploys/package.json b/examples/multitenant-deploys/package.json new file mode 100644 index 0000000000..e321b1b2d6 --- /dev/null +++ b/examples/multitenant-deploys/package.json @@ -0,0 +1,23 @@ +{ + "name": "multitenant-deploys", + "packageManager": "yarn@4.6.0", + "scripts": { + "dev": "tsx src/index.ts", + "test": "vitest run", + "build": "tsc" + }, + "dependencies": { + "@hono/node-server": "^1.7.0", + "@rivet-gg/api-full": "workspace:*", + "axios": "^1.6.7", + "hono": "^4.0.5", + "temp": "^0.9.4" + }, + "devDependencies": { + "@types/node": "^20.11.19", + "@types/temp": "^0.9.4", + "tsx": "^4.7.0", + "typescript": "^5.3.3", + "vitest": "^1.2.2" + } +} diff --git a/examples/multitenant-deploys/src/app.ts b/examples/multitenant-deploys/src/app.ts new file mode 100644 index 0000000000..5f359b9d7b --- /dev/null +++ b/examples/multitenant-deploys/src/app.ts @@ -0,0 +1,155 @@ +import { Hono } from "hono"; +import { exec } from "node:child_process"; +import { promisify } from "node:util"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import temp from "temp"; +import { RivetClient } from "@rivet-gg/api-full"; + +const execAsync = promisify(exec); + +// Auto-track and cleanup temp directories/files +temp.track(); + +// Config +const RIVET_CLOUD_TOKEN = process.env.RIVET_CLOUD_TOKEN; +const RIVET_PROJECT = process.env.RIVET_PROJECT; +const RIVET_ENVIRONMENT = process.env.RIVET_ENVIRONMENT; + +if (!RIVET_CLOUD_TOKEN || !RIVET_PROJECT || !RIVET_ENVIRONMENT) { + throw new Error( + "Missing required environment variables: RIVET_CLOUD_TOKEN, RIVET_PROJECT, RIVET_ENVIRONMENT", + ); +} + +export const rivet = new RivetClient({ token: RIVET_CLOUD_TOKEN }); + +export const app = new Hono(); + +app.onError((err, c) => { + console.error("Error during operation:", err); + return c.json( + { + error: "Operation failed", + message: err instanceof Error ? err.message : String(err), + }, + 500, + ); +}); + +app.get("/", (c) => { + return c.text("Multitenant Deploy Example"); +}); + +app.post("/deploy/:appId", async (c) => { + const appId = c.req.param("appId"); + + // Get the form data + const formData = await c.req.formData(); + + if (!appId || typeof appId !== "string") { + return c.json({ error: "Missing or invalid appId" }, 400); + } + + // Validate app ID (alphanumeric and hyphens only, 3-30 chars) + if (!/^[a-z0-9-]{3,30}$/.test(appId)) { + return c.json( + { + error: "Invalid appId format. Must be 3-30 characters, lowercase alphanumeric with hyphens.", + }, + 400, + ); + } + + // Create a temp directory for the files + const tempDir = await temp.mkdir("rivet-deploy-"); + const tempDirProject = path.join(tempDir, "project"); + + // Process and save each file + let hasDockerfile = false; + for (const [fieldName, value] of formData.entries()) { + // Skip non-file fields + if (!(value instanceof File)) continue; + + const filePath = path.join(tempDirProject, fieldName); + + await fs.mkdir(path.dirname(filePath), { recursive: true }); + + await fs.writeFile(filePath, Buffer.from(await value.arrayBuffer())); + + if (fieldName === "Dockerfile") { + hasDockerfile = true; + } + } + + if (!hasDockerfile) { + return c.json({ error: "Dockerfile is required" }, 400); + } + + // Tags unique to this app's functions + const appTags = { + // Specifies that this app is deployed by a user + type: "user-app", + // Specifies which app this function belongs to + // + // Used for attributing billing & more + app: appId, + }; + + // Write Rivet config + const functionName = `fn-${appId}`; + const rivetConfig = { + functions: { + [functionName]: { + build_path: "./project/", + dockerfile: "./project/Dockerfile", + build_args: { + // See MY_ENV_VAR build args in Dockerfile + MY_ENV_VAR: "custom env var", + APP_ID: appId, + }, + tags: appTags, + route_subpaths: true, + strip_prefix: true, + resources: { cpu: 125, memory: 128 }, + // If you want to host at a subpath: + // path: "/foobar" + }, + }, + }; + await fs.writeFile( + path.join(tempDir, "rivet.json"), + JSON.stringify(rivetConfig), + ); + + // Run rivet publish command + console.log(`Deploying app ${appId} from ${tempDir}...`); + + // Run the deploy command + const deployResult = await execAsync( + `rivet deploy --environment ${RIVET_ENVIRONMENT} --non-interactive`, + { + cwd: tempDir, + }, + ); + + console.log("Publish output:", deployResult.stdout); + + // Get the function endpoint + const endpointResult = await execAsync( + `rivet function endpoint --environment prod ${functionName}`, + { + cwd: tempDir, + }, + ); + + // Strip any extra text and just get the URL + const endpointUrl = endpointResult.stdout.trim(); + console.log("Function endpoint:", endpointUrl); + + return c.json({ + success: true, + appId, + endpoint: endpointUrl, + }); +}); diff --git a/examples/multitenant-deploys/src/index.ts b/examples/multitenant-deploys/src/index.ts new file mode 100644 index 0000000000..85bd3eb658 --- /dev/null +++ b/examples/multitenant-deploys/src/index.ts @@ -0,0 +1,10 @@ +import { serve } from "@hono/node-server"; +import { app } from "./app"; + +const PORT = process.env.PORT || 3000; +console.log(`Server starting on port ${PORT}...`); +serve({ + fetch: app.fetch, + port: Number(PORT), +}); + diff --git a/examples/multitenant-deploys/tests/deploy.test.ts b/examples/multitenant-deploys/tests/deploy.test.ts new file mode 100644 index 0000000000..6c51868794 --- /dev/null +++ b/examples/multitenant-deploys/tests/deploy.test.ts @@ -0,0 +1,90 @@ +import { describe, it, expect } from "vitest"; +import { app } from "../src/app"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import * as os from "node:os"; + +describe("Deploy Endpoint", () => { + it("should deploy an application and return endpoint", async () => { + // Create a temporary Dockerfile for testing + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "rivet-test-")); + const dockerfilePath = path.join(tempDir, "Dockerfile"); + + await fs.writeFile( + dockerfilePath, + ` +FROM node:22-alpine +WORKDIR /app +COPY . . + +# Set env var from build arg +ARG MY_ENV_VAR +ENV MY_ENV_VAR=$MY_ENV_VAR +ARG APP_ID +ENV APP_ID=$APP_ID + +# Create a non-root user +RUN addgroup -S rivetgroup && adduser -S rivet -G rivetgroup +USER rivet + +CMD ["node", "server.js"] + `, + ); + + // Create a simple server.js file + const serverPath = path.join(tempDir, "server.js"); + await fs.writeFile( + serverPath, + ` +const http = require("http"); +const server = http.createServer((req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end(\`Hello \${process.env.MY_ENV_VAR} from \${process.env.APP_ID}!\`); +}); +// Port defaults to 8080 +console.log("Server started"); +server.listen(8080); + `, + ); + + // Create a FormData instance + const formData = new FormData(); + + // Generate a unique app ID for testing + const testAppId = `test-${Math.floor(Math.random() * 10_000)}`; + + // Add the Dockerfile to the form data + const dockerfileContent = await fs.readFile(dockerfilePath); + const dockerfileBlob = new Blob([dockerfileContent], { + type: "application/octet-stream", + }); + formData.append("Dockerfile", dockerfileBlob, "Dockerfile"); + + // Add server.js to the form data + const serverContent = await fs.readFile(serverPath); + const serverBlob = new Blob([serverContent], { + type: "application/octet-stream", + }); + formData.append("server.js", serverBlob, "server.js"); + + // Make the request to the deploy endpoint + const res = await app.request( + `/deploy/${encodeURIComponent(testAppId)}`, + { + method: "POST", + body: formData, + }, + ); + + // Verify the response + expect(res.status).toBe(200); + + const responseData = await res.json(); + expect(responseData.success).toBe(true); + expect(responseData.appId).toBe(testAppId); + expect(responseData.endpoint).toBeDefined(); + + // Clean up the temporary directory + await fs.rm(tempDir, { recursive: true, force: true }); + }, 120000); // 2 minute timeout for this test +}); diff --git a/examples/multitenant-deploys/tsconfig.json b/examples/multitenant-deploys/tsconfig.json new file mode 100644 index 0000000000..b59c2086ec --- /dev/null +++ b/examples/multitenant-deploys/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src/**/*"] +} \ No newline at end of file diff --git a/examples/system-test-route/Dockerfile b/examples/system-test-route/Dockerfile index affb28ca4f..90e753ad64 100644 --- a/examples/system-test-route/Dockerfile +++ b/examples/system-test-route/Dockerfile @@ -23,4 +23,4 @@ RUN chown -R rivet:rivet /app/dist USER rivet # Start the server -CMD ["node", "dist/src/container/main.js"] +CMD ["node", "dist/src/index.js"] diff --git a/examples/system-test-route/package.json b/examples/system-test-route/package.json index 1c64c694f0..b5f8edb322 100644 --- a/examples/system-test-route/package.json +++ b/examples/system-test-route/package.json @@ -10,7 +10,6 @@ }, "devDependencies": { "@rivet-gg/actor-core": "^5.1.2", - "@rivet-gg/api-full": "workspace:*", "@types/deno": "^2.2.0", "@types/node": "^22.13.9", "@types/ws": "^8.18.0", diff --git a/examples/system-test-route/rivet.jsonc b/examples/system-test-route/rivet.jsonc index 83b3ab8506..442952e272 100644 --- a/examples/system-test-route/rivet.jsonc +++ b/examples/system-test-route/rivet.jsonc @@ -1,10 +1,6 @@ { "functions": { - "http-isolate": { - "script": "src/isolate/main.ts", - "path": "/isolate" - }, - "http-container": { + "http": { "dockerfile": "Dockerfile", "path": "/container" } diff --git a/examples/system-test-route/src/container/main.ts b/examples/system-test-route/src/container/main.ts deleted file mode 100644 index b4059469de..0000000000 --- a/examples/system-test-route/src/container/main.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { serve } from "@hono/node-server"; -import { createNodeWebSocket } from "@hono/node-ws"; -import { createAndStartServer } from "../shared/server.js"; - -let injectWebSocket: any; -const { app, port } = createAndStartServer((app) => { - // Get Node.js WebSocket handler - const result = createNodeWebSocket({ app }); - injectWebSocket = result.injectWebSocket; - return result.upgradeWebSocket; -}); - -const server = serve({ fetch: app.fetch, port }); -injectWebSocket(server); diff --git a/examples/system-test-route/src/index.ts b/examples/system-test-route/src/index.ts new file mode 100644 index 0000000000..f2dd821ae4 --- /dev/null +++ b/examples/system-test-route/src/index.ts @@ -0,0 +1,50 @@ +import { serve } from "@hono/node-server"; +import { Hono } from "hono"; + +// Setup auto-exit timer +setTimeout(() => { + console.error( + "Actor should've been destroyed by now. Automatically exiting.", + ); + + if (typeof Deno !== "undefined") Deno.exit(1); + else process.exit(1); +}, 60 * 1000); + +let tickIndex = 0; +setInterval(() => { + tickIndex++; + console.log("Tick", tickIndex); + console.log(JSON.stringify({ level: "info", message: "tick", tickIndex })); + console.log(`level=info message=tick tickIndex=${tickIndex}`); +}, 1000); + +// Get port from environment +const portEnv = + typeof Deno !== "undefined" + ? Deno.env.get("PORT_HTTP") + : process.env.PORT_HTTP; +if (!portEnv) { + throw new Error("missing PORT_HTTP"); +} +const port = Number.parseInt(portEnv); + +// Create app with health endpoint +const app = new Hono(); + +app.get("/health", (c) => c.text("ok")); + +// Add a catch-all route to handle any other path (for testing routeSubpaths) +app.all("*", (c) => { + console.log( + `Received request to ${c.req.url} from ${c.req.header("x-forwarded-for") || "unknown"}`, + ); + return c.json({ + path: c.req.path, + query: c.req.query(), + }); +}); + +console.log(`Listening on port ${port}`); + +serve({ fetch: app.fetch, port }); diff --git a/examples/system-test-route/src/isolate/main.ts b/examples/system-test-route/src/isolate/main.ts deleted file mode 100644 index 5af813e0e0..0000000000 --- a/examples/system-test-route/src/isolate/main.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { ActorContext } from "@rivet-gg/actor-core"; -import { upgradeWebSocket } from "hono/deno"; -import { createAndStartServer } from "../shared/server.js"; - -// Start server -export default { - async start(ctx: ActorContext) { - console.log("Isolate starting"); - - // Create and start server with Deno WebSocket upgrader - console.log("Starting HTTP server"); - const { app, port } = createAndStartServer(() => upgradeWebSocket, ctx.metadata.actor.id); - - const server = Deno.serve( - { - port, - }, - app.fetch, - ); - await server.finished; - }, -}; diff --git a/examples/system-test-route/src/shared/server.ts b/examples/system-test-route/src/shared/server.ts deleted file mode 100644 index 20227fc4ec..0000000000 --- a/examples/system-test-route/src/shared/server.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { type Context, Hono } from "hono"; -import type { UpgradeWebSocket } from "hono/ws"; - -type GetUpgradeWebSocketFn = (app: Hono) => UpgradeWebSocket; - -export function createAndStartServer( - getUpgradeWebSocket: GetUpgradeWebSocketFn, - actorId: string, -): { app: Hono; port: number } { - // Setup auto-exit timer - setTimeout(() => { - console.error( - "Actor should've been destroyed by now. Automatically exiting.", - ); - - if (typeof Deno !== "undefined") Deno.exit(1); - else process.exit(1); - }, 60 * 1000); - - let tickIndex = 0; - setInterval(() => { - tickIndex++; - console.log("Tick", tickIndex); - console.log( - JSON.stringify({ level: "info", message: "tick", tickIndex }), - ); - console.log(`level=info message=tick tickIndex=${tickIndex}`); - }, 1000); - - // Get port from environment - const portEnv = - typeof Deno !== "undefined" - ? Deno.env.get("PORT_HTTP") - : process.env.PORT_HTTP; - if (!portEnv) { - throw new Error("missing PORT_HTTP"); - } - const port = Number.parseInt(portEnv); - - // Create app with health endpoint - const app = new Hono(); - - app.get("/health", (c) => c.text("ok")); - - // Add a catch-all route to handle any other path (for testing routeSubpaths) - app.all("*", (c) => { - console.log( - `Received request to ${c.req.url} from ${c.req.header("x-forwarded-for") || "unknown"}`, - ); - return c.json({ - actorId, - path: c.req.path, - query: c.req.query(), - }); - }); - - console.log(`Listening on port ${port}, Actor ID: ${actorId}`); - - return { app, port }; -} diff --git a/examples/system-test-route/tests/client.ts b/examples/system-test-route/tests/client.ts index 3691af2a3a..be5ce46a1b 100644 --- a/examples/system-test-route/tests/client.ts +++ b/examples/system-test-route/tests/client.ts @@ -1,1709 +1,1709 @@ -import { RivetClient } from "@rivet-gg/api-full"; -import crypto from "crypto"; -import http from "http"; - -// Can be opt since they're not required for dev -const RIVET_ENDPOINT = process.env.RIVET_ENDPOINT; -const RIVET_SERVICE_TOKEN = process.env.RIVET_SERVICE_TOKEN; -const RIVET_PROJECT = process.env.RIVET_PROJECT; -const RIVET_ENVIRONMENT = process.env.RIVET_ENVIRONMENT; - -// Determine test kind from environment variable -const BUILD_NAME = process.env.BUILD; -if (BUILD_NAME !== "http-isolate" && BUILD_NAME !== "http-container") { - throw new Error( - "Must specify BUILD environment variable as either 'http-isolate' or 'http-container'", - ); -} - -let region = process.env.REGION; -if (!region || region.length === 0) { - region = undefined; -} - -const client = new RivetClient({ - environment: RIVET_ENDPOINT, - token: RIVET_SERVICE_TOKEN, -}); - -// Interface definitions -interface RouteConfig { - path: string; - routeSubpaths: boolean; - stripPrefix?: boolean; // Whether to strip the prefix from the path in request handlers - selectorIndex?: number; // Index of the selector to use from selectors array -} - -interface TestContext { - actorIds: string[]; - routeIds: string[]; - selectors: string[]; // Array of selectors - hostname: string; - actorsBySelector: Record; // Maps selectors to their actor IDs - routes?: RouteConfig[]; // Store the routes for this test -} - -interface TestConfig { - name: string; - numActors?: number; - numSelectors?: number; // Number of different selectors to create - routes?: RouteConfig[]; // Each route can specify which selector to use -} - -// Helper function to make HTTP requests with a custom host header -async function makeRequest(url: string, hostname: string): Promise { - return new Promise((resolve, reject) => { - const parsedUrl = new URL(url); - - const options = { - hostname: parsedUrl.hostname, - port: parsedUrl.port || 80, - path: parsedUrl.pathname + parsedUrl.search, - method: "GET", - headers: { - Accept: "application/json", - Host: hostname, - }, - }; - - const req = http.request(options, (res: any) => { - if (res.statusCode !== 200) { - console.error( - `Request failed: ${res.statusCode} ${res.statusMessage}`, - ); - // Don't reject, just continue the loop - resolve(null); - return; - } - - let rawData = ""; - res.on("data", (chunk: any) => { - rawData += chunk; - }); - res.on("end", () => { - try { - const parsedData = JSON.parse(rawData); - resolve(parsedData); - } catch (e) { - console.error("Error parsing response:", e); - reject(e); - } - }); - }); - - req.on("error", (e: any) => { - console.error(`Request error: ${e.message}`); - reject(e); - }); - - req.end(); - }); -} - -// Helper function to create actors with a specific selector -async function createActors( - selectorTag: string, - numberOfActors: number = 2, -): Promise { - const createdActorIds: string[] = []; - - for (let i = 1; i <= numberOfActors; i++) { - console.time(`create actor ${i}`); - console.log(`Creating actor ${i} with tag`, { - selector: selectorTag, - }); - - const { actor } = await client.actors.create({ - project: RIVET_PROJECT, - environment: RIVET_ENVIRONMENT, - body: { - region, - tags: { - selector: selectorTag, - instance: i.toString(), - }, - buildTags: { name: BUILD_NAME, current: "true" }, - network: { - ports: { - http: { - protocol: "https", - routing: { - guard: {}, - }, - }, - }, - }, - lifecycle: { - durable: false, - }, - ...(BUILD_NAME === "http-container" - ? { - resources: { - cpu: 100, - memory: 100, - }, - } - : {}), - }, - }); - - createdActorIds.push(actor.id); - console.timeEnd(`create actor ${i}`); - console.log(`Created actor ${i} with ID:`, actor.id); - } - - // Wait for actors to be ready - await new Promise((resolve) => setTimeout(resolve, 2000)); - return createdActorIds; -} - -// Helper function to create a route -async function createRoute( - routeId: string, - hostname: string, - selectorTag: string, - path: string, - routeSubpaths: boolean = false, - stripPrefix: boolean = true, -): Promise { - console.time(`create route ${routeId}`); - console.log(`Creating route ${routeId} with selector tag`, { - selector: selectorTag, - path, - routeSubpaths, - stripPrefix, - }); - - await client.routes.update(routeId, { - project: RIVET_PROJECT, - environment: RIVET_ENVIRONMENT, - body: { - hostname, - path, - routeSubpaths, - stripPrefix, - target: { - actors: { - selectorTags: { - selector: selectorTag, - }, - }, - }, - }, - }); - - console.timeEnd(`create route ${routeId}`); - console.log(`Created route ${routeId}.`); - - // Wait for route to be active - await new Promise((resolve) => setTimeout(resolve, 2000)); -} - -// Helper function to calculate expected path based on route configuration -function getExpectedPath( - requestPath: string, - routePath: string, - stripPrefix: boolean, -): string { - // Extract the query string if present - const queryStringIndex = requestPath.indexOf("?"); - const pathWithoutQuery = - queryStringIndex >= 0 - ? requestPath.substring(0, queryStringIndex) - : requestPath; - - // If stripPrefix is false, the full path should be returned - if (!stripPrefix) { - return pathWithoutQuery; - } - - // If stripPrefix is true, we need to strip the route path prefix - if (routePath === "") { - // For empty path routes, return the path as is - return pathWithoutQuery; - } - - // For non-empty paths with stripPrefix=true - if (pathWithoutQuery === routePath) { - // If exact match, return "/" - return "/"; - } else if (pathWithoutQuery.startsWith(routePath + "/")) { - // If it's a subpath, remove the prefix - return pathWithoutQuery.substring(routePath.length); - } - - // Default case - shouldn't happen with proper routing - return pathWithoutQuery; -} - -// Helper function to test a route -async function testRoute( - hostname: string, - path: string, - numActorsExpected: number = 2, - maxRequests: number = 20, - route?: RouteConfig, // Added route config parameter -): Promise> { - const actorIds = new Set(); - let successfulMatches = 0; - let totalRequests = 0; - let pathValidationFailed = false; - - // Using localhost with Host header for local testing - const testUrl = `http://localhost:7080${path}`; - console.log(`Testing route at: ${testUrl} (with Host: ${hostname})`); - console.time(`route-test-${path}`); - - // Calculate expected path if route is provided - let expectedPath = route - ? getExpectedPath( - path, - route.path, - route.stripPrefix !== undefined ? route.stripPrefix : true, - ) - : path; - - // URL-encoded characters like %20 will be decoded by the server - // Decode the expected path to match server behavior - expectedPath = decodeURIComponent(expectedPath); - - if (route) { - console.log( - `Route config: path=${route.path}, routeSubpaths=${route.routeSubpaths}, stripPrefix=${route.stripPrefix}`, - ); - console.log(`Expected path in response: ${expectedPath}`); - } - - while (actorIds.size < numActorsExpected && totalRequests < maxRequests) { - totalRequests++; - - try { - const data = await makeRequest(testUrl, hostname); - - // If request failed or returned null, continue to next iteration - if (!data) { - continue; - } - - console.log( - `Request ${totalRequests}: Response from actor ${data.actorId} with path ${data.path}`, - ); - - // Validate the path in the response matches the expected path - if (data.path !== expectedPath) { - console.error( - `❌ Path validation failed: Expected ${expectedPath}, got ${data.path}`, - ); - pathValidationFailed = true; - } else { - console.log(`✅ Path validation passed: ${data.path}`); - } - - // Log query parameters if present - if (data.query && Object.keys(data.query).length > 0) { - console.log(`Query parameters received:`, data.query); - } - - // Track the actor IDs we've seen - if (data.actorId) { - actorIds.add(data.actorId); - successfulMatches++; - } - - // If we've found all expected actors, we're done - if (actorIds.size === numActorsExpected) { - console.log( - `Successfully received responses from all ${numActorsExpected} actors!`, - ); - break; - } - - // Small delay between requests - await new Promise((resolve) => setTimeout(resolve, 200)); - } catch (error) { - console.error("Error making request:", error); - // Wait a bit longer if there's an error - await new Promise((resolve) => setTimeout(resolve, 500)); - } - } - - console.timeEnd(`route-test-${path}`); - console.log( - `Test completed. Matched ${actorIds.size}/${numActorsExpected} actors in ${totalRequests} requests.`, - ); - console.log(`Actors matched: ${Array.from(actorIds).join(", ")}`); - - if (actorIds.size < numActorsExpected) { - console.error( - `Failed to reach all ${numActorsExpected} actors through the route!`, - ); - } - - if (pathValidationFailed) { - console.error( - "Path validation failed: The path in the response did not match the expected path", - ); - throw new Error(`Path validation failed for ${path}`); - } - - // Final stats - console.log(` -Route Test Results for ${path}: ------------------- -Total requests: ${totalRequests} -Successful responses: ${successfulMatches} -Unique actors reached: ${actorIds.size}/${numActorsExpected} -Route: ${testUrl} (Host: ${hostname}) -Path validation: ${pathValidationFailed ? "❌ Failed" : "✅ Passed"} ------------------- - `); - - return actorIds; -} - -// Helper function to verify routes exist -async function verifyRouteExists(hostname: string): Promise { - console.time("list routes"); - console.log("Listing routes to verify our route exists"); - const { routes } = await client.routes.list({ - project: RIVET_PROJECT, - environment: RIVET_ENVIRONMENT, - }); - console.timeEnd("list routes"); - - // Find our route in the list - const ourRoute = routes.find((route) => route.hostname === hostname); - if (!ourRoute) { - console.error( - `Route with hostname ${hostname} not found in routes list!`, - ); - return false; - } - console.log("✅ Found our route in the list:", ourRoute); - return true; -} - -// Helper function to delete resources -async function cleanup(context: TestContext): Promise { - // Cleanup: delete routes first - for (const routeId of context.routeIds) { - console.log("Deleting route", routeId); - try { - await client.routes.delete(routeId, { - project: RIVET_PROJECT, - environment: RIVET_ENVIRONMENT, - }); - console.log(`Route ${routeId} deleted successfully`); - } catch (err) { - console.error(`Error deleting route ${routeId}:`, err); - } - } - - // Then delete all actors - for (let i = 0; i < context.actorIds.length; i++) { - const actorId = context.actorIds[i]; - console.log(`Destroying actor ${i + 1}:`, actorId); - try { - await client.actors.destroy(actorId, { - project: RIVET_PROJECT, - environment: RIVET_ENVIRONMENT, - }); - } catch (err) { - console.error(`Error destroying actor ${i + 1}:`, err); - } - } -} - -// Core test setup function that handles resource creation and cleanup -async function setupTest( - config: TestConfig, - testFn: (context: TestContext) => Promise, -): Promise { - console.log(`\n=== ${config.name} ===\n`); - - const baseSelector = `test-${crypto.randomBytes(4).toString("hex")}`; - const hostname = `route-${crypto.randomBytes(4).toString("hex")}.rivet-job.local`; - - const context: TestContext = { - actorIds: [], - routeIds: [], - selectors: [], - hostname, - actorsBySelector: {}, - routes: config.routes, - }; - - try { - // Create selectors based on config - const numSelectors = config.numSelectors || 1; - - // Create selectors and actors for each selector - for (let i = 0; i < numSelectors; i++) { - const selectorName = - numSelectors === 1 ? baseSelector : `${baseSelector}-${i + 1}`; - context.selectors.push(selectorName); - - console.log( - `Creating actors with selector ${selectorName} (${i + 1}/${numSelectors})`, - ); - const actors = await createActors( - selectorName, - config.numActors || 2, - ); - context.actorIds.push(...actors); - context.actorsBySelector[selectorName] = actors; - } - - // Create routes from config - if (config.routes && config.routes.length > 0) { - for (let i = 0; i < config.routes.length; i++) { - const route = config.routes[i]; - const routeId = `route-${crypto.randomBytes(4).toString("hex")}${i > 0 ? `-${i}` : ""}`; - - // Determine which selector to use for this route - const selectorIndex = - route.selectorIndex !== undefined ? route.selectorIndex : 0; - if (selectorIndex >= context.selectors.length) { - throw new Error( - `Route ${i} references selector ${selectorIndex} but only ${context.selectors.length} selectors were created`, - ); - } - - const selector = context.selectors[selectorIndex]; - - console.log( - `Creating route ${routeId} with path ${route.path} using selector ${selector} (index ${selectorIndex})`, - ); - await createRoute( - routeId, - context.hostname, - selector, - route.path, - route.routeSubpaths, - route.stripPrefix !== undefined ? route.stripPrefix : true, - ); - context.routeIds.push(routeId); - } - } - - // Verify routes exist - await verifyRouteExists(context.hostname); - - // Run the test function - await testFn(context); - - // If we get here, the test passed - console.log(`✅ Test "${config.name}" passed successfully`); - return true; - } catch (error) { - console.error(`❌ Error in ${config.name}:`, error); - return false; - } finally { - // Clean up all resources - await cleanup(context); - } -} - -// Test implementations -async function testBasicRoute(): Promise { - return await setupTest( - { - name: "Basic Route Test", - numSelectors: 1, - routes: [ - { - path: "/test", - routeSubpaths: false, - stripPrefix: true, - selectorIndex: 0, - }, - ], - }, - async (context) => { - // Get the actor IDs for the first selector - const selectorActors = - context.actorsBySelector[context.selectors[0]]; - const route = context.routes?.[0]; - - // Test the route - const result = await testRoute( - context.hostname, - "/test", - 2, - 20, - route, - ); - if (result.size < 2) { - throw new Error( - "Basic route test failed: Could not reach all expected actors", - ); - } - - // Verify we got responses from the correct actors - let matchedActors = 0; - for (const id of result) { - if (selectorActors.includes(id)) { - matchedActors++; - } - } - - if (matchedActors === result.size) { - console.log("✅ All requests routed to the correct actors"); - } else { - console.log( - `❌ Expected all requests to route to the correct actors, but only ${matchedActors}/${result.size} did`, - ); - throw new Error( - `Basic route test failed: ${matchedActors}/${result.size} requests routed to the correct actors`, - ); - } - }, - ); -} - -async function testPathPrefix(): Promise { - return await setupTest( - { - name: "Path Prefix Test", - numSelectors: 1, - routes: [ - { - path: "/api", - routeSubpaths: true, - stripPrefix: true, - selectorIndex: 0, - }, - ], - }, - async (context) => { - // Get the actor IDs for the first selector - const selectorActors = - context.actorsBySelector[context.selectors[0]]; - const route = context.routes?.[0]; - - // Test various paths that should match the prefix - console.log("Testing paths that should match the prefix /api"); - let result = await testRoute( - context.hostname, - "/api", - 2, - 20, - route, - ); - if (result.size < 2) { - throw new Error( - "Path prefix test failed: Could not reach actors at /api", - ); - } - - result = await testRoute(context.hostname, "/api/v1", 2, 20, route); - if (result.size < 2) { - throw new Error( - "Path prefix test failed: Could not reach actors at /api/v1", - ); - } - - result = await testRoute( - context.hostname, - "/api/users", - 2, - 20, - route, - ); - if (result.size < 2) { - throw new Error( - "Path prefix test failed: Could not reach actors at /api/users", - ); - } - - // Test a path that shouldn't match - console.log("Testing path that shouldn't match the prefix /api"); - const nonMatchingResult = await testRoute( - context.hostname, - "/other", - 2, - 5, - ); - - if (nonMatchingResult.size > 0) { - console.log( - "❌ Found match for non-matching path /other when it should have failed", - ); - throw new Error( - "Path prefix test failed: Found match for non-matching path /other", - ); - } else { - console.log( - "✅ Correctly found no matches for non-matching path /other", - ); - } - }, - ); -} - -async function testExactPath(): Promise { - return await setupTest( - { - name: "Exact Path Test", - numSelectors: 1, - routes: [ - { - path: "/exact", - routeSubpaths: false, - stripPrefix: true, - selectorIndex: 0, - }, - ], - }, - async (context) => { - // Get the actor IDs for the selector - const selectorActors = - context.actorsBySelector[context.selectors[0]]; - const route = context.routes?.[0]; - - // Test the exact path - console.log("Testing exact path /exact"); - const result = await testRoute( - context.hostname, - "/exact", - 2, - 20, - route, - ); - if (result.size < 2) { - throw new Error( - "Exact path test failed: Could not reach actors at exact path /exact", - ); - } - - // Test paths that shouldn't match - console.log( - "Testing paths that shouldn't match the exact path /exact", - ); - const subPathResult = await testRoute( - context.hostname, - "/exact/subpath", - 2, - 5, - ); - - if (subPathResult.size > 0) { - console.log( - "❌ Found match for /exact/subpath when it should have failed", - ); - throw new Error( - "Exact path test failed: Found match for subpath /exact/subpath", - ); - } else { - console.log("✅ Correctly found no matches for /exact/subpath"); - } - - const differentPathResult = await testRoute( - context.hostname, - "/different", - 2, - 5, - ); - - if (differentPathResult.size > 0) { - console.log( - "❌ Found match for /different when it should have failed", - ); - throw new Error( - "Exact path test failed: Found match for different path /different", - ); - } else { - console.log("✅ Correctly found no matches for /different"); - } - }, - ); -} - -async function testPathPriority(): Promise { - return await setupTest( - { - name: "Path Priority Test", - numSelectors: 2, - routes: [ - { - path: "/api", - routeSubpaths: true, - stripPrefix: true, - selectorIndex: 0, - }, // Less specific path - { - path: "/api/users", - routeSubpaths: true, - stripPrefix: true, - selectorIndex: 1, - }, // More specific path (higher priority) - ], - }, - async (context) => { - // Get the selectors and actor groups - const apiSelector = context.selectors[0]; - const usersSelector = context.selectors[1]; - - const apiActors = context.actorsBySelector[apiSelector]; // API actors (lower priority) - const usersActors = context.actorsBySelector[usersSelector]; // Users actors (higher priority) - - const apiRoute = context.routes?.[0]; - const usersRoute = context.routes?.[1]; - - console.log( - "API selector:", - apiSelector, - "with actors:", - apiActors, - ); - console.log( - "Users selector:", - usersSelector, - "with actors:", - usersActors, - ); - - // 1. First verify we can access both sets of actors directly with their exact paths - console.log( - "\nStep 1: Verifying direct access to both actor groups", - ); - - // 1.1 Test the less specific path (/api) - should route to first set of actors - console.log("Testing /api path - should route to API actors"); - const apiResult = await testRoute( - context.hostname, - "/api", - 2, - 10, - apiRoute, - ); - if (apiResult.size < 2) { - throw new Error( - "Path priority test failed: Could not reach actors at /api", - ); - } - - // Check we got responses from the API actors - let matchedApiActors = 0; - for (const id of apiResult) { - if (apiActors.includes(id)) { - matchedApiActors++; - } - } - - if (matchedApiActors === apiResult.size) { - console.log( - "✅ All requests to /api routed to API actors as expected", - ); - } else { - console.log( - `❌ Expected all requests to /api to route to API actors, but only ${matchedApiActors}/${apiResult.size} did`, - ); - throw new Error( - `Path priority test failed: ${matchedApiActors}/${apiResult.size} requests to /api routed to API actors`, - ); - } - - // 1.2 Test the more specific path (/api/users) - should route to users actors - console.log( - "\nTesting /api/users path - should route to Users actors", - ); - const usersResult = await testRoute( - context.hostname, - "/api/users", - 2, - 10, - usersRoute, - ); - if (usersResult.size < 2) { - throw new Error( - "Path priority test failed: Could not reach actors at /api/users", - ); - } - - // Check we got responses from the Users actors - let matchedUsersActors = 0; - for (const id of usersResult) { - if (usersActors.includes(id)) { - matchedUsersActors++; - } - } - - if (matchedUsersActors === usersResult.size) { - console.log( - "✅ All requests to /api/users routed to Users actors as expected", - ); - } else { - console.log( - `❌ Expected all requests to /api/users to route to Users actors, but only ${matchedUsersActors}/${usersResult.size} did`, - ); - throw new Error( - `Path priority test failed: ${matchedUsersActors}/${usersResult.size} requests to /api/users routed to Users actors`, - ); - } - - // 2. Test a path that would match the /api prefix but is a subpath of /api/users - // Should go to the /api/users actors as the more specific path has higher priority - console.log("\nStep 2: Testing a subpath priority"); - console.log( - "Testing /api/users/123 path - should route to Users actors (more specific path)", - ); - const subpathResult = await testRoute( - context.hostname, - "/api/users/123", - 2, - 10, - usersRoute, - ); - if (subpathResult.size < 2) { - throw new Error( - "Path priority test failed: Could not reach actors at /api/users/123", - ); - } - - // Check if we got responses from the expected actors (should be from Users actors path) - let matchedSubpathUsers = 0; - for (const id of subpathResult) { - if (usersActors.includes(id)) { - matchedSubpathUsers++; - } - } - - if (matchedSubpathUsers === subpathResult.size) { - console.log( - "✅ All requests for /api/users/123 routed to Users actors (more specific path) as expected", - ); - } else { - console.log( - `❌ Expected all requests to route to Users actors (more specific path), but only ${matchedSubpathUsers}/${subpathResult.size} did`, - ); - throw new Error( - `Path priority test failed for subpath: ${matchedSubpathUsers}/${subpathResult.size} requests routed to the Users actors`, - ); - } - - // 3. Test a path that matches the /api prefix but is NOT a subpath of /api/users - // Should go to the /api actors - console.log("\nStep 3: Testing another /api subpath"); - console.log( - "Testing /api/other path - should route to API actors (because it's not under /api/users)", - ); - const otherResult = await testRoute( - context.hostname, - "/api/other", - 2, - 10, - apiRoute, - ); - if (otherResult.size < 2) { - throw new Error( - "Path priority test failed: Could not reach actors at /api/other", - ); - } - - // Check if we got responses from the API actors - let matchedOtherApi = 0; - for (const id of otherResult) { - if (apiActors.includes(id)) { - matchedOtherApi++; - } - } - - if (matchedOtherApi === otherResult.size) { - console.log( - "✅ All requests for /api/other routed to API actors as expected", - ); - } else { - console.log( - `❌ Expected all requests to route to API actors, but only ${matchedOtherApi}/${otherResult.size} did`, - ); - throw new Error( - `Path priority test failed for other subpath: ${matchedOtherApi}/${otherResult.size} requests routed to the API actors`, - ); - } - }, - ); -} - -async function testEmptyPath(): Promise { - return await setupTest( - { - name: "Empty Path Test", - numSelectors: 1, - routes: [ - { - path: "", - routeSubpaths: true, - stripPrefix: true, - selectorIndex: 0, - }, - ], - }, - async (context) => { - // Get the actor IDs for the first selector - const selectorActors = - context.actorsBySelector[context.selectors[0]]; - const route = context.routes?.[0]; - - // Test various paths that should all match due to empty path with routeSubpaths=true - console.log("Testing empty path which should match any path"); - - let result = await testRoute(context.hostname, "/", 2, 20, route); - if (result.size < 2) { - throw new Error( - "Empty path test failed: Could not reach actors at /", - ); - } - - result = await testRoute(context.hostname, "/api", 2, 20, route); - if (result.size < 2) { - throw new Error( - "Empty path test failed: Could not reach actors at /api", - ); - } - - result = await testRoute(context.hostname, "/users", 2, 20, route); - if (result.size < 2) { - throw new Error( - "Empty path test failed: Could not reach actors at /users", - ); - } - - result = await testRoute( - context.hostname, - "/deep/nested/path", - 2, - 20, - route, - ); - if (result.size < 2) { - throw new Error( - "Empty path test failed: Could not reach actors at /deep/nested/path", - ); - } - }, - ); -} - -async function testNoStripPrefix(): Promise { - return await setupTest( - { - name: "No Strip Prefix Test", - numSelectors: 1, - routes: [ - { - path: "/prefix", - routeSubpaths: true, - stripPrefix: false, - selectorIndex: 0, - }, - ], - }, - async (context) => { - // Get the actor IDs for the first selector - const selectorActors = - context.actorsBySelector[context.selectors[0]]; - const route = context.routes?.[0]; - - // Test the exact path - the path in response should be the full path - console.log( - "Testing exact path with stripPrefix=false. Path should NOT be stripped.", - ); - const result = await testRoute( - context.hostname, - "/prefix", - 2, - 20, - route, - ); - if (result.size < 2) { - throw new Error( - "No strip prefix test failed: Could not reach actors at /prefix", - ); - } - - // Test a subpath - the path in response should be the full path again - console.log( - "Testing subpath with stripPrefix=false. Path should NOT be stripped.", - ); - const subpathResult = await testRoute( - context.hostname, - "/prefix/subpath", - 2, - 20, - route, - ); - if (subpathResult.size < 2) { - throw new Error( - "No strip prefix test failed: Could not reach actors at /prefix/subpath", - ); - } - }, - ); -} - -async function testMultipleRoutes(): Promise { - return await setupTest( - { - name: "Multiple Routes Test", - numSelectors: 2, - routes: [ - { - path: "/route1", - routeSubpaths: false, - stripPrefix: true, - selectorIndex: 0, - }, - { - path: "/route2", - routeSubpaths: false, - stripPrefix: true, - selectorIndex: 1, - }, - ], - }, - async (context) => { - // Get the actor IDs for both selectors - const selector1Actors = - context.actorsBySelector[context.selectors[0]]; - const selector2Actors = - context.actorsBySelector[context.selectors[1]]; - - const route1 = context.routes?.[0]; - const route2 = context.routes?.[1]; - - // Test first route - console.log("Testing first route /route1"); - const result1 = await testRoute( - context.hostname, - "/route1", - 2, - 20, - route1, - ); - if (result1.size < 2) { - throw new Error( - "Multiple routes test failed: Could not reach actors at /route1", - ); - } - - // Verify we got responses from the correct actors - let matchedActors1 = 0; - for (const id of result1) { - if (selector1Actors.includes(id)) { - matchedActors1++; - } - } - - if (matchedActors1 === result1.size) { - console.log( - "✅ All requests to /route1 routed to route1 actors as expected", - ); - } else { - console.log( - `❌ Expected all requests to route to route1 actors, but only ${matchedActors1}/${result1.size} did`, - ); - throw new Error( - `Multiple routes test failed: ${matchedActors1}/${result1.size} requests to /route1 routed to route1 actors`, - ); - } - - // Test second route - console.log("Testing second route /route2"); - const result2 = await testRoute( - context.hostname, - "/route2", - 2, - 20, - route2, - ); - if (result2.size < 2) { - throw new Error( - "Multiple routes test failed: Could not reach actors at /route2", - ); - } - - // Verify we got responses from the correct actors - let matchedActors2 = 0; - for (const id of result2) { - if (selector2Actors.includes(id)) { - matchedActors2++; - } - } - - if (matchedActors2 === result2.size) { - console.log( - "✅ All requests to /route2 routed to route2 actors as expected", - ); - } else { - console.log( - `❌ Expected all requests to route to route2 actors, but only ${matchedActors2}/${result2.size} did`, - ); - throw new Error( - `Multiple routes test failed: ${matchedActors2}/${result2.size} requests to /route2 routed to route2 actors`, - ); - } - }, - ); -} - -async function testQueryParameters(): Promise { - return await setupTest( - { - name: "Query Parameters Test", - numSelectors: 1, - routes: [ - { - path: "/query", - routeSubpaths: false, - stripPrefix: true, - selectorIndex: 0, - }, - ], - }, - async (context) => { - // Get the actor IDs for the selector - const selectorActors = - context.actorsBySelector[context.selectors[0]]; - const route = context.routes?.[0]; - - // Custom function to validate query parameters - async function testWithQueryValidation( - path: string, - expectedQuery: Record, - ): Promise { - // Make the request - const testUrl = `http://localhost:7080${path}`; - console.log( - `Testing route at: ${testUrl} (with Host: ${context.hostname})`, - ); - - const data = await makeRequest(testUrl, context.hostname); - if (!data) { - throw new Error(`Failed to get response from ${path}`); - } - - // Validate query parameters - console.log("Expected query parameters:", expectedQuery); - console.log("Actual query parameters:", data.query); - - // Check that all expected parameters are present - let queryValidationPassed = true; - for (const [key, value] of Object.entries(expectedQuery)) { - if (data.query[key] !== value) { - console.error( - `❌ Query parameter validation failed for ${key}: Expected ${value}, got ${data.query[key]}`, - ); - queryValidationPassed = false; - } - } - - if (queryValidationPassed) { - console.log("✅ Query parameter validation passed"); - } else { - throw new Error("Query parameter validation failed"); - } - } - - // Test path with simple query parameters - console.log("Testing path with simple query parameters"); - const result = await testRoute( - context.hostname, - "/query?param=value&another=123", - 2, - 20, - route, - ); - if (result.size < 2) { - throw new Error( - "Query parameters test failed: Could not reach actors at /query with query parameters", - ); - } - - // Validate simple query parameters with direct check - await testWithQueryValidation("/query?param=value&another=123", { - param: "value", - another: "123", - }); - - // Test more complex query parameters - console.log("Testing path with complex query parameters"); - const complexResult = await testRoute( - context.hostname, - "/query?complex=test%20with%20spaces&array[]=1&array[]=2", - 2, - 20, - route, - ); - if (complexResult.size < 2) { - throw new Error( - "Query parameters test failed: Could not reach actors at /query with complex query parameters", - ); - } - - // Validate complex query parameters with direct check - await testWithQueryValidation( - "/query?complex=test%20with%20spaces", - { - complex: "test with spaces", - }, - ); - }, - ); -} - -async function testSpecialCharacters(): Promise { - return await setupTest( - { - name: "Special Characters Test", - numSelectors: 1, - routes: [ - { - path: "/special-chars", - routeSubpaths: true, - stripPrefix: true, - selectorIndex: 0, - }, - ], - }, - async (context) => { - // Get the actor IDs for the selector - const selectorActors = - context.actorsBySelector[context.selectors[0]]; - const route = context.routes?.[0]; - - // Test paths with special characters - console.log("Testing path with hyphens"); - let result = await testRoute( - context.hostname, - "/special-chars/with-hyphens", - 2, - 20, - route, - ); - if (result.size < 2) { - throw new Error( - "Special characters test failed: Could not reach actors at path with hyphens", - ); - } - - console.log("Testing path with underscores"); - result = await testRoute( - context.hostname, - "/special-chars/with_underscores", - 2, - 20, - route, - ); - if (result.size < 2) { - throw new Error( - "Special characters test failed: Could not reach actors at path with underscores", - ); - } - - console.log("Testing path with dots"); - result = await testRoute( - context.hostname, - "/special-chars/with.dots", - 2, - 20, - route, - ); - if (result.size < 2) { - throw new Error( - "Special characters test failed: Could not reach actors at path with dots", - ); - } - - console.log("Testing path with encoded characters"); - result = await testRoute( - context.hostname, - "/special-chars/encoded%20space", - 2, - 20, - route, - ); - if (result.size < 2) { - throw new Error( - "Special characters test failed: Could not reach actors at path with encoded characters", - ); - } - }, - ); -} - -async function testLargeActorPool(): Promise { - return await setupTest( - { - name: "Large Actor Pool Test", - numActors: 10, // Use 10 actors instead of the default 2 - numSelectors: 1, - routes: [ - { - path: "/large-pool", - routeSubpaths: false, - stripPrefix: true, - selectorIndex: 0, - }, - ], - }, - async (context) => { - // Get the actor IDs for the selector - const selectorActors = - context.actorsBySelector[context.selectors[0]]; - const route = context.routes?.[0]; - - // Test with larger number of actors to verify load balancing - console.log("Testing with larger actor pool (10 actors)"); - - // Make 20 requests and track unique actor IDs seen - const allActorsResult = new Set(); - - // Make multiple requests to see distribution - for (let i = 0; i < 3; i++) { - console.log(`Batch ${i + 1} of requests to large actor pool:`); - const result = await testRoute( - context.hostname, - "/large-pool", - 5, - 20, - route, - ); - - // Add these results to our overall set - for (const id of result) { - allActorsResult.add(id); - } - - // Wait a bit between batches - await new Promise((resolve) => setTimeout(resolve, 500)); - } - - // Verify we've seen a good number of unique actors (at least 7 out of 10) - if (allActorsResult.size < 7) { - console.log( - `❌ Only reached ${allActorsResult.size}/10 unique actors, expected at least 7`, - ); - throw new Error( - `Large actor pool test: Only reached ${allActorsResult.size}/10 unique actors`, - ); - } else { - console.log( - `✅ Reached ${allActorsResult.size}/10 unique actors across all requests`, - ); - } - }, - ); -} - -async function testLongPath(): Promise { - return await setupTest( - { - name: "Long Path Test", - numSelectors: 1, - routes: [ - { - path: "/long", - routeSubpaths: true, - stripPrefix: true, - selectorIndex: 0, - }, - ], - }, - async (context) => { - // Get the actor IDs for the selector - const selectorActors = - context.actorsBySelector[context.selectors[0]]; - const route = context.routes?.[0]; - - // Create a very long path - let longPathSegment = ""; - for (let i = 0; i < 10; i++) { - longPathSegment += "segment-" + i + "/"; - } - const longPath = `/long/${longPathSegment}end`; - - console.log("Testing very long path"); - console.log(`Path length: ${longPath.length} characters`); - - const result = await testRoute( - context.hostname, - longPath, - 2, - 20, - route, - ); - if (result.size < 2) { - throw new Error( - "Long path test failed: Could not reach actors with very long path", - ); - } - }, - ); -} - -async function testMixedPrefixStripping(): Promise { - return await setupTest( - { - name: "Mixed Prefix Stripping Test", - numSelectors: 2, - routes: [ - { - path: "/strip", - routeSubpaths: true, - stripPrefix: true, - selectorIndex: 0, - }, - { - path: "/nostrip", - routeSubpaths: true, - stripPrefix: false, - selectorIndex: 1, - }, - ], - }, - async (context) => { - // Get the actor IDs for both selectors - const stripActors = context.actorsBySelector[context.selectors[0]]; - const noStripActors = - context.actorsBySelector[context.selectors[1]]; - - const stripRoute = context.routes?.[0]; - const noStripRoute = context.routes?.[1]; - - // Test the strip prefix route - console.log("Testing route with stripPrefix=true"); - const stripResult = await testRoute( - context.hostname, - "/strip/subpath", - 2, - 20, - stripRoute, - ); - if (stripResult.size < 2) { - throw new Error( - "Mixed prefix stripping test failed: Could not reach actors at /strip/subpath", - ); - } - - // Test the no strip prefix route - console.log("Testing route with stripPrefix=false"); - const noStripResult = await testRoute( - context.hostname, - "/nostrip/subpath", - 2, - 20, - noStripRoute, - ); - if (noStripResult.size < 2) { - throw new Error( - "Mixed prefix stripping test failed: Could not reach actors at /nostrip/subpath", - ); - } - - // Verify we got responses from the correct actors for strip route - let matchedStripActors = 0; - for (const id of stripResult) { - if (stripActors.includes(id)) { - matchedStripActors++; - } - } - - if (matchedStripActors === stripResult.size) { - console.log( - "✅ All requests to /strip/subpath routed to strip actors as expected", - ); - } else { - console.log( - `❌ Expected all requests to route to strip actors, but only ${matchedStripActors}/${stripResult.size} did`, - ); - throw new Error( - `Mixed prefix stripping test failed: ${matchedStripActors}/${stripResult.size} requests routed to strip actors`, - ); - } - - // Verify we got responses from the correct actors for no-strip route - let matchedNoStripActors = 0; - for (const id of noStripResult) { - if (noStripActors.includes(id)) { - matchedNoStripActors++; - } - } - - if (matchedNoStripActors === noStripResult.size) { - console.log( - "✅ All requests to /nostrip/subpath routed to no-strip actors as expected", - ); - } else { - console.log( - `❌ Expected all requests to route to no-strip actors, but only ${matchedNoStripActors}/${noStripResult.size} did`, - ); - throw new Error( - `Mixed prefix stripping test failed: ${matchedNoStripActors}/${noStripResult.size} requests routed to no-strip actors`, - ); - } - }, - ); -} - -async function test404Response(): Promise { - return await setupTest( - { - name: "404 Response Test", - numSelectors: 1, - routes: [ - { - path: "/test-path", - routeSubpaths: false, - stripPrefix: true, - selectorIndex: 0, - }, - ], - }, - async (context) => { - // Get the actor IDs for the selector - const selectorActors = - context.actorsBySelector[context.selectors[0]]; - const route = context.routes?.[0]; - - // First verify our route works - console.log("Verifying route /test-path exists and works"); - const result = await testRoute( - context.hostname, - "/test-path", - 2, - 20, - route, - ); - if (result.size < 2) { - throw new Error( - "404 test setup failed: Could not reach actors at /test-path", - ); - } - - // Now test a path that doesn't match any route - should return 404 - console.log("Testing path that should 404: /non-existent-path"); - - try { - // We expect this to return an empty set (no matches) - const notFoundResult = await testRoute( - context.hostname, - "/non-existent-path", - 2, - 5, - ); - - // If we get here with results, it means the 404 test failed - if (notFoundResult.size > 0) { - console.error( - "❌ Expected 404 for /non-existent-path but got a successful response", - ); - throw new Error( - "404 test failed: Got a successful response instead of 404", - ); - } else { - console.log( - "✅ Correctly received no matches for non-existent path", - ); - } - } catch (error) { - // Check if this is our expected path validation error - if ( - error instanceof Error && - error.message.includes("Path validation failed") - ) { - console.error( - "❌ Unexpected path returned for non-existent route", - ); - throw error; - } - - // If it's a different error (like connection refused), that's expected for a 404 - console.log( - "✅ Received expected error for non-existent path:", - error.message, - ); - } - - // Test with a different hostname that doesn't have any routes - const randomHostname = `nonexistent-${crypto.randomBytes(4).toString("hex")}.rivet-job.local`; - console.log("Testing with non-existent hostname:", randomHostname); - - try { - const nonExistentHostResult = await makeRequest( - `http://localhost:7080/test`, - randomHostname, - ); - - // If we get a response, it's unexpected - if (nonExistentHostResult) { - console.error( - "❌ Expected 404 for non-existent hostname but got a response:", - nonExistentHostResult, - ); - throw new Error( - "404 test failed: Got a response for non-existent hostname", - ); - } else { - console.log( - "✅ Correctly received no response for non-existent hostname", - ); - } - } catch (error) { - // This is expected - we should get an error - console.log( - "✅ Received expected error for non-existent hostname:", - error.message, - ); - } - }, - ); -} - -async function run() { - try { - const tests = [ - { name: "Basic Route Test", fn: testBasicRoute }, - { name: "Path Prefix Test", fn: testPathPrefix }, - { name: "Exact Path Test", fn: testExactPath }, - { name: "Path Priority Test", fn: testPathPriority }, - { name: "Empty Path Test", fn: testEmptyPath }, - { name: "No Strip Prefix Test", fn: testNoStripPrefix }, - { name: "Multiple Routes Test", fn: testMultipleRoutes }, - { name: "Query Parameters Test", fn: testQueryParameters }, - { name: "Special Characters Test", fn: testSpecialCharacters }, - { name: "Large Actor Pool Test", fn: testLargeActorPool }, - { name: "Long Path Test", fn: testLongPath }, - { - name: "Mixed Prefix Stripping Test", - fn: testMixedPrefixStripping, - }, - { name: "404 Response Test", fn: test404Response }, // 404 test should run last - ]; - - for (const test of tests) { - console.log(`\nRunning test: ${test.name}`); - const testPassed = await test.fn(); - - // If any test fails, exit immediately - if (!testPassed) { - console.error( - `\n❌ Test "${test.name}" failed. Exiting test suite.`, - ); - process.exit(1); - } - } - - console.log("\n=== All tests completed successfully ===\n"); - process.exit(0); - } catch (error) { - console.error("Error running tests:", error); - process.exit(1); - } -} - -// Run the test -run().catch((error) => { - console.error("Unhandled error in test suite:", error); - process.exit(1); -}); +//import { RivetClient } from "@rivet-gg/api-full"; +//import crypto from "crypto"; +//import http from "http"; +// +//// Can be opt since they're not required for dev +//const RIVET_ENDPOINT = process.env.RIVET_ENDPOINT; +//const RIVET_SERVICE_TOKEN = process.env.RIVET_SERVICE_TOKEN; +//const RIVET_PROJECT = process.env.RIVET_PROJECT; +//const RIVET_ENVIRONMENT = process.env.RIVET_ENVIRONMENT; +// +//// Determine test kind from environment variable +//const BUILD_NAME = process.env.BUILD; +//if (BUILD_NAME !== "http-isolate" && BUILD_NAME !== "http-container") { +// throw new Error( +// "Must specify BUILD environment variable as either 'http-isolate' or 'http-container'", +// ); +//} +// +//let region = process.env.REGION; +//if (!region || region.length === 0) { +// region = undefined; +//} +// +//const client = new RivetClient({ +// environment: RIVET_ENDPOINT, +// token: RIVET_SERVICE_TOKEN, +//}); +// +//// Interface definitions +//interface RouteConfig { +// path: string; +// routeSubpaths: boolean; +// stripPrefix?: boolean; // Whether to strip the prefix from the path in request handlers +// selectorIndex?: number; // Index of the selector to use from selectors array +//} +// +//interface TestContext { +// actorIds: string[]; +// routeIds: string[]; +// selectors: string[]; // Array of selectors +// hostname: string; +// actorsBySelector: Record; // Maps selectors to their actor IDs +// routes?: RouteConfig[]; // Store the routes for this test +//} +// +//interface TestConfig { +// name: string; +// numActors?: number; +// numSelectors?: number; // Number of different selectors to create +// routes?: RouteConfig[]; // Each route can specify which selector to use +//} +// +//// Helper function to make HTTP requests with a custom host header +//async function makeRequest(url: string, hostname: string): Promise { +// return new Promise((resolve, reject) => { +// const parsedUrl = new URL(url); +// +// const options = { +// hostname: parsedUrl.hostname, +// port: parsedUrl.port || 80, +// path: parsedUrl.pathname + parsedUrl.search, +// method: "GET", +// headers: { +// Accept: "application/json", +// Host: hostname, +// }, +// }; +// +// const req = http.request(options, (res: any) => { +// if (res.statusCode !== 200) { +// console.error( +// `Request failed: ${res.statusCode} ${res.statusMessage}`, +// ); +// // Don't reject, just continue the loop +// resolve(null); +// return; +// } +// +// let rawData = ""; +// res.on("data", (chunk: any) => { +// rawData += chunk; +// }); +// res.on("end", () => { +// try { +// const parsedData = JSON.parse(rawData); +// resolve(parsedData); +// } catch (e) { +// console.error("Error parsing response:", e); +// reject(e); +// } +// }); +// }); +// +// req.on("error", (e: any) => { +// console.error(`Request error: ${e.message}`); +// reject(e); +// }); +// +// req.end(); +// }); +//} +// +//// Helper function to create actors with a specific selector +//async function createActors( +// selectorTag: string, +// numberOfActors: number = 2, +//): Promise { +// const createdActorIds: string[] = []; +// +// for (let i = 1; i <= numberOfActors; i++) { +// console.time(`create actor ${i}`); +// console.log(`Creating actor ${i} with tag`, { +// selector: selectorTag, +// }); +// +// const { actor } = await client.actors.create({ +// project: RIVET_PROJECT, +// environment: RIVET_ENVIRONMENT, +// body: { +// region, +// tags: { +// selector: selectorTag, +// instance: i.toString(), +// }, +// buildTags: { name: BUILD_NAME, current: "true" }, +// network: { +// ports: { +// http: { +// protocol: "https", +// routing: { +// guard: {}, +// }, +// }, +// }, +// }, +// lifecycle: { +// durable: false, +// }, +// ...(BUILD_NAME === "http-container" +// ? { +// resources: { +// cpu: 100, +// memory: 100, +// }, +// } +// : {}), +// }, +// }); +// +// createdActorIds.push(actor.id); +// console.timeEnd(`create actor ${i}`); +// console.log(`Created actor ${i} with ID:`, actor.id); +// } +// +// // Wait for actors to be ready +// await new Promise((resolve) => setTimeout(resolve, 2000)); +// return createdActorIds; +//} +// +//// Helper function to create a route +//async function createRoute( +// routeId: string, +// hostname: string, +// selectorTag: string, +// path: string, +// routeSubpaths: boolean = false, +// stripPrefix: boolean = true, +//): Promise { +// console.time(`create route ${routeId}`); +// console.log(`Creating route ${routeId} with selector tag`, { +// selector: selectorTag, +// path, +// routeSubpaths, +// stripPrefix, +// }); +// +// await client.routes.update(routeId, { +// project: RIVET_PROJECT, +// environment: RIVET_ENVIRONMENT, +// body: { +// hostname, +// path, +// routeSubpaths, +// stripPrefix, +// target: { +// actors: { +// selectorTags: { +// selector: selectorTag, +// }, +// }, +// }, +// }, +// }); +// +// console.timeEnd(`create route ${routeId}`); +// console.log(`Created route ${routeId}.`); +// +// // Wait for route to be active +// await new Promise((resolve) => setTimeout(resolve, 2000)); +//} +// +//// Helper function to calculate expected path based on route configuration +//function getExpectedPath( +// requestPath: string, +// routePath: string, +// stripPrefix: boolean, +//): string { +// // Extract the query string if present +// const queryStringIndex = requestPath.indexOf("?"); +// const pathWithoutQuery = +// queryStringIndex >= 0 +// ? requestPath.substring(0, queryStringIndex) +// : requestPath; +// +// // If stripPrefix is false, the full path should be returned +// if (!stripPrefix) { +// return pathWithoutQuery; +// } +// +// // If stripPrefix is true, we need to strip the route path prefix +// if (routePath === "") { +// // For empty path routes, return the path as is +// return pathWithoutQuery; +// } +// +// // For non-empty paths with stripPrefix=true +// if (pathWithoutQuery === routePath) { +// // If exact match, return "/" +// return "/"; +// } else if (pathWithoutQuery.startsWith(routePath + "/")) { +// // If it's a subpath, remove the prefix +// return pathWithoutQuery.substring(routePath.length); +// } +// +// // Default case - shouldn't happen with proper routing +// return pathWithoutQuery; +//} +// +//// Helper function to test a route +//async function testRoute( +// hostname: string, +// path: string, +// numActorsExpected: number = 2, +// maxRequests: number = 20, +// route?: RouteConfig, // Added route config parameter +//): Promise> { +// const actorIds = new Set(); +// let successfulMatches = 0; +// let totalRequests = 0; +// let pathValidationFailed = false; +// +// // Using localhost with Host header for local testing +// const testUrl = `http://localhost:7080${path}`; +// console.log(`Testing route at: ${testUrl} (with Host: ${hostname})`); +// console.time(`route-test-${path}`); +// +// // Calculate expected path if route is provided +// let expectedPath = route +// ? getExpectedPath( +// path, +// route.path, +// route.stripPrefix !== undefined ? route.stripPrefix : true, +// ) +// : path; +// +// // URL-encoded characters like %20 will be decoded by the server +// // Decode the expected path to match server behavior +// expectedPath = decodeURIComponent(expectedPath); +// +// if (route) { +// console.log( +// `Route config: path=${route.path}, routeSubpaths=${route.routeSubpaths}, stripPrefix=${route.stripPrefix}`, +// ); +// console.log(`Expected path in response: ${expectedPath}`); +// } +// +// while (actorIds.size < numActorsExpected && totalRequests < maxRequests) { +// totalRequests++; +// +// try { +// const data = await makeRequest(testUrl, hostname); +// +// // If request failed or returned null, continue to next iteration +// if (!data) { +// continue; +// } +// +// console.log( +// `Request ${totalRequests}: Response from actor ${data.actorId} with path ${data.path}`, +// ); +// +// // Validate the path in the response matches the expected path +// if (data.path !== expectedPath) { +// console.error( +// `❌ Path validation failed: Expected ${expectedPath}, got ${data.path}`, +// ); +// pathValidationFailed = true; +// } else { +// console.log(`✅ Path validation passed: ${data.path}`); +// } +// +// // Log query parameters if present +// if (data.query && Object.keys(data.query).length > 0) { +// console.log(`Query parameters received:`, data.query); +// } +// +// // Track the actor IDs we've seen +// if (data.actorId) { +// actorIds.add(data.actorId); +// successfulMatches++; +// } +// +// // If we've found all expected actors, we're done +// if (actorIds.size === numActorsExpected) { +// console.log( +// `Successfully received responses from all ${numActorsExpected} actors!`, +// ); +// break; +// } +// +// // Small delay between requests +// await new Promise((resolve) => setTimeout(resolve, 200)); +// } catch (error) { +// console.error("Error making request:", error); +// // Wait a bit longer if there's an error +// await new Promise((resolve) => setTimeout(resolve, 500)); +// } +// } +// +// console.timeEnd(`route-test-${path}`); +// console.log( +// `Test completed. Matched ${actorIds.size}/${numActorsExpected} actors in ${totalRequests} requests.`, +// ); +// console.log(`Actors matched: ${Array.from(actorIds).join(", ")}`); +// +// if (actorIds.size < numActorsExpected) { +// console.error( +// `Failed to reach all ${numActorsExpected} actors through the route!`, +// ); +// } +// +// if (pathValidationFailed) { +// console.error( +// "Path validation failed: The path in the response did not match the expected path", +// ); +// throw new Error(`Path validation failed for ${path}`); +// } +// +// // Final stats +// console.log(` +//Route Test Results for ${path}: +//------------------ +//Total requests: ${totalRequests} +//Successful responses: ${successfulMatches} +//Unique actors reached: ${actorIds.size}/${numActorsExpected} +//Route: ${testUrl} (Host: ${hostname}) +//Path validation: ${pathValidationFailed ? "❌ Failed" : "✅ Passed"} +//------------------ +// `); +// +// return actorIds; +//} +// +//// Helper function to verify routes exist +//async function verifyRouteExists(hostname: string): Promise { +// console.time("list routes"); +// console.log("Listing routes to verify our route exists"); +// const { routes } = await client.routes.list({ +// project: RIVET_PROJECT, +// environment: RIVET_ENVIRONMENT, +// }); +// console.timeEnd("list routes"); +// +// // Find our route in the list +// const ourRoute = routes.find((route) => route.hostname === hostname); +// if (!ourRoute) { +// console.error( +// `Route with hostname ${hostname} not found in routes list!`, +// ); +// return false; +// } +// console.log("✅ Found our route in the list:", ourRoute); +// return true; +//} +// +//// Helper function to delete resources +//async function cleanup(context: TestContext): Promise { +// // Cleanup: delete routes first +// for (const routeId of context.routeIds) { +// console.log("Deleting route", routeId); +// try { +// await client.routes.delete(routeId, { +// project: RIVET_PROJECT, +// environment: RIVET_ENVIRONMENT, +// }); +// console.log(`Route ${routeId} deleted successfully`); +// } catch (err) { +// console.error(`Error deleting route ${routeId}:`, err); +// } +// } +// +// // Then delete all actors +// for (let i = 0; i < context.actorIds.length; i++) { +// const actorId = context.actorIds[i]; +// console.log(`Destroying actor ${i + 1}:`, actorId); +// try { +// await client.actors.destroy(actorId, { +// project: RIVET_PROJECT, +// environment: RIVET_ENVIRONMENT, +// }); +// } catch (err) { +// console.error(`Error destroying actor ${i + 1}:`, err); +// } +// } +//} +// +//// Core test setup function that handles resource creation and cleanup +//async function setupTest( +// config: TestConfig, +// testFn: (context: TestContext) => Promise, +//): Promise { +// console.log(`\n=== ${config.name} ===\n`); +// +// const baseSelector = `test-${crypto.randomBytes(4).toString("hex")}`; +// const hostname = `route-${crypto.randomBytes(4).toString("hex")}.rivet-job.local`; +// +// const context: TestContext = { +// actorIds: [], +// routeIds: [], +// selectors: [], +// hostname, +// actorsBySelector: {}, +// routes: config.routes, +// }; +// +// try { +// // Create selectors based on config +// const numSelectors = config.numSelectors || 1; +// +// // Create selectors and actors for each selector +// for (let i = 0; i < numSelectors; i++) { +// const selectorName = +// numSelectors === 1 ? baseSelector : `${baseSelector}-${i + 1}`; +// context.selectors.push(selectorName); +// +// console.log( +// `Creating actors with selector ${selectorName} (${i + 1}/${numSelectors})`, +// ); +// const actors = await createActors( +// selectorName, +// config.numActors || 2, +// ); +// context.actorIds.push(...actors); +// context.actorsBySelector[selectorName] = actors; +// } +// +// // Create routes from config +// if (config.routes && config.routes.length > 0) { +// for (let i = 0; i < config.routes.length; i++) { +// const route = config.routes[i]; +// const routeId = `route-${crypto.randomBytes(4).toString("hex")}${i > 0 ? `-${i}` : ""}`; +// +// // Determine which selector to use for this route +// const selectorIndex = +// route.selectorIndex !== undefined ? route.selectorIndex : 0; +// if (selectorIndex >= context.selectors.length) { +// throw new Error( +// `Route ${i} references selector ${selectorIndex} but only ${context.selectors.length} selectors were created`, +// ); +// } +// +// const selector = context.selectors[selectorIndex]; +// +// console.log( +// `Creating route ${routeId} with path ${route.path} using selector ${selector} (index ${selectorIndex})`, +// ); +// await createRoute( +// routeId, +// context.hostname, +// selector, +// route.path, +// route.routeSubpaths, +// route.stripPrefix !== undefined ? route.stripPrefix : true, +// ); +// context.routeIds.push(routeId); +// } +// } +// +// // Verify routes exist +// await verifyRouteExists(context.hostname); +// +// // Run the test function +// await testFn(context); +// +// // If we get here, the test passed +// console.log(`✅ Test "${config.name}" passed successfully`); +// return true; +// } catch (error) { +// console.error(`❌ Error in ${config.name}:`, error); +// return false; +// } finally { +// // Clean up all resources +// await cleanup(context); +// } +//} +// +//// Test implementations +//async function testBasicRoute(): Promise { +// return await setupTest( +// { +// name: "Basic Route Test", +// numSelectors: 1, +// routes: [ +// { +// path: "/test", +// routeSubpaths: false, +// stripPrefix: true, +// selectorIndex: 0, +// }, +// ], +// }, +// async (context) => { +// // Get the actor IDs for the first selector +// const selectorActors = +// context.actorsBySelector[context.selectors[0]]; +// const route = context.routes?.[0]; +// +// // Test the route +// const result = await testRoute( +// context.hostname, +// "/test", +// 2, +// 20, +// route, +// ); +// if (result.size < 2) { +// throw new Error( +// "Basic route test failed: Could not reach all expected actors", +// ); +// } +// +// // Verify we got responses from the correct actors +// let matchedActors = 0; +// for (const id of result) { +// if (selectorActors.includes(id)) { +// matchedActors++; +// } +// } +// +// if (matchedActors === result.size) { +// console.log("✅ All requests routed to the correct actors"); +// } else { +// console.log( +// `❌ Expected all requests to route to the correct actors, but only ${matchedActors}/${result.size} did`, +// ); +// throw new Error( +// `Basic route test failed: ${matchedActors}/${result.size} requests routed to the correct actors`, +// ); +// } +// }, +// ); +//} +// +//async function testPathPrefix(): Promise { +// return await setupTest( +// { +// name: "Path Prefix Test", +// numSelectors: 1, +// routes: [ +// { +// path: "/api", +// routeSubpaths: true, +// stripPrefix: true, +// selectorIndex: 0, +// }, +// ], +// }, +// async (context) => { +// // Get the actor IDs for the first selector +// const selectorActors = +// context.actorsBySelector[context.selectors[0]]; +// const route = context.routes?.[0]; +// +// // Test various paths that should match the prefix +// console.log("Testing paths that should match the prefix /api"); +// let result = await testRoute( +// context.hostname, +// "/api", +// 2, +// 20, +// route, +// ); +// if (result.size < 2) { +// throw new Error( +// "Path prefix test failed: Could not reach actors at /api", +// ); +// } +// +// result = await testRoute(context.hostname, "/api/v1", 2, 20, route); +// if (result.size < 2) { +// throw new Error( +// "Path prefix test failed: Could not reach actors at /api/v1", +// ); +// } +// +// result = await testRoute( +// context.hostname, +// "/api/users", +// 2, +// 20, +// route, +// ); +// if (result.size < 2) { +// throw new Error( +// "Path prefix test failed: Could not reach actors at /api/users", +// ); +// } +// +// // Test a path that shouldn't match +// console.log("Testing path that shouldn't match the prefix /api"); +// const nonMatchingResult = await testRoute( +// context.hostname, +// "/other", +// 2, +// 5, +// ); +// +// if (nonMatchingResult.size > 0) { +// console.log( +// "❌ Found match for non-matching path /other when it should have failed", +// ); +// throw new Error( +// "Path prefix test failed: Found match for non-matching path /other", +// ); +// } else { +// console.log( +// "✅ Correctly found no matches for non-matching path /other", +// ); +// } +// }, +// ); +//} +// +//async function testExactPath(): Promise { +// return await setupTest( +// { +// name: "Exact Path Test", +// numSelectors: 1, +// routes: [ +// { +// path: "/exact", +// routeSubpaths: false, +// stripPrefix: true, +// selectorIndex: 0, +// }, +// ], +// }, +// async (context) => { +// // Get the actor IDs for the selector +// const selectorActors = +// context.actorsBySelector[context.selectors[0]]; +// const route = context.routes?.[0]; +// +// // Test the exact path +// console.log("Testing exact path /exact"); +// const result = await testRoute( +// context.hostname, +// "/exact", +// 2, +// 20, +// route, +// ); +// if (result.size < 2) { +// throw new Error( +// "Exact path test failed: Could not reach actors at exact path /exact", +// ); +// } +// +// // Test paths that shouldn't match +// console.log( +// "Testing paths that shouldn't match the exact path /exact", +// ); +// const subPathResult = await testRoute( +// context.hostname, +// "/exact/subpath", +// 2, +// 5, +// ); +// +// if (subPathResult.size > 0) { +// console.log( +// "❌ Found match for /exact/subpath when it should have failed", +// ); +// throw new Error( +// "Exact path test failed: Found match for subpath /exact/subpath", +// ); +// } else { +// console.log("✅ Correctly found no matches for /exact/subpath"); +// } +// +// const differentPathResult = await testRoute( +// context.hostname, +// "/different", +// 2, +// 5, +// ); +// +// if (differentPathResult.size > 0) { +// console.log( +// "❌ Found match for /different when it should have failed", +// ); +// throw new Error( +// "Exact path test failed: Found match for different path /different", +// ); +// } else { +// console.log("✅ Correctly found no matches for /different"); +// } +// }, +// ); +//} +// +//async function testPathPriority(): Promise { +// return await setupTest( +// { +// name: "Path Priority Test", +// numSelectors: 2, +// routes: [ +// { +// path: "/api", +// routeSubpaths: true, +// stripPrefix: true, +// selectorIndex: 0, +// }, // Less specific path +// { +// path: "/api/users", +// routeSubpaths: true, +// stripPrefix: true, +// selectorIndex: 1, +// }, // More specific path (higher priority) +// ], +// }, +// async (context) => { +// // Get the selectors and actor groups +// const apiSelector = context.selectors[0]; +// const usersSelector = context.selectors[1]; +// +// const apiActors = context.actorsBySelector[apiSelector]; // API actors (lower priority) +// const usersActors = context.actorsBySelector[usersSelector]; // Users actors (higher priority) +// +// const apiRoute = context.routes?.[0]; +// const usersRoute = context.routes?.[1]; +// +// console.log( +// "API selector:", +// apiSelector, +// "with actors:", +// apiActors, +// ); +// console.log( +// "Users selector:", +// usersSelector, +// "with actors:", +// usersActors, +// ); +// +// // 1. First verify we can access both sets of actors directly with their exact paths +// console.log( +// "\nStep 1: Verifying direct access to both actor groups", +// ); +// +// // 1.1 Test the less specific path (/api) - should route to first set of actors +// console.log("Testing /api path - should route to API actors"); +// const apiResult = await testRoute( +// context.hostname, +// "/api", +// 2, +// 10, +// apiRoute, +// ); +// if (apiResult.size < 2) { +// throw new Error( +// "Path priority test failed: Could not reach actors at /api", +// ); +// } +// +// // Check we got responses from the API actors +// let matchedApiActors = 0; +// for (const id of apiResult) { +// if (apiActors.includes(id)) { +// matchedApiActors++; +// } +// } +// +// if (matchedApiActors === apiResult.size) { +// console.log( +// "✅ All requests to /api routed to API actors as expected", +// ); +// } else { +// console.log( +// `❌ Expected all requests to /api to route to API actors, but only ${matchedApiActors}/${apiResult.size} did`, +// ); +// throw new Error( +// `Path priority test failed: ${matchedApiActors}/${apiResult.size} requests to /api routed to API actors`, +// ); +// } +// +// // 1.2 Test the more specific path (/api/users) - should route to users actors +// console.log( +// "\nTesting /api/users path - should route to Users actors", +// ); +// const usersResult = await testRoute( +// context.hostname, +// "/api/users", +// 2, +// 10, +// usersRoute, +// ); +// if (usersResult.size < 2) { +// throw new Error( +// "Path priority test failed: Could not reach actors at /api/users", +// ); +// } +// +// // Check we got responses from the Users actors +// let matchedUsersActors = 0; +// for (const id of usersResult) { +// if (usersActors.includes(id)) { +// matchedUsersActors++; +// } +// } +// +// if (matchedUsersActors === usersResult.size) { +// console.log( +// "✅ All requests to /api/users routed to Users actors as expected", +// ); +// } else { +// console.log( +// `❌ Expected all requests to /api/users to route to Users actors, but only ${matchedUsersActors}/${usersResult.size} did`, +// ); +// throw new Error( +// `Path priority test failed: ${matchedUsersActors}/${usersResult.size} requests to /api/users routed to Users actors`, +// ); +// } +// +// // 2. Test a path that would match the /api prefix but is a subpath of /api/users +// // Should go to the /api/users actors as the more specific path has higher priority +// console.log("\nStep 2: Testing a subpath priority"); +// console.log( +// "Testing /api/users/123 path - should route to Users actors (more specific path)", +// ); +// const subpathResult = await testRoute( +// context.hostname, +// "/api/users/123", +// 2, +// 10, +// usersRoute, +// ); +// if (subpathResult.size < 2) { +// throw new Error( +// "Path priority test failed: Could not reach actors at /api/users/123", +// ); +// } +// +// // Check if we got responses from the expected actors (should be from Users actors path) +// let matchedSubpathUsers = 0; +// for (const id of subpathResult) { +// if (usersActors.includes(id)) { +// matchedSubpathUsers++; +// } +// } +// +// if (matchedSubpathUsers === subpathResult.size) { +// console.log( +// "✅ All requests for /api/users/123 routed to Users actors (more specific path) as expected", +// ); +// } else { +// console.log( +// `❌ Expected all requests to route to Users actors (more specific path), but only ${matchedSubpathUsers}/${subpathResult.size} did`, +// ); +// throw new Error( +// `Path priority test failed for subpath: ${matchedSubpathUsers}/${subpathResult.size} requests routed to the Users actors`, +// ); +// } +// +// // 3. Test a path that matches the /api prefix but is NOT a subpath of /api/users +// // Should go to the /api actors +// console.log("\nStep 3: Testing another /api subpath"); +// console.log( +// "Testing /api/other path - should route to API actors (because it's not under /api/users)", +// ); +// const otherResult = await testRoute( +// context.hostname, +// "/api/other", +// 2, +// 10, +// apiRoute, +// ); +// if (otherResult.size < 2) { +// throw new Error( +// "Path priority test failed: Could not reach actors at /api/other", +// ); +// } +// +// // Check if we got responses from the API actors +// let matchedOtherApi = 0; +// for (const id of otherResult) { +// if (apiActors.includes(id)) { +// matchedOtherApi++; +// } +// } +// +// if (matchedOtherApi === otherResult.size) { +// console.log( +// "✅ All requests for /api/other routed to API actors as expected", +// ); +// } else { +// console.log( +// `❌ Expected all requests to route to API actors, but only ${matchedOtherApi}/${otherResult.size} did`, +// ); +// throw new Error( +// `Path priority test failed for other subpath: ${matchedOtherApi}/${otherResult.size} requests routed to the API actors`, +// ); +// } +// }, +// ); +//} +// +//async function testEmptyPath(): Promise { +// return await setupTest( +// { +// name: "Empty Path Test", +// numSelectors: 1, +// routes: [ +// { +// path: "", +// routeSubpaths: true, +// stripPrefix: true, +// selectorIndex: 0, +// }, +// ], +// }, +// async (context) => { +// // Get the actor IDs for the first selector +// const selectorActors = +// context.actorsBySelector[context.selectors[0]]; +// const route = context.routes?.[0]; +// +// // Test various paths that should all match due to empty path with routeSubpaths=true +// console.log("Testing empty path which should match any path"); +// +// let result = await testRoute(context.hostname, "/", 2, 20, route); +// if (result.size < 2) { +// throw new Error( +// "Empty path test failed: Could not reach actors at /", +// ); +// } +// +// result = await testRoute(context.hostname, "/api", 2, 20, route); +// if (result.size < 2) { +// throw new Error( +// "Empty path test failed: Could not reach actors at /api", +// ); +// } +// +// result = await testRoute(context.hostname, "/users", 2, 20, route); +// if (result.size < 2) { +// throw new Error( +// "Empty path test failed: Could not reach actors at /users", +// ); +// } +// +// result = await testRoute( +// context.hostname, +// "/deep/nested/path", +// 2, +// 20, +// route, +// ); +// if (result.size < 2) { +// throw new Error( +// "Empty path test failed: Could not reach actors at /deep/nested/path", +// ); +// } +// }, +// ); +//} +// +//async function testNoStripPrefix(): Promise { +// return await setupTest( +// { +// name: "No Strip Prefix Test", +// numSelectors: 1, +// routes: [ +// { +// path: "/prefix", +// routeSubpaths: true, +// stripPrefix: false, +// selectorIndex: 0, +// }, +// ], +// }, +// async (context) => { +// // Get the actor IDs for the first selector +// const selectorActors = +// context.actorsBySelector[context.selectors[0]]; +// const route = context.routes?.[0]; +// +// // Test the exact path - the path in response should be the full path +// console.log( +// "Testing exact path with stripPrefix=false. Path should NOT be stripped.", +// ); +// const result = await testRoute( +// context.hostname, +// "/prefix", +// 2, +// 20, +// route, +// ); +// if (result.size < 2) { +// throw new Error( +// "No strip prefix test failed: Could not reach actors at /prefix", +// ); +// } +// +// // Test a subpath - the path in response should be the full path again +// console.log( +// "Testing subpath with stripPrefix=false. Path should NOT be stripped.", +// ); +// const subpathResult = await testRoute( +// context.hostname, +// "/prefix/subpath", +// 2, +// 20, +// route, +// ); +// if (subpathResult.size < 2) { +// throw new Error( +// "No strip prefix test failed: Could not reach actors at /prefix/subpath", +// ); +// } +// }, +// ); +//} +// +//async function testMultipleRoutes(): Promise { +// return await setupTest( +// { +// name: "Multiple Routes Test", +// numSelectors: 2, +// routes: [ +// { +// path: "/route1", +// routeSubpaths: false, +// stripPrefix: true, +// selectorIndex: 0, +// }, +// { +// path: "/route2", +// routeSubpaths: false, +// stripPrefix: true, +// selectorIndex: 1, +// }, +// ], +// }, +// async (context) => { +// // Get the actor IDs for both selectors +// const selector1Actors = +// context.actorsBySelector[context.selectors[0]]; +// const selector2Actors = +// context.actorsBySelector[context.selectors[1]]; +// +// const route1 = context.routes?.[0]; +// const route2 = context.routes?.[1]; +// +// // Test first route +// console.log("Testing first route /route1"); +// const result1 = await testRoute( +// context.hostname, +// "/route1", +// 2, +// 20, +// route1, +// ); +// if (result1.size < 2) { +// throw new Error( +// "Multiple routes test failed: Could not reach actors at /route1", +// ); +// } +// +// // Verify we got responses from the correct actors +// let matchedActors1 = 0; +// for (const id of result1) { +// if (selector1Actors.includes(id)) { +// matchedActors1++; +// } +// } +// +// if (matchedActors1 === result1.size) { +// console.log( +// "✅ All requests to /route1 routed to route1 actors as expected", +// ); +// } else { +// console.log( +// `❌ Expected all requests to route to route1 actors, but only ${matchedActors1}/${result1.size} did`, +// ); +// throw new Error( +// `Multiple routes test failed: ${matchedActors1}/${result1.size} requests to /route1 routed to route1 actors`, +// ); +// } +// +// // Test second route +// console.log("Testing second route /route2"); +// const result2 = await testRoute( +// context.hostname, +// "/route2", +// 2, +// 20, +// route2, +// ); +// if (result2.size < 2) { +// throw new Error( +// "Multiple routes test failed: Could not reach actors at /route2", +// ); +// } +// +// // Verify we got responses from the correct actors +// let matchedActors2 = 0; +// for (const id of result2) { +// if (selector2Actors.includes(id)) { +// matchedActors2++; +// } +// } +// +// if (matchedActors2 === result2.size) { +// console.log( +// "✅ All requests to /route2 routed to route2 actors as expected", +// ); +// } else { +// console.log( +// `❌ Expected all requests to route to route2 actors, but only ${matchedActors2}/${result2.size} did`, +// ); +// throw new Error( +// `Multiple routes test failed: ${matchedActors2}/${result2.size} requests to /route2 routed to route2 actors`, +// ); +// } +// }, +// ); +//} +// +//async function testQueryParameters(): Promise { +// return await setupTest( +// { +// name: "Query Parameters Test", +// numSelectors: 1, +// routes: [ +// { +// path: "/query", +// routeSubpaths: false, +// stripPrefix: true, +// selectorIndex: 0, +// }, +// ], +// }, +// async (context) => { +// // Get the actor IDs for the selector +// const selectorActors = +// context.actorsBySelector[context.selectors[0]]; +// const route = context.routes?.[0]; +// +// // Custom function to validate query parameters +// async function testWithQueryValidation( +// path: string, +// expectedQuery: Record, +// ): Promise { +// // Make the request +// const testUrl = `http://localhost:7080${path}`; +// console.log( +// `Testing route at: ${testUrl} (with Host: ${context.hostname})`, +// ); +// +// const data = await makeRequest(testUrl, context.hostname); +// if (!data) { +// throw new Error(`Failed to get response from ${path}`); +// } +// +// // Validate query parameters +// console.log("Expected query parameters:", expectedQuery); +// console.log("Actual query parameters:", data.query); +// +// // Check that all expected parameters are present +// let queryValidationPassed = true; +// for (const [key, value] of Object.entries(expectedQuery)) { +// if (data.query[key] !== value) { +// console.error( +// `❌ Query parameter validation failed for ${key}: Expected ${value}, got ${data.query[key]}`, +// ); +// queryValidationPassed = false; +// } +// } +// +// if (queryValidationPassed) { +// console.log("✅ Query parameter validation passed"); +// } else { +// throw new Error("Query parameter validation failed"); +// } +// } +// +// // Test path with simple query parameters +// console.log("Testing path with simple query parameters"); +// const result = await testRoute( +// context.hostname, +// "/query?param=value&another=123", +// 2, +// 20, +// route, +// ); +// if (result.size < 2) { +// throw new Error( +// "Query parameters test failed: Could not reach actors at /query with query parameters", +// ); +// } +// +// // Validate simple query parameters with direct check +// await testWithQueryValidation("/query?param=value&another=123", { +// param: "value", +// another: "123", +// }); +// +// // Test more complex query parameters +// console.log("Testing path with complex query parameters"); +// const complexResult = await testRoute( +// context.hostname, +// "/query?complex=test%20with%20spaces&array[]=1&array[]=2", +// 2, +// 20, +// route, +// ); +// if (complexResult.size < 2) { +// throw new Error( +// "Query parameters test failed: Could not reach actors at /query with complex query parameters", +// ); +// } +// +// // Validate complex query parameters with direct check +// await testWithQueryValidation( +// "/query?complex=test%20with%20spaces", +// { +// complex: "test with spaces", +// }, +// ); +// }, +// ); +//} +// +//async function testSpecialCharacters(): Promise { +// return await setupTest( +// { +// name: "Special Characters Test", +// numSelectors: 1, +// routes: [ +// { +// path: "/special-chars", +// routeSubpaths: true, +// stripPrefix: true, +// selectorIndex: 0, +// }, +// ], +// }, +// async (context) => { +// // Get the actor IDs for the selector +// const selectorActors = +// context.actorsBySelector[context.selectors[0]]; +// const route = context.routes?.[0]; +// +// // Test paths with special characters +// console.log("Testing path with hyphens"); +// let result = await testRoute( +// context.hostname, +// "/special-chars/with-hyphens", +// 2, +// 20, +// route, +// ); +// if (result.size < 2) { +// throw new Error( +// "Special characters test failed: Could not reach actors at path with hyphens", +// ); +// } +// +// console.log("Testing path with underscores"); +// result = await testRoute( +// context.hostname, +// "/special-chars/with_underscores", +// 2, +// 20, +// route, +// ); +// if (result.size < 2) { +// throw new Error( +// "Special characters test failed: Could not reach actors at path with underscores", +// ); +// } +// +// console.log("Testing path with dots"); +// result = await testRoute( +// context.hostname, +// "/special-chars/with.dots", +// 2, +// 20, +// route, +// ); +// if (result.size < 2) { +// throw new Error( +// "Special characters test failed: Could not reach actors at path with dots", +// ); +// } +// +// console.log("Testing path with encoded characters"); +// result = await testRoute( +// context.hostname, +// "/special-chars/encoded%20space", +// 2, +// 20, +// route, +// ); +// if (result.size < 2) { +// throw new Error( +// "Special characters test failed: Could not reach actors at path with encoded characters", +// ); +// } +// }, +// ); +//} +// +//async function testLargeActorPool(): Promise { +// return await setupTest( +// { +// name: "Large Actor Pool Test", +// numActors: 10, // Use 10 actors instead of the default 2 +// numSelectors: 1, +// routes: [ +// { +// path: "/large-pool", +// routeSubpaths: false, +// stripPrefix: true, +// selectorIndex: 0, +// }, +// ], +// }, +// async (context) => { +// // Get the actor IDs for the selector +// const selectorActors = +// context.actorsBySelector[context.selectors[0]]; +// const route = context.routes?.[0]; +// +// // Test with larger number of actors to verify load balancing +// console.log("Testing with larger actor pool (10 actors)"); +// +// // Make 20 requests and track unique actor IDs seen +// const allActorsResult = new Set(); +// +// // Make multiple requests to see distribution +// for (let i = 0; i < 3; i++) { +// console.log(`Batch ${i + 1} of requests to large actor pool:`); +// const result = await testRoute( +// context.hostname, +// "/large-pool", +// 5, +// 20, +// route, +// ); +// +// // Add these results to our overall set +// for (const id of result) { +// allActorsResult.add(id); +// } +// +// // Wait a bit between batches +// await new Promise((resolve) => setTimeout(resolve, 500)); +// } +// +// // Verify we've seen a good number of unique actors (at least 7 out of 10) +// if (allActorsResult.size < 7) { +// console.log( +// `❌ Only reached ${allActorsResult.size}/10 unique actors, expected at least 7`, +// ); +// throw new Error( +// `Large actor pool test: Only reached ${allActorsResult.size}/10 unique actors`, +// ); +// } else { +// console.log( +// `✅ Reached ${allActorsResult.size}/10 unique actors across all requests`, +// ); +// } +// }, +// ); +//} +// +//async function testLongPath(): Promise { +// return await setupTest( +// { +// name: "Long Path Test", +// numSelectors: 1, +// routes: [ +// { +// path: "/long", +// routeSubpaths: true, +// stripPrefix: true, +// selectorIndex: 0, +// }, +// ], +// }, +// async (context) => { +// // Get the actor IDs for the selector +// const selectorActors = +// context.actorsBySelector[context.selectors[0]]; +// const route = context.routes?.[0]; +// +// // Create a very long path +// let longPathSegment = ""; +// for (let i = 0; i < 10; i++) { +// longPathSegment += "segment-" + i + "/"; +// } +// const longPath = `/long/${longPathSegment}end`; +// +// console.log("Testing very long path"); +// console.log(`Path length: ${longPath.length} characters`); +// +// const result = await testRoute( +// context.hostname, +// longPath, +// 2, +// 20, +// route, +// ); +// if (result.size < 2) { +// throw new Error( +// "Long path test failed: Could not reach actors with very long path", +// ); +// } +// }, +// ); +//} +// +//async function testMixedPrefixStripping(): Promise { +// return await setupTest( +// { +// name: "Mixed Prefix Stripping Test", +// numSelectors: 2, +// routes: [ +// { +// path: "/strip", +// routeSubpaths: true, +// stripPrefix: true, +// selectorIndex: 0, +// }, +// { +// path: "/nostrip", +// routeSubpaths: true, +// stripPrefix: false, +// selectorIndex: 1, +// }, +// ], +// }, +// async (context) => { +// // Get the actor IDs for both selectors +// const stripActors = context.actorsBySelector[context.selectors[0]]; +// const noStripActors = +// context.actorsBySelector[context.selectors[1]]; +// +// const stripRoute = context.routes?.[0]; +// const noStripRoute = context.routes?.[1]; +// +// // Test the strip prefix route +// console.log("Testing route with stripPrefix=true"); +// const stripResult = await testRoute( +// context.hostname, +// "/strip/subpath", +// 2, +// 20, +// stripRoute, +// ); +// if (stripResult.size < 2) { +// throw new Error( +// "Mixed prefix stripping test failed: Could not reach actors at /strip/subpath", +// ); +// } +// +// // Test the no strip prefix route +// console.log("Testing route with stripPrefix=false"); +// const noStripResult = await testRoute( +// context.hostname, +// "/nostrip/subpath", +// 2, +// 20, +// noStripRoute, +// ); +// if (noStripResult.size < 2) { +// throw new Error( +// "Mixed prefix stripping test failed: Could not reach actors at /nostrip/subpath", +// ); +// } +// +// // Verify we got responses from the correct actors for strip route +// let matchedStripActors = 0; +// for (const id of stripResult) { +// if (stripActors.includes(id)) { +// matchedStripActors++; +// } +// } +// +// if (matchedStripActors === stripResult.size) { +// console.log( +// "✅ All requests to /strip/subpath routed to strip actors as expected", +// ); +// } else { +// console.log( +// `❌ Expected all requests to route to strip actors, but only ${matchedStripActors}/${stripResult.size} did`, +// ); +// throw new Error( +// `Mixed prefix stripping test failed: ${matchedStripActors}/${stripResult.size} requests routed to strip actors`, +// ); +// } +// +// // Verify we got responses from the correct actors for no-strip route +// let matchedNoStripActors = 0; +// for (const id of noStripResult) { +// if (noStripActors.includes(id)) { +// matchedNoStripActors++; +// } +// } +// +// if (matchedNoStripActors === noStripResult.size) { +// console.log( +// "✅ All requests to /nostrip/subpath routed to no-strip actors as expected", +// ); +// } else { +// console.log( +// `❌ Expected all requests to route to no-strip actors, but only ${matchedNoStripActors}/${noStripResult.size} did`, +// ); +// throw new Error( +// `Mixed prefix stripping test failed: ${matchedNoStripActors}/${noStripResult.size} requests routed to no-strip actors`, +// ); +// } +// }, +// ); +//} +// +//async function test404Response(): Promise { +// return await setupTest( +// { +// name: "404 Response Test", +// numSelectors: 1, +// routes: [ +// { +// path: "/test-path", +// routeSubpaths: false, +// stripPrefix: true, +// selectorIndex: 0, +// }, +// ], +// }, +// async (context) => { +// // Get the actor IDs for the selector +// const selectorActors = +// context.actorsBySelector[context.selectors[0]]; +// const route = context.routes?.[0]; +// +// // First verify our route works +// console.log("Verifying route /test-path exists and works"); +// const result = await testRoute( +// context.hostname, +// "/test-path", +// 2, +// 20, +// route, +// ); +// if (result.size < 2) { +// throw new Error( +// "404 test setup failed: Could not reach actors at /test-path", +// ); +// } +// +// // Now test a path that doesn't match any route - should return 404 +// console.log("Testing path that should 404: /non-existent-path"); +// +// try { +// // We expect this to return an empty set (no matches) +// const notFoundResult = await testRoute( +// context.hostname, +// "/non-existent-path", +// 2, +// 5, +// ); +// +// // If we get here with results, it means the 404 test failed +// if (notFoundResult.size > 0) { +// console.error( +// "❌ Expected 404 for /non-existent-path but got a successful response", +// ); +// throw new Error( +// "404 test failed: Got a successful response instead of 404", +// ); +// } else { +// console.log( +// "✅ Correctly received no matches for non-existent path", +// ); +// } +// } catch (error) { +// // Check if this is our expected path validation error +// if ( +// error instanceof Error && +// error.message.includes("Path validation failed") +// ) { +// console.error( +// "❌ Unexpected path returned for non-existent route", +// ); +// throw error; +// } +// +// // If it's a different error (like connection refused), that's expected for a 404 +// console.log( +// "✅ Received expected error for non-existent path:", +// error.message, +// ); +// } +// +// // Test with a different hostname that doesn't have any routes +// const randomHostname = `nonexistent-${crypto.randomBytes(4).toString("hex")}.rivet-job.local`; +// console.log("Testing with non-existent hostname:", randomHostname); +// +// try { +// const nonExistentHostResult = await makeRequest( +// `http://localhost:7080/test`, +// randomHostname, +// ); +// +// // If we get a response, it's unexpected +// if (nonExistentHostResult) { +// console.error( +// "❌ Expected 404 for non-existent hostname but got a response:", +// nonExistentHostResult, +// ); +// throw new Error( +// "404 test failed: Got a response for non-existent hostname", +// ); +// } else { +// console.log( +// "✅ Correctly received no response for non-existent hostname", +// ); +// } +// } catch (error) { +// // This is expected - we should get an error +// console.log( +// "✅ Received expected error for non-existent hostname:", +// error.message, +// ); +// } +// }, +// ); +//} +// +//async function run() { +// try { +// const tests = [ +// { name: "Basic Route Test", fn: testBasicRoute }, +// { name: "Path Prefix Test", fn: testPathPrefix }, +// { name: "Exact Path Test", fn: testExactPath }, +// { name: "Path Priority Test", fn: testPathPriority }, +// { name: "Empty Path Test", fn: testEmptyPath }, +// { name: "No Strip Prefix Test", fn: testNoStripPrefix }, +// { name: "Multiple Routes Test", fn: testMultipleRoutes }, +// { name: "Query Parameters Test", fn: testQueryParameters }, +// { name: "Special Characters Test", fn: testSpecialCharacters }, +// { name: "Large Actor Pool Test", fn: testLargeActorPool }, +// { name: "Long Path Test", fn: testLongPath }, +// { +// name: "Mixed Prefix Stripping Test", +// fn: testMixedPrefixStripping, +// }, +// { name: "404 Response Test", fn: test404Response }, // 404 test should run last +// ]; +// +// for (const test of tests) { +// console.log(`\nRunning test: ${test.name}`); +// const testPassed = await test.fn(); +// +// // If any test fails, exit immediately +// if (!testPassed) { +// console.error( +// `\n❌ Test "${test.name}" failed. Exiting test suite.`, +// ); +// process.exit(1); +// } +// } +// +// console.log("\n=== All tests completed successfully ===\n"); +// process.exit(0); +// } catch (error) { +// console.error("Error running tests:", error); +// process.exit(1); +// } +//} +// +//// Run the test +//run().catch((error) => { +// console.error("Unhandled error in test suite:", error); +// process.exit(1); +//}); diff --git a/examples/system-test-route/yarn.lock b/examples/system-test-route/yarn.lock new file mode 100644 index 0000000000..973d700871 --- /dev/null +++ b/examples/system-test-route/yarn.lock @@ -0,0 +1,1325 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@esbuild/aix-ppc64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/aix-ppc64@npm:0.25.5" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/android-arm64@npm:0.25.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/android-arm@npm:0.25.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/android-x64@npm:0.25.5" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/darwin-arm64@npm:0.25.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/darwin-x64@npm:0.25.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/freebsd-arm64@npm:0.25.5" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/freebsd-x64@npm:0.25.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-arm64@npm:0.25.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-arm@npm:0.25.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-ia32@npm:0.25.5" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-loong64@npm:0.25.5" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-mips64el@npm:0.25.5" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-ppc64@npm:0.25.5" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-riscv64@npm:0.25.5" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-s390x@npm:0.25.5" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-x64@npm:0.25.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-arm64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/netbsd-arm64@npm:0.25.5" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/netbsd-x64@npm:0.25.5" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-arm64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/openbsd-arm64@npm:0.25.5" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/openbsd-x64@npm:0.25.5" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/sunos-x64@npm:0.25.5" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/win32-arm64@npm:0.25.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/win32-ia32@npm:0.25.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/win32-x64@npm:0.25.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@hono/node-server@npm:^1.13.8": + version: 1.14.3 + resolution: "@hono/node-server@npm:1.14.3" + peerDependencies: + hono: ^4 + checksum: 10c0/2e873ed38be44bd2499ffd6e592782edd9216475935d992119ad1d3f61f14cb39dc8fe79d1ed5d7d8caeef1d9fd84b36f9715add0b9f98c3d040b5cc9ef872f8 + languageName: node + linkType: hard + +"@hono/node-ws@npm:^1.1.0": + version: 1.1.6 + resolution: "@hono/node-ws@npm:1.1.6" + dependencies: + ws: "npm:^8.17.0" + peerDependencies: + "@hono/node-server": ^1.11.1 + hono: ^4.6.0 + checksum: 10c0/3995341e8b6e6a0e67ac11e54c42f2d6a84e1379ed0c2ce8af1a79128b1815b8cc9beb65e7917faa43a623ead7a27adf9cb3a0584377190bde287714cc2c10a0 + languageName: node + linkType: hard + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 10c0/b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e + languageName: node + linkType: hard + +"@isaacs/fs-minipass@npm:^4.0.0": + version: 4.0.1 + resolution: "@isaacs/fs-minipass@npm:4.0.1" + dependencies: + minipass: "npm:^7.0.4" + checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^3.0.0": + version: 3.0.0 + resolution: "@npmcli/agent@npm:3.0.0" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^10.0.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10c0/efe37b982f30740ee77696a80c196912c274ecd2cb243bc6ae7053a50c733ce0f6c09fda085145f33ecf453be19654acca74b69e81eaad4c90f00ccffe2f9271 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/fs@npm:4.0.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10c0/c90935d5ce670c87b6b14fab04a965a3b8137e585f8b2a6257263bd7f97756dd736cb165bb470e5156a9e718ecd99413dccc54b1138c1a46d6ec7cf325982fe5 + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 10c0/5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd + languageName: node + linkType: hard + +"@rivet-gg/actor-core@npm:^5.1.2": + version: 5.1.3 + resolution: "@rivet-gg/actor-core@npm:5.1.3" + dependencies: + zod: "npm:^3.24.1" + checksum: 10c0/9723a95f2fc0701b8ebe2743378d49d9b37ffc8055cbdbf8dd7ff3ba40484c4b7e0c20caa3d34e1aed07d0a96adf654d417593fae2d01204d34bbe2a09f51a1d + languageName: node + linkType: hard + +"@types/deno@npm:^2.2.0": + version: 2.3.0 + resolution: "@types/deno@npm:2.3.0" + checksum: 10c0/7b4c9a2f905f1e737dcafffc8475575bd1647cbc3da140d71965f9a3289e34b6f3f2dc71c1e28f664aba6e999c0e9499d522329f734822bc4b11dd2c4f6342bc + languageName: node + linkType: hard + +"@types/node@npm:*, @types/node@npm:^22.13.9": + version: 22.15.29 + resolution: "@types/node@npm:22.15.29" + dependencies: + undici-types: "npm:~6.21.0" + checksum: 10c0/602cc88c6150780cd9b5b44604754e0ce13983ae876a538861d6ecfb1511dff289e5576fffd26c841cde2142418d4bb76e2a72a382b81c04557ccb17cff29e1d + languageName: node + linkType: hard + +"@types/ws@npm:^8.18.0": + version: 8.18.1 + resolution: "@types/ws@npm:8.18.1" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/61aff1129143fcc4312f083bc9e9e168aa3026b7dd6e70796276dcfb2c8211c4292603f9c4864fae702f2ed86e4abd4d38aa421831c2fd7f856c931a481afbab + languageName: node + linkType: hard + +"abbrev@npm:^3.0.0": + version: 3.0.1 + resolution: "abbrev@npm:3.0.1" + checksum: 10c0/21ba8f574ea57a3106d6d35623f2c4a9111d9ee3e9a5be47baed46ec2457d2eac46e07a5c4a60186f88cb98abbe3e24f2d4cca70bc2b12f1692523e2209a9ccf + languageName: node + linkType: hard + +"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": + version: 7.1.3 + resolution: "agent-base@npm:7.1.3" + checksum: 10c0/6192b580c5b1d8fb399b9c62bf8343d76654c2dd62afcb9a52b2cf44a8b6ace1e3b704d3fe3547d91555c857d3df02603341ff2cb961b9cfe2b12f9f3c38ee11 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.1.0 + resolution: "ansi-regex@npm:6.1.0" + checksum: 10c0/a91daeddd54746338478eef88af3439a7edf30f8e23196e2d6ed182da9add559c601266dbef01c2efa46a958ad6f1f8b176799657616c702b5b02e799e7fd8dc + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 + languageName: node + linkType: hard + +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: "npm:^1.0.0" + checksum: 10c0/b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f + languageName: node + linkType: hard + +"cacache@npm:^19.0.1": + version: 19.0.1 + resolution: "cacache@npm:19.0.1" + dependencies: + "@npmcli/fs": "npm:^4.0.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^10.0.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^7.0.2" + ssri: "npm:^12.0.0" + tar: "npm:^7.4.3" + unique-filename: "npm:^4.0.0" + checksum: 10c0/01f2134e1bd7d3ab68be851df96c8d63b492b1853b67f2eecb2c37bb682d37cb70bb858a16f2f0554d3c0071be6dfe21456a1ff6fa4b7eed996570d6a25ffe9c + languageName: node + linkType: hard + +"chownr@npm:^3.0.0": + version: 3.0.0 + resolution: "chownr@npm:3.0.0" + checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10 + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: "npm:~1.1.4" + checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.6": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 + languageName: node + linkType: hard + +"data-uri-to-buffer@npm:^4.0.0": + version: 4.0.1 + resolution: "data-uri-to-buffer@npm:4.0.1" + checksum: 10c0/20a6b93107597530d71d4cb285acee17f66bcdfc03fd81040921a81252f19db27588d87fc8fc69e1950c55cfb0bf8ae40d0e5e21d907230813eb5d5a7f9eb45b + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.4": + version: 4.4.1 + resolution: "debug@npm:4.4.1" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d2b44bc1afd912b49bb7ebb0d50a860dc93a4dd7d946e8de94abc957bb63726b7dd5aa48c18c2386c379ec024c46692e15ed3ed97d481729f929201e671fcd55 + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 10c0/26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: 10c0/b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 10c0/af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: 10c0/36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 10c0/b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + languageName: node + linkType: hard + +"esbuild@npm:~0.25.0": + version: 0.25.5 + resolution: "esbuild@npm:0.25.5" + dependencies: + "@esbuild/aix-ppc64": "npm:0.25.5" + "@esbuild/android-arm": "npm:0.25.5" + "@esbuild/android-arm64": "npm:0.25.5" + "@esbuild/android-x64": "npm:0.25.5" + "@esbuild/darwin-arm64": "npm:0.25.5" + "@esbuild/darwin-x64": "npm:0.25.5" + "@esbuild/freebsd-arm64": "npm:0.25.5" + "@esbuild/freebsd-x64": "npm:0.25.5" + "@esbuild/linux-arm": "npm:0.25.5" + "@esbuild/linux-arm64": "npm:0.25.5" + "@esbuild/linux-ia32": "npm:0.25.5" + "@esbuild/linux-loong64": "npm:0.25.5" + "@esbuild/linux-mips64el": "npm:0.25.5" + "@esbuild/linux-ppc64": "npm:0.25.5" + "@esbuild/linux-riscv64": "npm:0.25.5" + "@esbuild/linux-s390x": "npm:0.25.5" + "@esbuild/linux-x64": "npm:0.25.5" + "@esbuild/netbsd-arm64": "npm:0.25.5" + "@esbuild/netbsd-x64": "npm:0.25.5" + "@esbuild/openbsd-arm64": "npm:0.25.5" + "@esbuild/openbsd-x64": "npm:0.25.5" + "@esbuild/sunos-x64": "npm:0.25.5" + "@esbuild/win32-arm64": "npm:0.25.5" + "@esbuild/win32-ia32": "npm:0.25.5" + "@esbuild/win32-x64": "npm:0.25.5" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/aba8cbc11927fa77562722ed5e95541ce2853f67ad7bdc40382b558abc2e0ec57d92ffb820f082ba2047b4ef9f3bc3da068cdebe30dfd3850cfa3827a78d604e + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.2 + resolution: "exponential-backoff@npm:3.1.2" + checksum: 10c0/d9d3e1eafa21b78464297df91f1776f7fbaa3d5e3f7f0995648ca5b89c069d17055033817348d9f4a43d1c20b0eab84f75af6991751e839df53e4dfd6f22e844 + languageName: node + linkType: hard + +"fdir@npm:^6.4.4": + version: 6.4.5 + resolution: "fdir@npm:6.4.5" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/5d63330a1b97165e9b0fb20369fafc7cf826bc4b3e374efcb650bc77d7145ac01193b5da1a7591eab89ae6fd6b15cdd414085910b2a2b42296b1480c9f2677af + languageName: node + linkType: hard + +"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": + version: 3.2.0 + resolution: "fetch-blob@npm:3.2.0" + dependencies: + node-domexception: "npm:^1.0.0" + web-streams-polyfill: "npm:^3.0.3" + checksum: 10c0/60054bf47bfa10fb0ba6cb7742acec2f37c1f56344f79a70bb8b1c48d77675927c720ff3191fa546410a0442c998d27ab05e9144c32d530d8a52fbe68f843b69 + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.3.1 + resolution: "foreground-child@npm:3.3.1" + dependencies: + cross-spawn: "npm:^7.0.6" + signal-exit: "npm:^4.0.1" + checksum: 10c0/8986e4af2430896e65bc2788d6679067294d6aee9545daefc84923a0a4b399ad9c7a3ea7bd8c0b2b80fdf4a92de4c69df3f628233ff3224260e9c1541a9e9ed3 + languageName: node + linkType: hard + +"formdata-polyfill@npm:^4.0.10": + version: 4.0.10 + resolution: "formdata-polyfill@npm:4.0.10" + dependencies: + fetch-blob: "npm:^3.1.2" + checksum: 10c0/5392ec484f9ce0d5e0d52fb5a78e7486637d516179b0eb84d81389d7eccf9ca2f663079da56f761355c0a65792810e3b345dc24db9a8bbbcf24ef3c8c88570c6 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 + languageName: node + linkType: hard + +"fsevents@npm:~2.3.3": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + +"get-tsconfig@npm:^4.7.5": + version: 4.10.1 + resolution: "get-tsconfig@npm:4.10.1" + dependencies: + resolve-pkg-maps: "npm:^1.0.0" + checksum: 10c0/7f8e3dabc6a49b747920a800fb88e1952fef871cdf51b79e98db48275a5de6cdaf499c55ee67df5fa6fe7ce65f0063e26de0f2e53049b408c585aa74d39ffa21 + languageName: node + linkType: hard + +"glob@npm:^10.2.2": + version: 10.4.5 + resolution: "glob@npm:10.4.5" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^1.11.1" + bin: + glob: dist/esm/bin.mjs + checksum: 10c0/19a9759ea77b8e3ca0a43c2f07ecddc2ad46216b786bb8f993c445aee80d345925a21e5280c7b7c6c59e860a0154b84e4b2b60321fea92cd3c56b4a7489f160e + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + languageName: node + linkType: hard + +"hono@npm:^4.6.17": + version: 4.7.11 + resolution: "hono@npm:4.7.11" + checksum: 10c0/2821471b09f2e9f7bab5ad7412e2e44df5f07b7098508d70dd6e368b933580f03a06c30e20dd764b53e0121e1b1ff2132ae98cffa7fd1b286e299f9054effd6a + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.2.0 + resolution: "http-cache-semantics@npm:4.2.0" + checksum: 10c0/45b66a945cf13ec2d1f29432277201313babf4a01d9e52f44b31ca923434083afeca03f18417f599c9ab3d0e7b618ceb21257542338b57c54b710463b4a53e37 + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.6 + resolution: "https-proxy-agent@npm:7.0.6" + dependencies: + agent-base: "npm:^7.1.2" + debug: "npm:4" + checksum: 10c0/f729219bc735edb621fa30e6e84e60ee5d00802b8247aac0d7b79b0bd6d4b3294737a337b93b86a0bd9e68099d031858a39260c976dc14cdbba238ba1f8779ac + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + languageName: node + linkType: hard + +"ip-address@npm:^9.0.5": + version: 9.0.5 + resolution: "ip-address@npm:9.0.5" + dependencies: + jsbn: "npm:1.1.0" + sprintf-js: "npm:^1.1.3" + checksum: 10c0/331cd07fafcb3b24100613e4b53e1a2b4feab11e671e655d46dc09ee233da5011284d09ca40c4ecbdfe1d0004f462958675c224a804259f2f78d2465a87824bc + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: 10c0/bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + languageName: node + linkType: hard + +"jackspeak@npm:^3.1.2": + version: 3.4.3 + resolution: "jackspeak@npm:3.4.3" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 10c0/6acc10d139eaefdbe04d2f679e6191b3abf073f111edf10b1de5302c97ec93fffeb2fdd8681ed17f16268aa9dd4f8c588ed9d1d3bffbbfa6e8bf897cbb3149b9 + languageName: node + linkType: hard + +"jsbn@npm:1.1.0": + version: 1.1.0 + resolution: "jsbn@npm:1.1.0" + checksum: 10c0/4f907fb78d7b712e11dea8c165fe0921f81a657d3443dde75359ed52eb2b5d33ce6773d97985a089f09a65edd80b11cb75c767b57ba47391fee4c969f7215c96 + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": + version: 10.4.3 + resolution: "lru-cache@npm:10.4.3" + checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb + languageName: node + linkType: hard + +"make-fetch-happen@npm:^14.0.3": + version: 14.0.3 + resolution: "make-fetch-happen@npm:14.0.3" + dependencies: + "@npmcli/agent": "npm:^3.0.0" + cacache: "npm:^19.0.1" + http-cache-semantics: "npm:^4.1.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^4.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^1.0.0" + proc-log: "npm:^5.0.0" + promise-retry: "npm:^2.0.1" + ssri: "npm:^12.0.0" + checksum: 10c0/c40efb5e5296e7feb8e37155bde8eb70bc57d731b1f7d90e35a092fde403d7697c56fb49334d92d330d6f1ca29a98142036d6480a12681133a0a1453164cb2f0 + languageName: node + linkType: hard + +"minimatch@npm:^9.0.4": + version: 9.0.5 + resolution: "minimatch@npm:9.0.5" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10c0/de96cf5e35bdf0eab3e2c853522f98ffbe9a36c37797778d2665231ec1f20a9447a7e567cb640901f89e4daaa95ae5d70c65a9e8aa2bb0019b6facbc3c0575ed + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + languageName: node + linkType: hard + +"minipass-fetch@npm:^4.0.0": + version: 4.0.1 + resolution: "minipass-fetch@npm:4.0.1" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^3.0.1" + dependenciesMeta: + encoding: + optional: true + checksum: 10c0/a3147b2efe8e078c9bf9d024a0059339c5a09c5b1dded6900a219c218cc8b1b78510b62dae556b507304af226b18c3f1aeb1d48660283602d5b6586c399eed5c + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": + version: 7.1.2 + resolution: "minipass@npm:7.1.2" + checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557 + languageName: node + linkType: hard + +"minizlib@npm:^3.0.1": + version: 3.0.2 + resolution: "minizlib@npm:3.0.2" + dependencies: + minipass: "npm:^7.1.2" + checksum: 10c0/9f3bd35e41d40d02469cb30470c55ccc21cae0db40e08d1d0b1dff01cc8cc89a6f78e9c5d2b7c844e485ec0a8abc2238111213fdc5b2038e6d1012eacf316f78 + languageName: node + linkType: hard + +"mkdirp@npm:^3.0.1": + version: 3.0.1 + resolution: "mkdirp@npm:3.0.1" + bin: + mkdirp: dist/cjs/src/bin.js + checksum: 10c0/9f2b975e9246351f5e3a40dcfac99fcd0baa31fbfab615fe059fb11e51f10e4803c63de1f384c54d656e4db31d000e4767e9ef076a22e12a641357602e31d57d + languageName: node + linkType: hard + +"ms@npm:^2.1.3": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 + languageName: node + linkType: hard + +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b + languageName: node + linkType: hard + +"node-domexception@npm:^1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: 10c0/5e5d63cda29856402df9472335af4bb13875e1927ad3be861dc5ebde38917aecbf9ae337923777af52a48c426b70148815e890a5d72760f1b4d758cc671b1a2b + languageName: node + linkType: hard + +"node-fetch@npm:^3.3.2": + version: 3.3.2 + resolution: "node-fetch@npm:3.3.2" + dependencies: + data-uri-to-buffer: "npm:^4.0.0" + fetch-blob: "npm:^3.1.4" + formdata-polyfill: "npm:^4.0.10" + checksum: 10c0/f3d5e56190562221398c9f5750198b34cf6113aa304e34ee97c94fd300ec578b25b2c2906edba922050fce983338fde0d5d34fcb0fc3336ade5bd0e429ad7538 + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 11.2.0 + resolution: "node-gyp@npm:11.2.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^14.0.3" + nopt: "npm:^8.0.0" + proc-log: "npm:^5.0.0" + semver: "npm:^7.3.5" + tar: "npm:^7.4.3" + tinyglobby: "npm:^0.2.12" + which: "npm:^5.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10c0/bd8d8c76b06be761239b0c8680f655f6a6e90b48e44d43415b11c16f7e8c15be346fba0cbf71588c7cdfb52c419d928a7d3db353afc1d952d19756237d8f10b9 + languageName: node + linkType: hard + +"nopt@npm:^8.0.0": + version: 8.1.0 + resolution: "nopt@npm:8.1.0" + dependencies: + abbrev: "npm:^3.0.0" + bin: + nopt: bin/nopt.js + checksum: 10c0/62e9ea70c7a3eb91d162d2c706b6606c041e4e7b547cbbb48f8b3695af457dd6479904d7ace600856bf923dd8d1ed0696f06195c8c20f02ac87c1da0e1d315ef + languageName: node + linkType: hard + +"p-map@npm:^7.0.2": + version: 7.0.3 + resolution: "p-map@npm:7.0.3" + checksum: 10c0/46091610da2b38ce47bcd1d8b4835a6fa4e832848a6682cf1652bc93915770f4617afc844c10a77d1b3e56d2472bb2d5622353fa3ead01a7f42b04fc8e744a5c + languageName: node + linkType: hard + +"package-json-from-dist@npm:^1.0.0": + version: 1.0.1 + resolution: "package-json-from-dist@npm:1.0.1" + checksum: 10c0/62ba2785eb655fec084a257af34dbe24292ab74516d6aecef97ef72d4897310bc6898f6c85b5cd22770eaa1ce60d55a0230e150fb6a966e3ecd6c511e23d164b + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + languageName: node + linkType: hard + +"path-scurry@npm:^1.11.1": + version: 1.11.1 + resolution: "path-scurry@npm:1.11.1" + dependencies: + lru-cache: "npm:^10.2.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d + languageName: node + linkType: hard + +"picomatch@npm:^4.0.2": + version: 4.0.2 + resolution: "picomatch@npm:4.0.2" + checksum: 10c0/7c51f3ad2bb42c776f49ebf964c644958158be30d0a510efd5a395e8d49cb5acfed5b82c0c5b365523ce18e6ab85013c9ebe574f60305892ec3fa8eee8304ccc + languageName: node + linkType: hard + +"proc-log@npm:^5.0.0": + version: 5.0.0 + resolution: "proc-log@npm:5.0.0" + checksum: 10c0/bbe5edb944b0ad63387a1d5b1911ae93e05ce8d0f60de1035b218cdcceedfe39dbd2c697853355b70f1a090f8f58fe90da487c85216bf9671f9499d1a897e9e3 + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 10c0/9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 + languageName: node + linkType: hard + +"resolve-pkg-maps@npm:^1.0.0": + version: 1.0.0 + resolution: "resolve-pkg-maps@npm:1.0.0" + checksum: 10c0/fb8f7bbe2ca281a73b7ef423a1cbc786fb244bd7a95cbe5c3fba25b27d327150beca8ba02f622baea65919a57e061eb5005204daa5f93ed590d9b77463a567ab + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 10c0/59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + languageName: node + linkType: hard + +"semver@npm:^7.3.5": + version: 7.7.2 + resolution: "semver@npm:7.7.2" + bin: + semver: bin/semver.js + checksum: 10c0/aca305edfbf2383c22571cb7714f48cadc7ac95371b4b52362fb8eeffdfbc0de0669368b82b2b15978f8848f01d7114da65697e56cd8c37b0dab8c58e543f9ea + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.5 + resolution: "socks-proxy-agent@npm:8.0.5" + dependencies: + agent-base: "npm:^7.1.2" + debug: "npm:^4.3.4" + socks: "npm:^2.8.3" + checksum: 10c0/5d2c6cecba6821389aabf18728325730504bf9bb1d9e342e7987a5d13badd7a98838cc9a55b8ed3cb866ad37cc23e1086f09c4d72d93105ce9dfe76330e9d2a6 + languageName: node + linkType: hard + +"socks@npm:^2.8.3": + version: 2.8.4 + resolution: "socks@npm:2.8.4" + dependencies: + ip-address: "npm:^9.0.5" + smart-buffer: "npm:^4.2.0" + checksum: 10c0/00c3271e233ccf1fb83a3dd2060b94cc37817e0f797a93c560b9a7a86c4a0ec2961fb31263bdd24a3c28945e24868b5f063cd98744171d9e942c513454b50ae5 + languageName: node + linkType: hard + +"sprintf-js@npm:^1.1.3": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: 10c0/09270dc4f30d479e666aee820eacd9e464215cdff53848b443964202bf4051490538e5dd1b42e1a65cf7296916ca17640aebf63dae9812749c7542ee5f288dec + languageName: node + linkType: hard + +"ssri@npm:^12.0.0": + version: 12.0.0 + resolution: "ssri@npm:12.0.0" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/caddd5f544b2006e88fa6b0124d8d7b28208b83c72d7672d5ade44d794525d23b540f3396108c4eb9280dcb7c01f0bef50682f5b4b2c34291f7c5e211fd1417d + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: 10c0/1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 10c0/a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 + languageName: node + linkType: hard + +"system-test-route@workspace:.": + version: 0.0.0-use.local + resolution: "system-test-route@workspace:." + dependencies: + "@hono/node-server": "npm:^1.13.8" + "@hono/node-ws": "npm:^1.1.0" + "@rivet-gg/actor-core": "npm:^5.1.2" + "@types/deno": "npm:^2.2.0" + "@types/node": "npm:^22.13.9" + "@types/ws": "npm:^8.18.0" + hono: "npm:^4.6.17" + node-fetch: "npm:^3.3.2" + tsx: "npm:^4.7.0" + typescript: "npm:^5.3.3" + ws: "npm:^8.18.1" + languageName: unknown + linkType: soft + +"tar@npm:^7.4.3": + version: 7.4.3 + resolution: "tar@npm:7.4.3" + dependencies: + "@isaacs/fs-minipass": "npm:^4.0.0" + chownr: "npm:^3.0.0" + minipass: "npm:^7.1.2" + minizlib: "npm:^3.0.1" + mkdirp: "npm:^3.0.1" + yallist: "npm:^5.0.0" + checksum: 10c0/d4679609bb2a9b48eeaf84632b6d844128d2412b95b6de07d53d8ee8baf4ca0857c9331dfa510390a0727b550fd543d4d1a10995ad86cdf078423fbb8d99831d + languageName: node + linkType: hard + +"tinyglobby@npm:^0.2.12": + version: 0.2.14 + resolution: "tinyglobby@npm:0.2.14" + dependencies: + fdir: "npm:^6.4.4" + picomatch: "npm:^4.0.2" + checksum: 10c0/f789ed6c924287a9b7d3612056ed0cda67306cd2c80c249fd280cf1504742b12583a2089b61f4abbd24605f390809017240e250241f09938054c9b363e51c0a6 + languageName: node + linkType: hard + +"tsx@npm:^4.7.0": + version: 4.19.4 + resolution: "tsx@npm:4.19.4" + dependencies: + esbuild: "npm:~0.25.0" + fsevents: "npm:~2.3.3" + get-tsconfig: "npm:^4.7.5" + dependenciesMeta: + fsevents: + optional: true + bin: + tsx: dist/cli.mjs + checksum: 10c0/f7b8d44362343fbde1f2ecc9832d243a450e1168dd09702a545ebe5f699aa6912e45b431a54b885466db414cceda48e5067b36d182027c43b2c02a4f99d8721e + languageName: node + linkType: hard + +"typescript@npm:^5.3.3": + version: 5.8.3 + resolution: "typescript@npm:5.8.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48 + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^5.3.3#optional!builtin": + version: 5.8.3 + resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb + languageName: node + linkType: hard + +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04 + languageName: node + linkType: hard + +"unique-filename@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-filename@npm:4.0.0" + dependencies: + unique-slug: "npm:^5.0.0" + checksum: 10c0/38ae681cceb1408ea0587b6b01e29b00eee3c84baee1e41fd5c16b9ed443b80fba90c40e0ba69627e30855570a34ba8b06702d4a35035d4b5e198bf5a64c9ddc + languageName: node + linkType: hard + +"unique-slug@npm:^5.0.0": + version: 5.0.0 + resolution: "unique-slug@npm:5.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 10c0/d324c5a44887bd7e105ce800fcf7533d43f29c48757ac410afd42975de82cc38ea2035c0483f4de82d186691bf3208ef35c644f73aa2b1b20b8e651be5afd293 + languageName: node + linkType: hard + +"web-streams-polyfill@npm:^3.0.3": + version: 3.3.3 + resolution: "web-streams-polyfill@npm:3.3.3" + checksum: 10c0/64e855c47f6c8330b5436147db1c75cb7e7474d924166800e8e2aab5eb6c76aac4981a84261dd2982b3e754490900b99791c80ae1407a9fa0dcff74f82ea3a7f + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + languageName: node + linkType: hard + +"which@npm:^5.0.0": + version: 5.0.0 + resolution: "which@npm:5.0.0" + dependencies: + isexe: "npm:^3.1.1" + bin: + node-which: bin/which.js + checksum: 10c0/e556e4cd8b7dbf5df52408c9a9dd5ac6518c8c5267c8953f5b0564073c66ed5bf9503b14d876d0e9c7844d4db9725fb0dcf45d6e911e17e26ab363dc3965ae7b + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10c0/d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 + languageName: node + linkType: hard + +"ws@npm:^8.17.0, ws@npm:^8.18.1": + version: 8.18.2 + resolution: "ws@npm:8.18.2" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/4b50f67931b8c6943c893f59c524f0e4905bbd183016cfb0f2b8653aa7f28dad4e456b9d99d285bbb67cca4fedd9ce90dfdfaa82b898a11414ebd66ee99141e4 + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + languageName: node + linkType: hard + +"yallist@npm:^5.0.0": + version: 5.0.0 + resolution: "yallist@npm:5.0.0" + checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 + languageName: node + linkType: hard + +"zod@npm:^3.24.1": + version: 3.25.50 + resolution: "zod@npm:3.25.50" + checksum: 10c0/88f8e73198ebe986b1640610e8abf9b3fde1db5de1d1dcfe21138b4712e6d9e875521a15538c70b5eb4c52712f8d6261794e0716ad57b213c3b328e1bc423986 + languageName: node + linkType: hard