@codesandbox/api
Version:
The CodeSandbox API
231 lines • 9.48 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MAP_GITHUB_SCOPE_OPTIONS = exports.GITHUB_BASE_SCOPE = exports.waitForMessage = exports.openPopup = exports.handleRestResponse = exports.createRESTRequester = exports.onRequest = exports.ApiResponseError = void 0;
const tslib_1 = require("tslib");
const humps_1 = tslib_1.__importDefault(require("humps"));
const { camelizeKeys, camelize } = humps_1.default;
class ApiResponseError extends Error {
constructor(status, message, parsedErrorMessage) {
super(`(${status}) ${message}`);
this.type = "ERROR";
this.parsedErrorMessage = parsedErrorMessage;
if (status === 404 || message.toLowerCase().includes("not found")) {
this.type = "NOT_FOUND";
}
else if (status === 402) {
this.type = "WORKSPACE_FROZEN";
}
}
}
exports.ApiResponseError = ApiResponseError;
const onRequest = (request) => {
return fetch(request.url, {
method: request.method,
headers: request.headers,
body: request.method === "POST" ||
request.method === "PATCH" ||
request.method === "PUT"
? JSON.stringify(request.data)
: undefined,
}).then(async (response) => {
var _a;
return {
status: response.status,
data: await response.text(),
cookies: (_a = response.headers.get("set-cookie")) !== null && _a !== void 0 ? _a : "",
};
});
};
exports.onRequest = onRequest;
/**
* Combines the passed in OnRequest with custom headers, including authorization. Also detects
* if a response indicates being unauthorized
*/
function createRESTRequester(options) {
// We have to type the generic explicitly
return (_a) => {
var { path } = _a, requestOptions = tslib_1.__rest(_a, ["path"]);
const headers = Object.assign(Object.assign({}, requestOptions.headers), { "Content-Type": "application/json" });
const bearerToken = options.getBearerToken();
if (bearerToken) {
headers.Authorization = `Bearer ${bearerToken}`;
}
/**
* We add this header to identify the client type, but not when
* in the browser and the baseUrl differs from your location. This
* would create a CORS error
*/
if (typeof window === "undefined" ||
options.baseUrl.includes(window.location.origin)) {
headers["x-codesandbox-client"] = options.clientType;
}
const url = options.baseUrl + path;
return handleRestResponse(options
.onRequest(Object.assign(Object.assign({}, requestOptions), { url,
headers }))
.then((response) => {
options.onResponse(response);
return response;
}), url);
};
}
exports.createRESTRequester = createRESTRequester;
const customResponseHandlers = [
(path, data) => path.includes("/v1/sandboxes/") && path.endsWith("/env") ? data : undefined,
(path, data) => (path.includes("/beta/repos/secrets") ? data : undefined),
(path, data) => {
// The sandbox path, should really use a regexp here though
if (path.includes("/v1/sandboxes/") && !path.endsWith("/env")) {
if (!("data" in data) || data.data == null) {
return undefined;
}
const sandboxData = data.data;
return {
data: Object.keys(sandboxData).reduce((aggr, key) => {
// Do not camelize the keys of NPM dependencies
aggr[camelize(key)] =
key === "npm_dependencies"
? sandboxData[key]
: camelizeKeys(sandboxData[key]);
return aggr;
}, {}),
};
}
return undefined;
},
];
/**
* The response of REST is in the format of { data?, errors? } | data
*/
function handleRestResponse(responsePromise, requestUrl) {
return responsePromise.then((response) => {
try {
if (response.status === 404) {
throw new ApiResponseError(response.status, "Not found");
}
if (response.status === 402) {
throw new ApiResponseError(response.status, "WORKSPACE_FROZEN");
}
// Delete operations return a 204 (no-content) which cannot be deserialized as json
// Since there's no response object, we pass an empty object
if (response.status === 204) {
return {};
}
if (response.status >= 200 && response.status < 300) {
const parsedResponse = JSON.parse(response.data || "{}");
const customResponse = customResponseHandlers
.map((cb) => cb(requestUrl, parsedResponse))
.filter((value) => value !== undefined)[0];
const isGQLErrorResponse = !parsedResponse.data && Array.isArray(parsedResponse.errors);
if (isGQLErrorResponse) {
throw new ApiResponseError(response.status, `\n${JSON.stringify(parsedResponse.errors, null, 2)}`, parsedResponse.errors[0].message);
}
return (customResponse
? customResponse
: camelizeKeys(parsedResponse));
}
// At this point we can only have error states
const errorData = response.data
? JSON.parse(response.data)
: undefined;
// When getting a response with a "data" property we also possibly get "errors", which
// is either an array of errors or has a "detail" property with an array of errors
if (errorData && "errors" in errorData) {
let errors;
if (Array.isArray(errorData.errors)) {
errors = errorData.errors;
}
else if ("id" in errorData.errors) {
errors = errorData.errors.id;
}
else if ("code" in errorData.errors) {
errors = errorData.errors.code;
}
else if (errorData.errors.detail) {
errors = errorData.errors.detail;
}
else {
errors = ["Unknown error"];
}
throw new ApiResponseError(response.status, `\n${errors.join("\n")}`, errors[0]);
}
if (errorData && "error" in errorData) {
throw new ApiResponseError(response.status, `\n${typeof errorData.error === "string"
? errorData.error
: errorData.error.message}`);
}
if (errorData && "code" in errorData && "message" in errorData) {
throw new ApiResponseError(response.status, `\n${JSON.stringify(errorData.message)}`);
}
throw new ApiResponseError(response.status, `Unknown error`);
}
catch (error) {
if (error instanceof ApiResponseError) {
throw error;
}
throw new ApiResponseError(response.status, `- URL: ${requestUrl}
- DATA: ${String(response.data)}
- ERROR: ${String(error)}
`);
}
});
}
exports.handleRestResponse = handleRestResponse;
function getPopupOffset({ width, height }) {
const wLeft = window.screenLeft ? window.screenLeft : window.screenX;
const wTop = window.screenTop ? window.screenTop : window.screenY;
const left = wLeft + window.innerWidth / 2 - width / 2;
const top = wTop + window.innerHeight / 2 - height / 2;
return { top, left };
}
function getPopupSize() {
return { width: 1020, height: 618 };
}
function getPopupDimensions() {
const { width, height } = getPopupSize();
const { top, left } = getPopupOffset({ width, height });
return `width=${width},height=${height},top=${top},left=${left}`;
}
function openPopup(url, name) {
let closeResolver;
const popup = window.open(url, name, `scrollbars=no,toolbar=no,location=no,titlebar=no,directories=no,status=no,menubar=no, ${getPopupDimensions()}`);
if (popup) {
const timer = setInterval(function () {
if (popup.closed) {
clearInterval(timer);
closeResolver();
}
}, 500);
}
const close = () => {
popup === null || popup === void 0 ? void 0 : popup.close();
};
const closePromise = new Promise((resolve) => {
closeResolver = resolve;
});
return {
close,
closePromise,
};
}
exports.openPopup = openPopup;
function waitForMessage(cb) {
return new Promise((resolve) => {
window.addEventListener("message", function onMessage(event) {
if (typeof cb === "function" && cb(event.data)) {
window.removeEventListener("message", onMessage);
resolve(event.data);
}
});
});
}
exports.waitForMessage = waitForMessage;
/** Auth utils */
// Used at the sign in, 1:1 map to an actual GitHub scope.
exports.GITHUB_BASE_SCOPE = "user:email";
// Map scope options to the corresponding GitHub scopes.
exports.MAP_GITHUB_SCOPE_OPTIONS = {
public_repos: "public_repo,read:org,workflow",
private_repos: "repo,read:org,workflow",
};
//# sourceMappingURL=utils.js.map