mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-13 04:33:42 +08:00
feat: sync upstream code
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { ApiPath, DEFAULT_API_HOST } from "../constant";
|
||||
|
||||
export function corsPath(path: string) {
|
||||
const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_API_HOST}` : "";
|
||||
|
||||
if (baseUrl === "" && path === "") {
|
||||
return "";
|
||||
}
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
|
||||
if (!path.endsWith("/")) {
|
||||
path += "/";
|
||||
}
|
||||
|
||||
return `${baseUrl}${path}`;
|
||||
}
|
||||
246
app/utils/hmac.ts
Normal file
246
app/utils/hmac.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
// From https://gist.github.com/guillermodlpa/f6d955f838e9b10d1ef95b8e259b2c58
|
||||
// From https://gist.github.com/stevendesu/2d52f7b5e1f1184af3b667c0b5e054b8
|
||||
|
||||
// To ensure cross-browser support even without a proper SubtleCrypto
|
||||
// impelmentation (or without access to the impelmentation, as is the case with
|
||||
// Chrome loaded over HTTP instead of HTTPS), this library can create SHA-256
|
||||
// HMAC signatures using nothing but raw JavaScript
|
||||
|
||||
/* eslint-disable no-magic-numbers, id-length, no-param-reassign, new-cap */
|
||||
|
||||
// By giving internal functions names that we can mangle, future calls to
|
||||
// them are reduced to a single byte (minor space savings in minified file)
|
||||
const uint8Array = Uint8Array;
|
||||
const uint32Array = Uint32Array;
|
||||
const pow = Math.pow;
|
||||
|
||||
// Will be initialized below
|
||||
// Using a Uint32Array instead of a simple array makes the minified code
|
||||
// a bit bigger (we lose our `unshift()` hack), but comes with huge
|
||||
// performance gains
|
||||
const DEFAULT_STATE = new uint32Array(8);
|
||||
const ROUND_CONSTANTS: number[] = [];
|
||||
|
||||
// Reusable object for expanded message
|
||||
// Using a Uint32Array instead of a simple array makes the minified code
|
||||
// 7 bytes larger, but comes with huge performance gains
|
||||
const M = new uint32Array(64);
|
||||
|
||||
// After minification the code to compute the default state and round
|
||||
// constants is smaller than the output. More importantly, this serves as a
|
||||
// good educational aide for anyone wondering where the magic numbers come
|
||||
// from. No magic numbers FTW!
|
||||
function getFractionalBits(n: number) {
|
||||
return ((n - (n | 0)) * pow(2, 32)) | 0;
|
||||
}
|
||||
|
||||
let n = 2;
|
||||
let nPrime = 0;
|
||||
while (nPrime < 64) {
|
||||
// isPrime() was in-lined from its original function form to save
|
||||
// a few bytes
|
||||
let isPrime = true;
|
||||
// Math.sqrt() was replaced with pow(n, 1/2) to save a few bytes
|
||||
// var sqrtN = pow(n, 1 / 2);
|
||||
// So technically to determine if a number is prime you only need to
|
||||
// check numbers up to the square root. However this function only runs
|
||||
// once and we're only computing the first 64 primes (up to 311), so on
|
||||
// any modern CPU this whole function runs in a couple milliseconds.
|
||||
// By going to n / 2 instead of sqrt(n) we net 8 byte savings and no
|
||||
// scaling performance cost
|
||||
for (let factor = 2; factor <= n / 2; factor++) {
|
||||
if (n % factor === 0) {
|
||||
isPrime = false;
|
||||
}
|
||||
}
|
||||
if (isPrime) {
|
||||
if (nPrime < 8) {
|
||||
DEFAULT_STATE[nPrime] = getFractionalBits(pow(n, 1 / 2));
|
||||
}
|
||||
ROUND_CONSTANTS[nPrime] = getFractionalBits(pow(n, 1 / 3));
|
||||
|
||||
nPrime++;
|
||||
}
|
||||
|
||||
n++;
|
||||
}
|
||||
|
||||
// For cross-platform support we need to ensure that all 32-bit words are
|
||||
// in the same endianness. A UTF-8 TextEncoder will return BigEndian data,
|
||||
// so upon reading or writing to our ArrayBuffer we'll only swap the bytes
|
||||
// if our system is LittleEndian (which is about 99% of CPUs)
|
||||
const LittleEndian = !!new uint8Array(new uint32Array([1]).buffer)[0];
|
||||
|
||||
function convertEndian(word: number) {
|
||||
if (LittleEndian) {
|
||||
return (
|
||||
// byte 1 -> byte 4
|
||||
(word >>> 24) |
|
||||
// byte 2 -> byte 3
|
||||
(((word >>> 16) & 0xff) << 8) |
|
||||
// byte 3 -> byte 2
|
||||
((word & 0xff00) << 8) |
|
||||
// byte 4 -> byte 1
|
||||
(word << 24)
|
||||
);
|
||||
} else {
|
||||
return word;
|
||||
}
|
||||
}
|
||||
|
||||
function rightRotate(word: number, bits: number) {
|
||||
return (word >>> bits) | (word << (32 - bits));
|
||||
}
|
||||
|
||||
function sha256(data: Uint8Array) {
|
||||
// Copy default state
|
||||
const STATE = DEFAULT_STATE.slice();
|
||||
|
||||
// Caching this reduces occurrences of ".length" in minified JavaScript
|
||||
// 3 more byte savings! :D
|
||||
const legth = data.length;
|
||||
|
||||
// Pad data
|
||||
const bitLength = legth * 8;
|
||||
const newBitLength = 512 - ((bitLength + 64) % 512) - 1 + bitLength + 65;
|
||||
|
||||
// "bytes" and "words" are stored BigEndian
|
||||
const bytes = new uint8Array(newBitLength / 8);
|
||||
const words = new uint32Array(bytes.buffer);
|
||||
|
||||
bytes.set(data, 0);
|
||||
// Append a 1
|
||||
bytes[legth] = 0b10000000;
|
||||
// Store length in BigEndian
|
||||
words[words.length - 1] = convertEndian(bitLength);
|
||||
|
||||
// Loop iterator (avoid two instances of "var") -- saves 2 bytes
|
||||
let round;
|
||||
|
||||
// Process blocks (512 bits / 64 bytes / 16 words at a time)
|
||||
for (let block = 0; block < newBitLength / 32; block += 16) {
|
||||
const workingState = STATE.slice();
|
||||
|
||||
// Rounds
|
||||
for (round = 0; round < 64; round++) {
|
||||
let MRound;
|
||||
// Expand message
|
||||
if (round < 16) {
|
||||
// Convert to platform Endianness for later math
|
||||
MRound = convertEndian(words[block + round]);
|
||||
} else {
|
||||
const gamma0x = M[round - 15];
|
||||
const gamma1x = M[round - 2];
|
||||
MRound =
|
||||
M[round - 7] +
|
||||
M[round - 16] +
|
||||
(rightRotate(gamma0x, 7) ^
|
||||
rightRotate(gamma0x, 18) ^
|
||||
(gamma0x >>> 3)) +
|
||||
(rightRotate(gamma1x, 17) ^
|
||||
rightRotate(gamma1x, 19) ^
|
||||
(gamma1x >>> 10));
|
||||
}
|
||||
|
||||
// M array matches platform endianness
|
||||
M[round] = MRound |= 0;
|
||||
|
||||
// Computation
|
||||
const t1 =
|
||||
(rightRotate(workingState[4], 6) ^
|
||||
rightRotate(workingState[4], 11) ^
|
||||
rightRotate(workingState[4], 25)) +
|
||||
((workingState[4] & workingState[5]) ^
|
||||
(~workingState[4] & workingState[6])) +
|
||||
workingState[7] +
|
||||
MRound +
|
||||
ROUND_CONSTANTS[round];
|
||||
const t2 =
|
||||
(rightRotate(workingState[0], 2) ^
|
||||
rightRotate(workingState[0], 13) ^
|
||||
rightRotate(workingState[0], 22)) +
|
||||
((workingState[0] & workingState[1]) ^
|
||||
(workingState[2] & (workingState[0] ^ workingState[1])));
|
||||
for (let i = 7; i > 0; i--) {
|
||||
workingState[i] = workingState[i - 1];
|
||||
}
|
||||
workingState[0] = (t1 + t2) | 0;
|
||||
workingState[4] = (workingState[4] + t1) | 0;
|
||||
}
|
||||
|
||||
// Update state
|
||||
for (round = 0; round < 8; round++) {
|
||||
STATE[round] = (STATE[round] + workingState[round]) | 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally the state needs to be converted to BigEndian for output
|
||||
// And we want to return a Uint8Array, not a Uint32Array
|
||||
return new uint8Array(
|
||||
new uint32Array(
|
||||
STATE.map(function (val) {
|
||||
return convertEndian(val);
|
||||
}),
|
||||
).buffer,
|
||||
);
|
||||
}
|
||||
|
||||
function hmac(key: Uint8Array, data: ArrayLike<number>) {
|
||||
if (key.length > 64) key = sha256(key);
|
||||
|
||||
if (key.length < 64) {
|
||||
const tmp = new Uint8Array(64);
|
||||
tmp.set(key, 0);
|
||||
key = tmp;
|
||||
}
|
||||
|
||||
// Generate inner and outer keys
|
||||
const innerKey = new Uint8Array(64);
|
||||
const outerKey = new Uint8Array(64);
|
||||
for (let i = 0; i < 64; i++) {
|
||||
innerKey[i] = 0x36 ^ key[i];
|
||||
outerKey[i] = 0x5c ^ key[i];
|
||||
}
|
||||
|
||||
// Append the innerKey
|
||||
const msg = new Uint8Array(data.length + 64);
|
||||
msg.set(innerKey, 0);
|
||||
msg.set(data, 64);
|
||||
|
||||
// Has the previous message and append the outerKey
|
||||
const result = new Uint8Array(64 + 32);
|
||||
result.set(outerKey, 0);
|
||||
result.set(sha256(msg), 64);
|
||||
|
||||
// Hash the previous message
|
||||
return sha256(result);
|
||||
}
|
||||
|
||||
// Convert a string to a Uint8Array, SHA-256 it, and convert back to string
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
export function sign(
|
||||
inputKey: string | Uint8Array,
|
||||
inputData: string | Uint8Array,
|
||||
) {
|
||||
const key =
|
||||
typeof inputKey === "string" ? encoder.encode(inputKey) : inputKey;
|
||||
const data =
|
||||
typeof inputData === "string" ? encoder.encode(inputData) : inputData;
|
||||
return hmac(key, data);
|
||||
}
|
||||
|
||||
export function hex(bin: Uint8Array) {
|
||||
return bin.reduce((acc, val) => {
|
||||
const hexVal = "00" + val.toString(16);
|
||||
return acc + hexVal.substring(hexVal.length - 2);
|
||||
}, "");
|
||||
}
|
||||
|
||||
export function hash(str: string) {
|
||||
return hex(sha256(encoder.encode(str)));
|
||||
}
|
||||
|
||||
export function hashWithSecret(str: string, secret: string) {
|
||||
return hex(sign(secret, str)).toString();
|
||||
}
|
||||
@@ -1,12 +1,53 @@
|
||||
import { DEFAULT_MODELS } from "../constant";
|
||||
import { LLMModel } from "../client/api";
|
||||
|
||||
const CustomSeq = {
|
||||
val: -1000, //To ensure the custom model located at front, start from -1000, refer to constant.ts
|
||||
cache: new Map<string, number>(),
|
||||
next: (id: string) => {
|
||||
if (CustomSeq.cache.has(id)) {
|
||||
return CustomSeq.cache.get(id) as number;
|
||||
} else {
|
||||
let seq = CustomSeq.val++;
|
||||
CustomSeq.cache.set(id, seq);
|
||||
return seq;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const customProvider = (providerName: string) => ({
|
||||
id: providerName.toLowerCase(),
|
||||
providerName: providerName,
|
||||
providerType: "custom",
|
||||
sorted: CustomSeq.next(providerName),
|
||||
});
|
||||
|
||||
/**
|
||||
* Sorts an array of models based on specified rules.
|
||||
*
|
||||
* First, sorted by provider; if the same, sorted by model
|
||||
*/
|
||||
const sortModelTable = (models: ReturnType<typeof collectModels>) =>
|
||||
models.sort((a, b) => {
|
||||
if (a.provider && b.provider) {
|
||||
let cmp = a.provider.sorted - b.provider.sorted;
|
||||
return cmp === 0 ? a.sorted - b.sorted : cmp;
|
||||
} else {
|
||||
return a.sorted - b.sorted;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* get model name and provider from a formatted string,
|
||||
* e.g. `gpt-4@OpenAi` or `claude-3-5-sonnet@20240620@Google`
|
||||
* @param modelWithProvider model name with provider separated by last `@` char,
|
||||
* @returns [model, provider] tuple, if no `@` char found, provider is undefined
|
||||
*/
|
||||
export function getModelProvider(modelWithProvider: string): [string, string?] {
|
||||
const [model, provider] = modelWithProvider.split(/@(?!.*@)/);
|
||||
return [model, provider];
|
||||
}
|
||||
|
||||
export function collectModelTable(
|
||||
models: readonly LLMModel[],
|
||||
customModels: string,
|
||||
@@ -17,6 +58,7 @@ export function collectModelTable(
|
||||
available: boolean;
|
||||
name: string;
|
||||
displayName: string;
|
||||
sorted: number;
|
||||
provider?: LLMModel["provider"]; // Marked as optional
|
||||
isDefault?: boolean;
|
||||
}
|
||||
@@ -48,10 +90,10 @@ export function collectModelTable(
|
||||
);
|
||||
} else {
|
||||
// 1. find model by name, and set available value
|
||||
const [customModelName, customProviderName] = name.split("@");
|
||||
const [customModelName, customProviderName] = getModelProvider(name);
|
||||
let count = 0;
|
||||
for (const fullName in modelTable) {
|
||||
const [modelName, providerName] = fullName.split("@");
|
||||
const [modelName, providerName] = getModelProvider(fullName);
|
||||
if (
|
||||
customModelName == modelName &&
|
||||
(customProviderName === undefined ||
|
||||
@@ -71,7 +113,7 @@ export function collectModelTable(
|
||||
}
|
||||
// 2. if model not exists, create new model with available value
|
||||
if (count === 0) {
|
||||
let [customModelName, customProviderName] = name.split("@");
|
||||
let [customModelName, customProviderName] = getModelProvider(name);
|
||||
const provider = customProvider(
|
||||
customProviderName || customModelName,
|
||||
);
|
||||
@@ -84,6 +126,7 @@ export function collectModelTable(
|
||||
displayName: displayName || customModelName,
|
||||
available,
|
||||
provider, // Use optional chaining
|
||||
sorted: CustomSeq.next(`${customModelName}@${provider?.id}`),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -99,13 +142,16 @@ export function collectModelTableWithDefaultModel(
|
||||
) {
|
||||
let modelTable = collectModelTable(models, customModels);
|
||||
if (defaultModel && defaultModel !== "") {
|
||||
if (defaultModel.includes('@')) {
|
||||
if (defaultModel.includes("@")) {
|
||||
if (defaultModel in modelTable) {
|
||||
modelTable[defaultModel].isDefault = true;
|
||||
}
|
||||
} else {
|
||||
for (const key of Object.keys(modelTable)) {
|
||||
if (modelTable[key].available && key.split('@').shift() == defaultModel) {
|
||||
if (
|
||||
modelTable[key].available &&
|
||||
getModelProvider(key)[0] == defaultModel
|
||||
) {
|
||||
modelTable[key].isDefault = true;
|
||||
break;
|
||||
}
|
||||
@@ -123,7 +169,9 @@ export function collectModels(
|
||||
customModels: string,
|
||||
) {
|
||||
const modelTable = collectModelTable(models, customModels);
|
||||
const allModels = Object.values(modelTable);
|
||||
let allModels = Object.values(modelTable);
|
||||
|
||||
allModels = sortModelTable(allModels);
|
||||
|
||||
return allModels;
|
||||
}
|
||||
@@ -138,7 +186,10 @@ export function collectModelsWithDefaultModel(
|
||||
customModels,
|
||||
defaultModel,
|
||||
);
|
||||
const allModels = Object.values(modelTable);
|
||||
let allModels = Object.values(modelTable);
|
||||
|
||||
allModels = sortModelTable(allModels);
|
||||
|
||||
return allModels;
|
||||
}
|
||||
|
||||
|
||||
108
app/utils/stream.ts
Normal file
108
app/utils/stream.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
// using tauri command to send request
|
||||
// see src-tauri/src/stream.rs, and src-tauri/src/main.rs
|
||||
// 1. invoke('stream_fetch', {url, method, headers, body}), get response with headers.
|
||||
// 2. listen event: `stream-response` multi times to get body
|
||||
|
||||
type ResponseEvent = {
|
||||
id: number;
|
||||
payload: {
|
||||
request_id: number;
|
||||
status?: number;
|
||||
chunk?: number[];
|
||||
};
|
||||
};
|
||||
|
||||
type StreamResponse = {
|
||||
request_id: number;
|
||||
status: number;
|
||||
status_text: string;
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
export function fetch(url: string, options?: RequestInit): Promise<Response> {
|
||||
if (window.__TAURI__) {
|
||||
const {
|
||||
signal,
|
||||
method = "GET",
|
||||
headers: _headers = {},
|
||||
body = [],
|
||||
} = options || {};
|
||||
let unlisten: Function | undefined;
|
||||
let setRequestId: Function | undefined;
|
||||
const requestIdPromise = new Promise((resolve) => (setRequestId = resolve));
|
||||
const ts = new TransformStream();
|
||||
const writer = ts.writable.getWriter();
|
||||
|
||||
let closed = false;
|
||||
const close = () => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
unlisten && unlisten();
|
||||
writer.ready.then(() => {
|
||||
writer.close().catch((e) => console.error(e));
|
||||
});
|
||||
};
|
||||
|
||||
if (signal) {
|
||||
signal.addEventListener("abort", () => close());
|
||||
}
|
||||
// @ts-ignore 2. listen response multi times, and write to Response.body
|
||||
window.__TAURI__.event
|
||||
.listen("stream-response", (e: ResponseEvent) =>
|
||||
requestIdPromise.then((request_id) => {
|
||||
const { request_id: rid, chunk, status } = e?.payload || {};
|
||||
if (request_id != rid) {
|
||||
return;
|
||||
}
|
||||
if (chunk) {
|
||||
writer.ready.then(() => {
|
||||
writer.write(new Uint8Array(chunk));
|
||||
});
|
||||
} else if (status === 0) {
|
||||
// end of body
|
||||
close();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.then((u: Function) => (unlisten = u));
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "application/json, text/plain, */*",
|
||||
"Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
|
||||
"User-Agent": navigator.userAgent,
|
||||
};
|
||||
for (const item of new Headers(_headers || {})) {
|
||||
headers[item[0]] = item[1];
|
||||
}
|
||||
return window.__TAURI__
|
||||
.invoke("stream_fetch", {
|
||||
method: method.toUpperCase(),
|
||||
url,
|
||||
headers,
|
||||
// TODO FormData
|
||||
body:
|
||||
typeof body === "string"
|
||||
? Array.from(new TextEncoder().encode(body))
|
||||
: [],
|
||||
})
|
||||
.then((res: StreamResponse) => {
|
||||
const { request_id, status, status_text: statusText, headers } = res;
|
||||
setRequestId?.(request_id);
|
||||
const response = new Response(ts.readable, {
|
||||
status,
|
||||
statusText,
|
||||
headers,
|
||||
});
|
||||
if (status >= 300) {
|
||||
setTimeout(close, 100);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("stream error", e);
|
||||
// throw e;
|
||||
return new Response("", { status: 599 });
|
||||
});
|
||||
}
|
||||
return window.fetch(url, options);
|
||||
}
|
||||
102
app/utils/tencent.ts
Normal file
102
app/utils/tencent.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { sign, hash as getHash, hex } from "./hmac";
|
||||
|
||||
// 使用 SHA-256 和 secret 进行 HMAC 加密
|
||||
function sha256(message: any, secret: any, encoding?: string) {
|
||||
const result = sign(secret, message);
|
||||
return encoding == "hex" ? hex(result).toString() : result;
|
||||
}
|
||||
|
||||
function getDate(timestamp: number) {
|
||||
const date = new Date(timestamp * 1000);
|
||||
const year = date.getUTCFullYear();
|
||||
const month = ("0" + (date.getUTCMonth() + 1)).slice(-2);
|
||||
const day = ("0" + date.getUTCDate()).slice(-2);
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export async function getHeader(
|
||||
payload: any,
|
||||
SECRET_ID: string,
|
||||
SECRET_KEY: string,
|
||||
) {
|
||||
// https://cloud.tencent.com/document/api/1729/105701
|
||||
|
||||
const endpoint = "hunyuan.tencentcloudapi.com";
|
||||
const service = "hunyuan";
|
||||
const region = ""; // optional
|
||||
const action = "ChatCompletions";
|
||||
const version = "2023-09-01";
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
//时间处理, 获取世界时间日期
|
||||
const date = getDate(timestamp);
|
||||
|
||||
// ************* 步骤 1:拼接规范请求串 *************
|
||||
|
||||
const hashedRequestPayload = getHash(payload);
|
||||
const httpRequestMethod = "POST";
|
||||
const contentType = "application/json";
|
||||
const canonicalUri = "/";
|
||||
const canonicalQueryString = "";
|
||||
const canonicalHeaders =
|
||||
`content-type:${contentType}\n` +
|
||||
"host:" +
|
||||
endpoint +
|
||||
"\n" +
|
||||
"x-tc-action:" +
|
||||
action.toLowerCase() +
|
||||
"\n";
|
||||
const signedHeaders = "content-type;host;x-tc-action";
|
||||
|
||||
const canonicalRequest = [
|
||||
httpRequestMethod,
|
||||
canonicalUri,
|
||||
canonicalQueryString,
|
||||
canonicalHeaders,
|
||||
signedHeaders,
|
||||
hashedRequestPayload,
|
||||
].join("\n");
|
||||
|
||||
// ************* 步骤 2:拼接待签名字符串 *************
|
||||
const algorithm = "TC3-HMAC-SHA256";
|
||||
const hashedCanonicalRequest = getHash(canonicalRequest);
|
||||
const credentialScope = date + "/" + service + "/" + "tc3_request";
|
||||
const stringToSign =
|
||||
algorithm +
|
||||
"\n" +
|
||||
timestamp +
|
||||
"\n" +
|
||||
credentialScope +
|
||||
"\n" +
|
||||
hashedCanonicalRequest;
|
||||
|
||||
// ************* 步骤 3:计算签名 *************
|
||||
const kDate = sha256(date, "TC3" + SECRET_KEY);
|
||||
const kService = sha256(service, kDate);
|
||||
const kSigning = sha256("tc3_request", kService);
|
||||
const signature = sha256(stringToSign, kSigning, "hex");
|
||||
|
||||
// ************* 步骤 4:拼接 Authorization *************
|
||||
const authorization =
|
||||
algorithm +
|
||||
" " +
|
||||
"Credential=" +
|
||||
SECRET_ID +
|
||||
"/" +
|
||||
credentialScope +
|
||||
", " +
|
||||
"SignedHeaders=" +
|
||||
signedHeaders +
|
||||
", " +
|
||||
"Signature=" +
|
||||
signature;
|
||||
|
||||
return {
|
||||
Authorization: authorization,
|
||||
"Content-Type": contentType,
|
||||
Host: endpoint,
|
||||
"X-TC-Action": action,
|
||||
"X-TC-Timestamp": timestamp.toString(),
|
||||
"X-TC-Version": version,
|
||||
"X-TC-Region": region,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user