forklaunch 1.35.0

Launch faster with forklaunch
//! `voice`: outbound calls through the platform's Amazon Connect instance.
//!
//! The service gets a `VoiceClient` built by `createVoiceClient()` from the
//! managed env contract (no AWS credential, no Connect ids), and a handler
//! for the call events the platform relays (`voice.call.started`,
//! `voice.call.ended`, `voice.recording.ready`).

use anyhow::Result;

use super::{Capability, CapabilityEdit};

pub(crate) static VOICE: Capability = Capability {
    id: "voice",
    resource_type: "voice",
    summary: "outbound phone calls (Amazon Connect, held by the platform)",
    registration_key: "VoiceClient",
    receives_events: true,
    workers: true,
    add,
    remove,
};

const IMPORT_SOURCE: &str = "@forklaunch/core/http";
#[cfg(test)]
const IMPORT_NAMES: &[&str] = &["createVoiceClient", "VoiceClient"];
const IMPORT_LINE: &str = "import { createVoiceClient, VoiceClient } from \"@forklaunch/core/http\";";

const RUNTIME_BLOCK: &str = "const configInjector = createConfigInjector(SchemaValidator(), {
    VoiceClient: {
        lifetime: Lifetime.Scoped,
        type: VoiceClient,
        factory: ({ PLATFORM_GATEWAY_URL, INSTANCE_ID, INSTANCE_HMAC_KEY }) =>
            createVoiceClient({
                gatewayUrl: PLATFORM_GATEWAY_URL,
                instanceId: INSTANCE_ID,
                hmacKey: INSTANCE_HMAC_KEY
            })
    }
});";

const HANDLER_STUB: &str = "import type {
  PlatformEvent,
  VoiceCallEndedEvent,
  VoiceCallStartedEvent,
  VoiceRecordingReadyEvent
} from '@forklaunch/core/http';

/**
 * Voice call events from the ForkLaunch platform (Amazon Connect), already
 * verified as signed with this instance's key. Generated by
 * `forklaunch infra add <service> voice`; fill in what the app does.
 *
 * Deliveries can repeat: dedupe on event.id. Throwing makes the platform
 * retry. Place calls with `ci.resolve(tokens.VoiceClient).startOutboundCall(
 * { to, flow, attributes })`; never put health information in attributes.
 */
export async function handle(event: PlatformEvent): Promise<void> {
  switch (event.type) {
    case 'voice.call.started': {
      const { callId } = event.data as unknown as VoiceCallStartedEvent;
      // The call is ringing. Record callId against what it is about.
      void callId;
      return;
    }
    case 'voice.call.ended': {
      const { callId, durationSeconds, disconnectReason } =
        event.data as unknown as VoiceCallEndedEvent;
      // disconnectReason: customer, api, busy, no_answer, … Retry or follow up here.
      void [callId, durationSeconds, disconnectReason];
      return;
    }
    case 'voice.recording.ready': {
      const { callId, recordingKey } =
        event.data as unknown as VoiceRecordingReadyEvent;
      // recordingKey is a key in this instance's object store.
      void [callId, recordingKey];
      return;
    }
    default:
      return;
  }
}
";

fn add(edit: &mut CapabilityEdit) -> Result<()> {
    // registrations.ts already imports OpenTelemetryCollector from
    // @forklaunch/core/http; inject_registration merges into that import.
    edit.inject_registration(IMPORT_LINE, IMPORT_SOURCE, None, Some(RUNTIME_BLOCK))?;
    edit.ensure_platform_events("voice", HANDLER_STUB)?;
    Ok(())
}

fn remove(edit: &mut CapabilityEdit) -> Result<()> {
    // createVoiceClient and VoiceClient go with the registration.
    edit.remove_registration(None, &[], &["VoiceClient"])
}

#[cfg(test)]
mod tests;