| 1 | import type * as Presign from "@distilled.cloud/aws/Presign"; |
| 2 | import type * as secretsmanager from "@distilled.cloud/aws/secrets-manager"; |
| 3 | import type * as Effect from "effect/Effect"; |
| 4 | import type * as Redacted from "effect/Redacted"; |
| 5 | import * as Binding from "../../Binding.ts"; |
| 6 | import type { Input } from "../../Input.ts"; |
| 7 | import type { RuntimeContext } from "../../RuntimeContext.ts"; |
| 8 | import type { SecurityGroupId } from "../EC2/SecurityGroup.ts"; |
| 9 | import type { SubnetId } from "../EC2/Subnet.ts"; |
| 10 | import type { Secret } from "../SecretsManager/Secret.ts"; |
| 11 | import type { DBCluster } from "./DBCluster.ts"; |
| 12 | import type { DBProxy } from "./DBProxy.ts"; |
| 13 | import type { DBProxyEndpoint } from "./DBProxyEndpoint.ts"; |
| 14 | |
| 15 | export type ConnectResource = DBCluster | DBProxy | DBProxyEndpoint; |
| 16 | |
| 17 | export interface ConnectionInfo { |
| 18 | host: string; |
| 19 | port: number; |
| 20 | database?: string; |
| 21 | username?: string; |
| 22 | password?: string; |
| 23 | ssl: boolean; |
| 24 | /** |
| 25 | * RFC-3986 connection URL — feeds `Drizzle.Postgres` / a Hyperdrive |
| 26 | * origin directly. `Redacted` because it embeds the password. |
| 27 | */ |
| 28 | url: Redacted.Redacted<string>; |
| 29 | /** |
| 30 | * IAM auth mode only: re-mints a fresh short-lived (15 minute) IAM |
| 31 | * database authentication token. |
| 32 | * |
| 33 | * Per-execution pools (Lambda invoke, Worker event) never need it — the |
| 34 | * `Connect` runtime effect re-runs on each execution and mints a fresh |
| 35 | * token as `password`/`url`. Long-lived serverful pools (ECS Task / |
| 36 | * ServerHost) should wire this into the driver's lazy-password hook |
| 37 | * (`postgres.js` `pass: () => ...`, `pg` `password: () => ...`) so every |
| 38 | * new physical connection authenticates with a fresh token instead of a |
| 39 | * token minted at pool construction. |
| 40 | */ |
| 41 | refreshPassword?: Effect.Effect< |
| 42 | Redacted.Redacted<string>, |
| 43 | Presign.PresignError |
| 44 | >; |
| 45 | } |
| 46 | |
| 47 | interface ConnectOptionsBase { |
| 48 | database?: string; |
| 49 | port?: number; |
| 50 | ssl?: boolean; |
| 51 | /** |
| 52 | * Subnets the host Function should be attached to so it can open a |
| 53 | * socket to the cluster/proxy. Wired through the host's `vpc` binding |
| 54 | * channel — equivalent to setting the Function's `vpc` prop manually. |
| 55 | */ |
| 56 | subnetIds?: Input<SubnetId[]>; |
| 57 | /** |
| 58 | * Security groups for the host Function's VPC attachment (see |
| 59 | * {@link ConnectOptionsBase.subnetIds}). The referenced groups must be |
| 60 | * allowed ingress on the database port by the cluster/proxy's security |
| 61 | * group. |
| 62 | */ |
| 63 | securityGroupIds?: Input<SecurityGroupId[]>; |
| 64 | } |
| 65 | |
| 66 | /** |
| 67 | * Default credential strategy: read `{ username, password }` from a |
| 68 | * Secrets Manager secret. The deploy half grants |
| 69 | * `secretsmanager:GetSecretValue` on the secret; the runtime half fetches |
| 70 | * and parses it. |
| 71 | */ |
| 72 | export interface SecretConnectOptions extends ConnectOptionsBase { |
| 73 | /** @default "secret" */ |
| 74 | auth?: "secret"; |
| 75 | secret: Secret; |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * IAM database authentication: the deploy half grants `rds-db:connect` on |
| 80 | * `arn:aws:rds-db:{region}:{account}:dbuser:{resourceId}/{username}`; the |
| 81 | * runtime half presigns a short-lived auth token as the password. Requires |
| 82 | * `enableIAMDatabaseAuthentication: true` on the cluster and a database |
| 83 | * user granted the `rds_iam` role (Postgres) / `AWSAuthenticationPlugin` |
| 84 | * (MySQL). TLS is mandatory — `ssl` is forced to `true`. |
| 85 | */ |
| 86 | export interface IamConnectOptions extends ConnectOptionsBase { |
| 87 | auth: "iam"; |
| 88 | /** Database user the IAM token authenticates as. */ |
| 89 | username: string; |
| 90 | } |
| 91 | |
| 92 | export type ConnectOptions = SecretConnectOptions | IamConnectOptions; |
| 93 | |
| 94 | /** |
| 95 | * Runtime binding that resolves connection settings for an Aurora cluster, |
| 96 | * proxy, or proxy endpoint using a Secrets Manager secret or IAM database |
| 97 | * authentication. |
| 98 | * |
| 99 | * Binding it yields an Effect (not a callable) that resolves a |
| 100 | * `ConnectionInfo` — host, port, credentials, and a ready-to-use `url` — |
| 101 | * fresh on every execution. No socket is opened; feed the result into your |
| 102 | * database driver. Provide the implementation with |
| 103 | * `Effect.provide(AWS.RDS.ConnectHttp)`. |
| 104 | * ### Connecting to a Database |
| 105 | * **Example:** Resolve Credentials from a Secret |
| 106 | * ```typescript |
| 107 | * export default MyFunction.make( |
| 108 | * { main: import.meta.url, functionUrl: true }, |
| 109 | * Effect.gen(function* () { |
| 110 | * const db = yield* AWS.RDS.Aurora("AppDb", { |
| 111 | * subnetIds: [subnetA.subnetId, subnetB.subnetId], |
| 112 | * securityGroupIds: [dbSecurityGroup.groupId], |
| 113 | * }); |
| 114 | * // init — bind the cluster + admin secret; grants |
| 115 | * // secretsmanager:GetSecretValue and (optionally) attaches the |
| 116 | * // function to the given subnets/security groups |
| 117 | * const connect = yield* AWS.RDS.Connect(db.cluster, { |
| 118 | * secret: db.secret, |
| 119 | * database: "app", |
| 120 | * subnetIds: [subnetA.subnetId, subnetB.subnetId], |
| 121 | * securityGroupIds: [appSecurityGroup.groupId], |
| 122 | * }); |
| 123 | * |
| 124 | * return { |
| 125 | * fetch: Effect.gen(function* () { |
| 126 | * // runtime — resolve host/port/credentials, hand `info.url` |
| 127 | * // (Redacted) to postgres.js / pg / drizzle |
| 128 | * const info = yield* connect; |
| 129 | * return yield* HttpServerResponse.json({ |
| 130 | * host: info.host, |
| 131 | * port: info.port, |
| 132 | * database: info.database, |
| 133 | * }); |
| 134 | * }).pipe(Effect.orDie), |
| 135 | * }; |
| 136 | * }).pipe(Effect.provide(AWS.RDS.ConnectHttp)), |
| 137 | * ); |
| 138 | * ``` |
| 139 | * |
| 140 | * **Example:** IAM Database Authentication |
| 141 | * ```typescript |
| 142 | * // init — grants rds-db:connect for the `app_iam` user; the runtime half |
| 143 | * // presigns a short-lived (15 minute) auth token as the password |
| 144 | * const connect = yield* AWS.RDS.Connect(db.cluster, { |
| 145 | * auth: "iam", |
| 146 | * username: "app_iam", |
| 147 | * database: "app", |
| 148 | * }); |
| 149 | * |
| 150 | * // runtime — long-lived pools should wire info.refreshPassword into the |
| 151 | * // driver's lazy-password hook so each new connection gets a fresh token |
| 152 | * const info = yield* connect; |
| 153 | * ``` |
| 154 | * |
| 155 | * @binding |
| 156 | */ |
| 157 | export interface Connect extends Binding.Service< |
| 158 | Connect, |
| 159 | "AWS.RDS.Connect", |
| 160 | ( |
| 161 | resource: ConnectResource, |
| 162 | options: ConnectOptions, |
| 163 | ) => Effect.Effect< |
| 164 | Effect.Effect< |
| 165 | ConnectionInfo, |
| 166 | secretsmanager.GetSecretValueError | Presign.PresignError, |
| 167 | RuntimeContext |
| 168 | > |
| 169 | > |
| 170 | > {} |
| 171 | export const Connect = Binding.Service<Connect>("AWS.RDS.Connect"); |