import { env } from 'process'; import { LambdaFunctionURLHandler, APIGatewayProxyEventV2 } from 'aws-lambda'; import { BackgroundJob, CronJob, CookieJar, Context, Endpoint, SystemAttribute, requiresContext, } from 'wirejs-resources'; // @ts-ignore import * as api from './index.js'; function deriveLocation(event: APIGatewayProxyEventV2) { if (event.headers['x-wirejs-location']) { return new URL(event.headers['x-wirejs-location']); } else { const baseUrl = `https://${event.requestContext.domainName}${event.rawPath}`; const queryString = event.queryStringParameters ? `?${new URLSearchParams(event.queryStringParameters as any).toString()}` : ''; return new URL(`${baseUrl}${queryString}`); } } function createContext(event: APIGatewayProxyEventV2) { try { const cookies = new CookieJar(event.headers['cookie']); const location = deriveLocation(event); return new Context({ cookies, location, httpMethod: event.requestContext.http.method, requestBody: event.body, requestHeaders: event.headers as Record, runtimeAttributes: [ new SystemAttribute('wirejs', 'deployment-type', { description: 'Deployment under which your system is running.', value: 'wirejs-deploy-amplify-basic' }), new SystemAttribute('wirejs', 'http-origin-local', { description: 'HTTP origin (base address) to use for local development.', value: null, }), new SystemAttribute('wirejs', 'http-origin-network', { description: 'HTTP origin (base address) for machines on your network to use.', value: null, }), new SystemAttribute('wirejs', 'http-origin-public', { description: 'HTTP origin (base address) for machines outside your network to use. Only populated for `npm run start:public`, and only accessible in environments that support NAT-PMP.', value: location.origin }), new SystemAttribute('wirejs', 'aws-region', { description: 'AWS region where the application is deployed.', value: env.AWS_REGION || 'unknown' }), ] }); } catch { return undefined; } } async function callApiMethod(api: any, call: any, context: any) { try { const [scope, ...rest] = call.method; console.log('api method parsed', { scope, rest }); if (rest.length === 0) { console.log('api method resolved. invoking...'); if (requiresContext(api[scope])) { return { data: await (api[scope] as any)(context, ...call.args.slice(1)) }; } else { return { data: await api[scope](...call.args) }; } } else { console.log('nested scope found'); return callApiMethod(api[scope], { ...call, method: rest, }, context); } } catch (error: any) { console.log(error); return { error: error.message }; } } function isBackgroundJob(o: any): o is BackgroundJob { return typeof o === 'object' && typeof o.start === 'function'; } function extractSetCookies(context: Context) { const cookies: string[] = []; for (const cookie of context.cookies.getSetCookies()) { const cookieOptions = []; if (cookie.maxAge) cookieOptions.push(`Max-Age=${cookie.maxAge}`); if (cookie.httpOnly) cookieOptions.push('HttpOnly'); if (cookie.secure) cookieOptions.push('Secure'); if (cookie.path) cookieOptions.push(`Path=${cookie.path}`); cookies.push(`${cookie.name}=${cookie.value}; ${cookieOptions.join('; ')}`); } return cookies; } /** * * @param {Context} context * @param {http.ServerResponse} res */ function getResponseHeadersFromContext(context: Context) { const headers = {} as Record; for (const [k, v] of Object.entries(context.responseHeaders)) { headers[k] = v; } if (context.locationIsDirty) { headers['x-wirejs-redirect'] = context.location.href; } return headers; } /** * @param pattern string pattern, where `*` matches anything * @param text * @returns */ function globMatch(pattern: string, text: string) { const parts = pattern.split('%'); const regex = new RegExp(parts.join('.+')); return regex.test(text); } /** * Compare two strings by length for sorting in order of increasing length. * * @returns */ function byPathLength(a: Endpoint, b: Endpoint) { return a.path.length - b.path.length; } export const handler: LambdaFunctionURLHandler = async (event) => { // Handle EventBridge-scheduled CronJob invocations. // EventBridge events have a 'source' field; Lambda Function URL events do not. if ((event as any).source === 'wirejs-cron') { const cronEvent = event as any; const absoluteId: string | undefined = cronEvent['wirejs-cron-id']; if (!absoluteId) { console.error('Missing wirejs-cron-id in scheduled event'); return { statusCode: 400, body: JSON.stringify({ error: 'Missing wirejs-cron-id' }) }; } console.log('handling scheduled cron job', absoluteId); const entry = (CronJob as any).registeredJobs.get(absoluteId); if (!entry || typeof entry.handler !== 'function') { console.error('CronJob not found', absoluteId, [...(CronJob as any).registeredJobs.keys()]); return { statusCode: 404, body: JSON.stringify({ error: 'CronJob not found' }) }; } await entry.handler(); return { statusCode: 200, body: JSON.stringify({ message: 'CronJob complete' }) }; } const calls = event.body ? JSON.parse(event.body) : undefined; const wjsContext = createContext(event); const path = wjsContext?.location.pathname; if (wjsContext && (path === '/api' || path?.startsWith('/api/')) && Array.isArray(calls) ) { const responses = []; for (const call of calls) { responses.push(await callApiMethod(api, call, wjsContext)); } return { statusCode: wjsContext.responseCode ?? 200, cookies: extractSetCookies(wjsContext), headers: { ...getResponseHeadersFromContext(wjsContext), 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify(responses) } } else if (wjsContext) { const allHandlers = [...Endpoint.list()]; const matchingHandlers = allHandlers .filter(e => globMatch(e.path, wjsContext.location.pathname)); const matchingEndpoint = matchingHandlers.sort(byPathLength).pop(); if (!matchingEndpoint) { console.error('Invalid request format', calls); return { statusCode: 400, body: JSON.stringify({ error: 'Invalid request format' }) }; } const response = await matchingEndpoint.handle(wjsContext); return { statusCode: wjsContext.responseCode ?? 200, cookies: extractSetCookies(wjsContext), headers: { ...getResponseHeadersFromContext(wjsContext), }, body: response }; } else if (typeof calls === 'object' && calls.async) { const sanitized = { ...calls }; sanitized.selfInvocationId = sanitized.selfInvocationId.slice(0, 8); console.log('handling async execution'); if (!calls.absoluteId) { console.error('Missing absoluteId in async call'); return { statusCode: 400, body: JSON.stringify({ error: 'Missing absoluteId' }) }; } if (calls.selfInvocationId !== env.SELF_INVOCATION_ID) { console.error('Self invocation ID mismatch'); return { statusCode: 403, body: JSON.stringify({ error: 'Forbidden' }) }; } const handler = (BackgroundJob as any).registeredJobs[calls.absoluteId]; if (!handler || typeof handler !== 'function') { console.error('Background job not found', calls.absoluteId, (BackgroundJob as any).registeredJobs ); return { statusCode: 404, body: JSON.stringify({ error: 'Background job not found' }) }; } await handler(...calls.args) return { statusCode: 200, body: JSON.stringify({ message: 'Background job complete' }) }; } else { return { statusCode: 404, body: "Not found" }; } }