mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
feat: Use CF
This commit is contained in:
1
bun.lock
1
bun.lock
@@ -125,6 +125,7 @@
|
|||||||
"@aws-sdk/client-sqs": "^3.806.0",
|
"@aws-sdk/client-sqs": "^3.806.0",
|
||||||
"@nestri/core": "workspace:",
|
"@nestri/core": "workspace:",
|
||||||
"actor-core": "^0.8.0",
|
"actor-core": "^0.8.0",
|
||||||
|
"aws4fetch": "^1.0.20",
|
||||||
"hono": "^4.7.8",
|
"hono": "^4.7.8",
|
||||||
"hono-openapi": "^0.4.8",
|
"hono-openapi": "^0.4.8",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { domain } from "./dns";
|
|||||||
import { secret } from "./secret";
|
import { secret } from "./secret";
|
||||||
import { cluster } from "./cluster";
|
import { cluster } from "./cluster";
|
||||||
import { postgres } from "./postgres";
|
import { postgres } from "./postgres";
|
||||||
import { storage } from "./storage";
|
|
||||||
|
|
||||||
export const apiService = new sst.aws.Service("Api", {
|
export const apiService = new sst.aws.Service("Api", {
|
||||||
cluster,
|
cluster,
|
||||||
@@ -13,7 +12,6 @@ export const apiService = new sst.aws.Service("Api", {
|
|||||||
link: [
|
link: [
|
||||||
bus,
|
bus,
|
||||||
auth,
|
auth,
|
||||||
storage,
|
|
||||||
postgres,
|
postgres,
|
||||||
secret.SteamApiKey,
|
secret.SteamApiKey,
|
||||||
secret.PolarSecret,
|
secret.PolarSecret,
|
||||||
|
|||||||
197
infra/images.ts
197
infra/images.ts
@@ -1,170 +1,57 @@
|
|||||||
import {api} from "./api"
|
|
||||||
import { domain } from "./dns";
|
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 cache = new sst.cloudflare.Kv("ImageCache");
|
||||||
|
|
||||||
const bucket = new sst.cloudflare.Bucket("ImageBucket");
|
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", {
|
export const imageCdn = new sst.cloudflare.Worker("ImageCDN", {
|
||||||
url: true,
|
url: true,
|
||||||
domain: "cdn." + domain,
|
domain: "cdn." + domain,
|
||||||
link: [bucket, cache, api],
|
link: [bucket, cache, imageProcessorFunction, accessKey],
|
||||||
handler: "packages/functions/src/images/index.ts",
|
handler: "packages/functions/src/images/index.ts",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const outputs = {
|
export const outputs = {
|
||||||
cdn: imageCdn.url
|
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,
|
|
||||||
// });
|
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
"@aws-sdk/client-sqs": "^3.806.0",
|
"@aws-sdk/client-sqs": "^3.806.0",
|
||||||
"@nestri/core": "workspace:",
|
"@nestri/core": "workspace:",
|
||||||
"actor-core": "^0.8.0",
|
"actor-core": "^0.8.0",
|
||||||
|
"aws4fetch": "^1.0.20",
|
||||||
"hono": "^4.7.8",
|
"hono": "^4.7.8",
|
||||||
"hono-openapi": "^0.4.8"
|
"hono-openapi": "^0.4.8"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import "zod-openapi/extend";
|
|||||||
import { cors } from "hono/cors";
|
import { cors } from "hono/cors";
|
||||||
import { GameApi } from "./game";
|
import { GameApi } from "./game";
|
||||||
import { SteamApi } from "./steam";
|
import { SteamApi } from "./steam";
|
||||||
import { ImageApi } from "./image";
|
|
||||||
import { auth } from "./utils/auth";
|
import { auth } from "./utils/auth";
|
||||||
import { FriendApi } from "./friend";
|
import { FriendApi } from "./friend";
|
||||||
import { logger } from "hono/logger";
|
import { logger } from "hono/logger";
|
||||||
@@ -25,10 +24,6 @@ app
|
|||||||
return next();
|
return next();
|
||||||
})
|
})
|
||||||
.use(auth)
|
.use(auth)
|
||||||
|
|
||||||
// Private routes
|
|
||||||
app.
|
|
||||||
route("/image", ImageApi.route)
|
|
||||||
|
|
||||||
const routes = app
|
const routes = app
|
||||||
.get("/", (c) => c.text("Hello World!"))
|
.get("/", (c) => c.text("Hello World!"))
|
||||||
|
|||||||
@@ -1,67 +1,89 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
|
import { AwsClient } from 'aws4fetch'
|
||||||
import { Resource } from "sst";
|
import { Resource } from "sst";
|
||||||
import { HTTPException } from "hono/http-exception";
|
import { HTTPException } from "hono/http-exception";
|
||||||
|
|
||||||
|
|
||||||
export namespace ImageRoute {
|
export namespace ImageRoute {
|
||||||
export const route = new Hono()
|
export const route = new Hono()
|
||||||
.get(
|
.get(
|
||||||
"/:hashWithExt",
|
"/:hashWithExt",
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { hashWithExt } = c.req.param();
|
// const { hashWithExt } = c.req.param();
|
||||||
|
|
||||||
// Validate format
|
const client = new AwsClient({
|
||||||
// Split hash and extension
|
accessKeyId: Resource.ImageInvokerAccessKey.key,
|
||||||
const match = hashWithExt.match(/^([a-zA-Z0-9_-]+)\.(avif|webp)$/);
|
secretAccessKey: Resource.ImageInvokerAccessKey.secret,
|
||||||
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)
|
const LAMBDA_URL = `https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/${Resource.ImageProcessor.name}/invocations`
|
||||||
|
|
||||||
// Normalize and build cache key
|
const lambdaResponse = await client.fetch(LAMBDA_URL, {
|
||||||
// const cacheKey = `${hash}_${format}_w${width}${height ? `_h${height}` : ""}_dpr${dpr}`;
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ world: "hello" }),
|
||||||
|
})
|
||||||
|
|
||||||
// Add aggressive caching
|
if (!lambdaResponse.ok) {
|
||||||
// c.header("Cache-Control", "public, max-age=315360000, immutable");
|
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)
|
console.log(await lambdaResponse.json())
|
||||||
return c.newResponse(await imageBytes.arrayBuffer(),
|
|
||||||
// {
|
// // Validate format
|
||||||
// headers: {
|
// // Split hash and extension
|
||||||
// ...imageBytes.headers
|
// 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")
|
return c.text("success")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
9
sst-env.d.ts
vendored
@@ -57,6 +57,15 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ImageInvokerAccessKey": {
|
||||||
|
"key": string
|
||||||
|
"secret": string
|
||||||
|
"type": "aws.iam/accessKey.AccessKey"
|
||||||
|
}
|
||||||
|
"ImageProcessor": {
|
||||||
|
"name": string
|
||||||
|
"type": "sst.aws.Function"
|
||||||
|
}
|
||||||
"NestriFamilyMonthly": {
|
"NestriFamilyMonthly": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
|||||||
Reference in New Issue
Block a user