| 1 | import type * as Credentials from "@distilled.cloud/aws/Credentials"; |
| 2 | import * as Presign from "@distilled.cloud/aws/Presign"; |
| 3 | import type * as Region from "@distilled.cloud/aws/Region"; |
| 4 | import * as Duration from "effect/Duration"; |
| 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Redacted from "effect/Redacted"; |
| 7 | |
| 8 | export interface GenerateDbAuthTokenOptions { |
| 9 | /** |
| 10 | * SigV4 signing service name: `rds-db` for RDS/Aurora IAM database |
| 11 | * authentication, `dsql` for Aurora DSQL. |
| 12 | */ |
| 13 | service: "rds-db" | "dsql"; |
| 14 | /** Database endpoint hostname the token authorizes a connection to. */ |
| 15 | hostname: string; |
| 16 | /** |
| 17 | * Database endpoint port. Signed into the token for `rds-db` (the RDS |
| 18 | * signer scopes tokens to `host:port`); DSQL tokens are host-only. |
| 19 | */ |
| 20 | port?: number; |
| 21 | /** Database user (`rds-db` only — signed as the `DBUser` parameter). */ |
| 22 | username?: string; |
| 23 | /** |
| 24 | * DSQL connect action (`dsql` only). |
| 25 | * @default "DbConnect" |
| 26 | */ |
| 27 | action?: "DbConnect" | "DbConnectAdmin"; |
| 28 | /** |
| 29 | * How long the token remains valid. |
| 30 | * @default 15 minutes |
| 31 | */ |
| 32 | expiresIn?: Duration.Input; |
| 33 | /** SigV4 signing region. Defaults to the ambient `Region` service. */ |
| 34 | region?: string; |
| 35 | } |
| 36 | |
| 37 | /** |
| 38 | * Generate a short-lived IAM database authentication token. |
| 39 | * |
| 40 | * This is a pure client-side SigV4 *presign* — no AWS API call is made |
| 41 | * (there is no distilled operation for it). The token is the presigned URL |
| 42 | * with the `https://` scheme stripped, exactly what the `rds-db` and `dsql` |
| 43 | * data planes expect as the connection password. It inherits the IAM |
| 44 | * permissions of the identity resolved from the ambient distilled |
| 45 | * `Credentials` service (`rds-db:connect` on the DB user resource for RDS, |
| 46 | * `dsql:DbConnect`/`dsql:DbConnectAdmin` on the cluster for DSQL). |
| 47 | * |
| 48 | * Because per-execution pools re-run their build on the first query of each |
| 49 | * execution (see `Runtime/ExecutionMemo.ts`), minting inside a pool build |
| 50 | * means a token can never outlive its pool on workerd or Lambda. |
| 51 | */ |
| 52 | export const generateDbAuthToken: ( |
| 53 | options: GenerateDbAuthTokenOptions, |
| 54 | ) => Effect.Effect< |
| 55 | Redacted.Redacted<string>, |
| 56 | Presign.PresignError, |
| 57 | Credentials.Credentials | Region.Region |
| 58 | > = Effect.fn(function* (options: GenerateDbAuthTokenOptions) { |
| 59 | const hostAndPort = |
| 60 | options.port !== undefined |
| 61 | ? `${options.hostname}:${options.port}` |
| 62 | : options.hostname; |
| 63 | const url = new URL(`https://${hostAndPort}/`); |
| 64 | if (options.service === "rds-db") { |
| 65 | url.searchParams.set("Action", "connect"); |
| 66 | if (options.username !== undefined) { |
| 67 | url.searchParams.set("DBUser", options.username); |
| 68 | } |
| 69 | } else { |
| 70 | url.searchParams.set("Action", options.action ?? "DbConnect"); |
| 71 | } |
| 72 | |
| 73 | const signed = yield* Presign.presignUrl({ |
| 74 | method: "GET", |
| 75 | url: url.toString(), |
| 76 | service: options.service, |
| 77 | region: options.region, |
| 78 | expiresIn: |
| 79 | options.expiresIn !== undefined |
| 80 | ? Math.floor(Duration.toSeconds(options.expiresIn)) |
| 81 | : 900, |
| 82 | }); |
| 83 | |
| 84 | return Redacted.make(signed.slice("https://".length)); |
| 85 | }); |