Initial commit

This commit is contained in:
decolua 2026-01-05 09:58:59 +07:00
commit 3857598de4
159 changed files with 14537 additions and 0 deletions

92
src/shared/utils/api.js Normal file
View 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 };

View 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
View 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
View 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
View 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`;
}

View 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([]);
}

View 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';
}