From b6b18adcdb0027e32638318653abf2360879a284 Mon Sep 17 00:00:00 2001 From: Eithan Date: Thu, 8 Jan 2026 17:59:51 +1100 Subject: [PATCH 01/25] feat: service separation --- packages/common/package.json | 24 + packages/common/src/await-truthy.ts | 36 + packages/common/src/index.ts | 3 + packages/{server => common}/src/loop-base.ts | 9 +- packages/common/src/timeout.ts | 7 + packages/common/tsconfig.json | 21 + packages/gateway/package.json | 44 + packages/gateway/src/config.ts | 26 + packages/gateway/src/gateway.ts | 311 ++ packages/gateway/src/grpc-client.ts | 55 + packages/gateway/src/index.ts | 31 + packages/gateway/tsconfig.json | 24 + packages/grpc/package.json | 37 + packages/grpc/proto/runner.proto | 79 + packages/grpc/proto/server.proto | 240 ++ packages/grpc/src/runner.ts | 1157 ++++++ packages/grpc/src/server.ts | 3261 +++++++++++++++++ packages/grpc/tsconfig.json | 21 + packages/server/package.json | 15 +- packages/server/src/bouncer.ts | 2 - packages/server/src/config.ts | 3 + packages/server/src/grpc/index.ts | 80 + .../server/src/jobber/log-drivers/abstract.ts | 2 +- packages/server/src/jobber/runners/manager.ts | 4 +- packages/server/src/jobber/runners/server.ts | 3 +- packages/server/src/jobber/store.ts | 2 +- packages/server/src/jobber/telemetry.ts | 2 +- packages/server/src/jobber/triggers/cron.ts | 4 +- packages/server/src/jobber/triggers/http.ts | 5 +- packages/server/src/jobber/triggers/mqtt.ts | 2 +- packages/server/src/lock.ts | 2 +- packages/server/src/pg-backup.ts | 2 +- packages/server/src/util.ts | 38 - packages/web/vite.config.ts | 2 + pnpm-lock.yaml | 561 +++ 35 files changed, 6055 insertions(+), 60 deletions(-) create mode 100644 packages/common/package.json create mode 100644 packages/common/src/await-truthy.ts create mode 100644 packages/common/src/index.ts rename packages/{server => common}/src/loop-base.ts (89%) create mode 100644 packages/common/src/timeout.ts create mode 100644 packages/common/tsconfig.json create mode 100644 packages/gateway/package.json create mode 100644 packages/gateway/src/config.ts create mode 100644 packages/gateway/src/gateway.ts create mode 100644 packages/gateway/src/grpc-client.ts create mode 100644 packages/gateway/src/index.ts create mode 100644 packages/gateway/tsconfig.json create mode 100644 packages/grpc/package.json create mode 100644 packages/grpc/proto/runner.proto create mode 100644 packages/grpc/proto/server.proto create mode 100644 packages/grpc/src/runner.ts create mode 100644 packages/grpc/src/server.ts create mode 100644 packages/grpc/tsconfig.json create mode 100644 packages/server/src/grpc/index.ts diff --git a/packages/common/package.json b/packages/common/package.json new file mode 100644 index 0000000..c897f70 --- /dev/null +++ b/packages/common/package.json @@ -0,0 +1,24 @@ +{ + "name": "@jobber/common", + "version": "1.0.0", + "description": "Common utilities for Jobber Services", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "type": "module", + "scripts": { + "build": "rimraf ./dist/* && tsc && tsc-alias -p tsconfig.json" + }, + "keywords": [], + "author": "Eithan Hersey-Tuit", + "license": "MIT", + "dependencies": { + "@jobber/tcp-frame-socket": "workspace:*" + }, + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/node": "^20.16.12", + "rimraf": "^5.0.10", + "tsc-alias": "^1.8.10", + "typescript": "^5.6.3" + } +} diff --git a/packages/common/src/await-truthy.ts b/packages/common/src/await-truthy.ts new file mode 100644 index 0000000..d8cd352 --- /dev/null +++ b/packages/common/src/await-truthy.ts @@ -0,0 +1,36 @@ +import { timeout } from "./timeout.js"; + +/** + * Awaits until the callback yields true + */ +export const awaitTruthy = async ( + callback: () => Promise, + timeoutMs: number = 30_000 +) => { + let startTime = Date.now(); + + let index = 0; + while (true) { + if (Date.now() - startTime > timeoutMs) { + return false; + } + + if (await callback()) { + return true; + } + + index++; + + if (index <= 10) { + await timeout(10); + } + + if (index > 10 && index <= 20) { + await timeout(20); + } + + if (index > 20) { + await timeout(100); + } + } +}; diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts new file mode 100644 index 0000000..14fdaae --- /dev/null +++ b/packages/common/src/index.ts @@ -0,0 +1,3 @@ +export * from "./loop-base.js"; +export * from "./await-truthy.js"; +export * from "./timeout.js"; diff --git a/packages/server/src/loop-base.ts b/packages/common/src/loop-base.ts similarity index 89% rename from packages/server/src/loop-base.ts rename to packages/common/src/loop-base.ts index c502119..a4739ea 100644 --- a/packages/server/src/loop-base.ts +++ b/packages/common/src/loop-base.ts @@ -1,5 +1,6 @@ import assert from "node:assert"; -import { awaitTruthy, timeout } from "./util.js"; +import { awaitTruthy } from "./await-truthy.js"; +import { timeout } from "./timeout.js"; /** * Lifecycle: @@ -60,7 +61,11 @@ export abstract class LoopBase { this.isLoopRunning = true; while (this.status === "starting" || this.status === "started") { - await this.loopIteration(); + try { + await this.loopIteration(); + } catch (err) { + console.error(err); + } await timeout(this.loopDuration); } diff --git a/packages/common/src/timeout.ts b/packages/common/src/timeout.ts new file mode 100644 index 0000000..ddc8ab4 --- /dev/null +++ b/packages/common/src/timeout.ts @@ -0,0 +1,7 @@ +/** + * Creates a promise that resolves after a timeout + * @param ms Time to wait in milliseconds + * @returns + */ +export const timeout = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json new file mode 100644 index 0000000..0ac97f1 --- /dev/null +++ b/packages/common/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "inlineSourceMap": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "types": ["node"], + "outDir": "./dist", + }, + "include": [ + "./src" + ], + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Recommended" +} \ No newline at end of file diff --git a/packages/gateway/package.json b/packages/gateway/package.json new file mode 100644 index 0000000..8277c38 --- /dev/null +++ b/packages/gateway/package.json @@ -0,0 +1,44 @@ +{ + "name": "@jobber/gateway", + "version": "1.0.0", + "description": "Jobber API Gateway", + "main": "./dist/index.js", + "type": "module", + "scripts": { + "build:image": "docker build -t jobber-gateway ./", + "build": "rimraf ./dist/* && tsc && tsc-alias -p tsconfig.json", + "start": "node ./dist/index.js", + "dev": "pnpm build && node --env-file-if-exists=.env ./dist/index.js" + }, + "keywords": [], + "author": "Eithan Hersey-Tuit", + "license": "MIT", + "dependencies": { + "@jobber/grpc": "workspace:*", + "@jobber/common": "workspace:*", + "@grpc/grpc-js": "^1.14.3", + "@grpc/proto-loader": "^0.8.0", + "@hono/node-server": "^1.13.7", + "@jobber/tcp-frame-socket": "workspace:*", + "hono": "^4.6.11", + "long": "^5.3.2", + "nice-grpc": "^2.1.14", + "prom-client": "^15.1.3", + "protobufjs": "^8.0.0", + "reflect-metadata": "^0.2.2", + "semver": "^7.6.3", + "tsyringe": "^4.10.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/node": "^20.16.12", + "@types/semver": "^7.5.8", + "grpc-tools": "^1.13.1", + "rimraf": "^5.0.10", + "ts-proto": "^2.10.1", + "tsc-alias": "^1.8.10", + "typescript": "^5.6.3", + "vitest": "^3.2.4" + } +} diff --git a/packages/gateway/src/config.ts b/packages/gateway/src/config.ts new file mode 100644 index 0000000..80fe64b --- /dev/null +++ b/packages/gateway/src/config.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; + +export const ConfigurationOptionsSchema = z.object({ + // The port the gateway will listen to traffic on + GATEWAY_PORT: z.coerce.number().default(3001), + + // API Key for the gateway to authenticate with the central server + GRPC_MANAGEMENT_TOKEN: z.string(), + GRPC_MANAGEMENT_URL: z.string(), +}); + +export type ConfigurationOptionsSchemaType = z.infer< + typeof ConfigurationOptionsSchema +>; + +export type ConfigurationOptions = keyof ConfigurationOptionsSchemaType; + +export const getConfigOption = ( + option: T +): ConfigurationOptionsSchemaType[T] => { + const schema = ConfigurationOptionsSchema.shape[option]; + + return schema.parse(process.env[option], { + path: ["config", option], + }) as ConfigurationOptionsSchemaType[T]; +}; diff --git a/packages/gateway/src/gateway.ts b/packages/gateway/src/gateway.ts new file mode 100644 index 0000000..50d509c --- /dev/null +++ b/packages/gateway/src/gateway.ts @@ -0,0 +1,311 @@ +import { LoopBase } from "@jobber/common"; +import { + HttpEventRequest, + HttpHeaders, + RunnerDefinition, +} from "@jobber/grpc/runner.js"; +import { + Channel, + createChannel, + createClientFactory, + RawClient, +} from "nice-grpc"; +import { FromTsProtoServiceDefinition } from "nice-grpc/lib/service-definitions/ts-proto.js"; +import { IncomingMessage, Server } from "node:http"; +import { inject, singleton } from "tsyringe"; +import { GrpcClient } from "./grpc-client.js"; +import { + ActionItem, + JobItem, + RunnerItem, + TriggerHttpItem, +} from "@jobber/grpc/server.js"; +import assert from "node:assert"; +import { randomUUID } from "node:crypto"; +import { getConfigOption } from "./config.js"; + +type RunnerClient = RawClient>; + +async function asyncIterablePromiseAll( + iterable: AsyncIterable +): Promise { + const resolved: T[] = []; + + for await (const p of iterable) { + resolved.push(p); + } + + return resolved; +} + +@singleton() +export class GatewayClient extends LoopBase { + protected loopDuration = 1000; + + protected loopStarted = undefined; + protected loopClosed = undefined; + + private server: Server | null = null; + + private routes = new Map< + string, + { + hostname?: string; + method?: string; + path?: string; + + job: JobItem; + action: ActionItem; + trigger: TriggerHttpItem; + runners: RunnerItem[]; + } + >(); + + private runners = new Map< + string, + { + runner: RunnerItem; + channel: Channel; + client: RunnerClient; + } + >(); + + constructor( + @inject(GrpcClient, { isOptional: false }) + private grpcClient: GrpcClient + ) { + super(); + } + + protected async loopStarting() { + this.server = new Server(); + + this.server.listen(getConfigOption("GATEWAY_PORT")); + + this.server.on("request", async (req, res) => { + if (this.status === "neutral") { + res.statusCode = 503; + res.end("Service Unavailable - Gateway not started"); + return; + } + + if (this.status === "stopping") { + res.statusCode = 503; + res.end("Service Unavailable - Gateway stopping"); + return; + } + + const route = this.getRouteByRequest(req); + + if (!route) { + // handle bad gateway error + res.statusCode = 502; + res.end("Bad Gateway"); + return; + } + + if (route.action.runnerMode === "RUN_ONCE") { + throw new Error("RUN_ONCE not implemented yet"); + } + + let runner: RunnerItem; + + if (route.runners.length === 0) { + runner = await this.grpcClient.client.createRunner({ + jobId: route.job.id, + versionId: route.job.versionId, + }); + } else { + // select random runner + const randomIndex = Math.floor(Math.random() * route.runners.length); + runner = route.runners[randomIndex]; + } + + const runnerConnection = this.runners.get(runner.id); + if (!runnerConnection) { + res.statusCode = 502; + res.end("Bad Gateway - Runner connection not found"); + return; + } + + const requestIterable = + async function* (): AsyncIterable { + let headers: HttpHeaders[] = []; + + for (const [key, value] of Object.entries(req.headers)) { + if (Array.isArray(value)) { + for (const v of value) { + headers.push({ name: key, value: v }); + } + } else { + headers.push({ name: key, value: value || "" }); + } + } + + yield { + reqHead: { + id: randomUUID(), + scheme: "http", // TODO: this + method: req.method || "GET", + hostname: req.headers["host"] || "", + headers: headers, + path: req.url || "/", + }, + }; + + let dataSequence = 1; + + for await (const chunk of req) { + yield { + reqBody: { + id: randomUUID(), + seq: dataSequence++, + data: chunk, + end: false, + }, + }; + } + + yield { + reqBody: { + id: randomUUID(), + seq: dataSequence++, + data: new Uint8Array(0), + end: true, + }, + }; + }; + + const response = runnerConnection.client.eventHttp(requestIterable()); + + for await (const event of response) { + if (event.resHead) { + res.statusCode = event.resHead.status; + for (const header of event.resHead.headers) { + res.setHeader(header.name, header.value); + } + } + + if (event.resBody) { + res.write(event.resBody.data); + if (event.resBody.end) { + res.end(); + } + } + } + }); + } + + protected async loopClosing() { + // Await for server to close. + await new Promise((resolve, reject) => + this.server?.close((err) => { + if (err) { + reject(err); + } else { + resolve(true); + } + }) + ); + } + + protected async loopIteration() { + const gatewayConfig = await this.grpcClient.client.getGatewayConfig({}); + + for (const route of this.routes.values()) { + const hasRoute = gatewayConfig.items.find((item) => + item.httpTriggers.find( + (trigger) => + trigger.jobId === route.job.id && trigger.id === route.trigger.id + ) + ); + + if (!hasRoute) { + this.routes.delete(route.action.id); + + for (const runner of route.runners) { + const runnerInfo = this.runners.get(runner.id); + + if (!runnerInfo) { + continue; + } + + runnerInfo.channel.close(); + + this.runners.delete(runner.id); + } + } + } + + for (const item of gatewayConfig.items) { + for (const runner of item.runners) { + if (!this.runners.has(runner.id)) { + const channel = createChannel(`${runner.ip}:${runner.port}`); + + const client = createClientFactory().create( + RunnerDefinition, + channel + ); + + this.runners.set(runner.id, { + runner, + channel, + client, + }); + } + } + + for (const trigger of item.httpTriggers) { + assert(item.job); + assert(item.action); + + this.routes.set(trigger.id, { + hostname: trigger.hostname, + method: trigger.method, + path: trigger.path, + + job: item.job, + action: item.action, + trigger: trigger, + + runners: item.runners, + }); + } + } + } + + private getRouteByRequest(req: IncomingMessage) { + const host = req.headers["host"]; + const method = req.method; + const path = req.url; + + if (!host || !method || !path) { + return null; + } + + for (const route of this.routes.values()) { + if (route.hostname && route.hostname !== host) { + continue; + } + + if (route.method && route.method !== method) { + continue; + } + + if (route.path) { + if (route.path.startsWith("^")) { + const regex = new RegExp(route.path); + if (!regex.test(path)) { + continue; + } + } else if (route.path && route.path !== path) { + continue; + } + } + + return route; + } + + return null; + } +} diff --git a/packages/gateway/src/grpc-client.ts b/packages/gateway/src/grpc-client.ts new file mode 100644 index 0000000..6549c9a --- /dev/null +++ b/packages/gateway/src/grpc-client.ts @@ -0,0 +1,55 @@ +import { GeneralManagementDefinition } from "@jobber/grpc/server.js"; + +import { + Channel, + createChannel, + createClientFactory, + Metadata, + RawClient, +} from "nice-grpc"; +import { singleton, container } from "tsyringe"; +import { FromTsProtoServiceDefinition } from "nice-grpc/lib/service-definitions/ts-proto.js"; +import { LoopBase } from "@jobber/common"; + +@singleton() +export class GrpcClient extends LoopBase { + protected loopDuration = 1_000; + + protected loopStarted = undefined; + protected loopClosed = undefined; + + protected async loopIteration() {} + + private grpcChannel: Channel | null = null; + + private grpcClient: RawClient< + FromTsProtoServiceDefinition + > | null = null; + + protected async loopStarting() { + this.grpcChannel = createChannel(""); + + this.grpcClient = createClientFactory().create( + GeneralManagementDefinition, + this.grpcChannel + ); + } + + protected async loopClosing() { + this.grpcChannel?.close(); + this.grpcChannel = null; + this.grpcClient = null; + } + + public get client() { + if (!this.grpcClient) { + throw new Error("GrpcClient not started"); + } + + return this.grpcClient; + } +} + +export const getGrpcClient = () => { + return container.resolve(GrpcClient); +}; diff --git a/packages/gateway/src/index.ts b/packages/gateway/src/index.ts new file mode 100644 index 0000000..339b692 --- /dev/null +++ b/packages/gateway/src/index.ts @@ -0,0 +1,31 @@ +import { container } from "tsyringe"; +import { getGrpcClient } from "./grpc-client.js"; +import { GatewayClient } from "./gateway.js"; + +async function main() { + console.log("Starting Gateway Management Client..."); + + console.log("Starting gRPC Client..."); + const grpc = getGrpcClient(); + await grpc.start(); + console.log("started"); + + console.log("Starting Gateway..."); + const gateway = container.resolve(GatewayClient); + await gateway.start(); + console.log("Gateway Management Client started"); + + process.once("SIGINT", async () => { + console.log("Stopping Gateway..."); + await gateway.stop(); + console.log("stopped"); + + console.log("Stopping gRPC Client..."); + await grpc.stop(); + console.log("stopped"); + + process.exit(0); + }); +} + +main(); diff --git a/packages/gateway/tsconfig.json b/packages/gateway/tsconfig.json new file mode 100644 index 0000000..0b57a00 --- /dev/null +++ b/packages/gateway/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "inlineSourceMap": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": false, + "types": ["node"], + "outDir": "./dist", + "paths":{ + "~/*": ["./src/*"] + } + }, + "include": [ + "./src" + ], + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Recommended" +} \ No newline at end of file diff --git a/packages/grpc/package.json b/packages/grpc/package.json new file mode 100644 index 0000000..c69e9c8 --- /dev/null +++ b/packages/grpc/package.json @@ -0,0 +1,37 @@ +{ + "name": "@jobber/grpc", + "version": "1.0.0", + "description": "gRPC Definitions for Jobber", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": "./dist/index.js", + "./*": "./dist/*" + }, + "type": "module", + "scripts": { + "grpc": "rm -r ./dist && protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src --ts_proto_opt=outputServices=nice-grpc,outputServices=generic-definitions,useExactTypes=false,stringEnums=true,esModuleInterop=true,enumsAsLiterals=true,outputDefaultValues=true --proto_path=./proto ./proto/*", + "build": "pnpm grpc && tsc" + }, + "keywords": [], + "author": "Eithan Hersey-Tuit", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.10.2", + "@grpc/grpc-js": "^1.14.3", + "@grpc/proto-loader": "^0.8.0", + "long": "^5.3.2", + "nice-grpc": "^2.1.14", + "nice-grpc-common": "^2.0.2", + "prom-client": "^15.1.3", + "protobufjs": "^8.0.0" + }, + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/node": "^20.16.12", + "grpc-tools": "^1.13.1", + "ts-proto": "^2.10.1", + "tsc-alias": "^1.8.10", + "typescript": "^5.6.3" + } +} diff --git a/packages/grpc/proto/runner.proto b/packages/grpc/proto/runner.proto new file mode 100644 index 0000000..171a4c5 --- /dev/null +++ b/packages/grpc/proto/runner.proto @@ -0,0 +1,79 @@ +syntax = "proto3"; + +package Runner; + +service Runner { + rpc eventHttp(stream HttpEventRequest) returns (stream HttpEventResponse); + rpc eventMqtt(MqttEventRequest) returns (MqttEventResponse); + rpc eventSchedule(ScheduleEventRequest) returns (ScheduleEventResponse); +} + +message HttpHeaders { + string name = 1; + string value = 2; +} + +message HttpBody { + string id = 1; + uint64 seq = 2; + bytes data = 3; + bool end = 4; +} + +message HttpRequestHead { + string id = 1; + string method = 2; + string scheme = 3; + string hostname = 4; + string path = 5; + repeated HttpHeaders headers = 6; +} + +message HttpResponseHead { + string id = 1; + int32 status = 2; + repeated HttpHeaders headers = 3; +} + +message HttpEventRequest { + oneof event { + HttpRequestHead req_head = 1; + HttpBody req_body = 2; + } +} + +message HttpEventResponse { + oneof event { + HttpResponseHead res_head = 1; + HttpBody res_body = 2; + } +} + +message MqttEventRequest { + string topic = 1; + bytes payload = 2; +} + +message MqttEventResponse { + enum Status { + ACCEPTED = 0; + REJECTED = 1; + } + + Status status = 1; +} + +message ScheduleEventRequest { + string id = 1; + string name = 2; + uint64 scheduled_time = 3; +} + +message ScheduleEventResponse { + enum Status { + ACCEPTED = 0; + REJECTED = 1; + } + + Status status = 1; +} \ No newline at end of file diff --git a/packages/grpc/proto/server.proto b/packages/grpc/proto/server.proto new file mode 100644 index 0000000..743d9d2 --- /dev/null +++ b/packages/grpc/proto/server.proto @@ -0,0 +1,240 @@ +syntax = "proto3"; + +package Server; + +message Empty {} + +message ExportChunk { + string id = 1; + int64 sequence = 2; + bytes data = 3; +} + +service GeneralManagement { + // General + rpc getJobs (Empty) returns (stream JobItem); + rpc getJob (GetJobRequest) returns (JobItem); + + rpc getAction (GetActionRequest) returns (ActionItem); + + rpc getHttpTriggers (GetTriggerRequest) returns (stream TriggerHttpItem); + rpc getMqttTriggers (GetTriggerRequest) returns (stream TriggerMqttItem); + rpc getScheduleTriggers (GetTriggerRequest) returns (stream TriggerScheduleItem); + rpc getRunners (GetRunnersRequest) returns (stream RunnerItem); + + // Authentication related + rpc getPublicKeys(Empty) returns (PublicKeysResponse); + rpc createJwt (JwtRequest) returns (JwtResponse); + + + // Gateway Specific + rpc createRunner(CreateRunnerRequest) returns (RunnerItem); + rpc getGatewayConfig(Empty) returns (GetGatewayConfigResponse); +} + +message GetJobRequest { + string id = 1; +} + +message GetActionRequest { + string jobId = 1; + optional string versionId = 2; + optional string actionId = 3; +} + +message GetTriggerRequest { + string jobId = 1; + optional string versionId = 2; + optional string triggerId = 3; +} + +message GetRunnersRequest { + optional string jobId = 1; + optional string versionId = 2; + optional string actionId = 3; +} + +message CreateRunnerRequest { + string jobId = 1; + string versionId = 2; +} + +message PublicKeysResponse { + repeated string publicKeys = 1; +} + +message JwtRequest { + string token = 1; + repeated string scopes = 2; +} + +message JwtResponse { + string jwt = 1; +} + +message GetGatewayConfigResponse { + message Item { + JobItem job = 1; + ActionItem action = 2; + repeated TriggerHttpItem httpTriggers = 3; + repeated RunnerItem runners = 4; + } + + repeated Item items = 1; +} + + +// General Types + +enum JobStatus { + ENABLED = 0; + DISABLED = 1; +} + + +message JobItem { + string id = 1; + string jobName = 2; + optional string versionId = 3; + + JobStatus status = 4; +} + +enum ActionRunnerMode { + STANDARD = 0; + RUN_ONCE = 1; +} + +message ActionItem { + string id = 1; + bool runnerAsynchronous = 2; + uint32 runnerMinCount = 3; + uint32 runnerMaxCount = 4; + uint32 runnerTimeout = 5; + uint32 runnerMaxIdleAge = 6; + uint32 runnerMaxAge = 7; + uint32 runnerMaxAgeHard = 8; + ActionRunnerMode runnerMode = 9; +} + + +message TriggerHttpItem { + string id = 1; + string jobId = 2; + string jobVersionId = 3; + + optional string name = 4; + optional string method = 5; + optional string hostname = 6; + optional string path = 7; + + uint64 createdAt = 8; +} + +message TriggerMqttItem { + string id = 1; + string jobId = 2; + string jobVersionId = 3; + + optional string name = 4; + repeated string topics = 5; + optional string protocol = 6; + optional string port = 7; + optional string host = 8; + optional string username = 9; + optional string password = 10; + optional string clientId = 11; + + uint64 createdAt = 12; +} + +message TriggerScheduleItem { + string id = 1; + string jobId = 2; + string jobVersionId = 3; + + optional string name = 4; + string cron = 5; + optional string timezone = 6; + + uint64 createdAt = 7; +} + +enum RunnerStatus { + STARTING = 0; + READY = 1; + CLOSING = 2; + CLOSED = 3; +} + +message RunnerItem { + string id = 1; + string jobId = 2; + string jobVersionId = 3; + string actionId = 4; + + RunnerStatus status = 5; + + string ip = 6; + string hostname = 7; + uint32 port = 8; + + reserved 9; + + uint64 createdAt = 10; + optional uint64 readyAt = 11; + optional uint64 closingAt = 12; + optional uint64 closedAt = 13; +} + + + + +service RunnerManagement { + rpc storeGet (StoreGetRequest) returns (StoreItem); + rpc storeSet (StoreSetRequest) returns (StoreItem); + rpc storeDelete (StoreDeleteRequest) returns (StoreItem); + rpc mqttPublish (MqttPublishRequest) returns (Empty); + + // Backend management methods + rpc init(InitRequest) returns (InitResponse); + rpc archive(Empty) returns (stream ExportChunk); +} + +message StoreGetRequest { + string key = 1; +} + +message StoreSetRequest { + string key = 1; + string value = 2; + uint64 ttl = 3; +} + +message StoreDeleteRequest { + string key = 1; +} + +message StoreItem { + string key = 1; + string value = 2; + uint64 expiry = 3; + uint64 createdAt = 4; + uint64 modified = 5; +} + +message MqttPublishRequest { + string topic = 1; + bytes payload = 2; +} + +message InitRequest { + string token = 1; +} + +message InitResponse { + enum Status { + SUCCESS = 0; + FAILURE_GENERIC = 1; + } +} diff --git a/packages/grpc/src/runner.ts b/packages/grpc/src/runner.ts new file mode 100644 index 0000000..3433ca5 --- /dev/null +++ b/packages/grpc/src/runner.ts @@ -0,0 +1,1157 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.10.1 +// protoc v3.21.12 +// source: runner.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; +import type { CallContext, CallOptions } from "nice-grpc-common"; + +export const protobufPackage = "Runner"; + +export interface HttpHeaders { + name: string; + value: string; +} + +export interface HttpBody { + id: string; + seq: number; + data: Uint8Array; + end: boolean; +} + +export interface HttpRequestHead { + id: string; + method: string; + scheme: string; + hostname: string; + path: string; + headers: HttpHeaders[]; +} + +export interface HttpResponseHead { + id: string; + status: number; + headers: HttpHeaders[]; +} + +export interface HttpEventRequest { + reqHead?: HttpRequestHead | undefined; + reqBody?: HttpBody | undefined; +} + +export interface HttpEventResponse { + resHead?: HttpResponseHead | undefined; + resBody?: HttpBody | undefined; +} + +export interface MqttEventRequest { + topic: string; + payload: Uint8Array; +} + +export interface MqttEventResponse { + status: MqttEventResponse_Status; +} + +export const MqttEventResponse_Status = { + ACCEPTED: "ACCEPTED", + REJECTED: "REJECTED", + UNRECOGNIZED: "UNRECOGNIZED", +} as const; + +export type MqttEventResponse_Status = typeof MqttEventResponse_Status[keyof typeof MqttEventResponse_Status]; + +export namespace MqttEventResponse_Status { + export type ACCEPTED = typeof MqttEventResponse_Status.ACCEPTED; + export type REJECTED = typeof MqttEventResponse_Status.REJECTED; + export type UNRECOGNIZED = typeof MqttEventResponse_Status.UNRECOGNIZED; +} + +export function mqttEventResponse_StatusFromJSON(object: any): MqttEventResponse_Status { + switch (object) { + case 0: + case "ACCEPTED": + return MqttEventResponse_Status.ACCEPTED; + case 1: + case "REJECTED": + return MqttEventResponse_Status.REJECTED; + case -1: + case "UNRECOGNIZED": + default: + return MqttEventResponse_Status.UNRECOGNIZED; + } +} + +export function mqttEventResponse_StatusToJSON(object: MqttEventResponse_Status): string { + switch (object) { + case MqttEventResponse_Status.ACCEPTED: + return "ACCEPTED"; + case MqttEventResponse_Status.REJECTED: + return "REJECTED"; + case MqttEventResponse_Status.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function mqttEventResponse_StatusToNumber(object: MqttEventResponse_Status): number { + switch (object) { + case MqttEventResponse_Status.ACCEPTED: + return 0; + case MqttEventResponse_Status.REJECTED: + return 1; + case MqttEventResponse_Status.UNRECOGNIZED: + default: + return -1; + } +} + +export interface ScheduleEventRequest { + id: string; + name: string; + scheduledTime: number; +} + +export interface ScheduleEventResponse { + status: ScheduleEventResponse_Status; +} + +export const ScheduleEventResponse_Status = { + ACCEPTED: "ACCEPTED", + REJECTED: "REJECTED", + UNRECOGNIZED: "UNRECOGNIZED", +} as const; + +export type ScheduleEventResponse_Status = + typeof ScheduleEventResponse_Status[keyof typeof ScheduleEventResponse_Status]; + +export namespace ScheduleEventResponse_Status { + export type ACCEPTED = typeof ScheduleEventResponse_Status.ACCEPTED; + export type REJECTED = typeof ScheduleEventResponse_Status.REJECTED; + export type UNRECOGNIZED = typeof ScheduleEventResponse_Status.UNRECOGNIZED; +} + +export function scheduleEventResponse_StatusFromJSON(object: any): ScheduleEventResponse_Status { + switch (object) { + case 0: + case "ACCEPTED": + return ScheduleEventResponse_Status.ACCEPTED; + case 1: + case "REJECTED": + return ScheduleEventResponse_Status.REJECTED; + case -1: + case "UNRECOGNIZED": + default: + return ScheduleEventResponse_Status.UNRECOGNIZED; + } +} + +export function scheduleEventResponse_StatusToJSON(object: ScheduleEventResponse_Status): string { + switch (object) { + case ScheduleEventResponse_Status.ACCEPTED: + return "ACCEPTED"; + case ScheduleEventResponse_Status.REJECTED: + return "REJECTED"; + case ScheduleEventResponse_Status.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function scheduleEventResponse_StatusToNumber(object: ScheduleEventResponse_Status): number { + switch (object) { + case ScheduleEventResponse_Status.ACCEPTED: + return 0; + case ScheduleEventResponse_Status.REJECTED: + return 1; + case ScheduleEventResponse_Status.UNRECOGNIZED: + default: + return -1; + } +} + +function createBaseHttpHeaders(): HttpHeaders { + return { name: "", value: "" }; +} + +export const HttpHeaders: MessageFns = { + encode(message: HttpHeaders, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.name !== "") { + writer.uint32(10).string(message.name); + } + if (message.value !== "") { + writer.uint32(18).string(message.value); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): HttpHeaders { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseHttpHeaders(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.name = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.value = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): HttpHeaders { + return { + name: isSet(object.name) ? globalThis.String(object.name) : "", + value: isSet(object.value) ? globalThis.String(object.value) : "", + }; + }, + + toJSON(message: HttpHeaders): unknown { + const obj: any = {}; + if (message.name !== "") { + obj.name = message.name; + } + if (message.value !== "") { + obj.value = message.value; + } + return obj; + }, + + create(base?: DeepPartial): HttpHeaders { + return HttpHeaders.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): HttpHeaders { + const message = createBaseHttpHeaders(); + message.name = object.name ?? ""; + message.value = object.value ?? ""; + return message; + }, +}; + +function createBaseHttpBody(): HttpBody { + return { id: "", seq: 0, data: new Uint8Array(0), end: false }; +} + +export const HttpBody: MessageFns = { + encode(message: HttpBody, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.seq !== 0) { + writer.uint32(16).uint64(message.seq); + } + if (message.data.length !== 0) { + writer.uint32(26).bytes(message.data); + } + if (message.end !== false) { + writer.uint32(32).bool(message.end); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): HttpBody { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseHttpBody(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.seq = longToNumber(reader.uint64()); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.data = reader.bytes(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.end = reader.bool(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): HttpBody { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + seq: isSet(object.seq) ? globalThis.Number(object.seq) : 0, + data: isSet(object.data) ? bytesFromBase64(object.data) : new Uint8Array(0), + end: isSet(object.end) ? globalThis.Boolean(object.end) : false, + }; + }, + + toJSON(message: HttpBody): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.seq !== 0) { + obj.seq = Math.round(message.seq); + } + if (message.data.length !== 0) { + obj.data = base64FromBytes(message.data); + } + if (message.end !== false) { + obj.end = message.end; + } + return obj; + }, + + create(base?: DeepPartial): HttpBody { + return HttpBody.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): HttpBody { + const message = createBaseHttpBody(); + message.id = object.id ?? ""; + message.seq = object.seq ?? 0; + message.data = object.data ?? new Uint8Array(0); + message.end = object.end ?? false; + return message; + }, +}; + +function createBaseHttpRequestHead(): HttpRequestHead { + return { id: "", method: "", scheme: "", hostname: "", path: "", headers: [] }; +} + +export const HttpRequestHead: MessageFns = { + encode(message: HttpRequestHead, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.method !== "") { + writer.uint32(18).string(message.method); + } + if (message.scheme !== "") { + writer.uint32(26).string(message.scheme); + } + if (message.hostname !== "") { + writer.uint32(34).string(message.hostname); + } + if (message.path !== "") { + writer.uint32(42).string(message.path); + } + for (const v of message.headers) { + HttpHeaders.encode(v!, writer.uint32(50).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): HttpRequestHead { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseHttpRequestHead(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.method = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.scheme = reader.string(); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.hostname = reader.string(); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.path = reader.string(); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.headers.push(HttpHeaders.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): HttpRequestHead { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + method: isSet(object.method) ? globalThis.String(object.method) : "", + scheme: isSet(object.scheme) ? globalThis.String(object.scheme) : "", + hostname: isSet(object.hostname) ? globalThis.String(object.hostname) : "", + path: isSet(object.path) ? globalThis.String(object.path) : "", + headers: globalThis.Array.isArray(object?.headers) ? object.headers.map((e: any) => HttpHeaders.fromJSON(e)) : [], + }; + }, + + toJSON(message: HttpRequestHead): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.method !== "") { + obj.method = message.method; + } + if (message.scheme !== "") { + obj.scheme = message.scheme; + } + if (message.hostname !== "") { + obj.hostname = message.hostname; + } + if (message.path !== "") { + obj.path = message.path; + } + if (message.headers?.length) { + obj.headers = message.headers.map((e) => HttpHeaders.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): HttpRequestHead { + return HttpRequestHead.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): HttpRequestHead { + const message = createBaseHttpRequestHead(); + message.id = object.id ?? ""; + message.method = object.method ?? ""; + message.scheme = object.scheme ?? ""; + message.hostname = object.hostname ?? ""; + message.path = object.path ?? ""; + message.headers = object.headers?.map((e) => HttpHeaders.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseHttpResponseHead(): HttpResponseHead { + return { id: "", status: 0, headers: [] }; +} + +export const HttpResponseHead: MessageFns = { + encode(message: HttpResponseHead, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.status !== 0) { + writer.uint32(16).int32(message.status); + } + for (const v of message.headers) { + HttpHeaders.encode(v!, writer.uint32(26).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): HttpResponseHead { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseHttpResponseHead(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.status = reader.int32(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.headers.push(HttpHeaders.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): HttpResponseHead { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + status: isSet(object.status) ? globalThis.Number(object.status) : 0, + headers: globalThis.Array.isArray(object?.headers) ? object.headers.map((e: any) => HttpHeaders.fromJSON(e)) : [], + }; + }, + + toJSON(message: HttpResponseHead): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.status !== 0) { + obj.status = Math.round(message.status); + } + if (message.headers?.length) { + obj.headers = message.headers.map((e) => HttpHeaders.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): HttpResponseHead { + return HttpResponseHead.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): HttpResponseHead { + const message = createBaseHttpResponseHead(); + message.id = object.id ?? ""; + message.status = object.status ?? 0; + message.headers = object.headers?.map((e) => HttpHeaders.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseHttpEventRequest(): HttpEventRequest { + return { reqHead: undefined, reqBody: undefined }; +} + +export const HttpEventRequest: MessageFns = { + encode(message: HttpEventRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.reqHead !== undefined) { + HttpRequestHead.encode(message.reqHead, writer.uint32(10).fork()).join(); + } + if (message.reqBody !== undefined) { + HttpBody.encode(message.reqBody, writer.uint32(18).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): HttpEventRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseHttpEventRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.reqHead = HttpRequestHead.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.reqBody = HttpBody.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): HttpEventRequest { + return { + reqHead: isSet(object.reqHead) ? HttpRequestHead.fromJSON(object.reqHead) : undefined, + reqBody: isSet(object.reqBody) ? HttpBody.fromJSON(object.reqBody) : undefined, + }; + }, + + toJSON(message: HttpEventRequest): unknown { + const obj: any = {}; + if (message.reqHead !== undefined) { + obj.reqHead = HttpRequestHead.toJSON(message.reqHead); + } + if (message.reqBody !== undefined) { + obj.reqBody = HttpBody.toJSON(message.reqBody); + } + return obj; + }, + + create(base?: DeepPartial): HttpEventRequest { + return HttpEventRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): HttpEventRequest { + const message = createBaseHttpEventRequest(); + message.reqHead = (object.reqHead !== undefined && object.reqHead !== null) + ? HttpRequestHead.fromPartial(object.reqHead) + : undefined; + message.reqBody = (object.reqBody !== undefined && object.reqBody !== null) + ? HttpBody.fromPartial(object.reqBody) + : undefined; + return message; + }, +}; + +function createBaseHttpEventResponse(): HttpEventResponse { + return { resHead: undefined, resBody: undefined }; +} + +export const HttpEventResponse: MessageFns = { + encode(message: HttpEventResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.resHead !== undefined) { + HttpResponseHead.encode(message.resHead, writer.uint32(10).fork()).join(); + } + if (message.resBody !== undefined) { + HttpBody.encode(message.resBody, writer.uint32(18).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): HttpEventResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseHttpEventResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.resHead = HttpResponseHead.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.resBody = HttpBody.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): HttpEventResponse { + return { + resHead: isSet(object.resHead) ? HttpResponseHead.fromJSON(object.resHead) : undefined, + resBody: isSet(object.resBody) ? HttpBody.fromJSON(object.resBody) : undefined, + }; + }, + + toJSON(message: HttpEventResponse): unknown { + const obj: any = {}; + if (message.resHead !== undefined) { + obj.resHead = HttpResponseHead.toJSON(message.resHead); + } + if (message.resBody !== undefined) { + obj.resBody = HttpBody.toJSON(message.resBody); + } + return obj; + }, + + create(base?: DeepPartial): HttpEventResponse { + return HttpEventResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): HttpEventResponse { + const message = createBaseHttpEventResponse(); + message.resHead = (object.resHead !== undefined && object.resHead !== null) + ? HttpResponseHead.fromPartial(object.resHead) + : undefined; + message.resBody = (object.resBody !== undefined && object.resBody !== null) + ? HttpBody.fromPartial(object.resBody) + : undefined; + return message; + }, +}; + +function createBaseMqttEventRequest(): MqttEventRequest { + return { topic: "", payload: new Uint8Array(0) }; +} + +export const MqttEventRequest: MessageFns = { + encode(message: MqttEventRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.topic !== "") { + writer.uint32(10).string(message.topic); + } + if (message.payload.length !== 0) { + writer.uint32(18).bytes(message.payload); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): MqttEventRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseMqttEventRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.topic = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.payload = reader.bytes(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): MqttEventRequest { + return { + topic: isSet(object.topic) ? globalThis.String(object.topic) : "", + payload: isSet(object.payload) ? bytesFromBase64(object.payload) : new Uint8Array(0), + }; + }, + + toJSON(message: MqttEventRequest): unknown { + const obj: any = {}; + if (message.topic !== "") { + obj.topic = message.topic; + } + if (message.payload.length !== 0) { + obj.payload = base64FromBytes(message.payload); + } + return obj; + }, + + create(base?: DeepPartial): MqttEventRequest { + return MqttEventRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): MqttEventRequest { + const message = createBaseMqttEventRequest(); + message.topic = object.topic ?? ""; + message.payload = object.payload ?? new Uint8Array(0); + return message; + }, +}; + +function createBaseMqttEventResponse(): MqttEventResponse { + return { status: MqttEventResponse_Status.ACCEPTED }; +} + +export const MqttEventResponse: MessageFns = { + encode(message: MqttEventResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.status !== MqttEventResponse_Status.ACCEPTED) { + writer.uint32(8).int32(mqttEventResponse_StatusToNumber(message.status)); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): MqttEventResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseMqttEventResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.status = mqttEventResponse_StatusFromJSON(reader.int32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): MqttEventResponse { + return { + status: isSet(object.status) + ? mqttEventResponse_StatusFromJSON(object.status) + : MqttEventResponse_Status.ACCEPTED, + }; + }, + + toJSON(message: MqttEventResponse): unknown { + const obj: any = {}; + if (message.status !== MqttEventResponse_Status.ACCEPTED) { + obj.status = mqttEventResponse_StatusToJSON(message.status); + } + return obj; + }, + + create(base?: DeepPartial): MqttEventResponse { + return MqttEventResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): MqttEventResponse { + const message = createBaseMqttEventResponse(); + message.status = object.status ?? MqttEventResponse_Status.ACCEPTED; + return message; + }, +}; + +function createBaseScheduleEventRequest(): ScheduleEventRequest { + return { id: "", name: "", scheduledTime: 0 }; +} + +export const ScheduleEventRequest: MessageFns = { + encode(message: ScheduleEventRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.name !== "") { + writer.uint32(18).string(message.name); + } + if (message.scheduledTime !== 0) { + writer.uint32(24).uint64(message.scheduledTime); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ScheduleEventRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseScheduleEventRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.name = reader.string(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.scheduledTime = longToNumber(reader.uint64()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ScheduleEventRequest { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + name: isSet(object.name) ? globalThis.String(object.name) : "", + scheduledTime: isSet(object.scheduledTime) ? globalThis.Number(object.scheduledTime) : 0, + }; + }, + + toJSON(message: ScheduleEventRequest): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.name !== "") { + obj.name = message.name; + } + if (message.scheduledTime !== 0) { + obj.scheduledTime = Math.round(message.scheduledTime); + } + return obj; + }, + + create(base?: DeepPartial): ScheduleEventRequest { + return ScheduleEventRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ScheduleEventRequest { + const message = createBaseScheduleEventRequest(); + message.id = object.id ?? ""; + message.name = object.name ?? ""; + message.scheduledTime = object.scheduledTime ?? 0; + return message; + }, +}; + +function createBaseScheduleEventResponse(): ScheduleEventResponse { + return { status: ScheduleEventResponse_Status.ACCEPTED }; +} + +export const ScheduleEventResponse: MessageFns = { + encode(message: ScheduleEventResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.status !== ScheduleEventResponse_Status.ACCEPTED) { + writer.uint32(8).int32(scheduleEventResponse_StatusToNumber(message.status)); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ScheduleEventResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseScheduleEventResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.status = scheduleEventResponse_StatusFromJSON(reader.int32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ScheduleEventResponse { + return { + status: isSet(object.status) + ? scheduleEventResponse_StatusFromJSON(object.status) + : ScheduleEventResponse_Status.ACCEPTED, + }; + }, + + toJSON(message: ScheduleEventResponse): unknown { + const obj: any = {}; + if (message.status !== ScheduleEventResponse_Status.ACCEPTED) { + obj.status = scheduleEventResponse_StatusToJSON(message.status); + } + return obj; + }, + + create(base?: DeepPartial): ScheduleEventResponse { + return ScheduleEventResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ScheduleEventResponse { + const message = createBaseScheduleEventResponse(); + message.status = object.status ?? ScheduleEventResponse_Status.ACCEPTED; + return message; + }, +}; + +export type RunnerDefinition = typeof RunnerDefinition; +export const RunnerDefinition = { + name: "Runner", + fullName: "Runner.Runner", + methods: { + eventHttp: { + name: "eventHttp", + requestType: HttpEventRequest, + requestStream: true, + responseType: HttpEventResponse, + responseStream: true, + options: {}, + }, + eventMqtt: { + name: "eventMqtt", + requestType: MqttEventRequest, + requestStream: false, + responseType: MqttEventResponse, + responseStream: false, + options: {}, + }, + eventSchedule: { + name: "eventSchedule", + requestType: ScheduleEventRequest, + requestStream: false, + responseType: ScheduleEventResponse, + responseStream: false, + options: {}, + }, + }, +} as const; + +export interface RunnerServiceImplementation { + eventHttp( + request: AsyncIterable, + context: CallContext & CallContextExt, + ): ServerStreamingMethodResult>; + eventMqtt(request: MqttEventRequest, context: CallContext & CallContextExt): Promise>; + eventSchedule( + request: ScheduleEventRequest, + context: CallContext & CallContextExt, + ): Promise>; +} + +export interface RunnerClient { + eventHttp( + request: AsyncIterable>, + options?: CallOptions & CallOptionsExt, + ): AsyncIterable; + eventMqtt(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + eventSchedule( + request: DeepPartial, + options?: CallOptions & CallOptionsExt, + ): Promise; +} + +function bytesFromBase64(b64: string): Uint8Array { + if ((globalThis as any).Buffer) { + return Uint8Array.from(globalThis.Buffer.from(b64, "base64")); + } else { + const bin = globalThis.atob(b64); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; ++i) { + arr[i] = bin.charCodeAt(i); + } + return arr; + } +} + +function base64FromBytes(arr: Uint8Array): string { + if ((globalThis as any).Buffer) { + return globalThis.Buffer.from(arr).toString("base64"); + } else { + const bin: string[] = []; + arr.forEach((byte) => { + bin.push(globalThis.String.fromCharCode(byte)); + }); + return globalThis.btoa(bin.join("")); + } +} + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +function longToNumber(int64: { toString(): string }): number { + const num = globalThis.Number(int64.toString()); + if (num > globalThis.Number.MAX_SAFE_INTEGER) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + if (num < globalThis.Number.MIN_SAFE_INTEGER) { + throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); + } + return num; +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export type ServerStreamingMethodResult = { [Symbol.asyncIterator](): AsyncIterator }; + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create(base?: DeepPartial): T; + fromPartial(object: DeepPartial): T; +} diff --git a/packages/grpc/src/server.ts b/packages/grpc/src/server.ts new file mode 100644 index 0000000..1810929 --- /dev/null +++ b/packages/grpc/src/server.ts @@ -0,0 +1,3261 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.10.1 +// protoc v3.21.12 +// source: server.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; +import type { CallContext, CallOptions } from "nice-grpc-common"; + +export const protobufPackage = "Server"; + +export const JobStatus = { ENABLED: "ENABLED", DISABLED: "DISABLED", UNRECOGNIZED: "UNRECOGNIZED" } as const; + +export type JobStatus = typeof JobStatus[keyof typeof JobStatus]; + +export namespace JobStatus { + export type ENABLED = typeof JobStatus.ENABLED; + export type DISABLED = typeof JobStatus.DISABLED; + export type UNRECOGNIZED = typeof JobStatus.UNRECOGNIZED; +} + +export function jobStatusFromJSON(object: any): JobStatus { + switch (object) { + case 0: + case "ENABLED": + return JobStatus.ENABLED; + case 1: + case "DISABLED": + return JobStatus.DISABLED; + case -1: + case "UNRECOGNIZED": + default: + return JobStatus.UNRECOGNIZED; + } +} + +export function jobStatusToJSON(object: JobStatus): string { + switch (object) { + case JobStatus.ENABLED: + return "ENABLED"; + case JobStatus.DISABLED: + return "DISABLED"; + case JobStatus.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function jobStatusToNumber(object: JobStatus): number { + switch (object) { + case JobStatus.ENABLED: + return 0; + case JobStatus.DISABLED: + return 1; + case JobStatus.UNRECOGNIZED: + default: + return -1; + } +} + +export const ActionRunnerMode = { STANDARD: "STANDARD", RUN_ONCE: "RUN_ONCE", UNRECOGNIZED: "UNRECOGNIZED" } as const; + +export type ActionRunnerMode = typeof ActionRunnerMode[keyof typeof ActionRunnerMode]; + +export namespace ActionRunnerMode { + export type STANDARD = typeof ActionRunnerMode.STANDARD; + export type RUN_ONCE = typeof ActionRunnerMode.RUN_ONCE; + export type UNRECOGNIZED = typeof ActionRunnerMode.UNRECOGNIZED; +} + +export function actionRunnerModeFromJSON(object: any): ActionRunnerMode { + switch (object) { + case 0: + case "STANDARD": + return ActionRunnerMode.STANDARD; + case 1: + case "RUN_ONCE": + return ActionRunnerMode.RUN_ONCE; + case -1: + case "UNRECOGNIZED": + default: + return ActionRunnerMode.UNRECOGNIZED; + } +} + +export function actionRunnerModeToJSON(object: ActionRunnerMode): string { + switch (object) { + case ActionRunnerMode.STANDARD: + return "STANDARD"; + case ActionRunnerMode.RUN_ONCE: + return "RUN_ONCE"; + case ActionRunnerMode.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function actionRunnerModeToNumber(object: ActionRunnerMode): number { + switch (object) { + case ActionRunnerMode.STANDARD: + return 0; + case ActionRunnerMode.RUN_ONCE: + return 1; + case ActionRunnerMode.UNRECOGNIZED: + default: + return -1; + } +} + +export const RunnerStatus = { + STARTING: "STARTING", + READY: "READY", + CLOSING: "CLOSING", + CLOSED: "CLOSED", + UNRECOGNIZED: "UNRECOGNIZED", +} as const; + +export type RunnerStatus = typeof RunnerStatus[keyof typeof RunnerStatus]; + +export namespace RunnerStatus { + export type STARTING = typeof RunnerStatus.STARTING; + export type READY = typeof RunnerStatus.READY; + export type CLOSING = typeof RunnerStatus.CLOSING; + export type CLOSED = typeof RunnerStatus.CLOSED; + export type UNRECOGNIZED = typeof RunnerStatus.UNRECOGNIZED; +} + +export function runnerStatusFromJSON(object: any): RunnerStatus { + switch (object) { + case 0: + case "STARTING": + return RunnerStatus.STARTING; + case 1: + case "READY": + return RunnerStatus.READY; + case 2: + case "CLOSING": + return RunnerStatus.CLOSING; + case 3: + case "CLOSED": + return RunnerStatus.CLOSED; + case -1: + case "UNRECOGNIZED": + default: + return RunnerStatus.UNRECOGNIZED; + } +} + +export function runnerStatusToJSON(object: RunnerStatus): string { + switch (object) { + case RunnerStatus.STARTING: + return "STARTING"; + case RunnerStatus.READY: + return "READY"; + case RunnerStatus.CLOSING: + return "CLOSING"; + case RunnerStatus.CLOSED: + return "CLOSED"; + case RunnerStatus.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function runnerStatusToNumber(object: RunnerStatus): number { + switch (object) { + case RunnerStatus.STARTING: + return 0; + case RunnerStatus.READY: + return 1; + case RunnerStatus.CLOSING: + return 2; + case RunnerStatus.CLOSED: + return 3; + case RunnerStatus.UNRECOGNIZED: + default: + return -1; + } +} + +export interface Empty { +} + +export interface ExportChunk { + id: string; + sequence: number; + data: Uint8Array; +} + +export interface GetJobRequest { + id: string; +} + +export interface GetActionRequest { + jobId: string; + versionId?: string | undefined; + actionId?: string | undefined; +} + +export interface GetTriggerRequest { + jobId: string; + versionId?: string | undefined; + triggerId?: string | undefined; +} + +export interface GetRunnersRequest { + jobId?: string | undefined; + versionId?: string | undefined; + actionId?: string | undefined; +} + +export interface CreateRunnerRequest { + jobId: string; + versionId: string; +} + +export interface PublicKeysResponse { + publicKeys: string[]; +} + +export interface JwtRequest { + token: string; + scopes: string[]; +} + +export interface JwtResponse { + jwt: string; +} + +export interface GetGatewayConfigResponse { + items: GetGatewayConfigResponse_Item[]; +} + +export interface GetGatewayConfigResponse_Item { + job: JobItem | undefined; + action: ActionItem | undefined; + httpTriggers: TriggerHttpItem[]; + runners: RunnerItem[]; +} + +export interface JobItem { + id: string; + jobName: string; + versionId?: string | undefined; + status: JobStatus; +} + +export interface ActionItem { + id: string; + runnerAsynchronous: boolean; + runnerMinCount: number; + runnerMaxCount: number; + runnerTimeout: number; + runnerMaxIdleAge: number; + runnerMaxAge: number; + runnerMaxAgeHard: number; + runnerMode: ActionRunnerMode; +} + +export interface TriggerHttpItem { + id: string; + jobId: string; + jobVersionId: string; + name?: string | undefined; + method?: string | undefined; + hostname?: string | undefined; + path?: string | undefined; + createdAt: number; +} + +export interface TriggerMqttItem { + id: string; + jobId: string; + jobVersionId: string; + name?: string | undefined; + topics: string[]; + protocol?: string | undefined; + port?: string | undefined; + host?: string | undefined; + username?: string | undefined; + password?: string | undefined; + clientId?: string | undefined; + createdAt: number; +} + +export interface TriggerScheduleItem { + id: string; + jobId: string; + jobVersionId: string; + name?: string | undefined; + cron: string; + timezone?: string | undefined; + createdAt: number; +} + +export interface RunnerItem { + id: string; + jobId: string; + jobVersionId: string; + actionId: string; + status: RunnerStatus; + ip: string; + hostname: string; + port: number; + createdAt: number; + readyAt?: number | undefined; + closingAt?: number | undefined; + closedAt?: number | undefined; +} + +export interface StoreGetRequest { + key: string; +} + +export interface StoreSetRequest { + key: string; + value: string; + ttl: number; +} + +export interface StoreDeleteRequest { + key: string; +} + +export interface StoreItem { + key: string; + value: string; + expiry: number; + createdAt: number; + modified: number; +} + +export interface MqttPublishRequest { + topic: string; + payload: Uint8Array; +} + +export interface InitRequest { + token: string; +} + +export interface InitResponse { +} + +export const InitResponse_Status = { + SUCCESS: "SUCCESS", + FAILURE_GENERIC: "FAILURE_GENERIC", + UNRECOGNIZED: "UNRECOGNIZED", +} as const; + +export type InitResponse_Status = typeof InitResponse_Status[keyof typeof InitResponse_Status]; + +export namespace InitResponse_Status { + export type SUCCESS = typeof InitResponse_Status.SUCCESS; + export type FAILURE_GENERIC = typeof InitResponse_Status.FAILURE_GENERIC; + export type UNRECOGNIZED = typeof InitResponse_Status.UNRECOGNIZED; +} + +export function initResponse_StatusFromJSON(object: any): InitResponse_Status { + switch (object) { + case 0: + case "SUCCESS": + return InitResponse_Status.SUCCESS; + case 1: + case "FAILURE_GENERIC": + return InitResponse_Status.FAILURE_GENERIC; + case -1: + case "UNRECOGNIZED": + default: + return InitResponse_Status.UNRECOGNIZED; + } +} + +export function initResponse_StatusToJSON(object: InitResponse_Status): string { + switch (object) { + case InitResponse_Status.SUCCESS: + return "SUCCESS"; + case InitResponse_Status.FAILURE_GENERIC: + return "FAILURE_GENERIC"; + case InitResponse_Status.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function initResponse_StatusToNumber(object: InitResponse_Status): number { + switch (object) { + case InitResponse_Status.SUCCESS: + return 0; + case InitResponse_Status.FAILURE_GENERIC: + return 1; + case InitResponse_Status.UNRECOGNIZED: + default: + return -1; + } +} + +function createBaseEmpty(): Empty { + return {}; +} + +export const Empty: MessageFns = { + encode(_: Empty, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Empty { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseEmpty(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(_: any): Empty { + return {}; + }, + + toJSON(_: Empty): unknown { + const obj: any = {}; + return obj; + }, + + create(base?: DeepPartial): Empty { + return Empty.fromPartial(base ?? {}); + }, + fromPartial(_: DeepPartial): Empty { + const message = createBaseEmpty(); + return message; + }, +}; + +function createBaseExportChunk(): ExportChunk { + return { id: "", sequence: 0, data: new Uint8Array(0) }; +} + +export const ExportChunk: MessageFns = { + encode(message: ExportChunk, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.sequence !== 0) { + writer.uint32(16).int64(message.sequence); + } + if (message.data.length !== 0) { + writer.uint32(26).bytes(message.data); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ExportChunk { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseExportChunk(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.sequence = longToNumber(reader.int64()); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.data = reader.bytes(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ExportChunk { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + sequence: isSet(object.sequence) ? globalThis.Number(object.sequence) : 0, + data: isSet(object.data) ? bytesFromBase64(object.data) : new Uint8Array(0), + }; + }, + + toJSON(message: ExportChunk): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.sequence !== 0) { + obj.sequence = Math.round(message.sequence); + } + if (message.data.length !== 0) { + obj.data = base64FromBytes(message.data); + } + return obj; + }, + + create(base?: DeepPartial): ExportChunk { + return ExportChunk.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ExportChunk { + const message = createBaseExportChunk(); + message.id = object.id ?? ""; + message.sequence = object.sequence ?? 0; + message.data = object.data ?? new Uint8Array(0); + return message; + }, +}; + +function createBaseGetJobRequest(): GetJobRequest { + return { id: "" }; +} + +export const GetJobRequest: MessageFns = { + encode(message: GetJobRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetJobRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetJobRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): GetJobRequest { + return { id: isSet(object.id) ? globalThis.String(object.id) : "" }; + }, + + toJSON(message: GetJobRequest): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + return obj; + }, + + create(base?: DeepPartial): GetJobRequest { + return GetJobRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): GetJobRequest { + const message = createBaseGetJobRequest(); + message.id = object.id ?? ""; + return message; + }, +}; + +function createBaseGetActionRequest(): GetActionRequest { + return { jobId: "", versionId: undefined, actionId: undefined }; +} + +export const GetActionRequest: MessageFns = { + encode(message: GetActionRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.jobId !== "") { + writer.uint32(10).string(message.jobId); + } + if (message.versionId !== undefined) { + writer.uint32(18).string(message.versionId); + } + if (message.actionId !== undefined) { + writer.uint32(26).string(message.actionId); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetActionRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetActionRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.versionId = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.actionId = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): GetActionRequest { + return { + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + versionId: isSet(object.versionId) ? globalThis.String(object.versionId) : undefined, + actionId: isSet(object.actionId) ? globalThis.String(object.actionId) : undefined, + }; + }, + + toJSON(message: GetActionRequest): unknown { + const obj: any = {}; + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.versionId !== undefined) { + obj.versionId = message.versionId; + } + if (message.actionId !== undefined) { + obj.actionId = message.actionId; + } + return obj; + }, + + create(base?: DeepPartial): GetActionRequest { + return GetActionRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): GetActionRequest { + const message = createBaseGetActionRequest(); + message.jobId = object.jobId ?? ""; + message.versionId = object.versionId ?? undefined; + message.actionId = object.actionId ?? undefined; + return message; + }, +}; + +function createBaseGetTriggerRequest(): GetTriggerRequest { + return { jobId: "", versionId: undefined, triggerId: undefined }; +} + +export const GetTriggerRequest: MessageFns = { + encode(message: GetTriggerRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.jobId !== "") { + writer.uint32(10).string(message.jobId); + } + if (message.versionId !== undefined) { + writer.uint32(18).string(message.versionId); + } + if (message.triggerId !== undefined) { + writer.uint32(26).string(message.triggerId); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetTriggerRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetTriggerRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.versionId = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.triggerId = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): GetTriggerRequest { + return { + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + versionId: isSet(object.versionId) ? globalThis.String(object.versionId) : undefined, + triggerId: isSet(object.triggerId) ? globalThis.String(object.triggerId) : undefined, + }; + }, + + toJSON(message: GetTriggerRequest): unknown { + const obj: any = {}; + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.versionId !== undefined) { + obj.versionId = message.versionId; + } + if (message.triggerId !== undefined) { + obj.triggerId = message.triggerId; + } + return obj; + }, + + create(base?: DeepPartial): GetTriggerRequest { + return GetTriggerRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): GetTriggerRequest { + const message = createBaseGetTriggerRequest(); + message.jobId = object.jobId ?? ""; + message.versionId = object.versionId ?? undefined; + message.triggerId = object.triggerId ?? undefined; + return message; + }, +}; + +function createBaseGetRunnersRequest(): GetRunnersRequest { + return { jobId: undefined, versionId: undefined, actionId: undefined }; +} + +export const GetRunnersRequest: MessageFns = { + encode(message: GetRunnersRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.jobId !== undefined) { + writer.uint32(10).string(message.jobId); + } + if (message.versionId !== undefined) { + writer.uint32(18).string(message.versionId); + } + if (message.actionId !== undefined) { + writer.uint32(26).string(message.actionId); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetRunnersRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetRunnersRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.versionId = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.actionId = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): GetRunnersRequest { + return { + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : undefined, + versionId: isSet(object.versionId) ? globalThis.String(object.versionId) : undefined, + actionId: isSet(object.actionId) ? globalThis.String(object.actionId) : undefined, + }; + }, + + toJSON(message: GetRunnersRequest): unknown { + const obj: any = {}; + if (message.jobId !== undefined) { + obj.jobId = message.jobId; + } + if (message.versionId !== undefined) { + obj.versionId = message.versionId; + } + if (message.actionId !== undefined) { + obj.actionId = message.actionId; + } + return obj; + }, + + create(base?: DeepPartial): GetRunnersRequest { + return GetRunnersRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): GetRunnersRequest { + const message = createBaseGetRunnersRequest(); + message.jobId = object.jobId ?? undefined; + message.versionId = object.versionId ?? undefined; + message.actionId = object.actionId ?? undefined; + return message; + }, +}; + +function createBaseCreateRunnerRequest(): CreateRunnerRequest { + return { jobId: "", versionId: "" }; +} + +export const CreateRunnerRequest: MessageFns = { + encode(message: CreateRunnerRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.jobId !== "") { + writer.uint32(10).string(message.jobId); + } + if (message.versionId !== "") { + writer.uint32(18).string(message.versionId); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): CreateRunnerRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseCreateRunnerRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.versionId = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): CreateRunnerRequest { + return { + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + versionId: isSet(object.versionId) ? globalThis.String(object.versionId) : "", + }; + }, + + toJSON(message: CreateRunnerRequest): unknown { + const obj: any = {}; + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.versionId !== "") { + obj.versionId = message.versionId; + } + return obj; + }, + + create(base?: DeepPartial): CreateRunnerRequest { + return CreateRunnerRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): CreateRunnerRequest { + const message = createBaseCreateRunnerRequest(); + message.jobId = object.jobId ?? ""; + message.versionId = object.versionId ?? ""; + return message; + }, +}; + +function createBasePublicKeysResponse(): PublicKeysResponse { + return { publicKeys: [] }; +} + +export const PublicKeysResponse: MessageFns = { + encode(message: PublicKeysResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.publicKeys) { + writer.uint32(10).string(v!); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): PublicKeysResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBasePublicKeysResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.publicKeys.push(reader.string()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): PublicKeysResponse { + return { + publicKeys: globalThis.Array.isArray(object?.publicKeys) + ? object.publicKeys.map((e: any) => globalThis.String(e)) + : [], + }; + }, + + toJSON(message: PublicKeysResponse): unknown { + const obj: any = {}; + if (message.publicKeys?.length) { + obj.publicKeys = message.publicKeys; + } + return obj; + }, + + create(base?: DeepPartial): PublicKeysResponse { + return PublicKeysResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): PublicKeysResponse { + const message = createBasePublicKeysResponse(); + message.publicKeys = object.publicKeys?.map((e) => e) || []; + return message; + }, +}; + +function createBaseJwtRequest(): JwtRequest { + return { token: "", scopes: [] }; +} + +export const JwtRequest: MessageFns = { + encode(message: JwtRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.token !== "") { + writer.uint32(10).string(message.token); + } + for (const v of message.scopes) { + writer.uint32(18).string(v!); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JwtRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJwtRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.token = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.scopes.push(reader.string()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JwtRequest { + return { + token: isSet(object.token) ? globalThis.String(object.token) : "", + scopes: globalThis.Array.isArray(object?.scopes) ? object.scopes.map((e: any) => globalThis.String(e)) : [], + }; + }, + + toJSON(message: JwtRequest): unknown { + const obj: any = {}; + if (message.token !== "") { + obj.token = message.token; + } + if (message.scopes?.length) { + obj.scopes = message.scopes; + } + return obj; + }, + + create(base?: DeepPartial): JwtRequest { + return JwtRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JwtRequest { + const message = createBaseJwtRequest(); + message.token = object.token ?? ""; + message.scopes = object.scopes?.map((e) => e) || []; + return message; + }, +}; + +function createBaseJwtResponse(): JwtResponse { + return { jwt: "" }; +} + +export const JwtResponse: MessageFns = { + encode(message: JwtResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.jwt !== "") { + writer.uint32(10).string(message.jwt); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JwtResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJwtResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jwt = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JwtResponse { + return { jwt: isSet(object.jwt) ? globalThis.String(object.jwt) : "" }; + }, + + toJSON(message: JwtResponse): unknown { + const obj: any = {}; + if (message.jwt !== "") { + obj.jwt = message.jwt; + } + return obj; + }, + + create(base?: DeepPartial): JwtResponse { + return JwtResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JwtResponse { + const message = createBaseJwtResponse(); + message.jwt = object.jwt ?? ""; + return message; + }, +}; + +function createBaseGetGatewayConfigResponse(): GetGatewayConfigResponse { + return { items: [] }; +} + +export const GetGatewayConfigResponse: MessageFns = { + encode(message: GetGatewayConfigResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.items) { + GetGatewayConfigResponse_Item.encode(v!, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetGatewayConfigResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetGatewayConfigResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.items.push(GetGatewayConfigResponse_Item.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): GetGatewayConfigResponse { + return { + items: globalThis.Array.isArray(object?.items) + ? object.items.map((e: any) => GetGatewayConfigResponse_Item.fromJSON(e)) + : [], + }; + }, + + toJSON(message: GetGatewayConfigResponse): unknown { + const obj: any = {}; + if (message.items?.length) { + obj.items = message.items.map((e) => GetGatewayConfigResponse_Item.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): GetGatewayConfigResponse { + return GetGatewayConfigResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): GetGatewayConfigResponse { + const message = createBaseGetGatewayConfigResponse(); + message.items = object.items?.map((e) => GetGatewayConfigResponse_Item.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseGetGatewayConfigResponse_Item(): GetGatewayConfigResponse_Item { + return { job: undefined, action: undefined, httpTriggers: [], runners: [] }; +} + +export const GetGatewayConfigResponse_Item: MessageFns = { + encode(message: GetGatewayConfigResponse_Item, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.job !== undefined) { + JobItem.encode(message.job, writer.uint32(10).fork()).join(); + } + if (message.action !== undefined) { + ActionItem.encode(message.action, writer.uint32(18).fork()).join(); + } + for (const v of message.httpTriggers) { + TriggerHttpItem.encode(v!, writer.uint32(26).fork()).join(); + } + for (const v of message.runners) { + RunnerItem.encode(v!, writer.uint32(34).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetGatewayConfigResponse_Item { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetGatewayConfigResponse_Item(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.job = JobItem.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.action = ActionItem.decode(reader, reader.uint32()); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.httpTriggers.push(TriggerHttpItem.decode(reader, reader.uint32())); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.runners.push(RunnerItem.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): GetGatewayConfigResponse_Item { + return { + job: isSet(object.job) ? JobItem.fromJSON(object.job) : undefined, + action: isSet(object.action) ? ActionItem.fromJSON(object.action) : undefined, + httpTriggers: globalThis.Array.isArray(object?.httpTriggers) + ? object.httpTriggers.map((e: any) => TriggerHttpItem.fromJSON(e)) + : [], + runners: globalThis.Array.isArray(object?.runners) ? object.runners.map((e: any) => RunnerItem.fromJSON(e)) : [], + }; + }, + + toJSON(message: GetGatewayConfigResponse_Item): unknown { + const obj: any = {}; + if (message.job !== undefined) { + obj.job = JobItem.toJSON(message.job); + } + if (message.action !== undefined) { + obj.action = ActionItem.toJSON(message.action); + } + if (message.httpTriggers?.length) { + obj.httpTriggers = message.httpTriggers.map((e) => TriggerHttpItem.toJSON(e)); + } + if (message.runners?.length) { + obj.runners = message.runners.map((e) => RunnerItem.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): GetGatewayConfigResponse_Item { + return GetGatewayConfigResponse_Item.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): GetGatewayConfigResponse_Item { + const message = createBaseGetGatewayConfigResponse_Item(); + message.job = (object.job !== undefined && object.job !== null) ? JobItem.fromPartial(object.job) : undefined; + message.action = (object.action !== undefined && object.action !== null) + ? ActionItem.fromPartial(object.action) + : undefined; + message.httpTriggers = object.httpTriggers?.map((e) => TriggerHttpItem.fromPartial(e)) || []; + message.runners = object.runners?.map((e) => RunnerItem.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseJobItem(): JobItem { + return { id: "", jobName: "", versionId: undefined, status: JobStatus.ENABLED }; +} + +export const JobItem: MessageFns = { + encode(message: JobItem, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.jobName !== "") { + writer.uint32(18).string(message.jobName); + } + if (message.versionId !== undefined) { + writer.uint32(26).string(message.versionId); + } + if (message.status !== JobStatus.ENABLED) { + writer.uint32(32).int32(jobStatusToNumber(message.status)); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobItem { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.jobName = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.versionId = reader.string(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.status = jobStatusFromJSON(reader.int32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobItem { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + jobName: isSet(object.jobName) ? globalThis.String(object.jobName) : "", + versionId: isSet(object.versionId) ? globalThis.String(object.versionId) : undefined, + status: isSet(object.status) ? jobStatusFromJSON(object.status) : JobStatus.ENABLED, + }; + }, + + toJSON(message: JobItem): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.jobName !== "") { + obj.jobName = message.jobName; + } + if (message.versionId !== undefined) { + obj.versionId = message.versionId; + } + if (message.status !== JobStatus.ENABLED) { + obj.status = jobStatusToJSON(message.status); + } + return obj; + }, + + create(base?: DeepPartial): JobItem { + return JobItem.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobItem { + const message = createBaseJobItem(); + message.id = object.id ?? ""; + message.jobName = object.jobName ?? ""; + message.versionId = object.versionId ?? undefined; + message.status = object.status ?? JobStatus.ENABLED; + return message; + }, +}; + +function createBaseActionItem(): ActionItem { + return { + id: "", + runnerAsynchronous: false, + runnerMinCount: 0, + runnerMaxCount: 0, + runnerTimeout: 0, + runnerMaxIdleAge: 0, + runnerMaxAge: 0, + runnerMaxAgeHard: 0, + runnerMode: ActionRunnerMode.STANDARD, + }; +} + +export const ActionItem: MessageFns = { + encode(message: ActionItem, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.runnerAsynchronous !== false) { + writer.uint32(16).bool(message.runnerAsynchronous); + } + if (message.runnerMinCount !== 0) { + writer.uint32(24).uint32(message.runnerMinCount); + } + if (message.runnerMaxCount !== 0) { + writer.uint32(32).uint32(message.runnerMaxCount); + } + if (message.runnerTimeout !== 0) { + writer.uint32(40).uint32(message.runnerTimeout); + } + if (message.runnerMaxIdleAge !== 0) { + writer.uint32(48).uint32(message.runnerMaxIdleAge); + } + if (message.runnerMaxAge !== 0) { + writer.uint32(56).uint32(message.runnerMaxAge); + } + if (message.runnerMaxAgeHard !== 0) { + writer.uint32(64).uint32(message.runnerMaxAgeHard); + } + if (message.runnerMode !== ActionRunnerMode.STANDARD) { + writer.uint32(72).int32(actionRunnerModeToNumber(message.runnerMode)); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ActionItem { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseActionItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.runnerAsynchronous = reader.bool(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.runnerMinCount = reader.uint32(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.runnerMaxCount = reader.uint32(); + continue; + } + case 5: { + if (tag !== 40) { + break; + } + + message.runnerTimeout = reader.uint32(); + continue; + } + case 6: { + if (tag !== 48) { + break; + } + + message.runnerMaxIdleAge = reader.uint32(); + continue; + } + case 7: { + if (tag !== 56) { + break; + } + + message.runnerMaxAge = reader.uint32(); + continue; + } + case 8: { + if (tag !== 64) { + break; + } + + message.runnerMaxAgeHard = reader.uint32(); + continue; + } + case 9: { + if (tag !== 72) { + break; + } + + message.runnerMode = actionRunnerModeFromJSON(reader.int32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ActionItem { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + runnerAsynchronous: isSet(object.runnerAsynchronous) ? globalThis.Boolean(object.runnerAsynchronous) : false, + runnerMinCount: isSet(object.runnerMinCount) ? globalThis.Number(object.runnerMinCount) : 0, + runnerMaxCount: isSet(object.runnerMaxCount) ? globalThis.Number(object.runnerMaxCount) : 0, + runnerTimeout: isSet(object.runnerTimeout) ? globalThis.Number(object.runnerTimeout) : 0, + runnerMaxIdleAge: isSet(object.runnerMaxIdleAge) ? globalThis.Number(object.runnerMaxIdleAge) : 0, + runnerMaxAge: isSet(object.runnerMaxAge) ? globalThis.Number(object.runnerMaxAge) : 0, + runnerMaxAgeHard: isSet(object.runnerMaxAgeHard) ? globalThis.Number(object.runnerMaxAgeHard) : 0, + runnerMode: isSet(object.runnerMode) ? actionRunnerModeFromJSON(object.runnerMode) : ActionRunnerMode.STANDARD, + }; + }, + + toJSON(message: ActionItem): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.runnerAsynchronous !== false) { + obj.runnerAsynchronous = message.runnerAsynchronous; + } + if (message.runnerMinCount !== 0) { + obj.runnerMinCount = Math.round(message.runnerMinCount); + } + if (message.runnerMaxCount !== 0) { + obj.runnerMaxCount = Math.round(message.runnerMaxCount); + } + if (message.runnerTimeout !== 0) { + obj.runnerTimeout = Math.round(message.runnerTimeout); + } + if (message.runnerMaxIdleAge !== 0) { + obj.runnerMaxIdleAge = Math.round(message.runnerMaxIdleAge); + } + if (message.runnerMaxAge !== 0) { + obj.runnerMaxAge = Math.round(message.runnerMaxAge); + } + if (message.runnerMaxAgeHard !== 0) { + obj.runnerMaxAgeHard = Math.round(message.runnerMaxAgeHard); + } + if (message.runnerMode !== ActionRunnerMode.STANDARD) { + obj.runnerMode = actionRunnerModeToJSON(message.runnerMode); + } + return obj; + }, + + create(base?: DeepPartial): ActionItem { + return ActionItem.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ActionItem { + const message = createBaseActionItem(); + message.id = object.id ?? ""; + message.runnerAsynchronous = object.runnerAsynchronous ?? false; + message.runnerMinCount = object.runnerMinCount ?? 0; + message.runnerMaxCount = object.runnerMaxCount ?? 0; + message.runnerTimeout = object.runnerTimeout ?? 0; + message.runnerMaxIdleAge = object.runnerMaxIdleAge ?? 0; + message.runnerMaxAge = object.runnerMaxAge ?? 0; + message.runnerMaxAgeHard = object.runnerMaxAgeHard ?? 0; + message.runnerMode = object.runnerMode ?? ActionRunnerMode.STANDARD; + return message; + }, +}; + +function createBaseTriggerHttpItem(): TriggerHttpItem { + return { + id: "", + jobId: "", + jobVersionId: "", + name: undefined, + method: undefined, + hostname: undefined, + path: undefined, + createdAt: 0, + }; +} + +export const TriggerHttpItem: MessageFns = { + encode(message: TriggerHttpItem, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.jobId !== "") { + writer.uint32(18).string(message.jobId); + } + if (message.jobVersionId !== "") { + writer.uint32(26).string(message.jobVersionId); + } + if (message.name !== undefined) { + writer.uint32(34).string(message.name); + } + if (message.method !== undefined) { + writer.uint32(42).string(message.method); + } + if (message.hostname !== undefined) { + writer.uint32(50).string(message.hostname); + } + if (message.path !== undefined) { + writer.uint32(58).string(message.path); + } + if (message.createdAt !== 0) { + writer.uint32(64).uint64(message.createdAt); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): TriggerHttpItem { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTriggerHttpItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.jobVersionId = reader.string(); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.name = reader.string(); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.method = reader.string(); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.hostname = reader.string(); + continue; + } + case 7: { + if (tag !== 58) { + break; + } + + message.path = reader.string(); + continue; + } + case 8: { + if (tag !== 64) { + break; + } + + message.createdAt = longToNumber(reader.uint64()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): TriggerHttpItem { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + jobVersionId: isSet(object.jobVersionId) ? globalThis.String(object.jobVersionId) : "", + name: isSet(object.name) ? globalThis.String(object.name) : undefined, + method: isSet(object.method) ? globalThis.String(object.method) : undefined, + hostname: isSet(object.hostname) ? globalThis.String(object.hostname) : undefined, + path: isSet(object.path) ? globalThis.String(object.path) : undefined, + createdAt: isSet(object.createdAt) ? globalThis.Number(object.createdAt) : 0, + }; + }, + + toJSON(message: TriggerHttpItem): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.jobVersionId !== "") { + obj.jobVersionId = message.jobVersionId; + } + if (message.name !== undefined) { + obj.name = message.name; + } + if (message.method !== undefined) { + obj.method = message.method; + } + if (message.hostname !== undefined) { + obj.hostname = message.hostname; + } + if (message.path !== undefined) { + obj.path = message.path; + } + if (message.createdAt !== 0) { + obj.createdAt = Math.round(message.createdAt); + } + return obj; + }, + + create(base?: DeepPartial): TriggerHttpItem { + return TriggerHttpItem.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): TriggerHttpItem { + const message = createBaseTriggerHttpItem(); + message.id = object.id ?? ""; + message.jobId = object.jobId ?? ""; + message.jobVersionId = object.jobVersionId ?? ""; + message.name = object.name ?? undefined; + message.method = object.method ?? undefined; + message.hostname = object.hostname ?? undefined; + message.path = object.path ?? undefined; + message.createdAt = object.createdAt ?? 0; + return message; + }, +}; + +function createBaseTriggerMqttItem(): TriggerMqttItem { + return { + id: "", + jobId: "", + jobVersionId: "", + name: undefined, + topics: [], + protocol: undefined, + port: undefined, + host: undefined, + username: undefined, + password: undefined, + clientId: undefined, + createdAt: 0, + }; +} + +export const TriggerMqttItem: MessageFns = { + encode(message: TriggerMqttItem, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.jobId !== "") { + writer.uint32(18).string(message.jobId); + } + if (message.jobVersionId !== "") { + writer.uint32(26).string(message.jobVersionId); + } + if (message.name !== undefined) { + writer.uint32(34).string(message.name); + } + for (const v of message.topics) { + writer.uint32(42).string(v!); + } + if (message.protocol !== undefined) { + writer.uint32(50).string(message.protocol); + } + if (message.port !== undefined) { + writer.uint32(58).string(message.port); + } + if (message.host !== undefined) { + writer.uint32(66).string(message.host); + } + if (message.username !== undefined) { + writer.uint32(74).string(message.username); + } + if (message.password !== undefined) { + writer.uint32(82).string(message.password); + } + if (message.clientId !== undefined) { + writer.uint32(90).string(message.clientId); + } + if (message.createdAt !== 0) { + writer.uint32(96).uint64(message.createdAt); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): TriggerMqttItem { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTriggerMqttItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.jobVersionId = reader.string(); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.name = reader.string(); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.topics.push(reader.string()); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.protocol = reader.string(); + continue; + } + case 7: { + if (tag !== 58) { + break; + } + + message.port = reader.string(); + continue; + } + case 8: { + if (tag !== 66) { + break; + } + + message.host = reader.string(); + continue; + } + case 9: { + if (tag !== 74) { + break; + } + + message.username = reader.string(); + continue; + } + case 10: { + if (tag !== 82) { + break; + } + + message.password = reader.string(); + continue; + } + case 11: { + if (tag !== 90) { + break; + } + + message.clientId = reader.string(); + continue; + } + case 12: { + if (tag !== 96) { + break; + } + + message.createdAt = longToNumber(reader.uint64()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): TriggerMqttItem { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + jobVersionId: isSet(object.jobVersionId) ? globalThis.String(object.jobVersionId) : "", + name: isSet(object.name) ? globalThis.String(object.name) : undefined, + topics: globalThis.Array.isArray(object?.topics) ? object.topics.map((e: any) => globalThis.String(e)) : [], + protocol: isSet(object.protocol) ? globalThis.String(object.protocol) : undefined, + port: isSet(object.port) ? globalThis.String(object.port) : undefined, + host: isSet(object.host) ? globalThis.String(object.host) : undefined, + username: isSet(object.username) ? globalThis.String(object.username) : undefined, + password: isSet(object.password) ? globalThis.String(object.password) : undefined, + clientId: isSet(object.clientId) ? globalThis.String(object.clientId) : undefined, + createdAt: isSet(object.createdAt) ? globalThis.Number(object.createdAt) : 0, + }; + }, + + toJSON(message: TriggerMqttItem): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.jobVersionId !== "") { + obj.jobVersionId = message.jobVersionId; + } + if (message.name !== undefined) { + obj.name = message.name; + } + if (message.topics?.length) { + obj.topics = message.topics; + } + if (message.protocol !== undefined) { + obj.protocol = message.protocol; + } + if (message.port !== undefined) { + obj.port = message.port; + } + if (message.host !== undefined) { + obj.host = message.host; + } + if (message.username !== undefined) { + obj.username = message.username; + } + if (message.password !== undefined) { + obj.password = message.password; + } + if (message.clientId !== undefined) { + obj.clientId = message.clientId; + } + if (message.createdAt !== 0) { + obj.createdAt = Math.round(message.createdAt); + } + return obj; + }, + + create(base?: DeepPartial): TriggerMqttItem { + return TriggerMqttItem.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): TriggerMqttItem { + const message = createBaseTriggerMqttItem(); + message.id = object.id ?? ""; + message.jobId = object.jobId ?? ""; + message.jobVersionId = object.jobVersionId ?? ""; + message.name = object.name ?? undefined; + message.topics = object.topics?.map((e) => e) || []; + message.protocol = object.protocol ?? undefined; + message.port = object.port ?? undefined; + message.host = object.host ?? undefined; + message.username = object.username ?? undefined; + message.password = object.password ?? undefined; + message.clientId = object.clientId ?? undefined; + message.createdAt = object.createdAt ?? 0; + return message; + }, +}; + +function createBaseTriggerScheduleItem(): TriggerScheduleItem { + return { id: "", jobId: "", jobVersionId: "", name: undefined, cron: "", timezone: undefined, createdAt: 0 }; +} + +export const TriggerScheduleItem: MessageFns = { + encode(message: TriggerScheduleItem, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.jobId !== "") { + writer.uint32(18).string(message.jobId); + } + if (message.jobVersionId !== "") { + writer.uint32(26).string(message.jobVersionId); + } + if (message.name !== undefined) { + writer.uint32(34).string(message.name); + } + if (message.cron !== "") { + writer.uint32(42).string(message.cron); + } + if (message.timezone !== undefined) { + writer.uint32(50).string(message.timezone); + } + if (message.createdAt !== 0) { + writer.uint32(56).uint64(message.createdAt); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): TriggerScheduleItem { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTriggerScheduleItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.jobVersionId = reader.string(); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.name = reader.string(); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.cron = reader.string(); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.timezone = reader.string(); + continue; + } + case 7: { + if (tag !== 56) { + break; + } + + message.createdAt = longToNumber(reader.uint64()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): TriggerScheduleItem { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + jobVersionId: isSet(object.jobVersionId) ? globalThis.String(object.jobVersionId) : "", + name: isSet(object.name) ? globalThis.String(object.name) : undefined, + cron: isSet(object.cron) ? globalThis.String(object.cron) : "", + timezone: isSet(object.timezone) ? globalThis.String(object.timezone) : undefined, + createdAt: isSet(object.createdAt) ? globalThis.Number(object.createdAt) : 0, + }; + }, + + toJSON(message: TriggerScheduleItem): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.jobVersionId !== "") { + obj.jobVersionId = message.jobVersionId; + } + if (message.name !== undefined) { + obj.name = message.name; + } + if (message.cron !== "") { + obj.cron = message.cron; + } + if (message.timezone !== undefined) { + obj.timezone = message.timezone; + } + if (message.createdAt !== 0) { + obj.createdAt = Math.round(message.createdAt); + } + return obj; + }, + + create(base?: DeepPartial): TriggerScheduleItem { + return TriggerScheduleItem.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): TriggerScheduleItem { + const message = createBaseTriggerScheduleItem(); + message.id = object.id ?? ""; + message.jobId = object.jobId ?? ""; + message.jobVersionId = object.jobVersionId ?? ""; + message.name = object.name ?? undefined; + message.cron = object.cron ?? ""; + message.timezone = object.timezone ?? undefined; + message.createdAt = object.createdAt ?? 0; + return message; + }, +}; + +function createBaseRunnerItem(): RunnerItem { + return { + id: "", + jobId: "", + jobVersionId: "", + actionId: "", + status: RunnerStatus.STARTING, + ip: "", + hostname: "", + port: 0, + createdAt: 0, + readyAt: undefined, + closingAt: undefined, + closedAt: undefined, + }; +} + +export const RunnerItem: MessageFns = { + encode(message: RunnerItem, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.jobId !== "") { + writer.uint32(18).string(message.jobId); + } + if (message.jobVersionId !== "") { + writer.uint32(26).string(message.jobVersionId); + } + if (message.actionId !== "") { + writer.uint32(34).string(message.actionId); + } + if (message.status !== RunnerStatus.STARTING) { + writer.uint32(40).int32(runnerStatusToNumber(message.status)); + } + if (message.ip !== "") { + writer.uint32(50).string(message.ip); + } + if (message.hostname !== "") { + writer.uint32(58).string(message.hostname); + } + if (message.port !== 0) { + writer.uint32(64).uint32(message.port); + } + if (message.createdAt !== 0) { + writer.uint32(80).uint64(message.createdAt); + } + if (message.readyAt !== undefined) { + writer.uint32(88).uint64(message.readyAt); + } + if (message.closingAt !== undefined) { + writer.uint32(96).uint64(message.closingAt); + } + if (message.closedAt !== undefined) { + writer.uint32(104).uint64(message.closedAt); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): RunnerItem { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseRunnerItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.jobVersionId = reader.string(); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.actionId = reader.string(); + continue; + } + case 5: { + if (tag !== 40) { + break; + } + + message.status = runnerStatusFromJSON(reader.int32()); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.ip = reader.string(); + continue; + } + case 7: { + if (tag !== 58) { + break; + } + + message.hostname = reader.string(); + continue; + } + case 8: { + if (tag !== 64) { + break; + } + + message.port = reader.uint32(); + continue; + } + case 10: { + if (tag !== 80) { + break; + } + + message.createdAt = longToNumber(reader.uint64()); + continue; + } + case 11: { + if (tag !== 88) { + break; + } + + message.readyAt = longToNumber(reader.uint64()); + continue; + } + case 12: { + if (tag !== 96) { + break; + } + + message.closingAt = longToNumber(reader.uint64()); + continue; + } + case 13: { + if (tag !== 104) { + break; + } + + message.closedAt = longToNumber(reader.uint64()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): RunnerItem { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + jobVersionId: isSet(object.jobVersionId) ? globalThis.String(object.jobVersionId) : "", + actionId: isSet(object.actionId) ? globalThis.String(object.actionId) : "", + status: isSet(object.status) ? runnerStatusFromJSON(object.status) : RunnerStatus.STARTING, + ip: isSet(object.ip) ? globalThis.String(object.ip) : "", + hostname: isSet(object.hostname) ? globalThis.String(object.hostname) : "", + port: isSet(object.port) ? globalThis.Number(object.port) : 0, + createdAt: isSet(object.createdAt) ? globalThis.Number(object.createdAt) : 0, + readyAt: isSet(object.readyAt) ? globalThis.Number(object.readyAt) : undefined, + closingAt: isSet(object.closingAt) ? globalThis.Number(object.closingAt) : undefined, + closedAt: isSet(object.closedAt) ? globalThis.Number(object.closedAt) : undefined, + }; + }, + + toJSON(message: RunnerItem): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.jobVersionId !== "") { + obj.jobVersionId = message.jobVersionId; + } + if (message.actionId !== "") { + obj.actionId = message.actionId; + } + if (message.status !== RunnerStatus.STARTING) { + obj.status = runnerStatusToJSON(message.status); + } + if (message.ip !== "") { + obj.ip = message.ip; + } + if (message.hostname !== "") { + obj.hostname = message.hostname; + } + if (message.port !== 0) { + obj.port = Math.round(message.port); + } + if (message.createdAt !== 0) { + obj.createdAt = Math.round(message.createdAt); + } + if (message.readyAt !== undefined) { + obj.readyAt = Math.round(message.readyAt); + } + if (message.closingAt !== undefined) { + obj.closingAt = Math.round(message.closingAt); + } + if (message.closedAt !== undefined) { + obj.closedAt = Math.round(message.closedAt); + } + return obj; + }, + + create(base?: DeepPartial): RunnerItem { + return RunnerItem.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): RunnerItem { + const message = createBaseRunnerItem(); + message.id = object.id ?? ""; + message.jobId = object.jobId ?? ""; + message.jobVersionId = object.jobVersionId ?? ""; + message.actionId = object.actionId ?? ""; + message.status = object.status ?? RunnerStatus.STARTING; + message.ip = object.ip ?? ""; + message.hostname = object.hostname ?? ""; + message.port = object.port ?? 0; + message.createdAt = object.createdAt ?? 0; + message.readyAt = object.readyAt ?? undefined; + message.closingAt = object.closingAt ?? undefined; + message.closedAt = object.closedAt ?? undefined; + return message; + }, +}; + +function createBaseStoreGetRequest(): StoreGetRequest { + return { key: "" }; +} + +export const StoreGetRequest: MessageFns = { + encode(message: StoreGetRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.key !== "") { + writer.uint32(10).string(message.key); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): StoreGetRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseStoreGetRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.key = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): StoreGetRequest { + return { key: isSet(object.key) ? globalThis.String(object.key) : "" }; + }, + + toJSON(message: StoreGetRequest): unknown { + const obj: any = {}; + if (message.key !== "") { + obj.key = message.key; + } + return obj; + }, + + create(base?: DeepPartial): StoreGetRequest { + return StoreGetRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): StoreGetRequest { + const message = createBaseStoreGetRequest(); + message.key = object.key ?? ""; + return message; + }, +}; + +function createBaseStoreSetRequest(): StoreSetRequest { + return { key: "", value: "", ttl: 0 }; +} + +export const StoreSetRequest: MessageFns = { + encode(message: StoreSetRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.key !== "") { + writer.uint32(10).string(message.key); + } + if (message.value !== "") { + writer.uint32(18).string(message.value); + } + if (message.ttl !== 0) { + writer.uint32(24).uint64(message.ttl); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): StoreSetRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseStoreSetRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.key = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.value = reader.string(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.ttl = longToNumber(reader.uint64()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): StoreSetRequest { + return { + key: isSet(object.key) ? globalThis.String(object.key) : "", + value: isSet(object.value) ? globalThis.String(object.value) : "", + ttl: isSet(object.ttl) ? globalThis.Number(object.ttl) : 0, + }; + }, + + toJSON(message: StoreSetRequest): unknown { + const obj: any = {}; + if (message.key !== "") { + obj.key = message.key; + } + if (message.value !== "") { + obj.value = message.value; + } + if (message.ttl !== 0) { + obj.ttl = Math.round(message.ttl); + } + return obj; + }, + + create(base?: DeepPartial): StoreSetRequest { + return StoreSetRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): StoreSetRequest { + const message = createBaseStoreSetRequest(); + message.key = object.key ?? ""; + message.value = object.value ?? ""; + message.ttl = object.ttl ?? 0; + return message; + }, +}; + +function createBaseStoreDeleteRequest(): StoreDeleteRequest { + return { key: "" }; +} + +export const StoreDeleteRequest: MessageFns = { + encode(message: StoreDeleteRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.key !== "") { + writer.uint32(10).string(message.key); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): StoreDeleteRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseStoreDeleteRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.key = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): StoreDeleteRequest { + return { key: isSet(object.key) ? globalThis.String(object.key) : "" }; + }, + + toJSON(message: StoreDeleteRequest): unknown { + const obj: any = {}; + if (message.key !== "") { + obj.key = message.key; + } + return obj; + }, + + create(base?: DeepPartial): StoreDeleteRequest { + return StoreDeleteRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): StoreDeleteRequest { + const message = createBaseStoreDeleteRequest(); + message.key = object.key ?? ""; + return message; + }, +}; + +function createBaseStoreItem(): StoreItem { + return { key: "", value: "", expiry: 0, createdAt: 0, modified: 0 }; +} + +export const StoreItem: MessageFns = { + encode(message: StoreItem, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.key !== "") { + writer.uint32(10).string(message.key); + } + if (message.value !== "") { + writer.uint32(18).string(message.value); + } + if (message.expiry !== 0) { + writer.uint32(24).uint64(message.expiry); + } + if (message.createdAt !== 0) { + writer.uint32(32).uint64(message.createdAt); + } + if (message.modified !== 0) { + writer.uint32(40).uint64(message.modified); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): StoreItem { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseStoreItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.key = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.value = reader.string(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.expiry = longToNumber(reader.uint64()); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.createdAt = longToNumber(reader.uint64()); + continue; + } + case 5: { + if (tag !== 40) { + break; + } + + message.modified = longToNumber(reader.uint64()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): StoreItem { + return { + key: isSet(object.key) ? globalThis.String(object.key) : "", + value: isSet(object.value) ? globalThis.String(object.value) : "", + expiry: isSet(object.expiry) ? globalThis.Number(object.expiry) : 0, + createdAt: isSet(object.createdAt) ? globalThis.Number(object.createdAt) : 0, + modified: isSet(object.modified) ? globalThis.Number(object.modified) : 0, + }; + }, + + toJSON(message: StoreItem): unknown { + const obj: any = {}; + if (message.key !== "") { + obj.key = message.key; + } + if (message.value !== "") { + obj.value = message.value; + } + if (message.expiry !== 0) { + obj.expiry = Math.round(message.expiry); + } + if (message.createdAt !== 0) { + obj.createdAt = Math.round(message.createdAt); + } + if (message.modified !== 0) { + obj.modified = Math.round(message.modified); + } + return obj; + }, + + create(base?: DeepPartial): StoreItem { + return StoreItem.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): StoreItem { + const message = createBaseStoreItem(); + message.key = object.key ?? ""; + message.value = object.value ?? ""; + message.expiry = object.expiry ?? 0; + message.createdAt = object.createdAt ?? 0; + message.modified = object.modified ?? 0; + return message; + }, +}; + +function createBaseMqttPublishRequest(): MqttPublishRequest { + return { topic: "", payload: new Uint8Array(0) }; +} + +export const MqttPublishRequest: MessageFns = { + encode(message: MqttPublishRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.topic !== "") { + writer.uint32(10).string(message.topic); + } + if (message.payload.length !== 0) { + writer.uint32(18).bytes(message.payload); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): MqttPublishRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseMqttPublishRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.topic = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.payload = reader.bytes(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): MqttPublishRequest { + return { + topic: isSet(object.topic) ? globalThis.String(object.topic) : "", + payload: isSet(object.payload) ? bytesFromBase64(object.payload) : new Uint8Array(0), + }; + }, + + toJSON(message: MqttPublishRequest): unknown { + const obj: any = {}; + if (message.topic !== "") { + obj.topic = message.topic; + } + if (message.payload.length !== 0) { + obj.payload = base64FromBytes(message.payload); + } + return obj; + }, + + create(base?: DeepPartial): MqttPublishRequest { + return MqttPublishRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): MqttPublishRequest { + const message = createBaseMqttPublishRequest(); + message.topic = object.topic ?? ""; + message.payload = object.payload ?? new Uint8Array(0); + return message; + }, +}; + +function createBaseInitRequest(): InitRequest { + return { token: "" }; +} + +export const InitRequest: MessageFns = { + encode(message: InitRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.token !== "") { + writer.uint32(10).string(message.token); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): InitRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseInitRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.token = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): InitRequest { + return { token: isSet(object.token) ? globalThis.String(object.token) : "" }; + }, + + toJSON(message: InitRequest): unknown { + const obj: any = {}; + if (message.token !== "") { + obj.token = message.token; + } + return obj; + }, + + create(base?: DeepPartial): InitRequest { + return InitRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): InitRequest { + const message = createBaseInitRequest(); + message.token = object.token ?? ""; + return message; + }, +}; + +function createBaseInitResponse(): InitResponse { + return {}; +} + +export const InitResponse: MessageFns = { + encode(_: InitResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): InitResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseInitResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(_: any): InitResponse { + return {}; + }, + + toJSON(_: InitResponse): unknown { + const obj: any = {}; + return obj; + }, + + create(base?: DeepPartial): InitResponse { + return InitResponse.fromPartial(base ?? {}); + }, + fromPartial(_: DeepPartial): InitResponse { + const message = createBaseInitResponse(); + return message; + }, +}; + +export type GeneralManagementDefinition = typeof GeneralManagementDefinition; +export const GeneralManagementDefinition = { + name: "GeneralManagement", + fullName: "Server.GeneralManagement", + methods: { + /** General */ + getJobs: { + name: "getJobs", + requestType: Empty, + requestStream: false, + responseType: JobItem, + responseStream: true, + options: {}, + }, + getJob: { + name: "getJob", + requestType: GetJobRequest, + requestStream: false, + responseType: JobItem, + responseStream: false, + options: {}, + }, + getAction: { + name: "getAction", + requestType: GetActionRequest, + requestStream: false, + responseType: ActionItem, + responseStream: false, + options: {}, + }, + getHttpTriggers: { + name: "getHttpTriggers", + requestType: GetTriggerRequest, + requestStream: false, + responseType: TriggerHttpItem, + responseStream: true, + options: {}, + }, + getMqttTriggers: { + name: "getMqttTriggers", + requestType: GetTriggerRequest, + requestStream: false, + responseType: TriggerMqttItem, + responseStream: true, + options: {}, + }, + getScheduleTriggers: { + name: "getScheduleTriggers", + requestType: GetTriggerRequest, + requestStream: false, + responseType: TriggerScheduleItem, + responseStream: true, + options: {}, + }, + getRunners: { + name: "getRunners", + requestType: GetRunnersRequest, + requestStream: false, + responseType: RunnerItem, + responseStream: true, + options: {}, + }, + /** Authentication related */ + getPublicKeys: { + name: "getPublicKeys", + requestType: Empty, + requestStream: false, + responseType: PublicKeysResponse, + responseStream: false, + options: {}, + }, + createJwt: { + name: "createJwt", + requestType: JwtRequest, + requestStream: false, + responseType: JwtResponse, + responseStream: false, + options: {}, + }, + /** Gateway Specific */ + createRunner: { + name: "createRunner", + requestType: CreateRunnerRequest, + requestStream: false, + responseType: RunnerItem, + responseStream: false, + options: {}, + }, + getGatewayConfig: { + name: "getGatewayConfig", + requestType: Empty, + requestStream: false, + responseType: GetGatewayConfigResponse, + responseStream: false, + options: {}, + }, + }, +} as const; + +export interface GeneralManagementServiceImplementation { + /** General */ + getJobs(request: Empty, context: CallContext & CallContextExt): ServerStreamingMethodResult>; + getJob(request: GetJobRequest, context: CallContext & CallContextExt): Promise>; + getAction(request: GetActionRequest, context: CallContext & CallContextExt): Promise>; + getHttpTriggers( + request: GetTriggerRequest, + context: CallContext & CallContextExt, + ): ServerStreamingMethodResult>; + getMqttTriggers( + request: GetTriggerRequest, + context: CallContext & CallContextExt, + ): ServerStreamingMethodResult>; + getScheduleTriggers( + request: GetTriggerRequest, + context: CallContext & CallContextExt, + ): ServerStreamingMethodResult>; + getRunners( + request: GetRunnersRequest, + context: CallContext & CallContextExt, + ): ServerStreamingMethodResult>; + /** Authentication related */ + getPublicKeys(request: Empty, context: CallContext & CallContextExt): Promise>; + createJwt(request: JwtRequest, context: CallContext & CallContextExt): Promise>; + /** Gateway Specific */ + createRunner(request: CreateRunnerRequest, context: CallContext & CallContextExt): Promise>; + getGatewayConfig( + request: Empty, + context: CallContext & CallContextExt, + ): Promise>; +} + +export interface GeneralManagementClient { + /** General */ + getJobs(request: DeepPartial, options?: CallOptions & CallOptionsExt): AsyncIterable; + getJob(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + getAction(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + getHttpTriggers( + request: DeepPartial, + options?: CallOptions & CallOptionsExt, + ): AsyncIterable; + getMqttTriggers( + request: DeepPartial, + options?: CallOptions & CallOptionsExt, + ): AsyncIterable; + getScheduleTriggers( + request: DeepPartial, + options?: CallOptions & CallOptionsExt, + ): AsyncIterable; + getRunners( + request: DeepPartial, + options?: CallOptions & CallOptionsExt, + ): AsyncIterable; + /** Authentication related */ + getPublicKeys(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + createJwt(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + /** Gateway Specific */ + createRunner(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + getGatewayConfig( + request: DeepPartial, + options?: CallOptions & CallOptionsExt, + ): Promise; +} + +export type RunnerManagementDefinition = typeof RunnerManagementDefinition; +export const RunnerManagementDefinition = { + name: "RunnerManagement", + fullName: "Server.RunnerManagement", + methods: { + storeGet: { + name: "storeGet", + requestType: StoreGetRequest, + requestStream: false, + responseType: StoreItem, + responseStream: false, + options: {}, + }, + storeSet: { + name: "storeSet", + requestType: StoreSetRequest, + requestStream: false, + responseType: StoreItem, + responseStream: false, + options: {}, + }, + storeDelete: { + name: "storeDelete", + requestType: StoreDeleteRequest, + requestStream: false, + responseType: StoreItem, + responseStream: false, + options: {}, + }, + mqttPublish: { + name: "mqttPublish", + requestType: MqttPublishRequest, + requestStream: false, + responseType: Empty, + responseStream: false, + options: {}, + }, + /** Backend management methods */ + init: { + name: "init", + requestType: InitRequest, + requestStream: false, + responseType: InitResponse, + responseStream: false, + options: {}, + }, + archive: { + name: "archive", + requestType: Empty, + requestStream: false, + responseType: ExportChunk, + responseStream: true, + options: {}, + }, + }, +} as const; + +export interface RunnerManagementServiceImplementation { + storeGet(request: StoreGetRequest, context: CallContext & CallContextExt): Promise>; + storeSet(request: StoreSetRequest, context: CallContext & CallContextExt): Promise>; + storeDelete(request: StoreDeleteRequest, context: CallContext & CallContextExt): Promise>; + mqttPublish(request: MqttPublishRequest, context: CallContext & CallContextExt): Promise>; + /** Backend management methods */ + init(request: InitRequest, context: CallContext & CallContextExt): Promise>; + archive(request: Empty, context: CallContext & CallContextExt): ServerStreamingMethodResult>; +} + +export interface RunnerManagementClient { + storeGet(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + storeSet(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + storeDelete(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + mqttPublish(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + /** Backend management methods */ + init(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + archive(request: DeepPartial, options?: CallOptions & CallOptionsExt): AsyncIterable; +} + +function bytesFromBase64(b64: string): Uint8Array { + if ((globalThis as any).Buffer) { + return Uint8Array.from(globalThis.Buffer.from(b64, "base64")); + } else { + const bin = globalThis.atob(b64); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; ++i) { + arr[i] = bin.charCodeAt(i); + } + return arr; + } +} + +function base64FromBytes(arr: Uint8Array): string { + if ((globalThis as any).Buffer) { + return globalThis.Buffer.from(arr).toString("base64"); + } else { + const bin: string[] = []; + arr.forEach((byte) => { + bin.push(globalThis.String.fromCharCode(byte)); + }); + return globalThis.btoa(bin.join("")); + } +} + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +function longToNumber(int64: { toString(): string }): number { + const num = globalThis.Number(int64.toString()); + if (num > globalThis.Number.MAX_SAFE_INTEGER) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + if (num < globalThis.Number.MIN_SAFE_INTEGER) { + throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); + } + return num; +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export type ServerStreamingMethodResult = { [Symbol.asyncIterator](): AsyncIterator }; + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create(base?: DeepPartial): T; + fromPartial(object: DeepPartial): T; +} diff --git a/packages/grpc/tsconfig.json b/packages/grpc/tsconfig.json new file mode 100644 index 0000000..0ac97f1 --- /dev/null +++ b/packages/grpc/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "inlineSourceMap": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "types": ["node"], + "outDir": "./dist", + }, + "include": [ + "./src" + ], + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Recommended" +} \ No newline at end of file diff --git a/packages/server/package.json b/packages/server/package.json index 05d3314..6c5bde2 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -16,8 +16,10 @@ "author": "Eithan Hersey-Tuit", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.13.7", "@jobber/tcp-frame-socket": "workspace:*", + "@jobber/common": "workspace:*", + "@jobber/grpc": "workspace:*", + "@hono/node-server": "^1.13.7", "bcryptjs": "^3.0.2", "cron": "^3.2.1", "drizzle-orm": "^0.38.2", @@ -28,7 +30,12 @@ "reflect-metadata": "^0.2.2", "semver": "^7.6.3", "tsyringe": "^4.10.0", - "zod": "^3.23.8" + "zod": "^3.23.8", + "@grpc/grpc-js": "^1.14.3", + "@grpc/proto-loader": "^0.8.0", + "long": "^5.3.2", + "nice-grpc": "^2.1.14", + "protobufjs": "^8.0.0" }, "devDependencies": { "@tsconfig/node20": "^20.1.4", @@ -39,6 +46,8 @@ "rimraf": "^5.0.10", "tsc-alias": "^1.8.10", "typescript": "^5.6.3", - "vitest": "^3.2.4" + "vitest": "^3.2.4", + "grpc-tools": "^1.13.1", + "ts-proto": "^2.10.1" } } diff --git a/packages/server/src/bouncer.ts b/packages/server/src/bouncer.ts index f8fa5a1..3b9f790 100644 --- a/packages/server/src/bouncer.ts +++ b/packages/server/src/bouncer.ts @@ -1,4 +1,3 @@ -import assert from "node:assert"; import { ApiTokensTableType } from "./db/schema/api-tokens.js"; import { SessionsTableType } from "./db/schema/sessions.js"; import { UsersTableType } from "./db/schema/users.js"; @@ -8,7 +7,6 @@ import { JobberPermissions, } from "./permissions.js"; import { HTTPException } from "hono/http-exception"; -import { JobsTableType } from "./db/schema/jobs.js"; type BouncerOptions = | { diff --git a/packages/server/src/config.ts b/packages/server/src/config.ts index e0be2ce..ec95b89 100644 --- a/packages/server/src/config.ts +++ b/packages/server/src/config.ts @@ -38,6 +38,9 @@ export const ConfigurationOptionsSchema = z.object({ MANAGER_PORT: z.coerce.number().default(5211), MANAGER_HOST: z.string().default(hostname()), + MANAGER_GRPC_PORT: z.coerce.number().default(5212), + MANAGER_GRPC_BIND_ADDRESS: z.string().default("0.0.0.0"), + RUNNER_IMAGE_NODE24_URL: z .string() .default("eithan1231/runner-node-24:latest"), diff --git a/packages/server/src/grpc/index.ts b/packages/server/src/grpc/index.ts new file mode 100644 index 0000000..6dcda0e --- /dev/null +++ b/packages/server/src/grpc/index.ts @@ -0,0 +1,80 @@ +import { LoopBase } from "@jobber/common"; +import { GeneralManagementDefinition } from "@jobber/grpc/server.js"; +import { createServer, Server, ServiceImplementation } from "nice-grpc"; +import { singleton } from "tsyringe"; +import { getConfigOption } from "~/config.js"; + +const generalManagementDefinition: ServiceImplementation = + { + createJwt(request, context) { + throw new Error("Method not implemented."); + }, + + getPublicKeys(request, context) { + throw new Error("Method not implemented."); + }, + + createRunner(request, context) { + throw new Error("Method not implemented."); + }, + + getAction(request, context) { + throw new Error("Method not implemented."); + }, + + getJobs(request, context) { + throw new Error("Method not implemented."); + }, + + getRunners(request, context) { + throw new Error("Method not implemented."); + }, + + getHttpTriggers(request, context) { + throw new Error("Method not implemented."); + }, + + getMqttTriggers(request, context) { + throw new Error("Method not implemented."); + }, + + getScheduleTriggers(request, context) { + throw new Error("Method not implemented."); + }, + + getJob(request, context) { + throw new Error("Method not implemented."); + }, + + getGatewayConfig(request, context) { + throw new Error("Method not implemented."); + }, + }; + +@singleton() +export class GrpcServer extends LoopBase { + protected loopDuration = 1000; + + protected loopStarted = undefined; + protected loopClosed = undefined; + + private server: Server | null = null; + + protected async loopStarting() { + this.server = createServer({}); + + this.server.add(GeneralManagementDefinition, generalManagementDefinition); + + await this.server.listen( + `${getConfigOption("MANAGER_GRPC_BIND_ADDRESS")}:${getConfigOption( + "MANAGER_GRPC_PORT" + )}` + ); + } + + protected async loopClosing() { + await this.server?.shutdown(); + } + + protected async loopIteration() {} +} diff --git a/packages/server/src/jobber/log-drivers/abstract.ts b/packages/server/src/jobber/log-drivers/abstract.ts index d43eae8..6a61b7d 100644 --- a/packages/server/src/jobber/log-drivers/abstract.ts +++ b/packages/server/src/jobber/log-drivers/abstract.ts @@ -1,4 +1,4 @@ -import { LoopBase } from "~/loop-base.js"; +import { LoopBase } from "@jobber/common"; export type LogDriverBaseItem = { actionId: string; diff --git a/packages/server/src/jobber/runners/manager.ts b/packages/server/src/jobber/runners/manager.ts index 150e9f8..b1aecc5 100644 --- a/packages/server/src/jobber/runners/manager.ts +++ b/packages/server/src/jobber/runners/manager.ts @@ -21,7 +21,7 @@ import { pullDockerImage, stopDockerContainer, } from "~/docker.js"; -import { LoopBase } from "~/loop-base.js"; +import { LoopBase, awaitTruthy, timeout } from "@jobber/common"; import { counterRunnerRequests, gaugeActiveRunners, @@ -31,13 +31,11 @@ import { histogramRunnerStartupDuration, } from "~/metrics.js"; import { - awaitTruthy, createBenchmark, createToken, getUnixTimestamp, sanitiseSafeCharacters, shortenString, - timeout, } from "~/util.js"; import { getImage, getImages } from "../images.js"; import { LogDriverBase } from "../log-drivers/abstract.js"; diff --git a/packages/server/src/jobber/runners/server.ts b/packages/server/src/jobber/runners/server.ts index 91c6b6e..6a8adfa 100644 --- a/packages/server/src/jobber/runners/server.ts +++ b/packages/server/src/jobber/runners/server.ts @@ -1,3 +1,4 @@ +import { LoopBase, awaitTruthy, timeout } from "@jobber/common"; import { TcpFrameSocket } from "@jobber/tcp-frame-socket"; import EventEmitter from "events"; import { readFile } from "fs/promises"; @@ -6,7 +7,7 @@ import { getConfigOption } from "~/config.js"; import { ActionsTableType } from "~/db/schema/actions.js"; import { JobVersionsTableType } from "~/db/schema/job-versions.js"; import { getJobActionArchiveFile } from "~/paths.js"; -import { awaitTruthy, createToken, shortenString } from "~/util.js"; +import { createToken, shortenString } from "~/util.js"; import { Store } from "../store.js"; export type HandleRequestSchedule = { diff --git a/packages/server/src/jobber/store.ts b/packages/server/src/jobber/store.ts index 42d14aa..39fbf4a 100644 --- a/packages/server/src/jobber/store.ts +++ b/packages/server/src/jobber/store.ts @@ -2,7 +2,7 @@ import { and, eq, lt } from "drizzle-orm"; import { singleton } from "tsyringe"; import { getDrizzle } from "~/db/index.js"; import { storeTable } from "~/db/schema/store.js"; -import { LoopBase } from "~/loop-base.js"; +import { LoopBase } from "@jobber/common"; import { getUnixTimestamp } from "~/util.js"; type StoreItem = { diff --git a/packages/server/src/jobber/telemetry.ts b/packages/server/src/jobber/telemetry.ts index 8d3ba0e..ca6d75d 100644 --- a/packages/server/src/jobber/telemetry.ts +++ b/packages/server/src/jobber/telemetry.ts @@ -3,7 +3,7 @@ import { singleton } from "tsyringe"; import { getDrizzle } from "~/db/index.js"; import { jobsTable } from "~/db/schema/jobs.js"; import { storeTable } from "~/db/schema/store.js"; -import { LoopBase } from "~/loop-base.js"; +import { LoopBase } from "@jobber/common"; import { gaugeAppInfo, gaugeJobsInfo, gaugeJobStoreCount } from "~/metrics.js"; import { getUnixTimestamp } from "~/util.js"; diff --git a/packages/server/src/jobber/triggers/cron.ts b/packages/server/src/jobber/triggers/cron.ts index f47557c..62712c0 100644 --- a/packages/server/src/jobber/triggers/cron.ts +++ b/packages/server/src/jobber/triggers/cron.ts @@ -1,8 +1,9 @@ +import { LoopBase } from "@jobber/common"; import assert from "assert"; import { CronTime } from "cron"; import { CronError } from "cron/dist/errors.js"; import { and, eq, isNotNull, sql } from "drizzle-orm"; -import { autoInjectable, inject, singleton } from "tsyringe"; +import { inject, singleton } from "tsyringe"; import { getDrizzle } from "~/db/index.js"; import { actionsTable, ActionsTableType } from "~/db/schema/actions.js"; import { @@ -11,7 +12,6 @@ import { } from "~/db/schema/job-versions.js"; import { jobsTable, JobsTableType } from "~/db/schema/jobs.js"; import { triggersTable, TriggersTableType } from "~/db/schema/triggers.js"; -import { LoopBase } from "~/loop-base.js"; import { counterTriggerCron } from "~/metrics.js"; import { LogDriverBase } from "../log-drivers/abstract.js"; import { RunnerManager } from "../runners/manager.js"; diff --git a/packages/server/src/jobber/triggers/http.ts b/packages/server/src/jobber/triggers/http.ts index 19deec8..1abf427 100644 --- a/packages/server/src/jobber/triggers/http.ts +++ b/packages/server/src/jobber/triggers/http.ts @@ -8,13 +8,12 @@ import { } from "~/db/schema/job-versions.js"; import { jobsTable, JobsTableType } from "~/db/schema/jobs.js"; import { triggersTable, TriggersTableType } from "~/db/schema/triggers.js"; -import { LoopBase } from "~/loop-base.js"; +import { LoopBase } from "@jobber/common"; import { counterTriggerHttp } from "~/metrics.js"; -import { getUnixTimestamp } from "~/util.js"; import { LogDriverBase } from "../log-drivers/abstract.js"; import { RunnerManager } from "../runners/manager.js"; import { HandleRequest, HandleRequestHttp } from "../runners/server.js"; -import { autoInjectable, inject, singleton } from "tsyringe"; +import { inject, singleton } from "tsyringe"; type TriggerHttpItem = { trigger: Omit & { diff --git a/packages/server/src/jobber/triggers/mqtt.ts b/packages/server/src/jobber/triggers/mqtt.ts index 7d25d94..d78aa6f 100644 --- a/packages/server/src/jobber/triggers/mqtt.ts +++ b/packages/server/src/jobber/triggers/mqtt.ts @@ -13,7 +13,7 @@ import { } from "~/db/schema/job-versions.js"; import { jobsTable, JobsTableType } from "~/db/schema/jobs.js"; import { triggersTable, TriggersTableType } from "~/db/schema/triggers.js"; -import { LoopBase } from "~/loop-base.js"; +import { LoopBase } from "@jobber/common"; import { counterTriggerMqtt, counterTriggerMqttPublish } from "~/metrics.js"; import { createSha1Hash, shortenString } from "~/util.js"; import { LogDriverBase } from "../log-drivers/abstract.js"; diff --git a/packages/server/src/lock.ts b/packages/server/src/lock.ts index 50fa99a..04d92a6 100644 --- a/packages/server/src/lock.ts +++ b/packages/server/src/lock.ts @@ -1,7 +1,7 @@ import { eq, lt, sql } from "drizzle-orm"; import { getDrizzle } from "./db/index.js"; import { lockTable } from "./db/schema/lock.js"; -import { timeout } from "./util.js"; +import { timeout } from "@jobber/common"; export const acquireLock = async ( table: string, diff --git a/packages/server/src/pg-backup.ts b/packages/server/src/pg-backup.ts index 3fe9bb1..4c5127c 100644 --- a/packages/server/src/pg-backup.ts +++ b/packages/server/src/pg-backup.ts @@ -1,7 +1,7 @@ import { CronTime } from "cron"; import { singleton } from "tsyringe"; -import { LoopBase } from "~/loop-base.js"; +import { LoopBase } from "@jobber/common"; import { getConfigOption } from "./config.js"; import { getPgDumpDirectory } from "./paths.js"; diff --git a/packages/server/src/util.ts b/packages/server/src/util.ts index 7ab2ebf..9eb249e 100644 --- a/packages/server/src/util.ts +++ b/packages/server/src/util.ts @@ -9,44 +9,6 @@ import { ReadableStream } from "stream/web"; export const getUnixTimestamp = () => Math.round(Date.now() / 1000); -export const timeout = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); - -/** - * Awaits until the callback yields true - */ -export const awaitTruthy = async ( - callback: () => Promise, - timeoutMs: number = 30_000 -) => { - let startTime = Date.now(); - - let index = 0; - while (true) { - if (Date.now() - startTime > timeoutMs) { - return false; - } - - if (await callback()) { - return true; - } - - index++; - - if (index <= 10) { - await timeout(10); - } - - if (index > 10 && index <= 20) { - await timeout(20); - } - - if (index > 20) { - await timeout(100); - } - } -}; - export const sanitiseFilename = (filename: string) => { return filename.replaceAll(/[^0-9a-z-_ .]/gi, "").substring(0, 255); }; diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index 373f671..3c7c8ab 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -9,6 +9,8 @@ export default defineConfig({ "/api": { target: "http://localhost:3000", changeOrigin: true, + secure: false, + followRedirects: true, }, }, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4bc7d16..341b7cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,150 @@ importers: .: {} + packages/common: + dependencies: + '@jobber/tcp-frame-socket': + specifier: workspace:* + version: link:../tcp-frame-socket + devDependencies: + '@tsconfig/node20': + specifier: ^20.1.4 + version: 20.1.4 + '@types/node': + specifier: ^20.16.12 + version: 20.17.10 + rimraf: + specifier: ^5.0.10 + version: 5.0.10 + tsc-alias: + specifier: ^1.8.10 + version: 1.8.10 + typescript: + specifier: ^5.6.3 + version: 5.7.2 + + packages/gateway: + dependencies: + '@grpc/grpc-js': + specifier: ^1.14.3 + version: 1.14.3 + '@grpc/proto-loader': + specifier: ^0.8.0 + version: 0.8.0 + '@hono/node-server': + specifier: ^1.13.7 + version: 1.13.7(hono@4.6.13) + '@jobber/common': + specifier: workspace:* + version: link:../common + '@jobber/grpc': + specifier: workspace:* + version: link:../grpc + '@jobber/tcp-frame-socket': + specifier: workspace:* + version: link:../tcp-frame-socket + hono: + specifier: ^4.6.11 + version: 4.6.13 + long: + specifier: ^5.3.2 + version: 5.3.2 + nice-grpc: + specifier: ^2.1.14 + version: 2.1.14 + prom-client: + specifier: ^15.1.3 + version: 15.1.3 + protobufjs: + specifier: ^8.0.0 + version: 8.0.0 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + semver: + specifier: ^7.6.3 + version: 7.6.3 + tsyringe: + specifier: ^4.10.0 + version: 4.10.0 + zod: + specifier: ^3.23.8 + version: 3.23.8 + devDependencies: + '@tsconfig/node20': + specifier: ^20.1.4 + version: 20.1.4 + '@types/node': + specifier: ^20.16.12 + version: 20.17.10 + '@types/semver': + specifier: ^7.5.8 + version: 7.5.8 + grpc-tools: + specifier: ^1.13.1 + version: 1.13.1 + rimraf: + specifier: ^5.0.10 + version: 5.0.10 + ts-proto: + specifier: ^2.10.1 + version: 2.10.1 + tsc-alias: + specifier: ^1.8.10 + version: 1.8.10 + typescript: + specifier: ^5.6.3 + version: 5.7.2 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@20.17.10)(jiti@2.4.1)(yaml@2.6.1) + + packages/grpc: + dependencies: + '@bufbuild/protobuf': + specifier: ^2.10.2 + version: 2.10.2 + '@grpc/grpc-js': + specifier: ^1.14.3 + version: 1.14.3 + '@grpc/proto-loader': + specifier: ^0.8.0 + version: 0.8.0 + long: + specifier: ^5.3.2 + version: 5.3.2 + nice-grpc: + specifier: ^2.1.14 + version: 2.1.14 + nice-grpc-common: + specifier: ^2.0.2 + version: 2.0.2 + prom-client: + specifier: ^15.1.3 + version: 15.1.3 + protobufjs: + specifier: ^8.0.0 + version: 8.0.0 + devDependencies: + '@tsconfig/node20': + specifier: ^20.1.4 + version: 20.1.4 + '@types/node': + specifier: ^20.16.12 + version: 20.17.10 + grpc-tools: + specifier: ^1.13.1 + version: 1.13.1 + ts-proto: + specifier: ^2.10.1 + version: 2.10.1 + tsc-alias: + specifier: ^1.8.10 + version: 1.8.10 + typescript: + specifier: ^5.6.3 + version: 5.7.2 + packages/runner-node-entrypoint: dependencies: '@jobber/tcp-frame-socket': @@ -36,9 +180,21 @@ importers: packages/server: dependencies: + '@grpc/grpc-js': + specifier: ^1.14.3 + version: 1.14.3 + '@grpc/proto-loader': + specifier: ^0.8.0 + version: 0.8.0 '@hono/node-server': specifier: ^1.13.7 version: 1.13.7(hono@4.6.13) + '@jobber/common': + specifier: workspace:* + version: link:../common + '@jobber/grpc': + specifier: workspace:* + version: link:../grpc '@jobber/tcp-frame-socket': specifier: workspace:* version: link:../tcp-frame-socket @@ -54,15 +210,24 @@ importers: hono: specifier: ^4.6.11 version: 4.6.13 + long: + specifier: ^5.3.2 + version: 5.3.2 mqtt: specifier: ^5.10.3 version: 5.10.3 + nice-grpc: + specifier: ^2.1.14 + version: 2.1.14 pg: specifier: ^8.13.1 version: 8.13.1 prom-client: specifier: ^15.1.3 version: 15.1.3 + protobufjs: + specifier: ^8.0.0 + version: 8.0.0 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -91,9 +256,15 @@ importers: drizzle-kit: specifier: ^0.30.1 version: 0.30.1 + grpc-tools: + specifier: ^1.13.1 + version: 1.13.1 rimraf: specifier: ^5.0.10 version: 5.0.10 + ts-proto: + specifier: ^2.10.1 + version: 2.10.1 tsc-alias: specifier: ^1.8.10 version: 1.8.10 @@ -276,6 +447,9 @@ packages: resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} engines: {node: '>=6.9.0'} + '@bufbuild/protobuf@2.10.2': + resolution: {integrity: sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==} + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -735,6 +909,15 @@ packages: resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@grpc/grpc-js@1.14.3': + resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.8.0': + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + '@hono/node-server@1.13.7': resolution: {integrity: sha512-kTfUMsoloVKtRA2fLiGSd9qBddmru9KadNyhJCwgKBxTiNkaAJEwkVN9KV/rS4HtmmNRtUh6P+YpmjRMl0d9vQ==} engines: {node: '>=18.14.1'} @@ -765,6 +948,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -783,9 +970,17 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@mapbox/node-pre-gyp@2.0.3': + resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} + engines: {node: '>=18'} + hasBin: true + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -806,6 +1001,36 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@rollup/rollup-android-arm-eabi@4.28.1': resolution: {integrity: sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==} cpu: [arm] @@ -1055,6 +1280,13 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + abbrev@3.0.1: + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} + engines: {node: ^18.17.0 || >=20.5.0} + + abort-controller-x@0.4.3: + resolution: {integrity: sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -1069,6 +1301,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1178,6 +1414,10 @@ packages: caniuse-lite@1.0.30001687: resolution: {integrity: sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==} + case-anything@2.1.13: + resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==} + engines: {node: '>=12.13'} + chai@5.2.1: resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} engines: {node: '>=18'} @@ -1202,6 +1442,14 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1287,6 +1535,15 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -1297,6 +1554,9 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dprint-node@1.0.8: + resolution: {integrity: sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==} + drizzle-kit@0.30.1: resolution: {integrity: sha512-HmA/NeewvHywhJ2ENXD3KvOuM/+K2dGLJfxVfIHsGwaqKICJnS+Ke2L6UcSrSrtMJLJaT0Im1Qv4TFXfaZShyw==} hasBin: true @@ -1578,6 +1838,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} @@ -1612,6 +1876,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + grpc-tools@1.13.1: + resolution: {integrity: sha512-0sttMUxThNIkCTJq5qI0xXMz5zWqV2u3yG1kR3Sj9OokGIoyRBFjoInK9NyW7x5fH7knj48Roh1gq5xbl0VoDQ==} + hasBin: true + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1627,6 +1895,10 @@ packages: resolution: {integrity: sha512-haV0gaMdSjy9URCRN9hxBPlqHa7fMm/T72kAImIxvw4eQLbNz1rgjN4hHElLJSieDiNuiIAXC//cC6YGz2KCbg==} engines: {node: '>=16.9.0'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -1741,12 +2013,18 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loupe@3.2.0: resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} @@ -1785,6 +2063,10 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mqtt-packet@9.0.1: resolution: {integrity: sha512-koZF1V/X2RZUI6uD9wN5OK1JxxcG1ofAR4H3LjCw1FkeKzruZQ26aAA6v2m1lZyWONZIR5wMMJFrZJDRNzbiQw==} @@ -1811,9 +2093,29 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + nice-grpc-common@2.0.2: + resolution: {integrity: sha512-7RNWbls5kAL1QVUOXvBsv1uO0wPQK3lHv+cY1gwkTzirnG1Nop4cBJZubpgziNbaVc/bl9QJcyvsf/NQxa3rjQ==} + + nice-grpc@2.1.14: + resolution: {integrity: sha512-GK9pKNxlvnU5FAdaw7i2FFuR9CqBspcE+if2tqnKXBcE0R8525wj4BZvfcwj7FjvqbssqKxRHt2nwedalbJlww==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + nopt@8.1.0: + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -2055,6 +2357,14 @@ packages: resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} engines: {node: ^16 || ^18 || >=20} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + + protobufjs@8.0.0: + resolution: {integrity: sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==} + engines: {node: '>=12.0.0'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2130,6 +2440,10 @@ packages: reinterval@1.1.0: resolution: {integrity: sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2218,6 +2532,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} @@ -2273,6 +2588,10 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + tar@7.5.2: + resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} + engines: {node: '>=18'} + tdigest@0.1.2: resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} @@ -2316,6 +2635,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -2329,9 +2651,22 @@ packages: peerDependencies: typescript: '>=4.2.0' + ts-error@1.0.6: + resolution: {integrity: sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==} + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-poet@6.12.0: + resolution: {integrity: sha512-xo+iRNMWqyvXpFTaOAvLPA5QAWO6TZrSUs5s4Odaya3epqofBu/fMLHEWl8jPmjhA0s9sgj9sNvF1BmaQlmQkA==} + + ts-proto-descriptors@2.1.0: + resolution: {integrity: sha512-S5EZYEQ6L9KLFfjSRpZWDIXDV/W7tAj8uW7pLsihIxyr62EAVSiKuVPwE8iWnr849Bqa53enex1jhDUcpgquzA==} + + ts-proto@2.10.1: + resolution: {integrity: sha512-4sOE1hCs0uobJgdRCtcEwdbc8MAyKP+LJqUIKxZIiKac0rPBlVKsRGEGo2oQ1MnKA2Wwk0KuGP2POkiCwPtebw==} + hasBin: true + tsc-alias@1.8.10: resolution: {integrity: sha512-Ibv4KAWfFkFdKJxnWfVtdOmB0Zi1RJVxcbPGiCDsFpCQSsmpWyuzHG3rQyI5YkobWwxFPEyQfu1hdo4qLG2zPw==} hasBin: true @@ -2481,9 +2816,15 @@ packages: jsdom: optional: true + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -2534,14 +2875,30 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml@2.6.1: resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} engines: {node: '>= 14'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2672,6 +3029,8 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@bufbuild/protobuf@2.10.2': {} + '@drizzle-team/brocli@0.10.2': {} '@esbuild-kit/core-utils@3.3.2': @@ -2932,6 +3291,18 @@ snapshots: dependencies: levn: 0.4.1 + '@grpc/grpc-js@1.14.3': + dependencies: + '@grpc/proto-loader': 0.8.0 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.8.0': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + '@hono/node-server@1.13.7(hono@4.6.13)': dependencies: hono: 4.6.13 @@ -2958,6 +3329,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -2975,8 +3350,23 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@js-sdsl/ordered-map@4.4.2': {} + '@kurkle/color@0.3.4': {} + '@mapbox/node-pre-gyp@2.0.3': + dependencies: + consola: 3.3.0 + detect-libc: 2.1.2 + https-proxy-agent: 7.0.6 + node-fetch: 2.7.0 + nopt: 8.1.0 + semver: 7.6.3 + tar: 7.5.2 + transitivePeerDependencies: + - encoding + - supports-color + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2994,6 +3384,29 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@rollup/rollup-android-arm-eabi@4.28.1': optional: true @@ -3257,6 +3670,10 @@ snapshots: loupe: 3.2.0 tinyrainbow: 2.0.0 + abbrev@3.0.1: {} + + abort-controller-x@0.4.3: {} + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -3267,6 +3684,8 @@ snapshots: acorn@8.14.0: {} + agent-base@7.1.4: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -3366,6 +3785,8 @@ snapshots: caniuse-lite@1.0.30001687: {} + case-anything@2.1.13: {} + chai@5.2.1: dependencies: assertion-error: 2.0.1 @@ -3401,6 +3822,14 @@ snapshots: dependencies: readdirp: 4.0.2 + chownr@3.0.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3459,6 +3888,10 @@ snapshots: deep-is@0.1.4: {} + detect-libc@1.0.3: {} + + detect-libc@2.1.2: {} + didyoumean@1.2.2: {} dir-glob@3.0.1: @@ -3467,6 +3900,10 @@ snapshots: dlv@1.1.3: {} + dprint-node@1.0.8: + dependencies: + detect-libc: 1.0.3 + drizzle-kit@0.30.1: dependencies: '@drizzle-team/brocli': 0.10.2 @@ -3734,6 +4171,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-tsconfig@4.8.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -3772,6 +4211,13 @@ snapshots: graphemer@1.4.0: {} + grpc-tools@1.13.1: + dependencies: + '@mapbox/node-pre-gyp': 2.0.3 + transitivePeerDependencies: + - encoding + - supports-color + has-flag@4.0.0: {} hasown@2.0.2: @@ -3782,6 +4228,13 @@ snapshots: hono@4.6.13: {} + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -3867,10 +4320,14 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + lodash.merge@4.6.2: {} lodash.sortby@4.7.0: {} + long@5.3.2: {} + loupe@3.2.0: {} lru-cache@10.4.3: {} @@ -3904,6 +4361,10 @@ snapshots: minipass@7.1.2: {} + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + mqtt-packet@9.0.1: dependencies: bl: 6.0.16 @@ -3949,8 +4410,26 @@ snapshots: natural-compare@1.4.0: {} + nice-grpc-common@2.0.2: + dependencies: + ts-error: 1.0.6 + + nice-grpc@2.1.14: + dependencies: + '@grpc/grpc-js': 1.14.3 + abort-controller-x: 0.4.3 + nice-grpc-common: 2.0.2 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-releases@2.0.18: {} + nopt@8.1.0: + dependencies: + abbrev: 3.0.1 + normalize-path@3.0.0: {} normalize-range@0.1.2: {} @@ -4147,6 +4626,36 @@ snapshots: '@opentelemetry/api': 1.9.0 tdigest: 0.1.2 + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.17.10 + long: 5.3.2 + + protobufjs@8.0.0: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.17.10 + long: 5.3.2 + punycode@2.3.1: {} queue-lit@1.5.2: {} @@ -4213,6 +4722,8 @@ snapshots: reinterval@1.1.0: {} + require-directory@2.1.1: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -4378,6 +4889,14 @@ snapshots: transitivePeerDependencies: - ts-node + tar@7.5.2: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + tdigest@0.1.2: dependencies: bintrees: 1.0.2 @@ -4416,6 +4935,8 @@ snapshots: dependencies: is-number: 7.0.0 + tr46@0.0.3: {} + tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -4426,8 +4947,25 @@ snapshots: dependencies: typescript: 5.7.2 + ts-error@1.0.6: {} + ts-interface-checker@0.1.13: {} + ts-poet@6.12.0: + dependencies: + dprint-node: 1.0.8 + + ts-proto-descriptors@2.1.0: + dependencies: + '@bufbuild/protobuf': 2.10.2 + + ts-proto@2.10.1: + dependencies: + '@bufbuild/protobuf': 2.10.2 + case-anything: 2.1.13 + ts-poet: 6.12.0 + ts-proto-descriptors: 2.1.0 + tsc-alias@1.8.10: dependencies: chokidar: 3.6.0 @@ -4594,8 +5132,15 @@ snapshots: - tsx - yaml + webidl-conversions@3.0.1: {} + webidl-conversions@4.0.2: {} + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 @@ -4648,10 +5193,26 @@ snapshots: xtend@4.0.2: {} + y18n@5.0.8: {} + yallist@3.1.1: {} + yallist@5.0.0: {} + yaml@2.6.1: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} zod@3.23.8: {} From adcf2fee7c377af8d19467e2764a8eba2981c56e Mon Sep 17 00:00:00 2001 From: Eithan Date: Mon, 26 Jan 2026 21:37:15 +1100 Subject: [PATCH 02/25] chore: wip --- packages/common/src/loop-base.ts | 78 +++++--- packages/grpc/proto/server.proto | 17 +- packages/grpc/src/server.ts | 177 +++++++++++++----- packages/runner-node-entrypoint/src/index.ts | 2 +- packages/server/package.json | 23 +-- packages/server/src/bouncer.ts | 4 + packages/server/src/config.ts | 9 + packages/server/src/db/api-tokens.ts | 22 +++ packages/server/src/db/schema/jwt-keys.ts | 21 +++ packages/server/src/db/user.ts | 30 +++ packages/server/src/grpc/index.ts | 99 +++++++++- packages/server/src/index.ts | 100 +++++++++- packages/server/src/jobber/runners/manager.ts | 4 + .../server/src/middleware/response-time.ts | 2 +- packages/server/src/routes/api-tokens.ts | 2 +- pnpm-lock.yaml | 8 + 16 files changed, 495 insertions(+), 103 deletions(-) create mode 100644 packages/server/src/db/api-tokens.ts create mode 100644 packages/server/src/db/schema/jwt-keys.ts create mode 100644 packages/server/src/db/user.ts diff --git a/packages/common/src/loop-base.ts b/packages/common/src/loop-base.ts index a4739ea..2ae6a3b 100644 --- a/packages/common/src/loop-base.ts +++ b/packages/common/src/loop-base.ts @@ -1,6 +1,7 @@ import assert from "node:assert"; import { awaitTruthy } from "./await-truthy.js"; import { timeout } from "./timeout.js"; +import EventEmitter from "node:events"; /** * Lifecycle: @@ -12,55 +13,68 @@ import { timeout } from "./timeout.js"; */ export type StatusLifecycle = "neutral" | "starting" | "started" | "stopping"; -export abstract class LoopBase { - private isLoopRunning = false; +type EventEmitterEvents = { + neutral: []; + starting: []; + started: []; + stopping: []; +}; +export abstract class LoopBase { protected status: StatusLifecycle = "neutral"; protected abstract loopDuration: number; - public async start() { - assert(this.status === "neutral"); + private events = new EventEmitter(); - this.status = "starting"; + public start() { + return new Promise(async (resolve) => { + assert(this.status === "neutral"); - if (this.loopStarting) { - await this.loopStarting(); - } + this.events.once("started", () => { + resolve(); + }); - this.loop(); + this.status = "starting"; - await awaitTruthy(() => Promise.resolve(this.isLoopRunning)); + if (this.loopStarting) { + await this.loopStarting(); + } - this.status = "started"; + this.events.emit("starting"); - if (this.loopStarted) { - await this.loopStarted(); - } + this.loop(); + }); } - public async stop() { - assert(this.status === "started"); - - this.status = "stopping"; + public stop() { + return new Promise(async (resolve) => { + assert(this.status === "started"); - if (this.loopClosing) { - await this.loopClosing(); - } + this.events.once("neutral", () => { + resolve(); + }); - await awaitTruthy(() => Promise.resolve(!this.isLoopRunning)); + this.status = "stopping"; - this.status = "neutral"; + if (this.loopClosing) { + await this.loopClosing(); + } - if (this.loopClosed) { - await this.loopClosed(); - } + this.events.emit("stopping"); + }); } private async loop() { - this.isLoopRunning = true; + this.status = "started"; + + if (this.loopStarted) { + await this.loopStarted(); + } + + this.events.emit("started"); - while (this.status === "starting" || this.status === "started") { + while (this.status === "started") { try { await this.loopIteration(); } catch (err) { @@ -70,7 +84,13 @@ export abstract class LoopBase { await timeout(this.loopDuration); } - this.isLoopRunning = false; + this.status = "neutral"; + + if (this.loopClosed) { + await this.loopClosed(); + } + + this.events.emit("neutral"); } protected abstract loopIteration(): Promise; diff --git a/packages/grpc/proto/server.proto b/packages/grpc/proto/server.proto index 743d9d2..e4a52dc 100644 --- a/packages/grpc/proto/server.proto +++ b/packages/grpc/proto/server.proto @@ -22,9 +22,10 @@ service GeneralManagement { rpc getScheduleTriggers (GetTriggerRequest) returns (stream TriggerScheduleItem); rpc getRunners (GetRunnersRequest) returns (stream RunnerItem); + // Authentication related rpc getPublicKeys(Empty) returns (PublicKeysResponse); - rpc createJwt (JwtRequest) returns (JwtResponse); + rpc createRunnerJwt (CreateRunnerJwtRequest) returns (CreateRunnerJwtResponse); // Gateway Specific @@ -63,12 +64,18 @@ message PublicKeysResponse { repeated string publicKeys = 1; } -message JwtRequest { - string token = 1; - repeated string scopes = 2; +message CreateRunnerJwtRequest { + enum Scope { + RUNNER_EVENT_HTTP = 0; + RUNNER_EVENT_MQTT = 1; + RUNNER_EVENT_SCHEDULE = 2; + } + + string runnerId = 1; + repeated Scope scopes = 2; } -message JwtResponse { +message CreateRunnerJwtResponse { string jwt = 1; } diff --git a/packages/grpc/src/server.ts b/packages/grpc/src/server.ts index 1810929..47f0ed4 100644 --- a/packages/grpc/src/server.ts +++ b/packages/grpc/src/server.ts @@ -219,12 +219,75 @@ export interface PublicKeysResponse { publicKeys: string[]; } -export interface JwtRequest { - token: string; - scopes: string[]; +export interface CreateRunnerJwtRequest { + runnerId: string; + scopes: CreateRunnerJwtRequest_Scope[]; +} + +export const CreateRunnerJwtRequest_Scope = { + RUNNER_EVENT_HTTP: "RUNNER_EVENT_HTTP", + RUNNER_EVENT_MQTT: "RUNNER_EVENT_MQTT", + RUNNER_EVENT_SCHEDULE: "RUNNER_EVENT_SCHEDULE", + UNRECOGNIZED: "UNRECOGNIZED", +} as const; + +export type CreateRunnerJwtRequest_Scope = + typeof CreateRunnerJwtRequest_Scope[keyof typeof CreateRunnerJwtRequest_Scope]; + +export namespace CreateRunnerJwtRequest_Scope { + export type RUNNER_EVENT_HTTP = typeof CreateRunnerJwtRequest_Scope.RUNNER_EVENT_HTTP; + export type RUNNER_EVENT_MQTT = typeof CreateRunnerJwtRequest_Scope.RUNNER_EVENT_MQTT; + export type RUNNER_EVENT_SCHEDULE = typeof CreateRunnerJwtRequest_Scope.RUNNER_EVENT_SCHEDULE; + export type UNRECOGNIZED = typeof CreateRunnerJwtRequest_Scope.UNRECOGNIZED; +} + +export function createRunnerJwtRequest_ScopeFromJSON(object: any): CreateRunnerJwtRequest_Scope { + switch (object) { + case 0: + case "RUNNER_EVENT_HTTP": + return CreateRunnerJwtRequest_Scope.RUNNER_EVENT_HTTP; + case 1: + case "RUNNER_EVENT_MQTT": + return CreateRunnerJwtRequest_Scope.RUNNER_EVENT_MQTT; + case 2: + case "RUNNER_EVENT_SCHEDULE": + return CreateRunnerJwtRequest_Scope.RUNNER_EVENT_SCHEDULE; + case -1: + case "UNRECOGNIZED": + default: + return CreateRunnerJwtRequest_Scope.UNRECOGNIZED; + } +} + +export function createRunnerJwtRequest_ScopeToJSON(object: CreateRunnerJwtRequest_Scope): string { + switch (object) { + case CreateRunnerJwtRequest_Scope.RUNNER_EVENT_HTTP: + return "RUNNER_EVENT_HTTP"; + case CreateRunnerJwtRequest_Scope.RUNNER_EVENT_MQTT: + return "RUNNER_EVENT_MQTT"; + case CreateRunnerJwtRequest_Scope.RUNNER_EVENT_SCHEDULE: + return "RUNNER_EVENT_SCHEDULE"; + case CreateRunnerJwtRequest_Scope.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function createRunnerJwtRequest_ScopeToNumber(object: CreateRunnerJwtRequest_Scope): number { + switch (object) { + case CreateRunnerJwtRequest_Scope.RUNNER_EVENT_HTTP: + return 0; + case CreateRunnerJwtRequest_Scope.RUNNER_EVENT_MQTT: + return 1; + case CreateRunnerJwtRequest_Scope.RUNNER_EVENT_SCHEDULE: + return 2; + case CreateRunnerJwtRequest_Scope.UNRECOGNIZED: + default: + return -1; + } } -export interface JwtResponse { +export interface CreateRunnerJwtResponse { jwt: string; } @@ -1003,25 +1066,27 @@ export const PublicKeysResponse: MessageFns = { }, }; -function createBaseJwtRequest(): JwtRequest { - return { token: "", scopes: [] }; +function createBaseCreateRunnerJwtRequest(): CreateRunnerJwtRequest { + return { runnerId: "", scopes: [] }; } -export const JwtRequest: MessageFns = { - encode(message: JwtRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { - if (message.token !== "") { - writer.uint32(10).string(message.token); +export const CreateRunnerJwtRequest: MessageFns = { + encode(message: CreateRunnerJwtRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.runnerId !== "") { + writer.uint32(10).string(message.runnerId); } + writer.uint32(18).fork(); for (const v of message.scopes) { - writer.uint32(18).string(v!); + writer.int32(createRunnerJwtRequest_ScopeToNumber(v)); } + writer.join(); return writer; }, - decode(input: BinaryReader | Uint8Array, length?: number): JwtRequest { + decode(input: BinaryReader | Uint8Array, length?: number): CreateRunnerJwtRequest { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === undefined ? reader.len : reader.pos + length; - const message = createBaseJwtRequest(); + const message = createBaseCreateRunnerJwtRequest(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { @@ -1030,16 +1095,26 @@ export const JwtRequest: MessageFns = { break; } - message.token = reader.string(); + message.runnerId = reader.string(); continue; } case 2: { - if (tag !== 18) { - break; + if (tag === 16) { + message.scopes.push(createRunnerJwtRequest_ScopeFromJSON(reader.int32())); + + continue; } - message.scopes.push(reader.string()); - continue; + if (tag === 18) { + const end2 = reader.uint32() + reader.pos; + while (reader.pos < end2) { + message.scopes.push(createRunnerJwtRequest_ScopeFromJSON(reader.int32())); + } + + continue; + } + + break; } } if ((tag & 7) === 4 || tag === 0) { @@ -1050,51 +1125,53 @@ export const JwtRequest: MessageFns = { return message; }, - fromJSON(object: any): JwtRequest { + fromJSON(object: any): CreateRunnerJwtRequest { return { - token: isSet(object.token) ? globalThis.String(object.token) : "", - scopes: globalThis.Array.isArray(object?.scopes) ? object.scopes.map((e: any) => globalThis.String(e)) : [], + runnerId: isSet(object.runnerId) ? globalThis.String(object.runnerId) : "", + scopes: globalThis.Array.isArray(object?.scopes) + ? object.scopes.map((e: any) => createRunnerJwtRequest_ScopeFromJSON(e)) + : [], }; }, - toJSON(message: JwtRequest): unknown { + toJSON(message: CreateRunnerJwtRequest): unknown { const obj: any = {}; - if (message.token !== "") { - obj.token = message.token; + if (message.runnerId !== "") { + obj.runnerId = message.runnerId; } if (message.scopes?.length) { - obj.scopes = message.scopes; + obj.scopes = message.scopes.map((e) => createRunnerJwtRequest_ScopeToJSON(e)); } return obj; }, - create(base?: DeepPartial): JwtRequest { - return JwtRequest.fromPartial(base ?? {}); + create(base?: DeepPartial): CreateRunnerJwtRequest { + return CreateRunnerJwtRequest.fromPartial(base ?? {}); }, - fromPartial(object: DeepPartial): JwtRequest { - const message = createBaseJwtRequest(); - message.token = object.token ?? ""; + fromPartial(object: DeepPartial): CreateRunnerJwtRequest { + const message = createBaseCreateRunnerJwtRequest(); + message.runnerId = object.runnerId ?? ""; message.scopes = object.scopes?.map((e) => e) || []; return message; }, }; -function createBaseJwtResponse(): JwtResponse { +function createBaseCreateRunnerJwtResponse(): CreateRunnerJwtResponse { return { jwt: "" }; } -export const JwtResponse: MessageFns = { - encode(message: JwtResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { +export const CreateRunnerJwtResponse: MessageFns = { + encode(message: CreateRunnerJwtResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.jwt !== "") { writer.uint32(10).string(message.jwt); } return writer; }, - decode(input: BinaryReader | Uint8Array, length?: number): JwtResponse { + decode(input: BinaryReader | Uint8Array, length?: number): CreateRunnerJwtResponse { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === undefined ? reader.len : reader.pos + length; - const message = createBaseJwtResponse(); + const message = createBaseCreateRunnerJwtResponse(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { @@ -1115,11 +1192,11 @@ export const JwtResponse: MessageFns = { return message; }, - fromJSON(object: any): JwtResponse { + fromJSON(object: any): CreateRunnerJwtResponse { return { jwt: isSet(object.jwt) ? globalThis.String(object.jwt) : "" }; }, - toJSON(message: JwtResponse): unknown { + toJSON(message: CreateRunnerJwtResponse): unknown { const obj: any = {}; if (message.jwt !== "") { obj.jwt = message.jwt; @@ -1127,11 +1204,11 @@ export const JwtResponse: MessageFns = { return obj; }, - create(base?: DeepPartial): JwtResponse { - return JwtResponse.fromPartial(base ?? {}); + create(base?: DeepPartial): CreateRunnerJwtResponse { + return CreateRunnerJwtResponse.fromPartial(base ?? {}); }, - fromPartial(object: DeepPartial): JwtResponse { - const message = createBaseJwtResponse(); + fromPartial(object: DeepPartial): CreateRunnerJwtResponse { + const message = createBaseCreateRunnerJwtResponse(); message.jwt = object.jwt ?? ""; return message; }, @@ -3032,11 +3109,11 @@ export const GeneralManagementDefinition = { responseStream: false, options: {}, }, - createJwt: { - name: "createJwt", - requestType: JwtRequest, + createRunnerJwt: { + name: "createRunnerJwt", + requestType: CreateRunnerJwtRequest, requestStream: false, - responseType: JwtResponse, + responseType: CreateRunnerJwtResponse, responseStream: false, options: {}, }, @@ -3083,7 +3160,10 @@ export interface GeneralManagementServiceImplementation { ): ServerStreamingMethodResult>; /** Authentication related */ getPublicKeys(request: Empty, context: CallContext & CallContextExt): Promise>; - createJwt(request: JwtRequest, context: CallContext & CallContextExt): Promise>; + createRunnerJwt( + request: CreateRunnerJwtRequest, + context: CallContext & CallContextExt, + ): Promise>; /** Gateway Specific */ createRunner(request: CreateRunnerRequest, context: CallContext & CallContextExt): Promise>; getGatewayConfig( @@ -3115,7 +3195,10 @@ export interface GeneralManagementClient { ): AsyncIterable; /** Authentication related */ getPublicKeys(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; - createJwt(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + createRunnerJwt( + request: DeepPartial, + options?: CallOptions & CallOptionsExt, + ): Promise; /** Gateway Specific */ createRunner(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; getGatewayConfig( diff --git a/packages/runner-node-entrypoint/src/index.ts b/packages/runner-node-entrypoint/src/index.ts index 3ceee2b..a07ed31 100644 --- a/packages/runner-node-entrypoint/src/index.ts +++ b/packages/runner-node-entrypoint/src/index.ts @@ -25,7 +25,7 @@ const main = async () => { jobControllerHost, jobControllerPort, jobRunnerIdentifier, - jobDebug + jobDebug, ); await jobber.connect(); diff --git a/packages/server/package.json b/packages/server/package.json index 6c5bde2..d524c9e 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -16,26 +16,27 @@ "author": "Eithan Hersey-Tuit", "license": "MIT", "dependencies": { - "@jobber/tcp-frame-socket": "workspace:*", + "@grpc/grpc-js": "^1.14.3", + "@grpc/proto-loader": "^0.8.0", + "@hono/node-server": "^1.13.7", "@jobber/common": "workspace:*", "@jobber/grpc": "workspace:*", - "@hono/node-server": "^1.13.7", + "@jobber/tcp-frame-socket": "workspace:*", "bcryptjs": "^3.0.2", "cron": "^3.2.1", "drizzle-orm": "^0.38.2", "hono": "^4.6.11", + "jose": "^6.1.3", + "long": "^5.3.2", "mqtt": "^5.10.3", + "nice-grpc": "^2.1.14", "pg": "^8.13.1", "prom-client": "^15.1.3", + "protobufjs": "^8.0.0", "reflect-metadata": "^0.2.2", "semver": "^7.6.3", "tsyringe": "^4.10.0", - "zod": "^3.23.8", - "@grpc/grpc-js": "^1.14.3", - "@grpc/proto-loader": "^0.8.0", - "long": "^5.3.2", - "nice-grpc": "^2.1.14", - "protobufjs": "^8.0.0" + "zod": "^3.23.8" }, "devDependencies": { "@tsconfig/node20": "^20.1.4", @@ -43,11 +44,11 @@ "@types/pg": "^8.11.10", "@types/semver": "^7.5.8", "drizzle-kit": "^0.30.1", + "grpc-tools": "^1.13.1", "rimraf": "^5.0.10", + "ts-proto": "^2.10.1", "tsc-alias": "^1.8.10", "typescript": "^5.6.3", - "vitest": "^3.2.4", - "grpc-tools": "^1.13.1", - "ts-proto": "^2.10.1" + "vitest": "^3.2.4" } } diff --git a/packages/server/src/bouncer.ts b/packages/server/src/bouncer.ts index 3b9f790..2a308ec 100644 --- a/packages/server/src/bouncer.ts +++ b/packages/server/src/bouncer.ts @@ -218,6 +218,10 @@ export class Bouncer { return this.can(`users/${user.id}/permissions`, "write"); } + public canWriteGrpcRunnerJwt(): boolean { + return this.can(`grpc/runner-jwt`, "write"); + } + public get type() { return this.options.type; } diff --git a/packages/server/src/config.ts b/packages/server/src/config.ts index ec95b89..7ac6303 100644 --- a/packages/server/src/config.ts +++ b/packages/server/src/config.ts @@ -1,5 +1,6 @@ import { hostname } from "os"; import { z } from "zod"; +import { apiTokensTable } from "./db/schema/api-tokens.js"; export const ConfigurationOptionsSchema = z.object({ DATABASE_URL: z.string(), @@ -12,6 +13,14 @@ export const ConfigurationOptionsSchema = z.object({ STARTUP_USERNAME: z.string().optional().default("admin"), STARTUP_PASSWORD: z.string().optional().default("Password1!"), + // API_TOKEN_INTERNAL: z + // .string() + // .length(apiTokensTable.token._.length) + // .optional(), + // API_TOKEN_INTERNAL_FLAG: z + // .enum(["gateway-permissions", "none"]) + // .default("none"), + AUTH_PUBLIC_REGISTRATION_ENABLED: z .string() .transform((val) => val.toLowerCase() === "true") diff --git a/packages/server/src/db/api-tokens.ts b/packages/server/src/db/api-tokens.ts new file mode 100644 index 0000000..af6fe0e --- /dev/null +++ b/packages/server/src/db/api-tokens.ts @@ -0,0 +1,22 @@ +import { and, eq, lte } from "drizzle-orm"; +import { getDrizzle } from "./index.js"; +import { apiTokensTable } from "./schema/api-tokens.js"; + +async function byValidToken(token: string) { + return await getDrizzle() + .select() + .from(apiTokensTable) + .where( + and( + eq(apiTokensTable.token, token), + eq(apiTokensTable.status, "enabled"), + lte(apiTokensTable.expires, new Date()) + ) + ) + .limit(1) + .then((res) => res.at(0)); +} + +export const apiTokensModel = { + byValidToken, +}; diff --git a/packages/server/src/db/schema/jwt-keys.ts b/packages/server/src/db/schema/jwt-keys.ts new file mode 100644 index 0000000..3919c0b --- /dev/null +++ b/packages/server/src/db/schema/jwt-keys.ts @@ -0,0 +1,21 @@ +import { pgTable, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core"; + +export const jwtKeysTable = pgTable("jwtKey", { + id: uuid("id").primaryKey().defaultRandom().notNull(), + + privateKey: text().notNull(), + publicKey: text().notNull(), + + status: varchar({ + length: 16, + enum: ["enabled", "disabled"], + }) + .notNull() + .default("enabled"), + + expires: timestamp().notNull(), + created: timestamp().defaultNow().notNull(), +}); + +export type JwtKeysTableType = typeof jwtKeysTable.$inferSelect; +export type JwtKeysTableInsertType = typeof jwtKeysTable.$inferInsert; diff --git a/packages/server/src/db/user.ts b/packages/server/src/db/user.ts new file mode 100644 index 0000000..08563d3 --- /dev/null +++ b/packages/server/src/db/user.ts @@ -0,0 +1,30 @@ +import { eq } from "drizzle-orm"; +import { getDrizzle } from "./index.js"; +import { usersTable } from "./schema/users.js"; + +async function byId(id: string) { + const user = await getDrizzle() + .select() + .from(usersTable) + .where(eq(usersTable.id, id)) + .limit(1) + .then((res) => res.at(0)); + + return user; +} + +async function byUsername(username: string) { + const user = await getDrizzle() + .select() + .from(usersTable) + .where(eq(usersTable.username, username)) + .limit(1) + .then((res) => res.at(0)); + + return user; +} + +export const userModel = { + byId, + byUsername, +}; diff --git a/packages/server/src/grpc/index.ts b/packages/server/src/grpc/index.ts index 6dcda0e..7d7c9ce 100644 --- a/packages/server/src/grpc/index.ts +++ b/packages/server/src/grpc/index.ts @@ -1,14 +1,94 @@ import { LoopBase } from "@jobber/common"; import { GeneralManagementDefinition } from "@jobber/grpc/server.js"; -import { createServer, Server, ServiceImplementation } from "nice-grpc"; -import { singleton } from "tsyringe"; +import { + CallContext, + createServer, + Server, + ServiceImplementation, + ServerError, + Status, +} from "nice-grpc"; +import { container, singleton } from "tsyringe"; +import { Bouncer } from "~/bouncer.js"; import { getConfigOption } from "~/config.js"; +import { apiTokensModel } from "~/db/api-tokens.js"; + +import { SignJWT } from "jose"; +import { RunnerManager } from "~/jobber/runners/manager.js"; + +const authorizedCall = ( + callback: ( + request: TRequest, + context: CallContext, + bouncer: Bouncer + ) => Promise +) => { + return async ( + request: TRequest, + context: CallContext + ): Promise => { + try { + const token = context.metadata.get("authorization"); + + if (!token) { + throw new ServerError(Status.UNAUTHENTICATED, "Unauthenticated"); + } + + const apiToken = await apiTokensModel.byValidToken(token); + + if (!apiToken) { + throw new ServerError(Status.UNAUTHENTICATED, "Unauthenticated"); + } + + const bouncer = new Bouncer({ + type: "token", + token: apiToken, + permissions: apiToken.permissions, + }); + + return callback(request, context, bouncer); + } catch (err) { + if (err instanceof ServerError) { + throw err; + } + + console.log("gRPC Internal server error:", err); + throw new ServerError(Status.INTERNAL, "Internal server error"); + } + }; +}; const generalManagementDefinition: ServiceImplementation = { - createJwt(request, context) { - throw new Error("Method not implemented."); - }, + createRunnerJwt: authorizedCall(async (request, context, bouncer) => { + if (!bouncer.canWriteGrpcRunnerJwt()) { + throw new ServerError( + Status.PERMISSION_DENIED, + "Insufficient Permissions" + ); + } + + // ensure runner exists + const manager = container.resolve(RunnerManager); + + const runner = manager.fundRunnerById(request.runnerId); + + if (!runner) { + throw new ServerError(Status.NOT_FOUND, "Runner not found"); + } + + const jwt = await new SignJWT() + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("1h") + .setIssuer(getConfigOption("JOBBER_NAME")) + .setAudience(`runner:${request.runnerId}`) + .sign(Uint8Array.from("")); + + return { + jwt, + }; + }), getPublicKeys(request, context) { throw new Error("Method not implemented."); @@ -34,8 +114,12 @@ const generalManagementDefinition: ServiceImplementation Promise ) { diff --git a/packages/server/src/middleware/response-time.ts b/packages/server/src/middleware/response-time.ts index 7079334..d66d628 100644 --- a/packages/server/src/middleware/response-time.ts +++ b/packages/server/src/middleware/response-time.ts @@ -1,5 +1,5 @@ import { Context, Next } from "hono"; -import { timeout } from "~/util.js"; +import { timeout } from "@jobber/common"; export function createMiddlewareResponseTime(duration: number) { return async (c: Context, next: Next) => { diff --git a/packages/server/src/routes/api-tokens.ts b/packages/server/src/routes/api-tokens.ts index 7fa2f7f..91b0eb7 100644 --- a/packages/server/src/routes/api-tokens.ts +++ b/packages/server/src/routes/api-tokens.ts @@ -7,7 +7,7 @@ import { apiTokensTable, ApiTokensTableType } from "~/db/schema/api-tokens.js"; import { InternalHonoApp } from "~/index.js"; import { withLock } from "~/lock.js"; import { createMiddlewareAuth } from "~/middleware/auth.js"; -import { canPerformAction, JobberPermissionsSchema } from "~/permissions.js"; +import { JobberPermissionsSchema } from "~/permissions.js"; export async function createRouteApiTokens() { const app = new Hono(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 341b7cc..4ee1876 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -210,6 +210,9 @@ importers: hono: specifier: ^4.6.11 version: 4.6.13 + jose: + specifier: ^6.1.3 + version: 6.1.3 long: specifier: ^5.3.2 version: 5.3.2 @@ -1955,6 +1958,9 @@ packages: resolution: {integrity: sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g==} hasBin: true + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -4279,6 +4285,8 @@ snapshots: jiti@2.4.1: optional: true + jose@6.1.3: {} + joycon@3.1.1: {} js-sdsl@4.3.0: {} From 00ee46b7dae50710db814ff65ff997acafcf1245 Mon Sep 17 00:00:00 2001 From: Eithan Date: Sat, 7 Feb 2026 21:39:05 +1100 Subject: [PATCH 03/25] chore: add OAuth2 M2M support for services (gateway, runners), and a lot of other setup --- .vscode/settings.json | 7 + examples/http-typescript/publish.sh | 1 + packages/common/src/loop-base.ts | 9 +- packages/common/src/timeout.ts | 24 +- packages/common/tsconfig.json | 7 +- packages/gateway/tsconfig.json | 4 +- packages/grpc/package.json | 2 +- packages/grpc/proto/base.proto | 11 + packages/grpc/proto/basics/action.proto | 49 + packages/grpc/proto/basics/api-token.proto | 23 + packages/grpc/proto/basics/common.proto | 20 + packages/grpc/proto/basics/environment.proto | 24 + packages/grpc/proto/basics/job-version.proto | 13 + packages/grpc/proto/basics/job.proto | 22 + packages/grpc/proto/basics/jwt-key.proto | 19 + packages/grpc/proto/basics/runner.proto | 24 + packages/grpc/proto/basics/trigger.proto | 56 + packages/grpc/proto/general.proto | 124 ++ ...erver.proto => toremove-general-api.proto} | 22 +- .../{runner.proto => toremove-runner.proto} | 0 packages/grpc/src/base.ts | 211 +++ packages/grpc/src/basics/action.ts | 739 ++++++++ packages/grpc/src/basics/api-token.ts | 275 +++ packages/grpc/src/basics/common.ts | 254 +++ packages/grpc/src/basics/environment.ts | 388 ++++ packages/grpc/src/basics/job-version.ts | 163 ++ packages/grpc/src/basics/job.ts | 310 ++++ packages/grpc/src/basics/jwt-key.ts | 229 +++ packages/grpc/src/basics/jwt-keys.ts | 229 +++ packages/grpc/src/basics/runner.ts | 294 +++ packages/grpc/src/basics/trigger.ts | 763 ++++++++ packages/grpc/src/general.ts | 1574 +++++++++++++++++ .../{server.ts => toremove-general-api.ts} | 159 +- .../src/{runner.ts => toremove-runner.ts} | 2 +- packages/grpc/tsconfig.json | 7 +- packages/runner-node-entrypoint/tsconfig.json | 5 +- .../server/drizzle/0010_milky_microbe.sql | 40 + .../server/drizzle/meta/0010_snapshot.json | 1114 ++++++++++++ packages/server/drizzle/meta/_journal.json | 7 + packages/server/src/bouncer.ts | 46 +- packages/server/src/config.ts | 25 +- packages/server/src/db/actions.ts | 24 + packages/server/src/db/audit-log.ts | 72 + packages/server/src/db/job-versions.ts | 32 + packages/server/src/db/job.ts | 6 + .../server/src/db/oauth-service-client.ts | 52 + packages/server/src/db/oauth-signing-key.ts | 133 ++ packages/server/src/db/schema/audit-log.ts | 62 + packages/server/src/db/schema/jwt-keys.ts | 21 - .../src/db/schema/oauth-service-client.ts | 49 + .../server/src/db/schema/oauth-signing-key.ts | 50 + packages/server/src/db/triggers.ts | 34 + packages/server/src/grpc/index.ts | 391 +++- packages/server/src/index.ts | 79 +- packages/server/src/jobber/store.ts | 8 +- packages/server/src/rate-limit.ts | 71 + packages/server/src/routes/auth.ts | 16 +- packages/server/src/routes/oauth-admin.ts | 239 +++ packages/server/src/routes/oauth.ts | 222 +++ packages/server/src/service-clients.ts | 63 + packages/server/src/signing-keys.ts | 137 ++ packages/server/tests/permissions.test.ts | 2 +- packages/server/tsconfig.json | 3 +- packages/tcp-frame-socket/tsconfig.json | 7 +- packages/web/vite.config.ts | 12 + pnpm-lock.yaml | 32 +- 66 files changed, 8784 insertions(+), 328 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 packages/grpc/proto/base.proto create mode 100644 packages/grpc/proto/basics/action.proto create mode 100644 packages/grpc/proto/basics/api-token.proto create mode 100644 packages/grpc/proto/basics/common.proto create mode 100644 packages/grpc/proto/basics/environment.proto create mode 100644 packages/grpc/proto/basics/job-version.proto create mode 100644 packages/grpc/proto/basics/job.proto create mode 100644 packages/grpc/proto/basics/jwt-key.proto create mode 100644 packages/grpc/proto/basics/runner.proto create mode 100644 packages/grpc/proto/basics/trigger.proto create mode 100644 packages/grpc/proto/general.proto rename packages/grpc/proto/{server.proto => toremove-general-api.proto} (91%) rename packages/grpc/proto/{runner.proto => toremove-runner.proto} (100%) create mode 100644 packages/grpc/src/base.ts create mode 100644 packages/grpc/src/basics/action.ts create mode 100644 packages/grpc/src/basics/api-token.ts create mode 100644 packages/grpc/src/basics/common.ts create mode 100644 packages/grpc/src/basics/environment.ts create mode 100644 packages/grpc/src/basics/job-version.ts create mode 100644 packages/grpc/src/basics/job.ts create mode 100644 packages/grpc/src/basics/jwt-key.ts create mode 100644 packages/grpc/src/basics/jwt-keys.ts create mode 100644 packages/grpc/src/basics/runner.ts create mode 100644 packages/grpc/src/basics/trigger.ts create mode 100644 packages/grpc/src/general.ts rename packages/grpc/src/{server.ts => toremove-general-api.ts} (95%) rename packages/grpc/src/{runner.ts => toremove-runner.ts} (99%) create mode 100644 packages/server/drizzle/0010_milky_microbe.sql create mode 100644 packages/server/drizzle/meta/0010_snapshot.json create mode 100644 packages/server/src/db/actions.ts create mode 100644 packages/server/src/db/audit-log.ts create mode 100644 packages/server/src/db/job-versions.ts create mode 100644 packages/server/src/db/oauth-service-client.ts create mode 100644 packages/server/src/db/oauth-signing-key.ts create mode 100644 packages/server/src/db/schema/audit-log.ts delete mode 100644 packages/server/src/db/schema/jwt-keys.ts create mode 100644 packages/server/src/db/schema/oauth-service-client.ts create mode 100644 packages/server/src/db/schema/oauth-signing-key.ts create mode 100644 packages/server/src/db/triggers.ts create mode 100644 packages/server/src/rate-limit.ts create mode 100644 packages/server/src/routes/oauth-admin.ts create mode 100644 packages/server/src/routes/oauth.ts create mode 100644 packages/server/src/service-clients.ts create mode 100644 packages/server/src/signing-keys.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4ba1992 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "protoc": { + "options": [ + "--proto_path=${workspaceRoot}/packages/grpc/proto" + ] + } +} \ No newline at end of file diff --git a/examples/http-typescript/publish.sh b/examples/http-typescript/publish.sh index 722a8a2..f62dc63 100755 --- a/examples/http-typescript/publish.sh +++ b/examples/http-typescript/publish.sh @@ -25,6 +25,7 @@ curl \ --request POST \ --url 'http://localhost:3000/api/job/publish/' \ --header 'content-type: multipart/form-data' \ + --header 'Authorization: Bearer eab549247662024701c92a1cdd4af07c45d8ebcd5acf73be0f7a242926a03e266ff174' \ --form 'archive=@archive.zip;type=application/zip' rm archive.zip \ No newline at end of file diff --git a/packages/common/src/loop-base.ts b/packages/common/src/loop-base.ts index 2ae6a3b..878225d 100644 --- a/packages/common/src/loop-base.ts +++ b/packages/common/src/loop-base.ts @@ -23,6 +23,8 @@ type EventEmitterEvents = { export abstract class LoopBase { protected status: StatusLifecycle = "neutral"; + private signal: AbortController | null = null; + protected abstract loopDuration: number; private events = new EventEmitter(); @@ -31,6 +33,8 @@ export abstract class LoopBase { return new Promise(async (resolve) => { assert(this.status === "neutral"); + this.signal = new AbortController(); + this.events.once("started", () => { resolve(); }); @@ -57,6 +61,8 @@ export abstract class LoopBase { this.status = "stopping"; + this.signal?.abort(); + if (this.loopClosing) { await this.loopClosing(); } @@ -81,10 +87,11 @@ export abstract class LoopBase { console.error(err); } - await timeout(this.loopDuration); + await timeout(this.loopDuration, this.signal?.signal); } this.status = "neutral"; + this.signal = null; if (this.loopClosed) { await this.loopClosed(); diff --git a/packages/common/src/timeout.ts b/packages/common/src/timeout.ts index ddc8ab4..8afd2de 100644 --- a/packages/common/src/timeout.ts +++ b/packages/common/src/timeout.ts @@ -3,5 +3,25 @@ * @param ms Time to wait in milliseconds * @returns */ -export const timeout = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); +export const timeout = (ms: number, signal?: AbortSignal) => { + return new Promise((resolve, reject) => { + if (signal?.aborted) { + return resolve(); + } + + const resolver = () => { + clearTimeout(timeoutId); + signal?.removeEventListener("abort", resolver); + + resolve(); + }; + + const timeoutId = setTimeout(() => { + resolver(); + }, ms); + + signal?.addEventListener("abort", () => { + resolver(); + }); + }); +}; diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json index 0ac97f1..cd126f1 100644 --- a/packages/common/tsconfig.json +++ b/packages/common/tsconfig.json @@ -11,11 +11,12 @@ "forceConsistentCasingInFileNames": true, "declaration": true, "types": ["node"], + "rootDir": "./src", "outDir": "./dist", + "paths":{ + "~/*": ["./src/*"] + } }, - "include": [ - "./src" - ], "$schema": "https://json.schemastore.org/tsconfig", "display": "Recommended" } \ No newline at end of file diff --git a/packages/gateway/tsconfig.json b/packages/gateway/tsconfig.json index 0b57a00..6ffe955 100644 --- a/packages/gateway/tsconfig.json +++ b/packages/gateway/tsconfig.json @@ -11,14 +11,12 @@ "forceConsistentCasingInFileNames": true, "declaration": false, "types": ["node"], + "rootDir": "./src", "outDir": "./dist", "paths":{ "~/*": ["./src/*"] } }, - "include": [ - "./src" - ], "$schema": "https://json.schemastore.org/tsconfig", "display": "Recommended" } \ No newline at end of file diff --git a/packages/grpc/package.json b/packages/grpc/package.json index c69e9c8..dae4079 100644 --- a/packages/grpc/package.json +++ b/packages/grpc/package.json @@ -10,7 +10,7 @@ }, "type": "module", "scripts": { - "grpc": "rm -r ./dist && protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src --ts_proto_opt=outputServices=nice-grpc,outputServices=generic-definitions,useExactTypes=false,stringEnums=true,esModuleInterop=true,enumsAsLiterals=true,outputDefaultValues=true --proto_path=./proto ./proto/*", + "grpc": "rm -rf dist 2>/dev/null || : && protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt=importSuffix=.js --ts_proto_out=./src --ts_proto_opt=outputServices=nice-grpc,outputServices=generic-definitions,useExactTypes=false,stringEnums=true,esModuleInterop=true,enumsAsLiterals=true,outputDefaultValues=true --proto_path=./proto/ ./proto/*.proto ./proto/**/*.proto", "build": "pnpm grpc && tsc" }, "keywords": [], diff --git a/packages/grpc/proto/base.proto b/packages/grpc/proto/base.proto new file mode 100644 index 0000000..afc4441 --- /dev/null +++ b/packages/grpc/proto/base.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package Base; + +message Empty {} + +message ExportChunk { + string id = 1; + int64 sequence = 2; + bytes data = 3; +} diff --git a/packages/grpc/proto/basics/action.proto b/packages/grpc/proto/basics/action.proto new file mode 100644 index 0000000..105a1f5 --- /dev/null +++ b/packages/grpc/proto/basics/action.proto @@ -0,0 +1,49 @@ +syntax = "proto3"; + +package Action; + +message Item { + message DockerArguments { + message Volume { + enum VolumeMode { + READ_ONLY = 0; + READ_WRITE = 1; + } + + string source = 1; + string target = 2; + VolumeMode mode = 3; + } + + message Label { + string key = 1; + string value = 2; + } + + repeated string networks = 1; + repeated Volume volumes = 2; + repeated Label labels = 3; + optional string memoryLimit = 4; + repeated string directPassthroughArguments = 5; + } + + enum RunnerMode { + STANDARD = 0; + RUN_ONCE = 1; + } + + string id = 1; + string jobId = 2; + string versionId = 3; + + string runnerImage = 4; + bool runnerAsynchronous = 5; + uint32 runnerMinCount = 6; + uint32 runnerMaxCount = 7; + uint32 runnerTimeout = 8; + uint32 runnerMaxIdleAge = 9; + uint32 runnerMaxAge = 10; + uint32 runnerMaxAgeHard = 11; + DockerArguments dockerArguments = 12; + RunnerMode runnerMode = 13; +} diff --git a/packages/grpc/proto/basics/api-token.proto b/packages/grpc/proto/basics/api-token.proto new file mode 100644 index 0000000..ae3659b --- /dev/null +++ b/packages/grpc/proto/basics/api-token.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +import "basics/common.proto"; + +package ApiToken; + +message Item { + enum Status { + ENABLED = 0; + DISABLED = 1; + } + + string id = 1; + string token = 2; + string userId = 3; + string description = 4; + Status status = 5; + + repeated Common.Permission permissions = 6; + + string expires = 7; + string created = 8; +} diff --git a/packages/grpc/proto/basics/common.proto b/packages/grpc/proto/basics/common.proto new file mode 100644 index 0000000..53d691a --- /dev/null +++ b/packages/grpc/proto/basics/common.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package Common; + +message Permission { + enum Effect { + ALLOW = 0; + DENY = 1; + } + + enum Action { + READ = 0; + WRITE = 1; + DELETE = 2; + } + + Effect effect = 1; + string resource = 2; + repeated Action actions = 3; +} \ No newline at end of file diff --git a/packages/grpc/proto/basics/environment.proto b/packages/grpc/proto/basics/environment.proto new file mode 100644 index 0000000..73a0fbc --- /dev/null +++ b/packages/grpc/proto/basics/environment.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +import "basics/common.proto"; + +package Environment; + +message Item { + message ContextValue { + enum Type { + TEXT = 0; + SECRET = 1; + } + + Type type = 1; + optional string value = 2; + } + + string id = 1; + string jobId = 2; + + map context = 3; + + string modified = 4; +} diff --git a/packages/grpc/proto/basics/job-version.proto b/packages/grpc/proto/basics/job-version.proto new file mode 100644 index 0000000..26c74e3 --- /dev/null +++ b/packages/grpc/proto/basics/job-version.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +import "basics/common.proto"; + +package JobVersion; + +message Item { + string id = 1; + string jobId = 2; + string version = 3; + string modified = 4; + string created = 5; +} diff --git a/packages/grpc/proto/basics/job.proto b/packages/grpc/proto/basics/job.proto new file mode 100644 index 0000000..2ad3de3 --- /dev/null +++ b/packages/grpc/proto/basics/job.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package Job; + +message Item { + message Link { + string name = 1; + string url = 2; + } + + enum Status { + ENABLED = 0; + DISABLED = 1; + } + + string id = 1; + string jobName = 2; + Status status = 3; + optional string description = 4; + optional string versionId = 5; + repeated Link links = 6; +} diff --git a/packages/grpc/proto/basics/jwt-key.proto b/packages/grpc/proto/basics/jwt-key.proto new file mode 100644 index 0000000..d38cab2 --- /dev/null +++ b/packages/grpc/proto/basics/jwt-key.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +import "basics/common.proto"; + +package JwtKey; + +message Item { + enum Status { + ENABLED = 0; + DISABLED = 1; + } + + string id = 1; + optional string privateKey = 2; + string publicKey = 3; + Status status = 4; + string expires = 5; + string created = 6; +} diff --git a/packages/grpc/proto/basics/runner.proto b/packages/grpc/proto/basics/runner.proto new file mode 100644 index 0000000..aa9f63b --- /dev/null +++ b/packages/grpc/proto/basics/runner.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package Runner; + +message Item { + enum Status { + STARTING = 0; + READY = 1; + CLOSING = 2; + CLOSED = 3; + } + + string id = 1; + string jobId = 2; + string actionId = 3; + Status status = 4; + + reserved 5 to 10; + + string createdAt = 11; + optional string readyAt = 12; + optional string closingAt = 13; + optional string closedAt = 14; +} diff --git a/packages/grpc/proto/basics/trigger.proto b/packages/grpc/proto/basics/trigger.proto new file mode 100644 index 0000000..d22ba40 --- /dev/null +++ b/packages/grpc/proto/basics/trigger.proto @@ -0,0 +1,56 @@ +syntax = "proto3"; + +import "basics/common.proto"; + +package Trigger; + +message Item { + message TriggerSchedule { + optional string name = 1; + string cron = 2; + optional string timezone = 3; + } + + message TriggerHttp { + optional string name = 1; + optional string hostname = 2; + optional string method = 3; + optional string path = 4; + } + + message TriggerMqtt { + message Connection { + optional string protocol = 1; + optional string protocolVariable = 2; + + optional string port = 3; + optional string portVariable = 4; + + optional string host = 5; + optional string hostVariable = 6; + + optional string username = 7; + optional string usernameVariable = 8; + + optional string password = 9; + optional string passwordVariable = 10; + + optional string clientId = 11; + optional string clientIdVariable = 12; + } + + optional string name = 1; + repeated string topics = 2; + Connection connection = 3; + } + + string id = 1; + string jobId = 2; + string versionId = 3; + + oneof context { + TriggerSchedule schedule = 4; + TriggerHttp http = 5; + TriggerMqtt mqtt = 6; + } +} diff --git a/packages/grpc/proto/general.proto b/packages/grpc/proto/general.proto new file mode 100644 index 0000000..1ff8f23 --- /dev/null +++ b/packages/grpc/proto/general.proto @@ -0,0 +1,124 @@ +syntax = "proto3"; + +import "base.proto"; +import "basics/action.proto"; +import "basics/api-token.proto"; +import "basics/environment.proto"; +import "basics/job-version.proto"; +import "basics/job.proto"; +import "basics/jwt-key.proto"; +import "basics/runner.proto"; +import "basics/trigger.proto"; + +package GeneralAPI; + +service GeneralAPI { + rpc getJob(JobRequest) returns (JobResponse); + rpc getJobs(JobsRequest) returns (JobsResponse); + + rpc getJobVersion(JobVersionRequest) returns (JobVersionResponse); + rpc getJobVersions(JobVersionsRequest) returns (JobVersionsResponse); + + rpc getJobAction(JobActionRequest) returns (JobActionResponse); + rpc getJobActions(JobActionsRequest) returns (JobActionsResponse); + + rpc getJobTrigger(JobTriggerRequest) returns (JobTriggerResponse); + rpc getJobTriggers(JobTriggersRequest) returns (JobTriggersResponse); + + rpc getRunner(RunnerRequest) returns (RunnerResponse); + rpc getRunners(RunnersRequest) returns (RunnersResponse); +} + +/** getJob **/ +message JobRequest { + string jobId = 1; +} +message JobResponse { + Job.Item job = 1; +} + + +/** getJobs **/ +message JobsRequest {} +message JobsResponse { + repeated Job.Item jobs = 1; +} + + +/** getJobVersion **/ +message JobVersionRequest { + string jobVersionId = 1; +} +message JobVersionResponse { + JobVersion.Item jobVersion = 1; +} + + +/** getJobVersions **/ +message JobVersionsRequest { + string jobId = 1; +} +message JobVersionsResponse { + repeated JobVersion.Item jobVersions = 1; +} + + +/** getJobAction **/ +message JobActionRequest { + string jobId = 1; + string actionId = 2; +} +message JobActionResponse { + Action.Item action = 1; +} + + +/** getJobActions **/ +message JobActionsRequest { + string jobId = 1; + optional string versionId = 2; +} +message JobActionsResponse { + repeated Action.Item actions = 1; +} + + +/** getJobTrigger **/ +message JobTriggerRequest { + string jobId = 1; + string triggerId = 2; +} +message JobTriggerResponse { + Trigger.Item trigger = 1; +} + + +/** getJobTriggers **/ +message JobTriggersRequest { + string jobId = 1; + optional string versionId = 2; +} +message JobTriggersResponse { + repeated Trigger.Item triggers = 1; +} + + +/** getRunner **/ +message RunnerRequest { + string runnerId = 1; +} +message RunnerResponse { + Runner.Item runner = 1; +} + + +/** getRunners **/ +message RunnersRequest { + optional string jobId = 1; + optional string versionId = 2; + optional string actionId = 3; + optional Runner.Item.Status status = 4; +} +message RunnersResponse { + repeated Runner.Item runners = 1; +} \ No newline at end of file diff --git a/packages/grpc/proto/server.proto b/packages/grpc/proto/toremove-general-api.proto similarity index 91% rename from packages/grpc/proto/server.proto rename to packages/grpc/proto/toremove-general-api.proto index e4a52dc..dc5bed6 100644 --- a/packages/grpc/proto/server.proto +++ b/packages/grpc/proto/toremove-general-api.proto @@ -1,18 +1,12 @@ syntax = "proto3"; -package Server; - -message Empty {} +import "base.proto"; -message ExportChunk { - string id = 1; - int64 sequence = 2; - bytes data = 3; -} +package Server; -service GeneralManagement { +service GeneralAPI { // General - rpc getJobs (Empty) returns (stream JobItem); + rpc getJobs (Base.Empty) returns (stream JobItem); rpc getJob (GetJobRequest) returns (JobItem); rpc getAction (GetActionRequest) returns (ActionItem); @@ -24,13 +18,13 @@ service GeneralManagement { // Authentication related - rpc getPublicKeys(Empty) returns (PublicKeysResponse); + rpc getPublicKeys(Base.Empty) returns (PublicKeysResponse); rpc createRunnerJwt (CreateRunnerJwtRequest) returns (CreateRunnerJwtResponse); // Gateway Specific rpc createRunner(CreateRunnerRequest) returns (RunnerItem); - rpc getGatewayConfig(Empty) returns (GetGatewayConfigResponse); + rpc getGatewayConfig(Base.Empty) returns (GetGatewayConfigResponse); } message GetJobRequest { @@ -201,11 +195,11 @@ service RunnerManagement { rpc storeGet (StoreGetRequest) returns (StoreItem); rpc storeSet (StoreSetRequest) returns (StoreItem); rpc storeDelete (StoreDeleteRequest) returns (StoreItem); - rpc mqttPublish (MqttPublishRequest) returns (Empty); + rpc mqttPublish (MqttPublishRequest) returns (Base.Empty); // Backend management methods rpc init(InitRequest) returns (InitResponse); - rpc archive(Empty) returns (stream ExportChunk); + rpc archive(Base.Empty) returns (stream Base.ExportChunk); } message StoreGetRequest { diff --git a/packages/grpc/proto/runner.proto b/packages/grpc/proto/toremove-runner.proto similarity index 100% rename from packages/grpc/proto/runner.proto rename to packages/grpc/proto/toremove-runner.proto diff --git a/packages/grpc/src/base.ts b/packages/grpc/src/base.ts new file mode 100644 index 0000000..f8576ed --- /dev/null +++ b/packages/grpc/src/base.ts @@ -0,0 +1,211 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.10.1 +// protoc v3.21.12 +// source: base.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "Base"; + +export interface Empty { +} + +export interface ExportChunk { + id: string; + sequence: number; + data: Uint8Array; +} + +function createBaseEmpty(): Empty { + return {}; +} + +export const Empty: MessageFns = { + encode(_: Empty, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Empty { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseEmpty(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(_: any): Empty { + return {}; + }, + + toJSON(_: Empty): unknown { + const obj: any = {}; + return obj; + }, + + create(base?: DeepPartial): Empty { + return Empty.fromPartial(base ?? {}); + }, + fromPartial(_: DeepPartial): Empty { + const message = createBaseEmpty(); + return message; + }, +}; + +function createBaseExportChunk(): ExportChunk { + return { id: "", sequence: 0, data: new Uint8Array(0) }; +} + +export const ExportChunk: MessageFns = { + encode(message: ExportChunk, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.sequence !== 0) { + writer.uint32(16).int64(message.sequence); + } + if (message.data.length !== 0) { + writer.uint32(26).bytes(message.data); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ExportChunk { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseExportChunk(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.sequence = longToNumber(reader.int64()); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.data = reader.bytes(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ExportChunk { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + sequence: isSet(object.sequence) ? globalThis.Number(object.sequence) : 0, + data: isSet(object.data) ? bytesFromBase64(object.data) : new Uint8Array(0), + }; + }, + + toJSON(message: ExportChunk): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.sequence !== 0) { + obj.sequence = Math.round(message.sequence); + } + if (message.data.length !== 0) { + obj.data = base64FromBytes(message.data); + } + return obj; + }, + + create(base?: DeepPartial): ExportChunk { + return ExportChunk.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ExportChunk { + const message = createBaseExportChunk(); + message.id = object.id ?? ""; + message.sequence = object.sequence ?? 0; + message.data = object.data ?? new Uint8Array(0); + return message; + }, +}; + +function bytesFromBase64(b64: string): Uint8Array { + if ((globalThis as any).Buffer) { + return Uint8Array.from(globalThis.Buffer.from(b64, "base64")); + } else { + const bin = globalThis.atob(b64); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; ++i) { + arr[i] = bin.charCodeAt(i); + } + return arr; + } +} + +function base64FromBytes(arr: Uint8Array): string { + if ((globalThis as any).Buffer) { + return globalThis.Buffer.from(arr).toString("base64"); + } else { + const bin: string[] = []; + arr.forEach((byte) => { + bin.push(globalThis.String.fromCharCode(byte)); + }); + return globalThis.btoa(bin.join("")); + } +} + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +function longToNumber(int64: { toString(): string }): number { + const num = globalThis.Number(int64.toString()); + if (num > globalThis.Number.MAX_SAFE_INTEGER) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + if (num < globalThis.Number.MIN_SAFE_INTEGER) { + throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); + } + return num; +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create(base?: DeepPartial): T; + fromPartial(object: DeepPartial): T; +} diff --git a/packages/grpc/src/basics/action.ts b/packages/grpc/src/basics/action.ts new file mode 100644 index 0000000..ce6acea --- /dev/null +++ b/packages/grpc/src/basics/action.ts @@ -0,0 +1,739 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.10.1 +// protoc v3.21.12 +// source: basics/action.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "Action"; + +export interface Item { + id: string; + jobId: string; + versionId: string; + runnerImage: string; + runnerAsynchronous: boolean; + runnerMinCount: number; + runnerMaxCount: number; + runnerTimeout: number; + runnerMaxIdleAge: number; + runnerMaxAge: number; + runnerMaxAgeHard: number; + dockerArguments: Item_DockerArguments | undefined; + runnerMode: Item_RunnerMode; +} + +export const Item_RunnerMode = { STANDARD: "STANDARD", RUN_ONCE: "RUN_ONCE", UNRECOGNIZED: "UNRECOGNIZED" } as const; + +export type Item_RunnerMode = typeof Item_RunnerMode[keyof typeof Item_RunnerMode]; + +export namespace Item_RunnerMode { + export type STANDARD = typeof Item_RunnerMode.STANDARD; + export type RUN_ONCE = typeof Item_RunnerMode.RUN_ONCE; + export type UNRECOGNIZED = typeof Item_RunnerMode.UNRECOGNIZED; +} + +export function item_RunnerModeFromJSON(object: any): Item_RunnerMode { + switch (object) { + case 0: + case "STANDARD": + return Item_RunnerMode.STANDARD; + case 1: + case "RUN_ONCE": + return Item_RunnerMode.RUN_ONCE; + case -1: + case "UNRECOGNIZED": + default: + return Item_RunnerMode.UNRECOGNIZED; + } +} + +export function item_RunnerModeToJSON(object: Item_RunnerMode): string { + switch (object) { + case Item_RunnerMode.STANDARD: + return "STANDARD"; + case Item_RunnerMode.RUN_ONCE: + return "RUN_ONCE"; + case Item_RunnerMode.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function item_RunnerModeToNumber(object: Item_RunnerMode): number { + switch (object) { + case Item_RunnerMode.STANDARD: + return 0; + case Item_RunnerMode.RUN_ONCE: + return 1; + case Item_RunnerMode.UNRECOGNIZED: + default: + return -1; + } +} + +export interface Item_DockerArguments { + networks: string[]; + volumes: Item_DockerArguments_Volume[]; + labels: Item_DockerArguments_Label[]; + memoryLimit?: string | undefined; + directPassthroughArguments: string[]; +} + +export interface Item_DockerArguments_Volume { + source: string; + target: string; + mode: Item_DockerArguments_Volume_VolumeMode; +} + +export const Item_DockerArguments_Volume_VolumeMode = { + READ_ONLY: "READ_ONLY", + READ_WRITE: "READ_WRITE", + UNRECOGNIZED: "UNRECOGNIZED", +} as const; + +export type Item_DockerArguments_Volume_VolumeMode = + typeof Item_DockerArguments_Volume_VolumeMode[keyof typeof Item_DockerArguments_Volume_VolumeMode]; + +export namespace Item_DockerArguments_Volume_VolumeMode { + export type READ_ONLY = typeof Item_DockerArguments_Volume_VolumeMode.READ_ONLY; + export type READ_WRITE = typeof Item_DockerArguments_Volume_VolumeMode.READ_WRITE; + export type UNRECOGNIZED = typeof Item_DockerArguments_Volume_VolumeMode.UNRECOGNIZED; +} + +export function item_DockerArguments_Volume_VolumeModeFromJSON(object: any): Item_DockerArguments_Volume_VolumeMode { + switch (object) { + case 0: + case "READ_ONLY": + return Item_DockerArguments_Volume_VolumeMode.READ_ONLY; + case 1: + case "READ_WRITE": + return Item_DockerArguments_Volume_VolumeMode.READ_WRITE; + case -1: + case "UNRECOGNIZED": + default: + return Item_DockerArguments_Volume_VolumeMode.UNRECOGNIZED; + } +} + +export function item_DockerArguments_Volume_VolumeModeToJSON(object: Item_DockerArguments_Volume_VolumeMode): string { + switch (object) { + case Item_DockerArguments_Volume_VolumeMode.READ_ONLY: + return "READ_ONLY"; + case Item_DockerArguments_Volume_VolumeMode.READ_WRITE: + return "READ_WRITE"; + case Item_DockerArguments_Volume_VolumeMode.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function item_DockerArguments_Volume_VolumeModeToNumber(object: Item_DockerArguments_Volume_VolumeMode): number { + switch (object) { + case Item_DockerArguments_Volume_VolumeMode.READ_ONLY: + return 0; + case Item_DockerArguments_Volume_VolumeMode.READ_WRITE: + return 1; + case Item_DockerArguments_Volume_VolumeMode.UNRECOGNIZED: + default: + return -1; + } +} + +export interface Item_DockerArguments_Label { + key: string; + value: string; +} + +function createBaseItem(): Item { + return { + id: "", + jobId: "", + versionId: "", + runnerImage: "", + runnerAsynchronous: false, + runnerMinCount: 0, + runnerMaxCount: 0, + runnerTimeout: 0, + runnerMaxIdleAge: 0, + runnerMaxAge: 0, + runnerMaxAgeHard: 0, + dockerArguments: undefined, + runnerMode: Item_RunnerMode.STANDARD, + }; +} + +export const Item: MessageFns = { + encode(message: Item, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.jobId !== "") { + writer.uint32(18).string(message.jobId); + } + if (message.versionId !== "") { + writer.uint32(26).string(message.versionId); + } + if (message.runnerImage !== "") { + writer.uint32(34).string(message.runnerImage); + } + if (message.runnerAsynchronous !== false) { + writer.uint32(40).bool(message.runnerAsynchronous); + } + if (message.runnerMinCount !== 0) { + writer.uint32(48).uint32(message.runnerMinCount); + } + if (message.runnerMaxCount !== 0) { + writer.uint32(56).uint32(message.runnerMaxCount); + } + if (message.runnerTimeout !== 0) { + writer.uint32(64).uint32(message.runnerTimeout); + } + if (message.runnerMaxIdleAge !== 0) { + writer.uint32(72).uint32(message.runnerMaxIdleAge); + } + if (message.runnerMaxAge !== 0) { + writer.uint32(80).uint32(message.runnerMaxAge); + } + if (message.runnerMaxAgeHard !== 0) { + writer.uint32(88).uint32(message.runnerMaxAgeHard); + } + if (message.dockerArguments !== undefined) { + Item_DockerArguments.encode(message.dockerArguments, writer.uint32(98).fork()).join(); + } + if (message.runnerMode !== Item_RunnerMode.STANDARD) { + writer.uint32(104).int32(item_RunnerModeToNumber(message.runnerMode)); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.versionId = reader.string(); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.runnerImage = reader.string(); + continue; + } + case 5: { + if (tag !== 40) { + break; + } + + message.runnerAsynchronous = reader.bool(); + continue; + } + case 6: { + if (tag !== 48) { + break; + } + + message.runnerMinCount = reader.uint32(); + continue; + } + case 7: { + if (tag !== 56) { + break; + } + + message.runnerMaxCount = reader.uint32(); + continue; + } + case 8: { + if (tag !== 64) { + break; + } + + message.runnerTimeout = reader.uint32(); + continue; + } + case 9: { + if (tag !== 72) { + break; + } + + message.runnerMaxIdleAge = reader.uint32(); + continue; + } + case 10: { + if (tag !== 80) { + break; + } + + message.runnerMaxAge = reader.uint32(); + continue; + } + case 11: { + if (tag !== 88) { + break; + } + + message.runnerMaxAgeHard = reader.uint32(); + continue; + } + case 12: { + if (tag !== 98) { + break; + } + + message.dockerArguments = Item_DockerArguments.decode(reader, reader.uint32()); + continue; + } + case 13: { + if (tag !== 104) { + break; + } + + message.runnerMode = item_RunnerModeFromJSON(reader.int32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + versionId: isSet(object.versionId) ? globalThis.String(object.versionId) : "", + runnerImage: isSet(object.runnerImage) ? globalThis.String(object.runnerImage) : "", + runnerAsynchronous: isSet(object.runnerAsynchronous) ? globalThis.Boolean(object.runnerAsynchronous) : false, + runnerMinCount: isSet(object.runnerMinCount) ? globalThis.Number(object.runnerMinCount) : 0, + runnerMaxCount: isSet(object.runnerMaxCount) ? globalThis.Number(object.runnerMaxCount) : 0, + runnerTimeout: isSet(object.runnerTimeout) ? globalThis.Number(object.runnerTimeout) : 0, + runnerMaxIdleAge: isSet(object.runnerMaxIdleAge) ? globalThis.Number(object.runnerMaxIdleAge) : 0, + runnerMaxAge: isSet(object.runnerMaxAge) ? globalThis.Number(object.runnerMaxAge) : 0, + runnerMaxAgeHard: isSet(object.runnerMaxAgeHard) ? globalThis.Number(object.runnerMaxAgeHard) : 0, + dockerArguments: isSet(object.dockerArguments) + ? Item_DockerArguments.fromJSON(object.dockerArguments) + : undefined, + runnerMode: isSet(object.runnerMode) ? item_RunnerModeFromJSON(object.runnerMode) : Item_RunnerMode.STANDARD, + }; + }, + + toJSON(message: Item): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.versionId !== "") { + obj.versionId = message.versionId; + } + if (message.runnerImage !== "") { + obj.runnerImage = message.runnerImage; + } + if (message.runnerAsynchronous !== false) { + obj.runnerAsynchronous = message.runnerAsynchronous; + } + if (message.runnerMinCount !== 0) { + obj.runnerMinCount = Math.round(message.runnerMinCount); + } + if (message.runnerMaxCount !== 0) { + obj.runnerMaxCount = Math.round(message.runnerMaxCount); + } + if (message.runnerTimeout !== 0) { + obj.runnerTimeout = Math.round(message.runnerTimeout); + } + if (message.runnerMaxIdleAge !== 0) { + obj.runnerMaxIdleAge = Math.round(message.runnerMaxIdleAge); + } + if (message.runnerMaxAge !== 0) { + obj.runnerMaxAge = Math.round(message.runnerMaxAge); + } + if (message.runnerMaxAgeHard !== 0) { + obj.runnerMaxAgeHard = Math.round(message.runnerMaxAgeHard); + } + if (message.dockerArguments !== undefined) { + obj.dockerArguments = Item_DockerArguments.toJSON(message.dockerArguments); + } + if (message.runnerMode !== Item_RunnerMode.STANDARD) { + obj.runnerMode = item_RunnerModeToJSON(message.runnerMode); + } + return obj; + }, + + create(base?: DeepPartial): Item { + return Item.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item { + const message = createBaseItem(); + message.id = object.id ?? ""; + message.jobId = object.jobId ?? ""; + message.versionId = object.versionId ?? ""; + message.runnerImage = object.runnerImage ?? ""; + message.runnerAsynchronous = object.runnerAsynchronous ?? false; + message.runnerMinCount = object.runnerMinCount ?? 0; + message.runnerMaxCount = object.runnerMaxCount ?? 0; + message.runnerTimeout = object.runnerTimeout ?? 0; + message.runnerMaxIdleAge = object.runnerMaxIdleAge ?? 0; + message.runnerMaxAge = object.runnerMaxAge ?? 0; + message.runnerMaxAgeHard = object.runnerMaxAgeHard ?? 0; + message.dockerArguments = (object.dockerArguments !== undefined && object.dockerArguments !== null) + ? Item_DockerArguments.fromPartial(object.dockerArguments) + : undefined; + message.runnerMode = object.runnerMode ?? Item_RunnerMode.STANDARD; + return message; + }, +}; + +function createBaseItem_DockerArguments(): Item_DockerArguments { + return { networks: [], volumes: [], labels: [], memoryLimit: undefined, directPassthroughArguments: [] }; +} + +export const Item_DockerArguments: MessageFns = { + encode(message: Item_DockerArguments, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.networks) { + writer.uint32(10).string(v!); + } + for (const v of message.volumes) { + Item_DockerArguments_Volume.encode(v!, writer.uint32(18).fork()).join(); + } + for (const v of message.labels) { + Item_DockerArguments_Label.encode(v!, writer.uint32(26).fork()).join(); + } + if (message.memoryLimit !== undefined) { + writer.uint32(34).string(message.memoryLimit); + } + for (const v of message.directPassthroughArguments) { + writer.uint32(42).string(v!); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item_DockerArguments { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem_DockerArguments(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.networks.push(reader.string()); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.volumes.push(Item_DockerArguments_Volume.decode(reader, reader.uint32())); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.labels.push(Item_DockerArguments_Label.decode(reader, reader.uint32())); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.memoryLimit = reader.string(); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.directPassthroughArguments.push(reader.string()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item_DockerArguments { + return { + networks: globalThis.Array.isArray(object?.networks) ? object.networks.map((e: any) => globalThis.String(e)) : [], + volumes: globalThis.Array.isArray(object?.volumes) + ? object.volumes.map((e: any) => Item_DockerArguments_Volume.fromJSON(e)) + : [], + labels: globalThis.Array.isArray(object?.labels) + ? object.labels.map((e: any) => Item_DockerArguments_Label.fromJSON(e)) + : [], + memoryLimit: isSet(object.memoryLimit) ? globalThis.String(object.memoryLimit) : undefined, + directPassthroughArguments: globalThis.Array.isArray(object?.directPassthroughArguments) + ? object.directPassthroughArguments.map((e: any) => globalThis.String(e)) + : [], + }; + }, + + toJSON(message: Item_DockerArguments): unknown { + const obj: any = {}; + if (message.networks?.length) { + obj.networks = message.networks; + } + if (message.volumes?.length) { + obj.volumes = message.volumes.map((e) => Item_DockerArguments_Volume.toJSON(e)); + } + if (message.labels?.length) { + obj.labels = message.labels.map((e) => Item_DockerArguments_Label.toJSON(e)); + } + if (message.memoryLimit !== undefined) { + obj.memoryLimit = message.memoryLimit; + } + if (message.directPassthroughArguments?.length) { + obj.directPassthroughArguments = message.directPassthroughArguments; + } + return obj; + }, + + create(base?: DeepPartial): Item_DockerArguments { + return Item_DockerArguments.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item_DockerArguments { + const message = createBaseItem_DockerArguments(); + message.networks = object.networks?.map((e) => e) || []; + message.volumes = object.volumes?.map((e) => Item_DockerArguments_Volume.fromPartial(e)) || []; + message.labels = object.labels?.map((e) => Item_DockerArguments_Label.fromPartial(e)) || []; + message.memoryLimit = object.memoryLimit ?? undefined; + message.directPassthroughArguments = object.directPassthroughArguments?.map((e) => e) || []; + return message; + }, +}; + +function createBaseItem_DockerArguments_Volume(): Item_DockerArguments_Volume { + return { source: "", target: "", mode: Item_DockerArguments_Volume_VolumeMode.READ_ONLY }; +} + +export const Item_DockerArguments_Volume: MessageFns = { + encode(message: Item_DockerArguments_Volume, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.source !== "") { + writer.uint32(10).string(message.source); + } + if (message.target !== "") { + writer.uint32(18).string(message.target); + } + if (message.mode !== Item_DockerArguments_Volume_VolumeMode.READ_ONLY) { + writer.uint32(24).int32(item_DockerArguments_Volume_VolumeModeToNumber(message.mode)); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item_DockerArguments_Volume { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem_DockerArguments_Volume(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.source = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.target = reader.string(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.mode = item_DockerArguments_Volume_VolumeModeFromJSON(reader.int32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item_DockerArguments_Volume { + return { + source: isSet(object.source) ? globalThis.String(object.source) : "", + target: isSet(object.target) ? globalThis.String(object.target) : "", + mode: isSet(object.mode) + ? item_DockerArguments_Volume_VolumeModeFromJSON(object.mode) + : Item_DockerArguments_Volume_VolumeMode.READ_ONLY, + }; + }, + + toJSON(message: Item_DockerArguments_Volume): unknown { + const obj: any = {}; + if (message.source !== "") { + obj.source = message.source; + } + if (message.target !== "") { + obj.target = message.target; + } + if (message.mode !== Item_DockerArguments_Volume_VolumeMode.READ_ONLY) { + obj.mode = item_DockerArguments_Volume_VolumeModeToJSON(message.mode); + } + return obj; + }, + + create(base?: DeepPartial): Item_DockerArguments_Volume { + return Item_DockerArguments_Volume.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item_DockerArguments_Volume { + const message = createBaseItem_DockerArguments_Volume(); + message.source = object.source ?? ""; + message.target = object.target ?? ""; + message.mode = object.mode ?? Item_DockerArguments_Volume_VolumeMode.READ_ONLY; + return message; + }, +}; + +function createBaseItem_DockerArguments_Label(): Item_DockerArguments_Label { + return { key: "", value: "" }; +} + +export const Item_DockerArguments_Label: MessageFns = { + encode(message: Item_DockerArguments_Label, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.key !== "") { + writer.uint32(10).string(message.key); + } + if (message.value !== "") { + writer.uint32(18).string(message.value); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item_DockerArguments_Label { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem_DockerArguments_Label(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.key = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.value = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item_DockerArguments_Label { + return { + key: isSet(object.key) ? globalThis.String(object.key) : "", + value: isSet(object.value) ? globalThis.String(object.value) : "", + }; + }, + + toJSON(message: Item_DockerArguments_Label): unknown { + const obj: any = {}; + if (message.key !== "") { + obj.key = message.key; + } + if (message.value !== "") { + obj.value = message.value; + } + return obj; + }, + + create(base?: DeepPartial): Item_DockerArguments_Label { + return Item_DockerArguments_Label.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item_DockerArguments_Label { + const message = createBaseItem_DockerArguments_Label(); + message.key = object.key ?? ""; + message.value = object.value ?? ""; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create(base?: DeepPartial): T; + fromPartial(object: DeepPartial): T; +} diff --git a/packages/grpc/src/basics/api-token.ts b/packages/grpc/src/basics/api-token.ts new file mode 100644 index 0000000..f36c04f --- /dev/null +++ b/packages/grpc/src/basics/api-token.ts @@ -0,0 +1,275 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.10.1 +// protoc v3.21.12 +// source: basics/api-token.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; +import { Permission } from "./common.js"; + +export const protobufPackage = "ApiToken"; + +export interface Item { + id: string; + token: string; + userId: string; + description: string; + status: Item_Status; + permissions: Permission[]; + expires: string; + created: string; +} + +export const Item_Status = { ENABLED: "ENABLED", DISABLED: "DISABLED", UNRECOGNIZED: "UNRECOGNIZED" } as const; + +export type Item_Status = typeof Item_Status[keyof typeof Item_Status]; + +export namespace Item_Status { + export type ENABLED = typeof Item_Status.ENABLED; + export type DISABLED = typeof Item_Status.DISABLED; + export type UNRECOGNIZED = typeof Item_Status.UNRECOGNIZED; +} + +export function item_StatusFromJSON(object: any): Item_Status { + switch (object) { + case 0: + case "ENABLED": + return Item_Status.ENABLED; + case 1: + case "DISABLED": + return Item_Status.DISABLED; + case -1: + case "UNRECOGNIZED": + default: + return Item_Status.UNRECOGNIZED; + } +} + +export function item_StatusToJSON(object: Item_Status): string { + switch (object) { + case Item_Status.ENABLED: + return "ENABLED"; + case Item_Status.DISABLED: + return "DISABLED"; + case Item_Status.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function item_StatusToNumber(object: Item_Status): number { + switch (object) { + case Item_Status.ENABLED: + return 0; + case Item_Status.DISABLED: + return 1; + case Item_Status.UNRECOGNIZED: + default: + return -1; + } +} + +function createBaseItem(): Item { + return { + id: "", + token: "", + userId: "", + description: "", + status: Item_Status.ENABLED, + permissions: [], + expires: "", + created: "", + }; +} + +export const Item: MessageFns = { + encode(message: Item, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.token !== "") { + writer.uint32(18).string(message.token); + } + if (message.userId !== "") { + writer.uint32(26).string(message.userId); + } + if (message.description !== "") { + writer.uint32(34).string(message.description); + } + if (message.status !== Item_Status.ENABLED) { + writer.uint32(40).int32(item_StatusToNumber(message.status)); + } + for (const v of message.permissions) { + Permission.encode(v!, writer.uint32(50).fork()).join(); + } + if (message.expires !== "") { + writer.uint32(58).string(message.expires); + } + if (message.created !== "") { + writer.uint32(66).string(message.created); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.token = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.userId = reader.string(); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.description = reader.string(); + continue; + } + case 5: { + if (tag !== 40) { + break; + } + + message.status = item_StatusFromJSON(reader.int32()); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.permissions.push(Permission.decode(reader, reader.uint32())); + continue; + } + case 7: { + if (tag !== 58) { + break; + } + + message.expires = reader.string(); + continue; + } + case 8: { + if (tag !== 66) { + break; + } + + message.created = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + token: isSet(object.token) ? globalThis.String(object.token) : "", + userId: isSet(object.userId) ? globalThis.String(object.userId) : "", + description: isSet(object.description) ? globalThis.String(object.description) : "", + status: isSet(object.status) ? item_StatusFromJSON(object.status) : Item_Status.ENABLED, + permissions: globalThis.Array.isArray(object?.permissions) + ? object.permissions.map((e: any) => Permission.fromJSON(e)) + : [], + expires: isSet(object.expires) ? globalThis.String(object.expires) : "", + created: isSet(object.created) ? globalThis.String(object.created) : "", + }; + }, + + toJSON(message: Item): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.token !== "") { + obj.token = message.token; + } + if (message.userId !== "") { + obj.userId = message.userId; + } + if (message.description !== "") { + obj.description = message.description; + } + if (message.status !== Item_Status.ENABLED) { + obj.status = item_StatusToJSON(message.status); + } + if (message.permissions?.length) { + obj.permissions = message.permissions.map((e) => Permission.toJSON(e)); + } + if (message.expires !== "") { + obj.expires = message.expires; + } + if (message.created !== "") { + obj.created = message.created; + } + return obj; + }, + + create(base?: DeepPartial): Item { + return Item.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item { + const message = createBaseItem(); + message.id = object.id ?? ""; + message.token = object.token ?? ""; + message.userId = object.userId ?? ""; + message.description = object.description ?? ""; + message.status = object.status ?? Item_Status.ENABLED; + message.permissions = object.permissions?.map((e) => Permission.fromPartial(e)) || []; + message.expires = object.expires ?? ""; + message.created = object.created ?? ""; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create(base?: DeepPartial): T; + fromPartial(object: DeepPartial): T; +} diff --git a/packages/grpc/src/basics/common.ts b/packages/grpc/src/basics/common.ts new file mode 100644 index 0000000..bd77824 --- /dev/null +++ b/packages/grpc/src/basics/common.ts @@ -0,0 +1,254 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.10.1 +// protoc v3.21.12 +// source: basics/common.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "Common"; + +export interface Permission { + effect: Permission_Effect; + resource: string; + actions: Permission_Action[]; +} + +export const Permission_Effect = { ALLOW: "ALLOW", DENY: "DENY", UNRECOGNIZED: "UNRECOGNIZED" } as const; + +export type Permission_Effect = typeof Permission_Effect[keyof typeof Permission_Effect]; + +export namespace Permission_Effect { + export type ALLOW = typeof Permission_Effect.ALLOW; + export type DENY = typeof Permission_Effect.DENY; + export type UNRECOGNIZED = typeof Permission_Effect.UNRECOGNIZED; +} + +export function permission_EffectFromJSON(object: any): Permission_Effect { + switch (object) { + case 0: + case "ALLOW": + return Permission_Effect.ALLOW; + case 1: + case "DENY": + return Permission_Effect.DENY; + case -1: + case "UNRECOGNIZED": + default: + return Permission_Effect.UNRECOGNIZED; + } +} + +export function permission_EffectToJSON(object: Permission_Effect): string { + switch (object) { + case Permission_Effect.ALLOW: + return "ALLOW"; + case Permission_Effect.DENY: + return "DENY"; + case Permission_Effect.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function permission_EffectToNumber(object: Permission_Effect): number { + switch (object) { + case Permission_Effect.ALLOW: + return 0; + case Permission_Effect.DENY: + return 1; + case Permission_Effect.UNRECOGNIZED: + default: + return -1; + } +} + +export const Permission_Action = { + READ: "READ", + WRITE: "WRITE", + DELETE: "DELETE", + UNRECOGNIZED: "UNRECOGNIZED", +} as const; + +export type Permission_Action = typeof Permission_Action[keyof typeof Permission_Action]; + +export namespace Permission_Action { + export type READ = typeof Permission_Action.READ; + export type WRITE = typeof Permission_Action.WRITE; + export type DELETE = typeof Permission_Action.DELETE; + export type UNRECOGNIZED = typeof Permission_Action.UNRECOGNIZED; +} + +export function permission_ActionFromJSON(object: any): Permission_Action { + switch (object) { + case 0: + case "READ": + return Permission_Action.READ; + case 1: + case "WRITE": + return Permission_Action.WRITE; + case 2: + case "DELETE": + return Permission_Action.DELETE; + case -1: + case "UNRECOGNIZED": + default: + return Permission_Action.UNRECOGNIZED; + } +} + +export function permission_ActionToJSON(object: Permission_Action): string { + switch (object) { + case Permission_Action.READ: + return "READ"; + case Permission_Action.WRITE: + return "WRITE"; + case Permission_Action.DELETE: + return "DELETE"; + case Permission_Action.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function permission_ActionToNumber(object: Permission_Action): number { + switch (object) { + case Permission_Action.READ: + return 0; + case Permission_Action.WRITE: + return 1; + case Permission_Action.DELETE: + return 2; + case Permission_Action.UNRECOGNIZED: + default: + return -1; + } +} + +function createBasePermission(): Permission { + return { effect: Permission_Effect.ALLOW, resource: "", actions: [] }; +} + +export const Permission: MessageFns = { + encode(message: Permission, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.effect !== Permission_Effect.ALLOW) { + writer.uint32(8).int32(permission_EffectToNumber(message.effect)); + } + if (message.resource !== "") { + writer.uint32(18).string(message.resource); + } + writer.uint32(26).fork(); + for (const v of message.actions) { + writer.int32(permission_ActionToNumber(v)); + } + writer.join(); + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Permission { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBasePermission(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.effect = permission_EffectFromJSON(reader.int32()); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.resource = reader.string(); + continue; + } + case 3: { + if (tag === 24) { + message.actions.push(permission_ActionFromJSON(reader.int32())); + + continue; + } + + if (tag === 26) { + const end2 = reader.uint32() + reader.pos; + while (reader.pos < end2) { + message.actions.push(permission_ActionFromJSON(reader.int32())); + } + + continue; + } + + break; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Permission { + return { + effect: isSet(object.effect) ? permission_EffectFromJSON(object.effect) : Permission_Effect.ALLOW, + resource: isSet(object.resource) ? globalThis.String(object.resource) : "", + actions: globalThis.Array.isArray(object?.actions) + ? object.actions.map((e: any) => permission_ActionFromJSON(e)) + : [], + }; + }, + + toJSON(message: Permission): unknown { + const obj: any = {}; + if (message.effect !== Permission_Effect.ALLOW) { + obj.effect = permission_EffectToJSON(message.effect); + } + if (message.resource !== "") { + obj.resource = message.resource; + } + if (message.actions?.length) { + obj.actions = message.actions.map((e) => permission_ActionToJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): Permission { + return Permission.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Permission { + const message = createBasePermission(); + message.effect = object.effect ?? Permission_Effect.ALLOW; + message.resource = object.resource ?? ""; + message.actions = object.actions?.map((e) => e) || []; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create(base?: DeepPartial): T; + fromPartial(object: DeepPartial): T; +} diff --git a/packages/grpc/src/basics/environment.ts b/packages/grpc/src/basics/environment.ts new file mode 100644 index 0000000..fa533fe --- /dev/null +++ b/packages/grpc/src/basics/environment.ts @@ -0,0 +1,388 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.10.1 +// protoc v3.21.12 +// source: basics/environment.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "Environment"; + +export interface Item { + id: string; + jobId: string; + context: { [key: string]: Item_ContextValue }; + modified: string; +} + +export interface Item_ContextValue { + type: Item_ContextValue_Type; + value?: string | undefined; +} + +export const Item_ContextValue_Type = { TEXT: "TEXT", SECRET: "SECRET", UNRECOGNIZED: "UNRECOGNIZED" } as const; + +export type Item_ContextValue_Type = typeof Item_ContextValue_Type[keyof typeof Item_ContextValue_Type]; + +export namespace Item_ContextValue_Type { + export type TEXT = typeof Item_ContextValue_Type.TEXT; + export type SECRET = typeof Item_ContextValue_Type.SECRET; + export type UNRECOGNIZED = typeof Item_ContextValue_Type.UNRECOGNIZED; +} + +export function item_ContextValue_TypeFromJSON(object: any): Item_ContextValue_Type { + switch (object) { + case 0: + case "TEXT": + return Item_ContextValue_Type.TEXT; + case 1: + case "SECRET": + return Item_ContextValue_Type.SECRET; + case -1: + case "UNRECOGNIZED": + default: + return Item_ContextValue_Type.UNRECOGNIZED; + } +} + +export function item_ContextValue_TypeToJSON(object: Item_ContextValue_Type): string { + switch (object) { + case Item_ContextValue_Type.TEXT: + return "TEXT"; + case Item_ContextValue_Type.SECRET: + return "SECRET"; + case Item_ContextValue_Type.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function item_ContextValue_TypeToNumber(object: Item_ContextValue_Type): number { + switch (object) { + case Item_ContextValue_Type.TEXT: + return 0; + case Item_ContextValue_Type.SECRET: + return 1; + case Item_ContextValue_Type.UNRECOGNIZED: + default: + return -1; + } +} + +export interface Item_ContextEntry { + key: string; + value: Item_ContextValue | undefined; +} + +function createBaseItem(): Item { + return { id: "", jobId: "", context: {}, modified: "" }; +} + +export const Item: MessageFns = { + encode(message: Item, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.jobId !== "") { + writer.uint32(18).string(message.jobId); + } + globalThis.Object.entries(message.context).forEach(([key, value]: [string, Item_ContextValue]) => { + Item_ContextEntry.encode({ key: key as any, value }, writer.uint32(26).fork()).join(); + }); + if (message.modified !== "") { + writer.uint32(34).string(message.modified); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + const entry3 = Item_ContextEntry.decode(reader, reader.uint32()); + if (entry3.value !== undefined) { + message.context[entry3.key] = entry3.value; + } + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.modified = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + context: isObject(object.context) + ? (globalThis.Object.entries(object.context) as [string, any][]).reduce( + (acc: { [key: string]: Item_ContextValue }, [key, value]: [string, any]) => { + acc[key] = Item_ContextValue.fromJSON(value); + return acc; + }, + {}, + ) + : {}, + modified: isSet(object.modified) ? globalThis.String(object.modified) : "", + }; + }, + + toJSON(message: Item): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.context) { + const entries = globalThis.Object.entries(message.context) as [string, Item_ContextValue][]; + if (entries.length > 0) { + obj.context = {}; + entries.forEach(([k, v]) => { + obj.context[k] = Item_ContextValue.toJSON(v); + }); + } + } + if (message.modified !== "") { + obj.modified = message.modified; + } + return obj; + }, + + create(base?: DeepPartial): Item { + return Item.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item { + const message = createBaseItem(); + message.id = object.id ?? ""; + message.jobId = object.jobId ?? ""; + message.context = (globalThis.Object.entries(object.context ?? {}) as [string, Item_ContextValue][]).reduce( + (acc: { [key: string]: Item_ContextValue }, [key, value]: [string, Item_ContextValue]) => { + if (value !== undefined) { + acc[key] = Item_ContextValue.fromPartial(value); + } + return acc; + }, + {}, + ); + message.modified = object.modified ?? ""; + return message; + }, +}; + +function createBaseItem_ContextValue(): Item_ContextValue { + return { type: Item_ContextValue_Type.TEXT, value: undefined }; +} + +export const Item_ContextValue: MessageFns = { + encode(message: Item_ContextValue, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.type !== Item_ContextValue_Type.TEXT) { + writer.uint32(8).int32(item_ContextValue_TypeToNumber(message.type)); + } + if (message.value !== undefined) { + writer.uint32(18).string(message.value); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item_ContextValue { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem_ContextValue(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.type = item_ContextValue_TypeFromJSON(reader.int32()); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.value = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item_ContextValue { + return { + type: isSet(object.type) ? item_ContextValue_TypeFromJSON(object.type) : Item_ContextValue_Type.TEXT, + value: isSet(object.value) ? globalThis.String(object.value) : undefined, + }; + }, + + toJSON(message: Item_ContextValue): unknown { + const obj: any = {}; + if (message.type !== Item_ContextValue_Type.TEXT) { + obj.type = item_ContextValue_TypeToJSON(message.type); + } + if (message.value !== undefined) { + obj.value = message.value; + } + return obj; + }, + + create(base?: DeepPartial): Item_ContextValue { + return Item_ContextValue.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item_ContextValue { + const message = createBaseItem_ContextValue(); + message.type = object.type ?? Item_ContextValue_Type.TEXT; + message.value = object.value ?? undefined; + return message; + }, +}; + +function createBaseItem_ContextEntry(): Item_ContextEntry { + return { key: "", value: undefined }; +} + +export const Item_ContextEntry: MessageFns = { + encode(message: Item_ContextEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.key !== "") { + writer.uint32(10).string(message.key); + } + if (message.value !== undefined) { + Item_ContextValue.encode(message.value, writer.uint32(18).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item_ContextEntry { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem_ContextEntry(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.key = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.value = Item_ContextValue.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item_ContextEntry { + return { + key: isSet(object.key) ? globalThis.String(object.key) : "", + value: isSet(object.value) ? Item_ContextValue.fromJSON(object.value) : undefined, + }; + }, + + toJSON(message: Item_ContextEntry): unknown { + const obj: any = {}; + if (message.key !== "") { + obj.key = message.key; + } + if (message.value !== undefined) { + obj.value = Item_ContextValue.toJSON(message.value); + } + return obj; + }, + + create(base?: DeepPartial): Item_ContextEntry { + return Item_ContextEntry.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item_ContextEntry { + const message = createBaseItem_ContextEntry(); + message.key = object.key ?? ""; + message.value = (object.value !== undefined && object.value !== null) + ? Item_ContextValue.fromPartial(object.value) + : undefined; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +function isObject(value: any): boolean { + return typeof value === "object" && value !== null; +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create(base?: DeepPartial): T; + fromPartial(object: DeepPartial): T; +} diff --git a/packages/grpc/src/basics/job-version.ts b/packages/grpc/src/basics/job-version.ts new file mode 100644 index 0000000..94adfea --- /dev/null +++ b/packages/grpc/src/basics/job-version.ts @@ -0,0 +1,163 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.10.1 +// protoc v3.21.12 +// source: basics/job-version.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "JobVersion"; + +export interface Item { + id: string; + jobId: string; + version: string; + modified: string; + created: string; +} + +function createBaseItem(): Item { + return { id: "", jobId: "", version: "", modified: "", created: "" }; +} + +export const Item: MessageFns = { + encode(message: Item, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.jobId !== "") { + writer.uint32(18).string(message.jobId); + } + if (message.version !== "") { + writer.uint32(26).string(message.version); + } + if (message.modified !== "") { + writer.uint32(34).string(message.modified); + } + if (message.created !== "") { + writer.uint32(42).string(message.created); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.version = reader.string(); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.modified = reader.string(); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.created = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + version: isSet(object.version) ? globalThis.String(object.version) : "", + modified: isSet(object.modified) ? globalThis.String(object.modified) : "", + created: isSet(object.created) ? globalThis.String(object.created) : "", + }; + }, + + toJSON(message: Item): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.version !== "") { + obj.version = message.version; + } + if (message.modified !== "") { + obj.modified = message.modified; + } + if (message.created !== "") { + obj.created = message.created; + } + return obj; + }, + + create(base?: DeepPartial): Item { + return Item.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item { + const message = createBaseItem(); + message.id = object.id ?? ""; + message.jobId = object.jobId ?? ""; + message.version = object.version ?? ""; + message.modified = object.modified ?? ""; + message.created = object.created ?? ""; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create(base?: DeepPartial): T; + fromPartial(object: DeepPartial): T; +} diff --git a/packages/grpc/src/basics/job.ts b/packages/grpc/src/basics/job.ts new file mode 100644 index 0000000..fe88d04 --- /dev/null +++ b/packages/grpc/src/basics/job.ts @@ -0,0 +1,310 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.10.1 +// protoc v3.21.12 +// source: basics/job.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "Job"; + +export interface Item { + id: string; + jobName: string; + status: Item_Status; + description?: string | undefined; + versionId?: string | undefined; + links: Item_Link[]; +} + +export const Item_Status = { ENABLED: "ENABLED", DISABLED: "DISABLED", UNRECOGNIZED: "UNRECOGNIZED" } as const; + +export type Item_Status = typeof Item_Status[keyof typeof Item_Status]; + +export namespace Item_Status { + export type ENABLED = typeof Item_Status.ENABLED; + export type DISABLED = typeof Item_Status.DISABLED; + export type UNRECOGNIZED = typeof Item_Status.UNRECOGNIZED; +} + +export function item_StatusFromJSON(object: any): Item_Status { + switch (object) { + case 0: + case "ENABLED": + return Item_Status.ENABLED; + case 1: + case "DISABLED": + return Item_Status.DISABLED; + case -1: + case "UNRECOGNIZED": + default: + return Item_Status.UNRECOGNIZED; + } +} + +export function item_StatusToJSON(object: Item_Status): string { + switch (object) { + case Item_Status.ENABLED: + return "ENABLED"; + case Item_Status.DISABLED: + return "DISABLED"; + case Item_Status.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function item_StatusToNumber(object: Item_Status): number { + switch (object) { + case Item_Status.ENABLED: + return 0; + case Item_Status.DISABLED: + return 1; + case Item_Status.UNRECOGNIZED: + default: + return -1; + } +} + +export interface Item_Link { + name: string; + url: string; +} + +function createBaseItem(): Item { + return { id: "", jobName: "", status: Item_Status.ENABLED, description: undefined, versionId: undefined, links: [] }; +} + +export const Item: MessageFns = { + encode(message: Item, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.jobName !== "") { + writer.uint32(18).string(message.jobName); + } + if (message.status !== Item_Status.ENABLED) { + writer.uint32(24).int32(item_StatusToNumber(message.status)); + } + if (message.description !== undefined) { + writer.uint32(34).string(message.description); + } + if (message.versionId !== undefined) { + writer.uint32(42).string(message.versionId); + } + for (const v of message.links) { + Item_Link.encode(v!, writer.uint32(50).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.jobName = reader.string(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.status = item_StatusFromJSON(reader.int32()); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.description = reader.string(); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.versionId = reader.string(); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.links.push(Item_Link.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + jobName: isSet(object.jobName) ? globalThis.String(object.jobName) : "", + status: isSet(object.status) ? item_StatusFromJSON(object.status) : Item_Status.ENABLED, + description: isSet(object.description) ? globalThis.String(object.description) : undefined, + versionId: isSet(object.versionId) ? globalThis.String(object.versionId) : undefined, + links: globalThis.Array.isArray(object?.links) ? object.links.map((e: any) => Item_Link.fromJSON(e)) : [], + }; + }, + + toJSON(message: Item): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.jobName !== "") { + obj.jobName = message.jobName; + } + if (message.status !== Item_Status.ENABLED) { + obj.status = item_StatusToJSON(message.status); + } + if (message.description !== undefined) { + obj.description = message.description; + } + if (message.versionId !== undefined) { + obj.versionId = message.versionId; + } + if (message.links?.length) { + obj.links = message.links.map((e) => Item_Link.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): Item { + return Item.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item { + const message = createBaseItem(); + message.id = object.id ?? ""; + message.jobName = object.jobName ?? ""; + message.status = object.status ?? Item_Status.ENABLED; + message.description = object.description ?? undefined; + message.versionId = object.versionId ?? undefined; + message.links = object.links?.map((e) => Item_Link.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseItem_Link(): Item_Link { + return { name: "", url: "" }; +} + +export const Item_Link: MessageFns = { + encode(message: Item_Link, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.name !== "") { + writer.uint32(10).string(message.name); + } + if (message.url !== "") { + writer.uint32(18).string(message.url); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item_Link { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem_Link(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.name = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.url = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item_Link { + return { + name: isSet(object.name) ? globalThis.String(object.name) : "", + url: isSet(object.url) ? globalThis.String(object.url) : "", + }; + }, + + toJSON(message: Item_Link): unknown { + const obj: any = {}; + if (message.name !== "") { + obj.name = message.name; + } + if (message.url !== "") { + obj.url = message.url; + } + return obj; + }, + + create(base?: DeepPartial): Item_Link { + return Item_Link.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item_Link { + const message = createBaseItem_Link(); + message.name = object.name ?? ""; + message.url = object.url ?? ""; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create(base?: DeepPartial): T; + fromPartial(object: DeepPartial): T; +} diff --git a/packages/grpc/src/basics/jwt-key.ts b/packages/grpc/src/basics/jwt-key.ts new file mode 100644 index 0000000..813b617 --- /dev/null +++ b/packages/grpc/src/basics/jwt-key.ts @@ -0,0 +1,229 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.10.1 +// protoc v3.21.12 +// source: basics/jwt-key.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "JwtKey"; + +export interface Item { + id: string; + privateKey?: string | undefined; + publicKey: string; + status: Item_Status; + expires: string; + created: string; +} + +export const Item_Status = { ENABLED: "ENABLED", DISABLED: "DISABLED", UNRECOGNIZED: "UNRECOGNIZED" } as const; + +export type Item_Status = typeof Item_Status[keyof typeof Item_Status]; + +export namespace Item_Status { + export type ENABLED = typeof Item_Status.ENABLED; + export type DISABLED = typeof Item_Status.DISABLED; + export type UNRECOGNIZED = typeof Item_Status.UNRECOGNIZED; +} + +export function item_StatusFromJSON(object: any): Item_Status { + switch (object) { + case 0: + case "ENABLED": + return Item_Status.ENABLED; + case 1: + case "DISABLED": + return Item_Status.DISABLED; + case -1: + case "UNRECOGNIZED": + default: + return Item_Status.UNRECOGNIZED; + } +} + +export function item_StatusToJSON(object: Item_Status): string { + switch (object) { + case Item_Status.ENABLED: + return "ENABLED"; + case Item_Status.DISABLED: + return "DISABLED"; + case Item_Status.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function item_StatusToNumber(object: Item_Status): number { + switch (object) { + case Item_Status.ENABLED: + return 0; + case Item_Status.DISABLED: + return 1; + case Item_Status.UNRECOGNIZED: + default: + return -1; + } +} + +function createBaseItem(): Item { + return { id: "", privateKey: undefined, publicKey: "", status: Item_Status.ENABLED, expires: "", created: "" }; +} + +export const Item: MessageFns = { + encode(message: Item, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.privateKey !== undefined) { + writer.uint32(18).string(message.privateKey); + } + if (message.publicKey !== "") { + writer.uint32(26).string(message.publicKey); + } + if (message.status !== Item_Status.ENABLED) { + writer.uint32(32).int32(item_StatusToNumber(message.status)); + } + if (message.expires !== "") { + writer.uint32(42).string(message.expires); + } + if (message.created !== "") { + writer.uint32(50).string(message.created); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.privateKey = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.publicKey = reader.string(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.status = item_StatusFromJSON(reader.int32()); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.expires = reader.string(); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.created = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + privateKey: isSet(object.privateKey) ? globalThis.String(object.privateKey) : undefined, + publicKey: isSet(object.publicKey) ? globalThis.String(object.publicKey) : "", + status: isSet(object.status) ? item_StatusFromJSON(object.status) : Item_Status.ENABLED, + expires: isSet(object.expires) ? globalThis.String(object.expires) : "", + created: isSet(object.created) ? globalThis.String(object.created) : "", + }; + }, + + toJSON(message: Item): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.privateKey !== undefined) { + obj.privateKey = message.privateKey; + } + if (message.publicKey !== "") { + obj.publicKey = message.publicKey; + } + if (message.status !== Item_Status.ENABLED) { + obj.status = item_StatusToJSON(message.status); + } + if (message.expires !== "") { + obj.expires = message.expires; + } + if (message.created !== "") { + obj.created = message.created; + } + return obj; + }, + + create(base?: DeepPartial): Item { + return Item.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item { + const message = createBaseItem(); + message.id = object.id ?? ""; + message.privateKey = object.privateKey ?? undefined; + message.publicKey = object.publicKey ?? ""; + message.status = object.status ?? Item_Status.ENABLED; + message.expires = object.expires ?? ""; + message.created = object.created ?? ""; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create(base?: DeepPartial): T; + fromPartial(object: DeepPartial): T; +} diff --git a/packages/grpc/src/basics/jwt-keys.ts b/packages/grpc/src/basics/jwt-keys.ts new file mode 100644 index 0000000..8d465b0 --- /dev/null +++ b/packages/grpc/src/basics/jwt-keys.ts @@ -0,0 +1,229 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.10.1 +// protoc v3.21.12 +// source: basics/jwt-keys.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "JwtKey"; + +export interface Item { + id: string; + privateKey?: string | undefined; + publicKey: string; + status: Item_Status; + expires: string; + created: string; +} + +export const Item_Status = { ENABLED: "ENABLED", DISABLED: "DISABLED", UNRECOGNIZED: "UNRECOGNIZED" } as const; + +export type Item_Status = typeof Item_Status[keyof typeof Item_Status]; + +export namespace Item_Status { + export type ENABLED = typeof Item_Status.ENABLED; + export type DISABLED = typeof Item_Status.DISABLED; + export type UNRECOGNIZED = typeof Item_Status.UNRECOGNIZED; +} + +export function item_StatusFromJSON(object: any): Item_Status { + switch (object) { + case 0: + case "ENABLED": + return Item_Status.ENABLED; + case 1: + case "DISABLED": + return Item_Status.DISABLED; + case -1: + case "UNRECOGNIZED": + default: + return Item_Status.UNRECOGNIZED; + } +} + +export function item_StatusToJSON(object: Item_Status): string { + switch (object) { + case Item_Status.ENABLED: + return "ENABLED"; + case Item_Status.DISABLED: + return "DISABLED"; + case Item_Status.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function item_StatusToNumber(object: Item_Status): number { + switch (object) { + case Item_Status.ENABLED: + return 0; + case Item_Status.DISABLED: + return 1; + case Item_Status.UNRECOGNIZED: + default: + return -1; + } +} + +function createBaseItem(): Item { + return { id: "", privateKey: undefined, publicKey: "", status: Item_Status.ENABLED, expires: "", created: "" }; +} + +export const Item: MessageFns = { + encode(message: Item, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.privateKey !== undefined) { + writer.uint32(18).string(message.privateKey); + } + if (message.publicKey !== "") { + writer.uint32(26).string(message.publicKey); + } + if (message.status !== Item_Status.ENABLED) { + writer.uint32(32).int32(item_StatusToNumber(message.status)); + } + if (message.expires !== "") { + writer.uint32(42).string(message.expires); + } + if (message.created !== "") { + writer.uint32(50).string(message.created); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.privateKey = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.publicKey = reader.string(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.status = item_StatusFromJSON(reader.int32()); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.expires = reader.string(); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.created = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + privateKey: isSet(object.privateKey) ? globalThis.String(object.privateKey) : undefined, + publicKey: isSet(object.publicKey) ? globalThis.String(object.publicKey) : "", + status: isSet(object.status) ? item_StatusFromJSON(object.status) : Item_Status.ENABLED, + expires: isSet(object.expires) ? globalThis.String(object.expires) : "", + created: isSet(object.created) ? globalThis.String(object.created) : "", + }; + }, + + toJSON(message: Item): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.privateKey !== undefined) { + obj.privateKey = message.privateKey; + } + if (message.publicKey !== "") { + obj.publicKey = message.publicKey; + } + if (message.status !== Item_Status.ENABLED) { + obj.status = item_StatusToJSON(message.status); + } + if (message.expires !== "") { + obj.expires = message.expires; + } + if (message.created !== "") { + obj.created = message.created; + } + return obj; + }, + + create(base?: DeepPartial): Item { + return Item.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item { + const message = createBaseItem(); + message.id = object.id ?? ""; + message.privateKey = object.privateKey ?? undefined; + message.publicKey = object.publicKey ?? ""; + message.status = object.status ?? Item_Status.ENABLED; + message.expires = object.expires ?? ""; + message.created = object.created ?? ""; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create(base?: DeepPartial): T; + fromPartial(object: DeepPartial): T; +} diff --git a/packages/grpc/src/basics/runner.ts b/packages/grpc/src/basics/runner.ts new file mode 100644 index 0000000..52892cd --- /dev/null +++ b/packages/grpc/src/basics/runner.ts @@ -0,0 +1,294 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.10.1 +// protoc v3.21.12 +// source: basics/runner.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "Runner"; + +export interface Item { + id: string; + jobId: string; + actionId: string; + status: Item_Status; + createdAt: string; + readyAt?: string | undefined; + closingAt?: string | undefined; + closedAt?: string | undefined; +} + +export const Item_Status = { + STARTING: "STARTING", + READY: "READY", + CLOSING: "CLOSING", + CLOSED: "CLOSED", + UNRECOGNIZED: "UNRECOGNIZED", +} as const; + +export type Item_Status = typeof Item_Status[keyof typeof Item_Status]; + +export namespace Item_Status { + export type STARTING = typeof Item_Status.STARTING; + export type READY = typeof Item_Status.READY; + export type CLOSING = typeof Item_Status.CLOSING; + export type CLOSED = typeof Item_Status.CLOSED; + export type UNRECOGNIZED = typeof Item_Status.UNRECOGNIZED; +} + +export function item_StatusFromJSON(object: any): Item_Status { + switch (object) { + case 0: + case "STARTING": + return Item_Status.STARTING; + case 1: + case "READY": + return Item_Status.READY; + case 2: + case "CLOSING": + return Item_Status.CLOSING; + case 3: + case "CLOSED": + return Item_Status.CLOSED; + case -1: + case "UNRECOGNIZED": + default: + return Item_Status.UNRECOGNIZED; + } +} + +export function item_StatusToJSON(object: Item_Status): string { + switch (object) { + case Item_Status.STARTING: + return "STARTING"; + case Item_Status.READY: + return "READY"; + case Item_Status.CLOSING: + return "CLOSING"; + case Item_Status.CLOSED: + return "CLOSED"; + case Item_Status.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export function item_StatusToNumber(object: Item_Status): number { + switch (object) { + case Item_Status.STARTING: + return 0; + case Item_Status.READY: + return 1; + case Item_Status.CLOSING: + return 2; + case Item_Status.CLOSED: + return 3; + case Item_Status.UNRECOGNIZED: + default: + return -1; + } +} + +function createBaseItem(): Item { + return { + id: "", + jobId: "", + actionId: "", + status: Item_Status.STARTING, + createdAt: "", + readyAt: undefined, + closingAt: undefined, + closedAt: undefined, + }; +} + +export const Item: MessageFns = { + encode(message: Item, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.jobId !== "") { + writer.uint32(18).string(message.jobId); + } + if (message.actionId !== "") { + writer.uint32(26).string(message.actionId); + } + if (message.status !== Item_Status.STARTING) { + writer.uint32(32).int32(item_StatusToNumber(message.status)); + } + if (message.createdAt !== "") { + writer.uint32(90).string(message.createdAt); + } + if (message.readyAt !== undefined) { + writer.uint32(98).string(message.readyAt); + } + if (message.closingAt !== undefined) { + writer.uint32(106).string(message.closingAt); + } + if (message.closedAt !== undefined) { + writer.uint32(114).string(message.closedAt); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.actionId = reader.string(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.status = item_StatusFromJSON(reader.int32()); + continue; + } + case 11: { + if (tag !== 90) { + break; + } + + message.createdAt = reader.string(); + continue; + } + case 12: { + if (tag !== 98) { + break; + } + + message.readyAt = reader.string(); + continue; + } + case 13: { + if (tag !== 106) { + break; + } + + message.closingAt = reader.string(); + continue; + } + case 14: { + if (tag !== 114) { + break; + } + + message.closedAt = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + actionId: isSet(object.actionId) ? globalThis.String(object.actionId) : "", + status: isSet(object.status) ? item_StatusFromJSON(object.status) : Item_Status.STARTING, + createdAt: isSet(object.createdAt) ? globalThis.String(object.createdAt) : "", + readyAt: isSet(object.readyAt) ? globalThis.String(object.readyAt) : undefined, + closingAt: isSet(object.closingAt) ? globalThis.String(object.closingAt) : undefined, + closedAt: isSet(object.closedAt) ? globalThis.String(object.closedAt) : undefined, + }; + }, + + toJSON(message: Item): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.actionId !== "") { + obj.actionId = message.actionId; + } + if (message.status !== Item_Status.STARTING) { + obj.status = item_StatusToJSON(message.status); + } + if (message.createdAt !== "") { + obj.createdAt = message.createdAt; + } + if (message.readyAt !== undefined) { + obj.readyAt = message.readyAt; + } + if (message.closingAt !== undefined) { + obj.closingAt = message.closingAt; + } + if (message.closedAt !== undefined) { + obj.closedAt = message.closedAt; + } + return obj; + }, + + create(base?: DeepPartial): Item { + return Item.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item { + const message = createBaseItem(); + message.id = object.id ?? ""; + message.jobId = object.jobId ?? ""; + message.actionId = object.actionId ?? ""; + message.status = object.status ?? Item_Status.STARTING; + message.createdAt = object.createdAt ?? ""; + message.readyAt = object.readyAt ?? undefined; + message.closingAt = object.closingAt ?? undefined; + message.closedAt = object.closedAt ?? undefined; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create(base?: DeepPartial): T; + fromPartial(object: DeepPartial): T; +} diff --git a/packages/grpc/src/basics/trigger.ts b/packages/grpc/src/basics/trigger.ts new file mode 100644 index 0000000..d6ebaf4 --- /dev/null +++ b/packages/grpc/src/basics/trigger.ts @@ -0,0 +1,763 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.10.1 +// protoc v3.21.12 +// source: basics/trigger.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "Trigger"; + +export interface Item { + id: string; + jobId: string; + versionId: string; + schedule?: Item_TriggerSchedule | undefined; + http?: Item_TriggerHttp | undefined; + mqtt?: Item_TriggerMqtt | undefined; +} + +export interface Item_TriggerSchedule { + name?: string | undefined; + cron: string; + timezone?: string | undefined; +} + +export interface Item_TriggerHttp { + name?: string | undefined; + hostname?: string | undefined; + method?: string | undefined; + path?: string | undefined; +} + +export interface Item_TriggerMqtt { + name?: string | undefined; + topics: string[]; + connection: Item_TriggerMqtt_Connection | undefined; +} + +export interface Item_TriggerMqtt_Connection { + protocol?: string | undefined; + protocolVariable?: string | undefined; + port?: string | undefined; + portVariable?: string | undefined; + host?: string | undefined; + hostVariable?: string | undefined; + username?: string | undefined; + usernameVariable?: string | undefined; + password?: string | undefined; + passwordVariable?: string | undefined; + clientId?: string | undefined; + clientIdVariable?: string | undefined; +} + +function createBaseItem(): Item { + return { id: "", jobId: "", versionId: "", schedule: undefined, http: undefined, mqtt: undefined }; +} + +export const Item: MessageFns = { + encode(message: Item, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.jobId !== "") { + writer.uint32(18).string(message.jobId); + } + if (message.versionId !== "") { + writer.uint32(26).string(message.versionId); + } + if (message.schedule !== undefined) { + Item_TriggerSchedule.encode(message.schedule, writer.uint32(34).fork()).join(); + } + if (message.http !== undefined) { + Item_TriggerHttp.encode(message.http, writer.uint32(42).fork()).join(); + } + if (message.mqtt !== undefined) { + Item_TriggerMqtt.encode(message.mqtt, writer.uint32(50).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.versionId = reader.string(); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.schedule = Item_TriggerSchedule.decode(reader, reader.uint32()); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.http = Item_TriggerHttp.decode(reader, reader.uint32()); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.mqtt = Item_TriggerMqtt.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + versionId: isSet(object.versionId) ? globalThis.String(object.versionId) : "", + schedule: isSet(object.schedule) ? Item_TriggerSchedule.fromJSON(object.schedule) : undefined, + http: isSet(object.http) ? Item_TriggerHttp.fromJSON(object.http) : undefined, + mqtt: isSet(object.mqtt) ? Item_TriggerMqtt.fromJSON(object.mqtt) : undefined, + }; + }, + + toJSON(message: Item): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.versionId !== "") { + obj.versionId = message.versionId; + } + if (message.schedule !== undefined) { + obj.schedule = Item_TriggerSchedule.toJSON(message.schedule); + } + if (message.http !== undefined) { + obj.http = Item_TriggerHttp.toJSON(message.http); + } + if (message.mqtt !== undefined) { + obj.mqtt = Item_TriggerMqtt.toJSON(message.mqtt); + } + return obj; + }, + + create(base?: DeepPartial): Item { + return Item.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item { + const message = createBaseItem(); + message.id = object.id ?? ""; + message.jobId = object.jobId ?? ""; + message.versionId = object.versionId ?? ""; + message.schedule = (object.schedule !== undefined && object.schedule !== null) + ? Item_TriggerSchedule.fromPartial(object.schedule) + : undefined; + message.http = (object.http !== undefined && object.http !== null) + ? Item_TriggerHttp.fromPartial(object.http) + : undefined; + message.mqtt = (object.mqtt !== undefined && object.mqtt !== null) + ? Item_TriggerMqtt.fromPartial(object.mqtt) + : undefined; + return message; + }, +}; + +function createBaseItem_TriggerSchedule(): Item_TriggerSchedule { + return { name: undefined, cron: "", timezone: undefined }; +} + +export const Item_TriggerSchedule: MessageFns = { + encode(message: Item_TriggerSchedule, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.name !== undefined) { + writer.uint32(10).string(message.name); + } + if (message.cron !== "") { + writer.uint32(18).string(message.cron); + } + if (message.timezone !== undefined) { + writer.uint32(26).string(message.timezone); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item_TriggerSchedule { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem_TriggerSchedule(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.name = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.cron = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.timezone = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item_TriggerSchedule { + return { + name: isSet(object.name) ? globalThis.String(object.name) : undefined, + cron: isSet(object.cron) ? globalThis.String(object.cron) : "", + timezone: isSet(object.timezone) ? globalThis.String(object.timezone) : undefined, + }; + }, + + toJSON(message: Item_TriggerSchedule): unknown { + const obj: any = {}; + if (message.name !== undefined) { + obj.name = message.name; + } + if (message.cron !== "") { + obj.cron = message.cron; + } + if (message.timezone !== undefined) { + obj.timezone = message.timezone; + } + return obj; + }, + + create(base?: DeepPartial): Item_TriggerSchedule { + return Item_TriggerSchedule.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item_TriggerSchedule { + const message = createBaseItem_TriggerSchedule(); + message.name = object.name ?? undefined; + message.cron = object.cron ?? ""; + message.timezone = object.timezone ?? undefined; + return message; + }, +}; + +function createBaseItem_TriggerHttp(): Item_TriggerHttp { + return { name: undefined, hostname: undefined, method: undefined, path: undefined }; +} + +export const Item_TriggerHttp: MessageFns = { + encode(message: Item_TriggerHttp, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.name !== undefined) { + writer.uint32(10).string(message.name); + } + if (message.hostname !== undefined) { + writer.uint32(18).string(message.hostname); + } + if (message.method !== undefined) { + writer.uint32(26).string(message.method); + } + if (message.path !== undefined) { + writer.uint32(34).string(message.path); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item_TriggerHttp { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem_TriggerHttp(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.name = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.hostname = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.method = reader.string(); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.path = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item_TriggerHttp { + return { + name: isSet(object.name) ? globalThis.String(object.name) : undefined, + hostname: isSet(object.hostname) ? globalThis.String(object.hostname) : undefined, + method: isSet(object.method) ? globalThis.String(object.method) : undefined, + path: isSet(object.path) ? globalThis.String(object.path) : undefined, + }; + }, + + toJSON(message: Item_TriggerHttp): unknown { + const obj: any = {}; + if (message.name !== undefined) { + obj.name = message.name; + } + if (message.hostname !== undefined) { + obj.hostname = message.hostname; + } + if (message.method !== undefined) { + obj.method = message.method; + } + if (message.path !== undefined) { + obj.path = message.path; + } + return obj; + }, + + create(base?: DeepPartial): Item_TriggerHttp { + return Item_TriggerHttp.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item_TriggerHttp { + const message = createBaseItem_TriggerHttp(); + message.name = object.name ?? undefined; + message.hostname = object.hostname ?? undefined; + message.method = object.method ?? undefined; + message.path = object.path ?? undefined; + return message; + }, +}; + +function createBaseItem_TriggerMqtt(): Item_TriggerMqtt { + return { name: undefined, topics: [], connection: undefined }; +} + +export const Item_TriggerMqtt: MessageFns = { + encode(message: Item_TriggerMqtt, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.name !== undefined) { + writer.uint32(10).string(message.name); + } + for (const v of message.topics) { + writer.uint32(18).string(v!); + } + if (message.connection !== undefined) { + Item_TriggerMqtt_Connection.encode(message.connection, writer.uint32(26).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item_TriggerMqtt { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem_TriggerMqtt(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.name = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.topics.push(reader.string()); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.connection = Item_TriggerMqtt_Connection.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item_TriggerMqtt { + return { + name: isSet(object.name) ? globalThis.String(object.name) : undefined, + topics: globalThis.Array.isArray(object?.topics) ? object.topics.map((e: any) => globalThis.String(e)) : [], + connection: isSet(object.connection) ? Item_TriggerMqtt_Connection.fromJSON(object.connection) : undefined, + }; + }, + + toJSON(message: Item_TriggerMqtt): unknown { + const obj: any = {}; + if (message.name !== undefined) { + obj.name = message.name; + } + if (message.topics?.length) { + obj.topics = message.topics; + } + if (message.connection !== undefined) { + obj.connection = Item_TriggerMqtt_Connection.toJSON(message.connection); + } + return obj; + }, + + create(base?: DeepPartial): Item_TriggerMqtt { + return Item_TriggerMqtt.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item_TriggerMqtt { + const message = createBaseItem_TriggerMqtt(); + message.name = object.name ?? undefined; + message.topics = object.topics?.map((e) => e) || []; + message.connection = (object.connection !== undefined && object.connection !== null) + ? Item_TriggerMqtt_Connection.fromPartial(object.connection) + : undefined; + return message; + }, +}; + +function createBaseItem_TriggerMqtt_Connection(): Item_TriggerMqtt_Connection { + return { + protocol: undefined, + protocolVariable: undefined, + port: undefined, + portVariable: undefined, + host: undefined, + hostVariable: undefined, + username: undefined, + usernameVariable: undefined, + password: undefined, + passwordVariable: undefined, + clientId: undefined, + clientIdVariable: undefined, + }; +} + +export const Item_TriggerMqtt_Connection: MessageFns = { + encode(message: Item_TriggerMqtt_Connection, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.protocol !== undefined) { + writer.uint32(10).string(message.protocol); + } + if (message.protocolVariable !== undefined) { + writer.uint32(18).string(message.protocolVariable); + } + if (message.port !== undefined) { + writer.uint32(26).string(message.port); + } + if (message.portVariable !== undefined) { + writer.uint32(34).string(message.portVariable); + } + if (message.host !== undefined) { + writer.uint32(42).string(message.host); + } + if (message.hostVariable !== undefined) { + writer.uint32(50).string(message.hostVariable); + } + if (message.username !== undefined) { + writer.uint32(58).string(message.username); + } + if (message.usernameVariable !== undefined) { + writer.uint32(66).string(message.usernameVariable); + } + if (message.password !== undefined) { + writer.uint32(74).string(message.password); + } + if (message.passwordVariable !== undefined) { + writer.uint32(82).string(message.passwordVariable); + } + if (message.clientId !== undefined) { + writer.uint32(90).string(message.clientId); + } + if (message.clientIdVariable !== undefined) { + writer.uint32(98).string(message.clientIdVariable); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Item_TriggerMqtt_Connection { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseItem_TriggerMqtt_Connection(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.protocol = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.protocolVariable = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.port = reader.string(); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.portVariable = reader.string(); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.host = reader.string(); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.hostVariable = reader.string(); + continue; + } + case 7: { + if (tag !== 58) { + break; + } + + message.username = reader.string(); + continue; + } + case 8: { + if (tag !== 66) { + break; + } + + message.usernameVariable = reader.string(); + continue; + } + case 9: { + if (tag !== 74) { + break; + } + + message.password = reader.string(); + continue; + } + case 10: { + if (tag !== 82) { + break; + } + + message.passwordVariable = reader.string(); + continue; + } + case 11: { + if (tag !== 90) { + break; + } + + message.clientId = reader.string(); + continue; + } + case 12: { + if (tag !== 98) { + break; + } + + message.clientIdVariable = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Item_TriggerMqtt_Connection { + return { + protocol: isSet(object.protocol) ? globalThis.String(object.protocol) : undefined, + protocolVariable: isSet(object.protocolVariable) ? globalThis.String(object.protocolVariable) : undefined, + port: isSet(object.port) ? globalThis.String(object.port) : undefined, + portVariable: isSet(object.portVariable) ? globalThis.String(object.portVariable) : undefined, + host: isSet(object.host) ? globalThis.String(object.host) : undefined, + hostVariable: isSet(object.hostVariable) ? globalThis.String(object.hostVariable) : undefined, + username: isSet(object.username) ? globalThis.String(object.username) : undefined, + usernameVariable: isSet(object.usernameVariable) ? globalThis.String(object.usernameVariable) : undefined, + password: isSet(object.password) ? globalThis.String(object.password) : undefined, + passwordVariable: isSet(object.passwordVariable) ? globalThis.String(object.passwordVariable) : undefined, + clientId: isSet(object.clientId) ? globalThis.String(object.clientId) : undefined, + clientIdVariable: isSet(object.clientIdVariable) ? globalThis.String(object.clientIdVariable) : undefined, + }; + }, + + toJSON(message: Item_TriggerMqtt_Connection): unknown { + const obj: any = {}; + if (message.protocol !== undefined) { + obj.protocol = message.protocol; + } + if (message.protocolVariable !== undefined) { + obj.protocolVariable = message.protocolVariable; + } + if (message.port !== undefined) { + obj.port = message.port; + } + if (message.portVariable !== undefined) { + obj.portVariable = message.portVariable; + } + if (message.host !== undefined) { + obj.host = message.host; + } + if (message.hostVariable !== undefined) { + obj.hostVariable = message.hostVariable; + } + if (message.username !== undefined) { + obj.username = message.username; + } + if (message.usernameVariable !== undefined) { + obj.usernameVariable = message.usernameVariable; + } + if (message.password !== undefined) { + obj.password = message.password; + } + if (message.passwordVariable !== undefined) { + obj.passwordVariable = message.passwordVariable; + } + if (message.clientId !== undefined) { + obj.clientId = message.clientId; + } + if (message.clientIdVariable !== undefined) { + obj.clientIdVariable = message.clientIdVariable; + } + return obj; + }, + + create(base?: DeepPartial): Item_TriggerMqtt_Connection { + return Item_TriggerMqtt_Connection.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): Item_TriggerMqtt_Connection { + const message = createBaseItem_TriggerMqtt_Connection(); + message.protocol = object.protocol ?? undefined; + message.protocolVariable = object.protocolVariable ?? undefined; + message.port = object.port ?? undefined; + message.portVariable = object.portVariable ?? undefined; + message.host = object.host ?? undefined; + message.hostVariable = object.hostVariable ?? undefined; + message.username = object.username ?? undefined; + message.usernameVariable = object.usernameVariable ?? undefined; + message.password = object.password ?? undefined; + message.passwordVariable = object.passwordVariable ?? undefined; + message.clientId = object.clientId ?? undefined; + message.clientIdVariable = object.clientIdVariable ?? undefined; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create(base?: DeepPartial): T; + fromPartial(object: DeepPartial): T; +} diff --git a/packages/grpc/src/general.ts b/packages/grpc/src/general.ts new file mode 100644 index 0000000..236d6e8 --- /dev/null +++ b/packages/grpc/src/general.ts @@ -0,0 +1,1574 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.10.1 +// protoc v3.21.12 +// source: general.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; +import type { CallContext, CallOptions } from "nice-grpc-common"; +import { Item as Item2 } from "./basics/action.js"; +import { Item as Item1 } from "./basics/job-version.js"; +import { Item } from "./basics/job.js"; +import { + Item as Item4, + Item_Status, + item_StatusFromJSON, + item_StatusToJSON, + item_StatusToNumber, +} from "./basics/runner.js"; +import { Item as Item3 } from "./basics/trigger.js"; + +export const protobufPackage = "GeneralAPI"; + +/** getJob * */ +export interface JobRequest { + jobId: string; +} + +export interface JobResponse { + job: Item | undefined; +} + +/** getJobs * */ +export interface JobsRequest { +} + +export interface JobsResponse { + jobs: Item[]; +} + +/** getJobVersion * */ +export interface JobVersionRequest { + jobVersionId: string; +} + +export interface JobVersionResponse { + jobVersion: Item1 | undefined; +} + +/** getJobVersions * */ +export interface JobVersionsRequest { + jobId: string; +} + +export interface JobVersionsResponse { + jobVersions: Item1[]; +} + +/** getJobAction * */ +export interface JobActionRequest { + jobId: string; + actionId: string; +} + +export interface JobActionResponse { + action: Item2 | undefined; +} + +/** getJobActions * */ +export interface JobActionsRequest { + jobId: string; + versionId?: string | undefined; +} + +export interface JobActionsResponse { + actions: Item2[]; +} + +/** getJobTrigger * */ +export interface JobTriggerRequest { + jobId: string; + triggerId: string; +} + +export interface JobTriggerResponse { + trigger: Item3 | undefined; +} + +/** getJobTriggers * */ +export interface JobTriggersRequest { + jobId: string; + versionId?: string | undefined; +} + +export interface JobTriggersResponse { + triggers: Item3[]; +} + +/** getRunner * */ +export interface RunnerRequest { + runnerId: string; +} + +export interface RunnerResponse { + runner: Item4 | undefined; +} + +/** getRunners * */ +export interface RunnersRequest { + jobId?: string | undefined; + versionId?: string | undefined; + actionId?: string | undefined; + status?: Item_Status | undefined; +} + +export interface RunnersResponse { + runners: Item4[]; +} + +function createBaseJobRequest(): JobRequest { + return { jobId: "" }; +} + +export const JobRequest: MessageFns = { + encode(message: JobRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.jobId !== "") { + writer.uint32(10).string(message.jobId); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobId = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobRequest { + return { jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "" }; + }, + + toJSON(message: JobRequest): unknown { + const obj: any = {}; + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + return obj; + }, + + create(base?: DeepPartial): JobRequest { + return JobRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobRequest { + const message = createBaseJobRequest(); + message.jobId = object.jobId ?? ""; + return message; + }, +}; + +function createBaseJobResponse(): JobResponse { + return { job: undefined }; +} + +export const JobResponse: MessageFns = { + encode(message: JobResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.job !== undefined) { + Item.encode(message.job, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.job = Item.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobResponse { + return { job: isSet(object.job) ? Item.fromJSON(object.job) : undefined }; + }, + + toJSON(message: JobResponse): unknown { + const obj: any = {}; + if (message.job !== undefined) { + obj.job = Item.toJSON(message.job); + } + return obj; + }, + + create(base?: DeepPartial): JobResponse { + return JobResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobResponse { + const message = createBaseJobResponse(); + message.job = (object.job !== undefined && object.job !== null) ? Item.fromPartial(object.job) : undefined; + return message; + }, +}; + +function createBaseJobsRequest(): JobsRequest { + return {}; +} + +export const JobsRequest: MessageFns = { + encode(_: JobsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobsRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobsRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(_: any): JobsRequest { + return {}; + }, + + toJSON(_: JobsRequest): unknown { + const obj: any = {}; + return obj; + }, + + create(base?: DeepPartial): JobsRequest { + return JobsRequest.fromPartial(base ?? {}); + }, + fromPartial(_: DeepPartial): JobsRequest { + const message = createBaseJobsRequest(); + return message; + }, +}; + +function createBaseJobsResponse(): JobsResponse { + return { jobs: [] }; +} + +export const JobsResponse: MessageFns = { + encode(message: JobsResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.jobs) { + Item.encode(v!, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobsResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobsResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobs.push(Item.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobsResponse { + return { jobs: globalThis.Array.isArray(object?.jobs) ? object.jobs.map((e: any) => Item.fromJSON(e)) : [] }; + }, + + toJSON(message: JobsResponse): unknown { + const obj: any = {}; + if (message.jobs?.length) { + obj.jobs = message.jobs.map((e) => Item.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): JobsResponse { + return JobsResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobsResponse { + const message = createBaseJobsResponse(); + message.jobs = object.jobs?.map((e) => Item.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseJobVersionRequest(): JobVersionRequest { + return { jobVersionId: "" }; +} + +export const JobVersionRequest: MessageFns = { + encode(message: JobVersionRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.jobVersionId !== "") { + writer.uint32(10).string(message.jobVersionId); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobVersionRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobVersionRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobVersionId = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobVersionRequest { + return { jobVersionId: isSet(object.jobVersionId) ? globalThis.String(object.jobVersionId) : "" }; + }, + + toJSON(message: JobVersionRequest): unknown { + const obj: any = {}; + if (message.jobVersionId !== "") { + obj.jobVersionId = message.jobVersionId; + } + return obj; + }, + + create(base?: DeepPartial): JobVersionRequest { + return JobVersionRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobVersionRequest { + const message = createBaseJobVersionRequest(); + message.jobVersionId = object.jobVersionId ?? ""; + return message; + }, +}; + +function createBaseJobVersionResponse(): JobVersionResponse { + return { jobVersion: undefined }; +} + +export const JobVersionResponse: MessageFns = { + encode(message: JobVersionResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.jobVersion !== undefined) { + Item1.encode(message.jobVersion, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobVersionResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobVersionResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobVersion = Item1.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobVersionResponse { + return { jobVersion: isSet(object.jobVersion) ? Item1.fromJSON(object.jobVersion) : undefined }; + }, + + toJSON(message: JobVersionResponse): unknown { + const obj: any = {}; + if (message.jobVersion !== undefined) { + obj.jobVersion = Item1.toJSON(message.jobVersion); + } + return obj; + }, + + create(base?: DeepPartial): JobVersionResponse { + return JobVersionResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobVersionResponse { + const message = createBaseJobVersionResponse(); + message.jobVersion = (object.jobVersion !== undefined && object.jobVersion !== null) + ? Item1.fromPartial(object.jobVersion) + : undefined; + return message; + }, +}; + +function createBaseJobVersionsRequest(): JobVersionsRequest { + return { jobId: "" }; +} + +export const JobVersionsRequest: MessageFns = { + encode(message: JobVersionsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.jobId !== "") { + writer.uint32(10).string(message.jobId); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobVersionsRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobVersionsRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobId = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobVersionsRequest { + return { jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "" }; + }, + + toJSON(message: JobVersionsRequest): unknown { + const obj: any = {}; + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + return obj; + }, + + create(base?: DeepPartial): JobVersionsRequest { + return JobVersionsRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobVersionsRequest { + const message = createBaseJobVersionsRequest(); + message.jobId = object.jobId ?? ""; + return message; + }, +}; + +function createBaseJobVersionsResponse(): JobVersionsResponse { + return { jobVersions: [] }; +} + +export const JobVersionsResponse: MessageFns = { + encode(message: JobVersionsResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.jobVersions) { + Item1.encode(v!, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobVersionsResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobVersionsResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobVersions.push(Item1.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobVersionsResponse { + return { + jobVersions: globalThis.Array.isArray(object?.jobVersions) + ? object.jobVersions.map((e: any) => Item1.fromJSON(e)) + : [], + }; + }, + + toJSON(message: JobVersionsResponse): unknown { + const obj: any = {}; + if (message.jobVersions?.length) { + obj.jobVersions = message.jobVersions.map((e) => Item1.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): JobVersionsResponse { + return JobVersionsResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobVersionsResponse { + const message = createBaseJobVersionsResponse(); + message.jobVersions = object.jobVersions?.map((e) => Item1.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseJobActionRequest(): JobActionRequest { + return { jobId: "", actionId: "" }; +} + +export const JobActionRequest: MessageFns = { + encode(message: JobActionRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.jobId !== "") { + writer.uint32(10).string(message.jobId); + } + if (message.actionId !== "") { + writer.uint32(18).string(message.actionId); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobActionRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobActionRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.actionId = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobActionRequest { + return { + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + actionId: isSet(object.actionId) ? globalThis.String(object.actionId) : "", + }; + }, + + toJSON(message: JobActionRequest): unknown { + const obj: any = {}; + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.actionId !== "") { + obj.actionId = message.actionId; + } + return obj; + }, + + create(base?: DeepPartial): JobActionRequest { + return JobActionRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobActionRequest { + const message = createBaseJobActionRequest(); + message.jobId = object.jobId ?? ""; + message.actionId = object.actionId ?? ""; + return message; + }, +}; + +function createBaseJobActionResponse(): JobActionResponse { + return { action: undefined }; +} + +export const JobActionResponse: MessageFns = { + encode(message: JobActionResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.action !== undefined) { + Item2.encode(message.action, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobActionResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobActionResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.action = Item2.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobActionResponse { + return { action: isSet(object.action) ? Item2.fromJSON(object.action) : undefined }; + }, + + toJSON(message: JobActionResponse): unknown { + const obj: any = {}; + if (message.action !== undefined) { + obj.action = Item2.toJSON(message.action); + } + return obj; + }, + + create(base?: DeepPartial): JobActionResponse { + return JobActionResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobActionResponse { + const message = createBaseJobActionResponse(); + message.action = (object.action !== undefined && object.action !== null) + ? Item2.fromPartial(object.action) + : undefined; + return message; + }, +}; + +function createBaseJobActionsRequest(): JobActionsRequest { + return { jobId: "", versionId: undefined }; +} + +export const JobActionsRequest: MessageFns = { + encode(message: JobActionsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.jobId !== "") { + writer.uint32(10).string(message.jobId); + } + if (message.versionId !== undefined) { + writer.uint32(18).string(message.versionId); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobActionsRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobActionsRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.versionId = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobActionsRequest { + return { + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + versionId: isSet(object.versionId) ? globalThis.String(object.versionId) : undefined, + }; + }, + + toJSON(message: JobActionsRequest): unknown { + const obj: any = {}; + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.versionId !== undefined) { + obj.versionId = message.versionId; + } + return obj; + }, + + create(base?: DeepPartial): JobActionsRequest { + return JobActionsRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobActionsRequest { + const message = createBaseJobActionsRequest(); + message.jobId = object.jobId ?? ""; + message.versionId = object.versionId ?? undefined; + return message; + }, +}; + +function createBaseJobActionsResponse(): JobActionsResponse { + return { actions: [] }; +} + +export const JobActionsResponse: MessageFns = { + encode(message: JobActionsResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.actions) { + Item2.encode(v!, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobActionsResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobActionsResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.actions.push(Item2.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobActionsResponse { + return { + actions: globalThis.Array.isArray(object?.actions) ? object.actions.map((e: any) => Item2.fromJSON(e)) : [], + }; + }, + + toJSON(message: JobActionsResponse): unknown { + const obj: any = {}; + if (message.actions?.length) { + obj.actions = message.actions.map((e) => Item2.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): JobActionsResponse { + return JobActionsResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobActionsResponse { + const message = createBaseJobActionsResponse(); + message.actions = object.actions?.map((e) => Item2.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseJobTriggerRequest(): JobTriggerRequest { + return { jobId: "", triggerId: "" }; +} + +export const JobTriggerRequest: MessageFns = { + encode(message: JobTriggerRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.jobId !== "") { + writer.uint32(10).string(message.jobId); + } + if (message.triggerId !== "") { + writer.uint32(18).string(message.triggerId); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobTriggerRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobTriggerRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.triggerId = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobTriggerRequest { + return { + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + triggerId: isSet(object.triggerId) ? globalThis.String(object.triggerId) : "", + }; + }, + + toJSON(message: JobTriggerRequest): unknown { + const obj: any = {}; + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.triggerId !== "") { + obj.triggerId = message.triggerId; + } + return obj; + }, + + create(base?: DeepPartial): JobTriggerRequest { + return JobTriggerRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobTriggerRequest { + const message = createBaseJobTriggerRequest(); + message.jobId = object.jobId ?? ""; + message.triggerId = object.triggerId ?? ""; + return message; + }, +}; + +function createBaseJobTriggerResponse(): JobTriggerResponse { + return { trigger: undefined }; +} + +export const JobTriggerResponse: MessageFns = { + encode(message: JobTriggerResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.trigger !== undefined) { + Item3.encode(message.trigger, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobTriggerResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobTriggerResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.trigger = Item3.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobTriggerResponse { + return { trigger: isSet(object.trigger) ? Item3.fromJSON(object.trigger) : undefined }; + }, + + toJSON(message: JobTriggerResponse): unknown { + const obj: any = {}; + if (message.trigger !== undefined) { + obj.trigger = Item3.toJSON(message.trigger); + } + return obj; + }, + + create(base?: DeepPartial): JobTriggerResponse { + return JobTriggerResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobTriggerResponse { + const message = createBaseJobTriggerResponse(); + message.trigger = (object.trigger !== undefined && object.trigger !== null) + ? Item3.fromPartial(object.trigger) + : undefined; + return message; + }, +}; + +function createBaseJobTriggersRequest(): JobTriggersRequest { + return { jobId: "", versionId: undefined }; +} + +export const JobTriggersRequest: MessageFns = { + encode(message: JobTriggersRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.jobId !== "") { + writer.uint32(10).string(message.jobId); + } + if (message.versionId !== undefined) { + writer.uint32(18).string(message.versionId); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobTriggersRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobTriggersRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.versionId = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobTriggersRequest { + return { + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : "", + versionId: isSet(object.versionId) ? globalThis.String(object.versionId) : undefined, + }; + }, + + toJSON(message: JobTriggersRequest): unknown { + const obj: any = {}; + if (message.jobId !== "") { + obj.jobId = message.jobId; + } + if (message.versionId !== undefined) { + obj.versionId = message.versionId; + } + return obj; + }, + + create(base?: DeepPartial): JobTriggersRequest { + return JobTriggersRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobTriggersRequest { + const message = createBaseJobTriggersRequest(); + message.jobId = object.jobId ?? ""; + message.versionId = object.versionId ?? undefined; + return message; + }, +}; + +function createBaseJobTriggersResponse(): JobTriggersResponse { + return { triggers: [] }; +} + +export const JobTriggersResponse: MessageFns = { + encode(message: JobTriggersResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.triggers) { + Item3.encode(v!, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): JobTriggersResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseJobTriggersResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.triggers.push(Item3.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): JobTriggersResponse { + return { + triggers: globalThis.Array.isArray(object?.triggers) ? object.triggers.map((e: any) => Item3.fromJSON(e)) : [], + }; + }, + + toJSON(message: JobTriggersResponse): unknown { + const obj: any = {}; + if (message.triggers?.length) { + obj.triggers = message.triggers.map((e) => Item3.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): JobTriggersResponse { + return JobTriggersResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): JobTriggersResponse { + const message = createBaseJobTriggersResponse(); + message.triggers = object.triggers?.map((e) => Item3.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseRunnerRequest(): RunnerRequest { + return { runnerId: "" }; +} + +export const RunnerRequest: MessageFns = { + encode(message: RunnerRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.runnerId !== "") { + writer.uint32(10).string(message.runnerId); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): RunnerRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseRunnerRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.runnerId = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): RunnerRequest { + return { runnerId: isSet(object.runnerId) ? globalThis.String(object.runnerId) : "" }; + }, + + toJSON(message: RunnerRequest): unknown { + const obj: any = {}; + if (message.runnerId !== "") { + obj.runnerId = message.runnerId; + } + return obj; + }, + + create(base?: DeepPartial): RunnerRequest { + return RunnerRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): RunnerRequest { + const message = createBaseRunnerRequest(); + message.runnerId = object.runnerId ?? ""; + return message; + }, +}; + +function createBaseRunnerResponse(): RunnerResponse { + return { runner: undefined }; +} + +export const RunnerResponse: MessageFns = { + encode(message: RunnerResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.runner !== undefined) { + Item4.encode(message.runner, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): RunnerResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseRunnerResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.runner = Item4.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): RunnerResponse { + return { runner: isSet(object.runner) ? Item4.fromJSON(object.runner) : undefined }; + }, + + toJSON(message: RunnerResponse): unknown { + const obj: any = {}; + if (message.runner !== undefined) { + obj.runner = Item4.toJSON(message.runner); + } + return obj; + }, + + create(base?: DeepPartial): RunnerResponse { + return RunnerResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): RunnerResponse { + const message = createBaseRunnerResponse(); + message.runner = (object.runner !== undefined && object.runner !== null) + ? Item4.fromPartial(object.runner) + : undefined; + return message; + }, +}; + +function createBaseRunnersRequest(): RunnersRequest { + return { jobId: undefined, versionId: undefined, actionId: undefined, status: undefined }; +} + +export const RunnersRequest: MessageFns = { + encode(message: RunnersRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.jobId !== undefined) { + writer.uint32(10).string(message.jobId); + } + if (message.versionId !== undefined) { + writer.uint32(18).string(message.versionId); + } + if (message.actionId !== undefined) { + writer.uint32(26).string(message.actionId); + } + if (message.status !== undefined) { + writer.uint32(32).int32(item_StatusToNumber(message.status)); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): RunnersRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseRunnersRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.jobId = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.versionId = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.actionId = reader.string(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.status = item_StatusFromJSON(reader.int32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): RunnersRequest { + return { + jobId: isSet(object.jobId) ? globalThis.String(object.jobId) : undefined, + versionId: isSet(object.versionId) ? globalThis.String(object.versionId) : undefined, + actionId: isSet(object.actionId) ? globalThis.String(object.actionId) : undefined, + status: isSet(object.status) ? item_StatusFromJSON(object.status) : undefined, + }; + }, + + toJSON(message: RunnersRequest): unknown { + const obj: any = {}; + if (message.jobId !== undefined) { + obj.jobId = message.jobId; + } + if (message.versionId !== undefined) { + obj.versionId = message.versionId; + } + if (message.actionId !== undefined) { + obj.actionId = message.actionId; + } + if (message.status !== undefined) { + obj.status = item_StatusToJSON(message.status); + } + return obj; + }, + + create(base?: DeepPartial): RunnersRequest { + return RunnersRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): RunnersRequest { + const message = createBaseRunnersRequest(); + message.jobId = object.jobId ?? undefined; + message.versionId = object.versionId ?? undefined; + message.actionId = object.actionId ?? undefined; + message.status = object.status ?? undefined; + return message; + }, +}; + +function createBaseRunnersResponse(): RunnersResponse { + return { runners: [] }; +} + +export const RunnersResponse: MessageFns = { + encode(message: RunnersResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.runners) { + Item4.encode(v!, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): RunnersResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseRunnersResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.runners.push(Item4.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): RunnersResponse { + return { + runners: globalThis.Array.isArray(object?.runners) ? object.runners.map((e: any) => Item4.fromJSON(e)) : [], + }; + }, + + toJSON(message: RunnersResponse): unknown { + const obj: any = {}; + if (message.runners?.length) { + obj.runners = message.runners.map((e) => Item4.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): RunnersResponse { + return RunnersResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): RunnersResponse { + const message = createBaseRunnersResponse(); + message.runners = object.runners?.map((e) => Item4.fromPartial(e)) || []; + return message; + }, +}; + +export type GeneralAPIDefinition = typeof GeneralAPIDefinition; +export const GeneralAPIDefinition = { + name: "GeneralAPI", + fullName: "GeneralAPI.GeneralAPI", + methods: { + getJob: { + name: "getJob", + requestType: JobRequest, + requestStream: false, + responseType: JobResponse, + responseStream: false, + options: {}, + }, + getJobs: { + name: "getJobs", + requestType: JobsRequest, + requestStream: false, + responseType: JobsResponse, + responseStream: false, + options: {}, + }, + getJobVersion: { + name: "getJobVersion", + requestType: JobVersionRequest, + requestStream: false, + responseType: JobVersionResponse, + responseStream: false, + options: {}, + }, + getJobVersions: { + name: "getJobVersions", + requestType: JobVersionsRequest, + requestStream: false, + responseType: JobVersionsResponse, + responseStream: false, + options: {}, + }, + getJobAction: { + name: "getJobAction", + requestType: JobActionRequest, + requestStream: false, + responseType: JobActionResponse, + responseStream: false, + options: {}, + }, + getJobActions: { + name: "getJobActions", + requestType: JobActionsRequest, + requestStream: false, + responseType: JobActionsResponse, + responseStream: false, + options: {}, + }, + getJobTrigger: { + name: "getJobTrigger", + requestType: JobTriggerRequest, + requestStream: false, + responseType: JobTriggerResponse, + responseStream: false, + options: {}, + }, + getJobTriggers: { + name: "getJobTriggers", + requestType: JobTriggersRequest, + requestStream: false, + responseType: JobTriggersResponse, + responseStream: false, + options: {}, + }, + getRunner: { + name: "getRunner", + requestType: RunnerRequest, + requestStream: false, + responseType: RunnerResponse, + responseStream: false, + options: {}, + }, + getRunners: { + name: "getRunners", + requestType: RunnersRequest, + requestStream: false, + responseType: RunnersResponse, + responseStream: false, + options: {}, + }, + }, +} as const; + +export interface GeneralAPIServiceImplementation { + getJob(request: JobRequest, context: CallContext & CallContextExt): Promise>; + getJobs(request: JobsRequest, context: CallContext & CallContextExt): Promise>; + getJobVersion( + request: JobVersionRequest, + context: CallContext & CallContextExt, + ): Promise>; + getJobVersions( + request: JobVersionsRequest, + context: CallContext & CallContextExt, + ): Promise>; + getJobAction( + request: JobActionRequest, + context: CallContext & CallContextExt, + ): Promise>; + getJobActions( + request: JobActionsRequest, + context: CallContext & CallContextExt, + ): Promise>; + getJobTrigger( + request: JobTriggerRequest, + context: CallContext & CallContextExt, + ): Promise>; + getJobTriggers( + request: JobTriggersRequest, + context: CallContext & CallContextExt, + ): Promise>; + getRunner(request: RunnerRequest, context: CallContext & CallContextExt): Promise>; + getRunners(request: RunnersRequest, context: CallContext & CallContextExt): Promise>; +} + +export interface GeneralAPIClient { + getJob(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + getJobs(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + getJobVersion( + request: DeepPartial, + options?: CallOptions & CallOptionsExt, + ): Promise; + getJobVersions( + request: DeepPartial, + options?: CallOptions & CallOptionsExt, + ): Promise; + getJobAction( + request: DeepPartial, + options?: CallOptions & CallOptionsExt, + ): Promise; + getJobActions( + request: DeepPartial, + options?: CallOptions & CallOptionsExt, + ): Promise; + getJobTrigger( + request: DeepPartial, + options?: CallOptions & CallOptionsExt, + ): Promise; + getJobTriggers( + request: DeepPartial, + options?: CallOptions & CallOptionsExt, + ): Promise; + getRunner(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + getRunners(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; +} + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create(base?: DeepPartial): T; + fromPartial(object: DeepPartial): T; +} diff --git a/packages/grpc/src/server.ts b/packages/grpc/src/toremove-general-api.ts similarity index 95% rename from packages/grpc/src/server.ts rename to packages/grpc/src/toremove-general-api.ts index 47f0ed4..dba332d 100644 --- a/packages/grpc/src/server.ts +++ b/packages/grpc/src/toremove-general-api.ts @@ -2,11 +2,12 @@ // versions: // protoc-gen-ts_proto v2.10.1 // protoc v3.21.12 -// source: server.proto +// source: toremove-general-api.proto /* eslint-disable */ import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; import type { CallContext, CallOptions } from "nice-grpc-common"; +import { Empty, ExportChunk } from "./base.js"; export const protobufPackage = "Server"; @@ -179,15 +180,6 @@ export function runnerStatusToNumber(object: RunnerStatus): number { } } -export interface Empty { -} - -export interface ExportChunk { - id: string; - sequence: number; - data: Uint8Array; -} - export interface GetJobRequest { id: string; } @@ -459,141 +451,6 @@ export function initResponse_StatusToNumber(object: InitResponse_Status): number } } -function createBaseEmpty(): Empty { - return {}; -} - -export const Empty: MessageFns = { - encode(_: Empty, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { - return writer; - }, - - decode(input: BinaryReader | Uint8Array, length?: number): Empty { - const reader = input instanceof BinaryReader ? input : new BinaryReader(input); - const end = length === undefined ? reader.len : reader.pos + length; - const message = createBaseEmpty(); - while (reader.pos < end) { - const tag = reader.uint32(); - switch (tag >>> 3) { - } - if ((tag & 7) === 4 || tag === 0) { - break; - } - reader.skip(tag & 7); - } - return message; - }, - - fromJSON(_: any): Empty { - return {}; - }, - - toJSON(_: Empty): unknown { - const obj: any = {}; - return obj; - }, - - create(base?: DeepPartial): Empty { - return Empty.fromPartial(base ?? {}); - }, - fromPartial(_: DeepPartial): Empty { - const message = createBaseEmpty(); - return message; - }, -}; - -function createBaseExportChunk(): ExportChunk { - return { id: "", sequence: 0, data: new Uint8Array(0) }; -} - -export const ExportChunk: MessageFns = { - encode(message: ExportChunk, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { - if (message.id !== "") { - writer.uint32(10).string(message.id); - } - if (message.sequence !== 0) { - writer.uint32(16).int64(message.sequence); - } - if (message.data.length !== 0) { - writer.uint32(26).bytes(message.data); - } - return writer; - }, - - decode(input: BinaryReader | Uint8Array, length?: number): ExportChunk { - const reader = input instanceof BinaryReader ? input : new BinaryReader(input); - const end = length === undefined ? reader.len : reader.pos + length; - const message = createBaseExportChunk(); - while (reader.pos < end) { - const tag = reader.uint32(); - switch (tag >>> 3) { - case 1: { - if (tag !== 10) { - break; - } - - message.id = reader.string(); - continue; - } - case 2: { - if (tag !== 16) { - break; - } - - message.sequence = longToNumber(reader.int64()); - continue; - } - case 3: { - if (tag !== 26) { - break; - } - - message.data = reader.bytes(); - continue; - } - } - if ((tag & 7) === 4 || tag === 0) { - break; - } - reader.skip(tag & 7); - } - return message; - }, - - fromJSON(object: any): ExportChunk { - return { - id: isSet(object.id) ? globalThis.String(object.id) : "", - sequence: isSet(object.sequence) ? globalThis.Number(object.sequence) : 0, - data: isSet(object.data) ? bytesFromBase64(object.data) : new Uint8Array(0), - }; - }, - - toJSON(message: ExportChunk): unknown { - const obj: any = {}; - if (message.id !== "") { - obj.id = message.id; - } - if (message.sequence !== 0) { - obj.sequence = Math.round(message.sequence); - } - if (message.data.length !== 0) { - obj.data = base64FromBytes(message.data); - } - return obj; - }, - - create(base?: DeepPartial): ExportChunk { - return ExportChunk.fromPartial(base ?? {}); - }, - fromPartial(object: DeepPartial): ExportChunk { - const message = createBaseExportChunk(); - message.id = object.id ?? ""; - message.sequence = object.sequence ?? 0; - message.data = object.data ?? new Uint8Array(0); - return message; - }, -}; - function createBaseGetJobRequest(): GetJobRequest { return { id: "" }; } @@ -3038,10 +2895,10 @@ export const InitResponse: MessageFns = { }, }; -export type GeneralManagementDefinition = typeof GeneralManagementDefinition; -export const GeneralManagementDefinition = { - name: "GeneralManagement", - fullName: "Server.GeneralManagement", +export type GeneralAPIDefinition = typeof GeneralAPIDefinition; +export const GeneralAPIDefinition = { + name: "GeneralAPI", + fullName: "Server.GeneralAPI", methods: { /** General */ getJobs: { @@ -3137,7 +2994,7 @@ export const GeneralManagementDefinition = { }, } as const; -export interface GeneralManagementServiceImplementation { +export interface GeneralAPIServiceImplementation { /** General */ getJobs(request: Empty, context: CallContext & CallContextExt): ServerStreamingMethodResult>; getJob(request: GetJobRequest, context: CallContext & CallContextExt): Promise>; @@ -3172,7 +3029,7 @@ export interface GeneralManagementServiceImplementation { ): Promise>; } -export interface GeneralManagementClient { +export interface GeneralAPIClient { /** General */ getJobs(request: DeepPartial, options?: CallOptions & CallOptionsExt): AsyncIterable; getJob(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; diff --git a/packages/grpc/src/runner.ts b/packages/grpc/src/toremove-runner.ts similarity index 99% rename from packages/grpc/src/runner.ts rename to packages/grpc/src/toremove-runner.ts index 3433ca5..5d60497 100644 --- a/packages/grpc/src/runner.ts +++ b/packages/grpc/src/toremove-runner.ts @@ -2,7 +2,7 @@ // versions: // protoc-gen-ts_proto v2.10.1 // protoc v3.21.12 -// source: runner.proto +// source: toremove-runner.proto /* eslint-disable */ import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; diff --git a/packages/grpc/tsconfig.json b/packages/grpc/tsconfig.json index 0ac97f1..cd126f1 100644 --- a/packages/grpc/tsconfig.json +++ b/packages/grpc/tsconfig.json @@ -11,11 +11,12 @@ "forceConsistentCasingInFileNames": true, "declaration": true, "types": ["node"], + "rootDir": "./src", "outDir": "./dist", + "paths":{ + "~/*": ["./src/*"] + } }, - "include": [ - "./src" - ], "$schema": "https://json.schemastore.org/tsconfig", "display": "Recommended" } \ No newline at end of file diff --git a/packages/runner-node-entrypoint/tsconfig.json b/packages/runner-node-entrypoint/tsconfig.json index 5cfa7ab..cd126f1 100644 --- a/packages/runner-node-entrypoint/tsconfig.json +++ b/packages/runner-node-entrypoint/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "experimentalDecorators": true, "inlineSourceMap": true, "target": "ES2022", "module": "NodeNext", @@ -10,14 +11,12 @@ "forceConsistentCasingInFileNames": true, "declaration": true, "types": ["node"], + "rootDir": "./src", "outDir": "./dist", "paths":{ "~/*": ["./src/*"] } }, - "include": [ - "./src" - ], "$schema": "https://json.schemastore.org/tsconfig", "display": "Recommended" } \ No newline at end of file diff --git a/packages/server/drizzle/0010_milky_microbe.sql b/packages/server/drizzle/0010_milky_microbe.sql new file mode 100644 index 0000000..5f9947e --- /dev/null +++ b/packages/server/drizzle/0010_milky_microbe.sql @@ -0,0 +1,40 @@ +CREATE TABLE "auditLog" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "subject" jsonb NOT NULL, + "entry" jsonb NOT NULL, + "created" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "oauthServiceClient" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "is_system_managed" boolean DEFAULT false NOT NULL, + "name" varchar(255) NOT NULL, + "description" text, + "clientId" varchar(255) NOT NULL, + "metadata" jsonb NOT NULL, + "allowedAudiences" jsonb NOT NULL, + "allowedScopes" jsonb NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "expiresAt" timestamp, + "createdAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "oauthSigningKey" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "parent_id" uuid, + "child_id" uuid, + "created_by_user_id" uuid, + "status" varchar(255) NOT NULL, + "alg" varchar(255) NOT NULL, + "use" varchar(255) NOT NULL, + "private_key_encrypted" text NOT NULL, + "public_key" text NOT NULL, + "expiresAt" timestamp, + "renewsAt" timestamp, + "createdAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "actions" ALTER COLUMN "runnerImage" SET DEFAULT 'node24';--> statement-breakpoint +ALTER TABLE "oauthSigningKey" ADD CONSTRAINT "oauthSigningKey_parent_id_oauthSigningKey_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."oauthSigningKey"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "oauthSigningKey" ADD CONSTRAINT "oauthSigningKey_child_id_oauthSigningKey_id_fk" FOREIGN KEY ("child_id") REFERENCES "public"."oauthSigningKey"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "oauthSigningKey" ADD CONSTRAINT "oauthSigningKey_created_by_user_id_users_id_fk" FOREIGN KEY ("created_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/packages/server/drizzle/meta/0010_snapshot.json b/packages/server/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..5003cfb --- /dev/null +++ b/packages/server/drizzle/meta/0010_snapshot.json @@ -0,0 +1,1114 @@ +{ + "id": "27149207-986a-408b-9532-d90a800afdcc", + "prevId": "367b35f4-8ff6-43e1-a52e-51e1a76978b1", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.actions": { + "name": "actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "jobId": { + "name": "jobId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "jobVersionId": { + "name": "jobVersionId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "runnerImage": { + "name": "runnerImage", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'node24'" + }, + "runnerAsynchronous": { + "name": "runnerAsynchronous", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "runnerMinCount": { + "name": "runnerMinCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "runnerMaxCount": { + "name": "runnerMaxCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 16 + }, + "runnerTimeout": { + "name": "runnerTimeout", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 60 + }, + "runnerMaxIdleAge": { + "name": "runnerMaxIdleAge", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "runnerMaxAge": { + "name": "runnerMaxAge", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 900 + }, + "runnerMaxAgeHard": { + "name": "runnerMaxAgeHard", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 960 + }, + "runnerDockerArguments": { + "name": "runnerDockerArguments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "runnerMode": { + "name": "runnerMode", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'standard'" + } + }, + "indexes": {}, + "foreignKeys": { + "actions_jobId_jobs_id_fk": { + "name": "actions_jobId_jobs_id_fk", + "tableFrom": "actions", + "tableTo": "jobs", + "columnsFrom": [ + "jobId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "actions_jobVersionId_job-versions_id_fk": { + "name": "actions_jobVersionId_job-versions_id_fk", + "tableFrom": "actions", + "tableTo": "job-versions", + "columnsFrom": [ + "jobVersionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apiTokens": { + "name": "apiTokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token": { + "name": "token", + "type": "varchar(70)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'enabled'" + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "apiTokens_userId_users_id_fk": { + "name": "apiTokens_userId_users_id_fk", + "tableFrom": "apiTokens", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "apiTokens_token_unique": { + "name": "apiTokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auditLog": { + "name": "auditLog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subject": { + "name": "subject", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entry": { + "name": "entry", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "jobId": { + "name": "jobId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "modified": { + "name": "modified", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "environments_jobId_jobs_id_fk": { + "name": "environments_jobId_jobs_id_fk", + "tableFrom": "environments", + "tableTo": "jobs", + "columnsFrom": [ + "jobId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environments_jobId_unique": { + "name": "environments_jobId_unique", + "nullsNotDistinct": false, + "columns": [ + "jobId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job-versions": { + "name": "job-versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "jobId": { + "name": "jobId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "modified": { + "name": "modified", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "job-versions_jobId_jobs_id_fk": { + "name": "job-versions_jobId_jobs_id_fk", + "tableFrom": "job-versions", + "tableTo": "jobs", + "columnsFrom": [ + "jobId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "job-versions_jobId_version_unique": { + "name": "job-versions_jobId_version_unique", + "nullsNotDistinct": false, + "columns": [ + "jobId", + "version" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jobs": { + "name": "jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "jobName": { + "name": "jobName", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jobVersionId": { + "name": "jobVersionId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false, + "default": "'enabled'" + }, + "links": { + "name": "links", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "jobs_jobVersionId_job-versions_id_fk": { + "name": "jobs_jobVersionId_job-versions_id_fk", + "tableFrom": "jobs", + "tableTo": "job-versions", + "columnsFrom": [ + "jobVersionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "jobs_jobName_unique": { + "name": "jobs_jobName_unique", + "nullsNotDistinct": false, + "columns": [ + "jobName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lock": { + "name": "lock", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "lockKey": { + "name": "lockKey", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "NOW() + INTERVAL '5 minutes'" + }, + "created": { + "name": "created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "NOW()" + }, + "modified": { + "name": "modified", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "NOW()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "lock_lockKey_unique": { + "name": "lock_lockKey_unique", + "nullsNotDistinct": false, + "columns": [ + "lockKey" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.logs": { + "name": "logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "jobId": { + "name": "jobId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actionId": { + "name": "actionId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "sort": { + "name": "sort", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created": { + "name": "created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "jobId_created_idx": { + "name": "jobId_created_idx", + "columns": [ + { + "expression": "jobId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauthServiceClient": { + "name": "oauthServiceClient", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "is_system_managed": { + "name": "is_system_managed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "allowedAudiences": { + "name": "allowedAudiences", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "allowedScopes": { + "name": "allowedScopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauthSigningKey": { + "name": "oauthSigningKey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "child_id": { + "name": "child_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alg": { + "name": "alg", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "use": { + "name": "use", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "private_key_encrypted": { + "name": "private_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "renewsAt": { + "name": "renewsAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "oauthSigningKey_parent_id_oauthSigningKey_id_fk": { + "name": "oauthSigningKey_parent_id_oauthSigningKey_id_fk", + "tableFrom": "oauthSigningKey", + "tableTo": "oauthSigningKey", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauthSigningKey_child_id_oauthSigningKey_id_fk": { + "name": "oauthSigningKey_child_id_oauthSigningKey_id_fk", + "tableFrom": "oauthSigningKey", + "tableTo": "oauthSigningKey", + "columnsFrom": [ + "child_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauthSigningKey_created_by_user_id_users_id_fk": { + "name": "oauthSigningKey_created_by_user_id_users_id_fk", + "tableFrom": "oauthSigningKey", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token": { + "name": "token", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_userId_users_id_fk": { + "name": "sessions_userId_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.store": { + "name": "store", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "jobId": { + "name": "jobId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "storeKey": { + "name": "storeKey", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "storeValue": { + "name": "storeValue", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiry": { + "name": "expiry", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "modified": { + "name": "modified", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "store_jobId_jobs_id_fk": { + "name": "store_jobId_jobs_id_fk", + "tableFrom": "store", + "tableTo": "jobs", + "columnsFrom": [ + "jobId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "store_jobId_storeKey_unique": { + "name": "store_jobId_storeKey_unique", + "nullsNotDistinct": false, + "columns": [ + "jobId", + "storeKey" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.triggers": { + "name": "triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "jobId": { + "name": "jobId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "jobVersionId": { + "name": "jobVersionId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "triggers_jobId_jobs_id_fk": { + "name": "triggers_jobId_jobs_id_fk", + "tableFrom": "triggers", + "tableTo": "jobs", + "columnsFrom": [ + "jobId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "triggers_jobVersionId_job-versions_id_fk": { + "name": "triggers_jobVersionId_job-versions_id_fk", + "tableFrom": "triggers", + "tableTo": "job-versions", + "columnsFrom": [ + "jobVersionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usernameUniqueIndex": { + "name": "usernameUniqueIndex", + "columns": [ + { + "expression": "lower(\"username\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/server/drizzle/meta/_journal.json b/packages/server/drizzle/meta/_journal.json index 6e78cff..eeee2d2 100644 --- a/packages/server/drizzle/meta/_journal.json +++ b/packages/server/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1756893257221, "tag": "0009_short_zuras", "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1770445177561, + "tag": "0010_milky_microbe", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/server/src/bouncer.ts b/packages/server/src/bouncer.ts index 2a308ec..6fc2f8b 100644 --- a/packages/server/src/bouncer.ts +++ b/packages/server/src/bouncer.ts @@ -75,21 +75,21 @@ export class Bouncer { public canReadJobEnvironment( environment: { jobId: string }, - name: string + name: string, ): boolean { return this.can(`job/${environment.jobId}/environment/${name}`, "read"); } public canWriteJobEnvironment( environment: { jobId: string }, - name: string + name: string, ): boolean { return this.can(`job/${environment.jobId}/environment/${name}`, "write"); } public canDeleteJobEnvironment( environment: { jobId: string }, - name: string + name: string, ): boolean { return this.can(`job/${environment.jobId}/environment/${name}`, "delete"); } @@ -222,6 +222,46 @@ export class Bouncer { return this.can(`grpc/runner-jwt`, "write"); } + public canReadOauthServiceClientGenerally(): boolean { + return this.can(`oauth/service-client`, "read"); + } + + public canWriteOauthServiceClientGenerally(): boolean { + return this.can(`oauth/service-client`, "read"); + } + + public canReadOauthServiceClient(serviceClient: { id: string }): boolean { + return this.can(`oauth/service-client/${serviceClient.id}`, "read"); + } + + public canWriteOauthServiceClient(serviceClient: { id: string }): boolean { + return this.can(`oauth/service-client/${serviceClient.id}`, "write"); + } + + public canDeleteOauthServiceClient(serviceClient: { id: string }): boolean { + return this.can(`oauth/service-client/${serviceClient.id}`, "delete"); + } + + public canReadOauthSigningKeyGenerally(): boolean { + return this.can(`oauth/signing-key`, "read"); + } + + public canWriteOauthSigningKeyGenerally(): boolean { + return this.can(`oauth/signing-key`, "write"); + } + + public canReadOauthSigningKey(signingKey: { id: string }): boolean { + return this.can(`oauth/signing-key/${signingKey.id}`, "read"); + } + + public canWriteOauthSigningKey(signingKey: { id: string }): boolean { + return this.can(`oauth/signing-key/${signingKey.id}`, "write"); + } + + public canDeleteOauthSigningKey(signingKey: { id: string }): boolean { + return this.can(`oauth/signing-key/${signingKey.id}`, "delete"); + } + public get type() { return this.options.type; } diff --git a/packages/server/src/config.ts b/packages/server/src/config.ts index 7ac6303..c8df8d4 100644 --- a/packages/server/src/config.ts +++ b/packages/server/src/config.ts @@ -1,8 +1,9 @@ import { hostname } from "os"; import { z } from "zod"; -import { apiTokensTable } from "./db/schema/api-tokens.js"; export const ConfigurationOptionsSchema = z.object({ + SECRET_PASSPHRASE: z.string().min(32).max(512), + DATABASE_URL: z.string(), DATABASE_BACKUP_SCHEDULE: z.string().default("0 0 * * *"), DATABASE_BACKUP_SCHEDULE_TIMEZONE: z.string().default("UTC"), @@ -32,6 +33,20 @@ export const ConfigurationOptionsSchema = z.object({ .pipe(z.boolean()) .default("true"), + API_URL: z.string(), + + OAUTH_ISSUER: z.string().default("http://localhost:5211"), + OAUTH_SIGNING_KEY_ROTATE_IN_DAYS: z.coerce.number().default(5), // Rotate X days after creation + OAUTH_SIGNING_KEY_EXPIRE_IN_DAYS: z.coerce.number().default(30), // Expire X days after creation + OAUTH_MANAGEMENT_ALLOW_MANUAL_UPLOAD: z + .string() + .transform((val) => val.toLowerCase() === "true") + .pipe(z.boolean()) + .default("false") + .describe( + "Determines whether or not you can manually upload signing keys through the API. This includes frontend and backend.", + ), + DEBUG_HTTP: z .string() .transform((val) => val.toLowerCase() === "true") @@ -76,8 +91,8 @@ export const ConfigurationOptionsSchema = z.object({ "labels", "memoryLimit", "directPassthroughArguments", - ]) - ) + ]), + ), ) .default(""), @@ -120,7 +135,7 @@ export const ConfigurationOptionsSchema = z.object({ .min(1) .default(15) .describe( - "The step in seconds for the Prometheus query. Default is 15 seconds." + "The step in seconds for the Prometheus query. Default is 15 seconds.", ), }); @@ -131,7 +146,7 @@ export type ConfigurationOptionsSchemaType = z.infer< export type ConfigurationOptions = keyof ConfigurationOptionsSchemaType; export const getConfigOption = ( - option: T + option: T, ): ConfigurationOptionsSchemaType[T] => { const schema = ConfigurationOptionsSchema.shape[option]; diff --git a/packages/server/src/db/actions.ts b/packages/server/src/db/actions.ts new file mode 100644 index 0000000..d54a4ab --- /dev/null +++ b/packages/server/src/db/actions.ts @@ -0,0 +1,24 @@ +import { eq } from "drizzle-orm"; +import { getDrizzle } from "./index.js"; +import { actionsTable } from "./schema/actions.js"; + +async function byId(id: string) { + const action = await getDrizzle() + .select() + .from(actionsTable) + .where(eq(actionsTable.id, id)) + .limit(1) + .then((res) => res.at(0)); + + return action; +} + +async function all() { + const actions = await getDrizzle().select().from(actionsTable); + return actions; +} + +export const actionsModel = { + byId, + all, +}; diff --git a/packages/server/src/db/audit-log.ts b/packages/server/src/db/audit-log.ts new file mode 100644 index 0000000..daff5df --- /dev/null +++ b/packages/server/src/db/audit-log.ts @@ -0,0 +1,72 @@ +import { eq } from "drizzle-orm"; +import { getDrizzle } from "./index.js"; +import { auditLogTable } from "./schema/audit-log.js"; +import { AuditEntry } from "./schema/audit-log.js"; + +async function byId(id: string) { + const auditLog = await getDrizzle() + .select() + .from(auditLogTable) + .where(eq(auditLogTable.id, id)) + .limit(1) + .then((res) => res.at(0)); + + return auditLog; +} + +async function createUserLog(userId: string, entry: AuditEntry) { + const auditLog = await getDrizzle() + .insert(auditLogTable) + .values({ + subject: { + type: "user", + userId, + }, + entry, + }) + .returning() + .then((res) => res.at(0)); + + return auditLog; +} + +async function createServiceClientLog( + serviceClientId: string, + entry: AuditEntry, +) { + const auditLog = await getDrizzle() + .insert(auditLogTable) + .values({ + subject: { + type: "service-client", + serviceClientId, + }, + entry, + }) + .returning() + .then((res) => res.at(0)); + + return auditLog; +} + +async function createSystemLog(entry: AuditEntry) { + const auditLog = await getDrizzle() + .insert(auditLogTable) + .values({ + subject: { + type: "system", + }, + entry, + }) + .returning() + .then((res) => res.at(0)); + + return auditLog; +} + +export const auditLogsModel = { + byId, + createUserLog, + createServiceClientLog, + createSystemLog, +}; diff --git a/packages/server/src/db/job-versions.ts b/packages/server/src/db/job-versions.ts new file mode 100644 index 0000000..72fefbc --- /dev/null +++ b/packages/server/src/db/job-versions.ts @@ -0,0 +1,32 @@ +import { and, eq } from "drizzle-orm"; +import { getDrizzle } from "./index.js"; +import { jobVersionsTable } from "./schema/job-versions.js"; + +async function byId(id: string) { + const jobVersion = await getDrizzle() + .select() + .from(jobVersionsTable) + .where(eq(jobVersionsTable.id, id)) + .limit(1) + .then((res) => res.at(0)); + + return jobVersion; +} + +async function all(constraints: { jobId?: string } = {}) { + const conditions = Object.entries(constraints).map(([key, value]) => + eq((jobVersionsTable as any)[key], value), + ); + + const jobVersions = await getDrizzle() + .select() + .from(jobVersionsTable) + .where(and(...conditions)); + + return jobVersions; +} + +export const jobVersionsModel = { + byId, + all, +}; diff --git a/packages/server/src/db/job.ts b/packages/server/src/db/job.ts index b5a4781..0cc02dc 100644 --- a/packages/server/src/db/job.ts +++ b/packages/server/src/db/job.ts @@ -13,6 +13,12 @@ async function byId(id: string) { return job; } +async function all() { + const jobs = await getDrizzle().select().from(jobsTable); + return jobs; +} + export const jobModel = { byId, + all, }; diff --git a/packages/server/src/db/oauth-service-client.ts b/packages/server/src/db/oauth-service-client.ts new file mode 100644 index 0000000..4f3384e --- /dev/null +++ b/packages/server/src/db/oauth-service-client.ts @@ -0,0 +1,52 @@ +import { eq } from "drizzle-orm"; +import { getDrizzle } from "./index.js"; +import { + oauthServiceClientTable, + OauthServiceClientTableInsertType, +} from "./schema/oauth-service-client.js"; + +async function byId(id: string) { + const serviceClient = await getDrizzle() + .select() + .from(oauthServiceClientTable) + .where(eq(oauthServiceClientTable.id, id)) + .limit(1) + .then((res) => res.at(0)); + + return serviceClient; +} + +async function byClientId(clientId: string) { + const serviceClient = await getDrizzle() + .select() + .from(oauthServiceClientTable) + .where(eq(oauthServiceClientTable.clientId, clientId)) + .limit(1) + .then((res) => res.at(0)); + + return serviceClient; +} + +async function create(serviceClient: OauthServiceClientTableInsertType) { + const createdServiceClient = await getDrizzle() + .insert(oauthServiceClientTable) + .values(serviceClient) + .returning() + .then((res) => res.at(0)); + + return createdServiceClient; +} + +async function all() { + const serviceClients = await getDrizzle() + .select() + .from(oauthServiceClientTable); + return serviceClients; +} + +export const oauthServiceClientModel = { + byId, + byClientId, + all, + create, +}; diff --git a/packages/server/src/db/oauth-signing-key.ts b/packages/server/src/db/oauth-signing-key.ts new file mode 100644 index 0000000..fdc30bd --- /dev/null +++ b/packages/server/src/db/oauth-signing-key.ts @@ -0,0 +1,133 @@ +import { + and, + count, + desc, + eq, + gte, + inArray, + isNull, + lte, + or, +} from "drizzle-orm"; +import { getDrizzle } from "./index.js"; +import { + oauthSigningKeyTable, + OauthSigningKeyTableInsertType, + OauthSigningKeyTableType, +} from "./schema/oauth-signing-key.js"; + +async function byId(id: string) { + const item = await getDrizzle() + .select() + .from(oauthSigningKeyTable) + .where(eq(oauthSigningKeyTable.id, id)) + .limit(1) + .then((res) => res.at(0)); + + return item; +} + +async function create(item: OauthSigningKeyTableInsertType) { + const createdItem = await getDrizzle() + .insert(oauthSigningKeyTable) + .values(item) + .returning() + .then((res) => res.at(0)); + + return createdItem; +} + +async function all() { + const items = await getDrizzle().select().from(oauthSigningKeyTable); + return items; +} + +async function getValidKeys() { + const items = await getDrizzle() + .select() + .from(oauthSigningKeyTable) + .where( + and( + inArray(oauthSigningKeyTable.status, ["active", "retiring"]), + or( + gte(oauthSigningKeyTable.expiresAt, new Date()), + isNull(oauthSigningKeyTable.expiresAt), + ), + ), + ); + + return items; +} + +async function getValidKey() { + const item = await getDrizzle() + .select() + .from(oauthSigningKeyTable) + .where( + and( + inArray(oauthSigningKeyTable.status, ["active", "retiring"]), + or( + gte(oauthSigningKeyTable.expiresAt, new Date()), + isNull(oauthSigningKeyTable.expiresAt), + ), + ), + ) + .orderBy(desc(oauthSigningKeyTable.createdAt)) + .limit(1) + .then((res) => res.at(0)); + + return item; +} + +async function update( + id: string, + data: Partial, +) { + await getDrizzle() + .update(oauthSigningKeyTable) + .set(data) + .where(eq(oauthSigningKeyTable.id, id)); +} + +async function paginate( + page: number, + pageSize: number, + filters?: Partial>, +) { + const whereClauses = []; + + if (filters?.status) { + whereClauses.push(eq(oauthSigningKeyTable.status, filters.status)); + } + + const items = await getDrizzle() + .select() + .from(oauthSigningKeyTable) + .where(and(...whereClauses)) + .orderBy(desc(oauthSigningKeyTable.createdAt)) + .limit(pageSize) + .offset((page - 1) * pageSize); + + const totalItems = await getDrizzle() + .select({ count: count(oauthSigningKeyTable.id) }) + .from(oauthSigningKeyTable) + .where(and(...whereClauses)) + .then((res) => res.at(0)?.count ?? 0); + + return { + items, + totalItems, + totalPages: Math.ceil(totalItems / pageSize), + currentPage: page, + }; +} + +export const oauthSigningKeyModel = { + all, + byId, + create, + getValidKey, + getValidKeys, + paginate, + update, +}; diff --git a/packages/server/src/db/schema/audit-log.ts b/packages/server/src/db/schema/audit-log.ts new file mode 100644 index 0000000..4fd9739 --- /dev/null +++ b/packages/server/src/db/schema/audit-log.ts @@ -0,0 +1,62 @@ +import { jsonb, pgTable, timestamp, uuid } from "drizzle-orm/pg-core"; + +export type AuditSubject = + | { + type: "user"; + userId: string; + } + | { + type: "service-client"; + serviceClientId: string; + } + | { + type: "system"; + }; + +export type AuditEntry = + | { + type: "generic"; + message: string; + } + | { + type: "oauth-invalid-client-id"; + clientId: string; + } + | { + type: "oauth-invalid-client-secret"; + clientId: string; + } + | { + type: "oauth-disabled-client"; + clientId: string; + } + | { + type: "oauth-expired-client"; + clientId: string; + } + | { + type: "oauth-unsupported-grant-type"; + clientId: string; + grantType: string; + } + | { + type: "oauth-rate-limited"; + clientId: string; + reason: "global" | "client-id" | "ip"; + } + | { + type: "oauth-valid-client"; + clientId: string; + }; + +export const auditLogTable = pgTable("auditLog", { + id: uuid("id").primaryKey().defaultRandom().notNull(), + + subject: jsonb("subject").$type().notNull(), + entry: jsonb("entry").$type().notNull(), + + created: timestamp().defaultNow().notNull(), +}); + +export type AuditLogTableType = typeof auditLogTable.$inferSelect; +export type AuditLogTableInsertType = typeof auditLogTable.$inferInsert; diff --git a/packages/server/src/db/schema/jwt-keys.ts b/packages/server/src/db/schema/jwt-keys.ts deleted file mode 100644 index 3919c0b..0000000 --- a/packages/server/src/db/schema/jwt-keys.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { pgTable, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core"; - -export const jwtKeysTable = pgTable("jwtKey", { - id: uuid("id").primaryKey().defaultRandom().notNull(), - - privateKey: text().notNull(), - publicKey: text().notNull(), - - status: varchar({ - length: 16, - enum: ["enabled", "disabled"], - }) - .notNull() - .default("enabled"), - - expires: timestamp().notNull(), - created: timestamp().defaultNow().notNull(), -}); - -export type JwtKeysTableType = typeof jwtKeysTable.$inferSelect; -export type JwtKeysTableInsertType = typeof jwtKeysTable.$inferInsert; diff --git a/packages/server/src/db/schema/oauth-service-client.ts b/packages/server/src/db/schema/oauth-service-client.ts new file mode 100644 index 0000000..5e8e949 --- /dev/null +++ b/packages/server/src/db/schema/oauth-service-client.ts @@ -0,0 +1,49 @@ +import { + boolean, + jsonb, + pgTable, + text, + timestamp, + uuid, + varchar, +} from "drizzle-orm/pg-core"; + +type ServiceClientMetadataClientSecretBasic = { + type: "client_secret_basic"; + clientSecretHashed: string; +}; + +type ServiceClientMetadataPrivateKeyJwt = { + type: "private_key_jwt"; + publicKey: string; +}; + +type ServiceClientMetadata = + | ServiceClientMetadataClientSecretBasic + | ServiceClientMetadataPrivateKeyJwt; + +export const oauthServiceClientTable = pgTable("oauthServiceClient", { + id: uuid("id").primaryKey().defaultRandom().notNull(), + + isSystemManaged: boolean("is_system_managed").notNull().default(false), + + name: varchar("name", { length: 255 }).notNull(), + description: text("description"), + + clientId: varchar("clientId", { length: 255 }).notNull(), + + metadata: jsonb("metadata").$type().notNull(), + + allowedAudiences: jsonb("allowedAudiences").$type().notNull(), + allowedScopes: jsonb("allowedScopes").$type().notNull(), + + enabled: boolean("enabled").default(true).notNull(), + + expiresAt: timestamp(), + createdAt: timestamp().defaultNow().notNull(), +}); + +export type OauthServiceClientTableType = + typeof oauthServiceClientTable.$inferSelect; +export type OauthServiceClientTableInsertType = + typeof oauthServiceClientTable.$inferInsert; diff --git a/packages/server/src/db/schema/oauth-signing-key.ts b/packages/server/src/db/schema/oauth-signing-key.ts new file mode 100644 index 0000000..e7c9a79 --- /dev/null +++ b/packages/server/src/db/schema/oauth-signing-key.ts @@ -0,0 +1,50 @@ +import { + boolean, + jsonb, + PgColumn, + pgTable, + text, + timestamp, + uuid, + varchar, +} from "drizzle-orm/pg-core"; +import { usersTable } from "./users.js"; + +export const oauthSigningKeyTable = pgTable("oauthSigningKey", { + id: uuid("id").primaryKey().defaultRandom().notNull(), + parentId: uuid("parent_id").references( + (): PgColumn => oauthSigningKeyTable.id, + { + onDelete: "set null", + }, + ), + childId: uuid("child_id").references( + (): PgColumn => oauthSigningKeyTable.id, + { + onDelete: "set null", + }, + ), + + createdByUserId: uuid("created_by_user_id").references(() => usersTable.id, { + onDelete: "set null", + }), + + status: varchar("status", { + length: 255, + enum: ["active", "retiring", "inactive"], + }).notNull(), + + alg: varchar("alg", { length: 255, enum: ["RS256"] }).notNull(), + use: varchar("use", { length: 255, enum: ["sig", "enc"] }).notNull(), + + privateKeyEncrypted: text("private_key_encrypted").notNull(), + publicKey: text("public_key").notNull(), + + expiresAt: timestamp(), + renewsAt: timestamp(), + createdAt: timestamp().defaultNow().notNull(), +}); + +export type OauthSigningKeyTableType = typeof oauthSigningKeyTable.$inferSelect; +export type OauthSigningKeyTableInsertType = + typeof oauthSigningKeyTable.$inferInsert; diff --git a/packages/server/src/db/triggers.ts b/packages/server/src/db/triggers.ts new file mode 100644 index 0000000..a863fb2 --- /dev/null +++ b/packages/server/src/db/triggers.ts @@ -0,0 +1,34 @@ +import { and, eq } from "drizzle-orm"; +import { getDrizzle } from "./index.js"; +import { triggersTable } from "./schema/triggers.js"; + +async function byId(id: string) { + const trigger = await getDrizzle() + .select() + .from(triggersTable) + .where(eq(triggersTable.id, id)) + .limit(1) + .then((res) => res.at(0)); + + return trigger; +} + +async function all( + constraints: Partial<{ jobId: string; jobVersionId: string }> = {}, +) { + const conditions = Object.entries(constraints).map(([key, value]) => + eq((triggersTable as any)[key], value), + ); + + const triggers = await getDrizzle() + .select() + .from(triggersTable) + .where(and(...conditions)); + + return triggers; +} + +export const triggersModel = { + byId, + all, +}; diff --git a/packages/server/src/grpc/index.ts b/packages/server/src/grpc/index.ts index 7d7c9ce..bbd4a09 100644 --- a/packages/server/src/grpc/index.ts +++ b/packages/server/src/grpc/index.ts @@ -1,5 +1,5 @@ import { LoopBase } from "@jobber/common"; -import { GeneralManagementDefinition } from "@jobber/grpc/server.js"; +import { GeneralAPIDefinition } from "@jobber/grpc/general.js"; import { CallContext, createServer, @@ -15,17 +15,29 @@ import { apiTokensModel } from "~/db/api-tokens.js"; import { SignJWT } from "jose"; import { RunnerManager } from "~/jobber/runners/manager.js"; +import { jobModel } from "~/db/job.js"; +import * as grpcJob from "@jobber/grpc/basics/job.js"; +import * as grpcAction from "@jobber/grpc/basics/action.js"; +import * as grpcTrigger from "@jobber/grpc/basics/trigger.js"; +import * as grpcJobVersion from "@jobber/grpc/basics/job-version.js"; +import { JobsTableType } from "~/db/schema/jobs.js"; +import { actionsModel } from "~/db/actions.js"; +import { ActionsTableType } from "~/db/schema/actions.js"; +import { TriggersTableType } from "~/db/schema/triggers.js"; +import { triggersModel } from "~/db/triggers.js"; +import { jobVersionsModel } from "~/db/job-versions.js"; +import { JobVersionsTableType } from "~/db/schema/job-versions.js"; const authorizedCall = ( callback: ( request: TRequest, context: CallContext, - bouncer: Bouncer - ) => Promise + bouncer: Bouncer, + ) => Promise, ) => { return async ( request: TRequest, - context: CallContext + context: CallContext, ): Promise => { try { const token = context.metadata.get("authorization"); @@ -58,83 +70,320 @@ const authorizedCall = ( }; }; -const generalManagementDefinition: ServiceImplementation = - { - createRunnerJwt: authorizedCall(async (request, context, bouncer) => { - if (!bouncer.canWriteGrpcRunnerJwt()) { - throw new ServerError( - Status.PERMISSION_DENIED, - "Insufficient Permissions" - ); - } - - // ensure runner exists - const manager = container.resolve(RunnerManager); +const mapGrpcJob = (job: JobsTableType): grpcJob.Item => { + let status: grpcJob.Item_Status; + if (job.status === "enabled") { + status = grpcJob.Item_Status.ENABLED; + } else if (job.status === "disabled") { + status = grpcJob.Item_Status.DISABLED; + } else { + throw new ServerError(Status.INTERNAL, "Unknown job status"); + } - const runner = manager.fundRunnerById(request.runnerId); + return { + id: job.id, + jobName: job.jobName, + status: status, + description: job.description ?? undefined, + versionId: job.jobVersionId || undefined, + links: job.links.map((link) => ({ + name: link.name, + url: link.url, + })), + }; +}; - if (!runner) { - throw new ServerError(Status.NOT_FOUND, "Runner not found"); - } +const mapGrpcAction = (action: ActionsTableType): grpcAction.Item => { + let runnerMode: grpcAction.Item_RunnerMode; + if (action.runnerMode === "run-once") { + runnerMode = grpcAction.Item_RunnerMode.RUN_ONCE; + } else if (action.runnerMode === "standard") { + runnerMode = grpcAction.Item_RunnerMode.STANDARD; + } else { + throw new ServerError(Status.INTERNAL, "Unknown job status"); + } - const jwt = await new SignJWT() - .setProtectedHeader({ alg: "HS256" }) - .setIssuedAt() - .setExpirationTime("1h") - .setIssuer(getConfigOption("JOBBER_NAME")) - .setAudience(`runner:${request.runnerId}`) - .sign(Uint8Array.from("")); - - return { - jwt, - }; - }), - - getPublicKeys(request, context) { - throw new Error("Method not implemented."); + return { + id: action.id, + jobId: action.jobId, + versionId: action.jobVersionId, + + runnerImage: action.runnerImage, + runnerAsynchronous: action.runnerAsynchronous, + runnerMinCount: action.runnerMinCount, + runnerMaxCount: action.runnerMaxCount, + runnerTimeout: action.runnerTimeout, + runnerMaxIdleAge: action.runnerMaxIdleAge, + runnerMaxAge: action.runnerMaxAge, + runnerMaxAgeHard: action.runnerMaxAgeHard, + + dockerArguments: { + networks: action.runnerDockerArguments.networks ?? [], + + volumes: + action.runnerDockerArguments.volumes?.map((volume) => { + let volumeMode: grpcAction.Item_DockerArguments_Volume_VolumeMode; + if (volume.mode === "ro") { + volumeMode = + grpcAction.Item_DockerArguments_Volume_VolumeMode.READ_ONLY; + } else if (volume.mode === "rw") { + volumeMode = + grpcAction.Item_DockerArguments_Volume_VolumeMode.READ_WRITE; + } else { + volumeMode = + grpcAction.Item_DockerArguments_Volume_VolumeMode.READ_WRITE; + } + + return { + source: volume.source, + target: volume.target, + mode: volumeMode, + }; + }) ?? [], + + labels: action.runnerDockerArguments.labels || [], + + memoryLimit: action.runnerDockerArguments.memoryLimit || undefined, + + directPassthroughArguments: + action.runnerDockerArguments.directPassthroughArguments || [], }, - createRunner(request, context) { - throw new Error("Method not implemented."); - }, + runnerMode: runnerMode, + }; +}; - getAction(request, context) { - throw new Error("Method not implemented."); - }, +const mapGrpcTrigger = (trigger: TriggersTableType): grpcTrigger.Item => { + return { + id: trigger.id, + jobId: trigger.jobId, + versionId: trigger.jobVersionId, + + schedule: + trigger.context.type === "schedule" + ? { + name: trigger.context.name ?? undefined, + cron: trigger.context.cron, + timezone: trigger.context.timezone ?? undefined, + } + : undefined, + + http: + trigger.context.type === "http" + ? { + name: trigger.context.name ?? undefined, + hostname: trigger.context.hostname ?? undefined, + method: trigger.context.method ?? undefined, + path: trigger.context.path ?? undefined, + } + : undefined, + + mqtt: + trigger.context.type === "mqtt" + ? { + name: trigger.context.name ?? undefined, + topics: trigger.context.topics, + connection: { + protocol: trigger.context.connection.protocol ?? undefined, + protocolVariable: + trigger.context.connection.protocolVariable ?? undefined, + port: trigger.context.connection.port ?? undefined, + portVariable: + trigger.context.connection.portVariable ?? undefined, + host: trigger.context.connection.host ?? undefined, + hostVariable: + trigger.context.connection.hostVariable ?? undefined, + username: trigger.context.connection.username ?? undefined, + usernameVariable: + trigger.context.connection.usernameVariable ?? undefined, + password: trigger.context.connection.password ?? undefined, + passwordVariable: + trigger.context.connection.passwordVariable ?? undefined, + clientId: trigger.context.connection.clientId ?? undefined, + clientIdVariable: + trigger.context.connection.clientIdVariable ?? undefined, + }, + } + : undefined, + }; +}; - getJobs(request, context) { - throw new Error("Method not implemented."); - }, +const mapGrpcJobVersion = ( + jobVersion: JobVersionsTableType, +): grpcJobVersion.Item => { + return { + id: jobVersion.id, + jobId: jobVersion.jobId, + version: jobVersion.version, + created: new Date(jobVersion.created * 1000).toISOString(), + modified: new Date(jobVersion.modified * 1000).toISOString(), + }; +}; - getRunners(request, context) { - throw new Error("Method not implemented."); - }, +const generalApiDefinition: ServiceImplementation = { + getJob: authorizedCall(async (request, _context, bouncer) => { + const job = await jobModel.byId(request.jobId); - getHttpTriggers(request, context) { - throw new Error("Method not implemented."); - }, + if (!job) { + throw new ServerError(Status.NOT_FOUND, "Job not found"); + } - async *getMqttTriggers(request, context) { - let count = 0; - while (count < 10) { - yield { id: `mqtt-trigger-${count}` }; - count++; - } - }, + if (!bouncer.canReadJob(job)) { + throw new ServerError( + Status.PERMISSION_DENIED, + "Insufficient permissions", + ); + } - getScheduleTriggers(request, context) { - throw new Error("Method not implemented."); - }, + return { + job: mapGrpcJob(job), + }; + }), - getJob(request, context) { - throw new Error("Method not implemented."); - }, + getJobs: authorizedCall(async (_request, _context, bouncer) => { + const jobs = (await jobModel.all()) + .filter(bouncer.canReadJob) + .map(mapGrpcJob); - getGatewayConfig(request, context) { - throw new Error("Method not implemented."); - }, - }; -// + return { + jobs, + }; + }), + + getJobAction: authorizedCall(async (request, _context, bouncer) => { + const action = await actionsModel.byId(request.actionId); + + if (!action) { + throw new ServerError(Status.NOT_FOUND, "Action not found"); + } + + if (action.jobId !== request.jobId) { + throw new ServerError(Status.NOT_FOUND, "Action not found"); + } + + if (!bouncer.canReadJobAction(action)) { + throw new ServerError( + Status.PERMISSION_DENIED, + "Insufficient permissions", + ); + } + + return { + action: mapGrpcAction(action), + }; + }), + + getJobActions: authorizedCall(async (request, _context, bouncer) => { + const actions = (await actionsModel.all()) + .filter((action) => { + if (action.jobId !== request.jobId) { + return false; + } + + if (request.versionId && action.jobVersionId !== request.versionId) { + return false; + } + + return bouncer.canReadJobAction(action); + }) + .map(mapGrpcAction); + + return { + actions, + }; + }), + + getJobTrigger: authorizedCall(async (request, _context, bouncer) => { + const trigger = await triggersModel.byId(request.triggerId); + + if (!trigger) { + throw new ServerError(Status.NOT_FOUND, "Trigger not found"); + } + + if (trigger.jobId !== request.jobId) { + throw new ServerError(Status.NOT_FOUND, "Trigger not found"); + } + + if (!bouncer.canReadJobTriggers(trigger)) { + throw new ServerError( + Status.PERMISSION_DENIED, + "Insufficient permissions", + ); + } + + return { + trigger: mapGrpcTrigger(trigger), + }; + }), + + getJobTriggers: authorizedCall(async (request, _context, bouncer) => { + const triggers = ( + await triggersModel.all({ + jobId: request.jobId, + jobVersionId: request.versionId || undefined, + }) + ) + .filter((trigger) => { + if (trigger.jobId !== request.jobId) { + return false; + } + + if (request.versionId && trigger.jobVersionId !== request.versionId) { + return false; + } + + return bouncer.canReadJobTriggers(trigger); + }) + .map(mapGrpcTrigger); + + return { + triggers, + }; + }), + + getJobVersion: authorizedCall(async (request, _context, bouncer) => { + const jobVersion = await jobVersionsModel.byId(request.jobVersionId); + + if (!jobVersion) { + throw new ServerError(Status.NOT_FOUND, "Job version not found"); + } + + if (!bouncer.canReadJobVersion(jobVersion)) { + throw new ServerError( + Status.PERMISSION_DENIED, + "Insufficient permissions", + ); + } + + return { + jobVersion: mapGrpcJobVersion(jobVersion), + }; + }), + + getJobVersions: authorizedCall(async (request, _context, bouncer) => { + const jobVersions = (await jobVersionsModel.all({ jobId: request.jobId })) + .filter((jobVersion) => { + if (jobVersion.jobId !== request.jobId) { + return false; + } + + return bouncer.canReadJobVersion(jobVersion); + }) + .map(mapGrpcJobVersion); + + return { + jobVersions, + }; + }), + + getRunner: authorizedCall(async (request, _context, bouncer) => { + throw new ServerError(Status.UNIMPLEMENTED, "Not implemented"); + }), + + getRunners: authorizedCall(async (request, _context, bouncer) => { + throw new ServerError(Status.UNIMPLEMENTED, "Not implemented"); + }), +}; @singleton() export class GrpcServer extends LoopBase { @@ -148,12 +397,12 @@ export class GrpcServer extends LoopBase { protected async loopStarting() { this.server = createServer({}); - this.server.add(GeneralManagementDefinition, generalManagementDefinition); + this.server.add(GeneralAPIDefinition, generalApiDefinition); await this.server.listen( `${getConfigOption("MANAGER_GRPC_BIND_ADDRESS")}:${getConfigOption( - "MANAGER_GRPC_PORT" - )}` + "MANAGER_GRPC_PORT", + )}`, ); } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 9a2164b..1c9605d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -54,6 +54,10 @@ import { PgBackup } from "./pg-backup.js"; import { Bouncer } from "./bouncer.js"; import { JobsTableType } from "./db/schema/jobs.js"; import { userModel } from "./db/user.js"; +import { RateLimit } from "./rate-limit.js"; +import { createRouteOAuth } from "./routes/oauth.js"; +import { createRouteOAuthAdmin } from "./routes/oauth-admin.js"; +import { OAuthSigningKeys } from "./signing-keys.js"; export type InternalHonoApp = { Variables: { @@ -76,14 +80,14 @@ async function createInternalHono() { issue.path.at(0) === "request" && (issue.path.at(1) === "body" || issue.path.at(1) === "query" || - issue.path.at(1) === "param") + issue.path.at(1) === "param"), ) ) { return c.json({ success: false, message: "Malformed request body", errors: err.errors.map( - (issue) => `${issue.path.join(".")} - ${issue.message}` + (issue) => `${issue.path.join(".")} - ${issue.message}`, ), }); } @@ -96,7 +100,7 @@ async function createInternalHono() { success: false, message: "Internal Server Error", }, - 500 + 500, ); }); @@ -106,7 +110,7 @@ async function createInternalHono() { success: false, message: "Not Found", }, - 404 + 404, ); }); @@ -125,14 +129,18 @@ async function createInternalHono() { app.route("/api/", await createRouteConfig()); app.route("/api/", await createRouteVersions()); app.route("/api/", await createRouteMetrics()); + app.route("/api/", await createRouteOAuthAdmin()); - app.get("/", async (c) => c.redirect("/jobber/")); + // Not within /api/ for compliance with OAuth 2.0 best practices. + app.route("/", await createRouteOAuth()); + + app.get("/", async (c) => c.redirect("/home/")); app.use( "/*", serveStatic({ root: "./public", - }) + }), ); app.use( @@ -140,7 +148,7 @@ async function createInternalHono() { serveStatic({ path: "index.html", root: "./public/", - }) + }), ); return app; @@ -177,7 +185,7 @@ async function createGatewayHono() { if (acceptHeader.includes("text/html")) { const badGatewayPage = await readFile( - "./src/static-templates/bad-gateway.html" + "./src/static-templates/bad-gateway.html", ); return c.html(badGatewayPage.toString(), 502); @@ -189,7 +197,7 @@ async function createGatewayHono() { success: false, message: `Jobber: Gateway error!`, }, - 502 + 502, ); } @@ -199,7 +207,7 @@ async function createGatewayHono() { 502, { "Content-Type": "application/xml", - } + }, ); } @@ -212,7 +220,7 @@ async function createGatewayHono() { success: false, message: `Jobber: Gateway Error! No HTTP response received.`, }, - 502 + 502, ); } @@ -221,7 +229,7 @@ async function createGatewayHono() { return c.body( Uint8Array.from(response.http.body).buffer, response.http.status as StatusCode, - response.http.headers + response.http.headers, ); }); @@ -234,7 +242,7 @@ async function createStartupAccount() { if (!configUsername || !configPassword) { console.log( - "[createStartupAccount] No startup username or password configured. Skipping account creation." + "[createStartupAccount] No startup username or password configured. Skipping account creation.", ); return; @@ -245,7 +253,7 @@ async function createStartupAccount() { if (!parsedUsername.success || !parsedPassword.success) { console.error( - "[createStartupAccount] Invalid startup username or password. Please check your configuration." + "[createStartupAccount] Invalid startup username or password. Please check your configuration.", ); return; @@ -269,14 +277,14 @@ async function createStartupAccount() { if (!user) { console.log( - "[createStartupAccount] User already exists or could not be created." + "[createStartupAccount] User already exists or could not be created.", ); return; } console.log( - `[createStartupAccount] Startup account created successfully: ${user.username}` + `[createStartupAccount] Startup account created successfully: ${user.username}`, ); } @@ -395,7 +403,7 @@ async function createAnonymousAccount() { async function main() { console.log( - "WARNING: This is an experimental runtime, and issues ARE expected! Report any issue, or raise a PR with a fix. Issues WILL be investigated and fixed." + "WARNING: This is an experimental runtime, and issues ARE expected! Report any issue, or raise a PR with a fix. Issues WILL be investigated and fixed.", ); console.log("[main] Initialising Database connection..."); @@ -421,13 +429,16 @@ async function main() { console.log(`[main] done.`); console.log(`[main] Starting db lock cleanup...`); await cleanupLocks(); - const lockCleanupInterval = setInterval(async () => { - try { - await cleanupLocks(); - } catch (err) { - console.error("[main] Error during lock cleanup:", err); - } - }, 1000 * 60 * 5); // Every 5 minutes + const lockCleanupInterval = setInterval( + async () => { + try { + await cleanupLocks(); + } catch (err) { + console.error("[main] Error during lock cleanup:", err); + } + }, + 1000 * 60 * 5, + ); // Every 5 minutes console.log(`[main] done.`); console.log(`[main] Creating startup account...`); @@ -438,6 +449,11 @@ async function main() { await createAnonymousAccount(); console.log(`[main] done.`); + console.log("[main] Initialising OAuth Signing Keys..."); + const oauthSigningKeys = container.resolve(OAuthSigningKeys); + await oauthSigningKeys.start(); + console.log(`[main] done.`); + // console.log(`[main] Creating internal API token...`); // await createApiTokenInternal(); // console.log(`[main] done.`); @@ -476,10 +492,15 @@ async function main() { console.log(`[main] Registering MQTT Publish Handler...`); runnerManager.registerMqttPublishHandler((...args) => - triggerMqtt.publishMqttMessage(...args) + triggerMqtt.publishMqttMessage(...args), ); console.log(`[main] done.`); + console.log(`[main] Initialising rate limiter...`); + const rateLimit = container.resolve(RateLimit); + await rateLimit.start(); + console.log(`[main] done.`); + console.log(`[main] Initialising telemetry...`); const telemetry = container.resolve(Telemetry); await telemetry.start(); @@ -552,6 +573,14 @@ async function main() { serverGateway.close(); console.log(`[signalRoutine] done.`); + console.log(`[signalRoutine] Stopping rate limiter...`); + await rateLimit.stop(); + console.log(`[signalRoutine] done.`); + + console.log(`[signalRoutine] Stopping OAuth Signing Keys...`); + await oauthSigningKeys.stop(); + console.log(`[signalRoutine] done.`); + console.log(`[signalRoutine] Ending Database connection...`); const dbPool = getPool(); /*await*/ dbPool.end(); // TODO: Look into why this hangs. diff --git a/packages/server/src/jobber/store.ts b/packages/server/src/jobber/store.ts index 39fbf4a..e70ebf5 100644 --- a/packages/server/src/jobber/store.ts +++ b/packages/server/src/jobber/store.ts @@ -95,7 +95,7 @@ export class Store extends LoopBase { public async getItemById( jobId: string, - id: string + id: string, ): Promise { const result = ( await getDrizzle() @@ -126,7 +126,7 @@ export class Store extends LoopBase { options: { value: string; ttl?: number; - } + }, ): Promise { const expiry = options.ttl ? getUnixTimestamp() + options.ttl : null; @@ -169,7 +169,7 @@ export class Store extends LoopBase { public async deleteItem( jobId: string, - key: string + key: string, ): Promise { const result = ( await getDrizzle() @@ -195,7 +195,7 @@ export class Store extends LoopBase { public async deleteItemById( jobId: string, - id: string + id: string, ): Promise { const result = ( await getDrizzle() diff --git a/packages/server/src/rate-limit.ts b/packages/server/src/rate-limit.ts new file mode 100644 index 0000000..fadd429 --- /dev/null +++ b/packages/server/src/rate-limit.ts @@ -0,0 +1,71 @@ +import { LoopBase } from "@jobber/common"; +import { singleton } from "tsyringe"; + +type BucketItem = { + count: number; + + created: number; + expires: number; +}; + +// Minutely +const RATE_LIMIT_PERIOD_MS = 60 * 1000; + +@singleton() +export class RateLimit extends LoopBase { + protected loopDuration = 60 * 1000; // 1 minute + + protected loopStarting = undefined; + protected loopStarted = undefined; + protected loopClosing = undefined; + protected loopClosed = undefined; + + private buckets = new Map(); + + private createBucketKey(key: string) { + const calculatedPeriod = Date.now() - (Date.now() % RATE_LIMIT_PERIOD_MS); + + return `${key}:${calculatedPeriod}`; + } + + public isRateLimited(key: string, limit: number) { + if (this.status !== "started") { + throw new Error("RateLimit is not started"); + } + + const bucketKey = this.createBucketKey(key); + const bucket = this.buckets.get(bucketKey); + + return bucket && bucket.count >= limit; + } + + public increment(key: string) { + if (this.status !== "started") { + throw new Error("RateLimit is not started"); + } + + const bucketKey = this.createBucketKey(key); + const bucket = this.buckets.get(bucketKey); + + if (!bucket) { + this.buckets.set(bucketKey, { + count: 1, + created: Date.now(), + expires: Date.now() + RATE_LIMIT_PERIOD_MS, + }); + return; + } + + bucket.count += 1; + } + + protected async loopIteration() { + const now = Date.now(); + + for (const [key, bucket] of this.buckets) { + if (bucket.expires <= now) { + this.buckets.delete(key); + } + } + } +} diff --git a/packages/server/src/routes/auth.ts b/packages/server/src/routes/auth.ts index 660f630..27534f8 100644 --- a/packages/server/src/routes/auth.ts +++ b/packages/server/src/routes/auth.ts @@ -31,7 +31,7 @@ export async function createRouteAuth() { if (!getConfigOption("AUTH_PUBLIC_LOGIN_ENABLED")) { return c.json( { success: false, message: "Public login is disabled" }, - 403 + 403, ); } @@ -57,7 +57,7 @@ export async function createRouteAuth() { if (!user) { return c.json( { success: false, message: "Invalid username or password" }, - 401 + 401, ); } @@ -66,7 +66,7 @@ export async function createRouteAuth() { if (!isValidPassword) { return c.json( { success: false, message: "Invalid username or password" }, - 401 + 401, ); } @@ -95,7 +95,7 @@ export async function createRouteAuth() { session: {}, }, }); - } + }, ); app.post( @@ -105,7 +105,7 @@ export async function createRouteAuth() { if (!getConfigOption("AUTH_PUBLIC_REGISTRATION_ENABLED")) { return c.json( { success: false, message: "Public registration is disabled" }, - 403 + 403, ); } @@ -131,7 +131,7 @@ export async function createRouteAuth() { if (existingUser) { return c.json( { success: false, message: "Username already exists" }, - 409 + 409, ); } @@ -151,7 +151,7 @@ export async function createRouteAuth() { if (!user) { return c.json( { success: false, message: "Failed to create user" }, - 500 + 500, ); } @@ -180,7 +180,7 @@ export async function createRouteAuth() { user: {}, }, }); - } + }, ); app.get("/auth", createMiddlewareAuth(), async (c) => { diff --git a/packages/server/src/routes/oauth-admin.ts b/packages/server/src/routes/oauth-admin.ts new file mode 100644 index 0000000..ec5d3f8 --- /dev/null +++ b/packages/server/src/routes/oauth-admin.ts @@ -0,0 +1,239 @@ +import { Hono } from "hono"; +import { container } from "tsyringe"; +import { z } from "zod"; +import { oauthServiceClientModel } from "~/db/oauth-service-client.js"; +import { oauthSigningKeyModel } from "~/db/oauth-signing-key.js"; +import { OauthServiceClientTableType } from "~/db/schema/oauth-service-client.js"; +import { OauthSigningKeyTableType } from "~/db/schema/oauth-signing-key.js"; +import { InternalHonoApp } from "~/index.js"; +import { createMiddlewareAuth } from "~/middleware/auth.js"; +import { OAuthServiceClients } from "~/service-clients.js"; +import { OAuthSigningKeys } from "~/signing-keys.js"; + +function censorKey(key: OauthSigningKeyTableType) { + return { + id: key.id, + parentId: key.parentId, + childId: key.childId, + + createdByUserId: key.createdByUserId, + + status: key.status, + + alg: key.alg, + use: key.use, + + publicKey: key.publicKey, + + expiresAt: key.expiresAt, + renewsAt: key.renewsAt, + createdAt: key.createdAt, + } as const; +} + +function censorServiceClient(client: OauthServiceClientTableType) { + return { + id: client.id, + clientId: client.clientId, + + name: client.name, + description: client.description, + + isSystemManaged: client.isSystemManaged, + + allowedAudiences: client.allowedAudiences, + allowedScopes: client.allowedScopes, + + enabled: client.enabled, + + expiresAt: client.expiresAt, + createdAt: client.createdAt, + } as const; +} + +export async function createRouteOAuthAdmin() { + const serviceClients = container.resolve(OAuthServiceClients); + const signingKeys = container.resolve(OAuthSigningKeys); + + const app = new Hono(); + + app.get("/oauth/signing-keys/", createMiddlewareAuth(), async (c) => { + const bouncer = c.get("bouncer")!; + + if (!bouncer.canReadOauthSigningKeyGenerally()) { + return c.json( + { success: false, message: "Insufficient Permissions" }, + 403, + ); + } + + const keys = await oauthSigningKeyModel.all(); + + const result = keys + .filter((key) => bouncer.canReadOauthSigningKey(key)) + .map(censorKey); + + return c.json({ + success: true, + data: result, + }); + }); + + app.put("/oauth/signing-keys/:id", createMiddlewareAuth(), async (c) => { + const bouncer = c.get("bouncer")!; + + const { id } = c.req.param(); + + const key = await oauthSigningKeyModel.byId(id); + + if (!key) { + return c.json({ success: false, message: "Key not found" }, 404); + } + + if (!bouncer.canWriteOauthSigningKey(key)) { + return c.json( + { success: false, message: "Insufficient Permissions" }, + 403, + ); + } + + const schema = z.object({ + status: z.enum(["active", "retiring", "inactive"]).optional(), + expiresAt: z.string().datetime().optional(), + }); + + const body = await schema.parseAsync(await c.req.parseBody(), { + path: ["request", "body"], + }); + + await oauthSigningKeyModel.update(id, { + status: body.status, + expiresAt: body.expiresAt ? new Date(body.expiresAt) : undefined, + }); + + return c.json({ + success: true, + }); + }); + + app.post("/oauth/signing-keys/", createMiddlewareAuth(), async (c) => { + const bouncer = c.get("bouncer")!; + + if (!bouncer.canWriteOauthSigningKeyGenerally()) { + return c.json( + { success: false, message: "Insufficient Permissions" }, + 403, + ); + } + + const schema = z.object({ + alg: z.enum(["RS256"]), + use: z.enum(["sig", "enc"]), + + expiresAt: z.string().datetime().optional(), + renewsAt: z.string().datetime().optional(), + + parentId: z.string().optional(), + }); + + const body = await schema.parseAsync(await c.req.parseBody(), { + path: ["request", "body"], + }); + + const key = await signingKeys.createSigningKey({ + alg: body.alg, + use: body.use, + + expiresAt: body.expiresAt ? new Date(body.expiresAt) : undefined, + renewsAt: body.renewsAt ? new Date(body.renewsAt) : undefined, + + parentId: body.parentId, + }); + + if (!key) { + return c.json( + { success: false, message: "Failed to create signing key" }, + 500, + ); + } + + return c.json({ + success: true, + data: censorKey(key), + }); + }); + + app.get("/oauth/service-client/", createMiddlewareAuth(), async (c) => { + const bouncer = c.get("bouncer")!; + + if (!bouncer.canReadOauthServiceClientGenerally()) { + return c.json( + { success: false, message: "Insufficient Permissions" }, + 403, + ); + } + + const serviceClients = await oauthServiceClientModel.all(); + + const result = serviceClients + .filter((client) => bouncer.canReadOauthServiceClient(client)) + .map(censorServiceClient); + + return c.json({ + success: true, + data: result, + }); + }); + + app.post("/oauth/service-client/", createMiddlewareAuth(), async (c) => { + const bouncer = c.get("bouncer")!; + + if (!bouncer.canWriteOauthServiceClientGenerally()) { + return c.json( + { success: false, message: "Insufficient Permissions" }, + 403, + ); + } + + const schema = z.object({ + name: z.string(), + description: z.string().optional(), + + allowedAudiences: z.array(z.string()), + allowedScopes: z.array(z.string()), + + expiresAt: z.string().datetime().optional(), + }); + + const body = await schema.parseAsync(await c.req.parseBody(), { + path: ["request", "body"], + }); + + const { client, secret } = await serviceClients.createServiceClient({ + name: body.name, + description: body.description, + + allowedAudiences: body.allowedAudiences, + allowedScopes: body.allowedScopes, + + expiresAt: body.expiresAt ? new Date(body.expiresAt) : undefined, + }); + + if (!client) { + return c.json( + { success: false, message: "Failed to create service client" }, + 500, + ); + } + + return c.json({ + success: true, + data: { + client: censorServiceClient(client), + secret, + }, + }); + }); + + return app; +} diff --git a/packages/server/src/routes/oauth.ts b/packages/server/src/routes/oauth.ts new file mode 100644 index 0000000..13dcbf0 --- /dev/null +++ b/packages/server/src/routes/oauth.ts @@ -0,0 +1,222 @@ +import bcrypt from "bcryptjs"; +import { Hono } from "hono"; +import { exportJWK, importSPKI, SignJWT } from "jose"; +import { createPrivateKey } from "node:crypto"; +import { container } from "tsyringe"; +import { z } from "zod"; +import { getConfigOption } from "~/config.js"; +import { auditLogsModel } from "~/db/audit-log.js"; +import { oauthServiceClientModel } from "~/db/oauth-service-client.js"; +import { oauthSigningKeyModel } from "~/db/oauth-signing-key.js"; +import { InternalHonoApp } from "~/index.js"; +import { createMiddlewareResponseTime } from "~/middleware/response-time.js"; +import { RateLimit } from "~/rate-limit.js"; + +export async function createRouteOAuth() { + const rateLimit = container.resolve(RateLimit); + + const app = new Hono(); + + app.get("/.well-known/openid-configuration", async (c) => { + return c.json({ + issuer: getConfigOption("OAUTH_ISSUER"), + token_endpoint: `${getConfigOption("API_URL")}/oauth/token`, + jwks_uri: `${getConfigOption("API_URL")}/.well-known/jwks.json`, + token_endpoint_auth_methods_supported: ["client_secret_basic"], + }); + }); + + app.get("/.well-known/jwks.json", async (c) => { + const signedKeys = await oauthSigningKeyModel.getValidKeys(); + + const keys = await Promise.all( + signedKeys.map(async (key) => { + const publicKeyObject = await importSPKI(key.publicKey, key.alg); + + const jwk = await exportJWK(publicKeyObject); + + jwk.kid = key.id; + jwk.use = key.use; + jwk.alg = key.alg; + + return jwk; + }), + ); + + return c.json({ + keys: keys, + }); + }); + + app.post("/oauth/token", createMiddlewareResponseTime(2000), async (c) => { + const schema = z.object({ + grant_type: z.literal("client_credentials"), + client_id: z.string(), + client_secret: z.string(), + }); + + const body = await schema.parseAsync(await c.req.parseBody(), { + path: ["request", "body"], + }); + + const rateLimitKeys = { + global: () => `oauth-token-global`, + clientIdOk: (clientId: string) => `oauth-token-client-id-${clientId}`, + clientIdFail: (clientId: string) => `oauth-token-client-id-${clientId}`, + ip: (ip: string) => `oauth-token-ip-${ip.replace(/:/g, "-")}`, + } as const; + + rateLimit.increment(rateLimitKeys.global()); + + if (rateLimit.isRateLimited(rateLimitKeys.global(), 120)) { + await auditLogsModel.createServiceClientLog(body.client_id, { + type: "oauth-rate-limited", + clientId: body.client_id, + reason: "global", + }); + + return c.json({ error: "rate_limited" }, 429); + } + + if (rateLimit.isRateLimited(rateLimitKeys.clientIdOk(body.client_id), 20)) { + await auditLogsModel.createServiceClientLog(body.client_id, { + type: "oauth-rate-limited", + clientId: body.client_id, + reason: "client-id", + }); + + return c.json({ error: "rate_limited" }, 429); + } + + if ( + rateLimit.isRateLimited(rateLimitKeys.clientIdFail(body.client_id), 5) + ) { + await auditLogsModel.createServiceClientLog(body.client_id, { + type: "oauth-rate-limited", + clientId: body.client_id, + reason: "client-id", + }); + + return c.json({ error: "rate_limited" }, 429); + } + + const serviceClient = await oauthServiceClientModel.byClientId( + body.client_id, + ); + + if (!serviceClient) { + rateLimit.increment(rateLimitKeys.clientIdFail(body.client_id)); + + await auditLogsModel.createServiceClientLog(body.client_id, { + type: "oauth-invalid-client-id", + clientId: body.client_id, + }); + + return c.json({ error: "invalid_client" }, 401); + } + + if (!serviceClient.enabled) { + rateLimit.increment(rateLimitKeys.clientIdFail(body.client_id)); + + await auditLogsModel.createServiceClientLog(serviceClient.id, { + type: "oauth-disabled-client", + clientId: body.client_id, + }); + + return c.json({ error: "invalid_client" }, 401); + } + + if (serviceClient.expiresAt && serviceClient.expiresAt < new Date()) { + rateLimit.increment(rateLimitKeys.clientIdFail(body.client_id)); + + await auditLogsModel.createServiceClientLog(serviceClient.id, { + type: "oauth-expired-client", + clientId: body.client_id, + }); + + return c.json({ error: "invalid_client" }, 401); + } + + if (serviceClient.metadata.type !== "client_secret_basic") { + rateLimit.increment(rateLimitKeys.clientIdFail(body.client_id)); + + await auditLogsModel.createServiceClientLog(serviceClient.id, { + type: "oauth-unsupported-grant-type", + clientId: body.client_id, + grantType: serviceClient.metadata.type, + }); + + return c.json({ error: "invalid_client" }, 401); + } + + const isSecretValid = bcrypt.compare( + body.client_secret, + serviceClient.metadata.clientSecretHashed, + ); + + if (!isSecretValid) { + rateLimit.increment(rateLimitKeys.clientIdFail(body.client_id)); + + await auditLogsModel.createServiceClientLog(serviceClient.id, { + type: "oauth-invalid-client-secret", + clientId: body.client_id, + }); + + return c.json({ error: "invalid_client" }, 401); + } + + rateLimit.increment(rateLimitKeys.clientIdOk(body.client_id)); + + await auditLogsModel.createServiceClientLog(serviceClient.id, { + type: "oauth-valid-client", + clientId: body.client_id, + }); + + // Set expiration to 10 minutes from now, or if the client is expiring within 10 minutes, set it to that expiration. + let expiration = new Date(Date.now() + 10 * 60 * 1000); + if (serviceClient.expiresAt && serviceClient.expiresAt < expiration) { + expiration = serviceClient.expiresAt; + } + + let jti = `${serviceClient.id}-${Date.now()}`; + + const validKey = await oauthSigningKeyModel.getValidKey(); + + if (!validKey) { + console.error( + `[OAuthTokenRoute] No valid signing key found when trying to issue token for client ${serviceClient.id}`, + ); + + return c.json({ error: "server_error" }, 500); + } + + const key = createPrivateKey({ + key: validKey.privateKeyEncrypted, + format: "pem", + passphrase: getConfigOption("SECRET_PASSPHRASE"), + }); + + const jwt = new SignJWT({ + sub: serviceClient.id, + kid: validKey.id, + typ: "JWT", + }) + .setProtectedHeader({ + alg: validKey.alg, + kid: validKey.id, + }) + .setIssuer(getConfigOption("OAUTH_ISSUER")) + .setAudience(serviceClient.allowedAudiences) + .setExpirationTime(expiration) + .setJti(jti) + .sign(key); + + return c.json({ + access_token: jwt, + token_type: "Bearer", + expires_in: Math.floor((expiration.getTime() - Date.now()) / 1000), + }); + }); + + return app; +} diff --git a/packages/server/src/service-clients.ts b/packages/server/src/service-clients.ts new file mode 100644 index 0000000..1d0cade --- /dev/null +++ b/packages/server/src/service-clients.ts @@ -0,0 +1,63 @@ +import { LoopBase } from "@jobber/common"; +import { singleton } from "tsyringe"; +import { OauthServiceClientTableInsertType } from "./db/schema/oauth-service-client.js"; +import { secureRandomBytes } from "./util.js"; +import { genSalt as bcryptGenSalt, hash as bcryptHash } from "bcryptjs"; +import { oauthServiceClientModel } from "./db/oauth-service-client.js"; + +@singleton() +export class OAuthServiceClients extends LoopBase { + protected loopDuration = 60 * 1000; // 1 minute + + protected loopStarting = undefined; + protected loopStarted = undefined; + protected loopClosing = undefined; + protected loopClosed = undefined; + + protected async loopIteration() { + // await this.validateSigningKeys(); + } + + public async createServiceClient( + data: Pick< + OauthServiceClientTableInsertType, + | "name" + | "description" + | "allowedAudiences" + | "allowedScopes" + | "enabled" + | "expiresAt" + | "isSystemManaged" + >, + ) { + const secretKey = secureRandomBytes(56); + const secretKeyAscii = secretKey.toString("ascii"); + const secretKeyEncoded = secretKey.toString("base64"); + const secretKeyHashed = await bcryptHash( + secretKeyAscii, + await bcryptGenSalt(16), + ); + + const clientId = secureRandomBytes(36) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); + // + + const client = await oauthServiceClientModel.create({ + ...data, + + clientId, + metadata: { + type: "client_secret_basic", + clientSecretHashed: secretKeyHashed, + }, + }); + + return { + client, + secret: secretKeyEncoded, + }; + } +} diff --git a/packages/server/src/signing-keys.ts b/packages/server/src/signing-keys.ts new file mode 100644 index 0000000..cf1323d --- /dev/null +++ b/packages/server/src/signing-keys.ts @@ -0,0 +1,137 @@ +import { LoopBase } from "@jobber/common"; +import { singleton } from "tsyringe"; +import { oauthSigningKeyModel } from "./db/oauth-signing-key.js"; +import { OauthSigningKeyTableInsertType } from "./db/schema/oauth-signing-key.js"; +import { generateKeyPair } from "node:crypto"; +import { promisify } from "node:util"; +import { getConfigOption } from "./config.js"; + +const generateKeyPairPromised = promisify(generateKeyPair); + +@singleton() +export class OAuthSigningKeys extends LoopBase { + protected loopDuration = 60 * 1000; // 1 minute + + protected loopStarting = undefined; + protected loopStarted = undefined; + protected loopClosing = undefined; + protected loopClosed = undefined; + + protected async loopIteration() { + await this.validateSigningKeys(); + } + + public async validateSigningKeys() { + const validKeys = await oauthSigningKeyModel.getValidKeys(); + + if (validKeys.length === 0) { + return await this.createSigningKey(); + } + + for (const key of validKeys) { + // If the key has expired, mark it as inactive + if ( + key.expiresAt && + new Date() > key.expiresAt && + key.status !== "inactive" + ) { + await oauthSigningKeyModel.update(key.id, { status: "inactive" }); + } + + // If the key needs to be rotated, create a new key and mark the old key as retiring + if ( + key.renewsAt && + new Date() > key.renewsAt && + key.status === "active" + ) { + // create a new key with the current key as the parent + const replacementKey = await this.createSigningKey({ + parentId: key.id, + createdByUserId: key.createdByUserId, + }); + + if (!replacementKey) { + console.warn( + `[OAuthSigningKeys/validateSigningKeys] Failed to create replacement key for key ${key.id}`, + ); + continue; + } + + await oauthSigningKeyModel.update(key.id, { + status: "retiring", + childId: replacementKey.id, + }); + } + } + } + + public async createSigningKey( + data?: Partial< + Pick< + OauthSigningKeyTableInsertType, + | "createdByUserId" + | "parentId" + | "childId" + | "use" + | "alg" + | "expiresAt" + | "renewsAt" + > + >, + ) { + console.log( + `[OAuthSigningKeys/createSigningKey] Creating new signing key with data: ${JSON.stringify( + data, + )}`, + ); + + const { privateKey, publicKey } = await generateKeyPairPromised("rsa", { + modulusLength: 2048, + publicKeyEncoding: { + type: "spki", + format: "pem", + }, + privateKeyEncoding: { + type: "pkcs8", + format: "pem", + cipher: "aes-256-cbc", + passphrase: getConfigOption("SECRET_PASSPHRASE"), + }, + }); + + return await oauthSigningKeyModel.create({ + parentId: data?.parentId, + createdByUserId: data?.createdByUserId, + + status: "active", + + alg: data?.alg ?? "RS256", + use: data?.use ?? "sig", + + privateKeyEncrypted: privateKey, + publicKey: publicKey, + + expiresAt: + data?.expiresAt ?? + new Date( + Date.now() + + getConfigOption("OAUTH_SIGNING_KEY_EXPIRE_IN_DAYS") * + 60 * + 60 * + 24 * + 1000, + ), + + renewsAt: + data?.renewsAt ?? + new Date( + Date.now() + + getConfigOption("OAUTH_SIGNING_KEY_ROTATE_IN_DAYS") * + 60 * + 60 * + 24 * + 1000, + ), + }); + } +} diff --git a/packages/server/tests/permissions.test.ts b/packages/server/tests/permissions.test.ts index 6d4454b..b61b557 100644 --- a/packages/server/tests/permissions.test.ts +++ b/packages/server/tests/permissions.test.ts @@ -1,4 +1,4 @@ -import { resourceMatches } from "../src/permissions"; +import { resourceMatches } from "../src/permissions.js"; import { describe, expect, it } from "vitest"; diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 0b57a00..721d76a 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -12,12 +12,13 @@ "declaration": false, "types": ["node"], "outDir": "./dist", + "rootDir": "./src", "paths":{ "~/*": ["./src/*"] } }, "include": [ - "./src" + "src/**/*" ], "$schema": "https://json.schemastore.org/tsconfig", "display": "Recommended" diff --git a/packages/tcp-frame-socket/tsconfig.json b/packages/tcp-frame-socket/tsconfig.json index 5cfa7ab..6ffe955 100644 --- a/packages/tcp-frame-socket/tsconfig.json +++ b/packages/tcp-frame-socket/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "experimentalDecorators": true, "inlineSourceMap": true, "target": "ES2022", "module": "NodeNext", @@ -8,16 +9,14 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "declaration": true, + "declaration": false, "types": ["node"], + "rootDir": "./src", "outDir": "./dist", "paths":{ "~/*": ["./src/*"] } }, - "include": [ - "./src" - ], "$schema": "https://json.schemastore.org/tsconfig", "display": "Recommended" } \ No newline at end of file diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index 3c7c8ab..81da684 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -6,6 +6,18 @@ export default defineConfig({ plugins: [react()], server: { proxy: { + "/oauth": { + target: "http://localhost:3000", + changeOrigin: true, + secure: false, + followRedirects: true, + }, + "/.well-known": { + target: "http://localhost:3000", + changeOrigin: true, + secure: false, + followRedirects: true, + }, "/api": { target: "http://localhost:3000", changeOrigin: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ee1876..b0ccfc8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,13 +191,13 @@ importers: version: 1.13.7(hono@4.6.13) '@jobber/common': specifier: workspace:* - version: link:../common + version: file:packages/common '@jobber/grpc': specifier: workspace:* - version: link:../grpc + version: file:packages/grpc '@jobber/tcp-frame-socket': specifier: workspace:* - version: link:../tcp-frame-socket + version: file:packages/tcp-frame-socket bcryptjs: specifier: ^3.0.2 version: 3.0.2 @@ -955,6 +955,15 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@jobber/common@file:packages/common': + resolution: {directory: packages/common, type: directory} + + '@jobber/grpc@file:packages/grpc': + resolution: {directory: packages/grpc, type: directory} + + '@jobber/tcp-frame-socket@file:packages/tcp-frame-socket': + resolution: {directory: packages/tcp-frame-socket, type: directory} + '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -3339,6 +3348,23 @@ snapshots: dependencies: minipass: 7.1.2 + '@jobber/common@file:packages/common': + dependencies: + '@jobber/tcp-frame-socket': file:packages/tcp-frame-socket + + '@jobber/grpc@file:packages/grpc': + dependencies: + '@bufbuild/protobuf': 2.10.2 + '@grpc/grpc-js': 1.14.3 + '@grpc/proto-loader': 0.8.0 + long: 5.3.2 + nice-grpc: 2.1.14 + nice-grpc-common: 2.0.2 + prom-client: 15.1.3 + protobufjs: 8.0.0 + + '@jobber/tcp-frame-socket@file:packages/tcp-frame-socket': {} + '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 From fb80ddb32b4d8f731d93c28232c66df30d96f7d8 Mon Sep 17 00:00:00 2001 From: Eithan Date: Sun, 8 Feb 2026 11:57:09 +1100 Subject: [PATCH 04/25] feat: frontend for signing keys and service clients --- packages/server/src/routes/oauth-admin.ts | 53 +++- packages/web/src/api/oauth-admin.ts | 128 ++++++++ packages/web/src/hooks/use-service-client.ts | 40 +++ packages/web/src/hooks/use-service-clients.ts | 41 +++ packages/web/src/hooks/use-signing-key.ts | 34 +++ packages/web/src/hooks/use-signing-keys.ts | 34 +++ packages/web/src/pages/home/index.tsx | 82 +++++ .../[serviceClientId]/view.tsx | 215 +++++++++++++ .../home/oauth/service-clients/landing.tsx | 289 ++++++++++++++++++ .../signing-keys/[signingKeyId]/view.tsx | 240 +++++++++++++++ .../pages/home/oauth/signing-keys/landing.tsx | 244 +++++++++++++++ 11 files changed, 1399 insertions(+), 1 deletion(-) create mode 100644 packages/web/src/api/oauth-admin.ts create mode 100644 packages/web/src/hooks/use-service-client.ts create mode 100644 packages/web/src/hooks/use-service-clients.ts create mode 100644 packages/web/src/hooks/use-signing-key.ts create mode 100644 packages/web/src/hooks/use-signing-keys.ts create mode 100644 packages/web/src/pages/home/oauth/service-clients/[serviceClientId]/view.tsx create mode 100644 packages/web/src/pages/home/oauth/service-clients/landing.tsx create mode 100644 packages/web/src/pages/home/oauth/signing-keys/[signingKeyId]/view.tsx create mode 100644 packages/web/src/pages/home/oauth/signing-keys/landing.tsx diff --git a/packages/server/src/routes/oauth-admin.ts b/packages/server/src/routes/oauth-admin.ts index ec5d3f8..cdda189 100644 --- a/packages/server/src/routes/oauth-admin.ts +++ b/packages/server/src/routes/oauth-admin.ts @@ -79,6 +79,30 @@ export async function createRouteOAuthAdmin() { }); }); + app.get("/oauth/signing-keys/:id", createMiddlewareAuth(), async (c) => { + const bouncer = c.get("bouncer")!; + + const { id } = c.req.param(); + + const key = await oauthSigningKeyModel.byId(id); + + if (!key) { + return c.json({ success: false, message: "Key not found" }, 404); + } + + if (!bouncer.canReadOauthSigningKey(key)) { + return c.json( + { success: false, message: "Insufficient Permissions" }, + 403, + ); + } + + return c.json({ + success: true, + data: censorKey(key), + }); + }); + app.put("/oauth/signing-keys/:id", createMiddlewareAuth(), async (c) => { const bouncer = c.get("bouncer")!; @@ -99,7 +123,7 @@ export async function createRouteOAuthAdmin() { const schema = z.object({ status: z.enum(["active", "retiring", "inactive"]).optional(), - expiresAt: z.string().datetime().optional(), + expiresAt: z.string().datetime().nullable().optional(), }); const body = await schema.parseAsync(await c.req.parseBody(), { @@ -185,6 +209,33 @@ export async function createRouteOAuthAdmin() { }); }); + app.get("/oauth/service-client/:id", createMiddlewareAuth(), async (c) => { + const bouncer = c.get("bouncer")!; + + const { id } = c.req.param(); + + const client = await oauthServiceClientModel.byId(id); + + if (!client) { + return c.json( + { success: false, message: "Service Client not found" }, + 404, + ); + } + + if (!bouncer.canReadOauthServiceClient(client)) { + return c.json( + { success: false, message: "Insufficient Permissions" }, + 403, + ); + } + + return c.json({ + success: true, + data: censorServiceClient(client), + }); + }); + app.post("/oauth/service-client/", createMiddlewareAuth(), async (c) => { const bouncer = c.get("bouncer")!; diff --git a/packages/web/src/api/oauth-admin.ts b/packages/web/src/api/oauth-admin.ts new file mode 100644 index 0000000..136f5aa --- /dev/null +++ b/packages/web/src/api/oauth-admin.ts @@ -0,0 +1,128 @@ +import { JobberGenericResponse, JobberPermissions } from "./common"; + +export type JobberOAuthSigningKey = { + id: string; + parentId: string | null; + childId: string | null; + + createdByUserId: string; + + status: "active" | "inactive" | "revoked"; + + alg: string; + use: string; + + publicKey: string; + + expiresAt: string | null; + renewsAt: string | null; + createdAt: string; +}; + +export type JobberOAuthServiceClient = { + id: string; + clientId: string; + + name: string; + description: string; + + isSystemManaged: boolean; + + allowedAudiences: string[]; + allowedScopes: string[]; + + enabled: boolean; + + expiresAt: string | null; + createdAt: string; +}; + +export const getOAuthSigningKeys = async (): Promise< + JobberGenericResponse +> => { + const result = await fetch(`/api/oauth/signing-keys/`); + + return result.json(); +}; + +export const getOAuthSigningKey = async ( + keyId: string, +): Promise> => { + const result = await fetch(`/api/oauth/signing-keys/${keyId}`); + + return result.json(); +}; + +export const updateOAuthSigningKey = async ( + keyId: string, + payload: { + status?: "active" | "inactive" | "revoked"; + expiresAt?: string | null; + }, +): Promise> => { + const result = await fetch(`/api/oauth/signing-keys/${keyId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + return result.json(); +}; + +export const createOAuthSigningKey = async (payload: { + alg: "RS256"; + use: "sig" | "enc"; + + expiresAt?: string | null; + renewsAt?: string | null; + + parentId?: string | null; +}): Promise> => { + const result = await fetch(`/api/oauth/signing-keys/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + return result.json(); +}; + +export const getOAuthServiceClients = async (): Promise< + JobberGenericResponse +> => { + const result = await fetch(`/api/oauth/service-client/`); + + return result.json(); +}; + +export const getOAuthServiceClient = async ( + clientId: string, +): Promise> => { + const result = await fetch(`/api/oauth/service-client/${clientId}`); + + return result.json(); +}; + +export const createOAuthServiceClient = async (payload: { + name: string; + description?: string; + + allowedAudiences: string[]; + allowedScopes: string[]; + + expiresAt?: string | null; +}): Promise> => { + const result = await fetch(`/api/oauth/service-client/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + return result.json(); +}; diff --git a/packages/web/src/hooks/use-service-client.ts b/packages/web/src/hooks/use-service-client.ts new file mode 100644 index 0000000..746fa21 --- /dev/null +++ b/packages/web/src/hooks/use-service-client.ts @@ -0,0 +1,40 @@ +import { useEffect, useState } from "react"; +import { + getOAuthServiceClient, + JobberOAuthServiceClient, +} from "../api/oauth-admin"; + +export const useServiceClient = (clientId: string) => { + const [serviceClient, setServiceClient] = + useState(null); + const [error, setError] = useState(null); + const [reloadFlag, setReloadFlag] = useState(0); + + const handleUpdate = () => { + getOAuthServiceClient(clientId).then((res) => { + if (!res.success) { + setError("Failed to fetch service client"); + + console.error("Failed to fetch service client", res.message); + + return; + } + + setServiceClient(res.data); + }); + }; + + const reload = () => { + setReloadFlag((prev) => prev + 1); + }; + + useEffect(() => { + handleUpdate(); + }, [reloadFlag, clientId]); + + return { + serviceClient, + serviceClientError: error, + reloadServiceClient: reload, + }; +}; diff --git a/packages/web/src/hooks/use-service-clients.ts b/packages/web/src/hooks/use-service-clients.ts new file mode 100644 index 0000000..08106ac --- /dev/null +++ b/packages/web/src/hooks/use-service-clients.ts @@ -0,0 +1,41 @@ +import { useEffect, useState } from "react"; +import { + getOAuthServiceClients, + JobberOAuthServiceClient, +} from "../api/oauth-admin"; + +export const useServiceClients = () => { + const [serviceClients, setServiceClients] = useState< + JobberOAuthServiceClient[] | null + >(null); + const [error, setError] = useState(null); + const [reloadFlag, setReloadFlag] = useState(0); + + const handleUpdate = () => { + getOAuthServiceClients().then((res) => { + if (!res.success) { + setError("Failed to fetch service clients"); + + console.error("Failed to fetch service clients", res.message); + + return; + } + + setServiceClients(res.data); + }); + }; + + const reload = () => { + setReloadFlag((prev) => prev + 1); + }; + + useEffect(() => { + handleUpdate(); + }, [reloadFlag]); + + return { + serviceClients, + serviceClientsError: error, + reloadServiceClients: reload, + }; +}; diff --git a/packages/web/src/hooks/use-signing-key.ts b/packages/web/src/hooks/use-signing-key.ts new file mode 100644 index 0000000..0ee5495 --- /dev/null +++ b/packages/web/src/hooks/use-signing-key.ts @@ -0,0 +1,34 @@ +import { useEffect, useState } from "react"; +import { getOAuthSigningKey, JobberOAuthSigningKey } from "../api/oauth-admin"; + +export const useSigningKey = (keyId: string) => { + const [signingKey, setSigningKey] = useState( + null, + ); + const [error, setError] = useState(null); + const [reloadFlag, setReloadFlag] = useState(0); + + const handleUpdate = () => { + getOAuthSigningKey(keyId).then((res) => { + if (!res.success) { + setError("Failed to fetch signing key"); + + console.error("Failed to fetch signing key", res.message); + + return; + } + + setSigningKey(res.data); + }); + }; + + const reload = () => { + setReloadFlag((prev) => prev + 1); + }; + + useEffect(() => { + handleUpdate(); + }, [reloadFlag, keyId]); + + return { signingKey, signingKeyError: error, reloadSigningKey: reload }; +}; diff --git a/packages/web/src/hooks/use-signing-keys.ts b/packages/web/src/hooks/use-signing-keys.ts new file mode 100644 index 0000000..6d1071b --- /dev/null +++ b/packages/web/src/hooks/use-signing-keys.ts @@ -0,0 +1,34 @@ +import { useEffect, useState } from "react"; +import { getOAuthSigningKeys, JobberOAuthSigningKey } from "../api/oauth-admin"; + +export const useSigningKeys = () => { + const [signingKeys, setSigningKeys] = useState< + JobberOAuthSigningKey[] | null + >(null); + const [error, setError] = useState(null); + const [reloadFlag, setReloadFlag] = useState(0); + + const handleUpdate = () => { + getOAuthSigningKeys().then((res) => { + if (!res.success) { + setError("Failed to fetch signing keys"); + + console.error("Failed to fetch signing keys", res.message); + + return; + } + + setSigningKeys(res.data); + }); + }; + + const reload = () => { + setReloadFlag((prev) => prev + 1); + }; + + useEffect(() => { + handleUpdate(); + }, [reloadFlag]); + + return { signingKeys, signingKeysError: error, reloadSigningKeys: reload }; +}; diff --git a/packages/web/src/pages/home/index.tsx b/packages/web/src/pages/home/index.tsx index d47b875..b9f2f80 100644 --- a/packages/web/src/pages/home/index.tsx +++ b/packages/web/src/pages/home/index.tsx @@ -16,6 +16,10 @@ import TokensTokenIdEditComponent from "./api-tokens/[tokenId]/edit"; import TokensTokenIdLandingComponent from "./api-tokens/[tokenId]/landing"; import TokensComponent from "./api-tokens/landing"; import TokensNewComponent from "./api-tokens/new"; +import SigningKeysComponent from "./oauth/signing-keys/landing"; +import SigningKeyViewComponent from "./oauth/signing-keys/[signingKeyId]/view"; +import ServiceClientsComponent from "./oauth/service-clients/landing"; +import ServiceClientViewComponent from "./oauth/service-clients/[serviceClientId]/view"; import JobIdEnvironmentComponent from "./jobs/[jobId]/environment"; import JobIdLandingComponent from "./jobs/[jobId]/landing"; import JobIdLogsComponent from "./jobs/[jobId]/logs"; @@ -205,6 +209,64 @@ const Component = () => { API Tokens + + + + + + + Signing Keys + + + + + + + + + Service Clients + + {sortedJobs && sortedJobs.length > 0 && ( @@ -305,6 +367,26 @@ export default { Component: TokensTokenIdLandingComponent, }, + // OAUTH SIGNING KEYS + { + path: "oauth/signing-keys/", + Component: SigningKeysComponent, + }, + { + path: "oauth/signing-keys/:signingKeyId/", + Component: SigningKeyViewComponent, + }, + + // OAUTH SERVICE CLIENTS + { + path: "oauth/service-clients/", + Component: ServiceClientsComponent, + }, + { + path: "oauth/service-clients/:serviceClientId/", + Component: ServiceClientViewComponent, + }, + // JOBS { path: "job/:jobId/", diff --git a/packages/web/src/pages/home/oauth/service-clients/[serviceClientId]/view.tsx b/packages/web/src/pages/home/oauth/service-clients/[serviceClientId]/view.tsx new file mode 100644 index 0000000..ffb889a --- /dev/null +++ b/packages/web/src/pages/home/oauth/service-clients/[serviceClientId]/view.tsx @@ -0,0 +1,215 @@ +import { Link, useParams } from "react-router-dom"; +import { HomePageComponent } from "../../../../../components/home-page-component"; +import { TimeSinceComponent } from "../../../../../components/time-since-component"; +import { useServiceClient } from "../../../../../hooks/use-service-client"; +import { PermissionGuardComponent } from "../../../../../components/permission-guard"; + +const Component = () => { + const serviceClientId = useParams().serviceClientId || ""; + + const { serviceClient, serviceClientError } = + useServiceClient(serviceClientId); + + if (!serviceClient && !serviceClientError) { + return ( + +
+
+ + + + +
+
+
+ ); + } + + if (serviceClientError || !serviceClient) { + return ( + +
+
+ + + +

+ Error loading service client +

+

{serviceClientError}

+
+
+
+ ); + } + + return ( + + +
+ {/* Header */} +
+ + ← Back to Service Clients + +
+ + {/* Single Client Card */} +
+ {/* Client Header */} +
+
+
+

+ {serviceClient.name} +

+
+ {serviceClient.description || ( + + No description + + )} +
+
+
+ + {serviceClient.enabled ? "enabled" : "disabled"} + + {serviceClient.isSystemManaged && ( + + system managed + + )} +
+
+
+ + {/* Client Details */} +
+

+ Details +

+
+
+
Client ID
+
+ {serviceClient.clientId} +
+
+
+
Created
+
+ +
+
+
+
Expires
+
+ {serviceClient.expiresAt ? ( + + ) : ( + Never + )} +
+
+
+
+ + {/* Allowed Audiences */} +
+

+ Allowed Audiences +

+ {serviceClient.allowedAudiences.length > 0 ? ( +
+ {serviceClient.allowedAudiences.map((aud, i) => ( + + {aud} + + ))} +
+ ) : ( +

+ No audiences configured +

+ )} +
+ + {/* Allowed Scopes */} +
+

+ Allowed Scopes +

+ {serviceClient.allowedScopes.length > 0 ? ( +
+ {serviceClient.allowedScopes.map((scope, i) => ( + + {scope} + + ))} +
+ ) : ( +

+ No scopes configured +

+ )} +
+
+
+
+
+ ); +}; + +export default Component; diff --git a/packages/web/src/pages/home/oauth/service-clients/landing.tsx b/packages/web/src/pages/home/oauth/service-clients/landing.tsx new file mode 100644 index 0000000..c06c3cc --- /dev/null +++ b/packages/web/src/pages/home/oauth/service-clients/landing.tsx @@ -0,0 +1,289 @@ +import { Link } from "react-router-dom"; +import { HomePageComponent } from "../../../../components/home-page-component"; +import { PermissionGuardComponent } from "../../../../components/permission-guard"; +import { TimeSinceComponent } from "../../../../components/time-since-component"; +import { useServiceClients } from "../../../../hooks/use-service-clients"; + +const Component = () => { + const { serviceClients, serviceClientsError } = useServiceClients(); + + return ( + + +
+
+
+

+ OAuth Service Clients +

+

+ Manage OAuth service clients for machine-to-machine + authentication +

+
+ {/* + + + + + Create New Client + + */} +
+ +
+
+ + + + + + + + + + + + + {serviceClients ? ( + serviceClients.map((client) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
+ Client + + Audiences + + Scopes + + Expires + + Created + + Actions +
+
+
+ + + +
+
+
+ + {client.name} + + + {client.enabled ? "enabled" : "disabled"} + + {client.isSystemManaged && ( + + system + + )} +
+
+ {client.description || ( + + No description + + )} +
+
+ {client.clientId.slice(0, 16)}... +
+
+
+
+
+ {client.allowedAudiences.length > 0 ? ( + client.allowedAudiences + .slice(0, 2) + .map((aud, i) => ( + + {aud.length > 20 + ? `${aud.slice(0, 20)}...` + : aud} + + )) + ) : ( + + None + + )} + {client.allowedAudiences.length > 2 && ( + + +{client.allowedAudiences.length - 2} more + + )} +
+
+
+ {client.allowedScopes.length > 0 ? ( + client.allowedScopes + .slice(0, 2) + .map((scope, i) => ( + + {scope} + + )) + ) : ( + + None + + )} + {client.allowedScopes.length > 2 && ( + + +{client.allowedScopes.length - 2} more + + )} +
+
+ {client.expiresAt ? ( + + ) : ( + Never + )} + + + + + View Details + + + + +
+ {serviceClientsError ? ( +
+ + + +

+ Error loading service clients +

+

+ {serviceClientsError} +

+
+ ) : ( +
+ + + + +

+ Loading service clients... +

+
+ )} +
+
+
+
+
+
+ ); +}; + +export default Component; diff --git a/packages/web/src/pages/home/oauth/signing-keys/[signingKeyId]/view.tsx b/packages/web/src/pages/home/oauth/signing-keys/[signingKeyId]/view.tsx new file mode 100644 index 0000000..3b7e8b5 --- /dev/null +++ b/packages/web/src/pages/home/oauth/signing-keys/[signingKeyId]/view.tsx @@ -0,0 +1,240 @@ +import { Link, useParams } from "react-router-dom"; +import { HomePageComponent } from "../../../../../components/home-page-component"; +import { TimeSinceComponent } from "../../../../../components/time-since-component"; +import { useSigningKey } from "../../../../../hooks/use-signing-key"; +import { PermissionGuardComponent } from "../../../../../components/permission-guard"; + +const Component = () => { + const signingKeyId = useParams().signingKeyId || ""; + + const { signingKey, signingKeyError } = useSigningKey(signingKeyId); + + if (!signingKey && !signingKeyError) { + return ( + +
+
+ + + + +
+
+
+ ); + } + + if (signingKeyError || !signingKey) { + return ( + +
+
+ + + +

+ Error loading signing key +

+

{signingKeyError}

+
+
+
+ ); + } + + return ( + + +
+ {/* Header */} +
+ + ← Back to Signing Keys + +
+ + {/* Single Key Card */} +
+ {/* Key Header */} +
+
+
+

+ OAuth Signing Key +

+
+ {signingKey.id} +
+
+ + {signingKey.status} + +
+
+ + {/* Key Details */} +
+

+ Details +

+
+
+
Algorithm
+
+ + {signingKey.alg} + +
+
+
+
Use
+
+ {signingKey.use === "sig" ? "Signature" : "Encryption"} +
+
+
+
Created
+
+ +
+
+
+
Expires
+
+ {signingKey.expiresAt ? ( + + ) : ( + Never + )} +
+
+
+
Renews
+
+ {signingKey.renewsAt ? ( + + ) : ( + N/A + )} +
+
+
+
Created By
+
+ {signingKey.createdByUserId + ? `${signingKey.createdByUserId.slice(0, 8)}...` + : "System"} +
+
+ {signingKey.parentId && ( +
+
Parent Key
+
+ + {signingKey.parentId.slice(0, 8)}... + +
+
+ )} + {signingKey.childId && ( +
+
Child Key
+
+ + {signingKey.childId.slice(0, 8)}... + +
+
+ )} +
+
+ + {/* Public Key */} +
+

+ Public Key +

+
+
+                  {signingKey.publicKey}
+                
+
+

+ This public key can be used to verify JWT tokens signed by this + key. It is also available via the{" "} + + JWKS endpoint + + . +

+
+
+
+
+
+ ); +}; + +export default Component; diff --git a/packages/web/src/pages/home/oauth/signing-keys/landing.tsx b/packages/web/src/pages/home/oauth/signing-keys/landing.tsx new file mode 100644 index 0000000..c03ee09 --- /dev/null +++ b/packages/web/src/pages/home/oauth/signing-keys/landing.tsx @@ -0,0 +1,244 @@ +import { Link } from "react-router-dom"; +import { HomePageComponent } from "../../../../components/home-page-component"; +import { PermissionGuardComponent } from "../../../../components/permission-guard"; +import { TimeSinceComponent } from "../../../../components/time-since-component"; +import { useSigningKeys } from "../../../../hooks/use-signing-keys"; + +const Component = () => { + const { signingKeys, signingKeysError } = useSigningKeys(); + + return ( + + +
+
+
+

+ OAuth Signing Keys +

+

+ Manage JWT signing keys for OAuth authentication +

+
+
+ + {/* JWKS Endpoint Info */} +
+
+ + + +
+

+ Public JWKS Endpoint +

+

+ Active public keys are available at:{" "} + + /.well-known/jwks.json + +

+

+ Use this endpoint to verify JWT tokens issued by this server. +

+
+
+
+ +
+
+ + + + + + + + + + + + + {signingKeys ? ( + signingKeys.map((key) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
+ Key + + Algorithm + + Use + + Expires + + Created + + Actions +
+
+
+ + + +
+
+
+ + {key.status} + +
+
+ ID: {key.id.slice(0, 8)}... +
+
+
+
+ + {key.alg} + + + + {key.use === "sig" ? "Signature" : "Encryption"} + + + {key.expiresAt ? ( + + ) : ( + Never + )} + + + + + View Details + + + + +
+ {signingKeysError ? ( +
+ + + +

+ Error loading signing keys +

+

+ {signingKeysError} +

+
+ ) : ( +
+ + + + +

+ Loading signing keys... +

+
+ )} +
+
+
+
+
+
+ ); +}; + +export default Component; From fb9af0c7fc2ab3900d64f3f8d0819bee84ac79f2 Mon Sep 17 00:00:00 2001 From: Eithan Date: Sun, 8 Feb 2026 14:16:08 +1100 Subject: [PATCH 05/25] chore: oauth2 working implementation --- packages/grpc/package.json | 2 +- packages/grpc/src/protoset.bin | Bin 0 -> 13383 bytes packages/server/package.json | 1 + packages/server/src/grpc/index.ts | 114 +++--- packages/server/src/index.ts | 10 + packages/server/src/routes/oauth-admin.ts | 10 +- packages/server/src/routes/oauth.ts | 31 +- packages/server/src/service-clients.ts | 12 + packages/server/src/signing-keys.ts | 23 ++ packages/web/src/api/oauth-admin.ts | 4 +- packages/web/src/pages/home/index.tsx | 5 + .../home/oauth/service-clients/landing.tsx | 4 +- .../pages/home/oauth/service-clients/new.tsx | 364 ++++++++++++++++++ pnpm-lock.yaml | 54 ++- 14 files changed, 512 insertions(+), 122 deletions(-) create mode 100644 packages/grpc/src/protoset.bin create mode 100644 packages/web/src/pages/home/oauth/service-clients/new.tsx diff --git a/packages/grpc/package.json b/packages/grpc/package.json index dae4079..42b35cd 100644 --- a/packages/grpc/package.json +++ b/packages/grpc/package.json @@ -10,7 +10,7 @@ }, "type": "module", "scripts": { - "grpc": "rm -rf dist 2>/dev/null || : && protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt=importSuffix=.js --ts_proto_out=./src --ts_proto_opt=outputServices=nice-grpc,outputServices=generic-definitions,useExactTypes=false,stringEnums=true,esModuleInterop=true,enumsAsLiterals=true,outputDefaultValues=true --proto_path=./proto/ ./proto/*.proto ./proto/**/*.proto", + "grpc": "rm -rf dist 2>/dev/null || : && protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt=importSuffix=.js --ts_proto_out=./src --ts_proto_opt=outputServices=nice-grpc,outputServices=generic-definitions,useExactTypes=false,stringEnums=true,esModuleInterop=true,enumsAsLiterals=true,outputDefaultValues=true --descriptor_set_out=src/protoset.bin --include_imports --proto_path=./proto/ ./proto/*.proto ./proto/**/*.proto", "build": "pnpm grpc && tsc" }, "keywords": [], diff --git a/packages/grpc/src/protoset.bin b/packages/grpc/src/protoset.bin new file mode 100644 index 0000000000000000000000000000000000000000..a62e472ecc46bb0ff1c2e4ca4fe87a30abe22c7d GIT binary patch literal 13383 zcmcgz&2M8zayKQ45{neYX?Z*}?XkVjW3N5CLpxl@JG-_M>w_N2*2uF@@@#f<5IEGc z#2tyG`JObRB!|TU!3J3%IV3@VoEHcJ1W1rmfaH(>`3C~zun3T^OD+L&$t6H?NL5!= zzkcMSJi9UW#_m^LU0q#WRsHKCi~$~W z9G?vyni|aY4;zJ-3a^zz{AHT36i1((MT1_{n8V)r(6x<$;$b)GHs-Ov8rt6vN_LX3 z!M`rSDo)z(#cy&=#gF!UjKJ6YwjiUYTcbOI#rdW$F2EF6)a4M z7;SPMcipwkd$4#uJV8-oG?jT2^ySPf6RcYIEp4b{%r~KQ(tQw}#Ar@5I=FQ74v~g3 z!n^{N)97?KzS!xX_7k?Zt*1i2^S25G^Gnb??2n^fa<3c52^!tm(Q&HX1-qV0D4Ks# zfD(&so`F(4JR9R7QB1kqbQN%A}zBH#Z%Hh9hF+Ub?Mr83Q>^V7CSch3IKni zKC=wvu(P&#@bT`>cj`~!u?5@wcDTLYsTZ1Wz=GA0sl)uk=t9J#p9W9#dH3Xu{Rf_C z+gPtb^}thN@ES67hGD9(w70*uySWx_;$+wgKiS2pH*hNFVtz2^Tug6xdYZbJ4ciL- zu>f!{8lU!K@*CzCpcH-jDei`qt=4|F#dNKdrq!_op%9Y!jU&-JIX^T%ZJV#bB0DxV zpnQIOazdpHu0x5ZFlAU++u8Z}+qgRPgqxk+@6-#y>mcrt`bfR_6$wSh2B5Ur+3BFZ z=bRG$eGZ;cKRfE*N`?<3b+t=tqy9d%o9c;uH3dJOo9%%u8}-1pL6&wFqlZ=ZD=qT9 zhf&-c_ebRA)%SW`<`&v+(oMwQ*4kHFDj2(zJ^L{fBJ&nhMsC+)^t*IA&Gcr4KACC2 zV)S@~r!8)f2QG)&q%Xa3)J>v8>vZvpG=rC+#B-YpEOvI+)^|E$ZJXPB+)AzOFXo{k zijD^7eT*RFBqgd!~a%8BtkWub2#2Dut;xxv2ofPA~iEQfr0JU$C9kPzJ(>2Fp>mL} z#Hs6t^DgRgo~PR4=pFpaETha%?9p9u71-Vj`exekjM z0}e0n8)da++!E~&EAE{PlG*;=)4~mmlo<3w-f&Z z&Tx})|L7uU(b)^_I-eyCjW0Aws>o z0p<-T9w*7jUc)MwkL3;ALINCQ32DITX9?fMW(~ND1T=sMn`f{xA%MB&`xhc4Z$lf5 zj;DuFUA&C_O`D$Jh3(gd15TyMEL;x`TE}bj0Wq-qq@>M1Ee;n{>5nks}KXeb1me~Zr%wvFQHD!O~$|~&}xjRYxF^f~$&>PqM{RM#Z6RF+8 z2)*G6#{n`M?V**w0d;NO?~eQ32WWn(zo{>(D#SS0*P<4Sgm}=0FGH1nIxaS4+L{kD zy~2SsX^pkKRYZ%#mT;h-j)V2JTMJ>;?~$z26=OV^!L~d2I$S|= z$<%7&lr8D<|y; z-m_vjMxvfC!=j84{VZogaXMO8B&@9Zg8*J|l*=%sI2F zh#)P7%Jnq6cJMaTlsL5#rgm1s1bjkD{4MzVf(~>FKzsnLT~8&3Z*Ecq$=nzKP(pkb&#`%?m3EVVls#!)o9Y+!zYB{zG&Ykos_AB9 zVsI+&E(Iu>SlpVqK0%swsLdB!nv2^?mlu8i6=){IF+Pu+N4F$Fy44-^6(-n= z#^=%4PmqEaVfl_VR1tUbO)&Uj0qRJh`+Z4LlIL9}!Vu-*U3*V7EBIiw+X)Xk_dC1$2V495 z_Yex2?7sW0eZ<6Z!>(qi_cpdVo1g69GY`h^Yg04JxkXU)dyn`rtBgtZqa^sFIcT79 z-a+i~QTGDx)1UT_T>kkEEcEdKS7X;Jy0BRTIK=aq=HDpV`6WNeUr`DdoGSBcz`&6gw|yEReiUQnEmpM@l!dWP#x2I_)si=Rh>Zl01kp zMf3Ef#6$CZERZoI1egi*Ai#){UWNM@J4)d`@}E6^;Y4hjlvVVg?-Zi+@IL}al(ho` zgFnLx3^{TH*3tR-d1xht7P%xsp+z4BWQAayR`f^}LZBWi!~^wEQ?xpM@5Ot7!|Hqp zj_ieiybJ_Kb1I_7K-oeTQ9}ZSi3A`+5KntZsLW;%GM2v?7M>C$EJeTwa3aaG_*MqW zqK3{F{96H5Wycg@|9S12!sYzvG!2)#9aB_%N;$M`vz4R6g5R0f%8)O3X+CCRk7}9) zVr)^TPV1|+pH{cR4khc??I9W23Nb308xb0#r%S34T$`R52;-CjF#9`eGh}wYTKu;w zf@F)B(an6jvIckQ+ZDZT6%hgV->#ng{i$3Bd_7#*qYNK22TiG_ihhERglO*)qWsDr zdhJb;6PubBL;4xK3dYX38J$E)G<(wDEI`>3T1%M{YQ}u%f9#Vfzr+@?`iBZhF0#8q z29nto++$Z~K{kSpMB9kl8FHXDZHM4Y92@xRSY;y43K6N)S4ezp-1}r>qqDb%ndd63e6Y5?^GVn_xYNPBbbF&- zxcMrSU5;`|3CztF)F-3qWd&L{--oJyOoaBQcn@v=BnzRup5h>t);g84+S=9TWCHr8 zMTU+TZMmvP7VQ+5DZ4N=2JsoXL-PWSOCNBMteJ7wrb{ll+gjKiSizx`L1+exmvG5; zk%4sT%(K#3l$sfl&HOBkG*}~}k6{%h(pc~$%$`$$g;>YZ0+Hc+8Fav39dEJTI#c~{ zzYM=^F{t$ZW-|NN)VWgvcV7u%g_9cQhxa#xn=ZYKHD8?hU9L>O3{{_}acke0zN_Xf z$gDvf)tk9sWmoi)ih$dnA=7oWe}d&ayrjF`LH8)4Dl+pnKBJ3EMWjzwIpJiR?qZR; zI30&xc4A!Wck{-*3l%#bJ5&UH{3$bDHgELXSpJ&2w{-Nh&ujYIjTM;1J~}kBs?&My zt?44H@6NCUWU)K$9rw?pte&rk8%QhH-%P8%{sG{sgP_J1l~weKHIENU6*{$BSQ3Ib zLvGCxx?5YPCNBou2trt*f$PJ=i>cHY;|-R|TY8LJ_>=ff7RXa{iw0bpw-nOP;Li)N zLQKU^kWf>+F-A!p0N+NWW$S{mv;ym;?B$e@2x?_A5Xo&cBe59>l?_PdMs)yKqq%s@ z%8)9pRmHYFJ%vgW^r6lY^TC=WvfPVF;0Fk^c_N52`9)hC z?90&bx#@wYnVD;x3K1WCz7TO%h;^vb`O2u_r_Cs0xZ?4Ub=-ETF1_y7+ArxUrby*s zMRzj5H$+Z^DZ;N|s&Q{$3uwd6hn)>-6@o52tuZAd{Nz7P@UWzHFkF^{h4wWvXeuu{ zJfItaWQEJFcON^0Ux%v`Q^?u~&g0T!UfT^_TQ2#}w7&K~W0gMJ9u~utt;LYBU@GBR zq`&g!vpHW_;MWQma^Dv&r2mwQw;L98PPuqrWZ65gY~|2Z;=78-qCFXTuFv?&+!WdW EKc!=4lK=n! literal 0 HcmV?d00001 diff --git a/packages/server/package.json b/packages/server/package.json index d524c9e..84476e9 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -30,6 +30,7 @@ "long": "^5.3.2", "mqtt": "^5.10.3", "nice-grpc": "^2.1.14", + "nice-grpc-server-reflection": "^3.0.3", "pg": "^8.13.1", "prom-client": "^15.1.3", "protobufjs": "^8.0.0", diff --git a/packages/server/src/grpc/index.ts b/packages/server/src/grpc/index.ts index bbd4a09..3b4b416 100644 --- a/packages/server/src/grpc/index.ts +++ b/packages/server/src/grpc/index.ts @@ -8,13 +8,16 @@ import { ServerError, Status, } from "nice-grpc"; +import { + ServerReflectionService, + ServerReflection, +} from "nice-grpc-server-reflection"; + +import { ServerCredentials } from "@grpc/grpc-js"; import { container, singleton } from "tsyringe"; -import { Bouncer } from "~/bouncer.js"; import { getConfigOption } from "~/config.js"; -import { apiTokensModel } from "~/db/api-tokens.js"; -import { SignJWT } from "jose"; -import { RunnerManager } from "~/jobber/runners/manager.js"; +import { createLocalJWKSet, jwtVerify, errors as joseErrors } from "jose"; import { jobModel } from "~/db/job.js"; import * as grpcJob from "@jobber/grpc/basics/job.js"; import * as grpcAction from "@jobber/grpc/basics/action.js"; @@ -27,43 +30,50 @@ import { TriggersTableType } from "~/db/schema/triggers.js"; import { triggersModel } from "~/db/triggers.js"; import { jobVersionsModel } from "~/db/job-versions.js"; import { JobVersionsTableType } from "~/db/schema/job-versions.js"; +import { readFile } from "node:fs/promises"; +import { OAuthSigningKeys } from "~/signing-keys.js"; +import { OAuthServiceClients } from "~/service-clients.js"; const authorizedCall = ( - callback: ( - request: TRequest, - context: CallContext, - bouncer: Bouncer, - ) => Promise, + callback: (request: TRequest, context: CallContext) => Promise, ) => { return async ( request: TRequest, context: CallContext, ): Promise => { try { - const token = context.metadata.get("authorization"); + const oauthSigningKeys = container.resolve(OAuthSigningKeys); + const oauthServiceClients = container.resolve(OAuthServiceClients); + + let token = context.metadata.get("Authorization"); if (!token) { + console.log("gRPC Unauthorized error: No token provided"); throw new ServerError(Status.UNAUTHENTICATED, "Unauthenticated"); } - const apiToken = await apiTokensModel.byValidToken(token); - - if (!apiToken) { - throw new ServerError(Status.UNAUTHENTICATED, "Unauthenticated"); + if (token.startsWith("Bearer ")) { + token = token.slice("Bearer ".length); } - const bouncer = new Bouncer({ - type: "token", - token: apiToken, - permissions: apiToken.permissions, + const jwks = createLocalJWKSet(await oauthSigningKeys.createJwksSet()); + + await jwtVerify(token, jwks, { + issuer: getConfigOption("OAUTH_ISSUER"), + audience: oauthServiceClients.getAudienceGeneralApi(), }); - return callback(request, context, bouncer); + return callback(request, context); } catch (err) { if (err instanceof ServerError) { throw err; } + if (err instanceof joseErrors.JOSEError) { + console.log("gRPC Unauthorized error:", err); + throw new ServerError(Status.UNAUTHENTICATED, "Unauthenticated"); + } + console.log("gRPC Internal server error:", err); throw new ServerError(Status.INTERNAL, "Internal server error"); } @@ -221,36 +231,27 @@ const mapGrpcJobVersion = ( }; const generalApiDefinition: ServiceImplementation = { - getJob: authorizedCall(async (request, _context, bouncer) => { + getJob: authorizedCall(async (request, _context) => { const job = await jobModel.byId(request.jobId); if (!job) { throw new ServerError(Status.NOT_FOUND, "Job not found"); } - if (!bouncer.canReadJob(job)) { - throw new ServerError( - Status.PERMISSION_DENIED, - "Insufficient permissions", - ); - } - return { job: mapGrpcJob(job), }; }), - getJobs: authorizedCall(async (_request, _context, bouncer) => { - const jobs = (await jobModel.all()) - .filter(bouncer.canReadJob) - .map(mapGrpcJob); + getJobs: authorizedCall(async (_request, _context) => { + const jobs = (await jobModel.all()).map(mapGrpcJob); return { jobs, }; }), - getJobAction: authorizedCall(async (request, _context, bouncer) => { + getJobAction: authorizedCall(async (request, _context) => { const action = await actionsModel.byId(request.actionId); if (!action) { @@ -261,19 +262,12 @@ const generalApiDefinition: ServiceImplementation = { throw new ServerError(Status.NOT_FOUND, "Action not found"); } - if (!bouncer.canReadJobAction(action)) { - throw new ServerError( - Status.PERMISSION_DENIED, - "Insufficient permissions", - ); - } - return { action: mapGrpcAction(action), }; }), - getJobActions: authorizedCall(async (request, _context, bouncer) => { + getJobActions: authorizedCall(async (request, _context) => { const actions = (await actionsModel.all()) .filter((action) => { if (action.jobId !== request.jobId) { @@ -284,7 +278,7 @@ const generalApiDefinition: ServiceImplementation = { return false; } - return bouncer.canReadJobAction(action); + return true; }) .map(mapGrpcAction); @@ -293,7 +287,7 @@ const generalApiDefinition: ServiceImplementation = { }; }), - getJobTrigger: authorizedCall(async (request, _context, bouncer) => { + getJobTrigger: authorizedCall(async (request, _context) => { const trigger = await triggersModel.byId(request.triggerId); if (!trigger) { @@ -304,19 +298,12 @@ const generalApiDefinition: ServiceImplementation = { throw new ServerError(Status.NOT_FOUND, "Trigger not found"); } - if (!bouncer.canReadJobTriggers(trigger)) { - throw new ServerError( - Status.PERMISSION_DENIED, - "Insufficient permissions", - ); - } - return { trigger: mapGrpcTrigger(trigger), }; }), - getJobTriggers: authorizedCall(async (request, _context, bouncer) => { + getJobTriggers: authorizedCall(async (request, _context) => { const triggers = ( await triggersModel.all({ jobId: request.jobId, @@ -332,7 +319,7 @@ const generalApiDefinition: ServiceImplementation = { return false; } - return bouncer.canReadJobTriggers(trigger); + return true; }) .map(mapGrpcTrigger); @@ -341,33 +328,26 @@ const generalApiDefinition: ServiceImplementation = { }; }), - getJobVersion: authorizedCall(async (request, _context, bouncer) => { + getJobVersion: authorizedCall(async (request, _context) => { const jobVersion = await jobVersionsModel.byId(request.jobVersionId); if (!jobVersion) { throw new ServerError(Status.NOT_FOUND, "Job version not found"); } - if (!bouncer.canReadJobVersion(jobVersion)) { - throw new ServerError( - Status.PERMISSION_DENIED, - "Insufficient permissions", - ); - } - return { jobVersion: mapGrpcJobVersion(jobVersion), }; }), - getJobVersions: authorizedCall(async (request, _context, bouncer) => { + getJobVersions: authorizedCall(async (request, _context) => { const jobVersions = (await jobVersionsModel.all({ jobId: request.jobId })) .filter((jobVersion) => { if (jobVersion.jobId !== request.jobId) { return false; } - return bouncer.canReadJobVersion(jobVersion); + return true; }) .map(mapGrpcJobVersion); @@ -376,11 +356,11 @@ const generalApiDefinition: ServiceImplementation = { }; }), - getRunner: authorizedCall(async (request, _context, bouncer) => { + getRunner: authorizedCall(async (request, _context) => { throw new ServerError(Status.UNIMPLEMENTED, "Not implemented"); }), - getRunners: authorizedCall(async (request, _context, bouncer) => { + getRunners: authorizedCall(async (request, _context) => { throw new ServerError(Status.UNIMPLEMENTED, "Not implemented"); }), }; @@ -399,10 +379,18 @@ export class GrpcServer extends LoopBase { this.server.add(GeneralAPIDefinition, generalApiDefinition); + // this.server.add( + // ServerReflectionService, + // ServerReflection(await readFile("../grpc/src/protoset.bin"), [ + // GeneralAPIDefinition.fullName, + // ]), + // ); + await this.server.listen( `${getConfigOption("MANAGER_GRPC_BIND_ADDRESS")}:${getConfigOption( "MANAGER_GRPC_PORT", )}`, + ServerCredentials.createInsecure(), ); } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 1c9605d..7335e14 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -58,6 +58,7 @@ import { RateLimit } from "./rate-limit.js"; import { createRouteOAuth } from "./routes/oauth.js"; import { createRouteOAuthAdmin } from "./routes/oauth-admin.js"; import { OAuthSigningKeys } from "./signing-keys.js"; +import { GrpcServer } from "./grpc/index.js"; export type InternalHonoApp = { Variables: { @@ -506,6 +507,11 @@ async function main() { await telemetry.start(); console.log(`[main] done.`); + console.log("[main] Initialising gRPC server..."); + const grpcServer = container.resolve(GrpcServer); + await grpcServer.start(); + console.log("[main] done."); + console.log(`[main] Initialising APIs (API Internal, API Gateway)...`); const appInternal = await createInternalHono(); const appGateway = await createGatewayHono(); @@ -557,6 +563,10 @@ async function main() { await runnerManager.stop(); console.log(`[signalRoutine] done.`); + console.log("[signalRoutine] Stopping gRPC server."); + await grpcServer.stop(); + console.log("[signalRoutine] done."); + console.log(`[signalRoutine] Stopping pg backup service.`); await pgBackup.stop(); console.log(`[signalRoutine] done.`); diff --git a/packages/server/src/routes/oauth-admin.ts b/packages/server/src/routes/oauth-admin.ts index cdda189..c3ecddc 100644 --- a/packages/server/src/routes/oauth-admin.ts +++ b/packages/server/src/routes/oauth-admin.ts @@ -126,7 +126,7 @@ export async function createRouteOAuthAdmin() { expiresAt: z.string().datetime().nullable().optional(), }); - const body = await schema.parseAsync(await c.req.parseBody(), { + const body = await schema.parseAsync(await c.req.json(), { path: ["request", "body"], }); @@ -160,7 +160,7 @@ export async function createRouteOAuthAdmin() { parentId: z.string().optional(), }); - const body = await schema.parseAsync(await c.req.parseBody(), { + const body = await schema.parseAsync(await c.req.json(), { path: ["request", "body"], }); @@ -250,13 +250,13 @@ export async function createRouteOAuthAdmin() { name: z.string(), description: z.string().optional(), - allowedAudiences: z.array(z.string()), - allowedScopes: z.array(z.string()), + allowedAudiences: z.array(z.string()).default([]), + allowedScopes: z.array(z.string()).default([]), expiresAt: z.string().datetime().optional(), }); - const body = await schema.parseAsync(await c.req.parseBody(), { + const body = await schema.parseAsync(await c.req.json(), { path: ["request", "body"], }); diff --git a/packages/server/src/routes/oauth.ts b/packages/server/src/routes/oauth.ts index 13dcbf0..30f2908 100644 --- a/packages/server/src/routes/oauth.ts +++ b/packages/server/src/routes/oauth.ts @@ -11,9 +11,11 @@ import { oauthSigningKeyModel } from "~/db/oauth-signing-key.js"; import { InternalHonoApp } from "~/index.js"; import { createMiddlewareResponseTime } from "~/middleware/response-time.js"; import { RateLimit } from "~/rate-limit.js"; +import { OAuthSigningKeys } from "~/signing-keys.js"; export async function createRouteOAuth() { const rateLimit = container.resolve(RateLimit); + const oauthSigningKeys = container.resolve(OAuthSigningKeys); const app = new Hono(); @@ -27,25 +29,7 @@ export async function createRouteOAuth() { }); app.get("/.well-known/jwks.json", async (c) => { - const signedKeys = await oauthSigningKeyModel.getValidKeys(); - - const keys = await Promise.all( - signedKeys.map(async (key) => { - const publicKeyObject = await importSPKI(key.publicKey, key.alg); - - const jwk = await exportJWK(publicKeyObject); - - jwk.kid = key.id; - jwk.use = key.use; - jwk.alg = key.alg; - - return jwk; - }), - ); - - return c.json({ - keys: keys, - }); + return c.json(await oauthSigningKeys.createJwksSet()); }); app.post("/oauth/token", createMiddlewareResponseTime(2000), async (c) => { @@ -149,8 +133,13 @@ export async function createRouteOAuth() { return c.json({ error: "invalid_client" }, 401); } - const isSecretValid = bcrypt.compare( + const clientSecretDecoded = Buffer.from( body.client_secret, + "base64", + ).toString("ascii"); + + const isSecretValid = await bcrypt.compare( + clientSecretDecoded, serviceClient.metadata.clientSecretHashed, ); @@ -196,7 +185,7 @@ export async function createRouteOAuth() { passphrase: getConfigOption("SECRET_PASSPHRASE"), }); - const jwt = new SignJWT({ + const jwt = await new SignJWT({ sub: serviceClient.id, kid: validKey.id, typ: "JWT", diff --git a/packages/server/src/service-clients.ts b/packages/server/src/service-clients.ts index 1d0cade..43b6179 100644 --- a/packages/server/src/service-clients.ts +++ b/packages/server/src/service-clients.ts @@ -60,4 +60,16 @@ export class OAuthServiceClients extends LoopBase { secret: secretKeyEncoded, }; } + + public getAudienceGeneralApi() { + return `jobber-api`; + } + + public getAudienceRunnerApi(runnerId: string) { + return `jobber-runner:${runnerId}`; + } + + public getAudienceGatewayApi() { + return `jobber-gateway`; + } } diff --git a/packages/server/src/signing-keys.ts b/packages/server/src/signing-keys.ts index cf1323d..38083e9 100644 --- a/packages/server/src/signing-keys.ts +++ b/packages/server/src/signing-keys.ts @@ -5,6 +5,7 @@ import { OauthSigningKeyTableInsertType } from "./db/schema/oauth-signing-key.js import { generateKeyPair } from "node:crypto"; import { promisify } from "node:util"; import { getConfigOption } from "./config.js"; +import { exportJWK, importSPKI } from "jose"; const generateKeyPairPromised = promisify(generateKeyPair); @@ -134,4 +135,26 @@ export class OAuthSigningKeys extends LoopBase { ), }); } + + public async createJwksSet() { + const signedKeys = await oauthSigningKeyModel.getValidKeys(); + + const keys = await Promise.all( + signedKeys.map(async (key) => { + const publicKeyObject = await importSPKI(key.publicKey, key.alg); + + const jwk = await exportJWK(publicKeyObject); + + jwk.kid = key.id; + jwk.use = key.use; + jwk.alg = key.alg; + + return jwk; + }), + ); + + return { + keys: keys, + }; + } } diff --git a/packages/web/src/api/oauth-admin.ts b/packages/web/src/api/oauth-admin.ts index 136f5aa..0c9ab63 100644 --- a/packages/web/src/api/oauth-admin.ts +++ b/packages/web/src/api/oauth-admin.ts @@ -115,7 +115,9 @@ export const createOAuthServiceClient = async (payload: { allowedScopes: string[]; expiresAt?: string | null; -}): Promise> => { +}): Promise< + JobberGenericResponse<{ client: JobberOAuthServiceClient; secret: string }> +> => { const result = await fetch(`/api/oauth/service-client/`, { method: "POST", headers: { diff --git a/packages/web/src/pages/home/index.tsx b/packages/web/src/pages/home/index.tsx index b9f2f80..3162013 100644 --- a/packages/web/src/pages/home/index.tsx +++ b/packages/web/src/pages/home/index.tsx @@ -20,6 +20,7 @@ import SigningKeysComponent from "./oauth/signing-keys/landing"; import SigningKeyViewComponent from "./oauth/signing-keys/[signingKeyId]/view"; import ServiceClientsComponent from "./oauth/service-clients/landing"; import ServiceClientViewComponent from "./oauth/service-clients/[serviceClientId]/view"; +import ServiceClientNewComponent from "./oauth/service-clients/new"; import JobIdEnvironmentComponent from "./jobs/[jobId]/environment"; import JobIdLandingComponent from "./jobs/[jobId]/landing"; import JobIdLogsComponent from "./jobs/[jobId]/logs"; @@ -382,6 +383,10 @@ export default { path: "oauth/service-clients/", Component: ServiceClientsComponent, }, + { + path: "oauth/service-clients/new", + Component: ServiceClientNewComponent, + }, { path: "oauth/service-clients/:serviceClientId/", Component: ServiceClientViewComponent, diff --git a/packages/web/src/pages/home/oauth/service-clients/landing.tsx b/packages/web/src/pages/home/oauth/service-clients/landing.tsx index c06c3cc..a16f1c3 100644 --- a/packages/web/src/pages/home/oauth/service-clients/landing.tsx +++ b/packages/web/src/pages/home/oauth/service-clients/landing.tsx @@ -21,7 +21,7 @@ const Component = () => { authentication

- {/* @@ -44,7 +44,7 @@ const Component = () => { Create New Client - */} +
diff --git a/packages/web/src/pages/home/oauth/service-clients/new.tsx b/packages/web/src/pages/home/oauth/service-clients/new.tsx new file mode 100644 index 0000000..f67e240 --- /dev/null +++ b/packages/web/src/pages/home/oauth/service-clients/new.tsx @@ -0,0 +1,364 @@ +import { MouseEvent, useState } from "react"; +import { Link } from "react-router-dom"; +import { createOAuthServiceClient } from "../../../../api/oauth-admin"; +import { HomePageComponent } from "../../../../components/home-page-component"; +import { PermissionGuardComponent } from "../../../../components/permission-guard"; + +const TTL_OPTIONS = [ + { value: 0, label: "Never expires" }, + { value: 2592000, label: "30 days" }, + { value: 7776000, label: "90 days" }, + { value: 31536000, label: "1 year" }, + { value: 157680000, label: "5 years" }, +]; + +const Component = () => { + const [payloadName, setPayloadName] = useState(""); + const [payloadDescription, setPayloadDescription] = useState(""); + const [payloadAudiences, setPayloadAudiences] = useState(""); + const [payloadScopes, setPayloadScopes] = useState(""); + const [payloadTtl, setPayloadTtl] = useState(TTL_OPTIONS[0].value); + + const [_loading, setLoading] = useState(false); + const [result, setResult] = useState<{ + success: boolean; + message: string; + clientId?: string; + clientSecret?: string; + } | null>(null); + + const handleCreateClient = async (e: MouseEvent) => { + e.preventDefault(); + + setResult(null); + setLoading(true); + + if (!payloadName.trim()) { + setResult({ + success: false, + message: "Name is required", + }); + setLoading(false); + return; + } + + // Parse audiences (comma or newline separated) + const audiences = payloadAudiences + .split(/[,\n]/) + .map((s) => s.trim()) + .filter((s) => s.length > 0); + + // Parse scopes (comma or newline separated) + const scopes = payloadScopes + .split(/[,\n]/) + .map((s) => s.trim()) + .filter((s) => s.length > 0); + + const expiresAt = + payloadTtl > 0 + ? new Date(Date.now() + payloadTtl * 1000).toISOString() + : undefined; + + const response = await createOAuthServiceClient({ + name: payloadName.trim(), + description: payloadDescription.trim() || undefined, + allowedAudiences: audiences, + allowedScopes: scopes, + expiresAt, + }); + + if (!response.success) { + setResult({ + success: false, + message: `Failed to create service client: ${response.message}`, + }); + setLoading(false); + return; + } + + setResult({ + success: true, + message: "Service client created successfully", + clientId: response.data.client.clientId, + clientSecret: response.data.secret, + }); + setLoading(false); + }; + + return ( + + +
+ {/* Header */} +
+ + ← Back to Service Clients + +
+ + {/* Create Card */} +
+ {/* Header */} +
+

+ Create Service Client +

+

+ Create a new OAuth service client for machine-to-machine + authentication +

+
+ + {/* Success Message */} + {result && result.success && ( +
+
+ + + +
+

+ Service Client Created Successfully! +

+

+ Make sure to copy the client secret now as you won't be + able to see it again. +

+ + {/* Client ID */} +
+
+ + Client ID + + +
+ + {result.clientId} + +
+ + {/* Client Secret */} +
+
+ + Client Secret + + +
+ + {result.clientSecret} + +
+ +
+ + View all service clients + + + + +
+
+
+
+ )} + + {/* Form Section */} +
+
+
+ + setPayloadName(e.target.value)} + placeholder="e.g., My Application" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white text-gray-900" + /> +
+ +
+ + setPayloadDescription(e.target.value)} + placeholder="e.g., Backend service for processing jobs" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white text-gray-900" + /> +
+ +
+ + +
+ +
+ +