feat: Use CF

This commit is contained in:
Wanjohi
2025-06-06 09:09:11 +03:00
parent a47dc91b22
commit 5fd5608e6e
8 changed files with 129 additions and 212 deletions

View File

@@ -125,6 +125,7 @@
"@aws-sdk/client-sqs": "^3.806.0",
"@nestri/core": "workspace:",
"actor-core": "^0.8.0",
"aws4fetch": "^1.0.20",
"hono": "^4.7.8",
"hono-openapi": "^0.4.8",
},

View File

@@ -4,7 +4,6 @@ import { domain } from "./dns";
import { secret } from "./secret";
import { cluster } from "./cluster";
import { postgres } from "./postgres";
import { storage } from "./storage";
export const apiService = new sst.aws.Service("Api", {
cluster,
@@ -13,7 +12,6 @@ export const apiService = new sst.aws.Service("Api", {
link: [
bus,
auth,
storage,
postgres,
secret.SteamApiKey,
secret.PolarSecret,

View File

@@ -1,170 +1,57 @@
import {api} from "./api"
import { domain } from "./dns";
import { storage } from "./storage";
sst.Linkable.wrap(aws.iam.AccessKey, (resource) => ({
properties: {
key: resource.id,
secret: resource.secret,
},
}))
const cache = new sst.cloudflare.Kv("ImageCache");
const bucket = new sst.cloudflare.Bucket("ImageBucket");
const lambdaInvokerUser = new aws.iam.User("ImageIAMUser", {
name: `${$app.name}-${$app.stage}-ImageIAMUser`,
forceDestroy: true
});
const imageProcessorFunction = new sst.aws.Function("ImageProcessor",
{
memory: "1024 MB",
link: [storage],
timeout: "30 seconds",
nodejs: { install: ["sharp"] },
handler: "packages/functions/src/images/processor.handler",
},
);
new aws.iam.UserPolicy("InvokeLambdaPolicy", {
user: lambdaInvokerUser.name,
policy: $output({
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: ["lambda:InvokeFunction"],
Resource: imageProcessorFunction.arn,
},
],
}).apply(JSON.stringify),
});
const accessKey = new aws.iam.AccessKey("ImageInvokerAccessKey", {
user: lambdaInvokerUser.name,
});
export const imageCdn = new sst.cloudflare.Worker("ImageCDN", {
url: true,
domain: "cdn." + domain,
link: [bucket, cache, api],
link: [bucket, cache, imageProcessorFunction, accessKey],
handler: "packages/functions/src/images/index.ts",
});
export const outputs = {
cdn: imageCdn.url
}
// const transformedImageBucket = new sst.aws.Bucket("TranformedStorage");
// const imageProcessorFunction = new sst.aws.Function("ImageProcessor",
// {
// handler: "src/image-processor.handler",
// nodejs: { install: ["sharp"] },
// memory: "1024 MB",
// timeout: "30 seconds",
// url: true,
// link: [storage, transformedImageBucket],
// environment: {
// transformedImageCacheTTL:
// process.env.transformedImageCacheTTL ?? "max-age=31622400",
// maxImageSize: process.env.MAX_IMAGE_SIZE ?? "4700000",
// },
// },
// );
// const cloudfrontOAI = new aws.cloudfront.OriginAccessIdentity("CloudfrontOAI");
// const lambdaOAC = new aws.cloudfront.OriginAccessControl("OriginAccessControl",
// {
// name: `${$app.name}-${$app.stage}-OriginAccessControl`,
// originAccessControlOriginType: "lambda",
// signingBehavior: "always",
// signingProtocol: "sigv4",
// },
// );
// const cloudfrontResponseHeadersPolicy =
// new aws.cloudfront.ResponseHeadersPolicy("ResponseHeadersPolicy",
// {
// name: `${$app.name}-${$app.stage}-ResponseHeadersPolicy`,
// customHeadersConfig: {
// items: [
// {
// header: "x-aws-image-optimization",
// value: "v1.0",
// override: true,
// },
// { header: "vary", value: "accept", override: true },
// ],
// },
// corsConfig: {
// accessControlAllowCredentials: false,
// accessControlAllowHeaders: {
// items: ["*"],
// },
// accessControlAllowMethods: {
// items: ["GET"],
// },
// accessControlAllowOrigins: {
// items: ["*"],
// },
// accessControlMaxAgeSec: 600,
// originOverride: false,
// },
// },
// );
// const cachePolicy = new aws.cloudfront.CachePolicy("CachePolicy",
// {
// name: `${$app.name}-${$app.stage}-CachePolicy`,
// defaultTtl: 86400,
// maxTtl: 31536000,
// minTtl: 0,
// parametersInCacheKeyAndForwardedToOrigin: {
// cookiesConfig: {
// cookieBehavior: "none",
// },
// headersConfig: {
// headerBehavior: "none",
// },
// queryStringsConfig: {
// queryStringBehavior: "whitelist",
// queryStrings: {
// items: ["w", "h", "dpr", "format"]
// },
// },
// },
// },
// );
// const groupOriginId = `${$app.name}-${$app.stage}-"GroupOrigin`;
// const primaryOriginId = `${$app.name}-${$app.stage}-PrimaryOrigin`;
// const secondaryOriginId = `${$app.name}-${$app.stage}-SecondaryOrigin`;
// const s3Distribution = new sst.aws.Cdn("ImageDistribution",
// {
// originGroups: [
// {
// originId: groupOriginId,
// failoverCriteria: {
// statusCodes: [403, 500, 503, 504],
// },
// members: [
// {
// originId: primaryOriginId,
// },
// {
// originId: secondaryOriginId,
// },
// ],
// },
// ],
// origins: [
// {
// originId: primaryOriginId,
// domainName: imageProcessorFunction.url.apply(
// (url) => new URL(url).hostname,
// ),
// originAccessControlId: lambdaOAC.id,
// customOriginConfig: {
// originProtocolPolicy: "https-only",
// httpPort: 443,
// httpsPort: 443,
// originSslProtocols: ["TLSv1.2"],
// },
// },
// {
// originId: secondaryOriginId,
// domainName:
// transformedImageBucket.nodes.bucket.bucketRegionalDomainName,
// s3OriginConfig: {
// originAccessIdentity: cloudfrontOAI.cloudfrontAccessIdentityPath,
// },
// },
// ],
// defaultCacheBehavior: {
// cachePolicyId: cachePolicy.id,
// allowedMethods: ["GET", "HEAD"],
// cachedMethods: ["GET", "HEAD"],
// targetOriginId: groupOriginId,
// viewerProtocolPolicy: "redirect-to-https",
// functionAssociations: [
// {
// eventType: "viewer-request",
// functionArn: cloudfrontRewriteFunction.arn,
// },
// ],
// responseHeadersPolicyId: cloudfrontResponseHeadersPolicy.id,
// },
// },
// );
// new aws.lambda.Permission("AllowCloudFrontServicePrincipal", {
// action: "lambda:InvokeFunctionUrl",
// function: imageProcessorFunction.arn,
// principal: "cloudfront.amazonaws.com",
// statementId: "AllowCloudFrontServicePrincipal",
// sourceArn: s3Distribution.nodes.distribution.arn,
// });

View File

@@ -25,6 +25,7 @@
"@aws-sdk/client-sqs": "^3.806.0",
"@nestri/core": "workspace:",
"actor-core": "^0.8.0",
"aws4fetch": "^1.0.20",
"hono": "^4.7.8",
"hono-openapi": "^0.4.8"
}

View File

@@ -2,7 +2,6 @@ import "zod-openapi/extend";
import { cors } from "hono/cors";
import { GameApi } from "./game";
import { SteamApi } from "./steam";
import { ImageApi } from "./image";
import { auth } from "./utils/auth";
import { FriendApi } from "./friend";
import { logger } from "hono/logger";
@@ -26,10 +25,6 @@ app
})
.use(auth)
// Private routes
app.
route("/image", ImageApi.route)
const routes = app
.get("/", (c) => c.text("Hello World!"))
.route("/games", GameApi.route)

