| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Binding from "../../Binding.ts"; |
| 3 | import type { RotationRules } from "./RotationSchedule.ts"; |
| 4 | import type { Secret } from "./Secret.ts"; |
| 5 | |
| 6 | /** A step of the Secrets Manager rotation protocol. */ |
| 7 | export type RotationStep = |
| 8 | | "createSecret" |
| 9 | | "setSecret" |
| 10 | | "testSecret" |
| 11 | | "finishSecret"; |
| 12 | |
| 13 | /** |
| 14 | * The payload Secrets Manager sends the rotation function on each step of a |
| 15 | * rotation. |
| 16 | */ |
| 17 | export interface SecretRotationEvent { |
| 18 | /** Which step of the rotation protocol to perform. */ |
| 19 | Step: RotationStep; |
| 20 | /** ARN of the secret being rotated. */ |
| 21 | SecretId: string; |
| 22 | /** |
| 23 | * Request token that becomes the `VersionId` of the new secret version — |
| 24 | * pass it as `ClientRequestToken` to `PutSecretValue` and as |
| 25 | * `MoveToVersionId` to `UpdateSecretVersionStage`. |
| 26 | */ |
| 27 | ClientRequestToken: string; |
| 28 | } |
| 29 | |
| 30 | export interface RotationEventSourceProps { |
| 31 | /** |
| 32 | * When the rotation runs. Provide `scheduleExpression` or |
| 33 | * `automaticallyAfter`. |
| 34 | */ |
| 35 | rotationRules?: RotationRules; |
| 36 | /** |
| 37 | * Rotate the secret immediately at deploy time. If `false`, Secrets |
| 38 | * Manager only validates the configuration by invoking the `testSecret` |
| 39 | * step. |
| 40 | * @default false |
| 41 | */ |
| 42 | rotateImmediately?: boolean; |
| 43 | } |
| 44 | |
| 45 | export type RotationEventSourceService = <Req = never>( |
| 46 | secret: Secret, |
| 47 | props: RotationEventSourceProps, |
| 48 | process: (event: SecretRotationEvent) => Effect.Effect<void, never, Req>, |
| 49 | ) => Effect.Effect<void, never, never>; |
| 50 | |
| 51 | /** |
| 52 | * Register the current Lambda function as the rotation function for a |
| 53 | * Secrets Manager {@link Secret}. |
| 54 | * |
| 55 | * @param secret The secret to rotate. |
| 56 | * @param props Optional rotation schedule configuration. |
| 57 | * @param process The handler invoked once per rotation step (last argument). |
| 58 | * |
| 59 | * @example |
| 60 | * ```typescript |
| 61 | * yield* SecretsManager.onSecretRotation(secret, (event) => |
| 62 | * Effect.gen(function* () { |
| 63 | * switch (event.Step) { |
| 64 | * case "createSecret": // stage a new AWSPENDING value |
| 65 | * case "setSecret": // apply it to the downstream system |
| 66 | * case "testSecret": // verify the new credential works |
| 67 | * case "finishSecret": // move AWSCURRENT onto the new version |
| 68 | * } |
| 69 | * }).pipe(Effect.orDie), |
| 70 | * ); |
| 71 | * ``` |
| 72 | * |
| 73 | * @example With a rotation schedule |
| 74 | * ```typescript |
| 75 | * yield* SecretsManager.onSecretRotation( |
| 76 | * secret, |
| 77 | * { rotationRules: { automaticallyAfter: "30 days" } }, |
| 78 | * handleRotation, |
| 79 | * ); |
| 80 | * ``` |
| 81 | */ |
| 82 | export function onSecretRotation<S extends Secret, Req = never>( |
| 83 | secret: S, |
| 84 | process: (event: SecretRotationEvent) => Effect.Effect<void, never, Req>, |
| 85 | ): Effect.Effect<void, never, RotationEventSource>; |
| 86 | export function onSecretRotation<S extends Secret, Req = never>( |
| 87 | secret: S, |
| 88 | props: RotationEventSourceProps, |
| 89 | process: (event: SecretRotationEvent) => Effect.Effect<void, never, Req>, |
| 90 | ): Effect.Effect<void, never, RotationEventSource>; |
| 91 | export function onSecretRotation<S extends Secret, Req = never>( |
| 92 | secret: S, |
| 93 | propsOrProcess: |
| 94 | | RotationEventSourceProps |
| 95 | | ((event: SecretRotationEvent) => Effect.Effect<void, never, Req>), |
| 96 | maybeProcess?: ( |
| 97 | event: SecretRotationEvent, |
| 98 | ) => Effect.Effect<void, never, Req>, |
| 99 | ): Effect.Effect<void, never, RotationEventSource> { |
| 100 | const [props, process] = |
| 101 | typeof propsOrProcess === "function" |
| 102 | ? [{} as RotationEventSourceProps, propsOrProcess] |
| 103 | : [propsOrProcess, maybeProcess!]; |
| 104 | return RotationEventSource.use((source) => source(secret, props, process)); |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * Event source connecting a Secrets Manager {@link Secret}'s rotation to the |
| 109 | * hosting Lambda function. |
| 110 | * |
| 111 | * The contract is a `Binding.Service`; the Lambda implementation layer is |
| 112 | * `Lambda.SecretRotationEventSource` — at deploy time it grants Secrets |
| 113 | * Manager permission to invoke the function, attaches the rotation-protocol |
| 114 | * IAM actions for the secret, and provisions the |
| 115 | * {@link RotationSchedule}; at runtime it narrows incoming invocations to |
| 116 | * rotation events for the bound secret. Consume it through the |
| 117 | * {@link onSecretRotation} helper. |
| 118 | * ### Rotating Secrets |
| 119 | * **Example:** Rotation Function in a Lambda |
| 120 | * ```typescript |
| 121 | * export default RotationFunction.make( |
| 122 | * { main: import.meta.url }, |
| 123 | * Effect.gen(function* () { |
| 124 | * const secret = yield* SecretsManager.Secret("DbPassword", { |
| 125 | * secretString: Redacted.make("initial"), |
| 126 | * }); |
| 127 | * const getValue = yield* SecretsManager.GetSecretValue(secret); |
| 128 | * const putValue = yield* SecretsManager.PutSecretValue(secret); |
| 129 | * const updateStage = yield* SecretsManager.UpdateSecretVersionStage(secret); |
| 130 | * const describe = yield* SecretsManager.DescribeSecret(secret); |
| 131 | * const randomPassword = yield* SecretsManager.GetRandomPassword(); |
| 132 | * |
| 133 | * yield* SecretsManager.onSecretRotation( |
| 134 | * secret, |
| 135 | * { rotationRules: { automaticallyAfter: "30 days" } }, |
| 136 | * (event) => rotate(event).pipe(Effect.orDie), |
| 137 | * ); |
| 138 | * }).pipe(Effect.provide(Lambda.SecretRotationEventSource)), |
| 139 | * ); |
| 140 | * ``` |
| 141 | * |
| 142 | * @binding |
| 143 | */ |
| 144 | export interface RotationEventSource extends Binding.Service< |
| 145 | RotationEventSource, |
| 146 | "AWS.SecretsManager.RotationEventSource", |
| 147 | RotationEventSourceService |
| 148 | > {} |
| 149 | |
| 150 | export const RotationEventSource = Binding.Service<RotationEventSource>( |
| 151 | "AWS.SecretsManager.RotationEventSource", |
| 152 | ); |