chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["deno.window", "deno.worker"],
|
||||
"strict": true
|
||||
},
|
||||
"lint": {
|
||||
"files": {
|
||||
"include": ["./**/*.ts", "./**/*.js"]
|
||||
},
|
||||
"rules": {
|
||||
"tags": ["recommended"]
|
||||
}
|
||||
},
|
||||
"fmt": {
|
||||
"files": {
|
||||
"include": ["./**/*.ts", "./**/*.js"]
|
||||
},
|
||||
"options": {
|
||||
"useTabs": false,
|
||||
"lineWidth": 100,
|
||||
"indentWidth": 2,
|
||||
"singleQuote": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# InsForge Edge Function Examples
|
||||
|
||||
This folder contains **example serverless (edge) functions** you can deploy to InsForge.
|
||||
|
||||
## Files
|
||||
|
||||
- `demo-hello-world.js`: public function (GET/POST) with CORS + secret example (`HELLO_PREFIX`)
|
||||
- `demo-whoami.js`: authenticated function (GET) that returns the current user
|
||||
|
||||
## Deploy
|
||||
|
||||
Use the InsForge MCP tools:
|
||||
|
||||
- `create-function` with `slug` matching the function name you want (e.g. `demo-hello-world`)
|
||||
- `update-function` to redeploy after edits
|
||||
|
||||
## Invoke from a client app (SDK)
|
||||
|
||||
```js
|
||||
// GET
|
||||
await insforge.functions.invoke('demo-hello-world', { method: 'GET' })
|
||||
|
||||
// POST
|
||||
await insforge.functions.invoke('demo-hello-world', {
|
||||
body: { name: 'Gary' }
|
||||
})
|
||||
|
||||
// Authenticated GET (SDK auto-includes user token if logged in)
|
||||
await insforge.functions.invoke('demo-whoami', { method: 'GET' })
|
||||
```
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Demo: Public "hello world" function
|
||||
*
|
||||
* - Supports CORS (OPTIONS)
|
||||
* - GET: reads from query string (?name=...)
|
||||
* - POST: reads JSON body { name }
|
||||
* - Demonstrates reading secrets via Deno.env.get('KEY')
|
||||
*
|
||||
* Notes:
|
||||
* - `createClient` and `Deno` are injected by InsForge's worker template.
|
||||
* - Keep exports exactly: module.exports = async function(request) { ... }
|
||||
*/
|
||||
|
||||
module.exports = async function (request) {
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
};
|
||||
|
||||
if (request.method === 'OPTIONS') {
|
||||
return new Response(null, { status: 204, headers: corsHeaders });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
let body = null;
|
||||
if (request.method !== 'GET') {
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
body = null;
|
||||
}
|
||||
}
|
||||
|
||||
const name =
|
||||
(typeof body?.name === 'string' && body.name.trim()) ||
|
||||
(url.searchParams.get('name') || '').trim() ||
|
||||
'World';
|
||||
|
||||
// Example secret (configure in InsForge -> Functions -> Secrets):
|
||||
// - HELLO_PREFIX="Hello"
|
||||
const helloPrefix = Deno.env.get('HELLO_PREFIX') || 'Hello';
|
||||
|
||||
return new Response(
|
||||
JSON.stringify(
|
||||
{
|
||||
message: `${helloPrefix}, ${name}!`,
|
||||
timestamp: new Date().toISOString(),
|
||||
method: request.method,
|
||||
path: url.pathname,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
{
|
||||
status: 200,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Demo: Authenticated "whoami" function
|
||||
*
|
||||
* - Requires Authorization: Bearer <user_jwt>
|
||||
* - Uses injected createClient() + edgeFunctionToken to call auth.getCurrentUser()
|
||||
* - Returns 401 if no valid user
|
||||
*/
|
||||
|
||||
module.exports = async function (request) {
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
};
|
||||
|
||||
if (request.method === 'OPTIONS') {
|
||||
return new Response(null, { status: 204, headers: corsHeaders });
|
||||
}
|
||||
|
||||
if (request.method !== 'GET') {
|
||||
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
|
||||
status: 405,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const authHeader = request.headers.get('Authorization') || '';
|
||||
const userToken = authHeader.startsWith('Bearer ')
|
||||
? authHeader.slice('Bearer '.length).trim()
|
||||
: null;
|
||||
|
||||
if (!userToken) {
|
||||
return new Response(JSON.stringify({ error: 'Missing bearer token' }), {
|
||||
status: 401,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const client = createClient({
|
||||
baseUrl: Deno.env.get('INSFORGE_INTERNAL_URL') || 'http://insforge:7130',
|
||||
edgeFunctionToken: userToken,
|
||||
});
|
||||
|
||||
const { data, error } = await client.auth.getCurrentUser();
|
||||
if (error || !data?.user?.id) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify(
|
||||
{
|
||||
user: {
|
||||
id: data.user.id,
|
||||
email: data.user.email,
|
||||
created_at: data.user.created_at,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
{
|
||||
status: 200,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Worker Template for Serverless Functions
|
||||
*
|
||||
* This code runs inside a Web Worker environment created by Deno.
|
||||
* Each worker is created fresh for a single request, executes once, and terminates.
|
||||
*/
|
||||
/* eslint-env worker */
|
||||
/* global self, Request, Deno */
|
||||
|
||||
// --- SECURITY BLACKOUT (Top-level) ---
|
||||
// We polyfill Deno.env and process.env BEFORE any imports using Top-Level Await.
|
||||
// This prevents libraries (like 'debug') from triggering 'NotCapable' errors
|
||||
// when using a strict native whitelist (env: false).
|
||||
const sterileEnv = {
|
||||
NODE_ENV: 'production',
|
||||
};
|
||||
|
||||
try {
|
||||
// Shadow Deno.env with a pure JS implementation
|
||||
const mockDenoEnv = {
|
||||
get: (key) => sterileEnv[key] ?? undefined,
|
||||
set: () => {
|
||||
throw new Error('Deno.env.set is disabled');
|
||||
},
|
||||
delete: () => {
|
||||
throw new Error('Deno.env.delete is disabled');
|
||||
},
|
||||
toObject: () => ({ ...sterileEnv }),
|
||||
has: (key) => key in sterileEnv,
|
||||
};
|
||||
|
||||
// Replace global Deno.env
|
||||
Object.defineProperty(globalThis, 'Deno', {
|
||||
value: Object.freeze({ env: mockDenoEnv }),
|
||||
configurable: false, // Lock down permanently (Audit Finding)
|
||||
writable: false,
|
||||
});
|
||||
|
||||
// Shadow process.env (Node compatibility)
|
||||
if (!globalThis.process) globalThis.process = {};
|
||||
globalThis.process.env = { ...sterileEnv };
|
||||
} catch (e) {
|
||||
// FATAL: Security setup failed. Terminate immediately to prevent leakage.
|
||||
console.error('Security shadow application failed:', e);
|
||||
self.postMessage({
|
||||
success: false,
|
||||
error: 'Security Initialization Error',
|
||||
status: 500,
|
||||
});
|
||||
self.close();
|
||||
throw new Error('Security initialization failed - halting worker');
|
||||
}
|
||||
// ----------------------------
|
||||
|
||||
// ----------------------------
|
||||
// EARLY MESSAGE BUFFERING (Race Fix)
|
||||
// ----------------------------
|
||||
// The runtime (server.ts -> executeInWorker) calls worker.postMessage()
|
||||
// immediately after `new Worker()`. Web Workers do NOT queue messages that
|
||||
// arrive before an `onmessage` handler is registered, so if we only attached
|
||||
// the handler AFTER the top-level `await import(...)` below (cold import can
|
||||
// take >200ms), the request message was silently dropped -> 504 timeout.
|
||||
//
|
||||
// Fix: register a synchronous handler BEFORE any top-level await. It buffers
|
||||
// any early message(s). Once imports finish, we swap `self.onmessage` to the
|
||||
// real handler and drain the buffer through it. Single-threaded execution
|
||||
// guarantees no message can slip between the swap and the drain.
|
||||
const __earlyMessages = [];
|
||||
self.onmessage = (e) => {
|
||||
__earlyMessages.push(e);
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// LATE IMPORTS (Pre-emptive Mocking)
|
||||
// ----------------------------
|
||||
// We use dynamic imports AFTER the environment is shadowed.
|
||||
//
|
||||
// If an import fails (e.g. npm registry unreachable under restricted egress),
|
||||
// the worker can never produce a real handler. Answer the request with an
|
||||
// explicit 500 (same {success,error,status} shape server.ts onmessage expects)
|
||||
// instead of hanging to the runtime's 60s timeout.
|
||||
//
|
||||
// The request message may NOT have been delivered yet when the import rejects:
|
||||
// literal dynamic imports are prefetched with the module graph, so a failed
|
||||
// import rejects before the first event-loop turn. Closing the worker at that
|
||||
// point would drop the undelivered request and reproduce the silent 504, so we
|
||||
// only close() after a message has been answered (with a deadline as backstop).
|
||||
let createClient, encodeBase64, decodeBase64;
|
||||
let __importFailed = false;
|
||||
try {
|
||||
({ createClient } = await import('npm:@insforge/sdk'));
|
||||
({ encodeBase64, decodeBase64 } =
|
||||
await import('https://deno.land/std@0.224.0/encoding/base64.ts'));
|
||||
} catch (importError) {
|
||||
__importFailed = true;
|
||||
const failMsg = 'SDK import failed: ' + (importError?.message || importError);
|
||||
console.error(failMsg);
|
||||
const answerAndClose = () => {
|
||||
self.postMessage({ success: false, error: failMsg, status: 500 });
|
||||
self.close();
|
||||
};
|
||||
if (__earlyMessages.splice(0).length > 0) {
|
||||
answerAndClose();
|
||||
} else {
|
||||
self.onmessage = answerAndClose;
|
||||
setTimeout(() => self.close(), 10000);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the single message with code, request data, and secrets
|
||||
const handleMessage = async (e) => {
|
||||
const { code, requestData, secrets = {} } = e.data;
|
||||
|
||||
try {
|
||||
/**
|
||||
* MOCK DENO OBJECT:
|
||||
* Providing safe secrets access even under strict native lock-down (env: false).
|
||||
* This fake 'Deno' object is injected into the user function's scope, ensuring
|
||||
* they only see the secrets we explicitly allow, while the native Deno runtime
|
||||
* remains blindfolded at the C++ layer.
|
||||
*/
|
||||
const mockDeno = {
|
||||
// Mock only the required Deno.env API for secret retrieval
|
||||
env: {
|
||||
get: (key) => secrets[key] ?? undefined,
|
||||
// (toObject removed for security to prevent secret enumeration)
|
||||
},
|
||||
// Explicitly block all subprocess APIs as a secondary defense tier
|
||||
run: () => {
|
||||
throw new Error('Deno.run is natively disabled');
|
||||
},
|
||||
spawn: () => {
|
||||
throw new Error('Deno.spawn is natively disabled');
|
||||
},
|
||||
Command: function () {
|
||||
throw new Error('Deno.Command is natively disabled');
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* FUNCTION WRAPPING:
|
||||
* Injecting mocks into the user function execution scope.
|
||||
* We pass mockDeno instead of the real Deno global.
|
||||
*/
|
||||
const wrapper = new Function(
|
||||
'exports',
|
||||
'module',
|
||||
'createClient',
|
||||
'Deno',
|
||||
'encodeBase64',
|
||||
'decodeBase64',
|
||||
code
|
||||
);
|
||||
const exports = {};
|
||||
const module = { exports };
|
||||
|
||||
// Execute the wrapper, passing mockDeno as the Deno global
|
||||
wrapper(exports, module, createClient, mockDeno, encodeBase64, decodeBase64);
|
||||
|
||||
// Get the exported function
|
||||
const functionHandler = module.exports || exports.default || exports;
|
||||
|
||||
if (typeof functionHandler !== 'function') {
|
||||
throw new Error(
|
||||
'No function exported. Expected: module.exports = async function(req) { ... }'
|
||||
);
|
||||
}
|
||||
|
||||
// Create Request object from data
|
||||
const request = new Request(requestData.url, {
|
||||
method: requestData.method,
|
||||
headers: requestData.headers,
|
||||
body: requestData.body,
|
||||
});
|
||||
|
||||
// Execute the function
|
||||
const response = await functionHandler(request);
|
||||
|
||||
// Serialize and send response
|
||||
let body = null;
|
||||
if (![204, 205, 304].includes(response.status)) {
|
||||
body = await response.text();
|
||||
}
|
||||
|
||||
const responseData = {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: Object.fromEntries(response.headers),
|
||||
body: body,
|
||||
};
|
||||
|
||||
self.postMessage({ success: true, response: responseData });
|
||||
} catch (error) {
|
||||
if (error instanceof Response) {
|
||||
let body = null;
|
||||
if (![204, 205, 304].includes(error.status)) {
|
||||
body = await error.text();
|
||||
}
|
||||
const responseData = {
|
||||
status: error.status,
|
||||
statusText: error.statusText,
|
||||
headers: Object.fromEntries(error.headers),
|
||||
body: body,
|
||||
};
|
||||
self.postMessage({ success: true, response: responseData });
|
||||
} else {
|
||||
self.postMessage({
|
||||
success: false,
|
||||
error: error.message || 'Unknown error',
|
||||
status: error.status || 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Activate the real handler and drain any messages that arrived during import.
|
||||
if (!__importFailed) {
|
||||
self.onmessage = handleMessage;
|
||||
for (const e of __earlyMessages.splice(0)) {
|
||||
await handleMessage(e);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user