1import type * as Credentials from "@distilled.cloud/aws/Credentials";
2import * as Presign from "@distilled.cloud/aws/Presign";
3import type * as Region from "@distilled.cloud/aws/Region";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Redacted from "effect/Redacted";
7
8export 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 */
52export 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});