diff --git a/packages/workers/live/.dev.vars.example b/packages/workers/live/.dev.vars.example new file mode 100644 index 000000000000..3bf130924b5c --- /dev/null +++ b/packages/workers/live/.dev.vars.example @@ -0,0 +1 @@ +LIVEPEER_API_KEY="" diff --git a/packages/workers/live/.eslintrc.js b/packages/workers/live/.eslintrc.js new file mode 100644 index 000000000000..484e1ab14a27 --- /dev/null +++ b/packages/workers/live/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: [require.resolve('@hey/config/eslint/base.js')], + rules: { + 'import/no-anonymous-default-export': 'off' + } +}; diff --git a/packages/workers/live/README.md b/packages/workers/live/README.md new file mode 100644 index 000000000000..2f91941febb0 --- /dev/null +++ b/packages/workers/live/README.md @@ -0,0 +1 @@ +# Live worker diff --git a/packages/workers/live/package.json b/packages/workers/live/package.json new file mode 100644 index 000000000000..3cda5a8a9184 --- /dev/null +++ b/packages/workers/live/package.json @@ -0,0 +1,29 @@ +{ + "name": "@workers/live", + "version": "0.0.0", + "private": true, + "license": "AGPL-3.0", + "scripts": { + "dev": "wrangler dev --port 8096", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --fix --ext .ts", + "prettier": "prettier --check \"**/*.{js,ts,tsx,md}\" --cache", + "prettier:fix": "prettier --write \"**/*.{js,ts,tsx,md}\" --cache", + "start": "pnpm dev", + "typecheck": "tsc --pretty", + "worker:deploy": "wrangler deploy --var RELEASE:\"$(git rev-parse HEAD)\"" + }, + "dependencies": { + "@hey/data": "workspace:*", + "@hey/lib": "workspace:*", + "@tsndr/cloudflare-worker-jwt": "^2.2.2", + "itty-router": "^4.0.23", + "zod": "^3.22.2" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20230922.0", + "@hey/config": "workspace:*", + "typescript": "^5.2.2", + "wrangler": "^3.10.1" + } +} diff --git a/packages/workers/live/src/handlers/createStream.ts b/packages/workers/live/src/handlers/createStream.ts new file mode 100644 index 000000000000..f579b9db8af0 --- /dev/null +++ b/packages/workers/live/src/handlers/createStream.ts @@ -0,0 +1,72 @@ +import { Errors } from '@hey/data/errors'; +import hasOwnedLensProfiles from '@hey/lib/hasOwnedLensProfiles'; +import response from '@hey/lib/response'; +import validateLensAccount from '@hey/lib/validateLensAccount'; +import jwt from '@tsndr/cloudflare-worker-jwt'; +import { boolean, object, string } from 'zod'; + +import type { WorkerRequest } from '../types'; + +type ExtensionRequest = { + id: string; + isMainnet: boolean; +}; + +const validationSchema = object({ + id: string(), + isMainnet: boolean() +}); + +export default async (request: WorkerRequest) => { + const body = await request.json(); + if (!body) { + return response({ success: false, error: Errors.NoBody }); + } + + const accessToken = request.headers.get('X-Access-Token'); + if (!accessToken) { + return response({ success: false, error: Errors.NoAccessToken }); + } + + const validation = validationSchema.safeParse(body); + + if (!validation.success) { + return response({ success: false, error: validation.error.issues }); + } + + const { id, isMainnet } = body as ExtensionRequest; + + try { + const isAuthenticated = await validateLensAccount(accessToken, isMainnet); + if (!isAuthenticated) { + return response({ success: false, error: Errors.InvalidAccesstoken }); + } + + const { payload } = jwt.decode(accessToken); + const hasOwned = await hasOwnedLensProfiles(payload.id, id, isMainnet); + if (!hasOwned) { + return response({ success: false, error: Errors.InvalidProfileId }); + } + + const livepeerResponse = await fetch('https://livepeer.studio/api/stream', { + method: 'POST', + headers: { + 'content-type': 'application/json', + Authorization: `Bearer ${request.env.LIVEPEER_API_KEY}` + }, + body: JSON.stringify({ + name: `${id}-${crypto.randomUUID()}`, + profiles: [ + { name: '480p0', fps: 0, bitrate: 1600000, width: 854, height: 480 }, + { name: '720p0', fps: 0, bitrate: 3000000, width: 1280, height: 720 } + ] + }) + }); + + const result = await livepeerResponse.json(); + + return response({ success: true, result: result }); + } catch (error) { + throw error; + } +}; diff --git a/packages/workers/live/src/helpers/buildRequest.ts b/packages/workers/live/src/helpers/buildRequest.ts new file mode 100644 index 000000000000..7e779e95e42a --- /dev/null +++ b/packages/workers/live/src/helpers/buildRequest.ts @@ -0,0 +1,16 @@ +import type { Env, WorkerRequest } from '../types'; + +const buildRequest = ( + request: Request, + env: Env, + ctx: ExecutionContext +): WorkerRequest => { + const temp: WorkerRequest = request as WorkerRequest; + temp.req = request; + temp.env = env; + temp.ctx = ctx; + + return temp; +}; + +export default buildRequest; diff --git a/packages/workers/live/src/index.ts b/packages/workers/live/src/index.ts new file mode 100644 index 000000000000..27acd2830559 --- /dev/null +++ b/packages/workers/live/src/index.ts @@ -0,0 +1,43 @@ +import { Errors } from '@hey/data/errors'; +import response from '@hey/lib/response'; +import { createCors, error, Router, status } from 'itty-router'; + +import createStream from './handlers/createStream'; +import buildRequest from './helpers/buildRequest'; +import type { Env, WorkerRequest } from './types'; + +const { preflight, corsify } = createCors({ + origins: ['*'], + methods: ['HEAD', 'GET', 'POST'] +}); + +const router = Router(); + +router + .all('*', preflight) + .head('*', () => status(200)) + .get('/', (request: WorkerRequest) => + response({ + message: 'gm, to live service 👋', + version: request.env.RELEASE ?? 'unknown' + }) + ) + .post('/create', createStream) + .all('*', () => error(404)); + +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + const incomingRequest = buildRequest(request, env, ctx); + + return await router + .handle(incomingRequest) + .then(corsify) + .catch(() => { + return error(500, Errors.InternalServerError); + }); + } +}; diff --git a/packages/workers/live/src/types.ts b/packages/workers/live/src/types.ts new file mode 100644 index 000000000000..8763cb28a371 --- /dev/null +++ b/packages/workers/live/src/types.ts @@ -0,0 +1,12 @@ +import type { IRequestStrict } from 'itty-router'; + +export interface Env { + RELEASE: string; + LIVEPEER_API_KEY: string; +} + +export type WorkerRequest = { + req: Request; + env: Env; + ctx: ExecutionContext; +} & IRequestStrict; diff --git a/packages/workers/live/tsconfig.json b/packages/workers/live/tsconfig.json new file mode 100644 index 000000000000..33dc4d0f2e28 --- /dev/null +++ b/packages/workers/live/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@hey/config/base.tsconfig.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types"] + } +} diff --git a/packages/workers/live/wrangler.toml b/packages/workers/live/wrangler.toml new file mode 100644 index 000000000000..a58139560539 --- /dev/null +++ b/packages/workers/live/wrangler.toml @@ -0,0 +1,16 @@ +name = "live" +main = "src/index.ts" +compatibility_date = "2023-01-25" +keep_vars = true +node_compat = true + +routes = [ + { pattern = "live.hey.xyz", custom_domain = true } +] + +[placement] +mode = "smart" + +[env.production.vars] +RELEASE = "" +LIVEPEER_API_KEY = "" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b2920a0d0d6..897839fc63eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -793,6 +793,37 @@ importers: specifier: ^3.10.1 version: 3.10.1 + packages/workers/live: + dependencies: + '@hey/data': + specifier: workspace:* + version: link:../../data + '@hey/lib': + specifier: workspace:* + version: link:../../lib + '@tsndr/cloudflare-worker-jwt': + specifier: ^2.2.2 + version: 2.2.2 + itty-router: + specifier: ^4.0.23 + version: 4.0.23 + zod: + specifier: ^3.22.2 + version: 3.22.2 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20230922.0 + version: 4.20230922.0 + '@hey/config': + specifier: workspace:* + version: link:../../config + typescript: + specifier: ^5.2.2 + version: 5.2.2 + wrangler: + specifier: ^3.10.1 + version: 3.10.1 + packages/workers/metadata: dependencies: '@hey/bundlr':