diff --git a/.changeset/config.json b/.changeset/config.json index 5e8716336b..20abe07644 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -14,6 +14,8 @@ "ignore": [ "webapp", "emails", + "proxy", + "yalt", "@trigger.dev/database" ], "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { diff --git a/.changeset/new-panthers-do.md b/.changeset/new-panthers-do.md new file mode 100644 index 0000000000..5cb40d4eb8 --- /dev/null +++ b/.changeset/new-panthers-do.md @@ -0,0 +1,6 @@ +--- +"@trigger.dev/yalt": patch +"@trigger.dev/cli": patch +--- + +Add support for the built-in Trigger.dev tunnel (Yalt) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 2027f880d4..0af231049f 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -64,6 +64,9 @@ const EnvironmentSchema = z.object({ DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(10), DEFAULT_DEV_ENV_EXECUTION_ATTEMPTS: z.coerce.number().int().positive().default(1), + + TUNNEL_HOST: z.string().optional(), + TUNNEL_SECRET_KEY: z.string().optional(), }); export type Environment = z.infer; diff --git a/apps/webapp/app/routes/api.v1.tunnels.ts b/apps/webapp/app/routes/api.v1.tunnels.ts new file mode 100644 index 0000000000..6c0986159f --- /dev/null +++ b/apps/webapp/app/routes/api.v1.tunnels.ts @@ -0,0 +1,69 @@ +import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { json } from "@remix-run/server-runtime"; +import { env } from "~/env.server"; +import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { YaltApiClient } from "@trigger.dev/yalt"; +import { logger } from "~/services/logger.server"; +import { prisma } from "~/db.server"; + +// This is for HEAD requests to check if the API supports tunneling +export async function loader({ request }: LoaderFunctionArgs) { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); + + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } + + if (!env.TUNNEL_HOST || !env.TUNNEL_SECRET_KEY) { + return json({ error: "Tunneling is not supported" }, { status: 501 }); + } + + return json({ ok: true }); +} + +export async function action({ request }: LoaderFunctionArgs) { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); + + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } + + if (authenticationResult.environment.type !== "DEVELOPMENT") { + return json({ error: "Tunneling is only supported in development" }, { status: 501 }); + } + + if (!env.TUNNEL_HOST || !env.TUNNEL_SECRET_KEY) { + return json({ error: "Tunneling is not supported" }, { status: 501 }); + } + + const yaltClient = new YaltApiClient(env.TUNNEL_HOST, env.TUNNEL_SECRET_KEY); + + let tunnelId = authenticationResult.environment.tunnelId; + + if (!tunnelId) { + try { + tunnelId = await yaltClient.createTunnel(); + + await prisma.runtimeEnvironment.update({ + where: { + id: authenticationResult.environment.id, + }, + data: { + tunnelId, + }, + }); + } catch (error) { + logger.error("Failed to create tunnel", { error }); + + return json({ error: "Failed to create tunnel" }, { status: 500 }); + } + } + + if (!tunnelId) { + return json({ error: "Failed to create tunnel" }, { status: 500 }); + } + + return json({ url: yaltClient.connectUrl(tunnelId) }); +} diff --git a/apps/webapp/package.json b/apps/webapp/package.json index c2b2c41c6e..d3fd085421 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -72,6 +72,7 @@ "@trigger.dev/core-backend": "workspace:*", "@trigger.dev/database": "workspace:*", "@trigger.dev/sdk": "workspace:*", + "@trigger.dev/yalt": "workspace:*", "@types/pg": "8.6.6", "@uiw/react-codemirror": "^4.19.5", "@whatwg-node/fetch": "^0.9.14", diff --git a/apps/webapp/remix.config.js b/apps/webapp/remix.config.js index e1e0c2d40f..d183b450b9 100644 --- a/apps/webapp/remix.config.js +++ b/apps/webapp/remix.config.js @@ -15,6 +15,7 @@ module.exports = { "@trigger.dev/core-backend", "@trigger.dev/sdk", "@trigger.dev/billing", + "@trigger.dev/yalt", "emails", "highlight.run", "random-words", @@ -24,6 +25,7 @@ module.exports = { "../../packages/core/src/**/*", "../../packages/core-backend/src/**/*", "../../packages/trigger-sdk/src/**/*", + "../../packages/yalt/src/**/*", "../../packages/emails/src/**/*", ]; }, diff --git a/apps/webapp/tsconfig.json b/apps/webapp/tsconfig.json index bd08a2f277..de26bf4b1b 100644 --- a/apps/webapp/tsconfig.json +++ b/apps/webapp/tsconfig.json @@ -27,6 +27,8 @@ "@trigger.dev/core-backend/*": ["../../packages/core-backend/src/*"], "@trigger.dev/database": ["../../packages/database/src/index"], "@trigger.dev/database/*": ["../../packages/database/src/*"], + "@trigger.dev/yalt": ["../../packages/yalt/src/index"], + "@trigger.dev/yalt/*": ["../../packages/yalt/src/*"], "emails": ["../../packages/emails/src/index"], "emails/*": ["../../packages/emails/src/*"] }, diff --git a/apps/yalt/.dev.vars.example b/apps/yalt/.dev.vars.example new file mode 100644 index 0000000000..77bf4c3338 --- /dev/null +++ b/apps/yalt/.dev.vars.example @@ -0,0 +1 @@ +SECRET_KEY= \ No newline at end of file diff --git a/apps/yalt/.editorconfig b/apps/yalt/.editorconfig new file mode 100644 index 0000000000..64ab2601f9 --- /dev/null +++ b/apps/yalt/.editorconfig @@ -0,0 +1,13 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = tab +tab_width = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_style = space diff --git a/apps/yalt/.gitignore b/apps/yalt/.gitignore new file mode 100644 index 0000000000..3b0fe33c47 --- /dev/null +++ b/apps/yalt/.gitignore @@ -0,0 +1,172 @@ +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +# wrangler project + +.dev.vars +.wrangler/ diff --git a/apps/yalt/.prettierrc b/apps/yalt/.prettierrc new file mode 100644 index 0000000000..5c7b5d3c7a --- /dev/null +++ b/apps/yalt/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 140, + "singleQuote": true, + "semi": true, + "useTabs": true +} diff --git a/apps/yalt/README.md b/apps/yalt/README.md new file mode 100644 index 0000000000..d4604f0db4 --- /dev/null +++ b/apps/yalt/README.md @@ -0,0 +1,26 @@ +## Trigger.dev Yalt Server + +Yalt (Yet Another Local Tunnel) is the Trigger.dev tunneling service that powers the local development of Trigger.dev cloud users. + +## Why? + +The Trigger.dev server communicates with user endpoints over HTTP, so during local development we need a way for the Trigger.dev server to make HTTP requests over the public internet to the user's local machine. This is accomplished via a tunneling service like ngrok, which we've been using up until now. Unfortunately, the ngrok free plan has pretty aggressive rate limits and some Trigger.dev users have faced issues with their local jobs not running/working because of this. Yalt.dev is our solution. + +## How does it work? + +Yalt.dev is a Cloudflare Worker that uses Durable Objects to persist a websocket connection from the `@trigger.dev/cli dev` command and proxies requests through the websocket connection and then returns responses from the websocket connection. + +- There is an admin API available at `admin.trigger.dev` that allows tunnels to be created (authenticated via a SECRET_KEY) +- The Cloudflare Worker has a wildcard subdomain route `*.yalt.dev/*` +- When the client receives the tunnel ID, they can connect to the server via `wss://${tunnelId}.yalt.dev/connect` +- Now, requests to `https://${tunnelId}.yalt.dev/api/trigger` are sent to the Durable Object (`YaltConnection`) using the subdomain/tunnelId +- Requests are serialized to a JSON string and sent to the WebSocket +- The WebSocket client running in `@trigger.dev/cli dev` receives the request message and makes a real request to the local dev server +- The `@trigger.dev/cli dev` serializes the response and sends it back to the server. +- The server responds to the original request + +Along with this server there is a package called `@trigger.dev/yalt` that has shared code and is used in: + +- Yalt.dev server (this project) +- `@trigger.dev/cli dev` command +- The Trigger.dev server (to create the tunnels) diff --git a/apps/yalt/package.json b/apps/yalt/package.json new file mode 100644 index 0000000000..21ca1304e4 --- /dev/null +++ b/apps/yalt/package.json @@ -0,0 +1,17 @@ +{ + "name": "yalt", + "version": "0.0.1", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev --port 8787", + "start": "wrangler dev" + }, + "devDependencies": { + "@cloudflare/workers-types": "~4.20231121.0", + "wrangler": "^3.20.0" + }, + "dependencies": { + "@trigger.dev/yalt": "workspace:*" + } +} \ No newline at end of file diff --git a/apps/yalt/src/index.ts b/apps/yalt/src/index.ts new file mode 100644 index 0000000000..6c1b3b0525 --- /dev/null +++ b/apps/yalt/src/index.ts @@ -0,0 +1,258 @@ +import { ClientMessages, createRequestMessage } from '@trigger.dev/yalt'; + +export interface Env { + // environemnt variables + SECRET_KEY: string; + WORKER_HOST: string; + + // bindings + connections: DurableObjectNamespace; + tunnelIds: KVNamespace; +} + +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const url = new URL(request.url); + const route = routeUrl(url, env); + + switch (route.type) { + case 'management': { + return handleManagementRequest(request, env, ctx); + } + case 'tunnel': { + console.log(`Handling tunnel request for ${route.name}`); + + const id = await env.tunnelIds.get(route.name); + + if (!id) { + return new Response('Not Found', { status: 404 }); + } + + const tunnel = env.connections.get(env.connections.idFromString(id)); + return tunnel.fetch(request); + } + case 'not_found': { + return new Response('Not Found', { status: 404 }); + } + } + + return new Response('Not Found', { status: 404 }); + }, +}; + +type RouteDecision = + | { + type: 'management'; + } + | { + type: 'tunnel'; + name: string; + } + | { type: 'not_found' }; + +function routeUrl(url: URL, env: Env): RouteDecision { + if (url.host.startsWith('localhost:')) { + const searchParams = new URLSearchParams(url.search); + const name = searchParams.get('t'); + + if (name) { + return { type: 'tunnel', name }; + } + + return { type: 'management' }; + } + + if (!url.host.includes(env.WORKER_HOST)) { + return { type: 'not_found' }; + } + + const parts = url.host.split('.'); + + if (parts.length === 2) { + return { type: 'management' }; + } + + const tunnelName = parts[0]; + + if (tunnelName === 'admin') { + return { type: 'management' }; + } + + if (parts.length === 3) { + return { type: 'tunnel', name: parts[0] }; + } + + return { type: 'not_found' }; +} + +async function handleManagementRequest(request: Request, env: Env, ctx: ExecutionContext): Promise { + const authHeader = request.headers.get('authorization'); + if (!authHeader) { + return new Response('Authorization header is required', { status: 401 }); + } + + const authHeaderParts = authHeader.split(' '); + if (authHeaderParts.length !== 2) { + return new Response('Authorization header is invalid', { status: 401 }); + } + + const [authType, authKey] = authHeaderParts; + + if (authType !== 'Bearer') { + return new Response('Authorization header is invalid', { status: 401 }); + } + + if (authKey !== env.SECRET_KEY) { + return new Response('Authorization header is invalid', { status: 401 }); + } + + // Okay now we can actually handle the request + // We need to look at the path and see what we are doing + // POST /api/tunnels -> create a new tunnel + const url = new URL(request.url); + + if (url.pathname === '/api/tunnels' && request.method === 'POST') { + return handleCreateTunnel(request, env, ctx); + } + + return new Response('Not Found', { status: 404 }); +} + +async function handleCreateTunnel(request: Request, env: Env, ctx: ExecutionContext): Promise { + const tunnelId = env.connections.newUniqueId(); + const tunnelName = crypto.randomUUID(); + + await env.tunnelIds.put(tunnelName, tunnelId.toString()); + + return new Response(JSON.stringify({ id: tunnelName }), { status: 201 }); +} + +type Resolver = { + resolve: (value: T) => void; + reject: (error: Error) => void; +}; + +export class YaltConnection implements DurableObject { + private socket?: WebSocket; + private responseResolvers: Record> = {}; + + constructor( + private state: DurableObjectState, + private env: Env, + ) {} + + async fetch(request: Request>): Promise { + const url = new URL(request.url); + + switch (url.pathname) { + case '/connect': { + console.log(`Handling connect request`); + + // This is a request from the client to connect to the tunnel + if (request.headers.get('Upgrade') !== 'websocket') { + return new Response('expected websocket', { status: 400 }); + } + + const pair = new WebSocketPair(); + const [clientSocket, serverSocket] = Object.values(pair); + + await this.handleSocket(serverSocket); + + console.log(`Successfully connected to tunnel`); + + return new Response(null, { status: 101, webSocket: clientSocket }); + } + case '/api/trigger': { + return this.handleTunnelRequest(request); + } + default: { + return new Response('Not Found', { status: 404 }); + } + } + } + + private async handleSocket(socket: WebSocket) { + if (this.socket) { + this.socket.close(1000, 'replaced'); + } + + this.socket = socket; + + this.socket.addEventListener('message', (event) => { + // Here we need to listen for "response" messages from the client and send them to the server via stored promises on this object + const data = JSON.parse(typeof event.data === 'string' ? event.data : new TextDecoder('utf-8').decode(event.data)); + + const message = ClientMessages.safeParse(data); + + if (!message.success) { + console.error(message.error); + return; + } + + switch (message.data.type) { + case 'response': { + const { id, ...response } = message.data; + + const resolver = this.responseResolvers[id]; + + if (!resolver) { + console.error(`No resolver found for ${id}`); + return; + } + + delete this.responseResolvers[id]; + + resolver.resolve( + new Response(response.body, { + status: response.status, + headers: response.headers, + }), + ); + + break; + } + } + }); + + this.socket.accept(); + } + + private async handleTunnelRequest(request: Request): Promise { + if (!this.socket) { + return createErrorResponse(); + } + + if (this.socket.readyState !== WebSocket.READY_STATE_OPEN) { + return createErrorResponse(); + } + + const id = crypto.randomUUID(); + + const promise = new Promise((resolve, reject) => { + this.responseResolvers[id] = { resolve, reject }; + }); + + try { + const message = await createRequestMessage(id, request); + this.socket.send(JSON.stringify(message)); + } catch (error) { + console.error(error); + delete this.responseResolvers[id]; + + return createErrorResponse(); + } + + return promise; + } +} + +const createErrorResponse = () => + new Response( + JSON.stringify({ + message: 'Could not connect to your dev server. Make sure you are running the `npx @trigger.dev/cli@latest dev` command', + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }, + ); diff --git a/apps/yalt/tsconfig.json b/apps/yalt/tsconfig.json new file mode 100644 index 0000000000..7457187220 --- /dev/null +++ b/apps/yalt/tsconfig.json @@ -0,0 +1,107 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Projects */ + // "incremental": true, /* Enable incremental compilation */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, + "jsx": "react" /* Specify what JSX code is generated. */, + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ + // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + + /* Modules */ + "module": "es2022" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ + "types": [ + "@cloudflare/workers-types/2023-07-01" + ] /* Specify type package names to be included without being referenced in a source file. */, + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + "resolveJsonModule": true /* Enable importing .json files */, + // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, + "checkJs": false /* Enable error reporting in type-checked JavaScript files. */, + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + "noEmit": true /* Disable emitting files from a compilation. */, + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, + "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, + // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ + // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ + // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + "paths": { + "@trigger.dev/yalt": ["../../packages/yalt/src/index"], + "@trigger.dev/yalt/*": ["../../packages/yalt/src/*"] + } + } +} diff --git a/apps/yalt/wrangler.toml b/apps/yalt/wrangler.toml new file mode 100644 index 0000000000..fdd43401a6 --- /dev/null +++ b/apps/yalt/wrangler.toml @@ -0,0 +1,24 @@ +name = "yalt" +main = "src/index.ts" +compatibility_date = "2023-12-06" +workers_dev = false + +routes = [ + { pattern = "*.yalt.dev/*", zone_name = "yalt.dev" }, + { pattern = "yalt.dev/*", zone_name = "yalt.dev" }, +] + +[vars] +WORKER_HOST = "yalt.dev" + +[[kv_namespaces]] +binding = "tunnelIds" +id = "e8cf1fcc9ad34fa0819694d86afa97ca" + +[[durable_objects.bindings]] +name = "connections" +class_name = "YaltConnection" + +[[migrations]] +tag = "v1" +new_classes = ["YaltConnection"] diff --git a/packages/cli/package.json b/packages/cli/package.json index 22f3a918ff..76001fe56a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -41,6 +41,7 @@ "@types/mock-fs": "^4.13.1", "@types/node": "16", "@types/node-fetch": "^2.6.2", + "@types/ws": "^8.5.3", "rimraf": "^3.0.2", "tsup": "^6.5.0", "type-fest": "^3.6.0", @@ -57,6 +58,7 @@ }, "dependencies": { "@trigger.dev/core": "workspace:*", + "@trigger.dev/yalt": "workspace:*", "@types/degit": "^2.8.3", "boxen": "^7.1.1", "chalk": "^5.2.0", @@ -85,9 +87,10 @@ "terminal-link": "^3.0.0", "tsconfck": "^2.1.2", "url": "^0.11.1", + "ws": "^8.11.0", "zod": "3.22.3" }, "engines": { "node": ">=18.0.0" } -} +} \ No newline at end of file diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index 0d92526e18..9cb039589f 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -19,6 +19,8 @@ import { RequireKeys } from "../utils/requiredKeys"; import { Throttle } from "../utils/throttle"; import { TriggerApi } from "../utils/triggerApi"; import { wait } from "../utils/wait"; +import { YaltTunnel } from "@trigger.dev/yalt"; +import chalk from "chalk"; const asyncExecFile = util.promisify(childProcess.execFile); @@ -84,8 +86,8 @@ export async function devCommand(path: string, anyOptions: any) { const resolvedPath = resolvePath(path); runtime = await getJsRuntime(resolvedPath, logger); - //check for outdated packages, don't await this - runtime.checkForOutdatedPackages(); + //check for outdated packages, don't immediately await this + const checkForOutdatedPackagesPromise = runtime.checkForOutdatedPackages(); // Read from package.json to get the endpointId const endpointId = await getEndpointId(runtime, options.clientId); @@ -128,7 +130,8 @@ export async function devCommand(path: string, anyOptions: any) { telemetryClient.dev.serverRunning(path, resolvedOptions); // Setup tunnel - const endpointUrl = await resolveEndpointUrl(apiUrl, verifiedEndpoint); + const endpointUrl = await resolveEndpointUrl(apiUrl, apiKey, verifiedEndpoint); + if (!endpointUrl) { telemetryClient.dev.failed("failed_to_create_tunnel", resolvedOptions); return; @@ -148,6 +151,17 @@ export async function devCommand(path: string, anyOptions: any) { ignoreInitial: true, }); + const outdatedPackages = await checkForOutdatedPackagesPromise; + + if (outdatedPackages) { + console.log( + chalk.bgYellow( + `New @trigger.dev/* packages available (${outdatedPackages.from} -> ${outdatedPackages.to})` + ) + ); + console.log(chalk.bgBlue("Run npx @trigger.dev/cli@latest update")); + } + const connectingSpinner = ora(`[trigger.dev] Registering endpoint ${endpointHandlerUrl}...`); let hasConnected = false; const abortController = new AbortController(); @@ -444,7 +458,7 @@ function findServerUrls(resolvedOptions: ResolvedOptions, framework?: Framework) return urls; } -async function resolveEndpointUrl(apiUrl: string, endpoint: ServerEndpoint) { +async function resolveEndpointUrl(apiUrl: string, apiKey: string, endpoint: ServerEndpoint) { // use tunnel URL if provided if (endpoint.type === "tunnel") { return endpoint.url; @@ -457,18 +471,67 @@ async function resolveEndpointUrl(apiUrl: string, endpoint: ServerEndpoint) { return `http://${endpoint.hostname}:${endpoint.port}`; } - // Setup tunnel - const tunnelSpinner = ora(`🚇 Creating tunnel`).start(); - const tunnelUrl = await createTunnel(endpoint.hostname, endpoint.port, tunnelSpinner); + const triggerApi = new TriggerApi(apiKey, apiUrl); + + const supportsTunneling = await triggerApi.supportsTunneling(); - if (tunnelUrl) { - tunnelSpinner.succeed(`🚇 Created tunnel: ${tunnelUrl}`); + if (supportsTunneling) { + const tunnelSpinner = ora(`🚇 Creating Trigger.dev tunnel`).start(); + const tunnelUrl = await createNativeTunnel( + endpoint.hostname, + endpoint.port, + triggerApi, + tunnelSpinner + ); + + if (tunnelUrl) { + tunnelSpinner.succeed(`🚇 Trigger.dev tunnel ready`); + } + + return tunnelUrl; + } else { + // Setup tunnel + const tunnelSpinner = ora(`🚇 Creating tunnel`).start(); + const tunnelUrl = await createNgrokTunnel(endpoint.hostname, endpoint.port, tunnelSpinner); + + if (tunnelUrl) { + tunnelSpinner.succeed(`🚇 Created tunnel: ${tunnelUrl}`); + } + + return tunnelUrl; } +} + +let yaltTunnel: YaltTunnel | null = null; + +async function createNativeTunnel( + hostname: string, + port: number, + triggerApi: TriggerApi, + spinner: Ora +) { + try { + const response = await triggerApi.createTunnel(); + + // import WS dynamically + const WebSocket = await import("ws"); - return tunnelUrl; + yaltTunnel = new YaltTunnel(response.url, `${hostname}:${port}`, { + WebSocket: WebSocket.default, + connectionTimeout: 1000, + maxRetries: 10, + }); + + await yaltTunnel.connect(); + + return `https://${response.url}`; + } catch (e) { + spinner.fail(`Failed to create tunnel.\n${e}`); + return; + } } -async function createTunnel(hostname: string, port: number, spinner: Ora) { +async function createNgrokTunnel(hostname: string, port: number, spinner: Ora) { try { return await ngrok.connect({ addr: `${hostname}:${port}` }); } catch (error: any) { diff --git a/packages/cli/src/utils/jsRuntime.ts b/packages/cli/src/utils/jsRuntime.ts index 2e453c2aa0..733d796f1e 100644 --- a/packages/cli/src/utils/jsRuntime.ts +++ b/packages/cli/src/utils/jsRuntime.ts @@ -2,7 +2,6 @@ import { Framework, getFramework } from "../frameworks"; import { PackageManager, getUserPackageManager } from "./getUserPkgManager"; import { Logger } from "./logger"; import { run as ncuRun } from "npm-check-updates"; -import chalk from "chalk"; import fs from "fs/promises"; import pathModule from "path"; @@ -14,7 +13,7 @@ export abstract class JsRuntime { this.projectRootPath = projectRootPath; } abstract get id(): string; - abstract checkForOutdatedPackages(): Promise; + abstract checkForOutdatedPackages(): Promise<{ from: string; to: string } | undefined>; abstract getUserPackageManager(): Promise; abstract getFramework(): Promise; abstract getEndpointId(): Promise; @@ -46,7 +45,7 @@ class NodeJsRuntime extends JsRuntime { return pathModule.join(this.projectRootPath, "package.json"); } - async checkForOutdatedPackages(): Promise { + async checkForOutdatedPackages(): Promise<{ from: string; to: string } | undefined> { const updates = (await ncuRun({ packageFile: `${this.packageJsonPath}`, filter: "/trigger.dev/.+$/", @@ -62,12 +61,27 @@ class NodeJsRuntime extends JsRuntime { const packageFile = await fs.readFile(this.packageJsonPath); const data = JSON.parse(Buffer.from(packageFile).toString("utf8")); const dependencies = data.dependencies; - console.log(chalk.bgYellow("Updates available for trigger.dev packages")); - console.log(chalk.bgBlue("Run npx @trigger.dev/cli@latest update")); - for (let dep in updates) { - console.log(`${dep} ${dependencies[dep]} → ${updates[dep]}`); + const hasUpdates = Object.keys(updates).length > 0; + + if (!hasUpdates) { + return; + } + + const firstDep = Object.keys(updates)[0]; + + if (!firstDep) { + return; } + + const from = dependencies[firstDep]; + const to = updates[firstDep]; + + if (!to || !from) { + return; + } + + return { from, to }; } async getUserPackageManager() { @@ -118,8 +132,8 @@ class DenoRuntime extends JsRuntime { } } - async checkForOutdatedPackages() { - // not implemented currently + async checkForOutdatedPackages(): Promise<{ from: string; to: string } | undefined> { + return; } async getUserPackageManager() { return undefined; diff --git a/packages/cli/src/utils/triggerApi.ts b/packages/cli/src/utils/triggerApi.ts index a8708f56de..e206d527f9 100644 --- a/packages/cli/src/utils/triggerApi.ts +++ b/packages/cli/src/utils/triggerApi.ts @@ -24,9 +24,9 @@ export type EndpointData = { export type EndpointResponse = | { - ok: true; - data: EndpointData; - } + ok: true; + data: EndpointData; + } | { ok: false; error: string; retryable: boolean }; const RETRYABLE_PATTERN = /Could not connect to endpoint/i; @@ -55,13 +55,53 @@ const WhoamiResponseSchema = z.object({ userId: z.string().optional(), }); +const CreateTunnelResponseSchema = z.object({ + url: z.string(), +}); + export type WhoamiResponse = z.infer; export class TriggerApi { constructor( private apiKey: string, private baseUrl: string - ) {} + ) { } + + async supportsTunneling(): Promise { + const response = await fetch(`${this.baseUrl}/api/v1/tunnels`, { + method: "HEAD", + headers: { + Accept: "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + }); + + return response.ok; + } + + async createTunnel() { + const response = await fetch(`${this.baseUrl}/api/v1/tunnels`, { + method: "POST", + headers: { + Accept: "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + }); + + if (!response.ok) { + throw new Error(`Could not create tunnel: ${response.status}`); + } + + const body = await response.json(); + + const parsedBody = CreateTunnelResponseSchema.safeParse(body); + + if (!parsedBody.success) { + throw new Error(`Could not create tunnel: ${parsedBody.error.message}`); + } + + return parsedBody.data; + } async whoami(): Promise { const response = await fetch(`${this.baseUrl}/api/v1/whoami`, { diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index b765e2ade9..a0999d3806 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -46,7 +46,10 @@ "useDefineForClassFields": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - "types": ["vitest/globals"] + "types": ["vitest/globals"], + "paths": { + "@trigger.dev/yalt": ["../yalt/src/index"] + } }, "exclude": ["node_modules"] } diff --git a/packages/database/prisma/migrations/20231214103115_add_tunnel_id_to_runtime_environment/migration.sql b/packages/database/prisma/migrations/20231214103115_add_tunnel_id_to_runtime_environment/migration.sql new file mode 100644 index 0000000000..fe8830c223 --- /dev/null +++ b/packages/database/prisma/migrations/20231214103115_add_tunnel_id_to_runtime_environment/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "RuntimeEnvironment" ADD COLUMN "tunnelId" TEXT; diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index f8a52f746d..433b7a6928 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -316,6 +316,8 @@ model RuntimeEnvironment { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + tunnelId String? + endpoints Endpoint[] jobVersions JobVersion[] events EventRecord[] diff --git a/packages/yalt/README.md b/packages/yalt/README.md new file mode 100644 index 0000000000..e36b42a80b --- /dev/null +++ b/packages/yalt/README.md @@ -0,0 +1 @@ +## @trigger.dev/yalt diff --git a/packages/yalt/package.json b/packages/yalt/package.json new file mode 100644 index 0000000000..d957977ee6 --- /dev/null +++ b/packages/yalt/package.json @@ -0,0 +1,51 @@ +{ + "name": "@trigger.dev/yalt", + "version": "2.3.5", + "description": "yalt.dev client library", + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "module": "./dist/index.mjs", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && npm run build:tsup", + "build:tsup": "tsup --dts-resolve", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "zod": "3.22.3", + "partysocket": "^0.0.17" + }, + "devDependencies": { + "@trigger.dev/tsconfig": "workspace:*", + "@types/jest": "^29.5.3", + "@types/node": "18", + "jest": "^29.6.2", + "rimraf": "^3.0.2", + "ts-jest": "^29.1.1", + "tsup": "^8.0.1", + "@trigger.dev/tsup": "workspace:*", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file diff --git a/packages/yalt/src/index.ts b/packages/yalt/src/index.ts new file mode 100644 index 0000000000..ff783f8ef8 --- /dev/null +++ b/packages/yalt/src/index.ts @@ -0,0 +1,178 @@ +import { z } from "zod"; +import { WebSocket } from "partysocket"; + +export const RequestMesssage = z.object({ + type: z.literal("request"), + id: z.string(), + headers: z.record(z.string()), + method: z.string(), + url: z.string(), + body: z.string(), +}); + +export type RequestMessage = z.infer; + +export const ResponseMessage = z.object({ + type: z.literal("response"), + id: z.string(), + status: z.number(), + headers: z.record(z.string()), + body: z.string(), +}); + +export type ResponseMessage = z.infer; + +export const ClientMessages = z.discriminatedUnion("type", [ResponseMessage]); +export const ServerMessages = z.discriminatedUnion("type", [RequestMesssage]); + +export type ClientMessage = z.infer; +export type ServerMessage = z.infer; + +export async function createRequestMessage(id: string, request: Request): Promise { + const { headers, method, url } = request; + + const body = await request.text(); + + return { + type: "request", + id, + headers: Object.fromEntries(headers), + method, + url, + body, + }; +} + +async function createResponseMessage(id: string, response: Response): Promise { + const { headers, status } = response; + + const body = await response.text(); + + return { + type: "response", + id, + headers: Object.fromEntries(headers), + status, + body, + }; +} + +export class YaltApiClient { + constructor( + private host: string, + private apiKey: string + ) {} + + async createTunnel(): Promise { + const response = await fetch(`https://admin.${this.host}/api/tunnels`, { + method: "POST", + headers: { + accept: "application/json", + authorization: `Bearer ${this.apiKey}`, + }, + }); + + if (!response.ok) { + throw new Error(`Could not create tunnel: ${response.status}`); + } + + const body = await response.json(); + + return body.id; + } + + connectUrl(id: string): string { + return `${id}.${this.host}`; + } +} + +export type YaltTunnelOptions = { + WebSocket?: any; + connectionTimeout?: number; + maxRetries?: number; +}; +export class YaltTunnel { + socket?: WebSocket; + + constructor( + private url: string, + private address: string, + private options: YaltTunnelOptions = {} + ) {} + + async connect() { + this.socket = new WebSocket(`wss://${this.url}/connect`, [], this.options); + + this.socket.addEventListener("open", () => {}); + + this.socket.addEventListener("close", (event) => {}); + + this.socket.addEventListener("message", async (event) => { + const data = JSON.parse( + typeof event.data === "string" ? event.data : new TextDecoder("utf-8").decode(event.data) + ); + + const message = ServerMessages.safeParse(data); + + if (!message.success) { + console.error(message.error); + return; + } + + switch (message.data.type) { + case "request": { + await this.handleRequest(message.data); + + break; + } + default: { + console.error(`Unknown message type: ${message.data.type}`); + } + } + }); + + this.socket.addEventListener("error", (event) => { + console.error(event); + }); + } + + private async handleRequest(request: RequestMessage) { + if (!this.socket) { + throw new Error("Socket is not connected"); + } + + const url = new URL(request.url); + // Construct the original url to be the same as the request URL but with a different hostname and using http instead of https + const originalUrl = new URL(`http://${this.address}${url.pathname}${url.search}${url.hash}`); + + let response: Response | null = null; + + try { + response = await fetch(originalUrl.href, { + method: request.method, + headers: request.headers, + body: request.body, + }); + } catch (error) { + // Return a 502 response + response = new Response( + JSON.stringify({ + message: `Could not connect to ${originalUrl.href}. Make sure you are running your local app server`, + }), + { status: 502, headers: { "Content-Type": "application/json" } } + ); + } + + try { + await sendResponse(request.id, response, this.socket); + } catch (error) { + console.error(error); + } + } +} + +async function sendResponse(id: string, response: Response, socket: WebSocket) { + const message = await createResponseMessage(id, response); + + return socket.send(JSON.stringify(message)); +} diff --git a/packages/yalt/tsconfig.json b/packages/yalt/tsconfig.json new file mode 100644 index 0000000000..daa6330e22 --- /dev/null +++ b/packages/yalt/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@trigger.dev/tsconfig/node18.json", + "include": ["src/globals.d.ts", "./src/**/*.ts", "tsup.config.ts"], + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": false, + "declarationMap": false, + "paths": { + "@trigger.dev/tsup/*": ["../../config-packages/tsup/src/*"], + "@trigger.dev/tsup": ["../../config-packages/tsup/src/index"] + }, + "lib": ["DOM", "DOM.Iterable"] + }, + "exclude": ["node_modules"] +} diff --git a/packages/yalt/tsup.config.ts b/packages/yalt/tsup.config.ts new file mode 100644 index 0000000000..1d1b59a08b --- /dev/null +++ b/packages/yalt/tsup.config.ts @@ -0,0 +1,3 @@ +import { defineConfigPackage } from "@trigger.dev/tsup"; + +export default defineConfigPackage; diff --git a/perf/src/trigger.ts b/perf/src/trigger.ts index 3d79eb07f6..a48728596a 100644 --- a/perf/src/trigger.ts +++ b/perf/src/trigger.ts @@ -6,11 +6,6 @@ export const triggerClient = new TriggerClient({ apiUrl: process.env.TRIGGER_API_URL!, }); -const concurrencyLimit = triggerClient.defineConcurrencyLimit({ - id: `perf-test-shared`, - limit: 5, -}); - triggerClient.defineJob({ id: `perf-test-1`, name: `Perf Test 1`, @@ -18,7 +13,6 @@ triggerClient.defineJob({ trigger: eventTrigger({ name: "perf.test", }), - concurrencyLimit, run: async (payload, io, ctx) => { await io.runTask( "task-1", @@ -65,7 +59,6 @@ triggerClient.defineJob({ trigger: eventTrigger({ name: "perf.test", }), - concurrencyLimit: 5, run: async (payload, io, ctx) => { await io.runTask( "task-1", @@ -112,7 +105,6 @@ triggerClient.defineJob({ trigger: eventTrigger({ name: "perf.test", }), - concurrencyLimit, run: async (payload, io, ctx) => { await io.runTask( "task-1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48dae346d5..2dcf8b28f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -140,6 +140,7 @@ importers: '@trigger.dev/database': workspace:* '@trigger.dev/sdk': workspace:* '@trigger.dev/tailwind-config': workspace:* + '@trigger.dev/yalt': workspace:* '@types/bcryptjs': ^2.4.2 '@types/compression': ^1.7.2 '@types/eslint': ^8.4.6 @@ -277,6 +278,7 @@ importers: '@trigger.dev/core-backend': link:../../packages/core-backend '@trigger.dev/database': link:../../packages/database '@trigger.dev/sdk': link:../../packages/trigger-sdk + '@trigger.dev/yalt': link:../../packages/yalt '@types/pg': 8.6.6 '@uiw/react-codemirror': 4.19.5_th22fcplkuhrqjnlojwclcaim4 '@whatwg-node/fetch': 0.9.14 @@ -393,6 +395,17 @@ importers: tsconfig-paths: 3.14.1 typescript: 5.2.2 + apps/yalt: + specifiers: + '@cloudflare/workers-types': ~4.20231121.0 + '@trigger.dev/yalt': workspace:* + wrangler: ^3.20.0 + dependencies: + '@trigger.dev/yalt': link:../../packages/yalt + devDependencies: + '@cloudflare/workers-types': 4.20231121.0 + wrangler: 3.20.0 + config-packages/eslint-config-custom: specifiers: eslint: ^8.24.0 @@ -787,12 +800,14 @@ importers: specifiers: '@trigger.dev/core': workspace:* '@trigger.dev/tsconfig': workspace:* + '@trigger.dev/yalt': workspace:* '@types/degit': ^2.8.3 '@types/gradient-string': ^1.1.2 '@types/inquirer': ^9.0.3 '@types/mock-fs': ^4.13.1 '@types/node': '16' '@types/node-fetch': ^2.6.2 + '@types/ws': ^8.5.3 boxen: ^7.1.1 chalk: ^5.2.0 chokidar: ^3.5.3 @@ -825,9 +840,11 @@ importers: typescript: ^4.9.5 url: ^0.11.1 vitest: ^0.34.4 + ws: ^8.11.0 zod: 3.22.3 dependencies: '@trigger.dev/core': link:../core + '@trigger.dev/yalt': link:../yalt '@types/degit': 2.8.3 boxen: 7.1.1 chalk: 5.2.0 @@ -856,6 +873,7 @@ importers: terminal-link: 3.0.0 tsconfck: 2.1.2_typescript@4.9.5 url: 0.11.1 + ws: 8.12.0 zod: 3.22.3 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig @@ -864,6 +882,7 @@ importers: '@types/mock-fs': 4.13.1 '@types/node': 16.18.11 '@types/node-fetch': 2.6.2 + '@types/ws': 8.5.4 rimraf: 3.0.2 tsup: 6.6.3_typescript@4.9.5 type-fest: 3.6.0 @@ -1289,6 +1308,33 @@ importers: typed-emitter: 2.1.0 typescript: 5.3.2 + packages/yalt: + specifiers: + '@trigger.dev/tsconfig': workspace:* + '@trigger.dev/tsup': workspace:* + '@types/jest': ^29.5.3 + '@types/node': '18' + jest: ^29.6.2 + partysocket: ^0.0.17 + rimraf: ^3.0.2 + ts-jest: ^29.1.1 + tsup: ^8.0.1 + typescript: ^5.3.0 + zod: 3.22.3 + dependencies: + partysocket: 0.0.17 + zod: 3.22.3 + devDependencies: + '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@trigger.dev/tsup': link:../../config-packages/tsup + '@types/jest': 29.5.3 + '@types/node': 18.17.1 + jest: 29.6.2_@types+node@18.17.1 + rimraf: 3.0.2 + ts-jest: 29.1.1_3tyr5rkhps22cjyuxtwz6gzoky + tsup: 8.0.1_a5ztaafw5l4qfghy2hjjuynb34_typescript@5.3.2 + typescript: 5.3.2 + perf: specifiers: '@trigger.dev/cli': workspace:* @@ -16052,7 +16098,7 @@ packages: /axios/0.21.4_debug@4.3.2: resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: - follow-redirects: 1.15.2_debug@4.3.2 + follow-redirects: 1.15.2 transitivePeerDependencies: - debug dev: false @@ -20880,18 +20926,6 @@ packages: debug: optional: true - /follow-redirects/1.15.2_debug@4.3.2: - resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dependencies: - debug: 4.3.2 - dev: false - /for-each/0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -25198,6 +25232,29 @@ packages: - utf-8-validate dev: true + /miniflare/3.20231030.4: + resolution: {integrity: sha512-7MBz0ArLuDop1WJGZC6tFgN6c5MRyDOIlxbm3yp0TRBpvDS/KsTuWCQcCjsxN4QQ5zvL3JTkuIZbQzRRw/j6ow==} + engines: {node: '>=16.13'} + hasBin: true + dependencies: + acorn: 8.10.0 + acorn-walk: 8.2.0 + capnp-ts: 0.7.0 + exit-hook: 2.2.1 + glob-to-regexp: 0.4.1 + source-map-support: 0.5.21 + stoppable: 1.1.0 + undici: 5.25.4 + workerd: 1.20231030.0 + ws: 8.12.0 + youch: 3.3.3 + zod: 3.22.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + /minimal-polyfills/2.2.2: resolution: {integrity: sha512-eEOUq/LH/DbLWihrxUP050Wi7H/N/I2dQT98Ep6SqOpmIbk4sXOI4wqalve66QoZa+6oljbZWU6I6T4dehQGmw==} dev: false @@ -26732,6 +26789,10 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + /partysocket/0.0.17: + resolution: {integrity: sha512-8Re9nmgP2LzQhq+FBs9+BZNTjmMwoF4geEKlpH0lxW1JKp3FmplN74306afGH9EsOjdfcXqKY2VCZtc3iAHIow==} + dev: false + /pascal-case/3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} dependencies: @@ -33070,6 +33131,33 @@ packages: - utf-8-validate dev: true + /wrangler/3.20.0: + resolution: {integrity: sha512-7mg25zJByhBmrfG+CbImSid7JNd5lxGovLA167ndtE8Yrqd3TUukrGWL8o0RCQIm0FUcgl2nCzWArJDShlZVKA==} + engines: {node: '>=16.17.0'} + hasBin: true + dependencies: + '@cloudflare/kv-asset-handler': 0.2.0 + '@esbuild-plugins/node-globals-polyfill': 0.2.3_esbuild@0.17.19 + '@esbuild-plugins/node-modules-polyfill': 0.2.2_esbuild@0.17.19 + blake3-wasm: 2.1.5 + chokidar: 3.5.3 + esbuild: 0.17.19 + miniflare: 3.20231030.4 + nanoid: 3.3.6 + path-to-regexp: 6.2.1 + resolve.exports: 2.0.2 + selfsigned: 2.4.1 + source-map: 0.6.1 + source-map-support: 0.5.21 + xxhash-wasm: 1.0.2 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + /wrap-ansi/6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'}