Initial commit
This commit is contained in:
commit
3857598de4
159 changed files with 14537 additions and 0 deletions
92
src/shared/utils/api.js
Normal file
92
src/shared/utils/api.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* API utility functions for making HTTP requests
|
||||
*/
|
||||
|
||||
const DEFAULT_HEADERS = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
/**
|
||||
* Make a GET request
|
||||
* @param {string} url - API endpoint
|
||||
* @param {object} options - Fetch options
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
export async function get(url, options = {}) {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { ...DEFAULT_HEADERS, ...options.headers },
|
||||
...options,
|
||||
});
|
||||
return handleResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a POST request
|
||||
* @param {string} url - API endpoint
|
||||
* @param {object} data - Request body
|
||||
* @param {object} options - Fetch options
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
export async function post(url, data, options = {}) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { ...DEFAULT_HEADERS, ...options.headers },
|
||||
body: JSON.stringify(data),
|
||||
...options,
|
||||
});
|
||||
return handleResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a PUT request
|
||||
* @param {string} url - API endpoint
|
||||
* @param {object} data - Request body
|
||||
* @param {object} options - Fetch options
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
export async function put(url, data, options = {}) {
|
||||
const response = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers: { ...DEFAULT_HEADERS, ...options.headers },
|
||||
body: JSON.stringify(data),
|
||||
...options,
|
||||
});
|
||||
return handleResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a DELETE request
|
||||
* @param {string} url - API endpoint
|
||||
* @param {object} options - Fetch options
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
export async function del(url, options = {}) {
|
||||
const response = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: { ...DEFAULT_HEADERS, ...options.headers },
|
||||
...options,
|
||||
});
|
||||
return handleResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle API response
|
||||
* @param {Response} response - Fetch response
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async function handleResponse(response) {
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(data.error || "An error occurred");
|
||||
error.status = response.status;
|
||||
error.data = data;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export default { get, post, put, del };
|
||||
|
||||
98
src/shared/utils/apiKey.js
Normal file
98
src/shared/utils/apiKey.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import crypto from "crypto";
|
||||
|
||||
const API_KEY_SECRET = process.env.API_KEY_SECRET || "endpoint-proxy-api-key-secret";
|
||||
|
||||
/**
|
||||
* Generate 6-char random keyId
|
||||
*/
|
||||
function generateKeyId() {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
for (let i = 0; i < 6; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CRC (8-char HMAC)
|
||||
*/
|
||||
function generateCrc(machineId, keyId) {
|
||||
return crypto
|
||||
.createHmac("sha256", API_KEY_SECRET)
|
||||
.update(machineId + keyId)
|
||||
.digest("hex")
|
||||
.slice(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate API key with machineId embedded
|
||||
* Format: sk-{machineId}-{keyId}-{crc8}
|
||||
* @param {string} machineId - 16-char machine ID
|
||||
* @returns {{ key: string, keyId: string }}
|
||||
*/
|
||||
export function generateApiKeyWithMachine(machineId) {
|
||||
const keyId = generateKeyId();
|
||||
const crc = generateCrc(machineId, keyId);
|
||||
const key = `sk-${machineId}-${keyId}-${crc}`;
|
||||
return { key, keyId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse API key and extract machineId + keyId
|
||||
* Supports both formats:
|
||||
* - New: sk-{machineId}-{keyId}-{crc8}
|
||||
* - Old: sk-{random8}
|
||||
* @param {string} apiKey
|
||||
* @returns {{ machineId: string, keyId: string, isNewFormat: boolean } | null}
|
||||
*/
|
||||
export function parseApiKey(apiKey) {
|
||||
if (!apiKey || !apiKey.startsWith("sk-")) return null;
|
||||
|
||||
const parts = apiKey.split("-");
|
||||
|
||||
// New format: sk-{machineId}-{keyId}-{crc8} = 4 parts
|
||||
if (parts.length === 4) {
|
||||
const [, machineId, keyId, crc] = parts;
|
||||
|
||||
// Validate CRC
|
||||
const expectedCrc = generateCrc(machineId, keyId);
|
||||
if (crc !== expectedCrc) return null;
|
||||
|
||||
return { machineId, keyId, isNewFormat: true };
|
||||
}
|
||||
|
||||
// Old format: sk-{random8} = 2 parts
|
||||
if (parts.length === 2) {
|
||||
return { machineId: null, keyId: parts[1], isNewFormat: false };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify API key CRC (only for new format)
|
||||
* @param {string} apiKey
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function verifyApiKeyCrc(apiKey) {
|
||||
const parsed = parseApiKey(apiKey);
|
||||
if (!parsed) return false;
|
||||
|
||||
// Old format doesn't have CRC, always valid if parsed
|
||||
if (!parsed.isNewFormat) return true;
|
||||
|
||||
// New format already verified in parseApiKey
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if API key is new format (contains machineId)
|
||||
* @param {string} apiKey
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isNewFormatKey(apiKey) {
|
||||
const parsed = parseApiKey(apiKey);
|
||||
return parsed?.isNewFormat === true;
|
||||
}
|
||||
|
||||
40
src/shared/utils/cloud.js
Normal file
40
src/shared/utils/cloud.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { getMachineId } from "@/shared/utils/machine";
|
||||
|
||||
// Function to get cloud URL with machine ID
|
||||
export function getCloudUrl(machineId) {
|
||||
// Get from environment or default to localhost:8787
|
||||
const cloudUrl = process.env.NEXT_PUBLIC_CLOUD_URL || "http://localhost:8787";
|
||||
return `${cloudUrl}/${machineId}/v1/chat/completions`;
|
||||
}
|
||||
|
||||
// Function to call cloud with machine ID
|
||||
export async function callCloudWithMachineId(request) {
|
||||
const machineId = await getMachineId();
|
||||
if (!machineId) {
|
||||
throw new Error("Could not get machine ID");
|
||||
}
|
||||
|
||||
const cloudUrl = getCloudUrl(machineId);
|
||||
|
||||
// Get the original request body and headers
|
||||
const body = await request.json();
|
||||
const headers = new Headers(request.headers);
|
||||
|
||||
// Remove authorization header since cloud won't need it (uses machineId instead)
|
||||
headers.delete("authorization");
|
||||
|
||||
// Call the cloud with machine ID
|
||||
const response = await fetch(cloudUrl, {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Function to periodically sync provider data to cloud (now a no-op)
|
||||
export function startProviderSync(cloudUrl, intervalMs = 900000) { // Default 15 minutes
|
||||
console.log("Frontend sync is disabled. Use backend sync instead.");
|
||||
return null;
|
||||
}
|
||||
11
src/shared/utils/cn.js
Normal file
11
src/shared/utils/cn.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Utility function to merge class names
|
||||
// Handles conditional classes and removes duplicates
|
||||
|
||||
export function cn(...classes) {
|
||||
return classes
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
32
src/shared/utils/index.js
Normal file
32
src/shared/utils/index.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Shared Utils - Export all
|
||||
export { cn } from "./cn";
|
||||
export * as api from "./api";
|
||||
|
||||
/**
|
||||
* Extract error code from error message (401, 429, 503...)
|
||||
* @param {string} lastError - Error message
|
||||
* @returns {string|null} Error code or null
|
||||
*/
|
||||
export function getErrorCode(lastError) {
|
||||
if (!lastError) return null;
|
||||
const match = lastError.match(/\b([45]\d{2})\b/);
|
||||
return match ? match[1] : "ERR";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relative time string (e.g. "5 min ago")
|
||||
* @param {string} isoDate - ISO date string
|
||||
* @returns {string} Relative time
|
||||
*/
|
||||
export function getRelativeTime(isoDate) {
|
||||
if (!isoDate) return "";
|
||||
const diff = Date.now() - new Date(isoDate).getTime();
|
||||
const mins = Math.floor(diff / 60000);
|
||||
if (mins < 1) return "just now";
|
||||
if (mins < 60) return `${mins}m ago`;
|
||||
const hours = Math.floor(mins / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}d ago`;
|
||||
}
|
||||
|
||||
18
src/shared/utils/machine.js
Normal file
18
src/shared/utils/machine.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { getConsistentMachineId } from './machineId';
|
||||
|
||||
// Get machine ID using node-machine-id with salt
|
||||
export async function getMachineId() {
|
||||
return await getConsistentMachineId();
|
||||
}
|
||||
|
||||
// Keep sync functions for backward compatibility but make them no-ops
|
||||
// (Frontend sync is disabled - use backend sync instead)
|
||||
export async function syncProviderDataToCloud(cloudUrl) {
|
||||
console.log("Frontend sync is disabled. Use backend sync instead.");
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
export async function getProvidersNeedingRefresh() {
|
||||
console.log("Frontend sync is disabled. Use backend sync instead.");
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
58
src/shared/utils/machineId.js
Normal file
58
src/shared/utils/machineId.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { machineIdSync } from 'node-machine-id';
|
||||
|
||||
/**
|
||||
* Get consistent machine ID using node-machine-id with salt
|
||||
* This ensures the same physical machine gets the same ID across runs
|
||||
*
|
||||
* @param {string} salt - Optional salt to use (defaults to environment variable)
|
||||
* @returns {Promise<string>} Machine ID (16-character base32)
|
||||
*/
|
||||
export async function getConsistentMachineId(salt = null) {
|
||||
// For server-side, use node-machine-id with salt
|
||||
const saltValue = salt || process.env.MACHINE_ID_SALT || 'endpoint-proxy-salt';
|
||||
try {
|
||||
const rawMachineId = machineIdSync();
|
||||
// Create consistent ID using salt
|
||||
const crypto = await import('crypto');
|
||||
const hashedMachineId = crypto.createHash('sha256').update(rawMachineId + saltValue).digest('hex');
|
||||
// Return only first 16 characters for brevity
|
||||
return hashedMachineId.substring(0, 16);
|
||||
} catch (error) {
|
||||
console.log('Error getting machine ID:', error);
|
||||
// Fallback to random ID if node-machine-id fails
|
||||
return crypto.randomUUID ? crypto.randomUUID() :
|
||||
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw machine ID without hashing (for debugging purposes)
|
||||
* @returns {Promise<string>} Raw machine ID
|
||||
*/
|
||||
export async function getRawMachineId() {
|
||||
// For server-side, use raw node-machine-id
|
||||
try {
|
||||
return machineIdSync();
|
||||
} catch (error) {
|
||||
console.log('Error getting raw machine ID:', error);
|
||||
// Fallback to random ID if node-machine-id fails
|
||||
return crypto.randomUUID ? crypto.randomUUID() :
|
||||
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're running in browser or server environment
|
||||
* @returns {boolean} True if in browser, false if in server
|
||||
*/
|
||||
export function isBrowser() {
|
||||
return typeof window !== 'undefined';
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue