first commit

This commit is contained in:
Zakaria
2026-09-22 16:13:21 -04:00
commit 7e49be27f1
297 changed files with 63162 additions and 0 deletions
@@ -0,0 +1,244 @@
import {ValidationError} from '../cloud-spi/errors'
import {
CreateFunctionCommand,
DeleteFunctionCommand,
GetFunctionCommand,
InvokeCommand,
type InvokeCommandOutput,
ListFunctionsCommand,
type LambdaClient,
} from "@aws-sdk/client-lambda";
import { awsServerlessSchema } from "../cloud-spi/serverlessSchema";
import type {
CloudResource,
CloudServiceAdapter,
CreateResourceInput,
ResourceQuery,
ServerlessInvokeResult,
ServiceSchema,
} from "../cloud-spi/types";
import { lambda as defaultLambda } from "../aws";
import { createZipArchive, handlerFileName } from "./zipArchive";
import {lambdaStarterCode} from '../cloud-spi/lambdaStarterCode'
export class AwsServerlessAdapter implements CloudServiceAdapter {
readonly cloud = "aws" as const;
readonly service = "serverless" as const;
constructor(private readonly lambda: LambdaClient = defaultLambda) {}
schema(): ServiceSchema {
return awsServerlessSchema();
}
async list(query: ResourceQuery = {}): Promise<CloudResource[]> {
const res = await this.lambda.send(new ListFunctionsCommand({}));
const resources: CloudResource[] = (res.Functions ?? []).map((fn) => ({
id: fn.FunctionName ?? fn.FunctionArn ?? "",
name: fn.FunctionName ?? "",
cloud: "aws" as const,
service: "serverless" as const,
type: "lambda",
region: null,
createdAt: null,
status: fn.State ?? null,
metadata: {
arn: fn.FunctionArn,
runtime: fn.Runtime,
handler: fn.Handler,
lastModified: fn.LastModified,
memorySize: fn.MemorySize,
timeout: fn.Timeout,
codeSize: fn.CodeSize,
packageType: fn.PackageType,
description: fn.Description,
},
}));
return filterBySearch(resources, query.search);
}
async get(id: string): Promise<CloudResource | null> {
try {
const res = await this.lambda.send(
new GetFunctionCommand({ FunctionName: id }),
);
const config = res.Configuration;
if (!config) return null;
return {
id: config.FunctionName ?? config.FunctionArn ?? id,
name: config.FunctionName ?? id,
cloud: "aws",
service: "serverless",
type: "lambda",
region: null,
createdAt: null,
status: config.State ?? null,
metadata: {
arn: config.FunctionArn,
runtime: config.Runtime,
handler: config.Handler,
lastModified: config.LastModified,
memorySize: config.MemorySize,
timeout: config.Timeout,
codeSize: config.CodeSize,
packageType: config.PackageType,
description: config.Description,
role: config.Role,
version: config.Version,
codeSha256: config.CodeSha256,
environment: config.Environment?.Variables,
},
};
} catch (error) {
if (hasHttpStatus(error, 404)) return null;
throw error;
}
}
async create(input: CreateResourceInput): Promise<CloudResource> {
const values = input.values;
const functionName = String(
values.functionName ?? values.name ?? "",
).trim();
const runtime = String(values.runtime ?? "").trim();
const architecture = String(values.architecture ?? 'x86_64').trim();
const handler = String(values.handler ?? "").trim();
const role = String(values.role ?? "").trim();
const description = String(values.description ?? "").trim();
const memorySize = positiveNumberOrDefault(values.memorySize, 128);
const timeout = positiveNumberOrDefault(values.timeout, 3);
const zipFile = String(values.zipFile ?? '').trim();
const code =
String(values.code ?? "").trim() ||
lambdaStarterCode(runtime)
if (!functionName) throw new ValidationError("functionName is required");
if (!runtime) throw new ValidationError("runtime is required");
if (architecture !== 'x86_64' && architecture !== 'arm64') {
throw new ValidationError('architecture must be x86_64 or arm64')
}
if (!handler) throw new ValidationError("handler is required");
if (!role) throw new ValidationError("role is required");
const res = await this.lambda.send(
new CreateFunctionCommand({
FunctionName: functionName,
Runtime: runtime as never,
Architectures: [architecture as never],
Handler: handler,
Role: role,
Description: description || undefined,
MemorySize: memorySize,
Timeout: timeout,
Code: {
// The browser submits uploaded files as base64. Otherwise package the
// editor source into the same AWS ZIP payload.
ZipFile: zipFile
? decodeBase64Zip(zipFile)
: createZipArchive([
{ name: handlerFileName(handler, runtime), content: code },
]),
},
}),
);
return {
id: res.FunctionName ?? res.FunctionArn ?? functionName,
name: res.FunctionName ?? functionName,
cloud: "aws",
service: "serverless",
type: "lambda",
region: null,
createdAt: null,
status: res.State ?? null,
metadata: {
arn: res.FunctionArn,
runtime: res.Runtime,
handler: res.Handler,
lastModified: res.LastModified,
memorySize: res.MemorySize,
timeout: res.Timeout,
codeSize: res.CodeSize,
packageType: res.PackageType,
description: res.Description,
role: res.Role,
version: res.Version,
codeSha256: res.CodeSha256,
},
};
}
async delete(id: string): Promise<void> {
await this.lambda.send(new DeleteFunctionCommand({ FunctionName: id }));
}
async invoke(id: string, payload: string): Promise<ServerlessInvokeResult> {
const startedAt = performance.now();
const res = await this.lambda.send(
new InvokeCommand({
FunctionName: id,
Payload: new TextEncoder().encode(payload || "{}"),
// Tail returns the last 4 KB of the execution log, base64 encoded.
LogType: "Tail",
}),
);
const executionDuration = Math.round(performance.now() - startedAt);
return {
statusCode: res.StatusCode ?? 0,
payload: decodePayload(res.Payload),
...(res.FunctionError ? { functionError: res.FunctionError } : {}),
...(res.LogResult ? { logResult: decodeLogResult(res.LogResult) } : {}),
executionDuration,
};
}
}
function decodeBase64Zip(value: string): Uint8Array {
if (value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
throw new ValidationError('zipFile must be a base64-encoded ZIP archive')
}
return new Uint8Array(Buffer.from(value, 'base64'))
}
function positiveNumberOrDefault(value: unknown, defaultValue: number): number {
if (value === undefined || value === null || String(value).trim() === '') {
return defaultValue
}
const number = Number(value)
return Number.isFinite(number) && number > 0 ? number : defaultValue
}
function decodePayload(payload: InvokeCommandOutput["Payload"]): string {
if (!payload) return "";
return new TextDecoder().decode(payload);
}
/** Lambda returns the tailed log base64 encoded; surface it as plain text. */
function decodeLogResult(logResult: string): string {
try {
return Buffer.from(logResult, "base64").toString("utf8");
} catch {
return logResult;
}
}
function filterBySearch(
resources: CloudResource[],
search?: string,
): CloudResource[] {
const normalized = search?.trim().toLowerCase();
if (!normalized) return resources;
return resources.filter((r) => r.name.toLowerCase().includes(normalized));
}
function hasHttpStatus(error: unknown, status: number): boolean {
if (typeof error !== "object" || error === null) return false;
const metadata = (error as { $metadata?: { httpStatusCode?: number } })
.$metadata;
return metadata?.httpStatusCode === status;
}