View File

@@ -1,67 +1,89 @@
import { Hono } from "hono";
import { AwsClient } from 'aws4fetch'
import { Resource } from "sst";
import { HTTPException } from "hono/http-exception";
export namespace ImageRoute {
export const route = new Hono()
.get(
"/:hashWithExt",
async (c) => {
const { hashWithExt } = c.req.param();
// const { hashWithExt } = c.req.param();
// Validate format
// Split hash and extension
const match = hashWithExt.match(/^([a-zA-Z0-9_-]+)\.(avif|webp)$/);
if (!match) {
throw new HTTPException(400, { message: "Invalid image hash or format" });
}
const [, hash, format] = match;
const query = c.req.query();
// Parse dimensions
const width = parseInt(query.w || query.width || "");
const height = parseInt(query.h || query.height || "");
const dpr = parseFloat(query.dpr || "1");
if (isNaN(width) || width <= 0) {
throw new HTTPException(400, { message: "Invalid width" });
}
if (!isNaN(height) && height < 0) {
throw new HTTPException(400, { message: "Invalid height" });
}
if (dpr < 1 || dpr > 4) {
throw new HTTPException(400, { message: "Invalid dpr" });
}
console.log("url",Resource.Api.url)
const imageBytes = await fetch(`${Resource.Api.url}/image/${hash}`,{
method:"POST",
body:JSON.stringify({
dpr,
width,
height,
format
})
const client = new AwsClient({
accessKeyId: Resource.ImageInvokerAccessKey.key,
secretAccessKey: Resource.ImageInvokerAccessKey.secret,
})
console.log("imahe",imageBytes.headers)
const LAMBDA_URL = `https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/${Resource.ImageProcessor.name}/invocations`
// Normalize and build cache key
// const cacheKey = `${hash}_${format}_w${width}${height ? `_h${height}` : ""}_dpr${dpr}`;
const lambdaResponse = await client.fetch(LAMBDA_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ world: "hello" }),
})
// Add aggressive caching
// c.header("Cache-Control", "public, max-age=315360000, immutable");
if (!lambdaResponse.ok) {
console.error(await lambdaResponse.text())
return c.json({ error: `Lambda API returned ${lambdaResponse.status}` }, { status: 500 })
}
// Placeholder image response (to be replaced by real logic)
return c.newResponse(await imageBytes.arrayBuffer(),
// {
// headers: {
// ...imageBytes.headers
// }
// }
);
console.log(await lambdaResponse.json())
// // Validate format
// // Split hash and extension
// const match = hashWithExt.match(/^([a-zA-Z0-9_-]+)\.(avif|webp)$/);
// if (!match) {
// throw new HTTPException(400, { message: "Invalid image hash or format" });
// }
// const [, hash, format] = match;
// const query = c.req.query();
// // Parse dimensions
// const width = parseInt(query.w || query.width || "");
// const height = parseInt(query.h || query.height || "");
// const dpr = parseFloat(query.dpr || "1");
// if (isNaN(width) || width <= 0) {
// throw new HTTPException(400, { message: "Invalid width" });
// }
// if (!isNaN(height) && height < 0) {
// throw new HTTPException(400, { message: "Invalid height" });
// }
// if (dpr < 1 || dpr > 4) {
// throw new HTTPException(400, { message: "Invalid dpr" });
// }
// console.log("url",Resource.Api.url)
// const imageBytes = await fetch(`${Resource.Api.url}/image/${hash}`,{
// method:"POST",
// body:JSON.stringify({
// dpr,
// width,
// height,
// format
// })
// })
// console.log("imahe",imageBytes.headers)
// // Normalize and build cache key
// // const cacheKey = `${hash}_${format}_w${width}${height ? `_h${height}` : ""}_dpr${dpr}`;
// // Add aggressive caching
// // c.header("Cache-Control", "public, max-age=315360000, immutable");
// // Placeholder image response (to be replaced by real logic)
// return c.newResponse(await imageBytes.arrayBuffer(),
// // {
// // headers: {
// // ...imageBytes.headers
// // }
// // }
// );
return c.text("success")
}

View File

@@ -1 +1,5 @@
export const handler = () => { }
export async function handler(event: any) {
console.log('Task completion event received:', JSON.stringify(event, null, 2));
return JSON.stringify({ hello: "world" })
}

9
sst-env.d.ts vendored
View File

@@ -57,6 +57,15 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ImageInvokerAccessKey": {
"key": string
"secret": string
"type": "aws.iam/accessKey.AccessKey"
}
"ImageProcessor": {
"name": string
"type": "sst.aws.Function"
}
"NestriFamilyMonthly": {
"type": "sst.sst.Secret"
"value": string