chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
import { Client } from 'https://deno.land/x/postgres@v0.17.0/mod.ts';
|
||||
import { join, dirname, fromFileUrl } from 'https://deno.land/std@0.224.0/path/mod.ts';
|
||||
|
||||
/* eslint-disable no-console */
|
||||
const rawPort = Deno.env.get('PORT');
|
||||
const hasValidNumericPort = rawPort !== undefined && /^\d+$/.test(rawPort);
|
||||
const parsedPort = hasValidNumericPort ? Number(rawPort) : Number.NaN;
|
||||
const port =
|
||||
Number.isInteger(parsedPort) && parsedPort >= 1 && parsedPort <= 65535 ? parsedPort : 7133;
|
||||
const hostname = Deno.env.get('HOST') ?? '::';
|
||||
|
||||
if (rawPort !== undefined && port === 7133 && rawPort !== '7133') {
|
||||
console.warn(`Invalid PORT value "${rawPort}", falling back to 7133`);
|
||||
}
|
||||
|
||||
console.log(`Deno serverless runtime running on ${hostname}:${port}`);
|
||||
|
||||
// Configuration
|
||||
const WORKER_TIMEOUT_MS = parseInt(Deno.env.get('WORKER_TIMEOUT_MS') ?? '60000');
|
||||
|
||||
// Worker template code - loaded on first use
|
||||
let workerTemplateCode: string | null = null;
|
||||
|
||||
async function getWorkerTemplateCode(): Promise<string> {
|
||||
if (!workerTemplateCode) {
|
||||
const currentDir = dirname(fromFileUrl(import.meta.url));
|
||||
workerTemplateCode = await Deno.readTextFile(join(currentDir, 'worker-template.js'));
|
||||
}
|
||||
return workerTemplateCode;
|
||||
}
|
||||
|
||||
// Decrypt function for Deno (compatible with Node.js encryption)
|
||||
async function decryptSecret(ciphertext: string, key: string): Promise<string> {
|
||||
try {
|
||||
const parts = ciphertext.split(':');
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Invalid ciphertext format');
|
||||
}
|
||||
|
||||
// Get the encryption key by hashing the JWT secret
|
||||
const keyData = new TextEncoder().encode(key);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', keyData);
|
||||
const cryptoKey = await crypto.subtle.importKey('raw', hashBuffer, { name: 'AES-GCM' }, false, [
|
||||
'decrypt',
|
||||
]);
|
||||
|
||||
// Extract IV, auth tag, and encrypted data
|
||||
const iv = Uint8Array.from(parts[0].match(/.{2}/g)!.map((byte) => parseInt(byte, 16)));
|
||||
const authTag = Uint8Array.from(parts[1].match(/.{2}/g)!.map((byte) => parseInt(byte, 16)));
|
||||
const encrypted = Uint8Array.from(parts[2].match(/.{2}/g)!.map((byte) => parseInt(byte, 16)));
|
||||
|
||||
// Combine encrypted data and auth tag (GCM expects them together)
|
||||
const cipherData = new Uint8Array(encrypted.length + authTag.length);
|
||||
cipherData.set(encrypted);
|
||||
cipherData.set(authTag, encrypted.length);
|
||||
|
||||
// Decrypt
|
||||
const decryptedBuffer = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
cryptoKey,
|
||||
cipherData
|
||||
);
|
||||
|
||||
return new TextDecoder().decode(decryptedBuffer);
|
||||
} catch (error) {
|
||||
console.error('Failed to decrypt secret:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Database connection
|
||||
const dbConfig = {
|
||||
user: Deno.env.get('POSTGRES_USER') || 'postgres',
|
||||
password: Deno.env.get('POSTGRES_PASSWORD') || 'postgres',
|
||||
database: Deno.env.get('POSTGRES_DB') || 'insforge',
|
||||
hostname: Deno.env.get('POSTGRES_HOST') || 'postgres',
|
||||
port: parseInt(Deno.env.get('POSTGRES_PORT') || '5432', 10),
|
||||
};
|
||||
|
||||
// Get function code from database
|
||||
async function getFunctionCode(slug: string): Promise<string | null> {
|
||||
const client = new Client(dbConfig);
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
|
||||
const result = await client.queryObject<{ code: string }>`
|
||||
SELECT code FROM functions.definitions
|
||||
WHERE slug = ${slug} AND status = 'active'
|
||||
`;
|
||||
|
||||
if (!result.rows.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.rows[0].code;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching function ${slug}:`, error);
|
||||
return null;
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Get all secrets from main secrets table and decrypt them
|
||||
async function getFunctionSecrets(): Promise<Record<string, string>> {
|
||||
const client = new Client(dbConfig);
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
|
||||
// Get the encryption key from environment
|
||||
const encryptionKey = Deno.env.get('ENCRYPTION_KEY') || Deno.env.get('JWT_SECRET');
|
||||
if (!encryptionKey) {
|
||||
console.error('No encryption key available for decrypting secrets');
|
||||
return {};
|
||||
}
|
||||
|
||||
// Fetch all active secrets from system.secrets table
|
||||
const result = await client.queryObject<{
|
||||
key: string;
|
||||
value_ciphertext: string;
|
||||
}>`
|
||||
SELECT key, value_ciphertext
|
||||
FROM system.secrets
|
||||
WHERE is_active = true
|
||||
AND (expires_at IS NULL OR expires_at > NOW())
|
||||
`;
|
||||
|
||||
const secrets: Record<string, string> = {};
|
||||
|
||||
// Decrypt each secret
|
||||
for (const row of result.rows) {
|
||||
try {
|
||||
secrets[row.key] = await decryptSecret(row.value_ciphertext, encryptionKey);
|
||||
} catch (error) {
|
||||
console.error(`Failed to decrypt secret ${row.key}:`, error);
|
||||
// Skip this secret if decryption fails
|
||||
}
|
||||
}
|
||||
|
||||
return secrets;
|
||||
} catch (error) {
|
||||
console.error('Error fetching secrets:', error);
|
||||
return {};
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Execute function in isolated worker
|
||||
async function executeInWorker(code: string, request: Request): Promise<Response> {
|
||||
// Get worker template
|
||||
const template = await getWorkerTemplateCode();
|
||||
|
||||
// Fetch all function secrets
|
||||
const secrets = await getFunctionSecrets();
|
||||
|
||||
// Create blob for worker
|
||||
const workerBlob = new Blob([template], { type: 'application/javascript' });
|
||||
const workerUrl = URL.createObjectURL(workerBlob);
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
const worker = new Worker(workerUrl, {
|
||||
type: 'module',
|
||||
deno: {
|
||||
permissions: {
|
||||
// RESTRICTED: Native environment access is disabled to shield host secrets.
|
||||
// Secrets are passed explicitly via message payload.
|
||||
env: false,
|
||||
// SDK/External REQUIREMENT: Allow network access for API connectivity and external integrations.
|
||||
net: true,
|
||||
read: false,
|
||||
write: false,
|
||||
run: false,
|
||||
ffi: false,
|
||||
sys: false,
|
||||
import: false,
|
||||
hrtime: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Set timeout for worker execution
|
||||
const timeout = setTimeout(() => {
|
||||
worker.terminate();
|
||||
URL.revokeObjectURL(workerUrl);
|
||||
resolve(
|
||||
new Response(JSON.stringify({ error: 'Function timeout' }), {
|
||||
status: 504,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
);
|
||||
}, WORKER_TIMEOUT_MS);
|
||||
|
||||
// Handle worker response
|
||||
worker.onmessage = (e) => {
|
||||
clearTimeout(timeout);
|
||||
worker.terminate();
|
||||
URL.revokeObjectURL(workerUrl);
|
||||
|
||||
if (e.data.success) {
|
||||
const { response } = e.data;
|
||||
// The worker now properly sends null for bodyless responses
|
||||
resolve(
|
||||
new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
resolve(
|
||||
new Response(JSON.stringify({ error: e.data.error }), {
|
||||
status: e.data.status || 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle worker errors
|
||||
worker.onerror = (error) => {
|
||||
clearTimeout(timeout);
|
||||
worker.terminate();
|
||||
URL.revokeObjectURL(workerUrl);
|
||||
console.error('Worker error:', error);
|
||||
resolve(
|
||||
new Response(JSON.stringify({ error: 'Worker execution error' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Prepare request data
|
||||
const body = request.body ? await request.text() : null;
|
||||
const requestData = {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers),
|
||||
body,
|
||||
};
|
||||
|
||||
// Send message with code, request data, and secrets
|
||||
worker.postMessage({ code, requestData, secrets });
|
||||
});
|
||||
}
|
||||
|
||||
Deno.serve({ hostname, port }, async (req: Request) => {
|
||||
const url = new URL(req.url);
|
||||
const pathname = url.pathname;
|
||||
|
||||
// Health check
|
||||
if (pathname === '/health') {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
status: 'ok',
|
||||
runtime: 'deno',
|
||||
version: Deno.version.deno,
|
||||
typescript: Deno.version.typescript,
|
||||
v8: Deno.version.v8,
|
||||
}),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Function execution - match ONLY exact slug, no subpaths
|
||||
const slugMatch = pathname.match(/^\/([a-zA-Z0-9_-]+)$/);
|
||||
if (slugMatch) {
|
||||
const slug = slugMatch[1];
|
||||
const startTime = Date.now();
|
||||
|
||||
// Get function code from database
|
||||
const code = await getFunctionCode(slug);
|
||||
|
||||
if (!code) {
|
||||
return new Response(JSON.stringify({ error: 'Function not found or not active' }), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// Execute in worker with original request
|
||||
try {
|
||||
const response = await executeInWorker(code, req);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// Log completed invocations only
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'info',
|
||||
slug,
|
||||
method: req.method,
|
||||
status: response.status,
|
||||
duration: `${duration}ms`,
|
||||
})
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'error',
|
||||
slug,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration: `${duration}ms`,
|
||||
})
|
||||
);
|
||||
return new Response(JSON.stringify({ error: 'Function execution failed' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Runtime info
|
||||
if (pathname === '/info') {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
runtime: 'deno',
|
||||
version: Deno.version,
|
||||
env: Deno.env.get('DENO_ENV') || 'production',
|
||||
database: {
|
||||
host: dbConfig.hostname,
|
||||
database: dbConfig.database,
|
||||
},
|
||||
}),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 404
|
||||
return new Response('Not Found', { status: 404 });
|
||||
});
|
||||
Reference in New Issue
Block a user