From 81f0815b66ed73abe6bb257590b1d43f29911abd Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 23 Sep 2025 14:12:51 +0800 Subject: [PATCH 1/8] vercel function oidc --- packages/web-backend/package.json | 5 ++- packages/web-backend/src/S3Buckets/index.ts | 21 ++++++--- pnpm-lock.yaml | 47 ++++++++++++++++----- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/packages/web-backend/package.json b/packages/web-backend/package.json index a34b8b7fd..96cee441b 100644 --- a/packages/web-backend/package.json +++ b/packages/web-backend/package.json @@ -15,9 +15,10 @@ "@effect/platform": "^0.90.1", "@effect/rpc": "^0.68.3", "@smithy/types": "^4.3.1", + "@vercel/functions": "^3.1.0", "drizzle-orm": "0.43.1", "effect": "^3.17.7", - "server-only": "^0.0.1", - "next": "14.2.3" + "next": "14.2.3", + "server-only": "^0.0.1" } } diff --git a/packages/web-backend/src/S3Buckets/index.ts b/packages/web-backend/src/S3Buckets/index.ts index 0d4507d8e..49864d7c6 100644 --- a/packages/web-backend/src/S3Buckets/index.ts +++ b/packages/web-backend/src/S3Buckets/index.ts @@ -3,7 +3,9 @@ import * as CloudFrontPresigner from "@aws-sdk/cloudfront-signer"; import { decrypt } from "@cap/database/crypto"; import { S3_BUCKET_URL } from "@cap/utils"; import type { S3Bucket } from "@cap/web-domain"; +import { awsCredentialsProvider } from "@vercel/functions/oidc"; import { Config, Context, Effect, Layer, Option } from "effect"; + import { S3BucketAccess } from "./S3BucketAccess"; import { S3BucketClientProvider } from "./S3BucketClientProvider"; import { S3BucketsRepo } from "./S3BucketsRepo"; @@ -34,8 +36,18 @@ export class S3Buckets extends Effect.Service()("S3Buckets", { ), ), region: yield* Config.string("CAP_AWS_REGION"), - accessKey: yield* Config.string("CAP_AWS_ACCESS_KEY"), - secretKey: yield* Config.string("CAP_AWS_SECRET_KEY"), + credentials: yield* Config.string("CAP_AWS_ACCESS_KEY").pipe( + Effect.zip(Config.string("CAP_AWS_SECRET_KEY")), + Effect.map(([accessKeyId, secretAccessKey]) => ({ + accessKeyId, + secretAccessKey, + })), + Effect.catchAll(() => + Config.string("VERCEL_AWS_ROLE_ARN").pipe( + Effect.map((arn) => awsCredentialsProvider({ roleArn: arn })), + ), + ), + ), forcePathStyle: Option.getOrNull( yield* Config.boolean("S3_PATH_STYLE").pipe(Config.option), @@ -49,10 +61,7 @@ export class S3Buckets extends Effect.Service()("S3Buckets", { ? defaultConfigs.internalEndpoint : defaultConfigs.publicEndpoint, region: defaultConfigs.region, - credentials: { - accessKeyId: defaultConfigs.accessKey, - secretAccessKey: defaultConfigs.secretKey, - }, + credentials: defaultConfigs.credentials, forcePathStyle: defaultConfigs.forcePathStyle, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdd639470..2c3780916 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1224,6 +1224,9 @@ importers: '@smithy/types': specifier: ^4.3.1 version: 4.3.1 + '@vercel/functions': + specifier: ^3.1.0 + version: 3.1.0(@aws-sdk/credential-provider-web-identity@3.804.0) drizzle-orm: specifier: 0.43.1 version: 0.43.1(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(mysql2@3.14.1) @@ -5831,10 +5834,10 @@ packages: react-dom: optional: true - '@storybook/builder-vite@10.0.0-beta.2': - resolution: {integrity: sha512-H4U+LXXrxXFezTzPc0pfvZA/qjyg9XmgPCGSXa0Q41LH+8UrW9uP42TDVarQkD77jS4Trd7QIFCN64xe34XAoQ==} + '@storybook/builder-vite@10.0.0-beta.6': + resolution: {integrity: sha512-vtVS4cjw1+8eO7J6TxhWQ2ijjzCDY/J03NVr6clTECaP3JKrSVMnQCQ1LEOz1Z9jXPXyl2pGqiMLbYdfryINkg==} peerDependencies: - storybook: ^10.0.0-beta.2 + storybook: ^10.0.0-beta.6 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 '@storybook/core@8.6.12': @@ -5845,12 +5848,12 @@ packages: prettier: optional: true - '@storybook/csf-plugin@10.0.0-beta.2': - resolution: {integrity: sha512-OBjwaEdG3OrgsvUncu0AptCOH9u9BDQeIlMChj/QxnlNCy4yTUdPYNJ140mYzl51a20IKi6OTRverLopP7RNUg==} + '@storybook/csf-plugin@10.0.0-beta.6': + resolution: {integrity: sha512-2aG6qjedB4ObAtibwVGBjMdVdq48P9kYl/juHbAn+KbEPHTWZwnH4bQEgEwbdzMACCk0jlJwNpMcpnRSeTrZZg==} peerDependencies: esbuild: '*' rollup: '*' - storybook: ^10.0.0-beta.2 + storybook: ^10.0.0-beta.6 vite: '*' webpack: '*' peerDependenciesMeta: @@ -6753,6 +6756,15 @@ packages: resolution: {integrity: sha512-1++yncEyIAi68D3UEOlytYb1IUcIulMWdoSzX2h9LuSeeyR7JtaIgR8DcTQ6+DmYOQn+5MCh6LY+UmK6QBByNA==} deprecated: This package is deprecated. You should to use `@vercel/functions` instead. + '@vercel/functions@3.1.0': + resolution: {integrity: sha512-V+p8dO+sg1VjiJJUO5rYPp1KG17SzDcR74OWwW7Euyde6L8U5wuTMe9QfEOfLTiWPUPzN1MXZvLcYxqSYhKc4Q==} + engines: {node: '>= 20'} + peerDependencies: + '@aws-sdk/credential-provider-web-identity': '*' + peerDependenciesMeta: + '@aws-sdk/credential-provider-web-identity': + optional: true + '@vercel/nft@0.27.7': resolution: {integrity: sha512-FG6H5YkP4bdw9Ll1qhmbxuE8KwW2E/g8fJpM183fWQLeVDGqzeywMIeJ9h2txdWZ03psgWMn6QymTxaDLmdwUg==} engines: {node: '>=16'} @@ -6763,6 +6775,10 @@ packages: engines: {node: '>=18'} hasBin: true + '@vercel/oidc@3.0.0': + resolution: {integrity: sha512-XOoUcf/1VfGArUAfq0ELxk6TD7l4jGcrOsWjQibj4wYM74uNihzZ9gA46ywWegoqKWWdph4y5CKxGI9823deoA==} + engines: {node: '>= 20'} + '@vinxi/listhen@1.5.6': resolution: {integrity: sha512-WSN1z931BtasZJlgPp704zJFnQFRg7yzSjkm3MzAWQYe4uXFXlFr1hc5Ac2zae5/HDOz5x1/zDM5Cb54vTCnWw==} hasBin: true @@ -19271,9 +19287,9 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@storybook/builder-vite@10.0.0-beta.2(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))': + '@storybook/builder-vite@10.0.0-beta.6(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))': dependencies: - '@storybook/csf-plugin': 10.0.0-beta.2(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)) + '@storybook/csf-plugin': 10.0.0-beta.6(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)) storybook: 8.6.12(prettier@3.5.3) ts-dedent: 2.2.0 vite: 6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1) @@ -19303,7 +19319,7 @@ snapshots: - supports-color - utf-8-validate - '@storybook/csf-plugin@10.0.0-beta.2(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))': + '@storybook/csf-plugin@10.0.0-beta.6(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))': dependencies: storybook: 8.6.12(prettier@3.5.3) unplugin: 2.3.8 @@ -20287,6 +20303,12 @@ snapshots: '@vercel/edge@1.2.1': {} + '@vercel/functions@3.1.0(@aws-sdk/credential-provider-web-identity@3.804.0)': + dependencies: + '@vercel/oidc': 3.0.0 + optionalDependencies: + '@aws-sdk/credential-provider-web-identity': 3.804.0 + '@vercel/nft@0.27.7(encoding@0.1.13)(rollup@4.40.2)': dependencies: '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) @@ -20325,6 +20347,11 @@ snapshots: - rollup - supports-color + '@vercel/oidc@3.0.0': + dependencies: + '@types/ms': 2.1.0 + ms: 2.1.3 + '@vinxi/listhen@1.5.6': dependencies: '@parcel/watcher': 2.5.1 @@ -27499,7 +27526,7 @@ snapshots: storybook-solidjs-vite@1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(esbuild@0.25.4)(rollup@4.40.2)(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)): dependencies: - '@storybook/builder-vite': 10.0.0-beta.2(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)) + '@storybook/builder-vite': 10.0.0-beta.6(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)) '@storybook/types': 9.0.0-alpha.1(storybook@8.6.12(prettier@3.5.3)) magic-string: 0.30.17 solid-js: 1.9.6 From 2c21d12b14eb26ea069b8780326bd7ddfd8964f5 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 23 Sep 2025 17:58:27 +0800 Subject: [PATCH 2/8] use vercel oidc --- apps/web/package.json | 1 + apps/web/utils/s3.ts | 20 +++-- infra/sst-env.d.ts | 18 +--- infra/sst.config.ts | 196 ++++++++++++------------------------------ pnpm-lock.yaml | 3 + 5 files changed, 73 insertions(+), 165 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 56307e69a..675d9d711 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -63,6 +63,7 @@ "@tanstack/store": "^0.5.5", "@ts-rest/core": "^3.52.1", "@uidotdev/usehooks": "^2.4.1", + "@vercel/functions": "^3.1.0", "@virtual-grid/react": "^2.0.3", "@workos-inc/node": "^7.34.0", "aws-sdk": "^2.1530.0", diff --git a/apps/web/utils/s3.ts b/apps/web/utils/s3.ts index 32ada2562..6f49b5f2e 100644 --- a/apps/web/utils/s3.ts +++ b/apps/web/utils/s3.ts @@ -40,6 +40,7 @@ import type { RequestPresigningArguments, StreamingBlobPayloadInputTypes, } from "@smithy/types"; +import { awsCredentialsProvider } from "@vercel/functions/oidc"; import type { InferSelectModel } from "drizzle-orm"; type S3Config = { @@ -64,16 +65,19 @@ async function tryDecrypt( export async function getS3Config(config?: S3Config, internal = false) { if (!config) { + const env = serverEnv(); return { endpoint: internal - ? (serverEnv().S3_INTERNAL_ENDPOINT ?? serverEnv().CAP_AWS_ENDPOINT) - : (serverEnv().S3_PUBLIC_ENDPOINT ?? serverEnv().CAP_AWS_ENDPOINT), - region: serverEnv().CAP_AWS_REGION, - credentials: { - accessKeyId: serverEnv().CAP_AWS_ACCESS_KEY ?? "", - secretAccessKey: serverEnv().CAP_AWS_SECRET_KEY ?? "", - }, - forcePathStyle: serverEnv().S3_PATH_STYLE, + ? (env.S3_INTERNAL_ENDPOINT ?? env.CAP_AWS_ENDPOINT) + : (env.S3_PUBLIC_ENDPOINT ?? env.CAP_AWS_ENDPOINT), + region: env.CAP_AWS_REGION, + credentials: env.VERCEL_AWS_ROLE_ARN + ? awsCredentialsProvider({ roleArn: env.VERCEL_AWS_ROLE_ARN }) + : { + accessKeyId: env.CAP_AWS_ACCESS_KEY ?? "", + secretAccessKey: env.CAP_AWS_SECRET_KEY ?? "", + }, + forcePathStyle: env.S3_PATH_STYLE, }; } diff --git a/infra/sst-env.d.ts b/infra/sst-env.d.ts index cb54317ed..c1b62bdd4 100644 --- a/infra/sst-env.d.ts +++ b/infra/sst-env.d.ts @@ -4,20 +4,10 @@ /* deno-fmt-ignore-file */ declare module "sst" { - export interface Resource { - DISCORD_BOT_TOKEN: { - type: "sst.sst.Secret"; - value: string; - }; - DiscordBotScript: { - type: "sst.cloudflare.Worker"; - }; - GITHUB_APP_PRIVATE_KEY: { - type: "sst.sst.Secret"; - value: string; - }; - } + export interface Resource { + } } /// -import "sst"; +import "sst" +export {} \ No newline at end of file diff --git a/infra/sst.config.ts b/infra/sst.config.ts index c85d7dae6..e8f0ba1bd 100644 --- a/infra/sst.config.ts +++ b/infra/sst.config.ts @@ -19,6 +19,7 @@ export default $config({ providers: { vercel: { team: VERCEL_TEAM_ID, + version: "3.15.1", }, github: { owner: GITHUB_ORG, @@ -28,6 +29,8 @@ export default $config({ }; }, async run() { + // const planetscale = Planetscale(); + const recordingsBucket = new aws.s3.BucketV2("RecordingsBucket"); new aws.s3.BucketAccelerateConfigurationV2("RecordingsBucketAcceleration", { @@ -35,159 +38,54 @@ export default $config({ status: "Enabled", }); - const cloudfrontDistribution = new aws.cloudfront.Distribution( - "CapSoCloudfrontDistribution", - { - aliases: ["v.cap.so"], - defaultCacheBehavior: { - cachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6", - compress: true, - allowedMethods: ["GET", "HEAD", "OPTIONS"], - cachedMethods: ["GET", "HEAD", "OPTIONS"], - targetOriginId: recordingsBucket.bucketRegionalDomainName, - viewerProtocolPolicy: "redirect-to-https", - originRequestPolicyId: "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf", - responseHeadersPolicyId: "07e54f95-0547-4c80-a967-95a236bd9b94", - }, - isIpv6Enabled: true, - enabled: true, - restrictions: { geoRestriction: { restrictionType: "none" } }, - viewerCertificate: { - acmCertificateArn: - "arn:aws:acm:us-east-1:211125561119:certificate/9165b27f-0f9e-497b-9ff5-5b6a885c5eed", - minimumProtocolVersion: "TLSv1.2_2021", - sslSupportMethod: "sni-only", - }, - webAclId: - "arn:aws:wafv2:us-east-1:211125561119:global/webacl/CreatedByCloudFront-4f671e75-3f7c-45dd-9283-979b497f5af7/0e2022cf-dd4a-4427-908f-f7e88530894b", - orderedCacheBehaviors: [ - { - allowedMethods: ["GET", "HEAD", "OPTIONS"], - cachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6", - cachedMethods: ["GET", "HEAD", "OPTIONS"], - compress: true, - originRequestPolicyId: "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf", - pathPattern: "_recording", - realtimeLogConfigArn: "", - responseHeadersPolicyId: "5cc3b908-e619-4b99-88e5-2cf7f45965bd", - targetOriginId: "cap.so", - viewerProtocolPolicy: "redirect-to-https", - }, - { - allowedMethods: ["GET", "HEAD", "OPTIONS"], - cachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6", - cachedMethods: ["GET", "HEAD", "OPTIONS"], - compress: true, - originRequestPolicyId: "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf", - pathPattern: "/dev/*", - responseHeadersPolicyId: "07e54f95-0547-4c80-a967-95a236bd9b94", - targetOriginId: "capso-dev.s3.us-east-1.amazonaws.com", - viewerProtocolPolicy: "redirect-to-https", - }, - ], - origins: [ - { - connectionAttempts: 3, - connectionTimeout: 10, - customOriginConfig: { - httpPort: 80, - httpsPort: 443, - originKeepaliveTimeout: 5, - originProtocolPolicy: "https-only", - originReadTimeout: 30, - originSslProtocols: ["TLSv1.2"], - }, - domainName: "cap.link", - originId: "cap.link", - originPath: "", - originShield: { - enabled: true, - originShieldRegion: "us-east-1", - }, - }, - { - customOriginConfig: { - httpPort: 80, - httpsPort: 443, - originKeepaliveTimeout: 5, - originProtocolPolicy: "https-only", - originReadTimeout: 30, - originSslProtocols: ["TLSv1.2"], - }, - domainName: "cap.so", - originId: "cap.so", - originShield: { - enabled: true, - originShieldRegion: "us-east-1", - }, - }, - { - domainName: "capso-dev.s3.us-east-1.amazonaws.com", - originAccessControlId: "E2CB8AE0M9IHH8", - originId: "capso-dev.s3.us-east-1.amazonaws.com", - }, - { - domainName: recordingsBucket.bucketRegionalDomainName, - originAccessControlId: "E26H3W7A2N2HP3", - originId: recordingsBucket.bucketRegionalDomainName, - originShield: { - enabled: true, - originShieldRegion: recordingsBucket.region, - }, - }, - ], - }, - ); + // const cloudfrontDistribution = aws.cloudfront.getDistributionOutput({ + // id: "E36XSZEM0VIIYB", + // }); - const vercelUser = new aws.iam.User( - "VercelUser", - { - name: "uploader", - forceDestroy: false, - }, - { import: "uploader" }, - ); + const vercelUser = new aws.iam.User("VercelUser", { forceDestroy: false }); const vercelAccessKey = new aws.iam.AccessKey("VercelS3AccessKey", { user: vercelUser.name, }); - const vercelProject = new vercel.Project("VercelProject", { - buildCommand: "cd ../.. && pnpm turbo run build --filter=@cap/web", - installCommand: "pnpm install --no-frozen-lockfile", - framework: "nextjs", - gitRepository: { - productionBranch: "main", - repo: `${GITHUB_ORG}/${GITHUB_REPO}`, - type: "github", - }, - protectionBypassForAutomation: true, - rootDirectory: "apps/web", - }); - - new vercel.ProjectEnvironmentVariable("VercelS3AccessEnv", { + const vercelProject = vercel.getProjectOutput({ name: "cap-web" }); + + function vercelEnvVar( + name: string, + args: Omit< + vercel.ProjectEnvironmentVariableArgs, + "projectId" | "customEnvironmentIds" | "targets" + >, + ) { + new vercel.ProjectEnvironmentVariable(name, { + ...args, + projectId: vercelProject.id, + customEnvironmentIds: + $app.stage === "staging" + ? ["env_CFbtmnpsI11e4o8X5UD8MZzxELQi"] + : undefined, + targets: + $app.stage === "staging" ? undefined : ["preview", "production"], + }); + } + + vercelEnvVar("VercelS3AccessEnv", { key: "CAP_AWS_ACCESS_KEY", value: vercelAccessKey.id, - projectId: vercelProject.id, - targets: ["production", "preview", "development"], }); - new vercel.ProjectEnvironmentVariable("VercelCloudfrontEnv", { - key: "CAP_CLOUDFRONT_DISTRIBUTION_ID", - value: cloudfrontDistribution.id, - projectId: vercelProject.id, - targets: ["production", "preview", "development"], - }); + // vercelEnvVar("VercelCloudfrontEnv", { + // key: "CAP_CLOUDFRONT_DISTRIBUTION_ID", + // value: cloudfrontDistribution.id, + // }); - new aws.iam.OpenIdConnectProvider("VercelOIDCProvider", { + const vercelOidc = aws.iam.getOpenIdConnectProviderOutput({ url: "https://oidc.vercel.com", - clientIdLists: [`https://vercel.com/${VERCEL_TEAM_ID}`], }); const awsAccount = await aws.getCallerIdentity(); const vercelAwsAccessRole = new aws.iam.Role("VercelAWSAccessRole", { - name: "VercelOIDCRole", assumeRolePolicy: { Version: "2012-10-17", Statement: [ @@ -203,8 +101,7 @@ export default $config({ }, StringLike: { [`oidc.vercel.com/${VERCEL_TEAM_SLUG}:sub`]: [ - `owner:${VERCEL_TEAM_SLUG}:project:${vercelProject.name}:environment:preview`, - `owner:${VERCEL_TEAM_SLUG}:project:${vercelProject.name}:environment:production`, + `owner:${VERCEL_TEAM_SLUG}:project:${vercelProject.name}:environment:${$app.stage}`, ], }, }, @@ -213,17 +110,30 @@ export default $config({ }, }); - new vercel.ProjectEnvironmentVariable("VercelAWSAccessRoleArn", { - key: "AWS_ROLE_ARN", + vercelEnvVar("VercelAWSAccessRoleArn", { + key: "VERCEL_AWS_ROLE_ARN", value: vercelAwsAccessRole.arn, - projectId: vercelProject.id, - targets: ["production", "preview"], }); - DiscordBot(); + // DiscordBot(); }, }); +// 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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c3780916..77f837605 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -576,6 +576,9 @@ importers: '@uidotdev/usehooks': specifier: ^2.4.1 version: 2.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@vercel/functions': + specifier: ^3.1.0 + version: 3.1.0(@aws-sdk/credential-provider-web-identity@3.804.0) '@virtual-grid/react': specifier: ^2.0.3 version: 2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) From f11533298836d0d9f1dcfac17021e779423b89c2 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 23 Sep 2025 18:04:57 +0800 Subject: [PATCH 3/8] make aws endpoint optional --- packages/web-backend/src/S3Buckets/index.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/web-backend/src/S3Buckets/index.ts b/packages/web-backend/src/S3Buckets/index.ts index 49864d7c6..940b73b93 100644 --- a/packages/web-backend/src/S3Buckets/index.ts +++ b/packages/web-backend/src/S3Buckets/index.ts @@ -18,22 +18,10 @@ export class S3Buckets extends Effect.Service()("S3Buckets", { publicEndpoint: yield* Config.string("S3_PUBLIC_ENDPOINT").pipe( Config.orElse(() => Config.string("CAP_AWS_ENDPOINT")), Config.option, - Effect.flatten, - Effect.catchTag("NoSuchElementException", () => - Effect.dieMessage( - "Neither S3_PUBLIC_ENDPOINT nor CAP_AWS_ENDPOINT provided", - ), - ), ), internalEndpoint: yield* Config.string("S3_INTERNAL_ENDPOINT").pipe( Config.orElse(() => Config.string("CAP_AWS_ENDPOINT")), Config.option, - Effect.flatten, - Effect.catchTag("NoSuchElementException", () => - Effect.dieMessage( - "Neither S3_INTERNAL_ENDPOINT nor CAP_AWS_ENDPOINT provided", - ), - ), ), region: yield* Config.string("CAP_AWS_REGION"), credentials: yield* Config.string("CAP_AWS_ACCESS_KEY").pipe( @@ -58,8 +46,8 @@ export class S3Buckets extends Effect.Service()("S3Buckets", { const createDefaultClient = (internal: boolean) => new S3.S3Client({ endpoint: internal - ? defaultConfigs.internalEndpoint - : defaultConfigs.publicEndpoint, + ? Option.getOrUndefined(defaultConfigs.internalEndpoint) + : Option.getOrUndefined(defaultConfigs.publicEndpoint), region: defaultConfigs.region, credentials: defaultConfigs.credentials, forcePathStyle: defaultConfigs.forcePathStyle, From 068f6236a7373c39ddaf642abbf50d6cc0576d3c Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 23 Sep 2025 19:06:19 +0800 Subject: [PATCH 4/8] handle VERCEL_AWS_ROLE_ARN --- infra/sst-env.d.ts | 4 ++++ infra/sst.config.ts | 40 +++++++++++++++++++++++++++++++++------- packages/env/server.ts | 1 + 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/infra/sst-env.d.ts b/infra/sst-env.d.ts index c1b62bdd4..a570b0cfe 100644 --- a/infra/sst-env.d.ts +++ b/infra/sst-env.d.ts @@ -5,6 +5,10 @@ declare module "sst" { export interface Resource { + "DATABASE_URL": { + "type": "sst.sst.Secret" + "value": string + } } } /// diff --git a/infra/sst.config.ts b/infra/sst.config.ts index e8f0ba1bd..c1664a26a 100644 --- a/infra/sst.config.ts +++ b/infra/sst.config.ts @@ -29,6 +29,11 @@ export default $config({ }; }, async run() { + const WEB_URLS: Record = { + production: "https://cap.so", + staging: "https://cap-staging.brendonovich.dev", + }; + const webUrl = WEB_URLS[$app.stage]; // const planetscale = Planetscale(); const recordingsBucket = new aws.s3.BucketV2("RecordingsBucket"); @@ -44,10 +49,6 @@ export default $config({ const vercelUser = new aws.iam.User("VercelUser", { forceDestroy: false }); - const vercelAccessKey = new aws.iam.AccessKey("VercelS3AccessKey", { - user: vercelUser.name, - }); - const vercelProject = vercel.getProjectOutput({ name: "cap-web" }); function vercelEnvVar( @@ -69,16 +70,41 @@ export default $config({ }); } - vercelEnvVar("VercelS3AccessEnv", { - key: "CAP_AWS_ACCESS_KEY", - value: vercelAccessKey.id, + vercelEnvVar("VercelDatabaseURLEnv", { + key: "DATABASE_URL", + value: new sst.Secret("DATABASE_URL").value, }); + if (webUrl) { + vercelEnvVar("VercelWebURLEnv", { + key: "WEB_URL", + value: webUrl, + }); + vercelEnvVar("VercelNextPublicWebURLEnv", { + key: "NEXT_PUBLIC_WEB_URL", + value: webUrl, + }); + vercelEnvVar("VercelNextAuthURLEnv", { + key: "NEXTAUTH_URL", + value: webUrl, + }); + } + // vercelEnvVar("VercelCloudfrontEnv", { // key: "CAP_CLOUDFRONT_DISTRIBUTION_ID", // value: cloudfrontDistribution.id, // }); + vercelEnvVar("VercelAWSBucketEnv", { + key: "CAP_AWS_BUCKET", + value: recordingsBucket.bucket, + }); + + vercelEnvVar("VercelNextPublicAWSBucketEnv", { + key: "NEXT_PUBLIC_CAP_AWS_BUCKET", + value: recordingsBucket.bucket, + }); + const vercelOidc = aws.iam.getOpenIdConnectProviderOutput({ url: "https://oidc.vercel.com", }); diff --git a/packages/env/server.ts b/packages/env/server.ts index 678825bc5..1003d7560 100644 --- a/packages/env/server.ts +++ b/packages/env/server.ts @@ -65,6 +65,7 @@ function createServerEnv() { CLOUDFRONT_KEYPAIR_PRIVATE_KEY: z.string().optional(), S3_PUBLIC_ENDPOINT: z.string().optional(), S3_INTERNAL_ENDPOINT: z.string().optional(), + VERCEL_AWS_ROLE_ARN: z.string().optional(), }, experimental__runtimeEnv: { ...process.env, From 897e99bb558d4fd4036fe166fe2776cd3510a32a Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 23 Sep 2025 20:26:47 +0800 Subject: [PATCH 5/8] use get range=0-0 instead of head --- .../[videoId]/_components/CapVideoPlayer.tsx | 5 +++- apps/web/lib/transcribe.ts | 5 +++- infra/sst.config.ts | 30 ++++++++++++++++--- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx index 76cbd487f..4fe6da89a 100644 --- a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx +++ b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx @@ -88,7 +88,10 @@ export function CapVideoPlayer({ ? `${videoSrc}&_t=${timestamp}` : `${videoSrc}?_t=${timestamp}`; - const response = await fetch(urlWithTimestamp, { method: "HEAD" }); + const response = await fetch(urlWithTimestamp, { + method: "GET", + headers: { range: "bytes=0-0" }, + }); const finalUrl = response.redirected ? response.url : urlWithTimestamp; // Check if the resolved URL is from a CORS-incompatible service diff --git a/apps/web/lib/transcribe.ts b/apps/web/lib/transcribe.ts index 8fdc660de..d3e50c078 100644 --- a/apps/web/lib/transcribe.ts +++ b/apps/web/lib/transcribe.ts @@ -80,7 +80,10 @@ export async function transcribeVideo( // Check if video file actually exists before transcribing try { - const headResponse = await fetch(videoUrl, { method: "HEAD" }); + const headResponse = await fetch(videoUrl, { + method: "GET", + headers: { range: "bytes=0-0" }, + }); if (!headResponse.ok) { // Video not ready yet - reset to null for retry await db() diff --git a/infra/sst.config.ts b/infra/sst.config.ts index c1664a26a..2de230476 100644 --- a/infra/sst.config.ts +++ b/infra/sst.config.ts @@ -106,10 +106,10 @@ export default $config({ }); const vercelOidc = aws.iam.getOpenIdConnectProviderOutput({ - url: "https://oidc.vercel.com", + url: `https://oidc.vercel.com/${VERCEL_TEAM_SLUG}`, }); - const awsAccount = await aws.getCallerIdentity(); + const awsAccount = aws.getCallerIdentityOutput(); const vercelAwsAccessRole = new aws.iam.Role("VercelAWSAccessRole", { assumeRolePolicy: { @@ -118,7 +118,7 @@ export default $config({ { Effect: "Allow", Principal: { - Federated: `arn:aws:iam::${awsAccount.id}:oidc-provider/oidc.vercel.com/${VERCEL_TEAM_SLUG}`, + Federated: $interpolate`arn:aws:iam::${awsAccount.id}:oidc-provider/oidc.vercel.com/${VERCEL_TEAM_SLUG}`, }, Action: "sts:AssumeRoleWithWebIdentity", Condition: { @@ -127,13 +127,35 @@ export default $config({ }, StringLike: { [`oidc.vercel.com/${VERCEL_TEAM_SLUG}:sub`]: [ - `owner:${VERCEL_TEAM_SLUG}:project:${vercelProject.name}:environment:${$app.stage}`, + `owner:${VERCEL_TEAM_SLUG}:project:*:environment:staging`, ], }, }, }, ], }, + inlinePolicies: [ + { + name: "VercelAWSAccessPolicy", + policy: recordingsBucket.arn.apply((arn) => + JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Action: ["s3:*"], + Resource: `${arn}/*`, + }, + { + Effect: "Allow", + Action: ["s3:*"], + Resource: `${arn}`, + }, + ], + }), + ), + }, + ], }); vercelEnvVar("VercelAWSAccessRoleArn", { From e66fcd8247f1b7702c819a0acfdc6e32f3dcfa0d Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 23 Sep 2025 20:31:36 +0800 Subject: [PATCH 6/8] optional secret keys --- infra/sst.config.ts | 11 +++++------ packages/env/server.ts | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/infra/sst.config.ts b/infra/sst.config.ts index 2de230476..b2c7f9a22 100644 --- a/infra/sst.config.ts +++ b/infra/sst.config.ts @@ -123,11 +123,10 @@ export default $config({ Action: "sts:AssumeRoleWithWebIdentity", Condition: { StringEquals: { - [`oidc.vercel.com/${VERCEL_TEAM_SLUG}:aud`]: `https://vercel.com/${VERCEL_TEAM_SLUG}`, - }, - StringLike: { + [`oidc.vercel.com/${VERCEL_TEAM_SLUG}:aud`]: + vercelOidc.clientIdLists[0], [`oidc.vercel.com/${VERCEL_TEAM_SLUG}:sub`]: [ - `owner:${VERCEL_TEAM_SLUG}:project:*:environment:staging`, + `owner:${VERCEL_TEAM_SLUG}:project:${vercelProject.name}:environment:staging`, ], }, }, @@ -149,10 +148,10 @@ export default $config({ { Effect: "Allow", Action: ["s3:*"], - Resource: `${arn}`, + Resource: arn, }, ], - }), + } satisfies aws.iam.PolicyDocument), ), }, ], diff --git a/packages/env/server.ts b/packages/env/server.ts index 1003d7560..1d97b2fe7 100644 --- a/packages/env/server.ts +++ b/packages/env/server.ts @@ -21,8 +21,8 @@ function createServerEnv() { CAP_AWS_BUCKET: z.string(), CAP_AWS_REGION: z.string(), CAP_AWS_BUCKET_URL: z.string().optional(), - CAP_AWS_ACCESS_KEY: z.string(), - CAP_AWS_SECRET_KEY: z.string(), + CAP_AWS_ACCESS_KEY: z.string().optional(), + CAP_AWS_SECRET_KEY: z.string().optional(), CAP_AWS_ENDPOINT: z.string().optional(), CAP_AWS_MEDIACONVERT_ROLE_ARN: z.string().optional(), CAP_CLOUDFRONT_DISTRIBUTION_ID: z.string().optional(), From c16116d2ebd6c8b2a52d93b1922c4610c87e77d9 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 23 Sep 2025 23:05:44 +0800 Subject: [PATCH 7/8] stanging env vars --- infra/sst.config.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/infra/sst.config.ts b/infra/sst.config.ts index b2c7f9a22..bbaee4ac2 100644 --- a/infra/sst.config.ts +++ b/infra/sst.config.ts @@ -31,7 +31,7 @@ export default $config({ async run() { const WEB_URLS: Record = { production: "https://cap.so", - staging: "https://cap-staging.brendonovich.dev", + staging: "https://staging.cap.so", }; const webUrl = WEB_URLS[$app.stage]; // const planetscale = Planetscale(); @@ -123,10 +123,11 @@ export default $config({ Action: "sts:AssumeRoleWithWebIdentity", Condition: { StringEquals: { - [`oidc.vercel.com/${VERCEL_TEAM_SLUG}:aud`]: - vercelOidc.clientIdLists[0], + [`oidc.vercel.com/${VERCEL_TEAM_SLUG}:aud`]: `https://vercel.com/${VERCEL_TEAM_SLUG}`, + }, + StringLike: { [`oidc.vercel.com/${VERCEL_TEAM_SLUG}:sub`]: [ - `owner:${VERCEL_TEAM_SLUG}:project:${vercelProject.name}:environment:staging`, + `owner:${VERCEL_TEAM_SLUG}:project:*:environment:staging`, ], }, }, @@ -148,10 +149,10 @@ export default $config({ { Effect: "Allow", Action: ["s3:*"], - Resource: arn, + Resource: `${arn}`, }, ], - } satisfies aws.iam.PolicyDocument), + }), ), }, ], From 271ae30a699182b1c821bf9f00be66c4152b2dc7 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 24 Sep 2025 12:33:43 +0800 Subject: [PATCH 8/8] format --- infra/sst-env.d.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/infra/sst-env.d.ts b/infra/sst-env.d.ts index a570b0cfe..ba2fdf30c 100644 --- a/infra/sst-env.d.ts +++ b/infra/sst-env.d.ts @@ -4,14 +4,13 @@ /* deno-fmt-ignore-file */ declare module "sst" { - export interface Resource { - "DATABASE_URL": { - "type": "sst.sst.Secret" - "value": string - } - } + export interface Resource { + DATABASE_URL: { + type: "sst.sst.Secret"; + value: string; + }; + } } /// -import "sst" -export {} \ No newline at end of file +import "sst";