Add unsandbox.com API key settings to modal
- Add public/src/unsandbox.js browser SDK with HMAC-SHA256 auth - Add settings UI for unsandbox public/secret keys in modal - Refactor code-execution.js to use SDK when keys configured - Falls back to unauthenticated endpoint when not configured - Add English translations for new settings
This commit is contained in:
parent
a00c7fd9e2
commit
52b08894c6
4 changed files with 459 additions and 84 deletions
|
|
@ -1,5 +1,6 @@
|
|||
// Code execution sandbox functionality
|
||||
import { CODE_EXEC_URL } from "./config.js";
|
||||
import * as unsandbox from "./unsandbox.js";
|
||||
|
||||
// Helper function to sleep/wait
|
||||
function sleep(ms) {
|
||||
|
|
@ -28,90 +29,11 @@ export async function executeCode(code, language, resultsContainer, playButton)
|
|||
language = 'python';
|
||||
}
|
||||
|
||||
// Use /execute/async endpoint with polling
|
||||
const asyncResponse = await fetch(`${CODE_EXEC_URL}/execute/async`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
language: language,
|
||||
code: code
|
||||
})
|
||||
});
|
||||
|
||||
if (!asyncResponse.ok) {
|
||||
throw new Error(`HTTP error! status: ${asyncResponse.status}`);
|
||||
}
|
||||
|
||||
const { job_id } = await asyncResponse.json();
|
||||
|
||||
// Poll for results: 300ms, 750ms, 1450ms, 2350ms, 3000ms, 4600ms, 6600ms+
|
||||
const delays = [300, 450, 700, 900, 650, 1600, 2000];
|
||||
let pollCount = 0;
|
||||
let cancelButtonShown = false;
|
||||
|
||||
// Create cancel button (hidden initially)
|
||||
let cancelButton = resultsContainer.querySelector('.uncloseai-cancel-execution-btn');
|
||||
if (!cancelButton) {
|
||||
cancelButton = document.createElement('button');
|
||||
cancelButton.textContent = 'Cancel';
|
||||
cancelButton.className = 'uncloseai-cancel-execution-btn uncloseai-btn-small';
|
||||
cancelButton.style.display = 'none';
|
||||
cancelButton.style.marginTop = '8px';
|
||||
cancelButton.onclick = async () => {
|
||||
try {
|
||||
await fetch(`${CODE_EXEC_URL}/jobs/${job_id}`, { method: 'DELETE' });
|
||||
cancelButton.disabled = true;
|
||||
cancelButton.textContent = 'Cancelling...';
|
||||
} catch (error) {
|
||||
console.error('Error cancelling job:', error);
|
||||
}
|
||||
};
|
||||
resultsContainer.appendChild(cancelButton);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
await sleep(delays[Math.min(pollCount, delays.length - 1)]);
|
||||
pollCount++;
|
||||
|
||||
const jobResponse = await fetch(`${CODE_EXEC_URL}/jobs/${job_id}`);
|
||||
if (!jobResponse.ok) {
|
||||
throw new Error(`Failed to fetch job status: ${jobResponse.status}`);
|
||||
}
|
||||
|
||||
const job = await jobResponse.json();
|
||||
|
||||
if (job.status !== 'pending' && job.status !== 'running') {
|
||||
// Job finished - hide cancel button
|
||||
if (cancelButton) {
|
||||
cancelButton.style.display = 'none';
|
||||
}
|
||||
|
||||
if (job.status === 'completed') {
|
||||
const result = job.result;
|
||||
displayExecutionResults(result, resultsContainer, language);
|
||||
break;
|
||||
}
|
||||
|
||||
// timeout or cancelled
|
||||
const errorMsg = job.result?.error || 'Execution failed';
|
||||
const partialOutput = job.result?.partial_output;
|
||||
|
||||
let outputHtml = `<div style="color: #dc3545; font-weight: bold;">${escapeHtml(errorMsg)}</div>`;
|
||||
if (partialOutput) {
|
||||
outputHtml += '<div style="color: var(--uncloseai-secondary-text); margin-top: 8px;">Partial output before timeout:</div>';
|
||||
outputHtml += `<div style="color: var(--uncloseai-primary-text); margin-left: 10px;">${escapeHtml(partialOutput)}</div>`;
|
||||
}
|
||||
resultsContainer.innerHTML = outputHtml;
|
||||
break;
|
||||
}
|
||||
|
||||
// Show cancel button after poll #5 (3000ms) if still running
|
||||
if (!cancelButtonShown && pollCount === 5) {
|
||||
cancelButtonShown = true;
|
||||
cancelButton.style.display = 'inline-block';
|
||||
}
|
||||
// Check if unsandbox.com API is configured
|
||||
if (unsandbox.isConfigured()) {
|
||||
await executeWithUnsandbox(code, language, resultsContainer);
|
||||
} else {
|
||||
await executeWithFallback(code, language, resultsContainer);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
|
@ -124,6 +46,157 @@ export async function executeCode(code, language, resultsContainer, playButton)
|
|||
}
|
||||
}
|
||||
|
||||
// Execute using unsandbox.com SDK (authenticated)
|
||||
async function executeWithUnsandbox(code, language, resultsContainer) {
|
||||
let pollCount = 0;
|
||||
let cancelButton = null;
|
||||
let currentJobId = null;
|
||||
|
||||
try {
|
||||
// Start async execution to get job_id for cancel button
|
||||
currentJobId = await unsandbox.executeAsync(language, code);
|
||||
|
||||
// Create cancel button (shown after 3 seconds)
|
||||
cancelButton = createCancelButton(resultsContainer, async () => {
|
||||
if (currentJobId) {
|
||||
try {
|
||||
await unsandbox.cancelJob(currentJobId);
|
||||
cancelButton.disabled = true;
|
||||
cancelButton.textContent = 'Cancelling...';
|
||||
} catch (error) {
|
||||
console.error('Error cancelling job:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for job with progress callback
|
||||
const result = await unsandbox.waitForJob(currentJobId, null, null, (count) => {
|
||||
pollCount = count;
|
||||
// Show cancel button after poll #5 (around 3 seconds)
|
||||
if (count >= 5 && cancelButton) {
|
||||
cancelButton.style.display = 'inline-block';
|
||||
}
|
||||
});
|
||||
|
||||
// Hide cancel button
|
||||
if (cancelButton) {
|
||||
cancelButton.style.display = 'none';
|
||||
}
|
||||
|
||||
// Display result
|
||||
if (result.status === 'completed') {
|
||||
displayExecutionResults(result, resultsContainer, language);
|
||||
} else {
|
||||
displayErrorResult(result, resultsContainer);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (cancelButton) {
|
||||
cancelButton.style.display = 'none';
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute using fallback endpoint (unauthenticated)
|
||||
async function executeWithFallback(code, language, resultsContainer) {
|
||||
// Use /execute/async endpoint with polling
|
||||
const asyncResponse = await fetch(`${CODE_EXEC_URL}/execute/async`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
language: language,
|
||||
code: code
|
||||
})
|
||||
});
|
||||
|
||||
if (!asyncResponse.ok) {
|
||||
throw new Error(`HTTP error! status: ${asyncResponse.status}`);
|
||||
}
|
||||
|
||||
const { job_id } = await asyncResponse.json();
|
||||
|
||||
// Poll for results using same delays as SDK
|
||||
const delays = [300, 450, 700, 900, 650, 1600, 2000];
|
||||
let pollCount = 0;
|
||||
|
||||
// Create cancel button
|
||||
const cancelButton = createCancelButton(resultsContainer, async () => {
|
||||
try {
|
||||
await fetch(`${CODE_EXEC_URL}/jobs/${job_id}`, { method: 'DELETE' });
|
||||
cancelButton.disabled = true;
|
||||
cancelButton.textContent = 'Cancelling...';
|
||||
} catch (error) {
|
||||
console.error('Error cancelling job:', error);
|
||||
}
|
||||
});
|
||||
|
||||
while (true) {
|
||||
await sleep(delays[Math.min(pollCount, delays.length - 1)]);
|
||||
pollCount++;
|
||||
|
||||
const jobResponse = await fetch(`${CODE_EXEC_URL}/jobs/${job_id}`);
|
||||
if (!jobResponse.ok) {
|
||||
throw new Error(`Failed to fetch job status: ${jobResponse.status}`);
|
||||
}
|
||||
|
||||
const job = await jobResponse.json();
|
||||
|
||||
if (job.status !== 'pending' && job.status !== 'running') {
|
||||
// Job finished - hide cancel button
|
||||
cancelButton.style.display = 'none';
|
||||
|
||||
if (job.status === 'completed') {
|
||||
const result = job.result || job;
|
||||
displayExecutionResults(result, resultsContainer, language);
|
||||
} else {
|
||||
displayErrorResult(job.result || job, resultsContainer);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Show cancel button after poll #5 (around 3 seconds)
|
||||
if (pollCount === 5) {
|
||||
cancelButton.style.display = 'inline-block';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create cancel button helper
|
||||
function createCancelButton(resultsContainer, onCancel) {
|
||||
let cancelButton = resultsContainer.querySelector('.uncloseai-cancel-execution-btn');
|
||||
if (!cancelButton) {
|
||||
cancelButton = document.createElement('button');
|
||||
cancelButton.textContent = 'Cancel';
|
||||
cancelButton.className = 'uncloseai-cancel-execution-btn uncloseai-btn-small';
|
||||
cancelButton.style.display = 'none';
|
||||
cancelButton.style.marginTop = '8px';
|
||||
cancelButton.onclick = onCancel;
|
||||
resultsContainer.appendChild(cancelButton);
|
||||
} else {
|
||||
cancelButton.disabled = false;
|
||||
cancelButton.textContent = 'Cancel';
|
||||
cancelButton.style.display = 'none';
|
||||
cancelButton.onclick = onCancel;
|
||||
}
|
||||
return cancelButton;
|
||||
}
|
||||
|
||||
// Display error result
|
||||
function displayErrorResult(result, resultsContainer) {
|
||||
const errorMsg = result.error || result.stderr || result.message || `Execution ${result.status || 'failed'}`;
|
||||
const partialOutput = result.stdout || result.partial_output;
|
||||
|
||||
let outputHtml = `<div style="color: #dc3545; font-weight: bold;">${escapeHtml(errorMsg)}</div>`;
|
||||
if (partialOutput) {
|
||||
outputHtml += '<div style="color: var(--uncloseai-secondary-text); margin-top: 8px;">Partial output:</div>';
|
||||
outputHtml += `<div style="color: var(--uncloseai-primary-text); margin-left: 10px;">${escapeHtml(partialOutput)}</div>`;
|
||||
}
|
||||
resultsContainer.innerHTML = outputHtml;
|
||||
}
|
||||
|
||||
// Helper function to display execution results
|
||||
export function displayExecutionResults(result, resultsContainer, language) {
|
||||
// Format and display results
|
||||
|
|
|
|||
|
|
@ -112,4 +112,9 @@ export const en = {
|
|||
pleaseEnterTranslateText: "Please enter some text to translate!",
|
||||
aiModalNotAvailable: "AI modal not available in this context",
|
||||
useCustomAPI: "Use a custom openai compatible endpoint.",
|
||||
|
||||
// Unsandbox code execution settings
|
||||
useUnsandbox: "Use unsandbox.com API keys for code execution.",
|
||||
unsandboxPublicKey: "Public Key (unsb-pk-...)",
|
||||
unsandboxSecretKey: "Secret Key (unsb-sk-...)",
|
||||
};
|
||||
|
|
@ -572,6 +572,76 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
apiConfigSection.appendChild(customAPIToggle);
|
||||
apiConfigSection.appendChild(apiConfigInputs);
|
||||
|
||||
// Unsandbox Code Execution section
|
||||
const unsandboxSection = document.createElement("div");
|
||||
unsandboxSection.className = "uncloseai-section";
|
||||
|
||||
// Toggle for unsandbox API keys
|
||||
const unsandboxToggle = document.createElement("div");
|
||||
unsandboxToggle.className = "uncloseai-toggle-container";
|
||||
|
||||
const unsandboxCheckbox = document.createElement("input");
|
||||
unsandboxCheckbox.type = "checkbox";
|
||||
unsandboxCheckbox.id = "unsandbox-toggle";
|
||||
unsandboxCheckbox.checked = localStorage.getItem("useUnsandbox") === "true";
|
||||
|
||||
const unsandboxToggleLabel = document.createElement("label");
|
||||
unsandboxToggleLabel.htmlFor = "unsandbox-toggle";
|
||||
unsandboxToggleLabel.textContent = getUIText("useUnsandbox");
|
||||
unsandboxToggleLabel.className = "uncloseai-toggle-label";
|
||||
|
||||
unsandboxToggle.appendChild(unsandboxCheckbox);
|
||||
unsandboxToggle.appendChild(unsandboxToggleLabel);
|
||||
|
||||
// Public Key input
|
||||
const unsandboxPublicKeyContainer = document.createElement("div");
|
||||
unsandboxPublicKeyContainer.className = "uncloseai-input-container";
|
||||
|
||||
const unsandboxPublicKeyInput = document.createElement("input");
|
||||
unsandboxPublicKeyInput.type = "text";
|
||||
unsandboxPublicKeyInput.placeholder = getUIText("unsandboxPublicKey");
|
||||
unsandboxPublicKeyInput.value = localStorage.getItem("unsandboxPublicKey") || "";
|
||||
unsandboxPublicKeyInput.className = "uncloseai-input";
|
||||
|
||||
unsandboxPublicKeyContainer.appendChild(unsandboxPublicKeyInput);
|
||||
|
||||
// Secret Key input
|
||||
const unsandboxSecretKeyContainer = document.createElement("div");
|
||||
unsandboxSecretKeyContainer.className = "uncloseai-input-container";
|
||||
|
||||
const unsandboxSecretKeyInput = document.createElement("input");
|
||||
unsandboxSecretKeyInput.type = "password";
|
||||
unsandboxSecretKeyInput.placeholder = getUIText("unsandboxSecretKey");
|
||||
unsandboxSecretKeyInput.value = localStorage.getItem("unsandboxSecretKey") || "";
|
||||
unsandboxSecretKeyInput.className = "uncloseai-input";
|
||||
|
||||
unsandboxSecretKeyContainer.appendChild(unsandboxSecretKeyInput);
|
||||
|
||||
// Unsandbox inputs container
|
||||
const unsandboxInputs = document.createElement("div");
|
||||
unsandboxInputs.className = "uncloseai-api-inputs";
|
||||
unsandboxInputs.style.display = unsandboxCheckbox.checked ? "block" : "none";
|
||||
unsandboxInputs.appendChild(unsandboxPublicKeyContainer);
|
||||
unsandboxInputs.appendChild(unsandboxSecretKeyContainer);
|
||||
|
||||
// Event handlers for unsandbox
|
||||
unsandboxCheckbox.onchange = () => {
|
||||
const useUnsandbox = unsandboxCheckbox.checked;
|
||||
localStorage.setItem("useUnsandbox", useUnsandbox.toString());
|
||||
unsandboxInputs.style.display = useUnsandbox ? "block" : "none";
|
||||
};
|
||||
|
||||
unsandboxPublicKeyInput.onchange = () => {
|
||||
localStorage.setItem("unsandboxPublicKey", unsandboxPublicKeyInput.value);
|
||||
};
|
||||
|
||||
unsandboxSecretKeyInput.onchange = () => {
|
||||
localStorage.setItem("unsandboxSecretKey", unsandboxSecretKeyInput.value);
|
||||
};
|
||||
|
||||
unsandboxSection.appendChild(unsandboxToggle);
|
||||
unsandboxSection.appendChild(unsandboxInputs);
|
||||
|
||||
// Action buttons
|
||||
const actionsSection = document.createElement("div");
|
||||
actionsSection.className = "uncloseai-section";
|
||||
|
|
@ -650,6 +720,7 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
const rightColumn = document.createElement("div");
|
||||
rightColumn.className = "uncloseai-settings-column";
|
||||
rightColumn.appendChild(apiConfigSection);
|
||||
rightColumn.appendChild(unsandboxSection);
|
||||
|
||||
// Quick actions span both columns
|
||||
actionsSection.className = "uncloseai-section uncloseai-actions-full-width";
|
||||
|
|
|
|||
226
public/src/unsandbox.js
Normal file
226
public/src/unsandbox.js
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
/**
|
||||
* Unsandbox.com Browser SDK
|
||||
* Browser-compatible wrapper for unsandbox.com API
|
||||
* Based on official SDK patterns from lib/un.js
|
||||
*/
|
||||
|
||||
const API_BASE = "https://api.unsandbox.com";
|
||||
const POLL_DELAYS_MS = [300, 450, 700, 900, 650, 1600, 2000];
|
||||
|
||||
/**
|
||||
* Get credentials from localStorage
|
||||
*/
|
||||
export function getCredentials() {
|
||||
const useUnsandbox = localStorage.getItem("useUnsandbox") === "true";
|
||||
if (!useUnsandbox) return null;
|
||||
|
||||
const publicKey = localStorage.getItem("unsandboxPublicKey");
|
||||
const secretKey = localStorage.getItem("unsandboxSecretKey");
|
||||
|
||||
if (publicKey && secretKey) {
|
||||
return { publicKey, secretKey };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if unsandbox.com API is configured
|
||||
*/
|
||||
export function isConfigured() {
|
||||
return getCredentials() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a request using HMAC-SHA256 (Web Crypto API)
|
||||
* Message format: "timestamp:METHOD:path:body"
|
||||
*/
|
||||
async function signRequest(secretKey, timestamp, method, path, body = "") {
|
||||
const message = `${timestamp}:${method}:${path}:${body}`;
|
||||
const encoder = new TextEncoder();
|
||||
const keyData = encoder.encode(secretKey);
|
||||
const messageData = encoder.encode(message);
|
||||
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyData,
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"]
|
||||
);
|
||||
|
||||
const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
|
||||
const hashArray = Array.from(new Uint8Array(signature));
|
||||
return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an authenticated request to the unsandbox.com API
|
||||
*/
|
||||
async function makeRequest(method, path, publicKey, secretKey, data = null) {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const body = data ? JSON.stringify(data) : "";
|
||||
const signature = await signRequest(secretKey, timestamp, method, path, data ? body : "");
|
||||
|
||||
const headers = {
|
||||
"Authorization": `Bearer ${publicKey}`,
|
||||
"X-Timestamp": timestamp.toString(),
|
||||
"X-Signature": signature,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
const options = {
|
||||
method,
|
||||
headers,
|
||||
};
|
||||
|
||||
if (data) {
|
||||
options.body = body;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}${path}`, options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute code and wait for completion
|
||||
*/
|
||||
export async function executeCode(language, code, publicKey = null, secretKey = null) {
|
||||
// Use provided credentials or get from localStorage
|
||||
if (!publicKey || !secretKey) {
|
||||
const creds = getCredentials();
|
||||
if (!creds) {
|
||||
throw new Error("No unsandbox.com credentials configured");
|
||||
}
|
||||
publicKey = creds.publicKey;
|
||||
secretKey = creds.secretKey;
|
||||
}
|
||||
|
||||
const response = await makeRequest("POST", "/execute", publicKey, secretKey, {
|
||||
language,
|
||||
code,
|
||||
});
|
||||
|
||||
// Check if we got immediate result or need to poll
|
||||
const status = response.status;
|
||||
if (status === "completed" || status === "failed" || status === "timeout" || status === "cancelled") {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Need to poll
|
||||
const jobId = response.job_id;
|
||||
if (!jobId) {
|
||||
throw new Error("No job_id returned from execute endpoint");
|
||||
}
|
||||
|
||||
return waitForJob(jobId, publicKey, secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute code asynchronously (returns job_id immediately)
|
||||
*/
|
||||
export async function executeAsync(language, code, publicKey = null, secretKey = null) {
|
||||
if (!publicKey || !secretKey) {
|
||||
const creds = getCredentials();
|
||||
if (!creds) {
|
||||
throw new Error("No unsandbox.com credentials configured");
|
||||
}
|
||||
publicKey = creds.publicKey;
|
||||
secretKey = creds.secretKey;
|
||||
}
|
||||
|
||||
const response = await makeRequest("POST", "/execute", publicKey, secretKey, {
|
||||
language,
|
||||
code,
|
||||
});
|
||||
|
||||
return response.job_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get job status/result
|
||||
*/
|
||||
export async function getJob(jobId, publicKey = null, secretKey = null) {
|
||||
if (!publicKey || !secretKey) {
|
||||
const creds = getCredentials();
|
||||
if (!creds) {
|
||||
throw new Error("No unsandbox.com credentials configured");
|
||||
}
|
||||
publicKey = creds.publicKey;
|
||||
secretKey = creds.secretKey;
|
||||
}
|
||||
|
||||
return makeRequest("GET", `/jobs/${jobId}`, publicKey, secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for job completion with exponential backoff polling
|
||||
*/
|
||||
export async function waitForJob(jobId, publicKey = null, secretKey = null, onPoll = null) {
|
||||
if (!publicKey || !secretKey) {
|
||||
const creds = getCredentials();
|
||||
if (!creds) {
|
||||
throw new Error("No unsandbox.com credentials configured");
|
||||
}
|
||||
publicKey = creds.publicKey;
|
||||
secretKey = creds.secretKey;
|
||||
}
|
||||
|
||||
let pollCount = 0;
|
||||
|
||||
while (true) {
|
||||
// Sleep before polling
|
||||
const delayIdx = Math.min(pollCount, POLL_DELAYS_MS.length - 1);
|
||||
await new Promise(resolve => setTimeout(resolve, POLL_DELAYS_MS[delayIdx]));
|
||||
pollCount++;
|
||||
|
||||
const response = await getJob(jobId, publicKey, secretKey);
|
||||
|
||||
// Callback for progress updates
|
||||
if (onPoll) {
|
||||
onPoll(pollCount, response);
|
||||
}
|
||||
|
||||
const status = response.status;
|
||||
if (status === "completed" || status === "failed" || status === "timeout" || status === "cancelled") {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a running job
|
||||
*/
|
||||
export async function cancelJob(jobId, publicKey = null, secretKey = null) {
|
||||
if (!publicKey || !secretKey) {
|
||||
const creds = getCredentials();
|
||||
if (!creds) {
|
||||
throw new Error("No unsandbox.com credentials configured");
|
||||
}
|
||||
publicKey = creds.publicKey;
|
||||
secretKey = creds.secretKey;
|
||||
}
|
||||
|
||||
return makeRequest("DELETE", `/jobs/${jobId}`, publicKey, secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported languages
|
||||
*/
|
||||
export async function getLanguages(publicKey = null, secretKey = null) {
|
||||
if (!publicKey || !secretKey) {
|
||||
const creds = getCredentials();
|
||||
if (!creds) {
|
||||
throw new Error("No unsandbox.com credentials configured");
|
||||
}
|
||||
publicKey = creds.publicKey;
|
||||
secretKey = creds.secretKey;
|
||||
}
|
||||
|
||||
const response = await makeRequest("GET", "/languages", publicKey, secretKey);
|
||||
return response.languages || [];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue