-
Notifications
You must be signed in to change notification settings - Fork 1.1k
last few staging things #1190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
last few staging things #1190
Changes from all commits
8b976e4
b12b221
fe273ee
dc2f22b
84c21bb
921549e
8c8f599
1bcee3a
fe3180e
40ee84f
b4361e4
2a91e42
b489244
96a6aa0
bf1d661
9435124
039b88b
9e02df8
c512587
1cdc863
71f9f74
3052627
0c9d8db
7ebeb56
4edf301
a6024a5
21f5b0f
3e0132a
311c839
cb09c4a
148f18d
5dace4a
c698566
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -45,13 +45,16 @@ export default $config({ | |||||||||||||||||||||||||||
| const secrets = Secrets(); | ||||||||||||||||||||||||||||
| // const planetscale = Planetscale(); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const recordingsBucket = new aws.s3.BucketV2("RecordingsBucket"); | ||||||||||||||||||||||||||||
| const recordingsBucket = new aws.s3.BucketV2( | ||||||||||||||||||||||||||||
| "RecordingsBucket", | ||||||||||||||||||||||||||||
| {}, | ||||||||||||||||||||||||||||
| { retainOnDelete: true }, | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const vercelVariables = [ | ||||||||||||||||||||||||||||
| { key: "NEXT_PUBLIC_AXIOM_TOKEN", value: AXIOM_API_TOKEN }, | ||||||||||||||||||||||||||||
| { key: "NEXT_PUBLIC_AXIOM_DATASET", value: AXIOM_DATASET }, | ||||||||||||||||||||||||||||
| { key: "CAP_AWS_BUCKET", value: recordingsBucket.bucket }, | ||||||||||||||||||||||||||||
| { key: "NEXT_PUBLIC_CAP_AWS_BUCKET", value: recordingsBucket.bucket }, | ||||||||||||||||||||||||||||
| { key: "DATABASE_URL", value: secrets.DATABASE_URL_MYSQL.value }, | ||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||
|
Comment on lines
54
to
59
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Use secret-backed AXIOM token for Vercel env like other sensitive values. Replace the direct constant usage with the Secret value. - { key: "NEXT_PUBLIC_AXIOM_TOKEN", value: AXIOM_API_TOKEN },
+ { key: "NEXT_PUBLIC_AXIOM_TOKEN", value: secrets.AXIOM_API_TOKEN.value },Note: Although labeled “NEXT_PUBLIC”, avoid committing the token in code; source-of-truth via Secret prevents accidental reuse and rotation issues. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -60,21 +63,21 @@ export default $config({ | |||||||||||||||||||||||||||
| // status: "Enabled", | ||||||||||||||||||||||||||||
| // }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // const cloudfrontDistribution = aws.cloudfront.getDistributionOutput({ | ||||||||||||||||||||||||||||
| // id: "E36XSZEM0VIIYB", | ||||||||||||||||||||||||||||
| // }); | ||||||||||||||||||||||||||||
| const cloudfrontDistribution = | ||||||||||||||||||||||||||||
| $app.stage === "production" | ||||||||||||||||||||||||||||
| ? aws.cloudfront.getDistributionOutput({ id: "E36XSZEM0VIIYB" }) | ||||||||||||||||||||||||||||
| : null; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const vercelUser = new aws.iam.User("VercelUser", { forceDestroy: false }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const vercelProject = vercel.getProjectOutput({ name: "cap-web" }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (webUrl) { | ||||||||||||||||||||||||||||
| if (webUrl) | ||||||||||||||||||||||||||||
| vercelVariables.push( | ||||||||||||||||||||||||||||
| { key: "WEB_URL", value: webUrl }, | ||||||||||||||||||||||||||||
| { key: "NEXT_PUBLIC_WEB_URL", value: webUrl }, | ||||||||||||||||||||||||||||
| { key: "NEXTAUTH_URL", value: webUrl }, | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // vercelEnvVar("VercelCloudfrontEnv", { | ||||||||||||||||||||||||||||
| // key: "CAP_CLOUDFRONT_DISTRIBUTION_ID", | ||||||||||||||||||||||||||||
|
|
@@ -89,16 +92,14 @@ export default $config({ | |||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||
| aud, | ||||||||||||||||||||||||||||
| url, | ||||||||||||||||||||||||||||
| provider: await aws.iam | ||||||||||||||||||||||||||||
| .getOpenIdConnectProvider({ url: `https://${url}` }) | ||||||||||||||||||||||||||||
| .catch( | ||||||||||||||||||||||||||||
| () => | ||||||||||||||||||||||||||||
| new aws.iam.OpenIdConnectProvider( | ||||||||||||||||||||||||||||
| provider: | ||||||||||||||||||||||||||||
| $app.stage === "production" || $app.stage === "staging" | ||||||||||||||||||||||||||||
| ? aws.iam.getOpenIdConnectProviderOutput({ url: `https://${url}` }) | ||||||||||||||||||||||||||||
| : new aws.iam.OpenIdConnectProvider( | ||||||||||||||||||||||||||||
| "VercelAWSOIDC", | ||||||||||||||||||||||||||||
| { url: `https://${url}`, clientIdLists: [aud] }, | ||||||||||||||||||||||||||||
| { retainOnDelete: true }, | ||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| })(); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -118,7 +119,7 @@ export default $config({ | |||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| StringLike: { | ||||||||||||||||||||||||||||
| [`${oidc.url}:sub`]: [ | ||||||||||||||||||||||||||||
| `owner:${VERCEL_TEAM_SLUG}:project:*:environment:staging`, | ||||||||||||||||||||||||||||
| `owner:${VERCEL_TEAM_SLUG}:project:*:environment:${$app.stage}`, | ||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||
|
Comment on lines
120
to
123
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainNarrow OIDC Scope If the project name is - [`${oidc.url}:sub`]: [`owner:${VERCEL_TEAM_SLUG}:project:*:environment:${$app.stage}`]
+ [`${oidc.url}:sub`]: [`owner:${VERCEL_TEAM_SLUG}:project:cap-web:environment:${$app.stage}`]Verify the exact 🌐 Web query: 💡 Result: Exact
How to scope an IAM role trust to a single Vercel project + environment
{
Sources
Narrow OIDC - [`${oidc.url}:sub`]: [`owner:${VERCEL_TEAM_SLUG}:project:*:environment:${$app.stage}`]
+ [`${oidc.url}:sub`]: [`owner:${VERCEL_TEAM_SLUG}:project:cap-web:environment:${$app.stage}`]📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
|
|
@@ -128,40 +129,57 @@ export default $config({ | |||||||||||||||||||||||||||
| inlinePolicies: [ | ||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||
| name: "VercelAWSAccessPolicy", | ||||||||||||||||||||||||||||
| policy: recordingsBucket.arn.apply((arn) => | ||||||||||||||||||||||||||||
| policy: $resolve([ | ||||||||||||||||||||||||||||
| recordingsBucket.arn, | ||||||||||||||||||||||||||||
| cloudfrontDistribution?.arn, | ||||||||||||||||||||||||||||
| ] as const).apply(([bucketArn, cloudfrontArn]) => | ||||||||||||||||||||||||||||
| JSON.stringify({ | ||||||||||||||||||||||||||||
| Version: "2012-10-17", | ||||||||||||||||||||||||||||
| Statement: [ | ||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||
| Effect: "Allow", | ||||||||||||||||||||||||||||
| Action: ["s3:*"], | ||||||||||||||||||||||||||||
| Resource: `${arn}/*`, | ||||||||||||||||||||||||||||
| Resource: `${bucketArn}/*`, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||
| Effect: "Allow", | ||||||||||||||||||||||||||||
| Action: ["s3:*"], | ||||||||||||||||||||||||||||
| Resource: `${arn}`, | ||||||||||||||||||||||||||||
| Resource: bucketArn, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||
| cloudfrontArn && { | ||||||||||||||||||||||||||||
| Effect: "Allow", | ||||||||||||||||||||||||||||
| Action: ["cloudfront:CreateInvalidation"], | ||||||||||||||||||||||||||||
| Resource: cloudfrontArn, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| ].filter(Boolean), | ||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
|
Comment on lines
+132
to
157
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chainIAM policy hardening: principle of least privilege and CloudFront resource scoping.
Apply this diff to scope actions; adjust CloudFront after verification: - Action: ["s3:*"],
+ Action: ["s3:ListBucket"],
Resource: ${bucketArn},
},
{
Effect: "Allow",
- Action: ["s3:*"],
+ Action: ["s3:GetObject","s3:PutObject","s3:DeleteObject"],
Resource: `${bucketArn}/*`,
},
- cloudfrontArn && {
- Effect: "Allow",
- Action: ["cloudfront:CreateInvalidation"],
- Resource: cloudfrontArn,
- },
+ // After verification, either keep ARN if supported or use "*"
+ cloudfrontArn && {
+ Effect: "Allow",
+ Action: ["cloudfront:CreateInvalidation"],
+ Resource: cloudfrontArn, // replace with "*" if ARNs aren't supported
+ },Also, ensure 🌐 Web query: 💡 Result: Yes — cloudfront:CreateInvalidation supports resource-level permissions. The Service Authorization Reference shows CreateInvalidation requires the distribution resource (arn:...:distribution/...) so you can restrict Resource to that distribution ARN instead of "*". [1][2] Sources:
Scope S3 actions and narrow CloudFront invalidation resource
{
Effect: "Allow",
- Action: ["s3:*"],
+ Action: ["s3:ListBucket"],
Resource: ${bucketArn},
},
{
Effect: "Allow",
- Action: ["s3:*"],
+ Action: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
Resource: `${bucketArn}/*`,
},
- cloudfrontArn && {
- Effect: "Allow",
- Action: ["cloudfront:CreateInvalidation"],
- Resource: cloudfrontArn,
- },
+ cloudfrontArn && {
+ Effect: "Allow",
+ Action: ["cloudfront:CreateInvalidation"],
+ Resource: cloudfrontArn,
+ },
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const workflowCluster = await WorkflowCluster(recordingsBucket, secrets); | ||||||||||||||||||||||||||||
| const workflowCluster = | ||||||||||||||||||||||||||||
| $app.stage === "staging" | ||||||||||||||||||||||||||||
| ? await WorkflowCluster(recordingsBucket, secrets) | ||||||||||||||||||||||||||||
| : null; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if ($app.stage === "staging" || $app.stage === "production") { | ||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||
| ...vercelVariables, | ||||||||||||||||||||||||||||
| { key: "WORKFLOWS_RPC_URL", value: workflowCluster.api.url }, | ||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||
| workflowCluster && { | ||||||||||||||||||||||||||||
| key: "WORKFLOWS_RPC_URL", | ||||||||||||||||||||||||||||
| value: workflowCluster.api.url, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| workflowCluster && { | ||||||||||||||||||||||||||||
| key: "WORKFLOWS_RPC_SECRET", | ||||||||||||||||||||||||||||
| value: secrets.WORKFLOWS_RPC_SECRET.result, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| { key: "VERCEL_AWS_ROLE_ARN", value: vercelAwsAccessRole.arn }, | ||||||||||||||||||||||||||||
| ].map( | ||||||||||||||||||||||||||||
| (v) => | ||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||
| .filter(Boolean) | ||||||||||||||||||||||||||||
| .forEach((_v) => { | ||||||||||||||||||||||||||||
| const v = _v as NonNullable<typeof _v>; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| new vercel.ProjectEnvironmentVariable(`VercelEnv${v.key}`, { | ||||||||||||||||||||||||||||
| ...v, | ||||||||||||||||||||||||||||
| projectId: vercelProject.id, | ||||||||||||||||||||||||||||
|
|
@@ -171,8 +189,8 @@ export default $config({ | |||||||||||||||||||||||||||
| : undefined, | ||||||||||||||||||||||||||||
| targets: | ||||||||||||||||||||||||||||
| $app.stage === "staging" ? undefined : ["preview", "production"], | ||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // DiscordBot(); | ||||||||||||||||||||||||||||
|
|
@@ -193,21 +211,6 @@ function Secrets() { | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| type Secrets = ReturnType<typeof Secrets>; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // function Planetscale() { | ||||||||||||||||||||||||||||
| // const org = planetscale.getOrganizationOutput({ name: "cap" }); | ||||||||||||||||||||||||||||
| // const db = planetscale.getDatabaseOutput({ | ||||||||||||||||||||||||||||
| // name: "cap-production", | ||||||||||||||||||||||||||||
| // organization: org.name, | ||||||||||||||||||||||||||||
| // }); | ||||||||||||||||||||||||||||
| // const branch = planetscale.getBranchOutput({ | ||||||||||||||||||||||||||||
| // name: $app.stage === "production" ? "main" : "staging", | ||||||||||||||||||||||||||||
| // database: db.name, | ||||||||||||||||||||||||||||
| // organization: org.name, | ||||||||||||||||||||||||||||
| // }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // return { org, db, branch }; | ||||||||||||||||||||||||||||
| // } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // function DiscordBot() { | ||||||||||||||||||||||||||||
| // new sst.cloudflare.Worker("DiscordBotScript", { | ||||||||||||||||||||||||||||
| // handler: "../apps/discord-bot/src/index.ts", | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { fromContainerMetadata, fromSSO } from "@aws-sdk/credential-providers"; | ||
| import type { | ||
| AwsCredentialIdentity, | ||
| AwsCredentialIdentityProvider, | ||
| } from "@smithy/types"; | ||
| import { awsCredentialsProvider } from "@vercel/functions/oidc"; | ||
| import { Config, Effect, Option } from "effect"; | ||
|
|
||
| export class AwsCredentials extends Effect.Service<AwsCredentials>()( | ||
| "AwsCredentials", | ||
| { | ||
| effect: Effect.gen(function* () { | ||
| let credentials: AwsCredentialIdentity | AwsCredentialIdentityProvider; | ||
|
|
||
| const accessKeys = yield* Config.option( | ||
| Config.all([ | ||
| Config.string("CAP_AWS_ACCESS_KEY"), | ||
| Config.string("CAP_AWS_SECRET_KEY"), | ||
| ]), | ||
| ); | ||
| const vercelAwsRole = yield* Config.option( | ||
| Config.string("VERCEL_AWS_ROLE_ARN"), | ||
| ); | ||
|
|
||
| if (Option.isSome(accessKeys)) { | ||
| const [accessKeyId, secretAccessKey] = accessKeys.value; | ||
| yield* Effect.log("Using CAP_AWS_ACCESS_KEY and CAP_AWS_SECRET_KEY"); | ||
| credentials = { accessKeyId, secretAccessKey }; | ||
| } else if (Option.isSome(vercelAwsRole)) { | ||
| yield* Effect.log("Using VERCEL_AWS_ROLE_ARN"); | ||
| credentials = awsCredentialsProvider({ roleArn: vercelAwsRole.value }); | ||
| } else if (process.env.NODE_ENV === "development") { | ||
| yield* Effect.log("Using AWS_DEFAULT_PROFILE"); | ||
| credentials = fromSSO({ profile: process.env.AWS_DEFAULT_PROFILE }); | ||
| } else { | ||
| yield* Effect.log("Falling back to ECS metadata"); | ||
| credentials = fromContainerMetadata(); | ||
| } | ||
|
|
||
| return { credentials }; | ||
| }), | ||
| }, | ||
| ) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
S3 bucket lacks security hardening (public access block, SSE, ownership controls).
Harden defaults for the recordings bucket.
Keep the bucket instantiation, and add these resources below it:
🤖 Prompt for AI Agents