@turnkey/http
Version:
Typed HTTP client for interacting with Turnkey API
164 lines (161 loc) • 5.41 kB
JavaScript
import { signWithApiKey } from '@turnkey/api-key-stamper';
import { fetch } from './universal.mjs';
import { getConfig, getBrowserConfig } from './config.mjs';
import { stringToBase64urlString } from '@turnkey/encoding';
import { getWebAuthnAssertion } from './webauthn.mjs';
const sharedHeaders = {};
const sharedRequestOptions = {
redirect: "follow",
};
/**
* @deprecated
*/
async function signedRequest(input) {
const { uri: inputUri, query: inputQuery = {}, substitution: inputSubstitution = {}, body: inputBody = {}, } = input;
const url = constructUrl({
uri: inputUri,
query: inputQuery,
substitution: inputSubstitution,
});
const body = JSON.stringify(inputBody);
const stamp = await getWebAuthnAssertion(body, input.options);
return {
url: url.toString(),
body,
stamp,
};
}
async function request(input) {
const { uri: inputUri, method, headers: inputHeaders = {}, query: inputQuery = {}, substitution: inputSubstitution = {}, body: inputBody = {}, } = input;
const url = constructUrl({
uri: inputUri,
query: inputQuery,
substitution: inputSubstitution,
});
const { sealedBody, xStamp } = await sealAndStampRequestBody({
body: inputBody,
});
const response = await fetch(url.toString(), {
...sharedRequestOptions,
method,
headers: {
...sharedHeaders,
...inputHeaders,
"X-Stamp": xStamp,
},
body: sealedBody,
});
if (!response.ok) {
// Can't use native `cause` here because it's not well supported on Node v16
// https://node.green/#ES2022-features-Error-cause-property
let res;
try {
res = await response.json();
}
catch (_) {
throw new Error(`${response.status} ${response.statusText}`);
}
throw new TurnkeyRequestError(res);
}
const data = await response.json();
return data;
}
function constructUrl(input) {
const { uri, query, substitution } = input;
const baseUrl = getBaseUrl();
const url = new URL(substitutePath(uri, substitution), baseUrl);
for (const key in query) {
const value = query[key];
if (Array.isArray(value)) {
for (const item of value) {
url.searchParams.append(key, item);
}
}
else {
url.searchParams.append(key, value ?? "");
}
}
return url;
}
function getBaseUrl() {
try {
const { baseUrl } = getConfig();
return baseUrl;
}
catch (e) {
const { baseUrl } = getBrowserConfig();
return baseUrl;
}
}
function substitutePath(uri, substitutionMap) {
let result = uri;
const keyList = Object.keys(substitutionMap);
for (const key of keyList) {
const output = result.replaceAll(`{${key}}`, substitutionMap[key]);
invariant(output !== result, `Substitution error: cannot find "${key}" in URI "${uri}". \`substitutionMap\`: ${JSON.stringify(substitutionMap)}`);
result = output;
}
invariant(!/\{.*\}/.test(result), `Substitution error: found unsubstituted components in "${result}"`);
return result;
}
function invariant(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function stableStringify(input) {
return JSON.stringify(input);
}
/**
* Seals and stamps the request body with your Turnkey API credentials.
*
* You can either:
* - Before calling `sealAndStampRequestBody(...)`, initialize with your Turnkey API credentials via `init(...)`
* - Or, provide `apiPublicKey` and `apiPrivateKey` here as arguments
*/
async function sealAndStampRequestBody(input) {
const { body } = input;
let { apiPublicKey, apiPrivateKey } = input;
if (!apiPublicKey) {
const config = getConfig();
apiPublicKey = config.apiPublicKey;
}
if (!apiPrivateKey) {
const config = getConfig();
apiPrivateKey = config.apiPrivateKey;
}
const sealedBody = stableStringify(body);
const signature = await signWithApiKey({
content: sealedBody,
privateKey: apiPrivateKey,
publicKey: apiPublicKey,
});
const sealedStamp = stableStringify({
publicKey: apiPublicKey,
scheme: "SIGNATURE_SCHEME_TK_API_P256",
signature: signature,
});
const xStamp = stringToBase64urlString(sealedStamp);
return {
sealedBody,
xStamp,
};
}
// Check if the client is an instance of TurnkeyClient. We check the name field here since the 'instanceof' operator does not work across if the http client isn't EXACTLY the same (mismatching versions).
function isHttpClient(client) {
return client?.name === "TurnkeyClient";
}
class TurnkeyRequestError extends Error {
constructor(input) {
let turnkeyErrorMessage = `Turnkey error ${input.code}: ${input.message}`;
if (input.details != null) {
turnkeyErrorMessage += ` (Details: ${JSON.stringify(input.details)})`;
}
super(turnkeyErrorMessage);
this.name = "TurnkeyRequestError";
this.details = input.details ?? null;
this.code = input.code;
}
}
export { TurnkeyRequestError, fetch, isHttpClient, request, sealAndStampRequestBody, signedRequest };
//# sourceMappingURL=base.mjs.map