1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import type { RotationRules } from "./RotationSchedule.ts";
4import type { Secret } from "./Secret.ts";
5
6/** A step of the Secrets Manager rotation protocol. */
7export 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 */
17export 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
30export 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
45export 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 */
82export 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>;
86export 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>;
91export 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 */
144export interface RotationEventSource extends Binding.Service<
145 RotationEventSource,
146 "AWS.SecretsManager.RotationEventSource",
147 RotationEventSourceService
148> {}
149
150export const RotationEventSource = Binding.Service<RotationEventSource>(
151 "AWS.SecretsManager.RotationEventSource",
152);