diff --git a/backend/openapi.yaml b/backend/openapi.yaml index 729d67b235..942ec9b4d4 100644 --- a/backend/openapi.yaml +++ b/backend/openapi.yaml @@ -2551,6 +2551,29 @@ paths: schema: type: string + /w/{workspace}/jobs/job_signature/{job_id}/{resume_id}: + get: + summary: create an HMac signature given a job id and a resume id + operationId: createJobSignature + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/JobId" + - name: resume_id + in: path + required: true + schema: + type: integer + responses: + "200": + description: job signature + content: + text/plain: + schema: + type: string + + /w/{workspace}/jobs/resume/{job_id}/{resume_id}/{signature}: get: summary: resume a job for a suspended flow diff --git a/deno-client/.gitignore b/deno-client/.gitignore index 87ca018220..0a6e1f82df 100644 --- a/deno-client/.gitignore +++ b/deno-client/.gitignore @@ -1 +1 @@ -windmill-api +windmill-api/ diff --git a/deno-client/mod.ts b/deno-client/mod.ts index 44a215d740..49935e0414 100644 --- a/deno-client/mod.ts +++ b/deno-client/mod.ts @@ -1,4 +1,4 @@ -import { ResourceApi, VariableApi, ServerConfiguration } from './windmill-api/index.ts' +import { ResourceApi, VariableApi, ServerConfiguration, JobApi } from './windmill-api/index.ts' import { createConfiguration, type Configuration as Configuration } from './windmill-api/configuration.ts' export { @@ -14,11 +14,15 @@ export type Email = string export type Base64 = string export type Resource = any +type Conf = Configuration & { workspace_id: string } + +export const SHARED_FOLDER = '/shared' + /** * Create a client configuration from env variables * @returns client configuration */ -export function createConf(): Configuration & { workspace_id: string } { +export function createConf(): Conf { const token = Deno.env.get("WM_TOKEN") ?? 'no_token' const base_url = Deno.env.get("BASE_INTERNAL_URL") ?? 'http://localhost:8000' return { @@ -135,3 +139,90 @@ export async function databaseUrlFromResource(path: string): Promise { return `postgresql://${resource.user}:${resource.password}@${resource.host}:${resource.port}/${resource.dbname}?sslmode=${resource.sslmode}` } + +export async function genNounceAndHmac(conf: Conf, jobId: string) { + const nounce = Math.floor(Math.random() * 4294967295); + const sig = await fetch(Deno.env.get("WM_BASE_URL") + + `/api/w/${conf.workspace_id}/jobs/job_signature/${jobId}/${nounce}?token=${Deno.env.get("WM_TOKEN")}`) + return { + nounce, + signature: await sig.text() + }; +} + +export async function getResumeEndpoints() { + const conf = createConf(); + const { nounce, signature } = await genNounceAndHmac( + conf, + Deno.env.get("WM_JOB_ID") ?? "no_job_id", + ); + const url_prefix = Deno.env.get("WM_BASE_URL") + + `/api/w/${conf.workspace_id}/jobs/`; + + function getResumeUrl(op: string) { + return url_prefix + + `${op}/${Deno.env.get("WM_JOB_ID")}/${nounce}/${signature}`; + } + + return { + resume: getResumeUrl("resume"), + cancel: getResumeUrl("cancel"), + }; +} + + +export function base64ToUint8Array(data: string): Uint8Array { + return Uint8Array.from(atob(data), c => c.charCodeAt(0)) +} + +export function uint8ArrayToBase64(arrayBuffer: Uint8Array): string { + let base64 = '' + const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + + const bytes = new Uint8Array(arrayBuffer) + const byteLength = bytes.byteLength + const byteRemainder = byteLength % 3 + const mainLength = byteLength - byteRemainder + + let a, b, c, d + let chunk + + // Main loop deals with bytes in chunks of 3 + for (let i = 0; i < mainLength; i = i + 3) { + // Combine the three bytes into a single integer + chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2] + + // Use bitmasks to extract 6-bit segments from the triplet + a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18 + b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12 + c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6 + d = chunk & 63 // 63 = 2^6 - 1 + + // Convert the raw binary segments to the appropriate ASCII encoding + base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d] + } + + // Deal with the remaining bytes and padding + if (byteRemainder == 1) { + chunk = bytes[mainLength] + + a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2 + + // Set the 4 least significant bits to zero + b = (chunk & 3) << 4 // 3 = 2^2 - 1 + + base64 += encodings[a] + encodings[b] + '==' + } else if (byteRemainder == 2) { + chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1] + + a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10 + b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4 + + // Set the 2 least significant bits to zero + c = (chunk & 15) << 2 // 15 = 2^4 - 1 + + base64 += encodings[a] + encodings[b] + encodings[c] + '=' + } + + return base64 +} \ No newline at end of file