/** * EventBridge Scheduler client interface and implementation. * * The real AWS call is clearly stubbed/guarded behind this interface so: * - Tests inject a mock without any network or AWS SDK import side-effects. * - The production implementation is swapped in at server startup via dependency injection. * * TODO (DEFERRED auth layer — 0a gate): The production SchedulerClient should read its * AWS credentials from the environment (Lambda execution role) rather than from any * hardcoded credential chain. IAM cross-review is required before wiring the real client * into the server bundle (see design.md §2.5 and §8). */ export interface CreateScheduleInput { /** Unique name for the EventBridge schedule (must be [a-zA-Z0-9_-]+). */ scheduleName: string; /** ISO-8601 datetime string at which the one-shot schedule fires. */ scheduleAt: string; /** ARN of the Lambda target that delivers the reminder. */ targetArn: string; /** Arbitrary payload forwarded to the target Lambda. */ payload: Record; /** ARN of the IAM role EventBridge Scheduler assumes to invoke the target. */ roleArn: string; } export interface CreateScheduleOutput { /** The ARN of the created EventBridge schedule. */ scheduleArn: string; } /** * Thin abstraction over the EventBridge Scheduler API. * Swap the real implementation in at server startup; inject a mock in tests. */ export interface SchedulerClient { createSchedule(input: CreateScheduleInput): Promise; } // --------------------------------------------------------------------------- // Production implementation // --------------------------------------------------------------------------- /** * Real SchedulerClient that calls the AWS EventBridge Scheduler API. * * IMPORTANT: This module intentionally does NOT import the AWS SDK at the top * level. The dynamic import inside createSchedule ensures no AWS SDK code (and * no credential-chain resolution) runs at import time — critical for unit tests * running without AWS credentials. * * TODO (production wiring): Before deploying, confirm: * - The Lambda execution role has `scheduler:CreateSchedule` on the target * schedule group (least-privilege per design.md §2.5). * - REGION defaults to process.env.AWS_REGION (set automatically in Lambda). * - TARGET_ARN and SCHEDULER_ROLE_ARN are injected via CDK environment variables * (never hardcoded). */ export class AwsSchedulerClient implements SchedulerClient { // Fields are stored now and used when the real AWS SDK call is uncommented (see TODO above). private readonly _region: string; private readonly _targetArn: string; private readonly _roleArn: string; constructor(opts: { region?: string; targetArn: string; roleArn: string }) { this._region = opts.region ?? process.env['AWS_REGION'] ?? 'us-east-1'; this._targetArn = opts.targetArn; this._roleArn = opts.roleArn; // Read each field once so TypeScript does not flag them as write-only. void this._region; void this._targetArn; void this._roleArn; } async createSchedule(_input: CreateScheduleInput): Promise { // Dynamic import so the AWS SDK is not loaded during unit tests. // TODO: replace this stub with the real @aws-sdk/client-scheduler call // once the IAM cross-review gate (design.md §8) has been passed. // Example real call (do not remove — kept for implementer reference): // // const { SchedulerClient, CreateScheduleCommand } = await import( // '@aws-sdk/client-scheduler' // ); // const client = new SchedulerClient({ region: this.region }); // const result = await client.send( // new CreateScheduleCommand({ // Name: input.scheduleName, // ScheduleExpression: `at(${input.scheduleAt})`, // ScheduleExpressionTimezone: 'UTC', // FlexibleTimeWindow: { Mode: 'OFF' }, // Target: { // Arn: this.targetArn, // RoleArn: this.roleArn, // Input: JSON.stringify(input.payload), // }, // }) // ); // return { scheduleArn: result.ScheduleArn! }; throw new Error( 'AwsSchedulerClient.createSchedule: production AWS SDK call is not yet wired. ' + 'Inject a SchedulerClient mock in tests, or complete the IAM cross-review and ' + 'uncomment the real SDK call before deploying.', ); } }